diff --git a/LICENSE b/LICENSE index 2bb9ad2..4947287 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,4 @@ + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ diff --git a/bfxapi/__init__.py b/bfxapi/__init__.py index c11c9ab..4fbdfd6 100644 --- a/bfxapi/__init__.py +++ b/bfxapi/__init__.py @@ -1 +1,6 @@ -from .client import Client, Constants \ No newline at end of file +from .client import Client + +from .urls import REST_HOST, PUB_REST_HOST, STAGING_REST_HOST, \ + WSS_HOST, PUB_WSS_HOST, STAGING_WSS_HOST + +NAME = "bfxapi" \ No newline at end of file diff --git a/bfxapi/client.py b/bfxapi/client.py index e866235..f3121ac 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -1,35 +1,31 @@ from .rest import BfxRestInterface from .websocket import BfxWebsocketClient +from .urls import REST_HOST, WSS_HOST -from typing import Optional - -from enum import Enum - -class Constants(str, Enum): - REST_HOST = "https://api.bitfinex.com/v2" - PUB_REST_HOST = "https://api-pub.bitfinex.com/v2" - - WSS_HOST = "wss://api.bitfinex.com/ws/2" - PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2" +from typing import List, Optional class Client(object): def __init__( self, - REST_HOST: str = Constants.REST_HOST, - WSS_HOST: str = Constants.WSS_HOST, + REST_HOST: str = REST_HOST, + WSS_HOST: str = WSS_HOST, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None, - log_level: str = "WARNING" + filter: Optional[List[str]] = None, + log_level: str = "INFO" ): + credentials = None + + if API_KEY and API_SECRET: + credentials = { "API_KEY": API_KEY, "API_SECRET": API_SECRET, "filter": filter } + self.rest = BfxRestInterface( host=REST_HOST, - API_KEY=API_KEY, - API_SECRET=API_SECRET + credentials=credentials ) self.wss = BfxWebsocketClient( host=WSS_HOST, - API_KEY=API_KEY, - API_SECRET=API_SECRET, + credentials=credentials, log_level=log_level ) \ No newline at end of file diff --git a/bfxapi/exceptions.py b/bfxapi/exceptions.py index 1033837..d876946 100644 --- a/bfxapi/exceptions.py +++ b/bfxapi/exceptions.py @@ -1,9 +1,7 @@ __all__ = [ "BfxBaseException", - + "LabelerSerializerException", - "IntegerUnderflowError", - "IntegerOverflowflowError" ] class BfxBaseException(Exception): @@ -18,18 +16,4 @@ class LabelerSerializerException(BfxBaseException): This exception indicates an error thrown by the _Serializer class in bfxapi/labeler.py. """ - pass - -class IntegerUnderflowError(BfxBaseException): - """ - This error indicates an underflow in one of the integer types defined in bfxapi/utils/integers.py. - """ - - pass - -class IntegerOverflowflowError(BfxBaseException): - """ - This error indicates an overflow in one of the integer types defined in bfxapi/utils/integers.py. - """ - pass \ No newline at end of file diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 4575146..213752c 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -1,22 +1,77 @@ from .exceptions import LabelerSerializerException -from typing import Generic, TypeVar, Iterable, Optional, List, Tuple, Any, cast +from typing import Type, Generic, TypeVar, Iterable, Optional, Dict, List, Tuple, Any, cast -T = TypeVar("T") +T = TypeVar("T", bound="_Type") + +def compose(*decorators): + def wrapper(function): + for decorator in reversed(decorators): + function = decorator(function) + return function + + return wrapper + +def partial(cls): + def __init__(self, **kwargs): + for annotation in self.__annotations__.keys(): + if annotation not in kwargs: + self.__setattr__(annotation, None) + else: self.__setattr__(annotation, kwargs[annotation]) + + kwargs.pop(annotation, None) + + if len(kwargs) != 0: + raise TypeError(f"{cls.__name__}.__init__() got an unexpected keyword argument '{list(kwargs.keys())[0]}'") + + cls.__init__ = __init__ + + return cls + +class _Type(object): + """ + Base class for any dataclass serializable by the _Serializer generic class. + """ + + pass class _Serializer(Generic[T]): - def __init__(self, name: str, labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]): - self.name, self.__labels, self.__IGNORE = name, labels, IGNORE + def __init__(self, name: str, klass: Type[_Type], labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]): + self.name, self.klass, self.__labels, self.__IGNORE = name, klass, labels, IGNORE def _serialize(self, *args: Any, skip: Optional[List[str]] = None) -> Iterable[Tuple[str, Any]]: labels = list(filter(lambda label: label not in (skip or list()), self.__labels)) if len(labels) > len(args): - raise LabelerSerializerException(" and <*args> arguments should contain the same amount of elements.") + raise LabelerSerializerException(f"{self.name} -> and <*args> arguments should contain the same amount of elements.") for index, label in enumerate(labels): if label not in self.__IGNORE: yield label, args[index] def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: - return cast(T, dict(self._serialize(*values, skip=skip))) \ No newline at end of file + return cast(T, self.klass(**dict(self._serialize(*values, skip=skip)))) + + def get_labels(self) -> List[str]: + return [ label for label in self.__labels if label not in self.__IGNORE ] + +class _RecursiveSerializer(_Serializer, Generic[T]): + def __init__(self, name: str, klass: Type[_Type], labels: List[str], serializers: Dict[str, _Serializer[Any]], IGNORE: List[str] = ["_PLACEHOLDER"]): + super().__init__(name, klass, labels, IGNORE) + + self.serializers = serializers + + def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: + serialization = dict(self._serialize(*values, skip=skip)) + + for key in serialization: + if key in self.serializers.keys(): + serialization[key] = self.serializers[key].parse(*serialization[key], skip=skip) + + return cast(T, self.klass(**serialization)) + +def generate_labeler_serializer(name: str, klass: Type[T], labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> _Serializer[T]: + return _Serializer[T](name, klass, labels, IGNORE) + +def generate_recursive_serializer(name: str, klass: Type[T], labels: List[str], serializers: Dict[str, _Serializer[Any]], IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> _RecursiveSerializer[T]: + return _RecursiveSerializer[T](name, klass, labels, serializers, IGNORE) \ No newline at end of file diff --git a/bfxapi/notification.py b/bfxapi/notification.py index 90d2f12..bf4818a 100644 --- a/bfxapi/notification.py +++ b/bfxapi/notification.py @@ -1,35 +1,38 @@ -from typing import List, Dict, Union, Optional, Any, TypedDict, cast +from typing import List, Dict, Union, Optional, Any, TypedDict, Generic, TypeVar, cast +from dataclasses import dataclass +from .labeler import _Type, _Serializer -from .labeler import _Serializer +T = TypeVar("T") -class Notification(TypedDict): - MTS: int - TYPE: str - MESSAGE_ID: Optional[int] - NOTIFY_INFO: Union[Dict[str, Any], List[Dict[str, Any]]] - CODE: Optional[int] - STATUS: str - TEXT: str +@dataclass +class Notification(_Type, Generic[T]): + mts: int + type: str + message_id: Optional[int] + notify_info: T + code: Optional[int] + status: str + text: str -class _Notification(_Serializer): - __LABELS = [ "MTS", "TYPE", "MESSAGE_ID", "_PLACEHOLDER", "NOTIFY_INFO", "CODE", "STATUS", "TEXT" ] +class _Notification(_Serializer, Generic[T]): + __LABELS = [ "mts", "type", "message_id", "_PLACEHOLDER", "notify_info", "code", "status", "text" ] - def __init__(self, serializer: Optional[_Serializer] = None, iterate: bool = False): - super().__init__("Notification", _Notification.__LABELS, IGNORE = [ "_PLACEHOLDER" ]) + def __init__(self, serializer: Optional[_Serializer] = None, is_iterable: bool = False): + super().__init__("Notification", Notification, _Notification.__LABELS, IGNORE = [ "_PLACEHOLDER" ]) - self.serializer, self.iterate = serializer, iterate + self.serializer, self.is_iterable = serializer, is_iterable - def parse(self, *values: Any, skip: Optional[List[str]] = None) -> Notification: - notification = dict(self._serialize(*values)) + def parse(self, *values: Any, skip: Optional[List[str]] = None) -> Notification[T]: + notification = cast(Notification[T], Notification(**dict(self._serialize(*values)))) if isinstance(self.serializer, _Serializer): - if self.iterate == False: - NOTIFY_INFO = notification["NOTIFY_INFO"] + NOTIFY_INFO = cast(List[Any], notification.notify_info) + if self.is_iterable == False: if len(NOTIFY_INFO) == 1 and isinstance(NOTIFY_INFO[0], list): NOTIFY_INFO = NOTIFY_INFO[0] - notification["NOTIFY_INFO"] = dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip)) - else: notification["NOTIFY_INFO"] = [ dict(self.serializer._serialize(*data, skip=skip)) for data in notification["NOTIFY_INFO"] ] + notification.notify_info = cast(T, self.serializer.klass(**dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip)))) + else: notification.notify_info = cast(T, [ self.serializer.klass(**dict(self.serializer._serialize(*data, skip=skip))) for data in NOTIFY_INFO ]) - return cast(Notification, notification) \ No newline at end of file + return notification \ No newline at end of file diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py deleted file mode 100644 index 3a02593..0000000 --- a/bfxapi/rest/BfxRestInterface.py +++ /dev/null @@ -1,398 +0,0 @@ -import time, hmac, hashlib, json, requests - -from decimal import Decimal -from datetime import datetime -from http import HTTPStatus - -from typing import List, Union, Literal, Optional, Any, cast - -from . import serializers - -from .typings import * -from .enums import Config, Sort, OrderType, FundingOfferType, Error -from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError - -from .. utils.encoder import JSONEncoder - -class BfxRestInterface(object): - def __init__(self, host, API_KEY = None, API_SECRET = None): - self.public = _RestPublicEndpoints(host=host) - - self.auth = _RestAuthenticatedEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET) - -class _Requests(object): - def __init__(self, host, API_KEY = None, API_SECRET = None): - self.host, self.API_KEY, self.API_SECRET = host, API_KEY, API_SECRET - - def __build_authentication_headers(self, endpoint, data): - nonce = str(int(time.time()) * 1000) - - path = f"/api/v2/{endpoint}{nonce}" - - if data != None: path += data - - signature = hmac.new( - self.API_SECRET.encode("utf8"), - path.encode("utf8"), - hashlib.sha384 - ).hexdigest() - - return { - "bfx-nonce": nonce, - "bfx-signature": signature, - "bfx-apikey": self.API_KEY - } - - def _GET(self, endpoint, params = None): - response = requests.get(f"{self.host}/{endpoint}", params=params) - - if response.status_code == HTTPStatus.NOT_FOUND: - raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") - - data = response.json() - - if len(data) and data[0] == "error": - if data[1] == Error.ERR_PARAMS: - raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") - - if data[1] == None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC: - raise UnknownGenericError("The server replied to the request with a generic error with message: <{data[2]}>.") - - return data - - def _POST(self, endpoint, params = None, data = None, _append_authentication_headers = True): - headers = { "Content-Type": "application/json" } - - if isinstance(data, dict): - data = json.dumps({ key: value for key, value in data.items() if value != None}, cls=JSONEncoder) - - if _append_authentication_headers: - headers = { **headers, **self.__build_authentication_headers(endpoint, data) } - - response = requests.post(f"{self.host}/{endpoint}", params=params, data=data, headers=headers) - - if response.status_code == HTTPStatus.NOT_FOUND: - raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") - - data = response.json() - - if len(data) and data[0] == "error": - if data[1] == Error.ERR_PARAMS: - raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") - - if data[1] == Error.ERR_AUTH_FAIL: - raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.") - - if data[1] == None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC: - raise UnknownGenericError(f"The server replied to the request with a generic error with message: <{data[2]}>.") - - return data - -class _RestPublicEndpoints(_Requests): - def get_platform_status(self) -> PlatformStatus: - return serializers.PlatformStatus.parse(*self._GET("platform/status")) - - def get_tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]: - data = self._GET("tickers", params={ "symbols": ",".join(symbols) }) - - parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse } - - return [ parsers[subdata[0][0]](*subdata) for subdata in data ] - - def get_t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]: - if isinstance(pairs, str) and pairs == "ALL": - return [ cast(TradingPairTicker, subdata) for subdata in self.get_tickers([ "ALL" ]) if cast(str, subdata["SYMBOL"]).startswith("t") ] - - data = self.get_tickers([ "t" + pair for pair in pairs ]) - - return cast(List[TradingPairTicker], data) - - def get_f_tickers(self, currencies: Union[List[str], Literal["ALL"]]) -> List[FundingCurrencyTicker]: - if isinstance(currencies, str) and currencies == "ALL": - return [ cast(FundingCurrencyTicker, subdata) for subdata in self.get_tickers([ "ALL" ]) if cast(str, subdata["SYMBOL"]).startswith("f") ] - - data = self.get_tickers([ "f" + currency for currency in currencies ]) - - return cast(List[FundingCurrencyTicker], data) - - def get_t_ticker(self, pair: str) -> TradingPairTicker: - return serializers.TradingPairTicker.parse(*self._GET(f"ticker/t{pair}"), skip=["SYMBOL"]) - - def get_f_ticker(self, currency: str) -> FundingCurrencyTicker: - return serializers.FundingCurrencyTicker.parse(*self._GET(f"ticker/f{currency}"), skip=["SYMBOL"]) - - def get_tickers_history(self, symbols: List[str], start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[TickersHistory]: - params = { - "symbols": ",".join(symbols), - "start": start, "end": end, - "limit": limit - } - - data = self._GET("tickers/hist", params=params) - - return [ serializers.TickersHistory.parse(*subdata) for subdata in data ] - - def get_t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[TradingPairTrade]: - params = { "limit": limit, "start": start, "end": end, "sort": sort } - data = self._GET(f"trades/{'t' + pair}/hist", params=params) - return [ serializers.TradingPairTrade.parse(*subdata) for subdata in data ] - - def get_f_trades(self, currency: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[FundingCurrencyTrade]: - params = { "limit": limit, "start": start, "end": end, "sort": sort } - data = self._GET(f"trades/{'f' + currency}/hist", params=params) - return [ serializers.FundingCurrencyTrade.parse(*subdata) for subdata in data ] - - def get_t_book(self, pair: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairBook]: - return [ serializers.TradingPairBook.parse(*subdata) for subdata in self._GET(f"book/{'t' + pair}/{precision}", params={ "len": len }) ] - - def get_f_book(self, currency: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyBook]: - return [ serializers.FundingCurrencyBook.parse(*subdata) for subdata in self._GET(f"book/{'f' + currency}/{precision}", params={ "len": len }) ] - - def get_t_raw_book(self, pair: str, len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairRawBook]: - return [ serializers.TradingPairRawBook.parse(*subdata) for subdata in self._GET(f"book/{'t' + pair}/R0", params={ "len": len }) ] - - def get_f_raw_book(self, currency: str, len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyRawBook]: - return [ serializers.FundingCurrencyRawBook.parse(*subdata) for subdata in self._GET(f"book/{'f' + currency}/R0", params={ "len": len }) ] - - def get_stats_hist( - self, - resource: str, - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> List[Statistic]: - params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"stats1/{resource}/hist", params=params) - return [ serializers.Statistic.parse(*subdata) for subdata in data ] - - def get_stats_last( - self, - resource: str, - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Statistic: - params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"stats1/{resource}/last", params=params) - return serializers.Statistic.parse(*data) - - def get_candles_hist( - self, - resource: str, - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> List[Candle]: - params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"candles/{resource}/hist", params=params) - return [ serializers.Candle.parse(*subdata) for subdata in data ] - - def get_candles_last( - self, - resource: str, - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Candle: - params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"candles/{resource}/last", params=params) - return serializers.Candle.parse(*data) - - def get_derivatives_status(self, type: str, keys: List[str]) -> List[DerivativesStatus]: - params = { "keys": ",".join(keys) } - - data = self._GET(f"status/{type}", params=params) - - return [ serializers.DerivativesStatus.parse(*subdata) for subdata in data ] - - def get_derivatives_status_history( - self, - type: str, symbol: str, - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> List[DerivativesStatus]: - params = { "sort": sort, "start": start, "end": end, "limit": limit } - - data = self._GET(f"status/{type}/{symbol}/hist", params=params) - - return [ serializers.DerivativesStatus.parse(*subdata, skip=[ "KEY" ]) for subdata in data ] - - def get_liquidations(self, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Liquidation]: - params = { "sort": sort, "start": start, "end": end, "limit": limit } - - data = self._GET("liquidations/hist", params=params) - - return [ serializers.Liquidation.parse(*subdata[0]) for subdata in data ] - - def get_leaderboards_hist( - self, - resource: str, - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> List[Leaderboard]: - params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"rankings/{resource}/hist", params=params) - return [ serializers.Leaderboard.parse(*subdata) for subdata in data ] - - def get_leaderboards_last( - self, - resource: str, - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Leaderboard: - params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"rankings/{resource}/last", params=params) - return serializers.Leaderboard.parse(*data) - - def get_funding_stats(self, symbol: str, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingStatistic]: - params = { "start": start, "end": end, "limit": limit } - - data = self._GET(f"funding/stats/{symbol}/hist", params=params) - - return [ serializers.FundingStatistic.parse(*subdata) for subdata in data ] - - def conf(self, config: Config) -> Any: - return self._GET(f"conf/{config}")[0] - -class _RestAuthenticatedEndpoints(_Requests): - def get_wallets(self) -> List[Wallet]: - return [ serializers.Wallet.parse(*subdata) for subdata in self._POST("auth/r/wallets") ] - - def get_orders(self, symbol: Optional[str] = None, ids: Optional[List[str]] = None) -> List[Order]: - endpoint = "auth/r/orders" - - if symbol != None: - endpoint += f"/{symbol}" - - return [ serializers.Order.parse(*subdata) for subdata in self._POST(endpoint, data={ "id": ids }) ] - - def get_positions(self) -> List[Position]: - return [ serializers.Position.parse(*subdata) for subdata in self._POST("auth/r/positions") ] - - def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str], - price: Optional[Union[Decimal, str]] = None, lev: Optional[int] = None, - price_trailing: Optional[Union[Decimal, str]] = None, price_aux_limit: Optional[Union[Decimal, str]] = None, price_oco_stop: Optional[Union[Decimal, str]] = None, - gid: Optional[int] = None, cid: Optional[int] = None, - flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification: - data = { - "type": type, "symbol": symbol, "amount": amount, - "price": price, "lev": lev, - "price_trailing": price_trailing, "price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop, - "gid": gid, "cid": cid, - "flags": flags, "tif": tif, "meta": meta - } - - return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/submit", data=data)) - - def update_order(self, id: int, amount: Optional[Union[Decimal, str]] = None, price: Optional[Union[Decimal, str]] = None, - cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, - flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, str]] = None, - price_aux_limit: Optional[Union[Decimal, str]] = None, price_trailing: Optional[Union[Decimal, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification: - data = { - "id": id, "amount": amount, "price": price, - "cid": cid, "cid_date": cid_date, "gid": gid, - "flags": flags, "lev": lev, "delta": delta, - "price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif - } - - return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/update", data=data)) - - def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None) -> Notification: - data = { - "id": id, - "cid": cid, - "cid_date": cid_date - } - - return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/cancel", data=data)) - - def cancel_order_multi(self, ids: Optional[List[int]] = None, cids: Optional[List[Tuple[int, str]]] = None, gids: Optional[List[int]] = None, all: bool = False) -> Notification: - data = { - "ids": ids, - "cids": cids, - "gids": gids, - - "all": int(all) - } - - return serializers._Notification(serializer=serializers.Order, iterate=True).parse(*self._POST("auth/w/order/cancel/multi", data=data)) - - def get_orders_history(self, symbol: Optional[str] = None, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Order]: - if symbol == None: - endpoint = "auth/r/orders/hist" - else: endpoint = f"auth/r/orders/{symbol}/hist" - - data = { - "id": ids, - "start": start, "end": end, - "limit": limit - } - - return [ serializers.Order.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] - - def get_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Trade]: - if symbol == None: - endpoint = "auth/r/trades/hist" - else: endpoint = f"auth/r/trades/{symbol}/hist" - - data = { - "sort": sort, - "start": start, "end": end, - "limit": limit - } - - return [ serializers.Trade.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] - - def get_order_trades(self, symbol: str, id: int) -> List[OrderTrade]: - return [ serializers.OrderTrade.parse(*subdata) for subdata in self._POST(f"auth/r/order/{symbol}:{id}/trades") ] - - def get_ledgers(self, currency: str, category: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Ledger]: - data = { - "category": category, - "start": start, "end": end, - "limit": limit - } - - return [ serializers.Ledger.parse(*subdata) for subdata in self._POST(f"auth/r/ledgers/{currency}/hist", data=data) ] - - def get_funding_offers(self, symbol: Optional[str] = None) -> List[FundingOffer]: - endpoint = "auth/r/funding/offers" - - if symbol != None: - endpoint += f"/{symbol}" - - return [ serializers.FundingOffer.parse(*subdata) for subdata in self._POST(endpoint) ] - - def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, str], - rate: Union[Decimal, str], period: int, - flags: Optional[int] = 0) -> Notification: - data = { - "type": type, "symbol": symbol, "amount": amount, - "rate": rate, "period": period, - "flags": flags - } - - return serializers._Notification(serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/submit", data=data)) - - def cancel_funding_offer(self, id: int) -> Notification: - return serializers._Notification(serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/cancel", data={ "id": id })) - - def get_funding_offers_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingOffer]: - if symbol == None: - endpoint = "auth/r/funding/offers/hist" - else: endpoint = f"auth/r/funding/offers/{symbol}/hist" - - data = { - "start": start, "end": end, - "limit": limit - } - - return [ serializers.FundingOffer.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] - - def get_funding_credits(self, symbol: Optional[str] = None) -> List[FundingCredit]: - if symbol == None: - endpoint = "auth/r/funding/credits" - else: endpoint = f"auth/r/funding/credits/{symbol}" - - return [ serializers.FundingCredit.parse(*subdata) for subdata in self._POST(endpoint) ] - - def get_funding_credits_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingCredit]: - if symbol == None: - endpoint = "auth/r/funding/credits/hist" - else: endpoint = f"auth/r/funding/credits/{symbol}/hist" - - data = { - "start": start, "end": end, - "limit": limit - } - - return [ serializers.FundingCredit.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] \ No newline at end of file diff --git a/bfxapi/rest/__init__.py b/bfxapi/rest/__init__.py index 0bf3d2e..71e3b54 100644 --- a/bfxapi/rest/__init__.py +++ b/bfxapi/rest/__init__.py @@ -1 +1,4 @@ -from .BfxRestInterface import BfxRestInterface \ No newline at end of file +from .endpoints import BfxRestInterface, RestPublicEndpoints, RestAuthenticatedEndpoints, \ + RestMerchantEndpoints + +NAME = "rest" \ No newline at end of file diff --git a/bfxapi/rest/endpoints/__init__.py b/bfxapi/rest/endpoints/__init__.py new file mode 100644 index 0000000..e35d6fb --- /dev/null +++ b/bfxapi/rest/endpoints/__init__.py @@ -0,0 +1,7 @@ +from .bfx_rest_interface import BfxRestInterface + +from .rest_public_endpoints import RestPublicEndpoints +from .rest_authenticated_endpoints import RestAuthenticatedEndpoints +from .rest_merchant_endpoints import RestMerchantEndpoints + +NAME = "endpoints" \ No newline at end of file diff --git a/bfxapi/rest/endpoints/bfx_rest_interface.py b/bfxapi/rest/endpoints/bfx_rest_interface.py new file mode 100644 index 0000000..b117fa6 --- /dev/null +++ b/bfxapi/rest/endpoints/bfx_rest_interface.py @@ -0,0 +1,16 @@ +from typing import Optional + +from .rest_public_endpoints import RestPublicEndpoints +from .rest_authenticated_endpoints import RestAuthenticatedEndpoints +from .rest_merchant_endpoints import RestMerchantEndpoints + +class BfxRestInterface(object): + VERSION = 2 + + def __init__(self, host, credentials = None): + API_KEY, API_SECRET = credentials and \ + (credentials["API_KEY"], credentials["API_SECRET"]) or (None, None) + + self.public = RestPublicEndpoints(host=host) + self.auth = RestAuthenticatedEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET) + self.merchant = RestMerchantEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET) \ No newline at end of file diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py new file mode 100644 index 0000000..7b4e11e --- /dev/null +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -0,0 +1,321 @@ +from typing import List, Tuple, Union, Literal, Optional +from decimal import Decimal +from datetime import datetime + +from .. types import * + +from .. import serializers +from .. enums import Sort, OrderType, FundingOfferType +from .. middleware import Middleware + +class RestAuthenticatedEndpoints(Middleware): + def get_user_info(self) -> UserInfo: + return serializers.UserInfo.parse(*self._POST(f"auth/r/info/user")) + + def get_login_history(self) -> List[LoginHistory]: + return [ serializers.LoginHistory.parse(*sub_data) for sub_data in self._POST("auth/r/logins/hist") ] + + def get_balance_available_for_orders_or_offers(self, symbol: str, type: str, dir: Optional[int] = None, rate: Optional[str] = None, lev: Optional[str] = None) -> BalanceAvailable: + return serializers.BalanceAvailable.parse(*self._POST("auth/calc/order/avail", body={ + "symbol": symbol, "type": type, "dir": dir, + "rate": rate, "lev": lev + })) + + def get_wallets(self) -> List[Wallet]: + return [ serializers.Wallet.parse(*sub_data) for sub_data in self._POST("auth/r/wallets") ] + + def get_orders(self, symbol: Optional[str] = None, ids: Optional[List[str]] = None) -> List[Order]: + endpoint = "auth/r/orders" + + if symbol != None: + endpoint += f"/{symbol}" + + return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, body={ "id": ids }) ] + + def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, float, str], + price: Optional[Union[Decimal, float, str]] = None, lev: Optional[int] = None, + price_trailing: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_oco_stop: Optional[Union[Decimal, float, str]] = None, + gid: Optional[int] = None, cid: Optional[int] = None, + flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification[Order]: + body = { + "type": type, "symbol": symbol, "amount": amount, + "price": price, "lev": lev, + "price_trailing": price_trailing, "price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop, + "gid": gid, "cid": cid, + "flags": flags, "tif": tif, "meta": meta + } + + return serializers._Notification[Order](serializers.Order).parse(*self._POST("auth/w/order/submit", body=body)) + + def update_order(self, id: int, amount: Optional[Union[Decimal, float, str]] = None, price: Optional[Union[Decimal, float, str]] = None, + cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, + flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, float, str]] = None, + price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification[Order]: + body = { + "id": id, "amount": amount, "price": price, + "cid": cid, "cid_date": cid_date, "gid": gid, + "flags": flags, "lev": lev, "delta": delta, + "price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif + } + + return serializers._Notification[Order](serializers.Order).parse(*self._POST("auth/w/order/update", body=body)) + + def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None) -> Notification[Order]: + body = { + "id": id, + "cid": cid, + "cid_date": cid_date + } + + return serializers._Notification[Order](serializers.Order).parse(*self._POST("auth/w/order/cancel", body=body)) + + def cancel_order_multi(self, ids: Optional[List[int]] = None, cids: Optional[List[Tuple[int, str]]] = None, gids: Optional[List[int]] = None, all: bool = False) -> Notification[List[Order]]: + body = { + "ids": ids, + "cids": cids, + "gids": gids, + + "all": int(all) + } + + return serializers._Notification[List[Order]](serializers.Order, is_iterable=True).parse(*self._POST("auth/w/order/cancel/multi", body=body)) + + def get_orders_history(self, symbol: Optional[str] = None, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Order]: + if symbol == None: + endpoint = "auth/r/orders/hist" + else: endpoint = f"auth/r/orders/{symbol}/hist" + + body = { + "id": ids, + "start": start, "end": end, + "limit": limit + } + + return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] + + def get_order_trades(self, symbol: str, id: int) -> List[OrderTrade]: + return [ serializers.OrderTrade.parse(*sub_data) for sub_data in self._POST(f"auth/r/order/{symbol}:{id}/trades") ] + + def get_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Trade]: + if symbol == None: + endpoint = "auth/r/trades/hist" + else: endpoint = f"auth/r/trades/{symbol}/hist" + + body = { + "sort": sort, + "start": start, "end": end, + "limit": limit + } + + return [ serializers.Trade.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] + + def get_ledgers(self, currency: str, category: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Ledger]: + body = { + "category": category, + "start": start, "end": end, + "limit": limit + } + + return [ serializers.Ledger.parse(*sub_data) for sub_data in self._POST(f"auth/r/ledgers/{currency}/hist", body=body) ] + + def get_base_margin_info(self) -> BaseMarginInfo: + return serializers.BaseMarginInfo.parse(*(self._POST(f"auth/r/info/margin/base")[1])) + + def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo: + response = self._POST(f"auth/r/info/margin/{symbol}") + data = [response[1]] + response[2] + return serializers.SymbolMarginInfo.parse(*data) + + def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]: + return [ serializers.SymbolMarginInfo.parse(*([sub_data[1]] + sub_data[2])) for sub_data in self._POST(f"auth/r/info/margin/sym_all") ] + + def get_positions(self) -> List[Position]: + return [ serializers.Position.parse(*sub_data) for sub_data in self._POST("auth/r/positions") ] + + def claim_position(self, id: int, amount: Optional[Union[Decimal, float, str]] = None) -> Notification[PositionClaim]: + return serializers._Notification[PositionClaim](serializers.PositionClaim).parse( + *self._POST("auth/w/position/claim", body={ "id": id, "amount": amount }) + ) + + def increase_position(self, symbol: str, amount: Union[Decimal, float, str]) -> Notification[PositionIncrease]: + return serializers._Notification[PositionIncrease](serializers.PositionIncrease).parse( + *self._POST("auth/w/position/increase", body={ "symbol": symbol, "amount": amount }) + ) + + def get_increase_position_info(self, symbol: str, amount: Union[Decimal, float, str]) -> PositionIncreaseInfo: + response = self._POST(f"auth/r/position/increase/info", body={ "symbol": symbol, "amount": amount }) + data = response[0] + [response[1][0]] + response[1][1] + [response[1][2]] + response[4] + response[5] + return serializers.PositionIncreaseInfo.parse(*data) + + def get_positions_history(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionHistory]: + return [ serializers.PositionHistory.parse(*sub_data) for sub_data in self._POST("auth/r/positions/hist", body={ "start": start, "end": end, "limit": limit }) ] + + def get_positions_snapshot(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionSnapshot]: + return [ serializers.PositionSnapshot.parse(*sub_data) for sub_data in self._POST("auth/r/positions/snap", body={ "start": start, "end": end, "limit": limit }) ] + + def get_positions_audit(self, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionAudit]: + return [ serializers.PositionAudit.parse(*sub_data) for sub_data in self._POST("auth/r/positions/audit", body={ "ids": ids, "start": start, "end": end, "limit": limit }) ] + + def set_derivative_position_collateral(self, symbol: str, collateral: Union[Decimal, float, str]) -> DerivativePositionCollateral: + return serializers.DerivativePositionCollateral.parse(*(self._POST("auth/w/deriv/collateral/set", body={ "symbol": symbol, "collateral": collateral })[0])) + + def get_derivative_position_collateral_limits(self, symbol: str) -> DerivativePositionCollateralLimits: + return serializers.DerivativePositionCollateralLimits.parse(*self._POST("auth/calc/deriv/collateral/limits", body={ "symbol": symbol })) + + def get_funding_offers(self, symbol: Optional[str] = None) -> List[FundingOffer]: + endpoint = "auth/r/funding/offers" + + if symbol != None: + endpoint += f"/{symbol}" + + return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint) ] + + def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, float, str], + rate: Union[Decimal, float, str], period: int, + flags: Optional[int] = 0) -> Notification[FundingOffer]: + body = { + "type": type, "symbol": symbol, "amount": amount, + "rate": rate, "period": period, + "flags": flags + } + + return serializers._Notification[FundingOffer](serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/submit", body=body)) + + def cancel_funding_offer(self, id: int) -> Notification[FundingOffer]: + return serializers._Notification[FundingOffer](serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/cancel", body={ "id": id })) + + def cancel_all_funding_offers(self, currency: str) -> Notification[Literal[None]]: + return serializers._Notification[Literal[None]](None).parse( + *self._POST("auth/w/funding/offer/cancel/all", body={ "currency": currency }) + ) + + def submit_funding_close(self, id: int) -> Notification[Literal[None]]: + return serializers._Notification[Literal[None]](None).parse( + *self._POST("auth/w/funding/close", body={ "id": id }) + ) + + def toggle_auto_renew(self, status: bool, currency: str, amount: Optional[str] = None, rate: Optional[int] = None, period: Optional[int] = None) -> Notification[FundingAutoRenew]: + return serializers._Notification[FundingAutoRenew](serializers.FundingAutoRenew).parse(*self._POST("auth/w/funding/auto", body={ + "status": int(status), + "currency": currency, "amount": amount, + "rate": rate, "period": period + })) + + def toggle_keep(self, type: Literal["credit", "loan"], ids: Optional[List[int]] = None, changes: Optional[Dict[int, bool]] = None) -> Notification[Literal[None]]: + return serializers._Notification[Literal[None]](None).parse(*self._POST("auth/w/funding/keep", body={ + "type": type, + "id": ids, + "changes": changes + })) + + def get_funding_offers_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingOffer]: + if symbol == None: + endpoint = "auth/r/funding/offers/hist" + else: endpoint = f"auth/r/funding/offers/{symbol}/hist" + + body = { + "start": start, "end": end, + "limit": limit + } + + return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] + + def get_funding_loans(self, symbol: Optional[str] = None) -> List[FundingLoan]: + if symbol == None: + endpoint = "auth/r/funding/loans" + else: endpoint = f"auth/r/funding/loans/{symbol}" + + return [ serializers.FundingLoan.parse(*sub_data) for sub_data in self._POST(endpoint) ] + + def get_funding_loans_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingLoan]: + if symbol == None: + endpoint = "auth/r/funding/loans/hist" + else: endpoint = f"auth/r/funding/loans/{symbol}/hist" + + body = { + "start": start, "end": end, + "limit": limit + } + + return [ serializers.FundingLoan.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] + + def get_funding_credits(self, symbol: Optional[str] = None) -> List[FundingCredit]: + if symbol == None: + endpoint = "auth/r/funding/credits" + else: endpoint = f"auth/r/funding/credits/{symbol}" + + return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint) ] + + def get_funding_credits_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingCredit]: + if symbol == None: + endpoint = "auth/r/funding/credits/hist" + else: endpoint = f"auth/r/funding/credits/{symbol}/hist" + + body = { + "start": start, "end": end, + "limit": limit + } + + return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] + + def get_funding_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingTrade]: + if symbol == None: + endpoint = "auth/r/funding/trades/hist" + else: endpoint = f"auth/r/funding/trades/{symbol}/hist" + + body = { + "sort": sort, + "start": start, "end": end, + "limit": limit + } + + return [ serializers.FundingTrade.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] + + def get_funding_info(self, key: str) -> FundingInfo: + response = self._POST(f"auth/r/info/funding/{key}") + data = [response[1]] + response[2] + return serializers.FundingInfo.parse(*data) + + def transfer_between_wallets(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, float, str]) -> Notification[Transfer]: + body = { + "from": from_wallet, "to": to_wallet, + "currency": currency, "currency_to": currency_to, + "amount": amount + } + + return serializers._Notification[Transfer](serializers.Transfer).parse(*self._POST("auth/w/transfer", body=body)) + + def submit_wallet_withdrawal(self, wallet: str, method: str, address: str, amount: Union[Decimal, float, str]) -> Notification[Withdrawal]: + return serializers._Notification[Withdrawal](serializers.Withdrawal).parse(*self._POST("auth/w/withdraw", body={ + "wallet": wallet, "method": method, + "address": address, "amount": amount, + })) + + def get_deposit_address(self, wallet: str, method: str, renew: bool = False) -> Notification[DepositAddress]: + body = { + "wallet": wallet, + "method": method, + "renew": int(renew) + } + + return serializers._Notification[DepositAddress](serializers.DepositAddress).parse(*self._POST("auth/w/deposit/address", body=body)) + + def generate_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, float, str]) -> LightningNetworkInvoice: + body = { + "wallet": wallet, "currency": currency, + "amount": amount + } + + return serializers.LightningNetworkInvoice.parse(*self._POST("auth/w/deposit/invoice", body=body)) + + def get_movements(self, currency: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Movement]: + if currency == None: + endpoint = "auth/r/movements/hist" + else: endpoint = f"auth/r/movements/{currency}/hist" + + body = { + "start": start, "end": end, + "limit": limit + } + + return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] \ No newline at end of file diff --git a/bfxapi/rest/endpoints/rest_merchant_endpoints.py b/bfxapi/rest/endpoints/rest_merchant_endpoints.py new file mode 100644 index 0000000..0c80110 --- /dev/null +++ b/bfxapi/rest/endpoints/rest_merchant_endpoints.py @@ -0,0 +1,69 @@ +from typing import TypedDict, List, Union, Literal, Optional + +from decimal import Decimal + +from .. types import * +from .. middleware import Middleware +from ...utils.camel_and_snake_case_helpers import to_snake_case_keys, to_camel_case_keys + +_CustomerInfo = TypedDict("_CustomerInfo", { + "nationality": str, "resid_country": str, "resid_city": str, + "resid_zip_code": str, "resid_street": str, "resid_building_no": str, + "full_name": str, "email": str, "tos_accepted": bool +}) + +class RestMerchantEndpoints(Middleware): + def submit_invoice(self, amount: Union[Decimal, float, str], currency: str, order_id: str, + customer_info: _CustomerInfo, pay_currencies: List[str], duration: Optional[int] = None, + webhook: Optional[str] = None, redirect_url: Optional[str] = None) -> InvoiceSubmission: + body = to_camel_case_keys({ + "amount": amount, "currency": currency, "order_id": order_id, + "customer_info": customer_info, "pay_currencies": pay_currencies, "duration": duration, + "webhook": webhook, "redirect_url": redirect_url + }) + + data = to_snake_case_keys(self._POST("auth/w/ext/pay/invoice/create", body=body)) + + return InvoiceSubmission.parse(data) + + def get_invoices(self, id: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[InvoiceSubmission]: + return [ InvoiceSubmission.parse(sub_data) for sub_data in to_snake_case_keys(self._POST("auth/r/ext/pay/invoices", body={ + "id": id, "start": start, "end": end, + "limit": limit + })) ] + + def get_invoice_count_stats(self, status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"], format: str) -> List[InvoiceStats]: + return [ InvoiceStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/count", body={ "status": status, "format": format }) ] + + def get_invoice_earning_stats(self, currency: str, format: str) -> List[InvoiceStats]: + return [ InvoiceStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/earning", body={ "currency": currency, "format": format }) ] + + def complete_invoice(self, id: str, pay_currency: str, deposit_id: Optional[int] = None, ledger_id: Optional[int] = None) -> InvoiceSubmission: + return InvoiceSubmission.parse(to_snake_case_keys(self._POST("auth/w/ext/pay/invoice/complete", body={ + "id": id, "payCcy": pay_currency, "depositId": deposit_id, + "ledgerId": ledger_id + }))) + + def expire_invoice(self, id: str) -> InvoiceSubmission: + return InvoiceSubmission.parse(to_snake_case_keys(self._POST("auth/w/ext/pay/invoice/expire", body={ "id": id }))) + + def get_currency_conversion_list(self) -> List[CurrencyConversion]: + return [ + CurrencyConversion( + base_currency=sub_data["baseCcy"], + convert_currency=sub_data["convertCcy"], + created=sub_data["created"] + ) for sub_data in self._POST("auth/r/ext/pay/settings/convert/list") + ] + + def add_currency_conversion(self, base_currency: str, convert_currency: str) -> bool: + return bool(self._POST("auth/w/ext/pay/settings/convert/create", body={ + "baseCcy": base_currency, + "convertCcy": convert_currency + })) + + def remove_currency_conversion(self, base_currency: str, convert_currency: str) -> bool: + return bool(self._POST("auth/w/ext/pay/settings/convert/remove", body={ + "baseCcy": base_currency, + "convertCcy": convert_currency + })) \ No newline at end of file diff --git a/bfxapi/rest/endpoints/rest_public_endpoints.py b/bfxapi/rest/endpoints/rest_public_endpoints.py new file mode 100644 index 0000000..b5313fd --- /dev/null +++ b/bfxapi/rest/endpoints/rest_public_endpoints.py @@ -0,0 +1,186 @@ +from typing import List, Union, Literal, Optional, Any, cast +from decimal import Decimal + +from .. types import * + +from .. import serializers +from .. enums import Config, Sort +from .. middleware import Middleware + +class RestPublicEndpoints(Middleware): + def conf(self, config: Config) -> Any: + return self._GET(f"conf/{config}")[0] + + def get_platform_status(self) -> PlatformStatus: + return serializers.PlatformStatus.parse(*self._GET("platform/status")) + + def get_tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]: + data = self._GET("tickers", params={ "symbols": ",".join(symbols) }) + + parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse } + + return [ cast(Union[TradingPairTicker, FundingCurrencyTicker], parsers[sub_data[0][0]](*sub_data)) for sub_data in data ] + + def get_t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]: + if isinstance(pairs, str) and pairs == "ALL": + return [ cast(TradingPairTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.symbol).startswith("t") ] + + data = self.get_tickers([ pair for pair in pairs ]) + + return cast(List[TradingPairTicker], data) + + def get_f_tickers(self, currencies: Union[List[str], Literal["ALL"]]) -> List[FundingCurrencyTicker]: + if isinstance(currencies, str) and currencies == "ALL": + return [ cast(FundingCurrencyTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.symbol).startswith("f") ] + + data = self.get_tickers([ currency for currency in currencies ]) + + return cast(List[FundingCurrencyTicker], data) + + def get_t_ticker(self, pair: str) -> TradingPairTicker: + return serializers.TradingPairTicker.parse(*self._GET(f"ticker/t{pair}"), skip=["SYMBOL"]) + + def get_f_ticker(self, currency: str) -> FundingCurrencyTicker: + return serializers.FundingCurrencyTicker.parse(*self._GET(f"ticker/f{currency}"), skip=["SYMBOL"]) + + def get_tickers_history(self, symbols: List[str], start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[TickersHistory]: + return [ serializers.TickersHistory.parse(*sub_data) for sub_data in self._GET("tickers/hist", params={ + "symbols": ",".join(symbols), + "start": start, "end": end, + "limit": limit + }) ] + + def get_t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[TradingPairTrade]: + params = { "limit": limit, "start": start, "end": end, "sort": sort } + data = self._GET(f"trades/{pair}/hist", params=params) + return [ serializers.TradingPairTrade.parse(*sub_data) for sub_data in data ] + + def get_f_trades(self, currency: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[FundingCurrencyTrade]: + params = { "limit": limit, "start": start, "end": end, "sort": sort } + data = self._GET(f"trades/{currency}/hist", params=params) + return [ serializers.FundingCurrencyTrade.parse(*sub_data) for sub_data in data ] + + def get_t_book(self, pair: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairBook]: + return [ serializers.TradingPairBook.parse(*sub_data) for sub_data in self._GET(f"book/{pair}/{precision}", params={ "len": len }) ] + + def get_f_book(self, currency: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyBook]: + return [ serializers.FundingCurrencyBook.parse(*sub_data) for sub_data in self._GET(f"book/{currency}/{precision}", params={ "len": len }) ] + + def get_t_raw_book(self, pair: str, len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairRawBook]: + return [ serializers.TradingPairRawBook.parse(*sub_data) for sub_data in self._GET(f"book/{pair}/R0", params={ "len": len }) ] + + def get_f_raw_book(self, currency: str, len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyRawBook]: + return [ serializers.FundingCurrencyRawBook.parse(*sub_data) for sub_data in self._GET(f"book/{currency}/R0", params={ "len": len }) ] + + def get_stats_hist( + self, + resource: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> List[Statistic]: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self._GET(f"stats1/{resource}/hist", params=params) + return [ serializers.Statistic.parse(*sub_data) for sub_data in data ] + + def get_stats_last( + self, + resource: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Statistic: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self._GET(f"stats1/{resource}/last", params=params) + return serializers.Statistic.parse(*data) + + def get_candles_hist( + self, + symbol: str, tf: str = "1m", + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> List[Candle]: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self._GET(f"candles/trade:{tf}:{symbol}/hist", params=params) + return [ serializers.Candle.parse(*sub_data) for sub_data in data ] + + def get_candles_last( + self, + symbol: str, tf: str = "1m", + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Candle: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self._GET(f"candles/trade:{tf}:{symbol}/last", params=params) + return serializers.Candle.parse(*data) + + def get_derivatives_status(self, keys: Union[List[str], Literal["ALL"]]) -> List[DerivativesStatus]: + if keys == "ALL": + params = { "keys": "ALL" } + else: params = { "keys": ",".join(keys) } + + data = self._GET(f"status/deriv", params=params) + + return [ serializers.DerivativesStatus.parse(*sub_data) for sub_data in data ] + + def get_derivatives_status_history( + self, + type: str, symbol: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> List[DerivativesStatus]: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self._GET(f"status/{type}/{symbol}/hist", params=params) + return [ serializers.DerivativesStatus.parse(*sub_data, skip=[ "KEY" ]) for sub_data in data ] + + def get_liquidations(self, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Liquidation]: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self._GET("liquidations/hist", params=params) + return [ serializers.Liquidation.parse(*sub_data[0]) for sub_data in data ] + + def get_seed_candles(self, symbol: str, tf: str = '1m', sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Candle]: + params = {"sort": sort, "start": start, "end": end, "limit": limit} + data = self._GET(f"candles/trade:{tf}:{symbol}/hist?limit={limit}&start={start}&end={end}&sort={sort}", params=params) + return [ serializers.Candle.parse(*sub_data) for sub_data in data ] + + def get_leaderboards_hist( + self, + resource: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> List[Leaderboard]: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self._GET(f"rankings/{resource}/hist", params=params) + return [ serializers.Leaderboard.parse(*sub_data) for sub_data in data ] + + def get_leaderboards_last( + self, + resource: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Leaderboard: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self._GET(f"rankings/{resource}/last", params=params) + return serializers.Leaderboard.parse(*data) + + def get_funding_stats(self, symbol: str, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingStatistic]: + params = { "start": start, "end": end, "limit": limit } + data = self._GET(f"funding/stats/{symbol}/hist", params=params) + return [ serializers.FundingStatistic.parse(*sub_data) for sub_data in data ] + + def get_pulse_profile(self, nickname: str) -> PulseProfile: + return serializers.PulseProfile.parse(*self._GET(f"pulse/profile/{nickname}")) + + def get_pulse_history(self, end: Optional[str] = None, limit: Optional[int] = None) -> List[PulseMessage]: + messages = list() + + for subdata in self._GET("pulse/hist", params={ "end": end, "limit": limit }): + subdata[18] = subdata[18][0] + message = serializers.PulseMessage.parse(*subdata) + messages.append(message) + + return messages + + def get_trading_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], price_limit: Optional[Union[Decimal, float, str]] = None) -> TradingMarketAveragePrice: + return serializers.TradingMarketAveragePrice.parse(*self._POST("calc/trade/avg", body={ + "symbol": symbol, "amount": amount, "price_limit": price_limit + })) + + def get_funding_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], period: int, rate_limit: Optional[Union[Decimal, float, str]] = None) -> FundingMarketAveragePrice: + return serializers.FundingMarketAveragePrice.parse(*self._POST("calc/trade/avg", body={ + "symbol": symbol, "amount": amount, "period": period, "rate_limit": rate_limit + })) + + def get_fx_rate(self, ccy1: str, ccy2: str) -> FxRate: + return serializers.FxRate.parse(*self._POST("calc/fx", body={ "ccy1": ccy1, "ccy2": ccy2 })) \ No newline at end of file diff --git a/bfxapi/rest/exceptions.py b/bfxapi/rest/exceptions.py index beff7bc..9fbf3a4 100644 --- a/bfxapi/rest/exceptions.py +++ b/bfxapi/rest/exceptions.py @@ -3,6 +3,7 @@ from .. exceptions import BfxBaseException __all__ = [ "BfxRestException", + "ResourceNotFound", "RequestParametersError", "ResourceNotFound", "InvalidAuthenticationCredentials" diff --git a/bfxapi/rest/middleware/__init__.py b/bfxapi/rest/middleware/__init__.py new file mode 100644 index 0000000..d7e276b --- /dev/null +++ b/bfxapi/rest/middleware/__init__.py @@ -0,0 +1,3 @@ +from .middleware import Middleware + +NAME = "middleware" \ No newline at end of file diff --git a/bfxapi/rest/middleware/middleware.py b/bfxapi/rest/middleware/middleware.py new file mode 100644 index 0000000..9180841 --- /dev/null +++ b/bfxapi/rest/middleware/middleware.py @@ -0,0 +1,82 @@ +import time, hmac, hashlib, json, requests + +from typing import TYPE_CHECKING, Optional, Any, cast + +from http import HTTPStatus +from ..enums import Error +from ..exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError + +from ...utils.JSONEncoder import JSONEncoder + +if TYPE_CHECKING: + from requests.sessions import _Params + +class Middleware(object): + def __init__(self, host: str, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None): + self.host, self.API_KEY, self.API_SECRET = host, API_KEY, API_SECRET + + def __build_authentication_headers(self, endpoint: str, data: Optional[str] = None): + assert isinstance(self.API_KEY, str) and isinstance(self.API_SECRET, str), \ + "API_KEY and API_SECRET must be both str to call __build_authentication_headers" + + nonce = str(int(time.time()) * 1000) + + if data == None: + path = f"/api/v2/{endpoint}{nonce}" + else: path = f"/api/v2/{endpoint}{nonce}{data}" + + signature = hmac.new( + self.API_SECRET.encode("utf8"), + path.encode("utf8"), + hashlib.sha384 + ).hexdigest() + + return { + "bfx-nonce": nonce, + "bfx-signature": signature, + "bfx-apikey": self.API_KEY + } + + def _GET(self, endpoint: str, params: Optional["_Params"] = None) -> Any: + response = requests.get(f"{self.host}/{endpoint}", params=params) + + if response.status_code == HTTPStatus.NOT_FOUND: + raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") + + data = response.json() + + if len(data) and data[0] == "error": + if data[1] == Error.ERR_PARAMS: + raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") + + if data[1] == None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC: + raise UnknownGenericError(f"The server replied to the request with a generic error with message: <{data[2]}>.") + + return data + + def _POST(self, endpoint: str, params: Optional["_Params"] = None, body: Optional[Any] = None, _ignore_authentication_headers: bool = False) -> Any: + data = body and json.dumps(body, cls=JSONEncoder) or None + + headers = { "Content-Type": "application/json" } + + if self.API_KEY and self.API_SECRET and _ignore_authentication_headers == False: + headers = { **headers, **self.__build_authentication_headers(endpoint, data) } + + response = requests.post(f"{self.host}/{endpoint}", params=params, data=data, headers=headers) + + if response.status_code == HTTPStatus.NOT_FOUND: + raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") + + data = response.json() + + if isinstance(data, list) and len(data) and data[0] == "error": + if data[1] == Error.ERR_PARAMS: + raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") + + if data[1] == Error.ERR_AUTH_FAIL: + raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.") + + if data[1] == None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC: + raise UnknownGenericError(f"The server replied to the request with a generic error with message: <{data[2]}>.") + + return data \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 9e2612a..bcdc7f3 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -1,54 +1,75 @@ -from . import typings +from . import types -from .. labeler import _Serializer +from .. labeler import generate_labeler_serializer, generate_recursive_serializer from .. notification import _Notification +__serializers__ = [ + "PlatformStatus", "TradingPairTicker", "FundingCurrencyTicker", + "TickersHistory", "TradingPairTrade", "FundingCurrencyTrade", + "TradingPairBook", "FundingCurrencyBook", "TradingPairRawBook", + "FundingCurrencyRawBook", "Statistic", "Candle", + "DerivativesStatus", "Liquidation", "Leaderboard", + "FundingStatistic", "PulseProfile", "PulseMessage", + "TradingMarketAveragePrice", "FundingMarketAveragePrice", "FxRate", + + "UserInfo", "LoginHistory", "BalanceAvailable", + "Order", "Position", "Trade", + "FundingTrade", "OrderTrade", "Ledger", + "FundingOffer", "FundingCredit", "FundingLoan", + "FundingAutoRenew", "FundingInfo", "Wallet", + "Transfer", "Withdrawal", "DepositAddress", + "LightningNetworkInvoice", "Movement", "SymbolMarginInfo", + "BaseMarginInfo", "PositionClaim", "PositionIncreaseInfo", + "PositionIncrease", "PositionHistory", "PositionSnapshot", + "PositionAudit", "DerivativePositionCollateral", "DerivativePositionCollateralLimits", +] + #region Serializers definition for Rest Public Endpoints -PlatformStatus = _Serializer[typings.PlatformStatus]("PlatformStatus", labels=[ - "OPERATIVE" +PlatformStatus = generate_labeler_serializer("PlatformStatus", klass=types.PlatformStatus, labels=[ + "status" ]) -TradingPairTicker = _Serializer[typings.TradingPairTicker]("TradingPairTicker", labels=[ - "SYMBOL", - "BID", - "BID_SIZE", - "ASK", - "ASK_SIZE", - "DAILY_CHANGE", - "DAILY_CHANGE_RELATIVE", - "LAST_PRICE", - "VOLUME", - "HIGH", - "LOW" +TradingPairTicker = generate_labeler_serializer("TradingPairTicker", klass=types.TradingPairTicker, labels=[ + "symbol", + "bid", + "bid_size", + "ask", + "ask_size", + "daily_change", + "daily_change_relative", + "last_price", + "volume", + "high", + "low" ]) -FundingCurrencyTicker = _Serializer[typings.FundingCurrencyTicker]("FundingCurrencyTicker", labels=[ - "SYMBOL", - "FRR", - "BID", - "BID_PERIOD", - "BID_SIZE", - "ASK", - "ASK_PERIOD", - "ASK_SIZE", - "DAILY_CHANGE", - "DAILY_CHANGE_RELATIVE", - "LAST_PRICE", - "VOLUME", - "HIGH", - "LOW", +FundingCurrencyTicker = generate_labeler_serializer("FundingCurrencyTicker", klass=types.FundingCurrencyTicker, labels=[ + "symbol", + "frr", + "bid", + "bid_period", + "bid_size", + "ask", + "ask_period", + "ask_size", + "daily_change", + "daily_change_relative", + "last_price", + "volume", + "high", + "low", "_PLACEHOLDER", "_PLACEHOLDER", - "FRR_AMOUNT_AVAILABLE" + "frr_amount_available" ]) -TickersHistory = _Serializer[typings.TickersHistory]("TickersHistory", labels=[ - "SYMBOL", - "BID", +TickersHistory = generate_labeler_serializer("TickersHistory", klass=types.TickersHistory, labels=[ + "symbol", + "bid", "_PLACEHOLDER", - "ASK", + "ask", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", @@ -57,295 +78,672 @@ TickersHistory = _Serializer[typings.TickersHistory]("TickersHistory", labels=[ "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "MTS" + "mts" ]) -TradingPairTrade = _Serializer[typings.TradingPairTrade]("TradingPairTrade", labels=[ - "ID", - "MTS", - "AMOUNT", - "PRICE" +TradingPairTrade = generate_labeler_serializer("TradingPairTrade", klass=types.TradingPairTrade, labels=[ + "id", + "mts", + "amount", + "price" ]) -FundingCurrencyTrade = _Serializer[typings.FundingCurrencyTrade]("FundingCurrencyTrade", labels=[ - "ID", - "MTS", - "AMOUNT", - "RATE", - "PERIOD" +FundingCurrencyTrade = generate_labeler_serializer("FundingCurrencyTrade", klass=types.FundingCurrencyTrade, labels=[ + "id", + "mts", + "amount", + "rate", + "period" ]) -TradingPairBook = _Serializer[typings.TradingPairBook]("TradingPairBook", labels=[ - "PRICE", - "COUNT", - "AMOUNT" +TradingPairBook = generate_labeler_serializer("TradingPairBook", klass=types.TradingPairBook, labels=[ + "price", + "count", + "amount" ]) -FundingCurrencyBook = _Serializer[typings.FundingCurrencyBook]("FundingCurrencyBook", labels=[ - "RATE", - "PERIOD", - "COUNT", - "AMOUNT" +FundingCurrencyBook = generate_labeler_serializer("FundingCurrencyBook", klass=types.FundingCurrencyBook, labels=[ + "rate", + "period", + "count", + "amount" ]) -TradingPairRawBook = _Serializer[typings.TradingPairRawBook]("TradingPairRawBook", labels=[ - "ORDER_ID", - "PRICE", - "AMOUNT" +TradingPairRawBook = generate_labeler_serializer("TradingPairRawBook", klass=types.TradingPairRawBook, labels=[ + "order_id", + "price", + "amount" ]) -FundingCurrencyRawBook = _Serializer[typings.FundingCurrencyRawBook]("FundingCurrencyRawBook", labels=[ - "OFFER_ID", - "PERIOD", - "RATE", - "AMOUNT" +FundingCurrencyRawBook = generate_labeler_serializer("FundingCurrencyRawBook", klass=types.FundingCurrencyRawBook, labels=[ + "offer_id", + "period", + "rate", + "amount" ]) -Statistic = _Serializer[typings.Statistic]("Statistic", labels=[ - "MTS", - "VALUE" +Statistic = generate_labeler_serializer("Statistic", klass=types.Statistic, labels=[ + "mts", + "value" ]) -Candle = _Serializer[typings.Candle]("Candle", labels=[ - "MTS", - "OPEN", - "CLOSE", - "HIGH", - "LOW", - "VOLUME" +Candle = generate_labeler_serializer("Candle", klass=types.Candle, labels=[ + "mts", + "open", + "close", + "high", + "low", + "volume" ]) -DerivativesStatus = _Serializer[typings.DerivativesStatus]("DerivativesStatus", labels=[ - "KEY", - "MTS", +DerivativesStatus = generate_labeler_serializer("DerivativesStatus", klass=types.DerivativesStatus, labels=[ + "key", + "mts", "_PLACEHOLDER", - "DERIV_PRICE", - "SPOT_PRICE", + "deriv_price", + "spot_price", "_PLACEHOLDER", - "INSURANCE_FUND_BALANCE", + "insurance_fund_balance", "_PLACEHOLDER", - "NEXT_FUNDING_EVT_TIMESTAMP_MS", - "NEXT_FUNDING_ACCRUED", - "NEXT_FUNDING_STEP", + "next_funding_evt_timestamp_ms", + "next_funding_accrued", + "next_funding_step", "_PLACEHOLDER", - "CURRENT_FUNDING", + "current_funding", "_PLACEHOLDER", "_PLACEHOLDER", - "MARK_PRICE", + "mark_price", "_PLACEHOLDER", "_PLACEHOLDER", - "OPEN_INTEREST", + "open_interest", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "CLAMP_MIN", - "CLAMP_MAX" + "clamp_min", + "clamp_max" ]) -Liquidation = _Serializer[typings.Liquidation]("Liquidation", labels=[ +Liquidation = generate_labeler_serializer("Liquidation", klass=types.Liquidation, labels=[ "_PLACEHOLDER", - "POS_ID", - "MTS", + "pos_id", + "mts", "_PLACEHOLDER", - "SYMBOL", - "AMOUNT", - "BASE_PRICE", + "symbol", + "amount", + "base_price", "_PLACEHOLDER", - "IS_MATCH", - "IS_MARKET_SOLD", + "is_match", + "is_market_sold", "_PLACEHOLDER", - "PRICE_ACQUIRED" + "price_acquired" ]) -Leaderboard = _Serializer[typings.Leaderboard]("Leaderboard", labels=[ - "MTS", +Leaderboard = generate_labeler_serializer("Leaderboard", klass=types.Leaderboard, labels=[ + "mts", "_PLACEHOLDER", - "USERNAME", - "RANKING", + "username", + "ranking", "_PLACEHOLDER", "_PLACEHOLDER", - "VALUE", + "value", "_PLACEHOLDER", "_PLACEHOLDER", - "TWITTER_HANDLE" + "twitter_handle" ]) -FundingStatistic = _Serializer[typings.FundingStatistic]("FundingStatistic", labels=[ - "TIMESTAMP", +FundingStatistic = generate_labeler_serializer("FundingStatistic", klass=types.FundingStatistic, labels=[ + "timestamp", "_PLACEHOLDER", "_PLACEHOLDER", - "FRR", - "AVG_PERIOD", + "frr", + "avg_period", "_PLACEHOLDER", "_PLACEHOLDER", - "FUNDING_AMOUNT", - "FUNDING_AMOUNT_USED", + "funding_amount", + "funding_amount_used", "_PLACEHOLDER", "_PLACEHOLDER", - "FUNDING_BELOW_THRESHOLD" + "funding_below_threshold" +]) + +PulseProfile = generate_labeler_serializer("PulseProfile", klass=types.PulseProfile, labels=[ + "puid", + "mts", + "_PLACEHOLDER", + "nickname", + "_PLACEHOLDER", + "picture", + "text", + "_PLACEHOLDER", + "_PLACEHOLDER", + "twitter_handle", + "_PLACEHOLDER", + "followers", + "following", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "tipping_status" +]) + +PulseMessage = generate_recursive_serializer("PulseMessage", klass=types.PulseMessage, serializers={ "profile": PulseProfile }, labels=[ + "pid", + "mts", + "_PLACEHOLDER", + "puid", + "_PLACEHOLDER", + "title", + "content", + "_PLACEHOLDER", + "_PLACEHOLDER", + "is_pin", + "is_public", + "comments_disabled", + "tags", + "attachments", + "meta", + "likes", + "_PLACEHOLDER", + "_PLACEHOLDER", + "profile", + "comments", + "_PLACEHOLDER", + "_PLACEHOLDER" +]) + +TradingMarketAveragePrice = generate_labeler_serializer("TradingMarketAveragePrice", klass=types.TradingMarketAveragePrice, labels=[ + "price_avg", + "amount" +]) + +FundingMarketAveragePrice = generate_labeler_serializer("FundingMarketAveragePrice", klass=types.FundingMarketAveragePrice, labels=[ + "rate_avg", + "amount" +]) + +FxRate = generate_labeler_serializer("FxRate", klass=types.FxRate, labels=[ + "current_rate" ]) #endregion #region Serializers definition for Rest Authenticated Endpoints -Wallet = _Serializer[typings.Wallet]("Wallet", labels=[ - "WALLET_TYPE", - "CURRENCY", - "BALANCE", - "UNSETTLED_INTEREST", - "AVAILABLE_BALANCE", - "LAST_CHANGE", - "TRADE_DETAILS" +UserInfo = generate_labeler_serializer("UserInfo", klass=types.UserInfo, labels=[ + "id", + "email", + "username", + "mts_account_create", + "verified", + "verification_level", + "_PLACEHOLDER", + "timezone", + "locale", + "company", + "email_verified", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "mts_master_account_create", + "group_id", + "master_account_id", + "inherit_master_account_verification", + "is_group_master", + "group_withdraw_enabled", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "ppt_enabled", + "merchant_enabled", + "competition_enabled", + "two_factors_authentication_modes", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "is_securities_master", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "securities_enabled", + "allow_disable_ctxswitch", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "time_last_login", + "_PLACEHOLDER", + "_PLACEHOLDER", + "ctxtswitch_disabled", + "_PLACEHOLDER", + "comp_countries", + "compl_countries_resid", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "is_merchant_enterprise" ]) -Order = _Serializer[typings.Order]("Order", labels=[ - "ID", - "GID", - "CID", - "SYMBOL", - "MTS_CREATE", - "MTS_UPDATE", - "AMOUNT", - "AMOUNT_ORIG", - "ORDER_TYPE", - "TYPE_PREV", - "MTS_TIF", +LoginHistory = generate_labeler_serializer("LoginHistory", klass=types.LoginHistory, labels=[ + "id", "_PLACEHOLDER", - "FLAGS", - "ORDER_STATUS", + "time", + "_PLACEHOLDER", + "ip", "_PLACEHOLDER", "_PLACEHOLDER", - "PRICE", - "PRICE_AVG", - "PRICE_TRAILING", - "PRICE_AUX_LIMIT", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "NOTIFY", - "HIDDEN", - "PLACED_ID", - "_PLACEHOLDER", - "_PLACEHOLDER", - "ROUTING", - "_PLACEHOLDER", - "_PLACEHOLDER", - "META" + "extra_info" ]) -Position = _Serializer[typings.Position]("Position", labels=[ - "SYMBOL", - "STATUS", - "AMOUNT", - "BASE_PRICE", - "FUNDING", - "FUNDING_TYPE", - "PL", - "PL_PERC", - "PRICE_LIQ", - "LEVERAGE", - "_PLACEHOLDER", - "POSITION_ID", - "MTS_CREATE", - "MTS_UPDATE", - "_PLACEHOLDER", - "TYPE", - "_PLACEHOLDER", - "COLLATERAL", - "COLLATERAL_MIN", - "META" +BalanceAvailable = generate_labeler_serializer("BalanceAvailable", klass=types.BalanceAvailable, labels=[ + "amount" ]) -FundingOffer = _Serializer[typings.FundingOffer]("FundingOffer", labels=[ - "ID", - "SYMBOL", - "MTS_CREATED", - "MTS_UPDATED", - "AMOUNT", - "AMOUNT_ORIG", - "OFFER_TYPE", +Order = generate_labeler_serializer("Order", klass=types.Order, labels=[ + "id", + "gid", + "cid", + "symbol", + "mts_create", + "mts_update", + "amount", + "amount_orig", + "order_type", + "type_prev", + "mts_tif", + "_PLACEHOLDER", + "flags", + "order_status", "_PLACEHOLDER", "_PLACEHOLDER", - "FLAGS", - "OFFER_STATUS", + "price", + "price_avg", + "price_trailing", + "price_aux_limit", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "RATE", - "PERIOD", - "NOTIFY", - "HIDDEN", + "notify", + "hidden", + "placed_id", "_PLACEHOLDER", - "RENEW", + "_PLACEHOLDER", + "routing", + "_PLACEHOLDER", + "_PLACEHOLDER", + "meta" +]) + +Position = generate_labeler_serializer("Position", klass=types.Position, labels=[ + "symbol", + "status", + "amount", + "base_price", + "margin_funding", + "margin_funding_type", + "pl", + "pl_perc", + "price_liq", + "leverage", + "_PLACEHOLDER", + "position_id", + "mts_create", + "mts_update", + "_PLACEHOLDER", + "type", + "_PLACEHOLDER", + "collateral", + "collateral_min", + "meta" +]) + +Trade = generate_labeler_serializer("Trade", klass=types.Trade, labels=[ + "id", + "symbol", + "mts_create", + "order_id", + "exec_amount", + "exec_price", + "order_type", + "order_price", + "maker", + "fee", + "fee_currency", + "cid" +]) + +FundingTrade = generate_labeler_serializer("FundingTrade", klass=types.FundingTrade, labels=[ + "id", + "currency", + "mts_create", + "offer_id", + "amount", + "rate", + "period" +]) + +OrderTrade = generate_labeler_serializer("OrderTrade", klass=types.OrderTrade, labels=[ + "id", + "symbol", + "mts_create", + "order_id", + "exec_amount", + "exec_price", + "_PLACEHOLDER", + "_PLACEHOLDER", + "maker", + "fee", + "fee_currency", + "cid" +]) + +Ledger = generate_labeler_serializer("Ledger", klass=types.Ledger, labels=[ + "id", + "currency", + "_PLACEHOLDER", + "mts", + "_PLACEHOLDER", + "amount", + "balance", + "_PLACEHOLDER", + "description" +]) + +FundingOffer = generate_labeler_serializer("FundingOffer", klass=types.FundingOffer, labels=[ + "id", + "symbol", + "mts_create", + "mts_update", + "amount", + "amount_orig", + "offer_type", + "_PLACEHOLDER", + "_PLACEHOLDER", + "flags", + "offer_status", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "rate", + "period", + "notify", + "hidden", + "_PLACEHOLDER", + "renew", "_PLACEHOLDER" ]) -Trade = _Serializer[typings.Trade]("Trade", labels=[ - "ID", - "PAIR", - "MTS_CREATE", - "ORDER_ID", - "EXEC_AMOUNT", - "EXEC_PRICE", - "ORDER_TYPE", - "ORDER_PRICE", - "MAKER", - "FEE", - "FEE_CURRENCY", - "CID" +FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.FundingCredit, labels=[ + "id", + "symbol", + "side", + "mts_create", + "mts_update", + "amount", + "flags", + "status", + "rate_type", + "_PLACEHOLDER", + "_PLACEHOLDER", + "rate", + "period", + "mts_opening", + "mts_last_payout", + "notify", + "hidden", + "_PLACEHOLDER", + "renew", + "_PLACEHOLDER", + "no_close", + "position_pair" ]) -OrderTrade = _Serializer[typings.OrderTrade]("OrderTrade", labels=[ - "ID", - "PAIR", - "MTS_CREATE", - "ORDER_ID", - "EXEC_AMOUNT", - "EXEC_PRICE", +FundingLoan = generate_labeler_serializer("FundingLoan", klass=types.FundingLoan, labels=[ + "id", + "symbol", + "side", + "mts_create", + "mts_update", + "amount", + "flags", + "status", + "rate_type", "_PLACEHOLDER", "_PLACEHOLDER", - "MAKER", - "FEE", - "FEE_CURRENCY", - "CID" + "rate", + "period", + "mts_opening", + "mts_last_payout", + "notify", + "hidden", + "_PLACEHOLDER", + "renew", + "_PLACEHOLDER", + "no_close" ]) -Ledger = _Serializer[typings.Ledger]("Ledger", labels=[ - "ID", - "CURRENCY", - "_PLACEHOLDER", - "MTS", - "_PLACEHOLDER", - "AMOUNT", - "BALANCE", - "_PLACEHOLDER", - "DESCRIPTION" +FundingAutoRenew = generate_labeler_serializer("FundingAutoRenew", klass=types.FundingAutoRenew, labels=[ + "currency", + "period", + "rate", + "threshold" ]) -FundingCredit = _Serializer[typings.FundingCredit]("FundingCredit", labels=[ - "ID", - "SYMBOL", - "SIDE", - "MTS_CREATE", - "MTS_UPDATE", - "AMOUNT", - "FLAGS", - "STATUS", - "RATE_TYPE", +FundingInfo = generate_labeler_serializer("FundingInfo", klass=types.FundingInfo, labels=[ + "symbol", + "yield_loan", + "yield_lend", + "duration_loan", + "duration_lend" +]) + +Wallet = generate_labeler_serializer("Wallet", klass=types.Wallet, labels=[ + "wallet_type", + "currency", + "balance", + "unsettled_interest", + "available_balance", + "last_change", + "trade_details" +]) + +Transfer = generate_labeler_serializer("Transfer", klass=types.Transfer, labels=[ + "mts", + "wallet_from", + "wallet_to", + "_PLACEHOLDER", + "currency", + "currency_to", + "_PLACEHOLDER", + "amount" +]) + +Withdrawal = generate_labeler_serializer("Withdrawal", klass=types.Withdrawal, labels=[ + "withdrawal_id", + "_PLACEHOLDER", + "method", + "payment_id", + "wallet", + "amount", "_PLACEHOLDER", "_PLACEHOLDER", - "RATE", - "PERIOD", - "MTS_OPENING", - "MTS_LAST_PAYOUT", - "NOTIFY", - "HIDDEN", + "withdrawal_fee" +]) + +DepositAddress = generate_labeler_serializer("DepositAddress", klass=types.DepositAddress, labels=[ "_PLACEHOLDER", - "RENEW", + "method", + "currency_code", "_PLACEHOLDER", - "NO_CLOSE", - "POSITION_PAIR" + "address", + "pool_address" +]) + +LightningNetworkInvoice = generate_labeler_serializer("LightningNetworkInvoice", klass=types.LightningNetworkInvoice, labels=[ + "invoice_hash", + "invoice", + "_PLACEHOLDER", + "_PLACEHOLDER", + "amount" +]) + +Movement = generate_labeler_serializer("Movement", klass=types.Movement, labels=[ + "id", + "currency", + "currency_name", + "_PLACEHOLDER", + "_PLACEHOLDER", + "mts_start", + "mts_update", + "_PLACEHOLDER", + "_PLACEHOLDER", + "status", + "_PLACEHOLDER", + "_PLACEHOLDER", + "amount", + "fees", + "_PLACEHOLDER", + "_PLACEHOLDER", + "destination_address", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "transaction_id", + "withdraw_transaction_note" +]) + +SymbolMarginInfo = generate_labeler_serializer("SymbolMarginInfo", klass=types.SymbolMarginInfo, labels=[ + "symbol", + "tradable_balance", + "gross_balance", + "buy", + "sell" +]) + +BaseMarginInfo = generate_labeler_serializer("BaseMarginInfo", klass=types.BaseMarginInfo, labels=[ + "user_pl", + "user_swaps", + "margin_balance", + "margin_net", + "margin_min" +]) + +PositionClaim = generate_labeler_serializer("PositionClaim", klass=types.PositionClaim, labels=[ + "symbol", + "position_status", + "amount", + "base_price", + "margin_funding", + "margin_funding_type", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "position_id", + "mts_create", + "mts_update", + "_PLACEHOLDER", + "pos_type", + "_PLACEHOLDER", + "collateral", + "min_collateral", + "meta" +]) + +PositionIncreaseInfo = generate_labeler_serializer("PositionIncreaseInfo", klass=types.PositionIncreaseInfo, labels=[ + "max_pos", + "current_pos", + "base_currency_balance", + "tradable_balance_quote_currency", + "tradable_balance_quote_total", + "tradable_balance_base_currency", + "tradable_balance_base_total", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "funding_avail", + "funding_value", + "funding_required", + "funding_value_currency", + "funding_required_currency" +]) + +PositionIncrease = generate_labeler_serializer("PositionIncrease", klass=types.PositionIncrease, labels=[ + "symbol", + "_PLACEHOLDER", + "amount", + "base_price" +]) + +PositionHistory = generate_labeler_serializer("PositionHistory", klass=types.PositionHistory, labels=[ + "symbol", + "status", + "amount", + "base_price", + "funding", + "funding_type", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "position_id", + "mts_create", + "mts_update" +]) + +PositionSnapshot = generate_labeler_serializer("PositionSnapshot", klass=types.PositionSnapshot, labels=[ + "symbol", + "status", + "amount", + "base_price", + "funding", + "funding_type", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "position_id", + "mts_create", + "mts_update" +]) + +PositionAudit = generate_labeler_serializer("PositionAudit", klass=types.PositionAudit, labels=[ + "symbol", + "status", + "amount", + "base_price", + "funding", + "funding_type", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "position_id", + "mts_create", + "mts_update", + "_PLACEHOLDER", + "type", + "_PLACEHOLDER", + "collateral", + "collateral_min", + "meta" +]) + +DerivativePositionCollateral = generate_labeler_serializer("DerivativePositionCollateral", klass=types.DerivativePositionCollateral, labels=[ + "status" +]) + +DerivativePositionCollateralLimits = generate_labeler_serializer("DerivativePositionCollateralLimits", klass=types.DerivativePositionCollateralLimits, labels=[ + "min_collateral", + "max_collateral" ]) #endregion \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py new file mode 100644 index 0000000..e1c39af --- /dev/null +++ b/bfxapi/rest/types.py @@ -0,0 +1,652 @@ +from typing import List, Dict, Optional, Literal, Any + +from dataclasses import dataclass + +from .. labeler import _Type, partial, compose +from .. notification import Notification +from .. utils.JSONEncoder import JSON + +#region Type hinting for Rest Public Endpoints + +@dataclass +class PlatformStatus(_Type): + status: int + +@dataclass +class TradingPairTicker(_Type): + symbol: Optional[str] + bid: float + bid_size: float + ask: float + ask_size: float + daily_change: float + daily_change_relative: float + last_price: float + volume: float + high: float + low: float + +@dataclass +class FundingCurrencyTicker(_Type): + symbol: Optional[str] + frr: float + bid: float + bid_period: int + bid_size: float + ask: float + ask_period: int + ask_size: float + daily_change: float + daily_change_relative: float + last_price: float + volume: float + high: float + low: float + frr_amount_available: float + +@dataclass +class TickersHistory(_Type): + symbol: str + bid: float + ask: float + mts: int + +@dataclass +class TradingPairTrade(_Type): + id: int + mts: int + amount: float + price: float + +@dataclass +class FundingCurrencyTrade(_Type): + id: int + mts: int + amount: float + rate: float + period: int + +@dataclass +class TradingPairBook(_Type): + price: float + count: int + amount: float + +@dataclass +class FundingCurrencyBook(_Type): + rate: float + period: int + count: int + amount: float + +@dataclass +class TradingPairRawBook(_Type): + order_id: int + price: float + amount: float + +@dataclass +class FundingCurrencyRawBook(_Type): + offer_id: int + period: int + rate: float + amount: float + +@dataclass +class Statistic(_Type): + mts: int + value: float + +@dataclass +class Candle(_Type): + mts: int + open: float + close: float + high: float + low: float + volume: float + +@dataclass +class DerivativesStatus(_Type): + key: Optional[str] + mts: int + deriv_price: float + spot_price: float + insurance_fund_balance: float + next_funding_evt_timestamp_ms: int + next_funding_accrued: float + next_funding_step: int + current_funding: float + mark_price: float + open_interest: float + clamp_min: float + clamp_max: float + +@dataclass +class Liquidation(_Type): + pos_id: int + mts: int + symbol: str + amount: float + base_price: float + is_match: int + is_market_sold: int + price_acquired: float + +@dataclass +class Leaderboard(_Type): + mts: int + username: str + ranking: int + value: float + twitter_handle: Optional[str] + +@dataclass +class FundingStatistic(_Type): + timestamp: int + frr: float + avg_period: float + funding_amount: float + funding_amount_used: float + funding_below_threshold: float + +@dataclass +class PulseProfile(_Type): + puid: str + mts: int + nickname: str + picture: str + text: str + twitter_handle: str + followers: int + following: int + tipping_status: int + +@dataclass +class PulseMessage(_Type): + pid: str + mts: int + puid: str + title: str + content: str + is_pin: int + is_public: int + comments_disabled: int + tags: List[str] + attachments: List[str] + meta: List[JSON] + likes: int + profile: PulseProfile + comments: int + +@dataclass +class TradingMarketAveragePrice(_Type): + price_avg: float + amount: float + +@dataclass +class FundingMarketAveragePrice(_Type): + rate_avg: float + amount: float + +@dataclass +class FxRate(_Type): + current_rate: float + +#endregion + +#region Type hinting for Rest Authenticated Endpoints + +@dataclass +class UserInfo(_Type): + id: int + email: str + username: str + mts_account_create: int + verified: int + verification_level: int + timezone: str + locale: str + company: str + email_verified: int + mts_master_account_create: int + group_id: int + master_account_id: int + inherit_master_account_verification: int + is_group_master: int + group_withdraw_enabled: int + ppt_enabled: int + merchant_enabled: int + competition_enabled: int + two_factors_authentication_modes: List[str] + is_securities_master: int + securities_enabled: int + allow_disable_ctxswitch: int + time_last_login: int + ctxtswitch_disabled: int + comp_countries: List[str] + compl_countries_resid: List[str] + is_merchant_enterprise: int + +@dataclass +class LoginHistory(_Type): + id: int + time: int + ip: str + extra_info: JSON + +@dataclass +class BalanceAvailable(_Type): + amount: float + +@dataclass +class Order(_Type): + id: int + gid: int + cid: int + symbol: str + mts_create: int + mts_update: int + amount: float + amount_orig: float + order_type: str + type_prev: str + mts_tif: int + flags: int + order_status: str + price: float + price_avg: float + price_trailing: float + price_aux_limit: float + notify: int + hidden: int + placed_id: int + routing: str + meta: JSON + +@dataclass +class Position(_Type): + symbol: str + status: str + amount: float + base_price: float + margin_funding: float + margin_funding_type: int + pl: float + pl_perc: float + price_liq: float + leverage: float + position_id: int + mts_create: int + mts_update: int + type: int + collateral: float + collateral_min: float + meta: JSON + +@dataclass +class Trade(_Type): + id: int + symbol: str + mts_create: int + order_id: int + exec_amount: float + exec_price: float + order_type: str + order_price: float + maker:int + fee: float + fee_currency: str + cid: int + +@dataclass() +class FundingTrade(_Type): + id: int + currency: str + mts_create: int + offer_id: int + amount: float + rate: float + period: int + +@dataclass +class OrderTrade(_Type): + id: int + symbol: str + mts_create: int + order_id: int + exec_amount: float + exec_price: float + maker:int + fee: float + fee_currency: str + cid: int + +@dataclass +class Ledger(_Type): + id: int + currency: str + mts: int + amount: float + balance: float + description: str + +@dataclass +class FundingOffer(_Type): + id: int + symbol: str + mts_create: int + mts_update: int + amount: float + amount_orig: float + offer_type: str + flags: int + offer_status: str + rate: float + period: int + notify: int + hidden: int + renew: int + +@dataclass +class FundingCredit(_Type): + id: int + symbol: str + side: int + mts_create: int + mts_update: int + amount: float + flags: int + status: str + rate_type: str + rate: float + period: int + mts_opening: int + mts_last_payout: int + notify: int + hidden: int + renew: int + no_close: int + position_pair: str + +@dataclass +class FundingLoan(_Type): + id: int + symbol: str + side: int + mts_create: int + mts_update: int + amount: float + flags: int + status: str + rate_type: str + rate: float + period: int + mts_opening: int + mts_last_payout: int + notify: int + hidden: int + renew: int + no_close: int + +@dataclass +class FundingAutoRenew(_Type): + currency: str + period: int + rate: float + threshold: float + +@dataclass() +class FundingInfo(_Type): + symbol: str + yield_loan: float + yield_lend: float + duration_loan: float + duration_lend: float + +@dataclass +class Wallet(_Type): + wallet_type: str + currency: str + balance: float + unsettled_interest: float + available_balance: float + last_change: str + trade_details: JSON + +@dataclass +class Transfer(_Type): + mts: int + wallet_from: str + wallet_to: str + currency: str + currency_to: str + amount: int + +@dataclass +class Withdrawal(_Type): + withdrawal_id: int + method: str + payment_id: str + wallet: str + amount: float + withdrawal_fee: float + +@dataclass +class DepositAddress(_Type): + method: str + currency_code: str + address: str + pool_address: str + +@dataclass +class LightningNetworkInvoice(_Type): + invoice_hash: str + invoice: str + amount: str + +@dataclass +class Movement(_Type): + id: str + currency: str + currency_name: str + mts_start: int + mts_update: int + status: str + amount: int + fees: int + destination_address: str + transaction_id: str + withdraw_transaction_note: str + +@dataclass +class SymbolMarginInfo(_Type): + symbol: str + tradable_balance: float + gross_balance: float + buy: float + sell: float + +@dataclass +class BaseMarginInfo(_Type): + user_pl: float + user_swaps: float + margin_balance: float + margin_net: float + margin_min: float + +@dataclass +class PositionClaim(_Type): + symbol: str + position_status: str + amount: float + base_price: float + margin_funding: float + margin_funding_type: int + position_id: int + mts_create: int + mts_update: int + pos_type: int + collateral: str + min_collateral: str + meta: JSON + +@dataclass +class PositionIncreaseInfo(_Type): + max_pos: int + current_pos: float + base_currency_balance: float + tradable_balance_quote_currency: float + tradable_balance_quote_total: float + tradable_balance_base_currency: float + tradable_balance_base_total: float + funding_avail: float + funding_value: float + funding_required: float + funding_value_currency: str + funding_required_currency: str + +@dataclass +class PositionIncrease(_Type): + symbol: str + amount: float + base_price: float + +@dataclass +class PositionHistory(_Type): + symbol: str + status: str + amount: float + base_price: float + funding: float + funding_type: int + position_id: int + mts_create: int + mts_update: int + +@dataclass +class PositionSnapshot(_Type): + symbol: str + status: str + amount: float + base_price: float + funding: float + funding_type: int + position_id: int + mts_create: int + mts_update: int + +@dataclass +class PositionAudit(_Type): + symbol: str + status: str + amount: float + base_price: float + funding: float + funding_type: int + position_id: int + mts_create: int + mts_update: int + type: int + collateral: float + collateral_min: float + meta: JSON + +@dataclass +class DerivativePositionCollateral(_Type): + status: int + +@dataclass +class DerivativePositionCollateralLimits(_Type): + min_collateral: float + max_collateral: float + +#endregion + +#region Type hinting for Rest Merchant Endpoints + +@compose(dataclass, partial) +class InvoiceSubmission(_Type): + id: str + t: int + type: Literal["ECOMMERCE", "POS"] + duration: int + amount: float + currency: str + order_id: str + pay_currencies: List[str] + webhook: str + redirect_url: str + status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"] + customer_info: "CustomerInfo" + invoices: List["Invoice"] + payment: "Payment" + additional_payments: List["Payment"] + merchant_name: str + + @classmethod + def parse(cls, data: Dict[str, Any]) -> "InvoiceSubmission": + if "customer_info" in data and data["customer_info"] != None: + data["customer_info"] = InvoiceSubmission.CustomerInfo(**data["customer_info"]) + + for index, invoice in enumerate(data["invoices"]): + data["invoices"][index] = InvoiceSubmission.Invoice(**invoice) + + if "payment" in data and data["payment"] != None: + data["payment"] = InvoiceSubmission.Payment(**data["payment"]) + + if "additional_payments" in data and data["additional_payments"] != None: + for index, additional_payment in enumerate(data["additional_payments"]): + data["additional_payments"][index] = InvoiceSubmission.Payment(**additional_payment) + + return InvoiceSubmission(**data) + + @compose(dataclass, partial) + class CustomerInfo: + nationality: str + resid_country: str + resid_state: str + resid_city: str + resid_zip_code: str + resid_street: str + resid_building_no: str + full_name: str + email: str + tos_accepted: bool + + @compose(dataclass, partial) + class Invoice: + amount: float + currency: str + pay_currency: str + pool_currency: str + address: str + ext: JSON + + @compose(dataclass, partial) + class Payment: + txid: str + amount: float + currency: str + method: str + status: Literal["CREATED", "COMPLETED", "PROCESSING"] + confirmations: int + created_at: str + updated_at: str + deposit_id: int + ledger_id: int + force_completed: bool + amount_diff: str + +@dataclass +class InvoiceStats(_Type): + time: str + count: float + +@dataclass +class CurrencyConversion(_Type): + base_currency: str + convert_currency: str + created: int + +#endregion \ No newline at end of file diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py deleted file mode 100644 index d9d37a7..0000000 --- a/bfxapi/rest/typings.py +++ /dev/null @@ -1,261 +0,0 @@ -from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any - -from .. notification import Notification - -JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] - -#region Type hinting for Rest Public Endpoints - -class PlatformStatus(TypedDict): - OPERATIVE: int - -class TradingPairTicker(TypedDict): - SYMBOL: Optional[str] - BID: float - BID_SIZE: float - ASK: float - ASK_SIZE: float - DAILY_CHANGE: float - DAILY_CHANGE_RELATIVE: float - LAST_PRICE: float - VOLUME: float - HIGH: float - LOW: float - -class FundingCurrencyTicker(TypedDict): - SYMBOL: Optional[str] - FRR: float - BID: float - BID_PERIOD: int - BID_SIZE: float - ASK: float - ASK_PERIOD: int - ASK_SIZE: float - DAILY_CHANGE: float - DAILY_CHANGE_RELATIVE: float - LAST_PRICE: float - VOLUME: float - HIGH: float - LOW: float - FRR_AMOUNT_AVAILABLE: float - -class TickersHistory(TypedDict): - SYMBOL: str - BID: float - ASK: float - MTS: int - -class TradingPairTrade(TypedDict): - ID: int - MTS: int - AMOUNT: float - PRICE: float - -class FundingCurrencyTrade(TypedDict): - ID: int - MTS: int - AMOUNT: float - RATE: float - PERIOD: int - -class TradingPairBook(TypedDict): - PRICE: float - COUNT: int - AMOUNT: float - -class FundingCurrencyBook(TypedDict): - RATE: float - PERIOD: int - COUNT: int - AMOUNT: float - -class TradingPairRawBook(TypedDict): - ORDER_ID: int - PRICE: float - AMOUNT: float - -class FundingCurrencyRawBook(TypedDict): - OFFER_ID: int - PERIOD: int - RATE: float - AMOUNT: float - -class Statistic(TypedDict): - MTS: int - VALUE: float - -class Candle(TypedDict): - MTS: int - OPEN: float - CLOSE: float - HIGH: float - LOW: float - VOLUME: float - -class DerivativesStatus(TypedDict): - KEY: Optional[str] - MTS: int - DERIV_PRICE: float - SPOT_PRICE: float - INSURANCE_FUND_BALANCE: float - NEXT_FUNDING_EVT_TIMESTAMP_MS: int - NEXT_FUNDING_ACCRUED: float - NEXT_FUNDING_STEP: int - CURRENT_FUNDING: float - MARK_PRICE: float - OPEN_INTEREST: float - CLAMP_MIN: float - CLAMP_MAX: float - -class Liquidation(TypedDict): - POS_ID: int - MTS: int - SYMBOL: str - AMOUNT: float - BASE_PRICE: float - IS_MATCH: int - IS_MARKET_SOLD: int - PRICE_ACQUIRED: float - -class Leaderboard(TypedDict): - MTS: int - USERNAME: str - RANKING: int - VALUE: float - TWITTER_HANDLE: Optional[str] - -class FundingStatistic(TypedDict): - TIMESTAMP: int - FRR: float - AVG_PERIOD: float - FUNDING_AMOUNT: float - FUNDING_AMOUNT_USED: float - FUNDING_BELOW_THRESHOLD: float - -#endregion - -#region Type hinting for Rest Authenticated Endpoints - -class Wallet(TypedDict): - WALLET_TYPE: str - CURRENCY: str - BALANCE: float - UNSETTLED_INTEREST: float - AVAILABLE_BALANCE: float - LAST_CHANGE: str - TRADE_DETAILS: JSON - -class Order(TypedDict): - ID: int - GID: int - CID: int - SYMBOL: str - MTS_CREATE: int - MTS_UPDATE: int - AMOUNT: float - AMOUNT_ORIG: float - ORDER_TYPE: str - TYPE_PREV: str - MTS_TIF: int - FLAGS: int - ORDER_STATUS: str - PRICE: float - PRICE_AVG: float - PRICE_TRAILING: float - PRICE_AUX_LIMIT: float - NOTIFY: int - HIDDEN: int - PLACED_ID: int - ROUTING: str - META: JSON - -class Position(TypedDict): - SYMBOL: str - STATUS: str - AMOUNT: float - BASE_PRICE: float - MARGIN_FUNDING: float - MARGIN_FUNDING_TYPE: int - PL: float - PL_PERC: float - PRICE_LIQ: float - LEVERAGE: float - POSITION_ID: int - MTS_CREATE: int - MTS_UPDATE: int - TYPE: int - COLLATERAL: float - COLLATERAL_MIN: float - META: JSON - -class FundingOffer(TypedDict): - ID: int - SYMBOL: str - MTS_CREATE: int - MTS_UPDATE: int - AMOUNT: float - AMOUNT_ORIG: float - OFFER_TYPE: str - FLAGS: int - OFFER_STATUS: str - RATE: float - PERIOD: int - NOTIFY: bool - HIDDEN: int - RENEW: bool - -class Trade(TypedDict): - ID: int - SYMBOL: str - MTS_CREATE: int - ORDER_ID: int - EXEC_AMOUNT: float - EXEC_PRICE: float - ORDER_TYPE: str - ORDER_PRICE: float - MAKER:int - FEE: float - FEE_CURRENCY: str - CID: int - -class OrderTrade(TypedDict): - ID: int - SYMBOL: str - MTS_CREATE: int - ORDER_ID: int - EXEC_AMOUNT: float - EXEC_PRICE: float - MAKER:int - FEE: float - FEE_CURRENCY: str - CID: int - -class Ledger(TypedDict): - ID: int - CURRENCY: str - MTS: int - AMOUNT: float - BALANCE: float - description: str - -class FundingCredit(TypedDict): - ID: int - SYMBOL: str - SIDE: int - MTS_CREATE: int - MTS_UPDATE: int - AMOUNT: float - FLAGS: int - STATUS: str - RATE: float - PERIOD: int - MTS_OPENING: int - MTS_LAST_PAYOUT: int - NOTIFY: int - HIDDEN: int - RENEW: int - RATE_REAL: float - NO_CLOSE: int - POSITION_PAIR: str - -#endregion \ No newline at end of file diff --git a/bfxapi/tests/__init__.py b/bfxapi/tests/__init__.py new file mode 100644 index 0000000..057c2c0 --- /dev/null +++ b/bfxapi/tests/__init__.py @@ -0,0 +1,18 @@ +import unittest +from .test_rest_serializers import TestRestSerializers +from .test_websocket_serializers import TestWebsocketSerializers +from .test_labeler import TestLabeler +from .test_notification import TestNotification + +NAME = "tests" + +def suite(): + return unittest.TestSuite([ + unittest.makeSuite(TestRestSerializers), + unittest.makeSuite(TestWebsocketSerializers), + unittest.makeSuite(TestLabeler), + unittest.makeSuite(TestNotification), + ]) + +if __name__ == "__main__": + unittest.TextTestRunner().run(suite()) \ No newline at end of file diff --git a/bfxapi/tests/test_labeler.py b/bfxapi/tests/test_labeler.py new file mode 100644 index 0000000..a4310ef --- /dev/null +++ b/bfxapi/tests/test_labeler.py @@ -0,0 +1,56 @@ +import unittest + +from dataclasses import dataclass +from ..exceptions import LabelerSerializerException +from ..labeler import _Type, generate_labeler_serializer, generate_recursive_serializer + +class TestLabeler(unittest.TestCase): + def test_generate_labeler_serializer(self): + @dataclass + class Test(_Type): + A: int + B: float + C: str + + labels = [ "A", "_PLACEHOLDER", "B", "_PLACEHOLDER", "C" ] + + serializer = generate_labeler_serializer("Test", Test, labels) + + self.assertEqual(serializer.parse(5, None, 65.0, None, "X"), Test(5, 65.0, "X"), + msg="_Serializer should produce the right result.") + + self.assertEqual(serializer.parse(5, 65.0, "X", skip=[ "_PLACEHOLDER" ]), Test(5, 65.0, "X"), + msg="_Serializer should produce the right result when skip parameter is given.") + + self.assertListEqual(serializer.get_labels(), [ "A", "B", "C" ], + msg="_Serializer::get_labels() should return the right list of labels.") + + with self.assertRaises(LabelerSerializerException, + msg="_Serializer should raise LabelerSerializerException if given fewer arguments than the serializer labels."): + serializer.parse(5, 65.0, "X") + + def test_generate_recursive_serializer(self): + @dataclass + class Outer(_Type): + A: int + B: float + C: "Middle" + + @dataclass + class Middle(_Type): + D: str + E: "Inner" + + @dataclass + class Inner(_Type): + F: bool + + inner = generate_labeler_serializer("Inner", Inner, ["F"]) + middle = generate_recursive_serializer("Middle", Middle, ["D", "E"], { "E": inner }) + outer = generate_recursive_serializer("Outer", Outer, ["A", "B", "C"], { "C": middle }) + + self.assertEqual(outer.parse(10, 45.5, [ "Y", [ True ] ]), Outer(10, 45.5, Middle("Y", Inner(True))), + msg="_RecursiveSerializer should produce the right result.") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/bfxapi/tests/test_notification.py b/bfxapi/tests/test_notification.py new file mode 100644 index 0000000..f71df60 --- /dev/null +++ b/bfxapi/tests/test_notification.py @@ -0,0 +1,25 @@ +import unittest + +from dataclasses import dataclass +from ..labeler import generate_labeler_serializer +from ..notification import _Type, _Notification, Notification + +class TestNotification(unittest.TestCase): + def test_notification(self): + @dataclass + class Test(_Type): + A: int + B: float + C: str + + test = generate_labeler_serializer("Test", Test, + [ "A", "_PLACEHOLDER", "B", "_PLACEHOLDER", "C" ]) + + notification = _Notification[Test](test) + + self.assertEqual(notification.parse(*[1675787861506, "test", None, None, [ 5, None, 65.0, None, "X" ], 0, "SUCCESS", "This is just a test notification."]), + Notification[Test](1675787861506, "test", None, Test(5, 65.0, "X"), 0, "SUCCESS", "This is just a test notification."), + msg="_Notification should produce the right notification.") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/bfxapi/tests/test_rest_serializers.py b/bfxapi/tests/test_rest_serializers.py new file mode 100644 index 0000000..4c24992 --- /dev/null +++ b/bfxapi/tests/test_rest_serializers.py @@ -0,0 +1,17 @@ +import unittest + +from ..labeler import _Type + +from ..rest import serializers + +class TestRestSerializers(unittest.TestCase): + def test_rest_serializers(self): + for serializer in map(serializers.__dict__.get, serializers.__serializers__): + self.assertTrue(issubclass(serializer.klass, _Type), + f"_Serializer <{serializer.name}>: .klass field must be a subclass of _Type (got {serializer.klass}).") + + self.assertListEqual(serializer.get_labels(), list(serializer.klass.__annotations__), + f"_Serializer <{serializer.name}> and _Type <{serializer.klass.__name__}> must have matching labels and fields.") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/bfxapi/tests/test_websocket_serializers.py b/bfxapi/tests/test_websocket_serializers.py new file mode 100644 index 0000000..a559565 --- /dev/null +++ b/bfxapi/tests/test_websocket_serializers.py @@ -0,0 +1,17 @@ +import unittest + +from ..labeler import _Type + +from ..websocket import serializers + +class TestWebsocketSerializers(unittest.TestCase): + def test_websocket_serializers(self): + for serializer in map(serializers.__dict__.get, serializers.__serializers__): + self.assertTrue(issubclass(serializer.klass, _Type), + f"_Serializer <{serializer.name}>: .klass field must be a subclass of _Type (got {serializer.klass}).") + + self.assertListEqual(serializer.get_labels(), list(serializer.klass.__annotations__), + f"_Serializer <{serializer.name}> and _Type <{serializer.klass.__name__}> must have matching labels and fields.") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/bfxapi/urls.py b/bfxapi/urls.py new file mode 100644 index 0000000..c9a622b --- /dev/null +++ b/bfxapi/urls.py @@ -0,0 +1,7 @@ +REST_HOST = "https://api.bitfinex.com/v2" +PUB_REST_HOST = "https://api-pub.bitfinex.com/v2" +STAGING_REST_HOST = "https://api.staging.bitfinex.com/v2" + +WSS_HOST = "wss://api.bitfinex.com/ws/2" +PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2" +STAGING_WSS_HOST = "wss://api.staging.bitfinex.com/ws/2" \ No newline at end of file diff --git a/bfxapi/utils/JSONEncoder.py b/bfxapi/utils/JSONEncoder.py new file mode 100644 index 0000000..edaba00 --- /dev/null +++ b/bfxapi/utils/JSONEncoder.py @@ -0,0 +1,29 @@ +import json +from decimal import Decimal +from datetime import datetime + +from typing import Type, List, Dict, Union, Any + +JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] + +def _strip(dictionary: Dict) -> Dict: + return { key: value for key, value in dictionary.items() if value != None} + +def _convert_float_to_str(data: JSON) -> JSON: + if isinstance(data, float): + return format(Decimal(repr(data)), "f") + elif isinstance(data, list): + return [ _convert_float_to_str(sub_data) for sub_data in data ] + elif isinstance(data, dict): + return _strip({ key: _convert_float_to_str(value) for key, value in data.items() }) + else: return data + +class JSONEncoder(json.JSONEncoder): + def encode(self, obj: JSON) -> str: + return json.JSONEncoder.encode(self, _convert_float_to_str(obj)) + + def default(self, obj: Any) -> Any: + if isinstance(obj, Decimal): return format(obj, "f") + elif isinstance(obj, datetime): return str(obj) + + return json.JSONEncoder.default(self, obj) \ No newline at end of file diff --git a/bfxapi/utils/camel_and_snake_case_helpers.py b/bfxapi/utils/camel_and_snake_case_helpers.py new file mode 100644 index 0000000..7255940 --- /dev/null +++ b/bfxapi/utils/camel_and_snake_case_helpers.py @@ -0,0 +1,22 @@ +import re + +from typing import TypeVar, Callable, Dict, Any, cast + +T = TypeVar("T") + +_to_snake_case: Callable[[str], str] = lambda string: re.sub(r"(? T: + if isinstance(data, list): + return cast(T, [ _scheme(sub_data, adapter) for sub_data in data ]) + elif isinstance(data, dict): + return cast(T, { adapter(key): _scheme(value, adapter) for key, value in data.items() }) + else: return data + +def to_snake_case_keys(dictionary: T) -> T: + return _scheme(dictionary, _to_snake_case) + +def to_camel_case_keys(dictionary: T) -> T: + return _scheme(dictionary, _to_camel_case) \ No newline at end of file diff --git a/bfxapi/utils/cid.py b/bfxapi/utils/cid.py deleted file mode 100644 index 43150bb..0000000 --- a/bfxapi/utils/cid.py +++ /dev/null @@ -1,4 +0,0 @@ -import time - -def generate_unique_cid(multiplier: int = 1000) -> int: - return int(round(time.time() * multiplier)) diff --git a/bfxapi/utils/encoder.py b/bfxapi/utils/encoder.py deleted file mode 100644 index 3649823..0000000 --- a/bfxapi/utils/encoder.py +++ /dev/null @@ -1,9 +0,0 @@ -import json -from decimal import Decimal -from datetime import datetime - -class JSONEncoder(json.JSONEncoder): - def default(self, obj): - if isinstance(obj, Decimal) or isinstance(obj, datetime): - return str(obj) - return json.JSONEncoder.default(self, obj) \ No newline at end of file diff --git a/bfxapi/utils/flags.py b/bfxapi/utils/flags.py deleted file mode 100644 index f897103..0000000 --- a/bfxapi/utils/flags.py +++ /dev/null @@ -1,29 +0,0 @@ -from .. enums import Flag - -def calculate_order_flags( - hidden : bool = False, - close : bool = False, - reduce_only : bool = False, - post_only : bool = False, - oco : bool = False, - no_var_rates: bool = False -) -> int: - flags = 0 - - if hidden: flags += Flag.HIDDEN - if close: flags += Flag.CLOSE - if reduce_only: flags += Flag.REDUCE_ONLY - if post_only: flags += Flag.POST_ONLY - if oco: flags += Flag.OCO - if no_var_rates: flags += Flag.NO_VAR_RATES - - return flags - -def calculate_offer_flags( - hidden : bool = False -) -> int: - flags = 0 - - if hidden: flags += Flag.HIDDEN - - return flags \ No newline at end of file diff --git a/bfxapi/utils/integers.py b/bfxapi/utils/integers.py deleted file mode 100644 index 08582c6..0000000 --- a/bfxapi/utils/integers.py +++ /dev/null @@ -1,43 +0,0 @@ -from typing import cast, TypeVar, Union - -from .. exceptions import IntegerUnderflowError, IntegerOverflowflowError - -__all__ = [ "Int16", "Int32", "Int45", "Int64" ] - -T = TypeVar("T") - -class _Int(int): - def __new__(cls: T, integer: int) -> T: - assert hasattr(cls, "_BITS"), "_Int must be extended by a class that has a static member _BITS (indicating the number of bits with which to represent the integers)." - - bits = cls._BITS - 1 - - min, max = -(2 ** bits), (2 ** bits) - 1 - - if integer < min: - raise IntegerUnderflowError(f"Underflow. Cannot store <{integer}> in {cls._BITS} bits integer. The min and max bounds are {min} and {max}.") - - if integer > max: - raise IntegerOverflowflowError(f"Overflow. Cannot store <{integer}> in {cls._BITS} bits integer. The min and max bounds are {min} and {max}.") - - return cast(T, super().__new__(int, integer)) - -class Int16(_Int): - _BITS = 16 - -int16 = Union[Int16, int] - -class Int32(_Int): - _BITS = 32 - -int32 = Union[Int32, int] - -class Int45(_Int): - _BITS = 45 - -int45 = Union[Int45, int] - -class Int64(_Int): - _BITS = 64 - -int64 = Union[Int64, int] \ No newline at end of file diff --git a/bfxapi/utils/logger.py b/bfxapi/utils/logger.py index 0ea3894..cf3e970 100644 --- a/bfxapi/utils/logger.py +++ b/bfxapi/utils/logger.py @@ -1,99 +1,52 @@ -""" -Module used to describe all of the different data types -""" - import logging +BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) + RESET_SEQ = "\033[0m" + COLOR_SEQ = "\033[1;%dm" +ITALIC_COLOR_SEQ = "\033[3;%dm" +UNDERLINE_COLOR_SEQ = "\033[4;%dm" + BOLD_SEQ = "\033[1m" -UNDERLINE_SEQ = "\033[04m" - -YELLOW = '\033[93m' -WHITE = '\33[37m' -BLUE = '\033[34m' -LIGHT_BLUE = '\033[94m' -RED = '\033[91m' -GREY = '\33[90m' - -KEYWORD_COLORS = { - 'WARNING': YELLOW, - 'INFO': LIGHT_BLUE, - 'DEBUG': WHITE, - 'CRITICAL': YELLOW, - 'ERROR': RED, - 'TRADE': '\33[102m\33[30m' -} def formatter_message(message, use_color = True): - """ - Syntax highlight certain keywords - """ if use_color: message = message.replace("$RESET", RESET_SEQ).replace("$BOLD", BOLD_SEQ) else: message = message.replace("$RESET", "").replace("$BOLD", "") return message -def format_word(message, word, color_seq, bold=False, underline=False): - """ - Surround the given word with a sequence - """ - replacer = color_seq + word + RESET_SEQ - if underline: - replacer = UNDERLINE_SEQ + replacer - if bold: - replacer = BOLD_SEQ + replacer - return message.replace(word, replacer) +COLORS = { + "DEBUG": CYAN, + "INFO": BLUE, + "WARNING": YELLOW, + "ERROR": RED +} -class Formatter(logging.Formatter): - """ - This Formatted simply colors in the levelname i.e 'INFO', 'DEBUG' - """ - def __init__(self, msg, use_color = True): - logging.Formatter.__init__(self, msg) - self.use_color = use_color +class _ColoredFormatter(logging.Formatter): + def __init__(self, msg, use_color = True): + logging.Formatter.__init__(self, msg, "%d-%m-%Y %H:%M:%S") + self.use_color = use_color - def format(self, record): - """ - Format and highlight certain keywords - """ - levelname = record.levelname - if self.use_color and levelname in KEYWORD_COLORS: - levelname_color = KEYWORD_COLORS[levelname] + levelname + RESET_SEQ - record.levelname = levelname_color - record.name = GREY + record.name + RESET_SEQ - return logging.Formatter.format(self, record) + def format(self, record): + levelname = record.levelname + if self.use_color and levelname in COLORS: + levelname_color = COLOR_SEQ % (30 + COLORS[levelname]) + levelname + RESET_SEQ + record.levelname = levelname_color + record.name = ITALIC_COLOR_SEQ % (30 + BLACK) + record.name + RESET_SEQ + return logging.Formatter.format(self, record) -class CustomLogger(logging.Logger): - """ - This adds extra logging functions such as logger.trade and also - sets the logger to use the custom formatter - """ - FORMAT = "[$BOLD%(name)s$RESET] [%(levelname)s] %(message)s" +class ColoredLogger(logging.Logger): + FORMAT = "[$BOLD%(name)s$RESET] [%(asctime)s] [%(levelname)s] %(message)s" + COLOR_FORMAT = formatter_message(FORMAT, True) - TRADE = 50 + + def __init__(self, name, level): + logging.Logger.__init__(self, name, level) - def __init__(self, name, logLevel='DEBUG'): - logging.Logger.__init__(self, name, logLevel) - color_formatter = Formatter(self.COLOR_FORMAT) + colored_formatter = _ColoredFormatter(self.COLOR_FORMAT) console = logging.StreamHandler() - console.setFormatter(color_formatter) - self.addHandler(console) - logging.addLevelName(self.TRADE, "TRADE") - return + console.setFormatter(colored_formatter) - def set_level(self, level): - logging.Logger.setLevel(self, level) - - def trade(self, message, *args, **kws): - """ - Print a syntax highlighted trade signal - """ - if self.isEnabledFor(self.TRADE): - message = format_word(message, 'CLOSED ', YELLOW, bold=True) - message = format_word(message, 'OPENED ', LIGHT_BLUE, bold=True) - message = format_word(message, 'UPDATED ', BLUE, bold=True) - message = format_word(message, 'CLOSED_ALL ', RED, bold=True) - # Yes, logger takes its '*args' as 'args'. - self._log(self.TRADE, message, args, **kws) \ No newline at end of file + self.addHandler(console) \ No newline at end of file diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py deleted file mode 100644 index c5b2f31..0000000 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ /dev/null @@ -1,231 +0,0 @@ -import traceback, json, asyncio, hmac, hashlib, time, uuid, websockets - -from typing import Literal, TypeVar, Callable, cast - -from pyee.asyncio import AsyncIOEventEmitter - -from ._BfxWebsocketInputs import _BfxWebsocketInputs -from .handlers import Channels, PublicChannelsHandler, AuthenticatedChannelsHandler -from .exceptions import ConnectionNotOpen, TooManySubscriptions, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion - -from ..utils.encoder import JSONEncoder - -from ..utils.logger import Formatter, CustomLogger - -_HEARTBEAT = "hb" - -F = TypeVar("F", bound=Callable[..., Literal[None]]) - -def _require_websocket_connection(function: F) -> F: - async def wrapper(self, *args, **kwargs): - if self.websocket == None or self.websocket.open == False: - raise ConnectionNotOpen("No open connection with the server.") - - await function(self, *args, **kwargs) - - return cast(F, wrapper) - -def _require_websocket_authentication(function: F) -> F: - async def wrapper(self, *args, **kwargs): - if self.authentication == False: - raise WebsocketAuthenticationRequired("To perform this action you need to authenticate using your API_KEY and API_SECRET.") - - await _require_websocket_connection(function)(self, *args, **kwargs) - - return cast(F, wrapper) - -class BfxWebsocketClient(object): - VERSION = 2 - - MAXIMUM_BUCKETS_AMOUNT = 20 - - EVENTS = [ - "open", "subscribed", "authenticated", "wss-error", - *PublicChannelsHandler.EVENTS, - *AuthenticatedChannelsHandler.EVENTS - ] - - def __init__(self, host, buckets=5, log_level = "WARNING", API_KEY=None, API_SECRET=None, filter=None): - self.host, self.websocket, self.event_emitter = host, None, AsyncIOEventEmitter() - - self.event_emitter.add_listener("error", - lambda exception: self.logger.error("\n" + str().join(traceback.format_exception(type(exception), exception, exception.__traceback__))[:-1]) - ) - - self.API_KEY, self.API_SECRET, self.filter, self.authentication = API_KEY, API_SECRET, filter, False - - self.handler = AuthenticatedChannelsHandler(event_emitter=self.event_emitter) - - self.buckets = [ _BfxWebsocketBucket(self.host, self.event_emitter, self.__bucket_open_signal) for _ in range(buckets) ] - - self.inputs = _BfxWebsocketInputs(self.__handle_websocket_input) - - self.logger = CustomLogger("BfxWebsocketClient", logLevel=log_level) - - if buckets > BfxWebsocketClient.MAXIMUM_BUCKETS_AMOUNT: - self.logger.warning(f"It is not safe to use more than {BfxWebsocketClient.MAXIMUM_BUCKETS_AMOUNT} buckets from the same connection ({buckets} in use), the server could momentarily block the client with <429 Too Many Requests>.") - - def run(self): - return asyncio.run(self.start()) - - async def start(self): - tasks = [ bucket._connect(index) for index, bucket in enumerate(self.buckets) ] - - if self.API_KEY != None and self.API_SECRET != None: - tasks.append(self.__connect(self.API_KEY, self.API_SECRET, self.filter)) - - await asyncio.gather(*tasks) - - async def __connect(self, API_KEY, API_SECRET, filter=None): - async for websocket in websockets.connect(self.host): - self.websocket = websocket - - await self.__authenticate(API_KEY, API_SECRET, filter) - - try: - async for message in websocket: - message = json.loads(message) - - if isinstance(message, dict) and message["event"] == "auth": - if message["status"] == "OK": - self.event_emitter.emit("authenticated", message); self.authentication = True - else: raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.") - elif isinstance(message, dict) and message["event"] == "error": - self.event_emitter.emit("wss-error", message["code"], message["msg"]) - elif isinstance(message, list) and (chanId := message[0]) == 0 and message[1] != _HEARTBEAT: - self.handler.handle(message[1], message[2]) - except websockets.ConnectionClosedError: continue - finally: await self.websocket.wait_closed(); break - - async def __authenticate(self, API_KEY, API_SECRET, filter=None): - data = { "event": "auth", "filter": filter, "apiKey": API_KEY } - - data["authNonce"] = int(time.time()) * 1000 - - data["authPayload"] = "AUTH" + str(data["authNonce"]) - - data["authSig"] = hmac.new( - API_SECRET.encode("utf8"), - data["authPayload"].encode("utf8"), - hashlib.sha384 - ).hexdigest() - - await self.websocket.send(json.dumps(data)) - - async def subscribe(self, channel, **kwargs): - counters = [ len(bucket.pendings) + len(bucket.subscriptions) for bucket in self.buckets ] - - index = counters.index(min(counters)) - - await self.buckets[index]._subscribe(channel, **kwargs) - - async def unsubscribe(self, chanId): - for bucket in self.buckets: - if chanId in bucket.subscriptions.keys(): - await bucket._unsubscribe(chanId=chanId) - - async def close(self, code=1000, reason=str()): - if self.websocket != None and self.websocket.open == True: - await self.websocket.close(code=code, reason=reason) - - for bucket in self.buckets: - await bucket._close(code=code, reason=reason) - - @_require_websocket_authentication - async def notify(self, info, MESSAGE_ID=None, **kwargs): - await self.websocket.send(json.dumps([ 0, "n", MESSAGE_ID, { "type": "ucm-test", "info": info, **kwargs } ])) - - @_require_websocket_authentication - async def __handle_websocket_input(self, input, data): - await self.websocket.send(json.dumps([ 0, input, None, data], cls=JSONEncoder)) - - def __bucket_open_signal(self, index): - if all(bucket.websocket != None and bucket.websocket.open == True for bucket in self.buckets): - self.event_emitter.emit("open") - - def on(self, event, callback = None): - if event not in BfxWebsocketClient.EVENTS: - raise EventNotSupported(f"Event <{event}> is not supported. To get a list of available events print BfxWebsocketClient.EVENTS") - - if callback != None: - return self.event_emitter.on(event, callback) - - def handler(function): - self.event_emitter.on(event, function) - return handler - - def once(self, event, callback = None): - if event not in BfxWebsocketClient.EVENTS: - raise EventNotSupported(f"Event <{event}> is not supported. To get a list of available events print BfxWebsocketClient.EVENTS") - - if callback != None: - return self.event_emitter.once(event, callback) - - def handler(function): - self.event_emitter.once(event, function) - return handler - -class _BfxWebsocketBucket(object): - MAXIMUM_SUBSCRIPTIONS_AMOUNT = 25 - - def __init__(self, host, event_emitter, __bucket_open_signal): - self.host, self.event_emitter, self.__bucket_open_signal = host, event_emitter, __bucket_open_signal - - self.websocket, self.subscriptions, self.pendings = None, dict(), list() - - self.handler = PublicChannelsHandler(event_emitter=self.event_emitter) - - async def _connect(self, index): - async for websocket in websockets.connect(self.host): - self.websocket = websocket - - self.__bucket_open_signal(index) - - try: - async for message in websocket: - message = json.loads(message) - - if isinstance(message, dict) and message["event"] == "info" and "version" in message: - if BfxWebsocketClient.VERSION != message["version"]: - raise OutdatedClientVersion(f"Mismatch between the client version and the server version. Update the library to the latest version to continue (client version: {BfxWebsocketClient.VERSION}, server version: {message['version']}).") - elif isinstance(message, dict) and message["event"] == "subscribed" and (chanId := message["chanId"]): - self.pendings = [ pending for pending in self.pendings if pending["subId"] != message["subId"] ] - self.subscriptions[chanId] = message - self.event_emitter.emit("subscribed", message) - elif isinstance(message, dict) and message["event"] == "unsubscribed" and (chanId := message["chanId"]): - if message["status"] == "OK": - del self.subscriptions[chanId] - elif isinstance(message, dict) and message["event"] == "error": - self.event_emitter.emit("wss-error", message["code"], message["msg"]) - elif isinstance(message, list) and (chanId := message[0]) and message[1] != _HEARTBEAT: - self.handler.handle(self.subscriptions[chanId], *message[1:]) - except websockets.ConnectionClosedError: continue - finally: await self.websocket.wait_closed(); break - - @_require_websocket_connection - async def _subscribe(self, channel, subId=None, **kwargs): - if len(self.subscriptions) + len(self.pendings) == _BfxWebsocketBucket.MAXIMUM_SUBSCRIPTIONS_AMOUNT: - raise TooManySubscriptions("The client has reached the maximum number of subscriptions.") - - subscription = { - "event": "subscribe", - "channel": channel, - "subId": subId or str(uuid.uuid4()), - - **kwargs - } - - self.pendings.append(subscription) - - await self.websocket.send(json.dumps(subscription)) - - @_require_websocket_connection - async def _unsubscribe(self, chanId): - await self.websocket.send(json.dumps({ - "event": "unsubscribe", - "chanId": chanId - })) - - @_require_websocket_connection - async def _close(self, code=1000, reason=str()): - await self.websocket.close(code=code, reason=reason) \ No newline at end of file diff --git a/bfxapi/websocket/__init__.py b/bfxapi/websocket/__init__.py index e24f778..1287433 100644 --- a/bfxapi/websocket/__init__.py +++ b/bfxapi/websocket/__init__.py @@ -1 +1,3 @@ -from .BfxWebsocketClient import BfxWebsocketClient \ No newline at end of file +from .client import BfxWebsocketClient, BfxWebsocketBucket, BfxWebsocketInputs + +NAME = "websocket" \ No newline at end of file diff --git a/bfxapi/websocket/client/__init__.py b/bfxapi/websocket/client/__init__.py new file mode 100644 index 0000000..50057cb --- /dev/null +++ b/bfxapi/websocket/client/__init__.py @@ -0,0 +1,5 @@ +from .bfx_websocket_client import BfxWebsocketClient +from .bfx_websocket_bucket import BfxWebsocketBucket +from .bfx_websocket_inputs import BfxWebsocketInputs + +NAME = "client" \ No newline at end of file diff --git a/bfxapi/websocket/client/bfx_websocket_bucket.py b/bfxapi/websocket/client/bfx_websocket_bucket.py new file mode 100644 index 0000000..90c8d21 --- /dev/null +++ b/bfxapi/websocket/client/bfx_websocket_bucket.py @@ -0,0 +1,107 @@ +import json, uuid, websockets + +from typing import Literal, TypeVar, Callable, cast + +from ..handlers import PublicChannelsHandler + +from ..exceptions import ConnectionNotOpen, TooManySubscriptions, OutdatedClientVersion + +_HEARTBEAT = "hb" + +F = TypeVar("F", bound=Callable[..., Literal[None]]) + +def _require_websocket_connection(function: F) -> F: + async def wrapper(self, *args, **kwargs): + if self.websocket == None or self.websocket.open == False: + raise ConnectionNotOpen("No open connection with the server.") + + await function(self, *args, **kwargs) + + return cast(F, wrapper) + +class BfxWebsocketBucket(object): + VERSION = 2 + + MAXIMUM_SUBSCRIPTIONS_AMOUNT = 25 + + def __init__(self, host, event_emitter, on_open_event): + self.host, self.event_emitter, self.on_open_event = host, event_emitter, on_open_event + + self.websocket, self.subscriptions, self.pendings = None, dict(), list() + + self.handler = PublicChannelsHandler(event_emitter=self.event_emitter) + + async def _connect(self, index): + reconnection = False + + async for websocket in websockets.connect(self.host): + self.websocket = websocket + + self.on_open_event.set() + + if reconnection == True or (reconnection := False): + for pending in self.pendings: + await self.websocket.send(json.dumps(pending)) + + for _, subscription in self.subscriptions.items(): + await self._subscribe(**subscription) + + self.subscriptions.clear() + + try: + async for message in websocket: + message = json.loads(message) + + if isinstance(message, dict) and message["event"] == "subscribed" and (chanId := message["chanId"]): + self.pendings = [ pending for pending in self.pendings if pending["subId"] != message["subId"] ] + self.subscriptions[chanId] = message + self.event_emitter.emit("subscribed", message) + elif isinstance(message, dict) and message["event"] == "unsubscribed" and (chanId := message["chanId"]): + if message["status"] == "OK": + del self.subscriptions[chanId] + elif isinstance(message, dict) and message["event"] == "error": + self.event_emitter.emit("wss-error", message["code"], message["msg"]) + elif isinstance(message, list) and (chanId := message[0]) and message[1] != _HEARTBEAT: + self.handler.handle(self.subscriptions[chanId], *message[1:]) + except websockets.ConnectionClosedError as error: + if error.code == 1006: + self.on_open_event.clear() + reconnection = True + continue + + raise error + + break + + @_require_websocket_connection + async def _subscribe(self, channel, subId=None, **kwargs): + if len(self.subscriptions) + len(self.pendings) == BfxWebsocketBucket.MAXIMUM_SUBSCRIPTIONS_AMOUNT: + raise TooManySubscriptions("The client has reached the maximum number of subscriptions.") + + subscription = { + **kwargs, + + "event": "subscribe", + "channel": channel, + "subId": subId or str(uuid.uuid4()), + } + + self.pendings.append(subscription) + + await self.websocket.send(json.dumps(subscription)) + + @_require_websocket_connection + async def _unsubscribe(self, chanId): + await self.websocket.send(json.dumps({ + "event": "unsubscribe", + "chanId": chanId + })) + + @_require_websocket_connection + async def _close(self, code=1000, reason=str()): + await self.websocket.close(code=code, reason=reason) + + def _get_chan_id(self, subId): + for subscription in self.subscriptions.values(): + if subscription["subId"] == subId: + return subscription["chanId"] \ No newline at end of file diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py new file mode 100644 index 0000000..7dc06ed --- /dev/null +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -0,0 +1,246 @@ +import traceback, json, asyncio, hmac, hashlib, time, websockets, socket, random + +from typing import cast + +from collections import namedtuple + +from datetime import datetime + +from pyee.asyncio import AsyncIOEventEmitter + +from .bfx_websocket_bucket import _HEARTBEAT, F, _require_websocket_connection, BfxWebsocketBucket + +from .bfx_websocket_inputs import BfxWebsocketInputs +from ..handlers import PublicChannelsHandler, AuthenticatedChannelsHandler +from ..exceptions import WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion + +from ...utils.JSONEncoder import JSONEncoder + +from ...utils.logger import ColoredLogger + +def _require_websocket_authentication(function: F) -> F: + async def wrapper(self, *args, **kwargs): + if hasattr(self, "authentication") and self.authentication == False: + raise WebsocketAuthenticationRequired("To perform this action you need to authenticate using your API_KEY and API_SECRET.") + + await _require_websocket_connection(function)(self, *args, **kwargs) + + return cast(F, wrapper) + +class BfxWebsocketClient(object): + VERSION = BfxWebsocketBucket.VERSION + + MAXIMUM_CONNECTIONS_AMOUNT = 20 + + EVENTS = [ + "open", "subscribed", "authenticated", "wss-error", + *PublicChannelsHandler.EVENTS, + *AuthenticatedChannelsHandler.EVENTS + ] + + def __init__(self, host, credentials = None, log_level = "INFO"): + self.websocket = None + + self.host, self.credentials, self.event_emitter = host, credentials, AsyncIOEventEmitter() + + self.inputs = BfxWebsocketInputs(handle_websocket_input=self.__handle_websocket_input) + + self.handler = AuthenticatedChannelsHandler(event_emitter=self.event_emitter) + + self.logger = ColoredLogger("BfxWebsocketClient", level=log_level) + + self.event_emitter.add_listener("error", + lambda exception: self.logger.error(f"{type(exception).__name__}: {str(exception)}" + "\n" + + str().join(traceback.format_exception(type(exception), exception, exception.__traceback__))[:-1]) + ) + + def run(self, connections = 5): + return asyncio.run(self.start(connections)) + + async def start(self, connections = 5): + if connections > BfxWebsocketClient.MAXIMUM_CONNECTIONS_AMOUNT: + self.logger.warning(f"It is not safe to use more than {BfxWebsocketClient.MAXIMUM_CONNECTIONS_AMOUNT} buckets from the same " + + f"connection ({connections} in use), the server could momentarily block the client with <429 Too Many Requests>.") + + self.on_open_events = [ asyncio.Event() for _ in range(connections) ] + + self.buckets = [ + BfxWebsocketBucket(self.host, self.event_emitter, self.on_open_events[index]) + for index in range(connections) + ] + + tasks = [ bucket._connect(index) for index, bucket in enumerate(self.buckets) ] + + tasks.append(self.__connect(self.credentials)) + + await asyncio.gather(*tasks) + + async def __connect(self, credentials = None): + Reconnection = namedtuple("Reconnection", ["status", "attempts", "timestamp"]) + + reconnection, delay = Reconnection(status=False, attempts=0, timestamp=None), None + + async def _connection(): + nonlocal reconnection + + async with websockets.connect(self.host) as websocket: + if reconnection.status == True: + self.logger.info(f"Reconnect attempt successful (attempt no.{reconnection.attempts}): The " + + f"client has been offline for a total of {datetime.now() - reconnection.timestamp} " + + f"(connection lost at: {reconnection.timestamp:%d-%m-%Y at %H:%M:%S}).") + + reconnection = Reconnection(status=False, attempts=0, timestamp=None) + + self.websocket, self.authentication = websocket, False + + if await asyncio.gather(*[on_open_event.wait() for on_open_event in self.on_open_events]): + self.event_emitter.emit("open") + + if self.credentials: + await self.__authenticate(**self.credentials) + + async for message in websocket: + message = json.loads(message) + + if isinstance(message, dict) and message["event"] == "info" and "version" in message: + if BfxWebsocketClient.VERSION != message["version"]: + raise OutdatedClientVersion(f"Mismatch between the client version and the server version. " + + f"Update the library to the latest version to continue (client version: {BfxWebsocketClient.VERSION}, " + + f"server version: {message['version']}).") + elif isinstance(message, dict) and message["event"] == "info" and message["code"] == 20051: + rcvd = websockets.frames.Close(code=1012, reason="Stop/Restart Websocket Server (please reconnect).") + + raise websockets.ConnectionClosedError(rcvd=rcvd, sent=None) + elif isinstance(message, dict) and message["event"] == "auth": + if message["status"] == "OK": + self.event_emitter.emit("authenticated", message); self.authentication = True + else: raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.") + elif isinstance(message, dict) and message["event"] == "error": + self.event_emitter.emit("wss-error", message["code"], message["msg"]) + elif isinstance(message, list) and (chanId := message[0]) == 0 and message[1] != _HEARTBEAT: + self.handler.handle(message[1], message[2]) + + class _Delay: + BACKOFF_MIN, BACKOFF_MAX = 1.92, 60.0 + + BACKOFF_INITIAL = 5.0 + + def __init__(self, backoff_factor): + self.__backoff_factor = backoff_factor + self.__backoff_delay = _Delay.BACKOFF_MIN + self.__initial_delay = random.random() * _Delay.BACKOFF_INITIAL + + def next(self): + backoff_delay = self.peek() + __backoff_delay = self.__backoff_delay * self.__backoff_factor + self.__backoff_delay = min(__backoff_delay, _Delay.BACKOFF_MAX) + + return backoff_delay + + def peek(self): + return (self.__backoff_delay == _Delay.BACKOFF_MIN) \ + and self.__initial_delay or self.__backoff_delay + + while True: + if reconnection.status == True: + await asyncio.sleep(delay.next()) + + try: + await _connection() + except (websockets.ConnectionClosedError, socket.gaierror) as error: + if isinstance(error, websockets.ConnectionClosedError) and (error.code == 1006 or error.code == 1012): + if error.code == 1006: + self.logger.error("Connection lost: no close frame received " + + "or sent (1006). Attempting to reconnect...") + + if error.code == 1012: + self.logger.info("WSS server is about to restart, reconnection " + + "required (client received 20051). Attempt in progress...") + + reconnection = Reconnection(status=True, attempts=1, timestamp=datetime.now()); + + delay = _Delay(backoff_factor=1.618) + elif isinstance(error, socket.gaierror) and reconnection.status == True: + self.logger.warning(f"Reconnection attempt no.{reconnection.attempts} has failed. " + + f"Next reconnection attempt in ~{round(delay.peek()):.1f} seconds." + + f"(at the moment the client has been offline for {datetime.now() - reconnection.timestamp})") + + reconnection = reconnection._replace(attempts=reconnection.attempts + 1) + else: raise error + + if reconnection.status == False: + break + + async def __authenticate(self, API_KEY, API_SECRET, filter=None): + data = { "event": "auth", "filter": filter, "apiKey": API_KEY } + + data["authNonce"] = int(time.time()) * 1000 + + data["authPayload"] = "AUTH" + str(data["authNonce"]) + + data["authSig"] = hmac.new( + API_SECRET.encode("utf8"), + data["authPayload"].encode("utf8"), + hashlib.sha384 + ).hexdigest() + + await self.websocket.send(json.dumps(data)) + + async def subscribe(self, channel, **kwargs): + counters = [ len(bucket.pendings) + len(bucket.subscriptions) for bucket in self.buckets ] + + index = counters.index(min(counters)) + + await self.buckets[index]._subscribe(channel, **kwargs) + + async def unsubscribe(self, subId): + for bucket in self.buckets: + if (chanId := bucket._get_chan_id(subId)): + await bucket._unsubscribe(chanId=chanId) + + async def close(self, code=1000, reason=str()): + if self.websocket != None and self.websocket.open == True: + await self.websocket.close(code=code, reason=reason) + + for bucket in self.buckets: + await bucket._close(code=code, reason=reason) + + @_require_websocket_authentication + async def notify(self, info, MESSAGE_ID=None, **kwargs): + await self.websocket.send(json.dumps([ 0, "n", MESSAGE_ID, { "type": "ucm-test", "info": info, **kwargs } ])) + + @_require_websocket_authentication + async def __handle_websocket_input(self, input, data): + await self.websocket.send(json.dumps([ 0, input, None, data], cls=JSONEncoder)) + + def on(self, *events, callback = None): + for event in events: + if event not in BfxWebsocketClient.EVENTS: + raise EventNotSupported(f"Event <{event}> is not supported. To get a list of available events print BfxWebsocketClient.EVENTS") + + if callback != None: + for event in events: + self.event_emitter.on(event, callback) + + if callback == None: + def handler(function): + for event in events: + self.event_emitter.on(event, function) + + return handler + + def once(self, *events, callback = None): + for event in events: + if event not in BfxWebsocketClient.EVENTS: + raise EventNotSupported(f"Event <{event}> is not supported. To get a list of available events print BfxWebsocketClient.EVENTS") + + if callback != None: + for event in events: + self.event_emitter.once(event, callback) + + if callback == None: + def handler(function): + for event in events: + self.event_emitter.once(event, function) + + return handler \ No newline at end of file diff --git a/bfxapi/websocket/_BfxWebsocketInputs.py b/bfxapi/websocket/client/bfx_websocket_inputs.py similarity index 51% rename from bfxapi/websocket/_BfxWebsocketInputs.py rename to bfxapi/websocket/client/bfx_websocket_inputs.py index fda6e19..4b4e04c 100644 --- a/bfxapi/websocket/_BfxWebsocketInputs.py +++ b/bfxapi/websocket/client/bfx_websocket_inputs.py @@ -2,77 +2,59 @@ from decimal import Decimal from datetime import datetime from typing import Union, Optional, List, Tuple -from .typings import JSON -from .enums import OrderType, FundingOfferType +from .. enums import OrderType, FundingOfferType +from ... utils.JSONEncoder import JSON -def _strip(dictionary): - return { key: value for key, value in dictionary.items() if value != None} +class BfxWebsocketInputs(object): + def __init__(self, handle_websocket_input): + self.handle_websocket_input = handle_websocket_input -class _BfxWebsocketInputs(object): - def __init__(self, __handle_websocket_input): - self.__handle_websocket_input = __handle_websocket_input - - async def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str], - price: Optional[Union[Decimal, str]] = None, lev: Optional[int] = None, - price_trailing: Optional[Union[Decimal, str]] = None, price_aux_limit: Optional[Union[Decimal, str]] = None, price_oco_stop: Optional[Union[Decimal, str]] = None, + async def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, float, str], + price: Optional[Union[Decimal, float, str]] = None, lev: Optional[int] = None, + price_trailing: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_oco_stop: Optional[Union[Decimal, float, str]] = None, gid: Optional[int] = None, cid: Optional[int] = None, flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None): - data = _strip({ + await self.handle_websocket_input("on", { "type": type, "symbol": symbol, "amount": amount, "price": price, "lev": lev, "price_trailing": price_trailing, "price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop, "gid": gid, "cid": cid, "flags": flags, "tif": tif, "meta": meta }) - - await self.__handle_websocket_input("on", data) - async def update_order(self, id: int, amount: Optional[Union[Decimal, str]] = None, price: Optional[Union[Decimal, str]] = None, + async def update_order(self, id: int, amount: Optional[Union[Decimal, float, str]] = None, price: Optional[Union[Decimal, float, str]] = None, cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, - flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, str]] = None, - price_aux_limit: Optional[Union[Decimal, str]] = None, price_trailing: Optional[Union[Decimal, str]] = None, tif: Optional[Union[datetime, str]] = None): - data = _strip({ + flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, float, str]] = None, + price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, tif: Optional[Union[datetime, str]] = None): + await self.handle_websocket_input("ou", { "id": id, "amount": amount, "price": price, "cid": cid, "cid_date": cid_date, "gid": gid, "flags": flags, "lev": lev, "delta": delta, "price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif }) - - await self.__handle_websocket_input("ou", data) async def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None): - data = _strip({ - "id": id, - "cid": cid, - "cid_date": cid_date + await self.handle_websocket_input("oc", { + "id": id, "cid": cid, "cid_date": cid_date }) - await self.__handle_websocket_input("oc", data) - async def cancel_order_multi(self, ids: Optional[List[int]] = None, cids: Optional[List[Tuple[int, str]]] = None, gids: Optional[List[int]] = None, all: bool = False): - data = _strip({ - "ids": ids, - "cids": cids, - "gids": gids, - + await self.handle_websocket_input("oc_multi", { + "ids": ids, "cids": cids, "gids": gids, "all": int(all) }) - - await self.__handle_websocket_input("oc_multi", data) - async def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, str], - rate: Union[Decimal, str], period: int, + async def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, float, str], + rate: Union[Decimal, float, str], period: int, flags: Optional[int] = 0): - data = { + await self.handle_websocket_input("fon", { "type": type, "symbol": symbol, "amount": amount, "rate": rate, "period": period, "flags": flags - } - - await self.__handle_websocket_input("fon", data) + }) async def cancel_funding_offer(self, id: int): - await self.__handle_websocket_input("foc", { "id": id }) + await self.handle_websocket_input("foc", { "id": id }) async def calc(self, *args: str): - await self.__handle_websocket_input("calc", list(map(lambda arg: [arg], args))) \ No newline at end of file + await self.handle_websocket_input("calc", list(map(lambda arg: [arg], args))) \ No newline at end of file diff --git a/bfxapi/websocket/enums.py b/bfxapi/websocket/enums.py index 8f06f62..1877cea 100644 --- a/bfxapi/websocket/enums.py +++ b/bfxapi/websocket/enums.py @@ -1,6 +1,6 @@ -from ..enums import * +from .. enums import * -class Channels(str, Enum): +class Channel(str, Enum): TICKER = "ticker" TRADES = "trades" BOOK = "book" diff --git a/bfxapi/websocket/exceptions.py b/bfxapi/websocket/exceptions.py index 5691af8..40a6a1e 100644 --- a/bfxapi/websocket/exceptions.py +++ b/bfxapi/websocket/exceptions.py @@ -58,4 +58,11 @@ class InvalidAuthenticationCredentials(BfxWebsocketException): This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication. """ + pass + +class HandlerNotFound(BfxWebsocketException): + """ + This error indicates that a handler was not found for an incoming message. + """ + pass \ No newline at end of file diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py deleted file mode 100644 index 83c1408..0000000 --- a/bfxapi/websocket/handlers.py +++ /dev/null @@ -1,186 +0,0 @@ -from . import serializers -from .enums import Channels -from .exceptions import BfxWebsocketException - -def _get_sub_dictionary(dictionary, keys): - return { key: dictionary[key] for key in dictionary if key in keys } - -class PublicChannelsHandler(object): - EVENTS = [ - "t_ticker_update", "f_ticker_update", - "t_trade_executed", "t_trade_execution_update", "f_trade_executed", "f_trade_execution_update", "t_trades_snapshot", "f_trades_snapshot", - "t_book_snapshot", "f_book_snapshot", "t_raw_book_snapshot", "f_raw_book_snapshot", "t_book_update", "f_book_update", "t_raw_book_update", "f_raw_book_update", - "candles_snapshot", "candles_update", - "derivatives_status_update", - ] - - def __init__(self, event_emitter): - self.event_emitter = event_emitter - - self.__handlers = { - Channels.TICKER: self.__ticker_channel_handler, - Channels.TRADES: self.__trades_channel_handler, - Channels.BOOK: self.__book_channel_handler, - Channels.CANDLES: self.__candles_channel_handler, - Channels.STATUS: self.__status_channel_handler - } - - def handle(self, subscription, *stream): - if channel := subscription["channel"] or channel in self.__handlers.keys(): - return self.__handlers[channel](subscription, *stream) - - def __ticker_channel_handler(self, subscription, *stream): - if subscription["symbol"].startswith("t"): - return self.event_emitter.emit( - "t_ticker_update", - _get_sub_dictionary(subscription, [ "chanId", "symbol", "pair" ]), - serializers.TradingPairTicker.parse(*stream[0]) - ) - - if subscription["symbol"].startswith("f"): - return self.event_emitter.emit( - "f_ticker_update", - _get_sub_dictionary(subscription, [ "chanId", "symbol", "currency" ]), - serializers.FundingCurrencyTicker.parse(*stream[0]) - ) - - def __trades_channel_handler(self, subscription, *stream): - if type := stream[0] or type in [ "te", "tu", "fte", "ftu" ]: - if subscription["symbol"].startswith("t"): - return self.event_emitter.emit( - { "te": "t_trade_executed", "tu": "t_trade_execution_update" }[type], - _get_sub_dictionary(subscription, [ "chanId", "symbol", "pair" ]), - serializers.TradingPairTrade.parse(*stream[1]) - ) - - if subscription["symbol"].startswith("f"): - return self.event_emitter.emit( - { "fte": "f_trade_executed", "ftu": "f_trade_execution_update" }[type], - _get_sub_dictionary(subscription, [ "chanId", "symbol", "currency" ]), - serializers.FundingCurrencyTrade.parse(*stream[1]) - ) - - if subscription["symbol"].startswith("t"): - return self.event_emitter.emit( - "t_trades_snapshot", - _get_sub_dictionary(subscription, [ "chanId", "symbol", "pair" ]), - [ serializers.TradingPairTrade.parse(*substream) for substream in stream[0] ] - ) - - if subscription["symbol"].startswith("f"): - return self.event_emitter.emit( - "f_trades_snapshot", - _get_sub_dictionary(subscription, [ "chanId", "symbol", "currency" ]), - [ serializers.FundingCurrencyTrade.parse(*substream) for substream in stream[0] ] - ) - - def __book_channel_handler(self, subscription, *stream): - subscription = _get_sub_dictionary(subscription, [ "chanId", "symbol", "prec", "freq", "len", "subId", "pair" ]) - - type = subscription["symbol"][0] - - if subscription["prec"] == "R0": - _trading_pair_serializer, _funding_currency_serializer, IS_RAW_BOOK = serializers.TradingPairRawBook, serializers.FundingCurrencyRawBook, True - else: _trading_pair_serializer, _funding_currency_serializer, IS_RAW_BOOK = serializers.TradingPairBook, serializers.FundingCurrencyBook, False - - if all(isinstance(substream, list) for substream in stream[0]): - return self.event_emitter.emit( - type + "_" + (IS_RAW_BOOK and "raw_book" or "book") + "_snapshot", - subscription, - [ { "t": _trading_pair_serializer, "f": _funding_currency_serializer }[type].parse(*substream) for substream in stream[0] ] - ) - - return self.event_emitter.emit( - type + "_" + (IS_RAW_BOOK and "raw_book" or "book") + "_update", - subscription, - { "t": _trading_pair_serializer, "f": _funding_currency_serializer }[type].parse(*stream[0]) - ) - - def __candles_channel_handler(self, subscription, *stream): - subscription = _get_sub_dictionary(subscription, [ "chanId", "key" ]) - - if all(isinstance(substream, list) for substream in stream[0]): - return self.event_emitter.emit( - "candles_snapshot", - subscription, - [ serializers.Candle.parse(*substream) for substream in stream[0] ] - ) - - return self.event_emitter.emit( - "candles_update", - subscription, - serializers.Candle.parse(*stream[0]) - ) - - def __status_channel_handler(self, subscription, *stream): - subscription = _get_sub_dictionary(subscription, [ "chanId", "key" ]) - - if subscription["key"].startswith("deriv:"): - return self.event_emitter.emit( - "derivatives_status_update", - subscription, - serializers.DerivativesStatus.parse(*stream[0]) - ) - -class AuthenticatedChannelsHandler(object): - __abbreviations = { - "os": "order_snapshot", "on": "order_new", "ou": "order_update", "oc": "order_cancel", - "ps": "position_snapshot", "pn": "position_new", "pu": "position_update", "pc": "position_close", - "te": "trade_executed", "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", - "bu": "balance_update", - } - - __serializers = { - ("os", "on", "ou", "oc",): serializers.Order, - ("ps", "pn", "pu", "pc",): serializers.Position, - ("te",): serializers.TradeExecuted, - ("tu",): serializers.TradeExecutionUpdate, - ("fos", "fon", "fou", "foc",): serializers.FundingOffer, - ("fcs", "fcn", "fcu", "fcc",): serializers.FundingCredit, - ("fls", "fln", "flu", "flc",): serializers.FundingLoan, - ("ws", "wu",): serializers.Wallet, - ("bu",): serializers.BalanceInfo - } - - EVENTS = [ - "notification", - "on-req-notification", "ou-req-notification", "oc-req-notification", - "oc_multi-notification", - "fon-req-notification", "foc-req-notification", - *list(__abbreviations.values()) - ] - - def __init__(self, event_emitter, strict = False): - self.event_emitter, self.strict = event_emitter, strict - - def handle(self, type, stream): - if type == "n": - return self.__notification(stream) - - for types, serializer in AuthenticatedChannelsHandler.__serializers.items(): - if type in types: - event = AuthenticatedChannelsHandler.__abbreviations[type] - - if all(isinstance(substream, list) for substream in stream): - return self.event_emitter.emit(event, [ serializer.parse(*substream) for substream in stream ]) - - return self.event_emitter.emit(event, serializer.parse(*stream)) - - if self.strict == True: - raise BfxWebsocketException(f"Event of type <{type}> not found in self.__handlers.") - - def __notification(self, stream): - if stream[1] == "on-req" or stream[1] == "ou-req" or stream[1] == "oc-req": - return self.event_emitter.emit(f"{stream[1]}-notification", serializers._Notification(serializer=serializers.Order).parse(*stream)) - - if stream[1] == "oc_multi-req": - return self.event_emitter.emit(f"{stream[1]}-notification", serializers._Notification(serializer=serializers.Order, iterate=True).parse(*stream)) - - if stream[1] == "fon-req" or stream[1] == "foc-req": - return self.event_emitter.emit(f"{stream[1]}-notification", serializers._Notification(serializer=serializers.FundingOffer).parse(*stream)) - - return self.event_emitter.emit("notification", serializers._Notification(serializer=None).parse(*stream)) \ No newline at end of file diff --git a/bfxapi/websocket/handlers/__init__.py b/bfxapi/websocket/handlers/__init__.py new file mode 100644 index 0000000..02e9c81 --- /dev/null +++ b/bfxapi/websocket/handlers/__init__.py @@ -0,0 +1,4 @@ +from .public_channels_handler import PublicChannelsHandler +from .authenticated_channels_handler import AuthenticatedChannelsHandler + +NAME = "handlers" \ No newline at end of file diff --git a/bfxapi/websocket/handlers/authenticated_channels_handler.py b/bfxapi/websocket/handlers/authenticated_channels_handler.py new file mode 100644 index 0000000..2dbd83f --- /dev/null +++ b/bfxapi/websocket/handlers/authenticated_channels_handler.py @@ -0,0 +1,69 @@ +from .. import serializers + +from .. types import * + +from .. exceptions import HandlerNotFound + +class AuthenticatedChannelsHandler(object): + __abbreviations = { + "os": "order_snapshot", "on": "order_new", "ou": "order_update", "oc": "order_cancel", + "ps": "position_snapshot", "pn": "position_new", "pu": "position_update", "pc": "position_close", + "te": "trade_executed", "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", + "bu": "balance_update", + } + + __serializers = { + ("os", "on", "ou", "oc",): serializers.Order, + ("ps", "pn", "pu", "pc",): serializers.Position, + ("te", "tu"): serializers.Trade, + ("fos", "fon", "fou", "foc",): serializers.FundingOffer, + ("fcs", "fcn", "fcu", "fcc",): serializers.FundingCredit, + ("fls", "fln", "flu", "flc",): serializers.FundingLoan, + ("ws", "wu",): serializers.Wallet, + ("bu",): serializers.Balance + } + + EVENTS = [ + "notification", + "on-req-notification", "ou-req-notification", "oc-req-notification", + "oc_multi-notification", + "fon-req-notification", "foc-req-notification", + *list(__abbreviations.values()) + ] + + def __init__(self, event_emitter, strict = True): + self.event_emitter, self.strict = event_emitter, strict + + def handle(self, type, stream): + if type == "n": + return self.__notification(stream) + + for types, serializer in AuthenticatedChannelsHandler.__serializers.items(): + if type in types: + event = AuthenticatedChannelsHandler.__abbreviations[type] + + if all(isinstance(substream, list) for substream in stream): + return self.event_emitter.emit(event, [ serializer.parse(*substream) for substream in stream ]) + + return self.event_emitter.emit(event, serializer.parse(*stream)) + + if self.strict: + raise HandlerNotFound(f"No handler found for event of type <{type}>.") + + def __notification(self, stream): + type, serializer = "notification", serializers._Notification(serializer=None) + + if stream[1] == "on-req" or stream[1] == "ou-req" or stream[1] == "oc-req": + type, serializer = f"{stream[1]}-notification", serializers._Notification(serializer=serializers.Order) + + if stream[1] == "oc_multi-req": + type, serializer = f"{stream[1]}-notification", serializers._Notification(serializer=serializers.Order, iterate=True) + + if stream[1] == "fon-req" or stream[1] == "foc-req": + type, serializer = f"{stream[1]}-notification", serializers._Notification(serializer=serializers.FundingOffer) + + return self.event_emitter.emit(type, serializer.parse(*stream)) \ No newline at end of file diff --git a/bfxapi/websocket/handlers/public_channels_handler.py b/bfxapi/websocket/handlers/public_channels_handler.py new file mode 100644 index 0000000..52e47ef --- /dev/null +++ b/bfxapi/websocket/handlers/public_channels_handler.py @@ -0,0 +1,121 @@ +from .. import serializers + +from .. types import * + +from .. exceptions import HandlerNotFound + +class PublicChannelsHandler(object): + EVENTS = [ + "t_ticker_update", "f_ticker_update", + "t_trade_executed", "t_trade_execution_update", "f_trade_executed", "f_trade_execution_update", "t_trades_snapshot", "f_trades_snapshot", + "t_book_snapshot", "f_book_snapshot", "t_raw_book_snapshot", "f_raw_book_snapshot", "t_book_update", "f_book_update", "t_raw_book_update", "f_raw_book_update", + "candles_snapshot", "candles_update", + "derivatives_status_update", + ] + + def __init__(self, event_emitter, strict = True): + self.event_emitter, self.strict = event_emitter, strict + + self.__handlers = { + "ticker": self.__ticker_channel_handler, + "trades": self.__trades_channel_handler, + "book": self.__book_channel_handler, + "candles": self.__candles_channel_handler, + "status": self.__status_channel_handler + } + + def handle(self, subscription, *stream): + _clear = lambda dictionary, *args: { key: value for key, value in dictionary.items() if key not in args } + + if (channel := subscription["channel"]) and channel in self.__handlers.keys(): + return self.__handlers[channel](_clear(subscription, "event", "channel", "chanId"), *stream) + + if self.strict: + raise HandlerNotFound(f"No handler found for channel <{subscription['channel']}>.") + + def __ticker_channel_handler(self, subscription, *stream): + if subscription["symbol"].startswith("t"): + return self.event_emitter.emit( + "t_ticker_update", + subscription, + serializers.TradingPairTicker.parse(*stream[0]) + ) + + if subscription["symbol"].startswith("f"): + return self.event_emitter.emit( + "f_ticker_update", + subscription, + serializers.FundingCurrencyTicker.parse(*stream[0]) + ) + + def __trades_channel_handler(self, subscription, *stream): + if (type := stream[0]) and type in [ "te", "tu", "fte", "ftu" ]: + if subscription["symbol"].startswith("t"): + return self.event_emitter.emit( + { "te": "t_trade_executed", "tu": "t_trade_execution_update" }[type], + subscription, + serializers.TradingPairTrade.parse(*stream[1]) + ) + + if subscription["symbol"].startswith("f"): + return self.event_emitter.emit( + { "fte": "f_trade_executed", "ftu": "f_trade_execution_update" }[type], + subscription, + serializers.FundingCurrencyTrade.parse(*stream[1]) + ) + + if subscription["symbol"].startswith("t"): + return self.event_emitter.emit( + "t_trades_snapshot", + subscription, + [ serializers.TradingPairTrade.parse(*substream) for substream in stream[0] ] + ) + + if subscription["symbol"].startswith("f"): + return self.event_emitter.emit( + "f_trades_snapshot", + subscription, + [ serializers.FundingCurrencyTrade.parse(*substream) for substream in stream[0] ] + ) + + def __book_channel_handler(self, subscription, *stream): + type = subscription["symbol"][0] + + if subscription["prec"] == "R0": + _trading_pair_serializer, _funding_currency_serializer, IS_RAW_BOOK = serializers.TradingPairRawBook, serializers.FundingCurrencyRawBook, True + else: _trading_pair_serializer, _funding_currency_serializer, IS_RAW_BOOK = serializers.TradingPairBook, serializers.FundingCurrencyBook, False + + if all(isinstance(substream, list) for substream in stream[0]): + return self.event_emitter.emit( + type + "_" + (IS_RAW_BOOK and "raw_book" or "book") + "_snapshot", + subscription, + [ { "t": _trading_pair_serializer, "f": _funding_currency_serializer }[type].parse(*substream) for substream in stream[0] ] + ) + + return self.event_emitter.emit( + type + "_" + (IS_RAW_BOOK and "raw_book" or "book") + "_update", + subscription, + { "t": _trading_pair_serializer, "f": _funding_currency_serializer }[type].parse(*stream[0]) + ) + + def __candles_channel_handler(self, subscription, *stream): + if all(isinstance(substream, list) for substream in stream[0]): + return self.event_emitter.emit( + "candles_snapshot", + subscription, + [ serializers.Candle.parse(*substream) for substream in stream[0] ] + ) + + return self.event_emitter.emit( + "candles_update", + subscription, + serializers.Candle.parse(*stream[0]) + ) + + def __status_channel_handler(self, subscription, *stream): + if subscription["key"].startswith("deriv:"): + return self.event_emitter.emit( + "derivatives_status_update", + subscription, + serializers.DerivativesStatus.parse(*stream[0]) + ) \ No newline at end of file diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index a9dd805..29c28c7 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -1,297 +1,293 @@ -from . import typings +from . import types -from .. labeler import _Serializer +from .. labeler import generate_labeler_serializer from .. notification import _Notification +__serializers__ = [ + "TradingPairTicker", "FundingCurrencyTicker", "TradingPairTrade", + "FundingCurrencyTrade", "TradingPairBook", "FundingCurrencyBook", + "TradingPairRawBook", "FundingCurrencyRawBook", "Candle", + "DerivativesStatus", + + "Order", "Position", "Trade", + "FundingOffer", "FundingCredit", "FundingLoan", + "Wallet", "Balance", +] + #region Serializers definition for Websocket Public Channels -TradingPairTicker = _Serializer[typings.TradingPairTicker]("TradingPairTicker", labels=[ - "BID", - "BID_SIZE", - "ASK", - "ASK_SIZE", - "DAILY_CHANGE", - "DAILY_CHANGE_RELATIVE", - "LAST_PRICE", - "VOLUME", - "HIGH", - "LOW" +TradingPairTicker = generate_labeler_serializer("TradingPairTicker", klass=types.TradingPairTicker, labels=[ + "bid", + "bid_size", + "ask", + "ask_size", + "daily_change", + "daily_change_relative", + "last_price", + "volume", + "high", + "low" ]) -FundingCurrencyTicker = _Serializer[typings.FundingCurrencyTicker]("FundingCurrencyTicker", labels=[ - "FRR", - "BID", - "BID_PERIOD", - "BID_SIZE", - "ASK", - "ASK_PERIOD", - "ASK_SIZE", - "DAILY_CHANGE", - "DAILY_CHANGE_RELATIVE", - "LAST_PRICE", - "VOLUME", - "HIGH", - "LOW" +FundingCurrencyTicker = generate_labeler_serializer("FundingCurrencyTicker", klass=types.FundingCurrencyTicker, labels=[ + "frr", + "bid", + "bid_period", + "bid_size", + "ask", + "ask_period", + "ask_size", + "daily_change", + "daily_change_relative", + "last_price", + "volume", + "high", + "low", "_PLACEHOLDER", "_PLACEHOLDER", - "FRR_AMOUNT_AVAILABLE" + "frr_amount_available" ]) -TradingPairTrade = _Serializer[typings.TradingPairTrade]("TradingPairTrade", labels=[ - "ID", - "MTS", - "AMOUNT", - "PRICE" +TradingPairTrade = generate_labeler_serializer("TradingPairTrade", klass=types.TradingPairTrade, labels=[ + "id", + "mts", + "amount", + "price" ]) -FundingCurrencyTrade = _Serializer[typings.FundingCurrencyTrade]("FundingCurrencyTrade", labels=[ - "ID", - "MTS", - "AMOUNT", - "RATE", - "PERIOD" +FundingCurrencyTrade = generate_labeler_serializer("FundingCurrencyTrade", klass=types.FundingCurrencyTrade, labels=[ + "id", + "mts", + "amount", + "rate", + "period" ]) -TradingPairBook = _Serializer[typings.TradingPairBook]("TradingPairBook", labels=[ - "PRICE", - "COUNT", - "AMOUNT" +TradingPairBook = generate_labeler_serializer("TradingPairBook", klass=types.TradingPairBook, labels=[ + "price", + "count", + "amount" ]) -FundingCurrencyBook = _Serializer[typings.FundingCurrencyBook]("FundingCurrencyBook", labels=[ - "RATE", - "PERIOD", - "COUNT", - "AMOUNT" +FundingCurrencyBook = generate_labeler_serializer("FundingCurrencyBook", klass=types.FundingCurrencyBook, labels=[ + "rate", + "period", + "count", + "amount" ]) -TradingPairRawBook = _Serializer[typings.TradingPairRawBook]("TradingPairRawBook", labels=[ - "ORDER_ID", - "PRICE", - "AMOUNT" +TradingPairRawBook = generate_labeler_serializer("TradingPairRawBook", klass=types.TradingPairRawBook, labels=[ + "order_id", + "price", + "amount" ]) -FundingCurrencyRawBook = _Serializer[typings.FundingCurrencyRawBook]("FundingCurrencyRawBook", labels=[ - "OFFER_ID", - "PERIOD", - "RATE", - "AMOUNT" +FundingCurrencyRawBook = generate_labeler_serializer("FundingCurrencyRawBook", klass=types.FundingCurrencyRawBook, labels=[ + "offer_id", + "period", + "rate", + "amount" ]) -Candle = _Serializer[typings.Candle]("Candle", labels=[ - "MTS", - "OPEN", - "CLOSE", - "HIGH", - "LOW", - "VOLUME" +Candle = generate_labeler_serializer("Candle", klass=types.Candle, labels=[ + "mts", + "open", + "close", + "high", + "low", + "volume" ]) -DerivativesStatus = _Serializer[typings.DerivativesStatus]("DerivativesStatus", labels=[ - "TIME_MS", +DerivativesStatus = generate_labeler_serializer("DerivativesStatus", klass=types.DerivativesStatus, labels=[ + "mts", "_PLACEHOLDER", - "DERIV_PRICE", - "SPOT_PRICE", + "deriv_price", + "spot_price", "_PLACEHOLDER", - "INSURANCE_FUND_BALANCE", + "insurance_fund_balance", "_PLACEHOLDER", - "NEXT_FUNDING_EVT_TIMESTAMP_MS", - "NEXT_FUNDING_ACCRUED", - "NEXT_FUNDING_STEP", + "next_funding_evt_timestamp_ms", + "next_funding_accrued", + "next_funding_step", "_PLACEHOLDER", - "CURRENT_FUNDING" + "current_funding", "_PLACEHOLDER", "_PLACEHOLDER", - "MARK_PRICE", + "mark_price", "_PLACEHOLDER", "_PLACEHOLDER", - "OPEN_INTEREST", + "open_interest", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "CLAMP_MIN", - "CLAMP_MAX" + "clamp_min", + "clamp_max" ]) #endregion #region Serializers definition for Websocket Authenticated Channels -Order = _Serializer[typings.Order]("Order", labels=[ - "ID", - "GID", - "CID", - "SYMBOL", - "MTS_CREATE", - "MTS_UPDATE", - "AMOUNT", - "AMOUNT_ORIG", - "ORDER_TYPE", - "TYPE_PREV", - "MTS_TIF", +Order = generate_labeler_serializer("Order", klass=types.Order, labels=[ + "id", + "gid", + "cid", + "symbol", + "mts_create", + "mts_update", + "amount", + "amount_orig", + "order_type", + "type_prev", + "mts_tif", "_PLACEHOLDER", - "FLAGS", - "ORDER_STATUS", + "flags", + "order_status", "_PLACEHOLDER", "_PLACEHOLDER", - "PRICE", - "PRICE_AVG", - "PRICE_TRAILING", - "PRICE_AUX_LIMIT", + "price", + "price_avg", + "price_trailing", + "price_aux_limit", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "NOTIFY", - "HIDDEN", - "PLACED_ID", + "notify", + "hidden", + "placed_id", "_PLACEHOLDER", "_PLACEHOLDER", - "ROUTING", + "routing", "_PLACEHOLDER", "_PLACEHOLDER", - "META" + "meta" ]) -Position = _Serializer[typings.Position]("Position", labels=[ - "SYMBOL", - "STATUS", - "AMOUNT", - "BASE_PRICE", - "MARGIN_FUNDING", - "MARGIN_FUNDING_TYPE", - "PL", - "PL_PERC", - "PRICE_LIQ", - "LEVERAGE", - "FLAG", - "POSITION_ID", - "MTS_CREATE", - "MTS_UPDATE", +Position = generate_labeler_serializer("Position", klass=types.Position, labels=[ + "symbol", + "status", + "amount", + "base_price", + "margin_funding", + "margin_funding_type", + "pl", + "pl_perc", + "price_liq", + "leverage", + "flag", + "position_id", + "mts_create", + "mts_update", "_PLACEHOLDER", - "TYPE", + "type", "_PLACEHOLDER", - "COLLATERAL", - "COLLATERAL_MIN", - "META" + "collateral", + "collateral_min", + "meta" ]) -TradeExecuted = _Serializer[typings.TradeExecuted]("TradeExecuted", labels=[ - "ID", - "SYMBOL", - "MTS_CREATE", - "ORDER_ID", - "EXEC_AMOUNT", - "EXEC_PRICE", - "ORDER_TYPE", - "ORDER_PRICE", - "MAKER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "CID" +Trade = generate_labeler_serializer("Trade", klass=types.Trade, labels=[ + "id", + "symbol", + "mts_create", + "order_id", + "exec_amount", + "exec_price", + "order_type", + "order_price", + "maker", + "fee", + "fee_currency", + "cid" ]) -TradeExecutionUpdate = _Serializer[typings.TradeExecutionUpdate]("TradeExecutionUpdate", labels=[ - "ID", - "SYMBOL", - "MTS_CREATE", - "ORDER_ID", - "EXEC_AMOUNT", - "EXEC_PRICE", - "ORDER_TYPE", - "ORDER_PRICE", - "MAKER", - "FEE", - "FEE_CURRENCY", - "CID" -]) - -FundingOffer = _Serializer[typings.FundingOffer]("FundingOffer", labels=[ - "ID", - "SYMBOL", - "MTS_CREATED", - "MTS_UPDATED", - "AMOUNT", - "AMOUNT_ORIG", - "OFFER_TYPE", +FundingOffer = generate_labeler_serializer("FundingOffer", klass=types.FundingOffer, labels=[ + "id", + "symbol", + "mts_create", + "mts_update", + "amount", + "amount_orig", + "offer_type", "_PLACEHOLDER", "_PLACEHOLDER", - "FLAGS", - "STATUS", + "flags", + "offer_status", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "RATE", - "PERIOD", - "NOTIFY", - "HIDDEN", + "rate", + "period", + "notify", + "hidden", "_PLACEHOLDER", - "RENEW", + "renew", "_PLACEHOLDER" ]) -FundingCredit = _Serializer[typings.FundingCredit]("FundingCredit", labels=[ - "ID", - "SYMBOL", - "SIDE", - "MTS_CREATE", - "MTS_UPDATE", - "AMOUNT", - "FLAGS", - "STATUS", +FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.FundingCredit, labels=[ + "id", + "symbol", + "side", + "mts_create", + "mts_update", + "amount", + "flags", + "status", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "RATE", - "PERIOD", - "MTS_OPENING", - "MTS_LAST_PAYOUT", - "NOTIFY", - "HIDDEN", + "rate", + "period", + "mts_opening", + "mts_last_payout", + "notify", + "hidden", "_PLACEHOLDER", - "RENEW", - "RATE_REAL", - "NO_CLOSE", - "POSITION_PAIR" + "renew", + "_PLACEHOLDER", + "no_close", + "position_pair" ]) -FundingLoan = _Serializer[typings.FundingLoan]("FundingLoan", labels=[ - "ID", - "SYMBOL", - "SIDE", - "MTS_CREATE", - "MTS_UPDATE", - "AMOUNT", - "FLAGS", - "STATUS", +FundingLoan = generate_labeler_serializer("FundingLoan", klass=types.FundingLoan, labels=[ + "id", + "symbol", + "side", + "mts_create", + "mts_update", + "amount", + "flags", + "status", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "RATE", - "PERIOD", - "MTS_OPENING", - "MTS_LAST_PAYOUT", - "NOTIFY", - "HIDDEN", + "rate", + "period", + "mts_opening", + "mts_last_payout", + "notify", + "hidden", "_PLACEHOLDER", - "RENEW", - "RATE_REAL", - "NO_CLOSE" + "renew", + "_PLACEHOLDER", + "no_close" ]) -Wallet = _Serializer[typings.Wallet]("Wallet", labels=[ - "WALLET_TYPE", - "CURRENCY", - "BALANCE", - "UNSETTLED_INTEREST", - "BALANCE_AVAILABLE", - "DESCRIPTION", - "META" +Wallet = generate_labeler_serializer("Wallet", klass=types.Wallet, labels=[ + "wallet_type", + "currency", + "balance", + "unsettled_interest", + "available_balance", + "last_change", + "trade_details" ]) -BalanceInfo = _Serializer[typings.BalanceInfo]("BalanceInfo", labels=[ - "AUM", - "AUM_NET", +Balance = generate_labeler_serializer("Balance", klass=types.Balance, labels=[ + "aum", + "aum_net", ]) #endregion \ No newline at end of file diff --git a/bfxapi/websocket/subscriptions.py b/bfxapi/websocket/subscriptions.py new file mode 100644 index 0000000..10cbbfe --- /dev/null +++ b/bfxapi/websocket/subscriptions.py @@ -0,0 +1,41 @@ +from typing import TypedDict, Union, Literal, Optional + +__all__ = [ + "Subscription", + + "Ticker", + "Trades", + "Book", + "Candles", + "Status" +] + +_Header = TypedDict("_Header", { "event": Literal["subscribed"], "channel": str, "chanId": int }) + +Subscription = Union["Ticker", "Trades", "Book", "Candles", "Status"] + +class Ticker(TypedDict): + subId: str; symbol: str + pair: Optional[str] + currency: Optional[str] + +class Trades(TypedDict): + subId: str; symbol: str + pair: Optional[str] + currency: Optional[str] + +class Book(TypedDict): + subId: str + symbol: str + prec: str + freq: str + len: str + pair: str + +class Candles(TypedDict): + subId: str + key: str + +class Status(TypedDict): + subId: str + key: str \ No newline at end of file diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py new file mode 100644 index 0000000..063836a --- /dev/null +++ b/bfxapi/websocket/types.py @@ -0,0 +1,241 @@ +from typing import Optional + +from dataclasses import dataclass + +from .. labeler import _Type +from .. notification import Notification +from .. utils.JSONEncoder import JSON + +#region Type hinting for Websocket Public Channels + +@dataclass +class TradingPairTicker(_Type): + bid: float + bid_size: float + ask: float + ask_size: float + daily_change: float + daily_change_relative: float + last_price: float + volume: float + high: float + low: float + +@dataclass +class FundingCurrencyTicker(_Type): + frr: float + bid: float + bid_period: int + bid_size: float + ask: float + ask_period: int + ask_size: float + daily_change: float + daily_change_relative: float + last_price: float + volume: float + high: float + low: float + frr_amount_available: float + +@dataclass +class TradingPairTrade(_Type): + id: int + mts: int + amount: float + price: float + +@dataclass +class FundingCurrencyTrade(_Type): + id: int + mts: int + amount: float + rate: float + period: int + +@dataclass +class TradingPairBook(_Type): + price: float + count: int + amount: float + +@dataclass +class FundingCurrencyBook(_Type): + rate: float + period: int + count: int + amount: float + +@dataclass +class TradingPairRawBook(_Type): + order_id: int + price: float + amount: float + +@dataclass +class FundingCurrencyRawBook(_Type): + offer_id: int + period: int + rate: float + amount: float + +@dataclass +class Candle(_Type): + mts: int + open: float + close: float + high: float + low: float + volume: float + +@dataclass +class DerivativesStatus(_Type): + mts: int + deriv_price: float + spot_price: float + insurance_fund_balance: float + next_funding_evt_timestamp_ms: int + next_funding_accrued: float + next_funding_step: int + current_funding: float + mark_price: float + open_interest: float + clamp_min: float + clamp_max: float + +#endregion + +#region Type hinting for Websocket Authenticated Channels +@dataclass +class Order(_Type): + id: int + gid: int + cid: int + symbol: str + mts_create: int + mts_update: int + amount: float + amount_orig: float + order_type: str + type_prev: str + mts_tif: int + flags: int + order_status: str + price: float + price_avg: float + price_trailing: float + price_aux_limit: float + notify: int + hidden: int + placed_id: int + routing: str + meta: JSON + +@dataclass +class Position(_Type): + symbol: str + status: str + amount: float + base_price: float + margin_funding: float + margin_funding_type: int + pl: float + pl_perc: float + price_liq: float + leverage: float + flag: int + position_id: int + mts_create: int + mts_update: int + type: int + collateral: float + collateral_min: float + meta: JSON + +@dataclass +class Trade(_Type): + id: int + symbol: str + mts_create: int + order_id: int + exec_amount: float + exec_price: float + order_type: str + order_price: float + maker:int + fee: Optional[float] + fee_currency: Optional[str] + cid: int + +@dataclass +class FundingOffer(_Type): + id: int + symbol: str + mts_create: int + mts_update: int + amount: float + amount_orig: float + offer_type: str + flags: int + offer_status: str + rate: float + period: int + notify: int + hidden: int + renew: int + +@dataclass +class FundingCredit(_Type): + id: int + symbol: str + side: int + mts_create: int + mts_update: int + amount: float + flags: int + status: str + rate: float + period: int + mts_opening: int + mts_last_payout: int + notify: int + hidden: int + renew: int + no_close: int + position_pair: str + +@dataclass +class FundingLoan(_Type): + id: int + symbol: str + side: int + mts_create: int + mts_update: int + amount: float + flags: int + status: str + rate: float + period: int + mts_opening: int + mts_last_payout: int + notify: int + hidden: int + renew: int + no_close: int + +@dataclass +class Wallet(_Type): + wallet_type: str + currency: str + balance: float + unsettled_interest: float + available_balance: float + last_change: str + trade_details: JSON + +@dataclass +class Balance(_Type): + aum: float + aum_net: float + +#endregion \ No newline at end of file diff --git a/bfxapi/websocket/typings.py b/bfxapi/websocket/typings.py deleted file mode 100644 index ced6aaa..0000000 --- a/bfxapi/websocket/typings.py +++ /dev/null @@ -1,277 +0,0 @@ -from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any - -from .. notification import Notification - -JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] - -#region Type hinting for subscription objects - -class Subscriptions: - class TradingPairTicker(TypedDict): - chanId: int - symbol: str - pair: str - - class FundingCurrencyTicker(TypedDict): - chanId: int - symbol: str - currency: str - - class TradingPairTrades(TypedDict): - chanId: int - symbol: str - pair: str - - class FundingCurrencyTrades(TypedDict): - chanId: int - symbol: str - currency: str - - class Book(TypedDict): - chanId: int - symbol: str - prec: str - freq: str - len: str - subId: int - pair: str - - class Candles(TypedDict): - chanId: int - key: str - - class DerivativesStatus(TypedDict): - chanId: int - key: str - -#endregion - -#region Type hinting for Websocket Public Channels - -class TradingPairTicker(TypedDict): - BID: float - BID_SIZE: float - ASK: float - ASK_SIZE: float - DAILY_CHANGE: float - DAILY_CHANGE_RELATIVE: float - LAST_PRICE: float - VOLUME: float - HIGH: float - LOW: float - -class FundingCurrencyTicker(TypedDict): - FRR: float - BID: float - BID_PERIOD: int - BID_SIZE: float - ASK: float - ASK_PERIOD: int - ASK_SIZE: float - DAILY_CHANGE: float - DAILY_CHANGE_RELATIVE: float - LAST_PRICE: float - VOLUME: float - HIGH: float - LOW: float - FRR_AMOUNT_AVAILABLE: float - -class TradingPairTrade(TypedDict): - ID: int - MTS: int - AMOUNT: float - PRICE: float - -class FundingCurrencyTrade(TypedDict): - ID: int - MTS: int - AMOUNT: float - RATE: float - PERIOD: int - -class TradingPairBook(TypedDict): - PRICE: float - COUNT: int - AMOUNT: float - -class FundingCurrencyBook(TypedDict): - RATE: float - PERIOD: int - COUNT: int - AMOUNT: float - -class TradingPairRawBook(TypedDict): - ORDER_ID: int - PRICE: float - AMOUNT: float - -class FundingCurrencyRawBook(TypedDict): - OFFER_ID: int - PERIOD: int - RATE: float - AMOUNT: float - -class Candle(TypedDict): - MTS: int - OPEN: float - CLOSE: float - HIGH: float - LOW: float - VOLUME: float - -class DerivativesStatus(TypedDict): - TIME_MS: int - DERIV_PRICE: float - SPOT_PRICE: float - INSURANCE_FUND_BALANCE: float - NEXT_FUNDING_EVT_TIMESTAMP_MS: int - NEXT_FUNDING_ACCRUED: float - NEXT_FUNDING_STEP: int - CURRENT_FUNDING: float - MARK_PRICE: float - OPEN_INTEREST: float - CLAMP_MIN: float - CLAMP_MAX: float - -#endregion - -#region Type hinting for Websocket Authenticated Channels - -class Order(TypedDict): - ID: int - GID: int - CID: int - SYMBOL: str - MTS_CREATE: int - MTS_UPDATE: int - AMOUNT: float - AMOUNT_ORIG: float - ORDER_TYPE: str - TYPE_PREV: str - MTS_TIF: int - FLAGS: int - ORDER_STATUS: str - PRICE: float - PRICE_AVG: float - PRICE_TRAILING: float - PRICE_AUX_LIMIT: float - NOTIFY: int - HIDDEN: int - PLACED_ID: int - ROUTING: str - META: JSON - -class Position(TypedDict): - SYMBOL: str - STATUS: str - AMOUNT: float - BASE_PRICE: float - MARGIN_FUNDING: float - MARGIN_FUNDING_TYPE: int - PL: float - PL_PERC: float - PRICE_LIQ: float - LEVERAGE: float - POSITION_ID: int - MTS_CREATE: int - MTS_UPDATE: int - TYPE: int - COLLATERAL: float - COLLATERAL_MIN: float - META: JSON - -class TradeExecuted(TypedDict): - ID: int - SYMBOL: str - MTS_CREATE: int - ORDER_ID: int - EXEC_AMOUNT: float - EXEC_PRICE: float - ORDER_TYPE: str - ORDER_PRICE: float - MAKER:int - CID: int - -class TradeExecutionUpdate(TypedDict): - ID: int - SYMBOL: str - MTS_CREATE: int - ORDER_ID: int - EXEC_AMOUNT: float - EXEC_PRICE: float - ORDER_TYPE: str - ORDER_PRICE: float - MAKER:int - FEE: float - FEE_CURRENCY: str - CID: int - -class FundingOffer(TypedDict): - ID: int - SYMBOL: str - MTS_CREATED: int - MTS_UPDATED: int - AMOUNT: float - AMOUNT_ORIG: float - OFFER_TYPE: str - FLAGS: int - STATUS: str - RATE: float - PERIOD: int - NOTIFY: int - HIDDEN: int - RENEW: int - -class FundingCredit(TypedDict): - ID: int - SYMBOL: str - SIDE: int - MTS_CREATE: int - MTS_UPDATE: int - AMOUNT: float - FLAGS: int - STATUS: str - RATE: float - PERIOD: int - MTS_OPENING: int - MTS_LAST_PAYOUT: int - NOTIFY: int - HIDDEN: int - RENEW: int - RATE_REAL: float - NO_CLOSE: int - POSITION_PAIR: str - -class FundingLoan(TypedDict): - ID: int - SYMBOL: str - SIDE: int - MTS_CREATE: int - MTS_UPDATE: int - AMOUNT: float - FLAGS: int - STATUS: str - RATE: float - PERIOD: int - MTS_OPENING: int - MTS_LAST_PAYOUT: int - NOTIFY: int - HIDDEN: int - RENEW: int - RATE_REAL: float - NO_CLOSE: int - -class Wallet(TypedDict): - WALLET_TYPE: str - CURRENCY: str - BALANCE: float - UNSETTLED_INTEREST: float - BALANCE_AVAILABLE: float - DESCRIPTION: str - META: JSON - -class BalanceInfo(TypedDict): - AUM: float - AUM_NET: float - -#endregion \ No newline at end of file diff --git a/examples/rest/claim_position.py b/examples/rest/claim_position.py new file mode 100644 index 0000000..084c9d0 --- /dev/null +++ b/examples/rest/claim_position.py @@ -0,0 +1,19 @@ +# python -c "import examples.rest.claim_position" + +import os + +from bfxapi.client import Client, REST_HOST + +bfx = Client( + REST_HOST=REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +open_margin_positions = bfx.rest.auth.get_positions() + +# claim all positions +for position in open_margin_positions: + print(f"Position {position}") + claim = bfx.rest.auth.claim_position(position.position_id, amount=0.000001) + print(f"PositionClaim {claim.notify_info}") \ No newline at end of file diff --git a/examples/rest/create_funding_offer.py b/examples/rest/create_funding_offer.py index 2be1e5a..f462325 100644 --- a/examples/rest/create_funding_offer.py +++ b/examples/rest/create_funding_offer.py @@ -1,11 +1,12 @@ +# python -c "import examples.rest.create_funding_offer" + import os -from bfxapi.client import Client, Constants -from bfxapi.enums import FundingOfferType -from bfxapi.utils.flags import calculate_offer_flags +from bfxapi.client import Client, REST_HOST +from bfxapi.enums import FundingOfferType, Flag bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) @@ -16,11 +17,16 @@ notification = bfx.rest.auth.submit_funding_offer( amount="123.45", rate="0.001", period=2, - flags=calculate_offer_flags(hidden=True) + flags=Flag.HIDDEN ) print("Offer notification:", notification) -offers = bfx.rest.auth.get_active_funding_offers(symbol="fUSD") +offers = bfx.rest.auth.get_funding_offers(symbol="fUSD") -print("Offers:", offers) \ No newline at end of file +print("Offers:", offers) + +# Cancel all funding offers +notification = bfx.rest.auth.cancel_all_funding_offers(currency="fUSD") + +print(notification) \ No newline at end of file diff --git a/examples/rest/create_order.py b/examples/rest/create_order.py index 34408aa..607f7c9 100644 --- a/examples/rest/create_order.py +++ b/examples/rest/create_order.py @@ -1,11 +1,11 @@ -import os +# python -c "import examples.rest.create_order" -from bfxapi.client import Client, Constants -from bfxapi.enums import OrderType -from bfxapi.utils.flags import calculate_order_flags +import os +from bfxapi.client import Client, REST_HOST +from bfxapi.enums import OrderType, Flag bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) @@ -16,14 +16,14 @@ submitted_order = bfx.rest.auth.submit_order( symbol="tBTCUST", amount="0.015", price="10000", - flags=calculate_order_flags(hidden=False) + flags=Flag.HIDDEN + Flag.OCO + Flag.CLOSE ) print("Submit Order Notification:", submitted_order) # Update it updated_order = bfx.rest.auth.update_order( - id=submitted_order["NOTIFY_INFO"]["ID"], + id=submitted_order.notify_info.id, amount="0.020", price="10100" ) @@ -31,6 +31,6 @@ updated_order = bfx.rest.auth.update_order( print("Update Order Notification:", updated_order) # Delete it -canceled_order = bfx.rest.auth.cancel_order(id=submitted_order["NOTIFY_INFO"]["ID"]) +canceled_order = bfx.rest.auth.cancel_order(id=submitted_order.notify_info.id) print("Cancel Order Notification:", canceled_order) diff --git a/examples/rest/derivatives.py b/examples/rest/derivatives.py new file mode 100644 index 0000000..58a0031 --- /dev/null +++ b/examples/rest/derivatives.py @@ -0,0 +1,31 @@ +# python -c "import examples.rest.derivatives" + +import os + +from bfxapi.client import Client, REST_HOST + +bfx = Client( + REST_HOST=REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +# Create a new order +submitted_order = bfx.rest.auth.submit_order( + symbol="tBTCF0:USTF0", + amount="0.015", + price="16700", + lev=10, + type="LIMIT" +) + +print("Submit Order Notification:", submitted_order) + +# Get position collateral limits +limits = bfx.rest.auth.get_derivative_position_collateral_limits(symbol="tBTCF0:USTF0") +print(f"Limits {limits}") + +# Update position collateral +response = bfx.rest.auth.set_derivative_position_collateral(symbol="tBTCF0:USTF0", collateral=50) +print(response.status) + diff --git a/examples/rest/extra_calcs.py b/examples/rest/extra_calcs.py new file mode 100644 index 0000000..8ef93cb --- /dev/null +++ b/examples/rest/extra_calcs.py @@ -0,0 +1,28 @@ +# python -c "import examples.rest.extra_calcs" + +from bfxapi.client import Client, REST_HOST + +bfx = Client( + REST_HOST=REST_HOST +) + +t_symbol_response = bfx.rest.public.get_trading_market_average_price( + symbol="tBTCUSD", + amount=-100, + price_limit="20000.5" +) + +print(t_symbol_response.price_avg) + +f_symbol_response = bfx.rest.public.get_funding_market_average_price( + symbol="fUSD", + amount=100, + period=2, + rate_limit="0.00015" +) + +print(f_symbol_response.rate_avg) + +fx_rate = bfx.rest.public.get_fx_rate(ccy1="USD", ccy2="EUR") + +print(fx_rate.current_rate) \ No newline at end of file diff --git a/examples/rest/funding_auto_renew.py b/examples/rest/funding_auto_renew.py new file mode 100644 index 0000000..f546707 --- /dev/null +++ b/examples/rest/funding_auto_renew.py @@ -0,0 +1,21 @@ +# python -c "import examples.rest.funding_auto_renew" + +import os + +from bfxapi.client import Client, REST_HOST + +bfx = Client( + REST_HOST=REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +notification = bfx.rest.auth.toggle_auto_renew( + status=True, + currency="USD", + amount="150", + rate="0", # FRR + period=2 +) + +print("Renew toggle notification:", notification) \ No newline at end of file diff --git a/examples/rest/get_authenticated_data.py b/examples/rest/get_authenticated_data.py index 881cbf6..c3226af 100644 --- a/examples/rest/get_authenticated_data.py +++ b/examples/rest/get_authenticated_data.py @@ -1,18 +1,29 @@ -# python -c "from examples.rest.get_authenticated_data import *" +# python -c "import examples.rest.get_authenticated_data" import os import time -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) now = int(round(time.time() * 1000)) + +def log_user_info(): + user_info = bfx.rest.auth.get_user_info() + print(user_info) + + +def log_login_history(): + login_history = bfx.rest.auth.get_login_history() + print(login_history) + + def log_wallets(): wallets = bfx.rest.auth.get_wallets() print("Wallets:") @@ -38,14 +49,14 @@ def log_positions(): def log_trades(): - trades = bfx.rest.auth.get_trades(symbol='tBTCUSD', start=0, end=now) + trades = bfx.rest.auth.get_trades_history(symbol='tBTCUSD', start=0, end=now) print("Trades:") [print(t) for t in trades] def log_order_trades(): order_id = 82406909127 - trades = bfx.rest.auth.get_order_trades(symbol='tBTCUSD', order_id=order_id) + trades = bfx.rest.auth.get_order_trades(symbol='tBTCUSD', id=order_id) print("Trade orders:") [print(t) for t in trades] @@ -69,7 +80,7 @@ def log_funding_loans(): def log_funding_loans_history(): - loans = bfx.rest.auth.get_funding_loan_history(symbol='fUSD', start=0, end=now) + loans = bfx.rest.auth.get_funding_loans_history(symbol='fUSD', start=0, end=now) print("Funding loan history:") [print(l) for l in loans] @@ -85,8 +96,18 @@ def log_funding_credits_history(): print("Funding credit history:") [print(c) for c in credit] +def log_margin_info(): + btcusd_margin_info = bfx.rest.auth.get_symbol_margin_info('tBTCUSD') + print(f"tBTCUSD margin info {btcusd_margin_info}") + + sym_all_margin_info = bfx.rest.auth.get_all_symbols_margin_info() + print(f"Sym all margin info {sym_all_margin_info}") + + base_margin_info = bfx.rest.auth.get_base_margin_info() + print(f"Base margin info {base_margin_info}") def run(): + log_user_info() log_wallets() log_orders() log_orders_history() @@ -97,5 +118,6 @@ def run(): log_funding_offer_history() log_funding_credits() log_funding_credits_history() + log_margin_info() run() \ No newline at end of file diff --git a/examples/rest/get_candles_hist.py b/examples/rest/get_candles_hist.py new file mode 100644 index 0000000..d8d9881 --- /dev/null +++ b/examples/rest/get_candles_hist.py @@ -0,0 +1,13 @@ +# python -c "import examples.rest.get_candles_hist" + +from bfxapi.client import Client, REST_HOST + +bfx = Client( + REST_HOST=REST_HOST +) + +print(f"Candles: {bfx.rest.public.get_candles_hist(symbol='tBTCUSD')}") + +# Be sure to specify a period or aggregated period when retrieving funding candles. +# If you wish to mimic the candles found in the UI, use the following setup to aggregate all funding candles: a30:p2:p30 +print(f"Candles: {bfx.rest.public.get_candles_hist(tf='15m', symbol='fUSD:a30:p2:p30')}") \ No newline at end of file diff --git a/examples/rest/get_funding_info.py b/examples/rest/get_funding_info.py new file mode 100644 index 0000000..82bf150 --- /dev/null +++ b/examples/rest/get_funding_info.py @@ -0,0 +1,13 @@ +# python -c "import examples.rest.get_funding_info" + +import os + +from bfxapi.client import Client, REST_HOST + +bfx = Client( + REST_HOST=REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +print(bfx.rest.auth.get_funding_info(key="fUSD")) \ No newline at end of file diff --git a/examples/rest/get_funding_trades_history.py b/examples/rest/get_funding_trades_history.py new file mode 100644 index 0000000..3af19d8 --- /dev/null +++ b/examples/rest/get_funding_trades_history.py @@ -0,0 +1,13 @@ +# python -c "import examples.rest.get_funding_trades_history" + +import os + +from bfxapi.client import Client, REST_HOST + +bfx = Client( + REST_HOST=REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +print(bfx.rest.auth.get_funding_trades_history()) \ No newline at end of file diff --git a/examples/rest/get_liquidations.py b/examples/rest/get_liquidations.py new file mode 100644 index 0000000..588c83a --- /dev/null +++ b/examples/rest/get_liquidations.py @@ -0,0 +1,14 @@ +# python -c "import examples.rest.get_liquidations" + +import time + +from bfxapi.client import Client, REST_HOST + +bfx = Client( + REST_HOST=REST_HOST +) + +now = int(round(time.time() * 1000)) + +liquidations = bfx.rest.public.get_liquidations(start=0, end=now) +print(f"Liquidations: {liquidations}") \ No newline at end of file diff --git a/examples/rest/get_positions.py b/examples/rest/get_positions.py new file mode 100644 index 0000000..62cd309 --- /dev/null +++ b/examples/rest/get_positions.py @@ -0,0 +1,23 @@ +# python -c "import examples.rest.get_positions" + +import os +import time + +from bfxapi.client import Client, REST_HOST + +bfx = Client( + REST_HOST=REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +now = int(round(time.time() * 1000)) + +positions_snapshot = bfx.rest.auth.get_positions_snapshot(end=now, limit=50) +print(positions_snapshot) + +positions_history = bfx.rest.auth.get_positions_history(end=now, limit=50) +print(positions_history) + +positions_audit = bfx.rest.auth.get_positions_audit(end=now, limit=50) +print(positions_audit) \ No newline at end of file diff --git a/examples/rest/get_public_data.py b/examples/rest/get_public_data.py new file mode 100644 index 0000000..125f97c --- /dev/null +++ b/examples/rest/get_public_data.py @@ -0,0 +1,52 @@ +# python -c "import examples.rest.get_public_data" + +import time + +from bfxapi.client import Client, REST_HOST + +bfx = Client( + REST_HOST=REST_HOST +) + +now = int(round(time.time() * 1000)) + + +def log_historical_candles(): + candles = bfx.rest.public.get_candles_hist(start=0, end=now, resource='trade:1m:tBTCUSD') + print("Candles:") + [print(c) for c in candles] + + +def log_historical_trades(): + trades = bfx.rest.public.get_t_trades(pair='tBTCUSD', start=0, end=now) + print("Trades:") + [print(t) for t in trades] + + +def log_books(): + orders = bfx.rest.public.get_t_book(pair='BTCUSD', precision='P0') + print("Order book:") + [print(o) for o in orders] + + +def log_tickers(): + tickers = bfx.rest.public.get_t_tickers(pairs=['BTCUSD']) + print("Tickers:") + print(tickers) + + +def log_derivative_status(): + status = bfx.rest.public.get_derivatives_status('ALL') + print("Deriv status:") + print(status) + + +def run(): + log_historical_candles() + log_historical_trades() + log_books() + log_tickers() + log_derivative_status() + + +run() \ No newline at end of file diff --git a/examples/rest/get_pulse_data.py b/examples/rest/get_pulse_data.py new file mode 100644 index 0000000..b0cc369 --- /dev/null +++ b/examples/rest/get_pulse_data.py @@ -0,0 +1,22 @@ +# python -c "import examples.rest.get_pulse_data" + +import time + +from bfxapi.client import Client, REST_HOST + +bfx = Client( + REST_HOST=REST_HOST +) + +now = int(round(time.time() * 1000)) + +messages = bfx.rest.public.get_pulse_history(end=now, limit=100) + +for message in messages: + print(f"Message: {message}") + print(message.content) + print(message.profile.picture) + +profile = bfx.rest.public.get_pulse_profile("News") +print(f"Profile: {profile}") +print(f"Profile picture: {profile.picture}") \ No newline at end of file diff --git a/examples/rest/increase_position.py b/examples/rest/increase_position.py new file mode 100644 index 0000000..add66e3 --- /dev/null +++ b/examples/rest/increase_position.py @@ -0,0 +1,18 @@ +# python -c "import examples.rest.increase_position" + +import os + +from bfxapi.client import Client, REST_HOST + +bfx = Client( + REST_HOST=REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +increase_info = bfx.rest.auth.get_increase_position_info(symbol="tBTCUSD", amount=0.0001) +print(increase_info) + +# increase a margin position +notification = bfx.rest.auth.increase_position(symbol="tBTCUSD", amount=0.0001) +print(notification.notify_info) diff --git a/examples/rest/keep_taken_funding.py b/examples/rest/keep_taken_funding.py new file mode 100644 index 0000000..21e60f4 --- /dev/null +++ b/examples/rest/keep_taken_funding.py @@ -0,0 +1,26 @@ +# python -c "import examples.rest.keep_taken_funding" + +import os + +from bfxapi.client import Client, REST_HOST + +bfx = Client( + REST_HOST=REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +loans = bfx.rest.auth.get_funding_loans(symbol="fUSD") + +for loan in loans: + print(f"Loan {loan}") + + notification = bfx.rest.auth.toggle_keep( + funding_type="loan", + ids=[loan.id], + changes={ + loan.id: 2 # (1 if true, 2 if false) + } + ) + + print("Funding keep notification:", notification) \ No newline at end of file diff --git a/examples/rest/merchant.py b/examples/rest/merchant.py new file mode 100644 index 0000000..84d9b9b --- /dev/null +++ b/examples/rest/merchant.py @@ -0,0 +1,44 @@ +# python -c "import examples.rest.merchant" + +import os + +from bfxapi.client import Client, REST_HOST + +bfx = Client( + REST_HOST=REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +customer_info = { + "nationality": "GB", + "resid_country": "DE", + "resid_city": "Berlin", + "resid_zip_code": 1, + "resid_street": "Timechain", + "full_name": "Satoshi", + "email": "satoshi3@bitfinex.com" +} + +invoice = bfx.rest.merchant.submit_invoice( + amount=1, + currency="USD", + duration=864000, + order_id="order123", + customer_info=customer_info, + pay_currencies=["ETH"] +) + +print(bfx.rest.merchant.get_invoices()) + +print(bfx.rest.merchant.get_invoice_count_stats(status="CREATED", format="Y")) + +print(bfx.rest.merchant.get_invoice_earning_stats(currency="USD", format="Y")) + +print(bfx.rest.merchant.get_currency_conversion_list()) + +print(bfx.rest.merchant.complete_invoice( + id=invoice.id, + pay_currency="ETH", + deposit_id=1 +)) \ No newline at end of file diff --git a/examples/rest/return_taken_funding.py b/examples/rest/return_taken_funding.py new file mode 100644 index 0000000..73d5a33 --- /dev/null +++ b/examples/rest/return_taken_funding.py @@ -0,0 +1,22 @@ +# python -c "import examples.rest.return_taken_funding" + +import os + +from bfxapi.client import Client, REST_HOST + +bfx = Client( + REST_HOST=REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +loans = bfx.rest.auth.get_funding_loans(symbol="fUSD") + +for loan in loans: + print(f"Loan {loan}") + + notification = bfx.rest.auth.submit_funding_close( + id=loan.id + ) + + print("Funding close notification:", notification) \ No newline at end of file diff --git a/examples/rest/transfer_wallet.py b/examples/rest/transfer_wallet.py new file mode 100644 index 0000000..9384bd8 --- /dev/null +++ b/examples/rest/transfer_wallet.py @@ -0,0 +1,46 @@ +# python -c "import examples.rest.transfer_wallet" + +import os + +from bfxapi.client import Client, REST_HOST + +bfx = Client( + REST_HOST=REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +def transfer_wallet(): + response = bfx.rest.auth.transfer_between_wallets(from_wallet="exchange", to_wallet="funding", from_currency="ETH", to_currency="ETH", amount=0.001) + print("Transfer:", response.notify_info) + +def get_existing_deposit_address(): + response = bfx.rest.auth.get_deposit_address(wallet="exchange", method="bitcoin", renew=False) + print("Address:", response.notify_info) + +def create_new_deposit_address(): + response = bfx.rest.auth.get_deposit_address(wallet="exchange", method="bitcoin", renew=True) + print("Address:", response.notify_info) + +def withdraw(): + # tetheruse = Tether (ERC20) + response = bfx.rest.auth.submit_wallet_withdrawal(wallet="exchange", method="tetheruse", amount=1, address="0x742d35Cc6634C0532925a3b844Bc454e4438f44e") + print("Address:", response.notify_info) + +def create_lighting_network_deposit_address(): + invoice = bfx.rest.auth.generate_deposit_invoice(wallet="funding", currency="LNX", amount=0.001) + print("Invoice:", invoice) + +def get_movements(): + movements = bfx.rest.auth.get_movements(currency="BTC") + print("Movements:", movements) + +def run(): + transfer_wallet() + get_existing_deposit_address() + create_new_deposit_address() + withdraw() + create_lighting_network_deposit_address() + get_movements() + +run() \ No newline at end of file diff --git a/examples/websocket/create_order.py b/examples/websocket/create_order.py index b3146e9..48f4957 100644 --- a/examples/websocket/create_order.py +++ b/examples/websocket/create_order.py @@ -1,13 +1,13 @@ -# python -c "from examples.websocket.create_order import *" +# python -c "import examples.websocket.create_order" import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, WSS_HOST from bfxapi.websocket.enums import Error, OrderType -from bfxapi.websocket.typings import Notification, Order +from bfxapi.websocket.types import Notification, Order bfx = Client( - WSS_HOST=Constants.WSS_HOST, + WSS_HOST=WSS_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) @@ -30,7 +30,7 @@ async def on_authenticated(event): print("The order has been sent.") @bfx.wss.on("on-req-notification") -async def on_notification(notification: Notification): +async def on_notification(notification: Notification[Order]): print(f"Notification: {notification}.") @bfx.wss.on("order_new") diff --git a/examples/websocket/derivatives_status.py b/examples/websocket/derivatives_status.py new file mode 100644 index 0000000..3099431 --- /dev/null +++ b/examples/websocket/derivatives_status.py @@ -0,0 +1,23 @@ +# python -c "import examples.websocket.derivatives_status" + +from bfxapi import Client, PUB_WSS_HOST +from bfxapi.websocket.enums import Error, Channel +from bfxapi.websocket.types import DerivativesStatus + +from bfxapi.websocket import subscriptions + +bfx = Client(WSS_HOST=PUB_WSS_HOST) + +@bfx.wss.on("derivatives_status_update") +def on_derivatives_status_update(subscription: subscriptions.Status, data: DerivativesStatus): + print(f"{subscription}:", data) + +@bfx.wss.on("wss-error") +def on_wss_error(code: Error, msg: str): + print(code, msg) + +@bfx.wss.once("open") +async def open(): + await bfx.wss.subscribe(Channel.STATUS, key="deriv:tBTCF0:USTF0") + +bfx.wss.run() \ No newline at end of file diff --git a/examples/websocket/order_book.py b/examples/websocket/order_book.py index 0035cf8..55e4ae3 100644 --- a/examples/websocket/order_book.py +++ b/examples/websocket/order_book.py @@ -1,13 +1,14 @@ -# python -c "from examples.websocket.order_book import *" +# python -c "import examples.websocket.order_book" from collections import OrderedDict from typing import List -from bfxapi import Client, Constants +from bfxapi import Client, PUB_WSS_HOST -from bfxapi.websocket.enums import Channels, Error -from bfxapi.websocket.typings import Subscriptions, TradingPairBook +from bfxapi.websocket import subscriptions +from bfxapi.websocket.enums import Channel, Error +from bfxapi.websocket.types import TradingPairBook class OrderBook(object): def __init__(self, symbols: List[str]): @@ -18,7 +19,7 @@ class OrderBook(object): } def update(self, symbol: str, data: TradingPairBook) -> None: - price, count, amount = data["PRICE"], data["COUNT"], data["AMOUNT"] + price, count, amount = data.price, data.count, data.amount kind = (amount > 0) and "bids" or "asks" @@ -37,7 +38,7 @@ SYMBOLS = [ "tBTCUSD", "tLTCUSD", "tLTCBTC", "tETHUSD", "tETHBTC" ] order_book = OrderBook(symbols=SYMBOLS) -bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) +bfx = Client(WSS_HOST=PUB_WSS_HOST) @bfx.wss.on("wss-error") def on_wss_error(code: Error, msg: str): @@ -46,19 +47,19 @@ def on_wss_error(code: Error, msg: str): @bfx.wss.on("open") async def on_open(): for symbol in SYMBOLS: - await bfx.wss.subscribe(Channels.BOOK, symbol=symbol) + await bfx.wss.subscribe(Channel.BOOK, symbol=symbol) @bfx.wss.on("subscribed") def on_subscribed(subscription): print(f"Subscription successful for pair <{subscription['pair']}>") @bfx.wss.on("t_book_snapshot") -def on_t_book_snapshot(subscription: Subscriptions.Book, snapshot: List[TradingPairBook]): +def on_t_book_snapshot(subscription: subscriptions.Book, snapshot: List[TradingPairBook]): for data in snapshot: order_book.update(subscription["symbol"], data) @bfx.wss.on("t_book_update") -def on_t_book_update(subscription: Subscriptions.Book, data: TradingPairBook): +def on_t_book_update(subscription: subscriptions.Book, data: TradingPairBook): order_book.update(subscription["symbol"], data) bfx.wss.run() \ No newline at end of file diff --git a/examples/websocket/raw_order_book.py b/examples/websocket/raw_order_book.py index 6cfc3c1..3ce9c6d 100644 --- a/examples/websocket/raw_order_book.py +++ b/examples/websocket/raw_order_book.py @@ -1,13 +1,14 @@ -# python -c "from examples.websocket.raw_order_book import *" +# python -c "import examples.websocket.raw_order_book" from collections import OrderedDict from typing import List -from bfxapi import Client, Constants +from bfxapi import Client, PUB_WSS_HOST -from bfxapi.websocket.enums import Channels, Error -from bfxapi.websocket.typings import Subscriptions, TradingPairRawBook +from bfxapi.websocket import subscriptions +from bfxapi.websocket.enums import Channel, Error +from bfxapi.websocket.types import TradingPairRawBook class RawOrderBook(object): def __init__(self, symbols: List[str]): @@ -18,7 +19,7 @@ class RawOrderBook(object): } def update(self, symbol: str, data: TradingPairRawBook) -> None: - order_id, price, amount = data["ORDER_ID"], data["PRICE"], data["AMOUNT"] + order_id, price, amount = data.order_id, data.price, data.amount kind = (amount > 0) and "bids" or "asks" @@ -37,7 +38,7 @@ SYMBOLS = [ "tBTCUSD", "tLTCUSD", "tLTCBTC", "tETHUSD", "tETHBTC" ] raw_order_book = RawOrderBook(symbols=SYMBOLS) -bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) +bfx = Client(WSS_HOST=PUB_WSS_HOST) @bfx.wss.on("wss-error") def on_wss_error(code: Error, msg: str): @@ -46,19 +47,19 @@ def on_wss_error(code: Error, msg: str): @bfx.wss.on("open") async def on_open(): for symbol in SYMBOLS: - await bfx.wss.subscribe(Channels.BOOK, symbol=symbol, prec="R0") + await bfx.wss.subscribe(Channel.BOOK, symbol=symbol, prec="R0") @bfx.wss.on("subscribed") def on_subscribed(subscription): print(f"Subscription successful for pair <{subscription['pair']}>") @bfx.wss.on("t_raw_book_snapshot") -def on_t_raw_book_snapshot(subscription: Subscriptions.Book, snapshot: List[TradingPairRawBook]): +def on_t_raw_book_snapshot(subscription: subscriptions.Book, snapshot: List[TradingPairRawBook]): for data in snapshot: raw_order_book.update(subscription["symbol"], data) @bfx.wss.on("t_raw_book_update") -def on_t_raw_book_update(subscription: Subscriptions.Book, data: TradingPairRawBook): +def on_t_raw_book_update(subscription: subscriptions.Book, data: TradingPairRawBook): raw_order_book.update(subscription["symbol"], data) bfx.wss.run() \ No newline at end of file diff --git a/examples/websocket/ticker.py b/examples/websocket/ticker.py index 5db8ed1..d4b4c91 100644 --- a/examples/websocket/ticker.py +++ b/examples/websocket/ticker.py @@ -1,21 +1,21 @@ -# python -c "from examples.websocket.ticker import *" +# python -c "import examples.websocket.ticker" -import asyncio +from bfxapi import Client, PUB_WSS_HOST -from bfxapi import Client, Constants -from bfxapi.websocket.enums import Channels -from bfxapi.websocket.typings import Subscriptions, TradingPairTicker +from bfxapi.websocket import subscriptions +from bfxapi.websocket.enums import Channel +from bfxapi.websocket.types import TradingPairTicker -bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) +bfx = Client(WSS_HOST=PUB_WSS_HOST) @bfx.wss.on("t_ticker_update") -def on_t_ticker_update(subscription: Subscriptions.TradingPairTicker, data: TradingPairTicker): - print(f"Subscription with channel ID: {subscription['chanId']}") +def on_t_ticker_update(subscription: subscriptions.Ticker, data: TradingPairTicker): + print(f"Subscription with subId: {subscription['subId']}") print(f"Data: {data}") @bfx.wss.once("open") async def open(): - await bfx.wss.subscribe(Channels.TICKER, symbol="tBTCUSD") + await bfx.wss.subscribe(Channel.TICKER, symbol="tBTCUSD") bfx.wss.run() \ No newline at end of file diff --git a/examples/websocket/trades.py b/examples/websocket/trades.py new file mode 100644 index 0000000..0a5291f --- /dev/null +++ b/examples/websocket/trades.py @@ -0,0 +1,29 @@ +# python -c "import examples.websocket.trades" + +from bfxapi import Client, PUB_WSS_HOST +from bfxapi.websocket.enums import Error, Channel +from bfxapi.websocket.types import Candle, TradingPairTrade + +from bfxapi.websocket import subscriptions + +bfx = Client(WSS_HOST=PUB_WSS_HOST) + +@bfx.wss.on("candles_update") +def on_candles_update(subscription: subscriptions.Candles, candle: Candle): + print(f"New candle: {candle}") + +@bfx.wss.on("t_trade_executed") +def on_t_trade_executed(subscription: subscriptions.Trades, trade: TradingPairTrade): + print(f"New trade: {trade}") + +@bfx.wss.on("wss-error") +def on_wss_error(code: Error, msg: str): + print(code, msg) + +@bfx.wss.once("open") +async def open(): + await bfx.wss.subscribe(Channel.CANDLES, key="trade:1m:tBTCUSD") + + await bfx.wss.subscribe(Channel.TRADES, symbol="tBTCUSD") + +bfx.wss.run() \ No newline at end of file diff --git a/examples/websocket/wallet_balance.py b/examples/websocket/wallet_balance.py new file mode 100644 index 0000000..0e1b489 --- /dev/null +++ b/examples/websocket/wallet_balance.py @@ -0,0 +1,30 @@ +# python -c "import examples.websocket.wallet_balance" + +import os + +from typing import List + +from bfxapi import Client, WSS_HOST +from bfxapi.websocket.enums import Error +from bfxapi.websocket.types import Wallet + +bfx = Client( + WSS_HOST=WSS_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +@bfx.wss.on("wallet_snapshot") +def log_snapshot(wallets: List[Wallet]): + for wallet in wallets: + print(f"Balance: {wallet}") + +@bfx.wss.on("wallet_update") +def log_update(wallet: Wallet): + print(f"Balance update: {wallet}") + +@bfx.wss.on("wss-error") +def on_wss_error(code: Error, msg: str): + print(code, msg) + +bfx.wss.run() \ No newline at end of file diff --git a/setup.py b/setup.py index 963f30a..93bcd68 100644 --- a/setup.py +++ b/setup.py @@ -2,14 +2,36 @@ from distutils.core import setup setup( name="bitfinex-api-py", - version="3.0.0", - packages=[ "bfxapi", "bfxapi.websocket", "bfxapi.rest", "bfxapi.utils" ], + version="3.0.0b1", + description="Official Bitfinex Python API", + long_description="A Python reference implementation of the Bitfinex API for both REST and websocket interaction", + long_description_content_type="text/markdown", url="https://github.com/bitfinexcom/bitfinex-api-py", - license="OSI Approved :: Apache Software License", author="Bitfinex", author_email="support@bitfinex.com", - description="Official Bitfinex Python API", + license="Apache-2.0", + classifiers=[ + "Development Status :: 4 - Beta", + + "Intended Audience :: Developers", + "Topic :: Software Development :: Build Tools", + + "License :: OSI Approved :: Apache-2.0", + + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + ], keywords="bitfinex,api,trading", + project_urls={ + "Bug Reports": "https://github.com/bitfinexcom/bitfinex-api-py/issues", + "Source": "https://github.com/bitfinexcom/bitfinex-api-py", + }, + packages=[ + "bfxapi", "bfxapi.utils", + "bfxapi.websocket", "bfxapi.websocket.client", "bfxapi.websocket.handlers", + "bfxapi.rest", "bfxapi.rest.endpoints", "bfxapi.rest.middleware", + ], install_requires=[ "certifi~=2022.12.7", "charset-normalizer~=2.1.1", @@ -25,8 +47,5 @@ setup( "urllib3~=1.26.13", "websockets~=10.4", ], - project_urls={ - "Bug Reports": "https://github.com/bitfinexcom/bitfinex-api-py/issues", - "Source": "https://github.com/bitfinexcom/bitfinex-api-py", - } + python_requires=">=3.8" ) \ No newline at end of file