diff --git a/bfxapi/client.py b/bfxapi/client.py index d019e90..e866235 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -1,13 +1,32 @@ +from .rest import BfxRestInterface from .websocket import BfxWebsocketClient +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" 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, + REST_HOST: str = Constants.REST_HOST, + WSS_HOST: str = Constants.WSS_HOST, + API_KEY: Optional[str] = None, + API_SECRET: Optional[str] = None, + log_level: str = "WARNING" + ): + self.rest = BfxRestInterface( + host=REST_HOST, + API_KEY=API_KEY, + API_SECRET=API_SECRET + ) + self.wss = BfxWebsocketClient( host=WSS_HOST, API_KEY=API_KEY, diff --git a/bfxapi/enums.py b/bfxapi/enums.py new file mode 100644 index 0000000..03b89bf --- /dev/null +++ b/bfxapi/enums.py @@ -0,0 +1,50 @@ +from enum import Enum + +class OrderType(str, Enum): + LIMIT = "LIMIT" + EXCHANGE_LIMIT = "EXCHANGE LIMIT" + MARKET = "MARKET" + EXCHANGE_MARKET = "EXCHANGE MARKET" + STOP = "STOP" + EXCHANGE_STOP = "EXCHANGE STOP" + STOP_LIMIT = "STOP LIMIT" + EXCHANGE_STOP_LIMIT = "EXCHANGE STOP LIMIT" + TRAILING_STOP = "TRAILING STOP" + EXCHANGE_TRAILING_STOP = "EXCHANGE TRAILING STOP" + FOK = "FOK" + EXCHANGE_FOK = "EXCHANGE FOK" + IOC = "IOC" + EXCHANGE_IOC = "EXCHANGE IOC" + +class FundingOfferType(str, Enum): + LIMIT = "LIMIT" + FRR_DELTA_FIX = "FRRDELTAFIX" + FRR_DELTA_VAR = "FRRDELTAVAR" + +class Flag(int, Enum): + HIDDEN = 64 + CLOSE = 512 + REDUCE_ONLY = 1024 + POST_ONLY = 4096 + OCO = 16384 + NO_VAR_RATES = 524288 + +class Error(int, Enum): + ERR_UNK = 10000 + ERR_GENERIC = 10001 + ERR_CONCURRENCY = 10008 + ERR_PARAMS = 10020 + ERR_CONF_FAIL = 10050 + ERR_AUTH_FAIL = 10100 + ERR_AUTH_PAYLOAD = 10111 + ERR_AUTH_SIG = 10112 + ERR_AUTH_HMAC = 10113 + ERR_AUTH_NONCE = 10114 + ERR_UNAUTH_FAIL = 10200 + ERR_SUB_FAIL = 10300 + ERR_SUB_MULTI = 10301 + ERR_SUB_UNK = 10302 + ERR_SUB_LIMIT = 10305 + ERR_UNSUB_FAIL = 10400 + ERR_UNSUB_NOT = 10401 + ERR_READY = 11000 \ No newline at end of file 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..4575146 --- /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]] = 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.") + + 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/notification.py b/bfxapi/notification.py new file mode 100644 index 0000000..90d2f12 --- /dev/null +++ b/bfxapi/notification.py @@ -0,0 +1,35 @@ +from typing import List, Dict, Union, Optional, Any, TypedDict, cast + +from .labeler import _Serializer + +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 + +class _Notification(_Serializer): + __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" ]) + + self.serializer, self.iterate = serializer, iterate + + def parse(self, *values: Any, skip: Optional[List[str]] = None) -> Notification: + notification = dict(self._serialize(*values)) + + if isinstance(self.serializer, _Serializer): + if self.iterate == False: + NOTIFY_INFO = notification["NOTIFY_INFO"] + + 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"] ] + + return cast(Notification, notification) \ No newline at end of file diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py new file mode 100644 index 0000000..c11f7af --- /dev/null +++ b/bfxapi/rest/BfxRestInterface.py @@ -0,0 +1,343 @@ +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, ids: Optional[List[str]] = None) -> List[Order]: + return [ serializers.Order.parse(*subdata) for subdata in self._POST("auth/r/orders", data={ "id": ids }) ] + + 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(self, symbol: str) -> List[Trade]: + return [ serializers.Trade.parse(*subdata) for subdata in self._POST(f"auth/r/trades/{symbol}/hist") ] + + 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_active_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)) \ No newline at end of file diff --git a/bfxapi/rest/__init__.py b/bfxapi/rest/__init__.py new file mode 100644 index 0000000..0bf3d2e --- /dev/null +++ b/bfxapi/rest/__init__.py @@ -0,0 +1 @@ +from .BfxRestInterface import BfxRestInterface \ No newline at end of file diff --git a/bfxapi/rest/enums.py b/bfxapi/rest/enums.py new file mode 100644 index 0000000..65c1e1a --- /dev/null +++ b/bfxapi/rest/enums.py @@ -0,0 +1,36 @@ +from ..enums import * + +class Config(str, Enum): + MAP_CURRENCY_SYM = "pub:map:currency:sym" + MAP_CURRENCY_LABEL = "pub:map:currency:label" + MAP_CURRENCY_UNIT = "pub:map:currency:unit" + MAP_CURRENCY_UNDL = "pub:map:currency:undl" + MAP_CURRENCY_POOL = "pub:map:currency:pool" + MAP_CURRENCY_EXPLORER = "pub:map:currency:explorer" + MAP_CURRENCY_TX_FEE = "pub:map:currency:tx:fee" + MAP_TX_METHOD = "pub:map:tx:method" + + LIST_PAIR_EXCHANGE = "pub:list:pair:exchange" + LIST_PAIR_MARGIN = "pub:list:pair:margin" + LIST_PAIR_FUTURES = "pub:list:pair:futures" + LIST_PAIR_SECURITIES = "pub:list:pair:securities" + LIST_CURRENCY = "pub:list:currency" + LIST_COMPETITIONS = "pub:list:competitions" + + INFO_PAIR = "pub:info:pair" + INFO_PAIR_FUTURES = "pub:info:pair:futures" + INFO_TX_STATUS = "pub:info:tx:status" + + SPEC_MARGIN = "pub:spec:margin", + FEES = "pub:fees" + +class Precision(str, Enum): + P0 = "P0" + P1 = "P1" + P2 = "P2" + P3 = "P3" + P4 = "P4" + +class Sort(int, Enum): + ASCENDING = +1 + DESCENDING = -1 \ No newline at end of file diff --git a/bfxapi/rest/exceptions.py b/bfxapi/rest/exceptions.py new file mode 100644 index 0000000..beff7bc --- /dev/null +++ b/bfxapi/rest/exceptions.py @@ -0,0 +1,44 @@ +from .. exceptions import BfxBaseException + +__all__ = [ + "BfxRestException", + + "RequestParametersError", + "ResourceNotFound", + "InvalidAuthenticationCredentials" +] + +class BfxRestException(BfxBaseException): + """ + Base class for all custom exceptions in bfxapi/rest/exceptions.py. + """ + + pass + +class ResourceNotFound(BfxRestException): + """ + This error indicates a failed HTTP request to a non-existent resource. + """ + + pass + +class RequestParametersError(BfxRestException): + """ + This error indicates that there are some invalid parameters sent along with an HTTP request. + """ + + pass + +class InvalidAuthenticationCredentials(BfxRestException): + """ + This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication. + """ + + pass + +class UnknownGenericError(BfxRestException): + """ + This error indicates an undefined problem processing an HTTP request sent to the APIs. + """ + + pass \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py new file mode 100644 index 0000000..ad0f2d1 --- /dev/null +++ b/bfxapi/rest/serializers.py @@ -0,0 +1,288 @@ +from . import typings + +from .. labeler import _Serializer + +from .. notification import _Notification + +#region Serializers definition for Rest Public Endpoints + +PlatformStatus = _Serializer[typings.PlatformStatus]("PlatformStatus", labels=[ + "OPERATIVE" +]) + +TradingPairTicker = _Serializer[typings.TradingPairTicker]("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", + "_PLACEHOLDER", + "_PLACEHOLDER", + "FRR_AMOUNT_AVAILABLE" +]) + +TickersHistory = _Serializer[typings.TickersHistory]("TickersHistory", labels=[ + "SYMBOL", + "BID", + "_PLACEHOLDER", + "ASK", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "MTS" +]) + +TradingPairTrade = _Serializer[typings.TradingPairTrade]("TradingPairTrade", labels=[ + "ID", + "MTS", + "AMOUNT", + "PRICE" +]) + +FundingCurrencyTrade = _Serializer[typings.FundingCurrencyTrade]("FundingCurrencyTrade", labels=[ + "ID", + "MTS", + "AMOUNT", + "RATE", + "PERIOD" +]) + +TradingPairBook = _Serializer[typings.TradingPairBook]("TradingPairBook", labels=[ + "PRICE", + "COUNT", + "AMOUNT" +]) + +FundingCurrencyBook = _Serializer[typings.FundingCurrencyBook]("FundingCurrencyBook", labels=[ + "RATE", + "PERIOD", + "COUNT", + "AMOUNT" +]) + +TradingPairRawBook = _Serializer[typings.TradingPairRawBook]("TradingPairRawBook", labels=[ + "ORDER_ID", + "PRICE", + "AMOUNT" +]) + +FundingCurrencyRawBook = _Serializer[typings.FundingCurrencyRawBook]("FundingCurrencyRawBook", labels=[ + "OFFER_ID", + "PERIOD", + "RATE", + "AMOUNT" +]) + +Statistic = _Serializer[typings.Statistic]("Statistic", labels=[ + "MTS", + "VALUE" +]) + +Candle = _Serializer[typings.Candle]("Candle", labels=[ + "MTS", + "OPEN", + "CLOSE", + "HIGH", + "LOW", + "VOLUME" +]) + +DerivativesStatus = _Serializer[typings.DerivativesStatus]("DerivativesStatus", labels=[ + "KEY", + "MTS", + "_PLACEHOLDER", + "DERIV_PRICE", + "SPOT_PRICE", + "_PLACEHOLDER", + "INSURANCE_FUND_BALANCE", + "_PLACEHOLDER", + "NEXT_FUNDING_EVT_TIMESTAMP_MS", + "NEXT_FUNDING_ACCRUED", + "NEXT_FUNDING_STEP", + "_PLACEHOLDER", + "CURRENT_FUNDING", + "_PLACEHOLDER", + "_PLACEHOLDER", + "MARK_PRICE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "OPEN_INTEREST", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "CLAMP_MIN", + "CLAMP_MAX" +]) + +Liquidation = _Serializer[typings.Liquidation]("Liquidation", labels=[ + "_PLACEHOLDER", + "POS_ID", + "MTS", + "_PLACEHOLDER", + "SYMBOL", + "AMOUNT", + "BASE_PRICE", + "_PLACEHOLDER", + "IS_MATCH", + "IS_MARKET_SOLD", + "_PLACEHOLDER", + "PRICE_ACQUIRED" +]) + +Leaderboard = _Serializer[typings.Leaderboard]("Leaderboard", labels=[ + "MTS", + "_PLACEHOLDER", + "USERNAME", + "RANKING", + "_PLACEHOLDER", + "_PLACEHOLDER", + "VALUE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "TWITTER_HANDLE" +]) + +FundingStatistic = _Serializer[typings.FundingStatistic]("FundingStatistic", labels=[ + "TIMESTAMP", + "_PLACEHOLDER", + "_PLACEHOLDER", + "FRR", + "AVG_PERIOD", + "_PLACEHOLDER", + "_PLACEHOLDER", + "FUNDING_AMOUNT", + "FUNDING_AMOUNT_USED", + "_PLACEHOLDER", + "_PLACEHOLDER", + "FUNDING_BELOW_THRESHOLD" +]) + +#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" +]) + +Order = _Serializer[typings.Order]("Order", labels=[ + "ID", + "GID", + "CID", + "SYMBOL", + "MTS_CREATE", + "MTS_UPDATE", + "AMOUNT", + "AMOUNT_ORIG", + "ORDER_TYPE", + "TYPE_PREV", + "MTS_TIF", + "_PLACEHOLDER", + "FLAGS", + "ORDER_STATUS", + "_PLACEHOLDER", + "_PLACEHOLDER", + "PRICE", + "PRICE_AVG", + "PRICE_TRAILING", + "PRICE_AUX_LIMIT", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "NOTIFY", + "HIDDEN", + "PLACED_ID", + "_PLACEHOLDER", + "_PLACEHOLDER", + "ROUTING", + "_PLACEHOLDER", + "_PLACEHOLDER", + "META" +]) + +FundingOffer = _Serializer[typings.FundingOffer]("FundingOffer", labels=[ + "ID", + "SYMBOL", + "MTS_CREATED", + "MTS_UPDATED", + "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" +]) + +Ledger = _Serializer[typings.Ledger]("Ledger", labels=[ + "ID", + "CURRENCY", + "_PLACEHOLDER", + "MTS", + "_PLACEHOLDER", + "AMOUNT", + "BALANCE", + "_PLACEHOLDER", + "DESCRIPTION" +]) + +#endregion \ No newline at end of file diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py new file mode 100644 index 0000000..20b2eed --- /dev/null +++ b/bfxapi/rest/typings.py @@ -0,0 +1,210 @@ +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 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 Ledger(TypedDict): + ID: int + CURRENCY: str + MTS: int + AMOUNT: float + BALANCE: float + description: str + +#endregion \ No newline at end of file diff --git a/bfxapi/utils/cid.py b/bfxapi/utils/cid.py new file mode 100644 index 0000000..43150bb --- /dev/null +++ b/bfxapi/utils/cid.py @@ -0,0 +1,4 @@ +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 new file mode 100644 index 0000000..3649823 --- /dev/null +++ b/bfxapi/utils/encoder.py @@ -0,0 +1,9 @@ +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 new file mode 100644 index 0000000..f897103 --- /dev/null +++ b/bfxapi/utils/flags.py @@ -0,0 +1,29 @@ +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 new file mode 100644 index 0000000..08582c6 --- /dev/null +++ b/bfxapi/utils/integers.py @@ -0,0 +1,43 @@ +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/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index e8bb4d0..7cd8728 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.encoder import JSONEncoder + 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=JSONEncoder)) 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/enums.py b/bfxapi/websocket/enums.py index 14c4234..8f06f62 100644 --- a/bfxapi/websocket/enums.py +++ b/bfxapi/websocket/enums.py @@ -1,33 +1,8 @@ -from enum import Enum +from ..enums import * class Channels(str, Enum): TICKER = "ticker" TRADES = "trades" BOOK = "book" CANDLES = "candles" - STATUS = "status" - -class Flags(int, Enum): - HIDDEN = 64 - CLOSE = 512 - REDUCE_ONLY = 1024 - POST_ONLY = 4096 - OCO = 16384 - NO_VAR_RATES = 524288 - -class Errors(int, Enum): - ERR_UNK = 10000 - ERR_GENERIC = 10001 - ERR_CONCURRENCY = 10008 - ERR_PARAMS = 10020 - ERR_CONF_FAIL = 10050 - ERR_AUTH_FAIL = 10100 - ERR_AUTH_PAYLOAD = 10111 - ERR_AUTH_SIG = 10112 - ERR_AUTH_HMAC = 10113 - ERR_AUTH_NONCE = 10114 - ERR_UNAUTH_FAIL = 10200 - ERR_SUB_FAIL = 10300 - ERR_SUB_MULTI = 10301 - ERR_UNSUB_FAIL = 10400 - ERR_READY = 11000 \ No newline at end of file + STATUS = "status" \ No newline at end of file diff --git a/bfxapi/websocket/exceptions.py b/bfxapi/websocket/exceptions.py index 3a1f900..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 bfx/websocket/errors.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..ee55dd3 100644 --- a/bfxapi/websocket/typings.py +++ b/bfxapi/websocket/typings.py @@ -4,299 +4,292 @@ from datetime import datetime from typing import Type, 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 TradingPairTicker(TypedDict): + chanId: int + symbol: str + pair: str - FundingCurrenciesTicker = TypedDict("Subscriptions.FundingCurrenciesTicker", { - "chanId": int, - "symbol": str, - "currency": str - }) + class FundingCurrencyTicker(TypedDict): + chanId: int + symbol: str + currency: str - TradingPairsTrades = TypedDict("Subscriptions.TradingPairsTrades", { - "chanId": int, - "symbol": str, - "pair": str - }) + class TradingPairTrades(TypedDict): + chanId: int + symbol: str + pair: str - FundingCurrenciesTrades = TypedDict("Subscriptions.FundingCurrenciesTrades", { - "chanId": int, - "symbol": str, - "currency": str - }) + class FundingCurrencyTrades(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 Type hinting 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/rest/create_funding_offer.py b/examples/rest/create_funding_offer.py new file mode 100644 index 0000000..ecd470b --- /dev/null +++ b/examples/rest/create_funding_offer.py @@ -0,0 +1,26 @@ +import os + +from bfxapi.client import Client, Constants +from bfxapi.enums import FundingOfferType +from bfxapi.utils.flags import calculate_offer_flags + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +notification = bfx.rest.auth.submit_funding_offer( + type=FundingOfferType.LIMIT, + symbol="fUSD", + amount="123.45", + rate="0.001", + period=2, + flags=calculate_offer_flags(hidden=True) +) + +print("Offer notification:", notification) + +offers = bfx.rest.auth.get_active_funding_offers() + +print("Offers:", offers) \ No newline at end of file diff --git a/examples/rest/create_order.py b/examples/rest/create_order.py new file mode 100644 index 0000000..34408aa --- /dev/null +++ b/examples/rest/create_order.py @@ -0,0 +1,36 @@ +import os + +from bfxapi.client import Client, Constants +from bfxapi.enums import OrderType +from bfxapi.utils.flags import calculate_order_flags + +bfx = Client( + REST_HOST=Constants.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( + type=OrderType.EXCHANGE_LIMIT, + symbol="tBTCUST", + amount="0.015", + price="10000", + flags=calculate_order_flags(hidden=False) +) + +print("Submit Order Notification:", submitted_order) + +# Update it +updated_order = bfx.rest.auth.update_order( + id=submitted_order["NOTIFY_INFO"]["ID"], + amount="0.020", + price="10100" +) + +print("Update Order Notification:", updated_order) + +# Delete it +canceled_order = bfx.rest.auth.cancel_order(id=submitted_order["NOTIFY_INFO"]["ID"]) + +print("Cancel Order Notification:", canceled_order) diff --git a/examples/websocket/create_order.py b/examples/websocket/create_order.py new file mode 100644 index 0000000..36cd5c2 --- /dev/null +++ b/examples/websocket/create_order.py @@ -0,0 +1,47 @@ +# python -c "from examples.websocket.create_order import *" + +import os + +from bfxapi.client import Client, Constants +from bfxapi.utils.cid import generate_unique_cid +from bfxapi.websocket.enums import Error, OrderType +from bfxapi.websocket.typings import Inputs + +bfx = Client( + WSS_HOST=Constants.WSS_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +@bfx.wss.on("wss-error") +def on_wss_error(code: Error, msg: str): + print(code, msg) + +@bfx.wss.on("authenticated") +async def on_open(event): + print(f"Auth event {event}") + + order: Inputs.Order.New = { + "gid": generate_unique_cid(), + "type": OrderType.EXCHANGE_LIMIT, + "symbol": "tBTCUST", + "amount": "0.1", + "price": "10000.0" + } + await bfx.wss.inputs.order_new(order) + + print(f"Order sent") + +@bfx.wss.on("notification") +async def on_notification(notification): + print(f"Notification {notification}") + +@bfx.wss.on("order_new") +async def on_order_new(order_new: Inputs.Order.New): + print(f"Order new {order_new}") + +@bfx.wss.on("subscribed") +def on_subscribed(subscription): + print(f"Subscription successful <{subscription}>") + +bfx.wss.run() \ No newline at end of file diff --git a/examples/websocket/order_book.py b/examples/websocket/order_book.py new file mode 100644 index 0000000..0035cf8 --- /dev/null +++ b/examples/websocket/order_book.py @@ -0,0 +1,64 @@ +# python -c "from examples.websocket.order_book import *" + +from collections import OrderedDict + +from typing import List + +from bfxapi import Client, Constants + +from bfxapi.websocket.enums import Channels, Error +from bfxapi.websocket.typings import Subscriptions, TradingPairBook + +class OrderBook(object): + def __init__(self, symbols: List[str]): + self.__order_book = { + symbol: { + "bids": OrderedDict(), "asks": OrderedDict() + } for symbol in symbols + } + + def update(self, symbol: str, data: TradingPairBook) -> None: + price, count, amount = data["PRICE"], data["COUNT"], data["AMOUNT"] + + kind = (amount > 0) and "bids" or "asks" + + if count > 0: + self.__order_book[symbol][kind][price] = { + "price": price, + "count": count, + "amount": amount + } + + if count == 0: + if price in self.__order_book[symbol][kind]: + del self.__order_book[symbol][kind][price] + +SYMBOLS = [ "tBTCUSD", "tLTCUSD", "tLTCBTC", "tETHUSD", "tETHBTC" ] + +order_book = OrderBook(symbols=SYMBOLS) + +bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) + +@bfx.wss.on("wss-error") +def on_wss_error(code: Error, msg: str): + print(code, msg) + +@bfx.wss.on("open") +async def on_open(): + for symbol in SYMBOLS: + await bfx.wss.subscribe(Channels.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]): + 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): + 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 new file mode 100644 index 0000000..6cfc3c1 --- /dev/null +++ b/examples/websocket/raw_order_book.py @@ -0,0 +1,64 @@ +# python -c "from examples.websocket.raw_order_book import *" + +from collections import OrderedDict + +from typing import List + +from bfxapi import Client, Constants + +from bfxapi.websocket.enums import Channels, Error +from bfxapi.websocket.typings import Subscriptions, TradingPairRawBook + +class RawOrderBook(object): + def __init__(self, symbols: List[str]): + self.__raw_order_book = { + symbol: { + "bids": OrderedDict(), "asks": OrderedDict() + } for symbol in symbols + } + + def update(self, symbol: str, data: TradingPairRawBook) -> None: + order_id, price, amount = data["ORDER_ID"], data["PRICE"], data["AMOUNT"] + + kind = (amount > 0) and "bids" or "asks" + + if price > 0: + self.__raw_order_book[symbol][kind][order_id] = { + "order_id": order_id, + "price": price, + "amount": amount + } + + if price == 0: + if order_id in self.__raw_order_book[symbol][kind]: + del self.__raw_order_book[symbol][kind][order_id] + +SYMBOLS = [ "tBTCUSD", "tLTCUSD", "tLTCBTC", "tETHUSD", "tETHBTC" ] + +raw_order_book = RawOrderBook(symbols=SYMBOLS) + +bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) + +@bfx.wss.on("wss-error") +def on_wss_error(code: Error, msg: str): + print(code, msg) + +@bfx.wss.on("open") +async def on_open(): + for symbol in SYMBOLS: + await bfx.wss.subscribe(Channels.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]): + 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): + 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 4e5d8e7..5db8ed1 100644 --- a/examples/websocket/ticker.py +++ b/examples/websocket/ticker.py @@ -1,14 +1,16 @@ +# python -c "from examples.websocket.ticker import *" + 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']}") +def on_t_ticker_update(subscription: Subscriptions.TradingPairTicker, data: TradingPairTicker): + print(f"Subscription with channel ID: {subscription['chanId']}") print(f"Data: {data}") @@ -16,4 +18,4 @@ def on_t_ticker_update(subscription: Subscriptions.TradingPairsTicker, data: Tra async def open(): await bfx.wss.subscribe(Channels.TICKER, symbol="tBTCUSD") -asyncio.run(bfx.wss.start()) \ No newline at end of file +bfx.wss.run() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 78560fa..71a2708 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/setup.py b/setup.py index c130b0d..963f30a 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from distutils.core import setup setup( name="bitfinex-api-py", version="3.0.0", - packages=[ "bfxapi", "bfxapi.websocket", "bfxapi.utils" ], + packages=[ "bfxapi", "bfxapi.websocket", "bfxapi.rest", "bfxapi.utils" ], url="https://github.com/bitfinexcom/bitfinex-api-py", license="OSI Approved :: Apache Software License", author="Bitfinex", @@ -11,8 +11,18 @@ setup( description="Official Bitfinex Python API", keywords="bitfinex,api,trading", install_requires=[ + "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", ], project_urls={