mirror of
https://github.com/aljazceru/bitfinex-api-py.git
synced 2025-12-19 06:44:22 +01:00
Merge pull request #35 from Davi0kProgramsThings/fix/refactoring
Merge branch `fix/refactoring` in branch `feature/rest`.
This commit is contained in:
1
LICENSE
1
LICENSE
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
Apache License
|
Apache License
|
||||||
Version 2.0, January 2004
|
Version 2.0, January 2004
|
||||||
http://www.apache.org/licenses/
|
http://www.apache.org/licenses/
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -1,37 +1,31 @@
|
|||||||
from .rest import BfxRestInterface
|
from .rest import BfxRestInterface
|
||||||
from .websocket import BfxWebsocketClient
|
from .websocket import BfxWebsocketClient
|
||||||
|
from .urls import REST_HOST, WSS_HOST
|
||||||
|
|
||||||
from typing import Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
class Constants(str, Enum):
|
|
||||||
REST_HOST = "https://api.bitfinex.com/v2"
|
|
||||||
PUB_REST_HOST = "https://api-pub.bitfinex.com/v2"
|
|
||||||
STAGING_REST_HOST = "https://api.staging.bitfinex.com/v2"
|
|
||||||
|
|
||||||
WSS_HOST = "wss://api.bitfinex.com/ws/2"
|
|
||||||
PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2"
|
|
||||||
STAGING_WSS_HOST = "wss://api.staging.bitfinex.com/ws/2"
|
|
||||||
|
|
||||||
class Client(object):
|
class Client(object):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
REST_HOST: str = Constants.REST_HOST,
|
REST_HOST: str = REST_HOST,
|
||||||
WSS_HOST: str = Constants.WSS_HOST,
|
WSS_HOST: str = WSS_HOST,
|
||||||
API_KEY: Optional[str] = None,
|
API_KEY: Optional[str] = None,
|
||||||
API_SECRET: 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(
|
self.rest = BfxRestInterface(
|
||||||
host=REST_HOST,
|
host=REST_HOST,
|
||||||
API_KEY=API_KEY,
|
credentials=credentials
|
||||||
API_SECRET=API_SECRET
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.wss = BfxWebsocketClient(
|
self.wss = BfxWebsocketClient(
|
||||||
host=WSS_HOST,
|
host=WSS_HOST,
|
||||||
API_KEY=API_KEY,
|
credentials=credentials,
|
||||||
API_SECRET=API_SECRET,
|
|
||||||
log_level=log_level
|
log_level=log_level
|
||||||
)
|
)
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
from .endpoints import BfxRestInterface, RestPublicEndpoints, RestAuthenticatedEndpoints
|
from .endpoints import BfxRestInterface, RestPublicEndpoints, RestAuthenticatedEndpoints, \
|
||||||
|
RestMerchantEndpoints
|
||||||
|
|
||||||
NAME = "rest"
|
NAME = "rest"
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
from .bfx_rest_interface import BfxRestInterface
|
from .bfx_rest_interface import BfxRestInterface
|
||||||
|
|
||||||
from .rest_public_endpoints import RestPublicEndpoints
|
from .rest_public_endpoints import RestPublicEndpoints
|
||||||
from .rest_authenticated_endpoints import RestAuthenticatedEndpoints
|
from .rest_authenticated_endpoints import RestAuthenticatedEndpoints
|
||||||
|
from .rest_merchant_endpoints import RestMerchantEndpoints
|
||||||
|
|
||||||
NAME = "endpoints"
|
NAME = "endpoints"
|
||||||
@@ -7,7 +7,10 @@ from .rest_merchant_endpoints import RestMerchantEndpoints
|
|||||||
class BfxRestInterface(object):
|
class BfxRestInterface(object):
|
||||||
VERSION = 2
|
VERSION = 2
|
||||||
|
|
||||||
def __init__(self, host: str, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None):
|
def __init__(self, host, credentials = None):
|
||||||
|
API_KEY, API_SECRET = credentials and \
|
||||||
|
(credentials["API_KEY"], credentials["API_SECRET"]) or (None, None)
|
||||||
|
|
||||||
self.public = RestPublicEndpoints(host=host)
|
self.public = RestPublicEndpoints(host=host)
|
||||||
self.auth = RestAuthenticatedEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET)
|
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)
|
self.merchant = RestMerchantEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import List, Union, Literal, Optional
|
from typing import List, Tuple, Union, Literal, Optional
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from typing import List, Union, Literal, Optional
|
from typing import TypedDict, List, Union, Literal, Optional
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from .. types import *
|
from .. types import *
|
||||||
from .. middleware import Middleware
|
from .. middleware import Middleware
|
||||||
from ... utils.camel_and_snake_case_adapters import to_snake_case_keys, to_camel_case_keys
|
from ...utils.camel_and_snake_case_helpers import to_snake_case_keys, to_camel_case_keys
|
||||||
|
|
||||||
_CustomerInfo = TypedDict("_CustomerInfo", {
|
_CustomerInfo = TypedDict("_CustomerInfo", {
|
||||||
"nationality": str, "resid_country": str, "resid_city": str,
|
"nationality": str, "resid_country": str, "resid_city": str,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class RestPublicEndpoints(Middleware):
|
|||||||
if isinstance(pairs, str) and pairs == "ALL":
|
if isinstance(pairs, str) and pairs == "ALL":
|
||||||
return [ cast(TradingPairTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.symbol).startswith("t") ]
|
return [ cast(TradingPairTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.symbol).startswith("t") ]
|
||||||
|
|
||||||
data = self.get_tickers([ "t" + pair for pair in pairs ])
|
data = self.get_tickers([ pair for pair in pairs ])
|
||||||
|
|
||||||
return cast(List[TradingPairTicker], data)
|
return cast(List[TradingPairTicker], data)
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ class RestPublicEndpoints(Middleware):
|
|||||||
if isinstance(currencies, str) and currencies == "ALL":
|
if isinstance(currencies, str) and currencies == "ALL":
|
||||||
return [ cast(FundingCurrencyTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.symbol).startswith("f") ]
|
return [ cast(FundingCurrencyTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.symbol).startswith("f") ]
|
||||||
|
|
||||||
data = self.get_tickers([ "f" + currency for currency in currencies ])
|
data = self.get_tickers([ currency for currency in currencies ])
|
||||||
|
|
||||||
return cast(List[FundingCurrencyTicker], data)
|
return cast(List[FundingCurrencyTicker], data)
|
||||||
|
|
||||||
@@ -52,25 +52,25 @@ class RestPublicEndpoints(Middleware):
|
|||||||
|
|
||||||
def get_t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[TradingPairTrade]:
|
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 }
|
params = { "limit": limit, "start": start, "end": end, "sort": sort }
|
||||||
data = self._GET(f"trades/{'t' + pair}/hist", params=params)
|
data = self._GET(f"trades/{pair}/hist", params=params)
|
||||||
return [ serializers.TradingPairTrade.parse(*sub_data) for sub_data in data ]
|
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]:
|
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 }
|
params = { "limit": limit, "start": start, "end": end, "sort": sort }
|
||||||
data = self._GET(f"trades/{'f' + currency}/hist", params=params)
|
data = self._GET(f"trades/{currency}/hist", params=params)
|
||||||
return [ serializers.FundingCurrencyTrade.parse(*sub_data) for sub_data in data ]
|
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]:
|
def get_t_book(self, pair: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairBook]:
|
||||||
return [ serializers.TradingPairBook.parse(*sub_data) for sub_data in self._GET(f"book/{'t' + pair}/{precision}", params={ "len": len }) ]
|
return [ serializers.TradingPairBook.parse(*sub_data) for sub_data in self._GET(f"book/{pair}/{precision}", params={ "len": len }) ]
|
||||||
|
|
||||||
def get_f_book(self, currency: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyBook]:
|
def get_f_book(self, currency: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyBook]:
|
||||||
return [ serializers.FundingCurrencyBook.parse(*sub_data) for sub_data in self._GET(f"book/{'f' + currency}/{precision}", params={ "len": len }) ]
|
return [ serializers.FundingCurrencyBook.parse(*sub_data) for sub_data in self._GET(f"book/{currency}/{precision}", params={ "len": len }) ]
|
||||||
|
|
||||||
def get_t_raw_book(self, pair: str, len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairRawBook]:
|
def get_t_raw_book(self, pair: str, len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairRawBook]:
|
||||||
return [ serializers.TradingPairRawBook.parse(*sub_data) for sub_data in self._GET(f"book/{'t' + pair}/R0", params={ "len": len }) ]
|
return [ serializers.TradingPairRawBook.parse(*sub_data) for sub_data in self._GET(f"book/{pair}/R0", params={ "len": len }) ]
|
||||||
|
|
||||||
def get_f_raw_book(self, currency: str, len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyRawBook]:
|
def get_f_raw_book(self, currency: str, len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyRawBook]:
|
||||||
return [ serializers.FundingCurrencyRawBook.parse(*sub_data) for sub_data in self._GET(f"book/{'f' + currency}/R0", params={ "len": len }) ]
|
return [ serializers.FundingCurrencyRawBook.parse(*sub_data) for sub_data in self._GET(f"book/{currency}/R0", params={ "len": len }) ]
|
||||||
|
|
||||||
def get_stats_hist(
|
def get_stats_hist(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Literal, Any
|
from typing import List, Dict, Optional, Literal, Any
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
@@ -575,69 +575,68 @@ class InvoiceSubmission(_Type):
|
|||||||
currency: str
|
currency: str
|
||||||
order_id: str
|
order_id: str
|
||||||
pay_currencies: List[str]
|
pay_currencies: List[str]
|
||||||
webhook: Optional[str]
|
webhook: str
|
||||||
redirect_url: Optional[str]
|
redirect_url: str
|
||||||
status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"]
|
status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"]
|
||||||
customer_info: Optional["_CustomerInfo"]
|
customer_info: "CustomerInfo"
|
||||||
invoices: List["_Invoice"]
|
invoices: List["Invoice"]
|
||||||
payment: Optional["_Payment"]
|
payment: "Payment"
|
||||||
additional_payments: Optional[List["_Payment"]]
|
additional_payments: List["Payment"]
|
||||||
|
|
||||||
merchant_name: str
|
merchant_name: str
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse(cls, data: Dict[str, Any]) -> "InvoiceSubmission":
|
def parse(cls, data: Dict[str, Any]) -> "InvoiceSubmission":
|
||||||
if "customer_info" in data and data["customer_info"] != None:
|
if "customer_info" in data and data["customer_info"] != None:
|
||||||
data["customer_info"] = _CustomerInfo(**data["customer_info"])
|
data["customer_info"] = InvoiceSubmission.CustomerInfo(**data["customer_info"])
|
||||||
|
|
||||||
for index, invoice in enumerate(data["invoices"]):
|
for index, invoice in enumerate(data["invoices"]):
|
||||||
data["invoices"][index] = _Invoice(**invoice)
|
data["invoices"][index] = InvoiceSubmission.Invoice(**invoice)
|
||||||
|
|
||||||
if "payment" in data and data["payment"] != None:
|
if "payment" in data and data["payment"] != None:
|
||||||
data["payment"] = _Payment(**data["payment"])
|
data["payment"] = InvoiceSubmission.Payment(**data["payment"])
|
||||||
|
|
||||||
if "additional_payments" in data and data["additional_payments"] != None:
|
if "additional_payments" in data and data["additional_payments"] != None:
|
||||||
for index, additional_payment in enumerate(data["additional_payments"]):
|
for index, additional_payment in enumerate(data["additional_payments"]):
|
||||||
data["additional_payments"][index] = _Payment(**additional_payment)
|
data["additional_payments"][index] = InvoiceSubmission.Payment(**additional_payment)
|
||||||
|
|
||||||
return InvoiceSubmission(**data)
|
return InvoiceSubmission(**data)
|
||||||
|
|
||||||
@compose(dataclass, partial)
|
@compose(dataclass, partial)
|
||||||
class _CustomerInfo:
|
class CustomerInfo:
|
||||||
nationality: str
|
nationality: str
|
||||||
resid_country: str
|
resid_country: str
|
||||||
resid_state: Optional[str]
|
resid_state: str
|
||||||
resid_city: str
|
resid_city: str
|
||||||
resid_zip_code: str
|
resid_zip_code: str
|
||||||
resid_street: str
|
resid_street: str
|
||||||
resid_building_no: Optional[str]
|
resid_building_no: str
|
||||||
full_name: str
|
full_name: str
|
||||||
email: str
|
email: str
|
||||||
tos_accepted: bool
|
tos_accepted: bool
|
||||||
|
|
||||||
@compose(dataclass, partial)
|
@compose(dataclass, partial)
|
||||||
class _Invoice:
|
class Invoice:
|
||||||
amount: float
|
amount: float
|
||||||
currency: str
|
currency: str
|
||||||
pay_currency: str
|
pay_currency: str
|
||||||
pool_currency: str
|
pool_currency: str
|
||||||
address: str
|
address: str
|
||||||
ext: JSON
|
ext: JSON
|
||||||
|
|
||||||
@compose(dataclass, partial)
|
@compose(dataclass, partial)
|
||||||
class _Payment:
|
class Payment:
|
||||||
txid: str
|
txid: str
|
||||||
amount: float
|
amount: float
|
||||||
currency: str
|
currency: str
|
||||||
method: str
|
method: str
|
||||||
status: Literal["CREATED", "COMPLETED", "PROCESSING"]
|
status: Literal["CREATED", "COMPLETED", "PROCESSING"]
|
||||||
confirmations: int
|
confirmations: int
|
||||||
created_at: str
|
created_at: str
|
||||||
updated_at: str
|
updated_at: str
|
||||||
deposit_id: Optional[int]
|
deposit_id: int
|
||||||
ledger_id: Optional[int]
|
ledger_id: int
|
||||||
force_completed: Optional[bool]
|
force_completed: bool
|
||||||
amount_diff: Optional[str]
|
amount_diff: str
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class InvoiceStats(_Type):
|
class InvoiceStats(_Type):
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from .test_rest_serializers_and_types import TestRestSerializersAndTypes
|
from .test_rest_serializers import TestRestSerializers
|
||||||
from .test_websocket_serializers_and_types import TestWebsocketSerializersAndTypes
|
from .test_websocket_serializers import TestWebsocketSerializers
|
||||||
from .test_labeler import TestLabeler
|
from .test_labeler import TestLabeler
|
||||||
from .test_notification import TestNotification
|
from .test_notification import TestNotification
|
||||||
|
|
||||||
@@ -8,8 +8,8 @@ NAME = "tests"
|
|||||||
|
|
||||||
def suite():
|
def suite():
|
||||||
return unittest.TestSuite([
|
return unittest.TestSuite([
|
||||||
unittest.makeSuite(TestRestSerializersAndTypes),
|
unittest.makeSuite(TestRestSerializers),
|
||||||
unittest.makeSuite(TestWebsocketSerializersAndTypes),
|
unittest.makeSuite(TestWebsocketSerializers),
|
||||||
unittest.makeSuite(TestLabeler),
|
unittest.makeSuite(TestLabeler),
|
||||||
unittest.makeSuite(TestNotification),
|
unittest.makeSuite(TestNotification),
|
||||||
])
|
])
|
||||||
|
|||||||
17
bfxapi/tests/test_rest_serializers.py
Normal file
17
bfxapi/tests/test_rest_serializers.py
Normal 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()
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import unittest
|
|
||||||
|
|
||||||
from ..rest import serializers, types
|
|
||||||
|
|
||||||
class TestRestSerializersAndTypes(unittest.TestCase):
|
|
||||||
def test_consistency(self):
|
|
||||||
for serializer in map(serializers.__dict__.get, serializers.__serializers__):
|
|
||||||
type = types.__dict__.get(serializer.name)
|
|
||||||
|
|
||||||
self.assertIsNotNone(type, f"_Serializer <{serializer.name}>: no respective _Type found in bfxapi.rest.types.")
|
|
||||||
self.assertEqual(serializer.klass, type, f"_Serializer <{serializer.name}>.klass: field does not match with respective _Type in bfxapi.rest.types.")
|
|
||||||
|
|
||||||
self.assertListEqual(serializer.get_labels(), list(type.__annotations__),
|
|
||||||
f"_Serializer <{serializer.name}> and _Type <{type.__name__}> must have matching labels and fields.")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
17
bfxapi/tests/test_websocket_serializers.py
Normal file
17
bfxapi/tests/test_websocket_serializers.py
Normal 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()
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import unittest
|
|
||||||
|
|
||||||
from ..websocket import serializers, types
|
|
||||||
|
|
||||||
class TestWebsocketSerializersAndTypes(unittest.TestCase):
|
|
||||||
def test_consistency(self):
|
|
||||||
for serializer in map(serializers.__dict__.get, serializers.__serializers__):
|
|
||||||
type = types.__dict__.get(serializer.name)
|
|
||||||
|
|
||||||
self.assertIsNotNone(type, f"_Serializer <{serializer.name}>: no respective _Type found in bfxapi.websocket.types.")
|
|
||||||
self.assertEqual(serializer.klass, type, f"_Serializer <{serializer.name}>.klass: field does not match with respective _Type in bfxapi.websocket.types.")
|
|
||||||
|
|
||||||
self.assertListEqual(serializer.get_labels(), list(type.__annotations__),
|
|
||||||
f"_Serializer <{serializer.name}> and _Type <{type.__name__}> must have matching labels and fields.")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
7
bfxapi/urls.py
Normal file
7
bfxapi/urls.py
Normal 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"
|
||||||
@@ -2,8 +2,6 @@ import json
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
from typing import Type, List, Dict, Union, Any
|
from typing import Type, List, Dict, Union, Any
|
||||||
|
|
||||||
JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]]
|
JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]]
|
||||||
|
|||||||
@@ -1,99 +1,52 @@
|
|||||||
"""
|
|
||||||
Module used to describe all of the different data types
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
|
||||||
|
|
||||||
RESET_SEQ = "\033[0m"
|
RESET_SEQ = "\033[0m"
|
||||||
|
|
||||||
COLOR_SEQ = "\033[1;%dm"
|
COLOR_SEQ = "\033[1;%dm"
|
||||||
|
ITALIC_COLOR_SEQ = "\033[3;%dm"
|
||||||
|
UNDERLINE_COLOR_SEQ = "\033[4;%dm"
|
||||||
|
|
||||||
BOLD_SEQ = "\033[1m"
|
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):
|
def formatter_message(message, use_color = True):
|
||||||
"""
|
|
||||||
Syntax highlight certain keywords
|
|
||||||
"""
|
|
||||||
if use_color:
|
if use_color:
|
||||||
message = message.replace("$RESET", RESET_SEQ).replace("$BOLD", BOLD_SEQ)
|
message = message.replace("$RESET", RESET_SEQ).replace("$BOLD", BOLD_SEQ)
|
||||||
else:
|
else:
|
||||||
message = message.replace("$RESET", "").replace("$BOLD", "")
|
message = message.replace("$RESET", "").replace("$BOLD", "")
|
||||||
return message
|
return message
|
||||||
|
|
||||||
def format_word(message, word, color_seq, bold=False, underline=False):
|
COLORS = {
|
||||||
"""
|
"DEBUG": CYAN,
|
||||||
Surround the given word with a sequence
|
"INFO": BLUE,
|
||||||
"""
|
"WARNING": YELLOW,
|
||||||
replacer = color_seq + word + RESET_SEQ
|
"ERROR": RED
|
||||||
if underline:
|
}
|
||||||
replacer = UNDERLINE_SEQ + replacer
|
|
||||||
if bold:
|
|
||||||
replacer = BOLD_SEQ + replacer
|
|
||||||
return message.replace(word, replacer)
|
|
||||||
|
|
||||||
class Formatter(logging.Formatter):
|
class _ColoredFormatter(logging.Formatter):
|
||||||
"""
|
def __init__(self, msg, use_color = True):
|
||||||
This Formatted simply colors in the levelname i.e 'INFO', 'DEBUG'
|
logging.Formatter.__init__(self, msg, "%d-%m-%Y %H:%M:%S")
|
||||||
"""
|
self.use_color = use_color
|
||||||
def __init__(self, msg, use_color = True):
|
|
||||||
logging.Formatter.__init__(self, msg)
|
|
||||||
self.use_color = use_color
|
|
||||||
|
|
||||||
def format(self, record):
|
def format(self, record):
|
||||||
"""
|
levelname = record.levelname
|
||||||
Format and highlight certain keywords
|
if self.use_color and levelname in COLORS:
|
||||||
"""
|
levelname_color = COLOR_SEQ % (30 + COLORS[levelname]) + levelname + RESET_SEQ
|
||||||
levelname = record.levelname
|
record.levelname = levelname_color
|
||||||
if self.use_color and levelname in KEYWORD_COLORS:
|
record.name = ITALIC_COLOR_SEQ % (30 + BLACK) + record.name + RESET_SEQ
|
||||||
levelname_color = KEYWORD_COLORS[levelname] + levelname + RESET_SEQ
|
return logging.Formatter.format(self, record)
|
||||||
record.levelname = levelname_color
|
|
||||||
record.name = GREY + record.name + RESET_SEQ
|
|
||||||
return logging.Formatter.format(self, record)
|
|
||||||
|
|
||||||
class CustomLogger(logging.Logger):
|
class ColoredLogger(logging.Logger):
|
||||||
"""
|
FORMAT = "[$BOLD%(name)s$RESET] [%(asctime)s] [%(levelname)s] %(message)s"
|
||||||
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"
|
|
||||||
COLOR_FORMAT = formatter_message(FORMAT, True)
|
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'):
|
colored_formatter = _ColoredFormatter(self.COLOR_FORMAT)
|
||||||
logging.Logger.__init__(self, name, logLevel)
|
|
||||||
color_formatter = Formatter(self.COLOR_FORMAT)
|
|
||||||
console = logging.StreamHandler()
|
console = logging.StreamHandler()
|
||||||
console.setFormatter(color_formatter)
|
console.setFormatter(colored_formatter)
|
||||||
self.addHandler(console)
|
|
||||||
logging.addLevelName(self.TRADE, "TRADE")
|
|
||||||
return
|
|
||||||
|
|
||||||
def set_level(self, level):
|
self.addHandler(console)
|
||||||
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)
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
import traceback, json, asyncio, hmac, hashlib, time, uuid, websockets
|
|
||||||
|
|
||||||
from typing import Literal, TypeVar, Callable, cast
|
|
||||||
|
|
||||||
from pyee.asyncio import AsyncIOEventEmitter
|
|
||||||
|
|
||||||
from ._BfxWebsocketBucket import _HEARTBEAT, F, _require_websocket_connection, _BfxWebsocketBucket
|
|
||||||
|
|
||||||
from ._BfxWebsocketInputs import _BfxWebsocketInputs
|
|
||||||
from .handlers import Channels, PublicChannelsHandler, AuthenticatedChannelsHandler
|
|
||||||
from .exceptions import WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported
|
|
||||||
|
|
||||||
from ..utils.JSONEncoder import JSONEncoder
|
|
||||||
|
|
||||||
from ..utils.logger import Formatter, CustomLogger
|
|
||||||
|
|
||||||
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 = _BfxWebsocketBucket.VERSION
|
|
||||||
|
|
||||||
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(str(exception) + "\n" +
|
|
||||||
str().join(traceback.format_exception(type(exception), exception, exception.__traceback__))[:-1])
|
|
||||||
)
|
|
||||||
|
|
||||||
self.API_KEY, self.API_SECRET, self.filter, self.authentication = API_KEY, API_SECRET, filter, False
|
|
||||||
|
|
||||||
self.handler = AuthenticatedChannelsHandler(event_emitter=self.event_emitter)
|
|
||||||
|
|
||||||
self.buckets = [ _BfxWebsocketBucket(self.host, self.event_emitter, self.__bucket_open_signal) for _ in range(buckets) ]
|
|
||||||
|
|
||||||
self.inputs = _BfxWebsocketInputs(self.__handle_websocket_input)
|
|
||||||
|
|
||||||
self.logger = CustomLogger("BfxWebsocketClient", logLevel=log_level)
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -1 +1,3 @@
|
|||||||
from .BfxWebsocketClient import BfxWebsocketClient
|
from .client import BfxWebsocketClient, BfxWebsocketBucket, BfxWebsocketInputs
|
||||||
|
|
||||||
|
NAME = "websocket"
|
||||||
5
bfxapi/websocket/client/__init__.py
Normal file
5
bfxapi/websocket/client/__init__.py
Normal 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"
|
||||||
@@ -2,9 +2,9 @@ import json, uuid, websockets
|
|||||||
|
|
||||||
from typing import Literal, TypeVar, Callable, cast
|
from typing import Literal, TypeVar, Callable, cast
|
||||||
|
|
||||||
from .handlers import PublicChannelsHandler
|
from ..handlers import PublicChannelsHandler
|
||||||
|
|
||||||
from .exceptions import ConnectionNotOpen, TooManySubscriptions, OutdatedClientVersion
|
from ..exceptions import ConnectionNotOpen, TooManySubscriptions, OutdatedClientVersion
|
||||||
|
|
||||||
_HEARTBEAT = "hb"
|
_HEARTBEAT = "hb"
|
||||||
|
|
||||||
@@ -19,32 +19,40 @@ def _require_websocket_connection(function: F) -> F:
|
|||||||
|
|
||||||
return cast(F, wrapper)
|
return cast(F, wrapper)
|
||||||
|
|
||||||
class _BfxWebsocketBucket(object):
|
class BfxWebsocketBucket(object):
|
||||||
VERSION = 2
|
VERSION = 2
|
||||||
|
|
||||||
MAXIMUM_SUBSCRIPTIONS_AMOUNT = 25
|
MAXIMUM_SUBSCRIPTIONS_AMOUNT = 25
|
||||||
|
|
||||||
def __init__(self, host, event_emitter, __bucket_open_signal):
|
def __init__(self, host, event_emitter, on_open_event):
|
||||||
self.host, self.event_emitter, self.__bucket_open_signal = host, event_emitter, __bucket_open_signal
|
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.websocket, self.subscriptions, self.pendings = None, dict(), list()
|
||||||
|
|
||||||
self.handler = PublicChannelsHandler(event_emitter=self.event_emitter)
|
self.handler = PublicChannelsHandler(event_emitter=self.event_emitter)
|
||||||
|
|
||||||
async def _connect(self, index):
|
async def _connect(self, index):
|
||||||
|
reconnection = False
|
||||||
|
|
||||||
async for websocket in websockets.connect(self.host):
|
async for websocket in websockets.connect(self.host):
|
||||||
self.websocket = websocket
|
self.websocket = websocket
|
||||||
|
|
||||||
self.__bucket_open_signal(index)
|
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:
|
try:
|
||||||
async for message in websocket:
|
async for message in websocket:
|
||||||
message = json.loads(message)
|
message = json.loads(message)
|
||||||
|
|
||||||
if isinstance(message, dict) and message["event"] == "info" and "version" in message:
|
if isinstance(message, dict) and message["event"] == "subscribed" and (chanId := message["chanId"]):
|
||||||
if _BfxWebsocketBucket.VERSION != message["version"]:
|
|
||||||
raise OutdatedClientVersion(f"Mismatch between the client version and the server version. Update the library to the latest version to continue (client version: {_BfxWebsocketBucket.VERSION}, server version: {message['version']}).")
|
|
||||||
elif isinstance(message, dict) and message["event"] == "subscribed" and (chanId := message["chanId"]):
|
|
||||||
self.pendings = [ pending for pending in self.pendings if pending["subId"] != message["subId"] ]
|
self.pendings = [ pending for pending in self.pendings if pending["subId"] != message["subId"] ]
|
||||||
self.subscriptions[chanId] = message
|
self.subscriptions[chanId] = message
|
||||||
self.event_emitter.emit("subscribed", message)
|
self.event_emitter.emit("subscribed", message)
|
||||||
@@ -55,20 +63,27 @@ class _BfxWebsocketBucket(object):
|
|||||||
self.event_emitter.emit("wss-error", message["code"], message["msg"])
|
self.event_emitter.emit("wss-error", message["code"], message["msg"])
|
||||||
elif isinstance(message, list) and (chanId := message[0]) and message[1] != _HEARTBEAT:
|
elif isinstance(message, list) and (chanId := message[0]) and message[1] != _HEARTBEAT:
|
||||||
self.handler.handle(self.subscriptions[chanId], *message[1:])
|
self.handler.handle(self.subscriptions[chanId], *message[1:])
|
||||||
except websockets.ConnectionClosedError: continue
|
except websockets.ConnectionClosedError as error:
|
||||||
finally: await self.websocket.wait_closed(); break
|
if error.code == 1006:
|
||||||
|
self.on_open_event.clear()
|
||||||
|
reconnection = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
raise error
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
@_require_websocket_connection
|
@_require_websocket_connection
|
||||||
async def _subscribe(self, channel, subId=None, **kwargs):
|
async def _subscribe(self, channel, subId=None, **kwargs):
|
||||||
if len(self.subscriptions) + len(self.pendings) == _BfxWebsocketBucket.MAXIMUM_SUBSCRIPTIONS_AMOUNT:
|
if len(self.subscriptions) + len(self.pendings) == BfxWebsocketBucket.MAXIMUM_SUBSCRIPTIONS_AMOUNT:
|
||||||
raise TooManySubscriptions("The client has reached the maximum number of subscriptions.")
|
raise TooManySubscriptions("The client has reached the maximum number of subscriptions.")
|
||||||
|
|
||||||
subscription = {
|
subscription = {
|
||||||
|
**kwargs,
|
||||||
|
|
||||||
"event": "subscribe",
|
"event": "subscribe",
|
||||||
"channel": channel,
|
"channel": channel,
|
||||||
"subId": subId or str(uuid.uuid4()),
|
"subId": subId or str(uuid.uuid4()),
|
||||||
|
|
||||||
**kwargs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.pendings.append(subscription)
|
self.pendings.append(subscription)
|
||||||
@@ -84,4 +99,9 @@ class _BfxWebsocketBucket(object):
|
|||||||
|
|
||||||
@_require_websocket_connection
|
@_require_websocket_connection
|
||||||
async def _close(self, code=1000, reason=str()):
|
async def _close(self, code=1000, reason=str()):
|
||||||
await self.websocket.close(code=code, reason=reason)
|
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"]
|
||||||
246
bfxapi/websocket/client/bfx_websocket_client.py
Normal file
246
bfxapi/websocket/client/bfx_websocket_client.py
Normal 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
|
||||||
@@ -2,19 +2,19 @@ from decimal import Decimal
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from typing import Union, Optional, List, Tuple
|
from typing import Union, Optional, List, Tuple
|
||||||
from .types import JSON
|
from .. enums import OrderType, FundingOfferType
|
||||||
from .enums import OrderType, FundingOfferType
|
from ... utils.JSONEncoder import JSON
|
||||||
|
|
||||||
class _BfxWebsocketInputs(object):
|
class BfxWebsocketInputs(object):
|
||||||
def __init__(self, __handle_websocket_input):
|
def __init__(self, handle_websocket_input):
|
||||||
self.__handle_websocket_input = __handle_websocket_input
|
self.handle_websocket_input = handle_websocket_input
|
||||||
|
|
||||||
async def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, float, str],
|
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: 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,
|
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,
|
gid: Optional[int] = None, cid: Optional[int] = None,
|
||||||
flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None):
|
flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None):
|
||||||
await self.__handle_websocket_input("on", {
|
await self.handle_websocket_input("on", {
|
||||||
"type": type, "symbol": symbol, "amount": amount,
|
"type": type, "symbol": symbol, "amount": amount,
|
||||||
"price": price, "lev": lev,
|
"price": price, "lev": lev,
|
||||||
"price_trailing": price_trailing, "price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop,
|
"price_trailing": price_trailing, "price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop,
|
||||||
@@ -26,7 +26,7 @@ class _BfxWebsocketInputs(object):
|
|||||||
cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None,
|
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,
|
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):
|
price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, tif: Optional[Union[datetime, str]] = None):
|
||||||
await self.__handle_websocket_input("ou", {
|
await self.handle_websocket_input("ou", {
|
||||||
"id": id, "amount": amount, "price": price,
|
"id": id, "amount": amount, "price": price,
|
||||||
"cid": cid, "cid_date": cid_date, "gid": gid,
|
"cid": cid, "cid_date": cid_date, "gid": gid,
|
||||||
"flags": flags, "lev": lev, "delta": delta,
|
"flags": flags, "lev": lev, "delta": delta,
|
||||||
@@ -34,12 +34,12 @@ class _BfxWebsocketInputs(object):
|
|||||||
})
|
})
|
||||||
|
|
||||||
async def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None):
|
async def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None):
|
||||||
await self.__handle_websocket_input("oc", {
|
await self.handle_websocket_input("oc", {
|
||||||
"id": id, "cid": cid, "cid_date": cid_date
|
"id": id, "cid": cid, "cid_date": cid_date
|
||||||
})
|
})
|
||||||
|
|
||||||
async def cancel_order_multi(self, ids: Optional[List[int]] = None, cids: Optional[List[Tuple[int, str]]] = None, gids: Optional[List[int]] = None, all: bool = False):
|
async def cancel_order_multi(self, ids: Optional[List[int]] = None, cids: Optional[List[Tuple[int, str]]] = None, gids: Optional[List[int]] = None, all: bool = False):
|
||||||
await self.__handle_websocket_input("oc_multi", {
|
await self.handle_websocket_input("oc_multi", {
|
||||||
"ids": ids, "cids": cids, "gids": gids,
|
"ids": ids, "cids": cids, "gids": gids,
|
||||||
"all": int(all)
|
"all": int(all)
|
||||||
})
|
})
|
||||||
@@ -47,14 +47,14 @@ class _BfxWebsocketInputs(object):
|
|||||||
async def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, float, str],
|
async def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, float, str],
|
||||||
rate: Union[Decimal, float, str], period: int,
|
rate: Union[Decimal, float, str], period: int,
|
||||||
flags: Optional[int] = 0):
|
flags: Optional[int] = 0):
|
||||||
await self.__handle_websocket_input("fon", {
|
await self.handle_websocket_input("fon", {
|
||||||
"type": type, "symbol": symbol, "amount": amount,
|
"type": type, "symbol": symbol, "amount": amount,
|
||||||
"rate": rate, "period": period,
|
"rate": rate, "period": period,
|
||||||
"flags": flags
|
"flags": flags
|
||||||
})
|
})
|
||||||
|
|
||||||
async def cancel_funding_offer(self, id: int):
|
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):
|
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)))
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from ..enums import *
|
from .. enums import *
|
||||||
|
|
||||||
class Channels(str, Enum):
|
class Channel(str, Enum):
|
||||||
TICKER = "ticker"
|
TICKER = "ticker"
|
||||||
TRADES = "trades"
|
TRADES = "trades"
|
||||||
BOOK = "book"
|
BOOK = "book"
|
||||||
|
|||||||
@@ -58,4 +58,11 @@ class InvalidAuthenticationCredentials(BfxWebsocketException):
|
|||||||
This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication.
|
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
|
pass
|
||||||
4
bfxapi/websocket/handlers/__init__.py
Normal file
4
bfxapi/websocket/handlers/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .public_channels_handler import PublicChannelsHandler
|
||||||
|
from .authenticated_channels_handler import AuthenticatedChannelsHandler
|
||||||
|
|
||||||
|
NAME = "handlers"
|
||||||
69
bfxapi/websocket/handlers/authenticated_channels_handler.py
Normal file
69
bfxapi/websocket/handlers/authenticated_channels_handler.py
Normal 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))
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
from typing import List
|
from .. import serializers
|
||||||
|
|
||||||
from .types import *
|
from .. types import *
|
||||||
|
|
||||||
from . import serializers
|
from .. exceptions import HandlerNotFound
|
||||||
from .enums import Channels
|
|
||||||
from .exceptions import BfxWebsocketException
|
|
||||||
|
|
||||||
class PublicChannelsHandler(object):
|
class PublicChannelsHandler(object):
|
||||||
EVENTS = [
|
EVENTS = [
|
||||||
@@ -15,22 +13,25 @@ class PublicChannelsHandler(object):
|
|||||||
"derivatives_status_update",
|
"derivatives_status_update",
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, event_emitter):
|
def __init__(self, event_emitter, strict = True):
|
||||||
self.event_emitter = event_emitter
|
self.event_emitter, self.strict = event_emitter, strict
|
||||||
|
|
||||||
self.__handlers = {
|
self.__handlers = {
|
||||||
Channels.TICKER: self.__ticker_channel_handler,
|
"ticker": self.__ticker_channel_handler,
|
||||||
Channels.TRADES: self.__trades_channel_handler,
|
"trades": self.__trades_channel_handler,
|
||||||
Channels.BOOK: self.__book_channel_handler,
|
"book": self.__book_channel_handler,
|
||||||
Channels.CANDLES: self.__candles_channel_handler,
|
"candles": self.__candles_channel_handler,
|
||||||
Channels.STATUS: self.__status_channel_handler
|
"status": self.__status_channel_handler
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle(self, subscription, *stream):
|
def handle(self, subscription, *stream):
|
||||||
_clear = lambda dictionary, *args: { key: value for key, value in dictionary.items() if key not in args }
|
_clear = lambda dictionary, *args: { key: value for key, value in dictionary.items() if key not in args }
|
||||||
|
|
||||||
if channel := subscription["channel"] or channel in self.__handlers.keys():
|
if (channel := subscription["channel"]) and channel in self.__handlers.keys():
|
||||||
return self.__handlers[channel](_clear(subscription, "event", "channel", "subId"), *stream)
|
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):
|
def __ticker_channel_handler(self, subscription, *stream):
|
||||||
if subscription["symbol"].startswith("t"):
|
if subscription["symbol"].startswith("t"):
|
||||||
@@ -48,7 +49,7 @@ class PublicChannelsHandler(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __trades_channel_handler(self, subscription, *stream):
|
def __trades_channel_handler(self, subscription, *stream):
|
||||||
if type := stream[0] or type in [ "te", "tu", "fte", "ftu" ]:
|
if (type := stream[0]) and type in [ "te", "tu", "fte", "ftu" ]:
|
||||||
if subscription["symbol"].startswith("t"):
|
if subscription["symbol"].startswith("t"):
|
||||||
return self.event_emitter.emit(
|
return self.event_emitter.emit(
|
||||||
{ "te": "t_trade_executed", "tu": "t_trade_execution_update" }[type],
|
{ "te": "t_trade_executed", "tu": "t_trade_execution_update" }[type],
|
||||||
@@ -117,68 +118,4 @@ class PublicChannelsHandler(object):
|
|||||||
"derivatives_status_update",
|
"derivatives_status_update",
|
||||||
subscription,
|
subscription,
|
||||||
serializers.DerivativesStatus.parse(*stream[0])
|
serializers.DerivativesStatus.parse(*stream[0])
|
||||||
)
|
)
|
||||||
|
|
||||||
class AuthenticatedChannelsHandler(object):
|
|
||||||
__abbreviations = {
|
|
||||||
"os": "order_snapshot", "on": "order_new", "ou": "order_update", "oc": "order_cancel",
|
|
||||||
"ps": "position_snapshot", "pn": "position_new", "pu": "position_update", "pc": "position_close",
|
|
||||||
"te": "trade_executed", "tu": "trade_execution_update",
|
|
||||||
"fos": "funding_offer_snapshot", "fon": "funding_offer_new", "fou": "funding_offer_update", "foc": "funding_offer_cancel",
|
|
||||||
"fcs": "funding_credit_snapshot", "fcn": "funding_credit_new", "fcu": "funding_credit_update", "fcc": "funding_credit_close",
|
|
||||||
"fls": "funding_loan_snapshot", "fln": "funding_loan_new", "flu": "funding_loan_update", "flc": "funding_loan_close",
|
|
||||||
"ws": "wallet_snapshot", "wu": "wallet_update",
|
|
||||||
"bu": "balance_update",
|
|
||||||
}
|
|
||||||
|
|
||||||
__serializers = {
|
|
||||||
("os", "on", "ou", "oc",): serializers.Order,
|
|
||||||
("ps", "pn", "pu", "pc",): serializers.Position,
|
|
||||||
("te", "tu"): serializers.Trade,
|
|
||||||
("fos", "fon", "fou", "foc",): serializers.FundingOffer,
|
|
||||||
("fcs", "fcn", "fcu", "fcc",): serializers.FundingCredit,
|
|
||||||
("fls", "fln", "flu", "flc",): serializers.FundingLoan,
|
|
||||||
("ws", "wu",): serializers.Wallet,
|
|
||||||
("bu",): serializers.Balance
|
|
||||||
}
|
|
||||||
|
|
||||||
EVENTS = [
|
|
||||||
"notification",
|
|
||||||
"on-req-notification", "ou-req-notification", "oc-req-notification",
|
|
||||||
"oc_multi-notification",
|
|
||||||
"fon-req-notification", "foc-req-notification",
|
|
||||||
*list(__abbreviations.values())
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, event_emitter, strict = False):
|
|
||||||
self.event_emitter, self.strict = event_emitter, strict
|
|
||||||
|
|
||||||
def handle(self, type, stream):
|
|
||||||
if type == "n":
|
|
||||||
return self.__notification(stream)
|
|
||||||
|
|
||||||
for types, serializer in AuthenticatedChannelsHandler.__serializers.items():
|
|
||||||
if type in types:
|
|
||||||
event = AuthenticatedChannelsHandler.__abbreviations[type]
|
|
||||||
|
|
||||||
if all(isinstance(substream, list) for substream in stream):
|
|
||||||
return self.event_emitter.emit(event, [ serializer.parse(*substream) for substream in stream ])
|
|
||||||
|
|
||||||
return self.event_emitter.emit(event, serializer.parse(*stream))
|
|
||||||
|
|
||||||
if self.strict == True:
|
|
||||||
raise BfxWebsocketException(f"Event of type <{type}> not found in self.__handlers.")
|
|
||||||
|
|
||||||
def __notification(self, stream):
|
|
||||||
type, serializer = "notification", serializers._Notification(serializer=None)
|
|
||||||
|
|
||||||
if stream[1] == "on-req" or stream[1] == "ou-req" or stream[1] == "oc-req":
|
|
||||||
type, serializer = f"{stream[1]}-notification", serializers._Notification[Order](serializer=serializers.Order)
|
|
||||||
|
|
||||||
if stream[1] == "oc_multi-req":
|
|
||||||
type, serializer = f"{stream[1]}-notification", serializers._Notification[List[Order]](serializer=serializers.Order, iterate=True)
|
|
||||||
|
|
||||||
if stream[1] == "fon-req" or stream[1] == "foc-req":
|
|
||||||
type, serializer = f"{stream[1]}-notification", serializers._Notification[FundingOffer](serializer=serializers.FundingOffer)
|
|
||||||
|
|
||||||
return self.event_emitter.emit(type, serializer.parse(*stream))
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
from typing import TypedDict, Optional
|
from typing import TypedDict, Union, Literal, Optional
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"Subscription",
|
||||||
|
|
||||||
"Ticker",
|
"Ticker",
|
||||||
"Trades",
|
"Trades",
|
||||||
"Book",
|
"Book",
|
||||||
@@ -8,18 +10,22 @@ __all__ = [
|
|||||||
"Status"
|
"Status"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
_Header = TypedDict("_Header", { "event": Literal["subscribed"], "channel": str, "chanId": int })
|
||||||
|
|
||||||
|
Subscription = Union["Ticker", "Trades", "Book", "Candles", "Status"]
|
||||||
|
|
||||||
class Ticker(TypedDict):
|
class Ticker(TypedDict):
|
||||||
chanId: int; symbol: str
|
subId: str; symbol: str
|
||||||
pair: Optional[str]
|
pair: Optional[str]
|
||||||
currency: Optional[str]
|
currency: Optional[str]
|
||||||
|
|
||||||
class Trades(TypedDict):
|
class Trades(TypedDict):
|
||||||
chanId: int; symbol: str
|
subId: str; symbol: str
|
||||||
pair: Optional[str]
|
pair: Optional[str]
|
||||||
currency: Optional[str]
|
currency: Optional[str]
|
||||||
|
|
||||||
class Book(TypedDict):
|
class Book(TypedDict):
|
||||||
chanId: int
|
subId: str
|
||||||
symbol: str
|
symbol: str
|
||||||
prec: str
|
prec: str
|
||||||
freq: str
|
freq: str
|
||||||
@@ -27,9 +33,9 @@ class Book(TypedDict):
|
|||||||
pair: str
|
pair: str
|
||||||
|
|
||||||
class Candles(TypedDict):
|
class Candles(TypedDict):
|
||||||
chanId: int
|
subId: str
|
||||||
key: str
|
key: str
|
||||||
|
|
||||||
class Status(TypedDict):
|
class Status(TypedDict):
|
||||||
chanId: int
|
subId: str
|
||||||
key: str
|
key: str
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any
|
from typing import Optional
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from ..labeler import _Type
|
from .. labeler import _Type
|
||||||
from ..notification import Notification
|
from .. notification import Notification
|
||||||
from ..utils.JSONEncoder import JSON
|
from .. utils.JSONEncoder import JSON
|
||||||
|
|
||||||
#region Type hinting for Websocket Public Channels
|
#region Type hinting for Websocket Public Channels
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from bfxapi.client import Client, Constants
|
from bfxapi.client import Client, REST_HOST
|
||||||
|
|
||||||
bfx = Client(
|
bfx = Client(
|
||||||
REST_HOST=Constants.REST_HOST,
|
REST_HOST=REST_HOST,
|
||||||
API_KEY=os.getenv("BFX_API_KEY"),
|
API_KEY=os.getenv("BFX_API_KEY"),
|
||||||
API_SECRET=os.getenv("BFX_API_SECRET")
|
API_SECRET=os.getenv("BFX_API_SECRET")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from bfxapi.client import Client, Constants
|
from bfxapi.client import Client, REST_HOST
|
||||||
from bfxapi.enums import FundingOfferType, Flag
|
from bfxapi.enums import FundingOfferType, Flag
|
||||||
|
|
||||||
bfx = Client(
|
bfx = Client(
|
||||||
REST_HOST=Constants.REST_HOST,
|
REST_HOST=REST_HOST,
|
||||||
API_KEY=os.getenv("BFX_API_KEY"),
|
API_KEY=os.getenv("BFX_API_KEY"),
|
||||||
API_SECRET=os.getenv("BFX_API_SECRET")
|
API_SECRET=os.getenv("BFX_API_SECRET")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# python -c "import examples.rest.create_order"
|
# python -c "import examples.rest.create_order"
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from bfxapi.client import Client, Constants
|
from bfxapi.client import Client, REST_HOST
|
||||||
from bfxapi.enums import OrderType, Flag
|
from bfxapi.enums import OrderType, Flag
|
||||||
|
|
||||||
bfx = Client(
|
bfx = Client(
|
||||||
REST_HOST=Constants.REST_HOST,
|
REST_HOST=REST_HOST,
|
||||||
API_KEY=os.getenv("BFX_API_KEY"),
|
API_KEY=os.getenv("BFX_API_KEY"),
|
||||||
API_SECRET=os.getenv("BFX_API_SECRET")
|
API_SECRET=os.getenv("BFX_API_SECRET")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from bfxapi.client import Client, Constants
|
from bfxapi.client import Client, REST_HOST
|
||||||
|
|
||||||
bfx = Client(
|
bfx = Client(
|
||||||
REST_HOST=Constants.REST_HOST,
|
REST_HOST=REST_HOST,
|
||||||
API_KEY=os.getenv("BFX_API_KEY"),
|
API_KEY=os.getenv("BFX_API_KEY"),
|
||||||
API_SECRET=os.getenv("BFX_API_SECRET")
|
API_SECRET=os.getenv("BFX_API_SECRET")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# python -c "import examples.rest.extra_calcs"
|
# python -c "import examples.rest.extra_calcs"
|
||||||
|
|
||||||
from bfxapi.client import Client, Constants
|
from bfxapi.client import Client, REST_HOST
|
||||||
|
|
||||||
bfx = Client(
|
bfx = Client(
|
||||||
REST_HOST=Constants.REST_HOST
|
REST_HOST=REST_HOST
|
||||||
)
|
)
|
||||||
|
|
||||||
t_symbol_response = bfx.rest.public.get_trading_market_average_price(
|
t_symbol_response = bfx.rest.public.get_trading_market_average_price(
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from bfxapi.client import Client, Constants
|
from bfxapi.client import Client, REST_HOST
|
||||||
|
|
||||||
bfx = Client(
|
bfx = Client(
|
||||||
REST_HOST=Constants.REST_HOST,
|
REST_HOST=REST_HOST,
|
||||||
API_KEY=os.getenv("BFX_API_KEY"),
|
API_KEY=os.getenv("BFX_API_KEY"),
|
||||||
API_SECRET=os.getenv("BFX_API_SECRET")
|
API_SECRET=os.getenv("BFX_API_SECRET")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from bfxapi.client import Client, Constants
|
from bfxapi.client import Client, REST_HOST
|
||||||
|
|
||||||
bfx = Client(
|
bfx = Client(
|
||||||
REST_HOST=Constants.REST_HOST,
|
REST_HOST=REST_HOST,
|
||||||
API_KEY=os.getenv("BFX_API_KEY"),
|
API_KEY=os.getenv("BFX_API_KEY"),
|
||||||
API_SECRET=os.getenv("BFX_API_SECRET")
|
API_SECRET=os.getenv("BFX_API_SECRET")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# python -c "import examples.rest.get_candles_hist"
|
# python -c "import examples.rest.get_candles_hist"
|
||||||
|
|
||||||
from bfxapi.client import Client, Constants
|
from bfxapi.client import Client, REST_HOST
|
||||||
|
|
||||||
bfx = Client(
|
bfx = Client(
|
||||||
REST_HOST=Constants.REST_HOST
|
REST_HOST=REST_HOST
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"Candles: {bfx.rest.public.get_candles_hist(symbol='tBTCUSD')}")
|
print(f"Candles: {bfx.rest.public.get_candles_hist(symbol='tBTCUSD')}")
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from bfxapi.client import Client, Constants
|
from bfxapi.client import Client, REST_HOST
|
||||||
|
|
||||||
bfx = Client(
|
bfx = Client(
|
||||||
REST_HOST=Constants.REST_HOST,
|
REST_HOST=REST_HOST,
|
||||||
API_KEY=os.getenv("BFX_API_KEY"),
|
API_KEY=os.getenv("BFX_API_KEY"),
|
||||||
API_SECRET=os.getenv("BFX_API_SECRET")
|
API_SECRET=os.getenv("BFX_API_SECRET")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from bfxapi.client import Client, Constants
|
from bfxapi.client import Client, REST_HOST
|
||||||
|
|
||||||
bfx = Client(
|
bfx = Client(
|
||||||
REST_HOST=Constants.REST_HOST,
|
REST_HOST=REST_HOST,
|
||||||
API_KEY=os.getenv("BFX_API_KEY"),
|
API_KEY=os.getenv("BFX_API_KEY"),
|
||||||
API_SECRET=os.getenv("BFX_API_SECRET")
|
API_SECRET=os.getenv("BFX_API_SECRET")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from bfxapi.client import Client, Constants
|
from bfxapi.client import Client, REST_HOST
|
||||||
|
|
||||||
bfx = Client(
|
bfx = Client(
|
||||||
REST_HOST=Constants.REST_HOST
|
REST_HOST=REST_HOST
|
||||||
)
|
)
|
||||||
|
|
||||||
now = int(round(time.time() * 1000))
|
now = int(round(time.time() * 1000))
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from bfxapi.client import Client, Constants
|
from bfxapi.client import Client, REST_HOST
|
||||||
|
|
||||||
bfx = Client(
|
bfx = Client(
|
||||||
REST_HOST=Constants.REST_HOST,
|
REST_HOST=REST_HOST,
|
||||||
API_KEY=os.getenv("BFX_API_KEY"),
|
API_KEY=os.getenv("BFX_API_KEY"),
|
||||||
API_SECRET=os.getenv("BFX_API_SECRET")
|
API_SECRET=os.getenv("BFX_API_SECRET")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from bfxapi.client import Client, Constants
|
from bfxapi.client import Client, REST_HOST
|
||||||
|
|
||||||
bfx = Client(
|
bfx = Client(
|
||||||
REST_HOST=Constants.REST_HOST
|
REST_HOST=REST_HOST
|
||||||
)
|
)
|
||||||
|
|
||||||
now = int(round(time.time() * 1000))
|
now = int(round(time.time() * 1000))
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from bfxapi.client import Client, Constants
|
from bfxapi.client import Client, REST_HOST
|
||||||
|
|
||||||
bfx = Client(
|
bfx = Client(
|
||||||
REST_HOST=Constants.REST_HOST
|
REST_HOST=REST_HOST
|
||||||
)
|
)
|
||||||
|
|
||||||
now = int(round(time.time() * 1000))
|
now = int(round(time.time() * 1000))
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from bfxapi.client import Client, Constants
|
from bfxapi.client import Client, REST_HOST
|
||||||
|
|
||||||
bfx = Client(
|
bfx = Client(
|
||||||
REST_HOST=Constants.REST_HOST,
|
REST_HOST=REST_HOST,
|
||||||
API_KEY=os.getenv("BFX_API_KEY"),
|
API_KEY=os.getenv("BFX_API_KEY"),
|
||||||
API_SECRET=os.getenv("BFX_API_SECRET")
|
API_SECRET=os.getenv("BFX_API_SECRET")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from bfxapi.client import Client, Constants
|
from bfxapi.client import Client, REST_HOST
|
||||||
|
|
||||||
bfx = Client(
|
bfx = Client(
|
||||||
REST_HOST=Constants.REST_HOST,
|
REST_HOST=REST_HOST,
|
||||||
API_KEY=os.getenv("BFX_API_KEY"),
|
API_KEY=os.getenv("BFX_API_KEY"),
|
||||||
API_SECRET=os.getenv("BFX_API_SECRET")
|
API_SECRET=os.getenv("BFX_API_SECRET")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from bfxapi.client import Client, Constants
|
from bfxapi.client import Client, REST_HOST
|
||||||
|
|
||||||
bfx = Client(
|
bfx = Client(
|
||||||
REST_HOST=Constants.REST_HOST,
|
REST_HOST=REST_HOST,
|
||||||
API_KEY=os.getenv("BFX_API_KEY"),
|
API_KEY=os.getenv("BFX_API_KEY"),
|
||||||
API_SECRET=os.getenv("BFX_API_SECRET")
|
API_SECRET=os.getenv("BFX_API_SECRET")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from bfxapi.client import Client, Constants
|
from bfxapi.client import Client, REST_HOST
|
||||||
|
|
||||||
bfx = Client(
|
bfx = Client(
|
||||||
REST_HOST=Constants.REST_HOST,
|
REST_HOST=REST_HOST,
|
||||||
API_KEY=os.getenv("BFX_API_KEY"),
|
API_KEY=os.getenv("BFX_API_KEY"),
|
||||||
API_SECRET=os.getenv("BFX_API_SECRET")
|
API_SECRET=os.getenv("BFX_API_SECRET")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from bfxapi.client import Client, Constants
|
from bfxapi.client import Client, REST_HOST
|
||||||
|
|
||||||
bfx = Client(
|
bfx = Client(
|
||||||
REST_HOST=Constants.REST_HOST,
|
REST_HOST=REST_HOST,
|
||||||
API_KEY=os.getenv("BFX_API_KEY"),
|
API_KEY=os.getenv("BFX_API_KEY"),
|
||||||
API_SECRET=os.getenv("BFX_API_SECRET")
|
API_SECRET=os.getenv("BFX_API_SECRET")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
import os
|
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.enums import Error, OrderType
|
||||||
from bfxapi.websocket.types import Notification, Order
|
from bfxapi.websocket.types import Notification, Order
|
||||||
|
|
||||||
bfx = Client(
|
bfx = Client(
|
||||||
WSS_HOST=Constants.WSS_HOST,
|
WSS_HOST=WSS_HOST,
|
||||||
API_KEY=os.getenv("BFX_API_KEY"),
|
API_KEY=os.getenv("BFX_API_KEY"),
|
||||||
API_SECRET=os.getenv("BFX_API_SECRET")
|
API_SECRET=os.getenv("BFX_API_SECRET")
|
||||||
)
|
)
|
||||||
|
|||||||
23
examples/websocket/derivatives_status.py
Normal file
23
examples/websocket/derivatives_status.py
Normal 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()
|
||||||
@@ -4,10 +4,10 @@ from collections import OrderedDict
|
|||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from bfxapi import Client, Constants
|
from bfxapi import Client, PUB_WSS_HOST
|
||||||
|
|
||||||
from bfxapi.websocket import subscriptions
|
from bfxapi.websocket import subscriptions
|
||||||
from bfxapi.websocket.enums import Channels, Error
|
from bfxapi.websocket.enums import Channel, Error
|
||||||
from bfxapi.websocket.types import TradingPairBook
|
from bfxapi.websocket.types import TradingPairBook
|
||||||
|
|
||||||
class OrderBook(object):
|
class OrderBook(object):
|
||||||
@@ -38,7 +38,7 @@ SYMBOLS = [ "tBTCUSD", "tLTCUSD", "tLTCBTC", "tETHUSD", "tETHBTC" ]
|
|||||||
|
|
||||||
order_book = OrderBook(symbols=SYMBOLS)
|
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")
|
@bfx.wss.on("wss-error")
|
||||||
def on_wss_error(code: Error, msg: str):
|
def on_wss_error(code: Error, msg: str):
|
||||||
@@ -47,7 +47,7 @@ def on_wss_error(code: Error, msg: str):
|
|||||||
@bfx.wss.on("open")
|
@bfx.wss.on("open")
|
||||||
async def on_open():
|
async def on_open():
|
||||||
for symbol in SYMBOLS:
|
for symbol in SYMBOLS:
|
||||||
await bfx.wss.subscribe(Channels.BOOK, symbol=symbol)
|
await bfx.wss.subscribe(Channel.BOOK, symbol=symbol)
|
||||||
|
|
||||||
@bfx.wss.on("subscribed")
|
@bfx.wss.on("subscribed")
|
||||||
def on_subscribed(subscription):
|
def on_subscribed(subscription):
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ from collections import OrderedDict
|
|||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from bfxapi import Client, Constants
|
from bfxapi import Client, PUB_WSS_HOST
|
||||||
|
|
||||||
from bfxapi.websocket import subscriptions
|
from bfxapi.websocket import subscriptions
|
||||||
from bfxapi.websocket.enums import Channels, Error
|
from bfxapi.websocket.enums import Channel, Error
|
||||||
from bfxapi.websocket.types import TradingPairRawBook
|
from bfxapi.websocket.types import TradingPairRawBook
|
||||||
|
|
||||||
class RawOrderBook(object):
|
class RawOrderBook(object):
|
||||||
@@ -38,7 +38,7 @@ SYMBOLS = [ "tBTCUSD", "tLTCUSD", "tLTCBTC", "tETHUSD", "tETHBTC" ]
|
|||||||
|
|
||||||
raw_order_book = RawOrderBook(symbols=SYMBOLS)
|
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")
|
@bfx.wss.on("wss-error")
|
||||||
def on_wss_error(code: Error, msg: str):
|
def on_wss_error(code: Error, msg: str):
|
||||||
@@ -47,7 +47,7 @@ def on_wss_error(code: Error, msg: str):
|
|||||||
@bfx.wss.on("open")
|
@bfx.wss.on("open")
|
||||||
async def on_open():
|
async def on_open():
|
||||||
for symbol in SYMBOLS:
|
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")
|
@bfx.wss.on("subscribed")
|
||||||
def on_subscribed(subscription):
|
def on_subscribed(subscription):
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
# python -c "import examples.websocket.ticker"
|
# python -c "import examples.websocket.ticker"
|
||||||
|
|
||||||
from bfxapi import Client, Constants
|
from bfxapi import Client, PUB_WSS_HOST
|
||||||
|
|
||||||
from bfxapi.websocket import subscriptions
|
from bfxapi.websocket import subscriptions
|
||||||
from bfxapi.websocket.enums import Channels
|
from bfxapi.websocket.enums import Channel
|
||||||
from bfxapi.websocket.types import TradingPairTicker
|
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")
|
@bfx.wss.on("t_ticker_update")
|
||||||
def on_t_ticker_update(subscription: subscriptions.Ticker, data: TradingPairTicker):
|
def on_t_ticker_update(subscription: subscriptions.Ticker, data: TradingPairTicker):
|
||||||
print(f"Subscription with channel ID: {subscription['chanId']}")
|
print(f"Subscription with subId: {subscription['subId']}")
|
||||||
|
|
||||||
print(f"Data: {data}")
|
print(f"Data: {data}")
|
||||||
|
|
||||||
@bfx.wss.once("open")
|
@bfx.wss.once("open")
|
||||||
async def open():
|
async def open():
|
||||||
await bfx.wss.subscribe(Channels.TICKER, symbol="tBTCUSD")
|
await bfx.wss.subscribe(Channel.TICKER, symbol="tBTCUSD")
|
||||||
|
|
||||||
bfx.wss.run()
|
bfx.wss.run()
|
||||||
29
examples/websocket/trades.py
Normal file
29
examples/websocket/trades.py
Normal 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()
|
||||||
30
examples/websocket/wallet_balance.py
Normal file
30
examples/websocket/wallet_balance.py
Normal 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()
|
||||||
39
setup.py
39
setup.py
@@ -2,18 +2,36 @@ from distutils.core import setup
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="bitfinex-api-py",
|
name="bitfinex-api-py",
|
||||||
version="3.0.0",
|
version="3.0.0b1",
|
||||||
packages=[
|
description="Official Bitfinex Python API",
|
||||||
"bfxapi", "bfxapi.utils",
|
long_description="A Python reference implementation of the Bitfinex API for both REST and websocket interaction",
|
||||||
"bfxapi.websocket",
|
long_description_content_type="text/markdown",
|
||||||
"bfxapi.rest", "bfxapi.rest.endpoints", "bfxapi.rest.middleware",
|
|
||||||
],
|
|
||||||
url="https://github.com/bitfinexcom/bitfinex-api-py",
|
url="https://github.com/bitfinexcom/bitfinex-api-py",
|
||||||
license="OSI Approved :: Apache Software License",
|
|
||||||
author="Bitfinex",
|
author="Bitfinex",
|
||||||
author_email="support@bitfinex.com",
|
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",
|
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=[
|
install_requires=[
|
||||||
"certifi~=2022.12.7",
|
"certifi~=2022.12.7",
|
||||||
"charset-normalizer~=2.1.1",
|
"charset-normalizer~=2.1.1",
|
||||||
@@ -29,8 +47,5 @@ setup(
|
|||||||
"urllib3~=1.26.13",
|
"urllib3~=1.26.13",
|
||||||
"websockets~=10.4",
|
"websockets~=10.4",
|
||||||
],
|
],
|
||||||
project_urls={
|
python_requires=">=3.8"
|
||||||
"Bug Reports": "https://github.com/bitfinexcom/bitfinex-api-py/issues",
|
|
||||||
"Source": "https://github.com/bitfinexcom/bitfinex-api-py",
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
Reference in New Issue
Block a user