Apply black to all python files (bfxapi/**/*.py).

This commit is contained in:
Davide Casale
2024-02-26 19:43:14 +01:00
parent 2b7dfc5b8a
commit 38dbff1141
33 changed files with 1843 additions and 1335 deletions

View File

@@ -13,9 +13,10 @@ from bfxapi.websocket.subscriptions import Subscription
_CHECKSUM_FLAG_VALUE = 131_072
def _strip(message: Dict[str, Any], keys: List[str]) -> Dict[str, Any]:
return { key: value for key, value in message.items() \
if not key in keys }
return {key: value for key, value in message.items() if not key in keys}
class BfxWebSocketBucket(Connection):
__MAXIMUM_SUBSCRIPTIONS_AMOUNT = 25
@@ -24,28 +25,26 @@ class BfxWebSocketBucket(Connection):
super().__init__(host)
self.__event_emitter = event_emitter
self.__pendings: List[Dict[str, Any]] = [ ]
self.__subscriptions: Dict[int, Subscription] = { }
self.__pendings: List[Dict[str, Any]] = []
self.__subscriptions: Dict[int, Subscription] = {}
self.__condition = asyncio.locks.Condition()
self.__handler = PublicChannelsHandler( \
event_emitter=self.__event_emitter)
self.__handler = PublicChannelsHandler(event_emitter=self.__event_emitter)
@property
def count(self) -> int:
return len(self.__pendings) + \
len(self.__subscriptions)
return len(self.__pendings) + len(self.__subscriptions)
@property
def is_full(self) -> bool:
return self.count == \
BfxWebSocketBucket.__MAXIMUM_SUBSCRIPTIONS_AMOUNT
return self.count == BfxWebSocketBucket.__MAXIMUM_SUBSCRIPTIONS_AMOUNT
@property
def ids(self) -> List[str]:
return [ pending["subId"] for pending in self.__pendings ] + \
[ subscription["sub_id"] for subscription in self.__subscriptions.values() ]
return [pending["subId"] for pending in self.__pendings] + [
subscription["sub_id"] for subscription in self.__subscriptions.values()
]
async def start(self) -> None:
async with websockets.client.connect(self._host) as websocket:
@@ -64,20 +63,25 @@ class BfxWebSocketBucket(Connection):
self.__on_subscribed(message)
if isinstance(message, list):
if (chan_id := cast(int, message[0])) and \
(subscription := self.__subscriptions.get(chan_id)) and \
(message[1] != Connection._HEARTBEAT):
if (
(chan_id := cast(int, message[0]))
and (subscription := self.__subscriptions.get(chan_id))
and (message[1] != Connection._HEARTBEAT)
):
self.__handler.handle(subscription, message[1:])
def __on_subscribed(self, message: Dict[str, Any]) -> None:
chan_id = cast(int, message["chan_id"])
subscription = cast(Subscription, _strip(message, \
keys=["chan_id", "event", "pair", "currency"]))
subscription = cast(
Subscription, _strip(message, keys=["chan_id", "event", "pair", "currency"])
)
self.__pendings = [ pending \
for pending in self.__pendings \
if pending["subId"] != message["sub_id"] ]
self.__pendings = [
pending
for pending in self.__pendings
if pending["subId"] != message["sub_id"]
]
self.__subscriptions[chan_id] = subscription
@@ -85,47 +89,43 @@ class BfxWebSocketBucket(Connection):
async def __recover_state(self) -> None:
for pending in self.__pendings:
await self._websocket.send(message = \
json.dumps(pending))
await self._websocket.send(message=json.dumps(pending))
for chan_id in list(self.__subscriptions.keys()):
subscription = self.__subscriptions.pop(chan_id)
await self.subscribe(**subscription)
await self.__set_config([ _CHECKSUM_FLAG_VALUE ])
await self.__set_config([_CHECKSUM_FLAG_VALUE])
async def __set_config(self, flags: List[int]) -> None:
await self._websocket.send(json.dumps( \
{ "event": "conf", "flags": sum(flags) }))
await self._websocket.send(json.dumps({"event": "conf", "flags": sum(flags)}))
@Connection._require_websocket_connection
async def subscribe(self,
channel: str,
sub_id: Optional[str] = None,
**kwargs: Any) -> None:
subscription: Dict[str, Any] = \
{ **kwargs, "event": "subscribe", "channel": channel }
async def subscribe(
self, channel: str, sub_id: Optional[str] = None, **kwargs: Any
) -> None:
subscription: Dict[str, Any] = {
**kwargs,
"event": "subscribe",
"channel": channel,
}
subscription["subId"] = sub_id or str(uuid.uuid4())
self.__pendings.append(subscription)
await self._websocket.send(message = \
json.dumps(subscription))
await self._websocket.send(message=json.dumps(subscription))
@Connection._require_websocket_connection
async def unsubscribe(self, sub_id: str) -> None:
for chan_id, subscription in list(self.__subscriptions.items()):
if subscription["sub_id"] == sub_id:
unsubscription = {
"event": "unsubscribe",
"chanId": chan_id }
unsubscription = {"event": "unsubscribe", "chanId": chan_id}
del self.__subscriptions[chan_id]
await self._websocket.send(message = \
json.dumps(unsubscription))
await self._websocket.send(message=json.dumps(unsubscription))
@Connection._require_websocket_connection
async def resubscribe(self, sub_id: str) -> None:
@@ -148,5 +148,4 @@ class BfxWebSocketBucket(Connection):
async def wait(self) -> None:
async with self.__condition:
await self.__condition \
.wait_for(lambda: self.open)
await self.__condition.wait_for(lambda: self.open)

View File

@@ -28,14 +28,17 @@ from bfxapi.websocket.exceptions import (
from .bfx_websocket_bucket import BfxWebSocketBucket
from .bfx_websocket_inputs import BfxWebSocketInputs
_Credentials = TypedDict("_Credentials", \
{ "api_key": str, "api_secret": str, "filters": Optional[List[str]] })
_Credentials = TypedDict(
"_Credentials", {"api_key": str, "api_secret": str, "filters": Optional[List[str]]}
)
_Reconnection = TypedDict("_Reconnection",
{ "attempts": int, "reason": str, "timestamp": datetime })
_Reconnection = TypedDict(
"_Reconnection", {"attempts": int, "reason": str, "timestamp": datetime}
)
_DEFAULT_LOGGER = Logger("bfxapi.websocket._client", level=0)
class _Delay:
__BACKOFF_MIN = 1.92
@@ -54,59 +57,61 @@ class _Delay:
return _backoff_delay
def peek(self) -> float:
return (self.__backoff_delay == _Delay.__BACKOFF_MIN) \
and self.__initial_delay or self.__backoff_delay
return (
(self.__backoff_delay == _Delay.__BACKOFF_MIN)
and self.__initial_delay
or self.__backoff_delay
)
def reset(self) -> None:
self.__backoff_delay = _Delay.__BACKOFF_MIN
#pylint: disable-next=too-many-instance-attributes
# pylint: disable-next=too-many-instance-attributes
class BfxWebSocketClient(Connection):
def __init__(self,
host: str,
*,
credentials: Optional[_Credentials] = None,
timeout: Optional[int] = 60 * 15,
logger: Logger = _DEFAULT_LOGGER) -> None:
def __init__(
self,
host: str,
*,
credentials: Optional[_Credentials] = None,
timeout: Optional[int] = 60 * 15,
logger: Logger = _DEFAULT_LOGGER,
) -> None:
super().__init__(host)
self.__credentials, self.__timeout, self.__logger = \
credentials, \
timeout, \
logger
self.__credentials, self.__timeout, self.__logger = credentials, timeout, logger
self.__buckets: Dict[BfxWebSocketBucket, Optional[Task]] = { }
self.__buckets: Dict[BfxWebSocketBucket, Optional[Task]] = {}
self.__reconnection: Optional[_Reconnection] = None
self.__event_emitter = BfxEventEmitter(loop=None)
self.__handler = AuthEventsHandler( \
event_emitter=self.__event_emitter)
self.__handler = AuthEventsHandler(event_emitter=self.__event_emitter)
self.__inputs = BfxWebSocketInputs( \
handle_websocket_input=self.__handle_websocket_input)
self.__inputs = BfxWebSocketInputs(
handle_websocket_input=self.__handle_websocket_input
)
@self.__event_emitter.listens_to("error")
def error(exception: Exception) -> None:
header = f"{type(exception).__name__}: {str(exception)}"
stack_trace = traceback.format_exception( \
type(exception), exception, exception.__traceback__)
stack_trace = traceback.format_exception(
type(exception), exception, exception.__traceback__
)
#pylint: disable-next=logging-not-lazy
self.__logger.critical(header + "\n" + \
str().join(stack_trace)[:-1])
# pylint: disable-next=logging-not-lazy
self.__logger.critical(header + "\n" + str().join(stack_trace)[:-1])
@property
def inputs(self) -> BfxWebSocketInputs:
return self.__inputs
def run(self) -> None:
return asyncio.get_event_loop() \
.run_until_complete(self.start())
return asyncio.get_event_loop().run_until_complete(self.start())
#pylint: disable-next=too-many-branches
# pylint: disable-next=too-many-branches
async def start(self) -> None:
_delay = _Delay(backoff_factor=1.618)
@@ -119,19 +124,20 @@ class BfxWebSocketClient(Connection):
while True:
if self.__reconnection:
_sleep = asyncio.create_task( \
asyncio.sleep(int(_delay.next())))
_sleep = asyncio.create_task(asyncio.sleep(int(_delay.next())))
try:
await _sleep
except asyncio.CancelledError:
raise ReconnectionTimeoutError("Connection has been offline for too long " \
f"without being able to reconnect (timeout: {self.__timeout}s).") \
from None
raise ReconnectionTimeoutError(
"Connection has been offline for too long "
f"without being able to reconnect (timeout: {self.__timeout}s)."
) from None
try:
await self.__connect()
except (ConnectionClosedError, InvalidStatusCode, gaierror) as error:
async def _cancel(task: Task) -> None:
task.cancel()
@@ -150,69 +156,87 @@ class BfxWebSocketClient(Connection):
await _cancel(task)
if isinstance(error, ConnectionClosedError) and error.code in (1006, 1012):
if isinstance(error, ConnectionClosedError) and error.code in (
1006,
1012,
):
if error.code == 1006:
self.__logger.error("Connection lost: trying to reconnect...")
if error.code == 1012:
self.__logger.warning("WSS server is restarting: all " \
"clients need to reconnect (server sent 20051).")
self.__logger.warning(
"WSS server is restarting: all "
"clients need to reconnect (server sent 20051)."
)
if self.__timeout:
asyncio.get_event_loop().call_later(
self.__timeout, _on_timeout)
asyncio.get_event_loop().call_later(self.__timeout, _on_timeout)
self.__reconnection = \
{ "attempts": 1, "reason": error.reason, "timestamp": datetime.now() }
self.__reconnection = {
"attempts": 1,
"reason": error.reason,
"timestamp": datetime.now(),
}
self._authentication = False
_delay.reset()
elif ((isinstance(error, InvalidStatusCode) and error.status_code == 408) or \
isinstance(error, gaierror)) and self.__reconnection:
#pylint: disable-next=logging-fstring-interpolation
self.__logger.warning("Reconnection attempt unsuccessful (no." \
f"{self.__reconnection['attempts']}): next attempt in " \
f"~{int(_delay.peek())}.0s.")
elif (
(isinstance(error, InvalidStatusCode) and error.status_code == 408)
or isinstance(error, gaierror)
) and self.__reconnection:
# pylint: disable-next=logging-fstring-interpolation
self.__logger.warning(
"Reconnection attempt unsuccessful (no."
f"{self.__reconnection['attempts']}): next attempt in "
f"~{int(_delay.peek())}.0s."
)
#pylint: disable-next=logging-fstring-interpolation
self.__logger.info(f"The client has been offline for " \
f"{datetime.now() - self.__reconnection['timestamp']}.")
# pylint: disable-next=logging-fstring-interpolation
self.__logger.info(
f"The client has been offline for "
f"{datetime.now() - self.__reconnection['timestamp']}."
)
self.__reconnection["attempts"] += 1
else:
raise error
if not self.__reconnection:
self.__event_emitter.emit("disconnected",
self._websocket.close_code, \
self._websocket.close_reason)
self.__event_emitter.emit(
"disconnected",
self._websocket.close_code,
self._websocket.close_reason,
)
break
async def __connect(self) -> None:
async with websockets.client.connect(self._host) as websocket:
if self.__reconnection:
#pylint: disable-next=logging-fstring-interpolation
self.__logger.warning("Reconnection attempt successful (no." \
f"{self.__reconnection['attempts']}): recovering " \
"connection state...")
# pylint: disable-next=logging-fstring-interpolation
self.__logger.warning(
"Reconnection attempt successful (no."
f"{self.__reconnection['attempts']}): recovering "
"connection state..."
)
self.__reconnection = None
self._websocket = websocket
for bucket in self.__buckets:
self.__buckets[bucket] = \
asyncio.create_task(bucket.start())
self.__buckets[bucket] = asyncio.create_task(bucket.start())
if len(self.__buckets) == 0 or \
(await asyncio.gather(*[bucket.wait() for bucket in self.__buckets])):
if len(self.__buckets) == 0 or (
await asyncio.gather(*[bucket.wait() for bucket in self.__buckets])
):
self.__event_emitter.emit("open")
if self.__credentials:
authentication = Connection. \
_get_authentication_message(**self.__credentials)
authentication = Connection._get_authentication_message(
**self.__credentials
)
await self._websocket.send(authentication)
@@ -222,61 +246,64 @@ class BfxWebSocketClient(Connection):
if isinstance(message, dict):
if message["event"] == "info" and "version" in message:
if message["version"] != 2:
raise VersionMismatchError("Mismatch between the client and the server version: " + \
"please update bitfinex-api-py to the latest version to resolve this error " + \
f"(client version: 2, server version: {message['version']}).")
raise VersionMismatchError(
"Mismatch between the client and the server version: "
+ "please update bitfinex-api-py to the latest version to resolve this error "
+ f"(client version: 2, server version: {message['version']})."
)
elif message["event"] == "info" and message["code"] == 20051:
rcvd = websockets.frames.Close( \
1012, "Stop/Restart WebSocket Server (please reconnect).")
rcvd = websockets.frames.Close(
1012, "Stop/Restart WebSocket Server (please reconnect)."
)
raise ConnectionClosedError(rcvd=rcvd, sent=None)
elif message["event"] == "auth":
if message["status"] != "OK":
raise InvalidCredentialError("Can't authenticate " + \
"with given API-KEY and API-SECRET.")
raise InvalidCredentialError(
"Can't authenticate "
+ "with given API-KEY and API-SECRET."
)
self.__event_emitter.emit("authenticated", message)
self._authentication = True
if isinstance(message, list) and \
message[0] == 0 and message[1] != Connection._HEARTBEAT:
if (
isinstance(message, list)
and message[0] == 0
and message[1] != Connection._HEARTBEAT
):
self.__handler.handle(message[1], message[2])
async def __new_bucket(self) -> BfxWebSocketBucket:
bucket = BfxWebSocketBucket( \
self._host, self.__event_emitter)
bucket = BfxWebSocketBucket(self._host, self.__event_emitter)
self.__buckets[bucket] = asyncio \
.create_task(bucket.start())
self.__buckets[bucket] = asyncio.create_task(bucket.start())
await bucket.wait()
return bucket
@Connection._require_websocket_connection
async def subscribe(self,
channel: str,
sub_id: Optional[str] = None,
**kwargs: Any) -> None:
async def subscribe(
self, channel: str, sub_id: Optional[str] = None, **kwargs: Any
) -> None:
if not channel in ["ticker", "trades", "book", "candles", "status"]:
raise UnknownChannelError("Available channels are: " + \
"ticker, trades, book, candles and status.")
raise UnknownChannelError(
"Available channels are: " + "ticker, trades, book, candles and status."
)
for bucket in self.__buckets:
if sub_id in bucket.ids:
raise SubIdError("sub_id must be " + \
"unique for all subscriptions.")
raise SubIdError("sub_id must be " + "unique for all subscriptions.")
for bucket in self.__buckets:
if not bucket.is_full:
return await bucket.subscribe( \
channel, sub_id, **kwargs)
return await bucket.subscribe(channel, sub_id, **kwargs)
bucket = await self.__new_bucket()
return await bucket.subscribe( \
channel, sub_id, **kwargs)
return await bucket.subscribe(channel, sub_id, **kwargs)
@Connection._require_websocket_connection
async def unsubscribe(self, sub_id: str) -> None:
@@ -286,13 +313,13 @@ class BfxWebSocketClient(Connection):
if bucket.count == 1:
del self.__buckets[bucket]
return await bucket.close( \
code=1001, reason="Going Away")
return await bucket.close(code=1001, reason="Going Away")
return await bucket.unsubscribe(sub_id)
raise UnknownSubscriptionError("Unable to find " + \
f"a subscription with sub_id <{sub_id}>.")
raise UnknownSubscriptionError(
"Unable to find " + f"a subscription with sub_id <{sub_id}>."
)
@Connection._require_websocket_connection
async def resubscribe(self, sub_id: str) -> None:
@@ -300,8 +327,9 @@ class BfxWebSocketClient(Connection):
if bucket.has(sub_id):
return await bucket.resubscribe(sub_id)
raise UnknownSubscriptionError("Unable to find " + \
f"a subscription with sub_id <{sub_id}>.")
raise UnknownSubscriptionError(
"Unable to find " + f"a subscription with sub_id <{sub_id}>."
)
@Connection._require_websocket_connection
async def close(self, code: int = 1000, reason: str = str()) -> None:
@@ -309,22 +337,21 @@ class BfxWebSocketClient(Connection):
await bucket.close(code=code, reason=reason)
if self._websocket.open:
await self._websocket.close( \
code=code, reason=reason)
await self._websocket.close(code=code, reason=reason)
@Connection._require_websocket_authentication
async def notify(self,
info: Any,
message_id: Optional[int] = None,
**kwargs: Any) -> None:
async def notify(
self, info: Any, message_id: Optional[int] = None, **kwargs: Any
) -> None:
await self._websocket.send(
json.dumps([ 0, "n", message_id,
{ "type": "ucm-test", "info": info, **kwargs } ]))
json.dumps(
[0, "n", message_id, {"type": "ucm-test", "info": info, **kwargs}]
)
)
@Connection._require_websocket_authentication
async def __handle_websocket_input(self, event: str, data: Any) -> None:
await self._websocket.send(json.dumps( \
[ 0, event, None, data], cls=JSONEncoder))
await self._websocket.send(json.dumps([0, event, None, data], cls=JSONEncoder))
def on(self, event, callback = None):
def on(self, event, callback=None):
return self.__event_emitter.on(event, callback)

View File

@@ -3,89 +3,127 @@ from typing import Any, Awaitable, Callable, List, Optional, Tuple, Union
_Handler = Callable[[str, Any], Awaitable[None]]
class BfxWebSocketInputs:
def __init__(self, handle_websocket_input: _Handler) -> None:
self.__handle_websocket_input = handle_websocket_input
async def submit_order(self,
type: str,
symbol: str,
amount: Union[str, float, Decimal],
price: Union[str, float, Decimal],
*,
lev: Optional[int] = None,
price_trailing: Optional[Union[str, float, Decimal]] = None,
price_aux_limit: Optional[Union[str, float, Decimal]] = None,
price_oco_stop: Optional[Union[str, float, Decimal]] = None,
gid: Optional[int] = None,
cid: Optional[int] = None,
flags: Optional[int] = None,
tif: Optional[str] = None) -> None:
await self.__handle_websocket_input("on", {
"type": type, "symbol": symbol, "amount": amount,
"price": price, "lev": lev, "price_trailing": price_trailing,
"price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop, "gid": gid,
"cid": cid, "flags": flags, "tif": tif
})
async def submit_order(
self,
type: str,
symbol: str,
amount: Union[str, float, Decimal],
price: Union[str, float, Decimal],
*,
lev: Optional[int] = None,
price_trailing: Optional[Union[str, float, Decimal]] = None,
price_aux_limit: Optional[Union[str, float, Decimal]] = None,
price_oco_stop: Optional[Union[str, float, Decimal]] = None,
gid: Optional[int] = None,
cid: Optional[int] = None,
flags: Optional[int] = None,
tif: Optional[str] = None,
) -> None:
await self.__handle_websocket_input(
"on",
{
"type": type,
"symbol": symbol,
"amount": amount,
"price": price,
"lev": lev,
"price_trailing": price_trailing,
"price_aux_limit": price_aux_limit,
"price_oco_stop": price_oco_stop,
"gid": gid,
"cid": cid,
"flags": flags,
"tif": tif,
},
)
async def update_order(self,
id: int,
*,
amount: Optional[Union[str, float, Decimal]] = None,
price: Optional[Union[str, float, Decimal]] = 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[str, float, Decimal]] = None,
price_aux_limit: Optional[Union[str, float, Decimal]] = None,
price_trailing: Optional[Union[str, float, Decimal]] = None,
tif: Optional[str] = None) -> None:
await self.__handle_websocket_input("ou", {
"id": id, "amount": amount, "price": price,
"cid": cid, "cid_date": cid_date, "gid": gid,
"flags": flags, "lev": lev, "delta": delta,
"price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif
})
async def update_order(
self,
id: int,
*,
amount: Optional[Union[str, float, Decimal]] = None,
price: Optional[Union[str, float, Decimal]] = 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[str, float, Decimal]] = None,
price_aux_limit: Optional[Union[str, float, Decimal]] = None,
price_trailing: Optional[Union[str, float, Decimal]] = None,
tif: Optional[str] = None,
) -> None:
await self.__handle_websocket_input(
"ou",
{
"id": id,
"amount": amount,
"price": price,
"cid": cid,
"cid_date": cid_date,
"gid": gid,
"flags": flags,
"lev": lev,
"delta": delta,
"price_aux_limit": price_aux_limit,
"price_trailing": price_trailing,
"tif": tif,
},
)
async def cancel_order(self,
*,
id: Optional[int] = None,
cid: Optional[int] = None,
cid_date: Optional[str] = None) -> None:
await self.__handle_websocket_input("oc", {
"id": id, "cid": cid, "cid_date": cid_date
})
async def cancel_order(
self,
*,
id: Optional[int] = None,
cid: Optional[int] = None,
cid_date: Optional[str] = None,
) -> None:
await self.__handle_websocket_input(
"oc", {"id": id, "cid": cid, "cid_date": cid_date}
)
async def cancel_order_multi(self,
*,
id: Optional[List[int]] = None,
cid: Optional[List[Tuple[int, str]]] = None,
gid: Optional[List[int]] = None,
all: Optional[bool] = None) -> None:
await self.__handle_websocket_input("oc_multi", {
"id": id, "cid": cid, "gid": gid,
"all": all
})
async def cancel_order_multi(
self,
*,
id: Optional[List[int]] = None,
cid: Optional[List[Tuple[int, str]]] = None,
gid: Optional[List[int]] = None,
all: Optional[bool] = None,
) -> None:
await self.__handle_websocket_input(
"oc_multi", {"id": id, "cid": cid, "gid": gid, "all": all}
)
#pylint: disable-next=too-many-arguments
async def submit_funding_offer(self,
type: str,
symbol: str,
amount: Union[str, float, Decimal],
rate: Union[str, float, Decimal],
period: int,
*,
flags: Optional[int] = None) -> None:
await self.__handle_websocket_input("fon", {
"type": type, "symbol": symbol, "amount": amount,
"rate": rate, "period": period, "flags": flags
})
# pylint: disable-next=too-many-arguments
async def submit_funding_offer(
self,
type: str,
symbol: str,
amount: Union[str, float, Decimal],
rate: Union[str, float, Decimal],
period: int,
*,
flags: Optional[int] = None,
) -> None:
await self.__handle_websocket_input(
"fon",
{
"type": type,
"symbol": symbol,
"amount": amount,
"rate": rate,
"period": period,
"flags": flags,
},
)
async def cancel_funding_offer(self, id: int) -> None:
await self.__handle_websocket_input("foc", { "id": id })
await self.__handle_websocket_input("foc", {"id": id})
async def calc(self, *args: str) -> None:
await self.__handle_websocket_input("calc",
list(map(lambda arg: [arg], args)))
await self.__handle_websocket_input("calc", list(map(lambda arg: [arg], args)))

View File

@@ -18,6 +18,7 @@ _R = TypeVar("_R")
_P = ParamSpec("_P")
class Connection(ABC):
_HEARTBEAT = "hb"
@@ -30,8 +31,7 @@ class Connection(ABC):
@property
def open(self) -> bool:
return self.__protocol is not None and \
self.__protocol.open
return self.__protocol is not None and self.__protocol.open
@property
def authentication(self) -> bool:
@@ -46,12 +46,11 @@ class Connection(ABC):
self.__protocol = protocol
@abstractmethod
async def start(self) -> None:
...
async def start(self) -> None: ...
@staticmethod
def _require_websocket_connection(
function: Callable[Concatenate[_S, _P], Awaitable[_R]]
function: Callable[Concatenate[_S, _P], Awaitable[_R]],
) -> Callable[Concatenate[_S, _P], Awaitable[_R]]:
@wraps(function)
async def wrapper(self: _S, *args: Any, **kwargs: Any) -> _R:
@@ -64,13 +63,15 @@ class Connection(ABC):
@staticmethod
def _require_websocket_authentication(
function: Callable[Concatenate[_S, _P], Awaitable[_R]]
function: Callable[Concatenate[_S, _P], Awaitable[_R]],
) -> Callable[Concatenate[_S, _P], Awaitable[_R]]:
@wraps(function)
async def wrapper(self: _S, *args: Any, **kwargs: Any) -> _R:
if not self.authentication:
raise ActionRequiresAuthentication("To perform this action you need to " \
"authenticate using your API_KEY and API_SECRET.")
raise ActionRequiresAuthentication(
"To perform this action you need to "
"authenticate using your API_KEY and API_SECRET."
)
internal = Connection._require_websocket_connection(function)
@@ -80,12 +81,13 @@ class Connection(ABC):
@staticmethod
def _get_authentication_message(
api_key: str,
api_secret: str,
filters: Optional[List[str]] = None
api_key: str, api_secret: str, filters: Optional[List[str]] = None
) -> str:
message: Dict[str, Any] = \
{ "event": "auth", "filter": filters, "apiKey": api_key }
message: Dict[str, Any] = {
"event": "auth",
"filter": filters,
"apiKey": api_key,
}
message["authNonce"] = round(datetime.now().timestamp() * 1_000_000)
@@ -94,7 +96,7 @@ class Connection(ABC):
auth_sig = hmac.new(
key=api_secret.encode("utf8"),
msg=message["authPayload"].encode("utf8"),
digestmod=hashlib.sha384
digestmod=hashlib.sha384,
)
message["authSig"] = auth_sig.hexdigest()

View File

@@ -9,57 +9,86 @@ from bfxapi.websocket.exceptions import UnknownEventError
_Handler = TypeVar("_Handler", bound=Callable[..., None])
_ONCE_PER_CONNECTION = [
"open", "authenticated", "order_snapshot",
"position_snapshot", "funding_offer_snapshot", "funding_credit_snapshot",
"funding_loan_snapshot", "wallet_snapshot"
"open",
"authenticated",
"order_snapshot",
"position_snapshot",
"funding_offer_snapshot",
"funding_credit_snapshot",
"funding_loan_snapshot",
"wallet_snapshot",
]
_ONCE_PER_SUBSCRIPTION = [
"subscribed", "t_trades_snapshot", "f_trades_snapshot",
"t_book_snapshot", "f_book_snapshot", "t_raw_book_snapshot",
"f_raw_book_snapshot", "candles_snapshot"
"subscribed",
"t_trades_snapshot",
"f_trades_snapshot",
"t_book_snapshot",
"f_book_snapshot",
"t_raw_book_snapshot",
"f_raw_book_snapshot",
"candles_snapshot",
]
_COMMON = [
"disconnected", "t_ticker_update", "f_ticker_update",
"t_trade_execution", "t_trade_execution_update", "f_trade_execution",
"f_trade_execution_update", "t_book_update", "f_book_update",
"t_raw_book_update", "f_raw_book_update", "candles_update",
"derivatives_status_update", "liquidation_feed_update", "checksum",
"order_new", "order_update", "order_cancel",
"position_new", "position_update", "position_close",
"funding_offer_new", "funding_offer_update", "funding_offer_cancel",
"funding_credit_new", "funding_credit_update", "funding_credit_close",
"funding_loan_new", "funding_loan_update", "funding_loan_close",
"trade_execution", "trade_execution_update", "wallet_update",
"notification", "on-req-notification", "ou-req-notification",
"oc-req-notification", "fon-req-notification", "foc-req-notification"
"disconnected",
"t_ticker_update",
"f_ticker_update",
"t_trade_execution",
"t_trade_execution_update",
"f_trade_execution",
"f_trade_execution_update",
"t_book_update",
"f_book_update",
"t_raw_book_update",
"f_raw_book_update",
"candles_update",
"derivatives_status_update",
"liquidation_feed_update",
"checksum",
"order_new",
"order_update",
"order_cancel",
"position_new",
"position_update",
"position_close",
"funding_offer_new",
"funding_offer_update",
"funding_offer_cancel",
"funding_credit_new",
"funding_credit_update",
"funding_credit_close",
"funding_loan_new",
"funding_loan_update",
"funding_loan_close",
"trade_execution",
"trade_execution_update",
"wallet_update",
"notification",
"on-req-notification",
"ou-req-notification",
"oc-req-notification",
"fon-req-notification",
"foc-req-notification",
]
class BfxEventEmitter(AsyncIOEventEmitter):
_EVENTS = _ONCE_PER_CONNECTION + \
_ONCE_PER_SUBSCRIPTION + \
_COMMON
_EVENTS = _ONCE_PER_CONNECTION + _ONCE_PER_SUBSCRIPTION + _COMMON
def __init__(self, loop: Optional[AbstractEventLoop] = None) -> None:
super().__init__(loop)
self._connection: List[str] = [ ]
self._connection: List[str] = []
self._subscriptions: Dict[str, List[str]] = \
defaultdict(lambda: [ ])
self._subscriptions: Dict[str, List[str]] = defaultdict(lambda: [])
def emit(
self,
event: str,
*args: Any,
**kwargs: Any
) -> bool:
def emit(self, event: str, *args: Any, **kwargs: Any) -> bool:
if event in _ONCE_PER_CONNECTION:
if event in self._connection:
return self._has_listeners(event)
self._connection += [ event ]
self._connection += [event]
if event in _ONCE_PER_SUBSCRIPTION:
sub_id = args[0]["sub_id"]
@@ -67,7 +96,7 @@ class BfxEventEmitter(AsyncIOEventEmitter):
if event in self._subscriptions[sub_id]:
return self._has_listeners(event)
self._subscriptions[sub_id] += [ event ]
self._subscriptions[sub_id] += [event]
return super().emit(event, *args, **kwargs)
@@ -75,8 +104,10 @@ class BfxEventEmitter(AsyncIOEventEmitter):
self, event: str, f: Optional[_Handler] = None
) -> Union[_Handler, Callable[[_Handler], _Handler]]:
if event not in BfxEventEmitter._EVENTS:
raise UnknownEventError(f"Can't register to unknown event: <{event}> " + \
"(to get a full list of available events see https://docs.bitfinex.com/).")
raise UnknownEventError(
f"Can't register to unknown event: <{event}> "
+ "(to get a full list of available events see https://docs.bitfinex.com/)."
)
return super().on(event, f)

View File

@@ -9,14 +9,30 @@ from bfxapi.types.serializers import _Notification
class AuthEventsHandler:
__ABBREVIATIONS = {
"os": "order_snapshot", "on": "order_new", "ou": "order_update",
"oc": "order_cancel", "ps": "position_snapshot", "pn": "position_new",
"pu": "position_update", "pc": "position_close", "te": "trade_execution",
"tu": "trade_execution_update", "fos": "funding_offer_snapshot", "fon": "funding_offer_new",
"fou": "funding_offer_update", "foc": "funding_offer_cancel", "fcs": "funding_credit_snapshot",
"fcn": "funding_credit_new", "fcu": "funding_credit_update", "fcc": "funding_credit_close",
"fls": "funding_loan_snapshot", "fln": "funding_loan_new", "flu": "funding_loan_update",
"flc": "funding_loan_close", "ws": "wallet_snapshot", "wu": "wallet_update"
"os": "order_snapshot",
"on": "order_new",
"ou": "order_update",
"oc": "order_cancel",
"ps": "position_snapshot",
"pn": "position_new",
"pu": "position_update",
"pc": "position_close",
"te": "trade_execution",
"tu": "trade_execution_update",
"fos": "funding_offer_snapshot",
"fon": "funding_offer_new",
"fou": "funding_offer_update",
"foc": "funding_offer_cancel",
"fcs": "funding_credit_snapshot",
"fcn": "funding_credit_new",
"fcu": "funding_credit_update",
"fcc": "funding_credit_close",
"fls": "funding_loan_snapshot",
"fln": "funding_loan_new",
"flu": "funding_loan_update",
"flc": "funding_loan_close",
"ws": "wallet_snapshot",
"wu": "wallet_update",
}
__SERIALIZERS: Dict[Tuple[str, ...], serializers._Serializer] = {
@@ -26,7 +42,7 @@ class AuthEventsHandler:
("fos", "fon", "fou", "foc"): serializers.FundingOffer,
("fcs", "fcn", "fcu", "fcc"): serializers.FundingCredit,
("fls", "fln", "flu", "flc"): serializers.FundingLoan,
("ws", "wu"): serializers.Wallet
("ws", "wu"): serializers.Wallet,
}
def __init__(self, event_emitter: EventEmitter) -> None:
@@ -41,7 +57,7 @@ class AuthEventsHandler:
event = AuthEventsHandler.__ABBREVIATIONS[abbrevation]
if all(isinstance(sub_stream, list) for sub_stream in stream):
data = [ serializer.parse(*sub_stream) for sub_stream in stream ]
data = [serializer.parse(*sub_stream) for sub_stream in stream]
else:
data = serializer.parse(*stream)
@@ -53,11 +69,13 @@ class AuthEventsHandler:
serializer: _Notification = _Notification[None](serializer=None)
if stream[1] in ("on-req", "ou-req", "oc-req"):
event, serializer = f"{stream[1]}-notification", \
_Notification[Order](serializer=serializers.Order)
event, serializer = f"{stream[1]}-notification", _Notification[Order](
serializer=serializers.Order
)
if stream[1] in ("fon-req", "foc-req"):
event, serializer = f"{stream[1]}-notification", \
_Notification[FundingOffer](serializer=serializers.FundingOffer)
event, serializer = f"{stream[1]}-notification", _Notification[
FundingOffer
](serializer=serializers.FundingOffer)
self.__event_emitter.emit(event, serializer.parse(*stream))

View File

@@ -14,6 +14,7 @@ from bfxapi.websocket.subscriptions import (
_CHECKSUM = "cs"
class PublicChannelsHandler:
def __init__(self, event_emitter: EventEmitter) -> None:
self.__event_emitter = event_emitter
@@ -38,101 +39,167 @@ class PublicChannelsHandler:
elif subscription["channel"] == "status":
self.__status_channel_handler(cast(Status, subscription), stream)
#pylint: disable-next=inconsistent-return-statements
# pylint: disable-next=inconsistent-return-statements
def __ticker_channel_handler(self, subscription: Ticker, stream: List[Any]):
if subscription["symbol"].startswith("t"):
return self.__event_emitter.emit("t_ticker_update", subscription, \
serializers.TradingPairTicker.parse(*stream[0]))
return self.__event_emitter.emit(
"t_ticker_update",
subscription,
serializers.TradingPairTicker.parse(*stream[0]),
)
if subscription["symbol"].startswith("f"):
return self.__event_emitter.emit("f_ticker_update", subscription, \
serializers.FundingCurrencyTicker.parse(*stream[0]))
return self.__event_emitter.emit(
"f_ticker_update",
subscription,
serializers.FundingCurrencyTicker.parse(*stream[0]),
)
#pylint: disable-next=inconsistent-return-statements
# pylint: disable-next=inconsistent-return-statements
def __trades_channel_handler(self, subscription: Trades, stream: List[Any]):
if (event := stream[0]) and event in [ "te", "tu", "fte", "ftu" ]:
events = { "te": "t_trade_execution", "tu": "t_trade_execution_update", \
"fte": "f_trade_execution", "ftu": "f_trade_execution_update" }
if (event := stream[0]) and event in ["te", "tu", "fte", "ftu"]:
events = {
"te": "t_trade_execution",
"tu": "t_trade_execution_update",
"fte": "f_trade_execution",
"ftu": "f_trade_execution_update",
}
if subscription["symbol"].startswith("t"):
return self.__event_emitter.emit(events[event], subscription, \
serializers.TradingPairTrade.parse(*stream[1]))
return self.__event_emitter.emit(
events[event],
subscription,
serializers.TradingPairTrade.parse(*stream[1]),
)
if subscription["symbol"].startswith("f"):
return self.__event_emitter.emit(events[event], subscription, \
serializers.FundingCurrencyTrade.parse(*stream[1]))
return self.__event_emitter.emit(
events[event],
subscription,
serializers.FundingCurrencyTrade.parse(*stream[1]),
)
if subscription["symbol"].startswith("t"):
return self.__event_emitter.emit("t_trades_snapshot", subscription, \
[ serializers.TradingPairTrade.parse(*sub_stream) \
for sub_stream in stream[0] ])
return self.__event_emitter.emit(
"t_trades_snapshot",
subscription,
[
serializers.TradingPairTrade.parse(*sub_stream)
for sub_stream in stream[0]
],
)
if subscription["symbol"].startswith("f"):
return self.__event_emitter.emit("f_trades_snapshot", subscription, \
[ serializers.FundingCurrencyTrade.parse(*sub_stream) \
for sub_stream in stream[0] ])
return self.__event_emitter.emit(
"f_trades_snapshot",
subscription,
[
serializers.FundingCurrencyTrade.parse(*sub_stream)
for sub_stream in stream[0]
],
)
#pylint: disable-next=inconsistent-return-statements
# pylint: disable-next=inconsistent-return-statements
def __book_channel_handler(self, subscription: Book, stream: List[Any]):
if subscription["symbol"].startswith("t"):
if all(isinstance(sub_stream, list) for sub_stream in stream[0]):
return self.__event_emitter.emit("t_book_snapshot", subscription, \
[ serializers.TradingPairBook.parse(*sub_stream) \
for sub_stream in stream[0] ])
return self.__event_emitter.emit(
"t_book_snapshot",
subscription,
[
serializers.TradingPairBook.parse(*sub_stream)
for sub_stream in stream[0]
],
)
return self.__event_emitter.emit("t_book_update", subscription, \
serializers.TradingPairBook.parse(*stream[0]))
return self.__event_emitter.emit(
"t_book_update",
subscription,
serializers.TradingPairBook.parse(*stream[0]),
)
if subscription["symbol"].startswith("f"):
if all(isinstance(sub_stream, list) for sub_stream in stream[0]):
return self.__event_emitter.emit("f_book_snapshot", subscription, \
[ serializers.FundingCurrencyBook.parse(*sub_stream) \
for sub_stream in stream[0] ])
return self.__event_emitter.emit(
"f_book_snapshot",
subscription,
[
serializers.FundingCurrencyBook.parse(*sub_stream)
for sub_stream in stream[0]
],
)
return self.__event_emitter.emit("f_book_update", subscription, \
serializers.FundingCurrencyBook.parse(*stream[0]))
return self.__event_emitter.emit(
"f_book_update",
subscription,
serializers.FundingCurrencyBook.parse(*stream[0]),
)
#pylint: disable-next=inconsistent-return-statements
# pylint: disable-next=inconsistent-return-statements
def __raw_book_channel_handler(self, subscription: Book, stream: List[Any]):
if subscription["symbol"].startswith("t"):
if all(isinstance(sub_stream, list) for sub_stream in stream[0]):
return self.__event_emitter.emit("t_raw_book_snapshot", subscription, \
[ serializers.TradingPairRawBook.parse(*sub_stream) \
for sub_stream in stream[0] ])
return self.__event_emitter.emit(
"t_raw_book_snapshot",
subscription,
[
serializers.TradingPairRawBook.parse(*sub_stream)
for sub_stream in stream[0]
],
)
return self.__event_emitter.emit("t_raw_book_update", subscription, \
serializers.TradingPairRawBook.parse(*stream[0]))
return self.__event_emitter.emit(
"t_raw_book_update",
subscription,
serializers.TradingPairRawBook.parse(*stream[0]),
)
if subscription["symbol"].startswith("f"):
if all(isinstance(sub_stream, list) for sub_stream in stream[0]):
return self.__event_emitter.emit("f_raw_book_snapshot", subscription, \
[ serializers.FundingCurrencyRawBook.parse(*sub_stream) \
for sub_stream in stream[0] ])
return self.__event_emitter.emit(
"f_raw_book_snapshot",
subscription,
[
serializers.FundingCurrencyRawBook.parse(*sub_stream)
for sub_stream in stream[0]
],
)
return self.__event_emitter.emit("f_raw_book_update", subscription, \
serializers.FundingCurrencyRawBook.parse(*stream[0]))
return self.__event_emitter.emit(
"f_raw_book_update",
subscription,
serializers.FundingCurrencyRawBook.parse(*stream[0]),
)
#pylint: disable-next=inconsistent-return-statements
# pylint: disable-next=inconsistent-return-statements
def __candles_channel_handler(self, subscription: Candles, stream: List[Any]):
if all(isinstance(sub_stream, list) for sub_stream in stream[0]):
return self.__event_emitter.emit("candles_snapshot", subscription, \
[ serializers.Candle.parse(*sub_stream) \
for sub_stream in stream[0] ])
return self.__event_emitter.emit(
"candles_snapshot",
subscription,
[serializers.Candle.parse(*sub_stream) for sub_stream in stream[0]],
)
return self.__event_emitter.emit("candles_update", subscription, \
serializers.Candle.parse(*stream[0]))
return self.__event_emitter.emit(
"candles_update", subscription, serializers.Candle.parse(*stream[0])
)
#pylint: disable-next=inconsistent-return-statements
# pylint: disable-next=inconsistent-return-statements
def __status_channel_handler(self, subscription: Status, stream: List[Any]):
if subscription["key"].startswith("deriv:"):
return self.__event_emitter.emit("derivatives_status_update", subscription, \
serializers.DerivativesStatus.parse(*stream[0]))
return self.__event_emitter.emit(
"derivatives_status_update",
subscription,
serializers.DerivativesStatus.parse(*stream[0]),
)
if subscription["key"].startswith("liq:"):
return self.__event_emitter.emit("liquidation_feed_update", subscription, \
serializers.Liquidation.parse(*stream[0][0]))
return self.__event_emitter.emit(
"liquidation_feed_update",
subscription,
serializers.Liquidation.parse(*stream[0][0]),
)
#pylint: disable-next=inconsistent-return-statements
# pylint: disable-next=inconsistent-return-statements
def __checksum_handler(self, subscription: Book, value: int):
return self.__event_emitter.emit( \
"checksum", subscription, value & 0xFFFFFFFF)
return self.__event_emitter.emit("checksum", subscription, value & 0xFFFFFFFF)

View File

@@ -4,23 +4,30 @@ from bfxapi.exceptions import BfxBaseException
class ConnectionNotOpen(BfxBaseException):
pass
class ActionRequiresAuthentication(BfxBaseException):
pass
class ReconnectionTimeoutError(BfxBaseException):
pass
class VersionMismatchError(BfxBaseException):
pass
class SubIdError(BfxBaseException):
pass
class UnknownChannelError(BfxBaseException):
pass
class UnknownEventError(BfxBaseException):
pass
class UnknownSubscriptionError(BfxBaseException):
pass

View File

@@ -4,16 +4,19 @@ Subscription = Union["Ticker", "Trades", "Book", "Candles", "Status"]
Channel = Literal["ticker", "trades", "book", "candles", "status"]
class Ticker(TypedDict):
channel: Literal["ticker"]
sub_id: str
symbol: str
class Trades(TypedDict):
channel: Literal["trades"]
sub_id: str
symbol: str
class Book(TypedDict):
channel: Literal["book"]
sub_id: str
@@ -22,11 +25,13 @@ class Book(TypedDict):
freq: Literal["F0", "F1"]
len: Literal["1", "25", "100", "250"]
class Candles(TypedDict):
channel: Literal["candles"]
sub_id: str
key: str
class Status(TypedDict):
channel: Literal["status"]
sub_id: str