diff --git a/bfxapi/client.py b/bfxapi/client.py index dd2fe93..75c3f2a 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -1,5 +1,7 @@ from .websocket import BfxWebsocketClient +from typing import Optional + from enum import Enum class Constants(str, Enum): @@ -10,7 +12,7 @@ class Constants(str, Enum): PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2" class Client(object): - def __init__(self, WSS_HOST: str = Constants.WSS_HOST, API_KEY: str = None, API_SECRET: str = None, log_level: str = "WARNING"): + def __init__(self, WSS_HOST: str = Constants.WSS_HOST, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None, log_level: str = "WARNING"): self.wss = BfxWebsocketClient( host=WSS_HOST, API_KEY=API_KEY, diff --git a/bfxapi/exceptions.py b/bfxapi/exceptions.py new file mode 100644 index 0000000..1033837 --- /dev/null +++ b/bfxapi/exceptions.py @@ -0,0 +1,35 @@ +__all__ = [ + "BfxBaseException", + + "LabelerSerializerException", + "IntegerUnderflowError", + "IntegerOverflowflowError" +] + +class BfxBaseException(Exception): + """ + Base class for every custom exception in bfxapi/rest/exceptions.py and bfxapi/websocket/exceptions.py. + """ + + pass + +class LabelerSerializerException(BfxBaseException): + """ + This exception indicates an error thrown by the _Serializer class in bfxapi/labeler.py. + """ + + pass + +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 new file mode 100644 index 0000000..bcf18c3 --- /dev/null +++ b/bfxapi/labeler.py @@ -0,0 +1,22 @@ +from .exceptions import LabelerSerializerException + +from typing import Generic, TypeVar, Iterable, Optional, List, Tuple, Any, cast + +T = TypeVar("T") + +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 __serialize(self, *args: Any, skip: Optional[List[str]]) -> 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.") + + 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 diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index e4bb11c..37f52d3 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -1,20 +1,41 @@ -import requests +import time, hmac, hashlib, json, requests from http import HTTPStatus -from typing import List, Union, Literal, Optional, Any +from typing import List, Union, Literal, Optional, Any, cast from . import serializers from .typings import * -from .enums import Configs -from .exceptions import RequestParametersError, ResourceNotFound +from .enums import Config, Precision, Sort +from .exceptions import RequestParametersError, ResourceNotFound, InvalidAuthenticationCredentials class BfxRestInterface(object): - def __init__(self, host): - self.host = host + def __init__(self, host, API_KEY = None, API_SECRET = None): + self.public = _RestPublicEndpoints(host=host) - def __GET(self, endpoint, params = None): + 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) + + signature = hmac.new( + self.API_SECRET.encode("utf8"), + f"/api/v2/{endpoint}{nonce}{json.dumps(data)}".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: @@ -28,136 +49,182 @@ class BfxRestInterface(object): return data + def _POST(self, endpoint, params = None, data = None, _append_authentication_headers = True): + headers = { "Content-Type": "application/json" } + + if _append_authentication_headers: + headers = { **headers, **self.__build_authentication_headers(f"{endpoint}", data) } + + response = requests.post(f"{self.host}/{endpoint}", params=params, data=json.dumps(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] == 10020: + raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") + + if data[1] == 10100: + raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.") + + return data + +class _RestPublicEndpoints(_Requests): def platform_status(self) -> PlatformStatus: - return serializers.PlatformStatus.parse(*self.__GET("platform/status")) + return serializers.PlatformStatus.parse(*self._GET("platform/status")) def tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]: - data = self.__GET("tickers", params={ "symbols": ",".join(symbols) }) + data = self._GET("tickers", params={ "symbols": ",".join(symbols) }) - return [ - { - "t": serializers.TradingPairTicker.parse, - "f": serializers.FundingCurrencyTicker.parse - }[subdata[0][0]](*subdata) + parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse } + + return [ parsers[subdata[0][0]](*subdata) for subdata in data ] - for subdata in data - ] + def 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.tickers([ "ALL" ]) if cast(str, subdata["SYMBOL"]).startswith("t") ] - def ticker(self, symbol: str) -> Union[TradingPairTicker, FundingCurrencyTicker]: - data = self.__GET(f"ticker/{symbol}") + data = self.tickers([ "t" + pair for pair in pairs ]) - return { - "t": serializers.TradingPairTicker.parse, - "f": serializers.FundingCurrencyTicker.parse - }[symbol[0]](*data, skip=["SYMBOL"]) + return cast(List[TradingPairTicker], data) - def tickers_history(self, symbols: List[str], start: Optional[int] = None, end: Optional[int] = None, limit: Optional[int] = None) -> TickerHistories: + def 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.tickers([ "ALL" ]) if cast(str, subdata["SYMBOL"]).startswith("f") ] + + data = self.tickers([ "f" + currency for currency in currencies ]) + + return cast(List[FundingCurrencyTicker], data) + + def t_ticker(self, pair: str) -> TradingPairTicker: + return serializers.TradingPairTicker.parse(*self._GET(f"ticker/t{pair}"), skip=["SYMBOL"]) + + def f_ticker(self, currency: str) -> FundingCurrencyTicker: + return serializers.FundingCurrencyTicker.parse(*self._GET(f"ticker/f{currency}"), skip=["SYMBOL"]) + + def 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) + data = self._GET("tickers/hist", params=params) - return [ serializers.TickerHistory.parse(*subdata) for subdata in data ] + return [ serializers.TickersHistory.parse(*subdata) for subdata in data ] - def trades(self, symbol: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[int] = None) -> Union[TradingPairTrades, FundingCurrencyTrades]: - params = { "symbol": symbol, "limit": limit, "start": start, "end": end, "sort": sort } - - data = self.__GET(f"trades/{symbol}/hist", params=params) + def 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 ] - return [ - { - "t": serializers.TradingPairTrade.parse, - "f": serializers.FundingCurrencyTrade.parse - }[symbol[0]](*subdata) + def 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 ] - for subdata in data - ] + def 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 book(self, symbol: str, precision: str, len: Optional[int] = None) -> Union[TradingPairBooks, FundingCurrencyBooks, TradingPairRawBooks, FundingCurrencyRawBooks]: - data = self.__GET(f"book/{symbol}/{precision}", params={ "len": len }) - - return [ - { - "t": precision == "R0" and serializers.TradingPairRawBook.parse or serializers.TradingPairBook.parse, - "f": precision == "R0" and serializers.FundingCurrencyRawBook.parse or serializers.FundingCurrencyBook.parse, - }[symbol[0]](*subdata) + def 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 }) ] - for subdata in data - ] + def 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 stats( + def 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 stats_hist( self, - resource: str, section: Literal["hist", "last"], - sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Union[Stat, Stats]: + 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 ] - data = self.__GET(f"stats1/{resource}/{section}", params=params) + def 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) - if section == "last": - return serializers.Stat.parse(*data) - return [ serializers.Stat.parse(*subdata) for subdata in data ] - - def candles( + def candles_hist( self, - resource: str, section: Literal["hist", "last"], - sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Union[Candle, Candles]: + 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}/{section}", params=params) - - if section == "last": - return serializers.Candle.parse(*data) + data = self._GET(f"candles/{resource}/hist", params=params) return [ serializers.Candle.parse(*subdata) for subdata in data ] - def derivatives_status(self, type: str, keys: List[str] = None) -> DerivativeStatuses: + def 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 derivatives_status(self, type: str, keys: List[str]) -> List[DerivativesStatus]: params = { "keys": ",".join(keys) } - data = self.__GET(f"status/{type}", params=params) + data = self._GET(f"status/{type}", params=params) return [ serializers.DerivativesStatus.parse(*subdata) for subdata in data ] def derivatives_status_history( self, type: str, symbol: str, - sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> DerivativeStatuses: + 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) + data = self._GET(f"status/{type}/{symbol}/hist", params=params) return [ serializers.DerivativesStatus.parse(*subdata, skip=[ "KEY" ]) for subdata in data ] - def liquidations(self, sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> Liquidations: + def 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) + data = self._GET("liquidations/hist", params=params) return [ serializers.Liquidation.parse(*subdata[0]) for subdata in data ] - def leaderboards( + def leaderboards_hist( self, - resource: str, section: Literal["hist", "last"], - sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Union[Leaderboard, Leaderboards]: + 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}/{section}", params=params) - - if section == "last": - return serializers.Leaderboard.parse(*data) + data = self._GET(f"rankings/{resource}/hist", params=params) return [ serializers.Leaderboard.parse(*subdata) for subdata in data ] - def funding_stats(self, symbol: str, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> FundingStats: + def 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 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) + data = self._GET(f"funding/stats/{symbol}/hist", params=params) - return [ serializers.FundingStat.parse(*subdata) for subdata in data ] + return [ serializers.FundingStatistic.parse(*subdata) for subdata in data ] - def conf(self, config: Configs) -> Any: - return self.__GET(f"conf/{config}")[0] \ No newline at end of file + def conf(self, config: Config) -> Any: + return self._GET(f"conf/{config}")[0] + +class _RestAuthenticatedEndpoints(_Requests): + __PREFIX = "auth/" \ No newline at end of file diff --git a/bfxapi/rest/enums.py b/bfxapi/rest/enums.py index 3bb05d3..70c2336 100644 --- a/bfxapi/rest/enums.py +++ b/bfxapi/rest/enums.py @@ -1,6 +1,6 @@ from enum import Enum -class Configs(str, Enum): +class Config(str, Enum): MAP_CURRENCY_SYM = "pub:map:currency:sym" MAP_CURRENCY_LABEL = "pub:map:currency:label" MAP_CURRENCY_UNIT = "pub:map:currency:unit" @@ -22,4 +22,15 @@ class Configs(str, Enum): INFO_TX_STATUS = "pub:info:tx:status" SPEC_MARGIN = "pub:spec:margin", - FEES = "pub:fees" \ No newline at end of file + FEES = "pub:fees" + +class Precision(str, Enum): + P0 = "P0" + P1 = "P1" + P2 = "P2" + P3 = "P3" + P4 = "P4" + +class Sort(int, Enum): + ASCENDING = +1 + DESCENDING = -1 \ No newline at end of file diff --git a/bfxapi/rest/exceptions.py b/bfxapi/rest/exceptions.py index 8afc74e..81bcb8f 100644 --- a/bfxapi/rest/exceptions.py +++ b/bfxapi/rest/exceptions.py @@ -1,11 +1,16 @@ +from .. exceptions import BfxBaseException + __all__ = [ + "BfxRestException", + "RequestParametersError", - "ResourceNotFound" + "ResourceNotFound", + "InvalidAuthenticationCredentials" ] -class BfxRestException(Exception): +class BfxRestException(BfxBaseException): """ - Base class for all exceptions defined in bfxapi/rest/exceptions.py. + Base class for all custom exceptions in bfxapi/rest/exceptions.py. """ pass @@ -22,4 +27,11 @@ class ResourceNotFound(BfxRestException): This error indicates a failed HTTP request to a non-existent resource. """ + pass + +class InvalidAuthenticationCredentials(BfxRestException): + """ + This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication. + """ + pass \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index a2f69df..95f4c26 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -1,27 +1,6 @@ -from typing import Generic, TypeVar, Iterable, Optional, List, Any - from . import typings -from .exceptions import BfxRestException - -T = TypeVar("T") - -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 __serialize(self, *args: Any, skip: Optional[List[str]]) -> Iterable[T]: - labels = list(filter(lambda label: label not in (skip or list()), self.__labels)) - - if len(labels) > len(args): - raise BfxRestException(" 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 dict(self.__serialize(*values, skip=skip)) +from .. labeler import _Serializer #region Serializers definition for Rest Public Endpoints @@ -63,7 +42,7 @@ FundingCurrencyTicker = _Serializer[typings.FundingCurrencyTicker]("FundingCurre "FRR_AMOUNT_AVAILABLE" ]) -TickerHistory = _Serializer[typings.TickerHistory]("TickerHistory", labels=[ +TickersHistory = _Serializer[typings.TickersHistory]("TickersHistory", labels=[ "SYMBOL", "BID", "_PLACEHOLDER", @@ -120,7 +99,7 @@ FundingCurrencyRawBook = _Serializer[typings.FundingCurrencyRawBook]("FundingCur "AMOUNT" ]) -Stat = _Serializer[typings.Stat]("Stat", labels=[ +Statistic = _Serializer[typings.Statistic]("Statistic", labels=[ "MTS", "VALUE" ]) @@ -189,7 +168,7 @@ Leaderboard = _Serializer[typings.Leaderboard]("Leaderboard", labels=[ "TWITTER_HANDLE" ]) -FundingStat = _Serializer[typings.FundingStat]("FundingStat", labels=[ +FundingStatistic = _Serializer[typings.FundingStatistic]("FundingStatistic", labels=[ "TIMESTAMP", "_PLACEHOLDER", "_PLACEHOLDER", diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index 828c228..7af75fb 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -2,140 +2,130 @@ from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any #region Type hinting for Rest Public Endpoints -PlatformStatus = TypedDict("PlatformStatus", { - "OPERATIVE": int -}) +class PlatformStatus(TypedDict): + OPERATIVE: int -TradingPairTicker = TypedDict("TradingPairTicker", { - "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 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 -FundingCurrencyTicker = TypedDict("FundingCurrencyTicker", { - "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 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 -TickerHistory = TypedDict("TickerHistory", { - "SYMBOL": str, - "BID": float, - "ASK": float, - "MTS": int -}) +class TickersHistory(TypedDict): + SYMBOL: str + BID: float + ASK: float + MTS: int -TickerHistories = List[TickerHistory] +class TradingPairTrade(TypedDict): + ID: int + MTS: int + AMOUNT: float + PRICE: float -(TradingPairTrade, FundingCurrencyTrade) = ( - TypedDict("TradingPairTrade", { "ID": int, "MTS": int, "AMOUNT": float, "PRICE": float }), - TypedDict("FundingCurrencyTrade", { "ID": int, "MTS": int, "AMOUNT": float, "RATE": float, "PERIOD": int }) -) +class FundingCurrencyTrade(TypedDict): + ID: int + MTS: int + AMOUNT: float + RATE: float + PERIOD: int -(TradingPairTrades, FundingCurrencyTrades) = (List[TradingPairTrade], List[FundingCurrencyTrade]) +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 -(TradingPairBook, FundingCurrencyBook) = ( - TypedDict("TradingPairBook", { "PRICE": float, "COUNT": int, "AMOUNT": float }), - TypedDict("FundingCurrencyBook", { "RATE": float, "PERIOD": int, "COUNT": int, "AMOUNT": float }) -) +class Statistic(TypedDict): + MTS: int + VALUE: float -(TradingPairBooks, FundingCurrencyBooks) = (List[TradingPairBook], List[FundingCurrencyBook]) +class Candle(TypedDict): + MTS: int + OPEN: float + CLOSE: float + HIGH: float + LOW: float + VOLUME: float -(TradingPairRawBook, FundingCurrencyRawBook) = ( - TypedDict("TradingPairRawBook", { "ORDER_ID": int, "PRICE": float, "AMOUNT": float }), - TypedDict("FundingCurrencyRawBook", { "OFFER_ID": int, "PERIOD": int, "RATE": float, "AMOUNT": 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 -(TradingPairRawBooks, FundingCurrencyRawBooks) = (List[TradingPairRawBook], List[FundingCurrencyRawBook]) +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 -Stat = TypedDict("Stat", { - "MTS": int, - "VALUE": float -}) +class Leaderboard(TypedDict): + MTS: int + USERNAME: str + RANKING: int + VALUE: float + TWITTER_HANDLE: Optional[str] -Stats = List[Stat] - -Candle = TypedDict("Candle", { - "MTS": int, - "OPEN": float, - "CLOSE": float, - "HIGH": float, - "LOW": float, - "VOLUME": float -}) - -Candles = List[Candle] - -DerivativesStatus = TypedDict("DerivativesStatus", { - "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 -}) - -DerivativeStatuses = List[DerivativesStatus] - -Liquidation = TypedDict("Liquidation", { - "POS_ID": int, - "MTS": int, - "SYMBOL": str, - "AMOUNT": float, - "BASE_PRICE": float, - "IS_MATCH": int, - "IS_MARKET_SOLD": int, - "PRICE_ACQUIRED": float -}) - -Liquidations = List[Liquidation] - -Leaderboard = TypedDict("Leaderboard", { - "MTS": int, - "USERNAME": str, - "RANKING": int, - "VALUE": float, - "TWITTER_HANDLE": Optional[str] -}) - -Leaderboards = List[Leaderboard] - -FundingStat = TypedDict("FundingStat", { - "TIMESTAMP": int, - "FRR": float, - "AVG_PERIOD": float, - "FUNDING_AMOUNT": float, - "FUNDING_AMOUNT_USED": float, - "FUNDING_BELOW_THRESHOLD": float -}) - -FundingStats = List[FundingStat] +class FundingStatistic(TypedDict): + TIMESTAMP: int + FRR: float + AVG_PERIOD: float + FUNDING_AMOUNT: float + FUNDING_AMOUNT_USED: float + FUNDING_BELOW_THRESHOLD: float #endregion \ No newline at end of file diff --git a/bfxapi/utils/decimal.py b/bfxapi/utils/decimal.py new file mode 100644 index 0000000..5a7af71 --- /dev/null +++ b/bfxapi/utils/decimal.py @@ -0,0 +1,9 @@ +import json + +from decimal import Decimal + +class DecimalEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, Decimal): + return str(obj) + return json.JSONEncoder.default(self, obj) \ No newline at end of file diff --git a/bfxapi/utils/integers.py b/bfxapi/utils/integers.py new file mode 100644 index 0000000..e38f107 --- /dev/null +++ b/bfxapi/utils/integers.py @@ -0,0 +1,35 @@ +from typing import cast, TypeVar + +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 + +class Int32(_Int): + _BITS = 32 + +class Int45(_Int): + _BITS = 45 + +class Int64(_Int): + _BITS = 64 \ No newline at end of file diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index e8bb4d0..775fa10 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -1,25 +1,40 @@ import traceback, json, asyncio, hmac, hashlib, time, uuid, websockets +from typing import Tuple, Union, Literal, TypeVar, Callable, cast + from enum import Enum from pyee.asyncio import AsyncIOEventEmitter -from .typings import Inputs, Tuple, Union +from .typings import Inputs from .handlers import Channels, PublicChannelsHandler, AuthenticatedChannelsHandler from .exceptions import ConnectionNotOpen, TooManySubscriptions, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion +from ..utils.decimal import DecimalEncoder + from ..utils.logger import Formatter, CustomLogger _HEARTBEAT = "hb" -def _require_websocket_connection(function): +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 wrapper + 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 @@ -118,22 +133,13 @@ class BfxWebsocketClient(object): for bucket in self.buckets: await bucket._close(code=code, reason=reason) - def __require_websocket_authentication(function): - 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 wrapper - - @__require_websocket_authentication + @_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 + @_require_websocket_authentication async def __handle_websocket_input(self, input, data): - await self.websocket.send(json.dumps([ 0, input, None, data])) + await self.websocket.send(json.dumps([ 0, input, None, data], cls=DecimalEncoder)) def __bucket_open_signal(self, index): if all(bucket.websocket != None and bucket.websocket.open == True for bucket in self.buckets): diff --git a/bfxapi/websocket/exceptions.py b/bfxapi/websocket/exceptions.py index c55b767..5691af8 100644 --- a/bfxapi/websocket/exceptions.py +++ b/bfxapi/websocket/exceptions.py @@ -1,4 +1,8 @@ +from .. exceptions import BfxBaseException + __all__ = [ + "BfxWebsocketException", + "ConnectionNotOpen", "TooManySubscriptions", "WebsocketAuthenticationRequired", @@ -7,9 +11,9 @@ __all__ = [ "OutdatedClientVersion" ] -class BfxWebsocketException(Exception): +class BfxWebsocketException(BfxBaseException): """ - Base class for all exceptions defined in bfxapi/websocket/exceptions.py. + Base class for all custom exceptions in bfxapi/websocket/exceptions.py. """ pass @@ -35,13 +39,6 @@ class WebsocketAuthenticationRequired(BfxWebsocketException): pass -class InvalidAuthenticationCredentials(BfxWebsocketException): - """ - This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication. - """ - - pass - class EventNotSupported(BfxWebsocketException): """ This error indicates a failed attempt to subscribe to an event not supported by the BfxWebsocketClient. @@ -54,4 +51,11 @@ class OutdatedClientVersion(BfxWebsocketException): This error indicates a mismatch between the client version and the server WSS version. """ + pass + +class InvalidAuthenticationCredentials(BfxWebsocketException): + """ + This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication. + """ + pass \ No newline at end of file diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index 64573cc..00f43d2 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -1,25 +1,6 @@ -from typing import Generic, TypeVar, Iterable, List, Any - from . import typings -from .exceptions import BfxWebsocketException - -T = TypeVar("T") - -class _Serializer(Generic[T]): - def __init__(self, name: str, labels: List[str]): - self.name, self.__labels = name, labels - - def __serialize(self, *args: Any, IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> Iterable[T]: - if len(self.__labels) != len(args): - raise BfxWebsocketException(" and <*args> arguments should contain the same amount of elements.") - - for index, label in enumerate(self.__labels): - if label not in IGNORE: - yield label, args[index] - - def parse(self, *values: Any) -> T: - return dict(self.__serialize(*values)) +from .. labeler import _Serializer #region Serializers definition for Websocket Public Channels @@ -315,7 +296,7 @@ BalanceInfo = _Serializer[typings.BalanceInfo]("BalanceInfo", labels=[ #region Serializers definition for Notifications channel -Notification = _Serializer("Notification", labels=[ +Notification = _Serializer[typings.Notification]("Notification", labels=[ "MTS", "TYPE", "MESSAGE_ID", diff --git a/bfxapi/websocket/typings.py b/bfxapi/websocket/typings.py index 4a3b918..9966d99 100644 --- a/bfxapi/websocket/typings.py +++ b/bfxapi/websocket/typings.py @@ -2,301 +2,294 @@ from decimal import Decimal from datetime import datetime -from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any +from typing import Type, NewType, Tuple, List, Dict, TypedDict, Union, Optional, Any -int16 = int32 = int45 = int64 = int +from ..utils.integers import Int16, Int32, Int45, Int64 JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] #region Type hinting for subscription objects class Subscriptions: - TradingPairsTicker = TypedDict("Subscriptions.TradingPairsTicker", { - "chanId": int, - "symbol": str, - "pair": str - }) + class TradingPairsTicker(TypedDict): + chanId: int + symbol: str + pair: str - FundingCurrenciesTicker = TypedDict("Subscriptions.FundingCurrenciesTicker", { - "chanId": int, - "symbol": str, - "currency": str - }) + class FundingCurrenciesTicker(TypedDict): + chanId: int + symbol: str + currency: str - TradingPairsTrades = TypedDict("Subscriptions.TradingPairsTrades", { - "chanId": int, - "symbol": str, - "pair": str - }) + class TradingPairsTrades(TypedDict): + chanId: int + symbol: str + pair: str - FundingCurrenciesTrades = TypedDict("Subscriptions.FundingCurrenciesTrades", { - "chanId": int, - "symbol": str, - "currency": str - }) + class FundingCurrenciesTrades(TypedDict): + chanId: int + symbol: str + currency: str - Book = TypedDict("Subscriptions.Book", { - "chanId": int, - "symbol": str, - "prec": str, - "freq": str, - "len": str, - "subId": int, - "pair": str - }) + class Book(TypedDict): + chanId: int + symbol: str + prec: str + freq: str + len: str + subId: int + pair: str - Candles = TypedDict("Subscriptions.Candles", { - "chanId": int, - "key": str - }) + class Candles(TypedDict): + chanId: int + key: str - DerivativesStatus = TypedDict("Subscriptions.DerivativesStatus", { - "chanId": int, - "key": str - }) + class DerivativesStatus(TypedDict): + chanId: int + key: str #endregion #region Type hinting for Websocket Public Channels -TradingPairTicker = TypedDict("TradingPairTicker", { - "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 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 -FundingCurrencyTicker = TypedDict("FundingCurrencyTicker", { - "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 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 -(TradingPairTrade, FundingCurrencyTrade) = ( - TypedDict("TradingPairTrade", { "ID": int, "MTS": int, "AMOUNT": float, "PRICE": float }), - TypedDict("FundingCurrencyTrade", { "ID": int, "MTS": int, "AMOUNT": float, "RATE": float, "PERIOD": int }) -) +class TradingPairTrade(TypedDict): + ID: int + MTS: int + AMOUNT: float + PRICE: float -(TradingPairTrades, FundingCurrencyTrades) = (List[TradingPairTrade], List[FundingCurrencyTrade]) +class FundingCurrencyTrade(TypedDict): + ID: int + MTS: int + AMOUNT: float + RATE: float + PERIOD: int -(TradingPairBook, FundingCurrencyBook) = ( - TypedDict("TradingPairBook", { "PRICE": float, "COUNT": int, "AMOUNT": float }), - TypedDict("FundingCurrencyBook", { "RATE": float, "PERIOD": int, "COUNT": int, "AMOUNT": float }) -) +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 -(TradingPairBooks, FundingCurrencyBooks) = (List[TradingPairBook], List[FundingCurrencyBook]) +class Candle(TypedDict): + MTS: int + OPEN: float + CLOSE: float + HIGH: float + LOW: float + VOLUME: float -(TradingPairRawBook, FundingCurrencyRawBook) = ( - TypedDict("TradingPairRawBook", { "ORDER_ID": int, "PRICE": float, "AMOUNT": float }), - TypedDict("FundingCurrencyRawBook", { "OFFER_ID": int, "PERIOD": int, "RATE": float, "AMOUNT": float }), -) - -(TradingPairRawBooks, FundingCurrencyRawBooks) = (List[TradingPairRawBook], List[FundingCurrencyRawBook]) - -Candle = TypedDict("Candle", { - "MTS": int, - "OPEN": float, - "CLOSE": float, - "HIGH": float, - "LOW": float, - "VOLUME": float -}) - -Candles = List[Candle] - -DerivativesStatus = TypedDict("DerivativesStatus", { - "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 -}) +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 -Order = TypedDict("Order", { - "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 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 -Orders = List[Order] +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 -Position = TypedDict("Position", { - "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 -Positions = List[Position] +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 -TradeExecuted = TypedDict("TradeExecuted", { - "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 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 -TradeExecutionUpdate = TypedDict("TradeExecutionUpdate", { - "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 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 -FundingOffer = TypedDict("FundingOffer", { - "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 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 -FundingOffers = List[FundingOffer] +class Wallet(TypedDict): + WALLET_TYPE: str + CURRENCY: str + BALANCE: float + UNSETTLED_INTEREST: float + BALANCE_AVAILABLE: float + DESCRIPTION: str + META: JSON -FundingCredit = TypedDict("FundingCredit", { - "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 BalanceInfo(TypedDict): + AUM: float + AUM_NET: float -FundingCredits = List[FundingCredit] +#endregion -FundingLoan = TypedDict("FundingLoan", { - "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 -}) +#region Serializers definition for Notifications channel -FundingLoans = List[FundingLoan] - -Wallet = TypedDict("Wallet", { - "WALLET_TYPE": str, - "CURRENCY": str, - "BALANCE": float, - "UNSETTLED_INTEREST": float, - "BALANCE_AVAILABLE": float, - "DESCRIPTION": str, - "META": JSON -}) - -Wallets = List[Wallet] - -BalanceInfo = TypedDict("BalanceInfo", { - "AUM": float, - "AUM_NET": float -}) +class Notification(TypedDict): + MTS: int + TYPE: str + MESSAGE_ID: int + NOTIFY_INFO: JSON + CODE: int + STATUS: str + TEXT: str #endregion @@ -304,55 +297,50 @@ BalanceInfo = TypedDict("BalanceInfo", { class Inputs: class Order: - New = TypedDict("Inputs.Order.New", { - "gid": Optional[int32], - "cid": int45, - "type": str, - "symbol": str, - "amount": Union[Decimal, str], - "price": Union[Decimal, str], - "lev": int, - "price_trailing": Union[Decimal, str], - "price_aux_limit": Union[Decimal, str], - "price_oco_stop": Union[Decimal, str], - "flags": int16, - "tif": Union[datetime, str], - "meta": JSON - }) + class New(TypedDict, total=False): + gid: Union[Int32, int] + cid: Union[Int45, int] + type: str + symbol: str + amount: Union[Decimal, str] + price: Union[Decimal, str] + lev: Union[Int32, int] + price_trailing: Union[Decimal, str] + price_aux_limit: Union[Decimal, str] + price_oco_stop: Union[Decimal, str] + flags: Union[Int16, int] + tif: Union[datetime, str] + meta: JSON - Update = TypedDict("Inputs.Order.Update", { - "id": int64, - "cid": int45, - "cid_date": str, - "gid": int32, - "price": Union[Decimal, str], - "amount": Union[Decimal, str], - "lev": int, - "delta": Union[Decimal, str], - "price_aux_limit": Union[Decimal, str], - "price_trailing": Union[Decimal, str], - "flags": int16, - "tif": Union[datetime, str] - }) + class Update(TypedDict, total=False): + id: Union[Int64, int] + cid: Union[Int45, int] + cid_date: str + gid: Union[Int32, int] + price: Union[Decimal, str] + amount: Union[Decimal, str] + lev: Union[Int32, int] + delta: Union[Decimal, str] + price_aux_limit: Union[Decimal, str] + price_trailing: Union[Decimal, str] + flags: Union[Int16, int] + tif: Union[datetime, str] - Cancel = TypedDict("Inputs.Order.Cancel", { - "id": int64, - "cid": int45, - "cid_date": str - }) + class Cancel(TypedDict, total=False): + id: Union[Int64, int] + cid: Union[Int45, int] + cid_date: Union[datetime, str] class Offer: - New = TypedDict("Inputs.Offer.New", { - "type": str, - "symbol": str, - "amount": Union[Decimal, str], - "rate": Union[Decimal, str], - "period": int, - "flags": int16 - }) + class New(TypedDict, total=False): + type: str + symbol: str + amount: Union[Decimal, str] + rate: Union[Decimal, str] + period: Union[Int32, int] + flags: Union[Int16, int] - Cancel = TypedDict("Inputs.Offer.Cancel", { - "id": int - }) + class Cancel(TypedDict, total=False): + id: Union[Int32, int] #endregion \ No newline at end of file diff --git a/examples/websocket/order_book.py b/examples/websocket/order_book.py index a100c2b..edb765e 100644 --- a/examples/websocket/order_book.py +++ b/examples/websocket/order_book.py @@ -1,13 +1,15 @@ from collections import OrderedDict +from typing import List + from bfxapi import Client, Constants from bfxapi.websocket import BfxWebsocketClient from bfxapi.websocket.enums import Channels, Errors -from bfxapi.websocket.typings import Subscriptions, TradingPairBooks, TradingPairBook +from bfxapi.websocket.typings import Subscriptions, TradingPairBook class OrderBook(object): - def __init__(self, symbols: list[str]): + def __init__(self, symbols: List[str]): self.__order_book = { symbol: { "bids": OrderedDict(), "asks": OrderedDict() @@ -50,7 +52,7 @@ 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: TradingPairBooks): +def on_t_book_snapshot(subscription: Subscriptions.Book, snapshot: List[TradingPairBook]): for data in snapshot: order_book.update(subscription["symbol"], data) diff --git a/examples/websocket/raw_order_book.py b/examples/websocket/raw_order_book.py index fe10490..b34ae8e 100644 --- a/examples/websocket/raw_order_book.py +++ b/examples/websocket/raw_order_book.py @@ -1,13 +1,15 @@ from collections import OrderedDict +from typing import List + from bfxapi import Client, Constants from bfxapi.websocket import BfxWebsocketClient from bfxapi.websocket.enums import Channels, Errors -from bfxapi.websocket.typings import Subscriptions, TradingPairRawBooks, TradingPairRawBook +from bfxapi.websocket.typings import Subscriptions, TradingPairRawBook class RawOrderBook(object): - def __init__(self, symbols: list[str]): + def __init__(self, symbols: List[str]): self.__raw_order_book = { symbol: { "bids": OrderedDict(), "asks": OrderedDict() @@ -50,7 +52,7 @@ 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: TradingPairRawBooks): +def on_t_raw_book_snapshot(subscription: Subscriptions.Book, snapshot: List[TradingPairRawBook]): for data in snapshot: raw_order_book.update(subscription["symbol"], data) diff --git a/examples/websocket/ticker.py b/examples/websocket/ticker.py index 4e5d8e7..107e367 100644 --- a/examples/websocket/ticker.py +++ b/examples/websocket/ticker.py @@ -1,14 +1,14 @@ import asyncio from bfxapi import Client, Constants -from bfxapi.websocket import Channels +from bfxapi.websocket.enums import Channels from bfxapi.websocket.typings import Subscriptions, TradingPairTicker bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) @bfx.wss.on("t_ticker_update") def on_t_ticker_update(subscription: Subscriptions.TradingPairsTicker, data: TradingPairTicker): - print(f"Subscription channel ID: {subscription['chanId']}") + print(f"Subscription with channel ID: {subscription['chanId']}") print(f"Data: {data}") diff --git a/requirements.txt b/requirements.txt index 549f7a6..71a2708 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/setup.py b/setup.py index a4c8397..963f30a 100644 --- a/setup.py +++ b/setup.py @@ -11,11 +11,16 @@ setup( description="Official Bitfinex Python API", keywords="bitfinex,api,trading", install_requires=[ - "certifi~=2022.9.24", + "certifi~=2022.12.7", "charset-normalizer~=2.1.1", "idna~=3.4", + "mypy~=0.991", + "mypy-extensions~=0.4.3", "pyee~=9.0.4", "requests~=2.28.1", + "tomli~=2.0.1", + "types-requests~=2.28.11.5", + "types-urllib3~=1.26.25.4", "typing_extensions~=4.4.0", "urllib3~=1.26.13", "websockets~=10.4",