Merge pull request #12 from Davi0kProgramsThings/feature/rest

Merge branch `feature/rest` in branch `master`.
This commit is contained in:
Davide Casale
2023-01-12 17:15:49 +01:00
committed by GitHub
28 changed files with 1748 additions and 377 deletions

View File

@@ -1,13 +1,32 @@
from .rest import BfxRestInterface
from .websocket import BfxWebsocketClient
from typing import Optional
from enum import Enum
class Constants(str, Enum):
REST_HOST = "https://api.bitfinex.com/v2"
PUB_REST_HOST = "https://api-pub.bitfinex.com/v2"
WSS_HOST = "wss://api.bitfinex.com/ws/2"
PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2"
class Client(object):
def __init__(self, WSS_HOST: str = Constants.WSS_HOST, API_KEY: str = None, API_SECRET: str = None, log_level: str = "WARNING"):
def __init__(
self,
REST_HOST: str = Constants.REST_HOST,
WSS_HOST: str = Constants.WSS_HOST,
API_KEY: Optional[str] = None,
API_SECRET: Optional[str] = None,
log_level: str = "WARNING"
):
self.rest = BfxRestInterface(
host=REST_HOST,
API_KEY=API_KEY,
API_SECRET=API_SECRET
)
self.wss = BfxWebsocketClient(
host=WSS_HOST,
API_KEY=API_KEY,

50
bfxapi/enums.py Normal file
View File

@@ -0,0 +1,50 @@
from enum import Enum
class OrderType(str, Enum):
LIMIT = "LIMIT"
EXCHANGE_LIMIT = "EXCHANGE LIMIT"
MARKET = "MARKET"
EXCHANGE_MARKET = "EXCHANGE MARKET"
STOP = "STOP"
EXCHANGE_STOP = "EXCHANGE STOP"
STOP_LIMIT = "STOP LIMIT"
EXCHANGE_STOP_LIMIT = "EXCHANGE STOP LIMIT"
TRAILING_STOP = "TRAILING STOP"
EXCHANGE_TRAILING_STOP = "EXCHANGE TRAILING STOP"
FOK = "FOK"
EXCHANGE_FOK = "EXCHANGE FOK"
IOC = "IOC"
EXCHANGE_IOC = "EXCHANGE IOC"
class FundingOfferType(str, Enum):
LIMIT = "LIMIT"
FRR_DELTA_FIX = "FRRDELTAFIX"
FRR_DELTA_VAR = "FRRDELTAVAR"
class Flag(int, Enum):
HIDDEN = 64
CLOSE = 512
REDUCE_ONLY = 1024
POST_ONLY = 4096
OCO = 16384
NO_VAR_RATES = 524288
class Error(int, Enum):
ERR_UNK = 10000
ERR_GENERIC = 10001
ERR_CONCURRENCY = 10008
ERR_PARAMS = 10020
ERR_CONF_FAIL = 10050
ERR_AUTH_FAIL = 10100
ERR_AUTH_PAYLOAD = 10111
ERR_AUTH_SIG = 10112
ERR_AUTH_HMAC = 10113
ERR_AUTH_NONCE = 10114
ERR_UNAUTH_FAIL = 10200
ERR_SUB_FAIL = 10300
ERR_SUB_MULTI = 10301
ERR_SUB_UNK = 10302
ERR_SUB_LIMIT = 10305
ERR_UNSUB_FAIL = 10400
ERR_UNSUB_NOT = 10401
ERR_READY = 11000

35
bfxapi/exceptions.py Normal file
View File

@@ -0,0 +1,35 @@
__all__ = [
"BfxBaseException",
"LabelerSerializerException",
"IntegerUnderflowError",
"IntegerOverflowflowError"
]
class BfxBaseException(Exception):
"""
Base class for every custom exception in bfxapi/rest/exceptions.py and bfxapi/websocket/exceptions.py.
"""
pass
class LabelerSerializerException(BfxBaseException):
"""
This exception indicates an error thrown by the _Serializer class in bfxapi/labeler.py.
"""
pass
class IntegerUnderflowError(BfxBaseException):
"""
This error indicates an underflow in one of the integer types defined in bfxapi/utils/integers.py.
"""
pass
class IntegerOverflowflowError(BfxBaseException):
"""
This error indicates an overflow in one of the integer types defined in bfxapi/utils/integers.py.
"""
pass

22
bfxapi/labeler.py Normal file
View File

@@ -0,0 +1,22 @@
from .exceptions import LabelerSerializerException
from typing import Generic, TypeVar, Iterable, Optional, List, Tuple, Any, cast
T = TypeVar("T")
class _Serializer(Generic[T]):
def __init__(self, name: str, labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]):
self.name, self.__labels, self.__IGNORE = name, labels, IGNORE
def _serialize(self, *args: Any, skip: Optional[List[str]] = None) -> Iterable[Tuple[str, Any]]:
labels = list(filter(lambda label: label not in (skip or list()), self.__labels))
if len(labels) > len(args):
raise LabelerSerializerException("<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)))

35
bfxapi/notification.py Normal file
View File

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

View File

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

1
bfxapi/rest/__init__.py Normal file
View File

@@ -0,0 +1 @@
from .BfxRestInterface import BfxRestInterface

36
bfxapi/rest/enums.py Normal file
View File

@@ -0,0 +1,36 @@
from ..enums import *
class Config(str, Enum):
MAP_CURRENCY_SYM = "pub:map:currency:sym"
MAP_CURRENCY_LABEL = "pub:map:currency:label"
MAP_CURRENCY_UNIT = "pub:map:currency:unit"
MAP_CURRENCY_UNDL = "pub:map:currency:undl"
MAP_CURRENCY_POOL = "pub:map:currency:pool"
MAP_CURRENCY_EXPLORER = "pub:map:currency:explorer"
MAP_CURRENCY_TX_FEE = "pub:map:currency:tx:fee"
MAP_TX_METHOD = "pub:map:tx:method"
LIST_PAIR_EXCHANGE = "pub:list:pair:exchange"
LIST_PAIR_MARGIN = "pub:list:pair:margin"
LIST_PAIR_FUTURES = "pub:list:pair:futures"
LIST_PAIR_SECURITIES = "pub:list:pair:securities"
LIST_CURRENCY = "pub:list:currency"
LIST_COMPETITIONS = "pub:list:competitions"
INFO_PAIR = "pub:info:pair"
INFO_PAIR_FUTURES = "pub:info:pair:futures"
INFO_TX_STATUS = "pub:info:tx:status"
SPEC_MARGIN = "pub:spec:margin",
FEES = "pub:fees"
class Precision(str, Enum):
P0 = "P0"
P1 = "P1"
P2 = "P2"
P3 = "P3"
P4 = "P4"
class Sort(int, Enum):
ASCENDING = +1
DESCENDING = -1

44
bfxapi/rest/exceptions.py Normal file
View File

@@ -0,0 +1,44 @@
from .. exceptions import BfxBaseException
__all__ = [
"BfxRestException",
"RequestParametersError",
"ResourceNotFound",
"InvalidAuthenticationCredentials"
]
class BfxRestException(BfxBaseException):
"""
Base class for all custom exceptions in bfxapi/rest/exceptions.py.
"""
pass
class ResourceNotFound(BfxRestException):
"""
This error indicates a failed HTTP request to a non-existent resource.
"""
pass
class RequestParametersError(BfxRestException):
"""
This error indicates that there are some invalid parameters sent along with an HTTP request.
"""
pass
class InvalidAuthenticationCredentials(BfxRestException):
"""
This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication.
"""
pass
class UnknownGenericError(BfxRestException):
"""
This error indicates an undefined problem processing an HTTP request sent to the APIs.
"""
pass

288
bfxapi/rest/serializers.py Normal file
View File

@@ -0,0 +1,288 @@
from . import typings
from .. labeler import _Serializer
from .. notification import _Notification
#region Serializers definition for Rest Public Endpoints
PlatformStatus = _Serializer[typings.PlatformStatus]("PlatformStatus", labels=[
"OPERATIVE"
])
TradingPairTicker = _Serializer[typings.TradingPairTicker]("TradingPairTicker", labels=[
"SYMBOL",
"BID",
"BID_SIZE",
"ASK",
"ASK_SIZE",
"DAILY_CHANGE",
"DAILY_CHANGE_RELATIVE",
"LAST_PRICE",
"VOLUME",
"HIGH",
"LOW"
])
FundingCurrencyTicker = _Serializer[typings.FundingCurrencyTicker]("FundingCurrencyTicker", labels=[
"SYMBOL",
"FRR",
"BID",
"BID_PERIOD",
"BID_SIZE",
"ASK",
"ASK_PERIOD",
"ASK_SIZE",
"DAILY_CHANGE",
"DAILY_CHANGE_RELATIVE",
"LAST_PRICE",
"VOLUME",
"HIGH",
"LOW",
"_PLACEHOLDER",
"_PLACEHOLDER",
"FRR_AMOUNT_AVAILABLE"
])
TickersHistory = _Serializer[typings.TickersHistory]("TickersHistory", labels=[
"SYMBOL",
"BID",
"_PLACEHOLDER",
"ASK",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"MTS"
])
TradingPairTrade = _Serializer[typings.TradingPairTrade]("TradingPairTrade", labels=[
"ID",
"MTS",
"AMOUNT",
"PRICE"
])
FundingCurrencyTrade = _Serializer[typings.FundingCurrencyTrade]("FundingCurrencyTrade", labels=[
"ID",
"MTS",
"AMOUNT",
"RATE",
"PERIOD"
])
TradingPairBook = _Serializer[typings.TradingPairBook]("TradingPairBook", labels=[
"PRICE",
"COUNT",
"AMOUNT"
])
FundingCurrencyBook = _Serializer[typings.FundingCurrencyBook]("FundingCurrencyBook", labels=[
"RATE",
"PERIOD",
"COUNT",
"AMOUNT"
])
TradingPairRawBook = _Serializer[typings.TradingPairRawBook]("TradingPairRawBook", labels=[
"ORDER_ID",
"PRICE",
"AMOUNT"
])
FundingCurrencyRawBook = _Serializer[typings.FundingCurrencyRawBook]("FundingCurrencyRawBook", labels=[
"OFFER_ID",
"PERIOD",
"RATE",
"AMOUNT"
])
Statistic = _Serializer[typings.Statistic]("Statistic", labels=[
"MTS",
"VALUE"
])
Candle = _Serializer[typings.Candle]("Candle", labels=[
"MTS",
"OPEN",
"CLOSE",
"HIGH",
"LOW",
"VOLUME"
])
DerivativesStatus = _Serializer[typings.DerivativesStatus]("DerivativesStatus", labels=[
"KEY",
"MTS",
"_PLACEHOLDER",
"DERIV_PRICE",
"SPOT_PRICE",
"_PLACEHOLDER",
"INSURANCE_FUND_BALANCE",
"_PLACEHOLDER",
"NEXT_FUNDING_EVT_TIMESTAMP_MS",
"NEXT_FUNDING_ACCRUED",
"NEXT_FUNDING_STEP",
"_PLACEHOLDER",
"CURRENT_FUNDING",
"_PLACEHOLDER",
"_PLACEHOLDER",
"MARK_PRICE",
"_PLACEHOLDER",
"_PLACEHOLDER",
"OPEN_INTEREST",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"CLAMP_MIN",
"CLAMP_MAX"
])
Liquidation = _Serializer[typings.Liquidation]("Liquidation", labels=[
"_PLACEHOLDER",
"POS_ID",
"MTS",
"_PLACEHOLDER",
"SYMBOL",
"AMOUNT",
"BASE_PRICE",
"_PLACEHOLDER",
"IS_MATCH",
"IS_MARKET_SOLD",
"_PLACEHOLDER",
"PRICE_ACQUIRED"
])
Leaderboard = _Serializer[typings.Leaderboard]("Leaderboard", labels=[
"MTS",
"_PLACEHOLDER",
"USERNAME",
"RANKING",
"_PLACEHOLDER",
"_PLACEHOLDER",
"VALUE",
"_PLACEHOLDER",
"_PLACEHOLDER",
"TWITTER_HANDLE"
])
FundingStatistic = _Serializer[typings.FundingStatistic]("FundingStatistic", labels=[
"TIMESTAMP",
"_PLACEHOLDER",
"_PLACEHOLDER",
"FRR",
"AVG_PERIOD",
"_PLACEHOLDER",
"_PLACEHOLDER",
"FUNDING_AMOUNT",
"FUNDING_AMOUNT_USED",
"_PLACEHOLDER",
"_PLACEHOLDER",
"FUNDING_BELOW_THRESHOLD"
])
#endregion
#region Serializers definition for Rest Authenticated Endpoints
Wallet = _Serializer[typings.Wallet]("Wallet", labels=[
"WALLET_TYPE",
"CURRENCY",
"BALANCE",
"UNSETTLED_INTEREST",
"AVAILABLE_BALANCE",
"LAST_CHANGE",
"TRADE_DETAILS"
])
Order = _Serializer[typings.Order]("Order", labels=[
"ID",
"GID",
"CID",
"SYMBOL",
"MTS_CREATE",
"MTS_UPDATE",
"AMOUNT",
"AMOUNT_ORIG",
"ORDER_TYPE",
"TYPE_PREV",
"MTS_TIF",
"_PLACEHOLDER",
"FLAGS",
"ORDER_STATUS",
"_PLACEHOLDER",
"_PLACEHOLDER",
"PRICE",
"PRICE_AVG",
"PRICE_TRAILING",
"PRICE_AUX_LIMIT",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"NOTIFY",
"HIDDEN",
"PLACED_ID",
"_PLACEHOLDER",
"_PLACEHOLDER",
"ROUTING",
"_PLACEHOLDER",
"_PLACEHOLDER",
"META"
])
FundingOffer = _Serializer[typings.FundingOffer]("FundingOffer", labels=[
"ID",
"SYMBOL",
"MTS_CREATED",
"MTS_UPDATED",
"AMOUNT",
"AMOUNT_ORIG",
"OFFER_TYPE",
"_PLACEHOLDER",
"_PLACEHOLDER",
"FLAGS",
"OFFER_STATUS",
"_PLACEHOLDER",
"_PLACEHOLDER",
"_PLACEHOLDER",
"RATE",
"PERIOD",
"NOTIFY",
"HIDDEN",
"_PLACEHOLDER",
"RENEW",
"_PLACEHOLDER"
])
Trade = _Serializer[typings.Trade]("Trade", labels=[
"ID",
"PAIR",
"MTS_CREATE",
"ORDER_ID",
"EXEC_AMOUNT",
"EXEC_PRICE",
"ORDER_TYPE",
"ORDER_PRICE",
"MAKER",
"FEE",
"FEE_CURRENCY",
"CID"
])
Ledger = _Serializer[typings.Ledger]("Ledger", labels=[
"ID",
"CURRENCY",
"_PLACEHOLDER",
"MTS",
"_PLACEHOLDER",
"AMOUNT",
"BALANCE",
"_PLACEHOLDER",
"DESCRIPTION"
])
#endregion

210
bfxapi/rest/typings.py Normal file
View File

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

4
bfxapi/utils/cid.py Normal file
View File

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

9
bfxapi/utils/encoder.py Normal file
View File

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

29
bfxapi/utils/flags.py Normal file
View File

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

43
bfxapi/utils/integers.py Normal file
View File

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

View File

@@ -1,25 +1,40 @@
import traceback, json, asyncio, hmac, hashlib, time, uuid, websockets
from typing import Tuple, Union, Literal, TypeVar, Callable, cast
from enum import Enum
from pyee.asyncio import AsyncIOEventEmitter
from .typings import Inputs, Tuple, Union
from .typings import Inputs
from .handlers import Channels, PublicChannelsHandler, AuthenticatedChannelsHandler
from .exceptions import ConnectionNotOpen, TooManySubscriptions, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion
from ..utils.encoder import JSONEncoder
from ..utils.logger import Formatter, CustomLogger
_HEARTBEAT = "hb"
def _require_websocket_connection(function):
F = TypeVar("F", bound=Callable[..., Literal[None]])
def _require_websocket_connection(function: F) -> F:
async def wrapper(self, *args, **kwargs):
if self.websocket == None or self.websocket.open == False:
raise ConnectionNotOpen("No open connection with the server.")
await function(self, *args, **kwargs)
return wrapper
return cast(F, wrapper)
def _require_websocket_authentication(function: F) -> F:
async def wrapper(self, *args, **kwargs):
if self.authentication == False:
raise WebsocketAuthenticationRequired("To perform this action you need to authenticate using your API_KEY and API_SECRET.")
await _require_websocket_connection(function)(self, *args, **kwargs)
return cast(F, wrapper)
class BfxWebsocketClient(object):
VERSION = 2
@@ -118,22 +133,13 @@ class BfxWebsocketClient(object):
for bucket in self.buckets:
await bucket._close(code=code, reason=reason)
def __require_websocket_authentication(function):
async def wrapper(self, *args, **kwargs):
if self.authentication == False:
raise WebsocketAuthenticationRequired("To perform this action you need to authenticate using your API_KEY and API_SECRET.")
await _require_websocket_connection(function)(self, *args, **kwargs)
return wrapper
@__require_websocket_authentication
@_require_websocket_authentication
async def notify(self, info, MESSAGE_ID=None, **kwargs):
await self.websocket.send(json.dumps([ 0, "n", MESSAGE_ID, { "type": "ucm-test", "info": info, **kwargs } ]))
@__require_websocket_authentication
@_require_websocket_authentication
async def __handle_websocket_input(self, input, data):
await self.websocket.send(json.dumps([ 0, input, None, data]))
await self.websocket.send(json.dumps([ 0, input, None, data], cls=JSONEncoder))
def __bucket_open_signal(self, index):
if all(bucket.websocket != None and bucket.websocket.open == True for bucket in self.buckets):

View File

@@ -1,33 +1,8 @@
from enum import Enum
from ..enums import *
class Channels(str, Enum):
TICKER = "ticker"
TRADES = "trades"
BOOK = "book"
CANDLES = "candles"
STATUS = "status"
class Flags(int, Enum):
HIDDEN = 64
CLOSE = 512
REDUCE_ONLY = 1024
POST_ONLY = 4096
OCO = 16384
NO_VAR_RATES = 524288
class Errors(int, Enum):
ERR_UNK = 10000
ERR_GENERIC = 10001
ERR_CONCURRENCY = 10008
ERR_PARAMS = 10020
ERR_CONF_FAIL = 10050
ERR_AUTH_FAIL = 10100
ERR_AUTH_PAYLOAD = 10111
ERR_AUTH_SIG = 10112
ERR_AUTH_HMAC = 10113
ERR_AUTH_NONCE = 10114
ERR_UNAUTH_FAIL = 10200
ERR_SUB_FAIL = 10300
ERR_SUB_MULTI = 10301
ERR_UNSUB_FAIL = 10400
ERR_READY = 11000
STATUS = "status"

View File

@@ -1,4 +1,8 @@
from .. exceptions import BfxBaseException
__all__ = [
"BfxWebsocketException",
"ConnectionNotOpen",
"TooManySubscriptions",
"WebsocketAuthenticationRequired",
@@ -7,9 +11,9 @@ __all__ = [
"OutdatedClientVersion"
]
class BfxWebsocketException(Exception):
class BfxWebsocketException(BfxBaseException):
"""
Base class for all exceptions defined in bfx/websocket/errors.py.
Base class for all custom exceptions in bfxapi/websocket/exceptions.py.
"""
pass
@@ -35,13 +39,6 @@ class WebsocketAuthenticationRequired(BfxWebsocketException):
pass
class InvalidAuthenticationCredentials(BfxWebsocketException):
"""
This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication.
"""
pass
class EventNotSupported(BfxWebsocketException):
"""
This error indicates a failed attempt to subscribe to an event not supported by the BfxWebsocketClient.
@@ -54,4 +51,11 @@ class OutdatedClientVersion(BfxWebsocketException):
This error indicates a mismatch between the client version and the server WSS version.
"""
pass
class InvalidAuthenticationCredentials(BfxWebsocketException):
"""
This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication.
"""
pass

View File

@@ -1,25 +1,6 @@
from typing import Generic, TypeVar, Iterable, List, Any
from . import typings
from .exceptions import BfxWebsocketException
T = TypeVar("T")
class _Serializer(Generic[T]):
def __init__(self, name: str, labels: List[str]):
self.name, self.__labels = name, labels
def __serialize(self, *args: Any, IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> Iterable[T]:
if len(self.__labels) != len(args):
raise BfxWebsocketException("<self.__labels> and <*args> arguments should contain the same amount of elements.")
for index, label in enumerate(self.__labels):
if label not in IGNORE:
yield label, args[index]
def parse(self, *values: Any) -> T:
return dict(self.__serialize(*values))
from .. labeler import _Serializer
#region Serializers definition for Websocket Public Channels
@@ -315,7 +296,7 @@ BalanceInfo = _Serializer[typings.BalanceInfo]("BalanceInfo", labels=[
#region Serializers definition for Notifications channel
Notification = _Serializer("Notification", labels=[
Notification = _Serializer[typings.Notification]("Notification", labels=[
"MTS",
"TYPE",
"MESSAGE_ID",

View File

@@ -4,299 +4,292 @@ from datetime import datetime
from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any
int16 = int32 = int45 = int64 = int
from ..utils.integers import Int16, Int32, Int45, Int64
JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]]
#region Type hinting for subscription objects
class Subscriptions:
TradingPairsTicker = TypedDict("Subscriptions.TradingPairsTicker", {
"chanId": int,
"symbol": str,
"pair": str
})
class TradingPairTicker(TypedDict):
chanId: int
symbol: str
pair: str
FundingCurrenciesTicker = TypedDict("Subscriptions.FundingCurrenciesTicker", {
"chanId": int,
"symbol": str,
"currency": str
})
class FundingCurrencyTicker(TypedDict):
chanId: int
symbol: str
currency: str
TradingPairsTrades = TypedDict("Subscriptions.TradingPairsTrades", {
"chanId": int,
"symbol": str,
"pair": str
})
class TradingPairTrades(TypedDict):
chanId: int
symbol: str
pair: str
FundingCurrenciesTrades = TypedDict("Subscriptions.FundingCurrenciesTrades", {
"chanId": int,
"symbol": str,
"currency": str
})
class FundingCurrencyTrades(TypedDict):
chanId: int
symbol: str
currency: str
Book = TypedDict("Subscriptions.Book", {
"chanId": int,
"symbol": str,
"prec": str,
"freq": str,
"len": str,
"subId": int,
"pair": str
})
class Book(TypedDict):
chanId: int
symbol: str
prec: str
freq: str
len: str
subId: int
pair: str
Candles = TypedDict("Subscriptions.Candles", {
"chanId": int,
"key": str
})
class Candles(TypedDict):
chanId: int
key: str
DerivativesStatus = TypedDict("Subscriptions.DerivativesStatus", {
"chanId": int,
"key": str
})
class DerivativesStatus(TypedDict):
chanId: int
key: str
#endregion
#region Type hinting for Websocket Public Channels
TradingPairTicker = TypedDict("TradingPairTicker", {
"BID": float,
"BID_SIZE": float,
"ASK": float,
"ASK_SIZE": float,
"DAILY_CHANGE": float,
"DAILY_CHANGE_RELATIVE": float,
"LAST_PRICE": float,
"VOLUME": float,
"HIGH": float,
"LOW": float
})
class TradingPairTicker(TypedDict):
BID: float
BID_SIZE: float
ASK: float
ASK_SIZE: float
DAILY_CHANGE: float
DAILY_CHANGE_RELATIVE: float
LAST_PRICE: float
VOLUME: float
HIGH: float
LOW: float
FundingCurrencyTicker = TypedDict("FundingCurrencyTicker", {
"FRR": float,
"BID": float,
"BID_PERIOD": int,
"BID_SIZE": float,
"ASK": float,
"ASK_PERIOD": int,
"ASK_SIZE": float,
"DAILY_CHANGE": float,
"DAILY_CHANGE_RELATIVE": float,
"LAST_PRICE": float,
"VOLUME": float,
"HIGH": float,
"LOW": float,
"FRR_AMOUNT_AVAILABLE": float
})
class FundingCurrencyTicker(TypedDict):
FRR: float
BID: float
BID_PERIOD: int
BID_SIZE: float
ASK: float
ASK_PERIOD: int
ASK_SIZE: float
DAILY_CHANGE: float
DAILY_CHANGE_RELATIVE: float
LAST_PRICE: float
VOLUME: float
HIGH: float
LOW: float
FRR_AMOUNT_AVAILABLE: float
(TradingPairTrade, FundingCurrencyTrade) = (
TypedDict("TradingPairTrade", { "ID": int, "MTS": int, "AMOUNT": float, "PRICE": float }),
TypedDict("FundingCurrencyTrade", { "ID": int, "MTS": int, "AMOUNT": float, "RATE": float, "PERIOD": int })
)
class TradingPairTrade(TypedDict):
ID: int
MTS: int
AMOUNT: float
PRICE: float
(TradingPairTrades, FundingCurrencyTrades) = (List[TradingPairTrade], List[FundingCurrencyTrade])
class FundingCurrencyTrade(TypedDict):
ID: int
MTS: int
AMOUNT: float
RATE: float
PERIOD: int
(TradingPairBook, FundingCurrencyBook) = (
TypedDict("TradingPairBook", { "PRICE": float, "COUNT": int, "AMOUNT": float }),
TypedDict("FundingCurrencyBook", { "RATE": float, "PERIOD": int, "COUNT": int, "AMOUNT": float })
)
class TradingPairBook(TypedDict):
PRICE: float
COUNT: int
AMOUNT: float
class FundingCurrencyBook(TypedDict):
RATE: float
PERIOD: int
COUNT: int
AMOUNT: float
class TradingPairRawBook(TypedDict):
ORDER_ID: int
PRICE: float
AMOUNT: float
class FundingCurrencyRawBook(TypedDict):
OFFER_ID: int
PERIOD: int
RATE: float
AMOUNT: float
(TradingPairBooks, FundingCurrencyBooks) = (List[TradingPairBook], List[FundingCurrencyBook])
class Candle(TypedDict):
MTS: int
OPEN: float
CLOSE: float
HIGH: float
LOW: float
VOLUME: float
(TradingPairRawBook, FundingCurrencyRawBook) = (
TypedDict("TradingPairRawBook", { "ORDER_ID": int, "PRICE": float, "AMOUNT": float }),
TypedDict("FundingCurrencyRawBook", { "OFFER_ID": int, "PERIOD": int, "RATE": float, "AMOUNT": float }),
)
(TradingPairRawBooks, FundingCurrencyRawBooks) = (List[TradingPairRawBook], List[FundingCurrencyRawBook])
Candle = TypedDict("Candle", {
"MTS": int,
"OPEN": float,
"CLOSE": float,
"HIGH": float,
"LOW": float,
"VOLUME": float
})
Candles = List[Candle]
DerivativesStatus = TypedDict("DerivativesStatus", {
"TIME_MS": int,
"DERIV_PRICE": float,
"SPOT_PRICE": float,
"INSURANCE_FUND_BALANCE": float,
"NEXT_FUNDING_EVT_TIMESTAMP_MS": int,
"NEXT_FUNDING_ACCRUED": float,
"NEXT_FUNDING_STEP": int,
"CURRENT_FUNDING": float,
"MARK_PRICE": float,
"OPEN_INTEREST": float,
"CLAMP_MIN": float,
"CLAMP_MAX": float
})
class DerivativesStatus(TypedDict):
TIME_MS: int
DERIV_PRICE: float
SPOT_PRICE: float
INSURANCE_FUND_BALANCE: float
NEXT_FUNDING_EVT_TIMESTAMP_MS: int
NEXT_FUNDING_ACCRUED: float
NEXT_FUNDING_STEP: int
CURRENT_FUNDING: float
MARK_PRICE: float
OPEN_INTEREST: float
CLAMP_MIN: float
CLAMP_MAX: float
#endregion
#region Type hinting for Websocket Authenticated Channels
Order = TypedDict("Order", {
"ID": int,
"GID": int,
"CID": int,
"SYMBOL": str,
"MTS_CREATE": int,
"MTS_UPDATE": int,
"AMOUNT": float,
"AMOUNT_ORIG": float,
"ORDER_TYPE": str,
"TYPE_PREV": str,
"MTS_TIF": int,
"FLAGS": int,
"ORDER_STATUS": str,
"PRICE": float,
"PRICE_AVG": float,
"PRICE_TRAILING": float,
"PRICE_AUX_LIMIT": float,
"NOTIFY": int,
"HIDDEN": int,
"PLACED_ID": int,
"ROUTING": str,
"META": JSON
})
class Order(TypedDict):
ID: int
GID: int
CID: int
SYMBOL: str
MTS_CREATE: int
MTS_UPDATE: int
AMOUNT: float
AMOUNT_ORIG: float
ORDER_TYPE: str
TYPE_PREV: str
MTS_TIF: int
FLAGS: int
ORDER_STATUS: str
PRICE: float
PRICE_AVG: float
PRICE_TRAILING: float
PRICE_AUX_LIMIT: float
NOTIFY: int
HIDDEN: int
PLACED_ID: int
ROUTING: str
META: JSON
Orders = List[Order]
class Position(TypedDict):
SYMBOL: str
STATUS: str
AMOUNT: float
BASE_PRICE: float
MARGIN_FUNDING: float
MARGIN_FUNDING_TYPE: int
PL: float
PL_PERC: float
PRICE_LIQ: float
LEVERAGE: float
POSITION_ID: int
MTS_CREATE: int
MTS_UPDATE: int
TYPE: int
COLLATERAL: float
COLLATERAL_MIN: float
META: JSON
Position = TypedDict("Position", {
"SYMBOL": str,
"STATUS": str,
"AMOUNT": float,
"BASE_PRICE": float,
"MARGIN_FUNDING": float,
"MARGIN_FUNDING_TYPE": int,
"PL": float,
"PL_PERC": float,
"PRICE_LIQ": float,
"LEVERAGE": float,
"POSITION_ID": int,
"MTS_CREATE": int,
"MTS_UPDATE": int,
"TYPE": int,
"COLLATERAL": float,
"COLLATERAL_MIN": float,
"META": JSON,
})
class TradeExecuted(TypedDict):
ID: int
SYMBOL: str
MTS_CREATE: int
ORDER_ID: int
EXEC_AMOUNT: float
EXEC_PRICE: float
ORDER_TYPE: str
ORDER_PRICE: float
MAKER:int
CID: int
Positions = List[Position]
class TradeExecutionUpdate(TypedDict):
ID: int
SYMBOL: str
MTS_CREATE: int
ORDER_ID: int
EXEC_AMOUNT: float
EXEC_PRICE: float
ORDER_TYPE: str
ORDER_PRICE: float
MAKER:int
FEE: float
FEE_CURRENCY: str
CID: int
TradeExecuted = TypedDict("TradeExecuted", {
"ID": int,
"SYMBOL": str,
"MTS_CREATE": int,
"ORDER_ID": int,
"EXEC_AMOUNT": float,
"EXEC_PRICE": float,
"ORDER_TYPE": str,
"ORDER_PRICE": float,
"MAKER":int,
"CID": int
})
class FundingOffer(TypedDict):
ID: int
SYMBOL: str
MTS_CREATED: int
MTS_UPDATED: int
AMOUNT: float
AMOUNT_ORIG: float
OFFER_TYPE: str
FLAGS: int
STATUS: str
RATE: float
PERIOD: int
NOTIFY: int
HIDDEN: int
RENEW: int
TradeExecutionUpdate = TypedDict("TradeExecutionUpdate", {
"ID": int,
"SYMBOL": str,
"MTS_CREATE": int,
"ORDER_ID": int,
"EXEC_AMOUNT": float,
"EXEC_PRICE": float,
"ORDER_TYPE": str,
"ORDER_PRICE": float,
"MAKER":int,
"FEE": float,
"FEE_CURRENCY": str,
"CID": int
})
class FundingCredit(TypedDict):
ID: int
SYMBOL: str
SIDE: int
MTS_CREATE: int
MTS_UPDATE: int
AMOUNT: float
FLAGS: int
STATUS: str
RATE: float
PERIOD: int
MTS_OPENING: int
MTS_LAST_PAYOUT: int
NOTIFY: int
HIDDEN: int
RENEW: int
RATE_REAL: float
NO_CLOSE: int
POSITION_PAIR: str
FundingOffer = TypedDict("FundingOffer", {
"ID": int,
"SYMBOL": str,
"MTS_CREATED": int,
"MTS_UPDATED": int,
"AMOUNT": float,
"AMOUNT_ORIG": float,
"OFFER_TYPE": str,
"FLAGS": int,
"STATUS": str,
"RATE": float,
"PERIOD": int,
"NOTIFY": int,
"HIDDEN": int,
"RENEW": int,
})
class FundingLoan(TypedDict):
ID: int
SYMBOL: str
SIDE: int
MTS_CREATE: int
MTS_UPDATE: int
AMOUNT: float
FLAGS: int
STATUS: str
RATE: float
PERIOD: int
MTS_OPENING: int
MTS_LAST_PAYOUT: int
NOTIFY: int
HIDDEN: int
RENEW: int
RATE_REAL: float
NO_CLOSE: int
FundingOffers = List[FundingOffer]
class Wallet(TypedDict):
WALLET_TYPE: str
CURRENCY: str
BALANCE: float
UNSETTLED_INTEREST: float
BALANCE_AVAILABLE: float
DESCRIPTION: str
META: JSON
FundingCredit = TypedDict("FundingCredit", {
"ID": int,
"SYMBOL": str,
"SIDE": int,
"MTS_CREATE": int,
"MTS_UPDATE": int,
"AMOUNT": float,
"FLAGS": int,
"STATUS": str,
"RATE": float,
"PERIOD": int,
"MTS_OPENING": int,
"MTS_LAST_PAYOUT": int,
"NOTIFY": int,
"HIDDEN": int,
"RENEW": int,
"RATE_REAL": float,
"NO_CLOSE": int,
"POSITION_PAIR": str
})
class BalanceInfo(TypedDict):
AUM: float
AUM_NET: float
FundingCredits = List[FundingCredit]
#endregion
FundingLoan = TypedDict("FundingLoan", {
"ID": int,
"SYMBOL": str,
"SIDE": int,
"MTS_CREATE": int,
"MTS_UPDATE": int,
"AMOUNT": float,
"FLAGS": int,
"STATUS": str,
"RATE": float,
"PERIOD": int,
"MTS_OPENING": int,
"MTS_LAST_PAYOUT": int,
"NOTIFY": int,
"HIDDEN": int,
"RENEW": int,
"RATE_REAL": float,
"NO_CLOSE": int
})
#region Type hinting for Notifications channel
FundingLoans = List[FundingLoan]
Wallet = TypedDict("Wallet", {
"WALLET_TYPE": str,
"CURRENCY": str,
"BALANCE": float,
"UNSETTLED_INTEREST": float,
"BALANCE_AVAILABLE": float,
"DESCRIPTION": str,
"META": JSON
})
Wallets = List[Wallet]
BalanceInfo = TypedDict("BalanceInfo", {
"AUM": float,
"AUM_NET": float
})
class Notification(TypedDict):
MTS: int
TYPE: str
MESSAGE_ID: int
NOTIFY_INFO: JSON
CODE: int
STATUS: str
TEXT: str
#endregion
@@ -304,55 +297,50 @@ BalanceInfo = TypedDict("BalanceInfo", {
class Inputs:
class Order:
New = TypedDict("Inputs.Order.New", {
"gid": Optional[int32],
"cid": int45,
"type": str,
"symbol": str,
"amount": Union[Decimal, str],
"price": Union[Decimal, str],
"lev": int,
"price_trailing": Union[Decimal, str],
"price_aux_limit": Union[Decimal, str],
"price_oco_stop": Union[Decimal, str],
"flags": int16,
"tif": Union[datetime, str],
"meta": JSON
})
class New(TypedDict, total=False):
gid: Union[Int32, int]
cid: Union[Int45, int]
type: str
symbol: str
amount: Union[Decimal, str]
price: Union[Decimal, str]
lev: Union[Int32, int]
price_trailing: Union[Decimal, str]
price_aux_limit: Union[Decimal, str]
price_oco_stop: Union[Decimal, str]
flags: Union[Int16, int]
tif: Union[datetime, str]
meta: JSON
Update = TypedDict("Inputs.Order.Update", {
"id": int64,
"cid": int45,
"cid_date": str,
"gid": int32,
"price": Union[Decimal, str],
"amount": Union[Decimal, str],
"lev": int,
"delta": Union[Decimal, str],
"price_aux_limit": Union[Decimal, str],
"price_trailing": Union[Decimal, str],
"flags": int16,
"tif": Union[datetime, str]
})
class Update(TypedDict, total=False):
id: Union[Int64, int]
cid: Union[Int45, int]
cid_date: str
gid: Union[Int32, int]
price: Union[Decimal, str]
amount: Union[Decimal, str]
lev: Union[Int32, int]
delta: Union[Decimal, str]
price_aux_limit: Union[Decimal, str]
price_trailing: Union[Decimal, str]
flags: Union[Int16, int]
tif: Union[datetime, str]
Cancel = TypedDict("Inputs.Order.Cancel", {
"id": int64,
"cid": int45,
"cid_date": str
})
class Cancel(TypedDict, total=False):
id: Union[Int64, int]
cid: Union[Int45, int]
cid_date: Union[datetime, str]
class Offer:
New = TypedDict("Inputs.Offer.New", {
"type": str,
"symbol": str,
"amount": Union[Decimal, str],
"rate": Union[Decimal, str],
"period": int,
"flags": int16
})
class New(TypedDict, total=False):
type: str
symbol: str
amount: Union[Decimal, str]
rate: Union[Decimal, str]
period: Union[Int32, int]
flags: Union[Int16, int]
Cancel = TypedDict("Inputs.Offer.Cancel", {
"id": int
})
class Cancel(TypedDict, total=False):
id: Union[Int32, int]
#endregion

View File

@@ -0,0 +1,26 @@
import os
from bfxapi.client import Client, Constants
from bfxapi.enums import FundingOfferType
from bfxapi.utils.flags import calculate_offer_flags
bfx = Client(
REST_HOST=Constants.REST_HOST,
API_KEY=os.getenv("BFX_API_KEY"),
API_SECRET=os.getenv("BFX_API_SECRET")
)
notification = bfx.rest.auth.submit_funding_offer(
type=FundingOfferType.LIMIT,
symbol="fUSD",
amount="123.45",
rate="0.001",
period=2,
flags=calculate_offer_flags(hidden=True)
)
print("Offer notification:", notification)
offers = bfx.rest.auth.get_active_funding_offers()
print("Offers:", offers)

View File

@@ -0,0 +1,36 @@
import os
from bfxapi.client import Client, Constants
from bfxapi.enums import OrderType
from bfxapi.utils.flags import calculate_order_flags
bfx = Client(
REST_HOST=Constants.REST_HOST,
API_KEY=os.getenv("BFX_API_KEY"),
API_SECRET=os.getenv("BFX_API_SECRET")
)
# Create a new order
submitted_order = bfx.rest.auth.submit_order(
type=OrderType.EXCHANGE_LIMIT,
symbol="tBTCUST",
amount="0.015",
price="10000",
flags=calculate_order_flags(hidden=False)
)
print("Submit Order Notification:", submitted_order)
# Update it
updated_order = bfx.rest.auth.update_order(
id=submitted_order["NOTIFY_INFO"]["ID"],
amount="0.020",
price="10100"
)
print("Update Order Notification:", updated_order)
# Delete it
canceled_order = bfx.rest.auth.cancel_order(id=submitted_order["NOTIFY_INFO"]["ID"])
print("Cancel Order Notification:", canceled_order)

View File

@@ -0,0 +1,47 @@
# python -c "from examples.websocket.create_order import *"
import os
from bfxapi.client import Client, Constants
from bfxapi.utils.cid import generate_unique_cid
from bfxapi.websocket.enums import Error, OrderType
from bfxapi.websocket.typings import Inputs
bfx = Client(
WSS_HOST=Constants.WSS_HOST,
API_KEY=os.getenv("BFX_API_KEY"),
API_SECRET=os.getenv("BFX_API_SECRET")
)
@bfx.wss.on("wss-error")
def on_wss_error(code: Error, msg: str):
print(code, msg)
@bfx.wss.on("authenticated")
async def on_open(event):
print(f"Auth event {event}")
order: Inputs.Order.New = {
"gid": generate_unique_cid(),
"type": OrderType.EXCHANGE_LIMIT,
"symbol": "tBTCUST",
"amount": "0.1",
"price": "10000.0"
}
await bfx.wss.inputs.order_new(order)
print(f"Order sent")
@bfx.wss.on("notification")
async def on_notification(notification):
print(f"Notification {notification}")
@bfx.wss.on("order_new")
async def on_order_new(order_new: Inputs.Order.New):
print(f"Order new {order_new}")
@bfx.wss.on("subscribed")
def on_subscribed(subscription):
print(f"Subscription successful <{subscription}>")
bfx.wss.run()

View File

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

View File

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

View File

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

Binary file not shown.

View File

@@ -3,7 +3,7 @@ from distutils.core import setup
setup(
name="bitfinex-api-py",
version="3.0.0",
packages=[ "bfxapi", "bfxapi.websocket", "bfxapi.utils" ],
packages=[ "bfxapi", "bfxapi.websocket", "bfxapi.rest", "bfxapi.utils" ],
url="https://github.com/bitfinexcom/bitfinex-api-py",
license="OSI Approved :: Apache Software License",
author="Bitfinex",
@@ -11,8 +11,18 @@ setup(
description="Official Bitfinex Python API",
keywords="bitfinex,api,trading",
install_requires=[
"certifi~=2022.12.7",
"charset-normalizer~=2.1.1",
"idna~=3.4",
"mypy~=0.991",
"mypy-extensions~=0.4.3",
"pyee~=9.0.4",
"requests~=2.28.1",
"tomli~=2.0.1",
"types-requests~=2.28.11.5",
"types-urllib3~=1.26.25.4",
"typing_extensions~=4.4.0",
"urllib3~=1.26.13",
"websockets~=10.4",
],
project_urls={