From 903f68c6e3da739f7288950388947ceb2d387bb4 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 13 Jan 2023 18:15:29 +0100 Subject: [PATCH 01/85] Add support for SimpleNamespace (instead of TypedDict) in bfxapi/labeler.py and bfxapi/notifications.py. Add generics Notification type in notifications.py. Add support for new changes in bfxapi/rest/BfxRestInterface.py. --- bfxapi/labeler.py | 4 ++- bfxapi/notification.py | 26 +++++++++-------- bfxapi/rest/BfxRestInterface.py | 30 ++++++++++---------- bfxapi/rest/typings.py | 50 +++++++++++++++++---------------- 4 files changed, 59 insertions(+), 51 deletions(-) diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 4575146..8a5845e 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -2,6 +2,8 @@ from .exceptions import LabelerSerializerException from typing import Generic, TypeVar, Iterable, Optional, List, Tuple, Any, cast +from types import SimpleNamespace + T = TypeVar("T") class _Serializer(Generic[T]): @@ -19,4 +21,4 @@ class _Serializer(Generic[T]): yield label, args[index] def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: - return cast(T, dict(self._serialize(*values, skip=skip))) \ No newline at end of file + return cast(T, SimpleNamespace(**dict(self._serialize(*values, skip=skip)))) \ No newline at end of file diff --git a/bfxapi/notification.py b/bfxapi/notification.py index 90d2f12..413f1b6 100644 --- a/bfxapi/notification.py +++ b/bfxapi/notification.py @@ -1,17 +1,21 @@ -from typing import List, Dict, Union, Optional, Any, TypedDict, cast +from typing import List, Dict, Union, Optional, Any, TypedDict, Generic, TypeVar, cast + +from types import SimpleNamespace from .labeler import _Serializer -class Notification(TypedDict): +T = TypeVar("T") + +class Notification(SimpleNamespace, Generic[T]): MTS: int TYPE: str MESSAGE_ID: Optional[int] - NOTIFY_INFO: Union[Dict[str, Any], List[Dict[str, Any]]] + NOTIFY_INFO: T CODE: Optional[int] STATUS: str TEXT: str -class _Notification(_Serializer): +class _Notification(_Serializer, Generic[T]): __LABELS = [ "MTS", "TYPE", "MESSAGE_ID", "_PLACEHOLDER", "NOTIFY_INFO", "CODE", "STATUS", "TEXT" ] def __init__(self, serializer: Optional[_Serializer] = None, iterate: bool = False): @@ -19,17 +23,17 @@ class _Notification(_Serializer): self.serializer, self.iterate = serializer, iterate - def parse(self, *values: Any, skip: Optional[List[str]] = None) -> Notification: - notification = dict(self._serialize(*values)) + def parse(self, *values: Any, skip: Optional[List[str]] = None) -> Notification[T]: + notification = cast(Notification[T], SimpleNamespace(**dict(self._serialize(*values)))) if isinstance(self.serializer, _Serializer): - if self.iterate == False: - NOTIFY_INFO = notification["NOTIFY_INFO"] + NOTIFY_INFO = cast(List[Any], notification.NOTIFY_INFO) + if self.iterate == False: if len(NOTIFY_INFO) == 1 and isinstance(NOTIFY_INFO[0], list): NOTIFY_INFO = NOTIFY_INFO[0] - notification["NOTIFY_INFO"] = dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip)) - else: notification["NOTIFY_INFO"] = [ dict(self.serializer._serialize(*data, skip=skip)) for data in notification["NOTIFY_INFO"] ] + notification.NOTIFY_INFO = cast(T, SimpleNamespace(**dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip)))) + else: notification.NOTIFY_INFO = cast(T, [ SimpleNamespace(**dict(self.serializer._serialize(*data, skip=skip))) for data in NOTIFY_INFO ]) - return cast(Notification, notification) \ No newline at end of file + return notification \ No newline at end of file diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 3a02593..b967775 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -97,11 +97,11 @@ class _RestPublicEndpoints(_Requests): parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse } - return [ parsers[subdata[0][0]](*subdata) for subdata in data ] + return [ cast(Union[TradingPairTicker, FundingCurrencyTicker], 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") ] + 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 ]) @@ -109,7 +109,7 @@ class _RestPublicEndpoints(_Requests): 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") ] + 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 ]) @@ -262,7 +262,7 @@ class _RestAuthenticatedEndpoints(_Requests): 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: + flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification[Order]: data = { "type": type, "symbol": symbol, "amount": amount, "price": price, "lev": lev, @@ -271,12 +271,12 @@ class _RestAuthenticatedEndpoints(_Requests): "flags": flags, "tif": tif, "meta": meta } - return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/submit", data=data)) + return serializers._Notification[Order](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: + price_aux_limit: Optional[Union[Decimal, str]] = None, price_trailing: Optional[Union[Decimal, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification[Order]: data = { "id": id, "amount": amount, "price": price, "cid": cid, "cid_date": cid_date, "gid": gid, @@ -284,18 +284,18 @@ class _RestAuthenticatedEndpoints(_Requests): "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)) + return serializers._Notification[Order](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: + def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None) -> Notification[Order]: data = { "id": id, "cid": cid, "cid_date": cid_date } - return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/cancel", data=data)) + return serializers._Notification[Order](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: + def cancel_order_multi(self, ids: Optional[List[int]] = None, cids: Optional[List[Tuple[int, str]]] = None, gids: Optional[List[int]] = None, all: bool = False) -> Notification[List[Order]]: data = { "ids": ids, "cids": cids, @@ -304,7 +304,7 @@ class _RestAuthenticatedEndpoints(_Requests): "all": int(all) } - return serializers._Notification(serializer=serializers.Order, iterate=True).parse(*self._POST("auth/w/order/cancel/multi", data=data)) + return serializers._Notification[List[Order]](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: @@ -354,17 +354,17 @@ class _RestAuthenticatedEndpoints(_Requests): def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, str], rate: Union[Decimal, str], period: int, - flags: Optional[int] = 0) -> Notification: + flags: Optional[int] = 0) -> Notification[FundingOffer]: 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)) + return serializers._Notification[FundingOffer](serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/submit", data=data)) - def cancel_funding_offer(self, id: int) -> Notification: - return serializers._Notification(serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/cancel", data={ "id": id })) + def cancel_funding_offer(self, id: int) -> Notification[FundingOffer]: + return serializers._Notification[FundingOffer](serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/cancel", data={ "id": id })) def get_funding_offers_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingOffer]: if symbol == None: diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index d9d37a7..1685b04 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -1,15 +1,17 @@ from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any +from types import SimpleNamespace + 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): +class PlatformStatus(SimpleNamespace): OPERATIVE: int -class TradingPairTicker(TypedDict): +class TradingPairTicker(SimpleNamespace): SYMBOL: Optional[str] BID: float BID_SIZE: float @@ -22,7 +24,7 @@ class TradingPairTicker(TypedDict): HIGH: float LOW: float -class FundingCurrencyTicker(TypedDict): +class FundingCurrencyTicker(SimpleNamespace): SYMBOL: Optional[str] FRR: float BID: float @@ -39,52 +41,52 @@ class FundingCurrencyTicker(TypedDict): LOW: float FRR_AMOUNT_AVAILABLE: float -class TickersHistory(TypedDict): +class TickersHistory(SimpleNamespace): SYMBOL: str BID: float ASK: float MTS: int -class TradingPairTrade(TypedDict): +class TradingPairTrade(SimpleNamespace): ID: int MTS: int AMOUNT: float PRICE: float -class FundingCurrencyTrade(TypedDict): +class FundingCurrencyTrade(SimpleNamespace): ID: int MTS: int AMOUNT: float RATE: float PERIOD: int -class TradingPairBook(TypedDict): +class TradingPairBook(SimpleNamespace): PRICE: float COUNT: int AMOUNT: float -class FundingCurrencyBook(TypedDict): +class FundingCurrencyBook(SimpleNamespace): RATE: float PERIOD: int COUNT: int AMOUNT: float -class TradingPairRawBook(TypedDict): +class TradingPairRawBook(SimpleNamespace): ORDER_ID: int PRICE: float AMOUNT: float -class FundingCurrencyRawBook(TypedDict): +class FundingCurrencyRawBook(SimpleNamespace): OFFER_ID: int PERIOD: int RATE: float AMOUNT: float -class Statistic(TypedDict): +class Statistic(SimpleNamespace): MTS: int VALUE: float -class Candle(TypedDict): +class Candle(SimpleNamespace): MTS: int OPEN: float CLOSE: float @@ -92,7 +94,7 @@ class Candle(TypedDict): LOW: float VOLUME: float -class DerivativesStatus(TypedDict): +class DerivativesStatus(SimpleNamespace): KEY: Optional[str] MTS: int DERIV_PRICE: float @@ -107,7 +109,7 @@ class DerivativesStatus(TypedDict): CLAMP_MIN: float CLAMP_MAX: float -class Liquidation(TypedDict): +class Liquidation(SimpleNamespace): POS_ID: int MTS: int SYMBOL: str @@ -117,14 +119,14 @@ class Liquidation(TypedDict): IS_MARKET_SOLD: int PRICE_ACQUIRED: float -class Leaderboard(TypedDict): +class Leaderboard(SimpleNamespace): MTS: int USERNAME: str RANKING: int VALUE: float TWITTER_HANDLE: Optional[str] -class FundingStatistic(TypedDict): +class FundingStatistic(SimpleNamespace): TIMESTAMP: int FRR: float AVG_PERIOD: float @@ -136,7 +138,7 @@ class FundingStatistic(TypedDict): #region Type hinting for Rest Authenticated Endpoints -class Wallet(TypedDict): +class Wallet(SimpleNamespace): WALLET_TYPE: str CURRENCY: str BALANCE: float @@ -145,7 +147,7 @@ class Wallet(TypedDict): LAST_CHANGE: str TRADE_DETAILS: JSON -class Order(TypedDict): +class Order(SimpleNamespace): ID: int GID: int CID: int @@ -169,7 +171,7 @@ class Order(TypedDict): ROUTING: str META: JSON -class Position(TypedDict): +class Position(SimpleNamespace): SYMBOL: str STATUS: str AMOUNT: float @@ -188,7 +190,7 @@ class Position(TypedDict): COLLATERAL_MIN: float META: JSON -class FundingOffer(TypedDict): +class FundingOffer(SimpleNamespace): ID: int SYMBOL: str MTS_CREATE: int @@ -204,7 +206,7 @@ class FundingOffer(TypedDict): HIDDEN: int RENEW: bool -class Trade(TypedDict): +class Trade(SimpleNamespace): ID: int SYMBOL: str MTS_CREATE: int @@ -218,7 +220,7 @@ class Trade(TypedDict): FEE_CURRENCY: str CID: int -class OrderTrade(TypedDict): +class OrderTrade(SimpleNamespace): ID: int SYMBOL: str MTS_CREATE: int @@ -230,7 +232,7 @@ class OrderTrade(TypedDict): FEE_CURRENCY: str CID: int -class Ledger(TypedDict): +class Ledger(SimpleNamespace): ID: int CURRENCY: str MTS: int @@ -238,7 +240,7 @@ class Ledger(TypedDict): BALANCE: float description: str -class FundingCredit(TypedDict): +class FundingCredit(SimpleNamespace): ID: int SYMBOL: str SIDE: int From e9ef39c1d68bca20a9fb46bf4db457d3027bb600 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Sun, 15 Jan 2023 23:19:09 +0100 Subject: [PATCH 02/85] add rest examples --- bfxapi/rest/BfxRestInterface.py | 6 ++-- examples/rest/get_liquidations.py | 14 +++++++++ examples/rest/get_public_data.py | 52 +++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 examples/rest/get_liquidations.py create mode 100644 examples/rest/get_public_data.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 3a02593..3b6db61 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -190,10 +190,10 @@ class _RestPublicEndpoints(_Requests): 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) } + def get_derivatives_status(self, symbols: Union[List[str], Literal["ALL"]]) -> List[DerivativesStatus]: + params = { "keys": ",".join(symbols) if type(symbols) == List else "ALL" } - data = self._GET(f"status/{type}", params=params) + data = self._GET(f"status/deriv", params=params) return [ serializers.DerivativesStatus.parse(*subdata) for subdata in data ] diff --git a/examples/rest/get_liquidations.py b/examples/rest/get_liquidations.py new file mode 100644 index 0000000..34ed47d --- /dev/null +++ b/examples/rest/get_liquidations.py @@ -0,0 +1,14 @@ +# python -c "from examples.rest.get_liquidations import *" + +import time + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST +) + +now = int(round(time.time() * 1000)) + +liquidations = bfx.rest.public.get_liquidations(start=0, end=now) +print(f"Liquidations: {liquidations}") \ No newline at end of file diff --git a/examples/rest/get_public_data.py b/examples/rest/get_public_data.py new file mode 100644 index 0000000..ff86c14 --- /dev/null +++ b/examples/rest/get_public_data.py @@ -0,0 +1,52 @@ +# python -c "from examples.rest.get_public_data import *" + +import time + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST +) + +now = int(round(time.time() * 1000)) + + +def log_historical_candles(): + candles = bfx.rest.public.get_candles_hist(start=0, end=now, resource='trade:1m:tBTCUSD') + print("Candles:") + [print(c) for c in candles] + + +def log_historical_trades(): + trades = bfx.rest.public.get_t_trades(pair='tBTCUSD', start=0, end=now) + print("Trades:") + [print(t) for t in trades] + + +def log_books(): + orders = bfx.rest.public.get_t_book(pair='BTCUSD', precision='P0') + print("Order book:") + [print(o) for o in orders] + + +def log_tickers(): + tickers = bfx.rest.public.get_t_tickers(pairs=['BTCUSD']) + print("Tickers:") + print(tickers) + + +def log_derivative_status(): + status = bfx.rest.public.get_derivatives_status('ALL') + print("Deriv status:") + print(status) + + +def run(): + log_historical_candles() + log_historical_trades() + log_books() + log_tickers() + log_derivative_status() + + +run() \ No newline at end of file From 0bb9f65a190656d1707f9a0cedf6ee98f5a2c401 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 16 Jan 2023 16:30:06 +0100 Subject: [PATCH 03/85] Replace SimpleNamespaces with dataclasses. Add base class _Typing in labeler.py to convert dictionaries to dataclasses. Remove SimpleNamespace references. --- bfxapi/labeler.py | 9 +++-- bfxapi/notification.py | 13 +++---- bfxapi/rest/typings.py | 80 ++++++++++++++++++++++++++++-------------- 3 files changed, 66 insertions(+), 36 deletions(-) diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 8a5845e..160e054 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -2,10 +2,13 @@ from .exceptions import LabelerSerializerException from typing import Generic, TypeVar, Iterable, Optional, List, Tuple, Any, cast -from types import SimpleNamespace - T = TypeVar("T") +class _Typing(object): + def __init__(self, **kwargs): + for key, value in kwargs.items(): + self.__setattr__(key,value) + 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 @@ -21,4 +24,4 @@ class _Serializer(Generic[T]): yield label, args[index] def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: - return cast(T, SimpleNamespace(**dict(self._serialize(*values, skip=skip)))) \ No newline at end of file + return cast(T, _Typing(**dict(self._serialize(*values, skip=skip)))) \ No newline at end of file diff --git a/bfxapi/notification.py b/bfxapi/notification.py index 413f1b6..b8cdb37 100644 --- a/bfxapi/notification.py +++ b/bfxapi/notification.py @@ -1,12 +1,13 @@ from typing import List, Dict, Union, Optional, Any, TypedDict, Generic, TypeVar, cast -from types import SimpleNamespace +from dataclasses import dataclass -from .labeler import _Serializer +from .labeler import _Typing, _Serializer T = TypeVar("T") -class Notification(SimpleNamespace, Generic[T]): +@dataclass +class Notification(_Typing, Generic[T]): MTS: int TYPE: str MESSAGE_ID: Optional[int] @@ -24,7 +25,7 @@ class _Notification(_Serializer, Generic[T]): self.serializer, self.iterate = serializer, iterate def parse(self, *values: Any, skip: Optional[List[str]] = None) -> Notification[T]: - notification = cast(Notification[T], SimpleNamespace(**dict(self._serialize(*values)))) + notification = cast(Notification[T], _Typing(**dict(self._serialize(*values)))) if isinstance(self.serializer, _Serializer): NOTIFY_INFO = cast(List[Any], notification.NOTIFY_INFO) @@ -33,7 +34,7 @@ class _Notification(_Serializer, Generic[T]): if len(NOTIFY_INFO) == 1 and isinstance(NOTIFY_INFO[0], list): NOTIFY_INFO = NOTIFY_INFO[0] - notification.NOTIFY_INFO = cast(T, SimpleNamespace(**dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip)))) - else: notification.NOTIFY_INFO = cast(T, [ SimpleNamespace(**dict(self.serializer._serialize(*data, skip=skip))) for data in NOTIFY_INFO ]) + notification.NOTIFY_INFO = cast(T, _Typing(**dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip)))) + else: notification.NOTIFY_INFO = cast(T, [ _Typing(**dict(self.serializer._serialize(*data, skip=skip))) for data in NOTIFY_INFO ]) return notification \ No newline at end of file diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index 1685b04..ef397a9 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -1,6 +1,8 @@ from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any -from types import SimpleNamespace +from dataclasses import dataclass + +from .. labeler import _Typing from .. notification import Notification @@ -8,10 +10,12 @@ JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] #region Type hinting for Rest Public Endpoints -class PlatformStatus(SimpleNamespace): +@dataclass +class PlatformStatus(_Typing): OPERATIVE: int -class TradingPairTicker(SimpleNamespace): +@dataclass +class TradingPairTicker(_Typing): SYMBOL: Optional[str] BID: float BID_SIZE: float @@ -24,7 +28,8 @@ class TradingPairTicker(SimpleNamespace): HIGH: float LOW: float -class FundingCurrencyTicker(SimpleNamespace): +@dataclass +class FundingCurrencyTicker(_Typing): SYMBOL: Optional[str] FRR: float BID: float @@ -41,52 +46,61 @@ class FundingCurrencyTicker(SimpleNamespace): LOW: float FRR_AMOUNT_AVAILABLE: float -class TickersHistory(SimpleNamespace): +@dataclass +class TickersHistory(_Typing): SYMBOL: str BID: float ASK: float MTS: int -class TradingPairTrade(SimpleNamespace): +@dataclass +class TradingPairTrade(_Typing): ID: int MTS: int AMOUNT: float PRICE: float -class FundingCurrencyTrade(SimpleNamespace): +@dataclass +class FundingCurrencyTrade(_Typing): ID: int MTS: int AMOUNT: float RATE: float PERIOD: int -class TradingPairBook(SimpleNamespace): +@dataclass +class TradingPairBook(_Typing): PRICE: float COUNT: int AMOUNT: float -class FundingCurrencyBook(SimpleNamespace): +@dataclass +class FundingCurrencyBook(_Typing): RATE: float PERIOD: int COUNT: int AMOUNT: float - -class TradingPairRawBook(SimpleNamespace): + +@dataclass +class TradingPairRawBook(_Typing): ORDER_ID: int PRICE: float AMOUNT: float - -class FundingCurrencyRawBook(SimpleNamespace): + +@dataclass +class FundingCurrencyRawBook(_Typing): OFFER_ID: int PERIOD: int RATE: float AMOUNT: float -class Statistic(SimpleNamespace): +@dataclass +class Statistic(_Typing): MTS: int VALUE: float -class Candle(SimpleNamespace): +@dataclass +class Candle(_Typing): MTS: int OPEN: float CLOSE: float @@ -94,7 +108,8 @@ class Candle(SimpleNamespace): LOW: float VOLUME: float -class DerivativesStatus(SimpleNamespace): +@dataclass +class DerivativesStatus(_Typing): KEY: Optional[str] MTS: int DERIV_PRICE: float @@ -109,7 +124,8 @@ class DerivativesStatus(SimpleNamespace): CLAMP_MIN: float CLAMP_MAX: float -class Liquidation(SimpleNamespace): +@dataclass +class Liquidation(_Typing): POS_ID: int MTS: int SYMBOL: str @@ -119,14 +135,16 @@ class Liquidation(SimpleNamespace): IS_MARKET_SOLD: int PRICE_ACQUIRED: float -class Leaderboard(SimpleNamespace): +@dataclass +class Leaderboard(_Typing): MTS: int USERNAME: str RANKING: int VALUE: float TWITTER_HANDLE: Optional[str] -class FundingStatistic(SimpleNamespace): +@dataclass +class FundingStatistic(_Typing): TIMESTAMP: int FRR: float AVG_PERIOD: float @@ -138,7 +156,8 @@ class FundingStatistic(SimpleNamespace): #region Type hinting for Rest Authenticated Endpoints -class Wallet(SimpleNamespace): +@dataclass +class Wallet(_Typing): WALLET_TYPE: str CURRENCY: str BALANCE: float @@ -147,7 +166,8 @@ class Wallet(SimpleNamespace): LAST_CHANGE: str TRADE_DETAILS: JSON -class Order(SimpleNamespace): +@dataclass +class Order(_Typing): ID: int GID: int CID: int @@ -171,7 +191,8 @@ class Order(SimpleNamespace): ROUTING: str META: JSON -class Position(SimpleNamespace): +@dataclass +class Position(_Typing): SYMBOL: str STATUS: str AMOUNT: float @@ -190,7 +211,8 @@ class Position(SimpleNamespace): COLLATERAL_MIN: float META: JSON -class FundingOffer(SimpleNamespace): +@dataclass +class FundingOffer(_Typing): ID: int SYMBOL: str MTS_CREATE: int @@ -206,7 +228,8 @@ class FundingOffer(SimpleNamespace): HIDDEN: int RENEW: bool -class Trade(SimpleNamespace): +@dataclass +class Trade(_Typing): ID: int SYMBOL: str MTS_CREATE: int @@ -220,7 +243,8 @@ class Trade(SimpleNamespace): FEE_CURRENCY: str CID: int -class OrderTrade(SimpleNamespace): +@dataclass +class OrderTrade(_Typing): ID: int SYMBOL: str MTS_CREATE: int @@ -232,7 +256,8 @@ class OrderTrade(SimpleNamespace): FEE_CURRENCY: str CID: int -class Ledger(SimpleNamespace): +@dataclass +class Ledger(_Typing): ID: int CURRENCY: str MTS: int @@ -240,7 +265,8 @@ class Ledger(SimpleNamespace): BALANCE: float description: str -class FundingCredit(SimpleNamespace): +@dataclass +class FundingCredit(_Typing): ID: int SYMBOL: str SIDE: int From 1613a56d813e1195e1c01fb5b3e3299a33224e50 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 16 Jan 2023 16:40:14 +0100 Subject: [PATCH 04/85] Rename bfxapi/rest/typings.py to bfxapi/rest/types.py. --- bfxapi/labeler.py | 4 +-- bfxapi/notification.py | 10 +++--- bfxapi/rest/BfxRestInterface.py | 2 +- bfxapi/rest/serializers.py | 50 ++++++++++++++-------------- bfxapi/rest/{typings.py => types.py} | 50 ++++++++++++++-------------- 5 files changed, 58 insertions(+), 58 deletions(-) rename bfxapi/rest/{typings.py => types.py} (85%) diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 160e054..9714b5c 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -4,7 +4,7 @@ from typing import Generic, TypeVar, Iterable, Optional, List, Tuple, Any, cast T = TypeVar("T") -class _Typing(object): +class _Type(object): def __init__(self, **kwargs): for key, value in kwargs.items(): self.__setattr__(key,value) @@ -24,4 +24,4 @@ class _Serializer(Generic[T]): yield label, args[index] def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: - return cast(T, _Typing(**dict(self._serialize(*values, skip=skip)))) \ No newline at end of file + return cast(T, _Type(**dict(self._serialize(*values, skip=skip)))) \ No newline at end of file diff --git a/bfxapi/notification.py b/bfxapi/notification.py index b8cdb37..e66ba60 100644 --- a/bfxapi/notification.py +++ b/bfxapi/notification.py @@ -2,12 +2,12 @@ from typing import List, Dict, Union, Optional, Any, TypedDict, Generic, TypeVar from dataclasses import dataclass -from .labeler import _Typing, _Serializer +from .labeler import _Type, _Serializer T = TypeVar("T") @dataclass -class Notification(_Typing, Generic[T]): +class Notification(_Type, Generic[T]): MTS: int TYPE: str MESSAGE_ID: Optional[int] @@ -25,7 +25,7 @@ class _Notification(_Serializer, Generic[T]): self.serializer, self.iterate = serializer, iterate def parse(self, *values: Any, skip: Optional[List[str]] = None) -> Notification[T]: - notification = cast(Notification[T], _Typing(**dict(self._serialize(*values)))) + notification = cast(Notification[T], _Type(**dict(self._serialize(*values)))) if isinstance(self.serializer, _Serializer): NOTIFY_INFO = cast(List[Any], notification.NOTIFY_INFO) @@ -34,7 +34,7 @@ class _Notification(_Serializer, Generic[T]): if len(NOTIFY_INFO) == 1 and isinstance(NOTIFY_INFO[0], list): NOTIFY_INFO = NOTIFY_INFO[0] - notification.NOTIFY_INFO = cast(T, _Typing(**dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip)))) - else: notification.NOTIFY_INFO = cast(T, [ _Typing(**dict(self.serializer._serialize(*data, skip=skip))) for data in NOTIFY_INFO ]) + notification.NOTIFY_INFO = cast(T, _Type(**dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip)))) + else: notification.NOTIFY_INFO = cast(T, [ _Type(**dict(self.serializer._serialize(*data, skip=skip))) for data in NOTIFY_INFO ]) return notification \ No newline at end of file diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index b967775..33f5657 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -8,7 +8,7 @@ from typing import List, Union, Literal, Optional, Any, cast from . import serializers -from .typings import * +from .types import * from .enums import Config, Sort, OrderType, FundingOfferType, Error from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 9e2612a..4196b52 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -1,4 +1,4 @@ -from . import typings +from . import types from .. labeler import _Serializer @@ -6,11 +6,11 @@ from .. notification import _Notification #region Serializers definition for Rest Public Endpoints -PlatformStatus = _Serializer[typings.PlatformStatus]("PlatformStatus", labels=[ +PlatformStatus = _Serializer[types.PlatformStatus]("PlatformStatus", labels=[ "OPERATIVE" ]) -TradingPairTicker = _Serializer[typings.TradingPairTicker]("TradingPairTicker", labels=[ +TradingPairTicker = _Serializer[types.TradingPairTicker]("TradingPairTicker", labels=[ "SYMBOL", "BID", "BID_SIZE", @@ -24,7 +24,7 @@ TradingPairTicker = _Serializer[typings.TradingPairTicker]("TradingPairTicker", "LOW" ]) -FundingCurrencyTicker = _Serializer[typings.FundingCurrencyTicker]("FundingCurrencyTicker", labels=[ +FundingCurrencyTicker = _Serializer[types.FundingCurrencyTicker]("FundingCurrencyTicker", labels=[ "SYMBOL", "FRR", "BID", @@ -44,7 +44,7 @@ FundingCurrencyTicker = _Serializer[typings.FundingCurrencyTicker]("FundingCurre "FRR_AMOUNT_AVAILABLE" ]) -TickersHistory = _Serializer[typings.TickersHistory]("TickersHistory", labels=[ +TickersHistory = _Serializer[types.TickersHistory]("TickersHistory", labels=[ "SYMBOL", "BID", "_PLACEHOLDER", @@ -60,14 +60,14 @@ TickersHistory = _Serializer[typings.TickersHistory]("TickersHistory", labels=[ "MTS" ]) -TradingPairTrade = _Serializer[typings.TradingPairTrade]("TradingPairTrade", labels=[ +TradingPairTrade = _Serializer[types.TradingPairTrade]("TradingPairTrade", labels=[ "ID", "MTS", "AMOUNT", "PRICE" ]) -FundingCurrencyTrade = _Serializer[typings.FundingCurrencyTrade]("FundingCurrencyTrade", labels=[ +FundingCurrencyTrade = _Serializer[types.FundingCurrencyTrade]("FundingCurrencyTrade", labels=[ "ID", "MTS", "AMOUNT", @@ -75,38 +75,38 @@ FundingCurrencyTrade = _Serializer[typings.FundingCurrencyTrade]("FundingCurrenc "PERIOD" ]) -TradingPairBook = _Serializer[typings.TradingPairBook]("TradingPairBook", labels=[ +TradingPairBook = _Serializer[types.TradingPairBook]("TradingPairBook", labels=[ "PRICE", "COUNT", "AMOUNT" ]) -FundingCurrencyBook = _Serializer[typings.FundingCurrencyBook]("FundingCurrencyBook", labels=[ +FundingCurrencyBook = _Serializer[types.FundingCurrencyBook]("FundingCurrencyBook", labels=[ "RATE", "PERIOD", "COUNT", "AMOUNT" ]) -TradingPairRawBook = _Serializer[typings.TradingPairRawBook]("TradingPairRawBook", labels=[ +TradingPairRawBook = _Serializer[types.TradingPairRawBook]("TradingPairRawBook", labels=[ "ORDER_ID", "PRICE", "AMOUNT" ]) -FundingCurrencyRawBook = _Serializer[typings.FundingCurrencyRawBook]("FundingCurrencyRawBook", labels=[ +FundingCurrencyRawBook = _Serializer[types.FundingCurrencyRawBook]("FundingCurrencyRawBook", labels=[ "OFFER_ID", "PERIOD", "RATE", "AMOUNT" ]) -Statistic = _Serializer[typings.Statistic]("Statistic", labels=[ +Statistic = _Serializer[types.Statistic]("Statistic", labels=[ "MTS", "VALUE" ]) -Candle = _Serializer[typings.Candle]("Candle", labels=[ +Candle = _Serializer[types.Candle]("Candle", labels=[ "MTS", "OPEN", "CLOSE", @@ -115,7 +115,7 @@ Candle = _Serializer[typings.Candle]("Candle", labels=[ "VOLUME" ]) -DerivativesStatus = _Serializer[typings.DerivativesStatus]("DerivativesStatus", labels=[ +DerivativesStatus = _Serializer[types.DerivativesStatus]("DerivativesStatus", labels=[ "KEY", "MTS", "_PLACEHOLDER", @@ -142,7 +142,7 @@ DerivativesStatus = _Serializer[typings.DerivativesStatus]("DerivativesStatus", "CLAMP_MAX" ]) -Liquidation = _Serializer[typings.Liquidation]("Liquidation", labels=[ +Liquidation = _Serializer[types.Liquidation]("Liquidation", labels=[ "_PLACEHOLDER", "POS_ID", "MTS", @@ -157,7 +157,7 @@ Liquidation = _Serializer[typings.Liquidation]("Liquidation", labels=[ "PRICE_ACQUIRED" ]) -Leaderboard = _Serializer[typings.Leaderboard]("Leaderboard", labels=[ +Leaderboard = _Serializer[types.Leaderboard]("Leaderboard", labels=[ "MTS", "_PLACEHOLDER", "USERNAME", @@ -170,7 +170,7 @@ Leaderboard = _Serializer[typings.Leaderboard]("Leaderboard", labels=[ "TWITTER_HANDLE" ]) -FundingStatistic = _Serializer[typings.FundingStatistic]("FundingStatistic", labels=[ +FundingStatistic = _Serializer[types.FundingStatistic]("FundingStatistic", labels=[ "TIMESTAMP", "_PLACEHOLDER", "_PLACEHOLDER", @@ -189,7 +189,7 @@ FundingStatistic = _Serializer[typings.FundingStatistic]("FundingStatistic", lab #region Serializers definition for Rest Authenticated Endpoints -Wallet = _Serializer[typings.Wallet]("Wallet", labels=[ +Wallet = _Serializer[types.Wallet]("Wallet", labels=[ "WALLET_TYPE", "CURRENCY", "BALANCE", @@ -199,7 +199,7 @@ Wallet = _Serializer[typings.Wallet]("Wallet", labels=[ "TRADE_DETAILS" ]) -Order = _Serializer[typings.Order]("Order", labels=[ +Order = _Serializer[types.Order]("Order", labels=[ "ID", "GID", "CID", @@ -234,7 +234,7 @@ Order = _Serializer[typings.Order]("Order", labels=[ "META" ]) -Position = _Serializer[typings.Position]("Position", labels=[ +Position = _Serializer[types.Position]("Position", labels=[ "SYMBOL", "STATUS", "AMOUNT", @@ -257,7 +257,7 @@ Position = _Serializer[typings.Position]("Position", labels=[ "META" ]) -FundingOffer = _Serializer[typings.FundingOffer]("FundingOffer", labels=[ +FundingOffer = _Serializer[types.FundingOffer]("FundingOffer", labels=[ "ID", "SYMBOL", "MTS_CREATED", @@ -281,7 +281,7 @@ FundingOffer = _Serializer[typings.FundingOffer]("FundingOffer", labels=[ "_PLACEHOLDER" ]) -Trade = _Serializer[typings.Trade]("Trade", labels=[ +Trade = _Serializer[types.Trade]("Trade", labels=[ "ID", "PAIR", "MTS_CREATE", @@ -296,7 +296,7 @@ Trade = _Serializer[typings.Trade]("Trade", labels=[ "CID" ]) -OrderTrade = _Serializer[typings.OrderTrade]("OrderTrade", labels=[ +OrderTrade = _Serializer[types.OrderTrade]("OrderTrade", labels=[ "ID", "PAIR", "MTS_CREATE", @@ -311,7 +311,7 @@ OrderTrade = _Serializer[typings.OrderTrade]("OrderTrade", labels=[ "CID" ]) -Ledger = _Serializer[typings.Ledger]("Ledger", labels=[ +Ledger = _Serializer[types.Ledger]("Ledger", labels=[ "ID", "CURRENCY", "_PLACEHOLDER", @@ -323,7 +323,7 @@ Ledger = _Serializer[typings.Ledger]("Ledger", labels=[ "DESCRIPTION" ]) -FundingCredit = _Serializer[typings.FundingCredit]("FundingCredit", labels=[ +FundingCredit = _Serializer[types.FundingCredit]("FundingCredit", labels=[ "ID", "SYMBOL", "SIDE", diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/types.py similarity index 85% rename from bfxapi/rest/typings.py rename to bfxapi/rest/types.py index ef397a9..a27a404 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/types.py @@ -2,7 +2,7 @@ from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any from dataclasses import dataclass -from .. labeler import _Typing +from .. labeler import _Type from .. notification import Notification @@ -11,11 +11,11 @@ JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] #region Type hinting for Rest Public Endpoints @dataclass -class PlatformStatus(_Typing): +class PlatformStatus(_Type): OPERATIVE: int @dataclass -class TradingPairTicker(_Typing): +class TradingPairTicker(_Type): SYMBOL: Optional[str] BID: float BID_SIZE: float @@ -29,7 +29,7 @@ class TradingPairTicker(_Typing): LOW: float @dataclass -class FundingCurrencyTicker(_Typing): +class FundingCurrencyTicker(_Type): SYMBOL: Optional[str] FRR: float BID: float @@ -47,21 +47,21 @@ class FundingCurrencyTicker(_Typing): FRR_AMOUNT_AVAILABLE: float @dataclass -class TickersHistory(_Typing): +class TickersHistory(_Type): SYMBOL: str BID: float ASK: float MTS: int @dataclass -class TradingPairTrade(_Typing): +class TradingPairTrade(_Type): ID: int MTS: int AMOUNT: float PRICE: float @dataclass -class FundingCurrencyTrade(_Typing): +class FundingCurrencyTrade(_Type): ID: int MTS: int AMOUNT: float @@ -69,38 +69,38 @@ class FundingCurrencyTrade(_Typing): PERIOD: int @dataclass -class TradingPairBook(_Typing): +class TradingPairBook(_Type): PRICE: float COUNT: int AMOUNT: float @dataclass -class FundingCurrencyBook(_Typing): +class FundingCurrencyBook(_Type): RATE: float PERIOD: int COUNT: int AMOUNT: float @dataclass -class TradingPairRawBook(_Typing): +class TradingPairRawBook(_Type): ORDER_ID: int PRICE: float AMOUNT: float @dataclass -class FundingCurrencyRawBook(_Typing): +class FundingCurrencyRawBook(_Type): OFFER_ID: int PERIOD: int RATE: float AMOUNT: float @dataclass -class Statistic(_Typing): +class Statistic(_Type): MTS: int VALUE: float @dataclass -class Candle(_Typing): +class Candle(_Type): MTS: int OPEN: float CLOSE: float @@ -109,7 +109,7 @@ class Candle(_Typing): VOLUME: float @dataclass -class DerivativesStatus(_Typing): +class DerivativesStatus(_Type): KEY: Optional[str] MTS: int DERIV_PRICE: float @@ -125,7 +125,7 @@ class DerivativesStatus(_Typing): CLAMP_MAX: float @dataclass -class Liquidation(_Typing): +class Liquidation(_Type): POS_ID: int MTS: int SYMBOL: str @@ -136,7 +136,7 @@ class Liquidation(_Typing): PRICE_ACQUIRED: float @dataclass -class Leaderboard(_Typing): +class Leaderboard(_Type): MTS: int USERNAME: str RANKING: int @@ -144,7 +144,7 @@ class Leaderboard(_Typing): TWITTER_HANDLE: Optional[str] @dataclass -class FundingStatistic(_Typing): +class FundingStatistic(_Type): TIMESTAMP: int FRR: float AVG_PERIOD: float @@ -157,7 +157,7 @@ class FundingStatistic(_Typing): #region Type hinting for Rest Authenticated Endpoints @dataclass -class Wallet(_Typing): +class Wallet(_Type): WALLET_TYPE: str CURRENCY: str BALANCE: float @@ -167,7 +167,7 @@ class Wallet(_Typing): TRADE_DETAILS: JSON @dataclass -class Order(_Typing): +class Order(_Type): ID: int GID: int CID: int @@ -192,7 +192,7 @@ class Order(_Typing): META: JSON @dataclass -class Position(_Typing): +class Position(_Type): SYMBOL: str STATUS: str AMOUNT: float @@ -212,7 +212,7 @@ class Position(_Typing): META: JSON @dataclass -class FundingOffer(_Typing): +class FundingOffer(_Type): ID: int SYMBOL: str MTS_CREATE: int @@ -229,7 +229,7 @@ class FundingOffer(_Typing): RENEW: bool @dataclass -class Trade(_Typing): +class Trade(_Type): ID: int SYMBOL: str MTS_CREATE: int @@ -244,7 +244,7 @@ class Trade(_Typing): CID: int @dataclass -class OrderTrade(_Typing): +class OrderTrade(_Type): ID: int SYMBOL: str MTS_CREATE: int @@ -257,7 +257,7 @@ class OrderTrade(_Typing): CID: int @dataclass -class Ledger(_Typing): +class Ledger(_Type): ID: int CURRENCY: str MTS: int @@ -266,7 +266,7 @@ class Ledger(_Typing): description: str @dataclass -class FundingCredit(_Typing): +class FundingCredit(_Type): ID: int SYMBOL: str SIDE: int From e185da4cc998fae0ab504bb5e445cb0a075983aa Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 16 Jan 2023 17:07:16 +0100 Subject: [PATCH 05/85] Rename bfxapi/websocket/typings.py to types.py. Replace TypedDicts with dataclasses (with _Type as base class). Update demos in examples/websocket to use new implementation. --- bfxapi/websocket/_BfxWebsocketInputs.py | 2 +- bfxapi/websocket/handlers.py | 14 +++-- bfxapi/websocket/serializers.py | 40 ++++++------- bfxapi/websocket/{typings.py => types.py} | 70 +++++++++++++++-------- 4 files changed, 77 insertions(+), 49 deletions(-) rename bfxapi/websocket/{typings.py => types.py} (83%) diff --git a/bfxapi/websocket/_BfxWebsocketInputs.py b/bfxapi/websocket/_BfxWebsocketInputs.py index fda6e19..85e29b0 100644 --- a/bfxapi/websocket/_BfxWebsocketInputs.py +++ b/bfxapi/websocket/_BfxWebsocketInputs.py @@ -2,7 +2,7 @@ from decimal import Decimal from datetime import datetime from typing import Union, Optional, List, Tuple -from .typings import JSON +from .types import JSON from .enums import OrderType, FundingOfferType def _strip(dictionary): diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index 83c1408..33c2654 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -1,3 +1,7 @@ +from typing import List + +from .types import * + from . import serializers from .enums import Channels from .exceptions import BfxWebsocketException @@ -174,13 +178,15 @@ class AuthenticatedChannelsHandler(object): raise BfxWebsocketException(f"Event of type <{type}> not found in self.__handlers.") def __notification(self, stream): + type, serializer = "notification", serializers._Notification(serializer=None) + if stream[1] == "on-req" or stream[1] == "ou-req" or stream[1] == "oc-req": - return self.event_emitter.emit(f"{stream[1]}-notification", serializers._Notification(serializer=serializers.Order).parse(*stream)) + type, serializer = f"{stream[1]}-notification", serializers._Notification[Order](serializer=serializers.Order) if stream[1] == "oc_multi-req": - return self.event_emitter.emit(f"{stream[1]}-notification", serializers._Notification(serializer=serializers.Order, iterate=True).parse(*stream)) + type, serializer = f"{stream[1]}-notification", serializers._Notification[List[Order]](serializer=serializers.Order, iterate=True) if stream[1] == "fon-req" or stream[1] == "foc-req": - return self.event_emitter.emit(f"{stream[1]}-notification", serializers._Notification(serializer=serializers.FundingOffer).parse(*stream)) + type, serializer = f"{stream[1]}-notification", serializers._Notification[FundingOffer](serializer=serializers.FundingOffer) - return self.event_emitter.emit("notification", serializers._Notification(serializer=None).parse(*stream)) \ No newline at end of file + return self.event_emitter.emit(type, serializer.parse(*stream)) \ No newline at end of file diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index a9dd805..6bd56c1 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -1,4 +1,4 @@ -from . import typings +from . import types from .. labeler import _Serializer @@ -6,7 +6,7 @@ from .. notification import _Notification #region Serializers definition for Websocket Public Channels -TradingPairTicker = _Serializer[typings.TradingPairTicker]("TradingPairTicker", labels=[ +TradingPairTicker = _Serializer[types.TradingPairTicker]("TradingPairTicker", labels=[ "BID", "BID_SIZE", "ASK", @@ -19,7 +19,7 @@ TradingPairTicker = _Serializer[typings.TradingPairTicker]("TradingPairTicker", "LOW" ]) -FundingCurrencyTicker = _Serializer[typings.FundingCurrencyTicker]("FundingCurrencyTicker", labels=[ +FundingCurrencyTicker = _Serializer[types.FundingCurrencyTicker]("FundingCurrencyTicker", labels=[ "FRR", "BID", "BID_PERIOD", @@ -38,14 +38,14 @@ FundingCurrencyTicker = _Serializer[typings.FundingCurrencyTicker]("FundingCurre "FRR_AMOUNT_AVAILABLE" ]) -TradingPairTrade = _Serializer[typings.TradingPairTrade]("TradingPairTrade", labels=[ +TradingPairTrade = _Serializer[types.TradingPairTrade]("TradingPairTrade", labels=[ "ID", "MTS", "AMOUNT", "PRICE" ]) -FundingCurrencyTrade = _Serializer[typings.FundingCurrencyTrade]("FundingCurrencyTrade", labels=[ +FundingCurrencyTrade = _Serializer[types.FundingCurrencyTrade]("FundingCurrencyTrade", labels=[ "ID", "MTS", "AMOUNT", @@ -53,33 +53,33 @@ FundingCurrencyTrade = _Serializer[typings.FundingCurrencyTrade]("FundingCurrenc "PERIOD" ]) -TradingPairBook = _Serializer[typings.TradingPairBook]("TradingPairBook", labels=[ +TradingPairBook = _Serializer[types.TradingPairBook]("TradingPairBook", labels=[ "PRICE", "COUNT", "AMOUNT" ]) -FundingCurrencyBook = _Serializer[typings.FundingCurrencyBook]("FundingCurrencyBook", labels=[ +FundingCurrencyBook = _Serializer[types.FundingCurrencyBook]("FundingCurrencyBook", labels=[ "RATE", "PERIOD", "COUNT", "AMOUNT" ]) -TradingPairRawBook = _Serializer[typings.TradingPairRawBook]("TradingPairRawBook", labels=[ +TradingPairRawBook = _Serializer[types.TradingPairRawBook]("TradingPairRawBook", labels=[ "ORDER_ID", "PRICE", "AMOUNT" ]) -FundingCurrencyRawBook = _Serializer[typings.FundingCurrencyRawBook]("FundingCurrencyRawBook", labels=[ +FundingCurrencyRawBook = _Serializer[types.FundingCurrencyRawBook]("FundingCurrencyRawBook", labels=[ "OFFER_ID", "PERIOD", "RATE", "AMOUNT" ]) -Candle = _Serializer[typings.Candle]("Candle", labels=[ +Candle = _Serializer[types.Candle]("Candle", labels=[ "MTS", "OPEN", "CLOSE", @@ -88,7 +88,7 @@ Candle = _Serializer[typings.Candle]("Candle", labels=[ "VOLUME" ]) -DerivativesStatus = _Serializer[typings.DerivativesStatus]("DerivativesStatus", labels=[ +DerivativesStatus = _Serializer[types.DerivativesStatus]("DerivativesStatus", labels=[ "TIME_MS", "_PLACEHOLDER", "DERIV_PRICE", @@ -118,7 +118,7 @@ DerivativesStatus = _Serializer[typings.DerivativesStatus]("DerivativesStatus", #region Serializers definition for Websocket Authenticated Channels -Order = _Serializer[typings.Order]("Order", labels=[ +Order = _Serializer[types.Order]("Order", labels=[ "ID", "GID", "CID", @@ -153,7 +153,7 @@ Order = _Serializer[typings.Order]("Order", labels=[ "META" ]) -Position = _Serializer[typings.Position]("Position", labels=[ +Position = _Serializer[types.Position]("Position", labels=[ "SYMBOL", "STATUS", "AMOUNT", @@ -176,7 +176,7 @@ Position = _Serializer[typings.Position]("Position", labels=[ "META" ]) -TradeExecuted = _Serializer[typings.TradeExecuted]("TradeExecuted", labels=[ +TradeExecuted = _Serializer[types.TradeExecuted]("TradeExecuted", labels=[ "ID", "SYMBOL", "MTS_CREATE", @@ -191,7 +191,7 @@ TradeExecuted = _Serializer[typings.TradeExecuted]("TradeExecuted", labels=[ "CID" ]) -TradeExecutionUpdate = _Serializer[typings.TradeExecutionUpdate]("TradeExecutionUpdate", labels=[ +TradeExecutionUpdate = _Serializer[types.TradeExecutionUpdate]("TradeExecutionUpdate", labels=[ "ID", "SYMBOL", "MTS_CREATE", @@ -206,7 +206,7 @@ TradeExecutionUpdate = _Serializer[typings.TradeExecutionUpdate]("TradeExecution "CID" ]) -FundingOffer = _Serializer[typings.FundingOffer]("FundingOffer", labels=[ +FundingOffer = _Serializer[types.FundingOffer]("FundingOffer", labels=[ "ID", "SYMBOL", "MTS_CREATED", @@ -230,7 +230,7 @@ FundingOffer = _Serializer[typings.FundingOffer]("FundingOffer", labels=[ "_PLACEHOLDER" ]) -FundingCredit = _Serializer[typings.FundingCredit]("FundingCredit", labels=[ +FundingCredit = _Serializer[types.FundingCredit]("FundingCredit", labels=[ "ID", "SYMBOL", "SIDE", @@ -255,7 +255,7 @@ FundingCredit = _Serializer[typings.FundingCredit]("FundingCredit", labels=[ "POSITION_PAIR" ]) -FundingLoan = _Serializer[typings.FundingLoan]("FundingLoan", labels=[ +FundingLoan = _Serializer[types.FundingLoan]("FundingLoan", labels=[ "ID", "SYMBOL", "SIDE", @@ -279,7 +279,7 @@ FundingLoan = _Serializer[typings.FundingLoan]("FundingLoan", labels=[ "NO_CLOSE" ]) -Wallet = _Serializer[typings.Wallet]("Wallet", labels=[ +Wallet = _Serializer[types.Wallet]("Wallet", labels=[ "WALLET_TYPE", "CURRENCY", "BALANCE", @@ -289,7 +289,7 @@ Wallet = _Serializer[typings.Wallet]("Wallet", labels=[ "META" ]) -BalanceInfo = _Serializer[typings.BalanceInfo]("BalanceInfo", labels=[ +BalanceInfo = _Serializer[types.BalanceInfo]("BalanceInfo", labels=[ "AUM", "AUM_NET", ]) diff --git a/bfxapi/websocket/typings.py b/bfxapi/websocket/types.py similarity index 83% rename from bfxapi/websocket/typings.py rename to bfxapi/websocket/types.py index ced6aaa..41838d5 100644 --- a/bfxapi/websocket/typings.py +++ b/bfxapi/websocket/types.py @@ -1,6 +1,10 @@ from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any -from .. notification import Notification +from dataclasses import dataclass + +from ..labeler import _Type + +from ..notification import Notification JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] @@ -48,7 +52,8 @@ class Subscriptions: #region Type hinting for Websocket Public Channels -class TradingPairTicker(TypedDict): +@dataclass +class TradingPairTicker(_Type): BID: float BID_SIZE: float ASK: float @@ -60,7 +65,8 @@ class TradingPairTicker(TypedDict): HIGH: float LOW: float -class FundingCurrencyTicker(TypedDict): +@dataclass +class FundingCurrencyTicker(_Type): FRR: float BID: float BID_PERIOD: int @@ -76,42 +82,49 @@ class FundingCurrencyTicker(TypedDict): LOW: float FRR_AMOUNT_AVAILABLE: float -class TradingPairTrade(TypedDict): +@dataclass +class TradingPairTrade(_Type): ID: int MTS: int AMOUNT: float PRICE: float -class FundingCurrencyTrade(TypedDict): +@dataclass +class FundingCurrencyTrade(_Type): ID: int MTS: int AMOUNT: float RATE: float PERIOD: int -class TradingPairBook(TypedDict): +@dataclass +class TradingPairBook(_Type): PRICE: float COUNT: int AMOUNT: float - -class FundingCurrencyBook(TypedDict): + +@dataclass +class FundingCurrencyBook(_Type): RATE: float PERIOD: int COUNT: int AMOUNT: float - -class TradingPairRawBook(TypedDict): + +@dataclass +class TradingPairRawBook(_Type): ORDER_ID: int PRICE: float AMOUNT: float - -class FundingCurrencyRawBook(TypedDict): + +@dataclass +class FundingCurrencyRawBook(_Type): OFFER_ID: int PERIOD: int RATE: float AMOUNT: float -class Candle(TypedDict): +@dataclass +class Candle(_Type): MTS: int OPEN: float CLOSE: float @@ -119,7 +132,8 @@ class Candle(TypedDict): LOW: float VOLUME: float -class DerivativesStatus(TypedDict): +@dataclass +class DerivativesStatus(_Type): TIME_MS: int DERIV_PRICE: float SPOT_PRICE: float @@ -136,8 +150,8 @@ class DerivativesStatus(TypedDict): #endregion #region Type hinting for Websocket Authenticated Channels - -class Order(TypedDict): +@dataclass +class Order(_Type): ID: int GID: int CID: int @@ -161,7 +175,8 @@ class Order(TypedDict): ROUTING: str META: JSON -class Position(TypedDict): +@dataclass +class Position(_Type): SYMBOL: str STATUS: str AMOUNT: float @@ -180,7 +195,8 @@ class Position(TypedDict): COLLATERAL_MIN: float META: JSON -class TradeExecuted(TypedDict): +@dataclass +class TradeExecuted(_Type): ID: int SYMBOL: str MTS_CREATE: int @@ -192,7 +208,8 @@ class TradeExecuted(TypedDict): MAKER:int CID: int -class TradeExecutionUpdate(TypedDict): +@dataclass +class TradeExecutionUpdate(_Type): ID: int SYMBOL: str MTS_CREATE: int @@ -206,7 +223,8 @@ class TradeExecutionUpdate(TypedDict): FEE_CURRENCY: str CID: int -class FundingOffer(TypedDict): +@dataclass +class FundingOffer(_Type): ID: int SYMBOL: str MTS_CREATED: int @@ -222,7 +240,8 @@ class FundingOffer(TypedDict): HIDDEN: int RENEW: int -class FundingCredit(TypedDict): +@dataclass +class FundingCredit(_Type): ID: int SYMBOL: str SIDE: int @@ -242,7 +261,8 @@ class FundingCredit(TypedDict): NO_CLOSE: int POSITION_PAIR: str -class FundingLoan(TypedDict): +@dataclass +class FundingLoan(_Type): ID: int SYMBOL: str SIDE: int @@ -261,7 +281,8 @@ class FundingLoan(TypedDict): RATE_REAL: float NO_CLOSE: int -class Wallet(TypedDict): +@dataclass +class Wallet(_Type): WALLET_TYPE: str CURRENCY: str BALANCE: float @@ -270,7 +291,8 @@ class Wallet(TypedDict): DESCRIPTION: str META: JSON -class BalanceInfo(TypedDict): +@dataclass +class BalanceInfo(_Type): AUM: float AUM_NET: float From 2afcc766476d3e5d0af623fa0e1cb33de7c616c5 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 16 Jan 2023 18:18:12 +0100 Subject: [PATCH 06/85] Fix bug regarding new typing with dataclasses. --- bfxapi/labeler.py | 13 +++++---- bfxapi/notification.py | 8 +++--- bfxapi/rest/serializers.py | 50 ++++++++++++++++----------------- bfxapi/websocket/serializers.py | 40 +++++++++++++------------- 4 files changed, 57 insertions(+), 54 deletions(-) diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 9714b5c..38b527b 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -1,17 +1,17 @@ from .exceptions import LabelerSerializerException -from typing import Generic, TypeVar, Iterable, Optional, List, Tuple, Any, cast +from typing import Type, Generic, TypeVar, Iterable, Optional, List, Tuple, Any, cast T = TypeVar("T") class _Type(object): def __init__(self, **kwargs): for key, value in kwargs.items(): - self.__setattr__(key,value) + self.__setattr__(key, value) class _Serializer(Generic[T]): - def __init__(self, name: str, labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]): - self.name, self.__labels, self.__IGNORE = name, labels, IGNORE + def __init__(self, name: str, klass: Type[_Type], labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]): + self.name, self.klass, self.__labels, self.__IGNORE = name, klass, labels, IGNORE def _serialize(self, *args: Any, skip: Optional[List[str]] = None) -> Iterable[Tuple[str, Any]]: labels = list(filter(lambda label: label not in (skip or list()), self.__labels)) @@ -24,4 +24,7 @@ class _Serializer(Generic[T]): yield label, args[index] def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: - return cast(T, _Type(**dict(self._serialize(*values, skip=skip)))) \ No newline at end of file + return cast(T, self.klass(**dict(self._serialize(*values, skip=skip)))) + +def generate_labeler_serializer(name: str, klass: T, labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> _Serializer[T]: + return _Serializer[T](name, klass, labels, IGNORE) \ No newline at end of file diff --git a/bfxapi/notification.py b/bfxapi/notification.py index e66ba60..b2f90b8 100644 --- a/bfxapi/notification.py +++ b/bfxapi/notification.py @@ -20,12 +20,12 @@ class _Notification(_Serializer, Generic[T]): __LABELS = [ "MTS", "TYPE", "MESSAGE_ID", "_PLACEHOLDER", "NOTIFY_INFO", "CODE", "STATUS", "TEXT" ] def __init__(self, serializer: Optional[_Serializer] = None, iterate: bool = False): - super().__init__("Notification", _Notification.__LABELS, IGNORE = [ "_PLACEHOLDER" ]) + super().__init__("Notification", Notification, _Notification.__LABELS, IGNORE = [ "_PLACEHOLDER" ]) self.serializer, self.iterate = serializer, iterate def parse(self, *values: Any, skip: Optional[List[str]] = None) -> Notification[T]: - notification = cast(Notification[T], _Type(**dict(self._serialize(*values)))) + notification = cast(Notification[T], Notification(**dict(self._serialize(*values)))) if isinstance(self.serializer, _Serializer): NOTIFY_INFO = cast(List[Any], notification.NOTIFY_INFO) @@ -34,7 +34,7 @@ class _Notification(_Serializer, Generic[T]): if len(NOTIFY_INFO) == 1 and isinstance(NOTIFY_INFO[0], list): NOTIFY_INFO = NOTIFY_INFO[0] - notification.NOTIFY_INFO = cast(T, _Type(**dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip)))) - else: notification.NOTIFY_INFO = cast(T, [ _Type(**dict(self.serializer._serialize(*data, skip=skip))) for data in NOTIFY_INFO ]) + notification.NOTIFY_INFO = cast(T, self.serializer.klass(**dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip)))) + else: notification.NOTIFY_INFO = cast(T, [ self.serializer.klass(**dict(self.serializer._serialize(*data, skip=skip))) for data in NOTIFY_INFO ]) return notification \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 4196b52..35df8ae 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -1,16 +1,16 @@ from . import types -from .. labeler import _Serializer +from .. labeler import generate_labeler_serializer from .. notification import _Notification #region Serializers definition for Rest Public Endpoints -PlatformStatus = _Serializer[types.PlatformStatus]("PlatformStatus", labels=[ +PlatformStatus = generate_labeler_serializer("PlatformStatus", klass=types.PlatformStatus, labels=[ "OPERATIVE" ]) -TradingPairTicker = _Serializer[types.TradingPairTicker]("TradingPairTicker", labels=[ +TradingPairTicker = generate_labeler_serializer("TradingPairTicker", klass=types.TradingPairTicker, labels=[ "SYMBOL", "BID", "BID_SIZE", @@ -24,7 +24,7 @@ TradingPairTicker = _Serializer[types.TradingPairTicker]("TradingPairTicker", la "LOW" ]) -FundingCurrencyTicker = _Serializer[types.FundingCurrencyTicker]("FundingCurrencyTicker", labels=[ +FundingCurrencyTicker = generate_labeler_serializer("FundingCurrencyTicker", klass=types.FundingCurrencyTicker, labels=[ "SYMBOL", "FRR", "BID", @@ -44,7 +44,7 @@ FundingCurrencyTicker = _Serializer[types.FundingCurrencyTicker]("FundingCurrenc "FRR_AMOUNT_AVAILABLE" ]) -TickersHistory = _Serializer[types.TickersHistory]("TickersHistory", labels=[ +TickersHistory = generate_labeler_serializer("TickersHistory", klass=types.TickersHistory, labels=[ "SYMBOL", "BID", "_PLACEHOLDER", @@ -60,14 +60,14 @@ TickersHistory = _Serializer[types.TickersHistory]("TickersHistory", labels=[ "MTS" ]) -TradingPairTrade = _Serializer[types.TradingPairTrade]("TradingPairTrade", labels=[ +TradingPairTrade = generate_labeler_serializer("TradingPairTrade", klass=types.TradingPairTrade, labels=[ "ID", "MTS", "AMOUNT", "PRICE" ]) -FundingCurrencyTrade = _Serializer[types.FundingCurrencyTrade]("FundingCurrencyTrade", labels=[ +FundingCurrencyTrade = generate_labeler_serializer("FundingCurrencyTrade", klass=types.FundingCurrencyTrade, labels=[ "ID", "MTS", "AMOUNT", @@ -75,38 +75,38 @@ FundingCurrencyTrade = _Serializer[types.FundingCurrencyTrade]("FundingCurrencyT "PERIOD" ]) -TradingPairBook = _Serializer[types.TradingPairBook]("TradingPairBook", labels=[ +TradingPairBook = generate_labeler_serializer("TradingPairBook", klass=types.TradingPairBook, labels=[ "PRICE", "COUNT", "AMOUNT" ]) -FundingCurrencyBook = _Serializer[types.FundingCurrencyBook]("FundingCurrencyBook", labels=[ +FundingCurrencyBook = generate_labeler_serializer("FundingCurrencyBook", klass=types.FundingCurrencyBook, labels=[ "RATE", "PERIOD", "COUNT", "AMOUNT" ]) -TradingPairRawBook = _Serializer[types.TradingPairRawBook]("TradingPairRawBook", labels=[ +TradingPairRawBook = generate_labeler_serializer("TradingPairRawBook", klass=types.TradingPairRawBook, labels=[ "ORDER_ID", "PRICE", "AMOUNT" ]) -FundingCurrencyRawBook = _Serializer[types.FundingCurrencyRawBook]("FundingCurrencyRawBook", labels=[ +FundingCurrencyRawBook = generate_labeler_serializer("FundingCurrencyRawBook", klass=types.FundingCurrencyRawBook, labels=[ "OFFER_ID", "PERIOD", "RATE", "AMOUNT" ]) -Statistic = _Serializer[types.Statistic]("Statistic", labels=[ +Statistic = generate_labeler_serializer("Statistic", klass=types.Statistic, labels=[ "MTS", "VALUE" ]) -Candle = _Serializer[types.Candle]("Candle", labels=[ +Candle = generate_labeler_serializer("Candle", klass=types.Candle, labels=[ "MTS", "OPEN", "CLOSE", @@ -115,7 +115,7 @@ Candle = _Serializer[types.Candle]("Candle", labels=[ "VOLUME" ]) -DerivativesStatus = _Serializer[types.DerivativesStatus]("DerivativesStatus", labels=[ +DerivativesStatus = generate_labeler_serializer("DerivativesStatus", klass=types.DerivativesStatus, labels=[ "KEY", "MTS", "_PLACEHOLDER", @@ -142,7 +142,7 @@ DerivativesStatus = _Serializer[types.DerivativesStatus]("DerivativesStatus", la "CLAMP_MAX" ]) -Liquidation = _Serializer[types.Liquidation]("Liquidation", labels=[ +Liquidation = generate_labeler_serializer("Liquidation", klass=types.Liquidation, labels=[ "_PLACEHOLDER", "POS_ID", "MTS", @@ -157,7 +157,7 @@ Liquidation = _Serializer[types.Liquidation]("Liquidation", labels=[ "PRICE_ACQUIRED" ]) -Leaderboard = _Serializer[types.Leaderboard]("Leaderboard", labels=[ +Leaderboard = generate_labeler_serializer("Leaderboard", klass=types.Leaderboard, labels=[ "MTS", "_PLACEHOLDER", "USERNAME", @@ -170,7 +170,7 @@ Leaderboard = _Serializer[types.Leaderboard]("Leaderboard", labels=[ "TWITTER_HANDLE" ]) -FundingStatistic = _Serializer[types.FundingStatistic]("FundingStatistic", labels=[ +FundingStatistic = generate_labeler_serializer("FundingStatistic", klass=types.FundingStatistic, labels=[ "TIMESTAMP", "_PLACEHOLDER", "_PLACEHOLDER", @@ -189,7 +189,7 @@ FundingStatistic = _Serializer[types.FundingStatistic]("FundingStatistic", label #region Serializers definition for Rest Authenticated Endpoints -Wallet = _Serializer[types.Wallet]("Wallet", labels=[ +Wallet = generate_labeler_serializer("Wallet", klass=types.Wallet, labels=[ "WALLET_TYPE", "CURRENCY", "BALANCE", @@ -199,7 +199,7 @@ Wallet = _Serializer[types.Wallet]("Wallet", labels=[ "TRADE_DETAILS" ]) -Order = _Serializer[types.Order]("Order", labels=[ +Order = generate_labeler_serializer("Order", klass=types.Order, labels=[ "ID", "GID", "CID", @@ -234,7 +234,7 @@ Order = _Serializer[types.Order]("Order", labels=[ "META" ]) -Position = _Serializer[types.Position]("Position", labels=[ +Position = generate_labeler_serializer("Position", klass=types.Position, labels=[ "SYMBOL", "STATUS", "AMOUNT", @@ -257,7 +257,7 @@ Position = _Serializer[types.Position]("Position", labels=[ "META" ]) -FundingOffer = _Serializer[types.FundingOffer]("FundingOffer", labels=[ +FundingOffer = generate_labeler_serializer("FundingOffer", klass=types.FundingOffer, labels=[ "ID", "SYMBOL", "MTS_CREATED", @@ -281,7 +281,7 @@ FundingOffer = _Serializer[types.FundingOffer]("FundingOffer", labels=[ "_PLACEHOLDER" ]) -Trade = _Serializer[types.Trade]("Trade", labels=[ +Trade = generate_labeler_serializer("Trade", klass=types.Trade, labels=[ "ID", "PAIR", "MTS_CREATE", @@ -296,7 +296,7 @@ Trade = _Serializer[types.Trade]("Trade", labels=[ "CID" ]) -OrderTrade = _Serializer[types.OrderTrade]("OrderTrade", labels=[ +OrderTrade = generate_labeler_serializer("OrderTrade", klass=types.OrderTrade, labels=[ "ID", "PAIR", "MTS_CREATE", @@ -311,7 +311,7 @@ OrderTrade = _Serializer[types.OrderTrade]("OrderTrade", labels=[ "CID" ]) -Ledger = _Serializer[types.Ledger]("Ledger", labels=[ +Ledger = generate_labeler_serializer("Ledger", klass=types.Ledger, labels=[ "ID", "CURRENCY", "_PLACEHOLDER", @@ -323,7 +323,7 @@ Ledger = _Serializer[types.Ledger]("Ledger", labels=[ "DESCRIPTION" ]) -FundingCredit = _Serializer[types.FundingCredit]("FundingCredit", labels=[ +FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.FundingCredit, labels=[ "ID", "SYMBOL", "SIDE", diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index 6bd56c1..1e14dbc 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -1,12 +1,12 @@ from . import types -from .. labeler import _Serializer +from .. labeler import generate_labeler_serializer from .. notification import _Notification #region Serializers definition for Websocket Public Channels -TradingPairTicker = _Serializer[types.TradingPairTicker]("TradingPairTicker", labels=[ +TradingPairTicker = generate_labeler_serializer("TradingPairTicker", klass=types.TradingPairTicker, labels=[ "BID", "BID_SIZE", "ASK", @@ -19,7 +19,7 @@ TradingPairTicker = _Serializer[types.TradingPairTicker]("TradingPairTicker", la "LOW" ]) -FundingCurrencyTicker = _Serializer[types.FundingCurrencyTicker]("FundingCurrencyTicker", labels=[ +FundingCurrencyTicker = generate_labeler_serializer("FundingCurrencyTicker", klass=types.FundingCurrencyTicker, labels=[ "FRR", "BID", "BID_PERIOD", @@ -38,14 +38,14 @@ FundingCurrencyTicker = _Serializer[types.FundingCurrencyTicker]("FundingCurrenc "FRR_AMOUNT_AVAILABLE" ]) -TradingPairTrade = _Serializer[types.TradingPairTrade]("TradingPairTrade", labels=[ +TradingPairTrade = generate_labeler_serializer("TradingPairTrade", klass=types.TradingPairTrade, labels=[ "ID", "MTS", "AMOUNT", "PRICE" ]) -FundingCurrencyTrade = _Serializer[types.FundingCurrencyTrade]("FundingCurrencyTrade", labels=[ +FundingCurrencyTrade = generate_labeler_serializer("FundingCurrencyTrade", klass=types.FundingCurrencyTrade, labels=[ "ID", "MTS", "AMOUNT", @@ -53,33 +53,33 @@ FundingCurrencyTrade = _Serializer[types.FundingCurrencyTrade]("FundingCurrencyT "PERIOD" ]) -TradingPairBook = _Serializer[types.TradingPairBook]("TradingPairBook", labels=[ +TradingPairBook = generate_labeler_serializer("TradingPairBook", klass=types.TradingPairBook, labels=[ "PRICE", "COUNT", "AMOUNT" ]) -FundingCurrencyBook = _Serializer[types.FundingCurrencyBook]("FundingCurrencyBook", labels=[ +FundingCurrencyBook = generate_labeler_serializer("FundingCurrencyBook", klass=types.FundingCurrencyBook, labels=[ "RATE", "PERIOD", "COUNT", "AMOUNT" ]) -TradingPairRawBook = _Serializer[types.TradingPairRawBook]("TradingPairRawBook", labels=[ +TradingPairRawBook = generate_labeler_serializer("TradingPairRawBook", klass=types.TradingPairRawBook, labels=[ "ORDER_ID", "PRICE", "AMOUNT" ]) -FundingCurrencyRawBook = _Serializer[types.FundingCurrencyRawBook]("FundingCurrencyRawBook", labels=[ +FundingCurrencyRawBook = generate_labeler_serializer("FundingCurrencyRawBook", klass=types.FundingCurrencyRawBook, labels=[ "OFFER_ID", "PERIOD", "RATE", "AMOUNT" ]) -Candle = _Serializer[types.Candle]("Candle", labels=[ +Candle = generate_labeler_serializer("Candle", klass=types.Candle, labels=[ "MTS", "OPEN", "CLOSE", @@ -88,7 +88,7 @@ Candle = _Serializer[types.Candle]("Candle", labels=[ "VOLUME" ]) -DerivativesStatus = _Serializer[types.DerivativesStatus]("DerivativesStatus", labels=[ +DerivativesStatus = generate_labeler_serializer("DerivativesStatus", klass=types.DerivativesStatus, labels=[ "TIME_MS", "_PLACEHOLDER", "DERIV_PRICE", @@ -118,7 +118,7 @@ DerivativesStatus = _Serializer[types.DerivativesStatus]("DerivativesStatus", la #region Serializers definition for Websocket Authenticated Channels -Order = _Serializer[types.Order]("Order", labels=[ +Order = generate_labeler_serializer("Order", klass=types.Order, labels=[ "ID", "GID", "CID", @@ -153,7 +153,7 @@ Order = _Serializer[types.Order]("Order", labels=[ "META" ]) -Position = _Serializer[types.Position]("Position", labels=[ +Position = generate_labeler_serializer("Position", klass=types.Position, labels=[ "SYMBOL", "STATUS", "AMOUNT", @@ -176,7 +176,7 @@ Position = _Serializer[types.Position]("Position", labels=[ "META" ]) -TradeExecuted = _Serializer[types.TradeExecuted]("TradeExecuted", labels=[ +TradeExecuted = generate_labeler_serializer("TradeExecuted", klass=types.TradeExecuted, labels=[ "ID", "SYMBOL", "MTS_CREATE", @@ -191,7 +191,7 @@ TradeExecuted = _Serializer[types.TradeExecuted]("TradeExecuted", labels=[ "CID" ]) -TradeExecutionUpdate = _Serializer[types.TradeExecutionUpdate]("TradeExecutionUpdate", labels=[ +TradeExecutionUpdate = generate_labeler_serializer("TradeExecutionUpdate", klass=types.TradeExecutionUpdate, labels=[ "ID", "SYMBOL", "MTS_CREATE", @@ -206,7 +206,7 @@ TradeExecutionUpdate = _Serializer[types.TradeExecutionUpdate]("TradeExecutionUp "CID" ]) -FundingOffer = _Serializer[types.FundingOffer]("FundingOffer", labels=[ +FundingOffer = generate_labeler_serializer("FundingOffer", klass=types.FundingOffer, labels=[ "ID", "SYMBOL", "MTS_CREATED", @@ -230,7 +230,7 @@ FundingOffer = _Serializer[types.FundingOffer]("FundingOffer", labels=[ "_PLACEHOLDER" ]) -FundingCredit = _Serializer[types.FundingCredit]("FundingCredit", labels=[ +FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.FundingCredit, labels=[ "ID", "SYMBOL", "SIDE", @@ -255,7 +255,7 @@ FundingCredit = _Serializer[types.FundingCredit]("FundingCredit", labels=[ "POSITION_PAIR" ]) -FundingLoan = _Serializer[types.FundingLoan]("FundingLoan", labels=[ +FundingLoan = generate_labeler_serializer("FundingLoan", klass=types.FundingLoan, labels=[ "ID", "SYMBOL", "SIDE", @@ -279,7 +279,7 @@ FundingLoan = _Serializer[types.FundingLoan]("FundingLoan", labels=[ "NO_CLOSE" ]) -Wallet = _Serializer[types.Wallet]("Wallet", labels=[ +Wallet = generate_labeler_serializer("Wallet", klass=types.Wallet, labels=[ "WALLET_TYPE", "CURRENCY", "BALANCE", @@ -289,7 +289,7 @@ Wallet = _Serializer[types.Wallet]("Wallet", labels=[ "META" ]) -BalanceInfo = _Serializer[types.BalanceInfo]("BalanceInfo", labels=[ +BalanceInfo = generate_labeler_serializer("BalanceInfo", klass=types.BalanceInfo, labels=[ "AUM", "AUM_NET", ]) From 6f8e37cad0d70edce37441bd6f1cfd3137d107ac Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 16 Jan 2023 18:27:03 +0100 Subject: [PATCH 07/85] Fix all mypy errors and warnings due to new dataclass implementation. --- bfxapi/labeler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 38b527b..87889e2 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -2,7 +2,7 @@ from .exceptions import LabelerSerializerException from typing import Type, Generic, TypeVar, Iterable, Optional, List, Tuple, Any, cast -T = TypeVar("T") +T = TypeVar("T", bound="_Type") class _Type(object): def __init__(self, **kwargs): @@ -26,5 +26,5 @@ class _Serializer(Generic[T]): def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: return cast(T, self.klass(**dict(self._serialize(*values, skip=skip)))) -def generate_labeler_serializer(name: str, klass: T, labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> _Serializer[T]: +def generate_labeler_serializer(name: str, klass: Type[T], labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> _Serializer[T]: return _Serializer[T](name, klass, labels, IGNORE) \ No newline at end of file From 89c921335ca455a608e8f3397d172d2833650f90 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Tue, 17 Jan 2023 12:13:06 +0100 Subject: [PATCH 08/85] Use namespace --- examples/rest/create_order.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/rest/create_order.py b/examples/rest/create_order.py index 34408aa..71a99ca 100644 --- a/examples/rest/create_order.py +++ b/examples/rest/create_order.py @@ -23,7 +23,7 @@ print("Submit Order Notification:", submitted_order) # Update it updated_order = bfx.rest.auth.update_order( - id=submitted_order["NOTIFY_INFO"]["ID"], + id=submitted_order.NOTIFY_INFO.ID, amount="0.020", price="10100" ) @@ -31,6 +31,6 @@ updated_order = bfx.rest.auth.update_order( print("Update Order Notification:", updated_order) # Delete it -canceled_order = bfx.rest.auth.cancel_order(id=submitted_order["NOTIFY_INFO"]["ID"]) +canceled_order = bfx.rest.auth.cancel_order(id=submitted_order.NOTIFY_INFO.ID) print("Cancel Order Notification:", canceled_order) From 061ca22752ddab371cca3fa97f39a07f14a6b5b2 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Tue, 17 Jan 2023 12:40:09 +0100 Subject: [PATCH 09/85] add seed candles --- bfxapi/rest/BfxRestInterface.py | 10 ++++++++++ examples/rest/get_seed_candles.py | 15 +++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 examples/rest/get_seed_candles.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index c49ca56..deb5bdc 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -45,6 +45,8 @@ class _Requests(object): def _GET(self, endpoint, params = None): response = requests.get(f"{self.host}/{endpoint}", params=params) + + print(f"{self.host}/{endpoint}") if response.status_code == HTTPStatus.NOT_FOUND: raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") @@ -215,6 +217,14 @@ class _RestPublicEndpoints(_Requests): return [ serializers.Liquidation.parse(*subdata[0]) for subdata in data ] + def get_seed_candles(self, symbol: str, tf: str = '1m', sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Candle]: + + params = {"sort": sort, "start": start, "end": end, "limit": limit} + + data = self._GET(f"candles/trade:{tf}:{symbol}/hist?limit={limit}&start={start}&end={end}&sort={sort}", params=params) + + return [ serializers.Candle.parse(*subdata) for subdata in data ] + def get_leaderboards_hist( self, resource: str, diff --git a/examples/rest/get_seed_candles.py b/examples/rest/get_seed_candles.py new file mode 100644 index 0000000..ef02550 --- /dev/null +++ b/examples/rest/get_seed_candles.py @@ -0,0 +1,15 @@ +# python -c "from examples.rest.get_seed_candles import *" + +import time + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST +) + +print(f"Candles: {bfx.rest.public.get_seed_candles(symbol='tBTCUSD')}") + +# Be sure to specify a period or aggregated period when retrieving funding candles. +# If you wish to mimic the candles found in the UI, use the following setup to aggregate all funding candles: a30:p2:p30 +print(f"Candles: {bfx.rest.public.get_seed_candles(symbol='fUSD:a30:p2:p30', tf='15m')}") \ No newline at end of file From 7ea8b5ae1c54339a24ea81bf80652ad8dabcd1b0 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Tue, 17 Jan 2023 12:40:09 +0100 Subject: [PATCH 10/85] add seed candles --- bfxapi/rest/BfxRestInterface.py | 8 ++++---- examples/rest/get_candles_hist.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 examples/rest/get_candles_hist.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index deb5bdc..c16c555 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -176,20 +176,20 @@ class _RestPublicEndpoints(_Requests): def get_candles_hist( self, - resource: str, + symbol: str, tf: str = "1m", sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> List[Candle]: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"candles/{resource}/hist", params=params) + data = self._GET(f"candles/trade:{tf}:{symbol}/hist", params=params) return [ serializers.Candle.parse(*subdata) for subdata in data ] def get_candles_last( self, - resource: str, + symbol: str, tf: str = "1m", sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> Candle: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"candles/{resource}/last", params=params) + data = self._GET(f"candles/trade:{tf}:{symbol}/last", params=params) return serializers.Candle.parse(*data) def get_derivatives_status(self, symbols: Union[List[str], Literal["ALL"]]) -> List[DerivativesStatus]: diff --git a/examples/rest/get_candles_hist.py b/examples/rest/get_candles_hist.py new file mode 100644 index 0000000..fde6212 --- /dev/null +++ b/examples/rest/get_candles_hist.py @@ -0,0 +1,13 @@ +# python -c "from examples.rest.get_candles_hist import *" + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST +) + +print(f"Candles: {bfx.rest.public.get_candles_hist(symbol='tBTCUSD')}") + +# Be sure to specify a period or aggregated period when retrieving funding candles. +# If you wish to mimic the candles found in the UI, use the following setup to aggregate all funding candles: a30:p2:p30 +print(f"Candles: {bfx.rest.public.get_candles_hist(tf='15m', symbol='fUSD:a30:p2:p30')}") \ No newline at end of file From 430f7be51d383cb17825520d46dfa6d8d0d8f474 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Tue, 17 Jan 2023 13:14:55 +0100 Subject: [PATCH 11/85] add wallet deposit/transfer --- bfxapi/rest/BfxRestInterface.py | 54 +++++++++++++++++++++++-- bfxapi/rest/serializers.py | 65 +++++++++++++++++++++++++++++++ bfxapi/rest/types.py | 45 +++++++++++++++++++++ examples/rest/get_seed_candles.py | 15 ------- examples/rest/transfer_wallet.py | 46 ++++++++++++++++++++++ 5 files changed, 207 insertions(+), 18 deletions(-) delete mode 100644 examples/rest/get_seed_candles.py create mode 100644 examples/rest/transfer_wallet.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index c16c555..741a3ec 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -192,8 +192,10 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"candles/trade:{tf}:{symbol}/last", params=params) return serializers.Candle.parse(*data) - def get_derivatives_status(self, symbols: Union[List[str], Literal["ALL"]]) -> List[DerivativesStatus]: - params = { "keys": ",".join(symbols) if type(symbols) == List else "ALL" } + def get_derivatives_status(self, keys: Union[List[str], Literal["ALL"]]) -> List[DerivativesStatus]: + if keys == "ALL": + params = { "keys": "ALL" } + else: params = { "keys": ",".join(keys) } data = self._GET(f"status/deriv", params=params) @@ -405,4 +407,50 @@ class _RestAuthenticatedEndpoints(_Requests): "limit": limit } - return [ serializers.FundingCredit.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] \ No newline at end of file + return [ serializers.FundingCredit.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] + + def submit_wallet_transfer(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, str]) -> Notification[Transfer]: + data = { + "from": from_wallet, "to": to_wallet, + "currency": currency, "currency_to": currency_to, + "amount": amount + } + + return serializers._Notification[Transfer](serializer=serializers.Transfer).parse(*self._POST("auth/w/transfer", data=data)) + + def submit_wallet_withdraw(self, wallet: str, method: str, address: str, amount: Union[Decimal, str]) -> Notification[Withdrawal]: + data = { + "wallet": wallet, "method": method, + "address": address, "amount": amount, + } + + return serializers._Notification[Withdrawal](serializer=serializers.Withdrawal).parse(*self._POST("auth/w/withdraw", data=data)) + + def get_deposit_address(self, wallet: str, method: str, renew: bool = False) -> Notification[DepositAddress]: + data = { + "wallet": wallet, + "method": method, + "renew": int(renew) + } + + return serializers._Notification[DepositAddress](serializer=serializers.DepositAddress).parse(*self._POST("auth/w/deposit/address", data=data)) + + def get_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, str]) -> Invoice: + data = { + "wallet": wallet, "currency": currency, + "amount": amount + } + + return serializers.Invoice.parse(*self._POST("auth/w/deposit/invoice", data=data)) + + def get_movements(self, currency: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Movement]: + if currency == None: + endpoint = "auth/r/movements/hist" + else: endpoint = f"auth/r/movements/{currency}/hist" + + data = { + "start": start, "end": end, + "limit": limit + } + + return [ serializers.Movement.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 35df8ae..b6af19f 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -348,4 +348,69 @@ FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.Funding "POSITION_PAIR" ]) +Transfer = generate_labeler_serializer("Transfer", klass=types.Transfer, labels=[ + "MTS", + "WALLET_FROM", + "WALLET_TO", + "_PLACEHOLDER", + "CURRENCY", + "CURRENCY_TO", + "_PLACEHOLDER", + "AMOUNT" +]) + +Withdrawal = generate_labeler_serializer("Withdrawal", klass=types.Withdrawal, labels=[ + "WITHDRAWAL_ID", + "_PLACEHOLDER", + "METHOD", + "PAYMENT_ID", + "WALLET", + "AMOUNT", + "_PLACEHOLDER", + "_PLACEHOLDER", + "WITHDRAWAL_FEE" +]) + +DepositAddress = generate_labeler_serializer("DepositAddress", klass=types.DepositAddress, labels=[ + "_PLACEHOLDER", + "METHOD", + "CURRENCY_CODE", + "_PLACEHOLDER", + "ADDRESS", + "POOL_ADDRESS" +]) + +Invoice = generate_labeler_serializer("Invoice", klass=types.Invoice, labels=[ + "INVOICE_HASH", + "INVOICE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "AMOUNT" +]) + +Movement = generate_labeler_serializer("Movement", klass=types.Movement, labels=[ + "ID", + "CURRENCY", + "CURRENCY_NAME", + "_PLACEHOLDER", + "_PLACEHOLDER", + "MTS_STARTED", + "MTS_UPDATED", + "_PLACEHOLDER", + "_PLACEHOLDER", + "STATUS", + "_PLACEHOLDER", + "_PLACEHOLDER", + "AMOUNT", + "FEES", + "_PLACEHOLDER", + "_PLACEHOLDER", + "DESTINATION_ADDRESS", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "TRANSACTION_ID", + "WITHDRAW_TRANSACTION_NOTE" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index a27a404..1dbc533 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -286,4 +286,49 @@ class FundingCredit(_Type): NO_CLOSE: int POSITION_PAIR: str +@dataclass +class Transfer(_Type): + MTS: int + WALLET_FROM: str + WALLET_TO: str + CURRENCY: str + CURRENCY_TO: str + AMOUNT: int + +@dataclass +class Withdrawal(_Type): + WITHDRAWAL_ID: int + METHOD: str + PAYMENT_ID: str + WALLET: str + AMOUNT: float + WITHDRAWAL_FEE: float + +@dataclass +class DepositAddress(_Type): + METHOD: str + CURRENCY_CODE: str + ADDRESS: str + POOL_ADDRESS: str + +@dataclass +class Invoice(_Type): + INVOICE_HASH: str + INVOICE: str + AMOUNT: str + +@dataclass +class Movement(_Type): + ID: str + CURRENCY: str + CURRENCY_NAME: str + MTS_STARTED: int + MTS_UPDATED: int + STATUS: str + AMOUNT: int + FEES: int + DESTINATION_ADDRESS: str + TRANSACTION_ID: str + WITHDRAW_TRANSACTION_NOTE: str + #endregion \ No newline at end of file diff --git a/examples/rest/get_seed_candles.py b/examples/rest/get_seed_candles.py deleted file mode 100644 index ef02550..0000000 --- a/examples/rest/get_seed_candles.py +++ /dev/null @@ -1,15 +0,0 @@ -# python -c "from examples.rest.get_seed_candles import *" - -import time - -from bfxapi.client import Client, Constants - -bfx = Client( - REST_HOST=Constants.REST_HOST -) - -print(f"Candles: {bfx.rest.public.get_seed_candles(symbol='tBTCUSD')}") - -# Be sure to specify a period or aggregated period when retrieving funding candles. -# If you wish to mimic the candles found in the UI, use the following setup to aggregate all funding candles: a30:p2:p30 -print(f"Candles: {bfx.rest.public.get_seed_candles(symbol='fUSD:a30:p2:p30', tf='15m')}") \ No newline at end of file diff --git a/examples/rest/transfer_wallet.py b/examples/rest/transfer_wallet.py new file mode 100644 index 0000000..28dcccb --- /dev/null +++ b/examples/rest/transfer_wallet.py @@ -0,0 +1,46 @@ +# python -c "from examples.rest.transfer_wallet import *" + +import os + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +def transfer_wallet(): + response = bfx.rest.auth.submit_wallet_transfer(from_wallet="exchange", to_wallet="funding", from_currency="ETH", to_currency="ETH", amount=0.001) + print("Transfer:", response.NOTIFY_INFO) + +def get_existing_deposit_address(): + response = bfx.rest.auth.get_deposit_address(wallet="exchange", method="bitcoin", renew=False) + print("Address:", response.NOTIFY_INFO) + +def create_new_deposit_address(): + response = bfx.rest.auth.get_deposit_address(wallet="exchange", method="bitcoin", renew=True) + print("Address:", response.NOTIFY_INFO) + +def withdraw(): + # tetheruse = Tether (ERC20) + response = bfx.rest.auth.submit_wallet_withdraw(wallet="exchange", method="tetheruse", amount=1, address="0x742d35Cc6634C0532925a3b844Bc454e4438f44e") + print("Address:", response.NOTIFY_INFO) + +def create_lighting_network_deposit_address(): + invoice = bfx.rest.auth.get_deposit_invoice(wallet="funding", currency="LNX", amount=0.001) + print("Invoice:", invoice) + +def get_movements(): + movements = bfx.rest.auth.get_movements(currency="BTC") + print("Movements:", movements) + +def run(): + transfer_wallet() + get_existing_deposit_address() + create_new_deposit_address() + withdraw() + create_lighting_network_deposit_address() + get_movements() + +run() \ No newline at end of file From 578882b4c25238ff4c174cfeb2cbff06042c48b7 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 18 Jan 2023 19:00:37 +0100 Subject: [PATCH 12/85] Small sync with new documentation improvements. --- bfxapi/rest/serializers.py | 2 +- bfxapi/rest/types.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index b6af19f..7cea3db 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -7,7 +7,7 @@ from .. notification import _Notification #region Serializers definition for Rest Public Endpoints PlatformStatus = generate_labeler_serializer("PlatformStatus", klass=types.PlatformStatus, labels=[ - "OPERATIVE" + "STATUS" ]) TradingPairTicker = generate_labeler_serializer("TradingPairTicker", klass=types.TradingPairTicker, labels=[ diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 1dbc533..9cd14b9 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -12,7 +12,7 @@ JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] @dataclass class PlatformStatus(_Type): - OPERATIVE: int + STATUS: int @dataclass class TradingPairTicker(_Type): From c471a3b52bec966e89ae995394a8222816558ca0 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 19 Jan 2023 16:35:17 +0100 Subject: [PATCH 13/85] Sync demos in examples/websocket with new bfxapi improvements. --- examples/websocket/create_order.py | 4 ++-- examples/websocket/order_book.py | 4 ++-- examples/websocket/raw_order_book.py | 4 ++-- examples/websocket/ticker.py | 4 +--- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/examples/websocket/create_order.py b/examples/websocket/create_order.py index b3146e9..7a6f645 100644 --- a/examples/websocket/create_order.py +++ b/examples/websocket/create_order.py @@ -4,7 +4,7 @@ import os from bfxapi.client import Client, Constants from bfxapi.websocket.enums import Error, OrderType -from bfxapi.websocket.typings import Notification, Order +from bfxapi.websocket.types import Notification, Order bfx = Client( WSS_HOST=Constants.WSS_HOST, @@ -30,7 +30,7 @@ async def on_authenticated(event): print("The order has been sent.") @bfx.wss.on("on-req-notification") -async def on_notification(notification: Notification): +async def on_notification(notification: Notification[Order]): print(f"Notification: {notification}.") @bfx.wss.on("order_new") diff --git a/examples/websocket/order_book.py b/examples/websocket/order_book.py index 0035cf8..fb583e1 100644 --- a/examples/websocket/order_book.py +++ b/examples/websocket/order_book.py @@ -7,7 +7,7 @@ from typing import List from bfxapi import Client, Constants from bfxapi.websocket.enums import Channels, Error -from bfxapi.websocket.typings import Subscriptions, TradingPairBook +from bfxapi.websocket.types import Subscriptions, TradingPairBook class OrderBook(object): def __init__(self, symbols: List[str]): @@ -18,7 +18,7 @@ class OrderBook(object): } def update(self, symbol: str, data: TradingPairBook) -> None: - price, count, amount = data["PRICE"], data["COUNT"], data["AMOUNT"] + price, count, amount = data.PRICE, data.COUNT, data.AMOUNT kind = (amount > 0) and "bids" or "asks" diff --git a/examples/websocket/raw_order_book.py b/examples/websocket/raw_order_book.py index 6cfc3c1..6819820 100644 --- a/examples/websocket/raw_order_book.py +++ b/examples/websocket/raw_order_book.py @@ -7,7 +7,7 @@ from typing import List from bfxapi import Client, Constants from bfxapi.websocket.enums import Channels, Error -from bfxapi.websocket.typings import Subscriptions, TradingPairRawBook +from bfxapi.websocket.types import Subscriptions, TradingPairRawBook class RawOrderBook(object): def __init__(self, symbols: List[str]): @@ -18,7 +18,7 @@ class RawOrderBook(object): } def update(self, symbol: str, data: TradingPairRawBook) -> None: - order_id, price, amount = data["ORDER_ID"], data["PRICE"], data["AMOUNT"] + order_id, price, amount = data.ORDER_ID, data.PRICE, data.AMOUNT kind = (amount > 0) and "bids" or "asks" diff --git a/examples/websocket/ticker.py b/examples/websocket/ticker.py index 5db8ed1..92e7058 100644 --- a/examples/websocket/ticker.py +++ b/examples/websocket/ticker.py @@ -1,10 +1,8 @@ # python -c "from examples.websocket.ticker import *" -import asyncio - from bfxapi import Client, Constants from bfxapi.websocket.enums import Channels -from bfxapi.websocket.typings import Subscriptions, TradingPairTicker +from bfxapi.websocket.types import Subscriptions, TradingPairTicker bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) From 5fe4d83902e88d7f38e86e8573c03c6fc4eb130a Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 19 Jan 2023 18:00:51 +0100 Subject: [PATCH 14/85] Move subscriptions type hinting from bfxapi/websocket/types.py to bfxapi/websocket/subscriptions.py. --- bfxapi/labeler.py | 8 ++++-- bfxapi/websocket/handlers.py | 25 ++++++----------- bfxapi/websocket/subscriptions.py | 28 +++++++++++++++++++ bfxapi/websocket/types.py | 42 ---------------------------- examples/websocket/order_book.py | 7 +++-- examples/websocket/raw_order_book.py | 7 +++-- examples/websocket/ticker.py | 6 ++-- 7 files changed, 54 insertions(+), 69 deletions(-) create mode 100644 bfxapi/websocket/subscriptions.py diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 87889e2..d2fbdb5 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -5,9 +5,11 @@ from typing import Type, Generic, TypeVar, Iterable, Optional, List, Tuple, Any, T = TypeVar("T", bound="_Type") class _Type(object): - def __init__(self, **kwargs): - for key, value in kwargs.items(): - self.__setattr__(key, value) + """ + Base class for any dataclass serializable by the _Serializer generic class. + """ + + pass class _Serializer(Generic[T]): def __init__(self, name: str, klass: Type[_Type], labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]): diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index 33c2654..f2fd7ea 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -6,9 +6,6 @@ from . import serializers from .enums import Channels from .exceptions import BfxWebsocketException -def _get_sub_dictionary(dictionary, keys): - return { key: dictionary[key] for key in dictionary if key in keys } - class PublicChannelsHandler(object): EVENTS = [ "t_ticker_update", "f_ticker_update", @@ -30,21 +27,23 @@ class PublicChannelsHandler(object): } def handle(self, subscription, *stream): + _clear = lambda dictionary, *args: { key: value for key, value in dictionary.items() if key not in args } + if channel := subscription["channel"] or channel in self.__handlers.keys(): - return self.__handlers[channel](subscription, *stream) + return self.__handlers[channel](_clear(subscription, "event", "channel"), *stream) def __ticker_channel_handler(self, subscription, *stream): if subscription["symbol"].startswith("t"): return self.event_emitter.emit( "t_ticker_update", - _get_sub_dictionary(subscription, [ "chanId", "symbol", "pair" ]), + subscription, serializers.TradingPairTicker.parse(*stream[0]) ) if subscription["symbol"].startswith("f"): return self.event_emitter.emit( "f_ticker_update", - _get_sub_dictionary(subscription, [ "chanId", "symbol", "currency" ]), + subscription, serializers.FundingCurrencyTicker.parse(*stream[0]) ) @@ -53,34 +52,32 @@ class PublicChannelsHandler(object): if subscription["symbol"].startswith("t"): return self.event_emitter.emit( { "te": "t_trade_executed", "tu": "t_trade_execution_update" }[type], - _get_sub_dictionary(subscription, [ "chanId", "symbol", "pair" ]), + subscription, serializers.TradingPairTrade.parse(*stream[1]) ) if subscription["symbol"].startswith("f"): return self.event_emitter.emit( { "fte": "f_trade_executed", "ftu": "f_trade_execution_update" }[type], - _get_sub_dictionary(subscription, [ "chanId", "symbol", "currency" ]), + subscription, serializers.FundingCurrencyTrade.parse(*stream[1]) ) if subscription["symbol"].startswith("t"): return self.event_emitter.emit( "t_trades_snapshot", - _get_sub_dictionary(subscription, [ "chanId", "symbol", "pair" ]), + subscription, [ serializers.TradingPairTrade.parse(*substream) for substream in stream[0] ] ) if subscription["symbol"].startswith("f"): return self.event_emitter.emit( "f_trades_snapshot", - _get_sub_dictionary(subscription, [ "chanId", "symbol", "currency" ]), + subscription, [ serializers.FundingCurrencyTrade.parse(*substream) for substream in stream[0] ] ) def __book_channel_handler(self, subscription, *stream): - subscription = _get_sub_dictionary(subscription, [ "chanId", "symbol", "prec", "freq", "len", "subId", "pair" ]) - type = subscription["symbol"][0] if subscription["prec"] == "R0": @@ -101,8 +98,6 @@ class PublicChannelsHandler(object): ) def __candles_channel_handler(self, subscription, *stream): - subscription = _get_sub_dictionary(subscription, [ "chanId", "key" ]) - if all(isinstance(substream, list) for substream in stream[0]): return self.event_emitter.emit( "candles_snapshot", @@ -117,8 +112,6 @@ class PublicChannelsHandler(object): ) def __status_channel_handler(self, subscription, *stream): - subscription = _get_sub_dictionary(subscription, [ "chanId", "key" ]) - if subscription["key"].startswith("deriv:"): return self.event_emitter.emit( "derivatives_status_update", diff --git a/bfxapi/websocket/subscriptions.py b/bfxapi/websocket/subscriptions.py new file mode 100644 index 0000000..08b7b21 --- /dev/null +++ b/bfxapi/websocket/subscriptions.py @@ -0,0 +1,28 @@ +from typing import TypedDict, Optional + +class Ticker(TypedDict): + chanId: int; symbol: str + pair: Optional[str] + currency: Optional[str] + +class Trades(TypedDict): + chanId: int; symbol: str + pair: Optional[str] + currency: Optional[str] + +class Book(TypedDict): + chanId: int + symbol: str + prec: str + freq: str + len: str + subId: int + pair: str + +class Candles(TypedDict): + chanId: int + key: str + +class Status(TypedDict): + chanId: int + key: str \ No newline at end of file diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py index 41838d5..97ef37b 100644 --- a/bfxapi/websocket/types.py +++ b/bfxapi/websocket/types.py @@ -8,48 +8,6 @@ from ..notification import Notification JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] -#region Type hinting for subscription objects - -class Subscriptions: - class TradingPairTicker(TypedDict): - chanId: int - symbol: str - pair: str - - class FundingCurrencyTicker(TypedDict): - chanId: int - symbol: str - currency: str - - class TradingPairTrades(TypedDict): - chanId: int - symbol: str - pair: str - - class FundingCurrencyTrades(TypedDict): - chanId: int - symbol: str - currency: str - - class Book(TypedDict): - chanId: int - symbol: str - prec: str - freq: str - len: str - subId: int - pair: str - - class Candles(TypedDict): - chanId: int - key: str - - class DerivativesStatus(TypedDict): - chanId: int - key: str - -#endregion - #region Type hinting for Websocket Public Channels @dataclass diff --git a/examples/websocket/order_book.py b/examples/websocket/order_book.py index fb583e1..57d0c0a 100644 --- a/examples/websocket/order_book.py +++ b/examples/websocket/order_book.py @@ -6,8 +6,9 @@ from typing import List from bfxapi import Client, Constants +from bfxapi.websocket import subscriptions from bfxapi.websocket.enums import Channels, Error -from bfxapi.websocket.types import Subscriptions, TradingPairBook +from bfxapi.websocket.types import TradingPairBook class OrderBook(object): def __init__(self, symbols: List[str]): @@ -53,12 +54,12 @@ def on_subscribed(subscription): print(f"Subscription successful for pair <{subscription['pair']}>") @bfx.wss.on("t_book_snapshot") -def on_t_book_snapshot(subscription: Subscriptions.Book, snapshot: List[TradingPairBook]): +def on_t_book_snapshot(subscription: subscriptions.Book, snapshot: List[TradingPairBook]): for data in snapshot: order_book.update(subscription["symbol"], data) @bfx.wss.on("t_book_update") -def on_t_book_update(subscription: Subscriptions.Book, data: TradingPairBook): +def on_t_book_update(subscription: subscriptions.Book, data: TradingPairBook): order_book.update(subscription["symbol"], data) bfx.wss.run() \ No newline at end of file diff --git a/examples/websocket/raw_order_book.py b/examples/websocket/raw_order_book.py index 6819820..e9076cc 100644 --- a/examples/websocket/raw_order_book.py +++ b/examples/websocket/raw_order_book.py @@ -6,8 +6,9 @@ from typing import List from bfxapi import Client, Constants +from bfxapi.websocket import subscriptions from bfxapi.websocket.enums import Channels, Error -from bfxapi.websocket.types import Subscriptions, TradingPairRawBook +from bfxapi.websocket.types import TradingPairRawBook class RawOrderBook(object): def __init__(self, symbols: List[str]): @@ -53,12 +54,12 @@ def on_subscribed(subscription): print(f"Subscription successful for pair <{subscription['pair']}>") @bfx.wss.on("t_raw_book_snapshot") -def on_t_raw_book_snapshot(subscription: Subscriptions.Book, snapshot: List[TradingPairRawBook]): +def on_t_raw_book_snapshot(subscription: subscriptions.Book, snapshot: List[TradingPairRawBook]): for data in snapshot: raw_order_book.update(subscription["symbol"], data) @bfx.wss.on("t_raw_book_update") -def on_t_raw_book_update(subscription: Subscriptions.Book, data: TradingPairRawBook): +def on_t_raw_book_update(subscription: subscriptions.Book, data: TradingPairRawBook): raw_order_book.update(subscription["symbol"], data) bfx.wss.run() \ No newline at end of file diff --git a/examples/websocket/ticker.py b/examples/websocket/ticker.py index 92e7058..aeb21cc 100644 --- a/examples/websocket/ticker.py +++ b/examples/websocket/ticker.py @@ -1,13 +1,15 @@ # python -c "from examples.websocket.ticker import *" from bfxapi import Client, Constants + +from bfxapi.websocket import subscriptions from bfxapi.websocket.enums import Channels -from bfxapi.websocket.types import Subscriptions, TradingPairTicker +from bfxapi.websocket.types import TradingPairTicker bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) @bfx.wss.on("t_ticker_update") -def on_t_ticker_update(subscription: Subscriptions.TradingPairTicker, data: TradingPairTicker): +def on_t_ticker_update(subscription: subscriptions.Ticker, data: TradingPairTicker): print(f"Subscription with channel ID: {subscription['chanId']}") print(f"Data: {data}") From 36725a183e7a04670016946ed9dc051e84f8aad5 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 19 Jan 2023 18:12:12 +0100 Subject: [PATCH 15/85] Move _BfxWebsocketBucket class in its own file bfxapi/websocket/_BfxWebsocketBucket.py. --- bfxapi/websocket/BfxWebsocketClient.py | 86 ++---------------------- bfxapi/websocket/_BfxWebsocketBucket.py | 87 +++++++++++++++++++++++++ bfxapi/websocket/subscriptions.py | 8 +++ 3 files changed, 100 insertions(+), 81 deletions(-) create mode 100644 bfxapi/websocket/_BfxWebsocketBucket.py diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index c5b2f31..e5c7efb 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -4,27 +4,16 @@ from typing import Literal, TypeVar, Callable, cast from pyee.asyncio import AsyncIOEventEmitter +from ._BfxWebsocketBucket import _HEARTBEAT, F, _require_websocket_connection, _BfxWebsocketBucket + from ._BfxWebsocketInputs import _BfxWebsocketInputs from .handlers import Channels, PublicChannelsHandler, AuthenticatedChannelsHandler -from .exceptions import ConnectionNotOpen, TooManySubscriptions, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion +from .exceptions import WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported from ..utils.encoder import JSONEncoder from ..utils.logger import Formatter, CustomLogger -_HEARTBEAT = "hb" - -F = TypeVar("F", bound=Callable[..., Literal[None]]) - -def _require_websocket_connection(function: F) -> F: - async def wrapper(self, *args, **kwargs): - if self.websocket == None or self.websocket.open == False: - raise ConnectionNotOpen("No open connection with the server.") - - await function(self, *args, **kwargs) - - return cast(F, wrapper) - def _require_websocket_authentication(function: F) -> F: async def wrapper(self, *args, **kwargs): if self.authentication == False: @@ -35,7 +24,7 @@ def _require_websocket_authentication(function: F) -> F: return cast(F, wrapper) class BfxWebsocketClient(object): - VERSION = 2 + VERSION = _BfxWebsocketBucket.VERSION MAXIMUM_BUCKETS_AMOUNT = 20 @@ -163,69 +152,4 @@ class BfxWebsocketClient(object): def handler(function): self.event_emitter.once(event, function) - return handler - -class _BfxWebsocketBucket(object): - MAXIMUM_SUBSCRIPTIONS_AMOUNT = 25 - - def __init__(self, host, event_emitter, __bucket_open_signal): - self.host, self.event_emitter, self.__bucket_open_signal = host, event_emitter, __bucket_open_signal - - self.websocket, self.subscriptions, self.pendings = None, dict(), list() - - self.handler = PublicChannelsHandler(event_emitter=self.event_emitter) - - async def _connect(self, index): - async for websocket in websockets.connect(self.host): - self.websocket = websocket - - self.__bucket_open_signal(index) - - try: - async for message in websocket: - message = json.loads(message) - - if isinstance(message, dict) and message["event"] == "info" and "version" in message: - if BfxWebsocketClient.VERSION != message["version"]: - raise OutdatedClientVersion(f"Mismatch between the client version and the server version. Update the library to the latest version to continue (client version: {BfxWebsocketClient.VERSION}, server version: {message['version']}).") - elif isinstance(message, dict) and message["event"] == "subscribed" and (chanId := message["chanId"]): - self.pendings = [ pending for pending in self.pendings if pending["subId"] != message["subId"] ] - self.subscriptions[chanId] = message - self.event_emitter.emit("subscribed", message) - elif isinstance(message, dict) and message["event"] == "unsubscribed" and (chanId := message["chanId"]): - if message["status"] == "OK": - del self.subscriptions[chanId] - elif isinstance(message, dict) and message["event"] == "error": - self.event_emitter.emit("wss-error", message["code"], message["msg"]) - elif isinstance(message, list) and (chanId := message[0]) and message[1] != _HEARTBEAT: - self.handler.handle(self.subscriptions[chanId], *message[1:]) - except websockets.ConnectionClosedError: continue - finally: await self.websocket.wait_closed(); break - - @_require_websocket_connection - async def _subscribe(self, channel, subId=None, **kwargs): - if len(self.subscriptions) + len(self.pendings) == _BfxWebsocketBucket.MAXIMUM_SUBSCRIPTIONS_AMOUNT: - raise TooManySubscriptions("The client has reached the maximum number of subscriptions.") - - subscription = { - "event": "subscribe", - "channel": channel, - "subId": subId or str(uuid.uuid4()), - - **kwargs - } - - self.pendings.append(subscription) - - await self.websocket.send(json.dumps(subscription)) - - @_require_websocket_connection - async def _unsubscribe(self, chanId): - await self.websocket.send(json.dumps({ - "event": "unsubscribe", - "chanId": chanId - })) - - @_require_websocket_connection - async def _close(self, code=1000, reason=str()): - await self.websocket.close(code=code, reason=reason) \ No newline at end of file + return handler \ No newline at end of file diff --git a/bfxapi/websocket/_BfxWebsocketBucket.py b/bfxapi/websocket/_BfxWebsocketBucket.py new file mode 100644 index 0000000..2cfe48c --- /dev/null +++ b/bfxapi/websocket/_BfxWebsocketBucket.py @@ -0,0 +1,87 @@ +import json, uuid, websockets + +from typing import Literal, TypeVar, Callable, cast + +from .handlers import PublicChannelsHandler + +from .exceptions import ConnectionNotOpen, TooManySubscriptions, OutdatedClientVersion + +_HEARTBEAT = "hb" + +F = TypeVar("F", bound=Callable[..., Literal[None]]) + +def _require_websocket_connection(function: F) -> F: + async def wrapper(self, *args, **kwargs): + if self.websocket == None or self.websocket.open == False: + raise ConnectionNotOpen("No open connection with the server.") + + await function(self, *args, **kwargs) + + return cast(F, wrapper) + +class _BfxWebsocketBucket(object): + VERSION = 2 + + MAXIMUM_SUBSCRIPTIONS_AMOUNT = 25 + + def __init__(self, host, event_emitter, __bucket_open_signal): + self.host, self.event_emitter, self.__bucket_open_signal = host, event_emitter, __bucket_open_signal + + self.websocket, self.subscriptions, self.pendings = None, dict(), list() + + self.handler = PublicChannelsHandler(event_emitter=self.event_emitter) + + async def _connect(self, index): + async for websocket in websockets.connect(self.host): + self.websocket = websocket + + self.__bucket_open_signal(index) + + try: + async for message in websocket: + message = json.loads(message) + + if isinstance(message, dict) and message["event"] == "info" and "version" in message: + if _BfxWebsocketBucket.VERSION != message["version"]: + raise OutdatedClientVersion(f"Mismatch between the client version and the server version. Update the library to the latest version to continue (client version: {_BfxWebsocketBucket.VERSION}, server version: {message['version']}).") + elif isinstance(message, dict) and message["event"] == "subscribed" and (chanId := message["chanId"]): + self.pendings = [ pending for pending in self.pendings if pending["subId"] != message["subId"] ] + self.subscriptions[chanId] = message + self.event_emitter.emit("subscribed", message) + elif isinstance(message, dict) and message["event"] == "unsubscribed" and (chanId := message["chanId"]): + if message["status"] == "OK": + del self.subscriptions[chanId] + elif isinstance(message, dict) and message["event"] == "error": + self.event_emitter.emit("wss-error", message["code"], message["msg"]) + elif isinstance(message, list) and (chanId := message[0]) and message[1] != _HEARTBEAT: + self.handler.handle(self.subscriptions[chanId], *message[1:]) + except websockets.ConnectionClosedError: continue + finally: await self.websocket.wait_closed(); break + + @_require_websocket_connection + async def _subscribe(self, channel, subId=None, **kwargs): + if len(self.subscriptions) + len(self.pendings) == _BfxWebsocketBucket.MAXIMUM_SUBSCRIPTIONS_AMOUNT: + raise TooManySubscriptions("The client has reached the maximum number of subscriptions.") + + subscription = { + "event": "subscribe", + "channel": channel, + "subId": subId or str(uuid.uuid4()), + + **kwargs + } + + self.pendings.append(subscription) + + await self.websocket.send(json.dumps(subscription)) + + @_require_websocket_connection + async def _unsubscribe(self, chanId): + await self.websocket.send(json.dumps({ + "event": "unsubscribe", + "chanId": chanId + })) + + @_require_websocket_connection + async def _close(self, code=1000, reason=str()): + await self.websocket.close(code=code, reason=reason) \ No newline at end of file diff --git a/bfxapi/websocket/subscriptions.py b/bfxapi/websocket/subscriptions.py index 08b7b21..2acc1af 100644 --- a/bfxapi/websocket/subscriptions.py +++ b/bfxapi/websocket/subscriptions.py @@ -1,5 +1,13 @@ from typing import TypedDict, Optional +__all__ = [ + "Ticker", + "Trades", + "Book", + "Candles", + "Status" +] + class Ticker(TypedDict): chanId: int; symbol: str pair: Optional[str] From ae42fb7d93cea286487ea2b7afebf10771bb0009 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Tue, 24 Jan 2023 12:10:57 +0100 Subject: [PATCH 16/85] Add _RecursiveSerializer class in bfxapi/labeler.py file. Add support to new pulse endpoints (with serializers and types). Add examples/rest/get_pulse_data.py demo. --- bfxapi/labeler.py | 22 +++++++++++++-- bfxapi/rest/BfxRestInterface.py | 15 ++++++++++- bfxapi/rest/serializers.py | 47 ++++++++++++++++++++++++++++++++- bfxapi/rest/types.py | 29 ++++++++++++++++++++ examples/rest/get_pulse_data.py | 22 +++++++++++++++ 5 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 examples/rest/get_pulse_data.py diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index d2fbdb5..52b0e2b 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -1,6 +1,6 @@ from .exceptions import LabelerSerializerException -from typing import Type, Generic, TypeVar, Iterable, Optional, List, Tuple, Any, cast +from typing import Type, Generic, TypeVar, Iterable, Optional, Dict, List, Tuple, Any, cast T = TypeVar("T", bound="_Type") @@ -28,5 +28,23 @@ class _Serializer(Generic[T]): def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: return cast(T, self.klass(**dict(self._serialize(*values, skip=skip)))) +class _RecursiveSerializer(_Serializer, Generic[T]): + def __init__(self, name: str, klass: Type[_Type], labels: List[str], serializers: Dict[str, Type[_Serializer]], IGNORE: List[str] = ["_PLACEHOLDER"]): + super().__init__(name, klass, labels, IGNORE) + + self.serializers = serializers + + def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: + serialization = dict(self._serialize(*values, skip=skip)) + + for key in serialization: + if key in self.serializers.keys(): + serialization[key] = self.serializers[key].parse(*serialization[key], skip=skip) + + return cast(T, self.klass(**serialization)) + def generate_labeler_serializer(name: str, klass: Type[T], labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> _Serializer[T]: - return _Serializer[T](name, klass, labels, IGNORE) \ No newline at end of file + return _Serializer[T](name, klass, labels, IGNORE) + +def generate_recursive_serializer(name: str, klass: Type[T], labels: List[str], serializers: Dict[str, Type[_Serializer]], IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> _RecursiveSerializer[T]: + return _RecursiveSerializer[T](name, klass, labels, serializers, IGNORE) \ No newline at end of file diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 741a3ec..bca18c1 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -58,7 +58,7 @@ class _Requests(object): 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]}>.") + raise UnknownGenericError(f"The server replied to the request with a generic error with message: <{data[2]}>.") return data @@ -255,6 +255,19 @@ class _RestPublicEndpoints(_Requests): def conf(self, config: Config) -> Any: return self._GET(f"conf/{config}")[0] + def get_pulse_profile(self, nickname: str) -> PulseProfile: + return serializers.PulseProfile.parse(*self._GET(f"pulse/profile/{nickname}")) + + def get_pulse_history(self, end: Optional[str] = None, limit: Optional[int] = None) -> List[PulseMessage]: + messages = list() + + for subdata in self._GET("pulse/hist", params={ "end": end, "limit": limit }): + subdata[18] = subdata[18][0] + message = serializers.PulseMessage.parse(*subdata) + messages.append(message) + + return messages + class _RestAuthenticatedEndpoints(_Requests): def get_wallets(self) -> List[Wallet]: return [ serializers.Wallet.parse(*subdata) for subdata in self._POST("auth/r/wallets") ] diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 7cea3db..5209d6f 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -1,6 +1,6 @@ from . import types -from .. labeler import generate_labeler_serializer +from .. labeler import generate_labeler_serializer, generate_recursive_serializer from .. notification import _Notification @@ -185,6 +185,51 @@ FundingStatistic = generate_labeler_serializer("FundingStatistic", klass=types.F "FUNDING_BELOW_THRESHOLD" ]) +PulseProfile = generate_labeler_serializer("PulseProfile", klass=types.PulseProfile, labels=[ + "PUID", + "MTS", + "_PLACEHOLDER", + "NICKNAME", + "_PLACEHOLDER", + "PICTURE", + "TEXT", + "_PLACEHOLDER", + "_PLACEHOLDER", + "TWITTER_HANDLE", + "_PLACEHOLDER", + "FOLLOWERS", + "FOLLOWING", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "TIPPING_STATUS" +]) + +PulseMessage = generate_recursive_serializer("PulseMessage", klass=types.PulseMessage, serializers={ "PROFILE": PulseProfile }, labels=[ + "PID", + "MTS", + "_PLACEHOLDER", + "PUID", + "_PLACEHOLDER", + "TITLE", + "CONTENT", + "_PLACEHOLDER", + "_PLACEHOLDER", + "IS_PIN", + "IS_PUBLIC", + "COMMENTS_DISABLED", + "TAGS", + "ATTACHMENTS", + "META", + "LIKES", + "_PLACEHOLDER", + "_PLACEHOLDER", + "PROFILE", + "COMMENTS", + "_PLACEHOLDER", + "_PLACEHOLDER" +]) + #endregion #region Serializers definition for Rest Authenticated Endpoints diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 9cd14b9..7193d5c 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -152,6 +152,35 @@ class FundingStatistic(_Type): FUNDING_AMOUNT_USED: float FUNDING_BELOW_THRESHOLD: float +@dataclass +class PulseProfile(_Type): + PUID: str + MTS: int + NICKNAME: str + PICTURE: str + TEXT: str + TWITTER_HANDLE: str + FOLLOWERS: int + FOLLOWING: int + TIPPING_STATUS: int + +@dataclass +class PulseMessage(_Type): + PID: str + MTS: int + PUID: str + TITLE: str + CONTENT: str + IS_PIN: int + IS_PUBLIC: int + COMMENTS_DISABLED: int + TAGS: List[str] + ATTACHMENTS: List[str] + META: List[JSON] + LIKES: int + PROFILE: PulseProfile + COMMENTS: int + #endregion #region Type hinting for Rest Authenticated Endpoints diff --git a/examples/rest/get_pulse_data.py b/examples/rest/get_pulse_data.py new file mode 100644 index 0000000..fc5c15f --- /dev/null +++ b/examples/rest/get_pulse_data.py @@ -0,0 +1,22 @@ +# python -c "from examples.rest.get_pulse_data import *" + +import time + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST +) + +now = int(round(time.time() * 1000)) + +messages = bfx.rest.public.get_pulse_history(end=now, limit=100) + +for message in messages: + print(f"Message: {message}") + print(message.CONTENT) + print(message.PROFILE.PICTURE) + +profile = bfx.rest.public.get_pulse_profile("News") +print(f"Profile: {profile}") +print(f"Profile picture: {profile.PICTURE}") \ No newline at end of file From 02a2e962d303fa371a6ed0412079898fbf0a603c Mon Sep 17 00:00:00 2001 From: itsdeka Date: Tue, 24 Jan 2023 12:35:12 +0100 Subject: [PATCH 17/85] Add support to new calculation endpoints. --- bfxapi/rest/BfxRestInterface.py | 19 +++++++++++++++++-- bfxapi/rest/serializers.py | 11 +++++++++++ bfxapi/rest/types.py | 10 ++++++++++ examples/rest/extra_calcs.py | 24 ++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 examples/rest/extra_calcs.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index bca18c1..3dc559f 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -62,13 +62,13 @@ class _Requests(object): return data - def _POST(self, endpoint, params = None, data = None, _append_authentication_headers = True): + def _POST(self, endpoint, params = None, data = None): 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: + if self.API_KEY and self.API_SECRET: headers = { **headers, **self.__build_authentication_headers(endpoint, data) } response = requests.post(f"{self.host}/{endpoint}", params=params, data=data, headers=headers) @@ -268,6 +268,21 @@ class _RestPublicEndpoints(_Requests): return messages + def get_trading_market_average_price(self, symbol: str, amount: Union[Decimal, str], price_limit: Optional[Union[Decimal, str]] = None) -> TradingMarketAveragePrice: + data = { + "symbol": symbol, "amount": amount, "price_limit": price_limit + } + + return serializers.TradingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data=data)) + + def get_funding_market_average_price(self, symbol: str, amount: Union[Decimal, str], period: int, rate_limit: Optional[Union[Decimal, str]] = None) -> FundingMarketAveragePrice: + data = { + "symbol": symbol, "amount": amount, "period": period, + "rate_limit": rate_limit + } + + return serializers.FundingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data=data)) + class _RestAuthenticatedEndpoints(_Requests): def get_wallets(self) -> List[Wallet]: return [ serializers.Wallet.parse(*subdata) for subdata in self._POST("auth/r/wallets") ] diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 5209d6f..822e4cf 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -230,6 +230,17 @@ PulseMessage = generate_recursive_serializer("PulseMessage", klass=types.PulseMe "_PLACEHOLDER" ]) +TradingMarketAveragePrice = generate_labeler_serializer("TradingMarketAveragePrice", klass=types.TradingMarketAveragePrice, labels=[ + "PRICE_AVG", + "AMOUNT" +]) + + +FundingMarketAveragePrice = generate_labeler_serializer("FundingMarketAveragePrice", klass=types.FundingMarketAveragePrice, labels=[ + "RATE_AVG", + "AMOUNT" +]) + #endregion #region Serializers definition for Rest Authenticated Endpoints diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 7193d5c..c0a2611 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -181,6 +181,16 @@ class PulseMessage(_Type): PROFILE: PulseProfile COMMENTS: int +@dataclass +class TradingMarketAveragePrice(_Type): + PRICE_AVG: float + AMOUNT: float + +@dataclass +class FundingMarketAveragePrice(_Type): + RATE_AVG: float + AMOUNT: float + #endregion #region Type hinting for Rest Authenticated Endpoints diff --git a/examples/rest/extra_calcs.py b/examples/rest/extra_calcs.py new file mode 100644 index 0000000..cbda41c --- /dev/null +++ b/examples/rest/extra_calcs.py @@ -0,0 +1,24 @@ +# python -c "from examples.rest.extra_calcs import *" + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST +) + +t_symbol_response = bfx.rest.public.get_trading_market_average_price( + symbol="tBTCUSD", + amount=-100, + price_limit="20000.5" +) + +print(t_symbol_response) + +f_symbol_response = bfx.rest.public.get_funding_market_average_price( + symbol="fUSD", + amount=100, + period=2, + rate_limit="0.00015" +) + +print(f_symbol_response) \ No newline at end of file From 01c8192d10de65cd32f27eec7280241fa827f556 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Tue, 24 Jan 2023 12:49:01 +0100 Subject: [PATCH 18/85] fx rate --- bfxapi/rest/BfxRestInterface.py | 3 +++ bfxapi/rest/serializers.py | 4 ++++ bfxapi/rest/types.py | 4 ++++ examples/rest/extra_calcs.py | 8 ++++++-- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 3dc559f..4e63aba 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -283,6 +283,9 @@ class _RestPublicEndpoints(_Requests): return serializers.FundingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data=data)) + def get_fx_rate(self, ccy1: str, ccy2: str) -> FxRate: + return serializers.FxRate.parse(*self._POST("calc/fx", data={ "ccy1": ccy1, "ccy2": ccy2 })) + class _RestAuthenticatedEndpoints(_Requests): def get_wallets(self) -> List[Wallet]: return [ serializers.Wallet.parse(*subdata) for subdata in self._POST("auth/r/wallets") ] diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 822e4cf..3a69c53 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -241,6 +241,10 @@ FundingMarketAveragePrice = generate_labeler_serializer("FundingMarketAveragePri "AMOUNT" ]) +FxRate = generate_labeler_serializer("FxRate", klass=types.FxRate, labels=[ + "CURRENT_RATE" +]) + #endregion #region Serializers definition for Rest Authenticated Endpoints diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index c0a2611..4b04661 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -191,6 +191,10 @@ class FundingMarketAveragePrice(_Type): RATE_AVG: float AMOUNT: float +@dataclass +class FxRate(_Type): + CURRENT_RATE: float + #endregion #region Type hinting for Rest Authenticated Endpoints diff --git a/examples/rest/extra_calcs.py b/examples/rest/extra_calcs.py index cbda41c..2603cd8 100644 --- a/examples/rest/extra_calcs.py +++ b/examples/rest/extra_calcs.py @@ -12,7 +12,7 @@ t_symbol_response = bfx.rest.public.get_trading_market_average_price( price_limit="20000.5" ) -print(t_symbol_response) +print(t_symbol_response.PRICE_AVG) f_symbol_response = bfx.rest.public.get_funding_market_average_price( symbol="fUSD", @@ -21,4 +21,8 @@ f_symbol_response = bfx.rest.public.get_funding_market_average_price( rate_limit="0.00015" ) -print(f_symbol_response) \ No newline at end of file +print(f_symbol_response.RATE_AVG) + +fx_rate = bfx.rest.public.get_fx_rate(ccy1="USD", ccy2="EUR") + +print(fx_rate.CURRENT_RATE) \ No newline at end of file From 8047e3609d74f1f979de68c6429c425d4611d9e3 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Tue, 24 Jan 2023 15:23:48 +0100 Subject: [PATCH 19/85] rename subdata to sub_data, add margin info endpoints --- bfxapi/rest/BfxRestInterface.py | 71 ++++++++++++++----------- bfxapi/rest/serializers.py | 16 ++++++ bfxapi/rest/types.py | 16 ++++++ examples/rest/get_authenticated_data.py | 10 ++++ 4 files changed, 83 insertions(+), 30 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 4e63aba..347833b 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -99,11 +99,11 @@ class _RestPublicEndpoints(_Requests): parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse } - return [ cast(Union[TradingPairTicker, FundingCurrencyTicker], parsers[subdata[0][0]](*subdata)) for subdata in data ] + return [ cast(Union[TradingPairTicker, FundingCurrencyTicker], parsers[sub_data[0][0]](*sub_data)) for sub_data in data ] def get_t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]: if isinstance(pairs, str) and pairs == "ALL": - return [ cast(TradingPairTicker, subdata) for subdata in self.get_tickers([ "ALL" ]) if cast(str, subdata.SYMBOL).startswith("t") ] + return [ cast(TradingPairTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.SYMBOL).startswith("t") ] data = self.get_tickers([ "t" + pair for pair in pairs ]) @@ -111,7 +111,7 @@ class _RestPublicEndpoints(_Requests): 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") ] + return [ cast(FundingCurrencyTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.SYMBOL).startswith("f") ] data = self.get_tickers([ "f" + currency for currency in currencies ]) @@ -132,29 +132,29 @@ class _RestPublicEndpoints(_Requests): data = self._GET("tickers/hist", params=params) - return [ serializers.TickersHistory.parse(*subdata) for subdata in data ] + return [ serializers.TickersHistory.parse(*sub_data) for sub_data 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 ] + return [ serializers.TradingPairTrade.parse(*sub_data) for sub_data in data ] def get_f_trades(self, currency: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[FundingCurrencyTrade]: params = { "limit": limit, "start": start, "end": end, "sort": sort } data = self._GET(f"trades/{'f' + currency}/hist", params=params) - return [ serializers.FundingCurrencyTrade.parse(*subdata) for subdata in data ] + return [ serializers.FundingCurrencyTrade.parse(*sub_data) for sub_data in data ] def get_t_book(self, pair: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairBook]: - return [ serializers.TradingPairBook.parse(*subdata) for subdata in self._GET(f"book/{'t' + pair}/{precision}", params={ "len": len }) ] + return [ serializers.TradingPairBook.parse(*sub_data) for sub_data 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 }) ] + return [ serializers.FundingCurrencyBook.parse(*sub_data) for sub_data 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 }) ] + return [ serializers.TradingPairRawBook.parse(*sub_data) for sub_data 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 }) ] + return [ serializers.FundingCurrencyRawBook.parse(*sub_data) for sub_data in self._GET(f"book/{'f' + currency}/R0", params={ "len": len }) ] def get_stats_hist( self, @@ -163,7 +163,7 @@ class _RestPublicEndpoints(_Requests): ) -> 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 ] + return [ serializers.Statistic.parse(*sub_data) for sub_data in data ] def get_stats_last( self, @@ -181,7 +181,7 @@ class _RestPublicEndpoints(_Requests): ) -> List[Candle]: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self._GET(f"candles/trade:{tf}:{symbol}/hist", params=params) - return [ serializers.Candle.parse(*subdata) for subdata in data ] + return [ serializers.Candle.parse(*sub_data) for sub_data in data ] def get_candles_last( self, @@ -199,7 +199,7 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"status/deriv", params=params) - return [ serializers.DerivativesStatus.parse(*subdata) for subdata in data ] + return [ serializers.DerivativesStatus.parse(*sub_data) for sub_data in data ] def get_derivatives_status_history( self, @@ -210,14 +210,14 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"status/{type}/{symbol}/hist", params=params) - return [ serializers.DerivativesStatus.parse(*subdata, skip=[ "KEY" ]) for subdata in data ] + return [ serializers.DerivativesStatus.parse(*sub_data, skip=[ "KEY" ]) for sub_data in data ] def get_liquidations(self, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Liquidation]: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self._GET("liquidations/hist", params=params) - return [ serializers.Liquidation.parse(*subdata[0]) for subdata in data ] + return [ serializers.Liquidation.parse(*sub_data[0]) for sub_data in data ] def get_seed_candles(self, symbol: str, tf: str = '1m', sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Candle]: @@ -225,7 +225,7 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"candles/trade:{tf}:{symbol}/hist?limit={limit}&start={start}&end={end}&sort={sort}", params=params) - return [ serializers.Candle.parse(*subdata) for subdata in data ] + return [ serializers.Candle.parse(*sub_data) for sub_data in data ] def get_leaderboards_hist( self, @@ -234,7 +234,7 @@ class _RestPublicEndpoints(_Requests): ) -> 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 ] + return [ serializers.Leaderboard.parse(*sub_data) for sub_data in data ] def get_leaderboards_last( self, @@ -250,7 +250,7 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"funding/stats/{symbol}/hist", params=params) - return [ serializers.FundingStatistic.parse(*subdata) for subdata in data ] + return [ serializers.FundingStatistic.parse(*sub_data) for sub_data in data ] def conf(self, config: Config) -> Any: return self._GET(f"conf/{config}")[0] @@ -288,7 +288,7 @@ class _RestPublicEndpoints(_Requests): class _RestAuthenticatedEndpoints(_Requests): def get_wallets(self) -> List[Wallet]: - return [ serializers.Wallet.parse(*subdata) for subdata in self._POST("auth/r/wallets") ] + return [ serializers.Wallet.parse(*sub_data) for sub_data in self._POST("auth/r/wallets") ] def get_orders(self, symbol: Optional[str] = None, ids: Optional[List[str]] = None) -> List[Order]: endpoint = "auth/r/orders" @@ -296,10 +296,10 @@ class _RestAuthenticatedEndpoints(_Requests): if symbol != None: endpoint += f"/{symbol}" - return [ serializers.Order.parse(*subdata) for subdata in self._POST(endpoint, data={ "id": ids }) ] + return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, data={ "id": ids }) ] def get_positions(self) -> List[Position]: - return [ serializers.Position.parse(*subdata) for subdata in self._POST("auth/r/positions") ] + return [ serializers.Position.parse(*sub_data) for sub_data in self._POST("auth/r/positions") ] def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str], price: Optional[Union[Decimal, str]] = None, lev: Optional[int] = None, @@ -360,7 +360,7 @@ class _RestAuthenticatedEndpoints(_Requests): "limit": limit } - return [ serializers.Order.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] + return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] def get_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Trade]: if symbol == None: @@ -373,10 +373,10 @@ class _RestAuthenticatedEndpoints(_Requests): "limit": limit } - return [ serializers.Trade.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] + return [ serializers.Trade.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] def get_order_trades(self, symbol: str, id: int) -> List[OrderTrade]: - return [ serializers.OrderTrade.parse(*subdata) for subdata in self._POST(f"auth/r/order/{symbol}:{id}/trades") ] + return [ serializers.OrderTrade.parse(*sub_data) for sub_data in self._POST(f"auth/r/order/{symbol}:{id}/trades") ] def get_ledgers(self, currency: str, category: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Ledger]: data = { @@ -385,7 +385,7 @@ class _RestAuthenticatedEndpoints(_Requests): "limit": limit } - return [ serializers.Ledger.parse(*subdata) for subdata in self._POST(f"auth/r/ledgers/{currency}/hist", data=data) ] + return [ serializers.Ledger.parse(*sub_data) for sub_data in self._POST(f"auth/r/ledgers/{currency}/hist", data=data) ] def get_funding_offers(self, symbol: Optional[str] = None) -> List[FundingOffer]: endpoint = "auth/r/funding/offers" @@ -393,7 +393,7 @@ class _RestAuthenticatedEndpoints(_Requests): if symbol != None: endpoint += f"/{symbol}" - return [ serializers.FundingOffer.parse(*subdata) for subdata in self._POST(endpoint) ] + return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint) ] def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, str], rate: Union[Decimal, str], period: int, @@ -419,14 +419,14 @@ class _RestAuthenticatedEndpoints(_Requests): "limit": limit } - return [ serializers.FundingOffer.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] + return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] def get_funding_credits(self, symbol: Optional[str] = None) -> List[FundingCredit]: if symbol == None: endpoint = "auth/r/funding/credits" else: endpoint = f"auth/r/funding/credits/{symbol}" - return [ serializers.FundingCredit.parse(*subdata) for subdata in self._POST(endpoint) ] + return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint) ] def get_funding_credits_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingCredit]: if symbol == None: @@ -438,7 +438,7 @@ class _RestAuthenticatedEndpoints(_Requests): "limit": limit } - return [ serializers.FundingCredit.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] + return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] def submit_wallet_transfer(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, str]) -> Notification[Transfer]: data = { @@ -484,4 +484,15 @@ class _RestAuthenticatedEndpoints(_Requests): "limit": limit } - return [ serializers.Movement.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] \ No newline at end of file + return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + + def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo: + data = self._POST(f"auth/r/info/margin/{symbol}") + + return serializers.SymbolMarginInfo.parse(*([data[1]] + data[2])) + + def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]: + return [ serializers.SymbolMarginInfo.parse(*([sub_data[1]] + sub_data[2])) for sub_data in self._POST(f"auth/r/info/margin/sym_all") ] + + def get_base_margin_info(self) -> BaseMarginInfo: + return serializers.BaseMarginInfo.parse(*(self._POST(f"auth/r/info/margin/base")[1])) diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 3a69c53..044eb51 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -473,4 +473,20 @@ Movement = generate_labeler_serializer("Movement", klass=types.Movement, labels= "WITHDRAW_TRANSACTION_NOTE" ]) +SymbolMarginInfo = generate_labeler_serializer("SymbolMarginInfo", klass=types.SymbolMarginInfo, labels=[ + "SYMBOL", + "TRADABLE_BALANCE", + "GROSS_BALANCE", + "BUY", + "SELL" +]) + +BaseMarginInfo = generate_labeler_serializer("BaseMarginInfo", klass=types.BaseMarginInfo, labels=[ + "USER_PL", + "USER_SWAPS", + "MARGIN_BALANCE", + "MARGIN_NET", + "MARGIN_MIN" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 4b04661..2387bea 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -373,5 +373,21 @@ class Movement(_Type): DESTINATION_ADDRESS: str TRANSACTION_ID: str WITHDRAW_TRANSACTION_NOTE: str + +@dataclass +class SymbolMarginInfo(_Type): + SYMBOL: str + TRADABLE_BALANCE: float + GROSS_BALANCE: float + BUY: float + SELL: float + +@dataclass +class BaseMarginInfo(_Type): + USER_PL: float + USER_SWAPS: float + MARGIN_BALANCE: float + MARGIN_NET: float + MARGIN_MIN: float #endregion \ No newline at end of file diff --git a/examples/rest/get_authenticated_data.py b/examples/rest/get_authenticated_data.py index 881cbf6..79a24d9 100644 --- a/examples/rest/get_authenticated_data.py +++ b/examples/rest/get_authenticated_data.py @@ -85,6 +85,15 @@ def log_funding_credits_history(): print("Funding credit history:") [print(c) for c in credit] +def log_margin_info(): + btcusd_margin_info = bfx.rest.auth.get_symbol_margin_info('tBTCUSD') + print(f"tBTCUSD margin info {btcusd_margin_info}") + + sym_all_margin_info = bfx.rest.auth.get_all_symbols_margin_info() + print(f"Sym all margin info {sym_all_margin_info}") + + base_margin_info = bfx.rest.auth.get_base_margin_info() + print(f"Base margin info {base_margin_info}") def run(): log_wallets() @@ -97,5 +106,6 @@ def run(): log_funding_offer_history() log_funding_credits() log_funding_credits_history() + log_margin_info() run() \ No newline at end of file From 28c00d460b6155787c60d7a4e7a4b3cb09de20de Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 24 Jan 2023 19:02:44 +0100 Subject: [PATCH 20/85] Exclude subId field from every subscription dictionary. --- bfxapi/websocket/handlers.py | 2 +- bfxapi/websocket/subscriptions.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index f2fd7ea..c56046a 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -30,7 +30,7 @@ class PublicChannelsHandler(object): _clear = lambda dictionary, *args: { key: value for key, value in dictionary.items() if key not in args } if channel := subscription["channel"] or channel in self.__handlers.keys(): - return self.__handlers[channel](_clear(subscription, "event", "channel"), *stream) + return self.__handlers[channel](_clear(subscription, "event", "channel", "subId"), *stream) def __ticker_channel_handler(self, subscription, *stream): if subscription["symbol"].startswith("t"): diff --git a/bfxapi/websocket/subscriptions.py b/bfxapi/websocket/subscriptions.py index 2acc1af..e22bb5e 100644 --- a/bfxapi/websocket/subscriptions.py +++ b/bfxapi/websocket/subscriptions.py @@ -24,7 +24,6 @@ class Book(TypedDict): prec: str freq: str len: str - subId: int pair: str class Candles(TypedDict): From ed12bf473f4cad24a77ec58949e99c48dc9dedad Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 25 Jan 2023 18:18:15 +0100 Subject: [PATCH 21/85] Rewrite and extend custom JSONEncoder in bfxapi/utils/encoder.py to automatically convert floats to strs. Change every Union[Decimal, str] type to Union[Decimal, float, str]. Fix type hinting bug in labeler.py. --- bfxapi/labeler.py | 4 ++-- bfxapi/rest/BfxRestInterface.py | 26 ++++++++++++------------- bfxapi/rest/types.py | 4 +--- bfxapi/utils/encoder.py | 26 +++++++++++++++++++++++-- bfxapi/websocket/_BfxWebsocketInputs.py | 16 +++++++-------- bfxapi/websocket/types.py | 4 +--- 6 files changed, 49 insertions(+), 31 deletions(-) diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 52b0e2b..6201b82 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -29,7 +29,7 @@ class _Serializer(Generic[T]): return cast(T, self.klass(**dict(self._serialize(*values, skip=skip)))) class _RecursiveSerializer(_Serializer, Generic[T]): - def __init__(self, name: str, klass: Type[_Type], labels: List[str], serializers: Dict[str, Type[_Serializer]], IGNORE: List[str] = ["_PLACEHOLDER"]): + def __init__(self, name: str, klass: Type[_Type], labels: List[str], serializers: Dict[str, _Serializer[Any]], IGNORE: List[str] = ["_PLACEHOLDER"]): super().__init__(name, klass, labels, IGNORE) self.serializers = serializers @@ -46,5 +46,5 @@ class _RecursiveSerializer(_Serializer, Generic[T]): def generate_labeler_serializer(name: str, klass: Type[T], labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> _Serializer[T]: return _Serializer[T](name, klass, labels, IGNORE) -def generate_recursive_serializer(name: str, klass: Type[T], labels: List[str], serializers: Dict[str, Type[_Serializer]], IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> _RecursiveSerializer[T]: +def generate_recursive_serializer(name: str, klass: Type[T], labels: List[str], serializers: Dict[str, _Serializer[Any]], IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> _RecursiveSerializer[T]: return _RecursiveSerializer[T](name, klass, labels, serializers, IGNORE) \ No newline at end of file diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 347833b..88a08f5 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -268,14 +268,14 @@ class _RestPublicEndpoints(_Requests): return messages - def get_trading_market_average_price(self, symbol: str, amount: Union[Decimal, str], price_limit: Optional[Union[Decimal, str]] = None) -> TradingMarketAveragePrice: + def get_trading_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], price_limit: Optional[Union[Decimal, float, str]] = None) -> TradingMarketAveragePrice: data = { "symbol": symbol, "amount": amount, "price_limit": price_limit } return serializers.TradingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data=data)) - def get_funding_market_average_price(self, symbol: str, amount: Union[Decimal, str], period: int, rate_limit: Optional[Union[Decimal, str]] = None) -> FundingMarketAveragePrice: + def get_funding_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], period: int, rate_limit: Optional[Union[Decimal, float, str]] = None) -> FundingMarketAveragePrice: data = { "symbol": symbol, "amount": amount, "period": period, "rate_limit": rate_limit @@ -301,9 +301,9 @@ class _RestAuthenticatedEndpoints(_Requests): def get_positions(self) -> List[Position]: return [ serializers.Position.parse(*sub_data) for sub_data in self._POST("auth/r/positions") ] - def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str], - price: Optional[Union[Decimal, str]] = None, lev: Optional[int] = None, - price_trailing: Optional[Union[Decimal, str]] = None, price_aux_limit: Optional[Union[Decimal, str]] = None, price_oco_stop: Optional[Union[Decimal, str]] = None, + def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, float, str], + price: Optional[Union[Decimal, float, str]] = None, lev: Optional[int] = None, + price_trailing: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_oco_stop: Optional[Union[Decimal, float, str]] = None, gid: Optional[int] = None, cid: Optional[int] = None, flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification[Order]: data = { @@ -316,10 +316,10 @@ class _RestAuthenticatedEndpoints(_Requests): return serializers._Notification[Order](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, + def update_order(self, id: int, amount: Optional[Union[Decimal, float, str]] = None, price: Optional[Union[Decimal, float, str]] = None, cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, - flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, str]] = None, - price_aux_limit: Optional[Union[Decimal, str]] = None, price_trailing: Optional[Union[Decimal, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification[Order]: + flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, float, str]] = None, + price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification[Order]: data = { "id": id, "amount": amount, "price": price, "cid": cid, "cid_date": cid_date, "gid": gid, @@ -395,8 +395,8 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint) ] - def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, str], - rate: Union[Decimal, str], period: int, + def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, float, str], + rate: Union[Decimal, float, str], period: int, flags: Optional[int] = 0) -> Notification[FundingOffer]: data = { "type": type, "symbol": symbol, "amount": amount, @@ -440,7 +440,7 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] - def submit_wallet_transfer(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, str]) -> Notification[Transfer]: + def submit_wallet_transfer(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, float, str]) -> Notification[Transfer]: data = { "from": from_wallet, "to": to_wallet, "currency": currency, "currency_to": currency_to, @@ -449,7 +449,7 @@ class _RestAuthenticatedEndpoints(_Requests): return serializers._Notification[Transfer](serializer=serializers.Transfer).parse(*self._POST("auth/w/transfer", data=data)) - def submit_wallet_withdraw(self, wallet: str, method: str, address: str, amount: Union[Decimal, str]) -> Notification[Withdrawal]: + def submit_wallet_withdraw(self, wallet: str, method: str, address: str, amount: Union[Decimal, float, str]) -> Notification[Withdrawal]: data = { "wallet": wallet, "method": method, "address": address, "amount": amount, @@ -466,7 +466,7 @@ class _RestAuthenticatedEndpoints(_Requests): return serializers._Notification[DepositAddress](serializer=serializers.DepositAddress).parse(*self._POST("auth/w/deposit/address", data=data)) - def get_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, str]) -> Invoice: + def get_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, float, str]) -> Invoice: data = { "wallet": wallet, "currency": currency, "amount": amount diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 2387bea..f9dc96d 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -3,10 +3,8 @@ from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any from dataclasses import dataclass from .. labeler import _Type - from .. notification import Notification - -JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] +from .. utils.encoder import JSON #region Type hinting for Rest Public Endpoints diff --git a/bfxapi/utils/encoder.py b/bfxapi/utils/encoder.py index 3649823..885ab91 100644 --- a/bfxapi/utils/encoder.py +++ b/bfxapi/utils/encoder.py @@ -2,8 +2,30 @@ import json from decimal import Decimal from datetime import datetime +from typing import Type, List, Dict, Union, Any + +JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] + class JSONEncoder(json.JSONEncoder): - def default(self, obj): - if isinstance(obj, Decimal) or isinstance(obj, datetime): + def encode(self, obj: JSON) -> str: + def _convert_float_to_str(data: JSON) -> JSON: + if isinstance(data, float): + return format(Decimal(repr(data)), "f") + elif isinstance(data, list): + return [ _convert_float_to_str(sub_data) for sub_data in data ] + elif isinstance(data, dict): + return { key: _convert_float_to_str(value) for key, value in data.items() } + else: return data + + data = _convert_float_to_str(obj) + + return json.JSONEncoder.encode(self, data) + + def default(self, obj: Any) -> Any: + if isinstance(obj, Decimal): + return format(obj, "f") + + if isinstance(obj, datetime): return str(obj) + return json.JSONEncoder.default(self, obj) \ No newline at end of file diff --git a/bfxapi/websocket/_BfxWebsocketInputs.py b/bfxapi/websocket/_BfxWebsocketInputs.py index 85e29b0..041405f 100644 --- a/bfxapi/websocket/_BfxWebsocketInputs.py +++ b/bfxapi/websocket/_BfxWebsocketInputs.py @@ -12,9 +12,9 @@ class _BfxWebsocketInputs(object): def __init__(self, __handle_websocket_input): self.__handle_websocket_input = __handle_websocket_input - async def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str], - price: Optional[Union[Decimal, str]] = None, lev: Optional[int] = None, - price_trailing: Optional[Union[Decimal, str]] = None, price_aux_limit: Optional[Union[Decimal, str]] = None, price_oco_stop: Optional[Union[Decimal, str]] = None, + async def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, float, str], + price: Optional[Union[Decimal, float, str]] = None, lev: Optional[int] = None, + price_trailing: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_oco_stop: Optional[Union[Decimal, float, str]] = None, gid: Optional[int] = None, cid: Optional[int] = None, flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None): data = _strip({ @@ -27,10 +27,10 @@ class _BfxWebsocketInputs(object): await self.__handle_websocket_input("on", data) - async def update_order(self, id: int, amount: Optional[Union[Decimal, str]] = None, price: Optional[Union[Decimal, str]] = None, + async def update_order(self, id: int, amount: Optional[Union[Decimal, float, str]] = None, price: Optional[Union[Decimal, float, str]] = None, cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, - flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, str]] = None, - price_aux_limit: Optional[Union[Decimal, str]] = None, price_trailing: Optional[Union[Decimal, str]] = None, tif: Optional[Union[datetime, str]] = None): + flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, float, str]] = None, + price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, tif: Optional[Union[datetime, str]] = None): data = _strip({ "id": id, "amount": amount, "price": price, "cid": cid, "cid_date": cid_date, "gid": gid, @@ -60,8 +60,8 @@ class _BfxWebsocketInputs(object): await self.__handle_websocket_input("oc_multi", data) - async def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, str], - rate: Union[Decimal, str], period: int, + async def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, float, str], + rate: Union[Decimal, float, str], period: int, flags: Optional[int] = 0): data = { "type": type, "symbol": symbol, "amount": amount, diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py index 97ef37b..19ed3ef 100644 --- a/bfxapi/websocket/types.py +++ b/bfxapi/websocket/types.py @@ -3,10 +3,8 @@ from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any from dataclasses import dataclass from ..labeler import _Type - from ..notification import Notification - -JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] +from .. utils.encoder import JSON #region Type hinting for Websocket Public Channels From 0278825d358f72078c696a4da8a88ff6068a573c Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 25 Jan 2023 18:25:47 +0100 Subject: [PATCH 22/85] add endpoints, minor fixs, use format instead of str Co-authored-by: itsdeka --- bfxapi/rest/BfxRestInterface.py | 3 +++ bfxapi/rest/serializers.py | 28 ++++++++++++++++++++++++--- bfxapi/rest/types.py | 16 +++++++++++++++ examples/rest/claim_position.py | 19 ++++++++++++++++++ examples/rest/create_funding_offer.py | 2 ++ examples/rest/create_order.py | 2 ++ 6 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 examples/rest/claim_position.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 88a08f5..eac75ca 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -496,3 +496,6 @@ class _RestAuthenticatedEndpoints(_Requests): def get_base_margin_info(self) -> BaseMarginInfo: return serializers.BaseMarginInfo.parse(*(self._POST(f"auth/r/info/margin/base")[1])) + + def claim_position(self, id: int, amount: Optional[Union[Decimal, float, str]] = None) -> Notification[Claim]: + return serializers._Notification[Claim](serializer=serializers.Claim).parse(*self._POST("auth/w/position/claim", data={ "id": id, "amount": amount })) \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 044eb51..ec09995 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -235,7 +235,6 @@ TradingMarketAveragePrice = generate_labeler_serializer("TradingMarketAveragePri "AMOUNT" ]) - FundingMarketAveragePrice = generate_labeler_serializer("FundingMarketAveragePrice", klass=types.FundingMarketAveragePrice, labels=[ "RATE_AVG", "AMOUNT" @@ -299,8 +298,8 @@ Position = generate_labeler_serializer("Position", klass=types.Position, labels= "STATUS", "AMOUNT", "BASE_PRICE", - "FUNDING", - "FUNDING_TYPE", + "MARGIN_FUNDING", + "MARGIN_FUNDING_TYPE", "PL", "PL_PERC", "PRICE_LIQ", @@ -489,4 +488,27 @@ BaseMarginInfo = generate_labeler_serializer("BaseMarginInfo", klass=types.BaseM "MARGIN_MIN" ]) +Claim = generate_labeler_serializer("Claim", klass=types.Claim, labels=[ + "SYMBOL", + "POSITION_STATUS", + "AMOUNT", + "BASE_PRICE", + "MARGIN_FUNDING", + "MARGIN_FUNDING_TYPE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "POSITION_ID", + "MTS_CREATE", + "MTS_UPDATE", + "_PLACEHOLDER", + "POS_TYPE", + "_PLACEHOLDER", + "COLLATERAL", + "MIN_COLLATERAL", + "META" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index f9dc96d..720874e 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -388,4 +388,20 @@ class BaseMarginInfo(_Type): MARGIN_NET: float MARGIN_MIN: float +@dataclass +class Claim(_Type): + SYMBOL: str + POSITION_STATUS: str + AMOUNT: float + BASE_PRICE: float + MARGIN_FUNDING: float + MARGIN_FUNDING_TYPE: int + POSITION_ID: int + MTS_CREATE: int + MTS_UPDATE: int + POS_TYPE: int + COLLATERAL: str + MIN_COLLATERAL: str + META: JSON + #endregion \ No newline at end of file diff --git a/examples/rest/claim_position.py b/examples/rest/claim_position.py new file mode 100644 index 0000000..de409a3 --- /dev/null +++ b/examples/rest/claim_position.py @@ -0,0 +1,19 @@ +# python -c "from examples.rest.claim_position import *" + +import os + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +open_margin_positions = bfx.rest.auth.get_positions() + +# claim all positions +for position in open_margin_positions: + print(f"Position {position}") + claim = bfx.rest.auth.claim_position(position.POSITION_ID, amount=0.000001) + print(f"Claim {claim.NOTIFY_INFO}") \ No newline at end of file diff --git a/examples/rest/create_funding_offer.py b/examples/rest/create_funding_offer.py index 2be1e5a..e41e3b3 100644 --- a/examples/rest/create_funding_offer.py +++ b/examples/rest/create_funding_offer.py @@ -1,3 +1,5 @@ +# python -c "from examples.rest.create_funding_offer import *" + import os from bfxapi.client import Client, Constants diff --git a/examples/rest/create_order.py b/examples/rest/create_order.py index 71a99ca..c4286d2 100644 --- a/examples/rest/create_order.py +++ b/examples/rest/create_order.py @@ -1,3 +1,5 @@ +# python -c "from examples.rest.create_order import *" + import os from bfxapi.client import Client, Constants From 05827ff5d16f6828735839ab9fc2fc9c64bc3226 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 25 Jan 2023 18:33:27 +0100 Subject: [PATCH 23/85] Change comment line in all examples/folder demos. --- examples/rest/claim_position.py | 2 +- examples/rest/create_funding_offer.py | 2 +- examples/rest/create_order.py | 2 +- examples/rest/extra_calcs.py | 2 +- examples/rest/get_authenticated_data.py | 2 +- examples/rest/get_candles_hist.py | 2 +- examples/rest/get_liquidations.py | 2 +- examples/rest/get_public_data.py | 2 +- examples/rest/get_pulse_data.py | 2 +- examples/rest/transfer_wallet.py | 2 +- examples/websocket/create_order.py | 2 +- examples/websocket/order_book.py | 2 +- examples/websocket/raw_order_book.py | 2 +- examples/websocket/ticker.py | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/rest/claim_position.py b/examples/rest/claim_position.py index de409a3..de3b5f2 100644 --- a/examples/rest/claim_position.py +++ b/examples/rest/claim_position.py @@ -1,4 +1,4 @@ -# python -c "from examples.rest.claim_position import *" +# python -c "import examples.rest.claim_position" import os diff --git a/examples/rest/create_funding_offer.py b/examples/rest/create_funding_offer.py index e41e3b3..4046e28 100644 --- a/examples/rest/create_funding_offer.py +++ b/examples/rest/create_funding_offer.py @@ -1,4 +1,4 @@ -# python -c "from examples.rest.create_funding_offer import *" +# python -c "import examples.rest.create_funding_offer" import os diff --git a/examples/rest/create_order.py b/examples/rest/create_order.py index c4286d2..1f63e46 100644 --- a/examples/rest/create_order.py +++ b/examples/rest/create_order.py @@ -1,4 +1,4 @@ -# python -c "from examples.rest.create_order import *" +# python -c "import examples.rest.create_order" import os diff --git a/examples/rest/extra_calcs.py b/examples/rest/extra_calcs.py index 2603cd8..0e6fb1e 100644 --- a/examples/rest/extra_calcs.py +++ b/examples/rest/extra_calcs.py @@ -1,4 +1,4 @@ -# python -c "from examples.rest.extra_calcs import *" +# python -c "import examples.rest.extra_calcs" from bfxapi.client import Client, Constants diff --git a/examples/rest/get_authenticated_data.py b/examples/rest/get_authenticated_data.py index 79a24d9..4af5165 100644 --- a/examples/rest/get_authenticated_data.py +++ b/examples/rest/get_authenticated_data.py @@ -1,4 +1,4 @@ -# python -c "from examples.rest.get_authenticated_data import *" +# python -c "import examples.rest.get_authenticated_data" import os import time diff --git a/examples/rest/get_candles_hist.py b/examples/rest/get_candles_hist.py index fde6212..98f9da7 100644 --- a/examples/rest/get_candles_hist.py +++ b/examples/rest/get_candles_hist.py @@ -1,4 +1,4 @@ -# python -c "from examples.rest.get_candles_hist import *" +# python -c "import examples.rest.get_candles_hist" from bfxapi.client import Client, Constants diff --git a/examples/rest/get_liquidations.py b/examples/rest/get_liquidations.py index 34ed47d..6113a25 100644 --- a/examples/rest/get_liquidations.py +++ b/examples/rest/get_liquidations.py @@ -1,4 +1,4 @@ -# python -c "from examples.rest.get_liquidations import *" +# python -c "import examples.rest.get_liquidations" import time diff --git a/examples/rest/get_public_data.py b/examples/rest/get_public_data.py index ff86c14..a6c388b 100644 --- a/examples/rest/get_public_data.py +++ b/examples/rest/get_public_data.py @@ -1,4 +1,4 @@ -# python -c "from examples.rest.get_public_data import *" +# python -c "import examples.rest.get_public_data" import time diff --git a/examples/rest/get_pulse_data.py b/examples/rest/get_pulse_data.py index fc5c15f..9fb8832 100644 --- a/examples/rest/get_pulse_data.py +++ b/examples/rest/get_pulse_data.py @@ -1,4 +1,4 @@ -# python -c "from examples.rest.get_pulse_data import *" +# python -c "import examples.rest.get_pulse_data" import time diff --git a/examples/rest/transfer_wallet.py b/examples/rest/transfer_wallet.py index 28dcccb..b1d9fd3 100644 --- a/examples/rest/transfer_wallet.py +++ b/examples/rest/transfer_wallet.py @@ -1,4 +1,4 @@ -# python -c "from examples.rest.transfer_wallet import *" +# python -c "import examples.rest.transfer_wallet" import os diff --git a/examples/websocket/create_order.py b/examples/websocket/create_order.py index 7a6f645..f72f9d4 100644 --- a/examples/websocket/create_order.py +++ b/examples/websocket/create_order.py @@ -1,4 +1,4 @@ -# python -c "from examples.websocket.create_order import *" +# python -c "import examples.websocket.create_order" import os diff --git a/examples/websocket/order_book.py b/examples/websocket/order_book.py index 57d0c0a..8774b8a 100644 --- a/examples/websocket/order_book.py +++ b/examples/websocket/order_book.py @@ -1,4 +1,4 @@ -# python -c "from examples.websocket.order_book import *" +# python -c "import examples.websocket.order_book" from collections import OrderedDict diff --git a/examples/websocket/raw_order_book.py b/examples/websocket/raw_order_book.py index e9076cc..172873e 100644 --- a/examples/websocket/raw_order_book.py +++ b/examples/websocket/raw_order_book.py @@ -1,4 +1,4 @@ -# python -c "from examples.websocket.raw_order_book import *" +# python -c "import examples.websocket.raw_order_book" from collections import OrderedDict diff --git a/examples/websocket/ticker.py b/examples/websocket/ticker.py index aeb21cc..1c081b2 100644 --- a/examples/websocket/ticker.py +++ b/examples/websocket/ticker.py @@ -1,4 +1,4 @@ -# python -c "from examples.websocket.ticker import *" +# python -c "import examples.websocket.ticker" from bfxapi import Client, Constants From 374f65d660a95b4ceded115c2e5ea05ad5615cc9 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Wed, 25 Jan 2023 20:14:00 +0100 Subject: [PATCH 24/85] increase position --- bfxapi/rest/BfxRestInterface.py | 12 +++++++++++- bfxapi/rest/serializers.py | 26 ++++++++++++++++++++++++++ bfxapi/rest/types.py | 21 +++++++++++++++++++++ examples/rest/increase_position.py | 18 ++++++++++++++++++ 4 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 examples/rest/increase_position.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index eac75ca..697784c 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -498,4 +498,14 @@ class _RestAuthenticatedEndpoints(_Requests): return serializers.BaseMarginInfo.parse(*(self._POST(f"auth/r/info/margin/base")[1])) def claim_position(self, id: int, amount: Optional[Union[Decimal, float, str]] = None) -> Notification[Claim]: - return serializers._Notification[Claim](serializer=serializers.Claim).parse(*self._POST("auth/w/position/claim", data={ "id": id, "amount": amount })) \ No newline at end of file + return serializers._Notification[Claim](serializer=serializers.Claim).parse(*self._POST("auth/w/position/claim", data={ "id": id, "amount": amount })) + + def get_increase_position_info(self, symbol: str, amount: Union[Decimal, float, str]) -> IncreaseInfo: + data = self._POST(f"auth/r/position/increase/info", data={ "symbol": symbol, "amount": amount }) + + return serializers.IncreaseInfo.parse(*( + data[0] + [data[1][0]] + data[1][1] + [data[1][2]] + data[4] + data[5] + )) + + def increase_position(self, symbol: str, amount: Union[Decimal, float, str]) -> Notification[Increase]: + return serializers._Notification[Increase](serializer=serializers.Increase).parse(*self._POST("auth/w/position/increase", data={ "symbol": symbol, "amount": amount })) \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index ec09995..cb4bb39 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -511,4 +511,30 @@ Claim = generate_labeler_serializer("Claim", klass=types.Claim, labels=[ "META" ]) +IncreaseInfo = generate_labeler_serializer("IncreaseInfo", klass=types.IncreaseInfo, labels=[ + "MAX_POS", + "CURRENT_POS", + "BASE_CURRENCY_BALANCE", + "TRADABLE_BALANCE_QUOTE_CURRENCY", + "TRADABLE_BALANCE_QUOTE_TOTAL", + "TRADABLE_BALANCE_BASE_CURRENCY", + "TRADABLE_BALANCE_BASE_TOTAL", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "FUNDING_AVAIL", + "FUNDING_VALUE", + "FUNDING_REQUIRED", + "FUNDING_VALUE_CURRENCY", + "FUNDING_REQUIRED_CURRENCY" +]) + +Increase = generate_labeler_serializer("Increase", klass=types.Increase, labels=[ + "SYMBOL", + "_PLACEHOLDER", + "AMOUNT", + "BASE_PRICE" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 720874e..b2c6d79 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -404,4 +404,25 @@ class Claim(_Type): MIN_COLLATERAL: str META: JSON +@dataclass +class IncreaseInfo(_Type): + MAX_POS: int + CURRENT_POS: float + BASE_CURRENCY_BALANCE: float + TRADABLE_BALANCE_QUOTE_CURRENCY: float + TRADABLE_BALANCE_QUOTE_TOTAL: float + TRADABLE_BALANCE_BASE_CURRENCY: float + TRADABLE_BALANCE_BASE_TOTAL: float + FUNDING_AVAIL: float + FUNDING_VALUE: float + FUNDING_REQUIRED: float + FUNDING_VALUE_CURRENCY: str + FUNDING_REQUIRED_CURRENCY: str + +@dataclass +class Increase(_Type): + SYMBOL: str + AMOUNT: float + BASE_PRICE: float + #endregion \ No newline at end of file diff --git a/examples/rest/increase_position.py b/examples/rest/increase_position.py new file mode 100644 index 0000000..440f9c8 --- /dev/null +++ b/examples/rest/increase_position.py @@ -0,0 +1,18 @@ +# python -c "import examples.rest.increase_position" + +import os + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +increase_info = bfx.rest.auth.get_increase_position_info(symbol="tBTCUSD", amount=0.0001) +print(increase_info) + +# increase a margin position +notification = bfx.rest.auth.increase_position(symbol="tBTCUSD", amount=0.0001) +print(notification.NOTIFY_INFO) From 3565811ec92b60ffdc32c09c9427e38bc885ecab Mon Sep 17 00:00:00 2001 From: itsdeka Date: Wed, 25 Jan 2023 20:46:55 +0100 Subject: [PATCH 25/85] position history --- bfxapi/rest/BfxRestInterface.py | 15 ++++++++------- bfxapi/rest/serializers.py | 17 +++++++++++++++++ bfxapi/rest/types.py | 12 ++++++++++++ examples/rest/get_positions_history.py | 17 +++++++++++++++++ 4 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 examples/rest/get_positions_history.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 697784c..84ca7f2 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -45,8 +45,6 @@ class _Requests(object): def _GET(self, endpoint, params = None): response = requests.get(f"{self.host}/{endpoint}", params=params) - - print(f"{self.host}/{endpoint}") if response.status_code == HTTPStatus.NOT_FOUND: raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") @@ -487,9 +485,9 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo: - data = self._POST(f"auth/r/info/margin/{symbol}") + response = self._POST(f"auth/r/info/margin/{symbol}") - return serializers.SymbolMarginInfo.parse(*([data[1]] + data[2])) + return serializers.SymbolMarginInfo.parse(*([response[1]] + response[2])) def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]: return [ serializers.SymbolMarginInfo.parse(*([sub_data[1]] + sub_data[2])) for sub_data in self._POST(f"auth/r/info/margin/sym_all") ] @@ -501,11 +499,14 @@ class _RestAuthenticatedEndpoints(_Requests): return serializers._Notification[Claim](serializer=serializers.Claim).parse(*self._POST("auth/w/position/claim", data={ "id": id, "amount": amount })) def get_increase_position_info(self, symbol: str, amount: Union[Decimal, float, str]) -> IncreaseInfo: - data = self._POST(f"auth/r/position/increase/info", data={ "symbol": symbol, "amount": amount }) + response = self._POST(f"auth/r/position/increase/info", data={ "symbol": symbol, "amount": amount }) return serializers.IncreaseInfo.parse(*( - data[0] + [data[1][0]] + data[1][1] + [data[1][2]] + data[4] + data[5] + response[0] + [response[1][0]] + response[1][1] + [response[1][2]] + response[4] + response[5] )) def increase_position(self, symbol: str, amount: Union[Decimal, float, str]) -> Notification[Increase]: - return serializers._Notification[Increase](serializer=serializers.Increase).parse(*self._POST("auth/w/position/increase", data={ "symbol": symbol, "amount": amount })) \ No newline at end of file + return serializers._Notification[Increase](serializer=serializers.Increase).parse(*self._POST("auth/w/position/increase", data={ "symbol": symbol, "amount": amount })) + + def get_position_history(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionHistory]: + return [ serializers.PositionHistory.parse(*sub_data) for sub_data in self._POST("auth/r/positions/hist", data={ "start": start, "end": end, "limit": limit }) ] diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index cb4bb39..57ab4eb 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -537,4 +537,21 @@ Increase = generate_labeler_serializer("Increase", klass=types.Increase, labels= "BASE_PRICE" ]) +PositionHistory = generate_labeler_serializer("PositionHistory", klass=types.PositionHistory, labels=[ + "SYMBOL", + "STATUS", + "AMOUNT", + "BASE_PRICE", + "FUNDING", + "FUNDING_TYPE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "POSITION_ID", + "MTS_CREATE", + "MTS_UPDATE" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index b2c6d79..6f27c0c 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -425,4 +425,16 @@ class Increase(_Type): AMOUNT: float BASE_PRICE: float +@dataclass +class PositionHistory(_Type): + SYMBOL: str + STATUS: str + AMOUNT: float + BASE_PRICE: float + FUNDING: float + FUNDING_TYPE: int + POSITION_ID: int + MTS_CREATE: int + MTS_UPDATE: int + #endregion \ No newline at end of file diff --git a/examples/rest/get_positions_history.py b/examples/rest/get_positions_history.py new file mode 100644 index 0000000..87a4b24 --- /dev/null +++ b/examples/rest/get_positions_history.py @@ -0,0 +1,17 @@ +# python -c "import examples.rest.get_positions_history" + +import os +import time + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +now = int(round(time.time() * 1000)) + +positions_history = bfx.rest.auth.get_position_history(end=now, limit=50) +print(positions_history) From 4fbe1b89c68f6a4c6ab16de92728c0c1aca0e560 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Thu, 26 Jan 2023 15:25:00 +0100 Subject: [PATCH 26/85] positions endpoint --- bfxapi/rest/BfxRestInterface.py | 8 +++++- bfxapi/rest/serializers.py | 40 ++++++++++++++++++++++++++ bfxapi/rest/types.py | 28 ++++++++++++++++++ examples/rest/get_positions.py | 23 +++++++++++++++ examples/rest/get_positions_history.py | 17 ----------- 5 files changed, 98 insertions(+), 18 deletions(-) create mode 100644 examples/rest/get_positions.py delete mode 100644 examples/rest/get_positions_history.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 84ca7f2..b1293e7 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -508,5 +508,11 @@ class _RestAuthenticatedEndpoints(_Requests): def increase_position(self, symbol: str, amount: Union[Decimal, float, str]) -> Notification[Increase]: return serializers._Notification[Increase](serializer=serializers.Increase).parse(*self._POST("auth/w/position/increase", data={ "symbol": symbol, "amount": amount })) - def get_position_history(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionHistory]: + def get_positions_history(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionHistory]: return [ serializers.PositionHistory.parse(*sub_data) for sub_data in self._POST("auth/r/positions/hist", data={ "start": start, "end": end, "limit": limit }) ] + + def get_positions_snapshot(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionSnapshot]: + return [ serializers.PositionSnapshot.parse(*sub_data) for sub_data in self._POST("auth/r/positions/snap", data={ "start": start, "end": end, "limit": limit }) ] + + def get_positions_audit(self, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionAudit]: + return [ serializers.PositionAudit.parse(*sub_data) for sub_data in self._POST("auth/r/positions/audit", data={ "ids": ids, "start": start, "end": end, "limit": limit }) ] diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 57ab4eb..95c7f63 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -554,4 +554,44 @@ PositionHistory = generate_labeler_serializer("PositionHistory", klass=types.Pos "MTS_UPDATE" ]) +PositionSnapshot = generate_labeler_serializer("PositionSnapshot", klass=types.PositionSnapshot, labels=[ + "SYMBOL", + "STATUS", + "AMOUNT", + "BASE_PRICE", + "FUNDING", + "FUNDING_TYPE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "POSITION_ID", + "MTS_CREATE", + "MTS_UPDATE" +]) + +PositionAudit = generate_labeler_serializer("PositionAudit", klass=types.PositionAudit, labels=[ + "SYMBOL", + "STATUS", + "AMOUNT", + "BASE_PRICE", + "FUNDING", + "FUNDING_TYPE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "POSITION_ID", + "MTS_CREATE", + "MTS_UPDATE", + "_PLACEHOLDER", + "TYPE", + "_PLACEHOLDER", + "COLLATERAL", + "COLLATERAL_MIN", + "META" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 6f27c0c..0c47c53 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -437,4 +437,32 @@ class PositionHistory(_Type): MTS_CREATE: int MTS_UPDATE: int +@dataclass +class PositionSnapshot(_Type): + SYMBOL: str + STATUS: str + AMOUNT: float + BASE_PRICE: float + FUNDING: float + FUNDING_TYPE: int + POSITION_ID: int + MTS_CREATE: int + MTS_UPDATE: int + +@dataclass +class PositionAudit(_Type): + SYMBOL: str + STATUS: str + AMOUNT: float + BASE_PRICE: float + FUNDING: float + FUNDING_TYPE: int + POSITION_ID: int + MTS_CREATE: int + MTS_UPDATE: int + TYPE: int + COLLATERAL: float + COLLATERAL_MIN: float + META: JSON + #endregion \ No newline at end of file diff --git a/examples/rest/get_positions.py b/examples/rest/get_positions.py new file mode 100644 index 0000000..5b6cfca --- /dev/null +++ b/examples/rest/get_positions.py @@ -0,0 +1,23 @@ +# python -c "import examples.rest.get_positions_snapshot" + +import os +import time + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +now = int(round(time.time() * 1000)) + +positions_snapshot = bfx.rest.auth.get_positions_snapshot(end=now, limit=50) +print(positions_snapshot) + +positions_history = bfx.rest.auth.get_positions_history(end=now, limit=50) +print(positions_history) + +positions_audit = bfx.rest.auth.get_positions_audit(end=now, limit=50) +print(positions_audit) \ No newline at end of file diff --git a/examples/rest/get_positions_history.py b/examples/rest/get_positions_history.py deleted file mode 100644 index 87a4b24..0000000 --- a/examples/rest/get_positions_history.py +++ /dev/null @@ -1,17 +0,0 @@ -# python -c "import examples.rest.get_positions_history" - -import os -import time - -from bfxapi.client import Client, Constants - -bfx = Client( - REST_HOST=Constants.REST_HOST, - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET") -) - -now = int(round(time.time() * 1000)) - -positions_history = bfx.rest.auth.get_position_history(end=now, limit=50) -print(positions_history) From ca91588067472bc75088ab40fd5ec787de37ec8f Mon Sep 17 00:00:00 2001 From: itsdeka Date: Thu, 26 Jan 2023 15:36:00 +0100 Subject: [PATCH 27/85] cancel all funding offers --- bfxapi/rest/BfxRestInterface.py | 3 +++ examples/rest/create_funding_offer.py | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index b1293e7..1853388 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -407,6 +407,9 @@ class _RestAuthenticatedEndpoints(_Requests): def cancel_funding_offer(self, id: int) -> Notification[FundingOffer]: return serializers._Notification[FundingOffer](serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/cancel", data={ "id": id })) + def cancel_all_funding_offers(self, currency: str) -> Notification: + return serializers._Notification().parse(*self._POST("auth/w/funding/offer/cancel/all", data={ "currency": currency })) + def get_funding_offers_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingOffer]: if symbol == None: endpoint = "auth/r/funding/offers/hist" diff --git a/examples/rest/create_funding_offer.py b/examples/rest/create_funding_offer.py index 4046e28..343fa1d 100644 --- a/examples/rest/create_funding_offer.py +++ b/examples/rest/create_funding_offer.py @@ -25,4 +25,9 @@ print("Offer notification:", notification) offers = bfx.rest.auth.get_active_funding_offers(symbol="fUSD") -print("Offers:", offers) \ No newline at end of file +print("Offers:", offers) + +# Cancel all funding offers +notification = bfx.rest.auth.cancel_all_funding_offers(currency="fUSD") + +print(notification) \ No newline at end of file From 48433fbb01085b772f696c15cfa75f62801bcfed Mon Sep 17 00:00:00 2001 From: itsdeka Date: Thu, 26 Jan 2023 16:44:32 +0100 Subject: [PATCH 28/85] derivatives --- bfxapi/rest/BfxRestInterface.py | 6 ++++++ bfxapi/rest/serializers.py | 9 +++++++++ bfxapi/rest/types.py | 9 +++++++++ examples/rest/derivatives.py | 31 +++++++++++++++++++++++++++++++ examples/rest/get_positions.py | 2 +- 5 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 examples/rest/derivatives.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 1853388..bf4e1cf 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -519,3 +519,9 @@ class _RestAuthenticatedEndpoints(_Requests): def get_positions_audit(self, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionAudit]: return [ serializers.PositionAudit.parse(*sub_data) for sub_data in self._POST("auth/r/positions/audit", data={ "ids": ids, "start": start, "end": end, "limit": limit }) ] + + def set_derivative_position_collateral(self, symbol: str, collateral: Union[Decimal, float, str]) -> DerivativePositionCollateral: + return serializers.DerivativePositionCollateral.parse(*(self._POST("auth/w/deriv/collateral/set", data={ "symbol": symbol, "collateral": collateral })[0])) + + def get_derivative_position_collateral_limits(self, symbol: str) -> DerivativePositionCollateralLimits: + return serializers.DerivativePositionCollateralLimits.parse(*self._POST("auth/calc/deriv/collateral/limits", data={ "symbol": symbol })) diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 95c7f63..32d2d44 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -594,4 +594,13 @@ PositionAudit = generate_labeler_serializer("PositionAudit", klass=types.Positio "META" ]) +DerivativePositionCollateral = generate_labeler_serializer("DerivativePositionCollateral", klass=types.DerivativePositionCollateral, labels=[ + "STATUS" +]) + +DerivativePositionCollateralLimits = generate_labeler_serializer("DerivativePositionCollateralLimits", klass=types.DerivativePositionCollateralLimits, labels=[ + "MIN_COLLATERAL", + "MAX_COLLATERAL" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 0c47c53..4f2647f 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -465,4 +465,13 @@ class PositionAudit(_Type): COLLATERAL_MIN: float META: JSON +@dataclass +class DerivativePositionCollateral(_Type): + STATUS: int + +@dataclass +class DerivativePositionCollateralLimits(_Type): + MIN_COLLATERAL: float + MAX_COLLATERAL: float + #endregion \ No newline at end of file diff --git a/examples/rest/derivatives.py b/examples/rest/derivatives.py new file mode 100644 index 0000000..89865a5 --- /dev/null +++ b/examples/rest/derivatives.py @@ -0,0 +1,31 @@ +# python -c "import examples.rest.derivatives" + +import os + +from bfxapi.client import Client, Constants + +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( + symbol="tBTCF0:USTF0", + amount="0.015", + price="16700", + lev=10, + type="LIMIT" +) + +print("Submit Order Notification:", submitted_order) + +# Get position collateral limits +limits = bfx.rest.auth.get_derivative_position_collateral_limits(symbol="tBTCF0:USTF0") +print(f"Limits {limits}") + +# Update position collateral +response = bfx.rest.auth.set_derivative_position_collateral(symbol="tBTCF0:USTF0", collateral=50) +print(response.STATUS) + diff --git a/examples/rest/get_positions.py b/examples/rest/get_positions.py index 5b6cfca..7e71824 100644 --- a/examples/rest/get_positions.py +++ b/examples/rest/get_positions.py @@ -1,4 +1,4 @@ -# python -c "import examples.rest.get_positions_snapshot" +# python -c "import examples.rest.get_positions" import os import time From d767e5dcfe8e805b74b9eb9a9d08a47e80309608 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 26 Jan 2023 18:59:01 +0100 Subject: [PATCH 29/85] Replace every snake case (uppercase) to snake case (lowercase) in serializers, types and notification. --- bfxapi/notification.py | 22 +- bfxapi/rest/serializers.py | 662 ++++++++++++++++---------------- bfxapi/rest/types.py | 658 +++++++++++++++---------------- bfxapi/websocket/serializers.py | 370 +++++++++--------- bfxapi/websocket/types.py | 368 +++++++++--------- 5 files changed, 1040 insertions(+), 1040 deletions(-) diff --git a/bfxapi/notification.py b/bfxapi/notification.py index b2f90b8..f4301b3 100644 --- a/bfxapi/notification.py +++ b/bfxapi/notification.py @@ -8,16 +8,16 @@ T = TypeVar("T") @dataclass class Notification(_Type, Generic[T]): - MTS: int - TYPE: str - MESSAGE_ID: Optional[int] - NOTIFY_INFO: T - CODE: Optional[int] - STATUS: str - TEXT: str + mts: int + type: str + message_id: Optional[int] + notify_info: T + code: Optional[int] + status: str + text: str class _Notification(_Serializer, Generic[T]): - __LABELS = [ "MTS", "TYPE", "MESSAGE_ID", "_PLACEHOLDER", "NOTIFY_INFO", "CODE", "STATUS", "TEXT" ] + __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, _Notification.__LABELS, IGNORE = [ "_PLACEHOLDER" ]) @@ -28,13 +28,13 @@ class _Notification(_Serializer, Generic[T]): notification = cast(Notification[T], Notification(**dict(self._serialize(*values)))) if isinstance(self.serializer, _Serializer): - NOTIFY_INFO = cast(List[Any], notification.NOTIFY_INFO) + NOTIFY_INFO = cast(List[Any], notification.notify_info) if self.iterate == False: if len(NOTIFY_INFO) == 1 and isinstance(NOTIFY_INFO[0], list): NOTIFY_INFO = NOTIFY_INFO[0] - notification.NOTIFY_INFO = cast(T, self.serializer.klass(**dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip)))) - else: notification.NOTIFY_INFO = cast(T, [ self.serializer.klass(**dict(self.serializer._serialize(*data, skip=skip))) for data in NOTIFY_INFO ]) + notification.notify_info = cast(T, self.serializer.klass(**dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip)))) + else: notification.notify_info = cast(T, [ self.serializer.klass(**dict(self.serializer._serialize(*data, skip=skip))) for data in NOTIFY_INFO ]) return notification \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 32d2d44..a6cf582 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -7,48 +7,48 @@ from .. notification import _Notification #region Serializers definition for Rest Public Endpoints PlatformStatus = generate_labeler_serializer("PlatformStatus", klass=types.PlatformStatus, labels=[ - "STATUS" + "status" ]) TradingPairTicker = generate_labeler_serializer("TradingPairTicker", klass=types.TradingPairTicker, labels=[ - "SYMBOL", - "BID", - "BID_SIZE", - "ASK", - "ASK_SIZE", - "DAILY_CHANGE", - "DAILY_CHANGE_RELATIVE", - "LAST_PRICE", - "VOLUME", - "HIGH", - "LOW" + "symbol", + "bid", + "bid_size", + "ask", + "ask_size", + "daily_change", + "daily_change_relative", + "last_price", + "volume", + "high", + "low" ]) FundingCurrencyTicker = generate_labeler_serializer("FundingCurrencyTicker", klass=types.FundingCurrencyTicker, labels=[ - "SYMBOL", - "FRR", - "BID", - "BID_PERIOD", - "BID_SIZE", - "ASK", - "ASK_PERIOD", - "ASK_SIZE", - "DAILY_CHANGE", - "DAILY_CHANGE_RELATIVE", - "LAST_PRICE", - "VOLUME", - "HIGH", - "LOW", + "symbol", + "frr", + "bid", + "bid_period", + "bid_size", + "ask", + "ask_period", + "ask_size", + "daily_change", + "daily_change_relative", + "last_price", + "volume", + "high", + "low", "_PLACEHOLDER", "_PLACEHOLDER", - "FRR_AMOUNT_AVAILABLE" + "frr_amount_available" ]) TickersHistory = generate_labeler_serializer("TickersHistory", klass=types.TickersHistory, labels=[ - "SYMBOL", - "BID", + "symbol", + "bid", "_PLACEHOLDER", - "ASK", + "ask", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", @@ -57,191 +57,191 @@ TickersHistory = generate_labeler_serializer("TickersHistory", klass=types.Ticke "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "MTS" + "mts" ]) TradingPairTrade = generate_labeler_serializer("TradingPairTrade", klass=types.TradingPairTrade, labels=[ - "ID", - "MTS", - "AMOUNT", - "PRICE" + "id", + "mts", + "amount", + "price" ]) FundingCurrencyTrade = generate_labeler_serializer("FundingCurrencyTrade", klass=types.FundingCurrencyTrade, labels=[ - "ID", - "MTS", - "AMOUNT", - "RATE", - "PERIOD" + "id", + "mts", + "amount", + "rate", + "period" ]) TradingPairBook = generate_labeler_serializer("TradingPairBook", klass=types.TradingPairBook, labels=[ - "PRICE", - "COUNT", - "AMOUNT" + "price", + "count", + "amount" ]) FundingCurrencyBook = generate_labeler_serializer("FundingCurrencyBook", klass=types.FundingCurrencyBook, labels=[ - "RATE", - "PERIOD", - "COUNT", - "AMOUNT" + "rate", + "period", + "count", + "amount" ]) TradingPairRawBook = generate_labeler_serializer("TradingPairRawBook", klass=types.TradingPairRawBook, labels=[ - "ORDER_ID", - "PRICE", - "AMOUNT" + "order_id", + "price", + "amount" ]) FundingCurrencyRawBook = generate_labeler_serializer("FundingCurrencyRawBook", klass=types.FundingCurrencyRawBook, labels=[ - "OFFER_ID", - "PERIOD", - "RATE", - "AMOUNT" + "offer_id", + "period", + "rate", + "amount" ]) Statistic = generate_labeler_serializer("Statistic", klass=types.Statistic, labels=[ - "MTS", - "VALUE" + "mts", + "value" ]) Candle = generate_labeler_serializer("Candle", klass=types.Candle, labels=[ - "MTS", - "OPEN", - "CLOSE", - "HIGH", - "LOW", - "VOLUME" + "mts", + "open", + "close", + "high", + "low", + "volume" ]) DerivativesStatus = generate_labeler_serializer("DerivativesStatus", klass=types.DerivativesStatus, labels=[ - "KEY", - "MTS", + "key", + "mts", "_PLACEHOLDER", - "DERIV_PRICE", - "SPOT_PRICE", + "deriv_price", + "spot_price", "_PLACEHOLDER", - "INSURANCE_FUND_BALANCE", + "insurance_fund_balance", "_PLACEHOLDER", - "NEXT_FUNDING_EVT_TIMESTAMP_MS", - "NEXT_FUNDING_ACCRUED", - "NEXT_FUNDING_STEP", + "next_funding_evt_timestamp_ms", + "next_funding_accrued", + "next_funding_step", "_PLACEHOLDER", - "CURRENT_FUNDING", + "current_funding", "_PLACEHOLDER", "_PLACEHOLDER", - "MARK_PRICE", + "mark_price", "_PLACEHOLDER", "_PLACEHOLDER", - "OPEN_INTEREST", + "open_interest", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "CLAMP_MIN", - "CLAMP_MAX" + "clamp_min", + "clamp_max" ]) Liquidation = generate_labeler_serializer("Liquidation", klass=types.Liquidation, labels=[ "_PLACEHOLDER", - "POS_ID", - "MTS", + "pos_id", + "mts", "_PLACEHOLDER", - "SYMBOL", - "AMOUNT", - "BASE_PRICE", + "symbol", + "amount", + "base_price", "_PLACEHOLDER", - "IS_MATCH", - "IS_MARKET_SOLD", + "is_match", + "is_market_sold", "_PLACEHOLDER", - "PRICE_ACQUIRED" + "price_acquired" ]) Leaderboard = generate_labeler_serializer("Leaderboard", klass=types.Leaderboard, labels=[ - "MTS", + "mts", "_PLACEHOLDER", - "USERNAME", - "RANKING", + "username", + "ranking", "_PLACEHOLDER", "_PLACEHOLDER", - "VALUE", + "value", "_PLACEHOLDER", "_PLACEHOLDER", - "TWITTER_HANDLE" + "twitter_handle" ]) FundingStatistic = generate_labeler_serializer("FundingStatistic", klass=types.FundingStatistic, labels=[ - "TIMESTAMP", + "timestamp", "_PLACEHOLDER", "_PLACEHOLDER", - "FRR", - "AVG_PERIOD", + "frr", + "avg_period", "_PLACEHOLDER", "_PLACEHOLDER", - "FUNDING_AMOUNT", - "FUNDING_AMOUNT_USED", + "funding_amount", + "funding_amount_used", "_PLACEHOLDER", "_PLACEHOLDER", - "FUNDING_BELOW_THRESHOLD" + "funding_below_threshold" ]) PulseProfile = generate_labeler_serializer("PulseProfile", klass=types.PulseProfile, labels=[ - "PUID", - "MTS", + "puid", + "mts", "_PLACEHOLDER", - "NICKNAME", + "nickname", "_PLACEHOLDER", - "PICTURE", - "TEXT", + "picture", + "text", "_PLACEHOLDER", "_PLACEHOLDER", - "TWITTER_HANDLE", + "twitter_handle", "_PLACEHOLDER", - "FOLLOWERS", - "FOLLOWING", + "followers", + "following", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "TIPPING_STATUS" + "tipping_status" ]) -PulseMessage = generate_recursive_serializer("PulseMessage", klass=types.PulseMessage, serializers={ "PROFILE": PulseProfile }, labels=[ - "PID", - "MTS", +PulseMessage = generate_recursive_serializer("PulseMessage", klass=types.PulseMessage, serializers={ "profile": PulseProfile }, labels=[ + "pid", + "mts", "_PLACEHOLDER", - "PUID", + "puid", "_PLACEHOLDER", - "TITLE", - "CONTENT", + "title", + "content", "_PLACEHOLDER", "_PLACEHOLDER", - "IS_PIN", - "IS_PUBLIC", - "COMMENTS_DISABLED", - "TAGS", - "ATTACHMENTS", - "META", - "LIKES", + "is_pin", + "is_public", + "comments_disabled", + "tags", + "attachments", + "meta", + "likes", "_PLACEHOLDER", "_PLACEHOLDER", - "PROFILE", - "COMMENTS", + "profile", + "comments", "_PLACEHOLDER", "_PLACEHOLDER" ]) TradingMarketAveragePrice = generate_labeler_serializer("TradingMarketAveragePrice", klass=types.TradingMarketAveragePrice, labels=[ - "PRICE_AVG", - "AMOUNT" + "price_avg", + "amount" ]) FundingMarketAveragePrice = generate_labeler_serializer("FundingMarketAveragePrice", klass=types.FundingMarketAveragePrice, labels=[ - "RATE_AVG", - "AMOUNT" + "rate_avg", + "amount" ]) FxRate = generate_labeler_serializer("FxRate", klass=types.FxRate, labels=[ - "CURRENT_RATE" + "current_rate" ]) #endregion @@ -249,358 +249,358 @@ FxRate = generate_labeler_serializer("FxRate", klass=types.FxRate, labels=[ #region Serializers definition for Rest Authenticated Endpoints Wallet = generate_labeler_serializer("Wallet", klass=types.Wallet, labels=[ - "WALLET_TYPE", - "CURRENCY", - "BALANCE", - "UNSETTLED_INTEREST", - "AVAILABLE_BALANCE", - "LAST_CHANGE", - "TRADE_DETAILS" + "wallet_type", + "currency", + "balance", + "unsettled_interest", + "available_balance", + "last_change", + "trade_details" ]) Order = generate_labeler_serializer("Order", klass=types.Order, labels=[ - "ID", - "GID", - "CID", - "SYMBOL", - "MTS_CREATE", - "MTS_UPDATE", - "AMOUNT", - "AMOUNT_ORIG", - "ORDER_TYPE", - "TYPE_PREV", - "MTS_TIF", + "id", + "gid", + "cid", + "symbol", + "mts_create", + "mts_update", + "amount", + "amount_orig", + "order_type", + "type_prev", + "mts_tif", "_PLACEHOLDER", - "FLAGS", - "ORDER_STATUS", + "flags", + "order_status", "_PLACEHOLDER", "_PLACEHOLDER", - "PRICE", - "PRICE_AVG", - "PRICE_TRAILING", - "PRICE_AUX_LIMIT", + "price", + "price_avg", + "price_trailing", + "price_aux_limit", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "NOTIFY", - "HIDDEN", - "PLACED_ID", + "notify", + "hidden", + "placed_id", "_PLACEHOLDER", "_PLACEHOLDER", - "ROUTING", + "routing", "_PLACEHOLDER", "_PLACEHOLDER", - "META" + "meta" ]) Position = generate_labeler_serializer("Position", klass=types.Position, labels=[ - "SYMBOL", - "STATUS", - "AMOUNT", - "BASE_PRICE", - "MARGIN_FUNDING", - "MARGIN_FUNDING_TYPE", - "PL", - "PL_PERC", - "PRICE_LIQ", - "LEVERAGE", + "symbol", + "status", + "amount", + "base_price", + "margin_funding", + "margin_funding_type", + "pl", + "pl_perc", + "price_liq", + "leverage", "_PLACEHOLDER", - "POSITION_ID", - "MTS_CREATE", - "MTS_UPDATE", + "position_id", + "mts_create", + "mts_update", "_PLACEHOLDER", - "TYPE", + "type", "_PLACEHOLDER", - "COLLATERAL", - "COLLATERAL_MIN", - "META" + "collateral", + "collateral_min", + "meta" ]) FundingOffer = generate_labeler_serializer("FundingOffer", klass=types.FundingOffer, labels=[ - "ID", - "SYMBOL", - "MTS_CREATED", - "MTS_UPDATED", - "AMOUNT", - "AMOUNT_ORIG", - "OFFER_TYPE", + "id", + "symbol", + "mts_created", + "mts_updated", + "amount", + "amount_orig", + "offer_type", "_PLACEHOLDER", "_PLACEHOLDER", - "FLAGS", - "OFFER_STATUS", + "flags", + "offer_status", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "RATE", - "PERIOD", - "NOTIFY", - "HIDDEN", + "rate", + "period", + "notify", + "hidden", "_PLACEHOLDER", - "RENEW", + "renew", "_PLACEHOLDER" ]) Trade = generate_labeler_serializer("Trade", klass=types.Trade, labels=[ - "ID", - "PAIR", - "MTS_CREATE", - "ORDER_ID", - "EXEC_AMOUNT", - "EXEC_PRICE", - "ORDER_TYPE", - "ORDER_PRICE", - "MAKER", - "FEE", - "FEE_CURRENCY", - "CID" + "id", + "pair", + "mts_create", + "order_id", + "exec_amount", + "exec_price", + "order_type", + "order_price", + "maker", + "fee", + "fee_currency", + "cid" ]) OrderTrade = generate_labeler_serializer("OrderTrade", klass=types.OrderTrade, labels=[ - "ID", - "PAIR", - "MTS_CREATE", - "ORDER_ID", - "EXEC_AMOUNT", - "EXEC_PRICE", + "id", + "pair", + "mts_create", + "order_id", + "exec_amount", + "exec_price", "_PLACEHOLDER", "_PLACEHOLDER", - "MAKER", - "FEE", - "FEE_CURRENCY", - "CID" + "maker", + "fee", + "fee_currency", + "cid" ]) Ledger = generate_labeler_serializer("Ledger", klass=types.Ledger, labels=[ - "ID", - "CURRENCY", + "id", + "currency", "_PLACEHOLDER", - "MTS", + "mts", "_PLACEHOLDER", - "AMOUNT", - "BALANCE", + "amount", + "balance", "_PLACEHOLDER", - "DESCRIPTION" + "description" ]) FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.FundingCredit, labels=[ - "ID", - "SYMBOL", - "SIDE", - "MTS_CREATE", - "MTS_UPDATE", - "AMOUNT", - "FLAGS", - "STATUS", - "RATE_TYPE", + "id", + "symbol", + "side", + "mts_create", + "mts_update", + "amount", + "flags", + "status", + "rate_type", "_PLACEHOLDER", "_PLACEHOLDER", - "RATE", - "PERIOD", - "MTS_OPENING", - "MTS_LAST_PAYOUT", - "NOTIFY", - "HIDDEN", + "rate", + "period", + "mts_opening", + "mts_last_payout", + "notify", + "hidden", "_PLACEHOLDER", - "RENEW", + "renew", "_PLACEHOLDER", - "NO_CLOSE", - "POSITION_PAIR" + "no_close", + "position_pair" ]) Transfer = generate_labeler_serializer("Transfer", klass=types.Transfer, labels=[ - "MTS", - "WALLET_FROM", - "WALLET_TO", + "mts", + "wallet_from", + "wallet_to", "_PLACEHOLDER", - "CURRENCY", - "CURRENCY_TO", + "currency", + "currency_to", "_PLACEHOLDER", - "AMOUNT" + "amount" ]) Withdrawal = generate_labeler_serializer("Withdrawal", klass=types.Withdrawal, labels=[ - "WITHDRAWAL_ID", + "withdrawal_id", "_PLACEHOLDER", - "METHOD", - "PAYMENT_ID", - "WALLET", - "AMOUNT", + "method", + "payment_id", + "wallet", + "amount", "_PLACEHOLDER", "_PLACEHOLDER", - "WITHDRAWAL_FEE" + "withdrawal_fee" ]) DepositAddress = generate_labeler_serializer("DepositAddress", klass=types.DepositAddress, labels=[ "_PLACEHOLDER", - "METHOD", - "CURRENCY_CODE", + "method", + "currency_code", "_PLACEHOLDER", - "ADDRESS", - "POOL_ADDRESS" + "address", + "pool_address" ]) Invoice = generate_labeler_serializer("Invoice", klass=types.Invoice, labels=[ - "INVOICE_HASH", - "INVOICE", + "invoice_hash", + "invoice", "_PLACEHOLDER", "_PLACEHOLDER", - "AMOUNT" + "amount" ]) Movement = generate_labeler_serializer("Movement", klass=types.Movement, labels=[ - "ID", - "CURRENCY", - "CURRENCY_NAME", + "id", + "currency", + "currency_name", "_PLACEHOLDER", "_PLACEHOLDER", - "MTS_STARTED", - "MTS_UPDATED", + "mts_started", + "mts_updated", "_PLACEHOLDER", "_PLACEHOLDER", - "STATUS", + "status", "_PLACEHOLDER", "_PLACEHOLDER", - "AMOUNT", - "FEES", + "amount", + "fees", "_PLACEHOLDER", "_PLACEHOLDER", - "DESTINATION_ADDRESS", + "destination_address", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "TRANSACTION_ID", - "WITHDRAW_TRANSACTION_NOTE" + "transaction_id", + "withdraw_transaction_note" ]) SymbolMarginInfo = generate_labeler_serializer("SymbolMarginInfo", klass=types.SymbolMarginInfo, labels=[ - "SYMBOL", - "TRADABLE_BALANCE", - "GROSS_BALANCE", - "BUY", - "SELL" + "symbol", + "tradable_balance", + "gross_balance", + "buy", + "sell" ]) BaseMarginInfo = generate_labeler_serializer("BaseMarginInfo", klass=types.BaseMarginInfo, labels=[ - "USER_PL", - "USER_SWAPS", - "MARGIN_BALANCE", - "MARGIN_NET", - "MARGIN_MIN" + "user_pl", + "user_swaps", + "margin_balance", + "margin_net", + "margin_min" ]) Claim = generate_labeler_serializer("Claim", klass=types.Claim, labels=[ - "SYMBOL", - "POSITION_STATUS", - "AMOUNT", - "BASE_PRICE", - "MARGIN_FUNDING", - "MARGIN_FUNDING_TYPE", + "symbol", + "position_status", + "amount", + "base_price", + "margin_funding", + "margin_funding_type", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "POSITION_ID", - "MTS_CREATE", - "MTS_UPDATE", + "position_id", + "mts_create", + "mts_update", "_PLACEHOLDER", - "POS_TYPE", + "pos_type", "_PLACEHOLDER", - "COLLATERAL", - "MIN_COLLATERAL", - "META" + "collateral", + "min_collateral", + "meta" ]) IncreaseInfo = generate_labeler_serializer("IncreaseInfo", klass=types.IncreaseInfo, labels=[ - "MAX_POS", - "CURRENT_POS", - "BASE_CURRENCY_BALANCE", - "TRADABLE_BALANCE_QUOTE_CURRENCY", - "TRADABLE_BALANCE_QUOTE_TOTAL", - "TRADABLE_BALANCE_BASE_CURRENCY", - "TRADABLE_BALANCE_BASE_TOTAL", + "max_pos", + "current_pos", + "base_currency_balance", + "tradable_balance_quote_currency", + "tradable_balance_quote_total", + "tradable_balance_base_currency", + "tradable_balance_base_total", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "FUNDING_AVAIL", - "FUNDING_VALUE", - "FUNDING_REQUIRED", - "FUNDING_VALUE_CURRENCY", - "FUNDING_REQUIRED_CURRENCY" + "funding_avail", + "funding_value", + "funding_required", + "funding_value_currency", + "funding_required_currency" ]) Increase = generate_labeler_serializer("Increase", klass=types.Increase, labels=[ - "SYMBOL", + "symbol", "_PLACEHOLDER", - "AMOUNT", - "BASE_PRICE" + "amount", + "base_price" ]) PositionHistory = generate_labeler_serializer("PositionHistory", klass=types.PositionHistory, labels=[ - "SYMBOL", - "STATUS", - "AMOUNT", - "BASE_PRICE", - "FUNDING", - "FUNDING_TYPE", + "symbol", + "status", + "amount", + "base_price", + "funding", + "funding_type", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "POSITION_ID", - "MTS_CREATE", - "MTS_UPDATE" + "position_id", + "mts_create", + "mts_update" ]) PositionSnapshot = generate_labeler_serializer("PositionSnapshot", klass=types.PositionSnapshot, labels=[ - "SYMBOL", - "STATUS", - "AMOUNT", - "BASE_PRICE", - "FUNDING", - "FUNDING_TYPE", + "symbol", + "status", + "amount", + "base_price", + "funding", + "funding_type", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "POSITION_ID", - "MTS_CREATE", - "MTS_UPDATE" + "position_id", + "mts_create", + "mts_update" ]) PositionAudit = generate_labeler_serializer("PositionAudit", klass=types.PositionAudit, labels=[ - "SYMBOL", - "STATUS", - "AMOUNT", - "BASE_PRICE", - "FUNDING", - "FUNDING_TYPE", + "symbol", + "status", + "amount", + "base_price", + "funding", + "funding_type", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "POSITION_ID", - "MTS_CREATE", - "MTS_UPDATE", + "position_id", + "mts_create", + "mts_update", "_PLACEHOLDER", - "TYPE", + "type", "_PLACEHOLDER", - "COLLATERAL", - "COLLATERAL_MIN", - "META" + "collateral", + "collateral_min", + "meta" ]) DerivativePositionCollateral = generate_labeler_serializer("DerivativePositionCollateral", klass=types.DerivativePositionCollateral, labels=[ - "STATUS" + "status" ]) DerivativePositionCollateralLimits = generate_labeler_serializer("DerivativePositionCollateralLimits", klass=types.DerivativePositionCollateralLimits, labels=[ - "MIN_COLLATERAL", - "MAX_COLLATERAL" + "min_collateral", + "max_collateral" ]) #endregion \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 4f2647f..4850404 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -10,188 +10,188 @@ from .. utils.encoder import JSON @dataclass class PlatformStatus(_Type): - STATUS: int + status: int @dataclass class TradingPairTicker(_Type): - SYMBOL: Optional[str] - BID: float - BID_SIZE: float - ASK: float - ASK_SIZE: float - DAILY_CHANGE: float - DAILY_CHANGE_RELATIVE: float - LAST_PRICE: float - VOLUME: float - HIGH: float - LOW: float + symbol: Optional[str] + bid: float + bid_size: float + ask: float + ask_size: float + daily_change: float + daily_change_relative: float + last_price: float + volume: float + high: float + low: float @dataclass class FundingCurrencyTicker(_Type): - SYMBOL: Optional[str] - FRR: float - BID: float - BID_PERIOD: int - BID_SIZE: float - ASK: float - ASK_PERIOD: int - ASK_SIZE: float - DAILY_CHANGE: float - DAILY_CHANGE_RELATIVE: float - LAST_PRICE: float - VOLUME: float - HIGH: float - LOW: float - FRR_AMOUNT_AVAILABLE: float + symbol: Optional[str] + frr: float + bid: float + bid_period: int + bid_size: float + ask: float + ask_period: int + ask_size: float + daily_change: float + daily_change_relative: float + last_price: float + volume: float + high: float + low: float + frr_amount_available: float @dataclass class TickersHistory(_Type): - SYMBOL: str - BID: float - ASK: float - MTS: int + symbol: str + bid: float + ask: float + mts: int @dataclass class TradingPairTrade(_Type): - ID: int - MTS: int - AMOUNT: float - PRICE: float + id: int + mts: int + amount: float + price: float @dataclass class FundingCurrencyTrade(_Type): - ID: int - MTS: int - AMOUNT: float - RATE: float - PERIOD: int + id: int + mts: int + amount: float + rate: float + period: int @dataclass class TradingPairBook(_Type): - PRICE: float - COUNT: int - AMOUNT: float + price: float + count: int + amount: float @dataclass class FundingCurrencyBook(_Type): - RATE: float - PERIOD: int - COUNT: int - AMOUNT: float + rate: float + period: int + count: int + amount: float @dataclass class TradingPairRawBook(_Type): - ORDER_ID: int - PRICE: float - AMOUNT: float + order_id: int + price: float + amount: float @dataclass class FundingCurrencyRawBook(_Type): - OFFER_ID: int - PERIOD: int - RATE: float - AMOUNT: float + offer_id: int + period: int + rate: float + amount: float @dataclass class Statistic(_Type): - MTS: int - VALUE: float + mts: int + value: float @dataclass class Candle(_Type): - MTS: int - OPEN: float - CLOSE: float - HIGH: float - LOW: float - VOLUME: float + mts: int + open: float + close: float + high: float + low: float + volume: float @dataclass class DerivativesStatus(_Type): - KEY: Optional[str] - MTS: int - DERIV_PRICE: float - SPOT_PRICE: float - INSURANCE_FUND_BALANCE: float - NEXT_FUNDING_EVT_TIMESTAMP_MS: int - NEXT_FUNDING_ACCRUED: float - NEXT_FUNDING_STEP: int - CURRENT_FUNDING: float - MARK_PRICE: float - OPEN_INTEREST: float - CLAMP_MIN: float - CLAMP_MAX: float + key: Optional[str] + mts: int + deriv_price: float + spot_price: float + insurance_fund_balance: float + next_funding_evt_timestamp_ms: int + next_funding_accrued: float + next_funding_step: int + current_funding: float + mark_price: float + open_interest: float + clamp_min: float + clamp_max: float @dataclass class Liquidation(_Type): - POS_ID: int - MTS: int - SYMBOL: str - AMOUNT: float - BASE_PRICE: float - IS_MATCH: int - IS_MARKET_SOLD: int - PRICE_ACQUIRED: float + pos_id: int + mts: int + symbol: str + amount: float + base_price: float + is_match: int + is_market_sold: int + price_acquired: float @dataclass class Leaderboard(_Type): - MTS: int - USERNAME: str - RANKING: int - VALUE: float - TWITTER_HANDLE: Optional[str] + mts: int + username: str + ranking: int + value: float + twitter_handle: Optional[str] @dataclass class FundingStatistic(_Type): - TIMESTAMP: int - FRR: float - AVG_PERIOD: float - FUNDING_AMOUNT: float - FUNDING_AMOUNT_USED: float - FUNDING_BELOW_THRESHOLD: float + timestamp: int + frr: float + avg_period: float + funding_amount: float + funding_amount_used: float + funding_below_threshold: float @dataclass class PulseProfile(_Type): - PUID: str - MTS: int - NICKNAME: str - PICTURE: str - TEXT: str - TWITTER_HANDLE: str - FOLLOWERS: int - FOLLOWING: int - TIPPING_STATUS: int + puid: str + mts: int + nickname: str + picture: str + text: str + twitter_handle: str + followers: int + following: int + tipping_status: int @dataclass class PulseMessage(_Type): - PID: str - MTS: int - PUID: str - TITLE: str - CONTENT: str - IS_PIN: int - IS_PUBLIC: int - COMMENTS_DISABLED: int - TAGS: List[str] - ATTACHMENTS: List[str] - META: List[JSON] - LIKES: int - PROFILE: PulseProfile - COMMENTS: int + pid: str + mts: int + puid: str + title: str + content: str + is_pin: int + is_public: int + comments_disabled: int + tags: List[str] + attachments: List[str] + meta: List[JSON] + likes: int + profile: PulseProfile + comments: int @dataclass class TradingMarketAveragePrice(_Type): - PRICE_AVG: float - AMOUNT: float + price_avg: float + amount: float @dataclass class FundingMarketAveragePrice(_Type): - RATE_AVG: float - AMOUNT: float + rate_avg: float + amount: float @dataclass class FxRate(_Type): - CURRENT_RATE: float + current_rate: float #endregion @@ -199,279 +199,279 @@ class FxRate(_Type): @dataclass class Wallet(_Type): - WALLET_TYPE: str - CURRENCY: str - BALANCE: float - UNSETTLED_INTEREST: float - AVAILABLE_BALANCE: float - LAST_CHANGE: str - TRADE_DETAILS: JSON + wallet_type: str + currency: str + balance: float + unsettled_interest: float + available_balance: float + last_change: str + trade_details: JSON @dataclass class Order(_Type): - ID: int - GID: int - CID: int - SYMBOL: str - MTS_CREATE: int - MTS_UPDATE: int - AMOUNT: float - AMOUNT_ORIG: float - ORDER_TYPE: str - TYPE_PREV: str - MTS_TIF: int - FLAGS: int - ORDER_STATUS: str - PRICE: float - PRICE_AVG: float - PRICE_TRAILING: float - PRICE_AUX_LIMIT: float - NOTIFY: int - HIDDEN: int - PLACED_ID: int - ROUTING: str - META: JSON + id: int + gid: int + cid: int + symbol: str + mts_create: int + mts_update: int + amount: float + amount_orig: float + order_type: str + type_prev: str + mts_tif: int + flags: int + order_status: str + price: float + price_avg: float + price_trailing: float + price_aux_limit: float + notify: int + hidden: int + placed_id: int + routing: str + meta: JSON @dataclass class Position(_Type): - SYMBOL: str - STATUS: str - AMOUNT: float - BASE_PRICE: float - MARGIN_FUNDING: float - MARGIN_FUNDING_TYPE: int - PL: float - PL_PERC: float - PRICE_LIQ: float - LEVERAGE: float - POSITION_ID: int - MTS_CREATE: int - MTS_UPDATE: int - TYPE: int - COLLATERAL: float - COLLATERAL_MIN: float - META: JSON + symbol: str + status: str + amount: float + base_price: float + margin_funding: float + margin_funding_type: int + pl: float + pl_perc: float + price_liq: float + leverage: float + position_id: int + mts_create: int + mts_update: int + type: int + collateral: float + collateral_min: float + meta: JSON @dataclass class FundingOffer(_Type): - ID: int - SYMBOL: str - MTS_CREATE: int - MTS_UPDATE: int - AMOUNT: float - AMOUNT_ORIG: float - OFFER_TYPE: str - FLAGS: int - OFFER_STATUS: str - RATE: float - PERIOD: int - NOTIFY: bool - HIDDEN: int - RENEW: bool + 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 @dataclass class Trade(_Type): - ID: int - SYMBOL: str - MTS_CREATE: int - ORDER_ID: int - EXEC_AMOUNT: float - EXEC_PRICE: float - ORDER_TYPE: str - ORDER_PRICE: float - MAKER:int - FEE: float - FEE_CURRENCY: str - CID: int + id: int + symbol: str + mts_create: int + order_id: int + exec_amount: float + exec_price: float + order_type: str + order_price: float + maker:int + fee: float + fee_currency: str + cid: int @dataclass class OrderTrade(_Type): - ID: int - SYMBOL: str - MTS_CREATE: int - ORDER_ID: int - EXEC_AMOUNT: float - EXEC_PRICE: float - MAKER:int - FEE: float - FEE_CURRENCY: str - CID: int + id: int + symbol: str + mts_create: int + order_id: int + exec_amount: float + exec_price: float + maker:int + fee: float + fee_currency: str + cid: int @dataclass class Ledger(_Type): - ID: int - CURRENCY: str - MTS: int - AMOUNT: float - BALANCE: float + id: int + currency: str + mts: int + amount: float + balance: float description: str @dataclass class FundingCredit(_Type): - ID: int - SYMBOL: str - SIDE: int - MTS_CREATE: int - MTS_UPDATE: int - AMOUNT: float - FLAGS: int - STATUS: str - RATE: float - PERIOD: int - MTS_OPENING: int - MTS_LAST_PAYOUT: int - NOTIFY: int - HIDDEN: int - RENEW: int - RATE_REAL: float - NO_CLOSE: int - POSITION_PAIR: str + 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 @dataclass class Transfer(_Type): - MTS: int - WALLET_FROM: str - WALLET_TO: str - CURRENCY: str - CURRENCY_TO: str - AMOUNT: int + mts: int + wallet_from: str + wallet_to: str + currency: str + currency_to: str + amount: int @dataclass class Withdrawal(_Type): - WITHDRAWAL_ID: int - METHOD: str - PAYMENT_ID: str - WALLET: str - AMOUNT: float - WITHDRAWAL_FEE: float + withdrawal_id: int + method: str + payment_id: str + wallet: str + amount: float + withdrawal_fee: float @dataclass class DepositAddress(_Type): - METHOD: str - CURRENCY_CODE: str - ADDRESS: str - POOL_ADDRESS: str + method: str + currency_code: str + address: str + pool_address: str @dataclass class Invoice(_Type): - INVOICE_HASH: str - INVOICE: str - AMOUNT: str + invoice_hash: str + invoice: str + amount: str @dataclass class Movement(_Type): - ID: str - CURRENCY: str - CURRENCY_NAME: str - MTS_STARTED: int - MTS_UPDATED: int - STATUS: str - AMOUNT: int - FEES: int - DESTINATION_ADDRESS: str - TRANSACTION_ID: str - WITHDRAW_TRANSACTION_NOTE: str + id: str + currency: str + currency_name: str + mts_started: int + mts_updated: int + status: str + amount: int + fees: int + destination_address: str + transaction_id: str + withdraw_transaction_note: str @dataclass class SymbolMarginInfo(_Type): - SYMBOL: str - TRADABLE_BALANCE: float - GROSS_BALANCE: float - BUY: float - SELL: float + symbol: str + tradable_balance: float + gross_balance: float + buy: float + sell: float @dataclass class BaseMarginInfo(_Type): - USER_PL: float - USER_SWAPS: float - MARGIN_BALANCE: float - MARGIN_NET: float - MARGIN_MIN: float + user_pl: float + user_swaps: float + margin_balance: float + margin_net: float + margin_min: float @dataclass class Claim(_Type): - SYMBOL: str - POSITION_STATUS: str - AMOUNT: float - BASE_PRICE: float - MARGIN_FUNDING: float - MARGIN_FUNDING_TYPE: int - POSITION_ID: int - MTS_CREATE: int - MTS_UPDATE: int - POS_TYPE: int - COLLATERAL: str - MIN_COLLATERAL: str - META: JSON + symbol: str + position_status: str + amount: float + base_price: float + margin_funding: float + margin_funding_type: int + position_id: int + mts_create: int + mts_update: int + pos_type: int + collateral: str + min_collateral: str + meta: JSON @dataclass class IncreaseInfo(_Type): - MAX_POS: int - CURRENT_POS: float - BASE_CURRENCY_BALANCE: float - TRADABLE_BALANCE_QUOTE_CURRENCY: float - TRADABLE_BALANCE_QUOTE_TOTAL: float - TRADABLE_BALANCE_BASE_CURRENCY: float - TRADABLE_BALANCE_BASE_TOTAL: float - FUNDING_AVAIL: float - FUNDING_VALUE: float - FUNDING_REQUIRED: float - FUNDING_VALUE_CURRENCY: str - FUNDING_REQUIRED_CURRENCY: str + max_pos: int + current_pos: float + base_currency_balance: float + tradable_balance_quote_currency: float + tradable_balance_quote_total: float + tradable_balance_base_currency: float + tradable_balance_base_total: float + funding_avail: float + funding_value: float + funding_required: float + funding_value_currency: str + funding_required_currency: str @dataclass class Increase(_Type): - SYMBOL: str - AMOUNT: float - BASE_PRICE: float + symbol: str + amount: float + base_price: float @dataclass class PositionHistory(_Type): - SYMBOL: str - STATUS: str - AMOUNT: float - BASE_PRICE: float - FUNDING: float - FUNDING_TYPE: int - POSITION_ID: int - MTS_CREATE: int - MTS_UPDATE: int + symbol: str + status: str + amount: float + base_price: float + funding: float + funding_type: int + position_id: int + mts_create: int + mts_update: int @dataclass class PositionSnapshot(_Type): - SYMBOL: str - STATUS: str - AMOUNT: float - BASE_PRICE: float - FUNDING: float - FUNDING_TYPE: int - POSITION_ID: int - MTS_CREATE: int - MTS_UPDATE: int + symbol: str + status: str + amount: float + base_price: float + funding: float + funding_type: int + position_id: int + mts_create: int + mts_update: int @dataclass class PositionAudit(_Type): - SYMBOL: str - STATUS: str - AMOUNT: float - BASE_PRICE: float - FUNDING: float - FUNDING_TYPE: int - POSITION_ID: int - MTS_CREATE: int - MTS_UPDATE: int - TYPE: int - COLLATERAL: float - COLLATERAL_MIN: float - META: JSON + symbol: str + status: str + amount: float + base_price: float + funding: float + funding_type: int + position_id: int + mts_create: int + mts_update: int + type: int + collateral: float + collateral_min: float + meta: JSON @dataclass class DerivativePositionCollateral(_Type): - STATUS: int + status: int @dataclass class DerivativePositionCollateralLimits(_Type): - MIN_COLLATERAL: float - MAX_COLLATERAL: float + min_collateral: float + max_collateral: float #endregion \ No newline at end of file diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index 1e14dbc..5991cc9 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -7,111 +7,111 @@ from .. notification import _Notification #region Serializers definition for Websocket Public Channels TradingPairTicker = generate_labeler_serializer("TradingPairTicker", klass=types.TradingPairTicker, labels=[ - "BID", - "BID_SIZE", - "ASK", - "ASK_SIZE", - "DAILY_CHANGE", - "DAILY_CHANGE_RELATIVE", - "LAST_PRICE", - "VOLUME", - "HIGH", - "LOW" + "bid", + "bid_size", + "ask", + "ask_size", + "daily_change", + "daily_change_relative", + "last_price", + "volume", + "high", + "low" ]) FundingCurrencyTicker = generate_labeler_serializer("FundingCurrencyTicker", klass=types.FundingCurrencyTicker, labels=[ - "FRR", - "BID", - "BID_PERIOD", - "BID_SIZE", - "ASK", - "ASK_PERIOD", - "ASK_SIZE", - "DAILY_CHANGE", - "DAILY_CHANGE_RELATIVE", - "LAST_PRICE", - "VOLUME", - "HIGH", - "LOW" + "frr", + "bid", + "bid_period", + "bid_size", + "ask", + "ask_period", + "ask_size", + "daily_change", + "daily_change_relative", + "last_price", + "volume", + "high", + "low" "_PLACEHOLDER", "_PLACEHOLDER", - "FRR_AMOUNT_AVAILABLE" + "frr_amount_available" ]) TradingPairTrade = generate_labeler_serializer("TradingPairTrade", klass=types.TradingPairTrade, labels=[ - "ID", - "MTS", - "AMOUNT", - "PRICE" + "id", + "mts", + "amount", + "price" ]) FundingCurrencyTrade = generate_labeler_serializer("FundingCurrencyTrade", klass=types.FundingCurrencyTrade, labels=[ - "ID", - "MTS", - "AMOUNT", - "RATE", - "PERIOD" + "id", + "mts", + "amount", + "rate", + "period" ]) TradingPairBook = generate_labeler_serializer("TradingPairBook", klass=types.TradingPairBook, labels=[ - "PRICE", - "COUNT", - "AMOUNT" + "price", + "count", + "amount" ]) FundingCurrencyBook = generate_labeler_serializer("FundingCurrencyBook", klass=types.FundingCurrencyBook, labels=[ - "RATE", - "PERIOD", - "COUNT", - "AMOUNT" + "rate", + "period", + "count", + "amount" ]) TradingPairRawBook = generate_labeler_serializer("TradingPairRawBook", klass=types.TradingPairRawBook, labels=[ - "ORDER_ID", - "PRICE", - "AMOUNT" + "order_id", + "price", + "amount" ]) FundingCurrencyRawBook = generate_labeler_serializer("FundingCurrencyRawBook", klass=types.FundingCurrencyRawBook, labels=[ - "OFFER_ID", - "PERIOD", - "RATE", - "AMOUNT" + "offer_id", + "period", + "rate", + "amount" ]) Candle = generate_labeler_serializer("Candle", klass=types.Candle, labels=[ - "MTS", - "OPEN", - "CLOSE", - "HIGH", - "LOW", - "VOLUME" + "mts", + "open", + "close", + "high", + "low", + "volume" ]) DerivativesStatus = generate_labeler_serializer("DerivativesStatus", klass=types.DerivativesStatus, labels=[ - "TIME_MS", + "time_ms", "_PLACEHOLDER", - "DERIV_PRICE", - "SPOT_PRICE", + "deriv_price", + "spot_price", "_PLACEHOLDER", - "INSURANCE_FUND_BALANCE", + "insurance_fund_balance", "_PLACEHOLDER", - "NEXT_FUNDING_EVT_TIMESTAMP_MS", - "NEXT_FUNDING_ACCRUED", - "NEXT_FUNDING_STEP", + "next_funding_evt_timestamp_ms", + "next_funding_accrued", + "next_funding_step", "_PLACEHOLDER", - "CURRENT_FUNDING" + "current_funding" "_PLACEHOLDER", "_PLACEHOLDER", - "MARK_PRICE", + "mark_price", "_PLACEHOLDER", "_PLACEHOLDER", - "OPEN_INTEREST", + "open_interest", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "CLAMP_MIN", - "CLAMP_MAX" + "clamp_min", + "clamp_max" ]) #endregion @@ -119,179 +119,179 @@ DerivativesStatus = generate_labeler_serializer("DerivativesStatus", klass=types #region Serializers definition for Websocket Authenticated Channels Order = generate_labeler_serializer("Order", klass=types.Order, labels=[ - "ID", - "GID", - "CID", - "SYMBOL", - "MTS_CREATE", - "MTS_UPDATE", - "AMOUNT", - "AMOUNT_ORIG", - "ORDER_TYPE", - "TYPE_PREV", - "MTS_TIF", + "id", + "gid", + "cid", + "symbol", + "mts_create", + "mts_update", + "amount", + "amount_orig", + "order_type", + "type_prev", + "mts_tif", "_PLACEHOLDER", - "FLAGS", - "ORDER_STATUS", + "flags", + "order_status", "_PLACEHOLDER", "_PLACEHOLDER", - "PRICE", - "PRICE_AVG", - "PRICE_TRAILING", - "PRICE_AUX_LIMIT", + "price", + "price_avg", + "price_trailing", + "price_aux_limit", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "NOTIFY", - "HIDDEN", - "PLACED_ID", + "notify", + "hidden", + "placed_id", "_PLACEHOLDER", "_PLACEHOLDER", - "ROUTING", + "routing", "_PLACEHOLDER", "_PLACEHOLDER", - "META" + "meta" ]) Position = generate_labeler_serializer("Position", klass=types.Position, labels=[ - "SYMBOL", - "STATUS", - "AMOUNT", - "BASE_PRICE", - "MARGIN_FUNDING", - "MARGIN_FUNDING_TYPE", - "PL", - "PL_PERC", - "PRICE_LIQ", - "LEVERAGE", - "FLAG", - "POSITION_ID", - "MTS_CREATE", - "MTS_UPDATE", + "symbol", + "status", + "amount", + "base_price", + "margin_funding", + "margin_funding_type", + "pl", + "pl_perc", + "price_liq", + "leverage", + "flag", + "position_id", + "mts_create", + "mts_update", "_PLACEHOLDER", - "TYPE", + "type", "_PLACEHOLDER", - "COLLATERAL", - "COLLATERAL_MIN", - "META" + "collateral", + "collateral_min", + "meta" ]) TradeExecuted = generate_labeler_serializer("TradeExecuted", klass=types.TradeExecuted, labels=[ - "ID", - "SYMBOL", - "MTS_CREATE", - "ORDER_ID", - "EXEC_AMOUNT", - "EXEC_PRICE", - "ORDER_TYPE", - "ORDER_PRICE", - "MAKER", + "id", + "symbol", + "mts_create", + "order_id", + "exec_amount", + "exec_price", + "order_type", + "order_price", + "maker", "_PLACEHOLDER", "_PLACEHOLDER", - "CID" + "cid" ]) TradeExecutionUpdate = generate_labeler_serializer("TradeExecutionUpdate", klass=types.TradeExecutionUpdate, labels=[ - "ID", - "SYMBOL", - "MTS_CREATE", - "ORDER_ID", - "EXEC_AMOUNT", - "EXEC_PRICE", - "ORDER_TYPE", - "ORDER_PRICE", - "MAKER", - "FEE", - "FEE_CURRENCY", - "CID" + "id", + "symbol", + "mts_create", + "order_id", + "exec_amount", + "exec_price", + "order_type", + "order_price", + "maker", + "fee", + "fee_currency", + "cid" ]) FundingOffer = generate_labeler_serializer("FundingOffer", klass=types.FundingOffer, labels=[ - "ID", - "SYMBOL", - "MTS_CREATED", - "MTS_UPDATED", - "AMOUNT", - "AMOUNT_ORIG", - "OFFER_TYPE", + "id", + "symbol", + "mts_created", + "mts_updated", + "amount", + "amount_orig", + "offer_type", "_PLACEHOLDER", "_PLACEHOLDER", - "FLAGS", - "STATUS", + "flags", + "status", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "RATE", - "PERIOD", - "NOTIFY", - "HIDDEN", + "rate", + "period", + "notify", + "hidden", "_PLACEHOLDER", - "RENEW", + "renew", "_PLACEHOLDER" ]) FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.FundingCredit, labels=[ - "ID", - "SYMBOL", - "SIDE", - "MTS_CREATE", - "MTS_UPDATE", - "AMOUNT", - "FLAGS", - "STATUS", + "id", + "symbol", + "side", + "mts_create", + "mts_update", + "amount", + "flags", + "status", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "RATE", - "PERIOD", - "MTS_OPENING", - "MTS_LAST_PAYOUT", - "NOTIFY", - "HIDDEN", + "rate", + "period", + "mts_opening", + "mts_last_payout", + "notify", + "hidden", "_PLACEHOLDER", - "RENEW", - "RATE_REAL", - "NO_CLOSE", - "POSITION_PAIR" + "renew", + "rate_real", + "no_close", + "position_pair" ]) FundingLoan = generate_labeler_serializer("FundingLoan", klass=types.FundingLoan, labels=[ - "ID", - "SYMBOL", - "SIDE", - "MTS_CREATE", - "MTS_UPDATE", - "AMOUNT", - "FLAGS", - "STATUS", + "id", + "symbol", + "side", + "mts_create", + "mts_update", + "amount", + "flags", + "status", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "RATE", - "PERIOD", - "MTS_OPENING", - "MTS_LAST_PAYOUT", - "NOTIFY", - "HIDDEN", + "rate", + "period", + "mts_opening", + "mts_last_payout", + "notify", + "hidden", "_PLACEHOLDER", - "RENEW", - "RATE_REAL", - "NO_CLOSE" + "renew", + "rate_real", + "no_close" ]) Wallet = generate_labeler_serializer("Wallet", klass=types.Wallet, labels=[ - "WALLET_TYPE", - "CURRENCY", - "BALANCE", - "UNSETTLED_INTEREST", - "BALANCE_AVAILABLE", - "DESCRIPTION", - "META" + "wallet_type", + "currency", + "balance", + "unsettled_interest", + "balance_available", + "description", + "meta" ]) BalanceInfo = generate_labeler_serializer("BalanceInfo", klass=types.BalanceInfo, labels=[ - "AUM", - "AUM_NET", + "aum", + "aum_net", ]) #endregion \ No newline at end of file diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py index 19ed3ef..91bac2a 100644 --- a/bfxapi/websocket/types.py +++ b/bfxapi/websocket/types.py @@ -10,246 +10,246 @@ from .. utils.encoder import JSON @dataclass class TradingPairTicker(_Type): - BID: float - BID_SIZE: float - ASK: float - ASK_SIZE: float - DAILY_CHANGE: float - DAILY_CHANGE_RELATIVE: float - LAST_PRICE: float - VOLUME: float - HIGH: float - LOW: float + bid: float + bid_size: float + ask: float + ask_size: float + daily_change: float + daily_change_relative: float + last_price: float + volume: float + high: float + low: float @dataclass class FundingCurrencyTicker(_Type): - FRR: float - BID: float - BID_PERIOD: int - BID_SIZE: float - ASK: float - ASK_PERIOD: int - ASK_SIZE: float - DAILY_CHANGE: float - DAILY_CHANGE_RELATIVE: float - LAST_PRICE: float - VOLUME: float - HIGH: float - LOW: float - FRR_AMOUNT_AVAILABLE: float + frr: float + bid: float + bid_period: int + bid_size: float + ask: float + ask_period: int + ask_size: float + daily_change: float + daily_change_relative: float + last_price: float + volume: float + high: float + low: float + frr_amount_available: float @dataclass class TradingPairTrade(_Type): - ID: int - MTS: int - AMOUNT: float - PRICE: float + id: int + mts: int + amount: float + price: float @dataclass class FundingCurrencyTrade(_Type): - ID: int - MTS: int - AMOUNT: float - RATE: float - PERIOD: int + id: int + mts: int + amount: float + rate: float + period: int @dataclass class TradingPairBook(_Type): - PRICE: float - COUNT: int - AMOUNT: float + price: float + count: int + amount: float @dataclass class FundingCurrencyBook(_Type): - RATE: float - PERIOD: int - COUNT: int - AMOUNT: float + rate: float + period: int + count: int + amount: float @dataclass class TradingPairRawBook(_Type): - ORDER_ID: int - PRICE: float - AMOUNT: float + order_id: int + price: float + amount: float @dataclass class FundingCurrencyRawBook(_Type): - OFFER_ID: int - PERIOD: int - RATE: float - AMOUNT: float + offer_id: int + period: int + rate: float + amount: float @dataclass class Candle(_Type): - MTS: int - OPEN: float - CLOSE: float - HIGH: float - LOW: float - VOLUME: float + mts: int + open: float + close: float + high: float + low: float + volume: float @dataclass class DerivativesStatus(_Type): - 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 + 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 @dataclass class Order(_Type): - ID: int - GID: int - CID: int - SYMBOL: str - MTS_CREATE: int - MTS_UPDATE: int - AMOUNT: float - AMOUNT_ORIG: float - ORDER_TYPE: str - TYPE_PREV: str - MTS_TIF: int - FLAGS: int - ORDER_STATUS: str - PRICE: float - PRICE_AVG: float - PRICE_TRAILING: float - PRICE_AUX_LIMIT: float - NOTIFY: int - HIDDEN: int - PLACED_ID: int - ROUTING: str - META: JSON + id: int + gid: int + cid: int + symbol: str + mts_create: int + mts_update: int + amount: float + amount_orig: float + order_type: str + type_prev: str + mts_tif: int + flags: int + order_status: str + price: float + price_avg: float + price_trailing: float + price_aux_limit: float + notify: int + hidden: int + placed_id: int + routing: str + meta: JSON @dataclass class Position(_Type): - SYMBOL: str - STATUS: str - AMOUNT: float - BASE_PRICE: float - MARGIN_FUNDING: float - MARGIN_FUNDING_TYPE: int - PL: float - PL_PERC: float - PRICE_LIQ: float - LEVERAGE: float - POSITION_ID: int - MTS_CREATE: int - MTS_UPDATE: int - TYPE: int - COLLATERAL: float - COLLATERAL_MIN: float - META: JSON + symbol: str + status: str + amount: float + base_price: float + margin_funding: float + margin_funding_type: int + pl: float + pl_perc: float + price_liq: float + leverage: float + position_id: int + mts_create: int + mts_update: int + type: int + collateral: float + collateral_min: float + meta: JSON @dataclass class TradeExecuted(_Type): - ID: int - SYMBOL: str - MTS_CREATE: int - ORDER_ID: int - EXEC_AMOUNT: float - EXEC_PRICE: float - ORDER_TYPE: str - ORDER_PRICE: float - MAKER:int - CID: int + 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 @dataclass class TradeExecutionUpdate(_Type): - ID: int - SYMBOL: str - MTS_CREATE: int - ORDER_ID: int - EXEC_AMOUNT: float - EXEC_PRICE: float - ORDER_TYPE: str - ORDER_PRICE: float - MAKER:int - FEE: float - FEE_CURRENCY: str - CID: int + id: int + symbol: str + mts_create: int + order_id: int + exec_amount: float + exec_price: float + order_type: str + order_price: float + maker:int + fee: float + fee_currency: str + cid: int @dataclass class FundingOffer(_Type): - 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 + 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 @dataclass class FundingCredit(_Type): - ID: int - SYMBOL: str - SIDE: int - MTS_CREATE: int - MTS_UPDATE: int - AMOUNT: float - FLAGS: int - STATUS: str - RATE: float - PERIOD: int - MTS_OPENING: int - MTS_LAST_PAYOUT: int - NOTIFY: int - HIDDEN: int - RENEW: int - RATE_REAL: float - NO_CLOSE: int - POSITION_PAIR: str + 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 @dataclass class FundingLoan(_Type): - ID: int - SYMBOL: str - SIDE: int - MTS_CREATE: int - MTS_UPDATE: int - AMOUNT: float - FLAGS: int - STATUS: str - RATE: float - PERIOD: int - MTS_OPENING: int - MTS_LAST_PAYOUT: int - NOTIFY: int - HIDDEN: int - RENEW: int - RATE_REAL: float - NO_CLOSE: int + 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 @dataclass class Wallet(_Type): - WALLET_TYPE: str - CURRENCY: str - BALANCE: float - UNSETTLED_INTEREST: float - BALANCE_AVAILABLE: float - DESCRIPTION: str - META: JSON + wallet_type: str + currency: str + balance: float + unsettled_interest: float + balance_available: float + description: str + meta: JSON @dataclass class BalanceInfo(_Type): - AUM: float - AUM_NET: float + aum: float + aum_net: float #endregion \ No newline at end of file From 6e96cda584ba69d1dfc53297e6352038bf95301a Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 27 Jan 2023 15:57:36 +0100 Subject: [PATCH 30/85] Edit demos in examples/ folder to use lowercase property identifiers. --- bfxapi/rest/BfxRestInterface.py | 5 ++--- examples/rest/claim_position.py | 4 ++-- examples/rest/create_order.py | 4 ++-- examples/rest/derivatives.py | 2 +- examples/rest/extra_calcs.py | 6 +++--- examples/rest/get_pulse_data.py | 6 +++--- examples/rest/increase_position.py | 2 +- examples/rest/transfer_wallet.py | 8 ++++---- examples/websocket/order_book.py | 2 +- examples/websocket/raw_order_book.py | 2 +- 10 files changed, 20 insertions(+), 21 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index bf4e1cf..d50799c 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -101,7 +101,7 @@ class _RestPublicEndpoints(_Requests): def get_t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]: if isinstance(pairs, str) and pairs == "ALL": - return [ cast(TradingPairTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.SYMBOL).startswith("t") ] + return [ cast(TradingPairTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.symbol).startswith("t") ] data = self.get_tickers([ "t" + pair for pair in pairs ]) @@ -109,7 +109,7 @@ class _RestPublicEndpoints(_Requests): def get_f_tickers(self, currencies: Union[List[str], Literal["ALL"]]) -> List[FundingCurrencyTicker]: if isinstance(currencies, str) and currencies == "ALL": - return [ cast(FundingCurrencyTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.SYMBOL).startswith("f") ] + return [ cast(FundingCurrencyTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.symbol).startswith("f") ] data = self.get_tickers([ "f" + currency for currency in currencies ]) @@ -218,7 +218,6 @@ class _RestPublicEndpoints(_Requests): return [ serializers.Liquidation.parse(*sub_data[0]) for sub_data in data ] def get_seed_candles(self, symbol: str, tf: str = '1m', sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Candle]: - params = {"sort": sort, "start": start, "end": end, "limit": limit} data = self._GET(f"candles/trade:{tf}:{symbol}/hist?limit={limit}&start={start}&end={end}&sort={sort}", params=params) diff --git a/examples/rest/claim_position.py b/examples/rest/claim_position.py index de3b5f2..caf5e23 100644 --- a/examples/rest/claim_position.py +++ b/examples/rest/claim_position.py @@ -15,5 +15,5 @@ open_margin_positions = bfx.rest.auth.get_positions() # claim all positions for position in open_margin_positions: print(f"Position {position}") - claim = bfx.rest.auth.claim_position(position.POSITION_ID, amount=0.000001) - print(f"Claim {claim.NOTIFY_INFO}") \ No newline at end of file + claim = bfx.rest.auth.claim_position(position.position_id, amount=0.000001) + print(f"Claim {claim.notify_info}") \ No newline at end of file diff --git a/examples/rest/create_order.py b/examples/rest/create_order.py index 1f63e46..2c13ae2 100644 --- a/examples/rest/create_order.py +++ b/examples/rest/create_order.py @@ -25,7 +25,7 @@ print("Submit Order Notification:", submitted_order) # Update it updated_order = bfx.rest.auth.update_order( - id=submitted_order.NOTIFY_INFO.ID, + id=submitted_order.notify_info.id, amount="0.020", price="10100" ) @@ -33,6 +33,6 @@ updated_order = bfx.rest.auth.update_order( print("Update Order Notification:", updated_order) # Delete it -canceled_order = bfx.rest.auth.cancel_order(id=submitted_order.NOTIFY_INFO.ID) +canceled_order = bfx.rest.auth.cancel_order(id=submitted_order.notify_info.id) print("Cancel Order Notification:", canceled_order) diff --git a/examples/rest/derivatives.py b/examples/rest/derivatives.py index 89865a5..4aedd00 100644 --- a/examples/rest/derivatives.py +++ b/examples/rest/derivatives.py @@ -27,5 +27,5 @@ print(f"Limits {limits}") # Update position collateral response = bfx.rest.auth.set_derivative_position_collateral(symbol="tBTCF0:USTF0", collateral=50) -print(response.STATUS) +print(response.status) diff --git a/examples/rest/extra_calcs.py b/examples/rest/extra_calcs.py index 0e6fb1e..e380aef 100644 --- a/examples/rest/extra_calcs.py +++ b/examples/rest/extra_calcs.py @@ -12,7 +12,7 @@ t_symbol_response = bfx.rest.public.get_trading_market_average_price( price_limit="20000.5" ) -print(t_symbol_response.PRICE_AVG) +print(t_symbol_response.price_avg) f_symbol_response = bfx.rest.public.get_funding_market_average_price( symbol="fUSD", @@ -21,8 +21,8 @@ f_symbol_response = bfx.rest.public.get_funding_market_average_price( rate_limit="0.00015" ) -print(f_symbol_response.RATE_AVG) +print(f_symbol_response.rate_avg) fx_rate = bfx.rest.public.get_fx_rate(ccy1="USD", ccy2="EUR") -print(fx_rate.CURRENT_RATE) \ No newline at end of file +print(fx_rate.current_rate) \ No newline at end of file diff --git a/examples/rest/get_pulse_data.py b/examples/rest/get_pulse_data.py index 9fb8832..75b55ae 100644 --- a/examples/rest/get_pulse_data.py +++ b/examples/rest/get_pulse_data.py @@ -14,9 +14,9 @@ messages = bfx.rest.public.get_pulse_history(end=now, limit=100) for message in messages: print(f"Message: {message}") - print(message.CONTENT) - print(message.PROFILE.PICTURE) + print(message.content) + print(message.profile.picture) profile = bfx.rest.public.get_pulse_profile("News") print(f"Profile: {profile}") -print(f"Profile picture: {profile.PICTURE}") \ No newline at end of file +print(f"Profile picture: {profile.picture}") \ No newline at end of file diff --git a/examples/rest/increase_position.py b/examples/rest/increase_position.py index 440f9c8..65595c8 100644 --- a/examples/rest/increase_position.py +++ b/examples/rest/increase_position.py @@ -15,4 +15,4 @@ print(increase_info) # increase a margin position notification = bfx.rest.auth.increase_position(symbol="tBTCUSD", amount=0.0001) -print(notification.NOTIFY_INFO) +print(notification.notify_info) diff --git a/examples/rest/transfer_wallet.py b/examples/rest/transfer_wallet.py index b1d9fd3..e986bfd 100644 --- a/examples/rest/transfer_wallet.py +++ b/examples/rest/transfer_wallet.py @@ -12,20 +12,20 @@ bfx = Client( def transfer_wallet(): response = bfx.rest.auth.submit_wallet_transfer(from_wallet="exchange", to_wallet="funding", from_currency="ETH", to_currency="ETH", amount=0.001) - print("Transfer:", response.NOTIFY_INFO) + print("Transfer:", response.notify_info) def get_existing_deposit_address(): response = bfx.rest.auth.get_deposit_address(wallet="exchange", method="bitcoin", renew=False) - print("Address:", response.NOTIFY_INFO) + print("Address:", response.notify_info) def create_new_deposit_address(): response = bfx.rest.auth.get_deposit_address(wallet="exchange", method="bitcoin", renew=True) - print("Address:", response.NOTIFY_INFO) + print("Address:", response.notify_info) def withdraw(): # tetheruse = Tether (ERC20) response = bfx.rest.auth.submit_wallet_withdraw(wallet="exchange", method="tetheruse", amount=1, address="0x742d35Cc6634C0532925a3b844Bc454e4438f44e") - print("Address:", response.NOTIFY_INFO) + print("Address:", response.notify_info) def create_lighting_network_deposit_address(): invoice = bfx.rest.auth.get_deposit_invoice(wallet="funding", currency="LNX", amount=0.001) diff --git a/examples/websocket/order_book.py b/examples/websocket/order_book.py index 8774b8a..82bd105 100644 --- a/examples/websocket/order_book.py +++ b/examples/websocket/order_book.py @@ -19,7 +19,7 @@ class OrderBook(object): } def update(self, symbol: str, data: TradingPairBook) -> None: - price, count, amount = data.PRICE, data.COUNT, data.AMOUNT + price, count, amount = data.price, data.count, data.amount kind = (amount > 0) and "bids" or "asks" diff --git a/examples/websocket/raw_order_book.py b/examples/websocket/raw_order_book.py index 172873e..896e351 100644 --- a/examples/websocket/raw_order_book.py +++ b/examples/websocket/raw_order_book.py @@ -19,7 +19,7 @@ class RawOrderBook(object): } def update(self, symbol: str, data: TradingPairRawBook) -> None: - order_id, price, amount = data.ORDER_ID, data.PRICE, data.AMOUNT + order_id, price, amount = data.order_id, data.price, data.amount kind = (amount > 0) and "bids" or "asks" From f12981b841b6403bbbfde742ee4ef28812eabae0 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 27 Jan 2023 16:10:11 +0100 Subject: [PATCH 31/85] Refactor some _RestPublicEndpoints's methods. --- bfxapi/rest/BfxRestInterface.py | 39 ++++++++++----------------------- 1 file changed, 11 insertions(+), 28 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index d50799c..2555eae 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -89,6 +89,9 @@ class _Requests(object): return data class _RestPublicEndpoints(_Requests): + def conf(self, config: Config) -> Any: + return self._GET(f"conf/{config}")[0] + def get_platform_status(self) -> PlatformStatus: return serializers.PlatformStatus.parse(*self._GET("platform/status")) @@ -121,16 +124,12 @@ class _RestPublicEndpoints(_Requests): 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 = { + def get_tickers_history(self, symbols: List[str], start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[TickersHistory]: + return [ serializers.TickersHistory.parse(*sub_data) for sub_data in self._GET("tickers/hist", params={ "symbols": ",".join(symbols), "start": start, "end": end, "limit": limit - } - - data = self._GET("tickers/hist", params=params) - - return [ serializers.TickersHistory.parse(*sub_data) for sub_data 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 } @@ -205,23 +204,17 @@ class _RestPublicEndpoints(_Requests): sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> List[DerivativesStatus]: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"status/{type}/{symbol}/hist", params=params) - return [ serializers.DerivativesStatus.parse(*sub_data, skip=[ "KEY" ]) for sub_data in data ] def get_liquidations(self, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Liquidation]: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET("liquidations/hist", params=params) - return [ serializers.Liquidation.parse(*sub_data[0]) for sub_data in data ] def get_seed_candles(self, symbol: str, tf: str = '1m', sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Candle]: params = {"sort": sort, "start": start, "end": end, "limit": limit} - data = self._GET(f"candles/trade:{tf}:{symbol}/hist?limit={limit}&start={start}&end={end}&sort={sort}", params=params) - return [ serializers.Candle.parse(*sub_data) for sub_data in data ] def get_leaderboards_hist( @@ -244,14 +237,9 @@ class _RestPublicEndpoints(_Requests): def get_funding_stats(self, symbol: str, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingStatistic]: params = { "start": start, "end": end, "limit": limit } - data = self._GET(f"funding/stats/{symbol}/hist", params=params) - return [ serializers.FundingStatistic.parse(*sub_data) for sub_data in data ] - def conf(self, config: Config) -> Any: - return self._GET(f"conf/{config}")[0] - def get_pulse_profile(self, nickname: str) -> PulseProfile: return serializers.PulseProfile.parse(*self._GET(f"pulse/profile/{nickname}")) @@ -266,19 +254,14 @@ class _RestPublicEndpoints(_Requests): return messages def get_trading_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], price_limit: Optional[Union[Decimal, float, str]] = None) -> TradingMarketAveragePrice: - data = { + return serializers.TradingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data={ "symbol": symbol, "amount": amount, "price_limit": price_limit - } - - return serializers.TradingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data=data)) + })) def get_funding_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], period: int, rate_limit: Optional[Union[Decimal, float, str]] = None) -> FundingMarketAveragePrice: - data = { - "symbol": symbol, "amount": amount, "period": period, - "rate_limit": rate_limit - } - - return serializers.FundingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data=data)) + return serializers.FundingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data={ + "symbol": symbol, "amount": amount, "period": period, "rate_limit": rate_limit + })) def get_fx_rate(self, ccy1: str, ccy2: str) -> FxRate: return serializers.FxRate.parse(*self._POST("calc/fx", data={ "ccy1": ccy1, "ccy2": ccy2 })) From 17fc29d4fa37b404b0e81ff209529b7318eb64dc Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 27 Jan 2023 16:38:58 +0100 Subject: [PATCH 32/85] Separate in different classes the content of BfxRestInterface.py script. --- bfxapi/rest/BfxRestInterface.py | 510 +-------------------- bfxapi/rest/_Requests.py | 75 +++ bfxapi/rest/_RestAuthenticatedEndpoints.py | 253 ++++++++++ bfxapi/rest/_RestPublicEndpoints.py | 188 ++++++++ 4 files changed, 523 insertions(+), 503 deletions(-) create mode 100644 bfxapi/rest/_Requests.py create mode 100644 bfxapi/rest/_RestAuthenticatedEndpoints.py create mode 100644 bfxapi/rest/_RestPublicEndpoints.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 2555eae..91d31a2 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -1,509 +1,13 @@ -import time, hmac, hashlib, json, requests +from typing import Optional -from decimal import Decimal -from datetime import datetime -from http import HTTPStatus +from ._RestPublicEndpoints import _RestPublicEndpoints -from typing import List, Union, Literal, Optional, Any, cast - -from . import serializers - -from .types import * -from .enums import Config, Sort, OrderType, FundingOfferType, Error -from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError - -from .. utils.encoder import JSONEncoder +from ._RestAuthenticatedEndpoints import _RestAuthenticatedEndpoints class BfxRestInterface(object): - def __init__(self, host, API_KEY = None, API_SECRET = None): + VERSION = 2 + + def __init__(self, host: str, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = 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(f"The server replied to the request with a generic error with message: <{data[2]}>.") - - return data - - def _POST(self, endpoint, params = None, data = None): - 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 self.API_KEY and self.API_SECRET: - 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 conf(self, config: Config) -> Any: - return self._GET(f"conf/{config}")[0] - - def get_platform_status(self) -> PlatformStatus: - return serializers.PlatformStatus.parse(*self._GET("platform/status")) - - def get_tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]: - data = self._GET("tickers", params={ "symbols": ",".join(symbols) }) - - parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse } - - return [ cast(Union[TradingPairTicker, FundingCurrencyTicker], parsers[sub_data[0][0]](*sub_data)) for sub_data in data ] - - def get_t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]: - if isinstance(pairs, str) and pairs == "ALL": - return [ cast(TradingPairTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.symbol).startswith("t") ] - - data = self.get_tickers([ "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, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.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]: - return [ serializers.TickersHistory.parse(*sub_data) for sub_data in self._GET("tickers/hist", params={ - "symbols": ",".join(symbols), - "start": start, "end": end, - "limit": limit - }) ] - - def get_t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[TradingPairTrade]: - params = { "limit": limit, "start": start, "end": end, "sort": sort } - data = self._GET(f"trades/{'t' + pair}/hist", params=params) - return [ serializers.TradingPairTrade.parse(*sub_data) for sub_data in data ] - - def get_f_trades(self, currency: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[FundingCurrencyTrade]: - params = { "limit": limit, "start": start, "end": end, "sort": sort } - data = self._GET(f"trades/{'f' + currency}/hist", params=params) - return [ serializers.FundingCurrencyTrade.parse(*sub_data) for sub_data in data ] - - def get_t_book(self, pair: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairBook]: - return [ serializers.TradingPairBook.parse(*sub_data) for sub_data in self._GET(f"book/{'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(*sub_data) for sub_data 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(*sub_data) for sub_data 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(*sub_data) for sub_data 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(*sub_data) for sub_data in data ] - - def get_stats_last( - self, - resource: str, - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Statistic: - params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"stats1/{resource}/last", params=params) - return serializers.Statistic.parse(*data) - - def get_candles_hist( - self, - symbol: str, tf: str = "1m", - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> List[Candle]: - params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"candles/trade:{tf}:{symbol}/hist", params=params) - return [ serializers.Candle.parse(*sub_data) for sub_data in data ] - - def get_candles_last( - self, - symbol: str, tf: str = "1m", - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Candle: - params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"candles/trade:{tf}:{symbol}/last", params=params) - return serializers.Candle.parse(*data) - - def get_derivatives_status(self, keys: Union[List[str], Literal["ALL"]]) -> List[DerivativesStatus]: - if keys == "ALL": - params = { "keys": "ALL" } - else: params = { "keys": ",".join(keys) } - - data = self._GET(f"status/deriv", params=params) - - return [ serializers.DerivativesStatus.parse(*sub_data) for sub_data in data ] - - def get_derivatives_status_history( - self, - type: str, symbol: str, - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> List[DerivativesStatus]: - params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"status/{type}/{symbol}/hist", params=params) - return [ serializers.DerivativesStatus.parse(*sub_data, skip=[ "KEY" ]) for sub_data in data ] - - def get_liquidations(self, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Liquidation]: - params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET("liquidations/hist", params=params) - return [ serializers.Liquidation.parse(*sub_data[0]) for sub_data in data ] - - def get_seed_candles(self, symbol: str, tf: str = '1m', sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Candle]: - params = {"sort": sort, "start": start, "end": end, "limit": limit} - data = self._GET(f"candles/trade:{tf}:{symbol}/hist?limit={limit}&start={start}&end={end}&sort={sort}", params=params) - return [ serializers.Candle.parse(*sub_data) for sub_data in data ] - - def get_leaderboards_hist( - self, - resource: str, - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> List[Leaderboard]: - params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"rankings/{resource}/hist", params=params) - return [ serializers.Leaderboard.parse(*sub_data) for sub_data in data ] - - def get_leaderboards_last( - self, - resource: str, - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Leaderboard: - params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"rankings/{resource}/last", params=params) - return serializers.Leaderboard.parse(*data) - - def get_funding_stats(self, symbol: str, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingStatistic]: - params = { "start": start, "end": end, "limit": limit } - data = self._GET(f"funding/stats/{symbol}/hist", params=params) - return [ serializers.FundingStatistic.parse(*sub_data) for sub_data in data ] - - def get_pulse_profile(self, nickname: str) -> PulseProfile: - return serializers.PulseProfile.parse(*self._GET(f"pulse/profile/{nickname}")) - - def get_pulse_history(self, end: Optional[str] = None, limit: Optional[int] = None) -> List[PulseMessage]: - messages = list() - - for subdata in self._GET("pulse/hist", params={ "end": end, "limit": limit }): - subdata[18] = subdata[18][0] - message = serializers.PulseMessage.parse(*subdata) - messages.append(message) - - return messages - - def get_trading_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], price_limit: Optional[Union[Decimal, float, str]] = None) -> TradingMarketAveragePrice: - return serializers.TradingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data={ - "symbol": symbol, "amount": amount, "price_limit": price_limit - })) - - def get_funding_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], period: int, rate_limit: Optional[Union[Decimal, float, str]] = None) -> FundingMarketAveragePrice: - return serializers.FundingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data={ - "symbol": symbol, "amount": amount, "period": period, "rate_limit": rate_limit - })) - - def get_fx_rate(self, ccy1: str, ccy2: str) -> FxRate: - return serializers.FxRate.parse(*self._POST("calc/fx", data={ "ccy1": ccy1, "ccy2": ccy2 })) - -class _RestAuthenticatedEndpoints(_Requests): - def get_wallets(self) -> List[Wallet]: - return [ serializers.Wallet.parse(*sub_data) for sub_data in self._POST("auth/r/wallets") ] - - def get_orders(self, symbol: Optional[str] = None, ids: Optional[List[str]] = None) -> List[Order]: - endpoint = "auth/r/orders" - - if symbol != None: - endpoint += f"/{symbol}" - - return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, data={ "id": ids }) ] - - def get_positions(self) -> List[Position]: - return [ serializers.Position.parse(*sub_data) for sub_data in self._POST("auth/r/positions") ] - - def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, float, str], - price: Optional[Union[Decimal, float, str]] = None, lev: Optional[int] = None, - price_trailing: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_oco_stop: Optional[Union[Decimal, float, str]] = None, - gid: Optional[int] = None, cid: Optional[int] = None, - flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification[Order]: - 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[Order](serializer=serializers.Order).parse(*self._POST("auth/w/order/submit", data=data)) - - def update_order(self, id: int, amount: Optional[Union[Decimal, float, str]] = None, price: Optional[Union[Decimal, float, str]] = None, - cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, - flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, float, str]] = None, - price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification[Order]: - 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[Order](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[Order]: - data = { - "id": id, - "cid": cid, - "cid_date": cid_date - } - - return serializers._Notification[Order](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[List[Order]]: - data = { - "ids": ids, - "cids": cids, - "gids": gids, - - "all": int(all) - } - - return serializers._Notification[List[Order]](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(*sub_data) for sub_data in self._POST(endpoint, data=data) ] - - def get_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Trade]: - if symbol == None: - endpoint = "auth/r/trades/hist" - else: endpoint = f"auth/r/trades/{symbol}/hist" - - data = { - "sort": sort, - "start": start, "end": end, - "limit": limit - } - - return [ serializers.Trade.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] - - def get_order_trades(self, symbol: str, id: int) -> List[OrderTrade]: - return [ serializers.OrderTrade.parse(*sub_data) for sub_data in self._POST(f"auth/r/order/{symbol}:{id}/trades") ] - - def get_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(*sub_data) for sub_data in self._POST(f"auth/r/ledgers/{currency}/hist", data=data) ] - - def get_funding_offers(self, symbol: Optional[str] = None) -> List[FundingOffer]: - endpoint = "auth/r/funding/offers" - - if symbol != None: - endpoint += f"/{symbol}" - - return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint) ] - - def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, float, str], - rate: Union[Decimal, float, str], period: int, - flags: Optional[int] = 0) -> Notification[FundingOffer]: - data = { - "type": type, "symbol": symbol, "amount": amount, - "rate": rate, "period": period, - "flags": flags - } - - return serializers._Notification[FundingOffer](serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/submit", data=data)) - - def cancel_funding_offer(self, id: int) -> Notification[FundingOffer]: - return serializers._Notification[FundingOffer](serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/cancel", data={ "id": id })) - - def cancel_all_funding_offers(self, currency: str) -> Notification: - return serializers._Notification().parse(*self._POST("auth/w/funding/offer/cancel/all", data={ "currency": currency })) - - def get_funding_offers_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingOffer]: - if symbol == None: - endpoint = "auth/r/funding/offers/hist" - else: endpoint = f"auth/r/funding/offers/{symbol}/hist" - - data = { - "start": start, "end": end, - "limit": limit - } - - return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] - - def get_funding_credits(self, symbol: Optional[str] = None) -> List[FundingCredit]: - if symbol == None: - endpoint = "auth/r/funding/credits" - else: endpoint = f"auth/r/funding/credits/{symbol}" - - return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint) ] - - def get_funding_credits_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingCredit]: - if symbol == None: - endpoint = "auth/r/funding/credits/hist" - else: endpoint = f"auth/r/funding/credits/{symbol}/hist" - - data = { - "start": start, "end": end, - "limit": limit - } - - return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] - - def submit_wallet_transfer(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, float, str]) -> Notification[Transfer]: - data = { - "from": from_wallet, "to": to_wallet, - "currency": currency, "currency_to": currency_to, - "amount": amount - } - - return serializers._Notification[Transfer](serializer=serializers.Transfer).parse(*self._POST("auth/w/transfer", data=data)) - - def submit_wallet_withdraw(self, wallet: str, method: str, address: str, amount: Union[Decimal, float, str]) -> Notification[Withdrawal]: - data = { - "wallet": wallet, "method": method, - "address": address, "amount": amount, - } - - return serializers._Notification[Withdrawal](serializer=serializers.Withdrawal).parse(*self._POST("auth/w/withdraw", data=data)) - - def get_deposit_address(self, wallet: str, method: str, renew: bool = False) -> Notification[DepositAddress]: - data = { - "wallet": wallet, - "method": method, - "renew": int(renew) - } - - return serializers._Notification[DepositAddress](serializer=serializers.DepositAddress).parse(*self._POST("auth/w/deposit/address", data=data)) - - def get_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, float, str]) -> Invoice: - data = { - "wallet": wallet, "currency": currency, - "amount": amount - } - - return serializers.Invoice.parse(*self._POST("auth/w/deposit/invoice", data=data)) - - def get_movements(self, currency: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Movement]: - if currency == None: - endpoint = "auth/r/movements/hist" - else: endpoint = f"auth/r/movements/{currency}/hist" - - data = { - "start": start, "end": end, - "limit": limit - } - - return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] - - def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo: - response = self._POST(f"auth/r/info/margin/{symbol}") - - return serializers.SymbolMarginInfo.parse(*([response[1]] + response[2])) - - def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]: - return [ serializers.SymbolMarginInfo.parse(*([sub_data[1]] + sub_data[2])) for sub_data in self._POST(f"auth/r/info/margin/sym_all") ] - - def get_base_margin_info(self) -> BaseMarginInfo: - return serializers.BaseMarginInfo.parse(*(self._POST(f"auth/r/info/margin/base")[1])) - - def claim_position(self, id: int, amount: Optional[Union[Decimal, float, str]] = None) -> Notification[Claim]: - return serializers._Notification[Claim](serializer=serializers.Claim).parse(*self._POST("auth/w/position/claim", data={ "id": id, "amount": amount })) - - def get_increase_position_info(self, symbol: str, amount: Union[Decimal, float, str]) -> IncreaseInfo: - response = self._POST(f"auth/r/position/increase/info", data={ "symbol": symbol, "amount": amount }) - - return serializers.IncreaseInfo.parse(*( - response[0] + [response[1][0]] + response[1][1] + [response[1][2]] + response[4] + response[5] - )) - - def increase_position(self, symbol: str, amount: Union[Decimal, float, str]) -> Notification[Increase]: - return serializers._Notification[Increase](serializer=serializers.Increase).parse(*self._POST("auth/w/position/increase", data={ "symbol": symbol, "amount": amount })) - - def get_positions_history(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionHistory]: - return [ serializers.PositionHistory.parse(*sub_data) for sub_data in self._POST("auth/r/positions/hist", data={ "start": start, "end": end, "limit": limit }) ] - - def get_positions_snapshot(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionSnapshot]: - return [ serializers.PositionSnapshot.parse(*sub_data) for sub_data in self._POST("auth/r/positions/snap", data={ "start": start, "end": end, "limit": limit }) ] - - def get_positions_audit(self, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionAudit]: - return [ serializers.PositionAudit.parse(*sub_data) for sub_data in self._POST("auth/r/positions/audit", data={ "ids": ids, "start": start, "end": end, "limit": limit }) ] - - def set_derivative_position_collateral(self, symbol: str, collateral: Union[Decimal, float, str]) -> DerivativePositionCollateral: - return serializers.DerivativePositionCollateral.parse(*(self._POST("auth/w/deriv/collateral/set", data={ "symbol": symbol, "collateral": collateral })[0])) - - def get_derivative_position_collateral_limits(self, symbol: str) -> DerivativePositionCollateralLimits: - return serializers.DerivativePositionCollateralLimits.parse(*self._POST("auth/calc/deriv/collateral/limits", data={ "symbol": symbol })) + self.auth = _RestAuthenticatedEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET) \ No newline at end of file diff --git a/bfxapi/rest/_Requests.py b/bfxapi/rest/_Requests.py new file mode 100644 index 0000000..6e4383a --- /dev/null +++ b/bfxapi/rest/_Requests.py @@ -0,0 +1,75 @@ +import time, hmac, hashlib, json, requests + +from http import HTTPStatus +from .enums import Error +from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError + +from .. utils.encoder import JSONEncoder + +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(f"The server replied to the request with a generic error with message: <{data[2]}>.") + + return data + + def _POST(self, endpoint, params = None, data = None, _ignore_authentication_headers = False): + 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 self.API_KEY and self.API_SECRET and _ignore_authentication_headers == False: + headers = { **headers, **self.__build_authentication_headers(endpoint, data) } + + response = requests.post(f"{self.host}/{endpoint}", params=params, data=data, headers=headers) + + if response.status_code == HTTPStatus.NOT_FOUND: + raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") + + data = response.json() + + if len(data) and data[0] == "error": + if data[1] == Error.ERR_PARAMS: + raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") + + if data[1] == Error.ERR_AUTH_FAIL: + raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.") + + if data[1] == None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC: + raise UnknownGenericError(f"The server replied to the request with a generic error with message: <{data[2]}>.") + + return data \ No newline at end of file diff --git a/bfxapi/rest/_RestAuthenticatedEndpoints.py b/bfxapi/rest/_RestAuthenticatedEndpoints.py new file mode 100644 index 0000000..a464b8e --- /dev/null +++ b/bfxapi/rest/_RestAuthenticatedEndpoints.py @@ -0,0 +1,253 @@ +from typing import List, Union, Literal, Optional, Any, cast + +from .types import * + +from . import serializers + +from .enums import Config, Sort, OrderType, FundingOfferType +from decimal import Decimal +from datetime import datetime + +from ._Requests import _Requests + +class _RestAuthenticatedEndpoints(_Requests): + def get_wallets(self) -> List[Wallet]: + return [ serializers.Wallet.parse(*sub_data) for sub_data in self._POST("auth/r/wallets") ] + + def get_orders(self, symbol: Optional[str] = None, ids: Optional[List[str]] = None) -> List[Order]: + endpoint = "auth/r/orders" + + if symbol != None: + endpoint += f"/{symbol}" + + return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, data={ "id": ids }) ] + + def get_positions(self) -> List[Position]: + return [ serializers.Position.parse(*sub_data) for sub_data in self._POST("auth/r/positions") ] + + def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, float, str], + price: Optional[Union[Decimal, float, str]] = None, lev: Optional[int] = None, + price_trailing: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_oco_stop: Optional[Union[Decimal, float, str]] = None, + gid: Optional[int] = None, cid: Optional[int] = None, + flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification[Order]: + 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[Order](serializer=serializers.Order).parse(*self._POST("auth/w/order/submit", data=data)) + + def update_order(self, id: int, amount: Optional[Union[Decimal, float, str]] = None, price: Optional[Union[Decimal, float, str]] = None, + cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, + flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, float, str]] = None, + price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification[Order]: + 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[Order](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[Order]: + data = { + "id": id, + "cid": cid, + "cid_date": cid_date + } + + return serializers._Notification[Order](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[List[Order]]: + data = { + "ids": ids, + "cids": cids, + "gids": gids, + + "all": int(all) + } + + return serializers._Notification[List[Order]](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(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + + def get_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Trade]: + if symbol == None: + endpoint = "auth/r/trades/hist" + else: endpoint = f"auth/r/trades/{symbol}/hist" + + data = { + "sort": sort, + "start": start, "end": end, + "limit": limit + } + + return [ serializers.Trade.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + + def get_order_trades(self, symbol: str, id: int) -> List[OrderTrade]: + return [ serializers.OrderTrade.parse(*sub_data) for sub_data in self._POST(f"auth/r/order/{symbol}:{id}/trades") ] + + def get_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(*sub_data) for sub_data in self._POST(f"auth/r/ledgers/{currency}/hist", data=data) ] + + def get_funding_offers(self, symbol: Optional[str] = None) -> List[FundingOffer]: + endpoint = "auth/r/funding/offers" + + if symbol != None: + endpoint += f"/{symbol}" + + return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint) ] + + def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, float, str], + rate: Union[Decimal, float, str], period: int, + flags: Optional[int] = 0) -> Notification[FundingOffer]: + data = { + "type": type, "symbol": symbol, "amount": amount, + "rate": rate, "period": period, + "flags": flags + } + + return serializers._Notification[FundingOffer](serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/submit", data=data)) + + def cancel_funding_offer(self, id: int) -> Notification[FundingOffer]: + return serializers._Notification[FundingOffer](serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/cancel", data={ "id": id })) + + def cancel_all_funding_offers(self, currency: str) -> Notification: + return serializers._Notification().parse(*self._POST("auth/w/funding/offer/cancel/all", data={ "currency": currency })) + + def get_funding_offers_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingOffer]: + if symbol == None: + endpoint = "auth/r/funding/offers/hist" + else: endpoint = f"auth/r/funding/offers/{symbol}/hist" + + data = { + "start": start, "end": end, + "limit": limit + } + + return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + + def get_funding_credits(self, symbol: Optional[str] = None) -> List[FundingCredit]: + if symbol == None: + endpoint = "auth/r/funding/credits" + else: endpoint = f"auth/r/funding/credits/{symbol}" + + return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint) ] + + def get_funding_credits_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingCredit]: + if symbol == None: + endpoint = "auth/r/funding/credits/hist" + else: endpoint = f"auth/r/funding/credits/{symbol}/hist" + + data = { + "start": start, "end": end, + "limit": limit + } + + return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + + def submit_wallet_transfer(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, float, str]) -> Notification[Transfer]: + data = { + "from": from_wallet, "to": to_wallet, + "currency": currency, "currency_to": currency_to, + "amount": amount + } + + return serializers._Notification[Transfer](serializer=serializers.Transfer).parse(*self._POST("auth/w/transfer", data=data)) + + def submit_wallet_withdraw(self, wallet: str, method: str, address: str, amount: Union[Decimal, float, str]) -> Notification[Withdrawal]: + data = { + "wallet": wallet, "method": method, + "address": address, "amount": amount, + } + + return serializers._Notification[Withdrawal](serializer=serializers.Withdrawal).parse(*self._POST("auth/w/withdraw", data=data)) + + def get_deposit_address(self, wallet: str, method: str, renew: bool = False) -> Notification[DepositAddress]: + data = { + "wallet": wallet, + "method": method, + "renew": int(renew) + } + + return serializers._Notification[DepositAddress](serializer=serializers.DepositAddress).parse(*self._POST("auth/w/deposit/address", data=data)) + + def get_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, float, str]) -> Invoice: + data = { + "wallet": wallet, "currency": currency, + "amount": amount + } + + return serializers.Invoice.parse(*self._POST("auth/w/deposit/invoice", data=data)) + + def get_movements(self, currency: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Movement]: + if currency == None: + endpoint = "auth/r/movements/hist" + else: endpoint = f"auth/r/movements/{currency}/hist" + + data = { + "start": start, "end": end, + "limit": limit + } + + return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + + def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo: + response = self._POST(f"auth/r/info/margin/{symbol}") + + return serializers.SymbolMarginInfo.parse(*([response[1]] + response[2])) + + def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]: + return [ serializers.SymbolMarginInfo.parse(*([sub_data[1]] + sub_data[2])) for sub_data in self._POST(f"auth/r/info/margin/sym_all") ] + + def get_base_margin_info(self) -> BaseMarginInfo: + return serializers.BaseMarginInfo.parse(*(self._POST(f"auth/r/info/margin/base")[1])) + + def claim_position(self, id: int, amount: Optional[Union[Decimal, float, str]] = None) -> Notification[Claim]: + return serializers._Notification[Claim](serializer=serializers.Claim).parse(*self._POST("auth/w/position/claim", data={ "id": id, "amount": amount })) + + def get_increase_position_info(self, symbol: str, amount: Union[Decimal, float, str]) -> IncreaseInfo: + response = self._POST(f"auth/r/position/increase/info", data={ "symbol": symbol, "amount": amount }) + + return serializers.IncreaseInfo.parse(*( + response[0] + [response[1][0]] + response[1][1] + [response[1][2]] + response[4] + response[5] + )) + + def increase_position(self, symbol: str, amount: Union[Decimal, float, str]) -> Notification[Increase]: + return serializers._Notification[Increase](serializer=serializers.Increase).parse(*self._POST("auth/w/position/increase", data={ "symbol": symbol, "amount": amount })) + + def get_positions_history(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionHistory]: + return [ serializers.PositionHistory.parse(*sub_data) for sub_data in self._POST("auth/r/positions/hist", data={ "start": start, "end": end, "limit": limit }) ] + + def get_positions_snapshot(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionSnapshot]: + return [ serializers.PositionSnapshot.parse(*sub_data) for sub_data in self._POST("auth/r/positions/snap", data={ "start": start, "end": end, "limit": limit }) ] + + def get_positions_audit(self, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionAudit]: + return [ serializers.PositionAudit.parse(*sub_data) for sub_data in self._POST("auth/r/positions/audit", data={ "ids": ids, "start": start, "end": end, "limit": limit }) ] + + def set_derivative_position_collateral(self, symbol: str, collateral: Union[Decimal, float, str]) -> DerivativePositionCollateral: + return serializers.DerivativePositionCollateral.parse(*(self._POST("auth/w/deriv/collateral/set", data={ "symbol": symbol, "collateral": collateral })[0])) + + def get_derivative_position_collateral_limits(self, symbol: str) -> DerivativePositionCollateralLimits: + return serializers.DerivativePositionCollateralLimits.parse(*self._POST("auth/calc/deriv/collateral/limits", data={ "symbol": symbol })) \ No newline at end of file diff --git a/bfxapi/rest/_RestPublicEndpoints.py b/bfxapi/rest/_RestPublicEndpoints.py new file mode 100644 index 0000000..5157256 --- /dev/null +++ b/bfxapi/rest/_RestPublicEndpoints.py @@ -0,0 +1,188 @@ +from typing import List, Union, Literal, Optional, Any, cast + +from .types import * + +from . import serializers + +from .enums import Config, Sort +from decimal import Decimal + +from ._Requests import _Requests + +class _RestPublicEndpoints(_Requests): + def conf(self, config: Config) -> Any: + return self._GET(f"conf/{config}")[0] + + def get_platform_status(self) -> PlatformStatus: + return serializers.PlatformStatus.parse(*self._GET("platform/status")) + + def get_tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]: + data = self._GET("tickers", params={ "symbols": ",".join(symbols) }) + + parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse } + + return [ cast(Union[TradingPairTicker, FundingCurrencyTicker], parsers[sub_data[0][0]](*sub_data)) for sub_data in data ] + + def get_t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]: + if isinstance(pairs, str) and pairs == "ALL": + return [ cast(TradingPairTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.symbol).startswith("t") ] + + data = self.get_tickers([ "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, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.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]: + return [ serializers.TickersHistory.parse(*sub_data) for sub_data in self._GET("tickers/hist", params={ + "symbols": ",".join(symbols), + "start": start, "end": end, + "limit": limit + }) ] + + def get_t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[TradingPairTrade]: + params = { "limit": limit, "start": start, "end": end, "sort": sort } + data = self._GET(f"trades/{'t' + pair}/hist", params=params) + return [ serializers.TradingPairTrade.parse(*sub_data) for sub_data in data ] + + def get_f_trades(self, currency: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[FundingCurrencyTrade]: + params = { "limit": limit, "start": start, "end": end, "sort": sort } + data = self._GET(f"trades/{'f' + currency}/hist", params=params) + return [ serializers.FundingCurrencyTrade.parse(*sub_data) for sub_data in data ] + + def get_t_book(self, pair: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairBook]: + return [ serializers.TradingPairBook.parse(*sub_data) for sub_data in self._GET(f"book/{'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(*sub_data) for sub_data 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(*sub_data) for sub_data 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(*sub_data) for sub_data 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(*sub_data) for sub_data in data ] + + def get_stats_last( + self, + resource: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Statistic: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self._GET(f"stats1/{resource}/last", params=params) + return serializers.Statistic.parse(*data) + + def get_candles_hist( + self, + symbol: str, tf: str = "1m", + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> List[Candle]: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self._GET(f"candles/trade:{tf}:{symbol}/hist", params=params) + return [ serializers.Candle.parse(*sub_data) for sub_data in data ] + + def get_candles_last( + self, + symbol: str, tf: str = "1m", + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Candle: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self._GET(f"candles/trade:{tf}:{symbol}/last", params=params) + return serializers.Candle.parse(*data) + + def get_derivatives_status(self, keys: Union[List[str], Literal["ALL"]]) -> List[DerivativesStatus]: + if keys == "ALL": + params = { "keys": "ALL" } + else: params = { "keys": ",".join(keys) } + + data = self._GET(f"status/deriv", params=params) + + return [ serializers.DerivativesStatus.parse(*sub_data) for sub_data in data ] + + def get_derivatives_status_history( + self, + type: str, symbol: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> List[DerivativesStatus]: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self._GET(f"status/{type}/{symbol}/hist", params=params) + return [ serializers.DerivativesStatus.parse(*sub_data, skip=[ "KEY" ]) for sub_data in data ] + + def get_liquidations(self, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Liquidation]: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self._GET("liquidations/hist", params=params) + return [ serializers.Liquidation.parse(*sub_data[0]) for sub_data in data ] + + def get_seed_candles(self, symbol: str, tf: str = '1m', sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Candle]: + params = {"sort": sort, "start": start, "end": end, "limit": limit} + data = self._GET(f"candles/trade:{tf}:{symbol}/hist?limit={limit}&start={start}&end={end}&sort={sort}", params=params) + return [ serializers.Candle.parse(*sub_data) for sub_data in data ] + + def get_leaderboards_hist( + self, + resource: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> List[Leaderboard]: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self._GET(f"rankings/{resource}/hist", params=params) + return [ serializers.Leaderboard.parse(*sub_data) for sub_data in data ] + + def get_leaderboards_last( + self, + resource: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Leaderboard: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self._GET(f"rankings/{resource}/last", params=params) + return serializers.Leaderboard.parse(*data) + + def get_funding_stats(self, symbol: str, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingStatistic]: + params = { "start": start, "end": end, "limit": limit } + data = self._GET(f"funding/stats/{symbol}/hist", params=params) + return [ serializers.FundingStatistic.parse(*sub_data) for sub_data in data ] + + def get_pulse_profile(self, nickname: str) -> PulseProfile: + return serializers.PulseProfile.parse(*self._GET(f"pulse/profile/{nickname}")) + + def get_pulse_history(self, end: Optional[str] = None, limit: Optional[int] = None) -> List[PulseMessage]: + messages = list() + + for subdata in self._GET("pulse/hist", params={ "end": end, "limit": limit }): + subdata[18] = subdata[18][0] + message = serializers.PulseMessage.parse(*subdata) + messages.append(message) + + return messages + + def get_trading_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], price_limit: Optional[Union[Decimal, float, str]] = None) -> TradingMarketAveragePrice: + return serializers.TradingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data={ + "symbol": symbol, "amount": amount, "price_limit": price_limit + })) + + def get_funding_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], period: int, rate_limit: Optional[Union[Decimal, float, str]] = None) -> FundingMarketAveragePrice: + return serializers.FundingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data={ + "symbol": symbol, "amount": amount, "period": period, "rate_limit": rate_limit + })) + + def get_fx_rate(self, ccy1: str, ccy2: str) -> FxRate: + return serializers.FxRate.parse(*self._POST("calc/fx", data={ "ccy1": ccy1, "ccy2": ccy2 })) \ No newline at end of file From 2fc31db7a36ac2ec880487907d5a001791b99cd6 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 27 Jan 2023 17:23:41 +0100 Subject: [PATCH 33/85] Add get_funding_loans and get_funding_loans_history endpoints to _RestAuthenticatedEndpoints.py. --- bfxapi/rest/_RestAuthenticatedEndpoints.py | 19 ++++++++++++++++ bfxapi/rest/serializers.py | 25 ++++++++++++++++++++++ bfxapi/rest/types.py | 20 +++++++++++++++++ examples/rest/get_authenticated_data.py | 2 +- 4 files changed, 65 insertions(+), 1 deletion(-) diff --git a/bfxapi/rest/_RestAuthenticatedEndpoints.py b/bfxapi/rest/_RestAuthenticatedEndpoints.py index a464b8e..61ee64d 100644 --- a/bfxapi/rest/_RestAuthenticatedEndpoints.py +++ b/bfxapi/rest/_RestAuthenticatedEndpoints.py @@ -148,6 +148,25 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + def get_funding_loans(self, symbol: Optional[str] = None) -> List[FundingLoan]: + if symbol == None: + endpoint = "auth/r/funding/loans" + else: endpoint = f"auth/r/funding/loans/{symbol}" + + return [ serializers.FundingLoan.parse(*sub_data) for sub_data in self._POST(endpoint) ] + + def get_funding_loans_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingLoan]: + if symbol == None: + endpoint = "auth/r/funding/loans/hist" + else: endpoint = f"auth/r/funding/loans/{symbol}/hist" + + data = { + "start": start, "end": end, + "limit": limit + } + + return [ serializers.FundingLoan.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + def get_funding_credits(self, symbol: Optional[str] = None) -> List[FundingCredit]: if symbol == None: endpoint = "auth/r/funding/credits" diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index a6cf582..44296d9 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -382,6 +382,31 @@ Ledger = generate_labeler_serializer("Ledger", klass=types.Ledger, labels=[ "description" ]) +FundingLoan = generate_labeler_serializer("FundingLoan", klass=types.FundingLoan, labels=[ + "id", + "symbol", + "side", + "mts_create", + "mts_update", + "amount", + "flags", + "status", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "rate", + "period", + "mts_opening", + "mts_last_payout", + "notify", + "hidden", + "_PLACEHOLDER", + "renew", + "rate_real", + "no_close" +]) + + FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.FundingCredit, labels=[ "id", "symbol", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 4850404..58ce507 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -306,6 +306,26 @@ class Ledger(_Type): balance: float description: str +@dataclass +class FundingLoan(_Type): + id: int + symbol: str + side: int + mts_create: int + mts_update: int + amount: float + flags: int + status: str + rate: float + period: int + mts_opening: int + mts_last_payout: int + notify: int + hidden: int + renew: int + rate_real: float + no_close: int + @dataclass class FundingCredit(_Type): id: int diff --git a/examples/rest/get_authenticated_data.py b/examples/rest/get_authenticated_data.py index 4af5165..2ff1de6 100644 --- a/examples/rest/get_authenticated_data.py +++ b/examples/rest/get_authenticated_data.py @@ -69,7 +69,7 @@ def log_funding_loans(): def log_funding_loans_history(): - loans = bfx.rest.auth.get_funding_loan_history(symbol='fUSD', start=0, end=now) + loans = bfx.rest.auth.get_funding_loans_history(symbol='fUSD', start=0, end=now) print("Funding loan history:") [print(l) for l in loans] From 0dd23b3ba5de9396156994498e5d7fa00730a60a Mon Sep 17 00:00:00 2001 From: itsdeka Date: Mon, 30 Jan 2023 13:12:06 +0100 Subject: [PATCH 34/85] fix example --- examples/rest/create_funding_offer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/rest/create_funding_offer.py b/examples/rest/create_funding_offer.py index 343fa1d..fd10cc8 100644 --- a/examples/rest/create_funding_offer.py +++ b/examples/rest/create_funding_offer.py @@ -23,7 +23,7 @@ notification = bfx.rest.auth.submit_funding_offer( print("Offer notification:", notification) -offers = bfx.rest.auth.get_active_funding_offers(symbol="fUSD") +offers = bfx.rest.auth.get_funding_offers(symbol="fUSD") print("Offers:", offers) From 374739b6561edccf0d96fbbab03193cbbfdd12e5 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Mon, 30 Jan 2023 14:51:57 +0100 Subject: [PATCH 35/85] return taken funding --- bfxapi/rest/_RestAuthenticatedEndpoints.py | 5 +++++ bfxapi/rest/serializers.py | 8 ++++---- bfxapi/rest/types.py | 6 +++--- examples/rest/return_taken_funding.py | 22 ++++++++++++++++++++++ 4 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 examples/rest/return_taken_funding.py diff --git a/bfxapi/rest/_RestAuthenticatedEndpoints.py b/bfxapi/rest/_RestAuthenticatedEndpoints.py index 61ee64d..ce4dc1b 100644 --- a/bfxapi/rest/_RestAuthenticatedEndpoints.py +++ b/bfxapi/rest/_RestAuthenticatedEndpoints.py @@ -186,6 +186,11 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + def submit_funding_close(self, id: int) -> Notification[Literal[None]]: + return serializers._Notification[Literal[None]]().parse( + *self._POST("auth/w/funding/close", data={ "id": id }) + ) + def submit_wallet_transfer(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, float, str]) -> Notification[Transfer]: data = { "from": from_wallet, "to": to_wallet, diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index bde6ac7..2f2a8fd 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -319,8 +319,8 @@ Position = generate_labeler_serializer("Position", klass=types.Position, labels= FundingOffer = generate_labeler_serializer("FundingOffer", klass=types.FundingOffer, labels=[ "id", "symbol", - "mts_created", - "mts_updated", + "mts_create", + "mts_update", "amount", "amount_orig", "offer_type", @@ -477,8 +477,8 @@ Movement = generate_labeler_serializer("Movement", klass=types.Movement, labels= "currency_name", "_PLACEHOLDER", "_PLACEHOLDER", - "mts_started", - "mts_updated", + "mts_start", + "mts_update", "_PLACEHOLDER", "_PLACEHOLDER", "status", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 58ce507..af53672 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -268,7 +268,7 @@ class FundingOffer(_Type): notify: bool hidden: int renew: bool - + @dataclass class Trade(_Type): id: int @@ -383,8 +383,8 @@ class Movement(_Type): id: str currency: str currency_name: str - mts_started: int - mts_updated: int + mts_start: int + mts_update: int status: str amount: int fees: int diff --git a/examples/rest/return_taken_funding.py b/examples/rest/return_taken_funding.py new file mode 100644 index 0000000..ccb0c2b --- /dev/null +++ b/examples/rest/return_taken_funding.py @@ -0,0 +1,22 @@ +# python -c "import examples.rest.return_taken_funding" + +import os + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +loans = bfx.rest.auth.get_funding_loans(symbol="fUSD") + +for loan in loans: + print(f"Loan {loan}") + + notification = bfx.rest.auth.submit_funding_close( + id=loan.id + ) + + print("Funding close notification:", notification) \ No newline at end of file From 0ac14dfeb559ef662975459cb78e9fb76bab2430 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Mon, 30 Jan 2023 15:16:48 +0100 Subject: [PATCH 36/85] funding auto renew --- bfxapi/rest/_RestAuthenticatedEndpoints.py | 7 +++++++ bfxapi/rest/serializers.py | 7 +++++++ bfxapi/rest/types.py | 7 +++++++ examples/rest/funding_auto_renew.py | 21 +++++++++++++++++++++ 4 files changed, 42 insertions(+) create mode 100644 examples/rest/funding_auto_renew.py diff --git a/bfxapi/rest/_RestAuthenticatedEndpoints.py b/bfxapi/rest/_RestAuthenticatedEndpoints.py index ce4dc1b..af01a29 100644 --- a/bfxapi/rest/_RestAuthenticatedEndpoints.py +++ b/bfxapi/rest/_RestAuthenticatedEndpoints.py @@ -191,6 +191,13 @@ class _RestAuthenticatedEndpoints(_Requests): *self._POST("auth/w/funding/close", data={ "id": id }) ) + def submit_funding_toggle_auto_renew(self, status: bool, currency: str, amount: Optional[str] = None, rate: Optional[int] = None, period: Optional[int] = None) -> Notification[FundingAutoRenew]: + return serializers._Notification[FundingAutoRenew](serializer=serializers.FundingAutoRenew).parse(*self._POST("auth/w/funding/auto", data={ + "status": int(status), + "currency": currency, "amount": amount, + "rate": rate, "period": period + })) + def submit_wallet_transfer(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, float, str]) -> Notification[Transfer]: data = { "from": from_wallet, "to": to_wallet, diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 2f2a8fd..6e7103d 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -431,6 +431,13 @@ FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.Funding "position_pair" ]) +FundingAutoRenew = generate_labeler_serializer("FundingAutoRenew", klass=types.FundingAutoRenew, labels=[ + "currency", + "period", + "rate", + "threshold" +]) + Transfer = generate_labeler_serializer("Transfer", klass=types.Transfer, labels=[ "mts", "wallet_from", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index af53672..ce6d360 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -347,6 +347,13 @@ class FundingCredit(_Type): no_close: int position_pair: str +@dataclass +class FundingAutoRenew(_Type): + currency: str + period: int + rate: float + threshold: float + @dataclass class Transfer(_Type): mts: int diff --git a/examples/rest/funding_auto_renew.py b/examples/rest/funding_auto_renew.py new file mode 100644 index 0000000..eec7c47 --- /dev/null +++ b/examples/rest/funding_auto_renew.py @@ -0,0 +1,21 @@ +# python -c "import examples.rest.funding_auto_renew" + +import os + +from bfxapi.client import Client, Constants + +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_toggle_auto_renew( + status=True, + currency="USD", + amount="150", + rate="0", + period=2 +) + +print("Renew toggle notification:", notification) \ No newline at end of file From 01d638cf9cb11556b32b80963292c0b9a5773c93 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Mon, 30 Jan 2023 16:16:18 +0100 Subject: [PATCH 37/85] keep taken funding --- bfxapi/rest/_RestAuthenticatedEndpoints.py | 15 ++++++++++--- examples/rest/keep_taken_funding.py | 26 ++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 examples/rest/keep_taken_funding.py diff --git a/bfxapi/rest/_RestAuthenticatedEndpoints.py b/bfxapi/rest/_RestAuthenticatedEndpoints.py index af01a29..7dc8136 100644 --- a/bfxapi/rest/_RestAuthenticatedEndpoints.py +++ b/bfxapi/rest/_RestAuthenticatedEndpoints.py @@ -133,8 +133,10 @@ class _RestAuthenticatedEndpoints(_Requests): def cancel_funding_offer(self, id: int) -> Notification[FundingOffer]: return serializers._Notification[FundingOffer](serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/cancel", data={ "id": id })) - def cancel_all_funding_offers(self, currency: str) -> Notification: - return serializers._Notification().parse(*self._POST("auth/w/funding/offer/cancel/all", data={ "currency": currency })) + def cancel_all_funding_offers(self, currency: str) -> Notification[Literal[None]]: + return serializers._Notification[Literal[None]](serializer=None).parse( + *self._POST("auth/w/funding/offer/cancel/all", data={ "currency": currency }) + ) def get_funding_offers_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingOffer]: if symbol == None: @@ -187,7 +189,7 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] def submit_funding_close(self, id: int) -> Notification[Literal[None]]: - return serializers._Notification[Literal[None]]().parse( + return serializers._Notification[Literal[None]](serializer=None).parse( *self._POST("auth/w/funding/close", data={ "id": id }) ) @@ -198,6 +200,13 @@ class _RestAuthenticatedEndpoints(_Requests): "rate": rate, "period": period })) + def submit_funding_toggle_keep(self, type: Literal["credit", "loan"], ids: Optional[List[int]] = None, changes: Optional[Dict[int, bool]] = None) -> Notification[Literal[None]]: + return serializers._Notification[Literal[None]](serializer=None).parse(*self._POST("auth/w/funding/keep", data={ + "type": type, + "id": ids, + "changes": changes + })) + def submit_wallet_transfer(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, float, str]) -> Notification[Transfer]: data = { "from": from_wallet, "to": to_wallet, diff --git a/examples/rest/keep_taken_funding.py b/examples/rest/keep_taken_funding.py new file mode 100644 index 0000000..9693f95 --- /dev/null +++ b/examples/rest/keep_taken_funding.py @@ -0,0 +1,26 @@ +# python -c "import examples.rest.keep_taken_funding" + +import os + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +loans = bfx.rest.auth.get_funding_loans(symbol="fUSD") + +for loan in loans: + print(f"Loan {loan}") + + notification = bfx.rest.auth.submit_funding_toggle_keep( + funding_type="loan", + ids=[loan.id], + changes={ + loan.id: 2 # (1 if true, 2 if false) + } + ) + + print("Funding keep notification:", notification) \ No newline at end of file From b9850fa45152fc1a555eb5c16f3054f6e33a6300 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 30 Jan 2023 17:31:07 +0100 Subject: [PATCH 38/85] funding info Co-Authored-By: itsdeka --- bfxapi/rest/_RestAuthenticatedEndpoints.py | 5 +++++ bfxapi/rest/serializers.py | 8 ++++++++ bfxapi/rest/types.py | 8 ++++++++ examples/rest/get_funding_info.py | 13 +++++++++++++ 4 files changed, 34 insertions(+) create mode 100644 examples/rest/get_funding_info.py diff --git a/bfxapi/rest/_RestAuthenticatedEndpoints.py b/bfxapi/rest/_RestAuthenticatedEndpoints.py index 7dc8136..b3a7453 100644 --- a/bfxapi/rest/_RestAuthenticatedEndpoints.py +++ b/bfxapi/rest/_RestAuthenticatedEndpoints.py @@ -188,6 +188,11 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + def get_funding_info(self, key: str) -> FundingInfo: + response = self._POST(f"auth/r/info/funding/{key}") + + return serializers.FundingInfo.parse(*([response[1]] + response[2])) + def submit_funding_close(self, id: int) -> Notification[Literal[None]]: return serializers._Notification[Literal[None]](serializer=None).parse( *self._POST("auth/w/funding/close", data={ "id": id }) diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 6e7103d..56021fe 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -438,6 +438,14 @@ FundingAutoRenew = generate_labeler_serializer("FundingAutoRenew", klass=types.F "threshold" ]) +FundingInfo = generate_labeler_serializer("FundingInfo", klass=types.FundingInfo, labels=[ + "symbol", + "yield_loan", + "yield_lend", + "duration_loan", + "duration_lend" +]) + Transfer = generate_labeler_serializer("Transfer", klass=types.Transfer, labels=[ "mts", "wallet_from", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index ce6d360..7554499 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -354,6 +354,14 @@ class FundingAutoRenew(_Type): rate: float threshold: float +@dataclass() +class FundingInfo(_Type): + symbol: str + yield_loan: float + yield_lend: float + duration_loan: float + duration_lend: float + @dataclass class Transfer(_Type): mts: int diff --git a/examples/rest/get_funding_info.py b/examples/rest/get_funding_info.py new file mode 100644 index 0000000..83d0635 --- /dev/null +++ b/examples/rest/get_funding_info.py @@ -0,0 +1,13 @@ +# python -c "import examples.rest.get_funding_info" + +import os + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +print(bfx.rest.auth.get_funding_info(key="fUSD")) \ No newline at end of file From 0ddbd6e76eac3f2976ca2ef613c1d60dad685bf8 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 30 Jan 2023 17:36:50 +0100 Subject: [PATCH 39/85] funding trades Co-Authored-By: itsdeka --- bfxapi/rest/_RestAuthenticatedEndpoints.py | 13 +++++++++++++ bfxapi/rest/serializers.py | 10 ++++++++++ bfxapi/rest/types.py | 10 ++++++++++ examples/rest/get_funding_trades_history.py | 13 +++++++++++++ 4 files changed, 46 insertions(+) create mode 100644 examples/rest/get_funding_trades_history.py diff --git a/bfxapi/rest/_RestAuthenticatedEndpoints.py b/bfxapi/rest/_RestAuthenticatedEndpoints.py index b3a7453..d30d32f 100644 --- a/bfxapi/rest/_RestAuthenticatedEndpoints.py +++ b/bfxapi/rest/_RestAuthenticatedEndpoints.py @@ -99,6 +99,19 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.Trade.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + def get_funding_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingTrade]: + if symbol == None: + endpoint = "auth/r/funding/trades/hist" + else: endpoint = f"auth/r/funding/trades/{symbol}/hist" + + data = { + "sort": sort, + "start": start, "end": end, + "limit": limit + } + + return [ serializers.FundingTrade.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + def get_order_trades(self, symbol: str, id: int) -> List[OrderTrade]: return [ serializers.OrderTrade.parse(*sub_data) for sub_data in self._POST(f"auth/r/order/{symbol}:{id}/trades") ] diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 56021fe..9526922 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -355,6 +355,16 @@ Trade = generate_labeler_serializer("Trade", klass=types.Trade, labels=[ "cid" ]) +FundingTrade = generate_labeler_serializer("FundingTrade", klass=types.FundingTrade, labels=[ + "id", + "currency", + "mts_create", + "offer_id", + "amount", + "rate", + "period" +]) + OrderTrade = generate_labeler_serializer("OrderTrade", klass=types.OrderTrade, labels=[ "id", "pair", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 7554499..2152b3f 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -284,6 +284,16 @@ class Trade(_Type): fee_currency: str cid: int +@dataclass() +class FundingTrade(_Type): + id: int + currency: str + mts_create: int + offer_id: int + amount: float + rate: float + period: int + @dataclass class OrderTrade(_Type): id: int diff --git a/examples/rest/get_funding_trades_history.py b/examples/rest/get_funding_trades_history.py new file mode 100644 index 0000000..c1cc8e6 --- /dev/null +++ b/examples/rest/get_funding_trades_history.py @@ -0,0 +1,13 @@ +# python -c "import examples.rest.get_funding_trades_history" + +import os + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +print(bfx.rest.auth.get_funding_trades_history()) \ No newline at end of file From 7b313ddcab7b90326a19a66e45e90a1e61fc27ca Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 31 Jan 2023 17:27:14 +0100 Subject: [PATCH 40/85] Merge TradeExecutionUpdate and TradeExecuted serializers and types in websocket subpackage. --- bfxapi/rest/exceptions.py | 1 + bfxapi/websocket/handlers.py | 3 +-- bfxapi/websocket/serializers.py | 17 +---------------- bfxapi/websocket/types.py | 19 +++---------------- 4 files changed, 6 insertions(+), 34 deletions(-) diff --git a/bfxapi/rest/exceptions.py b/bfxapi/rest/exceptions.py index beff7bc..9fbf3a4 100644 --- a/bfxapi/rest/exceptions.py +++ b/bfxapi/rest/exceptions.py @@ -3,6 +3,7 @@ from .. exceptions import BfxBaseException __all__ = [ "BfxRestException", + "ResourceNotFound", "RequestParametersError", "ResourceNotFound", "InvalidAuthenticationCredentials" diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index c56046a..1456937 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -134,8 +134,7 @@ class AuthenticatedChannelsHandler(object): __serializers = { ("os", "on", "ou", "oc",): serializers.Order, ("ps", "pn", "pu", "pc",): serializers.Position, - ("te",): serializers.TradeExecuted, - ("tu",): serializers.TradeExecutionUpdate, + ("te", "tu"): serializers.Trade, ("fos", "fon", "fou", "foc",): serializers.FundingOffer, ("fcs", "fcn", "fcu", "fcc",): serializers.FundingCredit, ("fls", "fln", "flu", "flc",): serializers.FundingLoan, diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index 5991cc9..bee2f7a 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -176,22 +176,7 @@ Position = generate_labeler_serializer("Position", klass=types.Position, labels= "meta" ]) -TradeExecuted = generate_labeler_serializer("TradeExecuted", klass=types.TradeExecuted, labels=[ - "id", - "symbol", - "mts_create", - "order_id", - "exec_amount", - "exec_price", - "order_type", - "order_price", - "maker", - "_PLACEHOLDER", - "_PLACEHOLDER", - "cid" -]) - -TradeExecutionUpdate = generate_labeler_serializer("TradeExecutionUpdate", klass=types.TradeExecutionUpdate, labels=[ +Trade = generate_labeler_serializer("Trade", klass=types.Trade, labels=[ "id", "symbol", "mts_create", diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py index 91bac2a..cc70793 100644 --- a/bfxapi/websocket/types.py +++ b/bfxapi/websocket/types.py @@ -152,7 +152,7 @@ class Position(_Type): meta: JSON @dataclass -class TradeExecuted(_Type): +class Trade(_Type): id: int symbol: str mts_create: int @@ -162,21 +162,8 @@ class TradeExecuted(_Type): order_type: str order_price: float maker:int - cid: int - -@dataclass -class TradeExecutionUpdate(_Type): - id: int - symbol: str - mts_create: int - order_id: int - exec_amount: float - exec_price: float - order_type: str - order_price: float - maker:int - fee: float - fee_currency: str + fee: Optional[float] + fee_currency: Optional[str] cid: int @dataclass From 40a48184dafd46c7f630d007cb23aacc1141405e Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 31 Jan 2023 18:54:15 +0100 Subject: [PATCH 41/85] Fix bugs and differences in namings/types/fields between bfxapi.rest.types, bfxapi.rest.serializers and bfxapi.websocket.types, bfxapi.websocket.serializers. --- bfxapi/rest/serializers.py | 82 ++++++++++++++++----------------- bfxapi/rest/types.py | 70 ++++++++++++++-------------- bfxapi/websocket/handlers.py | 2 +- bfxapi/websocket/serializers.py | 20 ++++---- bfxapi/websocket/types.py | 18 ++++---- 5 files changed, 95 insertions(+), 97 deletions(-) diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 9526922..938aef5 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -248,16 +248,6 @@ FxRate = generate_labeler_serializer("FxRate", klass=types.FxRate, labels=[ #region Serializers definition for Rest Authenticated Endpoints -Wallet = generate_labeler_serializer("Wallet", klass=types.Wallet, labels=[ - "wallet_type", - "currency", - "balance", - "unsettled_interest", - "available_balance", - "last_change", - "trade_details" -]) - Order = generate_labeler_serializer("Order", klass=types.Order, labels=[ "id", "gid", @@ -316,30 +306,6 @@ Position = generate_labeler_serializer("Position", klass=types.Position, labels= "meta" ]) -FundingOffer = generate_labeler_serializer("FundingOffer", klass=types.FundingOffer, labels=[ - "id", - "symbol", - "mts_create", - "mts_update", - "amount", - "amount_orig", - "offer_type", - "_PLACEHOLDER", - "_PLACEHOLDER", - "flags", - "offer_status", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "rate", - "period", - "notify", - "hidden", - "_PLACEHOLDER", - "renew", - "_PLACEHOLDER" -]) - Trade = generate_labeler_serializer("Trade", klass=types.Trade, labels=[ "id", "pair", @@ -392,28 +358,28 @@ Ledger = generate_labeler_serializer("Ledger", klass=types.Ledger, labels=[ "description" ]) -FundingLoan = generate_labeler_serializer("FundingLoan", klass=types.FundingLoan, labels=[ +FundingOffer = generate_labeler_serializer("FundingOffer", klass=types.FundingOffer, labels=[ "id", "symbol", - "side", "mts_create", "mts_update", "amount", + "amount_orig", + "offer_type", + "_PLACEHOLDER", + "_PLACEHOLDER", "flags", - "status", + "offer_status", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", "rate", "period", - "mts_opening", - "mts_last_payout", "notify", "hidden", "_PLACEHOLDER", "renew", - "rate_real", - "no_close" + "_PLACEHOLDER" ]) FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.FundingCredit, labels=[ @@ -441,6 +407,30 @@ FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.Funding "position_pair" ]) +FundingLoan = generate_labeler_serializer("FundingLoan", klass=types.FundingLoan, labels=[ + "id", + "symbol", + "side", + "mts_create", + "mts_update", + "amount", + "flags", + "status", + "rate_type", + "_PLACEHOLDER", + "_PLACEHOLDER", + "rate", + "period", + "mts_opening", + "mts_last_payout", + "notify", + "hidden", + "_PLACEHOLDER", + "renew", + "_PLACEHOLDER", + "no_close" +]) + FundingAutoRenew = generate_labeler_serializer("FundingAutoRenew", klass=types.FundingAutoRenew, labels=[ "currency", "period", @@ -456,6 +446,16 @@ FundingInfo = generate_labeler_serializer("FundingInfo", klass=types.FundingInfo "duration_lend" ]) +Wallet = generate_labeler_serializer("Wallet", klass=types.Wallet, labels=[ + "wallet_type", + "currency", + "balance", + "unsettled_interest", + "available_balance", + "last_change", + "trade_details" +]) + Transfer = generate_labeler_serializer("Transfer", klass=types.Transfer, labels=[ "mts", "wallet_from", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 2152b3f..2330eeb 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -197,16 +197,6 @@ class FxRate(_Type): #region Type hinting for Rest Authenticated Endpoints -@dataclass -class Wallet(_Type): - wallet_type: str - currency: str - balance: float - unsettled_interest: float - available_balance: float - last_change: str - trade_details: JSON - @dataclass class Order(_Type): id: int @@ -252,23 +242,6 @@ class Position(_Type): collateral_min: float meta: JSON -@dataclass -class FundingOffer(_Type): - id: int - symbol: str - mts_create: int - mts_update: int - amount: float - amount_orig: float - offer_type: str - flags: int - offer_status: str - rate: float - period: int - notify: bool - hidden: int - renew: bool - @dataclass class Trade(_Type): id: int @@ -317,24 +290,21 @@ class Ledger(_Type): description: str @dataclass -class FundingLoan(_Type): +class FundingOffer(_Type): id: int symbol: str - side: int mts_create: int mts_update: int amount: float + amount_orig: float + offer_type: str flags: int - status: str + offer_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 @dataclass class FundingCredit(_Type): @@ -346,6 +316,7 @@ class FundingCredit(_Type): amount: float flags: int status: str + rate_type: str rate: float period: int mts_opening: int @@ -353,10 +324,29 @@ class FundingCredit(_Type): notify: int hidden: int renew: int - rate_real: float no_close: int position_pair: str +@dataclass +class FundingLoan(_Type): + id: int + symbol: str + side: int + mts_create: int + mts_update: int + amount: float + flags: int + status: str + rate_type: str + rate: float + period: int + mts_opening: int + mts_last_payout: int + notify: int + hidden: int + renew: int + no_close: int + @dataclass class FundingAutoRenew(_Type): currency: str @@ -372,6 +362,16 @@ class FundingInfo(_Type): duration_loan: float duration_lend: float +@dataclass +class Wallet(_Type): + wallet_type: str + currency: str + balance: float + unsettled_interest: float + available_balance: float + last_change: str + trade_details: JSON + @dataclass class Transfer(_Type): mts: int diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index 1456937..686501b 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -139,7 +139,7 @@ class AuthenticatedChannelsHandler(object): ("fcs", "fcn", "fcu", "fcc",): serializers.FundingCredit, ("fls", "fln", "flu", "flc",): serializers.FundingLoan, ("ws", "wu",): serializers.Wallet, - ("bu",): serializers.BalanceInfo + ("bu",): serializers.Balance } EVENTS = [ diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index bee2f7a..6207f33 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -89,7 +89,7 @@ Candle = generate_labeler_serializer("Candle", klass=types.Candle, labels=[ ]) DerivativesStatus = generate_labeler_serializer("DerivativesStatus", klass=types.DerivativesStatus, labels=[ - "time_ms", + "mts", "_PLACEHOLDER", "deriv_price", "spot_price", @@ -194,15 +194,15 @@ Trade = generate_labeler_serializer("Trade", klass=types.Trade, labels=[ FundingOffer = generate_labeler_serializer("FundingOffer", klass=types.FundingOffer, labels=[ "id", "symbol", - "mts_created", - "mts_updated", + "mts_create", + "mts_update", "amount", "amount_orig", "offer_type", "_PLACEHOLDER", "_PLACEHOLDER", "flags", - "status", + "offer_status", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", @@ -235,7 +235,7 @@ FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.Funding "hidden", "_PLACEHOLDER", "renew", - "rate_real", + "_PLACEHOLDER", "no_close", "position_pair" ]) @@ -260,7 +260,7 @@ FundingLoan = generate_labeler_serializer("FundingLoan", klass=types.FundingLoan "hidden", "_PLACEHOLDER", "renew", - "rate_real", + "_PLACEHOLDER", "no_close" ]) @@ -269,12 +269,12 @@ Wallet = generate_labeler_serializer("Wallet", klass=types.Wallet, labels=[ "currency", "balance", "unsettled_interest", - "balance_available", - "description", - "meta" + "available_balance", + "last_change", + "trade_details" ]) -BalanceInfo = generate_labeler_serializer("BalanceInfo", klass=types.BalanceInfo, labels=[ +Balance = generate_labeler_serializer("Balance", klass=types.Balance, labels=[ "aum", "aum_net", ]) diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py index cc70793..659fd29 100644 --- a/bfxapi/websocket/types.py +++ b/bfxapi/websocket/types.py @@ -90,7 +90,7 @@ class Candle(_Type): @dataclass class DerivativesStatus(_Type): - time_ms: int + mts: int deriv_price: float spot_price: float insurance_fund_balance: float @@ -170,13 +170,13 @@ class Trade(_Type): class FundingOffer(_Type): id: int symbol: str - mts_created: int - mts_updated: int + mts_create: int + mts_update: int amount: float amount_orig: float offer_type: str flags: int - status: str + offer_status: str rate: float period: int notify: int @@ -200,7 +200,6 @@ class FundingCredit(_Type): notify: int hidden: int renew: int - rate_real: float no_close: int position_pair: str @@ -221,7 +220,6 @@ class FundingLoan(_Type): notify: int hidden: int renew: int - rate_real: float no_close: int @dataclass @@ -230,12 +228,12 @@ class Wallet(_Type): currency: str balance: float unsettled_interest: float - balance_available: float - description: str - meta: JSON + available_balance: float + last_change: str + trade_details: JSON @dataclass -class BalanceInfo(_Type): +class Balance(_Type): aum: float aum_net: float From 05784cc8ec8bc6b4aabdcd29e369cca43996744f Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 1 Feb 2023 17:05:25 +0100 Subject: [PATCH 42/85] Add tests subpackage. Add TestRestSerializersAndTypes and TestWebsocketSerializersAndTypes unit tests. Fix consistency bugs between serializers and types. --- bfxapi/labeler.py | 3 +++ bfxapi/rest/serializers.py | 24 +++++++++++++++++-- bfxapi/rest/types.py | 22 ++++++++++++++++- bfxapi/tests/__init__.py | 8 +++++++ .../tests/test_rest_serializers_and_types.py | 23 ++++++++++++++++++ .../test_websocket_serializers_and_types.py | 23 ++++++++++++++++++ bfxapi/websocket/serializers.py | 15 ++++++++++-- bfxapi/websocket/types.py | 12 ++++++++++ 8 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 bfxapi/tests/__init__.py create mode 100644 bfxapi/tests/test_rest_serializers_and_types.py create mode 100644 bfxapi/tests/test_websocket_serializers_and_types.py diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 6201b82..1c2655d 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -28,6 +28,9 @@ class _Serializer(Generic[T]): def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: return cast(T, self.klass(**dict(self._serialize(*values, skip=skip)))) + def get_labels(self) -> List[str]: + return [ label for label in self.__labels if label not in self.__IGNORE ] + class _RecursiveSerializer(_Serializer, Generic[T]): def __init__(self, name: str, klass: Type[_Type], labels: List[str], serializers: Dict[str, _Serializer[Any]], IGNORE: List[str] = ["_PLACEHOLDER"]): super().__init__(name, klass, labels, IGNORE) diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 938aef5..3ef765a 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -4,6 +4,26 @@ from .. labeler import generate_labeler_serializer, generate_recursive_serialize from .. notification import _Notification +__serializers__ = [ + "PlatformStatus", "TradingPairTicker", "FundingCurrencyTicker", + "TickersHistory", "TradingPairTrade", "FundingCurrencyTrade", + "TradingPairBook", "FundingCurrencyBook", "TradingPairRawBook", + "FundingCurrencyRawBook", "Statistic", "Candle", + "DerivativesStatus", "Liquidation", "Leaderboard", + "FundingStatistic", "PulseProfile", "PulseMessage", + "TradingMarketAveragePrice", "FundingMarketAveragePrice", "FxRate", + + "Order", "Position", "Trade", + "FundingTrade", "OrderTrade", "Ledger", + "FundingOffer", "FundingCredit", "FundingLoan", + "FundingAutoRenew", "FundingInfo", "Wallet", + "Transfer", "Withdrawal", "DepositAddress", + "Invoice", "Movement", "SymbolMarginInfo", + "BaseMarginInfo", "Claim", "IncreaseInfo", + "Increase", "PositionHistory", "PositionSnapshot", + "PositionAudit", "DerivativePositionCollateral", "DerivativePositionCollateralLimits", +] + #region Serializers definition for Rest Public Endpoints PlatformStatus = generate_labeler_serializer("PlatformStatus", klass=types.PlatformStatus, labels=[ @@ -308,7 +328,7 @@ Position = generate_labeler_serializer("Position", klass=types.Position, labels= Trade = generate_labeler_serializer("Trade", klass=types.Trade, labels=[ "id", - "pair", + "symbol", "mts_create", "order_id", "exec_amount", @@ -333,7 +353,7 @@ FundingTrade = generate_labeler_serializer("FundingTrade", klass=types.FundingTr OrderTrade = generate_labeler_serializer("OrderTrade", klass=types.OrderTrade, labels=[ "id", - "pair", + "symbol", "mts_create", "order_id", "exec_amount", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 2330eeb..ad28321 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -6,12 +6,32 @@ from .. labeler import _Type from .. notification import Notification from .. utils.encoder import JSON +__types__ = [ + "PlatformStatus", "TradingPairTicker", "FundingCurrencyTicker", + "TickersHistory", "TradingPairTrade", "FundingCurrencyTrade", + "TradingPairBook", "FundingCurrencyBook", "TradingPairRawBook", + "FundingCurrencyRawBook", "Statistic", "Candle", + "DerivativesStatus", "Liquidation", "Leaderboard", + "FundingStatistic", "PulseProfile", "PulseMessage", + "TradingMarketAveragePrice", "FundingMarketAveragePrice", "FxRate", + + "Order", "Position", "Trade", + "FundingTrade", "OrderTrade", "Ledger", + "FundingOffer", "FundingCredit", "FundingLoan", + "FundingAutoRenew", "FundingInfo", "Wallet", + "Transfer", "Withdrawal", "DepositAddress", + "Invoice", "Movement", "SymbolMarginInfo", + "BaseMarginInfo", "Claim", "IncreaseInfo", + "Increase", "PositionHistory", "PositionSnapshot", + "PositionAudit", "DerivativePositionCollateral", "DerivativePositionCollateralLimits", +] + #region Type hinting for Rest Public Endpoints @dataclass class PlatformStatus(_Type): status: int - + @dataclass class TradingPairTicker(_Type): symbol: Optional[str] diff --git a/bfxapi/tests/__init__.py b/bfxapi/tests/__init__.py new file mode 100644 index 0000000..eee78b7 --- /dev/null +++ b/bfxapi/tests/__init__.py @@ -0,0 +1,8 @@ +import unittest +from .test_rest_serializers_and_types import TestRestSerializersAndTypes +from .test_websocket_serializers_and_types import TestWebsocketSerializersAndTypes + +NAME = "tests" + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/bfxapi/tests/test_rest_serializers_and_types.py b/bfxapi/tests/test_rest_serializers_and_types.py new file mode 100644 index 0000000..ff1b427 --- /dev/null +++ b/bfxapi/tests/test_rest_serializers_and_types.py @@ -0,0 +1,23 @@ +import unittest + +from ..rest import serializers, types + +class TestRestSerializersAndTypes(unittest.TestCase): + def test_consistency(self): + __types__ = list(map(types.__dict__.get, types.__types__)) + + for serializer in map(serializers.__dict__.get, serializers.__serializers__): + type = types.__dict__.get(serializer.name) + + __types__.remove(type) + self.assertIsNotNone(type, f"_Serializer <{serializer.name}>: no respective _Type found in bfxapi.rest.types.") + self.assertEqual(serializer.klass, type, f"_Serializer <{serializer.name}>.klass: field does not match with respective _Type in bfxapi.rest.types.") + + self.assertListEqual(serializer.get_labels(), list(type.__annotations__), + f"_Serializer <{serializer.name}> and _Type <{type.__name__}> must have matching labels and fields.") + + for type in __types__: + self.fail(f"_Type <{type.__name__}>: no respective _Serializer found in bfxapi.rest.serializers.") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/bfxapi/tests/test_websocket_serializers_and_types.py b/bfxapi/tests/test_websocket_serializers_and_types.py new file mode 100644 index 0000000..2d34d61 --- /dev/null +++ b/bfxapi/tests/test_websocket_serializers_and_types.py @@ -0,0 +1,23 @@ +import unittest + +from ..websocket import serializers, types + +class TestWebsocketSerializersAndTypes(unittest.TestCase): + def test_consistency(self): + __types__ = list(map(types.__dict__.get, types.__types__)) + + for serializer in map(serializers.__dict__.get, serializers.__serializers__): + type = types.__dict__.get(serializer.name) + + __types__.remove(type) + self.assertIsNotNone(type, f"_Serializer <{serializer.name}>: no respective _Type found in bfxapi.websocket.types.") + self.assertEqual(serializer.klass, type, f"_Serializer <{serializer.name}>.klass: field does not match with respective _Type in bfxapi.websocket.types.") + + self.assertListEqual(serializer.get_labels(), list(type.__annotations__), + f"_Serializer <{serializer.name}> and _Type <{type.__name__}> must have matching labels and fields.") + + for type in __types__: + self.fail(f"_Type <{type.__name__}>: no respective _Serializer found in bfxapi.websocket.serializers.") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index 6207f33..29c28c7 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -4,6 +4,17 @@ from .. labeler import generate_labeler_serializer from .. notification import _Notification +__serializers__ = [ + "TradingPairTicker", "FundingCurrencyTicker", "TradingPairTrade", + "FundingCurrencyTrade", "TradingPairBook", "FundingCurrencyBook", + "TradingPairRawBook", "FundingCurrencyRawBook", "Candle", + "DerivativesStatus", + + "Order", "Position", "Trade", + "FundingOffer", "FundingCredit", "FundingLoan", + "Wallet", "Balance", +] + #region Serializers definition for Websocket Public Channels TradingPairTicker = generate_labeler_serializer("TradingPairTicker", klass=types.TradingPairTicker, labels=[ @@ -32,7 +43,7 @@ FundingCurrencyTicker = generate_labeler_serializer("FundingCurrencyTicker", kla "last_price", "volume", "high", - "low" + "low", "_PLACEHOLDER", "_PLACEHOLDER", "frr_amount_available" @@ -100,7 +111,7 @@ DerivativesStatus = generate_labeler_serializer("DerivativesStatus", klass=types "next_funding_accrued", "next_funding_step", "_PLACEHOLDER", - "current_funding" + "current_funding", "_PLACEHOLDER", "_PLACEHOLDER", "mark_price", diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py index 659fd29..3e0991f 100644 --- a/bfxapi/websocket/types.py +++ b/bfxapi/websocket/types.py @@ -6,6 +6,17 @@ from ..labeler import _Type from ..notification import Notification from .. utils.encoder import JSON +__types__ = [ + "TradingPairTicker", "FundingCurrencyTicker", "TradingPairTrade", + "FundingCurrencyTrade", "TradingPairBook", "FundingCurrencyBook", + "TradingPairRawBook", "FundingCurrencyRawBook", "Candle", + "DerivativesStatus", + + "Order", "Position", "Trade", + "FundingOffer", "FundingCredit", "FundingLoan", + "Wallet", "Balance", +] + #region Type hinting for Websocket Public Channels @dataclass @@ -143,6 +154,7 @@ class Position(_Type): pl_perc: float price_liq: float leverage: float + flag: int position_id: int mts_create: int mts_update: int From 06dc9e1c0a842e5bb67b47606f62870cf1a48d70 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 1 Feb 2023 17:17:38 +0100 Subject: [PATCH 43/85] Remove cid.py and integers.py from bfxapi.utils subpackage. Rename encoder.py file to JSONEncoder.py. Remove IntegerUnderflowError and IntegerOverflowflowError exceptions from bfxapi/exceptions.py. --- bfxapi/exceptions.py | 18 +-------- bfxapi/rest/_Requests.py | 2 +- bfxapi/rest/types.py | 2 +- bfxapi/utils/{encoder.py => JSONEncoder.py} | 0 bfxapi/utils/cid.py | 4 -- bfxapi/utils/integers.py | 43 --------------------- bfxapi/websocket/BfxWebsocketClient.py | 2 +- bfxapi/websocket/types.py | 2 +- 8 files changed, 5 insertions(+), 68 deletions(-) rename bfxapi/utils/{encoder.py => JSONEncoder.py} (100%) delete mode 100644 bfxapi/utils/cid.py delete mode 100644 bfxapi/utils/integers.py diff --git a/bfxapi/exceptions.py b/bfxapi/exceptions.py index 1033837..d876946 100644 --- a/bfxapi/exceptions.py +++ b/bfxapi/exceptions.py @@ -1,9 +1,7 @@ __all__ = [ "BfxBaseException", - + "LabelerSerializerException", - "IntegerUnderflowError", - "IntegerOverflowflowError" ] class BfxBaseException(Exception): @@ -18,18 +16,4 @@ class LabelerSerializerException(BfxBaseException): This exception indicates an error thrown by the _Serializer class in bfxapi/labeler.py. """ - pass - -class IntegerUnderflowError(BfxBaseException): - """ - This error indicates an underflow in one of the integer types defined in bfxapi/utils/integers.py. - """ - - pass - -class IntegerOverflowflowError(BfxBaseException): - """ - This error indicates an overflow in one of the integer types defined in bfxapi/utils/integers.py. - """ - pass \ No newline at end of file diff --git a/bfxapi/rest/_Requests.py b/bfxapi/rest/_Requests.py index 6e4383a..8103d03 100644 --- a/bfxapi/rest/_Requests.py +++ b/bfxapi/rest/_Requests.py @@ -4,7 +4,7 @@ from http import HTTPStatus from .enums import Error from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError -from .. utils.encoder import JSONEncoder +from ..utils.JSONEncoder import JSONEncoder class _Requests(object): def __init__(self, host, API_KEY = None, API_SECRET = None): diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index ad28321..084bcc5 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from .. labeler import _Type from .. notification import Notification -from .. utils.encoder import JSON +from ..utils.JSONEncoder import JSON __types__ = [ "PlatformStatus", "TradingPairTicker", "FundingCurrencyTicker", diff --git a/bfxapi/utils/encoder.py b/bfxapi/utils/JSONEncoder.py similarity index 100% rename from bfxapi/utils/encoder.py rename to bfxapi/utils/JSONEncoder.py diff --git a/bfxapi/utils/cid.py b/bfxapi/utils/cid.py deleted file mode 100644 index 43150bb..0000000 --- a/bfxapi/utils/cid.py +++ /dev/null @@ -1,4 +0,0 @@ -import time - -def generate_unique_cid(multiplier: int = 1000) -> int: - return int(round(time.time() * multiplier)) diff --git a/bfxapi/utils/integers.py b/bfxapi/utils/integers.py deleted file mode 100644 index 08582c6..0000000 --- a/bfxapi/utils/integers.py +++ /dev/null @@ -1,43 +0,0 @@ -from typing import cast, TypeVar, Union - -from .. exceptions import IntegerUnderflowError, IntegerOverflowflowError - -__all__ = [ "Int16", "Int32", "Int45", "Int64" ] - -T = TypeVar("T") - -class _Int(int): - def __new__(cls: T, integer: int) -> T: - assert hasattr(cls, "_BITS"), "_Int must be extended by a class that has a static member _BITS (indicating the number of bits with which to represent the integers)." - - bits = cls._BITS - 1 - - min, max = -(2 ** bits), (2 ** bits) - 1 - - if integer < min: - raise IntegerUnderflowError(f"Underflow. Cannot store <{integer}> in {cls._BITS} bits integer. The min and max bounds are {min} and {max}.") - - if integer > max: - raise IntegerOverflowflowError(f"Overflow. Cannot store <{integer}> in {cls._BITS} bits integer. The min and max bounds are {min} and {max}.") - - return cast(T, super().__new__(int, integer)) - -class Int16(_Int): - _BITS = 16 - -int16 = Union[Int16, int] - -class Int32(_Int): - _BITS = 32 - -int32 = Union[Int32, int] - -class Int45(_Int): - _BITS = 45 - -int45 = Union[Int45, int] - -class Int64(_Int): - _BITS = 64 - -int64 = Union[Int64, int] \ No newline at end of file diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index e5c7efb..3bdb3d9 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -10,7 +10,7 @@ from ._BfxWebsocketInputs import _BfxWebsocketInputs from .handlers import Channels, PublicChannelsHandler, AuthenticatedChannelsHandler from .exceptions import WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported -from ..utils.encoder import JSONEncoder +from ..utils.JSONEncoder import JSONEncoder from ..utils.logger import Formatter, CustomLogger diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py index 3e0991f..d1c8ab4 100644 --- a/bfxapi/websocket/types.py +++ b/bfxapi/websocket/types.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from ..labeler import _Type from ..notification import Notification -from .. utils.encoder import JSON +from ..utils.JSONEncoder import JSON __types__ = [ "TradingPairTicker", "FundingCurrencyTicker", "TradingPairTrade", From 0dd6fa9dbbe3defdf040d819d7eefc5c2a338772 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 1 Feb 2023 18:00:00 +0100 Subject: [PATCH 44/85] Reorder and rename some method inside _RestAuthenticatedEndpoints class. --- bfxapi/rest/_RestAuthenticatedEndpoints.py | 172 +++++++++++---------- bfxapi/rest/serializers.py | 10 +- bfxapi/rest/types.py | 10 +- examples/rest/claim_position.py | 2 +- examples/rest/funding_auto_renew.py | 2 +- examples/rest/keep_taken_funding.py | 2 +- examples/rest/transfer_wallet.py | 6 +- 7 files changed, 103 insertions(+), 101 deletions(-) diff --git a/bfxapi/rest/_RestAuthenticatedEndpoints.py b/bfxapi/rest/_RestAuthenticatedEndpoints.py index d30d32f..6113e9c 100644 --- a/bfxapi/rest/_RestAuthenticatedEndpoints.py +++ b/bfxapi/rest/_RestAuthenticatedEndpoints.py @@ -22,9 +22,6 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, data={ "id": ids }) ] - def get_positions(self) -> List[Position]: - return [ serializers.Position.parse(*sub_data) for sub_data in self._POST("auth/r/positions") ] - def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, float, str], price: Optional[Union[Decimal, float, str]] = None, lev: Optional[int] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_oco_stop: Optional[Union[Decimal, float, str]] = None, @@ -86,6 +83,9 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + def get_order_trades(self, symbol: str, id: int) -> List[OrderTrade]: + return [ serializers.OrderTrade.parse(*sub_data) for sub_data in self._POST(f"auth/r/order/{symbol}:{id}/trades") ] + def get_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Trade]: if symbol == None: endpoint = "auth/r/trades/hist" @@ -99,22 +99,6 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.Trade.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] - def get_funding_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingTrade]: - if symbol == None: - endpoint = "auth/r/funding/trades/hist" - else: endpoint = f"auth/r/funding/trades/{symbol}/hist" - - data = { - "sort": sort, - "start": start, "end": end, - "limit": limit - } - - return [ serializers.FundingTrade.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] - - def get_order_trades(self, symbol: str, id: int) -> List[OrderTrade]: - return [ serializers.OrderTrade.parse(*sub_data) for sub_data in self._POST(f"auth/r/order/{symbol}:{id}/trades") ] - def get_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, @@ -124,6 +108,52 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.Ledger.parse(*sub_data) for sub_data in self._POST(f"auth/r/ledgers/{currency}/hist", data=data) ] + def get_base_margin_info(self) -> BaseMarginInfo: + return serializers.BaseMarginInfo.parse(*(self._POST(f"auth/r/info/margin/base")[1])) + + def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo: + response = self._POST(f"auth/r/info/margin/{symbol}") + + return serializers.SymbolMarginInfo.parse(*([response[1]] + response[2])) + + def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]: + return [ serializers.SymbolMarginInfo.parse(*([sub_data[1]] + sub_data[2])) for sub_data in self._POST(f"auth/r/info/margin/sym_all") ] + + def get_positions(self) -> List[Position]: + return [ serializers.Position.parse(*sub_data) for sub_data in self._POST("auth/r/positions") ] + + def claim_position(self, id: int, amount: Optional[Union[Decimal, float, str]] = None) -> Notification[PositionClaim]: + return serializers._Notification[PositionClaim](serializer=serializers.PositionClaim).parse( + *self._POST("auth/w/position/claim", data={ "id": id, "amount": amount }) + ) + + def increase_position(self, symbol: str, amount: Union[Decimal, float, str]) -> Notification[PositionIncrease]: + return serializers._Notification[PositionIncrease](serializer=serializers.PositionIncrease).parse( + *self._POST("auth/w/position/increase", data={ "symbol": symbol, "amount": amount }) + ) + + def get_increase_position_info(self, symbol: str, amount: Union[Decimal, float, str]) -> PositionIncreaseInfo: + response = self._POST(f"auth/r/position/increase/info", data={ "symbol": symbol, "amount": amount }) + + return serializers.PositionIncreaseInfo.parse(*( + response[0] + [response[1][0]] + response[1][1] + [response[1][2]] + response[4] + response[5] + )) + + def get_positions_history(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionHistory]: + return [ serializers.PositionHistory.parse(*sub_data) for sub_data in self._POST("auth/r/positions/hist", data={ "start": start, "end": end, "limit": limit }) ] + + def get_positions_snapshot(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionSnapshot]: + return [ serializers.PositionSnapshot.parse(*sub_data) for sub_data in self._POST("auth/r/positions/snap", data={ "start": start, "end": end, "limit": limit }) ] + + def get_positions_audit(self, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionAudit]: + return [ serializers.PositionAudit.parse(*sub_data) for sub_data in self._POST("auth/r/positions/audit", data={ "ids": ids, "start": start, "end": end, "limit": limit }) ] + + def set_derivative_position_collateral(self, symbol: str, collateral: Union[Decimal, float, str]) -> DerivativePositionCollateral: + return serializers.DerivativePositionCollateral.parse(*(self._POST("auth/w/deriv/collateral/set", data={ "symbol": symbol, "collateral": collateral })[0])) + + def get_derivative_position_collateral_limits(self, symbol: str) -> DerivativePositionCollateralLimits: + return serializers.DerivativePositionCollateralLimits.parse(*self._POST("auth/calc/deriv/collateral/limits", data={ "symbol": symbol })) + def get_funding_offers(self, symbol: Optional[str] = None) -> List[FundingOffer]: endpoint = "auth/r/funding/offers" @@ -151,6 +181,25 @@ class _RestAuthenticatedEndpoints(_Requests): *self._POST("auth/w/funding/offer/cancel/all", data={ "currency": currency }) ) + def submit_funding_close(self, id: int) -> Notification[Literal[None]]: + return serializers._Notification[Literal[None]](serializer=None).parse( + *self._POST("auth/w/funding/close", data={ "id": id }) + ) + + def toggle_auto_renew(self, status: bool, currency: str, amount: Optional[str] = None, rate: Optional[int] = None, period: Optional[int] = None) -> Notification[FundingAutoRenew]: + return serializers._Notification[FundingAutoRenew](serializer=serializers.FundingAutoRenew).parse(*self._POST("auth/w/funding/auto", data={ + "status": int(status), + "currency": currency, "amount": amount, + "rate": rate, "period": period + })) + + def toggle_keep(self, type: Literal["credit", "loan"], ids: Optional[List[int]] = None, changes: Optional[Dict[int, bool]] = None) -> Notification[Literal[None]]: + return serializers._Notification[Literal[None]](serializer=None).parse(*self._POST("auth/w/funding/keep", data={ + "type": type, + "id": ids, + "changes": changes + })) + def get_funding_offers_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingOffer]: if symbol == None: endpoint = "auth/r/funding/offers/hist" @@ -201,31 +250,25 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + def get_funding_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingTrade]: + if symbol == None: + endpoint = "auth/r/funding/trades/hist" + else: endpoint = f"auth/r/funding/trades/{symbol}/hist" + + data = { + "sort": sort, + "start": start, "end": end, + "limit": limit + } + + return [ serializers.FundingTrade.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + def get_funding_info(self, key: str) -> FundingInfo: response = self._POST(f"auth/r/info/funding/{key}") return serializers.FundingInfo.parse(*([response[1]] + response[2])) - def submit_funding_close(self, id: int) -> Notification[Literal[None]]: - return serializers._Notification[Literal[None]](serializer=None).parse( - *self._POST("auth/w/funding/close", data={ "id": id }) - ) - - def submit_funding_toggle_auto_renew(self, status: bool, currency: str, amount: Optional[str] = None, rate: Optional[int] = None, period: Optional[int] = None) -> Notification[FundingAutoRenew]: - return serializers._Notification[FundingAutoRenew](serializer=serializers.FundingAutoRenew).parse(*self._POST("auth/w/funding/auto", data={ - "status": int(status), - "currency": currency, "amount": amount, - "rate": rate, "period": period - })) - - def submit_funding_toggle_keep(self, type: Literal["credit", "loan"], ids: Optional[List[int]] = None, changes: Optional[Dict[int, bool]] = None) -> Notification[Literal[None]]: - return serializers._Notification[Literal[None]](serializer=None).parse(*self._POST("auth/w/funding/keep", data={ - "type": type, - "id": ids, - "changes": changes - })) - - def submit_wallet_transfer(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, float, str]) -> Notification[Transfer]: + def transfer_between_wallets(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, float, str]) -> Notification[Transfer]: data = { "from": from_wallet, "to": to_wallet, "currency": currency, "currency_to": currency_to, @@ -234,13 +277,11 @@ class _RestAuthenticatedEndpoints(_Requests): return serializers._Notification[Transfer](serializer=serializers.Transfer).parse(*self._POST("auth/w/transfer", data=data)) - def submit_wallet_withdraw(self, wallet: str, method: str, address: str, amount: Union[Decimal, float, str]) -> Notification[Withdrawal]: - data = { + def submit_wallet_withdrawal(self, wallet: str, method: str, address: str, amount: Union[Decimal, float, str]) -> Notification[Withdrawal]: + return serializers._Notification[Withdrawal](serializer=serializers.Withdrawal).parse(*self._POST("auth/w/withdraw", data={ "wallet": wallet, "method": method, "address": address, "amount": amount, - } - - return serializers._Notification[Withdrawal](serializer=serializers.Withdrawal).parse(*self._POST("auth/w/withdraw", data=data)) + })) def get_deposit_address(self, wallet: str, method: str, renew: bool = False) -> Notification[DepositAddress]: data = { @@ -251,7 +292,7 @@ class _RestAuthenticatedEndpoints(_Requests): return serializers._Notification[DepositAddress](serializer=serializers.DepositAddress).parse(*self._POST("auth/w/deposit/address", data=data)) - def get_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, float, str]) -> Invoice: + def generate_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, float, str]) -> Invoice: data = { "wallet": wallet, "currency": currency, "amount": amount @@ -269,43 +310,4 @@ class _RestAuthenticatedEndpoints(_Requests): "limit": limit } - return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] - - def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo: - response = self._POST(f"auth/r/info/margin/{symbol}") - - return serializers.SymbolMarginInfo.parse(*([response[1]] + response[2])) - - def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]: - return [ serializers.SymbolMarginInfo.parse(*([sub_data[1]] + sub_data[2])) for sub_data in self._POST(f"auth/r/info/margin/sym_all") ] - - def get_base_margin_info(self) -> BaseMarginInfo: - return serializers.BaseMarginInfo.parse(*(self._POST(f"auth/r/info/margin/base")[1])) - - def claim_position(self, id: int, amount: Optional[Union[Decimal, float, str]] = None) -> Notification[Claim]: - return serializers._Notification[Claim](serializer=serializers.Claim).parse(*self._POST("auth/w/position/claim", data={ "id": id, "amount": amount })) - - def get_increase_position_info(self, symbol: str, amount: Union[Decimal, float, str]) -> IncreaseInfo: - response = self._POST(f"auth/r/position/increase/info", data={ "symbol": symbol, "amount": amount }) - - return serializers.IncreaseInfo.parse(*( - response[0] + [response[1][0]] + response[1][1] + [response[1][2]] + response[4] + response[5] - )) - - def increase_position(self, symbol: str, amount: Union[Decimal, float, str]) -> Notification[Increase]: - return serializers._Notification[Increase](serializer=serializers.Increase).parse(*self._POST("auth/w/position/increase", data={ "symbol": symbol, "amount": amount })) - - def get_positions_history(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionHistory]: - return [ serializers.PositionHistory.parse(*sub_data) for sub_data in self._POST("auth/r/positions/hist", data={ "start": start, "end": end, "limit": limit }) ] - - def get_positions_snapshot(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionSnapshot]: - return [ serializers.PositionSnapshot.parse(*sub_data) for sub_data in self._POST("auth/r/positions/snap", data={ "start": start, "end": end, "limit": limit }) ] - - def get_positions_audit(self, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionAudit]: - return [ serializers.PositionAudit.parse(*sub_data) for sub_data in self._POST("auth/r/positions/audit", data={ "ids": ids, "start": start, "end": end, "limit": limit }) ] - - def set_derivative_position_collateral(self, symbol: str, collateral: Union[Decimal, float, str]) -> DerivativePositionCollateral: - return serializers.DerivativePositionCollateral.parse(*(self._POST("auth/w/deriv/collateral/set", data={ "symbol": symbol, "collateral": collateral })[0])) - - def get_derivative_position_collateral_limits(self, symbol: str) -> DerivativePositionCollateralLimits: - return serializers.DerivativePositionCollateralLimits.parse(*self._POST("auth/calc/deriv/collateral/limits", data={ "symbol": symbol })) \ No newline at end of file + return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 3ef765a..021211c 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -19,8 +19,8 @@ __serializers__ = [ "FundingAutoRenew", "FundingInfo", "Wallet", "Transfer", "Withdrawal", "DepositAddress", "Invoice", "Movement", "SymbolMarginInfo", - "BaseMarginInfo", "Claim", "IncreaseInfo", - "Increase", "PositionHistory", "PositionSnapshot", + "BaseMarginInfo", "PositionClaim", "PositionIncreaseInfo", + "PositionIncrease", "PositionHistory", "PositionSnapshot", "PositionAudit", "DerivativePositionCollateral", "DerivativePositionCollateralLimits", ] @@ -557,7 +557,7 @@ BaseMarginInfo = generate_labeler_serializer("BaseMarginInfo", klass=types.BaseM "margin_min" ]) -Claim = generate_labeler_serializer("Claim", klass=types.Claim, labels=[ +PositionClaim = generate_labeler_serializer("PositionClaim", klass=types.PositionClaim, labels=[ "symbol", "position_status", "amount", @@ -580,7 +580,7 @@ Claim = generate_labeler_serializer("Claim", klass=types.Claim, labels=[ "meta" ]) -IncreaseInfo = generate_labeler_serializer("IncreaseInfo", klass=types.IncreaseInfo, labels=[ +PositionIncreaseInfo = generate_labeler_serializer("PositionIncreaseInfo", klass=types.PositionIncreaseInfo, labels=[ "max_pos", "current_pos", "base_currency_balance", @@ -599,7 +599,7 @@ IncreaseInfo = generate_labeler_serializer("IncreaseInfo", klass=types.IncreaseI "funding_required_currency" ]) -Increase = generate_labeler_serializer("Increase", klass=types.Increase, labels=[ +PositionIncrease = generate_labeler_serializer("PositionIncrease", klass=types.PositionIncrease, labels=[ "symbol", "_PLACEHOLDER", "amount", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 084bcc5..431778a 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -21,8 +21,8 @@ __types__ = [ "FundingAutoRenew", "FundingInfo", "Wallet", "Transfer", "Withdrawal", "DepositAddress", "Invoice", "Movement", "SymbolMarginInfo", - "BaseMarginInfo", "Claim", "IncreaseInfo", - "Increase", "PositionHistory", "PositionSnapshot", + "BaseMarginInfo", "PositionClaim", "PositionIncreaseInfo", + "PositionIncrease", "PositionHistory", "PositionSnapshot", "PositionAudit", "DerivativePositionCollateral", "DerivativePositionCollateralLimits", ] @@ -454,7 +454,7 @@ class BaseMarginInfo(_Type): margin_min: float @dataclass -class Claim(_Type): +class PositionClaim(_Type): symbol: str position_status: str amount: float @@ -470,7 +470,7 @@ class Claim(_Type): meta: JSON @dataclass -class IncreaseInfo(_Type): +class PositionIncreaseInfo(_Type): max_pos: int current_pos: float base_currency_balance: float @@ -485,7 +485,7 @@ class IncreaseInfo(_Type): funding_required_currency: str @dataclass -class Increase(_Type): +class PositionIncrease(_Type): symbol: str amount: float base_price: float diff --git a/examples/rest/claim_position.py b/examples/rest/claim_position.py index caf5e23..ba3e4e0 100644 --- a/examples/rest/claim_position.py +++ b/examples/rest/claim_position.py @@ -16,4 +16,4 @@ open_margin_positions = bfx.rest.auth.get_positions() for position in open_margin_positions: print(f"Position {position}") claim = bfx.rest.auth.claim_position(position.position_id, amount=0.000001) - print(f"Claim {claim.notify_info}") \ No newline at end of file + print(f"PositionClaim {claim.notify_info}") \ No newline at end of file diff --git a/examples/rest/funding_auto_renew.py b/examples/rest/funding_auto_renew.py index eec7c47..c892ce3 100644 --- a/examples/rest/funding_auto_renew.py +++ b/examples/rest/funding_auto_renew.py @@ -10,7 +10,7 @@ bfx = Client( API_SECRET=os.getenv("BFX_API_SECRET") ) -notification = bfx.rest.auth.submit_funding_toggle_auto_renew( +notification = bfx.rest.auth.toggle_auto_renew( status=True, currency="USD", amount="150", diff --git a/examples/rest/keep_taken_funding.py b/examples/rest/keep_taken_funding.py index 9693f95..1314ffa 100644 --- a/examples/rest/keep_taken_funding.py +++ b/examples/rest/keep_taken_funding.py @@ -15,7 +15,7 @@ loans = bfx.rest.auth.get_funding_loans(symbol="fUSD") for loan in loans: print(f"Loan {loan}") - notification = bfx.rest.auth.submit_funding_toggle_keep( + notification = bfx.rest.auth.toggle_keep( funding_type="loan", ids=[loan.id], changes={ diff --git a/examples/rest/transfer_wallet.py b/examples/rest/transfer_wallet.py index e986bfd..8de15fd 100644 --- a/examples/rest/transfer_wallet.py +++ b/examples/rest/transfer_wallet.py @@ -11,7 +11,7 @@ bfx = Client( ) def transfer_wallet(): - response = bfx.rest.auth.submit_wallet_transfer(from_wallet="exchange", to_wallet="funding", from_currency="ETH", to_currency="ETH", amount=0.001) + response = bfx.rest.auth.transfer_between_wallets(from_wallet="exchange", to_wallet="funding", from_currency="ETH", to_currency="ETH", amount=0.001) print("Transfer:", response.notify_info) def get_existing_deposit_address(): @@ -24,11 +24,11 @@ def create_new_deposit_address(): def withdraw(): # tetheruse = Tether (ERC20) - response = bfx.rest.auth.submit_wallet_withdraw(wallet="exchange", method="tetheruse", amount=1, address="0x742d35Cc6634C0532925a3b844Bc454e4438f44e") + response = bfx.rest.auth.submit_wallet_withdrawal(wallet="exchange", method="tetheruse", amount=1, address="0x742d35Cc6634C0532925a3b844Bc454e4438f44e") print("Address:", response.notify_info) def create_lighting_network_deposit_address(): - invoice = bfx.rest.auth.get_deposit_invoice(wallet="funding", currency="LNX", amount=0.001) + invoice = bfx.rest.auth.generate_deposit_invoice(wallet="funding", currency="LNX", amount=0.001) print("Invoice:", invoice) def get_movements(): From 929ae62d2f85f83b0079c5a24dc4a321f320c954 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 6 Feb 2023 16:16:11 +0100 Subject: [PATCH 45/85] Fix bug in bfxapi.tests sub-package. --- bfxapi/rest/types.py | 20 ------------------- bfxapi/tests/__init__.py | 8 +++++++- .../tests/test_rest_serializers_and_types.py | 6 ------ .../test_websocket_serializers_and_types.py | 8 +------- bfxapi/websocket/types.py | 11 ---------- 5 files changed, 8 insertions(+), 45 deletions(-) diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 431778a..ab41fec 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -6,26 +6,6 @@ from .. labeler import _Type from .. notification import Notification from ..utils.JSONEncoder import JSON -__types__ = [ - "PlatformStatus", "TradingPairTicker", "FundingCurrencyTicker", - "TickersHistory", "TradingPairTrade", "FundingCurrencyTrade", - "TradingPairBook", "FundingCurrencyBook", "TradingPairRawBook", - "FundingCurrencyRawBook", "Statistic", "Candle", - "DerivativesStatus", "Liquidation", "Leaderboard", - "FundingStatistic", "PulseProfile", "PulseMessage", - "TradingMarketAveragePrice", "FundingMarketAveragePrice", "FxRate", - - "Order", "Position", "Trade", - "FundingTrade", "OrderTrade", "Ledger", - "FundingOffer", "FundingCredit", "FundingLoan", - "FundingAutoRenew", "FundingInfo", "Wallet", - "Transfer", "Withdrawal", "DepositAddress", - "Invoice", "Movement", "SymbolMarginInfo", - "BaseMarginInfo", "PositionClaim", "PositionIncreaseInfo", - "PositionIncrease", "PositionHistory", "PositionSnapshot", - "PositionAudit", "DerivativePositionCollateral", "DerivativePositionCollateralLimits", -] - #region Type hinting for Rest Public Endpoints @dataclass diff --git a/bfxapi/tests/__init__.py b/bfxapi/tests/__init__.py index eee78b7..bbf78c0 100644 --- a/bfxapi/tests/__init__.py +++ b/bfxapi/tests/__init__.py @@ -4,5 +4,11 @@ from .test_websocket_serializers_and_types import TestWebsocketSerializersAndTyp NAME = "tests" +def suite(): + return unittest.TestSuite([ + unittest.makeSuite(TestRestSerializersAndTypes), + unittest.makeSuite(TestWebsocketSerializersAndTypes), + ]) + if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.TextTestRunner().run(suite()) \ No newline at end of file diff --git a/bfxapi/tests/test_rest_serializers_and_types.py b/bfxapi/tests/test_rest_serializers_and_types.py index ff1b427..7bc7242 100644 --- a/bfxapi/tests/test_rest_serializers_and_types.py +++ b/bfxapi/tests/test_rest_serializers_and_types.py @@ -4,20 +4,14 @@ from ..rest import serializers, types class TestRestSerializersAndTypes(unittest.TestCase): def test_consistency(self): - __types__ = list(map(types.__dict__.get, types.__types__)) - for serializer in map(serializers.__dict__.get, serializers.__serializers__): type = types.__dict__.get(serializer.name) - __types__.remove(type) self.assertIsNotNone(type, f"_Serializer <{serializer.name}>: no respective _Type found in bfxapi.rest.types.") self.assertEqual(serializer.klass, type, f"_Serializer <{serializer.name}>.klass: field does not match with respective _Type in bfxapi.rest.types.") self.assertListEqual(serializer.get_labels(), list(type.__annotations__), f"_Serializer <{serializer.name}> and _Type <{type.__name__}> must have matching labels and fields.") - for type in __types__: - self.fail(f"_Type <{type.__name__}>: no respective _Serializer found in bfxapi.rest.serializers.") - if __name__ == "__main__": unittest.main() \ No newline at end of file diff --git a/bfxapi/tests/test_websocket_serializers_and_types.py b/bfxapi/tests/test_websocket_serializers_and_types.py index 2d34d61..338b959 100644 --- a/bfxapi/tests/test_websocket_serializers_and_types.py +++ b/bfxapi/tests/test_websocket_serializers_and_types.py @@ -4,20 +4,14 @@ from ..websocket import serializers, types class TestWebsocketSerializersAndTypes(unittest.TestCase): def test_consistency(self): - __types__ = list(map(types.__dict__.get, types.__types__)) - for serializer in map(serializers.__dict__.get, serializers.__serializers__): type = types.__dict__.get(serializer.name) - - __types__.remove(type) + self.assertIsNotNone(type, f"_Serializer <{serializer.name}>: no respective _Type found in bfxapi.websocket.types.") self.assertEqual(serializer.klass, type, f"_Serializer <{serializer.name}>.klass: field does not match with respective _Type in bfxapi.websocket.types.") self.assertListEqual(serializer.get_labels(), list(type.__annotations__), f"_Serializer <{serializer.name}> and _Type <{type.__name__}> must have matching labels and fields.") - for type in __types__: - self.fail(f"_Type <{type.__name__}>: no respective _Serializer found in bfxapi.websocket.serializers.") - if __name__ == "__main__": unittest.main() \ No newline at end of file diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py index d1c8ab4..0ffa870 100644 --- a/bfxapi/websocket/types.py +++ b/bfxapi/websocket/types.py @@ -6,17 +6,6 @@ from ..labeler import _Type from ..notification import Notification from ..utils.JSONEncoder import JSON -__types__ = [ - "TradingPairTicker", "FundingCurrencyTicker", "TradingPairTrade", - "FundingCurrencyTrade", "TradingPairBook", "FundingCurrencyBook", - "TradingPairRawBook", "FundingCurrencyRawBook", "Candle", - "DerivativesStatus", - - "Order", "Position", "Trade", - "FundingOffer", "FundingCredit", "FundingLoan", - "Wallet", "Balance", -] - #region Type hinting for Websocket Public Channels @dataclass From c588d9f20ca6d178406597b30070813e220c9396 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 6 Feb 2023 19:15:58 +0100 Subject: [PATCH 46/85] Rewrite bfxapi/rest/_Requests.py with type hinting. Add None values erasement in bfxapi/utils/JSONEncoder.py. Update code with new improvements. --- bfxapi/notification.py | 8 +- bfxapi/rest/_Requests.py | 27 ++++-- bfxapi/rest/_RestAuthenticatedEndpoints.py | 102 ++++++++++----------- bfxapi/rest/_RestPublicEndpoints.py | 6 +- bfxapi/utils/JSONEncoder.py | 5 +- bfxapi/websocket/_BfxWebsocketInputs.py | 34 ++----- 6 files changed, 86 insertions(+), 96 deletions(-) diff --git a/bfxapi/notification.py b/bfxapi/notification.py index f4301b3..bf4818a 100644 --- a/bfxapi/notification.py +++ b/bfxapi/notification.py @@ -1,7 +1,5 @@ from typing import List, Dict, Union, Optional, Any, TypedDict, Generic, TypeVar, cast - from dataclasses import dataclass - from .labeler import _Type, _Serializer T = TypeVar("T") @@ -19,10 +17,10 @@ class Notification(_Type, Generic[T]): class _Notification(_Serializer, Generic[T]): __LABELS = [ "mts", "type", "message_id", "_PLACEHOLDER", "notify_info", "code", "status", "text" ] - def __init__(self, serializer: Optional[_Serializer] = None, iterate: bool = False): + def __init__(self, serializer: Optional[_Serializer] = None, is_iterable: bool = False): super().__init__("Notification", Notification, _Notification.__LABELS, IGNORE = [ "_PLACEHOLDER" ]) - self.serializer, self.iterate = serializer, iterate + self.serializer, self.is_iterable = serializer, is_iterable def parse(self, *values: Any, skip: Optional[List[str]] = None) -> Notification[T]: notification = cast(Notification[T], Notification(**dict(self._serialize(*values)))) @@ -30,7 +28,7 @@ class _Notification(_Serializer, Generic[T]): if isinstance(self.serializer, _Serializer): NOTIFY_INFO = cast(List[Any], notification.notify_info) - if self.iterate == False: + if self.is_iterable == False: if len(NOTIFY_INFO) == 1 and isinstance(NOTIFY_INFO[0], list): NOTIFY_INFO = NOTIFY_INFO[0] diff --git a/bfxapi/rest/_Requests.py b/bfxapi/rest/_Requests.py index 8103d03..70557cb 100644 --- a/bfxapi/rest/_Requests.py +++ b/bfxapi/rest/_Requests.py @@ -1,21 +1,29 @@ import time, hmac, hashlib, json, requests +from typing import TYPE_CHECKING, Optional, Any + from http import HTTPStatus from .enums import Error from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError from ..utils.JSONEncoder import JSONEncoder +if TYPE_CHECKING: + from requests.sessions import _Params + class _Requests(object): - def __init__(self, host, API_KEY = None, API_SECRET = None): + def __init__(self, host: str, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None): self.host, self.API_KEY, self.API_SECRET = host, API_KEY, API_SECRET - def __build_authentication_headers(self, endpoint, data): + def __build_authentication_headers(self, endpoint: str, data: str): + assert isinstance(self.API_KEY, str) and isinstance(self.API_SECRET, str), \ + "API_KEY and API_SECRET must be both str to call __build_authentication_headers" + nonce = str(int(time.time()) * 1000) - path = f"/api/v2/{endpoint}{nonce}" - - if data != None: path += data + if data == None: + path = f"/api/v2/{endpoint}{nonce}" + else: path = f"/api/v2/{endpoint}{nonce}{data}" signature = hmac.new( self.API_SECRET.encode("utf8"), @@ -29,7 +37,7 @@ class _Requests(object): "bfx-apikey": self.API_KEY } - def _GET(self, endpoint, params = None): + def _GET(self, endpoint: str, params: Optional["_Params"] = None) -> Any: response = requests.get(f"{self.host}/{endpoint}", params=params) if response.status_code == HTTPStatus.NOT_FOUND: @@ -46,11 +54,10 @@ class _Requests(object): return data - def _POST(self, endpoint, params = None, data = None, _ignore_authentication_headers = False): - headers = { "Content-Type": "application/json" } + def _POST(self, endpoint: str, params: Optional["_Params"] = None, body: Optional[Any] = None, _ignore_authentication_headers: bool = False) -> Any: + data = json.dumps(body, cls=JSONEncoder) - if isinstance(data, dict): - data = json.dumps({ key: value for key, value in data.items() if value != None}, cls=JSONEncoder) + headers = { "Content-Type": "application/json" } if self.API_KEY and self.API_SECRET and _ignore_authentication_headers == False: headers = { **headers, **self.__build_authentication_headers(endpoint, data) } diff --git a/bfxapi/rest/_RestAuthenticatedEndpoints.py b/bfxapi/rest/_RestAuthenticatedEndpoints.py index 6113e9c..ab4b9bc 100644 --- a/bfxapi/rest/_RestAuthenticatedEndpoints.py +++ b/bfxapi/rest/_RestAuthenticatedEndpoints.py @@ -20,14 +20,14 @@ class _RestAuthenticatedEndpoints(_Requests): if symbol != None: endpoint += f"/{symbol}" - return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, data={ "id": ids }) ] + return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, body={ "id": ids }) ] def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, float, str], price: Optional[Union[Decimal, float, str]] = None, lev: Optional[int] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_oco_stop: Optional[Union[Decimal, float, str]] = None, gid: Optional[int] = None, cid: Optional[int] = None, flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification[Order]: - data = { + body = { "type": type, "symbol": symbol, "amount": amount, "price": price, "lev": lev, "price_trailing": price_trailing, "price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop, @@ -35,32 +35,32 @@ class _RestAuthenticatedEndpoints(_Requests): "flags": flags, "tif": tif, "meta": meta } - return serializers._Notification[Order](serializer=serializers.Order).parse(*self._POST("auth/w/order/submit", data=data)) + return serializers._Notification[Order](serializers.Order).parse(*self._POST("auth/w/order/submit", body=body)) def update_order(self, id: int, amount: Optional[Union[Decimal, float, str]] = None, price: Optional[Union[Decimal, float, str]] = None, cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification[Order]: - data = { + body = { "id": id, "amount": amount, "price": price, "cid": cid, "cid_date": cid_date, "gid": gid, "flags": flags, "lev": lev, "delta": delta, "price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif } - return serializers._Notification[Order](serializer=serializers.Order).parse(*self._POST("auth/w/order/update", data=data)) + return serializers._Notification[Order](serializers.Order).parse(*self._POST("auth/w/order/update", body=body)) def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None) -> Notification[Order]: - data = { + body = { "id": id, "cid": cid, "cid_date": cid_date } - return serializers._Notification[Order](serializer=serializers.Order).parse(*self._POST("auth/w/order/cancel", data=data)) + return serializers._Notification[Order](serializers.Order).parse(*self._POST("auth/w/order/cancel", body=body)) def cancel_order_multi(self, ids: Optional[List[int]] = None, cids: Optional[List[Tuple[int, str]]] = None, gids: Optional[List[int]] = None, all: bool = False) -> Notification[List[Order]]: - data = { + body = { "ids": ids, "cids": cids, "gids": gids, @@ -68,20 +68,20 @@ class _RestAuthenticatedEndpoints(_Requests): "all": int(all) } - return serializers._Notification[List[Order]](serializer=serializers.Order, iterate=True).parse(*self._POST("auth/w/order/cancel/multi", data=data)) + return serializers._Notification[List[Order]](serializers.Order, is_iterable=True).parse(*self._POST("auth/w/order/cancel/multi", body=body)) def get_orders_history(self, symbol: Optional[str] = None, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Order]: if symbol == None: endpoint = "auth/r/orders/hist" else: endpoint = f"auth/r/orders/{symbol}/hist" - data = { + body = { "id": ids, "start": start, "end": end, "limit": limit } - return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] def get_order_trades(self, symbol: str, id: int) -> List[OrderTrade]: return [ serializers.OrderTrade.parse(*sub_data) for sub_data in self._POST(f"auth/r/order/{symbol}:{id}/trades") ] @@ -91,22 +91,22 @@ class _RestAuthenticatedEndpoints(_Requests): endpoint = "auth/r/trades/hist" else: endpoint = f"auth/r/trades/{symbol}/hist" - data = { + body = { "sort": sort, "start": start, "end": end, "limit": limit } - return [ serializers.Trade.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + return [ serializers.Trade.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] def get_ledgers(self, currency: str, category: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Ledger]: - data = { + body = { "category": category, "start": start, "end": end, "limit": limit } - return [ serializers.Ledger.parse(*sub_data) for sub_data in self._POST(f"auth/r/ledgers/{currency}/hist", data=data) ] + return [ serializers.Ledger.parse(*sub_data) for sub_data in self._POST(f"auth/r/ledgers/{currency}/hist", body=body) ] def get_base_margin_info(self) -> BaseMarginInfo: return serializers.BaseMarginInfo.parse(*(self._POST(f"auth/r/info/margin/base")[1])) @@ -123,36 +123,36 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.Position.parse(*sub_data) for sub_data in self._POST("auth/r/positions") ] def claim_position(self, id: int, amount: Optional[Union[Decimal, float, str]] = None) -> Notification[PositionClaim]: - return serializers._Notification[PositionClaim](serializer=serializers.PositionClaim).parse( - *self._POST("auth/w/position/claim", data={ "id": id, "amount": amount }) + return serializers._Notification[PositionClaim](serializers.PositionClaim).parse( + *self._POST("auth/w/position/claim", body={ "id": id, "amount": amount }) ) def increase_position(self, symbol: str, amount: Union[Decimal, float, str]) -> Notification[PositionIncrease]: - return serializers._Notification[PositionIncrease](serializer=serializers.PositionIncrease).parse( - *self._POST("auth/w/position/increase", data={ "symbol": symbol, "amount": amount }) + return serializers._Notification[PositionIncrease](serializers.PositionIncrease).parse( + *self._POST("auth/w/position/increase", body={ "symbol": symbol, "amount": amount }) ) def get_increase_position_info(self, symbol: str, amount: Union[Decimal, float, str]) -> PositionIncreaseInfo: - response = self._POST(f"auth/r/position/increase/info", data={ "symbol": symbol, "amount": amount }) + response = self._POST(f"auth/r/position/increase/info", body={ "symbol": symbol, "amount": amount }) return serializers.PositionIncreaseInfo.parse(*( response[0] + [response[1][0]] + response[1][1] + [response[1][2]] + response[4] + response[5] )) def get_positions_history(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionHistory]: - return [ serializers.PositionHistory.parse(*sub_data) for sub_data in self._POST("auth/r/positions/hist", data={ "start": start, "end": end, "limit": limit }) ] + return [ serializers.PositionHistory.parse(*sub_data) for sub_data in self._POST("auth/r/positions/hist", body={ "start": start, "end": end, "limit": limit }) ] def get_positions_snapshot(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionSnapshot]: - return [ serializers.PositionSnapshot.parse(*sub_data) for sub_data in self._POST("auth/r/positions/snap", data={ "start": start, "end": end, "limit": limit }) ] + return [ serializers.PositionSnapshot.parse(*sub_data) for sub_data in self._POST("auth/r/positions/snap", body={ "start": start, "end": end, "limit": limit }) ] def get_positions_audit(self, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionAudit]: - return [ serializers.PositionAudit.parse(*sub_data) for sub_data in self._POST("auth/r/positions/audit", data={ "ids": ids, "start": start, "end": end, "limit": limit }) ] + return [ serializers.PositionAudit.parse(*sub_data) for sub_data in self._POST("auth/r/positions/audit", body={ "ids": ids, "start": start, "end": end, "limit": limit }) ] def set_derivative_position_collateral(self, symbol: str, collateral: Union[Decimal, float, str]) -> DerivativePositionCollateral: - return serializers.DerivativePositionCollateral.parse(*(self._POST("auth/w/deriv/collateral/set", data={ "symbol": symbol, "collateral": collateral })[0])) + return serializers.DerivativePositionCollateral.parse(*(self._POST("auth/w/deriv/collateral/set", body={ "symbol": symbol, "collateral": collateral })[0])) def get_derivative_position_collateral_limits(self, symbol: str) -> DerivativePositionCollateralLimits: - return serializers.DerivativePositionCollateralLimits.parse(*self._POST("auth/calc/deriv/collateral/limits", data={ "symbol": symbol })) + return serializers.DerivativePositionCollateralLimits.parse(*self._POST("auth/calc/deriv/collateral/limits", body={ "symbol": symbol })) def get_funding_offers(self, symbol: Optional[str] = None) -> List[FundingOffer]: endpoint = "auth/r/funding/offers" @@ -165,36 +165,36 @@ class _RestAuthenticatedEndpoints(_Requests): def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, float, str], rate: Union[Decimal, float, str], period: int, flags: Optional[int] = 0) -> Notification[FundingOffer]: - data = { + body = { "type": type, "symbol": symbol, "amount": amount, "rate": rate, "period": period, "flags": flags } - return serializers._Notification[FundingOffer](serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/submit", data=data)) + return serializers._Notification[FundingOffer](serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/submit", body=body)) def cancel_funding_offer(self, id: int) -> Notification[FundingOffer]: - return serializers._Notification[FundingOffer](serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/cancel", data={ "id": id })) + return serializers._Notification[FundingOffer](serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/cancel", body={ "id": id })) def cancel_all_funding_offers(self, currency: str) -> Notification[Literal[None]]: - return serializers._Notification[Literal[None]](serializer=None).parse( - *self._POST("auth/w/funding/offer/cancel/all", data={ "currency": currency }) + return serializers._Notification[Literal[None]](None).parse( + *self._POST("auth/w/funding/offer/cancel/all", body={ "currency": currency }) ) def submit_funding_close(self, id: int) -> Notification[Literal[None]]: - return serializers._Notification[Literal[None]](serializer=None).parse( - *self._POST("auth/w/funding/close", data={ "id": id }) + return serializers._Notification[Literal[None]](None).parse( + *self._POST("auth/w/funding/close", body={ "id": id }) ) def toggle_auto_renew(self, status: bool, currency: str, amount: Optional[str] = None, rate: Optional[int] = None, period: Optional[int] = None) -> Notification[FundingAutoRenew]: - return serializers._Notification[FundingAutoRenew](serializer=serializers.FundingAutoRenew).parse(*self._POST("auth/w/funding/auto", data={ + return serializers._Notification[FundingAutoRenew](serializers.FundingAutoRenew).parse(*self._POST("auth/w/funding/auto", body={ "status": int(status), "currency": currency, "amount": amount, "rate": rate, "period": period })) def toggle_keep(self, type: Literal["credit", "loan"], ids: Optional[List[int]] = None, changes: Optional[Dict[int, bool]] = None) -> Notification[Literal[None]]: - return serializers._Notification[Literal[None]](serializer=None).parse(*self._POST("auth/w/funding/keep", data={ + return serializers._Notification[Literal[None]](None).parse(*self._POST("auth/w/funding/keep", body={ "type": type, "id": ids, "changes": changes @@ -205,12 +205,12 @@ class _RestAuthenticatedEndpoints(_Requests): endpoint = "auth/r/funding/offers/hist" else: endpoint = f"auth/r/funding/offers/{symbol}/hist" - data = { + body = { "start": start, "end": end, "limit": limit } - return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] def get_funding_loans(self, symbol: Optional[str] = None) -> List[FundingLoan]: if symbol == None: @@ -224,12 +224,12 @@ class _RestAuthenticatedEndpoints(_Requests): endpoint = "auth/r/funding/loans/hist" else: endpoint = f"auth/r/funding/loans/{symbol}/hist" - data = { + body = { "start": start, "end": end, "limit": limit } - return [ serializers.FundingLoan.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + return [ serializers.FundingLoan.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] def get_funding_credits(self, symbol: Optional[str] = None) -> List[FundingCredit]: if symbol == None: @@ -243,25 +243,25 @@ class _RestAuthenticatedEndpoints(_Requests): endpoint = "auth/r/funding/credits/hist" else: endpoint = f"auth/r/funding/credits/{symbol}/hist" - data = { + body = { "start": start, "end": end, "limit": limit } - return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] def get_funding_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingTrade]: if symbol == None: endpoint = "auth/r/funding/trades/hist" else: endpoint = f"auth/r/funding/trades/{symbol}/hist" - data = { + body = { "sort": sort, "start": start, "end": end, "limit": limit } - return [ serializers.FundingTrade.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + return [ serializers.FundingTrade.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] def get_funding_info(self, key: str) -> FundingInfo: response = self._POST(f"auth/r/info/funding/{key}") @@ -269,45 +269,45 @@ class _RestAuthenticatedEndpoints(_Requests): return serializers.FundingInfo.parse(*([response[1]] + response[2])) def transfer_between_wallets(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, float, str]) -> Notification[Transfer]: - data = { + body = { "from": from_wallet, "to": to_wallet, "currency": currency, "currency_to": currency_to, "amount": amount } - return serializers._Notification[Transfer](serializer=serializers.Transfer).parse(*self._POST("auth/w/transfer", data=data)) + return serializers._Notification[Transfer](serializers.Transfer).parse(*self._POST("auth/w/transfer", body=body)) def submit_wallet_withdrawal(self, wallet: str, method: str, address: str, amount: Union[Decimal, float, str]) -> Notification[Withdrawal]: - return serializers._Notification[Withdrawal](serializer=serializers.Withdrawal).parse(*self._POST("auth/w/withdraw", data={ + return serializers._Notification[Withdrawal](serializers.Withdrawal).parse(*self._POST("auth/w/withdraw", body={ "wallet": wallet, "method": method, "address": address, "amount": amount, })) def get_deposit_address(self, wallet: str, method: str, renew: bool = False) -> Notification[DepositAddress]: - data = { + body = { "wallet": wallet, "method": method, "renew": int(renew) } - return serializers._Notification[DepositAddress](serializer=serializers.DepositAddress).parse(*self._POST("auth/w/deposit/address", data=data)) + return serializers._Notification[DepositAddress](serializers.DepositAddress).parse(*self._POST("auth/w/deposit/address", body=body)) def generate_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, float, str]) -> Invoice: - data = { + body = { "wallet": wallet, "currency": currency, "amount": amount } - return serializers.Invoice.parse(*self._POST("auth/w/deposit/invoice", data=data)) + return serializers.Invoice.parse(*self._POST("auth/w/deposit/invoice", body=body)) def get_movements(self, currency: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Movement]: if currency == None: endpoint = "auth/r/movements/hist" else: endpoint = f"auth/r/movements/{currency}/hist" - data = { + body = { "start": start, "end": end, "limit": limit } - return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] \ No newline at end of file + return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] \ No newline at end of file diff --git a/bfxapi/rest/_RestPublicEndpoints.py b/bfxapi/rest/_RestPublicEndpoints.py index 5157256..c3d2071 100644 --- a/bfxapi/rest/_RestPublicEndpoints.py +++ b/bfxapi/rest/_RestPublicEndpoints.py @@ -175,14 +175,14 @@ class _RestPublicEndpoints(_Requests): return messages def get_trading_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], price_limit: Optional[Union[Decimal, float, str]] = None) -> TradingMarketAveragePrice: - return serializers.TradingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data={ + return serializers.TradingMarketAveragePrice.parse(*self._POST("calc/trade/avg", body={ "symbol": symbol, "amount": amount, "price_limit": price_limit })) def get_funding_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], period: int, rate_limit: Optional[Union[Decimal, float, str]] = None) -> FundingMarketAveragePrice: - return serializers.FundingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data={ + return serializers.FundingMarketAveragePrice.parse(*self._POST("calc/trade/avg", body={ "symbol": symbol, "amount": amount, "period": period, "rate_limit": rate_limit })) def get_fx_rate(self, ccy1: str, ccy2: str) -> FxRate: - return serializers.FxRate.parse(*self._POST("calc/fx", data={ "ccy1": ccy1, "ccy2": ccy2 })) \ No newline at end of file + return serializers.FxRate.parse(*self._POST("calc/fx", body={ "ccy1": ccy1, "ccy2": ccy2 })) \ No newline at end of file diff --git a/bfxapi/utils/JSONEncoder.py b/bfxapi/utils/JSONEncoder.py index 885ab91..506bad1 100644 --- a/bfxapi/utils/JSONEncoder.py +++ b/bfxapi/utils/JSONEncoder.py @@ -8,13 +8,16 @@ JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] class JSONEncoder(json.JSONEncoder): def encode(self, obj: JSON) -> str: + def _strip(dictionary: Dict) -> Dict: + return { key: value for key, value in dictionary.items() if value != None} + def _convert_float_to_str(data: JSON) -> JSON: if isinstance(data, float): return format(Decimal(repr(data)), "f") elif isinstance(data, list): return [ _convert_float_to_str(sub_data) for sub_data in data ] elif isinstance(data, dict): - return { key: _convert_float_to_str(value) for key, value in data.items() } + return _strip({ key: _convert_float_to_str(value) for key, value in data.items() }) else: return data data = _convert_float_to_str(obj) diff --git a/bfxapi/websocket/_BfxWebsocketInputs.py b/bfxapi/websocket/_BfxWebsocketInputs.py index 041405f..0d9ee0b 100644 --- a/bfxapi/websocket/_BfxWebsocketInputs.py +++ b/bfxapi/websocket/_BfxWebsocketInputs.py @@ -5,9 +5,6 @@ from typing import Union, Optional, List, Tuple from .types import JSON from .enums import OrderType, FundingOfferType -def _strip(dictionary): - return { key: value for key, value in dictionary.items() if value != None} - class _BfxWebsocketInputs(object): def __init__(self, __handle_websocket_input): self.__handle_websocket_input = __handle_websocket_input @@ -17,59 +14,44 @@ class _BfxWebsocketInputs(object): price_trailing: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_oco_stop: Optional[Union[Decimal, float, str]] = None, gid: Optional[int] = None, cid: Optional[int] = None, flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None): - data = _strip({ + await self.__handle_websocket_input("on", { "type": type, "symbol": symbol, "amount": amount, "price": price, "lev": lev, "price_trailing": price_trailing, "price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop, "gid": gid, "cid": cid, "flags": flags, "tif": tif, "meta": meta }) - - await self.__handle_websocket_input("on", data) async def update_order(self, id: int, amount: Optional[Union[Decimal, float, str]] = None, price: Optional[Union[Decimal, float, str]] = None, cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, tif: Optional[Union[datetime, str]] = None): - data = _strip({ + await self.__handle_websocket_input("ou", { "id": id, "amount": amount, "price": price, "cid": cid, "cid_date": cid_date, "gid": gid, "flags": flags, "lev": lev, "delta": delta, "price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif }) - - await self.__handle_websocket_input("ou", data) async def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None): - data = _strip({ - "id": id, - "cid": cid, - "cid_date": cid_date + await self.__handle_websocket_input("oc", { + "id": id, "cid": cid, "cid_date": cid_date }) - await self.__handle_websocket_input("oc", data) - async def cancel_order_multi(self, ids: Optional[List[int]] = None, cids: Optional[List[Tuple[int, str]]] = None, gids: Optional[List[int]] = None, all: bool = False): - data = _strip({ - "ids": ids, - "cids": cids, - "gids": gids, - + await self.__handle_websocket_input("oc_multi", { + "ids": ids, "cids": cids, "gids": gids, "all": int(all) }) - - await self.__handle_websocket_input("oc_multi", data) async def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, float, str], rate: Union[Decimal, float, str], period: int, flags: Optional[int] = 0): - data = { + await self.__handle_websocket_input("fon", { "type": type, "symbol": symbol, "amount": amount, "rate": rate, "period": period, "flags": flags - } - - await self.__handle_websocket_input("fon", data) + }) async def cancel_funding_offer(self, id: int): await self.__handle_websocket_input("foc", { "id": id }) From 52ff5006b1c6a04fc52c799052ca38926d9c17ad Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 6 Feb 2023 19:36:54 +0100 Subject: [PATCH 47/85] Add bfxapi/tests/test_labeler.py unit test. --- bfxapi/tests/__init__.py | 2 ++ bfxapi/tests/test_labeler.py | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 bfxapi/tests/test_labeler.py diff --git a/bfxapi/tests/__init__.py b/bfxapi/tests/__init__.py index bbf78c0..25965b4 100644 --- a/bfxapi/tests/__init__.py +++ b/bfxapi/tests/__init__.py @@ -1,6 +1,7 @@ import unittest from .test_rest_serializers_and_types import TestRestSerializersAndTypes from .test_websocket_serializers_and_types import TestWebsocketSerializersAndTypes +from .test_labeler import TestLabeler NAME = "tests" @@ -8,6 +9,7 @@ def suite(): return unittest.TestSuite([ unittest.makeSuite(TestRestSerializersAndTypes), unittest.makeSuite(TestWebsocketSerializersAndTypes), + unittest.makeSuite(TestLabeler), ]) if __name__ == "__main__": diff --git a/bfxapi/tests/test_labeler.py b/bfxapi/tests/test_labeler.py new file mode 100644 index 0000000..721b20e --- /dev/null +++ b/bfxapi/tests/test_labeler.py @@ -0,0 +1,24 @@ +import unittest + +from dataclasses import dataclass +from ..exceptions import LabelerSerializerException +from ..labeler import _Type, generate_labeler_serializer, generate_recursive_serializer + +class TestLabeler(unittest.TestCase): + def test_generate_labeler_serializer(self): + @dataclass + class Test(_Type): + A: int + B: float + C: str + + labels = [ "A", "_PLACEHOLDER", "B", "_PLACEHOLDER", "C" ] + + serializer = generate_labeler_serializer("Test", Test, labels) + + self.assertEqual(serializer.parse(5, None, 65.0, None, "X"), Test(5, 65.0, "X")) + self.assertRaises(LabelerSerializerException, serializer.parse, 5, 65.0, "X") + self.assertListEqual(serializer.get_labels(), [ "A", "B", "C" ]) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 168b2eae259d71c0b3c0db764419b2d0a5765f9f Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 7 Feb 2023 17:21:51 +0100 Subject: [PATCH 48/85] Implement new unit tests in bfxapi/tests/test_labeler.py. --- bfxapi/tests/test_labeler.py | 38 +++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/bfxapi/tests/test_labeler.py b/bfxapi/tests/test_labeler.py index 721b20e..a4310ef 100644 --- a/bfxapi/tests/test_labeler.py +++ b/bfxapi/tests/test_labeler.py @@ -16,9 +16,41 @@ class TestLabeler(unittest.TestCase): serializer = generate_labeler_serializer("Test", Test, labels) - self.assertEqual(serializer.parse(5, None, 65.0, None, "X"), Test(5, 65.0, "X")) - self.assertRaises(LabelerSerializerException, serializer.parse, 5, 65.0, "X") - self.assertListEqual(serializer.get_labels(), [ "A", "B", "C" ]) + self.assertEqual(serializer.parse(5, None, 65.0, None, "X"), Test(5, 65.0, "X"), + msg="_Serializer should produce the right result.") + + self.assertEqual(serializer.parse(5, 65.0, "X", skip=[ "_PLACEHOLDER" ]), Test(5, 65.0, "X"), + msg="_Serializer should produce the right result when skip parameter is given.") + + self.assertListEqual(serializer.get_labels(), [ "A", "B", "C" ], + msg="_Serializer::get_labels() should return the right list of labels.") + + with self.assertRaises(LabelerSerializerException, + msg="_Serializer should raise LabelerSerializerException if given fewer arguments than the serializer labels."): + serializer.parse(5, 65.0, "X") + + def test_generate_recursive_serializer(self): + @dataclass + class Outer(_Type): + A: int + B: float + C: "Middle" + + @dataclass + class Middle(_Type): + D: str + E: "Inner" + + @dataclass + class Inner(_Type): + F: bool + + inner = generate_labeler_serializer("Inner", Inner, ["F"]) + middle = generate_recursive_serializer("Middle", Middle, ["D", "E"], { "E": inner }) + outer = generate_recursive_serializer("Outer", Outer, ["A", "B", "C"], { "C": middle }) + + self.assertEqual(outer.parse(10, 45.5, [ "Y", [ True ] ]), Outer(10, 45.5, Middle("Y", Inner(True))), + msg="_RecursiveSerializer should produce the right result.") if __name__ == "__main__": unittest.main() \ No newline at end of file From 0a9384e67045a28f847677de9f58d5dacae996fe Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 7 Feb 2023 17:45:03 +0100 Subject: [PATCH 49/85] Add new bfxapi/tests/test_notification unit test. --- bfxapi/labeler.py | 2 +- bfxapi/tests/__init__.py | 2 ++ bfxapi/tests/test_notification.py | 25 +++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 bfxapi/tests/test_notification.py diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 1c2655d..79f5ed4 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -19,7 +19,7 @@ class _Serializer(Generic[T]): labels = list(filter(lambda label: label not in (skip or list()), self.__labels)) if len(labels) > len(args): - raise LabelerSerializerException(" and <*args> arguments should contain the same amount of elements.") + raise LabelerSerializerException(f"{self.name} -> and <*args> arguments should contain the same amount of elements.") for index, label in enumerate(labels): if label not in self.__IGNORE: diff --git a/bfxapi/tests/__init__.py b/bfxapi/tests/__init__.py index 25965b4..a63ea0d 100644 --- a/bfxapi/tests/__init__.py +++ b/bfxapi/tests/__init__.py @@ -2,6 +2,7 @@ import unittest from .test_rest_serializers_and_types import TestRestSerializersAndTypes from .test_websocket_serializers_and_types import TestWebsocketSerializersAndTypes from .test_labeler import TestLabeler +from .test_notification import TestNotification NAME = "tests" @@ -10,6 +11,7 @@ def suite(): unittest.makeSuite(TestRestSerializersAndTypes), unittest.makeSuite(TestWebsocketSerializersAndTypes), unittest.makeSuite(TestLabeler), + unittest.makeSuite(TestNotification), ]) if __name__ == "__main__": diff --git a/bfxapi/tests/test_notification.py b/bfxapi/tests/test_notification.py new file mode 100644 index 0000000..f71df60 --- /dev/null +++ b/bfxapi/tests/test_notification.py @@ -0,0 +1,25 @@ +import unittest + +from dataclasses import dataclass +from ..labeler import generate_labeler_serializer +from ..notification import _Type, _Notification, Notification + +class TestNotification(unittest.TestCase): + def test_notification(self): + @dataclass + class Test(_Type): + A: int + B: float + C: str + + test = generate_labeler_serializer("Test", Test, + [ "A", "_PLACEHOLDER", "B", "_PLACEHOLDER", "C" ]) + + notification = _Notification[Test](test) + + self.assertEqual(notification.parse(*[1675787861506, "test", None, None, [ 5, None, 65.0, None, "X" ], 0, "SUCCESS", "This is just a test notification."]), + Notification[Test](1675787861506, "test", None, Test(5, 65.0, "X"), 0, "SUCCESS", "This is just a test notification."), + msg="_Notification should produce the right notification.") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 2d012611822e7f780b3925e3810e2d0e1b02db6e Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 7 Feb 2023 18:31:02 +0100 Subject: [PATCH 50/85] Organize rest sub-package. Create new endpoints and middleware sub-packages. Rename class Requests to Middleware. --- bfxapi/rest/BfxRestInterface.py | 13 ------------- bfxapi/rest/__init__.py | 4 +++- bfxapi/rest/endpoints/__init__.py | 5 +++++ bfxapi/rest/endpoints/bfx_rest_interface.py | 13 +++++++++++++ .../rest_authenticated_endpoints.py} | 12 ++++++------ .../rest_public_endpoints.py} | 10 +++++----- bfxapi/rest/middleware/__init__.py | 3 +++ .../rest/{_Requests.py => middleware/middleware.py} | 8 ++++---- setup.py | 6 +++++- 9 files changed, 44 insertions(+), 30 deletions(-) delete mode 100644 bfxapi/rest/BfxRestInterface.py create mode 100644 bfxapi/rest/endpoints/__init__.py create mode 100644 bfxapi/rest/endpoints/bfx_rest_interface.py rename bfxapi/rest/{_RestAuthenticatedEndpoints.py => endpoints/rest_authenticated_endpoints.py} (98%) rename bfxapi/rest/{_RestPublicEndpoints.py => endpoints/rest_public_endpoints.py} (98%) create mode 100644 bfxapi/rest/middleware/__init__.py rename bfxapi/rest/{_Requests.py => middleware/middleware.py} (93%) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py deleted file mode 100644 index 91d31a2..0000000 --- a/bfxapi/rest/BfxRestInterface.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Optional - -from ._RestPublicEndpoints import _RestPublicEndpoints - -from ._RestAuthenticatedEndpoints import _RestAuthenticatedEndpoints - -class BfxRestInterface(object): - VERSION = 2 - - def __init__(self, host: str, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None): - self.public = _RestPublicEndpoints(host=host) - - self.auth = _RestAuthenticatedEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET) \ No newline at end of file diff --git a/bfxapi/rest/__init__.py b/bfxapi/rest/__init__.py index 0bf3d2e..7ee9fed 100644 --- a/bfxapi/rest/__init__.py +++ b/bfxapi/rest/__init__.py @@ -1 +1,3 @@ -from .BfxRestInterface import BfxRestInterface \ No newline at end of file +from .endpoints import BfxRestInterface, RestPublicEndpoints, RestAuthenticatedEndpoints + +NAME = "rest" \ No newline at end of file diff --git a/bfxapi/rest/endpoints/__init__.py b/bfxapi/rest/endpoints/__init__.py new file mode 100644 index 0000000..24a005d --- /dev/null +++ b/bfxapi/rest/endpoints/__init__.py @@ -0,0 +1,5 @@ +from .bfx_rest_interface import BfxRestInterface +from .rest_public_endpoints import RestPublicEndpoints +from .rest_authenticated_endpoints import RestAuthenticatedEndpoints + +NAME = "endpoints" \ No newline at end of file diff --git a/bfxapi/rest/endpoints/bfx_rest_interface.py b/bfxapi/rest/endpoints/bfx_rest_interface.py new file mode 100644 index 0000000..53f87b0 --- /dev/null +++ b/bfxapi/rest/endpoints/bfx_rest_interface.py @@ -0,0 +1,13 @@ +from typing import Optional + +from .rest_public_endpoints import RestPublicEndpoints + +from .rest_authenticated_endpoints import RestAuthenticatedEndpoints + +class BfxRestInterface(object): + VERSION = 2 + + def __init__(self, host: str, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None): + self.public = RestPublicEndpoints(host=host) + + self.auth = RestAuthenticatedEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET) \ No newline at end of file diff --git a/bfxapi/rest/_RestAuthenticatedEndpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py similarity index 98% rename from bfxapi/rest/_RestAuthenticatedEndpoints.py rename to bfxapi/rest/endpoints/rest_authenticated_endpoints.py index ab4b9bc..a207673 100644 --- a/bfxapi/rest/_RestAuthenticatedEndpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -1,16 +1,16 @@ -from typing import List, Union, Literal, Optional, Any, cast +from typing import List, Union, Literal, Optional -from .types import * +from ..types import * -from . import serializers +from .. import serializers -from .enums import Config, Sort, OrderType, FundingOfferType +from ..enums import Sort, OrderType, FundingOfferType from decimal import Decimal from datetime import datetime -from ._Requests import _Requests +from ..middleware import Middleware -class _RestAuthenticatedEndpoints(_Requests): +class RestAuthenticatedEndpoints(Middleware): def get_wallets(self) -> List[Wallet]: return [ serializers.Wallet.parse(*sub_data) for sub_data in self._POST("auth/r/wallets") ] diff --git a/bfxapi/rest/_RestPublicEndpoints.py b/bfxapi/rest/endpoints/rest_public_endpoints.py similarity index 98% rename from bfxapi/rest/_RestPublicEndpoints.py rename to bfxapi/rest/endpoints/rest_public_endpoints.py index c3d2071..687a2f5 100644 --- a/bfxapi/rest/_RestPublicEndpoints.py +++ b/bfxapi/rest/endpoints/rest_public_endpoints.py @@ -1,15 +1,15 @@ from typing import List, Union, Literal, Optional, Any, cast -from .types import * +from ..types import * -from . import serializers +from .. import serializers -from .enums import Config, Sort +from ..enums import Config, Sort from decimal import Decimal -from ._Requests import _Requests +from ..middleware import Middleware -class _RestPublicEndpoints(_Requests): +class RestPublicEndpoints(Middleware): def conf(self, config: Config) -> Any: return self._GET(f"conf/{config}")[0] diff --git a/bfxapi/rest/middleware/__init__.py b/bfxapi/rest/middleware/__init__.py new file mode 100644 index 0000000..d7e276b --- /dev/null +++ b/bfxapi/rest/middleware/__init__.py @@ -0,0 +1,3 @@ +from .middleware import Middleware + +NAME = "middleware" \ No newline at end of file diff --git a/bfxapi/rest/_Requests.py b/bfxapi/rest/middleware/middleware.py similarity index 93% rename from bfxapi/rest/_Requests.py rename to bfxapi/rest/middleware/middleware.py index 70557cb..627c5cf 100644 --- a/bfxapi/rest/_Requests.py +++ b/bfxapi/rest/middleware/middleware.py @@ -3,15 +3,15 @@ import time, hmac, hashlib, json, requests from typing import TYPE_CHECKING, Optional, Any from http import HTTPStatus -from .enums import Error -from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError +from ..enums import Error +from ..exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError -from ..utils.JSONEncoder import JSONEncoder +from ...utils.JSONEncoder import JSONEncoder if TYPE_CHECKING: from requests.sessions import _Params -class _Requests(object): +class Middleware(object): def __init__(self, host: str, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None): self.host, self.API_KEY, self.API_SECRET = host, API_KEY, API_SECRET diff --git a/setup.py b/setup.py index 963f30a..54db508 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,11 @@ from distutils.core import setup setup( name="bitfinex-api-py", version="3.0.0", - packages=[ "bfxapi", "bfxapi.websocket", "bfxapi.rest", "bfxapi.utils" ], + packages=[ + "bfxapi", "bfxapi.utils", + "bfxapi.websocket", + "bfxapi.rest", "bfxapi.rest.endpoints", "bfxapi.rest.middleware", + ], url="https://github.com/bitfinexcom/bitfinex-api-py", license="OSI Approved :: Apache Software License", author="Bitfinex", From 851521c63fb8b3e03f7900f87a54d3549c941ac4 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 7 Feb 2023 19:00:50 +0100 Subject: [PATCH 51/85] Remove bfxapi/utils/flags.py file from bfxapi.utils sub-package. --- bfxapi/utils/flags.py | 29 --------------------------- examples/rest/create_funding_offer.py | 5 ++--- examples/rest/create_order.py | 6 ++---- 3 files changed, 4 insertions(+), 36 deletions(-) delete mode 100644 bfxapi/utils/flags.py diff --git a/bfxapi/utils/flags.py b/bfxapi/utils/flags.py deleted file mode 100644 index f897103..0000000 --- a/bfxapi/utils/flags.py +++ /dev/null @@ -1,29 +0,0 @@ -from .. enums import Flag - -def calculate_order_flags( - hidden : bool = False, - close : bool = False, - reduce_only : bool = False, - post_only : bool = False, - oco : bool = False, - no_var_rates: bool = False -) -> int: - flags = 0 - - if hidden: flags += Flag.HIDDEN - if close: flags += Flag.CLOSE - if reduce_only: flags += Flag.REDUCE_ONLY - if post_only: flags += Flag.POST_ONLY - if oco: flags += Flag.OCO - if no_var_rates: flags += Flag.NO_VAR_RATES - - return flags - -def calculate_offer_flags( - hidden : bool = False -) -> int: - flags = 0 - - if hidden: flags += Flag.HIDDEN - - return flags \ No newline at end of file diff --git a/examples/rest/create_funding_offer.py b/examples/rest/create_funding_offer.py index fd10cc8..c1031d8 100644 --- a/examples/rest/create_funding_offer.py +++ b/examples/rest/create_funding_offer.py @@ -3,8 +3,7 @@ import os from bfxapi.client import Client, Constants -from bfxapi.enums import FundingOfferType -from bfxapi.utils.flags import calculate_offer_flags +from bfxapi.enums import FundingOfferType, Flag bfx = Client( REST_HOST=Constants.REST_HOST, @@ -18,7 +17,7 @@ notification = bfx.rest.auth.submit_funding_offer( amount="123.45", rate="0.001", period=2, - flags=calculate_offer_flags(hidden=True) + flags=Flag.HIDDEN ) print("Offer notification:", notification) diff --git a/examples/rest/create_order.py b/examples/rest/create_order.py index 2c13ae2..ea70265 100644 --- a/examples/rest/create_order.py +++ b/examples/rest/create_order.py @@ -1,10 +1,8 @@ # python -c "import examples.rest.create_order" import os - from bfxapi.client import Client, Constants -from bfxapi.enums import OrderType -from bfxapi.utils.flags import calculate_order_flags +from bfxapi.enums import OrderType, Flag bfx = Client( REST_HOST=Constants.REST_HOST, @@ -18,7 +16,7 @@ submitted_order = bfx.rest.auth.submit_order( symbol="tBTCUST", amount="0.015", price="10000", - flags=calculate_order_flags(hidden=False) + flags=Flag.HIDDEN + Flag.OCO + Flag.CLOSE ) print("Submit Order Notification:", submitted_order) From 6693e376fc7b69a09e58eaa00e7d0c6150ccec31 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 8 Feb 2023 14:57:36 +0100 Subject: [PATCH 52/85] Fix bug in bfxapi.middleware sub-package. --- bfxapi/rest/middleware/middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bfxapi/rest/middleware/middleware.py b/bfxapi/rest/middleware/middleware.py index 627c5cf..0c1b02e 100644 --- a/bfxapi/rest/middleware/middleware.py +++ b/bfxapi/rest/middleware/middleware.py @@ -55,7 +55,7 @@ class Middleware(object): return data def _POST(self, endpoint: str, params: Optional["_Params"] = None, body: Optional[Any] = None, _ignore_authentication_headers: bool = False) -> Any: - data = json.dumps(body, cls=JSONEncoder) + data = body and json.dumps(body, cls=JSONEncoder) or None headers = { "Content-Type": "application/json" } From 15a2e41e438547a0f9c4547982d771066db62279 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Wed, 8 Feb 2023 11:56:45 +0100 Subject: [PATCH 53/85] user info + fixs --- .../endpoints/rest_authenticated_endpoints.py | 3 + bfxapi/rest/serializers.py | 58 +++++++++++++++++++ bfxapi/rest/types.py | 31 ++++++++++ examples/rest/funding_auto_renew.py | 2 +- examples/rest/get_authenticated_data.py | 11 +++- 5 files changed, 102 insertions(+), 3 deletions(-) diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index a207673..384a418 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -11,6 +11,9 @@ from datetime import datetime from ..middleware import Middleware class RestAuthenticatedEndpoints(Middleware): + def get_user_info(self) -> UserInfo: + return serializers.UserInfo.parse(*self._POST(f"auth/r/info/user")) + def get_wallets(self) -> List[Wallet]: return [ serializers.Wallet.parse(*sub_data) for sub_data in self._POST("auth/r/wallets") ] diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 021211c..a33a04d 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -268,6 +268,64 @@ FxRate = generate_labeler_serializer("FxRate", klass=types.FxRate, labels=[ #region Serializers definition for Rest Authenticated Endpoints +UserInfo = generate_labeler_serializer("UserInfo", klass=types.UserInfo, labels=[ + "id", + "email", + "username", + "mts_account_create", + "verified", + "verification_level", + "_PLACEHOLDER", + "timezone", + "locale", + "company", + "email_verified", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "mts_master_account_create", + "group_id", + "master_account_id", + "inherit_master_account_verification", + "is_group_master", + "group_withdraw_enabled", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "ppt_enabled", + "merchant_enabled", + "competition_enabled", + "two_factors_authentication_modes", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "is_securities_master", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "securities_enabled", + "allow_disable_ctxswitch", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "time_last_login", + "_PLACEHOLDER", + "_PLACEHOLDER", + "ctxtswitch_disabled", + "_PLACEHOLDER", + "comp_countries", + "compl_countries_resid", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "is_merchant_enterprise" +]) + Order = generate_labeler_serializer("Order", klass=types.Order, labels=[ "id", "gid", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index ab41fec..da0f1bc 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -197,6 +197,37 @@ class FxRate(_Type): #region Type hinting for Rest Authenticated Endpoints +@dataclass +class UserInfo(_Type): + id: int + email: str + username: str + mts_account_create: int + verified: int + verification_level: int + timezone: str + locale: str + company: str + email_verified: int + mts_master_account_create: int + group_id: int + master_account_id: int + inherit_master_account_verification: int + is_group_master: int + group_withdraw_enabled: int + ppt_enabled: int + merchant_enabled: int + competition_enabled: int + two_factors_authentication_modes: List[str] + is_securities_master: int + securities_enabled: int + allow_disable_ctxswitch: int + ctxtswitch_disabled: int + time_last_login: int + comp_countries: List[str] + compl_countries_resid: List[str] + is_merchant_enterprise: int + @dataclass class Order(_Type): id: int diff --git a/examples/rest/funding_auto_renew.py b/examples/rest/funding_auto_renew.py index c892ce3..11ee7ca 100644 --- a/examples/rest/funding_auto_renew.py +++ b/examples/rest/funding_auto_renew.py @@ -14,7 +14,7 @@ notification = bfx.rest.auth.toggle_auto_renew( status=True, currency="USD", amount="150", - rate="0", + rate="0", # FRR period=2 ) diff --git a/examples/rest/get_authenticated_data.py b/examples/rest/get_authenticated_data.py index 2ff1de6..f398773 100644 --- a/examples/rest/get_authenticated_data.py +++ b/examples/rest/get_authenticated_data.py @@ -13,6 +13,12 @@ bfx = Client( now = int(round(time.time() * 1000)) + +def log_user_info(): + user_info = bfx.rest.auth.get_user_info() + print(user_info) + + def log_wallets(): wallets = bfx.rest.auth.get_wallets() print("Wallets:") @@ -38,14 +44,14 @@ def log_positions(): def log_trades(): - trades = bfx.rest.auth.get_trades(symbol='tBTCUSD', start=0, end=now) + trades = bfx.rest.auth.get_trades_history(symbol='tBTCUSD', start=0, end=now) print("Trades:") [print(t) for t in trades] def log_order_trades(): order_id = 82406909127 - trades = bfx.rest.auth.get_order_trades(symbol='tBTCUSD', order_id=order_id) + trades = bfx.rest.auth.get_order_trades(symbol='tBTCUSD', id=order_id) print("Trade orders:") [print(t) for t in trades] @@ -96,6 +102,7 @@ def log_margin_info(): print(f"Base margin info {base_margin_info}") def run(): + log_user_info() log_wallets() log_orders() log_orders_history() From 48583786f7338e7e8c1711f296bb37859a514179 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Wed, 8 Feb 2023 12:39:51 +0100 Subject: [PATCH 54/85] login history + balance available --- .../endpoints/rest_authenticated_endpoints.py | 9 +++++++++ bfxapi/rest/serializers.py | 15 +++++++++++++++ bfxapi/rest/types.py | 11 +++++++++++ examples/rest/get_authenticated_data.py | 5 +++++ 4 files changed, 40 insertions(+) diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index 384a418..4307bc0 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -14,6 +14,15 @@ class RestAuthenticatedEndpoints(Middleware): def get_user_info(self) -> UserInfo: return serializers.UserInfo.parse(*self._POST(f"auth/r/info/user")) + def get_login_history(self) -> LoginHistory: + return [ serializers.LoginHistory.parse(*sub_data) for sub_data in self._POST("auth/r/logins/hist") ] + + def get_balance_available_for_orders_or_offers(self, symbol: str, type: str, dir: Optional[int] = None, rate: Optional[str] = None, lev: Optional[str] = None) -> BalanceAvailable: + return serializers.BalanceAvailable.parse(*self._POST("auth/calc/order/avail", body={ + "symbol": symbol, "type": type, "dir": dir, + "rate": rate, "lev": lev + })) + def get_wallets(self) -> List[Wallet]: return [ serializers.Wallet.parse(*sub_data) for sub_data in self._POST("auth/r/wallets") ] diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index a33a04d..43943ff 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -326,6 +326,21 @@ UserInfo = generate_labeler_serializer("UserInfo", klass=types.UserInfo, labels= "is_merchant_enterprise" ]) +LoginHistory = generate_labeler_serializer("LoginHistory", klass=types.LoginHistory, labels=[ + "id", + "_PLACEHOLDER", + "time", + "_PLACEHOLDER", + "ip", + "_PLACEHOLDER", + "_PLACEHOLDER", + "extra_info" +]) + +BalanceAvailable = generate_labeler_serializer("BalanceAvailable", klass=types.BalanceAvailable, labels=[ + "amount" +]) + Order = generate_labeler_serializer("Order", klass=types.Order, labels=[ "id", "gid", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index da0f1bc..eee3c30 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -228,6 +228,17 @@ class UserInfo(_Type): compl_countries_resid: List[str] is_merchant_enterprise: int +@dataclass +class LoginHistory(_Type): + id: int + time: int + ip: str + extra_info: JSON + +@dataclass +class BalanceAvailable(_Type): + amount: float + @dataclass class Order(_Type): id: int diff --git a/examples/rest/get_authenticated_data.py b/examples/rest/get_authenticated_data.py index f398773..ada724a 100644 --- a/examples/rest/get_authenticated_data.py +++ b/examples/rest/get_authenticated_data.py @@ -19,6 +19,11 @@ def log_user_info(): print(user_info) +def log_login_history(): + login_history = bfx.rest.auth.get_login_history() + print(login_history) + + def log_wallets(): wallets = bfx.rest.auth.get_wallets() print("Wallets:") From 5cf3b18ff3a793105a3429b7bde8482bee036ef2 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 9 Feb 2023 04:16:31 +0100 Subject: [PATCH 55/85] Fix bug in composing data object inside bfxapi/rest/endpoints/rest_authenticated_endpoints.py file. --- .../rest/endpoints/rest_authenticated_endpoints.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index 4307bc0..05502c1 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -125,8 +125,8 @@ class RestAuthenticatedEndpoints(Middleware): def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo: response = self._POST(f"auth/r/info/margin/{symbol}") - - return serializers.SymbolMarginInfo.parse(*([response[1]] + response[2])) + data = [response[1]] + response[2] + return serializers.SymbolMarginInfo.parse(*data) def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]: return [ serializers.SymbolMarginInfo.parse(*([sub_data[1]] + sub_data[2])) for sub_data in self._POST(f"auth/r/info/margin/sym_all") ] @@ -146,10 +146,8 @@ class RestAuthenticatedEndpoints(Middleware): def get_increase_position_info(self, symbol: str, amount: Union[Decimal, float, str]) -> PositionIncreaseInfo: response = self._POST(f"auth/r/position/increase/info", body={ "symbol": symbol, "amount": amount }) - - return serializers.PositionIncreaseInfo.parse(*( - response[0] + [response[1][0]] + response[1][1] + [response[1][2]] + response[4] + response[5] - )) + data = response[0] + [response[1][0]] + response[1][1] + [response[1][2]] + response[4] + response[5] + return serializers.PositionIncreaseInfo.parse(*data) def get_positions_history(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionHistory]: return [ serializers.PositionHistory.parse(*sub_data) for sub_data in self._POST("auth/r/positions/hist", body={ "start": start, "end": end, "limit": limit }) ] @@ -277,8 +275,8 @@ class RestAuthenticatedEndpoints(Middleware): def get_funding_info(self, key: str) -> FundingInfo: response = self._POST(f"auth/r/info/funding/{key}") - - return serializers.FundingInfo.parse(*([response[1]] + response[2])) + data = [response[1]] + response[2] + return serializers.FundingInfo.parse(*data) def transfer_between_wallets(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, float, str]) -> Notification[Transfer]: body = { From 7e7c4ef23aa7437674846eaa28877fc6ba7377c0 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 9 Feb 2023 04:22:22 +0100 Subject: [PATCH 56/85] Fix all mypy errors and warnings. --- bfxapi/rest/endpoints/rest_authenticated_endpoints.py | 2 +- bfxapi/rest/middleware/middleware.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index 05502c1..789ad5b 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -14,7 +14,7 @@ class RestAuthenticatedEndpoints(Middleware): def get_user_info(self) -> UserInfo: return serializers.UserInfo.parse(*self._POST(f"auth/r/info/user")) - def get_login_history(self) -> LoginHistory: + def get_login_history(self) -> List[LoginHistory]: return [ serializers.LoginHistory.parse(*sub_data) for sub_data in self._POST("auth/r/logins/hist") ] def get_balance_available_for_orders_or_offers(self, symbol: str, type: str, dir: Optional[int] = None, rate: Optional[str] = None, lev: Optional[str] = None) -> BalanceAvailable: diff --git a/bfxapi/rest/middleware/middleware.py b/bfxapi/rest/middleware/middleware.py index 0c1b02e..9180841 100644 --- a/bfxapi/rest/middleware/middleware.py +++ b/bfxapi/rest/middleware/middleware.py @@ -1,6 +1,6 @@ import time, hmac, hashlib, json, requests -from typing import TYPE_CHECKING, Optional, Any +from typing import TYPE_CHECKING, Optional, Any, cast from http import HTTPStatus from ..enums import Error @@ -15,7 +15,7 @@ class Middleware(object): def __init__(self, host: str, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None): self.host, self.API_KEY, self.API_SECRET = host, API_KEY, API_SECRET - def __build_authentication_headers(self, endpoint: str, data: str): + def __build_authentication_headers(self, endpoint: str, data: Optional[str] = None): assert isinstance(self.API_KEY, str) and isinstance(self.API_SECRET, str), \ "API_KEY and API_SECRET must be both str to call __build_authentication_headers" @@ -69,7 +69,7 @@ class Middleware(object): data = response.json() - if len(data) and data[0] == "error": + if isinstance(data, list) and len(data) and data[0] == "error": if data[1] == Error.ERR_PARAMS: raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") From 7dc043bbe50a2eee8c88e80144f03774f2ecee3f Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 9 Feb 2023 04:31:52 +0100 Subject: [PATCH 57/85] Small fix in BfxWebsocketClient class. --- bfxapi/websocket/BfxWebsocketClient.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 3bdb3d9..98b5b75 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -38,7 +38,8 @@ class BfxWebsocketClient(object): self.host, self.websocket, self.event_emitter = host, None, AsyncIOEventEmitter() self.event_emitter.add_listener("error", - lambda exception: self.logger.error("\n" + str().join(traceback.format_exception(type(exception), exception, exception.__traceback__))[:-1]) + lambda exception: self.logger.error(str(exception) + "\n" + + str().join(traceback.format_exception(type(exception), exception, exception.__traceback__))[:-1]) ) self.API_KEY, self.API_SECRET, self.filter, self.authentication = API_KEY, API_SECRET, filter, False @@ -52,7 +53,8 @@ class BfxWebsocketClient(object): self.logger = CustomLogger("BfxWebsocketClient", logLevel=log_level) if buckets > BfxWebsocketClient.MAXIMUM_BUCKETS_AMOUNT: - self.logger.warning(f"It is not safe to use more than {BfxWebsocketClient.MAXIMUM_BUCKETS_AMOUNT} buckets from the same connection ({buckets} in use), the server could momentarily block the client with <429 Too Many Requests>.") + self.logger.warning(f"It is not safe to use more than {BfxWebsocketClient.MAXIMUM_BUCKETS_AMOUNT} buckets from the same \ + connection ({buckets} in use), the server could momentarily block the client with <429 Too Many Requests>.") def run(self): return asyncio.run(self.start()) From 180e92fcb4194c6609914e82838dae510cc138c3 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 10 Feb 2023 04:27:35 +0100 Subject: [PATCH 58/85] merchant Co-Authored-By: itsdeka --- .../endpoints/rest_authenticated_endpoints.py | 24 ++++++++-- bfxapi/rest/serializers.py | 4 +- bfxapi/rest/types.py | 48 +++++++++++++++++-- bfxapi/utils/JSONEncoder.py | 38 +++++++-------- examples/rest/merchant.py | 33 +++++++++++++ 5 files changed, 119 insertions(+), 28 deletions(-) create mode 100644 examples/rest/merchant.py diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index 789ad5b..e4ca99f 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -302,13 +302,13 @@ class RestAuthenticatedEndpoints(Middleware): return serializers._Notification[DepositAddress](serializers.DepositAddress).parse(*self._POST("auth/w/deposit/address", body=body)) - def generate_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, float, str]) -> Invoice: + def generate_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, float, str]) -> LightningNetworkInvoice: body = { "wallet": wallet, "currency": currency, "amount": amount } - return serializers.Invoice.parse(*self._POST("auth/w/deposit/invoice", body=body)) + return serializers.LightningNetworkInvoice.parse(*self._POST("auth/w/deposit/invoice", body=body)) def get_movements(self, currency: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Movement]: if currency == None: @@ -320,4 +320,22 @@ class RestAuthenticatedEndpoints(Middleware): "limit": limit } - return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] \ No newline at end of file + return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] + + def submit_invoice(self, amount: Union[Decimal, float, str], currency: str, order_id: str, + customer_info: CustomerInfo, pay_currencies: List[str], duration: Optional[int] = None, + webhook: Optional[str] = None, redirect_url: Optional[str] = None) -> InvoiceSubmission: + data = self._POST("auth/w/ext/pay/invoice/create", body={ + "amount": amount, "currency": currency, "order_id": order_id, + "customer_info": customer_info, "pay_currencies": pay_currencies, "duration": duration, + "webhook": webhook, "redirect_url": redirect_url + }) + + if "customer_info" in data and data["customer_info"] != None: + data["customer_info"] = CustomerInfo(**data["customer_info"]) + + if "invoices" in data and data["invoices"] != None: + for index, invoice in enumerate(data["invoices"]): + data["invoices"][index] = Invoice(**invoice) + + return InvoiceSubmission(**data) \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 43943ff..6e80ed7 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -18,7 +18,7 @@ __serializers__ = [ "FundingOffer", "FundingCredit", "FundingLoan", "FundingAutoRenew", "FundingInfo", "Wallet", "Transfer", "Withdrawal", "DepositAddress", - "Invoice", "Movement", "SymbolMarginInfo", + "LightningNetworkInvoice", "Movement", "SymbolMarginInfo", "BaseMarginInfo", "PositionClaim", "PositionIncreaseInfo", "PositionIncrease", "PositionHistory", "PositionSnapshot", "PositionAudit", "DerivativePositionCollateral", "DerivativePositionCollateralLimits", @@ -581,7 +581,7 @@ DepositAddress = generate_labeler_serializer("DepositAddress", klass=types.Depos "pool_address" ]) -Invoice = generate_labeler_serializer("Invoice", klass=types.Invoice, labels=[ +LightningNetworkInvoice = generate_labeler_serializer("LightningNetworkInvoice", klass=types.LightningNetworkInvoice, labels=[ "invoice_hash", "invoice", "_PLACEHOLDER", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index eee3c30..964b24b 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -1,10 +1,12 @@ -from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any +from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Literal, Any from dataclasses import dataclass +from types import SimpleNamespace + from .. labeler import _Type from .. notification import Notification -from ..utils.JSONEncoder import JSON +from .. utils.JSONEncoder import JSON #region Type hinting for Rest Public Endpoints @@ -440,7 +442,7 @@ class DepositAddress(_Type): pool_address: str @dataclass -class Invoice(_Type): +class LightningNetworkInvoice(_Type): invoice_hash: str invoice: str amount: str @@ -561,4 +563,44 @@ class DerivativePositionCollateralLimits(_Type): min_collateral: float max_collateral: float +#endregion + +#region Type hinting for models which are not serializable + +@dataclass +class InvoiceSubmission(_Type): + id: str + t: int + type: Literal["ECOMMERCE", "POS"] + duration: int + amount: float + currency: str + order_id: str + pay_currencies: List[str] + webhook: str + redirect_url: str + status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"] + customer_info: Optional["CustomerInfo"] + invoices: List["Invoice"] + +class CustomerInfo(SimpleNamespace): + nationality: str + resid_country: str + resid_state: str + resid_city: str + resid_zip_code: str + resid_street: str + resid_building_no: str + full_name: str + email: str + tos_accepted: bool + +class Invoice(SimpleNamespace): + amount: float + currency: str + pay_currency: str + pool_currency: str + address: str + ext: JSON + #endregion \ No newline at end of file diff --git a/bfxapi/utils/JSONEncoder.py b/bfxapi/utils/JSONEncoder.py index 506bad1..b322795 100644 --- a/bfxapi/utils/JSONEncoder.py +++ b/bfxapi/utils/JSONEncoder.py @@ -2,33 +2,31 @@ import json from decimal import Decimal from datetime import datetime +from types import SimpleNamespace + from typing import Type, List, Dict, Union, Any JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] +def _strip(dictionary: Dict) -> Dict: + return { key: value for key, value in dictionary.items() if value != None} + +def _convert_float_to_str(data: JSON) -> JSON: + if isinstance(data, float): + return format(Decimal(repr(data)), "f") + elif isinstance(data, list): + return [ _convert_float_to_str(sub_data) for sub_data in data ] + elif isinstance(data, dict): + return _strip({ key: _convert_float_to_str(value) for key, value in data.items() }) + else: return data + class JSONEncoder(json.JSONEncoder): def encode(self, obj: JSON) -> str: - def _strip(dictionary: Dict) -> Dict: - return { key: value for key, value in dictionary.items() if value != None} - - def _convert_float_to_str(data: JSON) -> JSON: - if isinstance(data, float): - return format(Decimal(repr(data)), "f") - elif isinstance(data, list): - return [ _convert_float_to_str(sub_data) for sub_data in data ] - elif isinstance(data, dict): - return _strip({ key: _convert_float_to_str(value) for key, value in data.items() }) - else: return data - - data = _convert_float_to_str(obj) - - return json.JSONEncoder.encode(self, data) + return json.JSONEncoder.encode(self, _convert_float_to_str(obj)) def default(self, obj: Any) -> Any: - if isinstance(obj, Decimal): - return format(obj, "f") - - if isinstance(obj, datetime): - return str(obj) + if isinstance(obj, SimpleNamespace): return _convert_float_to_str(vars(obj)) + elif isinstance(obj, Decimal): return format(obj, "f") + elif isinstance(obj, datetime): return str(obj) return json.JSONEncoder.default(self, obj) \ No newline at end of file diff --git a/examples/rest/merchant.py b/examples/rest/merchant.py new file mode 100644 index 0000000..dcacbbf --- /dev/null +++ b/examples/rest/merchant.py @@ -0,0 +1,33 @@ +# python -c "import examples.rest.merchant" + +import os + +from bfxapi.client import Client, Constants +from bfxapi.rest.types import CustomerInfo + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +customer_info: CustomerInfo = CustomerInfo( + nationality="GB", + resid_country="DE", + resid_city="Berlin", + resid_zip_code=1, + resid_street="Timechain", + full_name="Satoshi", + email="satoshi3@bitfinex.com", + tos_accepted=None, + resid_building_no=None +) + +print(bfx.rest.auth.submit_invoice( + amount=1, + currency="USD", + duration=864000, + order_id="order123", + customer_info=customer_info, + pay_currencies=["ETH"], +)) \ No newline at end of file From 56476b96fcdb0436b274972871e0bd62b187d1c4 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 10 Feb 2023 04:33:29 +0100 Subject: [PATCH 59/85] Add new serializers to __serializers__ constant in bfxapi.rest.serializers. --- bfxapi/rest/serializers.py | 1 + bfxapi/rest/types.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 6e80ed7..bcdc7f3 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -13,6 +13,7 @@ __serializers__ = [ "FundingStatistic", "PulseProfile", "PulseMessage", "TradingMarketAveragePrice", "FundingMarketAveragePrice", "FxRate", + "UserInfo", "LoginHistory", "BalanceAvailable", "Order", "Position", "Trade", "FundingTrade", "OrderTrade", "Ledger", "FundingOffer", "FundingCredit", "FundingLoan", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 964b24b..90aa31d 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -224,8 +224,8 @@ class UserInfo(_Type): is_securities_master: int securities_enabled: int allow_disable_ctxswitch: int - ctxtswitch_disabled: int time_last_login: int + ctxtswitch_disabled: int comp_countries: List[str] compl_countries_resid: List[str] is_merchant_enterprise: int From 7e421d380369bebd09db5ff9dade00914e7943d8 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 10 Feb 2023 04:47:03 +0100 Subject: [PATCH 60/85] Fix bug in examples/rest/merchant.py demo. --- examples/rest/merchant.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/rest/merchant.py b/examples/rest/merchant.py index dcacbbf..13d051c 100644 --- a/examples/rest/merchant.py +++ b/examples/rest/merchant.py @@ -19,8 +19,6 @@ customer_info: CustomerInfo = CustomerInfo( resid_street="Timechain", full_name="Satoshi", email="satoshi3@bitfinex.com", - tos_accepted=None, - resid_building_no=None ) print(bfx.rest.auth.submit_invoice( From 9ada3b05a24dded48ddaca831ade15986ad0fc6b Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Sun, 12 Feb 2023 21:24:42 +0100 Subject: [PATCH 61/85] Fix bug in submit_invoice method (bfxapi.rest.endpoints.rest_authenticated_endpoints). --- bfxapi/client.py | 2 ++ bfxapi/labeler.py | 21 ++++++++++++ bfxapi/rest/endpoints/bfx_rest_interface.py | 2 -- .../endpoints/rest_authenticated_endpoints.py | 33 ++++++++++--------- .../rest/endpoints/rest_public_endpoints.py | 12 +++---- bfxapi/rest/types.py | 23 +++++++++---- bfxapi/utils/JSONEncoder.py | 3 +- bfxapi/utils/camel_and_snake_case_adapters.py | 22 +++++++++++++ examples/rest/merchant.py | 21 ++++++------ 9 files changed, 95 insertions(+), 44 deletions(-) create mode 100644 bfxapi/utils/camel_and_snake_case_adapters.py diff --git a/bfxapi/client.py b/bfxapi/client.py index e866235..ec72fb3 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -8,9 +8,11 @@ from enum import Enum class Constants(str, Enum): REST_HOST = "https://api.bitfinex.com/v2" PUB_REST_HOST = "https://api-pub.bitfinex.com/v2" + STAGING_REST_HOST = "https://api.staging.bitfinex.com/v2" WSS_HOST = "wss://api.bitfinex.com/ws/2" PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2" + STAGING_WSS_HOST = "wss://api.staging.bitfinex.com/ws/2" class Client(object): def __init__( diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 79f5ed4..02b6699 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -4,6 +4,27 @@ from typing import Type, Generic, TypeVar, Iterable, Optional, Dict, List, Tuple T = TypeVar("T", bound="_Type") +def compose(*decorators): + def wrapper(function): + for decorator in reversed(decorators): + function = decorator(function) + return function + + return wrapper + +def partial(cls): + def __init__(self, **kwargs): + for key, value in kwargs.items(): + self.__setattr__(key, value) + + for annotation in self.__annotations__.keys(): + if annotation not in kwargs: + self.__setattr__(annotation, None) + + cls.__init__ = __init__ + + return cls + class _Type(object): """ Base class for any dataclass serializable by the _Serializer generic class. diff --git a/bfxapi/rest/endpoints/bfx_rest_interface.py b/bfxapi/rest/endpoints/bfx_rest_interface.py index 53f87b0..0bddbbc 100644 --- a/bfxapi/rest/endpoints/bfx_rest_interface.py +++ b/bfxapi/rest/endpoints/bfx_rest_interface.py @@ -1,7 +1,5 @@ from typing import Optional - from .rest_public_endpoints import RestPublicEndpoints - from .rest_authenticated_endpoints import RestAuthenticatedEndpoints class BfxRestInterface(object): diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index e4ca99f..2dae262 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -1,14 +1,20 @@ from typing import List, Union, Literal, Optional - -from ..types import * - -from .. import serializers - -from ..enums import Sort, OrderType, FundingOfferType from decimal import Decimal from datetime import datetime -from ..middleware import Middleware +from .. types import * + +from .. import serializers +from .. enums import Sort, OrderType, FundingOfferType +from .. middleware import Middleware + +from ... utils.camel_and_snake_case_adapters import to_snake_case_keys, to_camel_case_keys + +_CustomerInfo = TypedDict("_CustomerInfo", { + "nationality": str, "resid_country": str, "resid_city": str, + "resid_zip_code": str, "resid_street": str, "resid_building_no": str, + "full_name": str, "email": str, "tos_accepted": bool +}) class RestAuthenticatedEndpoints(Middleware): def get_user_info(self) -> UserInfo: @@ -323,19 +329,14 @@ class RestAuthenticatedEndpoints(Middleware): return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] def submit_invoice(self, amount: Union[Decimal, float, str], currency: str, order_id: str, - customer_info: CustomerInfo, pay_currencies: List[str], duration: Optional[int] = None, + customer_info: _CustomerInfo, pay_currencies: List[str], duration: Optional[int] = None, webhook: Optional[str] = None, redirect_url: Optional[str] = None) -> InvoiceSubmission: - data = self._POST("auth/w/ext/pay/invoice/create", body={ + body = to_camel_case_keys({ "amount": amount, "currency": currency, "order_id": order_id, "customer_info": customer_info, "pay_currencies": pay_currencies, "duration": duration, "webhook": webhook, "redirect_url": redirect_url }) - if "customer_info" in data and data["customer_info"] != None: - data["customer_info"] = CustomerInfo(**data["customer_info"]) - - if "invoices" in data and data["invoices"] != None: - for index, invoice in enumerate(data["invoices"]): - data["invoices"][index] = Invoice(**invoice) + data = to_snake_case_keys(self._POST("auth/w/ext/pay/invoice/create", body=body)) - return InvoiceSubmission(**data) \ No newline at end of file + return InvoiceSubmission.parse(data) \ No newline at end of file diff --git a/bfxapi/rest/endpoints/rest_public_endpoints.py b/bfxapi/rest/endpoints/rest_public_endpoints.py index 687a2f5..3810e97 100644 --- a/bfxapi/rest/endpoints/rest_public_endpoints.py +++ b/bfxapi/rest/endpoints/rest_public_endpoints.py @@ -1,13 +1,11 @@ from typing import List, Union, Literal, Optional, Any, cast - -from ..types import * - -from .. import serializers - -from ..enums import Config, Sort from decimal import Decimal -from ..middleware import Middleware +from .. types import * + +from .. import serializers +from .. enums import Config, Sort +from .. middleware import Middleware class RestPublicEndpoints(Middleware): def conf(self, config: Config) -> Any: diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 90aa31d..148a60c 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -2,9 +2,7 @@ from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Literal, from dataclasses import dataclass -from types import SimpleNamespace - -from .. labeler import _Type +from .. labeler import _Type, partial, compose from .. notification import Notification from .. utils.JSONEncoder import JSON @@ -567,7 +565,7 @@ class DerivativePositionCollateralLimits(_Type): #region Type hinting for models which are not serializable -@dataclass +@compose(dataclass, partial) class InvoiceSubmission(_Type): id: str t: int @@ -583,7 +581,19 @@ class InvoiceSubmission(_Type): customer_info: Optional["CustomerInfo"] invoices: List["Invoice"] -class CustomerInfo(SimpleNamespace): + @classmethod + def parse(cls, data: Dict[str, Any]) -> "InvoiceSubmission": + if "customer_info" in data and data["customer_info"] != None: + data["customer_info"] = CustomerInfo(**data["customer_info"]) + + if "invoices" in data and data["invoices"] != None: + for index, invoice in enumerate(data["invoices"]): + data["invoices"][index] = Invoice(**invoice) + + return cls(**data) + +@compose(dataclass, partial) +class CustomerInfo(_Type): nationality: str resid_country: str resid_state: str @@ -595,7 +605,8 @@ class CustomerInfo(SimpleNamespace): email: str tos_accepted: bool -class Invoice(SimpleNamespace): +@compose(dataclass, partial) +class Invoice(_Type): amount: float currency: str pay_currency: str diff --git a/bfxapi/utils/JSONEncoder.py b/bfxapi/utils/JSONEncoder.py index b322795..5124376 100644 --- a/bfxapi/utils/JSONEncoder.py +++ b/bfxapi/utils/JSONEncoder.py @@ -25,8 +25,7 @@ class JSONEncoder(json.JSONEncoder): return json.JSONEncoder.encode(self, _convert_float_to_str(obj)) def default(self, obj: Any) -> Any: - if isinstance(obj, SimpleNamespace): return _convert_float_to_str(vars(obj)) - elif isinstance(obj, Decimal): return format(obj, "f") + if isinstance(obj, Decimal): return format(obj, "f") elif isinstance(obj, datetime): return str(obj) return json.JSONEncoder.default(self, obj) \ No newline at end of file diff --git a/bfxapi/utils/camel_and_snake_case_adapters.py b/bfxapi/utils/camel_and_snake_case_adapters.py new file mode 100644 index 0000000..85ccdf4 --- /dev/null +++ b/bfxapi/utils/camel_and_snake_case_adapters.py @@ -0,0 +1,22 @@ +import re + +from typing import TypeVar, Callable, Dict, Any, cast + +T = TypeVar("T") + +_to_snake_case: Callable[[str], str] = lambda string: re.sub(r"(? T: + if isinstance(data, list): + return cast(T, [ _scheme(sub_data, adapter) for sub_data in data ]) + elif isinstance(data, dict): + return cast(T, { adapter(key): _scheme(value, adapter) for key, value in data.items() }) + else: return data + +def to_snake_case_keys(dictionary: Dict[str, Any]) -> Dict[str, Any]: + return _scheme(dictionary, _to_snake_case) + +def to_camel_case_keys(dictionary: Dict[str, Any]) -> Dict[str, Any]: + return _scheme(dictionary, _to_camel_case) \ No newline at end of file diff --git a/examples/rest/merchant.py b/examples/rest/merchant.py index 13d051c..69a1d8b 100644 --- a/examples/rest/merchant.py +++ b/examples/rest/merchant.py @@ -3,7 +3,6 @@ import os from bfxapi.client import Client, Constants -from bfxapi.rest.types import CustomerInfo bfx = Client( REST_HOST=Constants.REST_HOST, @@ -11,15 +10,15 @@ bfx = Client( API_SECRET=os.getenv("BFX_API_SECRET") ) -customer_info: CustomerInfo = CustomerInfo( - nationality="GB", - resid_country="DE", - resid_city="Berlin", - resid_zip_code=1, - resid_street="Timechain", - full_name="Satoshi", - email="satoshi3@bitfinex.com", -) +customer_info = { + "nationality": "GB", + "resid_country": "DE", + "resid_city": "Berlin", + "resid_zip_code": 1, + "resid_street": "Timechain", + "full_name": "Satoshi", + "email": "satoshi3@bitfinex.com" +} print(bfx.rest.auth.submit_invoice( amount=1, @@ -27,5 +26,5 @@ print(bfx.rest.auth.submit_invoice( duration=864000, order_id="order123", customer_info=customer_info, - pay_currencies=["ETH"], + pay_currencies=["ETH"] )) \ No newline at end of file From 3c377928b277120e85f511db13b8f59cee65bff0 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Sun, 12 Feb 2023 21:41:33 +0100 Subject: [PATCH 62/85] fix errors merchant Co-Authored-By: itsdeka --- bfxapi/rest/endpoints/rest_authenticated_endpoints.py | 11 ++++++++++- bfxapi/rest/types.py | 8 ++++++-- examples/rest/merchant.py | 6 +++++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index 2dae262..a69ea28 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -339,4 +339,13 @@ class RestAuthenticatedEndpoints(Middleware): data = to_snake_case_keys(self._POST("auth/w/ext/pay/invoice/create", body=body)) - return InvoiceSubmission.parse(data) \ No newline at end of file + return InvoiceSubmission.parse(data) + + def get_invoices(self, id: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[InvoiceSubmission]: + return [ InvoiceSubmission.parse(sub_data) for sub_data in self._POST("auth/r/ext/pay/invoices", body={ + "id": id, "start": start, "end": end, + "limit": limit + }) ] + + def get_invoice_count_stats(self, status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"], format: str) -> List[InvoiceCountStats]: + return [ InvoiceCountStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/count", body={ "status": status, "format": format }) ] \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 148a60c..5f98ca8 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -564,7 +564,6 @@ class DerivativePositionCollateralLimits(_Type): #endregion #region Type hinting for models which are not serializable - @compose(dataclass, partial) class InvoiceSubmission(_Type): id: str @@ -590,7 +589,7 @@ class InvoiceSubmission(_Type): for index, invoice in enumerate(data["invoices"]): data["invoices"][index] = Invoice(**invoice) - return cls(**data) + return InvoiceSubmission(**data) @compose(dataclass, partial) class CustomerInfo(_Type): @@ -614,4 +613,9 @@ class Invoice(_Type): address: str ext: JSON +@dataclass +class InvoiceCountStats(_Type): + time: str + count: float + #endregion \ No newline at end of file diff --git a/examples/rest/merchant.py b/examples/rest/merchant.py index 69a1d8b..a113b3f 100644 --- a/examples/rest/merchant.py +++ b/examples/rest/merchant.py @@ -27,4 +27,8 @@ print(bfx.rest.auth.submit_invoice( order_id="order123", customer_info=customer_info, pay_currencies=["ETH"] -)) \ No newline at end of file +)) + +print(bfx.rest.auth.get_invoices()) + +print(bfx.rest.auth.get_invoice_count_stats(status="CREATED", format="Y")) \ No newline at end of file From c41c00259f8154c2ec8f832ec7e03269b0b63083 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Sun, 12 Feb 2023 21:45:01 +0100 Subject: [PATCH 63/85] add endpoints Co-Authored-By: itsdeka --- bfxapi/rest/endpoints/rest_authenticated_endpoints.py | 5 ++++- bfxapi/rest/types.py | 5 +++++ examples/rest/merchant.py | 4 +++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index a69ea28..1493489 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -348,4 +348,7 @@ class RestAuthenticatedEndpoints(Middleware): }) ] def get_invoice_count_stats(self, status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"], format: str) -> List[InvoiceCountStats]: - return [ InvoiceCountStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/count", body={ "status": status, "format": format }) ] \ No newline at end of file + return [ InvoiceCountStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/count", body={ "status": status, "format": format }) ] + + def get_invoice_earning_stats(self, currency: str, format: str) -> List[InvoiceEarningStats]: + return [ InvoiceEarningStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/earning", body={ "currency": currency, "format": format }) ] \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 5f98ca8..3d659b4 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -618,4 +618,9 @@ class InvoiceCountStats(_Type): time: str count: float +@dataclass +class InvoiceEarningStats(_Type): + time: str + count: float + #endregion \ No newline at end of file diff --git a/examples/rest/merchant.py b/examples/rest/merchant.py index a113b3f..9e85d88 100644 --- a/examples/rest/merchant.py +++ b/examples/rest/merchant.py @@ -31,4 +31,6 @@ print(bfx.rest.auth.submit_invoice( print(bfx.rest.auth.get_invoices()) -print(bfx.rest.auth.get_invoice_count_stats(status="CREATED", format="Y")) \ No newline at end of file +print(bfx.rest.auth.get_invoice_count_stats(status="CREATED", format="Y")) + +print(bfx.rest.auth.get_invoice_earning_stats(currency="USD", format="Y")) \ No newline at end of file From 917a4a83673c6a1be9189250fbbe4e53db9e6c79 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Sun, 12 Feb 2023 22:01:12 +0100 Subject: [PATCH 64/85] add Payment Co-Authored-By: itsdeka --- .../endpoints/rest_authenticated_endpoints.py | 26 +++++++++++--- bfxapi/rest/types.py | 35 +++++++++++++++---- examples/rest/merchant.py | 14 ++++++-- 3 files changed, 61 insertions(+), 14 deletions(-) diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index 1493489..9ec3ca0 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -347,8 +347,26 @@ class RestAuthenticatedEndpoints(Middleware): "limit": limit }) ] - def get_invoice_count_stats(self, status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"], format: str) -> List[InvoiceCountStats]: - return [ InvoiceCountStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/count", body={ "status": status, "format": format }) ] + def get_invoice_count_stats(self, status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"], format: str) -> List[InvoiceStats]: + return [ InvoiceStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/count", body={ "status": status, "format": format }) ] - def get_invoice_earning_stats(self, currency: str, format: str) -> List[InvoiceEarningStats]: - return [ InvoiceEarningStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/earning", body={ "currency": currency, "format": format }) ] \ No newline at end of file + def get_invoice_earning_stats(self, currency: str, format: str) -> List[InvoiceStats]: + return [ InvoiceStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/earning", body={ "currency": currency, "format": format }) ] + + def complete_invoice(self, id: str, pay_currency: str, deposit_id: Optional[int] = None, ledger_id: Optional[int] = None) -> InvoiceSubmission: + return InvoiceSubmission.parse(self._POST("auth/w/ext/pay/invoice/complete", body={ + "id": id, "payCcy": pay_currency, "depositId": deposit_id, + "ledgerId": ledger_id + })) + + def expire_invoice(self, id: str) -> InvoiceSubmission: + return InvoiceSubmission.parse(self._POST("auth/w/ext/pay/invoice/expire", body={ "id": id })) + + def get_currency_conversion_list(self) -> List[CurrencyConversion]: + return [ + CurrencyConversion( + base_currency=sub_data["baseCcy"], + convert_currency=sub_data["convertCcy"], + created=sub_data["created"] + ) for sub_data in self._POST("auth/r/ext/pay/settings/convert/list") + ] \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 3d659b4..a2dd647 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -564,20 +564,21 @@ class DerivativePositionCollateralLimits(_Type): #endregion #region Type hinting for models which are not serializable + @compose(dataclass, partial) class InvoiceSubmission(_Type): id: str t: int + merchant_name: str type: Literal["ECOMMERCE", "POS"] duration: int amount: float currency: str order_id: str pay_currencies: List[str] - webhook: str - redirect_url: str status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"] customer_info: Optional["CustomerInfo"] + payment: Optional["Payment"] invoices: List["Invoice"] @classmethod @@ -585,6 +586,9 @@ class InvoiceSubmission(_Type): if "customer_info" in data and data["customer_info"] != None: data["customer_info"] = CustomerInfo(**data["customer_info"]) + if "payment" in data and data["payment"] != None: + data["payment"] = Payment(**data["payment"]) + if "invoices" in data and data["invoices"] != None: for index, invoice in enumerate(data["invoices"]): data["invoices"][index] = Invoice(**invoice) @@ -595,7 +599,6 @@ class InvoiceSubmission(_Type): class CustomerInfo(_Type): nationality: str resid_country: str - resid_state: str resid_city: str resid_zip_code: str resid_street: str @@ -604,6 +607,23 @@ class CustomerInfo(_Type): email: str tos_accepted: bool +@compose(dataclass, partial) +class Payment(_Type): + transaction_id: str + amount: str + currency: str + method: str + status: str + confirmations: int + created: str + updated: str + deposit_id: int + ledger_id: int + force_completed: bool + amount_diff: str + additional_payments: JSON + additional_payment: JSON + @compose(dataclass, partial) class Invoice(_Type): amount: float @@ -614,13 +634,14 @@ class Invoice(_Type): ext: JSON @dataclass -class InvoiceCountStats(_Type): +class InvoiceStats(_Type): time: str count: float @dataclass -class InvoiceEarningStats(_Type): - time: str - count: float +class CurrencyConversion(_Type): + base_currency: str + convert_currency: str + created: int #endregion \ No newline at end of file diff --git a/examples/rest/merchant.py b/examples/rest/merchant.py index 9e85d88..7a4c835 100644 --- a/examples/rest/merchant.py +++ b/examples/rest/merchant.py @@ -20,17 +20,25 @@ customer_info = { "email": "satoshi3@bitfinex.com" } -print(bfx.rest.auth.submit_invoice( +invoice = bfx.rest.auth.submit_invoice( amount=1, currency="USD", duration=864000, order_id="order123", customer_info=customer_info, pay_currencies=["ETH"] -)) +) print(bfx.rest.auth.get_invoices()) print(bfx.rest.auth.get_invoice_count_stats(status="CREATED", format="Y")) -print(bfx.rest.auth.get_invoice_earning_stats(currency="USD", format="Y")) \ No newline at end of file +print(bfx.rest.auth.get_invoice_earning_stats(currency="USD", format="Y")) + +print(bfx.rest.auth.get_currency_conversion_list()) + +print(bfx.rest.auth.complete_invoice( + id=invoice.id, + pay_currency="ETH", + deposit_id=1 +)) \ No newline at end of file From 52bc47597103f721b953c7c9e65981e918f2d05f Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Sun, 12 Feb 2023 22:03:10 +0100 Subject: [PATCH 65/85] add currency endpoints Co-Authored-By: itsdeka --- .../rest/endpoints/rest_authenticated_endpoints.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index 9ec3ca0..0991336 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -369,4 +369,16 @@ class RestAuthenticatedEndpoints(Middleware): convert_currency=sub_data["convertCcy"], created=sub_data["created"] ) for sub_data in self._POST("auth/r/ext/pay/settings/convert/list") - ] \ No newline at end of file + ] + + def add_currency_conversion(self, base_currency: str, convert_currency: str) -> bool: + return bool(self._POST("auth/w/ext/pay/settings/convert/create", body={ + "baseCcy": base_currency, + "convertCcy": convert_currency + })) + + def remove_currency_conversion(self, base_currency: str, convert_currency: str) -> bool: + return bool(self._POST("auth/w/ext/pay/settings/convert/remove", body={ + "baseCcy": base_currency, + "convertCcy": convert_currency + })) \ No newline at end of file From e5b0c1af9c58f46b9d6cea8f6016368aaa999080 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Sun, 12 Feb 2023 22:09:44 +0100 Subject: [PATCH 66/85] Move merchant endpoints in bfxapi.rest.endpoints.rest_merchant_endpoints sub-package. --- bfxapi/rest/endpoints/bfx_rest_interface.py | 8 ++- .../endpoints/rest_authenticated_endpoints.py | 65 +----------------- .../rest/endpoints/rest_merchant_endpoints.py | 68 +++++++++++++++++++ bfxapi/rest/types.py | 2 +- examples/rest/merchant.py | 12 ++-- 5 files changed, 81 insertions(+), 74 deletions(-) create mode 100644 bfxapi/rest/endpoints/rest_merchant_endpoints.py diff --git a/bfxapi/rest/endpoints/bfx_rest_interface.py b/bfxapi/rest/endpoints/bfx_rest_interface.py index 0bddbbc..a2dc6ec 100644 --- a/bfxapi/rest/endpoints/bfx_rest_interface.py +++ b/bfxapi/rest/endpoints/bfx_rest_interface.py @@ -1,11 +1,13 @@ from typing import Optional + from .rest_public_endpoints import RestPublicEndpoints from .rest_authenticated_endpoints import RestAuthenticatedEndpoints +from .rest_merchant_endpoints import RestMerchantEndpoints class BfxRestInterface(object): VERSION = 2 def __init__(self, host: str, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None): - self.public = RestPublicEndpoints(host=host) - - self.auth = RestAuthenticatedEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET) \ No newline at end of file + self.public = RestPublicEndpoints(host=host) + self.auth = RestAuthenticatedEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET) + self.merchant = RestMerchantEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET) \ No newline at end of file diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index 0991336..947032b 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -8,14 +8,6 @@ from .. import serializers from .. enums import Sort, OrderType, FundingOfferType from .. middleware import Middleware -from ... utils.camel_and_snake_case_adapters import to_snake_case_keys, to_camel_case_keys - -_CustomerInfo = TypedDict("_CustomerInfo", { - "nationality": str, "resid_country": str, "resid_city": str, - "resid_zip_code": str, "resid_street": str, "resid_building_no": str, - "full_name": str, "email": str, "tos_accepted": bool -}) - class RestAuthenticatedEndpoints(Middleware): def get_user_info(self) -> UserInfo: return serializers.UserInfo.parse(*self._POST(f"auth/r/info/user")) @@ -326,59 +318,4 @@ class RestAuthenticatedEndpoints(Middleware): "limit": limit } - return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] - - def submit_invoice(self, amount: Union[Decimal, float, str], currency: str, order_id: str, - customer_info: _CustomerInfo, pay_currencies: List[str], duration: Optional[int] = None, - webhook: Optional[str] = None, redirect_url: Optional[str] = None) -> InvoiceSubmission: - body = to_camel_case_keys({ - "amount": amount, "currency": currency, "order_id": order_id, - "customer_info": customer_info, "pay_currencies": pay_currencies, "duration": duration, - "webhook": webhook, "redirect_url": redirect_url - }) - - data = to_snake_case_keys(self._POST("auth/w/ext/pay/invoice/create", body=body)) - - return InvoiceSubmission.parse(data) - - def get_invoices(self, id: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[InvoiceSubmission]: - return [ InvoiceSubmission.parse(sub_data) for sub_data in self._POST("auth/r/ext/pay/invoices", body={ - "id": id, "start": start, "end": end, - "limit": limit - }) ] - - def get_invoice_count_stats(self, status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"], format: str) -> List[InvoiceStats]: - return [ InvoiceStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/count", body={ "status": status, "format": format }) ] - - def get_invoice_earning_stats(self, currency: str, format: str) -> List[InvoiceStats]: - return [ InvoiceStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/earning", body={ "currency": currency, "format": format }) ] - - def complete_invoice(self, id: str, pay_currency: str, deposit_id: Optional[int] = None, ledger_id: Optional[int] = None) -> InvoiceSubmission: - return InvoiceSubmission.parse(self._POST("auth/w/ext/pay/invoice/complete", body={ - "id": id, "payCcy": pay_currency, "depositId": deposit_id, - "ledgerId": ledger_id - })) - - def expire_invoice(self, id: str) -> InvoiceSubmission: - return InvoiceSubmission.parse(self._POST("auth/w/ext/pay/invoice/expire", body={ "id": id })) - - def get_currency_conversion_list(self) -> List[CurrencyConversion]: - return [ - CurrencyConversion( - base_currency=sub_data["baseCcy"], - convert_currency=sub_data["convertCcy"], - created=sub_data["created"] - ) for sub_data in self._POST("auth/r/ext/pay/settings/convert/list") - ] - - def add_currency_conversion(self, base_currency: str, convert_currency: str) -> bool: - return bool(self._POST("auth/w/ext/pay/settings/convert/create", body={ - "baseCcy": base_currency, - "convertCcy": convert_currency - })) - - def remove_currency_conversion(self, base_currency: str, convert_currency: str) -> bool: - return bool(self._POST("auth/w/ext/pay/settings/convert/remove", body={ - "baseCcy": base_currency, - "convertCcy": convert_currency - })) \ No newline at end of file + return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] \ No newline at end of file diff --git a/bfxapi/rest/endpoints/rest_merchant_endpoints.py b/bfxapi/rest/endpoints/rest_merchant_endpoints.py new file mode 100644 index 0000000..3c6f5c7 --- /dev/null +++ b/bfxapi/rest/endpoints/rest_merchant_endpoints.py @@ -0,0 +1,68 @@ +from typing import List, Union, Literal, Optional +from decimal import Decimal + +from .. types import * +from .. middleware import Middleware +from ... utils.camel_and_snake_case_adapters import to_snake_case_keys, to_camel_case_keys + +_CustomerInfo = TypedDict("_CustomerInfo", { + "nationality": str, "resid_country": str, "resid_city": str, + "resid_zip_code": str, "resid_street": str, "resid_building_no": str, + "full_name": str, "email": str, "tos_accepted": bool +}) + +class RestMerchantEndpoints(Middleware): + def submit_invoice(self, amount: Union[Decimal, float, str], currency: str, order_id: str, + customer_info: _CustomerInfo, pay_currencies: List[str], duration: Optional[int] = None, + webhook: Optional[str] = None, redirect_url: Optional[str] = None) -> InvoiceSubmission: + body = to_camel_case_keys({ + "amount": amount, "currency": currency, "order_id": order_id, + "customer_info": customer_info, "pay_currencies": pay_currencies, "duration": duration, + "webhook": webhook, "redirect_url": redirect_url + }) + + data = to_snake_case_keys(self._POST("auth/w/ext/pay/invoice/create", body=body)) + + return InvoiceSubmission.parse(data) + + def get_invoices(self, id: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[InvoiceSubmission]: + return [ InvoiceSubmission.parse(sub_data) for sub_data in self._POST("auth/r/ext/pay/invoices", body={ + "id": id, "start": start, "end": end, + "limit": limit + }) ] + + def get_invoice_count_stats(self, status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"], format: str) -> List[InvoiceStats]: + return [ InvoiceStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/count", body={ "status": status, "format": format }) ] + + def get_invoice_earning_stats(self, currency: str, format: str) -> List[InvoiceStats]: + return [ InvoiceStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/earning", body={ "currency": currency, "format": format }) ] + + def complete_invoice(self, id: str, pay_currency: str, deposit_id: Optional[int] = None, ledger_id: Optional[int] = None) -> InvoiceSubmission: + return InvoiceSubmission.parse(self._POST("auth/w/ext/pay/invoice/complete", body={ + "id": id, "payCcy": pay_currency, "depositId": deposit_id, + "ledgerId": ledger_id + })) + + def expire_invoice(self, id: str) -> InvoiceSubmission: + return InvoiceSubmission.parse(self._POST("auth/w/ext/pay/invoice/expire", body={ "id": id })) + + def get_currency_conversion_list(self) -> List[CurrencyConversion]: + return [ + CurrencyConversion( + base_currency=sub_data["baseCcy"], + convert_currency=sub_data["convertCcy"], + created=sub_data["created"] + ) for sub_data in self._POST("auth/r/ext/pay/settings/convert/list") + ] + + def add_currency_conversion(self, base_currency: str, convert_currency: str) -> bool: + return bool(self._POST("auth/w/ext/pay/settings/convert/create", body={ + "baseCcy": base_currency, + "convertCcy": convert_currency + })) + + def remove_currency_conversion(self, base_currency: str, convert_currency: str) -> bool: + return bool(self._POST("auth/w/ext/pay/settings/convert/remove", body={ + "baseCcy": base_currency, + "convertCcy": convert_currency + })) \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index a2dd647..3bd12e8 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -563,7 +563,7 @@ class DerivativePositionCollateralLimits(_Type): #endregion -#region Type hinting for models which are not serializable +#region Type hinting for Rest Merchant Endpoints @compose(dataclass, partial) class InvoiceSubmission(_Type): diff --git a/examples/rest/merchant.py b/examples/rest/merchant.py index 7a4c835..ec6727b 100644 --- a/examples/rest/merchant.py +++ b/examples/rest/merchant.py @@ -20,7 +20,7 @@ customer_info = { "email": "satoshi3@bitfinex.com" } -invoice = bfx.rest.auth.submit_invoice( +invoice = bfx.rest.merchant.submit_invoice( amount=1, currency="USD", duration=864000, @@ -29,15 +29,15 @@ invoice = bfx.rest.auth.submit_invoice( pay_currencies=["ETH"] ) -print(bfx.rest.auth.get_invoices()) +print(bfx.rest.merchant.get_invoices()) -print(bfx.rest.auth.get_invoice_count_stats(status="CREATED", format="Y")) +print(bfx.rest.merchant.get_invoice_count_stats(status="CREATED", format="Y")) -print(bfx.rest.auth.get_invoice_earning_stats(currency="USD", format="Y")) +print(bfx.rest.merchant.get_invoice_earning_stats(currency="USD", format="Y")) -print(bfx.rest.auth.get_currency_conversion_list()) +print(bfx.rest.merchant.get_currency_conversion_list()) -print(bfx.rest.auth.complete_invoice( +print(bfx.rest.merchant.complete_invoice( id=invoice.id, pay_currency="ETH", deposit_id=1 From f0d14a230f64ef9636c40a5d5aba52c29f47fc49 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Sun, 12 Feb 2023 23:26:44 +0100 Subject: [PATCH 67/85] Fix bug in bfxapi.rest.endpoints.merchant sub-package. --- bfxapi/labeler.py | 9 ++- .../rest/endpoints/rest_merchant_endpoints.py | 10 +-- bfxapi/rest/types.py | 64 ++++++++++--------- bfxapi/utils/camel_and_snake_case_adapters.py | 4 +- 4 files changed, 48 insertions(+), 39 deletions(-) diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 02b6699..213752c 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -14,12 +14,15 @@ def compose(*decorators): def partial(cls): def __init__(self, **kwargs): - for key, value in kwargs.items(): - self.__setattr__(key, value) - for annotation in self.__annotations__.keys(): if annotation not in kwargs: self.__setattr__(annotation, None) + else: self.__setattr__(annotation, kwargs[annotation]) + + kwargs.pop(annotation, None) + + if len(kwargs) != 0: + raise TypeError(f"{cls.__name__}.__init__() got an unexpected keyword argument '{list(kwargs.keys())[0]}'") cls.__init__ = __init__ diff --git a/bfxapi/rest/endpoints/rest_merchant_endpoints.py b/bfxapi/rest/endpoints/rest_merchant_endpoints.py index 3c6f5c7..dd760c5 100644 --- a/bfxapi/rest/endpoints/rest_merchant_endpoints.py +++ b/bfxapi/rest/endpoints/rest_merchant_endpoints.py @@ -26,10 +26,10 @@ class RestMerchantEndpoints(Middleware): return InvoiceSubmission.parse(data) def get_invoices(self, id: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[InvoiceSubmission]: - return [ InvoiceSubmission.parse(sub_data) for sub_data in self._POST("auth/r/ext/pay/invoices", body={ + return [ InvoiceSubmission.parse(sub_data) for sub_data in to_snake_case_keys(self._POST("auth/r/ext/pay/invoices", body={ "id": id, "start": start, "end": end, "limit": limit - }) ] + })) ] def get_invoice_count_stats(self, status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"], format: str) -> List[InvoiceStats]: return [ InvoiceStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/count", body={ "status": status, "format": format }) ] @@ -38,13 +38,13 @@ class RestMerchantEndpoints(Middleware): return [ InvoiceStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/earning", body={ "currency": currency, "format": format }) ] def complete_invoice(self, id: str, pay_currency: str, deposit_id: Optional[int] = None, ledger_id: Optional[int] = None) -> InvoiceSubmission: - return InvoiceSubmission.parse(self._POST("auth/w/ext/pay/invoice/complete", body={ + return InvoiceSubmission.parse(to_snake_case_keys(self._POST("auth/w/ext/pay/invoice/complete", body={ "id": id, "payCcy": pay_currency, "depositId": deposit_id, "ledgerId": ledger_id - })) + }))) def expire_invoice(self, id: str) -> InvoiceSubmission: - return InvoiceSubmission.parse(self._POST("auth/w/ext/pay/invoice/expire", body={ "id": id })) + return InvoiceSubmission.parse(to_snake_case_keys(self._POST("auth/w/ext/pay/invoice/expire", body={ "id": id }))) def get_currency_conversion_list(self) -> List[CurrencyConversion]: return [ diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 3bd12e8..b0c7270 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -569,63 +569,54 @@ class DerivativePositionCollateralLimits(_Type): class InvoiceSubmission(_Type): id: str t: int - merchant_name: str type: Literal["ECOMMERCE", "POS"] duration: int amount: float currency: str order_id: str pay_currencies: List[str] + webhook: Optional[str] + redirect_url: Optional[str] status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"] - customer_info: Optional["CustomerInfo"] - payment: Optional["Payment"] - invoices: List["Invoice"] + customer_info: Optional["_CustomerInfo"] + invoices: List["_Invoice"] + payment: Optional["_Payment"] + additional_payments: Optional[List["_Payment"]] + + merchant_name: str @classmethod def parse(cls, data: Dict[str, Any]) -> "InvoiceSubmission": if "customer_info" in data and data["customer_info"] != None: - data["customer_info"] = CustomerInfo(**data["customer_info"]) + data["customer_info"] = _CustomerInfo(**data["customer_info"]) + + for index, invoice in enumerate(data["invoices"]): + data["invoices"][index] = _Invoice(**invoice) if "payment" in data and data["payment"] != None: - data["payment"] = Payment(**data["payment"]) + data["payment"] = _Payment(**data["payment"]) - if "invoices" in data and data["invoices"] != None: - for index, invoice in enumerate(data["invoices"]): - data["invoices"][index] = Invoice(**invoice) + if "additional_payments" in data and data["additional_payments"] != None: + for index, additional_payment in enumerate(data["additional_payments"]): + data["additional_payments"][index] = _Payment(**additional_payment) return InvoiceSubmission(**data) @compose(dataclass, partial) -class CustomerInfo(_Type): +class _CustomerInfo: nationality: str resid_country: str + resid_state: Optional[str] resid_city: str resid_zip_code: str resid_street: str - resid_building_no: str + resid_building_no: Optional[str] full_name: str email: str tos_accepted: bool @compose(dataclass, partial) -class Payment(_Type): - transaction_id: str - amount: str - currency: str - method: str - status: str - confirmations: int - created: str - updated: str - deposit_id: int - ledger_id: int - force_completed: bool - amount_diff: str - additional_payments: JSON - additional_payment: JSON - -@compose(dataclass, partial) -class Invoice(_Type): +class _Invoice: amount: float currency: str pay_currency: str @@ -633,6 +624,21 @@ class Invoice(_Type): address: str ext: JSON +@compose(dataclass, partial) +class _Payment: + txid: str + amount: float + currency: str + method: str + status: Literal["CREATED", "COMPLETED", "PROCESSING"] + confirmations: int + created_at: str + updated_at: str + deposit_id: Optional[int] + ledger_id: Optional[int] + force_completed: Optional[bool] + amount_diff: Optional[str] + @dataclass class InvoiceStats(_Type): time: str diff --git a/bfxapi/utils/camel_and_snake_case_adapters.py b/bfxapi/utils/camel_and_snake_case_adapters.py index 85ccdf4..7255940 100644 --- a/bfxapi/utils/camel_and_snake_case_adapters.py +++ b/bfxapi/utils/camel_and_snake_case_adapters.py @@ -15,8 +15,8 @@ def _scheme(data: T, adapter: Callable[[str], str]) -> T: return cast(T, { adapter(key): _scheme(value, adapter) for key, value in data.items() }) else: return data -def to_snake_case_keys(dictionary: Dict[str, Any]) -> Dict[str, Any]: +def to_snake_case_keys(dictionary: T) -> T: return _scheme(dictionary, _to_snake_case) -def to_camel_case_keys(dictionary: Dict[str, Any]) -> Dict[str, Any]: +def to_camel_case_keys(dictionary: T) -> T: return _scheme(dictionary, _to_camel_case) \ No newline at end of file From 39f317ba4001095d23e53e06188de28d0718971f Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 13 Feb 2023 16:22:39 +0100 Subject: [PATCH 68/85] Rename bfxapi.utils.camel_and_snake_case_adapters to bfxapi.utils.camel_and_snake_case_helpers. --- bfxapi/rest/endpoints/rest_merchant_endpoints.py | 2 +- ...d_snake_case_adapters.py => camel_and_snake_case_helpers.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename bfxapi/utils/{camel_and_snake_case_adapters.py => camel_and_snake_case_helpers.py} (100%) diff --git a/bfxapi/rest/endpoints/rest_merchant_endpoints.py b/bfxapi/rest/endpoints/rest_merchant_endpoints.py index dd760c5..8290a2c 100644 --- a/bfxapi/rest/endpoints/rest_merchant_endpoints.py +++ b/bfxapi/rest/endpoints/rest_merchant_endpoints.py @@ -3,7 +3,7 @@ from decimal import Decimal from .. types import * from .. middleware import Middleware -from ... utils.camel_and_snake_case_adapters import to_snake_case_keys, to_camel_case_keys +from ...utils.camel_and_snake_case_helpers import to_snake_case_keys, to_camel_case_keys _CustomerInfo = TypedDict("_CustomerInfo", { "nationality": str, "resid_country": str, "resid_city": str, diff --git a/bfxapi/utils/camel_and_snake_case_adapters.py b/bfxapi/utils/camel_and_snake_case_helpers.py similarity index 100% rename from bfxapi/utils/camel_and_snake_case_adapters.py rename to bfxapi/utils/camel_and_snake_case_helpers.py From 821541134a91c23676820ba5c88a173d1c1937c1 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 13 Feb 2023 17:29:35 +0100 Subject: [PATCH 69/85] Fix bug and refactor code in bfxapi.rest.types sub-package. --- bfxapi/rest/types.py | 85 ++++++++++++++++++++++---------------------- 1 file changed, 42 insertions(+), 43 deletions(-) diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index b0c7270..5d6b22c 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -578,66 +578,65 @@ class InvoiceSubmission(_Type): webhook: Optional[str] redirect_url: Optional[str] status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"] - customer_info: Optional["_CustomerInfo"] - invoices: List["_Invoice"] - payment: Optional["_Payment"] - additional_payments: Optional[List["_Payment"]] - + customer_info: Optional["CustomerInfo"] + invoices: List["Invoice"] + payment: Optional["Payment"] + additional_payments: Optional[List["Payment"]] merchant_name: str @classmethod def parse(cls, data: Dict[str, Any]) -> "InvoiceSubmission": if "customer_info" in data and data["customer_info"] != None: - data["customer_info"] = _CustomerInfo(**data["customer_info"]) + data["customer_info"] = InvoiceSubmission.CustomerInfo(**data["customer_info"]) for index, invoice in enumerate(data["invoices"]): - data["invoices"][index] = _Invoice(**invoice) + data["invoices"][index] = InvoiceSubmission.Invoice(**invoice) if "payment" in data and data["payment"] != None: - data["payment"] = _Payment(**data["payment"]) + data["payment"] = InvoiceSubmission.Payment(**data["payment"]) if "additional_payments" in data and data["additional_payments"] != None: for index, additional_payment in enumerate(data["additional_payments"]): - data["additional_payments"][index] = _Payment(**additional_payment) + data["additional_payments"][index] = InvoiceSubmission.Payment(**additional_payment) return InvoiceSubmission(**data) -@compose(dataclass, partial) -class _CustomerInfo: - nationality: str - resid_country: str - resid_state: Optional[str] - resid_city: str - resid_zip_code: str - resid_street: str - resid_building_no: Optional[str] - full_name: str - email: str - tos_accepted: bool + @compose(dataclass, partial) + class CustomerInfo: + nationality: str + resid_country: str + resid_state: Optional[str] + resid_city: str + resid_zip_code: str + resid_street: str + resid_building_no: Optional[str] + full_name: str + email: str + tos_accepted: bool -@compose(dataclass, partial) -class _Invoice: - amount: float - currency: str - pay_currency: str - pool_currency: str - address: str - ext: JSON + @compose(dataclass, partial) + class Invoice: + amount: float + currency: str + pay_currency: str + pool_currency: str + address: str + ext: JSON -@compose(dataclass, partial) -class _Payment: - txid: str - amount: float - currency: str - method: str - status: Literal["CREATED", "COMPLETED", "PROCESSING"] - confirmations: int - created_at: str - updated_at: str - deposit_id: Optional[int] - ledger_id: Optional[int] - force_completed: Optional[bool] - amount_diff: Optional[str] + @compose(dataclass, partial) + class Payment: + txid: str + amount: float + currency: str + method: str + status: Literal["CREATED", "COMPLETED", "PROCESSING"] + confirmations: int + created_at: str + updated_at: str + deposit_id: Optional[int] + ledger_id: Optional[int] + force_completed: Optional[bool] + amount_diff: Optional[str] @dataclass class InvoiceStats(_Type): From 6a7577f98b30525603971492c51c975741e67c55 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 13 Feb 2023 17:32:46 +0100 Subject: [PATCH 70/85] Remove Optional typing in bfxapi.rest.types. --- bfxapi/rest/types.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 5d6b22c..12fdb29 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -575,13 +575,13 @@ class InvoiceSubmission(_Type): currency: str order_id: str pay_currencies: List[str] - webhook: Optional[str] - redirect_url: Optional[str] + webhook: str + redirect_url: str status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"] - customer_info: Optional["CustomerInfo"] + customer_info: "CustomerInfo" invoices: List["Invoice"] - payment: Optional["Payment"] - additional_payments: Optional[List["Payment"]] + payment: "Payment" + additional_payments: List["Payment"] merchant_name: str @classmethod @@ -605,11 +605,11 @@ class InvoiceSubmission(_Type): class CustomerInfo: nationality: str resid_country: str - resid_state: Optional[str] + resid_state: str resid_city: str resid_zip_code: str resid_street: str - resid_building_no: Optional[str] + resid_building_no: str full_name: str email: str tos_accepted: bool @@ -633,10 +633,10 @@ class InvoiceSubmission(_Type): confirmations: int created_at: str updated_at: str - deposit_id: Optional[int] - ledger_id: Optional[int] - force_completed: Optional[bool] - amount_diff: Optional[str] + deposit_id: int + ledger_id: int + force_completed: bool + amount_diff: str @dataclass class InvoiceStats(_Type): From f0f150cec2f79d7c40f20cf00a30a364378a27e0 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 13 Feb 2023 19:09:38 +0100 Subject: [PATCH 71/85] Split websocket package in multiple sub-package. Split handlers.py in public_channels_handler.py and authenticated_channels_handler.py. Rename files attaining to new conventions. --- bfxapi/rest/__init__.py | 3 +- bfxapi/rest/endpoints/__init__.py | 2 + bfxapi/websocket/__init__.py | 4 +- bfxapi/websocket/client/__init__.py | 5 ++ .../bfx_websocket_bucket.py} | 12 +-- .../bfx_websocket_client.py} | 20 ++--- .../bfx_websocket_inputs.py} | 6 +- bfxapi/websocket/handlers/__init__.py | 3 + .../authenticated_channels_handler.py | 71 ++++++++++++++++++ .../public_channels_handler.py} | 74 +------------------ bfxapi/websocket/subscriptions.py | 8 +- 11 files changed, 117 insertions(+), 91 deletions(-) create mode 100644 bfxapi/websocket/client/__init__.py rename bfxapi/websocket/{_BfxWebsocketBucket.py => client/bfx_websocket_bucket.py} (87%) rename bfxapi/websocket/{BfxWebsocketClient.py => client/bfx_websocket_client.py} (89%) rename bfxapi/websocket/{_BfxWebsocketInputs.py => client/bfx_websocket_inputs.py} (96%) create mode 100644 bfxapi/websocket/handlers/__init__.py create mode 100644 bfxapi/websocket/handlers/authenticated_channels_handler.py rename bfxapi/websocket/{handlers.py => handlers/public_channels_handler.py} (60%) diff --git a/bfxapi/rest/__init__.py b/bfxapi/rest/__init__.py index 7ee9fed..71e3b54 100644 --- a/bfxapi/rest/__init__.py +++ b/bfxapi/rest/__init__.py @@ -1,3 +1,4 @@ -from .endpoints import BfxRestInterface, RestPublicEndpoints, RestAuthenticatedEndpoints +from .endpoints import BfxRestInterface, RestPublicEndpoints, RestAuthenticatedEndpoints, \ + RestMerchantEndpoints NAME = "rest" \ No newline at end of file diff --git a/bfxapi/rest/endpoints/__init__.py b/bfxapi/rest/endpoints/__init__.py index 24a005d..e35d6fb 100644 --- a/bfxapi/rest/endpoints/__init__.py +++ b/bfxapi/rest/endpoints/__init__.py @@ -1,5 +1,7 @@ from .bfx_rest_interface import BfxRestInterface + from .rest_public_endpoints import RestPublicEndpoints from .rest_authenticated_endpoints import RestAuthenticatedEndpoints +from .rest_merchant_endpoints import RestMerchantEndpoints NAME = "endpoints" \ No newline at end of file diff --git a/bfxapi/websocket/__init__.py b/bfxapi/websocket/__init__.py index e24f778..1287433 100644 --- a/bfxapi/websocket/__init__.py +++ b/bfxapi/websocket/__init__.py @@ -1 +1,3 @@ -from .BfxWebsocketClient import BfxWebsocketClient \ No newline at end of file +from .client import BfxWebsocketClient, BfxWebsocketBucket, BfxWebsocketInputs + +NAME = "websocket" \ No newline at end of file diff --git a/bfxapi/websocket/client/__init__.py b/bfxapi/websocket/client/__init__.py new file mode 100644 index 0000000..50057cb --- /dev/null +++ b/bfxapi/websocket/client/__init__.py @@ -0,0 +1,5 @@ +from .bfx_websocket_client import BfxWebsocketClient +from .bfx_websocket_bucket import BfxWebsocketBucket +from .bfx_websocket_inputs import BfxWebsocketInputs + +NAME = "client" \ No newline at end of file diff --git a/bfxapi/websocket/_BfxWebsocketBucket.py b/bfxapi/websocket/client/bfx_websocket_bucket.py similarity index 87% rename from bfxapi/websocket/_BfxWebsocketBucket.py rename to bfxapi/websocket/client/bfx_websocket_bucket.py index 2cfe48c..550581d 100644 --- a/bfxapi/websocket/_BfxWebsocketBucket.py +++ b/bfxapi/websocket/client/bfx_websocket_bucket.py @@ -2,9 +2,9 @@ import json, uuid, websockets from typing import Literal, TypeVar, Callable, cast -from .handlers import PublicChannelsHandler +from ..handlers import PublicChannelsHandler -from .exceptions import ConnectionNotOpen, TooManySubscriptions, OutdatedClientVersion +from ..exceptions import ConnectionNotOpen, TooManySubscriptions, OutdatedClientVersion _HEARTBEAT = "hb" @@ -19,7 +19,7 @@ def _require_websocket_connection(function: F) -> F: return cast(F, wrapper) -class _BfxWebsocketBucket(object): +class BfxWebsocketBucket(object): VERSION = 2 MAXIMUM_SUBSCRIPTIONS_AMOUNT = 25 @@ -42,8 +42,8 @@ class _BfxWebsocketBucket(object): message = json.loads(message) if isinstance(message, dict) and message["event"] == "info" and "version" in message: - if _BfxWebsocketBucket.VERSION != message["version"]: - raise OutdatedClientVersion(f"Mismatch between the client version and the server version. Update the library to the latest version to continue (client version: {_BfxWebsocketBucket.VERSION}, server version: {message['version']}).") + if BfxWebsocketBucket.VERSION != message["version"]: + raise OutdatedClientVersion(f"Mismatch between the client version and the server version. Update the library to the latest version to continue (client version: {BfxWebsocketBucket.VERSION}, server version: {message['version']}).") elif isinstance(message, dict) and message["event"] == "subscribed" and (chanId := message["chanId"]): self.pendings = [ pending for pending in self.pendings if pending["subId"] != message["subId"] ] self.subscriptions[chanId] = message @@ -60,7 +60,7 @@ class _BfxWebsocketBucket(object): @_require_websocket_connection async def _subscribe(self, channel, subId=None, **kwargs): - if len(self.subscriptions) + len(self.pendings) == _BfxWebsocketBucket.MAXIMUM_SUBSCRIPTIONS_AMOUNT: + if len(self.subscriptions) + len(self.pendings) == BfxWebsocketBucket.MAXIMUM_SUBSCRIPTIONS_AMOUNT: raise TooManySubscriptions("The client has reached the maximum number of subscriptions.") subscription = { diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/client/bfx_websocket_client.py similarity index 89% rename from bfxapi/websocket/BfxWebsocketClient.py rename to bfxapi/websocket/client/bfx_websocket_client.py index 98b5b75..8dccf79 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -4,15 +4,17 @@ from typing import Literal, TypeVar, Callable, cast from pyee.asyncio import AsyncIOEventEmitter -from ._BfxWebsocketBucket import _HEARTBEAT, F, _require_websocket_connection, _BfxWebsocketBucket +from .bfx_websocket_bucket import _HEARTBEAT, F, _require_websocket_connection, BfxWebsocketBucket -from ._BfxWebsocketInputs import _BfxWebsocketInputs -from .handlers import Channels, PublicChannelsHandler, AuthenticatedChannelsHandler -from .exceptions import WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported +from .bfx_websocket_inputs import BfxWebsocketInputs +from ..handlers import PublicChannelsHandler, AuthenticatedChannelsHandler +from ..exceptions import WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported -from ..utils.JSONEncoder import JSONEncoder +from ..enums import Channels -from ..utils.logger import Formatter, CustomLogger +from ...utils.JSONEncoder import JSONEncoder + +from ...utils.logger import Formatter, CustomLogger def _require_websocket_authentication(function: F) -> F: async def wrapper(self, *args, **kwargs): @@ -24,7 +26,7 @@ def _require_websocket_authentication(function: F) -> F: return cast(F, wrapper) class BfxWebsocketClient(object): - VERSION = _BfxWebsocketBucket.VERSION + VERSION = BfxWebsocketBucket.VERSION MAXIMUM_BUCKETS_AMOUNT = 20 @@ -46,9 +48,9 @@ class BfxWebsocketClient(object): self.handler = AuthenticatedChannelsHandler(event_emitter=self.event_emitter) - self.buckets = [ _BfxWebsocketBucket(self.host, self.event_emitter, self.__bucket_open_signal) for _ in range(buckets) ] + self.buckets = [ BfxWebsocketBucket(self.host, self.event_emitter, self.__bucket_open_signal) for _ in range(buckets) ] - self.inputs = _BfxWebsocketInputs(self.__handle_websocket_input) + self.inputs = BfxWebsocketInputs(self.__handle_websocket_input) self.logger = CustomLogger("BfxWebsocketClient", logLevel=log_level) diff --git a/bfxapi/websocket/_BfxWebsocketInputs.py b/bfxapi/websocket/client/bfx_websocket_inputs.py similarity index 96% rename from bfxapi/websocket/_BfxWebsocketInputs.py rename to bfxapi/websocket/client/bfx_websocket_inputs.py index 0d9ee0b..48e3137 100644 --- a/bfxapi/websocket/_BfxWebsocketInputs.py +++ b/bfxapi/websocket/client/bfx_websocket_inputs.py @@ -2,10 +2,10 @@ from decimal import Decimal from datetime import datetime from typing import Union, Optional, List, Tuple -from .types import JSON -from .enums import OrderType, FundingOfferType +from ..types import JSON +from ..enums import OrderType, FundingOfferType -class _BfxWebsocketInputs(object): +class BfxWebsocketInputs(object): def __init__(self, __handle_websocket_input): self.__handle_websocket_input = __handle_websocket_input diff --git a/bfxapi/websocket/handlers/__init__.py b/bfxapi/websocket/handlers/__init__.py new file mode 100644 index 0000000..4fe650a --- /dev/null +++ b/bfxapi/websocket/handlers/__init__.py @@ -0,0 +1,3 @@ +from .public_channels_handler import PublicChannelsHandler +from .authenticated_channels_handler import AuthenticatedChannelsHandler +NAME = "handlers" \ No newline at end of file diff --git a/bfxapi/websocket/handlers/authenticated_channels_handler.py b/bfxapi/websocket/handlers/authenticated_channels_handler.py new file mode 100644 index 0000000..2205012 --- /dev/null +++ b/bfxapi/websocket/handlers/authenticated_channels_handler.py @@ -0,0 +1,71 @@ +from typing import List + +from ..types import * + +from .. import serializers + +from ..exceptions import BfxWebsocketException + +class AuthenticatedChannelsHandler(object): + __abbreviations = { + "os": "order_snapshot", "on": "order_new", "ou": "order_update", "oc": "order_cancel", + "ps": "position_snapshot", "pn": "position_new", "pu": "position_update", "pc": "position_close", + "te": "trade_executed", "tu": "trade_execution_update", + "fos": "funding_offer_snapshot", "fon": "funding_offer_new", "fou": "funding_offer_update", "foc": "funding_offer_cancel", + "fcs": "funding_credit_snapshot", "fcn": "funding_credit_new", "fcu": "funding_credit_update", "fcc": "funding_credit_close", + "fls": "funding_loan_snapshot", "fln": "funding_loan_new", "flu": "funding_loan_update", "flc": "funding_loan_close", + "ws": "wallet_snapshot", "wu": "wallet_update", + "bu": "balance_update", + } + + __serializers = { + ("os", "on", "ou", "oc",): serializers.Order, + ("ps", "pn", "pu", "pc",): serializers.Position, + ("te", "tu"): serializers.Trade, + ("fos", "fon", "fou", "foc",): serializers.FundingOffer, + ("fcs", "fcn", "fcu", "fcc",): serializers.FundingCredit, + ("fls", "fln", "flu", "flc",): serializers.FundingLoan, + ("ws", "wu",): serializers.Wallet, + ("bu",): serializers.Balance + } + + EVENTS = [ + "notification", + "on-req-notification", "ou-req-notification", "oc-req-notification", + "oc_multi-notification", + "fon-req-notification", "foc-req-notification", + *list(__abbreviations.values()) + ] + + def __init__(self, event_emitter, strict = False): + self.event_emitter, self.strict = event_emitter, strict + + def handle(self, type, stream): + if type == "n": + return self.__notification(stream) + + for types, serializer in AuthenticatedChannelsHandler.__serializers.items(): + if type in types: + event = AuthenticatedChannelsHandler.__abbreviations[type] + + if all(isinstance(substream, list) for substream in stream): + return self.event_emitter.emit(event, [ serializer.parse(*substream) for substream in stream ]) + + return self.event_emitter.emit(event, serializer.parse(*stream)) + + if self.strict == True: + raise BfxWebsocketException(f"Event of type <{type}> not found in self.__handlers.") + + def __notification(self, stream): + type, serializer = "notification", serializers._Notification(serializer=None) + + if stream[1] == "on-req" or stream[1] == "ou-req" or stream[1] == "oc-req": + type, serializer = f"{stream[1]}-notification", serializers._Notification[Order](serializer=serializers.Order) + + if stream[1] == "oc_multi-req": + type, serializer = f"{stream[1]}-notification", serializers._Notification[List[Order]](serializer=serializers.Order, iterate=True) + + if stream[1] == "fon-req" or stream[1] == "foc-req": + type, serializer = f"{stream[1]}-notification", serializers._Notification[FundingOffer](serializer=serializers.FundingOffer) + + return self.event_emitter.emit(type, serializer.parse(*stream)) \ No newline at end of file diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers/public_channels_handler.py similarity index 60% rename from bfxapi/websocket/handlers.py rename to bfxapi/websocket/handlers/public_channels_handler.py index 686501b..154e677 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers/public_channels_handler.py @@ -1,10 +1,8 @@ -from typing import List +from ..types import * -from .types import * +from .. import serializers -from . import serializers -from .enums import Channels -from .exceptions import BfxWebsocketException +from ..enums import Channels class PublicChannelsHandler(object): EVENTS = [ @@ -117,68 +115,4 @@ class PublicChannelsHandler(object): "derivatives_status_update", subscription, serializers.DerivativesStatus.parse(*stream[0]) - ) - -class AuthenticatedChannelsHandler(object): - __abbreviations = { - "os": "order_snapshot", "on": "order_new", "ou": "order_update", "oc": "order_cancel", - "ps": "position_snapshot", "pn": "position_new", "pu": "position_update", "pc": "position_close", - "te": "trade_executed", "tu": "trade_execution_update", - "fos": "funding_offer_snapshot", "fon": "funding_offer_new", "fou": "funding_offer_update", "foc": "funding_offer_cancel", - "fcs": "funding_credit_snapshot", "fcn": "funding_credit_new", "fcu": "funding_credit_update", "fcc": "funding_credit_close", - "fls": "funding_loan_snapshot", "fln": "funding_loan_new", "flu": "funding_loan_update", "flc": "funding_loan_close", - "ws": "wallet_snapshot", "wu": "wallet_update", - "bu": "balance_update", - } - - __serializers = { - ("os", "on", "ou", "oc",): serializers.Order, - ("ps", "pn", "pu", "pc",): serializers.Position, - ("te", "tu"): serializers.Trade, - ("fos", "fon", "fou", "foc",): serializers.FundingOffer, - ("fcs", "fcn", "fcu", "fcc",): serializers.FundingCredit, - ("fls", "fln", "flu", "flc",): serializers.FundingLoan, - ("ws", "wu",): serializers.Wallet, - ("bu",): serializers.Balance - } - - EVENTS = [ - "notification", - "on-req-notification", "ou-req-notification", "oc-req-notification", - "oc_multi-notification", - "fon-req-notification", "foc-req-notification", - *list(__abbreviations.values()) - ] - - def __init__(self, event_emitter, strict = False): - self.event_emitter, self.strict = event_emitter, strict - - def handle(self, type, stream): - if type == "n": - return self.__notification(stream) - - for types, serializer in AuthenticatedChannelsHandler.__serializers.items(): - if type in types: - event = AuthenticatedChannelsHandler.__abbreviations[type] - - if all(isinstance(substream, list) for substream in stream): - return self.event_emitter.emit(event, [ serializer.parse(*substream) for substream in stream ]) - - return self.event_emitter.emit(event, serializer.parse(*stream)) - - if self.strict == True: - raise BfxWebsocketException(f"Event of type <{type}> not found in self.__handlers.") - - def __notification(self, stream): - type, serializer = "notification", serializers._Notification(serializer=None) - - if stream[1] == "on-req" or stream[1] == "ou-req" or stream[1] == "oc-req": - type, serializer = f"{stream[1]}-notification", serializers._Notification[Order](serializer=serializers.Order) - - if stream[1] == "oc_multi-req": - type, serializer = f"{stream[1]}-notification", serializers._Notification[List[Order]](serializer=serializers.Order, iterate=True) - - if stream[1] == "fon-req" or stream[1] == "foc-req": - type, serializer = f"{stream[1]}-notification", serializers._Notification[FundingOffer](serializer=serializers.FundingOffer) - - return self.event_emitter.emit(type, serializer.parse(*stream)) \ No newline at end of file + ) \ No newline at end of file diff --git a/bfxapi/websocket/subscriptions.py b/bfxapi/websocket/subscriptions.py index e22bb5e..5e2d692 100644 --- a/bfxapi/websocket/subscriptions.py +++ b/bfxapi/websocket/subscriptions.py @@ -1,6 +1,8 @@ -from typing import TypedDict, Optional +from typing import TypedDict, Union, Optional __all__ = [ + "Subscription", + "Ticker", "Trades", "Book", @@ -8,6 +10,10 @@ __all__ = [ "Status" ] +_Header = TypedDict("_Header", { "event": str, "channel": str, "subId": str }) + +Subscription = Union["Ticker", "Trades", "Book", "Candles", "Status"] + class Ticker(TypedDict): chanId: int; symbol: str pair: Optional[str] From 17c95027334c7c1d5efe6a0108cbd34c1690b563 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 14 Feb 2023 16:29:50 +0100 Subject: [PATCH 72/85] Apply fixes and refactoring to the bfxapi.handlers sub-package. --- bfxapi/websocket/exceptions.py | 7 +++++ .../authenticated_channels_handler.py | 22 +++++++-------- .../handlers/public_channels_handler.py | 27 ++++++++++--------- 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/bfxapi/websocket/exceptions.py b/bfxapi/websocket/exceptions.py index 5691af8..40a6a1e 100644 --- a/bfxapi/websocket/exceptions.py +++ b/bfxapi/websocket/exceptions.py @@ -58,4 +58,11 @@ class InvalidAuthenticationCredentials(BfxWebsocketException): This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication. """ + pass + +class HandlerNotFound(BfxWebsocketException): + """ + This error indicates that a handler was not found for an incoming message. + """ + pass \ No newline at end of file diff --git a/bfxapi/websocket/handlers/authenticated_channels_handler.py b/bfxapi/websocket/handlers/authenticated_channels_handler.py index 2205012..2dbd83f 100644 --- a/bfxapi/websocket/handlers/authenticated_channels_handler.py +++ b/bfxapi/websocket/handlers/authenticated_channels_handler.py @@ -1,10 +1,8 @@ -from typing import List - -from ..types import * - from .. import serializers -from ..exceptions import BfxWebsocketException +from .. types import * + +from .. exceptions import HandlerNotFound class AuthenticatedChannelsHandler(object): __abbreviations = { @@ -37,7 +35,7 @@ class AuthenticatedChannelsHandler(object): *list(__abbreviations.values()) ] - def __init__(self, event_emitter, strict = False): + def __init__(self, event_emitter, strict = True): self.event_emitter, self.strict = event_emitter, strict def handle(self, type, stream): @@ -52,20 +50,20 @@ class AuthenticatedChannelsHandler(object): return self.event_emitter.emit(event, [ serializer.parse(*substream) for substream in stream ]) return self.event_emitter.emit(event, serializer.parse(*stream)) - - if self.strict == True: - raise BfxWebsocketException(f"Event of type <{type}> not found in self.__handlers.") + + if self.strict: + raise HandlerNotFound(f"No handler found for event of type <{type}>.") def __notification(self, stream): type, serializer = "notification", serializers._Notification(serializer=None) if stream[1] == "on-req" or stream[1] == "ou-req" or stream[1] == "oc-req": - type, serializer = f"{stream[1]}-notification", serializers._Notification[Order](serializer=serializers.Order) + type, serializer = f"{stream[1]}-notification", serializers._Notification(serializer=serializers.Order) if stream[1] == "oc_multi-req": - type, serializer = f"{stream[1]}-notification", serializers._Notification[List[Order]](serializer=serializers.Order, iterate=True) + type, serializer = f"{stream[1]}-notification", serializers._Notification(serializer=serializers.Order, iterate=True) if stream[1] == "fon-req" or stream[1] == "foc-req": - type, serializer = f"{stream[1]}-notification", serializers._Notification[FundingOffer](serializer=serializers.FundingOffer) + type, serializer = f"{stream[1]}-notification", serializers._Notification(serializer=serializers.FundingOffer) return self.event_emitter.emit(type, serializer.parse(*stream)) \ No newline at end of file diff --git a/bfxapi/websocket/handlers/public_channels_handler.py b/bfxapi/websocket/handlers/public_channels_handler.py index 154e677..d63ea1f 100644 --- a/bfxapi/websocket/handlers/public_channels_handler.py +++ b/bfxapi/websocket/handlers/public_channels_handler.py @@ -1,8 +1,8 @@ -from ..types import * - from .. import serializers -from ..enums import Channels +from .. types import * + +from .. exceptions import HandlerNotFound class PublicChannelsHandler(object): EVENTS = [ @@ -13,23 +13,26 @@ class PublicChannelsHandler(object): "derivatives_status_update", ] - def __init__(self, event_emitter): - self.event_emitter = event_emitter + def __init__(self, event_emitter, strict = True): + self.event_emitter, self.strict = event_emitter, strict self.__handlers = { - Channels.TICKER: self.__ticker_channel_handler, - Channels.TRADES: self.__trades_channel_handler, - Channels.BOOK: self.__book_channel_handler, - Channels.CANDLES: self.__candles_channel_handler, - Channels.STATUS: self.__status_channel_handler + "ticker": self.__ticker_channel_handler, + "trades": self.__trades_channel_handler, + "book": self.__book_channel_handler, + "candles": self.__candles_channel_handler, + "status": self.__status_channel_handler } def handle(self, subscription, *stream): _clear = lambda dictionary, *args: { key: value for key, value in dictionary.items() if key not in args } - if channel := subscription["channel"] or channel in self.__handlers.keys(): + if (channel := subscription["channel"]) and channel in self.__handlers.keys(): return self.__handlers[channel](_clear(subscription, "event", "channel", "subId"), *stream) + if self.strict: + raise HandlerNotFound(f"No handler found for channel <{subscription['channel']}>.") + def __ticker_channel_handler(self, subscription, *stream): if subscription["symbol"].startswith("t"): return self.event_emitter.emit( @@ -46,7 +49,7 @@ class PublicChannelsHandler(object): ) def __trades_channel_handler(self, subscription, *stream): - if type := stream[0] or type in [ "te", "tu", "fte", "ftu" ]: + if (type := stream[0]) and type in [ "te", "tu", "fte", "ftu" ]: if subscription["symbol"].startswith("t"): return self.event_emitter.emit( { "te": "t_trade_executed", "tu": "t_trade_execution_update" }[type], From 49517f9709c708c52ddd29af42c7627bf55e28af Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 14 Feb 2023 17:03:16 +0100 Subject: [PATCH 73/85] Small fix in package import statements. --- bfxapi/rest/endpoints/rest_authenticated_endpoints.py | 2 +- bfxapi/rest/endpoints/rest_merchant_endpoints.py | 3 ++- bfxapi/rest/types.py | 2 +- bfxapi/utils/JSONEncoder.py | 2 -- bfxapi/websocket/client/bfx_websocket_inputs.py | 4 ++-- bfxapi/websocket/enums.py | 2 +- bfxapi/websocket/types.py | 8 ++++---- 7 files changed, 11 insertions(+), 12 deletions(-) diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index 947032b..7b4e11e 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -1,4 +1,4 @@ -from typing import List, Union, Literal, Optional +from typing import List, Tuple, Union, Literal, Optional from decimal import Decimal from datetime import datetime diff --git a/bfxapi/rest/endpoints/rest_merchant_endpoints.py b/bfxapi/rest/endpoints/rest_merchant_endpoints.py index 8290a2c..0c80110 100644 --- a/bfxapi/rest/endpoints/rest_merchant_endpoints.py +++ b/bfxapi/rest/endpoints/rest_merchant_endpoints.py @@ -1,4 +1,5 @@ -from typing import List, Union, Literal, Optional +from typing import TypedDict, List, Union, Literal, Optional + from decimal import Decimal from .. types import * diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 12fdb29..e1c39af 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -1,4 +1,4 @@ -from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Literal, Any +from typing import List, Dict, Optional, Literal, Any from dataclasses import dataclass diff --git a/bfxapi/utils/JSONEncoder.py b/bfxapi/utils/JSONEncoder.py index 5124376..edaba00 100644 --- a/bfxapi/utils/JSONEncoder.py +++ b/bfxapi/utils/JSONEncoder.py @@ -2,8 +2,6 @@ import json from decimal import Decimal from datetime import datetime -from types import SimpleNamespace - from typing import Type, List, Dict, Union, Any JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] diff --git a/bfxapi/websocket/client/bfx_websocket_inputs.py b/bfxapi/websocket/client/bfx_websocket_inputs.py index 48e3137..141f817 100644 --- a/bfxapi/websocket/client/bfx_websocket_inputs.py +++ b/bfxapi/websocket/client/bfx_websocket_inputs.py @@ -2,8 +2,8 @@ from decimal import Decimal from datetime import datetime from typing import Union, Optional, List, Tuple -from ..types import JSON -from ..enums import OrderType, FundingOfferType +from .. enums import OrderType, FundingOfferType +from ... utils.JSONEncoder import JSON class BfxWebsocketInputs(object): def __init__(self, __handle_websocket_input): diff --git a/bfxapi/websocket/enums.py b/bfxapi/websocket/enums.py index 8f06f62..b9530db 100644 --- a/bfxapi/websocket/enums.py +++ b/bfxapi/websocket/enums.py @@ -1,4 +1,4 @@ -from ..enums import * +from .. enums import * class Channels(str, Enum): TICKER = "ticker" diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py index 0ffa870..063836a 100644 --- a/bfxapi/websocket/types.py +++ b/bfxapi/websocket/types.py @@ -1,10 +1,10 @@ -from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any +from typing import Optional from dataclasses import dataclass -from ..labeler import _Type -from ..notification import Notification -from ..utils.JSONEncoder import JSON +from .. labeler import _Type +from .. notification import Notification +from .. utils.JSONEncoder import JSON #region Type hinting for Websocket Public Channels From 99f58ddb0410e4425668bdae5f944c85f056e32b Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 14 Feb 2023 18:49:45 +0100 Subject: [PATCH 74/85] Add new packages to setup.py. Add new feature in .on and .once methods in bfxapi.websocket.client.bfx_websocket_client. Fix small typo in __init__.py. --- .../websocket/client/bfx_websocket_client.py | 64 +++++++++++-------- bfxapi/websocket/handlers/__init__.py | 1 + setup.py | 2 +- 3 files changed, 38 insertions(+), 29 deletions(-) diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 8dccf79..2439bc0 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -1,6 +1,6 @@ -import traceback, json, asyncio, hmac, hashlib, time, uuid, websockets +import traceback, json, asyncio, hmac, hashlib, time, websockets -from typing import Literal, TypeVar, Callable, cast +from typing import cast from pyee.asyncio import AsyncIOEventEmitter @@ -10,8 +10,6 @@ from .bfx_websocket_inputs import BfxWebsocketInputs from ..handlers import PublicChannelsHandler, AuthenticatedChannelsHandler from ..exceptions import WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported -from ..enums import Channels - from ...utils.JSONEncoder import JSONEncoder from ...utils.logger import Formatter, CustomLogger @@ -36,28 +34,28 @@ class BfxWebsocketClient(object): *AuthenticatedChannelsHandler.EVENTS ] - def __init__(self, host, buckets=5, log_level = "WARNING", API_KEY=None, API_SECRET=None, filter=None): + def __init__(self, host, API_KEY = None, API_SECRET = None, filter = None, buckets = 5, log_level = "WARNING"): self.host, self.websocket, self.event_emitter = host, None, AsyncIOEventEmitter() - self.event_emitter.add_listener("error", - lambda exception: self.logger.error(str(exception) + "\n" + - str().join(traceback.format_exception(type(exception), exception, exception.__traceback__))[:-1]) - ) - self.API_KEY, self.API_SECRET, self.filter, self.authentication = API_KEY, API_SECRET, filter, False self.handler = AuthenticatedChannelsHandler(event_emitter=self.event_emitter) - self.buckets = [ BfxWebsocketBucket(self.host, self.event_emitter, self.__bucket_open_signal) for _ in range(buckets) ] - - self.inputs = BfxWebsocketInputs(self.__handle_websocket_input) - self.logger = CustomLogger("BfxWebsocketClient", logLevel=log_level) + self.event_emitter.add_listener("error", + lambda exception: self.logger.error(f"{type(exception).__name__}: {str(exception)}" + "\n" + + str().join(traceback.format_exception(type(exception), exception, exception.__traceback__))[:-1]) + ) + if buckets > BfxWebsocketClient.MAXIMUM_BUCKETS_AMOUNT: self.logger.warning(f"It is not safe to use more than {BfxWebsocketClient.MAXIMUM_BUCKETS_AMOUNT} buckets from the same \ connection ({buckets} in use), the server could momentarily block the client with <429 Too Many Requests>.") + self.buckets = [ BfxWebsocketBucket(self.host, self.event_emitter, self.__bucket_open_signal) for _ in range(buckets) ] + + self.inputs = BfxWebsocketInputs(self.__handle_websocket_input) + def run(self): return asyncio.run(self.start()) @@ -136,24 +134,34 @@ class BfxWebsocketClient(object): if all(bucket.websocket != None and bucket.websocket.open == True for bucket in self.buckets): self.event_emitter.emit("open") - def on(self, event, callback = None): - if event not in BfxWebsocketClient.EVENTS: - raise EventNotSupported(f"Event <{event}> is not supported. To get a list of available events print BfxWebsocketClient.EVENTS") + def on(self, *events, callback = None): + for event in events: + if event not in BfxWebsocketClient.EVENTS: + raise EventNotSupported(f"Event <{event}> is not supported. To get a list of available events print BfxWebsocketClient.EVENTS") if callback != None: - return self.event_emitter.on(event, callback) + for event in events: + self.event_emitter.on(event, callback) - def handler(function): - self.event_emitter.on(event, function) - return handler + if callback == None: + def handler(function): + for event in events: + self.event_emitter.on(event, function) - def once(self, event, callback = None): - if event not in BfxWebsocketClient.EVENTS: - raise EventNotSupported(f"Event <{event}> is not supported. To get a list of available events print BfxWebsocketClient.EVENTS") + return handler + + def once(self, *events, callback = None): + for event in events: + if event not in BfxWebsocketClient.EVENTS: + raise EventNotSupported(f"Event <{event}> is not supported. To get a list of available events print BfxWebsocketClient.EVENTS") if callback != None: - return self.event_emitter.once(event, callback) + for event in events: + self.event_emitter.once(event, callback) - def handler(function): - self.event_emitter.once(event, function) - return handler \ No newline at end of file + if callback == None: + def handler(function): + for event in events: + self.event_emitter.once(event, function) + + return handler \ No newline at end of file diff --git a/bfxapi/websocket/handlers/__init__.py b/bfxapi/websocket/handlers/__init__.py index 4fe650a..02e9c81 100644 --- a/bfxapi/websocket/handlers/__init__.py +++ b/bfxapi/websocket/handlers/__init__.py @@ -1,3 +1,4 @@ from .public_channels_handler import PublicChannelsHandler from .authenticated_channels_handler import AuthenticatedChannelsHandler + NAME = "handlers" \ No newline at end of file diff --git a/setup.py b/setup.py index 54db508..ce3ae3e 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( version="3.0.0", packages=[ "bfxapi", "bfxapi.utils", - "bfxapi.websocket", + "bfxapi.websocket", "bfxapi.websocket.client", "bfxapi.websocket.handlers", "bfxapi.rest", "bfxapi.rest.endpoints", "bfxapi.rest.middleware", ], url="https://github.com/bitfinexcom/bitfinex-api-py", From fa9bdfc33351f8221ae41999da9d37db8b3d6a44 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 15 Feb 2023 21:48:34 +0100 Subject: [PATCH 75/85] Rewrite reconnection system with numerous fixes. --- .../websocket/client/bfx_websocket_bucket.py | 31 +++++--- .../websocket/client/bfx_websocket_client.py | 79 +++++++++++++------ .../websocket/client/bfx_websocket_inputs.py | 18 ++--- bfxapi/websocket/enums.py | 2 +- .../handlers/public_channels_handler.py | 2 +- bfxapi/websocket/subscriptions.py | 14 ++-- examples/websocket/order_book.py | 4 +- examples/websocket/raw_order_book.py | 4 +- examples/websocket/ticker.py | 4 +- 9 files changed, 98 insertions(+), 60 deletions(-) diff --git a/bfxapi/websocket/client/bfx_websocket_bucket.py b/bfxapi/websocket/client/bfx_websocket_bucket.py index 550581d..15caffc 100644 --- a/bfxapi/websocket/client/bfx_websocket_bucket.py +++ b/bfxapi/websocket/client/bfx_websocket_bucket.py @@ -24,27 +24,35 @@ class BfxWebsocketBucket(object): MAXIMUM_SUBSCRIPTIONS_AMOUNT = 25 - def __init__(self, host, event_emitter, __bucket_open_signal): - self.host, self.event_emitter, self.__bucket_open_signal = host, event_emitter, __bucket_open_signal + def __init__(self, host, event_emitter, on_open_event): + self.host, self.event_emitter, self.on_open_event = host, event_emitter, on_open_event self.websocket, self.subscriptions, self.pendings = None, dict(), list() self.handler = PublicChannelsHandler(event_emitter=self.event_emitter) async def _connect(self, index): + reconnection = False + async for websocket in websockets.connect(self.host): self.websocket = websocket - self.__bucket_open_signal(index) + if reconnection == True or (reconnection := False): + for pending in self.pendings: + await self.websocket.send(json.dumps(pending)) + + for _, subscription in self.subscriptions.items(): + await self._subscribe(**subscription) + + self.subscriptions.clear() + + self.on_open_event.set() try: async for message in websocket: message = json.loads(message) - if isinstance(message, dict) and message["event"] == "info" and "version" in message: - if BfxWebsocketBucket.VERSION != message["version"]: - raise OutdatedClientVersion(f"Mismatch between the client version and the server version. Update the library to the latest version to continue (client version: {BfxWebsocketBucket.VERSION}, server version: {message['version']}).") - elif isinstance(message, dict) and message["event"] == "subscribed" and (chanId := message["chanId"]): + if isinstance(message, dict) and message["event"] == "subscribed" and (chanId := message["chanId"]): self.pendings = [ pending for pending in self.pendings if pending["subId"] != message["subId"] ] self.subscriptions[chanId] = message self.event_emitter.emit("subscribed", message) @@ -55,8 +63,9 @@ class BfxWebsocketBucket(object): self.event_emitter.emit("wss-error", message["code"], message["msg"]) elif isinstance(message, list) and (chanId := message[0]) and message[1] != _HEARTBEAT: self.handler.handle(self.subscriptions[chanId], *message[1:]) - except websockets.ConnectionClosedError: continue - finally: await self.websocket.wait_closed(); break + except websockets.ConnectionClosedError: self.on_open_event.clear(); reconnection = True; continue + + await self.websocket.wait_closed(); break @_require_websocket_connection async def _subscribe(self, channel, subId=None, **kwargs): @@ -64,11 +73,11 @@ class BfxWebsocketBucket(object): raise TooManySubscriptions("The client has reached the maximum number of subscriptions.") subscription = { + **kwargs, + "event": "subscribe", "channel": channel, "subId": subId or str(uuid.uuid4()), - - **kwargs } self.pendings.append(subscription) diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 2439bc0..8f2a54d 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -2,13 +2,17 @@ import traceback, json, asyncio, hmac, hashlib, time, websockets from typing import cast +from collections import namedtuple + +from datetime import datetime + from pyee.asyncio import AsyncIOEventEmitter from .bfx_websocket_bucket import _HEARTBEAT, F, _require_websocket_connection, BfxWebsocketBucket from .bfx_websocket_inputs import BfxWebsocketInputs from ..handlers import PublicChannelsHandler, AuthenticatedChannelsHandler -from ..exceptions import WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported +from ..exceptions import WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion from ...utils.JSONEncoder import JSONEncoder @@ -16,7 +20,7 @@ from ...utils.logger import Formatter, CustomLogger def _require_websocket_authentication(function: F) -> F: async def wrapper(self, *args, **kwargs): - if self.authentication == False: + if hasattr(self, "authentication") and self.authentication == False: raise WebsocketAuthenticationRequired("To perform this action you need to authenticate using your API_KEY and API_SECRET.") await _require_websocket_connection(function)(self, *args, **kwargs) @@ -26,7 +30,7 @@ def _require_websocket_authentication(function: F) -> F: class BfxWebsocketClient(object): VERSION = BfxWebsocketBucket.VERSION - MAXIMUM_BUCKETS_AMOUNT = 20 + MAXIMUM_CONNECTIONS_AMOUNT = 20 EVENTS = [ "open", "subscribed", "authenticated", "wss-error", @@ -34,10 +38,12 @@ class BfxWebsocketClient(object): *AuthenticatedChannelsHandler.EVENTS ] - def __init__(self, host, API_KEY = None, API_SECRET = None, filter = None, buckets = 5, log_level = "WARNING"): + def __init__(self, host, API_KEY = None, API_SECRET = None, filter = None, log_level = "WARNING"): self.host, self.websocket, self.event_emitter = host, None, AsyncIOEventEmitter() - self.API_KEY, self.API_SECRET, self.filter, self.authentication = API_KEY, API_SECRET, filter, False + self.API_KEY, self.API_SECRET, self.filter = API_KEY, API_SECRET, filter + + self.inputs = BfxWebsocketInputs(handle_websocket_input=self.__handle_websocket_input) self.handler = AuthenticatedChannelsHandler(event_emitter=self.event_emitter) @@ -48,36 +54,58 @@ class BfxWebsocketClient(object): str().join(traceback.format_exception(type(exception), exception, exception.__traceback__))[:-1]) ) - if buckets > BfxWebsocketClient.MAXIMUM_BUCKETS_AMOUNT: - self.logger.warning(f"It is not safe to use more than {BfxWebsocketClient.MAXIMUM_BUCKETS_AMOUNT} buckets from the same \ - connection ({buckets} in use), the server could momentarily block the client with <429 Too Many Requests>.") + def run(self, connections = 5): + return asyncio.run(self.start(connections)) - self.buckets = [ BfxWebsocketBucket(self.host, self.event_emitter, self.__bucket_open_signal) for _ in range(buckets) ] + async def start(self, connections = 5): + if connections > BfxWebsocketClient.MAXIMUM_CONNECTIONS_AMOUNT: + self.logger.warning(f"It is not safe to use more than {BfxWebsocketClient.MAXIMUM_CONNECTIONS_AMOUNT} buckets from the same " + + f"connection ({connections} in use), the server could momentarily block the client with <429 Too Many Requests>.") - self.inputs = BfxWebsocketInputs(self.__handle_websocket_input) + self.on_open_events = [ asyncio.Event() for _ in range(connections) ] - def run(self): - return asyncio.run(self.start()) + self.buckets = [ + BfxWebsocketBucket(self.host, self.event_emitter, self.on_open_events[index]) + for index in range(connections) + ] - async def start(self): tasks = [ bucket._connect(index) for index, bucket in enumerate(self.buckets) ] - if self.API_KEY != None and self.API_SECRET != None: - tasks.append(self.__connect(self.API_KEY, self.API_SECRET, self.filter)) + tasks.append(self.__connect(self.API_KEY, self.API_SECRET, self.filter)) await asyncio.gather(*tasks) async def __connect(self, API_KEY, API_SECRET, filter=None): + Reconnection = namedtuple("Reconnection", ["status", "code", "timestamp"]) + + reconnection = Reconnection(status=False, code=0, timestamp=None) + async for websocket in websockets.connect(self.host): - self.websocket = websocket - - await self.__authenticate(API_KEY, API_SECRET, filter) + self.websocket, self.authentication = websocket, False + + if (await asyncio.gather(*[ on_open_event.wait() for on_open_event in self.on_open_events ])): + self.event_emitter.emit("open") + + if self.API_KEY != None and self.API_SECRET != None: + await self.__authenticate(API_KEY=API_KEY, API_SECRET=API_SECRET, filter=filter) try: async for message in websocket: + if reconnection.status == True: + self.logger.warning(f"Reconnect Attempt Successful (error <{reconnection.code}>): The " + + f"client has been offline for a total of {datetime.now() - reconnection.timestamp} " + + f"(first reconnection attempt: {reconnection.timestamp:%d-%m-%Y at %H:%M:%S}).") + + reconnection = Reconnection(status=False, code=0, timestamp=None) + message = json.loads(message) - if isinstance(message, dict) and message["event"] == "auth": + if isinstance(message, dict) and message["event"] == "info" and "version" in message: + if BfxWebsocketClient.VERSION != message["version"]: + raise OutdatedClientVersion(f"Mismatch between the client version and the server version. " + + f"Update the library to the latest version to continue (client version: {BfxWebsocketClient.VERSION}, " + + f"server version: {message['version']}).") + elif isinstance(message, dict) and message["event"] == "auth": if message["status"] == "OK": self.event_emitter.emit("authenticated", message); self.authentication = True else: raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.") @@ -85,8 +113,13 @@ class BfxWebsocketClient(object): self.event_emitter.emit("wss-error", message["code"], message["msg"]) elif isinstance(message, list) and (chanId := message[0]) == 0 and message[1] != _HEARTBEAT: self.handler.handle(message[1], message[2]) - except websockets.ConnectionClosedError: continue - finally: await self.websocket.wait_closed(); break + except websockets.ConnectionClosedError as error: + self.logger.error(f"Connection terminated due to an error (status code: <{error.code}>) -> {str(error)}. Attempting to reconnect...") + reconnection = Reconnection(status=True, code=error.code, timestamp=datetime.now()); + continue + + if reconnection.status == False: + await self.websocket.wait_closed(); break async def __authenticate(self, API_KEY, API_SECRET, filter=None): data = { "event": "auth", "filter": filter, "apiKey": API_KEY } @@ -130,10 +163,6 @@ class BfxWebsocketClient(object): async def __handle_websocket_input(self, input, data): await self.websocket.send(json.dumps([ 0, input, None, data], cls=JSONEncoder)) - def __bucket_open_signal(self, index): - if all(bucket.websocket != None and bucket.websocket.open == True for bucket in self.buckets): - self.event_emitter.emit("open") - def on(self, *events, callback = None): for event in events: if event not in BfxWebsocketClient.EVENTS: diff --git a/bfxapi/websocket/client/bfx_websocket_inputs.py b/bfxapi/websocket/client/bfx_websocket_inputs.py index 141f817..4b4e04c 100644 --- a/bfxapi/websocket/client/bfx_websocket_inputs.py +++ b/bfxapi/websocket/client/bfx_websocket_inputs.py @@ -6,15 +6,15 @@ from .. enums import OrderType, FundingOfferType from ... utils.JSONEncoder import JSON class BfxWebsocketInputs(object): - def __init__(self, __handle_websocket_input): - self.__handle_websocket_input = __handle_websocket_input + def __init__(self, handle_websocket_input): + self.handle_websocket_input = handle_websocket_input async def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, float, str], price: Optional[Union[Decimal, float, str]] = None, lev: Optional[int] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_oco_stop: Optional[Union[Decimal, float, str]] = None, gid: Optional[int] = None, cid: Optional[int] = None, flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None): - await self.__handle_websocket_input("on", { + await self.handle_websocket_input("on", { "type": type, "symbol": symbol, "amount": amount, "price": price, "lev": lev, "price_trailing": price_trailing, "price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop, @@ -26,7 +26,7 @@ class BfxWebsocketInputs(object): cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, tif: Optional[Union[datetime, str]] = None): - await self.__handle_websocket_input("ou", { + await self.handle_websocket_input("ou", { "id": id, "amount": amount, "price": price, "cid": cid, "cid_date": cid_date, "gid": gid, "flags": flags, "lev": lev, "delta": delta, @@ -34,12 +34,12 @@ class BfxWebsocketInputs(object): }) async def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None): - await self.__handle_websocket_input("oc", { + await self.handle_websocket_input("oc", { "id": id, "cid": cid, "cid_date": cid_date }) async def cancel_order_multi(self, ids: Optional[List[int]] = None, cids: Optional[List[Tuple[int, str]]] = None, gids: Optional[List[int]] = None, all: bool = False): - await self.__handle_websocket_input("oc_multi", { + await self.handle_websocket_input("oc_multi", { "ids": ids, "cids": cids, "gids": gids, "all": int(all) }) @@ -47,14 +47,14 @@ class BfxWebsocketInputs(object): async def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, float, str], rate: Union[Decimal, float, str], period: int, flags: Optional[int] = 0): - await self.__handle_websocket_input("fon", { + await self.handle_websocket_input("fon", { "type": type, "symbol": symbol, "amount": amount, "rate": rate, "period": period, "flags": flags }) async def cancel_funding_offer(self, id: int): - await self.__handle_websocket_input("foc", { "id": id }) + await self.handle_websocket_input("foc", { "id": id }) async def calc(self, *args: str): - await self.__handle_websocket_input("calc", list(map(lambda arg: [arg], args))) \ No newline at end of file + await self.handle_websocket_input("calc", list(map(lambda arg: [arg], args))) \ No newline at end of file diff --git a/bfxapi/websocket/enums.py b/bfxapi/websocket/enums.py index b9530db..1877cea 100644 --- a/bfxapi/websocket/enums.py +++ b/bfxapi/websocket/enums.py @@ -1,6 +1,6 @@ from .. enums import * -class Channels(str, Enum): +class Channel(str, Enum): TICKER = "ticker" TRADES = "trades" BOOK = "book" diff --git a/bfxapi/websocket/handlers/public_channels_handler.py b/bfxapi/websocket/handlers/public_channels_handler.py index d63ea1f..52e47ef 100644 --- a/bfxapi/websocket/handlers/public_channels_handler.py +++ b/bfxapi/websocket/handlers/public_channels_handler.py @@ -28,7 +28,7 @@ class PublicChannelsHandler(object): _clear = lambda dictionary, *args: { key: value for key, value in dictionary.items() if key not in args } if (channel := subscription["channel"]) and channel in self.__handlers.keys(): - return self.__handlers[channel](_clear(subscription, "event", "channel", "subId"), *stream) + return self.__handlers[channel](_clear(subscription, "event", "channel", "chanId"), *stream) if self.strict: raise HandlerNotFound(f"No handler found for channel <{subscription['channel']}>.") diff --git a/bfxapi/websocket/subscriptions.py b/bfxapi/websocket/subscriptions.py index 5e2d692..10cbbfe 100644 --- a/bfxapi/websocket/subscriptions.py +++ b/bfxapi/websocket/subscriptions.py @@ -1,4 +1,4 @@ -from typing import TypedDict, Union, Optional +from typing import TypedDict, Union, Literal, Optional __all__ = [ "Subscription", @@ -10,22 +10,22 @@ __all__ = [ "Status" ] -_Header = TypedDict("_Header", { "event": str, "channel": str, "subId": str }) +_Header = TypedDict("_Header", { "event": Literal["subscribed"], "channel": str, "chanId": int }) Subscription = Union["Ticker", "Trades", "Book", "Candles", "Status"] class Ticker(TypedDict): - chanId: int; symbol: str + subId: str; symbol: str pair: Optional[str] currency: Optional[str] class Trades(TypedDict): - chanId: int; symbol: str + subId: str; symbol: str pair: Optional[str] currency: Optional[str] class Book(TypedDict): - chanId: int + subId: str symbol: str prec: str freq: str @@ -33,9 +33,9 @@ class Book(TypedDict): pair: str class Candles(TypedDict): - chanId: int + subId: str key: str class Status(TypedDict): - chanId: int + subId: str key: str \ No newline at end of file diff --git a/examples/websocket/order_book.py b/examples/websocket/order_book.py index 82bd105..a419454 100644 --- a/examples/websocket/order_book.py +++ b/examples/websocket/order_book.py @@ -7,7 +7,7 @@ from typing import List from bfxapi import Client, Constants from bfxapi.websocket import subscriptions -from bfxapi.websocket.enums import Channels, Error +from bfxapi.websocket.enums import Channel, Error from bfxapi.websocket.types import TradingPairBook class OrderBook(object): @@ -47,7 +47,7 @@ def on_wss_error(code: Error, msg: str): @bfx.wss.on("open") async def on_open(): for symbol in SYMBOLS: - await bfx.wss.subscribe(Channels.BOOK, symbol=symbol) + await bfx.wss.subscribe(Channel.BOOK, symbol=symbol) @bfx.wss.on("subscribed") def on_subscribed(subscription): diff --git a/examples/websocket/raw_order_book.py b/examples/websocket/raw_order_book.py index 896e351..a039060 100644 --- a/examples/websocket/raw_order_book.py +++ b/examples/websocket/raw_order_book.py @@ -7,7 +7,7 @@ from typing import List from bfxapi import Client, Constants from bfxapi.websocket import subscriptions -from bfxapi.websocket.enums import Channels, Error +from bfxapi.websocket.enums import Channel, Error from bfxapi.websocket.types import TradingPairRawBook class RawOrderBook(object): @@ -47,7 +47,7 @@ def on_wss_error(code: Error, msg: str): @bfx.wss.on("open") async def on_open(): for symbol in SYMBOLS: - await bfx.wss.subscribe(Channels.BOOK, symbol=symbol, prec="R0") + await bfx.wss.subscribe(Channel.BOOK, symbol=symbol, prec="R0") @bfx.wss.on("subscribed") def on_subscribed(subscription): diff --git a/examples/websocket/ticker.py b/examples/websocket/ticker.py index 1c081b2..729f3ea 100644 --- a/examples/websocket/ticker.py +++ b/examples/websocket/ticker.py @@ -3,7 +3,7 @@ from bfxapi import Client, Constants from bfxapi.websocket import subscriptions -from bfxapi.websocket.enums import Channels +from bfxapi.websocket.enums import Channel from bfxapi.websocket.types import TradingPairTicker bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) @@ -16,6 +16,6 @@ def on_t_ticker_update(subscription: subscriptions.Ticker, data: TradingPairTick @bfx.wss.once("open") async def open(): - await bfx.wss.subscribe(Channels.TICKER, symbol="tBTCUSD") + await bfx.wss.subscribe(Channel.TICKER, symbol="tBTCUSD") bfx.wss.run() \ No newline at end of file From b8a5bcb5157874f4250c89bb73b105fbbb3be4a4 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 16 Feb 2023 20:08:05 +0100 Subject: [PATCH 76/85] Fix bugs and rewrite regions of new reconnection system. --- bfxapi/client.py | 12 ++- .../websocket/client/bfx_websocket_bucket.py | 7 +- .../websocket/client/bfx_websocket_client.py | 97 +++++++++++++------ 3 files changed, 83 insertions(+), 33 deletions(-) diff --git a/bfxapi/client.py b/bfxapi/client.py index ec72fb3..84b9d76 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -1,7 +1,7 @@ from .rest import BfxRestInterface from .websocket import BfxWebsocketClient -from typing import Optional +from typing import List, Optional from enum import Enum @@ -21,8 +21,15 @@ class Client(object): WSS_HOST: str = Constants.WSS_HOST, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None, + filter: Optional[List[str]] = None, log_level: str = "WARNING" ): + credentials = { + "API_KEY": API_KEY, + "API_SECRET": API_SECRET, + "filter": filter + } + self.rest = BfxRestInterface( host=REST_HOST, API_KEY=API_KEY, @@ -31,7 +38,6 @@ class Client(object): self.wss = BfxWebsocketClient( host=WSS_HOST, - API_KEY=API_KEY, - API_SECRET=API_SECRET, + credentials=credentials, log_level=log_level ) \ No newline at end of file diff --git a/bfxapi/websocket/client/bfx_websocket_bucket.py b/bfxapi/websocket/client/bfx_websocket_bucket.py index 15caffc..b4c573b 100644 --- a/bfxapi/websocket/client/bfx_websocket_bucket.py +++ b/bfxapi/websocket/client/bfx_websocket_bucket.py @@ -63,9 +63,12 @@ class BfxWebsocketBucket(object): self.event_emitter.emit("wss-error", message["code"], message["msg"]) elif isinstance(message, list) and (chanId := message[0]) and message[1] != _HEARTBEAT: self.handler.handle(self.subscriptions[chanId], *message[1:]) - except websockets.ConnectionClosedError: self.on_open_event.clear(); reconnection = True; continue + except websockets.ConnectionClosedError: + self.on_open_event.clear() + reconnection = True + continue - await self.websocket.wait_closed(); break + break @_require_websocket_connection async def _subscribe(self, channel, subId=None, **kwargs): diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 8f2a54d..15faf89 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -1,4 +1,4 @@ -import traceback, json, asyncio, hmac, hashlib, time, websockets +import traceback, json, asyncio, hmac, hashlib, time, websockets, socket, random from typing import cast @@ -38,10 +38,10 @@ class BfxWebsocketClient(object): *AuthenticatedChannelsHandler.EVENTS ] - def __init__(self, host, API_KEY = None, API_SECRET = None, filter = None, log_level = "WARNING"): - self.host, self.websocket, self.event_emitter = host, None, AsyncIOEventEmitter() + def __init__(self, host, credentials = None, log_level = "WARNING"): + self.websocket = None - self.API_KEY, self.API_SECRET, self.filter = API_KEY, API_SECRET, filter + self.host, self.credentials, self.event_emitter = host, credentials, AsyncIOEventEmitter() self.inputs = BfxWebsocketInputs(handle_websocket_input=self.__handle_websocket_input) @@ -71,33 +71,35 @@ class BfxWebsocketClient(object): tasks = [ bucket._connect(index) for index, bucket in enumerate(self.buckets) ] - tasks.append(self.__connect(self.API_KEY, self.API_SECRET, self.filter)) + tasks.append(self.__connect(self.credentials)) await asyncio.gather(*tasks) - async def __connect(self, API_KEY, API_SECRET, filter=None): - Reconnection = namedtuple("Reconnection", ["status", "code", "timestamp"]) + async def __connect(self, credentials = None): + Reconnection = namedtuple("Reconnection", ["status", "attempts", "timestamp"]) - reconnection = Reconnection(status=False, code=0, timestamp=None) + reconnection = Reconnection(status=False, attempts=0, timestamp=None) - async for websocket in websockets.connect(self.host): - self.websocket, self.authentication = websocket, False + async def _connection(): + nonlocal reconnection - if (await asyncio.gather(*[ on_open_event.wait() for on_open_event in self.on_open_events ])): - self.event_emitter.emit("open") + async with websockets.connect(self.host) as websocket: + if reconnection.status == True: + self.logger.info(f"Reconnect attempt successful (attempt N°{reconnection.attempts}): The " + + f"client has been offline for a total of {datetime.now() - reconnection.timestamp} " + + f"(first reconnection attempt: {reconnection.timestamp:%d-%m-%Y at %H:%M:%S}).") - if self.API_KEY != None and self.API_SECRET != None: - await self.__authenticate(API_KEY=API_KEY, API_SECRET=API_SECRET, filter=filter) + reconnection = Reconnection(status=False, attempts=0, timestamp=None) + + self.websocket, self.authentication = websocket, False + + if await asyncio.gather(*[on_open_event.wait() for on_open_event in self.on_open_events]): + self.event_emitter.emit("open") + + if self.credentials != None: + await self.__authenticate(**self.credentials) - try: async for message in websocket: - if reconnection.status == True: - self.logger.warning(f"Reconnect Attempt Successful (error <{reconnection.code}>): The " + - f"client has been offline for a total of {datetime.now() - reconnection.timestamp} " + - f"(first reconnection attempt: {reconnection.timestamp:%d-%m-%Y at %H:%M:%S}).") - - reconnection = Reconnection(status=False, code=0, timestamp=None) - message = json.loads(message) if isinstance(message, dict) and message["event"] == "info" and "version" in message: @@ -113,13 +115,52 @@ class BfxWebsocketClient(object): self.event_emitter.emit("wss-error", message["code"], message["msg"]) elif isinstance(message, list) and (chanId := message[0]) == 0 and message[1] != _HEARTBEAT: self.handler.handle(message[1], message[2]) - except websockets.ConnectionClosedError as error: - self.logger.error(f"Connection terminated due to an error (status code: <{error.code}>) -> {str(error)}. Attempting to reconnect...") - reconnection = Reconnection(status=True, code=error.code, timestamp=datetime.now()); - continue - + + class _Delay: + BACKOFF_MIN, BACKOFF_MAX = 1.92, 60.0 + + BACKOFF_INITIAL = 5.0 + + def __init__(self, backoff_factor): + self.__backoff_factor = backoff_factor + self.__backoff_delay = _Delay.BACKOFF_MIN + self.__initial_delay = random.random() * _Delay.BACKOFF_INITIAL + + def next(self): + backoff_delay = self.peek() + + __backoff_delay = self.__backoff_delay * self.__backoff_factor + self.__backoff_delay = min(__backoff_delay, _Delay.BACKOFF_MAX) + return backoff_delay + + def peek(self): + return (self.__backoff_delay == _Delay.BACKOFF_MIN) \ + and self.__initial_delay or self.__backoff_delay + + delay = _Delay(backoff_factor=1.618) + + while True: + if reconnection.status == True: + await asyncio.sleep(delay.next()) + + try: + await _connection() + except (websockets.ConnectionClosedError, socket.gaierror) as error: + if isinstance(error, websockets.ConnectionClosedError) and error.code == 1006: + self.logger.error("Connection lost: no close frame received " + + "or sent (1006). Attempting to reconnect...") + + reconnection = Reconnection(status=True, attempts=1, timestamp=datetime.now()); + elif isinstance(error, socket.gaierror) and reconnection.status == True: + self.logger.warning(f"Reconnection attempt no.{reconnection.attempts} has failed. " + + f"Next reconnection attempt in ~{round(delay.peek()):.1f} seconds." + + f"(at the moment the client has been offline for {datetime.now() - reconnection.timestamp})") + + reconnection = reconnection._replace(attempts=reconnection.attempts + 1) + else: raise error + if reconnection.status == False: - await self.websocket.wait_closed(); break + break async def __authenticate(self, API_KEY, API_SECRET, filter=None): data = { "event": "auth", "filter": filter, "apiKey": API_KEY } From e536515bbdd1ee788fc885088dd6ac82b7b628bc Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 17 Feb 2023 02:58:53 +0100 Subject: [PATCH 77/85] Fix bugs and rewrite code in bfxapi.websocket.client sub-package. --- .../websocket/client/bfx_websocket_bucket.py | 24 ++++++++++++------- .../websocket/client/bfx_websocket_client.py | 10 ++++---- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/bfxapi/websocket/client/bfx_websocket_bucket.py b/bfxapi/websocket/client/bfx_websocket_bucket.py index b4c573b..90c8d21 100644 --- a/bfxapi/websocket/client/bfx_websocket_bucket.py +++ b/bfxapi/websocket/client/bfx_websocket_bucket.py @@ -37,6 +37,8 @@ class BfxWebsocketBucket(object): async for websocket in websockets.connect(self.host): self.websocket = websocket + self.on_open_event.set() + if reconnection == True or (reconnection := False): for pending in self.pendings: await self.websocket.send(json.dumps(pending)) @@ -46,8 +48,6 @@ class BfxWebsocketBucket(object): self.subscriptions.clear() - self.on_open_event.set() - try: async for message in websocket: message = json.loads(message) @@ -63,11 +63,14 @@ class BfxWebsocketBucket(object): self.event_emitter.emit("wss-error", message["code"], message["msg"]) elif isinstance(message, list) and (chanId := message[0]) and message[1] != _HEARTBEAT: self.handler.handle(self.subscriptions[chanId], *message[1:]) - except websockets.ConnectionClosedError: - self.on_open_event.clear() - reconnection = True - continue - + except websockets.ConnectionClosedError as error: + if error.code == 1006: + self.on_open_event.clear() + reconnection = True + continue + + raise error + break @_require_websocket_connection @@ -96,4 +99,9 @@ class BfxWebsocketBucket(object): @_require_websocket_connection async def _close(self, code=1000, reason=str()): - await self.websocket.close(code=code, reason=reason) \ No newline at end of file + await self.websocket.close(code=code, reason=reason) + + def _get_chan_id(self, subId): + for subscription in self.subscriptions.values(): + if subscription["subId"] == subId: + return subscription["chanId"] \ No newline at end of file diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 15faf89..64deb0b 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -78,7 +78,7 @@ class BfxWebsocketClient(object): async def __connect(self, credentials = None): Reconnection = namedtuple("Reconnection", ["status", "attempts", "timestamp"]) - reconnection = Reconnection(status=False, attempts=0, timestamp=None) + reconnection, delay = Reconnection(status=False, attempts=0, timestamp=None), None async def _connection(): nonlocal reconnection @@ -137,8 +137,6 @@ class BfxWebsocketClient(object): return (self.__backoff_delay == _Delay.BACKOFF_MIN) \ and self.__initial_delay or self.__backoff_delay - delay = _Delay(backoff_factor=1.618) - while True: if reconnection.status == True: await asyncio.sleep(delay.next()) @@ -151,6 +149,8 @@ class BfxWebsocketClient(object): + "or sent (1006). Attempting to reconnect...") reconnection = Reconnection(status=True, attempts=1, timestamp=datetime.now()); + + delay = _Delay(backoff_factor=1.618) elif isinstance(error, socket.gaierror) and reconnection.status == True: self.logger.warning(f"Reconnection attempt no.{reconnection.attempts} has failed. " + f"Next reconnection attempt in ~{round(delay.peek()):.1f} seconds." @@ -184,9 +184,9 @@ class BfxWebsocketClient(object): await self.buckets[index]._subscribe(channel, **kwargs) - async def unsubscribe(self, chanId): + async def unsubscribe(self, subId): for bucket in self.buckets: - if chanId in bucket.subscriptions.keys(): + if (chanId := bucket._get_chan_id(subId)): await bucket._unsubscribe(chanId=chanId) async def close(self, code=1000, reason=str()): From 4d0fa49e2271a650c0dc8e195a18e0bcea8d2935 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 17 Feb 2023 04:01:47 +0100 Subject: [PATCH 78/85] Rewrite bfxapi/utils/logger.py with new ColoredLogger. --- bfxapi/client.py | 2 +- bfxapi/utils/logger.py | 111 +++++------------- .../websocket/client/bfx_websocket_client.py | 10 +- 3 files changed, 38 insertions(+), 85 deletions(-) diff --git a/bfxapi/client.py b/bfxapi/client.py index 84b9d76..aa7eaf2 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -22,7 +22,7 @@ class Client(object): API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None, filter: Optional[List[str]] = None, - log_level: str = "WARNING" + log_level: str = "INFO" ): credentials = { "API_KEY": API_KEY, diff --git a/bfxapi/utils/logger.py b/bfxapi/utils/logger.py index 0ea3894..cf3e970 100644 --- a/bfxapi/utils/logger.py +++ b/bfxapi/utils/logger.py @@ -1,99 +1,52 @@ -""" -Module used to describe all of the different data types -""" - import logging +BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) + RESET_SEQ = "\033[0m" + COLOR_SEQ = "\033[1;%dm" +ITALIC_COLOR_SEQ = "\033[3;%dm" +UNDERLINE_COLOR_SEQ = "\033[4;%dm" + BOLD_SEQ = "\033[1m" -UNDERLINE_SEQ = "\033[04m" - -YELLOW = '\033[93m' -WHITE = '\33[37m' -BLUE = '\033[34m' -LIGHT_BLUE = '\033[94m' -RED = '\033[91m' -GREY = '\33[90m' - -KEYWORD_COLORS = { - 'WARNING': YELLOW, - 'INFO': LIGHT_BLUE, - 'DEBUG': WHITE, - 'CRITICAL': YELLOW, - 'ERROR': RED, - 'TRADE': '\33[102m\33[30m' -} def formatter_message(message, use_color = True): - """ - Syntax highlight certain keywords - """ if use_color: message = message.replace("$RESET", RESET_SEQ).replace("$BOLD", BOLD_SEQ) else: message = message.replace("$RESET", "").replace("$BOLD", "") return message -def format_word(message, word, color_seq, bold=False, underline=False): - """ - Surround the given word with a sequence - """ - replacer = color_seq + word + RESET_SEQ - if underline: - replacer = UNDERLINE_SEQ + replacer - if bold: - replacer = BOLD_SEQ + replacer - return message.replace(word, replacer) +COLORS = { + "DEBUG": CYAN, + "INFO": BLUE, + "WARNING": YELLOW, + "ERROR": RED +} -class Formatter(logging.Formatter): - """ - This Formatted simply colors in the levelname i.e 'INFO', 'DEBUG' - """ - def __init__(self, msg, use_color = True): - logging.Formatter.__init__(self, msg) - self.use_color = use_color +class _ColoredFormatter(logging.Formatter): + def __init__(self, msg, use_color = True): + logging.Formatter.__init__(self, msg, "%d-%m-%Y %H:%M:%S") + self.use_color = use_color - def format(self, record): - """ - Format and highlight certain keywords - """ - levelname = record.levelname - if self.use_color and levelname in KEYWORD_COLORS: - levelname_color = KEYWORD_COLORS[levelname] + levelname + RESET_SEQ - record.levelname = levelname_color - record.name = GREY + record.name + RESET_SEQ - return logging.Formatter.format(self, record) + def format(self, record): + levelname = record.levelname + if self.use_color and levelname in COLORS: + levelname_color = COLOR_SEQ % (30 + COLORS[levelname]) + levelname + RESET_SEQ + record.levelname = levelname_color + record.name = ITALIC_COLOR_SEQ % (30 + BLACK) + record.name + RESET_SEQ + return logging.Formatter.format(self, record) -class CustomLogger(logging.Logger): - """ - This adds extra logging functions such as logger.trade and also - sets the logger to use the custom formatter - """ - FORMAT = "[$BOLD%(name)s$RESET] [%(levelname)s] %(message)s" +class ColoredLogger(logging.Logger): + FORMAT = "[$BOLD%(name)s$RESET] [%(asctime)s] [%(levelname)s] %(message)s" + COLOR_FORMAT = formatter_message(FORMAT, True) - TRADE = 50 + + def __init__(self, name, level): + logging.Logger.__init__(self, name, level) - def __init__(self, name, logLevel='DEBUG'): - logging.Logger.__init__(self, name, logLevel) - color_formatter = Formatter(self.COLOR_FORMAT) + colored_formatter = _ColoredFormatter(self.COLOR_FORMAT) console = logging.StreamHandler() - console.setFormatter(color_formatter) - self.addHandler(console) - logging.addLevelName(self.TRADE, "TRADE") - return + console.setFormatter(colored_formatter) - def set_level(self, level): - logging.Logger.setLevel(self, level) - - def trade(self, message, *args, **kws): - """ - Print a syntax highlighted trade signal - """ - if self.isEnabledFor(self.TRADE): - message = format_word(message, 'CLOSED ', YELLOW, bold=True) - message = format_word(message, 'OPENED ', LIGHT_BLUE, bold=True) - message = format_word(message, 'UPDATED ', BLUE, bold=True) - message = format_word(message, 'CLOSED_ALL ', RED, bold=True) - # Yes, logger takes its '*args' as 'args'. - self._log(self.TRADE, message, args, **kws) \ No newline at end of file + self.addHandler(console) \ No newline at end of file diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 64deb0b..f3c354d 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -16,7 +16,7 @@ from ..exceptions import WebsocketAuthenticationRequired, InvalidAuthenticationC from ...utils.JSONEncoder import JSONEncoder -from ...utils.logger import Formatter, CustomLogger +from ...utils.logger import ColoredLogger def _require_websocket_authentication(function: F) -> F: async def wrapper(self, *args, **kwargs): @@ -38,7 +38,7 @@ class BfxWebsocketClient(object): *AuthenticatedChannelsHandler.EVENTS ] - def __init__(self, host, credentials = None, log_level = "WARNING"): + def __init__(self, host, credentials = None, log_level = "INFO"): self.websocket = None self.host, self.credentials, self.event_emitter = host, credentials, AsyncIOEventEmitter() @@ -47,7 +47,7 @@ class BfxWebsocketClient(object): self.handler = AuthenticatedChannelsHandler(event_emitter=self.event_emitter) - self.logger = CustomLogger("BfxWebsocketClient", logLevel=log_level) + self.logger = ColoredLogger("BfxWebsocketClient", level=log_level) self.event_emitter.add_listener("error", lambda exception: self.logger.error(f"{type(exception).__name__}: {str(exception)}" + "\n" + @@ -85,9 +85,9 @@ class BfxWebsocketClient(object): async with websockets.connect(self.host) as websocket: if reconnection.status == True: - self.logger.info(f"Reconnect attempt successful (attempt N°{reconnection.attempts}): The " + + self.logger.info(f"Reconnect attempt successful (attempt no.{reconnection.attempts}): The " + f"client has been offline for a total of {datetime.now() - reconnection.timestamp} " + - f"(first reconnection attempt: {reconnection.timestamp:%d-%m-%Y at %H:%M:%S}).") + f"(connection lost at: {reconnection.timestamp:%d-%m-%Y at %H:%M:%S}).") reconnection = Reconnection(status=False, attempts=0, timestamp=None) From 9eb2c73407a56d6229311d6ef132ebb47bbc50df Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 17 Feb 2023 04:04:50 +0100 Subject: [PATCH 79/85] Fix small bug in examples/websocket/ticker.py demo. --- bfxapi/websocket/client/bfx_websocket_client.py | 4 ++-- examples/websocket/ticker.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index f3c354d..4bb826a 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -96,8 +96,8 @@ class BfxWebsocketClient(object): if await asyncio.gather(*[on_open_event.wait() for on_open_event in self.on_open_events]): self.event_emitter.emit("open") - if self.credentials != None: - await self.__authenticate(**self.credentials) + if credentials and credentials["API_KEY"] and credentials["API_SECRET"]: + await self.__authenticate(**credentials) async for message in websocket: message = json.loads(message) diff --git a/examples/websocket/ticker.py b/examples/websocket/ticker.py index 729f3ea..a335a28 100644 --- a/examples/websocket/ticker.py +++ b/examples/websocket/ticker.py @@ -10,7 +10,7 @@ bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) @bfx.wss.on("t_ticker_update") def on_t_ticker_update(subscription: subscriptions.Ticker, data: TradingPairTicker): - print(f"Subscription with channel ID: {subscription['chanId']}") + print(f"Subscription with subId: {subscription['subId']}") print(f"Data: {data}") From fde27e933f1f21156a07c62f877284b0819e08f9 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 17 Feb 2023 04:15:38 +0100 Subject: [PATCH 80/85] Add handling for <20051 : Stop/Restart Websocket Server (please reconnect)>. --- bfxapi/websocket/client/bfx_websocket_client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 4bb826a..0218cd7 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -107,6 +107,8 @@ class BfxWebsocketClient(object): raise OutdatedClientVersion(f"Mismatch between the client version and the server version. " + f"Update the library to the latest version to continue (client version: {BfxWebsocketClient.VERSION}, " + f"server version: {message['version']}).") + elif isinstance(message, dict) and message["event"] == "info" and message["code"] == 20051: + raise websockets.ConnectionClosedError(rcvd=None, sent=None) elif isinstance(message, dict) and message["event"] == "auth": if message["status"] == "OK": self.event_emitter.emit("authenticated", message); self.authentication = True From 16d0ee525edaabd56953eca4a557f3fd73baa46f Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 17 Feb 2023 04:29:26 +0100 Subject: [PATCH 81/85] Remove test_rest_serializers_and_types.py and test_websocket_serializers_and_types.py. Add new test_rest_serializers.py and test_websocket_serializers.py unit tests. Edit bfxapi.tests.__init__.py's test suite. --- bfxapi/tests/__init__.py | 8 ++++---- bfxapi/tests/test_rest_serializers.py | 17 +++++++++++++++++ bfxapi/tests/test_rest_serializers_and_types.py | 17 ----------------- bfxapi/tests/test_websocket_serializers.py | 17 +++++++++++++++++ .../test_websocket_serializers_and_types.py | 17 ----------------- 5 files changed, 38 insertions(+), 38 deletions(-) create mode 100644 bfxapi/tests/test_rest_serializers.py delete mode 100644 bfxapi/tests/test_rest_serializers_and_types.py create mode 100644 bfxapi/tests/test_websocket_serializers.py delete mode 100644 bfxapi/tests/test_websocket_serializers_and_types.py diff --git a/bfxapi/tests/__init__.py b/bfxapi/tests/__init__.py index a63ea0d..057c2c0 100644 --- a/bfxapi/tests/__init__.py +++ b/bfxapi/tests/__init__.py @@ -1,6 +1,6 @@ import unittest -from .test_rest_serializers_and_types import TestRestSerializersAndTypes -from .test_websocket_serializers_and_types import TestWebsocketSerializersAndTypes +from .test_rest_serializers import TestRestSerializers +from .test_websocket_serializers import TestWebsocketSerializers from .test_labeler import TestLabeler from .test_notification import TestNotification @@ -8,8 +8,8 @@ NAME = "tests" def suite(): return unittest.TestSuite([ - unittest.makeSuite(TestRestSerializersAndTypes), - unittest.makeSuite(TestWebsocketSerializersAndTypes), + unittest.makeSuite(TestRestSerializers), + unittest.makeSuite(TestWebsocketSerializers), unittest.makeSuite(TestLabeler), unittest.makeSuite(TestNotification), ]) diff --git a/bfxapi/tests/test_rest_serializers.py b/bfxapi/tests/test_rest_serializers.py new file mode 100644 index 0000000..4c24992 --- /dev/null +++ b/bfxapi/tests/test_rest_serializers.py @@ -0,0 +1,17 @@ +import unittest + +from ..labeler import _Type + +from ..rest import serializers + +class TestRestSerializers(unittest.TestCase): + def test_rest_serializers(self): + for serializer in map(serializers.__dict__.get, serializers.__serializers__): + self.assertTrue(issubclass(serializer.klass, _Type), + f"_Serializer <{serializer.name}>: .klass field must be a subclass of _Type (got {serializer.klass}).") + + self.assertListEqual(serializer.get_labels(), list(serializer.klass.__annotations__), + f"_Serializer <{serializer.name}> and _Type <{serializer.klass.__name__}> must have matching labels and fields.") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/bfxapi/tests/test_rest_serializers_and_types.py b/bfxapi/tests/test_rest_serializers_and_types.py deleted file mode 100644 index 7bc7242..0000000 --- a/bfxapi/tests/test_rest_serializers_and_types.py +++ /dev/null @@ -1,17 +0,0 @@ -import unittest - -from ..rest import serializers, types - -class TestRestSerializersAndTypes(unittest.TestCase): - def test_consistency(self): - for serializer in map(serializers.__dict__.get, serializers.__serializers__): - type = types.__dict__.get(serializer.name) - - self.assertIsNotNone(type, f"_Serializer <{serializer.name}>: no respective _Type found in bfxapi.rest.types.") - self.assertEqual(serializer.klass, type, f"_Serializer <{serializer.name}>.klass: field does not match with respective _Type in bfxapi.rest.types.") - - self.assertListEqual(serializer.get_labels(), list(type.__annotations__), - f"_Serializer <{serializer.name}> and _Type <{type.__name__}> must have matching labels and fields.") - -if __name__ == "__main__": - unittest.main() \ No newline at end of file diff --git a/bfxapi/tests/test_websocket_serializers.py b/bfxapi/tests/test_websocket_serializers.py new file mode 100644 index 0000000..a559565 --- /dev/null +++ b/bfxapi/tests/test_websocket_serializers.py @@ -0,0 +1,17 @@ +import unittest + +from ..labeler import _Type + +from ..websocket import serializers + +class TestWebsocketSerializers(unittest.TestCase): + def test_websocket_serializers(self): + for serializer in map(serializers.__dict__.get, serializers.__serializers__): + self.assertTrue(issubclass(serializer.klass, _Type), + f"_Serializer <{serializer.name}>: .klass field must be a subclass of _Type (got {serializer.klass}).") + + self.assertListEqual(serializer.get_labels(), list(serializer.klass.__annotations__), + f"_Serializer <{serializer.name}> and _Type <{serializer.klass.__name__}> must have matching labels and fields.") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/bfxapi/tests/test_websocket_serializers_and_types.py b/bfxapi/tests/test_websocket_serializers_and_types.py deleted file mode 100644 index 338b959..0000000 --- a/bfxapi/tests/test_websocket_serializers_and_types.py +++ /dev/null @@ -1,17 +0,0 @@ -import unittest - -from ..websocket import serializers, types - -class TestWebsocketSerializersAndTypes(unittest.TestCase): - def test_consistency(self): - for serializer in map(serializers.__dict__.get, serializers.__serializers__): - type = types.__dict__.get(serializer.name) - - self.assertIsNotNone(type, f"_Serializer <{serializer.name}>: no respective _Type found in bfxapi.websocket.types.") - self.assertEqual(serializer.klass, type, f"_Serializer <{serializer.name}>.klass: field does not match with respective _Type in bfxapi.websocket.types.") - - self.assertListEqual(serializer.get_labels(), list(type.__annotations__), - f"_Serializer <{serializer.name}> and _Type <{type.__name__}> must have matching labels and fields.") - -if __name__ == "__main__": - unittest.main() \ No newline at end of file From d72fcf3981bb36570c31705c44bfae631610d469 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 17 Feb 2023 17:02:09 +0100 Subject: [PATCH 82/85] Add better handling for info code 20051. --- bfxapi/websocket/client/bfx_websocket_client.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 0218cd7..adea696 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -108,7 +108,9 @@ class BfxWebsocketClient(object): f"Update the library to the latest version to continue (client version: {BfxWebsocketClient.VERSION}, " + f"server version: {message['version']}).") elif isinstance(message, dict) and message["event"] == "info" and message["code"] == 20051: - raise websockets.ConnectionClosedError(rcvd=None, sent=None) + rcvd = websockets.frames.Close(code=1012, reason="Stop/Restart Websocket Server (please reconnect).") + + raise websockets.ConnectionClosedError(rcvd=rcvd, sent=None) elif isinstance(message, dict) and message["event"] == "auth": if message["status"] == "OK": self.event_emitter.emit("authenticated", message); self.authentication = True @@ -130,9 +132,9 @@ class BfxWebsocketClient(object): def next(self): backoff_delay = self.peek() - __backoff_delay = self.__backoff_delay * self.__backoff_factor self.__backoff_delay = min(__backoff_delay, _Delay.BACKOFF_MAX) + return backoff_delay def peek(self): @@ -146,9 +148,14 @@ class BfxWebsocketClient(object): try: await _connection() except (websockets.ConnectionClosedError, socket.gaierror) as error: - if isinstance(error, websockets.ConnectionClosedError) and error.code == 1006: - self.logger.error("Connection lost: no close frame received " - + "or sent (1006). Attempting to reconnect...") + if isinstance(error, websockets.ConnectionClosedError) and (error.code == 1006 or error.code == 1012): + if error.code == 1006: + self.logger.error("Connection lost: no close frame received " + + "or sent (1006). Attempting to reconnect...") + + if error.code == 1012: + self.logger.info("WSS server is about to restart, reconnection " + + "required (client received 20051). Attempt in progress...") reconnection = Reconnection(status=True, attempts=1, timestamp=datetime.now()); From ab66170cf3eb16ff4db3f06a2f4d71f2d5024771 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 17 Feb 2023 18:38:07 +0100 Subject: [PATCH 83/85] Apply refactoring to root package bfxapi. --- bfxapi/__init__.py | 7 ++++- bfxapi/client.py | 28 ++++++------------- bfxapi/rest/endpoints/bfx_rest_interface.py | 5 +++- .../rest/endpoints/rest_public_endpoints.py | 16 +++++------ bfxapi/urls.py | 7 +++++ .../websocket/client/bfx_websocket_client.py | 4 +-- examples/rest/claim_position.py | 4 +-- examples/rest/create_funding_offer.py | 4 +-- examples/rest/create_order.py | 4 +-- examples/rest/derivatives.py | 4 +-- examples/rest/extra_calcs.py | 4 +-- examples/rest/funding_auto_renew.py | 4 +-- examples/rest/get_authenticated_data.py | 4 +-- examples/rest/get_candles_hist.py | 4 +-- examples/rest/get_funding_info.py | 4 +-- examples/rest/get_funding_trades_history.py | 4 +-- examples/rest/get_liquidations.py | 4 +-- examples/rest/get_positions.py | 4 +-- examples/rest/get_public_data.py | 4 +-- examples/rest/get_pulse_data.py | 4 +-- examples/rest/increase_position.py | 4 +-- examples/rest/keep_taken_funding.py | 4 +-- examples/rest/merchant.py | 4 +-- examples/rest/return_taken_funding.py | 4 +-- examples/rest/transfer_wallet.py | 4 +-- examples/websocket/create_order.py | 4 +-- examples/websocket/order_book.py | 4 +-- examples/websocket/raw_order_book.py | 4 +-- examples/websocket/ticker.py | 4 +-- 29 files changed, 81 insertions(+), 78 deletions(-) create mode 100644 bfxapi/urls.py diff --git a/bfxapi/__init__.py b/bfxapi/__init__.py index c11c9ab..4fbdfd6 100644 --- a/bfxapi/__init__.py +++ b/bfxapi/__init__.py @@ -1 +1,6 @@ -from .client import Client, Constants \ No newline at end of file +from .client import Client + +from .urls import REST_HOST, PUB_REST_HOST, STAGING_REST_HOST, \ + WSS_HOST, PUB_WSS_HOST, STAGING_WSS_HOST + +NAME = "bfxapi" \ No newline at end of file diff --git a/bfxapi/client.py b/bfxapi/client.py index aa7eaf2..f3121ac 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -1,39 +1,27 @@ from .rest import BfxRestInterface from .websocket import BfxWebsocketClient +from .urls import REST_HOST, WSS_HOST from typing import List, 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" - STAGING_REST_HOST = "https://api.staging.bitfinex.com/v2" - - WSS_HOST = "wss://api.bitfinex.com/ws/2" - PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2" - STAGING_WSS_HOST = "wss://api.staging.bitfinex.com/ws/2" - class Client(object): def __init__( self, - REST_HOST: str = Constants.REST_HOST, - WSS_HOST: str = Constants.WSS_HOST, + REST_HOST: str = REST_HOST, + WSS_HOST: str = WSS_HOST, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None, filter: Optional[List[str]] = None, log_level: str = "INFO" ): - credentials = { - "API_KEY": API_KEY, - "API_SECRET": API_SECRET, - "filter": filter - } + credentials = None + + if API_KEY and API_SECRET: + credentials = { "API_KEY": API_KEY, "API_SECRET": API_SECRET, "filter": filter } self.rest = BfxRestInterface( host=REST_HOST, - API_KEY=API_KEY, - API_SECRET=API_SECRET + credentials=credentials ) self.wss = BfxWebsocketClient( diff --git a/bfxapi/rest/endpoints/bfx_rest_interface.py b/bfxapi/rest/endpoints/bfx_rest_interface.py index a2dc6ec..b117fa6 100644 --- a/bfxapi/rest/endpoints/bfx_rest_interface.py +++ b/bfxapi/rest/endpoints/bfx_rest_interface.py @@ -7,7 +7,10 @@ from .rest_merchant_endpoints import RestMerchantEndpoints class BfxRestInterface(object): VERSION = 2 - def __init__(self, host: str, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None): + def __init__(self, host, credentials = None): + API_KEY, API_SECRET = credentials and \ + (credentials["API_KEY"], credentials["API_SECRET"]) or (None, None) + self.public = RestPublicEndpoints(host=host) self.auth = RestAuthenticatedEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET) self.merchant = RestMerchantEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET) \ No newline at end of file diff --git a/bfxapi/rest/endpoints/rest_public_endpoints.py b/bfxapi/rest/endpoints/rest_public_endpoints.py index 3810e97..b5313fd 100644 --- a/bfxapi/rest/endpoints/rest_public_endpoints.py +++ b/bfxapi/rest/endpoints/rest_public_endpoints.py @@ -25,7 +25,7 @@ class RestPublicEndpoints(Middleware): if isinstance(pairs, str) and pairs == "ALL": return [ cast(TradingPairTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.symbol).startswith("t") ] - data = self.get_tickers([ "t" + pair for pair in pairs ]) + data = self.get_tickers([ pair for pair in pairs ]) return cast(List[TradingPairTicker], data) @@ -33,7 +33,7 @@ class RestPublicEndpoints(Middleware): if isinstance(currencies, str) and currencies == "ALL": return [ cast(FundingCurrencyTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.symbol).startswith("f") ] - data = self.get_tickers([ "f" + currency for currency in currencies ]) + data = self.get_tickers([ currency for currency in currencies ]) return cast(List[FundingCurrencyTicker], data) @@ -52,25 +52,25 @@ class RestPublicEndpoints(Middleware): 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) + data = self._GET(f"trades/{pair}/hist", params=params) return [ serializers.TradingPairTrade.parse(*sub_data) for sub_data in data ] def get_f_trades(self, currency: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[FundingCurrencyTrade]: params = { "limit": limit, "start": start, "end": end, "sort": sort } - data = self._GET(f"trades/{'f' + currency}/hist", params=params) + data = self._GET(f"trades/{currency}/hist", params=params) return [ serializers.FundingCurrencyTrade.parse(*sub_data) for sub_data in data ] def get_t_book(self, pair: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairBook]: - return [ serializers.TradingPairBook.parse(*sub_data) for sub_data in self._GET(f"book/{'t' + pair}/{precision}", params={ "len": len }) ] + return [ serializers.TradingPairBook.parse(*sub_data) for sub_data in self._GET(f"book/{pair}/{precision}", params={ "len": len }) ] def get_f_book(self, currency: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyBook]: - return [ serializers.FundingCurrencyBook.parse(*sub_data) for sub_data in self._GET(f"book/{'f' + currency}/{precision}", params={ "len": len }) ] + return [ serializers.FundingCurrencyBook.parse(*sub_data) for sub_data in self._GET(f"book/{currency}/{precision}", params={ "len": len }) ] def get_t_raw_book(self, pair: str, len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairRawBook]: - return [ serializers.TradingPairRawBook.parse(*sub_data) for sub_data in self._GET(f"book/{'t' + pair}/R0", params={ "len": len }) ] + return [ serializers.TradingPairRawBook.parse(*sub_data) for sub_data in self._GET(f"book/{pair}/R0", params={ "len": len }) ] def get_f_raw_book(self, currency: str, len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyRawBook]: - return [ serializers.FundingCurrencyRawBook.parse(*sub_data) for sub_data in self._GET(f"book/{'f' + currency}/R0", params={ "len": len }) ] + return [ serializers.FundingCurrencyRawBook.parse(*sub_data) for sub_data in self._GET(f"book/{currency}/R0", params={ "len": len }) ] def get_stats_hist( self, diff --git a/bfxapi/urls.py b/bfxapi/urls.py new file mode 100644 index 0000000..c9a622b --- /dev/null +++ b/bfxapi/urls.py @@ -0,0 +1,7 @@ +REST_HOST = "https://api.bitfinex.com/v2" +PUB_REST_HOST = "https://api-pub.bitfinex.com/v2" +STAGING_REST_HOST = "https://api.staging.bitfinex.com/v2" + +WSS_HOST = "wss://api.bitfinex.com/ws/2" +PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2" +STAGING_WSS_HOST = "wss://api.staging.bitfinex.com/ws/2" \ No newline at end of file diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index adea696..7dc06ed 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -96,8 +96,8 @@ class BfxWebsocketClient(object): if await asyncio.gather(*[on_open_event.wait() for on_open_event in self.on_open_events]): self.event_emitter.emit("open") - if credentials and credentials["API_KEY"] and credentials["API_SECRET"]: - await self.__authenticate(**credentials) + if self.credentials: + await self.__authenticate(**self.credentials) async for message in websocket: message = json.loads(message) diff --git a/examples/rest/claim_position.py b/examples/rest/claim_position.py index ba3e4e0..084c9d0 100644 --- a/examples/rest/claim_position.py +++ b/examples/rest/claim_position.py @@ -2,10 +2,10 @@ import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/create_funding_offer.py b/examples/rest/create_funding_offer.py index c1031d8..f462325 100644 --- a/examples/rest/create_funding_offer.py +++ b/examples/rest/create_funding_offer.py @@ -2,11 +2,11 @@ import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST from bfxapi.enums import FundingOfferType, Flag bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/create_order.py b/examples/rest/create_order.py index ea70265..607f7c9 100644 --- a/examples/rest/create_order.py +++ b/examples/rest/create_order.py @@ -1,11 +1,11 @@ # python -c "import examples.rest.create_order" import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST from bfxapi.enums import OrderType, Flag bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/derivatives.py b/examples/rest/derivatives.py index 4aedd00..58a0031 100644 --- a/examples/rest/derivatives.py +++ b/examples/rest/derivatives.py @@ -2,10 +2,10 @@ import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/extra_calcs.py b/examples/rest/extra_calcs.py index e380aef..8ef93cb 100644 --- a/examples/rest/extra_calcs.py +++ b/examples/rest/extra_calcs.py @@ -1,9 +1,9 @@ # python -c "import examples.rest.extra_calcs" -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST + REST_HOST=REST_HOST ) t_symbol_response = bfx.rest.public.get_trading_market_average_price( diff --git a/examples/rest/funding_auto_renew.py b/examples/rest/funding_auto_renew.py index 11ee7ca..f546707 100644 --- a/examples/rest/funding_auto_renew.py +++ b/examples/rest/funding_auto_renew.py @@ -2,10 +2,10 @@ import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/get_authenticated_data.py b/examples/rest/get_authenticated_data.py index ada724a..c3226af 100644 --- a/examples/rest/get_authenticated_data.py +++ b/examples/rest/get_authenticated_data.py @@ -3,10 +3,10 @@ import os import time -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/get_candles_hist.py b/examples/rest/get_candles_hist.py index 98f9da7..d8d9881 100644 --- a/examples/rest/get_candles_hist.py +++ b/examples/rest/get_candles_hist.py @@ -1,9 +1,9 @@ # python -c "import examples.rest.get_candles_hist" -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST + REST_HOST=REST_HOST ) print(f"Candles: {bfx.rest.public.get_candles_hist(symbol='tBTCUSD')}") diff --git a/examples/rest/get_funding_info.py b/examples/rest/get_funding_info.py index 83d0635..82bf150 100644 --- a/examples/rest/get_funding_info.py +++ b/examples/rest/get_funding_info.py @@ -2,10 +2,10 @@ import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/get_funding_trades_history.py b/examples/rest/get_funding_trades_history.py index c1cc8e6..3af19d8 100644 --- a/examples/rest/get_funding_trades_history.py +++ b/examples/rest/get_funding_trades_history.py @@ -2,10 +2,10 @@ import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/get_liquidations.py b/examples/rest/get_liquidations.py index 6113a25..588c83a 100644 --- a/examples/rest/get_liquidations.py +++ b/examples/rest/get_liquidations.py @@ -2,10 +2,10 @@ import time -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST + REST_HOST=REST_HOST ) now = int(round(time.time() * 1000)) diff --git a/examples/rest/get_positions.py b/examples/rest/get_positions.py index 7e71824..62cd309 100644 --- a/examples/rest/get_positions.py +++ b/examples/rest/get_positions.py @@ -3,10 +3,10 @@ import os import time -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/get_public_data.py b/examples/rest/get_public_data.py index a6c388b..125f97c 100644 --- a/examples/rest/get_public_data.py +++ b/examples/rest/get_public_data.py @@ -2,10 +2,10 @@ import time -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST + REST_HOST=REST_HOST ) now = int(round(time.time() * 1000)) diff --git a/examples/rest/get_pulse_data.py b/examples/rest/get_pulse_data.py index 75b55ae..b0cc369 100644 --- a/examples/rest/get_pulse_data.py +++ b/examples/rest/get_pulse_data.py @@ -2,10 +2,10 @@ import time -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST + REST_HOST=REST_HOST ) now = int(round(time.time() * 1000)) diff --git a/examples/rest/increase_position.py b/examples/rest/increase_position.py index 65595c8..add66e3 100644 --- a/examples/rest/increase_position.py +++ b/examples/rest/increase_position.py @@ -2,10 +2,10 @@ import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/keep_taken_funding.py b/examples/rest/keep_taken_funding.py index 1314ffa..21e60f4 100644 --- a/examples/rest/keep_taken_funding.py +++ b/examples/rest/keep_taken_funding.py @@ -2,10 +2,10 @@ import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/merchant.py b/examples/rest/merchant.py index ec6727b..84d9b9b 100644 --- a/examples/rest/merchant.py +++ b/examples/rest/merchant.py @@ -2,10 +2,10 @@ import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/return_taken_funding.py b/examples/rest/return_taken_funding.py index ccb0c2b..73d5a33 100644 --- a/examples/rest/return_taken_funding.py +++ b/examples/rest/return_taken_funding.py @@ -2,10 +2,10 @@ import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/transfer_wallet.py b/examples/rest/transfer_wallet.py index 8de15fd..9384bd8 100644 --- a/examples/rest/transfer_wallet.py +++ b/examples/rest/transfer_wallet.py @@ -2,10 +2,10 @@ import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/websocket/create_order.py b/examples/websocket/create_order.py index f72f9d4..48f4957 100644 --- a/examples/websocket/create_order.py +++ b/examples/websocket/create_order.py @@ -2,12 +2,12 @@ import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, WSS_HOST from bfxapi.websocket.enums import Error, OrderType from bfxapi.websocket.types import Notification, Order bfx = Client( - WSS_HOST=Constants.WSS_HOST, + WSS_HOST=WSS_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/websocket/order_book.py b/examples/websocket/order_book.py index a419454..55e4ae3 100644 --- a/examples/websocket/order_book.py +++ b/examples/websocket/order_book.py @@ -4,7 +4,7 @@ from collections import OrderedDict from typing import List -from bfxapi import Client, Constants +from bfxapi import Client, PUB_WSS_HOST from bfxapi.websocket import subscriptions from bfxapi.websocket.enums import Channel, Error @@ -38,7 +38,7 @@ SYMBOLS = [ "tBTCUSD", "tLTCUSD", "tLTCBTC", "tETHUSD", "tETHBTC" ] order_book = OrderBook(symbols=SYMBOLS) -bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) +bfx = Client(WSS_HOST=PUB_WSS_HOST) @bfx.wss.on("wss-error") def on_wss_error(code: Error, msg: str): diff --git a/examples/websocket/raw_order_book.py b/examples/websocket/raw_order_book.py index a039060..3ce9c6d 100644 --- a/examples/websocket/raw_order_book.py +++ b/examples/websocket/raw_order_book.py @@ -4,7 +4,7 @@ from collections import OrderedDict from typing import List -from bfxapi import Client, Constants +from bfxapi import Client, PUB_WSS_HOST from bfxapi.websocket import subscriptions from bfxapi.websocket.enums import Channel, Error @@ -38,7 +38,7 @@ SYMBOLS = [ "tBTCUSD", "tLTCUSD", "tLTCBTC", "tETHUSD", "tETHBTC" ] raw_order_book = RawOrderBook(symbols=SYMBOLS) -bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) +bfx = Client(WSS_HOST=PUB_WSS_HOST) @bfx.wss.on("wss-error") def on_wss_error(code: Error, msg: str): diff --git a/examples/websocket/ticker.py b/examples/websocket/ticker.py index a335a28..d4b4c91 100644 --- a/examples/websocket/ticker.py +++ b/examples/websocket/ticker.py @@ -1,12 +1,12 @@ # python -c "import examples.websocket.ticker" -from bfxapi import Client, Constants +from bfxapi import Client, PUB_WSS_HOST from bfxapi.websocket import subscriptions from bfxapi.websocket.enums import Channel from bfxapi.websocket.types import TradingPairTicker -bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) +bfx = Client(WSS_HOST=PUB_WSS_HOST) @bfx.wss.on("t_ticker_update") def on_t_ticker_update(subscription: subscriptions.Ticker, data: TradingPairTicker): From 32a179fc004026eab5d8491eb0875bfdd61502e3 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 17 Feb 2023 19:08:28 +0100 Subject: [PATCH 84/85] Add LICENSE.md file (Apache-V2). Edit setup.py with new arguments. Prepare to distribute on PyPI. --- LICENSE | 1 + setup.py | 37 ++++++++++++++++++++++++++----------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/LICENSE b/LICENSE index 2bb9ad2..4947287 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,4 @@ + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ diff --git a/setup.py b/setup.py index ce3ae3e..93bcd68 100644 --- a/setup.py +++ b/setup.py @@ -2,18 +2,36 @@ from distutils.core import setup setup( name="bitfinex-api-py", - version="3.0.0", + version="3.0.0b1", + description="Official Bitfinex Python API", + long_description="A Python reference implementation of the Bitfinex API for both REST and websocket interaction", + long_description_content_type="text/markdown", + url="https://github.com/bitfinexcom/bitfinex-api-py", + author="Bitfinex", + author_email="support@bitfinex.com", + license="Apache-2.0", + classifiers=[ + "Development Status :: 4 - Beta", + + "Intended Audience :: Developers", + "Topic :: Software Development :: Build Tools", + + "License :: OSI Approved :: Apache-2.0", + + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + ], + keywords="bitfinex,api,trading", + project_urls={ + "Bug Reports": "https://github.com/bitfinexcom/bitfinex-api-py/issues", + "Source": "https://github.com/bitfinexcom/bitfinex-api-py", + }, packages=[ "bfxapi", "bfxapi.utils", "bfxapi.websocket", "bfxapi.websocket.client", "bfxapi.websocket.handlers", "bfxapi.rest", "bfxapi.rest.endpoints", "bfxapi.rest.middleware", ], - url="https://github.com/bitfinexcom/bitfinex-api-py", - license="OSI Approved :: Apache Software License", - author="Bitfinex", - author_email="support@bitfinex.com", - description="Official Bitfinex Python API", - keywords="bitfinex,api,trading", install_requires=[ "certifi~=2022.12.7", "charset-normalizer~=2.1.1", @@ -29,8 +47,5 @@ setup( "urllib3~=1.26.13", "websockets~=10.4", ], - project_urls={ - "Bug Reports": "https://github.com/bitfinexcom/bitfinex-api-py/issues", - "Source": "https://github.com/bitfinexcom/bitfinex-api-py", - } + python_requires=">=3.8" ) \ No newline at end of file From f4c6a21ef490d1c8d2b19be8a1a707f83c1ef5c7 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 17 Feb 2023 20:23:59 +0100 Subject: [PATCH 85/85] Ws examples Co-Authored-By: itsdeka --- examples/websocket/derivatives_status.py | 23 ++++++++++++++++++ examples/websocket/trades.py | 29 +++++++++++++++++++++++ examples/websocket/wallet_balance.py | 30 ++++++++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 examples/websocket/derivatives_status.py create mode 100644 examples/websocket/trades.py create mode 100644 examples/websocket/wallet_balance.py diff --git a/examples/websocket/derivatives_status.py b/examples/websocket/derivatives_status.py new file mode 100644 index 0000000..3099431 --- /dev/null +++ b/examples/websocket/derivatives_status.py @@ -0,0 +1,23 @@ +# python -c "import examples.websocket.derivatives_status" + +from bfxapi import Client, PUB_WSS_HOST +from bfxapi.websocket.enums import Error, Channel +from bfxapi.websocket.types import DerivativesStatus + +from bfxapi.websocket import subscriptions + +bfx = Client(WSS_HOST=PUB_WSS_HOST) + +@bfx.wss.on("derivatives_status_update") +def on_derivatives_status_update(subscription: subscriptions.Status, data: DerivativesStatus): + print(f"{subscription}:", data) + +@bfx.wss.on("wss-error") +def on_wss_error(code: Error, msg: str): + print(code, msg) + +@bfx.wss.once("open") +async def open(): + await bfx.wss.subscribe(Channel.STATUS, key="deriv:tBTCF0:USTF0") + +bfx.wss.run() \ No newline at end of file diff --git a/examples/websocket/trades.py b/examples/websocket/trades.py new file mode 100644 index 0000000..0a5291f --- /dev/null +++ b/examples/websocket/trades.py @@ -0,0 +1,29 @@ +# python -c "import examples.websocket.trades" + +from bfxapi import Client, PUB_WSS_HOST +from bfxapi.websocket.enums import Error, Channel +from bfxapi.websocket.types import Candle, TradingPairTrade + +from bfxapi.websocket import subscriptions + +bfx = Client(WSS_HOST=PUB_WSS_HOST) + +@bfx.wss.on("candles_update") +def on_candles_update(subscription: subscriptions.Candles, candle: Candle): + print(f"New candle: {candle}") + +@bfx.wss.on("t_trade_executed") +def on_t_trade_executed(subscription: subscriptions.Trades, trade: TradingPairTrade): + print(f"New trade: {trade}") + +@bfx.wss.on("wss-error") +def on_wss_error(code: Error, msg: str): + print(code, msg) + +@bfx.wss.once("open") +async def open(): + await bfx.wss.subscribe(Channel.CANDLES, key="trade:1m:tBTCUSD") + + await bfx.wss.subscribe(Channel.TRADES, symbol="tBTCUSD") + +bfx.wss.run() \ No newline at end of file diff --git a/examples/websocket/wallet_balance.py b/examples/websocket/wallet_balance.py new file mode 100644 index 0000000..0e1b489 --- /dev/null +++ b/examples/websocket/wallet_balance.py @@ -0,0 +1,30 @@ +# python -c "import examples.websocket.wallet_balance" + +import os + +from typing import List + +from bfxapi import Client, WSS_HOST +from bfxapi.websocket.enums import Error +from bfxapi.websocket.types import Wallet + +bfx = Client( + WSS_HOST=WSS_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +@bfx.wss.on("wallet_snapshot") +def log_snapshot(wallets: List[Wallet]): + for wallet in wallets: + print(f"Balance: {wallet}") + +@bfx.wss.on("wallet_update") +def log_update(wallet: Wallet): + print(f"Balance update: {wallet}") + +@bfx.wss.on("wss-error") +def on_wss_error(code: Error, msg: str): + print(code, msg) + +bfx.wss.run() \ No newline at end of file