From 4cfeab8a79b877a8bbc13a4e39e355b637cb916e Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 30 Nov 2022 18:25:15 +0100 Subject: [PATCH 01/38] Add barebone files for rest section. --- bfxapi/client.py | 3 +++ bfxapi/rest/BfxRestInterface.py | 12 ++++++++++++ bfxapi/rest/__init__.py | 1 + bfxapi/rest/exceptions.py | 6 ++++++ bfxapi/rest/serializers.py | 30 ++++++++++++++++++++++++++++++ bfxapi/rest/typings.py | 9 +++++++++ bfxapi/websocket/exceptions.py | 2 +- requirements.txt | Bin 116 -> 304 bytes setup.py | 5 +++++ 9 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 bfxapi/rest/BfxRestInterface.py create mode 100644 bfxapi/rest/__init__.py create mode 100644 bfxapi/rest/exceptions.py create mode 100644 bfxapi/rest/serializers.py create mode 100644 bfxapi/rest/typings.py diff --git a/bfxapi/client.py b/bfxapi/client.py index d019e90..dd2fe93 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -3,6 +3,9 @@ from .websocket import BfxWebsocketClient from enum import Enum class Constants(str, Enum): + REST_HOST = "https://api.bitfinex.com/v2" + PUB_REST_HOST = "https://api-pub.bitfinex.com/v2" + WSS_HOST = "wss://api.bitfinex.com/ws/2" PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2" diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py new file mode 100644 index 0000000..f7eed3b --- /dev/null +++ b/bfxapi/rest/BfxRestInterface.py @@ -0,0 +1,12 @@ +import requests +from . import serializers +from .typings import PlatformStatus + +class BfxRestInterface(object): + def __init__(self, host): + self.host = host + + def platform_status(self) -> PlatformStatus: + return serializers.PlatformStatus.parse( + *requests.get(f"{self.host}/platform/status").json() + ) diff --git a/bfxapi/rest/__init__.py b/bfxapi/rest/__init__.py new file mode 100644 index 0000000..0bf3d2e --- /dev/null +++ b/bfxapi/rest/__init__.py @@ -0,0 +1 @@ +from .BfxRestInterface import BfxRestInterface \ No newline at end of file diff --git a/bfxapi/rest/exceptions.py b/bfxapi/rest/exceptions.py new file mode 100644 index 0000000..9aa8555 --- /dev/null +++ b/bfxapi/rest/exceptions.py @@ -0,0 +1,6 @@ +class BfxRestException(Exception): + """ + Base class for all exceptions defined in bfxapi/rest/exceptions.py. + """ + + pass \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py new file mode 100644 index 0000000..5ffcb0e --- /dev/null +++ b/bfxapi/rest/serializers.py @@ -0,0 +1,30 @@ +from typing import Generic, TypeVar, Iterable, List, Any + +from . import typings + +from .exceptions import BfxRestException + +T = TypeVar("T") + +class _Serializer(Generic[T]): + def __init__(self, name: str, labels: List[str]): + self.name, self.__labels = name, labels + + def __serialize(self, *args: Any, IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> Iterable[T]: + if len(self.__labels) != len(args): + raise BfxRestException(" and <*args> arguments should contain the same amount of elements.") + + for index, label in enumerate(self.__labels): + if label not in IGNORE: + yield label, args[index] + + def parse(self, *values: Any) -> T: + return dict(self.__serialize(*values)) + +#region Serializers definition for Rest Public Endpoints + +PlatformStatus = _Serializer[typings.PlatformStatus]("PlatformStatus", labels=[ + "OPERATIVE" +]) + +#endregion \ No newline at end of file diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py new file mode 100644 index 0000000..dd7732e --- /dev/null +++ b/bfxapi/rest/typings.py @@ -0,0 +1,9 @@ +from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any + +#region Type hinting for Rest Public Endpoints + +PlatformStatus = TypedDict("PlatformStatus", { + "OPERATIVE": int +}) + +#endregion \ No newline at end of file diff --git a/bfxapi/websocket/exceptions.py b/bfxapi/websocket/exceptions.py index 3a1f900..c55b767 100644 --- a/bfxapi/websocket/exceptions.py +++ b/bfxapi/websocket/exceptions.py @@ -9,7 +9,7 @@ __all__ = [ class BfxWebsocketException(Exception): """ - Base class for all exceptions defined in bfx/websocket/errors.py. + Base class for all exceptions defined in bfxapi/websocket/exceptions.py. """ pass diff --git a/requirements.txt b/requirements.txt index 78560fa31112931f95f0af059daf152fa323d63f..549f7a67024ef377ed41842b9f7abfb02005904f 100644 GIT binary patch literal 304 zcmYk1Sq_3g5JcqV)Vna7(i4a0x}Sn!>eyvC5ALGMb+!#^(<7X*Pu#!X2TUL zWMTzcGqF<5HB+IZGMooX%)9xrb3c=|B;=$WoPdI5XoWs2^6RJO0vCzayN! z^r#n&+#Orow~ueJnwSIWj-8DT+Ty(7EIB!})}FpcYD~}e?MJ%S)-+dQRNpby(x{1f Su26C)E&qDr{kK2*>-PpCnJ!8I delta 34 lcmdnMR3iTWUjaiULn;v30-+^?9)kf8n@n79Ihlu11pu!12%Z1{ diff --git a/setup.py b/setup.py index c130b0d..e6fa6e3 100644 --- a/setup.py +++ b/setup.py @@ -11,8 +11,13 @@ setup( description="Official Bitfinex Python API", keywords="bitfinex,api,trading", install_requires=[ + "certifi~=2022.9.24", + "charset-normalizer~=2.1.1", + "idna~=3.4", "pyee~=9.0.4", + "requests~=2.28.1", "typing_extensions~=4.4.0", + "urllib3~=1.26.13", "websockets~=10.4", ], project_urls={ From 8e8719e3d71ba25dbf5510fd15fd322da2a9cc19 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 30 Nov 2022 18:25:50 +0100 Subject: [PATCH 02/38] Add bfxapi.rest subpackage to setup.py. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e6fa6e3..a4c8397 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from distutils.core import setup setup( name="bitfinex-api-py", version="3.0.0", - packages=[ "bfxapi", "bfxapi.websocket", "bfxapi.utils" ], + packages=[ "bfxapi", "bfxapi.websocket", "bfxapi.rest", "bfxapi.utils" ], url="https://github.com/bitfinexcom/bitfinex-api-py", license="OSI Approved :: Apache Software License", author="Bitfinex", From ea6044a5eb7ab7a6ed800c51ae02e2aac8d12231 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 1 Dec 2022 17:48:50 +0100 Subject: [PATCH 03/38] Add support for new rest public endpoints (in BfxRestInterface.py, serializers.py and typings.py). --- bfxapi/rest/BfxRestInterface.py | 58 ++++++++++++++++++++-- bfxapi/rest/exceptions.py | 11 +++++ bfxapi/rest/serializers.py | 87 +++++++++++++++++++++++++++++---- bfxapi/rest/typings.py | 44 +++++++++++++++++ 4 files changed, 186 insertions(+), 14 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index f7eed3b..7423c1a 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -1,12 +1,62 @@ import requests + +from http import HTTPStatus + +from typing import List, Union, Optional + from . import serializers -from .typings import PlatformStatus +from .typings import PlatformStatus, TradingPairTicker, FundingCurrencyTicker, TickerHistory, TradingPairTrade, FundingCurrencyTrade +from .exceptions import RequestParametersError class BfxRestInterface(object): def __init__(self, host): self.host = host + def __GET(self, endpoint, params = None): + data = requests.get(f"{self.host}/{endpoint}", params=params).json() + + if data[0] == "error": + if data[1] == 10020: + raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") + + return data + def platform_status(self) -> PlatformStatus: - return serializers.PlatformStatus.parse( - *requests.get(f"{self.host}/platform/status").json() - ) + return serializers.PlatformStatus.parse(*self.__GET("platform/status")) + + def tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]: + return [ + { + "t": serializers.TradingPairTicker.parse, + "f": serializers.FundingCurrencyTicker.parse + }[subdata[0][0]](*subdata) + + for subdata in self.__GET("tickers", params={ "symbols": ",".join(symbols) }) + ] + + def ticker(self, symbol: str) -> Union[TradingPairTicker, FundingCurrencyTicker]: + return { + "t": serializers.TradingPairTicker.parse, + "f": serializers.FundingCurrencyTicker.parse + }[symbol[0]](*self.__GET(f"ticker/{symbol}"), skip=["SYMBOL"]) + + def tickers_hist(self, symbols: List[str], start: Optional[int] = None, end: Optional[int] = None, limit: Optional[int] = None) -> List[TickerHistory]: + params = { + "symbols": ",".join(symbols), + "start": start, "end": end, + "limit": limit + } + + return [ serializers.TickerHistory.parse(*subdata) for subdata in self.__GET("tickers/hist", params=params) ] + + def trades(self, symbol: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[int] = None) -> Union[List[TradingPairTrade], List[FundingCurrencyTicker]]: + params = { "symbol": symbol, "limit": limit, "start": start, "end": end, "sort": sort } + + return [ + { + "t": serializers.TradingPairTrade.parse, + "f": serializers.FundingCurrencyTrade.parse + }[symbol[0]](*subdata) + + for subdata in self.__GET(f"trades/{symbol}/hist", params=params) + ] \ No newline at end of file diff --git a/bfxapi/rest/exceptions.py b/bfxapi/rest/exceptions.py index 9aa8555..033848e 100644 --- a/bfxapi/rest/exceptions.py +++ b/bfxapi/rest/exceptions.py @@ -1,6 +1,17 @@ +__all__ = [ + "RequestParametersError" +] + class BfxRestException(Exception): """ Base class for all exceptions defined in bfxapi/rest/exceptions.py. """ + pass + +class RequestParametersError(BfxRestException): + """ + This error indicates that there are some invalid parameters sent along with an HTTP request. + """ + pass \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 5ffcb0e..a002cdf 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -1,4 +1,4 @@ -from typing import Generic, TypeVar, Iterable, List, Any +from typing import Generic, TypeVar, Iterable, Optional, List, Any from . import typings @@ -7,19 +7,21 @@ from .exceptions import BfxRestException T = TypeVar("T") class _Serializer(Generic[T]): - def __init__(self, name: str, labels: List[str]): - self.name, self.__labels = name, labels + def __init__(self, name: str, labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]): + self.name, self.__labels, self.__IGNORE = name, labels, IGNORE - def __serialize(self, *args: Any, IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> Iterable[T]: - if len(self.__labels) != len(args): - raise BfxRestException(" and <*args> arguments should contain the same amount of elements.") + def __serialize(self, *args: Any, skip: Optional[List[str]]) -> Iterable[T]: + labels = list(filter(lambda label: label not in (skip or list()), self.__labels)) - for index, label in enumerate(self.__labels): - if label not in IGNORE: + if len(labels) != len(args): + raise BfxRestException(" and <*args> arguments should contain the same amount of elements.") + + for index, label in enumerate(labels): + if label not in self.__IGNORE: yield label, args[index] - def parse(self, *values: Any) -> T: - return dict(self.__serialize(*values)) + def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: + return dict(self.__serialize(*values, skip=skip)) #region Serializers definition for Rest Public Endpoints @@ -27,4 +29,69 @@ PlatformStatus = _Serializer[typings.PlatformStatus]("PlatformStatus", labels=[ "OPERATIVE" ]) +TradingPairTicker = _Serializer[typings.TradingPairTicker]("TradingPairTicker", labels=[ + "SYMBOL", + "BID", + "BID_SIZE", + "ASK", + "ASK_SIZE", + "DAILY_CHANGE", + "DAILY_CHANGE_RELATIVE", + "LAST_PRICE", + "VOLUME", + "HIGH", + "LOW" +]) + +FundingCurrencyTicker = _Serializer[typings.FundingCurrencyTicker]("FundingCurrencyTicker", labels=[ + "SYMBOL", + "FRR", + "BID", + "BID_PERIOD", + "BID_SIZE", + "ASK", + "ASK_PERIOD", + "ASK_SIZE", + "DAILY_CHANGE", + "DAILY_CHANGE_RELATIVE", + "LAST_PRICE", + "VOLUME", + "HIGH", + "LOW", + "_PLACEHOLDER", + "_PLACEHOLDER", + "FRR_AMOUNT_AVAILABLE" +]) + +TickerHistory = _Serializer[typings.TickerHistory]("TickerHistory", labels=[ + "SYMBOL", + "BID", + "_PLACEHOLDER", + "ASK", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "MTS" +]) + +TradingPairTrade = _Serializer[typings.TradingPairTrade]("TradingPairTrade", labels=[ + "ID", + "MTS", + "AMOUNT", + "PRICE" +]) + +FundingCurrencyTrade = _Serializer[typings.FundingCurrencyTrade]("FundingCurrencyTrade", labels=[ + "ID", + "MTS", + "AMOUNT", + "RATE", + "PERIOD" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index dd7732e..ebaf7c2 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -6,4 +6,48 @@ PlatformStatus = TypedDict("PlatformStatus", { "OPERATIVE": int }) +TradingPairTicker = TypedDict("TradingPairTicker", { + "SYMBOL": Optional[str], + "BID": float, + "BID_SIZE": float, + "ASK": float, + "ASK_SIZE": float, + "DAILY_CHANGE": float, + "DAILY_CHANGE_RELATIVE": float, + "LAST_PRICE": float, + "VOLUME": float, + "HIGH": float, + "LOW": float +}) + +FundingCurrencyTicker = TypedDict("FundingCurrencyTicker", { + "SYMBOL": Optional[str], + "FRR": float, + "BID": float, + "BID_PERIOD": int, + "BID_SIZE": float, + "ASK": float, + "ASK_PERIOD": int, + "ASK_SIZE": float, + "DAILY_CHANGE": float, + "DAILY_CHANGE_RELATIVE": float, + "LAST_PRICE": float, + "VOLUME": float, + "HIGH": float, + "LOW": float, + "FRR_AMOUNT_AVAILABLE": float +}) + +TickerHistory = TypedDict("TickerHistory", { + "SYMBOL": str, + "BID": float, + "ASK": float, + "MTS": int +}) + +(TradingPairTrade, FundingCurrencyTrade) = ( + TypedDict("TradingPairTrade", { "ID": int, "MTS": int, "AMOUNT": float, "PRICE": float }), + TypedDict("FundingCurrencyTrade", { "ID": int, "MTS": int, "AMOUNT": float, "RATE": float, "PERIOD": int }) +) + #endregion \ No newline at end of file From 6e470dc925d42bbe3ce4cbee87c0b2dd2c643383 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 1 Dec 2022 17:53:57 +0100 Subject: [PATCH 04/38] Fix type hinting bug in rest section. --- bfxapi/rest/BfxRestInterface.py | 6 +++--- bfxapi/rest/typings.py | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 7423c1a..c60cbc1 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -5,7 +5,7 @@ from http import HTTPStatus from typing import List, Union, Optional from . import serializers -from .typings import PlatformStatus, TradingPairTicker, FundingCurrencyTicker, TickerHistory, TradingPairTrade, FundingCurrencyTrade +from .typings import PlatformStatus, TradingPairTicker, FundingCurrencyTicker, TickerHistories, TradingPairTrades, FundingCurrencyTrades from .exceptions import RequestParametersError class BfxRestInterface(object): @@ -40,7 +40,7 @@ class BfxRestInterface(object): "f": serializers.FundingCurrencyTicker.parse }[symbol[0]](*self.__GET(f"ticker/{symbol}"), skip=["SYMBOL"]) - def tickers_hist(self, symbols: List[str], start: Optional[int] = None, end: Optional[int] = None, limit: Optional[int] = None) -> List[TickerHistory]: + def tickers_hist(self, symbols: List[str], start: Optional[int] = None, end: Optional[int] = None, limit: Optional[int] = None) -> TickerHistories: params = { "symbols": ",".join(symbols), "start": start, "end": end, @@ -49,7 +49,7 @@ class BfxRestInterface(object): return [ serializers.TickerHistory.parse(*subdata) for subdata in self.__GET("tickers/hist", params=params) ] - def trades(self, symbol: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[int] = None) -> Union[List[TradingPairTrade], List[FundingCurrencyTicker]]: + def trades(self, symbol: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[int] = None) -> Union[TradingPairTrades, FundingCurrencyTrades]: params = { "symbol": symbol, "limit": limit, "start": start, "end": end, "sort": sort } return [ diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index ebaf7c2..d511088 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -45,9 +45,13 @@ TickerHistory = TypedDict("TickerHistory", { "MTS": int }) +TickerHistories = List[TickerHistory] + (TradingPairTrade, FundingCurrencyTrade) = ( TypedDict("TradingPairTrade", { "ID": int, "MTS": int, "AMOUNT": float, "PRICE": float }), TypedDict("FundingCurrencyTrade", { "ID": int, "MTS": int, "AMOUNT": float, "RATE": float, "PERIOD": int }) ) +(TradingPairTrades, FundingCurrencyTrades) = (List[TradingPairTrade], List[FundingCurrencyTrade]) + #endregion \ No newline at end of file From 52d007c05dd46e1acd457b8d93c81ccdbb29422e Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 1 Dec 2022 18:24:02 +0100 Subject: [PATCH 05/38] Add examples/websocket/order_book.py and raw_order_book.py demos. --- examples/websocket/order_book.py | 61 ++++++++++++++++++++++++++++ examples/websocket/raw_order_book.py | 61 ++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 examples/websocket/order_book.py create mode 100644 examples/websocket/raw_order_book.py diff --git a/examples/websocket/order_book.py b/examples/websocket/order_book.py new file mode 100644 index 0000000..3950ff9 --- /dev/null +++ b/examples/websocket/order_book.py @@ -0,0 +1,61 @@ +from collections import OrderedDict + +from bfxapi import Client, Constants + +from bfxapi.websocket import BfxWebsocketClient +from bfxapi.websocket.enums import Channels, Errors +from bfxapi.websocket.typings import Subscriptions, TradingPairBooks, TradingPairBook + +class OrderBook(object): + def __init__(self, symbols: list[str]): + self.order_book = { + symbol: { + "bids": OrderedDict(), "asks": OrderedDict() + } for symbol in symbols + } + + def update(self, symbol: str, data: TradingPairBook) -> None: + price, count, amount = data["PRICE"], data["COUNT"], data["AMOUNT"] + + kind = (amount > 0) and "bids" or "asks" + + if count > 0: + self.order_book[symbol][kind][price] = { + "price": price, + "count": count, + "amount": amount + } + + if count == 0: + if price in self.order_book[symbol][kind]: + del self.order_book[symbol][kind][price] + +SYMBOLS = [ "tBTCUSD", "tLTCUSD", "tLTCBTC", "tETHUSD", "tETHBTC" ] + +order_book = OrderBook(symbols=SYMBOLS) + +bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) + +@bfx.wss.on("wss-error") +def on_wss_error(code: Errors, msg: str): + print(code, msg) + +@bfx.wss.on("open") +async def on_open(): + for symbol in SYMBOLS: + await bfx.wss.subscribe(Channels.BOOK, symbol=symbol) + +@bfx.wss.on("subscribed") +def on_subscribed(subscription): + print(f"Subscription successful for pair <{subscription['pair']}>") + +@bfx.wss.on("t_book_snapshot") +def on_t_book_snapshot(subscription: Subscriptions.Book, snapshot: TradingPairBooks): + for data in snapshot: + order_book.update(subscription["symbol"], data) + +@bfx.wss.on("t_book_update") +def on_t_book_update(subscription: Subscriptions.Book, data: TradingPairBook): + order_book.update(subscription["symbol"], data) + +bfx.wss.run() \ No newline at end of file diff --git a/examples/websocket/raw_order_book.py b/examples/websocket/raw_order_book.py new file mode 100644 index 0000000..8198f7c --- /dev/null +++ b/examples/websocket/raw_order_book.py @@ -0,0 +1,61 @@ +from collections import OrderedDict + +from bfxapi import Client, Constants + +from bfxapi.websocket import BfxWebsocketClient +from bfxapi.websocket.enums import Channels, Errors +from bfxapi.websocket.typings import Subscriptions, TradingPairRawBooks, TradingPairRawBook + +class RawOrderBook(object): + def __init__(self, symbols: list[str]): + self.raw_order_book = { + symbol: { + "bids": OrderedDict(), "asks": OrderedDict() + } for symbol in symbols + } + + def update(self, symbol: str, data: TradingPairRawBook) -> None: + order_id, price, amount = data["ORDER_ID"], data["PRICE"], data["AMOUNT"] + + kind = (amount > 0) and "bids" or "asks" + + if price > 0: + self.raw_order_book[symbol][kind][order_id] = { + "order_id": order_id, + "price": price, + "amount": amount + } + + if price == 0: + if order_id in self.raw_order_book[symbol][kind]: + del self.raw_order_book[symbol][kind][order_id] + +SYMBOLS = [ "tBTCUSD", "tLTCUSD", "tLTCBTC", "tETHUSD", "tETHBTC" ] + +raw_order_book = RawOrderBook(symbols=SYMBOLS) + +bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) + +@bfx.wss.on("wss-error") +def on_wss_error(code: Errors, msg: str): + print(code, msg) + +@bfx.wss.on("open") +async def on_open(): + for symbol in SYMBOLS: + await bfx.wss.subscribe(Channels.BOOK, symbol=symbol, prec="R0") + +@bfx.wss.on("subscribed") +def on_subscribed(subscription): + print(f"Subscription successful for pair <{subscription['pair']}>") + +@bfx.wss.on("t_raw_book_snapshot") +def on_t_raw_book_snapshot(subscription: Subscriptions.Book, snapshot: TradingPairRawBooks): + for data in snapshot: + raw_order_book.update(subscription["symbol"], data) + +@bfx.wss.on("t_raw_book_update") +def on_t_raw_book_update(subscription: Subscriptions.Book, data: TradingPairRawBook): + raw_order_book.update(subscription["symbol"], data) + +bfx.wss.run() \ No newline at end of file From 8c9d52c1863b0675b491008a1d678ecc8823772f Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 2 Dec 2022 18:57:21 +0100 Subject: [PATCH 06/38] Rename class members in order_book.py and raw_order_book.py. --- examples/websocket/order_book.py | 8 ++++---- examples/websocket/raw_order_book.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/websocket/order_book.py b/examples/websocket/order_book.py index 3950ff9..a100c2b 100644 --- a/examples/websocket/order_book.py +++ b/examples/websocket/order_book.py @@ -8,7 +8,7 @@ from bfxapi.websocket.typings import Subscriptions, TradingPairBooks, TradingPai class OrderBook(object): def __init__(self, symbols: list[str]): - self.order_book = { + self.__order_book = { symbol: { "bids": OrderedDict(), "asks": OrderedDict() } for symbol in symbols @@ -20,15 +20,15 @@ class OrderBook(object): kind = (amount > 0) and "bids" or "asks" if count > 0: - self.order_book[symbol][kind][price] = { + self.__order_book[symbol][kind][price] = { "price": price, "count": count, "amount": amount } if count == 0: - if price in self.order_book[symbol][kind]: - del self.order_book[symbol][kind][price] + if price in self.__order_book[symbol][kind]: + del self.__order_book[symbol][kind][price] SYMBOLS = [ "tBTCUSD", "tLTCUSD", "tLTCBTC", "tETHUSD", "tETHBTC" ] diff --git a/examples/websocket/raw_order_book.py b/examples/websocket/raw_order_book.py index 8198f7c..fe10490 100644 --- a/examples/websocket/raw_order_book.py +++ b/examples/websocket/raw_order_book.py @@ -8,7 +8,7 @@ from bfxapi.websocket.typings import Subscriptions, TradingPairRawBooks, Trading class RawOrderBook(object): def __init__(self, symbols: list[str]): - self.raw_order_book = { + self.__raw_order_book = { symbol: { "bids": OrderedDict(), "asks": OrderedDict() } for symbol in symbols @@ -20,15 +20,15 @@ class RawOrderBook(object): kind = (amount > 0) and "bids" or "asks" if price > 0: - self.raw_order_book[symbol][kind][order_id] = { + self.__raw_order_book[symbol][kind][order_id] = { "order_id": order_id, "price": price, "amount": amount } if price == 0: - if order_id in self.raw_order_book[symbol][kind]: - del self.raw_order_book[symbol][kind][order_id] + if order_id in self.__raw_order_book[symbol][kind]: + del self.__raw_order_book[symbol][kind][order_id] SYMBOLS = [ "tBTCUSD", "tLTCUSD", "tLTCBTC", "tETHUSD", "tETHBTC" ] From e0785f9f4a5225db40cb2dec01a1b205e9fd95ea Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 8 Dec 2022 17:35:39 +0100 Subject: [PATCH 07/38] Add support for GET book/{Symbol}/{Precision} endpoint. --- bfxapi/rest/BfxRestInterface.py | 12 +++++++++++- bfxapi/rest/serializers.py | 26 ++++++++++++++++++++++++++ bfxapi/rest/typings.py | 14 ++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index c60cbc1..662a94c 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -5,7 +5,7 @@ from http import HTTPStatus from typing import List, Union, Optional from . import serializers -from .typings import PlatformStatus, TradingPairTicker, FundingCurrencyTicker, TickerHistories, TradingPairTrades, FundingCurrencyTrades +from .typings import * from .exceptions import RequestParametersError class BfxRestInterface(object): @@ -59,4 +59,14 @@ class BfxRestInterface(object): }[symbol[0]](*subdata) for subdata in self.__GET(f"trades/{symbol}/hist", params=params) + ] + + def book(self, symbol: str, precision: str, len: Optional[int]) -> Union[TradingPairBooks, FundingCurrencyBooks, TradingPairRawBooks, FundingCurrencyRawBooks]: + return [ + { + "t": precision == "R0" and serializers.TradingPairRawBook.parse or serializers.TradingPairBook.parse, + "f": precision == "R0" and serializers.FundingCurrencyRawBook.parse or serializers.FundingCurrencyBook.parse, + }[symbol[0]](*subdata) + + for subdata in self.__GET(f"book/{symbol}/{precision}", params={ "len": len }) ] \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index a002cdf..d1c64c2 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -94,4 +94,30 @@ FundingCurrencyTrade = _Serializer[typings.FundingCurrencyTrade]("FundingCurrenc "PERIOD" ]) +TradingPairBook = _Serializer[typings.TradingPairBook]("TradingPairBook", labels=[ + "PRICE", + "COUNT", + "AMOUNT" +]) + +FundingCurrencyBook = _Serializer[typings.FundingCurrencyBook]("FundingCurrencyBook", labels=[ + "RATE", + "PERIOD", + "COUNT", + "AMOUNT" +]) + +TradingPairRawBook = _Serializer[typings.TradingPairRawBook]("TradingPairRawBook", labels=[ + "ORDER_ID", + "PRICE", + "AMOUNT" +]) + +FundingCurrencyRawBook = _Serializer[typings.FundingCurrencyRawBook]("FundingCurrencyRawBook", labels=[ + "OFFER_ID", + "PERIOD", + "RATE", + "AMOUNT" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index d511088..3b52a49 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -54,4 +54,18 @@ TickerHistories = List[TickerHistory] (TradingPairTrades, FundingCurrencyTrades) = (List[TradingPairTrade], List[FundingCurrencyTrade]) +(TradingPairBook, FundingCurrencyBook) = ( + TypedDict("TradingPairBook", { "PRICE": float, "COUNT": int, "AMOUNT": float }), + TypedDict("FundingCurrencyBook", { "RATE": float, "PERIOD": int, "COUNT": int, "AMOUNT": float }) +) + +(TradingPairBooks, FundingCurrencyBooks) = (List[TradingPairBook], List[FundingCurrencyBook]) + +(TradingPairRawBook, FundingCurrencyRawBook) = ( + TypedDict("TradingPairRawBook", { "ORDER_ID": int, "PRICE": float, "AMOUNT": float }), + TypedDict("FundingCurrencyRawBook", { "OFFER_ID": int, "PERIOD": int, "RATE": float, "AMOUNT": float }), +) + +(TradingPairRawBooks, FundingCurrencyRawBooks) = (List[TradingPairRawBook], List[FundingCurrencyRawBook]) + #endregion \ No newline at end of file From cd5ef4211812ca146d13b35528acb37720a0004b Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 9 Dec 2022 16:16:15 +0100 Subject: [PATCH 08/38] Add support for new various endpoints. Add ResourceNotFound error in bfxapi/rest/exceptions.py. Fix bug in BfxRestInterface.__GET method. --- bfxapi/rest/BfxRestInterface.py | 64 +++++++++++++++++++++++++++++---- bfxapi/rest/exceptions.py | 10 +++++- bfxapi/rest/serializers.py | 41 +++++++++++++++++++++ bfxapi/rest/typings.py | 36 +++++++++++++++++++ 4 files changed, 143 insertions(+), 8 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 662a94c..e511692 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -2,20 +2,25 @@ import requests from http import HTTPStatus -from typing import List, Union, Optional +from typing import List, Union, Literal, Optional from . import serializers from .typings import * -from .exceptions import RequestParametersError +from .exceptions import RequestParametersError, ResourceNotFound class BfxRestInterface(object): def __init__(self, host): self.host = host def __GET(self, endpoint, params = None): - data = requests.get(f"{self.host}/{endpoint}", params=params).json() + response = requests.get(f"{self.host}/{endpoint}", params=params) - if data[0] == "error": + if response.status_code == HTTPStatus.NOT_FOUND: + raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") + + data = response.json() + + if len(data) and data[0] == "error": if data[1] == 10020: raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") @@ -40,7 +45,7 @@ class BfxRestInterface(object): "f": serializers.FundingCurrencyTicker.parse }[symbol[0]](*self.__GET(f"ticker/{symbol}"), skip=["SYMBOL"]) - def tickers_hist(self, symbols: List[str], start: Optional[int] = None, end: Optional[int] = None, limit: Optional[int] = None) -> TickerHistories: + def tickers_history(self, symbols: List[str], start: Optional[int] = None, end: Optional[int] = None, limit: Optional[int] = None) -> TickerHistories: params = { "symbols": ",".join(symbols), "start": start, "end": end, @@ -61,7 +66,7 @@ class BfxRestInterface(object): for subdata in self.__GET(f"trades/{symbol}/hist", params=params) ] - def book(self, symbol: str, precision: str, len: Optional[int]) -> Union[TradingPairBooks, FundingCurrencyBooks, TradingPairRawBooks, FundingCurrencyRawBooks]: + def book(self, symbol: str, precision: str, len: Optional[int] = None) -> Union[TradingPairBooks, FundingCurrencyBooks, TradingPairRawBooks, FundingCurrencyRawBooks]: return [ { "t": precision == "R0" and serializers.TradingPairRawBook.parse or serializers.TradingPairBook.parse, @@ -69,4 +74,49 @@ class BfxRestInterface(object): }[symbol[0]](*subdata) for subdata in self.__GET(f"book/{symbol}/{precision}", params={ "len": len }) - ] \ No newline at end of file + ] + + def stats( + self, + resource: str, section: Literal["hist", "last"], + sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Union[Stat, Stats]: + endpoint = f"stats1/{resource}/{section}" + + params = { "sort": sort, "start": start, "end": end, "limit": limit } + + if section == "last": + return serializers.Stat.parse(*self.__GET(endpoint, params=params)) + return [ serializers.Stat.parse(*subdata) for subdata in self.__GET(endpoint, params=params) ] + + def candles( + self, + resource: str, section: Literal["hist", "last"], + sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Union[Candle, Candles]: + endpoint = f"candles/{resource}/{section}" + + params = { "sort": sort, "start": start, "end": end, "limit": limit } + + if section == "last": + return serializers.Candle.parse(*self.__GET(endpoint, params=params)) + return [ serializers.Candle.parse(*subdata) for subdata in self.__GET(endpoint, params=params) ] + + def derivatives_status(self, type: str, keys: Optional[List[str]] = None) -> DerivativeStatuses: + params = None + + if keys != None: + params = { "keys": ",".join(keys) } + + return [ serializers.DerivativesStatus.parse(*subdata) for subdata in self.__GET(f"status/{type}", params=params) ] + + def derivatives_status_history( + self, + type: str, symbol: str, + sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> DerivativeStatuses: + endpoint = f"status/{type}/{symbol}/hist" + + params = { "sort": sort, "start": start, "end": end, "limit": limit } + + return [ serializers.DerivativesStatus.parse(*subdata, skip=[ "KEY" ]) for subdata in self.__GET(endpoint, params=params) ] \ No newline at end of file diff --git a/bfxapi/rest/exceptions.py b/bfxapi/rest/exceptions.py index 033848e..8afc74e 100644 --- a/bfxapi/rest/exceptions.py +++ b/bfxapi/rest/exceptions.py @@ -1,5 +1,6 @@ __all__ = [ - "RequestParametersError" + "RequestParametersError", + "ResourceNotFound" ] class BfxRestException(Exception): @@ -14,4 +15,11 @@ class RequestParametersError(BfxRestException): This error indicates that there are some invalid parameters sent along with an HTTP request. """ + pass + +class ResourceNotFound(BfxRestException): + """ + This error indicates a failed HTTP request to a non-existent resource. + """ + pass \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index d1c64c2..43f6ace 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -120,4 +120,45 @@ FundingCurrencyRawBook = _Serializer[typings.FundingCurrencyRawBook]("FundingCur "AMOUNT" ]) +Stat = _Serializer[typings.Stat]("Stat", labels=[ + "MTS", + "VALUE" +]) + +Candle = _Serializer[typings.Candle]("Candle", labels=[ + "MTS", + "OPEN", + "CLOSE", + "HIGH", + "LOW", + "VOLUME" +]) + +DerivativesStatus = _Serializer[typings.DerivativesStatus]("DerivativesStatus", labels=[ + "KEY", + "MTS", + "_PLACEHOLDER", + "DERIV_PRICE", + "SPOT_PRICE", + "_PLACEHOLDER", + "INSURANCE_FUND_BALANCE", + "_PLACEHOLDER", + "NEXT_FUNDING_EVT_TIMESTAMP_MS", + "NEXT_FUNDING_ACCRUED", + "NEXT_FUNDING_STEP", + "_PLACEHOLDER", + "CURRENT_FUNDING", + "_PLACEHOLDER", + "_PLACEHOLDER", + "MARK_PRICE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "OPEN_INTEREST", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "CLAMP_MIN", + "CLAMP_MAX" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index 3b52a49..7c8f127 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -68,4 +68,40 @@ TickerHistories = List[TickerHistory] (TradingPairRawBooks, FundingCurrencyRawBooks) = (List[TradingPairRawBook], List[FundingCurrencyRawBook]) +Stat = TypedDict("Stat", { + "MTS": int, + "VALUE": float +}) + +Stats = List[Stat] + +Candle = TypedDict("Candle", { + "MTS": int, + "OPEN": float, + "CLOSE": float, + "HIGH": float, + "LOW": float, + "VOLUME": float +}) + +Candles = List[Candle] + +DerivativesStatus = TypedDict("DerivativesStatus", { + "KEY": str, + "MTS": int, + "DERIV_PRICE": float, + "SPOT_PRICE": float, + "INSURANCE_FUND_BALANCE": float, + "NEXT_FUNDING_EVT_TIMESTAMP_MS": int, + "NEXT_FUNDING_ACCRUED": float, + "NEXT_FUNDING_STEP": int, + "CURRENT_FUNDING": float, + "MARK_PRICE": float, + "OPEN_INTEREST": float, + "CLAMP_MIN": float, + "CLAMP_MAX": float +}) + +DerivativeStatuses = List[DerivativesStatus] + #endregion \ No newline at end of file From 6a368d139db82a4e99afab0870d44133e0e896ff Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 9 Dec 2022 16:23:51 +0100 Subject: [PATCH 09/38] Add support for GET liquidations/hist endpoint. --- bfxapi/rest/BfxRestInterface.py | 7 ++++++- bfxapi/rest/serializers.py | 15 +++++++++++++++ bfxapi/rest/typings.py | 13 +++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index e511692..375d0d6 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -119,4 +119,9 @@ class BfxRestInterface(object): params = { "sort": sort, "start": start, "end": end, "limit": limit } - return [ serializers.DerivativesStatus.parse(*subdata, skip=[ "KEY" ]) for subdata in self.__GET(endpoint, params=params) ] \ No newline at end of file + return [ serializers.DerivativesStatus.parse(*subdata, skip=[ "KEY" ]) for subdata in self.__GET(endpoint, params=params) ] + + def liquidations(self, sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> Liquidations: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + + return [ serializers.Liquidation.parse(*subdata[0]) for subdata in self.__GET("liquidations/hist", params=params) ] \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 43f6ace..22817e0 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -161,4 +161,19 @@ DerivativesStatus = _Serializer[typings.DerivativesStatus]("DerivativesStatus", "CLAMP_MAX" ]) +Liquidation = _Serializer[typings.Liquidation]("Liquidation", labels=[ + "_PLACEHOLDER", + "POS_ID", + "MTS", + "_PLACEHOLDER", + "SYMBOL", + "AMOUNT", + "BASE_PRICE", + "_PLACEHOLDER", + "IS_MATCH", + "IS_MARKET_SOLD", + "_PLACEHOLDER", + "PRICE_ACQUIRED" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index 7c8f127..5afc040 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -104,4 +104,17 @@ DerivativesStatus = TypedDict("DerivativesStatus", { DerivativeStatuses = List[DerivativesStatus] +Liquidation = TypedDict("Liquidation", { + "POS_ID": int, + "MTS": int, + "SYMBOL": str, + "AMOUNT": float, + "BASE_PRICE": float, + "IS_MATCH": int, + "IS_MARKET_SOLD": int, + "PRICE_ACQUIRED": float +}) + +Liquidations = List[Liquidation] + #endregion \ No newline at end of file From 376ac37273d46e7b54b5067d6d4199cc39347179 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 12 Dec 2022 15:23:43 +0100 Subject: [PATCH 10/38] Fix small bug in BfxRestInterface.py file. --- bfxapi/rest/BfxRestInterface.py | 57 ++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 375d0d6..35153fc 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -30,20 +30,24 @@ class BfxRestInterface(object): return serializers.PlatformStatus.parse(*self.__GET("platform/status")) def tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]: + data = self.__GET("tickers", params={ "symbols": ",".join(symbols) }) + return [ { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse }[subdata[0][0]](*subdata) - for subdata in self.__GET("tickers", params={ "symbols": ",".join(symbols) }) + for subdata in data ] def ticker(self, symbol: str) -> Union[TradingPairTicker, FundingCurrencyTicker]: + data = self.__GET(f"ticker/{symbol}") + return { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse - }[symbol[0]](*self.__GET(f"ticker/{symbol}"), skip=["SYMBOL"]) + }[symbol[0]](*data, skip=["SYMBOL"]) def tickers_history(self, symbols: List[str], start: Optional[int] = None, end: Optional[int] = None, limit: Optional[int] = None) -> TickerHistories: params = { @@ -51,29 +55,35 @@ class BfxRestInterface(object): "start": start, "end": end, "limit": limit } + + data = self.__GET("tickers/hist", params=params) - return [ serializers.TickerHistory.parse(*subdata) for subdata in self.__GET("tickers/hist", params=params) ] + return [ serializers.TickerHistory.parse(*subdata) for subdata in data ] def trades(self, symbol: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[int] = None) -> Union[TradingPairTrades, FundingCurrencyTrades]: params = { "symbol": symbol, "limit": limit, "start": start, "end": end, "sort": sort } + data = self.__GET(f"trades/{symbol}/hist", params=params) + return [ { "t": serializers.TradingPairTrade.parse, "f": serializers.FundingCurrencyTrade.parse }[symbol[0]](*subdata) - for subdata in self.__GET(f"trades/{symbol}/hist", params=params) + for subdata in data ] def book(self, symbol: str, precision: str, len: Optional[int] = None) -> Union[TradingPairBooks, FundingCurrencyBooks, TradingPairRawBooks, FundingCurrencyRawBooks]: + data = self.__GET(f"book/{symbol}/{precision}", params={ "len": len }) + return [ { "t": precision == "R0" and serializers.TradingPairRawBook.parse or serializers.TradingPairBook.parse, "f": precision == "R0" and serializers.FundingCurrencyRawBook.parse or serializers.FundingCurrencyBook.parse, }[symbol[0]](*subdata) - for subdata in self.__GET(f"book/{symbol}/{precision}", params={ "len": len }) + for subdata in data ] def stats( @@ -81,47 +91,48 @@ class BfxRestInterface(object): resource: str, section: Literal["hist", "last"], sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> Union[Stat, Stats]: - endpoint = f"stats1/{resource}/{section}" - params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self.__GET(f"stats1/{resource}/{section}", params=params) + if section == "last": - return serializers.Stat.parse(*self.__GET(endpoint, params=params)) - return [ serializers.Stat.parse(*subdata) for subdata in self.__GET(endpoint, params=params) ] + return serializers.Stat.parse(*data) + return [ serializers.Stat.parse(*subdata) for subdata in data ] def candles( self, resource: str, section: Literal["hist", "last"], sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> Union[Candle, Candles]: - endpoint = f"candles/{resource}/{section}" - params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self.__GET(f"candles/{resource}/{section}", params=params) + if section == "last": - return serializers.Candle.parse(*self.__GET(endpoint, params=params)) - return [ serializers.Candle.parse(*subdata) for subdata in self.__GET(endpoint, params=params) ] + return serializers.Candle.parse(*data) + return [ serializers.Candle.parse(*subdata) for subdata in data ] - def derivatives_status(self, type: str, keys: Optional[List[str]] = None) -> DerivativeStatuses: - params = None - - if keys != None: - params = { "keys": ",".join(keys) } + def derivatives_status(self, type: str, keys: List[str] = None) -> DerivativeStatuses: + params = { "keys": ",".join(keys) } - return [ serializers.DerivativesStatus.parse(*subdata) for subdata in self.__GET(f"status/{type}", params=params) ] + data = self.__GET(f"status/{type}", params=params) + + return [ serializers.DerivativesStatus.parse(*subdata) for subdata in data ] def derivatives_status_history( self, type: str, symbol: str, sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> DerivativeStatuses: - endpoint = f"status/{type}/{symbol}/hist" - params = { "sort": sort, "start": start, "end": end, "limit": limit } - return [ serializers.DerivativesStatus.parse(*subdata, skip=[ "KEY" ]) for subdata in self.__GET(endpoint, params=params) ] + data = self.__GET(f"status/{type}/{symbol}/hist", params=params) + + return [ serializers.DerivativesStatus.parse(*subdata, skip=[ "KEY" ]) for subdata in data ] def liquidations(self, sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> Liquidations: params = { "sort": sort, "start": start, "end": end, "limit": limit } - return [ serializers.Liquidation.parse(*subdata[0]) for subdata in self.__GET("liquidations/hist", params=params) ] \ No newline at end of file + data = self.__GET("liquidations/hist", params=params) + + return [ serializers.Liquidation.parse(*subdata[0]) for subdata in data ] \ No newline at end of file From 32d698285e232bd6eb1f40e246b4203fb8b90660 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 12 Dec 2022 17:06:33 +0100 Subject: [PATCH 11/38] Add new endpoints in BfxRestInterfaces.py (with serializers and typings). --- bfxapi/rest/BfxRestInterface.py | 24 ++++++++++++++++++++++-- bfxapi/rest/serializers.py | 30 +++++++++++++++++++++++++++++- bfxapi/rest/typings.py | 23 ++++++++++++++++++++++- 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 35153fc..0c02687 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -2,7 +2,7 @@ import requests from http import HTTPStatus -from typing import List, Union, Literal, Optional +from typing import List, Union, Literal, Optional, Any from . import serializers from .typings import * @@ -135,4 +135,24 @@ class BfxRestInterface(object): data = self.__GET("liquidations/hist", params=params) - return [ serializers.Liquidation.parse(*subdata[0]) for subdata in data ] \ No newline at end of file + return [ serializers.Liquidation.parse(*subdata[0]) for subdata in data ] + + def leaderboards( + self, + resource: str, section: Literal["hist", "last"], + sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Union[Leaderboard, Leaderboards]: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + + data = self.__GET(f"rankings/{resource}/{section}", params=params) + + if section == "last": + return serializers.Leaderboard.parse(*data) + return [ serializers.Leaderboard.parse(*subdata) for subdata in data ] + + def funding_stats(self, symbol: str, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> FundingStats: + params = { "start": start, "end": end, "limit": limit } + + data = self.__GET(f"funding/stats/{symbol}/hist", params=params) + + return [ serializers.FundingStat.parse(*subdata) for subdata in data ] \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 22817e0..a2f69df 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -13,7 +13,7 @@ class _Serializer(Generic[T]): def __serialize(self, *args: Any, skip: Optional[List[str]]) -> Iterable[T]: labels = list(filter(lambda label: label not in (skip or list()), self.__labels)) - if len(labels) != len(args): + if len(labels) > len(args): raise BfxRestException(" and <*args> arguments should contain the same amount of elements.") for index, label in enumerate(labels): @@ -176,4 +176,32 @@ Liquidation = _Serializer[typings.Liquidation]("Liquidation", labels=[ "PRICE_ACQUIRED" ]) +Leaderboard = _Serializer[typings.Leaderboard]("Leaderboard", labels=[ + "MTS", + "_PLACEHOLDER", + "USERNAME", + "RANKING", + "_PLACEHOLDER", + "_PLACEHOLDER", + "VALUE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "TWITTER_HANDLE" +]) + +FundingStat = _Serializer[typings.FundingStat]("FundingStat", labels=[ + "TIMESTAMP", + "_PLACEHOLDER", + "_PLACEHOLDER", + "FRR", + "AVG_PERIOD", + "_PLACEHOLDER", + "_PLACEHOLDER", + "FUNDING_AMOUNT", + "FUNDING_AMOUNT_USED", + "_PLACEHOLDER", + "_PLACEHOLDER", + "FUNDING_BELOW_THRESHOLD" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index 5afc040..828c228 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -87,7 +87,7 @@ Candle = TypedDict("Candle", { Candles = List[Candle] DerivativesStatus = TypedDict("DerivativesStatus", { - "KEY": str, + "KEY": Optional[str], "MTS": int, "DERIV_PRICE": float, "SPOT_PRICE": float, @@ -117,4 +117,25 @@ Liquidation = TypedDict("Liquidation", { Liquidations = List[Liquidation] +Leaderboard = TypedDict("Leaderboard", { + "MTS": int, + "USERNAME": str, + "RANKING": int, + "VALUE": float, + "TWITTER_HANDLE": Optional[str] +}) + +Leaderboards = List[Leaderboard] + +FundingStat = TypedDict("FundingStat", { + "TIMESTAMP": int, + "FRR": float, + "AVG_PERIOD": float, + "FUNDING_AMOUNT": float, + "FUNDING_AMOUNT_USED": float, + "FUNDING_BELOW_THRESHOLD": float +}) + +FundingStats = List[FundingStat] + #endregion \ No newline at end of file From 862ba6d48132347a6ea217a04d16021ae644eec5 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 12 Dec 2022 17:14:58 +0100 Subject: [PATCH 12/38] Add support for GET conf/pub:{Action}:{Object}:{Detail} endpoint. Add bfxapi/rest/enums.py script. Add Configs enumeration in enums.py. --- bfxapi/rest/BfxRestInterface.py | 7 ++++++- bfxapi/rest/enums.py | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 bfxapi/rest/enums.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 0c02687..e4bb11c 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -5,7 +5,9 @@ from http import HTTPStatus from typing import List, Union, Literal, Optional, Any from . import serializers + from .typings import * +from .enums import Configs from .exceptions import RequestParametersError, ResourceNotFound class BfxRestInterface(object): @@ -155,4 +157,7 @@ class BfxRestInterface(object): data = self.__GET(f"funding/stats/{symbol}/hist", params=params) - return [ serializers.FundingStat.parse(*subdata) for subdata in data ] \ No newline at end of file + return [ serializers.FundingStat.parse(*subdata) for subdata in data ] + + def conf(self, config: Configs) -> Any: + return self.__GET(f"conf/{config}")[0] \ No newline at end of file diff --git a/bfxapi/rest/enums.py b/bfxapi/rest/enums.py new file mode 100644 index 0000000..3bb05d3 --- /dev/null +++ b/bfxapi/rest/enums.py @@ -0,0 +1,25 @@ +from enum import Enum + +class Configs(str, Enum): + MAP_CURRENCY_SYM = "pub:map:currency:sym" + MAP_CURRENCY_LABEL = "pub:map:currency:label" + MAP_CURRENCY_UNIT = "pub:map:currency:unit" + MAP_CURRENCY_UNDL = "pub:map:currency:undl" + MAP_CURRENCY_POOL = "pub:map:currency:pool" + MAP_CURRENCY_EXPLORER = "pub:map:currency:explorer" + MAP_CURRENCY_TX_FEE = "pub:map:currency:tx:fee" + MAP_TX_METHOD = "pub:map:tx:method" + + LIST_PAIR_EXCHANGE = "pub:list:pair:exchange" + LIST_PAIR_MARGIN = "pub:list:pair:margin" + LIST_PAIR_FUTURES = "pub:list:pair:futures" + LIST_PAIR_SECURITIES = "pub:list:pair:securities" + LIST_CURRENCY = "pub:list:currency" + LIST_COMPETITIONS = "pub:list:competitions" + + INFO_PAIR = "pub:info:pair" + INFO_PAIR_FUTURES = "pub:info:pair:futures" + INFO_TX_STATUS = "pub:info:tx:status" + + SPEC_MARGIN = "pub:spec:margin", + FEES = "pub:fees" \ No newline at end of file From ec821a07520efa605e83626f23c61b27e6467b87 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 14 Dec 2022 18:05:45 +0100 Subject: [PATCH 13/38] Split BfxRestInterface methods in t_ and f_ handlers. --- bfxapi/rest/BfxRestInterface.py | 158 ++++++++++++++++++-------------- bfxapi/rest/enums.py | 15 ++- bfxapi/rest/serializers.py | 2 +- bfxapi/rest/typings.py | 4 +- 4 files changed, 105 insertions(+), 74 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index e4bb11c..018ce7b 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -2,12 +2,12 @@ import requests from http import HTTPStatus -from typing import List, Union, Literal, Optional, Any +from typing import List, Union, Literal, Optional, Any, cast from . import serializers from .typings import * -from .enums import Configs +from .enums import Config, Precision, Sort from .exceptions import RequestParametersError, ResourceNotFound class BfxRestInterface(object): @@ -34,24 +34,33 @@ class BfxRestInterface(object): def tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]: data = self.__GET("tickers", params={ "symbols": ",".join(symbols) }) - return [ - { - "t": serializers.TradingPairTicker.parse, - "f": serializers.FundingCurrencyTicker.parse - }[subdata[0][0]](*subdata) + parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse } + + return [ parsers[subdata[0][0]](*subdata) for subdata in data ] - for subdata in data - ] + def t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]: + if isinstance(pairs, str) and pairs == "ALL": + return [ subdata for subdata in self.tickers([ "ALL" ]) if subdata["SYMBOL"].startswith("t") ] - def ticker(self, symbol: str) -> Union[TradingPairTicker, FundingCurrencyTicker]: - data = self.__GET(f"ticker/{symbol}") + data = self.tickers([ "t" + pair for pair in pairs ]) - return { - "t": serializers.TradingPairTicker.parse, - "f": serializers.FundingCurrencyTicker.parse - }[symbol[0]](*data, skip=["SYMBOL"]) + return cast(List[TradingPairTicker], data) - def tickers_history(self, symbols: List[str], start: Optional[int] = None, end: Optional[int] = None, limit: Optional[int] = None) -> TickerHistories: + def f_tickers(self, currencies: Union[List[str], Literal["ALL"]]) -> List[FundingCurrencyTicker]: + if isinstance(currencies, str) and currencies == "ALL": + return [ subdata for subdata in self.tickers([ "ALL" ]) if subdata["SYMBOL"].startswith("f") ] + + data = self.tickers([ "f" + currency for currency in currencies ]) + + return cast(List[FundingCurrencyTicker], data) + + def t_ticker(self, pair: str) -> TradingPairTicker: + return serializers.TradingPairTicker.parse(*self.__GET(f"ticker/t{pair}"), skip=["SYMBOL"]) + + def f_ticker(self, currency: str) -> FundingCurrencyTicker: + return serializers.FundingCurrencyTicker.parse(*self.__GET(f"ticker/f{currency}"), skip=["SYMBOL"]) + + def tickers_history(self, symbols: List[str], start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> TickersHistories: params = { "symbols": ",".join(symbols), "start": start, "end": end, @@ -60,61 +69,67 @@ class BfxRestInterface(object): data = self.__GET("tickers/hist", params=params) - return [ serializers.TickerHistory.parse(*subdata) for subdata in data ] + return [ serializers.TickersHistory.parse(*subdata) for subdata in data ] - def trades(self, symbol: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[int] = None) -> Union[TradingPairTrades, FundingCurrencyTrades]: - params = { "symbol": symbol, "limit": limit, "start": start, "end": end, "sort": sort } - - data = self.__GET(f"trades/{symbol}/hist", params=params) + def t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> TradingPairTrades: + params = { "limit": limit, "start": start, "end": end, "sort": sort } + data = self.__GET(f"trades/{'t' + pair}/hist", params=params) + return [ serializers.TradingPairTrade.parse(*subdata) for subdata in data ] - return [ - { - "t": serializers.TradingPairTrade.parse, - "f": serializers.FundingCurrencyTrade.parse - }[symbol[0]](*subdata) + def f_trades(self, currency: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> FundingCurrencyTrades: + params = { "limit": limit, "start": start, "end": end, "sort": sort } + data = self.__GET(f"trades/{'f' + currency}/hist", params=params) + return [ serializers.FundingCurrencyTrade.parse(*subdata) for subdata in data ] - for subdata in data - ] + def t_book(self, pair: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> TradingPairBooks: + return [ serializers.TradingPairBook.parse(*subdata) for subdata in self.__GET(f"book/{'t' + pair}/{precision}", params={ "len": len }) ] - def book(self, symbol: str, precision: str, len: Optional[int] = None) -> Union[TradingPairBooks, FundingCurrencyBooks, TradingPairRawBooks, FundingCurrencyRawBooks]: - data = self.__GET(f"book/{symbol}/{precision}", params={ "len": len }) - - return [ - { - "t": precision == "R0" and serializers.TradingPairRawBook.parse or serializers.TradingPairBook.parse, - "f": precision == "R0" and serializers.FundingCurrencyRawBook.parse or serializers.FundingCurrencyBook.parse, - }[symbol[0]](*subdata) + def f_book(self, currency: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> FundingCurrencyBooks: + return [ serializers.FundingCurrencyBook.parse(*subdata) for subdata in self.__GET(f"book/{'f' + currency}/{precision}", params={ "len": len }) ] - for subdata in data - ] + def t_raw_book(self, pair: str, len: Optional[Literal[1, 25, 100]] = None) -> TradingPairRawBooks: + return [ serializers.TradingPairRawBook.parse(*subdata) for subdata in self.__GET(f"book/{'t' + pair}/R0", params={ "len": len }) ] - def stats( + def f_raw_book(self, currency: str, len: Optional[Literal[1, 25, 100]] = None) -> FundingCurrencyRawBooks: + return [ serializers.FundingCurrencyRawBook.parse(*subdata) for subdata in self.__GET(f"book/{'f' + currency}/R0", params={ "len": len }) ] + + def stats_hist( self, - resource: str, section: Literal["hist", "last"], - sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Union[Stat, Stats]: + resource: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Stats: params = { "sort": sort, "start": start, "end": end, "limit": limit } - - data = self.__GET(f"stats1/{resource}/{section}", params=params) - - if section == "last": - return serializers.Stat.parse(*data) + data = self.__GET(f"stats1/{resource}/hist", params=params) return [ serializers.Stat.parse(*subdata) for subdata in data ] - def candles( - self, - resource: str, section: Literal["hist", "last"], - sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Union[Candle, Candles]: + def stats_last( + self, + resource: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Stat: params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self.__GET(f"stats1/{resource}/last", params=params) + return serializers.Stat.parse(*data) - data = self.__GET(f"candles/{resource}/{section}", params=params) - - if section == "last": - return serializers.Candle.parse(*data) + def candles_hist( + self, + resource: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Candles: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self.__GET(f"candles/{resource}/hist", params=params) return [ serializers.Candle.parse(*subdata) for subdata in data ] - def derivatives_status(self, type: str, keys: List[str] = None) -> DerivativeStatuses: + def candles_last( + self, + resource: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Candle: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self.__GET(f"candles/{resource}/last", params=params) + return serializers.Candle.parse(*data) + + def derivatives_status(self, type: str, keys: List[str]) -> DerivativeStatuses: params = { "keys": ",".join(keys) } data = self.__GET(f"status/{type}", params=params) @@ -124,7 +139,7 @@ class BfxRestInterface(object): def derivatives_status_history( self, type: str, symbol: str, - sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> DerivativeStatuses: params = { "sort": sort, "start": start, "end": end, "limit": limit } @@ -132,26 +147,31 @@ class BfxRestInterface(object): return [ serializers.DerivativesStatus.parse(*subdata, skip=[ "KEY" ]) for subdata in data ] - def liquidations(self, sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> Liquidations: + def liquidations(self, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> Liquidations: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self.__GET("liquidations/hist", params=params) return [ serializers.Liquidation.parse(*subdata[0]) for subdata in data ] - def leaderboards( + def leaderboards_hist( self, - resource: str, section: Literal["hist", "last"], - sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Union[Leaderboard, Leaderboards]: + resource: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Leaderboards: params = { "sort": sort, "start": start, "end": end, "limit": limit } - - data = self.__GET(f"rankings/{resource}/{section}", params=params) - - if section == "last": - return serializers.Leaderboard.parse(*data) + data = self.__GET(f"rankings/{resource}/hist", params=params) return [ serializers.Leaderboard.parse(*subdata) for subdata in data ] + def leaderboards_last( + self, + resource: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Leaderboard: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self.__GET(f"rankings/{resource}/last", params=params) + return serializers.Leaderboard.parse(*data) + def funding_stats(self, symbol: str, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> FundingStats: params = { "start": start, "end": end, "limit": limit } @@ -159,5 +179,5 @@ class BfxRestInterface(object): return [ serializers.FundingStat.parse(*subdata) for subdata in data ] - def conf(self, config: Configs) -> Any: + def conf(self, config: Config) -> Any: return self.__GET(f"conf/{config}")[0] \ No newline at end of file diff --git a/bfxapi/rest/enums.py b/bfxapi/rest/enums.py index 3bb05d3..70c2336 100644 --- a/bfxapi/rest/enums.py +++ b/bfxapi/rest/enums.py @@ -1,6 +1,6 @@ from enum import Enum -class Configs(str, Enum): +class Config(str, Enum): MAP_CURRENCY_SYM = "pub:map:currency:sym" MAP_CURRENCY_LABEL = "pub:map:currency:label" MAP_CURRENCY_UNIT = "pub:map:currency:unit" @@ -22,4 +22,15 @@ class Configs(str, Enum): INFO_TX_STATUS = "pub:info:tx:status" SPEC_MARGIN = "pub:spec:margin", - FEES = "pub:fees" \ No newline at end of file + FEES = "pub:fees" + +class Precision(str, Enum): + P0 = "P0" + P1 = "P1" + P2 = "P2" + P3 = "P3" + P4 = "P4" + +class Sort(int, Enum): + ASCENDING = +1 + DESCENDING = -1 \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index a2f69df..8bcdf34 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -63,7 +63,7 @@ FundingCurrencyTicker = _Serializer[typings.FundingCurrencyTicker]("FundingCurre "FRR_AMOUNT_AVAILABLE" ]) -TickerHistory = _Serializer[typings.TickerHistory]("TickerHistory", labels=[ +TickersHistory = _Serializer[typings.TickersHistory]("TickersHistory", labels=[ "SYMBOL", "BID", "_PLACEHOLDER", diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index 828c228..4b225f4 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -38,14 +38,14 @@ FundingCurrencyTicker = TypedDict("FundingCurrencyTicker", { "FRR_AMOUNT_AVAILABLE": float }) -TickerHistory = TypedDict("TickerHistory", { +TickersHistory = TypedDict("TickersHistory", { "SYMBOL": str, "BID": float, "ASK": float, "MTS": int }) -TickerHistories = List[TickerHistory] +TickersHistories = List[TickersHistory] (TradingPairTrade, FundingCurrencyTrade) = ( TypedDict("TradingPairTrade", { "ID": int, "MTS": int, "AMOUNT": float, "PRICE": float }), From 07241b1ba850bec476842e710fd2363dd8730859 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 14 Dec 2022 18:17:29 +0100 Subject: [PATCH 14/38] Add _Requests and _RestPublicEndpoints classes in bfxapi/rest/BfxRestInterface.py. --- bfxapi/rest/BfxRestInterface.py | 51 ++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 018ce7b..30109c0 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -12,9 +12,13 @@ from .exceptions import RequestParametersError, ResourceNotFound class BfxRestInterface(object): def __init__(self, host): + self.public = _RestPublicEndpoints(host=host) + +class _Requests(object): + def __init__(self, host: str): self.host = host - def __GET(self, endpoint, params = None): + def _GET(self, endpoint, params = None): response = requests.get(f"{self.host}/{endpoint}", params=params) if response.status_code == HTTPStatus.NOT_FOUND: @@ -28,11 +32,12 @@ class BfxRestInterface(object): return data +class _RestPublicEndpoints(_Requests): def platform_status(self) -> PlatformStatus: - return serializers.PlatformStatus.parse(*self.__GET("platform/status")) + return serializers.PlatformStatus.parse(*self._GET("platform/status")) def tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]: - data = self.__GET("tickers", params={ "symbols": ",".join(symbols) }) + data = self._GET("tickers", params={ "symbols": ",".join(symbols) }) parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse } @@ -55,10 +60,10 @@ class BfxRestInterface(object): return cast(List[FundingCurrencyTicker], data) def t_ticker(self, pair: str) -> TradingPairTicker: - return serializers.TradingPairTicker.parse(*self.__GET(f"ticker/t{pair}"), skip=["SYMBOL"]) + return serializers.TradingPairTicker.parse(*self._GET(f"ticker/t{pair}"), skip=["SYMBOL"]) def f_ticker(self, currency: str) -> FundingCurrencyTicker: - return serializers.FundingCurrencyTicker.parse(*self.__GET(f"ticker/f{currency}"), skip=["SYMBOL"]) + return serializers.FundingCurrencyTicker.parse(*self._GET(f"ticker/f{currency}"), skip=["SYMBOL"]) def tickers_history(self, symbols: List[str], start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> TickersHistories: params = { @@ -67,31 +72,31 @@ class BfxRestInterface(object): "limit": limit } - data = self.__GET("tickers/hist", params=params) + data = self._GET("tickers/hist", params=params) return [ serializers.TickersHistory.parse(*subdata) for subdata in data ] def t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> TradingPairTrades: params = { "limit": limit, "start": start, "end": end, "sort": sort } - data = self.__GET(f"trades/{'t' + pair}/hist", params=params) + data = self._GET(f"trades/{'t' + pair}/hist", params=params) return [ serializers.TradingPairTrade.parse(*subdata) for subdata in data ] def f_trades(self, currency: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> FundingCurrencyTrades: params = { "limit": limit, "start": start, "end": end, "sort": sort } - data = self.__GET(f"trades/{'f' + currency}/hist", params=params) + data = self._GET(f"trades/{'f' + currency}/hist", params=params) return [ serializers.FundingCurrencyTrade.parse(*subdata) for subdata in data ] def t_book(self, pair: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> TradingPairBooks: - return [ serializers.TradingPairBook.parse(*subdata) for subdata in self.__GET(f"book/{'t' + pair}/{precision}", params={ "len": len }) ] + return [ serializers.TradingPairBook.parse(*subdata) for subdata in self._GET(f"book/{'t' + pair}/{precision}", params={ "len": len }) ] def f_book(self, currency: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> FundingCurrencyBooks: - return [ serializers.FundingCurrencyBook.parse(*subdata) for subdata in self.__GET(f"book/{'f' + currency}/{precision}", params={ "len": len }) ] + return [ serializers.FundingCurrencyBook.parse(*subdata) for subdata in self._GET(f"book/{'f' + currency}/{precision}", params={ "len": len }) ] def t_raw_book(self, pair: str, len: Optional[Literal[1, 25, 100]] = None) -> TradingPairRawBooks: - return [ serializers.TradingPairRawBook.parse(*subdata) for subdata in self.__GET(f"book/{'t' + pair}/R0", params={ "len": len }) ] + return [ serializers.TradingPairRawBook.parse(*subdata) for subdata in self._GET(f"book/{'t' + pair}/R0", params={ "len": len }) ] def f_raw_book(self, currency: str, len: Optional[Literal[1, 25, 100]] = None) -> FundingCurrencyRawBooks: - return [ serializers.FundingCurrencyRawBook.parse(*subdata) for subdata in self.__GET(f"book/{'f' + currency}/R0", params={ "len": len }) ] + return [ serializers.FundingCurrencyRawBook.parse(*subdata) for subdata in self._GET(f"book/{'f' + currency}/R0", params={ "len": len }) ] def stats_hist( self, @@ -99,7 +104,7 @@ class BfxRestInterface(object): sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> Stats: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self.__GET(f"stats1/{resource}/hist", params=params) + data = self._GET(f"stats1/{resource}/hist", params=params) return [ serializers.Stat.parse(*subdata) for subdata in data ] def stats_last( @@ -108,7 +113,7 @@ class BfxRestInterface(object): sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> Stat: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self.__GET(f"stats1/{resource}/last", params=params) + data = self._GET(f"stats1/{resource}/last", params=params) return serializers.Stat.parse(*data) def candles_hist( @@ -117,7 +122,7 @@ class BfxRestInterface(object): sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> Candles: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self.__GET(f"candles/{resource}/hist", params=params) + data = self._GET(f"candles/{resource}/hist", params=params) return [ serializers.Candle.parse(*subdata) for subdata in data ] def candles_last( @@ -126,13 +131,13 @@ class BfxRestInterface(object): 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/{resource}/last", params=params) return serializers.Candle.parse(*data) def derivatives_status(self, type: str, keys: List[str]) -> DerivativeStatuses: params = { "keys": ",".join(keys) } - data = self.__GET(f"status/{type}", params=params) + data = self._GET(f"status/{type}", params=params) return [ serializers.DerivativesStatus.parse(*subdata) for subdata in data ] @@ -143,14 +148,14 @@ class BfxRestInterface(object): ) -> DerivativeStatuses: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self.__GET(f"status/{type}/{symbol}/hist", params=params) + data = self._GET(f"status/{type}/{symbol}/hist", params=params) return [ serializers.DerivativesStatus.parse(*subdata, skip=[ "KEY" ]) for subdata in data ] def liquidations(self, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> Liquidations: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self.__GET("liquidations/hist", params=params) + data = self._GET("liquidations/hist", params=params) return [ serializers.Liquidation.parse(*subdata[0]) for subdata in data ] @@ -160,7 +165,7 @@ class BfxRestInterface(object): sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> Leaderboards: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self.__GET(f"rankings/{resource}/hist", params=params) + data = self._GET(f"rankings/{resource}/hist", params=params) return [ serializers.Leaderboard.parse(*subdata) for subdata in data ] def leaderboards_last( @@ -169,15 +174,15 @@ class BfxRestInterface(object): 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) + data = self._GET(f"rankings/{resource}/last", params=params) return serializers.Leaderboard.parse(*data) def funding_stats(self, symbol: str, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> FundingStats: params = { "start": start, "end": end, "limit": limit } - data = self.__GET(f"funding/stats/{symbol}/hist", params=params) + data = self._GET(f"funding/stats/{symbol}/hist", params=params) return [ serializers.FundingStat.parse(*subdata) for subdata in data ] def conf(self, config: Config) -> Any: - return self.__GET(f"conf/{config}")[0] \ No newline at end of file + return self._GET(f"conf/{config}")[0] \ No newline at end of file From 851184bf7577341246a15a49da0d792d181b40ab Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 14 Dec 2022 18:56:03 +0100 Subject: [PATCH 15/38] Add authentication logic to _Requests class in BfxRestInterface.py. Add _RestAuthenticatedEndpoints class. Add InvalidAuthenticationCredentials in bfxapi/rest/exceptions.py. --- bfxapi/rest/BfxRestInterface.py | 54 +++++++++++++++++++++++++++++---- bfxapi/rest/exceptions.py | 10 +++++- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 30109c0..b7a8893 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -1,4 +1,4 @@ -import requests +import time, hmac, hashlib, json, requests from http import HTTPStatus @@ -8,15 +8,32 @@ from . import serializers from .typings import * from .enums import Config, Precision, Sort -from .exceptions import RequestParametersError, ResourceNotFound +from .exceptions import RequestParametersError, ResourceNotFound, InvalidAuthenticationCredentials class BfxRestInterface(object): - def __init__(self, host): + def __init__(self, host, API_KEY = None, API_SECRET = None): self.public = _RestPublicEndpoints(host=host) + self.auth = _RestAuthenticatedEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET) + class _Requests(object): - def __init__(self, host: str): - self.host = host + def __init__(self, host, API_KEY = None, API_SECRET = None): + self.host, self.API_KEY, self.API_SECRET = host, API_KEY, API_SECRET + + def __build_authentication_headers(self, endpoint, data): + nonce = str(int(time.time()) * 1000) + + signature = hmac.new( + self.API_SECRET.encode("utf8"), + f"/api/v2/{endpoint}{nonce}{json.dumps(data)}".encode("utf8"), + hashlib.sha384 + ).hexdigest() + + return { + "bfx-nonce": nonce, + "bfx-signature": signature, + "bfx-apikey": self.API_KEY + } def _GET(self, endpoint, params = None): response = requests.get(f"{self.host}/{endpoint}", params=params) @@ -32,6 +49,28 @@ class _Requests(object): return data + def _POST(self, endpoint, params = None, data = None, _append_authentication_headers = True): + headers = { "Content-Type": "application/json" } + + if _append_authentication_headers: + headers = { **headers, **self.__build_authentication_headers(f"{endpoint}", data) } + + response = requests.post(f"{self.host}/{endpoint}", params=params, data=json.dumps(data), headers=headers) + + if response.status_code == HTTPStatus.NOT_FOUND: + raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") + + data = response.json() + + if len(data) and data[0] == "error": + if data[1] == 10020: + raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") + + if data[1] == 10100: + raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.") + + return data + class _RestPublicEndpoints(_Requests): def platform_status(self) -> PlatformStatus: return serializers.PlatformStatus.parse(*self._GET("platform/status")) @@ -185,4 +224,7 @@ class _RestPublicEndpoints(_Requests): return [ serializers.FundingStat.parse(*subdata) for subdata in data ] def conf(self, config: Config) -> Any: - return self._GET(f"conf/{config}")[0] \ No newline at end of file + return self._GET(f"conf/{config}")[0] + +class _RestAuthenticatedEndpoints(_Requests): + __PREFIX = "auth/" \ No newline at end of file diff --git a/bfxapi/rest/exceptions.py b/bfxapi/rest/exceptions.py index 8afc74e..973fb5a 100644 --- a/bfxapi/rest/exceptions.py +++ b/bfxapi/rest/exceptions.py @@ -1,6 +1,7 @@ __all__ = [ "RequestParametersError", - "ResourceNotFound" + "ResourceNotFound", + "InvalidAuthenticationCredentials" ] class BfxRestException(Exception): @@ -22,4 +23,11 @@ class ResourceNotFound(BfxRestException): This error indicates a failed HTTP request to a non-existent resource. """ + pass + +class InvalidAuthenticationCredentials(BfxRestException): + """ + This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication. + """ + pass \ No newline at end of file From c9f86d6d030419388710eb8c72c60cfd9f1eac16 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 15 Dec 2022 19:07:55 +0100 Subject: [PATCH 16/38] Add labeler.py to root package (bfxapi). Remove List aliases in bfxapi/rest/typings.py. Update BfxRestInterface.py to use new standards. --- bfxapi/labeler.py | 20 +++ bfxapi/rest/BfxRestInterface.py | 36 ++--- bfxapi/rest/serializers.py | 27 +--- bfxapi/rest/typings.py | 236 +++++++++++++++----------------- 4 files changed, 154 insertions(+), 165 deletions(-) create mode 100644 bfxapi/labeler.py diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py new file mode 100644 index 0000000..eb7076c --- /dev/null +++ b/bfxapi/labeler.py @@ -0,0 +1,20 @@ +from typing import Generic, TypeVar, Iterable, Optional, List, Any + +T = TypeVar("T") + +class _Serializer(Generic[T]): + def __init__(self, name: str, labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]): + self.name, self.__labels, self.__IGNORE = name, labels, IGNORE + + def __serialize(self, *args: Any, skip: Optional[List[str]]) -> Iterable[T]: + labels = list(filter(lambda label: label not in (skip or list()), self.__labels)) + + if len(labels) > len(args): + raise Exception(" and <*args> arguments should contain the same amount of elements.") + + for index, label in enumerate(labels): + if label not in self.__IGNORE: + yield label, args[index] + + def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: + return dict(self.__serialize(*values, skip=skip)) \ No newline at end of file diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index b7a8893..6644247 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -104,7 +104,7 @@ class _RestPublicEndpoints(_Requests): def f_ticker(self, currency: str) -> FundingCurrencyTicker: return serializers.FundingCurrencyTicker.parse(*self._GET(f"ticker/f{currency}"), skip=["SYMBOL"]) - def tickers_history(self, symbols: List[str], start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> TickersHistories: + def tickers_history(self, symbols: List[str], start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[TickersHistory]: params = { "symbols": ",".join(symbols), "start": start, "end": end, @@ -115,51 +115,51 @@ class _RestPublicEndpoints(_Requests): return [ serializers.TickersHistory.parse(*subdata) for subdata in data ] - def t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> TradingPairTrades: + def t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[TradingPairTrade]: params = { "limit": limit, "start": start, "end": end, "sort": sort } data = self._GET(f"trades/{'t' + pair}/hist", params=params) return [ serializers.TradingPairTrade.parse(*subdata) for subdata in data ] - def f_trades(self, currency: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> FundingCurrencyTrades: + def f_trades(self, currency: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[FundingCurrencyTrade]: params = { "limit": limit, "start": start, "end": end, "sort": sort } data = self._GET(f"trades/{'f' + currency}/hist", params=params) return [ serializers.FundingCurrencyTrade.parse(*subdata) for subdata in data ] - def t_book(self, pair: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> TradingPairBooks: + def t_book(self, pair: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairBook]: return [ serializers.TradingPairBook.parse(*subdata) for subdata in self._GET(f"book/{'t' + pair}/{precision}", params={ "len": len }) ] - def f_book(self, currency: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> FundingCurrencyBooks: + def f_book(self, currency: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyBook]: return [ serializers.FundingCurrencyBook.parse(*subdata) for subdata in self._GET(f"book/{'f' + currency}/{precision}", params={ "len": len }) ] - def t_raw_book(self, pair: str, len: Optional[Literal[1, 25, 100]] = None) -> TradingPairRawBooks: + def t_raw_book(self, pair: str, len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairRawBook]: return [ serializers.TradingPairRawBook.parse(*subdata) for subdata in self._GET(f"book/{'t' + pair}/R0", params={ "len": len }) ] - def f_raw_book(self, currency: str, len: Optional[Literal[1, 25, 100]] = None) -> FundingCurrencyRawBooks: + def f_raw_book(self, currency: str, len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyRawBook]: return [ serializers.FundingCurrencyRawBook.parse(*subdata) for subdata in self._GET(f"book/{'f' + currency}/R0", params={ "len": len }) ] def stats_hist( self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Stats: + ) -> List[Statistic]: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self._GET(f"stats1/{resource}/hist", params=params) - return [ serializers.Stat.parse(*subdata) for subdata in data ] + return [ serializers.Statistic.parse(*subdata) for subdata in data ] def stats_last( self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Stat: + ) -> Statistic: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self._GET(f"stats1/{resource}/last", params=params) - return serializers.Stat.parse(*data) + return serializers.Statistic.parse(*data) def candles_hist( self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Candles: + ) -> List[Candle]: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self._GET(f"candles/{resource}/hist", params=params) return [ serializers.Candle.parse(*subdata) for subdata in data ] @@ -173,7 +173,7 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"candles/{resource}/last", params=params) return serializers.Candle.parse(*data) - def derivatives_status(self, type: str, keys: List[str]) -> DerivativeStatuses: + def derivatives_status(self, type: str, keys: List[str]) -> List[DerivativesStatus]: params = { "keys": ",".join(keys) } data = self._GET(f"status/{type}", params=params) @@ -184,14 +184,14 @@ class _RestPublicEndpoints(_Requests): self, type: str, symbol: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> DerivativeStatuses: + ) -> List[DerivativesStatus]: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self._GET(f"status/{type}/{symbol}/hist", params=params) return [ serializers.DerivativesStatus.parse(*subdata, skip=[ "KEY" ]) for subdata in data ] - def liquidations(self, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> Liquidations: + def liquidations(self, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Liquidation]: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self._GET("liquidations/hist", params=params) @@ -202,7 +202,7 @@ class _RestPublicEndpoints(_Requests): self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Leaderboards: + ) -> 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 ] @@ -216,12 +216,12 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"rankings/{resource}/last", params=params) return serializers.Leaderboard.parse(*data) - def funding_stats(self, symbol: str, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> FundingStats: + def funding_stats(self, symbol: str, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingStatistic]: params = { "start": start, "end": end, "limit": limit } data = self._GET(f"funding/stats/{symbol}/hist", params=params) - return [ serializers.FundingStat.parse(*subdata) for subdata in data ] + return [ serializers.FundingStatistic.parse(*subdata) for subdata in data ] def conf(self, config: Config) -> Any: return self._GET(f"conf/{config}")[0] diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 8bcdf34..95f4c26 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -1,27 +1,6 @@ -from typing import Generic, TypeVar, Iterable, Optional, List, Any - from . import typings -from .exceptions import BfxRestException - -T = TypeVar("T") - -class _Serializer(Generic[T]): - def __init__(self, name: str, labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]): - self.name, self.__labels, self.__IGNORE = name, labels, IGNORE - - def __serialize(self, *args: Any, skip: Optional[List[str]]) -> Iterable[T]: - labels = list(filter(lambda label: label not in (skip or list()), self.__labels)) - - if len(labels) > len(args): - raise BfxRestException(" and <*args> arguments should contain the same amount of elements.") - - for index, label in enumerate(labels): - if label not in self.__IGNORE: - yield label, args[index] - - def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: - return dict(self.__serialize(*values, skip=skip)) +from .. labeler import _Serializer #region Serializers definition for Rest Public Endpoints @@ -120,7 +99,7 @@ FundingCurrencyRawBook = _Serializer[typings.FundingCurrencyRawBook]("FundingCur "AMOUNT" ]) -Stat = _Serializer[typings.Stat]("Stat", labels=[ +Statistic = _Serializer[typings.Statistic]("Statistic", labels=[ "MTS", "VALUE" ]) @@ -189,7 +168,7 @@ Leaderboard = _Serializer[typings.Leaderboard]("Leaderboard", labels=[ "TWITTER_HANDLE" ]) -FundingStat = _Serializer[typings.FundingStat]("FundingStat", labels=[ +FundingStatistic = _Serializer[typings.FundingStatistic]("FundingStatistic", labels=[ "TIMESTAMP", "_PLACEHOLDER", "_PLACEHOLDER", diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index 4b225f4..7af75fb 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -2,140 +2,130 @@ from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any #region Type hinting for Rest Public Endpoints -PlatformStatus = TypedDict("PlatformStatus", { - "OPERATIVE": int -}) +class PlatformStatus(TypedDict): + OPERATIVE: int -TradingPairTicker = TypedDict("TradingPairTicker", { - "SYMBOL": Optional[str], - "BID": float, - "BID_SIZE": float, - "ASK": float, - "ASK_SIZE": float, - "DAILY_CHANGE": float, - "DAILY_CHANGE_RELATIVE": float, - "LAST_PRICE": float, - "VOLUME": float, - "HIGH": float, - "LOW": float -}) +class TradingPairTicker(TypedDict): + SYMBOL: Optional[str] + BID: float + BID_SIZE: float + ASK: float + ASK_SIZE: float + DAILY_CHANGE: float + DAILY_CHANGE_RELATIVE: float + LAST_PRICE: float + VOLUME: float + HIGH: float + LOW: float -FundingCurrencyTicker = TypedDict("FundingCurrencyTicker", { - "SYMBOL": Optional[str], - "FRR": float, - "BID": float, - "BID_PERIOD": int, - "BID_SIZE": float, - "ASK": float, - "ASK_PERIOD": int, - "ASK_SIZE": float, - "DAILY_CHANGE": float, - "DAILY_CHANGE_RELATIVE": float, - "LAST_PRICE": float, - "VOLUME": float, - "HIGH": float, - "LOW": float, - "FRR_AMOUNT_AVAILABLE": float -}) +class FundingCurrencyTicker(TypedDict): + SYMBOL: Optional[str] + FRR: float + BID: float + BID_PERIOD: int + BID_SIZE: float + ASK: float + ASK_PERIOD: int + ASK_SIZE: float + DAILY_CHANGE: float + DAILY_CHANGE_RELATIVE: float + LAST_PRICE: float + VOLUME: float + HIGH: float + LOW: float + FRR_AMOUNT_AVAILABLE: float -TickersHistory = TypedDict("TickersHistory", { - "SYMBOL": str, - "BID": float, - "ASK": float, - "MTS": int -}) +class TickersHistory(TypedDict): + SYMBOL: str + BID: float + ASK: float + MTS: int -TickersHistories = List[TickersHistory] +class TradingPairTrade(TypedDict): + ID: int + MTS: int + AMOUNT: float + PRICE: float -(TradingPairTrade, FundingCurrencyTrade) = ( - TypedDict("TradingPairTrade", { "ID": int, "MTS": int, "AMOUNT": float, "PRICE": float }), - TypedDict("FundingCurrencyTrade", { "ID": int, "MTS": int, "AMOUNT": float, "RATE": float, "PERIOD": int }) -) +class FundingCurrencyTrade(TypedDict): + ID: int + MTS: int + AMOUNT: float + RATE: float + PERIOD: int -(TradingPairTrades, FundingCurrencyTrades) = (List[TradingPairTrade], List[FundingCurrencyTrade]) +class TradingPairBook(TypedDict): + PRICE: float + COUNT: int + AMOUNT: float + +class FundingCurrencyBook(TypedDict): + RATE: float + PERIOD: int + COUNT: int + AMOUNT: float + +class TradingPairRawBook(TypedDict): + ORDER_ID: int + PRICE: float + AMOUNT: float + +class FundingCurrencyRawBook(TypedDict): + OFFER_ID: int + PERIOD: int + RATE: float + AMOUNT: float -(TradingPairBook, FundingCurrencyBook) = ( - TypedDict("TradingPairBook", { "PRICE": float, "COUNT": int, "AMOUNT": float }), - TypedDict("FundingCurrencyBook", { "RATE": float, "PERIOD": int, "COUNT": int, "AMOUNT": float }) -) +class Statistic(TypedDict): + MTS: int + VALUE: float -(TradingPairBooks, FundingCurrencyBooks) = (List[TradingPairBook], List[FundingCurrencyBook]) +class Candle(TypedDict): + MTS: int + OPEN: float + CLOSE: float + HIGH: float + LOW: float + VOLUME: float -(TradingPairRawBook, FundingCurrencyRawBook) = ( - TypedDict("TradingPairRawBook", { "ORDER_ID": int, "PRICE": float, "AMOUNT": float }), - TypedDict("FundingCurrencyRawBook", { "OFFER_ID": int, "PERIOD": int, "RATE": float, "AMOUNT": float }), -) +class DerivativesStatus(TypedDict): + KEY: Optional[str] + MTS: int + DERIV_PRICE: float + SPOT_PRICE: float + INSURANCE_FUND_BALANCE: float + NEXT_FUNDING_EVT_TIMESTAMP_MS: int + NEXT_FUNDING_ACCRUED: float + NEXT_FUNDING_STEP: int + CURRENT_FUNDING: float + MARK_PRICE: float + OPEN_INTEREST: float + CLAMP_MIN: float + CLAMP_MAX: float -(TradingPairRawBooks, FundingCurrencyRawBooks) = (List[TradingPairRawBook], List[FundingCurrencyRawBook]) +class Liquidation(TypedDict): + POS_ID: int + MTS: int + SYMBOL: str + AMOUNT: float + BASE_PRICE: float + IS_MATCH: int + IS_MARKET_SOLD: int + PRICE_ACQUIRED: float -Stat = TypedDict("Stat", { - "MTS": int, - "VALUE": float -}) +class Leaderboard(TypedDict): + MTS: int + USERNAME: str + RANKING: int + VALUE: float + TWITTER_HANDLE: Optional[str] -Stats = List[Stat] - -Candle = TypedDict("Candle", { - "MTS": int, - "OPEN": float, - "CLOSE": float, - "HIGH": float, - "LOW": float, - "VOLUME": float -}) - -Candles = List[Candle] - -DerivativesStatus = TypedDict("DerivativesStatus", { - "KEY": Optional[str], - "MTS": int, - "DERIV_PRICE": float, - "SPOT_PRICE": float, - "INSURANCE_FUND_BALANCE": float, - "NEXT_FUNDING_EVT_TIMESTAMP_MS": int, - "NEXT_FUNDING_ACCRUED": float, - "NEXT_FUNDING_STEP": int, - "CURRENT_FUNDING": float, - "MARK_PRICE": float, - "OPEN_INTEREST": float, - "CLAMP_MIN": float, - "CLAMP_MAX": float -}) - -DerivativeStatuses = List[DerivativesStatus] - -Liquidation = TypedDict("Liquidation", { - "POS_ID": int, - "MTS": int, - "SYMBOL": str, - "AMOUNT": float, - "BASE_PRICE": float, - "IS_MATCH": int, - "IS_MARKET_SOLD": int, - "PRICE_ACQUIRED": float -}) - -Liquidations = List[Liquidation] - -Leaderboard = TypedDict("Leaderboard", { - "MTS": int, - "USERNAME": str, - "RANKING": int, - "VALUE": float, - "TWITTER_HANDLE": Optional[str] -}) - -Leaderboards = List[Leaderboard] - -FundingStat = TypedDict("FundingStat", { - "TIMESTAMP": int, - "FRR": float, - "AVG_PERIOD": float, - "FUNDING_AMOUNT": float, - "FUNDING_AMOUNT_USED": float, - "FUNDING_BELOW_THRESHOLD": float -}) - -FundingStats = List[FundingStat] +class FundingStatistic(TypedDict): + TIMESTAMP: int + FRR: float + AVG_PERIOD: float + FUNDING_AMOUNT: float + FUNDING_AMOUNT_USED: float + FUNDING_BELOW_THRESHOLD: float #endregion \ No newline at end of file From 24b105378ac8f5803133c10854d4f7453bba5219 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 15 Dec 2022 19:14:00 +0100 Subject: [PATCH 17/38] Add hierarchy logic to custom exceptions. --- bfxapi/exceptions.py | 18 ++++++++++++++++++ bfxapi/labeler.py | 4 +++- bfxapi/rest/exceptions.py | 8 ++++++-- 3 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 bfxapi/exceptions.py diff --git a/bfxapi/exceptions.py b/bfxapi/exceptions.py new file mode 100644 index 0000000..8e9b6e5 --- /dev/null +++ b/bfxapi/exceptions.py @@ -0,0 +1,18 @@ +__all__ = [ + "BfxBaseException", + "LabelerSerializerException" +] + +class BfxBaseException(Exception): + """ + Base class for every custom exception in bfxapi/rest/exceptions.py and bfxapi/websocket/exceptions.py. + """ + + pass + +class LabelerSerializerException(BfxBaseException): + """ + This exception indicates an error thrown by the _Serializer class in bfxapi/labeler.py. + """ + + pass \ No newline at end of file diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index eb7076c..d358ffd 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -1,3 +1,5 @@ +from .exceptions import LabelerSerializerException + from typing import Generic, TypeVar, Iterable, Optional, List, Any T = TypeVar("T") @@ -10,7 +12,7 @@ class _Serializer(Generic[T]): labels = list(filter(lambda label: label not in (skip or list()), self.__labels)) if len(labels) > len(args): - raise Exception(" and <*args> arguments should contain the same amount of elements.") + raise LabelerSerializerException(" 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/rest/exceptions.py b/bfxapi/rest/exceptions.py index 973fb5a..0fc6de8 100644 --- a/bfxapi/rest/exceptions.py +++ b/bfxapi/rest/exceptions.py @@ -1,12 +1,16 @@ +from ..exceptions import BfxBaseException + __all__ = [ + "BfxRestException", + "RequestParametersError", "ResourceNotFound", "InvalidAuthenticationCredentials" ] -class BfxRestException(Exception): +class BfxRestException(BfxBaseException): """ - Base class for all exceptions defined in bfxapi/rest/exceptions.py. + Base class for all custom exceptions in bfxapi/rest/exceptions.py. """ pass From 2595b8a7609ff5178670a4e9713fe35f3b545897 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 15 Dec 2022 19:21:19 +0100 Subject: [PATCH 18/38] Fix mypy errors and warnings in bfxapi/labeler.py script. --- bfxapi/labeler.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index d358ffd..bcf18c3 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -1,6 +1,6 @@ from .exceptions import LabelerSerializerException -from typing import Generic, TypeVar, Iterable, Optional, List, Any +from typing import Generic, TypeVar, Iterable, Optional, List, Tuple, Any, cast T = TypeVar("T") @@ -8,7 +8,7 @@ class _Serializer(Generic[T]): def __init__(self, name: str, labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]): self.name, self.__labels, self.__IGNORE = name, labels, IGNORE - def __serialize(self, *args: Any, skip: Optional[List[str]]) -> Iterable[T]: + def __serialize(self, *args: Any, skip: Optional[List[str]]) -> Iterable[Tuple[str, Any]]: labels = list(filter(lambda label: label not in (skip or list()), self.__labels)) if len(labels) > len(args): @@ -19,4 +19,4 @@ class _Serializer(Generic[T]): yield label, args[index] def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: - return dict(self.__serialize(*values, skip=skip)) \ No newline at end of file + return cast(T, dict(self.__serialize(*values, skip=skip))) \ No newline at end of file From 0e4cbd40a64c7f5ce8ab85466d4fcbc8d8ed824e Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 16 Dec 2022 16:03:28 +0100 Subject: [PATCH 19/38] Fix other mypy errors and warnings. --- bfxapi/client.py | 4 +++- bfxapi/rest/BfxRestInterface.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/bfxapi/client.py b/bfxapi/client.py index dd2fe93..75c3f2a 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -1,5 +1,7 @@ from .websocket import BfxWebsocketClient +from typing import Optional + from enum import Enum class Constants(str, Enum): @@ -10,7 +12,7 @@ class Constants(str, Enum): PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2" class Client(object): - def __init__(self, WSS_HOST: str = Constants.WSS_HOST, API_KEY: str = None, API_SECRET: str = None, log_level: str = "WARNING"): + def __init__(self, WSS_HOST: str = Constants.WSS_HOST, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None, log_level: str = "WARNING"): self.wss = BfxWebsocketClient( host=WSS_HOST, API_KEY=API_KEY, diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 6644247..37f52d3 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -84,7 +84,7 @@ class _RestPublicEndpoints(_Requests): def t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]: if isinstance(pairs, str) and pairs == "ALL": - return [ subdata for subdata in self.tickers([ "ALL" ]) if subdata["SYMBOL"].startswith("t") ] + return [ cast(TradingPairTicker, subdata) for subdata in self.tickers([ "ALL" ]) if cast(str, subdata["SYMBOL"]).startswith("t") ] data = self.tickers([ "t" + pair for pair in pairs ]) @@ -92,7 +92,7 @@ class _RestPublicEndpoints(_Requests): def f_tickers(self, currencies: Union[List[str], Literal["ALL"]]) -> List[FundingCurrencyTicker]: if isinstance(currencies, str) and currencies == "ALL": - return [ subdata for subdata in self.tickers([ "ALL" ]) if subdata["SYMBOL"].startswith("f") ] + return [ cast(FundingCurrencyTicker, subdata) for subdata in self.tickers([ "ALL" ]) if cast(str, subdata["SYMBOL"]).startswith("f") ] data = self.tickers([ "f" + currency for currency in currencies ]) From 0a53ab7f7e8fc16d5e62451c4e329d895d8f510b Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 16 Dec 2022 18:30:41 +0100 Subject: [PATCH 20/38] Apply lots of refactoring to the websocket subpackage (fix every mypy error and warning). Add integers.py and decimal.py to bfxapi.utils package. Update requirements.txt and setup.py with new mypy dependencies. --- bfxapi/exceptions.py | 19 +- bfxapi/rest/exceptions.py | 2 +- bfxapi/utils/decimal.py | 9 + bfxapi/utils/integers.py | 35 ++ bfxapi/websocket/BfxWebsocketClient.py | 36 +- bfxapi/websocket/exceptions.py | 22 +- bfxapi/websocket/serializers.py | 23 +- bfxapi/websocket/typings.py | 588 ++++++++++++------------- requirements.txt | Bin 304 -> 512 bytes setup.py | 7 +- 10 files changed, 393 insertions(+), 348 deletions(-) create mode 100644 bfxapi/utils/decimal.py create mode 100644 bfxapi/utils/integers.py diff --git a/bfxapi/exceptions.py b/bfxapi/exceptions.py index 8e9b6e5..1033837 100644 --- a/bfxapi/exceptions.py +++ b/bfxapi/exceptions.py @@ -1,6 +1,9 @@ __all__ = [ "BfxBaseException", - "LabelerSerializerException" + + "LabelerSerializerException", + "IntegerUnderflowError", + "IntegerOverflowflowError" ] class BfxBaseException(Exception): @@ -15,4 +18,18 @@ 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/exceptions.py b/bfxapi/rest/exceptions.py index 0fc6de8..81bcb8f 100644 --- a/bfxapi/rest/exceptions.py +++ b/bfxapi/rest/exceptions.py @@ -1,4 +1,4 @@ -from ..exceptions import BfxBaseException +from .. exceptions import BfxBaseException __all__ = [ "BfxRestException", diff --git a/bfxapi/utils/decimal.py b/bfxapi/utils/decimal.py new file mode 100644 index 0000000..5a7af71 --- /dev/null +++ b/bfxapi/utils/decimal.py @@ -0,0 +1,9 @@ +import json + +from decimal import Decimal + +class DecimalEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, Decimal): + return str(obj) + return json.JSONEncoder.default(self, obj) \ No newline at end of file diff --git a/bfxapi/utils/integers.py b/bfxapi/utils/integers.py new file mode 100644 index 0000000..e38f107 --- /dev/null +++ b/bfxapi/utils/integers.py @@ -0,0 +1,35 @@ +from typing import cast, TypeVar + +from .. exceptions import IntegerUnderflowError, IntegerOverflowflowError + +__all__ = [ "Int16", "Int32", "Int45", "Int64" ] + +T = TypeVar("T") + +class _Int(int): + def __new__(cls: T, integer: int) -> T: + assert hasattr(cls, "_BITS"), "_Int must be extended by a class that has a static member _BITS (indicating the number of bits with which to represent the integers)." + + bits = cls._BITS - 1 + + min, max = -(2 ** bits), (2 ** bits) - 1 + + if integer < min: + raise IntegerUnderflowError(f"Underflow. Cannot store <{integer}> in {cls._BITS} bits integer. The min and max bounds are {min} and {max}.") + + if integer > max: + raise IntegerOverflowflowError(f"Overflow. Cannot store <{integer}> in {cls._BITS} bits integer. The min and max bounds are {min} and {max}.") + + return cast(T, super().__new__(int, integer)) + +class Int16(_Int): + _BITS = 16 + +class Int32(_Int): + _BITS = 32 + +class Int45(_Int): + _BITS = 45 + +class Int64(_Int): + _BITS = 64 \ No newline at end of file diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index e8bb4d0..775fa10 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -1,25 +1,40 @@ import traceback, json, asyncio, hmac, hashlib, time, uuid, websockets +from typing import Tuple, Union, Literal, TypeVar, Callable, cast + from enum import Enum from pyee.asyncio import AsyncIOEventEmitter -from .typings import Inputs, Tuple, Union +from .typings import Inputs from .handlers import Channels, PublicChannelsHandler, AuthenticatedChannelsHandler from .exceptions import ConnectionNotOpen, TooManySubscriptions, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion +from ..utils.decimal import DecimalEncoder + from ..utils.logger import Formatter, CustomLogger _HEARTBEAT = "hb" -def _require_websocket_connection(function): +F = TypeVar("F", bound=Callable[..., Literal[None]]) + +def _require_websocket_connection(function: F) -> F: async def wrapper(self, *args, **kwargs): if self.websocket == None or self.websocket.open == False: raise ConnectionNotOpen("No open connection with the server.") await function(self, *args, **kwargs) - return wrapper + return cast(F, wrapper) + +def _require_websocket_authentication(function: F) -> F: + async def wrapper(self, *args, **kwargs): + if self.authentication == False: + raise WebsocketAuthenticationRequired("To perform this action you need to authenticate using your API_KEY and API_SECRET.") + + await _require_websocket_connection(function)(self, *args, **kwargs) + + return cast(F, wrapper) class BfxWebsocketClient(object): VERSION = 2 @@ -118,22 +133,13 @@ class BfxWebsocketClient(object): for bucket in self.buckets: await bucket._close(code=code, reason=reason) - def __require_websocket_authentication(function): - async def wrapper(self, *args, **kwargs): - if self.authentication == False: - raise WebsocketAuthenticationRequired("To perform this action you need to authenticate using your API_KEY and API_SECRET.") - - await _require_websocket_connection(function)(self, *args, **kwargs) - - return wrapper - - @__require_websocket_authentication + @_require_websocket_authentication async def notify(self, info, MESSAGE_ID=None, **kwargs): await self.websocket.send(json.dumps([ 0, "n", MESSAGE_ID, { "type": "ucm-test", "info": info, **kwargs } ])) - @__require_websocket_authentication + @_require_websocket_authentication async def __handle_websocket_input(self, input, data): - await self.websocket.send(json.dumps([ 0, input, None, data])) + await self.websocket.send(json.dumps([ 0, input, None, data], cls=DecimalEncoder)) def __bucket_open_signal(self, index): if all(bucket.websocket != None and bucket.websocket.open == True for bucket in self.buckets): diff --git a/bfxapi/websocket/exceptions.py b/bfxapi/websocket/exceptions.py index c55b767..5691af8 100644 --- a/bfxapi/websocket/exceptions.py +++ b/bfxapi/websocket/exceptions.py @@ -1,4 +1,8 @@ +from .. exceptions import BfxBaseException + __all__ = [ + "BfxWebsocketException", + "ConnectionNotOpen", "TooManySubscriptions", "WebsocketAuthenticationRequired", @@ -7,9 +11,9 @@ __all__ = [ "OutdatedClientVersion" ] -class BfxWebsocketException(Exception): +class BfxWebsocketException(BfxBaseException): """ - Base class for all exceptions defined in bfxapi/websocket/exceptions.py. + Base class for all custom exceptions in bfxapi/websocket/exceptions.py. """ pass @@ -35,13 +39,6 @@ class WebsocketAuthenticationRequired(BfxWebsocketException): pass -class InvalidAuthenticationCredentials(BfxWebsocketException): - """ - This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication. - """ - - pass - class EventNotSupported(BfxWebsocketException): """ This error indicates a failed attempt to subscribe to an event not supported by the BfxWebsocketClient. @@ -54,4 +51,11 @@ class OutdatedClientVersion(BfxWebsocketException): This error indicates a mismatch between the client version and the server WSS version. """ + pass + +class InvalidAuthenticationCredentials(BfxWebsocketException): + """ + This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication. + """ + pass \ No newline at end of file diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index 64573cc..00f43d2 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -1,25 +1,6 @@ -from typing import Generic, TypeVar, Iterable, List, Any - from . import typings -from .exceptions import BfxWebsocketException - -T = TypeVar("T") - -class _Serializer(Generic[T]): - def __init__(self, name: str, labels: List[str]): - self.name, self.__labels = name, labels - - def __serialize(self, *args: Any, IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> Iterable[T]: - if len(self.__labels) != len(args): - raise BfxWebsocketException(" and <*args> arguments should contain the same amount of elements.") - - for index, label in enumerate(self.__labels): - if label not in IGNORE: - yield label, args[index] - - def parse(self, *values: Any) -> T: - return dict(self.__serialize(*values)) +from .. labeler import _Serializer #region Serializers definition for Websocket Public Channels @@ -315,7 +296,7 @@ BalanceInfo = _Serializer[typings.BalanceInfo]("BalanceInfo", labels=[ #region Serializers definition for Notifications channel -Notification = _Serializer("Notification", labels=[ +Notification = _Serializer[typings.Notification]("Notification", labels=[ "MTS", "TYPE", "MESSAGE_ID", diff --git a/bfxapi/websocket/typings.py b/bfxapi/websocket/typings.py index 4a3b918..9966d99 100644 --- a/bfxapi/websocket/typings.py +++ b/bfxapi/websocket/typings.py @@ -2,301 +2,294 @@ from decimal import Decimal from datetime import datetime -from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any +from typing import Type, NewType, Tuple, List, Dict, TypedDict, Union, Optional, Any -int16 = int32 = int45 = int64 = int +from ..utils.integers import Int16, Int32, Int45, Int64 JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] #region Type hinting for subscription objects class Subscriptions: - TradingPairsTicker = TypedDict("Subscriptions.TradingPairsTicker", { - "chanId": int, - "symbol": str, - "pair": str - }) + class TradingPairsTicker(TypedDict): + chanId: int + symbol: str + pair: str - FundingCurrenciesTicker = TypedDict("Subscriptions.FundingCurrenciesTicker", { - "chanId": int, - "symbol": str, - "currency": str - }) + class FundingCurrenciesTicker(TypedDict): + chanId: int + symbol: str + currency: str - TradingPairsTrades = TypedDict("Subscriptions.TradingPairsTrades", { - "chanId": int, - "symbol": str, - "pair": str - }) + class TradingPairsTrades(TypedDict): + chanId: int + symbol: str + pair: str - FundingCurrenciesTrades = TypedDict("Subscriptions.FundingCurrenciesTrades", { - "chanId": int, - "symbol": str, - "currency": str - }) + class FundingCurrenciesTrades(TypedDict): + chanId: int + symbol: str + currency: str - Book = TypedDict("Subscriptions.Book", { - "chanId": int, - "symbol": str, - "prec": str, - "freq": str, - "len": str, - "subId": int, - "pair": str - }) + class Book(TypedDict): + chanId: int + symbol: str + prec: str + freq: str + len: str + subId: int + pair: str - Candles = TypedDict("Subscriptions.Candles", { - "chanId": int, - "key": str - }) + class Candles(TypedDict): + chanId: int + key: str - DerivativesStatus = TypedDict("Subscriptions.DerivativesStatus", { - "chanId": int, - "key": str - }) + class DerivativesStatus(TypedDict): + chanId: int + key: str #endregion #region Type hinting for Websocket Public Channels -TradingPairTicker = TypedDict("TradingPairTicker", { - "BID": float, - "BID_SIZE": float, - "ASK": float, - "ASK_SIZE": float, - "DAILY_CHANGE": float, - "DAILY_CHANGE_RELATIVE": float, - "LAST_PRICE": float, - "VOLUME": float, - "HIGH": float, - "LOW": float -}) +class TradingPairTicker(TypedDict): + BID: float + BID_SIZE: float + ASK: float + ASK_SIZE: float + DAILY_CHANGE: float + DAILY_CHANGE_RELATIVE: float + LAST_PRICE: float + VOLUME: float + HIGH: float + LOW: float -FundingCurrencyTicker = TypedDict("FundingCurrencyTicker", { - "FRR": float, - "BID": float, - "BID_PERIOD": int, - "BID_SIZE": float, - "ASK": float, - "ASK_PERIOD": int, - "ASK_SIZE": float, - "DAILY_CHANGE": float, - "DAILY_CHANGE_RELATIVE": float, - "LAST_PRICE": float, - "VOLUME": float, - "HIGH": float, - "LOW": float, - "FRR_AMOUNT_AVAILABLE": float -}) +class FundingCurrencyTicker(TypedDict): + FRR: float + BID: float + BID_PERIOD: int + BID_SIZE: float + ASK: float + ASK_PERIOD: int + ASK_SIZE: float + DAILY_CHANGE: float + DAILY_CHANGE_RELATIVE: float + LAST_PRICE: float + VOLUME: float + HIGH: float + LOW: float + FRR_AMOUNT_AVAILABLE: float -(TradingPairTrade, FundingCurrencyTrade) = ( - TypedDict("TradingPairTrade", { "ID": int, "MTS": int, "AMOUNT": float, "PRICE": float }), - TypedDict("FundingCurrencyTrade", { "ID": int, "MTS": int, "AMOUNT": float, "RATE": float, "PERIOD": int }) -) +class TradingPairTrade(TypedDict): + ID: int + MTS: int + AMOUNT: float + PRICE: float -(TradingPairTrades, FundingCurrencyTrades) = (List[TradingPairTrade], List[FundingCurrencyTrade]) +class FundingCurrencyTrade(TypedDict): + ID: int + MTS: int + AMOUNT: float + RATE: float + PERIOD: int -(TradingPairBook, FundingCurrencyBook) = ( - TypedDict("TradingPairBook", { "PRICE": float, "COUNT": int, "AMOUNT": float }), - TypedDict("FundingCurrencyBook", { "RATE": float, "PERIOD": int, "COUNT": int, "AMOUNT": float }) -) +class TradingPairBook(TypedDict): + PRICE: float + COUNT: int + AMOUNT: float + +class FundingCurrencyBook(TypedDict): + RATE: float + PERIOD: int + COUNT: int + AMOUNT: float + +class TradingPairRawBook(TypedDict): + ORDER_ID: int + PRICE: float + AMOUNT: float + +class FundingCurrencyRawBook(TypedDict): + OFFER_ID: int + PERIOD: int + RATE: float + AMOUNT: float -(TradingPairBooks, FundingCurrencyBooks) = (List[TradingPairBook], List[FundingCurrencyBook]) +class Candle(TypedDict): + MTS: int + OPEN: float + CLOSE: float + HIGH: float + LOW: float + VOLUME: float -(TradingPairRawBook, FundingCurrencyRawBook) = ( - TypedDict("TradingPairRawBook", { "ORDER_ID": int, "PRICE": float, "AMOUNT": float }), - TypedDict("FundingCurrencyRawBook", { "OFFER_ID": int, "PERIOD": int, "RATE": float, "AMOUNT": float }), -) - -(TradingPairRawBooks, FundingCurrencyRawBooks) = (List[TradingPairRawBook], List[FundingCurrencyRawBook]) - -Candle = TypedDict("Candle", { - "MTS": int, - "OPEN": float, - "CLOSE": float, - "HIGH": float, - "LOW": float, - "VOLUME": float -}) - -Candles = List[Candle] - -DerivativesStatus = TypedDict("DerivativesStatus", { - "TIME_MS": int, - "DERIV_PRICE": float, - "SPOT_PRICE": float, - "INSURANCE_FUND_BALANCE": float, - "NEXT_FUNDING_EVT_TIMESTAMP_MS": int, - "NEXT_FUNDING_ACCRUED": float, - "NEXT_FUNDING_STEP": int, - "CURRENT_FUNDING": float, - "MARK_PRICE": float, - "OPEN_INTEREST": float, - "CLAMP_MIN": float, - "CLAMP_MAX": float -}) +class DerivativesStatus(TypedDict): + TIME_MS: int + DERIV_PRICE: float + SPOT_PRICE: float + INSURANCE_FUND_BALANCE: float + NEXT_FUNDING_EVT_TIMESTAMP_MS: int + NEXT_FUNDING_ACCRUED: float + NEXT_FUNDING_STEP: int + CURRENT_FUNDING: float + MARK_PRICE: float + OPEN_INTEREST: float + CLAMP_MIN: float + CLAMP_MAX: float #endregion #region Type hinting for Websocket Authenticated Channels -Order = TypedDict("Order", { - "ID": int, - "GID": int, - "CID": int, - "SYMBOL": str, - "MTS_CREATE": int, - "MTS_UPDATE": int, - "AMOUNT": float, - "AMOUNT_ORIG": float, - "ORDER_TYPE": str, - "TYPE_PREV": str, - "MTS_TIF": int, - "FLAGS": int, - "ORDER_STATUS": str, - "PRICE": float, - "PRICE_AVG": float, - "PRICE_TRAILING": float, - "PRICE_AUX_LIMIT": float, - "NOTIFY": int, - "HIDDEN": int, - "PLACED_ID": int, - "ROUTING": str, - "META": JSON -}) +class Order(TypedDict): + ID: int + GID: int + CID: int + SYMBOL: str + MTS_CREATE: int + MTS_UPDATE: int + AMOUNT: float + AMOUNT_ORIG: float + ORDER_TYPE: str + TYPE_PREV: str + MTS_TIF: int + FLAGS: int + ORDER_STATUS: str + PRICE: float + PRICE_AVG: float + PRICE_TRAILING: float + PRICE_AUX_LIMIT: float + NOTIFY: int + HIDDEN: int + PLACED_ID: int + ROUTING: str + META: JSON -Orders = List[Order] +class Position(TypedDict): + SYMBOL: str + STATUS: str + AMOUNT: float + BASE_PRICE: float + MARGIN_FUNDING: float + MARGIN_FUNDING_TYPE: int + PL: float + PL_PERC: float + PRICE_LIQ: float + LEVERAGE: float + POSITION_ID: int + MTS_CREATE: int + MTS_UPDATE: int + TYPE: int + COLLATERAL: float + COLLATERAL_MIN: float + META: JSON -Position = TypedDict("Position", { - "SYMBOL": str, - "STATUS": str, - "AMOUNT": float, - "BASE_PRICE": float, - "MARGIN_FUNDING": float, - "MARGIN_FUNDING_TYPE": int, - "PL": float, - "PL_PERC": float, - "PRICE_LIQ": float, - "LEVERAGE": float, - "POSITION_ID": int, - "MTS_CREATE": int, - "MTS_UPDATE": int, - "TYPE": int, - "COLLATERAL": float, - "COLLATERAL_MIN": float, - "META": JSON, -}) +class TradeExecuted(TypedDict): + ID: int + SYMBOL: str + MTS_CREATE: int + ORDER_ID: int + EXEC_AMOUNT: float + EXEC_PRICE: float + ORDER_TYPE: str + ORDER_PRICE: float + MAKER:int + CID: int -Positions = List[Position] +class TradeExecutionUpdate(TypedDict): + ID: int + SYMBOL: str + MTS_CREATE: int + ORDER_ID: int + EXEC_AMOUNT: float + EXEC_PRICE: float + ORDER_TYPE: str + ORDER_PRICE: float + MAKER:int + FEE: float + FEE_CURRENCY: str + CID: int -TradeExecuted = TypedDict("TradeExecuted", { - "ID": int, - "SYMBOL": str, - "MTS_CREATE": int, - "ORDER_ID": int, - "EXEC_AMOUNT": float, - "EXEC_PRICE": float, - "ORDER_TYPE": str, - "ORDER_PRICE": float, - "MAKER":int, - "CID": int -}) +class FundingOffer(TypedDict): + ID: int + SYMBOL: str + MTS_CREATED: int + MTS_UPDATED: int + AMOUNT: float + AMOUNT_ORIG: float + OFFER_TYPE: str + FLAGS: int + STATUS: str + RATE: float + PERIOD: int + NOTIFY: int + HIDDEN: int + RENEW: int -TradeExecutionUpdate = TypedDict("TradeExecutionUpdate", { - "ID": int, - "SYMBOL": str, - "MTS_CREATE": int, - "ORDER_ID": int, - "EXEC_AMOUNT": float, - "EXEC_PRICE": float, - "ORDER_TYPE": str, - "ORDER_PRICE": float, - "MAKER":int, - "FEE": float, - "FEE_CURRENCY": str, - "CID": int -}) +class FundingCredit(TypedDict): + ID: int + SYMBOL: str + SIDE: int + MTS_CREATE: int + MTS_UPDATE: int + AMOUNT: float + FLAGS: int + STATUS: str + RATE: float + PERIOD: int + MTS_OPENING: int + MTS_LAST_PAYOUT: int + NOTIFY: int + HIDDEN: int + RENEW: int + RATE_REAL: float + NO_CLOSE: int + POSITION_PAIR: str -FundingOffer = TypedDict("FundingOffer", { - "ID": int, - "SYMBOL": str, - "MTS_CREATED": int, - "MTS_UPDATED": int, - "AMOUNT": float, - "AMOUNT_ORIG": float, - "OFFER_TYPE": str, - "FLAGS": int, - "STATUS": str, - "RATE": float, - "PERIOD": int, - "NOTIFY": int, - "HIDDEN": int, - "RENEW": int, -}) +class FundingLoan(TypedDict): + ID: int + SYMBOL: str + SIDE: int + MTS_CREATE: int + MTS_UPDATE: int + AMOUNT: float + FLAGS: int + STATUS: str + RATE: float + PERIOD: int + MTS_OPENING: int + MTS_LAST_PAYOUT: int + NOTIFY: int + HIDDEN: int + RENEW: int + RATE_REAL: float + NO_CLOSE: int -FundingOffers = List[FundingOffer] +class Wallet(TypedDict): + WALLET_TYPE: str + CURRENCY: str + BALANCE: float + UNSETTLED_INTEREST: float + BALANCE_AVAILABLE: float + DESCRIPTION: str + META: JSON -FundingCredit = TypedDict("FundingCredit", { - "ID": int, - "SYMBOL": str, - "SIDE": int, - "MTS_CREATE": int, - "MTS_UPDATE": int, - "AMOUNT": float, - "FLAGS": int, - "STATUS": str, - "RATE": float, - "PERIOD": int, - "MTS_OPENING": int, - "MTS_LAST_PAYOUT": int, - "NOTIFY": int, - "HIDDEN": int, - "RENEW": int, - "RATE_REAL": float, - "NO_CLOSE": int, - "POSITION_PAIR": str -}) +class BalanceInfo(TypedDict): + AUM: float + AUM_NET: float -FundingCredits = List[FundingCredit] +#endregion -FundingLoan = TypedDict("FundingLoan", { - "ID": int, - "SYMBOL": str, - "SIDE": int, - "MTS_CREATE": int, - "MTS_UPDATE": int, - "AMOUNT": float, - "FLAGS": int, - "STATUS": str, - "RATE": float, - "PERIOD": int, - "MTS_OPENING": int, - "MTS_LAST_PAYOUT": int, - "NOTIFY": int, - "HIDDEN": int, - "RENEW": int, - "RATE_REAL": float, - "NO_CLOSE": int -}) +#region Serializers definition for Notifications channel -FundingLoans = List[FundingLoan] - -Wallet = TypedDict("Wallet", { - "WALLET_TYPE": str, - "CURRENCY": str, - "BALANCE": float, - "UNSETTLED_INTEREST": float, - "BALANCE_AVAILABLE": float, - "DESCRIPTION": str, - "META": JSON -}) - -Wallets = List[Wallet] - -BalanceInfo = TypedDict("BalanceInfo", { - "AUM": float, - "AUM_NET": float -}) +class Notification(TypedDict): + MTS: int + TYPE: str + MESSAGE_ID: int + NOTIFY_INFO: JSON + CODE: int + STATUS: str + TEXT: str #endregion @@ -304,55 +297,50 @@ BalanceInfo = TypedDict("BalanceInfo", { class Inputs: class Order: - New = TypedDict("Inputs.Order.New", { - "gid": Optional[int32], - "cid": int45, - "type": str, - "symbol": str, - "amount": Union[Decimal, str], - "price": Union[Decimal, str], - "lev": int, - "price_trailing": Union[Decimal, str], - "price_aux_limit": Union[Decimal, str], - "price_oco_stop": Union[Decimal, str], - "flags": int16, - "tif": Union[datetime, str], - "meta": JSON - }) + class New(TypedDict, total=False): + gid: Union[Int32, int] + cid: Union[Int45, int] + type: str + symbol: str + amount: Union[Decimal, str] + price: Union[Decimal, str] + lev: Union[Int32, int] + price_trailing: Union[Decimal, str] + price_aux_limit: Union[Decimal, str] + price_oco_stop: Union[Decimal, str] + flags: Union[Int16, int] + tif: Union[datetime, str] + meta: JSON - Update = TypedDict("Inputs.Order.Update", { - "id": int64, - "cid": int45, - "cid_date": str, - "gid": int32, - "price": Union[Decimal, str], - "amount": Union[Decimal, str], - "lev": int, - "delta": Union[Decimal, str], - "price_aux_limit": Union[Decimal, str], - "price_trailing": Union[Decimal, str], - "flags": int16, - "tif": Union[datetime, str] - }) + class Update(TypedDict, total=False): + id: Union[Int64, int] + cid: Union[Int45, int] + cid_date: str + gid: Union[Int32, int] + price: Union[Decimal, str] + amount: Union[Decimal, str] + lev: Union[Int32, int] + delta: Union[Decimal, str] + price_aux_limit: Union[Decimal, str] + price_trailing: Union[Decimal, str] + flags: Union[Int16, int] + tif: Union[datetime, str] - Cancel = TypedDict("Inputs.Order.Cancel", { - "id": int64, - "cid": int45, - "cid_date": str - }) + class Cancel(TypedDict, total=False): + id: Union[Int64, int] + cid: Union[Int45, int] + cid_date: Union[datetime, str] class Offer: - New = TypedDict("Inputs.Offer.New", { - "type": str, - "symbol": str, - "amount": Union[Decimal, str], - "rate": Union[Decimal, str], - "period": int, - "flags": int16 - }) + class New(TypedDict, total=False): + type: str + symbol: str + amount: Union[Decimal, str] + rate: Union[Decimal, str] + period: Union[Int32, int] + flags: Union[Int16, int] - Cancel = TypedDict("Inputs.Offer.Cancel", { - "id": int - }) + class Cancel(TypedDict, total=False): + id: Union[Int32, int] #endregion \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 549f7a67024ef377ed41842b9f7abfb02005904f..71a2708e787aaa497e5eaac30bc59566d18b7a57 100644 GIT binary patch delta 146 zcmdnM)W9-9j@^*Kh(V9Re4=WgaxOz9Lje%m0-*s=&=Lp@8F(4E7+^BG6Awyqf}~7< z*mz>0t7<+_E{7o#tOsNeND`u_1Z-L=LotKy#C}ClL!g8y*f3;?=d$cZKw+SP6W2Ea E0F%WVDgXcg delta 24 fcmZo**}ybGj@^<$kHLt+WTI-|!~+hK+Zd|=Oos;L diff --git a/setup.py b/setup.py index a4c8397..963f30a 100644 --- a/setup.py +++ b/setup.py @@ -11,11 +11,16 @@ setup( description="Official Bitfinex Python API", keywords="bitfinex,api,trading", install_requires=[ - "certifi~=2022.9.24", + "certifi~=2022.12.7", "charset-normalizer~=2.1.1", "idna~=3.4", + "mypy~=0.991", + "mypy-extensions~=0.4.3", "pyee~=9.0.4", "requests~=2.28.1", + "tomli~=2.0.1", + "types-requests~=2.28.11.5", + "types-urllib3~=1.26.25.4", "typing_extensions~=4.4.0", "urllib3~=1.26.13", "websockets~=10.4", From ea3eefd32c211a23d5e62748cf5746699b1a4907 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 16 Dec 2022 18:42:59 +0100 Subject: [PATCH 21/38] Apply refactoring with new standards in examples/websockets/*.py demos. --- examples/websocket/order_book.py | 8 +++++--- examples/websocket/raw_order_book.py | 8 +++++--- examples/websocket/ticker.py | 4 ++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/examples/websocket/order_book.py b/examples/websocket/order_book.py index a100c2b..edb765e 100644 --- a/examples/websocket/order_book.py +++ b/examples/websocket/order_book.py @@ -1,13 +1,15 @@ from collections import OrderedDict +from typing import List + from bfxapi import Client, Constants from bfxapi.websocket import BfxWebsocketClient from bfxapi.websocket.enums import Channels, Errors -from bfxapi.websocket.typings import Subscriptions, TradingPairBooks, TradingPairBook +from bfxapi.websocket.typings import Subscriptions, TradingPairBook class OrderBook(object): - def __init__(self, symbols: list[str]): + def __init__(self, symbols: List[str]): self.__order_book = { symbol: { "bids": OrderedDict(), "asks": OrderedDict() @@ -50,7 +52,7 @@ def on_subscribed(subscription): print(f"Subscription successful for pair <{subscription['pair']}>") @bfx.wss.on("t_book_snapshot") -def on_t_book_snapshot(subscription: Subscriptions.Book, snapshot: TradingPairBooks): +def on_t_book_snapshot(subscription: Subscriptions.Book, snapshot: List[TradingPairBook]): for data in snapshot: order_book.update(subscription["symbol"], data) diff --git a/examples/websocket/raw_order_book.py b/examples/websocket/raw_order_book.py index fe10490..b34ae8e 100644 --- a/examples/websocket/raw_order_book.py +++ b/examples/websocket/raw_order_book.py @@ -1,13 +1,15 @@ from collections import OrderedDict +from typing import List + from bfxapi import Client, Constants from bfxapi.websocket import BfxWebsocketClient from bfxapi.websocket.enums import Channels, Errors -from bfxapi.websocket.typings import Subscriptions, TradingPairRawBooks, TradingPairRawBook +from bfxapi.websocket.typings import Subscriptions, TradingPairRawBook class RawOrderBook(object): - def __init__(self, symbols: list[str]): + def __init__(self, symbols: List[str]): self.__raw_order_book = { symbol: { "bids": OrderedDict(), "asks": OrderedDict() @@ -50,7 +52,7 @@ def on_subscribed(subscription): print(f"Subscription successful for pair <{subscription['pair']}>") @bfx.wss.on("t_raw_book_snapshot") -def on_t_raw_book_snapshot(subscription: Subscriptions.Book, snapshot: TradingPairRawBooks): +def on_t_raw_book_snapshot(subscription: Subscriptions.Book, snapshot: List[TradingPairRawBook]): for data in snapshot: raw_order_book.update(subscription["symbol"], data) diff --git a/examples/websocket/ticker.py b/examples/websocket/ticker.py index 4e5d8e7..107e367 100644 --- a/examples/websocket/ticker.py +++ b/examples/websocket/ticker.py @@ -1,14 +1,14 @@ import asyncio from bfxapi import Client, Constants -from bfxapi.websocket import Channels +from bfxapi.websocket.enums import Channels from bfxapi.websocket.typings import Subscriptions, TradingPairTicker bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) @bfx.wss.on("t_ticker_update") def on_t_ticker_update(subscription: Subscriptions.TradingPairsTicker, data: TradingPairTicker): - print(f"Subscription channel ID: {subscription['chanId']}") + print(f"Subscription with channel ID: {subscription['chanId']}") print(f"Data: {data}") From 87bb6dc5c75be079641fc916f298bcce941ae628 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 20 Dec 2022 17:48:12 +0100 Subject: [PATCH 22/38] Add generic error handling (UnknownGenericError in bfxapi/rest/exceptions.py). Add support for new endpoints in _RestAuthenticatedEndpoints class. Extend serializers.py and typings.py. --- bfxapi/rest/BfxRestInterface.py | 16 ++++++-- bfxapi/rest/exceptions.py | 15 ++++++-- bfxapi/rest/serializers.py | 65 +++++++++++++++++++++++++++++++++ bfxapi/rest/typings.py | 58 +++++++++++++++++++++++++++++ bfxapi/websocket/typings.py | 12 +++--- examples/websocket/ticker.py | 2 +- 6 files changed, 154 insertions(+), 14 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 37f52d3..8fc5b07 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -8,7 +8,7 @@ from . import serializers from .typings import * from .enums import Config, Precision, Sort -from .exceptions import RequestParametersError, ResourceNotFound, InvalidAuthenticationCredentials +from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError class BfxRestInterface(object): def __init__(self, host, API_KEY = None, API_SECRET = None): @@ -47,13 +47,16 @@ class _Requests(object): if data[1] == 10020: raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") + if data[1] == None: + raise UnknownGenericError("The server replied to the request with a generic error.") + return data def _POST(self, endpoint, params = None, data = None, _append_authentication_headers = True): headers = { "Content-Type": "application/json" } if _append_authentication_headers: - headers = { **headers, **self.__build_authentication_headers(f"{endpoint}", data) } + headers = { **headers, **self.__build_authentication_headers(endpoint, data) } response = requests.post(f"{self.host}/{endpoint}", params=params, data=json.dumps(data), headers=headers) @@ -69,6 +72,9 @@ class _Requests(object): if data[1] == 10100: raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.") + if data[1] == None: + raise UnknownGenericError("The server replied to the request with a generic error.") + return data class _RestPublicEndpoints(_Requests): @@ -227,4 +233,8 @@ class _RestPublicEndpoints(_Requests): return self._GET(f"conf/{config}")[0] class _RestAuthenticatedEndpoints(_Requests): - __PREFIX = "auth/" \ No newline at end of file + def wallets(self) -> List[Wallet]: + return [ serializers.Wallet.parse(*subdata) for subdata in self._POST("auth/r/wallets") ] + + def retrieve_orders(self, ids: Optional[List[str]] = None) -> List[Order]: + return [ serializers.Order.parse(*subdata) for subdata in self._POST("auth/r/orders", data={ "id": ids }) ] \ No newline at end of file diff --git a/bfxapi/rest/exceptions.py b/bfxapi/rest/exceptions.py index 81bcb8f..beff7bc 100644 --- a/bfxapi/rest/exceptions.py +++ b/bfxapi/rest/exceptions.py @@ -15,16 +15,16 @@ class BfxRestException(BfxBaseException): pass -class RequestParametersError(BfxRestException): +class ResourceNotFound(BfxRestException): """ - This error indicates that there are some invalid parameters sent along with an HTTP request. + This error indicates a failed HTTP request to a non-existent resource. """ pass -class ResourceNotFound(BfxRestException): +class RequestParametersError(BfxRestException): """ - This error indicates a failed HTTP request to a non-existent resource. + This error indicates that there are some invalid parameters sent along with an HTTP request. """ pass @@ -34,4 +34,11 @@ class InvalidAuthenticationCredentials(BfxRestException): This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication. """ + pass + +class UnknownGenericError(BfxRestException): + """ + This error indicates an undefined problem processing an HTTP request sent to the APIs. + """ + pass \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 95f4c26..70f0887 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -183,4 +183,69 @@ FundingStatistic = _Serializer[typings.FundingStatistic]("FundingStatistic", lab "FUNDING_BELOW_THRESHOLD" ]) +#endregion + +#region Serializers definition for Rest Authenticated Endpoints + +Wallet = _Serializer[typings.Wallet]("Wallet", labels=[ + "WALLET_TYPE", + "CURRENCY", + "BALANCE", + "UNSETTLED_INTEREST", + "AVAILABLE_BALANCE", + "LAST_CHANGE", + "TRADE_DETAILS" +]) + +Order = _Serializer[typings.Order]("Order", labels=[ + "ID", + "GID", + "CID", + "SYMBOL", + "MTS_CREATE", + "MTS_UPDATE", + "AMOUNT", + "AMOUNT_ORIG", + "ORDER_TYPE", + "TYPE_PREV", + "MTS_TIF", + "_PLACEHOLDER", + "FLAGS", + "ORDER_STATUS", + "_PLACEHOLDER", + "_PLACEHOLDER", + "PRICE", + "PRICE_AVG", + "PRICE_TRAILING", + "PRICE_AUX_LIMIT", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "NOTIFY", + "HIDDEN", + "PLACED_ID", + "_PLACEHOLDER", + "_PLACEHOLDER", + "ROUTING", + "_PLACEHOLDER", + "_PLACEHOLDER", + "META" +]) + + +#endregion + +#region Serializers definition for Notifications channel + +Notification = _Serializer[typings.Notification]("Notification", labels=[ + "MTS", + "TYPE", + "MESSAGE_ID", + "_PLACEHOLDER", + "NOTIFY_INFO", + "CODE", + "STATUS", + "TEXT" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index 7af75fb..810d9da 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -1,5 +1,13 @@ +from decimal import Decimal + +from datetime import datetime + from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any +from ..utils.integers import Int16, Int32, Int45, Int64 + +JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] + #region Type hinting for Rest Public Endpoints class PlatformStatus(TypedDict): @@ -128,4 +136,54 @@ class FundingStatistic(TypedDict): FUNDING_AMOUNT_USED: float FUNDING_BELOW_THRESHOLD: float +#endregion + +#region Type hinting for Rest Authenticated Endpoints + +class Wallet(TypedDict): + WALLET_TYPE: str + CURRENCY: str + BALANCE: float + UNSETTLED_INTEREST: float + AVAILABLE_BALANCE: float + LAST_CHANGE: str + TRADE_DETAILS: JSON + +class Order(TypedDict): + ID: int + GID: int + CID: int + SYMBOL: str + MTS_CREATE: int + MTS_UPDATE: int + AMOUNT: float + AMOUNT_ORIG: float + ORDER_TYPE: str + TYPE_PREV: str + MTS_TIF: int + FLAGS: int + ORDER_STATUS: str + PRICE: float + PRICE_AVG: float + PRICE_TRAILING: float + PRICE_AUX_LIMIT: float + NOTIFY: int + HIDDEN: int + PLACED_ID: int + ROUTING: str + META: JSON + +#endregion + +#region Type hinting for Notifications channel + +class Notification(TypedDict): + MTS: int + TYPE: str + MESSAGE_ID: int + NOTIFY_INFO: JSON + CODE: int + STATUS: str + TEXT: str + #endregion \ No newline at end of file diff --git a/bfxapi/websocket/typings.py b/bfxapi/websocket/typings.py index 9966d99..ee55dd3 100644 --- a/bfxapi/websocket/typings.py +++ b/bfxapi/websocket/typings.py @@ -2,7 +2,7 @@ from decimal import Decimal from datetime import datetime -from typing import Type, NewType, Tuple, List, Dict, TypedDict, Union, Optional, Any +from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any from ..utils.integers import Int16, Int32, Int45, Int64 @@ -11,22 +11,22 @@ JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] #region Type hinting for subscription objects class Subscriptions: - class TradingPairsTicker(TypedDict): + class TradingPairTicker(TypedDict): chanId: int symbol: str pair: str - class FundingCurrenciesTicker(TypedDict): + class FundingCurrencyTicker(TypedDict): chanId: int symbol: str currency: str - class TradingPairsTrades(TypedDict): + class TradingPairTrades(TypedDict): chanId: int symbol: str pair: str - class FundingCurrenciesTrades(TypedDict): + class FundingCurrencyTrades(TypedDict): chanId: int symbol: str currency: str @@ -280,7 +280,7 @@ class BalanceInfo(TypedDict): #endregion -#region Serializers definition for Notifications channel +#region Type hinting for Notifications channel class Notification(TypedDict): MTS: int diff --git a/examples/websocket/ticker.py b/examples/websocket/ticker.py index 107e367..ff8d899 100644 --- a/examples/websocket/ticker.py +++ b/examples/websocket/ticker.py @@ -7,7 +7,7 @@ from bfxapi.websocket.typings import Subscriptions, TradingPairTicker bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) @bfx.wss.on("t_ticker_update") -def on_t_ticker_update(subscription: Subscriptions.TradingPairsTicker, data: TradingPairTicker): +def on_t_ticker_update(subscription: Subscriptions.TradingPairTicker, data: TradingPairTicker): print(f"Subscription with channel ID: {subscription['chanId']}") print(f"Data: {data}") From 6217f9040cb8cdd30182aaecfcdbb5a3137f088e Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 20 Dec 2022 18:40:41 +0100 Subject: [PATCH 23/38] Rename bfxapi/utils/decimal.py to encoder.py. Add support for datetime JSON serialization. Update class reference in BfxWebsocketClient.py. --- bfxapi/utils/{decimal.py => encoder.py} | 6 +++--- bfxapi/websocket/BfxWebsocketClient.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) rename bfxapi/utils/{decimal.py => encoder.py} (52%) diff --git a/bfxapi/utils/decimal.py b/bfxapi/utils/encoder.py similarity index 52% rename from bfxapi/utils/decimal.py rename to bfxapi/utils/encoder.py index 5a7af71..3649823 100644 --- a/bfxapi/utils/decimal.py +++ b/bfxapi/utils/encoder.py @@ -1,9 +1,9 @@ import json - from decimal import Decimal +from datetime import datetime -class DecimalEncoder(json.JSONEncoder): +class JSONEncoder(json.JSONEncoder): def default(self, obj): - if isinstance(obj, Decimal): + if isinstance(obj, Decimal) or isinstance(obj, datetime): return str(obj) return json.JSONEncoder.default(self, obj) \ No newline at end of file diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 775fa10..7cd8728 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -10,7 +10,7 @@ from .typings import Inputs from .handlers import Channels, PublicChannelsHandler, AuthenticatedChannelsHandler from .exceptions import ConnectionNotOpen, TooManySubscriptions, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion -from ..utils.decimal import DecimalEncoder +from ..utils.encoder import JSONEncoder from ..utils.logger import Formatter, CustomLogger @@ -139,7 +139,7 @@ class BfxWebsocketClient(object): @_require_websocket_authentication async def __handle_websocket_input(self, input, data): - await self.websocket.send(json.dumps([ 0, input, None, data], cls=DecimalEncoder)) + 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): From 79ae0b48e0d48600fa12e4c8a10722f7529ea994 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 21 Dec 2022 18:27:54 +0100 Subject: [PATCH 24/38] Fix bug in _Requests's _GET and _POST methods. Add submit_order to handle POST auth/w/order/submit endpoint. Add OrderType enumeration in bfxapi/rest/enums.py. --- bfxapi/rest/BfxRestInterface.py | 27 +++++++++++++++++++++------ bfxapi/rest/enums.py | 16 ++++++++++++++++ bfxapi/rest/serializers.py | 1 - 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 8fc5b07..6af32a4 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -7,7 +7,7 @@ from typing import List, Union, Literal, Optional, Any, cast from . import serializers from .typings import * -from .enums import Config, Precision, Sort +from .enums import OrderType, Config, Precision, Sort from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError class BfxRestInterface(object): @@ -47,8 +47,8 @@ class _Requests(object): if data[1] == 10020: raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") - if data[1] == None: - raise UnknownGenericError("The server replied to the request with a generic error.") + if data[1] == None or data[1] == 10000 or data[1] == 10001: + raise UnknownGenericError("The server replied to the request with a generic error with message: <{data[2]}>.") return data @@ -72,8 +72,8 @@ class _Requests(object): if data[1] == 10100: raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.") - if data[1] == None: - raise UnknownGenericError("The server replied to the request with a generic error.") + if data[1] == None or data[1] == 10000 or data[1] == 10001: + raise UnknownGenericError(f"The server replied to the request with a generic error with message: <{data[2]}>.") return data @@ -237,4 +237,19 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.Wallet.parse(*subdata) for subdata in self._POST("auth/r/wallets") ] def retrieve_orders(self, ids: Optional[List[str]] = None) -> List[Order]: - return [ serializers.Order.parse(*subdata) for subdata in self._POST("auth/r/orders", data={ "id": ids }) ] \ No newline at end of file + return [ serializers.Order.parse(*subdata) for subdata in self._POST("auth/r/orders", data={ "id": ids }) ] + + def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str], + price: Optional[Union[Decimal, str]] = None, lev: Optional[Union[Int32, 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[Union[Int32, int]] = None, cid: Optional[Union[Int45, int]] = None, + flags: Optional[Union[Int16, int]] = None, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification: + data = { + "type": type, "symbol": symbol, "amount": amount, + "price": price, "lev": lev, + "price_trailing": price_trailing, "price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop, + "gid": gid, "cid": cid, + "flags": flags, "tif": tif, "meta": meta + } + + return self._POST("auth/w/order/submit", data=data) \ No newline at end of file diff --git a/bfxapi/rest/enums.py b/bfxapi/rest/enums.py index 70c2336..e1e3f49 100644 --- a/bfxapi/rest/enums.py +++ b/bfxapi/rest/enums.py @@ -1,5 +1,21 @@ from enum import Enum +class OrderType(str, Enum): + LIMIT = "LIMIT" + EXCHANGE_LIMIT = "EXCHANGE LIMIT" + MARKET = "MARKET" + EXCHANGE_MARKET = "EXCHANGE MARKET" + STOP = "STOP" + EXCHANGE_STOP = "EXCHANGE STOP" + STOP_LIMIT = "STOP LIMIT" + EXCHANGE_STOP_LIMIT = "EXCHANGE STOP LIMIT" + TRAILING_STOP = "TRAILING STOP" + EXCHANGE_TRAILING_STOP = "EXCHANGE TRAILING STOP" + FOK = "FOK" + EXCHANGE_FOK = "EXCHANGE FOK" + IOC = "IOC" + EXCHANGE_IOC = "EXCHANGE IOC" + class Config(str, Enum): MAP_CURRENCY_SYM = "pub:map:currency:sym" MAP_CURRENCY_LABEL = "pub:map:currency:label" diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 70f0887..fe7d5a7 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -232,7 +232,6 @@ Order = _Serializer[typings.Order]("Order", labels=[ "META" ]) - #endregion #region Serializers definition for Notifications channel From d5ace495554bde36ad040fd1249bb6c1a137cc4f Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 22 Dec 2022 17:07:36 +0100 Subject: [PATCH 25/38] Add implementation for submit_order, update_order and cancel_order endpoint handlers in BfxRestInterface.py. --- bfxapi/rest/BfxRestInterface.py | 22 ++++++++++++++++++++-- bfxapi/rest/typings.py | 6 ------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 6af32a4..4eed8b7 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -10,6 +10,8 @@ from .typings import * from .enums import OrderType, Config, Precision, Sort from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError +from .. utils.encoder import JSONEncoder + class BfxRestInterface(object): def __init__(self, host, API_KEY = None, API_SECRET = None): self.public = _RestPublicEndpoints(host=host) @@ -58,7 +60,7 @@ class _Requests(object): if _append_authentication_headers: headers = { **headers, **self.__build_authentication_headers(endpoint, data) } - response = requests.post(f"{self.host}/{endpoint}", params=params, data=json.dumps(data), headers=headers) + response = requests.post(f"{self.host}/{endpoint}", params=params, data=json.dumps(data, cls=JSONEncoder), headers=headers) if response.status_code == HTTPStatus.NOT_FOUND: raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") @@ -252,4 +254,20 @@ class _RestAuthenticatedEndpoints(_Requests): "flags": flags, "tif": tif, "meta": meta } - return self._POST("auth/w/order/submit", data=data) \ No newline at end of file + return serializers.Notification.parse(*self._POST("auth/w/order/submit", data=data)) + + def update_order(self, id: Union[Int64, int], amount: Optional[Union[Decimal, str]] = None, price: Optional[Union[Decimal, str]] = None, + cid: Optional[Union[Int45, int]] = None, cid_date: Optional[str] = None, gid: Optional[Union[Int32, int]] = None, + flags: Optional[Union[Int16, int]] = None, lev: Optional[Union[Int32, int]] = None, delta: Optional[Union[Decimal, str]] = None, + price_aux_limit: Optional[Union[Decimal, str]] = None, price_trailing: Optional[Union[Decimal, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification: + data = { + "id": id, "amount": amount, "price": price, + "cid": cid, "cid_date": cid_date, "gid": gid, + "flags": flags, "lev": lev, "delta": delta, + "price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif + } + + return serializers.Notification.parse(*self._POST("auth/w/order/update", data=data)) + + def cancel_order(self, id: Union[Int64, int]) -> Notification: + return serializers.Notification.parse(*self._POST("auth/w/order/cancel", data={ "id": id })) \ No newline at end of file diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index 810d9da..468a44e 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -1,11 +1,5 @@ -from decimal import Decimal - -from datetime import datetime - from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any -from ..utils.integers import Int16, Int32, Int45, Int64 - JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] #region Type hinting for Rest Public Endpoints From 4f63f4068edec56eeadceccd7c19fe84360c17d2 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 22 Dec 2022 18:24:56 +0100 Subject: [PATCH 26/38] Add and implement notification.py in root package (bfxapi). --- bfxapi/labeler.py | 4 ++-- bfxapi/notification.py | 28 ++++++++++++++++++++++++++++ bfxapi/rest/BfxRestInterface.py | 10 +++++++--- bfxapi/rest/serializers.py | 17 ++--------------- bfxapi/rest/typings.py | 15 ++------------- 5 files changed, 41 insertions(+), 33 deletions(-) create mode 100644 bfxapi/notification.py diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index bcf18c3..4575146 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -8,7 +8,7 @@ class _Serializer(Generic[T]): def __init__(self, name: str, labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]): self.name, self.__labels, self.__IGNORE = name, labels, IGNORE - def __serialize(self, *args: Any, skip: Optional[List[str]]) -> Iterable[Tuple[str, Any]]: + def _serialize(self, *args: Any, skip: Optional[List[str]] = None) -> Iterable[Tuple[str, Any]]: labels = list(filter(lambda label: label not in (skip or list()), self.__labels)) if len(labels) > len(args): @@ -19,4 +19,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, dict(self._serialize(*values, skip=skip))) \ No newline at end of file diff --git a/bfxapi/notification.py b/bfxapi/notification.py new file mode 100644 index 0000000..003867b --- /dev/null +++ b/bfxapi/notification.py @@ -0,0 +1,28 @@ +from typing import Dict, Optional, Any, TypedDict, cast + +from .labeler import _Serializer + +class Notification(TypedDict): + MTS: int + TYPE: str + MESSAGE_ID: Optional[int] + NOTIFY_INFO: Dict[str, Any] + CODE: Optional[int] + STATUS: str + TEXT: str + +class _Notification(_Serializer): + __LABELS = [ "MTS", "TYPE", "MESSAGE_ID", "_PLACEHOLDER", "NOTIFY_INFO", "CODE", "STATUS", "TEXT" ] + + def __init__(self, serializer: Optional[_Serializer] = None): + super().__init__("Notification", _Notification.__LABELS, IGNORE = [ "_PLACEHOLDER" ]) + + self.serializer = serializer + + def parse(self, *values: Any) -> Notification: + notification = dict(self._serialize(*values)) + + if self.serializer != None: + notification["NOTIFY_INFO"] = dict(self.serializer._serialize(*notification["NOTIFY_INFO"])) + + return cast(Notification, notification) \ No newline at end of file diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 4eed8b7..daf7c4e 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -1,5 +1,7 @@ import time, hmac, hashlib, json, requests +from decimal import Decimal +from datetime import datetime from http import HTTPStatus from typing import List, Union, Literal, Optional, Any, cast @@ -10,6 +12,8 @@ from .typings import * from .enums import OrderType, Config, Precision, Sort from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError +from .. utils.integers import Int16, Int32, Int45, Int64 + from .. utils.encoder import JSONEncoder class BfxRestInterface(object): @@ -254,7 +258,7 @@ class _RestAuthenticatedEndpoints(_Requests): "flags": flags, "tif": tif, "meta": meta } - return serializers.Notification.parse(*self._POST("auth/w/order/submit", data=data)) + return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/submit", data=data)) def update_order(self, id: Union[Int64, int], amount: Optional[Union[Decimal, str]] = None, price: Optional[Union[Decimal, str]] = None, cid: Optional[Union[Int45, int]] = None, cid_date: Optional[str] = None, gid: Optional[Union[Int32, int]] = None, @@ -267,7 +271,7 @@ class _RestAuthenticatedEndpoints(_Requests): "price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif } - return serializers.Notification.parse(*self._POST("auth/w/order/update", data=data)) + return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/update", data=data)) def cancel_order(self, id: Union[Int64, int]) -> Notification: - return serializers.Notification.parse(*self._POST("auth/w/order/cancel", data={ "id": id })) \ No newline at end of file + return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/cancel", data={ "id": id })) \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index fe7d5a7..3abea89 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -2,6 +2,8 @@ from . import typings from .. labeler import _Serializer +from .. notification import _Notification + #region Serializers definition for Rest Public Endpoints PlatformStatus = _Serializer[typings.PlatformStatus]("PlatformStatus", labels=[ @@ -232,19 +234,4 @@ Order = _Serializer[typings.Order]("Order", labels=[ "META" ]) -#endregion - -#region Serializers definition for Notifications channel - -Notification = _Serializer[typings.Notification]("Notification", labels=[ - "MTS", - "TYPE", - "MESSAGE_ID", - "_PLACEHOLDER", - "NOTIFY_INFO", - "CODE", - "STATUS", - "TEXT" -]) - #endregion \ No newline at end of file diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index 468a44e..f99b7c1 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -1,5 +1,7 @@ from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any +from .. notification import Notification + JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] #region Type hinting for Rest Public Endpoints @@ -167,17 +169,4 @@ class Order(TypedDict): ROUTING: str META: JSON -#endregion - -#region Type hinting for Notifications channel - -class Notification(TypedDict): - MTS: int - TYPE: str - MESSAGE_ID: int - NOTIFY_INFO: JSON - CODE: int - STATUS: str - TEXT: str - #endregion \ No newline at end of file From 454a7542ed4ef8b088d85c63bf6fea429fcada63 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 22 Dec 2022 18:42:55 +0100 Subject: [PATCH 27/38] Add bfxapi/enums.py file. Split enumerations in bfxapi/rest/enums.py and bfxapi/websocket/enums.py. Rename enumeration classes to use singular name identifiers. --- bfxapi/enums.py | 42 ++++++++++++++++++++++++++++ bfxapi/rest/BfxRestInterface.py | 12 ++++---- bfxapi/rest/enums.py | 18 +----------- bfxapi/websocket/enums.py | 29 ++----------------- examples/websocket/order_book.py | 4 +-- examples/websocket/raw_order_book.py | 4 +-- 6 files changed, 55 insertions(+), 54 deletions(-) create mode 100644 bfxapi/enums.py diff --git a/bfxapi/enums.py b/bfxapi/enums.py new file mode 100644 index 0000000..2293ff9 --- /dev/null +++ b/bfxapi/enums.py @@ -0,0 +1,42 @@ +from enum import Enum + +class OrderType(str, Enum): + LIMIT = "LIMIT" + EXCHANGE_LIMIT = "EXCHANGE LIMIT" + MARKET = "MARKET" + EXCHANGE_MARKET = "EXCHANGE MARKET" + STOP = "STOP" + EXCHANGE_STOP = "EXCHANGE STOP" + STOP_LIMIT = "STOP LIMIT" + EXCHANGE_STOP_LIMIT = "EXCHANGE STOP LIMIT" + TRAILING_STOP = "TRAILING STOP" + EXCHANGE_TRAILING_STOP = "EXCHANGE TRAILING STOP" + FOK = "FOK" + EXCHANGE_FOK = "EXCHANGE FOK" + IOC = "IOC" + EXCHANGE_IOC = "EXCHANGE IOC" + +class Flag(int, Enum): + HIDDEN = 64 + CLOSE = 512 + REDUCE_ONLY = 1024 + POST_ONLY = 4096 + OCO = 16384 + NO_VAR_RATES = 524288 + +class Error(int, Enum): + ERR_UNK = 10000 + ERR_GENERIC = 10001 + ERR_CONCURRENCY = 10008 + ERR_PARAMS = 10020 + ERR_CONF_FAIL = 10050 + ERR_AUTH_FAIL = 10100 + ERR_AUTH_PAYLOAD = 10111 + ERR_AUTH_SIG = 10112 + ERR_AUTH_HMAC = 10113 + ERR_AUTH_NONCE = 10114 + ERR_UNAUTH_FAIL = 10200 + ERR_SUB_FAIL = 10300 + ERR_SUB_MULTI = 10301 + ERR_UNSUB_FAIL = 10400 + ERR_READY = 11000 \ No newline at end of file diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index daf7c4e..31b529c 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -9,7 +9,7 @@ from typing import List, Union, Literal, Optional, Any, cast from . import serializers from .typings import * -from .enums import OrderType, Config, Precision, Sort +from .enums import Config, Precision, Sort, OrderType, Error from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError from .. utils.integers import Int16, Int32, Int45, Int64 @@ -50,10 +50,10 @@ class _Requests(object): data = response.json() if len(data) and data[0] == "error": - if data[1] == 10020: + 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] == 10000 or data[1] == 10001: + if data[1] == None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC: raise UnknownGenericError("The server replied to the request with a generic error with message: <{data[2]}>.") return data @@ -72,13 +72,13 @@ class _Requests(object): data = response.json() if len(data) and data[0] == "error": - if data[1] == 10020: + if data[1] == Error.ERR_PARAMS: raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") - if data[1] == 10100: + 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] == 10000 or data[1] == 10001: + 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 diff --git a/bfxapi/rest/enums.py b/bfxapi/rest/enums.py index e1e3f49..65c1e1a 100644 --- a/bfxapi/rest/enums.py +++ b/bfxapi/rest/enums.py @@ -1,20 +1,4 @@ -from enum import Enum - -class OrderType(str, Enum): - LIMIT = "LIMIT" - EXCHANGE_LIMIT = "EXCHANGE LIMIT" - MARKET = "MARKET" - EXCHANGE_MARKET = "EXCHANGE MARKET" - STOP = "STOP" - EXCHANGE_STOP = "EXCHANGE STOP" - STOP_LIMIT = "STOP LIMIT" - EXCHANGE_STOP_LIMIT = "EXCHANGE STOP LIMIT" - TRAILING_STOP = "TRAILING STOP" - EXCHANGE_TRAILING_STOP = "EXCHANGE TRAILING STOP" - FOK = "FOK" - EXCHANGE_FOK = "EXCHANGE FOK" - IOC = "IOC" - EXCHANGE_IOC = "EXCHANGE IOC" +from ..enums import * class Config(str, Enum): MAP_CURRENCY_SYM = "pub:map:currency:sym" diff --git a/bfxapi/websocket/enums.py b/bfxapi/websocket/enums.py index 14c4234..8f06f62 100644 --- a/bfxapi/websocket/enums.py +++ b/bfxapi/websocket/enums.py @@ -1,33 +1,8 @@ -from enum import Enum +from ..enums import * class Channels(str, Enum): TICKER = "ticker" TRADES = "trades" BOOK = "book" CANDLES = "candles" - STATUS = "status" - -class Flags(int, Enum): - HIDDEN = 64 - CLOSE = 512 - REDUCE_ONLY = 1024 - POST_ONLY = 4096 - OCO = 16384 - NO_VAR_RATES = 524288 - -class Errors(int, Enum): - ERR_UNK = 10000 - ERR_GENERIC = 10001 - ERR_CONCURRENCY = 10008 - ERR_PARAMS = 10020 - ERR_CONF_FAIL = 10050 - ERR_AUTH_FAIL = 10100 - ERR_AUTH_PAYLOAD = 10111 - ERR_AUTH_SIG = 10112 - ERR_AUTH_HMAC = 10113 - ERR_AUTH_NONCE = 10114 - ERR_UNAUTH_FAIL = 10200 - ERR_SUB_FAIL = 10300 - ERR_SUB_MULTI = 10301 - ERR_UNSUB_FAIL = 10400 - ERR_READY = 11000 \ No newline at end of file + STATUS = "status" \ No newline at end of file diff --git a/examples/websocket/order_book.py b/examples/websocket/order_book.py index edb765e..372a3f6 100644 --- a/examples/websocket/order_book.py +++ b/examples/websocket/order_book.py @@ -5,7 +5,7 @@ from typing import List from bfxapi import Client, Constants from bfxapi.websocket import BfxWebsocketClient -from bfxapi.websocket.enums import Channels, Errors +from bfxapi.websocket.enums import Channels, Error from bfxapi.websocket.typings import Subscriptions, TradingPairBook class OrderBook(object): @@ -39,7 +39,7 @@ order_book = OrderBook(symbols=SYMBOLS) bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) @bfx.wss.on("wss-error") -def on_wss_error(code: Errors, msg: str): +def on_wss_error(code: Error, msg: str): print(code, msg) @bfx.wss.on("open") diff --git a/examples/websocket/raw_order_book.py b/examples/websocket/raw_order_book.py index b34ae8e..5a65d78 100644 --- a/examples/websocket/raw_order_book.py +++ b/examples/websocket/raw_order_book.py @@ -5,7 +5,7 @@ from typing import List from bfxapi import Client, Constants from bfxapi.websocket import BfxWebsocketClient -from bfxapi.websocket.enums import Channels, Errors +from bfxapi.websocket.enums import Channels, Error from bfxapi.websocket.typings import Subscriptions, TradingPairRawBook class RawOrderBook(object): @@ -39,7 +39,7 @@ raw_order_book = RawOrderBook(symbols=SYMBOLS) bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) @bfx.wss.on("wss-error") -def on_wss_error(code: Errors, msg: str): +def on_wss_error(code: Error, msg: str): print(code, msg) @bfx.wss.on("open") From 18f9fef12de60bb70ce545ca6735d4bd1ab10fb1 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 22 Dec 2022 18:48:23 +0100 Subject: [PATCH 28/38] Fix some mypy errors and warnings. --- bfxapi/notification.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bfxapi/notification.py b/bfxapi/notification.py index 003867b..4dd618f 100644 --- a/bfxapi/notification.py +++ b/bfxapi/notification.py @@ -1,4 +1,4 @@ -from typing import Dict, Optional, Any, TypedDict, cast +from typing import List, Dict, Optional, Any, TypedDict, cast from .labeler import _Serializer @@ -19,10 +19,10 @@ class _Notification(_Serializer): self.serializer = serializer - def parse(self, *values: Any) -> Notification: + def parse(self, *values: Any, skip: Optional[List[str]] = None) -> Notification: notification = dict(self._serialize(*values)) - if self.serializer != None: - notification["NOTIFY_INFO"] = dict(self.serializer._serialize(*notification["NOTIFY_INFO"])) + if isinstance(self.serializer, _Serializer): + notification["NOTIFY_INFO"] = dict(self.serializer._serialize(*notification["NOTIFY_INFO"], skip=skip)) return cast(Notification, notification) \ No newline at end of file From db4438144d25308d3cf4aee9d283b69a906fee62 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 22 Dec 2022 18:57:57 +0100 Subject: [PATCH 29/38] Add new values in Error enumeration (bxapi/enums.py) according to new documentation update. --- bfxapi/enums.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bfxapi/enums.py b/bfxapi/enums.py index 2293ff9..fce3754 100644 --- a/bfxapi/enums.py +++ b/bfxapi/enums.py @@ -38,5 +38,8 @@ class Error(int, Enum): ERR_UNAUTH_FAIL = 10200 ERR_SUB_FAIL = 10300 ERR_SUB_MULTI = 10301 + ERR_SUB_UNK = 10302 + ERR_SUB_LIMIT = 10305 ERR_UNSUB_FAIL = 10400 + ERR_UNSUB_NOT = 10401 ERR_READY = 11000 \ No newline at end of file From 72a3252e32a1871ad71b536eec858f5be865a7ae Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 23 Dec 2022 16:36:51 +0100 Subject: [PATCH 30/38] Add support for new rest authenticated endpoints. --- bfxapi/notification.py | 14 ++++---- bfxapi/rest/BfxRestInterface.py | 60 ++++++++++++++++++++++++++++----- bfxapi/rest/serializers.py | 27 +++++++++++++++ bfxapi/rest/typings.py | 22 ++++++++++++ bfxapi/utils/integers.py | 12 +++++-- 5 files changed, 118 insertions(+), 17 deletions(-) diff --git a/bfxapi/notification.py b/bfxapi/notification.py index 4dd618f..5bb4cab 100644 --- a/bfxapi/notification.py +++ b/bfxapi/notification.py @@ -1,4 +1,4 @@ -from typing import List, Dict, Optional, Any, TypedDict, cast +from typing import List, Dict, Union, Optional, Any, TypedDict, cast from .labeler import _Serializer @@ -6,7 +6,7 @@ class Notification(TypedDict): MTS: int TYPE: str MESSAGE_ID: Optional[int] - NOTIFY_INFO: Dict[str, Any] + NOTIFY_INFO: Union[Dict[str, Any], List[Dict[str, Any]]] CODE: Optional[int] STATUS: str TEXT: str @@ -14,15 +14,17 @@ class Notification(TypedDict): class _Notification(_Serializer): __LABELS = [ "MTS", "TYPE", "MESSAGE_ID", "_PLACEHOLDER", "NOTIFY_INFO", "CODE", "STATUS", "TEXT" ] - def __init__(self, serializer: Optional[_Serializer] = None): + def __init__(self, serializer: Optional[_Serializer] = None, iterate: bool = False): super().__init__("Notification", _Notification.__LABELS, IGNORE = [ "_PLACEHOLDER" ]) - self.serializer = serializer + self.serializer, self.iterate = serializer, iterate def parse(self, *values: Any, skip: Optional[List[str]] = None) -> Notification: notification = dict(self._serialize(*values)) if isinstance(self.serializer, _Serializer): - notification["NOTIFY_INFO"] = dict(self.serializer._serialize(*notification["NOTIFY_INFO"], skip=skip)) - + if self.iterate == False: + notification["NOTIFY_INFO"] = dict(self.serializer._serialize(*notification["NOTIFY_INFO"], skip=skip)) + else: notification["NOTIFY_INFO"] = [ dict(self.serializer._serialize(*data, skip=skip)) for data in notification["NOTIFY_INFO"] ] + return cast(Notification, notification) \ No newline at end of file diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 31b529c..c44033a 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -12,7 +12,7 @@ from .typings import * from .enums import Config, Precision, Sort, OrderType, Error from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError -from .. utils.integers import Int16, Int32, Int45, Int64 +from .. utils.integers import Int16, int32, int45, int64 from .. utils.encoder import JSONEncoder @@ -246,10 +246,10 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.Order.parse(*subdata) for subdata in self._POST("auth/r/orders", data={ "id": ids }) ] def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str], - price: Optional[Union[Decimal, str]] = None, lev: Optional[Union[Int32, int]] = None, + 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[Union[Int32, int]] = None, cid: Optional[Union[Int45, int]] = None, - flags: Optional[Union[Int16, int]] = None, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification: + gid: Optional[int] = None, cid: Optional[int] = None, + flags: Optional[int] = None, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification: data = { "type": type, "symbol": symbol, "amount": amount, "price": price, "lev": lev, @@ -260,9 +260,9 @@ class _RestAuthenticatedEndpoints(_Requests): return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/submit", data=data)) - def update_order(self, id: Union[Int64, int], amount: Optional[Union[Decimal, str]] = None, price: Optional[Union[Decimal, str]] = None, - cid: Optional[Union[Int45, int]] = None, cid_date: Optional[str] = None, gid: Optional[Union[Int32, int]] = None, - flags: Optional[Union[Int16, int]] = None, lev: Optional[Union[Int32, int]] = None, delta: Optional[Union[Decimal, str]] = None, + 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] = None, lev: Optional[int] = None, delta: Optional[Union[Decimal, str]] = None, price_aux_limit: Optional[Union[Decimal, str]] = None, price_trailing: Optional[Union[Decimal, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification: data = { "id": id, "amount": amount, "price": price, @@ -273,5 +273,47 @@ class _RestAuthenticatedEndpoints(_Requests): return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/update", data=data)) - def cancel_order(self, id: Union[Int64, int]) -> Notification: - return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/cancel", data={ "id": id })) \ No newline at end of file + def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None) -> Notification: + data = { + "id": id, + "cid": cid, + "cid_date": cid_date + } + + return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/cancel", data=data)) + + def cancel_order_multi(self, ids: Optional[List[int]] = None, cids: Optional[List[Tuple[int, str]]] = None, gids: Optional[List[int]] = None, all: bool = False) -> Notification: + data = { + "ids": ids, + "cids": cids, + "gids": gids, + + "all": int(all) + } + + return serializers._Notification(serializer=serializers.Order, iterate=True).parse(*self._POST("auth/w/order/cancel/multi", data=data)) + + def orders_history(self, symbol: Optional[str] = None, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Order]: + if symbol == None: + endpoint = "auth/r/orders/hist" + else: endpoint = f"auth/r/orders/{symbol}/hist" + + data = { + "id": ids, + "start": start, "end": end, + "limit": limit + } + + return [ serializers.Order.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] + + def trades(self, symbol: str) -> List[Trade]: + return [ serializers.Trade.parse(*subdata) for subdata in self._POST(f"auth/r/trades/{symbol}/hist") ] + + def ledgers(self, currency: str, category: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Ledger]: + data = { + "category": category, + "start": start, "end": end, + "limit": limit + } + + return [ serializers.Ledger.parse(*subdata) for subdata in self._POST(f"auth/r/ledgers/{currency}/hist", data=data) ] \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 3abea89..9c4a320 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -234,4 +234,31 @@ Order = _Serializer[typings.Order]("Order", labels=[ "META" ]) +Trade = _Serializer[typings.Trade]("Trade", labels=[ + "ID", + "PAIR", + "MTS_CREATE", + "ORDER_ID", + "EXEC_AMOUNT", + "EXEC_PRICE", + "ORDER_TYPE", + "ORDER_PRICE", + "MAKER", + "FEE", + "FEE_CURRENCY", + "CID" +]) + +Ledger = _Serializer[typings.Ledger]("Ledger", labels=[ + "ID", + "CURRENCY", + "_PLACEHOLDER", + "MTS", + "_PLACEHOLDER", + "AMOUNT", + "BALANCE", + "_PLACEHOLDER", + "DESCRIPTION" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index f99b7c1..88c4a4c 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -169,4 +169,26 @@ class Order(TypedDict): ROUTING: str META: JSON +class Trade(TypedDict): + ID: int + SYMBOL: str + MTS_CREATE: int + ORDER_ID: int + EXEC_AMOUNT: float + EXEC_PRICE: float + ORDER_TYPE: str + ORDER_PRICE: float + MAKER:int + FEE: float + FEE_CURRENCY: str + CID: int + +class Ledger(TypedDict): + ID: int + CURRENCY: str + MTS: int + AMOUNT: float + BALANCE: float + description: str + #endregion \ No newline at end of file diff --git a/bfxapi/utils/integers.py b/bfxapi/utils/integers.py index e38f107..08582c6 100644 --- a/bfxapi/utils/integers.py +++ b/bfxapi/utils/integers.py @@ -1,4 +1,4 @@ -from typing import cast, TypeVar +from typing import cast, TypeVar, Union from .. exceptions import IntegerUnderflowError, IntegerOverflowflowError @@ -25,11 +25,19 @@ class _Int(int): 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 \ No newline at end of file + _BITS = 64 + +int64 = Union[Int64, int] \ No newline at end of file From ef836bbe1aa21403e565e13904bcb4dd60be7e23 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Fri, 6 Jan 2023 15:18:57 +0100 Subject: [PATCH 31/38] Add funding related rest endpoints, refactor pre-existent rest endpoints to use get_ prefix. Add function to calculate flags easily. Add example test to create a funding offer. --- bfxapi/client.py | 16 ++++- bfxapi/enums.py | 5 ++ bfxapi/rest/BfxRestInterface.py | 91 ++++++++++++++++----------- bfxapi/rest/serializers.py | 24 +++++++ bfxapi/rest/typings.py | 16 +++++ bfxapi/utils/flags.py | 29 +++++++++ examples/rest/create_funding_offer.py | 26 ++++++++ 7 files changed, 170 insertions(+), 37 deletions(-) create mode 100644 bfxapi/utils/flags.py create mode 100644 examples/rest/create_funding_offer.py diff --git a/bfxapi/client.py b/bfxapi/client.py index 75c3f2a..e866235 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -1,3 +1,4 @@ +from .rest import BfxRestInterface from .websocket import BfxWebsocketClient from typing import Optional @@ -12,7 +13,20 @@ class Constants(str, Enum): PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2" class Client(object): - def __init__(self, WSS_HOST: str = Constants.WSS_HOST, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None, log_level: str = "WARNING"): + def __init__( + self, + REST_HOST: str = Constants.REST_HOST, + WSS_HOST: str = Constants.WSS_HOST, + API_KEY: Optional[str] = None, + API_SECRET: Optional[str] = None, + log_level: str = "WARNING" + ): + self.rest = BfxRestInterface( + host=REST_HOST, + API_KEY=API_KEY, + API_SECRET=API_SECRET + ) + self.wss = BfxWebsocketClient( host=WSS_HOST, API_KEY=API_KEY, diff --git a/bfxapi/enums.py b/bfxapi/enums.py index fce3754..03b89bf 100644 --- a/bfxapi/enums.py +++ b/bfxapi/enums.py @@ -16,6 +16,11 @@ class OrderType(str, Enum): IOC = "IOC" EXCHANGE_IOC = "EXCHANGE IOC" +class FundingOfferType(str, Enum): + LIMIT = "LIMIT" + FRR_DELTA_FIX = "FRRDELTAFIX" + FRR_DELTA_VAR = "FRRDELTAVAR" + class Flag(int, Enum): HIDDEN = 64 CLOSE = 512 diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index c44033a..404eeaf 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -9,7 +9,7 @@ from typing import List, Union, Literal, Optional, Any, cast from . import serializers from .typings import * -from .enums import Config, Precision, Sort, OrderType, Error +from .enums import Config, Precision, Sort, OrderType, FundingOfferType, Error from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError from .. utils.integers import Int16, int32, int45, int64 @@ -84,39 +84,39 @@ class _Requests(object): return data class _RestPublicEndpoints(_Requests): - def platform_status(self) -> PlatformStatus: + def get_platform_status(self) -> PlatformStatus: return serializers.PlatformStatus.parse(*self._GET("platform/status")) - def tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]: + def get_tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]: data = self._GET("tickers", params={ "symbols": ",".join(symbols) }) parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse } return [ parsers[subdata[0][0]](*subdata) for subdata in data ] - def t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]: + 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.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.tickers([ "t" + pair for pair in pairs ]) + data = self.get_tickers([ "t" + pair for pair in pairs ]) return cast(List[TradingPairTicker], data) - def f_tickers(self, currencies: Union[List[str], Literal["ALL"]]) -> List[FundingCurrencyTicker]: + 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.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.tickers([ "f" + currency for currency in currencies ]) + data = self.get_tickers([ "f" + currency for currency in currencies ]) return cast(List[FundingCurrencyTicker], data) - def t_ticker(self, pair: str) -> TradingPairTicker: + def get_t_ticker(self, pair: str) -> TradingPairTicker: return serializers.TradingPairTicker.parse(*self._GET(f"ticker/t{pair}"), skip=["SYMBOL"]) - def f_ticker(self, currency: str) -> FundingCurrencyTicker: + def get_f_ticker(self, currency: str) -> FundingCurrencyTicker: return serializers.FundingCurrencyTicker.parse(*self._GET(f"ticker/f{currency}"), skip=["SYMBOL"]) - def tickers_history(self, symbols: List[str], start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[TickersHistory]: + def get_tickers_history(self, symbols: List[str], start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[TickersHistory]: params = { "symbols": ",".join(symbols), "start": start, "end": end, @@ -127,29 +127,29 @@ class _RestPublicEndpoints(_Requests): return [ serializers.TickersHistory.parse(*subdata) for subdata in data ] - def t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[TradingPairTrade]: + def get_t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[TradingPairTrade]: params = { "limit": limit, "start": start, "end": end, "sort": sort } data = self._GET(f"trades/{'t' + pair}/hist", params=params) return [ serializers.TradingPairTrade.parse(*subdata) for subdata in data ] - def f_trades(self, currency: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[FundingCurrencyTrade]: + def get_f_trades(self, currency: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[FundingCurrencyTrade]: params = { "limit": limit, "start": start, "end": end, "sort": sort } data = self._GET(f"trades/{'f' + currency}/hist", params=params) return [ serializers.FundingCurrencyTrade.parse(*subdata) for subdata in data ] - def t_book(self, pair: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairBook]: + def get_t_book(self, pair: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairBook]: return [ serializers.TradingPairBook.parse(*subdata) for subdata in self._GET(f"book/{'t' + pair}/{precision}", params={ "len": len }) ] - def f_book(self, currency: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyBook]: + def get_f_book(self, currency: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyBook]: return [ serializers.FundingCurrencyBook.parse(*subdata) for subdata in self._GET(f"book/{'f' + currency}/{precision}", params={ "len": len }) ] - def t_raw_book(self, pair: str, len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairRawBook]: + def get_t_raw_book(self, pair: str, len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairRawBook]: return [ serializers.TradingPairRawBook.parse(*subdata) for subdata in self._GET(f"book/{'t' + pair}/R0", params={ "len": len }) ] - def f_raw_book(self, currency: str, len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyRawBook]: + def get_f_raw_book(self, currency: str, len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyRawBook]: return [ serializers.FundingCurrencyRawBook.parse(*subdata) for subdata in self._GET(f"book/{'f' + currency}/R0", params={ "len": len }) ] - def stats_hist( + def get_stats_hist( self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None @@ -158,7 +158,7 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"stats1/{resource}/hist", params=params) return [ serializers.Statistic.parse(*subdata) for subdata in data ] - def stats_last( + def get_stats_last( self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None @@ -167,7 +167,7 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"stats1/{resource}/last", params=params) return serializers.Statistic.parse(*data) - def candles_hist( + def get_candles_hist( self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None @@ -176,7 +176,7 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"candles/{resource}/hist", params=params) return [ serializers.Candle.parse(*subdata) for subdata in data ] - def candles_last( + def get_candles_last( self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None @@ -185,14 +185,14 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"candles/{resource}/last", params=params) return serializers.Candle.parse(*data) - def derivatives_status(self, type: str, keys: List[str]) -> List[DerivativesStatus]: + def get_derivatives_status(self, type: str, keys: List[str]) -> List[DerivativesStatus]: params = { "keys": ",".join(keys) } data = self._GET(f"status/{type}", params=params) return [ serializers.DerivativesStatus.parse(*subdata) for subdata in data ] - def derivatives_status_history( + 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 @@ -203,14 +203,14 @@ class _RestPublicEndpoints(_Requests): return [ serializers.DerivativesStatus.parse(*subdata, skip=[ "KEY" ]) for subdata in data ] - def liquidations(self, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Liquidation]: + def get_liquidations(self, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Liquidation]: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self._GET("liquidations/hist", params=params) return [ serializers.Liquidation.parse(*subdata[0]) for subdata in data ] - def leaderboards_hist( + def get_leaderboards_hist( self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None @@ -219,7 +219,7 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"rankings/{resource}/hist", params=params) return [ serializers.Leaderboard.parse(*subdata) for subdata in data ] - def leaderboards_last( + def get_leaderboards_last( self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None @@ -228,7 +228,7 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"rankings/{resource}/last", params=params) return serializers.Leaderboard.parse(*data) - def funding_stats(self, symbol: str, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingStatistic]: + 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) @@ -239,17 +239,17 @@ class _RestPublicEndpoints(_Requests): return self._GET(f"conf/{config}")[0] class _RestAuthenticatedEndpoints(_Requests): - def wallets(self) -> List[Wallet]: + def get_wallets(self) -> List[Wallet]: return [ serializers.Wallet.parse(*subdata) for subdata in self._POST("auth/r/wallets") ] - def retrieve_orders(self, ids: Optional[List[str]] = None) -> List[Order]: + def get_orders(self, ids: Optional[List[str]] = None) -> List[Order]: return [ serializers.Order.parse(*subdata) for subdata in self._POST("auth/r/orders", data={ "id": ids }) ] def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str], price: Optional[Union[Decimal, str]] = None, lev: Optional[int] = None, price_trailing: Optional[Union[Decimal, str]] = None, price_aux_limit: Optional[Union[Decimal, str]] = None, price_oco_stop: Optional[Union[Decimal, str]] = None, gid: Optional[int] = None, cid: Optional[int] = None, - flags: Optional[int] = None, 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: data = { "type": type, "symbol": symbol, "amount": amount, "price": price, "lev": lev, @@ -262,7 +262,7 @@ class _RestAuthenticatedEndpoints(_Requests): 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] = None, lev: Optional[int] = None, delta: Optional[Union[Decimal, str]] = None, + flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, str]] = None, price_aux_limit: Optional[Union[Decimal, str]] = None, price_trailing: Optional[Union[Decimal, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification: data = { "id": id, "amount": amount, "price": price, @@ -293,7 +293,7 @@ class _RestAuthenticatedEndpoints(_Requests): return serializers._Notification(serializer=serializers.Order, iterate=True).parse(*self._POST("auth/w/order/cancel/multi", data=data)) - def 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]: + 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" @@ -306,14 +306,33 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.Order.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] - def trades(self, symbol: str) -> List[Trade]: + def get_trades(self, symbol: str) -> List[Trade]: return [ serializers.Trade.parse(*subdata) for subdata in self._POST(f"auth/r/trades/{symbol}/hist") ] - def ledgers(self, currency: str, category: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Ledger]: + def get_ledgers(self, currency: str, category: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Ledger]: data = { "category": category, "start": start, "end": end, "limit": limit } - return [ serializers.Ledger.parse(*subdata) for subdata in self._POST(f"auth/r/ledgers/{currency}/hist", data=data) ] \ No newline at end of file + return [ serializers.Ledger.parse(*subdata) for subdata in self._POST(f"auth/r/ledgers/{currency}/hist", data=data) ] + + def get_active_funding_offers(self, symbol: Optional[str] = None) -> List[FundingOffer]: + endpoint = "auth/r/funding/offers" + + if symbol != None: + endpoint += f"/{symbol}" + + return [ serializers.FundingOffer.parse(*subdata) for subdata in self._POST(endpoint) ] + + def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, str], + rate: Union[Decimal, str], period: int, + flags: Optional[int] = 0) -> Notification: + data = { + "type": type, "symbol": symbol, "amount": amount, + "rate": rate, "period": period, + "flags": flags + } + + return serializers._Notification(serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/submit", data=data)) \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 9c4a320..ad0f2d1 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -234,6 +234,30 @@ Order = _Serializer[typings.Order]("Order", labels=[ "META" ]) +FundingOffer = _Serializer[typings.FundingOffer]("FundingOffer", labels=[ + "ID", + "SYMBOL", + "MTS_CREATED", + "MTS_UPDATED", + "AMOUNT", + "AMOUNT_ORIG", + "OFFER_TYPE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "FLAGS", + "OFFER_STATUS", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "RATE", + "PERIOD", + "NOTIFY", + "HIDDEN", + "_PLACEHOLDER", + "RENEW", + "_PLACEHOLDER" +]) + Trade = _Serializer[typings.Trade]("Trade", labels=[ "ID", "PAIR", diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index 88c4a4c..20b2eed 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -169,6 +169,22 @@ class Order(TypedDict): ROUTING: str META: JSON +class FundingOffer(TypedDict): + ID: int + SYMBOL: str + MTS_CREATE: int + MTS_UPDATE: int + AMOUNT: float + AMOUNT_ORIG: float + OFFER_TYPE: str + FLAGS: int + OFFER_STATUS: str + RATE: float + PERIOD: int + NOTIFY: bool + HIDDEN: int + RENEW: bool + class Trade(TypedDict): ID: int SYMBOL: str diff --git a/bfxapi/utils/flags.py b/bfxapi/utils/flags.py new file mode 100644 index 0000000..f897103 --- /dev/null +++ b/bfxapi/utils/flags.py @@ -0,0 +1,29 @@ +from .. enums import Flag + +def calculate_order_flags( + hidden : bool = False, + close : bool = False, + reduce_only : bool = False, + post_only : bool = False, + oco : bool = False, + no_var_rates: bool = False +) -> int: + flags = 0 + + if hidden: flags += Flag.HIDDEN + if close: flags += Flag.CLOSE + if reduce_only: flags += Flag.REDUCE_ONLY + if post_only: flags += Flag.POST_ONLY + if oco: flags += Flag.OCO + if no_var_rates: flags += Flag.NO_VAR_RATES + + return flags + +def calculate_offer_flags( + hidden : bool = False +) -> int: + flags = 0 + + if hidden: flags += Flag.HIDDEN + + return flags \ No newline at end of file diff --git a/examples/rest/create_funding_offer.py b/examples/rest/create_funding_offer.py new file mode 100644 index 0000000..008d481 --- /dev/null +++ b/examples/rest/create_funding_offer.py @@ -0,0 +1,26 @@ +import os + +from bfxapi.client import Client, Constants +from bfxapi.utils.flags import calculate_offer_flags +from bfxapi.rest.typings import List, FundingOffer, Notification + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +notification: Notification = bfx.rest.auth.submit_funding_offer( + type="LIMIT", + symbol="fUSD", + amount="123.45", + rate="0.001", + period=2, + flags=calculate_offer_flags(hidden=True) +) + +print("Offer notification:", notification) + +offers: List[FundingOffer] = bfx.rest.auth.get_active_funding_offers() + +print("Offers:", offers) \ No newline at end of file From 22f6fe01fdd1b38035a52dab9f702588f859443b Mon Sep 17 00:00:00 2001 From: itsdeka Date: Fri, 6 Jan 2023 18:22:39 +0100 Subject: [PATCH 32/38] Add example to submit, cancel, edit order and adjust issue in labeler.py --- bfxapi/rest/BfxRestInterface.py | 8 +++--- bfxapi/utils/cid.py | 4 +++ examples/rest/create_funding_offer.py | 8 +++--- examples/rest/create_order.py | 36 +++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 bfxapi/utils/cid.py create mode 100644 examples/rest/create_order.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 404eeaf..02b2575 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -9,11 +9,9 @@ from typing import List, Union, Literal, Optional, Any, cast from . import serializers from .typings import * -from .enums import Config, Precision, Sort, OrderType, FundingOfferType, Error +from .enums import Config, Sort, OrderType, FundingOfferType, Error from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError -from .. utils.integers import Int16, int32, int45, int64 - from .. utils.encoder import JSONEncoder class BfxRestInterface(object): @@ -64,7 +62,9 @@ class _Requests(object): if _append_authentication_headers: headers = { **headers, **self.__build_authentication_headers(endpoint, data) } - response = requests.post(f"{self.host}/{endpoint}", params=params, data=json.dumps(data, cls=JSONEncoder), headers=headers) + data = (data and json.dumps({ key: value for key, value in data.items() if value != None}, cls=JSONEncoder) or None) + + 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}>.") diff --git a/bfxapi/utils/cid.py b/bfxapi/utils/cid.py new file mode 100644 index 0000000..43150bb --- /dev/null +++ b/bfxapi/utils/cid.py @@ -0,0 +1,4 @@ +import time + +def generate_unique_cid(multiplier: int = 1000) -> int: + return int(round(time.time() * multiplier)) diff --git a/examples/rest/create_funding_offer.py b/examples/rest/create_funding_offer.py index 008d481..ecd470b 100644 --- a/examples/rest/create_funding_offer.py +++ b/examples/rest/create_funding_offer.py @@ -1,8 +1,8 @@ import os from bfxapi.client import Client, Constants +from bfxapi.enums import FundingOfferType from bfxapi.utils.flags import calculate_offer_flags -from bfxapi.rest.typings import List, FundingOffer, Notification bfx = Client( REST_HOST=Constants.REST_HOST, @@ -10,8 +10,8 @@ bfx = Client( API_SECRET=os.getenv("BFX_API_SECRET") ) -notification: Notification = bfx.rest.auth.submit_funding_offer( - type="LIMIT", +notification = bfx.rest.auth.submit_funding_offer( + type=FundingOfferType.LIMIT, symbol="fUSD", amount="123.45", rate="0.001", @@ -21,6 +21,6 @@ notification: Notification = bfx.rest.auth.submit_funding_offer( print("Offer notification:", notification) -offers: List[FundingOffer] = bfx.rest.auth.get_active_funding_offers() +offers = bfx.rest.auth.get_active_funding_offers() print("Offers:", offers) \ No newline at end of file diff --git a/examples/rest/create_order.py b/examples/rest/create_order.py new file mode 100644 index 0000000..34408aa --- /dev/null +++ b/examples/rest/create_order.py @@ -0,0 +1,36 @@ +import os + +from bfxapi.client import Client, Constants +from bfxapi.enums import OrderType +from bfxapi.utils.flags import calculate_order_flags + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +# Create a new order +submitted_order = bfx.rest.auth.submit_order( + type=OrderType.EXCHANGE_LIMIT, + symbol="tBTCUST", + amount="0.015", + price="10000", + flags=calculate_order_flags(hidden=False) +) + +print("Submit Order Notification:", submitted_order) + +# Update it +updated_order = bfx.rest.auth.update_order( + id=submitted_order["NOTIFY_INFO"]["ID"], + amount="0.020", + price="10100" +) + +print("Update Order Notification:", updated_order) + +# Delete it +canceled_order = bfx.rest.auth.cancel_order(id=submitted_order["NOTIFY_INFO"]["ID"]) + +print("Cancel Order Notification:", canceled_order) From 10862aea79a0b38a3da834d5b3c04c4c8cd88fdd Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 10 Jan 2023 18:19:30 +0100 Subject: [PATCH 33/38] Fix bug in bfxapi/notifications.py. --- bfxapi/notification.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bfxapi/notification.py b/bfxapi/notification.py index 5bb4cab..90d2f12 100644 --- a/bfxapi/notification.py +++ b/bfxapi/notification.py @@ -24,7 +24,12 @@ class _Notification(_Serializer): if isinstance(self.serializer, _Serializer): if self.iterate == False: - notification["NOTIFY_INFO"] = dict(self.serializer._serialize(*notification["NOTIFY_INFO"], skip=skip)) + NOTIFY_INFO = notification["NOTIFY_INFO"] + + if len(NOTIFY_INFO) == 1 and isinstance(NOTIFY_INFO[0], list): + NOTIFY_INFO = NOTIFY_INFO[0] + + notification["NOTIFY_INFO"] = dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip)) else: notification["NOTIFY_INFO"] = [ dict(self.serializer._serialize(*data, skip=skip)) for data in notification["NOTIFY_INFO"] ] return cast(Notification, notification) \ No newline at end of file From bb79a58ee5929c9889ebb490920e2b7ff845b2fd Mon Sep 17 00:00:00 2001 From: itsdeka Date: Wed, 11 Jan 2023 10:35:09 +0100 Subject: [PATCH 34/38] Fix mistakes in BfxRestInterface.py --- bfxapi/rest/BfxRestInterface.py | 120 +++++++++++++++++++++++--------- 1 file changed, 86 insertions(+), 34 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 02b2575..11a395b 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -13,6 +13,7 @@ from .enums import Config, Sort, OrderType, FundingOfferType, Error from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError from .. utils.encoder import JSONEncoder +from .. utils.cid import generate_unique_cid class BfxRestInterface(object): def __init__(self, host, API_KEY = None, API_SECRET = None): @@ -30,7 +31,7 @@ class _Requests(object): signature = hmac.new( self.API_SECRET.encode("utf8"), f"/api/v2/{endpoint}{nonce}{json.dumps(data)}".encode("utf8"), - hashlib.sha384 + hashlib.sha384 ).hexdigest() return { @@ -41,7 +42,7 @@ class _Requests(object): 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}>.") @@ -62,10 +63,8 @@ class _Requests(object): if _append_authentication_headers: headers = { **headers, **self.__build_authentication_headers(endpoint, data) } - data = (data and json.dumps({ key: value for key, value in data.items() if value != None}, cls=JSONEncoder) or None) + response = requests.post(f"{self.host}/{endpoint}", params=params, data=json.dumps(data, cls=JSONEncoder), headers=headers) - 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}>.") @@ -89,9 +88,9 @@ class _RestPublicEndpoints(_Requests): def get_tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]: data = self._GET("tickers", params={ "symbols": ",".join(symbols) }) - + parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse } - + return [ parsers[subdata[0][0]](*subdata) for subdata in data ] def get_t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]: @@ -124,7 +123,7 @@ class _RestPublicEndpoints(_Requests): } data = self._GET("tickers/hist", params=params) - + return [ serializers.TickersHistory.parse(*subdata) for subdata in data ] def get_t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[TradingPairTrade]: @@ -150,7 +149,7 @@ class _RestPublicEndpoints(_Requests): return [ serializers.FundingCurrencyRawBook.parse(*subdata) for subdata in self._GET(f"book/{'f' + currency}/R0", params={ "len": len }) ] def get_stats_hist( - self, + self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> List[Statistic]: @@ -159,7 +158,7 @@ class _RestPublicEndpoints(_Requests): return [ serializers.Statistic.parse(*subdata) for subdata in data ] def get_stats_last( - self, + self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> Statistic: @@ -193,10 +192,10 @@ class _RestPublicEndpoints(_Requests): return [ serializers.DerivativesStatus.parse(*subdata) for subdata in data ] def get_derivatives_status_history( - self, + self, type: str, symbol: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> List[DerivativesStatus]: + ) -> List[DerivativesStatus]: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self._GET(f"status/{type}/{symbol}/hist", params=params) @@ -245,39 +244,92 @@ class _RestAuthenticatedEndpoints(_Requests): def get_orders(self, ids: Optional[List[str]] = None) -> List[Order]: return [ serializers.Order.parse(*subdata) for subdata in self._POST("auth/r/orders", data={ "id": ids }) ] - def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str], - price: Optional[Union[Decimal, str]] = None, lev: Optional[int] = None, + def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str], + price: Optional[Union[Decimal, str]] = None, lev: Optional[int] = None, price_trailing: Optional[Union[Decimal, str]] = None, price_aux_limit: Optional[Union[Decimal, str]] = None, price_oco_stop: Optional[Union[Decimal, str]] = None, - gid: Optional[int] = None, cid: Optional[int] = None, + gid: Optional[int] = None, flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification: + cid = generate_unique_cid() + 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 + "type": type, "symbol": symbol, "amount": str(amount), "price": str(price), "meta": meta, "cid": cid } - + + # add extra parameters + if flags: + data["flags"] = flags + + if price_trailing: + data["price_trailing"] = str(price_trailing) + + if price_aux_limit: + data["price_aux_limit"] = str(price_aux_limit) + + if price_oco_stop: + data["oco_stop_price"] = str(price_oco_stop) + + if tif: + data["tif"] = str(tif) + + if gid: + data["gid"] = gid + + if lev: + data["lev"] = str(lev) + return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/submit", data=data)) - def update_order(self, id: int, amount: Optional[Union[Decimal, str]] = None, price: Optional[Union[Decimal, str]] = None, + 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: 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 + "id": id } - + + if amount: + data["amount"] = str(amount) + + if price: + data["price"] = str(price) + + if cid: + data["cid"] = cid + + if cid_date: + data["cid_date"] = str(cid_date) + + if gid: + data["gid"] = gid + + if flags: + data["flags"] = flags + + if lev: + data["lev"] = str(lev) + + if delta: + data["deta"] = str(delta) + + if price_aux_limit: + data["price_aux_limit"] = str(price_aux_limit) + + if price_trailing: + data["price_trailing"] = str(price_trailing) + + if tif: + data["tif"] = str(tif) + return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/update", data=data)) def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None) -> Notification: - data = { - "id": id, - "cid": cid, - "cid_date": cid_date + data = { + "id": id, + "cid": cid, + "cid_date": cid_date } return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/cancel", data=data)) @@ -297,7 +349,7 @@ class _RestAuthenticatedEndpoints(_Requests): if symbol == None: endpoint = "auth/r/orders/hist" else: endpoint = f"auth/r/orders/{symbol}/hist" - + data = { "id": ids, "start": start, "end": end, @@ -331,7 +383,7 @@ class _RestAuthenticatedEndpoints(_Requests): flags: Optional[int] = 0) -> Notification: data = { "type": type, "symbol": symbol, "amount": amount, - "rate": rate, "period": period, + "rate": rate, "period": period, "flags": flags } From 44ba7e780a2f06be2efc0fdd8383bf407e60e806 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Wed, 11 Jan 2023 11:51:16 +0100 Subject: [PATCH 35/38] Add wss exmaple to create order, refactoring --- examples/websocket/create_order.py | 43 ++++++++++++++++++++++++++++ examples/websocket/order_book.py | 3 +- examples/websocket/raw_order_book.py | 3 +- examples/websocket/ticker.py | 4 ++- 4 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 examples/websocket/create_order.py diff --git a/examples/websocket/create_order.py b/examples/websocket/create_order.py new file mode 100644 index 0000000..7c7be64 --- /dev/null +++ b/examples/websocket/create_order.py @@ -0,0 +1,43 @@ +# python -c "from examples.websocket.create_order import *" + +import os + +from bfxapi.client import Client, Constants +from bfxapi.utils.cid import generate_unique_cid +from bfxapi.websocket.enums import Error, OrderType +from bfxapi.websocket.typings import Inputs + +bfx = Client( + WSS_HOST=Constants.WSS_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +@bfx.wss.on("wss-error") +def on_wss_error(code: Error, msg: str): + print(code, msg) +@bfx.wss.on("authenticated") +async def on_open(event): + print(f"Auth event {event}") + + order: Inputs.Order.New = { + "gid": generate_unique_cid(), + "type": OrderType.EXCHANGE_LIMIT, + "symbol": "tBTCUST", + "amount": "0.1", + "price": "10000.0" + } + await bfx.wss.inputs.order_new(order) + + print(f"Order sent") +@bfx.wss.on("notification") +async def on_notification(notification): + print(f"Notification {notification}") +@bfx.wss.on("order_new") +async def on_order_new(order_new: Inputs.Order.New): + print(f"Order new {order_new}") +@bfx.wss.on("subscribed") +def on_subscribed(subscription): + print(f"Subscription successful <{subscription}>") + +bfx.wss.run() \ No newline at end of file diff --git a/examples/websocket/order_book.py b/examples/websocket/order_book.py index 372a3f6..0035cf8 100644 --- a/examples/websocket/order_book.py +++ b/examples/websocket/order_book.py @@ -1,10 +1,11 @@ +# python -c "from examples.websocket.order_book import *" + from collections import OrderedDict from typing import List from bfxapi import Client, Constants -from bfxapi.websocket import BfxWebsocketClient from bfxapi.websocket.enums import Channels, Error from bfxapi.websocket.typings import Subscriptions, TradingPairBook diff --git a/examples/websocket/raw_order_book.py b/examples/websocket/raw_order_book.py index 5a65d78..6cfc3c1 100644 --- a/examples/websocket/raw_order_book.py +++ b/examples/websocket/raw_order_book.py @@ -1,10 +1,11 @@ +# python -c "from examples.websocket.raw_order_book import *" + from collections import OrderedDict from typing import List from bfxapi import Client, Constants -from bfxapi.websocket import BfxWebsocketClient from bfxapi.websocket.enums import Channels, Error from bfxapi.websocket.typings import Subscriptions, TradingPairRawBook diff --git a/examples/websocket/ticker.py b/examples/websocket/ticker.py index ff8d899..5db8ed1 100644 --- a/examples/websocket/ticker.py +++ b/examples/websocket/ticker.py @@ -1,3 +1,5 @@ +# python -c "from examples.websocket.ticker import *" + import asyncio from bfxapi import Client, Constants @@ -16,4 +18,4 @@ def on_t_ticker_update(subscription: Subscriptions.TradingPairTicker, data: Trad async def open(): await bfx.wss.subscribe(Channels.TICKER, symbol="tBTCUSD") -asyncio.run(bfx.wss.start()) \ No newline at end of file +bfx.wss.run() \ No newline at end of file From f9f72a4ebbecaadcf7e7e6decc394a4ea581af2d Mon Sep 17 00:00:00 2001 From: itsdeka Date: Wed, 11 Jan 2023 11:51:41 +0100 Subject: [PATCH 36/38] Add lines --- examples/websocket/create_order.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/websocket/create_order.py b/examples/websocket/create_order.py index 7c7be64..15ba521 100644 --- a/examples/websocket/create_order.py +++ b/examples/websocket/create_order.py @@ -30,12 +30,15 @@ async def on_open(event): await bfx.wss.inputs.order_new(order) print(f"Order sent") + @bfx.wss.on("notification") async def on_notification(notification): print(f"Notification {notification}") + @bfx.wss.on("order_new") async def on_order_new(order_new: Inputs.Order.New): print(f"Order new {order_new}") + @bfx.wss.on("subscribed") def on_subscribed(subscription): print(f"Subscription successful <{subscription}>") From 1ff16e26b96201e76be09e7176282a4a2e7b6a22 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Wed, 11 Jan 2023 11:51:59 +0100 Subject: [PATCH 37/38] Add lines --- examples/websocket/create_order.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/websocket/create_order.py b/examples/websocket/create_order.py index 15ba521..36cd5c2 100644 --- a/examples/websocket/create_order.py +++ b/examples/websocket/create_order.py @@ -16,6 +16,7 @@ bfx = Client( @bfx.wss.on("wss-error") def on_wss_error(code: Error, msg: str): print(code, msg) + @bfx.wss.on("authenticated") async def on_open(event): print(f"Auth event {event}") From 99726b8e2543ae7208aa29198fcb41e275bb4dc1 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 11 Jan 2023 16:43:40 +0100 Subject: [PATCH 38/38] Roll-back to previous BfxRestInterface.py code. Remove CID enforcement with generate_unique_cid. Fix small bug in Requests._POST method. --- bfxapi/rest/BfxRestInterface.py | 129 ++++++++++---------------------- 1 file changed, 41 insertions(+), 88 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 11a395b..c11f7af 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -13,7 +13,6 @@ from .enums import Config, Sort, OrderType, FundingOfferType, Error from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError from .. utils.encoder import JSONEncoder -from .. utils.cid import generate_unique_cid class BfxRestInterface(object): def __init__(self, host, API_KEY = None, API_SECRET = None): @@ -28,10 +27,14 @@ class _Requests(object): 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"), - f"/api/v2/{endpoint}{nonce}{json.dumps(data)}".encode("utf8"), - hashlib.sha384 + path.encode("utf8"), + hashlib.sha384 ).hexdigest() return { @@ -42,7 +45,7 @@ class _Requests(object): 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}>.") @@ -60,11 +63,14 @@ class _Requests(object): def _POST(self, endpoint, params = None, data = None, _append_authentication_headers = True): headers = { "Content-Type": "application/json" } + if isinstance(data, dict): + data = json.dumps({ key: value for key, value in data.items() if value != None}, cls=JSONEncoder) + if _append_authentication_headers: headers = { **headers, **self.__build_authentication_headers(endpoint, data) } - response = requests.post(f"{self.host}/{endpoint}", params=params, data=json.dumps(data, cls=JSONEncoder), headers=headers) - + 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}>.") @@ -88,9 +94,9 @@ class _RestPublicEndpoints(_Requests): def get_tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]: data = self._GET("tickers", params={ "symbols": ",".join(symbols) }) - + parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse } - + return [ parsers[subdata[0][0]](*subdata) for subdata in data ] def get_t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]: @@ -123,7 +129,7 @@ class _RestPublicEndpoints(_Requests): } data = self._GET("tickers/hist", params=params) - + return [ serializers.TickersHistory.parse(*subdata) for subdata in data ] def get_t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[TradingPairTrade]: @@ -149,7 +155,7 @@ class _RestPublicEndpoints(_Requests): return [ serializers.FundingCurrencyRawBook.parse(*subdata) for subdata in self._GET(f"book/{'f' + currency}/R0", params={ "len": len }) ] def get_stats_hist( - self, + self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> List[Statistic]: @@ -158,7 +164,7 @@ class _RestPublicEndpoints(_Requests): return [ serializers.Statistic.parse(*subdata) for subdata in data ] def get_stats_last( - self, + self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> Statistic: @@ -192,10 +198,10 @@ class _RestPublicEndpoints(_Requests): return [ serializers.DerivativesStatus.parse(*subdata) for subdata in data ] def get_derivatives_status_history( - self, + self, type: str, symbol: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> List[DerivativesStatus]: + ) -> List[DerivativesStatus]: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self._GET(f"status/{type}/{symbol}/hist", params=params) @@ -244,92 +250,39 @@ class _RestAuthenticatedEndpoints(_Requests): def get_orders(self, ids: Optional[List[str]] = None) -> List[Order]: return [ serializers.Order.parse(*subdata) for subdata in self._POST("auth/r/orders", data={ "id": ids }) ] - def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str], - price: Optional[Union[Decimal, str]] = None, lev: Optional[int] = None, + def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str], + price: Optional[Union[Decimal, str]] = None, lev: Optional[int] = None, price_trailing: Optional[Union[Decimal, str]] = None, price_aux_limit: Optional[Union[Decimal, str]] = None, price_oco_stop: Optional[Union[Decimal, str]] = None, - gid: Optional[int] = None, + gid: Optional[int] = None, cid: Optional[int] = None, flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification: - cid = generate_unique_cid() - data = { - "type": type, "symbol": symbol, "amount": str(amount), "price": str(price), "meta": meta, "cid": cid + "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 } - - # add extra parameters - if flags: - data["flags"] = flags - - if price_trailing: - data["price_trailing"] = str(price_trailing) - - if price_aux_limit: - data["price_aux_limit"] = str(price_aux_limit) - - if price_oco_stop: - data["oco_stop_price"] = str(price_oco_stop) - - if tif: - data["tif"] = str(tif) - - if gid: - data["gid"] = gid - - if lev: - data["lev"] = str(lev) - + return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/submit", data=data)) - def update_order(self, id: int, amount: Optional[Union[Decimal, str]] = None, - price: Optional[Union[Decimal, str]] = None, + 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: data = { - "id": id + "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 } - - if amount: - data["amount"] = str(amount) - - if price: - data["price"] = str(price) - - if cid: - data["cid"] = cid - - if cid_date: - data["cid_date"] = str(cid_date) - - if gid: - data["gid"] = gid - - if flags: - data["flags"] = flags - - if lev: - data["lev"] = str(lev) - - if delta: - data["deta"] = str(delta) - - if price_aux_limit: - data["price_aux_limit"] = str(price_aux_limit) - - if price_trailing: - data["price_trailing"] = str(price_trailing) - - if tif: - data["tif"] = str(tif) - + return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/update", data=data)) def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None) -> Notification: - data = { - "id": id, - "cid": cid, - "cid_date": cid_date + data = { + "id": id, + "cid": cid, + "cid_date": cid_date } return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/cancel", data=data)) @@ -349,7 +302,7 @@ class _RestAuthenticatedEndpoints(_Requests): if symbol == None: endpoint = "auth/r/orders/hist" else: endpoint = f"auth/r/orders/{symbol}/hist" - + data = { "id": ids, "start": start, "end": end, @@ -383,7 +336,7 @@ class _RestAuthenticatedEndpoints(_Requests): flags: Optional[int] = 0) -> Notification: data = { "type": type, "symbol": symbol, "amount": amount, - "rate": rate, "period": period, + "rate": rate, "period": period, "flags": flags }