Merge pull request #36 from Davi0kProgramsThings/feature/rest

Merge branch `feature/rest` in branch `master`.
This commit is contained in:
Davide Casale
2023-02-17 20:27:31 +01:00
committed by GitHub
75 changed files with 3980 additions and 2135 deletions

View File

@@ -1,3 +1,4 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

View File

@@ -1 +1,6 @@
from .client import Client, Constants
from .client import Client
from .urls import REST_HOST, PUB_REST_HOST, STAGING_REST_HOST, \
WSS_HOST, PUB_WSS_HOST, STAGING_WSS_HOST
NAME = "bfxapi"

View File

@@ -1,35 +1,31 @@
from .rest import BfxRestInterface
from .websocket import BfxWebsocketClient
from .urls import REST_HOST, WSS_HOST
from typing import Optional
from enum import Enum
class Constants(str, Enum):
REST_HOST = "https://api.bitfinex.com/v2"
PUB_REST_HOST = "https://api-pub.bitfinex.com/v2"
WSS_HOST = "wss://api.bitfinex.com/ws/2"
PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2"
from typing import List, Optional
class Client(object):
def __init__(
self,
REST_HOST: str = Constants.REST_HOST,
WSS_HOST: str = Constants.WSS_HOST,
REST_HOST: str = REST_HOST,
WSS_HOST: str = WSS_HOST,
API_KEY: Optional[str] = None,
API_SECRET: Optional[str] = None,
log_level: str = "WARNING"
filter: Optional[List[str]] = None,
log_level: str = "INFO"
):
credentials = None
if API_KEY and API_SECRET:
credentials = { "API_KEY": API_KEY, "API_SECRET": API_SECRET, "filter": filter }
self.rest = BfxRestInterface(
host=REST_HOST,
API_KEY=API_KEY,
API_SECRET=API_SECRET
credentials=credentials
)
self.wss = BfxWebsocketClient(
host=WSS_HOST,
API_KEY=API_KEY,
API_SECRET=API_SECRET,
credentials=credentials,
log_level=log_level
)

View File

@@ -1,9 +1,7 @@
__all__ = [
"BfxBaseException",
"LabelerSerializerException",
"IntegerUnderflowError",
"IntegerOverflowflowError"
]
class BfxBaseException(Exception):
@@ -18,18 +16,4 @@ class LabelerSerializerException(BfxBaseException):
This exception indicates an error thrown by the _Serializer class in bfxapi/labeler.py.
"""
pass
class IntegerUnderflowError(BfxBaseException):
"""
This error indicates an underflow in one of the integer types defined in bfxapi/utils/integers.py.
"""
pass
class IntegerOverflowflowError(BfxBaseException):
"""
This error indicates an overflow in one of the integer types defined in bfxapi/utils/integers.py.
"""
pass

View File

@@ -1,22 +1,77 @@
from .exceptions import LabelerSerializerException
from typing import Generic, TypeVar, Iterable, Optional, List, Tuple, Any, cast
from typing import Type, Generic, TypeVar, Iterable, Optional, Dict, List, Tuple, Any, cast
T = TypeVar("T")
T = TypeVar("T", bound="_Type")
def compose(*decorators):
def wrapper(function):
for decorator in reversed(decorators):
function = decorator(function)
return function
return wrapper
def partial(cls):
def __init__(self, **kwargs):
for annotation in self.__annotations__.keys():
if annotation not in kwargs:
self.__setattr__(annotation, None)
else: self.__setattr__(annotation, kwargs[annotation])
kwargs.pop(annotation, None)
if len(kwargs) != 0:
raise TypeError(f"{cls.__name__}.__init__() got an unexpected keyword argument '{list(kwargs.keys())[0]}'")
cls.__init__ = __init__
return cls
class _Type(object):
"""
Base class for any dataclass serializable by the _Serializer generic class.
"""
pass
class _Serializer(Generic[T]):
def __init__(self, name: str, labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]):
self.name, self.__labels, self.__IGNORE = name, labels, IGNORE
def __init__(self, name: str, klass: Type[_Type], labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]):
self.name, self.klass, self.__labels, self.__IGNORE = name, klass, labels, IGNORE
def _serialize(self, *args: Any, skip: Optional[List[str]] = None) -> Iterable[Tuple[str, Any]]:
labels = list(filter(lambda label: label not in (skip or list()), self.__labels))
if len(labels) > len(args):
raise LabelerSerializerException("<labels> and <*args> arguments should contain the same amount of elements.")
raise LabelerSerializerException(f"{self.name} -> <labels> and <*args> arguments should contain the same amount of elements.")
for index, label in enumerate(labels):
if label not in self.__IGNORE:
yield label, args[index]
def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T:
return cast(T, dict(self._serialize(*values, skip=skip)))
return cast(T, self.klass(**dict(self._serialize(*values, skip=skip))))
def get_labels(self) -> List[str]:
return [ label for label in self.__labels if label not in self.__IGNORE ]
class _RecursiveSerializer(_Serializer, Generic[T]):
def __init__(self, name: str, klass: Type[_Type], labels: List[str], serializers: Dict[str, _Serializer[Any]], IGNORE: List[str] = ["_PLACEHOLDER"]):
super().__init__(name, klass, labels, IGNORE)
self.serializers = serializers
def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T:
serialization = dict(self._serialize(*values, skip=skip))
for key in serialization:
if key in self.serializers.keys():
serialization[key] = self.serializers[key].parse(*serialization[key], skip=skip)
return cast(T, self.klass(**serialization))
def generate_labeler_serializer(name: str, klass: Type[T], labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> _Serializer[T]:
return _Serializer[T](name, klass, labels, IGNORE)
def generate_recursive_serializer(name: str, klass: Type[T], labels: List[str], serializers: Dict[str, _Serializer[Any]], IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> _RecursiveSerializer[T]:
return _RecursiveSerializer[T](name, klass, labels, serializers, IGNORE)

View File

@@ -1,35 +1,38 @@
from typing import List, Dict, Union, Optional, Any, TypedDict, cast
from typing import List, Dict, Union, Optional, Any, TypedDict, Generic, TypeVar, cast
from dataclasses import dataclass
from .labeler import _Type, _Serializer
from .labeler import _Serializer
T = TypeVar("T")
class Notification(TypedDict):
MTS: int
TYPE: str
MESSAGE_ID: Optional[int]
NOTIFY_INFO: Union[Dict[str, Any], List[Dict[str, Any]]]
CODE: Optional[int]
STATUS: str
TEXT: str
@dataclass
class Notification(_Type, Generic[T]):
mts: int
type: str
message_id: Optional[int]
notify_info: T
code: Optional[int]
status: str
text: str
class _Notification(_Serializer):
__LABELS = [ "MTS", "TYPE", "MESSAGE_ID", "_PLACEHOLDER", "NOTIFY_INFO", "CODE", "STATUS", "TEXT" ]
class _Notification(_Serializer, Generic[T]):
__LABELS = [ "mts", "type", "message_id", "_PLACEHOLDER", "notify_info", "code", "status", "text" ]
def __init__(self, serializer: Optional[_Serializer] = None, iterate: bool = False):
super().__init__("Notification", _Notification.__LABELS, IGNORE = [ "_PLACEHOLDER" ])
def __init__(self, serializer: Optional[_Serializer] = None, is_iterable: bool = False):
super().__init__("Notification", Notification, _Notification.__LABELS, IGNORE = [ "_PLACEHOLDER" ])
self.serializer, self.iterate = serializer, iterate
self.serializer, self.is_iterable = serializer, is_iterable
def parse(self, *values: Any, skip: Optional[List[str]] = None) -> Notification:
notification = dict(self._serialize(*values))
def parse(self, *values: Any, skip: Optional[List[str]] = None) -> Notification[T]:
notification = cast(Notification[T], Notification(**dict(self._serialize(*values))))
if isinstance(self.serializer, _Serializer):
if self.iterate == False:
NOTIFY_INFO = notification["NOTIFY_INFO"]
NOTIFY_INFO = cast(List[Any], notification.notify_info)
if self.is_iterable == False:
if len(NOTIFY_INFO) == 1 and isinstance(NOTIFY_INFO[0], list):
NOTIFY_INFO = NOTIFY_INFO[0]
notification["NOTIFY_INFO"] = dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip))
else: notification["NOTIFY_INFO"] = [ dict(self.serializer._serialize(*data, skip=skip)) for data in notification["NOTIFY_INFO"] ]
notification.notify_info = cast(T, self.serializer.klass(**dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip))))
else: notification.notify_info = cast(T, [ self.serializer.klass(**dict(self.serializer._serialize(*data, skip=skip))) for data in NOTIFY_INFO ])
return cast(Notification, notification)
return notification

View File

@@ -1,398 +0,0 @@
import time, hmac, hashlib, json, requests
from decimal import Decimal
from datetime import datetime
from http import HTTPStatus
from typing import List, Union, Literal, Optional, Any, cast
from . import serializers
from .typings import *
from .enums import Config, Sort, OrderType, FundingOfferType, Error
from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError
from .. utils.encoder import JSONEncoder
class BfxRestInterface(object):
def __init__(self, host, API_KEY = None, API_SECRET = None):
self.public = _RestPublicEndpoints(host=host)
self.auth = _RestAuthenticatedEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET)
class _Requests(object):
def __init__(self, host, API_KEY = None, API_SECRET = None):
self.host, self.API_KEY, self.API_SECRET = host, API_KEY, API_SECRET
def __build_authentication_headers(self, endpoint, data):
nonce = str(int(time.time()) * 1000)
path = f"/api/v2/{endpoint}{nonce}"
if data != None: path += data
signature = hmac.new(
self.API_SECRET.encode("utf8"),
path.encode("utf8"),
hashlib.sha384
).hexdigest()
return {
"bfx-nonce": nonce,
"bfx-signature": signature,
"bfx-apikey": self.API_KEY
}
def _GET(self, endpoint, params = None):
response = requests.get(f"{self.host}/{endpoint}", params=params)
if response.status_code == HTTPStatus.NOT_FOUND:
raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.")
data = response.json()
if len(data) and data[0] == "error":
if data[1] == Error.ERR_PARAMS:
raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>")
if data[1] == None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC:
raise UnknownGenericError("The server replied to the request with a generic error with message: <{data[2]}>.")
return data
def _POST(self, endpoint, params = None, data = None, _append_authentication_headers = True):
headers = { "Content-Type": "application/json" }
if isinstance(data, dict):
data = json.dumps({ key: value for key, value in data.items() if value != None}, cls=JSONEncoder)
if _append_authentication_headers:
headers = { **headers, **self.__build_authentication_headers(endpoint, data) }
response = requests.post(f"{self.host}/{endpoint}", params=params, data=data, headers=headers)
if response.status_code == HTTPStatus.NOT_FOUND:
raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.")
data = response.json()
if len(data) and data[0] == "error":
if data[1] == Error.ERR_PARAMS:
raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>")
if data[1] == Error.ERR_AUTH_FAIL:
raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.")
if data[1] == None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC:
raise UnknownGenericError(f"The server replied to the request with a generic error with message: <{data[2]}>.")
return data
class _RestPublicEndpoints(_Requests):
def get_platform_status(self) -> PlatformStatus:
return serializers.PlatformStatus.parse(*self._GET("platform/status"))
def get_tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]:
data = self._GET("tickers", params={ "symbols": ",".join(symbols) })
parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse }
return [ parsers[subdata[0][0]](*subdata) for subdata in data ]
def get_t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]:
if isinstance(pairs, str) and pairs == "ALL":
return [ cast(TradingPairTicker, subdata) for subdata in self.get_tickers([ "ALL" ]) if cast(str, subdata["SYMBOL"]).startswith("t") ]
data = self.get_tickers([ "t" + pair for pair in pairs ])
return cast(List[TradingPairTicker], data)
def get_f_tickers(self, currencies: Union[List[str], Literal["ALL"]]) -> List[FundingCurrencyTicker]:
if isinstance(currencies, str) and currencies == "ALL":
return [ cast(FundingCurrencyTicker, subdata) for subdata in self.get_tickers([ "ALL" ]) if cast(str, subdata["SYMBOL"]).startswith("f") ]
data = self.get_tickers([ "f" + currency for currency in currencies ])
return cast(List[FundingCurrencyTicker], data)
def get_t_ticker(self, pair: str) -> TradingPairTicker:
return serializers.TradingPairTicker.parse(*self._GET(f"ticker/t{pair}"), skip=["SYMBOL"])
def get_f_ticker(self, currency: str) -> FundingCurrencyTicker:
return serializers.FundingCurrencyTicker.parse(*self._GET(f"ticker/f{currency}"), skip=["SYMBOL"])
def get_tickers_history(self, symbols: List[str], start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[TickersHistory]:
params = {
"symbols": ",".join(symbols),
"start": start, "end": end,
"limit": limit
}
data = self._GET("tickers/hist", params=params)
return [ serializers.TickersHistory.parse(*subdata) for subdata in data ]
def get_t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[TradingPairTrade]:
params = { "limit": limit, "start": start, "end": end, "sort": sort }
data = self._GET(f"trades/{'t' + pair}/hist", params=params)
return [ serializers.TradingPairTrade.parse(*subdata) for subdata in data ]
def get_f_trades(self, currency: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[FundingCurrencyTrade]:
params = { "limit": limit, "start": start, "end": end, "sort": sort }
data = self._GET(f"trades/{'f' + currency}/hist", params=params)
return [ serializers.FundingCurrencyTrade.parse(*subdata) for subdata in data ]
def get_t_book(self, pair: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairBook]:
return [ serializers.TradingPairBook.parse(*subdata) for subdata in self._GET(f"book/{'t' + pair}/{precision}", params={ "len": len }) ]
def get_f_book(self, currency: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyBook]:
return [ serializers.FundingCurrencyBook.parse(*subdata) for subdata in self._GET(f"book/{'f' + currency}/{precision}", params={ "len": len }) ]
def get_t_raw_book(self, pair: str, len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairRawBook]:
return [ serializers.TradingPairRawBook.parse(*subdata) for subdata in self._GET(f"book/{'t' + pair}/R0", params={ "len": len }) ]
def get_f_raw_book(self, currency: str, len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyRawBook]:
return [ serializers.FundingCurrencyRawBook.parse(*subdata) for subdata in self._GET(f"book/{'f' + currency}/R0", params={ "len": len }) ]
def get_stats_hist(
self,
resource: str,
sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None
) -> List[Statistic]:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
data = self._GET(f"stats1/{resource}/hist", params=params)
return [ serializers.Statistic.parse(*subdata) for subdata in data ]
def get_stats_last(
self,
resource: str,
sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None
) -> Statistic:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
data = self._GET(f"stats1/{resource}/last", params=params)
return serializers.Statistic.parse(*data)
def get_candles_hist(
self,
resource: str,
sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None
) -> List[Candle]:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
data = self._GET(f"candles/{resource}/hist", params=params)
return [ serializers.Candle.parse(*subdata) for subdata in data ]
def get_candles_last(
self,
resource: str,
sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None
) -> Candle:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
data = self._GET(f"candles/{resource}/last", params=params)
return serializers.Candle.parse(*data)
def get_derivatives_status(self, type: str, keys: List[str]) -> List[DerivativesStatus]:
params = { "keys": ",".join(keys) }
data = self._GET(f"status/{type}", params=params)
return [ serializers.DerivativesStatus.parse(*subdata) for subdata in data ]
def get_derivatives_status_history(
self,
type: str, symbol: str,
sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None
) -> List[DerivativesStatus]:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
data = self._GET(f"status/{type}/{symbol}/hist", params=params)
return [ serializers.DerivativesStatus.parse(*subdata, skip=[ "KEY" ]) for subdata in data ]
def get_liquidations(self, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Liquidation]:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
data = self._GET("liquidations/hist", params=params)
return [ serializers.Liquidation.parse(*subdata[0]) for subdata in data ]
def get_leaderboards_hist(
self,
resource: str,
sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None
) -> List[Leaderboard]:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
data = self._GET(f"rankings/{resource}/hist", params=params)
return [ serializers.Leaderboard.parse(*subdata) for subdata in data ]
def get_leaderboards_last(
self,
resource: str,
sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None
) -> Leaderboard:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
data = self._GET(f"rankings/{resource}/last", params=params)
return serializers.Leaderboard.parse(*data)
def get_funding_stats(self, symbol: str, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingStatistic]:
params = { "start": start, "end": end, "limit": limit }
data = self._GET(f"funding/stats/{symbol}/hist", params=params)
return [ serializers.FundingStatistic.parse(*subdata) for subdata in data ]
def conf(self, config: Config) -> Any:
return self._GET(f"conf/{config}")[0]
class _RestAuthenticatedEndpoints(_Requests):
def get_wallets(self) -> List[Wallet]:
return [ serializers.Wallet.parse(*subdata) for subdata in self._POST("auth/r/wallets") ]
def get_orders(self, symbol: Optional[str] = None, ids: Optional[List[str]] = None) -> List[Order]:
endpoint = "auth/r/orders"
if symbol != None:
endpoint += f"/{symbol}"
return [ serializers.Order.parse(*subdata) for subdata in self._POST(endpoint, data={ "id": ids }) ]
def get_positions(self) -> List[Position]:
return [ serializers.Position.parse(*subdata) for subdata in self._POST("auth/r/positions") ]
def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str],
price: Optional[Union[Decimal, str]] = None, lev: Optional[int] = None,
price_trailing: Optional[Union[Decimal, str]] = None, price_aux_limit: Optional[Union[Decimal, str]] = None, price_oco_stop: Optional[Union[Decimal, str]] = None,
gid: Optional[int] = None, cid: Optional[int] = None,
flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification:
data = {
"type": type, "symbol": symbol, "amount": amount,
"price": price, "lev": lev,
"price_trailing": price_trailing, "price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop,
"gid": gid, "cid": cid,
"flags": flags, "tif": tif, "meta": meta
}
return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/submit", data=data))
def update_order(self, id: int, amount: Optional[Union[Decimal, str]] = None, price: Optional[Union[Decimal, str]] = None,
cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None,
flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, str]] = None,
price_aux_limit: Optional[Union[Decimal, str]] = None, price_trailing: Optional[Union[Decimal, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification:
data = {
"id": id, "amount": amount, "price": price,
"cid": cid, "cid_date": cid_date, "gid": gid,
"flags": flags, "lev": lev, "delta": delta,
"price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif
}
return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/update", data=data))
def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None) -> Notification:
data = {
"id": id,
"cid": cid,
"cid_date": cid_date
}
return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/cancel", data=data))
def cancel_order_multi(self, ids: Optional[List[int]] = None, cids: Optional[List[Tuple[int, str]]] = None, gids: Optional[List[int]] = None, all: bool = False) -> Notification:
data = {
"ids": ids,
"cids": cids,
"gids": gids,
"all": int(all)
}
return serializers._Notification(serializer=serializers.Order, iterate=True).parse(*self._POST("auth/w/order/cancel/multi", data=data))
def get_orders_history(self, symbol: Optional[str] = None, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Order]:
if symbol == None:
endpoint = "auth/r/orders/hist"
else: endpoint = f"auth/r/orders/{symbol}/hist"
data = {
"id": ids,
"start": start, "end": end,
"limit": limit
}
return [ serializers.Order.parse(*subdata) for subdata in self._POST(endpoint, data=data) ]
def get_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Trade]:
if symbol == None:
endpoint = "auth/r/trades/hist"
else: endpoint = f"auth/r/trades/{symbol}/hist"
data = {
"sort": sort,
"start": start, "end": end,
"limit": limit
}
return [ serializers.Trade.parse(*subdata) for subdata in self._POST(endpoint, data=data) ]
def get_order_trades(self, symbol: str, id: int) -> List[OrderTrade]:
return [ serializers.OrderTrade.parse(*subdata) for subdata in self._POST(f"auth/r/order/{symbol}:{id}/trades") ]
def get_ledgers(self, currency: str, category: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Ledger]:
data = {
"category": category,
"start": start, "end": end,
"limit": limit
}
return [ serializers.Ledger.parse(*subdata) for subdata in self._POST(f"auth/r/ledgers/{currency}/hist", data=data) ]
def get_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))
def cancel_funding_offer(self, id: int) -> Notification:
return serializers._Notification(serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/cancel", data={ "id": id }))
def get_funding_offers_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingOffer]:
if symbol == None:
endpoint = "auth/r/funding/offers/hist"
else: endpoint = f"auth/r/funding/offers/{symbol}/hist"
data = {
"start": start, "end": end,
"limit": limit
}
return [ serializers.FundingOffer.parse(*subdata) for subdata in self._POST(endpoint, data=data) ]
def get_funding_credits(self, symbol: Optional[str] = None) -> List[FundingCredit]:
if symbol == None:
endpoint = "auth/r/funding/credits"
else: endpoint = f"auth/r/funding/credits/{symbol}"
return [ serializers.FundingCredit.parse(*subdata) for subdata in self._POST(endpoint) ]
def get_funding_credits_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingCredit]:
if symbol == None:
endpoint = "auth/r/funding/credits/hist"
else: endpoint = f"auth/r/funding/credits/{symbol}/hist"
data = {
"start": start, "end": end,
"limit": limit
}
return [ serializers.FundingCredit.parse(*subdata) for subdata in self._POST(endpoint, data=data) ]

View File

@@ -1 +1,4 @@
from .BfxRestInterface import BfxRestInterface
from .endpoints import BfxRestInterface, RestPublicEndpoints, RestAuthenticatedEndpoints, \
RestMerchantEndpoints
NAME = "rest"

View File

@@ -0,0 +1,7 @@
from .bfx_rest_interface import BfxRestInterface
from .rest_public_endpoints import RestPublicEndpoints
from .rest_authenticated_endpoints import RestAuthenticatedEndpoints
from .rest_merchant_endpoints import RestMerchantEndpoints
NAME = "endpoints"

View File

@@ -0,0 +1,16 @@
from typing import Optional
from .rest_public_endpoints import RestPublicEndpoints
from .rest_authenticated_endpoints import RestAuthenticatedEndpoints
from .rest_merchant_endpoints import RestMerchantEndpoints
class BfxRestInterface(object):
VERSION = 2
def __init__(self, host, credentials = None):
API_KEY, API_SECRET = credentials and \
(credentials["API_KEY"], credentials["API_SECRET"]) or (None, None)
self.public = RestPublicEndpoints(host=host)
self.auth = RestAuthenticatedEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET)
self.merchant = RestMerchantEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET)

View File

@@ -0,0 +1,321 @@
from typing import List, Tuple, Union, Literal, Optional
from decimal import Decimal
from datetime import datetime
from .. types import *
from .. import serializers
from .. enums import Sort, OrderType, FundingOfferType
from .. middleware import Middleware
class RestAuthenticatedEndpoints(Middleware):
def get_user_info(self) -> UserInfo:
return serializers.UserInfo.parse(*self._POST(f"auth/r/info/user"))
def get_login_history(self) -> List[LoginHistory]:
return [ serializers.LoginHistory.parse(*sub_data) for sub_data in self._POST("auth/r/logins/hist") ]
def get_balance_available_for_orders_or_offers(self, symbol: str, type: str, dir: Optional[int] = None, rate: Optional[str] = None, lev: Optional[str] = None) -> BalanceAvailable:
return serializers.BalanceAvailable.parse(*self._POST("auth/calc/order/avail", body={
"symbol": symbol, "type": type, "dir": dir,
"rate": rate, "lev": lev
}))
def get_wallets(self) -> List[Wallet]:
return [ serializers.Wallet.parse(*sub_data) for sub_data in self._POST("auth/r/wallets") ]
def get_orders(self, symbol: Optional[str] = None, ids: Optional[List[str]] = None) -> List[Order]:
endpoint = "auth/r/orders"
if symbol != None:
endpoint += f"/{symbol}"
return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, body={ "id": ids }) ]
def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, float, str],
price: Optional[Union[Decimal, float, str]] = None, lev: Optional[int] = None,
price_trailing: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_oco_stop: Optional[Union[Decimal, float, str]] = None,
gid: Optional[int] = None, cid: Optional[int] = None,
flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification[Order]:
body = {
"type": type, "symbol": symbol, "amount": amount,
"price": price, "lev": lev,
"price_trailing": price_trailing, "price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop,
"gid": gid, "cid": cid,
"flags": flags, "tif": tif, "meta": meta
}
return serializers._Notification[Order](serializers.Order).parse(*self._POST("auth/w/order/submit", body=body))
def update_order(self, id: int, amount: Optional[Union[Decimal, float, str]] = None, price: Optional[Union[Decimal, float, str]] = None,
cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None,
flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, float, str]] = None,
price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification[Order]:
body = {
"id": id, "amount": amount, "price": price,
"cid": cid, "cid_date": cid_date, "gid": gid,
"flags": flags, "lev": lev, "delta": delta,
"price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif
}
return serializers._Notification[Order](serializers.Order).parse(*self._POST("auth/w/order/update", body=body))
def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None) -> Notification[Order]:
body = {
"id": id,
"cid": cid,
"cid_date": cid_date
}
return serializers._Notification[Order](serializers.Order).parse(*self._POST("auth/w/order/cancel", body=body))
def cancel_order_multi(self, ids: Optional[List[int]] = None, cids: Optional[List[Tuple[int, str]]] = None, gids: Optional[List[int]] = None, all: bool = False) -> Notification[List[Order]]:
body = {
"ids": ids,
"cids": cids,
"gids": gids,
"all": int(all)
}
return serializers._Notification[List[Order]](serializers.Order, is_iterable=True).parse(*self._POST("auth/w/order/cancel/multi", body=body))
def get_orders_history(self, symbol: Optional[str] = None, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Order]:
if symbol == None:
endpoint = "auth/r/orders/hist"
else: endpoint = f"auth/r/orders/{symbol}/hist"
body = {
"id": ids,
"start": start, "end": end,
"limit": limit
}
return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ]
def get_order_trades(self, symbol: str, id: int) -> List[OrderTrade]:
return [ serializers.OrderTrade.parse(*sub_data) for sub_data in self._POST(f"auth/r/order/{symbol}:{id}/trades") ]
def get_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Trade]:
if symbol == None:
endpoint = "auth/r/trades/hist"
else: endpoint = f"auth/r/trades/{symbol}/hist"
body = {
"sort": sort,
"start": start, "end": end,
"limit": limit
}
return [ serializers.Trade.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ]
def get_ledgers(self, currency: str, category: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Ledger]:
body = {
"category": category,
"start": start, "end": end,
"limit": limit
}
return [ serializers.Ledger.parse(*sub_data) for sub_data in self._POST(f"auth/r/ledgers/{currency}/hist", body=body) ]
def get_base_margin_info(self) -> BaseMarginInfo:
return serializers.BaseMarginInfo.parse(*(self._POST(f"auth/r/info/margin/base")[1]))
def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo:
response = self._POST(f"auth/r/info/margin/{symbol}")
data = [response[1]] + response[2]
return serializers.SymbolMarginInfo.parse(*data)
def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]:
return [ serializers.SymbolMarginInfo.parse(*([sub_data[1]] + sub_data[2])) for sub_data in self._POST(f"auth/r/info/margin/sym_all") ]
def get_positions(self) -> List[Position]:
return [ serializers.Position.parse(*sub_data) for sub_data in self._POST("auth/r/positions") ]
def claim_position(self, id: int, amount: Optional[Union[Decimal, float, str]] = None) -> Notification[PositionClaim]:
return serializers._Notification[PositionClaim](serializers.PositionClaim).parse(
*self._POST("auth/w/position/claim", body={ "id": id, "amount": amount })
)
def increase_position(self, symbol: str, amount: Union[Decimal, float, str]) -> Notification[PositionIncrease]:
return serializers._Notification[PositionIncrease](serializers.PositionIncrease).parse(
*self._POST("auth/w/position/increase", body={ "symbol": symbol, "amount": amount })
)
def get_increase_position_info(self, symbol: str, amount: Union[Decimal, float, str]) -> PositionIncreaseInfo:
response = self._POST(f"auth/r/position/increase/info", body={ "symbol": symbol, "amount": amount })
data = response[0] + [response[1][0]] + response[1][1] + [response[1][2]] + response[4] + response[5]
return serializers.PositionIncreaseInfo.parse(*data)
def get_positions_history(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionHistory]:
return [ serializers.PositionHistory.parse(*sub_data) for sub_data in self._POST("auth/r/positions/hist", body={ "start": start, "end": end, "limit": limit }) ]
def get_positions_snapshot(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionSnapshot]:
return [ serializers.PositionSnapshot.parse(*sub_data) for sub_data in self._POST("auth/r/positions/snap", body={ "start": start, "end": end, "limit": limit }) ]
def get_positions_audit(self, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionAudit]:
return [ serializers.PositionAudit.parse(*sub_data) for sub_data in self._POST("auth/r/positions/audit", body={ "ids": ids, "start": start, "end": end, "limit": limit }) ]
def set_derivative_position_collateral(self, symbol: str, collateral: Union[Decimal, float, str]) -> DerivativePositionCollateral:
return serializers.DerivativePositionCollateral.parse(*(self._POST("auth/w/deriv/collateral/set", body={ "symbol": symbol, "collateral": collateral })[0]))
def get_derivative_position_collateral_limits(self, symbol: str) -> DerivativePositionCollateralLimits:
return serializers.DerivativePositionCollateralLimits.parse(*self._POST("auth/calc/deriv/collateral/limits", body={ "symbol": symbol }))
def get_funding_offers(self, symbol: Optional[str] = None) -> List[FundingOffer]:
endpoint = "auth/r/funding/offers"
if symbol != None:
endpoint += f"/{symbol}"
return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint) ]
def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, float, str],
rate: Union[Decimal, float, str], period: int,
flags: Optional[int] = 0) -> Notification[FundingOffer]:
body = {
"type": type, "symbol": symbol, "amount": amount,
"rate": rate, "period": period,
"flags": flags
}
return serializers._Notification[FundingOffer](serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/submit", body=body))
def cancel_funding_offer(self, id: int) -> Notification[FundingOffer]:
return serializers._Notification[FundingOffer](serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/cancel", body={ "id": id }))
def cancel_all_funding_offers(self, currency: str) -> Notification[Literal[None]]:
return serializers._Notification[Literal[None]](None).parse(
*self._POST("auth/w/funding/offer/cancel/all", body={ "currency": currency })
)
def submit_funding_close(self, id: int) -> Notification[Literal[None]]:
return serializers._Notification[Literal[None]](None).parse(
*self._POST("auth/w/funding/close", body={ "id": id })
)
def toggle_auto_renew(self, status: bool, currency: str, amount: Optional[str] = None, rate: Optional[int] = None, period: Optional[int] = None) -> Notification[FundingAutoRenew]:
return serializers._Notification[FundingAutoRenew](serializers.FundingAutoRenew).parse(*self._POST("auth/w/funding/auto", body={
"status": int(status),
"currency": currency, "amount": amount,
"rate": rate, "period": period
}))
def toggle_keep(self, type: Literal["credit", "loan"], ids: Optional[List[int]] = None, changes: Optional[Dict[int, bool]] = None) -> Notification[Literal[None]]:
return serializers._Notification[Literal[None]](None).parse(*self._POST("auth/w/funding/keep", body={
"type": type,
"id": ids,
"changes": changes
}))
def get_funding_offers_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingOffer]:
if symbol == None:
endpoint = "auth/r/funding/offers/hist"
else: endpoint = f"auth/r/funding/offers/{symbol}/hist"
body = {
"start": start, "end": end,
"limit": limit
}
return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ]
def get_funding_loans(self, symbol: Optional[str] = None) -> List[FundingLoan]:
if symbol == None:
endpoint = "auth/r/funding/loans"
else: endpoint = f"auth/r/funding/loans/{symbol}"
return [ serializers.FundingLoan.parse(*sub_data) for sub_data in self._POST(endpoint) ]
def get_funding_loans_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingLoan]:
if symbol == None:
endpoint = "auth/r/funding/loans/hist"
else: endpoint = f"auth/r/funding/loans/{symbol}/hist"
body = {
"start": start, "end": end,
"limit": limit
}
return [ serializers.FundingLoan.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ]
def get_funding_credits(self, symbol: Optional[str] = None) -> List[FundingCredit]:
if symbol == None:
endpoint = "auth/r/funding/credits"
else: endpoint = f"auth/r/funding/credits/{symbol}"
return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint) ]
def get_funding_credits_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingCredit]:
if symbol == None:
endpoint = "auth/r/funding/credits/hist"
else: endpoint = f"auth/r/funding/credits/{symbol}/hist"
body = {
"start": start, "end": end,
"limit": limit
}
return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ]
def get_funding_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingTrade]:
if symbol == None:
endpoint = "auth/r/funding/trades/hist"
else: endpoint = f"auth/r/funding/trades/{symbol}/hist"
body = {
"sort": sort,
"start": start, "end": end,
"limit": limit
}
return [ serializers.FundingTrade.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ]
def get_funding_info(self, key: str) -> FundingInfo:
response = self._POST(f"auth/r/info/funding/{key}")
data = [response[1]] + response[2]
return serializers.FundingInfo.parse(*data)
def transfer_between_wallets(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, float, str]) -> Notification[Transfer]:
body = {
"from": from_wallet, "to": to_wallet,
"currency": currency, "currency_to": currency_to,
"amount": amount
}
return serializers._Notification[Transfer](serializers.Transfer).parse(*self._POST("auth/w/transfer", body=body))
def submit_wallet_withdrawal(self, wallet: str, method: str, address: str, amount: Union[Decimal, float, str]) -> Notification[Withdrawal]:
return serializers._Notification[Withdrawal](serializers.Withdrawal).parse(*self._POST("auth/w/withdraw", body={
"wallet": wallet, "method": method,
"address": address, "amount": amount,
}))
def get_deposit_address(self, wallet: str, method: str, renew: bool = False) -> Notification[DepositAddress]:
body = {
"wallet": wallet,
"method": method,
"renew": int(renew)
}
return serializers._Notification[DepositAddress](serializers.DepositAddress).parse(*self._POST("auth/w/deposit/address", body=body))
def generate_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, float, str]) -> LightningNetworkInvoice:
body = {
"wallet": wallet, "currency": currency,
"amount": amount
}
return serializers.LightningNetworkInvoice.parse(*self._POST("auth/w/deposit/invoice", body=body))
def get_movements(self, currency: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Movement]:
if currency == None:
endpoint = "auth/r/movements/hist"
else: endpoint = f"auth/r/movements/{currency}/hist"
body = {
"start": start, "end": end,
"limit": limit
}
return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ]

View File

@@ -0,0 +1,69 @@
from typing import TypedDict, List, Union, Literal, Optional
from decimal import Decimal
from .. types import *
from .. middleware import Middleware
from ...utils.camel_and_snake_case_helpers import to_snake_case_keys, to_camel_case_keys
_CustomerInfo = TypedDict("_CustomerInfo", {
"nationality": str, "resid_country": str, "resid_city": str,
"resid_zip_code": str, "resid_street": str, "resid_building_no": str,
"full_name": str, "email": str, "tos_accepted": bool
})
class RestMerchantEndpoints(Middleware):
def submit_invoice(self, amount: Union[Decimal, float, str], currency: str, order_id: str,
customer_info: _CustomerInfo, pay_currencies: List[str], duration: Optional[int] = None,
webhook: Optional[str] = None, redirect_url: Optional[str] = None) -> InvoiceSubmission:
body = to_camel_case_keys({
"amount": amount, "currency": currency, "order_id": order_id,
"customer_info": customer_info, "pay_currencies": pay_currencies, "duration": duration,
"webhook": webhook, "redirect_url": redirect_url
})
data = to_snake_case_keys(self._POST("auth/w/ext/pay/invoice/create", body=body))
return InvoiceSubmission.parse(data)
def get_invoices(self, id: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[InvoiceSubmission]:
return [ InvoiceSubmission.parse(sub_data) for sub_data in to_snake_case_keys(self._POST("auth/r/ext/pay/invoices", body={
"id": id, "start": start, "end": end,
"limit": limit
})) ]
def get_invoice_count_stats(self, status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"], format: str) -> List[InvoiceStats]:
return [ InvoiceStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/count", body={ "status": status, "format": format }) ]
def get_invoice_earning_stats(self, currency: str, format: str) -> List[InvoiceStats]:
return [ InvoiceStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/earning", body={ "currency": currency, "format": format }) ]
def complete_invoice(self, id: str, pay_currency: str, deposit_id: Optional[int] = None, ledger_id: Optional[int] = None) -> InvoiceSubmission:
return InvoiceSubmission.parse(to_snake_case_keys(self._POST("auth/w/ext/pay/invoice/complete", body={
"id": id, "payCcy": pay_currency, "depositId": deposit_id,
"ledgerId": ledger_id
})))
def expire_invoice(self, id: str) -> InvoiceSubmission:
return InvoiceSubmission.parse(to_snake_case_keys(self._POST("auth/w/ext/pay/invoice/expire", body={ "id": id })))
def get_currency_conversion_list(self) -> List[CurrencyConversion]:
return [
CurrencyConversion(
base_currency=sub_data["baseCcy"],
convert_currency=sub_data["convertCcy"],
created=sub_data["created"]
) for sub_data in self._POST("auth/r/ext/pay/settings/convert/list")
]
def add_currency_conversion(self, base_currency: str, convert_currency: str) -> bool:
return bool(self._POST("auth/w/ext/pay/settings/convert/create", body={
"baseCcy": base_currency,
"convertCcy": convert_currency
}))
def remove_currency_conversion(self, base_currency: str, convert_currency: str) -> bool:
return bool(self._POST("auth/w/ext/pay/settings/convert/remove", body={
"baseCcy": base_currency,
"convertCcy": convert_currency
}))

View File

@@ -0,0 +1,186 @@
from typing import List, Union, Literal, Optional, Any, cast
from decimal import Decimal
from .. types import *
from .. import serializers
from .. enums import Config, Sort
from .. middleware import Middleware
class RestPublicEndpoints(Middleware):
def conf(self, config: Config) -> Any:
return self._GET(f"conf/{config}")[0]
def get_platform_status(self) -> PlatformStatus:
return serializers.PlatformStatus.parse(*self._GET("platform/status"))
def get_tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]:
data = self._GET("tickers", params={ "symbols": ",".join(symbols) })
parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse }
return [ cast(Union[TradingPairTicker, FundingCurrencyTicker], parsers[sub_data[0][0]](*sub_data)) for sub_data in data ]
def get_t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]:
if isinstance(pairs, str) and pairs == "ALL":
return [ cast(TradingPairTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.symbol).startswith("t") ]
data = self.get_tickers([ pair for pair in pairs ])
return cast(List[TradingPairTicker], data)
def get_f_tickers(self, currencies: Union[List[str], Literal["ALL"]]) -> List[FundingCurrencyTicker]:
if isinstance(currencies, str) and currencies == "ALL":
return [ cast(FundingCurrencyTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.symbol).startswith("f") ]
data = self.get_tickers([ currency for currency in currencies ])
return cast(List[FundingCurrencyTicker], data)
def get_t_ticker(self, pair: str) -> TradingPairTicker:
return serializers.TradingPairTicker.parse(*self._GET(f"ticker/t{pair}"), skip=["SYMBOL"])
def get_f_ticker(self, currency: str) -> FundingCurrencyTicker:
return serializers.FundingCurrencyTicker.parse(*self._GET(f"ticker/f{currency}"), skip=["SYMBOL"])
def get_tickers_history(self, symbols: List[str], start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[TickersHistory]:
return [ serializers.TickersHistory.parse(*sub_data) for sub_data in self._GET("tickers/hist", params={
"symbols": ",".join(symbols),
"start": start, "end": end,
"limit": limit
}) ]
def get_t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[TradingPairTrade]:
params = { "limit": limit, "start": start, "end": end, "sort": sort }
data = self._GET(f"trades/{pair}/hist", params=params)
return [ serializers.TradingPairTrade.parse(*sub_data) for sub_data in data ]
def get_f_trades(self, currency: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[FundingCurrencyTrade]:
params = { "limit": limit, "start": start, "end": end, "sort": sort }
data = self._GET(f"trades/{currency}/hist", params=params)
return [ serializers.FundingCurrencyTrade.parse(*sub_data) for sub_data in data ]
def get_t_book(self, pair: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairBook]:
return [ serializers.TradingPairBook.parse(*sub_data) for sub_data in self._GET(f"book/{pair}/{precision}", params={ "len": len }) ]
def get_f_book(self, currency: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyBook]:
return [ serializers.FundingCurrencyBook.parse(*sub_data) for sub_data in self._GET(f"book/{currency}/{precision}", params={ "len": len }) ]
def get_t_raw_book(self, pair: str, len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairRawBook]:
return [ serializers.TradingPairRawBook.parse(*sub_data) for sub_data in self._GET(f"book/{pair}/R0", params={ "len": len }) ]
def get_f_raw_book(self, currency: str, len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyRawBook]:
return [ serializers.FundingCurrencyRawBook.parse(*sub_data) for sub_data in self._GET(f"book/{currency}/R0", params={ "len": len }) ]
def get_stats_hist(
self,
resource: str,
sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None
) -> List[Statistic]:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
data = self._GET(f"stats1/{resource}/hist", params=params)
return [ serializers.Statistic.parse(*sub_data) for sub_data in data ]
def get_stats_last(
self,
resource: str,
sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None
) -> Statistic:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
data = self._GET(f"stats1/{resource}/last", params=params)
return serializers.Statistic.parse(*data)
def get_candles_hist(
self,
symbol: str, tf: str = "1m",
sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None
) -> List[Candle]:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
data = self._GET(f"candles/trade:{tf}:{symbol}/hist", params=params)
return [ serializers.Candle.parse(*sub_data) for sub_data in data ]
def get_candles_last(
self,
symbol: str, tf: str = "1m",
sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None
) -> Candle:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
data = self._GET(f"candles/trade:{tf}:{symbol}/last", params=params)
return serializers.Candle.parse(*data)
def get_derivatives_status(self, keys: Union[List[str], Literal["ALL"]]) -> List[DerivativesStatus]:
if keys == "ALL":
params = { "keys": "ALL" }
else: params = { "keys": ",".join(keys) }
data = self._GET(f"status/deriv", params=params)
return [ serializers.DerivativesStatus.parse(*sub_data) for sub_data in data ]
def get_derivatives_status_history(
self,
type: str, symbol: str,
sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None
) -> List[DerivativesStatus]:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
data = self._GET(f"status/{type}/{symbol}/hist", params=params)
return [ serializers.DerivativesStatus.parse(*sub_data, skip=[ "KEY" ]) for sub_data in data ]
def get_liquidations(self, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Liquidation]:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
data = self._GET("liquidations/hist", params=params)
return [ serializers.Liquidation.parse(*sub_data[0]) for sub_data in data ]
def get_seed_candles(self, symbol: str, tf: str = '1m', sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Candle]:
params = {"sort": sort, "start": start, "end": end, "limit": limit}
data = self._GET(f"candles/trade:{tf}:{symbol}/hist?limit={limit}&start={start}&end={end}&sort={sort}", params=params)
return [ serializers.Candle.parse(*sub_data) for sub_data in data ]
def get_leaderboards_hist(
self,
resource: str,
sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None
) -> List[Leaderboard]:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
data = self._GET(f"rankings/{resource}/hist", params=params)
return [ serializers.Leaderboard.parse(*sub_data) for sub_data in data ]
def get_leaderboards_last(
self,
resource: str,
sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None
) -> Leaderboard:
params = { "sort": sort, "start": start, "end": end, "limit": limit }
data = self._GET(f"rankings/{resource}/last", params=params)
return serializers.Leaderboard.parse(*data)
def get_funding_stats(self, symbol: str, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingStatistic]:
params = { "start": start, "end": end, "limit": limit }
data = self._GET(f"funding/stats/{symbol}/hist", params=params)
return [ serializers.FundingStatistic.parse(*sub_data) for sub_data in data ]
def get_pulse_profile(self, nickname: str) -> PulseProfile:
return serializers.PulseProfile.parse(*self._GET(f"pulse/profile/{nickname}"))
def get_pulse_history(self, end: Optional[str] = None, limit: Optional[int] = None) -> List[PulseMessage]:
messages = list()
for subdata in self._GET("pulse/hist", params={ "end": end, "limit": limit }):
subdata[18] = subdata[18][0]
message = serializers.PulseMessage.parse(*subdata)
messages.append(message)
return messages
def get_trading_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], price_limit: Optional[Union[Decimal, float, str]] = None) -> TradingMarketAveragePrice:
return serializers.TradingMarketAveragePrice.parse(*self._POST("calc/trade/avg", body={
"symbol": symbol, "amount": amount, "price_limit": price_limit
}))
def get_funding_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], period: int, rate_limit: Optional[Union[Decimal, float, str]] = None) -> FundingMarketAveragePrice:
return serializers.FundingMarketAveragePrice.parse(*self._POST("calc/trade/avg", body={
"symbol": symbol, "amount": amount, "period": period, "rate_limit": rate_limit
}))
def get_fx_rate(self, ccy1: str, ccy2: str) -> FxRate:
return serializers.FxRate.parse(*self._POST("calc/fx", body={ "ccy1": ccy1, "ccy2": ccy2 }))

View File

@@ -3,6 +3,7 @@ from .. exceptions import BfxBaseException
__all__ = [
"BfxRestException",
"ResourceNotFound",
"RequestParametersError",
"ResourceNotFound",
"InvalidAuthenticationCredentials"

View File

@@ -0,0 +1,3 @@
from .middleware import Middleware
NAME = "middleware"

View File

@@ -0,0 +1,82 @@
import time, hmac, hashlib, json, requests
from typing import TYPE_CHECKING, Optional, Any, cast
from http import HTTPStatus
from ..enums import Error
from ..exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError
from ...utils.JSONEncoder import JSONEncoder
if TYPE_CHECKING:
from requests.sessions import _Params
class Middleware(object):
def __init__(self, host: str, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None):
self.host, self.API_KEY, self.API_SECRET = host, API_KEY, API_SECRET
def __build_authentication_headers(self, endpoint: str, data: Optional[str] = None):
assert isinstance(self.API_KEY, str) and isinstance(self.API_SECRET, str), \
"API_KEY and API_SECRET must be both str to call __build_authentication_headers"
nonce = str(int(time.time()) * 1000)
if data == None:
path = f"/api/v2/{endpoint}{nonce}"
else: path = f"/api/v2/{endpoint}{nonce}{data}"
signature = hmac.new(
self.API_SECRET.encode("utf8"),
path.encode("utf8"),
hashlib.sha384
).hexdigest()
return {
"bfx-nonce": nonce,
"bfx-signature": signature,
"bfx-apikey": self.API_KEY
}
def _GET(self, endpoint: str, params: Optional["_Params"] = None) -> Any:
response = requests.get(f"{self.host}/{endpoint}", params=params)
if response.status_code == HTTPStatus.NOT_FOUND:
raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.")
data = response.json()
if len(data) and data[0] == "error":
if data[1] == Error.ERR_PARAMS:
raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>")
if data[1] == None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC:
raise UnknownGenericError(f"The server replied to the request with a generic error with message: <{data[2]}>.")
return data
def _POST(self, endpoint: str, params: Optional["_Params"] = None, body: Optional[Any] = None, _ignore_authentication_headers: bool = False) -> Any:
data = body and json.dumps(body, cls=JSONEncoder) or None
headers = { "Content-Type": "application/json" }
if self.API_KEY and self.API_SECRET and _ignore_authentication_headers == False:
headers = { **headers, **self.__build_authentication_headers(endpoint, data) }
response = requests.post(f"{self.host}/{endpoint}", params=params, data=data, headers=headers)
if response.status_code == HTTPStatus.NOT_FOUND:
raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.")
data = response.json()
if isinstance(data, list) and len(data) and data[0] == "error":
if data[1] == Error.ERR_PARAMS:
raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>")
if data[1] == Error.ERR_AUTH_FAIL:
raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.")
if data[1] == None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC:
raise UnknownGenericError(f"The server replied to the request with a generic error with message: <{data[2]}>.")
return data

View File

@@ -1,54 +1,75 @@
from . import typings
from . import types
from .. labeler import _Serializer
from .. labeler import generate_labeler_serializer, generate_recursive_serializer
from .. notification import _Notification
__serializers__ = [
"PlatformStatus", "TradingPairTicker", "FundingCurrencyTicker",
"TickersHistory", "TradingPairTrade", "FundingCurrencyTrade",
"TradingPairBook", "FundingCurrencyBook", "TradingPairRawBook",
"FundingCurrencyRawBook", "Statistic", "Candle",
"DerivativesStatus", "Liquidation", "Leaderboard",
"FundingStatistic", "PulseProfile", "PulseMessage",
"TradingMarketAveragePrice", "FundingMarketAveragePrice", "FxRate",
"UserInfo", "LoginHistory", "BalanceAvailable",
"Order", "Position", "Trade",
"FundingTrade", "OrderTrade", "Ledger",
"FundingOffer", "FundingCredit", "FundingLoan",
"FundingAutoRenew", "FundingInfo", "Wallet",
"Transfer", "Withdrawal", "DepositAddress",
"LightningNetworkInvoice", "Movement", "SymbolMarginInfo",
"BaseMarginInfo", "PositionClaim", "PositionIncreaseInfo",
"PositionIncrease", "PositionHistory", "PositionSnapshot",
"PositionAudit", "DerivativePositionCollateral", "DerivativePositionCollateralLimits",
]
#region Serializers definition for Rest Public Endpoints
PlatformStatus = _Serializer[typings.PlatformStatus]("PlatformStatus", labels=[
"OPERATIVE"
PlatformStatus = generate_labeler_serializer("PlatformStatus", klass=types.PlatformStatus, labels=[
"status"
])
TradingPairTicker = _Serializer[typings.TradingPairTicker]("TradingPairTicker", labels=[
"SYMBOL",
"BID",
"BID_SIZE",
"ASK",
"ASK_SIZE",
"DAILY_CHANGE",
"DAILY_CHANGE_RELATIVE",
"LAST_PRICE",
"VOLUME",
"HIGH",
"LOW"
TradingPairTicker = generate_labeler_serializer("TradingPairTicker", klass=types.TradingPairTicker, labels=[
"symbol",
"bid",
"bid_size",
"ask",
"ask_size",
"daily_change",
"daily_change_relative",
"last_price",
"volume",
"high",
"low"
])
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",
FundingCurrencyTicker = generate_labeler_serializer("FundingCurrencyTicker", klass=types.FundingCurrencyTicker, labels=[
"symbol",
"frr",
"bid",
"bid_period",
"bid_size",
"ask",
"ask_period",
"ask_size",
"daily_change",
"daily_change_relative",
"last_price",
"volume",
"high",
"low",
"_PLACEHOLDER",
"_PLACEHOLDER",
"FRR_AMOUNT_AVAILABLE"
"frr_amount_available"
])
TickersHistory = _Serializer[typings.TickersHistory]("TickersHistory", labels=[
"SYMBOL",
"BID",
TickersHistory = generate_labeler_serializer("TickersHistory", klass=types.TickersHistory, labels=[
"symbol",
"bid",
"_PLACEHOLDER",
"ASK",
"ask",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
@@ -57,295 +78,672 @@ TickersHistory = _Serializer[typings.TickersHistory]("TickersHistory", labels=[
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"MTS"
"mts"
])
TradingPairTrade = _Serializer[typings.TradingPairTrade]("TradingPairTrade", labels=[
"ID",
"MTS",
"AMOUNT",
"PRICE"
TradingPairTrade = generate_labeler_serializer("TradingPairTrade", klass=types.TradingPairTrade, labels=[
"id",
"mts",
"amount",
"price"
])
FundingCurrencyTrade = _Serializer[typings.FundingCurrencyTrade]("FundingCurrencyTrade", labels=[
"ID",
"MTS",
"AMOUNT",
"RATE",
"PERIOD"
FundingCurrencyTrade = generate_labeler_serializer("FundingCurrencyTrade", klass=types.FundingCurrencyTrade, labels=[
"id",
"mts",
"amount",
"rate",
"period"
])
TradingPairBook = _Serializer[typings.TradingPairBook]("TradingPairBook", labels=[
"PRICE",
"COUNT",
"AMOUNT"
TradingPairBook = generate_labeler_serializer("TradingPairBook", klass=types.TradingPairBook, labels=[
"price",
"count",
"amount"
])
FundingCurrencyBook = _Serializer[typings.FundingCurrencyBook]("FundingCurrencyBook", labels=[
"RATE",
"PERIOD",
"COUNT",
"AMOUNT"
FundingCurrencyBook = generate_labeler_serializer("FundingCurrencyBook", klass=types.FundingCurrencyBook, labels=[
"rate",
"period",
"count",
"amount"
])
TradingPairRawBook = _Serializer[typings.TradingPairRawBook]("TradingPairRawBook", labels=[
"ORDER_ID",
"PRICE",
"AMOUNT"
TradingPairRawBook = generate_labeler_serializer("TradingPairRawBook", klass=types.TradingPairRawBook, labels=[
"order_id",
"price",
"amount"
])
FundingCurrencyRawBook = _Serializer[typings.FundingCurrencyRawBook]("FundingCurrencyRawBook", labels=[
"OFFER_ID",
"PERIOD",
"RATE",
"AMOUNT"
FundingCurrencyRawBook = generate_labeler_serializer("FundingCurrencyRawBook", klass=types.FundingCurrencyRawBook, labels=[
"offer_id",
"period",
"rate",
"amount"
])
Statistic = _Serializer[typings.Statistic]("Statistic", labels=[
"MTS",
"VALUE"
Statistic = generate_labeler_serializer("Statistic", klass=types.Statistic, labels=[
"mts",
"value"
])
Candle = _Serializer[typings.Candle]("Candle", labels=[
"MTS",
"OPEN",
"CLOSE",
"HIGH",
"LOW",
"VOLUME"
Candle = generate_labeler_serializer("Candle", klass=types.Candle, labels=[
"mts",
"open",
"close",
"high",
"low",
"volume"
])
DerivativesStatus = _Serializer[typings.DerivativesStatus]("DerivativesStatus", labels=[
"KEY",
"MTS",
DerivativesStatus = generate_labeler_serializer("DerivativesStatus", klass=types.DerivativesStatus, labels=[
"key",
"mts",
"_PLACEHOLDER",
"DERIV_PRICE",
"SPOT_PRICE",
"deriv_price",
"spot_price",
"_PLACEHOLDER",
"INSURANCE_FUND_BALANCE",
"insurance_fund_balance",
"_PLACEHOLDER",
"NEXT_FUNDING_EVT_TIMESTAMP_MS",
"NEXT_FUNDING_ACCRUED",
"NEXT_FUNDING_STEP",
"next_funding_evt_timestamp_ms",
"next_funding_accrued",
"next_funding_step",
"_PLACEHOLDER",
"CURRENT_FUNDING",
"current_funding",
"_PLACEHOLDER",
"_PLACEHOLDER",
"MARK_PRICE",
"mark_price",
"_PLACEHOLDER",
"_PLACEHOLDER",
"OPEN_INTEREST",
"open_interest",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"CLAMP_MIN",
"CLAMP_MAX"
"clamp_min",
"clamp_max"
])
Liquidation = _Serializer[typings.Liquidation]("Liquidation", labels=[
Liquidation = generate_labeler_serializer("Liquidation", klass=types.Liquidation, labels=[
"_PLACEHOLDER",
"POS_ID",
"MTS",
"pos_id",
"mts",
"_PLACEHOLDER",
"SYMBOL",
"AMOUNT",
"BASE_PRICE",
"symbol",
"amount",
"base_price",
"_PLACEHOLDER",
"IS_MATCH",
"IS_MARKET_SOLD",
"is_match",
"is_market_sold",
"_PLACEHOLDER",
"PRICE_ACQUIRED"
"price_acquired"
])
Leaderboard = _Serializer[typings.Leaderboard]("Leaderboard", labels=[
"MTS",
Leaderboard = generate_labeler_serializer("Leaderboard", klass=types.Leaderboard, labels=[
"mts",
"_PLACEHOLDER",
"USERNAME",
"RANKING",
"username",
"ranking",
"_PLACEHOLDER",
"_PLACEHOLDER",
"VALUE",
"value",
"_PLACEHOLDER",
"_PLACEHOLDER",
"TWITTER_HANDLE"
"twitter_handle"
])
FundingStatistic = _Serializer[typings.FundingStatistic]("FundingStatistic", labels=[
"TIMESTAMP",
FundingStatistic = generate_labeler_serializer("FundingStatistic", klass=types.FundingStatistic, labels=[
"timestamp",
"_PLACEHOLDER",
"_PLACEHOLDER",
"FRR",
"AVG_PERIOD",
"frr",
"avg_period",
"_PLACEHOLDER",
"_PLACEHOLDER",
"FUNDING_AMOUNT",
"FUNDING_AMOUNT_USED",
"funding_amount",
"funding_amount_used",
"_PLACEHOLDER",
"_PLACEHOLDER",
"FUNDING_BELOW_THRESHOLD"
"funding_below_threshold"
])
PulseProfile = generate_labeler_serializer("PulseProfile", klass=types.PulseProfile, labels=[
"puid",
"mts",
"_PLACEHOLDER",
"nickname",
"_PLACEHOLDER",
"picture",
"text",
"_PLACEHOLDER",
"_PLACEHOLDER",
"twitter_handle",
"_PLACEHOLDER",
"followers",
"following",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"tipping_status"
])
PulseMessage = generate_recursive_serializer("PulseMessage", klass=types.PulseMessage, serializers={ "profile": PulseProfile }, labels=[
"pid",
"mts",
"_PLACEHOLDER",
"puid",
"_PLACEHOLDER",
"title",
"content",
"_PLACEHOLDER",
"_PLACEHOLDER",
"is_pin",
"is_public",
"comments_disabled",
"tags",
"attachments",
"meta",
"likes",
"_PLACEHOLDER",
"_PLACEHOLDER",
"profile",
"comments",
"_PLACEHOLDER",
"_PLACEHOLDER"
])
TradingMarketAveragePrice = generate_labeler_serializer("TradingMarketAveragePrice", klass=types.TradingMarketAveragePrice, labels=[
"price_avg",
"amount"
])
FundingMarketAveragePrice = generate_labeler_serializer("FundingMarketAveragePrice", klass=types.FundingMarketAveragePrice, labels=[
"rate_avg",
"amount"
])
FxRate = generate_labeler_serializer("FxRate", klass=types.FxRate, labels=[
"current_rate"
])
#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"
UserInfo = generate_labeler_serializer("UserInfo", klass=types.UserInfo, labels=[
"id",
"email",
"username",
"mts_account_create",
"verified",
"verification_level",
"_PLACEHOLDER",
"timezone",
"locale",
"company",
"email_verified",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"mts_master_account_create",
"group_id",
"master_account_id",
"inherit_master_account_verification",
"is_group_master",
"group_withdraw_enabled",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"ppt_enabled",
"merchant_enabled",
"competition_enabled",
"two_factors_authentication_modes",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"is_securities_master",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"securities_enabled",
"allow_disable_ctxswitch",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"time_last_login",
"_PLACEHOLDER",
"_PLACEHOLDER",
"ctxtswitch_disabled",
"_PLACEHOLDER",
"comp_countries",
"compl_countries_resid",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"is_merchant_enterprise"
])
Order = _Serializer[typings.Order]("Order", labels=[
"ID",
"GID",
"CID",
"SYMBOL",
"MTS_CREATE",
"MTS_UPDATE",
"AMOUNT",
"AMOUNT_ORIG",
"ORDER_TYPE",
"TYPE_PREV",
"MTS_TIF",
LoginHistory = generate_labeler_serializer("LoginHistory", klass=types.LoginHistory, labels=[
"id",
"_PLACEHOLDER",
"FLAGS",
"ORDER_STATUS",
"time",
"_PLACEHOLDER",
"ip",
"_PLACEHOLDER",
"_PLACEHOLDER",
"PRICE",
"PRICE_AVG",
"PRICE_TRAILING",
"PRICE_AUX_LIMIT",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"NOTIFY",
"HIDDEN",
"PLACED_ID",
"_PLACEHOLDER",
"_PLACEHOLDER",
"ROUTING",
"_PLACEHOLDER",
"_PLACEHOLDER",
"META"
"extra_info"
])
Position = _Serializer[typings.Position]("Position", labels=[
"SYMBOL",
"STATUS",
"AMOUNT",
"BASE_PRICE",
"FUNDING",
"FUNDING_TYPE",
"PL",
"PL_PERC",
"PRICE_LIQ",
"LEVERAGE",
"_PLACEHOLDER",
"POSITION_ID",
"MTS_CREATE",
"MTS_UPDATE",
"_PLACEHOLDER",
"TYPE",
"_PLACEHOLDER",
"COLLATERAL",
"COLLATERAL_MIN",
"META"
BalanceAvailable = generate_labeler_serializer("BalanceAvailable", klass=types.BalanceAvailable, labels=[
"amount"
])
FundingOffer = _Serializer[typings.FundingOffer]("FundingOffer", labels=[
"ID",
"SYMBOL",
"MTS_CREATED",
"MTS_UPDATED",
"AMOUNT",
"AMOUNT_ORIG",
"OFFER_TYPE",
Order = generate_labeler_serializer("Order", klass=types.Order, labels=[
"id",
"gid",
"cid",
"symbol",
"mts_create",
"mts_update",
"amount",
"amount_orig",
"order_type",
"type_prev",
"mts_tif",
"_PLACEHOLDER",
"flags",
"order_status",
"_PLACEHOLDER",
"_PLACEHOLDER",
"FLAGS",
"OFFER_STATUS",
"price",
"price_avg",
"price_trailing",
"price_aux_limit",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"RATE",
"PERIOD",
"NOTIFY",
"HIDDEN",
"notify",
"hidden",
"placed_id",
"_PLACEHOLDER",
"RENEW",
"_PLACEHOLDER",
"routing",
"_PLACEHOLDER",
"_PLACEHOLDER",
"meta"
])
Position = generate_labeler_serializer("Position", klass=types.Position, labels=[
"symbol",
"status",
"amount",
"base_price",
"margin_funding",
"margin_funding_type",
"pl",
"pl_perc",
"price_liq",
"leverage",
"_PLACEHOLDER",
"position_id",
"mts_create",
"mts_update",
"_PLACEHOLDER",
"type",
"_PLACEHOLDER",
"collateral",
"collateral_min",
"meta"
])
Trade = generate_labeler_serializer("Trade", klass=types.Trade, labels=[
"id",
"symbol",
"mts_create",
"order_id",
"exec_amount",
"exec_price",
"order_type",
"order_price",
"maker",
"fee",
"fee_currency",
"cid"
])
FundingTrade = generate_labeler_serializer("FundingTrade", klass=types.FundingTrade, labels=[
"id",
"currency",
"mts_create",
"offer_id",
"amount",
"rate",
"period"
])
OrderTrade = generate_labeler_serializer("OrderTrade", klass=types.OrderTrade, labels=[
"id",
"symbol",
"mts_create",
"order_id",
"exec_amount",
"exec_price",
"_PLACEHOLDER",
"_PLACEHOLDER",
"maker",
"fee",
"fee_currency",
"cid"
])
Ledger = generate_labeler_serializer("Ledger", klass=types.Ledger, labels=[
"id",
"currency",
"_PLACEHOLDER",
"mts",
"_PLACEHOLDER",
"amount",
"balance",
"_PLACEHOLDER",
"description"
])
FundingOffer = generate_labeler_serializer("FundingOffer", klass=types.FundingOffer, labels=[
"id",
"symbol",
"mts_create",
"mts_update",
"amount",
"amount_orig",
"offer_type",
"_PLACEHOLDER",
"_PLACEHOLDER",
"flags",
"offer_status",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"rate",
"period",
"notify",
"hidden",
"_PLACEHOLDER",
"renew",
"_PLACEHOLDER"
])
Trade = _Serializer[typings.Trade]("Trade", labels=[
"ID",
"PAIR",
"MTS_CREATE",
"ORDER_ID",
"EXEC_AMOUNT",
"EXEC_PRICE",
"ORDER_TYPE",
"ORDER_PRICE",
"MAKER",
"FEE",
"FEE_CURRENCY",
"CID"
FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.FundingCredit, labels=[
"id",
"symbol",
"side",
"mts_create",
"mts_update",
"amount",
"flags",
"status",
"rate_type",
"_PLACEHOLDER",
"_PLACEHOLDER",
"rate",
"period",
"mts_opening",
"mts_last_payout",
"notify",
"hidden",
"_PLACEHOLDER",
"renew",
"_PLACEHOLDER",
"no_close",
"position_pair"
])
OrderTrade = _Serializer[typings.OrderTrade]("OrderTrade", labels=[
"ID",
"PAIR",
"MTS_CREATE",
"ORDER_ID",
"EXEC_AMOUNT",
"EXEC_PRICE",
FundingLoan = generate_labeler_serializer("FundingLoan", klass=types.FundingLoan, labels=[
"id",
"symbol",
"side",
"mts_create",
"mts_update",
"amount",
"flags",
"status",
"rate_type",
"_PLACEHOLDER",
"_PLACEHOLDER",
"MAKER",
"FEE",
"FEE_CURRENCY",
"CID"
"rate",
"period",
"mts_opening",
"mts_last_payout",
"notify",
"hidden",
"_PLACEHOLDER",
"renew",
"_PLACEHOLDER",
"no_close"
])
Ledger = _Serializer[typings.Ledger]("Ledger", labels=[
"ID",
"CURRENCY",
"_PLACEHOLDER",
"MTS",
"_PLACEHOLDER",
"AMOUNT",
"BALANCE",
"_PLACEHOLDER",
"DESCRIPTION"
FundingAutoRenew = generate_labeler_serializer("FundingAutoRenew", klass=types.FundingAutoRenew, labels=[
"currency",
"period",
"rate",
"threshold"
])
FundingCredit = _Serializer[typings.FundingCredit]("FundingCredit", labels=[
"ID",
"SYMBOL",
"SIDE",
"MTS_CREATE",
"MTS_UPDATE",
"AMOUNT",
"FLAGS",
"STATUS",
"RATE_TYPE",
FundingInfo = generate_labeler_serializer("FundingInfo", klass=types.FundingInfo, labels=[
"symbol",
"yield_loan",
"yield_lend",
"duration_loan",
"duration_lend"
])
Wallet = generate_labeler_serializer("Wallet", klass=types.Wallet, labels=[
"wallet_type",
"currency",
"balance",
"unsettled_interest",
"available_balance",
"last_change",
"trade_details"
])
Transfer = generate_labeler_serializer("Transfer", klass=types.Transfer, labels=[
"mts",
"wallet_from",
"wallet_to",
"_PLACEHOLDER",
"currency",
"currency_to",
"_PLACEHOLDER",
"amount"
])
Withdrawal = generate_labeler_serializer("Withdrawal", klass=types.Withdrawal, labels=[
"withdrawal_id",
"_PLACEHOLDER",
"method",
"payment_id",
"wallet",
"amount",
"_PLACEHOLDER",
"_PLACEHOLDER",
"RATE",
"PERIOD",
"MTS_OPENING",
"MTS_LAST_PAYOUT",
"NOTIFY",
"HIDDEN",
"withdrawal_fee"
])
DepositAddress = generate_labeler_serializer("DepositAddress", klass=types.DepositAddress, labels=[
"_PLACEHOLDER",
"RENEW",
"method",
"currency_code",
"_PLACEHOLDER",
"NO_CLOSE",
"POSITION_PAIR"
"address",
"pool_address"
])
LightningNetworkInvoice = generate_labeler_serializer("LightningNetworkInvoice", klass=types.LightningNetworkInvoice, labels=[
"invoice_hash",
"invoice",
"_PLACEHOLDER",
"_PLACEHOLDER",
"amount"
])
Movement = generate_labeler_serializer("Movement", klass=types.Movement, labels=[
"id",
"currency",
"currency_name",
"_PLACEHOLDER",
"_PLACEHOLDER",
"mts_start",
"mts_update",
"_PLACEHOLDER",
"_PLACEHOLDER",
"status",
"_PLACEHOLDER",
"_PLACEHOLDER",
"amount",
"fees",
"_PLACEHOLDER",
"_PLACEHOLDER",
"destination_address",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"transaction_id",
"withdraw_transaction_note"
])
SymbolMarginInfo = generate_labeler_serializer("SymbolMarginInfo", klass=types.SymbolMarginInfo, labels=[
"symbol",
"tradable_balance",
"gross_balance",
"buy",
"sell"
])
BaseMarginInfo = generate_labeler_serializer("BaseMarginInfo", klass=types.BaseMarginInfo, labels=[
"user_pl",
"user_swaps",
"margin_balance",
"margin_net",
"margin_min"
])
PositionClaim = generate_labeler_serializer("PositionClaim", klass=types.PositionClaim, labels=[
"symbol",
"position_status",
"amount",
"base_price",
"margin_funding",
"margin_funding_type",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"position_id",
"mts_create",
"mts_update",
"_PLACEHOLDER",
"pos_type",
"_PLACEHOLDER",
"collateral",
"min_collateral",
"meta"
])
PositionIncreaseInfo = generate_labeler_serializer("PositionIncreaseInfo", klass=types.PositionIncreaseInfo, labels=[
"max_pos",
"current_pos",
"base_currency_balance",
"tradable_balance_quote_currency",
"tradable_balance_quote_total",
"tradable_balance_base_currency",
"tradable_balance_base_total",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"funding_avail",
"funding_value",
"funding_required",
"funding_value_currency",
"funding_required_currency"
])
PositionIncrease = generate_labeler_serializer("PositionIncrease", klass=types.PositionIncrease, labels=[
"symbol",
"_PLACEHOLDER",
"amount",
"base_price"
])
PositionHistory = generate_labeler_serializer("PositionHistory", klass=types.PositionHistory, labels=[
"symbol",
"status",
"amount",
"base_price",
"funding",
"funding_type",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"position_id",
"mts_create",
"mts_update"
])
PositionSnapshot = generate_labeler_serializer("PositionSnapshot", klass=types.PositionSnapshot, labels=[
"symbol",
"status",
"amount",
"base_price",
"funding",
"funding_type",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"position_id",
"mts_create",
"mts_update"
])
PositionAudit = generate_labeler_serializer("PositionAudit", klass=types.PositionAudit, labels=[
"symbol",
"status",
"amount",
"base_price",
"funding",
"funding_type",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"position_id",
"mts_create",
"mts_update",
"_PLACEHOLDER",
"type",
"_PLACEHOLDER",
"collateral",
"collateral_min",
"meta"
])
DerivativePositionCollateral = generate_labeler_serializer("DerivativePositionCollateral", klass=types.DerivativePositionCollateral, labels=[
"status"
])
DerivativePositionCollateralLimits = generate_labeler_serializer("DerivativePositionCollateralLimits", klass=types.DerivativePositionCollateralLimits, labels=[
"min_collateral",
"max_collateral"
])
#endregion

652
bfxapi/rest/types.py Normal file
View File

@@ -0,0 +1,652 @@
from typing import List, Dict, Optional, Literal, Any
from dataclasses import dataclass
from .. labeler import _Type, partial, compose
from .. notification import Notification
from .. utils.JSONEncoder import JSON
#region Type hinting for Rest Public Endpoints
@dataclass
class PlatformStatus(_Type):
status: int
@dataclass
class TradingPairTicker(_Type):
symbol: Optional[str]
bid: float
bid_size: float
ask: float
ask_size: float
daily_change: float
daily_change_relative: float
last_price: float
volume: float
high: float
low: float
@dataclass
class FundingCurrencyTicker(_Type):
symbol: Optional[str]
frr: float
bid: float
bid_period: int
bid_size: float
ask: float
ask_period: int
ask_size: float
daily_change: float
daily_change_relative: float
last_price: float
volume: float
high: float
low: float
frr_amount_available: float
@dataclass
class TickersHistory(_Type):
symbol: str
bid: float
ask: float
mts: int
@dataclass
class TradingPairTrade(_Type):
id: int
mts: int
amount: float
price: float
@dataclass
class FundingCurrencyTrade(_Type):
id: int
mts: int
amount: float
rate: float
period: int
@dataclass
class TradingPairBook(_Type):
price: float
count: int
amount: float
@dataclass
class FundingCurrencyBook(_Type):
rate: float
period: int
count: int
amount: float
@dataclass
class TradingPairRawBook(_Type):
order_id: int
price: float
amount: float
@dataclass
class FundingCurrencyRawBook(_Type):
offer_id: int
period: int
rate: float
amount: float
@dataclass
class Statistic(_Type):
mts: int
value: float
@dataclass
class Candle(_Type):
mts: int
open: float
close: float
high: float
low: float
volume: float
@dataclass
class DerivativesStatus(_Type):
key: Optional[str]
mts: int
deriv_price: float
spot_price: float
insurance_fund_balance: float
next_funding_evt_timestamp_ms: int
next_funding_accrued: float
next_funding_step: int
current_funding: float
mark_price: float
open_interest: float
clamp_min: float
clamp_max: float
@dataclass
class Liquidation(_Type):
pos_id: int
mts: int
symbol: str
amount: float
base_price: float
is_match: int
is_market_sold: int
price_acquired: float
@dataclass
class Leaderboard(_Type):
mts: int
username: str
ranking: int
value: float
twitter_handle: Optional[str]
@dataclass
class FundingStatistic(_Type):
timestamp: int
frr: float
avg_period: float
funding_amount: float
funding_amount_used: float
funding_below_threshold: float
@dataclass
class PulseProfile(_Type):
puid: str
mts: int
nickname: str
picture: str
text: str
twitter_handle: str
followers: int
following: int
tipping_status: int
@dataclass
class PulseMessage(_Type):
pid: str
mts: int
puid: str
title: str
content: str
is_pin: int
is_public: int
comments_disabled: int
tags: List[str]
attachments: List[str]
meta: List[JSON]
likes: int
profile: PulseProfile
comments: int
@dataclass
class TradingMarketAveragePrice(_Type):
price_avg: float
amount: float
@dataclass
class FundingMarketAveragePrice(_Type):
rate_avg: float
amount: float
@dataclass
class FxRate(_Type):
current_rate: float
#endregion
#region Type hinting for Rest Authenticated Endpoints
@dataclass
class UserInfo(_Type):
id: int
email: str
username: str
mts_account_create: int
verified: int
verification_level: int
timezone: str
locale: str
company: str
email_verified: int
mts_master_account_create: int
group_id: int
master_account_id: int
inherit_master_account_verification: int
is_group_master: int
group_withdraw_enabled: int
ppt_enabled: int
merchant_enabled: int
competition_enabled: int
two_factors_authentication_modes: List[str]
is_securities_master: int
securities_enabled: int
allow_disable_ctxswitch: int
time_last_login: int
ctxtswitch_disabled: int
comp_countries: List[str]
compl_countries_resid: List[str]
is_merchant_enterprise: int
@dataclass
class LoginHistory(_Type):
id: int
time: int
ip: str
extra_info: JSON
@dataclass
class BalanceAvailable(_Type):
amount: float
@dataclass
class Order(_Type):
id: int
gid: int
cid: int
symbol: str
mts_create: int
mts_update: int
amount: float
amount_orig: float
order_type: str
type_prev: str
mts_tif: int
flags: int
order_status: str
price: float
price_avg: float
price_trailing: float
price_aux_limit: float
notify: int
hidden: int
placed_id: int
routing: str
meta: JSON
@dataclass
class Position(_Type):
symbol: str
status: str
amount: float
base_price: float
margin_funding: float
margin_funding_type: int
pl: float
pl_perc: float
price_liq: float
leverage: float
position_id: int
mts_create: int
mts_update: int
type: int
collateral: float
collateral_min: float
meta: JSON
@dataclass
class Trade(_Type):
id: int
symbol: str
mts_create: int
order_id: int
exec_amount: float
exec_price: float
order_type: str
order_price: float
maker:int
fee: float
fee_currency: str
cid: int
@dataclass()
class FundingTrade(_Type):
id: int
currency: str
mts_create: int
offer_id: int
amount: float
rate: float
period: int
@dataclass
class OrderTrade(_Type):
id: int
symbol: str
mts_create: int
order_id: int
exec_amount: float
exec_price: float
maker:int
fee: float
fee_currency: str
cid: int
@dataclass
class Ledger(_Type):
id: int
currency: str
mts: int
amount: float
balance: float
description: str
@dataclass
class FundingOffer(_Type):
id: int
symbol: str
mts_create: int
mts_update: int
amount: float
amount_orig: float
offer_type: str
flags: int
offer_status: str
rate: float
period: int
notify: int
hidden: int
renew: int
@dataclass
class FundingCredit(_Type):
id: int
symbol: str
side: int
mts_create: int
mts_update: int
amount: float
flags: int
status: str
rate_type: str
rate: float
period: int
mts_opening: int
mts_last_payout: int
notify: int
hidden: int
renew: int
no_close: int
position_pair: str
@dataclass
class FundingLoan(_Type):
id: int
symbol: str
side: int
mts_create: int
mts_update: int
amount: float
flags: int
status: str
rate_type: str
rate: float
period: int
mts_opening: int
mts_last_payout: int
notify: int
hidden: int
renew: int
no_close: int
@dataclass
class FundingAutoRenew(_Type):
currency: str
period: int
rate: float
threshold: float
@dataclass()
class FundingInfo(_Type):
symbol: str
yield_loan: float
yield_lend: float
duration_loan: float
duration_lend: float
@dataclass
class Wallet(_Type):
wallet_type: str
currency: str
balance: float
unsettled_interest: float
available_balance: float
last_change: str
trade_details: JSON
@dataclass
class Transfer(_Type):
mts: int
wallet_from: str
wallet_to: str
currency: str
currency_to: str
amount: int
@dataclass
class Withdrawal(_Type):
withdrawal_id: int
method: str
payment_id: str
wallet: str
amount: float
withdrawal_fee: float
@dataclass
class DepositAddress(_Type):
method: str
currency_code: str
address: str
pool_address: str
@dataclass
class LightningNetworkInvoice(_Type):
invoice_hash: str
invoice: str
amount: str
@dataclass
class Movement(_Type):
id: str
currency: str
currency_name: str
mts_start: int
mts_update: int
status: str
amount: int
fees: int
destination_address: str
transaction_id: str
withdraw_transaction_note: str
@dataclass
class SymbolMarginInfo(_Type):
symbol: str
tradable_balance: float
gross_balance: float
buy: float
sell: float
@dataclass
class BaseMarginInfo(_Type):
user_pl: float
user_swaps: float
margin_balance: float
margin_net: float
margin_min: float
@dataclass
class PositionClaim(_Type):
symbol: str
position_status: str
amount: float
base_price: float
margin_funding: float
margin_funding_type: int
position_id: int
mts_create: int
mts_update: int
pos_type: int
collateral: str
min_collateral: str
meta: JSON
@dataclass
class PositionIncreaseInfo(_Type):
max_pos: int
current_pos: float
base_currency_balance: float
tradable_balance_quote_currency: float
tradable_balance_quote_total: float
tradable_balance_base_currency: float
tradable_balance_base_total: float
funding_avail: float
funding_value: float
funding_required: float
funding_value_currency: str
funding_required_currency: str
@dataclass
class PositionIncrease(_Type):
symbol: str
amount: float
base_price: float
@dataclass
class PositionHistory(_Type):
symbol: str
status: str
amount: float
base_price: float
funding: float
funding_type: int
position_id: int
mts_create: int
mts_update: int
@dataclass
class PositionSnapshot(_Type):
symbol: str
status: str
amount: float
base_price: float
funding: float
funding_type: int
position_id: int
mts_create: int
mts_update: int
@dataclass
class PositionAudit(_Type):
symbol: str
status: str
amount: float
base_price: float
funding: float
funding_type: int
position_id: int
mts_create: int
mts_update: int
type: int
collateral: float
collateral_min: float
meta: JSON
@dataclass
class DerivativePositionCollateral(_Type):
status: int
@dataclass
class DerivativePositionCollateralLimits(_Type):
min_collateral: float
max_collateral: float
#endregion
#region Type hinting for Rest Merchant Endpoints
@compose(dataclass, partial)
class InvoiceSubmission(_Type):
id: str
t: int
type: Literal["ECOMMERCE", "POS"]
duration: int
amount: float
currency: str
order_id: str
pay_currencies: List[str]
webhook: str
redirect_url: str
status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"]
customer_info: "CustomerInfo"
invoices: List["Invoice"]
payment: "Payment"
additional_payments: List["Payment"]
merchant_name: str
@classmethod
def parse(cls, data: Dict[str, Any]) -> "InvoiceSubmission":
if "customer_info" in data and data["customer_info"] != None:
data["customer_info"] = InvoiceSubmission.CustomerInfo(**data["customer_info"])
for index, invoice in enumerate(data["invoices"]):
data["invoices"][index] = InvoiceSubmission.Invoice(**invoice)
if "payment" in data and data["payment"] != None:
data["payment"] = InvoiceSubmission.Payment(**data["payment"])
if "additional_payments" in data and data["additional_payments"] != None:
for index, additional_payment in enumerate(data["additional_payments"]):
data["additional_payments"][index] = InvoiceSubmission.Payment(**additional_payment)
return InvoiceSubmission(**data)
@compose(dataclass, partial)
class CustomerInfo:
nationality: str
resid_country: str
resid_state: str
resid_city: str
resid_zip_code: str
resid_street: str
resid_building_no: str
full_name: str
email: str
tos_accepted: bool
@compose(dataclass, partial)
class Invoice:
amount: float
currency: str
pay_currency: str
pool_currency: str
address: str
ext: JSON
@compose(dataclass, partial)
class Payment:
txid: str
amount: float
currency: str
method: str
status: Literal["CREATED", "COMPLETED", "PROCESSING"]
confirmations: int
created_at: str
updated_at: str
deposit_id: int
ledger_id: int
force_completed: bool
amount_diff: str
@dataclass
class InvoiceStats(_Type):
time: str
count: float
@dataclass
class CurrencyConversion(_Type):
base_currency: str
convert_currency: str
created: int
#endregion

View File

@@ -1,261 +0,0 @@
from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any
from .. notification import Notification
JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]]
#region Type hinting for Rest Public Endpoints
class PlatformStatus(TypedDict):
OPERATIVE: int
class TradingPairTicker(TypedDict):
SYMBOL: Optional[str]
BID: float
BID_SIZE: float
ASK: float
ASK_SIZE: float
DAILY_CHANGE: float
DAILY_CHANGE_RELATIVE: float
LAST_PRICE: float
VOLUME: float
HIGH: float
LOW: float
class FundingCurrencyTicker(TypedDict):
SYMBOL: Optional[str]
FRR: float
BID: float
BID_PERIOD: int
BID_SIZE: float
ASK: float
ASK_PERIOD: int
ASK_SIZE: float
DAILY_CHANGE: float
DAILY_CHANGE_RELATIVE: float
LAST_PRICE: float
VOLUME: float
HIGH: float
LOW: float
FRR_AMOUNT_AVAILABLE: float
class TickersHistory(TypedDict):
SYMBOL: str
BID: float
ASK: float
MTS: int
class TradingPairTrade(TypedDict):
ID: int
MTS: int
AMOUNT: float
PRICE: float
class FundingCurrencyTrade(TypedDict):
ID: int
MTS: int
AMOUNT: float
RATE: float
PERIOD: int
class TradingPairBook(TypedDict):
PRICE: float
COUNT: int
AMOUNT: float
class FundingCurrencyBook(TypedDict):
RATE: float
PERIOD: int
COUNT: int
AMOUNT: float
class TradingPairRawBook(TypedDict):
ORDER_ID: int
PRICE: float
AMOUNT: float
class FundingCurrencyRawBook(TypedDict):
OFFER_ID: int
PERIOD: int
RATE: float
AMOUNT: float
class Statistic(TypedDict):
MTS: int
VALUE: float
class Candle(TypedDict):
MTS: int
OPEN: float
CLOSE: float
HIGH: float
LOW: float
VOLUME: float
class DerivativesStatus(TypedDict):
KEY: Optional[str]
MTS: int
DERIV_PRICE: float
SPOT_PRICE: float
INSURANCE_FUND_BALANCE: float
NEXT_FUNDING_EVT_TIMESTAMP_MS: int
NEXT_FUNDING_ACCRUED: float
NEXT_FUNDING_STEP: int
CURRENT_FUNDING: float
MARK_PRICE: float
OPEN_INTEREST: float
CLAMP_MIN: float
CLAMP_MAX: float
class Liquidation(TypedDict):
POS_ID: int
MTS: int
SYMBOL: str
AMOUNT: float
BASE_PRICE: float
IS_MATCH: int
IS_MARKET_SOLD: int
PRICE_ACQUIRED: float
class Leaderboard(TypedDict):
MTS: int
USERNAME: str
RANKING: int
VALUE: float
TWITTER_HANDLE: Optional[str]
class FundingStatistic(TypedDict):
TIMESTAMP: int
FRR: float
AVG_PERIOD: float
FUNDING_AMOUNT: float
FUNDING_AMOUNT_USED: float
FUNDING_BELOW_THRESHOLD: float
#endregion
#region Type hinting for Rest Authenticated Endpoints
class Wallet(TypedDict):
WALLET_TYPE: str
CURRENCY: str
BALANCE: float
UNSETTLED_INTEREST: float
AVAILABLE_BALANCE: float
LAST_CHANGE: str
TRADE_DETAILS: JSON
class Order(TypedDict):
ID: int
GID: int
CID: int
SYMBOL: str
MTS_CREATE: int
MTS_UPDATE: int
AMOUNT: float
AMOUNT_ORIG: float
ORDER_TYPE: str
TYPE_PREV: str
MTS_TIF: int
FLAGS: int
ORDER_STATUS: str
PRICE: float
PRICE_AVG: float
PRICE_TRAILING: float
PRICE_AUX_LIMIT: float
NOTIFY: int
HIDDEN: int
PLACED_ID: int
ROUTING: str
META: JSON
class 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
class FundingOffer(TypedDict):
ID: int
SYMBOL: str
MTS_CREATE: int
MTS_UPDATE: int
AMOUNT: float
AMOUNT_ORIG: float
OFFER_TYPE: str
FLAGS: int
OFFER_STATUS: str
RATE: float
PERIOD: int
NOTIFY: bool
HIDDEN: int
RENEW: bool
class Trade(TypedDict):
ID: int
SYMBOL: str
MTS_CREATE: int
ORDER_ID: int
EXEC_AMOUNT: float
EXEC_PRICE: float
ORDER_TYPE: str
ORDER_PRICE: float
MAKER:int
FEE: float
FEE_CURRENCY: str
CID: int
class OrderTrade(TypedDict):
ID: int
SYMBOL: str
MTS_CREATE: int
ORDER_ID: int
EXEC_AMOUNT: float
EXEC_PRICE: float
MAKER:int
FEE: float
FEE_CURRENCY: str
CID: int
class Ledger(TypedDict):
ID: int
CURRENCY: str
MTS: int
AMOUNT: float
BALANCE: float
description: str
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
#endregion

18
bfxapi/tests/__init__.py Normal file
View File

@@ -0,0 +1,18 @@
import unittest
from .test_rest_serializers import TestRestSerializers
from .test_websocket_serializers import TestWebsocketSerializers
from .test_labeler import TestLabeler
from .test_notification import TestNotification
NAME = "tests"
def suite():
return unittest.TestSuite([
unittest.makeSuite(TestRestSerializers),
unittest.makeSuite(TestWebsocketSerializers),
unittest.makeSuite(TestLabeler),
unittest.makeSuite(TestNotification),
])
if __name__ == "__main__":
unittest.TextTestRunner().run(suite())

View File

@@ -0,0 +1,56 @@
import unittest
from dataclasses import dataclass
from ..exceptions import LabelerSerializerException
from ..labeler import _Type, generate_labeler_serializer, generate_recursive_serializer
class TestLabeler(unittest.TestCase):
def test_generate_labeler_serializer(self):
@dataclass
class Test(_Type):
A: int
B: float
C: str
labels = [ "A", "_PLACEHOLDER", "B", "_PLACEHOLDER", "C" ]
serializer = generate_labeler_serializer("Test", Test, labels)
self.assertEqual(serializer.parse(5, None, 65.0, None, "X"), Test(5, 65.0, "X"),
msg="_Serializer should produce the right result.")
self.assertEqual(serializer.parse(5, 65.0, "X", skip=[ "_PLACEHOLDER" ]), Test(5, 65.0, "X"),
msg="_Serializer should produce the right result when skip parameter is given.")
self.assertListEqual(serializer.get_labels(), [ "A", "B", "C" ],
msg="_Serializer::get_labels() should return the right list of labels.")
with self.assertRaises(LabelerSerializerException,
msg="_Serializer should raise LabelerSerializerException if given fewer arguments than the serializer labels."):
serializer.parse(5, 65.0, "X")
def test_generate_recursive_serializer(self):
@dataclass
class Outer(_Type):
A: int
B: float
C: "Middle"
@dataclass
class Middle(_Type):
D: str
E: "Inner"
@dataclass
class Inner(_Type):
F: bool
inner = generate_labeler_serializer("Inner", Inner, ["F"])
middle = generate_recursive_serializer("Middle", Middle, ["D", "E"], { "E": inner })
outer = generate_recursive_serializer("Outer", Outer, ["A", "B", "C"], { "C": middle })
self.assertEqual(outer.parse(10, 45.5, [ "Y", [ True ] ]), Outer(10, 45.5, Middle("Y", Inner(True))),
msg="_RecursiveSerializer should produce the right result.")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,25 @@
import unittest
from dataclasses import dataclass
from ..labeler import generate_labeler_serializer
from ..notification import _Type, _Notification, Notification
class TestNotification(unittest.TestCase):
def test_notification(self):
@dataclass
class Test(_Type):
A: int
B: float
C: str
test = generate_labeler_serializer("Test", Test,
[ "A", "_PLACEHOLDER", "B", "_PLACEHOLDER", "C" ])
notification = _Notification[Test](test)
self.assertEqual(notification.parse(*[1675787861506, "test", None, None, [ 5, None, 65.0, None, "X" ], 0, "SUCCESS", "This is just a test notification."]),
Notification[Test](1675787861506, "test", None, Test(5, 65.0, "X"), 0, "SUCCESS", "This is just a test notification."),
msg="_Notification should produce the right notification.")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,17 @@
import unittest
from ..labeler import _Type
from ..rest import serializers
class TestRestSerializers(unittest.TestCase):
def test_rest_serializers(self):
for serializer in map(serializers.__dict__.get, serializers.__serializers__):
self.assertTrue(issubclass(serializer.klass, _Type),
f"_Serializer <{serializer.name}>: .klass field must be a subclass of _Type (got {serializer.klass}).")
self.assertListEqual(serializer.get_labels(), list(serializer.klass.__annotations__),
f"_Serializer <{serializer.name}> and _Type <{serializer.klass.__name__}> must have matching labels and fields.")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,17 @@
import unittest
from ..labeler import _Type
from ..websocket import serializers
class TestWebsocketSerializers(unittest.TestCase):
def test_websocket_serializers(self):
for serializer in map(serializers.__dict__.get, serializers.__serializers__):
self.assertTrue(issubclass(serializer.klass, _Type),
f"_Serializer <{serializer.name}>: .klass field must be a subclass of _Type (got {serializer.klass}).")
self.assertListEqual(serializer.get_labels(), list(serializer.klass.__annotations__),
f"_Serializer <{serializer.name}> and _Type <{serializer.klass.__name__}> must have matching labels and fields.")
if __name__ == "__main__":
unittest.main()

7
bfxapi/urls.py Normal file
View File

@@ -0,0 +1,7 @@
REST_HOST = "https://api.bitfinex.com/v2"
PUB_REST_HOST = "https://api-pub.bitfinex.com/v2"
STAGING_REST_HOST = "https://api.staging.bitfinex.com/v2"
WSS_HOST = "wss://api.bitfinex.com/ws/2"
PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2"
STAGING_WSS_HOST = "wss://api.staging.bitfinex.com/ws/2"

View File

@@ -0,0 +1,29 @@
import json
from decimal import Decimal
from datetime import datetime
from typing import Type, List, Dict, Union, Any
JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]]
def _strip(dictionary: Dict) -> Dict:
return { key: value for key, value in dictionary.items() if value != None}
def _convert_float_to_str(data: JSON) -> JSON:
if isinstance(data, float):
return format(Decimal(repr(data)), "f")
elif isinstance(data, list):
return [ _convert_float_to_str(sub_data) for sub_data in data ]
elif isinstance(data, dict):
return _strip({ key: _convert_float_to_str(value) for key, value in data.items() })
else: return data
class JSONEncoder(json.JSONEncoder):
def encode(self, obj: JSON) -> str:
return json.JSONEncoder.encode(self, _convert_float_to_str(obj))
def default(self, obj: Any) -> Any:
if isinstance(obj, Decimal): return format(obj, "f")
elif isinstance(obj, datetime): return str(obj)
return json.JSONEncoder.default(self, obj)

View File

@@ -0,0 +1,22 @@
import re
from typing import TypeVar, Callable, Dict, Any, cast
T = TypeVar("T")
_to_snake_case: Callable[[str], str] = lambda string: re.sub(r"(?<!^)(?=[A-Z])", "_", string).lower()
_to_camel_case: Callable[[str], str] = lambda string: (components := string.split("_"))[0] + str().join(c.title() for c in components[1:])
def _scheme(data: T, adapter: Callable[[str], str]) -> T:
if isinstance(data, list):
return cast(T, [ _scheme(sub_data, adapter) for sub_data in data ])
elif isinstance(data, dict):
return cast(T, { adapter(key): _scheme(value, adapter) for key, value in data.items() })
else: return data
def to_snake_case_keys(dictionary: T) -> T:
return _scheme(dictionary, _to_snake_case)
def to_camel_case_keys(dictionary: T) -> T:
return _scheme(dictionary, _to_camel_case)

View File

@@ -1,4 +0,0 @@
import time
def generate_unique_cid(multiplier: int = 1000) -> int:
return int(round(time.time() * multiplier))

View File

@@ -1,9 +0,0 @@
import json
from decimal import Decimal
from datetime import datetime
class JSONEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Decimal) or isinstance(obj, datetime):
return str(obj)
return json.JSONEncoder.default(self, obj)

View File

@@ -1,29 +0,0 @@
from .. enums import Flag
def calculate_order_flags(
hidden : bool = False,
close : bool = False,
reduce_only : bool = False,
post_only : bool = False,
oco : bool = False,
no_var_rates: bool = False
) -> int:
flags = 0
if hidden: flags += Flag.HIDDEN
if close: flags += Flag.CLOSE
if reduce_only: flags += Flag.REDUCE_ONLY
if post_only: flags += Flag.POST_ONLY
if oco: flags += Flag.OCO
if no_var_rates: flags += Flag.NO_VAR_RATES
return flags
def calculate_offer_flags(
hidden : bool = False
) -> int:
flags = 0
if hidden: flags += Flag.HIDDEN
return flags

View File

@@ -1,43 +0,0 @@
from typing import cast, TypeVar, Union
from .. exceptions import IntegerUnderflowError, IntegerOverflowflowError
__all__ = [ "Int16", "Int32", "Int45", "Int64" ]
T = TypeVar("T")
class _Int(int):
def __new__(cls: T, integer: int) -> T:
assert hasattr(cls, "_BITS"), "_Int must be extended by a class that has a static member _BITS (indicating the number of bits with which to represent the integers)."
bits = cls._BITS - 1
min, max = -(2 ** bits), (2 ** bits) - 1
if integer < min:
raise IntegerUnderflowError(f"Underflow. Cannot store <{integer}> in {cls._BITS} bits integer. The min and max bounds are {min} and {max}.")
if integer > max:
raise IntegerOverflowflowError(f"Overflow. Cannot store <{integer}> in {cls._BITS} bits integer. The min and max bounds are {min} and {max}.")
return cast(T, super().__new__(int, integer))
class Int16(_Int):
_BITS = 16
int16 = Union[Int16, int]
class Int32(_Int):
_BITS = 32
int32 = Union[Int32, int]
class Int45(_Int):
_BITS = 45
int45 = Union[Int45, int]
class Int64(_Int):
_BITS = 64
int64 = Union[Int64, int]

View File

@@ -1,99 +1,52 @@
"""
Module used to describe all of the different data types
"""
import logging
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
RESET_SEQ = "\033[0m"
COLOR_SEQ = "\033[1;%dm"
ITALIC_COLOR_SEQ = "\033[3;%dm"
UNDERLINE_COLOR_SEQ = "\033[4;%dm"
BOLD_SEQ = "\033[1m"
UNDERLINE_SEQ = "\033[04m"
YELLOW = '\033[93m'
WHITE = '\33[37m'
BLUE = '\033[34m'
LIGHT_BLUE = '\033[94m'
RED = '\033[91m'
GREY = '\33[90m'
KEYWORD_COLORS = {
'WARNING': YELLOW,
'INFO': LIGHT_BLUE,
'DEBUG': WHITE,
'CRITICAL': YELLOW,
'ERROR': RED,
'TRADE': '\33[102m\33[30m'
}
def formatter_message(message, use_color = True):
"""
Syntax highlight certain keywords
"""
if use_color:
message = message.replace("$RESET", RESET_SEQ).replace("$BOLD", BOLD_SEQ)
else:
message = message.replace("$RESET", "").replace("$BOLD", "")
return message
def format_word(message, word, color_seq, bold=False, underline=False):
"""
Surround the given word with a sequence
"""
replacer = color_seq + word + RESET_SEQ
if underline:
replacer = UNDERLINE_SEQ + replacer
if bold:
replacer = BOLD_SEQ + replacer
return message.replace(word, replacer)
COLORS = {
"DEBUG": CYAN,
"INFO": BLUE,
"WARNING": YELLOW,
"ERROR": RED
}
class Formatter(logging.Formatter):
"""
This Formatted simply colors in the levelname i.e 'INFO', 'DEBUG'
"""
def __init__(self, msg, use_color = True):
logging.Formatter.__init__(self, msg)
self.use_color = use_color
class _ColoredFormatter(logging.Formatter):
def __init__(self, msg, use_color = True):
logging.Formatter.__init__(self, msg, "%d-%m-%Y %H:%M:%S")
self.use_color = use_color
def format(self, record):
"""
Format and highlight certain keywords
"""
levelname = record.levelname
if self.use_color and levelname in KEYWORD_COLORS:
levelname_color = KEYWORD_COLORS[levelname] + levelname + RESET_SEQ
record.levelname = levelname_color
record.name = GREY + record.name + RESET_SEQ
return logging.Formatter.format(self, record)
def format(self, record):
levelname = record.levelname
if self.use_color and levelname in COLORS:
levelname_color = COLOR_SEQ % (30 + COLORS[levelname]) + levelname + RESET_SEQ
record.levelname = levelname_color
record.name = ITALIC_COLOR_SEQ % (30 + BLACK) + record.name + RESET_SEQ
return logging.Formatter.format(self, record)
class CustomLogger(logging.Logger):
"""
This adds extra logging functions such as logger.trade and also
sets the logger to use the custom formatter
"""
FORMAT = "[$BOLD%(name)s$RESET] [%(levelname)s] %(message)s"
class ColoredLogger(logging.Logger):
FORMAT = "[$BOLD%(name)s$RESET] [%(asctime)s] [%(levelname)s] %(message)s"
COLOR_FORMAT = formatter_message(FORMAT, True)
TRADE = 50
def __init__(self, name, level):
logging.Logger.__init__(self, name, level)
def __init__(self, name, logLevel='DEBUG'):
logging.Logger.__init__(self, name, logLevel)
color_formatter = Formatter(self.COLOR_FORMAT)
colored_formatter = _ColoredFormatter(self.COLOR_FORMAT)
console = logging.StreamHandler()
console.setFormatter(color_formatter)
self.addHandler(console)
logging.addLevelName(self.TRADE, "TRADE")
return
console.setFormatter(colored_formatter)
def set_level(self, level):
logging.Logger.setLevel(self, level)
def trade(self, message, *args, **kws):
"""
Print a syntax highlighted trade signal
"""
if self.isEnabledFor(self.TRADE):
message = format_word(message, 'CLOSED ', YELLOW, bold=True)
message = format_word(message, 'OPENED ', LIGHT_BLUE, bold=True)
message = format_word(message, 'UPDATED ', BLUE, bold=True)
message = format_word(message, 'CLOSED_ALL ', RED, bold=True)
# Yes, logger takes its '*args' as 'args'.
self._log(self.TRADE, message, args, **kws)
self.addHandler(console)

View File

@@ -1,231 +0,0 @@
import traceback, json, asyncio, hmac, hashlib, time, uuid, websockets
from typing import Literal, TypeVar, Callable, cast
from pyee.asyncio import AsyncIOEventEmitter
from ._BfxWebsocketInputs import _BfxWebsocketInputs
from .handlers import Channels, PublicChannelsHandler, AuthenticatedChannelsHandler
from .exceptions import ConnectionNotOpen, TooManySubscriptions, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion
from ..utils.encoder import JSONEncoder
from ..utils.logger import Formatter, CustomLogger
_HEARTBEAT = "hb"
F = TypeVar("F", bound=Callable[..., Literal[None]])
def _require_websocket_connection(function: F) -> F:
async def wrapper(self, *args, **kwargs):
if self.websocket == None or self.websocket.open == False:
raise ConnectionNotOpen("No open connection with the server.")
await function(self, *args, **kwargs)
return cast(F, wrapper)
def _require_websocket_authentication(function: F) -> F:
async def wrapper(self, *args, **kwargs):
if self.authentication == False:
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
MAXIMUM_BUCKETS_AMOUNT = 20
EVENTS = [
"open", "subscribed", "authenticated", "wss-error",
*PublicChannelsHandler.EVENTS,
*AuthenticatedChannelsHandler.EVENTS
]
def __init__(self, host, buckets=5, log_level = "WARNING", API_KEY=None, API_SECRET=None, filter=None):
self.host, self.websocket, self.event_emitter = host, None, AsyncIOEventEmitter()
self.event_emitter.add_listener("error",
lambda exception: self.logger.error("\n" + str().join(traceback.format_exception(type(exception), exception, exception.__traceback__))[:-1])
)
self.API_KEY, self.API_SECRET, self.filter, self.authentication = API_KEY, API_SECRET, filter, False
self.handler = AuthenticatedChannelsHandler(event_emitter=self.event_emitter)
self.buckets = [ _BfxWebsocketBucket(self.host, self.event_emitter, self.__bucket_open_signal) for _ in range(buckets) ]
self.inputs = _BfxWebsocketInputs(self.__handle_websocket_input)
self.logger = CustomLogger("BfxWebsocketClient", logLevel=log_level)
if buckets > BfxWebsocketClient.MAXIMUM_BUCKETS_AMOUNT:
self.logger.warning(f"It is not safe to use more than {BfxWebsocketClient.MAXIMUM_BUCKETS_AMOUNT} buckets from the same connection ({buckets} in use), the server could momentarily block the client with <429 Too Many Requests>.")
def run(self):
return asyncio.run(self.start())
async def start(self):
tasks = [ bucket._connect(index) for index, bucket in enumerate(self.buckets) ]
if self.API_KEY != None and self.API_SECRET != None:
tasks.append(self.__connect(self.API_KEY, self.API_SECRET, self.filter))
await asyncio.gather(*tasks)
async def __connect(self, API_KEY, API_SECRET, filter=None):
async for websocket in websockets.connect(self.host):
self.websocket = websocket
await self.__authenticate(API_KEY, API_SECRET, filter)
try:
async for message in websocket:
message = json.loads(message)
if isinstance(message, dict) and message["event"] == "auth":
if message["status"] == "OK":
self.event_emitter.emit("authenticated", message); self.authentication = True
else: raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.")
elif isinstance(message, dict) and message["event"] == "error":
self.event_emitter.emit("wss-error", message["code"], message["msg"])
elif isinstance(message, list) and (chanId := message[0]) == 0 and message[1] != _HEARTBEAT:
self.handler.handle(message[1], message[2])
except websockets.ConnectionClosedError: continue
finally: await self.websocket.wait_closed(); break
async def __authenticate(self, API_KEY, API_SECRET, filter=None):
data = { "event": "auth", "filter": filter, "apiKey": API_KEY }
data["authNonce"] = int(time.time()) * 1000
data["authPayload"] = "AUTH" + str(data["authNonce"])
data["authSig"] = hmac.new(
API_SECRET.encode("utf8"),
data["authPayload"].encode("utf8"),
hashlib.sha384
).hexdigest()
await self.websocket.send(json.dumps(data))
async def subscribe(self, channel, **kwargs):
counters = [ len(bucket.pendings) + len(bucket.subscriptions) for bucket in self.buckets ]
index = counters.index(min(counters))
await self.buckets[index]._subscribe(channel, **kwargs)
async def unsubscribe(self, chanId):
for bucket in self.buckets:
if chanId in bucket.subscriptions.keys():
await bucket._unsubscribe(chanId=chanId)
async def close(self, code=1000, reason=str()):
if self.websocket != None and self.websocket.open == True:
await self.websocket.close(code=code, reason=reason)
for bucket in self.buckets:
await bucket._close(code=code, reason=reason)
@_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
async def __handle_websocket_input(self, input, data):
await self.websocket.send(json.dumps([ 0, input, None, data], cls=JSONEncoder))
def __bucket_open_signal(self, index):
if all(bucket.websocket != None and bucket.websocket.open == True for bucket in self.buckets):
self.event_emitter.emit("open")
def on(self, event, callback = None):
if event not in BfxWebsocketClient.EVENTS:
raise EventNotSupported(f"Event <{event}> is not supported. To get a list of available events print BfxWebsocketClient.EVENTS")
if callback != None:
return self.event_emitter.on(event, callback)
def handler(function):
self.event_emitter.on(event, function)
return handler
def once(self, event, callback = None):
if event not in BfxWebsocketClient.EVENTS:
raise EventNotSupported(f"Event <{event}> is not supported. To get a list of available events print BfxWebsocketClient.EVENTS")
if callback != None:
return self.event_emitter.once(event, callback)
def handler(function):
self.event_emitter.once(event, function)
return handler
class _BfxWebsocketBucket(object):
MAXIMUM_SUBSCRIPTIONS_AMOUNT = 25
def __init__(self, host, event_emitter, __bucket_open_signal):
self.host, self.event_emitter, self.__bucket_open_signal = host, event_emitter, __bucket_open_signal
self.websocket, self.subscriptions, self.pendings = None, dict(), list()
self.handler = PublicChannelsHandler(event_emitter=self.event_emitter)
async def _connect(self, index):
async for websocket in websockets.connect(self.host):
self.websocket = websocket
self.__bucket_open_signal(index)
try:
async for message in websocket:
message = json.loads(message)
if isinstance(message, dict) and message["event"] == "info" and "version" in message:
if BfxWebsocketClient.VERSION != message["version"]:
raise OutdatedClientVersion(f"Mismatch between the client version and the server version. Update the library to the latest version to continue (client version: {BfxWebsocketClient.VERSION}, server version: {message['version']}).")
elif isinstance(message, dict) and message["event"] == "subscribed" and (chanId := message["chanId"]):
self.pendings = [ pending for pending in self.pendings if pending["subId"] != message["subId"] ]
self.subscriptions[chanId] = message
self.event_emitter.emit("subscribed", message)
elif isinstance(message, dict) and message["event"] == "unsubscribed" and (chanId := message["chanId"]):
if message["status"] == "OK":
del self.subscriptions[chanId]
elif isinstance(message, dict) and message["event"] == "error":
self.event_emitter.emit("wss-error", message["code"], message["msg"])
elif isinstance(message, list) and (chanId := message[0]) and message[1] != _HEARTBEAT:
self.handler.handle(self.subscriptions[chanId], *message[1:])
except websockets.ConnectionClosedError: continue
finally: await self.websocket.wait_closed(); break
@_require_websocket_connection
async def _subscribe(self, channel, subId=None, **kwargs):
if len(self.subscriptions) + len(self.pendings) == _BfxWebsocketBucket.MAXIMUM_SUBSCRIPTIONS_AMOUNT:
raise TooManySubscriptions("The client has reached the maximum number of subscriptions.")
subscription = {
"event": "subscribe",
"channel": channel,
"subId": subId or str(uuid.uuid4()),
**kwargs
}
self.pendings.append(subscription)
await self.websocket.send(json.dumps(subscription))
@_require_websocket_connection
async def _unsubscribe(self, chanId):
await self.websocket.send(json.dumps({
"event": "unsubscribe",
"chanId": chanId
}))
@_require_websocket_connection
async def _close(self, code=1000, reason=str()):
await self.websocket.close(code=code, reason=reason)

View File

@@ -1 +1,3 @@
from .BfxWebsocketClient import BfxWebsocketClient
from .client import BfxWebsocketClient, BfxWebsocketBucket, BfxWebsocketInputs
NAME = "websocket"

View File

@@ -0,0 +1,5 @@
from .bfx_websocket_client import BfxWebsocketClient
from .bfx_websocket_bucket import BfxWebsocketBucket
from .bfx_websocket_inputs import BfxWebsocketInputs
NAME = "client"

View File

@@ -0,0 +1,107 @@
import json, uuid, websockets
from typing import Literal, TypeVar, Callable, cast
from ..handlers import PublicChannelsHandler
from ..exceptions import ConnectionNotOpen, TooManySubscriptions, OutdatedClientVersion
_HEARTBEAT = "hb"
F = TypeVar("F", bound=Callable[..., Literal[None]])
def _require_websocket_connection(function: F) -> F:
async def wrapper(self, *args, **kwargs):
if self.websocket == None or self.websocket.open == False:
raise ConnectionNotOpen("No open connection with the server.")
await function(self, *args, **kwargs)
return cast(F, wrapper)
class BfxWebsocketBucket(object):
VERSION = 2
MAXIMUM_SUBSCRIPTIONS_AMOUNT = 25
def __init__(self, host, event_emitter, on_open_event):
self.host, self.event_emitter, self.on_open_event = host, event_emitter, on_open_event
self.websocket, self.subscriptions, self.pendings = None, dict(), list()
self.handler = PublicChannelsHandler(event_emitter=self.event_emitter)
async def _connect(self, index):
reconnection = False
async for websocket in websockets.connect(self.host):
self.websocket = websocket
self.on_open_event.set()
if reconnection == True or (reconnection := False):
for pending in self.pendings:
await self.websocket.send(json.dumps(pending))
for _, subscription in self.subscriptions.items():
await self._subscribe(**subscription)
self.subscriptions.clear()
try:
async for message in websocket:
message = json.loads(message)
if isinstance(message, dict) and message["event"] == "subscribed" and (chanId := message["chanId"]):
self.pendings = [ pending for pending in self.pendings if pending["subId"] != message["subId"] ]
self.subscriptions[chanId] = message
self.event_emitter.emit("subscribed", message)
elif isinstance(message, dict) and message["event"] == "unsubscribed" and (chanId := message["chanId"]):
if message["status"] == "OK":
del self.subscriptions[chanId]
elif isinstance(message, dict) and message["event"] == "error":
self.event_emitter.emit("wss-error", message["code"], message["msg"])
elif isinstance(message, list) and (chanId := message[0]) and message[1] != _HEARTBEAT:
self.handler.handle(self.subscriptions[chanId], *message[1:])
except websockets.ConnectionClosedError as error:
if error.code == 1006:
self.on_open_event.clear()
reconnection = True
continue
raise error
break
@_require_websocket_connection
async def _subscribe(self, channel, subId=None, **kwargs):
if len(self.subscriptions) + len(self.pendings) == BfxWebsocketBucket.MAXIMUM_SUBSCRIPTIONS_AMOUNT:
raise TooManySubscriptions("The client has reached the maximum number of subscriptions.")
subscription = {
**kwargs,
"event": "subscribe",
"channel": channel,
"subId": subId or str(uuid.uuid4()),
}
self.pendings.append(subscription)
await self.websocket.send(json.dumps(subscription))
@_require_websocket_connection
async def _unsubscribe(self, chanId):
await self.websocket.send(json.dumps({
"event": "unsubscribe",
"chanId": chanId
}))
@_require_websocket_connection
async def _close(self, code=1000, reason=str()):
await self.websocket.close(code=code, reason=reason)
def _get_chan_id(self, subId):
for subscription in self.subscriptions.values():
if subscription["subId"] == subId:
return subscription["chanId"]

View File

@@ -0,0 +1,246 @@
import traceback, json, asyncio, hmac, hashlib, time, websockets, socket, random
from typing import cast
from collections import namedtuple
from datetime import datetime
from pyee.asyncio import AsyncIOEventEmitter
from .bfx_websocket_bucket import _HEARTBEAT, F, _require_websocket_connection, BfxWebsocketBucket
from .bfx_websocket_inputs import BfxWebsocketInputs
from ..handlers import PublicChannelsHandler, AuthenticatedChannelsHandler
from ..exceptions import WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion
from ...utils.JSONEncoder import JSONEncoder
from ...utils.logger import ColoredLogger
def _require_websocket_authentication(function: F) -> F:
async def wrapper(self, *args, **kwargs):
if hasattr(self, "authentication") and self.authentication == False:
raise WebsocketAuthenticationRequired("To perform this action you need to authenticate using your API_KEY and API_SECRET.")
await _require_websocket_connection(function)(self, *args, **kwargs)
return cast(F, wrapper)
class BfxWebsocketClient(object):
VERSION = BfxWebsocketBucket.VERSION
MAXIMUM_CONNECTIONS_AMOUNT = 20
EVENTS = [
"open", "subscribed", "authenticated", "wss-error",
*PublicChannelsHandler.EVENTS,
*AuthenticatedChannelsHandler.EVENTS
]
def __init__(self, host, credentials = None, log_level = "INFO"):
self.websocket = None
self.host, self.credentials, self.event_emitter = host, credentials, AsyncIOEventEmitter()
self.inputs = BfxWebsocketInputs(handle_websocket_input=self.__handle_websocket_input)
self.handler = AuthenticatedChannelsHandler(event_emitter=self.event_emitter)
self.logger = ColoredLogger("BfxWebsocketClient", level=log_level)
self.event_emitter.add_listener("error",
lambda exception: self.logger.error(f"{type(exception).__name__}: {str(exception)}" + "\n" +
str().join(traceback.format_exception(type(exception), exception, exception.__traceback__))[:-1])
)
def run(self, connections = 5):
return asyncio.run(self.start(connections))
async def start(self, connections = 5):
if connections > BfxWebsocketClient.MAXIMUM_CONNECTIONS_AMOUNT:
self.logger.warning(f"It is not safe to use more than {BfxWebsocketClient.MAXIMUM_CONNECTIONS_AMOUNT} buckets from the same " +
f"connection ({connections} in use), the server could momentarily block the client with <429 Too Many Requests>.")
self.on_open_events = [ asyncio.Event() for _ in range(connections) ]
self.buckets = [
BfxWebsocketBucket(self.host, self.event_emitter, self.on_open_events[index])
for index in range(connections)
]
tasks = [ bucket._connect(index) for index, bucket in enumerate(self.buckets) ]
tasks.append(self.__connect(self.credentials))
await asyncio.gather(*tasks)
async def __connect(self, credentials = None):
Reconnection = namedtuple("Reconnection", ["status", "attempts", "timestamp"])
reconnection, delay = Reconnection(status=False, attempts=0, timestamp=None), None
async def _connection():
nonlocal reconnection
async with websockets.connect(self.host) as websocket:
if reconnection.status == True:
self.logger.info(f"Reconnect attempt successful (attempt no.{reconnection.attempts}): The " +
f"client has been offline for a total of {datetime.now() - reconnection.timestamp} " +
f"(connection lost at: {reconnection.timestamp:%d-%m-%Y at %H:%M:%S}).")
reconnection = Reconnection(status=False, attempts=0, timestamp=None)
self.websocket, self.authentication = websocket, False
if await asyncio.gather(*[on_open_event.wait() for on_open_event in self.on_open_events]):
self.event_emitter.emit("open")
if self.credentials:
await self.__authenticate(**self.credentials)
async for message in websocket:
message = json.loads(message)
if isinstance(message, dict) and message["event"] == "info" and "version" in message:
if BfxWebsocketClient.VERSION != message["version"]:
raise OutdatedClientVersion(f"Mismatch between the client version and the server version. " +
f"Update the library to the latest version to continue (client version: {BfxWebsocketClient.VERSION}, " +
f"server version: {message['version']}).")
elif isinstance(message, dict) and message["event"] == "info" and message["code"] == 20051:
rcvd = websockets.frames.Close(code=1012, reason="Stop/Restart Websocket Server (please reconnect).")
raise websockets.ConnectionClosedError(rcvd=rcvd, sent=None)
elif isinstance(message, dict) and message["event"] == "auth":
if message["status"] == "OK":
self.event_emitter.emit("authenticated", message); self.authentication = True
else: raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.")
elif isinstance(message, dict) and message["event"] == "error":
self.event_emitter.emit("wss-error", message["code"], message["msg"])
elif isinstance(message, list) and (chanId := message[0]) == 0 and message[1] != _HEARTBEAT:
self.handler.handle(message[1], message[2])
class _Delay:
BACKOFF_MIN, BACKOFF_MAX = 1.92, 60.0
BACKOFF_INITIAL = 5.0
def __init__(self, backoff_factor):
self.__backoff_factor = backoff_factor
self.__backoff_delay = _Delay.BACKOFF_MIN
self.__initial_delay = random.random() * _Delay.BACKOFF_INITIAL
def next(self):
backoff_delay = self.peek()
__backoff_delay = self.__backoff_delay * self.__backoff_factor
self.__backoff_delay = min(__backoff_delay, _Delay.BACKOFF_MAX)
return backoff_delay
def peek(self):
return (self.__backoff_delay == _Delay.BACKOFF_MIN) \
and self.__initial_delay or self.__backoff_delay
while True:
if reconnection.status == True:
await asyncio.sleep(delay.next())
try:
await _connection()
except (websockets.ConnectionClosedError, socket.gaierror) as error:
if isinstance(error, websockets.ConnectionClosedError) and (error.code == 1006 or error.code == 1012):
if error.code == 1006:
self.logger.error("Connection lost: no close frame received "
+ "or sent (1006). Attempting to reconnect...")
if error.code == 1012:
self.logger.info("WSS server is about to restart, reconnection "
+ "required (client received 20051). Attempt in progress...")
reconnection = Reconnection(status=True, attempts=1, timestamp=datetime.now());
delay = _Delay(backoff_factor=1.618)
elif isinstance(error, socket.gaierror) and reconnection.status == True:
self.logger.warning(f"Reconnection attempt no.{reconnection.attempts} has failed. "
+ f"Next reconnection attempt in ~{round(delay.peek()):.1f} seconds."
+ f"(at the moment the client has been offline for {datetime.now() - reconnection.timestamp})")
reconnection = reconnection._replace(attempts=reconnection.attempts + 1)
else: raise error
if reconnection.status == False:
break
async def __authenticate(self, API_KEY, API_SECRET, filter=None):
data = { "event": "auth", "filter": filter, "apiKey": API_KEY }
data["authNonce"] = int(time.time()) * 1000
data["authPayload"] = "AUTH" + str(data["authNonce"])
data["authSig"] = hmac.new(
API_SECRET.encode("utf8"),
data["authPayload"].encode("utf8"),
hashlib.sha384
).hexdigest()
await self.websocket.send(json.dumps(data))
async def subscribe(self, channel, **kwargs):
counters = [ len(bucket.pendings) + len(bucket.subscriptions) for bucket in self.buckets ]
index = counters.index(min(counters))
await self.buckets[index]._subscribe(channel, **kwargs)
async def unsubscribe(self, subId):
for bucket in self.buckets:
if (chanId := bucket._get_chan_id(subId)):
await bucket._unsubscribe(chanId=chanId)
async def close(self, code=1000, reason=str()):
if self.websocket != None and self.websocket.open == True:
await self.websocket.close(code=code, reason=reason)
for bucket in self.buckets:
await bucket._close(code=code, reason=reason)
@_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
async def __handle_websocket_input(self, input, data):
await self.websocket.send(json.dumps([ 0, input, None, data], cls=JSONEncoder))
def on(self, *events, callback = None):
for event in events:
if event not in BfxWebsocketClient.EVENTS:
raise EventNotSupported(f"Event <{event}> is not supported. To get a list of available events print BfxWebsocketClient.EVENTS")
if callback != None:
for event in events:
self.event_emitter.on(event, callback)
if callback == None:
def handler(function):
for event in events:
self.event_emitter.on(event, function)
return handler
def once(self, *events, callback = None):
for event in events:
if event not in BfxWebsocketClient.EVENTS:
raise EventNotSupported(f"Event <{event}> is not supported. To get a list of available events print BfxWebsocketClient.EVENTS")
if callback != None:
for event in events:
self.event_emitter.once(event, callback)
if callback == None:
def handler(function):
for event in events:
self.event_emitter.once(event, function)
return handler

View File

@@ -2,77 +2,59 @@ from decimal import Decimal
from datetime import datetime
from typing import Union, Optional, List, Tuple
from .typings import JSON
from .enums import OrderType, FundingOfferType
from .. enums import OrderType, FundingOfferType
from ... utils.JSONEncoder import JSON
def _strip(dictionary):
return { key: value for key, value in dictionary.items() if value != None}
class BfxWebsocketInputs(object):
def __init__(self, handle_websocket_input):
self.handle_websocket_input = handle_websocket_input
class _BfxWebsocketInputs(object):
def __init__(self, __handle_websocket_input):
self.__handle_websocket_input = __handle_websocket_input
async def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str],
price: Optional[Union[Decimal, str]] = None, lev: Optional[int] = None,
price_trailing: Optional[Union[Decimal, str]] = None, price_aux_limit: Optional[Union[Decimal, str]] = None, price_oco_stop: Optional[Union[Decimal, str]] = None,
async def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, float, str],
price: Optional[Union[Decimal, float, str]] = None, lev: Optional[int] = None,
price_trailing: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_oco_stop: Optional[Union[Decimal, float, str]] = None,
gid: Optional[int] = None, cid: Optional[int] = None,
flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None):
data = _strip({
await self.handle_websocket_input("on", {
"type": type, "symbol": symbol, "amount": amount,
"price": price, "lev": lev,
"price_trailing": price_trailing, "price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop,
"gid": gid, "cid": cid,
"flags": flags, "tif": tif, "meta": meta
})
await self.__handle_websocket_input("on", data)
async def update_order(self, id: int, amount: Optional[Union[Decimal, str]] = None, price: Optional[Union[Decimal, str]] = None,
async def update_order(self, id: int, amount: Optional[Union[Decimal, float, str]] = None, price: Optional[Union[Decimal, float, str]] = None,
cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None,
flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, str]] = None,
price_aux_limit: Optional[Union[Decimal, str]] = None, price_trailing: Optional[Union[Decimal, str]] = None, tif: Optional[Union[datetime, str]] = None):
data = _strip({
flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, float, str]] = None,
price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, tif: Optional[Union[datetime, str]] = None):
await self.handle_websocket_input("ou", {
"id": id, "amount": amount, "price": price,
"cid": cid, "cid_date": cid_date, "gid": gid,
"flags": flags, "lev": lev, "delta": delta,
"price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif
})
await self.__handle_websocket_input("ou", data)
async def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None):
data = _strip({
"id": id,
"cid": cid,
"cid_date": cid_date
await self.handle_websocket_input("oc", {
"id": id, "cid": cid, "cid_date": cid_date
})
await self.__handle_websocket_input("oc", data)
async def cancel_order_multi(self, ids: Optional[List[int]] = None, cids: Optional[List[Tuple[int, str]]] = None, gids: Optional[List[int]] = None, all: bool = False):
data = _strip({
"ids": ids,
"cids": cids,
"gids": gids,
await self.handle_websocket_input("oc_multi", {
"ids": ids, "cids": cids, "gids": gids,
"all": int(all)
})
await self.__handle_websocket_input("oc_multi", data)
async def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, str],
rate: Union[Decimal, str], period: int,
async def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, float, str],
rate: Union[Decimal, float, str], period: int,
flags: Optional[int] = 0):
data = {
await self.handle_websocket_input("fon", {
"type": type, "symbol": symbol, "amount": amount,
"rate": rate, "period": period,
"flags": flags
}
await self.__handle_websocket_input("fon", data)
})
async def cancel_funding_offer(self, id: int):
await self.__handle_websocket_input("foc", { "id": id })
await self.handle_websocket_input("foc", { "id": id })
async def calc(self, *args: str):
await self.__handle_websocket_input("calc", list(map(lambda arg: [arg], args)))
await self.handle_websocket_input("calc", list(map(lambda arg: [arg], args)))

View File

@@ -1,6 +1,6 @@
from ..enums import *
from .. enums import *
class Channels(str, Enum):
class Channel(str, Enum):
TICKER = "ticker"
TRADES = "trades"
BOOK = "book"

View File

@@ -58,4 +58,11 @@ class InvalidAuthenticationCredentials(BfxWebsocketException):
This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication.
"""
pass
class HandlerNotFound(BfxWebsocketException):
"""
This error indicates that a handler was not found for an incoming message.
"""
pass

View File

@@ -1,186 +0,0 @@
from . import serializers
from .enums import Channels
from .exceptions import BfxWebsocketException
def _get_sub_dictionary(dictionary, keys):
return { key: dictionary[key] for key in dictionary if key in keys }
class PublicChannelsHandler(object):
EVENTS = [
"t_ticker_update", "f_ticker_update",
"t_trade_executed", "t_trade_execution_update", "f_trade_executed", "f_trade_execution_update", "t_trades_snapshot", "f_trades_snapshot",
"t_book_snapshot", "f_book_snapshot", "t_raw_book_snapshot", "f_raw_book_snapshot", "t_book_update", "f_book_update", "t_raw_book_update", "f_raw_book_update",
"candles_snapshot", "candles_update",
"derivatives_status_update",
]
def __init__(self, event_emitter):
self.event_emitter = event_emitter
self.__handlers = {
Channels.TICKER: self.__ticker_channel_handler,
Channels.TRADES: self.__trades_channel_handler,
Channels.BOOK: self.__book_channel_handler,
Channels.CANDLES: self.__candles_channel_handler,
Channels.STATUS: self.__status_channel_handler
}
def handle(self, subscription, *stream):
if channel := subscription["channel"] or channel in self.__handlers.keys():
return self.__handlers[channel](subscription, *stream)
def __ticker_channel_handler(self, subscription, *stream):
if subscription["symbol"].startswith("t"):
return self.event_emitter.emit(
"t_ticker_update",
_get_sub_dictionary(subscription, [ "chanId", "symbol", "pair" ]),
serializers.TradingPairTicker.parse(*stream[0])
)
if subscription["symbol"].startswith("f"):
return self.event_emitter.emit(
"f_ticker_update",
_get_sub_dictionary(subscription, [ "chanId", "symbol", "currency" ]),
serializers.FundingCurrencyTicker.parse(*stream[0])
)
def __trades_channel_handler(self, subscription, *stream):
if type := stream[0] or type in [ "te", "tu", "fte", "ftu" ]:
if subscription["symbol"].startswith("t"):
return self.event_emitter.emit(
{ "te": "t_trade_executed", "tu": "t_trade_execution_update" }[type],
_get_sub_dictionary(subscription, [ "chanId", "symbol", "pair" ]),
serializers.TradingPairTrade.parse(*stream[1])
)
if subscription["symbol"].startswith("f"):
return self.event_emitter.emit(
{ "fte": "f_trade_executed", "ftu": "f_trade_execution_update" }[type],
_get_sub_dictionary(subscription, [ "chanId", "symbol", "currency" ]),
serializers.FundingCurrencyTrade.parse(*stream[1])
)
if subscription["symbol"].startswith("t"):
return self.event_emitter.emit(
"t_trades_snapshot",
_get_sub_dictionary(subscription, [ "chanId", "symbol", "pair" ]),
[ serializers.TradingPairTrade.parse(*substream) for substream in stream[0] ]
)
if subscription["symbol"].startswith("f"):
return self.event_emitter.emit(
"f_trades_snapshot",
_get_sub_dictionary(subscription, [ "chanId", "symbol", "currency" ]),
[ serializers.FundingCurrencyTrade.parse(*substream) for substream in stream[0] ]
)
def __book_channel_handler(self, subscription, *stream):
subscription = _get_sub_dictionary(subscription, [ "chanId", "symbol", "prec", "freq", "len", "subId", "pair" ])
type = subscription["symbol"][0]
if subscription["prec"] == "R0":
_trading_pair_serializer, _funding_currency_serializer, IS_RAW_BOOK = serializers.TradingPairRawBook, serializers.FundingCurrencyRawBook, True
else: _trading_pair_serializer, _funding_currency_serializer, IS_RAW_BOOK = serializers.TradingPairBook, serializers.FundingCurrencyBook, False
if all(isinstance(substream, list) for substream in stream[0]):
return self.event_emitter.emit(
type + "_" + (IS_RAW_BOOK and "raw_book" or "book") + "_snapshot",
subscription,
[ { "t": _trading_pair_serializer, "f": _funding_currency_serializer }[type].parse(*substream) for substream in stream[0] ]
)
return self.event_emitter.emit(
type + "_" + (IS_RAW_BOOK and "raw_book" or "book") + "_update",
subscription,
{ "t": _trading_pair_serializer, "f": _funding_currency_serializer }[type].parse(*stream[0])
)
def __candles_channel_handler(self, subscription, *stream):
subscription = _get_sub_dictionary(subscription, [ "chanId", "key" ])
if all(isinstance(substream, list) for substream in stream[0]):
return self.event_emitter.emit(
"candles_snapshot",
subscription,
[ serializers.Candle.parse(*substream) for substream in stream[0] ]
)
return self.event_emitter.emit(
"candles_update",
subscription,
serializers.Candle.parse(*stream[0])
)
def __status_channel_handler(self, subscription, *stream):
subscription = _get_sub_dictionary(subscription, [ "chanId", "key" ])
if subscription["key"].startswith("deriv:"):
return self.event_emitter.emit(
"derivatives_status_update",
subscription,
serializers.DerivativesStatus.parse(*stream[0])
)
class AuthenticatedChannelsHandler(object):
__abbreviations = {
"os": "order_snapshot", "on": "order_new", "ou": "order_update", "oc": "order_cancel",
"ps": "position_snapshot", "pn": "position_new", "pu": "position_update", "pc": "position_close",
"te": "trade_executed", "tu": "trade_execution_update",
"fos": "funding_offer_snapshot", "fon": "funding_offer_new", "fou": "funding_offer_update", "foc": "funding_offer_cancel",
"fcs": "funding_credit_snapshot", "fcn": "funding_credit_new", "fcu": "funding_credit_update", "fcc": "funding_credit_close",
"fls": "funding_loan_snapshot", "fln": "funding_loan_new", "flu": "funding_loan_update", "flc": "funding_loan_close",
"ws": "wallet_snapshot", "wu": "wallet_update",
"bu": "balance_update",
}
__serializers = {
("os", "on", "ou", "oc",): serializers.Order,
("ps", "pn", "pu", "pc",): serializers.Position,
("te",): serializers.TradeExecuted,
("tu",): serializers.TradeExecutionUpdate,
("fos", "fon", "fou", "foc",): serializers.FundingOffer,
("fcs", "fcn", "fcu", "fcc",): serializers.FundingCredit,
("fls", "fln", "flu", "flc",): serializers.FundingLoan,
("ws", "wu",): serializers.Wallet,
("bu",): serializers.BalanceInfo
}
EVENTS = [
"notification",
"on-req-notification", "ou-req-notification", "oc-req-notification",
"oc_multi-notification",
"fon-req-notification", "foc-req-notification",
*list(__abbreviations.values())
]
def __init__(self, event_emitter, strict = False):
self.event_emitter, self.strict = event_emitter, strict
def handle(self, type, stream):
if type == "n":
return self.__notification(stream)
for types, serializer in AuthenticatedChannelsHandler.__serializers.items():
if type in types:
event = AuthenticatedChannelsHandler.__abbreviations[type]
if all(isinstance(substream, list) for substream in stream):
return self.event_emitter.emit(event, [ serializer.parse(*substream) for substream in stream ])
return self.event_emitter.emit(event, serializer.parse(*stream))
if self.strict == True:
raise BfxWebsocketException(f"Event of type <{type}> not found in self.__handlers.")
def __notification(self, stream):
if stream[1] == "on-req" or stream[1] == "ou-req" or stream[1] == "oc-req":
return self.event_emitter.emit(f"{stream[1]}-notification", serializers._Notification(serializer=serializers.Order).parse(*stream))
if stream[1] == "oc_multi-req":
return self.event_emitter.emit(f"{stream[1]}-notification", serializers._Notification(serializer=serializers.Order, iterate=True).parse(*stream))
if stream[1] == "fon-req" or stream[1] == "foc-req":
return self.event_emitter.emit(f"{stream[1]}-notification", serializers._Notification(serializer=serializers.FundingOffer).parse(*stream))
return self.event_emitter.emit("notification", serializers._Notification(serializer=None).parse(*stream))

View File

@@ -0,0 +1,4 @@
from .public_channels_handler import PublicChannelsHandler
from .authenticated_channels_handler import AuthenticatedChannelsHandler
NAME = "handlers"

View File

@@ -0,0 +1,69 @@
from .. import serializers
from .. types import *
from .. exceptions import HandlerNotFound
class AuthenticatedChannelsHandler(object):
__abbreviations = {
"os": "order_snapshot", "on": "order_new", "ou": "order_update", "oc": "order_cancel",
"ps": "position_snapshot", "pn": "position_new", "pu": "position_update", "pc": "position_close",
"te": "trade_executed", "tu": "trade_execution_update",
"fos": "funding_offer_snapshot", "fon": "funding_offer_new", "fou": "funding_offer_update", "foc": "funding_offer_cancel",
"fcs": "funding_credit_snapshot", "fcn": "funding_credit_new", "fcu": "funding_credit_update", "fcc": "funding_credit_close",
"fls": "funding_loan_snapshot", "fln": "funding_loan_new", "flu": "funding_loan_update", "flc": "funding_loan_close",
"ws": "wallet_snapshot", "wu": "wallet_update",
"bu": "balance_update",
}
__serializers = {
("os", "on", "ou", "oc",): serializers.Order,
("ps", "pn", "pu", "pc",): serializers.Position,
("te", "tu"): serializers.Trade,
("fos", "fon", "fou", "foc",): serializers.FundingOffer,
("fcs", "fcn", "fcu", "fcc",): serializers.FundingCredit,
("fls", "fln", "flu", "flc",): serializers.FundingLoan,
("ws", "wu",): serializers.Wallet,
("bu",): serializers.Balance
}
EVENTS = [
"notification",
"on-req-notification", "ou-req-notification", "oc-req-notification",
"oc_multi-notification",
"fon-req-notification", "foc-req-notification",
*list(__abbreviations.values())
]
def __init__(self, event_emitter, strict = True):
self.event_emitter, self.strict = event_emitter, strict
def handle(self, type, stream):
if type == "n":
return self.__notification(stream)
for types, serializer in AuthenticatedChannelsHandler.__serializers.items():
if type in types:
event = AuthenticatedChannelsHandler.__abbreviations[type]
if all(isinstance(substream, list) for substream in stream):
return self.event_emitter.emit(event, [ serializer.parse(*substream) for substream in stream ])
return self.event_emitter.emit(event, serializer.parse(*stream))
if self.strict:
raise HandlerNotFound(f"No handler found for event of type <{type}>.")
def __notification(self, stream):
type, serializer = "notification", serializers._Notification(serializer=None)
if stream[1] == "on-req" or stream[1] == "ou-req" or stream[1] == "oc-req":
type, serializer = f"{stream[1]}-notification", serializers._Notification(serializer=serializers.Order)
if stream[1] == "oc_multi-req":
type, serializer = f"{stream[1]}-notification", serializers._Notification(serializer=serializers.Order, iterate=True)
if stream[1] == "fon-req" or stream[1] == "foc-req":
type, serializer = f"{stream[1]}-notification", serializers._Notification(serializer=serializers.FundingOffer)
return self.event_emitter.emit(type, serializer.parse(*stream))

View File

@@ -0,0 +1,121 @@
from .. import serializers
from .. types import *
from .. exceptions import HandlerNotFound
class PublicChannelsHandler(object):
EVENTS = [
"t_ticker_update", "f_ticker_update",
"t_trade_executed", "t_trade_execution_update", "f_trade_executed", "f_trade_execution_update", "t_trades_snapshot", "f_trades_snapshot",
"t_book_snapshot", "f_book_snapshot", "t_raw_book_snapshot", "f_raw_book_snapshot", "t_book_update", "f_book_update", "t_raw_book_update", "f_raw_book_update",
"candles_snapshot", "candles_update",
"derivatives_status_update",
]
def __init__(self, event_emitter, strict = True):
self.event_emitter, self.strict = event_emitter, strict
self.__handlers = {
"ticker": self.__ticker_channel_handler,
"trades": self.__trades_channel_handler,
"book": self.__book_channel_handler,
"candles": self.__candles_channel_handler,
"status": self.__status_channel_handler
}
def handle(self, subscription, *stream):
_clear = lambda dictionary, *args: { key: value for key, value in dictionary.items() if key not in args }
if (channel := subscription["channel"]) and channel in self.__handlers.keys():
return self.__handlers[channel](_clear(subscription, "event", "channel", "chanId"), *stream)
if self.strict:
raise HandlerNotFound(f"No handler found for channel <{subscription['channel']}>.")
def __ticker_channel_handler(self, subscription, *stream):
if subscription["symbol"].startswith("t"):
return self.event_emitter.emit(
"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])
)
def __trades_channel_handler(self, subscription, *stream):
if (type := stream[0]) and type in [ "te", "tu", "fte", "ftu" ]:
if subscription["symbol"].startswith("t"):
return self.event_emitter.emit(
{ "te": "t_trade_executed", "tu": "t_trade_execution_update" }[type],
subscription,
serializers.TradingPairTrade.parse(*stream[1])
)
if subscription["symbol"].startswith("f"):
return self.event_emitter.emit(
{ "fte": "f_trade_executed", "ftu": "f_trade_execution_update" }[type],
subscription,
serializers.FundingCurrencyTrade.parse(*stream[1])
)
if subscription["symbol"].startswith("t"):
return self.event_emitter.emit(
"t_trades_snapshot",
subscription,
[ serializers.TradingPairTrade.parse(*substream) for substream in stream[0] ]
)
if subscription["symbol"].startswith("f"):
return self.event_emitter.emit(
"f_trades_snapshot",
subscription,
[ serializers.FundingCurrencyTrade.parse(*substream) for substream in stream[0] ]
)
def __book_channel_handler(self, subscription, *stream):
type = subscription["symbol"][0]
if subscription["prec"] == "R0":
_trading_pair_serializer, _funding_currency_serializer, IS_RAW_BOOK = serializers.TradingPairRawBook, serializers.FundingCurrencyRawBook, True
else: _trading_pair_serializer, _funding_currency_serializer, IS_RAW_BOOK = serializers.TradingPairBook, serializers.FundingCurrencyBook, False
if all(isinstance(substream, list) for substream in stream[0]):
return self.event_emitter.emit(
type + "_" + (IS_RAW_BOOK and "raw_book" or "book") + "_snapshot",
subscription,
[ { "t": _trading_pair_serializer, "f": _funding_currency_serializer }[type].parse(*substream) for substream in stream[0] ]
)
return self.event_emitter.emit(
type + "_" + (IS_RAW_BOOK and "raw_book" or "book") + "_update",
subscription,
{ "t": _trading_pair_serializer, "f": _funding_currency_serializer }[type].parse(*stream[0])
)
def __candles_channel_handler(self, subscription, *stream):
if all(isinstance(substream, list) for substream in stream[0]):
return self.event_emitter.emit(
"candles_snapshot",
subscription,
[ serializers.Candle.parse(*substream) for substream in stream[0] ]
)
return self.event_emitter.emit(
"candles_update",
subscription,
serializers.Candle.parse(*stream[0])
)
def __status_channel_handler(self, subscription, *stream):
if subscription["key"].startswith("deriv:"):
return self.event_emitter.emit(
"derivatives_status_update",
subscription,
serializers.DerivativesStatus.parse(*stream[0])
)

View File

@@ -1,297 +1,293 @@
from . import typings
from . import types
from .. labeler import _Serializer
from .. labeler import generate_labeler_serializer
from .. notification import _Notification
__serializers__ = [
"TradingPairTicker", "FundingCurrencyTicker", "TradingPairTrade",
"FundingCurrencyTrade", "TradingPairBook", "FundingCurrencyBook",
"TradingPairRawBook", "FundingCurrencyRawBook", "Candle",
"DerivativesStatus",
"Order", "Position", "Trade",
"FundingOffer", "FundingCredit", "FundingLoan",
"Wallet", "Balance",
]
#region Serializers definition for Websocket Public Channels
TradingPairTicker = _Serializer[typings.TradingPairTicker]("TradingPairTicker", labels=[
"BID",
"BID_SIZE",
"ASK",
"ASK_SIZE",
"DAILY_CHANGE",
"DAILY_CHANGE_RELATIVE",
"LAST_PRICE",
"VOLUME",
"HIGH",
"LOW"
TradingPairTicker = generate_labeler_serializer("TradingPairTicker", klass=types.TradingPairTicker, labels=[
"bid",
"bid_size",
"ask",
"ask_size",
"daily_change",
"daily_change_relative",
"last_price",
"volume",
"high",
"low"
])
FundingCurrencyTicker = _Serializer[typings.FundingCurrencyTicker]("FundingCurrencyTicker", labels=[
"FRR",
"BID",
"BID_PERIOD",
"BID_SIZE",
"ASK",
"ASK_PERIOD",
"ASK_SIZE",
"DAILY_CHANGE",
"DAILY_CHANGE_RELATIVE",
"LAST_PRICE",
"VOLUME",
"HIGH",
"LOW"
FundingCurrencyTicker = generate_labeler_serializer("FundingCurrencyTicker", klass=types.FundingCurrencyTicker, labels=[
"frr",
"bid",
"bid_period",
"bid_size",
"ask",
"ask_period",
"ask_size",
"daily_change",
"daily_change_relative",
"last_price",
"volume",
"high",
"low",
"_PLACEHOLDER",
"_PLACEHOLDER",
"FRR_AMOUNT_AVAILABLE"
"frr_amount_available"
])
TradingPairTrade = _Serializer[typings.TradingPairTrade]("TradingPairTrade", labels=[
"ID",
"MTS",
"AMOUNT",
"PRICE"
TradingPairTrade = generate_labeler_serializer("TradingPairTrade", klass=types.TradingPairTrade, labels=[
"id",
"mts",
"amount",
"price"
])
FundingCurrencyTrade = _Serializer[typings.FundingCurrencyTrade]("FundingCurrencyTrade", labels=[
"ID",
"MTS",
"AMOUNT",
"RATE",
"PERIOD"
FundingCurrencyTrade = generate_labeler_serializer("FundingCurrencyTrade", klass=types.FundingCurrencyTrade, labels=[
"id",
"mts",
"amount",
"rate",
"period"
])
TradingPairBook = _Serializer[typings.TradingPairBook]("TradingPairBook", labels=[
"PRICE",
"COUNT",
"AMOUNT"
TradingPairBook = generate_labeler_serializer("TradingPairBook", klass=types.TradingPairBook, labels=[
"price",
"count",
"amount"
])
FundingCurrencyBook = _Serializer[typings.FundingCurrencyBook]("FundingCurrencyBook", labels=[
"RATE",
"PERIOD",
"COUNT",
"AMOUNT"
FundingCurrencyBook = generate_labeler_serializer("FundingCurrencyBook", klass=types.FundingCurrencyBook, labels=[
"rate",
"period",
"count",
"amount"
])
TradingPairRawBook = _Serializer[typings.TradingPairRawBook]("TradingPairRawBook", labels=[
"ORDER_ID",
"PRICE",
"AMOUNT"
TradingPairRawBook = generate_labeler_serializer("TradingPairRawBook", klass=types.TradingPairRawBook, labels=[
"order_id",
"price",
"amount"
])
FundingCurrencyRawBook = _Serializer[typings.FundingCurrencyRawBook]("FundingCurrencyRawBook", labels=[
"OFFER_ID",
"PERIOD",
"RATE",
"AMOUNT"
FundingCurrencyRawBook = generate_labeler_serializer("FundingCurrencyRawBook", klass=types.FundingCurrencyRawBook, labels=[
"offer_id",
"period",
"rate",
"amount"
])
Candle = _Serializer[typings.Candle]("Candle", labels=[
"MTS",
"OPEN",
"CLOSE",
"HIGH",
"LOW",
"VOLUME"
Candle = generate_labeler_serializer("Candle", klass=types.Candle, labels=[
"mts",
"open",
"close",
"high",
"low",
"volume"
])
DerivativesStatus = _Serializer[typings.DerivativesStatus]("DerivativesStatus", labels=[
"TIME_MS",
DerivativesStatus = generate_labeler_serializer("DerivativesStatus", klass=types.DerivativesStatus, labels=[
"mts",
"_PLACEHOLDER",
"DERIV_PRICE",
"SPOT_PRICE",
"deriv_price",
"spot_price",
"_PLACEHOLDER",
"INSURANCE_FUND_BALANCE",
"insurance_fund_balance",
"_PLACEHOLDER",
"NEXT_FUNDING_EVT_TIMESTAMP_MS",
"NEXT_FUNDING_ACCRUED",
"NEXT_FUNDING_STEP",
"next_funding_evt_timestamp_ms",
"next_funding_accrued",
"next_funding_step",
"_PLACEHOLDER",
"CURRENT_FUNDING"
"current_funding",
"_PLACEHOLDER",
"_PLACEHOLDER",
"MARK_PRICE",
"mark_price",
"_PLACEHOLDER",
"_PLACEHOLDER",
"OPEN_INTEREST",
"open_interest",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"CLAMP_MIN",
"CLAMP_MAX"
"clamp_min",
"clamp_max"
])
#endregion
#region Serializers definition for Websocket Authenticated Channels
Order = _Serializer[typings.Order]("Order", labels=[
"ID",
"GID",
"CID",
"SYMBOL",
"MTS_CREATE",
"MTS_UPDATE",
"AMOUNT",
"AMOUNT_ORIG",
"ORDER_TYPE",
"TYPE_PREV",
"MTS_TIF",
Order = generate_labeler_serializer("Order", klass=types.Order, labels=[
"id",
"gid",
"cid",
"symbol",
"mts_create",
"mts_update",
"amount",
"amount_orig",
"order_type",
"type_prev",
"mts_tif",
"_PLACEHOLDER",
"FLAGS",
"ORDER_STATUS",
"flags",
"order_status",
"_PLACEHOLDER",
"_PLACEHOLDER",
"PRICE",
"PRICE_AVG",
"PRICE_TRAILING",
"PRICE_AUX_LIMIT",
"price",
"price_avg",
"price_trailing",
"price_aux_limit",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"NOTIFY",
"HIDDEN",
"PLACED_ID",
"notify",
"hidden",
"placed_id",
"_PLACEHOLDER",
"_PLACEHOLDER",
"ROUTING",
"routing",
"_PLACEHOLDER",
"_PLACEHOLDER",
"META"
"meta"
])
Position = _Serializer[typings.Position]("Position", labels=[
"SYMBOL",
"STATUS",
"AMOUNT",
"BASE_PRICE",
"MARGIN_FUNDING",
"MARGIN_FUNDING_TYPE",
"PL",
"PL_PERC",
"PRICE_LIQ",
"LEVERAGE",
"FLAG",
"POSITION_ID",
"MTS_CREATE",
"MTS_UPDATE",
Position = generate_labeler_serializer("Position", klass=types.Position, labels=[
"symbol",
"status",
"amount",
"base_price",
"margin_funding",
"margin_funding_type",
"pl",
"pl_perc",
"price_liq",
"leverage",
"flag",
"position_id",
"mts_create",
"mts_update",
"_PLACEHOLDER",
"TYPE",
"type",
"_PLACEHOLDER",
"COLLATERAL",
"COLLATERAL_MIN",
"META"
"collateral",
"collateral_min",
"meta"
])
TradeExecuted = _Serializer[typings.TradeExecuted]("TradeExecuted", labels=[
"ID",
"SYMBOL",
"MTS_CREATE",
"ORDER_ID",
"EXEC_AMOUNT",
"EXEC_PRICE",
"ORDER_TYPE",
"ORDER_PRICE",
"MAKER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"CID"
Trade = generate_labeler_serializer("Trade", klass=types.Trade, labels=[
"id",
"symbol",
"mts_create",
"order_id",
"exec_amount",
"exec_price",
"order_type",
"order_price",
"maker",
"fee",
"fee_currency",
"cid"
])
TradeExecutionUpdate = _Serializer[typings.TradeExecutionUpdate]("TradeExecutionUpdate", labels=[
"ID",
"SYMBOL",
"MTS_CREATE",
"ORDER_ID",
"EXEC_AMOUNT",
"EXEC_PRICE",
"ORDER_TYPE",
"ORDER_PRICE",
"MAKER",
"FEE",
"FEE_CURRENCY",
"CID"
])
FundingOffer = _Serializer[typings.FundingOffer]("FundingOffer", labels=[
"ID",
"SYMBOL",
"MTS_CREATED",
"MTS_UPDATED",
"AMOUNT",
"AMOUNT_ORIG",
"OFFER_TYPE",
FundingOffer = generate_labeler_serializer("FundingOffer", klass=types.FundingOffer, labels=[
"id",
"symbol",
"mts_create",
"mts_update",
"amount",
"amount_orig",
"offer_type",
"_PLACEHOLDER",
"_PLACEHOLDER",
"FLAGS",
"STATUS",
"flags",
"offer_status",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"RATE",
"PERIOD",
"NOTIFY",
"HIDDEN",
"rate",
"period",
"notify",
"hidden",
"_PLACEHOLDER",
"RENEW",
"renew",
"_PLACEHOLDER"
])
FundingCredit = _Serializer[typings.FundingCredit]("FundingCredit", labels=[
"ID",
"SYMBOL",
"SIDE",
"MTS_CREATE",
"MTS_UPDATE",
"AMOUNT",
"FLAGS",
"STATUS",
FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.FundingCredit, labels=[
"id",
"symbol",
"side",
"mts_create",
"mts_update",
"amount",
"flags",
"status",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"RATE",
"PERIOD",
"MTS_OPENING",
"MTS_LAST_PAYOUT",
"NOTIFY",
"HIDDEN",
"rate",
"period",
"mts_opening",
"mts_last_payout",
"notify",
"hidden",
"_PLACEHOLDER",
"RENEW",
"RATE_REAL",
"NO_CLOSE",
"POSITION_PAIR"
"renew",
"_PLACEHOLDER",
"no_close",
"position_pair"
])
FundingLoan = _Serializer[typings.FundingLoan]("FundingLoan", labels=[
"ID",
"SYMBOL",
"SIDE",
"MTS_CREATE",
"MTS_UPDATE",
"AMOUNT",
"FLAGS",
"STATUS",
FundingLoan = generate_labeler_serializer("FundingLoan", klass=types.FundingLoan, labels=[
"id",
"symbol",
"side",
"mts_create",
"mts_update",
"amount",
"flags",
"status",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"RATE",
"PERIOD",
"MTS_OPENING",
"MTS_LAST_PAYOUT",
"NOTIFY",
"HIDDEN",
"rate",
"period",
"mts_opening",
"mts_last_payout",
"notify",
"hidden",
"_PLACEHOLDER",
"RENEW",
"RATE_REAL",
"NO_CLOSE"
"renew",
"_PLACEHOLDER",
"no_close"
])
Wallet = _Serializer[typings.Wallet]("Wallet", labels=[
"WALLET_TYPE",
"CURRENCY",
"BALANCE",
"UNSETTLED_INTEREST",
"BALANCE_AVAILABLE",
"DESCRIPTION",
"META"
Wallet = generate_labeler_serializer("Wallet", klass=types.Wallet, labels=[
"wallet_type",
"currency",
"balance",
"unsettled_interest",
"available_balance",
"last_change",
"trade_details"
])
BalanceInfo = _Serializer[typings.BalanceInfo]("BalanceInfo", labels=[
"AUM",
"AUM_NET",
Balance = generate_labeler_serializer("Balance", klass=types.Balance, labels=[
"aum",
"aum_net",
])
#endregion

View File

@@ -0,0 +1,41 @@
from typing import TypedDict, Union, Literal, Optional
__all__ = [
"Subscription",
"Ticker",
"Trades",
"Book",
"Candles",
"Status"
]
_Header = TypedDict("_Header", { "event": Literal["subscribed"], "channel": str, "chanId": int })
Subscription = Union["Ticker", "Trades", "Book", "Candles", "Status"]
class Ticker(TypedDict):
subId: str; symbol: str
pair: Optional[str]
currency: Optional[str]
class Trades(TypedDict):
subId: str; symbol: str
pair: Optional[str]
currency: Optional[str]
class Book(TypedDict):
subId: str
symbol: str
prec: str
freq: str
len: str
pair: str
class Candles(TypedDict):
subId: str
key: str
class Status(TypedDict):
subId: str
key: str

241
bfxapi/websocket/types.py Normal file
View File

@@ -0,0 +1,241 @@
from typing import Optional
from dataclasses import dataclass
from .. labeler import _Type
from .. notification import Notification
from .. utils.JSONEncoder import JSON
#region Type hinting for Websocket Public Channels
@dataclass
class TradingPairTicker(_Type):
bid: float
bid_size: float
ask: float
ask_size: float
daily_change: float
daily_change_relative: float
last_price: float
volume: float
high: float
low: float
@dataclass
class FundingCurrencyTicker(_Type):
frr: float
bid: float
bid_period: int
bid_size: float
ask: float
ask_period: int
ask_size: float
daily_change: float
daily_change_relative: float
last_price: float
volume: float
high: float
low: float
frr_amount_available: float
@dataclass
class TradingPairTrade(_Type):
id: int
mts: int
amount: float
price: float
@dataclass
class FundingCurrencyTrade(_Type):
id: int
mts: int
amount: float
rate: float
period: int
@dataclass
class TradingPairBook(_Type):
price: float
count: int
amount: float
@dataclass
class FundingCurrencyBook(_Type):
rate: float
period: int
count: int
amount: float
@dataclass
class TradingPairRawBook(_Type):
order_id: int
price: float
amount: float
@dataclass
class FundingCurrencyRawBook(_Type):
offer_id: int
period: int
rate: float
amount: float
@dataclass
class Candle(_Type):
mts: int
open: float
close: float
high: float
low: float
volume: float
@dataclass
class DerivativesStatus(_Type):
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
#endregion
#region Type hinting for Websocket Authenticated Channels
@dataclass
class Order(_Type):
id: int
gid: int
cid: int
symbol: str
mts_create: int
mts_update: int
amount: float
amount_orig: float
order_type: str
type_prev: str
mts_tif: int
flags: int
order_status: str
price: float
price_avg: float
price_trailing: float
price_aux_limit: float
notify: int
hidden: int
placed_id: int
routing: str
meta: JSON
@dataclass
class Position(_Type):
symbol: str
status: str
amount: float
base_price: float
margin_funding: float
margin_funding_type: int
pl: float
pl_perc: float
price_liq: float
leverage: float
flag: int
position_id: int
mts_create: int
mts_update: int
type: int
collateral: float
collateral_min: float
meta: JSON
@dataclass
class Trade(_Type):
id: int
symbol: str
mts_create: int
order_id: int
exec_amount: float
exec_price: float
order_type: str
order_price: float
maker:int
fee: Optional[float]
fee_currency: Optional[str]
cid: int
@dataclass
class FundingOffer(_Type):
id: int
symbol: str
mts_create: int
mts_update: int
amount: float
amount_orig: float
offer_type: str
flags: int
offer_status: str
rate: float
period: int
notify: int
hidden: int
renew: int
@dataclass
class FundingCredit(_Type):
id: int
symbol: str
side: int
mts_create: int
mts_update: int
amount: float
flags: int
status: str
rate: float
period: int
mts_opening: int
mts_last_payout: int
notify: int
hidden: int
renew: int
no_close: int
position_pair: str
@dataclass
class FundingLoan(_Type):
id: int
symbol: str
side: int
mts_create: int
mts_update: int
amount: float
flags: int
status: str
rate: float
period: int
mts_opening: int
mts_last_payout: int
notify: int
hidden: int
renew: int
no_close: int
@dataclass
class Wallet(_Type):
wallet_type: str
currency: str
balance: float
unsettled_interest: float
available_balance: float
last_change: str
trade_details: JSON
@dataclass
class Balance(_Type):
aum: float
aum_net: float
#endregion

View File

@@ -1,277 +0,0 @@
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 subscription objects
class Subscriptions:
class TradingPairTicker(TypedDict):
chanId: int
symbol: str
pair: str
class FundingCurrencyTicker(TypedDict):
chanId: int
symbol: str
currency: str
class TradingPairTrades(TypedDict):
chanId: int
symbol: str
pair: str
class FundingCurrencyTrades(TypedDict):
chanId: int
symbol: str
currency: str
class Book(TypedDict):
chanId: int
symbol: str
prec: str
freq: str
len: str
subId: int
pair: str
class Candles(TypedDict):
chanId: int
key: str
class DerivativesStatus(TypedDict):
chanId: int
key: str
#endregion
#region Type hinting for Websocket Public Channels
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
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
class TradingPairTrade(TypedDict):
ID: int
MTS: int
AMOUNT: float
PRICE: float
class FundingCurrencyTrade(TypedDict):
ID: int
MTS: int
AMOUNT: float
RATE: float
PERIOD: int
class TradingPairBook(TypedDict):
PRICE: float
COUNT: int
AMOUNT: float
class FundingCurrencyBook(TypedDict):
RATE: float
PERIOD: int
COUNT: int
AMOUNT: float
class TradingPairRawBook(TypedDict):
ORDER_ID: int
PRICE: float
AMOUNT: float
class FundingCurrencyRawBook(TypedDict):
OFFER_ID: int
PERIOD: int
RATE: float
AMOUNT: float
class Candle(TypedDict):
MTS: int
OPEN: float
CLOSE: float
HIGH: float
LOW: float
VOLUME: 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
class Order(TypedDict):
ID: int
GID: int
CID: int
SYMBOL: str
MTS_CREATE: int
MTS_UPDATE: int
AMOUNT: float
AMOUNT_ORIG: float
ORDER_TYPE: str
TYPE_PREV: str
MTS_TIF: int
FLAGS: int
ORDER_STATUS: str
PRICE: float
PRICE_AVG: float
PRICE_TRAILING: float
PRICE_AUX_LIMIT: float
NOTIFY: int
HIDDEN: int
PLACED_ID: int
ROUTING: str
META: JSON
class 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
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
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
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
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
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
class Wallet(TypedDict):
WALLET_TYPE: str
CURRENCY: str
BALANCE: float
UNSETTLED_INTEREST: float
BALANCE_AVAILABLE: float
DESCRIPTION: str
META: JSON
class BalanceInfo(TypedDict):
AUM: float
AUM_NET: float
#endregion

View File

@@ -0,0 +1,19 @@
# python -c "import examples.rest.claim_position"
import os
from bfxapi.client import Client, REST_HOST
bfx = Client(
REST_HOST=REST_HOST,
API_KEY=os.getenv("BFX_API_KEY"),
API_SECRET=os.getenv("BFX_API_SECRET")
)
open_margin_positions = bfx.rest.auth.get_positions()
# claim all positions
for position in open_margin_positions:
print(f"Position {position}")
claim = bfx.rest.auth.claim_position(position.position_id, amount=0.000001)
print(f"PositionClaim {claim.notify_info}")

View File

@@ -1,11 +1,12 @@
# python -c "import examples.rest.create_funding_offer"
import os
from bfxapi.client import Client, Constants
from bfxapi.enums import FundingOfferType
from bfxapi.utils.flags import calculate_offer_flags
from bfxapi.client import Client, REST_HOST
from bfxapi.enums import FundingOfferType, Flag
bfx = Client(
REST_HOST=Constants.REST_HOST,
REST_HOST=REST_HOST,
API_KEY=os.getenv("BFX_API_KEY"),
API_SECRET=os.getenv("BFX_API_SECRET")
)
@@ -16,11 +17,16 @@ notification = bfx.rest.auth.submit_funding_offer(
amount="123.45",
rate="0.001",
period=2,
flags=calculate_offer_flags(hidden=True)
flags=Flag.HIDDEN
)
print("Offer notification:", notification)
offers = bfx.rest.auth.get_active_funding_offers(symbol="fUSD")
offers = bfx.rest.auth.get_funding_offers(symbol="fUSD")
print("Offers:", offers)
print("Offers:", offers)
# Cancel all funding offers
notification = bfx.rest.auth.cancel_all_funding_offers(currency="fUSD")
print(notification)

View File

@@ -1,11 +1,11 @@
import os
# python -c "import examples.rest.create_order"
from bfxapi.client import Client, Constants
from bfxapi.enums import OrderType
from bfxapi.utils.flags import calculate_order_flags
import os
from bfxapi.client import Client, REST_HOST
from bfxapi.enums import OrderType, Flag
bfx = Client(
REST_HOST=Constants.REST_HOST,
REST_HOST=REST_HOST,
API_KEY=os.getenv("BFX_API_KEY"),
API_SECRET=os.getenv("BFX_API_SECRET")
)
@@ -16,14 +16,14 @@ submitted_order = bfx.rest.auth.submit_order(
symbol="tBTCUST",
amount="0.015",
price="10000",
flags=calculate_order_flags(hidden=False)
flags=Flag.HIDDEN + Flag.OCO + Flag.CLOSE
)
print("Submit Order Notification:", submitted_order)
# Update it
updated_order = bfx.rest.auth.update_order(
id=submitted_order["NOTIFY_INFO"]["ID"],
id=submitted_order.notify_info.id,
amount="0.020",
price="10100"
)
@@ -31,6 +31,6 @@ updated_order = bfx.rest.auth.update_order(
print("Update Order Notification:", updated_order)
# Delete it
canceled_order = bfx.rest.auth.cancel_order(id=submitted_order["NOTIFY_INFO"]["ID"])
canceled_order = bfx.rest.auth.cancel_order(id=submitted_order.notify_info.id)
print("Cancel Order Notification:", canceled_order)

View File

@@ -0,0 +1,31 @@
# python -c "import examples.rest.derivatives"
import os
from bfxapi.client import Client, REST_HOST
bfx = Client(
REST_HOST=REST_HOST,
API_KEY=os.getenv("BFX_API_KEY"),
API_SECRET=os.getenv("BFX_API_SECRET")
)
# Create a new order
submitted_order = bfx.rest.auth.submit_order(
symbol="tBTCF0:USTF0",
amount="0.015",
price="16700",
lev=10,
type="LIMIT"
)
print("Submit Order Notification:", submitted_order)
# Get position collateral limits
limits = bfx.rest.auth.get_derivative_position_collateral_limits(symbol="tBTCF0:USTF0")
print(f"Limits {limits}")
# Update position collateral
response = bfx.rest.auth.set_derivative_position_collateral(symbol="tBTCF0:USTF0", collateral=50)
print(response.status)

View File

@@ -0,0 +1,28 @@
# python -c "import examples.rest.extra_calcs"
from bfxapi.client import Client, REST_HOST
bfx = Client(
REST_HOST=REST_HOST
)
t_symbol_response = bfx.rest.public.get_trading_market_average_price(
symbol="tBTCUSD",
amount=-100,
price_limit="20000.5"
)
print(t_symbol_response.price_avg)
f_symbol_response = bfx.rest.public.get_funding_market_average_price(
symbol="fUSD",
amount=100,
period=2,
rate_limit="0.00015"
)
print(f_symbol_response.rate_avg)
fx_rate = bfx.rest.public.get_fx_rate(ccy1="USD", ccy2="EUR")
print(fx_rate.current_rate)

View File

@@ -0,0 +1,21 @@
# python -c "import examples.rest.funding_auto_renew"
import os
from bfxapi.client import Client, REST_HOST
bfx = Client(
REST_HOST=REST_HOST,
API_KEY=os.getenv("BFX_API_KEY"),
API_SECRET=os.getenv("BFX_API_SECRET")
)
notification = bfx.rest.auth.toggle_auto_renew(
status=True,
currency="USD",
amount="150",
rate="0", # FRR
period=2
)
print("Renew toggle notification:", notification)

View File

@@ -1,18 +1,29 @@
# python -c "from examples.rest.get_authenticated_data import *"
# python -c "import examples.rest.get_authenticated_data"
import os
import time
from bfxapi.client import Client, Constants
from bfxapi.client import Client, REST_HOST
bfx = Client(
REST_HOST=Constants.REST_HOST,
REST_HOST=REST_HOST,
API_KEY=os.getenv("BFX_API_KEY"),
API_SECRET=os.getenv("BFX_API_SECRET")
)
now = int(round(time.time() * 1000))
def log_user_info():
user_info = bfx.rest.auth.get_user_info()
print(user_info)
def log_login_history():
login_history = bfx.rest.auth.get_login_history()
print(login_history)
def log_wallets():
wallets = bfx.rest.auth.get_wallets()
print("Wallets:")
@@ -38,14 +49,14 @@ def log_positions():
def log_trades():
trades = bfx.rest.auth.get_trades(symbol='tBTCUSD', start=0, end=now)
trades = bfx.rest.auth.get_trades_history(symbol='tBTCUSD', start=0, end=now)
print("Trades:")
[print(t) for t in trades]
def log_order_trades():
order_id = 82406909127
trades = bfx.rest.auth.get_order_trades(symbol='tBTCUSD', order_id=order_id)
trades = bfx.rest.auth.get_order_trades(symbol='tBTCUSD', id=order_id)
print("Trade orders:")
[print(t) for t in trades]
@@ -69,7 +80,7 @@ def log_funding_loans():
def log_funding_loans_history():
loans = bfx.rest.auth.get_funding_loan_history(symbol='fUSD', start=0, end=now)
loans = bfx.rest.auth.get_funding_loans_history(symbol='fUSD', start=0, end=now)
print("Funding loan history:")
[print(l) for l in loans]
@@ -85,8 +96,18 @@ def log_funding_credits_history():
print("Funding credit history:")
[print(c) for c in credit]
def log_margin_info():
btcusd_margin_info = bfx.rest.auth.get_symbol_margin_info('tBTCUSD')
print(f"tBTCUSD margin info {btcusd_margin_info}")
sym_all_margin_info = bfx.rest.auth.get_all_symbols_margin_info()
print(f"Sym all margin info {sym_all_margin_info}")
base_margin_info = bfx.rest.auth.get_base_margin_info()
print(f"Base margin info {base_margin_info}")
def run():
log_user_info()
log_wallets()
log_orders()
log_orders_history()
@@ -97,5 +118,6 @@ def run():
log_funding_offer_history()
log_funding_credits()
log_funding_credits_history()
log_margin_info()
run()

View File

@@ -0,0 +1,13 @@
# python -c "import examples.rest.get_candles_hist"
from bfxapi.client import Client, REST_HOST
bfx = Client(
REST_HOST=REST_HOST
)
print(f"Candles: {bfx.rest.public.get_candles_hist(symbol='tBTCUSD')}")
# Be sure to specify a period or aggregated period when retrieving funding candles.
# If you wish to mimic the candles found in the UI, use the following setup to aggregate all funding candles: a30:p2:p30
print(f"Candles: {bfx.rest.public.get_candles_hist(tf='15m', symbol='fUSD:a30:p2:p30')}")

View File

@@ -0,0 +1,13 @@
# python -c "import examples.rest.get_funding_info"
import os
from bfxapi.client import Client, REST_HOST
bfx = Client(
REST_HOST=REST_HOST,
API_KEY=os.getenv("BFX_API_KEY"),
API_SECRET=os.getenv("BFX_API_SECRET")
)
print(bfx.rest.auth.get_funding_info(key="fUSD"))

View File

@@ -0,0 +1,13 @@
# python -c "import examples.rest.get_funding_trades_history"
import os
from bfxapi.client import Client, REST_HOST
bfx = Client(
REST_HOST=REST_HOST,
API_KEY=os.getenv("BFX_API_KEY"),
API_SECRET=os.getenv("BFX_API_SECRET")
)
print(bfx.rest.auth.get_funding_trades_history())

View File

@@ -0,0 +1,14 @@
# python -c "import examples.rest.get_liquidations"
import time
from bfxapi.client import Client, REST_HOST
bfx = Client(
REST_HOST=REST_HOST
)
now = int(round(time.time() * 1000))
liquidations = bfx.rest.public.get_liquidations(start=0, end=now)
print(f"Liquidations: {liquidations}")

View File

@@ -0,0 +1,23 @@
# python -c "import examples.rest.get_positions"
import os
import time
from bfxapi.client import Client, REST_HOST
bfx = Client(
REST_HOST=REST_HOST,
API_KEY=os.getenv("BFX_API_KEY"),
API_SECRET=os.getenv("BFX_API_SECRET")
)
now = int(round(time.time() * 1000))
positions_snapshot = bfx.rest.auth.get_positions_snapshot(end=now, limit=50)
print(positions_snapshot)
positions_history = bfx.rest.auth.get_positions_history(end=now, limit=50)
print(positions_history)
positions_audit = bfx.rest.auth.get_positions_audit(end=now, limit=50)
print(positions_audit)

View File

@@ -0,0 +1,52 @@
# python -c "import examples.rest.get_public_data"
import time
from bfxapi.client import Client, REST_HOST
bfx = Client(
REST_HOST=REST_HOST
)
now = int(round(time.time() * 1000))
def log_historical_candles():
candles = bfx.rest.public.get_candles_hist(start=0, end=now, resource='trade:1m:tBTCUSD')
print("Candles:")
[print(c) for c in candles]
def log_historical_trades():
trades = bfx.rest.public.get_t_trades(pair='tBTCUSD', start=0, end=now)
print("Trades:")
[print(t) for t in trades]
def log_books():
orders = bfx.rest.public.get_t_book(pair='BTCUSD', precision='P0')
print("Order book:")
[print(o) for o in orders]
def log_tickers():
tickers = bfx.rest.public.get_t_tickers(pairs=['BTCUSD'])
print("Tickers:")
print(tickers)
def log_derivative_status():
status = bfx.rest.public.get_derivatives_status('ALL')
print("Deriv status:")
print(status)
def run():
log_historical_candles()
log_historical_trades()
log_books()
log_tickers()
log_derivative_status()
run()

View File

@@ -0,0 +1,22 @@
# python -c "import examples.rest.get_pulse_data"
import time
from bfxapi.client import Client, REST_HOST
bfx = Client(
REST_HOST=REST_HOST
)
now = int(round(time.time() * 1000))
messages = bfx.rest.public.get_pulse_history(end=now, limit=100)
for message in messages:
print(f"Message: {message}")
print(message.content)
print(message.profile.picture)
profile = bfx.rest.public.get_pulse_profile("News")
print(f"Profile: {profile}")
print(f"Profile picture: {profile.picture}")

View File

@@ -0,0 +1,18 @@
# python -c "import examples.rest.increase_position"
import os
from bfxapi.client import Client, REST_HOST
bfx = Client(
REST_HOST=REST_HOST,
API_KEY=os.getenv("BFX_API_KEY"),
API_SECRET=os.getenv("BFX_API_SECRET")
)
increase_info = bfx.rest.auth.get_increase_position_info(symbol="tBTCUSD", amount=0.0001)
print(increase_info)
# increase a margin position
notification = bfx.rest.auth.increase_position(symbol="tBTCUSD", amount=0.0001)
print(notification.notify_info)

View File

@@ -0,0 +1,26 @@
# python -c "import examples.rest.keep_taken_funding"
import os
from bfxapi.client import Client, REST_HOST
bfx = Client(
REST_HOST=REST_HOST,
API_KEY=os.getenv("BFX_API_KEY"),
API_SECRET=os.getenv("BFX_API_SECRET")
)
loans = bfx.rest.auth.get_funding_loans(symbol="fUSD")
for loan in loans:
print(f"Loan {loan}")
notification = bfx.rest.auth.toggle_keep(
funding_type="loan",
ids=[loan.id],
changes={
loan.id: 2 # (1 if true, 2 if false)
}
)
print("Funding keep notification:", notification)

44
examples/rest/merchant.py Normal file
View File

@@ -0,0 +1,44 @@
# python -c "import examples.rest.merchant"
import os
from bfxapi.client import Client, REST_HOST
bfx = Client(
REST_HOST=REST_HOST,
API_KEY=os.getenv("BFX_API_KEY"),
API_SECRET=os.getenv("BFX_API_SECRET")
)
customer_info = {
"nationality": "GB",
"resid_country": "DE",
"resid_city": "Berlin",
"resid_zip_code": 1,
"resid_street": "Timechain",
"full_name": "Satoshi",
"email": "satoshi3@bitfinex.com"
}
invoice = bfx.rest.merchant.submit_invoice(
amount=1,
currency="USD",
duration=864000,
order_id="order123",
customer_info=customer_info,
pay_currencies=["ETH"]
)
print(bfx.rest.merchant.get_invoices())
print(bfx.rest.merchant.get_invoice_count_stats(status="CREATED", format="Y"))
print(bfx.rest.merchant.get_invoice_earning_stats(currency="USD", format="Y"))
print(bfx.rest.merchant.get_currency_conversion_list())
print(bfx.rest.merchant.complete_invoice(
id=invoice.id,
pay_currency="ETH",
deposit_id=1
))

View File

@@ -0,0 +1,22 @@
# python -c "import examples.rest.return_taken_funding"
import os
from bfxapi.client import Client, REST_HOST
bfx = Client(
REST_HOST=REST_HOST,
API_KEY=os.getenv("BFX_API_KEY"),
API_SECRET=os.getenv("BFX_API_SECRET")
)
loans = bfx.rest.auth.get_funding_loans(symbol="fUSD")
for loan in loans:
print(f"Loan {loan}")
notification = bfx.rest.auth.submit_funding_close(
id=loan.id
)
print("Funding close notification:", notification)

View File

@@ -0,0 +1,46 @@
# python -c "import examples.rest.transfer_wallet"
import os
from bfxapi.client import Client, REST_HOST
bfx = Client(
REST_HOST=REST_HOST,
API_KEY=os.getenv("BFX_API_KEY"),
API_SECRET=os.getenv("BFX_API_SECRET")
)
def transfer_wallet():
response = bfx.rest.auth.transfer_between_wallets(from_wallet="exchange", to_wallet="funding", from_currency="ETH", to_currency="ETH", amount=0.001)
print("Transfer:", response.notify_info)
def get_existing_deposit_address():
response = bfx.rest.auth.get_deposit_address(wallet="exchange", method="bitcoin", renew=False)
print("Address:", response.notify_info)
def create_new_deposit_address():
response = bfx.rest.auth.get_deposit_address(wallet="exchange", method="bitcoin", renew=True)
print("Address:", response.notify_info)
def withdraw():
# tetheruse = Tether (ERC20)
response = bfx.rest.auth.submit_wallet_withdrawal(wallet="exchange", method="tetheruse", amount=1, address="0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
print("Address:", response.notify_info)
def create_lighting_network_deposit_address():
invoice = bfx.rest.auth.generate_deposit_invoice(wallet="funding", currency="LNX", amount=0.001)
print("Invoice:", invoice)
def get_movements():
movements = bfx.rest.auth.get_movements(currency="BTC")
print("Movements:", movements)
def run():
transfer_wallet()
get_existing_deposit_address()
create_new_deposit_address()
withdraw()
create_lighting_network_deposit_address()
get_movements()
run()

View File

@@ -1,13 +1,13 @@
# python -c "from examples.websocket.create_order import *"
# python -c "import examples.websocket.create_order"
import os
from bfxapi.client import Client, Constants
from bfxapi.client import Client, WSS_HOST
from bfxapi.websocket.enums import Error, OrderType
from bfxapi.websocket.typings import Notification, Order
from bfxapi.websocket.types import Notification, Order
bfx = Client(
WSS_HOST=Constants.WSS_HOST,
WSS_HOST=WSS_HOST,
API_KEY=os.getenv("BFX_API_KEY"),
API_SECRET=os.getenv("BFX_API_SECRET")
)
@@ -30,7 +30,7 @@ async def on_authenticated(event):
print("The order has been sent.")
@bfx.wss.on("on-req-notification")
async def on_notification(notification: Notification):
async def on_notification(notification: Notification[Order]):
print(f"Notification: {notification}.")
@bfx.wss.on("order_new")

View File

@@ -0,0 +1,23 @@
# python -c "import examples.websocket.derivatives_status"
from bfxapi import Client, PUB_WSS_HOST
from bfxapi.websocket.enums import Error, Channel
from bfxapi.websocket.types import DerivativesStatus
from bfxapi.websocket import subscriptions
bfx = Client(WSS_HOST=PUB_WSS_HOST)
@bfx.wss.on("derivatives_status_update")
def on_derivatives_status_update(subscription: subscriptions.Status, data: DerivativesStatus):
print(f"{subscription}:", data)
@bfx.wss.on("wss-error")
def on_wss_error(code: Error, msg: str):
print(code, msg)
@bfx.wss.once("open")
async def open():
await bfx.wss.subscribe(Channel.STATUS, key="deriv:tBTCF0:USTF0")
bfx.wss.run()

View File

@@ -1,13 +1,14 @@
# python -c "from examples.websocket.order_book import *"
# python -c "import examples.websocket.order_book"
from collections import OrderedDict
from typing import List
from bfxapi import Client, Constants
from bfxapi import Client, PUB_WSS_HOST
from bfxapi.websocket.enums import Channels, Error
from bfxapi.websocket.typings import Subscriptions, TradingPairBook
from bfxapi.websocket import subscriptions
from bfxapi.websocket.enums import Channel, Error
from bfxapi.websocket.types import TradingPairBook
class OrderBook(object):
def __init__(self, symbols: List[str]):
@@ -18,7 +19,7 @@ class OrderBook(object):
}
def update(self, symbol: str, data: TradingPairBook) -> None:
price, count, amount = data["PRICE"], data["COUNT"], data["AMOUNT"]
price, count, amount = data.price, data.count, data.amount
kind = (amount > 0) and "bids" or "asks"
@@ -37,7 +38,7 @@ SYMBOLS = [ "tBTCUSD", "tLTCUSD", "tLTCBTC", "tETHUSD", "tETHBTC" ]
order_book = OrderBook(symbols=SYMBOLS)
bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST)
bfx = Client(WSS_HOST=PUB_WSS_HOST)
@bfx.wss.on("wss-error")
def on_wss_error(code: Error, msg: str):
@@ -46,19 +47,19 @@ def on_wss_error(code: Error, msg: str):
@bfx.wss.on("open")
async def on_open():
for symbol in SYMBOLS:
await bfx.wss.subscribe(Channels.BOOK, symbol=symbol)
await bfx.wss.subscribe(Channel.BOOK, symbol=symbol)
@bfx.wss.on("subscribed")
def on_subscribed(subscription):
print(f"Subscription successful for pair <{subscription['pair']}>")
@bfx.wss.on("t_book_snapshot")
def on_t_book_snapshot(subscription: Subscriptions.Book, snapshot: List[TradingPairBook]):
def on_t_book_snapshot(subscription: subscriptions.Book, snapshot: List[TradingPairBook]):
for data in snapshot:
order_book.update(subscription["symbol"], data)
@bfx.wss.on("t_book_update")
def on_t_book_update(subscription: Subscriptions.Book, data: TradingPairBook):
def on_t_book_update(subscription: subscriptions.Book, data: TradingPairBook):
order_book.update(subscription["symbol"], data)
bfx.wss.run()

View File

@@ -1,13 +1,14 @@
# python -c "from examples.websocket.raw_order_book import *"
# python -c "import examples.websocket.raw_order_book"
from collections import OrderedDict
from typing import List
from bfxapi import Client, Constants
from bfxapi import Client, PUB_WSS_HOST
from bfxapi.websocket.enums import Channels, Error
from bfxapi.websocket.typings import Subscriptions, TradingPairRawBook
from bfxapi.websocket import subscriptions
from bfxapi.websocket.enums import Channel, Error
from bfxapi.websocket.types import TradingPairRawBook
class RawOrderBook(object):
def __init__(self, symbols: List[str]):
@@ -18,7 +19,7 @@ class RawOrderBook(object):
}
def update(self, symbol: str, data: TradingPairRawBook) -> None:
order_id, price, amount = data["ORDER_ID"], data["PRICE"], data["AMOUNT"]
order_id, price, amount = data.order_id, data.price, data.amount
kind = (amount > 0) and "bids" or "asks"
@@ -37,7 +38,7 @@ SYMBOLS = [ "tBTCUSD", "tLTCUSD", "tLTCBTC", "tETHUSD", "tETHBTC" ]
raw_order_book = RawOrderBook(symbols=SYMBOLS)
bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST)
bfx = Client(WSS_HOST=PUB_WSS_HOST)
@bfx.wss.on("wss-error")
def on_wss_error(code: Error, msg: str):
@@ -46,19 +47,19 @@ def on_wss_error(code: Error, msg: str):
@bfx.wss.on("open")
async def on_open():
for symbol in SYMBOLS:
await bfx.wss.subscribe(Channels.BOOK, symbol=symbol, prec="R0")
await bfx.wss.subscribe(Channel.BOOK, symbol=symbol, prec="R0")
@bfx.wss.on("subscribed")
def on_subscribed(subscription):
print(f"Subscription successful for pair <{subscription['pair']}>")
@bfx.wss.on("t_raw_book_snapshot")
def on_t_raw_book_snapshot(subscription: Subscriptions.Book, snapshot: List[TradingPairRawBook]):
def on_t_raw_book_snapshot(subscription: subscriptions.Book, snapshot: List[TradingPairRawBook]):
for data in snapshot:
raw_order_book.update(subscription["symbol"], data)
@bfx.wss.on("t_raw_book_update")
def on_t_raw_book_update(subscription: Subscriptions.Book, data: TradingPairRawBook):
def on_t_raw_book_update(subscription: subscriptions.Book, data: TradingPairRawBook):
raw_order_book.update(subscription["symbol"], data)
bfx.wss.run()

View File

@@ -1,21 +1,21 @@
# python -c "from examples.websocket.ticker import *"
# python -c "import examples.websocket.ticker"
import asyncio
from bfxapi import Client, PUB_WSS_HOST
from bfxapi import Client, Constants
from bfxapi.websocket.enums import Channels
from bfxapi.websocket.typings import Subscriptions, TradingPairTicker
from bfxapi.websocket import subscriptions
from bfxapi.websocket.enums import Channel
from bfxapi.websocket.types import TradingPairTicker
bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST)
bfx = Client(WSS_HOST=PUB_WSS_HOST)
@bfx.wss.on("t_ticker_update")
def on_t_ticker_update(subscription: Subscriptions.TradingPairTicker, data: TradingPairTicker):
print(f"Subscription with channel ID: {subscription['chanId']}")
def on_t_ticker_update(subscription: subscriptions.Ticker, data: TradingPairTicker):
print(f"Subscription with subId: {subscription['subId']}")
print(f"Data: {data}")
@bfx.wss.once("open")
async def open():
await bfx.wss.subscribe(Channels.TICKER, symbol="tBTCUSD")
await bfx.wss.subscribe(Channel.TICKER, symbol="tBTCUSD")
bfx.wss.run()

View File

@@ -0,0 +1,29 @@
# python -c "import examples.websocket.trades"
from bfxapi import Client, PUB_WSS_HOST
from bfxapi.websocket.enums import Error, Channel
from bfxapi.websocket.types import Candle, TradingPairTrade
from bfxapi.websocket import subscriptions
bfx = Client(WSS_HOST=PUB_WSS_HOST)
@bfx.wss.on("candles_update")
def on_candles_update(subscription: subscriptions.Candles, candle: Candle):
print(f"New candle: {candle}")
@bfx.wss.on("t_trade_executed")
def on_t_trade_executed(subscription: subscriptions.Trades, trade: TradingPairTrade):
print(f"New trade: {trade}")
@bfx.wss.on("wss-error")
def on_wss_error(code: Error, msg: str):
print(code, msg)
@bfx.wss.once("open")
async def open():
await bfx.wss.subscribe(Channel.CANDLES, key="trade:1m:tBTCUSD")
await bfx.wss.subscribe(Channel.TRADES, symbol="tBTCUSD")
bfx.wss.run()

View File

@@ -0,0 +1,30 @@
# python -c "import examples.websocket.wallet_balance"
import os
from typing import List
from bfxapi import Client, WSS_HOST
from bfxapi.websocket.enums import Error
from bfxapi.websocket.types import Wallet
bfx = Client(
WSS_HOST=WSS_HOST,
API_KEY=os.getenv("BFX_API_KEY"),
API_SECRET=os.getenv("BFX_API_SECRET")
)
@bfx.wss.on("wallet_snapshot")
def log_snapshot(wallets: List[Wallet]):
for wallet in wallets:
print(f"Balance: {wallet}")
@bfx.wss.on("wallet_update")
def log_update(wallet: Wallet):
print(f"Balance update: {wallet}")
@bfx.wss.on("wss-error")
def on_wss_error(code: Error, msg: str):
print(code, msg)
bfx.wss.run()

View File

@@ -2,14 +2,36 @@ from distutils.core import setup
setup(
name="bitfinex-api-py",
version="3.0.0",
packages=[ "bfxapi", "bfxapi.websocket", "bfxapi.rest", "bfxapi.utils" ],
version="3.0.0b1",
description="Official Bitfinex Python API",
long_description="A Python reference implementation of the Bitfinex API for both REST and websocket interaction",
long_description_content_type="text/markdown",
url="https://github.com/bitfinexcom/bitfinex-api-py",
license="OSI Approved :: Apache Software License",
author="Bitfinex",
author_email="support@bitfinex.com",
description="Official Bitfinex Python API",
license="Apache-2.0",
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Topic :: Software Development :: Build Tools",
"License :: OSI Approved :: Apache-2.0",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
],
keywords="bitfinex,api,trading",
project_urls={
"Bug Reports": "https://github.com/bitfinexcom/bitfinex-api-py/issues",
"Source": "https://github.com/bitfinexcom/bitfinex-api-py",
},
packages=[
"bfxapi", "bfxapi.utils",
"bfxapi.websocket", "bfxapi.websocket.client", "bfxapi.websocket.handlers",
"bfxapi.rest", "bfxapi.rest.endpoints", "bfxapi.rest.middleware",
],
install_requires=[
"certifi~=2022.12.7",
"charset-normalizer~=2.1.1",
@@ -25,8 +47,5 @@ setup(
"urllib3~=1.26.13",
"websockets~=10.4",
],
project_urls={
"Bug Reports": "https://github.com/bitfinexcom/bitfinex-api-py/issues",
"Source": "https://github.com/bitfinexcom/bitfinex-api-py",
}
python_requires=">=3.8"
)