From 8fd261fd19dc55ad7ae1fd168b6338053e6a2571 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 7 Nov 2022 17:20:30 +0100 Subject: [PATCH 001/265] Add some barebone files. --- .gitignore | 3 + LICENSE | 176 ++++++++++++++++++++++++++++++++++++++++++++++ examples/.gitkeep | 0 3 files changed, 179 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 examples/.gitkeep diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bfceb70 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__ +venv +!.gitkeep \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2bb9ad2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/examples/.gitkeep b/examples/.gitkeep new file mode 100644 index 0000000..e69de29 From 048eedcd8ed245adad3eac702261ea471257c6c0 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 7 Nov 2022 18:35:39 +0100 Subject: [PATCH 002/265] Add bfxapi package. Add bfxapi.websocket subpackage. Add requirements.txt file. --- bfxapi/__init__.py | 1 + bfxapi/client.py | 11 ++++ bfxapi/websocket/BfxWebsocketClient.py | 68 +++++++++++++++++++++++++ bfxapi/websocket/__init__.py | 3 ++ bfxapi/websocket/channels.py | 6 +++ requirements.txt | Bin 0 -> 116 bytes 6 files changed, 89 insertions(+) create mode 100644 bfxapi/__init__.py create mode 100644 bfxapi/client.py create mode 100644 bfxapi/websocket/BfxWebsocketClient.py create mode 100644 bfxapi/websocket/__init__.py create mode 100644 bfxapi/websocket/channels.py create mode 100644 requirements.txt diff --git a/bfxapi/__init__.py b/bfxapi/__init__.py new file mode 100644 index 0000000..c11c9ab --- /dev/null +++ b/bfxapi/__init__.py @@ -0,0 +1 @@ +from .client import Client, Constants \ No newline at end of file diff --git a/bfxapi/client.py b/bfxapi/client.py new file mode 100644 index 0000000..7374251 --- /dev/null +++ b/bfxapi/client.py @@ -0,0 +1,11 @@ +from .websocket import BfxWebsocketClient + +from enum import Enum + +class Constants(str, Enum): + WSS_HOST = "wss://api.bitfinex.com/ws/2" + PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2" + +class Client(object): + def __init__(self, WSS_HOST: str = Constants.WSS_HOST): + self.wss = BfxWebsocketClient(host=WSS_HOST) \ No newline at end of file diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py new file mode 100644 index 0000000..587576d --- /dev/null +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -0,0 +1,68 @@ +import json, asyncio, websockets + +from pyee.asyncio import AsyncIOEventEmitter + +from .channels import Channels + +class BfxWebsocketClient(object): + def __init__(self, host, channels=None): + self.host = host + + self.chanIds, self.event_emitter = dict(), AsyncIOEventEmitter() + + self.channels = channels or list() + + def run_forever(self): + asyncio.run(self.connect()) + + async def connect(self): + async for websocket in websockets.connect(self.host): + try: + self.websocket = websocket + + for channel, parameters in self.channels: + await self.subscribe(channel, **parameters) + else: self.event_emitter.emit("open") + + async for message in websocket: + message = json.loads(message) + + if isinstance(message, dict) and message["event"] == "subscribed": + self.chanIds[message["chanId"]] = message + + self.event_emitter.emit("subscribed", message) + + if isinstance(message, list): + chanId, parameters = message[0], message[1:] + + subscription = self.chanIds[chanId] + + if subscription["channel"] == Channels.TICKER: + self.event_emitter.emit("ticker", subscription, parameters[0]) + + if subscription["channel"] == Channels.TRADES: + if len(parameters) == 1: + self.event_emitter.emit("trades_snapshot", subscription, parameters[0]) + + if len(parameters) == 2: + self.event_emitter.emit("trades_update", subscription, parameters[0], parameters[1]) + + if subscription["channel"] == Channels.BOOK: + if all(isinstance(element, list) for element in parameters[0]): + self.event_emitter.emit("book_snapshot", subscription, parameters[0]) + else: self.event_emitter.emit("book_update", subscription, parameters[0]) + except websockets.ConnectionClosed: + continue + + async def subscribe(self, channel, **kwargs): + await self.websocket.send(json.dumps({ + "event": "subscribe", + "channel": channel, + **kwargs + })) + + def on(self, event): + def handler(function): + self.event_emitter.on(event, function) + + return handler diff --git a/bfxapi/websocket/__init__.py b/bfxapi/websocket/__init__.py new file mode 100644 index 0000000..f33aa63 --- /dev/null +++ b/bfxapi/websocket/__init__.py @@ -0,0 +1,3 @@ +from .BfxWebsocketClient import BfxWebsocketClient + +from .channels import Channels \ No newline at end of file diff --git a/bfxapi/websocket/channels.py b/bfxapi/websocket/channels.py new file mode 100644 index 0000000..5db673c --- /dev/null +++ b/bfxapi/websocket/channels.py @@ -0,0 +1,6 @@ +from enum import Enum + +class Channels(str, Enum): + TICKER = "ticker" + TRADES = "trades" + BOOK = "book" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..78560fa31112931f95f0af059daf152fa323d63f GIT binary patch literal 116 zcmezWuYjSFAr%O1fzXmckHG+lO&E9?xEM-+G6f8o40#Oc4Dmn}6+n?xAg>rGmJg;u hdQ8CTLHa;?%7H49fGYACk{Pmr>=Gawq{ Date: Tue, 8 Nov 2022 17:04:22 +0100 Subject: [PATCH 003/265] Add bfxapi/websocket/manager.py file. Separate channel handlers from BfxWebsocketClient.py to manager.py. Fix bug in subscribe method. --- bfxapi/websocket/BfxWebsocketClient.py | 30 +++++++++----------------- bfxapi/websocket/manager.py | 29 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 20 deletions(-) create mode 100644 bfxapi/websocket/manager.py diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 587576d..5d5d033 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -2,13 +2,15 @@ import json, asyncio, websockets from pyee.asyncio import AsyncIOEventEmitter +from .manager import Manager + from .channels import Channels class BfxWebsocketClient(object): def __init__(self, host, channels=None): - self.host = host + self.host, self.chanIds, self.event_emitter = host, dict(), AsyncIOEventEmitter() - self.chanIds, self.event_emitter = dict(), AsyncIOEventEmitter() + self.manager, self.websocket = Manager(event_emitter=self.event_emitter), None self.channels = channels or list() @@ -28,33 +30,21 @@ class BfxWebsocketClient(object): message = json.loads(message) if isinstance(message, dict) and message["event"] == "subscribed": + del message["event"] self.chanIds[message["chanId"]] = message - self.event_emitter.emit("subscribed", message) if isinstance(message, list): chanId, parameters = message[0], message[1:] - subscription = self.chanIds[chanId] - - if subscription["channel"] == Channels.TICKER: - self.event_emitter.emit("ticker", subscription, parameters[0]) - - if subscription["channel"] == Channels.TRADES: - if len(parameters) == 1: - self.event_emitter.emit("trades_snapshot", subscription, parameters[0]) - - if len(parameters) == 2: - self.event_emitter.emit("trades_update", subscription, parameters[0], parameters[1]) - - if subscription["channel"] == Channels.BOOK: - if all(isinstance(element, list) for element in parameters[0]): - self.event_emitter.emit("book_snapshot", subscription, parameters[0]) - else: self.event_emitter.emit("book_update", subscription, parameters[0]) + self.manager.handle(subscription, *parameters) except websockets.ConnectionClosed: continue async def subscribe(self, channel, **kwargs): + if self.websocket == None: + return self.channels.append((channel, kwargs)) + await self.websocket.send(json.dumps({ "event": "subscribe", "channel": channel, @@ -65,4 +55,4 @@ class BfxWebsocketClient(object): def handler(function): self.event_emitter.on(event, function) - return handler + return handler \ No newline at end of file diff --git a/bfxapi/websocket/manager.py b/bfxapi/websocket/manager.py new file mode 100644 index 0000000..3e3e9cd --- /dev/null +++ b/bfxapi/websocket/manager.py @@ -0,0 +1,29 @@ +from .channels import Channels + +class Manager(object): + def __init__(self, event_emitter): + self.event_emitter = event_emitter + + self.__handlers = { + Channels.TICKER: self.__ticker_channel_handler, + Channels.TRADES: self.__trades_channel_handler, + Channels.BOOK: self.__book_channel_handler + } + + def handle(self, subscription, *parameters): + return self.__handlers[subscription["channel"]](subscription, *parameters) + + def __ticker_channel_handler(self, subscription, *parameters): + self.event_emitter.emit("ticker", subscription, parameters[0]) + + def __trades_channel_handler(self, subscription, *parameters): + if len(parameters) == 1: + self.event_emitter.emit("trades_snapshot", subscription, parameters[0]) + + if len(parameters) == 2: + self.event_emitter.emit("trades_update", subscription, parameters[0], parameters[1]) + + def __book_channel_handler(self, subscription, *parameters): + if all(isinstance(element, list) for element in parameters[0]): + self.event_emitter.emit("book_snapshot", subscription, parameters[0]) + else: self.event_emitter.emit("book_update", subscription, parameters[0]) \ No newline at end of file From a37b7dda339dadd4674bdfdc5b18ec7d2b150b61 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 8 Nov 2022 17:46:45 +0100 Subject: [PATCH 004/265] Add bfxapi/websocket/errors.py script. Add __require_websocket_connection decorator inside BfxWebsocketClient class. Implement unsubscribe and clear methods. --- bfxapi/websocket/BfxWebsocketClient.py | 35 ++++++++++++++++++++------ bfxapi/websocket/__init__.py | 4 +-- bfxapi/websocket/errors.py | 7 ++++++ 3 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 bfxapi/websocket/errors.py diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 5d5d033..dad9ec1 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -3,8 +3,8 @@ import json, asyncio, websockets from pyee.asyncio import AsyncIOEventEmitter from .manager import Manager - from .channels import Channels +from .errors import ConnectionNotOpen class BfxWebsocketClient(object): def __init__(self, host, channels=None): @@ -34,23 +34,44 @@ class BfxWebsocketClient(object): self.chanIds[message["chanId"]] = message self.event_emitter.emit("subscribed", message) - if isinstance(message, list): + elif isinstance(message, dict) and message["event"] == "unsubscribed": + if message["status"] == "OK": + del self.chanIds[message["chanId"]] + + elif isinstance(message, list): chanId, parameters = message[0], message[1:] - subscription = self.chanIds[chanId] - self.manager.handle(subscription, *parameters) + self.manager.handle(self.chanIds[chanId], *parameters) except websockets.ConnectionClosed: continue - async def subscribe(self, channel, **kwargs): - if self.websocket == None: - return self.channels.append((channel, kwargs)) + def __require_websocket_connection(function): + async def wrapper(self, *args, **kwargs): + if self.websocket == None or self.websocket.open == False: + raise ConnectionNotOpen("No open connection with the server.") + + await function(self, *args, **kwargs) + return wrapper + + @__require_websocket_connection + async def subscribe(self, channel, **kwargs): await self.websocket.send(json.dumps({ "event": "subscribe", "channel": channel, **kwargs })) + @__require_websocket_connection + async def unsubscribe(self, chanId): + await self.websocket.send(json.dumps({ + "event": "unsubscribe", + "chanId": chanId + })) + + async def clear(self): + for chanId in self.chanIds.keys(): + await self.unsubscribe(chanId) + def on(self, event): def handler(function): self.event_emitter.on(event, function) diff --git a/bfxapi/websocket/__init__.py b/bfxapi/websocket/__init__.py index f33aa63..ed83944 100644 --- a/bfxapi/websocket/__init__.py +++ b/bfxapi/websocket/__init__.py @@ -1,3 +1,3 @@ from .BfxWebsocketClient import BfxWebsocketClient - -from .channels import Channels \ No newline at end of file +from .channels import Channels +from .errors import ConnectionNotOpen \ No newline at end of file diff --git a/bfxapi/websocket/errors.py b/bfxapi/websocket/errors.py new file mode 100644 index 0000000..d049ac7 --- /dev/null +++ b/bfxapi/websocket/errors.py @@ -0,0 +1,7 @@ +class ConnectionNotOpen(Exception): + """ + This error indicates an attempt to communicate via websocket before starting the connection with the servers. + + """ + + pass \ No newline at end of file From 7e416219e08424de7ae3a5a5723def008b2ccfa9 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 8 Nov 2022 18:16:24 +0100 Subject: [PATCH 005/265] Add handlers in manager.py for CANDLES and STATUS channels. Add support for heartbeat data. --- bfxapi/websocket/channels.py | 4 +++- bfxapi/websocket/manager.py | 19 ++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/bfxapi/websocket/channels.py b/bfxapi/websocket/channels.py index 5db673c..45b3d8e 100644 --- a/bfxapi/websocket/channels.py +++ b/bfxapi/websocket/channels.py @@ -3,4 +3,6 @@ from enum import Enum class Channels(str, Enum): TICKER = "ticker" TRADES = "trades" - BOOK = "book" \ No newline at end of file + BOOK = "book" + CANDLES = "candles" + STATUS = "status" \ No newline at end of file diff --git a/bfxapi/websocket/manager.py b/bfxapi/websocket/manager.py index 3e3e9cd..6b57549 100644 --- a/bfxapi/websocket/manager.py +++ b/bfxapi/websocket/manager.py @@ -1,5 +1,7 @@ from .channels import Channels +HEARTBEAT = "hb" + class Manager(object): def __init__(self, event_emitter): self.event_emitter = event_emitter @@ -7,11 +9,14 @@ class Manager(object): self.__handlers = { Channels.TICKER: self.__ticker_channel_handler, Channels.TRADES: self.__trades_channel_handler, - Channels.BOOK: self.__book_channel_handler + Channels.BOOK: self.__book_channel_handler, + Channels.CANDLES: self.__candles_channel_handler, + Channels.STATUS: self.__status_channel_handler, } def handle(self, subscription, *parameters): - return self.__handlers[subscription["channel"]](subscription, *parameters) + if parameters[0] != HEARTBEAT: + self.__handlers[subscription["channel"]](subscription, *parameters) def __ticker_channel_handler(self, subscription, *parameters): self.event_emitter.emit("ticker", subscription, parameters[0]) @@ -26,4 +31,12 @@ class Manager(object): def __book_channel_handler(self, subscription, *parameters): if all(isinstance(element, list) for element in parameters[0]): self.event_emitter.emit("book_snapshot", subscription, parameters[0]) - else: self.event_emitter.emit("book_update", subscription, parameters[0]) \ No newline at end of file + else: self.event_emitter.emit("book_update", subscription, parameters[0]) + + def __candles_channel_handler(self, subscription, *parameters): + if all(isinstance(element, list) for element in parameters[0]): + self.event_emitter.emit("candles_snapshot", subscription, parameters[0]) + else: self.event_emitter.emit("candles_update", subscription, parameters[0]) + + def __status_channel_handler(self, subscription, *parameters): + self.event_emitter.emit("status", subscription, parameters[0]) From 1fc9aacd86df7138906679ca3535cb3c5b9d7d9f Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 9 Nov 2022 18:26:30 +0100 Subject: [PATCH 006/265] Add authenticate method in BfxWebsocketClient. Add API_KEY and API_SECRET to Client's constructor parameters. Add new AuthenticationCredentialsError exception. --- bfxapi/client.py | 4 +- bfxapi/websocket/BfxWebsocketClient.py | 52 +++++++++++++++++++++----- bfxapi/websocket/errors.py | 8 +++- 3 files changed, 51 insertions(+), 13 deletions(-) diff --git a/bfxapi/client.py b/bfxapi/client.py index 7374251..cefd5ce 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -7,5 +7,5 @@ class Constants(str, Enum): PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2" class Client(object): - def __init__(self, WSS_HOST: str = Constants.WSS_HOST): - self.wss = BfxWebsocketClient(host=WSS_HOST) \ No newline at end of file + def __init__(self, WSS_HOST: str = Constants.WSS_HOST, API_KEY: str = None, API_SECRET: str = None): + self.wss = BfxWebsocketClient(host=WSS_HOST, API_KEY=API_KEY, API_SECRET=API_SECRET) \ No newline at end of file diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index dad9ec1..3b8ee70 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -1,30 +1,31 @@ -import json, asyncio, websockets +import json, asyncio, hmac, hashlib, time, websockets from pyee.asyncio import AsyncIOEventEmitter from .manager import Manager -from .channels import Channels -from .errors import ConnectionNotOpen + +from .errors import ConnectionNotOpen, AuthenticationCredentialsError class BfxWebsocketClient(object): - def __init__(self, host, channels=None): + def __init__(self, host, API_KEY=None, API_SECRET=None): self.host, self.chanIds, self.event_emitter = host, dict(), AsyncIOEventEmitter() self.manager, self.websocket = Manager(event_emitter=self.event_emitter), None - self.channels = channels or list() + self.API_KEY, self.API_SECRET = API_KEY, API_SECRET def run_forever(self): asyncio.run(self.connect()) async def connect(self): async for websocket in websockets.connect(self.host): - try: - self.websocket = websocket + self.websocket = websocket - for channel, parameters in self.channels: - await self.subscribe(channel, **parameters) - else: self.event_emitter.emit("open") + try: + self.event_emitter.emit("open") + + if self.API_KEY != None and self.API_SECRET != None: + self.authenticate(self.API_KEY, self.API_SECRET) async for message in websocket: message = json.loads(message) @@ -38,12 +39,21 @@ class BfxWebsocketClient(object): if message["status"] == "OK": del self.chanIds[message["chanId"]] + elif isinstance(message, dict) and message["event"] == "auth": + if message["status"] == "OK": + self.chanIds[message["chanId"]] = message + + self.event_emitter.emit("authenticated", message) + else: raise AuthenticationCredentialsError("Cannot authenticate with given API-KEY and API-SECRET.") + elif isinstance(message, list): chanId, parameters = message[0], message[1:] + self.manager.handle(self.chanIds[chanId], *parameters) except websockets.ConnectionClosed: continue + @staticmethod def __require_websocket_connection(function): async def wrapper(self, *args, **kwargs): if self.websocket == None or self.websocket.open == False: @@ -68,6 +78,22 @@ class BfxWebsocketClient(object): "chanId": chanId })) + @__require_websocket_connection + 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 clear(self): for chanId in self.chanIds.keys(): await self.unsubscribe(chanId) @@ -76,4 +102,10 @@ class BfxWebsocketClient(object): def handler(function): self.event_emitter.on(event, function) + return handler + + def once(self, event): + def handler(function): + self.event_emitter.once(event, function) + return handler \ No newline at end of file diff --git a/bfxapi/websocket/errors.py b/bfxapi/websocket/errors.py index d049ac7..693dbe3 100644 --- a/bfxapi/websocket/errors.py +++ b/bfxapi/websocket/errors.py @@ -1,7 +1,13 @@ class ConnectionNotOpen(Exception): """ This error indicates an attempt to communicate via websocket before starting the connection with the servers. - + """ + + pass + +class AuthenticationCredentialsError(Exception): + """ + This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication. """ pass \ No newline at end of file From 50644e811643c8767ab9f211bffa8e716c5a2bac Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 9 Nov 2022 19:13:48 +0100 Subject: [PATCH 007/265] Rename manager.py to handlers.py. Add code to BfxWebsocketClient.py to handle authenticated channel. Update bfxapi/websocket/__init__.py imports. --- bfxapi/websocket/BfxWebsocketClient.py | 36 ++++++++++---------- bfxapi/websocket/__init__.py | 4 +-- bfxapi/websocket/channels.py | 8 ----- bfxapi/websocket/{manager.py => handlers.py} | 28 ++++++++++++--- 4 files changed, 43 insertions(+), 33 deletions(-) delete mode 100644 bfxapi/websocket/channels.py rename bfxapi/websocket/{manager.py => handlers.py} (71%) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 3b8ee70..1ac041e 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -2,20 +2,22 @@ import json, asyncio, hmac, hashlib, time, websockets from pyee.asyncio import AsyncIOEventEmitter -from .manager import Manager +from .handlers import Channels, PublicChannelsHandler, AuthenticatedEventsHandler from .errors import ConnectionNotOpen, AuthenticationCredentialsError +HEARTBEAT = "hb" + class BfxWebsocketClient(object): def __init__(self, host, API_KEY=None, API_SECRET=None): self.host, self.chanIds, self.event_emitter = host, dict(), AsyncIOEventEmitter() - self.manager, self.websocket = Manager(event_emitter=self.event_emitter), None + self.websocket, self.API_KEY, self.API_SECRET = None, API_KEY, API_SECRET - self.API_KEY, self.API_SECRET = API_KEY, API_SECRET - - def run_forever(self): - asyncio.run(self.connect()) + self.handlers = { + "public": PublicChannelsHandler(event_emitter=self.event_emitter), + "authenticated": AuthenticatedEventsHandler(event_emitter=self.event_emitter) + } async def connect(self): async for websocket in websockets.connect(self.host): @@ -25,31 +27,26 @@ class BfxWebsocketClient(object): self.event_emitter.emit("open") if self.API_KEY != None and self.API_SECRET != None: - self.authenticate(self.API_KEY, self.API_SECRET) + await self.authenticate(self.API_KEY, self.API_SECRET) async for message in websocket: message = json.loads(message) if isinstance(message, dict) and message["event"] == "subscribed": - del message["event"] self.chanIds[message["chanId"]] = message - self.event_emitter.emit("subscribed", message) + self.event_emitter.emit("subscribed", message) elif isinstance(message, dict) and message["event"] == "unsubscribed": if message["status"] == "OK": del self.chanIds[message["chanId"]] - elif isinstance(message, dict) and message["event"] == "auth": if message["status"] == "OK": - self.chanIds[message["chanId"]] = message - self.event_emitter.emit("authenticated", message) else: raise AuthenticationCredentialsError("Cannot authenticate with given API-KEY and API-SECRET.") - - elif isinstance(message, list): - chanId, parameters = message[0], message[1:] - - self.manager.handle(self.chanIds[chanId], *parameters) + elif isinstance(message, list) and (chanId := message[0]) and message[1] != HEARTBEAT: + if chanId == 0: + self.handlers["authenticated"].handle(message[1], *message[2:]) + else: self.handlers["public"].handle(self.chanIds[chanId], *message[1:]) except websockets.ConnectionClosed: continue @@ -108,4 +105,7 @@ class BfxWebsocketClient(object): def handler(function): self.event_emitter.once(event, function) - return handler \ No newline at end of file + return handler + + def run(self): + asyncio.run(self.connect()) \ No newline at end of file diff --git a/bfxapi/websocket/__init__.py b/bfxapi/websocket/__init__.py index ed83944..e18ac12 100644 --- a/bfxapi/websocket/__init__.py +++ b/bfxapi/websocket/__init__.py @@ -1,3 +1,3 @@ from .BfxWebsocketClient import BfxWebsocketClient -from .channels import Channels -from .errors import ConnectionNotOpen \ No newline at end of file +from .handlers import Channels +from .errors import ConnectionNotOpen, AuthenticationCredentialsError \ No newline at end of file diff --git a/bfxapi/websocket/channels.py b/bfxapi/websocket/channels.py deleted file mode 100644 index 45b3d8e..0000000 --- a/bfxapi/websocket/channels.py +++ /dev/null @@ -1,8 +0,0 @@ -from enum import Enum - -class Channels(str, Enum): - TICKER = "ticker" - TRADES = "trades" - BOOK = "book" - CANDLES = "candles" - STATUS = "status" \ No newline at end of file diff --git a/bfxapi/websocket/manager.py b/bfxapi/websocket/handlers.py similarity index 71% rename from bfxapi/websocket/manager.py rename to bfxapi/websocket/handlers.py index 6b57549..450b277 100644 --- a/bfxapi/websocket/manager.py +++ b/bfxapi/websocket/handlers.py @@ -1,8 +1,13 @@ -from .channels import Channels +from enum import Enum -HEARTBEAT = "hb" +class Channels(str, Enum): + TICKER = "ticker" + TRADES = "trades" + BOOK = "book" + CANDLES = "candles" + STATUS = "status" -class Manager(object): +class PublicChannelsHandler(object): def __init__(self, event_emitter): self.event_emitter = event_emitter @@ -15,8 +20,7 @@ class Manager(object): } def handle(self, subscription, *parameters): - if parameters[0] != HEARTBEAT: - self.__handlers[subscription["channel"]](subscription, *parameters) + self.__handlers[subscription["channel"]](subscription, *parameters) def __ticker_channel_handler(self, subscription, *parameters): self.event_emitter.emit("ticker", subscription, parameters[0]) @@ -40,3 +44,17 @@ class Manager(object): def __status_channel_handler(self, subscription, *parameters): self.event_emitter.emit("status", subscription, parameters[0]) + +class AuthenticatedEventsHandler(object): + def __init__(self, event_emitter): + self.event_emitter = event_emitter + + self.__handlers = { + "bu": self.__bu_event_handler + } + + def handle(self, type, *parameters): + self.__handlers[type](*parameters) + + def __bu_event_handler(self, AUM, AUM_NET): + self.event_emitter.emit("balance_update", AUM, AUM_NET) \ No newline at end of file From 61b082db2e5ade6cb332e93e908eb2c1b2b74a0c Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 10 Nov 2022 11:57:57 +0100 Subject: [PATCH 008/265] Rewrite AuthenticatedEventsHandler with _label_array_elements logic. Add support to multiple new events. Fix bug in BfxWebsocketClient.py. --- bfxapi/websocket/BfxWebsocketClient.py | 4 +- bfxapi/websocket/handlers.py | 139 ++++++++++++++++++++++++- 2 files changed, 136 insertions(+), 7 deletions(-) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 1ac041e..49c9a82 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -43,9 +43,9 @@ class BfxWebsocketClient(object): if message["status"] == "OK": self.event_emitter.emit("authenticated", message) else: raise AuthenticationCredentialsError("Cannot authenticate with given API-KEY and API-SECRET.") - elif isinstance(message, list) and (chanId := message[0]) and message[1] != HEARTBEAT: + elif isinstance(message, list) and ((chanId := message[0]) or True) and message[1] != HEARTBEAT: if chanId == 0: - self.handlers["authenticated"].handle(message[1], *message[2:]) + self.handlers["authenticated"].handle(message[1], message[2]) else: self.handlers["public"].handle(self.chanIds[chanId], *message[1:]) except websockets.ConnectionClosed: continue diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index 450b277..7704bc7 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -50,11 +50,140 @@ class AuthenticatedEventsHandler(object): self.event_emitter = event_emitter self.__handlers = { - "bu": self.__bu_event_handler + "bu": self.__bu_event_handler, + "ws": self.__ws_event_handler, + "wu": self.__wu_event_handler, + "os": self.__os_event_handler, + "on": self.__on_event_handler } - def handle(self, type, *parameters): - self.__handlers[type](*parameters) + def handle(self, type, parameters): + if type in self.__handlers: + self.__handlers[type](*parameters) - def __bu_event_handler(self, AUM, AUM_NET): - self.event_emitter.emit("balance_update", AUM, AUM_NET) \ No newline at end of file + def __bu_event_handler(self, *parameters): + self.event_emitter.emit("balance_update", _label_array_elements( + [ + "AUM", + "AUM_NET" + ], + *parameters + )) + + def __ws_event_handler(self, *parameters): + self.event_emitter.emit("wallet_snapshot", [ + _label_array_elements( + [ + "WALLET_TYPE", + "CURRENCY", + "BALANCE", + "UNSETTLED_INTEREST", + "BALANCE_AVAILABLE", + "DESCRIPTION", + "META" + ], + *parameter + ) for parameter in parameters + ]) + + def __wu_event_handler(self, *parameters): + self.event_emitter.emit("wallet_update", _label_array_elements( + [ + "WALLET_TYPE", + "CURRENCY", + "BALANCE", + "UNSETTLED_INTEREST", + "BALANCE_AVAILABLE", + "DESCRIPTION", + "META" + ], + *parameters + )) + + def __os_event_handler(self, *parameters): + self.event_emitter.emit("order_snapshot", [ + _label_array_elements( + [ + "ID", + "GID", + "CID", + "SYMBOL", + "MTS_CREATE", + "MTS_UPDATE", + "AMOUNT", + "AMOUNT_ORIG", + "ORDER_TYPE", + "TYPE_PREV", + "MTS_TIF", + "_PLACEHOLDER", + "FLAGS", + "STATUS", + "_PLACEHOLDER", + "_PLACEHOLDER", + "PRICE", + "PRICE_AVG", + "PRICE_TRAILING", + "PRICE_AUX_LIMIT", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "NOTIFY", + "HIDDEN", + "PLACED_ID", + "_PLACEHOLDER", + "_PLACEHOLDER", + "ROUTING", + "_PLACEHOLDER", + "_PLACEHOLDER", + "META" + ], + *parameter + ) for parameter in parameters + ]) + + def __on_event_handler(self, *parameters): + self.event_emitter.emit("new_order", _label_array_elements( + [ + "ID", + "GID", + "CID", + "SYMBOL", + "MTS_CREATE", + "MTS_UPDATE", + "AMOUNT", + "AMOUNT_ORIG", + "ORDER_TYPE", + "TYPE_PREV", + "MTS_TIF", + "_PLACEHOLDER", + "FLAGS", + "ORDER_STATUS", + "_PLACEHOLDER", + "_PLACEHOLDER", + "PRICE", + "PRICE_AVG", + "PRICE_TRAILING", + "PRICE_AUX_LIMIT", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "NOTIFY", + "HIDDEN", + "PLACED_ID", + "_PLACEHOLDER", + "_PLACEHOLDER", + "ROUTING", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER" + ], + *parameters + )) + +def _label_array_elements(labels, *args): + if len(labels) != len(args): + raise Exception(" and <*args> arguments should contain the same amount of elements.") + + _PLACEHOLDER = "_PLACEHOLDER" + + return { label: args[index] for index, label in enumerate(labels) if label != _PLACEHOLDER } \ No newline at end of file From 54542ac23b3449cc4b3ee3016b920be8e8a1df21 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 10 Nov 2022 12:24:50 +0100 Subject: [PATCH 009/265] Add bfxapi/websocket/typing.py script and define some custom types. --- bfxapi/websocket/typings.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 bfxapi/websocket/typings.py diff --git a/bfxapi/websocket/typings.py b/bfxapi/websocket/typings.py new file mode 100644 index 0000000..f1e39ec --- /dev/null +++ b/bfxapi/websocket/typings.py @@ -0,0 +1,16 @@ +from typing import TypedDict, Optional + +class BalanceUpdateStream(TypedDict): + AUM: float + AUM_NET: float + +class WalletUpdateStream(TypedDict): + WALLET_TYPE: str + CURRENCY: str + BALANCE: float + UNSETTLED_INTEREST: float + BALANCE_AVAILABLE: Optional[float] + DESCRIPTION: str + META: dict + +WalletSnapshotStream = list[WalletUpdateStream] \ No newline at end of file From 2c70d299b3d517574006c817c21b29f2ad6eccbe Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 10 Nov 2022 12:27:35 +0100 Subject: [PATCH 010/265] Fix small bug in bfxapi/websocket/typings.py file. --- bfxapi/websocket/typings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bfxapi/websocket/typings.py b/bfxapi/websocket/typings.py index f1e39ec..f49c22c 100644 --- a/bfxapi/websocket/typings.py +++ b/bfxapi/websocket/typings.py @@ -1,4 +1,4 @@ -from typing import TypedDict, Optional +from typing import TypedDict, List, Optional class BalanceUpdateStream(TypedDict): AUM: float @@ -13,4 +13,4 @@ class WalletUpdateStream(TypedDict): DESCRIPTION: str META: dict -WalletSnapshotStream = list[WalletUpdateStream] \ No newline at end of file +WalletSnapshotStream = List[WalletUpdateStream] \ No newline at end of file From a03a82d57a6dfdc446574c3ce849dfc305274793 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 11 Nov 2022 18:54:38 +0100 Subject: [PATCH 011/265] Add support for various new authenticated channels. Add new typings in bfxapi/websocket/typings.py. Add BfxWebsocketException in bfxapi/websocket/errors.py. --- bfxapi/websocket/BfxWebsocketClient.py | 4 +- bfxapi/websocket/__init__.py | 2 +- bfxapi/websocket/errors.py | 17 +- bfxapi/websocket/handlers.py | 370 +++++++++++++++++++++++-- bfxapi/websocket/typings.py | 135 ++++++++- 5 files changed, 483 insertions(+), 45 deletions(-) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 49c9a82..9ba6b45 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -4,7 +4,7 @@ from pyee.asyncio import AsyncIOEventEmitter from .handlers import Channels, PublicChannelsHandler, AuthenticatedEventsHandler -from .errors import ConnectionNotOpen, AuthenticationCredentialsError +from .errors import BfxWebsocketException, ConnectionNotOpen, InvalidAuthenticationCredentials HEARTBEAT = "hb" @@ -42,7 +42,7 @@ class BfxWebsocketClient(object): elif isinstance(message, dict) and message["event"] == "auth": if message["status"] == "OK": self.event_emitter.emit("authenticated", message) - else: raise AuthenticationCredentialsError("Cannot authenticate with given API-KEY and API-SECRET.") + else: raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.") elif isinstance(message, list) and ((chanId := message[0]) or True) and message[1] != HEARTBEAT: if chanId == 0: self.handlers["authenticated"].handle(message[1], message[2]) diff --git a/bfxapi/websocket/__init__.py b/bfxapi/websocket/__init__.py index e18ac12..c3aee02 100644 --- a/bfxapi/websocket/__init__.py +++ b/bfxapi/websocket/__init__.py @@ -1,3 +1,3 @@ from .BfxWebsocketClient import BfxWebsocketClient from .handlers import Channels -from .errors import ConnectionNotOpen, AuthenticationCredentialsError \ No newline at end of file +from .errors import BfxWebsocketException, ConnectionNotOpen, InvalidAuthenticationCredentials \ No newline at end of file diff --git a/bfxapi/websocket/errors.py b/bfxapi/websocket/errors.py index 693dbe3..d8f4b03 100644 --- a/bfxapi/websocket/errors.py +++ b/bfxapi/websocket/errors.py @@ -1,11 +1,24 @@ -class ConnectionNotOpen(Exception): +__all__ = [ + "BfxWebsocketException", + "ConnectionNotOpen", + "InvalidAuthenticationCredentials" +] + +class BfxWebsocketException(Exception): + """ + Base class for all exceptions defined in bfx/websocket/errors.py. + """ + + pass + +class ConnectionNotOpen(BfxWebsocketException): """ This error indicates an attempt to communicate via websocket before starting the connection with the servers. """ pass -class AuthenticationCredentialsError(Exception): +class InvalidAuthenticationCredentials(BfxWebsocketException): """ This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication. """ diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index 7704bc7..a601aa6 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -1,5 +1,7 @@ from enum import Enum +from .errors import BfxWebsocketException + class Channels(str, Enum): TICKER = "ticker" TRADES = "trades" @@ -46,33 +48,45 @@ class PublicChannelsHandler(object): self.event_emitter.emit("status", subscription, parameters[0]) class AuthenticatedEventsHandler(object): - def __init__(self, event_emitter): - self.event_emitter = event_emitter + def __init__(self, event_emitter, strict = False): + self.event_emitter, self.strict = event_emitter, strict self.__handlers = { "bu": self.__bu_event_handler, "ws": self.__ws_event_handler, "wu": self.__wu_event_handler, "os": self.__os_event_handler, - "on": self.__on_event_handler + "on": self.__on_event_handler, + "ou": self.__ou_event_handler, + "oc": self.__oc_event_handler, + "ps": self.__ps_event_handler, + "pn": self.__pn_event_handler, + "pu": self.__pu_event_handler, + "pc": self.__pc_event_handler, + "fos": self.__fos_event_handler, + "fon": self.__fon_event_handler, + "fou": self.__fou_event_handler, + "foc": self.__foc_event_handler, } - def handle(self, type, parameters): + def handle(self, type, stream): if type in self.__handlers: - self.__handlers[type](*parameters) + self.__handlers[type](*stream) + elif self.strict == True: + raise BfxWebsocketException(f"Event of type <{type}> not found in self.__handlers.") - def __bu_event_handler(self, *parameters): - self.event_emitter.emit("balance_update", _label_array_elements( + def __bu_event_handler(self, *stream): + self.event_emitter.emit("balance_update", _label_stream_data( [ "AUM", "AUM_NET" ], - *parameters + *stream )) - def __ws_event_handler(self, *parameters): + def __ws_event_handler(self, *stream): self.event_emitter.emit("wallet_snapshot", [ - _label_array_elements( + _label_stream_data( [ "WALLET_TYPE", "CURRENCY", @@ -82,12 +96,12 @@ class AuthenticatedEventsHandler(object): "DESCRIPTION", "META" ], - *parameter - ) for parameter in parameters + *substream + ) for substream in stream ]) - def __wu_event_handler(self, *parameters): - self.event_emitter.emit("wallet_update", _label_array_elements( + def __wu_event_handler(self, *stream): + self.event_emitter.emit("wallet_update", _label_stream_data( [ "WALLET_TYPE", "CURRENCY", @@ -97,12 +111,12 @@ class AuthenticatedEventsHandler(object): "DESCRIPTION", "META" ], - *parameters + *stream )) - def __os_event_handler(self, *parameters): + def __os_event_handler(self, *stream): self.event_emitter.emit("order_snapshot", [ - _label_array_elements( + _label_stream_data( [ "ID", "GID", @@ -137,12 +151,12 @@ class AuthenticatedEventsHandler(object): "_PLACEHOLDER", "META" ], - *parameter - ) for parameter in parameters + *substream + ) for substream in stream ]) - def __on_event_handler(self, *parameters): - self.event_emitter.emit("new_order", _label_array_elements( + def __on_event_handler(self, *stream): + self.event_emitter.emit("new_order", _label_stream_data( [ "ID", "GID", @@ -177,13 +191,315 @@ class AuthenticatedEventsHandler(object): "_PLACEHOLDER", "_PLACEHOLDER" ], - *parameters + *stream )) -def _label_array_elements(labels, *args): + def __ou_event_handler(self, *stream): + self.event_emitter.emit("order_update", _label_stream_data( + [ + "ID", + "GID", + "CID", + "SYMBOL", + "MTS_CREATE", + "MTS_UPDATE", + "AMOUNT", + "AMOUNT_ORIG", + "ORDER_TYPE", + "TYPE_PREV", + "MTS_TIF", + "_PLACEHOLDER", + "FLAGS", + "ORDER_STATUS", + "_PLACEHOLDER", + "_PLACEHOLDER", + "PRICE", + "PRICE_AVG", + "PRICE_TRAILING", + "PRICE_AUX_LIMIT", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "NOTIFY", + "HIDDEN", + "PLACED_ID", + "_PLACEHOLDER", + "_PLACEHOLDER", + "ROUTING", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER" + ], + *stream + )) + + def __oc_event_handler(self, *stream): + self.event_emitter.emit("order_cancel", _label_stream_data( + [ + "ID", + "GID", + "CID", + "SYMBOL", + "MTS_CREATE", + "MTS_UPDATE", + "AMOUNT", + "AMOUNT_ORIG", + "ORDER_TYPE", + "TYPE_PREV", + "MTS_TIF", + "_PLACEHOLDER", + "FLAGS", + "ORDER_STATUS", + "_PLACEHOLDER", + "_PLACEHOLDER", + "PRICE", + "PRICE_AVG", + "PRICE_TRAILING", + "PRICE_AUX_LIMIT", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "NOTIFY", + "HIDDEN", + "PLACED_ID", + "_PLACEHOLDER", + "_PLACEHOLDER", + "ROUTING", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER" + ], + *stream + )) + + def __ps_event_handler(self, *stream): + self.event_emitter.emit("position_snapshot", [ + _label_stream_data( + [ + "SYMBOL", + "STATUS", + "AMOUNT", + "BASE_PRICE", + "MARGIN_FUNDING", + "MARGIN_FUNDING_TYPE", + "PL", + "PL_PERC", + "PRICE_LIQ", + "LEVERAGE", + "FLAG", + "POSITION_ID", + "MTS_CREATE", + "MTS_UPDATE", + "_PLACEHOLDER", + "TYPE", + "_PLACEHOLDER", + "COLLATERAL", + "COLLATERAL_MIN", + "META" + ], + *substream + ) + for substream in stream + ]) + + def __pn_event_handler(self, *stream): + self.event_emitter.emit("new_position", _label_stream_data( + [ + "SYMBOL", + "STATUS", + "AMOUNT", + "BASE_PRICE", + "MARGIN_FUNDING", + "MARGIN_FUNDING_TYPE", + "PL", + "PL_PERC", + "PRICE_LIQ", + "LEVERAGE", + "FLAG", + "POSITION_ID", + "MTS_CREATE", + "MTS_UPDATE", + "_PLACEHOLDER", + "TYPE", + "_PLACEHOLDER", + "COLLATERAL", + "COLLATERAL_MIN", + "META" + ], + *stream + )) + + def __pu_event_handler(self, *stream): + self.event_emitter.emit("position_update", _label_stream_data( + [ + "SYMBOL", + "STATUS", + "AMOUNT", + "BASE_PRICE", + "MARGIN_FUNDING", + "MARGIN_FUNDING_TYPE", + "PL", + "PL_PERC", + "PRICE_LIQ", + "LEVERAGE", + "FLAG", + "POSITION_ID", + "MTS_CREATE", + "MTS_UPDATE", + "_PLACEHOLDER", + "TYPE", + "_PLACEHOLDER", + "COLLATERAL", + "COLLATERAL_MIN", + "META" + ], + *stream + )) + + def __pc_event_handler(self, *stream): + self.event_emitter.emit("position_cancel", _label_stream_data( + [ + "SYMBOL", + "STATUS", + "AMOUNT", + "BASE_PRICE", + "MARGIN_FUNDING", + "MARGIN_FUNDING_TYPE", + "PL", + "PL_PERC", + "PRICE_LIQ", + "LEVERAGE", + "FLAG", + "POSITION_ID", + "MTS_CREATE", + "MTS_UPDATE", + "_PLACEHOLDER", + "TYPE", + "_PLACEHOLDER", + "COLLATERAL", + "COLLATERAL_MIN", + "META" + ], + *stream + )) + + def __fos_event_handler(self, *stream): + self.event_emitter.emit("funding_offer_snapshot", [ + _label_stream_data( + [ + "ID", + "SYMBOL", + "MTS_CREATED", + "MTS_UPDATED", + "AMOUNT", + "AMOUNT_ORIG", + "OFFER_TYPE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "FLAGS", + "STATUS", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "RATE", + "PERIOD", + "NOTIFY", + "HIDDEN", + "_PLACEHOLDER", + "RENEW", + "_PLACEHOLDER", + ], + *substream + ) + for substream in stream + ]) + + def __fon_event_handler(self, *stream): + self.event_emitter.emit("funding_offer_new", _label_stream_data( + [ + "ID", + "SYMBOL", + "MTS_CREATED", + "MTS_UPDATED", + "AMOUNT", + "AMOUNT_ORIG", + "TYPE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "FLAGS", + "STATUS", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "RATE", + "PERIOD", + "NOTIFY", + "HIDDEN", + "_PLACEHOLDER", + "RENEW", + "RATE_REAL" + ], + *stream + )) + + def __fou_event_handler(self, *stream): + self.event_emitter.emit("funding_offer_update", _label_stream_data( + [ + "ID", + "SYMBOL", + "MTS_CREATED", + "MTS_UPDATED", + "AMOUNT", + "AMOUNT_ORIG", + "TYPE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "FLAGS", + "STATUS", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "RATE", + "PERIOD", + "NOTIFY", + "HIDDEN", + "_PLACEHOLDER", + "RENEW", + "RATE_REAL" + ], + *stream + )) + + def __foc_event_handler(self, *stream): + self.event_emitter.emit("funding_offer_cancel", _label_stream_data( + [ + "ID", + "SYMBOL", + "MTS_CREATED", + "MTS_UPDATED", + "AMOUNT", + "AMOUNT_ORIG", + "TYPE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "FLAGS", + "STATUS", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "RATE", + "PERIOD", + "NOTIFY", + "HIDDEN", + "_PLACEHOLDER", + "RENEW", + "RATE_REAL" + ], + *stream + )) + +def _label_stream_data(labels, *args, IGNORE = [ "_PLACEHOLDER" ]): if len(labels) != len(args): - raise Exception(" and <*args> arguments should contain the same amount of elements.") + raise BfxWebsocketException(" and <*args> arguments should contain the same amount of elements.") - _PLACEHOLDER = "_PLACEHOLDER" - - return { label: args[index] for index, label in enumerate(labels) if label != _PLACEHOLDER } \ No newline at end of file + return { label: args[index] for index, label in enumerate(labels) if label not in IGNORE } \ No newline at end of file diff --git a/bfxapi/websocket/typings.py b/bfxapi/websocket/typings.py index f49c22c..8172da8 100644 --- a/bfxapi/websocket/typings.py +++ b/bfxapi/websocket/typings.py @@ -1,16 +1,125 @@ -from typing import TypedDict, List, Optional +from typing import Type, List, Dict, TypedDict, Union, Optional, Any -class BalanceUpdateStream(TypedDict): - AUM: float - AUM_NET: float +JSON = Union[Dict[str, Any], List[Any], int, str, float, bool, Type[None]] -class WalletUpdateStream(TypedDict): - WALLET_TYPE: str - CURRENCY: str - BALANCE: float - UNSETTLED_INTEREST: float - BALANCE_AVAILABLE: Optional[float] - DESCRIPTION: str - META: dict +BalanceUpdateStream = TypedDict("BalanceUpdateStream", { + "AUM": float, + "AUM_NET": float +}) -WalletSnapshotStream = List[WalletUpdateStream] \ No newline at end of file +WalletSnapshotStream = List[TypedDict("WalletSnapshotStream", { + "WALLET_TYPE": str, + "CURRENCY": str, + "BALANCE": float, + "UNSETTLED_INTEREST": float, + "BALANCE_AVAILABLE": Optional[float], + "DESCRIPTION": str, + "META": JSON +})] + +WalletUpdateStream = TypedDict("WalletUpdateStream", { + "WALLET_TYPE": str, + "CURRENCY": str, + "BALANCE": float, + "UNSETTLED_INTEREST": float, + "BALANCE_AVAILABLE": Optional[float], + "DESCRIPTION": str, + "META": JSON +}) + +OrderSnapshotStream = List[TypedDict("OrderSnapshotStream", { + "ID": int, + "GID": int, + "CID": int, + "SYMBOL": str, + "MTS_CREATE": int, + "MTS_UPDATE": int, + "AMOUNT": float, + "AMOUNT_ORIG": float, + "ORDER_TYPE": str, + "TYPE_PREV": str, + "MTS_TIF": int, + "FLAGS": int, + "STATUS": str, + "PRICE": float, + "PRICE_AVG": float, + "PRICE_TRAILING": float, + "PRICE_AUX_LIMIT": float, + "NOTIFY": int, + "HIDDEN": int, + "PLACED_ID": int, + "ROUTING": str, + "META": JSON +})] + +NewOrderStream = TypedDict("NewOrderStream", { + "ID": int, + "GID": int, + "CID": int, + "SYMBOL": str, + "MTS_CREATE": int, + "MTS_UPDATE": int, + "AMOUNT": float, + "AMOUNT_ORIG": float, + "ORDER_TYPE": str, + "TYPE_PREV": str, + "MTS_TIF": int, + "FLAGS": int, + "ORDER_STATUS": str, + "PRICE": float, + "PRICE_AVG": float, + "PRICE_TRAILING": float, + "PRICE_AUX_LIMIT": float, + "NOTIFY": int, + "HIDDEN": int, + "PLACED_ID": int, + "ROUTING": str +}) + +OrderUpdateStream = TypedDict("OrderUpdateStream", { + "ID": int, + "GID": int, + "CID": int, + "SYMBOL": str, + "MTS_CREATE": int, + "MTS_UPDATE": int, + "AMOUNT": float, + "AMOUNT_ORIG": float, + "ORDER_TYPE": str, + "TYPE_PREV": str, + "MTS_TIF": int, + "FLAGS": int, + "ORDER_STATUS": str, + "PRICE": float, + "PRICE_AVG": float, + "PRICE_TRAILING": float, + "PRICE_AUX_LIMIT": float, + "NOTIFY": int, + "HIDDEN": int, + "PLACED_ID": int, + "ROUTING": str +}) + +OrderCancelStream = TypedDict("OrderCancelStream", { + "ID": int, + "GID": int, + "CID": int, + "SYMBOL": str, + "MTS_CREATE": int, + "MTS_UPDATE": int, + "AMOUNT": float, + "AMOUNT_ORIG": float, + "ORDER_TYPE": str, + "TYPE_PREV": str, + "MTS_TIF": int, + "FLAGS": int, + "ORDER_STATUS": str, + "PRICE": float, + "PRICE_AVG": float, + "PRICE_TRAILING": float, + "PRICE_AUX_LIMIT": float, + "NOTIFY": int, + "HIDDEN": int, + "PLACED_ID": int, + "ROUTING": str +}) \ No newline at end of file From 495b51a2f670670fe8ea4cca748622ae86afc036 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 14 Nov 2022 17:41:09 +0100 Subject: [PATCH 012/265] Rewrite bfxapi/websocket/typings.py script adding new types and fixing some naming bug. --- bfxapi/websocket/typings.py | 263 ++++++++++++++++++++---------------- 1 file changed, 143 insertions(+), 120 deletions(-) diff --git a/bfxapi/websocket/typings.py b/bfxapi/websocket/typings.py index 8172da8..b215187 100644 --- a/bfxapi/websocket/typings.py +++ b/bfxapi/websocket/typings.py @@ -1,125 +1,148 @@ -from typing import Type, List, Dict, TypedDict, Union, Optional, Any +from typing import Type, List, Dict, TypedDict, Union, Optional -JSON = Union[Dict[str, Any], List[Any], int, str, float, bool, Type[None]] +JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] -BalanceUpdateStream = TypedDict("BalanceUpdateStream", { +Order = TypedDict("Order", { + "ID": int, + "GID": int, + "CID": int, + "SYMBOL": str, + "MTS_CREATE": int, + "MTS_UPDATE": int, + "AMOUNT": float, + "AMOUNT_ORIG": float, + "ORDER_TYPE": str, + "TYPE_PREV": str, + "MTS_TIF": int, + "FLAGS": int, + "ORDER_STATUS": str, + "PRICE": float, + "PRICE_AVG": float, + "PRICE_TRAILING": float, + "PRICE_AUX_LIMIT": float, + "NOTIFY": int, + "HIDDEN": int, + "PLACED_ID": int, + "ROUTING": str, + "META": JSON +}) + +Orders = List[Order] + +Position = TypedDict("Position", { + "SYMBOL": str, + "STATUS": str, + "AMOUNT": float, + "BASE_PRICE": float, + "MARGIN_FUNDING": float, + "MARGIN_FUNDING_TYPE": int, + "PL": float, + "PL_PERC": float, + "PRICE_LIQ": float, + "LEVERAGE": float, + "POSITION_ID": int, + "MTS_CREATE": int, + "MTS_UPDATE": int, + "TYPE": int, + "COLLATERAL": float, + "COLLATERAL_MIN": float, + "META": JSON, +}) + +Positions = List[Position] + +Trade = TypedDict("Trade", { + "ID": int, + "CID": int, + "SYMBOL": str, + "MTS_CREATE": int, + "ORDER_ID": int, + "EXEC_AMOUNT": float, + "EXEC_PRICE": float, + "ORDER_TYPE": str, + "ORDER_PRICE": float, + "MAKER": int, + "FEE": float, + "FEE_CURRENCY": str +}) + +FundingOffer = TypedDict("FundingOffer", { + "ID": int, + "SYMBOL": str, + "MTS_CREATED": int, + "MTS_UPDATED": int, + "AMOUNT": float, + "AMOUNT_ORIG": float, + "OFFER_TYPE": str, + "FLAGS": int, + "STATUS": str, + "RATE": float, + "PERIOD": int, + "NOTIFY": int, + "HIDDEN": int, + "RENEW": int, +}) + +FundingOffers = List[FundingOffer] + +FundingCredit = TypedDict("FundingCredit", { + "ID": int, + "SYMBOL": str, + "SIDE": int, + "MTS_CREATE": int, + "MTS_UPDATE": int, + "AMOUNT": float, + "FLAGS": int, + "STATUS": str, + "RATE": float, + "PERIOD": int, + "MTS_OPENING": int, + "MTS_LAST_PAYOUT": int, + "NOTIFY": int, + "HIDDEN": int, + "RENEW": int, + "RATE_REAL": float, + "NO_CLOSE": int, + "POSITION_PAIR": str +}) + +FundingCredits = List[FundingCredit] + +FundingLoan = TypedDict("FundingLoan", { + "ID": int, + "SYMBOL": str, + "SIDE": int, + "MTS_CREATE": int, + "MTS_UPDATE": int, + "AMOUNT": float, + "FLAGS": int, + "STATUS": str, + "RATE": float, + "PERIOD": int, + "MTS_OPENING": int, + "MTS_LAST_PAYOUT": int, + "NOTIFY": int, + "HIDDEN": int, + "RENEW": int, + "RATE_REAL": float, + "NO_CLOSE": int +}) + +FundingLoans = List[FundingLoan] + +Wallet = TypedDict("Wallet", { + "WALLET_TYPE": str, + "CURRENCY": str, + "BALANCE": float, + "UNSETTLED_INTEREST": float, + "BALANCE_AVAILABLE": float, + "DESCRIPTION": str, + "META": JSON +}) + +Wallets = List[Wallet] + +BalanceInfo = TypedDict("BalanceInfo", { "AUM": float, "AUM_NET": float -}) - -WalletSnapshotStream = List[TypedDict("WalletSnapshotStream", { - "WALLET_TYPE": str, - "CURRENCY": str, - "BALANCE": float, - "UNSETTLED_INTEREST": float, - "BALANCE_AVAILABLE": Optional[float], - "DESCRIPTION": str, - "META": JSON -})] - -WalletUpdateStream = TypedDict("WalletUpdateStream", { - "WALLET_TYPE": str, - "CURRENCY": str, - "BALANCE": float, - "UNSETTLED_INTEREST": float, - "BALANCE_AVAILABLE": Optional[float], - "DESCRIPTION": str, - "META": JSON -}) - -OrderSnapshotStream = List[TypedDict("OrderSnapshotStream", { - "ID": int, - "GID": int, - "CID": int, - "SYMBOL": str, - "MTS_CREATE": int, - "MTS_UPDATE": int, - "AMOUNT": float, - "AMOUNT_ORIG": float, - "ORDER_TYPE": str, - "TYPE_PREV": str, - "MTS_TIF": int, - "FLAGS": int, - "STATUS": str, - "PRICE": float, - "PRICE_AVG": float, - "PRICE_TRAILING": float, - "PRICE_AUX_LIMIT": float, - "NOTIFY": int, - "HIDDEN": int, - "PLACED_ID": int, - "ROUTING": str, - "META": JSON -})] - -NewOrderStream = TypedDict("NewOrderStream", { - "ID": int, - "GID": int, - "CID": int, - "SYMBOL": str, - "MTS_CREATE": int, - "MTS_UPDATE": int, - "AMOUNT": float, - "AMOUNT_ORIG": float, - "ORDER_TYPE": str, - "TYPE_PREV": str, - "MTS_TIF": int, - "FLAGS": int, - "ORDER_STATUS": str, - "PRICE": float, - "PRICE_AVG": float, - "PRICE_TRAILING": float, - "PRICE_AUX_LIMIT": float, - "NOTIFY": int, - "HIDDEN": int, - "PLACED_ID": int, - "ROUTING": str -}) - -OrderUpdateStream = TypedDict("OrderUpdateStream", { - "ID": int, - "GID": int, - "CID": int, - "SYMBOL": str, - "MTS_CREATE": int, - "MTS_UPDATE": int, - "AMOUNT": float, - "AMOUNT_ORIG": float, - "ORDER_TYPE": str, - "TYPE_PREV": str, - "MTS_TIF": int, - "FLAGS": int, - "ORDER_STATUS": str, - "PRICE": float, - "PRICE_AVG": float, - "PRICE_TRAILING": float, - "PRICE_AUX_LIMIT": float, - "NOTIFY": int, - "HIDDEN": int, - "PLACED_ID": int, - "ROUTING": str -}) - -OrderCancelStream = TypedDict("OrderCancelStream", { - "ID": int, - "GID": int, - "CID": int, - "SYMBOL": str, - "MTS_CREATE": int, - "MTS_UPDATE": int, - "AMOUNT": float, - "AMOUNT_ORIG": float, - "ORDER_TYPE": str, - "TYPE_PREV": str, - "MTS_TIF": int, - "FLAGS": int, - "ORDER_STATUS": str, - "PRICE": float, - "PRICE_AVG": float, - "PRICE_TRAILING": float, - "PRICE_AUX_LIMIT": float, - "NOTIFY": int, - "HIDDEN": int, - "PLACED_ID": int, - "ROUTING": str }) \ No newline at end of file From 7d6ba4302ad6ce4096096fad215e622b1dad1f02 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 14 Nov 2022 18:37:31 +0100 Subject: [PATCH 013/265] Rename AuthenticatedEventsHandler to AuthenticatedChannelsHandler. Rewrite handlers.py gathering events in channels. Update references in bfxapi. --- bfxapi/websocket/BfxWebsocketClient.py | 4 +- bfxapi/websocket/handlers.py | 657 +++++++++---------------- 2 files changed, 238 insertions(+), 423 deletions(-) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 9ba6b45..628e316 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -2,7 +2,7 @@ import json, asyncio, hmac, hashlib, time, websockets from pyee.asyncio import AsyncIOEventEmitter -from .handlers import Channels, PublicChannelsHandler, AuthenticatedEventsHandler +from .handlers import Channels, PublicChannelsHandler, AuthenticatedChannelsHandler from .errors import BfxWebsocketException, ConnectionNotOpen, InvalidAuthenticationCredentials @@ -16,7 +16,7 @@ class BfxWebsocketClient(object): self.handlers = { "public": PublicChannelsHandler(event_emitter=self.event_emitter), - "authenticated": AuthenticatedEventsHandler(event_emitter=self.event_emitter) + "authenticated": AuthenticatedChannelsHandler(event_emitter=self.event_emitter) } async def connect(self): diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index a601aa6..d9433aa 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -47,456 +47,271 @@ class PublicChannelsHandler(object): def __status_channel_handler(self, subscription, *parameters): self.event_emitter.emit("status", subscription, parameters[0]) -class AuthenticatedEventsHandler(object): +class AuthenticatedChannelsHandler(object): def __init__(self, event_emitter, strict = False): self.event_emitter, self.strict = event_emitter, strict self.__handlers = { - "bu": self.__bu_event_handler, - "ws": self.__ws_event_handler, - "wu": self.__wu_event_handler, - "os": self.__os_event_handler, - "on": self.__on_event_handler, - "ou": self.__ou_event_handler, - "oc": self.__oc_event_handler, - "ps": self.__ps_event_handler, - "pn": self.__pn_event_handler, - "pu": self.__pu_event_handler, - "pc": self.__pc_event_handler, - "fos": self.__fos_event_handler, - "fon": self.__fon_event_handler, - "fou": self.__fou_event_handler, - "foc": self.__foc_event_handler, + ("os", "on", "ou", "oc",): self.__orders_channel_handler, + ("ps", "pn", "pu", "pc",): self.__positions_channel_handler, + ("te", "tu",): self.__trades_channel_handler, + ("fos", "fon", "fou", "foc",): self.__funding_offers_channel_handler, + ("fcs", "fcn", "fcu", "fcc",): self.__funding_credits_channel_handler, + ("fls", "fln", "flu", "flc",): self.__funding_loans_channel_handler, + ("ws", "wu",): self.__wallets_channel_handler, + ("bu",): self.__balance_info_channel_handler } def handle(self, type, stream): - if type in self.__handlers: - self.__handlers[type](*stream) - elif self.strict == True: + for abbreviations in self.__handlers.keys(): + if type in abbreviations: + return self.__handlers[abbreviations](type, stream) + + if self.strict == True: raise BfxWebsocketException(f"Event of type <{type}> not found in self.__handlers.") - def __bu_event_handler(self, *stream): - self.event_emitter.emit("balance_update", _label_stream_data( - [ - "AUM", - "AUM_NET" - ], - *stream - )) + def __orders_channel_handler(self, type, stream): + _labels = [ + "ID", + "GID", + "CID", + "SYMBOL", + "MTS_CREATE", + "MTS_UPDATE", + "AMOUNT", + "AMOUNT_ORIG", + "ORDER_TYPE", + "TYPE_PREV", + "MTS_TIF", + "_PLACEHOLDER", + "FLAGS", + "ORDER_STATUS", + "_PLACEHOLDER", + "_PLACEHOLDER", + "PRICE", + "PRICE_AVG", + "PRICE_TRAILING", + "PRICE_AUX_LIMIT", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "NOTIFY", + "HIDDEN", + "PLACED_ID", + "_PLACEHOLDER", + "_PLACEHOLDER", + "ROUTING", + "_PLACEHOLDER", + "_PLACEHOLDER", + "META" + ] - def __ws_event_handler(self, *stream): - self.event_emitter.emit("wallet_snapshot", [ - _label_stream_data( - [ - "WALLET_TYPE", - "CURRENCY", - "BALANCE", - "UNSETTLED_INTEREST", - "BALANCE_AVAILABLE", - "DESCRIPTION", - "META" - ], - *substream - ) for substream in stream - ]) - - def __wu_event_handler(self, *stream): - self.event_emitter.emit("wallet_update", _label_stream_data( - [ - "WALLET_TYPE", - "CURRENCY", - "BALANCE", - "UNSETTLED_INTEREST", - "BALANCE_AVAILABLE", - "DESCRIPTION", - "META" - ], - *stream - )) + if type == "os": + self.event_emitter.emit("order_snapshot", [ _label_stream_data(_labels, *substream) for substream in stream ]) - def __os_event_handler(self, *stream): - self.event_emitter.emit("order_snapshot", [ - _label_stream_data( - [ - "ID", - "GID", - "CID", - "SYMBOL", - "MTS_CREATE", - "MTS_UPDATE", - "AMOUNT", - "AMOUNT_ORIG", - "ORDER_TYPE", - "TYPE_PREV", - "MTS_TIF", - "_PLACEHOLDER", - "FLAGS", - "STATUS", - "_PLACEHOLDER", - "_PLACEHOLDER", - "PRICE", - "PRICE_AVG", - "PRICE_TRAILING", - "PRICE_AUX_LIMIT", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "NOTIFY", - "HIDDEN", - "PLACED_ID", - "_PLACEHOLDER", - "_PLACEHOLDER", - "ROUTING", - "_PLACEHOLDER", - "_PLACEHOLDER", - "META" - ], - *substream - ) for substream in stream - ]) + if type == "on" or type == "ou" or type == "oc": + self.event_emitter.emit({ + "on": "new_order", + "ou": "order_update", + "oc": "order_cancel" + }[type], _label_stream_data(_labels, *stream)) - def __on_event_handler(self, *stream): - self.event_emitter.emit("new_order", _label_stream_data( - [ + def __positions_channel_handler(self, type, stream): + _labels = [ + "SYMBOL", + "STATUS", + "AMOUNT", + "BASE_PRICE", + "MARGIN_FUNDING", + "MARGIN_FUNDING_TYPE", + "PL", + "PL_PERC", + "PRICE_LIQ", + "LEVERAGE", + "FLAG", + "POSITION_ID", + "MTS_CREATE", + "MTS_UPDATE", + "_PLACEHOLDER", + "TYPE", + "_PLACEHOLDER", + "COLLATERAL", + "COLLATERAL_MIN", + "META" + ] + + if type == "ps": + self.event_emitter.emit("position_snapshot", [ _label_stream_data(_labels, *substream) for substream in stream ]) + + if type == "pn" or type == "pu" or type == "pc": + self.event_emitter.emit({ + "pn": "new_position", + "pu": "position_update", + "pc": "position_close" + }[type], _label_stream_data(_labels, *stream)) + + def __trades_channel_handler(self, type, stream): + if type == "te": + self.event_emitter.emit("trade_executed", _label_stream_data([ "ID", - "GID", - "CID", "SYMBOL", - "MTS_CREATE", - "MTS_UPDATE", - "AMOUNT", - "AMOUNT_ORIG", + "MTS_CREATE", + "ORDER_ID", + "EXEC_AMOUNT", + "EXEC_PRICE", "ORDER_TYPE", - "TYPE_PREV", - "MTS_TIF", + "ORDER_PRICE", + "MAKER", "_PLACEHOLDER", - "FLAGS", - "ORDER_STATUS", - "_PLACEHOLDER", - "_PLACEHOLDER", - "PRICE", - "PRICE_AVG", - "PRICE_TRAILING", - "PRICE_AUX_LIMIT", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "NOTIFY", - "HIDDEN", - "PLACED_ID", - "_PLACEHOLDER", - "_PLACEHOLDER", - "ROUTING", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER" - ], - *stream - )) + "_PLACEHOLDER", + "CID" + ], *stream)) - def __ou_event_handler(self, *stream): - self.event_emitter.emit("order_update", _label_stream_data( - [ + if type == "tu": + self.event_emitter.emit("trade_execution_update", _label_stream_data([ "ID", - "GID", - "CID", "SYMBOL", - "MTS_CREATE", - "MTS_UPDATE", - "AMOUNT", - "AMOUNT_ORIG", + "MTS_CREATE", + "ORDER_ID", + "EXEC_AMOUNT", + "EXEC_PRICE", "ORDER_TYPE", - "TYPE_PREV", - "MTS_TIF", - "_PLACEHOLDER", - "FLAGS", - "ORDER_STATUS", - "_PLACEHOLDER", - "_PLACEHOLDER", - "PRICE", - "PRICE_AVG", - "PRICE_TRAILING", - "PRICE_AUX_LIMIT", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "NOTIFY", - "HIDDEN", - "PLACED_ID", - "_PLACEHOLDER", - "_PLACEHOLDER", - "ROUTING", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER" - ], - *stream - )) + "ORDER_PRICE", + "MAKER", + "FEE", + "FEE_CURRENCY", + "CID" + ], *stream)) - def __oc_event_handler(self, *stream): - self.event_emitter.emit("order_cancel", _label_stream_data( - [ - "ID", - "GID", - "CID", - "SYMBOL", - "MTS_CREATE", - "MTS_UPDATE", - "AMOUNT", - "AMOUNT_ORIG", - "ORDER_TYPE", - "TYPE_PREV", - "MTS_TIF", - "_PLACEHOLDER", - "FLAGS", - "ORDER_STATUS", - "_PLACEHOLDER", - "_PLACEHOLDER", - "PRICE", - "PRICE_AVG", - "PRICE_TRAILING", - "PRICE_AUX_LIMIT", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "NOTIFY", - "HIDDEN", - "PLACED_ID", - "_PLACEHOLDER", - "_PLACEHOLDER", - "ROUTING", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER" - ], - *stream - )) + def __funding_offers_channel_handler(self, type, stream): + _labels = [ + "ID", + "SYMBOL", + "MTS_CREATED", + "MTS_UPDATED", + "AMOUNT", + "AMOUNT_ORIG", + "OFFER_TYPE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "FLAGS", + "STATUS", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "RATE", + "PERIOD", + "NOTIFY", + "HIDDEN", + "_PLACEHOLDER", + "RENEW", + "_PLACEHOLDER", + ] - def __ps_event_handler(self, *stream): - self.event_emitter.emit("position_snapshot", [ - _label_stream_data( - [ - "SYMBOL", - "STATUS", - "AMOUNT", - "BASE_PRICE", - "MARGIN_FUNDING", - "MARGIN_FUNDING_TYPE", - "PL", - "PL_PERC", - "PRICE_LIQ", - "LEVERAGE", - "FLAG", - "POSITION_ID", - "MTS_CREATE", - "MTS_UPDATE", - "_PLACEHOLDER", - "TYPE", - "_PLACEHOLDER", - "COLLATERAL", - "COLLATERAL_MIN", - "META" - ], - *substream - ) - for substream in stream - ]) + if type == "fos": + self.event_emitter.emit("funding_offer_snapshot", [ _label_stream_data(_labels, *substream) for substream in stream ]) - def __pn_event_handler(self, *stream): - self.event_emitter.emit("new_position", _label_stream_data( - [ - "SYMBOL", - "STATUS", - "AMOUNT", - "BASE_PRICE", - "MARGIN_FUNDING", - "MARGIN_FUNDING_TYPE", - "PL", - "PL_PERC", - "PRICE_LIQ", - "LEVERAGE", - "FLAG", - "POSITION_ID", - "MTS_CREATE", - "MTS_UPDATE", - "_PLACEHOLDER", - "TYPE", - "_PLACEHOLDER", - "COLLATERAL", - "COLLATERAL_MIN", - "META" - ], - *stream - )) + if type == "fon" or type == "fou" or type == "foc": + self.event_emitter.emit({ + "fon": "funding_offer_new", + "fou": "funding_offer_update", + "foc": "funding_offer_cancel" + }[type], _label_stream_data(_labels, *stream)) - def __pu_event_handler(self, *stream): - self.event_emitter.emit("position_update", _label_stream_data( - [ - "SYMBOL", - "STATUS", - "AMOUNT", - "BASE_PRICE", - "MARGIN_FUNDING", - "MARGIN_FUNDING_TYPE", - "PL", - "PL_PERC", - "PRICE_LIQ", - "LEVERAGE", - "FLAG", - "POSITION_ID", - "MTS_CREATE", - "MTS_UPDATE", - "_PLACEHOLDER", - "TYPE", - "_PLACEHOLDER", - "COLLATERAL", - "COLLATERAL_MIN", - "META" - ], - *stream - )) + def __funding_credits_channel_handler(self, type, stream): + _labels = [ + "ID", + "SYMBOL", + "SIDE", + "MTS_CREATE", + "MTS_UPDATE", + "AMOUNT", + "FLAGS", + "STATUS", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "RATE", + "PERIOD", + "MTS_OPENING", + "MTS_LAST_PAYOUT", + "NOTIFY", + "HIDDEN", + "_PLACEHOLDER", + "RENEW", + "RATE_REAL", + "NO_CLOSE", + "POSITION_PAIR" + ] - def __pc_event_handler(self, *stream): - self.event_emitter.emit("position_cancel", _label_stream_data( - [ - "SYMBOL", - "STATUS", - "AMOUNT", - "BASE_PRICE", - "MARGIN_FUNDING", - "MARGIN_FUNDING_TYPE", - "PL", - "PL_PERC", - "PRICE_LIQ", - "LEVERAGE", - "FLAG", - "POSITION_ID", - "MTS_CREATE", - "MTS_UPDATE", - "_PLACEHOLDER", - "TYPE", - "_PLACEHOLDER", - "COLLATERAL", - "COLLATERAL_MIN", - "META" - ], - *stream - )) + if type == "fcs": + self.event_emitter.emit("funding_credit_snapshot", [ _label_stream_data(_labels, *substream) for substream in stream ]) - def __fos_event_handler(self, *stream): - self.event_emitter.emit("funding_offer_snapshot", [ - _label_stream_data( - [ - "ID", - "SYMBOL", - "MTS_CREATED", - "MTS_UPDATED", - "AMOUNT", - "AMOUNT_ORIG", - "OFFER_TYPE", - "_PLACEHOLDER", - "_PLACEHOLDER", - "FLAGS", - "STATUS", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "RATE", - "PERIOD", - "NOTIFY", - "HIDDEN", - "_PLACEHOLDER", - "RENEW", - "_PLACEHOLDER", - ], - *substream - ) - for substream in stream - ]) + if type == "fcn" or type == "fcu" or type == "fcc": + self.event_emitter.emit({ + "fcn": "funding_credit_new", + "fcu": "funding_credit_update", + "fcc": "funding_credit_close" + }[type], _label_stream_data(_labels, *stream)) - def __fon_event_handler(self, *stream): - self.event_emitter.emit("funding_offer_new", _label_stream_data( - [ - "ID", - "SYMBOL", - "MTS_CREATED", - "MTS_UPDATED", - "AMOUNT", - "AMOUNT_ORIG", - "TYPE", - "_PLACEHOLDER", - "_PLACEHOLDER", - "FLAGS", - "STATUS", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "RATE", - "PERIOD", - "NOTIFY", - "HIDDEN", - "_PLACEHOLDER", - "RENEW", - "RATE_REAL" - ], - *stream - )) + def __funding_loans_channel_handler(self, type, stream): + _labels = [ + "ID", + "SYMBOL", + "SIDE", + "MTS_CREATE", + "MTS_UPDATE", + "AMOUNT", + "FLAGS", + "STATUS", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "RATE", + "PERIOD", + "MTS_OPENING", + "MTS_LAST_PAYOUT", + "NOTIFY", + "HIDDEN", + "_PLACEHOLDER", + "RENEW", + "RATE_REAL", + "NO_CLOSE" + ] - def __fou_event_handler(self, *stream): - self.event_emitter.emit("funding_offer_update", _label_stream_data( - [ - "ID", - "SYMBOL", - "MTS_CREATED", - "MTS_UPDATED", - "AMOUNT", - "AMOUNT_ORIG", - "TYPE", - "_PLACEHOLDER", - "_PLACEHOLDER", - "FLAGS", - "STATUS", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "RATE", - "PERIOD", - "NOTIFY", - "HIDDEN", - "_PLACEHOLDER", - "RENEW", - "RATE_REAL" - ], - *stream - )) + if type == "fls": + self.event_emitter.emit("funding_loan_snapshot", [ _label_stream_data(_labels, *substream) for substream in stream ]) - def __foc_event_handler(self, *stream): - self.event_emitter.emit("funding_offer_cancel", _label_stream_data( - [ - "ID", - "SYMBOL", - "MTS_CREATED", - "MTS_UPDATED", - "AMOUNT", - "AMOUNT_ORIG", - "TYPE", - "_PLACEHOLDER", - "_PLACEHOLDER", - "FLAGS", - "STATUS", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "RATE", - "PERIOD", - "NOTIFY", - "HIDDEN", - "_PLACEHOLDER", - "RENEW", - "RATE_REAL" - ], - *stream - )) + if type == "fln" or type == "flu" or type == "flc": + self.event_emitter.emit({ + "fln": "funding_loan_new", + "flu": "funding_loan_update", + "flc": "funding_loan_close" + }[type], _label_stream_data(_labels, *stream)) + + def __wallets_channel_handler(self, type, stream): + _labels = [ + "WALLET_TYPE", + "CURRENCY", + "BALANCE", + "UNSETTLED_INTEREST", + "BALANCE_AVAILABLE", + "DESCRIPTION", + "META" + ] + + if type == "ws": + self.event_emitter.emit("wallet_snapshot", [ _label_stream_data(_labels, *substream) for substream in stream ]) + + if type == "wu": + self.event_emitter.emit("wallet_update", _label_stream_data(_labels, *stream)) + + def __balance_info_channel_handler(self, type, stream): + if type == "bu": + self.event_emitter.emit("balance_update", _label_stream_data([ + "AUM", + "AUM_NET" + ], *stream)) def _label_stream_data(labels, *args, IGNORE = [ "_PLACEHOLDER" ]): if len(labels) != len(args): From b9db5dd133341ea4ed71db00f5a9ebabd8ad5d1c Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 15 Nov 2022 04:34:29 +0100 Subject: [PATCH 014/265] Update bfxapi/websocket/handlers.py and bfxapi/websocket/typings.py with new public channel handling logic. --- bfxapi/websocket/handlers.py | 50 +++++++++++++++++------------------- bfxapi/websocket/typings.py | 34 +++++++++++++++++++++++- 2 files changed, 56 insertions(+), 28 deletions(-) diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index d9433aa..d8bca05 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -14,39 +14,32 @@ class PublicChannelsHandler(object): self.event_emitter = event_emitter self.__handlers = { - Channels.TICKER: self.__ticker_channel_handler, - Channels.TRADES: self.__trades_channel_handler, Channels.BOOK: self.__book_channel_handler, - Channels.CANDLES: self.__candles_channel_handler, - Channels.STATUS: self.__status_channel_handler, } - def handle(self, subscription, *parameters): - self.__handlers[subscription["channel"]](subscription, *parameters) + def handle(self, subscription, *stream): + return self.__handlers[subscription["channel"]](subscription, *stream) - def __ticker_channel_handler(self, subscription, *parameters): - self.event_emitter.emit("ticker", subscription, parameters[0]) + def __book_channel_handler(self, subscription, *stream): + subscription = _filter_dictionary_keys(subscription, [ "chanId", "symbol", "prec", "freq", "len", "subId", "pair" ]) - def __trades_channel_handler(self, subscription, *parameters): - if len(parameters) == 1: - self.event_emitter.emit("trades_snapshot", subscription, parameters[0]) + if subscription["prec"] == "R0": + _trading_pairs_labels, _funding_currencies_labels, IS_RAW_BOOK = [ "ORDER_ID", "PRICE", "AMOUNT" ], [ "OFFER_ID", "PERIOD", "RATE", "AMOUNT" ], True + else: _trading_pairs_labels, _funding_currencies_labels, IS_RAW_BOOK = [ "PRICE", "COUNT", "AMOUNT" ], [ "RATE", "PERIOD", "COUNT", "AMOUNT" ], False - if len(parameters) == 2: - self.event_emitter.emit("trades_update", subscription, parameters[0], parameters[1]) - - def __book_channel_handler(self, subscription, *parameters): - if all(isinstance(element, list) for element in parameters[0]): - self.event_emitter.emit("book_snapshot", subscription, parameters[0]) - else: self.event_emitter.emit("book_update", subscription, parameters[0]) - - def __candles_channel_handler(self, subscription, *parameters): - if all(isinstance(element, list) for element in parameters[0]): - self.event_emitter.emit("candles_snapshot", subscription, parameters[0]) - else: self.event_emitter.emit("candles_update", subscription, parameters[0]) - - def __status_channel_handler(self, subscription, *parameters): - self.event_emitter.emit("status", subscription, parameters[0]) + if all(isinstance(substream, list) for substream in stream[0]): + return self.event_emitter.emit( + IS_RAW_BOOK and "raw_book_snapshot" or "book_snapshot", + subscription, + [ _label_stream_data({ 3: _trading_pairs_labels, 4: _funding_currencies_labels }[len(substream)], *substream) for substream in stream[0] ] + ) + return self.event_emitter.emit( + IS_RAW_BOOK and "raw_book_update" or "book_update", + subscription, + _label_stream_data({ 3: _trading_pairs_labels, 4: _funding_currencies_labels }[len(stream[0])], *stream[0]) + ) + class AuthenticatedChannelsHandler(object): def __init__(self, event_emitter, strict = False): self.event_emitter, self.strict = event_emitter, strict @@ -317,4 +310,7 @@ def _label_stream_data(labels, *args, IGNORE = [ "_PLACEHOLDER" ]): if len(labels) != len(args): raise BfxWebsocketException(" and <*args> arguments should contain the same amount of elements.") - return { label: args[index] for index, label in enumerate(labels) if label not in IGNORE } \ No newline at end of file + return { label: args[index] for index, label in enumerate(labels) if label not in IGNORE } + +def _filter_dictionary_keys(dictionary, keys): + return { key: dictionary[key] for key in dictionary if key in keys } \ No newline at end of file diff --git a/bfxapi/websocket/typings.py b/bfxapi/websocket/typings.py index b215187..1bf5128 100644 --- a/bfxapi/websocket/typings.py +++ b/bfxapi/websocket/typings.py @@ -2,6 +2,36 @@ from typing import Type, List, Dict, TypedDict, Union, Optional JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] +#region Type hinting for Websocket Public Channels + +BookSubscription = TypedDict("BookSubscription", { + "chanId": int, + "symbol": str, + "prec": str, + "freq": str, + "len": str, + "subId": int, + "pair": str +}) + +Book = Union[ + TypedDict("TradingPairBook", { "PRICE": float, "COUNT": int, "AMOUNT": float }), + TypedDict("FundingCurrencyBook", { "RATE": float, "PERIOD": int, "COUNT": int, "AMOUNT": float }), +] + +Books = List[Book] + +RawBook = Union[ + TypedDict("TradingPairRawBook", { "ORDER_ID": int, "PRICE": float, "AMOUNT": float }), + TypedDict("FundingCurrencyRawBook", { "OFFER_ID": int, "PERIOD": int, "RATE": float, "AMOUNT": float }), +] + +RawBooks = List[RawBook] + +#endregion + +#region Type hinting for Websocket Authenticated Channels + Order = TypedDict("Order", { "ID": int, "GID": int, @@ -145,4 +175,6 @@ Wallets = List[Wallet] BalanceInfo = TypedDict("BalanceInfo", { "AUM": float, "AUM_NET": float -}) \ No newline at end of file +}) + +#endregion \ No newline at end of file From b7ac7f9caf984c8bcc93ae9f777aa09726fef8b7 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 15 Nov 2022 15:22:00 +0100 Subject: [PATCH 015/265] Separate some nested typings in bfxapi/websocket/typings.py. --- bfxapi/websocket/typings.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/bfxapi/websocket/typings.py b/bfxapi/websocket/typings.py index 1bf5128..baecc8a 100644 --- a/bfxapi/websocket/typings.py +++ b/bfxapi/websocket/typings.py @@ -14,19 +14,27 @@ BookSubscription = TypedDict("BookSubscription", { "pair": str }) -Book = Union[ +(TradingPairBook, FundingCurrencyBook) = ( TypedDict("TradingPairBook", { "PRICE": float, "COUNT": int, "AMOUNT": float }), - TypedDict("FundingCurrencyBook", { "RATE": float, "PERIOD": int, "COUNT": int, "AMOUNT": float }), -] + TypedDict("FundingCurrencyBook", { "RATE": float, "PERIOD": int, "COUNT": int, "AMOUNT": float }) +) -Books = List[Book] +(TradingPairBooks, FundingCurrencyBooks) = (List[TradingPairBook], List[FundingCurrencyBook]) -RawBook = Union[ +Book = Union[TradingPairBook, FundingCurrencyBook] + +Books = Union[TradingPairBooks, FundingCurrencyBooks] + +(TradingPairRawBook, FundingCurrencyRawBook) = ( TypedDict("TradingPairRawBook", { "ORDER_ID": int, "PRICE": float, "AMOUNT": float }), TypedDict("FundingCurrencyRawBook", { "OFFER_ID": int, "PERIOD": int, "RATE": float, "AMOUNT": float }), -] +) -RawBooks = List[RawBook] +(TradingPairRawBooks, FundingCurrencyRawBooks) = (List[TradingPairRawBook], List[FundingCurrencyRawBook]) + +RawBook = Union[TradingPairRawBook, FundingCurrencyRawBook] + +RawBooks = Union[TradingPairRawBooks, FundingCurrencyRawBooks] #endregion From 217f767a3008242d814f3ceb4a2ed67da5677da9 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 15 Nov 2022 17:19:17 +0100 Subject: [PATCH 016/265] Add handlers for ticker, trades and candles public channels. Add new type hinting inside bfxapi/websocket/typings.py. --- bfxapi/websocket/handlers.py | 127 +++++++++++++++++++++++++++++++---- bfxapi/websocket/typings.py | 83 ++++++++++++++++++++++- 2 files changed, 196 insertions(+), 14 deletions(-) diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index d8bca05..ed406a3 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -2,6 +2,15 @@ from enum import Enum from .errors import BfxWebsocketException +def _get_sub_dictionary(dictionary, keys): + return { key: dictionary[key] for key in dictionary if key in keys } + +def _label_stream_data(labels, *args, IGNORE = [ "_PLACEHOLDER" ]): + if len(labels) != len(args): + raise BfxWebsocketException(" and <*args> arguments should contain the same amount of elements.") + + return { label: args[index] for index, label in enumerate(labels) if label not in IGNORE } + class Channels(str, Enum): TICKER = "ticker" TRADES = "trades" @@ -14,14 +23,97 @@ class PublicChannelsHandler(object): self.event_emitter = event_emitter self.__handlers = { + Channels.TICKER: self.__ticker_channel_handler, + Channels.TRADES: self.__trades_channel_handler, Channels.BOOK: self.__book_channel_handler, + Channels.CANDLES: self.__candles_channel_handler } def handle(self, subscription, *stream): return self.__handlers[subscription["channel"]](subscription, *stream) + def __ticker_channel_handler(self, subscription, *stream): + _trading_pairs_labels = [ + "BID", + "BID_SIZE", + "ASK", + "ASK_SIZE", + "DAILY_CHANGE", + "DAILY_CHANGE_RELATIVE", + "LAST_PRICE", + "VOLUME", + "HIGH", + "LOW" + ] + + _funding_currencies_labels = [ + "FRR", + "BID", + "BID_PERIOD", + "BID_SIZE", + "ASK", + "ASK_PERIOD", + "ASK_SIZE", + "DAILY_CHANGE", + "DAILY_CHANGE_RELATIVE", + "LAST_PRICE", + "VOLUME", + "HIGH", + "LOW" + "_PLACEHOLDER", + "_PLACEHOLDER", + "FRR_AMOUNT_AVAILABLE" + ] + + if subscription["symbol"].startswith("t"): + return self.event_emitter.emit( + "tp_ticker_update", + _get_sub_dictionary(subscription, [ "chanId", "symbol", "pair" ]), + _label_stream_data(_trading_pairs_labels, *stream[0]) + ) + + if subscription["symbol"].startswith("f"): + return self.event_emitter.emit( + "fc_ticker_update", + _get_sub_dictionary(subscription, [ "chanId", "symbol", "currency" ]), + _label_stream_data(_funding_currencies_labels, *stream[0]) + ) + + def __trades_channel_handler(self, subscription, *stream): + _trading_pairs_labels, _funding_currencies_labels = [ "ID", "MTS", "AMOUNT", "PRICE" ], [ "ID", "MTS", "AMOUNT", "RATE", "PERIOD" ] + + if len(stream) == 1: + if subscription["symbol"].startswith("t"): + return self.event_emitter.emit( + "tp_trades_snapshot", + _get_sub_dictionary(subscription, [ "chanId", "symbol", "pair" ]), + [ _label_stream_data(_trading_pairs_labels, *substream) for substream in stream[0] ] + ) + + if subscription["symbol"].startswith("f"): + return self.event_emitter.emit( + "fc_trades_snapshot", + _get_sub_dictionary(subscription, [ "chanId", "symbol", "currency" ]), + [ _label_stream_data(_funding_currencies_labels, *substream) for substream in stream[0] ] + ) + + if type := stream[0] or type in [ "te", "tu", "fte", "ftu" ]: + if subscription["symbol"].startswith("t"): + return self.event_emitter.emit( + { "te": "tp_trade_executed", "tu": "tp_trade_execution_update" }[type], + _get_sub_dictionary(subscription, [ "chanId", "symbol", "pair" ]), + _label_stream_data(_trading_pairs_labels, *stream[1]) + ) + + if subscription["symbol"].startswith("f"): + return self.event_emitter.emit( + { "fte": "fc_trade_executed", "ftu": "fc_trade_execution_update" }[type], + _get_sub_dictionary(subscription, [ "chanId", "symbol", "currency" ]), + _label_stream_data(_funding_currencies_labels, *stream[1]) + ) + def __book_channel_handler(self, subscription, *stream): - subscription = _filter_dictionary_keys(subscription, [ "chanId", "symbol", "prec", "freq", "len", "subId", "pair" ]) + subscription = _get_sub_dictionary(subscription, [ "chanId", "symbol", "prec", "freq", "len", "subId", "pair" ]) if subscription["prec"] == "R0": _trading_pairs_labels, _funding_currencies_labels, IS_RAW_BOOK = [ "ORDER_ID", "PRICE", "AMOUNT" ], [ "OFFER_ID", "PERIOD", "RATE", "AMOUNT" ], True @@ -31,15 +123,33 @@ class PublicChannelsHandler(object): return self.event_emitter.emit( IS_RAW_BOOK and "raw_book_snapshot" or "book_snapshot", subscription, - [ _label_stream_data({ 3: _trading_pairs_labels, 4: _funding_currencies_labels }[len(substream)], *substream) for substream in stream[0] ] + [ _label_stream_data({ "t": _trading_pairs_labels, "f": _funding_currencies_labels }[subscription["symbol"][0]], *substream) for substream in stream[0] ] ) return self.event_emitter.emit( IS_RAW_BOOK and "raw_book_update" or "book_update", subscription, - _label_stream_data({ 3: _trading_pairs_labels, 4: _funding_currencies_labels }[len(stream[0])], *stream[0]) + _label_stream_data({ "t": _trading_pairs_labels, "f": _funding_currencies_labels }[subscription["symbol"][0]], *stream[0]) ) + def __candles_channel_handler(self, subscription, *stream): + _labels = [ "MTS", "OPEN", "CLOSE", "HIGH", "LOW", "VOLUME" ] + + subscription = _get_sub_dictionary(subscription, [ "chanId", "key" ]) + + if all(isinstance(substream, list) for substream in stream[0]): + return self.event_emitter.emit( + "candles_snapshot", + subscription, + [ _label_stream_data(_labels, *substream) for substream in stream[0] ] + ) + + return self.event_emitter.emit( + "candles_update", + subscription, + _label_stream_data(_labels, *stream[0]) + ) + class AuthenticatedChannelsHandler(object): def __init__(self, event_emitter, strict = False): self.event_emitter, self.strict = event_emitter, strict @@ -304,13 +414,4 @@ class AuthenticatedChannelsHandler(object): self.event_emitter.emit("balance_update", _label_stream_data([ "AUM", "AUM_NET" - ], *stream)) - -def _label_stream_data(labels, *args, IGNORE = [ "_PLACEHOLDER" ]): - if len(labels) != len(args): - raise BfxWebsocketException(" and <*args> arguments should contain the same amount of elements.") - - return { label: args[index] for index, label in enumerate(labels) if label not in IGNORE } - -def _filter_dictionary_keys(dictionary, keys): - return { key: dictionary[key] for key in dictionary if key in keys } \ No newline at end of file + ], *stream)) \ No newline at end of file diff --git a/bfxapi/websocket/typings.py b/bfxapi/websocket/typings.py index baecc8a..bf432fb 100644 --- a/bfxapi/websocket/typings.py +++ b/bfxapi/websocket/typings.py @@ -2,7 +2,31 @@ from typing import Type, List, Dict, TypedDict, Union, Optional JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] -#region Type hinting for Websocket Public Channels +#region Type hinting for subscription objects + +TradingPairsTickerSubscription = TypedDict("TradingPairsTickerSubscription", { + "chanId": int, + "symbol": str, + "pair": str +}) + +FundingCurrenciesTickerSubscription = TypedDict("FundingCurrenciesTickerSubscription", { + "chanId": int, + "symbol": str, + "currency": str +}) + +TradingPairsTradesSubscription = TypedDict("TradingPairsTradesSubscription", { + "chanId": int, + "symbol": str, + "pair": str +}) + +FundingCurrenciesTradesSubscription = TypedDict("FundingCurrenciesTradesSubscription", { + "chanId": int, + "symbol": str, + "currency": str +}) BookSubscription = TypedDict("BookSubscription", { "chanId": int, @@ -14,6 +38,52 @@ BookSubscription = TypedDict("BookSubscription", { "pair": str }) +CandlesSubscription = TypedDict("CandlesSubscription", { + "chanId": int, + "key": str +}) + +#endregion + +#region Type hinting for Websocket Public Channels + +TradingPairTicker = TypedDict("TradingPairTicker", { + "BID": float, + "BID_SIZE": float, + "ASK": float, + "ASK_SIZE": float, + "DAILY_CHANGE": float, + "DAILY_CHANGE_RELATIVE": float, + "LAST_PRICE": float, + "VOLUME": float, + "HIGH": float, + "LOW": float +}) + +FundingCurrencyTicker = TypedDict("FundingCurrencyTicker", { + "FRR": float, + "BID": float, + "BID_PERIOD": int, + "BID_SIZE": float, + "ASK": float, + "ASK_PERIOD": int, + "ASK_SIZE": float, + "DAILY_CHANGE": float, + "DAILY_CHANGE_RELATIVE": float, + "LAST_PRICE": float, + "VOLUME": float, + "HIGH": float, + "LOW": float, + "FRR_AMOUNT_AVAILABLE": float +}) + +(TradingPairTrade, FundingCurrencyTrade) = ( + TypedDict("TradingPairTrade", { "ID": int, "MTS": int, "AMOUNT": float, "PRICE": float }), + TypedDict("FundingCurrencyTrade", { "ID": int, "MTS": int, "AMOUNT": float, "RATE": float, "PERIOD": int }) +) + +(TradingPairTrades, FundingCurrencyTrades) = (List[TradingPairTrade], List[FundingCurrencyTrade]) + (TradingPairBook, FundingCurrencyBook) = ( TypedDict("TradingPairBook", { "PRICE": float, "COUNT": int, "AMOUNT": float }), TypedDict("FundingCurrencyBook", { "RATE": float, "PERIOD": int, "COUNT": int, "AMOUNT": float }) @@ -36,6 +106,17 @@ RawBook = Union[TradingPairRawBook, FundingCurrencyRawBook] RawBooks = Union[TradingPairRawBooks, FundingCurrencyRawBooks] +Candle = TypedDict("Candle", { + "MTS": int, + "OPEN": float, + "CLOSE": float, + "HIGH": float, + "LOW": float, + "VOLUME": float +}) + +Candles = List[Candle] + #endregion #region Type hinting for Websocket Authenticated Channels From 7fcd2e0970d47b0484f2d2a0f321d69e89ffc73b Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 15 Nov 2022 18:06:21 +0100 Subject: [PATCH 017/265] Add OutdatedClientVersion exception in bfxapi/websocket/errors.py. Add check for version mismatch between client and server. Add support for error event. --- bfxapi/websocket/BfxWebsocketClient.py | 14 ++++++++++---- bfxapi/websocket/__init__.py | 2 +- bfxapi/websocket/errors.py | 7 +++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 628e316..32abb99 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -4,11 +4,13 @@ from pyee.asyncio import AsyncIOEventEmitter from .handlers import Channels, PublicChannelsHandler, AuthenticatedChannelsHandler -from .errors import BfxWebsocketException, ConnectionNotOpen, InvalidAuthenticationCredentials +from .errors import BfxWebsocketException, ConnectionNotOpen, InvalidAuthenticationCredentials, OutdatedClientVersion HEARTBEAT = "hb" class BfxWebsocketClient(object): + VERSION = 1 + def __init__(self, host, API_KEY=None, API_SECRET=None): self.host, self.chanIds, self.event_emitter = host, dict(), AsyncIOEventEmitter() @@ -32,7 +34,10 @@ class BfxWebsocketClient(object): async for message in websocket: message = json.loads(message) - if isinstance(message, dict) and message["event"] == "subscribed": + if isinstance(message, dict) and message["event"] == "info" and "version" in message: + if BfxWebsocketClient.VERSION != message["version"]: + raise OutdatedClientVersion(f"Mismatch between the client version and the server version. Update the library to the latest version to continue (client version: {BfxWebsocketClient.VERSION}, server version: {message['version']}).") + elif isinstance(message, dict) and message["event"] == "subscribed": self.chanIds[message["chanId"]] = message self.event_emitter.emit("subscribed", message) @@ -43,14 +48,15 @@ class BfxWebsocketClient(object): if message["status"] == "OK": self.event_emitter.emit("authenticated", message) else: raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.") + elif isinstance(message, dict) and message["event"] == "error": + self.event_emitter.emit("error", message["code"], message["msg"]) elif isinstance(message, list) and ((chanId := message[0]) or True) and message[1] != HEARTBEAT: if chanId == 0: self.handlers["authenticated"].handle(message[1], message[2]) else: self.handlers["public"].handle(self.chanIds[chanId], *message[1:]) except websockets.ConnectionClosed: continue - - @staticmethod + def __require_websocket_connection(function): async def wrapper(self, *args, **kwargs): if self.websocket == None or self.websocket.open == False: diff --git a/bfxapi/websocket/__init__.py b/bfxapi/websocket/__init__.py index c3aee02..5253ab5 100644 --- a/bfxapi/websocket/__init__.py +++ b/bfxapi/websocket/__init__.py @@ -1,3 +1,3 @@ from .BfxWebsocketClient import BfxWebsocketClient from .handlers import Channels -from .errors import BfxWebsocketException, ConnectionNotOpen, InvalidAuthenticationCredentials \ No newline at end of file +from .errors import BfxWebsocketException, ConnectionNotOpen, InvalidAuthenticationCredentials, OutdatedClientVersion \ No newline at end of file diff --git a/bfxapi/websocket/errors.py b/bfxapi/websocket/errors.py index d8f4b03..c5338d1 100644 --- a/bfxapi/websocket/errors.py +++ b/bfxapi/websocket/errors.py @@ -23,4 +23,11 @@ class InvalidAuthenticationCredentials(BfxWebsocketException): This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication. """ + pass + +class OutdatedClientVersion(BfxWebsocketException): + """ + This error indicates a mismatch between the client version and the server WSS version. + """ + pass \ No newline at end of file From d3715d3f9cd9d1444d122aed3d4d772156ddbd44 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 15 Nov 2022 18:37:17 +0100 Subject: [PATCH 018/265] Fix small bug in bfxapi/websocket/BfxWebsocketClient.py. --- bfxapi/websocket/BfxWebsocketClient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 32abb99..026bc74 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -9,7 +9,7 @@ from .errors import BfxWebsocketException, ConnectionNotOpen, InvalidAuthenticat HEARTBEAT = "hb" class BfxWebsocketClient(object): - VERSION = 1 + VERSION = 2 def __init__(self, host, API_KEY=None, API_SECRET=None): self.host, self.chanIds, self.event_emitter = host, dict(), AsyncIOEventEmitter() From 6448fd59b9aecd8e1f9a03f6a9e316700f2071c8 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 16 Nov 2022 18:33:33 +0100 Subject: [PATCH 019/265] Add bfxapi/websocket/serializers.py. Implement _Serializer internal class. Separate labeling from PublicChannelsHandler in handlers.py. --- bfxapi/websocket/handlers.py | 92 ++++++++++--------------------- bfxapi/websocket/serializers.py | 98 +++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 63 deletions(-) create mode 100644 bfxapi/websocket/serializers.py diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index ed406a3..c4331b8 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -1,5 +1,7 @@ from enum import Enum +from . import serializers + from .errors import BfxWebsocketException def _get_sub_dictionary(dictionary, keys): @@ -26,128 +28,92 @@ class PublicChannelsHandler(object): Channels.TICKER: self.__ticker_channel_handler, Channels.TRADES: self.__trades_channel_handler, Channels.BOOK: self.__book_channel_handler, - Channels.CANDLES: self.__candles_channel_handler + Channels.CANDLES: self.__candles_channel_handler, } def handle(self, subscription, *stream): - return self.__handlers[subscription["channel"]](subscription, *stream) + if channel := subscription["channel"] or channel in self.__handlers.keys(): + return self.__handlers[channel](subscription, *stream) def __ticker_channel_handler(self, subscription, *stream): - _trading_pairs_labels = [ - "BID", - "BID_SIZE", - "ASK", - "ASK_SIZE", - "DAILY_CHANGE", - "DAILY_CHANGE_RELATIVE", - "LAST_PRICE", - "VOLUME", - "HIGH", - "LOW" - ] - - _funding_currencies_labels = [ - "FRR", - "BID", - "BID_PERIOD", - "BID_SIZE", - "ASK", - "ASK_PERIOD", - "ASK_SIZE", - "DAILY_CHANGE", - "DAILY_CHANGE_RELATIVE", - "LAST_PRICE", - "VOLUME", - "HIGH", - "LOW" - "_PLACEHOLDER", - "_PLACEHOLDER", - "FRR_AMOUNT_AVAILABLE" - ] - if subscription["symbol"].startswith("t"): return self.event_emitter.emit( "tp_ticker_update", _get_sub_dictionary(subscription, [ "chanId", "symbol", "pair" ]), - _label_stream_data(_trading_pairs_labels, *stream[0]) + serializers.TradingPairTicker(*stream[0]) ) if subscription["symbol"].startswith("f"): return self.event_emitter.emit( "fc_ticker_update", _get_sub_dictionary(subscription, [ "chanId", "symbol", "currency" ]), - _label_stream_data(_funding_currencies_labels, *stream[0]) + serializers.FundingCurrencyTicker(*stream[0]) ) def __trades_channel_handler(self, subscription, *stream): - _trading_pairs_labels, _funding_currencies_labels = [ "ID", "MTS", "AMOUNT", "PRICE" ], [ "ID", "MTS", "AMOUNT", "RATE", "PERIOD" ] - - if len(stream) == 1: - if subscription["symbol"].startswith("t"): - return self.event_emitter.emit( - "tp_trades_snapshot", - _get_sub_dictionary(subscription, [ "chanId", "symbol", "pair" ]), - [ _label_stream_data(_trading_pairs_labels, *substream) for substream in stream[0] ] - ) - - if subscription["symbol"].startswith("f"): - return self.event_emitter.emit( - "fc_trades_snapshot", - _get_sub_dictionary(subscription, [ "chanId", "symbol", "currency" ]), - [ _label_stream_data(_funding_currencies_labels, *substream) for substream in stream[0] ] - ) - if type := stream[0] or type in [ "te", "tu", "fte", "ftu" ]: if subscription["symbol"].startswith("t"): return self.event_emitter.emit( { "te": "tp_trade_executed", "tu": "tp_trade_execution_update" }[type], _get_sub_dictionary(subscription, [ "chanId", "symbol", "pair" ]), - _label_stream_data(_trading_pairs_labels, *stream[1]) + serializers.TradingPairTrade(*stream[1]) ) if subscription["symbol"].startswith("f"): return self.event_emitter.emit( { "fte": "fc_trade_executed", "ftu": "fc_trade_execution_update" }[type], _get_sub_dictionary(subscription, [ "chanId", "symbol", "currency" ]), - _label_stream_data(_funding_currencies_labels, *stream[1]) + serializers.FundingCurrencyTrade(*stream[1]) ) + if subscription["symbol"].startswith("t"): + return self.event_emitter.emit( + "tp_trades_snapshot", + _get_sub_dictionary(subscription, [ "chanId", "symbol", "pair" ]), + [ serializers.TradingPairTrade(*substream) for substream in stream[0] ] + ) + + if subscription["symbol"].startswith("f"): + return self.event_emitter.emit( + "fc_trades_snapshot", + _get_sub_dictionary(subscription, [ "chanId", "symbol", "currency" ]), + [ serializers.FundingCurrencyTrade(*substream) for substream in stream[0] ] + ) + def __book_channel_handler(self, subscription, *stream): subscription = _get_sub_dictionary(subscription, [ "chanId", "symbol", "prec", "freq", "len", "subId", "pair" ]) if subscription["prec"] == "R0": - _trading_pairs_labels, _funding_currencies_labels, IS_RAW_BOOK = [ "ORDER_ID", "PRICE", "AMOUNT" ], [ "OFFER_ID", "PERIOD", "RATE", "AMOUNT" ], True - else: _trading_pairs_labels, _funding_currencies_labels, IS_RAW_BOOK = [ "PRICE", "COUNT", "AMOUNT" ], [ "RATE", "PERIOD", "COUNT", "AMOUNT" ], False + _trading_pair_serializer, _funding_currency_serializer, IS_RAW_BOOK = serializers.TradingPairRawBook, serializers.FundingCurrencyRawBook, True + else: _trading_pair_serializer, _funding_currency_serializer, IS_RAW_BOOK = serializers.TradingPairBook, serializers.FundingCurrencyBook, False if all(isinstance(substream, list) for substream in stream[0]): return self.event_emitter.emit( IS_RAW_BOOK and "raw_book_snapshot" or "book_snapshot", subscription, - [ _label_stream_data({ "t": _trading_pairs_labels, "f": _funding_currencies_labels }[subscription["symbol"][0]], *substream) for substream in stream[0] ] + [ { "t": _trading_pair_serializer, "f": _funding_currency_serializer }[subscription["symbol"][0]](*substream) for substream in stream[0] ] ) return self.event_emitter.emit( IS_RAW_BOOK and "raw_book_update" or "book_update", subscription, - _label_stream_data({ "t": _trading_pairs_labels, "f": _funding_currencies_labels }[subscription["symbol"][0]], *stream[0]) + { "t": _trading_pair_serializer, "f": _funding_currency_serializer }[subscription["symbol"][0]](*stream[0]) ) def __candles_channel_handler(self, subscription, *stream): - _labels = [ "MTS", "OPEN", "CLOSE", "HIGH", "LOW", "VOLUME" ] - subscription = _get_sub_dictionary(subscription, [ "chanId", "key" ]) if all(isinstance(substream, list) for substream in stream[0]): return self.event_emitter.emit( "candles_snapshot", subscription, - [ _label_stream_data(_labels, *substream) for substream in stream[0] ] + [ serializers.Candle(*substream) for substream in stream[0] ] ) return self.event_emitter.emit( "candles_update", subscription, - _label_stream_data(_labels, *stream[0]) + serializers.Candle(*stream[0]) ) class AuthenticatedChannelsHandler(object): diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py new file mode 100644 index 0000000..448e17f --- /dev/null +++ b/bfxapi/websocket/serializers.py @@ -0,0 +1,98 @@ +from .errors import BfxWebsocketException + +class _Serializer(object): + def __init__(self, name, labels): + self.name, self.__labels = name, labels + + def __serialize(self, *args, IGNORE = [ "_PLACEHOLDER" ]): + if len(self.__labels) != len(args): + raise BfxWebsocketException(" and <*args> arguments should contain the same amount of elements.") + + for index, label in enumerate(self.__labels): + if label not in IGNORE: + yield label, args[index] + + def __call__(self, *values): + return dict(self.__serialize(*values)) + +TradingPairTicker = _Serializer("TradingPairTicker", labels=[ + "BID", + "BID_SIZE", + "ASK", + "ASK_SIZE", + "DAILY_CHANGE", + "DAILY_CHANGE_RELATIVE", + "LAST_PRICE", + "VOLUME", + "HIGH", + "LOW" +]) + +FundingCurrencyTicker = _Serializer("FundingCurrencyTicker", labels=[ + "FRR", + "BID", + "BID_PERIOD", + "BID_SIZE", + "ASK", + "ASK_PERIOD", + "ASK_SIZE", + "DAILY_CHANGE", + "DAILY_CHANGE_RELATIVE", + "LAST_PRICE", + "VOLUME", + "HIGH", + "LOW" + "_PLACEHOLDER", + "_PLACEHOLDER", + "FRR_AMOUNT_AVAILABLE" +]) + +TradingPairTrade = _Serializer("TradingPairTrade", labels=[ + "ID", + "MTS", + "AMOUNT", + "PRICE" +]) + +FundingCurrencyTrade = _Serializer("FundingCurrencyTrade", labels=[ + "ID", + "MTS", + "AMOUNT", + "RATE", + "PERIOD" +]) + +TradingPairBook = _Serializer("TradingPairBook", labels=[ + "PRICE", + "COUNT", + "AMOUNT" +]) + +FundingCurrencyBook = _Serializer("FundingCurrencyBook", labels=[ + "RATE", + "PERIOD", + "COUNT", + "AMOUNT" +]) + +TradingPairRawBook = _Serializer("TradingPairRawBook", labels=[ + "ORDER_ID", + "PRICE", + "AMOUNT" +]) + +FundingCurrencyRawBook = _Serializer("FundingCurrencyRawBook", labels=[ + "OFFER_ID", + "PERIOD", + "RATE", + "AMOUNT" +]) + +Candle = _Serializer("Candle", labels=[ + "MTS", + "OPEN", + "CLOSE", + "HIGH", + "LOW", + "VOLUME" +]) \ No newline at end of file From 41aa49d2bb3530846b204736c13551d35a68d069 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 16 Nov 2022 18:45:25 +0100 Subject: [PATCH 020/265] Add derivatives_status_update event in handlers.py. Add DerivativesStatus in serializers.py and typings.py. --- bfxapi/websocket/handlers.py | 11 +++++++++++ bfxapi/websocket/serializers.py | 26 ++++++++++++++++++++++++++ bfxapi/websocket/typings.py | 15 +++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index c4331b8..34817af 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -29,6 +29,7 @@ class PublicChannelsHandler(object): Channels.TRADES: self.__trades_channel_handler, Channels.BOOK: self.__book_channel_handler, Channels.CANDLES: self.__candles_channel_handler, + Channels.STATUS: self.__status_channel_handler } def handle(self, subscription, *stream): @@ -116,6 +117,16 @@ class PublicChannelsHandler(object): serializers.Candle(*stream[0]) ) + def __status_channel_handler(self, subscription, *stream): + subscription = _get_sub_dictionary(subscription, [ "chanId", "key" ]) + + if subscription["key"].startswith("deriv:"): + return self.event_emitter.emit( + "derivatives_status_update", + subscription, + serializers.DerivativesStatus(*stream[0]) + ) + class AuthenticatedChannelsHandler(object): def __init__(self, event_emitter, strict = False): self.event_emitter, self.strict = event_emitter, strict diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index 448e17f..f5fcb64 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -95,4 +95,30 @@ Candle = _Serializer("Candle", labels=[ "HIGH", "LOW", "VOLUME" +]) + +DerivativesStatus = _Serializer("DerivativesStatus", labels=[ + "TIME_MS", + "_PLACEHOLDER", + "DERIV_PRICE", + "SPOT_PRICE", + "_PLACEHOLDER", + "INSURANCE_FUND_BALANCE", + "_PLACEHOLDER", + "NEXT_FUNDING_EVT_TIMESTAMP_MS", + "NEXT_FUNDING_ACCRUED", + "NEXT_FUNDING_STEP", + "_PLACEHOLDER", + "CURRENT_FUNDING" + "_PLACEHOLDER", + "_PLACEHOLDER", + "MARK_PRICE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "OPEN_INTEREST", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "CLAMP_MIN", + "CLAMP_MAX" ]) \ No newline at end of file diff --git a/bfxapi/websocket/typings.py b/bfxapi/websocket/typings.py index bf432fb..5d4f53d 100644 --- a/bfxapi/websocket/typings.py +++ b/bfxapi/websocket/typings.py @@ -117,6 +117,21 @@ Candle = TypedDict("Candle", { Candles = List[Candle] +DerivativesStatus = TypedDict("DerivativesStatus", { + "TIME_MS": int, + "DERIV_PRICE": float, + "SPOT_PRICE": float, + "INSURANCE_FUND_BALANCE": float, + "NEXT_FUNDING_EVT_TIMESTAMP_MS": int, + "NEXT_FUNDING_ACCRUED": float, + "NEXT_FUNDING_STEP": int, + "CURRENT_FUNDING": float, + "MARK_PRICE": float, + "OPEN_INTEREST": float, + "CLAMP_MIN": float, + "CLAMP_MAX": float +}) + #endregion #region Type hinting for Websocket Authenticated Channels From d58c60b02d3499ca72f862638270461a0b862404 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 16 Nov 2022 19:01:52 +0100 Subject: [PATCH 021/265] Add EventNotSupported exception in errors.py. --- bfxapi/websocket/BfxWebsocketClient.py | 14 +++++++++++++- bfxapi/websocket/__init__.py | 2 +- bfxapi/websocket/errors.py | 7 +++++++ bfxapi/websocket/handlers.py | 21 ++++++++++++++++++++- bfxapi/websocket/serializers.py | 6 +++++- 5 files changed, 46 insertions(+), 4 deletions(-) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 026bc74..afb24a3 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -4,13 +4,19 @@ from pyee.asyncio import AsyncIOEventEmitter from .handlers import Channels, PublicChannelsHandler, AuthenticatedChannelsHandler -from .errors import BfxWebsocketException, ConnectionNotOpen, InvalidAuthenticationCredentials, OutdatedClientVersion +from .errors import BfxWebsocketException, ConnectionNotOpen, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion HEARTBEAT = "hb" class BfxWebsocketClient(object): VERSION = 2 + EVENTS = [ + "open", "subscribed", "authenticated", "error", + *PublicChannelsHandler.EVENTS, + *AuthenticatedChannelsHandler.EVENTS + ] + def __init__(self, host, API_KEY=None, API_SECRET=None): self.host, self.chanIds, self.event_emitter = host, dict(), AsyncIOEventEmitter() @@ -102,12 +108,18 @@ class BfxWebsocketClient(object): await self.unsubscribe(chanId) def on(self, event): + if event not in BfxWebsocketClient.EVENTS: + raise EventNotSupported(f"Event <{event}> is not supported. To get a list of available events use BfxWebsocketClient.EVENTS.") + def handler(function): self.event_emitter.on(event, function) return handler def once(self, event): + if event not in BfxWebsocketClient.EVENTS: + raise EventNotSupported(f"Event <{event}> is not supported. To get a list of available events use BfxWebsocketClient.EVENTS.") + def handler(function): self.event_emitter.once(event, function) diff --git a/bfxapi/websocket/__init__.py b/bfxapi/websocket/__init__.py index 5253ab5..2a4bfa0 100644 --- a/bfxapi/websocket/__init__.py +++ b/bfxapi/websocket/__init__.py @@ -1,3 +1,3 @@ from .BfxWebsocketClient import BfxWebsocketClient from .handlers import Channels -from .errors import BfxWebsocketException, ConnectionNotOpen, InvalidAuthenticationCredentials, OutdatedClientVersion \ No newline at end of file +from .errors import BfxWebsocketException, ConnectionNotOpen, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion \ No newline at end of file diff --git a/bfxapi/websocket/errors.py b/bfxapi/websocket/errors.py index c5338d1..6a45a94 100644 --- a/bfxapi/websocket/errors.py +++ b/bfxapi/websocket/errors.py @@ -25,6 +25,13 @@ class InvalidAuthenticationCredentials(BfxWebsocketException): pass +class EventNotSupported(BfxWebsocketException): + """ + This error indicates a failed attempt to subscribe to an event not supported by the BfxWebsocketClient. + """ + + pass + class OutdatedClientVersion(BfxWebsocketException): """ This error indicates a mismatch between the client version and the server WSS version. diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index 34817af..6bdc1ec 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -21,6 +21,14 @@ class Channels(str, Enum): STATUS = "status" class PublicChannelsHandler(object): + EVENTS = [ + "tp_ticker_update", "fc_ticker_update", + "tp_trade_executed", "tp_trade_execution_update", "fc_trade_executed", "fc_trade_execution_update", "tp_trades_snapshot", "fc_trades_snapshot", + "book_snapshot", "raw_book_snapshot", "book_update", "raw_book_update", + "candles_snapshot", "candles_update", + "derivatives_status_update" + ] + def __init__(self, event_emitter): self.event_emitter = event_emitter @@ -126,8 +134,19 @@ class PublicChannelsHandler(object): subscription, serializers.DerivativesStatus(*stream[0]) ) - + class AuthenticatedChannelsHandler(object): + EVENTS = [ + "order_snapshot", "new_order", "order_update", "order_cancel", + "position_snapshot", "new_position", "position_update", "position_close", + "trade_executed", "trade_execution_update", + "funding_offer_snapshot", "funding_offer_new", "funding_offer_update", "funding_offer_cancel", + "funding_credit_snapshot", "funding_credit_new", "funding_credit_update", "funding_credit_close", + "funding_loan_snapshot", "funding_loan_new", "funding_loan_update", "funding_loan_close", + "wallet_snapshot", "wallet_update", + "balance_update" + ] + def __init__(self, event_emitter, strict = False): self.event_emitter, self.strict = event_emitter, strict diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index f5fcb64..cca4cbe 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -15,6 +15,8 @@ class _Serializer(object): def __call__(self, *values): return dict(self.__serialize(*values)) +#region Serializers definition for Websocket Public Channels + TradingPairTicker = _Serializer("TradingPairTicker", labels=[ "BID", "BID_SIZE", @@ -121,4 +123,6 @@ DerivativesStatus = _Serializer("DerivativesStatus", labels=[ "_PLACEHOLDER", "CLAMP_MIN", "CLAMP_MAX" -]) \ No newline at end of file +]) + +#endregion \ No newline at end of file From 1cda4fcb3c31548bdc2e2174585943d0e8bd31ea Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 17 Nov 2022 16:54:32 +0100 Subject: [PATCH 022/265] Wrap type hinting for subscription objects inside Subscriptions namespace class. Update _Serializer class in serializers.py. Separate Books and Raw Books channels into tp_ and fc_ versions. --- bfxapi/websocket/handlers.py | 36 ++++++++------- bfxapi/websocket/serializers.py | 2 +- bfxapi/websocket/typings.py | 80 ++++++++++++++++----------------- 3 files changed, 59 insertions(+), 59 deletions(-) diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index 6bdc1ec..c49fc57 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -24,9 +24,9 @@ class PublicChannelsHandler(object): EVENTS = [ "tp_ticker_update", "fc_ticker_update", "tp_trade_executed", "tp_trade_execution_update", "fc_trade_executed", "fc_trade_execution_update", "tp_trades_snapshot", "fc_trades_snapshot", - "book_snapshot", "raw_book_snapshot", "book_update", "raw_book_update", + "tp_book_snapshot", "fc_book_snapshot", "tp_raw_book_snapshot", "fc_raw_book_snapshot", "tp_book_update", "fc_book_update", "tp_raw_book_update", "fc_raw_book_update", "candles_snapshot", "candles_update", - "derivatives_status_update" + "derivatives_status_update", ] def __init__(self, event_emitter): @@ -49,14 +49,14 @@ class PublicChannelsHandler(object): return self.event_emitter.emit( "tp_ticker_update", _get_sub_dictionary(subscription, [ "chanId", "symbol", "pair" ]), - serializers.TradingPairTicker(*stream[0]) + serializers.TradingPairTicker.parse(*stream[0]) ) if subscription["symbol"].startswith("f"): return self.event_emitter.emit( "fc_ticker_update", _get_sub_dictionary(subscription, [ "chanId", "symbol", "currency" ]), - serializers.FundingCurrencyTicker(*stream[0]) + serializers.FundingCurrencyTicker.parse(*stream[0]) ) def __trades_channel_handler(self, subscription, *stream): @@ -65,48 +65,50 @@ class PublicChannelsHandler(object): return self.event_emitter.emit( { "te": "tp_trade_executed", "tu": "tp_trade_execution_update" }[type], _get_sub_dictionary(subscription, [ "chanId", "symbol", "pair" ]), - serializers.TradingPairTrade(*stream[1]) + serializers.TradingPairTrade.parse(*stream[1]) ) if subscription["symbol"].startswith("f"): return self.event_emitter.emit( { "fte": "fc_trade_executed", "ftu": "fc_trade_execution_update" }[type], _get_sub_dictionary(subscription, [ "chanId", "symbol", "currency" ]), - serializers.FundingCurrencyTrade(*stream[1]) + serializers.FundingCurrencyTrade.parse(*stream[1]) ) if subscription["symbol"].startswith("t"): return self.event_emitter.emit( "tp_trades_snapshot", _get_sub_dictionary(subscription, [ "chanId", "symbol", "pair" ]), - [ serializers.TradingPairTrade(*substream) for substream in stream[0] ] + [ serializers.TradingPairTrade.parse(*substream) for substream in stream[0] ] ) if subscription["symbol"].startswith("f"): return self.event_emitter.emit( "fc_trades_snapshot", _get_sub_dictionary(subscription, [ "chanId", "symbol", "currency" ]), - [ serializers.FundingCurrencyTrade(*substream) for substream in stream[0] ] + [ serializers.FundingCurrencyTrade.parse(*substream) for substream in stream[0] ] ) def __book_channel_handler(self, subscription, *stream): subscription = _get_sub_dictionary(subscription, [ "chanId", "symbol", "prec", "freq", "len", "subId", "pair" ]) + type = subscription["symbol"][0] + if subscription["prec"] == "R0": _trading_pair_serializer, _funding_currency_serializer, IS_RAW_BOOK = serializers.TradingPairRawBook, serializers.FundingCurrencyRawBook, True else: _trading_pair_serializer, _funding_currency_serializer, IS_RAW_BOOK = serializers.TradingPairBook, serializers.FundingCurrencyBook, False if all(isinstance(substream, list) for substream in stream[0]): - return self.event_emitter.emit( - IS_RAW_BOOK and "raw_book_snapshot" or "book_snapshot", + return self.event_emitter.emit( + { "t": "tp_", "f": "fc_" }[type] + (IS_RAW_BOOK and "raw_book" or "book") + "_snapshot", subscription, - [ { "t": _trading_pair_serializer, "f": _funding_currency_serializer }[subscription["symbol"][0]](*substream) for substream in stream[0] ] + [ { "t": _trading_pair_serializer, "f": _funding_currency_serializer }[type].parse(*substream) for substream in stream[0] ] ) return self.event_emitter.emit( - IS_RAW_BOOK and "raw_book_update" or "book_update", + { "t": "tp_", "f": "fc_" }[type] + (IS_RAW_BOOK and "raw_book" or "book") + "_update", subscription, - { "t": _trading_pair_serializer, "f": _funding_currency_serializer }[subscription["symbol"][0]](*stream[0]) + { "t": _trading_pair_serializer, "f": _funding_currency_serializer }[type].parse(*stream[0]) ) def __candles_channel_handler(self, subscription, *stream): @@ -116,13 +118,13 @@ class PublicChannelsHandler(object): return self.event_emitter.emit( "candles_snapshot", subscription, - [ serializers.Candle(*substream) for substream in stream[0] ] + [ serializers.Candle.parse(*substream) for substream in stream[0] ] ) return self.event_emitter.emit( "candles_update", subscription, - serializers.Candle(*stream[0]) + serializers.Candle.parse(*stream[0]) ) def __status_channel_handler(self, subscription, *stream): @@ -132,7 +134,7 @@ class PublicChannelsHandler(object): return self.event_emitter.emit( "derivatives_status_update", subscription, - serializers.DerivativesStatus(*stream[0]) + serializers.DerivativesStatus.parse(*stream[0]) ) class AuthenticatedChannelsHandler(object): @@ -144,7 +146,7 @@ class AuthenticatedChannelsHandler(object): "funding_credit_snapshot", "funding_credit_new", "funding_credit_update", "funding_credit_close", "funding_loan_snapshot", "funding_loan_new", "funding_loan_update", "funding_loan_close", "wallet_snapshot", "wallet_update", - "balance_update" + "balance_update", ] def __init__(self, event_emitter, strict = False): diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index cca4cbe..3fc7041 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -12,7 +12,7 @@ class _Serializer(object): if label not in IGNORE: yield label, args[index] - def __call__(self, *values): + def parse(self, *values): return dict(self.__serialize(*values)) #region Serializers definition for Websocket Public Channels diff --git a/bfxapi/websocket/typings.py b/bfxapi/websocket/typings.py index 5d4f53d..490d3c5 100644 --- a/bfxapi/websocket/typings.py +++ b/bfxapi/websocket/typings.py @@ -4,44 +4,50 @@ JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] #region Type hinting for subscription objects -TradingPairsTickerSubscription = TypedDict("TradingPairsTickerSubscription", { - "chanId": int, - "symbol": str, - "pair": str -}) +class Subscriptions: + TradingPairsTicker = TypedDict("Subscriptions.TradingPairsTicker", { + "chanId": int, + "symbol": str, + "pair": str + }) -FundingCurrenciesTickerSubscription = TypedDict("FundingCurrenciesTickerSubscription", { - "chanId": int, - "symbol": str, - "currency": str -}) + FundingCurrenciesTicker = TypedDict("Subscriptions.FundingCurrenciesTicker", { + "chanId": int, + "symbol": str, + "currency": str + }) -TradingPairsTradesSubscription = TypedDict("TradingPairsTradesSubscription", { - "chanId": int, - "symbol": str, - "pair": str -}) + TradingPairsTrades = TypedDict("Subscriptions.TradingPairsTrades", { + "chanId": int, + "symbol": str, + "pair": str + }) -FundingCurrenciesTradesSubscription = TypedDict("FundingCurrenciesTradesSubscription", { - "chanId": int, - "symbol": str, - "currency": str -}) + FundingCurrenciesTrades = TypedDict("Subscriptions.FundingCurrenciesTrades", { + "chanId": int, + "symbol": str, + "currency": str + }) -BookSubscription = TypedDict("BookSubscription", { - "chanId": int, - "symbol": str, - "prec": str, - "freq": str, - "len": str, - "subId": int, - "pair": str -}) + Book = TypedDict("Subscriptions.Book", { + "chanId": int, + "symbol": str, + "prec": str, + "freq": str, + "len": str, + "subId": int, + "pair": str + }) -CandlesSubscription = TypedDict("CandlesSubscription", { - "chanId": int, - "key": str -}) + Candles = TypedDict("Subscriptions.Candles", { + "chanId": int, + "key": str + }) + + DerivativesStatus = TypedDict("Subscriptions.DerivativesStatus", { + "chanId": int, + "key": str + }) #endregion @@ -91,10 +97,6 @@ FundingCurrencyTicker = TypedDict("FundingCurrencyTicker", { (TradingPairBooks, FundingCurrencyBooks) = (List[TradingPairBook], List[FundingCurrencyBook]) -Book = Union[TradingPairBook, FundingCurrencyBook] - -Books = Union[TradingPairBooks, FundingCurrencyBooks] - (TradingPairRawBook, FundingCurrencyRawBook) = ( TypedDict("TradingPairRawBook", { "ORDER_ID": int, "PRICE": float, "AMOUNT": float }), TypedDict("FundingCurrencyRawBook", { "OFFER_ID": int, "PERIOD": int, "RATE": float, "AMOUNT": float }), @@ -102,10 +104,6 @@ Books = Union[TradingPairBooks, FundingCurrencyBooks] (TradingPairRawBooks, FundingCurrencyRawBooks) = (List[TradingPairRawBook], List[FundingCurrencyRawBook]) -RawBook = Union[TradingPairRawBook, FundingCurrencyRawBook] - -RawBooks = Union[TradingPairRawBooks, FundingCurrencyRawBooks] - Candle = TypedDict("Candle", { "MTS": int, "OPEN": float, From 971e4759fa760b55e16b8b5a9cc1599f9578a09a Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 17 Nov 2022 17:44:18 +0100 Subject: [PATCH 023/265] Add and implement serializers for websocket authenticated channels in bfxapi/websocket/handlers.py. --- bfxapi/websocket/handlers.py | 232 +++++--------------------------- bfxapi/websocket/serializers.py | 180 +++++++++++++++++++++++++ bfxapi/websocket/typings.py | 22 +-- 3 files changed, 228 insertions(+), 206 deletions(-) diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index c49fc57..aebe66c 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -172,244 +172,86 @@ class AuthenticatedChannelsHandler(object): raise BfxWebsocketException(f"Event of type <{type}> not found in self.__handlers.") def __orders_channel_handler(self, type, stream): - _labels = [ - "ID", - "GID", - "CID", - "SYMBOL", - "MTS_CREATE", - "MTS_UPDATE", - "AMOUNT", - "AMOUNT_ORIG", - "ORDER_TYPE", - "TYPE_PREV", - "MTS_TIF", - "_PLACEHOLDER", - "FLAGS", - "ORDER_STATUS", - "_PLACEHOLDER", - "_PLACEHOLDER", - "PRICE", - "PRICE_AVG", - "PRICE_TRAILING", - "PRICE_AUX_LIMIT", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "NOTIFY", - "HIDDEN", - "PLACED_ID", - "_PLACEHOLDER", - "_PLACEHOLDER", - "ROUTING", - "_PLACEHOLDER", - "_PLACEHOLDER", - "META" - ] - if type == "os": - self.event_emitter.emit("order_snapshot", [ _label_stream_data(_labels, *substream) for substream in stream ]) + return self.event_emitter.emit("order_snapshot", [ + serializers.Order.parse(*substream) for substream in stream + ]) - if type == "on" or type == "ou" or type == "oc": - self.event_emitter.emit({ + if type in [ "on", "ou", "oc" ]: + return self.event_emitter.emit({ "on": "new_order", "ou": "order_update", "oc": "order_cancel" - }[type], _label_stream_data(_labels, *stream)) + }[type], serializers.Order.parse(*stream)) def __positions_channel_handler(self, type, stream): - _labels = [ - "SYMBOL", - "STATUS", - "AMOUNT", - "BASE_PRICE", - "MARGIN_FUNDING", - "MARGIN_FUNDING_TYPE", - "PL", - "PL_PERC", - "PRICE_LIQ", - "LEVERAGE", - "FLAG", - "POSITION_ID", - "MTS_CREATE", - "MTS_UPDATE", - "_PLACEHOLDER", - "TYPE", - "_PLACEHOLDER", - "COLLATERAL", - "COLLATERAL_MIN", - "META" - ] - if type == "ps": - self.event_emitter.emit("position_snapshot", [ _label_stream_data(_labels, *substream) for substream in stream ]) + return self.event_emitter.emit("position_snapshot", [ + serializers.Position.parse(*substream) for substream in stream + ]) - if type == "pn" or type == "pu" or type == "pc": - self.event_emitter.emit({ + if type in [ "pn", "pu", "pc" ]: + return self.event_emitter.emit({ "pn": "new_position", "pu": "position_update", "pc": "position_close" - }[type], _label_stream_data(_labels, *stream)) + }[type], serializers.Position.parse(*stream)) def __trades_channel_handler(self, type, stream): if type == "te": - self.event_emitter.emit("trade_executed", _label_stream_data([ - "ID", - "SYMBOL", - "MTS_CREATE", - "ORDER_ID", - "EXEC_AMOUNT", - "EXEC_PRICE", - "ORDER_TYPE", - "ORDER_PRICE", - "MAKER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "CID" - ], *stream)) + self.event_emitter.emit("trade_executed", serializers.TradeExecuted.parse(*stream)) if type == "tu": - self.event_emitter.emit("trade_execution_update", _label_stream_data([ - "ID", - "SYMBOL", - "MTS_CREATE", - "ORDER_ID", - "EXEC_AMOUNT", - "EXEC_PRICE", - "ORDER_TYPE", - "ORDER_PRICE", - "MAKER", - "FEE", - "FEE_CURRENCY", - "CID" - ], *stream)) + self.event_emitter.emit("trade_execution_update", serializers.TradeExecutionUpdate.parse(*stream)) def __funding_offers_channel_handler(self, type, stream): - _labels = [ - "ID", - "SYMBOL", - "MTS_CREATED", - "MTS_UPDATED", - "AMOUNT", - "AMOUNT_ORIG", - "OFFER_TYPE", - "_PLACEHOLDER", - "_PLACEHOLDER", - "FLAGS", - "STATUS", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "RATE", - "PERIOD", - "NOTIFY", - "HIDDEN", - "_PLACEHOLDER", - "RENEW", - "_PLACEHOLDER", - ] - if type == "fos": - self.event_emitter.emit("funding_offer_snapshot", [ _label_stream_data(_labels, *substream) for substream in stream ]) + return self.event_emitter.emit("funding_offer_snapshot", [ + serializers.FundingOffer.parse(*substream) for substream in stream + ]) - if type == "fon" or type == "fou" or type == "foc": - self.event_emitter.emit({ + if type in [ "fon", "fou", "foc" ]: + return self.event_emitter.emit({ "fon": "funding_offer_new", "fou": "funding_offer_update", "foc": "funding_offer_cancel" - }[type], _label_stream_data(_labels, *stream)) + }[type], serializers.FundingOffer.parse(*stream)) def __funding_credits_channel_handler(self, type, stream): - _labels = [ - "ID", - "SYMBOL", - "SIDE", - "MTS_CREATE", - "MTS_UPDATE", - "AMOUNT", - "FLAGS", - "STATUS", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "RATE", - "PERIOD", - "MTS_OPENING", - "MTS_LAST_PAYOUT", - "NOTIFY", - "HIDDEN", - "_PLACEHOLDER", - "RENEW", - "RATE_REAL", - "NO_CLOSE", - "POSITION_PAIR" - ] - if type == "fcs": - self.event_emitter.emit("funding_credit_snapshot", [ _label_stream_data(_labels, *substream) for substream in stream ]) + return self.event_emitter.emit("funding_credit_snapshot", [ + serializers.FundingCredit.parse(*substream) for substream in stream + ]) - if type == "fcn" or type == "fcu" or type == "fcc": - self.event_emitter.emit({ + if type in [ "fcn", "fcu", "fcc" ]: + return self.event_emitter.emit({ "fcn": "funding_credit_new", "fcu": "funding_credit_update", "fcc": "funding_credit_close" - }[type], _label_stream_data(_labels, *stream)) + }[type], serializers.FundingCredit.parse(*stream)) def __funding_loans_channel_handler(self, type, stream): - _labels = [ - "ID", - "SYMBOL", - "SIDE", - "MTS_CREATE", - "MTS_UPDATE", - "AMOUNT", - "FLAGS", - "STATUS", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "RATE", - "PERIOD", - "MTS_OPENING", - "MTS_LAST_PAYOUT", - "NOTIFY", - "HIDDEN", - "_PLACEHOLDER", - "RENEW", - "RATE_REAL", - "NO_CLOSE" - ] - if type == "fls": - self.event_emitter.emit("funding_loan_snapshot", [ _label_stream_data(_labels, *substream) for substream in stream ]) + return self.event_emitter.emit("funding_loan_snapshot", [ + serializers.FundingLoan.parse(*substream) for substream in stream + ]) - if type == "fln" or type == "flu" or type == "flc": - self.event_emitter.emit({ + if type in [ "fln", "flu", "flc" ]: + return self.event_emitter.emit({ "fln": "funding_loan_new", "flu": "funding_loan_update", "flc": "funding_loan_close" - }[type], _label_stream_data(_labels, *stream)) + }[type], serializers.FundingLoan.parse(*stream)) def __wallets_channel_handler(self, type, stream): - _labels = [ - "WALLET_TYPE", - "CURRENCY", - "BALANCE", - "UNSETTLED_INTEREST", - "BALANCE_AVAILABLE", - "DESCRIPTION", - "META" - ] - if type == "ws": - self.event_emitter.emit("wallet_snapshot", [ _label_stream_data(_labels, *substream) for substream in stream ]) + return self.event_emitter.emit("wallet_snapshot", [ + serializers.Wallet.parse(*substream) for substream in stream + ]) if type == "wu": - self.event_emitter.emit("wallet_update", _label_stream_data(_labels, *stream)) + return self.event_emitter.emit("wallet_update", serializers.Wallet.parse(*stream)) def __balance_info_channel_handler(self, type, stream): if type == "bu": - self.event_emitter.emit("balance_update", _label_stream_data([ - "AUM", - "AUM_NET" - ], *stream)) \ No newline at end of file + return self.event_emitter.emit("balance_update", serializers.BalanceInfo.parse(*stream)) \ No newline at end of file diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index 3fc7041..47cd770 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -125,4 +125,184 @@ DerivativesStatus = _Serializer("DerivativesStatus", labels=[ "CLAMP_MAX" ]) +#endregion + +#region Serializers definition for Websocket Authenticated Channels + +Order = _Serializer("Order", labels=[ + "ID", + "GID", + "CID", + "SYMBOL", + "MTS_CREATE", + "MTS_UPDATE", + "AMOUNT", + "AMOUNT_ORIG", + "ORDER_TYPE", + "TYPE_PREV", + "MTS_TIF", + "_PLACEHOLDER", + "FLAGS", + "ORDER_STATUS", + "_PLACEHOLDER", + "_PLACEHOLDER", + "PRICE", + "PRICE_AVG", + "PRICE_TRAILING", + "PRICE_AUX_LIMIT", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "NOTIFY", + "HIDDEN", + "PLACED_ID", + "_PLACEHOLDER", + "_PLACEHOLDER", + "ROUTING", + "_PLACEHOLDER", + "_PLACEHOLDER", + "META" +]) + +Position = _Serializer("Position", labels=[ + "SYMBOL", + "STATUS", + "AMOUNT", + "BASE_PRICE", + "MARGIN_FUNDING", + "MARGIN_FUNDING_TYPE", + "PL", + "PL_PERC", + "PRICE_LIQ", + "LEVERAGE", + "FLAG", + "POSITION_ID", + "MTS_CREATE", + "MTS_UPDATE", + "_PLACEHOLDER", + "TYPE", + "_PLACEHOLDER", + "COLLATERAL", + "COLLATERAL_MIN", + "META" +]) + +TradeExecuted = _Serializer("TradeExecuted", labels=[ + "ID", + "SYMBOL", + "MTS_CREATE", + "ORDER_ID", + "EXEC_AMOUNT", + "EXEC_PRICE", + "ORDER_TYPE", + "ORDER_PRICE", + "MAKER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "CID" +]) + +TradeExecutionUpdate = _Serializer("TradeExecutionUpdate", labels=[ + "ID", + "SYMBOL", + "MTS_CREATE", + "ORDER_ID", + "EXEC_AMOUNT", + "EXEC_PRICE", + "ORDER_TYPE", + "ORDER_PRICE", + "MAKER", + "FEE", + "FEE_CURRENCY", + "CID" +]) + +FundingOffer = _Serializer("FundingOffer", labels=[ + "ID", + "SYMBOL", + "MTS_CREATED", + "MTS_UPDATED", + "AMOUNT", + "AMOUNT_ORIG", + "OFFER_TYPE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "FLAGS", + "STATUS", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "RATE", + "PERIOD", + "NOTIFY", + "HIDDEN", + "_PLACEHOLDER", + "RENEW", + "_PLACEHOLDER" +]) + +FundingCredit = _Serializer("FundingCredit", labels=[ + "ID", + "SYMBOL", + "SIDE", + "MTS_CREATE", + "MTS_UPDATE", + "AMOUNT", + "FLAGS", + "STATUS", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "RATE", + "PERIOD", + "MTS_OPENING", + "MTS_LAST_PAYOUT", + "NOTIFY", + "HIDDEN", + "_PLACEHOLDER", + "RENEW", + "RATE_REAL", + "NO_CLOSE", + "POSITION_PAIR" +]) + +FundingLoan = _Serializer("FundingLoan", labels=[ + "ID", + "SYMBOL", + "SIDE", + "MTS_CREATE", + "MTS_UPDATE", + "AMOUNT", + "FLAGS", + "STATUS", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "RATE", + "PERIOD", + "MTS_OPENING", + "MTS_LAST_PAYOUT", + "NOTIFY", + "HIDDEN", + "_PLACEHOLDER", + "RENEW", + "RATE_REAL", + "NO_CLOSE" +]) + +Wallet = _Serializer("Wallet", labels=[ + "WALLET_TYPE", + "CURRENCY", + "BALANCE", + "UNSETTLED_INTEREST", + "BALANCE_AVAILABLE", + "DESCRIPTION", + "META" +]) + +BalanceInfo = _Serializer("BalanceInfo", labels=[ + "AUM", + "AUM_NET", +]) + #endregion \ No newline at end of file diff --git a/bfxapi/websocket/typings.py b/bfxapi/websocket/typings.py index 490d3c5..55c37ef 100644 --- a/bfxapi/websocket/typings.py +++ b/bfxapi/websocket/typings.py @@ -184,18 +184,18 @@ Position = TypedDict("Position", { Positions = List[Position] Trade = TypedDict("Trade", { - "ID": int, - "CID": int, - "SYMBOL": str, + "ID": int, + "SYMBOL": str, "MTS_CREATE": int, - "ORDER_ID": int, - "EXEC_AMOUNT": float, - "EXEC_PRICE": float, - "ORDER_TYPE": str, - "ORDER_PRICE": float, - "MAKER": int, - "FEE": float, - "FEE_CURRENCY": str + "ORDER_ID": int, + "EXEC_AMOUNT": float, + "EXEC_PRICE": float, + "ORDER_TYPE": str, + "ORDER_PRICE": float, + "MAKER":int, + "FEE": Optional[float], + "FEE_CURRENCY": Optional[str], + "CID": int }) FundingOffer = TypedDict("FundingOffer", { From 7314578dd7257134322f265a3cdb2845db3b017a Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 17 Nov 2022 18:54:43 +0100 Subject: [PATCH 024/265] Separate _require_websocket_connection decorator from BfxWebsocketClient class. Add _require_websocket_authentication decorator. Implement .notify method in bfxapi/websocket/BfxWebsocketClient.py. --- bfxapi/websocket/BfxWebsocketClient.py | 48 ++++++++++++++++++-------- bfxapi/websocket/__init__.py | 2 +- bfxapi/websocket/errors.py | 7 ++++ 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index afb24a3..1d32e53 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -4,10 +4,29 @@ from pyee.asyncio import AsyncIOEventEmitter from .handlers import Channels, PublicChannelsHandler, AuthenticatedChannelsHandler -from .errors import BfxWebsocketException, ConnectionNotOpen, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion +from .errors import ConnectionNotOpen, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion HEARTBEAT = "hb" +def _require_websocket_connection(function): + async def wrapper(self, *args, **kwargs): + if self.websocket == None or self.websocket.open == False: + raise ConnectionNotOpen("No open connection with the server.") + + await function(self, *args, **kwargs) + + return wrapper + +def _require_websocket_authentication(function): + @_require_websocket_connection + 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 function(self, *args, **kwargs) + + return wrapper + class BfxWebsocketClient(object): VERSION = 2 @@ -22,6 +41,8 @@ class BfxWebsocketClient(object): self.websocket, self.API_KEY, self.API_SECRET = None, API_KEY, API_SECRET + self.authentication = False + self.handlers = { "public": PublicChannelsHandler(event_emitter=self.event_emitter), "authenticated": AuthenticatedChannelsHandler(event_emitter=self.event_emitter) @@ -53,26 +74,19 @@ class BfxWebsocketClient(object): 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("error", message["code"], message["msg"]) - elif isinstance(message, list) and ((chanId := message[0]) or True) and message[1] != HEARTBEAT: - if chanId == 0: + elif isinstance(message, list) and message[1] != HEARTBEAT: + if ((chanId := message[0]) or True) and chanId == 0: self.handlers["authenticated"].handle(message[1], message[2]) else: self.handlers["public"].handle(self.chanIds[chanId], *message[1:]) except websockets.ConnectionClosed: continue - def __require_websocket_connection(function): - async def wrapper(self, *args, **kwargs): - if self.websocket == None or self.websocket.open == False: - raise ConnectionNotOpen("No open connection with the server.") - - await function(self, *args, **kwargs) - - return wrapper - - @__require_websocket_connection + @_require_websocket_connection async def subscribe(self, channel, **kwargs): await self.websocket.send(json.dumps({ "event": "subscribe", @@ -80,14 +94,14 @@ class BfxWebsocketClient(object): **kwargs })) - @__require_websocket_connection + @_require_websocket_connection async def unsubscribe(self, chanId): await self.websocket.send(json.dumps({ "event": "unsubscribe", "chanId": chanId })) - @__require_websocket_connection + @_require_websocket_connection async def authenticate(self, API_KEY, API_SECRET, filter=None): data = { "event": "auth", "filter": filter, "apiKey": API_KEY } @@ -103,6 +117,10 @@ class BfxWebsocketClient(object): await self.websocket.send(json.dumps(data)) + @_require_websocket_authentication + async def notify(self, MESSAGE_ID, info): + await self.websocket.send(json.dumps([ 0, "n", MESSAGE_ID, { "type": "ucm-test", "info": info } ])) + async def clear(self): for chanId in self.chanIds.keys(): await self.unsubscribe(chanId) diff --git a/bfxapi/websocket/__init__.py b/bfxapi/websocket/__init__.py index 2a4bfa0..f36a1b0 100644 --- a/bfxapi/websocket/__init__.py +++ b/bfxapi/websocket/__init__.py @@ -1,3 +1,3 @@ from .BfxWebsocketClient import BfxWebsocketClient from .handlers import Channels -from .errors import BfxWebsocketException, ConnectionNotOpen, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion \ No newline at end of file +from .errors import BfxWebsocketException, ConnectionNotOpen, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion \ No newline at end of file diff --git a/bfxapi/websocket/errors.py b/bfxapi/websocket/errors.py index 6a45a94..7565bc9 100644 --- a/bfxapi/websocket/errors.py +++ b/bfxapi/websocket/errors.py @@ -18,6 +18,13 @@ class ConnectionNotOpen(BfxWebsocketException): pass +class WebsocketAuthenticationRequired(BfxWebsocketException): + """ + This error indicates an attempt to access a protected resource without logging in first. + """ + + pass + class InvalidAuthenticationCredentials(BfxWebsocketException): """ This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication. From ba992d34b365ae917709b0de6782be788d51f2fe Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 21 Nov 2022 16:24:10 +0100 Subject: [PATCH 025/265] Change tp_ and fc_ prefixes to t_ and f_. --- bfxapi/websocket/handlers.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index aebe66c..e6f7dab 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -22,9 +22,9 @@ class Channels(str, Enum): class PublicChannelsHandler(object): EVENTS = [ - "tp_ticker_update", "fc_ticker_update", - "tp_trade_executed", "tp_trade_execution_update", "fc_trade_executed", "fc_trade_execution_update", "tp_trades_snapshot", "fc_trades_snapshot", - "tp_book_snapshot", "fc_book_snapshot", "tp_raw_book_snapshot", "fc_raw_book_snapshot", "tp_book_update", "fc_book_update", "tp_raw_book_update", "fc_raw_book_update", + "t_ticker_update", "f_ticker_update", + "t_trade_executed", "t_trade_execution_update", "f_trade_executed", "f_trade_execution_update", "t_trades_snapshot", "f_trades_snapshot", + "t_book_snapshot", "f_book_snapshot", "t_raw_book_snapshot", "f_raw_book_snapshot", "t_book_update", "f_book_update", "t_raw_book_update", "f_raw_book_update", "candles_snapshot", "candles_update", "derivatives_status_update", ] @@ -47,14 +47,14 @@ class PublicChannelsHandler(object): def __ticker_channel_handler(self, subscription, *stream): if subscription["symbol"].startswith("t"): return self.event_emitter.emit( - "tp_ticker_update", + "t_ticker_update", _get_sub_dictionary(subscription, [ "chanId", "symbol", "pair" ]), serializers.TradingPairTicker.parse(*stream[0]) ) if subscription["symbol"].startswith("f"): return self.event_emitter.emit( - "fc_ticker_update", + "f_ticker_update", _get_sub_dictionary(subscription, [ "chanId", "symbol", "currency" ]), serializers.FundingCurrencyTicker.parse(*stream[0]) ) @@ -63,28 +63,28 @@ class PublicChannelsHandler(object): if type := stream[0] or type in [ "te", "tu", "fte", "ftu" ]: if subscription["symbol"].startswith("t"): return self.event_emitter.emit( - { "te": "tp_trade_executed", "tu": "tp_trade_execution_update" }[type], + { "te": "t_trade_executed", "tu": "t_trade_execution_update" }[type], _get_sub_dictionary(subscription, [ "chanId", "symbol", "pair" ]), serializers.TradingPairTrade.parse(*stream[1]) ) if subscription["symbol"].startswith("f"): return self.event_emitter.emit( - { "fte": "fc_trade_executed", "ftu": "fc_trade_execution_update" }[type], + { "fte": "f_trade_executed", "ftu": "f_trade_execution_update" }[type], _get_sub_dictionary(subscription, [ "chanId", "symbol", "currency" ]), serializers.FundingCurrencyTrade.parse(*stream[1]) ) if subscription["symbol"].startswith("t"): return self.event_emitter.emit( - "tp_trades_snapshot", + "t_trades_snapshot", _get_sub_dictionary(subscription, [ "chanId", "symbol", "pair" ]), [ serializers.TradingPairTrade.parse(*substream) for substream in stream[0] ] ) if subscription["symbol"].startswith("f"): return self.event_emitter.emit( - "fc_trades_snapshot", + "f_trades_snapshot", _get_sub_dictionary(subscription, [ "chanId", "symbol", "currency" ]), [ serializers.FundingCurrencyTrade.parse(*substream) for substream in stream[0] ] ) @@ -100,13 +100,13 @@ class PublicChannelsHandler(object): if all(isinstance(substream, list) for substream in stream[0]): return self.event_emitter.emit( - { "t": "tp_", "f": "fc_" }[type] + (IS_RAW_BOOK and "raw_book" or "book") + "_snapshot", + type + "_" + (IS_RAW_BOOK and "raw_book" or "book") + "_snapshot", subscription, [ { "t": _trading_pair_serializer, "f": _funding_currency_serializer }[type].parse(*substream) for substream in stream[0] ] ) return self.event_emitter.emit( - { "t": "tp_", "f": "fc_" }[type] + (IS_RAW_BOOK and "raw_book" or "book") + "_update", + type + "_" + (IS_RAW_BOOK and "raw_book" or "book") + "_update", subscription, { "t": _trading_pair_serializer, "f": _funding_currency_serializer }[type].parse(*stream[0]) ) From 8e56e9971e34b41c49d86c3710a9952b103da89d Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 21 Nov 2022 16:28:55 +0100 Subject: [PATCH 026/265] Rename error event to wss-error. --- bfxapi/websocket/BfxWebsocketClient.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 1d32e53..f2ed76a 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -31,7 +31,7 @@ class BfxWebsocketClient(object): VERSION = 2 EVENTS = [ - "open", "subscribed", "authenticated", "error", + "open", "subscribed", "authenticated", "wss-error", *PublicChannelsHandler.EVENTS, *AuthenticatedChannelsHandler.EVENTS ] @@ -78,7 +78,7 @@ class BfxWebsocketClient(object): 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("error", message["code"], message["msg"]) + self.event_emitter.emit("wss-error", message["code"], message["msg"]) elif isinstance(message, list) and message[1] != HEARTBEAT: if ((chanId := message[0]) or True) and chanId == 0: self.handlers["authenticated"].handle(message[1], message[2]) From 92f6e691f54fc035d75256974cc8c6e389e9b336 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 21 Nov 2022 18:41:05 +0100 Subject: [PATCH 027/265] Add BfxWebsocketBucket class in bfxapi/websocket/BfxWebsocketClient.py. Implement running multiple websocket client concurrently using asyncio to allow more than 25 connections to public channels. Rewrite BfxWebsocketClient to handle only websocket authenticated channels. --- bfxapi/websocket/BfxWebsocketClient.py | 163 +++++++++++++------------ 1 file changed, 87 insertions(+), 76 deletions(-) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index f2ed76a..5a92e83 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -17,16 +17,6 @@ def _require_websocket_connection(function): return wrapper -def _require_websocket_authentication(function): - @_require_websocket_connection - 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 function(self, *args, **kwargs) - - return wrapper - class BfxWebsocketClient(object): VERSION = 2 @@ -36,73 +26,42 @@ class BfxWebsocketClient(object): *AuthenticatedChannelsHandler.EVENTS ] - def __init__(self, host, API_KEY=None, API_SECRET=None): - self.host, self.chanIds, self.event_emitter = host, dict(), AsyncIOEventEmitter() + def __init__(self, host, buckets=5, API_KEY=None, API_SECRET=None): + self.host, self.websocket, self.event_emitter = host, None, AsyncIOEventEmitter() - self.websocket, self.API_KEY, self.API_SECRET = None, API_KEY, API_SECRET + self.API_KEY, self.API_SECRET, self.authentication = API_KEY, API_SECRET, False - self.authentication = False + self.handler = AuthenticatedChannelsHandler(event_emitter=self.event_emitter) - self.handlers = { - "public": PublicChannelsHandler(event_emitter=self.event_emitter), - "authenticated": AuthenticatedChannelsHandler(event_emitter=self.event_emitter) - } + self.buckets = [ _BfxWebsocketBucket(self.host, self.event_emitter, self.__bucket_open_signal) for _ in range(buckets) ] - async def connect(self): - async for websocket in websockets.connect(self.host): + 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)) + + await asyncio.gather(*tasks) + + async def __connect(self, API_KEY, API_SECRET, filter=None): + async with websockets.connect(self.host) as websocket: self.websocket = websocket - try: - self.event_emitter.emit("open") + await self.__authenticate(API_KEY, API_SECRET, filter) - if self.API_KEY != None and self.API_SECRET != None: - await self.authenticate(self.API_KEY, self.API_SECRET) + async for message in websocket: + message = json.loads(message) - 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]) - if isinstance(message, dict) and message["event"] == "info" and "version" in message: - if BfxWebsocketClient.VERSION != message["version"]: - raise OutdatedClientVersion(f"Mismatch between the client version and the server version. Update the library to the latest version to continue (client version: {BfxWebsocketClient.VERSION}, server version: {message['version']}).") - elif isinstance(message, dict) and message["event"] == "subscribed": - self.chanIds[message["chanId"]] = message - - self.event_emitter.emit("subscribed", message) - elif isinstance(message, dict) and message["event"] == "unsubscribed": - if message["status"] == "OK": - del self.chanIds[message["chanId"]] - 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 message[1] != HEARTBEAT: - if ((chanId := message[0]) or True) and chanId == 0: - self.handlers["authenticated"].handle(message[1], message[2]) - else: self.handlers["public"].handle(self.chanIds[chanId], *message[1:]) - except websockets.ConnectionClosed: - continue - - @_require_websocket_connection - async def subscribe(self, channel, **kwargs): - await self.websocket.send(json.dumps({ - "event": "subscribe", - "channel": channel, - **kwargs - })) - - @_require_websocket_connection - async def unsubscribe(self, chanId): - await self.websocket.send(json.dumps({ - "event": "unsubscribe", - "chanId": chanId - })) - - @_require_websocket_connection - async def authenticate(self, API_KEY, API_SECRET, filter=None): + async def __authenticate(self, API_KEY, API_SECRET, filter=None): data = { "event": "auth", "filter": filter, "apiKey": API_KEY } data["authNonce"] = int(time.time()) * 1000 @@ -117,13 +76,26 @@ class BfxWebsocketClient(object): await self.websocket.send(json.dumps(data)) - @_require_websocket_authentication - async def notify(self, MESSAGE_ID, info): - await self.websocket.send(json.dumps([ 0, "n", MESSAGE_ID, { "type": "ucm-test", "info": info } ])) + async def subscribe(self, channel, **kwargs): + counters = [ bucket.count for bucket in self.buckets ] - async def clear(self): - for chanId in self.chanIds.keys(): - await self.unsubscribe(chanId) + index = counters.index(min(counters)) + + await self.buckets[index]._subscribe(channel, **kwargs) + + def __require_websocket_authentication(function): + @_require_websocket_connection + 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 function(self, *args, **kwargs) + + return wrapper + + 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): if event not in BfxWebsocketClient.EVENTS: @@ -143,5 +115,44 @@ class BfxWebsocketClient(object): return handler - def run(self): - asyncio.run(self.connect()) \ No newline at end of file +class _BfxWebsocketBucket(object): + def __init__(self, host, event_emitter, __bucket_open_signal): + self.host, self.event_emitter, self.__bucket_open_signal = host, event_emitter, __bucket_open_signal + + self.websocket, self.chanIds, self.count = None, dict(), 0 + + self.handler = PublicChannelsHandler(event_emitter=self.event_emitter) + + async def _connect(self, index): + async with websockets.connect(self.host) as websocket: + self.websocket = websocket + + self.__bucket_open_signal(index) + + async for message in websocket: + message = json.loads(message) + + if isinstance(message, dict) and message["event"] == "info" and "version" in message: + if BfxWebsocketClient.VERSION != message["version"]: + raise OutdatedClientVersion(f"Mismatch between the client version and the server version. Update the library to the latest version to continue (client version: {BfxWebsocketClient.VERSION}, server version: {message['version']}).") + elif isinstance(message, dict) and message["event"] == "subscribed" and (chanId := message["chanId"]): + self.chanIds[chanId] = message + + self.event_emitter.emit("subscribed", message) + elif isinstance(message, dict) and message["event"] == "unsubscribed" and (chanId := message["chanId"]): + if message["status"] == "OK": + del self.chanIds[chanId] + elif isinstance(message, dict) and message["event"] == "error": + self.event_emitter.emit("wss-error", message["code"], message["msg"]) + elif isinstance(message, list) and (chanId := message[0]) and message[1] != HEARTBEAT: + self.handler.handle(self.chanIds[chanId], *message[1:]) + + @_require_websocket_connection + async def _subscribe(self, channel, **kwargs): + self.count += 1 + + await self.websocket.send(json.dumps({ + "event": "subscribe", + "channel": channel, + **kwargs + })) \ No newline at end of file From 2f561a4fba40153eb039ff082e1ae02bae9a6a97 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 21 Nov 2022 18:41:40 +0100 Subject: [PATCH 028/265] Fix small bug in bfxapi/websocket/errors.py (__all__). --- bfxapi/websocket/errors.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bfxapi/websocket/errors.py b/bfxapi/websocket/errors.py index 7565bc9..c631cca 100644 --- a/bfxapi/websocket/errors.py +++ b/bfxapi/websocket/errors.py @@ -1,7 +1,9 @@ __all__ = [ - "BfxWebsocketException", "ConnectionNotOpen", - "InvalidAuthenticationCredentials" + "WebsocketAuthenticationRequired", + "InvalidAuthenticationCredentials", + "EventNotSupported", + "OutdatedClientVersion" ] class BfxWebsocketException(Exception): From 721e82b86d89763bcdf1a561132f48982d24e691 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 22 Nov 2022 17:21:21 +0100 Subject: [PATCH 029/265] Add pendings in _BfxWebsocketBucket. Add new logic for selecting the bucket with less connections. Add ._unsubscribe coroutine. --- bfxapi/websocket/BfxWebsocketClient.py | 37 ++++++++++++++++++++------ bfxapi/websocket/__init__.py | 2 +- bfxapi/websocket/errors.py | 8 ++++++ 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 5a92e83..07cc8b4 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -1,10 +1,10 @@ -import json, asyncio, hmac, hashlib, time, websockets +import json, asyncio, hmac, hashlib, time, uuid, websockets from pyee.asyncio import AsyncIOEventEmitter from .handlers import Channels, PublicChannelsHandler, AuthenticatedChannelsHandler -from .errors import ConnectionNotOpen, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion +from .errors import ConnectionNotOpen, TooManySubscriptions, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion HEARTBEAT = "hb" @@ -77,12 +77,17 @@ class BfxWebsocketClient(object): await self.websocket.send(json.dumps(data)) async def subscribe(self, channel, **kwargs): - counters = [ bucket.count for bucket in self.buckets ] + counters = [ len(bucket.pendings) + len(bucket.chanIds) 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.chanIds.keys(): + await bucket._unsubscribe(chanId=chanId) + def __require_websocket_authentication(function): @_require_websocket_connection async def wrapper(self, *args, **kwargs): @@ -116,10 +121,12 @@ class BfxWebsocketClient(object): return handler class _BfxWebsocketBucket(object): + MAXIMUM_SUBSCRIPTIONS_AMOUNT = 25 + def __init__(self, host, event_emitter, __bucket_open_signal): self.host, self.event_emitter, self.__bucket_open_signal = host, event_emitter, __bucket_open_signal - self.websocket, self.chanIds, self.count = None, dict(), 0 + self.websocket, self.chanIds, self.pendings = None, dict(), list() self.handler = PublicChannelsHandler(event_emitter=self.event_emitter) @@ -136,8 +143,8 @@ class _BfxWebsocketBucket(object): if BfxWebsocketClient.VERSION != message["version"]: raise OutdatedClientVersion(f"Mismatch between the client version and the server version. Update the library to the latest version to continue (client version: {BfxWebsocketClient.VERSION}, server version: {message['version']}).") elif isinstance(message, dict) and message["event"] == "subscribed" and (chanId := message["chanId"]): + self.pendings = [ pending for pending in self.pendings if pending["subId"] != message["subId"] ] self.chanIds[chanId] = message - self.event_emitter.emit("subscribed", message) elif isinstance(message, dict) and message["event"] == "unsubscribed" and (chanId := message["chanId"]): if message["status"] == "OK": @@ -148,11 +155,25 @@ class _BfxWebsocketBucket(object): self.handler.handle(self.chanIds[chanId], *message[1:]) @_require_websocket_connection - async def _subscribe(self, channel, **kwargs): - self.count += 1 + async def _subscribe(self, channel, subId=None, **kwargs): + if len(self.chanIds) + len(self.pendings) == _BfxWebsocketBucket.MAXIMUM_SUBSCRIPTIONS_AMOUNT: + raise TooManySubscriptions("The client has reached the maximum number of subscriptions.") - await self.websocket.send(json.dumps({ + subscription = { "event": "subscribe", "channel": channel, + "subId": subId or str(uuid.uuid4()), + **kwargs + } + + self.pendings.append(subscription) + + await self.websocket.send(json.dumps(subscription)) + + @_require_websocket_connection + async def _unsubscribe(self, chanId): + await self.websocket.send(json.dumps({ + "event": "unsubscribe", + "chanId": chanId })) \ No newline at end of file diff --git a/bfxapi/websocket/__init__.py b/bfxapi/websocket/__init__.py index f36a1b0..dee1570 100644 --- a/bfxapi/websocket/__init__.py +++ b/bfxapi/websocket/__init__.py @@ -1,3 +1,3 @@ from .BfxWebsocketClient import BfxWebsocketClient from .handlers import Channels -from .errors import BfxWebsocketException, ConnectionNotOpen, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion \ No newline at end of file +from .errors import BfxWebsocketException, ConnectionNotOpen, TooManySubscriptions, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion \ No newline at end of file diff --git a/bfxapi/websocket/errors.py b/bfxapi/websocket/errors.py index c631cca..3a1f900 100644 --- a/bfxapi/websocket/errors.py +++ b/bfxapi/websocket/errors.py @@ -1,5 +1,6 @@ __all__ = [ "ConnectionNotOpen", + "TooManySubscriptions", "WebsocketAuthenticationRequired", "InvalidAuthenticationCredentials", "EventNotSupported", @@ -20,6 +21,13 @@ class ConnectionNotOpen(BfxWebsocketException): pass +class TooManySubscriptions(BfxWebsocketException): + """ + This error indicates an attempt to subscribe to a public channel after reaching the limit of simultaneous connections. + """ + + pass + class WebsocketAuthenticationRequired(BfxWebsocketException): """ This error indicates an attempt to access a protected resource without logging in first. From 999766a30711ae53e6294cd91a1f6428dba8d80f Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 22 Nov 2022 18:02:19 +0100 Subject: [PATCH 030/265] Add .close coroutine to BfxWebsocketClient and _BfxWebsocketBucket classes. --- bfxapi/websocket/BfxWebsocketClient.py | 75 +++++++++++++++----------- 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 07cc8b4..cf4eba6 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -44,22 +44,24 @@ class BfxWebsocketClient(object): await asyncio.gather(*tasks) async def __connect(self, API_KEY, API_SECRET, filter=None): - async with websockets.connect(self.host) as websocket: + async for websocket in websockets.connect(self.host): self.websocket = websocket - + await self.__authenticate(API_KEY, API_SECRET, filter) - async for message in websocket: - message = json.loads(message) + 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]) + 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 async def __authenticate(self, API_KEY, API_SECRET, filter=None): data = { "event": "auth", "filter": filter, "apiKey": API_KEY } @@ -88,6 +90,13 @@ class BfxWebsocketClient(object): if chanId in bucket.chanIds.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) + def __require_websocket_authentication(function): @_require_websocket_connection async def wrapper(self, *args, **kwargs): @@ -131,28 +140,30 @@ class _BfxWebsocketBucket(object): self.handler = PublicChannelsHandler(event_emitter=self.event_emitter) async def _connect(self, index): - async with websockets.connect(self.host) as websocket: + async for websocket in websockets.connect(self.host): self.websocket = websocket self.__bucket_open_signal(index) - async for message in websocket: - message = json.loads(message) + try: + async for message in websocket: + message = json.loads(message) - if isinstance(message, dict) and message["event"] == "info" and "version" in message: - if BfxWebsocketClient.VERSION != message["version"]: - raise OutdatedClientVersion(f"Mismatch between the client version and the server version. Update the library to the latest version to continue (client version: {BfxWebsocketClient.VERSION}, server version: {message['version']}).") - elif isinstance(message, dict) and message["event"] == "subscribed" and (chanId := message["chanId"]): - self.pendings = [ pending for pending in self.pendings if pending["subId"] != message["subId"] ] - self.chanIds[chanId] = message - self.event_emitter.emit("subscribed", message) - elif isinstance(message, dict) and message["event"] == "unsubscribed" and (chanId := message["chanId"]): - if message["status"] == "OK": - del self.chanIds[chanId] - elif isinstance(message, dict) and message["event"] == "error": - self.event_emitter.emit("wss-error", message["code"], message["msg"]) - elif isinstance(message, list) and (chanId := message[0]) and message[1] != HEARTBEAT: - self.handler.handle(self.chanIds[chanId], *message[1:]) + if isinstance(message, dict) and message["event"] == "info" and "version" in message: + if BfxWebsocketClient.VERSION != message["version"]: + raise OutdatedClientVersion(f"Mismatch between the client version and the server version. Update the library to the latest version to continue (client version: {BfxWebsocketClient.VERSION}, server version: {message['version']}).") + elif isinstance(message, dict) and message["event"] == "subscribed" and (chanId := message["chanId"]): + self.pendings = [ pending for pending in self.pendings if pending["subId"] != message["subId"] ] + self.chanIds[chanId] = message + self.event_emitter.emit("subscribed", message) + elif isinstance(message, dict) and message["event"] == "unsubscribed" and (chanId := message["chanId"]): + if message["status"] == "OK": + del self.chanIds[chanId] + elif isinstance(message, dict) and message["event"] == "error": + self.event_emitter.emit("wss-error", message["code"], message["msg"]) + elif isinstance(message, list) and (chanId := message[0]) and message[1] != HEARTBEAT: + self.handler.handle(self.chanIds[chanId], *message[1:]) + except websockets.ConnectionClosedError: continue @_require_websocket_connection async def _subscribe(self, channel, subId=None, **kwargs): @@ -176,4 +187,8 @@ class _BfxWebsocketBucket(object): await self.websocket.send(json.dumps({ "event": "unsubscribe", "chanId": chanId - })) \ No newline at end of file + })) + + @_require_websocket_connection + async def close(self, code=1000, reason=str()): + await self.websocket.close(code=code, reason=reason) \ No newline at end of file From a8dec5c6bcde45dced71542eb43085fa7063ab06 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 22 Nov 2022 18:11:05 +0100 Subject: [PATCH 031/265] Rename bfxapi/websocket/errors.py to exceptions.py. Add Errors enumeration inside BfxWebsocketClient.py and bfxapi/websocket/__init__.py. --- bfxapi/websocket/BfxWebsocketClient.py | 23 +++++++++++++++++-- bfxapi/websocket/__init__.py | 4 ++-- bfxapi/websocket/{errors.py => exceptions.py} | 0 bfxapi/websocket/handlers.py | 2 +- bfxapi/websocket/serializers.py | 2 +- 5 files changed, 25 insertions(+), 6 deletions(-) rename bfxapi/websocket/{errors.py => exceptions.py} (100%) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index cf4eba6..e040530 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -1,10 +1,12 @@ import json, asyncio, hmac, hashlib, time, uuid, websockets +from enum import Enum + from pyee.asyncio import AsyncIOEventEmitter from .handlers import Channels, PublicChannelsHandler, AuthenticatedChannelsHandler -from .errors import ConnectionNotOpen, TooManySubscriptions, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion +from .exceptions import ConnectionNotOpen, TooManySubscriptions, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion HEARTBEAT = "hb" @@ -191,4 +193,21 @@ class _BfxWebsocketBucket(object): @_require_websocket_connection async def close(self, code=1000, reason=str()): - await self.websocket.close(code=code, reason=reason) \ No newline at end of file + await self.websocket.close(code=code, reason=reason) + +class Errors(int, Enum): + ERR_UNK = 10000 + ERR_GENERIC = 10001 + ERR_CONCURRENCY = 10008 + ERR_PARAMS = 10020 + ERR_CONF_FAIL = 10050 + ERR_AUTH_FAIL = 10100 + ERR_AUTH_PAYLOAD = 10111 + ERR_AUTH_SIG = 10112 + ERR_AUTH_HMAC = 10113 + ERR_AUTH_NONCE = 10114 + ERR_UNAUTH_FAIL = 10200 + ERR_SUB_FAIL = 10300 + ERR_SUB_MULTI = 10301 + ERR_UNSUB_FAIL = 10400 + ERR_READY = 11000 \ No newline at end of file diff --git a/bfxapi/websocket/__init__.py b/bfxapi/websocket/__init__.py index dee1570..4704773 100644 --- a/bfxapi/websocket/__init__.py +++ b/bfxapi/websocket/__init__.py @@ -1,3 +1,3 @@ -from .BfxWebsocketClient import BfxWebsocketClient +from .BfxWebsocketClient import BfxWebsocketClient, Errors from .handlers import Channels -from .errors import BfxWebsocketException, ConnectionNotOpen, TooManySubscriptions, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion \ No newline at end of file +from .exceptions import BfxWebsocketException, ConnectionNotOpen, TooManySubscriptions, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion \ No newline at end of file diff --git a/bfxapi/websocket/errors.py b/bfxapi/websocket/exceptions.py similarity index 100% rename from bfxapi/websocket/errors.py rename to bfxapi/websocket/exceptions.py diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index e6f7dab..dd85b76 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -2,7 +2,7 @@ from enum import Enum from . import serializers -from .errors import BfxWebsocketException +from .exceptions import BfxWebsocketException def _get_sub_dictionary(dictionary, keys): return { key: dictionary[key] for key in dictionary if key in keys } diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index 47cd770..b8796fd 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -1,4 +1,4 @@ -from .errors import BfxWebsocketException +from .exceptions import BfxWebsocketException class _Serializer(object): def __init__(self, name, labels): From f24ed520751044d661d8110386428e0cc8516ebb Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 22 Nov 2022 18:13:06 +0100 Subject: [PATCH 032/265] Add setup.py file in project root. --- setup.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 setup.py diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..9eff973 --- /dev/null +++ b/setup.py @@ -0,0 +1,22 @@ +from distutils.core import setup + +setup( + name="bitfinex-api-py", + version="3.0.0", + packages=[ "bfxapi", "bfxapi.websocket" ], + url="https://github.com/bitfinexcom/bitfinex-api-py", + license="OSI Approved :: Apache Software License", + author="Bitfinex", + author_email="support@bitfinex.com", + description="Official Bitfinex Python API", + keywords="bitfinex,api,trading", + install_requires=[ + "pyee~=9.0.4", + "typing_extensions~=4.4.0", + "websockets~=10.4", + ], + project_urls={ + "Bug Reports": "https://github.com/bitfinexcom/bitfinex-api-py/issues", + "Source": "https://github.com/bitfinexcom/bitfinex-api-py", + } +) \ No newline at end of file From 958134b0f4f0877c076b68cbe7ffecf2aa4bb189 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Mon, 21 Nov 2022 13:04:59 +0100 Subject: [PATCH 033/265] Add bfxapi/utils subpackage. Add bfxapi/utils/logger.py. Implement logger with log_level in BfxWebsocketClient.py. --- bfxapi/client.py | 9 ++- bfxapi/utils/__init__.py | 1 + bfxapi/utils/logger.py | 99 ++++++++++++++++++++++++++ bfxapi/websocket/BfxWebsocketClient.py | 10 ++- 4 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 bfxapi/utils/__init__.py create mode 100644 bfxapi/utils/logger.py diff --git a/bfxapi/client.py b/bfxapi/client.py index cefd5ce..e53ca66 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -7,5 +7,10 @@ class Constants(str, Enum): PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2" class Client(object): - def __init__(self, WSS_HOST: str = Constants.WSS_HOST, API_KEY: str = None, API_SECRET: str = None): - self.wss = BfxWebsocketClient(host=WSS_HOST, API_KEY=API_KEY, API_SECRET=API_SECRET) \ No newline at end of file + def __init__(self, WSS_HOST: str = Constants.WSS_HOST, API_KEY: str = None, API_SECRET: str = None, log_level: str = "INFO"): + self.wss = BfxWebsocketClient( + host=WSS_HOST, + API_KEY=API_KEY, + API_SECRET=API_SECRET, + log_level=log_level + ) \ No newline at end of file diff --git a/bfxapi/utils/__init__.py b/bfxapi/utils/__init__.py new file mode 100644 index 0000000..5a6afd1 --- /dev/null +++ b/bfxapi/utils/__init__.py @@ -0,0 +1 @@ +NAME = "utils" \ No newline at end of file diff --git a/bfxapi/utils/logger.py b/bfxapi/utils/logger.py new file mode 100644 index 0000000..0ea3894 --- /dev/null +++ b/bfxapi/utils/logger.py @@ -0,0 +1,99 @@ +""" +Module used to describe all of the different data types +""" + +import logging + +RESET_SEQ = "\033[0m" +COLOR_SEQ = "\033[1;%dm" +BOLD_SEQ = "\033[1m" +UNDERLINE_SEQ = "\033[04m" + +YELLOW = '\033[93m' +WHITE = '\33[37m' +BLUE = '\033[34m' +LIGHT_BLUE = '\033[94m' +RED = '\033[91m' +GREY = '\33[90m' + +KEYWORD_COLORS = { + 'WARNING': YELLOW, + 'INFO': LIGHT_BLUE, + 'DEBUG': WHITE, + 'CRITICAL': YELLOW, + 'ERROR': RED, + 'TRADE': '\33[102m\33[30m' +} + +def formatter_message(message, use_color = True): + """ + Syntax highlight certain keywords + """ + if use_color: + message = message.replace("$RESET", RESET_SEQ).replace("$BOLD", BOLD_SEQ) + else: + message = message.replace("$RESET", "").replace("$BOLD", "") + return message + +def format_word(message, word, color_seq, bold=False, underline=False): + """ + Surround the given word with a sequence + """ + replacer = color_seq + word + RESET_SEQ + if underline: + replacer = UNDERLINE_SEQ + replacer + if bold: + replacer = BOLD_SEQ + replacer + return message.replace(word, replacer) + +class Formatter(logging.Formatter): + """ + This Formatted simply colors in the levelname i.e 'INFO', 'DEBUG' + """ + def __init__(self, msg, use_color = True): + logging.Formatter.__init__(self, msg) + self.use_color = use_color + + def format(self, record): + """ + Format and highlight certain keywords + """ + levelname = record.levelname + if self.use_color and levelname in KEYWORD_COLORS: + levelname_color = KEYWORD_COLORS[levelname] + levelname + RESET_SEQ + record.levelname = levelname_color + record.name = GREY + record.name + RESET_SEQ + return logging.Formatter.format(self, record) + +class CustomLogger(logging.Logger): + """ + This adds extra logging functions such as logger.trade and also + sets the logger to use the custom formatter + """ + FORMAT = "[$BOLD%(name)s$RESET] [%(levelname)s] %(message)s" + COLOR_FORMAT = formatter_message(FORMAT, True) + TRADE = 50 + + def __init__(self, name, logLevel='DEBUG'): + logging.Logger.__init__(self, name, logLevel) + color_formatter = Formatter(self.COLOR_FORMAT) + console = logging.StreamHandler() + console.setFormatter(color_formatter) + self.addHandler(console) + logging.addLevelName(self.TRADE, "TRADE") + return + + def set_level(self, level): + logging.Logger.setLevel(self, level) + + def trade(self, message, *args, **kws): + """ + Print a syntax highlighted trade signal + """ + if self.isEnabledFor(self.TRADE): + message = format_word(message, 'CLOSED ', YELLOW, bold=True) + message = format_word(message, 'OPENED ', LIGHT_BLUE, bold=True) + message = format_word(message, 'UPDATED ', BLUE, bold=True) + message = format_word(message, 'CLOSED_ALL ', RED, bold=True) + # Yes, logger takes its '*args' as 'args'. + self._log(self.TRADE, message, args, **kws) \ No newline at end of file diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 1d32e53..91153ad 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -6,6 +6,8 @@ from .handlers import Channels, PublicChannelsHandler, AuthenticatedChannelsHand from .errors import ConnectionNotOpen, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion +from ..utils.logger import CustomLogger + HEARTBEAT = "hb" def _require_websocket_connection(function): @@ -36,7 +38,7 @@ class BfxWebsocketClient(object): *AuthenticatedChannelsHandler.EVENTS ] - def __init__(self, host, API_KEY=None, API_SECRET=None): + def __init__(self, host, log_level = "INFO", API_KEY=None, API_SECRET=None): self.host, self.chanIds, self.event_emitter = host, dict(), AsyncIOEventEmitter() self.websocket, self.API_KEY, self.API_SECRET = None, API_KEY, API_SECRET @@ -48,6 +50,8 @@ class BfxWebsocketClient(object): "authenticated": AuthenticatedChannelsHandler(event_emitter=self.event_emitter) } + self.logger = CustomLogger("BfxWebsocketClient", logLevel=log_level) + async def connect(self): async for websocket in websockets.connect(self.host): self.websocket = websocket @@ -127,7 +131,7 @@ class BfxWebsocketClient(object): def on(self, event): if event not in BfxWebsocketClient.EVENTS: - raise EventNotSupported(f"Event <{event}> is not supported. To get a list of available events use BfxWebsocketClient.EVENTS.") + raise EventNotSupported(f"Event <{event}> is not supported. To get a list of available events print BfxWebsocketClient.EVENTS") def handler(function): self.event_emitter.on(event, function) @@ -136,7 +140,7 @@ class BfxWebsocketClient(object): def once(self, event): if event not in BfxWebsocketClient.EVENTS: - raise EventNotSupported(f"Event <{event}> is not supported. To get a list of available events use BfxWebsocketClient.EVENTS.") + raise EventNotSupported(f"Event <{event}> is not supported. To get a list of available events print BfxWebsocketClient.EVENTS") def handler(function): self.event_emitter.once(event, function) From acbed0bc576d9f513cafea5fa6e5ed0fd860cb7c Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 22 Nov 2022 18:58:36 +0100 Subject: [PATCH 034/265] Fix small bug from merge conflicts resolving. --- bfxapi/websocket/BfxWebsocketClient.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 5347b0d..07a4c11 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -8,7 +8,7 @@ from .handlers import Channels, PublicChannelsHandler, AuthenticatedChannelsHand from .exceptions import ConnectionNotOpen, TooManySubscriptions, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion -from ..utils.logger import CustomLogger +from ..utils.logger import Formatter, CustomLogger HEARTBEAT = "hb" @@ -30,7 +30,7 @@ class BfxWebsocketClient(object): *AuthenticatedChannelsHandler.EVENTS ] - def __init__(self, host, buckets=5, API_KEY=None, API_SECRET=None): + def __init__(self, host, buckets=5, log_level = "INFO", API_KEY=None, API_SECRET=None): self.host, self.websocket, self.event_emitter = host, None, AsyncIOEventEmitter() self.API_KEY, self.API_SECRET, self.authentication = API_KEY, API_SECRET, False @@ -39,6 +39,8 @@ class BfxWebsocketClient(object): self.buckets = [ _BfxWebsocketBucket(self.host, self.event_emitter, self.__bucket_open_signal) for _ in range(buckets) ] + self.logger = CustomLogger("BfxWebsocketClient", logLevel=log_level) + async def start(self): tasks = [ bucket._connect(index) for index, bucket in enumerate(self.buckets) ] From 729a3ce8a3817f2ddf00f5970df9f739a243bd8f Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 22 Nov 2022 19:01:57 +0100 Subject: [PATCH 035/265] Add examples/websocket/ticker.py demo. --- examples/websocket/ticker.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 examples/websocket/ticker.py diff --git a/examples/websocket/ticker.py b/examples/websocket/ticker.py new file mode 100644 index 0000000..4e5d8e7 --- /dev/null +++ b/examples/websocket/ticker.py @@ -0,0 +1,19 @@ +import asyncio + +from bfxapi import Client, Constants +from bfxapi.websocket import Channels +from bfxapi.websocket.typings import Subscriptions, TradingPairTicker + +bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) + +@bfx.wss.on("t_ticker_update") +def on_t_ticker_update(subscription: Subscriptions.TradingPairsTicker, data: TradingPairTicker): + print(f"Subscription channel ID: {subscription['chanId']}") + + print(f"Data: {data}") + +@bfx.wss.once("open") +async def open(): + await bfx.wss.subscribe(Channels.TICKER, symbol="tBTCUSD") + +asyncio.run(bfx.wss.start()) \ No newline at end of file From 16bbe19634c7ff11bcd3f9c6327d49e612eeee01 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 22 Nov 2022 19:10:43 +0100 Subject: [PATCH 036/265] Add bfx.api subpackage to setup.py. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9eff973..c130b0d 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from distutils.core import setup setup( name="bitfinex-api-py", version="3.0.0", - packages=[ "bfxapi", "bfxapi.websocket" ], + packages=[ "bfxapi", "bfxapi.websocket", "bfxapi.utils" ], url="https://github.com/bitfinexcom/bitfinex-api-py", license="OSI Approved :: Apache Software License", author="Bitfinex", From 45a9565cf4ea95bf7ad5994e24566477ee4eebb0 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 23 Nov 2022 16:28:05 +0100 Subject: [PATCH 037/265] Fix bug regarding closing websocket connection with .close coroutine method. --- bfxapi/websocket/BfxWebsocketClient.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 07a4c11..dd11168 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -68,6 +68,7 @@ class BfxWebsocketClient(object): 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 } @@ -170,6 +171,7 @@ class _BfxWebsocketBucket(object): elif isinstance(message, list) and (chanId := message[0]) and message[1] != HEARTBEAT: self.handler.handle(self.chanIds[chanId], *message[1:]) except websockets.ConnectionClosedError: continue + finally: await self.websocket.wait_closed(); break @_require_websocket_connection async def _subscribe(self, channel, subId=None, **kwargs): From 4933ae6d9aa0fa1031a45bcd60d0bc47fb6511b5 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 23 Nov 2022 16:45:33 +0100 Subject: [PATCH 038/265] Add new_order (coroutine) websocket authenticated input. Fix bug in BfxWebsocketClient.__require_websocket_authentication decorator. --- bfxapi/websocket/BfxWebsocketClient.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index dd11168..e2b8105 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -105,15 +105,18 @@ class BfxWebsocketClient(object): await bucket.close(code=code, reason=reason) def __require_websocket_authentication(function): - @_require_websocket_connection 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 function(self, *args, **kwargs) + await _require_websocket_connection(function)(self, *args, **kwargs) return wrapper + @__require_websocket_authentication + async def new_order(self, data): + await self.websocket.send(json.dumps([ 0, "on", None, data ])) + 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") From c8d85868418a0cede632e77b3f5d1f4d76e6442e Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 23 Nov 2022 16:56:13 +0100 Subject: [PATCH 039/265] Add bfxapi/websocket/enums.py file. Rewrite bfxapi.websocket's __init__.py. Rename HEARTBEAT constant to _HEARTBEAT to make it internal. --- bfxapi/websocket/BfxWebsocketClient.py | 25 ++++--------------------- bfxapi/websocket/__init__.py | 4 +--- bfxapi/websocket/enums.py | 25 +++++++++++++++++++++++++ bfxapi/websocket/handlers.py | 19 ++----------------- 4 files changed, 32 insertions(+), 41 deletions(-) create mode 100644 bfxapi/websocket/enums.py diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index e2b8105..000efc3 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -10,7 +10,7 @@ from .exceptions import ConnectionNotOpen, TooManySubscriptions, WebsocketAuthen from ..utils.logger import Formatter, CustomLogger -HEARTBEAT = "hb" +_HEARTBEAT = "hb" def _require_websocket_connection(function): async def wrapper(self, *args, **kwargs): @@ -65,7 +65,7 @@ class BfxWebsocketClient(object): 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: + 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 @@ -171,7 +171,7 @@ class _BfxWebsocketBucket(object): del self.chanIds[chanId] elif isinstance(message, dict) and message["event"] == "error": self.event_emitter.emit("wss-error", message["code"], message["msg"]) - elif isinstance(message, list) and (chanId := message[0]) and message[1] != HEARTBEAT: + elif isinstance(message, list) and (chanId := message[0]) and message[1] != _HEARTBEAT: self.handler.handle(self.chanIds[chanId], *message[1:]) except websockets.ConnectionClosedError: continue finally: await self.websocket.wait_closed(); break @@ -202,21 +202,4 @@ class _BfxWebsocketBucket(object): @_require_websocket_connection async def close(self, code=1000, reason=str()): - await self.websocket.close(code=code, reason=reason) - -class Errors(int, Enum): - ERR_UNK = 10000 - ERR_GENERIC = 10001 - ERR_CONCURRENCY = 10008 - ERR_PARAMS = 10020 - ERR_CONF_FAIL = 10050 - ERR_AUTH_FAIL = 10100 - ERR_AUTH_PAYLOAD = 10111 - ERR_AUTH_SIG = 10112 - ERR_AUTH_HMAC = 10113 - ERR_AUTH_NONCE = 10114 - ERR_UNAUTH_FAIL = 10200 - ERR_SUB_FAIL = 10300 - ERR_SUB_MULTI = 10301 - ERR_UNSUB_FAIL = 10400 - ERR_READY = 11000 \ No newline at end of file + await self.websocket.close(code=code, reason=reason) \ No newline at end of file diff --git a/bfxapi/websocket/__init__.py b/bfxapi/websocket/__init__.py index 4704773..e24f778 100644 --- a/bfxapi/websocket/__init__.py +++ b/bfxapi/websocket/__init__.py @@ -1,3 +1 @@ -from .BfxWebsocketClient import BfxWebsocketClient, Errors -from .handlers import Channels -from .exceptions import BfxWebsocketException, ConnectionNotOpen, TooManySubscriptions, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion \ No newline at end of file +from .BfxWebsocketClient import BfxWebsocketClient \ No newline at end of file diff --git a/bfxapi/websocket/enums.py b/bfxapi/websocket/enums.py new file mode 100644 index 0000000..9a89eb0 --- /dev/null +++ b/bfxapi/websocket/enums.py @@ -0,0 +1,25 @@ +from enum import Enum + +class Channels(str, Enum): + TICKER = "ticker" + TRADES = "trades" + BOOK = "book" + CANDLES = "candles" + STATUS = "status" + +class Errors(int, Enum): + ERR_UNK = 10000 + ERR_GENERIC = 10001 + ERR_CONCURRENCY = 10008 + ERR_PARAMS = 10020 + ERR_CONF_FAIL = 10050 + ERR_AUTH_FAIL = 10100 + ERR_AUTH_PAYLOAD = 10111 + ERR_AUTH_SIG = 10112 + ERR_AUTH_HMAC = 10113 + ERR_AUTH_NONCE = 10114 + ERR_UNAUTH_FAIL = 10200 + ERR_SUB_FAIL = 10300 + ERR_SUB_MULTI = 10301 + ERR_UNSUB_FAIL = 10400 + ERR_READY = 11000 \ No newline at end of file diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index dd85b76..0d79072 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -1,25 +1,10 @@ -from enum import Enum - from . import serializers - +from .enums import Channels from .exceptions import BfxWebsocketException def _get_sub_dictionary(dictionary, keys): return { key: dictionary[key] for key in dictionary if key in keys } - -def _label_stream_data(labels, *args, IGNORE = [ "_PLACEHOLDER" ]): - if len(labels) != len(args): - raise BfxWebsocketException(" and <*args> arguments should contain the same amount of elements.") - - return { label: args[index] for index, label in enumerate(labels) if label not in IGNORE } - -class Channels(str, Enum): - TICKER = "ticker" - TRADES = "trades" - BOOK = "book" - CANDLES = "candles" - STATUS = "status" - + class PublicChannelsHandler(object): EVENTS = [ "t_ticker_update", "f_ticker_update", From 6f1befbcf06ed80f4d9cd86823da3d27a8eb6d34 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 24 Nov 2022 16:24:31 +0100 Subject: [PATCH 040/265] Rewrite and apply some refactoring to AuthenticatedChannelsHandler in handlers.py. --- bfxapi/websocket/handlers.py | 143 ++++++++--------------------------- 1 file changed, 33 insertions(+), 110 deletions(-) diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index 0d79072..8c4170d 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -123,120 +123,43 @@ class PublicChannelsHandler(object): ) class AuthenticatedChannelsHandler(object): - EVENTS = [ - "order_snapshot", "new_order", "order_update", "order_cancel", - "position_snapshot", "new_position", "position_update", "position_close", - "trade_executed", "trade_execution_update", - "funding_offer_snapshot", "funding_offer_new", "funding_offer_update", "funding_offer_cancel", - "funding_credit_snapshot", "funding_credit_new", "funding_credit_update", "funding_credit_close", - "funding_loan_snapshot", "funding_loan_new", "funding_loan_update", "funding_loan_close", - "wallet_snapshot", "wallet_update", - "balance_update", - ] + __abbreviations = { + "os": "order_snapshot", "on": "new_order", "ou": "order_update", "oc": "order_cancel", + "ps": "position_snapshot", "pn": "new_position", "pu": "position_update", "pc": "position_close", + "te": "trade_executed", "tu": "trade_execution_update", + "fos": "funding_offer_snapshot", "fon": "funding_offer_new", "fou": "funding_offer_update", "foc": "funding_offer_cancel", + "fcs": "funding_credit_snapshot", "fcn": "funding_credit_new", "fcu": "funding_credit_update", "fcc": "funding_credit_close", + "fls": "funding_loan_snapshot", "fln": "funding_loan_new", "flu": "funding_loan_update", "flc": "funding_loan_close", + "ws": "wallet_snapshot", "wu": "wallet_update", + "bu": "balance_update", + } + + __serializers = { + ("os", "on", "ou", "oc",): serializers.Order, + ("ps", "pn", "pu", "pc",): serializers.Position, + ("te",): serializers.TradeExecuted, + ("tu",): serializers.TradeExecutionUpdate, + ("fos", "fon", "fou", "foc",): serializers.FundingOffer, + ("fcs", "fcn", "fcu", "fcc",): serializers.FundingCredit, + ("fls", "fln", "flu", "flc",): serializers.FundingLoan, + ("ws", "wu",): serializers.Wallet, + ("bu",): serializers.BalanceInfo + } + + EVENTS = list(__abbreviations.values()) def __init__(self, event_emitter, strict = False): self.event_emitter, self.strict = event_emitter, strict - self.__handlers = { - ("os", "on", "ou", "oc",): self.__orders_channel_handler, - ("ps", "pn", "pu", "pc",): self.__positions_channel_handler, - ("te", "tu",): self.__trades_channel_handler, - ("fos", "fon", "fou", "foc",): self.__funding_offers_channel_handler, - ("fcs", "fcn", "fcu", "fcc",): self.__funding_credits_channel_handler, - ("fls", "fln", "flu", "flc",): self.__funding_loans_channel_handler, - ("ws", "wu",): self.__wallets_channel_handler, - ("bu",): self.__balance_info_channel_handler - } - def handle(self, type, stream): - for abbreviations in self.__handlers.keys(): - if type in abbreviations: - return self.__handlers[abbreviations](type, 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 __orders_channel_handler(self, type, stream): - if type == "os": - return self.event_emitter.emit("order_snapshot", [ - serializers.Order.parse(*substream) for substream in stream - ]) - - if type in [ "on", "ou", "oc" ]: - return self.event_emitter.emit({ - "on": "new_order", - "ou": "order_update", - "oc": "order_cancel" - }[type], serializers.Order.parse(*stream)) - - def __positions_channel_handler(self, type, stream): - if type == "ps": - return self.event_emitter.emit("position_snapshot", [ - serializers.Position.parse(*substream) for substream in stream - ]) - - if type in [ "pn", "pu", "pc" ]: - return self.event_emitter.emit({ - "pn": "new_position", - "pu": "position_update", - "pc": "position_close" - }[type], serializers.Position.parse(*stream)) - - def __trades_channel_handler(self, type, stream): - if type == "te": - self.event_emitter.emit("trade_executed", serializers.TradeExecuted.parse(*stream)) - - if type == "tu": - self.event_emitter.emit("trade_execution_update", serializers.TradeExecutionUpdate.parse(*stream)) - - def __funding_offers_channel_handler(self, type, stream): - if type == "fos": - return self.event_emitter.emit("funding_offer_snapshot", [ - serializers.FundingOffer.parse(*substream) for substream in stream - ]) - - if type in [ "fon", "fou", "foc" ]: - return self.event_emitter.emit({ - "fon": "funding_offer_new", - "fou": "funding_offer_update", - "foc": "funding_offer_cancel" - }[type], serializers.FundingOffer.parse(*stream)) - - def __funding_credits_channel_handler(self, type, stream): - if type == "fcs": - return self.event_emitter.emit("funding_credit_snapshot", [ - serializers.FundingCredit.parse(*substream) for substream in stream - ]) - - if type in [ "fcn", "fcu", "fcc" ]: - return self.event_emitter.emit({ - "fcn": "funding_credit_new", - "fcu": "funding_credit_update", - "fcc": "funding_credit_close" - }[type], serializers.FundingCredit.parse(*stream)) - - def __funding_loans_channel_handler(self, type, stream): - if type == "fls": - return self.event_emitter.emit("funding_loan_snapshot", [ - serializers.FundingLoan.parse(*substream) for substream in stream - ]) - - if type in [ "fln", "flu", "flc" ]: - return self.event_emitter.emit({ - "fln": "funding_loan_new", - "flu": "funding_loan_update", - "flc": "funding_loan_close" - }[type], serializers.FundingLoan.parse(*stream)) - - def __wallets_channel_handler(self, type, stream): - if type == "ws": - return self.event_emitter.emit("wallet_snapshot", [ - serializers.Wallet.parse(*substream) for substream in stream - ]) - - if type == "wu": - return self.event_emitter.emit("wallet_update", serializers.Wallet.parse(*stream)) - - def __balance_info_channel_handler(self, type, stream): - if type == "bu": - return self.event_emitter.emit("balance_update", serializers.BalanceInfo.parse(*stream)) \ No newline at end of file + raise BfxWebsocketException(f"Event of type <{type}> not found in self.__handlers.") \ No newline at end of file From 3a09ba2e90ede12c43bb85073d9dc5af235d15a3 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 24 Nov 2022 17:31:47 +0100 Subject: [PATCH 041/265] Add filter parameter to BfxWebsocketClient's __init__.py. Rewrite .notify coroutine adding new feature. Add Notifications channel handlers in handlers.py. Add Notification serializer in serializers.py. --- bfxapi/websocket/BfxWebsocketClient.py | 10 +++++++--- bfxapi/websocket/handlers.py | 10 ++++++++-- bfxapi/websocket/serializers.py | 15 +++++++++++++++ 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 000efc3..5800f72 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -30,10 +30,10 @@ class BfxWebsocketClient(object): *AuthenticatedChannelsHandler.EVENTS ] - def __init__(self, host, buckets=5, log_level = "INFO", API_KEY=None, API_SECRET=None): + def __init__(self, host, buckets=5, log_level = "INFO", API_KEY=None, API_SECRET=None, filter=None): self.host, self.websocket, self.event_emitter = host, None, AsyncIOEventEmitter() - self.API_KEY, self.API_SECRET, self.authentication = API_KEY, API_SECRET, False + self.API_KEY, self.API_SECRET, self.filter, self.authentication = API_KEY, API_SECRET, filter, False self.handler = AuthenticatedChannelsHandler(event_emitter=self.event_emitter) @@ -45,7 +45,7 @@ class BfxWebsocketClient(object): 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)) + tasks.append(self.__connect(self.API_KEY, self.API_SECRET, self.filter)) await asyncio.gather(*tasks) @@ -113,6 +113,10 @@ class BfxWebsocketClient(object): return wrapper + @__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 new_order(self, data): await self.websocket.send(json.dumps([ 0, "on", None, data ])) diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index 8c4170d..ad3f7b1 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -146,12 +146,15 @@ class AuthenticatedChannelsHandler(object): ("bu",): serializers.BalanceInfo } - EVENTS = list(__abbreviations.values()) + EVENTS = [ "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] @@ -162,4 +165,7 @@ class AuthenticatedChannelsHandler(object): 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.") \ No newline at end of file + raise BfxWebsocketException(f"Event of type <{type}> not found in self.__handlers.") + + def __notification(self, stream): + return self.event_emitter.emit("notification", serializers.Notification.parse(*stream)) \ No newline at end of file diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index b8796fd..ed92d82 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -305,4 +305,19 @@ BalanceInfo = _Serializer("BalanceInfo", labels=[ "AUM_NET", ]) +#endregion + +#region Serializers definition for Notifications channel + +Notification = _Serializer("Notification", labels=[ + "MTS", + "TYPE", + "MESSAGE_ID", + "_PLACEHOLDER", + "NOTIFY_INFO", + "CODE", + "STATUS", + "TEXT" +]) + #endregion \ No newline at end of file From a5083c36cf7f53fad1bb97ca4e3e2a2c715eb0e0 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 24 Nov 2022 17:48:41 +0100 Subject: [PATCH 042/265] Add type hinting using typings.py in bfxapi/websocket/serializers.py. --- bfxapi/websocket/serializers.py | 52 ++++++++++++++++++--------------- bfxapi/websocket/typings.py | 19 ++++++++++-- 2 files changed, 45 insertions(+), 26 deletions(-) diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index ed92d82..64573cc 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -1,10 +1,16 @@ +from typing import Generic, TypeVar, Iterable, List, Any + +from . import typings + from .exceptions import BfxWebsocketException -class _Serializer(object): - def __init__(self, name, labels): +T = TypeVar("T") + +class _Serializer(Generic[T]): + def __init__(self, name: str, labels: List[str]): self.name, self.__labels = name, labels - def __serialize(self, *args, IGNORE = [ "_PLACEHOLDER" ]): + def __serialize(self, *args: Any, IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> Iterable[T]: if len(self.__labels) != len(args): raise BfxWebsocketException(" and <*args> arguments should contain the same amount of elements.") @@ -12,12 +18,12 @@ class _Serializer(object): if label not in IGNORE: yield label, args[index] - def parse(self, *values): + def parse(self, *values: Any) -> T: return dict(self.__serialize(*values)) #region Serializers definition for Websocket Public Channels -TradingPairTicker = _Serializer("TradingPairTicker", labels=[ +TradingPairTicker = _Serializer[typings.TradingPairTicker]("TradingPairTicker", labels=[ "BID", "BID_SIZE", "ASK", @@ -30,7 +36,7 @@ TradingPairTicker = _Serializer("TradingPairTicker", labels=[ "LOW" ]) -FundingCurrencyTicker = _Serializer("FundingCurrencyTicker", labels=[ +FundingCurrencyTicker = _Serializer[typings.FundingCurrencyTicker]("FundingCurrencyTicker", labels=[ "FRR", "BID", "BID_PERIOD", @@ -49,14 +55,14 @@ FundingCurrencyTicker = _Serializer("FundingCurrencyTicker", labels=[ "FRR_AMOUNT_AVAILABLE" ]) -TradingPairTrade = _Serializer("TradingPairTrade", labels=[ +TradingPairTrade = _Serializer[typings.TradingPairTrade]("TradingPairTrade", labels=[ "ID", "MTS", "AMOUNT", "PRICE" ]) -FundingCurrencyTrade = _Serializer("FundingCurrencyTrade", labels=[ +FundingCurrencyTrade = _Serializer[typings.FundingCurrencyTrade]("FundingCurrencyTrade", labels=[ "ID", "MTS", "AMOUNT", @@ -64,33 +70,33 @@ FundingCurrencyTrade = _Serializer("FundingCurrencyTrade", labels=[ "PERIOD" ]) -TradingPairBook = _Serializer("TradingPairBook", labels=[ +TradingPairBook = _Serializer[typings.TradingPairBook]("TradingPairBook", labels=[ "PRICE", "COUNT", "AMOUNT" ]) -FundingCurrencyBook = _Serializer("FundingCurrencyBook", labels=[ +FundingCurrencyBook = _Serializer[typings.FundingCurrencyBook]("FundingCurrencyBook", labels=[ "RATE", "PERIOD", "COUNT", "AMOUNT" ]) -TradingPairRawBook = _Serializer("TradingPairRawBook", labels=[ +TradingPairRawBook = _Serializer[typings.TradingPairRawBook]("TradingPairRawBook", labels=[ "ORDER_ID", "PRICE", "AMOUNT" ]) -FundingCurrencyRawBook = _Serializer("FundingCurrencyRawBook", labels=[ +FundingCurrencyRawBook = _Serializer[typings.FundingCurrencyRawBook]("FundingCurrencyRawBook", labels=[ "OFFER_ID", "PERIOD", "RATE", "AMOUNT" ]) -Candle = _Serializer("Candle", labels=[ +Candle = _Serializer[typings.Candle]("Candle", labels=[ "MTS", "OPEN", "CLOSE", @@ -99,7 +105,7 @@ Candle = _Serializer("Candle", labels=[ "VOLUME" ]) -DerivativesStatus = _Serializer("DerivativesStatus", labels=[ +DerivativesStatus = _Serializer[typings.DerivativesStatus]("DerivativesStatus", labels=[ "TIME_MS", "_PLACEHOLDER", "DERIV_PRICE", @@ -129,7 +135,7 @@ DerivativesStatus = _Serializer("DerivativesStatus", labels=[ #region Serializers definition for Websocket Authenticated Channels -Order = _Serializer("Order", labels=[ +Order = _Serializer[typings.Order]("Order", labels=[ "ID", "GID", "CID", @@ -164,7 +170,7 @@ Order = _Serializer("Order", labels=[ "META" ]) -Position = _Serializer("Position", labels=[ +Position = _Serializer[typings.Position]("Position", labels=[ "SYMBOL", "STATUS", "AMOUNT", @@ -187,7 +193,7 @@ Position = _Serializer("Position", labels=[ "META" ]) -TradeExecuted = _Serializer("TradeExecuted", labels=[ +TradeExecuted = _Serializer[typings.TradeExecuted]("TradeExecuted", labels=[ "ID", "SYMBOL", "MTS_CREATE", @@ -202,7 +208,7 @@ TradeExecuted = _Serializer("TradeExecuted", labels=[ "CID" ]) -TradeExecutionUpdate = _Serializer("TradeExecutionUpdate", labels=[ +TradeExecutionUpdate = _Serializer[typings.TradeExecutionUpdate]("TradeExecutionUpdate", labels=[ "ID", "SYMBOL", "MTS_CREATE", @@ -217,7 +223,7 @@ TradeExecutionUpdate = _Serializer("TradeExecutionUpdate", labels=[ "CID" ]) -FundingOffer = _Serializer("FundingOffer", labels=[ +FundingOffer = _Serializer[typings.FundingOffer]("FundingOffer", labels=[ "ID", "SYMBOL", "MTS_CREATED", @@ -241,7 +247,7 @@ FundingOffer = _Serializer("FundingOffer", labels=[ "_PLACEHOLDER" ]) -FundingCredit = _Serializer("FundingCredit", labels=[ +FundingCredit = _Serializer[typings.FundingCredit]("FundingCredit", labels=[ "ID", "SYMBOL", "SIDE", @@ -266,7 +272,7 @@ FundingCredit = _Serializer("FundingCredit", labels=[ "POSITION_PAIR" ]) -FundingLoan = _Serializer("FundingLoan", labels=[ +FundingLoan = _Serializer[typings.FundingLoan]("FundingLoan", labels=[ "ID", "SYMBOL", "SIDE", @@ -290,7 +296,7 @@ FundingLoan = _Serializer("FundingLoan", labels=[ "NO_CLOSE" ]) -Wallet = _Serializer("Wallet", labels=[ +Wallet = _Serializer[typings.Wallet]("Wallet", labels=[ "WALLET_TYPE", "CURRENCY", "BALANCE", @@ -300,7 +306,7 @@ Wallet = _Serializer("Wallet", labels=[ "META" ]) -BalanceInfo = _Serializer("BalanceInfo", labels=[ +BalanceInfo = _Serializer[typings.BalanceInfo]("BalanceInfo", labels=[ "AUM", "AUM_NET", ]) diff --git a/bfxapi/websocket/typings.py b/bfxapi/websocket/typings.py index 55c37ef..fe4b2de 100644 --- a/bfxapi/websocket/typings.py +++ b/bfxapi/websocket/typings.py @@ -183,7 +183,7 @@ Position = TypedDict("Position", { Positions = List[Position] -Trade = TypedDict("Trade", { +TradeExecuted = TypedDict("TradeExecuted", { "ID": int, "SYMBOL": str, "MTS_CREATE": int, @@ -193,8 +193,21 @@ Trade = TypedDict("Trade", { "ORDER_TYPE": str, "ORDER_PRICE": float, "MAKER":int, - "FEE": Optional[float], - "FEE_CURRENCY": Optional[str], + "CID": int +}) + +TradeExecutionUpdate = TypedDict("TradeExecutionUpdate", { + "ID": int, + "SYMBOL": str, + "MTS_CREATE": int, + "ORDER_ID": int, + "EXEC_AMOUNT": float, + "EXEC_PRICE": float, + "ORDER_TYPE": str, + "ORDER_PRICE": float, + "MAKER":int, + "FEE": float, + "FEE_CURRENCY": str, "CID": int }) From 9e8bea905a6038cc8179018a61abb77499ab1c51 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 28 Nov 2022 16:50:07 +0100 Subject: [PATCH 043/265] Rename .chanIds field in _BfxWebsocketBucket to .subscriptions. --- bfxapi/websocket/BfxWebsocketClient.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 5800f72..14c3bb0 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -86,7 +86,7 @@ class BfxWebsocketClient(object): await self.websocket.send(json.dumps(data)) async def subscribe(self, channel, **kwargs): - counters = [ len(bucket.pendings) + len(bucket.chanIds) for bucket in self.buckets ] + counters = [ len(bucket.pendings) + len(bucket.subscriptions) for bucket in self.buckets ] index = counters.index(min(counters)) @@ -94,7 +94,7 @@ class BfxWebsocketClient(object): async def unsubscribe(self, chanId): for bucket in self.buckets: - if chanId in bucket.chanIds.keys(): + if chanId in bucket.subscriptions.keys(): await bucket._unsubscribe(chanId=chanId) async def close(self, code=1000, reason=str()): @@ -149,7 +149,7 @@ class _BfxWebsocketBucket(object): def __init__(self, host, event_emitter, __bucket_open_signal): self.host, self.event_emitter, self.__bucket_open_signal = host, event_emitter, __bucket_open_signal - self.websocket, self.chanIds, self.pendings = None, dict(), list() + self.websocket, self.subscriptions, self.pendings = None, dict(), list() self.handler = PublicChannelsHandler(event_emitter=self.event_emitter) @@ -168,21 +168,21 @@ class _BfxWebsocketBucket(object): raise OutdatedClientVersion(f"Mismatch between the client version and the server version. Update the library to the latest version to continue (client version: {BfxWebsocketClient.VERSION}, server version: {message['version']}).") elif isinstance(message, dict) and message["event"] == "subscribed" and (chanId := message["chanId"]): self.pendings = [ pending for pending in self.pendings if pending["subId"] != message["subId"] ] - self.chanIds[chanId] = message + self.subscriptions[chanId] = message self.event_emitter.emit("subscribed", message) elif isinstance(message, dict) and message["event"] == "unsubscribed" and (chanId := message["chanId"]): if message["status"] == "OK": - del self.chanIds[chanId] + del self.subscriptions[chanId] elif isinstance(message, dict) and message["event"] == "error": self.event_emitter.emit("wss-error", message["code"], message["msg"]) elif isinstance(message, list) and (chanId := message[0]) and message[1] != _HEARTBEAT: - self.handler.handle(self.chanIds[chanId], *message[1:]) + self.handler.handle(self.subscriptions[chanId], *message[1:]) except websockets.ConnectionClosedError: continue finally: await self.websocket.wait_closed(); break @_require_websocket_connection async def _subscribe(self, channel, subId=None, **kwargs): - if len(self.chanIds) + 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.") subscription = { From 4bb62a942fa162ccafd803fc3cdafc2396af416e Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 28 Nov 2022 16:58:09 +0100 Subject: [PATCH 044/265] Allow users to use .on and .once as non-decorators methods. --- bfxapi/websocket/BfxWebsocketClient.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 14c3bb0..6a67982 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -125,22 +125,26 @@ class BfxWebsocketClient(object): if all(bucket.websocket != None and bucket.websocket.open == True for bucket in self.buckets): self.event_emitter.emit("open") - def on(self, event): + 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): + def once(self, event, callback = None): if event not in BfxWebsocketClient.EVENTS: raise EventNotSupported(f"Event <{event}> is not supported. To get a list of available events print BfxWebsocketClient.EVENTS") + if callback != None: + return self.event_emitter.once(event, callback) + def handler(function): self.event_emitter.once(event, function) - return handler class _BfxWebsocketBucket(object): From fa073823ce6c8fb0c688c6e3d0f88b0ecd585fb7 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 28 Nov 2022 17:08:27 +0100 Subject: [PATCH 045/265] Add .run method in BfxWebsocketClient class. --- bfxapi/websocket/BfxWebsocketClient.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 6a67982..4e7d1d1 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -41,6 +41,9 @@ class BfxWebsocketClient(object): self.logger = CustomLogger("BfxWebsocketClient", logLevel=log_level) + def run(self): + return asyncio.run(self.start()) + async def start(self): tasks = [ bucket._connect(index) for index, bucket in enumerate(self.buckets) ] From b308bbacbd603901b335b82e8c6d244ac77f8282 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 29 Nov 2022 15:26:02 +0100 Subject: [PATCH 046/265] Add Flags enumeration to enums.py. Rename new_order and new_position to order_new and position_new to mantain consistency. Add _BfxWebsocketInputs class with order_new, order_update and order_cancel inputs. --- bfxapi/websocket/BfxWebsocketClient.py | 25 ++++++++++++++++++++----- bfxapi/websocket/enums.py | 8 ++++++++ bfxapi/websocket/handlers.py | 4 ++-- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 4e7d1d1..94aeaca 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -39,6 +39,8 @@ class BfxWebsocketClient(object): 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) def run(self): @@ -105,7 +107,7 @@ class BfxWebsocketClient(object): await self.websocket.close(code=code, reason=reason) for bucket in self.buckets: - await bucket.close(code=code, reason=reason) + await bucket._close(code=code, reason=reason) def __require_websocket_authentication(function): async def wrapper(self, *args, **kwargs): @@ -121,8 +123,8 @@ class BfxWebsocketClient(object): await self.websocket.send(json.dumps([ 0, "n", MESSAGE_ID, { "type": "ucm-test", "info": info, **kwargs } ])) @__require_websocket_authentication - async def new_order(self, data): - await self.websocket.send(json.dumps([ 0, "on", None, data ])) + async def __handle_websocket_input(self, input, data): + await self.websocket.send(json.dumps([ 0, input, None, data])) def __bucket_open_signal(self, index): if all(bucket.websocket != None and bucket.websocket.open == True for bucket in self.buckets): @@ -212,5 +214,18 @@ class _BfxWebsocketBucket(object): })) @_require_websocket_connection - async def close(self, code=1000, reason=str()): - await self.websocket.close(code=code, reason=reason) \ No newline at end of file + async def _close(self, code=1000, reason=str()): + await self.websocket.close(code=code, reason=reason) + +class _BfxWebsocketInputs(object): + def __init__(self, __handle_websocket_input): + self.__handle_websocket_input = __handle_websocket_input + + async def order_new(self, data): + await self.__handle_websocket_input("on", data) + + async def order_update(self, data): + await self.__handle_websocket_input("ou", data) + + async def order_cancel(self, data): + await self.__handle_websocket_input("oc", data) \ No newline at end of file diff --git a/bfxapi/websocket/enums.py b/bfxapi/websocket/enums.py index 9a89eb0..14c4234 100644 --- a/bfxapi/websocket/enums.py +++ b/bfxapi/websocket/enums.py @@ -7,6 +7,14 @@ class Channels(str, Enum): CANDLES = "candles" STATUS = "status" +class Flags(int, Enum): + HIDDEN = 64 + CLOSE = 512 + REDUCE_ONLY = 1024 + POST_ONLY = 4096 + OCO = 16384 + NO_VAR_RATES = 524288 + class Errors(int, Enum): ERR_UNK = 10000 ERR_GENERIC = 10001 diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index ad3f7b1..9edd986 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -124,8 +124,8 @@ class PublicChannelsHandler(object): class AuthenticatedChannelsHandler(object): __abbreviations = { - "os": "order_snapshot", "on": "new_order", "ou": "order_update", "oc": "order_cancel", - "ps": "position_snapshot", "pn": "new_position", "pu": "position_update", "pc": "position_close", + "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", From e262c81f97da04c3c69c1aafa044f7d8e91f1f5c Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 29 Nov 2022 17:17:17 +0100 Subject: [PATCH 047/265] Change default log_level value (from 'INFO' to 'ERROR'). Implement error event into AsyncIOEventEmitter. Add error logging. --- bfxapi/client.py | 2 +- bfxapi/websocket/BfxWebsocketClient.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/bfxapi/client.py b/bfxapi/client.py index e53ca66..104388d 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -7,7 +7,7 @@ class Constants(str, Enum): PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2" class Client(object): - def __init__(self, WSS_HOST: str = Constants.WSS_HOST, API_KEY: str = None, API_SECRET: str = None, log_level: str = "INFO"): + def __init__(self, WSS_HOST: str = Constants.WSS_HOST, API_KEY: str = None, API_SECRET: str = None, log_level: str = "ERROR"): self.wss = BfxWebsocketClient( host=WSS_HOST, API_KEY=API_KEY, diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 94aeaca..a0a4ac2 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -30,9 +30,11 @@ class BfxWebsocketClient(object): *AuthenticatedChannelsHandler.EVENTS ] - def __init__(self, host, buckets=5, log_level = "INFO", API_KEY=None, API_SECRET=None, filter=None): + def __init__(self, host, buckets=5, log_level = "ERROR", 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 message: self.logger.error(message)) + self.API_KEY, self.API_SECRET, self.filter, self.authentication = API_KEY, API_SECRET, filter, False self.handler = AuthenticatedChannelsHandler(event_emitter=self.event_emitter) From bfd05cf7c739d4db00baee22f5069f92f64ccfdf Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 29 Nov 2022 17:53:39 +0100 Subject: [PATCH 048/265] Add traceback import and implementation. --- bfxapi/websocket/BfxWebsocketClient.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index a0a4ac2..ba5593e 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -1,4 +1,4 @@ -import json, asyncio, hmac, hashlib, time, uuid, websockets +import traceback, json, asyncio, hmac, hashlib, time, uuid, websockets from enum import Enum @@ -33,7 +33,7 @@ class BfxWebsocketClient(object): def __init__(self, host, buckets=5, log_level = "ERROR", 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 message: self.logger.error(message)) + self.event_emitter.add_listener("error", lambda message: self.logger.error(str(message) + "\n" + traceback.format_exc())) self.API_KEY, self.API_SECRET, self.filter, self.authentication = API_KEY, API_SECRET, filter, False From d9ecbaa9f01910ee1719d26b6e670e27fb998ddc Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 29 Nov 2022 18:05:20 +0100 Subject: [PATCH 049/265] Change exception formatting inside error .add_listener in BfxWebsocketClient. --- bfxapi/websocket/BfxWebsocketClient.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index ba5593e..059ac95 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -33,7 +33,9 @@ class BfxWebsocketClient(object): def __init__(self, host, buckets=5, log_level = "ERROR", 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 message: self.logger.error(str(message) + "\n" + traceback.format_exc())) + self.event_emitter.add_listener("error", + lambda exception: self.logger.error("\n" + str().join(traceback.format_exception(type(exception), exception, exception.__traceback__))[:-1]) + ) self.API_KEY, self.API_SECRET, self.filter, self.authentication = API_KEY, API_SECRET, filter, False From e71d4b6e263b3055f5142b146b2f6cdb4620afca Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 30 Nov 2022 17:24:51 +0100 Subject: [PATCH 050/265] Add new websocket authenticated inputs in _BfxWebsocketInputs class. Add new type hinting inside bfxapi/websocket/typings.py. --- bfxapi/websocket/BfxWebsocketClient.py | 19 ++++++-- bfxapi/websocket/typings.py | 65 +++++++++++++++++++++++++- 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 059ac95..3fa2281 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -4,8 +4,8 @@ from enum import Enum from pyee.asyncio import AsyncIOEventEmitter +from .typings import Inputs from .handlers import Channels, PublicChannelsHandler, AuthenticatedChannelsHandler - from .exceptions import ConnectionNotOpen, TooManySubscriptions, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion from ..utils.logger import Formatter, CustomLogger @@ -225,11 +225,20 @@ class _BfxWebsocketInputs(object): def __init__(self, __handle_websocket_input): self.__handle_websocket_input = __handle_websocket_input - async def order_new(self, data): + async def order_new(self, data: Inputs.Order.New): await self.__handle_websocket_input("on", data) - async def order_update(self, data): + async def order_update(self, data: Inputs.Order.Update): await self.__handle_websocket_input("ou", data) - async def order_cancel(self, data): - await self.__handle_websocket_input("oc", data) \ No newline at end of file + async def order_cancel(self, data: Inputs.Order.Cancel): + await self.__handle_websocket_input("oc", data) + + async def offer_new(self, data: Inputs.Offer.New): + await self.__handle_websocket_input("fon", data) + + async def offer_cancel(self, data: Inputs.Offer.Cancel): + await self.__handle_websocket_input("foc", data) + + async def calc(self, *args: str): + await self.__handle_websocket_input("calc", list(map(lambda arg: [arg], args))) \ No newline at end of file diff --git a/bfxapi/websocket/typings.py b/bfxapi/websocket/typings.py index fe4b2de..8bd5dde 100644 --- a/bfxapi/websocket/typings.py +++ b/bfxapi/websocket/typings.py @@ -1,4 +1,10 @@ -from typing import Type, List, Dict, TypedDict, Union, Optional +from decimal import Decimal + +from datetime import datetime + +from typing import Type, List, Dict, TypedDict, Union, Optional, Any + +int16 = int32 = int45 = int64 = int JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] @@ -292,4 +298,61 @@ BalanceInfo = TypedDict("BalanceInfo", { "AUM_NET": float }) +#endregion + +#region Type hinting for Websocket Authenticated Inputs + +class Inputs: + class Order: + New = TypedDict("Inputs.Order.New", { + "gid": Optional[int32], + "cid": int45, + "type": str, + "symbol": str, + "amount": Union[Decimal, str], + "price": Union[Decimal, str], + "lev": int, + "price_trailing": Union[Decimal, str], + "price_aux_limit": Union[Decimal, str], + "price_oco_stop": Union[Decimal, str], + "flags": int16, + "tif": Union[datetime, str], + "meta": JSON + }) + + Update = TypedDict("Inputs.Order.Update", { + "id": int64, + "cid": int45, + "cid_date": str, + "gid": int32, + "price": Union[Decimal, str], + "amount": Union[Decimal, str], + "lev": int, + "delta": Union[Decimal, str], + "price_aux_limit": Union[Decimal, str], + "price_trailing": Union[Decimal, str], + "flags": int16, + "tif": Union[datetime, str] + }) + + Cancel = TypedDict("Inputs.Order.Cancel", { + "id": int64, + "cid": int45, + "cid_date": str + }) + + class Offer: + New = TypedDict("Inputs.Offer.New", { + "type": str, + "symbol": str, + "amount": Union[Decimal, str], + "rate": Union[Decimal, str], + "period": int, + "flags": int16 + }) + + Cancel = TypedDict("Inputs.Offer.Cancel", { + "id": int + }) + #endregion \ No newline at end of file From 509223ae9b54310e99704f29ab465cbf4aab7708 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 30 Nov 2022 17:34:13 +0100 Subject: [PATCH 051/265] Add order_multiple_operations input in _BfxWebsocketInputs. --- bfxapi/websocket/BfxWebsocketClient.py | 5 ++++- bfxapi/websocket/typings.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 3fa2281..0884c6f 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -4,7 +4,7 @@ from enum import Enum from pyee.asyncio import AsyncIOEventEmitter -from .typings import Inputs +from .typings import Inputs, Tuple, Union from .handlers import Channels, PublicChannelsHandler, AuthenticatedChannelsHandler from .exceptions import ConnectionNotOpen, TooManySubscriptions, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion @@ -234,6 +234,9 @@ class _BfxWebsocketInputs(object): async def order_cancel(self, data: Inputs.Order.Cancel): await self.__handle_websocket_input("oc", data) + async def order_multiple_operations(self, *args: Tuple[str, Union[Inputs.Order.New, Inputs.Order.Update, Inputs.Order.Cancel]]): + await self.__handle_websocket_input("ox_multi", args) + async def offer_new(self, data: Inputs.Offer.New): await self.__handle_websocket_input("fon", data) diff --git a/bfxapi/websocket/typings.py b/bfxapi/websocket/typings.py index 8bd5dde..4a3b918 100644 --- a/bfxapi/websocket/typings.py +++ b/bfxapi/websocket/typings.py @@ -2,7 +2,7 @@ from decimal import Decimal from datetime import datetime -from typing import Type, List, Dict, TypedDict, Union, Optional, Any +from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any int16 = int32 = int45 = int64 = int From 48f1745f10de3de0b72b5babe91ed5456b218ce1 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 30 Nov 2022 17:55:10 +0100 Subject: [PATCH 052/265] Add MAXIMUM_BUCKETS_AMOUNT constant to BfxWebsocketClient class. --- bfxapi/client.py | 2 +- bfxapi/websocket/BfxWebsocketClient.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/bfxapi/client.py b/bfxapi/client.py index 104388d..d019e90 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -7,7 +7,7 @@ class Constants(str, Enum): PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2" class Client(object): - def __init__(self, WSS_HOST: str = Constants.WSS_HOST, API_KEY: str = None, API_SECRET: str = None, log_level: str = "ERROR"): + def __init__(self, WSS_HOST: str = Constants.WSS_HOST, API_KEY: str = None, API_SECRET: str = None, log_level: str = "WARNING"): self.wss = BfxWebsocketClient( host=WSS_HOST, API_KEY=API_KEY, diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 0884c6f..e8bb4d0 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -24,13 +24,15 @@ def _require_websocket_connection(function): class BfxWebsocketClient(object): VERSION = 2 + MAXIMUM_BUCKETS_AMOUNT = 20 + EVENTS = [ "open", "subscribed", "authenticated", "wss-error", *PublicChannelsHandler.EVENTS, *AuthenticatedChannelsHandler.EVENTS ] - def __init__(self, host, buckets=5, log_level = "ERROR", API_KEY=None, API_SECRET=None, filter=None): + 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", @@ -47,6 +49,9 @@ class BfxWebsocketClient(object): 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()) From 4cfeab8a79b877a8bbc13a4e39e355b637cb916e Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 30 Nov 2022 18:25:15 +0100 Subject: [PATCH 053/265] Add barebone files for rest section. --- bfxapi/client.py | 3 +++ bfxapi/rest/BfxRestInterface.py | 12 ++++++++++++ bfxapi/rest/__init__.py | 1 + bfxapi/rest/exceptions.py | 6 ++++++ bfxapi/rest/serializers.py | 30 ++++++++++++++++++++++++++++++ bfxapi/rest/typings.py | 9 +++++++++ bfxapi/websocket/exceptions.py | 2 +- requirements.txt | Bin 116 -> 304 bytes setup.py | 5 +++++ 9 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 bfxapi/rest/BfxRestInterface.py create mode 100644 bfxapi/rest/__init__.py create mode 100644 bfxapi/rest/exceptions.py create mode 100644 bfxapi/rest/serializers.py create mode 100644 bfxapi/rest/typings.py diff --git a/bfxapi/client.py b/bfxapi/client.py index d019e90..dd2fe93 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -3,6 +3,9 @@ from .websocket import BfxWebsocketClient from enum import Enum class Constants(str, Enum): + REST_HOST = "https://api.bitfinex.com/v2" + PUB_REST_HOST = "https://api-pub.bitfinex.com/v2" + WSS_HOST = "wss://api.bitfinex.com/ws/2" PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2" diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py new file mode 100644 index 0000000..f7eed3b --- /dev/null +++ b/bfxapi/rest/BfxRestInterface.py @@ -0,0 +1,12 @@ +import requests +from . import serializers +from .typings import PlatformStatus + +class BfxRestInterface(object): + def __init__(self, host): + self.host = host + + def platform_status(self) -> PlatformStatus: + return serializers.PlatformStatus.parse( + *requests.get(f"{self.host}/platform/status").json() + ) diff --git a/bfxapi/rest/__init__.py b/bfxapi/rest/__init__.py new file mode 100644 index 0000000..0bf3d2e --- /dev/null +++ b/bfxapi/rest/__init__.py @@ -0,0 +1 @@ +from .BfxRestInterface import BfxRestInterface \ No newline at end of file diff --git a/bfxapi/rest/exceptions.py b/bfxapi/rest/exceptions.py new file mode 100644 index 0000000..9aa8555 --- /dev/null +++ b/bfxapi/rest/exceptions.py @@ -0,0 +1,6 @@ +class BfxRestException(Exception): + """ + Base class for all exceptions defined in bfxapi/rest/exceptions.py. + """ + + pass \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py new file mode 100644 index 0000000..5ffcb0e --- /dev/null +++ b/bfxapi/rest/serializers.py @@ -0,0 +1,30 @@ +from typing import Generic, TypeVar, Iterable, List, Any + +from . import typings + +from .exceptions import BfxRestException + +T = TypeVar("T") + +class _Serializer(Generic[T]): + def __init__(self, name: str, labels: List[str]): + self.name, self.__labels = name, labels + + def __serialize(self, *args: Any, IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> Iterable[T]: + if len(self.__labels) != len(args): + raise BfxRestException(" and <*args> arguments should contain the same amount of elements.") + + for index, label in enumerate(self.__labels): + if label not in IGNORE: + yield label, args[index] + + def parse(self, *values: Any) -> T: + return dict(self.__serialize(*values)) + +#region Serializers definition for Rest Public Endpoints + +PlatformStatus = _Serializer[typings.PlatformStatus]("PlatformStatus", labels=[ + "OPERATIVE" +]) + +#endregion \ No newline at end of file diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py new file mode 100644 index 0000000..dd7732e --- /dev/null +++ b/bfxapi/rest/typings.py @@ -0,0 +1,9 @@ +from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any + +#region Type hinting for Rest Public Endpoints + +PlatformStatus = TypedDict("PlatformStatus", { + "OPERATIVE": int +}) + +#endregion \ No newline at end of file diff --git a/bfxapi/websocket/exceptions.py b/bfxapi/websocket/exceptions.py index 3a1f900..c55b767 100644 --- a/bfxapi/websocket/exceptions.py +++ b/bfxapi/websocket/exceptions.py @@ -9,7 +9,7 @@ __all__ = [ class BfxWebsocketException(Exception): """ - Base class for all exceptions defined in bfx/websocket/errors.py. + Base class for all exceptions defined in bfxapi/websocket/exceptions.py. """ pass diff --git a/requirements.txt b/requirements.txt index 78560fa31112931f95f0af059daf152fa323d63f..549f7a67024ef377ed41842b9f7abfb02005904f 100644 GIT binary patch literal 304 zcmYk1Sq_3g5JcqV)Vna7(i4a0x}Sn!>eyvC5ALGMb+!#^(<7X*Pu#!X2TUL zWMTzcGqF<5HB+IZGMooX%)9xrb3c=|B;=$WoPdI5XoWs2^6RJO0vCzayN! z^r#n&+#Orow~ueJnwSIWj-8DT+Ty(7EIB!})}FpcYD~}e?MJ%S)-+dQRNpby(x{1f Su26C)E&qDr{kK2*>-PpCnJ!8I delta 34 lcmdnMR3iTWUjaiULn;v30-+^?9)kf8n@n79Ihlu11pu!12%Z1{ diff --git a/setup.py b/setup.py index c130b0d..e6fa6e3 100644 --- a/setup.py +++ b/setup.py @@ -11,8 +11,13 @@ setup( description="Official Bitfinex Python API", keywords="bitfinex,api,trading", install_requires=[ + "certifi~=2022.9.24", + "charset-normalizer~=2.1.1", + "idna~=3.4", "pyee~=9.0.4", + "requests~=2.28.1", "typing_extensions~=4.4.0", + "urllib3~=1.26.13", "websockets~=10.4", ], project_urls={ From 8e8719e3d71ba25dbf5510fd15fd322da2a9cc19 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 30 Nov 2022 18:25:50 +0100 Subject: [PATCH 054/265] Add bfxapi.rest subpackage to setup.py. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e6fa6e3..a4c8397 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from distutils.core import setup setup( name="bitfinex-api-py", version="3.0.0", - packages=[ "bfxapi", "bfxapi.websocket", "bfxapi.utils" ], + packages=[ "bfxapi", "bfxapi.websocket", "bfxapi.rest", "bfxapi.utils" ], url="https://github.com/bitfinexcom/bitfinex-api-py", license="OSI Approved :: Apache Software License", author="Bitfinex", From ea6044a5eb7ab7a6ed800c51ae02e2aac8d12231 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 1 Dec 2022 17:48:50 +0100 Subject: [PATCH 055/265] Add support for new rest public endpoints (in BfxRestInterface.py, serializers.py and typings.py). --- bfxapi/rest/BfxRestInterface.py | 58 ++++++++++++++++++++-- bfxapi/rest/exceptions.py | 11 +++++ bfxapi/rest/serializers.py | 87 +++++++++++++++++++++++++++++---- bfxapi/rest/typings.py | 44 +++++++++++++++++ 4 files changed, 186 insertions(+), 14 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index f7eed3b..7423c1a 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -1,12 +1,62 @@ import requests + +from http import HTTPStatus + +from typing import List, Union, Optional + from . import serializers -from .typings import PlatformStatus +from .typings import PlatformStatus, TradingPairTicker, FundingCurrencyTicker, TickerHistory, TradingPairTrade, FundingCurrencyTrade +from .exceptions import RequestParametersError class BfxRestInterface(object): def __init__(self, host): self.host = host + def __GET(self, endpoint, params = None): + data = requests.get(f"{self.host}/{endpoint}", params=params).json() + + if data[0] == "error": + if data[1] == 10020: + raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") + + return data + def platform_status(self) -> PlatformStatus: - return serializers.PlatformStatus.parse( - *requests.get(f"{self.host}/platform/status").json() - ) + return serializers.PlatformStatus.parse(*self.__GET("platform/status")) + + def tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]: + return [ + { + "t": serializers.TradingPairTicker.parse, + "f": serializers.FundingCurrencyTicker.parse + }[subdata[0][0]](*subdata) + + for subdata in self.__GET("tickers", params={ "symbols": ",".join(symbols) }) + ] + + def ticker(self, symbol: str) -> Union[TradingPairTicker, FundingCurrencyTicker]: + return { + "t": serializers.TradingPairTicker.parse, + "f": serializers.FundingCurrencyTicker.parse + }[symbol[0]](*self.__GET(f"ticker/{symbol}"), skip=["SYMBOL"]) + + def tickers_hist(self, symbols: List[str], start: Optional[int] = None, end: Optional[int] = None, limit: Optional[int] = None) -> List[TickerHistory]: + params = { + "symbols": ",".join(symbols), + "start": start, "end": end, + "limit": limit + } + + return [ serializers.TickerHistory.parse(*subdata) for subdata in self.__GET("tickers/hist", params=params) ] + + def trades(self, symbol: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[int] = None) -> Union[List[TradingPairTrade], List[FundingCurrencyTicker]]: + params = { "symbol": symbol, "limit": limit, "start": start, "end": end, "sort": sort } + + return [ + { + "t": serializers.TradingPairTrade.parse, + "f": serializers.FundingCurrencyTrade.parse + }[symbol[0]](*subdata) + + for subdata in self.__GET(f"trades/{symbol}/hist", params=params) + ] \ No newline at end of file diff --git a/bfxapi/rest/exceptions.py b/bfxapi/rest/exceptions.py index 9aa8555..033848e 100644 --- a/bfxapi/rest/exceptions.py +++ b/bfxapi/rest/exceptions.py @@ -1,6 +1,17 @@ +__all__ = [ + "RequestParametersError" +] + class BfxRestException(Exception): """ Base class for all exceptions defined in bfxapi/rest/exceptions.py. """ + pass + +class RequestParametersError(BfxRestException): + """ + This error indicates that there are some invalid parameters sent along with an HTTP request. + """ + pass \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 5ffcb0e..a002cdf 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -1,4 +1,4 @@ -from typing import Generic, TypeVar, Iterable, List, Any +from typing import Generic, TypeVar, Iterable, Optional, List, Any from . import typings @@ -7,19 +7,21 @@ from .exceptions import BfxRestException T = TypeVar("T") class _Serializer(Generic[T]): - def __init__(self, name: str, labels: List[str]): - self.name, self.__labels = name, labels + def __init__(self, name: str, labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]): + self.name, self.__labels, self.__IGNORE = name, labels, IGNORE - def __serialize(self, *args: Any, IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> Iterable[T]: - if len(self.__labels) != len(args): - raise BfxRestException(" and <*args> arguments should contain the same amount of elements.") + def __serialize(self, *args: Any, skip: Optional[List[str]]) -> Iterable[T]: + labels = list(filter(lambda label: label not in (skip or list()), self.__labels)) - for index, label in enumerate(self.__labels): - if label not in IGNORE: + if len(labels) != len(args): + raise BfxRestException(" and <*args> arguments should contain the same amount of elements.") + + for index, label in enumerate(labels): + if label not in self.__IGNORE: yield label, args[index] - def parse(self, *values: Any) -> T: - return dict(self.__serialize(*values)) + def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: + return dict(self.__serialize(*values, skip=skip)) #region Serializers definition for Rest Public Endpoints @@ -27,4 +29,69 @@ PlatformStatus = _Serializer[typings.PlatformStatus]("PlatformStatus", labels=[ "OPERATIVE" ]) +TradingPairTicker = _Serializer[typings.TradingPairTicker]("TradingPairTicker", labels=[ + "SYMBOL", + "BID", + "BID_SIZE", + "ASK", + "ASK_SIZE", + "DAILY_CHANGE", + "DAILY_CHANGE_RELATIVE", + "LAST_PRICE", + "VOLUME", + "HIGH", + "LOW" +]) + +FundingCurrencyTicker = _Serializer[typings.FundingCurrencyTicker]("FundingCurrencyTicker", labels=[ + "SYMBOL", + "FRR", + "BID", + "BID_PERIOD", + "BID_SIZE", + "ASK", + "ASK_PERIOD", + "ASK_SIZE", + "DAILY_CHANGE", + "DAILY_CHANGE_RELATIVE", + "LAST_PRICE", + "VOLUME", + "HIGH", + "LOW", + "_PLACEHOLDER", + "_PLACEHOLDER", + "FRR_AMOUNT_AVAILABLE" +]) + +TickerHistory = _Serializer[typings.TickerHistory]("TickerHistory", labels=[ + "SYMBOL", + "BID", + "_PLACEHOLDER", + "ASK", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "MTS" +]) + +TradingPairTrade = _Serializer[typings.TradingPairTrade]("TradingPairTrade", labels=[ + "ID", + "MTS", + "AMOUNT", + "PRICE" +]) + +FundingCurrencyTrade = _Serializer[typings.FundingCurrencyTrade]("FundingCurrencyTrade", labels=[ + "ID", + "MTS", + "AMOUNT", + "RATE", + "PERIOD" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index dd7732e..ebaf7c2 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -6,4 +6,48 @@ PlatformStatus = TypedDict("PlatformStatus", { "OPERATIVE": int }) +TradingPairTicker = TypedDict("TradingPairTicker", { + "SYMBOL": Optional[str], + "BID": float, + "BID_SIZE": float, + "ASK": float, + "ASK_SIZE": float, + "DAILY_CHANGE": float, + "DAILY_CHANGE_RELATIVE": float, + "LAST_PRICE": float, + "VOLUME": float, + "HIGH": float, + "LOW": float +}) + +FundingCurrencyTicker = TypedDict("FundingCurrencyTicker", { + "SYMBOL": Optional[str], + "FRR": float, + "BID": float, + "BID_PERIOD": int, + "BID_SIZE": float, + "ASK": float, + "ASK_PERIOD": int, + "ASK_SIZE": float, + "DAILY_CHANGE": float, + "DAILY_CHANGE_RELATIVE": float, + "LAST_PRICE": float, + "VOLUME": float, + "HIGH": float, + "LOW": float, + "FRR_AMOUNT_AVAILABLE": float +}) + +TickerHistory = TypedDict("TickerHistory", { + "SYMBOL": str, + "BID": float, + "ASK": float, + "MTS": int +}) + +(TradingPairTrade, FundingCurrencyTrade) = ( + TypedDict("TradingPairTrade", { "ID": int, "MTS": int, "AMOUNT": float, "PRICE": float }), + TypedDict("FundingCurrencyTrade", { "ID": int, "MTS": int, "AMOUNT": float, "RATE": float, "PERIOD": int }) +) + #endregion \ No newline at end of file From 6e470dc925d42bbe3ce4cbee87c0b2dd2c643383 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 1 Dec 2022 17:53:57 +0100 Subject: [PATCH 056/265] Fix type hinting bug in rest section. --- bfxapi/rest/BfxRestInterface.py | 6 +++--- bfxapi/rest/typings.py | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 7423c1a..c60cbc1 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -5,7 +5,7 @@ from http import HTTPStatus from typing import List, Union, Optional from . import serializers -from .typings import PlatformStatus, TradingPairTicker, FundingCurrencyTicker, TickerHistory, TradingPairTrade, FundingCurrencyTrade +from .typings import PlatformStatus, TradingPairTicker, FundingCurrencyTicker, TickerHistories, TradingPairTrades, FundingCurrencyTrades from .exceptions import RequestParametersError class BfxRestInterface(object): @@ -40,7 +40,7 @@ class BfxRestInterface(object): "f": serializers.FundingCurrencyTicker.parse }[symbol[0]](*self.__GET(f"ticker/{symbol}"), skip=["SYMBOL"]) - def tickers_hist(self, symbols: List[str], start: Optional[int] = None, end: Optional[int] = None, limit: Optional[int] = None) -> List[TickerHistory]: + def tickers_hist(self, symbols: List[str], start: Optional[int] = None, end: Optional[int] = None, limit: Optional[int] = None) -> TickerHistories: params = { "symbols": ",".join(symbols), "start": start, "end": end, @@ -49,7 +49,7 @@ class BfxRestInterface(object): return [ serializers.TickerHistory.parse(*subdata) for subdata in self.__GET("tickers/hist", params=params) ] - def trades(self, symbol: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[int] = None) -> Union[List[TradingPairTrade], List[FundingCurrencyTicker]]: + def trades(self, symbol: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[int] = None) -> Union[TradingPairTrades, FundingCurrencyTrades]: params = { "symbol": symbol, "limit": limit, "start": start, "end": end, "sort": sort } return [ diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index ebaf7c2..d511088 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -45,9 +45,13 @@ TickerHistory = TypedDict("TickerHistory", { "MTS": int }) +TickerHistories = List[TickerHistory] + (TradingPairTrade, FundingCurrencyTrade) = ( TypedDict("TradingPairTrade", { "ID": int, "MTS": int, "AMOUNT": float, "PRICE": float }), TypedDict("FundingCurrencyTrade", { "ID": int, "MTS": int, "AMOUNT": float, "RATE": float, "PERIOD": int }) ) +(TradingPairTrades, FundingCurrencyTrades) = (List[TradingPairTrade], List[FundingCurrencyTrade]) + #endregion \ No newline at end of file From 52d007c05dd46e1acd457b8d93c81ccdbb29422e Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 1 Dec 2022 18:24:02 +0100 Subject: [PATCH 057/265] Add examples/websocket/order_book.py and raw_order_book.py demos. --- examples/websocket/order_book.py | 61 ++++++++++++++++++++++++++++ examples/websocket/raw_order_book.py | 61 ++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 examples/websocket/order_book.py create mode 100644 examples/websocket/raw_order_book.py diff --git a/examples/websocket/order_book.py b/examples/websocket/order_book.py new file mode 100644 index 0000000..3950ff9 --- /dev/null +++ b/examples/websocket/order_book.py @@ -0,0 +1,61 @@ +from collections import OrderedDict + +from bfxapi import Client, Constants + +from bfxapi.websocket import BfxWebsocketClient +from bfxapi.websocket.enums import Channels, Errors +from bfxapi.websocket.typings import Subscriptions, TradingPairBooks, TradingPairBook + +class OrderBook(object): + def __init__(self, symbols: list[str]): + self.order_book = { + symbol: { + "bids": OrderedDict(), "asks": OrderedDict() + } for symbol in symbols + } + + def update(self, symbol: str, data: TradingPairBook) -> None: + price, count, amount = data["PRICE"], data["COUNT"], data["AMOUNT"] + + kind = (amount > 0) and "bids" or "asks" + + if count > 0: + self.order_book[symbol][kind][price] = { + "price": price, + "count": count, + "amount": amount + } + + if count == 0: + if price in self.order_book[symbol][kind]: + del self.order_book[symbol][kind][price] + +SYMBOLS = [ "tBTCUSD", "tLTCUSD", "tLTCBTC", "tETHUSD", "tETHBTC" ] + +order_book = OrderBook(symbols=SYMBOLS) + +bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) + +@bfx.wss.on("wss-error") +def on_wss_error(code: Errors, msg: str): + print(code, msg) + +@bfx.wss.on("open") +async def on_open(): + for symbol in SYMBOLS: + await bfx.wss.subscribe(Channels.BOOK, symbol=symbol) + +@bfx.wss.on("subscribed") +def on_subscribed(subscription): + print(f"Subscription successful for pair <{subscription['pair']}>") + +@bfx.wss.on("t_book_snapshot") +def on_t_book_snapshot(subscription: Subscriptions.Book, snapshot: TradingPairBooks): + for data in snapshot: + order_book.update(subscription["symbol"], data) + +@bfx.wss.on("t_book_update") +def on_t_book_update(subscription: Subscriptions.Book, data: TradingPairBook): + order_book.update(subscription["symbol"], data) + +bfx.wss.run() \ No newline at end of file diff --git a/examples/websocket/raw_order_book.py b/examples/websocket/raw_order_book.py new file mode 100644 index 0000000..8198f7c --- /dev/null +++ b/examples/websocket/raw_order_book.py @@ -0,0 +1,61 @@ +from collections import OrderedDict + +from bfxapi import Client, Constants + +from bfxapi.websocket import BfxWebsocketClient +from bfxapi.websocket.enums import Channels, Errors +from bfxapi.websocket.typings import Subscriptions, TradingPairRawBooks, TradingPairRawBook + +class RawOrderBook(object): + def __init__(self, symbols: list[str]): + self.raw_order_book = { + symbol: { + "bids": OrderedDict(), "asks": OrderedDict() + } for symbol in symbols + } + + def update(self, symbol: str, data: TradingPairRawBook) -> None: + order_id, price, amount = data["ORDER_ID"], data["PRICE"], data["AMOUNT"] + + kind = (amount > 0) and "bids" or "asks" + + if price > 0: + self.raw_order_book[symbol][kind][order_id] = { + "order_id": order_id, + "price": price, + "amount": amount + } + + if price == 0: + if order_id in self.raw_order_book[symbol][kind]: + del self.raw_order_book[symbol][kind][order_id] + +SYMBOLS = [ "tBTCUSD", "tLTCUSD", "tLTCBTC", "tETHUSD", "tETHBTC" ] + +raw_order_book = RawOrderBook(symbols=SYMBOLS) + +bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) + +@bfx.wss.on("wss-error") +def on_wss_error(code: Errors, msg: str): + print(code, msg) + +@bfx.wss.on("open") +async def on_open(): + for symbol in SYMBOLS: + await bfx.wss.subscribe(Channels.BOOK, symbol=symbol, prec="R0") + +@bfx.wss.on("subscribed") +def on_subscribed(subscription): + print(f"Subscription successful for pair <{subscription['pair']}>") + +@bfx.wss.on("t_raw_book_snapshot") +def on_t_raw_book_snapshot(subscription: Subscriptions.Book, snapshot: TradingPairRawBooks): + for data in snapshot: + raw_order_book.update(subscription["symbol"], data) + +@bfx.wss.on("t_raw_book_update") +def on_t_raw_book_update(subscription: Subscriptions.Book, data: TradingPairRawBook): + raw_order_book.update(subscription["symbol"], data) + +bfx.wss.run() \ No newline at end of file From 8c9d52c1863b0675b491008a1d678ecc8823772f Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 2 Dec 2022 18:57:21 +0100 Subject: [PATCH 058/265] Rename class members in order_book.py and raw_order_book.py. --- examples/websocket/order_book.py | 8 ++++---- examples/websocket/raw_order_book.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/websocket/order_book.py b/examples/websocket/order_book.py index 3950ff9..a100c2b 100644 --- a/examples/websocket/order_book.py +++ b/examples/websocket/order_book.py @@ -8,7 +8,7 @@ from bfxapi.websocket.typings import Subscriptions, TradingPairBooks, TradingPai class OrderBook(object): def __init__(self, symbols: list[str]): - self.order_book = { + self.__order_book = { symbol: { "bids": OrderedDict(), "asks": OrderedDict() } for symbol in symbols @@ -20,15 +20,15 @@ class OrderBook(object): kind = (amount > 0) and "bids" or "asks" if count > 0: - self.order_book[symbol][kind][price] = { + self.__order_book[symbol][kind][price] = { "price": price, "count": count, "amount": amount } if count == 0: - if price in self.order_book[symbol][kind]: - del self.order_book[symbol][kind][price] + if price in self.__order_book[symbol][kind]: + del self.__order_book[symbol][kind][price] SYMBOLS = [ "tBTCUSD", "tLTCUSD", "tLTCBTC", "tETHUSD", "tETHBTC" ] diff --git a/examples/websocket/raw_order_book.py b/examples/websocket/raw_order_book.py index 8198f7c..fe10490 100644 --- a/examples/websocket/raw_order_book.py +++ b/examples/websocket/raw_order_book.py @@ -8,7 +8,7 @@ from bfxapi.websocket.typings import Subscriptions, TradingPairRawBooks, Trading class RawOrderBook(object): def __init__(self, symbols: list[str]): - self.raw_order_book = { + self.__raw_order_book = { symbol: { "bids": OrderedDict(), "asks": OrderedDict() } for symbol in symbols @@ -20,15 +20,15 @@ class RawOrderBook(object): kind = (amount > 0) and "bids" or "asks" if price > 0: - self.raw_order_book[symbol][kind][order_id] = { + self.__raw_order_book[symbol][kind][order_id] = { "order_id": order_id, "price": price, "amount": amount } if price == 0: - if order_id in self.raw_order_book[symbol][kind]: - del self.raw_order_book[symbol][kind][order_id] + if order_id in self.__raw_order_book[symbol][kind]: + del self.__raw_order_book[symbol][kind][order_id] SYMBOLS = [ "tBTCUSD", "tLTCUSD", "tLTCBTC", "tETHUSD", "tETHBTC" ] From e0785f9f4a5225db40cb2dec01a1b205e9fd95ea Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 8 Dec 2022 17:35:39 +0100 Subject: [PATCH 059/265] Add support for GET book/{Symbol}/{Precision} endpoint. --- bfxapi/rest/BfxRestInterface.py | 12 +++++++++++- bfxapi/rest/serializers.py | 26 ++++++++++++++++++++++++++ bfxapi/rest/typings.py | 14 ++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index c60cbc1..662a94c 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -5,7 +5,7 @@ from http import HTTPStatus from typing import List, Union, Optional from . import serializers -from .typings import PlatformStatus, TradingPairTicker, FundingCurrencyTicker, TickerHistories, TradingPairTrades, FundingCurrencyTrades +from .typings import * from .exceptions import RequestParametersError class BfxRestInterface(object): @@ -59,4 +59,14 @@ class BfxRestInterface(object): }[symbol[0]](*subdata) for subdata in self.__GET(f"trades/{symbol}/hist", params=params) + ] + + def book(self, symbol: str, precision: str, len: Optional[int]) -> Union[TradingPairBooks, FundingCurrencyBooks, TradingPairRawBooks, FundingCurrencyRawBooks]: + return [ + { + "t": precision == "R0" and serializers.TradingPairRawBook.parse or serializers.TradingPairBook.parse, + "f": precision == "R0" and serializers.FundingCurrencyRawBook.parse or serializers.FundingCurrencyBook.parse, + }[symbol[0]](*subdata) + + for subdata in self.__GET(f"book/{symbol}/{precision}", params={ "len": len }) ] \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index a002cdf..d1c64c2 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -94,4 +94,30 @@ FundingCurrencyTrade = _Serializer[typings.FundingCurrencyTrade]("FundingCurrenc "PERIOD" ]) +TradingPairBook = _Serializer[typings.TradingPairBook]("TradingPairBook", labels=[ + "PRICE", + "COUNT", + "AMOUNT" +]) + +FundingCurrencyBook = _Serializer[typings.FundingCurrencyBook]("FundingCurrencyBook", labels=[ + "RATE", + "PERIOD", + "COUNT", + "AMOUNT" +]) + +TradingPairRawBook = _Serializer[typings.TradingPairRawBook]("TradingPairRawBook", labels=[ + "ORDER_ID", + "PRICE", + "AMOUNT" +]) + +FundingCurrencyRawBook = _Serializer[typings.FundingCurrencyRawBook]("FundingCurrencyRawBook", labels=[ + "OFFER_ID", + "PERIOD", + "RATE", + "AMOUNT" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index d511088..3b52a49 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -54,4 +54,18 @@ TickerHistories = List[TickerHistory] (TradingPairTrades, FundingCurrencyTrades) = (List[TradingPairTrade], List[FundingCurrencyTrade]) +(TradingPairBook, FundingCurrencyBook) = ( + TypedDict("TradingPairBook", { "PRICE": float, "COUNT": int, "AMOUNT": float }), + TypedDict("FundingCurrencyBook", { "RATE": float, "PERIOD": int, "COUNT": int, "AMOUNT": float }) +) + +(TradingPairBooks, FundingCurrencyBooks) = (List[TradingPairBook], List[FundingCurrencyBook]) + +(TradingPairRawBook, FundingCurrencyRawBook) = ( + TypedDict("TradingPairRawBook", { "ORDER_ID": int, "PRICE": float, "AMOUNT": float }), + TypedDict("FundingCurrencyRawBook", { "OFFER_ID": int, "PERIOD": int, "RATE": float, "AMOUNT": float }), +) + +(TradingPairRawBooks, FundingCurrencyRawBooks) = (List[TradingPairRawBook], List[FundingCurrencyRawBook]) + #endregion \ No newline at end of file From cd5ef4211812ca146d13b35528acb37720a0004b Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 9 Dec 2022 16:16:15 +0100 Subject: [PATCH 060/265] Add support for new various endpoints. Add ResourceNotFound error in bfxapi/rest/exceptions.py. Fix bug in BfxRestInterface.__GET method. --- bfxapi/rest/BfxRestInterface.py | 64 +++++++++++++++++++++++++++++---- bfxapi/rest/exceptions.py | 10 +++++- bfxapi/rest/serializers.py | 41 +++++++++++++++++++++ bfxapi/rest/typings.py | 36 +++++++++++++++++++ 4 files changed, 143 insertions(+), 8 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 662a94c..e511692 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -2,20 +2,25 @@ import requests from http import HTTPStatus -from typing import List, Union, Optional +from typing import List, Union, Literal, Optional from . import serializers from .typings import * -from .exceptions import RequestParametersError +from .exceptions import RequestParametersError, ResourceNotFound class BfxRestInterface(object): def __init__(self, host): self.host = host def __GET(self, endpoint, params = None): - data = requests.get(f"{self.host}/{endpoint}", params=params).json() + response = requests.get(f"{self.host}/{endpoint}", params=params) - if data[0] == "error": + if response.status_code == HTTPStatus.NOT_FOUND: + raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") + + data = response.json() + + if len(data) and data[0] == "error": if data[1] == 10020: raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") @@ -40,7 +45,7 @@ class BfxRestInterface(object): "f": serializers.FundingCurrencyTicker.parse }[symbol[0]](*self.__GET(f"ticker/{symbol}"), skip=["SYMBOL"]) - def tickers_hist(self, symbols: List[str], start: Optional[int] = None, end: Optional[int] = None, limit: Optional[int] = None) -> TickerHistories: + def tickers_history(self, symbols: List[str], start: Optional[int] = None, end: Optional[int] = None, limit: Optional[int] = None) -> TickerHistories: params = { "symbols": ",".join(symbols), "start": start, "end": end, @@ -61,7 +66,7 @@ class BfxRestInterface(object): for subdata in self.__GET(f"trades/{symbol}/hist", params=params) ] - def book(self, symbol: str, precision: str, len: Optional[int]) -> Union[TradingPairBooks, FundingCurrencyBooks, TradingPairRawBooks, FundingCurrencyRawBooks]: + def book(self, symbol: str, precision: str, len: Optional[int] = None) -> Union[TradingPairBooks, FundingCurrencyBooks, TradingPairRawBooks, FundingCurrencyRawBooks]: return [ { "t": precision == "R0" and serializers.TradingPairRawBook.parse or serializers.TradingPairBook.parse, @@ -69,4 +74,49 @@ class BfxRestInterface(object): }[symbol[0]](*subdata) for subdata in self.__GET(f"book/{symbol}/{precision}", params={ "len": len }) - ] \ No newline at end of file + ] + + def stats( + self, + resource: str, section: Literal["hist", "last"], + sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Union[Stat, Stats]: + endpoint = f"stats1/{resource}/{section}" + + params = { "sort": sort, "start": start, "end": end, "limit": limit } + + if section == "last": + return serializers.Stat.parse(*self.__GET(endpoint, params=params)) + return [ serializers.Stat.parse(*subdata) for subdata in self.__GET(endpoint, params=params) ] + + def candles( + self, + resource: str, section: Literal["hist", "last"], + sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Union[Candle, Candles]: + endpoint = f"candles/{resource}/{section}" + + params = { "sort": sort, "start": start, "end": end, "limit": limit } + + if section == "last": + return serializers.Candle.parse(*self.__GET(endpoint, params=params)) + return [ serializers.Candle.parse(*subdata) for subdata in self.__GET(endpoint, params=params) ] + + def derivatives_status(self, type: str, keys: Optional[List[str]] = None) -> DerivativeStatuses: + params = None + + if keys != None: + params = { "keys": ",".join(keys) } + + return [ serializers.DerivativesStatus.parse(*subdata) for subdata in self.__GET(f"status/{type}", params=params) ] + + def derivatives_status_history( + self, + type: str, symbol: str, + sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> DerivativeStatuses: + endpoint = f"status/{type}/{symbol}/hist" + + params = { "sort": sort, "start": start, "end": end, "limit": limit } + + return [ serializers.DerivativesStatus.parse(*subdata, skip=[ "KEY" ]) for subdata in self.__GET(endpoint, params=params) ] \ No newline at end of file diff --git a/bfxapi/rest/exceptions.py b/bfxapi/rest/exceptions.py index 033848e..8afc74e 100644 --- a/bfxapi/rest/exceptions.py +++ b/bfxapi/rest/exceptions.py @@ -1,5 +1,6 @@ __all__ = [ - "RequestParametersError" + "RequestParametersError", + "ResourceNotFound" ] class BfxRestException(Exception): @@ -14,4 +15,11 @@ class RequestParametersError(BfxRestException): This error indicates that there are some invalid parameters sent along with an HTTP request. """ + pass + +class ResourceNotFound(BfxRestException): + """ + This error indicates a failed HTTP request to a non-existent resource. + """ + pass \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index d1c64c2..43f6ace 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -120,4 +120,45 @@ FundingCurrencyRawBook = _Serializer[typings.FundingCurrencyRawBook]("FundingCur "AMOUNT" ]) +Stat = _Serializer[typings.Stat]("Stat", labels=[ + "MTS", + "VALUE" +]) + +Candle = _Serializer[typings.Candle]("Candle", labels=[ + "MTS", + "OPEN", + "CLOSE", + "HIGH", + "LOW", + "VOLUME" +]) + +DerivativesStatus = _Serializer[typings.DerivativesStatus]("DerivativesStatus", labels=[ + "KEY", + "MTS", + "_PLACEHOLDER", + "DERIV_PRICE", + "SPOT_PRICE", + "_PLACEHOLDER", + "INSURANCE_FUND_BALANCE", + "_PLACEHOLDER", + "NEXT_FUNDING_EVT_TIMESTAMP_MS", + "NEXT_FUNDING_ACCRUED", + "NEXT_FUNDING_STEP", + "_PLACEHOLDER", + "CURRENT_FUNDING", + "_PLACEHOLDER", + "_PLACEHOLDER", + "MARK_PRICE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "OPEN_INTEREST", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "CLAMP_MIN", + "CLAMP_MAX" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index 3b52a49..7c8f127 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -68,4 +68,40 @@ TickerHistories = List[TickerHistory] (TradingPairRawBooks, FundingCurrencyRawBooks) = (List[TradingPairRawBook], List[FundingCurrencyRawBook]) +Stat = TypedDict("Stat", { + "MTS": int, + "VALUE": float +}) + +Stats = List[Stat] + +Candle = TypedDict("Candle", { + "MTS": int, + "OPEN": float, + "CLOSE": float, + "HIGH": float, + "LOW": float, + "VOLUME": float +}) + +Candles = List[Candle] + +DerivativesStatus = TypedDict("DerivativesStatus", { + "KEY": str, + "MTS": int, + "DERIV_PRICE": float, + "SPOT_PRICE": float, + "INSURANCE_FUND_BALANCE": float, + "NEXT_FUNDING_EVT_TIMESTAMP_MS": int, + "NEXT_FUNDING_ACCRUED": float, + "NEXT_FUNDING_STEP": int, + "CURRENT_FUNDING": float, + "MARK_PRICE": float, + "OPEN_INTEREST": float, + "CLAMP_MIN": float, + "CLAMP_MAX": float +}) + +DerivativeStatuses = List[DerivativesStatus] + #endregion \ No newline at end of file From 6a368d139db82a4e99afab0870d44133e0e896ff Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 9 Dec 2022 16:23:51 +0100 Subject: [PATCH 061/265] Add support for GET liquidations/hist endpoint. --- bfxapi/rest/BfxRestInterface.py | 7 ++++++- bfxapi/rest/serializers.py | 15 +++++++++++++++ bfxapi/rest/typings.py | 13 +++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index e511692..375d0d6 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -119,4 +119,9 @@ class BfxRestInterface(object): params = { "sort": sort, "start": start, "end": end, "limit": limit } - return [ serializers.DerivativesStatus.parse(*subdata, skip=[ "KEY" ]) for subdata in self.__GET(endpoint, params=params) ] \ No newline at end of file + return [ serializers.DerivativesStatus.parse(*subdata, skip=[ "KEY" ]) for subdata in self.__GET(endpoint, params=params) ] + + def liquidations(self, sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> Liquidations: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + + return [ serializers.Liquidation.parse(*subdata[0]) for subdata in self.__GET("liquidations/hist", params=params) ] \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 43f6ace..22817e0 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -161,4 +161,19 @@ DerivativesStatus = _Serializer[typings.DerivativesStatus]("DerivativesStatus", "CLAMP_MAX" ]) +Liquidation = _Serializer[typings.Liquidation]("Liquidation", labels=[ + "_PLACEHOLDER", + "POS_ID", + "MTS", + "_PLACEHOLDER", + "SYMBOL", + "AMOUNT", + "BASE_PRICE", + "_PLACEHOLDER", + "IS_MATCH", + "IS_MARKET_SOLD", + "_PLACEHOLDER", + "PRICE_ACQUIRED" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index 7c8f127..5afc040 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -104,4 +104,17 @@ DerivativesStatus = TypedDict("DerivativesStatus", { DerivativeStatuses = List[DerivativesStatus] +Liquidation = TypedDict("Liquidation", { + "POS_ID": int, + "MTS": int, + "SYMBOL": str, + "AMOUNT": float, + "BASE_PRICE": float, + "IS_MATCH": int, + "IS_MARKET_SOLD": int, + "PRICE_ACQUIRED": float +}) + +Liquidations = List[Liquidation] + #endregion \ No newline at end of file From 376ac37273d46e7b54b5067d6d4199cc39347179 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 12 Dec 2022 15:23:43 +0100 Subject: [PATCH 062/265] Fix small bug in BfxRestInterface.py file. --- bfxapi/rest/BfxRestInterface.py | 57 ++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 375d0d6..35153fc 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -30,20 +30,24 @@ class BfxRestInterface(object): return serializers.PlatformStatus.parse(*self.__GET("platform/status")) def tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]: + data = self.__GET("tickers", params={ "symbols": ",".join(symbols) }) + return [ { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse }[subdata[0][0]](*subdata) - for subdata in self.__GET("tickers", params={ "symbols": ",".join(symbols) }) + for subdata in data ] def ticker(self, symbol: str) -> Union[TradingPairTicker, FundingCurrencyTicker]: + data = self.__GET(f"ticker/{symbol}") + return { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse - }[symbol[0]](*self.__GET(f"ticker/{symbol}"), skip=["SYMBOL"]) + }[symbol[0]](*data, skip=["SYMBOL"]) def tickers_history(self, symbols: List[str], start: Optional[int] = None, end: Optional[int] = None, limit: Optional[int] = None) -> TickerHistories: params = { @@ -51,29 +55,35 @@ class BfxRestInterface(object): "start": start, "end": end, "limit": limit } + + data = self.__GET("tickers/hist", params=params) - return [ serializers.TickerHistory.parse(*subdata) for subdata in self.__GET("tickers/hist", params=params) ] + return [ serializers.TickerHistory.parse(*subdata) for subdata in data ] def trades(self, symbol: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[int] = None) -> Union[TradingPairTrades, FundingCurrencyTrades]: params = { "symbol": symbol, "limit": limit, "start": start, "end": end, "sort": sort } + data = self.__GET(f"trades/{symbol}/hist", params=params) + return [ { "t": serializers.TradingPairTrade.parse, "f": serializers.FundingCurrencyTrade.parse }[symbol[0]](*subdata) - for subdata in self.__GET(f"trades/{symbol}/hist", params=params) + for subdata in data ] def book(self, symbol: str, precision: str, len: Optional[int] = None) -> Union[TradingPairBooks, FundingCurrencyBooks, TradingPairRawBooks, FundingCurrencyRawBooks]: + data = self.__GET(f"book/{symbol}/{precision}", params={ "len": len }) + return [ { "t": precision == "R0" and serializers.TradingPairRawBook.parse or serializers.TradingPairBook.parse, "f": precision == "R0" and serializers.FundingCurrencyRawBook.parse or serializers.FundingCurrencyBook.parse, }[symbol[0]](*subdata) - for subdata in self.__GET(f"book/{symbol}/{precision}", params={ "len": len }) + for subdata in data ] def stats( @@ -81,47 +91,48 @@ class BfxRestInterface(object): resource: str, section: Literal["hist", "last"], sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> Union[Stat, Stats]: - endpoint = f"stats1/{resource}/{section}" - params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self.__GET(f"stats1/{resource}/{section}", params=params) + if section == "last": - return serializers.Stat.parse(*self.__GET(endpoint, params=params)) - return [ serializers.Stat.parse(*subdata) for subdata in self.__GET(endpoint, params=params) ] + return serializers.Stat.parse(*data) + return [ serializers.Stat.parse(*subdata) for subdata in data ] def candles( self, resource: str, section: Literal["hist", "last"], sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> Union[Candle, Candles]: - endpoint = f"candles/{resource}/{section}" - params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self.__GET(f"candles/{resource}/{section}", params=params) + if section == "last": - return serializers.Candle.parse(*self.__GET(endpoint, params=params)) - return [ serializers.Candle.parse(*subdata) for subdata in self.__GET(endpoint, params=params) ] + return serializers.Candle.parse(*data) + return [ serializers.Candle.parse(*subdata) for subdata in data ] - def derivatives_status(self, type: str, keys: Optional[List[str]] = None) -> DerivativeStatuses: - params = None - - if keys != None: - params = { "keys": ",".join(keys) } + def derivatives_status(self, type: str, keys: List[str] = None) -> DerivativeStatuses: + params = { "keys": ",".join(keys) } - return [ serializers.DerivativesStatus.parse(*subdata) for subdata in self.__GET(f"status/{type}", params=params) ] + data = self.__GET(f"status/{type}", params=params) + + return [ serializers.DerivativesStatus.parse(*subdata) for subdata in data ] def derivatives_status_history( self, type: str, symbol: str, sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> DerivativeStatuses: - endpoint = f"status/{type}/{symbol}/hist" - params = { "sort": sort, "start": start, "end": end, "limit": limit } - return [ serializers.DerivativesStatus.parse(*subdata, skip=[ "KEY" ]) for subdata in self.__GET(endpoint, params=params) ] + data = self.__GET(f"status/{type}/{symbol}/hist", params=params) + + return [ serializers.DerivativesStatus.parse(*subdata, skip=[ "KEY" ]) for subdata in data ] def liquidations(self, sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> Liquidations: params = { "sort": sort, "start": start, "end": end, "limit": limit } - return [ serializers.Liquidation.parse(*subdata[0]) for subdata in self.__GET("liquidations/hist", params=params) ] \ No newline at end of file + data = self.__GET("liquidations/hist", params=params) + + return [ serializers.Liquidation.parse(*subdata[0]) for subdata in data ] \ No newline at end of file From 32d698285e232bd6eb1f40e246b4203fb8b90660 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 12 Dec 2022 17:06:33 +0100 Subject: [PATCH 063/265] Add new endpoints in BfxRestInterfaces.py (with serializers and typings). --- bfxapi/rest/BfxRestInterface.py | 24 ++++++++++++++++++++++-- bfxapi/rest/serializers.py | 30 +++++++++++++++++++++++++++++- bfxapi/rest/typings.py | 23 ++++++++++++++++++++++- 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 35153fc..0c02687 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -2,7 +2,7 @@ import requests from http import HTTPStatus -from typing import List, Union, Literal, Optional +from typing import List, Union, Literal, Optional, Any from . import serializers from .typings import * @@ -135,4 +135,24 @@ class BfxRestInterface(object): data = self.__GET("liquidations/hist", params=params) - return [ serializers.Liquidation.parse(*subdata[0]) for subdata in data ] \ No newline at end of file + return [ serializers.Liquidation.parse(*subdata[0]) for subdata in data ] + + def leaderboards( + self, + resource: str, section: Literal["hist", "last"], + sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Union[Leaderboard, Leaderboards]: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + + data = self.__GET(f"rankings/{resource}/{section}", params=params) + + if section == "last": + return serializers.Leaderboard.parse(*data) + return [ serializers.Leaderboard.parse(*subdata) for subdata in data ] + + def funding_stats(self, symbol: str, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> FundingStats: + params = { "start": start, "end": end, "limit": limit } + + data = self.__GET(f"funding/stats/{symbol}/hist", params=params) + + return [ serializers.FundingStat.parse(*subdata) for subdata in data ] \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 22817e0..a2f69df 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -13,7 +13,7 @@ class _Serializer(Generic[T]): def __serialize(self, *args: Any, skip: Optional[List[str]]) -> Iterable[T]: labels = list(filter(lambda label: label not in (skip or list()), self.__labels)) - if len(labels) != len(args): + if len(labels) > len(args): raise BfxRestException(" and <*args> arguments should contain the same amount of elements.") for index, label in enumerate(labels): @@ -176,4 +176,32 @@ Liquidation = _Serializer[typings.Liquidation]("Liquidation", labels=[ "PRICE_ACQUIRED" ]) +Leaderboard = _Serializer[typings.Leaderboard]("Leaderboard", labels=[ + "MTS", + "_PLACEHOLDER", + "USERNAME", + "RANKING", + "_PLACEHOLDER", + "_PLACEHOLDER", + "VALUE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "TWITTER_HANDLE" +]) + +FundingStat = _Serializer[typings.FundingStat]("FundingStat", labels=[ + "TIMESTAMP", + "_PLACEHOLDER", + "_PLACEHOLDER", + "FRR", + "AVG_PERIOD", + "_PLACEHOLDER", + "_PLACEHOLDER", + "FUNDING_AMOUNT", + "FUNDING_AMOUNT_USED", + "_PLACEHOLDER", + "_PLACEHOLDER", + "FUNDING_BELOW_THRESHOLD" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index 5afc040..828c228 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -87,7 +87,7 @@ Candle = TypedDict("Candle", { Candles = List[Candle] DerivativesStatus = TypedDict("DerivativesStatus", { - "KEY": str, + "KEY": Optional[str], "MTS": int, "DERIV_PRICE": float, "SPOT_PRICE": float, @@ -117,4 +117,25 @@ Liquidation = TypedDict("Liquidation", { Liquidations = List[Liquidation] +Leaderboard = TypedDict("Leaderboard", { + "MTS": int, + "USERNAME": str, + "RANKING": int, + "VALUE": float, + "TWITTER_HANDLE": Optional[str] +}) + +Leaderboards = List[Leaderboard] + +FundingStat = TypedDict("FundingStat", { + "TIMESTAMP": int, + "FRR": float, + "AVG_PERIOD": float, + "FUNDING_AMOUNT": float, + "FUNDING_AMOUNT_USED": float, + "FUNDING_BELOW_THRESHOLD": float +}) + +FundingStats = List[FundingStat] + #endregion \ No newline at end of file From 862ba6d48132347a6ea217a04d16021ae644eec5 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 12 Dec 2022 17:14:58 +0100 Subject: [PATCH 064/265] Add support for GET conf/pub:{Action}:{Object}:{Detail} endpoint. Add bfxapi/rest/enums.py script. Add Configs enumeration in enums.py. --- bfxapi/rest/BfxRestInterface.py | 7 ++++++- bfxapi/rest/enums.py | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 bfxapi/rest/enums.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 0c02687..e4bb11c 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -5,7 +5,9 @@ from http import HTTPStatus from typing import List, Union, Literal, Optional, Any from . import serializers + from .typings import * +from .enums import Configs from .exceptions import RequestParametersError, ResourceNotFound class BfxRestInterface(object): @@ -155,4 +157,7 @@ class BfxRestInterface(object): data = self.__GET(f"funding/stats/{symbol}/hist", params=params) - return [ serializers.FundingStat.parse(*subdata) for subdata in data ] \ No newline at end of file + return [ serializers.FundingStat.parse(*subdata) for subdata in data ] + + def conf(self, config: Configs) -> Any: + return self.__GET(f"conf/{config}")[0] \ No newline at end of file diff --git a/bfxapi/rest/enums.py b/bfxapi/rest/enums.py new file mode 100644 index 0000000..3bb05d3 --- /dev/null +++ b/bfxapi/rest/enums.py @@ -0,0 +1,25 @@ +from enum import Enum + +class Configs(str, Enum): + MAP_CURRENCY_SYM = "pub:map:currency:sym" + MAP_CURRENCY_LABEL = "pub:map:currency:label" + MAP_CURRENCY_UNIT = "pub:map:currency:unit" + MAP_CURRENCY_UNDL = "pub:map:currency:undl" + MAP_CURRENCY_POOL = "pub:map:currency:pool" + MAP_CURRENCY_EXPLORER = "pub:map:currency:explorer" + MAP_CURRENCY_TX_FEE = "pub:map:currency:tx:fee" + MAP_TX_METHOD = "pub:map:tx:method" + + LIST_PAIR_EXCHANGE = "pub:list:pair:exchange" + LIST_PAIR_MARGIN = "pub:list:pair:margin" + LIST_PAIR_FUTURES = "pub:list:pair:futures" + LIST_PAIR_SECURITIES = "pub:list:pair:securities" + LIST_CURRENCY = "pub:list:currency" + LIST_COMPETITIONS = "pub:list:competitions" + + INFO_PAIR = "pub:info:pair" + INFO_PAIR_FUTURES = "pub:info:pair:futures" + INFO_TX_STATUS = "pub:info:tx:status" + + SPEC_MARGIN = "pub:spec:margin", + FEES = "pub:fees" \ No newline at end of file From ec821a07520efa605e83626f23c61b27e6467b87 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 14 Dec 2022 18:05:45 +0100 Subject: [PATCH 065/265] Split BfxRestInterface methods in t_ and f_ handlers. --- bfxapi/rest/BfxRestInterface.py | 158 ++++++++++++++++++-------------- bfxapi/rest/enums.py | 15 ++- bfxapi/rest/serializers.py | 2 +- bfxapi/rest/typings.py | 4 +- 4 files changed, 105 insertions(+), 74 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index e4bb11c..018ce7b 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -2,12 +2,12 @@ import requests from http import HTTPStatus -from typing import List, Union, Literal, Optional, Any +from typing import List, Union, Literal, Optional, Any, cast from . import serializers from .typings import * -from .enums import Configs +from .enums import Config, Precision, Sort from .exceptions import RequestParametersError, ResourceNotFound class BfxRestInterface(object): @@ -34,24 +34,33 @@ class BfxRestInterface(object): def tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]: data = self.__GET("tickers", params={ "symbols": ",".join(symbols) }) - return [ - { - "t": serializers.TradingPairTicker.parse, - "f": serializers.FundingCurrencyTicker.parse - }[subdata[0][0]](*subdata) + parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse } + + return [ parsers[subdata[0][0]](*subdata) for subdata in data ] - for subdata in data - ] + def t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]: + if isinstance(pairs, str) and pairs == "ALL": + return [ subdata for subdata in self.tickers([ "ALL" ]) if subdata["SYMBOL"].startswith("t") ] - def ticker(self, symbol: str) -> Union[TradingPairTicker, FundingCurrencyTicker]: - data = self.__GET(f"ticker/{symbol}") + data = self.tickers([ "t" + pair for pair in pairs ]) - return { - "t": serializers.TradingPairTicker.parse, - "f": serializers.FundingCurrencyTicker.parse - }[symbol[0]](*data, skip=["SYMBOL"]) + return cast(List[TradingPairTicker], data) - def tickers_history(self, symbols: List[str], start: Optional[int] = None, end: Optional[int] = None, limit: Optional[int] = None) -> TickerHistories: + def f_tickers(self, currencies: Union[List[str], Literal["ALL"]]) -> List[FundingCurrencyTicker]: + if isinstance(currencies, str) and currencies == "ALL": + return [ subdata for subdata in self.tickers([ "ALL" ]) if subdata["SYMBOL"].startswith("f") ] + + data = self.tickers([ "f" + currency for currency in currencies ]) + + return cast(List[FundingCurrencyTicker], data) + + def t_ticker(self, pair: str) -> TradingPairTicker: + return serializers.TradingPairTicker.parse(*self.__GET(f"ticker/t{pair}"), skip=["SYMBOL"]) + + def f_ticker(self, currency: str) -> FundingCurrencyTicker: + return serializers.FundingCurrencyTicker.parse(*self.__GET(f"ticker/f{currency}"), skip=["SYMBOL"]) + + def tickers_history(self, symbols: List[str], start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> TickersHistories: params = { "symbols": ",".join(symbols), "start": start, "end": end, @@ -60,61 +69,67 @@ class BfxRestInterface(object): data = self.__GET("tickers/hist", params=params) - return [ serializers.TickerHistory.parse(*subdata) for subdata in data ] + return [ serializers.TickersHistory.parse(*subdata) for subdata in data ] - def trades(self, symbol: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[int] = None) -> Union[TradingPairTrades, FundingCurrencyTrades]: - params = { "symbol": symbol, "limit": limit, "start": start, "end": end, "sort": sort } - - data = self.__GET(f"trades/{symbol}/hist", params=params) + def t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> TradingPairTrades: + params = { "limit": limit, "start": start, "end": end, "sort": sort } + data = self.__GET(f"trades/{'t' + pair}/hist", params=params) + return [ serializers.TradingPairTrade.parse(*subdata) for subdata in data ] - return [ - { - "t": serializers.TradingPairTrade.parse, - "f": serializers.FundingCurrencyTrade.parse - }[symbol[0]](*subdata) + def f_trades(self, currency: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> FundingCurrencyTrades: + params = { "limit": limit, "start": start, "end": end, "sort": sort } + data = self.__GET(f"trades/{'f' + currency}/hist", params=params) + return [ serializers.FundingCurrencyTrade.parse(*subdata) for subdata in data ] - for subdata in data - ] + def t_book(self, pair: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> TradingPairBooks: + return [ serializers.TradingPairBook.parse(*subdata) for subdata in self.__GET(f"book/{'t' + pair}/{precision}", params={ "len": len }) ] - def book(self, symbol: str, precision: str, len: Optional[int] = None) -> Union[TradingPairBooks, FundingCurrencyBooks, TradingPairRawBooks, FundingCurrencyRawBooks]: - data = self.__GET(f"book/{symbol}/{precision}", params={ "len": len }) - - return [ - { - "t": precision == "R0" and serializers.TradingPairRawBook.parse or serializers.TradingPairBook.parse, - "f": precision == "R0" and serializers.FundingCurrencyRawBook.parse or serializers.FundingCurrencyBook.parse, - }[symbol[0]](*subdata) + def f_book(self, currency: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> FundingCurrencyBooks: + return [ serializers.FundingCurrencyBook.parse(*subdata) for subdata in self.__GET(f"book/{'f' + currency}/{precision}", params={ "len": len }) ] - for subdata in data - ] + def t_raw_book(self, pair: str, len: Optional[Literal[1, 25, 100]] = None) -> TradingPairRawBooks: + return [ serializers.TradingPairRawBook.parse(*subdata) for subdata in self.__GET(f"book/{'t' + pair}/R0", params={ "len": len }) ] - def stats( + def f_raw_book(self, currency: str, len: Optional[Literal[1, 25, 100]] = None) -> FundingCurrencyRawBooks: + return [ serializers.FundingCurrencyRawBook.parse(*subdata) for subdata in self.__GET(f"book/{'f' + currency}/R0", params={ "len": len }) ] + + def stats_hist( self, - resource: str, section: Literal["hist", "last"], - sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Union[Stat, Stats]: + resource: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Stats: params = { "sort": sort, "start": start, "end": end, "limit": limit } - - data = self.__GET(f"stats1/{resource}/{section}", params=params) - - if section == "last": - return serializers.Stat.parse(*data) + data = self.__GET(f"stats1/{resource}/hist", params=params) return [ serializers.Stat.parse(*subdata) for subdata in data ] - def candles( - self, - resource: str, section: Literal["hist", "last"], - sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Union[Candle, Candles]: + def stats_last( + self, + resource: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Stat: params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self.__GET(f"stats1/{resource}/last", params=params) + return serializers.Stat.parse(*data) - data = self.__GET(f"candles/{resource}/{section}", params=params) - - if section == "last": - return serializers.Candle.parse(*data) + def candles_hist( + self, + resource: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Candles: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self.__GET(f"candles/{resource}/hist", params=params) return [ serializers.Candle.parse(*subdata) for subdata in data ] - def derivatives_status(self, type: str, keys: List[str] = None) -> DerivativeStatuses: + def candles_last( + self, + resource: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Candle: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self.__GET(f"candles/{resource}/last", params=params) + return serializers.Candle.parse(*data) + + def derivatives_status(self, type: str, keys: List[str]) -> DerivativeStatuses: params = { "keys": ",".join(keys) } data = self.__GET(f"status/{type}", params=params) @@ -124,7 +139,7 @@ class BfxRestInterface(object): def derivatives_status_history( self, type: str, symbol: str, - sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> DerivativeStatuses: params = { "sort": sort, "start": start, "end": end, "limit": limit } @@ -132,26 +147,31 @@ class BfxRestInterface(object): return [ serializers.DerivativesStatus.parse(*subdata, skip=[ "KEY" ]) for subdata in data ] - def liquidations(self, sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> Liquidations: + def liquidations(self, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> Liquidations: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self.__GET("liquidations/hist", params=params) return [ serializers.Liquidation.parse(*subdata[0]) for subdata in data ] - def leaderboards( + def leaderboards_hist( self, - resource: str, section: Literal["hist", "last"], - sort: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Union[Leaderboard, Leaderboards]: + resource: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Leaderboards: params = { "sort": sort, "start": start, "end": end, "limit": limit } - - data = self.__GET(f"rankings/{resource}/{section}", params=params) - - if section == "last": - return serializers.Leaderboard.parse(*data) + data = self.__GET(f"rankings/{resource}/hist", params=params) return [ serializers.Leaderboard.parse(*subdata) for subdata in data ] + def leaderboards_last( + self, + resource: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Leaderboard: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self.__GET(f"rankings/{resource}/last", params=params) + return serializers.Leaderboard.parse(*data) + def funding_stats(self, symbol: str, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> FundingStats: params = { "start": start, "end": end, "limit": limit } @@ -159,5 +179,5 @@ class BfxRestInterface(object): return [ serializers.FundingStat.parse(*subdata) for subdata in data ] - def conf(self, config: Configs) -> Any: + def conf(self, config: Config) -> Any: return self.__GET(f"conf/{config}")[0] \ No newline at end of file diff --git a/bfxapi/rest/enums.py b/bfxapi/rest/enums.py index 3bb05d3..70c2336 100644 --- a/bfxapi/rest/enums.py +++ b/bfxapi/rest/enums.py @@ -1,6 +1,6 @@ from enum import Enum -class Configs(str, Enum): +class Config(str, Enum): MAP_CURRENCY_SYM = "pub:map:currency:sym" MAP_CURRENCY_LABEL = "pub:map:currency:label" MAP_CURRENCY_UNIT = "pub:map:currency:unit" @@ -22,4 +22,15 @@ class Configs(str, Enum): INFO_TX_STATUS = "pub:info:tx:status" SPEC_MARGIN = "pub:spec:margin", - FEES = "pub:fees" \ No newline at end of file + FEES = "pub:fees" + +class Precision(str, Enum): + P0 = "P0" + P1 = "P1" + P2 = "P2" + P3 = "P3" + P4 = "P4" + +class Sort(int, Enum): + ASCENDING = +1 + DESCENDING = -1 \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index a2f69df..8bcdf34 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -63,7 +63,7 @@ FundingCurrencyTicker = _Serializer[typings.FundingCurrencyTicker]("FundingCurre "FRR_AMOUNT_AVAILABLE" ]) -TickerHistory = _Serializer[typings.TickerHistory]("TickerHistory", labels=[ +TickersHistory = _Serializer[typings.TickersHistory]("TickersHistory", labels=[ "SYMBOL", "BID", "_PLACEHOLDER", diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index 828c228..4b225f4 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -38,14 +38,14 @@ FundingCurrencyTicker = TypedDict("FundingCurrencyTicker", { "FRR_AMOUNT_AVAILABLE": float }) -TickerHistory = TypedDict("TickerHistory", { +TickersHistory = TypedDict("TickersHistory", { "SYMBOL": str, "BID": float, "ASK": float, "MTS": int }) -TickerHistories = List[TickerHistory] +TickersHistories = List[TickersHistory] (TradingPairTrade, FundingCurrencyTrade) = ( TypedDict("TradingPairTrade", { "ID": int, "MTS": int, "AMOUNT": float, "PRICE": float }), From 07241b1ba850bec476842e710fd2363dd8730859 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 14 Dec 2022 18:17:29 +0100 Subject: [PATCH 066/265] Add _Requests and _RestPublicEndpoints classes in bfxapi/rest/BfxRestInterface.py. --- bfxapi/rest/BfxRestInterface.py | 51 ++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 018ce7b..30109c0 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -12,9 +12,13 @@ from .exceptions import RequestParametersError, ResourceNotFound class BfxRestInterface(object): def __init__(self, host): + self.public = _RestPublicEndpoints(host=host) + +class _Requests(object): + def __init__(self, host: str): self.host = host - def __GET(self, endpoint, params = None): + def _GET(self, endpoint, params = None): response = requests.get(f"{self.host}/{endpoint}", params=params) if response.status_code == HTTPStatus.NOT_FOUND: @@ -28,11 +32,12 @@ class BfxRestInterface(object): return data +class _RestPublicEndpoints(_Requests): def platform_status(self) -> PlatformStatus: - return serializers.PlatformStatus.parse(*self.__GET("platform/status")) + return serializers.PlatformStatus.parse(*self._GET("platform/status")) def tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]: - data = self.__GET("tickers", params={ "symbols": ",".join(symbols) }) + data = self._GET("tickers", params={ "symbols": ",".join(symbols) }) parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse } @@ -55,10 +60,10 @@ class BfxRestInterface(object): return cast(List[FundingCurrencyTicker], data) def t_ticker(self, pair: str) -> TradingPairTicker: - return serializers.TradingPairTicker.parse(*self.__GET(f"ticker/t{pair}"), skip=["SYMBOL"]) + return serializers.TradingPairTicker.parse(*self._GET(f"ticker/t{pair}"), skip=["SYMBOL"]) def f_ticker(self, currency: str) -> FundingCurrencyTicker: - return serializers.FundingCurrencyTicker.parse(*self.__GET(f"ticker/f{currency}"), skip=["SYMBOL"]) + return serializers.FundingCurrencyTicker.parse(*self._GET(f"ticker/f{currency}"), skip=["SYMBOL"]) def tickers_history(self, symbols: List[str], start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> TickersHistories: params = { @@ -67,31 +72,31 @@ class BfxRestInterface(object): "limit": limit } - data = self.__GET("tickers/hist", params=params) + data = self._GET("tickers/hist", params=params) return [ serializers.TickersHistory.parse(*subdata) for subdata in data ] def t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> TradingPairTrades: params = { "limit": limit, "start": start, "end": end, "sort": sort } - data = self.__GET(f"trades/{'t' + pair}/hist", params=params) + data = self._GET(f"trades/{'t' + pair}/hist", params=params) return [ serializers.TradingPairTrade.parse(*subdata) for subdata in data ] def f_trades(self, currency: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> FundingCurrencyTrades: params = { "limit": limit, "start": start, "end": end, "sort": sort } - data = self.__GET(f"trades/{'f' + currency}/hist", params=params) + data = self._GET(f"trades/{'f' + currency}/hist", params=params) return [ serializers.FundingCurrencyTrade.parse(*subdata) for subdata in data ] def t_book(self, pair: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> TradingPairBooks: - return [ serializers.TradingPairBook.parse(*subdata) for subdata in self.__GET(f"book/{'t' + pair}/{precision}", params={ "len": len }) ] + return [ serializers.TradingPairBook.parse(*subdata) for subdata in self._GET(f"book/{'t' + pair}/{precision}", params={ "len": len }) ] def f_book(self, currency: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> FundingCurrencyBooks: - return [ serializers.FundingCurrencyBook.parse(*subdata) for subdata in self.__GET(f"book/{'f' + currency}/{precision}", params={ "len": len }) ] + return [ serializers.FundingCurrencyBook.parse(*subdata) for subdata in self._GET(f"book/{'f' + currency}/{precision}", params={ "len": len }) ] def t_raw_book(self, pair: str, len: Optional[Literal[1, 25, 100]] = None) -> TradingPairRawBooks: - return [ serializers.TradingPairRawBook.parse(*subdata) for subdata in self.__GET(f"book/{'t' + pair}/R0", params={ "len": len }) ] + return [ serializers.TradingPairRawBook.parse(*subdata) for subdata in self._GET(f"book/{'t' + pair}/R0", params={ "len": len }) ] def f_raw_book(self, currency: str, len: Optional[Literal[1, 25, 100]] = None) -> FundingCurrencyRawBooks: - return [ serializers.FundingCurrencyRawBook.parse(*subdata) for subdata in self.__GET(f"book/{'f' + currency}/R0", params={ "len": len }) ] + return [ serializers.FundingCurrencyRawBook.parse(*subdata) for subdata in self._GET(f"book/{'f' + currency}/R0", params={ "len": len }) ] def stats_hist( self, @@ -99,7 +104,7 @@ class BfxRestInterface(object): sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> Stats: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self.__GET(f"stats1/{resource}/hist", params=params) + data = self._GET(f"stats1/{resource}/hist", params=params) return [ serializers.Stat.parse(*subdata) for subdata in data ] def stats_last( @@ -108,7 +113,7 @@ class BfxRestInterface(object): sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> Stat: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self.__GET(f"stats1/{resource}/last", params=params) + data = self._GET(f"stats1/{resource}/last", params=params) return serializers.Stat.parse(*data) def candles_hist( @@ -117,7 +122,7 @@ class BfxRestInterface(object): sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> Candles: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self.__GET(f"candles/{resource}/hist", params=params) + data = self._GET(f"candles/{resource}/hist", params=params) return [ serializers.Candle.parse(*subdata) for subdata in data ] def candles_last( @@ -126,13 +131,13 @@ class BfxRestInterface(object): sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> Candle: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self.__GET(f"candles/{resource}/last", params=params) + data = self._GET(f"candles/{resource}/last", params=params) return serializers.Candle.parse(*data) def derivatives_status(self, type: str, keys: List[str]) -> DerivativeStatuses: params = { "keys": ",".join(keys) } - data = self.__GET(f"status/{type}", params=params) + data = self._GET(f"status/{type}", params=params) return [ serializers.DerivativesStatus.parse(*subdata) for subdata in data ] @@ -143,14 +148,14 @@ class BfxRestInterface(object): ) -> DerivativeStatuses: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self.__GET(f"status/{type}/{symbol}/hist", params=params) + data = self._GET(f"status/{type}/{symbol}/hist", params=params) return [ serializers.DerivativesStatus.parse(*subdata, skip=[ "KEY" ]) for subdata in data ] def liquidations(self, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> Liquidations: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self.__GET("liquidations/hist", params=params) + data = self._GET("liquidations/hist", params=params) return [ serializers.Liquidation.parse(*subdata[0]) for subdata in data ] @@ -160,7 +165,7 @@ class BfxRestInterface(object): sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> Leaderboards: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self.__GET(f"rankings/{resource}/hist", params=params) + data = self._GET(f"rankings/{resource}/hist", params=params) return [ serializers.Leaderboard.parse(*subdata) for subdata in data ] def leaderboards_last( @@ -169,15 +174,15 @@ class BfxRestInterface(object): sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> Leaderboard: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self.__GET(f"rankings/{resource}/last", params=params) + data = self._GET(f"rankings/{resource}/last", params=params) return serializers.Leaderboard.parse(*data) def funding_stats(self, symbol: str, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> FundingStats: params = { "start": start, "end": end, "limit": limit } - data = self.__GET(f"funding/stats/{symbol}/hist", params=params) + data = self._GET(f"funding/stats/{symbol}/hist", params=params) return [ serializers.FundingStat.parse(*subdata) for subdata in data ] def conf(self, config: Config) -> Any: - return self.__GET(f"conf/{config}")[0] \ No newline at end of file + return self._GET(f"conf/{config}")[0] \ No newline at end of file From 851184bf7577341246a15a49da0d792d181b40ab Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 14 Dec 2022 18:56:03 +0100 Subject: [PATCH 067/265] Add authentication logic to _Requests class in BfxRestInterface.py. Add _RestAuthenticatedEndpoints class. Add InvalidAuthenticationCredentials in bfxapi/rest/exceptions.py. --- bfxapi/rest/BfxRestInterface.py | 54 +++++++++++++++++++++++++++++---- bfxapi/rest/exceptions.py | 10 +++++- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 30109c0..b7a8893 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -1,4 +1,4 @@ -import requests +import time, hmac, hashlib, json, requests from http import HTTPStatus @@ -8,15 +8,32 @@ from . import serializers from .typings import * from .enums import Config, Precision, Sort -from .exceptions import RequestParametersError, ResourceNotFound +from .exceptions import RequestParametersError, ResourceNotFound, InvalidAuthenticationCredentials class BfxRestInterface(object): - def __init__(self, host): + def __init__(self, host, API_KEY = None, API_SECRET = None): self.public = _RestPublicEndpoints(host=host) + self.auth = _RestAuthenticatedEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET) + class _Requests(object): - def __init__(self, host: str): - self.host = host + def __init__(self, host, API_KEY = None, API_SECRET = None): + self.host, self.API_KEY, self.API_SECRET = host, API_KEY, API_SECRET + + def __build_authentication_headers(self, endpoint, data): + nonce = str(int(time.time()) * 1000) + + signature = hmac.new( + self.API_SECRET.encode("utf8"), + f"/api/v2/{endpoint}{nonce}{json.dumps(data)}".encode("utf8"), + hashlib.sha384 + ).hexdigest() + + return { + "bfx-nonce": nonce, + "bfx-signature": signature, + "bfx-apikey": self.API_KEY + } def _GET(self, endpoint, params = None): response = requests.get(f"{self.host}/{endpoint}", params=params) @@ -32,6 +49,28 @@ class _Requests(object): return data + def _POST(self, endpoint, params = None, data = None, _append_authentication_headers = True): + headers = { "Content-Type": "application/json" } + + if _append_authentication_headers: + headers = { **headers, **self.__build_authentication_headers(f"{endpoint}", data) } + + response = requests.post(f"{self.host}/{endpoint}", params=params, data=json.dumps(data), headers=headers) + + if response.status_code == HTTPStatus.NOT_FOUND: + raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") + + data = response.json() + + if len(data) and data[0] == "error": + if data[1] == 10020: + raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") + + if data[1] == 10100: + raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.") + + return data + class _RestPublicEndpoints(_Requests): def platform_status(self) -> PlatformStatus: return serializers.PlatformStatus.parse(*self._GET("platform/status")) @@ -185,4 +224,7 @@ class _RestPublicEndpoints(_Requests): return [ serializers.FundingStat.parse(*subdata) for subdata in data ] def conf(self, config: Config) -> Any: - return self._GET(f"conf/{config}")[0] \ No newline at end of file + return self._GET(f"conf/{config}")[0] + +class _RestAuthenticatedEndpoints(_Requests): + __PREFIX = "auth/" \ No newline at end of file diff --git a/bfxapi/rest/exceptions.py b/bfxapi/rest/exceptions.py index 8afc74e..973fb5a 100644 --- a/bfxapi/rest/exceptions.py +++ b/bfxapi/rest/exceptions.py @@ -1,6 +1,7 @@ __all__ = [ "RequestParametersError", - "ResourceNotFound" + "ResourceNotFound", + "InvalidAuthenticationCredentials" ] class BfxRestException(Exception): @@ -22,4 +23,11 @@ class ResourceNotFound(BfxRestException): This error indicates a failed HTTP request to a non-existent resource. """ + pass + +class InvalidAuthenticationCredentials(BfxRestException): + """ + This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication. + """ + pass \ No newline at end of file From c9f86d6d030419388710eb8c72c60cfd9f1eac16 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 15 Dec 2022 19:07:55 +0100 Subject: [PATCH 068/265] Add labeler.py to root package (bfxapi). Remove List aliases in bfxapi/rest/typings.py. Update BfxRestInterface.py to use new standards. --- bfxapi/labeler.py | 20 +++ bfxapi/rest/BfxRestInterface.py | 36 ++--- bfxapi/rest/serializers.py | 27 +--- bfxapi/rest/typings.py | 236 +++++++++++++++----------------- 4 files changed, 154 insertions(+), 165 deletions(-) create mode 100644 bfxapi/labeler.py diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py new file mode 100644 index 0000000..eb7076c --- /dev/null +++ b/bfxapi/labeler.py @@ -0,0 +1,20 @@ +from typing import Generic, TypeVar, Iterable, Optional, List, Any + +T = TypeVar("T") + +class _Serializer(Generic[T]): + def __init__(self, name: str, labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]): + self.name, self.__labels, self.__IGNORE = name, labels, IGNORE + + def __serialize(self, *args: Any, skip: Optional[List[str]]) -> Iterable[T]: + labels = list(filter(lambda label: label not in (skip or list()), self.__labels)) + + if len(labels) > len(args): + raise Exception(" and <*args> arguments should contain the same amount of elements.") + + for index, label in enumerate(labels): + if label not in self.__IGNORE: + yield label, args[index] + + def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: + return dict(self.__serialize(*values, skip=skip)) \ No newline at end of file diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index b7a8893..6644247 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -104,7 +104,7 @@ class _RestPublicEndpoints(_Requests): def f_ticker(self, currency: str) -> FundingCurrencyTicker: return serializers.FundingCurrencyTicker.parse(*self._GET(f"ticker/f{currency}"), skip=["SYMBOL"]) - def tickers_history(self, symbols: List[str], start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> TickersHistories: + def tickers_history(self, symbols: List[str], start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[TickersHistory]: params = { "symbols": ",".join(symbols), "start": start, "end": end, @@ -115,51 +115,51 @@ class _RestPublicEndpoints(_Requests): return [ serializers.TickersHistory.parse(*subdata) for subdata in data ] - def t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> TradingPairTrades: + def t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[TradingPairTrade]: params = { "limit": limit, "start": start, "end": end, "sort": sort } data = self._GET(f"trades/{'t' + pair}/hist", params=params) return [ serializers.TradingPairTrade.parse(*subdata) for subdata in data ] - def f_trades(self, currency: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> FundingCurrencyTrades: + def f_trades(self, currency: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[FundingCurrencyTrade]: params = { "limit": limit, "start": start, "end": end, "sort": sort } data = self._GET(f"trades/{'f' + currency}/hist", params=params) return [ serializers.FundingCurrencyTrade.parse(*subdata) for subdata in data ] - def t_book(self, pair: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> TradingPairBooks: + def t_book(self, pair: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairBook]: return [ serializers.TradingPairBook.parse(*subdata) for subdata in self._GET(f"book/{'t' + pair}/{precision}", params={ "len": len }) ] - def f_book(self, currency: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> FundingCurrencyBooks: + def f_book(self, currency: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyBook]: return [ serializers.FundingCurrencyBook.parse(*subdata) for subdata in self._GET(f"book/{'f' + currency}/{precision}", params={ "len": len }) ] - def t_raw_book(self, pair: str, len: Optional[Literal[1, 25, 100]] = None) -> TradingPairRawBooks: + def t_raw_book(self, pair: str, len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairRawBook]: return [ serializers.TradingPairRawBook.parse(*subdata) for subdata in self._GET(f"book/{'t' + pair}/R0", params={ "len": len }) ] - def f_raw_book(self, currency: str, len: Optional[Literal[1, 25, 100]] = None) -> FundingCurrencyRawBooks: + def f_raw_book(self, currency: str, len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyRawBook]: return [ serializers.FundingCurrencyRawBook.parse(*subdata) for subdata in self._GET(f"book/{'f' + currency}/R0", params={ "len": len }) ] def stats_hist( self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Stats: + ) -> List[Statistic]: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self._GET(f"stats1/{resource}/hist", params=params) - return [ serializers.Stat.parse(*subdata) for subdata in data ] + return [ serializers.Statistic.parse(*subdata) for subdata in data ] def stats_last( self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Stat: + ) -> Statistic: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self._GET(f"stats1/{resource}/last", params=params) - return serializers.Stat.parse(*data) + return serializers.Statistic.parse(*data) def candles_hist( self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Candles: + ) -> List[Candle]: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self._GET(f"candles/{resource}/hist", params=params) return [ serializers.Candle.parse(*subdata) for subdata in data ] @@ -173,7 +173,7 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"candles/{resource}/last", params=params) return serializers.Candle.parse(*data) - def derivatives_status(self, type: str, keys: List[str]) -> DerivativeStatuses: + def derivatives_status(self, type: str, keys: List[str]) -> List[DerivativesStatus]: params = { "keys": ",".join(keys) } data = self._GET(f"status/{type}", params=params) @@ -184,14 +184,14 @@ class _RestPublicEndpoints(_Requests): self, type: str, symbol: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> DerivativeStatuses: + ) -> List[DerivativesStatus]: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self._GET(f"status/{type}/{symbol}/hist", params=params) return [ serializers.DerivativesStatus.parse(*subdata, skip=[ "KEY" ]) for subdata in data ] - def liquidations(self, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> Liquidations: + def liquidations(self, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Liquidation]: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self._GET("liquidations/hist", params=params) @@ -202,7 +202,7 @@ class _RestPublicEndpoints(_Requests): self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Leaderboards: + ) -> List[Leaderboard]: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self._GET(f"rankings/{resource}/hist", params=params) return [ serializers.Leaderboard.parse(*subdata) for subdata in data ] @@ -216,12 +216,12 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"rankings/{resource}/last", params=params) return serializers.Leaderboard.parse(*data) - def funding_stats(self, symbol: str, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> FundingStats: + def funding_stats(self, symbol: str, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingStatistic]: params = { "start": start, "end": end, "limit": limit } data = self._GET(f"funding/stats/{symbol}/hist", params=params) - return [ serializers.FundingStat.parse(*subdata) for subdata in data ] + return [ serializers.FundingStatistic.parse(*subdata) for subdata in data ] def conf(self, config: Config) -> Any: return self._GET(f"conf/{config}")[0] diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 8bcdf34..95f4c26 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -1,27 +1,6 @@ -from typing import Generic, TypeVar, Iterable, Optional, List, Any - from . import typings -from .exceptions import BfxRestException - -T = TypeVar("T") - -class _Serializer(Generic[T]): - def __init__(self, name: str, labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]): - self.name, self.__labels, self.__IGNORE = name, labels, IGNORE - - def __serialize(self, *args: Any, skip: Optional[List[str]]) -> Iterable[T]: - labels = list(filter(lambda label: label not in (skip or list()), self.__labels)) - - if len(labels) > len(args): - raise BfxRestException(" and <*args> arguments should contain the same amount of elements.") - - for index, label in enumerate(labels): - if label not in self.__IGNORE: - yield label, args[index] - - def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: - return dict(self.__serialize(*values, skip=skip)) +from .. labeler import _Serializer #region Serializers definition for Rest Public Endpoints @@ -120,7 +99,7 @@ FundingCurrencyRawBook = _Serializer[typings.FundingCurrencyRawBook]("FundingCur "AMOUNT" ]) -Stat = _Serializer[typings.Stat]("Stat", labels=[ +Statistic = _Serializer[typings.Statistic]("Statistic", labels=[ "MTS", "VALUE" ]) @@ -189,7 +168,7 @@ Leaderboard = _Serializer[typings.Leaderboard]("Leaderboard", labels=[ "TWITTER_HANDLE" ]) -FundingStat = _Serializer[typings.FundingStat]("FundingStat", labels=[ +FundingStatistic = _Serializer[typings.FundingStatistic]("FundingStatistic", labels=[ "TIMESTAMP", "_PLACEHOLDER", "_PLACEHOLDER", diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index 4b225f4..7af75fb 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -2,140 +2,130 @@ from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any #region Type hinting for Rest Public Endpoints -PlatformStatus = TypedDict("PlatformStatus", { - "OPERATIVE": int -}) +class PlatformStatus(TypedDict): + OPERATIVE: int -TradingPairTicker = TypedDict("TradingPairTicker", { - "SYMBOL": Optional[str], - "BID": float, - "BID_SIZE": float, - "ASK": float, - "ASK_SIZE": float, - "DAILY_CHANGE": float, - "DAILY_CHANGE_RELATIVE": float, - "LAST_PRICE": float, - "VOLUME": float, - "HIGH": float, - "LOW": float -}) +class TradingPairTicker(TypedDict): + SYMBOL: Optional[str] + BID: float + BID_SIZE: float + ASK: float + ASK_SIZE: float + DAILY_CHANGE: float + DAILY_CHANGE_RELATIVE: float + LAST_PRICE: float + VOLUME: float + HIGH: float + LOW: float -FundingCurrencyTicker = TypedDict("FundingCurrencyTicker", { - "SYMBOL": Optional[str], - "FRR": float, - "BID": float, - "BID_PERIOD": int, - "BID_SIZE": float, - "ASK": float, - "ASK_PERIOD": int, - "ASK_SIZE": float, - "DAILY_CHANGE": float, - "DAILY_CHANGE_RELATIVE": float, - "LAST_PRICE": float, - "VOLUME": float, - "HIGH": float, - "LOW": float, - "FRR_AMOUNT_AVAILABLE": float -}) +class FundingCurrencyTicker(TypedDict): + SYMBOL: Optional[str] + FRR: float + BID: float + BID_PERIOD: int + BID_SIZE: float + ASK: float + ASK_PERIOD: int + ASK_SIZE: float + DAILY_CHANGE: float + DAILY_CHANGE_RELATIVE: float + LAST_PRICE: float + VOLUME: float + HIGH: float + LOW: float + FRR_AMOUNT_AVAILABLE: float -TickersHistory = TypedDict("TickersHistory", { - "SYMBOL": str, - "BID": float, - "ASK": float, - "MTS": int -}) +class TickersHistory(TypedDict): + SYMBOL: str + BID: float + ASK: float + MTS: int -TickersHistories = List[TickersHistory] +class TradingPairTrade(TypedDict): + ID: int + MTS: int + AMOUNT: float + PRICE: float -(TradingPairTrade, FundingCurrencyTrade) = ( - TypedDict("TradingPairTrade", { "ID": int, "MTS": int, "AMOUNT": float, "PRICE": float }), - TypedDict("FundingCurrencyTrade", { "ID": int, "MTS": int, "AMOUNT": float, "RATE": float, "PERIOD": int }) -) +class FundingCurrencyTrade(TypedDict): + ID: int + MTS: int + AMOUNT: float + RATE: float + PERIOD: int -(TradingPairTrades, FundingCurrencyTrades) = (List[TradingPairTrade], List[FundingCurrencyTrade]) +class TradingPairBook(TypedDict): + PRICE: float + COUNT: int + AMOUNT: float + +class FundingCurrencyBook(TypedDict): + RATE: float + PERIOD: int + COUNT: int + AMOUNT: float + +class TradingPairRawBook(TypedDict): + ORDER_ID: int + PRICE: float + AMOUNT: float + +class FundingCurrencyRawBook(TypedDict): + OFFER_ID: int + PERIOD: int + RATE: float + AMOUNT: float -(TradingPairBook, FundingCurrencyBook) = ( - TypedDict("TradingPairBook", { "PRICE": float, "COUNT": int, "AMOUNT": float }), - TypedDict("FundingCurrencyBook", { "RATE": float, "PERIOD": int, "COUNT": int, "AMOUNT": float }) -) +class Statistic(TypedDict): + MTS: int + VALUE: float -(TradingPairBooks, FundingCurrencyBooks) = (List[TradingPairBook], List[FundingCurrencyBook]) +class Candle(TypedDict): + MTS: int + OPEN: float + CLOSE: float + HIGH: float + LOW: float + VOLUME: float -(TradingPairRawBook, FundingCurrencyRawBook) = ( - TypedDict("TradingPairRawBook", { "ORDER_ID": int, "PRICE": float, "AMOUNT": float }), - TypedDict("FundingCurrencyRawBook", { "OFFER_ID": int, "PERIOD": int, "RATE": float, "AMOUNT": float }), -) +class DerivativesStatus(TypedDict): + KEY: Optional[str] + MTS: int + DERIV_PRICE: float + SPOT_PRICE: float + INSURANCE_FUND_BALANCE: float + NEXT_FUNDING_EVT_TIMESTAMP_MS: int + NEXT_FUNDING_ACCRUED: float + NEXT_FUNDING_STEP: int + CURRENT_FUNDING: float + MARK_PRICE: float + OPEN_INTEREST: float + CLAMP_MIN: float + CLAMP_MAX: float -(TradingPairRawBooks, FundingCurrencyRawBooks) = (List[TradingPairRawBook], List[FundingCurrencyRawBook]) +class Liquidation(TypedDict): + POS_ID: int + MTS: int + SYMBOL: str + AMOUNT: float + BASE_PRICE: float + IS_MATCH: int + IS_MARKET_SOLD: int + PRICE_ACQUIRED: float -Stat = TypedDict("Stat", { - "MTS": int, - "VALUE": float -}) +class Leaderboard(TypedDict): + MTS: int + USERNAME: str + RANKING: int + VALUE: float + TWITTER_HANDLE: Optional[str] -Stats = List[Stat] - -Candle = TypedDict("Candle", { - "MTS": int, - "OPEN": float, - "CLOSE": float, - "HIGH": float, - "LOW": float, - "VOLUME": float -}) - -Candles = List[Candle] - -DerivativesStatus = TypedDict("DerivativesStatus", { - "KEY": Optional[str], - "MTS": int, - "DERIV_PRICE": float, - "SPOT_PRICE": float, - "INSURANCE_FUND_BALANCE": float, - "NEXT_FUNDING_EVT_TIMESTAMP_MS": int, - "NEXT_FUNDING_ACCRUED": float, - "NEXT_FUNDING_STEP": int, - "CURRENT_FUNDING": float, - "MARK_PRICE": float, - "OPEN_INTEREST": float, - "CLAMP_MIN": float, - "CLAMP_MAX": float -}) - -DerivativeStatuses = List[DerivativesStatus] - -Liquidation = TypedDict("Liquidation", { - "POS_ID": int, - "MTS": int, - "SYMBOL": str, - "AMOUNT": float, - "BASE_PRICE": float, - "IS_MATCH": int, - "IS_MARKET_SOLD": int, - "PRICE_ACQUIRED": float -}) - -Liquidations = List[Liquidation] - -Leaderboard = TypedDict("Leaderboard", { - "MTS": int, - "USERNAME": str, - "RANKING": int, - "VALUE": float, - "TWITTER_HANDLE": Optional[str] -}) - -Leaderboards = List[Leaderboard] - -FundingStat = TypedDict("FundingStat", { - "TIMESTAMP": int, - "FRR": float, - "AVG_PERIOD": float, - "FUNDING_AMOUNT": float, - "FUNDING_AMOUNT_USED": float, - "FUNDING_BELOW_THRESHOLD": float -}) - -FundingStats = List[FundingStat] +class FundingStatistic(TypedDict): + TIMESTAMP: int + FRR: float + AVG_PERIOD: float + FUNDING_AMOUNT: float + FUNDING_AMOUNT_USED: float + FUNDING_BELOW_THRESHOLD: float #endregion \ No newline at end of file From 24b105378ac8f5803133c10854d4f7453bba5219 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 15 Dec 2022 19:14:00 +0100 Subject: [PATCH 069/265] Add hierarchy logic to custom exceptions. --- bfxapi/exceptions.py | 18 ++++++++++++++++++ bfxapi/labeler.py | 4 +++- bfxapi/rest/exceptions.py | 8 ++++++-- 3 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 bfxapi/exceptions.py diff --git a/bfxapi/exceptions.py b/bfxapi/exceptions.py new file mode 100644 index 0000000..8e9b6e5 --- /dev/null +++ b/bfxapi/exceptions.py @@ -0,0 +1,18 @@ +__all__ = [ + "BfxBaseException", + "LabelerSerializerException" +] + +class BfxBaseException(Exception): + """ + Base class for every custom exception in bfxapi/rest/exceptions.py and bfxapi/websocket/exceptions.py. + """ + + pass + +class LabelerSerializerException(BfxBaseException): + """ + This exception indicates an error thrown by the _Serializer class in bfxapi/labeler.py. + """ + + pass \ No newline at end of file diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index eb7076c..d358ffd 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -1,3 +1,5 @@ +from .exceptions import LabelerSerializerException + from typing import Generic, TypeVar, Iterable, Optional, List, Any T = TypeVar("T") @@ -10,7 +12,7 @@ class _Serializer(Generic[T]): labels = list(filter(lambda label: label not in (skip or list()), self.__labels)) if len(labels) > len(args): - raise Exception(" and <*args> arguments should contain the same amount of elements.") + raise LabelerSerializerException(" and <*args> arguments should contain the same amount of elements.") for index, label in enumerate(labels): if label not in self.__IGNORE: diff --git a/bfxapi/rest/exceptions.py b/bfxapi/rest/exceptions.py index 973fb5a..0fc6de8 100644 --- a/bfxapi/rest/exceptions.py +++ b/bfxapi/rest/exceptions.py @@ -1,12 +1,16 @@ +from ..exceptions import BfxBaseException + __all__ = [ + "BfxRestException", + "RequestParametersError", "ResourceNotFound", "InvalidAuthenticationCredentials" ] -class BfxRestException(Exception): +class BfxRestException(BfxBaseException): """ - Base class for all exceptions defined in bfxapi/rest/exceptions.py. + Base class for all custom exceptions in bfxapi/rest/exceptions.py. """ pass From 2595b8a7609ff5178670a4e9713fe35f3b545897 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 15 Dec 2022 19:21:19 +0100 Subject: [PATCH 070/265] Fix mypy errors and warnings in bfxapi/labeler.py script. --- bfxapi/labeler.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index d358ffd..bcf18c3 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -1,6 +1,6 @@ from .exceptions import LabelerSerializerException -from typing import Generic, TypeVar, Iterable, Optional, List, Any +from typing import Generic, TypeVar, Iterable, Optional, List, Tuple, Any, cast T = TypeVar("T") @@ -8,7 +8,7 @@ class _Serializer(Generic[T]): def __init__(self, name: str, labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]): self.name, self.__labels, self.__IGNORE = name, labels, IGNORE - def __serialize(self, *args: Any, skip: Optional[List[str]]) -> Iterable[T]: + def __serialize(self, *args: Any, skip: Optional[List[str]]) -> Iterable[Tuple[str, Any]]: labels = list(filter(lambda label: label not in (skip or list()), self.__labels)) if len(labels) > len(args): @@ -19,4 +19,4 @@ class _Serializer(Generic[T]): yield label, args[index] def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: - return dict(self.__serialize(*values, skip=skip)) \ No newline at end of file + return cast(T, dict(self.__serialize(*values, skip=skip))) \ No newline at end of file From 0e4cbd40a64c7f5ce8ab85466d4fcbc8d8ed824e Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 16 Dec 2022 16:03:28 +0100 Subject: [PATCH 071/265] Fix other mypy errors and warnings. --- bfxapi/client.py | 4 +++- bfxapi/rest/BfxRestInterface.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/bfxapi/client.py b/bfxapi/client.py index dd2fe93..75c3f2a 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -1,5 +1,7 @@ from .websocket import BfxWebsocketClient +from typing import Optional + from enum import Enum class Constants(str, Enum): @@ -10,7 +12,7 @@ class Constants(str, Enum): PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2" class Client(object): - def __init__(self, WSS_HOST: str = Constants.WSS_HOST, API_KEY: str = None, API_SECRET: str = None, log_level: str = "WARNING"): + def __init__(self, WSS_HOST: str = Constants.WSS_HOST, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None, log_level: str = "WARNING"): self.wss = BfxWebsocketClient( host=WSS_HOST, API_KEY=API_KEY, diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 6644247..37f52d3 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -84,7 +84,7 @@ class _RestPublicEndpoints(_Requests): def t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]: if isinstance(pairs, str) and pairs == "ALL": - return [ subdata for subdata in self.tickers([ "ALL" ]) if subdata["SYMBOL"].startswith("t") ] + return [ cast(TradingPairTicker, subdata) for subdata in self.tickers([ "ALL" ]) if cast(str, subdata["SYMBOL"]).startswith("t") ] data = self.tickers([ "t" + pair for pair in pairs ]) @@ -92,7 +92,7 @@ class _RestPublicEndpoints(_Requests): def f_tickers(self, currencies: Union[List[str], Literal["ALL"]]) -> List[FundingCurrencyTicker]: if isinstance(currencies, str) and currencies == "ALL": - return [ subdata for subdata in self.tickers([ "ALL" ]) if subdata["SYMBOL"].startswith("f") ] + return [ cast(FundingCurrencyTicker, subdata) for subdata in self.tickers([ "ALL" ]) if cast(str, subdata["SYMBOL"]).startswith("f") ] data = self.tickers([ "f" + currency for currency in currencies ]) From 0a53ab7f7e8fc16d5e62451c4e329d895d8f510b Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 16 Dec 2022 18:30:41 +0100 Subject: [PATCH 072/265] Apply lots of refactoring to the websocket subpackage (fix every mypy error and warning). Add integers.py and decimal.py to bfxapi.utils package. Update requirements.txt and setup.py with new mypy dependencies. --- bfxapi/exceptions.py | 19 +- bfxapi/rest/exceptions.py | 2 +- bfxapi/utils/decimal.py | 9 + bfxapi/utils/integers.py | 35 ++ bfxapi/websocket/BfxWebsocketClient.py | 36 +- bfxapi/websocket/exceptions.py | 22 +- bfxapi/websocket/serializers.py | 23 +- bfxapi/websocket/typings.py | 588 ++++++++++++------------- requirements.txt | Bin 304 -> 512 bytes setup.py | 7 +- 10 files changed, 393 insertions(+), 348 deletions(-) create mode 100644 bfxapi/utils/decimal.py create mode 100644 bfxapi/utils/integers.py diff --git a/bfxapi/exceptions.py b/bfxapi/exceptions.py index 8e9b6e5..1033837 100644 --- a/bfxapi/exceptions.py +++ b/bfxapi/exceptions.py @@ -1,6 +1,9 @@ __all__ = [ "BfxBaseException", - "LabelerSerializerException" + + "LabelerSerializerException", + "IntegerUnderflowError", + "IntegerOverflowflowError" ] class BfxBaseException(Exception): @@ -15,4 +18,18 @@ class LabelerSerializerException(BfxBaseException): This exception indicates an error thrown by the _Serializer class in bfxapi/labeler.py. """ + pass + +class IntegerUnderflowError(BfxBaseException): + """ + This error indicates an underflow in one of the integer types defined in bfxapi/utils/integers.py. + """ + + pass + +class IntegerOverflowflowError(BfxBaseException): + """ + This error indicates an overflow in one of the integer types defined in bfxapi/utils/integers.py. + """ + pass \ No newline at end of file diff --git a/bfxapi/rest/exceptions.py b/bfxapi/rest/exceptions.py index 0fc6de8..81bcb8f 100644 --- a/bfxapi/rest/exceptions.py +++ b/bfxapi/rest/exceptions.py @@ -1,4 +1,4 @@ -from ..exceptions import BfxBaseException +from .. exceptions import BfxBaseException __all__ = [ "BfxRestException", diff --git a/bfxapi/utils/decimal.py b/bfxapi/utils/decimal.py new file mode 100644 index 0000000..5a7af71 --- /dev/null +++ b/bfxapi/utils/decimal.py @@ -0,0 +1,9 @@ +import json + +from decimal import Decimal + +class DecimalEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, Decimal): + return str(obj) + return json.JSONEncoder.default(self, obj) \ No newline at end of file diff --git a/bfxapi/utils/integers.py b/bfxapi/utils/integers.py new file mode 100644 index 0000000..e38f107 --- /dev/null +++ b/bfxapi/utils/integers.py @@ -0,0 +1,35 @@ +from typing import cast, TypeVar + +from .. exceptions import IntegerUnderflowError, IntegerOverflowflowError + +__all__ = [ "Int16", "Int32", "Int45", "Int64" ] + +T = TypeVar("T") + +class _Int(int): + def __new__(cls: T, integer: int) -> T: + assert hasattr(cls, "_BITS"), "_Int must be extended by a class that has a static member _BITS (indicating the number of bits with which to represent the integers)." + + bits = cls._BITS - 1 + + min, max = -(2 ** bits), (2 ** bits) - 1 + + if integer < min: + raise IntegerUnderflowError(f"Underflow. Cannot store <{integer}> in {cls._BITS} bits integer. The min and max bounds are {min} and {max}.") + + if integer > max: + raise IntegerOverflowflowError(f"Overflow. Cannot store <{integer}> in {cls._BITS} bits integer. The min and max bounds are {min} and {max}.") + + return cast(T, super().__new__(int, integer)) + +class Int16(_Int): + _BITS = 16 + +class Int32(_Int): + _BITS = 32 + +class Int45(_Int): + _BITS = 45 + +class Int64(_Int): + _BITS = 64 \ No newline at end of file diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index e8bb4d0..775fa10 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -1,25 +1,40 @@ import traceback, json, asyncio, hmac, hashlib, time, uuid, websockets +from typing import Tuple, Union, Literal, TypeVar, Callable, cast + from enum import Enum from pyee.asyncio import AsyncIOEventEmitter -from .typings import Inputs, Tuple, Union +from .typings import Inputs from .handlers import Channels, PublicChannelsHandler, AuthenticatedChannelsHandler from .exceptions import ConnectionNotOpen, TooManySubscriptions, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion +from ..utils.decimal import DecimalEncoder + from ..utils.logger import Formatter, CustomLogger _HEARTBEAT = "hb" -def _require_websocket_connection(function): +F = TypeVar("F", bound=Callable[..., Literal[None]]) + +def _require_websocket_connection(function: F) -> F: async def wrapper(self, *args, **kwargs): if self.websocket == None or self.websocket.open == False: raise ConnectionNotOpen("No open connection with the server.") await function(self, *args, **kwargs) - return wrapper + return cast(F, wrapper) + +def _require_websocket_authentication(function: F) -> F: + async def wrapper(self, *args, **kwargs): + if self.authentication == False: + raise WebsocketAuthenticationRequired("To perform this action you need to authenticate using your API_KEY and API_SECRET.") + + await _require_websocket_connection(function)(self, *args, **kwargs) + + return cast(F, wrapper) class BfxWebsocketClient(object): VERSION = 2 @@ -118,22 +133,13 @@ class BfxWebsocketClient(object): for bucket in self.buckets: await bucket._close(code=code, reason=reason) - def __require_websocket_authentication(function): - async def wrapper(self, *args, **kwargs): - if self.authentication == False: - raise WebsocketAuthenticationRequired("To perform this action you need to authenticate using your API_KEY and API_SECRET.") - - await _require_websocket_connection(function)(self, *args, **kwargs) - - return wrapper - - @__require_websocket_authentication + @_require_websocket_authentication async def notify(self, info, MESSAGE_ID=None, **kwargs): await self.websocket.send(json.dumps([ 0, "n", MESSAGE_ID, { "type": "ucm-test", "info": info, **kwargs } ])) - @__require_websocket_authentication + @_require_websocket_authentication async def __handle_websocket_input(self, input, data): - await self.websocket.send(json.dumps([ 0, input, None, data])) + await self.websocket.send(json.dumps([ 0, input, None, data], cls=DecimalEncoder)) def __bucket_open_signal(self, index): if all(bucket.websocket != None and bucket.websocket.open == True for bucket in self.buckets): diff --git a/bfxapi/websocket/exceptions.py b/bfxapi/websocket/exceptions.py index c55b767..5691af8 100644 --- a/bfxapi/websocket/exceptions.py +++ b/bfxapi/websocket/exceptions.py @@ -1,4 +1,8 @@ +from .. exceptions import BfxBaseException + __all__ = [ + "BfxWebsocketException", + "ConnectionNotOpen", "TooManySubscriptions", "WebsocketAuthenticationRequired", @@ -7,9 +11,9 @@ __all__ = [ "OutdatedClientVersion" ] -class BfxWebsocketException(Exception): +class BfxWebsocketException(BfxBaseException): """ - Base class for all exceptions defined in bfxapi/websocket/exceptions.py. + Base class for all custom exceptions in bfxapi/websocket/exceptions.py. """ pass @@ -35,13 +39,6 @@ class WebsocketAuthenticationRequired(BfxWebsocketException): pass -class InvalidAuthenticationCredentials(BfxWebsocketException): - """ - This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication. - """ - - pass - class EventNotSupported(BfxWebsocketException): """ This error indicates a failed attempt to subscribe to an event not supported by the BfxWebsocketClient. @@ -54,4 +51,11 @@ class OutdatedClientVersion(BfxWebsocketException): This error indicates a mismatch between the client version and the server WSS version. """ + pass + +class InvalidAuthenticationCredentials(BfxWebsocketException): + """ + This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication. + """ + pass \ No newline at end of file diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index 64573cc..00f43d2 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -1,25 +1,6 @@ -from typing import Generic, TypeVar, Iterable, List, Any - from . import typings -from .exceptions import BfxWebsocketException - -T = TypeVar("T") - -class _Serializer(Generic[T]): - def __init__(self, name: str, labels: List[str]): - self.name, self.__labels = name, labels - - def __serialize(self, *args: Any, IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> Iterable[T]: - if len(self.__labels) != len(args): - raise BfxWebsocketException(" and <*args> arguments should contain the same amount of elements.") - - for index, label in enumerate(self.__labels): - if label not in IGNORE: - yield label, args[index] - - def parse(self, *values: Any) -> T: - return dict(self.__serialize(*values)) +from .. labeler import _Serializer #region Serializers definition for Websocket Public Channels @@ -315,7 +296,7 @@ BalanceInfo = _Serializer[typings.BalanceInfo]("BalanceInfo", labels=[ #region Serializers definition for Notifications channel -Notification = _Serializer("Notification", labels=[ +Notification = _Serializer[typings.Notification]("Notification", labels=[ "MTS", "TYPE", "MESSAGE_ID", diff --git a/bfxapi/websocket/typings.py b/bfxapi/websocket/typings.py index 4a3b918..9966d99 100644 --- a/bfxapi/websocket/typings.py +++ b/bfxapi/websocket/typings.py @@ -2,301 +2,294 @@ from decimal import Decimal from datetime import datetime -from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any +from typing import Type, NewType, Tuple, List, Dict, TypedDict, Union, Optional, Any -int16 = int32 = int45 = int64 = int +from ..utils.integers import Int16, Int32, Int45, Int64 JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] #region Type hinting for subscription objects class Subscriptions: - TradingPairsTicker = TypedDict("Subscriptions.TradingPairsTicker", { - "chanId": int, - "symbol": str, - "pair": str - }) + class TradingPairsTicker(TypedDict): + chanId: int + symbol: str + pair: str - FundingCurrenciesTicker = TypedDict("Subscriptions.FundingCurrenciesTicker", { - "chanId": int, - "symbol": str, - "currency": str - }) + class FundingCurrenciesTicker(TypedDict): + chanId: int + symbol: str + currency: str - TradingPairsTrades = TypedDict("Subscriptions.TradingPairsTrades", { - "chanId": int, - "symbol": str, - "pair": str - }) + class TradingPairsTrades(TypedDict): + chanId: int + symbol: str + pair: str - FundingCurrenciesTrades = TypedDict("Subscriptions.FundingCurrenciesTrades", { - "chanId": int, - "symbol": str, - "currency": str - }) + class FundingCurrenciesTrades(TypedDict): + chanId: int + symbol: str + currency: str - Book = TypedDict("Subscriptions.Book", { - "chanId": int, - "symbol": str, - "prec": str, - "freq": str, - "len": str, - "subId": int, - "pair": str - }) + class Book(TypedDict): + chanId: int + symbol: str + prec: str + freq: str + len: str + subId: int + pair: str - Candles = TypedDict("Subscriptions.Candles", { - "chanId": int, - "key": str - }) + class Candles(TypedDict): + chanId: int + key: str - DerivativesStatus = TypedDict("Subscriptions.DerivativesStatus", { - "chanId": int, - "key": str - }) + class DerivativesStatus(TypedDict): + chanId: int + key: str #endregion #region Type hinting for Websocket Public Channels -TradingPairTicker = TypedDict("TradingPairTicker", { - "BID": float, - "BID_SIZE": float, - "ASK": float, - "ASK_SIZE": float, - "DAILY_CHANGE": float, - "DAILY_CHANGE_RELATIVE": float, - "LAST_PRICE": float, - "VOLUME": float, - "HIGH": float, - "LOW": float -}) +class TradingPairTicker(TypedDict): + BID: float + BID_SIZE: float + ASK: float + ASK_SIZE: float + DAILY_CHANGE: float + DAILY_CHANGE_RELATIVE: float + LAST_PRICE: float + VOLUME: float + HIGH: float + LOW: float -FundingCurrencyTicker = TypedDict("FundingCurrencyTicker", { - "FRR": float, - "BID": float, - "BID_PERIOD": int, - "BID_SIZE": float, - "ASK": float, - "ASK_PERIOD": int, - "ASK_SIZE": float, - "DAILY_CHANGE": float, - "DAILY_CHANGE_RELATIVE": float, - "LAST_PRICE": float, - "VOLUME": float, - "HIGH": float, - "LOW": float, - "FRR_AMOUNT_AVAILABLE": float -}) +class FundingCurrencyTicker(TypedDict): + FRR: float + BID: float + BID_PERIOD: int + BID_SIZE: float + ASK: float + ASK_PERIOD: int + ASK_SIZE: float + DAILY_CHANGE: float + DAILY_CHANGE_RELATIVE: float + LAST_PRICE: float + VOLUME: float + HIGH: float + LOW: float + FRR_AMOUNT_AVAILABLE: float -(TradingPairTrade, FundingCurrencyTrade) = ( - TypedDict("TradingPairTrade", { "ID": int, "MTS": int, "AMOUNT": float, "PRICE": float }), - TypedDict("FundingCurrencyTrade", { "ID": int, "MTS": int, "AMOUNT": float, "RATE": float, "PERIOD": int }) -) +class TradingPairTrade(TypedDict): + ID: int + MTS: int + AMOUNT: float + PRICE: float -(TradingPairTrades, FundingCurrencyTrades) = (List[TradingPairTrade], List[FundingCurrencyTrade]) +class FundingCurrencyTrade(TypedDict): + ID: int + MTS: int + AMOUNT: float + RATE: float + PERIOD: int -(TradingPairBook, FundingCurrencyBook) = ( - TypedDict("TradingPairBook", { "PRICE": float, "COUNT": int, "AMOUNT": float }), - TypedDict("FundingCurrencyBook", { "RATE": float, "PERIOD": int, "COUNT": int, "AMOUNT": float }) -) +class TradingPairBook(TypedDict): + PRICE: float + COUNT: int + AMOUNT: float + +class FundingCurrencyBook(TypedDict): + RATE: float + PERIOD: int + COUNT: int + AMOUNT: float + +class TradingPairRawBook(TypedDict): + ORDER_ID: int + PRICE: float + AMOUNT: float + +class FundingCurrencyRawBook(TypedDict): + OFFER_ID: int + PERIOD: int + RATE: float + AMOUNT: float -(TradingPairBooks, FundingCurrencyBooks) = (List[TradingPairBook], List[FundingCurrencyBook]) +class Candle(TypedDict): + MTS: int + OPEN: float + CLOSE: float + HIGH: float + LOW: float + VOLUME: float -(TradingPairRawBook, FundingCurrencyRawBook) = ( - TypedDict("TradingPairRawBook", { "ORDER_ID": int, "PRICE": float, "AMOUNT": float }), - TypedDict("FundingCurrencyRawBook", { "OFFER_ID": int, "PERIOD": int, "RATE": float, "AMOUNT": float }), -) - -(TradingPairRawBooks, FundingCurrencyRawBooks) = (List[TradingPairRawBook], List[FundingCurrencyRawBook]) - -Candle = TypedDict("Candle", { - "MTS": int, - "OPEN": float, - "CLOSE": float, - "HIGH": float, - "LOW": float, - "VOLUME": float -}) - -Candles = List[Candle] - -DerivativesStatus = TypedDict("DerivativesStatus", { - "TIME_MS": int, - "DERIV_PRICE": float, - "SPOT_PRICE": float, - "INSURANCE_FUND_BALANCE": float, - "NEXT_FUNDING_EVT_TIMESTAMP_MS": int, - "NEXT_FUNDING_ACCRUED": float, - "NEXT_FUNDING_STEP": int, - "CURRENT_FUNDING": float, - "MARK_PRICE": float, - "OPEN_INTEREST": float, - "CLAMP_MIN": float, - "CLAMP_MAX": float -}) +class DerivativesStatus(TypedDict): + TIME_MS: int + DERIV_PRICE: float + SPOT_PRICE: float + INSURANCE_FUND_BALANCE: float + NEXT_FUNDING_EVT_TIMESTAMP_MS: int + NEXT_FUNDING_ACCRUED: float + NEXT_FUNDING_STEP: int + CURRENT_FUNDING: float + MARK_PRICE: float + OPEN_INTEREST: float + CLAMP_MIN: float + CLAMP_MAX: float #endregion #region Type hinting for Websocket Authenticated Channels -Order = TypedDict("Order", { - "ID": int, - "GID": int, - "CID": int, - "SYMBOL": str, - "MTS_CREATE": int, - "MTS_UPDATE": int, - "AMOUNT": float, - "AMOUNT_ORIG": float, - "ORDER_TYPE": str, - "TYPE_PREV": str, - "MTS_TIF": int, - "FLAGS": int, - "ORDER_STATUS": str, - "PRICE": float, - "PRICE_AVG": float, - "PRICE_TRAILING": float, - "PRICE_AUX_LIMIT": float, - "NOTIFY": int, - "HIDDEN": int, - "PLACED_ID": int, - "ROUTING": str, - "META": JSON -}) +class Order(TypedDict): + ID: int + GID: int + CID: int + SYMBOL: str + MTS_CREATE: int + MTS_UPDATE: int + AMOUNT: float + AMOUNT_ORIG: float + ORDER_TYPE: str + TYPE_PREV: str + MTS_TIF: int + FLAGS: int + ORDER_STATUS: str + PRICE: float + PRICE_AVG: float + PRICE_TRAILING: float + PRICE_AUX_LIMIT: float + NOTIFY: int + HIDDEN: int + PLACED_ID: int + ROUTING: str + META: JSON -Orders = List[Order] +class Position(TypedDict): + SYMBOL: str + STATUS: str + AMOUNT: float + BASE_PRICE: float + MARGIN_FUNDING: float + MARGIN_FUNDING_TYPE: int + PL: float + PL_PERC: float + PRICE_LIQ: float + LEVERAGE: float + POSITION_ID: int + MTS_CREATE: int + MTS_UPDATE: int + TYPE: int + COLLATERAL: float + COLLATERAL_MIN: float + META: JSON -Position = TypedDict("Position", { - "SYMBOL": str, - "STATUS": str, - "AMOUNT": float, - "BASE_PRICE": float, - "MARGIN_FUNDING": float, - "MARGIN_FUNDING_TYPE": int, - "PL": float, - "PL_PERC": float, - "PRICE_LIQ": float, - "LEVERAGE": float, - "POSITION_ID": int, - "MTS_CREATE": int, - "MTS_UPDATE": int, - "TYPE": int, - "COLLATERAL": float, - "COLLATERAL_MIN": float, - "META": JSON, -}) +class TradeExecuted(TypedDict): + ID: int + SYMBOL: str + MTS_CREATE: int + ORDER_ID: int + EXEC_AMOUNT: float + EXEC_PRICE: float + ORDER_TYPE: str + ORDER_PRICE: float + MAKER:int + CID: int -Positions = List[Position] +class TradeExecutionUpdate(TypedDict): + ID: int + SYMBOL: str + MTS_CREATE: int + ORDER_ID: int + EXEC_AMOUNT: float + EXEC_PRICE: float + ORDER_TYPE: str + ORDER_PRICE: float + MAKER:int + FEE: float + FEE_CURRENCY: str + CID: int -TradeExecuted = TypedDict("TradeExecuted", { - "ID": int, - "SYMBOL": str, - "MTS_CREATE": int, - "ORDER_ID": int, - "EXEC_AMOUNT": float, - "EXEC_PRICE": float, - "ORDER_TYPE": str, - "ORDER_PRICE": float, - "MAKER":int, - "CID": int -}) +class FundingOffer(TypedDict): + ID: int + SYMBOL: str + MTS_CREATED: int + MTS_UPDATED: int + AMOUNT: float + AMOUNT_ORIG: float + OFFER_TYPE: str + FLAGS: int + STATUS: str + RATE: float + PERIOD: int + NOTIFY: int + HIDDEN: int + RENEW: int -TradeExecutionUpdate = TypedDict("TradeExecutionUpdate", { - "ID": int, - "SYMBOL": str, - "MTS_CREATE": int, - "ORDER_ID": int, - "EXEC_AMOUNT": float, - "EXEC_PRICE": float, - "ORDER_TYPE": str, - "ORDER_PRICE": float, - "MAKER":int, - "FEE": float, - "FEE_CURRENCY": str, - "CID": int -}) +class FundingCredit(TypedDict): + ID: int + SYMBOL: str + SIDE: int + MTS_CREATE: int + MTS_UPDATE: int + AMOUNT: float + FLAGS: int + STATUS: str + RATE: float + PERIOD: int + MTS_OPENING: int + MTS_LAST_PAYOUT: int + NOTIFY: int + HIDDEN: int + RENEW: int + RATE_REAL: float + NO_CLOSE: int + POSITION_PAIR: str -FundingOffer = TypedDict("FundingOffer", { - "ID": int, - "SYMBOL": str, - "MTS_CREATED": int, - "MTS_UPDATED": int, - "AMOUNT": float, - "AMOUNT_ORIG": float, - "OFFER_TYPE": str, - "FLAGS": int, - "STATUS": str, - "RATE": float, - "PERIOD": int, - "NOTIFY": int, - "HIDDEN": int, - "RENEW": int, -}) +class FundingLoan(TypedDict): + ID: int + SYMBOL: str + SIDE: int + MTS_CREATE: int + MTS_UPDATE: int + AMOUNT: float + FLAGS: int + STATUS: str + RATE: float + PERIOD: int + MTS_OPENING: int + MTS_LAST_PAYOUT: int + NOTIFY: int + HIDDEN: int + RENEW: int + RATE_REAL: float + NO_CLOSE: int -FundingOffers = List[FundingOffer] +class Wallet(TypedDict): + WALLET_TYPE: str + CURRENCY: str + BALANCE: float + UNSETTLED_INTEREST: float + BALANCE_AVAILABLE: float + DESCRIPTION: str + META: JSON -FundingCredit = TypedDict("FundingCredit", { - "ID": int, - "SYMBOL": str, - "SIDE": int, - "MTS_CREATE": int, - "MTS_UPDATE": int, - "AMOUNT": float, - "FLAGS": int, - "STATUS": str, - "RATE": float, - "PERIOD": int, - "MTS_OPENING": int, - "MTS_LAST_PAYOUT": int, - "NOTIFY": int, - "HIDDEN": int, - "RENEW": int, - "RATE_REAL": float, - "NO_CLOSE": int, - "POSITION_PAIR": str -}) +class BalanceInfo(TypedDict): + AUM: float + AUM_NET: float -FundingCredits = List[FundingCredit] +#endregion -FundingLoan = TypedDict("FundingLoan", { - "ID": int, - "SYMBOL": str, - "SIDE": int, - "MTS_CREATE": int, - "MTS_UPDATE": int, - "AMOUNT": float, - "FLAGS": int, - "STATUS": str, - "RATE": float, - "PERIOD": int, - "MTS_OPENING": int, - "MTS_LAST_PAYOUT": int, - "NOTIFY": int, - "HIDDEN": int, - "RENEW": int, - "RATE_REAL": float, - "NO_CLOSE": int -}) +#region Serializers definition for Notifications channel -FundingLoans = List[FundingLoan] - -Wallet = TypedDict("Wallet", { - "WALLET_TYPE": str, - "CURRENCY": str, - "BALANCE": float, - "UNSETTLED_INTEREST": float, - "BALANCE_AVAILABLE": float, - "DESCRIPTION": str, - "META": JSON -}) - -Wallets = List[Wallet] - -BalanceInfo = TypedDict("BalanceInfo", { - "AUM": float, - "AUM_NET": float -}) +class Notification(TypedDict): + MTS: int + TYPE: str + MESSAGE_ID: int + NOTIFY_INFO: JSON + CODE: int + STATUS: str + TEXT: str #endregion @@ -304,55 +297,50 @@ BalanceInfo = TypedDict("BalanceInfo", { class Inputs: class Order: - New = TypedDict("Inputs.Order.New", { - "gid": Optional[int32], - "cid": int45, - "type": str, - "symbol": str, - "amount": Union[Decimal, str], - "price": Union[Decimal, str], - "lev": int, - "price_trailing": Union[Decimal, str], - "price_aux_limit": Union[Decimal, str], - "price_oco_stop": Union[Decimal, str], - "flags": int16, - "tif": Union[datetime, str], - "meta": JSON - }) + class New(TypedDict, total=False): + gid: Union[Int32, int] + cid: Union[Int45, int] + type: str + symbol: str + amount: Union[Decimal, str] + price: Union[Decimal, str] + lev: Union[Int32, int] + price_trailing: Union[Decimal, str] + price_aux_limit: Union[Decimal, str] + price_oco_stop: Union[Decimal, str] + flags: Union[Int16, int] + tif: Union[datetime, str] + meta: JSON - Update = TypedDict("Inputs.Order.Update", { - "id": int64, - "cid": int45, - "cid_date": str, - "gid": int32, - "price": Union[Decimal, str], - "amount": Union[Decimal, str], - "lev": int, - "delta": Union[Decimal, str], - "price_aux_limit": Union[Decimal, str], - "price_trailing": Union[Decimal, str], - "flags": int16, - "tif": Union[datetime, str] - }) + class Update(TypedDict, total=False): + id: Union[Int64, int] + cid: Union[Int45, int] + cid_date: str + gid: Union[Int32, int] + price: Union[Decimal, str] + amount: Union[Decimal, str] + lev: Union[Int32, int] + delta: Union[Decimal, str] + price_aux_limit: Union[Decimal, str] + price_trailing: Union[Decimal, str] + flags: Union[Int16, int] + tif: Union[datetime, str] - Cancel = TypedDict("Inputs.Order.Cancel", { - "id": int64, - "cid": int45, - "cid_date": str - }) + class Cancel(TypedDict, total=False): + id: Union[Int64, int] + cid: Union[Int45, int] + cid_date: Union[datetime, str] class Offer: - New = TypedDict("Inputs.Offer.New", { - "type": str, - "symbol": str, - "amount": Union[Decimal, str], - "rate": Union[Decimal, str], - "period": int, - "flags": int16 - }) + class New(TypedDict, total=False): + type: str + symbol: str + amount: Union[Decimal, str] + rate: Union[Decimal, str] + period: Union[Int32, int] + flags: Union[Int16, int] - Cancel = TypedDict("Inputs.Offer.Cancel", { - "id": int - }) + class Cancel(TypedDict, total=False): + id: Union[Int32, int] #endregion \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 549f7a67024ef377ed41842b9f7abfb02005904f..71a2708e787aaa497e5eaac30bc59566d18b7a57 100644 GIT binary patch delta 146 zcmdnM)W9-9j@^*Kh(V9Re4=WgaxOz9Lje%m0-*s=&=Lp@8F(4E7+^BG6Awyqf}~7< z*mz>0t7<+_E{7o#tOsNeND`u_1Z-L=LotKy#C}ClL!g8y*f3;?=d$cZKw+SP6W2Ea E0F%WVDgXcg delta 24 fcmZo**}ybGj@^<$kHLt+WTI-|!~+hK+Zd|=Oos;L diff --git a/setup.py b/setup.py index a4c8397..963f30a 100644 --- a/setup.py +++ b/setup.py @@ -11,11 +11,16 @@ setup( description="Official Bitfinex Python API", keywords="bitfinex,api,trading", install_requires=[ - "certifi~=2022.9.24", + "certifi~=2022.12.7", "charset-normalizer~=2.1.1", "idna~=3.4", + "mypy~=0.991", + "mypy-extensions~=0.4.3", "pyee~=9.0.4", "requests~=2.28.1", + "tomli~=2.0.1", + "types-requests~=2.28.11.5", + "types-urllib3~=1.26.25.4", "typing_extensions~=4.4.0", "urllib3~=1.26.13", "websockets~=10.4", From ea3eefd32c211a23d5e62748cf5746699b1a4907 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 16 Dec 2022 18:42:59 +0100 Subject: [PATCH 073/265] Apply refactoring with new standards in examples/websockets/*.py demos. --- examples/websocket/order_book.py | 8 +++++--- examples/websocket/raw_order_book.py | 8 +++++--- examples/websocket/ticker.py | 4 ++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/examples/websocket/order_book.py b/examples/websocket/order_book.py index a100c2b..edb765e 100644 --- a/examples/websocket/order_book.py +++ b/examples/websocket/order_book.py @@ -1,13 +1,15 @@ from collections import OrderedDict +from typing import List + from bfxapi import Client, Constants from bfxapi.websocket import BfxWebsocketClient from bfxapi.websocket.enums import Channels, Errors -from bfxapi.websocket.typings import Subscriptions, TradingPairBooks, TradingPairBook +from bfxapi.websocket.typings import Subscriptions, TradingPairBook class OrderBook(object): - def __init__(self, symbols: list[str]): + def __init__(self, symbols: List[str]): self.__order_book = { symbol: { "bids": OrderedDict(), "asks": OrderedDict() @@ -50,7 +52,7 @@ def on_subscribed(subscription): print(f"Subscription successful for pair <{subscription['pair']}>") @bfx.wss.on("t_book_snapshot") -def on_t_book_snapshot(subscription: Subscriptions.Book, snapshot: TradingPairBooks): +def on_t_book_snapshot(subscription: Subscriptions.Book, snapshot: List[TradingPairBook]): for data in snapshot: order_book.update(subscription["symbol"], data) diff --git a/examples/websocket/raw_order_book.py b/examples/websocket/raw_order_book.py index fe10490..b34ae8e 100644 --- a/examples/websocket/raw_order_book.py +++ b/examples/websocket/raw_order_book.py @@ -1,13 +1,15 @@ from collections import OrderedDict +from typing import List + from bfxapi import Client, Constants from bfxapi.websocket import BfxWebsocketClient from bfxapi.websocket.enums import Channels, Errors -from bfxapi.websocket.typings import Subscriptions, TradingPairRawBooks, TradingPairRawBook +from bfxapi.websocket.typings import Subscriptions, TradingPairRawBook class RawOrderBook(object): - def __init__(self, symbols: list[str]): + def __init__(self, symbols: List[str]): self.__raw_order_book = { symbol: { "bids": OrderedDict(), "asks": OrderedDict() @@ -50,7 +52,7 @@ def on_subscribed(subscription): print(f"Subscription successful for pair <{subscription['pair']}>") @bfx.wss.on("t_raw_book_snapshot") -def on_t_raw_book_snapshot(subscription: Subscriptions.Book, snapshot: TradingPairRawBooks): +def on_t_raw_book_snapshot(subscription: Subscriptions.Book, snapshot: List[TradingPairRawBook]): for data in snapshot: raw_order_book.update(subscription["symbol"], data) diff --git a/examples/websocket/ticker.py b/examples/websocket/ticker.py index 4e5d8e7..107e367 100644 --- a/examples/websocket/ticker.py +++ b/examples/websocket/ticker.py @@ -1,14 +1,14 @@ import asyncio from bfxapi import Client, Constants -from bfxapi.websocket import Channels +from bfxapi.websocket.enums import Channels from bfxapi.websocket.typings import Subscriptions, TradingPairTicker bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) @bfx.wss.on("t_ticker_update") def on_t_ticker_update(subscription: Subscriptions.TradingPairsTicker, data: TradingPairTicker): - print(f"Subscription channel ID: {subscription['chanId']}") + print(f"Subscription with channel ID: {subscription['chanId']}") print(f"Data: {data}") From 87bb6dc5c75be079641fc916f298bcce941ae628 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 20 Dec 2022 17:48:12 +0100 Subject: [PATCH 074/265] Add generic error handling (UnknownGenericError in bfxapi/rest/exceptions.py). Add support for new endpoints in _RestAuthenticatedEndpoints class. Extend serializers.py and typings.py. --- bfxapi/rest/BfxRestInterface.py | 16 ++++++-- bfxapi/rest/exceptions.py | 15 ++++++-- bfxapi/rest/serializers.py | 65 +++++++++++++++++++++++++++++++++ bfxapi/rest/typings.py | 58 +++++++++++++++++++++++++++++ bfxapi/websocket/typings.py | 12 +++--- examples/websocket/ticker.py | 2 +- 6 files changed, 154 insertions(+), 14 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 37f52d3..8fc5b07 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -8,7 +8,7 @@ from . import serializers from .typings import * from .enums import Config, Precision, Sort -from .exceptions import RequestParametersError, ResourceNotFound, InvalidAuthenticationCredentials +from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError class BfxRestInterface(object): def __init__(self, host, API_KEY = None, API_SECRET = None): @@ -47,13 +47,16 @@ class _Requests(object): if data[1] == 10020: raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") + if data[1] == None: + raise UnknownGenericError("The server replied to the request with a generic error.") + return data def _POST(self, endpoint, params = None, data = None, _append_authentication_headers = True): headers = { "Content-Type": "application/json" } if _append_authentication_headers: - headers = { **headers, **self.__build_authentication_headers(f"{endpoint}", data) } + headers = { **headers, **self.__build_authentication_headers(endpoint, data) } response = requests.post(f"{self.host}/{endpoint}", params=params, data=json.dumps(data), headers=headers) @@ -69,6 +72,9 @@ class _Requests(object): if data[1] == 10100: raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.") + if data[1] == None: + raise UnknownGenericError("The server replied to the request with a generic error.") + return data class _RestPublicEndpoints(_Requests): @@ -227,4 +233,8 @@ class _RestPublicEndpoints(_Requests): return self._GET(f"conf/{config}")[0] class _RestAuthenticatedEndpoints(_Requests): - __PREFIX = "auth/" \ No newline at end of file + def wallets(self) -> List[Wallet]: + return [ serializers.Wallet.parse(*subdata) for subdata in self._POST("auth/r/wallets") ] + + def retrieve_orders(self, ids: Optional[List[str]] = None) -> List[Order]: + return [ serializers.Order.parse(*subdata) for subdata in self._POST("auth/r/orders", data={ "id": ids }) ] \ No newline at end of file diff --git a/bfxapi/rest/exceptions.py b/bfxapi/rest/exceptions.py index 81bcb8f..beff7bc 100644 --- a/bfxapi/rest/exceptions.py +++ b/bfxapi/rest/exceptions.py @@ -15,16 +15,16 @@ class BfxRestException(BfxBaseException): pass -class RequestParametersError(BfxRestException): +class ResourceNotFound(BfxRestException): """ - This error indicates that there are some invalid parameters sent along with an HTTP request. + This error indicates a failed HTTP request to a non-existent resource. """ pass -class ResourceNotFound(BfxRestException): +class RequestParametersError(BfxRestException): """ - This error indicates a failed HTTP request to a non-existent resource. + This error indicates that there are some invalid parameters sent along with an HTTP request. """ pass @@ -34,4 +34,11 @@ class InvalidAuthenticationCredentials(BfxRestException): This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication. """ + pass + +class UnknownGenericError(BfxRestException): + """ + This error indicates an undefined problem processing an HTTP request sent to the APIs. + """ + pass \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 95f4c26..70f0887 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -183,4 +183,69 @@ FundingStatistic = _Serializer[typings.FundingStatistic]("FundingStatistic", lab "FUNDING_BELOW_THRESHOLD" ]) +#endregion + +#region Serializers definition for Rest Authenticated Endpoints + +Wallet = _Serializer[typings.Wallet]("Wallet", labels=[ + "WALLET_TYPE", + "CURRENCY", + "BALANCE", + "UNSETTLED_INTEREST", + "AVAILABLE_BALANCE", + "LAST_CHANGE", + "TRADE_DETAILS" +]) + +Order = _Serializer[typings.Order]("Order", labels=[ + "ID", + "GID", + "CID", + "SYMBOL", + "MTS_CREATE", + "MTS_UPDATE", + "AMOUNT", + "AMOUNT_ORIG", + "ORDER_TYPE", + "TYPE_PREV", + "MTS_TIF", + "_PLACEHOLDER", + "FLAGS", + "ORDER_STATUS", + "_PLACEHOLDER", + "_PLACEHOLDER", + "PRICE", + "PRICE_AVG", + "PRICE_TRAILING", + "PRICE_AUX_LIMIT", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "NOTIFY", + "HIDDEN", + "PLACED_ID", + "_PLACEHOLDER", + "_PLACEHOLDER", + "ROUTING", + "_PLACEHOLDER", + "_PLACEHOLDER", + "META" +]) + + +#endregion + +#region Serializers definition for Notifications channel + +Notification = _Serializer[typings.Notification]("Notification", labels=[ + "MTS", + "TYPE", + "MESSAGE_ID", + "_PLACEHOLDER", + "NOTIFY_INFO", + "CODE", + "STATUS", + "TEXT" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index 7af75fb..810d9da 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -1,5 +1,13 @@ +from decimal import Decimal + +from datetime import datetime + from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any +from ..utils.integers import Int16, Int32, Int45, Int64 + +JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] + #region Type hinting for Rest Public Endpoints class PlatformStatus(TypedDict): @@ -128,4 +136,54 @@ class FundingStatistic(TypedDict): FUNDING_AMOUNT_USED: float FUNDING_BELOW_THRESHOLD: float +#endregion + +#region Type hinting for Rest Authenticated Endpoints + +class Wallet(TypedDict): + WALLET_TYPE: str + CURRENCY: str + BALANCE: float + UNSETTLED_INTEREST: float + AVAILABLE_BALANCE: float + LAST_CHANGE: str + TRADE_DETAILS: JSON + +class Order(TypedDict): + ID: int + GID: int + CID: int + SYMBOL: str + MTS_CREATE: int + MTS_UPDATE: int + AMOUNT: float + AMOUNT_ORIG: float + ORDER_TYPE: str + TYPE_PREV: str + MTS_TIF: int + FLAGS: int + ORDER_STATUS: str + PRICE: float + PRICE_AVG: float + PRICE_TRAILING: float + PRICE_AUX_LIMIT: float + NOTIFY: int + HIDDEN: int + PLACED_ID: int + ROUTING: str + META: JSON + +#endregion + +#region Type hinting for Notifications channel + +class Notification(TypedDict): + MTS: int + TYPE: str + MESSAGE_ID: int + NOTIFY_INFO: JSON + CODE: int + STATUS: str + TEXT: str + #endregion \ No newline at end of file diff --git a/bfxapi/websocket/typings.py b/bfxapi/websocket/typings.py index 9966d99..ee55dd3 100644 --- a/bfxapi/websocket/typings.py +++ b/bfxapi/websocket/typings.py @@ -2,7 +2,7 @@ from decimal import Decimal from datetime import datetime -from typing import Type, NewType, Tuple, List, Dict, TypedDict, Union, Optional, Any +from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any from ..utils.integers import Int16, Int32, Int45, Int64 @@ -11,22 +11,22 @@ JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] #region Type hinting for subscription objects class Subscriptions: - class TradingPairsTicker(TypedDict): + class TradingPairTicker(TypedDict): chanId: int symbol: str pair: str - class FundingCurrenciesTicker(TypedDict): + class FundingCurrencyTicker(TypedDict): chanId: int symbol: str currency: str - class TradingPairsTrades(TypedDict): + class TradingPairTrades(TypedDict): chanId: int symbol: str pair: str - class FundingCurrenciesTrades(TypedDict): + class FundingCurrencyTrades(TypedDict): chanId: int symbol: str currency: str @@ -280,7 +280,7 @@ class BalanceInfo(TypedDict): #endregion -#region Serializers definition for Notifications channel +#region Type hinting for Notifications channel class Notification(TypedDict): MTS: int diff --git a/examples/websocket/ticker.py b/examples/websocket/ticker.py index 107e367..ff8d899 100644 --- a/examples/websocket/ticker.py +++ b/examples/websocket/ticker.py @@ -7,7 +7,7 @@ from bfxapi.websocket.typings import Subscriptions, TradingPairTicker bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) @bfx.wss.on("t_ticker_update") -def on_t_ticker_update(subscription: Subscriptions.TradingPairsTicker, data: TradingPairTicker): +def on_t_ticker_update(subscription: Subscriptions.TradingPairTicker, data: TradingPairTicker): print(f"Subscription with channel ID: {subscription['chanId']}") print(f"Data: {data}") From 6217f9040cb8cdd30182aaecfcdbb5a3137f088e Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 20 Dec 2022 18:40:41 +0100 Subject: [PATCH 075/265] Rename bfxapi/utils/decimal.py to encoder.py. Add support for datetime JSON serialization. Update class reference in BfxWebsocketClient.py. --- bfxapi/utils/{decimal.py => encoder.py} | 6 +++--- bfxapi/websocket/BfxWebsocketClient.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) rename bfxapi/utils/{decimal.py => encoder.py} (52%) diff --git a/bfxapi/utils/decimal.py b/bfxapi/utils/encoder.py similarity index 52% rename from bfxapi/utils/decimal.py rename to bfxapi/utils/encoder.py index 5a7af71..3649823 100644 --- a/bfxapi/utils/decimal.py +++ b/bfxapi/utils/encoder.py @@ -1,9 +1,9 @@ import json - from decimal import Decimal +from datetime import datetime -class DecimalEncoder(json.JSONEncoder): +class JSONEncoder(json.JSONEncoder): def default(self, obj): - if isinstance(obj, Decimal): + if isinstance(obj, Decimal) or isinstance(obj, datetime): return str(obj) return json.JSONEncoder.default(self, obj) \ No newline at end of file diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 775fa10..7cd8728 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -10,7 +10,7 @@ from .typings import Inputs from .handlers import Channels, PublicChannelsHandler, AuthenticatedChannelsHandler from .exceptions import ConnectionNotOpen, TooManySubscriptions, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion -from ..utils.decimal import DecimalEncoder +from ..utils.encoder import JSONEncoder from ..utils.logger import Formatter, CustomLogger @@ -139,7 +139,7 @@ class BfxWebsocketClient(object): @_require_websocket_authentication async def __handle_websocket_input(self, input, data): - await self.websocket.send(json.dumps([ 0, input, None, data], cls=DecimalEncoder)) + await self.websocket.send(json.dumps([ 0, input, None, data], cls=JSONEncoder)) def __bucket_open_signal(self, index): if all(bucket.websocket != None and bucket.websocket.open == True for bucket in self.buckets): From 79ae0b48e0d48600fa12e4c8a10722f7529ea994 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 21 Dec 2022 18:27:54 +0100 Subject: [PATCH 076/265] Fix bug in _Requests's _GET and _POST methods. Add submit_order to handle POST auth/w/order/submit endpoint. Add OrderType enumeration in bfxapi/rest/enums.py. --- bfxapi/rest/BfxRestInterface.py | 27 +++++++++++++++++++++------ bfxapi/rest/enums.py | 16 ++++++++++++++++ bfxapi/rest/serializers.py | 1 - 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 8fc5b07..6af32a4 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -7,7 +7,7 @@ from typing import List, Union, Literal, Optional, Any, cast from . import serializers from .typings import * -from .enums import Config, Precision, Sort +from .enums import OrderType, Config, Precision, Sort from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError class BfxRestInterface(object): @@ -47,8 +47,8 @@ class _Requests(object): if data[1] == 10020: raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") - if data[1] == None: - raise UnknownGenericError("The server replied to the request with a generic error.") + if data[1] == None or data[1] == 10000 or data[1] == 10001: + raise UnknownGenericError("The server replied to the request with a generic error with message: <{data[2]}>.") return data @@ -72,8 +72,8 @@ class _Requests(object): if data[1] == 10100: raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.") - if data[1] == None: - raise UnknownGenericError("The server replied to the request with a generic error.") + if data[1] == None or data[1] == 10000 or data[1] == 10001: + raise UnknownGenericError(f"The server replied to the request with a generic error with message: <{data[2]}>.") return data @@ -237,4 +237,19 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.Wallet.parse(*subdata) for subdata in self._POST("auth/r/wallets") ] def retrieve_orders(self, ids: Optional[List[str]] = None) -> List[Order]: - return [ serializers.Order.parse(*subdata) for subdata in self._POST("auth/r/orders", data={ "id": ids }) ] \ No newline at end of file + return [ serializers.Order.parse(*subdata) for subdata in self._POST("auth/r/orders", data={ "id": ids }) ] + + def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str], + price: Optional[Union[Decimal, str]] = None, lev: Optional[Union[Int32, int]] = None, + price_trailing: Optional[Union[Decimal, str]] = None, price_aux_limit: Optional[Union[Decimal, str]] = None, price_oco_stop: Optional[Union[Decimal, str]] = None, + gid: Optional[Union[Int32, int]] = None, cid: Optional[Union[Int45, int]] = None, + flags: Optional[Union[Int16, int]] = None, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification: + data = { + "type": type, "symbol": symbol, "amount": amount, + "price": price, "lev": lev, + "price_trailing": price_trailing, "price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop, + "gid": gid, "cid": cid, + "flags": flags, "tif": tif, "meta": meta + } + + return self._POST("auth/w/order/submit", data=data) \ No newline at end of file diff --git a/bfxapi/rest/enums.py b/bfxapi/rest/enums.py index 70c2336..e1e3f49 100644 --- a/bfxapi/rest/enums.py +++ b/bfxapi/rest/enums.py @@ -1,5 +1,21 @@ from enum import Enum +class OrderType(str, Enum): + LIMIT = "LIMIT" + EXCHANGE_LIMIT = "EXCHANGE LIMIT" + MARKET = "MARKET" + EXCHANGE_MARKET = "EXCHANGE MARKET" + STOP = "STOP" + EXCHANGE_STOP = "EXCHANGE STOP" + STOP_LIMIT = "STOP LIMIT" + EXCHANGE_STOP_LIMIT = "EXCHANGE STOP LIMIT" + TRAILING_STOP = "TRAILING STOP" + EXCHANGE_TRAILING_STOP = "EXCHANGE TRAILING STOP" + FOK = "FOK" + EXCHANGE_FOK = "EXCHANGE FOK" + IOC = "IOC" + EXCHANGE_IOC = "EXCHANGE IOC" + class Config(str, Enum): MAP_CURRENCY_SYM = "pub:map:currency:sym" MAP_CURRENCY_LABEL = "pub:map:currency:label" diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 70f0887..fe7d5a7 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -232,7 +232,6 @@ Order = _Serializer[typings.Order]("Order", labels=[ "META" ]) - #endregion #region Serializers definition for Notifications channel From d5ace495554bde36ad040fd1249bb6c1a137cc4f Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 22 Dec 2022 17:07:36 +0100 Subject: [PATCH 077/265] Add implementation for submit_order, update_order and cancel_order endpoint handlers in BfxRestInterface.py. --- bfxapi/rest/BfxRestInterface.py | 22 ++++++++++++++++++++-- bfxapi/rest/typings.py | 6 ------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 6af32a4..4eed8b7 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -10,6 +10,8 @@ from .typings import * from .enums import OrderType, Config, Precision, Sort from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError +from .. utils.encoder import JSONEncoder + class BfxRestInterface(object): def __init__(self, host, API_KEY = None, API_SECRET = None): self.public = _RestPublicEndpoints(host=host) @@ -58,7 +60,7 @@ class _Requests(object): if _append_authentication_headers: headers = { **headers, **self.__build_authentication_headers(endpoint, data) } - response = requests.post(f"{self.host}/{endpoint}", params=params, data=json.dumps(data), headers=headers) + response = requests.post(f"{self.host}/{endpoint}", params=params, data=json.dumps(data, cls=JSONEncoder), headers=headers) if response.status_code == HTTPStatus.NOT_FOUND: raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") @@ -252,4 +254,20 @@ class _RestAuthenticatedEndpoints(_Requests): "flags": flags, "tif": tif, "meta": meta } - return self._POST("auth/w/order/submit", data=data) \ No newline at end of file + return serializers.Notification.parse(*self._POST("auth/w/order/submit", data=data)) + + def update_order(self, id: Union[Int64, int], amount: Optional[Union[Decimal, str]] = None, price: Optional[Union[Decimal, str]] = None, + cid: Optional[Union[Int45, int]] = None, cid_date: Optional[str] = None, gid: Optional[Union[Int32, int]] = None, + flags: Optional[Union[Int16, int]] = None, lev: Optional[Union[Int32, int]] = None, delta: Optional[Union[Decimal, str]] = None, + price_aux_limit: Optional[Union[Decimal, str]] = None, price_trailing: Optional[Union[Decimal, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification: + data = { + "id": id, "amount": amount, "price": price, + "cid": cid, "cid_date": cid_date, "gid": gid, + "flags": flags, "lev": lev, "delta": delta, + "price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif + } + + return serializers.Notification.parse(*self._POST("auth/w/order/update", data=data)) + + def cancel_order(self, id: Union[Int64, int]) -> Notification: + return serializers.Notification.parse(*self._POST("auth/w/order/cancel", data={ "id": id })) \ No newline at end of file diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index 810d9da..468a44e 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -1,11 +1,5 @@ -from decimal import Decimal - -from datetime import datetime - from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any -from ..utils.integers import Int16, Int32, Int45, Int64 - JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] #region Type hinting for Rest Public Endpoints From 4f63f4068edec56eeadceccd7c19fe84360c17d2 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 22 Dec 2022 18:24:56 +0100 Subject: [PATCH 078/265] Add and implement notification.py in root package (bfxapi). --- bfxapi/labeler.py | 4 ++-- bfxapi/notification.py | 28 ++++++++++++++++++++++++++++ bfxapi/rest/BfxRestInterface.py | 10 +++++++--- bfxapi/rest/serializers.py | 17 ++--------------- bfxapi/rest/typings.py | 15 ++------------- 5 files changed, 41 insertions(+), 33 deletions(-) create mode 100644 bfxapi/notification.py diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index bcf18c3..4575146 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -8,7 +8,7 @@ class _Serializer(Generic[T]): def __init__(self, name: str, labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]): self.name, self.__labels, self.__IGNORE = name, labels, IGNORE - def __serialize(self, *args: Any, skip: Optional[List[str]]) -> Iterable[Tuple[str, Any]]: + def _serialize(self, *args: Any, skip: Optional[List[str]] = None) -> Iterable[Tuple[str, Any]]: labels = list(filter(lambda label: label not in (skip or list()), self.__labels)) if len(labels) > len(args): @@ -19,4 +19,4 @@ class _Serializer(Generic[T]): yield label, args[index] def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: - return cast(T, dict(self.__serialize(*values, skip=skip))) \ No newline at end of file + return cast(T, dict(self._serialize(*values, skip=skip))) \ No newline at end of file diff --git a/bfxapi/notification.py b/bfxapi/notification.py new file mode 100644 index 0000000..003867b --- /dev/null +++ b/bfxapi/notification.py @@ -0,0 +1,28 @@ +from typing import Dict, Optional, Any, TypedDict, cast + +from .labeler import _Serializer + +class Notification(TypedDict): + MTS: int + TYPE: str + MESSAGE_ID: Optional[int] + NOTIFY_INFO: Dict[str, Any] + CODE: Optional[int] + STATUS: str + TEXT: str + +class _Notification(_Serializer): + __LABELS = [ "MTS", "TYPE", "MESSAGE_ID", "_PLACEHOLDER", "NOTIFY_INFO", "CODE", "STATUS", "TEXT" ] + + def __init__(self, serializer: Optional[_Serializer] = None): + super().__init__("Notification", _Notification.__LABELS, IGNORE = [ "_PLACEHOLDER" ]) + + self.serializer = serializer + + def parse(self, *values: Any) -> Notification: + notification = dict(self._serialize(*values)) + + if self.serializer != None: + notification["NOTIFY_INFO"] = dict(self.serializer._serialize(*notification["NOTIFY_INFO"])) + + return cast(Notification, notification) \ No newline at end of file diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 4eed8b7..daf7c4e 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -1,5 +1,7 @@ import time, hmac, hashlib, json, requests +from decimal import Decimal +from datetime import datetime from http import HTTPStatus from typing import List, Union, Literal, Optional, Any, cast @@ -10,6 +12,8 @@ from .typings import * from .enums import OrderType, Config, Precision, Sort from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError +from .. utils.integers import Int16, Int32, Int45, Int64 + from .. utils.encoder import JSONEncoder class BfxRestInterface(object): @@ -254,7 +258,7 @@ class _RestAuthenticatedEndpoints(_Requests): "flags": flags, "tif": tif, "meta": meta } - return serializers.Notification.parse(*self._POST("auth/w/order/submit", data=data)) + return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/submit", data=data)) def update_order(self, id: Union[Int64, int], amount: Optional[Union[Decimal, str]] = None, price: Optional[Union[Decimal, str]] = None, cid: Optional[Union[Int45, int]] = None, cid_date: Optional[str] = None, gid: Optional[Union[Int32, int]] = None, @@ -267,7 +271,7 @@ class _RestAuthenticatedEndpoints(_Requests): "price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif } - return serializers.Notification.parse(*self._POST("auth/w/order/update", data=data)) + return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/update", data=data)) def cancel_order(self, id: Union[Int64, int]) -> Notification: - return serializers.Notification.parse(*self._POST("auth/w/order/cancel", data={ "id": id })) \ No newline at end of file + return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/cancel", data={ "id": id })) \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index fe7d5a7..3abea89 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -2,6 +2,8 @@ from . import typings from .. labeler import _Serializer +from .. notification import _Notification + #region Serializers definition for Rest Public Endpoints PlatformStatus = _Serializer[typings.PlatformStatus]("PlatformStatus", labels=[ @@ -232,19 +234,4 @@ Order = _Serializer[typings.Order]("Order", labels=[ "META" ]) -#endregion - -#region Serializers definition for Notifications channel - -Notification = _Serializer[typings.Notification]("Notification", labels=[ - "MTS", - "TYPE", - "MESSAGE_ID", - "_PLACEHOLDER", - "NOTIFY_INFO", - "CODE", - "STATUS", - "TEXT" -]) - #endregion \ No newline at end of file diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index 468a44e..f99b7c1 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -1,5 +1,7 @@ from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any +from .. notification import Notification + JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] #region Type hinting for Rest Public Endpoints @@ -167,17 +169,4 @@ class Order(TypedDict): ROUTING: str META: JSON -#endregion - -#region Type hinting for Notifications channel - -class Notification(TypedDict): - MTS: int - TYPE: str - MESSAGE_ID: int - NOTIFY_INFO: JSON - CODE: int - STATUS: str - TEXT: str - #endregion \ No newline at end of file From 454a7542ed4ef8b088d85c63bf6fea429fcada63 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 22 Dec 2022 18:42:55 +0100 Subject: [PATCH 079/265] Add bfxapi/enums.py file. Split enumerations in bfxapi/rest/enums.py and bfxapi/websocket/enums.py. Rename enumeration classes to use singular name identifiers. --- bfxapi/enums.py | 42 ++++++++++++++++++++++++++++ bfxapi/rest/BfxRestInterface.py | 12 ++++---- bfxapi/rest/enums.py | 18 +----------- bfxapi/websocket/enums.py | 29 ++----------------- examples/websocket/order_book.py | 4 +-- examples/websocket/raw_order_book.py | 4 +-- 6 files changed, 55 insertions(+), 54 deletions(-) create mode 100644 bfxapi/enums.py diff --git a/bfxapi/enums.py b/bfxapi/enums.py new file mode 100644 index 0000000..2293ff9 --- /dev/null +++ b/bfxapi/enums.py @@ -0,0 +1,42 @@ +from enum import Enum + +class OrderType(str, Enum): + LIMIT = "LIMIT" + EXCHANGE_LIMIT = "EXCHANGE LIMIT" + MARKET = "MARKET" + EXCHANGE_MARKET = "EXCHANGE MARKET" + STOP = "STOP" + EXCHANGE_STOP = "EXCHANGE STOP" + STOP_LIMIT = "STOP LIMIT" + EXCHANGE_STOP_LIMIT = "EXCHANGE STOP LIMIT" + TRAILING_STOP = "TRAILING STOP" + EXCHANGE_TRAILING_STOP = "EXCHANGE TRAILING STOP" + FOK = "FOK" + EXCHANGE_FOK = "EXCHANGE FOK" + IOC = "IOC" + EXCHANGE_IOC = "EXCHANGE IOC" + +class Flag(int, Enum): + HIDDEN = 64 + CLOSE = 512 + REDUCE_ONLY = 1024 + POST_ONLY = 4096 + OCO = 16384 + NO_VAR_RATES = 524288 + +class Error(int, Enum): + ERR_UNK = 10000 + ERR_GENERIC = 10001 + ERR_CONCURRENCY = 10008 + ERR_PARAMS = 10020 + ERR_CONF_FAIL = 10050 + ERR_AUTH_FAIL = 10100 + ERR_AUTH_PAYLOAD = 10111 + ERR_AUTH_SIG = 10112 + ERR_AUTH_HMAC = 10113 + ERR_AUTH_NONCE = 10114 + ERR_UNAUTH_FAIL = 10200 + ERR_SUB_FAIL = 10300 + ERR_SUB_MULTI = 10301 + ERR_UNSUB_FAIL = 10400 + ERR_READY = 11000 \ No newline at end of file diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index daf7c4e..31b529c 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -9,7 +9,7 @@ from typing import List, Union, Literal, Optional, Any, cast from . import serializers from .typings import * -from .enums import OrderType, Config, Precision, Sort +from .enums import Config, Precision, Sort, OrderType, Error from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError from .. utils.integers import Int16, Int32, Int45, Int64 @@ -50,10 +50,10 @@ class _Requests(object): data = response.json() if len(data) and data[0] == "error": - if data[1] == 10020: + if data[1] == Error.ERR_PARAMS: raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") - if data[1] == None or data[1] == 10000 or data[1] == 10001: + if data[1] == None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC: raise UnknownGenericError("The server replied to the request with a generic error with message: <{data[2]}>.") return data @@ -72,13 +72,13 @@ class _Requests(object): data = response.json() if len(data) and data[0] == "error": - if data[1] == 10020: + if data[1] == Error.ERR_PARAMS: raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") - if data[1] == 10100: + if data[1] == Error.ERR_AUTH_FAIL: raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.") - if data[1] == None or data[1] == 10000 or data[1] == 10001: + if data[1] == None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC: raise UnknownGenericError(f"The server replied to the request with a generic error with message: <{data[2]}>.") return data diff --git a/bfxapi/rest/enums.py b/bfxapi/rest/enums.py index e1e3f49..65c1e1a 100644 --- a/bfxapi/rest/enums.py +++ b/bfxapi/rest/enums.py @@ -1,20 +1,4 @@ -from enum import Enum - -class OrderType(str, Enum): - LIMIT = "LIMIT" - EXCHANGE_LIMIT = "EXCHANGE LIMIT" - MARKET = "MARKET" - EXCHANGE_MARKET = "EXCHANGE MARKET" - STOP = "STOP" - EXCHANGE_STOP = "EXCHANGE STOP" - STOP_LIMIT = "STOP LIMIT" - EXCHANGE_STOP_LIMIT = "EXCHANGE STOP LIMIT" - TRAILING_STOP = "TRAILING STOP" - EXCHANGE_TRAILING_STOP = "EXCHANGE TRAILING STOP" - FOK = "FOK" - EXCHANGE_FOK = "EXCHANGE FOK" - IOC = "IOC" - EXCHANGE_IOC = "EXCHANGE IOC" +from ..enums import * class Config(str, Enum): MAP_CURRENCY_SYM = "pub:map:currency:sym" diff --git a/bfxapi/websocket/enums.py b/bfxapi/websocket/enums.py index 14c4234..8f06f62 100644 --- a/bfxapi/websocket/enums.py +++ b/bfxapi/websocket/enums.py @@ -1,33 +1,8 @@ -from enum import Enum +from ..enums import * class Channels(str, Enum): TICKER = "ticker" TRADES = "trades" BOOK = "book" CANDLES = "candles" - STATUS = "status" - -class Flags(int, Enum): - HIDDEN = 64 - CLOSE = 512 - REDUCE_ONLY = 1024 - POST_ONLY = 4096 - OCO = 16384 - NO_VAR_RATES = 524288 - -class Errors(int, Enum): - ERR_UNK = 10000 - ERR_GENERIC = 10001 - ERR_CONCURRENCY = 10008 - ERR_PARAMS = 10020 - ERR_CONF_FAIL = 10050 - ERR_AUTH_FAIL = 10100 - ERR_AUTH_PAYLOAD = 10111 - ERR_AUTH_SIG = 10112 - ERR_AUTH_HMAC = 10113 - ERR_AUTH_NONCE = 10114 - ERR_UNAUTH_FAIL = 10200 - ERR_SUB_FAIL = 10300 - ERR_SUB_MULTI = 10301 - ERR_UNSUB_FAIL = 10400 - ERR_READY = 11000 \ No newline at end of file + STATUS = "status" \ No newline at end of file diff --git a/examples/websocket/order_book.py b/examples/websocket/order_book.py index edb765e..372a3f6 100644 --- a/examples/websocket/order_book.py +++ b/examples/websocket/order_book.py @@ -5,7 +5,7 @@ from typing import List from bfxapi import Client, Constants from bfxapi.websocket import BfxWebsocketClient -from bfxapi.websocket.enums import Channels, Errors +from bfxapi.websocket.enums import Channels, Error from bfxapi.websocket.typings import Subscriptions, TradingPairBook class OrderBook(object): @@ -39,7 +39,7 @@ order_book = OrderBook(symbols=SYMBOLS) bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) @bfx.wss.on("wss-error") -def on_wss_error(code: Errors, msg: str): +def on_wss_error(code: Error, msg: str): print(code, msg) @bfx.wss.on("open") diff --git a/examples/websocket/raw_order_book.py b/examples/websocket/raw_order_book.py index b34ae8e..5a65d78 100644 --- a/examples/websocket/raw_order_book.py +++ b/examples/websocket/raw_order_book.py @@ -5,7 +5,7 @@ from typing import List from bfxapi import Client, Constants from bfxapi.websocket import BfxWebsocketClient -from bfxapi.websocket.enums import Channels, Errors +from bfxapi.websocket.enums import Channels, Error from bfxapi.websocket.typings import Subscriptions, TradingPairRawBook class RawOrderBook(object): @@ -39,7 +39,7 @@ raw_order_book = RawOrderBook(symbols=SYMBOLS) bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) @bfx.wss.on("wss-error") -def on_wss_error(code: Errors, msg: str): +def on_wss_error(code: Error, msg: str): print(code, msg) @bfx.wss.on("open") From 18f9fef12de60bb70ce545ca6735d4bd1ab10fb1 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 22 Dec 2022 18:48:23 +0100 Subject: [PATCH 080/265] Fix some mypy errors and warnings. --- bfxapi/notification.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bfxapi/notification.py b/bfxapi/notification.py index 003867b..4dd618f 100644 --- a/bfxapi/notification.py +++ b/bfxapi/notification.py @@ -1,4 +1,4 @@ -from typing import Dict, Optional, Any, TypedDict, cast +from typing import List, Dict, Optional, Any, TypedDict, cast from .labeler import _Serializer @@ -19,10 +19,10 @@ class _Notification(_Serializer): self.serializer = serializer - def parse(self, *values: Any) -> Notification: + def parse(self, *values: Any, skip: Optional[List[str]] = None) -> Notification: notification = dict(self._serialize(*values)) - if self.serializer != None: - notification["NOTIFY_INFO"] = dict(self.serializer._serialize(*notification["NOTIFY_INFO"])) + if isinstance(self.serializer, _Serializer): + notification["NOTIFY_INFO"] = dict(self.serializer._serialize(*notification["NOTIFY_INFO"], skip=skip)) return cast(Notification, notification) \ No newline at end of file From db4438144d25308d3cf4aee9d283b69a906fee62 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 22 Dec 2022 18:57:57 +0100 Subject: [PATCH 081/265] Add new values in Error enumeration (bxapi/enums.py) according to new documentation update. --- bfxapi/enums.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bfxapi/enums.py b/bfxapi/enums.py index 2293ff9..fce3754 100644 --- a/bfxapi/enums.py +++ b/bfxapi/enums.py @@ -38,5 +38,8 @@ class Error(int, Enum): ERR_UNAUTH_FAIL = 10200 ERR_SUB_FAIL = 10300 ERR_SUB_MULTI = 10301 + ERR_SUB_UNK = 10302 + ERR_SUB_LIMIT = 10305 ERR_UNSUB_FAIL = 10400 + ERR_UNSUB_NOT = 10401 ERR_READY = 11000 \ No newline at end of file From 72a3252e32a1871ad71b536eec858f5be865a7ae Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 23 Dec 2022 16:36:51 +0100 Subject: [PATCH 082/265] Add support for new rest authenticated endpoints. --- bfxapi/notification.py | 14 ++++---- bfxapi/rest/BfxRestInterface.py | 60 ++++++++++++++++++++++++++++----- bfxapi/rest/serializers.py | 27 +++++++++++++++ bfxapi/rest/typings.py | 22 ++++++++++++ bfxapi/utils/integers.py | 12 +++++-- 5 files changed, 118 insertions(+), 17 deletions(-) diff --git a/bfxapi/notification.py b/bfxapi/notification.py index 4dd618f..5bb4cab 100644 --- a/bfxapi/notification.py +++ b/bfxapi/notification.py @@ -1,4 +1,4 @@ -from typing import List, Dict, Optional, Any, TypedDict, cast +from typing import List, Dict, Union, Optional, Any, TypedDict, cast from .labeler import _Serializer @@ -6,7 +6,7 @@ class Notification(TypedDict): MTS: int TYPE: str MESSAGE_ID: Optional[int] - NOTIFY_INFO: Dict[str, Any] + NOTIFY_INFO: Union[Dict[str, Any], List[Dict[str, Any]]] CODE: Optional[int] STATUS: str TEXT: str @@ -14,15 +14,17 @@ class Notification(TypedDict): class _Notification(_Serializer): __LABELS = [ "MTS", "TYPE", "MESSAGE_ID", "_PLACEHOLDER", "NOTIFY_INFO", "CODE", "STATUS", "TEXT" ] - def __init__(self, serializer: Optional[_Serializer] = None): + def __init__(self, serializer: Optional[_Serializer] = None, iterate: bool = False): super().__init__("Notification", _Notification.__LABELS, IGNORE = [ "_PLACEHOLDER" ]) - self.serializer = serializer + self.serializer, self.iterate = serializer, iterate def parse(self, *values: Any, skip: Optional[List[str]] = None) -> Notification: notification = dict(self._serialize(*values)) if isinstance(self.serializer, _Serializer): - notification["NOTIFY_INFO"] = dict(self.serializer._serialize(*notification["NOTIFY_INFO"], skip=skip)) - + if self.iterate == False: + notification["NOTIFY_INFO"] = dict(self.serializer._serialize(*notification["NOTIFY_INFO"], skip=skip)) + else: notification["NOTIFY_INFO"] = [ dict(self.serializer._serialize(*data, skip=skip)) for data in notification["NOTIFY_INFO"] ] + return cast(Notification, notification) \ No newline at end of file diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 31b529c..c44033a 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -12,7 +12,7 @@ from .typings import * from .enums import Config, Precision, Sort, OrderType, Error from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError -from .. utils.integers import Int16, Int32, Int45, Int64 +from .. utils.integers import Int16, int32, int45, int64 from .. utils.encoder import JSONEncoder @@ -246,10 +246,10 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.Order.parse(*subdata) for subdata in self._POST("auth/r/orders", data={ "id": ids }) ] def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str], - price: Optional[Union[Decimal, str]] = None, lev: Optional[Union[Int32, int]] = None, + price: Optional[Union[Decimal, str]] = None, lev: Optional[int] = None, price_trailing: Optional[Union[Decimal, str]] = None, price_aux_limit: Optional[Union[Decimal, str]] = None, price_oco_stop: Optional[Union[Decimal, str]] = None, - gid: Optional[Union[Int32, int]] = None, cid: Optional[Union[Int45, int]] = None, - flags: Optional[Union[Int16, int]] = None, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification: + gid: Optional[int] = None, cid: Optional[int] = None, + flags: Optional[int] = None, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification: data = { "type": type, "symbol": symbol, "amount": amount, "price": price, "lev": lev, @@ -260,9 +260,9 @@ class _RestAuthenticatedEndpoints(_Requests): return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/submit", data=data)) - def update_order(self, id: Union[Int64, int], amount: Optional[Union[Decimal, str]] = None, price: Optional[Union[Decimal, str]] = None, - cid: Optional[Union[Int45, int]] = None, cid_date: Optional[str] = None, gid: Optional[Union[Int32, int]] = None, - flags: Optional[Union[Int16, int]] = None, lev: Optional[Union[Int32, int]] = None, delta: Optional[Union[Decimal, str]] = None, + def update_order(self, id: int, amount: Optional[Union[Decimal, str]] = None, price: Optional[Union[Decimal, str]] = None, + cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, + flags: Optional[int] = None, lev: Optional[int] = None, delta: Optional[Union[Decimal, str]] = None, price_aux_limit: Optional[Union[Decimal, str]] = None, price_trailing: Optional[Union[Decimal, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification: data = { "id": id, "amount": amount, "price": price, @@ -273,5 +273,47 @@ class _RestAuthenticatedEndpoints(_Requests): return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/update", data=data)) - def cancel_order(self, id: Union[Int64, int]) -> Notification: - return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/cancel", data={ "id": id })) \ No newline at end of file + def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None) -> Notification: + data = { + "id": id, + "cid": cid, + "cid_date": cid_date + } + + return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/cancel", data=data)) + + def cancel_order_multi(self, ids: Optional[List[int]] = None, cids: Optional[List[Tuple[int, str]]] = None, gids: Optional[List[int]] = None, all: bool = False) -> Notification: + data = { + "ids": ids, + "cids": cids, + "gids": gids, + + "all": int(all) + } + + return serializers._Notification(serializer=serializers.Order, iterate=True).parse(*self._POST("auth/w/order/cancel/multi", data=data)) + + def orders_history(self, symbol: Optional[str] = None, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Order]: + if symbol == None: + endpoint = "auth/r/orders/hist" + else: endpoint = f"auth/r/orders/{symbol}/hist" + + data = { + "id": ids, + "start": start, "end": end, + "limit": limit + } + + return [ serializers.Order.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] + + def trades(self, symbol: str) -> List[Trade]: + return [ serializers.Trade.parse(*subdata) for subdata in self._POST(f"auth/r/trades/{symbol}/hist") ] + + def ledgers(self, currency: str, category: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Ledger]: + data = { + "category": category, + "start": start, "end": end, + "limit": limit + } + + return [ serializers.Ledger.parse(*subdata) for subdata in self._POST(f"auth/r/ledgers/{currency}/hist", data=data) ] \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 3abea89..9c4a320 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -234,4 +234,31 @@ Order = _Serializer[typings.Order]("Order", labels=[ "META" ]) +Trade = _Serializer[typings.Trade]("Trade", labels=[ + "ID", + "PAIR", + "MTS_CREATE", + "ORDER_ID", + "EXEC_AMOUNT", + "EXEC_PRICE", + "ORDER_TYPE", + "ORDER_PRICE", + "MAKER", + "FEE", + "FEE_CURRENCY", + "CID" +]) + +Ledger = _Serializer[typings.Ledger]("Ledger", labels=[ + "ID", + "CURRENCY", + "_PLACEHOLDER", + "MTS", + "_PLACEHOLDER", + "AMOUNT", + "BALANCE", + "_PLACEHOLDER", + "DESCRIPTION" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index f99b7c1..88c4a4c 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -169,4 +169,26 @@ class Order(TypedDict): ROUTING: str META: JSON +class Trade(TypedDict): + ID: int + SYMBOL: str + MTS_CREATE: int + ORDER_ID: int + EXEC_AMOUNT: float + EXEC_PRICE: float + ORDER_TYPE: str + ORDER_PRICE: float + MAKER:int + FEE: float + FEE_CURRENCY: str + CID: int + +class Ledger(TypedDict): + ID: int + CURRENCY: str + MTS: int + AMOUNT: float + BALANCE: float + description: str + #endregion \ No newline at end of file diff --git a/bfxapi/utils/integers.py b/bfxapi/utils/integers.py index e38f107..08582c6 100644 --- a/bfxapi/utils/integers.py +++ b/bfxapi/utils/integers.py @@ -1,4 +1,4 @@ -from typing import cast, TypeVar +from typing import cast, TypeVar, Union from .. exceptions import IntegerUnderflowError, IntegerOverflowflowError @@ -25,11 +25,19 @@ class _Int(int): class Int16(_Int): _BITS = 16 +int16 = Union[Int16, int] + class Int32(_Int): _BITS = 32 +int32 = Union[Int32, int] + class Int45(_Int): _BITS = 45 +int45 = Union[Int45, int] + class Int64(_Int): - _BITS = 64 \ No newline at end of file + _BITS = 64 + +int64 = Union[Int64, int] \ No newline at end of file From ef836bbe1aa21403e565e13904bcb4dd60be7e23 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Fri, 6 Jan 2023 15:18:57 +0100 Subject: [PATCH 083/265] Add funding related rest endpoints, refactor pre-existent rest endpoints to use get_ prefix. Add function to calculate flags easily. Add example test to create a funding offer. --- bfxapi/client.py | 16 ++++- bfxapi/enums.py | 5 ++ bfxapi/rest/BfxRestInterface.py | 91 ++++++++++++++++----------- bfxapi/rest/serializers.py | 24 +++++++ bfxapi/rest/typings.py | 16 +++++ bfxapi/utils/flags.py | 29 +++++++++ examples/rest/create_funding_offer.py | 26 ++++++++ 7 files changed, 170 insertions(+), 37 deletions(-) create mode 100644 bfxapi/utils/flags.py create mode 100644 examples/rest/create_funding_offer.py diff --git a/bfxapi/client.py b/bfxapi/client.py index 75c3f2a..e866235 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -1,3 +1,4 @@ +from .rest import BfxRestInterface from .websocket import BfxWebsocketClient from typing import Optional @@ -12,7 +13,20 @@ class Constants(str, Enum): PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2" class Client(object): - def __init__(self, WSS_HOST: str = Constants.WSS_HOST, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None, log_level: str = "WARNING"): + def __init__( + self, + REST_HOST: str = Constants.REST_HOST, + WSS_HOST: str = Constants.WSS_HOST, + API_KEY: Optional[str] = None, + API_SECRET: Optional[str] = None, + log_level: str = "WARNING" + ): + self.rest = BfxRestInterface( + host=REST_HOST, + API_KEY=API_KEY, + API_SECRET=API_SECRET + ) + self.wss = BfxWebsocketClient( host=WSS_HOST, API_KEY=API_KEY, diff --git a/bfxapi/enums.py b/bfxapi/enums.py index fce3754..03b89bf 100644 --- a/bfxapi/enums.py +++ b/bfxapi/enums.py @@ -16,6 +16,11 @@ class OrderType(str, Enum): IOC = "IOC" EXCHANGE_IOC = "EXCHANGE IOC" +class FundingOfferType(str, Enum): + LIMIT = "LIMIT" + FRR_DELTA_FIX = "FRRDELTAFIX" + FRR_DELTA_VAR = "FRRDELTAVAR" + class Flag(int, Enum): HIDDEN = 64 CLOSE = 512 diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index c44033a..404eeaf 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -9,7 +9,7 @@ from typing import List, Union, Literal, Optional, Any, cast from . import serializers from .typings import * -from .enums import Config, Precision, Sort, OrderType, Error +from .enums import Config, Precision, Sort, OrderType, FundingOfferType, Error from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError from .. utils.integers import Int16, int32, int45, int64 @@ -84,39 +84,39 @@ class _Requests(object): return data class _RestPublicEndpoints(_Requests): - def platform_status(self) -> PlatformStatus: + def get_platform_status(self) -> PlatformStatus: return serializers.PlatformStatus.parse(*self._GET("platform/status")) - def tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]: + def get_tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]: data = self._GET("tickers", params={ "symbols": ",".join(symbols) }) parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse } return [ parsers[subdata[0][0]](*subdata) for subdata in data ] - def t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]: + def get_t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]: if isinstance(pairs, str) and pairs == "ALL": - return [ cast(TradingPairTicker, subdata) for subdata in self.tickers([ "ALL" ]) if cast(str, subdata["SYMBOL"]).startswith("t") ] + return [ cast(TradingPairTicker, subdata) for subdata in self.get_tickers([ "ALL" ]) if cast(str, subdata["SYMBOL"]).startswith("t") ] - data = self.tickers([ "t" + pair for pair in pairs ]) + data = self.get_tickers([ "t" + pair for pair in pairs ]) return cast(List[TradingPairTicker], data) - def f_tickers(self, currencies: Union[List[str], Literal["ALL"]]) -> List[FundingCurrencyTicker]: + def get_f_tickers(self, currencies: Union[List[str], Literal["ALL"]]) -> List[FundingCurrencyTicker]: if isinstance(currencies, str) and currencies == "ALL": - return [ cast(FundingCurrencyTicker, subdata) for subdata in self.tickers([ "ALL" ]) if cast(str, subdata["SYMBOL"]).startswith("f") ] + return [ cast(FundingCurrencyTicker, subdata) for subdata in self.get_tickers([ "ALL" ]) if cast(str, subdata["SYMBOL"]).startswith("f") ] - data = self.tickers([ "f" + currency for currency in currencies ]) + data = self.get_tickers([ "f" + currency for currency in currencies ]) return cast(List[FundingCurrencyTicker], data) - def t_ticker(self, pair: str) -> TradingPairTicker: + def get_t_ticker(self, pair: str) -> TradingPairTicker: return serializers.TradingPairTicker.parse(*self._GET(f"ticker/t{pair}"), skip=["SYMBOL"]) - def f_ticker(self, currency: str) -> FundingCurrencyTicker: + def get_f_ticker(self, currency: str) -> FundingCurrencyTicker: return serializers.FundingCurrencyTicker.parse(*self._GET(f"ticker/f{currency}"), skip=["SYMBOL"]) - def tickers_history(self, symbols: List[str], start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[TickersHistory]: + def get_tickers_history(self, symbols: List[str], start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[TickersHistory]: params = { "symbols": ",".join(symbols), "start": start, "end": end, @@ -127,29 +127,29 @@ class _RestPublicEndpoints(_Requests): return [ serializers.TickersHistory.parse(*subdata) for subdata in data ] - def t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[TradingPairTrade]: + def get_t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[TradingPairTrade]: params = { "limit": limit, "start": start, "end": end, "sort": sort } data = self._GET(f"trades/{'t' + pair}/hist", params=params) return [ serializers.TradingPairTrade.parse(*subdata) for subdata in data ] - def f_trades(self, currency: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[FundingCurrencyTrade]: + def get_f_trades(self, currency: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[FundingCurrencyTrade]: params = { "limit": limit, "start": start, "end": end, "sort": sort } data = self._GET(f"trades/{'f' + currency}/hist", params=params) return [ serializers.FundingCurrencyTrade.parse(*subdata) for subdata in data ] - def t_book(self, pair: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairBook]: + def get_t_book(self, pair: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairBook]: return [ serializers.TradingPairBook.parse(*subdata) for subdata in self._GET(f"book/{'t' + pair}/{precision}", params={ "len": len }) ] - def f_book(self, currency: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyBook]: + def get_f_book(self, currency: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyBook]: return [ serializers.FundingCurrencyBook.parse(*subdata) for subdata in self._GET(f"book/{'f' + currency}/{precision}", params={ "len": len }) ] - def t_raw_book(self, pair: str, len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairRawBook]: + def get_t_raw_book(self, pair: str, len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairRawBook]: return [ serializers.TradingPairRawBook.parse(*subdata) for subdata in self._GET(f"book/{'t' + pair}/R0", params={ "len": len }) ] - def f_raw_book(self, currency: str, len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyRawBook]: + def get_f_raw_book(self, currency: str, len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyRawBook]: return [ serializers.FundingCurrencyRawBook.parse(*subdata) for subdata in self._GET(f"book/{'f' + currency}/R0", params={ "len": len }) ] - def stats_hist( + def get_stats_hist( self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None @@ -158,7 +158,7 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"stats1/{resource}/hist", params=params) return [ serializers.Statistic.parse(*subdata) for subdata in data ] - def stats_last( + def get_stats_last( self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None @@ -167,7 +167,7 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"stats1/{resource}/last", params=params) return serializers.Statistic.parse(*data) - def candles_hist( + def get_candles_hist( self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None @@ -176,7 +176,7 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"candles/{resource}/hist", params=params) return [ serializers.Candle.parse(*subdata) for subdata in data ] - def candles_last( + def get_candles_last( self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None @@ -185,14 +185,14 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"candles/{resource}/last", params=params) return serializers.Candle.parse(*data) - def derivatives_status(self, type: str, keys: List[str]) -> List[DerivativesStatus]: + def get_derivatives_status(self, type: str, keys: List[str]) -> List[DerivativesStatus]: params = { "keys": ",".join(keys) } data = self._GET(f"status/{type}", params=params) return [ serializers.DerivativesStatus.parse(*subdata) for subdata in data ] - def derivatives_status_history( + def get_derivatives_status_history( self, type: str, symbol: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None @@ -203,14 +203,14 @@ class _RestPublicEndpoints(_Requests): return [ serializers.DerivativesStatus.parse(*subdata, skip=[ "KEY" ]) for subdata in data ] - def liquidations(self, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Liquidation]: + def get_liquidations(self, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Liquidation]: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self._GET("liquidations/hist", params=params) return [ serializers.Liquidation.parse(*subdata[0]) for subdata in data ] - def leaderboards_hist( + def get_leaderboards_hist( self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None @@ -219,7 +219,7 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"rankings/{resource}/hist", params=params) return [ serializers.Leaderboard.parse(*subdata) for subdata in data ] - def leaderboards_last( + def get_leaderboards_last( self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None @@ -228,7 +228,7 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"rankings/{resource}/last", params=params) return serializers.Leaderboard.parse(*data) - def funding_stats(self, symbol: str, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingStatistic]: + def get_funding_stats(self, symbol: str, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingStatistic]: params = { "start": start, "end": end, "limit": limit } data = self._GET(f"funding/stats/{symbol}/hist", params=params) @@ -239,17 +239,17 @@ class _RestPublicEndpoints(_Requests): return self._GET(f"conf/{config}")[0] class _RestAuthenticatedEndpoints(_Requests): - def wallets(self) -> List[Wallet]: + def get_wallets(self) -> List[Wallet]: return [ serializers.Wallet.parse(*subdata) for subdata in self._POST("auth/r/wallets") ] - def retrieve_orders(self, ids: Optional[List[str]] = None) -> List[Order]: + def get_orders(self, ids: Optional[List[str]] = None) -> List[Order]: return [ serializers.Order.parse(*subdata) for subdata in self._POST("auth/r/orders", data={ "id": ids }) ] def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str], price: Optional[Union[Decimal, str]] = None, lev: Optional[int] = None, price_trailing: Optional[Union[Decimal, str]] = None, price_aux_limit: Optional[Union[Decimal, str]] = None, price_oco_stop: Optional[Union[Decimal, str]] = None, gid: Optional[int] = None, cid: Optional[int] = None, - flags: Optional[int] = None, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification: + flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification: data = { "type": type, "symbol": symbol, "amount": amount, "price": price, "lev": lev, @@ -262,7 +262,7 @@ class _RestAuthenticatedEndpoints(_Requests): def update_order(self, id: int, amount: Optional[Union[Decimal, str]] = None, price: Optional[Union[Decimal, str]] = None, cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, - flags: Optional[int] = None, lev: Optional[int] = None, delta: Optional[Union[Decimal, str]] = None, + flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, str]] = None, price_aux_limit: Optional[Union[Decimal, str]] = None, price_trailing: Optional[Union[Decimal, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification: data = { "id": id, "amount": amount, "price": price, @@ -293,7 +293,7 @@ class _RestAuthenticatedEndpoints(_Requests): return serializers._Notification(serializer=serializers.Order, iterate=True).parse(*self._POST("auth/w/order/cancel/multi", data=data)) - def orders_history(self, symbol: Optional[str] = None, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Order]: + def get_orders_history(self, symbol: Optional[str] = None, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Order]: if symbol == None: endpoint = "auth/r/orders/hist" else: endpoint = f"auth/r/orders/{symbol}/hist" @@ -306,14 +306,33 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.Order.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] - def trades(self, symbol: str) -> List[Trade]: + def get_trades(self, symbol: str) -> List[Trade]: return [ serializers.Trade.parse(*subdata) for subdata in self._POST(f"auth/r/trades/{symbol}/hist") ] - def ledgers(self, currency: str, category: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Ledger]: + def get_ledgers(self, currency: str, category: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Ledger]: data = { "category": category, "start": start, "end": end, "limit": limit } - return [ serializers.Ledger.parse(*subdata) for subdata in self._POST(f"auth/r/ledgers/{currency}/hist", data=data) ] \ No newline at end of file + return [ serializers.Ledger.parse(*subdata) for subdata in self._POST(f"auth/r/ledgers/{currency}/hist", data=data) ] + + def get_active_funding_offers(self, symbol: Optional[str] = None) -> List[FundingOffer]: + endpoint = "auth/r/funding/offers" + + if symbol != None: + endpoint += f"/{symbol}" + + return [ serializers.FundingOffer.parse(*subdata) for subdata in self._POST(endpoint) ] + + def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, str], + rate: Union[Decimal, str], period: int, + flags: Optional[int] = 0) -> Notification: + data = { + "type": type, "symbol": symbol, "amount": amount, + "rate": rate, "period": period, + "flags": flags + } + + return serializers._Notification(serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/submit", data=data)) \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 9c4a320..ad0f2d1 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -234,6 +234,30 @@ Order = _Serializer[typings.Order]("Order", labels=[ "META" ]) +FundingOffer = _Serializer[typings.FundingOffer]("FundingOffer", labels=[ + "ID", + "SYMBOL", + "MTS_CREATED", + "MTS_UPDATED", + "AMOUNT", + "AMOUNT_ORIG", + "OFFER_TYPE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "FLAGS", + "OFFER_STATUS", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "RATE", + "PERIOD", + "NOTIFY", + "HIDDEN", + "_PLACEHOLDER", + "RENEW", + "_PLACEHOLDER" +]) + Trade = _Serializer[typings.Trade]("Trade", labels=[ "ID", "PAIR", diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index 88c4a4c..20b2eed 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -169,6 +169,22 @@ class Order(TypedDict): ROUTING: str META: JSON +class FundingOffer(TypedDict): + ID: int + SYMBOL: str + MTS_CREATE: int + MTS_UPDATE: int + AMOUNT: float + AMOUNT_ORIG: float + OFFER_TYPE: str + FLAGS: int + OFFER_STATUS: str + RATE: float + PERIOD: int + NOTIFY: bool + HIDDEN: int + RENEW: bool + class Trade(TypedDict): ID: int SYMBOL: str diff --git a/bfxapi/utils/flags.py b/bfxapi/utils/flags.py new file mode 100644 index 0000000..f897103 --- /dev/null +++ b/bfxapi/utils/flags.py @@ -0,0 +1,29 @@ +from .. enums import Flag + +def calculate_order_flags( + hidden : bool = False, + close : bool = False, + reduce_only : bool = False, + post_only : bool = False, + oco : bool = False, + no_var_rates: bool = False +) -> int: + flags = 0 + + if hidden: flags += Flag.HIDDEN + if close: flags += Flag.CLOSE + if reduce_only: flags += Flag.REDUCE_ONLY + if post_only: flags += Flag.POST_ONLY + if oco: flags += Flag.OCO + if no_var_rates: flags += Flag.NO_VAR_RATES + + return flags + +def calculate_offer_flags( + hidden : bool = False +) -> int: + flags = 0 + + if hidden: flags += Flag.HIDDEN + + return flags \ No newline at end of file diff --git a/examples/rest/create_funding_offer.py b/examples/rest/create_funding_offer.py new file mode 100644 index 0000000..008d481 --- /dev/null +++ b/examples/rest/create_funding_offer.py @@ -0,0 +1,26 @@ +import os + +from bfxapi.client import Client, Constants +from bfxapi.utils.flags import calculate_offer_flags +from bfxapi.rest.typings import List, FundingOffer, Notification + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +notification: Notification = bfx.rest.auth.submit_funding_offer( + type="LIMIT", + symbol="fUSD", + amount="123.45", + rate="0.001", + period=2, + flags=calculate_offer_flags(hidden=True) +) + +print("Offer notification:", notification) + +offers: List[FundingOffer] = bfx.rest.auth.get_active_funding_offers() + +print("Offers:", offers) \ No newline at end of file From 22f6fe01fdd1b38035a52dab9f702588f859443b Mon Sep 17 00:00:00 2001 From: itsdeka Date: Fri, 6 Jan 2023 18:22:39 +0100 Subject: [PATCH 084/265] Add example to submit, cancel, edit order and adjust issue in labeler.py --- bfxapi/rest/BfxRestInterface.py | 8 +++--- bfxapi/utils/cid.py | 4 +++ examples/rest/create_funding_offer.py | 8 +++--- examples/rest/create_order.py | 36 +++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 bfxapi/utils/cid.py create mode 100644 examples/rest/create_order.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 404eeaf..02b2575 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -9,11 +9,9 @@ from typing import List, Union, Literal, Optional, Any, cast from . import serializers from .typings import * -from .enums import Config, Precision, Sort, OrderType, FundingOfferType, Error +from .enums import Config, Sort, OrderType, FundingOfferType, Error from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError -from .. utils.integers import Int16, int32, int45, int64 - from .. utils.encoder import JSONEncoder class BfxRestInterface(object): @@ -64,7 +62,9 @@ class _Requests(object): if _append_authentication_headers: headers = { **headers, **self.__build_authentication_headers(endpoint, data) } - response = requests.post(f"{self.host}/{endpoint}", params=params, data=json.dumps(data, cls=JSONEncoder), headers=headers) + data = (data and json.dumps({ key: value for key, value in data.items() if value != None}, cls=JSONEncoder) or None) + + response = requests.post(f"{self.host}/{endpoint}", params=params, data=data, headers=headers) if response.status_code == HTTPStatus.NOT_FOUND: raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") diff --git a/bfxapi/utils/cid.py b/bfxapi/utils/cid.py new file mode 100644 index 0000000..43150bb --- /dev/null +++ b/bfxapi/utils/cid.py @@ -0,0 +1,4 @@ +import time + +def generate_unique_cid(multiplier: int = 1000) -> int: + return int(round(time.time() * multiplier)) diff --git a/examples/rest/create_funding_offer.py b/examples/rest/create_funding_offer.py index 008d481..ecd470b 100644 --- a/examples/rest/create_funding_offer.py +++ b/examples/rest/create_funding_offer.py @@ -1,8 +1,8 @@ import os from bfxapi.client import Client, Constants +from bfxapi.enums import FundingOfferType from bfxapi.utils.flags import calculate_offer_flags -from bfxapi.rest.typings import List, FundingOffer, Notification bfx = Client( REST_HOST=Constants.REST_HOST, @@ -10,8 +10,8 @@ bfx = Client( API_SECRET=os.getenv("BFX_API_SECRET") ) -notification: Notification = bfx.rest.auth.submit_funding_offer( - type="LIMIT", +notification = bfx.rest.auth.submit_funding_offer( + type=FundingOfferType.LIMIT, symbol="fUSD", amount="123.45", rate="0.001", @@ -21,6 +21,6 @@ notification: Notification = bfx.rest.auth.submit_funding_offer( print("Offer notification:", notification) -offers: List[FundingOffer] = bfx.rest.auth.get_active_funding_offers() +offers = bfx.rest.auth.get_active_funding_offers() print("Offers:", offers) \ No newline at end of file diff --git a/examples/rest/create_order.py b/examples/rest/create_order.py new file mode 100644 index 0000000..34408aa --- /dev/null +++ b/examples/rest/create_order.py @@ -0,0 +1,36 @@ +import os + +from bfxapi.client import Client, Constants +from bfxapi.enums import OrderType +from bfxapi.utils.flags import calculate_order_flags + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +# Create a new order +submitted_order = bfx.rest.auth.submit_order( + type=OrderType.EXCHANGE_LIMIT, + symbol="tBTCUST", + amount="0.015", + price="10000", + flags=calculate_order_flags(hidden=False) +) + +print("Submit Order Notification:", submitted_order) + +# Update it +updated_order = bfx.rest.auth.update_order( + id=submitted_order["NOTIFY_INFO"]["ID"], + amount="0.020", + price="10100" +) + +print("Update Order Notification:", updated_order) + +# Delete it +canceled_order = bfx.rest.auth.cancel_order(id=submitted_order["NOTIFY_INFO"]["ID"]) + +print("Cancel Order Notification:", canceled_order) From 10862aea79a0b38a3da834d5b3c04c4c8cd88fdd Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 10 Jan 2023 18:19:30 +0100 Subject: [PATCH 085/265] Fix bug in bfxapi/notifications.py. --- bfxapi/notification.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bfxapi/notification.py b/bfxapi/notification.py index 5bb4cab..90d2f12 100644 --- a/bfxapi/notification.py +++ b/bfxapi/notification.py @@ -24,7 +24,12 @@ class _Notification(_Serializer): if isinstance(self.serializer, _Serializer): if self.iterate == False: - notification["NOTIFY_INFO"] = dict(self.serializer._serialize(*notification["NOTIFY_INFO"], skip=skip)) + NOTIFY_INFO = notification["NOTIFY_INFO"] + + if len(NOTIFY_INFO) == 1 and isinstance(NOTIFY_INFO[0], list): + NOTIFY_INFO = NOTIFY_INFO[0] + + notification["NOTIFY_INFO"] = dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip)) else: notification["NOTIFY_INFO"] = [ dict(self.serializer._serialize(*data, skip=skip)) for data in notification["NOTIFY_INFO"] ] return cast(Notification, notification) \ No newline at end of file From bb79a58ee5929c9889ebb490920e2b7ff845b2fd Mon Sep 17 00:00:00 2001 From: itsdeka Date: Wed, 11 Jan 2023 10:35:09 +0100 Subject: [PATCH 086/265] Fix mistakes in BfxRestInterface.py --- bfxapi/rest/BfxRestInterface.py | 120 +++++++++++++++++++++++--------- 1 file changed, 86 insertions(+), 34 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 02b2575..11a395b 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -13,6 +13,7 @@ from .enums import Config, Sort, OrderType, FundingOfferType, Error from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError from .. utils.encoder import JSONEncoder +from .. utils.cid import generate_unique_cid class BfxRestInterface(object): def __init__(self, host, API_KEY = None, API_SECRET = None): @@ -30,7 +31,7 @@ class _Requests(object): signature = hmac.new( self.API_SECRET.encode("utf8"), f"/api/v2/{endpoint}{nonce}{json.dumps(data)}".encode("utf8"), - hashlib.sha384 + hashlib.sha384 ).hexdigest() return { @@ -41,7 +42,7 @@ class _Requests(object): def _GET(self, endpoint, params = None): response = requests.get(f"{self.host}/{endpoint}", params=params) - + if response.status_code == HTTPStatus.NOT_FOUND: raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") @@ -62,10 +63,8 @@ class _Requests(object): if _append_authentication_headers: headers = { **headers, **self.__build_authentication_headers(endpoint, data) } - data = (data and json.dumps({ key: value for key, value in data.items() if value != None}, cls=JSONEncoder) or None) + response = requests.post(f"{self.host}/{endpoint}", params=params, data=json.dumps(data, cls=JSONEncoder), headers=headers) - response = requests.post(f"{self.host}/{endpoint}", params=params, data=data, headers=headers) - if response.status_code == HTTPStatus.NOT_FOUND: raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") @@ -89,9 +88,9 @@ class _RestPublicEndpoints(_Requests): def get_tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]: data = self._GET("tickers", params={ "symbols": ",".join(symbols) }) - + parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse } - + return [ parsers[subdata[0][0]](*subdata) for subdata in data ] def get_t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]: @@ -124,7 +123,7 @@ class _RestPublicEndpoints(_Requests): } data = self._GET("tickers/hist", params=params) - + return [ serializers.TickersHistory.parse(*subdata) for subdata in data ] def get_t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[TradingPairTrade]: @@ -150,7 +149,7 @@ class _RestPublicEndpoints(_Requests): return [ serializers.FundingCurrencyRawBook.parse(*subdata) for subdata in self._GET(f"book/{'f' + currency}/R0", params={ "len": len }) ] def get_stats_hist( - self, + self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> List[Statistic]: @@ -159,7 +158,7 @@ class _RestPublicEndpoints(_Requests): return [ serializers.Statistic.parse(*subdata) for subdata in data ] def get_stats_last( - self, + self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> Statistic: @@ -193,10 +192,10 @@ class _RestPublicEndpoints(_Requests): return [ serializers.DerivativesStatus.parse(*subdata) for subdata in data ] def get_derivatives_status_history( - self, + self, type: str, symbol: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> List[DerivativesStatus]: + ) -> List[DerivativesStatus]: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self._GET(f"status/{type}/{symbol}/hist", params=params) @@ -245,39 +244,92 @@ class _RestAuthenticatedEndpoints(_Requests): def get_orders(self, ids: Optional[List[str]] = None) -> List[Order]: return [ serializers.Order.parse(*subdata) for subdata in self._POST("auth/r/orders", data={ "id": ids }) ] - def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str], - price: Optional[Union[Decimal, str]] = None, lev: Optional[int] = None, + def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str], + price: Optional[Union[Decimal, str]] = None, lev: Optional[int] = None, price_trailing: Optional[Union[Decimal, str]] = None, price_aux_limit: Optional[Union[Decimal, str]] = None, price_oco_stop: Optional[Union[Decimal, str]] = None, - gid: Optional[int] = None, cid: Optional[int] = None, + gid: Optional[int] = None, flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification: + cid = generate_unique_cid() + data = { - "type": type, "symbol": symbol, "amount": amount, - "price": price, "lev": lev, - "price_trailing": price_trailing, "price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop, - "gid": gid, "cid": cid, - "flags": flags, "tif": tif, "meta": meta + "type": type, "symbol": symbol, "amount": str(amount), "price": str(price), "meta": meta, "cid": cid } - + + # add extra parameters + if flags: + data["flags"] = flags + + if price_trailing: + data["price_trailing"] = str(price_trailing) + + if price_aux_limit: + data["price_aux_limit"] = str(price_aux_limit) + + if price_oco_stop: + data["oco_stop_price"] = str(price_oco_stop) + + if tif: + data["tif"] = str(tif) + + if gid: + data["gid"] = gid + + if lev: + data["lev"] = str(lev) + return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/submit", data=data)) - def update_order(self, id: int, amount: Optional[Union[Decimal, str]] = None, price: Optional[Union[Decimal, str]] = None, + def update_order(self, id: int, amount: Optional[Union[Decimal, str]] = None, + price: Optional[Union[Decimal, str]] = None, cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, str]] = None, - price_aux_limit: Optional[Union[Decimal, str]] = None, price_trailing: Optional[Union[Decimal, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification: + price_aux_limit: Optional[Union[Decimal, str]] = None, + price_trailing: Optional[Union[Decimal, str]] = None, + tif: Optional[Union[datetime, str]] = None) -> Notification: data = { - "id": id, "amount": amount, "price": price, - "cid": cid, "cid_date": cid_date, "gid": gid, - "flags": flags, "lev": lev, "delta": delta, - "price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif + "id": id } - + + if amount: + data["amount"] = str(amount) + + if price: + data["price"] = str(price) + + if cid: + data["cid"] = cid + + if cid_date: + data["cid_date"] = str(cid_date) + + if gid: + data["gid"] = gid + + if flags: + data["flags"] = flags + + if lev: + data["lev"] = str(lev) + + if delta: + data["deta"] = str(delta) + + if price_aux_limit: + data["price_aux_limit"] = str(price_aux_limit) + + if price_trailing: + data["price_trailing"] = str(price_trailing) + + if tif: + data["tif"] = str(tif) + return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/update", data=data)) def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None) -> Notification: - data = { - "id": id, - "cid": cid, - "cid_date": cid_date + data = { + "id": id, + "cid": cid, + "cid_date": cid_date } return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/cancel", data=data)) @@ -297,7 +349,7 @@ class _RestAuthenticatedEndpoints(_Requests): if symbol == None: endpoint = "auth/r/orders/hist" else: endpoint = f"auth/r/orders/{symbol}/hist" - + data = { "id": ids, "start": start, "end": end, @@ -331,7 +383,7 @@ class _RestAuthenticatedEndpoints(_Requests): flags: Optional[int] = 0) -> Notification: data = { "type": type, "symbol": symbol, "amount": amount, - "rate": rate, "period": period, + "rate": rate, "period": period, "flags": flags } From 44ba7e780a2f06be2efc0fdd8383bf407e60e806 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Wed, 11 Jan 2023 11:51:16 +0100 Subject: [PATCH 087/265] Add wss exmaple to create order, refactoring --- examples/websocket/create_order.py | 43 ++++++++++++++++++++++++++++ examples/websocket/order_book.py | 3 +- examples/websocket/raw_order_book.py | 3 +- examples/websocket/ticker.py | 4 ++- 4 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 examples/websocket/create_order.py diff --git a/examples/websocket/create_order.py b/examples/websocket/create_order.py new file mode 100644 index 0000000..7c7be64 --- /dev/null +++ b/examples/websocket/create_order.py @@ -0,0 +1,43 @@ +# python -c "from examples.websocket.create_order import *" + +import os + +from bfxapi.client import Client, Constants +from bfxapi.utils.cid import generate_unique_cid +from bfxapi.websocket.enums import Error, OrderType +from bfxapi.websocket.typings import Inputs + +bfx = Client( + WSS_HOST=Constants.WSS_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +@bfx.wss.on("wss-error") +def on_wss_error(code: Error, msg: str): + print(code, msg) +@bfx.wss.on("authenticated") +async def on_open(event): + print(f"Auth event {event}") + + order: Inputs.Order.New = { + "gid": generate_unique_cid(), + "type": OrderType.EXCHANGE_LIMIT, + "symbol": "tBTCUST", + "amount": "0.1", + "price": "10000.0" + } + await bfx.wss.inputs.order_new(order) + + print(f"Order sent") +@bfx.wss.on("notification") +async def on_notification(notification): + print(f"Notification {notification}") +@bfx.wss.on("order_new") +async def on_order_new(order_new: Inputs.Order.New): + print(f"Order new {order_new}") +@bfx.wss.on("subscribed") +def on_subscribed(subscription): + print(f"Subscription successful <{subscription}>") + +bfx.wss.run() \ No newline at end of file diff --git a/examples/websocket/order_book.py b/examples/websocket/order_book.py index 372a3f6..0035cf8 100644 --- a/examples/websocket/order_book.py +++ b/examples/websocket/order_book.py @@ -1,10 +1,11 @@ +# python -c "from examples.websocket.order_book import *" + from collections import OrderedDict from typing import List from bfxapi import Client, Constants -from bfxapi.websocket import BfxWebsocketClient from bfxapi.websocket.enums import Channels, Error from bfxapi.websocket.typings import Subscriptions, TradingPairBook diff --git a/examples/websocket/raw_order_book.py b/examples/websocket/raw_order_book.py index 5a65d78..6cfc3c1 100644 --- a/examples/websocket/raw_order_book.py +++ b/examples/websocket/raw_order_book.py @@ -1,10 +1,11 @@ +# python -c "from examples.websocket.raw_order_book import *" + from collections import OrderedDict from typing import List from bfxapi import Client, Constants -from bfxapi.websocket import BfxWebsocketClient from bfxapi.websocket.enums import Channels, Error from bfxapi.websocket.typings import Subscriptions, TradingPairRawBook diff --git a/examples/websocket/ticker.py b/examples/websocket/ticker.py index ff8d899..5db8ed1 100644 --- a/examples/websocket/ticker.py +++ b/examples/websocket/ticker.py @@ -1,3 +1,5 @@ +# python -c "from examples.websocket.ticker import *" + import asyncio from bfxapi import Client, Constants @@ -16,4 +18,4 @@ def on_t_ticker_update(subscription: Subscriptions.TradingPairTicker, data: Trad async def open(): await bfx.wss.subscribe(Channels.TICKER, symbol="tBTCUSD") -asyncio.run(bfx.wss.start()) \ No newline at end of file +bfx.wss.run() \ No newline at end of file From f9f72a4ebbecaadcf7e7e6decc394a4ea581af2d Mon Sep 17 00:00:00 2001 From: itsdeka Date: Wed, 11 Jan 2023 11:51:41 +0100 Subject: [PATCH 088/265] Add lines --- examples/websocket/create_order.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/websocket/create_order.py b/examples/websocket/create_order.py index 7c7be64..15ba521 100644 --- a/examples/websocket/create_order.py +++ b/examples/websocket/create_order.py @@ -30,12 +30,15 @@ async def on_open(event): await bfx.wss.inputs.order_new(order) print(f"Order sent") + @bfx.wss.on("notification") async def on_notification(notification): print(f"Notification {notification}") + @bfx.wss.on("order_new") async def on_order_new(order_new: Inputs.Order.New): print(f"Order new {order_new}") + @bfx.wss.on("subscribed") def on_subscribed(subscription): print(f"Subscription successful <{subscription}>") From 1ff16e26b96201e76be09e7176282a4a2e7b6a22 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Wed, 11 Jan 2023 11:51:59 +0100 Subject: [PATCH 089/265] Add lines --- examples/websocket/create_order.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/websocket/create_order.py b/examples/websocket/create_order.py index 15ba521..36cd5c2 100644 --- a/examples/websocket/create_order.py +++ b/examples/websocket/create_order.py @@ -16,6 +16,7 @@ bfx = Client( @bfx.wss.on("wss-error") def on_wss_error(code: Error, msg: str): print(code, msg) + @bfx.wss.on("authenticated") async def on_open(event): print(f"Auth event {event}") From 99726b8e2543ae7208aa29198fcb41e275bb4dc1 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 11 Jan 2023 16:43:40 +0100 Subject: [PATCH 090/265] Roll-back to previous BfxRestInterface.py code. Remove CID enforcement with generate_unique_cid. Fix small bug in Requests._POST method. --- bfxapi/rest/BfxRestInterface.py | 129 ++++++++++---------------------- 1 file changed, 41 insertions(+), 88 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 11a395b..c11f7af 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -13,7 +13,6 @@ from .enums import Config, Sort, OrderType, FundingOfferType, Error from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError from .. utils.encoder import JSONEncoder -from .. utils.cid import generate_unique_cid class BfxRestInterface(object): def __init__(self, host, API_KEY = None, API_SECRET = None): @@ -28,10 +27,14 @@ class _Requests(object): def __build_authentication_headers(self, endpoint, data): nonce = str(int(time.time()) * 1000) + path = f"/api/v2/{endpoint}{nonce}" + + if data != None: path += data + signature = hmac.new( self.API_SECRET.encode("utf8"), - f"/api/v2/{endpoint}{nonce}{json.dumps(data)}".encode("utf8"), - hashlib.sha384 + path.encode("utf8"), + hashlib.sha384 ).hexdigest() return { @@ -42,7 +45,7 @@ class _Requests(object): def _GET(self, endpoint, params = None): response = requests.get(f"{self.host}/{endpoint}", params=params) - + if response.status_code == HTTPStatus.NOT_FOUND: raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") @@ -60,11 +63,14 @@ class _Requests(object): def _POST(self, endpoint, params = None, data = None, _append_authentication_headers = True): headers = { "Content-Type": "application/json" } + if isinstance(data, dict): + data = json.dumps({ key: value for key, value in data.items() if value != None}, cls=JSONEncoder) + if _append_authentication_headers: headers = { **headers, **self.__build_authentication_headers(endpoint, data) } - response = requests.post(f"{self.host}/{endpoint}", params=params, data=json.dumps(data, cls=JSONEncoder), headers=headers) - + response = requests.post(f"{self.host}/{endpoint}", params=params, data=data, headers=headers) + if response.status_code == HTTPStatus.NOT_FOUND: raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") @@ -88,9 +94,9 @@ class _RestPublicEndpoints(_Requests): def get_tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]: data = self._GET("tickers", params={ "symbols": ",".join(symbols) }) - + parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse } - + return [ parsers[subdata[0][0]](*subdata) for subdata in data ] def get_t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]: @@ -123,7 +129,7 @@ class _RestPublicEndpoints(_Requests): } data = self._GET("tickers/hist", params=params) - + return [ serializers.TickersHistory.parse(*subdata) for subdata in data ] def get_t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[TradingPairTrade]: @@ -149,7 +155,7 @@ class _RestPublicEndpoints(_Requests): return [ serializers.FundingCurrencyRawBook.parse(*subdata) for subdata in self._GET(f"book/{'f' + currency}/R0", params={ "len": len }) ] def get_stats_hist( - self, + self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> List[Statistic]: @@ -158,7 +164,7 @@ class _RestPublicEndpoints(_Requests): return [ serializers.Statistic.parse(*subdata) for subdata in data ] def get_stats_last( - self, + self, resource: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> Statistic: @@ -192,10 +198,10 @@ class _RestPublicEndpoints(_Requests): return [ serializers.DerivativesStatus.parse(*subdata) for subdata in data ] def get_derivatives_status_history( - self, + self, type: str, symbol: str, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> List[DerivativesStatus]: + ) -> List[DerivativesStatus]: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self._GET(f"status/{type}/{symbol}/hist", params=params) @@ -244,92 +250,39 @@ class _RestAuthenticatedEndpoints(_Requests): def get_orders(self, ids: Optional[List[str]] = None) -> List[Order]: return [ serializers.Order.parse(*subdata) for subdata in self._POST("auth/r/orders", data={ "id": ids }) ] - def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str], - price: Optional[Union[Decimal, str]] = None, lev: Optional[int] = None, + def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str], + price: Optional[Union[Decimal, str]] = None, lev: Optional[int] = None, price_trailing: Optional[Union[Decimal, str]] = None, price_aux_limit: Optional[Union[Decimal, str]] = None, price_oco_stop: Optional[Union[Decimal, str]] = None, - gid: Optional[int] = None, + gid: Optional[int] = None, cid: Optional[int] = None, flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification: - cid = generate_unique_cid() - data = { - "type": type, "symbol": symbol, "amount": str(amount), "price": str(price), "meta": meta, "cid": cid + "type": type, "symbol": symbol, "amount": amount, + "price": price, "lev": lev, + "price_trailing": price_trailing, "price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop, + "gid": gid, "cid": cid, + "flags": flags, "tif": tif, "meta": meta } - - # add extra parameters - if flags: - data["flags"] = flags - - if price_trailing: - data["price_trailing"] = str(price_trailing) - - if price_aux_limit: - data["price_aux_limit"] = str(price_aux_limit) - - if price_oco_stop: - data["oco_stop_price"] = str(price_oco_stop) - - if tif: - data["tif"] = str(tif) - - if gid: - data["gid"] = gid - - if lev: - data["lev"] = str(lev) - + return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/submit", data=data)) - def update_order(self, id: int, amount: Optional[Union[Decimal, str]] = None, - price: Optional[Union[Decimal, str]] = None, + def update_order(self, id: int, amount: Optional[Union[Decimal, str]] = None, price: Optional[Union[Decimal, str]] = None, cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, str]] = None, - price_aux_limit: Optional[Union[Decimal, str]] = None, - price_trailing: Optional[Union[Decimal, str]] = None, - tif: Optional[Union[datetime, str]] = None) -> Notification: + price_aux_limit: Optional[Union[Decimal, str]] = None, price_trailing: Optional[Union[Decimal, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification: data = { - "id": id + "id": id, "amount": amount, "price": price, + "cid": cid, "cid_date": cid_date, "gid": gid, + "flags": flags, "lev": lev, "delta": delta, + "price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif } - - if amount: - data["amount"] = str(amount) - - if price: - data["price"] = str(price) - - if cid: - data["cid"] = cid - - if cid_date: - data["cid_date"] = str(cid_date) - - if gid: - data["gid"] = gid - - if flags: - data["flags"] = flags - - if lev: - data["lev"] = str(lev) - - if delta: - data["deta"] = str(delta) - - if price_aux_limit: - data["price_aux_limit"] = str(price_aux_limit) - - if price_trailing: - data["price_trailing"] = str(price_trailing) - - if tif: - data["tif"] = str(tif) - + return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/update", data=data)) def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None) -> Notification: - data = { - "id": id, - "cid": cid, - "cid_date": cid_date + data = { + "id": id, + "cid": cid, + "cid_date": cid_date } return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/cancel", data=data)) @@ -349,7 +302,7 @@ class _RestAuthenticatedEndpoints(_Requests): if symbol == None: endpoint = "auth/r/orders/hist" else: endpoint = f"auth/r/orders/{symbol}/hist" - + data = { "id": ids, "start": start, "end": end, @@ -383,7 +336,7 @@ class _RestAuthenticatedEndpoints(_Requests): flags: Optional[int] = 0) -> Notification: data = { "type": type, "symbol": symbol, "amount": amount, - "rate": rate, "period": period, + "rate": rate, "period": period, "flags": flags } From ff58f049a76859552ae6b4365788a93f18a45867 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 12 Jan 2023 18:10:00 +0100 Subject: [PATCH 091/265] Separate _BfxWebsocketInputs from bfxapi/websocket/BfxWebsocketClient.py to _BfxWebsocketInputs.py. Rename websocket authenticated inputs methods. Refactoring to maintain consistency across the library. --- bfxapi/rest/BfxRestInterface.py | 5 +- bfxapi/websocket/BfxWebsocketClient.py | 33 +---------- bfxapi/websocket/_BfxWebsocketInputs.py | 78 +++++++++++++++++++++++++ bfxapi/websocket/typings.py | 58 ------------------ examples/rest/create_funding_offer.py | 2 +- examples/websocket/create_order.py | 31 +++++----- 6 files changed, 100 insertions(+), 107 deletions(-) create mode 100644 bfxapi/websocket/_BfxWebsocketInputs.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index c11f7af..4994bad 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -340,4 +340,7 @@ class _RestAuthenticatedEndpoints(_Requests): "flags": flags } - return serializers._Notification(serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/submit", data=data)) \ No newline at end of file + return serializers._Notification(serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/submit", data=data)) + + def cancel_funding_offer(self, id: int) -> Notification: + return serializers._Notification(serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/cancel", data={ "id": id })) \ No newline at end of file diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 7cd8728..c5b2f31 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -1,12 +1,10 @@ import traceback, json, asyncio, hmac, hashlib, time, uuid, websockets -from typing import Tuple, Union, Literal, TypeVar, Callable, cast - -from enum import Enum +from typing import Literal, TypeVar, Callable, cast from pyee.asyncio import AsyncIOEventEmitter -from .typings import Inputs +from ._BfxWebsocketInputs import _BfxWebsocketInputs from .handlers import Channels, PublicChannelsHandler, AuthenticatedChannelsHandler from .exceptions import ConnectionNotOpen, TooManySubscriptions, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion @@ -230,29 +228,4 @@ class _BfxWebsocketBucket(object): @_require_websocket_connection async def _close(self, code=1000, reason=str()): - await self.websocket.close(code=code, reason=reason) - -class _BfxWebsocketInputs(object): - def __init__(self, __handle_websocket_input): - self.__handle_websocket_input = __handle_websocket_input - - async def order_new(self, data: Inputs.Order.New): - await self.__handle_websocket_input("on", data) - - async def order_update(self, data: Inputs.Order.Update): - await self.__handle_websocket_input("ou", data) - - async def order_cancel(self, data: Inputs.Order.Cancel): - await self.__handle_websocket_input("oc", data) - - async def order_multiple_operations(self, *args: Tuple[str, Union[Inputs.Order.New, Inputs.Order.Update, Inputs.Order.Cancel]]): - await self.__handle_websocket_input("ox_multi", args) - - async def offer_new(self, data: Inputs.Offer.New): - await self.__handle_websocket_input("fon", data) - - async def offer_cancel(self, data: Inputs.Offer.Cancel): - await self.__handle_websocket_input("foc", data) - - async def calc(self, *args: str): - await self.__handle_websocket_input("calc", list(map(lambda arg: [arg], args))) \ No newline at end of file + await self.websocket.close(code=code, reason=reason) \ No newline at end of file diff --git a/bfxapi/websocket/_BfxWebsocketInputs.py b/bfxapi/websocket/_BfxWebsocketInputs.py new file mode 100644 index 0000000..fda6e19 --- /dev/null +++ b/bfxapi/websocket/_BfxWebsocketInputs.py @@ -0,0 +1,78 @@ +from decimal import Decimal +from datetime import datetime + +from typing import Union, Optional, List, Tuple +from .typings import JSON +from .enums import OrderType, FundingOfferType + +def _strip(dictionary): + return { key: value for key, value in dictionary.items() if value != None} + +class _BfxWebsocketInputs(object): + def __init__(self, __handle_websocket_input): + self.__handle_websocket_input = __handle_websocket_input + + async def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str], + price: Optional[Union[Decimal, str]] = None, lev: Optional[int] = None, + price_trailing: Optional[Union[Decimal, str]] = None, price_aux_limit: Optional[Union[Decimal, str]] = None, price_oco_stop: Optional[Union[Decimal, str]] = None, + gid: Optional[int] = None, cid: Optional[int] = None, + flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None): + data = _strip({ + "type": type, "symbol": symbol, "amount": amount, + "price": price, "lev": lev, + "price_trailing": price_trailing, "price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop, + "gid": gid, "cid": cid, + "flags": flags, "tif": tif, "meta": meta + }) + + await self.__handle_websocket_input("on", data) + + async def update_order(self, id: int, amount: Optional[Union[Decimal, str]] = None, price: Optional[Union[Decimal, str]] = None, + cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, + flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, str]] = None, + price_aux_limit: Optional[Union[Decimal, str]] = None, price_trailing: Optional[Union[Decimal, str]] = None, tif: Optional[Union[datetime, str]] = None): + data = _strip({ + "id": id, "amount": amount, "price": price, + "cid": cid, "cid_date": cid_date, "gid": gid, + "flags": flags, "lev": lev, "delta": delta, + "price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif + }) + + await self.__handle_websocket_input("ou", data) + + async def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None): + data = _strip({ + "id": id, + "cid": cid, + "cid_date": cid_date + }) + + await self.__handle_websocket_input("oc", data) + + async def cancel_order_multi(self, ids: Optional[List[int]] = None, cids: Optional[List[Tuple[int, str]]] = None, gids: Optional[List[int]] = None, all: bool = False): + data = _strip({ + "ids": ids, + "cids": cids, + "gids": gids, + + "all": int(all) + }) + + await self.__handle_websocket_input("oc_multi", data) + + async def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, str], + rate: Union[Decimal, str], period: int, + flags: Optional[int] = 0): + data = { + "type": type, "symbol": symbol, "amount": amount, + "rate": rate, "period": period, + "flags": flags + } + + await self.__handle_websocket_input("fon", data) + + async def cancel_funding_offer(self, id: int): + await self.__handle_websocket_input("foc", { "id": id }) + + async def calc(self, *args: str): + await self.__handle_websocket_input("calc", list(map(lambda arg: [arg], args))) \ No newline at end of file diff --git a/bfxapi/websocket/typings.py b/bfxapi/websocket/typings.py index ee55dd3..3488cd4 100644 --- a/bfxapi/websocket/typings.py +++ b/bfxapi/websocket/typings.py @@ -1,11 +1,5 @@ -from decimal import Decimal - -from datetime import datetime - from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any -from ..utils.integers import Int16, Int32, Int45, Int64 - JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] #region Type hinting for subscription objects @@ -291,56 +285,4 @@ class Notification(TypedDict): STATUS: str TEXT: str -#endregion - -#region Type hinting for Websocket Authenticated Inputs - -class Inputs: - class Order: - class New(TypedDict, total=False): - gid: Union[Int32, int] - cid: Union[Int45, int] - type: str - symbol: str - amount: Union[Decimal, str] - price: Union[Decimal, str] - lev: Union[Int32, int] - price_trailing: Union[Decimal, str] - price_aux_limit: Union[Decimal, str] - price_oco_stop: Union[Decimal, str] - flags: Union[Int16, int] - tif: Union[datetime, str] - meta: JSON - - class Update(TypedDict, total=False): - id: Union[Int64, int] - cid: Union[Int45, int] - cid_date: str - gid: Union[Int32, int] - price: Union[Decimal, str] - amount: Union[Decimal, str] - lev: Union[Int32, int] - delta: Union[Decimal, str] - price_aux_limit: Union[Decimal, str] - price_trailing: Union[Decimal, str] - flags: Union[Int16, int] - tif: Union[datetime, str] - - class Cancel(TypedDict, total=False): - id: Union[Int64, int] - cid: Union[Int45, int] - cid_date: Union[datetime, str] - - class Offer: - class New(TypedDict, total=False): - type: str - symbol: str - amount: Union[Decimal, str] - rate: Union[Decimal, str] - period: Union[Int32, int] - flags: Union[Int16, int] - - class Cancel(TypedDict, total=False): - id: Union[Int32, int] - #endregion \ No newline at end of file diff --git a/examples/rest/create_funding_offer.py b/examples/rest/create_funding_offer.py index ecd470b..2be1e5a 100644 --- a/examples/rest/create_funding_offer.py +++ b/examples/rest/create_funding_offer.py @@ -21,6 +21,6 @@ notification = bfx.rest.auth.submit_funding_offer( print("Offer notification:", notification) -offers = bfx.rest.auth.get_active_funding_offers() +offers = bfx.rest.auth.get_active_funding_offers(symbol="fUSD") print("Offers:", offers) \ No newline at end of file diff --git a/examples/websocket/create_order.py b/examples/websocket/create_order.py index 36cd5c2..92ede22 100644 --- a/examples/websocket/create_order.py +++ b/examples/websocket/create_order.py @@ -3,9 +3,8 @@ import os from bfxapi.client import Client, Constants -from bfxapi.utils.cid import generate_unique_cid from bfxapi.websocket.enums import Error, OrderType -from bfxapi.websocket.typings import Inputs +from bfxapi.websocket.typings import Order bfx = Client( WSS_HOST=Constants.WSS_HOST, @@ -18,30 +17,28 @@ def on_wss_error(code: Error, msg: str): print(code, msg) @bfx.wss.on("authenticated") -async def on_open(event): - print(f"Auth event {event}") +async def on_authenticated(event): + print(f"Authentication: {event}.") - order: Inputs.Order.New = { - "gid": generate_unique_cid(), - "type": OrderType.EXCHANGE_LIMIT, - "symbol": "tBTCUST", - "amount": "0.1", - "price": "10000.0" - } - await bfx.wss.inputs.order_new(order) + await bfx.wss.inputs.submit_order( + type=OrderType.EXCHANGE_LIMIT, + symbol="tBTCUSD", + amount="0.1", + price="10000.0" + ) - print(f"Order sent") + print("The order has been sent.") @bfx.wss.on("notification") async def on_notification(notification): - print(f"Notification {notification}") + print(f"Notification: {notification}.") @bfx.wss.on("order_new") -async def on_order_new(order_new: Inputs.Order.New): - print(f"Order new {order_new}") +async def on_order_new(order_new: Order): + print(f"Order new: {order_new}") @bfx.wss.on("subscribed") def on_subscribed(subscription): - print(f"Subscription successful <{subscription}>") + print(f"Subscription successful for <{subscription}>.") bfx.wss.run() \ No newline at end of file From e64c25bf192c81e7f259cedfa9dbd53c11df72f1 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 12 Jan 2023 18:36:23 +0100 Subject: [PATCH 092/265] Implement bfxapi/notifications.py in websocket subpackage. --- bfxapi/websocket/handlers.py | 19 +++++++++++++++++-- bfxapi/websocket/serializers.py | 17 ++--------------- bfxapi/websocket/typings.py | 15 ++------------- examples/websocket/create_order.py | 6 +++--- 4 files changed, 24 insertions(+), 33 deletions(-) diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index 9edd986..83c1408 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -146,7 +146,13 @@ class AuthenticatedChannelsHandler(object): ("bu",): serializers.BalanceInfo } - EVENTS = [ "notification", *list(__abbreviations.values()) ] + 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 @@ -168,4 +174,13 @@ class AuthenticatedChannelsHandler(object): raise BfxWebsocketException(f"Event of type <{type}> not found in self.__handlers.") def __notification(self, stream): - return self.event_emitter.emit("notification", serializers.Notification.parse(*stream)) \ No newline at end of file + if stream[1] == "on-req" or stream[1] == "ou-req" or stream[1] == "oc-req": + return self.event_emitter.emit(f"{stream[1]}-notification", serializers._Notification(serializer=serializers.Order).parse(*stream)) + + if stream[1] == "oc_multi-req": + return self.event_emitter.emit(f"{stream[1]}-notification", serializers._Notification(serializer=serializers.Order, iterate=True).parse(*stream)) + + if stream[1] == "fon-req" or stream[1] == "foc-req": + return self.event_emitter.emit(f"{stream[1]}-notification", serializers._Notification(serializer=serializers.FundingOffer).parse(*stream)) + + return self.event_emitter.emit("notification", serializers._Notification(serializer=None).parse(*stream)) \ No newline at end of file diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index 00f43d2..a9dd805 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -2,6 +2,8 @@ from . import typings from .. labeler import _Serializer +from .. notification import _Notification + #region Serializers definition for Websocket Public Channels TradingPairTicker = _Serializer[typings.TradingPairTicker]("TradingPairTicker", labels=[ @@ -292,19 +294,4 @@ BalanceInfo = _Serializer[typings.BalanceInfo]("BalanceInfo", labels=[ "AUM_NET", ]) -#endregion - -#region Serializers definition for Notifications channel - -Notification = _Serializer[typings.Notification]("Notification", labels=[ - "MTS", - "TYPE", - "MESSAGE_ID", - "_PLACEHOLDER", - "NOTIFY_INFO", - "CODE", - "STATUS", - "TEXT" -]) - #endregion \ No newline at end of file diff --git a/bfxapi/websocket/typings.py b/bfxapi/websocket/typings.py index 3488cd4..ced6aaa 100644 --- a/bfxapi/websocket/typings.py +++ b/bfxapi/websocket/typings.py @@ -1,5 +1,7 @@ from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any +from .. notification import Notification + JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] #region Type hinting for subscription objects @@ -272,17 +274,4 @@ class BalanceInfo(TypedDict): AUM: float AUM_NET: float -#endregion - -#region Type hinting for Notifications channel - -class Notification(TypedDict): - MTS: int - TYPE: str - MESSAGE_ID: int - NOTIFY_INFO: JSON - CODE: int - STATUS: str - TEXT: str - #endregion \ No newline at end of file diff --git a/examples/websocket/create_order.py b/examples/websocket/create_order.py index 92ede22..b3146e9 100644 --- a/examples/websocket/create_order.py +++ b/examples/websocket/create_order.py @@ -4,7 +4,7 @@ import os from bfxapi.client import Client, Constants from bfxapi.websocket.enums import Error, OrderType -from bfxapi.websocket.typings import Order +from bfxapi.websocket.typings import Notification, Order bfx = Client( WSS_HOST=Constants.WSS_HOST, @@ -29,8 +29,8 @@ async def on_authenticated(event): print("The order has been sent.") -@bfx.wss.on("notification") -async def on_notification(notification): +@bfx.wss.on("on-req-notification") +async def on_notification(notification: Notification): print(f"Notification: {notification}.") @bfx.wss.on("order_new") From 35cc360e3789d3029c512b754d003858208a4bff Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 13 Jan 2023 17:11:28 +0100 Subject: [PATCH 093/265] Add authenticated data example --- bfxapi/rest/BfxRestInterface.py | 64 +++++++++++++-- bfxapi/rest/serializers.py | 63 +++++++++++++++ bfxapi/rest/typings.py | 51 ++++++++++++ examples/rest/get_authenticated_data.py | 101 ++++++++++++++++++++++++ 4 files changed, 273 insertions(+), 6 deletions(-) create mode 100644 examples/rest/get_authenticated_data.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 4994bad..3a02593 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -247,8 +247,16 @@ class _RestAuthenticatedEndpoints(_Requests): def get_wallets(self) -> List[Wallet]: return [ serializers.Wallet.parse(*subdata) for subdata in self._POST("auth/r/wallets") ] - def get_orders(self, ids: Optional[List[str]] = None) -> List[Order]: - return [ serializers.Order.parse(*subdata) for subdata in self._POST("auth/r/orders", data={ "id": ids }) ] + def get_orders(self, symbol: Optional[str] = None, ids: Optional[List[str]] = None) -> List[Order]: + endpoint = "auth/r/orders" + + if symbol != None: + endpoint += f"/{symbol}" + + return [ serializers.Order.parse(*subdata) for subdata in self._POST(endpoint, data={ "id": ids }) ] + + def get_positions(self) -> List[Position]: + return [ serializers.Position.parse(*subdata) for subdata in self._POST("auth/r/positions") ] def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str], price: Optional[Union[Decimal, str]] = None, lev: Optional[int] = None, @@ -311,8 +319,21 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.Order.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] - def get_trades(self, symbol: str) -> List[Trade]: - return [ serializers.Trade.parse(*subdata) for subdata in self._POST(f"auth/r/trades/{symbol}/hist") ] + def get_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Trade]: + if symbol == None: + endpoint = "auth/r/trades/hist" + else: endpoint = f"auth/r/trades/{symbol}/hist" + + data = { + "sort": sort, + "start": start, "end": end, + "limit": limit + } + + return [ serializers.Trade.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] + + def get_order_trades(self, symbol: str, id: int) -> List[OrderTrade]: + return [ serializers.OrderTrade.parse(*subdata) for subdata in self._POST(f"auth/r/order/{symbol}:{id}/trades") ] def get_ledgers(self, currency: str, category: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Ledger]: data = { @@ -323,7 +344,7 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.Ledger.parse(*subdata) for subdata in self._POST(f"auth/r/ledgers/{currency}/hist", data=data) ] - def get_active_funding_offers(self, symbol: Optional[str] = None) -> List[FundingOffer]: + def get_funding_offers(self, symbol: Optional[str] = None) -> List[FundingOffer]: endpoint = "auth/r/funding/offers" if symbol != None: @@ -343,4 +364,35 @@ class _RestAuthenticatedEndpoints(_Requests): return serializers._Notification(serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/submit", data=data)) def cancel_funding_offer(self, id: int) -> Notification: - return serializers._Notification(serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/cancel", data={ "id": id })) \ No newline at end of file + return serializers._Notification(serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/cancel", data={ "id": id })) + + def get_funding_offers_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingOffer]: + if symbol == None: + endpoint = "auth/r/funding/offers/hist" + else: endpoint = f"auth/r/funding/offers/{symbol}/hist" + + data = { + "start": start, "end": end, + "limit": limit + } + + return [ serializers.FundingOffer.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] + + def get_funding_credits(self, symbol: Optional[str] = None) -> List[FundingCredit]: + if symbol == None: + endpoint = "auth/r/funding/credits" + else: endpoint = f"auth/r/funding/credits/{symbol}" + + return [ serializers.FundingCredit.parse(*subdata) for subdata in self._POST(endpoint) ] + + def get_funding_credits_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingCredit]: + if symbol == None: + endpoint = "auth/r/funding/credits/hist" + else: endpoint = f"auth/r/funding/credits/{symbol}/hist" + + data = { + "start": start, "end": end, + "limit": limit + } + + return [ serializers.FundingCredit.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index ad0f2d1..9e2612a 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -234,6 +234,29 @@ Order = _Serializer[typings.Order]("Order", labels=[ "META" ]) +Position = _Serializer[typings.Position]("Position", labels=[ + "SYMBOL", + "STATUS", + "AMOUNT", + "BASE_PRICE", + "FUNDING", + "FUNDING_TYPE", + "PL", + "PL_PERC", + "PRICE_LIQ", + "LEVERAGE", + "_PLACEHOLDER", + "POSITION_ID", + "MTS_CREATE", + "MTS_UPDATE", + "_PLACEHOLDER", + "TYPE", + "_PLACEHOLDER", + "COLLATERAL", + "COLLATERAL_MIN", + "META" +]) + FundingOffer = _Serializer[typings.FundingOffer]("FundingOffer", labels=[ "ID", "SYMBOL", @@ -273,6 +296,21 @@ Trade = _Serializer[typings.Trade]("Trade", labels=[ "CID" ]) +OrderTrade = _Serializer[typings.OrderTrade]("OrderTrade", labels=[ + "ID", + "PAIR", + "MTS_CREATE", + "ORDER_ID", + "EXEC_AMOUNT", + "EXEC_PRICE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "MAKER", + "FEE", + "FEE_CURRENCY", + "CID" +]) + Ledger = _Serializer[typings.Ledger]("Ledger", labels=[ "ID", "CURRENCY", @@ -285,4 +323,29 @@ Ledger = _Serializer[typings.Ledger]("Ledger", labels=[ "DESCRIPTION" ]) +FundingCredit = _Serializer[typings.FundingCredit]("FundingCredit", labels=[ + "ID", + "SYMBOL", + "SIDE", + "MTS_CREATE", + "MTS_UPDATE", + "AMOUNT", + "FLAGS", + "STATUS", + "RATE_TYPE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "RATE", + "PERIOD", + "MTS_OPENING", + "MTS_LAST_PAYOUT", + "NOTIFY", + "HIDDEN", + "_PLACEHOLDER", + "RENEW", + "_PLACEHOLDER", + "NO_CLOSE", + "POSITION_PAIR" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index 20b2eed..d9d37a7 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -169,6 +169,25 @@ class Order(TypedDict): ROUTING: str META: JSON +class Position(TypedDict): + SYMBOL: str + STATUS: str + AMOUNT: float + BASE_PRICE: float + MARGIN_FUNDING: float + MARGIN_FUNDING_TYPE: int + PL: float + PL_PERC: float + PRICE_LIQ: float + LEVERAGE: float + POSITION_ID: int + MTS_CREATE: int + MTS_UPDATE: int + TYPE: int + COLLATERAL: float + COLLATERAL_MIN: float + META: JSON + class FundingOffer(TypedDict): ID: int SYMBOL: str @@ -199,6 +218,18 @@ class Trade(TypedDict): FEE_CURRENCY: str CID: int +class OrderTrade(TypedDict): + ID: int + SYMBOL: str + MTS_CREATE: int + ORDER_ID: int + EXEC_AMOUNT: float + EXEC_PRICE: float + MAKER:int + FEE: float + FEE_CURRENCY: str + CID: int + class Ledger(TypedDict): ID: int CURRENCY: str @@ -207,4 +238,24 @@ class Ledger(TypedDict): BALANCE: float description: str +class FundingCredit(TypedDict): + ID: int + SYMBOL: str + SIDE: int + MTS_CREATE: int + MTS_UPDATE: int + AMOUNT: float + FLAGS: int + STATUS: str + RATE: float + PERIOD: int + MTS_OPENING: int + MTS_LAST_PAYOUT: int + NOTIFY: int + HIDDEN: int + RENEW: int + RATE_REAL: float + NO_CLOSE: int + POSITION_PAIR: str + #endregion \ No newline at end of file diff --git a/examples/rest/get_authenticated_data.py b/examples/rest/get_authenticated_data.py new file mode 100644 index 0000000..881cbf6 --- /dev/null +++ b/examples/rest/get_authenticated_data.py @@ -0,0 +1,101 @@ +# python -c "from examples.rest.get_authenticated_data import *" + +import os +import time + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +now = int(round(time.time() * 1000)) + +def log_wallets(): + wallets = bfx.rest.auth.get_wallets() + print("Wallets:") + [print(w) for w in wallets] + + +def log_orders(): + orders = bfx.rest.auth.get_orders(symbol='tBTCUSD') + print("Orders:") + [print(o) for o in orders] + + +def log_orders_history(): + orders = bfx.rest.auth.get_orders_history(symbol='tBTCUSD', start=0, end=now) + print("Orders:") + [print(o) for o in orders] + + +def log_positions(): + positions = bfx.rest.auth.get_positions() + print("Positions:") + [print(p) for p in positions] + + +def log_trades(): + trades = bfx.rest.auth.get_trades(symbol='tBTCUSD', start=0, end=now) + print("Trades:") + [print(t) for t in trades] + + +def log_order_trades(): + order_id = 82406909127 + trades = bfx.rest.auth.get_order_trades(symbol='tBTCUSD', order_id=order_id) + print("Trade orders:") + [print(t) for t in trades] + + +def log_funding_offers(): + offers = bfx.rest.auth.get_funding_offers(symbol='fUSD') + print("Offers:") + [print(o) for o in offers] + + +def log_funding_offer_history(): + offers = bfx.rest.auth.get_funding_offers_history(symbol='fUSD', start=0, end=now) + print("Offers history:") + [print(o) for o in offers] + + +def log_funding_loans(): + loans = bfx.rest.auth.get_funding_loans(symbol='fUSD') + print("Funding loans:") + [print(l) for l in loans] + + +def log_funding_loans_history(): + loans = bfx.rest.auth.get_funding_loan_history(symbol='fUSD', start=0, end=now) + print("Funding loan history:") + [print(l) for l in loans] + + +def log_funding_credits(): + credits = bfx.rest.auth.get_funding_credits(symbol='fUSD') + print("Funding credits:") + [print(c) for c in credits] + + +def log_funding_credits_history(): + credit = bfx.rest.auth.get_funding_credits_history(symbol='fUSD', start=0, end=now) + print("Funding credit history:") + [print(c) for c in credit] + + +def run(): + log_wallets() + log_orders() + log_orders_history() + log_positions() + log_trades() + log_order_trades() + log_funding_offers() + log_funding_offer_history() + log_funding_credits() + log_funding_credits_history() + +run() \ No newline at end of file From 903f68c6e3da739f7288950388947ceb2d387bb4 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 13 Jan 2023 18:15:29 +0100 Subject: [PATCH 094/265] Add support for SimpleNamespace (instead of TypedDict) in bfxapi/labeler.py and bfxapi/notifications.py. Add generics Notification type in notifications.py. Add support for new changes in bfxapi/rest/BfxRestInterface.py. --- bfxapi/labeler.py | 4 ++- bfxapi/notification.py | 26 +++++++++-------- bfxapi/rest/BfxRestInterface.py | 30 ++++++++++---------- bfxapi/rest/typings.py | 50 +++++++++++++++++---------------- 4 files changed, 59 insertions(+), 51 deletions(-) diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 4575146..8a5845e 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -2,6 +2,8 @@ from .exceptions import LabelerSerializerException from typing import Generic, TypeVar, Iterable, Optional, List, Tuple, Any, cast +from types import SimpleNamespace + T = TypeVar("T") class _Serializer(Generic[T]): @@ -19,4 +21,4 @@ class _Serializer(Generic[T]): yield label, args[index] def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: - return cast(T, dict(self._serialize(*values, skip=skip))) \ No newline at end of file + return cast(T, SimpleNamespace(**dict(self._serialize(*values, skip=skip)))) \ No newline at end of file diff --git a/bfxapi/notification.py b/bfxapi/notification.py index 90d2f12..413f1b6 100644 --- a/bfxapi/notification.py +++ b/bfxapi/notification.py @@ -1,17 +1,21 @@ -from typing import List, Dict, Union, Optional, Any, TypedDict, cast +from typing import List, Dict, Union, Optional, Any, TypedDict, Generic, TypeVar, cast + +from types import SimpleNamespace from .labeler import _Serializer -class Notification(TypedDict): +T = TypeVar("T") + +class Notification(SimpleNamespace, Generic[T]): MTS: int TYPE: str MESSAGE_ID: Optional[int] - NOTIFY_INFO: Union[Dict[str, Any], List[Dict[str, Any]]] + NOTIFY_INFO: T CODE: Optional[int] STATUS: str TEXT: str -class _Notification(_Serializer): +class _Notification(_Serializer, Generic[T]): __LABELS = [ "MTS", "TYPE", "MESSAGE_ID", "_PLACEHOLDER", "NOTIFY_INFO", "CODE", "STATUS", "TEXT" ] def __init__(self, serializer: Optional[_Serializer] = None, iterate: bool = False): @@ -19,17 +23,17 @@ class _Notification(_Serializer): self.serializer, self.iterate = serializer, iterate - def parse(self, *values: Any, skip: Optional[List[str]] = None) -> Notification: - notification = dict(self._serialize(*values)) + def parse(self, *values: Any, skip: Optional[List[str]] = None) -> Notification[T]: + notification = cast(Notification[T], SimpleNamespace(**dict(self._serialize(*values)))) if isinstance(self.serializer, _Serializer): - if self.iterate == False: - NOTIFY_INFO = notification["NOTIFY_INFO"] + NOTIFY_INFO = cast(List[Any], notification.NOTIFY_INFO) + if self.iterate == False: if len(NOTIFY_INFO) == 1 and isinstance(NOTIFY_INFO[0], list): NOTIFY_INFO = NOTIFY_INFO[0] - notification["NOTIFY_INFO"] = dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip)) - else: notification["NOTIFY_INFO"] = [ dict(self.serializer._serialize(*data, skip=skip)) for data in notification["NOTIFY_INFO"] ] + notification.NOTIFY_INFO = cast(T, SimpleNamespace(**dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip)))) + else: notification.NOTIFY_INFO = cast(T, [ SimpleNamespace(**dict(self.serializer._serialize(*data, skip=skip))) for data in NOTIFY_INFO ]) - return cast(Notification, notification) \ No newline at end of file + return notification \ No newline at end of file diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 3a02593..b967775 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -97,11 +97,11 @@ class _RestPublicEndpoints(_Requests): parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse } - return [ parsers[subdata[0][0]](*subdata) for subdata in data ] + return [ cast(Union[TradingPairTicker, FundingCurrencyTicker], parsers[subdata[0][0]](*subdata)) for subdata in data ] def get_t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]: if isinstance(pairs, str) and pairs == "ALL": - return [ cast(TradingPairTicker, subdata) for subdata in self.get_tickers([ "ALL" ]) if cast(str, subdata["SYMBOL"]).startswith("t") ] + return [ cast(TradingPairTicker, subdata) for subdata in self.get_tickers([ "ALL" ]) if cast(str, subdata.SYMBOL).startswith("t") ] data = self.get_tickers([ "t" + pair for pair in pairs ]) @@ -109,7 +109,7 @@ class _RestPublicEndpoints(_Requests): def get_f_tickers(self, currencies: Union[List[str], Literal["ALL"]]) -> List[FundingCurrencyTicker]: if isinstance(currencies, str) and currencies == "ALL": - return [ cast(FundingCurrencyTicker, subdata) for subdata in self.get_tickers([ "ALL" ]) if cast(str, subdata["SYMBOL"]).startswith("f") ] + return [ cast(FundingCurrencyTicker, subdata) for subdata in self.get_tickers([ "ALL" ]) if cast(str, subdata.SYMBOL).startswith("f") ] data = self.get_tickers([ "f" + currency for currency in currencies ]) @@ -262,7 +262,7 @@ class _RestAuthenticatedEndpoints(_Requests): price: Optional[Union[Decimal, str]] = None, lev: Optional[int] = None, price_trailing: Optional[Union[Decimal, str]] = None, price_aux_limit: Optional[Union[Decimal, str]] = None, price_oco_stop: Optional[Union[Decimal, str]] = None, gid: Optional[int] = None, cid: Optional[int] = None, - flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification: + flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification[Order]: data = { "type": type, "symbol": symbol, "amount": amount, "price": price, "lev": lev, @@ -271,12 +271,12 @@ class _RestAuthenticatedEndpoints(_Requests): "flags": flags, "tif": tif, "meta": meta } - return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/submit", data=data)) + return serializers._Notification[Order](serializer=serializers.Order).parse(*self._POST("auth/w/order/submit", data=data)) def update_order(self, id: int, amount: Optional[Union[Decimal, str]] = None, price: Optional[Union[Decimal, str]] = None, cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, str]] = None, - price_aux_limit: Optional[Union[Decimal, str]] = None, price_trailing: Optional[Union[Decimal, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification: + price_aux_limit: Optional[Union[Decimal, str]] = None, price_trailing: Optional[Union[Decimal, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification[Order]: data = { "id": id, "amount": amount, "price": price, "cid": cid, "cid_date": cid_date, "gid": gid, @@ -284,18 +284,18 @@ class _RestAuthenticatedEndpoints(_Requests): "price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif } - return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/update", data=data)) + return serializers._Notification[Order](serializer=serializers.Order).parse(*self._POST("auth/w/order/update", data=data)) - def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None) -> Notification: + def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None) -> Notification[Order]: data = { "id": id, "cid": cid, "cid_date": cid_date } - return serializers._Notification(serializer=serializers.Order).parse(*self._POST("auth/w/order/cancel", data=data)) + return serializers._Notification[Order](serializer=serializers.Order).parse(*self._POST("auth/w/order/cancel", data=data)) - def cancel_order_multi(self, ids: Optional[List[int]] = None, cids: Optional[List[Tuple[int, str]]] = None, gids: Optional[List[int]] = None, all: bool = False) -> Notification: + def cancel_order_multi(self, ids: Optional[List[int]] = None, cids: Optional[List[Tuple[int, str]]] = None, gids: Optional[List[int]] = None, all: bool = False) -> Notification[List[Order]]: data = { "ids": ids, "cids": cids, @@ -304,7 +304,7 @@ class _RestAuthenticatedEndpoints(_Requests): "all": int(all) } - return serializers._Notification(serializer=serializers.Order, iterate=True).parse(*self._POST("auth/w/order/cancel/multi", data=data)) + return serializers._Notification[List[Order]](serializer=serializers.Order, iterate=True).parse(*self._POST("auth/w/order/cancel/multi", data=data)) def get_orders_history(self, symbol: Optional[str] = None, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Order]: if symbol == None: @@ -354,17 +354,17 @@ class _RestAuthenticatedEndpoints(_Requests): def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, str], rate: Union[Decimal, str], period: int, - flags: Optional[int] = 0) -> Notification: + flags: Optional[int] = 0) -> Notification[FundingOffer]: data = { "type": type, "symbol": symbol, "amount": amount, "rate": rate, "period": period, "flags": flags } - return serializers._Notification(serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/submit", data=data)) + return serializers._Notification[FundingOffer](serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/submit", data=data)) - def cancel_funding_offer(self, id: int) -> Notification: - return serializers._Notification(serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/cancel", data={ "id": id })) + def cancel_funding_offer(self, id: int) -> Notification[FundingOffer]: + return serializers._Notification[FundingOffer](serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/cancel", data={ "id": id })) def get_funding_offers_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingOffer]: if symbol == None: diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index d9d37a7..1685b04 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -1,15 +1,17 @@ from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any +from types import SimpleNamespace + from .. notification import Notification JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] #region Type hinting for Rest Public Endpoints -class PlatformStatus(TypedDict): +class PlatformStatus(SimpleNamespace): OPERATIVE: int -class TradingPairTicker(TypedDict): +class TradingPairTicker(SimpleNamespace): SYMBOL: Optional[str] BID: float BID_SIZE: float @@ -22,7 +24,7 @@ class TradingPairTicker(TypedDict): HIGH: float LOW: float -class FundingCurrencyTicker(TypedDict): +class FundingCurrencyTicker(SimpleNamespace): SYMBOL: Optional[str] FRR: float BID: float @@ -39,52 +41,52 @@ class FundingCurrencyTicker(TypedDict): LOW: float FRR_AMOUNT_AVAILABLE: float -class TickersHistory(TypedDict): +class TickersHistory(SimpleNamespace): SYMBOL: str BID: float ASK: float MTS: int -class TradingPairTrade(TypedDict): +class TradingPairTrade(SimpleNamespace): ID: int MTS: int AMOUNT: float PRICE: float -class FundingCurrencyTrade(TypedDict): +class FundingCurrencyTrade(SimpleNamespace): ID: int MTS: int AMOUNT: float RATE: float PERIOD: int -class TradingPairBook(TypedDict): +class TradingPairBook(SimpleNamespace): PRICE: float COUNT: int AMOUNT: float -class FundingCurrencyBook(TypedDict): +class FundingCurrencyBook(SimpleNamespace): RATE: float PERIOD: int COUNT: int AMOUNT: float -class TradingPairRawBook(TypedDict): +class TradingPairRawBook(SimpleNamespace): ORDER_ID: int PRICE: float AMOUNT: float -class FundingCurrencyRawBook(TypedDict): +class FundingCurrencyRawBook(SimpleNamespace): OFFER_ID: int PERIOD: int RATE: float AMOUNT: float -class Statistic(TypedDict): +class Statistic(SimpleNamespace): MTS: int VALUE: float -class Candle(TypedDict): +class Candle(SimpleNamespace): MTS: int OPEN: float CLOSE: float @@ -92,7 +94,7 @@ class Candle(TypedDict): LOW: float VOLUME: float -class DerivativesStatus(TypedDict): +class DerivativesStatus(SimpleNamespace): KEY: Optional[str] MTS: int DERIV_PRICE: float @@ -107,7 +109,7 @@ class DerivativesStatus(TypedDict): CLAMP_MIN: float CLAMP_MAX: float -class Liquidation(TypedDict): +class Liquidation(SimpleNamespace): POS_ID: int MTS: int SYMBOL: str @@ -117,14 +119,14 @@ class Liquidation(TypedDict): IS_MARKET_SOLD: int PRICE_ACQUIRED: float -class Leaderboard(TypedDict): +class Leaderboard(SimpleNamespace): MTS: int USERNAME: str RANKING: int VALUE: float TWITTER_HANDLE: Optional[str] -class FundingStatistic(TypedDict): +class FundingStatistic(SimpleNamespace): TIMESTAMP: int FRR: float AVG_PERIOD: float @@ -136,7 +138,7 @@ class FundingStatistic(TypedDict): #region Type hinting for Rest Authenticated Endpoints -class Wallet(TypedDict): +class Wallet(SimpleNamespace): WALLET_TYPE: str CURRENCY: str BALANCE: float @@ -145,7 +147,7 @@ class Wallet(TypedDict): LAST_CHANGE: str TRADE_DETAILS: JSON -class Order(TypedDict): +class Order(SimpleNamespace): ID: int GID: int CID: int @@ -169,7 +171,7 @@ class Order(TypedDict): ROUTING: str META: JSON -class Position(TypedDict): +class Position(SimpleNamespace): SYMBOL: str STATUS: str AMOUNT: float @@ -188,7 +190,7 @@ class Position(TypedDict): COLLATERAL_MIN: float META: JSON -class FundingOffer(TypedDict): +class FundingOffer(SimpleNamespace): ID: int SYMBOL: str MTS_CREATE: int @@ -204,7 +206,7 @@ class FundingOffer(TypedDict): HIDDEN: int RENEW: bool -class Trade(TypedDict): +class Trade(SimpleNamespace): ID: int SYMBOL: str MTS_CREATE: int @@ -218,7 +220,7 @@ class Trade(TypedDict): FEE_CURRENCY: str CID: int -class OrderTrade(TypedDict): +class OrderTrade(SimpleNamespace): ID: int SYMBOL: str MTS_CREATE: int @@ -230,7 +232,7 @@ class OrderTrade(TypedDict): FEE_CURRENCY: str CID: int -class Ledger(TypedDict): +class Ledger(SimpleNamespace): ID: int CURRENCY: str MTS: int @@ -238,7 +240,7 @@ class Ledger(TypedDict): BALANCE: float description: str -class FundingCredit(TypedDict): +class FundingCredit(SimpleNamespace): ID: int SYMBOL: str SIDE: int From e9ef39c1d68bca20a9fb46bf4db457d3027bb600 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Sun, 15 Jan 2023 23:19:09 +0100 Subject: [PATCH 095/265] add rest examples --- bfxapi/rest/BfxRestInterface.py | 6 ++-- examples/rest/get_liquidations.py | 14 +++++++++ examples/rest/get_public_data.py | 52 +++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 examples/rest/get_liquidations.py create mode 100644 examples/rest/get_public_data.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 3a02593..3b6db61 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -190,10 +190,10 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"candles/{resource}/last", params=params) return serializers.Candle.parse(*data) - def get_derivatives_status(self, type: str, keys: List[str]) -> List[DerivativesStatus]: - params = { "keys": ",".join(keys) } + def get_derivatives_status(self, symbols: Union[List[str], Literal["ALL"]]) -> List[DerivativesStatus]: + params = { "keys": ",".join(symbols) if type(symbols) == List else "ALL" } - data = self._GET(f"status/{type}", params=params) + data = self._GET(f"status/deriv", params=params) return [ serializers.DerivativesStatus.parse(*subdata) for subdata in data ] diff --git a/examples/rest/get_liquidations.py b/examples/rest/get_liquidations.py new file mode 100644 index 0000000..34ed47d --- /dev/null +++ b/examples/rest/get_liquidations.py @@ -0,0 +1,14 @@ +# python -c "from examples.rest.get_liquidations import *" + +import time + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST +) + +now = int(round(time.time() * 1000)) + +liquidations = bfx.rest.public.get_liquidations(start=0, end=now) +print(f"Liquidations: {liquidations}") \ No newline at end of file diff --git a/examples/rest/get_public_data.py b/examples/rest/get_public_data.py new file mode 100644 index 0000000..ff86c14 --- /dev/null +++ b/examples/rest/get_public_data.py @@ -0,0 +1,52 @@ +# python -c "from examples.rest.get_public_data import *" + +import time + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST +) + +now = int(round(time.time() * 1000)) + + +def log_historical_candles(): + candles = bfx.rest.public.get_candles_hist(start=0, end=now, resource='trade:1m:tBTCUSD') + print("Candles:") + [print(c) for c in candles] + + +def log_historical_trades(): + trades = bfx.rest.public.get_t_trades(pair='tBTCUSD', start=0, end=now) + print("Trades:") + [print(t) for t in trades] + + +def log_books(): + orders = bfx.rest.public.get_t_book(pair='BTCUSD', precision='P0') + print("Order book:") + [print(o) for o in orders] + + +def log_tickers(): + tickers = bfx.rest.public.get_t_tickers(pairs=['BTCUSD']) + print("Tickers:") + print(tickers) + + +def log_derivative_status(): + status = bfx.rest.public.get_derivatives_status('ALL') + print("Deriv status:") + print(status) + + +def run(): + log_historical_candles() + log_historical_trades() + log_books() + log_tickers() + log_derivative_status() + + +run() \ No newline at end of file From 0bb9f65a190656d1707f9a0cedf6ee98f5a2c401 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 16 Jan 2023 16:30:06 +0100 Subject: [PATCH 096/265] Replace SimpleNamespaces with dataclasses. Add base class _Typing in labeler.py to convert dictionaries to dataclasses. Remove SimpleNamespace references. --- bfxapi/labeler.py | 9 +++-- bfxapi/notification.py | 13 +++---- bfxapi/rest/typings.py | 80 ++++++++++++++++++++++++++++-------------- 3 files changed, 66 insertions(+), 36 deletions(-) diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 8a5845e..160e054 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -2,10 +2,13 @@ from .exceptions import LabelerSerializerException from typing import Generic, TypeVar, Iterable, Optional, List, Tuple, Any, cast -from types import SimpleNamespace - T = TypeVar("T") +class _Typing(object): + def __init__(self, **kwargs): + for key, value in kwargs.items(): + self.__setattr__(key,value) + class _Serializer(Generic[T]): def __init__(self, name: str, labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]): self.name, self.__labels, self.__IGNORE = name, labels, IGNORE @@ -21,4 +24,4 @@ class _Serializer(Generic[T]): yield label, args[index] def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: - return cast(T, SimpleNamespace(**dict(self._serialize(*values, skip=skip)))) \ No newline at end of file + return cast(T, _Typing(**dict(self._serialize(*values, skip=skip)))) \ No newline at end of file diff --git a/bfxapi/notification.py b/bfxapi/notification.py index 413f1b6..b8cdb37 100644 --- a/bfxapi/notification.py +++ b/bfxapi/notification.py @@ -1,12 +1,13 @@ from typing import List, Dict, Union, Optional, Any, TypedDict, Generic, TypeVar, cast -from types import SimpleNamespace +from dataclasses import dataclass -from .labeler import _Serializer +from .labeler import _Typing, _Serializer T = TypeVar("T") -class Notification(SimpleNamespace, Generic[T]): +@dataclass +class Notification(_Typing, Generic[T]): MTS: int TYPE: str MESSAGE_ID: Optional[int] @@ -24,7 +25,7 @@ class _Notification(_Serializer, Generic[T]): self.serializer, self.iterate = serializer, iterate def parse(self, *values: Any, skip: Optional[List[str]] = None) -> Notification[T]: - notification = cast(Notification[T], SimpleNamespace(**dict(self._serialize(*values)))) + notification = cast(Notification[T], _Typing(**dict(self._serialize(*values)))) if isinstance(self.serializer, _Serializer): NOTIFY_INFO = cast(List[Any], notification.NOTIFY_INFO) @@ -33,7 +34,7 @@ class _Notification(_Serializer, Generic[T]): if len(NOTIFY_INFO) == 1 and isinstance(NOTIFY_INFO[0], list): NOTIFY_INFO = NOTIFY_INFO[0] - notification.NOTIFY_INFO = cast(T, SimpleNamespace(**dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip)))) - else: notification.NOTIFY_INFO = cast(T, [ SimpleNamespace(**dict(self.serializer._serialize(*data, skip=skip))) for data in NOTIFY_INFO ]) + notification.NOTIFY_INFO = cast(T, _Typing(**dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip)))) + else: notification.NOTIFY_INFO = cast(T, [ _Typing(**dict(self.serializer._serialize(*data, skip=skip))) for data in NOTIFY_INFO ]) return notification \ No newline at end of file diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/typings.py index 1685b04..ef397a9 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/typings.py @@ -1,6 +1,8 @@ from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any -from types import SimpleNamespace +from dataclasses import dataclass + +from .. labeler import _Typing from .. notification import Notification @@ -8,10 +10,12 @@ JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] #region Type hinting for Rest Public Endpoints -class PlatformStatus(SimpleNamespace): +@dataclass +class PlatformStatus(_Typing): OPERATIVE: int -class TradingPairTicker(SimpleNamespace): +@dataclass +class TradingPairTicker(_Typing): SYMBOL: Optional[str] BID: float BID_SIZE: float @@ -24,7 +28,8 @@ class TradingPairTicker(SimpleNamespace): HIGH: float LOW: float -class FundingCurrencyTicker(SimpleNamespace): +@dataclass +class FundingCurrencyTicker(_Typing): SYMBOL: Optional[str] FRR: float BID: float @@ -41,52 +46,61 @@ class FundingCurrencyTicker(SimpleNamespace): LOW: float FRR_AMOUNT_AVAILABLE: float -class TickersHistory(SimpleNamespace): +@dataclass +class TickersHistory(_Typing): SYMBOL: str BID: float ASK: float MTS: int -class TradingPairTrade(SimpleNamespace): +@dataclass +class TradingPairTrade(_Typing): ID: int MTS: int AMOUNT: float PRICE: float -class FundingCurrencyTrade(SimpleNamespace): +@dataclass +class FundingCurrencyTrade(_Typing): ID: int MTS: int AMOUNT: float RATE: float PERIOD: int -class TradingPairBook(SimpleNamespace): +@dataclass +class TradingPairBook(_Typing): PRICE: float COUNT: int AMOUNT: float -class FundingCurrencyBook(SimpleNamespace): +@dataclass +class FundingCurrencyBook(_Typing): RATE: float PERIOD: int COUNT: int AMOUNT: float - -class TradingPairRawBook(SimpleNamespace): + +@dataclass +class TradingPairRawBook(_Typing): ORDER_ID: int PRICE: float AMOUNT: float - -class FundingCurrencyRawBook(SimpleNamespace): + +@dataclass +class FundingCurrencyRawBook(_Typing): OFFER_ID: int PERIOD: int RATE: float AMOUNT: float -class Statistic(SimpleNamespace): +@dataclass +class Statistic(_Typing): MTS: int VALUE: float -class Candle(SimpleNamespace): +@dataclass +class Candle(_Typing): MTS: int OPEN: float CLOSE: float @@ -94,7 +108,8 @@ class Candle(SimpleNamespace): LOW: float VOLUME: float -class DerivativesStatus(SimpleNamespace): +@dataclass +class DerivativesStatus(_Typing): KEY: Optional[str] MTS: int DERIV_PRICE: float @@ -109,7 +124,8 @@ class DerivativesStatus(SimpleNamespace): CLAMP_MIN: float CLAMP_MAX: float -class Liquidation(SimpleNamespace): +@dataclass +class Liquidation(_Typing): POS_ID: int MTS: int SYMBOL: str @@ -119,14 +135,16 @@ class Liquidation(SimpleNamespace): IS_MARKET_SOLD: int PRICE_ACQUIRED: float -class Leaderboard(SimpleNamespace): +@dataclass +class Leaderboard(_Typing): MTS: int USERNAME: str RANKING: int VALUE: float TWITTER_HANDLE: Optional[str] -class FundingStatistic(SimpleNamespace): +@dataclass +class FundingStatistic(_Typing): TIMESTAMP: int FRR: float AVG_PERIOD: float @@ -138,7 +156,8 @@ class FundingStatistic(SimpleNamespace): #region Type hinting for Rest Authenticated Endpoints -class Wallet(SimpleNamespace): +@dataclass +class Wallet(_Typing): WALLET_TYPE: str CURRENCY: str BALANCE: float @@ -147,7 +166,8 @@ class Wallet(SimpleNamespace): LAST_CHANGE: str TRADE_DETAILS: JSON -class Order(SimpleNamespace): +@dataclass +class Order(_Typing): ID: int GID: int CID: int @@ -171,7 +191,8 @@ class Order(SimpleNamespace): ROUTING: str META: JSON -class Position(SimpleNamespace): +@dataclass +class Position(_Typing): SYMBOL: str STATUS: str AMOUNT: float @@ -190,7 +211,8 @@ class Position(SimpleNamespace): COLLATERAL_MIN: float META: JSON -class FundingOffer(SimpleNamespace): +@dataclass +class FundingOffer(_Typing): ID: int SYMBOL: str MTS_CREATE: int @@ -206,7 +228,8 @@ class FundingOffer(SimpleNamespace): HIDDEN: int RENEW: bool -class Trade(SimpleNamespace): +@dataclass +class Trade(_Typing): ID: int SYMBOL: str MTS_CREATE: int @@ -220,7 +243,8 @@ class Trade(SimpleNamespace): FEE_CURRENCY: str CID: int -class OrderTrade(SimpleNamespace): +@dataclass +class OrderTrade(_Typing): ID: int SYMBOL: str MTS_CREATE: int @@ -232,7 +256,8 @@ class OrderTrade(SimpleNamespace): FEE_CURRENCY: str CID: int -class Ledger(SimpleNamespace): +@dataclass +class Ledger(_Typing): ID: int CURRENCY: str MTS: int @@ -240,7 +265,8 @@ class Ledger(SimpleNamespace): BALANCE: float description: str -class FundingCredit(SimpleNamespace): +@dataclass +class FundingCredit(_Typing): ID: int SYMBOL: str SIDE: int From 1613a56d813e1195e1c01fb5b3e3299a33224e50 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 16 Jan 2023 16:40:14 +0100 Subject: [PATCH 097/265] Rename bfxapi/rest/typings.py to bfxapi/rest/types.py. --- bfxapi/labeler.py | 4 +-- bfxapi/notification.py | 10 +++--- bfxapi/rest/BfxRestInterface.py | 2 +- bfxapi/rest/serializers.py | 50 ++++++++++++++-------------- bfxapi/rest/{typings.py => types.py} | 50 ++++++++++++++-------------- 5 files changed, 58 insertions(+), 58 deletions(-) rename bfxapi/rest/{typings.py => types.py} (85%) diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 160e054..9714b5c 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -4,7 +4,7 @@ from typing import Generic, TypeVar, Iterable, Optional, List, Tuple, Any, cast T = TypeVar("T") -class _Typing(object): +class _Type(object): def __init__(self, **kwargs): for key, value in kwargs.items(): self.__setattr__(key,value) @@ -24,4 +24,4 @@ class _Serializer(Generic[T]): yield label, args[index] def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: - return cast(T, _Typing(**dict(self._serialize(*values, skip=skip)))) \ No newline at end of file + return cast(T, _Type(**dict(self._serialize(*values, skip=skip)))) \ No newline at end of file diff --git a/bfxapi/notification.py b/bfxapi/notification.py index b8cdb37..e66ba60 100644 --- a/bfxapi/notification.py +++ b/bfxapi/notification.py @@ -2,12 +2,12 @@ from typing import List, Dict, Union, Optional, Any, TypedDict, Generic, TypeVar from dataclasses import dataclass -from .labeler import _Typing, _Serializer +from .labeler import _Type, _Serializer T = TypeVar("T") @dataclass -class Notification(_Typing, Generic[T]): +class Notification(_Type, Generic[T]): MTS: int TYPE: str MESSAGE_ID: Optional[int] @@ -25,7 +25,7 @@ class _Notification(_Serializer, Generic[T]): self.serializer, self.iterate = serializer, iterate def parse(self, *values: Any, skip: Optional[List[str]] = None) -> Notification[T]: - notification = cast(Notification[T], _Typing(**dict(self._serialize(*values)))) + notification = cast(Notification[T], _Type(**dict(self._serialize(*values)))) if isinstance(self.serializer, _Serializer): NOTIFY_INFO = cast(List[Any], notification.NOTIFY_INFO) @@ -34,7 +34,7 @@ class _Notification(_Serializer, Generic[T]): if len(NOTIFY_INFO) == 1 and isinstance(NOTIFY_INFO[0], list): NOTIFY_INFO = NOTIFY_INFO[0] - notification.NOTIFY_INFO = cast(T, _Typing(**dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip)))) - else: notification.NOTIFY_INFO = cast(T, [ _Typing(**dict(self.serializer._serialize(*data, skip=skip))) for data in NOTIFY_INFO ]) + notification.NOTIFY_INFO = cast(T, _Type(**dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip)))) + else: notification.NOTIFY_INFO = cast(T, [ _Type(**dict(self.serializer._serialize(*data, skip=skip))) for data in NOTIFY_INFO ]) return notification \ No newline at end of file diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index b967775..33f5657 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -8,7 +8,7 @@ from typing import List, Union, Literal, Optional, Any, cast from . import serializers -from .typings import * +from .types import * from .enums import Config, Sort, OrderType, FundingOfferType, Error from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 9e2612a..4196b52 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -1,4 +1,4 @@ -from . import typings +from . import types from .. labeler import _Serializer @@ -6,11 +6,11 @@ from .. notification import _Notification #region Serializers definition for Rest Public Endpoints -PlatformStatus = _Serializer[typings.PlatformStatus]("PlatformStatus", labels=[ +PlatformStatus = _Serializer[types.PlatformStatus]("PlatformStatus", labels=[ "OPERATIVE" ]) -TradingPairTicker = _Serializer[typings.TradingPairTicker]("TradingPairTicker", labels=[ +TradingPairTicker = _Serializer[types.TradingPairTicker]("TradingPairTicker", labels=[ "SYMBOL", "BID", "BID_SIZE", @@ -24,7 +24,7 @@ TradingPairTicker = _Serializer[typings.TradingPairTicker]("TradingPairTicker", "LOW" ]) -FundingCurrencyTicker = _Serializer[typings.FundingCurrencyTicker]("FundingCurrencyTicker", labels=[ +FundingCurrencyTicker = _Serializer[types.FundingCurrencyTicker]("FundingCurrencyTicker", labels=[ "SYMBOL", "FRR", "BID", @@ -44,7 +44,7 @@ FundingCurrencyTicker = _Serializer[typings.FundingCurrencyTicker]("FundingCurre "FRR_AMOUNT_AVAILABLE" ]) -TickersHistory = _Serializer[typings.TickersHistory]("TickersHistory", labels=[ +TickersHistory = _Serializer[types.TickersHistory]("TickersHistory", labels=[ "SYMBOL", "BID", "_PLACEHOLDER", @@ -60,14 +60,14 @@ TickersHistory = _Serializer[typings.TickersHistory]("TickersHistory", labels=[ "MTS" ]) -TradingPairTrade = _Serializer[typings.TradingPairTrade]("TradingPairTrade", labels=[ +TradingPairTrade = _Serializer[types.TradingPairTrade]("TradingPairTrade", labels=[ "ID", "MTS", "AMOUNT", "PRICE" ]) -FundingCurrencyTrade = _Serializer[typings.FundingCurrencyTrade]("FundingCurrencyTrade", labels=[ +FundingCurrencyTrade = _Serializer[types.FundingCurrencyTrade]("FundingCurrencyTrade", labels=[ "ID", "MTS", "AMOUNT", @@ -75,38 +75,38 @@ FundingCurrencyTrade = _Serializer[typings.FundingCurrencyTrade]("FundingCurrenc "PERIOD" ]) -TradingPairBook = _Serializer[typings.TradingPairBook]("TradingPairBook", labels=[ +TradingPairBook = _Serializer[types.TradingPairBook]("TradingPairBook", labels=[ "PRICE", "COUNT", "AMOUNT" ]) -FundingCurrencyBook = _Serializer[typings.FundingCurrencyBook]("FundingCurrencyBook", labels=[ +FundingCurrencyBook = _Serializer[types.FundingCurrencyBook]("FundingCurrencyBook", labels=[ "RATE", "PERIOD", "COUNT", "AMOUNT" ]) -TradingPairRawBook = _Serializer[typings.TradingPairRawBook]("TradingPairRawBook", labels=[ +TradingPairRawBook = _Serializer[types.TradingPairRawBook]("TradingPairRawBook", labels=[ "ORDER_ID", "PRICE", "AMOUNT" ]) -FundingCurrencyRawBook = _Serializer[typings.FundingCurrencyRawBook]("FundingCurrencyRawBook", labels=[ +FundingCurrencyRawBook = _Serializer[types.FundingCurrencyRawBook]("FundingCurrencyRawBook", labels=[ "OFFER_ID", "PERIOD", "RATE", "AMOUNT" ]) -Statistic = _Serializer[typings.Statistic]("Statistic", labels=[ +Statistic = _Serializer[types.Statistic]("Statistic", labels=[ "MTS", "VALUE" ]) -Candle = _Serializer[typings.Candle]("Candle", labels=[ +Candle = _Serializer[types.Candle]("Candle", labels=[ "MTS", "OPEN", "CLOSE", @@ -115,7 +115,7 @@ Candle = _Serializer[typings.Candle]("Candle", labels=[ "VOLUME" ]) -DerivativesStatus = _Serializer[typings.DerivativesStatus]("DerivativesStatus", labels=[ +DerivativesStatus = _Serializer[types.DerivativesStatus]("DerivativesStatus", labels=[ "KEY", "MTS", "_PLACEHOLDER", @@ -142,7 +142,7 @@ DerivativesStatus = _Serializer[typings.DerivativesStatus]("DerivativesStatus", "CLAMP_MAX" ]) -Liquidation = _Serializer[typings.Liquidation]("Liquidation", labels=[ +Liquidation = _Serializer[types.Liquidation]("Liquidation", labels=[ "_PLACEHOLDER", "POS_ID", "MTS", @@ -157,7 +157,7 @@ Liquidation = _Serializer[typings.Liquidation]("Liquidation", labels=[ "PRICE_ACQUIRED" ]) -Leaderboard = _Serializer[typings.Leaderboard]("Leaderboard", labels=[ +Leaderboard = _Serializer[types.Leaderboard]("Leaderboard", labels=[ "MTS", "_PLACEHOLDER", "USERNAME", @@ -170,7 +170,7 @@ Leaderboard = _Serializer[typings.Leaderboard]("Leaderboard", labels=[ "TWITTER_HANDLE" ]) -FundingStatistic = _Serializer[typings.FundingStatistic]("FundingStatistic", labels=[ +FundingStatistic = _Serializer[types.FundingStatistic]("FundingStatistic", labels=[ "TIMESTAMP", "_PLACEHOLDER", "_PLACEHOLDER", @@ -189,7 +189,7 @@ FundingStatistic = _Serializer[typings.FundingStatistic]("FundingStatistic", lab #region Serializers definition for Rest Authenticated Endpoints -Wallet = _Serializer[typings.Wallet]("Wallet", labels=[ +Wallet = _Serializer[types.Wallet]("Wallet", labels=[ "WALLET_TYPE", "CURRENCY", "BALANCE", @@ -199,7 +199,7 @@ Wallet = _Serializer[typings.Wallet]("Wallet", labels=[ "TRADE_DETAILS" ]) -Order = _Serializer[typings.Order]("Order", labels=[ +Order = _Serializer[types.Order]("Order", labels=[ "ID", "GID", "CID", @@ -234,7 +234,7 @@ Order = _Serializer[typings.Order]("Order", labels=[ "META" ]) -Position = _Serializer[typings.Position]("Position", labels=[ +Position = _Serializer[types.Position]("Position", labels=[ "SYMBOL", "STATUS", "AMOUNT", @@ -257,7 +257,7 @@ Position = _Serializer[typings.Position]("Position", labels=[ "META" ]) -FundingOffer = _Serializer[typings.FundingOffer]("FundingOffer", labels=[ +FundingOffer = _Serializer[types.FundingOffer]("FundingOffer", labels=[ "ID", "SYMBOL", "MTS_CREATED", @@ -281,7 +281,7 @@ FundingOffer = _Serializer[typings.FundingOffer]("FundingOffer", labels=[ "_PLACEHOLDER" ]) -Trade = _Serializer[typings.Trade]("Trade", labels=[ +Trade = _Serializer[types.Trade]("Trade", labels=[ "ID", "PAIR", "MTS_CREATE", @@ -296,7 +296,7 @@ Trade = _Serializer[typings.Trade]("Trade", labels=[ "CID" ]) -OrderTrade = _Serializer[typings.OrderTrade]("OrderTrade", labels=[ +OrderTrade = _Serializer[types.OrderTrade]("OrderTrade", labels=[ "ID", "PAIR", "MTS_CREATE", @@ -311,7 +311,7 @@ OrderTrade = _Serializer[typings.OrderTrade]("OrderTrade", labels=[ "CID" ]) -Ledger = _Serializer[typings.Ledger]("Ledger", labels=[ +Ledger = _Serializer[types.Ledger]("Ledger", labels=[ "ID", "CURRENCY", "_PLACEHOLDER", @@ -323,7 +323,7 @@ Ledger = _Serializer[typings.Ledger]("Ledger", labels=[ "DESCRIPTION" ]) -FundingCredit = _Serializer[typings.FundingCredit]("FundingCredit", labels=[ +FundingCredit = _Serializer[types.FundingCredit]("FundingCredit", labels=[ "ID", "SYMBOL", "SIDE", diff --git a/bfxapi/rest/typings.py b/bfxapi/rest/types.py similarity index 85% rename from bfxapi/rest/typings.py rename to bfxapi/rest/types.py index ef397a9..a27a404 100644 --- a/bfxapi/rest/typings.py +++ b/bfxapi/rest/types.py @@ -2,7 +2,7 @@ from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any from dataclasses import dataclass -from .. labeler import _Typing +from .. labeler import _Type from .. notification import Notification @@ -11,11 +11,11 @@ JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] #region Type hinting for Rest Public Endpoints @dataclass -class PlatformStatus(_Typing): +class PlatformStatus(_Type): OPERATIVE: int @dataclass -class TradingPairTicker(_Typing): +class TradingPairTicker(_Type): SYMBOL: Optional[str] BID: float BID_SIZE: float @@ -29,7 +29,7 @@ class TradingPairTicker(_Typing): LOW: float @dataclass -class FundingCurrencyTicker(_Typing): +class FundingCurrencyTicker(_Type): SYMBOL: Optional[str] FRR: float BID: float @@ -47,21 +47,21 @@ class FundingCurrencyTicker(_Typing): FRR_AMOUNT_AVAILABLE: float @dataclass -class TickersHistory(_Typing): +class TickersHistory(_Type): SYMBOL: str BID: float ASK: float MTS: int @dataclass -class TradingPairTrade(_Typing): +class TradingPairTrade(_Type): ID: int MTS: int AMOUNT: float PRICE: float @dataclass -class FundingCurrencyTrade(_Typing): +class FundingCurrencyTrade(_Type): ID: int MTS: int AMOUNT: float @@ -69,38 +69,38 @@ class FundingCurrencyTrade(_Typing): PERIOD: int @dataclass -class TradingPairBook(_Typing): +class TradingPairBook(_Type): PRICE: float COUNT: int AMOUNT: float @dataclass -class FundingCurrencyBook(_Typing): +class FundingCurrencyBook(_Type): RATE: float PERIOD: int COUNT: int AMOUNT: float @dataclass -class TradingPairRawBook(_Typing): +class TradingPairRawBook(_Type): ORDER_ID: int PRICE: float AMOUNT: float @dataclass -class FundingCurrencyRawBook(_Typing): +class FundingCurrencyRawBook(_Type): OFFER_ID: int PERIOD: int RATE: float AMOUNT: float @dataclass -class Statistic(_Typing): +class Statistic(_Type): MTS: int VALUE: float @dataclass -class Candle(_Typing): +class Candle(_Type): MTS: int OPEN: float CLOSE: float @@ -109,7 +109,7 @@ class Candle(_Typing): VOLUME: float @dataclass -class DerivativesStatus(_Typing): +class DerivativesStatus(_Type): KEY: Optional[str] MTS: int DERIV_PRICE: float @@ -125,7 +125,7 @@ class DerivativesStatus(_Typing): CLAMP_MAX: float @dataclass -class Liquidation(_Typing): +class Liquidation(_Type): POS_ID: int MTS: int SYMBOL: str @@ -136,7 +136,7 @@ class Liquidation(_Typing): PRICE_ACQUIRED: float @dataclass -class Leaderboard(_Typing): +class Leaderboard(_Type): MTS: int USERNAME: str RANKING: int @@ -144,7 +144,7 @@ class Leaderboard(_Typing): TWITTER_HANDLE: Optional[str] @dataclass -class FundingStatistic(_Typing): +class FundingStatistic(_Type): TIMESTAMP: int FRR: float AVG_PERIOD: float @@ -157,7 +157,7 @@ class FundingStatistic(_Typing): #region Type hinting for Rest Authenticated Endpoints @dataclass -class Wallet(_Typing): +class Wallet(_Type): WALLET_TYPE: str CURRENCY: str BALANCE: float @@ -167,7 +167,7 @@ class Wallet(_Typing): TRADE_DETAILS: JSON @dataclass -class Order(_Typing): +class Order(_Type): ID: int GID: int CID: int @@ -192,7 +192,7 @@ class Order(_Typing): META: JSON @dataclass -class Position(_Typing): +class Position(_Type): SYMBOL: str STATUS: str AMOUNT: float @@ -212,7 +212,7 @@ class Position(_Typing): META: JSON @dataclass -class FundingOffer(_Typing): +class FundingOffer(_Type): ID: int SYMBOL: str MTS_CREATE: int @@ -229,7 +229,7 @@ class FundingOffer(_Typing): RENEW: bool @dataclass -class Trade(_Typing): +class Trade(_Type): ID: int SYMBOL: str MTS_CREATE: int @@ -244,7 +244,7 @@ class Trade(_Typing): CID: int @dataclass -class OrderTrade(_Typing): +class OrderTrade(_Type): ID: int SYMBOL: str MTS_CREATE: int @@ -257,7 +257,7 @@ class OrderTrade(_Typing): CID: int @dataclass -class Ledger(_Typing): +class Ledger(_Type): ID: int CURRENCY: str MTS: int @@ -266,7 +266,7 @@ class Ledger(_Typing): description: str @dataclass -class FundingCredit(_Typing): +class FundingCredit(_Type): ID: int SYMBOL: str SIDE: int From e185da4cc998fae0ab504bb5e445cb0a075983aa Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 16 Jan 2023 17:07:16 +0100 Subject: [PATCH 098/265] Rename bfxapi/websocket/typings.py to types.py. Replace TypedDicts with dataclasses (with _Type as base class). Update demos in examples/websocket to use new implementation. --- bfxapi/websocket/_BfxWebsocketInputs.py | 2 +- bfxapi/websocket/handlers.py | 14 +++-- bfxapi/websocket/serializers.py | 40 ++++++------- bfxapi/websocket/{typings.py => types.py} | 70 +++++++++++++++-------- 4 files changed, 77 insertions(+), 49 deletions(-) rename bfxapi/websocket/{typings.py => types.py} (83%) diff --git a/bfxapi/websocket/_BfxWebsocketInputs.py b/bfxapi/websocket/_BfxWebsocketInputs.py index fda6e19..85e29b0 100644 --- a/bfxapi/websocket/_BfxWebsocketInputs.py +++ b/bfxapi/websocket/_BfxWebsocketInputs.py @@ -2,7 +2,7 @@ from decimal import Decimal from datetime import datetime from typing import Union, Optional, List, Tuple -from .typings import JSON +from .types import JSON from .enums import OrderType, FundingOfferType def _strip(dictionary): diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index 83c1408..33c2654 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -1,3 +1,7 @@ +from typing import List + +from .types import * + from . import serializers from .enums import Channels from .exceptions import BfxWebsocketException @@ -174,13 +178,15 @@ class AuthenticatedChannelsHandler(object): 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": - return self.event_emitter.emit(f"{stream[1]}-notification", serializers._Notification(serializer=serializers.Order).parse(*stream)) + type, serializer = f"{stream[1]}-notification", serializers._Notification[Order](serializer=serializers.Order) if stream[1] == "oc_multi-req": - return self.event_emitter.emit(f"{stream[1]}-notification", serializers._Notification(serializer=serializers.Order, iterate=True).parse(*stream)) + 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": - return self.event_emitter.emit(f"{stream[1]}-notification", serializers._Notification(serializer=serializers.FundingOffer).parse(*stream)) + type, serializer = f"{stream[1]}-notification", serializers._Notification[FundingOffer](serializer=serializers.FundingOffer) - return self.event_emitter.emit("notification", serializers._Notification(serializer=None).parse(*stream)) \ No newline at end of file + return self.event_emitter.emit(type, serializer.parse(*stream)) \ No newline at end of file diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index a9dd805..6bd56c1 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -1,4 +1,4 @@ -from . import typings +from . import types from .. labeler import _Serializer @@ -6,7 +6,7 @@ from .. notification import _Notification #region Serializers definition for Websocket Public Channels -TradingPairTicker = _Serializer[typings.TradingPairTicker]("TradingPairTicker", labels=[ +TradingPairTicker = _Serializer[types.TradingPairTicker]("TradingPairTicker", labels=[ "BID", "BID_SIZE", "ASK", @@ -19,7 +19,7 @@ TradingPairTicker = _Serializer[typings.TradingPairTicker]("TradingPairTicker", "LOW" ]) -FundingCurrencyTicker = _Serializer[typings.FundingCurrencyTicker]("FundingCurrencyTicker", labels=[ +FundingCurrencyTicker = _Serializer[types.FundingCurrencyTicker]("FundingCurrencyTicker", labels=[ "FRR", "BID", "BID_PERIOD", @@ -38,14 +38,14 @@ FundingCurrencyTicker = _Serializer[typings.FundingCurrencyTicker]("FundingCurre "FRR_AMOUNT_AVAILABLE" ]) -TradingPairTrade = _Serializer[typings.TradingPairTrade]("TradingPairTrade", labels=[ +TradingPairTrade = _Serializer[types.TradingPairTrade]("TradingPairTrade", labels=[ "ID", "MTS", "AMOUNT", "PRICE" ]) -FundingCurrencyTrade = _Serializer[typings.FundingCurrencyTrade]("FundingCurrencyTrade", labels=[ +FundingCurrencyTrade = _Serializer[types.FundingCurrencyTrade]("FundingCurrencyTrade", labels=[ "ID", "MTS", "AMOUNT", @@ -53,33 +53,33 @@ FundingCurrencyTrade = _Serializer[typings.FundingCurrencyTrade]("FundingCurrenc "PERIOD" ]) -TradingPairBook = _Serializer[typings.TradingPairBook]("TradingPairBook", labels=[ +TradingPairBook = _Serializer[types.TradingPairBook]("TradingPairBook", labels=[ "PRICE", "COUNT", "AMOUNT" ]) -FundingCurrencyBook = _Serializer[typings.FundingCurrencyBook]("FundingCurrencyBook", labels=[ +FundingCurrencyBook = _Serializer[types.FundingCurrencyBook]("FundingCurrencyBook", labels=[ "RATE", "PERIOD", "COUNT", "AMOUNT" ]) -TradingPairRawBook = _Serializer[typings.TradingPairRawBook]("TradingPairRawBook", labels=[ +TradingPairRawBook = _Serializer[types.TradingPairRawBook]("TradingPairRawBook", labels=[ "ORDER_ID", "PRICE", "AMOUNT" ]) -FundingCurrencyRawBook = _Serializer[typings.FundingCurrencyRawBook]("FundingCurrencyRawBook", labels=[ +FundingCurrencyRawBook = _Serializer[types.FundingCurrencyRawBook]("FundingCurrencyRawBook", labels=[ "OFFER_ID", "PERIOD", "RATE", "AMOUNT" ]) -Candle = _Serializer[typings.Candle]("Candle", labels=[ +Candle = _Serializer[types.Candle]("Candle", labels=[ "MTS", "OPEN", "CLOSE", @@ -88,7 +88,7 @@ Candle = _Serializer[typings.Candle]("Candle", labels=[ "VOLUME" ]) -DerivativesStatus = _Serializer[typings.DerivativesStatus]("DerivativesStatus", labels=[ +DerivativesStatus = _Serializer[types.DerivativesStatus]("DerivativesStatus", labels=[ "TIME_MS", "_PLACEHOLDER", "DERIV_PRICE", @@ -118,7 +118,7 @@ DerivativesStatus = _Serializer[typings.DerivativesStatus]("DerivativesStatus", #region Serializers definition for Websocket Authenticated Channels -Order = _Serializer[typings.Order]("Order", labels=[ +Order = _Serializer[types.Order]("Order", labels=[ "ID", "GID", "CID", @@ -153,7 +153,7 @@ Order = _Serializer[typings.Order]("Order", labels=[ "META" ]) -Position = _Serializer[typings.Position]("Position", labels=[ +Position = _Serializer[types.Position]("Position", labels=[ "SYMBOL", "STATUS", "AMOUNT", @@ -176,7 +176,7 @@ Position = _Serializer[typings.Position]("Position", labels=[ "META" ]) -TradeExecuted = _Serializer[typings.TradeExecuted]("TradeExecuted", labels=[ +TradeExecuted = _Serializer[types.TradeExecuted]("TradeExecuted", labels=[ "ID", "SYMBOL", "MTS_CREATE", @@ -191,7 +191,7 @@ TradeExecuted = _Serializer[typings.TradeExecuted]("TradeExecuted", labels=[ "CID" ]) -TradeExecutionUpdate = _Serializer[typings.TradeExecutionUpdate]("TradeExecutionUpdate", labels=[ +TradeExecutionUpdate = _Serializer[types.TradeExecutionUpdate]("TradeExecutionUpdate", labels=[ "ID", "SYMBOL", "MTS_CREATE", @@ -206,7 +206,7 @@ TradeExecutionUpdate = _Serializer[typings.TradeExecutionUpdate]("TradeExecution "CID" ]) -FundingOffer = _Serializer[typings.FundingOffer]("FundingOffer", labels=[ +FundingOffer = _Serializer[types.FundingOffer]("FundingOffer", labels=[ "ID", "SYMBOL", "MTS_CREATED", @@ -230,7 +230,7 @@ FundingOffer = _Serializer[typings.FundingOffer]("FundingOffer", labels=[ "_PLACEHOLDER" ]) -FundingCredit = _Serializer[typings.FundingCredit]("FundingCredit", labels=[ +FundingCredit = _Serializer[types.FundingCredit]("FundingCredit", labels=[ "ID", "SYMBOL", "SIDE", @@ -255,7 +255,7 @@ FundingCredit = _Serializer[typings.FundingCredit]("FundingCredit", labels=[ "POSITION_PAIR" ]) -FundingLoan = _Serializer[typings.FundingLoan]("FundingLoan", labels=[ +FundingLoan = _Serializer[types.FundingLoan]("FundingLoan", labels=[ "ID", "SYMBOL", "SIDE", @@ -279,7 +279,7 @@ FundingLoan = _Serializer[typings.FundingLoan]("FundingLoan", labels=[ "NO_CLOSE" ]) -Wallet = _Serializer[typings.Wallet]("Wallet", labels=[ +Wallet = _Serializer[types.Wallet]("Wallet", labels=[ "WALLET_TYPE", "CURRENCY", "BALANCE", @@ -289,7 +289,7 @@ Wallet = _Serializer[typings.Wallet]("Wallet", labels=[ "META" ]) -BalanceInfo = _Serializer[typings.BalanceInfo]("BalanceInfo", labels=[ +BalanceInfo = _Serializer[types.BalanceInfo]("BalanceInfo", labels=[ "AUM", "AUM_NET", ]) diff --git a/bfxapi/websocket/typings.py b/bfxapi/websocket/types.py similarity index 83% rename from bfxapi/websocket/typings.py rename to bfxapi/websocket/types.py index ced6aaa..41838d5 100644 --- a/bfxapi/websocket/typings.py +++ b/bfxapi/websocket/types.py @@ -1,6 +1,10 @@ from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any -from .. notification import Notification +from dataclasses import dataclass + +from ..labeler import _Type + +from ..notification import Notification JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] @@ -48,7 +52,8 @@ class Subscriptions: #region Type hinting for Websocket Public Channels -class TradingPairTicker(TypedDict): +@dataclass +class TradingPairTicker(_Type): BID: float BID_SIZE: float ASK: float @@ -60,7 +65,8 @@ class TradingPairTicker(TypedDict): HIGH: float LOW: float -class FundingCurrencyTicker(TypedDict): +@dataclass +class FundingCurrencyTicker(_Type): FRR: float BID: float BID_PERIOD: int @@ -76,42 +82,49 @@ class FundingCurrencyTicker(TypedDict): LOW: float FRR_AMOUNT_AVAILABLE: float -class TradingPairTrade(TypedDict): +@dataclass +class TradingPairTrade(_Type): ID: int MTS: int AMOUNT: float PRICE: float -class FundingCurrencyTrade(TypedDict): +@dataclass +class FundingCurrencyTrade(_Type): ID: int MTS: int AMOUNT: float RATE: float PERIOD: int -class TradingPairBook(TypedDict): +@dataclass +class TradingPairBook(_Type): PRICE: float COUNT: int AMOUNT: float - -class FundingCurrencyBook(TypedDict): + +@dataclass +class FundingCurrencyBook(_Type): RATE: float PERIOD: int COUNT: int AMOUNT: float - -class TradingPairRawBook(TypedDict): + +@dataclass +class TradingPairRawBook(_Type): ORDER_ID: int PRICE: float AMOUNT: float - -class FundingCurrencyRawBook(TypedDict): + +@dataclass +class FundingCurrencyRawBook(_Type): OFFER_ID: int PERIOD: int RATE: float AMOUNT: float -class Candle(TypedDict): +@dataclass +class Candle(_Type): MTS: int OPEN: float CLOSE: float @@ -119,7 +132,8 @@ class Candle(TypedDict): LOW: float VOLUME: float -class DerivativesStatus(TypedDict): +@dataclass +class DerivativesStatus(_Type): TIME_MS: int DERIV_PRICE: float SPOT_PRICE: float @@ -136,8 +150,8 @@ class DerivativesStatus(TypedDict): #endregion #region Type hinting for Websocket Authenticated Channels - -class Order(TypedDict): +@dataclass +class Order(_Type): ID: int GID: int CID: int @@ -161,7 +175,8 @@ class Order(TypedDict): ROUTING: str META: JSON -class Position(TypedDict): +@dataclass +class Position(_Type): SYMBOL: str STATUS: str AMOUNT: float @@ -180,7 +195,8 @@ class Position(TypedDict): COLLATERAL_MIN: float META: JSON -class TradeExecuted(TypedDict): +@dataclass +class TradeExecuted(_Type): ID: int SYMBOL: str MTS_CREATE: int @@ -192,7 +208,8 @@ class TradeExecuted(TypedDict): MAKER:int CID: int -class TradeExecutionUpdate(TypedDict): +@dataclass +class TradeExecutionUpdate(_Type): ID: int SYMBOL: str MTS_CREATE: int @@ -206,7 +223,8 @@ class TradeExecutionUpdate(TypedDict): FEE_CURRENCY: str CID: int -class FundingOffer(TypedDict): +@dataclass +class FundingOffer(_Type): ID: int SYMBOL: str MTS_CREATED: int @@ -222,7 +240,8 @@ class FundingOffer(TypedDict): HIDDEN: int RENEW: int -class FundingCredit(TypedDict): +@dataclass +class FundingCredit(_Type): ID: int SYMBOL: str SIDE: int @@ -242,7 +261,8 @@ class FundingCredit(TypedDict): NO_CLOSE: int POSITION_PAIR: str -class FundingLoan(TypedDict): +@dataclass +class FundingLoan(_Type): ID: int SYMBOL: str SIDE: int @@ -261,7 +281,8 @@ class FundingLoan(TypedDict): RATE_REAL: float NO_CLOSE: int -class Wallet(TypedDict): +@dataclass +class Wallet(_Type): WALLET_TYPE: str CURRENCY: str BALANCE: float @@ -270,7 +291,8 @@ class Wallet(TypedDict): DESCRIPTION: str META: JSON -class BalanceInfo(TypedDict): +@dataclass +class BalanceInfo(_Type): AUM: float AUM_NET: float From 2afcc766476d3e5d0af623fa0e1cb33de7c616c5 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 16 Jan 2023 18:18:12 +0100 Subject: [PATCH 099/265] Fix bug regarding new typing with dataclasses. --- bfxapi/labeler.py | 13 +++++---- bfxapi/notification.py | 8 +++--- bfxapi/rest/serializers.py | 50 ++++++++++++++++----------------- bfxapi/websocket/serializers.py | 40 +++++++++++++------------- 4 files changed, 57 insertions(+), 54 deletions(-) diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 9714b5c..38b527b 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -1,17 +1,17 @@ from .exceptions import LabelerSerializerException -from typing import Generic, TypeVar, Iterable, Optional, List, Tuple, Any, cast +from typing import Type, Generic, TypeVar, Iterable, Optional, List, Tuple, Any, cast T = TypeVar("T") class _Type(object): def __init__(self, **kwargs): for key, value in kwargs.items(): - self.__setattr__(key,value) + self.__setattr__(key, value) class _Serializer(Generic[T]): - def __init__(self, name: str, labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]): - self.name, self.__labels, self.__IGNORE = name, labels, IGNORE + def __init__(self, name: str, klass: Type[_Type], labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]): + self.name, self.klass, self.__labels, self.__IGNORE = name, klass, labels, IGNORE def _serialize(self, *args: Any, skip: Optional[List[str]] = None) -> Iterable[Tuple[str, Any]]: labels = list(filter(lambda label: label not in (skip or list()), self.__labels)) @@ -24,4 +24,7 @@ class _Serializer(Generic[T]): yield label, args[index] def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: - return cast(T, _Type(**dict(self._serialize(*values, skip=skip)))) \ No newline at end of file + return cast(T, self.klass(**dict(self._serialize(*values, skip=skip)))) + +def generate_labeler_serializer(name: str, klass: T, labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> _Serializer[T]: + return _Serializer[T](name, klass, labels, IGNORE) \ No newline at end of file diff --git a/bfxapi/notification.py b/bfxapi/notification.py index e66ba60..b2f90b8 100644 --- a/bfxapi/notification.py +++ b/bfxapi/notification.py @@ -20,12 +20,12 @@ class _Notification(_Serializer, Generic[T]): __LABELS = [ "MTS", "TYPE", "MESSAGE_ID", "_PLACEHOLDER", "NOTIFY_INFO", "CODE", "STATUS", "TEXT" ] def __init__(self, serializer: Optional[_Serializer] = None, iterate: bool = False): - super().__init__("Notification", _Notification.__LABELS, IGNORE = [ "_PLACEHOLDER" ]) + super().__init__("Notification", Notification, _Notification.__LABELS, IGNORE = [ "_PLACEHOLDER" ]) self.serializer, self.iterate = serializer, iterate def parse(self, *values: Any, skip: Optional[List[str]] = None) -> Notification[T]: - notification = cast(Notification[T], _Type(**dict(self._serialize(*values)))) + notification = cast(Notification[T], Notification(**dict(self._serialize(*values)))) if isinstance(self.serializer, _Serializer): NOTIFY_INFO = cast(List[Any], notification.NOTIFY_INFO) @@ -34,7 +34,7 @@ class _Notification(_Serializer, Generic[T]): if len(NOTIFY_INFO) == 1 and isinstance(NOTIFY_INFO[0], list): NOTIFY_INFO = NOTIFY_INFO[0] - notification.NOTIFY_INFO = cast(T, _Type(**dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip)))) - else: notification.NOTIFY_INFO = cast(T, [ _Type(**dict(self.serializer._serialize(*data, skip=skip))) for data in NOTIFY_INFO ]) + notification.NOTIFY_INFO = cast(T, self.serializer.klass(**dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip)))) + else: notification.NOTIFY_INFO = cast(T, [ self.serializer.klass(**dict(self.serializer._serialize(*data, skip=skip))) for data in NOTIFY_INFO ]) return notification \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 4196b52..35df8ae 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -1,16 +1,16 @@ from . import types -from .. labeler import _Serializer +from .. labeler import generate_labeler_serializer from .. notification import _Notification #region Serializers definition for Rest Public Endpoints -PlatformStatus = _Serializer[types.PlatformStatus]("PlatformStatus", labels=[ +PlatformStatus = generate_labeler_serializer("PlatformStatus", klass=types.PlatformStatus, labels=[ "OPERATIVE" ]) -TradingPairTicker = _Serializer[types.TradingPairTicker]("TradingPairTicker", labels=[ +TradingPairTicker = generate_labeler_serializer("TradingPairTicker", klass=types.TradingPairTicker, labels=[ "SYMBOL", "BID", "BID_SIZE", @@ -24,7 +24,7 @@ TradingPairTicker = _Serializer[types.TradingPairTicker]("TradingPairTicker", la "LOW" ]) -FundingCurrencyTicker = _Serializer[types.FundingCurrencyTicker]("FundingCurrencyTicker", labels=[ +FundingCurrencyTicker = generate_labeler_serializer("FundingCurrencyTicker", klass=types.FundingCurrencyTicker, labels=[ "SYMBOL", "FRR", "BID", @@ -44,7 +44,7 @@ FundingCurrencyTicker = _Serializer[types.FundingCurrencyTicker]("FundingCurrenc "FRR_AMOUNT_AVAILABLE" ]) -TickersHistory = _Serializer[types.TickersHistory]("TickersHistory", labels=[ +TickersHistory = generate_labeler_serializer("TickersHistory", klass=types.TickersHistory, labels=[ "SYMBOL", "BID", "_PLACEHOLDER", @@ -60,14 +60,14 @@ TickersHistory = _Serializer[types.TickersHistory]("TickersHistory", labels=[ "MTS" ]) -TradingPairTrade = _Serializer[types.TradingPairTrade]("TradingPairTrade", labels=[ +TradingPairTrade = generate_labeler_serializer("TradingPairTrade", klass=types.TradingPairTrade, labels=[ "ID", "MTS", "AMOUNT", "PRICE" ]) -FundingCurrencyTrade = _Serializer[types.FundingCurrencyTrade]("FundingCurrencyTrade", labels=[ +FundingCurrencyTrade = generate_labeler_serializer("FundingCurrencyTrade", klass=types.FundingCurrencyTrade, labels=[ "ID", "MTS", "AMOUNT", @@ -75,38 +75,38 @@ FundingCurrencyTrade = _Serializer[types.FundingCurrencyTrade]("FundingCurrencyT "PERIOD" ]) -TradingPairBook = _Serializer[types.TradingPairBook]("TradingPairBook", labels=[ +TradingPairBook = generate_labeler_serializer("TradingPairBook", klass=types.TradingPairBook, labels=[ "PRICE", "COUNT", "AMOUNT" ]) -FundingCurrencyBook = _Serializer[types.FundingCurrencyBook]("FundingCurrencyBook", labels=[ +FundingCurrencyBook = generate_labeler_serializer("FundingCurrencyBook", klass=types.FundingCurrencyBook, labels=[ "RATE", "PERIOD", "COUNT", "AMOUNT" ]) -TradingPairRawBook = _Serializer[types.TradingPairRawBook]("TradingPairRawBook", labels=[ +TradingPairRawBook = generate_labeler_serializer("TradingPairRawBook", klass=types.TradingPairRawBook, labels=[ "ORDER_ID", "PRICE", "AMOUNT" ]) -FundingCurrencyRawBook = _Serializer[types.FundingCurrencyRawBook]("FundingCurrencyRawBook", labels=[ +FundingCurrencyRawBook = generate_labeler_serializer("FundingCurrencyRawBook", klass=types.FundingCurrencyRawBook, labels=[ "OFFER_ID", "PERIOD", "RATE", "AMOUNT" ]) -Statistic = _Serializer[types.Statistic]("Statistic", labels=[ +Statistic = generate_labeler_serializer("Statistic", klass=types.Statistic, labels=[ "MTS", "VALUE" ]) -Candle = _Serializer[types.Candle]("Candle", labels=[ +Candle = generate_labeler_serializer("Candle", klass=types.Candle, labels=[ "MTS", "OPEN", "CLOSE", @@ -115,7 +115,7 @@ Candle = _Serializer[types.Candle]("Candle", labels=[ "VOLUME" ]) -DerivativesStatus = _Serializer[types.DerivativesStatus]("DerivativesStatus", labels=[ +DerivativesStatus = generate_labeler_serializer("DerivativesStatus", klass=types.DerivativesStatus, labels=[ "KEY", "MTS", "_PLACEHOLDER", @@ -142,7 +142,7 @@ DerivativesStatus = _Serializer[types.DerivativesStatus]("DerivativesStatus", la "CLAMP_MAX" ]) -Liquidation = _Serializer[types.Liquidation]("Liquidation", labels=[ +Liquidation = generate_labeler_serializer("Liquidation", klass=types.Liquidation, labels=[ "_PLACEHOLDER", "POS_ID", "MTS", @@ -157,7 +157,7 @@ Liquidation = _Serializer[types.Liquidation]("Liquidation", labels=[ "PRICE_ACQUIRED" ]) -Leaderboard = _Serializer[types.Leaderboard]("Leaderboard", labels=[ +Leaderboard = generate_labeler_serializer("Leaderboard", klass=types.Leaderboard, labels=[ "MTS", "_PLACEHOLDER", "USERNAME", @@ -170,7 +170,7 @@ Leaderboard = _Serializer[types.Leaderboard]("Leaderboard", labels=[ "TWITTER_HANDLE" ]) -FundingStatistic = _Serializer[types.FundingStatistic]("FundingStatistic", labels=[ +FundingStatistic = generate_labeler_serializer("FundingStatistic", klass=types.FundingStatistic, labels=[ "TIMESTAMP", "_PLACEHOLDER", "_PLACEHOLDER", @@ -189,7 +189,7 @@ FundingStatistic = _Serializer[types.FundingStatistic]("FundingStatistic", label #region Serializers definition for Rest Authenticated Endpoints -Wallet = _Serializer[types.Wallet]("Wallet", labels=[ +Wallet = generate_labeler_serializer("Wallet", klass=types.Wallet, labels=[ "WALLET_TYPE", "CURRENCY", "BALANCE", @@ -199,7 +199,7 @@ Wallet = _Serializer[types.Wallet]("Wallet", labels=[ "TRADE_DETAILS" ]) -Order = _Serializer[types.Order]("Order", labels=[ +Order = generate_labeler_serializer("Order", klass=types.Order, labels=[ "ID", "GID", "CID", @@ -234,7 +234,7 @@ Order = _Serializer[types.Order]("Order", labels=[ "META" ]) -Position = _Serializer[types.Position]("Position", labels=[ +Position = generate_labeler_serializer("Position", klass=types.Position, labels=[ "SYMBOL", "STATUS", "AMOUNT", @@ -257,7 +257,7 @@ Position = _Serializer[types.Position]("Position", labels=[ "META" ]) -FundingOffer = _Serializer[types.FundingOffer]("FundingOffer", labels=[ +FundingOffer = generate_labeler_serializer("FundingOffer", klass=types.FundingOffer, labels=[ "ID", "SYMBOL", "MTS_CREATED", @@ -281,7 +281,7 @@ FundingOffer = _Serializer[types.FundingOffer]("FundingOffer", labels=[ "_PLACEHOLDER" ]) -Trade = _Serializer[types.Trade]("Trade", labels=[ +Trade = generate_labeler_serializer("Trade", klass=types.Trade, labels=[ "ID", "PAIR", "MTS_CREATE", @@ -296,7 +296,7 @@ Trade = _Serializer[types.Trade]("Trade", labels=[ "CID" ]) -OrderTrade = _Serializer[types.OrderTrade]("OrderTrade", labels=[ +OrderTrade = generate_labeler_serializer("OrderTrade", klass=types.OrderTrade, labels=[ "ID", "PAIR", "MTS_CREATE", @@ -311,7 +311,7 @@ OrderTrade = _Serializer[types.OrderTrade]("OrderTrade", labels=[ "CID" ]) -Ledger = _Serializer[types.Ledger]("Ledger", labels=[ +Ledger = generate_labeler_serializer("Ledger", klass=types.Ledger, labels=[ "ID", "CURRENCY", "_PLACEHOLDER", @@ -323,7 +323,7 @@ Ledger = _Serializer[types.Ledger]("Ledger", labels=[ "DESCRIPTION" ]) -FundingCredit = _Serializer[types.FundingCredit]("FundingCredit", labels=[ +FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.FundingCredit, labels=[ "ID", "SYMBOL", "SIDE", diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index 6bd56c1..1e14dbc 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -1,12 +1,12 @@ from . import types -from .. labeler import _Serializer +from .. labeler import generate_labeler_serializer from .. notification import _Notification #region Serializers definition for Websocket Public Channels -TradingPairTicker = _Serializer[types.TradingPairTicker]("TradingPairTicker", labels=[ +TradingPairTicker = generate_labeler_serializer("TradingPairTicker", klass=types.TradingPairTicker, labels=[ "BID", "BID_SIZE", "ASK", @@ -19,7 +19,7 @@ TradingPairTicker = _Serializer[types.TradingPairTicker]("TradingPairTicker", la "LOW" ]) -FundingCurrencyTicker = _Serializer[types.FundingCurrencyTicker]("FundingCurrencyTicker", labels=[ +FundingCurrencyTicker = generate_labeler_serializer("FundingCurrencyTicker", klass=types.FundingCurrencyTicker, labels=[ "FRR", "BID", "BID_PERIOD", @@ -38,14 +38,14 @@ FundingCurrencyTicker = _Serializer[types.FundingCurrencyTicker]("FundingCurrenc "FRR_AMOUNT_AVAILABLE" ]) -TradingPairTrade = _Serializer[types.TradingPairTrade]("TradingPairTrade", labels=[ +TradingPairTrade = generate_labeler_serializer("TradingPairTrade", klass=types.TradingPairTrade, labels=[ "ID", "MTS", "AMOUNT", "PRICE" ]) -FundingCurrencyTrade = _Serializer[types.FundingCurrencyTrade]("FundingCurrencyTrade", labels=[ +FundingCurrencyTrade = generate_labeler_serializer("FundingCurrencyTrade", klass=types.FundingCurrencyTrade, labels=[ "ID", "MTS", "AMOUNT", @@ -53,33 +53,33 @@ FundingCurrencyTrade = _Serializer[types.FundingCurrencyTrade]("FundingCurrencyT "PERIOD" ]) -TradingPairBook = _Serializer[types.TradingPairBook]("TradingPairBook", labels=[ +TradingPairBook = generate_labeler_serializer("TradingPairBook", klass=types.TradingPairBook, labels=[ "PRICE", "COUNT", "AMOUNT" ]) -FundingCurrencyBook = _Serializer[types.FundingCurrencyBook]("FundingCurrencyBook", labels=[ +FundingCurrencyBook = generate_labeler_serializer("FundingCurrencyBook", klass=types.FundingCurrencyBook, labels=[ "RATE", "PERIOD", "COUNT", "AMOUNT" ]) -TradingPairRawBook = _Serializer[types.TradingPairRawBook]("TradingPairRawBook", labels=[ +TradingPairRawBook = generate_labeler_serializer("TradingPairRawBook", klass=types.TradingPairRawBook, labels=[ "ORDER_ID", "PRICE", "AMOUNT" ]) -FundingCurrencyRawBook = _Serializer[types.FundingCurrencyRawBook]("FundingCurrencyRawBook", labels=[ +FundingCurrencyRawBook = generate_labeler_serializer("FundingCurrencyRawBook", klass=types.FundingCurrencyRawBook, labels=[ "OFFER_ID", "PERIOD", "RATE", "AMOUNT" ]) -Candle = _Serializer[types.Candle]("Candle", labels=[ +Candle = generate_labeler_serializer("Candle", klass=types.Candle, labels=[ "MTS", "OPEN", "CLOSE", @@ -88,7 +88,7 @@ Candle = _Serializer[types.Candle]("Candle", labels=[ "VOLUME" ]) -DerivativesStatus = _Serializer[types.DerivativesStatus]("DerivativesStatus", labels=[ +DerivativesStatus = generate_labeler_serializer("DerivativesStatus", klass=types.DerivativesStatus, labels=[ "TIME_MS", "_PLACEHOLDER", "DERIV_PRICE", @@ -118,7 +118,7 @@ DerivativesStatus = _Serializer[types.DerivativesStatus]("DerivativesStatus", la #region Serializers definition for Websocket Authenticated Channels -Order = _Serializer[types.Order]("Order", labels=[ +Order = generate_labeler_serializer("Order", klass=types.Order, labels=[ "ID", "GID", "CID", @@ -153,7 +153,7 @@ Order = _Serializer[types.Order]("Order", labels=[ "META" ]) -Position = _Serializer[types.Position]("Position", labels=[ +Position = generate_labeler_serializer("Position", klass=types.Position, labels=[ "SYMBOL", "STATUS", "AMOUNT", @@ -176,7 +176,7 @@ Position = _Serializer[types.Position]("Position", labels=[ "META" ]) -TradeExecuted = _Serializer[types.TradeExecuted]("TradeExecuted", labels=[ +TradeExecuted = generate_labeler_serializer("TradeExecuted", klass=types.TradeExecuted, labels=[ "ID", "SYMBOL", "MTS_CREATE", @@ -191,7 +191,7 @@ TradeExecuted = _Serializer[types.TradeExecuted]("TradeExecuted", labels=[ "CID" ]) -TradeExecutionUpdate = _Serializer[types.TradeExecutionUpdate]("TradeExecutionUpdate", labels=[ +TradeExecutionUpdate = generate_labeler_serializer("TradeExecutionUpdate", klass=types.TradeExecutionUpdate, labels=[ "ID", "SYMBOL", "MTS_CREATE", @@ -206,7 +206,7 @@ TradeExecutionUpdate = _Serializer[types.TradeExecutionUpdate]("TradeExecutionUp "CID" ]) -FundingOffer = _Serializer[types.FundingOffer]("FundingOffer", labels=[ +FundingOffer = generate_labeler_serializer("FundingOffer", klass=types.FundingOffer, labels=[ "ID", "SYMBOL", "MTS_CREATED", @@ -230,7 +230,7 @@ FundingOffer = _Serializer[types.FundingOffer]("FundingOffer", labels=[ "_PLACEHOLDER" ]) -FundingCredit = _Serializer[types.FundingCredit]("FundingCredit", labels=[ +FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.FundingCredit, labels=[ "ID", "SYMBOL", "SIDE", @@ -255,7 +255,7 @@ FundingCredit = _Serializer[types.FundingCredit]("FundingCredit", labels=[ "POSITION_PAIR" ]) -FundingLoan = _Serializer[types.FundingLoan]("FundingLoan", labels=[ +FundingLoan = generate_labeler_serializer("FundingLoan", klass=types.FundingLoan, labels=[ "ID", "SYMBOL", "SIDE", @@ -279,7 +279,7 @@ FundingLoan = _Serializer[types.FundingLoan]("FundingLoan", labels=[ "NO_CLOSE" ]) -Wallet = _Serializer[types.Wallet]("Wallet", labels=[ +Wallet = generate_labeler_serializer("Wallet", klass=types.Wallet, labels=[ "WALLET_TYPE", "CURRENCY", "BALANCE", @@ -289,7 +289,7 @@ Wallet = _Serializer[types.Wallet]("Wallet", labels=[ "META" ]) -BalanceInfo = _Serializer[types.BalanceInfo]("BalanceInfo", labels=[ +BalanceInfo = generate_labeler_serializer("BalanceInfo", klass=types.BalanceInfo, labels=[ "AUM", "AUM_NET", ]) From 6f8e37cad0d70edce37441bd6f1cfd3137d107ac Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 16 Jan 2023 18:27:03 +0100 Subject: [PATCH 100/265] Fix all mypy errors and warnings due to new dataclass implementation. --- bfxapi/labeler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 38b527b..87889e2 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -2,7 +2,7 @@ from .exceptions import LabelerSerializerException from typing import Type, Generic, TypeVar, Iterable, Optional, List, Tuple, Any, cast -T = TypeVar("T") +T = TypeVar("T", bound="_Type") class _Type(object): def __init__(self, **kwargs): @@ -26,5 +26,5 @@ class _Serializer(Generic[T]): def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: return cast(T, self.klass(**dict(self._serialize(*values, skip=skip)))) -def generate_labeler_serializer(name: str, klass: T, labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> _Serializer[T]: +def generate_labeler_serializer(name: str, klass: Type[T], labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> _Serializer[T]: return _Serializer[T](name, klass, labels, IGNORE) \ No newline at end of file From 89c921335ca455a608e8f3397d172d2833650f90 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Tue, 17 Jan 2023 12:13:06 +0100 Subject: [PATCH 101/265] Use namespace --- examples/rest/create_order.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/rest/create_order.py b/examples/rest/create_order.py index 34408aa..71a99ca 100644 --- a/examples/rest/create_order.py +++ b/examples/rest/create_order.py @@ -23,7 +23,7 @@ print("Submit Order Notification:", submitted_order) # Update it updated_order = bfx.rest.auth.update_order( - id=submitted_order["NOTIFY_INFO"]["ID"], + id=submitted_order.NOTIFY_INFO.ID, amount="0.020", price="10100" ) @@ -31,6 +31,6 @@ updated_order = bfx.rest.auth.update_order( print("Update Order Notification:", updated_order) # Delete it -canceled_order = bfx.rest.auth.cancel_order(id=submitted_order["NOTIFY_INFO"]["ID"]) +canceled_order = bfx.rest.auth.cancel_order(id=submitted_order.NOTIFY_INFO.ID) print("Cancel Order Notification:", canceled_order) From 061ca22752ddab371cca3fa97f39a07f14a6b5b2 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Tue, 17 Jan 2023 12:40:09 +0100 Subject: [PATCH 102/265] add seed candles --- bfxapi/rest/BfxRestInterface.py | 10 ++++++++++ examples/rest/get_seed_candles.py | 15 +++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 examples/rest/get_seed_candles.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index c49ca56..deb5bdc 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -45,6 +45,8 @@ class _Requests(object): def _GET(self, endpoint, params = None): response = requests.get(f"{self.host}/{endpoint}", params=params) + + print(f"{self.host}/{endpoint}") if response.status_code == HTTPStatus.NOT_FOUND: raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") @@ -215,6 +217,14 @@ class _RestPublicEndpoints(_Requests): return [ serializers.Liquidation.parse(*subdata[0]) for subdata in data ] + def get_seed_candles(self, symbol: str, tf: str = '1m', sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Candle]: + + params = {"sort": sort, "start": start, "end": end, "limit": limit} + + data = self._GET(f"candles/trade:{tf}:{symbol}/hist?limit={limit}&start={start}&end={end}&sort={sort}", params=params) + + return [ serializers.Candle.parse(*subdata) for subdata in data ] + def get_leaderboards_hist( self, resource: str, diff --git a/examples/rest/get_seed_candles.py b/examples/rest/get_seed_candles.py new file mode 100644 index 0000000..ef02550 --- /dev/null +++ b/examples/rest/get_seed_candles.py @@ -0,0 +1,15 @@ +# python -c "from examples.rest.get_seed_candles import *" + +import time + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST +) + +print(f"Candles: {bfx.rest.public.get_seed_candles(symbol='tBTCUSD')}") + +# Be sure to specify a period or aggregated period when retrieving funding candles. +# If you wish to mimic the candles found in the UI, use the following setup to aggregate all funding candles: a30:p2:p30 +print(f"Candles: {bfx.rest.public.get_seed_candles(symbol='fUSD:a30:p2:p30', tf='15m')}") \ No newline at end of file From 7ea8b5ae1c54339a24ea81bf80652ad8dabcd1b0 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Tue, 17 Jan 2023 12:40:09 +0100 Subject: [PATCH 103/265] add seed candles --- bfxapi/rest/BfxRestInterface.py | 8 ++++---- examples/rest/get_candles_hist.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 examples/rest/get_candles_hist.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index deb5bdc..c16c555 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -176,20 +176,20 @@ class _RestPublicEndpoints(_Requests): def get_candles_hist( self, - resource: str, + symbol: str, tf: str = "1m", sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> List[Candle]: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"candles/{resource}/hist", params=params) + data = self._GET(f"candles/trade:{tf}:{symbol}/hist", params=params) return [ serializers.Candle.parse(*subdata) for subdata in data ] def get_candles_last( self, - resource: str, + symbol: str, tf: str = "1m", sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> Candle: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"candles/{resource}/last", params=params) + data = self._GET(f"candles/trade:{tf}:{symbol}/last", params=params) return serializers.Candle.parse(*data) def get_derivatives_status(self, symbols: Union[List[str], Literal["ALL"]]) -> List[DerivativesStatus]: diff --git a/examples/rest/get_candles_hist.py b/examples/rest/get_candles_hist.py new file mode 100644 index 0000000..fde6212 --- /dev/null +++ b/examples/rest/get_candles_hist.py @@ -0,0 +1,13 @@ +# python -c "from examples.rest.get_candles_hist import *" + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST +) + +print(f"Candles: {bfx.rest.public.get_candles_hist(symbol='tBTCUSD')}") + +# Be sure to specify a period or aggregated period when retrieving funding candles. +# If you wish to mimic the candles found in the UI, use the following setup to aggregate all funding candles: a30:p2:p30 +print(f"Candles: {bfx.rest.public.get_candles_hist(tf='15m', symbol='fUSD:a30:p2:p30')}") \ No newline at end of file From 430f7be51d383cb17825520d46dfa6d8d0d8f474 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Tue, 17 Jan 2023 13:14:55 +0100 Subject: [PATCH 104/265] add wallet deposit/transfer --- bfxapi/rest/BfxRestInterface.py | 54 +++++++++++++++++++++++-- bfxapi/rest/serializers.py | 65 +++++++++++++++++++++++++++++++ bfxapi/rest/types.py | 45 +++++++++++++++++++++ examples/rest/get_seed_candles.py | 15 ------- examples/rest/transfer_wallet.py | 46 ++++++++++++++++++++++ 5 files changed, 207 insertions(+), 18 deletions(-) delete mode 100644 examples/rest/get_seed_candles.py create mode 100644 examples/rest/transfer_wallet.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index c16c555..741a3ec 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -192,8 +192,10 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"candles/trade:{tf}:{symbol}/last", params=params) return serializers.Candle.parse(*data) - def get_derivatives_status(self, symbols: Union[List[str], Literal["ALL"]]) -> List[DerivativesStatus]: - params = { "keys": ",".join(symbols) if type(symbols) == List else "ALL" } + def get_derivatives_status(self, keys: Union[List[str], Literal["ALL"]]) -> List[DerivativesStatus]: + if keys == "ALL": + params = { "keys": "ALL" } + else: params = { "keys": ",".join(keys) } data = self._GET(f"status/deriv", params=params) @@ -405,4 +407,50 @@ class _RestAuthenticatedEndpoints(_Requests): "limit": limit } - return [ serializers.FundingCredit.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] \ No newline at end of file + return [ serializers.FundingCredit.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] + + def submit_wallet_transfer(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, str]) -> Notification[Transfer]: + data = { + "from": from_wallet, "to": to_wallet, + "currency": currency, "currency_to": currency_to, + "amount": amount + } + + return serializers._Notification[Transfer](serializer=serializers.Transfer).parse(*self._POST("auth/w/transfer", data=data)) + + def submit_wallet_withdraw(self, wallet: str, method: str, address: str, amount: Union[Decimal, str]) -> Notification[Withdrawal]: + data = { + "wallet": wallet, "method": method, + "address": address, "amount": amount, + } + + return serializers._Notification[Withdrawal](serializer=serializers.Withdrawal).parse(*self._POST("auth/w/withdraw", data=data)) + + def get_deposit_address(self, wallet: str, method: str, renew: bool = False) -> Notification[DepositAddress]: + data = { + "wallet": wallet, + "method": method, + "renew": int(renew) + } + + return serializers._Notification[DepositAddress](serializer=serializers.DepositAddress).parse(*self._POST("auth/w/deposit/address", data=data)) + + def get_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, str]) -> Invoice: + data = { + "wallet": wallet, "currency": currency, + "amount": amount + } + + return serializers.Invoice.parse(*self._POST("auth/w/deposit/invoice", data=data)) + + def get_movements(self, currency: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Movement]: + if currency == None: + endpoint = "auth/r/movements/hist" + else: endpoint = f"auth/r/movements/{currency}/hist" + + data = { + "start": start, "end": end, + "limit": limit + } + + return [ serializers.Movement.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 35df8ae..b6af19f 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -348,4 +348,69 @@ FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.Funding "POSITION_PAIR" ]) +Transfer = generate_labeler_serializer("Transfer", klass=types.Transfer, labels=[ + "MTS", + "WALLET_FROM", + "WALLET_TO", + "_PLACEHOLDER", + "CURRENCY", + "CURRENCY_TO", + "_PLACEHOLDER", + "AMOUNT" +]) + +Withdrawal = generate_labeler_serializer("Withdrawal", klass=types.Withdrawal, labels=[ + "WITHDRAWAL_ID", + "_PLACEHOLDER", + "METHOD", + "PAYMENT_ID", + "WALLET", + "AMOUNT", + "_PLACEHOLDER", + "_PLACEHOLDER", + "WITHDRAWAL_FEE" +]) + +DepositAddress = generate_labeler_serializer("DepositAddress", klass=types.DepositAddress, labels=[ + "_PLACEHOLDER", + "METHOD", + "CURRENCY_CODE", + "_PLACEHOLDER", + "ADDRESS", + "POOL_ADDRESS" +]) + +Invoice = generate_labeler_serializer("Invoice", klass=types.Invoice, labels=[ + "INVOICE_HASH", + "INVOICE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "AMOUNT" +]) + +Movement = generate_labeler_serializer("Movement", klass=types.Movement, labels=[ + "ID", + "CURRENCY", + "CURRENCY_NAME", + "_PLACEHOLDER", + "_PLACEHOLDER", + "MTS_STARTED", + "MTS_UPDATED", + "_PLACEHOLDER", + "_PLACEHOLDER", + "STATUS", + "_PLACEHOLDER", + "_PLACEHOLDER", + "AMOUNT", + "FEES", + "_PLACEHOLDER", + "_PLACEHOLDER", + "DESTINATION_ADDRESS", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "TRANSACTION_ID", + "WITHDRAW_TRANSACTION_NOTE" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index a27a404..1dbc533 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -286,4 +286,49 @@ class FundingCredit(_Type): NO_CLOSE: int POSITION_PAIR: str +@dataclass +class Transfer(_Type): + MTS: int + WALLET_FROM: str + WALLET_TO: str + CURRENCY: str + CURRENCY_TO: str + AMOUNT: int + +@dataclass +class Withdrawal(_Type): + WITHDRAWAL_ID: int + METHOD: str + PAYMENT_ID: str + WALLET: str + AMOUNT: float + WITHDRAWAL_FEE: float + +@dataclass +class DepositAddress(_Type): + METHOD: str + CURRENCY_CODE: str + ADDRESS: str + POOL_ADDRESS: str + +@dataclass +class Invoice(_Type): + INVOICE_HASH: str + INVOICE: str + AMOUNT: str + +@dataclass +class Movement(_Type): + ID: str + CURRENCY: str + CURRENCY_NAME: str + MTS_STARTED: int + MTS_UPDATED: int + STATUS: str + AMOUNT: int + FEES: int + DESTINATION_ADDRESS: str + TRANSACTION_ID: str + WITHDRAW_TRANSACTION_NOTE: str + #endregion \ No newline at end of file diff --git a/examples/rest/get_seed_candles.py b/examples/rest/get_seed_candles.py deleted file mode 100644 index ef02550..0000000 --- a/examples/rest/get_seed_candles.py +++ /dev/null @@ -1,15 +0,0 @@ -# python -c "from examples.rest.get_seed_candles import *" - -import time - -from bfxapi.client import Client, Constants - -bfx = Client( - REST_HOST=Constants.REST_HOST -) - -print(f"Candles: {bfx.rest.public.get_seed_candles(symbol='tBTCUSD')}") - -# Be sure to specify a period or aggregated period when retrieving funding candles. -# If you wish to mimic the candles found in the UI, use the following setup to aggregate all funding candles: a30:p2:p30 -print(f"Candles: {bfx.rest.public.get_seed_candles(symbol='fUSD:a30:p2:p30', tf='15m')}") \ No newline at end of file diff --git a/examples/rest/transfer_wallet.py b/examples/rest/transfer_wallet.py new file mode 100644 index 0000000..28dcccb --- /dev/null +++ b/examples/rest/transfer_wallet.py @@ -0,0 +1,46 @@ +# python -c "from examples.rest.transfer_wallet import *" + +import os + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +def transfer_wallet(): + response = bfx.rest.auth.submit_wallet_transfer(from_wallet="exchange", to_wallet="funding", from_currency="ETH", to_currency="ETH", amount=0.001) + print("Transfer:", response.NOTIFY_INFO) + +def get_existing_deposit_address(): + response = bfx.rest.auth.get_deposit_address(wallet="exchange", method="bitcoin", renew=False) + print("Address:", response.NOTIFY_INFO) + +def create_new_deposit_address(): + response = bfx.rest.auth.get_deposit_address(wallet="exchange", method="bitcoin", renew=True) + print("Address:", response.NOTIFY_INFO) + +def withdraw(): + # tetheruse = Tether (ERC20) + response = bfx.rest.auth.submit_wallet_withdraw(wallet="exchange", method="tetheruse", amount=1, address="0x742d35Cc6634C0532925a3b844Bc454e4438f44e") + print("Address:", response.NOTIFY_INFO) + +def create_lighting_network_deposit_address(): + invoice = bfx.rest.auth.get_deposit_invoice(wallet="funding", currency="LNX", amount=0.001) + print("Invoice:", invoice) + +def get_movements(): + movements = bfx.rest.auth.get_movements(currency="BTC") + print("Movements:", movements) + +def run(): + transfer_wallet() + get_existing_deposit_address() + create_new_deposit_address() + withdraw() + create_lighting_network_deposit_address() + get_movements() + +run() \ No newline at end of file From 578882b4c25238ff4c174cfeb2cbff06042c48b7 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 18 Jan 2023 19:00:37 +0100 Subject: [PATCH 105/265] Small sync with new documentation improvements. --- bfxapi/rest/serializers.py | 2 +- bfxapi/rest/types.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index b6af19f..7cea3db 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -7,7 +7,7 @@ from .. notification import _Notification #region Serializers definition for Rest Public Endpoints PlatformStatus = generate_labeler_serializer("PlatformStatus", klass=types.PlatformStatus, labels=[ - "OPERATIVE" + "STATUS" ]) TradingPairTicker = generate_labeler_serializer("TradingPairTicker", klass=types.TradingPairTicker, labels=[ diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 1dbc533..9cd14b9 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -12,7 +12,7 @@ JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] @dataclass class PlatformStatus(_Type): - OPERATIVE: int + STATUS: int @dataclass class TradingPairTicker(_Type): From c471a3b52bec966e89ae995394a8222816558ca0 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 19 Jan 2023 16:35:17 +0100 Subject: [PATCH 106/265] Sync demos in examples/websocket with new bfxapi improvements. --- examples/websocket/create_order.py | 4 ++-- examples/websocket/order_book.py | 4 ++-- examples/websocket/raw_order_book.py | 4 ++-- examples/websocket/ticker.py | 4 +--- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/examples/websocket/create_order.py b/examples/websocket/create_order.py index b3146e9..7a6f645 100644 --- a/examples/websocket/create_order.py +++ b/examples/websocket/create_order.py @@ -4,7 +4,7 @@ import os from bfxapi.client import Client, Constants from bfxapi.websocket.enums import Error, OrderType -from bfxapi.websocket.typings import Notification, Order +from bfxapi.websocket.types import Notification, Order bfx = Client( WSS_HOST=Constants.WSS_HOST, @@ -30,7 +30,7 @@ async def on_authenticated(event): print("The order has been sent.") @bfx.wss.on("on-req-notification") -async def on_notification(notification: Notification): +async def on_notification(notification: Notification[Order]): print(f"Notification: {notification}.") @bfx.wss.on("order_new") diff --git a/examples/websocket/order_book.py b/examples/websocket/order_book.py index 0035cf8..fb583e1 100644 --- a/examples/websocket/order_book.py +++ b/examples/websocket/order_book.py @@ -7,7 +7,7 @@ from typing import List from bfxapi import Client, Constants from bfxapi.websocket.enums import Channels, Error -from bfxapi.websocket.typings import Subscriptions, TradingPairBook +from bfxapi.websocket.types import Subscriptions, TradingPairBook class OrderBook(object): def __init__(self, symbols: List[str]): @@ -18,7 +18,7 @@ class OrderBook(object): } def update(self, symbol: str, data: TradingPairBook) -> None: - price, count, amount = data["PRICE"], data["COUNT"], data["AMOUNT"] + price, count, amount = data.PRICE, data.COUNT, data.AMOUNT kind = (amount > 0) and "bids" or "asks" diff --git a/examples/websocket/raw_order_book.py b/examples/websocket/raw_order_book.py index 6cfc3c1..6819820 100644 --- a/examples/websocket/raw_order_book.py +++ b/examples/websocket/raw_order_book.py @@ -7,7 +7,7 @@ from typing import List from bfxapi import Client, Constants from bfxapi.websocket.enums import Channels, Error -from bfxapi.websocket.typings import Subscriptions, TradingPairRawBook +from bfxapi.websocket.types import Subscriptions, TradingPairRawBook class RawOrderBook(object): def __init__(self, symbols: List[str]): @@ -18,7 +18,7 @@ class RawOrderBook(object): } def update(self, symbol: str, data: TradingPairRawBook) -> None: - order_id, price, amount = data["ORDER_ID"], data["PRICE"], data["AMOUNT"] + order_id, price, amount = data.ORDER_ID, data.PRICE, data.AMOUNT kind = (amount > 0) and "bids" or "asks" diff --git a/examples/websocket/ticker.py b/examples/websocket/ticker.py index 5db8ed1..92e7058 100644 --- a/examples/websocket/ticker.py +++ b/examples/websocket/ticker.py @@ -1,10 +1,8 @@ # python -c "from examples.websocket.ticker import *" -import asyncio - from bfxapi import Client, Constants from bfxapi.websocket.enums import Channels -from bfxapi.websocket.typings import Subscriptions, TradingPairTicker +from bfxapi.websocket.types import Subscriptions, TradingPairTicker bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) From 5fe4d83902e88d7f38e86e8573c03c6fc4eb130a Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 19 Jan 2023 18:00:51 +0100 Subject: [PATCH 107/265] Move subscriptions type hinting from bfxapi/websocket/types.py to bfxapi/websocket/subscriptions.py. --- bfxapi/labeler.py | 8 ++++-- bfxapi/websocket/handlers.py | 25 ++++++----------- bfxapi/websocket/subscriptions.py | 28 +++++++++++++++++++ bfxapi/websocket/types.py | 42 ---------------------------- examples/websocket/order_book.py | 7 +++-- examples/websocket/raw_order_book.py | 7 +++-- examples/websocket/ticker.py | 6 ++-- 7 files changed, 54 insertions(+), 69 deletions(-) create mode 100644 bfxapi/websocket/subscriptions.py diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 87889e2..d2fbdb5 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -5,9 +5,11 @@ from typing import Type, Generic, TypeVar, Iterable, Optional, List, Tuple, Any, T = TypeVar("T", bound="_Type") class _Type(object): - def __init__(self, **kwargs): - for key, value in kwargs.items(): - self.__setattr__(key, value) + """ + Base class for any dataclass serializable by the _Serializer generic class. + """ + + pass class _Serializer(Generic[T]): def __init__(self, name: str, klass: Type[_Type], labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]): diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index 33c2654..f2fd7ea 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -6,9 +6,6 @@ from . import serializers from .enums import Channels from .exceptions import BfxWebsocketException -def _get_sub_dictionary(dictionary, keys): - return { key: dictionary[key] for key in dictionary if key in keys } - class PublicChannelsHandler(object): EVENTS = [ "t_ticker_update", "f_ticker_update", @@ -30,21 +27,23 @@ class PublicChannelsHandler(object): } def handle(self, subscription, *stream): + _clear = lambda dictionary, *args: { key: value for key, value in dictionary.items() if key not in args } + if channel := subscription["channel"] or channel in self.__handlers.keys(): - return self.__handlers[channel](subscription, *stream) + return self.__handlers[channel](_clear(subscription, "event", "channel"), *stream) def __ticker_channel_handler(self, subscription, *stream): if subscription["symbol"].startswith("t"): return self.event_emitter.emit( "t_ticker_update", - _get_sub_dictionary(subscription, [ "chanId", "symbol", "pair" ]), + subscription, serializers.TradingPairTicker.parse(*stream[0]) ) if subscription["symbol"].startswith("f"): return self.event_emitter.emit( "f_ticker_update", - _get_sub_dictionary(subscription, [ "chanId", "symbol", "currency" ]), + subscription, serializers.FundingCurrencyTicker.parse(*stream[0]) ) @@ -53,34 +52,32 @@ class PublicChannelsHandler(object): if subscription["symbol"].startswith("t"): return self.event_emitter.emit( { "te": "t_trade_executed", "tu": "t_trade_execution_update" }[type], - _get_sub_dictionary(subscription, [ "chanId", "symbol", "pair" ]), + subscription, serializers.TradingPairTrade.parse(*stream[1]) ) if subscription["symbol"].startswith("f"): return self.event_emitter.emit( { "fte": "f_trade_executed", "ftu": "f_trade_execution_update" }[type], - _get_sub_dictionary(subscription, [ "chanId", "symbol", "currency" ]), + subscription, serializers.FundingCurrencyTrade.parse(*stream[1]) ) if subscription["symbol"].startswith("t"): return self.event_emitter.emit( "t_trades_snapshot", - _get_sub_dictionary(subscription, [ "chanId", "symbol", "pair" ]), + subscription, [ serializers.TradingPairTrade.parse(*substream) for substream in stream[0] ] ) if subscription["symbol"].startswith("f"): return self.event_emitter.emit( "f_trades_snapshot", - _get_sub_dictionary(subscription, [ "chanId", "symbol", "currency" ]), + subscription, [ serializers.FundingCurrencyTrade.parse(*substream) for substream in stream[0] ] ) def __book_channel_handler(self, subscription, *stream): - subscription = _get_sub_dictionary(subscription, [ "chanId", "symbol", "prec", "freq", "len", "subId", "pair" ]) - type = subscription["symbol"][0] if subscription["prec"] == "R0": @@ -101,8 +98,6 @@ class PublicChannelsHandler(object): ) def __candles_channel_handler(self, subscription, *stream): - subscription = _get_sub_dictionary(subscription, [ "chanId", "key" ]) - if all(isinstance(substream, list) for substream in stream[0]): return self.event_emitter.emit( "candles_snapshot", @@ -117,8 +112,6 @@ class PublicChannelsHandler(object): ) def __status_channel_handler(self, subscription, *stream): - subscription = _get_sub_dictionary(subscription, [ "chanId", "key" ]) - if subscription["key"].startswith("deriv:"): return self.event_emitter.emit( "derivatives_status_update", diff --git a/bfxapi/websocket/subscriptions.py b/bfxapi/websocket/subscriptions.py new file mode 100644 index 0000000..08b7b21 --- /dev/null +++ b/bfxapi/websocket/subscriptions.py @@ -0,0 +1,28 @@ +from typing import TypedDict, Optional + +class Ticker(TypedDict): + chanId: int; symbol: str + pair: Optional[str] + currency: Optional[str] + +class Trades(TypedDict): + chanId: int; symbol: str + pair: Optional[str] + currency: Optional[str] + +class Book(TypedDict): + chanId: int + symbol: str + prec: str + freq: str + len: str + subId: int + pair: str + +class Candles(TypedDict): + chanId: int + key: str + +class Status(TypedDict): + chanId: int + key: str \ No newline at end of file diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py index 41838d5..97ef37b 100644 --- a/bfxapi/websocket/types.py +++ b/bfxapi/websocket/types.py @@ -8,48 +8,6 @@ from ..notification import Notification JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] -#region Type hinting for subscription objects - -class Subscriptions: - class TradingPairTicker(TypedDict): - chanId: int - symbol: str - pair: str - - class FundingCurrencyTicker(TypedDict): - chanId: int - symbol: str - currency: str - - class TradingPairTrades(TypedDict): - chanId: int - symbol: str - pair: str - - class FundingCurrencyTrades(TypedDict): - chanId: int - symbol: str - currency: str - - class Book(TypedDict): - chanId: int - symbol: str - prec: str - freq: str - len: str - subId: int - pair: str - - class Candles(TypedDict): - chanId: int - key: str - - class DerivativesStatus(TypedDict): - chanId: int - key: str - -#endregion - #region Type hinting for Websocket Public Channels @dataclass diff --git a/examples/websocket/order_book.py b/examples/websocket/order_book.py index fb583e1..57d0c0a 100644 --- a/examples/websocket/order_book.py +++ b/examples/websocket/order_book.py @@ -6,8 +6,9 @@ from typing import List from bfxapi import Client, Constants +from bfxapi.websocket import subscriptions from bfxapi.websocket.enums import Channels, Error -from bfxapi.websocket.types import Subscriptions, TradingPairBook +from bfxapi.websocket.types import TradingPairBook class OrderBook(object): def __init__(self, symbols: List[str]): @@ -53,12 +54,12 @@ def on_subscribed(subscription): print(f"Subscription successful for pair <{subscription['pair']}>") @bfx.wss.on("t_book_snapshot") -def on_t_book_snapshot(subscription: Subscriptions.Book, snapshot: List[TradingPairBook]): +def on_t_book_snapshot(subscription: subscriptions.Book, snapshot: List[TradingPairBook]): for data in snapshot: order_book.update(subscription["symbol"], data) @bfx.wss.on("t_book_update") -def on_t_book_update(subscription: Subscriptions.Book, data: TradingPairBook): +def on_t_book_update(subscription: subscriptions.Book, data: TradingPairBook): order_book.update(subscription["symbol"], data) bfx.wss.run() \ No newline at end of file diff --git a/examples/websocket/raw_order_book.py b/examples/websocket/raw_order_book.py index 6819820..e9076cc 100644 --- a/examples/websocket/raw_order_book.py +++ b/examples/websocket/raw_order_book.py @@ -6,8 +6,9 @@ from typing import List from bfxapi import Client, Constants +from bfxapi.websocket import subscriptions from bfxapi.websocket.enums import Channels, Error -from bfxapi.websocket.types import Subscriptions, TradingPairRawBook +from bfxapi.websocket.types import TradingPairRawBook class RawOrderBook(object): def __init__(self, symbols: List[str]): @@ -53,12 +54,12 @@ def on_subscribed(subscription): print(f"Subscription successful for pair <{subscription['pair']}>") @bfx.wss.on("t_raw_book_snapshot") -def on_t_raw_book_snapshot(subscription: Subscriptions.Book, snapshot: List[TradingPairRawBook]): +def on_t_raw_book_snapshot(subscription: subscriptions.Book, snapshot: List[TradingPairRawBook]): for data in snapshot: raw_order_book.update(subscription["symbol"], data) @bfx.wss.on("t_raw_book_update") -def on_t_raw_book_update(subscription: Subscriptions.Book, data: TradingPairRawBook): +def on_t_raw_book_update(subscription: subscriptions.Book, data: TradingPairRawBook): raw_order_book.update(subscription["symbol"], data) bfx.wss.run() \ No newline at end of file diff --git a/examples/websocket/ticker.py b/examples/websocket/ticker.py index 92e7058..aeb21cc 100644 --- a/examples/websocket/ticker.py +++ b/examples/websocket/ticker.py @@ -1,13 +1,15 @@ # python -c "from examples.websocket.ticker import *" from bfxapi import Client, Constants + +from bfxapi.websocket import subscriptions from bfxapi.websocket.enums import Channels -from bfxapi.websocket.types import Subscriptions, TradingPairTicker +from bfxapi.websocket.types import TradingPairTicker bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) @bfx.wss.on("t_ticker_update") -def on_t_ticker_update(subscription: Subscriptions.TradingPairTicker, data: TradingPairTicker): +def on_t_ticker_update(subscription: subscriptions.Ticker, data: TradingPairTicker): print(f"Subscription with channel ID: {subscription['chanId']}") print(f"Data: {data}") From 36725a183e7a04670016946ed9dc051e84f8aad5 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 19 Jan 2023 18:12:12 +0100 Subject: [PATCH 108/265] Move _BfxWebsocketBucket class in its own file bfxapi/websocket/_BfxWebsocketBucket.py. --- bfxapi/websocket/BfxWebsocketClient.py | 86 ++---------------------- bfxapi/websocket/_BfxWebsocketBucket.py | 87 +++++++++++++++++++++++++ bfxapi/websocket/subscriptions.py | 8 +++ 3 files changed, 100 insertions(+), 81 deletions(-) create mode 100644 bfxapi/websocket/_BfxWebsocketBucket.py diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index c5b2f31..e5c7efb 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -4,27 +4,16 @@ 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 ConnectionNotOpen, TooManySubscriptions, WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion +from .exceptions import WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported from ..utils.encoder import JSONEncoder from ..utils.logger import Formatter, CustomLogger -_HEARTBEAT = "hb" - -F = TypeVar("F", bound=Callable[..., Literal[None]]) - -def _require_websocket_connection(function: F) -> F: - async def wrapper(self, *args, **kwargs): - if self.websocket == None or self.websocket.open == False: - raise ConnectionNotOpen("No open connection with the server.") - - await function(self, *args, **kwargs) - - return cast(F, wrapper) - def _require_websocket_authentication(function: F) -> F: async def wrapper(self, *args, **kwargs): if self.authentication == False: @@ -35,7 +24,7 @@ def _require_websocket_authentication(function: F) -> F: return cast(F, wrapper) class BfxWebsocketClient(object): - VERSION = 2 + VERSION = _BfxWebsocketBucket.VERSION MAXIMUM_BUCKETS_AMOUNT = 20 @@ -163,69 +152,4 @@ class BfxWebsocketClient(object): def handler(function): self.event_emitter.once(event, function) - return handler - -class _BfxWebsocketBucket(object): - MAXIMUM_SUBSCRIPTIONS_AMOUNT = 25 - - def __init__(self, host, event_emitter, __bucket_open_signal): - self.host, self.event_emitter, self.__bucket_open_signal = host, event_emitter, __bucket_open_signal - - self.websocket, self.subscriptions, self.pendings = None, dict(), list() - - self.handler = PublicChannelsHandler(event_emitter=self.event_emitter) - - async def _connect(self, index): - async for websocket in websockets.connect(self.host): - self.websocket = websocket - - self.__bucket_open_signal(index) - - try: - async for message in websocket: - message = json.loads(message) - - if isinstance(message, dict) and message["event"] == "info" and "version" in message: - if BfxWebsocketClient.VERSION != message["version"]: - raise OutdatedClientVersion(f"Mismatch between the client version and the server version. Update the library to the latest version to continue (client version: {BfxWebsocketClient.VERSION}, server version: {message['version']}).") - elif isinstance(message, dict) and message["event"] == "subscribed" and (chanId := message["chanId"]): - self.pendings = [ pending for pending in self.pendings if pending["subId"] != message["subId"] ] - self.subscriptions[chanId] = message - self.event_emitter.emit("subscribed", message) - elif isinstance(message, dict) and message["event"] == "unsubscribed" and (chanId := message["chanId"]): - if message["status"] == "OK": - del self.subscriptions[chanId] - elif isinstance(message, dict) and message["event"] == "error": - self.event_emitter.emit("wss-error", message["code"], message["msg"]) - elif isinstance(message, list) and (chanId := message[0]) and message[1] != _HEARTBEAT: - self.handler.handle(self.subscriptions[chanId], *message[1:]) - except websockets.ConnectionClosedError: continue - finally: await self.websocket.wait_closed(); break - - @_require_websocket_connection - async def _subscribe(self, channel, subId=None, **kwargs): - if len(self.subscriptions) + len(self.pendings) == _BfxWebsocketBucket.MAXIMUM_SUBSCRIPTIONS_AMOUNT: - raise TooManySubscriptions("The client has reached the maximum number of subscriptions.") - - subscription = { - "event": "subscribe", - "channel": channel, - "subId": subId or str(uuid.uuid4()), - - **kwargs - } - - self.pendings.append(subscription) - - await self.websocket.send(json.dumps(subscription)) - - @_require_websocket_connection - async def _unsubscribe(self, chanId): - await self.websocket.send(json.dumps({ - "event": "unsubscribe", - "chanId": chanId - })) - - @_require_websocket_connection - async def _close(self, code=1000, reason=str()): - await self.websocket.close(code=code, reason=reason) \ No newline at end of file + return handler \ No newline at end of file diff --git a/bfxapi/websocket/_BfxWebsocketBucket.py b/bfxapi/websocket/_BfxWebsocketBucket.py new file mode 100644 index 0000000..2cfe48c --- /dev/null +++ b/bfxapi/websocket/_BfxWebsocketBucket.py @@ -0,0 +1,87 @@ +import json, uuid, websockets + +from typing import Literal, TypeVar, Callable, cast + +from .handlers import PublicChannelsHandler + +from .exceptions import ConnectionNotOpen, TooManySubscriptions, OutdatedClientVersion + +_HEARTBEAT = "hb" + +F = TypeVar("F", bound=Callable[..., Literal[None]]) + +def _require_websocket_connection(function: F) -> F: + async def wrapper(self, *args, **kwargs): + if self.websocket == None or self.websocket.open == False: + raise ConnectionNotOpen("No open connection with the server.") + + await function(self, *args, **kwargs) + + return cast(F, wrapper) + +class _BfxWebsocketBucket(object): + VERSION = 2 + + MAXIMUM_SUBSCRIPTIONS_AMOUNT = 25 + + def __init__(self, host, event_emitter, __bucket_open_signal): + self.host, self.event_emitter, self.__bucket_open_signal = host, event_emitter, __bucket_open_signal + + self.websocket, self.subscriptions, self.pendings = None, dict(), list() + + self.handler = PublicChannelsHandler(event_emitter=self.event_emitter) + + async def _connect(self, index): + async for websocket in websockets.connect(self.host): + self.websocket = websocket + + self.__bucket_open_signal(index) + + try: + async for message in websocket: + message = json.loads(message) + + if isinstance(message, dict) and message["event"] == "info" and "version" in message: + if _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.subscriptions[chanId] = message + self.event_emitter.emit("subscribed", message) + elif isinstance(message, dict) and message["event"] == "unsubscribed" and (chanId := message["chanId"]): + if message["status"] == "OK": + del self.subscriptions[chanId] + elif isinstance(message, dict) and message["event"] == "error": + self.event_emitter.emit("wss-error", message["code"], message["msg"]) + elif isinstance(message, list) and (chanId := message[0]) and message[1] != _HEARTBEAT: + self.handler.handle(self.subscriptions[chanId], *message[1:]) + except websockets.ConnectionClosedError: continue + finally: await self.websocket.wait_closed(); break + + @_require_websocket_connection + async def _subscribe(self, channel, subId=None, **kwargs): + if len(self.subscriptions) + len(self.pendings) == _BfxWebsocketBucket.MAXIMUM_SUBSCRIPTIONS_AMOUNT: + raise TooManySubscriptions("The client has reached the maximum number of subscriptions.") + + subscription = { + "event": "subscribe", + "channel": channel, + "subId": subId or str(uuid.uuid4()), + + **kwargs + } + + self.pendings.append(subscription) + + await self.websocket.send(json.dumps(subscription)) + + @_require_websocket_connection + async def _unsubscribe(self, chanId): + await self.websocket.send(json.dumps({ + "event": "unsubscribe", + "chanId": chanId + })) + + @_require_websocket_connection + async def _close(self, code=1000, reason=str()): + await self.websocket.close(code=code, reason=reason) \ No newline at end of file diff --git a/bfxapi/websocket/subscriptions.py b/bfxapi/websocket/subscriptions.py index 08b7b21..2acc1af 100644 --- a/bfxapi/websocket/subscriptions.py +++ b/bfxapi/websocket/subscriptions.py @@ -1,5 +1,13 @@ from typing import TypedDict, Optional +__all__ = [ + "Ticker", + "Trades", + "Book", + "Candles", + "Status" +] + class Ticker(TypedDict): chanId: int; symbol: str pair: Optional[str] From ae42fb7d93cea286487ea2b7afebf10771bb0009 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Tue, 24 Jan 2023 12:10:57 +0100 Subject: [PATCH 109/265] Add _RecursiveSerializer class in bfxapi/labeler.py file. Add support to new pulse endpoints (with serializers and types). Add examples/rest/get_pulse_data.py demo. --- bfxapi/labeler.py | 22 +++++++++++++-- bfxapi/rest/BfxRestInterface.py | 15 ++++++++++- bfxapi/rest/serializers.py | 47 ++++++++++++++++++++++++++++++++- bfxapi/rest/types.py | 29 ++++++++++++++++++++ examples/rest/get_pulse_data.py | 22 +++++++++++++++ 5 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 examples/rest/get_pulse_data.py diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index d2fbdb5..52b0e2b 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -1,6 +1,6 @@ from .exceptions import LabelerSerializerException -from typing import Type, Generic, TypeVar, Iterable, Optional, List, Tuple, Any, cast +from typing import Type, Generic, TypeVar, Iterable, Optional, Dict, List, Tuple, Any, cast T = TypeVar("T", bound="_Type") @@ -28,5 +28,23 @@ class _Serializer(Generic[T]): def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: return cast(T, self.klass(**dict(self._serialize(*values, skip=skip)))) +class _RecursiveSerializer(_Serializer, Generic[T]): + def __init__(self, name: str, klass: Type[_Type], labels: List[str], serializers: Dict[str, Type[_Serializer]], IGNORE: List[str] = ["_PLACEHOLDER"]): + super().__init__(name, klass, labels, IGNORE) + + self.serializers = serializers + + def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: + serialization = dict(self._serialize(*values, skip=skip)) + + for key in serialization: + if key in self.serializers.keys(): + serialization[key] = self.serializers[key].parse(*serialization[key], skip=skip) + + return cast(T, self.klass(**serialization)) + def generate_labeler_serializer(name: str, klass: Type[T], labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> _Serializer[T]: - return _Serializer[T](name, klass, labels, IGNORE) \ No newline at end of file + return _Serializer[T](name, klass, labels, IGNORE) + +def generate_recursive_serializer(name: str, klass: Type[T], labels: List[str], serializers: Dict[str, Type[_Serializer]], IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> _RecursiveSerializer[T]: + return _RecursiveSerializer[T](name, klass, labels, serializers, IGNORE) \ No newline at end of file diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 741a3ec..bca18c1 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -58,7 +58,7 @@ class _Requests(object): raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") if data[1] == None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC: - raise UnknownGenericError("The server replied to the request with a generic error with message: <{data[2]}>.") + raise UnknownGenericError(f"The server replied to the request with a generic error with message: <{data[2]}>.") return data @@ -255,6 +255,19 @@ class _RestPublicEndpoints(_Requests): def conf(self, config: Config) -> Any: return self._GET(f"conf/{config}")[0] + def get_pulse_profile(self, nickname: str) -> PulseProfile: + return serializers.PulseProfile.parse(*self._GET(f"pulse/profile/{nickname}")) + + def get_pulse_history(self, end: Optional[str] = None, limit: Optional[int] = None) -> List[PulseMessage]: + messages = list() + + for subdata in self._GET("pulse/hist", params={ "end": end, "limit": limit }): + subdata[18] = subdata[18][0] + message = serializers.PulseMessage.parse(*subdata) + messages.append(message) + + return messages + class _RestAuthenticatedEndpoints(_Requests): def get_wallets(self) -> List[Wallet]: return [ serializers.Wallet.parse(*subdata) for subdata in self._POST("auth/r/wallets") ] diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 7cea3db..5209d6f 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -1,6 +1,6 @@ from . import types -from .. labeler import generate_labeler_serializer +from .. labeler import generate_labeler_serializer, generate_recursive_serializer from .. notification import _Notification @@ -185,6 +185,51 @@ FundingStatistic = generate_labeler_serializer("FundingStatistic", klass=types.F "FUNDING_BELOW_THRESHOLD" ]) +PulseProfile = generate_labeler_serializer("PulseProfile", klass=types.PulseProfile, labels=[ + "PUID", + "MTS", + "_PLACEHOLDER", + "NICKNAME", + "_PLACEHOLDER", + "PICTURE", + "TEXT", + "_PLACEHOLDER", + "_PLACEHOLDER", + "TWITTER_HANDLE", + "_PLACEHOLDER", + "FOLLOWERS", + "FOLLOWING", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "TIPPING_STATUS" +]) + +PulseMessage = generate_recursive_serializer("PulseMessage", klass=types.PulseMessage, serializers={ "PROFILE": PulseProfile }, labels=[ + "PID", + "MTS", + "_PLACEHOLDER", + "PUID", + "_PLACEHOLDER", + "TITLE", + "CONTENT", + "_PLACEHOLDER", + "_PLACEHOLDER", + "IS_PIN", + "IS_PUBLIC", + "COMMENTS_DISABLED", + "TAGS", + "ATTACHMENTS", + "META", + "LIKES", + "_PLACEHOLDER", + "_PLACEHOLDER", + "PROFILE", + "COMMENTS", + "_PLACEHOLDER", + "_PLACEHOLDER" +]) + #endregion #region Serializers definition for Rest Authenticated Endpoints diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 9cd14b9..7193d5c 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -152,6 +152,35 @@ class FundingStatistic(_Type): FUNDING_AMOUNT_USED: float FUNDING_BELOW_THRESHOLD: float +@dataclass +class PulseProfile(_Type): + PUID: str + MTS: int + NICKNAME: str + PICTURE: str + TEXT: str + TWITTER_HANDLE: str + FOLLOWERS: int + FOLLOWING: int + TIPPING_STATUS: int + +@dataclass +class PulseMessage(_Type): + PID: str + MTS: int + PUID: str + TITLE: str + CONTENT: str + IS_PIN: int + IS_PUBLIC: int + COMMENTS_DISABLED: int + TAGS: List[str] + ATTACHMENTS: List[str] + META: List[JSON] + LIKES: int + PROFILE: PulseProfile + COMMENTS: int + #endregion #region Type hinting for Rest Authenticated Endpoints diff --git a/examples/rest/get_pulse_data.py b/examples/rest/get_pulse_data.py new file mode 100644 index 0000000..fc5c15f --- /dev/null +++ b/examples/rest/get_pulse_data.py @@ -0,0 +1,22 @@ +# python -c "from examples.rest.get_pulse_data import *" + +import time + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST +) + +now = int(round(time.time() * 1000)) + +messages = bfx.rest.public.get_pulse_history(end=now, limit=100) + +for message in messages: + print(f"Message: {message}") + print(message.CONTENT) + print(message.PROFILE.PICTURE) + +profile = bfx.rest.public.get_pulse_profile("News") +print(f"Profile: {profile}") +print(f"Profile picture: {profile.PICTURE}") \ No newline at end of file From 02a2e962d303fa371a6ed0412079898fbf0a603c Mon Sep 17 00:00:00 2001 From: itsdeka Date: Tue, 24 Jan 2023 12:35:12 +0100 Subject: [PATCH 110/265] Add support to new calculation endpoints. --- bfxapi/rest/BfxRestInterface.py | 19 +++++++++++++++++-- bfxapi/rest/serializers.py | 11 +++++++++++ bfxapi/rest/types.py | 10 ++++++++++ examples/rest/extra_calcs.py | 24 ++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 examples/rest/extra_calcs.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index bca18c1..3dc559f 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -62,13 +62,13 @@ class _Requests(object): return data - def _POST(self, endpoint, params = None, data = None, _append_authentication_headers = True): + def _POST(self, endpoint, params = None, data = None): headers = { "Content-Type": "application/json" } if isinstance(data, dict): data = json.dumps({ key: value for key, value in data.items() if value != None}, cls=JSONEncoder) - if _append_authentication_headers: + if self.API_KEY and self.API_SECRET: headers = { **headers, **self.__build_authentication_headers(endpoint, data) } response = requests.post(f"{self.host}/{endpoint}", params=params, data=data, headers=headers) @@ -268,6 +268,21 @@ class _RestPublicEndpoints(_Requests): return messages + def get_trading_market_average_price(self, symbol: str, amount: Union[Decimal, str], price_limit: Optional[Union[Decimal, str]] = None) -> TradingMarketAveragePrice: + data = { + "symbol": symbol, "amount": amount, "price_limit": price_limit + } + + return serializers.TradingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data=data)) + + def get_funding_market_average_price(self, symbol: str, amount: Union[Decimal, str], period: int, rate_limit: Optional[Union[Decimal, str]] = None) -> FundingMarketAveragePrice: + data = { + "symbol": symbol, "amount": amount, "period": period, + "rate_limit": rate_limit + } + + return serializers.FundingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data=data)) + class _RestAuthenticatedEndpoints(_Requests): def get_wallets(self) -> List[Wallet]: return [ serializers.Wallet.parse(*subdata) for subdata in self._POST("auth/r/wallets") ] diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 5209d6f..822e4cf 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -230,6 +230,17 @@ PulseMessage = generate_recursive_serializer("PulseMessage", klass=types.PulseMe "_PLACEHOLDER" ]) +TradingMarketAveragePrice = generate_labeler_serializer("TradingMarketAveragePrice", klass=types.TradingMarketAveragePrice, labels=[ + "PRICE_AVG", + "AMOUNT" +]) + + +FundingMarketAveragePrice = generate_labeler_serializer("FundingMarketAveragePrice", klass=types.FundingMarketAveragePrice, labels=[ + "RATE_AVG", + "AMOUNT" +]) + #endregion #region Serializers definition for Rest Authenticated Endpoints diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 7193d5c..c0a2611 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -181,6 +181,16 @@ class PulseMessage(_Type): PROFILE: PulseProfile COMMENTS: int +@dataclass +class TradingMarketAveragePrice(_Type): + PRICE_AVG: float + AMOUNT: float + +@dataclass +class FundingMarketAveragePrice(_Type): + RATE_AVG: float + AMOUNT: float + #endregion #region Type hinting for Rest Authenticated Endpoints diff --git a/examples/rest/extra_calcs.py b/examples/rest/extra_calcs.py new file mode 100644 index 0000000..cbda41c --- /dev/null +++ b/examples/rest/extra_calcs.py @@ -0,0 +1,24 @@ +# python -c "from examples.rest.extra_calcs import *" + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST +) + +t_symbol_response = bfx.rest.public.get_trading_market_average_price( + symbol="tBTCUSD", + amount=-100, + price_limit="20000.5" +) + +print(t_symbol_response) + +f_symbol_response = bfx.rest.public.get_funding_market_average_price( + symbol="fUSD", + amount=100, + period=2, + rate_limit="0.00015" +) + +print(f_symbol_response) \ No newline at end of file From 01c8192d10de65cd32f27eec7280241fa827f556 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Tue, 24 Jan 2023 12:49:01 +0100 Subject: [PATCH 111/265] fx rate --- bfxapi/rest/BfxRestInterface.py | 3 +++ bfxapi/rest/serializers.py | 4 ++++ bfxapi/rest/types.py | 4 ++++ examples/rest/extra_calcs.py | 8 ++++++-- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 3dc559f..4e63aba 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -283,6 +283,9 @@ class _RestPublicEndpoints(_Requests): return serializers.FundingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data=data)) + def get_fx_rate(self, ccy1: str, ccy2: str) -> FxRate: + return serializers.FxRate.parse(*self._POST("calc/fx", data={ "ccy1": ccy1, "ccy2": ccy2 })) + class _RestAuthenticatedEndpoints(_Requests): def get_wallets(self) -> List[Wallet]: return [ serializers.Wallet.parse(*subdata) for subdata in self._POST("auth/r/wallets") ] diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 822e4cf..3a69c53 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -241,6 +241,10 @@ FundingMarketAveragePrice = generate_labeler_serializer("FundingMarketAveragePri "AMOUNT" ]) +FxRate = generate_labeler_serializer("FxRate", klass=types.FxRate, labels=[ + "CURRENT_RATE" +]) + #endregion #region Serializers definition for Rest Authenticated Endpoints diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index c0a2611..4b04661 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -191,6 +191,10 @@ class FundingMarketAveragePrice(_Type): RATE_AVG: float AMOUNT: float +@dataclass +class FxRate(_Type): + CURRENT_RATE: float + #endregion #region Type hinting for Rest Authenticated Endpoints diff --git a/examples/rest/extra_calcs.py b/examples/rest/extra_calcs.py index cbda41c..2603cd8 100644 --- a/examples/rest/extra_calcs.py +++ b/examples/rest/extra_calcs.py @@ -12,7 +12,7 @@ t_symbol_response = bfx.rest.public.get_trading_market_average_price( price_limit="20000.5" ) -print(t_symbol_response) +print(t_symbol_response.PRICE_AVG) f_symbol_response = bfx.rest.public.get_funding_market_average_price( symbol="fUSD", @@ -21,4 +21,8 @@ f_symbol_response = bfx.rest.public.get_funding_market_average_price( rate_limit="0.00015" ) -print(f_symbol_response) \ No newline at end of file +print(f_symbol_response.RATE_AVG) + +fx_rate = bfx.rest.public.get_fx_rate(ccy1="USD", ccy2="EUR") + +print(fx_rate.CURRENT_RATE) \ No newline at end of file From 8047e3609d74f1f979de68c6429c425d4611d9e3 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Tue, 24 Jan 2023 15:23:48 +0100 Subject: [PATCH 112/265] rename subdata to sub_data, add margin info endpoints --- bfxapi/rest/BfxRestInterface.py | 71 ++++++++++++++----------- bfxapi/rest/serializers.py | 16 ++++++ bfxapi/rest/types.py | 16 ++++++ examples/rest/get_authenticated_data.py | 10 ++++ 4 files changed, 83 insertions(+), 30 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 4e63aba..347833b 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -99,11 +99,11 @@ class _RestPublicEndpoints(_Requests): parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse } - return [ cast(Union[TradingPairTicker, FundingCurrencyTicker], parsers[subdata[0][0]](*subdata)) for subdata in data ] + return [ cast(Union[TradingPairTicker, FundingCurrencyTicker], parsers[sub_data[0][0]](*sub_data)) for sub_data in data ] def get_t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]: if isinstance(pairs, str) and pairs == "ALL": - return [ cast(TradingPairTicker, subdata) for subdata in self.get_tickers([ "ALL" ]) if cast(str, subdata.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 ]) @@ -111,7 +111,7 @@ class _RestPublicEndpoints(_Requests): def get_f_tickers(self, currencies: Union[List[str], Literal["ALL"]]) -> List[FundingCurrencyTicker]: if isinstance(currencies, str) and currencies == "ALL": - return [ cast(FundingCurrencyTicker, subdata) for subdata in self.get_tickers([ "ALL" ]) if cast(str, subdata.SYMBOL).startswith("f") ] + 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 ]) @@ -132,29 +132,29 @@ class _RestPublicEndpoints(_Requests): data = self._GET("tickers/hist", params=params) - return [ serializers.TickersHistory.parse(*subdata) for subdata in data ] + return [ serializers.TickersHistory.parse(*sub_data) for sub_data in data ] def get_t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[TradingPairTrade]: params = { "limit": limit, "start": start, "end": end, "sort": sort } data = self._GET(f"trades/{'t' + pair}/hist", params=params) - return [ serializers.TradingPairTrade.parse(*subdata) for subdata in data ] + return [ serializers.TradingPairTrade.parse(*sub_data) for sub_data in data ] def get_f_trades(self, currency: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[FundingCurrencyTrade]: params = { "limit": limit, "start": start, "end": end, "sort": sort } data = self._GET(f"trades/{'f' + currency}/hist", params=params) - return [ serializers.FundingCurrencyTrade.parse(*subdata) for subdata 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]: - return [ serializers.TradingPairBook.parse(*subdata) for subdata 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/{'t' + pair}/{precision}", params={ "len": len }) ] def get_f_book(self, currency: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyBook]: - return [ serializers.FundingCurrencyBook.parse(*subdata) for subdata in self._GET(f"book/{'f' + currency}/{precision}", params={ "len": len }) ] + return [ serializers.FundingCurrencyBook.parse(*sub_data) for sub_data in self._GET(f"book/{'f' + currency}/{precision}", params={ "len": len }) ] def get_t_raw_book(self, pair: str, len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairRawBook]: - return [ serializers.TradingPairRawBook.parse(*subdata) for subdata in self._GET(f"book/{'t' + pair}/R0", params={ "len": len }) ] + return [ serializers.TradingPairRawBook.parse(*sub_data) for sub_data in self._GET(f"book/{'t' + pair}/R0", params={ "len": len }) ] def get_f_raw_book(self, currency: str, len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyRawBook]: - return [ serializers.FundingCurrencyRawBook.parse(*subdata) for subdata in self._GET(f"book/{'f' + currency}/R0", params={ "len": len }) ] + return [ serializers.FundingCurrencyRawBook.parse(*sub_data) for sub_data in self._GET(f"book/{'f' + currency}/R0", params={ "len": len }) ] def get_stats_hist( self, @@ -163,7 +163,7 @@ class _RestPublicEndpoints(_Requests): ) -> List[Statistic]: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self._GET(f"stats1/{resource}/hist", params=params) - return [ serializers.Statistic.parse(*subdata) for subdata in data ] + return [ serializers.Statistic.parse(*sub_data) for sub_data in data ] def get_stats_last( self, @@ -181,7 +181,7 @@ class _RestPublicEndpoints(_Requests): ) -> List[Candle]: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self._GET(f"candles/trade:{tf}:{symbol}/hist", params=params) - return [ serializers.Candle.parse(*subdata) for subdata in data ] + return [ serializers.Candle.parse(*sub_data) for sub_data in data ] def get_candles_last( self, @@ -199,7 +199,7 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"status/deriv", params=params) - return [ serializers.DerivativesStatus.parse(*subdata) for subdata in data ] + return [ serializers.DerivativesStatus.parse(*sub_data) for sub_data in data ] def get_derivatives_status_history( self, @@ -210,14 +210,14 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"status/{type}/{symbol}/hist", params=params) - return [ serializers.DerivativesStatus.parse(*subdata, skip=[ "KEY" ]) for subdata in data ] + return [ serializers.DerivativesStatus.parse(*sub_data, skip=[ "KEY" ]) for sub_data in data ] def get_liquidations(self, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Liquidation]: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self._GET("liquidations/hist", params=params) - return [ serializers.Liquidation.parse(*subdata[0]) for subdata in data ] + return [ serializers.Liquidation.parse(*sub_data[0]) for sub_data in data ] def get_seed_candles(self, symbol: str, tf: str = '1m', sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Candle]: @@ -225,7 +225,7 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"candles/trade:{tf}:{symbol}/hist?limit={limit}&start={start}&end={end}&sort={sort}", params=params) - return [ serializers.Candle.parse(*subdata) for subdata in data ] + return [ serializers.Candle.parse(*sub_data) for sub_data in data ] def get_leaderboards_hist( self, @@ -234,7 +234,7 @@ class _RestPublicEndpoints(_Requests): ) -> List[Leaderboard]: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self._GET(f"rankings/{resource}/hist", params=params) - return [ serializers.Leaderboard.parse(*subdata) for subdata in data ] + return [ serializers.Leaderboard.parse(*sub_data) for sub_data in data ] def get_leaderboards_last( self, @@ -250,7 +250,7 @@ class _RestPublicEndpoints(_Requests): data = self._GET(f"funding/stats/{symbol}/hist", params=params) - return [ serializers.FundingStatistic.parse(*subdata) for subdata in data ] + return [ serializers.FundingStatistic.parse(*sub_data) for sub_data in data ] def conf(self, config: Config) -> Any: return self._GET(f"conf/{config}")[0] @@ -288,7 +288,7 @@ class _RestPublicEndpoints(_Requests): class _RestAuthenticatedEndpoints(_Requests): def get_wallets(self) -> List[Wallet]: - return [ serializers.Wallet.parse(*subdata) for subdata in self._POST("auth/r/wallets") ] + return [ serializers.Wallet.parse(*sub_data) for sub_data in self._POST("auth/r/wallets") ] def get_orders(self, symbol: Optional[str] = None, ids: Optional[List[str]] = None) -> List[Order]: endpoint = "auth/r/orders" @@ -296,10 +296,10 @@ class _RestAuthenticatedEndpoints(_Requests): if symbol != None: endpoint += f"/{symbol}" - return [ serializers.Order.parse(*subdata) for subdata in self._POST(endpoint, data={ "id": ids }) ] + return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, data={ "id": ids }) ] def get_positions(self) -> List[Position]: - return [ serializers.Position.parse(*subdata) for subdata in self._POST("auth/r/positions") ] + return [ serializers.Position.parse(*sub_data) for sub_data in self._POST("auth/r/positions") ] def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str], price: Optional[Union[Decimal, str]] = None, lev: Optional[int] = None, @@ -360,7 +360,7 @@ class _RestAuthenticatedEndpoints(_Requests): "limit": limit } - return [ serializers.Order.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] + return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] def get_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Trade]: if symbol == None: @@ -373,10 +373,10 @@ class _RestAuthenticatedEndpoints(_Requests): "limit": limit } - return [ serializers.Trade.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] + return [ serializers.Trade.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] def get_order_trades(self, symbol: str, id: int) -> List[OrderTrade]: - return [ serializers.OrderTrade.parse(*subdata) for subdata in self._POST(f"auth/r/order/{symbol}:{id}/trades") ] + return [ serializers.OrderTrade.parse(*sub_data) for sub_data in self._POST(f"auth/r/order/{symbol}:{id}/trades") ] def get_ledgers(self, currency: str, category: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Ledger]: data = { @@ -385,7 +385,7 @@ class _RestAuthenticatedEndpoints(_Requests): "limit": limit } - return [ serializers.Ledger.parse(*subdata) for subdata in self._POST(f"auth/r/ledgers/{currency}/hist", data=data) ] + return [ serializers.Ledger.parse(*sub_data) for sub_data in self._POST(f"auth/r/ledgers/{currency}/hist", data=data) ] def get_funding_offers(self, symbol: Optional[str] = None) -> List[FundingOffer]: endpoint = "auth/r/funding/offers" @@ -393,7 +393,7 @@ class _RestAuthenticatedEndpoints(_Requests): if symbol != None: endpoint += f"/{symbol}" - return [ serializers.FundingOffer.parse(*subdata) for subdata in self._POST(endpoint) ] + return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint) ] def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, str], rate: Union[Decimal, str], period: int, @@ -419,14 +419,14 @@ class _RestAuthenticatedEndpoints(_Requests): "limit": limit } - return [ serializers.FundingOffer.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] + return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] def get_funding_credits(self, symbol: Optional[str] = None) -> List[FundingCredit]: if symbol == None: endpoint = "auth/r/funding/credits" else: endpoint = f"auth/r/funding/credits/{symbol}" - return [ serializers.FundingCredit.parse(*subdata) for subdata in self._POST(endpoint) ] + return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint) ] def get_funding_credits_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingCredit]: if symbol == None: @@ -438,7 +438,7 @@ class _RestAuthenticatedEndpoints(_Requests): "limit": limit } - return [ serializers.FundingCredit.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] + return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] def submit_wallet_transfer(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, str]) -> Notification[Transfer]: data = { @@ -484,4 +484,15 @@ class _RestAuthenticatedEndpoints(_Requests): "limit": limit } - return [ serializers.Movement.parse(*subdata) for subdata in self._POST(endpoint, data=data) ] \ No newline at end of file + return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + + def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo: + data = self._POST(f"auth/r/info/margin/{symbol}") + + return serializers.SymbolMarginInfo.parse(*([data[1]] + data[2])) + + def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]: + return [ serializers.SymbolMarginInfo.parse(*([sub_data[1]] + sub_data[2])) for sub_data in self._POST(f"auth/r/info/margin/sym_all") ] + + def get_base_margin_info(self) -> BaseMarginInfo: + return serializers.BaseMarginInfo.parse(*(self._POST(f"auth/r/info/margin/base")[1])) diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 3a69c53..044eb51 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -473,4 +473,20 @@ Movement = generate_labeler_serializer("Movement", klass=types.Movement, labels= "WITHDRAW_TRANSACTION_NOTE" ]) +SymbolMarginInfo = generate_labeler_serializer("SymbolMarginInfo", klass=types.SymbolMarginInfo, labels=[ + "SYMBOL", + "TRADABLE_BALANCE", + "GROSS_BALANCE", + "BUY", + "SELL" +]) + +BaseMarginInfo = generate_labeler_serializer("BaseMarginInfo", klass=types.BaseMarginInfo, labels=[ + "USER_PL", + "USER_SWAPS", + "MARGIN_BALANCE", + "MARGIN_NET", + "MARGIN_MIN" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 4b04661..2387bea 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -373,5 +373,21 @@ class Movement(_Type): DESTINATION_ADDRESS: str TRANSACTION_ID: str WITHDRAW_TRANSACTION_NOTE: str + +@dataclass +class SymbolMarginInfo(_Type): + SYMBOL: str + TRADABLE_BALANCE: float + GROSS_BALANCE: float + BUY: float + SELL: float + +@dataclass +class BaseMarginInfo(_Type): + USER_PL: float + USER_SWAPS: float + MARGIN_BALANCE: float + MARGIN_NET: float + MARGIN_MIN: float #endregion \ No newline at end of file diff --git a/examples/rest/get_authenticated_data.py b/examples/rest/get_authenticated_data.py index 881cbf6..79a24d9 100644 --- a/examples/rest/get_authenticated_data.py +++ b/examples/rest/get_authenticated_data.py @@ -85,6 +85,15 @@ def log_funding_credits_history(): print("Funding credit history:") [print(c) for c in credit] +def log_margin_info(): + btcusd_margin_info = bfx.rest.auth.get_symbol_margin_info('tBTCUSD') + print(f"tBTCUSD margin info {btcusd_margin_info}") + + sym_all_margin_info = bfx.rest.auth.get_all_symbols_margin_info() + print(f"Sym all margin info {sym_all_margin_info}") + + base_margin_info = bfx.rest.auth.get_base_margin_info() + print(f"Base margin info {base_margin_info}") def run(): log_wallets() @@ -97,5 +106,6 @@ def run(): log_funding_offer_history() log_funding_credits() log_funding_credits_history() + log_margin_info() run() \ No newline at end of file From 28c00d460b6155787c60d7a4e7a4b3cb09de20de Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 24 Jan 2023 19:02:44 +0100 Subject: [PATCH 113/265] Exclude subId field from every subscription dictionary. --- bfxapi/websocket/handlers.py | 2 +- bfxapi/websocket/subscriptions.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index f2fd7ea..c56046a 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -30,7 +30,7 @@ class PublicChannelsHandler(object): _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(): - return self.__handlers[channel](_clear(subscription, "event", "channel"), *stream) + return self.__handlers[channel](_clear(subscription, "event", "channel", "subId"), *stream) def __ticker_channel_handler(self, subscription, *stream): if subscription["symbol"].startswith("t"): diff --git a/bfxapi/websocket/subscriptions.py b/bfxapi/websocket/subscriptions.py index 2acc1af..e22bb5e 100644 --- a/bfxapi/websocket/subscriptions.py +++ b/bfxapi/websocket/subscriptions.py @@ -24,7 +24,6 @@ class Book(TypedDict): prec: str freq: str len: str - subId: int pair: str class Candles(TypedDict): From ed12bf473f4cad24a77ec58949e99c48dc9dedad Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 25 Jan 2023 18:18:15 +0100 Subject: [PATCH 114/265] Rewrite and extend custom JSONEncoder in bfxapi/utils/encoder.py to automatically convert floats to strs. Change every Union[Decimal, str] type to Union[Decimal, float, str]. Fix type hinting bug in labeler.py. --- bfxapi/labeler.py | 4 ++-- bfxapi/rest/BfxRestInterface.py | 26 ++++++++++++------------- bfxapi/rest/types.py | 4 +--- bfxapi/utils/encoder.py | 26 +++++++++++++++++++++++-- bfxapi/websocket/_BfxWebsocketInputs.py | 16 +++++++-------- bfxapi/websocket/types.py | 4 +--- 6 files changed, 49 insertions(+), 31 deletions(-) diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 52b0e2b..6201b82 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -29,7 +29,7 @@ class _Serializer(Generic[T]): return cast(T, self.klass(**dict(self._serialize(*values, skip=skip)))) class _RecursiveSerializer(_Serializer, Generic[T]): - def __init__(self, name: str, klass: Type[_Type], labels: List[str], serializers: Dict[str, Type[_Serializer]], IGNORE: List[str] = ["_PLACEHOLDER"]): + def __init__(self, name: str, klass: Type[_Type], labels: List[str], serializers: Dict[str, _Serializer[Any]], IGNORE: List[str] = ["_PLACEHOLDER"]): super().__init__(name, klass, labels, IGNORE) self.serializers = serializers @@ -46,5 +46,5 @@ class _RecursiveSerializer(_Serializer, Generic[T]): def generate_labeler_serializer(name: str, klass: Type[T], labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> _Serializer[T]: return _Serializer[T](name, klass, labels, IGNORE) -def generate_recursive_serializer(name: str, klass: Type[T], labels: List[str], serializers: Dict[str, Type[_Serializer]], IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> _RecursiveSerializer[T]: +def generate_recursive_serializer(name: str, klass: Type[T], labels: List[str], serializers: Dict[str, _Serializer[Any]], IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> _RecursiveSerializer[T]: return _RecursiveSerializer[T](name, klass, labels, serializers, IGNORE) \ No newline at end of file diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 347833b..88a08f5 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -268,14 +268,14 @@ class _RestPublicEndpoints(_Requests): return messages - def get_trading_market_average_price(self, symbol: str, amount: Union[Decimal, str], price_limit: Optional[Union[Decimal, str]] = None) -> TradingMarketAveragePrice: + def get_trading_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], price_limit: Optional[Union[Decimal, float, str]] = None) -> TradingMarketAveragePrice: data = { "symbol": symbol, "amount": amount, "price_limit": price_limit } return serializers.TradingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data=data)) - def get_funding_market_average_price(self, symbol: str, amount: Union[Decimal, str], period: int, rate_limit: Optional[Union[Decimal, str]] = None) -> FundingMarketAveragePrice: + def get_funding_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], period: int, rate_limit: Optional[Union[Decimal, float, str]] = None) -> FundingMarketAveragePrice: data = { "symbol": symbol, "amount": amount, "period": period, "rate_limit": rate_limit @@ -301,9 +301,9 @@ class _RestAuthenticatedEndpoints(_Requests): def get_positions(self) -> List[Position]: return [ serializers.Position.parse(*sub_data) for sub_data in self._POST("auth/r/positions") ] - def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str], - price: Optional[Union[Decimal, str]] = None, lev: Optional[int] = None, - price_trailing: Optional[Union[Decimal, str]] = None, price_aux_limit: Optional[Union[Decimal, str]] = None, price_oco_stop: Optional[Union[Decimal, str]] = None, + def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, float, str], + price: Optional[Union[Decimal, float, str]] = None, lev: Optional[int] = None, + price_trailing: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_oco_stop: Optional[Union[Decimal, float, str]] = None, gid: Optional[int] = None, cid: Optional[int] = None, flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification[Order]: data = { @@ -316,10 +316,10 @@ class _RestAuthenticatedEndpoints(_Requests): return serializers._Notification[Order](serializer=serializers.Order).parse(*self._POST("auth/w/order/submit", data=data)) - def update_order(self, id: int, amount: Optional[Union[Decimal, str]] = None, price: Optional[Union[Decimal, str]] = None, + def update_order(self, id: int, amount: Optional[Union[Decimal, float, str]] = None, price: Optional[Union[Decimal, float, str]] = None, cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, - flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, str]] = None, - price_aux_limit: Optional[Union[Decimal, str]] = None, price_trailing: Optional[Union[Decimal, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification[Order]: + flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, float, str]] = None, + price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification[Order]: data = { "id": id, "amount": amount, "price": price, "cid": cid, "cid_date": cid_date, "gid": gid, @@ -395,8 +395,8 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint) ] - def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, str], - rate: Union[Decimal, str], period: int, + def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, float, str], + rate: Union[Decimal, float, str], period: int, flags: Optional[int] = 0) -> Notification[FundingOffer]: data = { "type": type, "symbol": symbol, "amount": amount, @@ -440,7 +440,7 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] - def submit_wallet_transfer(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, str]) -> Notification[Transfer]: + def submit_wallet_transfer(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, float, str]) -> Notification[Transfer]: data = { "from": from_wallet, "to": to_wallet, "currency": currency, "currency_to": currency_to, @@ -449,7 +449,7 @@ class _RestAuthenticatedEndpoints(_Requests): return serializers._Notification[Transfer](serializer=serializers.Transfer).parse(*self._POST("auth/w/transfer", data=data)) - def submit_wallet_withdraw(self, wallet: str, method: str, address: str, amount: Union[Decimal, str]) -> Notification[Withdrawal]: + def submit_wallet_withdraw(self, wallet: str, method: str, address: str, amount: Union[Decimal, float, str]) -> Notification[Withdrawal]: data = { "wallet": wallet, "method": method, "address": address, "amount": amount, @@ -466,7 +466,7 @@ class _RestAuthenticatedEndpoints(_Requests): return serializers._Notification[DepositAddress](serializer=serializers.DepositAddress).parse(*self._POST("auth/w/deposit/address", data=data)) - def get_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, str]) -> Invoice: + def get_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, float, str]) -> Invoice: data = { "wallet": wallet, "currency": currency, "amount": amount diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 2387bea..f9dc96d 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -3,10 +3,8 @@ from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any from dataclasses import dataclass from .. labeler import _Type - from .. notification import Notification - -JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] +from .. utils.encoder import JSON #region Type hinting for Rest Public Endpoints diff --git a/bfxapi/utils/encoder.py b/bfxapi/utils/encoder.py index 3649823..885ab91 100644 --- a/bfxapi/utils/encoder.py +++ b/bfxapi/utils/encoder.py @@ -2,8 +2,30 @@ import json from decimal import Decimal from datetime import datetime +from typing import Type, List, Dict, Union, Any + +JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] + class JSONEncoder(json.JSONEncoder): - def default(self, obj): - if isinstance(obj, Decimal) or isinstance(obj, datetime): + def encode(self, obj: JSON) -> str: + def _convert_float_to_str(data: JSON) -> JSON: + if isinstance(data, float): + return format(Decimal(repr(data)), "f") + elif isinstance(data, list): + return [ _convert_float_to_str(sub_data) for sub_data in data ] + elif isinstance(data, dict): + return { key: _convert_float_to_str(value) for key, value in data.items() } + else: return data + + data = _convert_float_to_str(obj) + + return json.JSONEncoder.encode(self, data) + + def default(self, obj: Any) -> Any: + if isinstance(obj, Decimal): + return format(obj, "f") + + if isinstance(obj, datetime): return str(obj) + return json.JSONEncoder.default(self, obj) \ No newline at end of file diff --git a/bfxapi/websocket/_BfxWebsocketInputs.py b/bfxapi/websocket/_BfxWebsocketInputs.py index 85e29b0..041405f 100644 --- a/bfxapi/websocket/_BfxWebsocketInputs.py +++ b/bfxapi/websocket/_BfxWebsocketInputs.py @@ -12,9 +12,9 @@ class _BfxWebsocketInputs(object): def __init__(self, __handle_websocket_input): self.__handle_websocket_input = __handle_websocket_input - async def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, str], - price: Optional[Union[Decimal, str]] = None, lev: Optional[int] = None, - price_trailing: Optional[Union[Decimal, str]] = None, price_aux_limit: Optional[Union[Decimal, str]] = None, price_oco_stop: Optional[Union[Decimal, str]] = None, + async def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, float, str], + price: Optional[Union[Decimal, float, str]] = None, lev: Optional[int] = None, + price_trailing: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_oco_stop: Optional[Union[Decimal, float, str]] = None, gid: Optional[int] = None, cid: Optional[int] = None, flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None): data = _strip({ @@ -27,10 +27,10 @@ class _BfxWebsocketInputs(object): await self.__handle_websocket_input("on", data) - async def update_order(self, id: int, amount: Optional[Union[Decimal, str]] = None, price: Optional[Union[Decimal, str]] = None, + async def update_order(self, id: int, amount: Optional[Union[Decimal, float, str]] = None, price: Optional[Union[Decimal, float, str]] = None, cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, - flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, str]] = None, - price_aux_limit: Optional[Union[Decimal, str]] = None, price_trailing: Optional[Union[Decimal, str]] = None, tif: Optional[Union[datetime, str]] = None): + 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): data = _strip({ "id": id, "amount": amount, "price": price, "cid": cid, "cid_date": cid_date, "gid": gid, @@ -60,8 +60,8 @@ class _BfxWebsocketInputs(object): await self.__handle_websocket_input("oc_multi", data) - async def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, str], - rate: Union[Decimal, str], period: int, + async def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, float, str], + rate: Union[Decimal, float, str], period: int, flags: Optional[int] = 0): data = { "type": type, "symbol": symbol, "amount": amount, diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py index 97ef37b..19ed3ef 100644 --- a/bfxapi/websocket/types.py +++ b/bfxapi/websocket/types.py @@ -3,10 +3,8 @@ from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any from dataclasses import dataclass from ..labeler import _Type - from ..notification import Notification - -JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] +from .. utils.encoder import JSON #region Type hinting for Websocket Public Channels From 0278825d358f72078c696a4da8a88ff6068a573c Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 25 Jan 2023 18:25:47 +0100 Subject: [PATCH 115/265] add endpoints, minor fixs, use format instead of str Co-authored-by: itsdeka --- bfxapi/rest/BfxRestInterface.py | 3 +++ bfxapi/rest/serializers.py | 28 ++++++++++++++++++++++++--- bfxapi/rest/types.py | 16 +++++++++++++++ examples/rest/claim_position.py | 19 ++++++++++++++++++ examples/rest/create_funding_offer.py | 2 ++ examples/rest/create_order.py | 2 ++ 6 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 examples/rest/claim_position.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 88a08f5..eac75ca 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -496,3 +496,6 @@ class _RestAuthenticatedEndpoints(_Requests): def get_base_margin_info(self) -> BaseMarginInfo: return serializers.BaseMarginInfo.parse(*(self._POST(f"auth/r/info/margin/base")[1])) + + def claim_position(self, id: int, amount: Optional[Union[Decimal, float, str]] = None) -> Notification[Claim]: + return serializers._Notification[Claim](serializer=serializers.Claim).parse(*self._POST("auth/w/position/claim", data={ "id": id, "amount": amount })) \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 044eb51..ec09995 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -235,7 +235,6 @@ TradingMarketAveragePrice = generate_labeler_serializer("TradingMarketAveragePri "AMOUNT" ]) - FundingMarketAveragePrice = generate_labeler_serializer("FundingMarketAveragePrice", klass=types.FundingMarketAveragePrice, labels=[ "RATE_AVG", "AMOUNT" @@ -299,8 +298,8 @@ Position = generate_labeler_serializer("Position", klass=types.Position, labels= "STATUS", "AMOUNT", "BASE_PRICE", - "FUNDING", - "FUNDING_TYPE", + "MARGIN_FUNDING", + "MARGIN_FUNDING_TYPE", "PL", "PL_PERC", "PRICE_LIQ", @@ -489,4 +488,27 @@ BaseMarginInfo = generate_labeler_serializer("BaseMarginInfo", klass=types.BaseM "MARGIN_MIN" ]) +Claim = generate_labeler_serializer("Claim", klass=types.Claim, labels=[ + "SYMBOL", + "POSITION_STATUS", + "AMOUNT", + "BASE_PRICE", + "MARGIN_FUNDING", + "MARGIN_FUNDING_TYPE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "POSITION_ID", + "MTS_CREATE", + "MTS_UPDATE", + "_PLACEHOLDER", + "POS_TYPE", + "_PLACEHOLDER", + "COLLATERAL", + "MIN_COLLATERAL", + "META" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index f9dc96d..720874e 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -388,4 +388,20 @@ class BaseMarginInfo(_Type): MARGIN_NET: float MARGIN_MIN: float +@dataclass +class Claim(_Type): + SYMBOL: str + POSITION_STATUS: str + AMOUNT: float + BASE_PRICE: float + MARGIN_FUNDING: float + MARGIN_FUNDING_TYPE: int + POSITION_ID: int + MTS_CREATE: int + MTS_UPDATE: int + POS_TYPE: int + COLLATERAL: str + MIN_COLLATERAL: str + META: JSON + #endregion \ No newline at end of file diff --git a/examples/rest/claim_position.py b/examples/rest/claim_position.py new file mode 100644 index 0000000..de409a3 --- /dev/null +++ b/examples/rest/claim_position.py @@ -0,0 +1,19 @@ +# python -c "from examples.rest.claim_position import *" + +import os + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +open_margin_positions = bfx.rest.auth.get_positions() + +# claim all positions +for position in open_margin_positions: + print(f"Position {position}") + claim = bfx.rest.auth.claim_position(position.POSITION_ID, amount=0.000001) + print(f"Claim {claim.NOTIFY_INFO}") \ No newline at end of file diff --git a/examples/rest/create_funding_offer.py b/examples/rest/create_funding_offer.py index 2be1e5a..e41e3b3 100644 --- a/examples/rest/create_funding_offer.py +++ b/examples/rest/create_funding_offer.py @@ -1,3 +1,5 @@ +# python -c "from examples.rest.create_funding_offer import *" + import os from bfxapi.client import Client, Constants diff --git a/examples/rest/create_order.py b/examples/rest/create_order.py index 71a99ca..c4286d2 100644 --- a/examples/rest/create_order.py +++ b/examples/rest/create_order.py @@ -1,3 +1,5 @@ +# python -c "from examples.rest.create_order import *" + import os from bfxapi.client import Client, Constants From 05827ff5d16f6828735839ab9fc2fc9c64bc3226 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 25 Jan 2023 18:33:27 +0100 Subject: [PATCH 116/265] Change comment line in all examples/folder demos. --- examples/rest/claim_position.py | 2 +- examples/rest/create_funding_offer.py | 2 +- examples/rest/create_order.py | 2 +- examples/rest/extra_calcs.py | 2 +- examples/rest/get_authenticated_data.py | 2 +- examples/rest/get_candles_hist.py | 2 +- examples/rest/get_liquidations.py | 2 +- examples/rest/get_public_data.py | 2 +- examples/rest/get_pulse_data.py | 2 +- examples/rest/transfer_wallet.py | 2 +- examples/websocket/create_order.py | 2 +- examples/websocket/order_book.py | 2 +- examples/websocket/raw_order_book.py | 2 +- examples/websocket/ticker.py | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/rest/claim_position.py b/examples/rest/claim_position.py index de409a3..de3b5f2 100644 --- a/examples/rest/claim_position.py +++ b/examples/rest/claim_position.py @@ -1,4 +1,4 @@ -# python -c "from examples.rest.claim_position import *" +# python -c "import examples.rest.claim_position" import os diff --git a/examples/rest/create_funding_offer.py b/examples/rest/create_funding_offer.py index e41e3b3..4046e28 100644 --- a/examples/rest/create_funding_offer.py +++ b/examples/rest/create_funding_offer.py @@ -1,4 +1,4 @@ -# python -c "from examples.rest.create_funding_offer import *" +# python -c "import examples.rest.create_funding_offer" import os diff --git a/examples/rest/create_order.py b/examples/rest/create_order.py index c4286d2..1f63e46 100644 --- a/examples/rest/create_order.py +++ b/examples/rest/create_order.py @@ -1,4 +1,4 @@ -# python -c "from examples.rest.create_order import *" +# python -c "import examples.rest.create_order" import os diff --git a/examples/rest/extra_calcs.py b/examples/rest/extra_calcs.py index 2603cd8..0e6fb1e 100644 --- a/examples/rest/extra_calcs.py +++ b/examples/rest/extra_calcs.py @@ -1,4 +1,4 @@ -# python -c "from examples.rest.extra_calcs import *" +# python -c "import examples.rest.extra_calcs" from bfxapi.client import Client, Constants diff --git a/examples/rest/get_authenticated_data.py b/examples/rest/get_authenticated_data.py index 79a24d9..4af5165 100644 --- a/examples/rest/get_authenticated_data.py +++ b/examples/rest/get_authenticated_data.py @@ -1,4 +1,4 @@ -# python -c "from examples.rest.get_authenticated_data import *" +# python -c "import examples.rest.get_authenticated_data" import os import time diff --git a/examples/rest/get_candles_hist.py b/examples/rest/get_candles_hist.py index fde6212..98f9da7 100644 --- a/examples/rest/get_candles_hist.py +++ b/examples/rest/get_candles_hist.py @@ -1,4 +1,4 @@ -# python -c "from examples.rest.get_candles_hist import *" +# python -c "import examples.rest.get_candles_hist" from bfxapi.client import Client, Constants diff --git a/examples/rest/get_liquidations.py b/examples/rest/get_liquidations.py index 34ed47d..6113a25 100644 --- a/examples/rest/get_liquidations.py +++ b/examples/rest/get_liquidations.py @@ -1,4 +1,4 @@ -# python -c "from examples.rest.get_liquidations import *" +# python -c "import examples.rest.get_liquidations" import time diff --git a/examples/rest/get_public_data.py b/examples/rest/get_public_data.py index ff86c14..a6c388b 100644 --- a/examples/rest/get_public_data.py +++ b/examples/rest/get_public_data.py @@ -1,4 +1,4 @@ -# python -c "from examples.rest.get_public_data import *" +# python -c "import examples.rest.get_public_data" import time diff --git a/examples/rest/get_pulse_data.py b/examples/rest/get_pulse_data.py index fc5c15f..9fb8832 100644 --- a/examples/rest/get_pulse_data.py +++ b/examples/rest/get_pulse_data.py @@ -1,4 +1,4 @@ -# python -c "from examples.rest.get_pulse_data import *" +# python -c "import examples.rest.get_pulse_data" import time diff --git a/examples/rest/transfer_wallet.py b/examples/rest/transfer_wallet.py index 28dcccb..b1d9fd3 100644 --- a/examples/rest/transfer_wallet.py +++ b/examples/rest/transfer_wallet.py @@ -1,4 +1,4 @@ -# python -c "from examples.rest.transfer_wallet import *" +# python -c "import examples.rest.transfer_wallet" import os diff --git a/examples/websocket/create_order.py b/examples/websocket/create_order.py index 7a6f645..f72f9d4 100644 --- a/examples/websocket/create_order.py +++ b/examples/websocket/create_order.py @@ -1,4 +1,4 @@ -# python -c "from examples.websocket.create_order import *" +# python -c "import examples.websocket.create_order" import os diff --git a/examples/websocket/order_book.py b/examples/websocket/order_book.py index 57d0c0a..8774b8a 100644 --- a/examples/websocket/order_book.py +++ b/examples/websocket/order_book.py @@ -1,4 +1,4 @@ -# python -c "from examples.websocket.order_book import *" +# python -c "import examples.websocket.order_book" from collections import OrderedDict diff --git a/examples/websocket/raw_order_book.py b/examples/websocket/raw_order_book.py index e9076cc..172873e 100644 --- a/examples/websocket/raw_order_book.py +++ b/examples/websocket/raw_order_book.py @@ -1,4 +1,4 @@ -# python -c "from examples.websocket.raw_order_book import *" +# python -c "import examples.websocket.raw_order_book" from collections import OrderedDict diff --git a/examples/websocket/ticker.py b/examples/websocket/ticker.py index aeb21cc..1c081b2 100644 --- a/examples/websocket/ticker.py +++ b/examples/websocket/ticker.py @@ -1,4 +1,4 @@ -# python -c "from examples.websocket.ticker import *" +# python -c "import examples.websocket.ticker" from bfxapi import Client, Constants From 374f65d660a95b4ceded115c2e5ea05ad5615cc9 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Wed, 25 Jan 2023 20:14:00 +0100 Subject: [PATCH 117/265] increase position --- bfxapi/rest/BfxRestInterface.py | 12 +++++++++++- bfxapi/rest/serializers.py | 26 ++++++++++++++++++++++++++ bfxapi/rest/types.py | 21 +++++++++++++++++++++ examples/rest/increase_position.py | 18 ++++++++++++++++++ 4 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 examples/rest/increase_position.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index eac75ca..697784c 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -498,4 +498,14 @@ class _RestAuthenticatedEndpoints(_Requests): return serializers.BaseMarginInfo.parse(*(self._POST(f"auth/r/info/margin/base")[1])) def claim_position(self, id: int, amount: Optional[Union[Decimal, float, str]] = None) -> Notification[Claim]: - return serializers._Notification[Claim](serializer=serializers.Claim).parse(*self._POST("auth/w/position/claim", data={ "id": id, "amount": amount })) \ No newline at end of file + return serializers._Notification[Claim](serializer=serializers.Claim).parse(*self._POST("auth/w/position/claim", data={ "id": id, "amount": amount })) + + def get_increase_position_info(self, symbol: str, amount: Union[Decimal, float, str]) -> IncreaseInfo: + data = self._POST(f"auth/r/position/increase/info", data={ "symbol": symbol, "amount": amount }) + + return serializers.IncreaseInfo.parse(*( + data[0] + [data[1][0]] + data[1][1] + [data[1][2]] + data[4] + data[5] + )) + + def increase_position(self, symbol: str, amount: Union[Decimal, float, str]) -> Notification[Increase]: + return serializers._Notification[Increase](serializer=serializers.Increase).parse(*self._POST("auth/w/position/increase", data={ "symbol": symbol, "amount": amount })) \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index ec09995..cb4bb39 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -511,4 +511,30 @@ Claim = generate_labeler_serializer("Claim", klass=types.Claim, labels=[ "META" ]) +IncreaseInfo = generate_labeler_serializer("IncreaseInfo", klass=types.IncreaseInfo, labels=[ + "MAX_POS", + "CURRENT_POS", + "BASE_CURRENCY_BALANCE", + "TRADABLE_BALANCE_QUOTE_CURRENCY", + "TRADABLE_BALANCE_QUOTE_TOTAL", + "TRADABLE_BALANCE_BASE_CURRENCY", + "TRADABLE_BALANCE_BASE_TOTAL", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "FUNDING_AVAIL", + "FUNDING_VALUE", + "FUNDING_REQUIRED", + "FUNDING_VALUE_CURRENCY", + "FUNDING_REQUIRED_CURRENCY" +]) + +Increase = generate_labeler_serializer("Increase", klass=types.Increase, labels=[ + "SYMBOL", + "_PLACEHOLDER", + "AMOUNT", + "BASE_PRICE" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 720874e..b2c6d79 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -404,4 +404,25 @@ class Claim(_Type): MIN_COLLATERAL: str META: JSON +@dataclass +class IncreaseInfo(_Type): + MAX_POS: int + CURRENT_POS: float + BASE_CURRENCY_BALANCE: float + TRADABLE_BALANCE_QUOTE_CURRENCY: float + TRADABLE_BALANCE_QUOTE_TOTAL: float + TRADABLE_BALANCE_BASE_CURRENCY: float + TRADABLE_BALANCE_BASE_TOTAL: float + FUNDING_AVAIL: float + FUNDING_VALUE: float + FUNDING_REQUIRED: float + FUNDING_VALUE_CURRENCY: str + FUNDING_REQUIRED_CURRENCY: str + +@dataclass +class Increase(_Type): + SYMBOL: str + AMOUNT: float + BASE_PRICE: float + #endregion \ No newline at end of file diff --git a/examples/rest/increase_position.py b/examples/rest/increase_position.py new file mode 100644 index 0000000..440f9c8 --- /dev/null +++ b/examples/rest/increase_position.py @@ -0,0 +1,18 @@ +# python -c "import examples.rest.increase_position" + +import os + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +increase_info = bfx.rest.auth.get_increase_position_info(symbol="tBTCUSD", amount=0.0001) +print(increase_info) + +# increase a margin position +notification = bfx.rest.auth.increase_position(symbol="tBTCUSD", amount=0.0001) +print(notification.NOTIFY_INFO) From 3565811ec92b60ffdc32c09c9427e38bc885ecab Mon Sep 17 00:00:00 2001 From: itsdeka Date: Wed, 25 Jan 2023 20:46:55 +0100 Subject: [PATCH 118/265] position history --- bfxapi/rest/BfxRestInterface.py | 15 ++++++++------- bfxapi/rest/serializers.py | 17 +++++++++++++++++ bfxapi/rest/types.py | 12 ++++++++++++ examples/rest/get_positions_history.py | 17 +++++++++++++++++ 4 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 examples/rest/get_positions_history.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 697784c..84ca7f2 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -45,8 +45,6 @@ class _Requests(object): def _GET(self, endpoint, params = None): response = requests.get(f"{self.host}/{endpoint}", params=params) - - print(f"{self.host}/{endpoint}") if response.status_code == HTTPStatus.NOT_FOUND: raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") @@ -487,9 +485,9 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo: - data = self._POST(f"auth/r/info/margin/{symbol}") + response = self._POST(f"auth/r/info/margin/{symbol}") - return serializers.SymbolMarginInfo.parse(*([data[1]] + data[2])) + return serializers.SymbolMarginInfo.parse(*([response[1]] + response[2])) def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]: return [ serializers.SymbolMarginInfo.parse(*([sub_data[1]] + sub_data[2])) for sub_data in self._POST(f"auth/r/info/margin/sym_all") ] @@ -501,11 +499,14 @@ class _RestAuthenticatedEndpoints(_Requests): return serializers._Notification[Claim](serializer=serializers.Claim).parse(*self._POST("auth/w/position/claim", data={ "id": id, "amount": amount })) def get_increase_position_info(self, symbol: str, amount: Union[Decimal, float, str]) -> IncreaseInfo: - data = self._POST(f"auth/r/position/increase/info", data={ "symbol": symbol, "amount": amount }) + response = self._POST(f"auth/r/position/increase/info", data={ "symbol": symbol, "amount": amount }) return serializers.IncreaseInfo.parse(*( - data[0] + [data[1][0]] + data[1][1] + [data[1][2]] + data[4] + data[5] + response[0] + [response[1][0]] + response[1][1] + [response[1][2]] + response[4] + response[5] )) def increase_position(self, symbol: str, amount: Union[Decimal, float, str]) -> Notification[Increase]: - return serializers._Notification[Increase](serializer=serializers.Increase).parse(*self._POST("auth/w/position/increase", data={ "symbol": symbol, "amount": amount })) \ No newline at end of file + return serializers._Notification[Increase](serializer=serializers.Increase).parse(*self._POST("auth/w/position/increase", data={ "symbol": symbol, "amount": amount })) + + def get_position_history(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionHistory]: + return [ serializers.PositionHistory.parse(*sub_data) for sub_data in self._POST("auth/r/positions/hist", data={ "start": start, "end": end, "limit": limit }) ] diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index cb4bb39..57ab4eb 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -537,4 +537,21 @@ Increase = generate_labeler_serializer("Increase", klass=types.Increase, labels= "BASE_PRICE" ]) +PositionHistory = generate_labeler_serializer("PositionHistory", klass=types.PositionHistory, labels=[ + "SYMBOL", + "STATUS", + "AMOUNT", + "BASE_PRICE", + "FUNDING", + "FUNDING_TYPE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "POSITION_ID", + "MTS_CREATE", + "MTS_UPDATE" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index b2c6d79..6f27c0c 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -425,4 +425,16 @@ class Increase(_Type): AMOUNT: float BASE_PRICE: float +@dataclass +class PositionHistory(_Type): + SYMBOL: str + STATUS: str + AMOUNT: float + BASE_PRICE: float + FUNDING: float + FUNDING_TYPE: int + POSITION_ID: int + MTS_CREATE: int + MTS_UPDATE: int + #endregion \ No newline at end of file diff --git a/examples/rest/get_positions_history.py b/examples/rest/get_positions_history.py new file mode 100644 index 0000000..87a4b24 --- /dev/null +++ b/examples/rest/get_positions_history.py @@ -0,0 +1,17 @@ +# python -c "import examples.rest.get_positions_history" + +import os +import time + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +now = int(round(time.time() * 1000)) + +positions_history = bfx.rest.auth.get_position_history(end=now, limit=50) +print(positions_history) From 4fbe1b89c68f6a4c6ab16de92728c0c1aca0e560 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Thu, 26 Jan 2023 15:25:00 +0100 Subject: [PATCH 119/265] positions endpoint --- bfxapi/rest/BfxRestInterface.py | 8 +++++- bfxapi/rest/serializers.py | 40 ++++++++++++++++++++++++++ bfxapi/rest/types.py | 28 ++++++++++++++++++ examples/rest/get_positions.py | 23 +++++++++++++++ examples/rest/get_positions_history.py | 17 ----------- 5 files changed, 98 insertions(+), 18 deletions(-) create mode 100644 examples/rest/get_positions.py delete mode 100644 examples/rest/get_positions_history.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 84ca7f2..b1293e7 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -508,5 +508,11 @@ class _RestAuthenticatedEndpoints(_Requests): def increase_position(self, symbol: str, amount: Union[Decimal, float, str]) -> Notification[Increase]: return serializers._Notification[Increase](serializer=serializers.Increase).parse(*self._POST("auth/w/position/increase", data={ "symbol": symbol, "amount": amount })) - def get_position_history(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionHistory]: + def get_positions_history(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionHistory]: return [ serializers.PositionHistory.parse(*sub_data) for sub_data in self._POST("auth/r/positions/hist", data={ "start": start, "end": end, "limit": limit }) ] + + def get_positions_snapshot(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionSnapshot]: + return [ serializers.PositionSnapshot.parse(*sub_data) for sub_data in self._POST("auth/r/positions/snap", data={ "start": start, "end": end, "limit": limit }) ] + + def get_positions_audit(self, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionAudit]: + return [ serializers.PositionAudit.parse(*sub_data) for sub_data in self._POST("auth/r/positions/audit", data={ "ids": ids, "start": start, "end": end, "limit": limit }) ] diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 57ab4eb..95c7f63 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -554,4 +554,44 @@ PositionHistory = generate_labeler_serializer("PositionHistory", klass=types.Pos "MTS_UPDATE" ]) +PositionSnapshot = generate_labeler_serializer("PositionSnapshot", klass=types.PositionSnapshot, labels=[ + "SYMBOL", + "STATUS", + "AMOUNT", + "BASE_PRICE", + "FUNDING", + "FUNDING_TYPE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "POSITION_ID", + "MTS_CREATE", + "MTS_UPDATE" +]) + +PositionAudit = generate_labeler_serializer("PositionAudit", klass=types.PositionAudit, labels=[ + "SYMBOL", + "STATUS", + "AMOUNT", + "BASE_PRICE", + "FUNDING", + "FUNDING_TYPE", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "POSITION_ID", + "MTS_CREATE", + "MTS_UPDATE", + "_PLACEHOLDER", + "TYPE", + "_PLACEHOLDER", + "COLLATERAL", + "COLLATERAL_MIN", + "META" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 6f27c0c..0c47c53 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -437,4 +437,32 @@ class PositionHistory(_Type): MTS_CREATE: int MTS_UPDATE: int +@dataclass +class PositionSnapshot(_Type): + SYMBOL: str + STATUS: str + AMOUNT: float + BASE_PRICE: float + FUNDING: float + FUNDING_TYPE: int + POSITION_ID: int + MTS_CREATE: int + MTS_UPDATE: int + +@dataclass +class PositionAudit(_Type): + SYMBOL: str + STATUS: str + AMOUNT: float + BASE_PRICE: float + FUNDING: float + FUNDING_TYPE: int + POSITION_ID: int + MTS_CREATE: int + MTS_UPDATE: int + TYPE: int + COLLATERAL: float + COLLATERAL_MIN: float + META: JSON + #endregion \ No newline at end of file diff --git a/examples/rest/get_positions.py b/examples/rest/get_positions.py new file mode 100644 index 0000000..5b6cfca --- /dev/null +++ b/examples/rest/get_positions.py @@ -0,0 +1,23 @@ +# python -c "import examples.rest.get_positions_snapshot" + +import os +import time + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +now = int(round(time.time() * 1000)) + +positions_snapshot = bfx.rest.auth.get_positions_snapshot(end=now, limit=50) +print(positions_snapshot) + +positions_history = bfx.rest.auth.get_positions_history(end=now, limit=50) +print(positions_history) + +positions_audit = bfx.rest.auth.get_positions_audit(end=now, limit=50) +print(positions_audit) \ No newline at end of file diff --git a/examples/rest/get_positions_history.py b/examples/rest/get_positions_history.py deleted file mode 100644 index 87a4b24..0000000 --- a/examples/rest/get_positions_history.py +++ /dev/null @@ -1,17 +0,0 @@ -# python -c "import examples.rest.get_positions_history" - -import os -import time - -from bfxapi.client import Client, Constants - -bfx = Client( - REST_HOST=Constants.REST_HOST, - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET") -) - -now = int(round(time.time() * 1000)) - -positions_history = bfx.rest.auth.get_position_history(end=now, limit=50) -print(positions_history) From ca91588067472bc75088ab40fd5ec787de37ec8f Mon Sep 17 00:00:00 2001 From: itsdeka Date: Thu, 26 Jan 2023 15:36:00 +0100 Subject: [PATCH 120/265] cancel all funding offers --- bfxapi/rest/BfxRestInterface.py | 3 +++ examples/rest/create_funding_offer.py | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index b1293e7..1853388 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -407,6 +407,9 @@ class _RestAuthenticatedEndpoints(_Requests): def cancel_funding_offer(self, id: int) -> Notification[FundingOffer]: return serializers._Notification[FundingOffer](serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/cancel", data={ "id": id })) + def cancel_all_funding_offers(self, currency: str) -> Notification: + return serializers._Notification().parse(*self._POST("auth/w/funding/offer/cancel/all", data={ "currency": currency })) + def get_funding_offers_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingOffer]: if symbol == None: endpoint = "auth/r/funding/offers/hist" diff --git a/examples/rest/create_funding_offer.py b/examples/rest/create_funding_offer.py index 4046e28..343fa1d 100644 --- a/examples/rest/create_funding_offer.py +++ b/examples/rest/create_funding_offer.py @@ -25,4 +25,9 @@ print("Offer notification:", notification) offers = bfx.rest.auth.get_active_funding_offers(symbol="fUSD") -print("Offers:", offers) \ No newline at end of file +print("Offers:", offers) + +# Cancel all funding offers +notification = bfx.rest.auth.cancel_all_funding_offers(currency="fUSD") + +print(notification) \ No newline at end of file From 48433fbb01085b772f696c15cfa75f62801bcfed Mon Sep 17 00:00:00 2001 From: itsdeka Date: Thu, 26 Jan 2023 16:44:32 +0100 Subject: [PATCH 121/265] derivatives --- bfxapi/rest/BfxRestInterface.py | 6 ++++++ bfxapi/rest/serializers.py | 9 +++++++++ bfxapi/rest/types.py | 9 +++++++++ examples/rest/derivatives.py | 31 +++++++++++++++++++++++++++++++ examples/rest/get_positions.py | 2 +- 5 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 examples/rest/derivatives.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 1853388..bf4e1cf 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -519,3 +519,9 @@ class _RestAuthenticatedEndpoints(_Requests): def get_positions_audit(self, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionAudit]: return [ serializers.PositionAudit.parse(*sub_data) for sub_data in self._POST("auth/r/positions/audit", data={ "ids": ids, "start": start, "end": end, "limit": limit }) ] + + def set_derivative_position_collateral(self, symbol: str, collateral: Union[Decimal, float, str]) -> DerivativePositionCollateral: + return serializers.DerivativePositionCollateral.parse(*(self._POST("auth/w/deriv/collateral/set", data={ "symbol": symbol, "collateral": collateral })[0])) + + def get_derivative_position_collateral_limits(self, symbol: str) -> DerivativePositionCollateralLimits: + return serializers.DerivativePositionCollateralLimits.parse(*self._POST("auth/calc/deriv/collateral/limits", data={ "symbol": symbol })) diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 95c7f63..32d2d44 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -594,4 +594,13 @@ PositionAudit = generate_labeler_serializer("PositionAudit", klass=types.Positio "META" ]) +DerivativePositionCollateral = generate_labeler_serializer("DerivativePositionCollateral", klass=types.DerivativePositionCollateral, labels=[ + "STATUS" +]) + +DerivativePositionCollateralLimits = generate_labeler_serializer("DerivativePositionCollateralLimits", klass=types.DerivativePositionCollateralLimits, labels=[ + "MIN_COLLATERAL", + "MAX_COLLATERAL" +]) + #endregion \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 0c47c53..4f2647f 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -465,4 +465,13 @@ class PositionAudit(_Type): COLLATERAL_MIN: float META: JSON +@dataclass +class DerivativePositionCollateral(_Type): + STATUS: int + +@dataclass +class DerivativePositionCollateralLimits(_Type): + MIN_COLLATERAL: float + MAX_COLLATERAL: float + #endregion \ No newline at end of file diff --git a/examples/rest/derivatives.py b/examples/rest/derivatives.py new file mode 100644 index 0000000..89865a5 --- /dev/null +++ b/examples/rest/derivatives.py @@ -0,0 +1,31 @@ +# python -c "import examples.rest.derivatives" + +import os + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +# Create a new order +submitted_order = bfx.rest.auth.submit_order( + symbol="tBTCF0:USTF0", + amount="0.015", + price="16700", + lev=10, + type="LIMIT" +) + +print("Submit Order Notification:", submitted_order) + +# Get position collateral limits +limits = bfx.rest.auth.get_derivative_position_collateral_limits(symbol="tBTCF0:USTF0") +print(f"Limits {limits}") + +# Update position collateral +response = bfx.rest.auth.set_derivative_position_collateral(symbol="tBTCF0:USTF0", collateral=50) +print(response.STATUS) + diff --git a/examples/rest/get_positions.py b/examples/rest/get_positions.py index 5b6cfca..7e71824 100644 --- a/examples/rest/get_positions.py +++ b/examples/rest/get_positions.py @@ -1,4 +1,4 @@ -# python -c "import examples.rest.get_positions_snapshot" +# python -c "import examples.rest.get_positions" import os import time From d767e5dcfe8e805b74b9eb9a9d08a47e80309608 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 26 Jan 2023 18:59:01 +0100 Subject: [PATCH 122/265] Replace every snake case (uppercase) to snake case (lowercase) in serializers, types and notification. --- bfxapi/notification.py | 22 +- bfxapi/rest/serializers.py | 662 ++++++++++++++++---------------- bfxapi/rest/types.py | 658 +++++++++++++++---------------- bfxapi/websocket/serializers.py | 370 +++++++++--------- bfxapi/websocket/types.py | 368 +++++++++--------- 5 files changed, 1040 insertions(+), 1040 deletions(-) diff --git a/bfxapi/notification.py b/bfxapi/notification.py index b2f90b8..f4301b3 100644 --- a/bfxapi/notification.py +++ b/bfxapi/notification.py @@ -8,16 +8,16 @@ T = TypeVar("T") @dataclass class Notification(_Type, Generic[T]): - MTS: int - TYPE: str - MESSAGE_ID: Optional[int] - NOTIFY_INFO: T - CODE: Optional[int] - STATUS: str - TEXT: str + mts: int + type: str + message_id: Optional[int] + notify_info: T + code: Optional[int] + status: str + text: str class _Notification(_Serializer, Generic[T]): - __LABELS = [ "MTS", "TYPE", "MESSAGE_ID", "_PLACEHOLDER", "NOTIFY_INFO", "CODE", "STATUS", "TEXT" ] + __LABELS = [ "mts", "type", "message_id", "_PLACEHOLDER", "notify_info", "code", "status", "text" ] def __init__(self, serializer: Optional[_Serializer] = None, iterate: bool = False): super().__init__("Notification", Notification, _Notification.__LABELS, IGNORE = [ "_PLACEHOLDER" ]) @@ -28,13 +28,13 @@ class _Notification(_Serializer, Generic[T]): notification = cast(Notification[T], Notification(**dict(self._serialize(*values)))) if isinstance(self.serializer, _Serializer): - NOTIFY_INFO = cast(List[Any], notification.NOTIFY_INFO) + NOTIFY_INFO = cast(List[Any], notification.notify_info) if self.iterate == False: if len(NOTIFY_INFO) == 1 and isinstance(NOTIFY_INFO[0], list): NOTIFY_INFO = NOTIFY_INFO[0] - notification.NOTIFY_INFO = cast(T, self.serializer.klass(**dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip)))) - else: notification.NOTIFY_INFO = cast(T, [ self.serializer.klass(**dict(self.serializer._serialize(*data, skip=skip))) for data in NOTIFY_INFO ]) + notification.notify_info = cast(T, self.serializer.klass(**dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip)))) + else: notification.notify_info = cast(T, [ self.serializer.klass(**dict(self.serializer._serialize(*data, skip=skip))) for data in NOTIFY_INFO ]) return notification \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 32d2d44..a6cf582 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -7,48 +7,48 @@ from .. notification import _Notification #region Serializers definition for Rest Public Endpoints PlatformStatus = generate_labeler_serializer("PlatformStatus", klass=types.PlatformStatus, labels=[ - "STATUS" + "status" ]) TradingPairTicker = generate_labeler_serializer("TradingPairTicker", klass=types.TradingPairTicker, labels=[ - "SYMBOL", - "BID", - "BID_SIZE", - "ASK", - "ASK_SIZE", - "DAILY_CHANGE", - "DAILY_CHANGE_RELATIVE", - "LAST_PRICE", - "VOLUME", - "HIGH", - "LOW" + "symbol", + "bid", + "bid_size", + "ask", + "ask_size", + "daily_change", + "daily_change_relative", + "last_price", + "volume", + "high", + "low" ]) FundingCurrencyTicker = generate_labeler_serializer("FundingCurrencyTicker", klass=types.FundingCurrencyTicker, labels=[ - "SYMBOL", - "FRR", - "BID", - "BID_PERIOD", - "BID_SIZE", - "ASK", - "ASK_PERIOD", - "ASK_SIZE", - "DAILY_CHANGE", - "DAILY_CHANGE_RELATIVE", - "LAST_PRICE", - "VOLUME", - "HIGH", - "LOW", + "symbol", + "frr", + "bid", + "bid_period", + "bid_size", + "ask", + "ask_period", + "ask_size", + "daily_change", + "daily_change_relative", + "last_price", + "volume", + "high", + "low", "_PLACEHOLDER", "_PLACEHOLDER", - "FRR_AMOUNT_AVAILABLE" + "frr_amount_available" ]) TickersHistory = generate_labeler_serializer("TickersHistory", klass=types.TickersHistory, labels=[ - "SYMBOL", - "BID", + "symbol", + "bid", "_PLACEHOLDER", - "ASK", + "ask", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", @@ -57,191 +57,191 @@ TickersHistory = generate_labeler_serializer("TickersHistory", klass=types.Ticke "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "MTS" + "mts" ]) TradingPairTrade = generate_labeler_serializer("TradingPairTrade", klass=types.TradingPairTrade, labels=[ - "ID", - "MTS", - "AMOUNT", - "PRICE" + "id", + "mts", + "amount", + "price" ]) FundingCurrencyTrade = generate_labeler_serializer("FundingCurrencyTrade", klass=types.FundingCurrencyTrade, labels=[ - "ID", - "MTS", - "AMOUNT", - "RATE", - "PERIOD" + "id", + "mts", + "amount", + "rate", + "period" ]) TradingPairBook = generate_labeler_serializer("TradingPairBook", klass=types.TradingPairBook, labels=[ - "PRICE", - "COUNT", - "AMOUNT" + "price", + "count", + "amount" ]) FundingCurrencyBook = generate_labeler_serializer("FundingCurrencyBook", klass=types.FundingCurrencyBook, labels=[ - "RATE", - "PERIOD", - "COUNT", - "AMOUNT" + "rate", + "period", + "count", + "amount" ]) TradingPairRawBook = generate_labeler_serializer("TradingPairRawBook", klass=types.TradingPairRawBook, labels=[ - "ORDER_ID", - "PRICE", - "AMOUNT" + "order_id", + "price", + "amount" ]) FundingCurrencyRawBook = generate_labeler_serializer("FundingCurrencyRawBook", klass=types.FundingCurrencyRawBook, labels=[ - "OFFER_ID", - "PERIOD", - "RATE", - "AMOUNT" + "offer_id", + "period", + "rate", + "amount" ]) Statistic = generate_labeler_serializer("Statistic", klass=types.Statistic, labels=[ - "MTS", - "VALUE" + "mts", + "value" ]) Candle = generate_labeler_serializer("Candle", klass=types.Candle, labels=[ - "MTS", - "OPEN", - "CLOSE", - "HIGH", - "LOW", - "VOLUME" + "mts", + "open", + "close", + "high", + "low", + "volume" ]) DerivativesStatus = generate_labeler_serializer("DerivativesStatus", klass=types.DerivativesStatus, labels=[ - "KEY", - "MTS", + "key", + "mts", "_PLACEHOLDER", - "DERIV_PRICE", - "SPOT_PRICE", + "deriv_price", + "spot_price", "_PLACEHOLDER", - "INSURANCE_FUND_BALANCE", + "insurance_fund_balance", "_PLACEHOLDER", - "NEXT_FUNDING_EVT_TIMESTAMP_MS", - "NEXT_FUNDING_ACCRUED", - "NEXT_FUNDING_STEP", + "next_funding_evt_timestamp_ms", + "next_funding_accrued", + "next_funding_step", "_PLACEHOLDER", - "CURRENT_FUNDING", + "current_funding", "_PLACEHOLDER", "_PLACEHOLDER", - "MARK_PRICE", + "mark_price", "_PLACEHOLDER", "_PLACEHOLDER", - "OPEN_INTEREST", + "open_interest", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "CLAMP_MIN", - "CLAMP_MAX" + "clamp_min", + "clamp_max" ]) Liquidation = generate_labeler_serializer("Liquidation", klass=types.Liquidation, labels=[ "_PLACEHOLDER", - "POS_ID", - "MTS", + "pos_id", + "mts", "_PLACEHOLDER", - "SYMBOL", - "AMOUNT", - "BASE_PRICE", + "symbol", + "amount", + "base_price", "_PLACEHOLDER", - "IS_MATCH", - "IS_MARKET_SOLD", + "is_match", + "is_market_sold", "_PLACEHOLDER", - "PRICE_ACQUIRED" + "price_acquired" ]) Leaderboard = generate_labeler_serializer("Leaderboard", klass=types.Leaderboard, labels=[ - "MTS", + "mts", "_PLACEHOLDER", - "USERNAME", - "RANKING", + "username", + "ranking", "_PLACEHOLDER", "_PLACEHOLDER", - "VALUE", + "value", "_PLACEHOLDER", "_PLACEHOLDER", - "TWITTER_HANDLE" + "twitter_handle" ]) FundingStatistic = generate_labeler_serializer("FundingStatistic", klass=types.FundingStatistic, labels=[ - "TIMESTAMP", + "timestamp", "_PLACEHOLDER", "_PLACEHOLDER", - "FRR", - "AVG_PERIOD", + "frr", + "avg_period", "_PLACEHOLDER", "_PLACEHOLDER", - "FUNDING_AMOUNT", - "FUNDING_AMOUNT_USED", + "funding_amount", + "funding_amount_used", "_PLACEHOLDER", "_PLACEHOLDER", - "FUNDING_BELOW_THRESHOLD" + "funding_below_threshold" ]) PulseProfile = generate_labeler_serializer("PulseProfile", klass=types.PulseProfile, labels=[ - "PUID", - "MTS", + "puid", + "mts", "_PLACEHOLDER", - "NICKNAME", + "nickname", "_PLACEHOLDER", - "PICTURE", - "TEXT", + "picture", + "text", "_PLACEHOLDER", "_PLACEHOLDER", - "TWITTER_HANDLE", + "twitter_handle", "_PLACEHOLDER", - "FOLLOWERS", - "FOLLOWING", + "followers", + "following", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "TIPPING_STATUS" + "tipping_status" ]) -PulseMessage = generate_recursive_serializer("PulseMessage", klass=types.PulseMessage, serializers={ "PROFILE": PulseProfile }, labels=[ - "PID", - "MTS", +PulseMessage = generate_recursive_serializer("PulseMessage", klass=types.PulseMessage, serializers={ "profile": PulseProfile }, labels=[ + "pid", + "mts", "_PLACEHOLDER", - "PUID", + "puid", "_PLACEHOLDER", - "TITLE", - "CONTENT", + "title", + "content", "_PLACEHOLDER", "_PLACEHOLDER", - "IS_PIN", - "IS_PUBLIC", - "COMMENTS_DISABLED", - "TAGS", - "ATTACHMENTS", - "META", - "LIKES", + "is_pin", + "is_public", + "comments_disabled", + "tags", + "attachments", + "meta", + "likes", "_PLACEHOLDER", "_PLACEHOLDER", - "PROFILE", - "COMMENTS", + "profile", + "comments", "_PLACEHOLDER", "_PLACEHOLDER" ]) TradingMarketAveragePrice = generate_labeler_serializer("TradingMarketAveragePrice", klass=types.TradingMarketAveragePrice, labels=[ - "PRICE_AVG", - "AMOUNT" + "price_avg", + "amount" ]) FundingMarketAveragePrice = generate_labeler_serializer("FundingMarketAveragePrice", klass=types.FundingMarketAveragePrice, labels=[ - "RATE_AVG", - "AMOUNT" + "rate_avg", + "amount" ]) FxRate = generate_labeler_serializer("FxRate", klass=types.FxRate, labels=[ - "CURRENT_RATE" + "current_rate" ]) #endregion @@ -249,358 +249,358 @@ FxRate = generate_labeler_serializer("FxRate", klass=types.FxRate, labels=[ #region Serializers definition for Rest Authenticated Endpoints Wallet = generate_labeler_serializer("Wallet", klass=types.Wallet, labels=[ - "WALLET_TYPE", - "CURRENCY", - "BALANCE", - "UNSETTLED_INTEREST", - "AVAILABLE_BALANCE", - "LAST_CHANGE", - "TRADE_DETAILS" + "wallet_type", + "currency", + "balance", + "unsettled_interest", + "available_balance", + "last_change", + "trade_details" ]) Order = generate_labeler_serializer("Order", klass=types.Order, labels=[ - "ID", - "GID", - "CID", - "SYMBOL", - "MTS_CREATE", - "MTS_UPDATE", - "AMOUNT", - "AMOUNT_ORIG", - "ORDER_TYPE", - "TYPE_PREV", - "MTS_TIF", + "id", + "gid", + "cid", + "symbol", + "mts_create", + "mts_update", + "amount", + "amount_orig", + "order_type", + "type_prev", + "mts_tif", "_PLACEHOLDER", - "FLAGS", - "ORDER_STATUS", + "flags", + "order_status", "_PLACEHOLDER", "_PLACEHOLDER", - "PRICE", - "PRICE_AVG", - "PRICE_TRAILING", - "PRICE_AUX_LIMIT", + "price", + "price_avg", + "price_trailing", + "price_aux_limit", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "NOTIFY", - "HIDDEN", - "PLACED_ID", + "notify", + "hidden", + "placed_id", "_PLACEHOLDER", "_PLACEHOLDER", - "ROUTING", + "routing", "_PLACEHOLDER", "_PLACEHOLDER", - "META" + "meta" ]) Position = generate_labeler_serializer("Position", klass=types.Position, labels=[ - "SYMBOL", - "STATUS", - "AMOUNT", - "BASE_PRICE", - "MARGIN_FUNDING", - "MARGIN_FUNDING_TYPE", - "PL", - "PL_PERC", - "PRICE_LIQ", - "LEVERAGE", + "symbol", + "status", + "amount", + "base_price", + "margin_funding", + "margin_funding_type", + "pl", + "pl_perc", + "price_liq", + "leverage", "_PLACEHOLDER", - "POSITION_ID", - "MTS_CREATE", - "MTS_UPDATE", + "position_id", + "mts_create", + "mts_update", "_PLACEHOLDER", - "TYPE", + "type", "_PLACEHOLDER", - "COLLATERAL", - "COLLATERAL_MIN", - "META" + "collateral", + "collateral_min", + "meta" ]) FundingOffer = generate_labeler_serializer("FundingOffer", klass=types.FundingOffer, labels=[ - "ID", - "SYMBOL", - "MTS_CREATED", - "MTS_UPDATED", - "AMOUNT", - "AMOUNT_ORIG", - "OFFER_TYPE", + "id", + "symbol", + "mts_created", + "mts_updated", + "amount", + "amount_orig", + "offer_type", "_PLACEHOLDER", "_PLACEHOLDER", - "FLAGS", - "OFFER_STATUS", + "flags", + "offer_status", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "RATE", - "PERIOD", - "NOTIFY", - "HIDDEN", + "rate", + "period", + "notify", + "hidden", "_PLACEHOLDER", - "RENEW", + "renew", "_PLACEHOLDER" ]) Trade = generate_labeler_serializer("Trade", klass=types.Trade, labels=[ - "ID", - "PAIR", - "MTS_CREATE", - "ORDER_ID", - "EXEC_AMOUNT", - "EXEC_PRICE", - "ORDER_TYPE", - "ORDER_PRICE", - "MAKER", - "FEE", - "FEE_CURRENCY", - "CID" + "id", + "pair", + "mts_create", + "order_id", + "exec_amount", + "exec_price", + "order_type", + "order_price", + "maker", + "fee", + "fee_currency", + "cid" ]) OrderTrade = generate_labeler_serializer("OrderTrade", klass=types.OrderTrade, labels=[ - "ID", - "PAIR", - "MTS_CREATE", - "ORDER_ID", - "EXEC_AMOUNT", - "EXEC_PRICE", + "id", + "pair", + "mts_create", + "order_id", + "exec_amount", + "exec_price", "_PLACEHOLDER", "_PLACEHOLDER", - "MAKER", - "FEE", - "FEE_CURRENCY", - "CID" + "maker", + "fee", + "fee_currency", + "cid" ]) Ledger = generate_labeler_serializer("Ledger", klass=types.Ledger, labels=[ - "ID", - "CURRENCY", + "id", + "currency", "_PLACEHOLDER", - "MTS", + "mts", "_PLACEHOLDER", - "AMOUNT", - "BALANCE", + "amount", + "balance", "_PLACEHOLDER", - "DESCRIPTION" + "description" ]) FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.FundingCredit, labels=[ - "ID", - "SYMBOL", - "SIDE", - "MTS_CREATE", - "MTS_UPDATE", - "AMOUNT", - "FLAGS", - "STATUS", - "RATE_TYPE", + "id", + "symbol", + "side", + "mts_create", + "mts_update", + "amount", + "flags", + "status", + "rate_type", "_PLACEHOLDER", "_PLACEHOLDER", - "RATE", - "PERIOD", - "MTS_OPENING", - "MTS_LAST_PAYOUT", - "NOTIFY", - "HIDDEN", + "rate", + "period", + "mts_opening", + "mts_last_payout", + "notify", + "hidden", "_PLACEHOLDER", - "RENEW", + "renew", "_PLACEHOLDER", - "NO_CLOSE", - "POSITION_PAIR" + "no_close", + "position_pair" ]) Transfer = generate_labeler_serializer("Transfer", klass=types.Transfer, labels=[ - "MTS", - "WALLET_FROM", - "WALLET_TO", + "mts", + "wallet_from", + "wallet_to", "_PLACEHOLDER", - "CURRENCY", - "CURRENCY_TO", + "currency", + "currency_to", "_PLACEHOLDER", - "AMOUNT" + "amount" ]) Withdrawal = generate_labeler_serializer("Withdrawal", klass=types.Withdrawal, labels=[ - "WITHDRAWAL_ID", + "withdrawal_id", "_PLACEHOLDER", - "METHOD", - "PAYMENT_ID", - "WALLET", - "AMOUNT", + "method", + "payment_id", + "wallet", + "amount", "_PLACEHOLDER", "_PLACEHOLDER", - "WITHDRAWAL_FEE" + "withdrawal_fee" ]) DepositAddress = generate_labeler_serializer("DepositAddress", klass=types.DepositAddress, labels=[ "_PLACEHOLDER", - "METHOD", - "CURRENCY_CODE", + "method", + "currency_code", "_PLACEHOLDER", - "ADDRESS", - "POOL_ADDRESS" + "address", + "pool_address" ]) Invoice = generate_labeler_serializer("Invoice", klass=types.Invoice, labels=[ - "INVOICE_HASH", - "INVOICE", + "invoice_hash", + "invoice", "_PLACEHOLDER", "_PLACEHOLDER", - "AMOUNT" + "amount" ]) Movement = generate_labeler_serializer("Movement", klass=types.Movement, labels=[ - "ID", - "CURRENCY", - "CURRENCY_NAME", + "id", + "currency", + "currency_name", "_PLACEHOLDER", "_PLACEHOLDER", - "MTS_STARTED", - "MTS_UPDATED", + "mts_started", + "mts_updated", "_PLACEHOLDER", "_PLACEHOLDER", - "STATUS", + "status", "_PLACEHOLDER", "_PLACEHOLDER", - "AMOUNT", - "FEES", + "amount", + "fees", "_PLACEHOLDER", "_PLACEHOLDER", - "DESTINATION_ADDRESS", + "destination_address", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "TRANSACTION_ID", - "WITHDRAW_TRANSACTION_NOTE" + "transaction_id", + "withdraw_transaction_note" ]) SymbolMarginInfo = generate_labeler_serializer("SymbolMarginInfo", klass=types.SymbolMarginInfo, labels=[ - "SYMBOL", - "TRADABLE_BALANCE", - "GROSS_BALANCE", - "BUY", - "SELL" + "symbol", + "tradable_balance", + "gross_balance", + "buy", + "sell" ]) BaseMarginInfo = generate_labeler_serializer("BaseMarginInfo", klass=types.BaseMarginInfo, labels=[ - "USER_PL", - "USER_SWAPS", - "MARGIN_BALANCE", - "MARGIN_NET", - "MARGIN_MIN" + "user_pl", + "user_swaps", + "margin_balance", + "margin_net", + "margin_min" ]) Claim = generate_labeler_serializer("Claim", klass=types.Claim, labels=[ - "SYMBOL", - "POSITION_STATUS", - "AMOUNT", - "BASE_PRICE", - "MARGIN_FUNDING", - "MARGIN_FUNDING_TYPE", + "symbol", + "position_status", + "amount", + "base_price", + "margin_funding", + "margin_funding_type", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "POSITION_ID", - "MTS_CREATE", - "MTS_UPDATE", + "position_id", + "mts_create", + "mts_update", "_PLACEHOLDER", - "POS_TYPE", + "pos_type", "_PLACEHOLDER", - "COLLATERAL", - "MIN_COLLATERAL", - "META" + "collateral", + "min_collateral", + "meta" ]) IncreaseInfo = generate_labeler_serializer("IncreaseInfo", klass=types.IncreaseInfo, labels=[ - "MAX_POS", - "CURRENT_POS", - "BASE_CURRENCY_BALANCE", - "TRADABLE_BALANCE_QUOTE_CURRENCY", - "TRADABLE_BALANCE_QUOTE_TOTAL", - "TRADABLE_BALANCE_BASE_CURRENCY", - "TRADABLE_BALANCE_BASE_TOTAL", + "max_pos", + "current_pos", + "base_currency_balance", + "tradable_balance_quote_currency", + "tradable_balance_quote_total", + "tradable_balance_base_currency", + "tradable_balance_base_total", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "FUNDING_AVAIL", - "FUNDING_VALUE", - "FUNDING_REQUIRED", - "FUNDING_VALUE_CURRENCY", - "FUNDING_REQUIRED_CURRENCY" + "funding_avail", + "funding_value", + "funding_required", + "funding_value_currency", + "funding_required_currency" ]) Increase = generate_labeler_serializer("Increase", klass=types.Increase, labels=[ - "SYMBOL", + "symbol", "_PLACEHOLDER", - "AMOUNT", - "BASE_PRICE" + "amount", + "base_price" ]) PositionHistory = generate_labeler_serializer("PositionHistory", klass=types.PositionHistory, labels=[ - "SYMBOL", - "STATUS", - "AMOUNT", - "BASE_PRICE", - "FUNDING", - "FUNDING_TYPE", + "symbol", + "status", + "amount", + "base_price", + "funding", + "funding_type", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "POSITION_ID", - "MTS_CREATE", - "MTS_UPDATE" + "position_id", + "mts_create", + "mts_update" ]) PositionSnapshot = generate_labeler_serializer("PositionSnapshot", klass=types.PositionSnapshot, labels=[ - "SYMBOL", - "STATUS", - "AMOUNT", - "BASE_PRICE", - "FUNDING", - "FUNDING_TYPE", + "symbol", + "status", + "amount", + "base_price", + "funding", + "funding_type", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "POSITION_ID", - "MTS_CREATE", - "MTS_UPDATE" + "position_id", + "mts_create", + "mts_update" ]) PositionAudit = generate_labeler_serializer("PositionAudit", klass=types.PositionAudit, labels=[ - "SYMBOL", - "STATUS", - "AMOUNT", - "BASE_PRICE", - "FUNDING", - "FUNDING_TYPE", + "symbol", + "status", + "amount", + "base_price", + "funding", + "funding_type", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "POSITION_ID", - "MTS_CREATE", - "MTS_UPDATE", + "position_id", + "mts_create", + "mts_update", "_PLACEHOLDER", - "TYPE", + "type", "_PLACEHOLDER", - "COLLATERAL", - "COLLATERAL_MIN", - "META" + "collateral", + "collateral_min", + "meta" ]) DerivativePositionCollateral = generate_labeler_serializer("DerivativePositionCollateral", klass=types.DerivativePositionCollateral, labels=[ - "STATUS" + "status" ]) DerivativePositionCollateralLimits = generate_labeler_serializer("DerivativePositionCollateralLimits", klass=types.DerivativePositionCollateralLimits, labels=[ - "MIN_COLLATERAL", - "MAX_COLLATERAL" + "min_collateral", + "max_collateral" ]) #endregion \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 4f2647f..4850404 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -10,188 +10,188 @@ from .. utils.encoder import JSON @dataclass class PlatformStatus(_Type): - STATUS: int + status: int @dataclass class TradingPairTicker(_Type): - SYMBOL: Optional[str] - BID: float - BID_SIZE: float - ASK: float - ASK_SIZE: float - DAILY_CHANGE: float - DAILY_CHANGE_RELATIVE: float - LAST_PRICE: float - VOLUME: float - HIGH: float - LOW: float + symbol: Optional[str] + bid: float + bid_size: float + ask: float + ask_size: float + daily_change: float + daily_change_relative: float + last_price: float + volume: float + high: float + low: float @dataclass class FundingCurrencyTicker(_Type): - SYMBOL: Optional[str] - FRR: float - BID: float - BID_PERIOD: int - BID_SIZE: float - ASK: float - ASK_PERIOD: int - ASK_SIZE: float - DAILY_CHANGE: float - DAILY_CHANGE_RELATIVE: float - LAST_PRICE: float - VOLUME: float - HIGH: float - LOW: float - FRR_AMOUNT_AVAILABLE: float + symbol: Optional[str] + frr: float + bid: float + bid_period: int + bid_size: float + ask: float + ask_period: int + ask_size: float + daily_change: float + daily_change_relative: float + last_price: float + volume: float + high: float + low: float + frr_amount_available: float @dataclass class TickersHistory(_Type): - SYMBOL: str - BID: float - ASK: float - MTS: int + symbol: str + bid: float + ask: float + mts: int @dataclass class TradingPairTrade(_Type): - ID: int - MTS: int - AMOUNT: float - PRICE: float + id: int + mts: int + amount: float + price: float @dataclass class FundingCurrencyTrade(_Type): - ID: int - MTS: int - AMOUNT: float - RATE: float - PERIOD: int + id: int + mts: int + amount: float + rate: float + period: int @dataclass class TradingPairBook(_Type): - PRICE: float - COUNT: int - AMOUNT: float + price: float + count: int + amount: float @dataclass class FundingCurrencyBook(_Type): - RATE: float - PERIOD: int - COUNT: int - AMOUNT: float + rate: float + period: int + count: int + amount: float @dataclass class TradingPairRawBook(_Type): - ORDER_ID: int - PRICE: float - AMOUNT: float + order_id: int + price: float + amount: float @dataclass class FundingCurrencyRawBook(_Type): - OFFER_ID: int - PERIOD: int - RATE: float - AMOUNT: float + offer_id: int + period: int + rate: float + amount: float @dataclass class Statistic(_Type): - MTS: int - VALUE: float + mts: int + value: float @dataclass class Candle(_Type): - MTS: int - OPEN: float - CLOSE: float - HIGH: float - LOW: float - VOLUME: float + mts: int + open: float + close: float + high: float + low: float + volume: float @dataclass class DerivativesStatus(_Type): - KEY: Optional[str] - MTS: int - DERIV_PRICE: float - SPOT_PRICE: float - INSURANCE_FUND_BALANCE: float - NEXT_FUNDING_EVT_TIMESTAMP_MS: int - NEXT_FUNDING_ACCRUED: float - NEXT_FUNDING_STEP: int - CURRENT_FUNDING: float - MARK_PRICE: float - OPEN_INTEREST: float - CLAMP_MIN: float - CLAMP_MAX: float + key: Optional[str] + mts: int + deriv_price: float + spot_price: float + insurance_fund_balance: float + next_funding_evt_timestamp_ms: int + next_funding_accrued: float + next_funding_step: int + current_funding: float + mark_price: float + open_interest: float + clamp_min: float + clamp_max: float @dataclass class Liquidation(_Type): - POS_ID: int - MTS: int - SYMBOL: str - AMOUNT: float - BASE_PRICE: float - IS_MATCH: int - IS_MARKET_SOLD: int - PRICE_ACQUIRED: float + pos_id: int + mts: int + symbol: str + amount: float + base_price: float + is_match: int + is_market_sold: int + price_acquired: float @dataclass class Leaderboard(_Type): - MTS: int - USERNAME: str - RANKING: int - VALUE: float - TWITTER_HANDLE: Optional[str] + mts: int + username: str + ranking: int + value: float + twitter_handle: Optional[str] @dataclass class FundingStatistic(_Type): - TIMESTAMP: int - FRR: float - AVG_PERIOD: float - FUNDING_AMOUNT: float - FUNDING_AMOUNT_USED: float - FUNDING_BELOW_THRESHOLD: float + timestamp: int + frr: float + avg_period: float + funding_amount: float + funding_amount_used: float + funding_below_threshold: float @dataclass class PulseProfile(_Type): - PUID: str - MTS: int - NICKNAME: str - PICTURE: str - TEXT: str - TWITTER_HANDLE: str - FOLLOWERS: int - FOLLOWING: int - TIPPING_STATUS: int + puid: str + mts: int + nickname: str + picture: str + text: str + twitter_handle: str + followers: int + following: int + tipping_status: int @dataclass class PulseMessage(_Type): - PID: str - MTS: int - PUID: str - TITLE: str - CONTENT: str - IS_PIN: int - IS_PUBLIC: int - COMMENTS_DISABLED: int - TAGS: List[str] - ATTACHMENTS: List[str] - META: List[JSON] - LIKES: int - PROFILE: PulseProfile - COMMENTS: int + pid: str + mts: int + puid: str + title: str + content: str + is_pin: int + is_public: int + comments_disabled: int + tags: List[str] + attachments: List[str] + meta: List[JSON] + likes: int + profile: PulseProfile + comments: int @dataclass class TradingMarketAveragePrice(_Type): - PRICE_AVG: float - AMOUNT: float + price_avg: float + amount: float @dataclass class FundingMarketAveragePrice(_Type): - RATE_AVG: float - AMOUNT: float + rate_avg: float + amount: float @dataclass class FxRate(_Type): - CURRENT_RATE: float + current_rate: float #endregion @@ -199,279 +199,279 @@ class FxRate(_Type): @dataclass class Wallet(_Type): - WALLET_TYPE: str - CURRENCY: str - BALANCE: float - UNSETTLED_INTEREST: float - AVAILABLE_BALANCE: float - LAST_CHANGE: str - TRADE_DETAILS: JSON + wallet_type: str + currency: str + balance: float + unsettled_interest: float + available_balance: float + last_change: str + trade_details: JSON @dataclass class Order(_Type): - ID: int - GID: int - CID: int - SYMBOL: str - MTS_CREATE: int - MTS_UPDATE: int - AMOUNT: float - AMOUNT_ORIG: float - ORDER_TYPE: str - TYPE_PREV: str - MTS_TIF: int - FLAGS: int - ORDER_STATUS: str - PRICE: float - PRICE_AVG: float - PRICE_TRAILING: float - PRICE_AUX_LIMIT: float - NOTIFY: int - HIDDEN: int - PLACED_ID: int - ROUTING: str - META: JSON + id: int + gid: int + cid: int + symbol: str + mts_create: int + mts_update: int + amount: float + amount_orig: float + order_type: str + type_prev: str + mts_tif: int + flags: int + order_status: str + price: float + price_avg: float + price_trailing: float + price_aux_limit: float + notify: int + hidden: int + placed_id: int + routing: str + meta: JSON @dataclass class Position(_Type): - SYMBOL: str - STATUS: str - AMOUNT: float - BASE_PRICE: float - MARGIN_FUNDING: float - MARGIN_FUNDING_TYPE: int - PL: float - PL_PERC: float - PRICE_LIQ: float - LEVERAGE: float - POSITION_ID: int - MTS_CREATE: int - MTS_UPDATE: int - TYPE: int - COLLATERAL: float - COLLATERAL_MIN: float - META: JSON + symbol: str + status: str + amount: float + base_price: float + margin_funding: float + margin_funding_type: int + pl: float + pl_perc: float + price_liq: float + leverage: float + position_id: int + mts_create: int + mts_update: int + type: int + collateral: float + collateral_min: float + meta: JSON @dataclass class FundingOffer(_Type): - ID: int - SYMBOL: str - MTS_CREATE: int - MTS_UPDATE: int - AMOUNT: float - AMOUNT_ORIG: float - OFFER_TYPE: str - FLAGS: int - OFFER_STATUS: str - RATE: float - PERIOD: int - NOTIFY: bool - HIDDEN: int - RENEW: bool + id: int + symbol: str + mts_create: int + mts_update: int + amount: float + amount_orig: float + offer_type: str + flags: int + offer_status: str + rate: float + period: int + notify: bool + hidden: int + renew: bool @dataclass class Trade(_Type): - ID: int - SYMBOL: str - MTS_CREATE: int - ORDER_ID: int - EXEC_AMOUNT: float - EXEC_PRICE: float - ORDER_TYPE: str - ORDER_PRICE: float - MAKER:int - FEE: float - FEE_CURRENCY: str - CID: int + id: int + symbol: str + mts_create: int + order_id: int + exec_amount: float + exec_price: float + order_type: str + order_price: float + maker:int + fee: float + fee_currency: str + cid: int @dataclass class OrderTrade(_Type): - ID: int - SYMBOL: str - MTS_CREATE: int - ORDER_ID: int - EXEC_AMOUNT: float - EXEC_PRICE: float - MAKER:int - FEE: float - FEE_CURRENCY: str - CID: int + id: int + symbol: str + mts_create: int + order_id: int + exec_amount: float + exec_price: float + maker:int + fee: float + fee_currency: str + cid: int @dataclass class Ledger(_Type): - ID: int - CURRENCY: str - MTS: int - AMOUNT: float - BALANCE: float + id: int + currency: str + mts: int + amount: float + balance: float description: str @dataclass class FundingCredit(_Type): - ID: int - SYMBOL: str - SIDE: int - MTS_CREATE: int - MTS_UPDATE: int - AMOUNT: float - FLAGS: int - STATUS: str - RATE: float - PERIOD: int - MTS_OPENING: int - MTS_LAST_PAYOUT: int - NOTIFY: int - HIDDEN: int - RENEW: int - RATE_REAL: float - NO_CLOSE: int - POSITION_PAIR: str + id: int + symbol: str + side: int + mts_create: int + mts_update: int + amount: float + flags: int + status: str + rate: float + period: int + mts_opening: int + mts_last_payout: int + notify: int + hidden: int + renew: int + rate_real: float + no_close: int + position_pair: str @dataclass class Transfer(_Type): - MTS: int - WALLET_FROM: str - WALLET_TO: str - CURRENCY: str - CURRENCY_TO: str - AMOUNT: int + mts: int + wallet_from: str + wallet_to: str + currency: str + currency_to: str + amount: int @dataclass class Withdrawal(_Type): - WITHDRAWAL_ID: int - METHOD: str - PAYMENT_ID: str - WALLET: str - AMOUNT: float - WITHDRAWAL_FEE: float + withdrawal_id: int + method: str + payment_id: str + wallet: str + amount: float + withdrawal_fee: float @dataclass class DepositAddress(_Type): - METHOD: str - CURRENCY_CODE: str - ADDRESS: str - POOL_ADDRESS: str + method: str + currency_code: str + address: str + pool_address: str @dataclass class Invoice(_Type): - INVOICE_HASH: str - INVOICE: str - AMOUNT: str + invoice_hash: str + invoice: str + amount: str @dataclass class Movement(_Type): - ID: str - CURRENCY: str - CURRENCY_NAME: str - MTS_STARTED: int - MTS_UPDATED: int - STATUS: str - AMOUNT: int - FEES: int - DESTINATION_ADDRESS: str - TRANSACTION_ID: str - WITHDRAW_TRANSACTION_NOTE: str + id: str + currency: str + currency_name: str + mts_started: int + mts_updated: int + status: str + amount: int + fees: int + destination_address: str + transaction_id: str + withdraw_transaction_note: str @dataclass class SymbolMarginInfo(_Type): - SYMBOL: str - TRADABLE_BALANCE: float - GROSS_BALANCE: float - BUY: float - SELL: float + symbol: str + tradable_balance: float + gross_balance: float + buy: float + sell: float @dataclass class BaseMarginInfo(_Type): - USER_PL: float - USER_SWAPS: float - MARGIN_BALANCE: float - MARGIN_NET: float - MARGIN_MIN: float + user_pl: float + user_swaps: float + margin_balance: float + margin_net: float + margin_min: float @dataclass class Claim(_Type): - SYMBOL: str - POSITION_STATUS: str - AMOUNT: float - BASE_PRICE: float - MARGIN_FUNDING: float - MARGIN_FUNDING_TYPE: int - POSITION_ID: int - MTS_CREATE: int - MTS_UPDATE: int - POS_TYPE: int - COLLATERAL: str - MIN_COLLATERAL: str - META: JSON + symbol: str + position_status: str + amount: float + base_price: float + margin_funding: float + margin_funding_type: int + position_id: int + mts_create: int + mts_update: int + pos_type: int + collateral: str + min_collateral: str + meta: JSON @dataclass class IncreaseInfo(_Type): - MAX_POS: int - CURRENT_POS: float - BASE_CURRENCY_BALANCE: float - TRADABLE_BALANCE_QUOTE_CURRENCY: float - TRADABLE_BALANCE_QUOTE_TOTAL: float - TRADABLE_BALANCE_BASE_CURRENCY: float - TRADABLE_BALANCE_BASE_TOTAL: float - FUNDING_AVAIL: float - FUNDING_VALUE: float - FUNDING_REQUIRED: float - FUNDING_VALUE_CURRENCY: str - FUNDING_REQUIRED_CURRENCY: str + max_pos: int + current_pos: float + base_currency_balance: float + tradable_balance_quote_currency: float + tradable_balance_quote_total: float + tradable_balance_base_currency: float + tradable_balance_base_total: float + funding_avail: float + funding_value: float + funding_required: float + funding_value_currency: str + funding_required_currency: str @dataclass class Increase(_Type): - SYMBOL: str - AMOUNT: float - BASE_PRICE: float + symbol: str + amount: float + base_price: float @dataclass class PositionHistory(_Type): - SYMBOL: str - STATUS: str - AMOUNT: float - BASE_PRICE: float - FUNDING: float - FUNDING_TYPE: int - POSITION_ID: int - MTS_CREATE: int - MTS_UPDATE: int + symbol: str + status: str + amount: float + base_price: float + funding: float + funding_type: int + position_id: int + mts_create: int + mts_update: int @dataclass class PositionSnapshot(_Type): - SYMBOL: str - STATUS: str - AMOUNT: float - BASE_PRICE: float - FUNDING: float - FUNDING_TYPE: int - POSITION_ID: int - MTS_CREATE: int - MTS_UPDATE: int + symbol: str + status: str + amount: float + base_price: float + funding: float + funding_type: int + position_id: int + mts_create: int + mts_update: int @dataclass class PositionAudit(_Type): - SYMBOL: str - STATUS: str - AMOUNT: float - BASE_PRICE: float - FUNDING: float - FUNDING_TYPE: int - POSITION_ID: int - MTS_CREATE: int - MTS_UPDATE: int - TYPE: int - COLLATERAL: float - COLLATERAL_MIN: float - META: JSON + symbol: str + status: str + amount: float + base_price: float + funding: float + funding_type: int + position_id: int + mts_create: int + mts_update: int + type: int + collateral: float + collateral_min: float + meta: JSON @dataclass class DerivativePositionCollateral(_Type): - STATUS: int + status: int @dataclass class DerivativePositionCollateralLimits(_Type): - MIN_COLLATERAL: float - MAX_COLLATERAL: float + min_collateral: float + max_collateral: float #endregion \ No newline at end of file diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index 1e14dbc..5991cc9 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -7,111 +7,111 @@ from .. notification import _Notification #region Serializers definition for Websocket Public Channels TradingPairTicker = generate_labeler_serializer("TradingPairTicker", klass=types.TradingPairTicker, labels=[ - "BID", - "BID_SIZE", - "ASK", - "ASK_SIZE", - "DAILY_CHANGE", - "DAILY_CHANGE_RELATIVE", - "LAST_PRICE", - "VOLUME", - "HIGH", - "LOW" + "bid", + "bid_size", + "ask", + "ask_size", + "daily_change", + "daily_change_relative", + "last_price", + "volume", + "high", + "low" ]) FundingCurrencyTicker = generate_labeler_serializer("FundingCurrencyTicker", klass=types.FundingCurrencyTicker, labels=[ - "FRR", - "BID", - "BID_PERIOD", - "BID_SIZE", - "ASK", - "ASK_PERIOD", - "ASK_SIZE", - "DAILY_CHANGE", - "DAILY_CHANGE_RELATIVE", - "LAST_PRICE", - "VOLUME", - "HIGH", - "LOW" + "frr", + "bid", + "bid_period", + "bid_size", + "ask", + "ask_period", + "ask_size", + "daily_change", + "daily_change_relative", + "last_price", + "volume", + "high", + "low" "_PLACEHOLDER", "_PLACEHOLDER", - "FRR_AMOUNT_AVAILABLE" + "frr_amount_available" ]) TradingPairTrade = generate_labeler_serializer("TradingPairTrade", klass=types.TradingPairTrade, labels=[ - "ID", - "MTS", - "AMOUNT", - "PRICE" + "id", + "mts", + "amount", + "price" ]) FundingCurrencyTrade = generate_labeler_serializer("FundingCurrencyTrade", klass=types.FundingCurrencyTrade, labels=[ - "ID", - "MTS", - "AMOUNT", - "RATE", - "PERIOD" + "id", + "mts", + "amount", + "rate", + "period" ]) TradingPairBook = generate_labeler_serializer("TradingPairBook", klass=types.TradingPairBook, labels=[ - "PRICE", - "COUNT", - "AMOUNT" + "price", + "count", + "amount" ]) FundingCurrencyBook = generate_labeler_serializer("FundingCurrencyBook", klass=types.FundingCurrencyBook, labels=[ - "RATE", - "PERIOD", - "COUNT", - "AMOUNT" + "rate", + "period", + "count", + "amount" ]) TradingPairRawBook = generate_labeler_serializer("TradingPairRawBook", klass=types.TradingPairRawBook, labels=[ - "ORDER_ID", - "PRICE", - "AMOUNT" + "order_id", + "price", + "amount" ]) FundingCurrencyRawBook = generate_labeler_serializer("FundingCurrencyRawBook", klass=types.FundingCurrencyRawBook, labels=[ - "OFFER_ID", - "PERIOD", - "RATE", - "AMOUNT" + "offer_id", + "period", + "rate", + "amount" ]) Candle = generate_labeler_serializer("Candle", klass=types.Candle, labels=[ - "MTS", - "OPEN", - "CLOSE", - "HIGH", - "LOW", - "VOLUME" + "mts", + "open", + "close", + "high", + "low", + "volume" ]) DerivativesStatus = generate_labeler_serializer("DerivativesStatus", klass=types.DerivativesStatus, labels=[ - "TIME_MS", + "time_ms", "_PLACEHOLDER", - "DERIV_PRICE", - "SPOT_PRICE", + "deriv_price", + "spot_price", "_PLACEHOLDER", - "INSURANCE_FUND_BALANCE", + "insurance_fund_balance", "_PLACEHOLDER", - "NEXT_FUNDING_EVT_TIMESTAMP_MS", - "NEXT_FUNDING_ACCRUED", - "NEXT_FUNDING_STEP", + "next_funding_evt_timestamp_ms", + "next_funding_accrued", + "next_funding_step", "_PLACEHOLDER", - "CURRENT_FUNDING" + "current_funding" "_PLACEHOLDER", "_PLACEHOLDER", - "MARK_PRICE", + "mark_price", "_PLACEHOLDER", "_PLACEHOLDER", - "OPEN_INTEREST", + "open_interest", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "CLAMP_MIN", - "CLAMP_MAX" + "clamp_min", + "clamp_max" ]) #endregion @@ -119,179 +119,179 @@ DerivativesStatus = generate_labeler_serializer("DerivativesStatus", klass=types #region Serializers definition for Websocket Authenticated Channels Order = generate_labeler_serializer("Order", klass=types.Order, labels=[ - "ID", - "GID", - "CID", - "SYMBOL", - "MTS_CREATE", - "MTS_UPDATE", - "AMOUNT", - "AMOUNT_ORIG", - "ORDER_TYPE", - "TYPE_PREV", - "MTS_TIF", + "id", + "gid", + "cid", + "symbol", + "mts_create", + "mts_update", + "amount", + "amount_orig", + "order_type", + "type_prev", + "mts_tif", "_PLACEHOLDER", - "FLAGS", - "ORDER_STATUS", + "flags", + "order_status", "_PLACEHOLDER", "_PLACEHOLDER", - "PRICE", - "PRICE_AVG", - "PRICE_TRAILING", - "PRICE_AUX_LIMIT", + "price", + "price_avg", + "price_trailing", + "price_aux_limit", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "NOTIFY", - "HIDDEN", - "PLACED_ID", + "notify", + "hidden", + "placed_id", "_PLACEHOLDER", "_PLACEHOLDER", - "ROUTING", + "routing", "_PLACEHOLDER", "_PLACEHOLDER", - "META" + "meta" ]) Position = generate_labeler_serializer("Position", klass=types.Position, labels=[ - "SYMBOL", - "STATUS", - "AMOUNT", - "BASE_PRICE", - "MARGIN_FUNDING", - "MARGIN_FUNDING_TYPE", - "PL", - "PL_PERC", - "PRICE_LIQ", - "LEVERAGE", - "FLAG", - "POSITION_ID", - "MTS_CREATE", - "MTS_UPDATE", + "symbol", + "status", + "amount", + "base_price", + "margin_funding", + "margin_funding_type", + "pl", + "pl_perc", + "price_liq", + "leverage", + "flag", + "position_id", + "mts_create", + "mts_update", "_PLACEHOLDER", - "TYPE", + "type", "_PLACEHOLDER", - "COLLATERAL", - "COLLATERAL_MIN", - "META" + "collateral", + "collateral_min", + "meta" ]) TradeExecuted = generate_labeler_serializer("TradeExecuted", klass=types.TradeExecuted, labels=[ - "ID", - "SYMBOL", - "MTS_CREATE", - "ORDER_ID", - "EXEC_AMOUNT", - "EXEC_PRICE", - "ORDER_TYPE", - "ORDER_PRICE", - "MAKER", + "id", + "symbol", + "mts_create", + "order_id", + "exec_amount", + "exec_price", + "order_type", + "order_price", + "maker", "_PLACEHOLDER", "_PLACEHOLDER", - "CID" + "cid" ]) TradeExecutionUpdate = generate_labeler_serializer("TradeExecutionUpdate", klass=types.TradeExecutionUpdate, labels=[ - "ID", - "SYMBOL", - "MTS_CREATE", - "ORDER_ID", - "EXEC_AMOUNT", - "EXEC_PRICE", - "ORDER_TYPE", - "ORDER_PRICE", - "MAKER", - "FEE", - "FEE_CURRENCY", - "CID" + "id", + "symbol", + "mts_create", + "order_id", + "exec_amount", + "exec_price", + "order_type", + "order_price", + "maker", + "fee", + "fee_currency", + "cid" ]) FundingOffer = generate_labeler_serializer("FundingOffer", klass=types.FundingOffer, labels=[ - "ID", - "SYMBOL", - "MTS_CREATED", - "MTS_UPDATED", - "AMOUNT", - "AMOUNT_ORIG", - "OFFER_TYPE", + "id", + "symbol", + "mts_created", + "mts_updated", + "amount", + "amount_orig", + "offer_type", "_PLACEHOLDER", "_PLACEHOLDER", - "FLAGS", - "STATUS", + "flags", + "status", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "RATE", - "PERIOD", - "NOTIFY", - "HIDDEN", + "rate", + "period", + "notify", + "hidden", "_PLACEHOLDER", - "RENEW", + "renew", "_PLACEHOLDER" ]) FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.FundingCredit, labels=[ - "ID", - "SYMBOL", - "SIDE", - "MTS_CREATE", - "MTS_UPDATE", - "AMOUNT", - "FLAGS", - "STATUS", + "id", + "symbol", + "side", + "mts_create", + "mts_update", + "amount", + "flags", + "status", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "RATE", - "PERIOD", - "MTS_OPENING", - "MTS_LAST_PAYOUT", - "NOTIFY", - "HIDDEN", + "rate", + "period", + "mts_opening", + "mts_last_payout", + "notify", + "hidden", "_PLACEHOLDER", - "RENEW", - "RATE_REAL", - "NO_CLOSE", - "POSITION_PAIR" + "renew", + "rate_real", + "no_close", + "position_pair" ]) FundingLoan = generate_labeler_serializer("FundingLoan", klass=types.FundingLoan, labels=[ - "ID", - "SYMBOL", - "SIDE", - "MTS_CREATE", - "MTS_UPDATE", - "AMOUNT", - "FLAGS", - "STATUS", + "id", + "symbol", + "side", + "mts_create", + "mts_update", + "amount", + "flags", + "status", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", - "RATE", - "PERIOD", - "MTS_OPENING", - "MTS_LAST_PAYOUT", - "NOTIFY", - "HIDDEN", + "rate", + "period", + "mts_opening", + "mts_last_payout", + "notify", + "hidden", "_PLACEHOLDER", - "RENEW", - "RATE_REAL", - "NO_CLOSE" + "renew", + "rate_real", + "no_close" ]) Wallet = generate_labeler_serializer("Wallet", klass=types.Wallet, labels=[ - "WALLET_TYPE", - "CURRENCY", - "BALANCE", - "UNSETTLED_INTEREST", - "BALANCE_AVAILABLE", - "DESCRIPTION", - "META" + "wallet_type", + "currency", + "balance", + "unsettled_interest", + "balance_available", + "description", + "meta" ]) BalanceInfo = generate_labeler_serializer("BalanceInfo", klass=types.BalanceInfo, labels=[ - "AUM", - "AUM_NET", + "aum", + "aum_net", ]) #endregion \ No newline at end of file diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py index 19ed3ef..91bac2a 100644 --- a/bfxapi/websocket/types.py +++ b/bfxapi/websocket/types.py @@ -10,246 +10,246 @@ from .. utils.encoder import JSON @dataclass class TradingPairTicker(_Type): - BID: float - BID_SIZE: float - ASK: float - ASK_SIZE: float - DAILY_CHANGE: float - DAILY_CHANGE_RELATIVE: float - LAST_PRICE: float - VOLUME: float - HIGH: float - LOW: float + bid: float + bid_size: float + ask: float + ask_size: float + daily_change: float + daily_change_relative: float + last_price: float + volume: float + high: float + low: float @dataclass class FundingCurrencyTicker(_Type): - FRR: float - BID: float - BID_PERIOD: int - BID_SIZE: float - ASK: float - ASK_PERIOD: int - ASK_SIZE: float - DAILY_CHANGE: float - DAILY_CHANGE_RELATIVE: float - LAST_PRICE: float - VOLUME: float - HIGH: float - LOW: float - FRR_AMOUNT_AVAILABLE: float + frr: float + bid: float + bid_period: int + bid_size: float + ask: float + ask_period: int + ask_size: float + daily_change: float + daily_change_relative: float + last_price: float + volume: float + high: float + low: float + frr_amount_available: float @dataclass class TradingPairTrade(_Type): - ID: int - MTS: int - AMOUNT: float - PRICE: float + id: int + mts: int + amount: float + price: float @dataclass class FundingCurrencyTrade(_Type): - ID: int - MTS: int - AMOUNT: float - RATE: float - PERIOD: int + id: int + mts: int + amount: float + rate: float + period: int @dataclass class TradingPairBook(_Type): - PRICE: float - COUNT: int - AMOUNT: float + price: float + count: int + amount: float @dataclass class FundingCurrencyBook(_Type): - RATE: float - PERIOD: int - COUNT: int - AMOUNT: float + rate: float + period: int + count: int + amount: float @dataclass class TradingPairRawBook(_Type): - ORDER_ID: int - PRICE: float - AMOUNT: float + order_id: int + price: float + amount: float @dataclass class FundingCurrencyRawBook(_Type): - OFFER_ID: int - PERIOD: int - RATE: float - AMOUNT: float + offer_id: int + period: int + rate: float + amount: float @dataclass class Candle(_Type): - MTS: int - OPEN: float - CLOSE: float - HIGH: float - LOW: float - VOLUME: float + mts: int + open: float + close: float + high: float + low: float + volume: float @dataclass class DerivativesStatus(_Type): - TIME_MS: int - DERIV_PRICE: float - SPOT_PRICE: float - INSURANCE_FUND_BALANCE: float - NEXT_FUNDING_EVT_TIMESTAMP_MS: int - NEXT_FUNDING_ACCRUED: float - NEXT_FUNDING_STEP: int - CURRENT_FUNDING: float - MARK_PRICE: float - OPEN_INTEREST: float - CLAMP_MIN: float - CLAMP_MAX: float + time_ms: int + deriv_price: float + spot_price: float + insurance_fund_balance: float + next_funding_evt_timestamp_ms: int + next_funding_accrued: float + next_funding_step: int + current_funding: float + mark_price: float + open_interest: float + clamp_min: float + clamp_max: float #endregion #region Type hinting for Websocket Authenticated Channels @dataclass class Order(_Type): - ID: int - GID: int - CID: int - SYMBOL: str - MTS_CREATE: int - MTS_UPDATE: int - AMOUNT: float - AMOUNT_ORIG: float - ORDER_TYPE: str - TYPE_PREV: str - MTS_TIF: int - FLAGS: int - ORDER_STATUS: str - PRICE: float - PRICE_AVG: float - PRICE_TRAILING: float - PRICE_AUX_LIMIT: float - NOTIFY: int - HIDDEN: int - PLACED_ID: int - ROUTING: str - META: JSON + id: int + gid: int + cid: int + symbol: str + mts_create: int + mts_update: int + amount: float + amount_orig: float + order_type: str + type_prev: str + mts_tif: int + flags: int + order_status: str + price: float + price_avg: float + price_trailing: float + price_aux_limit: float + notify: int + hidden: int + placed_id: int + routing: str + meta: JSON @dataclass class Position(_Type): - SYMBOL: str - STATUS: str - AMOUNT: float - BASE_PRICE: float - MARGIN_FUNDING: float - MARGIN_FUNDING_TYPE: int - PL: float - PL_PERC: float - PRICE_LIQ: float - LEVERAGE: float - POSITION_ID: int - MTS_CREATE: int - MTS_UPDATE: int - TYPE: int - COLLATERAL: float - COLLATERAL_MIN: float - META: JSON + symbol: str + status: str + amount: float + base_price: float + margin_funding: float + margin_funding_type: int + pl: float + pl_perc: float + price_liq: float + leverage: float + position_id: int + mts_create: int + mts_update: int + type: int + collateral: float + collateral_min: float + meta: JSON @dataclass class TradeExecuted(_Type): - ID: int - SYMBOL: str - MTS_CREATE: int - ORDER_ID: int - EXEC_AMOUNT: float - EXEC_PRICE: float - ORDER_TYPE: str - ORDER_PRICE: float - MAKER:int - CID: int + id: int + symbol: str + mts_create: int + order_id: int + exec_amount: float + exec_price: float + order_type: str + order_price: float + maker:int + cid: int @dataclass class TradeExecutionUpdate(_Type): - ID: int - SYMBOL: str - MTS_CREATE: int - ORDER_ID: int - EXEC_AMOUNT: float - EXEC_PRICE: float - ORDER_TYPE: str - ORDER_PRICE: float - MAKER:int - FEE: float - FEE_CURRENCY: str - CID: int + id: int + symbol: str + mts_create: int + order_id: int + exec_amount: float + exec_price: float + order_type: str + order_price: float + maker:int + fee: float + fee_currency: str + cid: int @dataclass class FundingOffer(_Type): - ID: int - SYMBOL: str - MTS_CREATED: int - MTS_UPDATED: int - AMOUNT: float - AMOUNT_ORIG: float - OFFER_TYPE: str - FLAGS: int - STATUS: str - RATE: float - PERIOD: int - NOTIFY: int - HIDDEN: int - RENEW: int + id: int + symbol: str + mts_created: int + mts_updated: int + amount: float + amount_orig: float + offer_type: str + flags: int + status: str + rate: float + period: int + notify: int + hidden: int + renew: int @dataclass class FundingCredit(_Type): - ID: int - SYMBOL: str - SIDE: int - MTS_CREATE: int - MTS_UPDATE: int - AMOUNT: float - FLAGS: int - STATUS: str - RATE: float - PERIOD: int - MTS_OPENING: int - MTS_LAST_PAYOUT: int - NOTIFY: int - HIDDEN: int - RENEW: int - RATE_REAL: float - NO_CLOSE: int - POSITION_PAIR: str + id: int + symbol: str + side: int + mts_create: int + mts_update: int + amount: float + flags: int + status: str + rate: float + period: int + mts_opening: int + mts_last_payout: int + notify: int + hidden: int + renew: int + rate_real: float + no_close: int + position_pair: str @dataclass class FundingLoan(_Type): - ID: int - SYMBOL: str - SIDE: int - MTS_CREATE: int - MTS_UPDATE: int - AMOUNT: float - FLAGS: int - STATUS: str - RATE: float - PERIOD: int - MTS_OPENING: int - MTS_LAST_PAYOUT: int - NOTIFY: int - HIDDEN: int - RENEW: int - RATE_REAL: float - NO_CLOSE: int + id: int + symbol: str + side: int + mts_create: int + mts_update: int + amount: float + flags: int + status: str + rate: float + period: int + mts_opening: int + mts_last_payout: int + notify: int + hidden: int + renew: int + rate_real: float + no_close: int @dataclass class Wallet(_Type): - WALLET_TYPE: str - CURRENCY: str - BALANCE: float - UNSETTLED_INTEREST: float - BALANCE_AVAILABLE: float - DESCRIPTION: str - META: JSON + wallet_type: str + currency: str + balance: float + unsettled_interest: float + balance_available: float + description: str + meta: JSON @dataclass class BalanceInfo(_Type): - AUM: float - AUM_NET: float + aum: float + aum_net: float #endregion \ No newline at end of file From 6e96cda584ba69d1dfc53297e6352038bf95301a Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 27 Jan 2023 15:57:36 +0100 Subject: [PATCH 123/265] Edit demos in examples/ folder to use lowercase property identifiers. --- bfxapi/rest/BfxRestInterface.py | 5 ++--- examples/rest/claim_position.py | 4 ++-- examples/rest/create_order.py | 4 ++-- examples/rest/derivatives.py | 2 +- examples/rest/extra_calcs.py | 6 +++--- examples/rest/get_pulse_data.py | 6 +++--- examples/rest/increase_position.py | 2 +- examples/rest/transfer_wallet.py | 8 ++++---- examples/websocket/order_book.py | 2 +- examples/websocket/raw_order_book.py | 2 +- 10 files changed, 20 insertions(+), 21 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index bf4e1cf..d50799c 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -101,7 +101,7 @@ class _RestPublicEndpoints(_Requests): def get_t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]: if isinstance(pairs, str) and pairs == "ALL": - return [ cast(TradingPairTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.SYMBOL).startswith("t") ] + 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 ]) @@ -109,7 +109,7 @@ class _RestPublicEndpoints(_Requests): def get_f_tickers(self, currencies: Union[List[str], Literal["ALL"]]) -> List[FundingCurrencyTicker]: if isinstance(currencies, str) and currencies == "ALL": - return [ cast(FundingCurrencyTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.SYMBOL).startswith("f") ] + 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 ]) @@ -218,7 +218,6 @@ class _RestPublicEndpoints(_Requests): return [ serializers.Liquidation.parse(*sub_data[0]) for sub_data in data ] def get_seed_candles(self, symbol: str, tf: str = '1m', sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Candle]: - params = {"sort": sort, "start": start, "end": end, "limit": limit} data = self._GET(f"candles/trade:{tf}:{symbol}/hist?limit={limit}&start={start}&end={end}&sort={sort}", params=params) diff --git a/examples/rest/claim_position.py b/examples/rest/claim_position.py index de3b5f2..caf5e23 100644 --- a/examples/rest/claim_position.py +++ b/examples/rest/claim_position.py @@ -15,5 +15,5 @@ open_margin_positions = bfx.rest.auth.get_positions() # claim all positions for position in open_margin_positions: print(f"Position {position}") - claim = bfx.rest.auth.claim_position(position.POSITION_ID, amount=0.000001) - print(f"Claim {claim.NOTIFY_INFO}") \ No newline at end of file + claim = bfx.rest.auth.claim_position(position.position_id, amount=0.000001) + print(f"Claim {claim.notify_info}") \ No newline at end of file diff --git a/examples/rest/create_order.py b/examples/rest/create_order.py index 1f63e46..2c13ae2 100644 --- a/examples/rest/create_order.py +++ b/examples/rest/create_order.py @@ -25,7 +25,7 @@ print("Submit Order Notification:", submitted_order) # Update it updated_order = bfx.rest.auth.update_order( - id=submitted_order.NOTIFY_INFO.ID, + id=submitted_order.notify_info.id, amount="0.020", price="10100" ) @@ -33,6 +33,6 @@ updated_order = bfx.rest.auth.update_order( print("Update Order Notification:", updated_order) # Delete it -canceled_order = bfx.rest.auth.cancel_order(id=submitted_order.NOTIFY_INFO.ID) +canceled_order = bfx.rest.auth.cancel_order(id=submitted_order.notify_info.id) print("Cancel Order Notification:", canceled_order) diff --git a/examples/rest/derivatives.py b/examples/rest/derivatives.py index 89865a5..4aedd00 100644 --- a/examples/rest/derivatives.py +++ b/examples/rest/derivatives.py @@ -27,5 +27,5 @@ print(f"Limits {limits}") # Update position collateral response = bfx.rest.auth.set_derivative_position_collateral(symbol="tBTCF0:USTF0", collateral=50) -print(response.STATUS) +print(response.status) diff --git a/examples/rest/extra_calcs.py b/examples/rest/extra_calcs.py index 0e6fb1e..e380aef 100644 --- a/examples/rest/extra_calcs.py +++ b/examples/rest/extra_calcs.py @@ -12,7 +12,7 @@ t_symbol_response = bfx.rest.public.get_trading_market_average_price( price_limit="20000.5" ) -print(t_symbol_response.PRICE_AVG) +print(t_symbol_response.price_avg) f_symbol_response = bfx.rest.public.get_funding_market_average_price( symbol="fUSD", @@ -21,8 +21,8 @@ f_symbol_response = bfx.rest.public.get_funding_market_average_price( rate_limit="0.00015" ) -print(f_symbol_response.RATE_AVG) +print(f_symbol_response.rate_avg) fx_rate = bfx.rest.public.get_fx_rate(ccy1="USD", ccy2="EUR") -print(fx_rate.CURRENT_RATE) \ No newline at end of file +print(fx_rate.current_rate) \ No newline at end of file diff --git a/examples/rest/get_pulse_data.py b/examples/rest/get_pulse_data.py index 9fb8832..75b55ae 100644 --- a/examples/rest/get_pulse_data.py +++ b/examples/rest/get_pulse_data.py @@ -14,9 +14,9 @@ messages = bfx.rest.public.get_pulse_history(end=now, limit=100) for message in messages: print(f"Message: {message}") - print(message.CONTENT) - print(message.PROFILE.PICTURE) + print(message.content) + print(message.profile.picture) profile = bfx.rest.public.get_pulse_profile("News") print(f"Profile: {profile}") -print(f"Profile picture: {profile.PICTURE}") \ No newline at end of file +print(f"Profile picture: {profile.picture}") \ No newline at end of file diff --git a/examples/rest/increase_position.py b/examples/rest/increase_position.py index 440f9c8..65595c8 100644 --- a/examples/rest/increase_position.py +++ b/examples/rest/increase_position.py @@ -15,4 +15,4 @@ print(increase_info) # increase a margin position notification = bfx.rest.auth.increase_position(symbol="tBTCUSD", amount=0.0001) -print(notification.NOTIFY_INFO) +print(notification.notify_info) diff --git a/examples/rest/transfer_wallet.py b/examples/rest/transfer_wallet.py index b1d9fd3..e986bfd 100644 --- a/examples/rest/transfer_wallet.py +++ b/examples/rest/transfer_wallet.py @@ -12,20 +12,20 @@ bfx = Client( def transfer_wallet(): response = bfx.rest.auth.submit_wallet_transfer(from_wallet="exchange", to_wallet="funding", from_currency="ETH", to_currency="ETH", amount=0.001) - print("Transfer:", response.NOTIFY_INFO) + print("Transfer:", response.notify_info) def get_existing_deposit_address(): response = bfx.rest.auth.get_deposit_address(wallet="exchange", method="bitcoin", renew=False) - print("Address:", response.NOTIFY_INFO) + print("Address:", response.notify_info) def create_new_deposit_address(): response = bfx.rest.auth.get_deposit_address(wallet="exchange", method="bitcoin", renew=True) - print("Address:", response.NOTIFY_INFO) + print("Address:", response.notify_info) def withdraw(): # tetheruse = Tether (ERC20) response = bfx.rest.auth.submit_wallet_withdraw(wallet="exchange", method="tetheruse", amount=1, address="0x742d35Cc6634C0532925a3b844Bc454e4438f44e") - print("Address:", response.NOTIFY_INFO) + print("Address:", response.notify_info) def create_lighting_network_deposit_address(): invoice = bfx.rest.auth.get_deposit_invoice(wallet="funding", currency="LNX", amount=0.001) diff --git a/examples/websocket/order_book.py b/examples/websocket/order_book.py index 8774b8a..82bd105 100644 --- a/examples/websocket/order_book.py +++ b/examples/websocket/order_book.py @@ -19,7 +19,7 @@ class OrderBook(object): } def update(self, symbol: str, data: TradingPairBook) -> None: - price, count, amount = data.PRICE, data.COUNT, data.AMOUNT + price, count, amount = data.price, data.count, data.amount kind = (amount > 0) and "bids" or "asks" diff --git a/examples/websocket/raw_order_book.py b/examples/websocket/raw_order_book.py index 172873e..896e351 100644 --- a/examples/websocket/raw_order_book.py +++ b/examples/websocket/raw_order_book.py @@ -19,7 +19,7 @@ class RawOrderBook(object): } def update(self, symbol: str, data: TradingPairRawBook) -> None: - order_id, price, amount = data.ORDER_ID, data.PRICE, data.AMOUNT + order_id, price, amount = data.order_id, data.price, data.amount kind = (amount > 0) and "bids" or "asks" From f12981b841b6403bbbfde742ee4ef28812eabae0 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 27 Jan 2023 16:10:11 +0100 Subject: [PATCH 124/265] Refactor some _RestPublicEndpoints's methods. --- bfxapi/rest/BfxRestInterface.py | 39 ++++++++++----------------------- 1 file changed, 11 insertions(+), 28 deletions(-) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index d50799c..2555eae 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -89,6 +89,9 @@ class _Requests(object): return data class _RestPublicEndpoints(_Requests): + def conf(self, config: Config) -> Any: + return self._GET(f"conf/{config}")[0] + def get_platform_status(self) -> PlatformStatus: return serializers.PlatformStatus.parse(*self._GET("platform/status")) @@ -121,16 +124,12 @@ class _RestPublicEndpoints(_Requests): def get_f_ticker(self, currency: str) -> FundingCurrencyTicker: return serializers.FundingCurrencyTicker.parse(*self._GET(f"ticker/f{currency}"), skip=["SYMBOL"]) - def get_tickers_history(self, symbols: List[str], start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[TickersHistory]: - params = { + def get_tickers_history(self, symbols: List[str], start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[TickersHistory]: + return [ serializers.TickersHistory.parse(*sub_data) for sub_data in self._GET("tickers/hist", params={ "symbols": ",".join(symbols), "start": start, "end": end, "limit": limit - } - - data = self._GET("tickers/hist", params=params) - - return [ serializers.TickersHistory.parse(*sub_data) for sub_data in data ] + }) ] def get_t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[TradingPairTrade]: params = { "limit": limit, "start": start, "end": end, "sort": sort } @@ -205,23 +204,17 @@ class _RestPublicEndpoints(_Requests): sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None ) -> List[DerivativesStatus]: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"status/{type}/{symbol}/hist", params=params) - return [ serializers.DerivativesStatus.parse(*sub_data, skip=[ "KEY" ]) for sub_data in data ] def get_liquidations(self, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Liquidation]: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET("liquidations/hist", params=params) - return [ serializers.Liquidation.parse(*sub_data[0]) for sub_data in data ] def get_seed_candles(self, symbol: str, tf: str = '1m', sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Candle]: params = {"sort": sort, "start": start, "end": end, "limit": limit} - data = self._GET(f"candles/trade:{tf}:{symbol}/hist?limit={limit}&start={start}&end={end}&sort={sort}", params=params) - return [ serializers.Candle.parse(*sub_data) for sub_data in data ] def get_leaderboards_hist( @@ -244,14 +237,9 @@ class _RestPublicEndpoints(_Requests): def get_funding_stats(self, symbol: str, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingStatistic]: params = { "start": start, "end": end, "limit": limit } - data = self._GET(f"funding/stats/{symbol}/hist", params=params) - return [ serializers.FundingStatistic.parse(*sub_data) for sub_data in data ] - def conf(self, config: Config) -> Any: - return self._GET(f"conf/{config}")[0] - def get_pulse_profile(self, nickname: str) -> PulseProfile: return serializers.PulseProfile.parse(*self._GET(f"pulse/profile/{nickname}")) @@ -266,19 +254,14 @@ class _RestPublicEndpoints(_Requests): return messages def get_trading_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], price_limit: Optional[Union[Decimal, float, str]] = None) -> TradingMarketAveragePrice: - data = { + return serializers.TradingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data={ "symbol": symbol, "amount": amount, "price_limit": price_limit - } - - return serializers.TradingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data=data)) + })) def get_funding_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], period: int, rate_limit: Optional[Union[Decimal, float, str]] = None) -> FundingMarketAveragePrice: - data = { - "symbol": symbol, "amount": amount, "period": period, - "rate_limit": rate_limit - } - - return serializers.FundingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data=data)) + return serializers.FundingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data={ + "symbol": symbol, "amount": amount, "period": period, "rate_limit": rate_limit + })) def get_fx_rate(self, ccy1: str, ccy2: str) -> FxRate: return serializers.FxRate.parse(*self._POST("calc/fx", data={ "ccy1": ccy1, "ccy2": ccy2 })) From 17fc29d4fa37b404b0e81ff209529b7318eb64dc Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 27 Jan 2023 16:38:58 +0100 Subject: [PATCH 125/265] Separate in different classes the content of BfxRestInterface.py script. --- bfxapi/rest/BfxRestInterface.py | 510 +-------------------- bfxapi/rest/_Requests.py | 75 +++ bfxapi/rest/_RestAuthenticatedEndpoints.py | 253 ++++++++++ bfxapi/rest/_RestPublicEndpoints.py | 188 ++++++++ 4 files changed, 523 insertions(+), 503 deletions(-) create mode 100644 bfxapi/rest/_Requests.py create mode 100644 bfxapi/rest/_RestAuthenticatedEndpoints.py create mode 100644 bfxapi/rest/_RestPublicEndpoints.py diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py index 2555eae..91d31a2 100644 --- a/bfxapi/rest/BfxRestInterface.py +++ b/bfxapi/rest/BfxRestInterface.py @@ -1,509 +1,13 @@ -import time, hmac, hashlib, json, requests +from typing import Optional -from decimal import Decimal -from datetime import datetime -from http import HTTPStatus +from ._RestPublicEndpoints import _RestPublicEndpoints -from typing import List, Union, Literal, Optional, Any, cast - -from . import serializers - -from .types import * -from .enums import Config, Sort, OrderType, FundingOfferType, Error -from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError - -from .. utils.encoder import JSONEncoder +from ._RestAuthenticatedEndpoints import _RestAuthenticatedEndpoints class BfxRestInterface(object): - def __init__(self, host, API_KEY = None, API_SECRET = None): + VERSION = 2 + + def __init__(self, host: str, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None): self.public = _RestPublicEndpoints(host=host) - self.auth = _RestAuthenticatedEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET) - -class _Requests(object): - def __init__(self, host, API_KEY = None, API_SECRET = None): - self.host, self.API_KEY, self.API_SECRET = host, API_KEY, API_SECRET - - def __build_authentication_headers(self, endpoint, data): - nonce = str(int(time.time()) * 1000) - - path = f"/api/v2/{endpoint}{nonce}" - - if data != None: path += data - - signature = hmac.new( - self.API_SECRET.encode("utf8"), - path.encode("utf8"), - hashlib.sha384 - ).hexdigest() - - return { - "bfx-nonce": nonce, - "bfx-signature": signature, - "bfx-apikey": self.API_KEY - } - - def _GET(self, endpoint, params = None): - response = requests.get(f"{self.host}/{endpoint}", params=params) - - if response.status_code == HTTPStatus.NOT_FOUND: - raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") - - data = response.json() - - if len(data) and data[0] == "error": - if data[1] == Error.ERR_PARAMS: - raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") - - if data[1] == None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC: - raise UnknownGenericError(f"The server replied to the request with a generic error with message: <{data[2]}>.") - - return data - - def _POST(self, endpoint, params = None, data = None): - headers = { "Content-Type": "application/json" } - - if isinstance(data, dict): - data = json.dumps({ key: value for key, value in data.items() if value != None}, cls=JSONEncoder) - - if self.API_KEY and self.API_SECRET: - headers = { **headers, **self.__build_authentication_headers(endpoint, data) } - - response = requests.post(f"{self.host}/{endpoint}", params=params, data=data, headers=headers) - - if response.status_code == HTTPStatus.NOT_FOUND: - raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") - - data = response.json() - - if len(data) and data[0] == "error": - if data[1] == Error.ERR_PARAMS: - raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") - - if data[1] == Error.ERR_AUTH_FAIL: - raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.") - - if data[1] == None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC: - raise UnknownGenericError(f"The server replied to the request with a generic error with message: <{data[2]}>.") - - return data - -class _RestPublicEndpoints(_Requests): - def conf(self, config: Config) -> Any: - return self._GET(f"conf/{config}")[0] - - def get_platform_status(self) -> PlatformStatus: - return serializers.PlatformStatus.parse(*self._GET("platform/status")) - - def get_tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]: - data = self._GET("tickers", params={ "symbols": ",".join(symbols) }) - - parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse } - - return [ cast(Union[TradingPairTicker, FundingCurrencyTicker], parsers[sub_data[0][0]](*sub_data)) for sub_data in data ] - - def get_t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]: - if isinstance(pairs, str) and pairs == "ALL": - return [ cast(TradingPairTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.symbol).startswith("t") ] - - data = self.get_tickers([ "t" + pair for pair in pairs ]) - - return cast(List[TradingPairTicker], data) - - def get_f_tickers(self, currencies: Union[List[str], Literal["ALL"]]) -> List[FundingCurrencyTicker]: - if isinstance(currencies, str) and currencies == "ALL": - return [ cast(FundingCurrencyTicker, 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 ]) - - return cast(List[FundingCurrencyTicker], data) - - def get_t_ticker(self, pair: str) -> TradingPairTicker: - return serializers.TradingPairTicker.parse(*self._GET(f"ticker/t{pair}"), skip=["SYMBOL"]) - - def get_f_ticker(self, currency: str) -> FundingCurrencyTicker: - return serializers.FundingCurrencyTicker.parse(*self._GET(f"ticker/f{currency}"), skip=["SYMBOL"]) - - def get_tickers_history(self, symbols: List[str], start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[TickersHistory]: - return [ serializers.TickersHistory.parse(*sub_data) for sub_data in self._GET("tickers/hist", params={ - "symbols": ",".join(symbols), - "start": start, "end": end, - "limit": limit - }) ] - - def get_t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[TradingPairTrade]: - params = { "limit": limit, "start": start, "end": end, "sort": sort } - data = self._GET(f"trades/{'t' + pair}/hist", params=params) - return [ serializers.TradingPairTrade.parse(*sub_data) for sub_data in data ] - - def get_f_trades(self, currency: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[FundingCurrencyTrade]: - params = { "limit": limit, "start": start, "end": end, "sort": sort } - data = self._GET(f"trades/{'f' + currency}/hist", params=params) - return [ serializers.FundingCurrencyTrade.parse(*sub_data) for sub_data in data ] - - def get_t_book(self, pair: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairBook]: - return [ serializers.TradingPairBook.parse(*sub_data) for sub_data in self._GET(f"book/{'t' + pair}/{precision}", params={ "len": len }) ] - - def get_f_book(self, currency: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyBook]: - return [ serializers.FundingCurrencyBook.parse(*sub_data) for sub_data in self._GET(f"book/{'f' + currency}/{precision}", params={ "len": len }) ] - - def get_t_raw_book(self, pair: str, len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairRawBook]: - return [ serializers.TradingPairRawBook.parse(*sub_data) for sub_data in self._GET(f"book/{'t' + pair}/R0", params={ "len": len }) ] - - def get_f_raw_book(self, currency: str, len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyRawBook]: - return [ serializers.FundingCurrencyRawBook.parse(*sub_data) for sub_data in self._GET(f"book/{'f' + currency}/R0", params={ "len": len }) ] - - def get_stats_hist( - self, - resource: str, - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> List[Statistic]: - params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"stats1/{resource}/hist", params=params) - return [ serializers.Statistic.parse(*sub_data) for sub_data in data ] - - def get_stats_last( - self, - resource: str, - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Statistic: - params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"stats1/{resource}/last", params=params) - return serializers.Statistic.parse(*data) - - def get_candles_hist( - self, - symbol: str, tf: str = "1m", - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> List[Candle]: - params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"candles/trade:{tf}:{symbol}/hist", params=params) - return [ serializers.Candle.parse(*sub_data) for sub_data in data ] - - def get_candles_last( - self, - symbol: str, tf: str = "1m", - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Candle: - params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"candles/trade:{tf}:{symbol}/last", params=params) - return serializers.Candle.parse(*data) - - def get_derivatives_status(self, keys: Union[List[str], Literal["ALL"]]) -> List[DerivativesStatus]: - if keys == "ALL": - params = { "keys": "ALL" } - else: params = { "keys": ",".join(keys) } - - data = self._GET(f"status/deriv", params=params) - - return [ serializers.DerivativesStatus.parse(*sub_data) for sub_data in data ] - - def get_derivatives_status_history( - self, - type: str, symbol: str, - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> List[DerivativesStatus]: - params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"status/{type}/{symbol}/hist", params=params) - return [ serializers.DerivativesStatus.parse(*sub_data, skip=[ "KEY" ]) for sub_data in data ] - - def get_liquidations(self, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Liquidation]: - params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET("liquidations/hist", params=params) - return [ serializers.Liquidation.parse(*sub_data[0]) for sub_data in data ] - - def get_seed_candles(self, symbol: str, tf: str = '1m', sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Candle]: - params = {"sort": sort, "start": start, "end": end, "limit": limit} - data = self._GET(f"candles/trade:{tf}:{symbol}/hist?limit={limit}&start={start}&end={end}&sort={sort}", params=params) - return [ serializers.Candle.parse(*sub_data) for sub_data in data ] - - def get_leaderboards_hist( - self, - resource: str, - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> List[Leaderboard]: - params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"rankings/{resource}/hist", params=params) - return [ serializers.Leaderboard.parse(*sub_data) for sub_data in data ] - - def get_leaderboards_last( - self, - resource: str, - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Leaderboard: - params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"rankings/{resource}/last", params=params) - return serializers.Leaderboard.parse(*data) - - def get_funding_stats(self, symbol: str, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingStatistic]: - params = { "start": start, "end": end, "limit": limit } - data = self._GET(f"funding/stats/{symbol}/hist", params=params) - return [ serializers.FundingStatistic.parse(*sub_data) for sub_data in data ] - - def get_pulse_profile(self, nickname: str) -> PulseProfile: - return serializers.PulseProfile.parse(*self._GET(f"pulse/profile/{nickname}")) - - def get_pulse_history(self, end: Optional[str] = None, limit: Optional[int] = None) -> List[PulseMessage]: - messages = list() - - for subdata in self._GET("pulse/hist", params={ "end": end, "limit": limit }): - subdata[18] = subdata[18][0] - message = serializers.PulseMessage.parse(*subdata) - messages.append(message) - - return messages - - def get_trading_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], price_limit: Optional[Union[Decimal, float, str]] = None) -> TradingMarketAveragePrice: - return serializers.TradingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data={ - "symbol": symbol, "amount": amount, "price_limit": price_limit - })) - - def get_funding_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], period: int, rate_limit: Optional[Union[Decimal, float, str]] = None) -> FundingMarketAveragePrice: - return serializers.FundingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data={ - "symbol": symbol, "amount": amount, "period": period, "rate_limit": rate_limit - })) - - def get_fx_rate(self, ccy1: str, ccy2: str) -> FxRate: - return serializers.FxRate.parse(*self._POST("calc/fx", data={ "ccy1": ccy1, "ccy2": ccy2 })) - -class _RestAuthenticatedEndpoints(_Requests): - def get_wallets(self) -> List[Wallet]: - return [ serializers.Wallet.parse(*sub_data) for sub_data in self._POST("auth/r/wallets") ] - - def get_orders(self, symbol: Optional[str] = None, ids: Optional[List[str]] = None) -> List[Order]: - endpoint = "auth/r/orders" - - if symbol != None: - endpoint += f"/{symbol}" - - return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, data={ "id": ids }) ] - - def get_positions(self) -> List[Position]: - return [ serializers.Position.parse(*sub_data) for sub_data in self._POST("auth/r/positions") ] - - def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, float, str], - price: Optional[Union[Decimal, float, str]] = None, lev: Optional[int] = None, - price_trailing: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_oco_stop: Optional[Union[Decimal, float, str]] = None, - gid: Optional[int] = None, cid: Optional[int] = None, - flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification[Order]: - data = { - "type": type, "symbol": symbol, "amount": amount, - "price": price, "lev": lev, - "price_trailing": price_trailing, "price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop, - "gid": gid, "cid": cid, - "flags": flags, "tif": tif, "meta": meta - } - - return serializers._Notification[Order](serializer=serializers.Order).parse(*self._POST("auth/w/order/submit", data=data)) - - def update_order(self, id: int, amount: Optional[Union[Decimal, float, str]] = None, price: Optional[Union[Decimal, float, str]] = None, - cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, - flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, float, str]] = None, - price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification[Order]: - data = { - "id": id, "amount": amount, "price": price, - "cid": cid, "cid_date": cid_date, "gid": gid, - "flags": flags, "lev": lev, "delta": delta, - "price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif - } - - return serializers._Notification[Order](serializer=serializers.Order).parse(*self._POST("auth/w/order/update", data=data)) - - def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None) -> Notification[Order]: - data = { - "id": id, - "cid": cid, - "cid_date": cid_date - } - - return serializers._Notification[Order](serializer=serializers.Order).parse(*self._POST("auth/w/order/cancel", data=data)) - - def cancel_order_multi(self, ids: Optional[List[int]] = None, cids: Optional[List[Tuple[int, str]]] = None, gids: Optional[List[int]] = None, all: bool = False) -> Notification[List[Order]]: - data = { - "ids": ids, - "cids": cids, - "gids": gids, - - "all": int(all) - } - - return serializers._Notification[List[Order]](serializer=serializers.Order, iterate=True).parse(*self._POST("auth/w/order/cancel/multi", data=data)) - - def get_orders_history(self, symbol: Optional[str] = None, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Order]: - if symbol == None: - endpoint = "auth/r/orders/hist" - else: endpoint = f"auth/r/orders/{symbol}/hist" - - data = { - "id": ids, - "start": start, "end": end, - "limit": limit - } - - return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] - - def get_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Trade]: - if symbol == None: - endpoint = "auth/r/trades/hist" - else: endpoint = f"auth/r/trades/{symbol}/hist" - - data = { - "sort": sort, - "start": start, "end": end, - "limit": limit - } - - return [ serializers.Trade.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] - - def get_order_trades(self, symbol: str, id: int) -> List[OrderTrade]: - return [ serializers.OrderTrade.parse(*sub_data) for sub_data in self._POST(f"auth/r/order/{symbol}:{id}/trades") ] - - def get_ledgers(self, currency: str, category: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Ledger]: - data = { - "category": category, - "start": start, "end": end, - "limit": limit - } - - return [ serializers.Ledger.parse(*sub_data) for sub_data in self._POST(f"auth/r/ledgers/{currency}/hist", data=data) ] - - def get_funding_offers(self, symbol: Optional[str] = None) -> List[FundingOffer]: - endpoint = "auth/r/funding/offers" - - if symbol != None: - endpoint += f"/{symbol}" - - return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint) ] - - def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, float, str], - rate: Union[Decimal, float, str], period: int, - flags: Optional[int] = 0) -> Notification[FundingOffer]: - data = { - "type": type, "symbol": symbol, "amount": amount, - "rate": rate, "period": period, - "flags": flags - } - - return serializers._Notification[FundingOffer](serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/submit", data=data)) - - def cancel_funding_offer(self, id: int) -> Notification[FundingOffer]: - return serializers._Notification[FundingOffer](serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/cancel", data={ "id": id })) - - def cancel_all_funding_offers(self, currency: str) -> Notification: - return serializers._Notification().parse(*self._POST("auth/w/funding/offer/cancel/all", data={ "currency": currency })) - - def get_funding_offers_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingOffer]: - if symbol == None: - endpoint = "auth/r/funding/offers/hist" - else: endpoint = f"auth/r/funding/offers/{symbol}/hist" - - data = { - "start": start, "end": end, - "limit": limit - } - - return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] - - def get_funding_credits(self, symbol: Optional[str] = None) -> List[FundingCredit]: - if symbol == None: - endpoint = "auth/r/funding/credits" - else: endpoint = f"auth/r/funding/credits/{symbol}" - - return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint) ] - - def get_funding_credits_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingCredit]: - if symbol == None: - endpoint = "auth/r/funding/credits/hist" - else: endpoint = f"auth/r/funding/credits/{symbol}/hist" - - data = { - "start": start, "end": end, - "limit": limit - } - - return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] - - def submit_wallet_transfer(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, float, str]) -> Notification[Transfer]: - data = { - "from": from_wallet, "to": to_wallet, - "currency": currency, "currency_to": currency_to, - "amount": amount - } - - return serializers._Notification[Transfer](serializer=serializers.Transfer).parse(*self._POST("auth/w/transfer", data=data)) - - def submit_wallet_withdraw(self, wallet: str, method: str, address: str, amount: Union[Decimal, float, str]) -> Notification[Withdrawal]: - data = { - "wallet": wallet, "method": method, - "address": address, "amount": amount, - } - - return serializers._Notification[Withdrawal](serializer=serializers.Withdrawal).parse(*self._POST("auth/w/withdraw", data=data)) - - def get_deposit_address(self, wallet: str, method: str, renew: bool = False) -> Notification[DepositAddress]: - data = { - "wallet": wallet, - "method": method, - "renew": int(renew) - } - - return serializers._Notification[DepositAddress](serializer=serializers.DepositAddress).parse(*self._POST("auth/w/deposit/address", data=data)) - - def get_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, float, str]) -> Invoice: - data = { - "wallet": wallet, "currency": currency, - "amount": amount - } - - return serializers.Invoice.parse(*self._POST("auth/w/deposit/invoice", data=data)) - - def get_movements(self, currency: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Movement]: - if currency == None: - endpoint = "auth/r/movements/hist" - else: endpoint = f"auth/r/movements/{currency}/hist" - - data = { - "start": start, "end": end, - "limit": limit - } - - return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] - - def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo: - response = self._POST(f"auth/r/info/margin/{symbol}") - - return serializers.SymbolMarginInfo.parse(*([response[1]] + response[2])) - - def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]: - return [ serializers.SymbolMarginInfo.parse(*([sub_data[1]] + sub_data[2])) for sub_data in self._POST(f"auth/r/info/margin/sym_all") ] - - def get_base_margin_info(self) -> BaseMarginInfo: - return serializers.BaseMarginInfo.parse(*(self._POST(f"auth/r/info/margin/base")[1])) - - def claim_position(self, id: int, amount: Optional[Union[Decimal, float, str]] = None) -> Notification[Claim]: - return serializers._Notification[Claim](serializer=serializers.Claim).parse(*self._POST("auth/w/position/claim", data={ "id": id, "amount": amount })) - - def get_increase_position_info(self, symbol: str, amount: Union[Decimal, float, str]) -> IncreaseInfo: - response = self._POST(f"auth/r/position/increase/info", data={ "symbol": symbol, "amount": amount }) - - return serializers.IncreaseInfo.parse(*( - response[0] + [response[1][0]] + response[1][1] + [response[1][2]] + response[4] + response[5] - )) - - def increase_position(self, symbol: str, amount: Union[Decimal, float, str]) -> Notification[Increase]: - return serializers._Notification[Increase](serializer=serializers.Increase).parse(*self._POST("auth/w/position/increase", data={ "symbol": symbol, "amount": amount })) - - def get_positions_history(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionHistory]: - return [ serializers.PositionHistory.parse(*sub_data) for sub_data in self._POST("auth/r/positions/hist", data={ "start": start, "end": end, "limit": limit }) ] - - def get_positions_snapshot(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionSnapshot]: - return [ serializers.PositionSnapshot.parse(*sub_data) for sub_data in self._POST("auth/r/positions/snap", data={ "start": start, "end": end, "limit": limit }) ] - - def get_positions_audit(self, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionAudit]: - return [ serializers.PositionAudit.parse(*sub_data) for sub_data in self._POST("auth/r/positions/audit", data={ "ids": ids, "start": start, "end": end, "limit": limit }) ] - - def set_derivative_position_collateral(self, symbol: str, collateral: Union[Decimal, float, str]) -> DerivativePositionCollateral: - return serializers.DerivativePositionCollateral.parse(*(self._POST("auth/w/deriv/collateral/set", data={ "symbol": symbol, "collateral": collateral })[0])) - - def get_derivative_position_collateral_limits(self, symbol: str) -> DerivativePositionCollateralLimits: - return serializers.DerivativePositionCollateralLimits.parse(*self._POST("auth/calc/deriv/collateral/limits", data={ "symbol": symbol })) + self.auth = _RestAuthenticatedEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET) \ No newline at end of file diff --git a/bfxapi/rest/_Requests.py b/bfxapi/rest/_Requests.py new file mode 100644 index 0000000..6e4383a --- /dev/null +++ b/bfxapi/rest/_Requests.py @@ -0,0 +1,75 @@ +import time, hmac, hashlib, json, requests + +from http import HTTPStatus +from .enums import Error +from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError + +from .. utils.encoder import JSONEncoder + +class _Requests(object): + def __init__(self, host, API_KEY = None, API_SECRET = None): + self.host, self.API_KEY, self.API_SECRET = host, API_KEY, API_SECRET + + def __build_authentication_headers(self, endpoint, data): + nonce = str(int(time.time()) * 1000) + + path = f"/api/v2/{endpoint}{nonce}" + + if data != None: path += data + + signature = hmac.new( + self.API_SECRET.encode("utf8"), + path.encode("utf8"), + hashlib.sha384 + ).hexdigest() + + return { + "bfx-nonce": nonce, + "bfx-signature": signature, + "bfx-apikey": self.API_KEY + } + + def _GET(self, endpoint, params = None): + response = requests.get(f"{self.host}/{endpoint}", params=params) + + if response.status_code == HTTPStatus.NOT_FOUND: + raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") + + data = response.json() + + if len(data) and data[0] == "error": + if data[1] == Error.ERR_PARAMS: + raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") + + if data[1] == None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC: + raise UnknownGenericError(f"The server replied to the request with a generic error with message: <{data[2]}>.") + + return data + + def _POST(self, endpoint, params = None, data = None, _ignore_authentication_headers = False): + headers = { "Content-Type": "application/json" } + + if isinstance(data, dict): + data = json.dumps({ key: value for key, value in data.items() if value != None}, cls=JSONEncoder) + + if self.API_KEY and self.API_SECRET and _ignore_authentication_headers == False: + headers = { **headers, **self.__build_authentication_headers(endpoint, data) } + + response = requests.post(f"{self.host}/{endpoint}", params=params, data=data, headers=headers) + + if response.status_code == HTTPStatus.NOT_FOUND: + raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") + + data = response.json() + + if len(data) and data[0] == "error": + if data[1] == Error.ERR_PARAMS: + raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") + + if data[1] == Error.ERR_AUTH_FAIL: + raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.") + + if data[1] == None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC: + raise UnknownGenericError(f"The server replied to the request with a generic error with message: <{data[2]}>.") + + return data \ No newline at end of file diff --git a/bfxapi/rest/_RestAuthenticatedEndpoints.py b/bfxapi/rest/_RestAuthenticatedEndpoints.py new file mode 100644 index 0000000..a464b8e --- /dev/null +++ b/bfxapi/rest/_RestAuthenticatedEndpoints.py @@ -0,0 +1,253 @@ +from typing import List, Union, Literal, Optional, Any, cast + +from .types import * + +from . import serializers + +from .enums import Config, Sort, OrderType, FundingOfferType +from decimal import Decimal +from datetime import datetime + +from ._Requests import _Requests + +class _RestAuthenticatedEndpoints(_Requests): + def get_wallets(self) -> List[Wallet]: + return [ serializers.Wallet.parse(*sub_data) for sub_data in self._POST("auth/r/wallets") ] + + def get_orders(self, symbol: Optional[str] = None, ids: Optional[List[str]] = None) -> List[Order]: + endpoint = "auth/r/orders" + + if symbol != None: + endpoint += f"/{symbol}" + + return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, data={ "id": ids }) ] + + def get_positions(self) -> List[Position]: + return [ serializers.Position.parse(*sub_data) for sub_data in self._POST("auth/r/positions") ] + + def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, float, str], + price: Optional[Union[Decimal, float, str]] = None, lev: Optional[int] = None, + price_trailing: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_oco_stop: Optional[Union[Decimal, float, str]] = None, + gid: Optional[int] = None, cid: Optional[int] = None, + flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification[Order]: + data = { + "type": type, "symbol": symbol, "amount": amount, + "price": price, "lev": lev, + "price_trailing": price_trailing, "price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop, + "gid": gid, "cid": cid, + "flags": flags, "tif": tif, "meta": meta + } + + return serializers._Notification[Order](serializer=serializers.Order).parse(*self._POST("auth/w/order/submit", data=data)) + + def update_order(self, id: int, amount: Optional[Union[Decimal, float, str]] = None, price: Optional[Union[Decimal, float, str]] = None, + cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, + flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, float, str]] = None, + price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification[Order]: + data = { + "id": id, "amount": amount, "price": price, + "cid": cid, "cid_date": cid_date, "gid": gid, + "flags": flags, "lev": lev, "delta": delta, + "price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif + } + + return serializers._Notification[Order](serializer=serializers.Order).parse(*self._POST("auth/w/order/update", data=data)) + + def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None) -> Notification[Order]: + data = { + "id": id, + "cid": cid, + "cid_date": cid_date + } + + return serializers._Notification[Order](serializer=serializers.Order).parse(*self._POST("auth/w/order/cancel", data=data)) + + def cancel_order_multi(self, ids: Optional[List[int]] = None, cids: Optional[List[Tuple[int, str]]] = None, gids: Optional[List[int]] = None, all: bool = False) -> Notification[List[Order]]: + data = { + "ids": ids, + "cids": cids, + "gids": gids, + + "all": int(all) + } + + return serializers._Notification[List[Order]](serializer=serializers.Order, iterate=True).parse(*self._POST("auth/w/order/cancel/multi", data=data)) + + def get_orders_history(self, symbol: Optional[str] = None, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Order]: + if symbol == None: + endpoint = "auth/r/orders/hist" + else: endpoint = f"auth/r/orders/{symbol}/hist" + + data = { + "id": ids, + "start": start, "end": end, + "limit": limit + } + + return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + + def get_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Trade]: + if symbol == None: + endpoint = "auth/r/trades/hist" + else: endpoint = f"auth/r/trades/{symbol}/hist" + + data = { + "sort": sort, + "start": start, "end": end, + "limit": limit + } + + return [ serializers.Trade.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + + def get_order_trades(self, symbol: str, id: int) -> List[OrderTrade]: + return [ serializers.OrderTrade.parse(*sub_data) for sub_data in self._POST(f"auth/r/order/{symbol}:{id}/trades") ] + + def get_ledgers(self, currency: str, category: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Ledger]: + data = { + "category": category, + "start": start, "end": end, + "limit": limit + } + + return [ serializers.Ledger.parse(*sub_data) for sub_data in self._POST(f"auth/r/ledgers/{currency}/hist", data=data) ] + + def get_funding_offers(self, symbol: Optional[str] = None) -> List[FundingOffer]: + endpoint = "auth/r/funding/offers" + + if symbol != None: + endpoint += f"/{symbol}" + + return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint) ] + + def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, float, str], + rate: Union[Decimal, float, str], period: int, + flags: Optional[int] = 0) -> Notification[FundingOffer]: + data = { + "type": type, "symbol": symbol, "amount": amount, + "rate": rate, "period": period, + "flags": flags + } + + return serializers._Notification[FundingOffer](serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/submit", data=data)) + + def cancel_funding_offer(self, id: int) -> Notification[FundingOffer]: + return serializers._Notification[FundingOffer](serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/cancel", data={ "id": id })) + + def cancel_all_funding_offers(self, currency: str) -> Notification: + return serializers._Notification().parse(*self._POST("auth/w/funding/offer/cancel/all", data={ "currency": currency })) + + def get_funding_offers_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingOffer]: + if symbol == None: + endpoint = "auth/r/funding/offers/hist" + else: endpoint = f"auth/r/funding/offers/{symbol}/hist" + + data = { + "start": start, "end": end, + "limit": limit + } + + return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + + def get_funding_credits(self, symbol: Optional[str] = None) -> List[FundingCredit]: + if symbol == None: + endpoint = "auth/r/funding/credits" + else: endpoint = f"auth/r/funding/credits/{symbol}" + + return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint) ] + + def get_funding_credits_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingCredit]: + if symbol == None: + endpoint = "auth/r/funding/credits/hist" + else: endpoint = f"auth/r/funding/credits/{symbol}/hist" + + data = { + "start": start, "end": end, + "limit": limit + } + + return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + + def submit_wallet_transfer(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, float, str]) -> Notification[Transfer]: + data = { + "from": from_wallet, "to": to_wallet, + "currency": currency, "currency_to": currency_to, + "amount": amount + } + + return serializers._Notification[Transfer](serializer=serializers.Transfer).parse(*self._POST("auth/w/transfer", data=data)) + + def submit_wallet_withdraw(self, wallet: str, method: str, address: str, amount: Union[Decimal, float, str]) -> Notification[Withdrawal]: + data = { + "wallet": wallet, "method": method, + "address": address, "amount": amount, + } + + return serializers._Notification[Withdrawal](serializer=serializers.Withdrawal).parse(*self._POST("auth/w/withdraw", data=data)) + + def get_deposit_address(self, wallet: str, method: str, renew: bool = False) -> Notification[DepositAddress]: + data = { + "wallet": wallet, + "method": method, + "renew": int(renew) + } + + return serializers._Notification[DepositAddress](serializer=serializers.DepositAddress).parse(*self._POST("auth/w/deposit/address", data=data)) + + def get_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, float, str]) -> Invoice: + data = { + "wallet": wallet, "currency": currency, + "amount": amount + } + + return serializers.Invoice.parse(*self._POST("auth/w/deposit/invoice", data=data)) + + def get_movements(self, currency: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Movement]: + if currency == None: + endpoint = "auth/r/movements/hist" + else: endpoint = f"auth/r/movements/{currency}/hist" + + data = { + "start": start, "end": end, + "limit": limit + } + + return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + + def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo: + response = self._POST(f"auth/r/info/margin/{symbol}") + + return serializers.SymbolMarginInfo.parse(*([response[1]] + response[2])) + + def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]: + return [ serializers.SymbolMarginInfo.parse(*([sub_data[1]] + sub_data[2])) for sub_data in self._POST(f"auth/r/info/margin/sym_all") ] + + def get_base_margin_info(self) -> BaseMarginInfo: + return serializers.BaseMarginInfo.parse(*(self._POST(f"auth/r/info/margin/base")[1])) + + def claim_position(self, id: int, amount: Optional[Union[Decimal, float, str]] = None) -> Notification[Claim]: + return serializers._Notification[Claim](serializer=serializers.Claim).parse(*self._POST("auth/w/position/claim", data={ "id": id, "amount": amount })) + + def get_increase_position_info(self, symbol: str, amount: Union[Decimal, float, str]) -> IncreaseInfo: + response = self._POST(f"auth/r/position/increase/info", data={ "symbol": symbol, "amount": amount }) + + return serializers.IncreaseInfo.parse(*( + response[0] + [response[1][0]] + response[1][1] + [response[1][2]] + response[4] + response[5] + )) + + def increase_position(self, symbol: str, amount: Union[Decimal, float, str]) -> Notification[Increase]: + return serializers._Notification[Increase](serializer=serializers.Increase).parse(*self._POST("auth/w/position/increase", data={ "symbol": symbol, "amount": amount })) + + def get_positions_history(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionHistory]: + return [ serializers.PositionHistory.parse(*sub_data) for sub_data in self._POST("auth/r/positions/hist", data={ "start": start, "end": end, "limit": limit }) ] + + def get_positions_snapshot(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionSnapshot]: + return [ serializers.PositionSnapshot.parse(*sub_data) for sub_data in self._POST("auth/r/positions/snap", data={ "start": start, "end": end, "limit": limit }) ] + + def get_positions_audit(self, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionAudit]: + return [ serializers.PositionAudit.parse(*sub_data) for sub_data in self._POST("auth/r/positions/audit", data={ "ids": ids, "start": start, "end": end, "limit": limit }) ] + + def set_derivative_position_collateral(self, symbol: str, collateral: Union[Decimal, float, str]) -> DerivativePositionCollateral: + return serializers.DerivativePositionCollateral.parse(*(self._POST("auth/w/deriv/collateral/set", data={ "symbol": symbol, "collateral": collateral })[0])) + + def get_derivative_position_collateral_limits(self, symbol: str) -> DerivativePositionCollateralLimits: + return serializers.DerivativePositionCollateralLimits.parse(*self._POST("auth/calc/deriv/collateral/limits", data={ "symbol": symbol })) \ No newline at end of file diff --git a/bfxapi/rest/_RestPublicEndpoints.py b/bfxapi/rest/_RestPublicEndpoints.py new file mode 100644 index 0000000..5157256 --- /dev/null +++ b/bfxapi/rest/_RestPublicEndpoints.py @@ -0,0 +1,188 @@ +from typing import List, Union, Literal, Optional, Any, cast + +from .types import * + +from . import serializers + +from .enums import Config, Sort +from decimal import Decimal + +from ._Requests import _Requests + +class _RestPublicEndpoints(_Requests): + def conf(self, config: Config) -> Any: + return self._GET(f"conf/{config}")[0] + + def get_platform_status(self) -> PlatformStatus: + return serializers.PlatformStatus.parse(*self._GET("platform/status")) + + def get_tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]: + data = self._GET("tickers", params={ "symbols": ",".join(symbols) }) + + parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse } + + return [ cast(Union[TradingPairTicker, FundingCurrencyTicker], parsers[sub_data[0][0]](*sub_data)) for sub_data in data ] + + def get_t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]: + if isinstance(pairs, str) and pairs == "ALL": + return [ cast(TradingPairTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.symbol).startswith("t") ] + + data = self.get_tickers([ "t" + pair for pair in pairs ]) + + return cast(List[TradingPairTicker], data) + + def get_f_tickers(self, currencies: Union[List[str], Literal["ALL"]]) -> List[FundingCurrencyTicker]: + if isinstance(currencies, str) and currencies == "ALL": + return [ cast(FundingCurrencyTicker, 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 ]) + + return cast(List[FundingCurrencyTicker], data) + + def get_t_ticker(self, pair: str) -> TradingPairTicker: + return serializers.TradingPairTicker.parse(*self._GET(f"ticker/t{pair}"), skip=["SYMBOL"]) + + def get_f_ticker(self, currency: str) -> FundingCurrencyTicker: + return serializers.FundingCurrencyTicker.parse(*self._GET(f"ticker/f{currency}"), skip=["SYMBOL"]) + + def get_tickers_history(self, symbols: List[str], start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[TickersHistory]: + return [ serializers.TickersHistory.parse(*sub_data) for sub_data in self._GET("tickers/hist", params={ + "symbols": ",".join(symbols), + "start": start, "end": end, + "limit": limit + }) ] + + def get_t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[TradingPairTrade]: + params = { "limit": limit, "start": start, "end": end, "sort": sort } + data = self._GET(f"trades/{'t' + pair}/hist", params=params) + return [ serializers.TradingPairTrade.parse(*sub_data) for sub_data in data ] + + def get_f_trades(self, currency: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[FundingCurrencyTrade]: + params = { "limit": limit, "start": start, "end": end, "sort": sort } + data = self._GET(f"trades/{'f' + currency}/hist", params=params) + return [ serializers.FundingCurrencyTrade.parse(*sub_data) for sub_data in data ] + + def get_t_book(self, pair: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairBook]: + return [ serializers.TradingPairBook.parse(*sub_data) for sub_data in self._GET(f"book/{'t' + pair}/{precision}", params={ "len": len }) ] + + def get_f_book(self, currency: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyBook]: + return [ serializers.FundingCurrencyBook.parse(*sub_data) for sub_data in self._GET(f"book/{'f' + currency}/{precision}", params={ "len": len }) ] + + def get_t_raw_book(self, pair: str, len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairRawBook]: + return [ serializers.TradingPairRawBook.parse(*sub_data) for sub_data in self._GET(f"book/{'t' + pair}/R0", params={ "len": len }) ] + + def get_f_raw_book(self, currency: str, len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyRawBook]: + return [ serializers.FundingCurrencyRawBook.parse(*sub_data) for sub_data in self._GET(f"book/{'f' + currency}/R0", params={ "len": len }) ] + + def get_stats_hist( + self, + resource: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> List[Statistic]: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self._GET(f"stats1/{resource}/hist", params=params) + return [ serializers.Statistic.parse(*sub_data) for sub_data in data ] + + def get_stats_last( + self, + resource: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Statistic: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self._GET(f"stats1/{resource}/last", params=params) + return serializers.Statistic.parse(*data) + + def get_candles_hist( + self, + symbol: str, tf: str = "1m", + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> List[Candle]: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self._GET(f"candles/trade:{tf}:{symbol}/hist", params=params) + return [ serializers.Candle.parse(*sub_data) for sub_data in data ] + + def get_candles_last( + self, + symbol: str, tf: str = "1m", + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Candle: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self._GET(f"candles/trade:{tf}:{symbol}/last", params=params) + return serializers.Candle.parse(*data) + + def get_derivatives_status(self, keys: Union[List[str], Literal["ALL"]]) -> List[DerivativesStatus]: + if keys == "ALL": + params = { "keys": "ALL" } + else: params = { "keys": ",".join(keys) } + + data = self._GET(f"status/deriv", params=params) + + return [ serializers.DerivativesStatus.parse(*sub_data) for sub_data in data ] + + def get_derivatives_status_history( + self, + type: str, symbol: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> List[DerivativesStatus]: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self._GET(f"status/{type}/{symbol}/hist", params=params) + return [ serializers.DerivativesStatus.parse(*sub_data, skip=[ "KEY" ]) for sub_data in data ] + + def get_liquidations(self, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Liquidation]: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self._GET("liquidations/hist", params=params) + return [ serializers.Liquidation.parse(*sub_data[0]) for sub_data in data ] + + def get_seed_candles(self, symbol: str, tf: str = '1m', sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Candle]: + params = {"sort": sort, "start": start, "end": end, "limit": limit} + data = self._GET(f"candles/trade:{tf}:{symbol}/hist?limit={limit}&start={start}&end={end}&sort={sort}", params=params) + return [ serializers.Candle.parse(*sub_data) for sub_data in data ] + + def get_leaderboards_hist( + self, + resource: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> List[Leaderboard]: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self._GET(f"rankings/{resource}/hist", params=params) + return [ serializers.Leaderboard.parse(*sub_data) for sub_data in data ] + + def get_leaderboards_last( + self, + resource: str, + sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None + ) -> Leaderboard: + params = { "sort": sort, "start": start, "end": end, "limit": limit } + data = self._GET(f"rankings/{resource}/last", params=params) + return serializers.Leaderboard.parse(*data) + + def get_funding_stats(self, symbol: str, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingStatistic]: + params = { "start": start, "end": end, "limit": limit } + data = self._GET(f"funding/stats/{symbol}/hist", params=params) + return [ serializers.FundingStatistic.parse(*sub_data) for sub_data in data ] + + def get_pulse_profile(self, nickname: str) -> PulseProfile: + return serializers.PulseProfile.parse(*self._GET(f"pulse/profile/{nickname}")) + + def get_pulse_history(self, end: Optional[str] = None, limit: Optional[int] = None) -> List[PulseMessage]: + messages = list() + + for subdata in self._GET("pulse/hist", params={ "end": end, "limit": limit }): + subdata[18] = subdata[18][0] + message = serializers.PulseMessage.parse(*subdata) + messages.append(message) + + return messages + + def get_trading_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], price_limit: Optional[Union[Decimal, float, str]] = None) -> TradingMarketAveragePrice: + return serializers.TradingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data={ + "symbol": symbol, "amount": amount, "price_limit": price_limit + })) + + def get_funding_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], period: int, rate_limit: Optional[Union[Decimal, float, str]] = None) -> FundingMarketAveragePrice: + return serializers.FundingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data={ + "symbol": symbol, "amount": amount, "period": period, "rate_limit": rate_limit + })) + + def get_fx_rate(self, ccy1: str, ccy2: str) -> FxRate: + return serializers.FxRate.parse(*self._POST("calc/fx", data={ "ccy1": ccy1, "ccy2": ccy2 })) \ No newline at end of file From 2fc31db7a36ac2ec880487907d5a001791b99cd6 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 27 Jan 2023 17:23:41 +0100 Subject: [PATCH 126/265] Add get_funding_loans and get_funding_loans_history endpoints to _RestAuthenticatedEndpoints.py. --- bfxapi/rest/_RestAuthenticatedEndpoints.py | 19 ++++++++++++++++ bfxapi/rest/serializers.py | 25 ++++++++++++++++++++++ bfxapi/rest/types.py | 20 +++++++++++++++++ examples/rest/get_authenticated_data.py | 2 +- 4 files changed, 65 insertions(+), 1 deletion(-) diff --git a/bfxapi/rest/_RestAuthenticatedEndpoints.py b/bfxapi/rest/_RestAuthenticatedEndpoints.py index a464b8e..61ee64d 100644 --- a/bfxapi/rest/_RestAuthenticatedEndpoints.py +++ b/bfxapi/rest/_RestAuthenticatedEndpoints.py @@ -148,6 +148,25 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + def get_funding_loans(self, symbol: Optional[str] = None) -> List[FundingLoan]: + if symbol == None: + endpoint = "auth/r/funding/loans" + else: endpoint = f"auth/r/funding/loans/{symbol}" + + return [ serializers.FundingLoan.parse(*sub_data) for sub_data in self._POST(endpoint) ] + + def get_funding_loans_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingLoan]: + if symbol == None: + endpoint = "auth/r/funding/loans/hist" + else: endpoint = f"auth/r/funding/loans/{symbol}/hist" + + data = { + "start": start, "end": end, + "limit": limit + } + + return [ serializers.FundingLoan.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + def get_funding_credits(self, symbol: Optional[str] = None) -> List[FundingCredit]: if symbol == None: endpoint = "auth/r/funding/credits" diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index a6cf582..44296d9 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -382,6 +382,31 @@ Ledger = generate_labeler_serializer("Ledger", klass=types.Ledger, labels=[ "description" ]) +FundingLoan = generate_labeler_serializer("FundingLoan", klass=types.FundingLoan, labels=[ + "id", + "symbol", + "side", + "mts_create", + "mts_update", + "amount", + "flags", + "status", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "rate", + "period", + "mts_opening", + "mts_last_payout", + "notify", + "hidden", + "_PLACEHOLDER", + "renew", + "rate_real", + "no_close" +]) + + FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.FundingCredit, labels=[ "id", "symbol", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 4850404..58ce507 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -306,6 +306,26 @@ class Ledger(_Type): balance: float description: str +@dataclass +class FundingLoan(_Type): + id: int + symbol: str + side: int + mts_create: int + mts_update: int + amount: float + flags: int + status: str + rate: float + period: int + mts_opening: int + mts_last_payout: int + notify: int + hidden: int + renew: int + rate_real: float + no_close: int + @dataclass class FundingCredit(_Type): id: int diff --git a/examples/rest/get_authenticated_data.py b/examples/rest/get_authenticated_data.py index 4af5165..2ff1de6 100644 --- a/examples/rest/get_authenticated_data.py +++ b/examples/rest/get_authenticated_data.py @@ -69,7 +69,7 @@ def log_funding_loans(): def log_funding_loans_history(): - loans = bfx.rest.auth.get_funding_loan_history(symbol='fUSD', start=0, end=now) + loans = bfx.rest.auth.get_funding_loans_history(symbol='fUSD', start=0, end=now) print("Funding loan history:") [print(l) for l in loans] From 0dd23b3ba5de9396156994498e5d7fa00730a60a Mon Sep 17 00:00:00 2001 From: itsdeka Date: Mon, 30 Jan 2023 13:12:06 +0100 Subject: [PATCH 127/265] fix example --- examples/rest/create_funding_offer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/rest/create_funding_offer.py b/examples/rest/create_funding_offer.py index 343fa1d..fd10cc8 100644 --- a/examples/rest/create_funding_offer.py +++ b/examples/rest/create_funding_offer.py @@ -23,7 +23,7 @@ notification = bfx.rest.auth.submit_funding_offer( print("Offer notification:", notification) -offers = bfx.rest.auth.get_active_funding_offers(symbol="fUSD") +offers = bfx.rest.auth.get_funding_offers(symbol="fUSD") print("Offers:", offers) From 374739b6561edccf0d96fbbab03193cbbfdd12e5 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Mon, 30 Jan 2023 14:51:57 +0100 Subject: [PATCH 128/265] return taken funding --- bfxapi/rest/_RestAuthenticatedEndpoints.py | 5 +++++ bfxapi/rest/serializers.py | 8 ++++---- bfxapi/rest/types.py | 6 +++--- examples/rest/return_taken_funding.py | 22 ++++++++++++++++++++++ 4 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 examples/rest/return_taken_funding.py diff --git a/bfxapi/rest/_RestAuthenticatedEndpoints.py b/bfxapi/rest/_RestAuthenticatedEndpoints.py index 61ee64d..ce4dc1b 100644 --- a/bfxapi/rest/_RestAuthenticatedEndpoints.py +++ b/bfxapi/rest/_RestAuthenticatedEndpoints.py @@ -186,6 +186,11 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + def submit_funding_close(self, id: int) -> Notification[Literal[None]]: + return serializers._Notification[Literal[None]]().parse( + *self._POST("auth/w/funding/close", data={ "id": id }) + ) + def submit_wallet_transfer(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, float, str]) -> Notification[Transfer]: data = { "from": from_wallet, "to": to_wallet, diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index bde6ac7..2f2a8fd 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -319,8 +319,8 @@ Position = generate_labeler_serializer("Position", klass=types.Position, labels= FundingOffer = generate_labeler_serializer("FundingOffer", klass=types.FundingOffer, labels=[ "id", "symbol", - "mts_created", - "mts_updated", + "mts_create", + "mts_update", "amount", "amount_orig", "offer_type", @@ -477,8 +477,8 @@ Movement = generate_labeler_serializer("Movement", klass=types.Movement, labels= "currency_name", "_PLACEHOLDER", "_PLACEHOLDER", - "mts_started", - "mts_updated", + "mts_start", + "mts_update", "_PLACEHOLDER", "_PLACEHOLDER", "status", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 58ce507..af53672 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -268,7 +268,7 @@ class FundingOffer(_Type): notify: bool hidden: int renew: bool - + @dataclass class Trade(_Type): id: int @@ -383,8 +383,8 @@ class Movement(_Type): id: str currency: str currency_name: str - mts_started: int - mts_updated: int + mts_start: int + mts_update: int status: str amount: int fees: int diff --git a/examples/rest/return_taken_funding.py b/examples/rest/return_taken_funding.py new file mode 100644 index 0000000..ccb0c2b --- /dev/null +++ b/examples/rest/return_taken_funding.py @@ -0,0 +1,22 @@ +# python -c "import examples.rest.return_taken_funding" + +import os + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +loans = bfx.rest.auth.get_funding_loans(symbol="fUSD") + +for loan in loans: + print(f"Loan {loan}") + + notification = bfx.rest.auth.submit_funding_close( + id=loan.id + ) + + print("Funding close notification:", notification) \ No newline at end of file From 0ac14dfeb559ef662975459cb78e9fb76bab2430 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Mon, 30 Jan 2023 15:16:48 +0100 Subject: [PATCH 129/265] funding auto renew --- bfxapi/rest/_RestAuthenticatedEndpoints.py | 7 +++++++ bfxapi/rest/serializers.py | 7 +++++++ bfxapi/rest/types.py | 7 +++++++ examples/rest/funding_auto_renew.py | 21 +++++++++++++++++++++ 4 files changed, 42 insertions(+) create mode 100644 examples/rest/funding_auto_renew.py diff --git a/bfxapi/rest/_RestAuthenticatedEndpoints.py b/bfxapi/rest/_RestAuthenticatedEndpoints.py index ce4dc1b..af01a29 100644 --- a/bfxapi/rest/_RestAuthenticatedEndpoints.py +++ b/bfxapi/rest/_RestAuthenticatedEndpoints.py @@ -191,6 +191,13 @@ class _RestAuthenticatedEndpoints(_Requests): *self._POST("auth/w/funding/close", data={ "id": id }) ) + def submit_funding_toggle_auto_renew(self, status: bool, currency: str, amount: Optional[str] = None, rate: Optional[int] = None, period: Optional[int] = None) -> Notification[FundingAutoRenew]: + return serializers._Notification[FundingAutoRenew](serializer=serializers.FundingAutoRenew).parse(*self._POST("auth/w/funding/auto", data={ + "status": int(status), + "currency": currency, "amount": amount, + "rate": rate, "period": period + })) + def submit_wallet_transfer(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, float, str]) -> Notification[Transfer]: data = { "from": from_wallet, "to": to_wallet, diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 2f2a8fd..6e7103d 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -431,6 +431,13 @@ FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.Funding "position_pair" ]) +FundingAutoRenew = generate_labeler_serializer("FundingAutoRenew", klass=types.FundingAutoRenew, labels=[ + "currency", + "period", + "rate", + "threshold" +]) + Transfer = generate_labeler_serializer("Transfer", klass=types.Transfer, labels=[ "mts", "wallet_from", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index af53672..ce6d360 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -347,6 +347,13 @@ class FundingCredit(_Type): no_close: int position_pair: str +@dataclass +class FundingAutoRenew(_Type): + currency: str + period: int + rate: float + threshold: float + @dataclass class Transfer(_Type): mts: int diff --git a/examples/rest/funding_auto_renew.py b/examples/rest/funding_auto_renew.py new file mode 100644 index 0000000..eec7c47 --- /dev/null +++ b/examples/rest/funding_auto_renew.py @@ -0,0 +1,21 @@ +# python -c "import examples.rest.funding_auto_renew" + +import os + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +notification = bfx.rest.auth.submit_funding_toggle_auto_renew( + status=True, + currency="USD", + amount="150", + rate="0", + period=2 +) + +print("Renew toggle notification:", notification) \ No newline at end of file From 01d638cf9cb11556b32b80963292c0b9a5773c93 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Mon, 30 Jan 2023 16:16:18 +0100 Subject: [PATCH 130/265] keep taken funding --- bfxapi/rest/_RestAuthenticatedEndpoints.py | 15 ++++++++++--- examples/rest/keep_taken_funding.py | 26 ++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 examples/rest/keep_taken_funding.py diff --git a/bfxapi/rest/_RestAuthenticatedEndpoints.py b/bfxapi/rest/_RestAuthenticatedEndpoints.py index af01a29..7dc8136 100644 --- a/bfxapi/rest/_RestAuthenticatedEndpoints.py +++ b/bfxapi/rest/_RestAuthenticatedEndpoints.py @@ -133,8 +133,10 @@ class _RestAuthenticatedEndpoints(_Requests): def cancel_funding_offer(self, id: int) -> Notification[FundingOffer]: return serializers._Notification[FundingOffer](serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/cancel", data={ "id": id })) - def cancel_all_funding_offers(self, currency: str) -> Notification: - return serializers._Notification().parse(*self._POST("auth/w/funding/offer/cancel/all", data={ "currency": currency })) + def cancel_all_funding_offers(self, currency: str) -> Notification[Literal[None]]: + return serializers._Notification[Literal[None]](serializer=None).parse( + *self._POST("auth/w/funding/offer/cancel/all", data={ "currency": currency }) + ) def get_funding_offers_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingOffer]: if symbol == None: @@ -187,7 +189,7 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] def submit_funding_close(self, id: int) -> Notification[Literal[None]]: - return serializers._Notification[Literal[None]]().parse( + return serializers._Notification[Literal[None]](serializer=None).parse( *self._POST("auth/w/funding/close", data={ "id": id }) ) @@ -198,6 +200,13 @@ class _RestAuthenticatedEndpoints(_Requests): "rate": rate, "period": period })) + def submit_funding_toggle_keep(self, type: Literal["credit", "loan"], ids: Optional[List[int]] = None, changes: Optional[Dict[int, bool]] = None) -> Notification[Literal[None]]: + return serializers._Notification[Literal[None]](serializer=None).parse(*self._POST("auth/w/funding/keep", data={ + "type": type, + "id": ids, + "changes": changes + })) + def submit_wallet_transfer(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, float, str]) -> Notification[Transfer]: data = { "from": from_wallet, "to": to_wallet, diff --git a/examples/rest/keep_taken_funding.py b/examples/rest/keep_taken_funding.py new file mode 100644 index 0000000..9693f95 --- /dev/null +++ b/examples/rest/keep_taken_funding.py @@ -0,0 +1,26 @@ +# python -c "import examples.rest.keep_taken_funding" + +import os + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +loans = bfx.rest.auth.get_funding_loans(symbol="fUSD") + +for loan in loans: + print(f"Loan {loan}") + + notification = bfx.rest.auth.submit_funding_toggle_keep( + funding_type="loan", + ids=[loan.id], + changes={ + loan.id: 2 # (1 if true, 2 if false) + } + ) + + print("Funding keep notification:", notification) \ No newline at end of file From b9850fa45152fc1a555eb5c16f3054f6e33a6300 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 30 Jan 2023 17:31:07 +0100 Subject: [PATCH 131/265] funding info Co-Authored-By: itsdeka --- bfxapi/rest/_RestAuthenticatedEndpoints.py | 5 +++++ bfxapi/rest/serializers.py | 8 ++++++++ bfxapi/rest/types.py | 8 ++++++++ examples/rest/get_funding_info.py | 13 +++++++++++++ 4 files changed, 34 insertions(+) create mode 100644 examples/rest/get_funding_info.py diff --git a/bfxapi/rest/_RestAuthenticatedEndpoints.py b/bfxapi/rest/_RestAuthenticatedEndpoints.py index 7dc8136..b3a7453 100644 --- a/bfxapi/rest/_RestAuthenticatedEndpoints.py +++ b/bfxapi/rest/_RestAuthenticatedEndpoints.py @@ -188,6 +188,11 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + def get_funding_info(self, key: str) -> FundingInfo: + response = self._POST(f"auth/r/info/funding/{key}") + + return serializers.FundingInfo.parse(*([response[1]] + response[2])) + def submit_funding_close(self, id: int) -> Notification[Literal[None]]: return serializers._Notification[Literal[None]](serializer=None).parse( *self._POST("auth/w/funding/close", data={ "id": id }) diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 6e7103d..56021fe 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -438,6 +438,14 @@ FundingAutoRenew = generate_labeler_serializer("FundingAutoRenew", klass=types.F "threshold" ]) +FundingInfo = generate_labeler_serializer("FundingInfo", klass=types.FundingInfo, labels=[ + "symbol", + "yield_loan", + "yield_lend", + "duration_loan", + "duration_lend" +]) + Transfer = generate_labeler_serializer("Transfer", klass=types.Transfer, labels=[ "mts", "wallet_from", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index ce6d360..7554499 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -354,6 +354,14 @@ class FundingAutoRenew(_Type): rate: float threshold: float +@dataclass() +class FundingInfo(_Type): + symbol: str + yield_loan: float + yield_lend: float + duration_loan: float + duration_lend: float + @dataclass class Transfer(_Type): mts: int diff --git a/examples/rest/get_funding_info.py b/examples/rest/get_funding_info.py new file mode 100644 index 0000000..83d0635 --- /dev/null +++ b/examples/rest/get_funding_info.py @@ -0,0 +1,13 @@ +# python -c "import examples.rest.get_funding_info" + +import os + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +print(bfx.rest.auth.get_funding_info(key="fUSD")) \ No newline at end of file From 0ddbd6e76eac3f2976ca2ef613c1d60dad685bf8 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 30 Jan 2023 17:36:50 +0100 Subject: [PATCH 132/265] funding trades Co-Authored-By: itsdeka --- bfxapi/rest/_RestAuthenticatedEndpoints.py | 13 +++++++++++++ bfxapi/rest/serializers.py | 10 ++++++++++ bfxapi/rest/types.py | 10 ++++++++++ examples/rest/get_funding_trades_history.py | 13 +++++++++++++ 4 files changed, 46 insertions(+) create mode 100644 examples/rest/get_funding_trades_history.py diff --git a/bfxapi/rest/_RestAuthenticatedEndpoints.py b/bfxapi/rest/_RestAuthenticatedEndpoints.py index b3a7453..d30d32f 100644 --- a/bfxapi/rest/_RestAuthenticatedEndpoints.py +++ b/bfxapi/rest/_RestAuthenticatedEndpoints.py @@ -99,6 +99,19 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.Trade.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + def get_funding_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingTrade]: + if symbol == None: + endpoint = "auth/r/funding/trades/hist" + else: endpoint = f"auth/r/funding/trades/{symbol}/hist" + + data = { + "sort": sort, + "start": start, "end": end, + "limit": limit + } + + return [ serializers.FundingTrade.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + def get_order_trades(self, symbol: str, id: int) -> List[OrderTrade]: return [ serializers.OrderTrade.parse(*sub_data) for sub_data in self._POST(f"auth/r/order/{symbol}:{id}/trades") ] diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 56021fe..9526922 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -355,6 +355,16 @@ Trade = generate_labeler_serializer("Trade", klass=types.Trade, labels=[ "cid" ]) +FundingTrade = generate_labeler_serializer("FundingTrade", klass=types.FundingTrade, labels=[ + "id", + "currency", + "mts_create", + "offer_id", + "amount", + "rate", + "period" +]) + OrderTrade = generate_labeler_serializer("OrderTrade", klass=types.OrderTrade, labels=[ "id", "pair", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 7554499..2152b3f 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -284,6 +284,16 @@ class Trade(_Type): fee_currency: str cid: int +@dataclass() +class FundingTrade(_Type): + id: int + currency: str + mts_create: int + offer_id: int + amount: float + rate: float + period: int + @dataclass class OrderTrade(_Type): id: int diff --git a/examples/rest/get_funding_trades_history.py b/examples/rest/get_funding_trades_history.py new file mode 100644 index 0000000..c1cc8e6 --- /dev/null +++ b/examples/rest/get_funding_trades_history.py @@ -0,0 +1,13 @@ +# python -c "import examples.rest.get_funding_trades_history" + +import os + +from bfxapi.client import Client, Constants + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +print(bfx.rest.auth.get_funding_trades_history()) \ No newline at end of file From 7b313ddcab7b90326a19a66e45e90a1e61fc27ca Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 31 Jan 2023 17:27:14 +0100 Subject: [PATCH 133/265] Merge TradeExecutionUpdate and TradeExecuted serializers and types in websocket subpackage. --- bfxapi/rest/exceptions.py | 1 + bfxapi/websocket/handlers.py | 3 +-- bfxapi/websocket/serializers.py | 17 +---------------- bfxapi/websocket/types.py | 19 +++---------------- 4 files changed, 6 insertions(+), 34 deletions(-) diff --git a/bfxapi/rest/exceptions.py b/bfxapi/rest/exceptions.py index beff7bc..9fbf3a4 100644 --- a/bfxapi/rest/exceptions.py +++ b/bfxapi/rest/exceptions.py @@ -3,6 +3,7 @@ from .. exceptions import BfxBaseException __all__ = [ "BfxRestException", + "ResourceNotFound", "RequestParametersError", "ResourceNotFound", "InvalidAuthenticationCredentials" diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index c56046a..1456937 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -134,8 +134,7 @@ class AuthenticatedChannelsHandler(object): __serializers = { ("os", "on", "ou", "oc",): serializers.Order, ("ps", "pn", "pu", "pc",): serializers.Position, - ("te",): serializers.TradeExecuted, - ("tu",): serializers.TradeExecutionUpdate, + ("te", "tu"): serializers.Trade, ("fos", "fon", "fou", "foc",): serializers.FundingOffer, ("fcs", "fcn", "fcu", "fcc",): serializers.FundingCredit, ("fls", "fln", "flu", "flc",): serializers.FundingLoan, diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index 5991cc9..bee2f7a 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -176,22 +176,7 @@ Position = generate_labeler_serializer("Position", klass=types.Position, labels= "meta" ]) -TradeExecuted = generate_labeler_serializer("TradeExecuted", klass=types.TradeExecuted, labels=[ - "id", - "symbol", - "mts_create", - "order_id", - "exec_amount", - "exec_price", - "order_type", - "order_price", - "maker", - "_PLACEHOLDER", - "_PLACEHOLDER", - "cid" -]) - -TradeExecutionUpdate = generate_labeler_serializer("TradeExecutionUpdate", klass=types.TradeExecutionUpdate, labels=[ +Trade = generate_labeler_serializer("Trade", klass=types.Trade, labels=[ "id", "symbol", "mts_create", diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py index 91bac2a..cc70793 100644 --- a/bfxapi/websocket/types.py +++ b/bfxapi/websocket/types.py @@ -152,7 +152,7 @@ class Position(_Type): meta: JSON @dataclass -class TradeExecuted(_Type): +class Trade(_Type): id: int symbol: str mts_create: int @@ -162,21 +162,8 @@ class TradeExecuted(_Type): order_type: str order_price: float maker:int - cid: int - -@dataclass -class TradeExecutionUpdate(_Type): - id: int - symbol: str - mts_create: int - order_id: int - exec_amount: float - exec_price: float - order_type: str - order_price: float - maker:int - fee: float - fee_currency: str + fee: Optional[float] + fee_currency: Optional[str] cid: int @dataclass From 40a48184dafd46c7f630d007cb23aacc1141405e Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 31 Jan 2023 18:54:15 +0100 Subject: [PATCH 134/265] Fix bugs and differences in namings/types/fields between bfxapi.rest.types, bfxapi.rest.serializers and bfxapi.websocket.types, bfxapi.websocket.serializers. --- bfxapi/rest/serializers.py | 82 ++++++++++++++++----------------- bfxapi/rest/types.py | 70 ++++++++++++++-------------- bfxapi/websocket/handlers.py | 2 +- bfxapi/websocket/serializers.py | 20 ++++---- bfxapi/websocket/types.py | 18 ++++---- 5 files changed, 95 insertions(+), 97 deletions(-) diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 9526922..938aef5 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -248,16 +248,6 @@ FxRate = generate_labeler_serializer("FxRate", klass=types.FxRate, labels=[ #region Serializers definition for Rest Authenticated Endpoints -Wallet = generate_labeler_serializer("Wallet", klass=types.Wallet, labels=[ - "wallet_type", - "currency", - "balance", - "unsettled_interest", - "available_balance", - "last_change", - "trade_details" -]) - Order = generate_labeler_serializer("Order", klass=types.Order, labels=[ "id", "gid", @@ -316,30 +306,6 @@ Position = generate_labeler_serializer("Position", klass=types.Position, labels= "meta" ]) -FundingOffer = generate_labeler_serializer("FundingOffer", klass=types.FundingOffer, labels=[ - "id", - "symbol", - "mts_create", - "mts_update", - "amount", - "amount_orig", - "offer_type", - "_PLACEHOLDER", - "_PLACEHOLDER", - "flags", - "offer_status", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "rate", - "period", - "notify", - "hidden", - "_PLACEHOLDER", - "renew", - "_PLACEHOLDER" -]) - Trade = generate_labeler_serializer("Trade", klass=types.Trade, labels=[ "id", "pair", @@ -392,28 +358,28 @@ Ledger = generate_labeler_serializer("Ledger", klass=types.Ledger, labels=[ "description" ]) -FundingLoan = generate_labeler_serializer("FundingLoan", klass=types.FundingLoan, labels=[ +FundingOffer = generate_labeler_serializer("FundingOffer", klass=types.FundingOffer, labels=[ "id", "symbol", - "side", "mts_create", "mts_update", "amount", + "amount_orig", + "offer_type", + "_PLACEHOLDER", + "_PLACEHOLDER", "flags", - "status", + "offer_status", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", "rate", "period", - "mts_opening", - "mts_last_payout", "notify", "hidden", "_PLACEHOLDER", "renew", - "rate_real", - "no_close" + "_PLACEHOLDER" ]) FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.FundingCredit, labels=[ @@ -441,6 +407,30 @@ FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.Funding "position_pair" ]) +FundingLoan = generate_labeler_serializer("FundingLoan", klass=types.FundingLoan, labels=[ + "id", + "symbol", + "side", + "mts_create", + "mts_update", + "amount", + "flags", + "status", + "rate_type", + "_PLACEHOLDER", + "_PLACEHOLDER", + "rate", + "period", + "mts_opening", + "mts_last_payout", + "notify", + "hidden", + "_PLACEHOLDER", + "renew", + "_PLACEHOLDER", + "no_close" +]) + FundingAutoRenew = generate_labeler_serializer("FundingAutoRenew", klass=types.FundingAutoRenew, labels=[ "currency", "period", @@ -456,6 +446,16 @@ FundingInfo = generate_labeler_serializer("FundingInfo", klass=types.FundingInfo "duration_lend" ]) +Wallet = generate_labeler_serializer("Wallet", klass=types.Wallet, labels=[ + "wallet_type", + "currency", + "balance", + "unsettled_interest", + "available_balance", + "last_change", + "trade_details" +]) + Transfer = generate_labeler_serializer("Transfer", klass=types.Transfer, labels=[ "mts", "wallet_from", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 2152b3f..2330eeb 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -197,16 +197,6 @@ class FxRate(_Type): #region Type hinting for Rest Authenticated Endpoints -@dataclass -class Wallet(_Type): - wallet_type: str - currency: str - balance: float - unsettled_interest: float - available_balance: float - last_change: str - trade_details: JSON - @dataclass class Order(_Type): id: int @@ -252,23 +242,6 @@ class Position(_Type): collateral_min: float meta: JSON -@dataclass -class FundingOffer(_Type): - id: int - symbol: str - mts_create: int - mts_update: int - amount: float - amount_orig: float - offer_type: str - flags: int - offer_status: str - rate: float - period: int - notify: bool - hidden: int - renew: bool - @dataclass class Trade(_Type): id: int @@ -317,24 +290,21 @@ class Ledger(_Type): description: str @dataclass -class FundingLoan(_Type): +class FundingOffer(_Type): id: int symbol: str - side: int mts_create: int mts_update: int amount: float + amount_orig: float + offer_type: str flags: int - status: str + offer_status: str rate: float period: int - mts_opening: int - mts_last_payout: int notify: int hidden: int renew: int - rate_real: float - no_close: int @dataclass class FundingCredit(_Type): @@ -346,6 +316,7 @@ class FundingCredit(_Type): amount: float flags: int status: str + rate_type: str rate: float period: int mts_opening: int @@ -353,10 +324,29 @@ class FundingCredit(_Type): notify: int hidden: int renew: int - rate_real: float no_close: int position_pair: str +@dataclass +class FundingLoan(_Type): + id: int + symbol: str + side: int + mts_create: int + mts_update: int + amount: float + flags: int + status: str + rate_type: str + rate: float + period: int + mts_opening: int + mts_last_payout: int + notify: int + hidden: int + renew: int + no_close: int + @dataclass class FundingAutoRenew(_Type): currency: str @@ -372,6 +362,16 @@ class FundingInfo(_Type): duration_loan: float duration_lend: float +@dataclass +class Wallet(_Type): + wallet_type: str + currency: str + balance: float + unsettled_interest: float + available_balance: float + last_change: str + trade_details: JSON + @dataclass class Transfer(_Type): mts: int diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers.py index 1456937..686501b 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers.py @@ -139,7 +139,7 @@ class AuthenticatedChannelsHandler(object): ("fcs", "fcn", "fcu", "fcc",): serializers.FundingCredit, ("fls", "fln", "flu", "flc",): serializers.FundingLoan, ("ws", "wu",): serializers.Wallet, - ("bu",): serializers.BalanceInfo + ("bu",): serializers.Balance } EVENTS = [ diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index bee2f7a..6207f33 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -89,7 +89,7 @@ Candle = generate_labeler_serializer("Candle", klass=types.Candle, labels=[ ]) DerivativesStatus = generate_labeler_serializer("DerivativesStatus", klass=types.DerivativesStatus, labels=[ - "time_ms", + "mts", "_PLACEHOLDER", "deriv_price", "spot_price", @@ -194,15 +194,15 @@ Trade = generate_labeler_serializer("Trade", klass=types.Trade, labels=[ FundingOffer = generate_labeler_serializer("FundingOffer", klass=types.FundingOffer, labels=[ "id", "symbol", - "mts_created", - "mts_updated", + "mts_create", + "mts_update", "amount", "amount_orig", "offer_type", "_PLACEHOLDER", "_PLACEHOLDER", "flags", - "status", + "offer_status", "_PLACEHOLDER", "_PLACEHOLDER", "_PLACEHOLDER", @@ -235,7 +235,7 @@ FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.Funding "hidden", "_PLACEHOLDER", "renew", - "rate_real", + "_PLACEHOLDER", "no_close", "position_pair" ]) @@ -260,7 +260,7 @@ FundingLoan = generate_labeler_serializer("FundingLoan", klass=types.FundingLoan "hidden", "_PLACEHOLDER", "renew", - "rate_real", + "_PLACEHOLDER", "no_close" ]) @@ -269,12 +269,12 @@ Wallet = generate_labeler_serializer("Wallet", klass=types.Wallet, labels=[ "currency", "balance", "unsettled_interest", - "balance_available", - "description", - "meta" + "available_balance", + "last_change", + "trade_details" ]) -BalanceInfo = generate_labeler_serializer("BalanceInfo", klass=types.BalanceInfo, labels=[ +Balance = generate_labeler_serializer("Balance", klass=types.Balance, labels=[ "aum", "aum_net", ]) diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py index cc70793..659fd29 100644 --- a/bfxapi/websocket/types.py +++ b/bfxapi/websocket/types.py @@ -90,7 +90,7 @@ class Candle(_Type): @dataclass class DerivativesStatus(_Type): - time_ms: int + mts: int deriv_price: float spot_price: float insurance_fund_balance: float @@ -170,13 +170,13 @@ class Trade(_Type): class FundingOffer(_Type): id: int symbol: str - mts_created: int - mts_updated: int + mts_create: int + mts_update: int amount: float amount_orig: float offer_type: str flags: int - status: str + offer_status: str rate: float period: int notify: int @@ -200,7 +200,6 @@ class FundingCredit(_Type): notify: int hidden: int renew: int - rate_real: float no_close: int position_pair: str @@ -221,7 +220,6 @@ class FundingLoan(_Type): notify: int hidden: int renew: int - rate_real: float no_close: int @dataclass @@ -230,12 +228,12 @@ class Wallet(_Type): currency: str balance: float unsettled_interest: float - balance_available: float - description: str - meta: JSON + available_balance: float + last_change: str + trade_details: JSON @dataclass -class BalanceInfo(_Type): +class Balance(_Type): aum: float aum_net: float From 05784cc8ec8bc6b4aabdcd29e369cca43996744f Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 1 Feb 2023 17:05:25 +0100 Subject: [PATCH 135/265] Add tests subpackage. Add TestRestSerializersAndTypes and TestWebsocketSerializersAndTypes unit tests. Fix consistency bugs between serializers and types. --- bfxapi/labeler.py | 3 +++ bfxapi/rest/serializers.py | 24 +++++++++++++++++-- bfxapi/rest/types.py | 22 ++++++++++++++++- bfxapi/tests/__init__.py | 8 +++++++ .../tests/test_rest_serializers_and_types.py | 23 ++++++++++++++++++ .../test_websocket_serializers_and_types.py | 23 ++++++++++++++++++ bfxapi/websocket/serializers.py | 15 ++++++++++-- bfxapi/websocket/types.py | 12 ++++++++++ 8 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 bfxapi/tests/__init__.py create mode 100644 bfxapi/tests/test_rest_serializers_and_types.py create mode 100644 bfxapi/tests/test_websocket_serializers_and_types.py diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 6201b82..1c2655d 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -28,6 +28,9 @@ class _Serializer(Generic[T]): def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: return cast(T, self.klass(**dict(self._serialize(*values, skip=skip)))) + def get_labels(self) -> List[str]: + return [ label for label in self.__labels if label not in self.__IGNORE ] + class _RecursiveSerializer(_Serializer, Generic[T]): def __init__(self, name: str, klass: Type[_Type], labels: List[str], serializers: Dict[str, _Serializer[Any]], IGNORE: List[str] = ["_PLACEHOLDER"]): super().__init__(name, klass, labels, IGNORE) diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 938aef5..3ef765a 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -4,6 +4,26 @@ from .. labeler import generate_labeler_serializer, generate_recursive_serialize from .. notification import _Notification +__serializers__ = [ + "PlatformStatus", "TradingPairTicker", "FundingCurrencyTicker", + "TickersHistory", "TradingPairTrade", "FundingCurrencyTrade", + "TradingPairBook", "FundingCurrencyBook", "TradingPairRawBook", + "FundingCurrencyRawBook", "Statistic", "Candle", + "DerivativesStatus", "Liquidation", "Leaderboard", + "FundingStatistic", "PulseProfile", "PulseMessage", + "TradingMarketAveragePrice", "FundingMarketAveragePrice", "FxRate", + + "Order", "Position", "Trade", + "FundingTrade", "OrderTrade", "Ledger", + "FundingOffer", "FundingCredit", "FundingLoan", + "FundingAutoRenew", "FundingInfo", "Wallet", + "Transfer", "Withdrawal", "DepositAddress", + "Invoice", "Movement", "SymbolMarginInfo", + "BaseMarginInfo", "Claim", "IncreaseInfo", + "Increase", "PositionHistory", "PositionSnapshot", + "PositionAudit", "DerivativePositionCollateral", "DerivativePositionCollateralLimits", +] + #region Serializers definition for Rest Public Endpoints PlatformStatus = generate_labeler_serializer("PlatformStatus", klass=types.PlatformStatus, labels=[ @@ -308,7 +328,7 @@ Position = generate_labeler_serializer("Position", klass=types.Position, labels= Trade = generate_labeler_serializer("Trade", klass=types.Trade, labels=[ "id", - "pair", + "symbol", "mts_create", "order_id", "exec_amount", @@ -333,7 +353,7 @@ FundingTrade = generate_labeler_serializer("FundingTrade", klass=types.FundingTr OrderTrade = generate_labeler_serializer("OrderTrade", klass=types.OrderTrade, labels=[ "id", - "pair", + "symbol", "mts_create", "order_id", "exec_amount", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 2330eeb..ad28321 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -6,12 +6,32 @@ from .. labeler import _Type from .. notification import Notification from .. utils.encoder import JSON +__types__ = [ + "PlatformStatus", "TradingPairTicker", "FundingCurrencyTicker", + "TickersHistory", "TradingPairTrade", "FundingCurrencyTrade", + "TradingPairBook", "FundingCurrencyBook", "TradingPairRawBook", + "FundingCurrencyRawBook", "Statistic", "Candle", + "DerivativesStatus", "Liquidation", "Leaderboard", + "FundingStatistic", "PulseProfile", "PulseMessage", + "TradingMarketAveragePrice", "FundingMarketAveragePrice", "FxRate", + + "Order", "Position", "Trade", + "FundingTrade", "OrderTrade", "Ledger", + "FundingOffer", "FundingCredit", "FundingLoan", + "FundingAutoRenew", "FundingInfo", "Wallet", + "Transfer", "Withdrawal", "DepositAddress", + "Invoice", "Movement", "SymbolMarginInfo", + "BaseMarginInfo", "Claim", "IncreaseInfo", + "Increase", "PositionHistory", "PositionSnapshot", + "PositionAudit", "DerivativePositionCollateral", "DerivativePositionCollateralLimits", +] + #region Type hinting for Rest Public Endpoints @dataclass class PlatformStatus(_Type): status: int - + @dataclass class TradingPairTicker(_Type): symbol: Optional[str] diff --git a/bfxapi/tests/__init__.py b/bfxapi/tests/__init__.py new file mode 100644 index 0000000..eee78b7 --- /dev/null +++ b/bfxapi/tests/__init__.py @@ -0,0 +1,8 @@ +import unittest +from .test_rest_serializers_and_types import TestRestSerializersAndTypes +from .test_websocket_serializers_and_types import TestWebsocketSerializersAndTypes + +NAME = "tests" + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/bfxapi/tests/test_rest_serializers_and_types.py b/bfxapi/tests/test_rest_serializers_and_types.py new file mode 100644 index 0000000..ff1b427 --- /dev/null +++ b/bfxapi/tests/test_rest_serializers_and_types.py @@ -0,0 +1,23 @@ +import unittest + +from ..rest import serializers, types + +class TestRestSerializersAndTypes(unittest.TestCase): + def test_consistency(self): + __types__ = list(map(types.__dict__.get, types.__types__)) + + for serializer in map(serializers.__dict__.get, serializers.__serializers__): + type = types.__dict__.get(serializer.name) + + __types__.remove(type) + 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.") + + for type in __types__: + self.fail(f"_Type <{type.__name__}>: no respective _Serializer found in bfxapi.rest.serializers.") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/bfxapi/tests/test_websocket_serializers_and_types.py b/bfxapi/tests/test_websocket_serializers_and_types.py new file mode 100644 index 0000000..2d34d61 --- /dev/null +++ b/bfxapi/tests/test_websocket_serializers_and_types.py @@ -0,0 +1,23 @@ +import unittest + +from ..websocket import serializers, types + +class TestWebsocketSerializersAndTypes(unittest.TestCase): + def test_consistency(self): + __types__ = list(map(types.__dict__.get, types.__types__)) + + for serializer in map(serializers.__dict__.get, serializers.__serializers__): + type = types.__dict__.get(serializer.name) + + __types__.remove(type) + 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.") + + for type in __types__: + self.fail(f"_Type <{type.__name__}>: no respective _Serializer found in bfxapi.websocket.serializers.") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index 6207f33..29c28c7 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -4,6 +4,17 @@ from .. labeler import generate_labeler_serializer from .. notification import _Notification +__serializers__ = [ + "TradingPairTicker", "FundingCurrencyTicker", "TradingPairTrade", + "FundingCurrencyTrade", "TradingPairBook", "FundingCurrencyBook", + "TradingPairRawBook", "FundingCurrencyRawBook", "Candle", + "DerivativesStatus", + + "Order", "Position", "Trade", + "FundingOffer", "FundingCredit", "FundingLoan", + "Wallet", "Balance", +] + #region Serializers definition for Websocket Public Channels TradingPairTicker = generate_labeler_serializer("TradingPairTicker", klass=types.TradingPairTicker, labels=[ @@ -32,7 +43,7 @@ FundingCurrencyTicker = generate_labeler_serializer("FundingCurrencyTicker", kla "last_price", "volume", "high", - "low" + "low", "_PLACEHOLDER", "_PLACEHOLDER", "frr_amount_available" @@ -100,7 +111,7 @@ DerivativesStatus = generate_labeler_serializer("DerivativesStatus", klass=types "next_funding_accrued", "next_funding_step", "_PLACEHOLDER", - "current_funding" + "current_funding", "_PLACEHOLDER", "_PLACEHOLDER", "mark_price", diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py index 659fd29..3e0991f 100644 --- a/bfxapi/websocket/types.py +++ b/bfxapi/websocket/types.py @@ -6,6 +6,17 @@ from ..labeler import _Type from ..notification import Notification from .. utils.encoder import JSON +__types__ = [ + "TradingPairTicker", "FundingCurrencyTicker", "TradingPairTrade", + "FundingCurrencyTrade", "TradingPairBook", "FundingCurrencyBook", + "TradingPairRawBook", "FundingCurrencyRawBook", "Candle", + "DerivativesStatus", + + "Order", "Position", "Trade", + "FundingOffer", "FundingCredit", "FundingLoan", + "Wallet", "Balance", +] + #region Type hinting for Websocket Public Channels @dataclass @@ -143,6 +154,7 @@ class Position(_Type): pl_perc: float price_liq: float leverage: float + flag: int position_id: int mts_create: int mts_update: int From 06dc9e1c0a842e5bb67b47606f62870cf1a48d70 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 1 Feb 2023 17:17:38 +0100 Subject: [PATCH 136/265] Remove cid.py and integers.py from bfxapi.utils subpackage. Rename encoder.py file to JSONEncoder.py. Remove IntegerUnderflowError and IntegerOverflowflowError exceptions from bfxapi/exceptions.py. --- bfxapi/exceptions.py | 18 +-------- bfxapi/rest/_Requests.py | 2 +- bfxapi/rest/types.py | 2 +- bfxapi/utils/{encoder.py => JSONEncoder.py} | 0 bfxapi/utils/cid.py | 4 -- bfxapi/utils/integers.py | 43 --------------------- bfxapi/websocket/BfxWebsocketClient.py | 2 +- bfxapi/websocket/types.py | 2 +- 8 files changed, 5 insertions(+), 68 deletions(-) rename bfxapi/utils/{encoder.py => JSONEncoder.py} (100%) delete mode 100644 bfxapi/utils/cid.py delete mode 100644 bfxapi/utils/integers.py diff --git a/bfxapi/exceptions.py b/bfxapi/exceptions.py index 1033837..d876946 100644 --- a/bfxapi/exceptions.py +++ b/bfxapi/exceptions.py @@ -1,9 +1,7 @@ __all__ = [ "BfxBaseException", - + "LabelerSerializerException", - "IntegerUnderflowError", - "IntegerOverflowflowError" ] class BfxBaseException(Exception): @@ -18,18 +16,4 @@ class LabelerSerializerException(BfxBaseException): This exception indicates an error thrown by the _Serializer class in bfxapi/labeler.py. """ - pass - -class IntegerUnderflowError(BfxBaseException): - """ - This error indicates an underflow in one of the integer types defined in bfxapi/utils/integers.py. - """ - - pass - -class IntegerOverflowflowError(BfxBaseException): - """ - This error indicates an overflow in one of the integer types defined in bfxapi/utils/integers.py. - """ - pass \ No newline at end of file diff --git a/bfxapi/rest/_Requests.py b/bfxapi/rest/_Requests.py index 6e4383a..8103d03 100644 --- a/bfxapi/rest/_Requests.py +++ b/bfxapi/rest/_Requests.py @@ -4,7 +4,7 @@ from http import HTTPStatus from .enums import Error from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError -from .. utils.encoder import JSONEncoder +from ..utils.JSONEncoder import JSONEncoder class _Requests(object): def __init__(self, host, API_KEY = None, API_SECRET = None): diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index ad28321..084bcc5 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from .. labeler import _Type from .. notification import Notification -from .. utils.encoder import JSON +from ..utils.JSONEncoder import JSON __types__ = [ "PlatformStatus", "TradingPairTicker", "FundingCurrencyTicker", diff --git a/bfxapi/utils/encoder.py b/bfxapi/utils/JSONEncoder.py similarity index 100% rename from bfxapi/utils/encoder.py rename to bfxapi/utils/JSONEncoder.py diff --git a/bfxapi/utils/cid.py b/bfxapi/utils/cid.py deleted file mode 100644 index 43150bb..0000000 --- a/bfxapi/utils/cid.py +++ /dev/null @@ -1,4 +0,0 @@ -import time - -def generate_unique_cid(multiplier: int = 1000) -> int: - return int(round(time.time() * multiplier)) diff --git a/bfxapi/utils/integers.py b/bfxapi/utils/integers.py deleted file mode 100644 index 08582c6..0000000 --- a/bfxapi/utils/integers.py +++ /dev/null @@ -1,43 +0,0 @@ -from typing import cast, TypeVar, Union - -from .. exceptions import IntegerUnderflowError, IntegerOverflowflowError - -__all__ = [ "Int16", "Int32", "Int45", "Int64" ] - -T = TypeVar("T") - -class _Int(int): - def __new__(cls: T, integer: int) -> T: - assert hasattr(cls, "_BITS"), "_Int must be extended by a class that has a static member _BITS (indicating the number of bits with which to represent the integers)." - - bits = cls._BITS - 1 - - min, max = -(2 ** bits), (2 ** bits) - 1 - - if integer < min: - raise IntegerUnderflowError(f"Underflow. Cannot store <{integer}> in {cls._BITS} bits integer. The min and max bounds are {min} and {max}.") - - if integer > max: - raise IntegerOverflowflowError(f"Overflow. Cannot store <{integer}> in {cls._BITS} bits integer. The min and max bounds are {min} and {max}.") - - return cast(T, super().__new__(int, integer)) - -class Int16(_Int): - _BITS = 16 - -int16 = Union[Int16, int] - -class Int32(_Int): - _BITS = 32 - -int32 = Union[Int32, int] - -class Int45(_Int): - _BITS = 45 - -int45 = Union[Int45, int] - -class Int64(_Int): - _BITS = 64 - -int64 = Union[Int64, int] \ No newline at end of file diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index e5c7efb..3bdb3d9 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -10,7 +10,7 @@ from ._BfxWebsocketInputs import _BfxWebsocketInputs from .handlers import Channels, PublicChannelsHandler, AuthenticatedChannelsHandler from .exceptions import WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported -from ..utils.encoder import JSONEncoder +from ..utils.JSONEncoder import JSONEncoder from ..utils.logger import Formatter, CustomLogger diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py index 3e0991f..d1c8ab4 100644 --- a/bfxapi/websocket/types.py +++ b/bfxapi/websocket/types.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from ..labeler import _Type from ..notification import Notification -from .. utils.encoder import JSON +from ..utils.JSONEncoder import JSON __types__ = [ "TradingPairTicker", "FundingCurrencyTicker", "TradingPairTrade", From 0dd6fa9dbbe3defdf040d819d7eefc5c2a338772 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 1 Feb 2023 18:00:00 +0100 Subject: [PATCH 137/265] Reorder and rename some method inside _RestAuthenticatedEndpoints class. --- bfxapi/rest/_RestAuthenticatedEndpoints.py | 172 +++++++++++---------- bfxapi/rest/serializers.py | 10 +- bfxapi/rest/types.py | 10 +- examples/rest/claim_position.py | 2 +- examples/rest/funding_auto_renew.py | 2 +- examples/rest/keep_taken_funding.py | 2 +- examples/rest/transfer_wallet.py | 6 +- 7 files changed, 103 insertions(+), 101 deletions(-) diff --git a/bfxapi/rest/_RestAuthenticatedEndpoints.py b/bfxapi/rest/_RestAuthenticatedEndpoints.py index d30d32f..6113e9c 100644 --- a/bfxapi/rest/_RestAuthenticatedEndpoints.py +++ b/bfxapi/rest/_RestAuthenticatedEndpoints.py @@ -22,9 +22,6 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, data={ "id": ids }) ] - def get_positions(self) -> List[Position]: - return [ serializers.Position.parse(*sub_data) for sub_data in self._POST("auth/r/positions") ] - def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, float, str], price: Optional[Union[Decimal, float, str]] = None, lev: Optional[int] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_oco_stop: Optional[Union[Decimal, float, str]] = None, @@ -86,6 +83,9 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + def get_order_trades(self, symbol: str, id: int) -> List[OrderTrade]: + return [ serializers.OrderTrade.parse(*sub_data) for sub_data in self._POST(f"auth/r/order/{symbol}:{id}/trades") ] + def get_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Trade]: if symbol == None: endpoint = "auth/r/trades/hist" @@ -99,22 +99,6 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.Trade.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] - def get_funding_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingTrade]: - if symbol == None: - endpoint = "auth/r/funding/trades/hist" - else: endpoint = f"auth/r/funding/trades/{symbol}/hist" - - data = { - "sort": sort, - "start": start, "end": end, - "limit": limit - } - - return [ serializers.FundingTrade.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] - - def get_order_trades(self, symbol: str, id: int) -> List[OrderTrade]: - return [ serializers.OrderTrade.parse(*sub_data) for sub_data in self._POST(f"auth/r/order/{symbol}:{id}/trades") ] - def get_ledgers(self, currency: str, category: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Ledger]: data = { "category": category, @@ -124,6 +108,52 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.Ledger.parse(*sub_data) for sub_data in self._POST(f"auth/r/ledgers/{currency}/hist", data=data) ] + def get_base_margin_info(self) -> BaseMarginInfo: + return serializers.BaseMarginInfo.parse(*(self._POST(f"auth/r/info/margin/base")[1])) + + def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo: + response = self._POST(f"auth/r/info/margin/{symbol}") + + return serializers.SymbolMarginInfo.parse(*([response[1]] + response[2])) + + def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]: + return [ serializers.SymbolMarginInfo.parse(*([sub_data[1]] + sub_data[2])) for sub_data in self._POST(f"auth/r/info/margin/sym_all") ] + + def get_positions(self) -> List[Position]: + return [ serializers.Position.parse(*sub_data) for sub_data in self._POST("auth/r/positions") ] + + def claim_position(self, id: int, amount: Optional[Union[Decimal, float, str]] = None) -> Notification[PositionClaim]: + return serializers._Notification[PositionClaim](serializer=serializers.PositionClaim).parse( + *self._POST("auth/w/position/claim", data={ "id": id, "amount": amount }) + ) + + def increase_position(self, symbol: str, amount: Union[Decimal, float, str]) -> Notification[PositionIncrease]: + return serializers._Notification[PositionIncrease](serializer=serializers.PositionIncrease).parse( + *self._POST("auth/w/position/increase", data={ "symbol": symbol, "amount": amount }) + ) + + def get_increase_position_info(self, symbol: str, amount: Union[Decimal, float, str]) -> PositionIncreaseInfo: + response = self._POST(f"auth/r/position/increase/info", data={ "symbol": symbol, "amount": amount }) + + return serializers.PositionIncreaseInfo.parse(*( + response[0] + [response[1][0]] + response[1][1] + [response[1][2]] + response[4] + response[5] + )) + + def get_positions_history(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionHistory]: + return [ serializers.PositionHistory.parse(*sub_data) for sub_data in self._POST("auth/r/positions/hist", data={ "start": start, "end": end, "limit": limit }) ] + + def get_positions_snapshot(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionSnapshot]: + return [ serializers.PositionSnapshot.parse(*sub_data) for sub_data in self._POST("auth/r/positions/snap", data={ "start": start, "end": end, "limit": limit }) ] + + def get_positions_audit(self, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionAudit]: + return [ serializers.PositionAudit.parse(*sub_data) for sub_data in self._POST("auth/r/positions/audit", data={ "ids": ids, "start": start, "end": end, "limit": limit }) ] + + def set_derivative_position_collateral(self, symbol: str, collateral: Union[Decimal, float, str]) -> DerivativePositionCollateral: + return serializers.DerivativePositionCollateral.parse(*(self._POST("auth/w/deriv/collateral/set", data={ "symbol": symbol, "collateral": collateral })[0])) + + def get_derivative_position_collateral_limits(self, symbol: str) -> DerivativePositionCollateralLimits: + return serializers.DerivativePositionCollateralLimits.parse(*self._POST("auth/calc/deriv/collateral/limits", data={ "symbol": symbol })) + def get_funding_offers(self, symbol: Optional[str] = None) -> List[FundingOffer]: endpoint = "auth/r/funding/offers" @@ -151,6 +181,25 @@ class _RestAuthenticatedEndpoints(_Requests): *self._POST("auth/w/funding/offer/cancel/all", data={ "currency": currency }) ) + def submit_funding_close(self, id: int) -> Notification[Literal[None]]: + return serializers._Notification[Literal[None]](serializer=None).parse( + *self._POST("auth/w/funding/close", data={ "id": id }) + ) + + def toggle_auto_renew(self, status: bool, currency: str, amount: Optional[str] = None, rate: Optional[int] = None, period: Optional[int] = None) -> Notification[FundingAutoRenew]: + return serializers._Notification[FundingAutoRenew](serializer=serializers.FundingAutoRenew).parse(*self._POST("auth/w/funding/auto", data={ + "status": int(status), + "currency": currency, "amount": amount, + "rate": rate, "period": period + })) + + def toggle_keep(self, type: Literal["credit", "loan"], ids: Optional[List[int]] = None, changes: Optional[Dict[int, bool]] = None) -> Notification[Literal[None]]: + return serializers._Notification[Literal[None]](serializer=None).parse(*self._POST("auth/w/funding/keep", data={ + "type": type, + "id": ids, + "changes": changes + })) + def get_funding_offers_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingOffer]: if symbol == None: endpoint = "auth/r/funding/offers/hist" @@ -201,31 +250,25 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + def get_funding_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingTrade]: + if symbol == None: + endpoint = "auth/r/funding/trades/hist" + else: endpoint = f"auth/r/funding/trades/{symbol}/hist" + + data = { + "sort": sort, + "start": start, "end": end, + "limit": limit + } + + return [ serializers.FundingTrade.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + def get_funding_info(self, key: str) -> FundingInfo: response = self._POST(f"auth/r/info/funding/{key}") return serializers.FundingInfo.parse(*([response[1]] + response[2])) - def submit_funding_close(self, id: int) -> Notification[Literal[None]]: - return serializers._Notification[Literal[None]](serializer=None).parse( - *self._POST("auth/w/funding/close", data={ "id": id }) - ) - - def submit_funding_toggle_auto_renew(self, status: bool, currency: str, amount: Optional[str] = None, rate: Optional[int] = None, period: Optional[int] = None) -> Notification[FundingAutoRenew]: - return serializers._Notification[FundingAutoRenew](serializer=serializers.FundingAutoRenew).parse(*self._POST("auth/w/funding/auto", data={ - "status": int(status), - "currency": currency, "amount": amount, - "rate": rate, "period": period - })) - - def submit_funding_toggle_keep(self, type: Literal["credit", "loan"], ids: Optional[List[int]] = None, changes: Optional[Dict[int, bool]] = None) -> Notification[Literal[None]]: - return serializers._Notification[Literal[None]](serializer=None).parse(*self._POST("auth/w/funding/keep", data={ - "type": type, - "id": ids, - "changes": changes - })) - - def submit_wallet_transfer(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, float, str]) -> Notification[Transfer]: + def transfer_between_wallets(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, float, str]) -> Notification[Transfer]: data = { "from": from_wallet, "to": to_wallet, "currency": currency, "currency_to": currency_to, @@ -234,13 +277,11 @@ class _RestAuthenticatedEndpoints(_Requests): return serializers._Notification[Transfer](serializer=serializers.Transfer).parse(*self._POST("auth/w/transfer", data=data)) - def submit_wallet_withdraw(self, wallet: str, method: str, address: str, amount: Union[Decimal, float, str]) -> Notification[Withdrawal]: - data = { + def submit_wallet_withdrawal(self, wallet: str, method: str, address: str, amount: Union[Decimal, float, str]) -> Notification[Withdrawal]: + return serializers._Notification[Withdrawal](serializer=serializers.Withdrawal).parse(*self._POST("auth/w/withdraw", data={ "wallet": wallet, "method": method, "address": address, "amount": amount, - } - - return serializers._Notification[Withdrawal](serializer=serializers.Withdrawal).parse(*self._POST("auth/w/withdraw", data=data)) + })) def get_deposit_address(self, wallet: str, method: str, renew: bool = False) -> Notification[DepositAddress]: data = { @@ -251,7 +292,7 @@ class _RestAuthenticatedEndpoints(_Requests): return serializers._Notification[DepositAddress](serializer=serializers.DepositAddress).parse(*self._POST("auth/w/deposit/address", data=data)) - def get_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, float, str]) -> Invoice: + def generate_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, float, str]) -> Invoice: data = { "wallet": wallet, "currency": currency, "amount": amount @@ -269,43 +310,4 @@ class _RestAuthenticatedEndpoints(_Requests): "limit": limit } - return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] - - def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo: - response = self._POST(f"auth/r/info/margin/{symbol}") - - return serializers.SymbolMarginInfo.parse(*([response[1]] + response[2])) - - def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]: - return [ serializers.SymbolMarginInfo.parse(*([sub_data[1]] + sub_data[2])) for sub_data in self._POST(f"auth/r/info/margin/sym_all") ] - - def get_base_margin_info(self) -> BaseMarginInfo: - return serializers.BaseMarginInfo.parse(*(self._POST(f"auth/r/info/margin/base")[1])) - - def claim_position(self, id: int, amount: Optional[Union[Decimal, float, str]] = None) -> Notification[Claim]: - return serializers._Notification[Claim](serializer=serializers.Claim).parse(*self._POST("auth/w/position/claim", data={ "id": id, "amount": amount })) - - def get_increase_position_info(self, symbol: str, amount: Union[Decimal, float, str]) -> IncreaseInfo: - response = self._POST(f"auth/r/position/increase/info", data={ "symbol": symbol, "amount": amount }) - - return serializers.IncreaseInfo.parse(*( - response[0] + [response[1][0]] + response[1][1] + [response[1][2]] + response[4] + response[5] - )) - - def increase_position(self, symbol: str, amount: Union[Decimal, float, str]) -> Notification[Increase]: - return serializers._Notification[Increase](serializer=serializers.Increase).parse(*self._POST("auth/w/position/increase", data={ "symbol": symbol, "amount": amount })) - - def get_positions_history(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionHistory]: - return [ serializers.PositionHistory.parse(*sub_data) for sub_data in self._POST("auth/r/positions/hist", data={ "start": start, "end": end, "limit": limit }) ] - - def get_positions_snapshot(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionSnapshot]: - return [ serializers.PositionSnapshot.parse(*sub_data) for sub_data in self._POST("auth/r/positions/snap", data={ "start": start, "end": end, "limit": limit }) ] - - def get_positions_audit(self, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionAudit]: - return [ serializers.PositionAudit.parse(*sub_data) for sub_data in self._POST("auth/r/positions/audit", data={ "ids": ids, "start": start, "end": end, "limit": limit }) ] - - def set_derivative_position_collateral(self, symbol: str, collateral: Union[Decimal, float, str]) -> DerivativePositionCollateral: - return serializers.DerivativePositionCollateral.parse(*(self._POST("auth/w/deriv/collateral/set", data={ "symbol": symbol, "collateral": collateral })[0])) - - def get_derivative_position_collateral_limits(self, symbol: str) -> DerivativePositionCollateralLimits: - return serializers.DerivativePositionCollateralLimits.parse(*self._POST("auth/calc/deriv/collateral/limits", data={ "symbol": symbol })) \ No newline at end of file + return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 3ef765a..021211c 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -19,8 +19,8 @@ __serializers__ = [ "FundingAutoRenew", "FundingInfo", "Wallet", "Transfer", "Withdrawal", "DepositAddress", "Invoice", "Movement", "SymbolMarginInfo", - "BaseMarginInfo", "Claim", "IncreaseInfo", - "Increase", "PositionHistory", "PositionSnapshot", + "BaseMarginInfo", "PositionClaim", "PositionIncreaseInfo", + "PositionIncrease", "PositionHistory", "PositionSnapshot", "PositionAudit", "DerivativePositionCollateral", "DerivativePositionCollateralLimits", ] @@ -557,7 +557,7 @@ BaseMarginInfo = generate_labeler_serializer("BaseMarginInfo", klass=types.BaseM "margin_min" ]) -Claim = generate_labeler_serializer("Claim", klass=types.Claim, labels=[ +PositionClaim = generate_labeler_serializer("PositionClaim", klass=types.PositionClaim, labels=[ "symbol", "position_status", "amount", @@ -580,7 +580,7 @@ Claim = generate_labeler_serializer("Claim", klass=types.Claim, labels=[ "meta" ]) -IncreaseInfo = generate_labeler_serializer("IncreaseInfo", klass=types.IncreaseInfo, labels=[ +PositionIncreaseInfo = generate_labeler_serializer("PositionIncreaseInfo", klass=types.PositionIncreaseInfo, labels=[ "max_pos", "current_pos", "base_currency_balance", @@ -599,7 +599,7 @@ IncreaseInfo = generate_labeler_serializer("IncreaseInfo", klass=types.IncreaseI "funding_required_currency" ]) -Increase = generate_labeler_serializer("Increase", klass=types.Increase, labels=[ +PositionIncrease = generate_labeler_serializer("PositionIncrease", klass=types.PositionIncrease, labels=[ "symbol", "_PLACEHOLDER", "amount", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 084bcc5..431778a 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -21,8 +21,8 @@ __types__ = [ "FundingAutoRenew", "FundingInfo", "Wallet", "Transfer", "Withdrawal", "DepositAddress", "Invoice", "Movement", "SymbolMarginInfo", - "BaseMarginInfo", "Claim", "IncreaseInfo", - "Increase", "PositionHistory", "PositionSnapshot", + "BaseMarginInfo", "PositionClaim", "PositionIncreaseInfo", + "PositionIncrease", "PositionHistory", "PositionSnapshot", "PositionAudit", "DerivativePositionCollateral", "DerivativePositionCollateralLimits", ] @@ -454,7 +454,7 @@ class BaseMarginInfo(_Type): margin_min: float @dataclass -class Claim(_Type): +class PositionClaim(_Type): symbol: str position_status: str amount: float @@ -470,7 +470,7 @@ class Claim(_Type): meta: JSON @dataclass -class IncreaseInfo(_Type): +class PositionIncreaseInfo(_Type): max_pos: int current_pos: float base_currency_balance: float @@ -485,7 +485,7 @@ class IncreaseInfo(_Type): funding_required_currency: str @dataclass -class Increase(_Type): +class PositionIncrease(_Type): symbol: str amount: float base_price: float diff --git a/examples/rest/claim_position.py b/examples/rest/claim_position.py index caf5e23..ba3e4e0 100644 --- a/examples/rest/claim_position.py +++ b/examples/rest/claim_position.py @@ -16,4 +16,4 @@ open_margin_positions = bfx.rest.auth.get_positions() for position in open_margin_positions: print(f"Position {position}") claim = bfx.rest.auth.claim_position(position.position_id, amount=0.000001) - print(f"Claim {claim.notify_info}") \ No newline at end of file + print(f"PositionClaim {claim.notify_info}") \ No newline at end of file diff --git a/examples/rest/funding_auto_renew.py b/examples/rest/funding_auto_renew.py index eec7c47..c892ce3 100644 --- a/examples/rest/funding_auto_renew.py +++ b/examples/rest/funding_auto_renew.py @@ -10,7 +10,7 @@ bfx = Client( API_SECRET=os.getenv("BFX_API_SECRET") ) -notification = bfx.rest.auth.submit_funding_toggle_auto_renew( +notification = bfx.rest.auth.toggle_auto_renew( status=True, currency="USD", amount="150", diff --git a/examples/rest/keep_taken_funding.py b/examples/rest/keep_taken_funding.py index 9693f95..1314ffa 100644 --- a/examples/rest/keep_taken_funding.py +++ b/examples/rest/keep_taken_funding.py @@ -15,7 +15,7 @@ loans = bfx.rest.auth.get_funding_loans(symbol="fUSD") for loan in loans: print(f"Loan {loan}") - notification = bfx.rest.auth.submit_funding_toggle_keep( + notification = bfx.rest.auth.toggle_keep( funding_type="loan", ids=[loan.id], changes={ diff --git a/examples/rest/transfer_wallet.py b/examples/rest/transfer_wallet.py index e986bfd..8de15fd 100644 --- a/examples/rest/transfer_wallet.py +++ b/examples/rest/transfer_wallet.py @@ -11,7 +11,7 @@ bfx = Client( ) def transfer_wallet(): - response = bfx.rest.auth.submit_wallet_transfer(from_wallet="exchange", to_wallet="funding", from_currency="ETH", to_currency="ETH", amount=0.001) + response = bfx.rest.auth.transfer_between_wallets(from_wallet="exchange", to_wallet="funding", from_currency="ETH", to_currency="ETH", amount=0.001) print("Transfer:", response.notify_info) def get_existing_deposit_address(): @@ -24,11 +24,11 @@ def create_new_deposit_address(): def withdraw(): # tetheruse = Tether (ERC20) - response = bfx.rest.auth.submit_wallet_withdraw(wallet="exchange", method="tetheruse", amount=1, address="0x742d35Cc6634C0532925a3b844Bc454e4438f44e") + response = bfx.rest.auth.submit_wallet_withdrawal(wallet="exchange", method="tetheruse", amount=1, address="0x742d35Cc6634C0532925a3b844Bc454e4438f44e") print("Address:", response.notify_info) def create_lighting_network_deposit_address(): - invoice = bfx.rest.auth.get_deposit_invoice(wallet="funding", currency="LNX", amount=0.001) + invoice = bfx.rest.auth.generate_deposit_invoice(wallet="funding", currency="LNX", amount=0.001) print("Invoice:", invoice) def get_movements(): From 929ae62d2f85f83b0079c5a24dc4a321f320c954 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 6 Feb 2023 16:16:11 +0100 Subject: [PATCH 138/265] Fix bug in bfxapi.tests sub-package. --- bfxapi/rest/types.py | 20 ------------------- bfxapi/tests/__init__.py | 8 +++++++- .../tests/test_rest_serializers_and_types.py | 6 ------ .../test_websocket_serializers_and_types.py | 8 +------- bfxapi/websocket/types.py | 11 ---------- 5 files changed, 8 insertions(+), 45 deletions(-) diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 431778a..ab41fec 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -6,26 +6,6 @@ from .. labeler import _Type from .. notification import Notification from ..utils.JSONEncoder import JSON -__types__ = [ - "PlatformStatus", "TradingPairTicker", "FundingCurrencyTicker", - "TickersHistory", "TradingPairTrade", "FundingCurrencyTrade", - "TradingPairBook", "FundingCurrencyBook", "TradingPairRawBook", - "FundingCurrencyRawBook", "Statistic", "Candle", - "DerivativesStatus", "Liquidation", "Leaderboard", - "FundingStatistic", "PulseProfile", "PulseMessage", - "TradingMarketAveragePrice", "FundingMarketAveragePrice", "FxRate", - - "Order", "Position", "Trade", - "FundingTrade", "OrderTrade", "Ledger", - "FundingOffer", "FundingCredit", "FundingLoan", - "FundingAutoRenew", "FundingInfo", "Wallet", - "Transfer", "Withdrawal", "DepositAddress", - "Invoice", "Movement", "SymbolMarginInfo", - "BaseMarginInfo", "PositionClaim", "PositionIncreaseInfo", - "PositionIncrease", "PositionHistory", "PositionSnapshot", - "PositionAudit", "DerivativePositionCollateral", "DerivativePositionCollateralLimits", -] - #region Type hinting for Rest Public Endpoints @dataclass diff --git a/bfxapi/tests/__init__.py b/bfxapi/tests/__init__.py index eee78b7..bbf78c0 100644 --- a/bfxapi/tests/__init__.py +++ b/bfxapi/tests/__init__.py @@ -4,5 +4,11 @@ from .test_websocket_serializers_and_types import TestWebsocketSerializersAndTyp NAME = "tests" +def suite(): + return unittest.TestSuite([ + unittest.makeSuite(TestRestSerializersAndTypes), + unittest.makeSuite(TestWebsocketSerializersAndTypes), + ]) + if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.TextTestRunner().run(suite()) \ No newline at end of file diff --git a/bfxapi/tests/test_rest_serializers_and_types.py b/bfxapi/tests/test_rest_serializers_and_types.py index ff1b427..7bc7242 100644 --- a/bfxapi/tests/test_rest_serializers_and_types.py +++ b/bfxapi/tests/test_rest_serializers_and_types.py @@ -4,20 +4,14 @@ from ..rest import serializers, types class TestRestSerializersAndTypes(unittest.TestCase): def test_consistency(self): - __types__ = list(map(types.__dict__.get, types.__types__)) - for serializer in map(serializers.__dict__.get, serializers.__serializers__): type = types.__dict__.get(serializer.name) - __types__.remove(type) 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.") - for type in __types__: - self.fail(f"_Type <{type.__name__}>: no respective _Serializer found in bfxapi.rest.serializers.") - if __name__ == "__main__": unittest.main() \ No newline at end of file diff --git a/bfxapi/tests/test_websocket_serializers_and_types.py b/bfxapi/tests/test_websocket_serializers_and_types.py index 2d34d61..338b959 100644 --- a/bfxapi/tests/test_websocket_serializers_and_types.py +++ b/bfxapi/tests/test_websocket_serializers_and_types.py @@ -4,20 +4,14 @@ from ..websocket import serializers, types class TestWebsocketSerializersAndTypes(unittest.TestCase): def test_consistency(self): - __types__ = list(map(types.__dict__.get, types.__types__)) - for serializer in map(serializers.__dict__.get, serializers.__serializers__): type = types.__dict__.get(serializer.name) - - __types__.remove(type) + 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.") - for type in __types__: - self.fail(f"_Type <{type.__name__}>: no respective _Serializer found in bfxapi.websocket.serializers.") - if __name__ == "__main__": unittest.main() \ No newline at end of file diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py index d1c8ab4..0ffa870 100644 --- a/bfxapi/websocket/types.py +++ b/bfxapi/websocket/types.py @@ -6,17 +6,6 @@ from ..labeler import _Type from ..notification import Notification from ..utils.JSONEncoder import JSON -__types__ = [ - "TradingPairTicker", "FundingCurrencyTicker", "TradingPairTrade", - "FundingCurrencyTrade", "TradingPairBook", "FundingCurrencyBook", - "TradingPairRawBook", "FundingCurrencyRawBook", "Candle", - "DerivativesStatus", - - "Order", "Position", "Trade", - "FundingOffer", "FundingCredit", "FundingLoan", - "Wallet", "Balance", -] - #region Type hinting for Websocket Public Channels @dataclass From c588d9f20ca6d178406597b30070813e220c9396 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 6 Feb 2023 19:15:58 +0100 Subject: [PATCH 139/265] Rewrite bfxapi/rest/_Requests.py with type hinting. Add None values erasement in bfxapi/utils/JSONEncoder.py. Update code with new improvements. --- bfxapi/notification.py | 8 +- bfxapi/rest/_Requests.py | 27 ++++-- bfxapi/rest/_RestAuthenticatedEndpoints.py | 102 ++++++++++----------- bfxapi/rest/_RestPublicEndpoints.py | 6 +- bfxapi/utils/JSONEncoder.py | 5 +- bfxapi/websocket/_BfxWebsocketInputs.py | 34 ++----- 6 files changed, 86 insertions(+), 96 deletions(-) diff --git a/bfxapi/notification.py b/bfxapi/notification.py index f4301b3..bf4818a 100644 --- a/bfxapi/notification.py +++ b/bfxapi/notification.py @@ -1,7 +1,5 @@ from typing import List, Dict, Union, Optional, Any, TypedDict, Generic, TypeVar, cast - from dataclasses import dataclass - from .labeler import _Type, _Serializer T = TypeVar("T") @@ -19,10 +17,10 @@ class Notification(_Type, Generic[T]): class _Notification(_Serializer, Generic[T]): __LABELS = [ "mts", "type", "message_id", "_PLACEHOLDER", "notify_info", "code", "status", "text" ] - def __init__(self, serializer: Optional[_Serializer] = None, iterate: bool = False): + def __init__(self, serializer: Optional[_Serializer] = None, is_iterable: bool = False): super().__init__("Notification", Notification, _Notification.__LABELS, IGNORE = [ "_PLACEHOLDER" ]) - self.serializer, self.iterate = serializer, iterate + self.serializer, self.is_iterable = serializer, is_iterable def parse(self, *values: Any, skip: Optional[List[str]] = None) -> Notification[T]: notification = cast(Notification[T], Notification(**dict(self._serialize(*values)))) @@ -30,7 +28,7 @@ class _Notification(_Serializer, Generic[T]): if isinstance(self.serializer, _Serializer): NOTIFY_INFO = cast(List[Any], notification.notify_info) - if self.iterate == False: + if self.is_iterable == False: if len(NOTIFY_INFO) == 1 and isinstance(NOTIFY_INFO[0], list): NOTIFY_INFO = NOTIFY_INFO[0] diff --git a/bfxapi/rest/_Requests.py b/bfxapi/rest/_Requests.py index 8103d03..70557cb 100644 --- a/bfxapi/rest/_Requests.py +++ b/bfxapi/rest/_Requests.py @@ -1,21 +1,29 @@ import time, hmac, hashlib, json, requests +from typing import TYPE_CHECKING, Optional, Any + from http import HTTPStatus from .enums import Error from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError from ..utils.JSONEncoder import JSONEncoder +if TYPE_CHECKING: + from requests.sessions import _Params + class _Requests(object): - def __init__(self, host, API_KEY = None, API_SECRET = None): + def __init__(self, host: str, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None): self.host, self.API_KEY, self.API_SECRET = host, API_KEY, API_SECRET - def __build_authentication_headers(self, endpoint, data): + def __build_authentication_headers(self, endpoint: str, data: str): + assert isinstance(self.API_KEY, str) and isinstance(self.API_SECRET, str), \ + "API_KEY and API_SECRET must be both str to call __build_authentication_headers" + nonce = str(int(time.time()) * 1000) - path = f"/api/v2/{endpoint}{nonce}" - - if data != None: path += data + if data == None: + path = f"/api/v2/{endpoint}{nonce}" + else: path = f"/api/v2/{endpoint}{nonce}{data}" signature = hmac.new( self.API_SECRET.encode("utf8"), @@ -29,7 +37,7 @@ class _Requests(object): "bfx-apikey": self.API_KEY } - def _GET(self, endpoint, params = None): + def _GET(self, endpoint: str, params: Optional["_Params"] = None) -> Any: response = requests.get(f"{self.host}/{endpoint}", params=params) if response.status_code == HTTPStatus.NOT_FOUND: @@ -46,11 +54,10 @@ class _Requests(object): return data - def _POST(self, endpoint, params = None, data = None, _ignore_authentication_headers = False): - headers = { "Content-Type": "application/json" } + def _POST(self, endpoint: str, params: Optional["_Params"] = None, body: Optional[Any] = None, _ignore_authentication_headers: bool = False) -> Any: + data = json.dumps(body, cls=JSONEncoder) - if isinstance(data, dict): - data = json.dumps({ key: value for key, value in data.items() if value != None}, cls=JSONEncoder) + headers = { "Content-Type": "application/json" } if self.API_KEY and self.API_SECRET and _ignore_authentication_headers == False: headers = { **headers, **self.__build_authentication_headers(endpoint, data) } diff --git a/bfxapi/rest/_RestAuthenticatedEndpoints.py b/bfxapi/rest/_RestAuthenticatedEndpoints.py index 6113e9c..ab4b9bc 100644 --- a/bfxapi/rest/_RestAuthenticatedEndpoints.py +++ b/bfxapi/rest/_RestAuthenticatedEndpoints.py @@ -20,14 +20,14 @@ class _RestAuthenticatedEndpoints(_Requests): if symbol != None: endpoint += f"/{symbol}" - return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, data={ "id": ids }) ] + return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, body={ "id": ids }) ] def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, float, str], price: Optional[Union[Decimal, float, str]] = None, lev: Optional[int] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_oco_stop: Optional[Union[Decimal, float, str]] = None, gid: Optional[int] = None, cid: Optional[int] = None, flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification[Order]: - data = { + body = { "type": type, "symbol": symbol, "amount": amount, "price": price, "lev": lev, "price_trailing": price_trailing, "price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop, @@ -35,32 +35,32 @@ class _RestAuthenticatedEndpoints(_Requests): "flags": flags, "tif": tif, "meta": meta } - return serializers._Notification[Order](serializer=serializers.Order).parse(*self._POST("auth/w/order/submit", data=data)) + return serializers._Notification[Order](serializers.Order).parse(*self._POST("auth/w/order/submit", body=body)) def update_order(self, id: int, amount: Optional[Union[Decimal, float, str]] = None, price: Optional[Union[Decimal, float, str]] = None, cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification[Order]: - data = { + body = { "id": id, "amount": amount, "price": price, "cid": cid, "cid_date": cid_date, "gid": gid, "flags": flags, "lev": lev, "delta": delta, "price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif } - return serializers._Notification[Order](serializer=serializers.Order).parse(*self._POST("auth/w/order/update", data=data)) + return serializers._Notification[Order](serializers.Order).parse(*self._POST("auth/w/order/update", body=body)) def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None) -> Notification[Order]: - data = { + body = { "id": id, "cid": cid, "cid_date": cid_date } - return serializers._Notification[Order](serializer=serializers.Order).parse(*self._POST("auth/w/order/cancel", data=data)) + return serializers._Notification[Order](serializers.Order).parse(*self._POST("auth/w/order/cancel", body=body)) def cancel_order_multi(self, ids: Optional[List[int]] = None, cids: Optional[List[Tuple[int, str]]] = None, gids: Optional[List[int]] = None, all: bool = False) -> Notification[List[Order]]: - data = { + body = { "ids": ids, "cids": cids, "gids": gids, @@ -68,20 +68,20 @@ class _RestAuthenticatedEndpoints(_Requests): "all": int(all) } - return serializers._Notification[List[Order]](serializer=serializers.Order, iterate=True).parse(*self._POST("auth/w/order/cancel/multi", data=data)) + return serializers._Notification[List[Order]](serializers.Order, is_iterable=True).parse(*self._POST("auth/w/order/cancel/multi", body=body)) def get_orders_history(self, symbol: Optional[str] = None, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Order]: if symbol == None: endpoint = "auth/r/orders/hist" else: endpoint = f"auth/r/orders/{symbol}/hist" - data = { + body = { "id": ids, "start": start, "end": end, "limit": limit } - return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] def get_order_trades(self, symbol: str, id: int) -> List[OrderTrade]: return [ serializers.OrderTrade.parse(*sub_data) for sub_data in self._POST(f"auth/r/order/{symbol}:{id}/trades") ] @@ -91,22 +91,22 @@ class _RestAuthenticatedEndpoints(_Requests): endpoint = "auth/r/trades/hist" else: endpoint = f"auth/r/trades/{symbol}/hist" - data = { + body = { "sort": sort, "start": start, "end": end, "limit": limit } - return [ serializers.Trade.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + return [ serializers.Trade.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] def get_ledgers(self, currency: str, category: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Ledger]: - data = { + body = { "category": category, "start": start, "end": end, "limit": limit } - return [ serializers.Ledger.parse(*sub_data) for sub_data in self._POST(f"auth/r/ledgers/{currency}/hist", data=data) ] + return [ serializers.Ledger.parse(*sub_data) for sub_data in self._POST(f"auth/r/ledgers/{currency}/hist", body=body) ] def get_base_margin_info(self) -> BaseMarginInfo: return serializers.BaseMarginInfo.parse(*(self._POST(f"auth/r/info/margin/base")[1])) @@ -123,36 +123,36 @@ class _RestAuthenticatedEndpoints(_Requests): return [ serializers.Position.parse(*sub_data) for sub_data in self._POST("auth/r/positions") ] def claim_position(self, id: int, amount: Optional[Union[Decimal, float, str]] = None) -> Notification[PositionClaim]: - return serializers._Notification[PositionClaim](serializer=serializers.PositionClaim).parse( - *self._POST("auth/w/position/claim", data={ "id": id, "amount": amount }) + return serializers._Notification[PositionClaim](serializers.PositionClaim).parse( + *self._POST("auth/w/position/claim", body={ "id": id, "amount": amount }) ) def increase_position(self, symbol: str, amount: Union[Decimal, float, str]) -> Notification[PositionIncrease]: - return serializers._Notification[PositionIncrease](serializer=serializers.PositionIncrease).parse( - *self._POST("auth/w/position/increase", data={ "symbol": symbol, "amount": amount }) + return serializers._Notification[PositionIncrease](serializers.PositionIncrease).parse( + *self._POST("auth/w/position/increase", body={ "symbol": symbol, "amount": amount }) ) def get_increase_position_info(self, symbol: str, amount: Union[Decimal, float, str]) -> PositionIncreaseInfo: - response = self._POST(f"auth/r/position/increase/info", data={ "symbol": symbol, "amount": amount }) + response = self._POST(f"auth/r/position/increase/info", body={ "symbol": symbol, "amount": amount }) return serializers.PositionIncreaseInfo.parse(*( response[0] + [response[1][0]] + response[1][1] + [response[1][2]] + response[4] + response[5] )) def get_positions_history(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionHistory]: - return [ serializers.PositionHistory.parse(*sub_data) for sub_data in self._POST("auth/r/positions/hist", data={ "start": start, "end": end, "limit": limit }) ] + return [ serializers.PositionHistory.parse(*sub_data) for sub_data in self._POST("auth/r/positions/hist", body={ "start": start, "end": end, "limit": limit }) ] def get_positions_snapshot(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionSnapshot]: - return [ serializers.PositionSnapshot.parse(*sub_data) for sub_data in self._POST("auth/r/positions/snap", data={ "start": start, "end": end, "limit": limit }) ] + return [ serializers.PositionSnapshot.parse(*sub_data) for sub_data in self._POST("auth/r/positions/snap", body={ "start": start, "end": end, "limit": limit }) ] def get_positions_audit(self, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionAudit]: - return [ serializers.PositionAudit.parse(*sub_data) for sub_data in self._POST("auth/r/positions/audit", data={ "ids": ids, "start": start, "end": end, "limit": limit }) ] + return [ serializers.PositionAudit.parse(*sub_data) for sub_data in self._POST("auth/r/positions/audit", body={ "ids": ids, "start": start, "end": end, "limit": limit }) ] def set_derivative_position_collateral(self, symbol: str, collateral: Union[Decimal, float, str]) -> DerivativePositionCollateral: - return serializers.DerivativePositionCollateral.parse(*(self._POST("auth/w/deriv/collateral/set", data={ "symbol": symbol, "collateral": collateral })[0])) + return serializers.DerivativePositionCollateral.parse(*(self._POST("auth/w/deriv/collateral/set", body={ "symbol": symbol, "collateral": collateral })[0])) def get_derivative_position_collateral_limits(self, symbol: str) -> DerivativePositionCollateralLimits: - return serializers.DerivativePositionCollateralLimits.parse(*self._POST("auth/calc/deriv/collateral/limits", data={ "symbol": symbol })) + return serializers.DerivativePositionCollateralLimits.parse(*self._POST("auth/calc/deriv/collateral/limits", body={ "symbol": symbol })) def get_funding_offers(self, symbol: Optional[str] = None) -> List[FundingOffer]: endpoint = "auth/r/funding/offers" @@ -165,36 +165,36 @@ class _RestAuthenticatedEndpoints(_Requests): def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, float, str], rate: Union[Decimal, float, str], period: int, flags: Optional[int] = 0) -> Notification[FundingOffer]: - data = { + body = { "type": type, "symbol": symbol, "amount": amount, "rate": rate, "period": period, "flags": flags } - return serializers._Notification[FundingOffer](serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/submit", data=data)) + return serializers._Notification[FundingOffer](serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/submit", body=body)) def cancel_funding_offer(self, id: int) -> Notification[FundingOffer]: - return serializers._Notification[FundingOffer](serializer=serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/cancel", data={ "id": id })) + return serializers._Notification[FundingOffer](serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/cancel", body={ "id": id })) def cancel_all_funding_offers(self, currency: str) -> Notification[Literal[None]]: - return serializers._Notification[Literal[None]](serializer=None).parse( - *self._POST("auth/w/funding/offer/cancel/all", data={ "currency": currency }) + return serializers._Notification[Literal[None]](None).parse( + *self._POST("auth/w/funding/offer/cancel/all", body={ "currency": currency }) ) def submit_funding_close(self, id: int) -> Notification[Literal[None]]: - return serializers._Notification[Literal[None]](serializer=None).parse( - *self._POST("auth/w/funding/close", data={ "id": id }) + return serializers._Notification[Literal[None]](None).parse( + *self._POST("auth/w/funding/close", body={ "id": id }) ) def toggle_auto_renew(self, status: bool, currency: str, amount: Optional[str] = None, rate: Optional[int] = None, period: Optional[int] = None) -> Notification[FundingAutoRenew]: - return serializers._Notification[FundingAutoRenew](serializer=serializers.FundingAutoRenew).parse(*self._POST("auth/w/funding/auto", data={ + return serializers._Notification[FundingAutoRenew](serializers.FundingAutoRenew).parse(*self._POST("auth/w/funding/auto", body={ "status": int(status), "currency": currency, "amount": amount, "rate": rate, "period": period })) def toggle_keep(self, type: Literal["credit", "loan"], ids: Optional[List[int]] = None, changes: Optional[Dict[int, bool]] = None) -> Notification[Literal[None]]: - return serializers._Notification[Literal[None]](serializer=None).parse(*self._POST("auth/w/funding/keep", data={ + return serializers._Notification[Literal[None]](None).parse(*self._POST("auth/w/funding/keep", body={ "type": type, "id": ids, "changes": changes @@ -205,12 +205,12 @@ class _RestAuthenticatedEndpoints(_Requests): endpoint = "auth/r/funding/offers/hist" else: endpoint = f"auth/r/funding/offers/{symbol}/hist" - data = { + body = { "start": start, "end": end, "limit": limit } - return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] def get_funding_loans(self, symbol: Optional[str] = None) -> List[FundingLoan]: if symbol == None: @@ -224,12 +224,12 @@ class _RestAuthenticatedEndpoints(_Requests): endpoint = "auth/r/funding/loans/hist" else: endpoint = f"auth/r/funding/loans/{symbol}/hist" - data = { + body = { "start": start, "end": end, "limit": limit } - return [ serializers.FundingLoan.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + return [ serializers.FundingLoan.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] def get_funding_credits(self, symbol: Optional[str] = None) -> List[FundingCredit]: if symbol == None: @@ -243,25 +243,25 @@ class _RestAuthenticatedEndpoints(_Requests): endpoint = "auth/r/funding/credits/hist" else: endpoint = f"auth/r/funding/credits/{symbol}/hist" - data = { + body = { "start": start, "end": end, "limit": limit } - return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] def get_funding_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingTrade]: if symbol == None: endpoint = "auth/r/funding/trades/hist" else: endpoint = f"auth/r/funding/trades/{symbol}/hist" - data = { + body = { "sort": sort, "start": start, "end": end, "limit": limit } - return [ serializers.FundingTrade.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] + return [ serializers.FundingTrade.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] def get_funding_info(self, key: str) -> FundingInfo: response = self._POST(f"auth/r/info/funding/{key}") @@ -269,45 +269,45 @@ class _RestAuthenticatedEndpoints(_Requests): return serializers.FundingInfo.parse(*([response[1]] + response[2])) def transfer_between_wallets(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, float, str]) -> Notification[Transfer]: - data = { + body = { "from": from_wallet, "to": to_wallet, "currency": currency, "currency_to": currency_to, "amount": amount } - return serializers._Notification[Transfer](serializer=serializers.Transfer).parse(*self._POST("auth/w/transfer", data=data)) + return serializers._Notification[Transfer](serializers.Transfer).parse(*self._POST("auth/w/transfer", body=body)) def submit_wallet_withdrawal(self, wallet: str, method: str, address: str, amount: Union[Decimal, float, str]) -> Notification[Withdrawal]: - return serializers._Notification[Withdrawal](serializer=serializers.Withdrawal).parse(*self._POST("auth/w/withdraw", data={ + return serializers._Notification[Withdrawal](serializers.Withdrawal).parse(*self._POST("auth/w/withdraw", body={ "wallet": wallet, "method": method, "address": address, "amount": amount, })) def get_deposit_address(self, wallet: str, method: str, renew: bool = False) -> Notification[DepositAddress]: - data = { + body = { "wallet": wallet, "method": method, "renew": int(renew) } - return serializers._Notification[DepositAddress](serializer=serializers.DepositAddress).parse(*self._POST("auth/w/deposit/address", data=data)) + return serializers._Notification[DepositAddress](serializers.DepositAddress).parse(*self._POST("auth/w/deposit/address", body=body)) def generate_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, float, str]) -> Invoice: - data = { + body = { "wallet": wallet, "currency": currency, "amount": amount } - return serializers.Invoice.parse(*self._POST("auth/w/deposit/invoice", data=data)) + return serializers.Invoice.parse(*self._POST("auth/w/deposit/invoice", body=body)) def get_movements(self, currency: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Movement]: if currency == None: endpoint = "auth/r/movements/hist" else: endpoint = f"auth/r/movements/{currency}/hist" - data = { + body = { "start": start, "end": end, "limit": limit } - return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, data=data) ] \ No newline at end of file + return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] \ No newline at end of file diff --git a/bfxapi/rest/_RestPublicEndpoints.py b/bfxapi/rest/_RestPublicEndpoints.py index 5157256..c3d2071 100644 --- a/bfxapi/rest/_RestPublicEndpoints.py +++ b/bfxapi/rest/_RestPublicEndpoints.py @@ -175,14 +175,14 @@ class _RestPublicEndpoints(_Requests): return messages def get_trading_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], price_limit: Optional[Union[Decimal, float, str]] = None) -> TradingMarketAveragePrice: - return serializers.TradingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data={ + return serializers.TradingMarketAveragePrice.parse(*self._POST("calc/trade/avg", body={ "symbol": symbol, "amount": amount, "price_limit": price_limit })) def get_funding_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], period: int, rate_limit: Optional[Union[Decimal, float, str]] = None) -> FundingMarketAveragePrice: - return serializers.FundingMarketAveragePrice.parse(*self._POST("calc/trade/avg", data={ + return serializers.FundingMarketAveragePrice.parse(*self._POST("calc/trade/avg", body={ "symbol": symbol, "amount": amount, "period": period, "rate_limit": rate_limit })) def get_fx_rate(self, ccy1: str, ccy2: str) -> FxRate: - return serializers.FxRate.parse(*self._POST("calc/fx", data={ "ccy1": ccy1, "ccy2": ccy2 })) \ No newline at end of file + return serializers.FxRate.parse(*self._POST("calc/fx", body={ "ccy1": ccy1, "ccy2": ccy2 })) \ No newline at end of file diff --git a/bfxapi/utils/JSONEncoder.py b/bfxapi/utils/JSONEncoder.py index 885ab91..506bad1 100644 --- a/bfxapi/utils/JSONEncoder.py +++ b/bfxapi/utils/JSONEncoder.py @@ -8,13 +8,16 @@ JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] class JSONEncoder(json.JSONEncoder): def encode(self, obj: JSON) -> str: + def _strip(dictionary: Dict) -> Dict: + return { key: value for key, value in dictionary.items() if value != None} + def _convert_float_to_str(data: JSON) -> JSON: if isinstance(data, float): return format(Decimal(repr(data)), "f") elif isinstance(data, list): return [ _convert_float_to_str(sub_data) for sub_data in data ] elif isinstance(data, dict): - return { key: _convert_float_to_str(value) for key, value in data.items() } + return _strip({ key: _convert_float_to_str(value) for key, value in data.items() }) else: return data data = _convert_float_to_str(obj) diff --git a/bfxapi/websocket/_BfxWebsocketInputs.py b/bfxapi/websocket/_BfxWebsocketInputs.py index 041405f..0d9ee0b 100644 --- a/bfxapi/websocket/_BfxWebsocketInputs.py +++ b/bfxapi/websocket/_BfxWebsocketInputs.py @@ -5,9 +5,6 @@ from typing import Union, Optional, List, Tuple from .types import JSON from .enums import OrderType, FundingOfferType -def _strip(dictionary): - return { key: value for key, value in dictionary.items() if value != None} - class _BfxWebsocketInputs(object): def __init__(self, __handle_websocket_input): self.__handle_websocket_input = __handle_websocket_input @@ -17,59 +14,44 @@ class _BfxWebsocketInputs(object): price_trailing: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_oco_stop: Optional[Union[Decimal, float, str]] = None, gid: Optional[int] = None, cid: Optional[int] = None, flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None): - data = _strip({ + await self.__handle_websocket_input("on", { "type": type, "symbol": symbol, "amount": amount, "price": price, "lev": lev, "price_trailing": price_trailing, "price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop, "gid": gid, "cid": cid, "flags": flags, "tif": tif, "meta": meta }) - - await self.__handle_websocket_input("on", data) async def update_order(self, id: int, amount: Optional[Union[Decimal, float, str]] = None, price: Optional[Union[Decimal, float, str]] = None, cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, tif: Optional[Union[datetime, str]] = None): - data = _strip({ + await self.__handle_websocket_input("ou", { "id": id, "amount": amount, "price": price, "cid": cid, "cid_date": cid_date, "gid": gid, "flags": flags, "lev": lev, "delta": delta, "price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif }) - - await self.__handle_websocket_input("ou", data) async def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None): - data = _strip({ - "id": id, - "cid": cid, - "cid_date": cid_date + await self.__handle_websocket_input("oc", { + "id": id, "cid": cid, "cid_date": cid_date }) - await self.__handle_websocket_input("oc", data) - async def cancel_order_multi(self, ids: Optional[List[int]] = None, cids: Optional[List[Tuple[int, str]]] = None, gids: Optional[List[int]] = None, all: bool = False): - data = _strip({ - "ids": ids, - "cids": cids, - "gids": gids, - + await self.__handle_websocket_input("oc_multi", { + "ids": ids, "cids": cids, "gids": gids, "all": int(all) }) - - await self.__handle_websocket_input("oc_multi", data) async def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, float, str], rate: Union[Decimal, float, str], period: int, flags: Optional[int] = 0): - data = { + await self.__handle_websocket_input("fon", { "type": type, "symbol": symbol, "amount": amount, "rate": rate, "period": period, "flags": flags - } - - await self.__handle_websocket_input("fon", data) + }) async def cancel_funding_offer(self, id: int): await self.__handle_websocket_input("foc", { "id": id }) From 52ff5006b1c6a04fc52c799052ca38926d9c17ad Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 6 Feb 2023 19:36:54 +0100 Subject: [PATCH 140/265] Add bfxapi/tests/test_labeler.py unit test. --- bfxapi/tests/__init__.py | 2 ++ bfxapi/tests/test_labeler.py | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 bfxapi/tests/test_labeler.py diff --git a/bfxapi/tests/__init__.py b/bfxapi/tests/__init__.py index bbf78c0..25965b4 100644 --- a/bfxapi/tests/__init__.py +++ b/bfxapi/tests/__init__.py @@ -1,6 +1,7 @@ import unittest from .test_rest_serializers_and_types import TestRestSerializersAndTypes from .test_websocket_serializers_and_types import TestWebsocketSerializersAndTypes +from .test_labeler import TestLabeler NAME = "tests" @@ -8,6 +9,7 @@ def suite(): return unittest.TestSuite([ unittest.makeSuite(TestRestSerializersAndTypes), unittest.makeSuite(TestWebsocketSerializersAndTypes), + unittest.makeSuite(TestLabeler), ]) if __name__ == "__main__": diff --git a/bfxapi/tests/test_labeler.py b/bfxapi/tests/test_labeler.py new file mode 100644 index 0000000..721b20e --- /dev/null +++ b/bfxapi/tests/test_labeler.py @@ -0,0 +1,24 @@ +import unittest + +from dataclasses import dataclass +from ..exceptions import LabelerSerializerException +from ..labeler import _Type, generate_labeler_serializer, generate_recursive_serializer + +class TestLabeler(unittest.TestCase): + def test_generate_labeler_serializer(self): + @dataclass + class Test(_Type): + A: int + B: float + C: str + + labels = [ "A", "_PLACEHOLDER", "B", "_PLACEHOLDER", "C" ] + + serializer = generate_labeler_serializer("Test", Test, labels) + + self.assertEqual(serializer.parse(5, None, 65.0, None, "X"), Test(5, 65.0, "X")) + self.assertRaises(LabelerSerializerException, serializer.parse, 5, 65.0, "X") + self.assertListEqual(serializer.get_labels(), [ "A", "B", "C" ]) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 168b2eae259d71c0b3c0db764419b2d0a5765f9f Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 7 Feb 2023 17:21:51 +0100 Subject: [PATCH 141/265] Implement new unit tests in bfxapi/tests/test_labeler.py. --- bfxapi/tests/test_labeler.py | 38 +++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/bfxapi/tests/test_labeler.py b/bfxapi/tests/test_labeler.py index 721b20e..a4310ef 100644 --- a/bfxapi/tests/test_labeler.py +++ b/bfxapi/tests/test_labeler.py @@ -16,9 +16,41 @@ class TestLabeler(unittest.TestCase): serializer = generate_labeler_serializer("Test", Test, labels) - self.assertEqual(serializer.parse(5, None, 65.0, None, "X"), Test(5, 65.0, "X")) - self.assertRaises(LabelerSerializerException, serializer.parse, 5, 65.0, "X") - self.assertListEqual(serializer.get_labels(), [ "A", "B", "C" ]) + self.assertEqual(serializer.parse(5, None, 65.0, None, "X"), Test(5, 65.0, "X"), + msg="_Serializer should produce the right result.") + + self.assertEqual(serializer.parse(5, 65.0, "X", skip=[ "_PLACEHOLDER" ]), Test(5, 65.0, "X"), + msg="_Serializer should produce the right result when skip parameter is given.") + + self.assertListEqual(serializer.get_labels(), [ "A", "B", "C" ], + msg="_Serializer::get_labels() should return the right list of labels.") + + with self.assertRaises(LabelerSerializerException, + msg="_Serializer should raise LabelerSerializerException if given fewer arguments than the serializer labels."): + serializer.parse(5, 65.0, "X") + + def test_generate_recursive_serializer(self): + @dataclass + class Outer(_Type): + A: int + B: float + C: "Middle" + + @dataclass + class Middle(_Type): + D: str + E: "Inner" + + @dataclass + class Inner(_Type): + F: bool + + inner = generate_labeler_serializer("Inner", Inner, ["F"]) + middle = generate_recursive_serializer("Middle", Middle, ["D", "E"], { "E": inner }) + outer = generate_recursive_serializer("Outer", Outer, ["A", "B", "C"], { "C": middle }) + + self.assertEqual(outer.parse(10, 45.5, [ "Y", [ True ] ]), Outer(10, 45.5, Middle("Y", Inner(True))), + msg="_RecursiveSerializer should produce the right result.") if __name__ == "__main__": unittest.main() \ No newline at end of file From 0a9384e67045a28f847677de9f58d5dacae996fe Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 7 Feb 2023 17:45:03 +0100 Subject: [PATCH 142/265] Add new bfxapi/tests/test_notification unit test. --- bfxapi/labeler.py | 2 +- bfxapi/tests/__init__.py | 2 ++ bfxapi/tests/test_notification.py | 25 +++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 bfxapi/tests/test_notification.py diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 1c2655d..79f5ed4 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -19,7 +19,7 @@ class _Serializer(Generic[T]): labels = list(filter(lambda label: label not in (skip or list()), self.__labels)) if len(labels) > len(args): - raise LabelerSerializerException(" and <*args> arguments should contain the same amount of elements.") + raise LabelerSerializerException(f"{self.name} -> and <*args> arguments should contain the same amount of elements.") for index, label in enumerate(labels): if label not in self.__IGNORE: diff --git a/bfxapi/tests/__init__.py b/bfxapi/tests/__init__.py index 25965b4..a63ea0d 100644 --- a/bfxapi/tests/__init__.py +++ b/bfxapi/tests/__init__.py @@ -2,6 +2,7 @@ import unittest from .test_rest_serializers_and_types import TestRestSerializersAndTypes from .test_websocket_serializers_and_types import TestWebsocketSerializersAndTypes from .test_labeler import TestLabeler +from .test_notification import TestNotification NAME = "tests" @@ -10,6 +11,7 @@ def suite(): unittest.makeSuite(TestRestSerializersAndTypes), unittest.makeSuite(TestWebsocketSerializersAndTypes), unittest.makeSuite(TestLabeler), + unittest.makeSuite(TestNotification), ]) if __name__ == "__main__": diff --git a/bfxapi/tests/test_notification.py b/bfxapi/tests/test_notification.py new file mode 100644 index 0000000..f71df60 --- /dev/null +++ b/bfxapi/tests/test_notification.py @@ -0,0 +1,25 @@ +import unittest + +from dataclasses import dataclass +from ..labeler import generate_labeler_serializer +from ..notification import _Type, _Notification, Notification + +class TestNotification(unittest.TestCase): + def test_notification(self): + @dataclass + class Test(_Type): + A: int + B: float + C: str + + test = generate_labeler_serializer("Test", Test, + [ "A", "_PLACEHOLDER", "B", "_PLACEHOLDER", "C" ]) + + notification = _Notification[Test](test) + + self.assertEqual(notification.parse(*[1675787861506, "test", None, None, [ 5, None, 65.0, None, "X" ], 0, "SUCCESS", "This is just a test notification."]), + Notification[Test](1675787861506, "test", None, Test(5, 65.0, "X"), 0, "SUCCESS", "This is just a test notification."), + msg="_Notification should produce the right notification.") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 2d012611822e7f780b3925e3810e2d0e1b02db6e Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 7 Feb 2023 18:31:02 +0100 Subject: [PATCH 143/265] Organize rest sub-package. Create new endpoints and middleware sub-packages. Rename class Requests to Middleware. --- bfxapi/rest/BfxRestInterface.py | 13 ------------- bfxapi/rest/__init__.py | 4 +++- bfxapi/rest/endpoints/__init__.py | 5 +++++ bfxapi/rest/endpoints/bfx_rest_interface.py | 13 +++++++++++++ .../rest_authenticated_endpoints.py} | 12 ++++++------ .../rest_public_endpoints.py} | 10 +++++----- bfxapi/rest/middleware/__init__.py | 3 +++ .../rest/{_Requests.py => middleware/middleware.py} | 8 ++++---- setup.py | 6 +++++- 9 files changed, 44 insertions(+), 30 deletions(-) delete mode 100644 bfxapi/rest/BfxRestInterface.py create mode 100644 bfxapi/rest/endpoints/__init__.py create mode 100644 bfxapi/rest/endpoints/bfx_rest_interface.py rename bfxapi/rest/{_RestAuthenticatedEndpoints.py => endpoints/rest_authenticated_endpoints.py} (98%) rename bfxapi/rest/{_RestPublicEndpoints.py => endpoints/rest_public_endpoints.py} (98%) create mode 100644 bfxapi/rest/middleware/__init__.py rename bfxapi/rest/{_Requests.py => middleware/middleware.py} (93%) diff --git a/bfxapi/rest/BfxRestInterface.py b/bfxapi/rest/BfxRestInterface.py deleted file mode 100644 index 91d31a2..0000000 --- a/bfxapi/rest/BfxRestInterface.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Optional - -from ._RestPublicEndpoints import _RestPublicEndpoints - -from ._RestAuthenticatedEndpoints import _RestAuthenticatedEndpoints - -class BfxRestInterface(object): - VERSION = 2 - - def __init__(self, host: str, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None): - self.public = _RestPublicEndpoints(host=host) - - self.auth = _RestAuthenticatedEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET) \ No newline at end of file diff --git a/bfxapi/rest/__init__.py b/bfxapi/rest/__init__.py index 0bf3d2e..7ee9fed 100644 --- a/bfxapi/rest/__init__.py +++ b/bfxapi/rest/__init__.py @@ -1 +1,3 @@ -from .BfxRestInterface import BfxRestInterface \ No newline at end of file +from .endpoints import BfxRestInterface, RestPublicEndpoints, RestAuthenticatedEndpoints + +NAME = "rest" \ No newline at end of file diff --git a/bfxapi/rest/endpoints/__init__.py b/bfxapi/rest/endpoints/__init__.py new file mode 100644 index 0000000..24a005d --- /dev/null +++ b/bfxapi/rest/endpoints/__init__.py @@ -0,0 +1,5 @@ +from .bfx_rest_interface import BfxRestInterface +from .rest_public_endpoints import RestPublicEndpoints +from .rest_authenticated_endpoints import RestAuthenticatedEndpoints + +NAME = "endpoints" \ No newline at end of file diff --git a/bfxapi/rest/endpoints/bfx_rest_interface.py b/bfxapi/rest/endpoints/bfx_rest_interface.py new file mode 100644 index 0000000..53f87b0 --- /dev/null +++ b/bfxapi/rest/endpoints/bfx_rest_interface.py @@ -0,0 +1,13 @@ +from typing import Optional + +from .rest_public_endpoints import RestPublicEndpoints + +from .rest_authenticated_endpoints import RestAuthenticatedEndpoints + +class BfxRestInterface(object): + VERSION = 2 + + def __init__(self, host: str, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None): + self.public = RestPublicEndpoints(host=host) + + self.auth = RestAuthenticatedEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET) \ No newline at end of file diff --git a/bfxapi/rest/_RestAuthenticatedEndpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py similarity index 98% rename from bfxapi/rest/_RestAuthenticatedEndpoints.py rename to bfxapi/rest/endpoints/rest_authenticated_endpoints.py index ab4b9bc..a207673 100644 --- a/bfxapi/rest/_RestAuthenticatedEndpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -1,16 +1,16 @@ -from typing import List, Union, Literal, Optional, Any, cast +from typing import List, Union, Literal, Optional -from .types import * +from ..types import * -from . import serializers +from .. import serializers -from .enums import Config, Sort, OrderType, FundingOfferType +from ..enums import Sort, OrderType, FundingOfferType from decimal import Decimal from datetime import datetime -from ._Requests import _Requests +from ..middleware import Middleware -class _RestAuthenticatedEndpoints(_Requests): +class RestAuthenticatedEndpoints(Middleware): def get_wallets(self) -> List[Wallet]: return [ serializers.Wallet.parse(*sub_data) for sub_data in self._POST("auth/r/wallets") ] diff --git a/bfxapi/rest/_RestPublicEndpoints.py b/bfxapi/rest/endpoints/rest_public_endpoints.py similarity index 98% rename from bfxapi/rest/_RestPublicEndpoints.py rename to bfxapi/rest/endpoints/rest_public_endpoints.py index c3d2071..687a2f5 100644 --- a/bfxapi/rest/_RestPublicEndpoints.py +++ b/bfxapi/rest/endpoints/rest_public_endpoints.py @@ -1,15 +1,15 @@ from typing import List, Union, Literal, Optional, Any, cast -from .types import * +from ..types import * -from . import serializers +from .. import serializers -from .enums import Config, Sort +from ..enums import Config, Sort from decimal import Decimal -from ._Requests import _Requests +from ..middleware import Middleware -class _RestPublicEndpoints(_Requests): +class RestPublicEndpoints(Middleware): def conf(self, config: Config) -> Any: return self._GET(f"conf/{config}")[0] diff --git a/bfxapi/rest/middleware/__init__.py b/bfxapi/rest/middleware/__init__.py new file mode 100644 index 0000000..d7e276b --- /dev/null +++ b/bfxapi/rest/middleware/__init__.py @@ -0,0 +1,3 @@ +from .middleware import Middleware + +NAME = "middleware" \ No newline at end of file diff --git a/bfxapi/rest/_Requests.py b/bfxapi/rest/middleware/middleware.py similarity index 93% rename from bfxapi/rest/_Requests.py rename to bfxapi/rest/middleware/middleware.py index 70557cb..627c5cf 100644 --- a/bfxapi/rest/_Requests.py +++ b/bfxapi/rest/middleware/middleware.py @@ -3,15 +3,15 @@ import time, hmac, hashlib, json, requests from typing import TYPE_CHECKING, Optional, Any from http import HTTPStatus -from .enums import Error -from .exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError +from ..enums import Error +from ..exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError -from ..utils.JSONEncoder import JSONEncoder +from ...utils.JSONEncoder import JSONEncoder if TYPE_CHECKING: from requests.sessions import _Params -class _Requests(object): +class Middleware(object): def __init__(self, host: str, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None): self.host, self.API_KEY, self.API_SECRET = host, API_KEY, API_SECRET diff --git a/setup.py b/setup.py index 963f30a..54db508 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,11 @@ from distutils.core import setup setup( name="bitfinex-api-py", version="3.0.0", - packages=[ "bfxapi", "bfxapi.websocket", "bfxapi.rest", "bfxapi.utils" ], + packages=[ + "bfxapi", "bfxapi.utils", + "bfxapi.websocket", + "bfxapi.rest", "bfxapi.rest.endpoints", "bfxapi.rest.middleware", + ], url="https://github.com/bitfinexcom/bitfinex-api-py", license="OSI Approved :: Apache Software License", author="Bitfinex", From 851521c63fb8b3e03f7900f87a54d3549c941ac4 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 7 Feb 2023 19:00:50 +0100 Subject: [PATCH 144/265] Remove bfxapi/utils/flags.py file from bfxapi.utils sub-package. --- bfxapi/utils/flags.py | 29 --------------------------- examples/rest/create_funding_offer.py | 5 ++--- examples/rest/create_order.py | 6 ++---- 3 files changed, 4 insertions(+), 36 deletions(-) delete mode 100644 bfxapi/utils/flags.py diff --git a/bfxapi/utils/flags.py b/bfxapi/utils/flags.py deleted file mode 100644 index f897103..0000000 --- a/bfxapi/utils/flags.py +++ /dev/null @@ -1,29 +0,0 @@ -from .. enums import Flag - -def calculate_order_flags( - hidden : bool = False, - close : bool = False, - reduce_only : bool = False, - post_only : bool = False, - oco : bool = False, - no_var_rates: bool = False -) -> int: - flags = 0 - - if hidden: flags += Flag.HIDDEN - if close: flags += Flag.CLOSE - if reduce_only: flags += Flag.REDUCE_ONLY - if post_only: flags += Flag.POST_ONLY - if oco: flags += Flag.OCO - if no_var_rates: flags += Flag.NO_VAR_RATES - - return flags - -def calculate_offer_flags( - hidden : bool = False -) -> int: - flags = 0 - - if hidden: flags += Flag.HIDDEN - - return flags \ No newline at end of file diff --git a/examples/rest/create_funding_offer.py b/examples/rest/create_funding_offer.py index fd10cc8..c1031d8 100644 --- a/examples/rest/create_funding_offer.py +++ b/examples/rest/create_funding_offer.py @@ -3,8 +3,7 @@ import os from bfxapi.client import Client, Constants -from bfxapi.enums import FundingOfferType -from bfxapi.utils.flags import calculate_offer_flags +from bfxapi.enums import FundingOfferType, Flag bfx = Client( REST_HOST=Constants.REST_HOST, @@ -18,7 +17,7 @@ notification = bfx.rest.auth.submit_funding_offer( amount="123.45", rate="0.001", period=2, - flags=calculate_offer_flags(hidden=True) + flags=Flag.HIDDEN ) print("Offer notification:", notification) diff --git a/examples/rest/create_order.py b/examples/rest/create_order.py index 2c13ae2..ea70265 100644 --- a/examples/rest/create_order.py +++ b/examples/rest/create_order.py @@ -1,10 +1,8 @@ # python -c "import examples.rest.create_order" import os - from bfxapi.client import Client, Constants -from bfxapi.enums import OrderType -from bfxapi.utils.flags import calculate_order_flags +from bfxapi.enums import OrderType, Flag bfx = Client( REST_HOST=Constants.REST_HOST, @@ -18,7 +16,7 @@ submitted_order = bfx.rest.auth.submit_order( symbol="tBTCUST", amount="0.015", price="10000", - flags=calculate_order_flags(hidden=False) + flags=Flag.HIDDEN + Flag.OCO + Flag.CLOSE ) print("Submit Order Notification:", submitted_order) From 6693e376fc7b69a09e58eaa00e7d0c6150ccec31 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 8 Feb 2023 14:57:36 +0100 Subject: [PATCH 145/265] Fix bug in bfxapi.middleware sub-package. --- bfxapi/rest/middleware/middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bfxapi/rest/middleware/middleware.py b/bfxapi/rest/middleware/middleware.py index 627c5cf..0c1b02e 100644 --- a/bfxapi/rest/middleware/middleware.py +++ b/bfxapi/rest/middleware/middleware.py @@ -55,7 +55,7 @@ class Middleware(object): return data def _POST(self, endpoint: str, params: Optional["_Params"] = None, body: Optional[Any] = None, _ignore_authentication_headers: bool = False) -> Any: - data = json.dumps(body, cls=JSONEncoder) + data = body and json.dumps(body, cls=JSONEncoder) or None headers = { "Content-Type": "application/json" } From 15a2e41e438547a0f9c4547982d771066db62279 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Wed, 8 Feb 2023 11:56:45 +0100 Subject: [PATCH 146/265] user info + fixs --- .../endpoints/rest_authenticated_endpoints.py | 3 + bfxapi/rest/serializers.py | 58 +++++++++++++++++++ bfxapi/rest/types.py | 31 ++++++++++ examples/rest/funding_auto_renew.py | 2 +- examples/rest/get_authenticated_data.py | 11 +++- 5 files changed, 102 insertions(+), 3 deletions(-) diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index a207673..384a418 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -11,6 +11,9 @@ from datetime import datetime from ..middleware import Middleware class RestAuthenticatedEndpoints(Middleware): + def get_user_info(self) -> UserInfo: + return serializers.UserInfo.parse(*self._POST(f"auth/r/info/user")) + def get_wallets(self) -> List[Wallet]: return [ serializers.Wallet.parse(*sub_data) for sub_data in self._POST("auth/r/wallets") ] diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 021211c..a33a04d 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -268,6 +268,64 @@ FxRate = generate_labeler_serializer("FxRate", klass=types.FxRate, labels=[ #region Serializers definition for Rest Authenticated Endpoints +UserInfo = generate_labeler_serializer("UserInfo", klass=types.UserInfo, labels=[ + "id", + "email", + "username", + "mts_account_create", + "verified", + "verification_level", + "_PLACEHOLDER", + "timezone", + "locale", + "company", + "email_verified", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "mts_master_account_create", + "group_id", + "master_account_id", + "inherit_master_account_verification", + "is_group_master", + "group_withdraw_enabled", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "ppt_enabled", + "merchant_enabled", + "competition_enabled", + "two_factors_authentication_modes", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "is_securities_master", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "securities_enabled", + "allow_disable_ctxswitch", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "time_last_login", + "_PLACEHOLDER", + "_PLACEHOLDER", + "ctxtswitch_disabled", + "_PLACEHOLDER", + "comp_countries", + "compl_countries_resid", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "is_merchant_enterprise" +]) + Order = generate_labeler_serializer("Order", klass=types.Order, labels=[ "id", "gid", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index ab41fec..da0f1bc 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -197,6 +197,37 @@ class FxRate(_Type): #region Type hinting for Rest Authenticated Endpoints +@dataclass +class UserInfo(_Type): + id: int + email: str + username: str + mts_account_create: int + verified: int + verification_level: int + timezone: str + locale: str + company: str + email_verified: int + mts_master_account_create: int + group_id: int + master_account_id: int + inherit_master_account_verification: int + is_group_master: int + group_withdraw_enabled: int + ppt_enabled: int + merchant_enabled: int + competition_enabled: int + two_factors_authentication_modes: List[str] + is_securities_master: int + securities_enabled: int + allow_disable_ctxswitch: int + ctxtswitch_disabled: int + time_last_login: int + comp_countries: List[str] + compl_countries_resid: List[str] + is_merchant_enterprise: int + @dataclass class Order(_Type): id: int diff --git a/examples/rest/funding_auto_renew.py b/examples/rest/funding_auto_renew.py index c892ce3..11ee7ca 100644 --- a/examples/rest/funding_auto_renew.py +++ b/examples/rest/funding_auto_renew.py @@ -14,7 +14,7 @@ notification = bfx.rest.auth.toggle_auto_renew( status=True, currency="USD", amount="150", - rate="0", + rate="0", # FRR period=2 ) diff --git a/examples/rest/get_authenticated_data.py b/examples/rest/get_authenticated_data.py index 2ff1de6..f398773 100644 --- a/examples/rest/get_authenticated_data.py +++ b/examples/rest/get_authenticated_data.py @@ -13,6 +13,12 @@ bfx = Client( now = int(round(time.time() * 1000)) + +def log_user_info(): + user_info = bfx.rest.auth.get_user_info() + print(user_info) + + def log_wallets(): wallets = bfx.rest.auth.get_wallets() print("Wallets:") @@ -38,14 +44,14 @@ def log_positions(): def log_trades(): - trades = bfx.rest.auth.get_trades(symbol='tBTCUSD', start=0, end=now) + trades = bfx.rest.auth.get_trades_history(symbol='tBTCUSD', start=0, end=now) print("Trades:") [print(t) for t in trades] def log_order_trades(): order_id = 82406909127 - trades = bfx.rest.auth.get_order_trades(symbol='tBTCUSD', order_id=order_id) + trades = bfx.rest.auth.get_order_trades(symbol='tBTCUSD', id=order_id) print("Trade orders:") [print(t) for t in trades] @@ -96,6 +102,7 @@ def log_margin_info(): print(f"Base margin info {base_margin_info}") def run(): + log_user_info() log_wallets() log_orders() log_orders_history() From 48583786f7338e7e8c1711f296bb37859a514179 Mon Sep 17 00:00:00 2001 From: itsdeka Date: Wed, 8 Feb 2023 12:39:51 +0100 Subject: [PATCH 147/265] login history + balance available --- .../endpoints/rest_authenticated_endpoints.py | 9 +++++++++ bfxapi/rest/serializers.py | 15 +++++++++++++++ bfxapi/rest/types.py | 11 +++++++++++ examples/rest/get_authenticated_data.py | 5 +++++ 4 files changed, 40 insertions(+) diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index 384a418..4307bc0 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -14,6 +14,15 @@ class RestAuthenticatedEndpoints(Middleware): def get_user_info(self) -> UserInfo: return serializers.UserInfo.parse(*self._POST(f"auth/r/info/user")) + def get_login_history(self) -> LoginHistory: + return [ serializers.LoginHistory.parse(*sub_data) for sub_data in self._POST("auth/r/logins/hist") ] + + def get_balance_available_for_orders_or_offers(self, symbol: str, type: str, dir: Optional[int] = None, rate: Optional[str] = None, lev: Optional[str] = None) -> BalanceAvailable: + return serializers.BalanceAvailable.parse(*self._POST("auth/calc/order/avail", body={ + "symbol": symbol, "type": type, "dir": dir, + "rate": rate, "lev": lev + })) + def get_wallets(self) -> List[Wallet]: return [ serializers.Wallet.parse(*sub_data) for sub_data in self._POST("auth/r/wallets") ] diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index a33a04d..43943ff 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -326,6 +326,21 @@ UserInfo = generate_labeler_serializer("UserInfo", klass=types.UserInfo, labels= "is_merchant_enterprise" ]) +LoginHistory = generate_labeler_serializer("LoginHistory", klass=types.LoginHistory, labels=[ + "id", + "_PLACEHOLDER", + "time", + "_PLACEHOLDER", + "ip", + "_PLACEHOLDER", + "_PLACEHOLDER", + "extra_info" +]) + +BalanceAvailable = generate_labeler_serializer("BalanceAvailable", klass=types.BalanceAvailable, labels=[ + "amount" +]) + Order = generate_labeler_serializer("Order", klass=types.Order, labels=[ "id", "gid", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index da0f1bc..eee3c30 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -228,6 +228,17 @@ class UserInfo(_Type): compl_countries_resid: List[str] is_merchant_enterprise: int +@dataclass +class LoginHistory(_Type): + id: int + time: int + ip: str + extra_info: JSON + +@dataclass +class BalanceAvailable(_Type): + amount: float + @dataclass class Order(_Type): id: int diff --git a/examples/rest/get_authenticated_data.py b/examples/rest/get_authenticated_data.py index f398773..ada724a 100644 --- a/examples/rest/get_authenticated_data.py +++ b/examples/rest/get_authenticated_data.py @@ -19,6 +19,11 @@ def log_user_info(): print(user_info) +def log_login_history(): + login_history = bfx.rest.auth.get_login_history() + print(login_history) + + def log_wallets(): wallets = bfx.rest.auth.get_wallets() print("Wallets:") From 5cf3b18ff3a793105a3429b7bde8482bee036ef2 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 9 Feb 2023 04:16:31 +0100 Subject: [PATCH 148/265] Fix bug in composing data object inside bfxapi/rest/endpoints/rest_authenticated_endpoints.py file. --- .../rest/endpoints/rest_authenticated_endpoints.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index 4307bc0..05502c1 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -125,8 +125,8 @@ class RestAuthenticatedEndpoints(Middleware): def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo: response = self._POST(f"auth/r/info/margin/{symbol}") - - return serializers.SymbolMarginInfo.parse(*([response[1]] + response[2])) + data = [response[1]] + response[2] + return serializers.SymbolMarginInfo.parse(*data) def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]: return [ serializers.SymbolMarginInfo.parse(*([sub_data[1]] + sub_data[2])) for sub_data in self._POST(f"auth/r/info/margin/sym_all") ] @@ -146,10 +146,8 @@ class RestAuthenticatedEndpoints(Middleware): def get_increase_position_info(self, symbol: str, amount: Union[Decimal, float, str]) -> PositionIncreaseInfo: response = self._POST(f"auth/r/position/increase/info", body={ "symbol": symbol, "amount": amount }) - - return serializers.PositionIncreaseInfo.parse(*( - response[0] + [response[1][0]] + response[1][1] + [response[1][2]] + response[4] + response[5] - )) + data = response[0] + [response[1][0]] + response[1][1] + [response[1][2]] + response[4] + response[5] + return serializers.PositionIncreaseInfo.parse(*data) def get_positions_history(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionHistory]: return [ serializers.PositionHistory.parse(*sub_data) for sub_data in self._POST("auth/r/positions/hist", body={ "start": start, "end": end, "limit": limit }) ] @@ -277,8 +275,8 @@ class RestAuthenticatedEndpoints(Middleware): def get_funding_info(self, key: str) -> FundingInfo: response = self._POST(f"auth/r/info/funding/{key}") - - return serializers.FundingInfo.parse(*([response[1]] + response[2])) + data = [response[1]] + response[2] + return serializers.FundingInfo.parse(*data) def transfer_between_wallets(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, float, str]) -> Notification[Transfer]: body = { From 7e7c4ef23aa7437674846eaa28877fc6ba7377c0 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 9 Feb 2023 04:22:22 +0100 Subject: [PATCH 149/265] Fix all mypy errors and warnings. --- bfxapi/rest/endpoints/rest_authenticated_endpoints.py | 2 +- bfxapi/rest/middleware/middleware.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index 05502c1..789ad5b 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -14,7 +14,7 @@ class RestAuthenticatedEndpoints(Middleware): def get_user_info(self) -> UserInfo: return serializers.UserInfo.parse(*self._POST(f"auth/r/info/user")) - def get_login_history(self) -> LoginHistory: + def get_login_history(self) -> List[LoginHistory]: return [ serializers.LoginHistory.parse(*sub_data) for sub_data in self._POST("auth/r/logins/hist") ] def get_balance_available_for_orders_or_offers(self, symbol: str, type: str, dir: Optional[int] = None, rate: Optional[str] = None, lev: Optional[str] = None) -> BalanceAvailable: diff --git a/bfxapi/rest/middleware/middleware.py b/bfxapi/rest/middleware/middleware.py index 0c1b02e..9180841 100644 --- a/bfxapi/rest/middleware/middleware.py +++ b/bfxapi/rest/middleware/middleware.py @@ -1,6 +1,6 @@ import time, hmac, hashlib, json, requests -from typing import TYPE_CHECKING, Optional, Any +from typing import TYPE_CHECKING, Optional, Any, cast from http import HTTPStatus from ..enums import Error @@ -15,7 +15,7 @@ class Middleware(object): def __init__(self, host: str, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None): self.host, self.API_KEY, self.API_SECRET = host, API_KEY, API_SECRET - def __build_authentication_headers(self, endpoint: str, data: str): + def __build_authentication_headers(self, endpoint: str, data: Optional[str] = None): assert isinstance(self.API_KEY, str) and isinstance(self.API_SECRET, str), \ "API_KEY and API_SECRET must be both str to call __build_authentication_headers" @@ -69,7 +69,7 @@ class Middleware(object): data = response.json() - if len(data) and data[0] == "error": + if isinstance(data, list) and len(data) and data[0] == "error": if data[1] == Error.ERR_PARAMS: raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") From 7dc043bbe50a2eee8c88e80144f03774f2ecee3f Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 9 Feb 2023 04:31:52 +0100 Subject: [PATCH 150/265] Small fix in BfxWebsocketClient class. --- bfxapi/websocket/BfxWebsocketClient.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/BfxWebsocketClient.py index 3bdb3d9..98b5b75 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/BfxWebsocketClient.py @@ -38,7 +38,8 @@ class BfxWebsocketClient(object): self.host, self.websocket, self.event_emitter = host, None, AsyncIOEventEmitter() self.event_emitter.add_listener("error", - lambda exception: self.logger.error("\n" + str().join(traceback.format_exception(type(exception), exception, exception.__traceback__))[:-1]) + 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 @@ -52,7 +53,8 @@ class BfxWebsocketClient(object): 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>.") + 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()) From 180e92fcb4194c6609914e82838dae510cc138c3 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 10 Feb 2023 04:27:35 +0100 Subject: [PATCH 151/265] merchant Co-Authored-By: itsdeka --- .../endpoints/rest_authenticated_endpoints.py | 24 ++++++++-- bfxapi/rest/serializers.py | 4 +- bfxapi/rest/types.py | 48 +++++++++++++++++-- bfxapi/utils/JSONEncoder.py | 38 +++++++-------- examples/rest/merchant.py | 33 +++++++++++++ 5 files changed, 119 insertions(+), 28 deletions(-) create mode 100644 examples/rest/merchant.py diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index 789ad5b..e4ca99f 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -302,13 +302,13 @@ class RestAuthenticatedEndpoints(Middleware): return serializers._Notification[DepositAddress](serializers.DepositAddress).parse(*self._POST("auth/w/deposit/address", body=body)) - def generate_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, float, str]) -> Invoice: + def generate_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, float, str]) -> LightningNetworkInvoice: body = { "wallet": wallet, "currency": currency, "amount": amount } - return serializers.Invoice.parse(*self._POST("auth/w/deposit/invoice", body=body)) + return serializers.LightningNetworkInvoice.parse(*self._POST("auth/w/deposit/invoice", body=body)) def get_movements(self, currency: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Movement]: if currency == None: @@ -320,4 +320,22 @@ class RestAuthenticatedEndpoints(Middleware): "limit": limit } - return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] \ No newline at end of file + return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] + + def submit_invoice(self, amount: Union[Decimal, float, str], currency: str, order_id: str, + customer_info: CustomerInfo, pay_currencies: List[str], duration: Optional[int] = None, + webhook: Optional[str] = None, redirect_url: Optional[str] = None) -> InvoiceSubmission: + data = self._POST("auth/w/ext/pay/invoice/create", body={ + "amount": amount, "currency": currency, "order_id": order_id, + "customer_info": customer_info, "pay_currencies": pay_currencies, "duration": duration, + "webhook": webhook, "redirect_url": redirect_url + }) + + if "customer_info" in data and data["customer_info"] != None: + data["customer_info"] = CustomerInfo(**data["customer_info"]) + + if "invoices" in data and data["invoices"] != None: + for index, invoice in enumerate(data["invoices"]): + data["invoices"][index] = Invoice(**invoice) + + return InvoiceSubmission(**data) \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 43943ff..6e80ed7 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -18,7 +18,7 @@ __serializers__ = [ "FundingOffer", "FundingCredit", "FundingLoan", "FundingAutoRenew", "FundingInfo", "Wallet", "Transfer", "Withdrawal", "DepositAddress", - "Invoice", "Movement", "SymbolMarginInfo", + "LightningNetworkInvoice", "Movement", "SymbolMarginInfo", "BaseMarginInfo", "PositionClaim", "PositionIncreaseInfo", "PositionIncrease", "PositionHistory", "PositionSnapshot", "PositionAudit", "DerivativePositionCollateral", "DerivativePositionCollateralLimits", @@ -581,7 +581,7 @@ DepositAddress = generate_labeler_serializer("DepositAddress", klass=types.Depos "pool_address" ]) -Invoice = generate_labeler_serializer("Invoice", klass=types.Invoice, labels=[ +LightningNetworkInvoice = generate_labeler_serializer("LightningNetworkInvoice", klass=types.LightningNetworkInvoice, labels=[ "invoice_hash", "invoice", "_PLACEHOLDER", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index eee3c30..964b24b 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -1,10 +1,12 @@ -from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any +from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Literal, Any from dataclasses import dataclass +from types import SimpleNamespace + from .. labeler import _Type from .. notification import Notification -from ..utils.JSONEncoder import JSON +from .. utils.JSONEncoder import JSON #region Type hinting for Rest Public Endpoints @@ -440,7 +442,7 @@ class DepositAddress(_Type): pool_address: str @dataclass -class Invoice(_Type): +class LightningNetworkInvoice(_Type): invoice_hash: str invoice: str amount: str @@ -561,4 +563,44 @@ class DerivativePositionCollateralLimits(_Type): min_collateral: float max_collateral: float +#endregion + +#region Type hinting for models which are not serializable + +@dataclass +class InvoiceSubmission(_Type): + id: str + t: int + type: Literal["ECOMMERCE", "POS"] + duration: int + amount: float + currency: str + order_id: str + pay_currencies: List[str] + webhook: str + redirect_url: str + status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"] + customer_info: Optional["CustomerInfo"] + invoices: List["Invoice"] + +class CustomerInfo(SimpleNamespace): + nationality: str + resid_country: str + resid_state: str + resid_city: str + resid_zip_code: str + resid_street: str + resid_building_no: str + full_name: str + email: str + tos_accepted: bool + +class Invoice(SimpleNamespace): + amount: float + currency: str + pay_currency: str + pool_currency: str + address: str + ext: JSON + #endregion \ No newline at end of file diff --git a/bfxapi/utils/JSONEncoder.py b/bfxapi/utils/JSONEncoder.py index 506bad1..b322795 100644 --- a/bfxapi/utils/JSONEncoder.py +++ b/bfxapi/utils/JSONEncoder.py @@ -2,33 +2,31 @@ import json from decimal import Decimal from datetime import datetime +from types import SimpleNamespace + from typing import Type, List, Dict, Union, Any JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] +def _strip(dictionary: Dict) -> Dict: + return { key: value for key, value in dictionary.items() if value != None} + +def _convert_float_to_str(data: JSON) -> JSON: + if isinstance(data, float): + return format(Decimal(repr(data)), "f") + elif isinstance(data, list): + return [ _convert_float_to_str(sub_data) for sub_data in data ] + elif isinstance(data, dict): + return _strip({ key: _convert_float_to_str(value) for key, value in data.items() }) + else: return data + class JSONEncoder(json.JSONEncoder): def encode(self, obj: JSON) -> str: - def _strip(dictionary: Dict) -> Dict: - return { key: value for key, value in dictionary.items() if value != None} - - def _convert_float_to_str(data: JSON) -> JSON: - if isinstance(data, float): - return format(Decimal(repr(data)), "f") - elif isinstance(data, list): - return [ _convert_float_to_str(sub_data) for sub_data in data ] - elif isinstance(data, dict): - return _strip({ key: _convert_float_to_str(value) for key, value in data.items() }) - else: return data - - data = _convert_float_to_str(obj) - - return json.JSONEncoder.encode(self, data) + return json.JSONEncoder.encode(self, _convert_float_to_str(obj)) def default(self, obj: Any) -> Any: - if isinstance(obj, Decimal): - return format(obj, "f") - - if isinstance(obj, datetime): - return str(obj) + if isinstance(obj, SimpleNamespace): return _convert_float_to_str(vars(obj)) + elif isinstance(obj, Decimal): return format(obj, "f") + elif isinstance(obj, datetime): return str(obj) return json.JSONEncoder.default(self, obj) \ No newline at end of file diff --git a/examples/rest/merchant.py b/examples/rest/merchant.py new file mode 100644 index 0000000..dcacbbf --- /dev/null +++ b/examples/rest/merchant.py @@ -0,0 +1,33 @@ +# python -c "import examples.rest.merchant" + +import os + +from bfxapi.client import Client, Constants +from bfxapi.rest.types import CustomerInfo + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +customer_info: CustomerInfo = CustomerInfo( + nationality="GB", + resid_country="DE", + resid_city="Berlin", + resid_zip_code=1, + resid_street="Timechain", + full_name="Satoshi", + email="satoshi3@bitfinex.com", + tos_accepted=None, + resid_building_no=None +) + +print(bfx.rest.auth.submit_invoice( + amount=1, + currency="USD", + duration=864000, + order_id="order123", + customer_info=customer_info, + pay_currencies=["ETH"], +)) \ No newline at end of file From 56476b96fcdb0436b274972871e0bd62b187d1c4 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 10 Feb 2023 04:33:29 +0100 Subject: [PATCH 152/265] Add new serializers to __serializers__ constant in bfxapi.rest.serializers. --- bfxapi/rest/serializers.py | 1 + bfxapi/rest/types.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 6e80ed7..bcdc7f3 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -13,6 +13,7 @@ __serializers__ = [ "FundingStatistic", "PulseProfile", "PulseMessage", "TradingMarketAveragePrice", "FundingMarketAveragePrice", "FxRate", + "UserInfo", "LoginHistory", "BalanceAvailable", "Order", "Position", "Trade", "FundingTrade", "OrderTrade", "Ledger", "FundingOffer", "FundingCredit", "FundingLoan", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 964b24b..90aa31d 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -224,8 +224,8 @@ class UserInfo(_Type): is_securities_master: int securities_enabled: int allow_disable_ctxswitch: int - ctxtswitch_disabled: int time_last_login: int + ctxtswitch_disabled: int comp_countries: List[str] compl_countries_resid: List[str] is_merchant_enterprise: int From 7e421d380369bebd09db5ff9dade00914e7943d8 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 10 Feb 2023 04:47:03 +0100 Subject: [PATCH 153/265] Fix bug in examples/rest/merchant.py demo. --- examples/rest/merchant.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/rest/merchant.py b/examples/rest/merchant.py index dcacbbf..13d051c 100644 --- a/examples/rest/merchant.py +++ b/examples/rest/merchant.py @@ -19,8 +19,6 @@ customer_info: CustomerInfo = CustomerInfo( resid_street="Timechain", full_name="Satoshi", email="satoshi3@bitfinex.com", - tos_accepted=None, - resid_building_no=None ) print(bfx.rest.auth.submit_invoice( From 9ada3b05a24dded48ddaca831ade15986ad0fc6b Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Sun, 12 Feb 2023 21:24:42 +0100 Subject: [PATCH 154/265] Fix bug in submit_invoice method (bfxapi.rest.endpoints.rest_authenticated_endpoints). --- bfxapi/client.py | 2 ++ bfxapi/labeler.py | 21 ++++++++++++ bfxapi/rest/endpoints/bfx_rest_interface.py | 2 -- .../endpoints/rest_authenticated_endpoints.py | 33 ++++++++++--------- .../rest/endpoints/rest_public_endpoints.py | 12 +++---- bfxapi/rest/types.py | 23 +++++++++---- bfxapi/utils/JSONEncoder.py | 3 +- bfxapi/utils/camel_and_snake_case_adapters.py | 22 +++++++++++++ examples/rest/merchant.py | 21 ++++++------ 9 files changed, 95 insertions(+), 44 deletions(-) create mode 100644 bfxapi/utils/camel_and_snake_case_adapters.py diff --git a/bfxapi/client.py b/bfxapi/client.py index e866235..ec72fb3 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -8,9 +8,11 @@ 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): def __init__( diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 79f5ed4..02b6699 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -4,6 +4,27 @@ from typing import Type, Generic, TypeVar, Iterable, Optional, Dict, List, Tuple T = TypeVar("T", bound="_Type") +def compose(*decorators): + def wrapper(function): + for decorator in reversed(decorators): + function = decorator(function) + return function + + return wrapper + +def partial(cls): + def __init__(self, **kwargs): + for key, value in kwargs.items(): + self.__setattr__(key, value) + + for annotation in self.__annotations__.keys(): + if annotation not in kwargs: + self.__setattr__(annotation, None) + + cls.__init__ = __init__ + + return cls + class _Type(object): """ Base class for any dataclass serializable by the _Serializer generic class. diff --git a/bfxapi/rest/endpoints/bfx_rest_interface.py b/bfxapi/rest/endpoints/bfx_rest_interface.py index 53f87b0..0bddbbc 100644 --- a/bfxapi/rest/endpoints/bfx_rest_interface.py +++ b/bfxapi/rest/endpoints/bfx_rest_interface.py @@ -1,7 +1,5 @@ from typing import Optional - from .rest_public_endpoints import RestPublicEndpoints - from .rest_authenticated_endpoints import RestAuthenticatedEndpoints class BfxRestInterface(object): diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index e4ca99f..2dae262 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -1,14 +1,20 @@ from typing import List, Union, Literal, Optional - -from ..types import * - -from .. import serializers - -from ..enums import Sort, OrderType, FundingOfferType from decimal import Decimal from datetime import datetime -from ..middleware import Middleware +from .. types import * + +from .. import serializers +from .. enums import Sort, OrderType, FundingOfferType +from .. middleware import Middleware + +from ... utils.camel_and_snake_case_adapters import to_snake_case_keys, to_camel_case_keys + +_CustomerInfo = TypedDict("_CustomerInfo", { + "nationality": str, "resid_country": str, "resid_city": str, + "resid_zip_code": str, "resid_street": str, "resid_building_no": str, + "full_name": str, "email": str, "tos_accepted": bool +}) class RestAuthenticatedEndpoints(Middleware): def get_user_info(self) -> UserInfo: @@ -323,19 +329,14 @@ class RestAuthenticatedEndpoints(Middleware): return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] def submit_invoice(self, amount: Union[Decimal, float, str], currency: str, order_id: str, - customer_info: CustomerInfo, pay_currencies: List[str], duration: Optional[int] = None, + customer_info: _CustomerInfo, pay_currencies: List[str], duration: Optional[int] = None, webhook: Optional[str] = None, redirect_url: Optional[str] = None) -> InvoiceSubmission: - data = self._POST("auth/w/ext/pay/invoice/create", body={ + body = to_camel_case_keys({ "amount": amount, "currency": currency, "order_id": order_id, "customer_info": customer_info, "pay_currencies": pay_currencies, "duration": duration, "webhook": webhook, "redirect_url": redirect_url }) - if "customer_info" in data and data["customer_info"] != None: - data["customer_info"] = CustomerInfo(**data["customer_info"]) - - if "invoices" in data and data["invoices"] != None: - for index, invoice in enumerate(data["invoices"]): - data["invoices"][index] = Invoice(**invoice) + data = to_snake_case_keys(self._POST("auth/w/ext/pay/invoice/create", body=body)) - return InvoiceSubmission(**data) \ No newline at end of file + return InvoiceSubmission.parse(data) \ No newline at end of file diff --git a/bfxapi/rest/endpoints/rest_public_endpoints.py b/bfxapi/rest/endpoints/rest_public_endpoints.py index 687a2f5..3810e97 100644 --- a/bfxapi/rest/endpoints/rest_public_endpoints.py +++ b/bfxapi/rest/endpoints/rest_public_endpoints.py @@ -1,13 +1,11 @@ from typing import List, Union, Literal, Optional, Any, cast - -from ..types import * - -from .. import serializers - -from ..enums import Config, Sort from decimal import Decimal -from ..middleware import Middleware +from .. types import * + +from .. import serializers +from .. enums import Config, Sort +from .. middleware import Middleware class RestPublicEndpoints(Middleware): def conf(self, config: Config) -> Any: diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 90aa31d..148a60c 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -2,9 +2,7 @@ from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Literal, from dataclasses import dataclass -from types import SimpleNamespace - -from .. labeler import _Type +from .. labeler import _Type, partial, compose from .. notification import Notification from .. utils.JSONEncoder import JSON @@ -567,7 +565,7 @@ class DerivativePositionCollateralLimits(_Type): #region Type hinting for models which are not serializable -@dataclass +@compose(dataclass, partial) class InvoiceSubmission(_Type): id: str t: int @@ -583,7 +581,19 @@ class InvoiceSubmission(_Type): customer_info: Optional["CustomerInfo"] invoices: List["Invoice"] -class CustomerInfo(SimpleNamespace): + @classmethod + def parse(cls, data: Dict[str, Any]) -> "InvoiceSubmission": + if "customer_info" in data and data["customer_info"] != None: + data["customer_info"] = CustomerInfo(**data["customer_info"]) + + if "invoices" in data and data["invoices"] != None: + for index, invoice in enumerate(data["invoices"]): + data["invoices"][index] = Invoice(**invoice) + + return cls(**data) + +@compose(dataclass, partial) +class CustomerInfo(_Type): nationality: str resid_country: str resid_state: str @@ -595,7 +605,8 @@ class CustomerInfo(SimpleNamespace): email: str tos_accepted: bool -class Invoice(SimpleNamespace): +@compose(dataclass, partial) +class Invoice(_Type): amount: float currency: str pay_currency: str diff --git a/bfxapi/utils/JSONEncoder.py b/bfxapi/utils/JSONEncoder.py index b322795..5124376 100644 --- a/bfxapi/utils/JSONEncoder.py +++ b/bfxapi/utils/JSONEncoder.py @@ -25,8 +25,7 @@ class JSONEncoder(json.JSONEncoder): return json.JSONEncoder.encode(self, _convert_float_to_str(obj)) def default(self, obj: Any) -> Any: - if isinstance(obj, SimpleNamespace): return _convert_float_to_str(vars(obj)) - elif isinstance(obj, Decimal): return format(obj, "f") + if isinstance(obj, Decimal): return format(obj, "f") elif isinstance(obj, datetime): return str(obj) return json.JSONEncoder.default(self, obj) \ No newline at end of file diff --git a/bfxapi/utils/camel_and_snake_case_adapters.py b/bfxapi/utils/camel_and_snake_case_adapters.py new file mode 100644 index 0000000..85ccdf4 --- /dev/null +++ b/bfxapi/utils/camel_and_snake_case_adapters.py @@ -0,0 +1,22 @@ +import re + +from typing import TypeVar, Callable, Dict, Any, cast + +T = TypeVar("T") + +_to_snake_case: Callable[[str], str] = lambda string: re.sub(r"(? T: + if isinstance(data, list): + return cast(T, [ _scheme(sub_data, adapter) for sub_data in data ]) + elif isinstance(data, dict): + return cast(T, { adapter(key): _scheme(value, adapter) for key, value in data.items() }) + else: return data + +def to_snake_case_keys(dictionary: Dict[str, Any]) -> Dict[str, Any]: + return _scheme(dictionary, _to_snake_case) + +def to_camel_case_keys(dictionary: Dict[str, Any]) -> Dict[str, Any]: + return _scheme(dictionary, _to_camel_case) \ No newline at end of file diff --git a/examples/rest/merchant.py b/examples/rest/merchant.py index 13d051c..69a1d8b 100644 --- a/examples/rest/merchant.py +++ b/examples/rest/merchant.py @@ -3,7 +3,6 @@ import os from bfxapi.client import Client, Constants -from bfxapi.rest.types import CustomerInfo bfx = Client( REST_HOST=Constants.REST_HOST, @@ -11,15 +10,15 @@ bfx = Client( API_SECRET=os.getenv("BFX_API_SECRET") ) -customer_info: CustomerInfo = CustomerInfo( - nationality="GB", - resid_country="DE", - resid_city="Berlin", - resid_zip_code=1, - resid_street="Timechain", - full_name="Satoshi", - email="satoshi3@bitfinex.com", -) +customer_info = { + "nationality": "GB", + "resid_country": "DE", + "resid_city": "Berlin", + "resid_zip_code": 1, + "resid_street": "Timechain", + "full_name": "Satoshi", + "email": "satoshi3@bitfinex.com" +} print(bfx.rest.auth.submit_invoice( amount=1, @@ -27,5 +26,5 @@ print(bfx.rest.auth.submit_invoice( duration=864000, order_id="order123", customer_info=customer_info, - pay_currencies=["ETH"], + pay_currencies=["ETH"] )) \ No newline at end of file From 3c377928b277120e85f511db13b8f59cee65bff0 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Sun, 12 Feb 2023 21:41:33 +0100 Subject: [PATCH 155/265] fix errors merchant Co-Authored-By: itsdeka --- bfxapi/rest/endpoints/rest_authenticated_endpoints.py | 11 ++++++++++- bfxapi/rest/types.py | 8 ++++++-- examples/rest/merchant.py | 6 +++++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index 2dae262..a69ea28 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -339,4 +339,13 @@ class RestAuthenticatedEndpoints(Middleware): data = to_snake_case_keys(self._POST("auth/w/ext/pay/invoice/create", body=body)) - return InvoiceSubmission.parse(data) \ No newline at end of file + return InvoiceSubmission.parse(data) + + def get_invoices(self, id: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[InvoiceSubmission]: + return [ InvoiceSubmission.parse(sub_data) for sub_data in self._POST("auth/r/ext/pay/invoices", body={ + "id": id, "start": start, "end": end, + "limit": limit + }) ] + + def get_invoice_count_stats(self, status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"], format: str) -> List[InvoiceCountStats]: + return [ InvoiceCountStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/count", body={ "status": status, "format": format }) ] \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 148a60c..5f98ca8 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -564,7 +564,6 @@ class DerivativePositionCollateralLimits(_Type): #endregion #region Type hinting for models which are not serializable - @compose(dataclass, partial) class InvoiceSubmission(_Type): id: str @@ -590,7 +589,7 @@ class InvoiceSubmission(_Type): for index, invoice in enumerate(data["invoices"]): data["invoices"][index] = Invoice(**invoice) - return cls(**data) + return InvoiceSubmission(**data) @compose(dataclass, partial) class CustomerInfo(_Type): @@ -614,4 +613,9 @@ class Invoice(_Type): address: str ext: JSON +@dataclass +class InvoiceCountStats(_Type): + time: str + count: float + #endregion \ No newline at end of file diff --git a/examples/rest/merchant.py b/examples/rest/merchant.py index 69a1d8b..a113b3f 100644 --- a/examples/rest/merchant.py +++ b/examples/rest/merchant.py @@ -27,4 +27,8 @@ print(bfx.rest.auth.submit_invoice( order_id="order123", customer_info=customer_info, pay_currencies=["ETH"] -)) \ No newline at end of file +)) + +print(bfx.rest.auth.get_invoices()) + +print(bfx.rest.auth.get_invoice_count_stats(status="CREATED", format="Y")) \ No newline at end of file From c41c00259f8154c2ec8f832ec7e03269b0b63083 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Sun, 12 Feb 2023 21:45:01 +0100 Subject: [PATCH 156/265] add endpoints Co-Authored-By: itsdeka --- bfxapi/rest/endpoints/rest_authenticated_endpoints.py | 5 ++++- bfxapi/rest/types.py | 5 +++++ examples/rest/merchant.py | 4 +++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index a69ea28..1493489 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -348,4 +348,7 @@ class RestAuthenticatedEndpoints(Middleware): }) ] def get_invoice_count_stats(self, status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"], format: str) -> List[InvoiceCountStats]: - return [ InvoiceCountStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/count", body={ "status": status, "format": format }) ] \ No newline at end of file + return [ InvoiceCountStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/count", body={ "status": status, "format": format }) ] + + def get_invoice_earning_stats(self, currency: str, format: str) -> List[InvoiceEarningStats]: + return [ InvoiceEarningStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/earning", body={ "currency": currency, "format": format }) ] \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 5f98ca8..3d659b4 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -618,4 +618,9 @@ class InvoiceCountStats(_Type): time: str count: float +@dataclass +class InvoiceEarningStats(_Type): + time: str + count: float + #endregion \ No newline at end of file diff --git a/examples/rest/merchant.py b/examples/rest/merchant.py index a113b3f..9e85d88 100644 --- a/examples/rest/merchant.py +++ b/examples/rest/merchant.py @@ -31,4 +31,6 @@ print(bfx.rest.auth.submit_invoice( print(bfx.rest.auth.get_invoices()) -print(bfx.rest.auth.get_invoice_count_stats(status="CREATED", format="Y")) \ No newline at end of file +print(bfx.rest.auth.get_invoice_count_stats(status="CREATED", format="Y")) + +print(bfx.rest.auth.get_invoice_earning_stats(currency="USD", format="Y")) \ No newline at end of file From 917a4a83673c6a1be9189250fbbe4e53db9e6c79 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Sun, 12 Feb 2023 22:01:12 +0100 Subject: [PATCH 157/265] add Payment Co-Authored-By: itsdeka --- .../endpoints/rest_authenticated_endpoints.py | 26 +++++++++++--- bfxapi/rest/types.py | 35 +++++++++++++++---- examples/rest/merchant.py | 14 ++++++-- 3 files changed, 61 insertions(+), 14 deletions(-) diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index 1493489..9ec3ca0 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -347,8 +347,26 @@ class RestAuthenticatedEndpoints(Middleware): "limit": limit }) ] - def get_invoice_count_stats(self, status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"], format: str) -> List[InvoiceCountStats]: - return [ InvoiceCountStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/count", body={ "status": status, "format": format }) ] + def get_invoice_count_stats(self, status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"], format: str) -> List[InvoiceStats]: + return [ InvoiceStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/count", body={ "status": status, "format": format }) ] - def get_invoice_earning_stats(self, currency: str, format: str) -> List[InvoiceEarningStats]: - return [ InvoiceEarningStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/earning", body={ "currency": currency, "format": format }) ] \ No newline at end of file + def get_invoice_earning_stats(self, currency: str, format: str) -> List[InvoiceStats]: + return [ InvoiceStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/earning", body={ "currency": currency, "format": format }) ] + + def complete_invoice(self, id: str, pay_currency: str, deposit_id: Optional[int] = None, ledger_id: Optional[int] = None) -> InvoiceSubmission: + return InvoiceSubmission.parse(self._POST("auth/w/ext/pay/invoice/complete", body={ + "id": id, "payCcy": pay_currency, "depositId": deposit_id, + "ledgerId": ledger_id + })) + + def expire_invoice(self, id: str) -> InvoiceSubmission: + return InvoiceSubmission.parse(self._POST("auth/w/ext/pay/invoice/expire", body={ "id": id })) + + def get_currency_conversion_list(self) -> List[CurrencyConversion]: + return [ + CurrencyConversion( + base_currency=sub_data["baseCcy"], + convert_currency=sub_data["convertCcy"], + created=sub_data["created"] + ) for sub_data in self._POST("auth/r/ext/pay/settings/convert/list") + ] \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 3d659b4..a2dd647 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -564,20 +564,21 @@ class DerivativePositionCollateralLimits(_Type): #endregion #region Type hinting for models which are not serializable + @compose(dataclass, partial) class InvoiceSubmission(_Type): id: str t: int + merchant_name: str type: Literal["ECOMMERCE", "POS"] duration: int amount: float currency: str order_id: str pay_currencies: List[str] - webhook: str - redirect_url: str status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"] customer_info: Optional["CustomerInfo"] + payment: Optional["Payment"] invoices: List["Invoice"] @classmethod @@ -585,6 +586,9 @@ class InvoiceSubmission(_Type): if "customer_info" in data and data["customer_info"] != None: data["customer_info"] = CustomerInfo(**data["customer_info"]) + if "payment" in data and data["payment"] != None: + data["payment"] = Payment(**data["payment"]) + if "invoices" in data and data["invoices"] != None: for index, invoice in enumerate(data["invoices"]): data["invoices"][index] = Invoice(**invoice) @@ -595,7 +599,6 @@ class InvoiceSubmission(_Type): class CustomerInfo(_Type): nationality: str resid_country: str - resid_state: str resid_city: str resid_zip_code: str resid_street: str @@ -604,6 +607,23 @@ class CustomerInfo(_Type): email: str tos_accepted: bool +@compose(dataclass, partial) +class Payment(_Type): + transaction_id: str + amount: str + currency: str + method: str + status: str + confirmations: int + created: str + updated: str + deposit_id: int + ledger_id: int + force_completed: bool + amount_diff: str + additional_payments: JSON + additional_payment: JSON + @compose(dataclass, partial) class Invoice(_Type): amount: float @@ -614,13 +634,14 @@ class Invoice(_Type): ext: JSON @dataclass -class InvoiceCountStats(_Type): +class InvoiceStats(_Type): time: str count: float @dataclass -class InvoiceEarningStats(_Type): - time: str - count: float +class CurrencyConversion(_Type): + base_currency: str + convert_currency: str + created: int #endregion \ No newline at end of file diff --git a/examples/rest/merchant.py b/examples/rest/merchant.py index 9e85d88..7a4c835 100644 --- a/examples/rest/merchant.py +++ b/examples/rest/merchant.py @@ -20,17 +20,25 @@ customer_info = { "email": "satoshi3@bitfinex.com" } -print(bfx.rest.auth.submit_invoice( +invoice = bfx.rest.auth.submit_invoice( amount=1, currency="USD", duration=864000, order_id="order123", customer_info=customer_info, pay_currencies=["ETH"] -)) +) print(bfx.rest.auth.get_invoices()) print(bfx.rest.auth.get_invoice_count_stats(status="CREATED", format="Y")) -print(bfx.rest.auth.get_invoice_earning_stats(currency="USD", format="Y")) \ No newline at end of file +print(bfx.rest.auth.get_invoice_earning_stats(currency="USD", format="Y")) + +print(bfx.rest.auth.get_currency_conversion_list()) + +print(bfx.rest.auth.complete_invoice( + id=invoice.id, + pay_currency="ETH", + deposit_id=1 +)) \ No newline at end of file From 52bc47597103f721b953c7c9e65981e918f2d05f Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Sun, 12 Feb 2023 22:03:10 +0100 Subject: [PATCH 158/265] add currency endpoints Co-Authored-By: itsdeka --- .../rest/endpoints/rest_authenticated_endpoints.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index 9ec3ca0..0991336 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -369,4 +369,16 @@ class RestAuthenticatedEndpoints(Middleware): convert_currency=sub_data["convertCcy"], created=sub_data["created"] ) for sub_data in self._POST("auth/r/ext/pay/settings/convert/list") - ] \ No newline at end of file + ] + + def add_currency_conversion(self, base_currency: str, convert_currency: str) -> bool: + return bool(self._POST("auth/w/ext/pay/settings/convert/create", body={ + "baseCcy": base_currency, + "convertCcy": convert_currency + })) + + def remove_currency_conversion(self, base_currency: str, convert_currency: str) -> bool: + return bool(self._POST("auth/w/ext/pay/settings/convert/remove", body={ + "baseCcy": base_currency, + "convertCcy": convert_currency + })) \ No newline at end of file From e5b0c1af9c58f46b9d6cea8f6016368aaa999080 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Sun, 12 Feb 2023 22:09:44 +0100 Subject: [PATCH 159/265] Move merchant endpoints in bfxapi.rest.endpoints.rest_merchant_endpoints sub-package. --- bfxapi/rest/endpoints/bfx_rest_interface.py | 8 ++- .../endpoints/rest_authenticated_endpoints.py | 65 +----------------- .../rest/endpoints/rest_merchant_endpoints.py | 68 +++++++++++++++++++ bfxapi/rest/types.py | 2 +- examples/rest/merchant.py | 12 ++-- 5 files changed, 81 insertions(+), 74 deletions(-) create mode 100644 bfxapi/rest/endpoints/rest_merchant_endpoints.py diff --git a/bfxapi/rest/endpoints/bfx_rest_interface.py b/bfxapi/rest/endpoints/bfx_rest_interface.py index 0bddbbc..a2dc6ec 100644 --- a/bfxapi/rest/endpoints/bfx_rest_interface.py +++ b/bfxapi/rest/endpoints/bfx_rest_interface.py @@ -1,11 +1,13 @@ from typing import Optional + from .rest_public_endpoints import RestPublicEndpoints from .rest_authenticated_endpoints import RestAuthenticatedEndpoints +from .rest_merchant_endpoints import RestMerchantEndpoints class BfxRestInterface(object): VERSION = 2 def __init__(self, host: str, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None): - self.public = RestPublicEndpoints(host=host) - - self.auth = RestAuthenticatedEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET) \ No newline at end of file + self.public = RestPublicEndpoints(host=host) + self.auth = RestAuthenticatedEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET) + self.merchant = RestMerchantEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET) \ No newline at end of file diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index 0991336..947032b 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -8,14 +8,6 @@ from .. import serializers from .. enums import Sort, OrderType, FundingOfferType from .. middleware import Middleware -from ... utils.camel_and_snake_case_adapters import to_snake_case_keys, to_camel_case_keys - -_CustomerInfo = TypedDict("_CustomerInfo", { - "nationality": str, "resid_country": str, "resid_city": str, - "resid_zip_code": str, "resid_street": str, "resid_building_no": str, - "full_name": str, "email": str, "tos_accepted": bool -}) - class RestAuthenticatedEndpoints(Middleware): def get_user_info(self) -> UserInfo: return serializers.UserInfo.parse(*self._POST(f"auth/r/info/user")) @@ -326,59 +318,4 @@ class RestAuthenticatedEndpoints(Middleware): "limit": limit } - return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] - - def submit_invoice(self, amount: Union[Decimal, float, str], currency: str, order_id: str, - customer_info: _CustomerInfo, pay_currencies: List[str], duration: Optional[int] = None, - webhook: Optional[str] = None, redirect_url: Optional[str] = None) -> InvoiceSubmission: - body = to_camel_case_keys({ - "amount": amount, "currency": currency, "order_id": order_id, - "customer_info": customer_info, "pay_currencies": pay_currencies, "duration": duration, - "webhook": webhook, "redirect_url": redirect_url - }) - - data = to_snake_case_keys(self._POST("auth/w/ext/pay/invoice/create", body=body)) - - return InvoiceSubmission.parse(data) - - def get_invoices(self, id: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[InvoiceSubmission]: - return [ InvoiceSubmission.parse(sub_data) for sub_data in self._POST("auth/r/ext/pay/invoices", body={ - "id": id, "start": start, "end": end, - "limit": limit - }) ] - - def get_invoice_count_stats(self, status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"], format: str) -> List[InvoiceStats]: - return [ InvoiceStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/count", body={ "status": status, "format": format }) ] - - def get_invoice_earning_stats(self, currency: str, format: str) -> List[InvoiceStats]: - return [ InvoiceStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/earning", body={ "currency": currency, "format": format }) ] - - def complete_invoice(self, id: str, pay_currency: str, deposit_id: Optional[int] = None, ledger_id: Optional[int] = None) -> InvoiceSubmission: - return InvoiceSubmission.parse(self._POST("auth/w/ext/pay/invoice/complete", body={ - "id": id, "payCcy": pay_currency, "depositId": deposit_id, - "ledgerId": ledger_id - })) - - def expire_invoice(self, id: str) -> InvoiceSubmission: - return InvoiceSubmission.parse(self._POST("auth/w/ext/pay/invoice/expire", body={ "id": id })) - - def get_currency_conversion_list(self) -> List[CurrencyConversion]: - return [ - CurrencyConversion( - base_currency=sub_data["baseCcy"], - convert_currency=sub_data["convertCcy"], - created=sub_data["created"] - ) for sub_data in self._POST("auth/r/ext/pay/settings/convert/list") - ] - - def add_currency_conversion(self, base_currency: str, convert_currency: str) -> bool: - return bool(self._POST("auth/w/ext/pay/settings/convert/create", body={ - "baseCcy": base_currency, - "convertCcy": convert_currency - })) - - def remove_currency_conversion(self, base_currency: str, convert_currency: str) -> bool: - return bool(self._POST("auth/w/ext/pay/settings/convert/remove", body={ - "baseCcy": base_currency, - "convertCcy": convert_currency - })) \ No newline at end of file + return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] \ No newline at end of file diff --git a/bfxapi/rest/endpoints/rest_merchant_endpoints.py b/bfxapi/rest/endpoints/rest_merchant_endpoints.py new file mode 100644 index 0000000..3c6f5c7 --- /dev/null +++ b/bfxapi/rest/endpoints/rest_merchant_endpoints.py @@ -0,0 +1,68 @@ +from typing import List, Union, Literal, Optional +from decimal import Decimal + +from .. types import * +from .. middleware import Middleware +from ... utils.camel_and_snake_case_adapters import to_snake_case_keys, to_camel_case_keys + +_CustomerInfo = TypedDict("_CustomerInfo", { + "nationality": str, "resid_country": str, "resid_city": str, + "resid_zip_code": str, "resid_street": str, "resid_building_no": str, + "full_name": str, "email": str, "tos_accepted": bool +}) + +class RestMerchantEndpoints(Middleware): + def submit_invoice(self, amount: Union[Decimal, float, str], currency: str, order_id: str, + customer_info: _CustomerInfo, pay_currencies: List[str], duration: Optional[int] = None, + webhook: Optional[str] = None, redirect_url: Optional[str] = None) -> InvoiceSubmission: + body = to_camel_case_keys({ + "amount": amount, "currency": currency, "order_id": order_id, + "customer_info": customer_info, "pay_currencies": pay_currencies, "duration": duration, + "webhook": webhook, "redirect_url": redirect_url + }) + + data = to_snake_case_keys(self._POST("auth/w/ext/pay/invoice/create", body=body)) + + return InvoiceSubmission.parse(data) + + def get_invoices(self, id: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[InvoiceSubmission]: + return [ InvoiceSubmission.parse(sub_data) for sub_data in self._POST("auth/r/ext/pay/invoices", body={ + "id": id, "start": start, "end": end, + "limit": limit + }) ] + + def get_invoice_count_stats(self, status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"], format: str) -> List[InvoiceStats]: + return [ InvoiceStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/count", body={ "status": status, "format": format }) ] + + def get_invoice_earning_stats(self, currency: str, format: str) -> List[InvoiceStats]: + return [ InvoiceStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/earning", body={ "currency": currency, "format": format }) ] + + def complete_invoice(self, id: str, pay_currency: str, deposit_id: Optional[int] = None, ledger_id: Optional[int] = None) -> InvoiceSubmission: + return InvoiceSubmission.parse(self._POST("auth/w/ext/pay/invoice/complete", body={ + "id": id, "payCcy": pay_currency, "depositId": deposit_id, + "ledgerId": ledger_id + })) + + def expire_invoice(self, id: str) -> InvoiceSubmission: + return InvoiceSubmission.parse(self._POST("auth/w/ext/pay/invoice/expire", body={ "id": id })) + + def get_currency_conversion_list(self) -> List[CurrencyConversion]: + return [ + CurrencyConversion( + base_currency=sub_data["baseCcy"], + convert_currency=sub_data["convertCcy"], + created=sub_data["created"] + ) for sub_data in self._POST("auth/r/ext/pay/settings/convert/list") + ] + + def add_currency_conversion(self, base_currency: str, convert_currency: str) -> bool: + return bool(self._POST("auth/w/ext/pay/settings/convert/create", body={ + "baseCcy": base_currency, + "convertCcy": convert_currency + })) + + def remove_currency_conversion(self, base_currency: str, convert_currency: str) -> bool: + return bool(self._POST("auth/w/ext/pay/settings/convert/remove", body={ + "baseCcy": base_currency, + "convertCcy": convert_currency + })) \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index a2dd647..3bd12e8 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -563,7 +563,7 @@ class DerivativePositionCollateralLimits(_Type): #endregion -#region Type hinting for models which are not serializable +#region Type hinting for Rest Merchant Endpoints @compose(dataclass, partial) class InvoiceSubmission(_Type): diff --git a/examples/rest/merchant.py b/examples/rest/merchant.py index 7a4c835..ec6727b 100644 --- a/examples/rest/merchant.py +++ b/examples/rest/merchant.py @@ -20,7 +20,7 @@ customer_info = { "email": "satoshi3@bitfinex.com" } -invoice = bfx.rest.auth.submit_invoice( +invoice = bfx.rest.merchant.submit_invoice( amount=1, currency="USD", duration=864000, @@ -29,15 +29,15 @@ invoice = bfx.rest.auth.submit_invoice( pay_currencies=["ETH"] ) -print(bfx.rest.auth.get_invoices()) +print(bfx.rest.merchant.get_invoices()) -print(bfx.rest.auth.get_invoice_count_stats(status="CREATED", format="Y")) +print(bfx.rest.merchant.get_invoice_count_stats(status="CREATED", format="Y")) -print(bfx.rest.auth.get_invoice_earning_stats(currency="USD", format="Y")) +print(bfx.rest.merchant.get_invoice_earning_stats(currency="USD", format="Y")) -print(bfx.rest.auth.get_currency_conversion_list()) +print(bfx.rest.merchant.get_currency_conversion_list()) -print(bfx.rest.auth.complete_invoice( +print(bfx.rest.merchant.complete_invoice( id=invoice.id, pay_currency="ETH", deposit_id=1 From f0d14a230f64ef9636c40a5d5aba52c29f47fc49 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Sun, 12 Feb 2023 23:26:44 +0100 Subject: [PATCH 160/265] Fix bug in bfxapi.rest.endpoints.merchant sub-package. --- bfxapi/labeler.py | 9 ++- .../rest/endpoints/rest_merchant_endpoints.py | 10 +-- bfxapi/rest/types.py | 64 ++++++++++--------- bfxapi/utils/camel_and_snake_case_adapters.py | 4 +- 4 files changed, 48 insertions(+), 39 deletions(-) diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 02b6699..213752c 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -14,12 +14,15 @@ def compose(*decorators): def partial(cls): def __init__(self, **kwargs): - for key, value in kwargs.items(): - self.__setattr__(key, value) - for annotation in self.__annotations__.keys(): if annotation not in kwargs: self.__setattr__(annotation, None) + else: self.__setattr__(annotation, kwargs[annotation]) + + kwargs.pop(annotation, None) + + if len(kwargs) != 0: + raise TypeError(f"{cls.__name__}.__init__() got an unexpected keyword argument '{list(kwargs.keys())[0]}'") cls.__init__ = __init__ diff --git a/bfxapi/rest/endpoints/rest_merchant_endpoints.py b/bfxapi/rest/endpoints/rest_merchant_endpoints.py index 3c6f5c7..dd760c5 100644 --- a/bfxapi/rest/endpoints/rest_merchant_endpoints.py +++ b/bfxapi/rest/endpoints/rest_merchant_endpoints.py @@ -26,10 +26,10 @@ class RestMerchantEndpoints(Middleware): return InvoiceSubmission.parse(data) def get_invoices(self, id: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[InvoiceSubmission]: - return [ InvoiceSubmission.parse(sub_data) for sub_data in self._POST("auth/r/ext/pay/invoices", body={ + return [ InvoiceSubmission.parse(sub_data) for sub_data in to_snake_case_keys(self._POST("auth/r/ext/pay/invoices", body={ "id": id, "start": start, "end": end, "limit": limit - }) ] + })) ] def get_invoice_count_stats(self, status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"], format: str) -> List[InvoiceStats]: return [ InvoiceStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/count", body={ "status": status, "format": format }) ] @@ -38,13 +38,13 @@ class RestMerchantEndpoints(Middleware): return [ InvoiceStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/earning", body={ "currency": currency, "format": format }) ] def complete_invoice(self, id: str, pay_currency: str, deposit_id: Optional[int] = None, ledger_id: Optional[int] = None) -> InvoiceSubmission: - return InvoiceSubmission.parse(self._POST("auth/w/ext/pay/invoice/complete", body={ + return InvoiceSubmission.parse(to_snake_case_keys(self._POST("auth/w/ext/pay/invoice/complete", body={ "id": id, "payCcy": pay_currency, "depositId": deposit_id, "ledgerId": ledger_id - })) + }))) def expire_invoice(self, id: str) -> InvoiceSubmission: - return InvoiceSubmission.parse(self._POST("auth/w/ext/pay/invoice/expire", body={ "id": id })) + return InvoiceSubmission.parse(to_snake_case_keys(self._POST("auth/w/ext/pay/invoice/expire", body={ "id": id }))) def get_currency_conversion_list(self) -> List[CurrencyConversion]: return [ diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 3bd12e8..b0c7270 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -569,63 +569,54 @@ class DerivativePositionCollateralLimits(_Type): class InvoiceSubmission(_Type): id: str t: int - merchant_name: str type: Literal["ECOMMERCE", "POS"] duration: int amount: float currency: str order_id: str pay_currencies: List[str] + webhook: Optional[str] + redirect_url: Optional[str] status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"] - customer_info: Optional["CustomerInfo"] - payment: Optional["Payment"] - invoices: List["Invoice"] + customer_info: Optional["_CustomerInfo"] + invoices: List["_Invoice"] + payment: Optional["_Payment"] + additional_payments: Optional[List["_Payment"]] + + merchant_name: str @classmethod def parse(cls, data: Dict[str, Any]) -> "InvoiceSubmission": if "customer_info" in data and data["customer_info"] != None: - data["customer_info"] = CustomerInfo(**data["customer_info"]) + data["customer_info"] = _CustomerInfo(**data["customer_info"]) + + for index, invoice in enumerate(data["invoices"]): + data["invoices"][index] = _Invoice(**invoice) if "payment" in data and data["payment"] != None: - data["payment"] = Payment(**data["payment"]) + data["payment"] = _Payment(**data["payment"]) - if "invoices" in data and data["invoices"] != None: - for index, invoice in enumerate(data["invoices"]): - data["invoices"][index] = Invoice(**invoice) + if "additional_payments" in data and data["additional_payments"] != None: + for index, additional_payment in enumerate(data["additional_payments"]): + data["additional_payments"][index] = _Payment(**additional_payment) return InvoiceSubmission(**data) @compose(dataclass, partial) -class CustomerInfo(_Type): +class _CustomerInfo: nationality: str resid_country: str + resid_state: Optional[str] resid_city: str resid_zip_code: str resid_street: str - resid_building_no: str + resid_building_no: Optional[str] full_name: str email: str tos_accepted: bool @compose(dataclass, partial) -class Payment(_Type): - transaction_id: str - amount: str - currency: str - method: str - status: str - confirmations: int - created: str - updated: str - deposit_id: int - ledger_id: int - force_completed: bool - amount_diff: str - additional_payments: JSON - additional_payment: JSON - -@compose(dataclass, partial) -class Invoice(_Type): +class _Invoice: amount: float currency: str pay_currency: str @@ -633,6 +624,21 @@ class Invoice(_Type): address: str ext: JSON +@compose(dataclass, partial) +class _Payment: + txid: str + amount: float + currency: str + method: str + status: Literal["CREATED", "COMPLETED", "PROCESSING"] + confirmations: int + created_at: str + updated_at: str + deposit_id: Optional[int] + ledger_id: Optional[int] + force_completed: Optional[bool] + amount_diff: Optional[str] + @dataclass class InvoiceStats(_Type): time: str diff --git a/bfxapi/utils/camel_and_snake_case_adapters.py b/bfxapi/utils/camel_and_snake_case_adapters.py index 85ccdf4..7255940 100644 --- a/bfxapi/utils/camel_and_snake_case_adapters.py +++ b/bfxapi/utils/camel_and_snake_case_adapters.py @@ -15,8 +15,8 @@ def _scheme(data: T, adapter: Callable[[str], str]) -> T: return cast(T, { adapter(key): _scheme(value, adapter) for key, value in data.items() }) else: return data -def to_snake_case_keys(dictionary: Dict[str, Any]) -> Dict[str, Any]: +def to_snake_case_keys(dictionary: T) -> T: return _scheme(dictionary, _to_snake_case) -def to_camel_case_keys(dictionary: Dict[str, Any]) -> Dict[str, Any]: +def to_camel_case_keys(dictionary: T) -> T: return _scheme(dictionary, _to_camel_case) \ No newline at end of file From 39f317ba4001095d23e53e06188de28d0718971f Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 13 Feb 2023 16:22:39 +0100 Subject: [PATCH 161/265] Rename bfxapi.utils.camel_and_snake_case_adapters to bfxapi.utils.camel_and_snake_case_helpers. --- bfxapi/rest/endpoints/rest_merchant_endpoints.py | 2 +- ...d_snake_case_adapters.py => camel_and_snake_case_helpers.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename bfxapi/utils/{camel_and_snake_case_adapters.py => camel_and_snake_case_helpers.py} (100%) diff --git a/bfxapi/rest/endpoints/rest_merchant_endpoints.py b/bfxapi/rest/endpoints/rest_merchant_endpoints.py index dd760c5..8290a2c 100644 --- a/bfxapi/rest/endpoints/rest_merchant_endpoints.py +++ b/bfxapi/rest/endpoints/rest_merchant_endpoints.py @@ -3,7 +3,7 @@ from decimal import Decimal from .. types import * 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", { "nationality": str, "resid_country": str, "resid_city": str, diff --git a/bfxapi/utils/camel_and_snake_case_adapters.py b/bfxapi/utils/camel_and_snake_case_helpers.py similarity index 100% rename from bfxapi/utils/camel_and_snake_case_adapters.py rename to bfxapi/utils/camel_and_snake_case_helpers.py From 821541134a91c23676820ba5c88a173d1c1937c1 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 13 Feb 2023 17:29:35 +0100 Subject: [PATCH 162/265] Fix bug and refactor code in bfxapi.rest.types sub-package. --- bfxapi/rest/types.py | 85 ++++++++++++++++++++++---------------------- 1 file changed, 42 insertions(+), 43 deletions(-) diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index b0c7270..5d6b22c 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -578,66 +578,65 @@ class InvoiceSubmission(_Type): webhook: Optional[str] redirect_url: Optional[str] status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"] - customer_info: Optional["_CustomerInfo"] - invoices: List["_Invoice"] - payment: Optional["_Payment"] - additional_payments: Optional[List["_Payment"]] - + customer_info: Optional["CustomerInfo"] + invoices: List["Invoice"] + payment: Optional["Payment"] + additional_payments: Optional[List["Payment"]] merchant_name: str @classmethod def parse(cls, data: Dict[str, Any]) -> "InvoiceSubmission": if "customer_info" in data and data["customer_info"] != None: - data["customer_info"] = _CustomerInfo(**data["customer_info"]) + data["customer_info"] = InvoiceSubmission.CustomerInfo(**data["customer_info"]) 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: - data["payment"] = _Payment(**data["payment"]) + data["payment"] = InvoiceSubmission.Payment(**data["payment"]) if "additional_payments" in data and data["additional_payments"] != None: for index, additional_payment in enumerate(data["additional_payments"]): - data["additional_payments"][index] = _Payment(**additional_payment) + data["additional_payments"][index] = InvoiceSubmission.Payment(**additional_payment) return InvoiceSubmission(**data) -@compose(dataclass, partial) -class _CustomerInfo: - nationality: str - resid_country: str - resid_state: Optional[str] - resid_city: str - resid_zip_code: str - resid_street: str - resid_building_no: Optional[str] - full_name: str - email: str - tos_accepted: bool + @compose(dataclass, partial) + class CustomerInfo: + nationality: str + resid_country: str + resid_state: Optional[str] + resid_city: str + resid_zip_code: str + resid_street: str + resid_building_no: Optional[str] + full_name: str + email: str + tos_accepted: bool -@compose(dataclass, partial) -class _Invoice: - amount: float - currency: str - pay_currency: str - pool_currency: str - address: str - ext: JSON + @compose(dataclass, partial) + class Invoice: + amount: float + currency: str + pay_currency: str + pool_currency: str + address: str + ext: JSON -@compose(dataclass, partial) -class _Payment: - txid: str - amount: float - currency: str - method: str - status: Literal["CREATED", "COMPLETED", "PROCESSING"] - confirmations: int - created_at: str - updated_at: str - deposit_id: Optional[int] - ledger_id: Optional[int] - force_completed: Optional[bool] - amount_diff: Optional[str] + @compose(dataclass, partial) + class Payment: + txid: str + amount: float + currency: str + method: str + status: Literal["CREATED", "COMPLETED", "PROCESSING"] + confirmations: int + created_at: str + updated_at: str + deposit_id: Optional[int] + ledger_id: Optional[int] + force_completed: Optional[bool] + amount_diff: Optional[str] @dataclass class InvoiceStats(_Type): From 6a7577f98b30525603971492c51c975741e67c55 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 13 Feb 2023 17:32:46 +0100 Subject: [PATCH 163/265] Remove Optional typing in bfxapi.rest.types. --- bfxapi/rest/types.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 5d6b22c..12fdb29 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -575,13 +575,13 @@ class InvoiceSubmission(_Type): currency: str order_id: str pay_currencies: List[str] - webhook: Optional[str] - redirect_url: Optional[str] + webhook: str + redirect_url: str status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"] - customer_info: Optional["CustomerInfo"] + customer_info: "CustomerInfo" invoices: List["Invoice"] - payment: Optional["Payment"] - additional_payments: Optional[List["Payment"]] + payment: "Payment" + additional_payments: List["Payment"] merchant_name: str @classmethod @@ -605,11 +605,11 @@ class InvoiceSubmission(_Type): class CustomerInfo: nationality: str resid_country: str - resid_state: Optional[str] + resid_state: str resid_city: str resid_zip_code: str resid_street: str - resid_building_no: Optional[str] + resid_building_no: str full_name: str email: str tos_accepted: bool @@ -633,10 +633,10 @@ class InvoiceSubmission(_Type): confirmations: int created_at: str updated_at: str - deposit_id: Optional[int] - ledger_id: Optional[int] - force_completed: Optional[bool] - amount_diff: Optional[str] + deposit_id: int + ledger_id: int + force_completed: bool + amount_diff: str @dataclass class InvoiceStats(_Type): From f0f150cec2f79d7c40f20cf00a30a364378a27e0 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 13 Feb 2023 19:09:38 +0100 Subject: [PATCH 164/265] Split websocket package in multiple sub-package. Split handlers.py in public_channels_handler.py and authenticated_channels_handler.py. Rename files attaining to new conventions. --- bfxapi/rest/__init__.py | 3 +- bfxapi/rest/endpoints/__init__.py | 2 + bfxapi/websocket/__init__.py | 4 +- bfxapi/websocket/client/__init__.py | 5 ++ .../bfx_websocket_bucket.py} | 12 +-- .../bfx_websocket_client.py} | 20 ++--- .../bfx_websocket_inputs.py} | 6 +- bfxapi/websocket/handlers/__init__.py | 3 + .../authenticated_channels_handler.py | 71 ++++++++++++++++++ .../public_channels_handler.py} | 74 +------------------ bfxapi/websocket/subscriptions.py | 8 +- 11 files changed, 117 insertions(+), 91 deletions(-) create mode 100644 bfxapi/websocket/client/__init__.py rename bfxapi/websocket/{_BfxWebsocketBucket.py => client/bfx_websocket_bucket.py} (87%) rename bfxapi/websocket/{BfxWebsocketClient.py => client/bfx_websocket_client.py} (89%) rename bfxapi/websocket/{_BfxWebsocketInputs.py => client/bfx_websocket_inputs.py} (96%) create mode 100644 bfxapi/websocket/handlers/__init__.py create mode 100644 bfxapi/websocket/handlers/authenticated_channels_handler.py rename bfxapi/websocket/{handlers.py => handlers/public_channels_handler.py} (60%) diff --git a/bfxapi/rest/__init__.py b/bfxapi/rest/__init__.py index 7ee9fed..71e3b54 100644 --- a/bfxapi/rest/__init__.py +++ b/bfxapi/rest/__init__.py @@ -1,3 +1,4 @@ -from .endpoints import BfxRestInterface, RestPublicEndpoints, RestAuthenticatedEndpoints +from .endpoints import BfxRestInterface, RestPublicEndpoints, RestAuthenticatedEndpoints, \ + RestMerchantEndpoints NAME = "rest" \ No newline at end of file diff --git a/bfxapi/rest/endpoints/__init__.py b/bfxapi/rest/endpoints/__init__.py index 24a005d..e35d6fb 100644 --- a/bfxapi/rest/endpoints/__init__.py +++ b/bfxapi/rest/endpoints/__init__.py @@ -1,5 +1,7 @@ from .bfx_rest_interface import BfxRestInterface + from .rest_public_endpoints import RestPublicEndpoints from .rest_authenticated_endpoints import RestAuthenticatedEndpoints +from .rest_merchant_endpoints import RestMerchantEndpoints NAME = "endpoints" \ No newline at end of file diff --git a/bfxapi/websocket/__init__.py b/bfxapi/websocket/__init__.py index e24f778..1287433 100644 --- a/bfxapi/websocket/__init__.py +++ b/bfxapi/websocket/__init__.py @@ -1 +1,3 @@ -from .BfxWebsocketClient import BfxWebsocketClient \ No newline at end of file +from .client import BfxWebsocketClient, BfxWebsocketBucket, BfxWebsocketInputs + +NAME = "websocket" \ No newline at end of file diff --git a/bfxapi/websocket/client/__init__.py b/bfxapi/websocket/client/__init__.py new file mode 100644 index 0000000..50057cb --- /dev/null +++ b/bfxapi/websocket/client/__init__.py @@ -0,0 +1,5 @@ +from .bfx_websocket_client import BfxWebsocketClient +from .bfx_websocket_bucket import BfxWebsocketBucket +from .bfx_websocket_inputs import BfxWebsocketInputs + +NAME = "client" \ No newline at end of file diff --git a/bfxapi/websocket/_BfxWebsocketBucket.py b/bfxapi/websocket/client/bfx_websocket_bucket.py similarity index 87% rename from bfxapi/websocket/_BfxWebsocketBucket.py rename to bfxapi/websocket/client/bfx_websocket_bucket.py index 2cfe48c..550581d 100644 --- a/bfxapi/websocket/_BfxWebsocketBucket.py +++ b/bfxapi/websocket/client/bfx_websocket_bucket.py @@ -2,9 +2,9 @@ import json, uuid, websockets 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" @@ -19,7 +19,7 @@ def _require_websocket_connection(function: F) -> F: return cast(F, wrapper) -class _BfxWebsocketBucket(object): +class BfxWebsocketBucket(object): VERSION = 2 MAXIMUM_SUBSCRIPTIONS_AMOUNT = 25 @@ -42,8 +42,8 @@ class _BfxWebsocketBucket(object): message = json.loads(message) if isinstance(message, dict) and message["event"] == "info" and "version" in message: - 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']}).") + 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.subscriptions[chanId] = message @@ -60,7 +60,7 @@ class _BfxWebsocketBucket(object): @_require_websocket_connection 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.") subscription = { diff --git a/bfxapi/websocket/BfxWebsocketClient.py b/bfxapi/websocket/client/bfx_websocket_client.py similarity index 89% rename from bfxapi/websocket/BfxWebsocketClient.py rename to bfxapi/websocket/client/bfx_websocket_client.py index 98b5b75..8dccf79 100644 --- a/bfxapi/websocket/BfxWebsocketClient.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -4,15 +4,17 @@ from typing import Literal, TypeVar, Callable, cast from pyee.asyncio import AsyncIOEventEmitter -from ._BfxWebsocketBucket import _HEARTBEAT, F, _require_websocket_connection, _BfxWebsocketBucket +from .bfx_websocket_bucket import _HEARTBEAT, F, _require_websocket_connection, BfxWebsocketBucket -from ._BfxWebsocketInputs import _BfxWebsocketInputs -from .handlers import Channels, PublicChannelsHandler, AuthenticatedChannelsHandler -from .exceptions import WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported +from .bfx_websocket_inputs import BfxWebsocketInputs +from ..handlers import PublicChannelsHandler, AuthenticatedChannelsHandler +from ..exceptions import WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported -from ..utils.JSONEncoder import JSONEncoder +from ..enums import Channels -from ..utils.logger import Formatter, CustomLogger +from ...utils.JSONEncoder import JSONEncoder + +from ...utils.logger import Formatter, CustomLogger def _require_websocket_authentication(function: F) -> F: async def wrapper(self, *args, **kwargs): @@ -24,7 +26,7 @@ def _require_websocket_authentication(function: F) -> F: return cast(F, wrapper) class BfxWebsocketClient(object): - VERSION = _BfxWebsocketBucket.VERSION + VERSION = BfxWebsocketBucket.VERSION MAXIMUM_BUCKETS_AMOUNT = 20 @@ -46,9 +48,9 @@ class BfxWebsocketClient(object): 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.buckets = [ BfxWebsocketBucket(self.host, self.event_emitter, self.__bucket_open_signal) for _ in range(buckets) ] - self.inputs = _BfxWebsocketInputs(self.__handle_websocket_input) + self.inputs = BfxWebsocketInputs(self.__handle_websocket_input) self.logger = CustomLogger("BfxWebsocketClient", logLevel=log_level) diff --git a/bfxapi/websocket/_BfxWebsocketInputs.py b/bfxapi/websocket/client/bfx_websocket_inputs.py similarity index 96% rename from bfxapi/websocket/_BfxWebsocketInputs.py rename to bfxapi/websocket/client/bfx_websocket_inputs.py index 0d9ee0b..48e3137 100644 --- a/bfxapi/websocket/_BfxWebsocketInputs.py +++ b/bfxapi/websocket/client/bfx_websocket_inputs.py @@ -2,10 +2,10 @@ from decimal import Decimal from datetime import datetime from typing import Union, Optional, List, Tuple -from .types import JSON -from .enums import OrderType, FundingOfferType +from ..types import JSON +from ..enums import OrderType, FundingOfferType -class _BfxWebsocketInputs(object): +class BfxWebsocketInputs(object): def __init__(self, __handle_websocket_input): self.__handle_websocket_input = __handle_websocket_input diff --git a/bfxapi/websocket/handlers/__init__.py b/bfxapi/websocket/handlers/__init__.py new file mode 100644 index 0000000..4fe650a --- /dev/null +++ b/bfxapi/websocket/handlers/__init__.py @@ -0,0 +1,3 @@ +from .public_channels_handler import PublicChannelsHandler +from .authenticated_channels_handler import AuthenticatedChannelsHandler +NAME = "handlers" \ No newline at end of file diff --git a/bfxapi/websocket/handlers/authenticated_channels_handler.py b/bfxapi/websocket/handlers/authenticated_channels_handler.py new file mode 100644 index 0000000..2205012 --- /dev/null +++ b/bfxapi/websocket/handlers/authenticated_channels_handler.py @@ -0,0 +1,71 @@ +from typing import List + +from ..types import * + +from .. import serializers + +from ..exceptions import BfxWebsocketException + +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)) \ No newline at end of file diff --git a/bfxapi/websocket/handlers.py b/bfxapi/websocket/handlers/public_channels_handler.py similarity index 60% rename from bfxapi/websocket/handlers.py rename to bfxapi/websocket/handlers/public_channels_handler.py index 686501b..154e677 100644 --- a/bfxapi/websocket/handlers.py +++ b/bfxapi/websocket/handlers/public_channels_handler.py @@ -1,10 +1,8 @@ -from typing import List +from ..types import * -from .types import * +from .. import serializers -from . import serializers -from .enums import Channels -from .exceptions import BfxWebsocketException +from ..enums import Channels class PublicChannelsHandler(object): EVENTS = [ @@ -117,68 +115,4 @@ class PublicChannelsHandler(object): "derivatives_status_update", subscription, serializers.DerivativesStatus.parse(*stream[0]) - ) - -class AuthenticatedChannelsHandler(object): - __abbreviations = { - "os": "order_snapshot", "on": "order_new", "ou": "order_update", "oc": "order_cancel", - "ps": "position_snapshot", "pn": "position_new", "pu": "position_update", "pc": "position_close", - "te": "trade_executed", "tu": "trade_execution_update", - "fos": "funding_offer_snapshot", "fon": "funding_offer_new", "fou": "funding_offer_update", "foc": "funding_offer_cancel", - "fcs": "funding_credit_snapshot", "fcn": "funding_credit_new", "fcu": "funding_credit_update", "fcc": "funding_credit_close", - "fls": "funding_loan_snapshot", "fln": "funding_loan_new", "flu": "funding_loan_update", "flc": "funding_loan_close", - "ws": "wallet_snapshot", "wu": "wallet_update", - "bu": "balance_update", - } - - __serializers = { - ("os", "on", "ou", "oc",): serializers.Order, - ("ps", "pn", "pu", "pc",): serializers.Position, - ("te", "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)) \ No newline at end of file + ) \ No newline at end of file diff --git a/bfxapi/websocket/subscriptions.py b/bfxapi/websocket/subscriptions.py index e22bb5e..5e2d692 100644 --- a/bfxapi/websocket/subscriptions.py +++ b/bfxapi/websocket/subscriptions.py @@ -1,6 +1,8 @@ -from typing import TypedDict, Optional +from typing import TypedDict, Union, Optional __all__ = [ + "Subscription", + "Ticker", "Trades", "Book", @@ -8,6 +10,10 @@ __all__ = [ "Status" ] +_Header = TypedDict("_Header", { "event": str, "channel": str, "subId": str }) + +Subscription = Union["Ticker", "Trades", "Book", "Candles", "Status"] + class Ticker(TypedDict): chanId: int; symbol: str pair: Optional[str] From 17c95027334c7c1d5efe6a0108cbd34c1690b563 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 14 Feb 2023 16:29:50 +0100 Subject: [PATCH 165/265] Apply fixes and refactoring to the bfxapi.handlers sub-package. --- bfxapi/websocket/exceptions.py | 7 +++++ .../authenticated_channels_handler.py | 22 +++++++-------- .../handlers/public_channels_handler.py | 27 ++++++++++--------- 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/bfxapi/websocket/exceptions.py b/bfxapi/websocket/exceptions.py index 5691af8..40a6a1e 100644 --- a/bfxapi/websocket/exceptions.py +++ b/bfxapi/websocket/exceptions.py @@ -58,4 +58,11 @@ class InvalidAuthenticationCredentials(BfxWebsocketException): This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication. """ + pass + +class HandlerNotFound(BfxWebsocketException): + """ + This error indicates that a handler was not found for an incoming message. + """ + pass \ No newline at end of file diff --git a/bfxapi/websocket/handlers/authenticated_channels_handler.py b/bfxapi/websocket/handlers/authenticated_channels_handler.py index 2205012..2dbd83f 100644 --- a/bfxapi/websocket/handlers/authenticated_channels_handler.py +++ b/bfxapi/websocket/handlers/authenticated_channels_handler.py @@ -1,10 +1,8 @@ -from typing import List - -from ..types import * - from .. import serializers -from ..exceptions import BfxWebsocketException +from .. types import * + +from .. exceptions import HandlerNotFound class AuthenticatedChannelsHandler(object): __abbreviations = { @@ -37,7 +35,7 @@ class AuthenticatedChannelsHandler(object): *list(__abbreviations.values()) ] - def __init__(self, event_emitter, strict = False): + def __init__(self, event_emitter, strict = True): self.event_emitter, self.strict = event_emitter, strict def handle(self, type, stream): @@ -52,20 +50,20 @@ class AuthenticatedChannelsHandler(object): 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.") + + 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[Order](serializer=serializers.Order) + 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[List[Order]](serializer=serializers.Order, iterate=True) + 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[FundingOffer](serializer=serializers.FundingOffer) + type, serializer = f"{stream[1]}-notification", serializers._Notification(serializer=serializers.FundingOffer) return self.event_emitter.emit(type, serializer.parse(*stream)) \ No newline at end of file diff --git a/bfxapi/websocket/handlers/public_channels_handler.py b/bfxapi/websocket/handlers/public_channels_handler.py index 154e677..d63ea1f 100644 --- a/bfxapi/websocket/handlers/public_channels_handler.py +++ b/bfxapi/websocket/handlers/public_channels_handler.py @@ -1,8 +1,8 @@ -from ..types import * - from .. import serializers -from ..enums import Channels +from .. types import * + +from .. exceptions import HandlerNotFound class PublicChannelsHandler(object): EVENTS = [ @@ -13,23 +13,26 @@ class PublicChannelsHandler(object): "derivatives_status_update", ] - def __init__(self, event_emitter): - self.event_emitter = event_emitter + def __init__(self, event_emitter, strict = True): + self.event_emitter, self.strict = event_emitter, strict self.__handlers = { - Channels.TICKER: self.__ticker_channel_handler, - Channels.TRADES: self.__trades_channel_handler, - Channels.BOOK: self.__book_channel_handler, - Channels.CANDLES: self.__candles_channel_handler, - Channels.STATUS: self.__status_channel_handler + "ticker": self.__ticker_channel_handler, + "trades": self.__trades_channel_handler, + "book": self.__book_channel_handler, + "candles": self.__candles_channel_handler, + "status": self.__status_channel_handler } def handle(self, subscription, *stream): _clear = lambda dictionary, *args: { key: value for key, value in dictionary.items() if key not in args } - if channel := subscription["channel"] 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) + if self.strict: + raise HandlerNotFound(f"No handler found for channel <{subscription['channel']}>.") + def __ticker_channel_handler(self, subscription, *stream): if subscription["symbol"].startswith("t"): return self.event_emitter.emit( @@ -46,7 +49,7 @@ class PublicChannelsHandler(object): ) 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"): return self.event_emitter.emit( { "te": "t_trade_executed", "tu": "t_trade_execution_update" }[type], From 49517f9709c708c52ddd29af42c7627bf55e28af Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 14 Feb 2023 17:03:16 +0100 Subject: [PATCH 166/265] Small fix in package import statements. --- bfxapi/rest/endpoints/rest_authenticated_endpoints.py | 2 +- bfxapi/rest/endpoints/rest_merchant_endpoints.py | 3 ++- bfxapi/rest/types.py | 2 +- bfxapi/utils/JSONEncoder.py | 2 -- bfxapi/websocket/client/bfx_websocket_inputs.py | 4 ++-- bfxapi/websocket/enums.py | 2 +- bfxapi/websocket/types.py | 8 ++++---- 7 files changed, 11 insertions(+), 12 deletions(-) diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index 947032b..7b4e11e 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -1,4 +1,4 @@ -from typing import List, Union, Literal, Optional +from typing import List, Tuple, Union, Literal, Optional from decimal import Decimal from datetime import datetime diff --git a/bfxapi/rest/endpoints/rest_merchant_endpoints.py b/bfxapi/rest/endpoints/rest_merchant_endpoints.py index 8290a2c..0c80110 100644 --- a/bfxapi/rest/endpoints/rest_merchant_endpoints.py +++ b/bfxapi/rest/endpoints/rest_merchant_endpoints.py @@ -1,4 +1,5 @@ -from typing import List, Union, Literal, Optional +from typing import TypedDict, List, Union, Literal, Optional + from decimal import Decimal from .. types import * diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 12fdb29..e1c39af 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -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 diff --git a/bfxapi/utils/JSONEncoder.py b/bfxapi/utils/JSONEncoder.py index 5124376..edaba00 100644 --- a/bfxapi/utils/JSONEncoder.py +++ b/bfxapi/utils/JSONEncoder.py @@ -2,8 +2,6 @@ import json from decimal import Decimal from datetime import datetime -from types import SimpleNamespace - from typing import Type, List, Dict, Union, Any JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] diff --git a/bfxapi/websocket/client/bfx_websocket_inputs.py b/bfxapi/websocket/client/bfx_websocket_inputs.py index 48e3137..141f817 100644 --- a/bfxapi/websocket/client/bfx_websocket_inputs.py +++ b/bfxapi/websocket/client/bfx_websocket_inputs.py @@ -2,8 +2,8 @@ from decimal import Decimal from datetime import datetime 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): def __init__(self, __handle_websocket_input): diff --git a/bfxapi/websocket/enums.py b/bfxapi/websocket/enums.py index 8f06f62..b9530db 100644 --- a/bfxapi/websocket/enums.py +++ b/bfxapi/websocket/enums.py @@ -1,4 +1,4 @@ -from ..enums import * +from .. enums import * class Channels(str, Enum): TICKER = "ticker" diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py index 0ffa870..063836a 100644 --- a/bfxapi/websocket/types.py +++ b/bfxapi/websocket/types.py @@ -1,10 +1,10 @@ -from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any +from typing import Optional from dataclasses import dataclass -from ..labeler import _Type -from ..notification import Notification -from ..utils.JSONEncoder import JSON +from .. labeler import _Type +from .. notification import Notification +from .. utils.JSONEncoder import JSON #region Type hinting for Websocket Public Channels From 99f58ddb0410e4425668bdae5f944c85f056e32b Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 14 Feb 2023 18:49:45 +0100 Subject: [PATCH 167/265] Add new packages to setup.py. Add new feature in .on and .once methods in bfxapi.websocket.client.bfx_websocket_client. Fix small typo in __init__.py. --- .../websocket/client/bfx_websocket_client.py | 64 +++++++++++-------- bfxapi/websocket/handlers/__init__.py | 1 + setup.py | 2 +- 3 files changed, 38 insertions(+), 29 deletions(-) diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 8dccf79..2439bc0 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -1,6 +1,6 @@ -import traceback, json, asyncio, hmac, hashlib, time, uuid, websockets +import traceback, json, asyncio, hmac, hashlib, time, websockets -from typing import Literal, TypeVar, Callable, cast +from typing import cast from pyee.asyncio import AsyncIOEventEmitter @@ -10,8 +10,6 @@ from .bfx_websocket_inputs import BfxWebsocketInputs from ..handlers import PublicChannelsHandler, AuthenticatedChannelsHandler from ..exceptions import WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported -from ..enums import Channels - from ...utils.JSONEncoder import JSONEncoder from ...utils.logger import Formatter, CustomLogger @@ -36,28 +34,28 @@ class BfxWebsocketClient(object): *AuthenticatedChannelsHandler.EVENTS ] - def __init__(self, host, buckets=5, log_level = "WARNING", API_KEY=None, API_SECRET=None, filter=None): + def __init__(self, host, API_KEY = None, API_SECRET = None, filter = None, buckets = 5, log_level = "WARNING"): 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) + 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]) + ) + 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>.") + self.buckets = [ BfxWebsocketBucket(self.host, self.event_emitter, self.__bucket_open_signal) for _ in range(buckets) ] + + self.inputs = BfxWebsocketInputs(self.__handle_websocket_input) + def run(self): return asyncio.run(self.start()) @@ -136,24 +134,34 @@ class BfxWebsocketClient(object): 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") + 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: - return self.event_emitter.on(event, callback) + for event in events: + self.event_emitter.on(event, callback) - def handler(function): - self.event_emitter.on(event, function) - return handler + if callback == None: + def handler(function): + for event in events: + self.event_emitter.on(event, function) - 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") + 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: - return self.event_emitter.once(event, callback) + for event in events: + self.event_emitter.once(event, callback) - def handler(function): - self.event_emitter.once(event, function) - return handler \ No newline at end of file + if callback == None: + def handler(function): + for event in events: + self.event_emitter.once(event, function) + + return handler \ No newline at end of file diff --git a/bfxapi/websocket/handlers/__init__.py b/bfxapi/websocket/handlers/__init__.py index 4fe650a..02e9c81 100644 --- a/bfxapi/websocket/handlers/__init__.py +++ b/bfxapi/websocket/handlers/__init__.py @@ -1,3 +1,4 @@ from .public_channels_handler import PublicChannelsHandler from .authenticated_channels_handler import AuthenticatedChannelsHandler + NAME = "handlers" \ No newline at end of file diff --git a/setup.py b/setup.py index 54db508..ce3ae3e 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( version="3.0.0", packages=[ "bfxapi", "bfxapi.utils", - "bfxapi.websocket", + "bfxapi.websocket", "bfxapi.websocket.client", "bfxapi.websocket.handlers", "bfxapi.rest", "bfxapi.rest.endpoints", "bfxapi.rest.middleware", ], url="https://github.com/bitfinexcom/bitfinex-api-py", From fa9bdfc33351f8221ae41999da9d37db8b3d6a44 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 15 Feb 2023 21:48:34 +0100 Subject: [PATCH 168/265] Rewrite reconnection system with numerous fixes. --- .../websocket/client/bfx_websocket_bucket.py | 31 +++++--- .../websocket/client/bfx_websocket_client.py | 79 +++++++++++++------ .../websocket/client/bfx_websocket_inputs.py | 18 ++--- bfxapi/websocket/enums.py | 2 +- .../handlers/public_channels_handler.py | 2 +- bfxapi/websocket/subscriptions.py | 14 ++-- examples/websocket/order_book.py | 4 +- examples/websocket/raw_order_book.py | 4 +- examples/websocket/ticker.py | 4 +- 9 files changed, 98 insertions(+), 60 deletions(-) diff --git a/bfxapi/websocket/client/bfx_websocket_bucket.py b/bfxapi/websocket/client/bfx_websocket_bucket.py index 550581d..15caffc 100644 --- a/bfxapi/websocket/client/bfx_websocket_bucket.py +++ b/bfxapi/websocket/client/bfx_websocket_bucket.py @@ -24,27 +24,35 @@ class BfxWebsocketBucket(object): MAXIMUM_SUBSCRIPTIONS_AMOUNT = 25 - def __init__(self, host, event_emitter, __bucket_open_signal): - self.host, self.event_emitter, self.__bucket_open_signal = host, event_emitter, __bucket_open_signal + def __init__(self, host, event_emitter, on_open_event): + self.host, self.event_emitter, self.on_open_event = host, event_emitter, on_open_event self.websocket, self.subscriptions, self.pendings = None, dict(), list() self.handler = PublicChannelsHandler(event_emitter=self.event_emitter) async def _connect(self, index): + reconnection = False + async for websocket in websockets.connect(self.host): self.websocket = websocket - self.__bucket_open_signal(index) + 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() + + self.on_open_event.set() try: async for message in websocket: message = json.loads(message) - if isinstance(message, dict) and message["event"] == "info" and "version" in message: - 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"]): + if isinstance(message, dict) and message["event"] == "subscribed" and (chanId := message["chanId"]): self.pendings = [ pending for pending in self.pendings if pending["subId"] != message["subId"] ] self.subscriptions[chanId] = message self.event_emitter.emit("subscribed", message) @@ -55,8 +63,9 @@ class BfxWebsocketBucket(object): self.event_emitter.emit("wss-error", message["code"], message["msg"]) elif isinstance(message, list) and (chanId := message[0]) and message[1] != _HEARTBEAT: self.handler.handle(self.subscriptions[chanId], *message[1:]) - except websockets.ConnectionClosedError: continue - finally: await self.websocket.wait_closed(); break + except websockets.ConnectionClosedError: self.on_open_event.clear(); reconnection = True; continue + + await self.websocket.wait_closed(); break @_require_websocket_connection async def _subscribe(self, channel, subId=None, **kwargs): @@ -64,11 +73,11 @@ class BfxWebsocketBucket(object): raise TooManySubscriptions("The client has reached the maximum number of subscriptions.") subscription = { + **kwargs, + "event": "subscribe", "channel": channel, "subId": subId or str(uuid.uuid4()), - - **kwargs } self.pendings.append(subscription) diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 2439bc0..8f2a54d 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -2,13 +2,17 @@ import traceback, json, asyncio, hmac, hashlib, time, websockets 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 +from ..exceptions import WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, OutdatedClientVersion from ...utils.JSONEncoder import JSONEncoder @@ -16,7 +20,7 @@ from ...utils.logger import Formatter, CustomLogger def _require_websocket_authentication(function: F) -> F: async def wrapper(self, *args, **kwargs): - if self.authentication == False: + 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) @@ -26,7 +30,7 @@ def _require_websocket_authentication(function: F) -> F: class BfxWebsocketClient(object): VERSION = BfxWebsocketBucket.VERSION - MAXIMUM_BUCKETS_AMOUNT = 20 + MAXIMUM_CONNECTIONS_AMOUNT = 20 EVENTS = [ "open", "subscribed", "authenticated", "wss-error", @@ -34,10 +38,12 @@ class BfxWebsocketClient(object): *AuthenticatedChannelsHandler.EVENTS ] - def __init__(self, host, API_KEY = None, API_SECRET = None, filter = None, buckets = 5, log_level = "WARNING"): + def __init__(self, host, API_KEY = None, API_SECRET = None, filter = None, log_level = "WARNING"): self.host, self.websocket, self.event_emitter = host, None, AsyncIOEventEmitter() - self.API_KEY, self.API_SECRET, self.filter, self.authentication = API_KEY, API_SECRET, filter, False + self.API_KEY, self.API_SECRET, self.filter = API_KEY, API_SECRET, filter + + self.inputs = BfxWebsocketInputs(handle_websocket_input=self.__handle_websocket_input) self.handler = AuthenticatedChannelsHandler(event_emitter=self.event_emitter) @@ -48,36 +54,58 @@ class BfxWebsocketClient(object): str().join(traceback.format_exception(type(exception), exception, exception.__traceback__))[:-1]) ) - 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, connections = 5): + return asyncio.run(self.start(connections)) - self.buckets = [ BfxWebsocketBucket(self.host, self.event_emitter, self.__bucket_open_signal) for _ in range(buckets) ] + 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.inputs = BfxWebsocketInputs(self.__handle_websocket_input) + self.on_open_events = [ asyncio.Event() for _ in range(connections) ] - def run(self): - return asyncio.run(self.start()) + self.buckets = [ + BfxWebsocketBucket(self.host, self.event_emitter, self.on_open_events[index]) + for index in range(connections) + ] - 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)) + 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): + Reconnection = namedtuple("Reconnection", ["status", "code", "timestamp"]) + + reconnection = Reconnection(status=False, code=0, timestamp=None) + async for websocket in websockets.connect(self.host): - self.websocket = websocket - - await self.__authenticate(API_KEY, API_SECRET, filter) + 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.API_KEY != None and self.API_SECRET != None: + await self.__authenticate(API_KEY=API_KEY, API_SECRET=API_SECRET, filter=filter) try: async for message in websocket: + if reconnection.status == True: + self.logger.warning(f"Reconnect Attempt Successful (error <{reconnection.code}>): The " + + f"client has been offline for a total of {datetime.now() - reconnection.timestamp} " + + f"(first reconnection attempt: {reconnection.timestamp:%d-%m-%Y at %H:%M:%S}).") + + reconnection = Reconnection(status=False, code=0, timestamp=None) + message = json.loads(message) - if isinstance(message, dict) and message["event"] == "auth": + 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"] == "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.") @@ -85,8 +113,13 @@ class BfxWebsocketClient(object): 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 + except websockets.ConnectionClosedError as error: + self.logger.error(f"Connection terminated due to an error (status code: <{error.code}>) -> {str(error)}. Attempting to reconnect...") + reconnection = Reconnection(status=True, code=error.code, timestamp=datetime.now()); + continue + + if reconnection.status == False: + await self.websocket.wait_closed(); break async def __authenticate(self, API_KEY, API_SECRET, filter=None): data = { "event": "auth", "filter": filter, "apiKey": API_KEY } @@ -130,10 +163,6 @@ class BfxWebsocketClient(object): 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, *events, callback = None): for event in events: if event not in BfxWebsocketClient.EVENTS: diff --git a/bfxapi/websocket/client/bfx_websocket_inputs.py b/bfxapi/websocket/client/bfx_websocket_inputs.py index 141f817..4b4e04c 100644 --- a/bfxapi/websocket/client/bfx_websocket_inputs.py +++ b/bfxapi/websocket/client/bfx_websocket_inputs.py @@ -6,15 +6,15 @@ from .. enums import OrderType, FundingOfferType from ... utils.JSONEncoder import JSON class BfxWebsocketInputs(object): - def __init__(self, __handle_websocket_input): - self.__handle_websocket_input = __handle_websocket_input + def __init__(self, handle_websocket_input): + self.handle_websocket_input = handle_websocket_input async def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, float, str], price: Optional[Union[Decimal, float, str]] = None, lev: Optional[int] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_oco_stop: Optional[Union[Decimal, float, str]] = None, gid: Optional[int] = None, cid: Optional[int] = None, flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None): - await self.__handle_websocket_input("on", { + await self.handle_websocket_input("on", { "type": type, "symbol": symbol, "amount": amount, "price": price, "lev": lev, "price_trailing": price_trailing, "price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop, @@ -26,7 +26,7 @@ class BfxWebsocketInputs(object): cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, tif: Optional[Union[datetime, str]] = None): - await self.__handle_websocket_input("ou", { + await self.handle_websocket_input("ou", { "id": id, "amount": amount, "price": price, "cid": cid, "cid_date": cid_date, "gid": gid, "flags": flags, "lev": lev, "delta": delta, @@ -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): - await self.__handle_websocket_input("oc", { + await self.handle_websocket_input("oc", { "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): - await self.__handle_websocket_input("oc_multi", { + await self.handle_websocket_input("oc_multi", { "ids": ids, "cids": cids, "gids": gids, "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], rate: Union[Decimal, float, str], period: int, flags: Optional[int] = 0): - await self.__handle_websocket_input("fon", { + await self.handle_websocket_input("fon", { "type": type, "symbol": symbol, "amount": amount, "rate": rate, "period": period, "flags": flags }) async def cancel_funding_offer(self, id: int): - await self.__handle_websocket_input("foc", { "id": id }) + await self.handle_websocket_input("foc", { "id": id }) async def calc(self, *args: str): - await self.__handle_websocket_input("calc", list(map(lambda arg: [arg], args))) \ No newline at end of file + await self.handle_websocket_input("calc", list(map(lambda arg: [arg], args))) \ No newline at end of file diff --git a/bfxapi/websocket/enums.py b/bfxapi/websocket/enums.py index b9530db..1877cea 100644 --- a/bfxapi/websocket/enums.py +++ b/bfxapi/websocket/enums.py @@ -1,6 +1,6 @@ from .. enums import * -class Channels(str, Enum): +class Channel(str, Enum): TICKER = "ticker" TRADES = "trades" BOOK = "book" diff --git a/bfxapi/websocket/handlers/public_channels_handler.py b/bfxapi/websocket/handlers/public_channels_handler.py index d63ea1f..52e47ef 100644 --- a/bfxapi/websocket/handlers/public_channels_handler.py +++ b/bfxapi/websocket/handlers/public_channels_handler.py @@ -28,7 +28,7 @@ class PublicChannelsHandler(object): _clear = lambda dictionary, *args: { key: value for key, value in dictionary.items() if key not in args } if (channel := subscription["channel"]) and channel in self.__handlers.keys(): - return self.__handlers[channel](_clear(subscription, "event", "channel", "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']}>.") diff --git a/bfxapi/websocket/subscriptions.py b/bfxapi/websocket/subscriptions.py index 5e2d692..10cbbfe 100644 --- a/bfxapi/websocket/subscriptions.py +++ b/bfxapi/websocket/subscriptions.py @@ -1,4 +1,4 @@ -from typing import TypedDict, Union, Optional +from typing import TypedDict, Union, Literal, Optional __all__ = [ "Subscription", @@ -10,22 +10,22 @@ __all__ = [ "Status" ] -_Header = TypedDict("_Header", { "event": str, "channel": str, "subId": str }) +_Header = TypedDict("_Header", { "event": Literal["subscribed"], "channel": str, "chanId": int }) Subscription = Union["Ticker", "Trades", "Book", "Candles", "Status"] class Ticker(TypedDict): - chanId: int; symbol: str + subId: str; symbol: str pair: Optional[str] currency: Optional[str] class Trades(TypedDict): - chanId: int; symbol: str + subId: str; symbol: str pair: Optional[str] currency: Optional[str] class Book(TypedDict): - chanId: int + subId: str symbol: str prec: str freq: str @@ -33,9 +33,9 @@ class Book(TypedDict): pair: str class Candles(TypedDict): - chanId: int + subId: str key: str class Status(TypedDict): - chanId: int + subId: str key: str \ No newline at end of file diff --git a/examples/websocket/order_book.py b/examples/websocket/order_book.py index 82bd105..a419454 100644 --- a/examples/websocket/order_book.py +++ b/examples/websocket/order_book.py @@ -7,7 +7,7 @@ from typing import List from bfxapi import Client, Constants 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 class OrderBook(object): @@ -47,7 +47,7 @@ def on_wss_error(code: Error, msg: str): @bfx.wss.on("open") async def on_open(): for symbol in SYMBOLS: - await bfx.wss.subscribe(Channels.BOOK, symbol=symbol) + await bfx.wss.subscribe(Channel.BOOK, symbol=symbol) @bfx.wss.on("subscribed") def on_subscribed(subscription): diff --git a/examples/websocket/raw_order_book.py b/examples/websocket/raw_order_book.py index 896e351..a039060 100644 --- a/examples/websocket/raw_order_book.py +++ b/examples/websocket/raw_order_book.py @@ -7,7 +7,7 @@ from typing import List from bfxapi import Client, Constants 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 class RawOrderBook(object): @@ -47,7 +47,7 @@ def on_wss_error(code: Error, msg: str): @bfx.wss.on("open") async def on_open(): for symbol in SYMBOLS: - await bfx.wss.subscribe(Channels.BOOK, symbol=symbol, prec="R0") + await bfx.wss.subscribe(Channel.BOOK, symbol=symbol, prec="R0") @bfx.wss.on("subscribed") def on_subscribed(subscription): diff --git a/examples/websocket/ticker.py b/examples/websocket/ticker.py index 1c081b2..729f3ea 100644 --- a/examples/websocket/ticker.py +++ b/examples/websocket/ticker.py @@ -3,7 +3,7 @@ from bfxapi import Client, Constants from bfxapi.websocket import subscriptions -from bfxapi.websocket.enums import Channels +from bfxapi.websocket.enums import Channel from bfxapi.websocket.types import TradingPairTicker bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) @@ -16,6 +16,6 @@ def on_t_ticker_update(subscription: subscriptions.Ticker, data: TradingPairTick @bfx.wss.once("open") async def open(): - await bfx.wss.subscribe(Channels.TICKER, symbol="tBTCUSD") + await bfx.wss.subscribe(Channel.TICKER, symbol="tBTCUSD") bfx.wss.run() \ No newline at end of file From b8a5bcb5157874f4250c89bb73b105fbbb3be4a4 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 16 Feb 2023 20:08:05 +0100 Subject: [PATCH 169/265] Fix bugs and rewrite regions of new reconnection system. --- bfxapi/client.py | 12 ++- .../websocket/client/bfx_websocket_bucket.py | 7 +- .../websocket/client/bfx_websocket_client.py | 97 +++++++++++++------ 3 files changed, 83 insertions(+), 33 deletions(-) diff --git a/bfxapi/client.py b/bfxapi/client.py index ec72fb3..84b9d76 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -1,7 +1,7 @@ from .rest import BfxRestInterface from .websocket import BfxWebsocketClient -from typing import Optional +from typing import List, Optional from enum import Enum @@ -21,8 +21,15 @@ class Client(object): WSS_HOST: str = Constants.WSS_HOST, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None, + filter: Optional[List[str]] = None, log_level: str = "WARNING" ): + credentials = { + "API_KEY": API_KEY, + "API_SECRET": API_SECRET, + "filter": filter + } + self.rest = BfxRestInterface( host=REST_HOST, API_KEY=API_KEY, @@ -31,7 +38,6 @@ class Client(object): self.wss = BfxWebsocketClient( host=WSS_HOST, - API_KEY=API_KEY, - API_SECRET=API_SECRET, + credentials=credentials, log_level=log_level ) \ No newline at end of file diff --git a/bfxapi/websocket/client/bfx_websocket_bucket.py b/bfxapi/websocket/client/bfx_websocket_bucket.py index 15caffc..b4c573b 100644 --- a/bfxapi/websocket/client/bfx_websocket_bucket.py +++ b/bfxapi/websocket/client/bfx_websocket_bucket.py @@ -63,9 +63,12 @@ class BfxWebsocketBucket(object): self.event_emitter.emit("wss-error", message["code"], message["msg"]) elif isinstance(message, list) and (chanId := message[0]) and message[1] != _HEARTBEAT: self.handler.handle(self.subscriptions[chanId], *message[1:]) - except websockets.ConnectionClosedError: self.on_open_event.clear(); reconnection = True; continue + except websockets.ConnectionClosedError: + self.on_open_event.clear() + reconnection = True + continue - await self.websocket.wait_closed(); break + break @_require_websocket_connection async def _subscribe(self, channel, subId=None, **kwargs): diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 8f2a54d..15faf89 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -1,4 +1,4 @@ -import traceback, json, asyncio, hmac, hashlib, time, websockets +import traceback, json, asyncio, hmac, hashlib, time, websockets, socket, random from typing import cast @@ -38,10 +38,10 @@ class BfxWebsocketClient(object): *AuthenticatedChannelsHandler.EVENTS ] - def __init__(self, host, API_KEY = None, API_SECRET = None, filter = None, log_level = "WARNING"): - self.host, self.websocket, self.event_emitter = host, None, AsyncIOEventEmitter() + def __init__(self, host, credentials = None, log_level = "WARNING"): + self.websocket = None - self.API_KEY, self.API_SECRET, self.filter = API_KEY, API_SECRET, filter + self.host, self.credentials, self.event_emitter = host, credentials, AsyncIOEventEmitter() self.inputs = BfxWebsocketInputs(handle_websocket_input=self.__handle_websocket_input) @@ -71,33 +71,35 @@ class BfxWebsocketClient(object): tasks = [ bucket._connect(index) for index, bucket in enumerate(self.buckets) ] - tasks.append(self.__connect(self.API_KEY, self.API_SECRET, self.filter)) + tasks.append(self.__connect(self.credentials)) await asyncio.gather(*tasks) - async def __connect(self, API_KEY, API_SECRET, filter=None): - Reconnection = namedtuple("Reconnection", ["status", "code", "timestamp"]) + async def __connect(self, credentials = None): + Reconnection = namedtuple("Reconnection", ["status", "attempts", "timestamp"]) - reconnection = Reconnection(status=False, code=0, timestamp=None) + reconnection = Reconnection(status=False, attempts=0, timestamp=None) - async for websocket in websockets.connect(self.host): - self.websocket, self.authentication = websocket, False + async def _connection(): + nonlocal reconnection - if (await asyncio.gather(*[ on_open_event.wait() for on_open_event in self.on_open_events ])): - self.event_emitter.emit("open") + async with websockets.connect(self.host) as websocket: + if reconnection.status == True: + self.logger.info(f"Reconnect attempt successful (attempt N°{reconnection.attempts}): The " + + f"client has been offline for a total of {datetime.now() - reconnection.timestamp} " + + f"(first reconnection attempt: {reconnection.timestamp:%d-%m-%Y at %H:%M:%S}).") - if self.API_KEY != None and self.API_SECRET != None: - await self.__authenticate(API_KEY=API_KEY, API_SECRET=API_SECRET, filter=filter) + 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 != None: + await self.__authenticate(**self.credentials) - try: async for message in websocket: - if reconnection.status == True: - self.logger.warning(f"Reconnect Attempt Successful (error <{reconnection.code}>): The " + - f"client has been offline for a total of {datetime.now() - reconnection.timestamp} " + - f"(first reconnection attempt: {reconnection.timestamp:%d-%m-%Y at %H:%M:%S}).") - - reconnection = Reconnection(status=False, code=0, timestamp=None) - message = json.loads(message) if isinstance(message, dict) and message["event"] == "info" and "version" in message: @@ -113,13 +115,52 @@ class BfxWebsocketClient(object): 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 as error: - self.logger.error(f"Connection terminated due to an error (status code: <{error.code}>) -> {str(error)}. Attempting to reconnect...") - reconnection = Reconnection(status=True, code=error.code, timestamp=datetime.now()); - continue - + + 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 + + delay = _Delay(backoff_factor=1.618) + + 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: + self.logger.error("Connection lost: no close frame received " + + "or sent (1006). Attempting to reconnect...") + + reconnection = Reconnection(status=True, attempts=1, timestamp=datetime.now()); + 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: - await self.websocket.wait_closed(); break + break async def __authenticate(self, API_KEY, API_SECRET, filter=None): data = { "event": "auth", "filter": filter, "apiKey": API_KEY } From e536515bbdd1ee788fc885088dd6ac82b7b628bc Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 17 Feb 2023 02:58:53 +0100 Subject: [PATCH 170/265] Fix bugs and rewrite code in bfxapi.websocket.client sub-package. --- .../websocket/client/bfx_websocket_bucket.py | 24 ++++++++++++------- .../websocket/client/bfx_websocket_client.py | 10 ++++---- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/bfxapi/websocket/client/bfx_websocket_bucket.py b/bfxapi/websocket/client/bfx_websocket_bucket.py index b4c573b..90c8d21 100644 --- a/bfxapi/websocket/client/bfx_websocket_bucket.py +++ b/bfxapi/websocket/client/bfx_websocket_bucket.py @@ -37,6 +37,8 @@ class BfxWebsocketBucket(object): async for websocket in websockets.connect(self.host): self.websocket = websocket + self.on_open_event.set() + if reconnection == True or (reconnection := False): for pending in self.pendings: await self.websocket.send(json.dumps(pending)) @@ -46,8 +48,6 @@ class BfxWebsocketBucket(object): self.subscriptions.clear() - self.on_open_event.set() - try: async for message in websocket: message = json.loads(message) @@ -63,11 +63,14 @@ class BfxWebsocketBucket(object): self.event_emitter.emit("wss-error", message["code"], message["msg"]) elif isinstance(message, list) and (chanId := message[0]) and message[1] != _HEARTBEAT: self.handler.handle(self.subscriptions[chanId], *message[1:]) - except websockets.ConnectionClosedError: - self.on_open_event.clear() - reconnection = True - continue - + except websockets.ConnectionClosedError as error: + if error.code == 1006: + self.on_open_event.clear() + reconnection = True + continue + + raise error + break @_require_websocket_connection @@ -96,4 +99,9 @@ class BfxWebsocketBucket(object): @_require_websocket_connection async def _close(self, code=1000, reason=str()): - await self.websocket.close(code=code, reason=reason) \ No newline at end of file + 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"] \ No newline at end of file diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 15faf89..64deb0b 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -78,7 +78,7 @@ class BfxWebsocketClient(object): async def __connect(self, credentials = None): Reconnection = namedtuple("Reconnection", ["status", "attempts", "timestamp"]) - reconnection = Reconnection(status=False, attempts=0, timestamp=None) + reconnection, delay = Reconnection(status=False, attempts=0, timestamp=None), None async def _connection(): nonlocal reconnection @@ -137,8 +137,6 @@ class BfxWebsocketClient(object): return (self.__backoff_delay == _Delay.BACKOFF_MIN) \ and self.__initial_delay or self.__backoff_delay - delay = _Delay(backoff_factor=1.618) - while True: if reconnection.status == True: await asyncio.sleep(delay.next()) @@ -151,6 +149,8 @@ class BfxWebsocketClient(object): + "or sent (1006). Attempting to reconnect...") 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." @@ -184,9 +184,9 @@ class BfxWebsocketClient(object): await self.buckets[index]._subscribe(channel, **kwargs) - async def unsubscribe(self, chanId): + async def unsubscribe(self, subId): for bucket in self.buckets: - if chanId in bucket.subscriptions.keys(): + if (chanId := bucket._get_chan_id(subId)): await bucket._unsubscribe(chanId=chanId) async def close(self, code=1000, reason=str()): From 4d0fa49e2271a650c0dc8e195a18e0bcea8d2935 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 17 Feb 2023 04:01:47 +0100 Subject: [PATCH 171/265] Rewrite bfxapi/utils/logger.py with new ColoredLogger. --- bfxapi/client.py | 2 +- bfxapi/utils/logger.py | 111 +++++------------- .../websocket/client/bfx_websocket_client.py | 10 +- 3 files changed, 38 insertions(+), 85 deletions(-) diff --git a/bfxapi/client.py b/bfxapi/client.py index 84b9d76..aa7eaf2 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -22,7 +22,7 @@ class Client(object): API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None, filter: Optional[List[str]] = None, - log_level: str = "WARNING" + log_level: str = "INFO" ): credentials = { "API_KEY": API_KEY, diff --git a/bfxapi/utils/logger.py b/bfxapi/utils/logger.py index 0ea3894..cf3e970 100644 --- a/bfxapi/utils/logger.py +++ b/bfxapi/utils/logger.py @@ -1,99 +1,52 @@ -""" -Module used to describe all of the different data types -""" - import logging +BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) + RESET_SEQ = "\033[0m" + COLOR_SEQ = "\033[1;%dm" +ITALIC_COLOR_SEQ = "\033[3;%dm" +UNDERLINE_COLOR_SEQ = "\033[4;%dm" + BOLD_SEQ = "\033[1m" -UNDERLINE_SEQ = "\033[04m" - -YELLOW = '\033[93m' -WHITE = '\33[37m' -BLUE = '\033[34m' -LIGHT_BLUE = '\033[94m' -RED = '\033[91m' -GREY = '\33[90m' - -KEYWORD_COLORS = { - 'WARNING': YELLOW, - 'INFO': LIGHT_BLUE, - 'DEBUG': WHITE, - 'CRITICAL': YELLOW, - 'ERROR': RED, - 'TRADE': '\33[102m\33[30m' -} def formatter_message(message, use_color = True): - """ - Syntax highlight certain keywords - """ if use_color: message = message.replace("$RESET", RESET_SEQ).replace("$BOLD", BOLD_SEQ) else: message = message.replace("$RESET", "").replace("$BOLD", "") return message -def format_word(message, word, color_seq, bold=False, underline=False): - """ - Surround the given word with a sequence - """ - replacer = color_seq + word + RESET_SEQ - if underline: - replacer = UNDERLINE_SEQ + replacer - if bold: - replacer = BOLD_SEQ + replacer - return message.replace(word, replacer) +COLORS = { + "DEBUG": CYAN, + "INFO": BLUE, + "WARNING": YELLOW, + "ERROR": RED +} -class Formatter(logging.Formatter): - """ - This Formatted simply colors in the levelname i.e 'INFO', 'DEBUG' - """ - def __init__(self, msg, use_color = True): - logging.Formatter.__init__(self, msg) - self.use_color = use_color +class _ColoredFormatter(logging.Formatter): + def __init__(self, msg, use_color = True): + logging.Formatter.__init__(self, msg, "%d-%m-%Y %H:%M:%S") + self.use_color = use_color - def format(self, record): - """ - Format and highlight certain keywords - """ - levelname = record.levelname - if self.use_color and levelname in KEYWORD_COLORS: - levelname_color = KEYWORD_COLORS[levelname] + levelname + RESET_SEQ - record.levelname = levelname_color - record.name = GREY + record.name + RESET_SEQ - return logging.Formatter.format(self, record) + def format(self, record): + levelname = record.levelname + if self.use_color and levelname in COLORS: + levelname_color = COLOR_SEQ % (30 + COLORS[levelname]) + levelname + RESET_SEQ + record.levelname = levelname_color + record.name = ITALIC_COLOR_SEQ % (30 + BLACK) + record.name + RESET_SEQ + return logging.Formatter.format(self, record) -class CustomLogger(logging.Logger): - """ - This adds extra logging functions such as logger.trade and also - sets the logger to use the custom formatter - """ - FORMAT = "[$BOLD%(name)s$RESET] [%(levelname)s] %(message)s" +class ColoredLogger(logging.Logger): + FORMAT = "[$BOLD%(name)s$RESET] [%(asctime)s] [%(levelname)s] %(message)s" + COLOR_FORMAT = formatter_message(FORMAT, True) - TRADE = 50 + + def __init__(self, name, level): + logging.Logger.__init__(self, name, level) - def __init__(self, name, logLevel='DEBUG'): - logging.Logger.__init__(self, name, logLevel) - color_formatter = Formatter(self.COLOR_FORMAT) + colored_formatter = _ColoredFormatter(self.COLOR_FORMAT) console = logging.StreamHandler() - console.setFormatter(color_formatter) - self.addHandler(console) - logging.addLevelName(self.TRADE, "TRADE") - return + console.setFormatter(colored_formatter) - def set_level(self, level): - logging.Logger.setLevel(self, level) - - def trade(self, message, *args, **kws): - """ - Print a syntax highlighted trade signal - """ - if self.isEnabledFor(self.TRADE): - message = format_word(message, 'CLOSED ', YELLOW, bold=True) - message = format_word(message, 'OPENED ', LIGHT_BLUE, bold=True) - message = format_word(message, 'UPDATED ', BLUE, bold=True) - message = format_word(message, 'CLOSED_ALL ', RED, bold=True) - # Yes, logger takes its '*args' as 'args'. - self._log(self.TRADE, message, args, **kws) \ No newline at end of file + self.addHandler(console) \ No newline at end of file diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 64deb0b..f3c354d 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -16,7 +16,7 @@ from ..exceptions import WebsocketAuthenticationRequired, InvalidAuthenticationC from ...utils.JSONEncoder import JSONEncoder -from ...utils.logger import Formatter, CustomLogger +from ...utils.logger import ColoredLogger def _require_websocket_authentication(function: F) -> F: async def wrapper(self, *args, **kwargs): @@ -38,7 +38,7 @@ class BfxWebsocketClient(object): *AuthenticatedChannelsHandler.EVENTS ] - def __init__(self, host, credentials = None, log_level = "WARNING"): + def __init__(self, host, credentials = None, log_level = "INFO"): self.websocket = None self.host, self.credentials, self.event_emitter = host, credentials, AsyncIOEventEmitter() @@ -47,7 +47,7 @@ class BfxWebsocketClient(object): self.handler = AuthenticatedChannelsHandler(event_emitter=self.event_emitter) - self.logger = CustomLogger("BfxWebsocketClient", logLevel=log_level) + 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" + @@ -85,9 +85,9 @@ class BfxWebsocketClient(object): async with websockets.connect(self.host) as websocket: if reconnection.status == True: - self.logger.info(f"Reconnect attempt successful (attempt N°{reconnection.attempts}): The " + + 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"(first reconnection attempt: {reconnection.timestamp:%d-%m-%Y at %H:%M:%S}).") + f"(connection lost at: {reconnection.timestamp:%d-%m-%Y at %H:%M:%S}).") reconnection = Reconnection(status=False, attempts=0, timestamp=None) From 9eb2c73407a56d6229311d6ef132ebb47bbc50df Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 17 Feb 2023 04:04:50 +0100 Subject: [PATCH 172/265] Fix small bug in examples/websocket/ticker.py demo. --- bfxapi/websocket/client/bfx_websocket_client.py | 4 ++-- examples/websocket/ticker.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index f3c354d..4bb826a 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -96,8 +96,8 @@ class BfxWebsocketClient(object): 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 != None: - await self.__authenticate(**self.credentials) + if credentials and credentials["API_KEY"] and credentials["API_SECRET"]: + await self.__authenticate(**credentials) async for message in websocket: message = json.loads(message) diff --git a/examples/websocket/ticker.py b/examples/websocket/ticker.py index 729f3ea..a335a28 100644 --- a/examples/websocket/ticker.py +++ b/examples/websocket/ticker.py @@ -10,7 +10,7 @@ bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) @bfx.wss.on("t_ticker_update") 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}") From fde27e933f1f21156a07c62f877284b0819e08f9 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 17 Feb 2023 04:15:38 +0100 Subject: [PATCH 173/265] Add handling for <20051 : Stop/Restart Websocket Server (please reconnect)>. --- bfxapi/websocket/client/bfx_websocket_client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 4bb826a..0218cd7 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -107,6 +107,8 @@ class BfxWebsocketClient(object): 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: + raise websockets.ConnectionClosedError(rcvd=None, sent=None) elif isinstance(message, dict) and message["event"] == "auth": if message["status"] == "OK": self.event_emitter.emit("authenticated", message); self.authentication = True From 16d0ee525edaabd56953eca4a557f3fd73baa46f Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 17 Feb 2023 04:29:26 +0100 Subject: [PATCH 174/265] Remove test_rest_serializers_and_types.py and test_websocket_serializers_and_types.py. Add new test_rest_serializers.py and test_websocket_serializers.py unit tests. Edit bfxapi.tests.__init__.py's test suite. --- bfxapi/tests/__init__.py | 8 ++++---- bfxapi/tests/test_rest_serializers.py | 17 +++++++++++++++++ bfxapi/tests/test_rest_serializers_and_types.py | 17 ----------------- bfxapi/tests/test_websocket_serializers.py | 17 +++++++++++++++++ .../test_websocket_serializers_and_types.py | 17 ----------------- 5 files changed, 38 insertions(+), 38 deletions(-) create mode 100644 bfxapi/tests/test_rest_serializers.py delete mode 100644 bfxapi/tests/test_rest_serializers_and_types.py create mode 100644 bfxapi/tests/test_websocket_serializers.py delete mode 100644 bfxapi/tests/test_websocket_serializers_and_types.py diff --git a/bfxapi/tests/__init__.py b/bfxapi/tests/__init__.py index a63ea0d..057c2c0 100644 --- a/bfxapi/tests/__init__.py +++ b/bfxapi/tests/__init__.py @@ -1,6 +1,6 @@ import unittest -from .test_rest_serializers_and_types import TestRestSerializersAndTypes -from .test_websocket_serializers_and_types import TestWebsocketSerializersAndTypes +from .test_rest_serializers import TestRestSerializers +from .test_websocket_serializers import TestWebsocketSerializers from .test_labeler import TestLabeler from .test_notification import TestNotification @@ -8,8 +8,8 @@ NAME = "tests" def suite(): return unittest.TestSuite([ - unittest.makeSuite(TestRestSerializersAndTypes), - unittest.makeSuite(TestWebsocketSerializersAndTypes), + unittest.makeSuite(TestRestSerializers), + unittest.makeSuite(TestWebsocketSerializers), unittest.makeSuite(TestLabeler), unittest.makeSuite(TestNotification), ]) diff --git a/bfxapi/tests/test_rest_serializers.py b/bfxapi/tests/test_rest_serializers.py new file mode 100644 index 0000000..4c24992 --- /dev/null +++ b/bfxapi/tests/test_rest_serializers.py @@ -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() \ No newline at end of file diff --git a/bfxapi/tests/test_rest_serializers_and_types.py b/bfxapi/tests/test_rest_serializers_and_types.py deleted file mode 100644 index 7bc7242..0000000 --- a/bfxapi/tests/test_rest_serializers_and_types.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/bfxapi/tests/test_websocket_serializers.py b/bfxapi/tests/test_websocket_serializers.py new file mode 100644 index 0000000..a559565 --- /dev/null +++ b/bfxapi/tests/test_websocket_serializers.py @@ -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() \ No newline at end of file diff --git a/bfxapi/tests/test_websocket_serializers_and_types.py b/bfxapi/tests/test_websocket_serializers_and_types.py deleted file mode 100644 index 338b959..0000000 --- a/bfxapi/tests/test_websocket_serializers_and_types.py +++ /dev/null @@ -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() \ No newline at end of file From d72fcf3981bb36570c31705c44bfae631610d469 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 17 Feb 2023 17:02:09 +0100 Subject: [PATCH 175/265] Add better handling for info code 20051. --- bfxapi/websocket/client/bfx_websocket_client.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 0218cd7..adea696 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -108,7 +108,9 @@ class BfxWebsocketClient(object): 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: - raise websockets.ConnectionClosedError(rcvd=None, sent=None) + 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 @@ -130,9 +132,9 @@ class BfxWebsocketClient(object): 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): @@ -146,9 +148,14 @@ class BfxWebsocketClient(object): try: await _connection() except (websockets.ConnectionClosedError, socket.gaierror) as error: - if isinstance(error, websockets.ConnectionClosedError) and error.code == 1006: - self.logger.error("Connection lost: no close frame received " - + "or sent (1006). Attempting to reconnect...") + 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()); From ab66170cf3eb16ff4db3f06a2f4d71f2d5024771 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 17 Feb 2023 18:38:07 +0100 Subject: [PATCH 176/265] Apply refactoring to root package bfxapi. --- bfxapi/__init__.py | 7 ++++- bfxapi/client.py | 28 ++++++------------- bfxapi/rest/endpoints/bfx_rest_interface.py | 5 +++- .../rest/endpoints/rest_public_endpoints.py | 16 +++++------ bfxapi/urls.py | 7 +++++ .../websocket/client/bfx_websocket_client.py | 4 +-- examples/rest/claim_position.py | 4 +-- examples/rest/create_funding_offer.py | 4 +-- examples/rest/create_order.py | 4 +-- examples/rest/derivatives.py | 4 +-- examples/rest/extra_calcs.py | 4 +-- examples/rest/funding_auto_renew.py | 4 +-- examples/rest/get_authenticated_data.py | 4 +-- examples/rest/get_candles_hist.py | 4 +-- examples/rest/get_funding_info.py | 4 +-- examples/rest/get_funding_trades_history.py | 4 +-- examples/rest/get_liquidations.py | 4 +-- examples/rest/get_positions.py | 4 +-- examples/rest/get_public_data.py | 4 +-- examples/rest/get_pulse_data.py | 4 +-- examples/rest/increase_position.py | 4 +-- examples/rest/keep_taken_funding.py | 4 +-- examples/rest/merchant.py | 4 +-- examples/rest/return_taken_funding.py | 4 +-- examples/rest/transfer_wallet.py | 4 +-- examples/websocket/create_order.py | 4 +-- examples/websocket/order_book.py | 4 +-- examples/websocket/raw_order_book.py | 4 +-- examples/websocket/ticker.py | 4 +-- 29 files changed, 81 insertions(+), 78 deletions(-) create mode 100644 bfxapi/urls.py diff --git a/bfxapi/__init__.py b/bfxapi/__init__.py index c11c9ab..4fbdfd6 100644 --- a/bfxapi/__init__.py +++ b/bfxapi/__init__.py @@ -1 +1,6 @@ -from .client import Client, Constants \ No newline at end of file +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" \ No newline at end of file diff --git a/bfxapi/client.py b/bfxapi/client.py index aa7eaf2..f3121ac 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -1,39 +1,27 @@ from .rest import BfxRestInterface from .websocket import BfxWebsocketClient +from .urls import REST_HOST, WSS_HOST 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): def __init__( self, - REST_HOST: str = Constants.REST_HOST, - WSS_HOST: str = Constants.WSS_HOST, + REST_HOST: str = REST_HOST, + WSS_HOST: str = WSS_HOST, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None, filter: Optional[List[str]] = None, log_level: str = "INFO" ): - credentials = { - "API_KEY": API_KEY, - "API_SECRET": API_SECRET, - "filter": filter - } + credentials = None + + if API_KEY and API_SECRET: + credentials = { "API_KEY": API_KEY, "API_SECRET": API_SECRET, "filter": filter } self.rest = BfxRestInterface( host=REST_HOST, - API_KEY=API_KEY, - API_SECRET=API_SECRET + credentials=credentials ) self.wss = BfxWebsocketClient( diff --git a/bfxapi/rest/endpoints/bfx_rest_interface.py b/bfxapi/rest/endpoints/bfx_rest_interface.py index a2dc6ec..b117fa6 100644 --- a/bfxapi/rest/endpoints/bfx_rest_interface.py +++ b/bfxapi/rest/endpoints/bfx_rest_interface.py @@ -7,7 +7,10 @@ from .rest_merchant_endpoints import RestMerchantEndpoints class BfxRestInterface(object): 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.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) \ No newline at end of file diff --git a/bfxapi/rest/endpoints/rest_public_endpoints.py b/bfxapi/rest/endpoints/rest_public_endpoints.py index 3810e97..b5313fd 100644 --- a/bfxapi/rest/endpoints/rest_public_endpoints.py +++ b/bfxapi/rest/endpoints/rest_public_endpoints.py @@ -25,7 +25,7 @@ class RestPublicEndpoints(Middleware): if isinstance(pairs, str) and pairs == "ALL": return [ cast(TradingPairTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.symbol).startswith("t") ] - data = self.get_tickers([ "t" + pair for pair in pairs ]) + data = self.get_tickers([ pair for pair in pairs ]) return cast(List[TradingPairTicker], data) @@ -33,7 +33,7 @@ class RestPublicEndpoints(Middleware): if isinstance(currencies, str) and currencies == "ALL": return [ cast(FundingCurrencyTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.symbol).startswith("f") ] - data = self.get_tickers([ "f" + currency for currency in currencies ]) + data = self.get_tickers([ currency for currency in currencies ]) 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]: 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 ] def get_f_trades(self, currency: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[FundingCurrencyTrade]: params = { "limit": limit, "start": start, "end": end, "sort": sort } - data = self._GET(f"trades/{'f' + currency}/hist", params=params) + data = self._GET(f"trades/{currency}/hist", params=params) return [ serializers.FundingCurrencyTrade.parse(*sub_data) for sub_data in data ] def get_t_book(self, pair: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairBook]: - return [ serializers.TradingPairBook.parse(*sub_data) for sub_data in self._GET(f"book/{'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]: - 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]: - 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]: - 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( self, diff --git a/bfxapi/urls.py b/bfxapi/urls.py new file mode 100644 index 0000000..c9a622b --- /dev/null +++ b/bfxapi/urls.py @@ -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" \ No newline at end of file diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index adea696..7dc06ed 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -96,8 +96,8 @@ class BfxWebsocketClient(object): if await asyncio.gather(*[on_open_event.wait() for on_open_event in self.on_open_events]): self.event_emitter.emit("open") - if credentials and credentials["API_KEY"] and credentials["API_SECRET"]: - await self.__authenticate(**credentials) + if self.credentials: + await self.__authenticate(**self.credentials) async for message in websocket: message = json.loads(message) diff --git a/examples/rest/claim_position.py b/examples/rest/claim_position.py index ba3e4e0..084c9d0 100644 --- a/examples/rest/claim_position.py +++ b/examples/rest/claim_position.py @@ -2,10 +2,10 @@ import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/create_funding_offer.py b/examples/rest/create_funding_offer.py index c1031d8..f462325 100644 --- a/examples/rest/create_funding_offer.py +++ b/examples/rest/create_funding_offer.py @@ -2,11 +2,11 @@ import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST from bfxapi.enums import FundingOfferType, Flag bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/create_order.py b/examples/rest/create_order.py index ea70265..607f7c9 100644 --- a/examples/rest/create_order.py +++ b/examples/rest/create_order.py @@ -1,11 +1,11 @@ # python -c "import examples.rest.create_order" import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST from bfxapi.enums import OrderType, Flag bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/derivatives.py b/examples/rest/derivatives.py index 4aedd00..58a0031 100644 --- a/examples/rest/derivatives.py +++ b/examples/rest/derivatives.py @@ -2,10 +2,10 @@ import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/extra_calcs.py b/examples/rest/extra_calcs.py index e380aef..8ef93cb 100644 --- a/examples/rest/extra_calcs.py +++ b/examples/rest/extra_calcs.py @@ -1,9 +1,9 @@ # python -c "import examples.rest.extra_calcs" -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST + REST_HOST=REST_HOST ) t_symbol_response = bfx.rest.public.get_trading_market_average_price( diff --git a/examples/rest/funding_auto_renew.py b/examples/rest/funding_auto_renew.py index 11ee7ca..f546707 100644 --- a/examples/rest/funding_auto_renew.py +++ b/examples/rest/funding_auto_renew.py @@ -2,10 +2,10 @@ import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/get_authenticated_data.py b/examples/rest/get_authenticated_data.py index ada724a..c3226af 100644 --- a/examples/rest/get_authenticated_data.py +++ b/examples/rest/get_authenticated_data.py @@ -3,10 +3,10 @@ import os import time -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/get_candles_hist.py b/examples/rest/get_candles_hist.py index 98f9da7..d8d9881 100644 --- a/examples/rest/get_candles_hist.py +++ b/examples/rest/get_candles_hist.py @@ -1,9 +1,9 @@ # python -c "import examples.rest.get_candles_hist" -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST + REST_HOST=REST_HOST ) print(f"Candles: {bfx.rest.public.get_candles_hist(symbol='tBTCUSD')}") diff --git a/examples/rest/get_funding_info.py b/examples/rest/get_funding_info.py index 83d0635..82bf150 100644 --- a/examples/rest/get_funding_info.py +++ b/examples/rest/get_funding_info.py @@ -2,10 +2,10 @@ import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/get_funding_trades_history.py b/examples/rest/get_funding_trades_history.py index c1cc8e6..3af19d8 100644 --- a/examples/rest/get_funding_trades_history.py +++ b/examples/rest/get_funding_trades_history.py @@ -2,10 +2,10 @@ import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/get_liquidations.py b/examples/rest/get_liquidations.py index 6113a25..588c83a 100644 --- a/examples/rest/get_liquidations.py +++ b/examples/rest/get_liquidations.py @@ -2,10 +2,10 @@ import time -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST + REST_HOST=REST_HOST ) now = int(round(time.time() * 1000)) diff --git a/examples/rest/get_positions.py b/examples/rest/get_positions.py index 7e71824..62cd309 100644 --- a/examples/rest/get_positions.py +++ b/examples/rest/get_positions.py @@ -3,10 +3,10 @@ import os import time -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/get_public_data.py b/examples/rest/get_public_data.py index a6c388b..125f97c 100644 --- a/examples/rest/get_public_data.py +++ b/examples/rest/get_public_data.py @@ -2,10 +2,10 @@ import time -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST + REST_HOST=REST_HOST ) now = int(round(time.time() * 1000)) diff --git a/examples/rest/get_pulse_data.py b/examples/rest/get_pulse_data.py index 75b55ae..b0cc369 100644 --- a/examples/rest/get_pulse_data.py +++ b/examples/rest/get_pulse_data.py @@ -2,10 +2,10 @@ import time -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST + REST_HOST=REST_HOST ) now = int(round(time.time() * 1000)) diff --git a/examples/rest/increase_position.py b/examples/rest/increase_position.py index 65595c8..add66e3 100644 --- a/examples/rest/increase_position.py +++ b/examples/rest/increase_position.py @@ -2,10 +2,10 @@ import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/keep_taken_funding.py b/examples/rest/keep_taken_funding.py index 1314ffa..21e60f4 100644 --- a/examples/rest/keep_taken_funding.py +++ b/examples/rest/keep_taken_funding.py @@ -2,10 +2,10 @@ import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/merchant.py b/examples/rest/merchant.py index ec6727b..84d9b9b 100644 --- a/examples/rest/merchant.py +++ b/examples/rest/merchant.py @@ -2,10 +2,10 @@ import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/return_taken_funding.py b/examples/rest/return_taken_funding.py index ccb0c2b..73d5a33 100644 --- a/examples/rest/return_taken_funding.py +++ b/examples/rest/return_taken_funding.py @@ -2,10 +2,10 @@ import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/rest/transfer_wallet.py b/examples/rest/transfer_wallet.py index 8de15fd..9384bd8 100644 --- a/examples/rest/transfer_wallet.py +++ b/examples/rest/transfer_wallet.py @@ -2,10 +2,10 @@ import os -from bfxapi.client import Client, Constants +from bfxapi.client import Client, REST_HOST bfx = Client( - REST_HOST=Constants.REST_HOST, + REST_HOST=REST_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/websocket/create_order.py b/examples/websocket/create_order.py index f72f9d4..48f4957 100644 --- a/examples/websocket/create_order.py +++ b/examples/websocket/create_order.py @@ -2,12 +2,12 @@ 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.types import Notification, Order bfx = Client( - WSS_HOST=Constants.WSS_HOST, + WSS_HOST=WSS_HOST, API_KEY=os.getenv("BFX_API_KEY"), API_SECRET=os.getenv("BFX_API_SECRET") ) diff --git a/examples/websocket/order_book.py b/examples/websocket/order_book.py index a419454..55e4ae3 100644 --- a/examples/websocket/order_book.py +++ b/examples/websocket/order_book.py @@ -4,7 +4,7 @@ from collections import OrderedDict from typing import List -from bfxapi import Client, Constants +from bfxapi import Client, PUB_WSS_HOST from bfxapi.websocket import subscriptions from bfxapi.websocket.enums import Channel, Error @@ -38,7 +38,7 @@ SYMBOLS = [ "tBTCUSD", "tLTCUSD", "tLTCBTC", "tETHUSD", "tETHBTC" ] order_book = OrderBook(symbols=SYMBOLS) -bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) +bfx = Client(WSS_HOST=PUB_WSS_HOST) @bfx.wss.on("wss-error") def on_wss_error(code: Error, msg: str): diff --git a/examples/websocket/raw_order_book.py b/examples/websocket/raw_order_book.py index a039060..3ce9c6d 100644 --- a/examples/websocket/raw_order_book.py +++ b/examples/websocket/raw_order_book.py @@ -4,7 +4,7 @@ from collections import OrderedDict from typing import List -from bfxapi import Client, Constants +from bfxapi import Client, PUB_WSS_HOST from bfxapi.websocket import subscriptions from bfxapi.websocket.enums import Channel, Error @@ -38,7 +38,7 @@ SYMBOLS = [ "tBTCUSD", "tLTCUSD", "tLTCBTC", "tETHUSD", "tETHBTC" ] raw_order_book = RawOrderBook(symbols=SYMBOLS) -bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) +bfx = Client(WSS_HOST=PUB_WSS_HOST) @bfx.wss.on("wss-error") def on_wss_error(code: Error, msg: str): diff --git a/examples/websocket/ticker.py b/examples/websocket/ticker.py index a335a28..d4b4c91 100644 --- a/examples/websocket/ticker.py +++ b/examples/websocket/ticker.py @@ -1,12 +1,12 @@ # 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.enums import Channel from bfxapi.websocket.types import TradingPairTicker -bfx = Client(WSS_HOST=Constants.PUB_WSS_HOST) +bfx = Client(WSS_HOST=PUB_WSS_HOST) @bfx.wss.on("t_ticker_update") def on_t_ticker_update(subscription: subscriptions.Ticker, data: TradingPairTicker): From 32a179fc004026eab5d8491eb0875bfdd61502e3 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 17 Feb 2023 19:08:28 +0100 Subject: [PATCH 177/265] Add LICENSE.md file (Apache-V2). Edit setup.py with new arguments. Prepare to distribute on PyPI. --- LICENSE | 1 + setup.py | 37 ++++++++++++++++++++++++++----------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/LICENSE b/LICENSE index 2bb9ad2..4947287 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,4 @@ + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ diff --git a/setup.py b/setup.py index ce3ae3e..93bcd68 100644 --- a/setup.py +++ b/setup.py @@ -2,18 +2,36 @@ from distutils.core import setup setup( name="bitfinex-api-py", - version="3.0.0", + version="3.0.0b1", + description="Official Bitfinex Python API", + long_description="A Python reference implementation of the Bitfinex API for both REST and websocket interaction", + long_description_content_type="text/markdown", + url="https://github.com/bitfinexcom/bitfinex-api-py", + author="Bitfinex", + author_email="support@bitfinex.com", + license="Apache-2.0", + classifiers=[ + "Development Status :: 4 - Beta", + + "Intended Audience :: Developers", + "Topic :: Software Development :: Build Tools", + + "License :: OSI Approved :: Apache-2.0", + + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + ], + keywords="bitfinex,api,trading", + project_urls={ + "Bug Reports": "https://github.com/bitfinexcom/bitfinex-api-py/issues", + "Source": "https://github.com/bitfinexcom/bitfinex-api-py", + }, packages=[ "bfxapi", "bfxapi.utils", "bfxapi.websocket", "bfxapi.websocket.client", "bfxapi.websocket.handlers", "bfxapi.rest", "bfxapi.rest.endpoints", "bfxapi.rest.middleware", ], - url="https://github.com/bitfinexcom/bitfinex-api-py", - license="OSI Approved :: Apache Software License", - author="Bitfinex", - author_email="support@bitfinex.com", - description="Official Bitfinex Python API", - keywords="bitfinex,api,trading", install_requires=[ "certifi~=2022.12.7", "charset-normalizer~=2.1.1", @@ -29,8 +47,5 @@ setup( "urllib3~=1.26.13", "websockets~=10.4", ], - project_urls={ - "Bug Reports": "https://github.com/bitfinexcom/bitfinex-api-py/issues", - "Source": "https://github.com/bitfinexcom/bitfinex-api-py", - } + python_requires=">=3.8" ) \ No newline at end of file From f4c6a21ef490d1c8d2b19be8a1a707f83c1ef5c7 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 17 Feb 2023 20:23:59 +0100 Subject: [PATCH 178/265] Ws examples Co-Authored-By: itsdeka --- examples/websocket/derivatives_status.py | 23 ++++++++++++++++++ examples/websocket/trades.py | 29 +++++++++++++++++++++++ examples/websocket/wallet_balance.py | 30 ++++++++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 examples/websocket/derivatives_status.py create mode 100644 examples/websocket/trades.py create mode 100644 examples/websocket/wallet_balance.py diff --git a/examples/websocket/derivatives_status.py b/examples/websocket/derivatives_status.py new file mode 100644 index 0000000..3099431 --- /dev/null +++ b/examples/websocket/derivatives_status.py @@ -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() \ No newline at end of file diff --git a/examples/websocket/trades.py b/examples/websocket/trades.py new file mode 100644 index 0000000..0a5291f --- /dev/null +++ b/examples/websocket/trades.py @@ -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() \ No newline at end of file diff --git a/examples/websocket/wallet_balance.py b/examples/websocket/wallet_balance.py new file mode 100644 index 0000000..0e1b489 --- /dev/null +++ b/examples/websocket/wallet_balance.py @@ -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() \ No newline at end of file From 5914d7fa8c2785d802b6e0675e538eba370a0fdc Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 17 Feb 2023 20:50:31 +0100 Subject: [PATCH 179/265] Add dist to .gitignore file. --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index bfceb70..20870ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ __pycache__ + +dist venv !.gitkeep \ No newline at end of file From 3c760eacf69ab4e3ea25411b4b3835737db0f5c5 Mon Sep 17 00:00:00 2001 From: Dario Moceri <31732142+itsdeka@users.noreply.github.com> Date: Mon, 20 Feb 2023 11:14:34 +0100 Subject: [PATCH 180/265] Nonce generation must be the same of the previous version --- bfxapi/rest/middleware/middleware.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bfxapi/rest/middleware/middleware.py b/bfxapi/rest/middleware/middleware.py index 9180841..6260640 100644 --- a/bfxapi/rest/middleware/middleware.py +++ b/bfxapi/rest/middleware/middleware.py @@ -19,7 +19,7 @@ class Middleware(object): assert isinstance(self.API_KEY, str) and isinstance(self.API_SECRET, str), \ "API_KEY and API_SECRET must be both str to call __build_authentication_headers" - nonce = str(int(time.time()) * 1000) + nonce = int(round(time.time() * 1000000)) if data == None: path = f"/api/v2/{endpoint}{nonce}" @@ -79,4 +79,4 @@ class Middleware(object): if data[1] == None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC: raise UnknownGenericError(f"The server replied to the request with a generic error with message: <{data[2]}>.") - return data \ No newline at end of file + return data From cec9d6ba68e0a4d79925512393dad1c87598e843 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 20 Feb 2023 18:11:10 +0100 Subject: [PATCH 181/265] Rewrite, edit and organize examples/rest demos. --- .../endpoints/rest_authenticated_endpoints.py | 2 +- examples/.gitkeep | 0 examples/rest/authenticated/claim_position.py | 19 +++ examples/rest/authenticated/get_wallets.py | 44 +++++++ .../set_derivative_position_collateral.py | 36 +++++ .../authenticated/submit_funding_offer.py | 30 +++++ examples/rest/authenticated/submit_order.py | 42 ++++++ .../rest/authenticated/toggle_keep_funding.py | 24 ++++ examples/rest/claim_position.py | 19 --- examples/rest/create_funding_offer.py | 32 ----- examples/rest/create_order.py | 36 ----- examples/rest/derivatives.py | 31 ----- examples/rest/extra_calcs.py | 28 ---- examples/rest/funding_auto_renew.py | 21 --- examples/rest/get_authenticated_data.py | 123 ------------------ examples/rest/get_funding_info.py | 13 -- examples/rest/get_funding_trades_history.py | 13 -- examples/rest/get_liquidations.py | 14 -- examples/rest/get_positions.py | 23 ---- examples/rest/get_public_data.py | 52 -------- examples/rest/get_pulse_data.py | 22 ---- examples/rest/increase_position.py | 18 --- examples/rest/keep_taken_funding.py | 26 ---- examples/rest/merchant.py | 44 ------- examples/rest/merchant/submit_invoice.py | 43 ++++++ examples/rest/public/book.py | 24 ++++ examples/rest/public/conf.py | 18 +++ .../rest/{ => public}/get_candles_hist.py | 8 +- examples/rest/public/pulse_endpoints.py | 24 ++++ .../rest/public/rest_calculation_endpoints.py | 28 ++++ examples/rest/public/trades.py | 17 +++ examples/rest/return_taken_funding.py | 22 ---- examples/rest/transfer_wallet.py | 46 ------- 33 files changed, 353 insertions(+), 589 deletions(-) delete mode 100644 examples/.gitkeep create mode 100644 examples/rest/authenticated/claim_position.py create mode 100644 examples/rest/authenticated/get_wallets.py create mode 100644 examples/rest/authenticated/set_derivative_position_collateral.py create mode 100644 examples/rest/authenticated/submit_funding_offer.py create mode 100644 examples/rest/authenticated/submit_order.py create mode 100644 examples/rest/authenticated/toggle_keep_funding.py delete mode 100644 examples/rest/claim_position.py delete mode 100644 examples/rest/create_funding_offer.py delete mode 100644 examples/rest/create_order.py delete mode 100644 examples/rest/derivatives.py delete mode 100644 examples/rest/extra_calcs.py delete mode 100644 examples/rest/funding_auto_renew.py delete mode 100644 examples/rest/get_authenticated_data.py delete mode 100644 examples/rest/get_funding_info.py delete mode 100644 examples/rest/get_funding_trades_history.py delete mode 100644 examples/rest/get_liquidations.py delete mode 100644 examples/rest/get_positions.py delete mode 100644 examples/rest/get_public_data.py delete mode 100644 examples/rest/get_pulse_data.py delete mode 100644 examples/rest/increase_position.py delete mode 100644 examples/rest/keep_taken_funding.py delete mode 100644 examples/rest/merchant.py create mode 100644 examples/rest/merchant/submit_invoice.py create mode 100644 examples/rest/public/book.py create mode 100644 examples/rest/public/conf.py rename examples/rest/{ => public}/get_candles_hist.py (72%) create mode 100644 examples/rest/public/pulse_endpoints.py create mode 100644 examples/rest/public/rest_calculation_endpoints.py create mode 100644 examples/rest/public/trades.py delete mode 100644 examples/rest/return_taken_funding.py delete mode 100644 examples/rest/transfer_wallet.py diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index 7b4e11e..69c3737 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -201,7 +201,7 @@ class RestAuthenticatedEndpoints(Middleware): "rate": rate, "period": period })) - def toggle_keep(self, type: Literal["credit", "loan"], ids: Optional[List[int]] = None, changes: Optional[Dict[int, bool]] = None) -> Notification[Literal[None]]: + def toggle_keep_funding(self, type: Literal["credit", "loan"], ids: Optional[List[int]] = None, changes: Optional[Dict[int, Literal[1, 2]]] = None) -> Notification[Literal[None]]: return serializers._Notification[Literal[None]](None).parse(*self._POST("auth/w/funding/keep", body={ "type": type, "id": ids, diff --git a/examples/.gitkeep b/examples/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/examples/rest/authenticated/claim_position.py b/examples/rest/authenticated/claim_position.py new file mode 100644 index 0000000..538a650 --- /dev/null +++ b/examples/rest/authenticated/claim_position.py @@ -0,0 +1,19 @@ +# python -c "import examples.rest.authenticated.claim_position" + +import os + +from bfxapi import Client, REST_HOST + +from bfxapi.rest.types import Notification, PositionClaim + +bfx = Client( + REST_HOST=REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +# Claims all active positions +for position in bfx.rest.auth.get_positions(): + notification: Notification[PositionClaim] = bfx.rest.auth.claim_position(position.position_id) + claim: PositionClaim = notification.notify_info + print(f"Position: {position} | PositionClaim: {claim}") \ No newline at end of file diff --git a/examples/rest/authenticated/get_wallets.py b/examples/rest/authenticated/get_wallets.py new file mode 100644 index 0000000..8156b74 --- /dev/null +++ b/examples/rest/authenticated/get_wallets.py @@ -0,0 +1,44 @@ +# python -c "import examples.rest.authenticated.get_wallets" + +import os + +from bfxapi import Client, REST_HOST + +from bfxapi.rest.types import List, Wallet, Transfer, \ + DepositAddress, LightningNetworkInvoice, Withdrawal, \ + Notification + +bfx = Client( + REST_HOST=REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +# Gets all user's available wallets +wallets: List[Wallet] = bfx.rest.auth.get_wallets() + +# Transfers funds (0.001 ETH) from exchange wallet to funding wallet +A: Notification[Transfer] = bfx.rest.auth.transfer_between_wallets( + from_wallet="exchange", to_wallet="funding", from_currency="ETH", + to_currency="ETH", amount=0.001) + +print("Transfer:", A.notify_info) + +# Retrieves the deposit address for bitcoin currency in exchange wallet. +B: Notification[DepositAddress] = bfx.rest.auth.get_deposit_address( + wallet="exchange", method="bitcoin", renew=False) + +print("Deposit address:", B.notify_info) + +# Generates a lightning network deposit invoice +C: Notification[LightningNetworkInvoice] = bfx.rest.auth.generate_deposit_invoice( + wallet="funding", currency="LNX", amount=0.001) + +print("Lightning network invoice:", C.notify_info) + +# Withdraws 1.0 UST from user's exchange wallet to address 0x742d35Cc6634C0532925a3b844Bc454e4438f44e +D: Notification[Withdrawal] = bfx.rest.auth.submit_wallet_withdrawal( + wallet="exchange", method="tetheruse", address="0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + amount=1.0) + +print("Withdrawal:", D.notify_info) \ No newline at end of file diff --git a/examples/rest/authenticated/set_derivative_position_collateral.py b/examples/rest/authenticated/set_derivative_position_collateral.py new file mode 100644 index 0000000..822b5e0 --- /dev/null +++ b/examples/rest/authenticated/set_derivative_position_collateral.py @@ -0,0 +1,36 @@ +# python -c "import examples.rest.authenticated.set_derivatives_position_collateral" + +import os + +from bfxapi import Client, REST_HOST + +from bfxapi.rest.types import DerivativePositionCollateral, DerivativePositionCollateralLimits + +bfx = Client( + REST_HOST=REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +submit_order_notification = bfx.rest.auth.submit_order( + type="LIMIT", + symbol="tBTCF0:USTF0", + amount="0.015", + price="16700", + lev=10 +) + +print("New Order:", submit_order_notification.notify_info) + +# Update the amount of collateral for tBTCF0:USTF0 derivative position +derivative_position_collateral: DerivativePositionCollateral = \ + bfx.rest.auth.set_derivative_position_collateral(symbol="tBTCF0:USTF0", collateral=50.0) + +print("Status:", bool(derivative_position_collateral.status)) + +# Calculate the minimum and maximum collateral that can be assigned to tBTCF0:USTF0. +derivative_position_collateral_limits: DerivativePositionCollateralLimits = \ + bfx.rest.auth.get_derivative_position_collateral_limits(symbol="tBTCF0:USTF0") + +print(f"Minimum collateral: {derivative_position_collateral_limits.min_collateral} | \ + Maximum collateral: {derivative_position_collateral_limits.max_collateral}") \ No newline at end of file diff --git a/examples/rest/authenticated/submit_funding_offer.py b/examples/rest/authenticated/submit_funding_offer.py new file mode 100644 index 0000000..bcaedcd --- /dev/null +++ b/examples/rest/authenticated/submit_funding_offer.py @@ -0,0 +1,30 @@ +# python -c "import examples.rest.authenticated.submit_funding_offer" + +import os + +from bfxapi import Client, REST_HOST +from bfxapi.enums import FundingOfferType, Flag +from bfxapi.rest.types import Notification, FundingOffer + +bfx = Client( + REST_HOST=REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +# Submit a new funding offer +notification: Notification[FundingOffer] = bfx.rest.auth.submit_funding_offer( + type=FundingOfferType.LIMIT, + symbol="fUSD", + amount=123.45, + rate=0.001, + period=2, + flags=Flag.HIDDEN +) + +print("Funding Offer notification:", notification) + +# Get all fUSD active funding offers +offers = bfx.rest.auth.get_funding_offers(symbol="fUSD") + +print("Offers (fUSD):", offers) \ No newline at end of file diff --git a/examples/rest/authenticated/submit_order.py b/examples/rest/authenticated/submit_order.py new file mode 100644 index 0000000..36d06e2 --- /dev/null +++ b/examples/rest/authenticated/submit_order.py @@ -0,0 +1,42 @@ +# python -c "import examples.rest.authenticated.submit_order" + +import os + +from bfxapi import Client, REST_HOST +from bfxapi.enums import OrderType, Flag +from bfxapi.rest.types import Notification, Order + +bfx = Client( + REST_HOST=REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +# Submit a new order +submit_order_notification: Notification[Order] = bfx.rest.auth.submit_order( + type=OrderType.EXCHANGE_LIMIT, + symbol="tBTCUST", + amount=0.015, + price=10000, + flags=Flag.HIDDEN + Flag.OCO + Flag.CLOSE +) + +print("Submit order notification:", submit_order_notification) + +order: Order = submit_order_notification.notify_info + +# Update its amount and its price +update_order_notification: Notification[Order] = bfx.rest.auth.update_order( + id=order.id, + amount=0.020, + price=10150 +) + +print("Update order notification:", update_order_notification) + +# Cancel it by its ID +cancel_order_notification: Notification[Order] = bfx.rest.auth.cancel_order( + id=order.id +) + +print("Cancel order notification:", cancel_order_notification) \ No newline at end of file diff --git a/examples/rest/authenticated/toggle_keep_funding.py b/examples/rest/authenticated/toggle_keep_funding.py new file mode 100644 index 0000000..96304b7 --- /dev/null +++ b/examples/rest/authenticated/toggle_keep_funding.py @@ -0,0 +1,24 @@ +# python -c "import examples.rest.authenticated.toggle_keep_funding" + +import os + +from bfxapi import Client, REST_HOST + +from bfxapi.rest.types import List, FundingLoan, Notification + +bfx = Client( + REST_HOST=REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +loans: List[FundingLoan] = bfx.rest.auth.get_funding_loans(symbol="fUSD") + +# Set every loan's keep funding status to (1: , 2: ) +notification: Notification[None] = bfx.rest.auth.toggle_keep_funding( + funding_type="loan", + ids=[ loan.id for loan in loans ], + changes={ loan.id: 2 for loan in loans } +) + +print("Toggle keep funding notification:", notification) \ No newline at end of file diff --git a/examples/rest/claim_position.py b/examples/rest/claim_position.py deleted file mode 100644 index 084c9d0..0000000 --- a/examples/rest/claim_position.py +++ /dev/null @@ -1,19 +0,0 @@ -# python -c "import examples.rest.claim_position" - -import os - -from bfxapi.client import Client, REST_HOST - -bfx = Client( - REST_HOST=REST_HOST, - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET") -) - -open_margin_positions = bfx.rest.auth.get_positions() - -# claim all positions -for position in open_margin_positions: - print(f"Position {position}") - claim = bfx.rest.auth.claim_position(position.position_id, amount=0.000001) - print(f"PositionClaim {claim.notify_info}") \ No newline at end of file diff --git a/examples/rest/create_funding_offer.py b/examples/rest/create_funding_offer.py deleted file mode 100644 index f462325..0000000 --- a/examples/rest/create_funding_offer.py +++ /dev/null @@ -1,32 +0,0 @@ -# python -c "import examples.rest.create_funding_offer" - -import os - -from bfxapi.client import Client, REST_HOST -from bfxapi.enums import FundingOfferType, Flag - -bfx = Client( - REST_HOST=REST_HOST, - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET") -) - -notification = bfx.rest.auth.submit_funding_offer( - type=FundingOfferType.LIMIT, - symbol="fUSD", - amount="123.45", - rate="0.001", - period=2, - flags=Flag.HIDDEN -) - -print("Offer notification:", notification) - -offers = bfx.rest.auth.get_funding_offers(symbol="fUSD") - -print("Offers:", offers) - -# Cancel all funding offers -notification = bfx.rest.auth.cancel_all_funding_offers(currency="fUSD") - -print(notification) \ No newline at end of file diff --git a/examples/rest/create_order.py b/examples/rest/create_order.py deleted file mode 100644 index 607f7c9..0000000 --- a/examples/rest/create_order.py +++ /dev/null @@ -1,36 +0,0 @@ -# python -c "import examples.rest.create_order" - -import os -from bfxapi.client import Client, REST_HOST -from bfxapi.enums import OrderType, Flag - -bfx = Client( - REST_HOST=REST_HOST, - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET") -) - -# Create a new order -submitted_order = bfx.rest.auth.submit_order( - type=OrderType.EXCHANGE_LIMIT, - symbol="tBTCUST", - amount="0.015", - price="10000", - flags=Flag.HIDDEN + Flag.OCO + Flag.CLOSE -) - -print("Submit Order Notification:", submitted_order) - -# Update it -updated_order = bfx.rest.auth.update_order( - id=submitted_order.notify_info.id, - amount="0.020", - price="10100" -) - -print("Update Order Notification:", updated_order) - -# Delete it -canceled_order = bfx.rest.auth.cancel_order(id=submitted_order.notify_info.id) - -print("Cancel Order Notification:", canceled_order) diff --git a/examples/rest/derivatives.py b/examples/rest/derivatives.py deleted file mode 100644 index 58a0031..0000000 --- a/examples/rest/derivatives.py +++ /dev/null @@ -1,31 +0,0 @@ -# python -c "import examples.rest.derivatives" - -import os - -from bfxapi.client import Client, REST_HOST - -bfx = Client( - REST_HOST=REST_HOST, - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET") -) - -# Create a new order -submitted_order = bfx.rest.auth.submit_order( - symbol="tBTCF0:USTF0", - amount="0.015", - price="16700", - lev=10, - type="LIMIT" -) - -print("Submit Order Notification:", submitted_order) - -# Get position collateral limits -limits = bfx.rest.auth.get_derivative_position_collateral_limits(symbol="tBTCF0:USTF0") -print(f"Limits {limits}") - -# Update position collateral -response = bfx.rest.auth.set_derivative_position_collateral(symbol="tBTCF0:USTF0", collateral=50) -print(response.status) - diff --git a/examples/rest/extra_calcs.py b/examples/rest/extra_calcs.py deleted file mode 100644 index 8ef93cb..0000000 --- a/examples/rest/extra_calcs.py +++ /dev/null @@ -1,28 +0,0 @@ -# python -c "import examples.rest.extra_calcs" - -from bfxapi.client import Client, REST_HOST - -bfx = Client( - REST_HOST=REST_HOST -) - -t_symbol_response = bfx.rest.public.get_trading_market_average_price( - symbol="tBTCUSD", - amount=-100, - price_limit="20000.5" -) - -print(t_symbol_response.price_avg) - -f_symbol_response = bfx.rest.public.get_funding_market_average_price( - symbol="fUSD", - amount=100, - period=2, - rate_limit="0.00015" -) - -print(f_symbol_response.rate_avg) - -fx_rate = bfx.rest.public.get_fx_rate(ccy1="USD", ccy2="EUR") - -print(fx_rate.current_rate) \ No newline at end of file diff --git a/examples/rest/funding_auto_renew.py b/examples/rest/funding_auto_renew.py deleted file mode 100644 index f546707..0000000 --- a/examples/rest/funding_auto_renew.py +++ /dev/null @@ -1,21 +0,0 @@ -# python -c "import examples.rest.funding_auto_renew" - -import os - -from bfxapi.client import Client, REST_HOST - -bfx = Client( - REST_HOST=REST_HOST, - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET") -) - -notification = bfx.rest.auth.toggle_auto_renew( - status=True, - currency="USD", - amount="150", - rate="0", # FRR - period=2 -) - -print("Renew toggle notification:", notification) \ No newline at end of file diff --git a/examples/rest/get_authenticated_data.py b/examples/rest/get_authenticated_data.py deleted file mode 100644 index c3226af..0000000 --- a/examples/rest/get_authenticated_data.py +++ /dev/null @@ -1,123 +0,0 @@ -# python -c "import examples.rest.get_authenticated_data" - -import os -import time - -from bfxapi.client import Client, REST_HOST - -bfx = Client( - REST_HOST=REST_HOST, - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET") -) - -now = int(round(time.time() * 1000)) - - -def log_user_info(): - user_info = bfx.rest.auth.get_user_info() - print(user_info) - - -def log_login_history(): - login_history = bfx.rest.auth.get_login_history() - print(login_history) - - -def log_wallets(): - wallets = bfx.rest.auth.get_wallets() - print("Wallets:") - [print(w) for w in wallets] - - -def log_orders(): - orders = bfx.rest.auth.get_orders(symbol='tBTCUSD') - print("Orders:") - [print(o) for o in orders] - - -def log_orders_history(): - orders = bfx.rest.auth.get_orders_history(symbol='tBTCUSD', start=0, end=now) - print("Orders:") - [print(o) for o in orders] - - -def log_positions(): - positions = bfx.rest.auth.get_positions() - print("Positions:") - [print(p) for p in positions] - - -def log_trades(): - trades = bfx.rest.auth.get_trades_history(symbol='tBTCUSD', start=0, end=now) - print("Trades:") - [print(t) for t in trades] - - -def log_order_trades(): - order_id = 82406909127 - trades = bfx.rest.auth.get_order_trades(symbol='tBTCUSD', id=order_id) - print("Trade orders:") - [print(t) for t in trades] - - -def log_funding_offers(): - offers = bfx.rest.auth.get_funding_offers(symbol='fUSD') - print("Offers:") - [print(o) for o in offers] - - -def log_funding_offer_history(): - offers = bfx.rest.auth.get_funding_offers_history(symbol='fUSD', start=0, end=now) - print("Offers history:") - [print(o) for o in offers] - - -def log_funding_loans(): - loans = bfx.rest.auth.get_funding_loans(symbol='fUSD') - print("Funding loans:") - [print(l) for l in loans] - - -def log_funding_loans_history(): - loans = bfx.rest.auth.get_funding_loans_history(symbol='fUSD', start=0, end=now) - print("Funding loan history:") - [print(l) for l in loans] - - -def log_funding_credits(): - credits = bfx.rest.auth.get_funding_credits(symbol='fUSD') - print("Funding credits:") - [print(c) for c in credits] - - -def log_funding_credits_history(): - credit = bfx.rest.auth.get_funding_credits_history(symbol='fUSD', start=0, end=now) - print("Funding credit history:") - [print(c) for c in credit] - -def log_margin_info(): - btcusd_margin_info = bfx.rest.auth.get_symbol_margin_info('tBTCUSD') - print(f"tBTCUSD margin info {btcusd_margin_info}") - - sym_all_margin_info = bfx.rest.auth.get_all_symbols_margin_info() - print(f"Sym all margin info {sym_all_margin_info}") - - base_margin_info = bfx.rest.auth.get_base_margin_info() - print(f"Base margin info {base_margin_info}") - -def run(): - log_user_info() - log_wallets() - log_orders() - log_orders_history() - log_positions() - log_trades() - log_order_trades() - log_funding_offers() - log_funding_offer_history() - log_funding_credits() - log_funding_credits_history() - log_margin_info() - -run() \ No newline at end of file diff --git a/examples/rest/get_funding_info.py b/examples/rest/get_funding_info.py deleted file mode 100644 index 82bf150..0000000 --- a/examples/rest/get_funding_info.py +++ /dev/null @@ -1,13 +0,0 @@ -# python -c "import examples.rest.get_funding_info" - -import os - -from bfxapi.client import Client, REST_HOST - -bfx = Client( - REST_HOST=REST_HOST, - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET") -) - -print(bfx.rest.auth.get_funding_info(key="fUSD")) \ No newline at end of file diff --git a/examples/rest/get_funding_trades_history.py b/examples/rest/get_funding_trades_history.py deleted file mode 100644 index 3af19d8..0000000 --- a/examples/rest/get_funding_trades_history.py +++ /dev/null @@ -1,13 +0,0 @@ -# python -c "import examples.rest.get_funding_trades_history" - -import os - -from bfxapi.client import Client, REST_HOST - -bfx = Client( - REST_HOST=REST_HOST, - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET") -) - -print(bfx.rest.auth.get_funding_trades_history()) \ No newline at end of file diff --git a/examples/rest/get_liquidations.py b/examples/rest/get_liquidations.py deleted file mode 100644 index 588c83a..0000000 --- a/examples/rest/get_liquidations.py +++ /dev/null @@ -1,14 +0,0 @@ -# python -c "import examples.rest.get_liquidations" - -import time - -from bfxapi.client import Client, REST_HOST - -bfx = Client( - REST_HOST=REST_HOST -) - -now = int(round(time.time() * 1000)) - -liquidations = bfx.rest.public.get_liquidations(start=0, end=now) -print(f"Liquidations: {liquidations}") \ No newline at end of file diff --git a/examples/rest/get_positions.py b/examples/rest/get_positions.py deleted file mode 100644 index 62cd309..0000000 --- a/examples/rest/get_positions.py +++ /dev/null @@ -1,23 +0,0 @@ -# python -c "import examples.rest.get_positions" - -import os -import time - -from bfxapi.client import Client, REST_HOST - -bfx = Client( - REST_HOST=REST_HOST, - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET") -) - -now = int(round(time.time() * 1000)) - -positions_snapshot = bfx.rest.auth.get_positions_snapshot(end=now, limit=50) -print(positions_snapshot) - -positions_history = bfx.rest.auth.get_positions_history(end=now, limit=50) -print(positions_history) - -positions_audit = bfx.rest.auth.get_positions_audit(end=now, limit=50) -print(positions_audit) \ No newline at end of file diff --git a/examples/rest/get_public_data.py b/examples/rest/get_public_data.py deleted file mode 100644 index 125f97c..0000000 --- a/examples/rest/get_public_data.py +++ /dev/null @@ -1,52 +0,0 @@ -# python -c "import examples.rest.get_public_data" - -import time - -from bfxapi.client import Client, REST_HOST - -bfx = Client( - REST_HOST=REST_HOST -) - -now = int(round(time.time() * 1000)) - - -def log_historical_candles(): - candles = bfx.rest.public.get_candles_hist(start=0, end=now, resource='trade:1m:tBTCUSD') - print("Candles:") - [print(c) for c in candles] - - -def log_historical_trades(): - trades = bfx.rest.public.get_t_trades(pair='tBTCUSD', start=0, end=now) - print("Trades:") - [print(t) for t in trades] - - -def log_books(): - orders = bfx.rest.public.get_t_book(pair='BTCUSD', precision='P0') - print("Order book:") - [print(o) for o in orders] - - -def log_tickers(): - tickers = bfx.rest.public.get_t_tickers(pairs=['BTCUSD']) - print("Tickers:") - print(tickers) - - -def log_derivative_status(): - status = bfx.rest.public.get_derivatives_status('ALL') - print("Deriv status:") - print(status) - - -def run(): - log_historical_candles() - log_historical_trades() - log_books() - log_tickers() - log_derivative_status() - - -run() \ No newline at end of file diff --git a/examples/rest/get_pulse_data.py b/examples/rest/get_pulse_data.py deleted file mode 100644 index b0cc369..0000000 --- a/examples/rest/get_pulse_data.py +++ /dev/null @@ -1,22 +0,0 @@ -# python -c "import examples.rest.get_pulse_data" - -import time - -from bfxapi.client import Client, REST_HOST - -bfx = Client( - REST_HOST=REST_HOST -) - -now = int(round(time.time() * 1000)) - -messages = bfx.rest.public.get_pulse_history(end=now, limit=100) - -for message in messages: - print(f"Message: {message}") - print(message.content) - print(message.profile.picture) - -profile = bfx.rest.public.get_pulse_profile("News") -print(f"Profile: {profile}") -print(f"Profile picture: {profile.picture}") \ No newline at end of file diff --git a/examples/rest/increase_position.py b/examples/rest/increase_position.py deleted file mode 100644 index add66e3..0000000 --- a/examples/rest/increase_position.py +++ /dev/null @@ -1,18 +0,0 @@ -# python -c "import examples.rest.increase_position" - -import os - -from bfxapi.client import Client, REST_HOST - -bfx = Client( - REST_HOST=REST_HOST, - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET") -) - -increase_info = bfx.rest.auth.get_increase_position_info(symbol="tBTCUSD", amount=0.0001) -print(increase_info) - -# increase a margin position -notification = bfx.rest.auth.increase_position(symbol="tBTCUSD", amount=0.0001) -print(notification.notify_info) diff --git a/examples/rest/keep_taken_funding.py b/examples/rest/keep_taken_funding.py deleted file mode 100644 index 21e60f4..0000000 --- a/examples/rest/keep_taken_funding.py +++ /dev/null @@ -1,26 +0,0 @@ -# python -c "import examples.rest.keep_taken_funding" - -import os - -from bfxapi.client import Client, REST_HOST - -bfx = Client( - REST_HOST=REST_HOST, - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET") -) - -loans = bfx.rest.auth.get_funding_loans(symbol="fUSD") - -for loan in loans: - print(f"Loan {loan}") - - notification = bfx.rest.auth.toggle_keep( - funding_type="loan", - ids=[loan.id], - changes={ - loan.id: 2 # (1 if true, 2 if false) - } - ) - - print("Funding keep notification:", notification) \ No newline at end of file diff --git a/examples/rest/merchant.py b/examples/rest/merchant.py deleted file mode 100644 index 84d9b9b..0000000 --- a/examples/rest/merchant.py +++ /dev/null @@ -1,44 +0,0 @@ -# python -c "import examples.rest.merchant" - -import os - -from bfxapi.client import Client, REST_HOST - -bfx = Client( - REST_HOST=REST_HOST, - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET") -) - -customer_info = { - "nationality": "GB", - "resid_country": "DE", - "resid_city": "Berlin", - "resid_zip_code": 1, - "resid_street": "Timechain", - "full_name": "Satoshi", - "email": "satoshi3@bitfinex.com" -} - -invoice = bfx.rest.merchant.submit_invoice( - amount=1, - currency="USD", - duration=864000, - order_id="order123", - customer_info=customer_info, - pay_currencies=["ETH"] -) - -print(bfx.rest.merchant.get_invoices()) - -print(bfx.rest.merchant.get_invoice_count_stats(status="CREATED", format="Y")) - -print(bfx.rest.merchant.get_invoice_earning_stats(currency="USD", format="Y")) - -print(bfx.rest.merchant.get_currency_conversion_list()) - -print(bfx.rest.merchant.complete_invoice( - id=invoice.id, - pay_currency="ETH", - deposit_id=1 -)) \ No newline at end of file diff --git a/examples/rest/merchant/submit_invoice.py b/examples/rest/merchant/submit_invoice.py new file mode 100644 index 0000000..cb71dce --- /dev/null +++ b/examples/rest/merchant/submit_invoice.py @@ -0,0 +1,43 @@ +# python -c "import examples.rest.merchant.submit_invoice" + +import os + +from bfxapi import Client, REST_HOST + +from bfxapi.rest.types import InvoiceSubmission + +bfx = Client( + REST_HOST=REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +customer_info = { + "nationality": "DE", + "residCountry": "GB", + "residCity": "London", + "residZipCode": "WC2H 7NA", + "residStreet": "5-6 Leicester Square", + "residBuildingNo": "23 A", + "fullName": "John Doe", + "email": "john@example.com" +} + +invoice: InvoiceSubmission = bfx.rest.merchant.submit_invoice( + amount=1.0, + currency="USD", + order_id="test", + customer_info=customer_info, + pay_currencies=["ETH"], + duration=86400 * 10 +) + +print("Invoice submission:", invoice) + +print(bfx.rest.merchant.complete_invoice( + id=invoice.id, + pay_currency="ETH", + deposit_id=1 +)) + +print(bfx.rest.merchant.get_invoices(limit=25)) \ No newline at end of file diff --git a/examples/rest/public/book.py b/examples/rest/public/book.py new file mode 100644 index 0000000..b5f1a64 --- /dev/null +++ b/examples/rest/public/book.py @@ -0,0 +1,24 @@ +# python -c "import examples.rest.public.book" + +from bfxapi import Client, PUB_REST_HOST + +from bfxapi.rest.types import List, TradingPairBook, TradingPairRawBook, \ + FundingCurrencyBook, FundingCurrencyRawBook + +bfx = Client(REST_HOST=PUB_REST_HOST) + +t_book: List[TradingPairBook] = bfx.rest.public.get_t_book("tBTCUSD", precision="P0", len=25) + +print("25 price points of tBTCUSD order book (with precision P0):", t_book) + +t_raw_book: List[TradingPairRawBook] = bfx.rest.public.get_t_raw_book("tBTCUSD") + +print("tBTCUSD raw order book:", t_raw_book) + +f_book: List[FundingCurrencyBook] = bfx.rest.public.get_f_book("fUSD", precision="P0", len=25) + +print("25 price points of fUSD order book (with precision P0):", f_book) + +f_raw_book: List[FundingCurrencyRawBook] = bfx.rest.public.get_f_raw_book("fUSD") + +print("fUSD raw order book:", f_raw_book) \ No newline at end of file diff --git a/examples/rest/public/conf.py b/examples/rest/public/conf.py new file mode 100644 index 0000000..8c04ae9 --- /dev/null +++ b/examples/rest/public/conf.py @@ -0,0 +1,18 @@ +# python -c "import examples.rest.public.conf" + +from bfxapi import Client, PUB_REST_HOST + +from bfxapi.rest.enums import Config + +bfx = Client(REST_HOST=PUB_REST_HOST) + +print("Available configs:", [ config.value for config in Config ]) + +# Prints a map from symbols to their API symbols (pub:map:currency:sym) +print (bfx.rest.public.conf(Config.MAP_CURRENCY_SYM)) + +# Prints all the available exchange trading pairs (pub:list:pair:exchange) +print(bfx.rest.public.conf(Config.LIST_PAIR_EXCHANGE)) + +# Prints all the available funding currencies (pub:list:currency) +print(bfx.rest.public.conf(Config.LIST_CURRENCY)) \ No newline at end of file diff --git a/examples/rest/get_candles_hist.py b/examples/rest/public/get_candles_hist.py similarity index 72% rename from examples/rest/get_candles_hist.py rename to examples/rest/public/get_candles_hist.py index d8d9881..a1a4e8f 100644 --- a/examples/rest/get_candles_hist.py +++ b/examples/rest/public/get_candles_hist.py @@ -1,10 +1,8 @@ -# python -c "import examples.rest.get_candles_hist" +# python -c "import examples.rest.public.get_candles_hist" -from bfxapi.client import Client, REST_HOST +from bfxapi import Client, PUB_REST_HOST -bfx = Client( - REST_HOST=REST_HOST -) +bfx = Client(REST_HOST=PUB_REST_HOST) print(f"Candles: {bfx.rest.public.get_candles_hist(symbol='tBTCUSD')}") diff --git a/examples/rest/public/pulse_endpoints.py b/examples/rest/public/pulse_endpoints.py new file mode 100644 index 0000000..462c955 --- /dev/null +++ b/examples/rest/public/pulse_endpoints.py @@ -0,0 +1,24 @@ +# python -c "import examples.rest.public.pulse_endpoints" + +import datetime + +from bfxapi import Client, PUB_REST_HOST + +from bfxapi.rest.types import List, PulseMessage, PulseProfile + +bfx = Client(REST_HOST=PUB_REST_HOST) + +# POSIX timestamp in milliseconds (check https://currentmillis.com/) +end = datetime.datetime(2020, 5, 2).timestamp() * 1000 + +# Retrieves 25 pulse messages up to 2020/05/02 +messages: List[PulseMessage] = bfx.rest.public.get_pulse_history(end=end, limit=25) + +for message in messages: + print(f"Message author: {message.profile.nickname} ({message.profile.puid})") + print(f"Title: <{message.title}>") + print(f"Tags: {message.tags}\n") + +profile: PulseProfile = bfx.rest.public.get_pulse_profile("News") +URL = profile.picture.replace("size", "small") +print(f"<{profile.nickname}>'s profile picture: https://s3-eu-west-1.amazonaws.com/bfx-pub/{URL}") \ No newline at end of file diff --git a/examples/rest/public/rest_calculation_endpoints.py b/examples/rest/public/rest_calculation_endpoints.py new file mode 100644 index 0000000..6022753 --- /dev/null +++ b/examples/rest/public/rest_calculation_endpoints.py @@ -0,0 +1,28 @@ +# python -c "import examples.rest.public.rest_calculation_endpoints" + +from bfxapi import Client, PUB_REST_HOST + +from bfxapi.rest.types import TradingMarketAveragePrice, FundingMarketAveragePrice, FxRate + +bfx = Client(REST_HOST=PUB_REST_HOST) + +trading_market_average_price: TradingMarketAveragePrice = bfx.rest.public.get_trading_market_average_price( + symbol="tBTCUSD", + amount=-100, + price_limit=20000.5 +) + +print("Average execution price for tBTCUSD:", trading_market_average_price.price_avg) + +funding_market_average_price: FundingMarketAveragePrice = bfx.rest.public.get_funding_market_average_price( + symbol="fUSD", + amount=100, + period=2, + rate_limit=0.00015 +) + +print("Average execution rate for fUSD:", funding_market_average_price.rate_avg) + +fx_rate: FxRate = bfx.rest.public.get_fx_rate(ccy1="USD", ccy2="EUR") + +print("Exchange rate between USD and EUR:", fx_rate.current_rate) \ No newline at end of file diff --git a/examples/rest/public/trades.py b/examples/rest/public/trades.py new file mode 100644 index 0000000..4dbf77e --- /dev/null +++ b/examples/rest/public/trades.py @@ -0,0 +1,17 @@ +# python -c "import examples.rest.public.trades" + +from bfxapi import Client, PUB_REST_HOST +from bfxapi.rest.enums import Sort +from bfxapi.rest.types import List, TradingPairTrade, FundingCurrencyTrade + +bfx = Client(REST_HOST=PUB_REST_HOST) + +t_trades: List[TradingPairTrade] = bfx.rest.public.get_t_trades("tBTCUSD", \ + limit=15, sort=Sort.ASCENDING) + +print("Latest 15 trades for tBTCUSD (in ascending order):", t_trades) + +f_trades: List[FundingCurrencyTrade] = bfx.rest.public.get_f_trades("fUSD", \ + limit=15, sort=Sort.DESCENDING) + +print("Latest 15 trades for fUSD (in descending order):", f_trades) \ No newline at end of file diff --git a/examples/rest/return_taken_funding.py b/examples/rest/return_taken_funding.py deleted file mode 100644 index 73d5a33..0000000 --- a/examples/rest/return_taken_funding.py +++ /dev/null @@ -1,22 +0,0 @@ -# python -c "import examples.rest.return_taken_funding" - -import os - -from bfxapi.client import Client, REST_HOST - -bfx = Client( - REST_HOST=REST_HOST, - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET") -) - -loans = bfx.rest.auth.get_funding_loans(symbol="fUSD") - -for loan in loans: - print(f"Loan {loan}") - - notification = bfx.rest.auth.submit_funding_close( - id=loan.id - ) - - print("Funding close notification:", notification) \ No newline at end of file diff --git a/examples/rest/transfer_wallet.py b/examples/rest/transfer_wallet.py deleted file mode 100644 index 9384bd8..0000000 --- a/examples/rest/transfer_wallet.py +++ /dev/null @@ -1,46 +0,0 @@ -# python -c "import examples.rest.transfer_wallet" - -import os - -from bfxapi.client import Client, REST_HOST - -bfx = Client( - REST_HOST=REST_HOST, - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET") -) - -def transfer_wallet(): - response = bfx.rest.auth.transfer_between_wallets(from_wallet="exchange", to_wallet="funding", from_currency="ETH", to_currency="ETH", amount=0.001) - print("Transfer:", response.notify_info) - -def get_existing_deposit_address(): - response = bfx.rest.auth.get_deposit_address(wallet="exchange", method="bitcoin", renew=False) - print("Address:", response.notify_info) - -def create_new_deposit_address(): - response = bfx.rest.auth.get_deposit_address(wallet="exchange", method="bitcoin", renew=True) - print("Address:", response.notify_info) - -def withdraw(): - # tetheruse = Tether (ERC20) - response = bfx.rest.auth.submit_wallet_withdrawal(wallet="exchange", method="tetheruse", amount=1, address="0x742d35Cc6634C0532925a3b844Bc454e4438f44e") - print("Address:", response.notify_info) - -def create_lighting_network_deposit_address(): - invoice = bfx.rest.auth.generate_deposit_invoice(wallet="funding", currency="LNX", amount=0.001) - print("Invoice:", invoice) - -def get_movements(): - movements = bfx.rest.auth.get_movements(currency="BTC") - print("Movements:", movements) - -def run(): - transfer_wallet() - get_existing_deposit_address() - create_new_deposit_address() - withdraw() - create_lighting_network_deposit_address() - get_movements() - -run() \ No newline at end of file From 6f61b983a525feee1e24cb8f12e6d563454efd2b Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 20 Feb 2023 18:22:57 +0100 Subject: [PATCH 182/265] Rewrite, edit and organize examples/websocket demos. --- bfxapi/rest/types.py | 2 +- bfxapi/websocket/types.py | 2 +- .../submit_order.py} | 8 ++--- examples/websocket/authenticated/wallets.py | 30 +++++++++++++++++++ .../{ => public}/derivatives_status.py | 2 +- examples/websocket/{ => public}/order_book.py | 2 +- .../websocket/{ => public}/raw_order_book.py | 2 +- examples/websocket/{ => public}/ticker.py | 2 +- examples/websocket/{ => public}/trades.py | 2 +- examples/websocket/wallet_balance.py | 30 ------------------- 10 files changed, 41 insertions(+), 41 deletions(-) rename examples/websocket/{create_order.py => authenticated/submit_order.py} (83%) create mode 100644 examples/websocket/authenticated/wallets.py rename examples/websocket/{ => public}/derivatives_status.py (90%) rename examples/websocket/{ => public}/order_book.py (97%) rename examples/websocket/{ => public}/raw_order_book.py (96%) rename examples/websocket/{ => public}/ticker.py (90%) rename examples/websocket/{ => public}/trades.py (93%) delete mode 100644 examples/websocket/wallet_balance.py diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index e1c39af..032416d 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -1,4 +1,4 @@ -from typing import List, Dict, Optional, Literal, Any +from typing import * from dataclasses import dataclass diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py index 063836a..ae082af 100644 --- a/bfxapi/websocket/types.py +++ b/bfxapi/websocket/types.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import * from dataclasses import dataclass diff --git a/examples/websocket/create_order.py b/examples/websocket/authenticated/submit_order.py similarity index 83% rename from examples/websocket/create_order.py rename to examples/websocket/authenticated/submit_order.py index 48f4957..ca671be 100644 --- a/examples/websocket/create_order.py +++ b/examples/websocket/authenticated/submit_order.py @@ -1,9 +1,9 @@ -# python -c "import examples.websocket.create_order" +# python -c "import examples.websocket.authenticated.submit_order" import os -from bfxapi.client import Client, WSS_HOST -from bfxapi.websocket.enums import Error, OrderType +from bfxapi import Client, WSS_HOST +from bfxapi.enums import Error, OrderType from bfxapi.websocket.types import Notification, Order bfx = Client( @@ -18,7 +18,7 @@ def on_wss_error(code: Error, msg: str): @bfx.wss.on("authenticated") async def on_authenticated(event): - print(f"Authentication: {event}.") + print(f"Authentication: {event}") await bfx.wss.inputs.submit_order( type=OrderType.EXCHANGE_LIMIT, diff --git a/examples/websocket/authenticated/wallets.py b/examples/websocket/authenticated/wallets.py new file mode 100644 index 0000000..039364a --- /dev/null +++ b/examples/websocket/authenticated/wallets.py @@ -0,0 +1,30 @@ +# python -c "import examples.websocket.authenticated.wallets" + +import os + +from bfxapi import Client +from bfxapi.enums import Error +from bfxapi.websocket.types import List, Wallet + +bfx = Client( + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET"), + filter=["wallet"] +) + +@bfx.wss.on("wss-error") +def on_wss_error(code: Error, msg: str): + print(code, msg) + +@bfx.wss.on("wallet_snapshot") +def on_wallet_snapshot(wallets: List[Wallet]): + for wallet in wallets: + print(f"Wallet: {wallet.wallet_type} | {wallet.currency}") + print(f"Available balance: {wallet.available_balance}") + print(f"Wallet trade details: {wallet.trade_details}") + +@bfx.wss.on("wallet_update") +def on_wallet_update(wallet: Wallet): + print(f"Wallet update: {wallet}") + +bfx.wss.run() \ No newline at end of file diff --git a/examples/websocket/derivatives_status.py b/examples/websocket/public/derivatives_status.py similarity index 90% rename from examples/websocket/derivatives_status.py rename to examples/websocket/public/derivatives_status.py index 3099431..05a9556 100644 --- a/examples/websocket/derivatives_status.py +++ b/examples/websocket/public/derivatives_status.py @@ -1,4 +1,4 @@ -# python -c "import examples.websocket.derivatives_status" +# python -c "import examples.websocket.public.derivatives_status" from bfxapi import Client, PUB_WSS_HOST from bfxapi.websocket.enums import Error, Channel diff --git a/examples/websocket/order_book.py b/examples/websocket/public/order_book.py similarity index 97% rename from examples/websocket/order_book.py rename to examples/websocket/public/order_book.py index 55e4ae3..536ce7c 100644 --- a/examples/websocket/order_book.py +++ b/examples/websocket/public/order_book.py @@ -1,4 +1,4 @@ -# python -c "import examples.websocket.order_book" +# python -c "import examples.websocket.public.order_book" from collections import OrderedDict diff --git a/examples/websocket/raw_order_book.py b/examples/websocket/public/raw_order_book.py similarity index 96% rename from examples/websocket/raw_order_book.py rename to examples/websocket/public/raw_order_book.py index 3ce9c6d..7909a61 100644 --- a/examples/websocket/raw_order_book.py +++ b/examples/websocket/public/raw_order_book.py @@ -1,4 +1,4 @@ -# python -c "import examples.websocket.raw_order_book" +# python -c "import examples.websocket.public.raw_order_book" from collections import OrderedDict diff --git a/examples/websocket/ticker.py b/examples/websocket/public/ticker.py similarity index 90% rename from examples/websocket/ticker.py rename to examples/websocket/public/ticker.py index d4b4c91..b2eadc8 100644 --- a/examples/websocket/ticker.py +++ b/examples/websocket/public/ticker.py @@ -1,4 +1,4 @@ -# python -c "import examples.websocket.ticker" +# python -c "import examples.websocket.public.ticker" from bfxapi import Client, PUB_WSS_HOST diff --git a/examples/websocket/trades.py b/examples/websocket/public/trades.py similarity index 93% rename from examples/websocket/trades.py rename to examples/websocket/public/trades.py index 0a5291f..186c46b 100644 --- a/examples/websocket/trades.py +++ b/examples/websocket/public/trades.py @@ -1,4 +1,4 @@ -# python -c "import examples.websocket.trades" +# python -c "import examples.websocket.public.trades" from bfxapi import Client, PUB_WSS_HOST from bfxapi.websocket.enums import Error, Channel diff --git a/examples/websocket/wallet_balance.py b/examples/websocket/wallet_balance.py deleted file mode 100644 index 0e1b489..0000000 --- a/examples/websocket/wallet_balance.py +++ /dev/null @@ -1,30 +0,0 @@ -# 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() \ No newline at end of file From 442155b48eb5993c2ac7c9d519ce898b953b2615 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 20 Feb 2023 18:29:08 +0100 Subject: [PATCH 183/265] Rename .notify_info to .data in Notification (bfxapi.notifications). --- bfxapi/notification.py | 14 +++++++------- examples/rest/authenticated/claim_position.py | 2 +- examples/rest/authenticated/get_wallets.py | 8 ++++---- .../set_derivative_position_collateral.py | 2 +- examples/rest/authenticated/submit_order.py | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/bfxapi/notification.py b/bfxapi/notification.py index bf4818a..5872c9a 100644 --- a/bfxapi/notification.py +++ b/bfxapi/notification.py @@ -9,13 +9,13 @@ class Notification(_Type, Generic[T]): mts: int type: str message_id: Optional[int] - notify_info: T + data: T code: Optional[int] status: str text: str class _Notification(_Serializer, Generic[T]): - __LABELS = [ "mts", "type", "message_id", "_PLACEHOLDER", "notify_info", "code", "status", "text" ] + __LABELS = [ "mts", "type", "message_id", "_PLACEHOLDER", "data", "code", "status", "text" ] def __init__(self, serializer: Optional[_Serializer] = None, is_iterable: bool = False): super().__init__("Notification", Notification, _Notification.__LABELS, IGNORE = [ "_PLACEHOLDER" ]) @@ -26,13 +26,13 @@ class _Notification(_Serializer, Generic[T]): notification = cast(Notification[T], Notification(**dict(self._serialize(*values)))) if isinstance(self.serializer, _Serializer): - NOTIFY_INFO = cast(List[Any], notification.notify_info) + data = cast(List[Any], notification.data) if self.is_iterable == False: - if len(NOTIFY_INFO) == 1 and isinstance(NOTIFY_INFO[0], list): - NOTIFY_INFO = NOTIFY_INFO[0] + if len(data) == 1 and isinstance(data[0], list): + data = data[0] - notification.notify_info = cast(T, self.serializer.klass(**dict(self.serializer._serialize(*NOTIFY_INFO, skip=skip)))) - else: notification.notify_info = cast(T, [ self.serializer.klass(**dict(self.serializer._serialize(*data, skip=skip))) for data in NOTIFY_INFO ]) + notification.data = cast(T, self.serializer.klass(**dict(self.serializer._serialize(*data, skip=skip)))) + else: notification.data = cast(T, [ self.serializer.klass(**dict(self.serializer._serialize(*sub_data, skip=skip))) for sub_data in data ]) return notification \ No newline at end of file diff --git a/examples/rest/authenticated/claim_position.py b/examples/rest/authenticated/claim_position.py index 538a650..f3a91e4 100644 --- a/examples/rest/authenticated/claim_position.py +++ b/examples/rest/authenticated/claim_position.py @@ -15,5 +15,5 @@ bfx = Client( # Claims all active positions for position in bfx.rest.auth.get_positions(): notification: Notification[PositionClaim] = bfx.rest.auth.claim_position(position.position_id) - claim: PositionClaim = notification.notify_info + claim: PositionClaim = notification.data print(f"Position: {position} | PositionClaim: {claim}") \ No newline at end of file diff --git a/examples/rest/authenticated/get_wallets.py b/examples/rest/authenticated/get_wallets.py index 8156b74..00dc129 100644 --- a/examples/rest/authenticated/get_wallets.py +++ b/examples/rest/authenticated/get_wallets.py @@ -22,23 +22,23 @@ A: Notification[Transfer] = bfx.rest.auth.transfer_between_wallets( from_wallet="exchange", to_wallet="funding", from_currency="ETH", to_currency="ETH", amount=0.001) -print("Transfer:", A.notify_info) +print("Transfer:", A.data) # Retrieves the deposit address for bitcoin currency in exchange wallet. B: Notification[DepositAddress] = bfx.rest.auth.get_deposit_address( wallet="exchange", method="bitcoin", renew=False) -print("Deposit address:", B.notify_info) +print("Deposit address:", B.data) # Generates a lightning network deposit invoice C: Notification[LightningNetworkInvoice] = bfx.rest.auth.generate_deposit_invoice( wallet="funding", currency="LNX", amount=0.001) -print("Lightning network invoice:", C.notify_info) +print("Lightning network invoice:", C.data) # Withdraws 1.0 UST from user's exchange wallet to address 0x742d35Cc6634C0532925a3b844Bc454e4438f44e D: Notification[Withdrawal] = bfx.rest.auth.submit_wallet_withdrawal( wallet="exchange", method="tetheruse", address="0x742d35Cc6634C0532925a3b844Bc454e4438f44e", amount=1.0) -print("Withdrawal:", D.notify_info) \ No newline at end of file +print("Withdrawal:", D.data) \ No newline at end of file diff --git a/examples/rest/authenticated/set_derivative_position_collateral.py b/examples/rest/authenticated/set_derivative_position_collateral.py index 822b5e0..c5f82d2 100644 --- a/examples/rest/authenticated/set_derivative_position_collateral.py +++ b/examples/rest/authenticated/set_derivative_position_collateral.py @@ -20,7 +20,7 @@ submit_order_notification = bfx.rest.auth.submit_order( lev=10 ) -print("New Order:", submit_order_notification.notify_info) +print("New Order:", submit_order_notification.data) # Update the amount of collateral for tBTCF0:USTF0 derivative position derivative_position_collateral: DerivativePositionCollateral = \ diff --git a/examples/rest/authenticated/submit_order.py b/examples/rest/authenticated/submit_order.py index 36d06e2..b831c80 100644 --- a/examples/rest/authenticated/submit_order.py +++ b/examples/rest/authenticated/submit_order.py @@ -23,7 +23,7 @@ submit_order_notification: Notification[Order] = bfx.rest.auth.submit_order( print("Submit order notification:", submit_order_notification) -order: Order = submit_order_notification.notify_info +order: Order = submit_order_notification.data # Update its amount and its price update_order_notification: Notification[Order] = bfx.rest.auth.update_order( From b06476e0d4e7af42d2d4226ad44a21e754466acf Mon Sep 17 00:00:00 2001 From: itsdeka Date: Mon, 27 Feb 2023 19:55:53 +0100 Subject: [PATCH 184/265] nonce must be str --- bfxapi/rest/middleware/middleware.py | 2 +- bfxapi/websocket/client/bfx_websocket_client.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bfxapi/rest/middleware/middleware.py b/bfxapi/rest/middleware/middleware.py index 1e77a71..01f6f60 100644 --- a/bfxapi/rest/middleware/middleware.py +++ b/bfxapi/rest/middleware/middleware.py @@ -19,7 +19,7 @@ class Middleware(object): assert isinstance(self.API_KEY, str) and isinstance(self.API_SECRET, str), \ "API_KEY and API_SECRET must be both str to call __build_authentication_headers" - nonce = int(round(time.time() * 1_000_000)) + nonce = str(round(time.time() * 1_000_000)) if data == None: path = f"/api/v2/{endpoint}{nonce}" diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index a8104be..4b3052e 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -174,9 +174,9 @@ class BfxWebsocketClient(object): async def __authenticate(self, API_KEY, API_SECRET, filter=None): data = { "event": "auth", "filter": filter, "apiKey": API_KEY } - data["authNonce"] = int(round(time.time() * 1_000_000)) + data["authNonce"] = str(round(time.time() * 1_000_000)) - data["authPayload"] = "AUTH" + str(data["authNonce"]) + data["authPayload"] = "AUTH" + data["authNonce"] data["authSig"] = hmac.new( API_SECRET.encode("utf8"), From 0300e6261a2ffa97cf994d4c6eccc834ad2a6354 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 3 Mar 2023 18:30:25 +0100 Subject: [PATCH 185/265] get invoices paginated Co-Authored-By: itsdeka --- .../rest/endpoints/rest_merchant_endpoints.py | 13 +++++++++++++ bfxapi/rest/types.py | 17 +++++++++++++++++ examples/rest/merchant/submit_invoice.py | 4 +++- 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/bfxapi/rest/endpoints/rest_merchant_endpoints.py b/bfxapi/rest/endpoints/rest_merchant_endpoints.py index 0c80110..d07315a 100644 --- a/bfxapi/rest/endpoints/rest_merchant_endpoints.py +++ b/bfxapi/rest/endpoints/rest_merchant_endpoints.py @@ -31,6 +31,19 @@ class RestMerchantEndpoints(Middleware): "id": id, "start": start, "end": end, "limit": limit })) ] + + def get_invoices_paginated(self, page: int = 1, page_size: int = 10, sort: Literal["asc", "desc"] = "asc", + sort_field: Literal["t", "amount", "status"] = "t", status: Optional[List[Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"]]] = None, fiat: Optional[List[str]] = None, + crypto: Optional[List[str]] = None, id: Optional[str] = None, order_id: Optional[str] = None) -> InvoicePage: + body = to_camel_case_keys({ + "page": page, "page_size": page_size, "sort": sort, + "sort_field": sort_field, "status": status, "fiat": fiat, + "crypto": crypto, "id": id, "order_id": order_id + }) + + data = to_snake_case_keys(self._POST("auth/r/ext/pay/invoices/paginated", body=body)) + + return InvoicePage.parse(data) def get_invoice_count_stats(self, status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"], format: str) -> List[InvoiceStats]: return [ InvoiceStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/count", body={ "status": status, "format": format }) ] diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 032416d..360e537 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -638,6 +638,23 @@ class InvoiceSubmission(_Type): force_completed: bool amount_diff: str +@dataclass +class InvoicePage(_Type): + page: int + page_size: int + sort: Literal["asc", "desc"] + sort_field: Literal["t", "amount", "status"] + total_pages: int + total_items: int + items: List[InvoiceSubmission] + + @classmethod + def parse(cls, data: Dict[str, Any]) -> "InvoicePage": + for index, item in enumerate(data["items"]): + data["items"][index] = InvoiceSubmission.parse(item) + + return InvoicePage(**data) + @dataclass class InvoiceStats(_Type): time: str diff --git a/examples/rest/merchant/submit_invoice.py b/examples/rest/merchant/submit_invoice.py index cb71dce..bec606e 100644 --- a/examples/rest/merchant/submit_invoice.py +++ b/examples/rest/merchant/submit_invoice.py @@ -40,4 +40,6 @@ print(bfx.rest.merchant.complete_invoice( deposit_id=1 )) -print(bfx.rest.merchant.get_invoices(limit=25)) \ No newline at end of file +print(bfx.rest.merchant.get_invoices(limit=25)) + +print(bfx.rest.merchant.get_invoices_paginated(page=1, page_size=60, sort="asc", sort_field="t")) \ No newline at end of file From 7562b21ada5bd3c00502f61d31c142de93b471fe Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 3 Mar 2023 20:03:23 +0100 Subject: [PATCH 186/265] settings Co-Authored-By: itsdeka --- .../rest/endpoints/rest_merchant_endpoints.py | 25 +++++++++++++++-- bfxapi/rest/enums.py | 14 ++++++++-- bfxapi/rest/types.py | 28 +++++++++++++++++++ examples/rest/merchant/settings.py | 28 +++++++++++++++++++ 4 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 examples/rest/merchant/settings.py diff --git a/bfxapi/rest/endpoints/rest_merchant_endpoints.py b/bfxapi/rest/endpoints/rest_merchant_endpoints.py index d07315a..2e445ce 100644 --- a/bfxapi/rest/endpoints/rest_merchant_endpoints.py +++ b/bfxapi/rest/endpoints/rest_merchant_endpoints.py @@ -1,9 +1,11 @@ -from typing import TypedDict, List, Union, Literal, Optional +from typing import TypedDict, List, Union, Literal, Optional, Any from decimal import Decimal from .. types import * +from .. enums import MerchantSettingsKey from .. middleware import Middleware + from ...utils.camel_and_snake_case_helpers import to_snake_case_keys, to_camel_case_keys _CustomerInfo = TypedDict("_CustomerInfo", { @@ -79,4 +81,23 @@ class RestMerchantEndpoints(Middleware): return bool(self._POST("auth/w/ext/pay/settings/convert/remove", body={ "baseCcy": base_currency, "convertCcy": convert_currency - })) \ No newline at end of file + })) + + def set_merchant_settings(self, key: MerchantSettingsKey, val: Any) -> bool: + return bool(self._POST("auth/w/ext/pay/settings/set", body={ "key": key, "val": val })) + + def get_merchant_settings(self, key: MerchantSettingsKey) -> Any: + return self._POST("auth/r/ext/pay/settings/get", body={ "key": key }) + + def list_merchant_settings(self, keys: List[MerchantSettingsKey] = list()) -> Dict[MerchantSettingsKey, Any]: + return self._POST("auth/r/ext/pay/settings/list", body={ "keys": keys }) + + def get_deposits(self, start: int, end: int, ccy: Optional[str] = None, unlinked: Optional[bool] = None) -> List[MerchantDeposit]: + return [ MerchantDeposit(**sub_data) for sub_data in to_snake_case_keys(self._POST("auth/r/ext/pay/deposits", body={ + "from": start, "to": end, "ccy": ccy, "unlinked": unlinked + })) ] + + def get_unlinked_deposits(self, ccy: str, start: Optional[int] = None, end: Optional[int] = None) -> List[MerchantUnlinkedDeposit]: + return [ MerchantUnlinkedDeposit(**sub_data) for sub_data in to_snake_case_keys(self._POST("/auth/r/ext/pay/deposits/unlinked", body={ + "ccy": ccy, "start": start, "end": end + })) ] \ No newline at end of file diff --git a/bfxapi/rest/enums.py b/bfxapi/rest/enums.py index 65c1e1a..c256a34 100644 --- a/bfxapi/rest/enums.py +++ b/bfxapi/rest/enums.py @@ -21,7 +21,7 @@ class Config(str, Enum): INFO_PAIR_FUTURES = "pub:info:pair:futures" INFO_TX_STATUS = "pub:info:tx:status" - SPEC_MARGIN = "pub:spec:margin", + SPEC_MARGIN = "pub:spec:margin" FEES = "pub:fees" class Precision(str, Enum): @@ -33,4 +33,14 @@ class Precision(str, Enum): class Sort(int, Enum): ASCENDING = +1 - DESCENDING = -1 \ No newline at end of file + DESCENDING = -1 + +class MerchantSettingsKey(str, Enum): + PREFERRED_FIAT = "bfx_pay_preferred_fiat" + RECOMMEND_STORE = "bfx_pay_recommend_store" + NOTIFY_PAYMENT_COMPLETED = "bfx_pay_notify_payment_completed" + NOTIFY_PAYMENT_COMPLETED_EMAIL = "bfx_pay_notify_payment_completed_email" + NOTIFY_AUTOCONVERT_EXECUTED = "bfx_pay_notify_autoconvert_executed" + DUST_BALANCE_UI = "bfx_pay_dust_balance_ui" + MERCHANT_CUSTOMER_SUPPORT_URL = "bfx_pay_merchant_customer_support_url" + MERCHANT_UNDERPAID_THRESHOLD = "bfx_pay_merchant_underpaid_threshold" \ No newline at end of file diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 360e537..9e3e752 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -666,4 +666,32 @@ class CurrencyConversion(_Type): convert_currency: str created: int +@dataclass +class MerchantDeposit(_Type): + id: int + invoice_id: Optional[str] + order_id: Optional[str] + type: Literal["ledger", "deposit"] + amount: float + t: int + txid: str + currency: str + method: str + pay_method: str + +@dataclass +class MerchantUnlinkedDeposit(_Type): + id: int + method: str + currency: str + created_at: int + updated_at: int + amount: float + fee: float + txid: str + address: str + payment_id: Optional[int] + status: str + note: Optional[str] + #endregion \ No newline at end of file diff --git a/examples/rest/merchant/settings.py b/examples/rest/merchant/settings.py new file mode 100644 index 0000000..4afe06b --- /dev/null +++ b/examples/rest/merchant/settings.py @@ -0,0 +1,28 @@ +# python -c "import examples.rest.merchant.settings" + +import os + +from bfxapi import Client, REST_HOST + +from bfxapi.rest.enums import MerchantSettingsKey + +bfx = Client( + REST_HOST=REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +if not bfx.rest.merchant.set_merchant_settings(MerchantSettingsKey.RECOMMEND_STORE, 1): + print(f"Cannot set <{MerchantSettingsKey.RECOMMEND_STORE}> to <1>.") + +print(f"The current <{MerchantSettingsKey.PREFERRED_FIAT}> value is:", + bfx.rest.merchant.get_merchant_settings(MerchantSettingsKey.PREFERRED_FIAT)) + +settings = bfx.rest.merchant.list_merchant_settings([ + MerchantSettingsKey.DUST_BALANCE_UI, + MerchantSettingsKey.MERCHANT_CUSTOMER_SUPPORT_URL, + MerchantSettingsKey.MERCHANT_UNDERPAID_THRESHOLD +]) + +for key, value in settings.items(): + print(f"<{key}>:", value) \ No newline at end of file From dc17b8f54ae7b47871499fffe26bd20ca62c0925 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 3 Mar 2023 20:14:51 +0100 Subject: [PATCH 187/265] Remove STAGING_REST_HOST and STAGING_WSS_HOST urls from bfxapi/urls.py. --- bfxapi/__init__.py | 4 ++-- bfxapi/urls.py | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/bfxapi/__init__.py b/bfxapi/__init__.py index 4fbdfd6..2cd5ce6 100644 --- a/bfxapi/__init__.py +++ b/bfxapi/__init__.py @@ -1,6 +1,6 @@ from .client import Client -from .urls import REST_HOST, PUB_REST_HOST, STAGING_REST_HOST, \ - WSS_HOST, PUB_WSS_HOST, STAGING_WSS_HOST +from .urls import REST_HOST, PUB_REST_HOST, \ + WSS_HOST, PUB_WSS_HOST NAME = "bfxapi" \ No newline at end of file diff --git a/bfxapi/urls.py b/bfxapi/urls.py index c9a622b..de31a04 100644 --- a/bfxapi/urls.py +++ b/bfxapi/urls.py @@ -1,7 +1,5 @@ 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" \ No newline at end of file +PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2" \ No newline at end of file From 3645c6badda584e3227bcbdbf1ec8cb9ed415633 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 3 Mar 2023 20:17:58 +0100 Subject: [PATCH 188/265] Add .vscode, *.pyc and *.log in .gitignore. --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 20870ef..00d1381 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +.vscode +*.pyc +*.log + __pycache__ dist From b3e1e2733114799d95abe5a93d4dfc057b8c871c Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 3 Mar 2023 21:12:34 +0100 Subject: [PATCH 189/265] Add and implement FileLogger in bfxapi.utils.logger. --- bfxapi/client.py | 4 +- bfxapi/utils/logger.py | 53 +++++++++---------- .../websocket/client/bfx_websocket_client.py | 8 +-- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/bfxapi/client.py b/bfxapi/client.py index f3121ac..757ee54 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -12,6 +12,7 @@ class Client(object): API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None, filter: Optional[List[str]] = None, + log_filename: Optional[str] = None, log_level: str = "INFO" ): credentials = None @@ -26,6 +27,7 @@ class Client(object): self.wss = BfxWebsocketClient( host=WSS_HOST, - credentials=credentials, + credentials=credentials, + log_filename=log_filename, log_level=log_level ) \ No newline at end of file diff --git a/bfxapi/utils/logger.py b/bfxapi/utils/logger.py index cf3e970..88d4a59 100644 --- a/bfxapi/utils/logger.py +++ b/bfxapi/utils/logger.py @@ -1,21 +1,8 @@ -import logging +import logging, sys BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) -RESET_SEQ = "\033[0m" - -COLOR_SEQ = "\033[1;%dm" -ITALIC_COLOR_SEQ = "\033[3;%dm" -UNDERLINE_COLOR_SEQ = "\033[4;%dm" - -BOLD_SEQ = "\033[1m" - -def formatter_message(message, use_color = True): - if use_color: - message = message.replace("$RESET", RESET_SEQ).replace("$BOLD", BOLD_SEQ) - else: - message = message.replace("$RESET", "").replace("$BOLD", "") - return message +COLOR_SEQ, ITALIC_COLOR_SEQ = "\033[1;%dm", "\033[3;%dm" COLORS = { "DEBUG": CYAN, @@ -24,29 +11,41 @@ COLORS = { "ERROR": RED } -class _ColoredFormatter(logging.Formatter): +RESET_SEQ = "\033[0m" + +class _ColorFormatter(logging.Formatter): def __init__(self, msg, use_color = True): logging.Formatter.__init__(self, msg, "%d-%m-%Y %H:%M:%S") + self.use_color = use_color def format(self, record): levelname = record.levelname if self.use_color and levelname in COLORS: - levelname_color = COLOR_SEQ % (30 + COLORS[levelname]) + levelname + RESET_SEQ - record.levelname = levelname_color - record.name = ITALIC_COLOR_SEQ % (30 + BLACK) + record.name + RESET_SEQ + record.name = ITALIC_COLOR_SEQ % (30 + BLACK) + record.name + RESET_SEQ + record.levelname = COLOR_SEQ % (30 + COLORS[levelname]) + levelname + RESET_SEQ return logging.Formatter.format(self, record) -class ColoredLogger(logging.Logger): - FORMAT = "[$BOLD%(name)s$RESET] [%(asctime)s] [%(levelname)s] %(message)s" - - COLOR_FORMAT = formatter_message(FORMAT, True) +class ColorLogger(logging.Logger): + FORMAT = "[%(name)s] [%(levelname)s] [%(asctime)s] %(message)s" def __init__(self, name, level): logging.Logger.__init__(self, name, level) - colored_formatter = _ColoredFormatter(self.COLOR_FORMAT) - console = logging.StreamHandler() - console.setFormatter(colored_formatter) + colored_formatter = _ColorFormatter(self.FORMAT, use_color=True) + console = logging.StreamHandler(stream=sys.stderr) + console.setFormatter(fmt=colored_formatter) - self.addHandler(console) \ No newline at end of file + self.addHandler(hdlr=console) + +class FileLogger(logging.Logger): + FORMAT = "[%(name)s] [%(levelname)s] [%(asctime)s] %(message)s" + + def __init__(self, name, level, filename): + logging.Logger.__init__(self, name, level) + + formatter = logging.Formatter(self.FORMAT) + fh = logging.FileHandler(filename=filename) + fh.setFormatter(fmt=formatter) + + self.addHandler(hdlr=fh) \ No newline at end of file diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 4b3052e..7b73db9 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -16,7 +16,7 @@ from ..exceptions import WebsocketAuthenticationRequired, InvalidAuthenticationC from ...utils.JSONEncoder import JSONEncoder -from ...utils.logger import ColoredLogger +from ...utils.logger import ColorLogger, FileLogger def _require_websocket_authentication(function: F) -> F: async def wrapper(self, *args, **kwargs): @@ -38,7 +38,7 @@ class BfxWebsocketClient(object): *AuthenticatedChannelsHandler.EVENTS ] - def __init__(self, host, credentials = None, log_level = "INFO"): + def __init__(self, host, credentials = None, log_filename = None, log_level = "INFO"): self.websocket = None self.host, self.credentials, self.event_emitter = host, credentials, AsyncIOEventEmitter() @@ -47,7 +47,9 @@ class BfxWebsocketClient(object): self.handler = AuthenticatedChannelsHandler(event_emitter=self.event_emitter) - self.logger = ColoredLogger("BfxWebsocketClient", level=log_level) + if log_filename == None: + self.logger = ColorLogger("BfxWebsocketClient", level=log_level) + else: self.logger = FileLogger("BfxWebsocketClient", level=log_level, filename=log_filename) self.event_emitter.add_listener("error", lambda exception: self.logger.error(f"{type(exception).__name__}: {str(exception)}" + "\n" + From 6c99d3aacfd6b779201f774fb3ee8bcbe6a5df18 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 6 Mar 2023 16:03:08 +0100 Subject: [PATCH 190/265] Add dev-requirements.txt. Split requirements between requirements.txt and dev-requirements.txt. Change install_requires in setup.py. --- dev-requirements.txt | Bin 0 -> 248 bytes requirements.txt | Bin 512 -> 304 bytes setup.py | 12 +----------- 3 files changed, 1 insertion(+), 11 deletions(-) create mode 100644 dev-requirements.txt diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..a6ffdb4b157564f93b3cf1a9e6046f9109245d6f GIT binary patch literal 248 zcmY*T%MOAt5S+7#|KJbQ&_azK{23AsBotIiB0sOrY~f%wyX|HkJKvAP3nzAmCl>Vf zOWa0e%s6wt*+(3Zu;RhKvnnDtmj6Q>bz)0B(`&L);7Q<|L}aHYnZYup@_t*93226Jywn?~0IE;b`)gC964C)of1 literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt index 71a2708e787aaa497e5eaac30bc59566d18b7a57..2c5f864aae14843952137491e2ff4b912577b925 100644 GIT binary patch delta 25 hcmZo**}ycRY~lfj$!&}pjHZ*^EY2zdYi delta 146 zcmdnM)W9;KOgWdKlA!>IZGq5$L65-_2n`u{8Mqi=GP)BFN^*jvOn}&UVxg;QK2R=) zArq{}2q*}Wgy<;&o0iH@%%D55Us2Q$C}9dV3|Zp2EV~g<7-- Date: Mon, 6 Mar 2023 16:36:56 +0100 Subject: [PATCH 191/265] Install and configure pylint. Add pylint to dev-requirements.txt. Start rewriting code to follow pylint's linting rules. --- .pylintrc | 11 +++++++ bfxapi/client.py | 30 ++++++++++-------- .../websocket/client/bfx_websocket_client.py | 4 +-- dev-requirements.txt | Bin 248 -> 600 bytes examples/rest/authenticated/claim_position.py | 6 ++-- examples/rest/authenticated/get_wallets.py | 6 ++-- .../set_derivative_position_collateral.py | 6 ++-- .../authenticated/submit_funding_offer.py | 6 ++-- examples/rest/authenticated/submit_order.py | 6 ++-- .../rest/authenticated/toggle_keep_funding.py | 6 ++-- examples/rest/merchant/settings.py | 6 ++-- examples/rest/merchant/submit_invoice.py | 6 ++-- examples/rest/public/book.py | 2 +- examples/rest/public/conf.py | 2 +- examples/rest/public/get_candles_hist.py | 2 +- examples/rest/public/pulse_endpoints.py | 2 +- .../rest/public/rest_calculation_endpoints.py | 2 +- examples/rest/public/trades.py | 2 +- .../websocket/authenticated/submit_order.py | 6 ++-- examples/websocket/authenticated/wallets.py | 6 ++-- .../websocket/public/derivatives_status.py | 2 +- examples/websocket/public/order_book.py | 2 +- examples/websocket/public/raw_order_book.py | 2 +- examples/websocket/public/ticker.py | 2 +- examples/websocket/public/trades.py | 2 +- requirements.txt | Bin 304 -> 300 bytes 26 files changed, 70 insertions(+), 57 deletions(-) create mode 100644 .pylintrc diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..6f5760f --- /dev/null +++ b/.pylintrc @@ -0,0 +1,11 @@ +[MAIN] + +py-version = 3.8.0 + +ignore=examples + +[MESSAGES CONTROL] + +disable= + missing-docstring, + too-few-public-methods \ No newline at end of file diff --git a/bfxapi/client.py b/bfxapi/client.py index 757ee54..025ee08 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -1,33 +1,35 @@ +from typing import List, Optional + from .rest import BfxRestInterface from .websocket import BfxWebsocketClient from .urls import REST_HOST, WSS_HOST -from typing import List, Optional - -class Client(object): +class Client: def __init__( self, - REST_HOST: str = REST_HOST, - WSS_HOST: str = WSS_HOST, - API_KEY: Optional[str] = None, - API_SECRET: Optional[str] = None, - filter: Optional[List[str]] = None, + api_key: Optional[str] = None, + api_secret: Optional[str] = None, + filters: Optional[List[str]] = None, + *, + rest_host: str = REST_HOST, + wss_host: str = WSS_HOST, log_filename: Optional[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 } + if api_key and api_secret: + credentials = { "API_KEY": api_key, "API_SECRET": api_secret, "filters": filters } self.rest = BfxRestInterface( - host=REST_HOST, + host=rest_host, credentials=credentials ) self.wss = BfxWebsocketClient( - host=WSS_HOST, + host=wss_host, credentials=credentials, - log_filename=log_filename, + log_filename=log_filename, log_level=log_level - ) \ No newline at end of file + ) + \ No newline at end of file diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 7b73db9..11cfa18 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -173,8 +173,8 @@ class BfxWebsocketClient(object): if reconnection.status == False: break - async def __authenticate(self, API_KEY, API_SECRET, filter=None): - data = { "event": "auth", "filter": filter, "apiKey": API_KEY } + async def __authenticate(self, API_KEY, API_SECRET, filters=None): + data = { "event": "auth", "filter": filters, "apiKey": API_KEY } data["authNonce"] = str(round(time.time() * 1_000_000)) diff --git a/dev-requirements.txt b/dev-requirements.txt index a6ffdb4b157564f93b3cf1a9e6046f9109245d6f..fff03cf052a26593c67097d8c3c4cd6ce90c4e5b 100644 GIT binary patch literal 600 zcmY*WTW-QY44m&u+@Ti;V#E8RhiL+p0$B)QQE3m~cE;X~R7DO%^YGaI{i!hGm371o z4|Mh~^6P?xJLY^pSu?y?MSNjkz42KP-SY|mkcwI<L6`sJHH@{p-?YtHCzpyD1kslj96s R&o}NOV diff --git a/examples/rest/authenticated/claim_position.py b/examples/rest/authenticated/claim_position.py index f3a91e4..0f8b385 100644 --- a/examples/rest/authenticated/claim_position.py +++ b/examples/rest/authenticated/claim_position.py @@ -7,9 +7,9 @@ from bfxapi import Client, REST_HOST from bfxapi.rest.types import Notification, PositionClaim bfx = Client( - REST_HOST=REST_HOST, - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET") + rest_host=REST_HOST, + api_key=os.getenv("BFX_API_KEY"), + api_secret=os.getenv("BFX_API_SECRET") ) # Claims all active positions diff --git a/examples/rest/authenticated/get_wallets.py b/examples/rest/authenticated/get_wallets.py index 00dc129..0ea1388 100644 --- a/examples/rest/authenticated/get_wallets.py +++ b/examples/rest/authenticated/get_wallets.py @@ -9,9 +9,9 @@ from bfxapi.rest.types import List, Wallet, Transfer, \ Notification bfx = Client( - REST_HOST=REST_HOST, - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET") + rest_host=REST_HOST, + api_key=os.getenv("BFX_API_KEY"), + api_secret=os.getenv("BFX_API_SECRET") ) # Gets all user's available wallets diff --git a/examples/rest/authenticated/set_derivative_position_collateral.py b/examples/rest/authenticated/set_derivative_position_collateral.py index c5f82d2..bb2100f 100644 --- a/examples/rest/authenticated/set_derivative_position_collateral.py +++ b/examples/rest/authenticated/set_derivative_position_collateral.py @@ -7,9 +7,9 @@ from bfxapi import Client, REST_HOST from bfxapi.rest.types import DerivativePositionCollateral, DerivativePositionCollateralLimits bfx = Client( - REST_HOST=REST_HOST, - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET") + rest_host=REST_HOST, + api_key=os.getenv("BFX_API_KEY"), + api_secret=os.getenv("BFX_API_SECRET") ) submit_order_notification = bfx.rest.auth.submit_order( diff --git a/examples/rest/authenticated/submit_funding_offer.py b/examples/rest/authenticated/submit_funding_offer.py index bcaedcd..0230eb6 100644 --- a/examples/rest/authenticated/submit_funding_offer.py +++ b/examples/rest/authenticated/submit_funding_offer.py @@ -7,9 +7,9 @@ from bfxapi.enums import FundingOfferType, Flag from bfxapi.rest.types import Notification, FundingOffer bfx = Client( - REST_HOST=REST_HOST, - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET") + rest_host=REST_HOST, + api_key=os.getenv("BFX_API_KEY"), + api_secret=os.getenv("BFX_API_SECRET") ) # Submit a new funding offer diff --git a/examples/rest/authenticated/submit_order.py b/examples/rest/authenticated/submit_order.py index b831c80..8c9fbb5 100644 --- a/examples/rest/authenticated/submit_order.py +++ b/examples/rest/authenticated/submit_order.py @@ -7,9 +7,9 @@ from bfxapi.enums import OrderType, Flag from bfxapi.rest.types import Notification, Order bfx = Client( - REST_HOST=REST_HOST, - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET") + rest_host=REST_HOST, + api_key=os.getenv("BFX_API_KEY"), + api_secret=os.getenv("BFX_API_SECRET") ) # Submit a new order diff --git a/examples/rest/authenticated/toggle_keep_funding.py b/examples/rest/authenticated/toggle_keep_funding.py index 96304b7..b17405f 100644 --- a/examples/rest/authenticated/toggle_keep_funding.py +++ b/examples/rest/authenticated/toggle_keep_funding.py @@ -7,9 +7,9 @@ from bfxapi import Client, REST_HOST from bfxapi.rest.types import List, FundingLoan, Notification bfx = Client( - REST_HOST=REST_HOST, - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET") + rest_host=REST_HOST, + api_key=os.getenv("BFX_API_KEY"), + api_secret=os.getenv("BFX_API_SECRET") ) loans: List[FundingLoan] = bfx.rest.auth.get_funding_loans(symbol="fUSD") diff --git a/examples/rest/merchant/settings.py b/examples/rest/merchant/settings.py index 4afe06b..62c015b 100644 --- a/examples/rest/merchant/settings.py +++ b/examples/rest/merchant/settings.py @@ -7,9 +7,9 @@ from bfxapi import Client, REST_HOST from bfxapi.rest.enums import MerchantSettingsKey bfx = Client( - REST_HOST=REST_HOST, - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET") + rest_host=REST_HOST, + api_key=os.getenv("BFX_API_KEY"), + api_secret=os.getenv("BFX_API_SECRET") ) if not bfx.rest.merchant.set_merchant_settings(MerchantSettingsKey.RECOMMEND_STORE, 1): diff --git a/examples/rest/merchant/submit_invoice.py b/examples/rest/merchant/submit_invoice.py index bec606e..907c4bf 100644 --- a/examples/rest/merchant/submit_invoice.py +++ b/examples/rest/merchant/submit_invoice.py @@ -7,9 +7,9 @@ from bfxapi import Client, REST_HOST from bfxapi.rest.types import InvoiceSubmission bfx = Client( - REST_HOST=REST_HOST, - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET") + rest_host=REST_HOST, + api_key=os.getenv("BFX_API_KEY"), + api_secret=os.getenv("BFX_API_SECRET") ) customer_info = { diff --git a/examples/rest/public/book.py b/examples/rest/public/book.py index b5f1a64..9014835 100644 --- a/examples/rest/public/book.py +++ b/examples/rest/public/book.py @@ -5,7 +5,7 @@ from bfxapi import Client, PUB_REST_HOST from bfxapi.rest.types import List, TradingPairBook, TradingPairRawBook, \ FundingCurrencyBook, FundingCurrencyRawBook -bfx = Client(REST_HOST=PUB_REST_HOST) +bfx = Client(rest_host=PUB_REST_HOST) t_book: List[TradingPairBook] = bfx.rest.public.get_t_book("tBTCUSD", precision="P0", len=25) diff --git a/examples/rest/public/conf.py b/examples/rest/public/conf.py index 8c04ae9..f4f6a55 100644 --- a/examples/rest/public/conf.py +++ b/examples/rest/public/conf.py @@ -4,7 +4,7 @@ from bfxapi import Client, PUB_REST_HOST from bfxapi.rest.enums import Config -bfx = Client(REST_HOST=PUB_REST_HOST) +bfx = Client(rest_host=PUB_REST_HOST) print("Available configs:", [ config.value for config in Config ]) diff --git a/examples/rest/public/get_candles_hist.py b/examples/rest/public/get_candles_hist.py index a1a4e8f..8cb28b3 100644 --- a/examples/rest/public/get_candles_hist.py +++ b/examples/rest/public/get_candles_hist.py @@ -2,7 +2,7 @@ from bfxapi import Client, PUB_REST_HOST -bfx = Client(REST_HOST=PUB_REST_HOST) +bfx = Client(rest_host=PUB_REST_HOST) print(f"Candles: {bfx.rest.public.get_candles_hist(symbol='tBTCUSD')}") diff --git a/examples/rest/public/pulse_endpoints.py b/examples/rest/public/pulse_endpoints.py index 462c955..c1a079a 100644 --- a/examples/rest/public/pulse_endpoints.py +++ b/examples/rest/public/pulse_endpoints.py @@ -6,7 +6,7 @@ from bfxapi import Client, PUB_REST_HOST from bfxapi.rest.types import List, PulseMessage, PulseProfile -bfx = Client(REST_HOST=PUB_REST_HOST) +bfx = Client(rest_host=PUB_REST_HOST) # POSIX timestamp in milliseconds (check https://currentmillis.com/) end = datetime.datetime(2020, 5, 2).timestamp() * 1000 diff --git a/examples/rest/public/rest_calculation_endpoints.py b/examples/rest/public/rest_calculation_endpoints.py index 6022753..2317aea 100644 --- a/examples/rest/public/rest_calculation_endpoints.py +++ b/examples/rest/public/rest_calculation_endpoints.py @@ -4,7 +4,7 @@ from bfxapi import Client, PUB_REST_HOST from bfxapi.rest.types import TradingMarketAveragePrice, FundingMarketAveragePrice, FxRate -bfx = Client(REST_HOST=PUB_REST_HOST) +bfx = Client(rest_host=PUB_REST_HOST) trading_market_average_price: TradingMarketAveragePrice = bfx.rest.public.get_trading_market_average_price( symbol="tBTCUSD", diff --git a/examples/rest/public/trades.py b/examples/rest/public/trades.py index 4dbf77e..4428368 100644 --- a/examples/rest/public/trades.py +++ b/examples/rest/public/trades.py @@ -4,7 +4,7 @@ from bfxapi import Client, PUB_REST_HOST from bfxapi.rest.enums import Sort from bfxapi.rest.types import List, TradingPairTrade, FundingCurrencyTrade -bfx = Client(REST_HOST=PUB_REST_HOST) +bfx = Client(rest_host=PUB_REST_HOST) t_trades: List[TradingPairTrade] = bfx.rest.public.get_t_trades("tBTCUSD", \ limit=15, sort=Sort.ASCENDING) diff --git a/examples/websocket/authenticated/submit_order.py b/examples/websocket/authenticated/submit_order.py index ca671be..31965c7 100644 --- a/examples/websocket/authenticated/submit_order.py +++ b/examples/websocket/authenticated/submit_order.py @@ -7,9 +7,9 @@ from bfxapi.enums import Error, OrderType from bfxapi.websocket.types import Notification, Order bfx = Client( - WSS_HOST=WSS_HOST, - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET") + wss_host=WSS_HOST, + api_key=os.getenv("BFX_API_KEY"), + api_secret=os.getenv("BFX_API_SECRET") ) @bfx.wss.on("wss-error") diff --git a/examples/websocket/authenticated/wallets.py b/examples/websocket/authenticated/wallets.py index 039364a..da4815c 100644 --- a/examples/websocket/authenticated/wallets.py +++ b/examples/websocket/authenticated/wallets.py @@ -7,9 +7,9 @@ from bfxapi.enums import Error from bfxapi.websocket.types import List, Wallet bfx = Client( - API_KEY=os.getenv("BFX_API_KEY"), - API_SECRET=os.getenv("BFX_API_SECRET"), - filter=["wallet"] + api_key=os.getenv("BFX_API_KEY"), + api_secret=os.getenv("BFX_API_SECRET"), + filters=["wallet"] ) @bfx.wss.on("wss-error") diff --git a/examples/websocket/public/derivatives_status.py b/examples/websocket/public/derivatives_status.py index 05a9556..dcb3c1d 100644 --- a/examples/websocket/public/derivatives_status.py +++ b/examples/websocket/public/derivatives_status.py @@ -6,7 +6,7 @@ from bfxapi.websocket.types import DerivativesStatus from bfxapi.websocket import subscriptions -bfx = Client(WSS_HOST=PUB_WSS_HOST) +bfx = Client(wss_host=PUB_WSS_HOST) @bfx.wss.on("derivatives_status_update") def on_derivatives_status_update(subscription: subscriptions.Status, data: DerivativesStatus): diff --git a/examples/websocket/public/order_book.py b/examples/websocket/public/order_book.py index 536ce7c..705ec41 100644 --- a/examples/websocket/public/order_book.py +++ b/examples/websocket/public/order_book.py @@ -38,7 +38,7 @@ SYMBOLS = [ "tBTCUSD", "tLTCUSD", "tLTCBTC", "tETHUSD", "tETHBTC" ] order_book = OrderBook(symbols=SYMBOLS) -bfx = Client(WSS_HOST=PUB_WSS_HOST) +bfx = Client(wss_host=PUB_WSS_HOST) @bfx.wss.on("wss-error") def on_wss_error(code: Error, msg: str): diff --git a/examples/websocket/public/raw_order_book.py b/examples/websocket/public/raw_order_book.py index 7909a61..9748abf 100644 --- a/examples/websocket/public/raw_order_book.py +++ b/examples/websocket/public/raw_order_book.py @@ -38,7 +38,7 @@ SYMBOLS = [ "tBTCUSD", "tLTCUSD", "tLTCBTC", "tETHUSD", "tETHBTC" ] raw_order_book = RawOrderBook(symbols=SYMBOLS) -bfx = Client(WSS_HOST=PUB_WSS_HOST) +bfx = Client(wss_host=PUB_WSS_HOST) @bfx.wss.on("wss-error") def on_wss_error(code: Error, msg: str): diff --git a/examples/websocket/public/ticker.py b/examples/websocket/public/ticker.py index b2eadc8..ff1120f 100644 --- a/examples/websocket/public/ticker.py +++ b/examples/websocket/public/ticker.py @@ -6,7 +6,7 @@ from bfxapi.websocket import subscriptions from bfxapi.websocket.enums import Channel from bfxapi.websocket.types import TradingPairTicker -bfx = Client(WSS_HOST=PUB_WSS_HOST) +bfx = Client(wss_host=PUB_WSS_HOST) @bfx.wss.on("t_ticker_update") def on_t_ticker_update(subscription: subscriptions.Ticker, data: TradingPairTicker): diff --git a/examples/websocket/public/trades.py b/examples/websocket/public/trades.py index 186c46b..ff178dc 100644 --- a/examples/websocket/public/trades.py +++ b/examples/websocket/public/trades.py @@ -6,7 +6,7 @@ from bfxapi.websocket.types import Candle, TradingPairTrade from bfxapi.websocket import subscriptions -bfx = Client(WSS_HOST=PUB_WSS_HOST) +bfx = Client(wss_host=PUB_WSS_HOST) @bfx.wss.on("candles_update") def on_candles_update(subscription: subscriptions.Candles, candle: Candle): diff --git a/requirements.txt b/requirements.txt index 2c5f864aae14843952137491e2ff4b912577b925..b2a3b7683962381e55063888496c3fa4478452d1 100644 GIT binary patch delta 7 OcmdnMw1#Pe4kG{xumU~+ delta 12 TcmZ3(w1H`Z4kHUM0~Z4T7CHi1 From ae14a5d0d163fa791c981914c4938dc13ec85a88 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 6 Mar 2023 17:14:03 +0100 Subject: [PATCH 192/265] Apply pylint's linting rules to bfxapi/__init__.py, bfxapi/enums.py, bfxapi/exceptions.py, bfxapi/labeler.py, bfxapi/notification.py and bfxapi/urls.py. --- .pylintrc | 9 +++++++-- bfxapi/__init__.py | 2 +- bfxapi/enums.py | 2 +- bfxapi/exceptions.py | 5 +---- bfxapi/labeler.py | 40 ++++++++++++++++++++++------------------ bfxapi/notification.py | 14 +++++++------- bfxapi/urls.py | 2 +- 7 files changed, 40 insertions(+), 34 deletions(-) diff --git a/.pylintrc b/.pylintrc index 6f5760f..67c33d8 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,6 +1,6 @@ [MAIN] -py-version = 3.8.0 +py-version=3.8.0 ignore=examples @@ -8,4 +8,9 @@ ignore=examples disable= missing-docstring, - too-few-public-methods \ No newline at end of file + too-few-public-methods, + dangerous-default-value + +[FORMAT] + +max-line-length=120 \ No newline at end of file diff --git a/bfxapi/__init__.py b/bfxapi/__init__.py index 2cd5ce6..304bc34 100644 --- a/bfxapi/__init__.py +++ b/bfxapi/__init__.py @@ -3,4 +3,4 @@ from .client import Client from .urls import REST_HOST, PUB_REST_HOST, \ WSS_HOST, PUB_WSS_HOST -NAME = "bfxapi" \ No newline at end of file +NAME = "bfxapi" diff --git a/bfxapi/enums.py b/bfxapi/enums.py index 03b89bf..9b06bc2 100644 --- a/bfxapi/enums.py +++ b/bfxapi/enums.py @@ -47,4 +47,4 @@ class Error(int, Enum): ERR_SUB_LIMIT = 10305 ERR_UNSUB_FAIL = 10400 ERR_UNSUB_NOT = 10401 - ERR_READY = 11000 \ No newline at end of file + ERR_READY = 11000 diff --git a/bfxapi/exceptions.py b/bfxapi/exceptions.py index d876946..f7723c3 100644 --- a/bfxapi/exceptions.py +++ b/bfxapi/exceptions.py @@ -9,11 +9,8 @@ class BfxBaseException(Exception): Base class for every custom exception in bfxapi/rest/exceptions.py and bfxapi/websocket/exceptions.py. """ - pass - class LabelerSerializerException(BfxBaseException): """ This exception indicates an error thrown by the _Serializer class in bfxapi/labeler.py. """ - - pass \ No newline at end of file + \ No newline at end of file diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 213752c..cac623a 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -1,12 +1,12 @@ -from .exceptions import LabelerSerializerException - from typing import Type, Generic, TypeVar, Iterable, Optional, Dict, List, Tuple, Any, cast +from .exceptions import LabelerSerializerException + T = TypeVar("T", bound="_Type") def compose(*decorators): def wrapper(function): - for decorator in reversed(decorators): + for decorator in reversed(decorators): function = decorator(function) return function @@ -28,36 +28,37 @@ def partial(cls): return cls -class _Type(object): +class _Type: """ Base class for any dataclass serializable by the _Serializer generic class. """ - pass - class _Serializer(Generic[T]): - def __init__(self, name: str, klass: Type[_Type], labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]): - self.name, self.klass, self.__labels, self.__IGNORE = name, klass, labels, IGNORE + def __init__(self, name: str, klass: Type[_Type], labels: List[str], + *, ignore: List[str] = [ "_PLACEHOLDER" ]): + self.name, self.klass, self.__labels, self.__ignore = name, klass, labels, ignore def _serialize(self, *args: Any, skip: Optional[List[str]] = None) -> Iterable[Tuple[str, Any]]: - labels = list(filter(lambda label: label not in (skip or list()), self.__labels)) + labels = list(filter(lambda label: label not in (skip or []), self.__labels)) if len(labels) > len(args): - raise LabelerSerializerException(f"{self.name} -> and <*args> arguments should contain the same amount of elements.") + raise LabelerSerializerException(f"{self.name} -> and <*args> " + + "arguments should contain the same amount of elements.") for index, label in enumerate(labels): - if label not in self.__IGNORE: + if label not in self.__ignore: yield label, args[index] def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: return cast(T, self.klass(**dict(self._serialize(*values, skip=skip)))) def get_labels(self) -> List[str]: - return [ label for label in self.__labels if label not in self.__IGNORE ] + return [ label for label in self.__labels if label not in self.__ignore ] class _RecursiveSerializer(_Serializer, Generic[T]): - def __init__(self, name: str, klass: Type[_Type], labels: List[str], serializers: Dict[str, _Serializer[Any]], IGNORE: List[str] = ["_PLACEHOLDER"]): - super().__init__(name, klass, labels, IGNORE) + def __init__(self, name: str, klass: Type[_Type], labels: List[str], + *, serializers: Dict[str, _Serializer[Any]], ignore: List[str] = ["_PLACEHOLDER"]): + super().__init__(name, klass, labels, ignore = ignore) self.serializers = serializers @@ -70,8 +71,11 @@ class _RecursiveSerializer(_Serializer, Generic[T]): return cast(T, self.klass(**serialization)) -def generate_labeler_serializer(name: str, klass: Type[T], labels: List[str], IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> _Serializer[T]: - return _Serializer[T](name, klass, labels, IGNORE) +def generate_labeler_serializer(name: str, klass: Type[T], labels: List[str], + *, ignore: List[str] = [ "_PLACEHOLDER" ]) -> _Serializer[T]: + return _Serializer[T](name, klass, labels, ignore=ignore) -def generate_recursive_serializer(name: str, klass: Type[T], labels: List[str], serializers: Dict[str, _Serializer[Any]], IGNORE: List[str] = [ "_PLACEHOLDER" ]) -> _RecursiveSerializer[T]: - return _RecursiveSerializer[T](name, klass, labels, serializers, IGNORE) \ No newline at end of file +def generate_recursive_serializer(name: str, klass: Type[T], labels: List[str], + *, serializers: Dict[str, _Serializer[Any]], ignore: List[str] = [ "_PLACEHOLDER" ] + ) -> _RecursiveSerializer[T]: + return _RecursiveSerializer[T](name, klass, labels, serializers=serializers, ignore=ignore) diff --git a/bfxapi/notification.py b/bfxapi/notification.py index 5872c9a..601b3f8 100644 --- a/bfxapi/notification.py +++ b/bfxapi/notification.py @@ -1,4 +1,4 @@ -from typing import List, Dict, Union, Optional, Any, TypedDict, Generic, TypeVar, cast +from typing import List, Optional, Any, Generic, TypeVar, cast from dataclasses import dataclass from .labeler import _Type, _Serializer @@ -7,7 +7,7 @@ T = TypeVar("T") @dataclass class Notification(_Type, Generic[T]): mts: int - type: str + type: str message_id: Optional[int] data: T code: Optional[int] @@ -18,7 +18,7 @@ class _Notification(_Serializer, Generic[T]): __LABELS = [ "mts", "type", "message_id", "_PLACEHOLDER", "data", "code", "status", "text" ] def __init__(self, serializer: Optional[_Serializer] = None, is_iterable: bool = False): - super().__init__("Notification", Notification, _Notification.__LABELS, IGNORE = [ "_PLACEHOLDER" ]) + super().__init__("Notification", Notification, _Notification.__LABELS, ignore = [ "_PLACEHOLDER" ]) self.serializer, self.is_iterable = serializer, is_iterable @@ -28,11 +28,11 @@ class _Notification(_Serializer, Generic[T]): if isinstance(self.serializer, _Serializer): data = cast(List[Any], notification.data) - if self.is_iterable == False: + if not self.is_iterable: if len(data) == 1 and isinstance(data[0], list): data = data[0] - notification.data = cast(T, self.serializer.klass(**dict(self.serializer._serialize(*data, skip=skip)))) - else: notification.data = cast(T, [ self.serializer.klass(**dict(self.serializer._serialize(*sub_data, skip=skip))) for sub_data in data ]) + notification.data = self.serializer.parse(*data, skip=skip) + else: notification.data = cast(T, [ self.serializer.parse(*sub_data, skip=skip) for sub_data in data ]) - return notification \ No newline at end of file + return notification diff --git a/bfxapi/urls.py b/bfxapi/urls.py index de31a04..556e4d9 100644 --- a/bfxapi/urls.py +++ b/bfxapi/urls.py @@ -2,4 +2,4 @@ REST_HOST = "https://api.bitfinex.com/v2" PUB_REST_HOST = "https://api-pub.bitfinex.com/v2" WSS_HOST = "wss://api.bitfinex.com/ws/2" -PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2" \ No newline at end of file +PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2" From 7288d05939a15e3d0225a3b047ee6a6566295440 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 6 Mar 2023 17:22:00 +0100 Subject: [PATCH 193/265] Apply pylint's linting rules to bfxapi/utils/*.py. --- .pylintrc | 3 ++- bfxapi/rest/middleware/middleware.py | 2 +- bfxapi/rest/types.py | 2 +- bfxapi/utils/__init__.py | 2 +- bfxapi/utils/camel_and_snake_case_helpers.py | 11 +++++----- .../utils/{JSONEncoder.py => json_encoder.py} | 22 ++++++++++--------- bfxapi/utils/logger.py | 20 ++++++++--------- .../websocket/client/bfx_websocket_client.py | 2 +- .../websocket/client/bfx_websocket_inputs.py | 2 +- bfxapi/websocket/types.py | 2 +- 10 files changed, 36 insertions(+), 32 deletions(-) rename bfxapi/utils/{JSONEncoder.py => json_encoder.py} (65%) diff --git a/.pylintrc b/.pylintrc index 67c33d8..965a203 100644 --- a/.pylintrc +++ b/.pylintrc @@ -7,9 +7,10 @@ ignore=examples [MESSAGES CONTROL] disable= + multiple-imports, missing-docstring, too-few-public-methods, - dangerous-default-value + dangerous-default-value, [FORMAT] diff --git a/bfxapi/rest/middleware/middleware.py b/bfxapi/rest/middleware/middleware.py index 01f6f60..db98723 100644 --- a/bfxapi/rest/middleware/middleware.py +++ b/bfxapi/rest/middleware/middleware.py @@ -6,7 +6,7 @@ from http import HTTPStatus from ..enums import Error from ..exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError -from ...utils.JSONEncoder import JSONEncoder +from ...utils.json_encoder import JSONEncoder if TYPE_CHECKING: from requests.sessions import _Params diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 9e3e752..32c2a9b 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from .. labeler import _Type, partial, compose from .. notification import Notification -from .. utils.JSONEncoder import JSON +from ..utils.json_encoder import JSON #region Type hinting for Rest Public Endpoints diff --git a/bfxapi/utils/__init__.py b/bfxapi/utils/__init__.py index 5a6afd1..df6e2da 100644 --- a/bfxapi/utils/__init__.py +++ b/bfxapi/utils/__init__.py @@ -1 +1 @@ -NAME = "utils" \ No newline at end of file +NAME = "utils" diff --git a/bfxapi/utils/camel_and_snake_case_helpers.py b/bfxapi/utils/camel_and_snake_case_helpers.py index 7255940..38a7993 100644 --- a/bfxapi/utils/camel_and_snake_case_helpers.py +++ b/bfxapi/utils/camel_and_snake_case_helpers.py @@ -1,22 +1,23 @@ import re -from typing import TypeVar, Callable, Dict, Any, cast +from typing import TypeVar, Callable, cast T = TypeVar("T") _to_snake_case: Callable[[str], str] = lambda string: re.sub(r"(? T: if isinstance(data, list): return cast(T, [ _scheme(sub_data, adapter) for sub_data in data ]) - elif isinstance(data, dict): + if isinstance(data, dict): return cast(T, { adapter(key): _scheme(value, adapter) for key, value in data.items() }) - else: return data + return data def to_snake_case_keys(dictionary: T) -> T: return _scheme(dictionary, _to_snake_case) def to_camel_case_keys(dictionary: T) -> T: - return _scheme(dictionary, _to_camel_case) \ No newline at end of file + return _scheme(dictionary, _to_camel_case) diff --git a/bfxapi/utils/JSONEncoder.py b/bfxapi/utils/json_encoder.py similarity index 65% rename from bfxapi/utils/JSONEncoder.py rename to bfxapi/utils/json_encoder.py index edaba00..21f0b7e 100644 --- a/bfxapi/utils/JSONEncoder.py +++ b/bfxapi/utils/json_encoder.py @@ -7,23 +7,25 @@ from typing import Type, List, Dict, Union, Any JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] def _strip(dictionary: Dict) -> Dict: - return { key: value for key, value in dictionary.items() if value != None} + return { key: value for key, value in dictionary.items() if value is not None } def _convert_float_to_str(data: JSON) -> JSON: if isinstance(data, float): return format(Decimal(repr(data)), "f") - elif isinstance(data, list): + if isinstance(data, list): return [ _convert_float_to_str(sub_data) for sub_data in data ] - elif isinstance(data, dict): + if isinstance(data, dict): return _strip({ key: _convert_float_to_str(value) for key, value in data.items() }) - else: return data + return data class JSONEncoder(json.JSONEncoder): - def encode(self, obj: JSON) -> str: - return json.JSONEncoder.encode(self, _convert_float_to_str(obj)) + def encode(self, o: JSON) -> str: + return json.JSONEncoder.encode(self, _convert_float_to_str(o)) - def default(self, obj: Any) -> Any: - if isinstance(obj, Decimal): return format(obj, "f") - elif isinstance(obj, datetime): return str(obj) + def default(self, o: Any) -> Any: + if isinstance(o, Decimal): + return format(o, "f") + if isinstance(o, datetime): + return str(o) - return json.JSONEncoder.default(self, obj) \ No newline at end of file + return json.JSONEncoder.default(self, o) diff --git a/bfxapi/utils/logger.py b/bfxapi/utils/logger.py index 88d4a59..6ebac5a 100644 --- a/bfxapi/utils/logger.py +++ b/bfxapi/utils/logger.py @@ -28,24 +28,24 @@ class _ColorFormatter(logging.Formatter): class ColorLogger(logging.Logger): FORMAT = "[%(name)s] [%(levelname)s] [%(asctime)s] %(message)s" - + def __init__(self, name, level): - logging.Logger.__init__(self, name, level) + logging.Logger.__init__(self, name, level) colored_formatter = _ColorFormatter(self.FORMAT, use_color=True) - console = logging.StreamHandler(stream=sys.stderr) - console.setFormatter(fmt=colored_formatter) + handler = logging.StreamHandler(stream=sys.stderr) + handler.setFormatter(fmt=colored_formatter) - self.addHandler(hdlr=console) + self.addHandler(hdlr=handler) class FileLogger(logging.Logger): FORMAT = "[%(name)s] [%(levelname)s] [%(asctime)s] %(message)s" - + def __init__(self, name, level, filename): - logging.Logger.__init__(self, name, level) + logging.Logger.__init__(self, name, level) formatter = logging.Formatter(self.FORMAT) - fh = logging.FileHandler(filename=filename) - fh.setFormatter(fmt=formatter) + handler = logging.FileHandler(filename=filename) + handler.setFormatter(fmt=formatter) - self.addHandler(hdlr=fh) \ No newline at end of file + self.addHandler(hdlr=handler) diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 11cfa18..ba8843d 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -14,7 +14,7 @@ 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.json_encoder import JSONEncoder from ...utils.logger import ColorLogger, FileLogger diff --git a/bfxapi/websocket/client/bfx_websocket_inputs.py b/bfxapi/websocket/client/bfx_websocket_inputs.py index 4b4e04c..f3d615f 100644 --- a/bfxapi/websocket/client/bfx_websocket_inputs.py +++ b/bfxapi/websocket/client/bfx_websocket_inputs.py @@ -3,7 +3,7 @@ from datetime import datetime from typing import Union, Optional, List, Tuple from .. enums import OrderType, FundingOfferType -from ... utils.JSONEncoder import JSON +from ...utils.json_encoder import JSON class BfxWebsocketInputs(object): def __init__(self, handle_websocket_input): diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py index ae082af..8951141 100644 --- a/bfxapi/websocket/types.py +++ b/bfxapi/websocket/types.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from .. labeler import _Type from .. notification import Notification -from .. utils.JSONEncoder import JSON +from ..utils.json_encoder import JSON #region Type hinting for Websocket Public Channels From a7f5aa684a5d9548c5f157fbbbe78e75d750cd6b Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 6 Mar 2023 17:28:56 +0100 Subject: [PATCH 194/265] Apply pylint's linting rules to bfxapi/websocket/__init__.py, bfxapi/websocket/enums.py, bfxapi/websocket/exceptions.py, bfxapi/websocket/serializers.py and bfxapi/websocket/subscriptions.py. --- .pylintrc | 2 +- bfxapi/websocket/__init__.py | 2 +- bfxapi/websocket/enums.py | 3 ++- bfxapi/websocket/exceptions.py | 16 +--------------- bfxapi/websocket/serializers.py | 7 ++++--- bfxapi/websocket/subscriptions.py | 8 +++++--- 6 files changed, 14 insertions(+), 24 deletions(-) diff --git a/.pylintrc b/.pylintrc index 965a203..1f33eb4 100644 --- a/.pylintrc +++ b/.pylintrc @@ -14,4 +14,4 @@ disable= [FORMAT] -max-line-length=120 \ No newline at end of file +max-line-length=130 \ No newline at end of file diff --git a/bfxapi/websocket/__init__.py b/bfxapi/websocket/__init__.py index 1287433..6be76dd 100644 --- a/bfxapi/websocket/__init__.py +++ b/bfxapi/websocket/__init__.py @@ -1,3 +1,3 @@ from .client import BfxWebsocketClient, BfxWebsocketBucket, BfxWebsocketInputs -NAME = "websocket" \ No newline at end of file +NAME = "websocket" diff --git a/bfxapi/websocket/enums.py b/bfxapi/websocket/enums.py index 1877cea..2ff57ca 100644 --- a/bfxapi/websocket/enums.py +++ b/bfxapi/websocket/enums.py @@ -1,3 +1,4 @@ +#pylint: disable-next=wildcard-import,unused-wildcard-import from .. enums import * class Channel(str, Enum): @@ -5,4 +6,4 @@ class Channel(str, Enum): TRADES = "trades" BOOK = "book" CANDLES = "candles" - STATUS = "status" \ No newline at end of file + STATUS = "status" diff --git a/bfxapi/websocket/exceptions.py b/bfxapi/websocket/exceptions.py index 40a6a1e..49049fd 100644 --- a/bfxapi/websocket/exceptions.py +++ b/bfxapi/websocket/exceptions.py @@ -16,53 +16,39 @@ class BfxWebsocketException(BfxBaseException): Base class for all custom exceptions in bfxapi/websocket/exceptions.py. """ - pass - class ConnectionNotOpen(BfxWebsocketException): """ This error indicates an attempt to communicate via websocket before starting the connection with the servers. """ - pass - class TooManySubscriptions(BfxWebsocketException): """ - This error indicates an attempt to subscribe to a public channel after reaching the limit of simultaneous connections. + This error indicates a subscription attempt after reaching the limit of simultaneous connections. """ - pass - class WebsocketAuthenticationRequired(BfxWebsocketException): """ This error indicates an attempt to access a protected resource without logging in first. """ - pass - class EventNotSupported(BfxWebsocketException): """ This error indicates a failed attempt to subscribe to an event not supported by the BfxWebsocketClient. """ - pass - class OutdatedClientVersion(BfxWebsocketException): """ This error indicates a mismatch between the client version and the server WSS version. """ - pass class InvalidAuthenticationCredentials(BfxWebsocketException): """ This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication. """ - pass class HandlerNotFound(BfxWebsocketException): """ This error indicates that a handler was not found for an incoming message. """ - - pass \ No newline at end of file diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index 29c28c7..ae2f04e 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -2,6 +2,7 @@ from . import types from .. labeler import generate_labeler_serializer +#pylint: disable-next=unused-import from .. notification import _Notification __serializers__ = [ @@ -49,14 +50,14 @@ FundingCurrencyTicker = generate_labeler_serializer("FundingCurrencyTicker", kla "frr_amount_available" ]) -TradingPairTrade = generate_labeler_serializer("TradingPairTrade", klass=types.TradingPairTrade, labels=[ +TradingPairTrade = generate_labeler_serializer("TradingPairTrade", klass=types.TradingPairTrade, labels=[ "id", "mts", "amount", "price" ]) -FundingCurrencyTrade = generate_labeler_serializer("FundingCurrencyTrade", klass=types.FundingCurrencyTrade, labels=[ +FundingCurrencyTrade = generate_labeler_serializer("FundingCurrencyTrade", klass=types.FundingCurrencyTrade, labels=[ "id", "mts", "amount", @@ -290,4 +291,4 @@ Balance = generate_labeler_serializer("Balance", klass=types.Balance, labels=[ "aum_net", ]) -#endregion \ No newline at end of file +#endregion diff --git a/bfxapi/websocket/subscriptions.py b/bfxapi/websocket/subscriptions.py index 10cbbfe..922a532 100644 --- a/bfxapi/websocket/subscriptions.py +++ b/bfxapi/websocket/subscriptions.py @@ -15,12 +15,14 @@ _Header = TypedDict("_Header", { "event": Literal["subscribed"], "channel": str, Subscription = Union["Ticker", "Trades", "Book", "Candles", "Status"] class Ticker(TypedDict): - subId: str; symbol: str + subId: str + symbol: str pair: Optional[str] currency: Optional[str] class Trades(TypedDict): - subId: str; symbol: str + subId: str + symbol: str pair: Optional[str] currency: Optional[str] @@ -38,4 +40,4 @@ class Candles(TypedDict): class Status(TypedDict): subId: str - key: str \ No newline at end of file + key: str From 7e627dd239144e29516b25dddccd9927151806e6 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 6 Mar 2023 17:53:54 +0100 Subject: [PATCH 195/265] Apply pylint's linting rules to bfxapi/websocket/handlers/*.py. --- .pylintrc | 1 - bfxapi/labeler.py | 2 + bfxapi/websocket/handlers/__init__.py | 2 +- .../authenticated_channels_handler.py | 52 +++++++-------- .../handlers/public_channels_handler.py | 63 ++++++++++--------- 5 files changed, 62 insertions(+), 58 deletions(-) diff --git a/.pylintrc b/.pylintrc index 1f33eb4..a7f91cd 100644 --- a/.pylintrc +++ b/.pylintrc @@ -10,7 +10,6 @@ disable= multiple-imports, missing-docstring, too-few-public-methods, - dangerous-default-value, [FORMAT] diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index cac623a..4d3a86f 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -1,3 +1,5 @@ +#pylint: disable=dangerous-default-value + from typing import Type, Generic, TypeVar, Iterable, Optional, Dict, List, Tuple, Any, cast from .exceptions import LabelerSerializerException diff --git a/bfxapi/websocket/handlers/__init__.py b/bfxapi/websocket/handlers/__init__.py index 02e9c81..e339402 100644 --- a/bfxapi/websocket/handlers/__init__.py +++ b/bfxapi/websocket/handlers/__init__.py @@ -1,4 +1,4 @@ from .public_channels_handler import PublicChannelsHandler from .authenticated_channels_handler import AuthenticatedChannelsHandler -NAME = "handlers" \ No newline at end of file +NAME = "handlers" diff --git a/bfxapi/websocket/handlers/authenticated_channels_handler.py b/bfxapi/websocket/handlers/authenticated_channels_handler.py index 2dbd83f..221217e 100644 --- a/bfxapi/websocket/handlers/authenticated_channels_handler.py +++ b/bfxapi/websocket/handlers/authenticated_channels_handler.py @@ -1,18 +1,19 @@ from .. import serializers -from .. types import * +from .. serializers import _Notification from .. exceptions import HandlerNotFound -class AuthenticatedChannelsHandler(object): +class AuthenticatedChannelsHandler: __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", + "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", } @@ -27,43 +28,42 @@ class AuthenticatedChannelsHandler(object): ("bu",): serializers.Balance } - EVENTS = [ + EVENTS = [ "notification", "on-req-notification", "ou-req-notification", "oc-req-notification", "oc_multi-notification", "fon-req-notification", "foc-req-notification", - *list(__abbreviations.values()) + *list(__abbreviations.values()) ] - def __init__(self, event_emitter, strict = True): - self.event_emitter, self.strict = event_emitter, strict + def __init__(self, event_emitter): + self.event_emitter = event_emitter - def handle(self, type, stream): - if type == "n": + def handle(self, abbrevation, stream): + if abbrevation == "n": return self.__notification(stream) - for types, serializer in AuthenticatedChannelsHandler.__serializers.items(): - if type in types: - event = AuthenticatedChannelsHandler.__abbreviations[type] + for abbrevations, serializer in AuthenticatedChannelsHandler.__serializers.items(): + if abbrevation in abbrevations: + event = AuthenticatedChannelsHandler.__abbreviations[abbrevation] 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}>.") - + raise HandlerNotFound(f"No handler found for event of type <{abbrevation}>.") + def __notification(self, stream): - type, serializer = "notification", serializers._Notification(serializer=None) + event, serializer = "notification", _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) + event, serializer = f"{stream[1]}-notification", _Notification(serializer=serializers.Order) if stream[1] == "oc_multi-req": - type, serializer = f"{stream[1]}-notification", serializers._Notification(serializer=serializers.Order, iterate=True) + event, serializer = f"{stream[1]}-notification", _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) + event, serializer = f"{stream[1]}-notification", _Notification(serializer=serializers.FundingOffer) - return self.event_emitter.emit(type, serializer.parse(*stream)) \ No newline at end of file + return self.event_emitter.emit(event, serializer.parse(*stream)) diff --git a/bfxapi/websocket/handlers/public_channels_handler.py b/bfxapi/websocket/handlers/public_channels_handler.py index 52e47ef..eb96482 100644 --- a/bfxapi/websocket/handlers/public_channels_handler.py +++ b/bfxapi/websocket/handlers/public_channels_handler.py @@ -1,20 +1,19 @@ -from .. import serializers +#pylint: disable=inconsistent-return-statements -from .. types import * +from .. import serializers from .. exceptions import HandlerNotFound -class PublicChannelsHandler(object): +class PublicChannelsHandler: EVENTS = [ - "t_ticker_update", "f_ticker_update", - "t_trade_executed", "t_trade_execution_update", "f_trade_executed", "f_trade_execution_update", "t_trades_snapshot", "f_trades_snapshot", - "t_book_snapshot", "f_book_snapshot", "t_raw_book_snapshot", "f_raw_book_snapshot", "t_book_update", "f_book_update", "t_raw_book_update", "f_raw_book_update", - "candles_snapshot", "candles_update", - "derivatives_status_update", + "t_ticker_update", "f_ticker_update", "t_trade_executed", "t_trade_execution_update", "f_trade_executed", + "f_trade_execution_update", "t_trades_snapshot", "f_trades_snapshot", "t_book_snapshot", "f_book_snapshot", + "t_raw_book_snapshot", "f_raw_book_snapshot", "t_book_update", "f_book_update", "t_raw_book_update", + "f_raw_book_update", "candles_snapshot", "candles_update", "derivatives_status_update", ] - def __init__(self, event_emitter, strict = True): - self.event_emitter, self.strict = event_emitter, strict + def __init__(self, event_emitter): + self.event_emitter = event_emitter self.__handlers = { "ticker": self.__ticker_channel_handler, @@ -25,13 +24,14 @@ class PublicChannelsHandler(object): } def handle(self, subscription, *stream): + #pylint: disable-next=unnecessary-lambda-assignment _clear = lambda dictionary, *args: { key: value for key, value in dictionary.items() if key not in args } + #pylint: disable-next=consider-iterating-dictionary if (channel := subscription["channel"]) and channel in self.__handlers.keys(): return self.__handlers[channel](_clear(subscription, "event", "channel", "chanId"), *stream) - if self.strict: - raise HandlerNotFound(f"No handler found for channel <{subscription['channel']}>.") + raise HandlerNotFound(f"No handler found for channel <{subscription['channel']}>.") def __ticker_channel_handler(self, subscription, *stream): if subscription["symbol"].startswith("t"): @@ -49,17 +49,17 @@ class PublicChannelsHandler(object): ) def __trades_channel_handler(self, subscription, *stream): - if (type := stream[0]) and type in [ "te", "tu", "fte", "ftu" ]: + if (event := stream[0]) and event in [ "te", "tu", "fte", "ftu" ]: if subscription["symbol"].startswith("t"): return self.event_emitter.emit( - { "te": "t_trade_executed", "tu": "t_trade_execution_update" }[type], + { "te": "t_trade_executed", "tu": "t_trade_execution_update" }[event], subscription, serializers.TradingPairTrade.parse(*stream[1]) ) if subscription["symbol"].startswith("f"): return self.event_emitter.emit( - { "fte": "f_trade_executed", "ftu": "f_trade_execution_update" }[type], + { "fte": "f_trade_executed", "ftu": "f_trade_execution_update" }[event], subscription, serializers.FundingCurrencyTrade.parse(*stream[1]) ) @@ -79,36 +79,39 @@ class PublicChannelsHandler(object): ) def __book_channel_handler(self, subscription, *stream): - type = subscription["symbol"][0] + event = subscription["symbol"][0] if subscription["prec"] == "R0": - _trading_pair_serializer, _funding_currency_serializer, IS_RAW_BOOK = serializers.TradingPairRawBook, serializers.FundingCurrencyRawBook, True - else: _trading_pair_serializer, _funding_currency_serializer, IS_RAW_BOOK = serializers.TradingPairBook, serializers.FundingCurrencyBook, False + _trading_pair_serializer, _funding_currency_serializer, is_raw_book = \ + serializers.TradingPairRawBook, serializers.FundingCurrencyRawBook, True + else: _trading_pair_serializer, _funding_currency_serializer, is_raw_book = \ + serializers.TradingPairBook, serializers.FundingCurrencyBook, False if all(isinstance(substream, list) for substream in stream[0]): - return self.event_emitter.emit( - type + "_" + (IS_RAW_BOOK and "raw_book" or "book") + "_snapshot", - subscription, - [ { "t": _trading_pair_serializer, "f": _funding_currency_serializer }[type].parse(*substream) for substream in stream[0] ] + return self.event_emitter.emit( + event + "_" + (is_raw_book and "raw_book" or "book") + "_snapshot", + subscription, + [ { "t": _trading_pair_serializer, "f": _funding_currency_serializer }[event] \ + .parse(*substream) for substream in stream[0] ] ) return self.event_emitter.emit( - type + "_" + (IS_RAW_BOOK and "raw_book" or "book") + "_update", - subscription, - { "t": _trading_pair_serializer, "f": _funding_currency_serializer }[type].parse(*stream[0]) + event + "_" + (is_raw_book and "raw_book" or "book") + "_update", + subscription, + { "t": _trading_pair_serializer, "f": _funding_currency_serializer }[event].parse(*stream[0]) ) - + def __candles_channel_handler(self, subscription, *stream): if all(isinstance(substream, list) for substream in stream[0]): return self.event_emitter.emit( "candles_snapshot", - subscription, + subscription, [ serializers.Candle.parse(*substream) for substream in stream[0] ] ) return self.event_emitter.emit( - "candles_update", - subscription, + "candles_update", + subscription, serializers.Candle.parse(*stream[0]) ) @@ -118,4 +121,4 @@ class PublicChannelsHandler(object): "derivatives_status_update", subscription, serializers.DerivativesStatus.parse(*stream[0]) - ) \ No newline at end of file + ) From 5c707d79290e09e60427e43a6dd32986faa6316e Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 6 Mar 2023 18:46:04 +0100 Subject: [PATCH 196/265] Apply pylint's linting rules to bfxapi/websocket/client/*.py. --- .pylintrc | 12 +- bfxapi/client.py | 2 +- bfxapi/labeler.py | 2 - bfxapi/rest/endpoints/bfx_rest_interface.py | 2 +- bfxapi/tests/test_labeler.py | 4 +- bfxapi/websocket/client/__init__.py | 2 +- .../websocket/client/bfx_websocket_bucket.py | 53 +++---- .../websocket/client/bfx_websocket_client.py | 133 ++++++++++-------- .../websocket/client/bfx_websocket_inputs.py | 88 ++++++++---- .../handlers/public_channels_handler.py | 2 - 10 files changed, 173 insertions(+), 127 deletions(-) diff --git a/.pylintrc b/.pylintrc index a7f91cd..58097c3 100644 --- a/.pylintrc +++ b/.pylintrc @@ -10,7 +10,17 @@ disable= multiple-imports, missing-docstring, too-few-public-methods, + too-many-instance-attributes, + dangerous-default-value, + inconsistent-return-statements, + logging-not-lazy [FORMAT] -max-line-length=130 \ No newline at end of file +max-line-length=130 + +good-names=id,on + +[TYPECHECK] + +generated-members=websockets \ No newline at end of file diff --git a/bfxapi/client.py b/bfxapi/client.py index 025ee08..db54b73 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -19,7 +19,7 @@ class Client: credentials = None if api_key and api_secret: - credentials = { "API_KEY": api_key, "API_SECRET": api_secret, "filters": filters } + credentials = { "api_key": api_key, "api_secret": api_secret, "filters": filters } self.rest = BfxRestInterface( host=rest_host, diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 4d3a86f..cac623a 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -1,5 +1,3 @@ -#pylint: disable=dangerous-default-value - from typing import Type, Generic, TypeVar, Iterable, Optional, Dict, List, Tuple, Any, cast from .exceptions import LabelerSerializerException diff --git a/bfxapi/rest/endpoints/bfx_rest_interface.py b/bfxapi/rest/endpoints/bfx_rest_interface.py index b117fa6..fdc05a3 100644 --- a/bfxapi/rest/endpoints/bfx_rest_interface.py +++ b/bfxapi/rest/endpoints/bfx_rest_interface.py @@ -9,7 +9,7 @@ class BfxRestInterface(object): def __init__(self, host, credentials = None): API_KEY, API_SECRET = credentials and \ - (credentials["API_KEY"], credentials["API_SECRET"]) or (None, None) + (credentials["api_key"], credentials["api_secret"]) or (None, None) self.public = RestPublicEndpoints(host=host) self.auth = RestAuthenticatedEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET) diff --git a/bfxapi/tests/test_labeler.py b/bfxapi/tests/test_labeler.py index a4310ef..9ee8ea0 100644 --- a/bfxapi/tests/test_labeler.py +++ b/bfxapi/tests/test_labeler.py @@ -46,8 +46,8 @@ class TestLabeler(unittest.TestCase): F: bool inner = generate_labeler_serializer("Inner", Inner, ["F"]) - middle = generate_recursive_serializer("Middle", Middle, ["D", "E"], { "E": inner }) - outer = generate_recursive_serializer("Outer", Outer, ["A", "B", "C"], { "C": middle }) + middle = generate_recursive_serializer("Middle", Middle, ["D", "E"], serializers={ "E": inner }) + outer = generate_recursive_serializer("Outer", Outer, ["A", "B", "C"], serializers={ "C": middle }) self.assertEqual(outer.parse(10, 45.5, [ "Y", [ True ] ]), Outer(10, 45.5, Middle("Y", Inner(True))), msg="_RecursiveSerializer should produce the right result.") diff --git a/bfxapi/websocket/client/__init__.py b/bfxapi/websocket/client/__init__.py index 50057cb..fa12a78 100644 --- a/bfxapi/websocket/client/__init__.py +++ b/bfxapi/websocket/client/__init__.py @@ -2,4 +2,4 @@ from .bfx_websocket_client import BfxWebsocketClient from .bfx_websocket_bucket import BfxWebsocketBucket from .bfx_websocket_inputs import BfxWebsocketInputs -NAME = "client" \ No newline at end of file +NAME = "client" diff --git a/bfxapi/websocket/client/bfx_websocket_bucket.py b/bfxapi/websocket/client/bfx_websocket_bucket.py index 90c8d21..c6f797d 100644 --- a/bfxapi/websocket/client/bfx_websocket_bucket.py +++ b/bfxapi/websocket/client/bfx_websocket_bucket.py @@ -1,10 +1,10 @@ -import json, uuid, websockets - from typing import Literal, TypeVar, Callable, cast +import json, uuid, websockets + from ..handlers import PublicChannelsHandler -from ..exceptions import ConnectionNotOpen, TooManySubscriptions, OutdatedClientVersion +from ..exceptions import ConnectionNotOpen, TooManySubscriptions _HEARTBEAT = "hb" @@ -12,14 +12,14 @@ F = TypeVar("F", bound=Callable[..., Literal[None]]) def _require_websocket_connection(function: F) -> F: async def wrapper(self, *args, **kwargs): - if self.websocket == None or self.websocket.open == False: + if self.websocket is None or not self.websocket.open: raise ConnectionNotOpen("No open connection with the server.") - + await function(self, *args, **kwargs) return cast(F, wrapper) -class BfxWebsocketBucket(object): +class BfxWebsocketBucket: VERSION = 2 MAXIMUM_SUBSCRIPTIONS_AMOUNT = 25 @@ -27,11 +27,12 @@ class BfxWebsocketBucket(object): def __init__(self, host, event_emitter, on_open_event): self.host, self.event_emitter, self.on_open_event = host, event_emitter, on_open_event - self.websocket, self.subscriptions, self.pendings = None, dict(), list() + self.websocket, self.subscriptions, self.pendings = None, {}, [] self.handler = PublicChannelsHandler(event_emitter=self.event_emitter) - async def _connect(self, index): + #pylint: disable-next=unused-argument + async def connect(self, index): reconnection = False async for websocket in websockets.connect(self.host): @@ -39,12 +40,12 @@ class BfxWebsocketBucket(object): self.on_open_event.set() - if reconnection == True or (reconnection := False): + if reconnection 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) + await self.subscribe(**subscription) self.subscriptions.clear() @@ -52,21 +53,21 @@ class BfxWebsocketBucket(object): async for message in websocket: message = json.loads(message) - if isinstance(message, dict) and message["event"] == "subscribed" and (chanId := message["chanId"]): + if isinstance(message, dict) and message["event"] == "subscribed" and (chan_id := message["chanId"]): self.pendings = [ pending for pending in self.pendings if pending["subId"] != message["subId"] ] - self.subscriptions[chanId] = message + self.subscriptions[chan_id] = message self.event_emitter.emit("subscribed", message) - elif isinstance(message, dict) and message["event"] == "unsubscribed" and (chanId := message["chanId"]): + elif isinstance(message, dict) and message["event"] == "unsubscribed" and (chan_id := message["chanId"]): if message["status"] == "OK": - del self.subscriptions[chanId] + del self.subscriptions[chan_id] elif isinstance(message, dict) and message["event"] == "error": self.event_emitter.emit("wss-error", message["code"], message["msg"]) - elif isinstance(message, list) and (chanId := message[0]) and message[1] != _HEARTBEAT: - self.handler.handle(self.subscriptions[chanId], *message[1:]) - except websockets.ConnectionClosedError as error: + elif isinstance(message, list) and (chan_id := message[0]) and message[1] != _HEARTBEAT: + self.handler.handle(self.subscriptions[chan_id], *message[1:]) + except websockets.ConnectionClosedError as error: if error.code == 1006: self.on_open_event.clear() - reconnection = True + reconnection = True continue raise error @@ -74,7 +75,7 @@ class BfxWebsocketBucket(object): break @_require_websocket_connection - async def _subscribe(self, channel, subId=None, **kwargs): + async def subscribe(self, channel, sub_id=None, **kwargs): if len(self.subscriptions) + len(self.pendings) == BfxWebsocketBucket.MAXIMUM_SUBSCRIPTIONS_AMOUNT: raise TooManySubscriptions("The client has reached the maximum number of subscriptions.") @@ -83,7 +84,7 @@ class BfxWebsocketBucket(object): "event": "subscribe", "channel": channel, - "subId": subId or str(uuid.uuid4()), + "subId": sub_id or str(uuid.uuid4()), } self.pendings.append(subscription) @@ -91,17 +92,17 @@ class BfxWebsocketBucket(object): await self.websocket.send(json.dumps(subscription)) @_require_websocket_connection - async def _unsubscribe(self, chanId): + async def unsubscribe(self, chan_id): await self.websocket.send(json.dumps({ "event": "unsubscribe", - "chanId": chanId + "chanId": chan_id })) @_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) - def _get_chan_id(self, subId): + def get_chan_id(self, sub_id): for subscription in self.subscriptions.values(): - if subscription["subId"] == subId: - return subscription["chanId"] \ No newline at end of file + if subscription["subId"] == sub_id: + return subscription["chanId"] diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index ba8843d..71ef5a1 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -1,18 +1,19 @@ -import traceback, json, asyncio, hmac, hashlib, time, websockets, socket, random - from typing import cast from collections import namedtuple from datetime import datetime +import traceback, json, asyncio, hmac, hashlib, time, socket, random, websockets + 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 ..exceptions import WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, \ + OutdatedClientVersion from ...utils.json_encoder import JSONEncoder @@ -20,14 +21,15 @@ from ...utils.logger import ColorLogger, FileLogger 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.") - + if hasattr(self, "authentication") and not self.authentication: + 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): +class BfxWebsocketClient: VERSION = BfxWebsocketBucket.VERSION MAXIMUM_CONNECTIONS_AMOUNT = 20 @@ -43,16 +45,18 @@ class BfxWebsocketClient(object): self.host, self.credentials, self.event_emitter = host, credentials, AsyncIOEventEmitter() + self.on_open_events, self.buckets, self.authentication = [], [], False + self.inputs = BfxWebsocketInputs(handle_websocket_input=self.__handle_websocket_input) self.handler = AuthenticatedChannelsHandler(event_emitter=self.event_emitter) - if log_filename == None: + if log_filename is None: self.logger = ColorLogger("BfxWebsocketClient", level=log_level) else: self.logger = FileLogger("BfxWebsocketClient", level=log_level, filename=log_filename) - self.event_emitter.add_listener("error", - lambda exception: self.logger.error(f"{type(exception).__name__}: {str(exception)}" + "\n" + + 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]) ) @@ -61,23 +65,24 @@ class BfxWebsocketClient(object): 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.logger.warning(f"It is not safe to use more than {BfxWebsocketClient.MAXIMUM_CONNECTIONS_AMOUNT} " + + f"buckets from the same 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) ] + for _ in range(connections): + self.on_open_events.append(asyncio.Event()) - self.buckets = [ - BfxWebsocketBucket(self.host, self.event_emitter, self.on_open_events[index]) - for index in range(connections) - ] + for index in range(connections): + self.buckets += [BfxWebsocketBucket(self.host, self.event_emitter, self.on_open_events[index])] - tasks = [ bucket._connect(index) for index, bucket in enumerate(self.buckets) ] - - tasks.append(self.__connect(self.credentials)) + tasks = [ bucket.connect(index) for index, bucket in enumerate(self.buckets) ] + + tasks.append(self.__connect()) await asyncio.gather(*tasks) - async def __connect(self, credentials = None): + #pylint: disable-next=too-many-statements + async def __connect(self): Reconnection = namedtuple("Reconnection", ["status", "attempts", "timestamp"]) reconnection, delay = Reconnection(status=False, attempts=0, timestamp=None), None @@ -86,10 +91,10 @@ class BfxWebsocketClient(object): 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}).") + if reconnection.status: + 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) @@ -106,20 +111,22 @@ class BfxWebsocketClient(object): 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']}).") + raise OutdatedClientVersion("Mismatch between the client version and the server version. " + + "Update the library to the latest version to continue (client version: " + + f"{BfxWebsocketClient.VERSION}, 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 + 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: + elif isinstance(message, list) and message[0] == 0 and message[1] != _HEARTBEAT: self.handler.handle(message[1], message[2]) class _Delay: @@ -138,52 +145,52 @@ class BfxWebsocketClient(object): 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: + if reconnection.status: 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 isinstance(error, websockets.ConnectionClosedError) and error.code in (1006, 1012): if error.code == 1006: - self.logger.error("Connection lost: no close frame received " + 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()); - + + reconnection = Reconnection(status=True, attempts=1, timestamp=datetime.now()) + delay = _Delay(backoff_factor=1.618) - elif isinstance(error, socket.gaierror) and reconnection.status == True: + elif isinstance(error, socket.gaierror) and reconnection.status: self.logger.warning(f"Reconnection attempt no.{reconnection.attempts} has failed. " - + f"Next reconnection attempt in ~{round(delay.peek()):.1f} seconds." + + 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: + if not reconnection.status: break - async def __authenticate(self, API_KEY, API_SECRET, filters=None): - data = { "event": "auth", "filter": filters, "apiKey": API_KEY } + async def __authenticate(self, api_key, api_secret, filters=None): + data = { "event": "auth", "filter": filters, "apiKey": api_key } data["authNonce"] = str(round(time.time() * 1_000_000)) data["authPayload"] = "AUTH" + data["authNonce"] data["authSig"] = hmac.new( - API_SECRET.encode("utf8"), + api_secret.encode("utf8"), data["authPayload"].encode("utf8"), - hashlib.sha384 + hashlib.sha384 ).hexdigest() await self.websocket.send(json.dumps(data)) @@ -193,56 +200,58 @@ class BfxWebsocketClient(object): index = counters.index(min(counters)) - await self.buckets[index]._subscribe(channel, **kwargs) + await self.buckets[index].subscribe(channel, **kwargs) - async def unsubscribe(self, subId): + async def unsubscribe(self, sub_id): for bucket in self.buckets: - if (chanId := bucket._get_chan_id(subId)): - await bucket._unsubscribe(chanId=chanId) + if (chan_id := bucket.get_chan_id(sub_id)): + await bucket.unsubscribe(chan_id=chan_id) async def close(self, code=1000, reason=str()): - if self.websocket != None and self.websocket.open == True: + if self.websocket is not None and self.websocket.open: await self.websocket.close(code=code, reason=reason) for bucket in self.buckets: - await bucket._close(code=code, reason=reason) + 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 } ])) + 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)) + async def __handle_websocket_input(self, event, data): + await self.websocket.send(json.dumps([ 0, event, 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") + raise EventNotSupported(f"Event <{event}> is not supported. To get a list " + + "of available events print BfxWebsocketClient.EVENTS") - if callback != None: + if callback is not None: for event in events: self.event_emitter.on(event, callback) - if callback == None: + if callback is None: def handler(function): for event in events: self.event_emitter.on(event, function) - return handler + 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") + raise EventNotSupported(f"Event <{event}> is not supported. To get a list " + + "of available events print BfxWebsocketClient.EVENTS") - if callback != None: + if callback is not None: for event in events: self.event_emitter.once(event, callback) - if callback == None: + if callback is None: def handler(function): for event in events: self.event_emitter.once(event, function) - return handler \ No newline at end of file + return handler diff --git a/bfxapi/websocket/client/bfx_websocket_inputs.py b/bfxapi/websocket/client/bfx_websocket_inputs.py index f3d615f..5ff0769 100644 --- a/bfxapi/websocket/client/bfx_websocket_inputs.py +++ b/bfxapi/websocket/client/bfx_websocket_inputs.py @@ -1,3 +1,5 @@ +#pylint: disable=invalid-name,redefined-builtin,too-many-arguments + from decimal import Decimal from datetime import datetime @@ -5,56 +7,84 @@ from typing import Union, Optional, List, Tuple from .. enums import OrderType, FundingOfferType from ...utils.json_encoder import JSON -class BfxWebsocketInputs(object): +class BfxWebsocketInputs: 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], - price: Optional[Union[Decimal, float, str]] = None, lev: Optional[int] = None, - price_trailing: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_oco_stop: Optional[Union[Decimal, float, str]] = None, - gid: Optional[int] = None, cid: Optional[int] = None, - flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None): - await self.handle_websocket_input("on", { + async def submit_order(self, + type: OrderType, + symbol: str, + amount: Union[Decimal, float, str], + price: Optional[Union[Decimal, float, str]] = None, + lev: Optional[int] = None, + price_trailing: Optional[Union[Decimal, float, str]] = None, + price_aux_limit: Optional[Union[Decimal, float, str]] = None, + price_oco_stop: Optional[Union[Decimal, float, str]] = None, + gid: Optional[int] = None, + cid: Optional[int] = None, + flags: Optional[int] = 0, + tif: Optional[Union[datetime, str]] = None, + meta: Optional[JSON] = None): + await self.__handle_websocket_input("on", { "type": type, "symbol": symbol, "amount": amount, - "price": price, "lev": lev, - "price_trailing": price_trailing, "price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop, - "gid": gid, "cid": cid, - "flags": flags, "tif": tif, "meta": meta + "price": price, "lev": lev, "price_trailing": price_trailing, + "price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop, "gid": gid, + "cid": cid, "flags": flags, "tif": tif, + "meta": meta }) - async def update_order(self, id: int, amount: Optional[Union[Decimal, float, str]] = None, price: Optional[Union[Decimal, float, str]] = None, - cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, - flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, float, str]] = None, - price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, tif: Optional[Union[datetime, str]] = None): - await self.handle_websocket_input("ou", { + async def update_order(self, + id: int, + amount: Optional[Union[Decimal, float, str]] = None, + price: Optional[Union[Decimal, float, str]] = None, + cid: Optional[int] = None, + cid_date: Optional[str] = None, + gid: Optional[int] = None, + flags: Optional[int] = 0, + lev: Optional[int] = None, + delta: Optional[Union[Decimal, float, str]] = None, + price_aux_limit: Optional[Union[Decimal, float, str]] = None, + price_trailing: Optional[Union[Decimal, float, str]] = None, + tif: Optional[Union[datetime, str]] = None): + await self.__handle_websocket_input("ou", { "id": id, "amount": amount, "price": price, "cid": cid, "cid_date": cid_date, "gid": gid, "flags": flags, "lev": lev, "delta": delta, "price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif }) - async def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None): - await self.handle_websocket_input("oc", { + async def cancel_order(self, + id: Optional[int] = None, + cid: Optional[int] = None, + cid_date: Optional[str] = None): + await self.__handle_websocket_input("oc", { "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): - await self.handle_websocket_input("oc_multi", { + 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", { "ids": ids, "cids": cids, "gids": gids, "all": int(all) }) - async def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, float, str], - rate: Union[Decimal, float, str], period: int, - flags: Optional[int] = 0): - await self.handle_websocket_input("fon", { + async def submit_funding_offer(self, + type: FundingOfferType, + symbol: str, + amount: Union[Decimal, float, str], + rate: Union[Decimal, float, str], + period: int, + flags: Optional[int] = 0): + await self.__handle_websocket_input("fon", { "type": type, "symbol": symbol, "amount": amount, - "rate": rate, "period": period, - "flags": flags + "rate": rate, "period": period, "flags": flags }) async def cancel_funding_offer(self, id: int): - await self.handle_websocket_input("foc", { "id": id }) + await self.__handle_websocket_input("foc", { "id": id }) async def calc(self, *args: str): - await self.handle_websocket_input("calc", list(map(lambda arg: [arg], args))) \ No newline at end of file + await self.__handle_websocket_input("calc", list(map(lambda arg: [arg], args))) diff --git a/bfxapi/websocket/handlers/public_channels_handler.py b/bfxapi/websocket/handlers/public_channels_handler.py index eb96482..8439d52 100644 --- a/bfxapi/websocket/handlers/public_channels_handler.py +++ b/bfxapi/websocket/handlers/public_channels_handler.py @@ -1,5 +1,3 @@ -#pylint: disable=inconsistent-return-statements - from .. import serializers from .. exceptions import HandlerNotFound From af25f25d3b1a731243453ea7e8a0cf7fc1646550 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 6 Mar 2023 18:49:22 +0100 Subject: [PATCH 197/265] Apply pylint's linting rules to bfxapi/tests/*.py. --- .pylintrc | 2 +- bfxapi/tests/__init__.py | 4 +- bfxapi/tests/test_labeler.py | 12 ++-- bfxapi/tests/test_notification.py | 18 +++--- bfxapi/tests/test_rest_serializers.py | 10 ++-- bfxapi/tests/test_websocket_serializers.py | 10 ++-- .../websocket/client/bfx_websocket_bucket.py | 3 +- .../websocket/client/bfx_websocket_client.py | 2 +- .../websocket/client/bfx_websocket_inputs.py | 2 +- bfxapi/websocket/types.py | 58 ++++++++++--------- 10 files changed, 66 insertions(+), 55 deletions(-) diff --git a/.pylintrc b/.pylintrc index 58097c3..6a9303c 100644 --- a/.pylintrc +++ b/.pylintrc @@ -19,7 +19,7 @@ disable= max-line-length=130 -good-names=id,on +good-names=id,on,pl,t,A,B,C,D,E,F [TYPECHECK] diff --git a/bfxapi/tests/__init__.py b/bfxapi/tests/__init__.py index 057c2c0..3084e94 100644 --- a/bfxapi/tests/__init__.py +++ b/bfxapi/tests/__init__.py @@ -13,6 +13,6 @@ def suite(): unittest.makeSuite(TestLabeler), unittest.makeSuite(TestNotification), ]) - + if __name__ == "__main__": - unittest.TextTestRunner().run(suite()) \ No newline at end of file + unittest.TextTestRunner().run(suite()) diff --git a/bfxapi/tests/test_labeler.py b/bfxapi/tests/test_labeler.py index 9ee8ea0..a586380 100644 --- a/bfxapi/tests/test_labeler.py +++ b/bfxapi/tests/test_labeler.py @@ -25,24 +25,24 @@ class TestLabeler(unittest.TestCase): self.assertListEqual(serializer.get_labels(), [ "A", "B", "C" ], msg="_Serializer::get_labels() should return the right list of labels.") - with self.assertRaises(LabelerSerializerException, + with self.assertRaises(LabelerSerializerException, msg="_Serializer should raise LabelerSerializerException if given fewer arguments than the serializer labels."): - serializer.parse(5, 65.0, "X") + serializer.parse(5, 65.0, "X") def test_generate_recursive_serializer(self): @dataclass - class Outer(_Type): + class Outer(_Type): A: int B: float C: "Middle" @dataclass - class Middle(_Type): + class Middle(_Type): D: str E: "Inner" @dataclass - class Inner(_Type): + class Inner(_Type): F: bool inner = generate_labeler_serializer("Inner", Inner, ["F"]) @@ -53,4 +53,4 @@ class TestLabeler(unittest.TestCase): msg="_RecursiveSerializer should produce the right result.") if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/bfxapi/tests/test_notification.py b/bfxapi/tests/test_notification.py index f71df60..0d3cb59 100644 --- a/bfxapi/tests/test_notification.py +++ b/bfxapi/tests/test_notification.py @@ -11,15 +11,19 @@ class TestNotification(unittest.TestCase): A: int B: float C: str - - test = generate_labeler_serializer("Test", Test, + + test = generate_labeler_serializer("Test", Test, [ "A", "_PLACEHOLDER", "B", "_PLACEHOLDER", "C" ]) notification = _Notification[Test](test) - self.assertEqual(notification.parse(*[1675787861506, "test", None, None, [ 5, None, 65.0, None, "X" ], 0, "SUCCESS", "This is just a test notification."]), - Notification[Test](1675787861506, "test", None, Test(5, 65.0, "X"), 0, "SUCCESS", "This is just a test notification."), - msg="_Notification should produce the right notification.") - + actual = notification.parse(*[ 1675787861506, "test", None, None, [ 5, None, 65.0, None, "X" ], \ + 0, "SUCCESS", "This is just a test notification." ]) + + expected = Notification[Test](1675787861506, "test", None, Test(5, 65.0, "X"), + 0, "SUCCESS", "This is just a test notification.") + + self.assertEqual(actual, expected, msg="_Notification should produce the right notification.") + if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/bfxapi/tests/test_rest_serializers.py b/bfxapi/tests/test_rest_serializers.py index 4c24992..17c3d98 100644 --- a/bfxapi/tests/test_rest_serializers.py +++ b/bfxapi/tests/test_rest_serializers.py @@ -1,3 +1,5 @@ +#pylint: disable=duplicate-code + import unittest from ..labeler import _Type @@ -7,11 +9,11 @@ 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), + 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__), + + 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() \ No newline at end of file + unittest.main() diff --git a/bfxapi/tests/test_websocket_serializers.py b/bfxapi/tests/test_websocket_serializers.py index a559565..111cde4 100644 --- a/bfxapi/tests/test_websocket_serializers.py +++ b/bfxapi/tests/test_websocket_serializers.py @@ -1,3 +1,5 @@ +#pylint: disable=duplicate-code + import unittest from ..labeler import _Type @@ -7,11 +9,11 @@ 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), + 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__), + + 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() \ No newline at end of file + unittest.main() diff --git a/bfxapi/websocket/client/bfx_websocket_bucket.py b/bfxapi/websocket/client/bfx_websocket_bucket.py index c6f797d..2579c11 100644 --- a/bfxapi/websocket/client/bfx_websocket_bucket.py +++ b/bfxapi/websocket/client/bfx_websocket_bucket.py @@ -31,8 +31,7 @@ class BfxWebsocketBucket: self.handler = PublicChannelsHandler(event_emitter=self.event_emitter) - #pylint: disable-next=unused-argument - async def connect(self, index): + async def connect(self): reconnection = False async for websocket in websockets.connect(self.host): diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 71ef5a1..7e5f2a6 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -75,7 +75,7 @@ class BfxWebsocketClient: for index in range(connections): self.buckets += [BfxWebsocketBucket(self.host, self.event_emitter, self.on_open_events[index])] - tasks = [ bucket.connect(index) for index, bucket in enumerate(self.buckets) ] + tasks = [ bucket.connect() for bucket in self.buckets ] tasks.append(self.__connect()) diff --git a/bfxapi/websocket/client/bfx_websocket_inputs.py b/bfxapi/websocket/client/bfx_websocket_inputs.py index 5ff0769..25ad8b4 100644 --- a/bfxapi/websocket/client/bfx_websocket_inputs.py +++ b/bfxapi/websocket/client/bfx_websocket_inputs.py @@ -1,4 +1,4 @@ -#pylint: disable=invalid-name,redefined-builtin,too-many-arguments +#pylint: disable=redefined-builtin,too-many-arguments from decimal import Decimal from datetime import datetime diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py index 8951141..2909030 100644 --- a/bfxapi/websocket/types.py +++ b/bfxapi/websocket/types.py @@ -1,9 +1,13 @@ +#pylint: disable-next=wildcard-import,unused-wildcard-import from typing import * from dataclasses import dataclass -from .. labeler import _Type +#pylint: disable-next=unused-import from .. notification import Notification + +from .. labeler import _Type + from ..utils.json_encoder import JSON #region Type hinting for Websocket Public Channels @@ -40,43 +44,43 @@ class FundingCurrencyTicker(_Type): @dataclass class TradingPairTrade(_Type): - id: int - mts: int - amount: float + id: int + mts: int + amount: float price: float @dataclass class FundingCurrencyTrade(_Type): - id: int - mts: int - amount: float - rate: float + id: int + mts: int + amount: float + rate: float period: int @dataclass class TradingPairBook(_Type): - price: float - count: int + price: float + count: int amount: float @dataclass class FundingCurrencyBook(_Type): - rate: float - period: int - count: int + rate: float + period: int + count: int amount: float -@dataclass +@dataclass class TradingPairRawBook(_Type): order_id: int - price: float + price: float amount: float -@dataclass +@dataclass class FundingCurrencyRawBook(_Type): - offer_id: int - period: int - rate: float + offer_id: int + period: int + rate: float amount: float @dataclass @@ -154,14 +158,14 @@ class Position(_Type): @dataclass class Trade(_Type): - id: int - symbol: str + id: int + symbol: str mts_create: int - order_id: int - exec_amount: float - exec_price: float - order_type: str - order_price: float + order_id: int + exec_amount: float + exec_price: float + order_type: str + order_price: float maker:int fee: Optional[float] fee_currency: Optional[str] @@ -238,4 +242,4 @@ class Balance(_Type): aum: float aum_net: float -#endregion \ No newline at end of file +#endregion From 08af0c8c90631e53ead19a7fa268d64fd7c198fa Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Mon, 6 Mar 2023 19:09:32 +0100 Subject: [PATCH 198/265] Add .github folder. Add ISSUE_TEMPLATE.md and PULL_REQUEST_TEMPLATE.md. --- .github/ISSUE_TEMPLATE.md | 35 ++++++++++++++++++++++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 30 +++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..d1f5f1d --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,35 @@ +## I'm submitting a... + +- [ ] bug report; +- [ ] feature request; +- [ ] documentation change; + +## What is the expected behaviour? + + +## What is the current behaviour? + + +## Possible solution (optional) + + + + +A possible solution could be... + +## Steps to reproduce (for bugs) + + +1.   +2.   +3.   + +### Python version + + +Python 3.10.6 x64 + +### Mypy version + + +mypy 0.991 (compiled: yes) \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..da17065 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,30 @@ +# Description + + +## Motivation and Context + + +## Related Issue + + + +PR fixes the following issue: + +## Type of change + +- [ ] Bug fix (non-breaking change which fixes an issue); +- [ ] New feature (non-breaking change which adds functionality); +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected); +- [ ] This change requires a documentation update; + +# Checklist: + +- [ ] My code follows the style guidelines of this project; +- [ ] I have performed a self-review of my code; +- [ ] I have commented my code, particularly in hard-to-understand areas; +- [ ] I have made corresponding changes to the documentation; +- [ ] My changes generate no new warnings; +- [ ] I have added tests that prove my fix is effective or that my feature works; +- [ ] New and existing unit tests pass locally with my changes; +- [ ] Mypy does not return any errors or warnings if runned on the root package; +- [ ] I have updated the library version and updated the CHANGELOG; \ No newline at end of file From 9e566bbc5a28d4006a974af62b488ec971a03e10 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 7 Mar 2023 15:28:02 +0100 Subject: [PATCH 199/265] Switch from max-line-length=130 to more standard max-line-length=120 in .pylintrc. --- .pylintrc | 2 +- bfxapi/tests/test_labeler.py | 3 +- bfxapi/tests/test_rest_serializers.py | 3 +- bfxapi/tests/test_websocket_serializers.py | 3 +- .../websocket/client/bfx_websocket_bucket.py | 28 +- .../websocket/client/bfx_websocket_client.py | 44 +- bfxapi/websocket/serializers.py | 574 ++++++++++-------- 7 files changed, 372 insertions(+), 285 deletions(-) diff --git a/.pylintrc b/.pylintrc index 6a9303c..7798ffd 100644 --- a/.pylintrc +++ b/.pylintrc @@ -17,7 +17,7 @@ disable= [FORMAT] -max-line-length=130 +max-line-length=120 good-names=id,on,pl,t,A,B,C,D,E,F diff --git a/bfxapi/tests/test_labeler.py b/bfxapi/tests/test_labeler.py index a586380..5750d9c 100644 --- a/bfxapi/tests/test_labeler.py +++ b/bfxapi/tests/test_labeler.py @@ -26,7 +26,8 @@ class TestLabeler(unittest.TestCase): msg="_Serializer::get_labels() should return the right list of labels.") with self.assertRaises(LabelerSerializerException, - msg="_Serializer should raise LabelerSerializerException if given fewer arguments than the serializer labels."): + msg="_Serializer should raise LabelerSerializerException if given " + + "fewer arguments than the serializer labels."): serializer.parse(5, 65.0, "X") def test_generate_recursive_serializer(self): diff --git a/bfxapi/tests/test_rest_serializers.py b/bfxapi/tests/test_rest_serializers.py index 17c3d98..0cf43b5 100644 --- a/bfxapi/tests/test_rest_serializers.py +++ b/bfxapi/tests/test_rest_serializers.py @@ -13,7 +13,8 @@ class TestRestSerializers(unittest.TestCase): 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.") + f"_Serializer <{serializer.name}> and _Type <{serializer.klass.__name__}> " + + "must have matching labels and fields.") if __name__ == "__main__": unittest.main() diff --git a/bfxapi/tests/test_websocket_serializers.py b/bfxapi/tests/test_websocket_serializers.py index 111cde4..56708f4 100644 --- a/bfxapi/tests/test_websocket_serializers.py +++ b/bfxapi/tests/test_websocket_serializers.py @@ -13,7 +13,8 @@ class TestWebsocketSerializers(unittest.TestCase): 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.") + f"_Serializer <{serializer.name}> and _Type <{serializer.klass.__name__}> " + + "must have matching labels and fields.") if __name__ == "__main__": unittest.main() diff --git a/bfxapi/websocket/client/bfx_websocket_bucket.py b/bfxapi/websocket/client/bfx_websocket_bucket.py index 2579c11..72c3419 100644 --- a/bfxapi/websocket/client/bfx_websocket_bucket.py +++ b/bfxapi/websocket/client/bfx_websocket_bucket.py @@ -52,17 +52,23 @@ class BfxWebsocketBucket: async for message in websocket: message = json.loads(message) - if isinstance(message, dict) and message["event"] == "subscribed" and (chan_id := message["chanId"]): - self.pendings = [ pending for pending in self.pendings if pending["subId"] != message["subId"] ] - self.subscriptions[chan_id] = message - self.event_emitter.emit("subscribed", message) - elif isinstance(message, dict) and message["event"] == "unsubscribed" and (chan_id := message["chanId"]): - if message["status"] == "OK": - del self.subscriptions[chan_id] - elif isinstance(message, dict) and message["event"] == "error": - self.event_emitter.emit("wss-error", message["code"], message["msg"]) - elif isinstance(message, list) and (chan_id := message[0]) and message[1] != _HEARTBEAT: - self.handler.handle(self.subscriptions[chan_id], *message[1:]) + if isinstance(message, dict): + if message["event"] == "subscribed" and (chan_id := message["chanId"]): + self.pendings = \ + [ pending for pending in self.pendings if pending["subId"] != message["subId"] ] + + self.subscriptions[chan_id] = message + + self.event_emitter.emit("subscribed", message) + elif message["event"] == "unsubscribed" and (chan_id := message["chanId"]): + if message["status"] == "OK": + del self.subscriptions[chan_id] + elif message["event"] == "error": + self.event_emitter.emit("wss-error", message["code"], message["msg"]) + + if isinstance(message, list): + if (chan_id := message[0]) and message[1] != _HEARTBEAT: + self.handler.handle(self.subscriptions[chan_id], *message[1:]) except websockets.ConnectionClosedError as error: if error.code == 1006: self.on_open_event.clear() diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 7e5f2a6..360f840 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -66,8 +66,8 @@ class BfxWebsocketClient: 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} " - + f"buckets from the same connection ({connections} in use), the server could momentarily block the " - + "client with <429 Too Many Requests>.") + + f"buckets from the same connection ({connections} in use), the server could momentarily " + + "block the client with <429 Too Many Requests>.") for _ in range(connections): self.on_open_events.append(asyncio.Event()) @@ -109,25 +109,31 @@ class BfxWebsocketClient: 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("Mismatch between the client version and the server version. " - + "Update the library to the latest version to continue (client version: " - + f"{BfxWebsocketClient.VERSION}, 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).") + if isinstance(message, dict): + if message["event"] == "info" and "version" in message: + if BfxWebsocketClient.VERSION != message["version"]: + raise OutdatedClientVersion("Mismatch between the client version and the server " + + "version. Update the library to the latest version to continue (client version: " + + f"{BfxWebsocketClient.VERSION}, server version: {message['version']}).") + elif 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 message["event"] == "auth": + if message["status"] != "OK": + raise InvalidAuthenticationCredentials( + "Cannot authenticate with given API-KEY and API-SECRET.") - 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 message[0] == 0 and message[1] != _HEARTBEAT: - self.handler.handle(message[1], message[2]) + elif message["event"] == "error": + self.event_emitter.emit("wss-error", message["code"], message["msg"]) + + if isinstance(message, list): + if message[0] == 0 and message[1] != _HEARTBEAT: + self.handler.handle(message[1], message[2]) class _Delay: BACKOFF_MIN, BACKOFF_MAX = 1.92, 60.0 @@ -171,8 +177,8 @@ class BfxWebsocketClient: delay = _Delay(backoff_factor=1.618) elif isinstance(error, socket.gaierror) and reconnection.status: 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})") + + f"Next reconnection attempt in ~{round(delay.peek()):.1f} seconds. (at the moment " + + f"the client has been offline for {datetime.now() - reconnection.timestamp})") reconnection = reconnection._replace(attempts=reconnection.attempts + 1) else: raise error diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index ae2f04e..2bcea77 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -18,277 +18,349 @@ __serializers__ = [ #region Serializers definition for Websocket Public Channels -TradingPairTicker = generate_labeler_serializer("TradingPairTicker", klass=types.TradingPairTicker, labels=[ - "bid", - "bid_size", - "ask", - "ask_size", - "daily_change", - "daily_change_relative", - "last_price", - "volume", - "high", - "low" -]) +TradingPairTicker = generate_labeler_serializer( + name="TradingPairTicker", + klass=types.TradingPairTicker, + labels=[ + "bid", + "bid_size", + "ask", + "ask_size", + "daily_change", + "daily_change_relative", + "last_price", + "volume", + "high", + "low" + ] +) -FundingCurrencyTicker = generate_labeler_serializer("FundingCurrencyTicker", klass=types.FundingCurrencyTicker, labels=[ - "frr", - "bid", - "bid_period", - "bid_size", - "ask", - "ask_period", - "ask_size", - "daily_change", - "daily_change_relative", - "last_price", - "volume", - "high", - "low", - "_PLACEHOLDER", - "_PLACEHOLDER", - "frr_amount_available" -]) +FundingCurrencyTicker = generate_labeler_serializer( + name="FundingCurrencyTicker", + klass=types.FundingCurrencyTicker, + labels=[ + "frr", + "bid", + "bid_period", + "bid_size", + "ask", + "ask_period", + "ask_size", + "daily_change", + "daily_change_relative", + "last_price", + "volume", + "high", + "low", + "_PLACEHOLDER", + "_PLACEHOLDER", + "frr_amount_available" + ] +) -TradingPairTrade = generate_labeler_serializer("TradingPairTrade", klass=types.TradingPairTrade, labels=[ - "id", - "mts", - "amount", - "price" -]) +TradingPairTrade = generate_labeler_serializer( + name="TradingPairTrade", + klass=types.TradingPairTrade, + labels=[ + "id", + "mts", + "amount", + "price" + ] +) -FundingCurrencyTrade = generate_labeler_serializer("FundingCurrencyTrade", klass=types.FundingCurrencyTrade, labels=[ - "id", - "mts", - "amount", - "rate", - "period" -]) +FundingCurrencyTrade = generate_labeler_serializer( + name="FundingCurrencyTrade", + klass=types.FundingCurrencyTrade, + labels=[ + "id", + "mts", + "amount", + "rate", + "period" + ] +) -TradingPairBook = generate_labeler_serializer("TradingPairBook", klass=types.TradingPairBook, labels=[ - "price", - "count", - "amount" -]) +TradingPairBook = generate_labeler_serializer( + name="TradingPairBook", + klass=types.TradingPairBook, + labels=[ + "price", + "count", + "amount" + ] +) -FundingCurrencyBook = generate_labeler_serializer("FundingCurrencyBook", klass=types.FundingCurrencyBook, labels=[ - "rate", - "period", - "count", - "amount" -]) +FundingCurrencyBook = generate_labeler_serializer( + name="FundingCurrencyBook", + klass=types.FundingCurrencyBook, + labels=[ + "rate", + "period", + "count", + "amount" + ] +) -TradingPairRawBook = generate_labeler_serializer("TradingPairRawBook", klass=types.TradingPairRawBook, labels=[ - "order_id", - "price", - "amount" -]) +TradingPairRawBook = generate_labeler_serializer( + name="TradingPairRawBook", + klass=types.TradingPairRawBook, + labels=[ + "order_id", + "price", + "amount" + ] +) -FundingCurrencyRawBook = generate_labeler_serializer("FundingCurrencyRawBook", klass=types.FundingCurrencyRawBook, labels=[ - "offer_id", - "period", - "rate", - "amount" -]) +FundingCurrencyRawBook = generate_labeler_serializer( + name="FundingCurrencyRawBook", + klass=types.FundingCurrencyRawBook, + labels=[ + "offer_id", + "period", + "rate", + "amount" + ] +) -Candle = generate_labeler_serializer("Candle", klass=types.Candle, labels=[ - "mts", - "open", - "close", - "high", - "low", - "volume" -]) +Candle = generate_labeler_serializer( + name="Candle", + klass=types.Candle, + labels=[ + "mts", + "open", + "close", + "high", + "low", + "volume" + ] +) -DerivativesStatus = generate_labeler_serializer("DerivativesStatus", klass=types.DerivativesStatus, labels=[ - "mts", - "_PLACEHOLDER", - "deriv_price", - "spot_price", - "_PLACEHOLDER", - "insurance_fund_balance", - "_PLACEHOLDER", - "next_funding_evt_timestamp_ms", - "next_funding_accrued", - "next_funding_step", - "_PLACEHOLDER", - "current_funding", - "_PLACEHOLDER", - "_PLACEHOLDER", - "mark_price", - "_PLACEHOLDER", - "_PLACEHOLDER", - "open_interest", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "clamp_min", - "clamp_max" -]) +DerivativesStatus = generate_labeler_serializer( + name="DerivativesStatus", + klass=types.DerivativesStatus, + labels=[ + "mts", + "_PLACEHOLDER", + "deriv_price", + "spot_price", + "_PLACEHOLDER", + "insurance_fund_balance", + "_PLACEHOLDER", + "next_funding_evt_timestamp_ms", + "next_funding_accrued", + "next_funding_step", + "_PLACEHOLDER", + "current_funding", + "_PLACEHOLDER", + "_PLACEHOLDER", + "mark_price", + "_PLACEHOLDER", + "_PLACEHOLDER", + "open_interest", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "clamp_min", + "clamp_max" + ] +) #endregion #region Serializers definition for Websocket Authenticated Channels -Order = generate_labeler_serializer("Order", klass=types.Order, labels=[ - "id", - "gid", - "cid", - "symbol", - "mts_create", - "mts_update", - "amount", - "amount_orig", - "order_type", - "type_prev", - "mts_tif", - "_PLACEHOLDER", - "flags", - "order_status", - "_PLACEHOLDER", - "_PLACEHOLDER", - "price", - "price_avg", - "price_trailing", - "price_aux_limit", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "notify", - "hidden", - "placed_id", - "_PLACEHOLDER", - "_PLACEHOLDER", - "routing", - "_PLACEHOLDER", - "_PLACEHOLDER", - "meta" -]) +Order = generate_labeler_serializer( + name="Order", + klass=types.Order, + labels=[ + "id", + "gid", + "cid", + "symbol", + "mts_create", + "mts_update", + "amount", + "amount_orig", + "order_type", + "type_prev", + "mts_tif", + "_PLACEHOLDER", + "flags", + "order_status", + "_PLACEHOLDER", + "_PLACEHOLDER", + "price", + "price_avg", + "price_trailing", + "price_aux_limit", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "notify", + "hidden", + "placed_id", + "_PLACEHOLDER", + "_PLACEHOLDER", + "routing", + "_PLACEHOLDER", + "_PLACEHOLDER", + "meta" + ] +) -Position = generate_labeler_serializer("Position", klass=types.Position, labels=[ - "symbol", - "status", - "amount", - "base_price", - "margin_funding", - "margin_funding_type", - "pl", - "pl_perc", - "price_liq", - "leverage", - "flag", - "position_id", - "mts_create", - "mts_update", - "_PLACEHOLDER", - "type", - "_PLACEHOLDER", - "collateral", - "collateral_min", - "meta" -]) +Position = generate_labeler_serializer( + name="Position", + klass=types.Position, + labels=[ + "symbol", + "status", + "amount", + "base_price", + "margin_funding", + "margin_funding_type", + "pl", + "pl_perc", + "price_liq", + "leverage", + "flag", + "position_id", + "mts_create", + "mts_update", + "_PLACEHOLDER", + "type", + "_PLACEHOLDER", + "collateral", + "collateral_min", + "meta" + ] +) -Trade = generate_labeler_serializer("Trade", klass=types.Trade, labels=[ - "id", - "symbol", - "mts_create", - "order_id", - "exec_amount", - "exec_price", - "order_type", - "order_price", - "maker", - "fee", - "fee_currency", - "cid" -]) +Trade = generate_labeler_serializer( + name="Trade", + klass=types.Trade, + labels=[ + "id", + "symbol", + "mts_create", + "order_id", + "exec_amount", + "exec_price", + "order_type", + "order_price", + "maker", + "fee", + "fee_currency", + "cid" + ] +) -FundingOffer = generate_labeler_serializer("FundingOffer", klass=types.FundingOffer, labels=[ - "id", - "symbol", - "mts_create", - "mts_update", - "amount", - "amount_orig", - "offer_type", - "_PLACEHOLDER", - "_PLACEHOLDER", - "flags", - "offer_status", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "rate", - "period", - "notify", - "hidden", - "_PLACEHOLDER", - "renew", - "_PLACEHOLDER" -]) +FundingOffer = generate_labeler_serializer( + name="FundingOffer", + klass=types.FundingOffer, + labels=[ + "id", + "symbol", + "mts_create", + "mts_update", + "amount", + "amount_orig", + "offer_type", + "_PLACEHOLDER", + "_PLACEHOLDER", + "flags", + "offer_status", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "rate", + "period", + "notify", + "hidden", + "_PLACEHOLDER", + "renew", + "_PLACEHOLDER" + ] +) -FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.FundingCredit, labels=[ - "id", - "symbol", - "side", - "mts_create", - "mts_update", - "amount", - "flags", - "status", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "rate", - "period", - "mts_opening", - "mts_last_payout", - "notify", - "hidden", - "_PLACEHOLDER", - "renew", - "_PLACEHOLDER", - "no_close", - "position_pair" -]) +FundingCredit = generate_labeler_serializer( + name="FundingCredit", + klass=types.FundingCredit, + labels=[ + "id", + "symbol", + "side", + "mts_create", + "mts_update", + "amount", + "flags", + "status", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "rate", + "period", + "mts_opening", + "mts_last_payout", + "notify", + "hidden", + "_PLACEHOLDER", + "renew", + "_PLACEHOLDER", + "no_close", + "position_pair" + ] +) -FundingLoan = generate_labeler_serializer("FundingLoan", klass=types.FundingLoan, labels=[ - "id", - "symbol", - "side", - "mts_create", - "mts_update", - "amount", - "flags", - "status", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "rate", - "period", - "mts_opening", - "mts_last_payout", - "notify", - "hidden", - "_PLACEHOLDER", - "renew", - "_PLACEHOLDER", - "no_close" -]) +FundingLoan = generate_labeler_serializer( + name="FundingLoan", + klass=types.FundingLoan, + labels=[ + "id", + "symbol", + "side", + "mts_create", + "mts_update", + "amount", + "flags", + "status", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "rate", + "period", + "mts_opening", + "mts_last_payout", + "notify", + "hidden", + "_PLACEHOLDER", + "renew", + "_PLACEHOLDER", + "no_close" + ] +) -Wallet = generate_labeler_serializer("Wallet", klass=types.Wallet, labels=[ - "wallet_type", - "currency", - "balance", - "unsettled_interest", - "available_balance", - "last_change", - "trade_details" -]) +Wallet = generate_labeler_serializer( + name="Wallet", + klass=types.Wallet, + labels=[ + "wallet_type", + "currency", + "balance", + "unsettled_interest", + "available_balance", + "last_change", + "trade_details" + ] +) -Balance = generate_labeler_serializer("Balance", klass=types.Balance, labels=[ - "aum", - "aum_net", -]) +Balance = generate_labeler_serializer( + name="Balance", + klass=types.Balance, + labels=[ + "aum", + "aum_net" + ] +) #endregion From 9e1b336a67656b9cb1284d93d9951384ce6334e8 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 7 Mar 2023 15:42:27 +0100 Subject: [PATCH 200/265] Move _Delay local class in global scope (bfxapi.websocket.client.bfx_websocket_client). Apply pylint's linting rules to bfxapi/rest/__init__.py, bfxapi/rest/enums.py, bfxapi/rest/exceptions.py, bfxapi/rest/serializers.py and bfxapi/rest/types.py." Apply pylint's linting rules to bfxapi/rest/endpoints/__init__.py, bfxapi/rest/endpoints/bfx_rest_interface.py and bfxapi/rest/endpoints/rest_public_endpoints.py. --- .pylintrc | 3 +- bfxapi/rest/__init__.py | 2 +- bfxapi/rest/endpoints/__init__.py | 2 +- bfxapi/rest/endpoints/bfx_rest_interface.py | 13 +- .../endpoints/rest_authenticated_endpoints.py | 88 +- .../rest/endpoints/rest_merchant_endpoints.py | 30 +- .../rest/endpoints/rest_public_endpoints.py | 263 ++- bfxapi/rest/enums.py | 3 +- bfxapi/rest/exceptions.py | 10 - bfxapi/rest/middleware/__init__.py | 2 +- bfxapi/rest/middleware/middleware.py | 67 +- bfxapi/rest/serializers.py | 1536 ++++++++++------- bfxapi/rest/types.py | 84 +- .../websocket/client/bfx_websocket_client.py | 42 +- .../websocket/client/bfx_websocket_inputs.py | 8 +- bfxapi/websocket/types.py | 4 +- 16 files changed, 1236 insertions(+), 921 deletions(-) diff --git a/.pylintrc b/.pylintrc index 7798ffd..7768472 100644 --- a/.pylintrc +++ b/.pylintrc @@ -10,6 +10,7 @@ disable= multiple-imports, missing-docstring, too-few-public-methods, + too-many-public-methods, too-many-instance-attributes, dangerous-default-value, inconsistent-return-statements, @@ -19,7 +20,7 @@ disable= max-line-length=120 -good-names=id,on,pl,t,A,B,C,D,E,F +good-names=id,on,pl,t,ip,tf,A,B,C,D,E,F [TYPECHECK] diff --git a/bfxapi/rest/__init__.py b/bfxapi/rest/__init__.py index 71e3b54..4f17f62 100644 --- a/bfxapi/rest/__init__.py +++ b/bfxapi/rest/__init__.py @@ -1,4 +1,4 @@ from .endpoints import BfxRestInterface, RestPublicEndpoints, RestAuthenticatedEndpoints, \ RestMerchantEndpoints -NAME = "rest" \ No newline at end of file +NAME = "rest" diff --git a/bfxapi/rest/endpoints/__init__.py b/bfxapi/rest/endpoints/__init__.py index e35d6fb..ef82b97 100644 --- a/bfxapi/rest/endpoints/__init__.py +++ b/bfxapi/rest/endpoints/__init__.py @@ -4,4 +4,4 @@ from .rest_public_endpoints import RestPublicEndpoints from .rest_authenticated_endpoints import RestAuthenticatedEndpoints from .rest_merchant_endpoints import RestMerchantEndpoints -NAME = "endpoints" \ No newline at end of file +NAME = "endpoints" diff --git a/bfxapi/rest/endpoints/bfx_rest_interface.py b/bfxapi/rest/endpoints/bfx_rest_interface.py index fdc05a3..12a06f4 100644 --- a/bfxapi/rest/endpoints/bfx_rest_interface.py +++ b/bfxapi/rest/endpoints/bfx_rest_interface.py @@ -1,16 +1,13 @@ -from typing import Optional - from .rest_public_endpoints import RestPublicEndpoints from .rest_authenticated_endpoints import RestAuthenticatedEndpoints from .rest_merchant_endpoints import RestMerchantEndpoints -class BfxRestInterface(object): +class BfxRestInterface: VERSION = 2 def __init__(self, host, credentials = None): - API_KEY, API_SECRET = credentials and \ - (credentials["api_key"], credentials["api_secret"]) or (None, None) + api_key, api_secret = (credentials['api_key'], credentials['api_secret']) if credentials else (None, None) - self.public = RestPublicEndpoints(host=host) - self.auth = RestAuthenticatedEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET) - self.merchant = RestMerchantEndpoints(host=host, API_KEY=API_KEY, API_SECRET=API_SECRET) \ No newline at end of file + self.public = RestPublicEndpoints(host=host) + self.auth = RestAuthenticatedEndpoints(host=host, api_key=api_key, api_secret=api_secret) + self.merchant = RestMerchantEndpoints(host=host, api_key=api_key, api_secret=api_secret) diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index 69c3737..0ed2241 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -10,19 +10,19 @@ from .. middleware import Middleware class RestAuthenticatedEndpoints(Middleware): def get_user_info(self) -> UserInfo: - return serializers.UserInfo.parse(*self._POST(f"auth/r/info/user")) + return serializers.UserInfo.parse(*self._post(f"auth/r/info/user")) def get_login_history(self) -> List[LoginHistory]: - return [ serializers.LoginHistory.parse(*sub_data) for sub_data in self._POST("auth/r/logins/hist") ] + return [ serializers.LoginHistory.parse(*sub_data) for sub_data in self._post("auth/r/logins/hist") ] def get_balance_available_for_orders_or_offers(self, symbol: str, type: str, dir: Optional[int] = None, rate: Optional[str] = None, lev: Optional[str] = None) -> BalanceAvailable: - return serializers.BalanceAvailable.parse(*self._POST("auth/calc/order/avail", body={ + return serializers.BalanceAvailable.parse(*self._post("auth/calc/order/avail", body={ "symbol": symbol, "type": type, "dir": dir, "rate": rate, "lev": lev })) def get_wallets(self) -> List[Wallet]: - return [ serializers.Wallet.parse(*sub_data) for sub_data in self._POST("auth/r/wallets") ] + return [ serializers.Wallet.parse(*sub_data) for sub_data in self._post("auth/r/wallets") ] def get_orders(self, symbol: Optional[str] = None, ids: Optional[List[str]] = None) -> List[Order]: endpoint = "auth/r/orders" @@ -30,7 +30,7 @@ class RestAuthenticatedEndpoints(Middleware): if symbol != None: endpoint += f"/{symbol}" - return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, body={ "id": ids }) ] + return [ serializers.Order.parse(*sub_data) for sub_data in self._post(endpoint, body={ "id": ids }) ] def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, float, str], price: Optional[Union[Decimal, float, str]] = None, lev: Optional[int] = None, @@ -45,7 +45,7 @@ class RestAuthenticatedEndpoints(Middleware): "flags": flags, "tif": tif, "meta": meta } - return serializers._Notification[Order](serializers.Order).parse(*self._POST("auth/w/order/submit", body=body)) + return serializers._Notification[Order](serializers.Order).parse(*self._post("auth/w/order/submit", body=body)) def update_order(self, id: int, amount: Optional[Union[Decimal, float, str]] = None, price: Optional[Union[Decimal, float, str]] = None, cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, @@ -58,7 +58,7 @@ class RestAuthenticatedEndpoints(Middleware): "price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif } - return serializers._Notification[Order](serializers.Order).parse(*self._POST("auth/w/order/update", body=body)) + return serializers._Notification[Order](serializers.Order).parse(*self._post("auth/w/order/update", body=body)) def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None) -> Notification[Order]: body = { @@ -67,7 +67,7 @@ class RestAuthenticatedEndpoints(Middleware): "cid_date": cid_date } - return serializers._Notification[Order](serializers.Order).parse(*self._POST("auth/w/order/cancel", body=body)) + return serializers._Notification[Order](serializers.Order).parse(*self._post("auth/w/order/cancel", body=body)) def cancel_order_multi(self, ids: Optional[List[int]] = None, cids: Optional[List[Tuple[int, str]]] = None, gids: Optional[List[int]] = None, all: bool = False) -> Notification[List[Order]]: body = { @@ -78,7 +78,7 @@ class RestAuthenticatedEndpoints(Middleware): "all": int(all) } - return serializers._Notification[List[Order]](serializers.Order, is_iterable=True).parse(*self._POST("auth/w/order/cancel/multi", body=body)) + return serializers._Notification[List[Order]](serializers.Order, is_iterable=True).parse(*self._post("auth/w/order/cancel/multi", body=body)) def get_orders_history(self, symbol: Optional[str] = None, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Order]: if symbol == None: @@ -91,10 +91,10 @@ class RestAuthenticatedEndpoints(Middleware): "limit": limit } - return [ serializers.Order.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] + return [ serializers.Order.parse(*sub_data) for sub_data in self._post(endpoint, body=body) ] def get_order_trades(self, symbol: str, id: int) -> List[OrderTrade]: - return [ serializers.OrderTrade.parse(*sub_data) for sub_data in self._POST(f"auth/r/order/{symbol}:{id}/trades") ] + return [ serializers.OrderTrade.parse(*sub_data) for sub_data in self._post(f"auth/r/order/{symbol}:{id}/trades") ] def get_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Trade]: if symbol == None: @@ -107,7 +107,7 @@ class RestAuthenticatedEndpoints(Middleware): "limit": limit } - return [ serializers.Trade.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] + return [ serializers.Trade.parse(*sub_data) for sub_data in self._post(endpoint, body=body) ] def get_ledgers(self, currency: str, category: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Ledger]: body = { @@ -116,51 +116,51 @@ class RestAuthenticatedEndpoints(Middleware): "limit": limit } - return [ serializers.Ledger.parse(*sub_data) for sub_data in self._POST(f"auth/r/ledgers/{currency}/hist", body=body) ] + return [ serializers.Ledger.parse(*sub_data) for sub_data in self._post(f"auth/r/ledgers/{currency}/hist", body=body) ] def get_base_margin_info(self) -> BaseMarginInfo: - return serializers.BaseMarginInfo.parse(*(self._POST(f"auth/r/info/margin/base")[1])) + return serializers.BaseMarginInfo.parse(*(self._post(f"auth/r/info/margin/base")[1])) def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo: - response = self._POST(f"auth/r/info/margin/{symbol}") + response = self._post(f"auth/r/info/margin/{symbol}") data = [response[1]] + response[2] return serializers.SymbolMarginInfo.parse(*data) def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]: - return [ serializers.SymbolMarginInfo.parse(*([sub_data[1]] + sub_data[2])) for sub_data in self._POST(f"auth/r/info/margin/sym_all") ] + return [ serializers.SymbolMarginInfo.parse(*([sub_data[1]] + sub_data[2])) for sub_data in self._post(f"auth/r/info/margin/sym_all") ] def get_positions(self) -> List[Position]: - return [ serializers.Position.parse(*sub_data) for sub_data in self._POST("auth/r/positions") ] + return [ serializers.Position.parse(*sub_data) for sub_data in self._post("auth/r/positions") ] def claim_position(self, id: int, amount: Optional[Union[Decimal, float, str]] = None) -> Notification[PositionClaim]: return serializers._Notification[PositionClaim](serializers.PositionClaim).parse( - *self._POST("auth/w/position/claim", body={ "id": id, "amount": amount }) + *self._post("auth/w/position/claim", body={ "id": id, "amount": amount }) ) def increase_position(self, symbol: str, amount: Union[Decimal, float, str]) -> Notification[PositionIncrease]: return serializers._Notification[PositionIncrease](serializers.PositionIncrease).parse( - *self._POST("auth/w/position/increase", body={ "symbol": symbol, "amount": amount }) + *self._post("auth/w/position/increase", body={ "symbol": symbol, "amount": amount }) ) def get_increase_position_info(self, symbol: str, amount: Union[Decimal, float, str]) -> PositionIncreaseInfo: - response = self._POST(f"auth/r/position/increase/info", body={ "symbol": symbol, "amount": amount }) + response = self._post(f"auth/r/position/increase/info", body={ "symbol": symbol, "amount": amount }) data = response[0] + [response[1][0]] + response[1][1] + [response[1][2]] + response[4] + response[5] return serializers.PositionIncreaseInfo.parse(*data) def get_positions_history(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionHistory]: - return [ serializers.PositionHistory.parse(*sub_data) for sub_data in self._POST("auth/r/positions/hist", body={ "start": start, "end": end, "limit": limit }) ] + return [ serializers.PositionHistory.parse(*sub_data) for sub_data in self._post("auth/r/positions/hist", body={ "start": start, "end": end, "limit": limit }) ] def get_positions_snapshot(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionSnapshot]: - return [ serializers.PositionSnapshot.parse(*sub_data) for sub_data in self._POST("auth/r/positions/snap", body={ "start": start, "end": end, "limit": limit }) ] + return [ serializers.PositionSnapshot.parse(*sub_data) for sub_data in self._post("auth/r/positions/snap", body={ "start": start, "end": end, "limit": limit }) ] def get_positions_audit(self, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionAudit]: - return [ serializers.PositionAudit.parse(*sub_data) for sub_data in self._POST("auth/r/positions/audit", body={ "ids": ids, "start": start, "end": end, "limit": limit }) ] + return [ serializers.PositionAudit.parse(*sub_data) for sub_data in self._post("auth/r/positions/audit", body={ "ids": ids, "start": start, "end": end, "limit": limit }) ] def set_derivative_position_collateral(self, symbol: str, collateral: Union[Decimal, float, str]) -> DerivativePositionCollateral: - return serializers.DerivativePositionCollateral.parse(*(self._POST("auth/w/deriv/collateral/set", body={ "symbol": symbol, "collateral": collateral })[0])) + return serializers.DerivativePositionCollateral.parse(*(self._post("auth/w/deriv/collateral/set", body={ "symbol": symbol, "collateral": collateral })[0])) def get_derivative_position_collateral_limits(self, symbol: str) -> DerivativePositionCollateralLimits: - return serializers.DerivativePositionCollateralLimits.parse(*self._POST("auth/calc/deriv/collateral/limits", body={ "symbol": symbol })) + return serializers.DerivativePositionCollateralLimits.parse(*self._post("auth/calc/deriv/collateral/limits", body={ "symbol": symbol })) def get_funding_offers(self, symbol: Optional[str] = None) -> List[FundingOffer]: endpoint = "auth/r/funding/offers" @@ -168,7 +168,7 @@ class RestAuthenticatedEndpoints(Middleware): if symbol != None: endpoint += f"/{symbol}" - return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint) ] + return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._post(endpoint) ] def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, float, str], rate: Union[Decimal, float, str], period: int, @@ -179,30 +179,30 @@ class RestAuthenticatedEndpoints(Middleware): "flags": flags } - return serializers._Notification[FundingOffer](serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/submit", body=body)) + return serializers._Notification[FundingOffer](serializers.FundingOffer).parse(*self._post("auth/w/funding/offer/submit", body=body)) def cancel_funding_offer(self, id: int) -> Notification[FundingOffer]: - return serializers._Notification[FundingOffer](serializers.FundingOffer).parse(*self._POST("auth/w/funding/offer/cancel", body={ "id": id })) + return serializers._Notification[FundingOffer](serializers.FundingOffer).parse(*self._post("auth/w/funding/offer/cancel", body={ "id": id })) def cancel_all_funding_offers(self, currency: str) -> Notification[Literal[None]]: return serializers._Notification[Literal[None]](None).parse( - *self._POST("auth/w/funding/offer/cancel/all", body={ "currency": currency }) + *self._post("auth/w/funding/offer/cancel/all", body={ "currency": currency }) ) def submit_funding_close(self, id: int) -> Notification[Literal[None]]: return serializers._Notification[Literal[None]](None).parse( - *self._POST("auth/w/funding/close", body={ "id": id }) + *self._post("auth/w/funding/close", body={ "id": id }) ) def toggle_auto_renew(self, status: bool, currency: str, amount: Optional[str] = None, rate: Optional[int] = None, period: Optional[int] = None) -> Notification[FundingAutoRenew]: - return serializers._Notification[FundingAutoRenew](serializers.FundingAutoRenew).parse(*self._POST("auth/w/funding/auto", body={ + return serializers._Notification[FundingAutoRenew](serializers.FundingAutoRenew).parse(*self._post("auth/w/funding/auto", body={ "status": int(status), "currency": currency, "amount": amount, "rate": rate, "period": period })) def toggle_keep_funding(self, type: Literal["credit", "loan"], ids: Optional[List[int]] = None, changes: Optional[Dict[int, Literal[1, 2]]] = None) -> Notification[Literal[None]]: - return serializers._Notification[Literal[None]](None).parse(*self._POST("auth/w/funding/keep", body={ + return serializers._Notification[Literal[None]](None).parse(*self._post("auth/w/funding/keep", body={ "type": type, "id": ids, "changes": changes @@ -218,14 +218,14 @@ class RestAuthenticatedEndpoints(Middleware): "limit": limit } - return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] + return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._post(endpoint, body=body) ] def get_funding_loans(self, symbol: Optional[str] = None) -> List[FundingLoan]: if symbol == None: endpoint = "auth/r/funding/loans" else: endpoint = f"auth/r/funding/loans/{symbol}" - return [ serializers.FundingLoan.parse(*sub_data) for sub_data in self._POST(endpoint) ] + return [ serializers.FundingLoan.parse(*sub_data) for sub_data in self._post(endpoint) ] def get_funding_loans_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingLoan]: if symbol == None: @@ -237,14 +237,14 @@ class RestAuthenticatedEndpoints(Middleware): "limit": limit } - return [ serializers.FundingLoan.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] + return [ serializers.FundingLoan.parse(*sub_data) for sub_data in self._post(endpoint, body=body) ] def get_funding_credits(self, symbol: Optional[str] = None) -> List[FundingCredit]: if symbol == None: endpoint = "auth/r/funding/credits" else: endpoint = f"auth/r/funding/credits/{symbol}" - return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint) ] + return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._post(endpoint) ] def get_funding_credits_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingCredit]: if symbol == None: @@ -256,7 +256,7 @@ class RestAuthenticatedEndpoints(Middleware): "limit": limit } - return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] + return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._post(endpoint, body=body) ] def get_funding_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingTrade]: if symbol == None: @@ -269,10 +269,10 @@ class RestAuthenticatedEndpoints(Middleware): "limit": limit } - return [ serializers.FundingTrade.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] + return [ serializers.FundingTrade.parse(*sub_data) for sub_data in self._post(endpoint, body=body) ] def get_funding_info(self, key: str) -> FundingInfo: - response = self._POST(f"auth/r/info/funding/{key}") + response = self._post(f"auth/r/info/funding/{key}") data = [response[1]] + response[2] return serializers.FundingInfo.parse(*data) @@ -283,10 +283,10 @@ class RestAuthenticatedEndpoints(Middleware): "amount": amount } - return serializers._Notification[Transfer](serializers.Transfer).parse(*self._POST("auth/w/transfer", body=body)) + return serializers._Notification[Transfer](serializers.Transfer).parse(*self._post("auth/w/transfer", body=body)) def submit_wallet_withdrawal(self, wallet: str, method: str, address: str, amount: Union[Decimal, float, str]) -> Notification[Withdrawal]: - return serializers._Notification[Withdrawal](serializers.Withdrawal).parse(*self._POST("auth/w/withdraw", body={ + return serializers._Notification[Withdrawal](serializers.Withdrawal).parse(*self._post("auth/w/withdraw", body={ "wallet": wallet, "method": method, "address": address, "amount": amount, })) @@ -298,7 +298,7 @@ class RestAuthenticatedEndpoints(Middleware): "renew": int(renew) } - return serializers._Notification[DepositAddress](serializers.DepositAddress).parse(*self._POST("auth/w/deposit/address", body=body)) + return serializers._Notification[DepositAddress](serializers.DepositAddress).parse(*self._post("auth/w/deposit/address", body=body)) def generate_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, float, str]) -> LightningNetworkInvoice: body = { @@ -306,7 +306,7 @@ class RestAuthenticatedEndpoints(Middleware): "amount": amount } - return serializers.LightningNetworkInvoice.parse(*self._POST("auth/w/deposit/invoice", body=body)) + return serializers.LightningNetworkInvoice.parse(*self._post("auth/w/deposit/invoice", body=body)) def get_movements(self, currency: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Movement]: if currency == None: @@ -318,4 +318,4 @@ class RestAuthenticatedEndpoints(Middleware): "limit": limit } - return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] \ No newline at end of file + return [ serializers.Movement.parse(*sub_data) for sub_data in self._post(endpoint, body=body) ] \ No newline at end of file diff --git a/bfxapi/rest/endpoints/rest_merchant_endpoints.py b/bfxapi/rest/endpoints/rest_merchant_endpoints.py index 2e445ce..d237aab 100644 --- a/bfxapi/rest/endpoints/rest_merchant_endpoints.py +++ b/bfxapi/rest/endpoints/rest_merchant_endpoints.py @@ -24,12 +24,12 @@ class RestMerchantEndpoints(Middleware): "webhook": webhook, "redirect_url": redirect_url }) - data = to_snake_case_keys(self._POST("auth/w/ext/pay/invoice/create", body=body)) + data = to_snake_case_keys(self._post("auth/w/ext/pay/invoice/create", body=body)) return InvoiceSubmission.parse(data) def get_invoices(self, id: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[InvoiceSubmission]: - return [ InvoiceSubmission.parse(sub_data) for sub_data in to_snake_case_keys(self._POST("auth/r/ext/pay/invoices", body={ + return [ InvoiceSubmission.parse(sub_data) for sub_data in to_snake_case_keys(self._post("auth/r/ext/pay/invoices", body={ "id": id, "start": start, "end": end, "limit": limit })) ] @@ -43,24 +43,24 @@ class RestMerchantEndpoints(Middleware): "crypto": crypto, "id": id, "order_id": order_id }) - data = to_snake_case_keys(self._POST("auth/r/ext/pay/invoices/paginated", body=body)) + data = to_snake_case_keys(self._post("auth/r/ext/pay/invoices/paginated", body=body)) return InvoicePage.parse(data) def get_invoice_count_stats(self, status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"], format: str) -> List[InvoiceStats]: - return [ InvoiceStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/count", body={ "status": status, "format": format }) ] + return [ InvoiceStats(**sub_data) for sub_data in self._post("auth/r/ext/pay/invoice/stats/count", body={ "status": status, "format": format }) ] def get_invoice_earning_stats(self, currency: str, format: str) -> List[InvoiceStats]: - return [ InvoiceStats(**sub_data) for sub_data in self._POST("auth/r/ext/pay/invoice/stats/earning", body={ "currency": currency, "format": format }) ] + return [ InvoiceStats(**sub_data) for sub_data in self._post("auth/r/ext/pay/invoice/stats/earning", body={ "currency": currency, "format": format }) ] def complete_invoice(self, id: str, pay_currency: str, deposit_id: Optional[int] = None, ledger_id: Optional[int] = None) -> InvoiceSubmission: - return InvoiceSubmission.parse(to_snake_case_keys(self._POST("auth/w/ext/pay/invoice/complete", body={ + return InvoiceSubmission.parse(to_snake_case_keys(self._post("auth/w/ext/pay/invoice/complete", body={ "id": id, "payCcy": pay_currency, "depositId": deposit_id, "ledgerId": ledger_id }))) def expire_invoice(self, id: str) -> InvoiceSubmission: - return InvoiceSubmission.parse(to_snake_case_keys(self._POST("auth/w/ext/pay/invoice/expire", body={ "id": id }))) + return InvoiceSubmission.parse(to_snake_case_keys(self._post("auth/w/ext/pay/invoice/expire", body={ "id": id }))) def get_currency_conversion_list(self) -> List[CurrencyConversion]: return [ @@ -68,36 +68,36 @@ class RestMerchantEndpoints(Middleware): base_currency=sub_data["baseCcy"], convert_currency=sub_data["convertCcy"], created=sub_data["created"] - ) for sub_data in self._POST("auth/r/ext/pay/settings/convert/list") + ) for sub_data in self._post("auth/r/ext/pay/settings/convert/list") ] def add_currency_conversion(self, base_currency: str, convert_currency: str) -> bool: - return bool(self._POST("auth/w/ext/pay/settings/convert/create", body={ + return bool(self._post("auth/w/ext/pay/settings/convert/create", body={ "baseCcy": base_currency, "convertCcy": convert_currency })) def remove_currency_conversion(self, base_currency: str, convert_currency: str) -> bool: - return bool(self._POST("auth/w/ext/pay/settings/convert/remove", body={ + return bool(self._post("auth/w/ext/pay/settings/convert/remove", body={ "baseCcy": base_currency, "convertCcy": convert_currency })) def set_merchant_settings(self, key: MerchantSettingsKey, val: Any) -> bool: - return bool(self._POST("auth/w/ext/pay/settings/set", body={ "key": key, "val": val })) + return bool(self._post("auth/w/ext/pay/settings/set", body={ "key": key, "val": val })) def get_merchant_settings(self, key: MerchantSettingsKey) -> Any: - return self._POST("auth/r/ext/pay/settings/get", body={ "key": key }) + return self._post("auth/r/ext/pay/settings/get", body={ "key": key }) def list_merchant_settings(self, keys: List[MerchantSettingsKey] = list()) -> Dict[MerchantSettingsKey, Any]: - return self._POST("auth/r/ext/pay/settings/list", body={ "keys": keys }) + return self._post("auth/r/ext/pay/settings/list", body={ "keys": keys }) def get_deposits(self, start: int, end: int, ccy: Optional[str] = None, unlinked: Optional[bool] = None) -> List[MerchantDeposit]: - return [ MerchantDeposit(**sub_data) for sub_data in to_snake_case_keys(self._POST("auth/r/ext/pay/deposits", body={ + return [ MerchantDeposit(**sub_data) for sub_data in to_snake_case_keys(self._post("auth/r/ext/pay/deposits", body={ "from": start, "to": end, "ccy": ccy, "unlinked": unlinked })) ] def get_unlinked_deposits(self, ccy: str, start: Optional[int] = None, end: Optional[int] = None) -> List[MerchantUnlinkedDeposit]: - return [ MerchantUnlinkedDeposit(**sub_data) for sub_data in to_snake_case_keys(self._POST("/auth/r/ext/pay/deposits/unlinked", body={ + return [ MerchantUnlinkedDeposit(**sub_data) for sub_data in to_snake_case_keys(self._post("/auth/r/ext/pay/deposits/unlinked", body={ "ccy": ccy, "start": start, "end": end })) ] \ No newline at end of file diff --git a/bfxapi/rest/endpoints/rest_public_endpoints.py b/bfxapi/rest/endpoints/rest_public_endpoints.py index b5313fd..66382f4 100644 --- a/bfxapi/rest/endpoints/rest_public_endpoints.py +++ b/bfxapi/rest/endpoints/rest_public_endpoints.py @@ -1,7 +1,17 @@ +#pylint: disable=redefined-builtin + from typing import List, Union, Literal, Optional, Any, cast + from decimal import Decimal -from .. types import * +from .. types import \ + PlatformStatus, TradingPairTicker, FundingCurrencyTicker, \ + TickersHistory, TradingPairTrade, FundingCurrencyTrade, \ + TradingPairBook, FundingCurrencyBook, TradingPairRawBook, \ + FundingCurrencyRawBook, Statistic, Candle, \ + DerivativesStatus, Liquidation, Leaderboard, \ + FundingStatistic, PulseProfile, PulseMessage, \ + TradingMarketAveragePrice, FundingMarketAveragePrice, FxRate from .. import serializers from .. enums import Config, Sort @@ -9,103 +19,151 @@ from .. middleware import Middleware class RestPublicEndpoints(Middleware): def conf(self, config: Config) -> Any: - return self._GET(f"conf/{config}")[0] + return self._get(f"conf/{config}")[0] def get_platform_status(self) -> PlatformStatus: - return serializers.PlatformStatus.parse(*self._GET("platform/status")) + return serializers.PlatformStatus.parse(*self._get("platform/status")) def get_tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]: - data = self._GET("tickers", params={ "symbols": ",".join(symbols) }) - + data = self._get("tickers", params={ "symbols": ",".join(symbols) }) + parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse } - - return [ cast(Union[TradingPairTicker, FundingCurrencyTicker], parsers[sub_data[0][0]](*sub_data)) for sub_data in data ] + + return [ cast(Union[TradingPairTicker, FundingCurrencyTicker], \ + parsers[sub_data[0][0]](*sub_data)) for sub_data in data ] def get_t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]: if isinstance(pairs, str) and pairs == "ALL": - return [ cast(TradingPairTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.symbol).startswith("t") ] + return [ cast(TradingPairTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) \ + if cast(str, sub_data.symbol).startswith("t") ] - data = self.get_tickers([ pair for pair in pairs ]) + data = self.get_tickers(list(pairs)) return cast(List[TradingPairTicker], data) def get_f_tickers(self, currencies: Union[List[str], Literal["ALL"]]) -> List[FundingCurrencyTicker]: if isinstance(currencies, str) and currencies == "ALL": - return [ cast(FundingCurrencyTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) if cast(str, sub_data.symbol).startswith("f") ] + return [ cast(FundingCurrencyTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) \ + if cast(str, sub_data.symbol).startswith("f") ] - data = self.get_tickers([ currency for currency in currencies ]) + data = self.get_tickers(list(currencies)) return cast(List[FundingCurrencyTicker], data) def get_t_ticker(self, pair: str) -> TradingPairTicker: - return serializers.TradingPairTicker.parse(*self._GET(f"ticker/t{pair}"), skip=["SYMBOL"]) + return serializers.TradingPairTicker.parse(*self._get(f"ticker/t{pair}"), skip=["SYMBOL"]) def get_f_ticker(self, currency: str) -> FundingCurrencyTicker: - return serializers.FundingCurrencyTicker.parse(*self._GET(f"ticker/f{currency}"), skip=["SYMBOL"]) + return serializers.FundingCurrencyTicker.parse(*self._get(f"ticker/f{currency}"), skip=["SYMBOL"]) - def get_tickers_history(self, symbols: List[str], start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[TickersHistory]: - return [ serializers.TickersHistory.parse(*sub_data) for sub_data in self._GET("tickers/hist", params={ + def get_tickers_history(self, + symbols: List[str], + *, + start: Optional[str] = None, + end: Optional[str] = None, + limit: Optional[int] = None) -> List[TickersHistory]: + return [ serializers.TickersHistory.parse(*sub_data) for sub_data in self._get("tickers/hist", params={ "symbols": ",".join(symbols), "start": start, "end": end, "limit": limit }) ] - def get_t_trades(self, pair: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[TradingPairTrade]: + def get_t_trades(self, + pair: str, + *, + limit: Optional[int] = None, + start: Optional[str] = None, + end: Optional[str] = None, + sort: Optional[Sort] = None) -> List[TradingPairTrade]: params = { "limit": limit, "start": start, "end": end, "sort": sort } - data = self._GET(f"trades/{pair}/hist", params=params) + data = self._get(f"trades/{pair}/hist", params=params) return [ serializers.TradingPairTrade.parse(*sub_data) for sub_data in data ] - def get_f_trades(self, currency: str, limit: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, sort: Optional[Sort] = None) -> List[FundingCurrencyTrade]: + def get_f_trades(self, + currency: str, + *, + limit: Optional[int] = None, + start: Optional[str] = None, + end: Optional[str] = None, + sort: Optional[Sort] = None) -> List[FundingCurrencyTrade]: params = { "limit": limit, "start": start, "end": end, "sort": sort } - data = self._GET(f"trades/{currency}/hist", params=params) + data = self._get(f"trades/{currency}/hist", params=params) return [ serializers.FundingCurrencyTrade.parse(*sub_data) for sub_data in data ] - def get_t_book(self, pair: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairBook]: - return [ serializers.TradingPairBook.parse(*sub_data) for sub_data in self._GET(f"book/{pair}/{precision}", params={ "len": len }) ] + def get_t_book(self, + pair: str, + precision: Literal["P0", "P1", "P2", "P3", "P4"], + *, + len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairBook]: + return [ serializers.TradingPairBook.parse(*sub_data) \ + for sub_data in self._get(f"book/{pair}/{precision}", params={ "len": len }) ] - def get_f_book(self, currency: str, precision: Literal["P0", "P1", "P2", "P3", "P4"], len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyBook]: - return [ serializers.FundingCurrencyBook.parse(*sub_data) for sub_data in self._GET(f"book/{currency}/{precision}", params={ "len": len }) ] + def get_f_book(self, + currency: str, + precision: Literal["P0", "P1", "P2", "P3", "P4"], + *, + len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyBook]: + return [ serializers.FundingCurrencyBook.parse(*sub_data) \ + for sub_data in self._get(f"book/{currency}/{precision}", params={ "len": len }) ] - def get_t_raw_book(self, pair: str, len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairRawBook]: - return [ serializers.TradingPairRawBook.parse(*sub_data) for sub_data in self._GET(f"book/{pair}/R0", params={ "len": len }) ] + def get_t_raw_book(self, + pair: str, + *, + len: Optional[Literal[1, 25, 100]] = None) -> List[TradingPairRawBook]: + return [ serializers.TradingPairRawBook.parse(*sub_data) \ + for sub_data in self._get(f"book/{pair}/R0", params={ "len": len }) ] - def get_f_raw_book(self, currency: str, len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyRawBook]: - return [ serializers.FundingCurrencyRawBook.parse(*sub_data) for sub_data in self._GET(f"book/{currency}/R0", params={ "len": len }) ] + def get_f_raw_book(self, + currency: str, + *, + len: Optional[Literal[1, 25, 100]] = None) -> List[FundingCurrencyRawBook]: + return [ serializers.FundingCurrencyRawBook.parse(*sub_data) \ + for sub_data in self._get(f"book/{currency}/R0", params={ "len": len }) ] - def get_stats_hist( - self, - resource: str, - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> List[Statistic]: + def get_stats_hist(self, + resource: str, + *, + sort: Optional[Sort] = None, + start: Optional[str] = None, + end: Optional[str] = None, + limit: Optional[int] = None) -> List[Statistic]: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"stats1/{resource}/hist", params=params) + data = self._get(f"stats1/{resource}/hist", params=params) return [ serializers.Statistic.parse(*sub_data) for sub_data in data ] - def get_stats_last( - self, - resource: str, - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Statistic: + def get_stats_last(self, + resource: str, + *, + sort: Optional[Sort] = None, + start: Optional[str] = None, + end: Optional[str] = None, + limit: Optional[int] = None) -> Statistic: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"stats1/{resource}/last", params=params) + data = self._get(f"stats1/{resource}/last", params=params) return serializers.Statistic.parse(*data) - def get_candles_hist( - self, - symbol: str, tf: str = "1m", - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> List[Candle]: + def get_candles_hist(self, + symbol: str, + tf: str = "1m", + *, + sort: Optional[Sort] = None, + start: Optional[str] = None, + end: Optional[str] = None, + limit: Optional[int] = None) -> List[Candle]: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"candles/trade:{tf}:{symbol}/hist", params=params) + data = self._get(f"candles/trade:{tf}:{symbol}/hist", params=params) return [ serializers.Candle.parse(*sub_data) for sub_data in data ] - def get_candles_last( - self, - symbol: str, tf: str = "1m", - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Candle: + def get_candles_last(self, + symbol: str, + tf: str = "1m", + *, + sort: Optional[Sort] = None, + start: Optional[str] = None, + end: Optional[str] = None, + limit: Optional[int] = None) -> Candle: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"candles/trade:{tf}:{symbol}/last", params=params) + data = self._get(f"candles/trade:{tf}:{symbol}/last", params=params) return serializers.Candle.parse(*data) def get_derivatives_status(self, keys: Union[List[str], Literal["ALL"]]) -> List[DerivativesStatus]: @@ -113,74 +171,109 @@ class RestPublicEndpoints(Middleware): params = { "keys": "ALL" } else: params = { "keys": ",".join(keys) } - data = self._GET(f"status/deriv", params=params) + data = self._get("status/deriv", params=params) return [ serializers.DerivativesStatus.parse(*sub_data) for sub_data in data ] - def get_derivatives_status_history( - self, - type: str, symbol: str, - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> List[DerivativesStatus]: + def get_derivatives_status_history(self, + type: str, + symbol: str, + *, + sort: Optional[Sort] = None, + start: Optional[str] = None, + end: Optional[str] = None, + limit: Optional[int] = None) -> List[DerivativesStatus]: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"status/{type}/{symbol}/hist", params=params) + data = self._get(f"status/{type}/{symbol}/hist", params=params) return [ serializers.DerivativesStatus.parse(*sub_data, skip=[ "KEY" ]) for sub_data in data ] - def get_liquidations(self, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Liquidation]: + def get_liquidations(self, + *, + sort: Optional[Sort] = None, + start: Optional[str] = None, + end: Optional[str] = None, + limit: Optional[int] = None) -> List[Liquidation]: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET("liquidations/hist", params=params) + data = self._get("liquidations/hist", params=params) return [ serializers.Liquidation.parse(*sub_data[0]) for sub_data in data ] - def get_seed_candles(self, symbol: str, tf: str = '1m', sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Candle]: + def get_seed_candles(self, + symbol: str, + tf: str = "1m", + *, + sort: Optional[Sort] = None, + start: Optional[str] = None, + end: Optional[str] = None, + limit: Optional[int] = None) -> List[Candle]: params = {"sort": sort, "start": start, "end": end, "limit": limit} - data = self._GET(f"candles/trade:{tf}:{symbol}/hist?limit={limit}&start={start}&end={end}&sort={sort}", params=params) + data = self._get(f"candles/trade:{tf}:{symbol}/hist", params=params) return [ serializers.Candle.parse(*sub_data) for sub_data in data ] - def get_leaderboards_hist( - self, - resource: str, - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> List[Leaderboard]: + def get_leaderboards_hist(self, + resource: str, + *, + sort: Optional[Sort] = None, + start: Optional[str] = None, + end: Optional[str] = None, + limit: Optional[int] = None) -> List[Leaderboard]: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"rankings/{resource}/hist", params=params) + data = self._get(f"rankings/{resource}/hist", params=params) return [ serializers.Leaderboard.parse(*sub_data) for sub_data in data ] - def get_leaderboards_last( - self, - resource: str, - sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None - ) -> Leaderboard: + def get_leaderboards_last(self, + resource: str, + *, + sort: Optional[Sort] = None, + start: Optional[str] = None, + end: Optional[str] = None, + limit: Optional[int] = None) -> Leaderboard: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._GET(f"rankings/{resource}/last", params=params) + data = self._get(f"rankings/{resource}/last", params=params) return serializers.Leaderboard.parse(*data) - def get_funding_stats(self, symbol: str, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingStatistic]: + def get_funding_stats(self, + symbol: str, + *, + start: Optional[str] = None, + end: Optional[str] = None, + limit: Optional[int] = None) -> List[FundingStatistic]: params = { "start": start, "end": end, "limit": limit } - data = self._GET(f"funding/stats/{symbol}/hist", params=params) + data = self._get(f"funding/stats/{symbol}/hist", params=params) return [ serializers.FundingStatistic.parse(*sub_data) for sub_data in data ] def get_pulse_profile(self, nickname: str) -> PulseProfile: - return serializers.PulseProfile.parse(*self._GET(f"pulse/profile/{nickname}")) + return serializers.PulseProfile.parse(*self._get(f"pulse/profile/{nickname}")) - def get_pulse_history(self, end: Optional[str] = None, limit: Optional[int] = None) -> List[PulseMessage]: - messages = list() + def get_pulse_history(self, *, end: Optional[str] = None, limit: Optional[int] = None) -> List[PulseMessage]: + messages = [] - for subdata in self._GET("pulse/hist", params={ "end": end, "limit": limit }): + for subdata in self._get("pulse/hist", params={ "end": end, "limit": limit }): subdata[18] = subdata[18][0] message = serializers.PulseMessage.parse(*subdata) messages.append(message) return messages - def get_trading_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], price_limit: Optional[Union[Decimal, float, str]] = None) -> TradingMarketAveragePrice: - return serializers.TradingMarketAveragePrice.parse(*self._POST("calc/trade/avg", body={ + def get_trading_market_average_price(self, + symbol: str, + amount: Union[Decimal, float, str], + *, + price_limit: Optional[Union[Decimal, float, str]] = None + ) -> TradingMarketAveragePrice: + return serializers.TradingMarketAveragePrice.parse(*self._post("calc/trade/avg", body={ "symbol": symbol, "amount": amount, "price_limit": price_limit })) - def get_funding_market_average_price(self, symbol: str, amount: Union[Decimal, float, str], period: int, rate_limit: Optional[Union[Decimal, float, str]] = None) -> FundingMarketAveragePrice: - return serializers.FundingMarketAveragePrice.parse(*self._POST("calc/trade/avg", body={ + def get_funding_market_average_price(self, + symbol: str, + amount: Union[Decimal, float, str], + period: int, + *, + rate_limit: Optional[Union[Decimal, float, str]] = None + ) -> FundingMarketAveragePrice: + return serializers.FundingMarketAveragePrice.parse(*self._post("calc/trade/avg", body={ "symbol": symbol, "amount": amount, "period": period, "rate_limit": rate_limit })) def get_fx_rate(self, ccy1: str, ccy2: str) -> FxRate: - return serializers.FxRate.parse(*self._POST("calc/fx", body={ "ccy1": ccy1, "ccy2": ccy2 })) \ No newline at end of file + return serializers.FxRate.parse(*self._post("calc/fx", body={ "ccy1": ccy1, "ccy2": ccy2 })) diff --git a/bfxapi/rest/enums.py b/bfxapi/rest/enums.py index c256a34..17b3753 100644 --- a/bfxapi/rest/enums.py +++ b/bfxapi/rest/enums.py @@ -1,3 +1,4 @@ +#pylint: disable-next=wildcard-import,unused-wildcard-import from ..enums import * class Config(str, Enum): @@ -43,4 +44,4 @@ class MerchantSettingsKey(str, Enum): NOTIFY_AUTOCONVERT_EXECUTED = "bfx_pay_notify_autoconvert_executed" DUST_BALANCE_UI = "bfx_pay_dust_balance_ui" MERCHANT_CUSTOMER_SUPPORT_URL = "bfx_pay_merchant_customer_support_url" - MERCHANT_UNDERPAID_THRESHOLD = "bfx_pay_merchant_underpaid_threshold" \ No newline at end of file + MERCHANT_UNDERPAID_THRESHOLD = "bfx_pay_merchant_underpaid_threshold" diff --git a/bfxapi/rest/exceptions.py b/bfxapi/rest/exceptions.py index 9fbf3a4..8ea47c3 100644 --- a/bfxapi/rest/exceptions.py +++ b/bfxapi/rest/exceptions.py @@ -14,32 +14,22 @@ class BfxRestException(BfxBaseException): Base class for all custom exceptions in bfxapi/rest/exceptions.py. """ - pass - class ResourceNotFound(BfxRestException): """ This error indicates a failed HTTP request to a non-existent resource. """ - pass - class RequestParametersError(BfxRestException): """ This error indicates that there are some invalid parameters sent along with an HTTP request. """ - pass - class InvalidAuthenticationCredentials(BfxRestException): """ This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication. """ - pass - class UnknownGenericError(BfxRestException): """ This error indicates an undefined problem processing an HTTP request sent to the APIs. """ - - pass \ No newline at end of file diff --git a/bfxapi/rest/middleware/__init__.py b/bfxapi/rest/middleware/__init__.py index d7e276b..d7f9c42 100644 --- a/bfxapi/rest/middleware/__init__.py +++ b/bfxapi/rest/middleware/__init__.py @@ -1,3 +1,3 @@ from .middleware import Middleware -NAME = "middleware" \ No newline at end of file +NAME = "middleware" diff --git a/bfxapi/rest/middleware/middleware.py b/bfxapi/rest/middleware/middleware.py index db98723..04c0f6c 100644 --- a/bfxapi/rest/middleware/middleware.py +++ b/bfxapi/rest/middleware/middleware.py @@ -1,45 +1,51 @@ -import time, hmac, hashlib, json, requests - -from typing import TYPE_CHECKING, Optional, Any, cast +from typing import TYPE_CHECKING, Optional, Any from http import HTTPStatus + +import time, hmac, hashlib, json, requests + from ..enums import Error from ..exceptions import ResourceNotFound, RequestParametersError, InvalidAuthenticationCredentials, UnknownGenericError - from ...utils.json_encoder import JSONEncoder if TYPE_CHECKING: from requests.sessions import _Params -class Middleware(object): - def __init__(self, host: str, API_KEY: Optional[str] = None, API_SECRET: Optional[str] = None): - self.host, self.API_KEY, self.API_SECRET = host, API_KEY, API_SECRET +class Middleware: + TIMEOUT = 30 + + def __init__(self, host: str, api_key: Optional[str] = None, api_secret: Optional[str] = None): + self.host, self.api_key, self.api_secret = host, api_key, api_secret def __build_authentication_headers(self, endpoint: str, data: Optional[str] = None): - assert isinstance(self.API_KEY, str) and isinstance(self.API_SECRET, str), \ + assert isinstance(self.api_key, str) and isinstance(self.api_secret, str), \ "API_KEY and API_SECRET must be both str to call __build_authentication_headers" nonce = str(round(time.time() * 1_000_000)) - if data == None: + if data is None: path = f"/api/v2/{endpoint}{nonce}" else: path = f"/api/v2/{endpoint}{nonce}{data}" signature = hmac.new( - self.API_SECRET.encode("utf8"), + self.api_secret.encode("utf8"), path.encode("utf8"), - hashlib.sha384 + hashlib.sha384 ).hexdigest() return { "bfx-nonce": nonce, "bfx-signature": signature, - "bfx-apikey": self.API_KEY + "bfx-apikey": self.api_key } - def _GET(self, endpoint: str, params: Optional["_Params"] = None) -> Any: - response = requests.get(f"{self.host}/{endpoint}", params=params) - + def _get(self, endpoint: str, params: Optional["_Params"] = None) -> Any: + response = requests.get( + url=f"{self.host}/{endpoint}", + params=params, + timeout=Middleware.TIMEOUT + ) + if response.status_code == HTTPStatus.NOT_FOUND: raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") @@ -47,23 +53,32 @@ class Middleware(object): if len(data) and data[0] == "error": if data[1] == Error.ERR_PARAMS: - raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") + raise RequestParametersError("The request was rejected with the " + + f"following parameter error: <{data[2]}>") - if data[1] == None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC: - raise UnknownGenericError(f"The server replied to the request with a generic error with message: <{data[2]}>.") + if data[1] is None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC: + raise UnknownGenericError("The server replied to the request with " + + f"a generic error with message: <{data[2]}>.") return data - def _POST(self, endpoint: str, params: Optional["_Params"] = None, body: Optional[Any] = None, _ignore_authentication_headers: bool = False) -> Any: + def _post(self, endpoint: str, params: Optional["_Params"] = None, + body: Optional[Any] = None, _ignore_authentication_headers: bool = False) -> Any: data = body and json.dumps(body, cls=JSONEncoder) or None headers = { "Content-Type": "application/json" } - if self.API_KEY and self.API_SECRET and _ignore_authentication_headers == False: + if self.api_key and self.api_secret and not _ignore_authentication_headers: headers = { **headers, **self.__build_authentication_headers(endpoint, data) } - response = requests.post(f"{self.host}/{endpoint}", params=params, data=data, headers=headers) - + response = requests.post( + url=f"{self.host}/{endpoint}", + params=params, + data=data, + headers=headers, + timeout=Middleware.TIMEOUT + ) + if response.status_code == HTTPStatus.NOT_FOUND: raise ResourceNotFound(f"No resources found at endpoint <{endpoint}>.") @@ -71,12 +86,14 @@ class Middleware(object): if isinstance(data, list) and len(data) and data[0] == "error": if data[1] == Error.ERR_PARAMS: - raise RequestParametersError(f"The request was rejected with the following parameter error: <{data[2]}>") + raise RequestParametersError("The request was rejected with the " + + f"following parameter error: <{data[2]}>") if data[1] == Error.ERR_AUTH_FAIL: raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.") - if data[1] == None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC: - raise UnknownGenericError(f"The server replied to the request with a generic error with message: <{data[2]}>.") + if data[1] is None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC: + raise UnknownGenericError("The server replied to the request with " + + f"a generic error with message: <{data[2]}>.") return data diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index bcdc7f3..3a2b673 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -2,6 +2,7 @@ from . import types from .. labeler import generate_labeler_serializer, generate_recursive_serializer +#pylint: disable-next=unused-import from .. notification import _Notification __serializers__ = [ @@ -27,723 +28,928 @@ __serializers__ = [ #region Serializers definition for Rest Public Endpoints -PlatformStatus = generate_labeler_serializer("PlatformStatus", klass=types.PlatformStatus, labels=[ - "status" -]) +PlatformStatus = generate_labeler_serializer( + name="PlatformStatus", + klass=types.PlatformStatus, + labels=[ + "status" + ] +) -TradingPairTicker = generate_labeler_serializer("TradingPairTicker", klass=types.TradingPairTicker, labels=[ - "symbol", - "bid", - "bid_size", - "ask", - "ask_size", - "daily_change", - "daily_change_relative", - "last_price", - "volume", - "high", - "low" -]) +TradingPairTicker = generate_labeler_serializer( + name="TradingPairTicker", + klass=types.TradingPairTicker, + labels=[ + "symbol", + "bid", + "bid_size", + "ask", + "ask_size", + "daily_change", + "daily_change_relative", + "last_price", + "volume", + "high", + "low" + ] +) -FundingCurrencyTicker = generate_labeler_serializer("FundingCurrencyTicker", klass=types.FundingCurrencyTicker, labels=[ - "symbol", - "frr", - "bid", - "bid_period", - "bid_size", - "ask", - "ask_period", - "ask_size", - "daily_change", - "daily_change_relative", - "last_price", - "volume", - "high", - "low", - "_PLACEHOLDER", - "_PLACEHOLDER", - "frr_amount_available" -]) +FundingCurrencyTicker = generate_labeler_serializer( + name="FundingCurrencyTicker", + klass=types.FundingCurrencyTicker, + labels=[ + "symbol", + "frr", + "bid", + "bid_period", + "bid_size", + "ask", + "ask_period", + "ask_size", + "daily_change", + "daily_change_relative", + "last_price", + "volume", + "high", + "low", + "_PLACEHOLDER", + "_PLACEHOLDER", + "frr_amount_available" + ] +) -TickersHistory = generate_labeler_serializer("TickersHistory", klass=types.TickersHistory, labels=[ - "symbol", - "bid", - "_PLACEHOLDER", - "ask", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "mts" -]) +TickersHistory = generate_labeler_serializer( + name="TickersHistory", + klass=types.TickersHistory, + labels=[ + "symbol", + "bid", + "_PLACEHOLDER", + "ask", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "mts" + ] +) -TradingPairTrade = generate_labeler_serializer("TradingPairTrade", klass=types.TradingPairTrade, labels=[ - "id", - "mts", - "amount", - "price" -]) +TradingPairTrade = generate_labeler_serializer( + name="TradingPairTrade", + klass=types.TradingPairTrade, + labels=[ + "id", + "mts", + "amount", + "price" + ] +) -FundingCurrencyTrade = generate_labeler_serializer("FundingCurrencyTrade", klass=types.FundingCurrencyTrade, labels=[ - "id", - "mts", - "amount", - "rate", - "period" -]) +FundingCurrencyTrade = generate_labeler_serializer( + name="FundingCurrencyTrade", + klass=types.FundingCurrencyTrade, + labels=[ + "id", + "mts", + "amount", + "rate", + "period" + ] +) -TradingPairBook = generate_labeler_serializer("TradingPairBook", klass=types.TradingPairBook, labels=[ - "price", - "count", - "amount" -]) +TradingPairBook = generate_labeler_serializer( + name="TradingPairBook", + klass=types.TradingPairBook, + labels=[ + "price", + "count", + "amount" + ] +) -FundingCurrencyBook = generate_labeler_serializer("FundingCurrencyBook", klass=types.FundingCurrencyBook, labels=[ - "rate", - "period", - "count", - "amount" -]) +FundingCurrencyBook = generate_labeler_serializer( + name="FundingCurrencyBook", + klass=types.FundingCurrencyBook, + labels=[ + "rate", + "period", + "count", + "amount" + ] +) -TradingPairRawBook = generate_labeler_serializer("TradingPairRawBook", klass=types.TradingPairRawBook, labels=[ - "order_id", - "price", - "amount" -]) +TradingPairRawBook = generate_labeler_serializer( + name="TradingPairRawBook", + klass=types.TradingPairRawBook, + labels=[ + "order_id", + "price", + "amount" + ] +) -FundingCurrencyRawBook = generate_labeler_serializer("FundingCurrencyRawBook", klass=types.FundingCurrencyRawBook, labels=[ - "offer_id", - "period", - "rate", - "amount" -]) +FundingCurrencyRawBook = generate_labeler_serializer( + name="FundingCurrencyRawBook", + klass=types.FundingCurrencyRawBook, + labels=[ + "offer_id", + "period", + "rate", + "amount" + ] +) -Statistic = generate_labeler_serializer("Statistic", klass=types.Statistic, labels=[ - "mts", - "value" -]) +Statistic = generate_labeler_serializer( + name="Statistic", + klass=types.Statistic, + labels=[ + "mts", + "value" + ] +) -Candle = generate_labeler_serializer("Candle", klass=types.Candle, labels=[ - "mts", - "open", - "close", - "high", - "low", - "volume" -]) +Candle = generate_labeler_serializer( + name="Candle", + klass=types.Candle, + labels=[ + "mts", + "open", + "close", + "high", + "low", + "volume" + ] +) -DerivativesStatus = generate_labeler_serializer("DerivativesStatus", klass=types.DerivativesStatus, labels=[ - "key", - "mts", - "_PLACEHOLDER", - "deriv_price", - "spot_price", - "_PLACEHOLDER", - "insurance_fund_balance", - "_PLACEHOLDER", - "next_funding_evt_timestamp_ms", - "next_funding_accrued", - "next_funding_step", - "_PLACEHOLDER", - "current_funding", - "_PLACEHOLDER", - "_PLACEHOLDER", - "mark_price", - "_PLACEHOLDER", - "_PLACEHOLDER", - "open_interest", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "clamp_min", - "clamp_max" -]) +DerivativesStatus = generate_labeler_serializer( + name="DerivativesStatus", + klass=types.DerivativesStatus, + labels=[ + "key", + "mts", + "_PLACEHOLDER", + "deriv_price", + "spot_price", + "_PLACEHOLDER", + "insurance_fund_balance", + "_PLACEHOLDER", + "next_funding_evt_timestamp_ms", + "next_funding_accrued", + "next_funding_step", + "_PLACEHOLDER", + "current_funding", + "_PLACEHOLDER", + "_PLACEHOLDER", + "mark_price", + "_PLACEHOLDER", + "_PLACEHOLDER", + "open_interest", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "clamp_min", + "clamp_max" + ] +) -Liquidation = generate_labeler_serializer("Liquidation", klass=types.Liquidation, labels=[ - "_PLACEHOLDER", - "pos_id", - "mts", - "_PLACEHOLDER", - "symbol", - "amount", - "base_price", - "_PLACEHOLDER", - "is_match", - "is_market_sold", - "_PLACEHOLDER", - "price_acquired" -]) +Liquidation = generate_labeler_serializer( + name="Liquidation", + klass=types.Liquidation, + labels=[ + "_PLACEHOLDER", + "pos_id", + "mts", + "_PLACEHOLDER", + "symbol", + "amount", + "base_price", + "_PLACEHOLDER", + "is_match", + "is_market_sold", + "_PLACEHOLDER", + "price_acquired" + ] +) -Leaderboard = generate_labeler_serializer("Leaderboard", klass=types.Leaderboard, labels=[ - "mts", - "_PLACEHOLDER", - "username", - "ranking", - "_PLACEHOLDER", - "_PLACEHOLDER", - "value", - "_PLACEHOLDER", - "_PLACEHOLDER", - "twitter_handle" -]) +Leaderboard = generate_labeler_serializer( + name="Leaderboard", + klass=types.Leaderboard, + labels=[ + "mts", + "_PLACEHOLDER", + "username", + "ranking", + "_PLACEHOLDER", + "_PLACEHOLDER", + "value", + "_PLACEHOLDER", + "_PLACEHOLDER", + "twitter_handle" + ] +) -FundingStatistic = generate_labeler_serializer("FundingStatistic", klass=types.FundingStatistic, labels=[ - "timestamp", - "_PLACEHOLDER", - "_PLACEHOLDER", - "frr", - "avg_period", - "_PLACEHOLDER", - "_PLACEHOLDER", - "funding_amount", - "funding_amount_used", - "_PLACEHOLDER", - "_PLACEHOLDER", - "funding_below_threshold" -]) +FundingStatistic = generate_labeler_serializer( + name="FundingStatistic", + klass=types.FundingStatistic, + labels=[ + "timestamp", + "_PLACEHOLDER", + "_PLACEHOLDER", + "frr", + "avg_period", + "_PLACEHOLDER", + "_PLACEHOLDER", + "funding_amount", + "funding_amount_used", + "_PLACEHOLDER", + "_PLACEHOLDER", + "funding_below_threshold" + ] +) -PulseProfile = generate_labeler_serializer("PulseProfile", klass=types.PulseProfile, labels=[ - "puid", - "mts", - "_PLACEHOLDER", - "nickname", - "_PLACEHOLDER", - "picture", - "text", - "_PLACEHOLDER", - "_PLACEHOLDER", - "twitter_handle", - "_PLACEHOLDER", - "followers", - "following", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "tipping_status" -]) +PulseProfile = generate_labeler_serializer( + name="PulseProfile", + klass=types.PulseProfile, + labels=[ + "puid", + "mts", + "_PLACEHOLDER", + "nickname", + "_PLACEHOLDER", + "picture", + "text", + "_PLACEHOLDER", + "_PLACEHOLDER", + "twitter_handle", + "_PLACEHOLDER", + "followers", + "following", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "tipping_status" + ] +) -PulseMessage = generate_recursive_serializer("PulseMessage", klass=types.PulseMessage, serializers={ "profile": PulseProfile }, labels=[ - "pid", - "mts", - "_PLACEHOLDER", - "puid", - "_PLACEHOLDER", - "title", - "content", - "_PLACEHOLDER", - "_PLACEHOLDER", - "is_pin", - "is_public", - "comments_disabled", - "tags", - "attachments", - "meta", - "likes", - "_PLACEHOLDER", - "_PLACEHOLDER", - "profile", - "comments", - "_PLACEHOLDER", - "_PLACEHOLDER" -]) +PulseMessage = generate_recursive_serializer( + name="PulseMessage", + klass=types.PulseMessage, + serializers={ "profile": PulseProfile }, + labels=[ + "pid", + "mts", + "_PLACEHOLDER", + "puid", + "_PLACEHOLDER", + "title", + "content", + "_PLACEHOLDER", + "_PLACEHOLDER", + "is_pin", + "is_public", + "comments_disabled", + "tags", + "attachments", + "meta", + "likes", + "_PLACEHOLDER", + "_PLACEHOLDER", + "profile", + "comments", + "_PLACEHOLDER", + "_PLACEHOLDER" + ] +) -TradingMarketAveragePrice = generate_labeler_serializer("TradingMarketAveragePrice", klass=types.TradingMarketAveragePrice, labels=[ - "price_avg", - "amount" -]) +TradingMarketAveragePrice = generate_labeler_serializer( + name="TradingMarketAveragePrice", + klass=types.TradingMarketAveragePrice, + labels=[ + "price_avg", + "amount" + ] +) -FundingMarketAveragePrice = generate_labeler_serializer("FundingMarketAveragePrice", klass=types.FundingMarketAveragePrice, labels=[ - "rate_avg", - "amount" -]) +FundingMarketAveragePrice = generate_labeler_serializer( + name="FundingMarketAveragePrice", + klass=types.FundingMarketAveragePrice, + labels=[ + "rate_avg", + "amount" + ] +) -FxRate = generate_labeler_serializer("FxRate", klass=types.FxRate, labels=[ - "current_rate" -]) +FxRate = generate_labeler_serializer( + name="FxRate", + klass=types.FxRate, + labels=[ + "current_rate" + ] +) #endregion #region Serializers definition for Rest Authenticated Endpoints -UserInfo = generate_labeler_serializer("UserInfo", klass=types.UserInfo, labels=[ - "id", - "email", - "username", - "mts_account_create", - "verified", - "verification_level", - "_PLACEHOLDER", - "timezone", - "locale", - "company", - "email_verified", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "mts_master_account_create", - "group_id", - "master_account_id", - "inherit_master_account_verification", - "is_group_master", - "group_withdraw_enabled", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "ppt_enabled", - "merchant_enabled", - "competition_enabled", - "two_factors_authentication_modes", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "is_securities_master", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "securities_enabled", - "allow_disable_ctxswitch", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "time_last_login", - "_PLACEHOLDER", - "_PLACEHOLDER", - "ctxtswitch_disabled", - "_PLACEHOLDER", - "comp_countries", - "compl_countries_resid", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "is_merchant_enterprise" -]) +UserInfo = generate_labeler_serializer( + name="UserInfo", + klass=types.UserInfo, + labels=[ + "id", + "email", + "username", + "mts_account_create", + "verified", + "verification_level", + "_PLACEHOLDER", + "timezone", + "locale", + "company", + "email_verified", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "mts_master_account_create", + "group_id", + "master_account_id", + "inherit_master_account_verification", + "is_group_master", + "group_withdraw_enabled", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "ppt_enabled", + "merchant_enabled", + "competition_enabled", + "two_factors_authentication_modes", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "is_securities_master", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "securities_enabled", + "allow_disable_ctxswitch", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "time_last_login", + "_PLACEHOLDER", + "_PLACEHOLDER", + "ctxtswitch_disabled", + "_PLACEHOLDER", + "comp_countries", + "compl_countries_resid", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "is_merchant_enterprise" + ] +) -LoginHistory = generate_labeler_serializer("LoginHistory", klass=types.LoginHistory, labels=[ - "id", - "_PLACEHOLDER", - "time", - "_PLACEHOLDER", - "ip", - "_PLACEHOLDER", - "_PLACEHOLDER", - "extra_info" -]) +LoginHistory = generate_labeler_serializer( + name="LoginHistory", + klass=types.LoginHistory, + labels=[ + "id", + "_PLACEHOLDER", + "time", + "_PLACEHOLDER", + "ip", + "_PLACEHOLDER", + "_PLACEHOLDER", + "extra_info" + ] +) -BalanceAvailable = generate_labeler_serializer("BalanceAvailable", klass=types.BalanceAvailable, labels=[ - "amount" -]) +BalanceAvailable = generate_labeler_serializer( + name="BalanceAvailable", + klass=types.BalanceAvailable, + labels=[ + "amount" + ] +) -Order = generate_labeler_serializer("Order", klass=types.Order, labels=[ - "id", - "gid", - "cid", - "symbol", - "mts_create", - "mts_update", - "amount", - "amount_orig", - "order_type", - "type_prev", - "mts_tif", - "_PLACEHOLDER", - "flags", - "order_status", - "_PLACEHOLDER", - "_PLACEHOLDER", - "price", - "price_avg", - "price_trailing", - "price_aux_limit", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "notify", - "hidden", - "placed_id", - "_PLACEHOLDER", - "_PLACEHOLDER", - "routing", - "_PLACEHOLDER", - "_PLACEHOLDER", - "meta" -]) +Order = generate_labeler_serializer( + name="Order", + klass=types.Order, + labels=[ + "id", + "gid", + "cid", + "symbol", + "mts_create", + "mts_update", + "amount", + "amount_orig", + "order_type", + "type_prev", + "mts_tif", + "_PLACEHOLDER", + "flags", + "order_status", + "_PLACEHOLDER", + "_PLACEHOLDER", + "price", + "price_avg", + "price_trailing", + "price_aux_limit", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "notify", + "hidden", + "placed_id", + "_PLACEHOLDER", + "_PLACEHOLDER", + "routing", + "_PLACEHOLDER", + "_PLACEHOLDER", + "meta" + ] +) -Position = generate_labeler_serializer("Position", klass=types.Position, labels=[ - "symbol", - "status", - "amount", - "base_price", - "margin_funding", - "margin_funding_type", - "pl", - "pl_perc", - "price_liq", - "leverage", - "_PLACEHOLDER", - "position_id", - "mts_create", - "mts_update", - "_PLACEHOLDER", - "type", - "_PLACEHOLDER", - "collateral", - "collateral_min", - "meta" -]) +Position = generate_labeler_serializer( + name="Position", + klass=types.Position, + labels=[ + "symbol", + "status", + "amount", + "base_price", + "margin_funding", + "margin_funding_type", + "pl", + "pl_perc", + "price_liq", + "leverage", + "_PLACEHOLDER", + "position_id", + "mts_create", + "mts_update", + "_PLACEHOLDER", + "type", + "_PLACEHOLDER", + "collateral", + "collateral_min", + "meta" + ] +) -Trade = generate_labeler_serializer("Trade", klass=types.Trade, labels=[ - "id", - "symbol", - "mts_create", - "order_id", - "exec_amount", - "exec_price", - "order_type", - "order_price", - "maker", - "fee", - "fee_currency", - "cid" -]) +Trade = generate_labeler_serializer( + name="Trade", + klass=types.Trade, + labels=[ + "id", + "symbol", + "mts_create", + "order_id", + "exec_amount", + "exec_price", + "order_type", + "order_price", + "maker", + "fee", + "fee_currency", + "cid" + ] +) -FundingTrade = generate_labeler_serializer("FundingTrade", klass=types.FundingTrade, labels=[ - "id", - "currency", - "mts_create", - "offer_id", - "amount", - "rate", - "period" -]) +FundingTrade = generate_labeler_serializer( + name="FundingTrade", + klass=types.FundingTrade, + labels=[ + "id", + "currency", + "mts_create", + "offer_id", + "amount", + "rate", + "period" + ] +) -OrderTrade = generate_labeler_serializer("OrderTrade", klass=types.OrderTrade, labels=[ - "id", - "symbol", - "mts_create", - "order_id", - "exec_amount", - "exec_price", - "_PLACEHOLDER", - "_PLACEHOLDER", - "maker", - "fee", - "fee_currency", - "cid" -]) +OrderTrade = generate_labeler_serializer( + name="OrderTrade", + klass=types.OrderTrade, + labels=[ + "id", + "symbol", + "mts_create", + "order_id", + "exec_amount", + "exec_price", + "_PLACEHOLDER", + "_PLACEHOLDER", + "maker", + "fee", + "fee_currency", + "cid" + ] +) -Ledger = generate_labeler_serializer("Ledger", klass=types.Ledger, labels=[ - "id", - "currency", - "_PLACEHOLDER", - "mts", - "_PLACEHOLDER", - "amount", - "balance", - "_PLACEHOLDER", - "description" -]) +Ledger = generate_labeler_serializer( + name="Ledger", + klass=types.Ledger, + labels=[ + "id", + "currency", + "_PLACEHOLDER", + "mts", + "_PLACEHOLDER", + "amount", + "balance", + "_PLACEHOLDER", + "description" + ] +) -FundingOffer = generate_labeler_serializer("FundingOffer", klass=types.FundingOffer, labels=[ - "id", - "symbol", - "mts_create", - "mts_update", - "amount", - "amount_orig", - "offer_type", - "_PLACEHOLDER", - "_PLACEHOLDER", - "flags", - "offer_status", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "rate", - "period", - "notify", - "hidden", - "_PLACEHOLDER", - "renew", - "_PLACEHOLDER" -]) +FundingOffer = generate_labeler_serializer( + name="FundingOffer", + klass=types.FundingOffer, + labels=[ + "id", + "symbol", + "mts_create", + "mts_update", + "amount", + "amount_orig", + "offer_type", + "_PLACEHOLDER", + "_PLACEHOLDER", + "flags", + "offer_status", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "rate", + "period", + "notify", + "hidden", + "_PLACEHOLDER", + "renew", + "_PLACEHOLDER" + ] +) -FundingCredit = generate_labeler_serializer("FundingCredit", klass=types.FundingCredit, labels=[ - "id", - "symbol", - "side", - "mts_create", - "mts_update", - "amount", - "flags", - "status", - "rate_type", - "_PLACEHOLDER", - "_PLACEHOLDER", - "rate", - "period", - "mts_opening", - "mts_last_payout", - "notify", - "hidden", - "_PLACEHOLDER", - "renew", - "_PLACEHOLDER", - "no_close", - "position_pair" -]) +FundingCredit = generate_labeler_serializer( + name="FundingCredit", + klass=types.FundingCredit, + labels=[ + "id", + "symbol", + "side", + "mts_create", + "mts_update", + "amount", + "flags", + "status", + "rate_type", + "_PLACEHOLDER", + "_PLACEHOLDER", + "rate", + "period", + "mts_opening", + "mts_last_payout", + "notify", + "hidden", + "_PLACEHOLDER", + "renew", + "_PLACEHOLDER", + "no_close", + "position_pair" + ] +) -FundingLoan = generate_labeler_serializer("FundingLoan", klass=types.FundingLoan, labels=[ - "id", - "symbol", - "side", - "mts_create", - "mts_update", - "amount", - "flags", - "status", - "rate_type", - "_PLACEHOLDER", - "_PLACEHOLDER", - "rate", - "period", - "mts_opening", - "mts_last_payout", - "notify", - "hidden", - "_PLACEHOLDER", - "renew", - "_PLACEHOLDER", - "no_close" -]) +FundingLoan = generate_labeler_serializer( + name="FundingLoan", + klass=types.FundingLoan, + labels=[ + "id", + "symbol", + "side", + "mts_create", + "mts_update", + "amount", + "flags", + "status", + "rate_type", + "_PLACEHOLDER", + "_PLACEHOLDER", + "rate", + "period", + "mts_opening", + "mts_last_payout", + "notify", + "hidden", + "_PLACEHOLDER", + "renew", + "_PLACEHOLDER", + "no_close" + ] +) -FundingAutoRenew = generate_labeler_serializer("FundingAutoRenew", klass=types.FundingAutoRenew, labels=[ - "currency", - "period", - "rate", - "threshold" -]) +FundingAutoRenew = generate_labeler_serializer( + name="FundingAutoRenew", + klass=types.FundingAutoRenew, + labels=[ + "currency", + "period", + "rate", + "threshold" + ] +) -FundingInfo = generate_labeler_serializer("FundingInfo", klass=types.FundingInfo, labels=[ - "symbol", - "yield_loan", - "yield_lend", - "duration_loan", - "duration_lend" -]) +FundingInfo = generate_labeler_serializer( + name="FundingInfo", + klass=types.FundingInfo, + labels=[ + "symbol", + "yield_loan", + "yield_lend", + "duration_loan", + "duration_lend" + ] +) -Wallet = generate_labeler_serializer("Wallet", klass=types.Wallet, labels=[ - "wallet_type", - "currency", - "balance", - "unsettled_interest", - "available_balance", - "last_change", - "trade_details" -]) +Wallet = generate_labeler_serializer( + name="Wallet", + klass=types.Wallet, + labels=[ + "wallet_type", + "currency", + "balance", + "unsettled_interest", + "available_balance", + "last_change", + "trade_details" + ] +) -Transfer = generate_labeler_serializer("Transfer", klass=types.Transfer, labels=[ - "mts", - "wallet_from", - "wallet_to", - "_PLACEHOLDER", - "currency", - "currency_to", - "_PLACEHOLDER", - "amount" -]) +Transfer = generate_labeler_serializer( + name="Transfer", + klass=types.Transfer, + labels=[ + "mts", + "wallet_from", + "wallet_to", + "_PLACEHOLDER", + "currency", + "currency_to", + "_PLACEHOLDER", + "amount" + ] +) -Withdrawal = generate_labeler_serializer("Withdrawal", klass=types.Withdrawal, labels=[ - "withdrawal_id", - "_PLACEHOLDER", - "method", - "payment_id", - "wallet", - "amount", - "_PLACEHOLDER", - "_PLACEHOLDER", - "withdrawal_fee" -]) +Withdrawal = generate_labeler_serializer( + name="Withdrawal", + klass=types.Withdrawal, + labels=[ + "withdrawal_id", + "_PLACEHOLDER", + "method", + "payment_id", + "wallet", + "amount", + "_PLACEHOLDER", + "_PLACEHOLDER", + "withdrawal_fee" + ] +) -DepositAddress = generate_labeler_serializer("DepositAddress", klass=types.DepositAddress, labels=[ - "_PLACEHOLDER", - "method", - "currency_code", - "_PLACEHOLDER", - "address", - "pool_address" -]) +DepositAddress = generate_labeler_serializer( + name="DepositAddress", + klass=types.DepositAddress, + labels=[ + "_PLACEHOLDER", + "method", + "currency_code", + "_PLACEHOLDER", + "address", + "pool_address" + ] +) -LightningNetworkInvoice = generate_labeler_serializer("LightningNetworkInvoice", klass=types.LightningNetworkInvoice, labels=[ - "invoice_hash", - "invoice", - "_PLACEHOLDER", - "_PLACEHOLDER", - "amount" -]) +LightningNetworkInvoice = generate_labeler_serializer( + name="LightningNetworkInvoice", + klass=types.LightningNetworkInvoice, + labels=[ + "invoice_hash", + "invoice", + "_PLACEHOLDER", + "_PLACEHOLDER", + "amount" + ] +) -Movement = generate_labeler_serializer("Movement", klass=types.Movement, labels=[ - "id", - "currency", - "currency_name", - "_PLACEHOLDER", - "_PLACEHOLDER", - "mts_start", - "mts_update", - "_PLACEHOLDER", - "_PLACEHOLDER", - "status", - "_PLACEHOLDER", - "_PLACEHOLDER", - "amount", - "fees", - "_PLACEHOLDER", - "_PLACEHOLDER", - "destination_address", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "transaction_id", - "withdraw_transaction_note" -]) +Movement = generate_labeler_serializer( + name="Movement", + klass=types.Movement, + labels=[ + "id", + "currency", + "currency_name", + "_PLACEHOLDER", + "_PLACEHOLDER", + "mts_start", + "mts_update", + "_PLACEHOLDER", + "_PLACEHOLDER", + "status", + "_PLACEHOLDER", + "_PLACEHOLDER", + "amount", + "fees", + "_PLACEHOLDER", + "_PLACEHOLDER", + "destination_address", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "transaction_id", + "withdraw_transaction_note" + ] +) -SymbolMarginInfo = generate_labeler_serializer("SymbolMarginInfo", klass=types.SymbolMarginInfo, labels=[ - "symbol", - "tradable_balance", - "gross_balance", - "buy", - "sell" -]) +SymbolMarginInfo = generate_labeler_serializer( + name="SymbolMarginInfo", + klass=types.SymbolMarginInfo, + labels=[ + "symbol", + "tradable_balance", + "gross_balance", + "buy", + "sell" + ] +) -BaseMarginInfo = generate_labeler_serializer("BaseMarginInfo", klass=types.BaseMarginInfo, labels=[ - "user_pl", - "user_swaps", - "margin_balance", - "margin_net", - "margin_min" -]) +BaseMarginInfo = generate_labeler_serializer( + name="BaseMarginInfo", + klass=types.BaseMarginInfo, + labels=[ + "user_pl", + "user_swaps", + "margin_balance", + "margin_net", + "margin_min" + ] +) -PositionClaim = generate_labeler_serializer("PositionClaim", klass=types.PositionClaim, labels=[ - "symbol", - "position_status", - "amount", - "base_price", - "margin_funding", - "margin_funding_type", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "position_id", - "mts_create", - "mts_update", - "_PLACEHOLDER", - "pos_type", - "_PLACEHOLDER", - "collateral", - "min_collateral", - "meta" -]) +PositionClaim = generate_labeler_serializer( + name="PositionClaim", + klass=types.PositionClaim, + labels=[ + "symbol", + "position_status", + "amount", + "base_price", + "margin_funding", + "margin_funding_type", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "position_id", + "mts_create", + "mts_update", + "_PLACEHOLDER", + "pos_type", + "_PLACEHOLDER", + "collateral", + "min_collateral", + "meta" + ] +) -PositionIncreaseInfo = generate_labeler_serializer("PositionIncreaseInfo", klass=types.PositionIncreaseInfo, labels=[ - "max_pos", - "current_pos", - "base_currency_balance", - "tradable_balance_quote_currency", - "tradable_balance_quote_total", - "tradable_balance_base_currency", - "tradable_balance_base_total", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "funding_avail", - "funding_value", - "funding_required", - "funding_value_currency", - "funding_required_currency" -]) +PositionIncreaseInfo = generate_labeler_serializer( + name="PositionIncreaseInfo", + klass=types.PositionIncreaseInfo, + labels=[ + "max_pos", + "current_pos", + "base_currency_balance", + "tradable_balance_quote_currency", + "tradable_balance_quote_total", + "tradable_balance_base_currency", + "tradable_balance_base_total", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "funding_avail", + "funding_value", + "funding_required", + "funding_value_currency", + "funding_required_currency" + ] +) -PositionIncrease = generate_labeler_serializer("PositionIncrease", klass=types.PositionIncrease, labels=[ - "symbol", - "_PLACEHOLDER", - "amount", - "base_price" -]) +PositionIncrease = generate_labeler_serializer( + name="PositionIncrease", + klass=types.PositionIncrease, + labels=[ + "symbol", + "_PLACEHOLDER", + "amount", + "base_price" + ] +) -PositionHistory = generate_labeler_serializer("PositionHistory", klass=types.PositionHistory, labels=[ - "symbol", - "status", - "amount", - "base_price", - "funding", - "funding_type", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "position_id", - "mts_create", - "mts_update" -]) +PositionHistory = generate_labeler_serializer( + name="PositionHistory", + klass=types.PositionHistory, + labels=[ + "symbol", + "status", + "amount", + "base_price", + "funding", + "funding_type", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "position_id", + "mts_create", + "mts_update" + ] +) -PositionSnapshot = generate_labeler_serializer("PositionSnapshot", klass=types.PositionSnapshot, labels=[ - "symbol", - "status", - "amount", - "base_price", - "funding", - "funding_type", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "position_id", - "mts_create", - "mts_update" -]) +PositionSnapshot = generate_labeler_serializer( + name="PositionSnapshot", + klass=types.PositionSnapshot, + labels=[ + "symbol", + "status", + "amount", + "base_price", + "funding", + "funding_type", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "position_id", + "mts_create", + "mts_update" + ] +) -PositionAudit = generate_labeler_serializer("PositionAudit", klass=types.PositionAudit, labels=[ - "symbol", - "status", - "amount", - "base_price", - "funding", - "funding_type", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "position_id", - "mts_create", - "mts_update", - "_PLACEHOLDER", - "type", - "_PLACEHOLDER", - "collateral", - "collateral_min", - "meta" -]) +PositionAudit = generate_labeler_serializer( + name="PositionAudit", + klass=types.PositionAudit, + labels=[ + "symbol", + "status", + "amount", + "base_price", + "funding", + "funding_type", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "_PLACEHOLDER", + "position_id", + "mts_create", + "mts_update", + "_PLACEHOLDER", + "type", + "_PLACEHOLDER", + "collateral", + "collateral_min", + "meta" + ] +) -DerivativePositionCollateral = generate_labeler_serializer("DerivativePositionCollateral", klass=types.DerivativePositionCollateral, labels=[ - "status" -]) +DerivativePositionCollateral = generate_labeler_serializer( + name="DerivativePositionCollateral", + klass=types.DerivativePositionCollateral, + labels=[ + "status" + ] +) -DerivativePositionCollateralLimits = generate_labeler_serializer("DerivativePositionCollateralLimits", klass=types.DerivativePositionCollateralLimits, labels=[ - "min_collateral", - "max_collateral" -]) +DerivativePositionCollateralLimits = generate_labeler_serializer( + name="DerivativePositionCollateralLimits", + klass=types.DerivativePositionCollateralLimits, + labels=[ + "min_collateral", + "max_collateral" + ] +) -#endregion \ No newline at end of file +#endregion diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 32c2a9b..65d5423 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -1,9 +1,13 @@ +#pylint: disable-next=wildcard-import,unused-wildcard-import from typing import * from dataclasses import dataclass from .. labeler import _Type, partial, compose + +#pylint: disable-next=unused-import from .. notification import Notification + from ..utils.json_encoder import JSON #region Type hinting for Rest Public Endpoints @@ -11,7 +15,7 @@ from ..utils.json_encoder import JSON @dataclass class PlatformStatus(_Type): status: int - + @dataclass class TradingPairTicker(_Type): symbol: Optional[str] @@ -53,43 +57,43 @@ class TickersHistory(_Type): @dataclass class TradingPairTrade(_Type): - id: int - mts: int - amount: float + id: int + mts: int + amount: float price: float @dataclass class FundingCurrencyTrade(_Type): - id: int - mts: int - amount: float - rate: float + id: int + mts: int + amount: float + rate: float period: int @dataclass class TradingPairBook(_Type): - price: float - count: int + price: float + count: int amount: float - + @dataclass class FundingCurrencyBook(_Type): - rate: float - period: int - count: int + rate: float + period: int + count: int amount: float -@dataclass +@dataclass class TradingPairRawBook(_Type): order_id: int - price: float + price: float amount: float -@dataclass +@dataclass class FundingCurrencyRawBook(_Type): - offer_id: int - period: int - rate: float + offer_id: int + period: int + rate: float amount: float @dataclass @@ -142,7 +146,7 @@ class Leaderboard(_Type): twitter_handle: Optional[str] @dataclass -class FundingStatistic(_Type): +class FundingStatistic(_Type): timestamp: int frr: float avg_period: float @@ -286,14 +290,14 @@ class Position(_Type): @dataclass class Trade(_Type): - id: int - symbol: str + id: int + symbol: str mts_create: int - order_id: int - exec_amount: float - exec_price: float - order_type: str - order_price: float + order_id: int + exec_amount: float + exec_price: float + order_type: str + order_price: float maker:int fee: float fee_currency: str @@ -311,12 +315,12 @@ class FundingTrade(_Type): @dataclass class OrderTrade(_Type): - id: int - symbol: str + id: int + symbol: str mts_create: int - order_id: int - exec_amount: float - exec_price: float + order_id: int + exec_amount: float + exec_price: float maker:int fee: float fee_currency: str @@ -325,7 +329,7 @@ class OrderTrade(_Type): @dataclass class Ledger(_Type): id: int - currency: str + currency: str mts: int amount: float balance: float @@ -413,7 +417,7 @@ class Wallet(_Type): available_balance: float last_change: str trade_details: JSON - + @dataclass class Transfer(_Type): mts: int @@ -458,7 +462,7 @@ class Movement(_Type): destination_address: str transaction_id: str withdraw_transaction_note: str - + @dataclass class SymbolMarginInfo(_Type): symbol: str @@ -586,16 +590,16 @@ class InvoiceSubmission(_Type): @classmethod 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"] is not None: data["customer_info"] = InvoiceSubmission.CustomerInfo(**data["customer_info"]) for index, invoice in enumerate(data["invoices"]): data["invoices"][index] = InvoiceSubmission.Invoice(**invoice) - if "payment" in data and data["payment"] != None: + if "payment" in data and data["payment"] is not None: 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"] is not None: for index, additional_payment in enumerate(data["additional_payments"]): data["additional_payments"][index] = InvoiceSubmission.Payment(**additional_payment) @@ -694,4 +698,4 @@ class MerchantUnlinkedDeposit(_Type): status: str note: Optional[str] -#endregion \ No newline at end of file +#endregion diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 360f840..88cb853 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -29,6 +29,27 @@ def _require_websocket_authentication(function: F) -> F: return cast(F, wrapper) +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 + class BfxWebsocketClient: VERSION = BfxWebsocketBucket.VERSION @@ -135,27 +156,6 @@ class BfxWebsocketClient: if 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: await asyncio.sleep(delay.next()) diff --git a/bfxapi/websocket/client/bfx_websocket_inputs.py b/bfxapi/websocket/client/bfx_websocket_inputs.py index 25ad8b4..77a1521 100644 --- a/bfxapi/websocket/client/bfx_websocket_inputs.py +++ b/bfxapi/websocket/client/bfx_websocket_inputs.py @@ -1,4 +1,4 @@ -#pylint: disable=redefined-builtin,too-many-arguments +#pylint: disable=redefined-builtin from decimal import Decimal from datetime import datetime @@ -15,6 +15,7 @@ class BfxWebsocketInputs: type: OrderType, symbol: str, amount: Union[Decimal, float, str], + *, price: Optional[Union[Decimal, float, str]] = None, lev: Optional[int] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, @@ -35,6 +36,7 @@ class BfxWebsocketInputs: async def update_order(self, id: int, + *, amount: Optional[Union[Decimal, float, str]] = None, price: Optional[Union[Decimal, float, str]] = None, cid: Optional[int] = None, @@ -54,6 +56,7 @@ class BfxWebsocketInputs: }) async def cancel_order(self, + *, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None): @@ -62,6 +65,7 @@ class BfxWebsocketInputs: }) async def cancel_order_multi(self, + *, ids: Optional[List[int]] = None, cids: Optional[List[Tuple[int, str]]] = None, gids: Optional[List[int]] = None, @@ -71,12 +75,14 @@ class BfxWebsocketInputs: "all": int(all) }) + #pylint: disable-next=too-many-arguments async def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, float, str], rate: Union[Decimal, float, str], period: int, + *, flags: Optional[int] = 0): await self.__handle_websocket_input("fon", { "type": type, "symbol": symbol, "amount": amount, diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py index 2909030..fa0972a 100644 --- a/bfxapi/websocket/types.py +++ b/bfxapi/websocket/types.py @@ -3,11 +3,11 @@ from typing import * from dataclasses import dataclass +from .. labeler import _Type + #pylint: disable-next=unused-import from .. notification import Notification -from .. labeler import _Type - from ..utils.json_encoder import JSON #region Type hinting for Websocket Public Channels From 06482ea3d3269fa9f850faf470956bdcf32de36f Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 7 Mar 2023 18:02:26 +0100 Subject: [PATCH 201/265] Apply pylint's linting rules to bfxapi/rest/endpoints/rest_authenticated_endpoints.py. --- .../endpoints/rest_authenticated_endpoints.py | 407 ++++++++++++------ 1 file changed, 287 insertions(+), 120 deletions(-) diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index 0ed2241..97f85e7 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -1,42 +1,80 @@ -from typing import List, Tuple, Union, Literal, Optional +#pylint: disable=redefined-builtin + +from typing import Dict, List, Tuple, Union, Literal, Optional from decimal import Decimal from datetime import datetime -from .. types import * +from .. types import Notification, \ + UserInfo, LoginHistory, BalanceAvailable, \ + Order, Position, Trade, \ + FundingTrade, OrderTrade, Ledger, \ + FundingOffer, FundingCredit, FundingLoan, \ + FundingAutoRenew, FundingInfo, Wallet, \ + Transfer, Withdrawal, DepositAddress, \ + LightningNetworkInvoice, Movement, SymbolMarginInfo, \ + BaseMarginInfo, PositionClaim, PositionIncreaseInfo, \ + PositionIncrease, PositionHistory, PositionSnapshot, \ + PositionAudit, DerivativePositionCollateral, DerivativePositionCollateralLimits from .. import serializers + +from .. serializers import _Notification from .. enums import Sort, OrderType, FundingOfferType from .. middleware import Middleware +from ...utils.json_encoder import JSON + class RestAuthenticatedEndpoints(Middleware): def get_user_info(self) -> UserInfo: - return serializers.UserInfo.parse(*self._post(f"auth/r/info/user")) + return serializers.UserInfo.parse(*self._post("auth/r/info/user")) def get_login_history(self) -> List[LoginHistory]: - return [ serializers.LoginHistory.parse(*sub_data) for sub_data in self._post("auth/r/logins/hist") ] + return [ serializers.LoginHistory.parse(*sub_data) \ + for sub_data in self._post("auth/r/logins/hist") ] - def get_balance_available_for_orders_or_offers(self, symbol: str, type: str, dir: Optional[int] = None, rate: Optional[str] = None, lev: Optional[str] = None) -> BalanceAvailable: + def get_balance_available_for_orders_or_offers(self, + symbol: str, + type: str, + *, + dir: Optional[int] = None, + rate: Optional[str] = None, + lev: Optional[str] = None) -> BalanceAvailable: return serializers.BalanceAvailable.parse(*self._post("auth/calc/order/avail", body={ "symbol": symbol, "type": type, "dir": dir, "rate": rate, "lev": lev })) def get_wallets(self) -> List[Wallet]: - return [ serializers.Wallet.parse(*sub_data) for sub_data in self._post("auth/r/wallets") ] + return [ serializers.Wallet.parse(*sub_data) \ + for sub_data in self._post("auth/r/wallets") ] - def get_orders(self, symbol: Optional[str] = None, ids: Optional[List[str]] = None) -> List[Order]: + def get_orders(self, + *, + symbol: Optional[str] = None, + ids: Optional[List[str]] = None) -> List[Order]: endpoint = "auth/r/orders" - if symbol != None: + if symbol is not None: endpoint += f"/{symbol}" - return [ serializers.Order.parse(*sub_data) for sub_data in self._post(endpoint, body={ "id": ids }) ] + return [ serializers.Order.parse(*sub_data) \ + for sub_data in self._post(endpoint, body={ "id": ids }) ] - def submit_order(self, type: OrderType, symbol: str, amount: Union[Decimal, float, str], - price: Optional[Union[Decimal, float, str]] = None, lev: Optional[int] = None, - price_trailing: Optional[Union[Decimal, float, str]] = None, price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_oco_stop: Optional[Union[Decimal, float, str]] = None, - gid: Optional[int] = None, cid: Optional[int] = None, - flags: Optional[int] = 0, tif: Optional[Union[datetime, str]] = None, meta: Optional[JSON] = None) -> Notification[Order]: + def submit_order(self, + type: OrderType, + symbol: str, + amount: Union[Decimal, float, str], + *, + price: Optional[Union[Decimal, float, str]] = None, + lev: Optional[int] = None, + price_trailing: Optional[Union[Decimal, float, str]] = None, + price_aux_limit: Optional[Union[Decimal, float, str]] = None, + price_oco_stop: Optional[Union[Decimal, float, str]] = None, + gid: Optional[int] = None, + cid: Optional[int] = None, + flags: Optional[int] = 0, + tif: Optional[Union[datetime, str]] = None, + meta: Optional[JSON] = None) -> Notification[Order]: body = { "type": type, "symbol": symbol, "amount": amount, "price": price, "lev": lev, @@ -44,32 +82,53 @@ class RestAuthenticatedEndpoints(Middleware): "gid": gid, "cid": cid, "flags": flags, "tif": tif, "meta": meta } - - return serializers._Notification[Order](serializers.Order).parse(*self._post("auth/w/order/submit", body=body)) - def update_order(self, id: int, amount: Optional[Union[Decimal, float, str]] = None, price: Optional[Union[Decimal, float, str]] = None, - cid: Optional[int] = None, cid_date: Optional[str] = None, gid: Optional[int] = None, - flags: Optional[int] = 0, lev: Optional[int] = None, delta: Optional[Union[Decimal, float, str]] = None, - price_aux_limit: Optional[Union[Decimal, float, str]] = None, price_trailing: Optional[Union[Decimal, float, str]] = None, tif: Optional[Union[datetime, str]] = None) -> Notification[Order]: + return _Notification[Order](serializers.Order).parse(*self._post("auth/w/order/submit", body=body)) + + def update_order(self, + id: int, + *, + amount: Optional[Union[Decimal, float, str]] = None, + price: Optional[Union[Decimal, float, str]] = None, + cid: Optional[int] = None, + cid_date: Optional[str] = None, + gid: Optional[int] = None, + flags: Optional[int] = 0, + lev: Optional[int] = None, + delta: Optional[Union[Decimal, float, str]] = None, + price_aux_limit: Optional[Union[Decimal, float, str]] = None, + price_trailing: Optional[Union[Decimal, float, str]] = None, + tif: Optional[Union[datetime, str]] = None) -> Notification[Order]: body = { "id": id, "amount": amount, "price": price, "cid": cid, "cid_date": cid_date, "gid": gid, "flags": flags, "lev": lev, "delta": delta, "price_aux_limit": price_aux_limit, "price_trailing": price_trailing, "tif": tif } - - return serializers._Notification[Order](serializers.Order).parse(*self._post("auth/w/order/update", body=body)) - def cancel_order(self, id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None) -> Notification[Order]: - body = { + return _Notification[Order](serializers.Order) \ + .parse(*self._post("auth/w/order/update", body=body)) + + def cancel_order(self, + *, + id: Optional[int] = None, + cid: Optional[int] = None, + cid_date: Optional[str] = None) -> Notification[Order]: + body = { "id": id, "cid": cid, "cid_date": cid_date } - return serializers._Notification[Order](serializers.Order).parse(*self._post("auth/w/order/cancel", body=body)) + return _Notification[Order](serializers.Order) \ + .parse(*self._post("auth/w/order/cancel", body=body)) - def cancel_order_multi(self, ids: Optional[List[int]] = None, cids: Optional[List[Tuple[int, str]]] = None, gids: Optional[List[int]] = None, all: bool = False) -> Notification[List[Order]]: + def cancel_order_multi(self, + *, + ids: Optional[List[int]] = None, + cids: Optional[List[Tuple[int, str]]] = None, + gids: Optional[List[int]] = None, + all: bool = False) -> Notification[List[Order]]: body = { "ids": ids, "cids": cids, @@ -78,13 +137,20 @@ class RestAuthenticatedEndpoints(Middleware): "all": int(all) } - return serializers._Notification[List[Order]](serializers.Order, is_iterable=True).parse(*self._post("auth/w/order/cancel/multi", body=body)) + return _Notification[List[Order]](serializers.Order, is_iterable=True) \ + .parse(*self._post("auth/w/order/cancel/multi", body=body)) - def get_orders_history(self, symbol: Optional[str] = None, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Order]: - if symbol == None: + def get_orders_history(self, + *, + symbol: Optional[str] = None, + ids: Optional[List[int]] = None, + start: Optional[str] = None, + end: Optional[str] = None, + limit: Optional[int] = None) -> List[Order]: + if symbol is None: endpoint = "auth/r/orders/hist" else: endpoint = f"auth/r/orders/{symbol}/hist" - + body = { "id": ids, "start": start, "end": end, @@ -93,14 +159,23 @@ class RestAuthenticatedEndpoints(Middleware): return [ serializers.Order.parse(*sub_data) for sub_data in self._post(endpoint, body=body) ] - def get_order_trades(self, symbol: str, id: int) -> List[OrderTrade]: - return [ serializers.OrderTrade.parse(*sub_data) for sub_data in self._post(f"auth/r/order/{symbol}:{id}/trades") ] + def get_order_trades(self, + symbol: str, + id: int) -> List[OrderTrade]: + return [ serializers.OrderTrade.parse(*sub_data) \ + for sub_data in self._post(f"auth/r/order/{symbol}:{id}/trades") ] - def get_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Trade]: - if symbol == None: + def get_trades_history(self, + *, + symbol: Optional[str] = None, + sort: Optional[Sort] = None, + start: Optional[str] = None, + end: Optional[str] = None, + limit: Optional[int] = None) -> List[Trade]: + if symbol is None: endpoint = "auth/r/trades/hist" else: endpoint = f"auth/r/trades/{symbol}/hist" - + body = { "sort": sort, "start": start, "end": end, @@ -109,17 +184,24 @@ class RestAuthenticatedEndpoints(Middleware): return [ serializers.Trade.parse(*sub_data) for sub_data in self._post(endpoint, body=body) ] - def get_ledgers(self, currency: str, category: Optional[int] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Ledger]: + def get_ledgers(self, + currency: str, + *, + category: Optional[int] = None, + start: Optional[str] = None, + end: Optional[str] = None, + limit: Optional[int] = None) -> List[Ledger]: body = { "category": category, "start": start, "end": end, "limit": limit } - return [ serializers.Ledger.parse(*sub_data) for sub_data in self._post(f"auth/r/ledgers/{currency}/hist", body=body) ] + return [ serializers.Ledger.parse(*sub_data) \ + for sub_data in self._post(f"auth/r/ledgers/{currency}/hist", body=body) ] def get_base_margin_info(self) -> BaseMarginInfo: - return serializers.BaseMarginInfo.parse(*(self._post(f"auth/r/info/margin/base")[1])) + return serializers.BaseMarginInfo.parse(*(self._post("auth/r/info/margin/base")[1])) def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo: response = self._post(f"auth/r/info/margin/{symbol}") @@ -127,51 +209,88 @@ class RestAuthenticatedEndpoints(Middleware): return serializers.SymbolMarginInfo.parse(*data) def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]: - return [ serializers.SymbolMarginInfo.parse(*([sub_data[1]] + sub_data[2])) for sub_data in self._post(f"auth/r/info/margin/sym_all") ] + return [ serializers.SymbolMarginInfo.parse(*([sub_data[1]] + sub_data[2])) \ + for sub_data in self._post("auth/r/info/margin/sym_all") ] def get_positions(self) -> List[Position]: - return [ serializers.Position.parse(*sub_data) for sub_data in self._post("auth/r/positions") ] + return [ serializers.Position.parse(*sub_data) for sub_data in self._post("auth/r/positions") ] - def claim_position(self, id: int, amount: Optional[Union[Decimal, float, str]] = None) -> Notification[PositionClaim]: - return serializers._Notification[PositionClaim](serializers.PositionClaim).parse( - *self._post("auth/w/position/claim", body={ "id": id, "amount": amount }) - ) + def claim_position(self, + id: int, + *, + amount: Optional[Union[Decimal, float, str]] = None) -> Notification[PositionClaim]: + return _Notification[PositionClaim](serializers.PositionClaim) \ + .parse(*self._post("auth/w/position/claim", body={ "id": id, "amount": amount })) - def increase_position(self, symbol: str, amount: Union[Decimal, float, str]) -> Notification[PositionIncrease]: - return serializers._Notification[PositionIncrease](serializers.PositionIncrease).parse( - *self._post("auth/w/position/increase", body={ "symbol": symbol, "amount": amount }) - ) + def increase_position(self, + symbol: str, + amount: Union[Decimal, float, str]) -> Notification[PositionIncrease]: + return _Notification[PositionIncrease](serializers.PositionIncrease) \ + .parse(*self._post("auth/w/position/increase", body={ "symbol": symbol, "amount": amount })) - def get_increase_position_info(self, symbol: str, amount: Union[Decimal, float, str]) -> PositionIncreaseInfo: - response = self._post(f"auth/r/position/increase/info", body={ "symbol": symbol, "amount": amount }) + def get_increase_position_info(self, + symbol: str, + amount: Union[Decimal, float, str]) -> PositionIncreaseInfo: + response = self._post("auth/r/position/increase/info", body={ "symbol": symbol, "amount": amount }) data = response[0] + [response[1][0]] + response[1][1] + [response[1][2]] + response[4] + response[5] return serializers.PositionIncreaseInfo.parse(*data) - def get_positions_history(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionHistory]: - return [ serializers.PositionHistory.parse(*sub_data) for sub_data in self._post("auth/r/positions/hist", body={ "start": start, "end": end, "limit": limit }) ] + def get_positions_history(self, + *, + start: Optional[str] = None, + end: Optional[str] = None, + limit: Optional[int] = None) -> List[PositionHistory]: + return [ serializers.PositionHistory.parse(*sub_data) \ + for sub_data in self._post("auth/r/positions/hist", \ + body={ "start": start, "end": end, "limit": limit }) ] - def get_positions_snapshot(self, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionSnapshot]: - return [ serializers.PositionSnapshot.parse(*sub_data) for sub_data in self._post("auth/r/positions/snap", body={ "start": start, "end": end, "limit": limit }) ] + def get_positions_snapshot(self, + *, + start: Optional[str] = None, + end: Optional[str] = None, + limit: Optional[int] = None) -> List[PositionSnapshot]: + return [ serializers.PositionSnapshot.parse(*sub_data) \ + for sub_data in self._post("auth/r/positions/snap", \ + body={ "start": start, "end": end, "limit": limit }) ] - def get_positions_audit(self, ids: Optional[List[int]] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionAudit]: - return [ serializers.PositionAudit.parse(*sub_data) for sub_data in self._post("auth/r/positions/audit", body={ "ids": ids, "start": start, "end": end, "limit": limit }) ] + def get_positions_audit(self, + *, + ids: Optional[List[int]] = None, + start: Optional[str] = None, + end: Optional[str] = None, + limit: Optional[int] = None) -> List[PositionAudit]: + return [ serializers.PositionAudit.parse(*sub_data) \ + for sub_data in self._post("auth/r/positions/audit", \ + body={ "ids": ids, "start": start, "end": end, "limit": limit }) ] - def set_derivative_position_collateral(self, symbol: str, collateral: Union[Decimal, float, str]) -> DerivativePositionCollateral: - return serializers.DerivativePositionCollateral.parse(*(self._post("auth/w/deriv/collateral/set", body={ "symbol": symbol, "collateral": collateral })[0])) + def set_derivative_position_collateral(self, + symbol: str, + collateral: Union[Decimal, float, str]) -> DerivativePositionCollateral: + body = { "symbol": symbol, "collateral": collateral } + response = self._post("auth/w/deriv/collateral/set", body=body) + return serializers.DerivativePositionCollateral.parse(*(response)[0]) def get_derivative_position_collateral_limits(self, symbol: str) -> DerivativePositionCollateralLimits: - return serializers.DerivativePositionCollateralLimits.parse(*self._post("auth/calc/deriv/collateral/limits", body={ "symbol": symbol })) + body = { "symbol": symbol } + response = self._post("auth/calc/deriv/collateral/limit", body=body) + return serializers.DerivativePositionCollateralLimits.parse(*response) - def get_funding_offers(self, symbol: Optional[str] = None) -> List[FundingOffer]: + def get_funding_offers(self, *, symbol: Optional[str] = None) -> List[FundingOffer]: endpoint = "auth/r/funding/offers" - if symbol != None: + if symbol is not None: endpoint += f"/{symbol}" return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._post(endpoint) ] - def submit_funding_offer(self, type: FundingOfferType, symbol: str, amount: Union[Decimal, float, str], - rate: Union[Decimal, float, str], period: int, + #pylint: disable-next=too-many-arguments + def submit_funding_offer(self, + type: FundingOfferType, + symbol: str, + amount: Union[Decimal, float, str], + rate: Union[Decimal, float, str], + period: int, + *, flags: Optional[int] = 0) -> Notification[FundingOffer]: body = { "type": type, "symbol": symbol, "amount": amount, @@ -179,37 +298,52 @@ class RestAuthenticatedEndpoints(Middleware): "flags": flags } - return serializers._Notification[FundingOffer](serializers.FundingOffer).parse(*self._post("auth/w/funding/offer/submit", body=body)) + return _Notification[FundingOffer](serializers.FundingOffer) \ + .parse(*self._post("auth/w/funding/offer/submit", body=body)) def cancel_funding_offer(self, id: int) -> Notification[FundingOffer]: - return serializers._Notification[FundingOffer](serializers.FundingOffer).parse(*self._post("auth/w/funding/offer/cancel", body={ "id": id })) + return _Notification[FundingOffer](serializers.FundingOffer) \ + .parse(*self._post("auth/w/funding/offer/cancel", body={ "id": id })) def cancel_all_funding_offers(self, currency: str) -> Notification[Literal[None]]: - return serializers._Notification[Literal[None]](None).parse( - *self._post("auth/w/funding/offer/cancel/all", body={ "currency": currency }) - ) + return _Notification[Literal[None]](None) \ + .parse(*self._post("auth/w/funding/offer/cancel/all", body={ "currency": currency })) def submit_funding_close(self, id: int) -> Notification[Literal[None]]: - return serializers._Notification[Literal[None]](None).parse( - *self._post("auth/w/funding/close", body={ "id": id }) - ) + return _Notification[Literal[None]](None) \ + .parse(*self._post("auth/w/funding/close", body={ "id": id })) - def toggle_auto_renew(self, status: bool, currency: str, amount: Optional[str] = None, rate: Optional[int] = None, period: Optional[int] = None) -> Notification[FundingAutoRenew]: - return serializers._Notification[FundingAutoRenew](serializers.FundingAutoRenew).parse(*self._post("auth/w/funding/auto", body={ - "status": int(status), - "currency": currency, "amount": amount, - "rate": rate, "period": period - })) + def toggle_auto_renew(self, + status: bool, + currency: str, + *, + amount: Optional[str] = None, + rate: Optional[int] = None, + period: Optional[int] = None) -> Notification[FundingAutoRenew]: + return _Notification[FundingAutoRenew](serializers.FundingAutoRenew) \ + .parse(*self._post("auth/w/funding/auto", body={ + "status": int(status), "currency": currency, "amount": amount, + "rate": rate, "period": period + })) - def toggle_keep_funding(self, type: Literal["credit", "loan"], ids: Optional[List[int]] = None, changes: Optional[Dict[int, Literal[1, 2]]] = None) -> Notification[Literal[None]]: - return serializers._Notification[Literal[None]](None).parse(*self._post("auth/w/funding/keep", body={ + def toggle_keep_funding(self, + type: Literal["credit", "loan"], + *, + ids: Optional[List[int]] = None, + changes: Optional[Dict[int, Literal[1, 2]]] = None) -> Notification[Literal[None]]: + return _Notification[Literal[None]](None).parse(*self._post("auth/w/funding/keep", body={ "type": type, "id": ids, "changes": changes })) - def get_funding_offers_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingOffer]: - if symbol == None: + def get_funding_offers_history(self, + *, + symbol: Optional[str] = None, + start: Optional[str] = None, + end: Optional[str] = None, + limit: Optional[int] = None) -> List[FundingOffer]: + if symbol is None: endpoint = "auth/r/funding/offers/hist" else: endpoint = f"auth/r/funding/offers/{symbol}/hist" @@ -220,18 +354,23 @@ class RestAuthenticatedEndpoints(Middleware): return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._post(endpoint, body=body) ] - def get_funding_loans(self, symbol: Optional[str] = None) -> List[FundingLoan]: - if symbol == None: + def get_funding_loans(self, *, symbol: Optional[str] = None) -> List[FundingLoan]: + if symbol is None: endpoint = "auth/r/funding/loans" else: endpoint = f"auth/r/funding/loans/{symbol}" return [ serializers.FundingLoan.parse(*sub_data) for sub_data in self._post(endpoint) ] - def get_funding_loans_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingLoan]: - if symbol == None: + def get_funding_loans_history(self, + *, + symbol: Optional[str] = None, + start: Optional[str] = None, + end: Optional[str] = None, + limit: Optional[int] = None) -> List[FundingLoan]: + if symbol is None: endpoint = "auth/r/funding/loans/hist" else: endpoint = f"auth/r/funding/loans/{symbol}/hist" - + body = { "start": start, "end": end, "limit": limit @@ -239,18 +378,23 @@ class RestAuthenticatedEndpoints(Middleware): return [ serializers.FundingLoan.parse(*sub_data) for sub_data in self._post(endpoint, body=body) ] - def get_funding_credits(self, symbol: Optional[str] = None) -> List[FundingCredit]: - if symbol == None: + def get_funding_credits(self, *, symbol: Optional[str] = None) -> List[FundingCredit]: + if symbol is None: endpoint = "auth/r/funding/credits" else: endpoint = f"auth/r/funding/credits/{symbol}" return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._post(endpoint) ] - def get_funding_credits_history(self, symbol: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingCredit]: - if symbol == None: + def get_funding_credits_history(self, + *, + symbol: Optional[str] = None, + start: Optional[str] = None, + end: Optional[str] = None, + limit: Optional[int] = None) -> List[FundingCredit]: + if symbol is None: endpoint = "auth/r/funding/credits/hist" else: endpoint = f"auth/r/funding/credits/{symbol}/hist" - + body = { "start": start, "end": end, "limit": limit @@ -258,8 +402,14 @@ class RestAuthenticatedEndpoints(Middleware): return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._post(endpoint, body=body) ] - def get_funding_trades_history(self, symbol: Optional[str] = None, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[FundingTrade]: - if symbol == None: + def get_funding_trades_history(self, + *, + symbol: Optional[str] = None, + sort: Optional[Sort] = None, + start: Optional[str] = None, + end: Optional[str] = None, + limit: Optional[int] = None) -> List[FundingTrade]: + if symbol is None: endpoint = "auth/r/funding/trades/hist" else: endpoint = f"auth/r/funding/trades/{symbol}/hist" @@ -276,46 +426,63 @@ class RestAuthenticatedEndpoints(Middleware): data = [response[1]] + response[2] return serializers.FundingInfo.parse(*data) - def transfer_between_wallets(self, from_wallet: str, to_wallet: str, currency: str, currency_to: str, amount: Union[Decimal, float, str]) -> Notification[Transfer]: + #pylint: disable-next=too-many-arguments + def transfer_between_wallets(self, + from_wallet: str, + to_wallet: str, + currency: str, + currency_to: str, + amount: Union[Decimal, float, str]) -> Notification[Transfer]: body = { "from": from_wallet, "to": to_wallet, "currency": currency, "currency_to": currency_to, "amount": amount } - return serializers._Notification[Transfer](serializers.Transfer).parse(*self._post("auth/w/transfer", body=body)) + return _Notification[Transfer](serializers.Transfer) \ + .parse(*self._post("auth/w/transfer", body=body)) - def submit_wallet_withdrawal(self, wallet: str, method: str, address: str, amount: Union[Decimal, float, str]) -> Notification[Withdrawal]: - return serializers._Notification[Withdrawal](serializers.Withdrawal).parse(*self._post("auth/w/withdraw", body={ - "wallet": wallet, "method": method, - "address": address, "amount": amount, + def submit_wallet_withdrawal(self, + wallet: str, + method: str, + address: str, + amount: Union[Decimal, float, str]) -> Notification[Withdrawal]: + return _Notification[Withdrawal](serializers.Withdrawal) \ + .parse(*self._post("auth/w/withdraw", body={ + "wallet": wallet, "method": method, "address": address, + "amount": amount, + })) + + def get_deposit_address(self, + wallet: str, + method: str, + renew: bool = False) -> Notification[DepositAddress]: + return _Notification[DepositAddress](serializers.DepositAddress) \ + .parse(*self._post("auth/w/deposit/address", body={ + "wallet": wallet, "method": method, "renew": int(renew) + })) + + def generate_deposit_invoice(self, + wallet: str, + currency: str, + amount: Union[Decimal, float, str]) -> LightningNetworkInvoice: + return serializers.LightningNetworkInvoice.parse(*self._post("auth/w/deposit/invoice", body={ + "wallet": wallet, "currency": currency, "amount": amount })) - def get_deposit_address(self, wallet: str, method: str, renew: bool = False) -> Notification[DepositAddress]: - body = { - "wallet": wallet, - "method": method, - "renew": int(renew) - } - - return serializers._Notification[DepositAddress](serializers.DepositAddress).parse(*self._post("auth/w/deposit/address", body=body)) - - def generate_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, float, str]) -> LightningNetworkInvoice: - body = { - "wallet": wallet, "currency": currency, - "amount": amount - } - - return serializers.LightningNetworkInvoice.parse(*self._post("auth/w/deposit/invoice", body=body)) - - def get_movements(self, currency: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Movement]: - if currency == None: + def get_movements(self, + *, + currency: Optional[str] = None, + start: Optional[str] = None, + end: Optional[str] = None, + limit: Optional[int] = None) -> List[Movement]: + if currency is None: endpoint = "auth/r/movements/hist" else: endpoint = f"auth/r/movements/{currency}/hist" - + body = { "start": start, "end": end, "limit": limit } - return [ serializers.Movement.parse(*sub_data) for sub_data in self._post(endpoint, body=body) ] \ No newline at end of file + return [ serializers.Movement.parse(*sub_data) for sub_data in self._post(endpoint, body=body) ] From 844c2526b96ce945ab8b4d53c8dd4d1f571dddc7 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 7 Mar 2023 18:12:32 +0100 Subject: [PATCH 202/265] Apply pylint's linting rules to bfxapi/rest/endpoints/rest_merchant_endpoints.py. --- .../rest/endpoints/rest_merchant_endpoints.py | 137 +++++++++++++----- 1 file changed, 98 insertions(+), 39 deletions(-) diff --git a/bfxapi/rest/endpoints/rest_merchant_endpoints.py b/bfxapi/rest/endpoints/rest_merchant_endpoints.py index d237aab..0c498c3 100644 --- a/bfxapi/rest/endpoints/rest_merchant_endpoints.py +++ b/bfxapi/rest/endpoints/rest_merchant_endpoints.py @@ -1,9 +1,15 @@ -from typing import TypedDict, List, Union, Literal, Optional, Any +#pylint: disable=redefined-builtin + +from typing import TypedDict, Dict, List, Union, Literal, Optional, Any from decimal import Decimal -from .. types import * +from .. types import \ + InvoiceSubmission, InvoicePage, InvoiceStats, \ + CurrencyConversion, MerchantDeposit, MerchantUnlinkedDeposit + from .. enums import MerchantSettingsKey + from .. middleware import Middleware from ...utils.camel_and_snake_case_helpers import to_snake_case_keys, to_camel_case_keys @@ -15,9 +21,17 @@ _CustomerInfo = TypedDict("_CustomerInfo", { }) class RestMerchantEndpoints(Middleware): - def submit_invoice(self, amount: Union[Decimal, float, str], currency: str, order_id: str, - customer_info: _CustomerInfo, pay_currencies: List[str], duration: Optional[int] = None, - webhook: Optional[str] = None, redirect_url: Optional[str] = None) -> InvoiceSubmission: + #pylint: disable-next=too-many-arguments + def submit_invoice(self, + amount: Union[Decimal, float, str], + currency: str, + order_id: str, + customer_info: _CustomerInfo, + pay_currencies: List[str], + *, + duration: Optional[int] = None, + webhook: Optional[str] = None, + redirect_url: Optional[str] = None) -> InvoiceSubmission: body = to_camel_case_keys({ "amount": amount, "currency": currency, "order_id": order_id, "customer_info": customer_info, "pay_currencies": pay_currencies, "duration": duration, @@ -25,18 +39,35 @@ class RestMerchantEndpoints(Middleware): }) data = to_snake_case_keys(self._post("auth/w/ext/pay/invoice/create", body=body)) - + return InvoiceSubmission.parse(data) - def get_invoices(self, id: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[InvoiceSubmission]: - return [ InvoiceSubmission.parse(sub_data) for sub_data in to_snake_case_keys(self._post("auth/r/ext/pay/invoices", body={ + def get_invoices(self, + *, + id: Optional[str] = None, + start: Optional[str] = None, + end: Optional[str] = None, + limit: Optional[int] = None) -> List[InvoiceSubmission]: + body = { "id": id, "start": start, "end": end, "limit": limit - })) ] - - def get_invoices_paginated(self, page: int = 1, page_size: int = 10, sort: Literal["asc", "desc"] = "asc", - sort_field: Literal["t", "amount", "status"] = "t", status: Optional[List[Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"]]] = None, fiat: Optional[List[str]] = None, - crypto: Optional[List[str]] = None, id: Optional[str] = None, order_id: Optional[str] = None) -> InvoicePage: + } + + response = self._post("auth/r/ext/pay/invoices", body=body) + + return [ InvoiceSubmission.parse(sub_data) for sub_data in to_snake_case_keys(response) ] + + def get_invoices_paginated(self, + page: int = 1, + page_size: int = 10, + sort: Literal["asc", "desc"] = "asc", + sort_field: Literal["t", "amount", "status"] = "t", + *, + status: Optional[List[Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"]]] = None, + fiat: Optional[List[str]] = None, + crypto: Optional[List[str]] = None, + id: Optional[str] = None, + order_id: Optional[str] = None) -> InvoicePage: body = to_camel_case_keys({ "page": page, "page_size": page_size, "sort": sort, "sort_field": sort_field, "status": status, "fiat": fiat, @@ -44,60 +75,88 @@ class RestMerchantEndpoints(Middleware): }) data = to_snake_case_keys(self._post("auth/r/ext/pay/invoices/paginated", body=body)) - + return InvoicePage.parse(data) - def get_invoice_count_stats(self, status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"], format: str) -> List[InvoiceStats]: - return [ InvoiceStats(**sub_data) for sub_data in self._post("auth/r/ext/pay/invoice/stats/count", body={ "status": status, "format": format }) ] + def get_invoice_count_stats(self, + status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"], + format: str) -> List[InvoiceStats]: + return [ InvoiceStats(**sub_data) for sub_data in \ + self._post("auth/r/ext/pay/invoice/stats/count", body={ "status": status, "format": format }) ] - def get_invoice_earning_stats(self, currency: str, format: str) -> List[InvoiceStats]: - return [ InvoiceStats(**sub_data) for sub_data in self._post("auth/r/ext/pay/invoice/stats/earning", body={ "currency": currency, "format": format }) ] + def get_invoice_earning_stats(self, + currency: str, + format: str) -> List[InvoiceStats]: + return [ InvoiceStats(**sub_data) for sub_data in \ + self._post("auth/r/ext/pay/invoice/stats/earning", body={ "currency": currency, "format": format }) ] - def complete_invoice(self, id: str, pay_currency: str, deposit_id: Optional[int] = None, ledger_id: Optional[int] = None) -> InvoiceSubmission: + def complete_invoice(self, + id: str, + pay_currency: str, + *, + deposit_id: Optional[int] = None, + ledger_id: Optional[int] = None) -> InvoiceSubmission: return InvoiceSubmission.parse(to_snake_case_keys(self._post("auth/w/ext/pay/invoice/complete", body={ "id": id, "payCcy": pay_currency, "depositId": deposit_id, "ledgerId": ledger_id }))) def expire_invoice(self, id: str) -> InvoiceSubmission: - return InvoiceSubmission.parse(to_snake_case_keys(self._post("auth/w/ext/pay/invoice/expire", body={ "id": id }))) + body = { "id": id } + response = self._post("auth/w/ext/pay/invoice/expire", body=body) + return InvoiceSubmission.parse(to_snake_case_keys(response)) def get_currency_conversion_list(self) -> List[CurrencyConversion]: return [ CurrencyConversion( - base_currency=sub_data["baseCcy"], - convert_currency=sub_data["convertCcy"], + base_currency=sub_data["baseCcy"], + convert_currency=sub_data["convertCcy"], created=sub_data["created"] ) for sub_data in self._post("auth/r/ext/pay/settings/convert/list") ] - def add_currency_conversion(self, base_currency: str, convert_currency: str) -> bool: + def add_currency_conversion(self, + base_currency: str, + convert_currency: str) -> bool: return bool(self._post("auth/w/ext/pay/settings/convert/create", body={ "baseCcy": base_currency, "convertCcy": convert_currency })) - def remove_currency_conversion(self, base_currency: str, convert_currency: str) -> bool: + def remove_currency_conversion(self, + base_currency: str, + convert_currency: str) -> bool: return bool(self._post("auth/w/ext/pay/settings/convert/remove", body={ "baseCcy": base_currency, "convertCcy": convert_currency })) - - def set_merchant_settings(self, key: MerchantSettingsKey, val: Any) -> bool: + + def set_merchant_settings(self, + key: MerchantSettingsKey, + val: Any) -> bool: return bool(self._post("auth/w/ext/pay/settings/set", body={ "key": key, "val": val })) - + def get_merchant_settings(self, key: MerchantSettingsKey) -> Any: return self._post("auth/r/ext/pay/settings/get", body={ "key": key }) - - def list_merchant_settings(self, keys: List[MerchantSettingsKey] = list()) -> Dict[MerchantSettingsKey, Any]: + + def list_merchant_settings(self, keys: List[MerchantSettingsKey] = []) -> Dict[MerchantSettingsKey, Any]: return self._post("auth/r/ext/pay/settings/list", body={ "keys": keys }) - - def get_deposits(self, start: int, end: int, ccy: Optional[str] = None, unlinked: Optional[bool] = None) -> List[MerchantDeposit]: - return [ MerchantDeposit(**sub_data) for sub_data in to_snake_case_keys(self._post("auth/r/ext/pay/deposits", body={ - "from": start, "to": end, "ccy": ccy, "unlinked": unlinked - })) ] - - def get_unlinked_deposits(self, ccy: str, start: Optional[int] = None, end: Optional[int] = None) -> List[MerchantUnlinkedDeposit]: - return [ MerchantUnlinkedDeposit(**sub_data) for sub_data in to_snake_case_keys(self._post("/auth/r/ext/pay/deposits/unlinked", body={ - "ccy": ccy, "start": start, "end": end - })) ] \ No newline at end of file + + def get_deposits(self, + start: int, + end: int, + *, + ccy: Optional[str] = None, + unlinked: Optional[bool] = None) -> List[MerchantDeposit]: + body = { "from": start, "to": end, "ccy": ccy, "unlinked": unlinked } + response = self._post("auth/r/ext/pay/deposits", body=body) + return [ MerchantDeposit(**sub_data) for sub_data in to_snake_case_keys(response) ] + + def get_unlinked_deposits(self, + ccy: str, + *, + start: Optional[int] = None, + end: Optional[int] = None) -> List[MerchantUnlinkedDeposit]: + body = { "ccy": ccy, "start": start, "end": end } + response = self._post("/auth/r/ext/pay/deposits/unlinked", body=body) + return [ MerchantUnlinkedDeposit(**sub_data) for sub_data in to_snake_case_keys(response) ] From 88e6c795321ceb9ba1fd221e294f69f402db8a57 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 7 Mar 2023 18:14:50 +0100 Subject: [PATCH 203/265] Add #pylint: disable=duplicate-code comment in serializers.py and types.py (both rest and websocket). --- bfxapi/rest/serializers.py | 2 ++ bfxapi/rest/types.py | 2 ++ bfxapi/websocket/serializers.py | 2 ++ bfxapi/websocket/types.py | 2 ++ 4 files changed, 8 insertions(+) diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 3a2b673..2238ad6 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -1,3 +1,5 @@ +#pylint: disable=duplicate-code + from . import types from .. labeler import generate_labeler_serializer, generate_recursive_serializer diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 65d5423..6418880 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -1,3 +1,5 @@ +#pylint: disable=duplicate-code + #pylint: disable-next=wildcard-import,unused-wildcard-import from typing import * diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index 2bcea77..e889cdd 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -1,3 +1,5 @@ +#pylint: disable=duplicate-code + from . import types from .. labeler import generate_labeler_serializer diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py index fa0972a..e028f91 100644 --- a/bfxapi/websocket/types.py +++ b/bfxapi/websocket/types.py @@ -1,3 +1,5 @@ +#pylint: disable=duplicate-code + #pylint: disable-next=wildcard-import,unused-wildcard-import from typing import * From 894391256dbcd55c69b9f9bc3bbfd9d530970861 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 8 Mar 2023 16:30:52 +0100 Subject: [PATCH 204/265] Rewrite long strings using \ instead of + (concatenation). --- .pylintrc | 5 ++- bfxapi/labeler.py | 2 +- bfxapi/rest/middleware/middleware.py | 16 +++---- bfxapi/tests/test_labeler.py | 4 +- bfxapi/tests/test_rest_serializers.py | 7 +-- bfxapi/tests/test_websocket_serializers.py | 7 +-- .../websocket/client/bfx_websocket_client.py | 44 +++++++++---------- 7 files changed, 44 insertions(+), 41 deletions(-) diff --git a/.pylintrc b/.pylintrc index 7768472..f63ddd0 100644 --- a/.pylintrc +++ b/.pylintrc @@ -9,12 +9,13 @@ ignore=examples disable= multiple-imports, missing-docstring, + logging-not-lazy, + logging-fstring-interpolation, too-few-public-methods, too-many-public-methods, too-many-instance-attributes, dangerous-default-value, inconsistent-return-statements, - logging-not-lazy [FORMAT] @@ -24,4 +25,4 @@ good-names=id,on,pl,t,ip,tf,A,B,C,D,E,F [TYPECHECK] -generated-members=websockets \ No newline at end of file +generated-members=websockets diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index cac623a..05dcf8e 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -42,7 +42,7 @@ class _Serializer(Generic[T]): labels = list(filter(lambda label: label not in (skip or []), self.__labels)) if len(labels) > len(args): - raise LabelerSerializerException(f"{self.name} -> and <*args> " + + raise LabelerSerializerException(f"{self.name} -> and <*args> " \ "arguments should contain the same amount of elements.") for index, label in enumerate(labels): diff --git a/bfxapi/rest/middleware/middleware.py b/bfxapi/rest/middleware/middleware.py index 04c0f6c..4bfe8b0 100644 --- a/bfxapi/rest/middleware/middleware.py +++ b/bfxapi/rest/middleware/middleware.py @@ -53,12 +53,12 @@ class Middleware: if len(data) and data[0] == "error": if data[1] == Error.ERR_PARAMS: - raise RequestParametersError("The request was rejected with the " - + f"following parameter error: <{data[2]}>") + raise RequestParametersError("The request was rejected with the " \ + f"following parameter error: <{data[2]}>") if data[1] is None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC: - raise UnknownGenericError("The server replied to the request with " - + f"a generic error with message: <{data[2]}>.") + raise UnknownGenericError("The server replied to the request with " \ + f"a generic error with message: <{data[2]}>.") return data @@ -86,14 +86,14 @@ class Middleware: if isinstance(data, list) and len(data) and data[0] == "error": if data[1] == Error.ERR_PARAMS: - raise RequestParametersError("The request was rejected with the " - + f"following parameter error: <{data[2]}>") + raise RequestParametersError("The request was rejected with the " \ + f"following parameter error: <{data[2]}>") if data[1] == Error.ERR_AUTH_FAIL: raise InvalidAuthenticationCredentials("Cannot authenticate with given API-KEY and API-SECRET.") if data[1] is None or data[1] == Error.ERR_UNK or data[1] == Error.ERR_GENERIC: - raise UnknownGenericError("The server replied to the request with " - + f"a generic error with message: <{data[2]}>.") + raise UnknownGenericError("The server replied to the request with " \ + f"a generic error with message: <{data[2]}>.") return data diff --git a/bfxapi/tests/test_labeler.py b/bfxapi/tests/test_labeler.py index 5750d9c..04fe2b9 100644 --- a/bfxapi/tests/test_labeler.py +++ b/bfxapi/tests/test_labeler.py @@ -26,8 +26,8 @@ class TestLabeler(unittest.TestCase): msg="_Serializer::get_labels() should return the right list of labels.") with self.assertRaises(LabelerSerializerException, - msg="_Serializer should raise LabelerSerializerException if given " - + "fewer arguments than the serializer labels."): + msg="_Serializer should raise LabelerSerializerException if given " \ + "fewer arguments than the serializer labels."): serializer.parse(5, 65.0, "X") def test_generate_recursive_serializer(self): diff --git a/bfxapi/tests/test_rest_serializers.py b/bfxapi/tests/test_rest_serializers.py index 0cf43b5..c9c1886 100644 --- a/bfxapi/tests/test_rest_serializers.py +++ b/bfxapi/tests/test_rest_serializers.py @@ -10,11 +10,12 @@ 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}).") + f"_Serializer <{serializer.name}>: .klass field must be a subclass " \ + f"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.") + f"_Serializer <{serializer.name}> and _Type <{serializer.klass.__name__}> " \ + "must have matching labels and fields.") if __name__ == "__main__": unittest.main() diff --git a/bfxapi/tests/test_websocket_serializers.py b/bfxapi/tests/test_websocket_serializers.py index 56708f4..b433868 100644 --- a/bfxapi/tests/test_websocket_serializers.py +++ b/bfxapi/tests/test_websocket_serializers.py @@ -10,11 +10,12 @@ 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}).") + f"_Serializer <{serializer.name}>: .klass field must be a subclass " \ + f"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.") + f"_Serializer <{serializer.name}> and _Type <{serializer.klass.__name__}> " \ + "must have matching labels and fields.") if __name__ == "__main__": unittest.main() diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 88cb853..3f893eb 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -22,8 +22,8 @@ from ...utils.logger import ColorLogger, FileLogger def _require_websocket_authentication(function: F) -> F: async def wrapper(self, *args, **kwargs): if hasattr(self, "authentication") and not self.authentication: - raise WebsocketAuthenticationRequired("To perform this action you need to authenticate " + - "using your API_KEY and API_SECRET.") + 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) @@ -86,9 +86,9 @@ class BfxWebsocketClient: 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} " - + f"buckets from the same connection ({connections} in use), the server could momentarily " - + "block the client with <429 Too Many Requests>.") + self.logger.warning(f"It is not safe to use more than {BfxWebsocketClient.MAXIMUM_CONNECTIONS_AMOUNT} " \ + f"buckets from the same connection ({connections} in use), the server could momentarily " \ + "block the client with <429 Too Many Requests>.") for _ in range(connections): self.on_open_events.append(asyncio.Event()) @@ -113,9 +113,9 @@ class BfxWebsocketClient: async with websockets.connect(self.host) as websocket: if reconnection.status: - 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}).") + 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) @@ -133,9 +133,9 @@ class BfxWebsocketClient: if isinstance(message, dict): if message["event"] == "info" and "version" in message: if BfxWebsocketClient.VERSION != message["version"]: - raise OutdatedClientVersion("Mismatch between the client version and the server " - + "version. Update the library to the latest version to continue (client version: " - + f"{BfxWebsocketClient.VERSION}, server version: {message['version']}).") + raise OutdatedClientVersion("Mismatch between the client version and the server " \ + "version. Update the library to the latest version to continue (client version: " \ + f"{BfxWebsocketClient.VERSION}, server version: {message['version']}).") elif message["event"] == "info" and message["code"] == 20051: rcvd = websockets.frames.Close(code=1012, reason="Stop/Restart Websocket Server (please reconnect).") @@ -165,20 +165,20 @@ class BfxWebsocketClient: except (websockets.ConnectionClosedError, socket.gaierror) as error: if isinstance(error, websockets.ConnectionClosedError) and error.code in (1006, 1012): if error.code == 1006: - self.logger.error("Connection lost: no close frame received " - + "or sent (1006). Attempting to reconnect...") + 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...") + 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: - self.logger.warning(f"Reconnection attempt no.{reconnection.attempts} has failed. " - + f"Next reconnection attempt in ~{round(delay.peek()):.1f} seconds. (at the moment " - + f"the client has been offline for {datetime.now() - reconnection.timestamp})") + self.logger.warning(f"Reconnection attempt no.{reconnection.attempts} has failed. " \ + f"Next reconnection attempt in ~{round(delay.peek()):.1f} seconds. (at the moment " \ + f"the client has been offline for {datetime.now() - reconnection.timestamp})") reconnection = reconnection._replace(attempts=reconnection.attempts + 1) else: raise error @@ -231,8 +231,8 @@ class BfxWebsocketClient: 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") + raise EventNotSupported(f"Event <{event}> is not supported. To get a list " \ + "of available events print BfxWebsocketClient.EVENTS") if callback is not None: for event in events: @@ -248,8 +248,8 @@ class BfxWebsocketClient: 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") + raise EventNotSupported(f"Event <{event}> is not supported. To get a list " \ + "of available events print BfxWebsocketClient.EVENTS") if callback is not None: for event in events: From 0ee8a8ab29e509e13dff7e0838c291e6af947356 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 8 Mar 2023 16:50:16 +0100 Subject: [PATCH 205/265] Edit .pylintrc with new rules. --- .pylintrc | 12 ++++++++++++ .../rest/endpoints/rest_authenticated_endpoints.py | 2 -- bfxapi/rest/endpoints/rest_merchant_endpoints.py | 2 -- bfxapi/rest/endpoints/rest_public_endpoints.py | 2 -- bfxapi/utils/__init__.py | 2 +- bfxapi/websocket/client/bfx_websocket_inputs.py | 2 -- 6 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.pylintrc b/.pylintrc index f63ddd0..0127cfd 100644 --- a/.pylintrc +++ b/.pylintrc @@ -17,12 +17,24 @@ disable= dangerous-default-value, inconsistent-return-statements, +[VARIABLES] + +allowed-redefined-builtins=type,dir,id,all,format,len + [FORMAT] max-line-length=120 +expected-line-ending-format=LF + +[BASIC] + good-names=id,on,pl,t,ip,tf,A,B,C,D,E,F [TYPECHECK] generated-members=websockets + +[STRING] + +check-quote-consistency=yes diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index 97f85e7..a65ac1d 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -1,5 +1,3 @@ -#pylint: disable=redefined-builtin - from typing import Dict, List, Tuple, Union, Literal, Optional from decimal import Decimal from datetime import datetime diff --git a/bfxapi/rest/endpoints/rest_merchant_endpoints.py b/bfxapi/rest/endpoints/rest_merchant_endpoints.py index 0c498c3..bf806af 100644 --- a/bfxapi/rest/endpoints/rest_merchant_endpoints.py +++ b/bfxapi/rest/endpoints/rest_merchant_endpoints.py @@ -1,5 +1,3 @@ -#pylint: disable=redefined-builtin - from typing import TypedDict, Dict, List, Union, Literal, Optional, Any from decimal import Decimal diff --git a/bfxapi/rest/endpoints/rest_public_endpoints.py b/bfxapi/rest/endpoints/rest_public_endpoints.py index 66382f4..a4eee06 100644 --- a/bfxapi/rest/endpoints/rest_public_endpoints.py +++ b/bfxapi/rest/endpoints/rest_public_endpoints.py @@ -1,5 +1,3 @@ -#pylint: disable=redefined-builtin - from typing import List, Union, Literal, Optional, Any, cast from decimal import Decimal diff --git a/bfxapi/utils/__init__.py b/bfxapi/utils/__init__.py index df6e2da..32f9209 100644 --- a/bfxapi/utils/__init__.py +++ b/bfxapi/utils/__init__.py @@ -1 +1 @@ -NAME = "utils" +NAME = "utils" diff --git a/bfxapi/websocket/client/bfx_websocket_inputs.py b/bfxapi/websocket/client/bfx_websocket_inputs.py index 77a1521..f306bcb 100644 --- a/bfxapi/websocket/client/bfx_websocket_inputs.py +++ b/bfxapi/websocket/client/bfx_websocket_inputs.py @@ -1,5 +1,3 @@ -#pylint: disable=redefined-builtin - from decimal import Decimal from datetime import datetime From 87ea7652812feec13a08a31a8cf77df9053c95b3 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 8 Mar 2023 18:32:03 +0100 Subject: [PATCH 206/265] Apply refactoring with pylint to bfxapi.rest.endpoints sub-package. --- bfxapi/labeler.py | 8 +- .../endpoints/rest_authenticated_endpoints.py | 224 ++++++++---------- .../rest/endpoints/rest_public_endpoints.py | 6 +- bfxapi/rest/serializers.py | 1 - bfxapi/rest/types.py | 3 +- bfxapi/tests/test_labeler.py | 6 +- 6 files changed, 118 insertions(+), 130 deletions(-) diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 05dcf8e..88b6d9f 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -39,7 +39,10 @@ class _Serializer(Generic[T]): self.name, self.klass, self.__labels, self.__ignore = name, klass, labels, ignore def _serialize(self, *args: Any, skip: Optional[List[str]] = None) -> Iterable[Tuple[str, Any]]: - labels = list(filter(lambda label: label not in (skip or []), self.__labels)) + labels, skips = [], [] + + for label in self.__labels: + (labels, skips)[label in (skip or [])].append(label) if len(labels) > len(args): raise LabelerSerializerException(f"{self.name} -> and <*args> " \ @@ -49,6 +52,9 @@ class _Serializer(Generic[T]): if label not in self.__ignore: yield label, args[index] + for skip in skips: + yield skip, None + def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: return cast(T, self.klass(**dict(self._serialize(*values, skip=skip)))) diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index a65ac1d..a78858e 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -24,10 +24,11 @@ from ...utils.json_encoder import JSON class RestAuthenticatedEndpoints(Middleware): def get_user_info(self) -> UserInfo: - return serializers.UserInfo.parse(*self._post("auth/r/info/user")) + return serializers.UserInfo \ + .parse(*self._post("auth/r/info/user")) def get_login_history(self) -> List[LoginHistory]: - return [ serializers.LoginHistory.parse(*sub_data) \ + return [ serializers.LoginHistory.parse(*sub_data) for sub_data in self._post("auth/r/logins/hist") ] def get_balance_available_for_orders_or_offers(self, @@ -37,10 +38,13 @@ class RestAuthenticatedEndpoints(Middleware): dir: Optional[int] = None, rate: Optional[str] = None, lev: Optional[str] = None) -> BalanceAvailable: - return serializers.BalanceAvailable.parse(*self._post("auth/calc/order/avail", body={ + body = { "symbol": symbol, "type": type, "dir": dir, "rate": rate, "lev": lev - })) + } + + return serializers.BalanceAvailable \ + .parse(*self._post("auth/calc/order/avail", body=body)) def get_wallets(self) -> List[Wallet]: return [ serializers.Wallet.parse(*sub_data) \ @@ -50,10 +54,9 @@ class RestAuthenticatedEndpoints(Middleware): *, symbol: Optional[str] = None, ids: Optional[List[str]] = None) -> List[Order]: - endpoint = "auth/r/orders" - - if symbol is not None: - endpoint += f"/{symbol}" + if symbol is None: + endpoint = "auth/r/orders" + else: endpoint = f"auth/r/orders/{symbol}" return [ serializers.Order.parse(*sub_data) \ for sub_data in self._post(endpoint, body={ "id": ids }) ] @@ -75,13 +78,14 @@ class RestAuthenticatedEndpoints(Middleware): meta: Optional[JSON] = None) -> Notification[Order]: body = { "type": type, "symbol": symbol, "amount": amount, - "price": price, "lev": lev, - "price_trailing": price_trailing, "price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop, - "gid": gid, "cid": cid, - "flags": flags, "tif": tif, "meta": meta + "price": price, "lev": lev, "price_trailing": price_trailing, + "price_aux_limit": price_aux_limit, "price_oco_stop": price_oco_stop, "gid": gid, + "cid": cid, "flags": flags, "tif": tif, + "meta": meta } - return _Notification[Order](serializers.Order).parse(*self._post("auth/w/order/submit", body=body)) + return _Notification[Order](serializers.Order) \ + .parse(*self._post("auth/w/order/submit", body=body)) def update_order(self, id: int, @@ -112,14 +116,9 @@ class RestAuthenticatedEndpoints(Middleware): id: Optional[int] = None, cid: Optional[int] = None, cid_date: Optional[str] = None) -> Notification[Order]: - body = { - "id": id, - "cid": cid, - "cid_date": cid_date - } - return _Notification[Order](serializers.Order) \ - .parse(*self._post("auth/w/order/cancel", body=body)) + .parse(*self._post("auth/w/order/cancel", \ + body={ "id": id, "cid": cid, "cid_date": cid_date })) def cancel_order_multi(self, *, @@ -128,10 +127,7 @@ class RestAuthenticatedEndpoints(Middleware): gids: Optional[List[int]] = None, all: bool = False) -> Notification[List[Order]]: body = { - "ids": ids, - "cids": cids, - "gids": gids, - + "ids": ids, "cids": cids, "gids": gids, "all": int(all) } @@ -149,13 +145,13 @@ class RestAuthenticatedEndpoints(Middleware): endpoint = "auth/r/orders/hist" else: endpoint = f"auth/r/orders/{symbol}/hist" - body = { - "id": ids, - "start": start, "end": end, - "limit": limit + body = { + "id": ids, "start": start, "end": end, + "limit": limit } - return [ serializers.Order.parse(*sub_data) for sub_data in self._post(endpoint, body=body) ] + return [ serializers.Order.parse(*sub_data) \ + for sub_data in self._post(endpoint, body=body) ] def get_order_trades(self, symbol: str, @@ -175,12 +171,12 @@ class RestAuthenticatedEndpoints(Middleware): else: endpoint = f"auth/r/trades/{symbol}/hist" body = { - "sort": sort, - "start": start, "end": end, + "sort": sort, "start": start, "end": end, "limit": limit } - return [ serializers.Trade.parse(*sub_data) for sub_data in self._post(endpoint, body=body) ] + return [ serializers.Trade.parse(*sub_data) \ + for sub_data in self._post(endpoint, body=body) ] def get_ledgers(self, currency: str, @@ -189,49 +185,45 @@ class RestAuthenticatedEndpoints(Middleware): start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Ledger]: - body = { - "category": category, - "start": start, "end": end, - "limit": limit + body = { + "category": category, "start": start, "end": end, + "limit": limit } return [ serializers.Ledger.parse(*sub_data) \ for sub_data in self._post(f"auth/r/ledgers/{currency}/hist", body=body) ] def get_base_margin_info(self) -> BaseMarginInfo: - return serializers.BaseMarginInfo.parse(*(self._post("auth/r/info/margin/base")[1])) + return serializers.BaseMarginInfo \ + .parse(*(self._post("auth/r/info/margin/base")[1])) def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo: - response = self._post(f"auth/r/info/margin/{symbol}") - data = [response[1]] + response[2] - return serializers.SymbolMarginInfo.parse(*data) + return serializers.SymbolMarginInfo \ + .parse(*(self._post(f"auth/r/info/margin/{symbol}")[2]), \ + skip=["symbol"]) def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]: return [ serializers.SymbolMarginInfo.parse(*([sub_data[1]] + sub_data[2])) \ for sub_data in self._post("auth/r/info/margin/sym_all") ] def get_positions(self) -> List[Position]: - return [ serializers.Position.parse(*sub_data) for sub_data in self._post("auth/r/positions") ] + return [ serializers.Position.parse(*sub_data) \ + for sub_data in self._post("auth/r/positions") ] def claim_position(self, id: int, *, amount: Optional[Union[Decimal, float, str]] = None) -> Notification[PositionClaim]: return _Notification[PositionClaim](serializers.PositionClaim) \ - .parse(*self._post("auth/w/position/claim", body={ "id": id, "amount": amount })) + .parse(*self._post("auth/w/position/claim", \ + body={ "id": id, "amount": amount })) def increase_position(self, symbol: str, amount: Union[Decimal, float, str]) -> Notification[PositionIncrease]: return _Notification[PositionIncrease](serializers.PositionIncrease) \ - .parse(*self._post("auth/w/position/increase", body={ "symbol": symbol, "amount": amount })) - - def get_increase_position_info(self, - symbol: str, - amount: Union[Decimal, float, str]) -> PositionIncreaseInfo: - response = self._post("auth/r/position/increase/info", body={ "symbol": symbol, "amount": amount }) - data = response[0] + [response[1][0]] + response[1][1] + [response[1][2]] + response[4] + response[5] - return serializers.PositionIncreaseInfo.parse(*data) + .parse(*self._post("auth/w/position/increase", \ + body={ "symbol": symbol, "amount": amount })) def get_positions_history(self, *, @@ -257,29 +249,32 @@ class RestAuthenticatedEndpoints(Middleware): start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionAudit]: + body = { + "ids": ids, "start": start, "end": end, + "limit": limit + } + return [ serializers.PositionAudit.parse(*sub_data) \ - for sub_data in self._post("auth/r/positions/audit", \ - body={ "ids": ids, "start": start, "end": end, "limit": limit }) ] + for sub_data in self._post("auth/r/positions/audit", body=body) ] def set_derivative_position_collateral(self, symbol: str, collateral: Union[Decimal, float, str]) -> DerivativePositionCollateral: - body = { "symbol": symbol, "collateral": collateral } - response = self._post("auth/w/deriv/collateral/set", body=body) - return serializers.DerivativePositionCollateral.parse(*(response)[0]) + return serializers.DerivativePositionCollateral \ + .parse(*(self._post("auth/w/deriv/collateral/set", \ + body={ "symbol": symbol, "collateral": collateral })[0])) def get_derivative_position_collateral_limits(self, symbol: str) -> DerivativePositionCollateralLimits: - body = { "symbol": symbol } - response = self._post("auth/calc/deriv/collateral/limit", body=body) - return serializers.DerivativePositionCollateralLimits.parse(*response) + return serializers.DerivativePositionCollateralLimits \ + .parse(*self._post("auth/calc/deriv/collateral/limit", body={ "symbol": symbol })) def get_funding_offers(self, *, symbol: Optional[str] = None) -> List[FundingOffer]: - endpoint = "auth/r/funding/offers" + if symbol is None: + endpoint = "auth/r/funding/offers" + else: endpoint = f"auth/r/funding/offers/{symbol}" - if symbol is not None: - endpoint += f"/{symbol}" - - return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._post(endpoint) ] + return [ serializers.FundingOffer.parse(*sub_data) \ + for sub_data in self._post(endpoint) ] #pylint: disable-next=too-many-arguments def submit_funding_offer(self, @@ -292,8 +287,7 @@ class RestAuthenticatedEndpoints(Middleware): flags: Optional[int] = 0) -> Notification[FundingOffer]: body = { "type": type, "symbol": symbol, "amount": amount, - "rate": rate, "period": period, - "flags": flags + "rate": rate, "period": period, "flags": flags } return _Notification[FundingOffer](serializers.FundingOffer) \ @@ -318,22 +312,22 @@ class RestAuthenticatedEndpoints(Middleware): amount: Optional[str] = None, rate: Optional[int] = None, period: Optional[int] = None) -> Notification[FundingAutoRenew]: + body = { + "status": int(status), "currency": currency, "amount": amount, + "rate": rate, "period": period + } + return _Notification[FundingAutoRenew](serializers.FundingAutoRenew) \ - .parse(*self._post("auth/w/funding/auto", body={ - "status": int(status), "currency": currency, "amount": amount, - "rate": rate, "period": period - })) + .parse(*self._post("auth/w/funding/auto", body=body)) def toggle_keep_funding(self, type: Literal["credit", "loan"], *, ids: Optional[List[int]] = None, changes: Optional[Dict[int, Literal[1, 2]]] = None) -> Notification[Literal[None]]: - return _Notification[Literal[None]](None).parse(*self._post("auth/w/funding/keep", body={ - "type": type, - "id": ids, - "changes": changes - })) + return _Notification[Literal[None]](None) \ + .parse(*self._post("auth/w/funding/keep", \ + body={ "type": type, "id": ids, "changes": changes })) def get_funding_offers_history(self, *, @@ -345,19 +339,17 @@ class RestAuthenticatedEndpoints(Middleware): endpoint = "auth/r/funding/offers/hist" else: endpoint = f"auth/r/funding/offers/{symbol}/hist" - body = { - "start": start, "end": end, - "limit": limit - } - - return [ serializers.FundingOffer.parse(*sub_data) for sub_data in self._post(endpoint, body=body) ] + return [ serializers.FundingOffer.parse(*sub_data) \ + for sub_data in self._post(endpoint, \ + body={ "start": start, "end": end, "limit": limit }) ] def get_funding_loans(self, *, symbol: Optional[str] = None) -> List[FundingLoan]: if symbol is None: endpoint = "auth/r/funding/loans" else: endpoint = f"auth/r/funding/loans/{symbol}" - return [ serializers.FundingLoan.parse(*sub_data) for sub_data in self._post(endpoint) ] + return [ serializers.FundingLoan.parse(*sub_data) \ + for sub_data in self._post(endpoint) ] def get_funding_loans_history(self, *, @@ -369,19 +361,17 @@ class RestAuthenticatedEndpoints(Middleware): endpoint = "auth/r/funding/loans/hist" else: endpoint = f"auth/r/funding/loans/{symbol}/hist" - body = { - "start": start, "end": end, - "limit": limit - } - - return [ serializers.FundingLoan.parse(*sub_data) for sub_data in self._post(endpoint, body=body) ] + return [ serializers.FundingLoan.parse(*sub_data) \ + for sub_data in self._post(endpoint, \ + body={ "start": start, "end": end, "limit": limit }) ] def get_funding_credits(self, *, symbol: Optional[str] = None) -> List[FundingCredit]: if symbol is None: endpoint = "auth/r/funding/credits" else: endpoint = f"auth/r/funding/credits/{symbol}" - return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._post(endpoint) ] + return [ serializers.FundingCredit.parse(*sub_data) \ + for sub_data in self._post(endpoint) ] def get_funding_credits_history(self, *, @@ -393,12 +383,9 @@ class RestAuthenticatedEndpoints(Middleware): endpoint = "auth/r/funding/credits/hist" else: endpoint = f"auth/r/funding/credits/{symbol}/hist" - body = { - "start": start, "end": end, - "limit": limit - } - - return [ serializers.FundingCredit.parse(*sub_data) for sub_data in self._post(endpoint, body=body) ] + return [ serializers.FundingCredit.parse(*sub_data) \ + for sub_data in self._post(endpoint, \ + body={ "start": start, "end": end, "limit": limit }) ] def get_funding_trades_history(self, *, @@ -412,17 +399,15 @@ class RestAuthenticatedEndpoints(Middleware): else: endpoint = f"auth/r/funding/trades/{symbol}/hist" body = { - "sort": sort, - "start": start, "end": end, - "limit": limit - } + "sort": sort, "start": start, "end": end, + "limit": limit } - return [ serializers.FundingTrade.parse(*sub_data) for sub_data in self._post(endpoint, body=body) ] + return [ serializers.FundingTrade.parse(*sub_data) \ + for sub_data in self._post(endpoint, body=body) ] def get_funding_info(self, key: str) -> FundingInfo: - response = self._post(f"auth/r/info/funding/{key}") - data = [response[1]] + response[2] - return serializers.FundingInfo.parse(*data) + return serializers.FundingInfo \ + .parse(*(self._post(f"auth/r/info/funding/{key}")[2])) #pylint: disable-next=too-many-arguments def transfer_between_wallets(self, @@ -432,9 +417,8 @@ class RestAuthenticatedEndpoints(Middleware): currency_to: str, amount: Union[Decimal, float, str]) -> Notification[Transfer]: body = { - "from": from_wallet, "to": to_wallet, - "currency": currency, "currency_to": currency_to, - "amount": amount + "from": from_wallet, "to": to_wallet, "currency": currency, + "currency_to": currency_to, "amount": amount } return _Notification[Transfer](serializers.Transfer) \ @@ -445,28 +429,29 @@ class RestAuthenticatedEndpoints(Middleware): method: str, address: str, amount: Union[Decimal, float, str]) -> Notification[Withdrawal]: + body = { + "wallet": wallet, "method": method, "address": address, + "amount": amount + } + return _Notification[Withdrawal](serializers.Withdrawal) \ - .parse(*self._post("auth/w/withdraw", body={ - "wallet": wallet, "method": method, "address": address, - "amount": amount, - })) + .parse(*self._post("auth/w/withdraw", body=body)) def get_deposit_address(self, wallet: str, method: str, renew: bool = False) -> Notification[DepositAddress]: return _Notification[DepositAddress](serializers.DepositAddress) \ - .parse(*self._post("auth/w/deposit/address", body={ - "wallet": wallet, "method": method, "renew": int(renew) - })) + .parse(*self._post("auth/w/deposit/address", \ + body={ "wallet": wallet, "method": method, "renew": int(renew) })) def generate_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, float, str]) -> LightningNetworkInvoice: - return serializers.LightningNetworkInvoice.parse(*self._post("auth/w/deposit/invoice", body={ - "wallet": wallet, "currency": currency, "amount": amount - })) + return serializers.LightningNetworkInvoice \ + .parse(*self._post("auth/w/deposit/invoice", \ + body={ "wallet": wallet, "currency": currency, "amount": amount })) def get_movements(self, *, @@ -478,9 +463,6 @@ class RestAuthenticatedEndpoints(Middleware): endpoint = "auth/r/movements/hist" else: endpoint = f"auth/r/movements/{currency}/hist" - body = { - "start": start, "end": end, - "limit": limit - } - - return [ serializers.Movement.parse(*sub_data) for sub_data in self._post(endpoint, body=body) ] + return [ serializers.Movement.parse(*sub_data) \ + for sub_data in self._post(endpoint, \ + body={ "start": start, "end": end, "limit": limit }) ] diff --git a/bfxapi/rest/endpoints/rest_public_endpoints.py b/bfxapi/rest/endpoints/rest_public_endpoints.py index a4eee06..1b76da3 100644 --- a/bfxapi/rest/endpoints/rest_public_endpoints.py +++ b/bfxapi/rest/endpoints/rest_public_endpoints.py @@ -49,10 +49,10 @@ class RestPublicEndpoints(Middleware): return cast(List[FundingCurrencyTicker], data) def get_t_ticker(self, pair: str) -> TradingPairTicker: - return serializers.TradingPairTicker.parse(*self._get(f"ticker/t{pair}"), skip=["SYMBOL"]) + return serializers.TradingPairTicker.parse(*self._get(f"ticker/{pair}"), skip=["symbol"]) def get_f_ticker(self, currency: str) -> FundingCurrencyTicker: - return serializers.FundingCurrencyTicker.parse(*self._get(f"ticker/f{currency}"), skip=["SYMBOL"]) + return serializers.FundingCurrencyTicker.parse(*self._get(f"ticker/{currency}"), skip=["symbol"]) def get_tickers_history(self, symbols: List[str], @@ -183,7 +183,7 @@ class RestPublicEndpoints(Middleware): limit: Optional[int] = None) -> List[DerivativesStatus]: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self._get(f"status/{type}/{symbol}/hist", params=params) - return [ serializers.DerivativesStatus.parse(*sub_data, skip=[ "KEY" ]) for sub_data in data ] + return [ serializers.DerivativesStatus.parse(*sub_data, skip=[ "key" ]) for sub_data in data ] def get_liquidations(self, *, diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 2238ad6..cee664d 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -676,7 +676,6 @@ FundingInfo = generate_labeler_serializer( name="FundingInfo", klass=types.FundingInfo, labels=[ - "symbol", "yield_loan", "yield_lend", "duration_loan", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 6418880..f8988ea 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -404,7 +404,6 @@ class FundingAutoRenew(_Type): @dataclass() class FundingInfo(_Type): - symbol: str yield_loan: float yield_lend: float duration_loan: float @@ -467,7 +466,7 @@ class Movement(_Type): @dataclass class SymbolMarginInfo(_Type): - symbol: str + symbol: Optional[str] tradable_balance: float gross_balance: float buy: float diff --git a/bfxapi/tests/test_labeler.py b/bfxapi/tests/test_labeler.py index 04fe2b9..cb88528 100644 --- a/bfxapi/tests/test_labeler.py +++ b/bfxapi/tests/test_labeler.py @@ -1,5 +1,7 @@ import unittest +from typing import Optional + from dataclasses import dataclass from ..exceptions import LabelerSerializerException from ..labeler import _Type, generate_labeler_serializer, generate_recursive_serializer @@ -8,7 +10,7 @@ class TestLabeler(unittest.TestCase): def test_generate_labeler_serializer(self): @dataclass class Test(_Type): - A: int + A: Optional[int] B: float C: str @@ -19,7 +21,7 @@ class TestLabeler(unittest.TestCase): self.assertEqual(serializer.parse(5, None, 65.0, None, "X"), Test(5, 65.0, "X"), msg="_Serializer should produce the right result.") - self.assertEqual(serializer.parse(5, 65.0, "X", skip=[ "_PLACEHOLDER" ]), Test(5, 65.0, "X"), + self.assertEqual(serializer.parse(None, 65.0, None, "X", skip=[ "A" ]), Test(None, 65.0, "X"), msg="_Serializer should produce the right result when skip parameter is given.") self.assertListEqual(serializer.get_labels(), [ "A", "B", "C" ], From bd09cc4ae499573cf6ed2a6b25a28b4fa1333c2e Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 8 Mar 2023 19:31:48 +0100 Subject: [PATCH 207/265] Remove skip parameter in _Serializer::parse, add flat argument in _Serializer::__init__. Add _Serializer::__flatten class method. Fix small bugs in bfxapi.rest.endpoints.rest_public_endpoints and bfxapi.rest.endpoints.rest_authenticated_endpoints. --- .pylintrc | 4 ++ bfxapi/labeler.py | 60 +++++++++++-------- bfxapi/notification.py | 6 +- .../endpoints/rest_authenticated_endpoints.py | 18 ++++-- .../rest/endpoints/rest_public_endpoints.py | 6 +- bfxapi/rest/serializers.py | 11 +++- bfxapi/rest/types.py | 8 +-- bfxapi/tests/test_labeler.py | 3 - 8 files changed, 70 insertions(+), 46 deletions(-) diff --git a/.pylintrc b/.pylintrc index 0127cfd..2b14e62 100644 --- a/.pylintrc +++ b/.pylintrc @@ -17,6 +17,10 @@ disable= dangerous-default-value, inconsistent-return-statements, +[SIMILARITIES] + +min-similarity-lines=6 + [VARIABLES] allowed-redefined-builtins=type,dir,id,all,format,len diff --git a/bfxapi/labeler.py b/bfxapi/labeler.py index 88b6d9f..dfb4881 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/labeler.py @@ -1,4 +1,4 @@ -from typing import Type, Generic, TypeVar, Iterable, Optional, Dict, List, Tuple, Any, cast +from typing import Type, Generic, TypeVar, Iterable, Dict, List, Tuple, Any, cast from .exceptions import LabelerSerializerException @@ -35,53 +35,63 @@ class _Type: class _Serializer(Generic[T]): def __init__(self, name: str, klass: Type[_Type], labels: List[str], - *, ignore: List[str] = [ "_PLACEHOLDER" ]): - self.name, self.klass, self.__labels, self.__ignore = name, klass, labels, ignore + *, flat: bool = False, ignore: List[str] = [ "_PLACEHOLDER" ]): + self.name, self.klass, self.__labels, self.__flat, self.__ignore = name, klass, labels, flat, ignore - def _serialize(self, *args: Any, skip: Optional[List[str]] = None) -> Iterable[Tuple[str, Any]]: - labels, skips = [], [] + def _serialize(self, *args: Any) -> Iterable[Tuple[str, Any]]: + if self.__flat: + args = tuple(_Serializer.__flatten(list(args))) - for label in self.__labels: - (labels, skips)[label in (skip or [])].append(label) - - if len(labels) > len(args): + if len(self.__labels) > len(args): raise LabelerSerializerException(f"{self.name} -> and <*args> " \ "arguments should contain the same amount of elements.") - for index, label in enumerate(labels): + for index, label in enumerate(self.__labels): if label not in self.__ignore: yield label, args[index] - for skip in skips: - yield skip, None - - def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: - return cast(T, self.klass(**dict(self._serialize(*values, skip=skip)))) + def parse(self, *values: Any) -> T: + return cast(T, self.klass(**dict(self._serialize(*values)))) def get_labels(self) -> List[str]: return [ label for label in self.__labels if label not in self.__ignore ] + @classmethod + def __flatten(cls, array: List[Any]) -> List[Any]: + if len(array) == 0: + return array + + if isinstance(array[0], list): + return cls.__flatten(array[0]) + cls.__flatten(array[1:]) + + return array[:1] + cls.__flatten(array[1:]) + class _RecursiveSerializer(_Serializer, Generic[T]): def __init__(self, name: str, klass: Type[_Type], labels: List[str], - *, serializers: Dict[str, _Serializer[Any]], ignore: List[str] = ["_PLACEHOLDER"]): - super().__init__(name, klass, labels, ignore = ignore) + *, serializers: Dict[str, _Serializer[Any]], + flat: bool = False, ignore: List[str] = [ "_PLACEHOLDER" ]): + super().__init__(name, klass, labels, flat=flat, ignore=ignore) self.serializers = serializers - def parse(self, *values: Any, skip: Optional[List[str]] = None) -> T: - serialization = dict(self._serialize(*values, skip=skip)) + def parse(self, *values: Any) -> T: + serialization = dict(self._serialize(*values)) for key in serialization: if key in self.serializers.keys(): - serialization[key] = self.serializers[key].parse(*serialization[key], skip=skip) + serialization[key] = self.serializers[key].parse(*serialization[key]) return cast(T, self.klass(**serialization)) def generate_labeler_serializer(name: str, klass: Type[T], labels: List[str], - *, ignore: List[str] = [ "_PLACEHOLDER" ]) -> _Serializer[T]: - return _Serializer[T](name, klass, labels, ignore=ignore) + *, flat: bool = False, ignore: List[str] = [ "_PLACEHOLDER" ] + ) -> _Serializer[T]: + return _Serializer[T](name, klass, labels, \ + flat=flat, ignore=ignore) def generate_recursive_serializer(name: str, klass: Type[T], labels: List[str], - *, serializers: Dict[str, _Serializer[Any]], ignore: List[str] = [ "_PLACEHOLDER" ] - ) -> _RecursiveSerializer[T]: - return _RecursiveSerializer[T](name, klass, labels, serializers=serializers, ignore=ignore) + *, serializers: Dict[str, _Serializer[Any]], + flat: bool = False, ignore: List[str] = [ "_PLACEHOLDER" ] + ) -> _RecursiveSerializer[T]: + return _RecursiveSerializer[T](name, klass, labels, \ + serializers=serializers, flat=flat, ignore=ignore) diff --git a/bfxapi/notification.py b/bfxapi/notification.py index 601b3f8..ae02259 100644 --- a/bfxapi/notification.py +++ b/bfxapi/notification.py @@ -22,7 +22,7 @@ class _Notification(_Serializer, Generic[T]): self.serializer, self.is_iterable = serializer, is_iterable - def parse(self, *values: Any, skip: Optional[List[str]] = None) -> Notification[T]: + def parse(self, *values: Any) -> Notification[T]: notification = cast(Notification[T], Notification(**dict(self._serialize(*values)))) if isinstance(self.serializer, _Serializer): @@ -32,7 +32,7 @@ class _Notification(_Serializer, Generic[T]): if len(data) == 1 and isinstance(data[0], list): data = data[0] - notification.data = self.serializer.parse(*data, skip=skip) - else: notification.data = cast(T, [ self.serializer.parse(*sub_data, skip=skip) for sub_data in data ]) + notification.data = self.serializer.parse(*data) + else: notification.data = cast(T, [ self.serializer.parse(*sub_data) for sub_data in data ]) return notification diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index a78858e..8a2e61e 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -145,7 +145,7 @@ class RestAuthenticatedEndpoints(Middleware): endpoint = "auth/r/orders/hist" else: endpoint = f"auth/r/orders/{symbol}/hist" - body = { + body = { "id": ids, "start": start, "end": end, "limit": limit } @@ -185,7 +185,7 @@ class RestAuthenticatedEndpoints(Middleware): start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Ledger]: - body = { + body = { "category": category, "start": start, "end": end, "limit": limit } @@ -199,11 +199,10 @@ class RestAuthenticatedEndpoints(Middleware): def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo: return serializers.SymbolMarginInfo \ - .parse(*(self._post(f"auth/r/info/margin/{symbol}")[2]), \ - skip=["symbol"]) + .parse(*self._post(f"auth/r/info/margin/{symbol}")) def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]: - return [ serializers.SymbolMarginInfo.parse(*([sub_data[1]] + sub_data[2])) \ + return [ serializers.SymbolMarginInfo.parse(*sub_data) \ for sub_data in self._post("auth/r/info/margin/sym_all") ] def get_positions(self) -> List[Position]: @@ -225,6 +224,13 @@ class RestAuthenticatedEndpoints(Middleware): .parse(*self._post("auth/w/position/increase", \ body={ "symbol": symbol, "amount": amount })) + def get_increase_position_info(self, + symbol: str, + amount: Union[Decimal, float, str]) -> PositionIncreaseInfo: + return serializers.PositionIncreaseInfo \ + .parse(*self._post("auth/r/position/increase/info", \ + body={ "symbol": symbol, "amount": amount })) + def get_positions_history(self, *, start: Optional[str] = None, @@ -249,7 +255,7 @@ class RestAuthenticatedEndpoints(Middleware): start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[PositionAudit]: - body = { + body = { "ids": ids, "start": start, "end": end, "limit": limit } diff --git a/bfxapi/rest/endpoints/rest_public_endpoints.py b/bfxapi/rest/endpoints/rest_public_endpoints.py index 1b76da3..ac494b6 100644 --- a/bfxapi/rest/endpoints/rest_public_endpoints.py +++ b/bfxapi/rest/endpoints/rest_public_endpoints.py @@ -49,10 +49,10 @@ class RestPublicEndpoints(Middleware): return cast(List[FundingCurrencyTicker], data) def get_t_ticker(self, pair: str) -> TradingPairTicker: - return serializers.TradingPairTicker.parse(*self._get(f"ticker/{pair}"), skip=["symbol"]) + return serializers.TradingPairTicker.parse(*([pair] + self._get(f"ticker/{pair}"))) def get_f_ticker(self, currency: str) -> FundingCurrencyTicker: - return serializers.FundingCurrencyTicker.parse(*self._get(f"ticker/{currency}"), skip=["symbol"]) + return serializers.FundingCurrencyTicker.parse(*([currency] + self._get(f"ticker/{currency}"))) def get_tickers_history(self, symbols: List[str], @@ -183,7 +183,7 @@ class RestPublicEndpoints(Middleware): limit: Optional[int] = None) -> List[DerivativesStatus]: params = { "sort": sort, "start": start, "end": end, "limit": limit } data = self._get(f"status/{type}/{symbol}/hist", params=params) - return [ serializers.DerivativesStatus.parse(*sub_data, skip=[ "key" ]) for sub_data in data ] + return [ serializers.DerivativesStatus.parse(*([symbol] + sub_data)) for sub_data in data ] def get_liquidations(self, *, diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index cee664d..1fd4684 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -786,12 +786,15 @@ SymbolMarginInfo = generate_labeler_serializer( name="SymbolMarginInfo", klass=types.SymbolMarginInfo, labels=[ + "_PLACEHOLDER", "symbol", "tradable_balance", "gross_balance", "buy", "sell" - ] + ], + + flat=True ) BaseMarginInfo = generate_labeler_serializer( @@ -849,11 +852,15 @@ PositionIncreaseInfo = generate_labeler_serializer( "_PLACEHOLDER", "_PLACEHOLDER", "funding_avail", + "_PLACEHOLDER", + "_PLACEHOLDER", "funding_value", "funding_required", "funding_value_currency", "funding_required_currency" - ] + ], + + flat=True ) PositionIncrease = generate_labeler_serializer( diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index f8988ea..34fdb46 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -20,7 +20,7 @@ class PlatformStatus(_Type): @dataclass class TradingPairTicker(_Type): - symbol: Optional[str] + symbol: str bid: float bid_size: float ask: float @@ -34,7 +34,7 @@ class TradingPairTicker(_Type): @dataclass class FundingCurrencyTicker(_Type): - symbol: Optional[str] + symbol: str frr: float bid: float bid_period: int @@ -114,7 +114,7 @@ class Candle(_Type): @dataclass class DerivativesStatus(_Type): - key: Optional[str] + key: str mts: int deriv_price: float spot_price: float @@ -466,7 +466,7 @@ class Movement(_Type): @dataclass class SymbolMarginInfo(_Type): - symbol: Optional[str] + symbol: str tradable_balance: float gross_balance: float buy: float diff --git a/bfxapi/tests/test_labeler.py b/bfxapi/tests/test_labeler.py index cb88528..c375798 100644 --- a/bfxapi/tests/test_labeler.py +++ b/bfxapi/tests/test_labeler.py @@ -21,9 +21,6 @@ class TestLabeler(unittest.TestCase): self.assertEqual(serializer.parse(5, None, 65.0, None, "X"), Test(5, 65.0, "X"), msg="_Serializer should produce the right result.") - self.assertEqual(serializer.parse(None, 65.0, None, "X", skip=[ "A" ]), Test(None, 65.0, "X"), - msg="_Serializer should produce the right result when skip parameter is given.") - self.assertListEqual(serializer.get_labels(), [ "A", "B", "C" ], msg="_Serializer::get_labels() should return the right list of labels.") From 88c016e1f8cec7ff6affce103836f655a9f2a72d Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 8 Mar 2023 19:39:00 +0100 Subject: [PATCH 208/265] Apply pylint's linting rules to examples/rest/authenticated/*.py. --- examples/rest/authenticated/claim_position.py | 2 +- examples/rest/authenticated/get_wallets.py | 6 +++--- .../set_derivative_position_collateral.py | 4 ++-- examples/rest/authenticated/submit_funding_offer.py | 12 ++++++------ examples/rest/authenticated/submit_order.py | 10 +++++----- examples/rest/authenticated/toggle_keep_funding.py | 4 ++-- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/examples/rest/authenticated/claim_position.py b/examples/rest/authenticated/claim_position.py index 0f8b385..5dbe89a 100644 --- a/examples/rest/authenticated/claim_position.py +++ b/examples/rest/authenticated/claim_position.py @@ -16,4 +16,4 @@ bfx = Client( for position in bfx.rest.auth.get_positions(): notification: Notification[PositionClaim] = bfx.rest.auth.claim_position(position.position_id) claim: PositionClaim = notification.data - print(f"Position: {position} | PositionClaim: {claim}") \ No newline at end of file + print(f"Position: {position} | PositionClaim: {claim}") diff --git a/examples/rest/authenticated/get_wallets.py b/examples/rest/authenticated/get_wallets.py index 0ea1388..1a2d105 100644 --- a/examples/rest/authenticated/get_wallets.py +++ b/examples/rest/authenticated/get_wallets.py @@ -19,8 +19,8 @@ wallets: List[Wallet] = bfx.rest.auth.get_wallets() # Transfers funds (0.001 ETH) from exchange wallet to funding wallet A: Notification[Transfer] = bfx.rest.auth.transfer_between_wallets( - from_wallet="exchange", to_wallet="funding", from_currency="ETH", - to_currency="ETH", amount=0.001) + from_wallet="exchange", to_wallet="funding", currency="ETH", + currency_to="ETH", amount=0.001) print("Transfer:", A.data) @@ -41,4 +41,4 @@ D: Notification[Withdrawal] = bfx.rest.auth.submit_wallet_withdrawal( wallet="exchange", method="tetheruse", address="0x742d35Cc6634C0532925a3b844Bc454e4438f44e", amount=1.0) -print("Withdrawal:", D.data) \ No newline at end of file +print("Withdrawal:", D.data) diff --git a/examples/rest/authenticated/set_derivative_position_collateral.py b/examples/rest/authenticated/set_derivative_position_collateral.py index bb2100f..0949cac 100644 --- a/examples/rest/authenticated/set_derivative_position_collateral.py +++ b/examples/rest/authenticated/set_derivative_position_collateral.py @@ -32,5 +32,5 @@ print("Status:", bool(derivative_position_collateral.status)) derivative_position_collateral_limits: DerivativePositionCollateralLimits = \ bfx.rest.auth.get_derivative_position_collateral_limits(symbol="tBTCF0:USTF0") -print(f"Minimum collateral: {derivative_position_collateral_limits.min_collateral} | \ - Maximum collateral: {derivative_position_collateral_limits.max_collateral}") \ No newline at end of file +print(f"Minimum collateral: {derivative_position_collateral_limits.min_collateral} | " \ + f"Maximum collateral: {derivative_position_collateral_limits.max_collateral}") diff --git a/examples/rest/authenticated/submit_funding_offer.py b/examples/rest/authenticated/submit_funding_offer.py index 0230eb6..8291e67 100644 --- a/examples/rest/authenticated/submit_funding_offer.py +++ b/examples/rest/authenticated/submit_funding_offer.py @@ -14,11 +14,11 @@ bfx = Client( # Submit a new funding offer notification: Notification[FundingOffer] = bfx.rest.auth.submit_funding_offer( - type=FundingOfferType.LIMIT, - symbol="fUSD", - amount=123.45, - rate=0.001, - period=2, + type=FundingOfferType.LIMIT, + symbol="fUSD", + amount=123.45, + rate=0.001, + period=2, flags=Flag.HIDDEN ) @@ -27,4 +27,4 @@ print("Funding Offer notification:", notification) # Get all fUSD active funding offers offers = bfx.rest.auth.get_funding_offers(symbol="fUSD") -print("Offers (fUSD):", offers) \ No newline at end of file +print("Offers (fUSD):", offers) diff --git a/examples/rest/authenticated/submit_order.py b/examples/rest/authenticated/submit_order.py index 8c9fbb5..1481de0 100644 --- a/examples/rest/authenticated/submit_order.py +++ b/examples/rest/authenticated/submit_order.py @@ -15,9 +15,9 @@ bfx = Client( # Submit a new order submit_order_notification: Notification[Order] = bfx.rest.auth.submit_order( type=OrderType.EXCHANGE_LIMIT, - symbol="tBTCUST", - amount=0.015, - price=10000, + symbol="tBTCUST", + amount=0.015, + price=10000, flags=Flag.HIDDEN + Flag.OCO + Flag.CLOSE ) @@ -28,7 +28,7 @@ order: Order = submit_order_notification.data # Update its amount and its price update_order_notification: Notification[Order] = bfx.rest.auth.update_order( id=order.id, - amount=0.020, + amount=0.020, price=10150 ) @@ -39,4 +39,4 @@ cancel_order_notification: Notification[Order] = bfx.rest.auth.cancel_order( id=order.id ) -print("Cancel order notification:", cancel_order_notification) \ No newline at end of file +print("Cancel order notification:", cancel_order_notification) diff --git a/examples/rest/authenticated/toggle_keep_funding.py b/examples/rest/authenticated/toggle_keep_funding.py index b17405f..4f924ae 100644 --- a/examples/rest/authenticated/toggle_keep_funding.py +++ b/examples/rest/authenticated/toggle_keep_funding.py @@ -16,9 +16,9 @@ loans: List[FundingLoan] = bfx.rest.auth.get_funding_loans(symbol="fUSD") # Set every loan's keep funding status to (1: , 2: ) notification: Notification[None] = bfx.rest.auth.toggle_keep_funding( - funding_type="loan", + type="loan", ids=[ loan.id for loan in loans ], changes={ loan.id: 2 for loan in loans } ) -print("Toggle keep funding notification:", notification) \ No newline at end of file +print("Toggle keep funding notification:", notification) From 7311ffae4f3a8d97f095f9ab7ce3f22855987cdb Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 8 Mar 2023 19:40:06 +0100 Subject: [PATCH 209/265] Apply pylint's linting rules to examples/rest/merchant/*.py. --- examples/rest/merchant/settings.py | 6 +++--- examples/rest/merchant/submit_invoice.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/rest/merchant/settings.py b/examples/rest/merchant/settings.py index 62c015b..4f974b7 100644 --- a/examples/rest/merchant/settings.py +++ b/examples/rest/merchant/settings.py @@ -2,7 +2,7 @@ import os -from bfxapi import Client, REST_HOST +from bfxapi import Client, REST_HOST from bfxapi.rest.enums import MerchantSettingsKey @@ -15,7 +15,7 @@ bfx = Client( if not bfx.rest.merchant.set_merchant_settings(MerchantSettingsKey.RECOMMEND_STORE, 1): print(f"Cannot set <{MerchantSettingsKey.RECOMMEND_STORE}> to <1>.") -print(f"The current <{MerchantSettingsKey.PREFERRED_FIAT}> value is:", +print(f"The current <{MerchantSettingsKey.PREFERRED_FIAT}> value is:", bfx.rest.merchant.get_merchant_settings(MerchantSettingsKey.PREFERRED_FIAT)) settings = bfx.rest.merchant.list_merchant_settings([ @@ -25,4 +25,4 @@ settings = bfx.rest.merchant.list_merchant_settings([ ]) for key, value in settings.items(): - print(f"<{key}>:", value) \ No newline at end of file + print(f"<{key}>:", value) diff --git a/examples/rest/merchant/submit_invoice.py b/examples/rest/merchant/submit_invoice.py index 907c4bf..7d7a110 100644 --- a/examples/rest/merchant/submit_invoice.py +++ b/examples/rest/merchant/submit_invoice.py @@ -2,7 +2,7 @@ import os -from bfxapi import Client, REST_HOST +from bfxapi import Client, REST_HOST from bfxapi.rest.types import InvoiceSubmission @@ -42,4 +42,4 @@ print(bfx.rest.merchant.complete_invoice( print(bfx.rest.merchant.get_invoices(limit=25)) -print(bfx.rest.merchant.get_invoices_paginated(page=1, page_size=60, sort="asc", sort_field="t")) \ No newline at end of file +print(bfx.rest.merchant.get_invoices_paginated(page=1, page_size=60, sort="asc", sort_field="t")) From b0da5e2fb119ccc5f403b50cf2c4e29440ed85b1 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 8 Mar 2023 21:27:46 +0100 Subject: [PATCH 210/265] Apply pylint's linting rules to examples/rest/public/*.py. --- examples/rest/public/book.py | 2 +- examples/rest/public/conf.py | 2 +- examples/rest/public/get_candles_hist.py | 2 +- examples/rest/public/pulse_endpoints.py | 4 ++-- examples/rest/public/rest_calculation_endpoints.py | 2 +- examples/rest/public/trades.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/rest/public/book.py b/examples/rest/public/book.py index 9014835..f716fc4 100644 --- a/examples/rest/public/book.py +++ b/examples/rest/public/book.py @@ -21,4 +21,4 @@ print("25 price points of fUSD order book (with precision P0):", f_book) f_raw_book: List[FundingCurrencyRawBook] = bfx.rest.public.get_f_raw_book("fUSD") -print("fUSD raw order book:", f_raw_book) \ No newline at end of file +print("fUSD raw order book:", f_raw_book) diff --git a/examples/rest/public/conf.py b/examples/rest/public/conf.py index f4f6a55..431eb26 100644 --- a/examples/rest/public/conf.py +++ b/examples/rest/public/conf.py @@ -15,4 +15,4 @@ print (bfx.rest.public.conf(Config.MAP_CURRENCY_SYM)) print(bfx.rest.public.conf(Config.LIST_PAIR_EXCHANGE)) # Prints all the available funding currencies (pub:list:currency) -print(bfx.rest.public.conf(Config.LIST_CURRENCY)) \ No newline at end of file +print(bfx.rest.public.conf(Config.LIST_CURRENCY)) diff --git a/examples/rest/public/get_candles_hist.py b/examples/rest/public/get_candles_hist.py index 8cb28b3..12588b1 100644 --- a/examples/rest/public/get_candles_hist.py +++ b/examples/rest/public/get_candles_hist.py @@ -8,4 +8,4 @@ print(f"Candles: {bfx.rest.public.get_candles_hist(symbol='tBTCUSD')}") # Be sure to specify a period or aggregated period when retrieving funding candles. # If you wish to mimic the candles found in the UI, use the following setup to aggregate all funding candles: a30:p2:p30 -print(f"Candles: {bfx.rest.public.get_candles_hist(tf='15m', symbol='fUSD:a30:p2:p30')}") \ No newline at end of file +print(f"Candles: {bfx.rest.public.get_candles_hist(tf='15m', symbol='fUSD:a30:p2:p30')}") diff --git a/examples/rest/public/pulse_endpoints.py b/examples/rest/public/pulse_endpoints.py index c1a079a..9b5b35b 100644 --- a/examples/rest/public/pulse_endpoints.py +++ b/examples/rest/public/pulse_endpoints.py @@ -9,7 +9,7 @@ from bfxapi.rest.types import List, PulseMessage, PulseProfile bfx = Client(rest_host=PUB_REST_HOST) # POSIX timestamp in milliseconds (check https://currentmillis.com/) -end = datetime.datetime(2020, 5, 2).timestamp() * 1000 +end = datetime.datetime(2020, 5, 2).timestamp() * 1000 # Retrieves 25 pulse messages up to 2020/05/02 messages: List[PulseMessage] = bfx.rest.public.get_pulse_history(end=end, limit=25) @@ -21,4 +21,4 @@ for message in messages: profile: PulseProfile = bfx.rest.public.get_pulse_profile("News") URL = profile.picture.replace("size", "small") -print(f"<{profile.nickname}>'s profile picture: https://s3-eu-west-1.amazonaws.com/bfx-pub/{URL}") \ No newline at end of file +print(f"<{profile.nickname}>'s profile picture: https://s3-eu-west-1.amazonaws.com/bfx-pub/{URL}") diff --git a/examples/rest/public/rest_calculation_endpoints.py b/examples/rest/public/rest_calculation_endpoints.py index 2317aea..e66ca70 100644 --- a/examples/rest/public/rest_calculation_endpoints.py +++ b/examples/rest/public/rest_calculation_endpoints.py @@ -25,4 +25,4 @@ print("Average execution rate for fUSD:", funding_market_average_price.rate_avg) fx_rate: FxRate = bfx.rest.public.get_fx_rate(ccy1="USD", ccy2="EUR") -print("Exchange rate between USD and EUR:", fx_rate.current_rate) \ No newline at end of file +print("Exchange rate between USD and EUR:", fx_rate.current_rate) diff --git a/examples/rest/public/trades.py b/examples/rest/public/trades.py index 4428368..5e9242a 100644 --- a/examples/rest/public/trades.py +++ b/examples/rest/public/trades.py @@ -14,4 +14,4 @@ print("Latest 15 trades for tBTCUSD (in ascending order):", t_trades) f_trades: List[FundingCurrencyTrade] = bfx.rest.public.get_f_trades("fUSD", \ limit=15, sort=Sort.DESCENDING) -print("Latest 15 trades for fUSD (in descending order):", f_trades) \ No newline at end of file +print("Latest 15 trades for fUSD (in descending order):", f_trades) From 834a664034ac38621a8658616c1196c15ae19b52 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 8 Mar 2023 21:28:32 +0100 Subject: [PATCH 211/265] Apply pylint's linting rules to examples/websocket/authenticated/*.py. --- examples/websocket/authenticated/submit_order.py | 2 +- examples/websocket/authenticated/wallets.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/websocket/authenticated/submit_order.py b/examples/websocket/authenticated/submit_order.py index 31965c7..5555493 100644 --- a/examples/websocket/authenticated/submit_order.py +++ b/examples/websocket/authenticated/submit_order.py @@ -41,4 +41,4 @@ async def on_order_new(order_new: Order): def on_subscribed(subscription): print(f"Subscription successful for <{subscription}>.") -bfx.wss.run() \ No newline at end of file +bfx.wss.run() diff --git a/examples/websocket/authenticated/wallets.py b/examples/websocket/authenticated/wallets.py index da4815c..7dcb58c 100644 --- a/examples/websocket/authenticated/wallets.py +++ b/examples/websocket/authenticated/wallets.py @@ -27,4 +27,4 @@ def on_wallet_snapshot(wallets: List[Wallet]): def on_wallet_update(wallet: Wallet): print(f"Wallet update: {wallet}") -bfx.wss.run() \ No newline at end of file +bfx.wss.run() From d04ebb96d550a0dd76dfe908c210f8cfeb7bd7bf Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 8 Mar 2023 21:36:25 +0100 Subject: [PATCH 212/265] Apply pylint's linting rules to examples/websocket/public/*.py. --- examples/websocket/public/derivatives_status.py | 6 +++--- examples/websocket/public/order_book.py | 12 ++++++------ examples/websocket/public/raw_order_book.py | 12 ++++++------ examples/websocket/public/ticker.py | 4 ++-- examples/websocket/public/trades.py | 12 ++++++------ 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/examples/websocket/public/derivatives_status.py b/examples/websocket/public/derivatives_status.py index dcb3c1d..3609cff 100644 --- a/examples/websocket/public/derivatives_status.py +++ b/examples/websocket/public/derivatives_status.py @@ -10,14 +10,14 @@ 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) + 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(): +async def on_open(): await bfx.wss.subscribe(Channel.STATUS, key="deriv:tBTCF0:USTF0") -bfx.wss.run() \ No newline at end of file +bfx.wss.run() diff --git a/examples/websocket/public/order_book.py b/examples/websocket/public/order_book.py index 705ec41..aef1d04 100644 --- a/examples/websocket/public/order_book.py +++ b/examples/websocket/public/order_book.py @@ -10,21 +10,21 @@ from bfxapi.websocket import subscriptions from bfxapi.websocket.enums import Channel, Error from bfxapi.websocket.types import TradingPairBook -class OrderBook(object): +class OrderBook: def __init__(self, symbols: List[str]): self.__order_book = { - symbol: { + symbol: { "bids": OrderedDict(), "asks": OrderedDict() } for symbol in symbols } - + def update(self, symbol: str, data: TradingPairBook) -> None: price, count, amount = data.price, data.count, data.amount - kind = (amount > 0) and "bids" or "asks" + kind = "bids" if amount > 0 else "asks" if count > 0: - self.__order_book[symbol][kind][price] = { + self.__order_book[symbol][kind][price] = { "price": price, "count": count, "amount": amount @@ -62,4 +62,4 @@ def on_t_book_snapshot(subscription: subscriptions.Book, snapshot: List[TradingP def on_t_book_update(subscription: subscriptions.Book, data: TradingPairBook): order_book.update(subscription["symbol"], data) -bfx.wss.run() \ No newline at end of file +bfx.wss.run() diff --git a/examples/websocket/public/raw_order_book.py b/examples/websocket/public/raw_order_book.py index 9748abf..07814af 100644 --- a/examples/websocket/public/raw_order_book.py +++ b/examples/websocket/public/raw_order_book.py @@ -10,21 +10,21 @@ from bfxapi.websocket import subscriptions from bfxapi.websocket.enums import Channel, Error from bfxapi.websocket.types import TradingPairRawBook -class RawOrderBook(object): +class RawOrderBook: def __init__(self, symbols: List[str]): self.__raw_order_book = { - symbol: { + symbol: { "bids": OrderedDict(), "asks": OrderedDict() } for symbol in symbols } - + def update(self, symbol: str, data: TradingPairRawBook) -> None: order_id, price, amount = data.order_id, data.price, data.amount - kind = (amount > 0) and "bids" or "asks" + kind = "bids" if amount > 0 else "asks" if price > 0: - self.__raw_order_book[symbol][kind][order_id] = { + self.__raw_order_book[symbol][kind][order_id] = { "order_id": order_id, "price": price, "amount": amount @@ -62,4 +62,4 @@ def on_t_raw_book_snapshot(subscription: subscriptions.Book, snapshot: List[Trad def on_t_raw_book_update(subscription: subscriptions.Book, data: TradingPairRawBook): raw_order_book.update(subscription["symbol"], data) -bfx.wss.run() \ No newline at end of file +bfx.wss.run() diff --git a/examples/websocket/public/ticker.py b/examples/websocket/public/ticker.py index ff1120f..c446cd0 100644 --- a/examples/websocket/public/ticker.py +++ b/examples/websocket/public/ticker.py @@ -15,7 +15,7 @@ def on_t_ticker_update(subscription: subscriptions.Ticker, data: TradingPairTick print(f"Data: {data}") @bfx.wss.once("open") -async def open(): +async def on_open(): await bfx.wss.subscribe(Channel.TICKER, symbol="tBTCUSD") -bfx.wss.run() \ No newline at end of file +bfx.wss.run() diff --git a/examples/websocket/public/trades.py b/examples/websocket/public/trades.py index ff178dc..9edd20e 100644 --- a/examples/websocket/public/trades.py +++ b/examples/websocket/public/trades.py @@ -9,21 +9,21 @@ 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}") +def on_candles_update(_sub: 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}") +def on_t_trade_executed(_sub: 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(): +async def on_open(): await bfx.wss.subscribe(Channel.CANDLES, key="trade:1m:tBTCUSD") await bfx.wss.subscribe(Channel.TRADES, symbol="tBTCUSD") -bfx.wss.run() \ No newline at end of file +bfx.wss.run() From bba1493043f9bf4557ca11d2f4d4efaa37bbcc6c Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 8 Mar 2023 21:50:41 +0100 Subject: [PATCH 213/265] Apply pylint's linting rules to setup.py. --- .gitignore | 4 +++- setup.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 00d1381..6137a9d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,10 @@ *.pyc *.log +bitfinex_api_py.egg-info + __pycache__ dist venv -!.gitkeep \ No newline at end of file +!.gitkeep diff --git a/setup.py b/setup.py index 36f7511..a11a161 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ setup( "Bug Reports": "https://github.com/bitfinexcom/bitfinex-api-py/issues", "Source": "https://github.com/bitfinexcom/bitfinex-api-py", }, - packages=[ + packages=[ "bfxapi", "bfxapi.utils", "bfxapi.websocket", "bfxapi.websocket.client", "bfxapi.websocket.handlers", "bfxapi.rest", "bfxapi.rest.endpoints", "bfxapi.rest.middleware", @@ -38,4 +38,4 @@ setup( "requests~=2.28.1" ], python_requires=">=3.8" -) \ No newline at end of file +) From ac08071ab1748987c6aec6bdc023808707f2577b Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 8 Mar 2023 22:00:50 +0100 Subject: [PATCH 214/265] Add pylint to .github/PULL_REQUEST_TEMPLATE.md's checklist. --- .github/PULL_REQUEST_TEMPLATE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index da17065..a6d5024 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -26,5 +26,6 @@ PR fixes the following issue: - [ ] My changes generate no new warnings; - [ ] I have added tests that prove my fix is effective or that my feature works; - [ ] New and existing unit tests pass locally with my changes; -- [ ] Mypy does not return any errors or warnings if runned on the root package; +- [ ] Mypy returns no errors or warnings when run on the root package; +- [ ] Pylint returns a score of 10.00/10.00 when run on the root package; - [ ] I have updated the library version and updated the CHANGELOG; \ No newline at end of file From 1bb3c95effb3e1b7738fb5b2ceb65ccb1b462300 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 8 Mar 2023 22:08:22 +0100 Subject: [PATCH 215/265] Apply refactoring to .pylintrc file. --- .pylintrc | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.pylintrc b/.pylintrc index 2b14e62..e3196fc 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,11 +1,8 @@ [MAIN] - py-version=3.8.0 - ignore=examples [MESSAGES CONTROL] - disable= multiple-imports, missing-docstring, @@ -18,27 +15,20 @@ disable= inconsistent-return-statements, [SIMILARITIES] - min-similarity-lines=6 [VARIABLES] - allowed-redefined-builtins=type,dir,id,all,format,len [FORMAT] - max-line-length=120 - expected-line-ending-format=LF [BASIC] - good-names=id,on,pl,t,ip,tf,A,B,C,D,E,F [TYPECHECK] - generated-members=websockets [STRING] - check-quote-consistency=yes From e99230b0cba35f28c69e9cc6c2b1b333c7c5e3b8 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 9 Mar 2023 18:05:28 +0100 Subject: [PATCH 216/265] Add .github/workflows/bitfinex-api-py-ci.yml workflow for Github Actions. --- .github/workflows/bitfinex-api-py-ci.yml | 31 ++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/bitfinex-api-py-ci.yml diff --git a/.github/workflows/bitfinex-api-py-ci.yml b/.github/workflows/bitfinex-api-py-ci.yml new file mode 100644 index 0000000..96146d5 --- /dev/null +++ b/.github/workflows/bitfinex-api-py-ci.yml @@ -0,0 +1,31 @@ +name: bitfinex-api-py-ci + +on: + push: + branches: + - feature/ci-cd + pull_request: + branches: + - feature/ci-cd + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.8 + - uses: actions/setup-python@v4 + with: + python-version: '3.8' + - name: Install bitfinex-api-py's dependencies + - run: python -m pip install -r dev-requirements.txt + - name: Lint the project with pylint (and fail if score is lower than 10.00/10.00) + - run: python -m pylint bfxapi + - name: Run mypy to check the correctness of type hinting (and fail if any error or warning is found) + - run: python -m mypy bfxapi + - name: Execute project's unit tests (unittest) + - run: python -m unittest bfxapi.tests From 87bc19da483c3d9dc0288ee34cd2100225455537 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 9 Mar 2023 18:06:04 +0100 Subject: [PATCH 217/265] Add .travis.yml for Travis-CI support. --- .travis.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c9210f6 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: python +python: + - "3.8.0" +before_install: + - python -m pip install --upgrade pip +install: + - pip install -r dev-requirements.txt +script: + - python -m pylint bfxapi + - python -m mypy bfxapi + - python -m unittest bfxapi.tests From e8c7374cf25b943ad4ed880ece7abd184b6405ba Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 9 Mar 2023 18:06:46 +0100 Subject: [PATCH 218/265] Fix bug in .github/workflows/bitfinex-api-py-ci.yml. --- .github/PULL_REQUEST_TEMPLATE.md | 1 + .github/workflows/bitfinex-api-py-ci.yml | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a6d5024..f08ca31 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -11,6 +11,7 @@ PR fixes the following issue: ## Type of change + - [ ] Bug fix (non-breaking change which fixes an issue); - [ ] New feature (non-breaking change which adds functionality); diff --git a/.github/workflows/bitfinex-api-py-ci.yml b/.github/workflows/bitfinex-api-py-ci.yml index 96146d5..4728359 100644 --- a/.github/workflows/bitfinex-api-py-ci.yml +++ b/.github/workflows/bitfinex-api-py-ci.yml @@ -18,14 +18,14 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python 3.8 - - uses: actions/setup-python@v4 + uses: actions/setup-python@v4 with: python-version: '3.8' - name: Install bitfinex-api-py's dependencies - - run: python -m pip install -r dev-requirements.txt + run: python -m pip install -r dev-requirements.txt - name: Lint the project with pylint (and fail if score is lower than 10.00/10.00) - - run: python -m pylint bfxapi + run: python -m pylint bfxapi - name: Run mypy to check the correctness of type hinting (and fail if any error or warning is found) - - run: python -m mypy bfxapi + run: python -m mypy bfxapi - name: Execute project's unit tests (unittest) - - run: python -m unittest bfxapi.tests + run: python -m unittest bfxapi.tests From 98c3e4f3522874ee3b380b06546b8d7f2db83c77 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 9 Mar 2023 18:37:13 +0100 Subject: [PATCH 219/265] Change branch from feature/ci-cd to v3-beta in .github/workflows/bitfinex-api-py-ci.yml. --- .github/workflows/bitfinex-api-py-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/bitfinex-api-py-ci.yml b/.github/workflows/bitfinex-api-py-ci.yml index 4728359..295ce9f 100644 --- a/.github/workflows/bitfinex-api-py-ci.yml +++ b/.github/workflows/bitfinex-api-py-ci.yml @@ -3,10 +3,10 @@ name: bitfinex-api-py-ci on: push: branches: - - feature/ci-cd + - v3-beta pull_request: branches: - - feature/ci-cd + - v3-beta permissions: contents: read From 249f19fe417762d1dbdeee4ff322985b797397df Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 14 Mar 2023 19:03:05 +0100 Subject: [PATCH 220/265] Add ZeroConnectionsError in bfxapi.websocket.exceptions. Add log if connections = 0 in BfxWebsocketClient::start. Add raise ZeroConnectionError in BfxWebsocketClient::subscribe if connections equals 0. --- .pylintrc | 1 - .../websocket/client/bfx_websocket_client.py | 12 ++++++-- bfxapi/websocket/exceptions.py | 28 +++++++++++-------- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/.pylintrc b/.pylintrc index e3196fc..996e616 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,6 +1,5 @@ [MAIN] py-version=3.8.0 -ignore=examples [MESSAGES CONTROL] disable= diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 3f893eb..a88d3cd 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -13,7 +13,7 @@ from .bfx_websocket_bucket import _HEARTBEAT, F, _require_websocket_connection, from .bfx_websocket_inputs import BfxWebsocketInputs from ..handlers import PublicChannelsHandler, AuthenticatedChannelsHandler from ..exceptions import WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, \ - OutdatedClientVersion + ZeroConnectionsError, OutdatedClientVersion from ...utils.json_encoder import JSONEncoder @@ -85,6 +85,10 @@ class BfxWebsocketClient: return asyncio.run(self.start(connections)) async def start(self, connections = 5): + if connections == 0: + self.logger.info("With connections set to 0 it will not be possible to subscribe to any public channel. " \ + "Attempting a subscription will cause a ZeroConnectionsError to be thrown.") + if connections > BfxWebsocketClient.MAXIMUM_CONNECTIONS_AMOUNT: self.logger.warning(f"It is not safe to use more than {BfxWebsocketClient.MAXIMUM_CONNECTIONS_AMOUNT} " \ f"buckets from the same connection ({connections} in use), the server could momentarily " \ @@ -121,7 +125,8 @@ class BfxWebsocketClient: self.websocket, self.authentication = websocket, False - if await asyncio.gather(*[on_open_event.wait() for on_open_event in self.on_open_events]): + if len(self.on_open_events) == 0 or \ + (await asyncio.gather(*[on_open_event.wait() for on_open_event in self.on_open_events])): self.event_emitter.emit("open") if self.credentials: @@ -202,6 +207,9 @@ class BfxWebsocketClient: await self.websocket.send(json.dumps(data)) async def subscribe(self, channel, **kwargs): + if len(self.buckets) == 0: + raise ZeroConnectionsError("Unable to subscribe: the number of connections must be greater than 0.") + counters = [ len(bucket.pendings) + len(bucket.subscriptions) for bucket in self.buckets ] index = counters.index(min(counters)) diff --git a/bfxapi/websocket/exceptions.py b/bfxapi/websocket/exceptions.py index 49049fd..e1ff53e 100644 --- a/bfxapi/websocket/exceptions.py +++ b/bfxapi/websocket/exceptions.py @@ -5,9 +5,11 @@ __all__ = [ "ConnectionNotOpen", "TooManySubscriptions", + "ZeroConnectionsError", "WebsocketAuthenticationRequired", "InvalidAuthenticationCredentials", "EventNotSupported", + "HandlerNotFound", "OutdatedClientVersion" ] @@ -26,29 +28,33 @@ class TooManySubscriptions(BfxWebsocketException): This error indicates a subscription attempt after reaching the limit of simultaneous connections. """ +class ZeroConnectionsError(BfxWebsocketException): + """ + This error indicates an attempt to subscribe to a public channel while the number of connections is 0. + """ + class WebsocketAuthenticationRequired(BfxWebsocketException): """ This error indicates an attempt to access a protected resource without logging in first. """ +class InvalidAuthenticationCredentials(BfxWebsocketException): + """ + This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication. + """ + class EventNotSupported(BfxWebsocketException): """ This error indicates a failed attempt to subscribe to an event not supported by the BfxWebsocketClient. """ -class OutdatedClientVersion(BfxWebsocketException): - """ - This error indicates a mismatch between the client version and the server WSS version. - """ - - -class InvalidAuthenticationCredentials(BfxWebsocketException): - """ - This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication. - """ - class HandlerNotFound(BfxWebsocketException): """ This error indicates that a handler was not found for an incoming message. """ + +class OutdatedClientVersion(BfxWebsocketException): + """ + This error indicates a mismatch between the client version and the server WSS version. + """ From 464d942fb0d31413d73f5a1c3fa7e40b65510118 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 14 Mar 2023 19:16:12 +0100 Subject: [PATCH 221/265] Replace BfxWebsocketClient::on_open_events with BfxWebsocketBucket::on_open_event field. --- bfxapi/websocket/client/bfx_websocket_bucket.py | 6 +++--- bfxapi/websocket/client/bfx_websocket_client.py | 17 +++++------------ 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/bfxapi/websocket/client/bfx_websocket_bucket.py b/bfxapi/websocket/client/bfx_websocket_bucket.py index 72c3419..2d5e248 100644 --- a/bfxapi/websocket/client/bfx_websocket_bucket.py +++ b/bfxapi/websocket/client/bfx_websocket_bucket.py @@ -1,6 +1,6 @@ from typing import Literal, TypeVar, Callable, cast -import json, uuid, websockets +import asyncio, json, uuid, websockets from ..handlers import PublicChannelsHandler @@ -24,8 +24,8 @@ class BfxWebsocketBucket: MAXIMUM_SUBSCRIPTIONS_AMOUNT = 25 - def __init__(self, host, event_emitter, on_open_event): - self.host, self.event_emitter, self.on_open_event = host, event_emitter, on_open_event + def __init__(self, host, event_emitter): + self.host, self.event_emitter, self.on_open_event = host, event_emitter, asyncio.locks.Event() self.websocket, self.subscriptions, self.pendings = None, {}, [] diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index a88d3cd..5071ab6 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -62,12 +62,10 @@ class BfxWebsocketClient: ] def __init__(self, host, credentials = None, log_filename = None, log_level = "INFO"): - self.websocket = None + self.websocket, self.buckets, self.authentication = None, [], False self.host, self.credentials, self.event_emitter = host, credentials, AsyncIOEventEmitter() - self.on_open_events, self.buckets, self.authentication = [], [], False - self.inputs = BfxWebsocketInputs(handle_websocket_input=self.__handle_websocket_input) self.handler = AuthenticatedChannelsHandler(event_emitter=self.event_emitter) @@ -95,14 +93,9 @@ class BfxWebsocketClient: "block the client with <429 Too Many Requests>.") for _ in range(connections): - self.on_open_events.append(asyncio.Event()) + self.buckets += [BfxWebsocketBucket(self.host, self.event_emitter)] - for index in range(connections): - self.buckets += [BfxWebsocketBucket(self.host, self.event_emitter, self.on_open_events[index])] - - tasks = [ bucket.connect() for bucket in self.buckets ] - - tasks.append(self.__connect()) + tasks = [ bucket.connect() for bucket in self.buckets ] + [ self.__connect() ] await asyncio.gather(*tasks) @@ -125,8 +118,8 @@ class BfxWebsocketClient: self.websocket, self.authentication = websocket, False - if len(self.on_open_events) == 0 or \ - (await asyncio.gather(*[on_open_event.wait() for on_open_event in self.on_open_events])): + if len(self.buckets) == 0 or \ + (await asyncio.gather(*[bucket.on_open_event.wait() for bucket in self.buckets])): self.event_emitter.emit("open") if self.credentials: From 41fd46dec7e9be8fffa5e4b96e55aad0ef7aea6c Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 14 Mar 2023 20:58:56 +0100 Subject: [PATCH 222/265] Align code with new Bitfinex API documentation improvements. --- bfxapi/rest/endpoints/rest_public_endpoints.py | 4 ++-- bfxapi/rest/serializers.py | 4 ++-- bfxapi/rest/types.py | 12 ++++++------ bfxapi/websocket/serializers.py | 2 +- bfxapi/websocket/types.py | 10 +++++----- examples/rest/public/pulse_endpoints.py | 4 ++-- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/bfxapi/rest/endpoints/rest_public_endpoints.py b/bfxapi/rest/endpoints/rest_public_endpoints.py index ac494b6..6f6d772 100644 --- a/bfxapi/rest/endpoints/rest_public_endpoints.py +++ b/bfxapi/rest/endpoints/rest_public_endpoints.py @@ -239,10 +239,10 @@ class RestPublicEndpoints(Middleware): data = self._get(f"funding/stats/{symbol}/hist", params=params) return [ serializers.FundingStatistic.parse(*sub_data) for sub_data in data ] - def get_pulse_profile(self, nickname: str) -> PulseProfile: + def get_pulse_profile_details(self, nickname: str) -> PulseProfile: return serializers.PulseProfile.parse(*self._get(f"pulse/profile/{nickname}")) - def get_pulse_history(self, *, end: Optional[str] = None, limit: Optional[int] = None) -> List[PulseMessage]: + def get_pulse_message_history(self, *, end: Optional[str] = None, limit: Optional[int] = None) -> List[PulseMessage]: messages = [] for subdata in self._get("pulse/hist", params={ "end": end, "limit": limit }): diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 1fd4684..7cb8b71 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -199,7 +199,7 @@ DerivativesStatus = generate_labeler_serializer( "_PLACEHOLDER", "insurance_fund_balance", "_PLACEHOLDER", - "next_funding_evt_timestamp_ms", + "next_funding_evt_mts", "next_funding_accrued", "next_funding_step", "_PLACEHOLDER", @@ -258,7 +258,7 @@ FundingStatistic = generate_labeler_serializer( name="FundingStatistic", klass=types.FundingStatistic, labels=[ - "timestamp", + "mts", "_PLACEHOLDER", "_PLACEHOLDER", "frr", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index 34fdb46..bea85c3 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -106,10 +106,10 @@ class Statistic(_Type): @dataclass class Candle(_Type): mts: int - open: float - close: float - high: float - low: float + open: int + close: int + high: int + low: int volume: float @dataclass @@ -119,7 +119,7 @@ class DerivativesStatus(_Type): deriv_price: float spot_price: float insurance_fund_balance: float - next_funding_evt_timestamp_ms: int + next_funding_evt_mts: int next_funding_accrued: float next_funding_step: int current_funding: float @@ -149,7 +149,7 @@ class Leaderboard(_Type): @dataclass class FundingStatistic(_Type): - timestamp: int + mts: int frr: float avg_period: float funding_amount: float diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index e889cdd..464fdd9 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -149,7 +149,7 @@ DerivativesStatus = generate_labeler_serializer( "_PLACEHOLDER", "insurance_fund_balance", "_PLACEHOLDER", - "next_funding_evt_timestamp_ms", + "next_funding_evt_mts", "next_funding_accrued", "next_funding_step", "_PLACEHOLDER", diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py index e028f91..dcef21f 100644 --- a/bfxapi/websocket/types.py +++ b/bfxapi/websocket/types.py @@ -88,10 +88,10 @@ class FundingCurrencyRawBook(_Type): @dataclass class Candle(_Type): mts: int - open: float - close: float - high: float - low: float + open: int + close: int + high: int + low: int volume: float @dataclass @@ -100,7 +100,7 @@ class DerivativesStatus(_Type): deriv_price: float spot_price: float insurance_fund_balance: float - next_funding_evt_timestamp_ms: int + next_funding_evt_mts: int next_funding_accrued: float next_funding_step: int current_funding: float diff --git a/examples/rest/public/pulse_endpoints.py b/examples/rest/public/pulse_endpoints.py index 9b5b35b..d2305ac 100644 --- a/examples/rest/public/pulse_endpoints.py +++ b/examples/rest/public/pulse_endpoints.py @@ -12,13 +12,13 @@ bfx = Client(rest_host=PUB_REST_HOST) end = datetime.datetime(2020, 5, 2).timestamp() * 1000 # Retrieves 25 pulse messages up to 2020/05/02 -messages: List[PulseMessage] = bfx.rest.public.get_pulse_history(end=end, limit=25) +messages: List[PulseMessage] = bfx.rest.public.get_pulse_message_history(end=end, limit=25) for message in messages: print(f"Message author: {message.profile.nickname} ({message.profile.puid})") print(f"Title: <{message.title}>") print(f"Tags: {message.tags}\n") -profile: PulseProfile = bfx.rest.public.get_pulse_profile("News") +profile: PulseProfile = bfx.rest.public.get_pulse_profile_details("News") URL = profile.picture.replace("size", "small") print(f"<{profile.nickname}>'s profile picture: https://s3-eu-west-1.amazonaws.com/bfx-pub/{URL}") From 275cff6a2a795d90f60abd9323c9266c99d35399 Mon Sep 17 00:00:00 2001 From: Davide Casale <111585055+Davi0kProgramsThings@users.noreply.github.com> Date: Mon, 20 Mar 2023 18:32:48 +0100 Subject: [PATCH 223/265] Add CODE_OF_CONDUCT.md in root folder. --- CODE_OF_CONDUCT.md | 128 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..99ebd53 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +support@bitfinex.com (Bitfinex). +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. From 34f583cfff597724c5d5915ad4e01f634528ba0d Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 22 Mar 2023 21:24:44 +0100 Subject: [PATCH 224/265] Add bfxapi/version.py to contain the package version. --- bfxapi/__init__.py | 2 +- bfxapi/rest/__init__.py | 2 -- bfxapi/rest/endpoints/__init__.py | 2 -- bfxapi/rest/endpoints/rest_public_endpoints.py | 5 ++++- bfxapi/rest/middleware/__init__.py | 2 -- bfxapi/tests/__init__.py | 3 +-- bfxapi/utils/__init__.py | 1 - bfxapi/version.py | 1 + bfxapi/websocket/__init__.py | 2 -- bfxapi/websocket/client/__init__.py | 2 -- bfxapi/websocket/handlers/__init__.py | 2 -- setup.py | 6 +++++- 12 files changed, 12 insertions(+), 18 deletions(-) create mode 100644 bfxapi/version.py diff --git a/bfxapi/__init__.py b/bfxapi/__init__.py index 304bc34..b583248 100644 --- a/bfxapi/__init__.py +++ b/bfxapi/__init__.py @@ -3,4 +3,4 @@ from .client import Client from .urls import REST_HOST, PUB_REST_HOST, \ WSS_HOST, PUB_WSS_HOST -NAME = "bfxapi" +from .version import __version__ diff --git a/bfxapi/rest/__init__.py b/bfxapi/rest/__init__.py index 4f17f62..e18526e 100644 --- a/bfxapi/rest/__init__.py +++ b/bfxapi/rest/__init__.py @@ -1,4 +1,2 @@ from .endpoints import BfxRestInterface, RestPublicEndpoints, RestAuthenticatedEndpoints, \ RestMerchantEndpoints - -NAME = "rest" diff --git a/bfxapi/rest/endpoints/__init__.py b/bfxapi/rest/endpoints/__init__.py index ef82b97..2775e2e 100644 --- a/bfxapi/rest/endpoints/__init__.py +++ b/bfxapi/rest/endpoints/__init__.py @@ -3,5 +3,3 @@ from .bfx_rest_interface import BfxRestInterface from .rest_public_endpoints import RestPublicEndpoints from .rest_authenticated_endpoints import RestAuthenticatedEndpoints from .rest_merchant_endpoints import RestMerchantEndpoints - -NAME = "endpoints" diff --git a/bfxapi/rest/endpoints/rest_public_endpoints.py b/bfxapi/rest/endpoints/rest_public_endpoints.py index 6f6d772..c80d86b 100644 --- a/bfxapi/rest/endpoints/rest_public_endpoints.py +++ b/bfxapi/rest/endpoints/rest_public_endpoints.py @@ -242,7 +242,10 @@ class RestPublicEndpoints(Middleware): def get_pulse_profile_details(self, nickname: str) -> PulseProfile: return serializers.PulseProfile.parse(*self._get(f"pulse/profile/{nickname}")) - def get_pulse_message_history(self, *, end: Optional[str] = None, limit: Optional[int] = None) -> List[PulseMessage]: + def get_pulse_message_history(self, + *, + end: Optional[str] = None, + limit: Optional[int] = None) -> List[PulseMessage]: messages = [] for subdata in self._get("pulse/hist", params={ "end": end, "limit": limit }): diff --git a/bfxapi/rest/middleware/__init__.py b/bfxapi/rest/middleware/__init__.py index d7f9c42..ae3488d 100644 --- a/bfxapi/rest/middleware/__init__.py +++ b/bfxapi/rest/middleware/__init__.py @@ -1,3 +1 @@ from .middleware import Middleware - -NAME = "middleware" diff --git a/bfxapi/tests/__init__.py b/bfxapi/tests/__init__.py index 3084e94..df78300 100644 --- a/bfxapi/tests/__init__.py +++ b/bfxapi/tests/__init__.py @@ -1,11 +1,10 @@ import unittest + from .test_rest_serializers import TestRestSerializers from .test_websocket_serializers import TestWebsocketSerializers from .test_labeler import TestLabeler from .test_notification import TestNotification -NAME = "tests" - def suite(): return unittest.TestSuite([ unittest.makeSuite(TestRestSerializers), diff --git a/bfxapi/utils/__init__.py b/bfxapi/utils/__init__.py index 32f9209..e69de29 100644 --- a/bfxapi/utils/__init__.py +++ b/bfxapi/utils/__init__.py @@ -1 +0,0 @@ -NAME = "utils" diff --git a/bfxapi/version.py b/bfxapi/version.py new file mode 100644 index 0000000..3996ce8 --- /dev/null +++ b/bfxapi/version.py @@ -0,0 +1 @@ +__version__ = "3.0.0b1" diff --git a/bfxapi/websocket/__init__.py b/bfxapi/websocket/__init__.py index 6be76dd..1509ba7 100644 --- a/bfxapi/websocket/__init__.py +++ b/bfxapi/websocket/__init__.py @@ -1,3 +1 @@ from .client import BfxWebsocketClient, BfxWebsocketBucket, BfxWebsocketInputs - -NAME = "websocket" diff --git a/bfxapi/websocket/client/__init__.py b/bfxapi/websocket/client/__init__.py index fa12a78..e9acea9 100644 --- a/bfxapi/websocket/client/__init__.py +++ b/bfxapi/websocket/client/__init__.py @@ -1,5 +1,3 @@ from .bfx_websocket_client import BfxWebsocketClient from .bfx_websocket_bucket import BfxWebsocketBucket from .bfx_websocket_inputs import BfxWebsocketInputs - -NAME = "client" diff --git a/bfxapi/websocket/handlers/__init__.py b/bfxapi/websocket/handlers/__init__.py index e339402..d55ea1e 100644 --- a/bfxapi/websocket/handlers/__init__.py +++ b/bfxapi/websocket/handlers/__init__.py @@ -1,4 +1,2 @@ from .public_channels_handler import PublicChannelsHandler from .authenticated_channels_handler import AuthenticatedChannelsHandler - -NAME = "handlers" diff --git a/setup.py b/setup.py index a11a161..b8484a6 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,12 @@ from distutils.core import setup +version = {} +with open("bfxapi/version.py", encoding="utf-8") as fp: + exec(fp.read(), version) #pylint: disable=exec-used + setup( name="bitfinex-api-py", - version="3.0.0b1", + version=version["__version__"], description="Official Bitfinex Python API", long_description="A Python reference implementation of the Bitfinex API for both REST and websocket interaction", long_description_content_type="text/markdown", From 4f43d547952ce7aa5f2b1d62910e952121077c0e Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 22 Mar 2023 21:37:54 +0100 Subject: [PATCH 225/265] Start writing brief documentation in README.md. --- README.md | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..420ac0c --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# bitfinex-api-py (v3-beta) + +Official implementation of the [Bitfinex APIs (V2)](https://docs.bitfinex.com/docs) for `Python 3.8+`. + +> DISCLAIMER:
+Production use of v3.0.0b1 (and all future beta versions) is HIGHLY discouraged.
+Beta versions should not be used in applications which require user authentication.
+Provide your API-KEY/API-SECRET, and manage your account and funds at your own risk.
+ +### Features +- User-friendly implementations for 75+ public and authenticated REST endpoints. + * A complete list of available REST endpoints can be found [here](https://docs.bitfinex.com/reference). +- New WebSocket client to ensure fast, secure and persistent connections. + * Support for all public channels + authenticated events and inputs (a list can be found [here](https://docs.bitfinex.com/docs/ws-public)). + * Automatic reconnection system in case of network fail (both client and server side). + - The WebSocket client logs every reconnection failure, success and attempt (as well as other events). + * Connection multiplexing to allow subscribing to a large number of public channels (without affecting performances). + - The WebSocket server sets a limit of 25 subscriptions per connection, connection multiplexing allows the WebSocket client to bypass this limit. +- Full type-hinting and type-checking support with [`mypy`](https://github.com/python/mypy). + * This allow text editors to show helpful hints about the value of a variable:
+ ![example](https://i.imgur.com/aDjapcN.png "Type-hinting example on a random code snippet") +--- + +1. [Installation](#installation) +2. [Basic Usage](#basic-usage) +3. [Getting started with the WebSocket client](#getting-started-with-the-websocket-client) + * [Authentication with API-KEY and API-SECRET](#authentication-with-api-key-and-api-secret) + * [Configure the WebSocket client logger](#configure-the-websocket-client-logger) +4. [Building the source code](#building-the-source-code) + * [Testing (with unittest)](#testing-with-unittest) + * [Linting the project with pylint](#linting-the-project-with-pylint) + * [Using mypy to ensure correct type-hinting](#using-mypy-to-ensure-correct-type-hinting) +5. [How to contribute](#how-to-contribute) + * [License](#license) + +## Installation + +To install the latest beta release of `bitfinex-api-py`: +```bash +python3 -m pip install --pre bitfinex-api-py +``` +Otherwise, to install a specific beta version: +```bash +python3 -m pip install bitfinex-api-py==3.0.0b1 +``` + +## Basic usage + +## Getting started with the WebSocket client + +### Authentication with API-KEY and API-SECRET + +### Configure the WebSocket client logger + +## Building the source code + +### Testing (with unittest) + +### Linting the project with pylint + +### Using mypy to ensure correct type-hinting + +## How to contribute + +### License +This project is released under the `Apache License 2.0`.
+The complete license can be found here: https://www.apache.org/licenses/LICENSE-2.0. From 4a9dc7bb620d119f4a5374b3a6b5adec65ba2bdf Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 24 Mar 2023 04:12:17 +0100 Subject: [PATCH 226/265] Edit README.md adding new paragraphs. --- README.md | 132 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 118 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 420ac0c..cc7da7b 100644 --- a/README.md +++ b/README.md @@ -2,30 +2,32 @@ Official implementation of the [Bitfinex APIs (V2)](https://docs.bitfinex.com/docs) for `Python 3.8+`. -> DISCLAIMER:
-Production use of v3.0.0b1 (and all future beta versions) is HIGHLY discouraged.
-Beta versions should not be used in applications which require user authentication.
-Provide your API-KEY/API-SECRET, and manage your account and funds at your own risk.
+> **DISCLAIMER:** \ +Production use of v3.0.0b1 (and all future beta versions) is HIGHLY discouraged. \ +Beta versions should not be used in applications which require user authentication. \ +Provide your API-KEY/API-SECRET, and manage your account and funds at your own risk. ### Features - User-friendly implementations for 75+ public and authenticated REST endpoints. * A complete list of available REST endpoints can be found [here](https://docs.bitfinex.com/reference). - New WebSocket client to ensure fast, secure and persistent connections. * Support for all public channels + authenticated events and inputs (a list can be found [here](https://docs.bitfinex.com/docs/ws-public)). - * Automatic reconnection system in case of network fail (both client and server side). + * Automatic reconnection system in case of network failure (both client and server side). - The WebSocket client logs every reconnection failure, success and attempt (as well as other events). * Connection multiplexing to allow subscribing to a large number of public channels (without affecting performances). - The WebSocket server sets a limit of 25 subscriptions per connection, connection multiplexing allows the WebSocket client to bypass this limit. - Full type-hinting and type-checking support with [`mypy`](https://github.com/python/mypy). - * This allow text editors to show helpful hints about the value of a variable:
- ![example](https://i.imgur.com/aDjapcN.png "Type-hinting example on a random code snippet") + * This allow text editors to show helpful hints about the value of a variable: ![example](https://i.imgur.com/aDjapcN.png "Type-hinting example on a random code snippet") + --- 1. [Installation](#installation) 2. [Basic Usage](#basic-usage) -3. [Getting started with the WebSocket client](#getting-started-with-the-websocket-client) +3. [Using the WebSocket client](#using-the-websocket-client) + * [Running the client](#running-the-client) + * [Subscribing to public channels](#subscribing-to-public-channels) + * [Listening to events](#listening-to-events) * [Authentication with API-KEY and API-SECRET](#authentication-with-api-key-and-api-secret) - * [Configure the WebSocket client logger](#configure-the-websocket-client-logger) 4. [Building the source code](#building-the-source-code) * [Testing (with unittest)](#testing-with-unittest) * [Linting the project with pylint](#linting-the-project-with-pylint) @@ -39,19 +41,120 @@ To install the latest beta release of `bitfinex-api-py`: ```bash python3 -m pip install --pre bitfinex-api-py ``` -Otherwise, to install a specific beta version: +To install a specific beta version: ```bash python3 -m pip install bitfinex-api-py==3.0.0b1 ``` ## Basic usage -## Getting started with the WebSocket client +## Using the WebSocket client + +```python +bfx = Client(wss_host=PUB_WSS_HOST) +``` + +`Client::wss` contains an instance of `BfxWebSocketClient` (core implementation of the WebSocket client). \ +The `wss_host` argument is used to indicate the URL to which the WebSocket client should connect. \ +The `bfxapi` package exports 2 constants to quickly set this URL: + +Constant | URL | When to use +--- | --- | --- +WSS_HOST | wss://api.bitfinex.com/ws/2 | Suitable for all situations, with support for authentication. +PUB_WSS_HOST | wss://api-pub.bitfinex.com/ws/2 | Recommended for connections that do not require authentication. + +> **NOTE:** The `wss_host` parameter is optional, and the default value is WSS_HOST. + +### Running the client + +### Subscribing to public channels + +Users can subscribe to public channels using the coroutine `BfxWebSocketClient::subscribe`: +```python +await bfx.wss.subscribe("ticker", symbol="tBTCUSD") +``` + +#### Setting a custom `sub_id` + +The client generates a random and unique `sub_id` for each subscription. \ +However, it is possible to force this value by using the `sub_id` argument: + +```python +await bfx.wss.subscribe("candles", key="trade:1m:tBTCUSD", sub_id="507f1f77bcf86cd799439011") +``` + +#### Using the `Channel` enumeration + +`Channel` is an enumeration that contains the names of all the available public channels: +```python +from bfxapi.websocket.enums import Channel +``` + +You can use `Channel` while subscribing to a new public channel: +```python +await bfx.wss.subscribe(Channel.CANDLES, key="trade:1m:tBTCUSD", sub_id="507f1f77bcf86cd799439011") +``` + +### Listening to events + +Whenever the WebSocket client receives data, it will emit a specific event. \ +Users can either ignore those events or listen for them by registering callback functions. \ +To add a listener for a specific event, users can use the `BfxWebSocketClient::on` decorator: +```python +@bfx.wss.on("candles_update") +def on_candles_update(sub: subscriptions.Candles, candle: Candle): + print(f"Candle update for key <{sub['key']}>: {candle}") +``` + +The same can be achieved without using decorators: +```python +bfx.wss.on("candles_update", callback=on_candles_update) +``` + +You can pass any number of events to register for the same callback function: +```python +bfx.wss.on("t_ticker_update", "f_ticker_update", callback=on_ticker_update) +``` + +> **NOTE:** Callback functions can be either synchronous or asynchronous, in fact the client fully support coroutines (`asyncio`). + +#### The `open` event + +When the connection to the server is established, the client will emit the `open` event. \ +This is the right place for all bootstrap activities, including subscribing to public channels. + +```python +@bfx.wss.once("open") +async def on_open(): + await bfx.wss.subscribe(Channel.TICKER, symbol="tBTCUSD") +``` + +#### The `authenticated` event + + +If authentication succeeds, the client will emit the `authenticated` event. \ +All operations that require authentication must be performed after the emission of this event. \ +The `data` argument contains information regarding the authentication, such as the `userId`, the `auth_id`, etc... + +```python +@bfx.wss.once("authenticated") +def on_authenticated(data): + print(f"Successful login for user <{data['userId']}>.) +``` + +#### Using `BfxWebSocketClient::once` instead of `BfxWebSocketClient::on` + +For events that are expected to be emitted only once, it is highly recommended to use `BfxWebSocketClient::once`. \ +This prevents the client from emitting those events again, for example, after the connection is re-established following a network failure: + +```python +@bfx.wss.once("t_book_snapshot") +def on_t_book_snapshot(sub: subscriptions.Book, snapshot: List[TradingPairBook]): + print(f"The snapshot ({sub['symbol']}) contains {len(snapshot)} price points.") +``` ### Authentication with API-KEY and API-SECRET -### Configure the WebSocket client logger - ## Building the source code ### Testing (with unittest) @@ -63,5 +166,6 @@ python3 -m pip install bitfinex-api-py==3.0.0b1 ## How to contribute ### License -This project is released under the `Apache License 2.0`.
+This project is released under the `Apache License 2.0`. + The complete license can be found here: https://www.apache.org/licenses/LICENSE-2.0. From f3843abe8e77e7ccec0031ebc14b8810b989778e Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Sun, 26 Mar 2023 06:24:47 +0200 Subject: [PATCH 227/265] Extend documentation in README.md. --- README.md | 109 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 61 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index cc7da7b..e823b2c 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,13 @@ Provide your API-KEY/API-SECRET, and manage your account and funds at your own r 2. [Basic Usage](#basic-usage) 3. [Using the WebSocket client](#using-the-websocket-client) * [Running the client](#running-the-client) + * [Connection multiplexing](#connection-multiplexing) * [Subscribing to public channels](#subscribing-to-public-channels) * [Listening to events](#listening-to-events) + * [Main events](#main-events) + * [Reconnection in case of network failure](#reconnection-in-case-of-network-failure) * [Authentication with API-KEY and API-SECRET](#authentication-with-api-key-and-api-secret) + * [Configuring the custom logger](#configuring-the-custom-logger) 4. [Building the source code](#building-the-source-code) * [Testing (with unittest)](#testing-with-unittest) * [Linting the project with pylint](#linting-the-project-with-pylint) @@ -67,46 +71,68 @@ PUB_WSS_HOST | wss://api-pub.bitfinex.com/ws/2 | Recommended for connections tha ### Running the client +The client can be run using `BfxWebSocketClient::run`: +```python +bfx.wss.run() +``` + +If an event loop is already running, users can start the client with `BfxWebSocketClient::start`: +```python +await bfx.wss.start() +``` + +### Connection multiplexing + +`BfxWebSocketClient::run` and `BfxWebSocketClient::start` accept a `connections` argument: +```python +bfx.wss.run(connections=3) +``` + +`connections` indicates the number of connections to run concurrently (through connection multiplexing). + +Each of these connections can handle up to 25 subscriptions to public channels. \ +So, using `N` connections will allow the client to handle at most `N * 25` subscriptions. \ +You should always use the minimum number of connections necessary to handle all the subscriptions that will be made. + +For example, if you know that your application will subscribe to 75 public channels, 75 / 25 = 3 connections will be enough to handle all the subscriptions. + +The default number of connections is 5; therefore, if the `connections` argument is not given, the client will be able to handle a maximum of 25 * 5 = 125 subscriptions. + +Keep in mind that using a large number of connections could slow down the client performance. + +The use of more than 20 connections is not recommended. + ### Subscribing to public channels -Users can subscribe to public channels using the coroutine `BfxWebSocketClient::subscribe`: +Users can subscribe to public channels using `BfxWebSocketClient::subscribe`: ```python await bfx.wss.subscribe("ticker", symbol="tBTCUSD") ``` #### Setting a custom `sub_id` -The client generates a random and unique `sub_id` for each subscription. \ -However, it is possible to force this value by using the `sub_id` argument: +The client generates a random `sub_id` for each subscription. \ +These values must be unique, as the client uses them to identify subscriptions. \ +However, it is possible to force this value by passing a custom `sub_id` to `BfxWebSocketClient::subscribe`: ```python await bfx.wss.subscribe("candles", key="trade:1m:tBTCUSD", sub_id="507f1f77bcf86cd799439011") ``` -#### Using the `Channel` enumeration - -`Channel` is an enumeration that contains the names of all the available public channels: -```python -from bfxapi.websocket.enums import Channel -``` - -You can use `Channel` while subscribing to a new public channel: -```python -await bfx.wss.subscribe(Channel.CANDLES, key="trade:1m:tBTCUSD", sub_id="507f1f77bcf86cd799439011") -``` - ### Listening to events Whenever the WebSocket client receives data, it will emit a specific event. \ Users can either ignore those events or listen for them by registering callback functions. \ -To add a listener for a specific event, users can use the `BfxWebSocketClient::on` decorator: +These callback functions can also be asynchronous; in fact the client fully supports coroutines ([`asyncio`](https://docs.python.org/3/library/asyncio.html)). + +To add a listener for a specific event, users can use the decorator `BfxWebSocketClient::on`: ```python @bfx.wss.on("candles_update") def on_candles_update(sub: subscriptions.Candles, candle: Candle): print(f"Candle update for key <{sub['key']}>: {candle}") ``` -The same can be achieved without using decorators: +The same can be done without using decorators: ```python bfx.wss.on("candles_update", callback=on_candles_update) ``` @@ -116,45 +142,32 @@ You can pass any number of events to register for the same callback function: bfx.wss.on("t_ticker_update", "f_ticker_update", callback=on_ticker_update) ``` -> **NOTE:** Callback functions can be either synchronous or asynchronous, in fact the client fully support coroutines (`asyncio`). +### Main events -#### The `open` event +### Reconnection in case of network failure -When the connection to the server is established, the client will emit the `open` event. \ -This is the right place for all bootstrap activities, including subscribing to public channels. +In case of network failure, the client will keep waiting until it is able to restore the connection with the server. +The client will try to reconnect with exponential backoff; the backoff delay starts at three seconds and increases up to one minute. + +After a successful reconnection attempt, the client will emit the `reconnection` event. + +This event accepts two arguments: \ +`attemps` (`int`) which is the number of reconnection attempts (including the successful one), \ +`timedelta` (`datetime.timedelta`) which contains the amount of time the client has been down. + +Users can use this event for a variety of things, such as sending a notification if the client has been down for too long: ```python -@bfx.wss.once("open") -async def on_open(): - await bfx.wss.subscribe(Channel.TICKER, symbol="tBTCUSD") -``` - -#### The `authenticated` event - - -If authentication succeeds, the client will emit the `authenticated` event. \ -All operations that require authentication must be performed after the emission of this event. \ -The `data` argument contains information regarding the authentication, such as the `userId`, the `auth_id`, etc... - -```python -@bfx.wss.once("authenticated") -def on_authenticated(data): - print(f"Successful login for user <{data['userId']}>.) -``` - -#### Using `BfxWebSocketClient::once` instead of `BfxWebSocketClient::on` - -For events that are expected to be emitted only once, it is highly recommended to use `BfxWebSocketClient::once`. \ -This prevents the client from emitting those events again, for example, after the connection is re-established following a network failure: - -```python -@bfx.wss.once("t_book_snapshot") -def on_t_book_snapshot(sub: subscriptions.Book, snapshot: List[TradingPairBook]): - print(f"The snapshot ({sub['symbol']}) contains {len(snapshot)} price points.") +@bfx.wss.on("reconnection") +async def on_reconnection(attempts: int, timedelta: datetime.timedelta): + if timedelta.total_seconds() >= 60 * 60: # 60s * 60s = 3600s = 1h + await bfx.wss.notify(f"The client has been down for {timedelta}.") ``` ### Authentication with API-KEY and API-SECRET +### Configuring the custom logger + ## Building the source code ### Testing (with unittest) From a275e1d1631a3970e815ba143ce628460e3c3bcf Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Sun, 26 Mar 2023 19:35:19 +0200 Subject: [PATCH 228/265] Add new paragraphs to documentation in README.md. --- README.md | 142 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 109 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index e823b2c..704280f 100644 --- a/README.md +++ b/README.md @@ -21,24 +21,6 @@ Provide your API-KEY/API-SECRET, and manage your account and funds at your own r --- -1. [Installation](#installation) -2. [Basic Usage](#basic-usage) -3. [Using the WebSocket client](#using-the-websocket-client) - * [Running the client](#running-the-client) - * [Connection multiplexing](#connection-multiplexing) - * [Subscribing to public channels](#subscribing-to-public-channels) - * [Listening to events](#listening-to-events) - * [Main events](#main-events) - * [Reconnection in case of network failure](#reconnection-in-case-of-network-failure) - * [Authentication with API-KEY and API-SECRET](#authentication-with-api-key-and-api-secret) - * [Configuring the custom logger](#configuring-the-custom-logger) -4. [Building the source code](#building-the-source-code) - * [Testing (with unittest)](#testing-with-unittest) - * [Linting the project with pylint](#linting-the-project-with-pylint) - * [Using mypy to ensure correct type-hinting](#using-mypy-to-ensure-correct-type-hinting) -5. [How to contribute](#how-to-contribute) - * [License](#license) - ## Installation To install the latest beta release of `bitfinex-api-py`: @@ -52,7 +34,34 @@ python3 -m pip install bitfinex-api-py==3.0.0b1 ## Basic usage -## Using the WebSocket client +--- + +### Index + +* [WebSocket Client documentation](#websocket-client-documentation) +* [Building the source code](#building-the-source-code) +* [How to contribute](#how-to-contribute) + +--- + +# WebSocket Client documentation + +1. [Instantiating the client](#instantiating-the-client) + * [Authentication](#authentication) + * [Configuring the logger](#configuring-the-logger) +2. [Running the client](#running-the-client) + * [Closing the client](#closing-the-client) + * [Connection multiplexing](#connection-multiplexing) +3. [Subscribing to public channels](#subscribing-to-public-channels) + * [Setting a custom `sub_id`](#setting-a-custom-sub_id) +4. [Listening to events](#listening-to-events) +5. [Events](#events) + * [`open`](#open) + * [`authenticated`](#authenticated) +6. [Sending custom notifications](#sending-custom-notifications) +7. [Handling reconnections in case of network failure](#handling-reconnections-in-case-of-network-failure) + +## Instantiating the client ```python bfx = Client(wss_host=PUB_WSS_HOST) @@ -69,7 +78,29 @@ PUB_WSS_HOST | wss://api-pub.bitfinex.com/ws/2 | Recommended for connections tha > **NOTE:** The `wss_host` parameter is optional, and the default value is WSS_HOST. -### Running the client +### Authentication + +### Configuring the logger + +`log_filename` (`Optional[str]`, default: `None`): \ +Relative path of the file where to save the logs the client will emit. \ +If not given, the client will emit logs on `stdout` (`stderr` for errors and warnings). + +`log_level` (`str`, default: `"INFO"`): \ +Available log levels are (in order): `ERROR`, `WARNING`, `INFO` and `DEBUG`. \ +The client will only log messages whose level is lower than or equal to `log_level`. \ +For example, if `log_level=WARNING`, the client will only log errors and warnings. + +```python +bfx = Client( + [...], + log_filename="2023-03-26.log", + log_level="WARNING" +) +``` + + +## Running the client The client can be run using `BfxWebSocketClient::run`: ```python @@ -81,6 +112,12 @@ If an event loop is already running, users can start the client with `BfxWebSock await bfx.wss.start() ``` +### Closing the client + +```python +await bfx.wss.close(code=1001, reason="Going Away") +``` + ### Connection multiplexing `BfxWebSocketClient::run` and `BfxWebSocketClient::start` accept a `connections` argument: @@ -102,14 +139,14 @@ Keep in mind that using a large number of connections could slow down the client The use of more than 20 connections is not recommended. -### Subscribing to public channels +## Subscribing to public channels Users can subscribe to public channels using `BfxWebSocketClient::subscribe`: ```python await bfx.wss.subscribe("ticker", symbol="tBTCUSD") ``` -#### Setting a custom `sub_id` +### Setting a custom `sub_id` The client generates a random `sub_id` for each subscription. \ These values must be unique, as the client uses them to identify subscriptions. \ @@ -119,7 +156,7 @@ However, it is possible to force this value by passing a custom `sub_id` to `Bfx await bfx.wss.subscribe("candles", key="trade:1m:tBTCUSD", sub_id="507f1f77bcf86cd799439011") ``` -### Listening to events +## Listening to events Whenever the WebSocket client receives data, it will emit a specific event. \ Users can either ignore those events or listen for them by registering callback functions. \ @@ -142,9 +179,48 @@ You can pass any number of events to register for the same callback function: bfx.wss.on("t_ticker_update", "f_ticker_update", callback=on_ticker_update) ``` -### Main events +## Events -### Reconnection in case of network failure +### `open`: + +When the connection with the server is established, the client will emit the `open` event. \ +This is the right place for all bootstrap activities, including subscribing to public channels. + +```python +@bfx.wss.on("open") +async def on_open(): + await bfx.wss.subscribe(Channel.TICKER, symbol="tBTCUSD") +``` + +### `authenticated`: + +If authentication succeeds, the client will emit the `authenticated` event. \ +All operations that require authentication must be performed after the emission of this event. \ +The `data` argument contains information regarding the authentication, such as the `userId`, the `auth_id`, etc... + +```python +@bfx.wss.on("authenticated") +def on_authenticated(data: Dict[str, Any]): + print(f"Successful login for user <{data['userId']}>.) +``` + +## Sending custom notifications + +**Sending custom notifications requires user authentication.** + +Users can send custom notifications using `BfxWebSocketClient::notify`: +```python +await bfx.wss.notify({ "foo": 1 }) +``` + +The server broadcasts custom notifications to each of its clients: +```python +@bfx.wss.on("notification") +def on_notification(notification: Notification[Any]): + print(notification.data) # { "foo": 1 } +``` + +## Handling reconnections in case of network failure In case of network failure, the client will keep waiting until it is able to restore the connection with the server. @@ -164,21 +240,21 @@ async def on_reconnection(attempts: int, timedelta: datetime.timedelta): await bfx.wss.notify(f"The client has been down for {timedelta}.") ``` -### Authentication with API-KEY and API-SECRET +--- -### Configuring the custom logger +# Building the source code -## Building the source code +## Testing (with unittest) -### Testing (with unittest) +## Linting the project with pylint -### Linting the project with pylint +## Using mypy to ensure correct type-hinting -### Using mypy to ensure correct type-hinting +--- -## How to contribute +# How to contribute -### License +## License This project is released under the `Apache License 2.0`. The complete license can be found here: https://www.apache.org/licenses/LICENSE-2.0. From 2d0fabde201009df9ac3957e7b1eb1c29d7c70a4 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 5 Apr 2023 23:17:47 +0200 Subject: [PATCH 229/265] Add new paragraphs to the documentation in README.md. --- README.md | 101 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 52 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 704280f..4b5be30 100644 --- a/README.md +++ b/README.md @@ -50,16 +50,14 @@ python3 -m pip install bitfinex-api-py==3.0.0b1 * [Authentication](#authentication) * [Configuring the logger](#configuring-the-logger) 2. [Running the client](#running-the-client) - * [Closing the client](#closing-the-client) - * [Connection multiplexing](#connection-multiplexing) + * [Closing the connection](#closing-the-connection) 3. [Subscribing to public channels](#subscribing-to-public-channels) * [Setting a custom `sub_id`](#setting-a-custom-sub_id) 4. [Listening to events](#listening-to-events) -5. [Events](#events) - * [`open`](#open) - * [`authenticated`](#authenticated) -6. [Sending custom notifications](#sending-custom-notifications) -7. [Handling reconnections in case of network failure](#handling-reconnections-in-case-of-network-failure) +5. [Advanced Features](#advanced-features) + * [Connection multiplexing](#connection-multiplexing) + * [Sending custom notifications](#sending-custom-notifications) + * [Handling reconnections in case of network failure](#handling-reconnections-in-case-of-network-failure) ## Instantiating the client @@ -73,8 +71,10 @@ The `bfxapi` package exports 2 constants to quickly set this URL: Constant | URL | When to use --- | --- | --- -WSS_HOST | wss://api.bitfinex.com/ws/2 | Suitable for all situations, with support for authentication. -PUB_WSS_HOST | wss://api-pub.bitfinex.com/ws/2 | Recommended for connections that do not require authentication. +WSS_HOST | wss://api.bitfinex.com/ws/2 | Suitable for all situations, supports authentication. +PUB_WSS_HOST | wss://api-pub.bitfinex.com/ws/2 | For public uses only, doesn't support authentication. + +PUB_WSS_HOST is recommended over WSS_HOST for applications that don't require authentication. > **NOTE:** The `wss_host` parameter is optional, and the default value is WSS_HOST. @@ -99,7 +99,6 @@ bfx = Client( ) ``` - ## Running the client The client can be run using `BfxWebSocketClient::run`: @@ -112,33 +111,35 @@ If an event loop is already running, users can start the client with `BfxWebSock await bfx.wss.start() ``` -### Closing the client +If the client succeeds in connecting to the server, it will emit the `open` event. \ +This is the right place for all bootstrap activities, such as subscribing to public channels. \ +To learn more about events and public channels, see [Listening to events](#listening-to-events) and [Subscribing to public channels](#subscribing-to-public-channels). +```python +@bfx.wss.on("open") +async def on_open(): + await bfx.wss.subscribe(Channel.TICKER, symbol="tBTCUSD") +``` + +### Closing the connection + +Users can close the connection with the WebSocket server using `BfxWebSocketClient::close`: +```python +await bfx.wss.close() +``` + +A custom [close code number](https://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number) (along with a verbose reason) can be given as a parameter: ```python await bfx.wss.close(code=1001, reason="Going Away") ``` -### Connection multiplexing - -`BfxWebSocketClient::run` and `BfxWebSocketClient::start` accept a `connections` argument: +After closing the connection, the client will emit the `disconnection` event: ```python -bfx.wss.run(connections=3) +@bfx.wss.on("disconnection") +def on_disconnection(code: int, reason: str): + print(f"Closing connection with code: <{code}>. Reason: {reason}.") ``` -`connections` indicates the number of connections to run concurrently (through connection multiplexing). - -Each of these connections can handle up to 25 subscriptions to public channels. \ -So, using `N` connections will allow the client to handle at most `N * 25` subscriptions. \ -You should always use the minimum number of connections necessary to handle all the subscriptions that will be made. - -For example, if you know that your application will subscribe to 75 public channels, 75 / 25 = 3 connections will be enough to handle all the subscriptions. - -The default number of connections is 5; therefore, if the `connections` argument is not given, the client will be able to handle a maximum of 25 * 5 = 125 subscriptions. - -Keep in mind that using a large number of connections could slow down the client performance. - -The use of more than 20 connections is not recommended. - ## Subscribing to public channels Users can subscribe to public channels using `BfxWebSocketClient::subscribe`: @@ -179,32 +180,30 @@ You can pass any number of events to register for the same callback function: bfx.wss.on("t_ticker_update", "f_ticker_update", callback=on_ticker_update) ``` -## Events +## Advanced features -### `open`: - -When the connection with the server is established, the client will emit the `open` event. \ -This is the right place for all bootstrap activities, including subscribing to public channels. +### Connection multiplexing +`BfxWebSocketClient::run` and `BfxWebSocketClient::start` accept a `connections` argument: ```python -@bfx.wss.on("open") -async def on_open(): - await bfx.wss.subscribe(Channel.TICKER, symbol="tBTCUSD") +bfx.wss.run(connections=3) ``` -### `authenticated`: +`connections` indicates the number of connections to run concurrently (through connection multiplexing). -If authentication succeeds, the client will emit the `authenticated` event. \ -All operations that require authentication must be performed after the emission of this event. \ -The `data` argument contains information regarding the authentication, such as the `userId`, the `auth_id`, etc... +Each of these connections can handle up to 25 subscriptions to public channels. \ +So, using `N` connections will allow the client to handle at most `N * 25` subscriptions. \ +You should always use the minimum number of connections necessary to handle all the subscriptions that will be made. -```python -@bfx.wss.on("authenticated") -def on_authenticated(data: Dict[str, Any]): - print(f"Successful login for user <{data['userId']}>.) -``` +For example, if you know that your application will subscribe to 75 public channels, 75 / 25 = 3 connections will be enough to handle all the subscriptions. -## Sending custom notifications +The default number of connections is 5; therefore, if the `connections` argument is not given, the client will be able to handle a maximum of 25 * 5 = 125 subscriptions. + +Keep in mind that using a large number of connections could slow down the client performance. + +The use of more than 20 connections is not recommended. + +### Sending custom notifications **Sending custom notifications requires user authentication.** @@ -213,14 +212,18 @@ Users can send custom notifications using `BfxWebSocketClient::notify`: await bfx.wss.notify({ "foo": 1 }) ``` -The server broadcasts custom notifications to each of its clients: +Any data can be sent along with a custom notification. + +Custom notifications are broadcast by the server on all user's open connections. \ +So, each custom notification will be sent to every online client of the current user. \ +Whenever a client receives a custom notification, it will emit the `notification` event: ```python @bfx.wss.on("notification") def on_notification(notification: Notification[Any]): print(notification.data) # { "foo": 1 } ``` -## Handling reconnections in case of network failure +### Handling reconnections in case of network failure In case of network failure, the client will keep waiting until it is able to restore the connection with the server. From 7a9a57e878e1d4bd5b09a03915fb66d1a3eb8266 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 7 Apr 2023 00:54:41 +0200 Subject: [PATCH 230/265] Edit Subscribing to public channels paragraph in README.md. --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b5be30..0b574ee 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ Users can close the connection with the WebSocket server using `BfxWebSocketClie await bfx.wss.close() ``` -A custom [close code number](https://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number) (along with a verbose reason) can be given as a parameter: +A custom [close code number](https://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number), along with a verbose reason, can be given as parameters: ```python await bfx.wss.close(code=1001, reason="Going Away") ``` @@ -147,6 +147,14 @@ Users can subscribe to public channels using `BfxWebSocketClient::subscribe`: await bfx.wss.subscribe("ticker", symbol="tBTCUSD") ``` +On each successful subscription, the client will emit the `subscribed` event: +```python +@bfx.wss.on("subscribed") +def on_subscribed(subscription: subscriptions.Subscription): + if subscription["channel"] == "ticker": + print(f"{subscription['symbol']}: {subscription['subId']}") # tBTCUSD: f2757df2-7e11-4244-9bb7-a53b7343bef8 +``` + ### Setting a custom `sub_id` The client generates a random `sub_id` for each subscription. \ From 4a9c9b1ac7550b0b761902fdcf7c1cf2a67d2770 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Sat, 8 Apr 2023 03:27:17 +0200 Subject: [PATCH 231/265] Add `Unsubscribing from a public channel` sub-paragraph to README.md. --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 0b574ee..e50f7ec 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ python3 -m pip install bitfinex-api-py==3.0.0b1 2. [Running the client](#running-the-client) * [Closing the connection](#closing-the-connection) 3. [Subscribing to public channels](#subscribing-to-public-channels) + * [Unsubscribing from a public channel](#unsubscribing-from-a-public-channel) * [Setting a custom `sub_id`](#setting-a-custom-sub_id) 4. [Listening to events](#listening-to-events) 5. [Advanced Features](#advanced-features) @@ -155,6 +156,16 @@ def on_subscribed(subscription: subscriptions.Subscription): print(f"{subscription['symbol']}: {subscription['subId']}") # tBTCUSD: f2757df2-7e11-4244-9bb7-a53b7343bef8 ``` +### Unsubscribing from a public channel + +It is possible to unsubscribe from a public channel at any time. \ +Unsubscribing from a public channel prevents the client from receiving any more data from it. \ +This can be done using `BfxWebSocketClient::unsubscribe`, and passing the `sub_id` of the public channel you want to unsubscribe from: + +```python +await bfx.wss.unsubscribe(sub_id="f2757df2-7e11-4244-9bb7-a53b7343bef8") +``` + ### Setting a custom `sub_id` The client generates a random `sub_id` for each subscription. \ From d78c90faa8a91c1353f88332e5903f7a0277d400 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Tue, 11 Apr 2023 05:23:50 +0200 Subject: [PATCH 232/265] Add `Authentication` paragraph and `Filtering` sub-paragraph to README.md documentation. --- README.md | 58 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index e50f7ec..21cd9be 100644 --- a/README.md +++ b/README.md @@ -38,17 +38,17 @@ python3 -m pip install bitfinex-api-py==3.0.0b1 ### Index -* [WebSocket Client documentation](#websocket-client-documentation) +* [WebSocket client documentation](#websocket-client-documentation) * [Building the source code](#building-the-source-code) * [How to contribute](#how-to-contribute) --- -# WebSocket Client documentation +# WebSocket client documentation 1. [Instantiating the client](#instantiating-the-client) * [Authentication](#authentication) - * [Configuring the logger](#configuring-the-logger) + - [Filtering](#filtering) 2. [Running the client](#running-the-client) * [Closing the connection](#closing-the-connection) 3. [Subscribing to public channels](#subscribing-to-public-channels) @@ -81,25 +81,57 @@ PUB_WSS_HOST is recommended over WSS_HOST for applications that don't require au ### Authentication -### Configuring the logger +Users can authenticate in their accounts by providing a pair of API-KEY and API-SECRET: +```python +bfx = Client( + [...], + api_key=os.getenv("BFX_API_KEY"), + api_secret=os.getenv("BFX_API_SECRET") +) +``` -`log_filename` (`Optional[str]`, default: `None`): \ -Relative path of the file where to save the logs the client will emit. \ -If not given, the client will emit logs on `stdout` (`stderr` for errors and warnings). +If authentication succeeds, the client will emit the `authenticated` event. \ +All operations that require authentication must be performed after the emission of this event. \ +The `data` argument contains information regarding the authentication, such as the `userId`, the `auth_id`, etc... -`log_level` (`str`, default: `"INFO"`): \ -Available log levels are (in order): `ERROR`, `WARNING`, `INFO` and `DEBUG`. \ -The client will only log messages whose level is lower than or equal to `log_level`. \ -For example, if `log_level=WARNING`, the client will only log errors and warnings. +```python +@bfx.wss.on("authenticated") +def on_authenticated(data: Dict[str, Any]): + print(f"Successful login for user <{data['userId']}>.") +``` + +> **NOTE:** A guide on how to create, edit and revoke API-KEYs and API-SECRETs can be found [here](https://support.bitfinex.com/hc/en-us/articles/115003363429-How-to-create-and-revoke-a-Bitfinex-API-Key). + + +#### Filtering + +`Client` accepts a `filters` argument, which you can use to select which channels the client should subscribe to: ```python bfx = Client( [...], - log_filename="2023-03-26.log", - log_level="WARNING" + api_key=os.getenv("BFX_API_KEY"), + api_secret=os.getenv("BFX_API_SECRET"), + filters=[ "wallet" ] ) ``` +If `filters` is not given, the client will subscribe to all channels. + +Filters can be very useful both for safety reasons and for lightening network traffic (by excluding useless channels). \ +Ideally, you should always use `filters`, selecting only the channels your application will actually use. \ +Below, you can find a complete list of available filters (and the events they will emit): + +Filter | Subscribes the client to... | Available events are... +--- | --- | --- +trading | All channels regarding the user's orders, positions and trades (all trading pairs). | `order_snapshot`, `order_new`, `order_update`, `order_cancel`, `position_snapshot`, `position_new`, `position_update`, `position_close`, `trade_execution`, `trade_execution_update` +trading-tBTCUSD | All channels regarding the user's orders, positions and trades (tBTCUSD). | `order_snapshot`, `order_new`, `order_update`, `order_cancel`, `position_snapshot`, `position_new`, `position_update`, `position_close`, `trade_executed`, `trade_execution_update` +funding | All channels regarding the user's offers, credits and loans (all funding currencies). | `funding_offer_snapshot`, `funding_offer_new`, `funding_offer_update`, `funding_offer_cancel`, `funding_credit_snapshot`, `funding_credit_new`, `funding_credit_update`, `funding_credit_close`, `funding_loan_snapshot`, `funding_loan_new`, `funding_loan_update`, `funding_loan_close` +funding-fBTC | All channels regarding the user's offers, credits and loans (fBTC). | `funding_offer_snapshot`, `funding_offer_new`, `funding_offer_update`, `funding_offer_cancel`, `funding_credit_snapshot`, `funding_credit_new`, `funding_credit_update`, `funding_credit_close`, `funding_loan_snapshot`, `funding_loan_new`, `funding_loan_update`, `funding_loan_close` +wallet | All channels regarding user's exchange, trading and deposit wallets. | `wallet_snapshot`, `wallet_update` +wallet-exchange-BTC | Channel regarding user's BTC exchange wallet. | `wallet_snapshot`, `wallet_update` +balance | Channel regarding user's balances (tradable balance, etc...). | `balance_update` + ## Running the client The client can be run using `BfxWebSocketClient::run`: From 1333e80a8caded0fda677f1a1dd63ee4a2fe5311 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 12 Apr 2023 03:12:36 +0200 Subject: [PATCH 233/265] Improve readability for some paragraphs in README.md documentation. --- README.md | 108 ++++++++++++++++++++++++------------------------------ 1 file changed, 48 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 21cd9be..01e2642 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ python3 -m pip install bitfinex-api-py==3.0.0b1 --- -### Index +## Index * [WebSocket client documentation](#websocket-client-documentation) * [Building the source code](#building-the-source-code) @@ -48,17 +48,17 @@ python3 -m pip install bitfinex-api-py==3.0.0b1 1. [Instantiating the client](#instantiating-the-client) * [Authentication](#authentication) - - [Filtering](#filtering) + - [Filtering the channels](#filtering-the-channels) 2. [Running the client](#running-the-client) * [Closing the connection](#closing-the-connection) 3. [Subscribing to public channels](#subscribing-to-public-channels) * [Unsubscribing from a public channel](#unsubscribing-from-a-public-channel) * [Setting a custom `sub_id`](#setting-a-custom-sub_id) 4. [Listening to events](#listening-to-events) -5. [Advanced Features](#advanced-features) - * [Connection multiplexing](#connection-multiplexing) - * [Sending custom notifications](#sending-custom-notifications) - * [Handling reconnections in case of network failure](#handling-reconnections-in-case-of-network-failure) + +### Advanced features +* [Sending custom notifications](#sending-custom-notifications) +* [Setting up connection multiplexing](#setting-up-connection-multiplexing) ## Instantiating the client @@ -71,7 +71,7 @@ The `wss_host` argument is used to indicate the URL to which the WebSocket clien The `bfxapi` package exports 2 constants to quickly set this URL: Constant | URL | When to use ---- | --- | --- +:--- | :--- | :--- WSS_HOST | wss://api.bitfinex.com/ws/2 | Suitable for all situations, supports authentication. PUB_WSS_HOST | wss://api-pub.bitfinex.com/ws/2 | For public uses only, doesn't support authentication. @@ -91,8 +91,8 @@ bfx = Client( ``` If authentication succeeds, the client will emit the `authenticated` event. \ -All operations that require authentication must be performed after the emission of this event. \ -The `data` argument contains information regarding the authentication, such as the `userId`, the `auth_id`, etc... +All operations that require authentication will fail if run before the emission of this event. \ +The `data` argument contains various information about the authentication, such as the `userId`, the `auth_id`, etc... ```python @bfx.wss.on("authenticated") @@ -100,12 +100,24 @@ def on_authenticated(data: Dict[str, Any]): print(f"Successful login for user <{data['userId']}>.") ``` +`data` can also be useful for checking API-KEY's permissions: + +```python +@bfx.wss.on("authenticated") +def on_authenticated(data: Dict[str, Any]): + if not data["caps"]["orders"]["read"]: + raise Exception("This application requires read permissions on orders.") + + if not data["caps"]["positions"]["write"]: + raise Exception("This application requires write permissions on positions.") +``` + > **NOTE:** A guide on how to create, edit and revoke API-KEYs and API-SECRETs can be found [here](https://support.bitfinex.com/hc/en-us/articles/115003363429-How-to-create-and-revoke-a-Bitfinex-API-Key). -#### Filtering +#### Filtering the channels -`Client` accepts a `filters` argument, which you can use to select which channels the client should subscribe to: +It is possible to select which channels the client should subscribe to using the `filters` argument: ```python bfx = Client( @@ -116,21 +128,17 @@ bfx = Client( ) ``` -If `filters` is not given, the client will subscribe to all channels. - -Filters can be very useful both for safety reasons and for lightening network traffic (by excluding useless channels). \ +Filtering can be very useful both for safety reasons and for lightening network traffic (by excluding useless channels). \ Ideally, you should always use `filters`, selecting only the channels your application will actually use. \ -Below, you can find a complete list of available filters (and the events they will emit): +Below, you can find a complete list of available filters (and the channels they will subscribe the client to): Filter | Subscribes the client to... | Available events are... ---- | --- | --- +:--- | :--- | :--- trading | All channels regarding the user's orders, positions and trades (all trading pairs). | `order_snapshot`, `order_new`, `order_update`, `order_cancel`, `position_snapshot`, `position_new`, `position_update`, `position_close`, `trade_execution`, `trade_execution_update` trading-tBTCUSD | All channels regarding the user's orders, positions and trades (tBTCUSD). | `order_snapshot`, `order_new`, `order_update`, `order_cancel`, `position_snapshot`, `position_new`, `position_update`, `position_close`, `trade_executed`, `trade_execution_update` funding | All channels regarding the user's offers, credits and loans (all funding currencies). | `funding_offer_snapshot`, `funding_offer_new`, `funding_offer_update`, `funding_offer_cancel`, `funding_credit_snapshot`, `funding_credit_new`, `funding_credit_update`, `funding_credit_close`, `funding_loan_snapshot`, `funding_loan_new`, `funding_loan_update`, `funding_loan_close` funding-fBTC | All channels regarding the user's offers, credits and loans (fBTC). | `funding_offer_snapshot`, `funding_offer_new`, `funding_offer_update`, `funding_offer_cancel`, `funding_credit_snapshot`, `funding_credit_new`, `funding_credit_update`, `funding_credit_close`, `funding_loan_snapshot`, `funding_loan_new`, `funding_loan_update`, `funding_loan_close` wallet | All channels regarding user's exchange, trading and deposit wallets. | `wallet_snapshot`, `wallet_update` -wallet-exchange-BTC | Channel regarding user's BTC exchange wallet. | `wallet_snapshot`, `wallet_update` -balance | Channel regarding user's balances (tradable balance, etc...). | `balance_update` ## Running the client @@ -231,9 +239,29 @@ You can pass any number of events to register for the same callback function: bfx.wss.on("t_ticker_update", "f_ticker_update", callback=on_ticker_update) ``` -## Advanced features +# Advanced features -### Connection multiplexing +## Sending custom notifications + +**Sending custom notifications requires user authentication.** + +Users can send custom notifications using `BfxWebSocketClient::notify`: +```python +await bfx.wss.notify({ "foo": 1 }) +``` + +Any data can be sent along with a custom notification. + +Custom notifications are broadcast by the server on all user's open connections. \ +So, each custom notification will be sent to every online client of the current user. \ +Whenever a client receives a custom notification, it will emit the `notification` event: +```python +@bfx.wss.on("notification") +def on_notification(notification: Notification[Any]): + print(notification.data) # { "foo": 1 } +``` + +## Setting up connection multiplexing `BfxWebSocketClient::run` and `BfxWebSocketClient::start` accept a `connections` argument: ```python @@ -254,46 +282,6 @@ Keep in mind that using a large number of connections could slow down the client The use of more than 20 connections is not recommended. -### Sending custom notifications - -**Sending custom notifications requires user authentication.** - -Users can send custom notifications using `BfxWebSocketClient::notify`: -```python -await bfx.wss.notify({ "foo": 1 }) -``` - -Any data can be sent along with a custom notification. - -Custom notifications are broadcast by the server on all user's open connections. \ -So, each custom notification will be sent to every online client of the current user. \ -Whenever a client receives a custom notification, it will emit the `notification` event: -```python -@bfx.wss.on("notification") -def on_notification(notification: Notification[Any]): - print(notification.data) # { "foo": 1 } -``` - -### Handling reconnections in case of network failure - -In case of network failure, the client will keep waiting until it is able to restore the connection with the server. - -The client will try to reconnect with exponential backoff; the backoff delay starts at three seconds and increases up to one minute. - -After a successful reconnection attempt, the client will emit the `reconnection` event. - -This event accepts two arguments: \ -`attemps` (`int`) which is the number of reconnection attempts (including the successful one), \ -`timedelta` (`datetime.timedelta`) which contains the amount of time the client has been down. - -Users can use this event for a variety of things, such as sending a notification if the client has been down for too long: -```python -@bfx.wss.on("reconnection") -async def on_reconnection(attempts: int, timedelta: datetime.timedelta): - if timedelta.total_seconds() >= 60 * 60: # 60s * 60s = 3600s = 1h - await bfx.wss.notify(f"The client has been down for {timedelta}.") -``` - --- # Building the source code From 51b4d2c18177b4c69efe941763eae5cb81b61179 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 13 Apr 2023 17:29:53 +0200 Subject: [PATCH 234/265] Add and write new `Building from source code` section in README.md. --- README.md | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 01e2642..85d694a 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ python3 -m pip install bitfinex-api-py==3.0.0b1 ## Index * [WebSocket client documentation](#websocket-client-documentation) -* [Building the source code](#building-the-source-code) +* [Building from source code](#building-from-source-code) * [How to contribute](#how-to-contribute) --- @@ -284,10 +284,42 @@ The use of more than 20 connections is not recommended. --- -# Building the source code +# Building from source code + +**Building from source code requires Python 3.8+** + +Make sure to clone the right branch of the repository (`v3-beta`): + +```bash +git clone -b v3-beta --single-branch https://github.com/bitfinexcom/bitfinex-api-py.git +``` + +### Installing the dependencies + +```bash +python3 -m pip install -r requirements.txt +``` + +If you are willing to contribute to the project, you'll need the correct versions of [`pylint`](https://github.com/pylint-dev/pylint) and [`mypy`](https://github.com/python/mypy). + +You can get both by installing the dependencies in `dev-requirements.txt`: + +```bash +python3 -m pip install -r dev-requirements.txt +``` + +### Optional steps + +1. [Testing (with unittest)](#testing-with-unittest) +2. [Linting the project with pylint](#linting-the-project-with-pylint) +3. [Using mypy to ensure correct type-hinting](#using-mypy-to-ensure-correct-type-hinting) ## Testing (with unittest) +```bash +python3 -m unittest -v bfxapi.tests +``` + ## Linting the project with pylint ## Using mypy to ensure correct type-hinting From a1a013fef87487a482a591e1cc560cb51571a937 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 14 Apr 2023 04:50:33 +0200 Subject: [PATCH 235/265] Move `Building from source code` in `How to contribute` section in README.md. --- README.md | 133 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 76 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 85d694a..56ea3b9 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Provide your API-KEY/API-SECRET, and manage your account and funds at your own r - The WebSocket client logs every reconnection failure, success and attempt (as well as other events). * Connection multiplexing to allow subscribing to a large number of public channels (without affecting performances). - The WebSocket server sets a limit of 25 subscriptions per connection, connection multiplexing allows the WebSocket client to bypass this limit. -- Full type-hinting and type-checking support with [`mypy`](https://github.com/python/mypy). +- Full type hinting and type checking support with [`mypy`](https://github.com/python/mypy). * This allow text editors to show helpful hints about the value of a variable: ![example](https://i.imgur.com/aDjapcN.png "Type-hinting example on a random code snippet") --- @@ -24,11 +24,11 @@ Provide your API-KEY/API-SECRET, and manage your account and funds at your own r ## Installation To install the latest beta release of `bitfinex-api-py`: -```bash +```console python3 -m pip install --pre bitfinex-api-py ``` To install a specific beta version: -```bash +```console python3 -m pip install bitfinex-api-py==3.0.0b1 ``` @@ -39,7 +39,7 @@ python3 -m pip install bitfinex-api-py==3.0.0b1 ## Index * [WebSocket client documentation](#websocket-client-documentation) -* [Building from source code](#building-from-source-code) + - [Advanced features](#advanced-features) * [How to contribute](#how-to-contribute) --- @@ -48,7 +48,6 @@ python3 -m pip install bitfinex-api-py==3.0.0b1 1. [Instantiating the client](#instantiating-the-client) * [Authentication](#authentication) - - [Filtering the channels](#filtering-the-channels) 2. [Running the client](#running-the-client) * [Closing the connection](#closing-the-connection) 3. [Subscribing to public channels](#subscribing-to-public-channels) @@ -114,32 +113,6 @@ def on_authenticated(data: Dict[str, Any]): > **NOTE:** A guide on how to create, edit and revoke API-KEYs and API-SECRETs can be found [here](https://support.bitfinex.com/hc/en-us/articles/115003363429-How-to-create-and-revoke-a-Bitfinex-API-Key). - -#### Filtering the channels - -It is possible to select which channels the client should subscribe to using the `filters` argument: - -```python -bfx = Client( - [...], - api_key=os.getenv("BFX_API_KEY"), - api_secret=os.getenv("BFX_API_SECRET"), - filters=[ "wallet" ] -) -``` - -Filtering can be very useful both for safety reasons and for lightening network traffic (by excluding useless channels). \ -Ideally, you should always use `filters`, selecting only the channels your application will actually use. \ -Below, you can find a complete list of available filters (and the channels they will subscribe the client to): - -Filter | Subscribes the client to... | Available events are... -:--- | :--- | :--- -trading | All channels regarding the user's orders, positions and trades (all trading pairs). | `order_snapshot`, `order_new`, `order_update`, `order_cancel`, `position_snapshot`, `position_new`, `position_update`, `position_close`, `trade_execution`, `trade_execution_update` -trading-tBTCUSD | All channels regarding the user's orders, positions and trades (tBTCUSD). | `order_snapshot`, `order_new`, `order_update`, `order_cancel`, `position_snapshot`, `position_new`, `position_update`, `position_close`, `trade_executed`, `trade_execution_update` -funding | All channels regarding the user's offers, credits and loans (all funding currencies). | `funding_offer_snapshot`, `funding_offer_new`, `funding_offer_update`, `funding_offer_cancel`, `funding_credit_snapshot`, `funding_credit_new`, `funding_credit_update`, `funding_credit_close`, `funding_loan_snapshot`, `funding_loan_new`, `funding_loan_update`, `funding_loan_close` -funding-fBTC | All channels regarding the user's offers, credits and loans (fBTC). | `funding_offer_snapshot`, `funding_offer_new`, `funding_offer_update`, `funding_offer_cancel`, `funding_credit_snapshot`, `funding_credit_new`, `funding_credit_update`, `funding_credit_close`, `funding_loan_snapshot`, `funding_loan_new`, `funding_loan_update`, `funding_loan_close` -wallet | All channels regarding user's exchange, trading and deposit wallets. | `wallet_snapshot`, `wallet_update` - ## Running the client The client can be run using `BfxWebSocketClient::run`: @@ -284,51 +257,97 @@ The use of more than 20 connections is not recommended. --- -# Building from source code +# How to contribute -**Building from source code requires Python 3.8+** +1. [Installation and setup](#installation-and-setup) + * [Cloning the repository](#cloning-the-repository) + * [Installing the dependencies](#installing-the-dependencies) +2. [Before opening a PR](#before-opening-a-pr) + * [Running the unit tests](#running-the-unit-tests) + * [Linting the project with pylint](#linting-the-project-with-pylint) + * [Using mypy to ensure correct type hinting](#using-mypy-to-ensure-correct-type-hinting) +3. [License](#license) -Make sure to clone the right branch of the repository (`v3-beta`): +## Installation and setup -```bash -git clone -b v3-beta --single-branch https://github.com/bitfinexcom/bitfinex-api-py.git +A brief guide on how to install and set up the project in your Python 3.8+ environment. + +### Cloning the repository + +The following command will only clone the `v3-beta` branch (excluding all others): + +```console +git clone --branch v3-beta --single-branch https://github.com/bitfinexcom/bitfinex-api-py.git ``` ### Installing the dependencies -```bash -python3 -m pip install -r requirements.txt -``` - -If you are willing to contribute to the project, you'll need the correct versions of [`pylint`](https://github.com/pylint-dev/pylint) and [`mypy`](https://github.com/python/mypy). - -You can get both by installing the dependencies in `dev-requirements.txt`: - -```bash +```console python3 -m pip install -r dev-requirements.txt ``` -### Optional steps +Make sure to install `dev-requirements.txt` instead of `requirements.txt`. \ +`dev-requirements.txt` will install all dependencies in `requirements.txt` plus any development dependencies. \ +This will also install the versions in use of [`pylint`](https://github.com/pylint-dev/pylint) and [`mypy`](https://github.com/python/mypy), which you should both use before opening your PRs. -1. [Testing (with unittest)](#testing-with-unittest) -2. [Linting the project with pylint](#linting-the-project-with-pylint) -3. [Using mypy to ensure correct type-hinting](#using-mypy-to-ensure-correct-type-hinting) +All done, your Python 3.8+ environment should now be able to run `bitfinex-api-py`'s source code. -## Testing (with unittest) +## Before opening a PR -```bash +Before opening a new pull request you must... +* run the unit tests. +* lint the project with pylint. +* use mypy to ensure correct type hinting. + +### Running the unit tests + +`bitfinex-api-py` comes with a set of unit tests (written using the [`unittest`](https://docs.python.org/3.8/library/unittest.html) unit testing framework). \ +Contributors must ensure that each unit test passes before opening a pull request. \ +You can run all project's unit tests by calling `unittest` on `bfxapi.tests`: +```console python3 -m unittest -v bfxapi.tests ``` -## Linting the project with pylint +A single unit test can be run as follows: +```console +python3 -m unittest -v bfxapi.tests.test_notification +``` -## Using mypy to ensure correct type-hinting +### Linting the project with pylint ---- +`bitfinex-api-py`'s source code follows [`pylint`](https://github.com/pylint-dev/pylint) style guidelines (with some exceptions and modifications). \ +For a full look at what rules are in use, take a look at the configuration file [`.pylintrc`](https://github.com/bitfinexcom/bitfinex-api-py/blob/v3-beta/.pylintrc). \ +Make sure that `pylint` returns a score of 10.00/10.00 before opening a PR. -# How to contribute +You can run `pylint` against all project's files (`*.py`) by calling it on `bfxapi` (root package): +```console +python3 -m pylint bfxapi +``` + +### Using mypy to ensure correct type hinting + +`bitfinex-api-py` uses [`mypy`](https://github.com/python/mypy) to ensure correct type hinting for end users. \ +`mypy` must not throw any error code when run on your code (excluding notes). \ +You can perform type checks on all project's files by running this command: + +```console +python3 -m mypy bfxapi +``` ## License -This project is released under the `Apache License 2.0`. -The complete license can be found here: https://www.apache.org/licenses/LICENSE-2.0. +``` +Copyright 2023 Bitfinex + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` From 82599e1ffba5c35e73bfb02935cb4bfab53ffc12 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 14 Apr 2023 22:05:55 +0200 Subject: [PATCH 236/265] Finish writing `How to contribute` section in README.md. --- README.md | 46 +++++++++++++++++++--------------------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 56ea3b9..b6b1ca9 100644 --- a/README.md +++ b/README.md @@ -259,13 +259,20 @@ The use of more than 20 connections is not recommended. # How to contribute +All contributions are welcome! :D + + +A guide on how to install and set up `bitfinex-api-py`'s source code can be found [here](#installation-and-setup). \ +Before opening any pull requests, please have a look at [Before Opening a PR](#before-opening-a-pr). \ +Contributors must uphold the [Contributor Covenant code of conduct](https://github.com/bitfinexcom/bitfinex-api-py/blob/v3-beta/CODE_OF_CONDUCT.md). + +### Index + 1. [Installation and setup](#installation-and-setup) * [Cloning the repository](#cloning-the-repository) * [Installing the dependencies](#installing-the-dependencies) 2. [Before opening a PR](#before-opening-a-pr) * [Running the unit tests](#running-the-unit-tests) - * [Linting the project with pylint](#linting-the-project-with-pylint) - * [Using mypy to ensure correct type hinting](#using-mypy-to-ensure-correct-type-hinting) 3. [License](#license) ## Installation and setup @@ -294,10 +301,16 @@ All done, your Python 3.8+ environment should now be able to run `bitfinex-api-p ## Before opening a PR -Before opening a new pull request you must... -* run the unit tests. -* lint the project with pylint. -* use mypy to ensure correct type hinting. +**We won't accept your PR or we will request changes if the following requirements aren't met.** + +Wheter you're submitting a bug fix, a new feature or a documentation change, you should first discuss it in an issue. + +All PRs must follow this [PULL_REQUEST_TEMPLATE](https://github.com/bitfinexcom/bitfinex-api-py/blob/v3-beta/.github/PULL_REQUEST_TEMPLATE.md) and include an exhaustive description. + +Before opening a pull request, you should also make sure that: +- [ ] all unit tests pass (see [Running the unit tests](#running-the-unit-tests)). +- [ ] [`pylint`](https://github.com/pylint-dev/pylint) returns a score of 10.00/10.00 when run against your code. +- [ ] [`mypy`](https://github.com/python/mypy) doesn't throw any error code when run on the project (excluding notes). ### Running the unit tests @@ -313,27 +326,6 @@ A single unit test can be run as follows: python3 -m unittest -v bfxapi.tests.test_notification ``` -### Linting the project with pylint - -`bitfinex-api-py`'s source code follows [`pylint`](https://github.com/pylint-dev/pylint) style guidelines (with some exceptions and modifications). \ -For a full look at what rules are in use, take a look at the configuration file [`.pylintrc`](https://github.com/bitfinexcom/bitfinex-api-py/blob/v3-beta/.pylintrc). \ -Make sure that `pylint` returns a score of 10.00/10.00 before opening a PR. - -You can run `pylint` against all project's files (`*.py`) by calling it on `bfxapi` (root package): -```console -python3 -m pylint bfxapi -``` - -### Using mypy to ensure correct type hinting - -`bitfinex-api-py` uses [`mypy`](https://github.com/python/mypy) to ensure correct type hinting for end users. \ -`mypy` must not throw any error code when run on your code (excluding notes). \ -You can perform type checks on all project's files by running this command: - -```console -python3 -m mypy bfxapi -``` - ## License ``` From 75d4787bbe75966ab8e00f6a9fac70e09a877159 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Sun, 16 Apr 2023 08:08:57 +0200 Subject: [PATCH 237/265] Write `Quickstart` section in README.md. --- README.md | 90 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 66 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index b6b1ca9..af846ec 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Beta versions should not be used in applications which require user authenticati Provide your API-KEY/API-SECRET, and manage your account and funds at your own risk. ### Features + - User-friendly implementations for 75+ public and authenticated REST endpoints. * A complete list of available REST endpoints can be found [here](https://docs.bitfinex.com/reference). - New WebSocket client to ensure fast, secure and persistent connections. @@ -19,27 +20,72 @@ Provide your API-KEY/API-SECRET, and manage your account and funds at your own r - Full type hinting and type checking support with [`mypy`](https://github.com/python/mypy). * This allow text editors to show helpful hints about the value of a variable: ![example](https://i.imgur.com/aDjapcN.png "Type-hinting example on a random code snippet") ---- - ## Installation -To install the latest beta release of `bitfinex-api-py`: ```console python3 -m pip install --pre bitfinex-api-py ``` -To install a specific beta version: + +### Selecting and installing a specific beta version + +It's also possible to select and install a specific beta version: ```console python3 -m pip install bitfinex-api-py==3.0.0b1 ``` -## Basic usage - --- -## Index +# Quickstart + +```python +from bfxapi import Client, REST_HOST + +from bfxapi.rest.types import Notification, Order + +bfx = Client( + rest_host=REST_HOST, + api_key="", + api_secret="" +) + +notification: Notification[Order] = bfx.rest.auth.submit_order( + type="EXCHANGE LIMIT", symbol="tBTCUSD", amount=0.165212, price=30264.0) + +order: Order = notification.data + +if notification.status == "SUCCESS": + print(f"Successful new order for {order.symbol} at {order.price}$.") + +if notification.status == "ERROR": + raise Exception(f"Something went wrong: {notification.text}") +``` + +## Authenticating in your account + +To authenticate in your account, you must provide a valid API-KEY and API-SECRET: +```python +bfx = Client( + [...], + api_key=os.getenv("BFX_API_KEY"), + api_secret=os.getenv("BFX_API_SECRET") +) +``` + +### Warning + +Remember to not share your API-KEYs and API-SECRETs with anyone. \ +Everyone who owns one of your API-KEYs and API-SECRETs will have full access to your account. \ +We suggest saving your credentials in a local `.env` file and accessing them as environment variables. + +_Revoke your API-KEYs and API-SECRETs immediately if you think they might have been stolen._ + +> **NOTE:** A guide on how to create, edit and revoke API-KEYs and API-SECRETs can be found [here](https://support.bitfinex.com/hc/en-us/articles/115003363429-How-to-create-and-revoke-a-Bitfinex-API-Key). + +## Next * [WebSocket client documentation](#websocket-client-documentation) - [Advanced features](#advanced-features) + - [Examples](#examples) * [How to contribute](#how-to-contribute) --- @@ -56,9 +102,11 @@ python3 -m pip install bitfinex-api-py==3.0.0b1 4. [Listening to events](#listening-to-events) ### Advanced features -* [Sending custom notifications](#sending-custom-notifications) +* [Using custom notifications](#using-custom-notifications) * [Setting up connection multiplexing](#setting-up-connection-multiplexing) +#### Examples + ## Instantiating the client ```python @@ -80,18 +128,11 @@ PUB_WSS_HOST is recommended over WSS_HOST for applications that don't require au ### Authentication -Users can authenticate in their accounts by providing a pair of API-KEY and API-SECRET: -```python -bfx = Client( - [...], - api_key=os.getenv("BFX_API_KEY"), - api_secret=os.getenv("BFX_API_SECRET") -) -``` +To learn how to authenticate in your account, have a look at [Authenticating in your account](#authenticating-in-your-account). -If authentication succeeds, the client will emit the `authenticated` event. \ +If authentication is successful, the client will emit the `authenticated` event. \ All operations that require authentication will fail if run before the emission of this event. \ -The `data` argument contains various information about the authentication, such as the `userId`, the `auth_id`, etc... +The `data` argument contains information about the authentication, such as the `userId`, the `auth_id`, etc... ```python @bfx.wss.on("authenticated") @@ -99,7 +140,7 @@ def on_authenticated(data: Dict[str, Any]): print(f"Successful login for user <{data['userId']}>.") ``` -`data` can also be useful for checking API-KEY's permissions: +`data` can also be useful for checking if an API-KEY has certain permissions: ```python @bfx.wss.on("authenticated") @@ -111,8 +152,6 @@ def on_authenticated(data: Dict[str, Any]): raise Exception("This application requires write permissions on positions.") ``` -> **NOTE:** A guide on how to create, edit and revoke API-KEYs and API-SECRETs can be found [here](https://support.bitfinex.com/hc/en-us/articles/115003363429-How-to-create-and-revoke-a-Bitfinex-API-Key). - ## Running the client The client can be run using `BfxWebSocketClient::run`: @@ -214,9 +253,9 @@ bfx.wss.on("t_ticker_update", "f_ticker_update", callback=on_ticker_update) # Advanced features -## Sending custom notifications +## Using custom notifications -**Sending custom notifications requires user authentication.** +**Using custom notifications requires user authentication.** Users can send custom notifications using `BfxWebSocketClient::notify`: ```python @@ -255,13 +294,16 @@ Keep in mind that using a large number of connections could slow down the client The use of more than 20 connections is not recommended. +# Examples + + + --- # How to contribute All contributions are welcome! :D - A guide on how to install and set up `bitfinex-api-py`'s source code can be found [here](#installation-and-setup). \ Before opening any pull requests, please have a look at [Before Opening a PR](#before-opening-a-pr). \ Contributors must uphold the [Contributor Covenant code of conduct](https://github.com/bitfinexcom/bitfinex-api-py/blob/v3-beta/CODE_OF_CONDUCT.md). From 0f95a4ce27eefd41782575b86c5f156a49b76897 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Sun, 16 Apr 2023 08:58:36 +0200 Subject: [PATCH 238/265] Write `Examples` sub-section and `Creating a new order` paragraph in README.md. --- README.md | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index af846ec..9e48ede 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,8 @@ _Revoke your API-KEYs and API-SECRETs immediately if you think they might have b * [Using custom notifications](#using-custom-notifications) * [Setting up connection multiplexing](#setting-up-connection-multiplexing) -#### Examples +### Examples +* [Creating a new order](#creating-a-new-order) ## Instantiating the client @@ -296,7 +297,37 @@ The use of more than 20 connections is not recommended. # Examples +## Creating a new order +```python +import os + +from bfxapi import Client, WSS_HOST + +from bfxapi.websocket.types import Notification, Order + +bfx = Client( + wss_host=WSS_HOST, + api_key=os.getenv("BFX_API_KEY"), + api_secret=os.getenv("BFX_API_SECRET") +) + +@bfx.wss.on("authenticated") +async def on_authenticated(_): + await bfx.wss.inputs.submit_order( + type="EXCHANGE LIMIT", symbol="tBTCUSD", amount=0.165212, price=30264.0) + +@bfx.wss.on("order_new") +def on_order_new(order: Order): + print(f"Successful new order for {order.symbol} at {order.price}$.") + +@bfx.wss.on("on-req-notification") +def on_notification(notification: Notification[Order]): + if notification.status == "ERROR": + raise Exception(f"Something went wrong: {notification.text}") + +bfx.wss.run() +``` --- From e6b5b329d537829fa184ffd681216d731abadd50 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Sun, 16 Apr 2023 18:11:37 +0200 Subject: [PATCH 239/265] Rewrite `Features` sub-paragraph in README.md. --- README.md | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 9e48ede..97d4553 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,10 @@ Provide your API-KEY/API-SECRET, and manage your account and funds at your own r ### Features -- User-friendly implementations for 75+ public and authenticated REST endpoints. - * A complete list of available REST endpoints can be found [here](https://docs.bitfinex.com/reference). -- New WebSocket client to ensure fast, secure and persistent connections. - * Support for all public channels + authenticated events and inputs (a list can be found [here](https://docs.bitfinex.com/docs/ws-public)). - * Automatic reconnection system in case of network failure (both client and server side). - - The WebSocket client logs every reconnection failure, success and attempt (as well as other events). - * Connection multiplexing to allow subscribing to a large number of public channels (without affecting performances). - - The WebSocket server sets a limit of 25 subscriptions per connection, connection multiplexing allows the WebSocket client to bypass this limit. -- Full type hinting and type checking support with [`mypy`](https://github.com/python/mypy). - * This allow text editors to show helpful hints about the value of a variable: ![example](https://i.imgur.com/aDjapcN.png "Type-hinting example on a random code snippet") +* Support for 75+ REST endpoints (a list of available endpoints can be found [here](https://docs.bitfinex.com/reference)) +* New WebSocket client to ensure fast, secure and persistent connections +* Full support for Bitfinex notifications (including custom notifications) +* Native support for type hinting and type checking with [`mypy`](https://github.com/python/mypy) ## Installation From 734375ec9f0fad2090170d5640f640e940571b1d Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Sun, 16 Apr 2023 20:04:46 +0200 Subject: [PATCH 240/265] Add and implement wss_timeout in BfxWebsocketClient (and Client). --- bfxapi/client.py | 2 ++ .../websocket/client/bfx_websocket_bucket.py | 2 +- .../websocket/client/bfx_websocket_client.py | 23 +++++++++++++++---- bfxapi/websocket/exceptions.py | 6 +++++ 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/bfxapi/client.py b/bfxapi/client.py index db54b73..dd64d06 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -13,6 +13,7 @@ class Client: *, rest_host: str = REST_HOST, wss_host: str = WSS_HOST, + wss_timeout: int = 60 * 15, log_filename: Optional[str] = None, log_level: str = "INFO" ): @@ -29,6 +30,7 @@ class Client: self.wss = BfxWebsocketClient( host=wss_host, credentials=credentials, + wss_timeout=wss_timeout, log_filename=log_filename, log_level=log_level ) diff --git a/bfxapi/websocket/client/bfx_websocket_bucket.py b/bfxapi/websocket/client/bfx_websocket_bucket.py index 2d5e248..01a117c 100644 --- a/bfxapi/websocket/client/bfx_websocket_bucket.py +++ b/bfxapi/websocket/client/bfx_websocket_bucket.py @@ -70,7 +70,7 @@ class BfxWebsocketBucket: if (chan_id := message[0]) and message[1] != _HEARTBEAT: self.handler.handle(self.subscriptions[chan_id], *message[1:]) except websockets.ConnectionClosedError as error: - if error.code == 1006: + if error.code in (1006, 1012): self.on_open_event.clear() reconnection = True continue diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 5071ab6..18747d5 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -13,7 +13,7 @@ from .bfx_websocket_bucket import _HEARTBEAT, F, _require_websocket_connection, from .bfx_websocket_inputs import BfxWebsocketInputs from ..handlers import PublicChannelsHandler, AuthenticatedChannelsHandler from ..exceptions import WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, \ - ZeroConnectionsError, OutdatedClientVersion + ZeroConnectionsError, ReconnectionTimeoutError, OutdatedClientVersion from ...utils.json_encoder import JSONEncoder @@ -61,10 +61,12 @@ class BfxWebsocketClient: *AuthenticatedChannelsHandler.EVENTS ] - def __init__(self, host, credentials = None, log_filename = None, log_level = "INFO"): + def __init__(self, host, credentials = None, wss_timeout = 60 * 15, log_filename = None, log_level = "INFO"): self.websocket, self.buckets, self.authentication = None, [], False - self.host, self.credentials, self.event_emitter = host, credentials, AsyncIOEventEmitter() + self.host, self.credentials, self.wss_timeout = host, credentials, wss_timeout + + self.event_emitter = AsyncIOEventEmitter() self.inputs = BfxWebsocketInputs(handle_websocket_input=self.__handle_websocket_input) @@ -102,8 +104,14 @@ class BfxWebsocketClient: #pylint: disable-next=too-many-statements async def __connect(self): Reconnection = namedtuple("Reconnection", ["status", "attempts", "timestamp"]) + reconnection = Reconnection(status=False, attempts=0, timestamp=None) + delay, timer, on_timeout_event = None, None, asyncio.locks.Event() - reconnection, delay = Reconnection(status=False, attempts=0, timestamp=None), None + def _on_timeout(): + on_timeout_event.set() + + raise ReconnectionTimeoutError("Connection has been offline for too long " \ + f"without being able to reconnect (wss_timeout is set to {self.wss_timeout}s).") async def _connection(): nonlocal reconnection @@ -116,6 +124,8 @@ class BfxWebsocketClient: reconnection = Reconnection(status=False, attempts=0, timestamp=None) + timer.cancel() + self.websocket, self.authentication = websocket, False if len(self.buckets) == 0 or \ @@ -158,6 +168,9 @@ class BfxWebsocketClient: if reconnection.status: await asyncio.sleep(delay.next()) + if on_timeout_event.is_set(): + break + try: await _connection() except (websockets.ConnectionClosedError, socket.gaierror) as error: @@ -172,6 +185,8 @@ class BfxWebsocketClient: reconnection = Reconnection(status=True, attempts=1, timestamp=datetime.now()) + timer = asyncio.get_event_loop().call_later(self.wss_timeout, _on_timeout) + delay = _Delay(backoff_factor=1.618) elif isinstance(error, socket.gaierror) and reconnection.status: self.logger.warning(f"Reconnection attempt no.{reconnection.attempts} has failed. " \ diff --git a/bfxapi/websocket/exceptions.py b/bfxapi/websocket/exceptions.py index e1ff53e..a130831 100644 --- a/bfxapi/websocket/exceptions.py +++ b/bfxapi/websocket/exceptions.py @@ -6,6 +6,7 @@ __all__ = [ "ConnectionNotOpen", "TooManySubscriptions", "ZeroConnectionsError", + "ReconnectionTimeoutError", "WebsocketAuthenticationRequired", "InvalidAuthenticationCredentials", "EventNotSupported", @@ -33,6 +34,11 @@ class ZeroConnectionsError(BfxWebsocketException): This error indicates an attempt to subscribe to a public channel while the number of connections is 0. """ +class ReconnectionTimeoutError(BfxWebsocketException): + """ + This error indicates that the connection has been offline for too long without being able to reconnect. + """ + class WebsocketAuthenticationRequired(BfxWebsocketException): """ This error indicates an attempt to access a protected resource without logging in first. From 6d868a828763afd87886ca1916260ee08f34985a Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Sun, 16 Apr 2023 21:01:36 +0200 Subject: [PATCH 241/265] Fix bug about wss_timeout by changing reconnection logic (in BfxWebsocketClient and BfxWebsocketBucket). --- .../websocket/client/bfx_websocket_bucket.py | 41 ++++++++----------- .../websocket/client/bfx_websocket_client.py | 21 ++++++---- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/bfxapi/websocket/client/bfx_websocket_bucket.py b/bfxapi/websocket/client/bfx_websocket_bucket.py index 01a117c..035abf2 100644 --- a/bfxapi/websocket/client/bfx_websocket_bucket.py +++ b/bfxapi/websocket/client/bfx_websocket_bucket.py @@ -32,23 +32,12 @@ class BfxWebsocketBucket: self.handler = PublicChannelsHandler(event_emitter=self.event_emitter) async def connect(self): - reconnection = False + async def _connection(): + async with websockets.connect(self.host) as websocket: + self.websocket = websocket + self.on_open_event.set() + await self.__recover_state() - async for websocket in websockets.connect(self.host): - self.websocket = websocket - - self.on_open_event.set() - - if reconnection or (reconnection := False): - for pending in self.pendings: - await self.websocket.send(json.dumps(pending)) - - for _, subscription in self.subscriptions.items(): - await self.subscribe(**subscription) - - self.subscriptions.clear() - - try: async for message in websocket: message = json.loads(message) @@ -69,15 +58,21 @@ class BfxWebsocketBucket: if isinstance(message, list): if (chan_id := message[0]) and message[1] != _HEARTBEAT: self.handler.handle(self.subscriptions[chan_id], *message[1:]) - except websockets.ConnectionClosedError as error: - if error.code in (1006, 1012): - self.on_open_event.clear() - reconnection = True - continue - raise error + try: + await _connection() + except websockets.ConnectionClosedError as error: + if error.code in (1006, 1012): + self.on_open_event.clear() - break + async def __recover_state(self): + 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() @_require_websocket_connection async def subscribe(self, channel, sub_id=None, **kwargs): diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 18747d5..58cb817 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -61,7 +61,7 @@ class BfxWebsocketClient: *AuthenticatedChannelsHandler.EVENTS ] - def __init__(self, host, credentials = None, wss_timeout = 60 * 15, log_filename = None, log_level = "INFO"): + def __init__(self, host, credentials, *, wss_timeout = 60 * 15, log_filename = None, log_level = "INFO"): self.websocket, self.buckets, self.authentication = None, [], False self.host, self.credentials, self.wss_timeout = host, credentials, wss_timeout @@ -97,15 +97,15 @@ class BfxWebsocketClient: for _ in range(connections): self.buckets += [BfxWebsocketBucket(self.host, self.event_emitter)] - tasks = [ bucket.connect() for bucket in self.buckets ] + [ self.__connect() ] - - await asyncio.gather(*tasks) + await self.__connect() #pylint: disable-next=too-many-statements async def __connect(self): Reconnection = namedtuple("Reconnection", ["status", "attempts", "timestamp"]) reconnection = Reconnection(status=False, attempts=0, timestamp=None) - delay, timer, on_timeout_event = None, None, asyncio.locks.Event() + delay, timer, tasks = None, None, [] + + on_timeout_event = asyncio.locks.Event() def _on_timeout(): on_timeout_event.set() @@ -114,7 +114,7 @@ class BfxWebsocketClient: f"without being able to reconnect (wss_timeout is set to {self.wss_timeout}s).") async def _connection(): - nonlocal reconnection + nonlocal reconnection, timer, tasks async with websockets.connect(self.host) as websocket: if reconnection.status: @@ -128,6 +128,10 @@ class BfxWebsocketClient: self.websocket, self.authentication = websocket, False + coroutines = [ BfxWebsocketBucket.connect(bucket) for bucket in self.buckets ] + + tasks = [ asyncio.create_task(coroutine) for coroutine in coroutines ] + if len(self.buckets) == 0 or \ (await asyncio.gather(*[bucket.on_open_event.wait() for bucket in self.buckets])): self.event_emitter.emit("open") @@ -183,10 +187,11 @@ class BfxWebsocketClient: self.logger.info("WSS server is about to restart, reconnection " \ "required (client received 20051). Attempt in progress...") + for task in tasks: + task.cancel() + reconnection = Reconnection(status=True, attempts=1, timestamp=datetime.now()) - timer = asyncio.get_event_loop().call_later(self.wss_timeout, _on_timeout) - delay = _Delay(backoff_factor=1.618) elif isinstance(error, socket.gaierror) and reconnection.status: self.logger.warning(f"Reconnection attempt no.{reconnection.attempts} has failed. " \ From e15b52eabd109c47725edc8c4afdf2fefda99aa8 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Sun, 16 Apr 2023 21:16:18 +0200 Subject: [PATCH 242/265] Fix bug in BfxWebsocketClient::close (bfxapi.websocket.client.bfx_websocket_client). --- bfxapi/websocket/client/bfx_websocket_bucket.py | 2 +- bfxapi/websocket/client/bfx_websocket_client.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bfxapi/websocket/client/bfx_websocket_bucket.py b/bfxapi/websocket/client/bfx_websocket_bucket.py index 035abf2..6757ce6 100644 --- a/bfxapi/websocket/client/bfx_websocket_bucket.py +++ b/bfxapi/websocket/client/bfx_websocket_bucket.py @@ -61,7 +61,7 @@ class BfxWebsocketBucket: try: await _connection() - except websockets.ConnectionClosedError as error: + except websockets.exceptions.ConnectionClosedError as error: if error.code in (1006, 1012): self.on_open_event.clear() diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 58cb817..e78d0ea 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -152,7 +152,7 @@ class BfxWebsocketClient: rcvd = websockets.frames.Close(code=1012, reason="Stop/Restart Websocket Server (please reconnect).") - raise websockets.ConnectionClosedError(rcvd=rcvd, sent=None) + raise websockets.exceptions.ConnectionClosedError(rcvd=rcvd, sent=None) elif message["event"] == "auth": if message["status"] != "OK": raise InvalidAuthenticationCredentials( @@ -177,8 +177,8 @@ class BfxWebsocketClient: try: await _connection() - except (websockets.ConnectionClosedError, socket.gaierror) as error: - if isinstance(error, websockets.ConnectionClosedError) and error.code in (1006, 1012): + except (websockets.exceptions.ConnectionClosedError, socket.gaierror) as error: + if isinstance(error, websockets.exceptions.ConnectionClosedError) and error.code in (1006, 1012): if error.code == 1006: self.logger.error("Connection lost: no close frame received " \ "or sent (1006). Attempting to reconnect...") @@ -235,12 +235,12 @@ class BfxWebsocketClient: await bucket.unsubscribe(chan_id=chan_id) async def close(self, code=1000, reason=str()): - if self.websocket is not None and self.websocket.open: - await self.websocket.close(code=code, reason=reason) - for bucket in self.buckets: await bucket.close(code=code, reason=reason) + if self.websocket is not None and self.websocket.open: + await self.websocket.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 } ])) From 986aa525d7e5995ed7734b3314509418efba41fd Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 19 Apr 2023 03:29:45 +0200 Subject: [PATCH 243/265] Change wss_timeout type from int to float. --- bfxapi/client.py | 2 +- bfxapi/websocket/client/bfx_websocket_client.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bfxapi/client.py b/bfxapi/client.py index dd64d06..8fe3f12 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -13,7 +13,7 @@ class Client: *, rest_host: str = REST_HOST, wss_host: str = WSS_HOST, - wss_timeout: int = 60 * 15, + wss_timeout: float = 60 * 15, log_filename: Optional[str] = None, log_level: str = "INFO" ): diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index e78d0ea..ba2f70c 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -103,15 +103,15 @@ class BfxWebsocketClient: async def __connect(self): Reconnection = namedtuple("Reconnection", ["status", "attempts", "timestamp"]) reconnection = Reconnection(status=False, attempts=0, timestamp=None) - delay, timer, tasks = None, None, [] + delay = _Delay(backoff_factor=1.618) + + timer, tasks, on_timeout_event = None, [], asyncio.locks.Event() - on_timeout_event = asyncio.locks.Event() - - def _on_timeout(): + def _on_wss_timeout(): on_timeout_event.set() raise ReconnectionTimeoutError("Connection has been offline for too long " \ - f"without being able to reconnect (wss_timeout is set to {self.wss_timeout}s).") + f"without being able to reconnect (wss_timeout: {self.wss_timeout}s).") async def _connection(): nonlocal reconnection, timer, tasks @@ -191,8 +191,8 @@ class BfxWebsocketClient: task.cancel() reconnection = Reconnection(status=True, attempts=1, timestamp=datetime.now()) - timer = asyncio.get_event_loop().call_later(self.wss_timeout, _on_timeout) - delay = _Delay(backoff_factor=1.618) + + timer = asyncio.get_event_loop().call_later(self.wss_timeout, _on_wss_timeout) elif isinstance(error, socket.gaierror) and reconnection.status: self.logger.warning(f"Reconnection attempt no.{reconnection.attempts} has failed. " \ f"Next reconnection attempt in ~{round(delay.peek()):.1f} seconds. (at the moment " \ From 3441d2af2ff9c37d0555710cc7272b8582ea96b5 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 19 Apr 2023 03:53:33 +0200 Subject: [PATCH 244/265] Emit on_disconnection event on connection close in BfxWebsocketClient. --- .../websocket/client/bfx_websocket_client.py | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index ba2f70c..60445b3 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -56,7 +56,7 @@ class BfxWebsocketClient: MAXIMUM_CONNECTIONS_AMOUNT = 20 EVENTS = [ - "open", "subscribed", "authenticated", "wss-error", + "open", "subscribed", "authenticated", "disconnection", "wss-error", *PublicChannelsHandler.EVENTS, *AuthenticatedChannelsHandler.EVENTS ] @@ -68,10 +68,10 @@ class BfxWebsocketClient: self.event_emitter = AsyncIOEventEmitter() - self.inputs = BfxWebsocketInputs(handle_websocket_input=self.__handle_websocket_input) - self.handler = AuthenticatedChannelsHandler(event_emitter=self.event_emitter) + self.inputs = BfxWebsocketInputs(handle_websocket_input=self.__handle_websocket_input) + if log_filename is None: self.logger = ColorLogger("BfxWebsocketClient", level=log_level) else: self.logger = FileLogger("BfxWebsocketClient", level=log_level, filename=log_filename) @@ -104,7 +104,7 @@ class BfxWebsocketClient: Reconnection = namedtuple("Reconnection", ["status", "attempts", "timestamp"]) reconnection = Reconnection(status=False, attempts=0, timestamp=None) delay = _Delay(backoff_factor=1.618) - + timer, tasks, on_timeout_event = None, [], asyncio.locks.Event() def _on_wss_timeout(): @@ -178,21 +178,22 @@ class BfxWebsocketClient: try: await _connection() except (websockets.exceptions.ConnectionClosedError, socket.gaierror) as error: - if isinstance(error, websockets.exceptions.ConnectionClosedError) and error.code in (1006, 1012): - if error.code == 1006: - self.logger.error("Connection lost: no close frame received " \ - "or sent (1006). Attempting to reconnect...") + if isinstance(error, websockets.exceptions.ConnectionClosedError): + if error.code in (1006, 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...") + if error.code == 1012: + self.logger.info("WSS server is about to restart, reconnection " \ + "required (client received 20051). Attempt in progress...") - for task in tasks: - task.cancel() + for task in tasks: + task.cancel() - reconnection = Reconnection(status=True, attempts=1, timestamp=datetime.now()) + reconnection = Reconnection(status=True, attempts=1, timestamp=datetime.now()) - timer = asyncio.get_event_loop().call_later(self.wss_timeout, _on_wss_timeout) + timer = asyncio.get_event_loop().call_later(self.wss_timeout, _on_wss_timeout) elif isinstance(error, socket.gaierror) and reconnection.status: self.logger.warning(f"Reconnection attempt no.{reconnection.attempts} has failed. " \ f"Next reconnection attempt in ~{round(delay.peek()):.1f} seconds. (at the moment " \ @@ -202,6 +203,9 @@ class BfxWebsocketClient: else: raise error if not reconnection.status: + self.event_emitter.emit("disconnection", + self.websocket.close_code, self.websocket.close_reason) + break async def __authenticate(self, api_key, api_secret, filters=None): From 3d9e7c7b2577a94747377d7186fc660a9ad073d9 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 19 Apr 2023 04:20:53 +0200 Subject: [PATCH 245/265] Remove BfxWebsocketClient::once. Replace all occurrences with BfxWebsocketClient::on. Add BfxWebsocketClient::ONCE_EVENTS class variable. --- .../websocket/client/bfx_websocket_client.py | 36 +++++++------------ .../websocket/public/derivatives_status.py | 2 +- examples/websocket/public/ticker.py | 2 +- examples/websocket/public/trades.py | 2 +- 4 files changed, 15 insertions(+), 27 deletions(-) diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 60445b3..7fb2661 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -55,8 +55,10 @@ class BfxWebsocketClient: MAXIMUM_CONNECTIONS_AMOUNT = 20 + ONCE_EVENTS = [ "open", "authenticated", "disconnection" ] + EVENTS = [ - "open", "subscribed", "authenticated", "disconnection", "wss-error", + *ONCE_EVENTS, "subscribed", "wss-error", *PublicChannelsHandler.EVENTS, *AuthenticatedChannelsHandler.EVENTS ] @@ -110,9 +112,6 @@ class BfxWebsocketClient: def _on_wss_timeout(): on_timeout_event.set() - raise ReconnectionTimeoutError("Connection has been offline for too long " \ - f"without being able to reconnect (wss_timeout: {self.wss_timeout}s).") - async def _connection(): nonlocal reconnection, timer, tasks @@ -173,7 +172,8 @@ class BfxWebsocketClient: await asyncio.sleep(delay.next()) if on_timeout_event.is_set(): - break + raise ReconnectionTimeoutError("Connection has been offline for too long " \ + f"without being able to reconnect (wss_timeout: {self.wss_timeout}s).") try: await _connection() @@ -259,30 +259,18 @@ class BfxWebsocketClient: raise EventNotSupported(f"Event <{event}> is not supported. To get a list " \ "of available events print BfxWebsocketClient.EVENTS") + def _register_event(event, function): + if event in BfxWebsocketClient.ONCE_EVENTS: + self.event_emitter.once(event, function) + else: self.event_emitter.on(event, function) + if callback is not None: for event in events: - self.event_emitter.on(event, callback) + _register_event(event, callback) if callback is 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 is not None: - for event in events: - self.event_emitter.once(event, callback) - - if callback is None: - def handler(function): - for event in events: - self.event_emitter.once(event, function) + _register_event(event, function) return handler diff --git a/examples/websocket/public/derivatives_status.py b/examples/websocket/public/derivatives_status.py index 3609cff..9212909 100644 --- a/examples/websocket/public/derivatives_status.py +++ b/examples/websocket/public/derivatives_status.py @@ -16,7 +16,7 @@ def on_derivatives_status_update(subscription: subscriptions.Status, data: Deriv def on_wss_error(code: Error, msg: str): print(code, msg) -@bfx.wss.once("open") +@bfx.wss.on("open") async def on_open(): await bfx.wss.subscribe(Channel.STATUS, key="deriv:tBTCF0:USTF0") diff --git a/examples/websocket/public/ticker.py b/examples/websocket/public/ticker.py index c446cd0..8a55aa4 100644 --- a/examples/websocket/public/ticker.py +++ b/examples/websocket/public/ticker.py @@ -14,7 +14,7 @@ def on_t_ticker_update(subscription: subscriptions.Ticker, data: TradingPairTick print(f"Data: {data}") -@bfx.wss.once("open") +@bfx.wss.on("open") async def on_open(): await bfx.wss.subscribe(Channel.TICKER, symbol="tBTCUSD") diff --git a/examples/websocket/public/trades.py b/examples/websocket/public/trades.py index 9edd20e..0495ba0 100644 --- a/examples/websocket/public/trades.py +++ b/examples/websocket/public/trades.py @@ -20,7 +20,7 @@ def on_t_trade_executed(_sub: subscriptions.Trades, trade: TradingPairTrade): def on_wss_error(code: Error, msg: str): print(code, msg) -@bfx.wss.once("open") +@bfx.wss.on("open") async def on_open(): await bfx.wss.subscribe(Channel.CANDLES, key="trade:1m:tBTCUSD") From 3de6eee337461dc52842db3f28cd58a7c2035fb0 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 19 Apr 2023 04:29:26 +0200 Subject: [PATCH 246/265] Rename AuthenticatedChannelsHandler to AuthenticatedEventsHandler (and bfxapi.websocket.handlers.authenticated_channels_handler to authenticated_events_handler). --- bfxapi/websocket/client/bfx_websocket_client.py | 6 +++--- bfxapi/websocket/handlers/__init__.py | 2 +- ..._channels_handler.py => authenticated_events_handler.py} | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) rename bfxapi/websocket/handlers/{authenticated_channels_handler.py => authenticated_events_handler.py} (93%) diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 7fb2661..d90230f 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -11,7 +11,7 @@ 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 ..handlers import PublicChannelsHandler, AuthenticatedEventsHandler from ..exceptions import WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, \ ZeroConnectionsError, ReconnectionTimeoutError, OutdatedClientVersion @@ -60,7 +60,7 @@ class BfxWebsocketClient: EVENTS = [ *ONCE_EVENTS, "subscribed", "wss-error", *PublicChannelsHandler.EVENTS, - *AuthenticatedChannelsHandler.EVENTS + *AuthenticatedEventsHandler.EVENTS ] def __init__(self, host, credentials, *, wss_timeout = 60 * 15, log_filename = None, log_level = "INFO"): @@ -70,7 +70,7 @@ class BfxWebsocketClient: self.event_emitter = AsyncIOEventEmitter() - self.handler = AuthenticatedChannelsHandler(event_emitter=self.event_emitter) + self.handler = AuthenticatedEventsHandler(event_emitter=self.event_emitter) self.inputs = BfxWebsocketInputs(handle_websocket_input=self.__handle_websocket_input) diff --git a/bfxapi/websocket/handlers/__init__.py b/bfxapi/websocket/handlers/__init__.py index d55ea1e..98dadbb 100644 --- a/bfxapi/websocket/handlers/__init__.py +++ b/bfxapi/websocket/handlers/__init__.py @@ -1,2 +1,2 @@ from .public_channels_handler import PublicChannelsHandler -from .authenticated_channels_handler import AuthenticatedChannelsHandler +from .authenticated_events_handler import AuthenticatedEventsHandler diff --git a/bfxapi/websocket/handlers/authenticated_channels_handler.py b/bfxapi/websocket/handlers/authenticated_events_handler.py similarity index 93% rename from bfxapi/websocket/handlers/authenticated_channels_handler.py rename to bfxapi/websocket/handlers/authenticated_events_handler.py index 221217e..6fd42c4 100644 --- a/bfxapi/websocket/handlers/authenticated_channels_handler.py +++ b/bfxapi/websocket/handlers/authenticated_events_handler.py @@ -4,7 +4,7 @@ from .. serializers import _Notification from .. exceptions import HandlerNotFound -class AuthenticatedChannelsHandler: +class AuthenticatedEventsHandler: __abbreviations = { "os": "order_snapshot", "on": "order_new", "ou": "order_update", "oc": "order_cancel", "ps": "position_snapshot", "pn": "position_new", @@ -43,9 +43,9 @@ class AuthenticatedChannelsHandler: if abbrevation == "n": return self.__notification(stream) - for abbrevations, serializer in AuthenticatedChannelsHandler.__serializers.items(): + for abbrevations, serializer in AuthenticatedEventsHandler.__serializers.items(): if abbrevation in abbrevations: - event = AuthenticatedChannelsHandler.__abbreviations[abbrevation] + event = AuthenticatedEventsHandler.__abbreviations[abbrevation] if all(isinstance(substream, list) for substream in stream): return self.event_emitter.emit(event, [ serializer.parse(*substream) for substream in stream ]) From 7f788dd239fe6b8f69426872a3a08680476ec1ba Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 19 Apr 2023 18:05:27 +0200 Subject: [PATCH 247/265] Improve reconnections by not emitting againg once events. --- .../websocket/client/bfx_websocket_bucket.py | 21 ++++--- .../websocket/client/bfx_websocket_client.py | 26 ++++++--- .../handlers/authenticated_events_handler.py | 41 ++++++++----- .../handlers/public_channels_handler.py | 57 +++++++++++++------ 4 files changed, 95 insertions(+), 50 deletions(-) diff --git a/bfxapi/websocket/client/bfx_websocket_bucket.py b/bfxapi/websocket/client/bfx_websocket_bucket.py index 6757ce6..ee9b5f9 100644 --- a/bfxapi/websocket/client/bfx_websocket_bucket.py +++ b/bfxapi/websocket/client/bfx_websocket_bucket.py @@ -24,12 +24,13 @@ class BfxWebsocketBucket: MAXIMUM_SUBSCRIPTIONS_AMOUNT = 25 - def __init__(self, host, event_emitter): - self.host, self.event_emitter, self.on_open_event = host, event_emitter, asyncio.locks.Event() - + def __init__(self, host, event_emitter, events_per_subscription): + self.host, self.event_emitter, self.events_per_subscription = host, event_emitter, events_per_subscription self.websocket, self.subscriptions, self.pendings = None, {}, [] + self.on_open_event = asyncio.locks.Event() - self.handler = PublicChannelsHandler(event_emitter=self.event_emitter) + self.handler = PublicChannelsHandler(event_emitter=self.event_emitter, \ + events_per_subscription=self.events_per_subscription) async def connect(self): async def _connection(): @@ -43,12 +44,16 @@ class BfxWebsocketBucket: if isinstance(message, dict): if message["event"] == "subscribed" and (chan_id := 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[chan_id] = message - self.event_emitter.emit("subscribed", message) + sub_id = message["subId"] + + if "subscribed" not in self.events_per_subscription.get(sub_id, []): + self.events_per_subscription.setdefault(sub_id, []).append("subscribed") + self.event_emitter.emit("subscribed", message) elif message["event"] == "unsubscribed" and (chan_id := message["chanId"]): if message["status"] == "OK": del self.subscriptions[chan_id] @@ -70,7 +75,7 @@ class BfxWebsocketBucket: await self.websocket.send(json.dumps(pending)) for _, subscription in self.subscriptions.items(): - await self.subscribe(**subscription) + await self.subscribe(sub_id=subscription.pop("subId"), **subscription) self.subscriptions.clear() diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index d90230f..f02b391 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -55,19 +55,25 @@ class BfxWebsocketClient: MAXIMUM_CONNECTIONS_AMOUNT = 20 - ONCE_EVENTS = [ "open", "authenticated", "disconnection" ] + ONCE_EVENTS = [ + "open", "authenticated", "disconnection", + *AuthenticatedEventsHandler.ONCE_EVENTS + ] EVENTS = [ - *ONCE_EVENTS, "subscribed", "wss-error", + "subscribed", "wss-error", + *ONCE_EVENTS, *PublicChannelsHandler.EVENTS, - *AuthenticatedEventsHandler.EVENTS + *AuthenticatedEventsHandler.ON_EVENTS ] def __init__(self, host, credentials, *, wss_timeout = 60 * 15, log_filename = None, log_level = "INFO"): - self.websocket, self.buckets, self.authentication = None, [], False + self.websocket, self.authentication, self.buckets = None, False, [] self.host, self.credentials, self.wss_timeout = host, credentials, wss_timeout + self.events_per_subscription = {} + self.event_emitter = AsyncIOEventEmitter() self.handler = AuthenticatedEventsHandler(event_emitter=self.event_emitter) @@ -97,7 +103,7 @@ class BfxWebsocketClient: "block the client with <429 Too Many Requests>.") for _ in range(connections): - self.buckets += [BfxWebsocketBucket(self.host, self.event_emitter)] + self.buckets += [BfxWebsocketBucket(self.host, self.event_emitter, self.events_per_subscription)] await self.__connect() @@ -105,10 +111,10 @@ class BfxWebsocketClient: async def __connect(self): Reconnection = namedtuple("Reconnection", ["status", "attempts", "timestamp"]) reconnection = Reconnection(status=False, attempts=0, timestamp=None) - delay = _Delay(backoff_factor=1.618) - timer, tasks, on_timeout_event = None, [], asyncio.locks.Event() + delay = None + def _on_wss_timeout(): on_timeout_event.set() @@ -125,7 +131,7 @@ class BfxWebsocketClient: timer.cancel() - self.websocket, self.authentication = websocket, False + self.websocket = websocket coroutines = [ BfxWebsocketBucket.connect(bucket) for bucket in self.buckets ] @@ -192,8 +198,10 @@ class BfxWebsocketClient: task.cancel() reconnection = Reconnection(status=True, attempts=1, timestamp=datetime.now()) - timer = asyncio.get_event_loop().call_later(self.wss_timeout, _on_wss_timeout) + delay = _Delay(backoff_factor=1.618) + + self.authentication = False elif isinstance(error, socket.gaierror) and reconnection.status: self.logger.warning(f"Reconnection attempt no.{reconnection.attempts} has failed. " \ f"Next reconnection attempt in ~{round(delay.peek()):.1f} seconds. (at the moment " \ diff --git a/bfxapi/websocket/handlers/authenticated_events_handler.py b/bfxapi/websocket/handlers/authenticated_events_handler.py index 6fd42c4..34bd2d0 100644 --- a/bfxapi/websocket/handlers/authenticated_events_handler.py +++ b/bfxapi/websocket/handlers/authenticated_events_handler.py @@ -5,16 +5,24 @@ from .. serializers import _Notification from .. exceptions import HandlerNotFound class AuthenticatedEventsHandler: - __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", + __once_abbreviations = { + "os": "order_snapshot", "ps": "position_snapshot", "fos": "funding_offer_snapshot", + "fcs": "funding_credit_snapshot", "fls": "funding_loan_snapshot", "ws": "wallet_snapshot" + } + + __on_abbreviations = { + "on": "order_new", "ou": "order_update", "oc": "order_cancel", + "pn": "position_new", "pu": "position_update", "pc": "position_close", + "fon": "funding_offer_new", "fou": "funding_offer_update", "foc": "funding_offer_cancel", "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", + "fln": "funding_loan_new", "flu": "funding_loan_update", "flc": "funding_loan_close", + "te": "trade_executed", "tu": "trade_execution_update", "wu": "wallet_update", + "bu": "balance_update" + } + + __abbreviations = { + **__once_abbreviations, + **__on_abbreviations } __serializers = { @@ -28,12 +36,15 @@ class AuthenticatedEventsHandler: ("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()) + ONCE_EVENTS = [ + *list(__once_abbreviations.values()) + ] + + ON_EVENTS = [ + *list(__on_abbreviations.values()), + "notification", "on-req-notification", "ou-req-notification", + "oc-req-notification", "oc_multi-notification", "fon-req-notification", + "foc-req-notification" ] def __init__(self, event_emitter): diff --git a/bfxapi/websocket/handlers/public_channels_handler.py b/bfxapi/websocket/handlers/public_channels_handler.py index 8439d52..86c3376 100644 --- a/bfxapi/websocket/handlers/public_channels_handler.py +++ b/bfxapi/websocket/handlers/public_channels_handler.py @@ -3,15 +3,23 @@ from .. import serializers from .. exceptions import HandlerNotFound class PublicChannelsHandler: - EVENTS = [ - "t_ticker_update", "f_ticker_update", "t_trade_executed", "t_trade_execution_update", "f_trade_executed", - "f_trade_execution_update", "t_trades_snapshot", "f_trades_snapshot", "t_book_snapshot", "f_book_snapshot", - "t_raw_book_snapshot", "f_raw_book_snapshot", "t_book_update", "f_book_update", "t_raw_book_update", - "f_raw_book_update", "candles_snapshot", "candles_update", "derivatives_status_update", + ONCE_PER_SUBSCRIPTION_EVENTS = [ + "t_trades_snapshot", "f_trades_snapshot", "t_book_snapshot", + "f_book_snapshot", "t_raw_book_snapshot", "f_raw_book_snapshot", + "candles_snapshot" ] - def __init__(self, event_emitter): - self.event_emitter = event_emitter + EVENTS = [ + *ONCE_PER_SUBSCRIPTION_EVENTS, + "t_ticker_update", "f_ticker_update", "t_trade_executed", + "t_trade_execution_update", "f_trade_executed", "f_trade_execution_update", + "t_book_update", "f_book_update", "t_raw_book_update", + "f_raw_book_update", "candles_update", "derivatives_status_update" + ] + + def __init__(self, event_emitter, events_per_subscription): + self.__event_emitter, self.__events_per_subscription = \ + event_emitter, events_per_subscription self.__handlers = { "ticker": self.__ticker_channel_handler, @@ -31,16 +39,29 @@ class PublicChannelsHandler: raise HandlerNotFound(f"No handler found for channel <{subscription['channel']}>.") + def __emit(self, event, sub, data): + sub_id, should_emit_event = sub["subId"], True + + if event in PublicChannelsHandler.ONCE_PER_SUBSCRIPTION_EVENTS: + if sub_id not in self.__events_per_subscription: + self.__events_per_subscription[sub_id] = [ event ] + elif event not in self.__events_per_subscription[sub_id]: + self.__events_per_subscription[sub_id] += [ event ] + else: should_emit_event = False + + if should_emit_event: + return self.__event_emitter.emit(event, sub, data) + def __ticker_channel_handler(self, subscription, *stream): if subscription["symbol"].startswith("t"): - return self.event_emitter.emit( + return self.__emit( "t_ticker_update", subscription, serializers.TradingPairTicker.parse(*stream[0]) ) if subscription["symbol"].startswith("f"): - return self.event_emitter.emit( + return self.__emit( "f_ticker_update", subscription, serializers.FundingCurrencyTicker.parse(*stream[0]) @@ -49,28 +70,28 @@ class PublicChannelsHandler: def __trades_channel_handler(self, subscription, *stream): if (event := stream[0]) and event in [ "te", "tu", "fte", "ftu" ]: if subscription["symbol"].startswith("t"): - return self.event_emitter.emit( + return self.__emit( { "te": "t_trade_executed", "tu": "t_trade_execution_update" }[event], subscription, serializers.TradingPairTrade.parse(*stream[1]) ) if subscription["symbol"].startswith("f"): - return self.event_emitter.emit( + return self.__emit( { "fte": "f_trade_executed", "ftu": "f_trade_execution_update" }[event], subscription, serializers.FundingCurrencyTrade.parse(*stream[1]) ) if subscription["symbol"].startswith("t"): - return self.event_emitter.emit( + return self.__emit( "t_trades_snapshot", subscription, [ serializers.TradingPairTrade.parse(*substream) for substream in stream[0] ] ) if subscription["symbol"].startswith("f"): - return self.event_emitter.emit( + return self.__emit( "f_trades_snapshot", subscription, [ serializers.FundingCurrencyTrade.parse(*substream) for substream in stream[0] ] @@ -86,14 +107,14 @@ class PublicChannelsHandler: serializers.TradingPairBook, serializers.FundingCurrencyBook, False if all(isinstance(substream, list) for substream in stream[0]): - return self.event_emitter.emit( + return self.__emit( event + "_" + (is_raw_book and "raw_book" or "book") + "_snapshot", subscription, [ { "t": _trading_pair_serializer, "f": _funding_currency_serializer }[event] \ .parse(*substream) for substream in stream[0] ] ) - return self.event_emitter.emit( + return self.__emit( event + "_" + (is_raw_book and "raw_book" or "book") + "_update", subscription, { "t": _trading_pair_serializer, "f": _funding_currency_serializer }[event].parse(*stream[0]) @@ -101,13 +122,13 @@ class PublicChannelsHandler: def __candles_channel_handler(self, subscription, *stream): if all(isinstance(substream, list) for substream in stream[0]): - return self.event_emitter.emit( + return self.__emit( "candles_snapshot", subscription, [ serializers.Candle.parse(*substream) for substream in stream[0] ] ) - return self.event_emitter.emit( + return self.__emit( "candles_update", subscription, serializers.Candle.parse(*stream[0]) @@ -115,7 +136,7 @@ class PublicChannelsHandler: def __status_channel_handler(self, subscription, *stream): if subscription["key"].startswith("deriv:"): - return self.event_emitter.emit( + return self.__emit( "derivatives_status_update", subscription, serializers.DerivativesStatus.parse(*stream[0]) From 7231c4846578c9c3425a1e2e85b2482e84a66105 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 20 Apr 2023 01:04:43 +0200 Subject: [PATCH 248/265] Improve log messages on reconnection in BfxWebsocketClient (bfxapi.websocket.client.bfx_websocket_client). --- bfxapi/websocket/client/bfx_websocket_client.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index f02b391..2c01fb8 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -121,11 +121,11 @@ class BfxWebsocketClient: async def _connection(): nonlocal reconnection, timer, tasks - async with websockets.connect(self.host) as websocket: + async with websockets.connect(self.host, ping_interval=None) as websocket: if reconnection.status: - self.logger.info(f"Reconnect attempt successful (attempt no.{reconnection.attempts}): The " \ + self.logger.info(f"Reconnection attempt successful (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}).") + f"(connection lost on: {reconnection.timestamp:%d-%m-%Y at %H:%M:%S}).") reconnection = Reconnection(status=False, attempts=0, timestamp=None) @@ -188,11 +188,11 @@ class BfxWebsocketClient: if error.code in (1006, 1012): if error.code == 1006: self.logger.error("Connection lost: no close frame received " \ - "or sent (1006). Attempting to reconnect...") + "or sent (1006). Trying to reconnect...") if error.code == 1012: - self.logger.info("WSS server is about to restart, reconnection " \ - "required (client received 20051). Attempt in progress...") + self.logger.info("WSS server is about to restart, clients need " \ + "to reconnect (server sent 20051). Reconnection attempt in progress...") for task in tasks: task.cancel() @@ -203,8 +203,8 @@ class BfxWebsocketClient: self.authentication = False elif isinstance(error, socket.gaierror) and reconnection.status: - self.logger.warning(f"Reconnection attempt no.{reconnection.attempts} has failed. " \ - f"Next reconnection attempt in ~{round(delay.peek()):.1f} seconds. (at the moment " \ + self.logger.warning(f"Reconnection attempt was unsuccessful (no.{reconnection.attempts}). " \ + f"Next reconnection attempt in {delay.peek():.2f} seconds. (at the moment " \ f"the client has been offline for {datetime.now() - reconnection.timestamp})") reconnection = reconnection._replace(attempts=reconnection.attempts + 1) From 92d6630013effc1a9a9c57534db26b057b872b3d Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 20 Apr 2023 01:06:54 +0200 Subject: [PATCH 249/265] Remove oc_multi-notification event from AuthenticatedEventsHandler (bfxapi.websocket.handlers.authenticated_events_handler). --- bfxapi/websocket/handlers/authenticated_events_handler.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/bfxapi/websocket/handlers/authenticated_events_handler.py b/bfxapi/websocket/handlers/authenticated_events_handler.py index 34bd2d0..289fe60 100644 --- a/bfxapi/websocket/handlers/authenticated_events_handler.py +++ b/bfxapi/websocket/handlers/authenticated_events_handler.py @@ -43,8 +43,7 @@ class AuthenticatedEventsHandler: ON_EVENTS = [ *list(__on_abbreviations.values()), "notification", "on-req-notification", "ou-req-notification", - "oc-req-notification", "oc_multi-notification", "fon-req-notification", - "foc-req-notification" + "oc-req-notification", "fon-req-notification", "foc-req-notification" ] def __init__(self, event_emitter): @@ -71,9 +70,6 @@ class AuthenticatedEventsHandler: if stream[1] == "on-req" or stream[1] == "ou-req" or stream[1] == "oc-req": event, serializer = f"{stream[1]}-notification", _Notification(serializer=serializers.Order) - if stream[1] == "oc_multi-req": - event, serializer = f"{stream[1]}-notification", _Notification(serializer=serializers.Order, iterate=True) - if stream[1] == "fon-req" or stream[1] == "foc-req": event, serializer = f"{stream[1]}-notification", _Notification(serializer=serializers.FundingOffer) From 99783c1a5a2301509f0be1c98b9af2954b2fb181 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 20 Apr 2023 01:26:25 +0200 Subject: [PATCH 250/265] Allow wss_timeout=None to disable timeout on reconnection. --- bfxapi/client.py | 6 +++--- bfxapi/websocket/client/bfx_websocket_client.py | 8 ++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/bfxapi/client.py b/bfxapi/client.py index 8fe3f12..d4ab10b 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Literal, Optional from .rest import BfxRestInterface from .websocket import BfxWebsocketClient @@ -13,9 +13,9 @@ class Client: *, rest_host: str = REST_HOST, wss_host: str = WSS_HOST, - wss_timeout: float = 60 * 15, + wss_timeout: Optional[float] = 60 * 15, log_filename: Optional[str] = None, - log_level: str = "INFO" + log_level: Literal["ERROR", "WARNING", "INFO", "DEBUG"] = "INFO" ): credentials = None diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 2c01fb8..5b73e11 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -129,7 +129,8 @@ class BfxWebsocketClient: reconnection = Reconnection(status=False, attempts=0, timestamp=None) - timer.cancel() + if isinstance(timer, asyncio.events.TimerHandle): + timer.cancel() self.websocket = websocket @@ -198,7 +199,10 @@ class BfxWebsocketClient: task.cancel() reconnection = Reconnection(status=True, attempts=1, timestamp=datetime.now()) - timer = asyncio.get_event_loop().call_later(self.wss_timeout, _on_wss_timeout) + + if self.wss_timeout is not None: + timer = asyncio.get_event_loop().call_later(self.wss_timeout, _on_wss_timeout) + delay = _Delay(backoff_factor=1.618) self.authentication = False From d80b8b87217071de12c8d42d045c3be34f79239e Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 20 Apr 2023 01:36:09 +0200 Subject: [PATCH 251/265] Rename BfxWebsocketClient, BfxWebsocketBucket and BfxWebsocketInputs to BfxWebSocketClient, BfxWebSocketBucket and BfxWebSocketInputs. --- bfxapi/client.py | 4 +- bfxapi/tests/__init__.py | 4 +- bfxapi/tests/test_websocket_serializers.py | 2 +- bfxapi/websocket/__init__.py | 2 +- bfxapi/websocket/client/__init__.py | 6 +-- .../websocket/client/bfx_websocket_bucket.py | 4 +- .../websocket/client/bfx_websocket_client.py | 38 +++++++++---------- .../websocket/client/bfx_websocket_inputs.py | 2 +- bfxapi/websocket/exceptions.py | 26 ++++++------- bfxapi/websocket/serializers.py | 4 +- bfxapi/websocket/types.py | 4 +- 11 files changed, 48 insertions(+), 48 deletions(-) diff --git a/bfxapi/client.py b/bfxapi/client.py index d4ab10b..dac3649 100644 --- a/bfxapi/client.py +++ b/bfxapi/client.py @@ -1,7 +1,7 @@ from typing import List, Literal, Optional from .rest import BfxRestInterface -from .websocket import BfxWebsocketClient +from .websocket import BfxWebSocketClient from .urls import REST_HOST, WSS_HOST class Client: @@ -27,7 +27,7 @@ class Client: credentials=credentials ) - self.wss = BfxWebsocketClient( + self.wss = BfxWebSocketClient( host=wss_host, credentials=credentials, wss_timeout=wss_timeout, diff --git a/bfxapi/tests/__init__.py b/bfxapi/tests/__init__.py index df78300..ac76a9b 100644 --- a/bfxapi/tests/__init__.py +++ b/bfxapi/tests/__init__.py @@ -1,14 +1,14 @@ import unittest from .test_rest_serializers import TestRestSerializers -from .test_websocket_serializers import TestWebsocketSerializers +from .test_websocket_serializers import TestWebSocketSerializers from .test_labeler import TestLabeler from .test_notification import TestNotification def suite(): return unittest.TestSuite([ unittest.makeSuite(TestRestSerializers), - unittest.makeSuite(TestWebsocketSerializers), + unittest.makeSuite(TestWebSocketSerializers), unittest.makeSuite(TestLabeler), unittest.makeSuite(TestNotification), ]) diff --git a/bfxapi/tests/test_websocket_serializers.py b/bfxapi/tests/test_websocket_serializers.py index b433868..d379c32 100644 --- a/bfxapi/tests/test_websocket_serializers.py +++ b/bfxapi/tests/test_websocket_serializers.py @@ -6,7 +6,7 @@ from ..labeler import _Type from ..websocket import serializers -class TestWebsocketSerializers(unittest.TestCase): +class TestWebSocketSerializers(unittest.TestCase): def test_websocket_serializers(self): for serializer in map(serializers.__dict__.get, serializers.__serializers__): self.assertTrue(issubclass(serializer.klass, _Type), diff --git a/bfxapi/websocket/__init__.py b/bfxapi/websocket/__init__.py index 1509ba7..52e603a 100644 --- a/bfxapi/websocket/__init__.py +++ b/bfxapi/websocket/__init__.py @@ -1 +1 @@ -from .client import BfxWebsocketClient, BfxWebsocketBucket, BfxWebsocketInputs +from .client import BfxWebSocketClient, BfxWebSocketBucket, BfxWebSocketInputs diff --git a/bfxapi/websocket/client/__init__.py b/bfxapi/websocket/client/__init__.py index e9acea9..05b843c 100644 --- a/bfxapi/websocket/client/__init__.py +++ b/bfxapi/websocket/client/__init__.py @@ -1,3 +1,3 @@ -from .bfx_websocket_client import BfxWebsocketClient -from .bfx_websocket_bucket import BfxWebsocketBucket -from .bfx_websocket_inputs import BfxWebsocketInputs +from .bfx_websocket_client import BfxWebSocketClient +from .bfx_websocket_bucket import BfxWebSocketBucket +from .bfx_websocket_inputs import BfxWebSocketInputs diff --git a/bfxapi/websocket/client/bfx_websocket_bucket.py b/bfxapi/websocket/client/bfx_websocket_bucket.py index ee9b5f9..500e9db 100644 --- a/bfxapi/websocket/client/bfx_websocket_bucket.py +++ b/bfxapi/websocket/client/bfx_websocket_bucket.py @@ -19,7 +19,7 @@ def _require_websocket_connection(function: F) -> F: return cast(F, wrapper) -class BfxWebsocketBucket: +class BfxWebSocketBucket: VERSION = 2 MAXIMUM_SUBSCRIPTIONS_AMOUNT = 25 @@ -81,7 +81,7 @@ class BfxWebsocketBucket: @_require_websocket_connection async def subscribe(self, channel, sub_id=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.") subscription = { diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 5b73e11..8971833 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -8,11 +8,11 @@ import traceback, json, asyncio, hmac, hashlib, time, socket, random, websockets from pyee.asyncio import AsyncIOEventEmitter -from .bfx_websocket_bucket import _HEARTBEAT, F, _require_websocket_connection, BfxWebsocketBucket +from .bfx_websocket_bucket import _HEARTBEAT, F, _require_websocket_connection, BfxWebSocketBucket -from .bfx_websocket_inputs import BfxWebsocketInputs +from .bfx_websocket_inputs import BfxWebSocketInputs from ..handlers import PublicChannelsHandler, AuthenticatedEventsHandler -from ..exceptions import WebsocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, \ +from ..exceptions import WebSocketAuthenticationRequired, InvalidAuthenticationCredentials, EventNotSupported, \ ZeroConnectionsError, ReconnectionTimeoutError, OutdatedClientVersion from ...utils.json_encoder import JSONEncoder @@ -22,7 +22,7 @@ from ...utils.logger import ColorLogger, FileLogger def _require_websocket_authentication(function: F) -> F: async def wrapper(self, *args, **kwargs): if hasattr(self, "authentication") and not self.authentication: - raise WebsocketAuthenticationRequired("To perform this action you need to " \ + 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) @@ -50,8 +50,8 @@ class _Delay: return (self.__backoff_delay == _Delay.BACKOFF_MIN) \ and self.__initial_delay or self.__backoff_delay -class BfxWebsocketClient: - VERSION = BfxWebsocketBucket.VERSION +class BfxWebSocketClient: + VERSION = BfxWebSocketBucket.VERSION MAXIMUM_CONNECTIONS_AMOUNT = 20 @@ -78,11 +78,11 @@ class BfxWebsocketClient: self.handler = AuthenticatedEventsHandler(event_emitter=self.event_emitter) - self.inputs = BfxWebsocketInputs(handle_websocket_input=self.__handle_websocket_input) + self.inputs = BfxWebSocketInputs(handle_websocket_input=self.__handle_websocket_input) if log_filename is None: - self.logger = ColorLogger("BfxWebsocketClient", level=log_level) - else: self.logger = FileLogger("BfxWebsocketClient", level=log_level, filename=log_filename) + self.logger = ColorLogger("BfxWebSocketClient", level=log_level) + else: self.logger = FileLogger("BfxWebSocketClient", level=log_level, filename=log_filename) self.event_emitter.add_listener("error", lambda exception: self.logger.error(f"{type(exception).__name__}: {str(exception)}" + "\n" + @@ -97,13 +97,13 @@ class BfxWebsocketClient: self.logger.info("With connections set to 0 it will not be possible to subscribe to any public channel. " \ "Attempting a subscription will cause a ZeroConnectionsError to be thrown.") - if connections > BfxWebsocketClient.MAXIMUM_CONNECTIONS_AMOUNT: - self.logger.warning(f"It is not safe to use more than {BfxWebsocketClient.MAXIMUM_CONNECTIONS_AMOUNT} " \ + if connections > BfxWebSocketClient.MAXIMUM_CONNECTIONS_AMOUNT: + self.logger.warning(f"It is not safe to use more than {BfxWebSocketClient.MAXIMUM_CONNECTIONS_AMOUNT} " \ f"buckets from the same connection ({connections} in use), the server could momentarily " \ "block the client with <429 Too Many Requests>.") for _ in range(connections): - self.buckets += [BfxWebsocketBucket(self.host, self.event_emitter, self.events_per_subscription)] + self.buckets += [BfxWebSocketBucket(self.host, self.event_emitter, self.events_per_subscription)] await self.__connect() @@ -134,7 +134,7 @@ class BfxWebsocketClient: self.websocket = websocket - coroutines = [ BfxWebsocketBucket.connect(bucket) for bucket in self.buckets ] + coroutines = [ BfxWebSocketBucket.connect(bucket) for bucket in self.buckets ] tasks = [ asyncio.create_task(coroutine) for coroutine in coroutines ] @@ -150,13 +150,13 @@ class BfxWebsocketClient: if isinstance(message, dict): if message["event"] == "info" and "version" in message: - if BfxWebsocketClient.VERSION != message["version"]: + if BfxWebSocketClient.VERSION != message["version"]: raise OutdatedClientVersion("Mismatch between the client version and the server " \ "version. Update the library to the latest version to continue (client version: " \ - f"{BfxWebsocketClient.VERSION}, server version: {message['version']}).") + f"{BfxWebSocketClient.VERSION}, server version: {message['version']}).") elif message["event"] == "info" and message["code"] == 20051: rcvd = websockets.frames.Close(code=1012, - reason="Stop/Restart Websocket Server (please reconnect).") + reason="Stop/Restart WebSocket Server (please reconnect).") raise websockets.exceptions.ConnectionClosedError(rcvd=rcvd, sent=None) elif message["event"] == "auth": @@ -267,12 +267,12 @@ class BfxWebsocketClient: def on(self, *events, callback = None): for event in events: - if event not in BfxWebsocketClient.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") + "of available events print BfxWebSocketClient.EVENTS") def _register_event(event, function): - if event in BfxWebsocketClient.ONCE_EVENTS: + if event in BfxWebSocketClient.ONCE_EVENTS: self.event_emitter.once(event, function) else: self.event_emitter.on(event, function) diff --git a/bfxapi/websocket/client/bfx_websocket_inputs.py b/bfxapi/websocket/client/bfx_websocket_inputs.py index f306bcb..aed658d 100644 --- a/bfxapi/websocket/client/bfx_websocket_inputs.py +++ b/bfxapi/websocket/client/bfx_websocket_inputs.py @@ -5,7 +5,7 @@ from typing import Union, Optional, List, Tuple from .. enums import OrderType, FundingOfferType from ...utils.json_encoder import JSON -class BfxWebsocketInputs: +class BfxWebSocketInputs: def __init__(self, handle_websocket_input): self.__handle_websocket_input = handle_websocket_input diff --git a/bfxapi/websocket/exceptions.py b/bfxapi/websocket/exceptions.py index a130831..cf229bc 100644 --- a/bfxapi/websocket/exceptions.py +++ b/bfxapi/websocket/exceptions.py @@ -1,66 +1,66 @@ from .. exceptions import BfxBaseException __all__ = [ - "BfxWebsocketException", + "BfxWebSocketException", "ConnectionNotOpen", "TooManySubscriptions", "ZeroConnectionsError", "ReconnectionTimeoutError", - "WebsocketAuthenticationRequired", + "WebSocketAuthenticationRequired", "InvalidAuthenticationCredentials", "EventNotSupported", "HandlerNotFound", "OutdatedClientVersion" ] -class BfxWebsocketException(BfxBaseException): +class BfxWebSocketException(BfxBaseException): """ Base class for all custom exceptions in bfxapi/websocket/exceptions.py. """ -class ConnectionNotOpen(BfxWebsocketException): +class ConnectionNotOpen(BfxWebSocketException): """ This error indicates an attempt to communicate via websocket before starting the connection with the servers. """ -class TooManySubscriptions(BfxWebsocketException): +class TooManySubscriptions(BfxWebSocketException): """ This error indicates a subscription attempt after reaching the limit of simultaneous connections. """ -class ZeroConnectionsError(BfxWebsocketException): +class ZeroConnectionsError(BfxWebSocketException): """ This error indicates an attempt to subscribe to a public channel while the number of connections is 0. """ -class ReconnectionTimeoutError(BfxWebsocketException): +class ReconnectionTimeoutError(BfxWebSocketException): """ This error indicates that the connection has been offline for too long without being able to reconnect. """ -class WebsocketAuthenticationRequired(BfxWebsocketException): +class WebSocketAuthenticationRequired(BfxWebSocketException): """ This error indicates an attempt to access a protected resource without logging in first. """ -class InvalidAuthenticationCredentials(BfxWebsocketException): +class InvalidAuthenticationCredentials(BfxWebSocketException): """ This error indicates that the user has provided incorrect credentials (API-KEY and API-SECRET) for authentication. """ -class EventNotSupported(BfxWebsocketException): +class EventNotSupported(BfxWebSocketException): """ - This error indicates a failed attempt to subscribe to an event not supported by the BfxWebsocketClient. + This error indicates a failed attempt to subscribe to an event not supported by the BfxWebSocketClient. """ -class HandlerNotFound(BfxWebsocketException): +class HandlerNotFound(BfxWebSocketException): """ This error indicates that a handler was not found for an incoming message. """ -class OutdatedClientVersion(BfxWebsocketException): +class OutdatedClientVersion(BfxWebSocketException): """ This error indicates a mismatch between the client version and the server WSS version. """ diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py index 464fdd9..be8e393 100644 --- a/bfxapi/websocket/serializers.py +++ b/bfxapi/websocket/serializers.py @@ -18,7 +18,7 @@ __serializers__ = [ "Wallet", "Balance", ] -#region Serializers definition for Websocket Public Channels +#region Serializers definition for WebSocket Public Channels TradingPairTicker = generate_labeler_serializer( name="TradingPairTicker", @@ -170,7 +170,7 @@ DerivativesStatus = generate_labeler_serializer( #endregion -#region Serializers definition for Websocket Authenticated Channels +#region Serializers definition for WebSocket Authenticated Channels Order = generate_labeler_serializer( name="Order", diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py index dcef21f..5c186f0 100644 --- a/bfxapi/websocket/types.py +++ b/bfxapi/websocket/types.py @@ -12,7 +12,7 @@ from .. notification import Notification from ..utils.json_encoder import JSON -#region Type hinting for Websocket Public Channels +#region Type hinting for WebSocket Public Channels @dataclass class TradingPairTicker(_Type): @@ -111,7 +111,7 @@ class DerivativesStatus(_Type): #endregion -#region Type hinting for Websocket Authenticated Channels +#region Type hinting for WebSocket Authenticated Channels @dataclass class Order(_Type): id: int From 1f9d1b719e2a1bdcc5818e3781433e2f8fc1fb43 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 20 Apr 2023 01:41:08 +0200 Subject: [PATCH 252/265] Fix bug in bfxapi.websocket.subscriptions. --- bfxapi/websocket/client/bfx_websocket_client.py | 3 ++- bfxapi/websocket/subscriptions.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/bfxapi/websocket/client/bfx_websocket_client.py b/bfxapi/websocket/client/bfx_websocket_client.py index 8971833..e0f90e2 100644 --- a/bfxapi/websocket/client/bfx_websocket_client.py +++ b/bfxapi/websocket/client/bfx_websocket_client.py @@ -107,7 +107,7 @@ class BfxWebSocketClient: await self.__connect() - #pylint: disable-next=too-many-statements + #pylint: disable-next=too-many-statements,too-many-branches async def __connect(self): Reconnection = namedtuple("Reconnection", ["status", "attempts", "timestamp"]) reconnection = Reconnection(status=False, attempts=0, timestamp=None) @@ -118,6 +118,7 @@ class BfxWebSocketClient: def _on_wss_timeout(): on_timeout_event.set() + #pylint: disable-next=too-many-branches async def _connection(): nonlocal reconnection, timer, tasks diff --git a/bfxapi/websocket/subscriptions.py b/bfxapi/websocket/subscriptions.py index 922a532..233becc 100644 --- a/bfxapi/websocket/subscriptions.py +++ b/bfxapi/websocket/subscriptions.py @@ -12,7 +12,7 @@ __all__ = [ _Header = TypedDict("_Header", { "event": Literal["subscribed"], "channel": str, "chanId": int }) -Subscription = Union["Ticker", "Trades", "Book", "Candles", "Status"] +Subscription = Union[_Header, "Ticker", "Trades", "Book", "Candles", "Status"] class Ticker(TypedDict): subId: str From 4edbf5d44c94bcba99b92163c1beb9aa415dacfa Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 20 Apr 2023 01:43:44 +0200 Subject: [PATCH 253/265] Change event trade_executed to trade_execution (and update references). --- bfxapi/websocket/handlers/authenticated_events_handler.py | 2 +- bfxapi/websocket/handlers/public_channels_handler.py | 8 ++++---- examples/websocket/public/trades.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bfxapi/websocket/handlers/authenticated_events_handler.py b/bfxapi/websocket/handlers/authenticated_events_handler.py index 289fe60..cf098a3 100644 --- a/bfxapi/websocket/handlers/authenticated_events_handler.py +++ b/bfxapi/websocket/handlers/authenticated_events_handler.py @@ -16,7 +16,7 @@ class AuthenticatedEventsHandler: "fon": "funding_offer_new", "fou": "funding_offer_update", "foc": "funding_offer_cancel", "fcn": "funding_credit_new", "fcu": "funding_credit_update", "fcc": "funding_credit_close", "fln": "funding_loan_new", "flu": "funding_loan_update", "flc": "funding_loan_close", - "te": "trade_executed", "tu": "trade_execution_update", "wu": "wallet_update", + "te": "trade_execution", "tu": "trade_execution_update", "wu": "wallet_update", "bu": "balance_update" } diff --git a/bfxapi/websocket/handlers/public_channels_handler.py b/bfxapi/websocket/handlers/public_channels_handler.py index 86c3376..e8cfaad 100644 --- a/bfxapi/websocket/handlers/public_channels_handler.py +++ b/bfxapi/websocket/handlers/public_channels_handler.py @@ -11,8 +11,8 @@ class PublicChannelsHandler: EVENTS = [ *ONCE_PER_SUBSCRIPTION_EVENTS, - "t_ticker_update", "f_ticker_update", "t_trade_executed", - "t_trade_execution_update", "f_trade_executed", "f_trade_execution_update", + "t_ticker_update", "f_ticker_update", "t_trade_execution", + "t_trade_execution_update", "f_trade_execution", "f_trade_execution_update", "t_book_update", "f_book_update", "t_raw_book_update", "f_raw_book_update", "candles_update", "derivatives_status_update" ] @@ -71,14 +71,14 @@ class PublicChannelsHandler: if (event := stream[0]) and event in [ "te", "tu", "fte", "ftu" ]: if subscription["symbol"].startswith("t"): return self.__emit( - { "te": "t_trade_executed", "tu": "t_trade_execution_update" }[event], + { "te": "t_trade_execution", "tu": "t_trade_execution_update" }[event], subscription, serializers.TradingPairTrade.parse(*stream[1]) ) if subscription["symbol"].startswith("f"): return self.__emit( - { "fte": "f_trade_executed", "ftu": "f_trade_execution_update" }[event], + { "fte": "f_trade_execution", "ftu": "f_trade_execution_update" }[event], subscription, serializers.FundingCurrencyTrade.parse(*stream[1]) ) diff --git a/examples/websocket/public/trades.py b/examples/websocket/public/trades.py index 0495ba0..1159ab4 100644 --- a/examples/websocket/public/trades.py +++ b/examples/websocket/public/trades.py @@ -12,8 +12,8 @@ bfx = Client(wss_host=PUB_WSS_HOST) def on_candles_update(_sub: subscriptions.Candles, candle: Candle): print(f"New candle: {candle}") -@bfx.wss.on("t_trade_executed") -def on_t_trade_executed(_sub: subscriptions.Trades, trade: TradingPairTrade): +@bfx.wss.on("t_trade_execution") +def on_t_trade_execution(_sub: subscriptions.Trades, trade: TradingPairTrade): print(f"New trade: {trade}") @bfx.wss.on("wss-error") From 6f207010e8595cc21c70298dfc3503b4e5f5ebce Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 20 Apr 2023 02:02:57 +0200 Subject: [PATCH 254/265] Remove bfxapi.utils.camel_and_snake_case_helpers and move code to bfxapi.rest.endpoints.rest_merchant_endpoints. --- .../rest/endpoints/rest_merchant_endpoints.py | 47 ++++++++++++++----- bfxapi/utils/camel_and_snake_case_helpers.py | 23 --------- 2 files changed, 36 insertions(+), 34 deletions(-) delete mode 100644 bfxapi/utils/camel_and_snake_case_helpers.py diff --git a/bfxapi/rest/endpoints/rest_merchant_endpoints.py b/bfxapi/rest/endpoints/rest_merchant_endpoints.py index bf806af..8677df8 100644 --- a/bfxapi/rest/endpoints/rest_merchant_endpoints.py +++ b/bfxapi/rest/endpoints/rest_merchant_endpoints.py @@ -1,4 +1,7 @@ -from typing import TypedDict, Dict, List, Union, Literal, Optional, Any +import re + +from typing import Callable, TypeVar, cast, \ + TypedDict, Dict, List, Union, Literal, Optional, Any from decimal import Decimal @@ -10,7 +13,29 @@ from .. enums import MerchantSettingsKey from .. middleware import Middleware -from ...utils.camel_and_snake_case_helpers import to_snake_case_keys, to_camel_case_keys +#region Defining methods to convert dictionary keys to snake_case and camelCase. + +T = TypeVar("T") + +_to_snake_case: Callable[[str], str] = lambda string: re.sub(r"(? T: + if isinstance(data, list): + return cast(T, [ _scheme(sub_data, adapter) for sub_data in data ]) + if isinstance(data, dict): + return cast(T, { adapter(key): _scheme(value, adapter) for key, value in data.items() }) + return data + +def _to_snake_case_keys(dictionary: T) -> T: + return _scheme(dictionary, _to_snake_case) + +def _to_camel_case_keys(dictionary: T) -> T: + return _scheme(dictionary, _to_camel_case) + +#endregion _CustomerInfo = TypedDict("_CustomerInfo", { "nationality": str, "resid_country": str, "resid_city": str, @@ -30,13 +55,13 @@ class RestMerchantEndpoints(Middleware): duration: Optional[int] = None, webhook: Optional[str] = None, redirect_url: Optional[str] = None) -> InvoiceSubmission: - body = to_camel_case_keys({ + body = _to_camel_case_keys({ "amount": amount, "currency": currency, "order_id": order_id, "customer_info": customer_info, "pay_currencies": pay_currencies, "duration": duration, "webhook": webhook, "redirect_url": redirect_url }) - data = to_snake_case_keys(self._post("auth/w/ext/pay/invoice/create", body=body)) + data = _to_snake_case_keys(self._post("auth/w/ext/pay/invoice/create", body=body)) return InvoiceSubmission.parse(data) @@ -53,7 +78,7 @@ class RestMerchantEndpoints(Middleware): response = self._post("auth/r/ext/pay/invoices", body=body) - return [ InvoiceSubmission.parse(sub_data) for sub_data in to_snake_case_keys(response) ] + return [ InvoiceSubmission.parse(sub_data) for sub_data in _to_snake_case_keys(response) ] def get_invoices_paginated(self, page: int = 1, @@ -66,13 +91,13 @@ class RestMerchantEndpoints(Middleware): crypto: Optional[List[str]] = None, id: Optional[str] = None, order_id: Optional[str] = None) -> InvoicePage: - body = to_camel_case_keys({ + body = _to_camel_case_keys({ "page": page, "page_size": page_size, "sort": sort, "sort_field": sort_field, "status": status, "fiat": fiat, "crypto": crypto, "id": id, "order_id": order_id }) - data = to_snake_case_keys(self._post("auth/r/ext/pay/invoices/paginated", body=body)) + data = _to_snake_case_keys(self._post("auth/r/ext/pay/invoices/paginated", body=body)) return InvoicePage.parse(data) @@ -94,7 +119,7 @@ class RestMerchantEndpoints(Middleware): *, deposit_id: Optional[int] = None, ledger_id: Optional[int] = None) -> InvoiceSubmission: - return InvoiceSubmission.parse(to_snake_case_keys(self._post("auth/w/ext/pay/invoice/complete", body={ + return InvoiceSubmission.parse(_to_snake_case_keys(self._post("auth/w/ext/pay/invoice/complete", body={ "id": id, "payCcy": pay_currency, "depositId": deposit_id, "ledgerId": ledger_id }))) @@ -102,7 +127,7 @@ class RestMerchantEndpoints(Middleware): def expire_invoice(self, id: str) -> InvoiceSubmission: body = { "id": id } response = self._post("auth/w/ext/pay/invoice/expire", body=body) - return InvoiceSubmission.parse(to_snake_case_keys(response)) + return InvoiceSubmission.parse(_to_snake_case_keys(response)) def get_currency_conversion_list(self) -> List[CurrencyConversion]: return [ @@ -148,7 +173,7 @@ class RestMerchantEndpoints(Middleware): unlinked: Optional[bool] = None) -> List[MerchantDeposit]: body = { "from": start, "to": end, "ccy": ccy, "unlinked": unlinked } response = self._post("auth/r/ext/pay/deposits", body=body) - return [ MerchantDeposit(**sub_data) for sub_data in to_snake_case_keys(response) ] + return [ MerchantDeposit(**sub_data) for sub_data in _to_snake_case_keys(response) ] def get_unlinked_deposits(self, ccy: str, @@ -157,4 +182,4 @@ class RestMerchantEndpoints(Middleware): end: Optional[int] = None) -> List[MerchantUnlinkedDeposit]: body = { "ccy": ccy, "start": start, "end": end } response = self._post("/auth/r/ext/pay/deposits/unlinked", body=body) - return [ MerchantUnlinkedDeposit(**sub_data) for sub_data in to_snake_case_keys(response) ] + return [ MerchantUnlinkedDeposit(**sub_data) for sub_data in _to_snake_case_keys(response) ] diff --git a/bfxapi/utils/camel_and_snake_case_helpers.py b/bfxapi/utils/camel_and_snake_case_helpers.py deleted file mode 100644 index 38a7993..0000000 --- a/bfxapi/utils/camel_and_snake_case_helpers.py +++ /dev/null @@ -1,23 +0,0 @@ -import re - -from typing import TypeVar, Callable, cast - -T = TypeVar("T") - -_to_snake_case: Callable[[str], str] = lambda string: re.sub(r"(? T: - if isinstance(data, list): - return cast(T, [ _scheme(sub_data, adapter) for sub_data in data ]) - if isinstance(data, dict): - return cast(T, { adapter(key): _scheme(value, adapter) for key, value in data.items() }) - return data - -def to_snake_case_keys(dictionary: T) -> T: - return _scheme(dictionary, _to_snake_case) - -def to_camel_case_keys(dictionary: T) -> T: - return _scheme(dictionary, _to_camel_case) From 34a1b0099e8714898cbd0ff7979d8240c1e467e2 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 20 Apr 2023 02:45:33 +0200 Subject: [PATCH 255/265] Improve some endpoints in RestPublicEndpoints (bfxapi.rest.endpoints.rest_public_endpoints). --- .../rest/endpoints/rest_public_endpoints.py | 64 +++++++++++-------- bfxapi/rest/serializers.py | 3 - bfxapi/rest/types.py | 3 - 3 files changed, 38 insertions(+), 32 deletions(-) diff --git a/bfxapi/rest/endpoints/rest_public_endpoints.py b/bfxapi/rest/endpoints/rest_public_endpoints.py index c80d86b..1c6a698 100644 --- a/bfxapi/rest/endpoints/rest_public_endpoints.py +++ b/bfxapi/rest/endpoints/rest_public_endpoints.py @@ -1,4 +1,4 @@ -from typing import List, Union, Literal, Optional, Any, cast +from typing import List, Dict, Union, Literal, Optional, Any, cast from decimal import Decimal @@ -22,37 +22,46 @@ class RestPublicEndpoints(Middleware): def get_platform_status(self) -> PlatformStatus: return serializers.PlatformStatus.parse(*self._get("platform/status")) - def get_tickers(self, symbols: List[str]) -> List[Union[TradingPairTicker, FundingCurrencyTicker]]: + def get_tickers(self, symbols: List[str]) -> Dict[str, Union[TradingPairTicker, FundingCurrencyTicker]]: data = self._get("tickers", params={ "symbols": ",".join(symbols) }) parsers = { "t": serializers.TradingPairTicker.parse, "f": serializers.FundingCurrencyTicker.parse } - return [ cast(Union[TradingPairTicker, FundingCurrencyTicker], \ - parsers[sub_data[0][0]](*sub_data)) for sub_data in data ] + return { + symbol: cast(Union[TradingPairTicker, FundingCurrencyTicker], + parsers[symbol[0]](*sub_data)) for sub_data in data + if (symbol := sub_data.pop(0)) + } - def get_t_tickers(self, pairs: Union[List[str], Literal["ALL"]]) -> List[TradingPairTicker]: - if isinstance(pairs, str) and pairs == "ALL": - return [ cast(TradingPairTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) \ - if cast(str, sub_data.symbol).startswith("t") ] + def get_t_tickers(self, symbols: Union[List[str], Literal["ALL"]]) -> Dict[str, TradingPairTicker]: + if isinstance(symbols, str) and symbols == "ALL": + return { + symbol: cast(TradingPairTicker, sub_data) + for symbol, sub_data in self.get_tickers([ "ALL" ]).items() + if symbol.startswith("t") + } - data = self.get_tickers(list(pairs)) + data = self.get_tickers(list(symbols)) - return cast(List[TradingPairTicker], data) + return cast(Dict[str, TradingPairTicker], data) - def get_f_tickers(self, currencies: Union[List[str], Literal["ALL"]]) -> List[FundingCurrencyTicker]: - if isinstance(currencies, str) and currencies == "ALL": - return [ cast(FundingCurrencyTicker, sub_data) for sub_data in self.get_tickers([ "ALL" ]) \ - if cast(str, sub_data.symbol).startswith("f") ] + def get_f_tickers(self, symbols: Union[List[str], Literal["ALL"]]) -> Dict[str, FundingCurrencyTicker]: + if isinstance(symbols, str) and symbols == "ALL": + return { + symbol: cast(FundingCurrencyTicker, sub_data) + for symbol, sub_data in self.get_tickers([ "ALL" ]).items() + if symbol.startswith("f") + } - data = self.get_tickers(list(currencies)) + data = self.get_tickers(list(symbols)) - return cast(List[FundingCurrencyTicker], data) + return cast(Dict[str, FundingCurrencyTicker], data) - def get_t_ticker(self, pair: str) -> TradingPairTicker: - return serializers.TradingPairTicker.parse(*([pair] + self._get(f"ticker/{pair}"))) + def get_t_ticker(self, symbol: str) -> TradingPairTicker: + return serializers.TradingPairTicker.parse(*self._get(f"ticker/{symbol}")) - def get_f_ticker(self, currency: str) -> FundingCurrencyTicker: - return serializers.FundingCurrencyTicker.parse(*([currency] + self._get(f"ticker/{currency}"))) + def get_f_ticker(self, symbol: str) -> FundingCurrencyTicker: + return serializers.FundingCurrencyTicker.parse(*self._get(f"ticker/{symbol}")) def get_tickers_history(self, symbols: List[str], @@ -164,26 +173,29 @@ class RestPublicEndpoints(Middleware): data = self._get(f"candles/trade:{tf}:{symbol}/last", params=params) return serializers.Candle.parse(*data) - def get_derivatives_status(self, keys: Union[List[str], Literal["ALL"]]) -> List[DerivativesStatus]: + def get_derivatives_status(self, keys: Union[List[str], Literal["ALL"]]) -> Dict[str, DerivativesStatus]: if keys == "ALL": params = { "keys": "ALL" } else: params = { "keys": ",".join(keys) } data = self._get("status/deriv", params=params) - return [ serializers.DerivativesStatus.parse(*sub_data) for sub_data in data ] + return { + key: serializers.DerivativesStatus.parse(*sub_data) + for sub_data in data + if (key := sub_data.pop(0)) + } def get_derivatives_status_history(self, - type: str, - symbol: str, + key: str, *, sort: Optional[Sort] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[DerivativesStatus]: params = { "sort": sort, "start": start, "end": end, "limit": limit } - data = self._get(f"status/{type}/{symbol}/hist", params=params) - return [ serializers.DerivativesStatus.parse(*([symbol] + sub_data)) for sub_data in data ] + data = self._get(f"status/deriv/{key}/hist", params=params) + return [ serializers.DerivativesStatus.parse(*sub_data) for sub_data in data ] def get_liquidations(self, *, diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 7cb8b71..f34ba5d 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -42,7 +42,6 @@ TradingPairTicker = generate_labeler_serializer( name="TradingPairTicker", klass=types.TradingPairTicker, labels=[ - "symbol", "bid", "bid_size", "ask", @@ -60,7 +59,6 @@ FundingCurrencyTicker = generate_labeler_serializer( name="FundingCurrencyTicker", klass=types.FundingCurrencyTicker, labels=[ - "symbol", "frr", "bid", "bid_period", @@ -191,7 +189,6 @@ DerivativesStatus = generate_labeler_serializer( name="DerivativesStatus", klass=types.DerivativesStatus, labels=[ - "key", "mts", "_PLACEHOLDER", "deriv_price", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index bea85c3..7a7ec7d 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -20,7 +20,6 @@ class PlatformStatus(_Type): @dataclass class TradingPairTicker(_Type): - symbol: str bid: float bid_size: float ask: float @@ -34,7 +33,6 @@ class TradingPairTicker(_Type): @dataclass class FundingCurrencyTicker(_Type): - symbol: str frr: float bid: float bid_period: int @@ -114,7 +112,6 @@ class Candle(_Type): @dataclass class DerivativesStatus(_Type): - key: str mts: int deriv_price: float spot_price: float From 0f9fa1bf6a25a9852e93eea343ba9066607bac0e Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 20 Apr 2023 03:44:13 +0200 Subject: [PATCH 256/265] Merge bfxapi.rest.types and bfxapi.websocket.types in bfxapi.tests sub-package. --- bfxapi/exceptions.py | 8 - .../endpoints/rest_authenticated_endpoints.py | 14 +- .../rest/endpoints/rest_merchant_endpoints.py | 8 +- .../rest/endpoints/rest_public_endpoints.py | 10 +- bfxapi/types/__init__.py | 26 ++ .../{rest/types.py => types/dataclasses.py} | 19 +- bfxapi/{ => types}/labeler.py | 7 +- bfxapi/{ => types}/notification.py | 2 +- bfxapi/{rest => types}/serializers.py | 115 +++--- .../handlers/authenticated_events_handler.py | 14 +- .../handlers/public_channels_handler.py | 4 +- bfxapi/websocket/serializers.py | 368 ------------------ bfxapi/websocket/types.py | 247 ------------ .../{authenticated => auth}/claim_position.py | 2 +- .../{authenticated => auth}/get_wallets.py | 7 +- .../set_derivative_position_collateral.py | 2 +- .../submit_funding_offer.py | 2 +- .../{authenticated => auth}/submit_order.py | 2 +- .../toggle_keep_funding.py | 4 +- examples/rest/merchant/submit_invoice.py | 2 +- examples/rest/public/book.py | 4 +- examples/rest/public/pulse_endpoints.py | 4 +- .../rest/public/rest_calculation_endpoints.py | 2 +- examples/rest/public/trades.py | 4 +- .../{authenticated => auth}/submit_order.py | 2 +- .../{authenticated => auth}/wallets.py | 2 +- .../websocket/public/derivatives_status.py | 8 +- examples/websocket/public/order_book.py | 8 +- examples/websocket/public/raw_order_book.py | 8 +- examples/websocket/public/ticker.py | 6 +- examples/websocket/public/trades.py | 10 +- setup.py | 2 +- 32 files changed, 164 insertions(+), 759 deletions(-) create mode 100644 bfxapi/types/__init__.py rename bfxapi/{rest/types.py => types/dataclasses.py} (96%) rename bfxapi/{ => types}/labeler.py (94%) rename bfxapi/{ => types}/notification.py (96%) rename bfxapi/{rest => types}/serializers.py (89%) delete mode 100644 bfxapi/websocket/serializers.py delete mode 100644 bfxapi/websocket/types.py rename examples/rest/{authenticated => auth}/claim_position.py (89%) rename examples/rest/{authenticated => auth}/get_wallets.py (90%) rename examples/rest/{authenticated => auth}/set_derivative_position_collateral.py (92%) rename examples/rest/{authenticated => auth}/submit_funding_offer.py (92%) rename examples/rest/{authenticated => auth}/submit_order.py (95%) rename examples/rest/{authenticated => auth}/toggle_keep_funding.py (89%) rename examples/websocket/{authenticated => auth}/submit_order.py (95%) rename examples/websocket/{authenticated => auth}/wallets.py (94%) diff --git a/bfxapi/exceptions.py b/bfxapi/exceptions.py index f7723c3..136f5f1 100644 --- a/bfxapi/exceptions.py +++ b/bfxapi/exceptions.py @@ -1,16 +1,8 @@ __all__ = [ "BfxBaseException", - - "LabelerSerializerException", ] class BfxBaseException(Exception): """ Base class for every custom exception in bfxapi/rest/exceptions.py and bfxapi/websocket/exceptions.py. """ - -class LabelerSerializerException(BfxBaseException): - """ - This exception indicates an error thrown by the _Serializer class in bfxapi/labeler.py. - """ - \ No newline at end of file diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index 8a2e61e..0247866 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -2,7 +2,11 @@ from typing import Dict, List, Tuple, Union, Literal, Optional from decimal import Decimal from datetime import datetime -from .. types import Notification, \ +from .. middleware import Middleware + +from .. enums import Sort, OrderType, FundingOfferType + +from ... types import JSON, Notification, \ UserInfo, LoginHistory, BalanceAvailable, \ Order, Position, Trade, \ FundingTrade, OrderTrade, Ledger, \ @@ -14,13 +18,9 @@ from .. types import Notification, \ PositionIncrease, PositionHistory, PositionSnapshot, \ PositionAudit, DerivativePositionCollateral, DerivativePositionCollateralLimits -from .. import serializers +from ... types import serializers -from .. serializers import _Notification -from .. enums import Sort, OrderType, FundingOfferType -from .. middleware import Middleware - -from ...utils.json_encoder import JSON +from ... types.serializers import _Notification class RestAuthenticatedEndpoints(Middleware): def get_user_info(self) -> UserInfo: diff --git a/bfxapi/rest/endpoints/rest_merchant_endpoints.py b/bfxapi/rest/endpoints/rest_merchant_endpoints.py index 8677df8..ff2c19d 100644 --- a/bfxapi/rest/endpoints/rest_merchant_endpoints.py +++ b/bfxapi/rest/endpoints/rest_merchant_endpoints.py @@ -5,13 +5,13 @@ from typing import Callable, TypeVar, cast, \ from decimal import Decimal -from .. types import \ - InvoiceSubmission, InvoicePage, InvoiceStats, \ - CurrencyConversion, MerchantDeposit, MerchantUnlinkedDeposit +from .. middleware import Middleware from .. enums import MerchantSettingsKey -from .. middleware import Middleware +from ... types import \ + InvoiceSubmission, InvoicePage, InvoiceStats, \ + CurrencyConversion, MerchantDeposit, MerchantUnlinkedDeposit #region Defining methods to convert dictionary keys to snake_case and camelCase. diff --git a/bfxapi/rest/endpoints/rest_public_endpoints.py b/bfxapi/rest/endpoints/rest_public_endpoints.py index 1c6a698..bb7ed2e 100644 --- a/bfxapi/rest/endpoints/rest_public_endpoints.py +++ b/bfxapi/rest/endpoints/rest_public_endpoints.py @@ -2,7 +2,11 @@ from typing import List, Dict, Union, Literal, Optional, Any, cast from decimal import Decimal -from .. types import \ +from .. middleware import Middleware + +from .. enums import Config, Sort + +from ... types import \ PlatformStatus, TradingPairTicker, FundingCurrencyTicker, \ TickersHistory, TradingPairTrade, FundingCurrencyTrade, \ TradingPairBook, FundingCurrencyBook, TradingPairRawBook, \ @@ -11,9 +15,7 @@ from .. types import \ FundingStatistic, PulseProfile, PulseMessage, \ TradingMarketAveragePrice, FundingMarketAveragePrice, FxRate -from .. import serializers -from .. enums import Config, Sort -from .. middleware import Middleware +from ... types import serializers class RestPublicEndpoints(Middleware): def conf(self, config: Config) -> Any: diff --git a/bfxapi/types/__init__.py b/bfxapi/types/__init__.py new file mode 100644 index 0000000..7dcac83 --- /dev/null +++ b/bfxapi/types/__init__.py @@ -0,0 +1,26 @@ +from . dataclasses import JSON, \ + PlatformStatus, TradingPairTicker, FundingCurrencyTicker, \ + TickersHistory, TradingPairTrade, FundingCurrencyTrade, \ + TradingPairBook, FundingCurrencyBook, TradingPairRawBook, \ + FundingCurrencyRawBook, Statistic, Candle, \ + DerivativesStatus, Liquidation, Leaderboard, \ + FundingStatistic, PulseProfile, PulseMessage, \ + TradingMarketAveragePrice, FundingMarketAveragePrice, FxRate + +from . dataclasses import \ + UserInfo, LoginHistory, BalanceAvailable, \ + Order, Position, Trade, \ + FundingTrade, OrderTrade, Ledger, \ + FundingOffer, FundingCredit, FundingLoan, \ + FundingAutoRenew, FundingInfo, Wallet, \ + Transfer, Withdrawal, DepositAddress, \ + LightningNetworkInvoice, Movement, SymbolMarginInfo, \ + BaseMarginInfo, PositionClaim, PositionIncreaseInfo, \ + PositionIncrease, PositionHistory, PositionSnapshot, \ + PositionAudit, DerivativePositionCollateral, DerivativePositionCollateralLimits + +from . dataclasses import \ + InvoiceSubmission, InvoicePage, InvoiceStats, \ + CurrencyConversion, MerchantDeposit, MerchantUnlinkedDeposit + +from . notification import Notification diff --git a/bfxapi/rest/types.py b/bfxapi/types/dataclasses.py similarity index 96% rename from bfxapi/rest/types.py rename to bfxapi/types/dataclasses.py index 7a7ec7d..e81d08c 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/types/dataclasses.py @@ -1,18 +1,13 @@ -#pylint: disable=duplicate-code - -#pylint: disable-next=wildcard-import,unused-wildcard-import -from typing import * +from typing import Union, Type, \ + List, Dict, Literal, Optional, Any from dataclasses import dataclass -from .. labeler import _Type, partial, compose +from . labeler import _Type, partial, compose -#pylint: disable-next=unused-import -from .. notification import Notification +JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] -from ..utils.json_encoder import JSON - -#region Type hinting for Rest Public Endpoints +#region Dataclass definitions for types of public use @dataclass class PlatformStatus(_Type): @@ -198,7 +193,7 @@ class FxRate(_Type): #endregion -#region Type hinting for Rest Authenticated Endpoints +#region Dataclass definitions for types of auth use @dataclass class UserInfo(_Type): @@ -565,7 +560,7 @@ class DerivativePositionCollateralLimits(_Type): #endregion -#region Type hinting for Rest Merchant Endpoints +#region Dataclass definitions for types of merchant use @compose(dataclass, partial) class InvoiceSubmission(_Type): diff --git a/bfxapi/labeler.py b/bfxapi/types/labeler.py similarity index 94% rename from bfxapi/labeler.py rename to bfxapi/types/labeler.py index dfb4881..52ac497 100644 --- a/bfxapi/labeler.py +++ b/bfxapi/types/labeler.py @@ -1,6 +1,5 @@ -from typing import Type, Generic, TypeVar, Iterable, Dict, List, Tuple, Any, cast - -from .exceptions import LabelerSerializerException +from typing import Type, Generic, TypeVar, Iterable, \ + Dict, List, Tuple, Any, cast T = TypeVar("T", bound="_Type") @@ -43,7 +42,7 @@ class _Serializer(Generic[T]): args = tuple(_Serializer.__flatten(list(args))) if len(self.__labels) > len(args): - raise LabelerSerializerException(f"{self.name} -> and <*args> " \ + raise AssertionError(f"{self.name} -> and <*args> " \ "arguments should contain the same amount of elements.") for index, label in enumerate(self.__labels): diff --git a/bfxapi/notification.py b/bfxapi/types/notification.py similarity index 96% rename from bfxapi/notification.py rename to bfxapi/types/notification.py index ae02259..87d37cd 100644 --- a/bfxapi/notification.py +++ b/bfxapi/types/notification.py @@ -1,6 +1,6 @@ from typing import List, Optional, Any, Generic, TypeVar, cast from dataclasses import dataclass -from .labeler import _Type, _Serializer +from . labeler import _Type, _Serializer T = TypeVar("T") diff --git a/bfxapi/rest/serializers.py b/bfxapi/types/serializers.py similarity index 89% rename from bfxapi/rest/serializers.py rename to bfxapi/types/serializers.py index f34ba5d..353915f 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/types/serializers.py @@ -1,11 +1,10 @@ -#pylint: disable=duplicate-code +from . import dataclasses -from . import types - -from .. labeler import generate_labeler_serializer, generate_recursive_serializer +from . labeler import \ + generate_labeler_serializer, generate_recursive_serializer #pylint: disable-next=unused-import -from .. notification import _Notification +from . notification import _Notification __serializers__ = [ "PlatformStatus", "TradingPairTicker", "FundingCurrencyTicker", @@ -28,11 +27,11 @@ __serializers__ = [ "PositionAudit", "DerivativePositionCollateral", "DerivativePositionCollateralLimits", ] -#region Serializers definition for Rest Public Endpoints +#region Serializer definitions for types of public use PlatformStatus = generate_labeler_serializer( name="PlatformStatus", - klass=types.PlatformStatus, + klass=dataclasses.PlatformStatus, labels=[ "status" ] @@ -40,7 +39,7 @@ PlatformStatus = generate_labeler_serializer( TradingPairTicker = generate_labeler_serializer( name="TradingPairTicker", - klass=types.TradingPairTicker, + klass=dataclasses.TradingPairTicker, labels=[ "bid", "bid_size", @@ -57,7 +56,7 @@ TradingPairTicker = generate_labeler_serializer( FundingCurrencyTicker = generate_labeler_serializer( name="FundingCurrencyTicker", - klass=types.FundingCurrencyTicker, + klass=dataclasses.FundingCurrencyTicker, labels=[ "frr", "bid", @@ -80,7 +79,7 @@ FundingCurrencyTicker = generate_labeler_serializer( TickersHistory = generate_labeler_serializer( name="TickersHistory", - klass=types.TickersHistory, + klass=dataclasses.TickersHistory, labels=[ "symbol", "bid", @@ -100,7 +99,7 @@ TickersHistory = generate_labeler_serializer( TradingPairTrade = generate_labeler_serializer( name="TradingPairTrade", - klass=types.TradingPairTrade, + klass=dataclasses.TradingPairTrade, labels=[ "id", "mts", @@ -111,7 +110,7 @@ TradingPairTrade = generate_labeler_serializer( FundingCurrencyTrade = generate_labeler_serializer( name="FundingCurrencyTrade", - klass=types.FundingCurrencyTrade, + klass=dataclasses.FundingCurrencyTrade, labels=[ "id", "mts", @@ -123,7 +122,7 @@ FundingCurrencyTrade = generate_labeler_serializer( TradingPairBook = generate_labeler_serializer( name="TradingPairBook", - klass=types.TradingPairBook, + klass=dataclasses.TradingPairBook, labels=[ "price", "count", @@ -133,7 +132,7 @@ TradingPairBook = generate_labeler_serializer( FundingCurrencyBook = generate_labeler_serializer( name="FundingCurrencyBook", - klass=types.FundingCurrencyBook, + klass=dataclasses.FundingCurrencyBook, labels=[ "rate", "period", @@ -144,7 +143,7 @@ FundingCurrencyBook = generate_labeler_serializer( TradingPairRawBook = generate_labeler_serializer( name="TradingPairRawBook", - klass=types.TradingPairRawBook, + klass=dataclasses.TradingPairRawBook, labels=[ "order_id", "price", @@ -154,7 +153,7 @@ TradingPairRawBook = generate_labeler_serializer( FundingCurrencyRawBook = generate_labeler_serializer( name="FundingCurrencyRawBook", - klass=types.FundingCurrencyRawBook, + klass=dataclasses.FundingCurrencyRawBook, labels=[ "offer_id", "period", @@ -165,7 +164,7 @@ FundingCurrencyRawBook = generate_labeler_serializer( Statistic = generate_labeler_serializer( name="Statistic", - klass=types.Statistic, + klass=dataclasses.Statistic, labels=[ "mts", "value" @@ -174,7 +173,7 @@ Statistic = generate_labeler_serializer( Candle = generate_labeler_serializer( name="Candle", - klass=types.Candle, + klass=dataclasses.Candle, labels=[ "mts", "open", @@ -187,7 +186,7 @@ Candle = generate_labeler_serializer( DerivativesStatus = generate_labeler_serializer( name="DerivativesStatus", - klass=types.DerivativesStatus, + klass=dataclasses.DerivativesStatus, labels=[ "mts", "_PLACEHOLDER", @@ -217,7 +216,7 @@ DerivativesStatus = generate_labeler_serializer( Liquidation = generate_labeler_serializer( name="Liquidation", - klass=types.Liquidation, + klass=dataclasses.Liquidation, labels=[ "_PLACEHOLDER", "pos_id", @@ -236,7 +235,7 @@ Liquidation = generate_labeler_serializer( Leaderboard = generate_labeler_serializer( name="Leaderboard", - klass=types.Leaderboard, + klass=dataclasses.Leaderboard, labels=[ "mts", "_PLACEHOLDER", @@ -253,7 +252,7 @@ Leaderboard = generate_labeler_serializer( FundingStatistic = generate_labeler_serializer( name="FundingStatistic", - klass=types.FundingStatistic, + klass=dataclasses.FundingStatistic, labels=[ "mts", "_PLACEHOLDER", @@ -272,7 +271,7 @@ FundingStatistic = generate_labeler_serializer( PulseProfile = generate_labeler_serializer( name="PulseProfile", - klass=types.PulseProfile, + klass=dataclasses.PulseProfile, labels=[ "puid", "mts", @@ -296,7 +295,7 @@ PulseProfile = generate_labeler_serializer( PulseMessage = generate_recursive_serializer( name="PulseMessage", - klass=types.PulseMessage, + klass=dataclasses.PulseMessage, serializers={ "profile": PulseProfile }, labels=[ "pid", @@ -326,7 +325,7 @@ PulseMessage = generate_recursive_serializer( TradingMarketAveragePrice = generate_labeler_serializer( name="TradingMarketAveragePrice", - klass=types.TradingMarketAveragePrice, + klass=dataclasses.TradingMarketAveragePrice, labels=[ "price_avg", "amount" @@ -335,7 +334,7 @@ TradingMarketAveragePrice = generate_labeler_serializer( FundingMarketAveragePrice = generate_labeler_serializer( name="FundingMarketAveragePrice", - klass=types.FundingMarketAveragePrice, + klass=dataclasses.FundingMarketAveragePrice, labels=[ "rate_avg", "amount" @@ -344,7 +343,7 @@ FundingMarketAveragePrice = generate_labeler_serializer( FxRate = generate_labeler_serializer( name="FxRate", - klass=types.FxRate, + klass=dataclasses.FxRate, labels=[ "current_rate" ] @@ -352,11 +351,11 @@ FxRate = generate_labeler_serializer( #endregion -#region Serializers definition for Rest Authenticated Endpoints +#region Serializer definitions for types of auth use UserInfo = generate_labeler_serializer( name="UserInfo", - klass=types.UserInfo, + klass=dataclasses.UserInfo, labels=[ "id", "email", @@ -418,7 +417,7 @@ UserInfo = generate_labeler_serializer( LoginHistory = generate_labeler_serializer( name="LoginHistory", - klass=types.LoginHistory, + klass=dataclasses.LoginHistory, labels=[ "id", "_PLACEHOLDER", @@ -433,7 +432,7 @@ LoginHistory = generate_labeler_serializer( BalanceAvailable = generate_labeler_serializer( name="BalanceAvailable", - klass=types.BalanceAvailable, + klass=dataclasses.BalanceAvailable, labels=[ "amount" ] @@ -441,7 +440,7 @@ BalanceAvailable = generate_labeler_serializer( Order = generate_labeler_serializer( name="Order", - klass=types.Order, + klass=dataclasses.Order, labels=[ "id", "gid", @@ -480,7 +479,7 @@ Order = generate_labeler_serializer( Position = generate_labeler_serializer( name="Position", - klass=types.Position, + klass=dataclasses.Position, labels=[ "symbol", "status", @@ -507,7 +506,7 @@ Position = generate_labeler_serializer( Trade = generate_labeler_serializer( name="Trade", - klass=types.Trade, + klass=dataclasses.Trade, labels=[ "id", "symbol", @@ -526,7 +525,7 @@ Trade = generate_labeler_serializer( FundingTrade = generate_labeler_serializer( name="FundingTrade", - klass=types.FundingTrade, + klass=dataclasses.FundingTrade, labels=[ "id", "currency", @@ -540,7 +539,7 @@ FundingTrade = generate_labeler_serializer( OrderTrade = generate_labeler_serializer( name="OrderTrade", - klass=types.OrderTrade, + klass=dataclasses.OrderTrade, labels=[ "id", "symbol", @@ -559,7 +558,7 @@ OrderTrade = generate_labeler_serializer( Ledger = generate_labeler_serializer( name="Ledger", - klass=types.Ledger, + klass=dataclasses.Ledger, labels=[ "id", "currency", @@ -575,7 +574,7 @@ Ledger = generate_labeler_serializer( FundingOffer = generate_labeler_serializer( name="FundingOffer", - klass=types.FundingOffer, + klass=dataclasses.FundingOffer, labels=[ "id", "symbol", @@ -603,7 +602,7 @@ FundingOffer = generate_labeler_serializer( FundingCredit = generate_labeler_serializer( name="FundingCredit", - klass=types.FundingCredit, + klass=dataclasses.FundingCredit, labels=[ "id", "symbol", @@ -632,7 +631,7 @@ FundingCredit = generate_labeler_serializer( FundingLoan = generate_labeler_serializer( name="FundingLoan", - klass=types.FundingLoan, + klass=dataclasses.FundingLoan, labels=[ "id", "symbol", @@ -660,7 +659,7 @@ FundingLoan = generate_labeler_serializer( FundingAutoRenew = generate_labeler_serializer( name="FundingAutoRenew", - klass=types.FundingAutoRenew, + klass=dataclasses.FundingAutoRenew, labels=[ "currency", "period", @@ -671,7 +670,7 @@ FundingAutoRenew = generate_labeler_serializer( FundingInfo = generate_labeler_serializer( name="FundingInfo", - klass=types.FundingInfo, + klass=dataclasses.FundingInfo, labels=[ "yield_loan", "yield_lend", @@ -682,7 +681,7 @@ FundingInfo = generate_labeler_serializer( Wallet = generate_labeler_serializer( name="Wallet", - klass=types.Wallet, + klass=dataclasses.Wallet, labels=[ "wallet_type", "currency", @@ -696,7 +695,7 @@ Wallet = generate_labeler_serializer( Transfer = generate_labeler_serializer( name="Transfer", - klass=types.Transfer, + klass=dataclasses.Transfer, labels=[ "mts", "wallet_from", @@ -711,7 +710,7 @@ Transfer = generate_labeler_serializer( Withdrawal = generate_labeler_serializer( name="Withdrawal", - klass=types.Withdrawal, + klass=dataclasses.Withdrawal, labels=[ "withdrawal_id", "_PLACEHOLDER", @@ -727,7 +726,7 @@ Withdrawal = generate_labeler_serializer( DepositAddress = generate_labeler_serializer( name="DepositAddress", - klass=types.DepositAddress, + klass=dataclasses.DepositAddress, labels=[ "_PLACEHOLDER", "method", @@ -740,7 +739,7 @@ DepositAddress = generate_labeler_serializer( LightningNetworkInvoice = generate_labeler_serializer( name="LightningNetworkInvoice", - klass=types.LightningNetworkInvoice, + klass=dataclasses.LightningNetworkInvoice, labels=[ "invoice_hash", "invoice", @@ -752,7 +751,7 @@ LightningNetworkInvoice = generate_labeler_serializer( Movement = generate_labeler_serializer( name="Movement", - klass=types.Movement, + klass=dataclasses.Movement, labels=[ "id", "currency", @@ -781,7 +780,7 @@ Movement = generate_labeler_serializer( SymbolMarginInfo = generate_labeler_serializer( name="SymbolMarginInfo", - klass=types.SymbolMarginInfo, + klass=dataclasses.SymbolMarginInfo, labels=[ "_PLACEHOLDER", "symbol", @@ -796,7 +795,7 @@ SymbolMarginInfo = generate_labeler_serializer( BaseMarginInfo = generate_labeler_serializer( name="BaseMarginInfo", - klass=types.BaseMarginInfo, + klass=dataclasses.BaseMarginInfo, labels=[ "user_pl", "user_swaps", @@ -808,7 +807,7 @@ BaseMarginInfo = generate_labeler_serializer( PositionClaim = generate_labeler_serializer( name="PositionClaim", - klass=types.PositionClaim, + klass=dataclasses.PositionClaim, labels=[ "symbol", "position_status", @@ -835,7 +834,7 @@ PositionClaim = generate_labeler_serializer( PositionIncreaseInfo = generate_labeler_serializer( name="PositionIncreaseInfo", - klass=types.PositionIncreaseInfo, + klass=dataclasses.PositionIncreaseInfo, labels=[ "max_pos", "current_pos", @@ -862,7 +861,7 @@ PositionIncreaseInfo = generate_labeler_serializer( PositionIncrease = generate_labeler_serializer( name="PositionIncrease", - klass=types.PositionIncrease, + klass=dataclasses.PositionIncrease, labels=[ "symbol", "_PLACEHOLDER", @@ -873,7 +872,7 @@ PositionIncrease = generate_labeler_serializer( PositionHistory = generate_labeler_serializer( name="PositionHistory", - klass=types.PositionHistory, + klass=dataclasses.PositionHistory, labels=[ "symbol", "status", @@ -894,7 +893,7 @@ PositionHistory = generate_labeler_serializer( PositionSnapshot = generate_labeler_serializer( name="PositionSnapshot", - klass=types.PositionSnapshot, + klass=dataclasses.PositionSnapshot, labels=[ "symbol", "status", @@ -915,7 +914,7 @@ PositionSnapshot = generate_labeler_serializer( PositionAudit = generate_labeler_serializer( name="PositionAudit", - klass=types.PositionAudit, + klass=dataclasses.PositionAudit, labels=[ "symbol", "status", @@ -942,7 +941,7 @@ PositionAudit = generate_labeler_serializer( DerivativePositionCollateral = generate_labeler_serializer( name="DerivativePositionCollateral", - klass=types.DerivativePositionCollateral, + klass=dataclasses.DerivativePositionCollateral, labels=[ "status" ] @@ -950,7 +949,7 @@ DerivativePositionCollateral = generate_labeler_serializer( DerivativePositionCollateralLimits = generate_labeler_serializer( name="DerivativePositionCollateralLimits", - klass=types.DerivativePositionCollateralLimits, + klass=dataclasses.DerivativePositionCollateralLimits, labels=[ "min_collateral", "max_collateral" diff --git a/bfxapi/websocket/handlers/authenticated_events_handler.py b/bfxapi/websocket/handlers/authenticated_events_handler.py index cf098a3..2e86be7 100644 --- a/bfxapi/websocket/handlers/authenticated_events_handler.py +++ b/bfxapi/websocket/handlers/authenticated_events_handler.py @@ -1,9 +1,9 @@ -from .. import serializers - -from .. serializers import _Notification - from .. exceptions import HandlerNotFound +from ... types import serializers + +from ... types.serializers import _Notification + class AuthenticatedEventsHandler: __once_abbreviations = { "os": "order_snapshot", "ps": "position_snapshot", "fos": "funding_offer_snapshot", @@ -16,8 +16,7 @@ class AuthenticatedEventsHandler: "fon": "funding_offer_new", "fou": "funding_offer_update", "foc": "funding_offer_cancel", "fcn": "funding_credit_new", "fcu": "funding_credit_update", "fcc": "funding_credit_close", "fln": "funding_loan_new", "flu": "funding_loan_update", "flc": "funding_loan_close", - "te": "trade_execution", "tu": "trade_execution_update", "wu": "wallet_update", - "bu": "balance_update" + "te": "trade_execution", "tu": "trade_execution_update", "wu": "wallet_update" } __abbreviations = { @@ -32,8 +31,7 @@ class AuthenticatedEventsHandler: ("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 + ("ws", "wu",): serializers.Wallet } ONCE_EVENTS = [ diff --git a/bfxapi/websocket/handlers/public_channels_handler.py b/bfxapi/websocket/handlers/public_channels_handler.py index e8cfaad..6f4b0dd 100644 --- a/bfxapi/websocket/handlers/public_channels_handler.py +++ b/bfxapi/websocket/handlers/public_channels_handler.py @@ -1,7 +1,7 @@ -from .. import serializers - from .. exceptions import HandlerNotFound +from ... types import serializers + class PublicChannelsHandler: ONCE_PER_SUBSCRIPTION_EVENTS = [ "t_trades_snapshot", "f_trades_snapshot", "t_book_snapshot", diff --git a/bfxapi/websocket/serializers.py b/bfxapi/websocket/serializers.py deleted file mode 100644 index be8e393..0000000 --- a/bfxapi/websocket/serializers.py +++ /dev/null @@ -1,368 +0,0 @@ -#pylint: disable=duplicate-code - -from . import types - -from .. labeler import generate_labeler_serializer - -#pylint: disable-next=unused-import -from .. notification import _Notification - -__serializers__ = [ - "TradingPairTicker", "FundingCurrencyTicker", "TradingPairTrade", - "FundingCurrencyTrade", "TradingPairBook", "FundingCurrencyBook", - "TradingPairRawBook", "FundingCurrencyRawBook", "Candle", - "DerivativesStatus", - - "Order", "Position", "Trade", - "FundingOffer", "FundingCredit", "FundingLoan", - "Wallet", "Balance", -] - -#region Serializers definition for WebSocket Public Channels - -TradingPairTicker = generate_labeler_serializer( - name="TradingPairTicker", - klass=types.TradingPairTicker, - labels=[ - "bid", - "bid_size", - "ask", - "ask_size", - "daily_change", - "daily_change_relative", - "last_price", - "volume", - "high", - "low" - ] -) - -FundingCurrencyTicker = generate_labeler_serializer( - name="FundingCurrencyTicker", - klass=types.FundingCurrencyTicker, - labels=[ - "frr", - "bid", - "bid_period", - "bid_size", - "ask", - "ask_period", - "ask_size", - "daily_change", - "daily_change_relative", - "last_price", - "volume", - "high", - "low", - "_PLACEHOLDER", - "_PLACEHOLDER", - "frr_amount_available" - ] -) - -TradingPairTrade = generate_labeler_serializer( - name="TradingPairTrade", - klass=types.TradingPairTrade, - labels=[ - "id", - "mts", - "amount", - "price" - ] -) - -FundingCurrencyTrade = generate_labeler_serializer( - name="FundingCurrencyTrade", - klass=types.FundingCurrencyTrade, - labels=[ - "id", - "mts", - "amount", - "rate", - "period" - ] -) - -TradingPairBook = generate_labeler_serializer( - name="TradingPairBook", - klass=types.TradingPairBook, - labels=[ - "price", - "count", - "amount" - ] -) - -FundingCurrencyBook = generate_labeler_serializer( - name="FundingCurrencyBook", - klass=types.FundingCurrencyBook, - labels=[ - "rate", - "period", - "count", - "amount" - ] -) - -TradingPairRawBook = generate_labeler_serializer( - name="TradingPairRawBook", - klass=types.TradingPairRawBook, - labels=[ - "order_id", - "price", - "amount" - ] -) - -FundingCurrencyRawBook = generate_labeler_serializer( - name="FundingCurrencyRawBook", - klass=types.FundingCurrencyRawBook, - labels=[ - "offer_id", - "period", - "rate", - "amount" - ] -) - -Candle = generate_labeler_serializer( - name="Candle", - klass=types.Candle, - labels=[ - "mts", - "open", - "close", - "high", - "low", - "volume" - ] -) - -DerivativesStatus = generate_labeler_serializer( - name="DerivativesStatus", - klass=types.DerivativesStatus, - labels=[ - "mts", - "_PLACEHOLDER", - "deriv_price", - "spot_price", - "_PLACEHOLDER", - "insurance_fund_balance", - "_PLACEHOLDER", - "next_funding_evt_mts", - "next_funding_accrued", - "next_funding_step", - "_PLACEHOLDER", - "current_funding", - "_PLACEHOLDER", - "_PLACEHOLDER", - "mark_price", - "_PLACEHOLDER", - "_PLACEHOLDER", - "open_interest", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "clamp_min", - "clamp_max" - ] -) - -#endregion - -#region Serializers definition for WebSocket Authenticated Channels - -Order = generate_labeler_serializer( - name="Order", - klass=types.Order, - labels=[ - "id", - "gid", - "cid", - "symbol", - "mts_create", - "mts_update", - "amount", - "amount_orig", - "order_type", - "type_prev", - "mts_tif", - "_PLACEHOLDER", - "flags", - "order_status", - "_PLACEHOLDER", - "_PLACEHOLDER", - "price", - "price_avg", - "price_trailing", - "price_aux_limit", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "notify", - "hidden", - "placed_id", - "_PLACEHOLDER", - "_PLACEHOLDER", - "routing", - "_PLACEHOLDER", - "_PLACEHOLDER", - "meta" - ] -) - -Position = generate_labeler_serializer( - name="Position", - klass=types.Position, - labels=[ - "symbol", - "status", - "amount", - "base_price", - "margin_funding", - "margin_funding_type", - "pl", - "pl_perc", - "price_liq", - "leverage", - "flag", - "position_id", - "mts_create", - "mts_update", - "_PLACEHOLDER", - "type", - "_PLACEHOLDER", - "collateral", - "collateral_min", - "meta" - ] -) - -Trade = generate_labeler_serializer( - name="Trade", - klass=types.Trade, - labels=[ - "id", - "symbol", - "mts_create", - "order_id", - "exec_amount", - "exec_price", - "order_type", - "order_price", - "maker", - "fee", - "fee_currency", - "cid" - ] -) - -FundingOffer = generate_labeler_serializer( - name="FundingOffer", - klass=types.FundingOffer, - labels=[ - "id", - "symbol", - "mts_create", - "mts_update", - "amount", - "amount_orig", - "offer_type", - "_PLACEHOLDER", - "_PLACEHOLDER", - "flags", - "offer_status", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "rate", - "period", - "notify", - "hidden", - "_PLACEHOLDER", - "renew", - "_PLACEHOLDER" - ] -) - -FundingCredit = generate_labeler_serializer( - name="FundingCredit", - klass=types.FundingCredit, - labels=[ - "id", - "symbol", - "side", - "mts_create", - "mts_update", - "amount", - "flags", - "status", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "rate", - "period", - "mts_opening", - "mts_last_payout", - "notify", - "hidden", - "_PLACEHOLDER", - "renew", - "_PLACEHOLDER", - "no_close", - "position_pair" - ] -) - -FundingLoan = generate_labeler_serializer( - name="FundingLoan", - klass=types.FundingLoan, - labels=[ - "id", - "symbol", - "side", - "mts_create", - "mts_update", - "amount", - "flags", - "status", - "_PLACEHOLDER", - "_PLACEHOLDER", - "_PLACEHOLDER", - "rate", - "period", - "mts_opening", - "mts_last_payout", - "notify", - "hidden", - "_PLACEHOLDER", - "renew", - "_PLACEHOLDER", - "no_close" - ] -) - -Wallet = generate_labeler_serializer( - name="Wallet", - klass=types.Wallet, - labels=[ - "wallet_type", - "currency", - "balance", - "unsettled_interest", - "available_balance", - "last_change", - "trade_details" - ] -) - -Balance = generate_labeler_serializer( - name="Balance", - klass=types.Balance, - labels=[ - "aum", - "aum_net" - ] -) - -#endregion diff --git a/bfxapi/websocket/types.py b/bfxapi/websocket/types.py deleted file mode 100644 index 5c186f0..0000000 --- a/bfxapi/websocket/types.py +++ /dev/null @@ -1,247 +0,0 @@ -#pylint: disable=duplicate-code - -#pylint: disable-next=wildcard-import,unused-wildcard-import -from typing import * - -from dataclasses import dataclass - -from .. labeler import _Type - -#pylint: disable-next=unused-import -from .. notification import Notification - -from ..utils.json_encoder import JSON - -#region Type hinting for WebSocket Public Channels - -@dataclass -class TradingPairTicker(_Type): - bid: float - bid_size: float - ask: float - ask_size: float - daily_change: float - daily_change_relative: float - last_price: float - volume: float - high: float - low: float - -@dataclass -class FundingCurrencyTicker(_Type): - frr: float - bid: float - bid_period: int - bid_size: float - ask: float - ask_period: int - ask_size: float - daily_change: float - daily_change_relative: float - last_price: float - volume: float - high: float - low: float - frr_amount_available: float - -@dataclass -class TradingPairTrade(_Type): - id: int - mts: int - amount: float - price: float - -@dataclass -class FundingCurrencyTrade(_Type): - id: int - mts: int - amount: float - rate: float - period: int - -@dataclass -class TradingPairBook(_Type): - price: float - count: int - amount: float - -@dataclass -class FundingCurrencyBook(_Type): - rate: float - period: int - count: int - amount: float - -@dataclass -class TradingPairRawBook(_Type): - order_id: int - price: float - amount: float - -@dataclass -class FundingCurrencyRawBook(_Type): - offer_id: int - period: int - rate: float - amount: float - -@dataclass -class Candle(_Type): - mts: int - open: int - close: int - high: int - low: int - volume: float - -@dataclass -class DerivativesStatus(_Type): - mts: int - deriv_price: float - spot_price: float - insurance_fund_balance: float - next_funding_evt_mts: int - next_funding_accrued: float - next_funding_step: int - current_funding: float - mark_price: float - open_interest: float - clamp_min: float - clamp_max: float - -#endregion - -#region Type hinting for WebSocket Authenticated Channels -@dataclass -class Order(_Type): - id: int - gid: int - cid: int - symbol: str - mts_create: int - mts_update: int - amount: float - amount_orig: float - order_type: str - type_prev: str - mts_tif: int - flags: int - order_status: str - price: float - price_avg: float - price_trailing: float - price_aux_limit: float - notify: int - hidden: int - placed_id: int - routing: str - meta: JSON - -@dataclass -class Position(_Type): - symbol: str - status: str - amount: float - base_price: float - margin_funding: float - margin_funding_type: int - pl: float - pl_perc: float - price_liq: float - leverage: float - flag: int - position_id: int - mts_create: int - mts_update: int - type: int - collateral: float - collateral_min: float - meta: JSON - -@dataclass -class Trade(_Type): - id: int - symbol: str - mts_create: int - order_id: int - exec_amount: float - exec_price: float - order_type: str - order_price: float - maker:int - fee: Optional[float] - fee_currency: Optional[str] - cid: int - -@dataclass -class FundingOffer(_Type): - id: int - symbol: str - mts_create: int - mts_update: int - amount: float - amount_orig: float - offer_type: str - flags: int - offer_status: str - rate: float - period: int - notify: int - hidden: int - renew: int - -@dataclass -class FundingCredit(_Type): - id: int - symbol: str - side: int - mts_create: int - mts_update: int - amount: float - flags: int - status: str - rate: float - period: int - mts_opening: int - mts_last_payout: int - notify: int - hidden: int - renew: int - no_close: int - position_pair: str - -@dataclass -class FundingLoan(_Type): - id: int - symbol: str - side: int - mts_create: int - mts_update: int - amount: float - flags: int - status: str - rate: float - period: int - mts_opening: int - mts_last_payout: int - notify: int - hidden: int - renew: int - no_close: int - -@dataclass -class Wallet(_Type): - wallet_type: str - currency: str - balance: float - unsettled_interest: float - available_balance: float - last_change: str - trade_details: JSON - -@dataclass -class Balance(_Type): - aum: float - aum_net: float - -#endregion diff --git a/examples/rest/authenticated/claim_position.py b/examples/rest/auth/claim_position.py similarity index 89% rename from examples/rest/authenticated/claim_position.py rename to examples/rest/auth/claim_position.py index 5dbe89a..53dfdb7 100644 --- a/examples/rest/authenticated/claim_position.py +++ b/examples/rest/auth/claim_position.py @@ -4,7 +4,7 @@ import os from bfxapi import Client, REST_HOST -from bfxapi.rest.types import Notification, PositionClaim +from bfxapi.types import Notification, PositionClaim bfx = Client( rest_host=REST_HOST, diff --git a/examples/rest/authenticated/get_wallets.py b/examples/rest/auth/get_wallets.py similarity index 90% rename from examples/rest/authenticated/get_wallets.py rename to examples/rest/auth/get_wallets.py index 1a2d105..effa431 100644 --- a/examples/rest/authenticated/get_wallets.py +++ b/examples/rest/auth/get_wallets.py @@ -2,11 +2,12 @@ import os +from typing import List + from bfxapi import Client, REST_HOST -from bfxapi.rest.types import List, Wallet, Transfer, \ - DepositAddress, LightningNetworkInvoice, Withdrawal, \ - Notification +from bfxapi.types import Wallet, Transfer, DepositAddress, \ + LightningNetworkInvoice, Withdrawal, Notification bfx = Client( rest_host=REST_HOST, diff --git a/examples/rest/authenticated/set_derivative_position_collateral.py b/examples/rest/auth/set_derivative_position_collateral.py similarity index 92% rename from examples/rest/authenticated/set_derivative_position_collateral.py rename to examples/rest/auth/set_derivative_position_collateral.py index 0949cac..5097898 100644 --- a/examples/rest/authenticated/set_derivative_position_collateral.py +++ b/examples/rest/auth/set_derivative_position_collateral.py @@ -4,7 +4,7 @@ import os from bfxapi import Client, REST_HOST -from bfxapi.rest.types import DerivativePositionCollateral, DerivativePositionCollateralLimits +from bfxapi.types import DerivativePositionCollateral, DerivativePositionCollateralLimits bfx = Client( rest_host=REST_HOST, diff --git a/examples/rest/authenticated/submit_funding_offer.py b/examples/rest/auth/submit_funding_offer.py similarity index 92% rename from examples/rest/authenticated/submit_funding_offer.py rename to examples/rest/auth/submit_funding_offer.py index 8291e67..2016fbe 100644 --- a/examples/rest/authenticated/submit_funding_offer.py +++ b/examples/rest/auth/submit_funding_offer.py @@ -3,8 +3,8 @@ import os from bfxapi import Client, REST_HOST +from bfxapi.types import Notification, FundingOffer from bfxapi.enums import FundingOfferType, Flag -from bfxapi.rest.types import Notification, FundingOffer bfx = Client( rest_host=REST_HOST, diff --git a/examples/rest/authenticated/submit_order.py b/examples/rest/auth/submit_order.py similarity index 95% rename from examples/rest/authenticated/submit_order.py rename to examples/rest/auth/submit_order.py index 1481de0..4179ee9 100644 --- a/examples/rest/authenticated/submit_order.py +++ b/examples/rest/auth/submit_order.py @@ -3,8 +3,8 @@ import os from bfxapi import Client, REST_HOST +from bfxapi.types import Notification, Order from bfxapi.enums import OrderType, Flag -from bfxapi.rest.types import Notification, Order bfx = Client( rest_host=REST_HOST, diff --git a/examples/rest/authenticated/toggle_keep_funding.py b/examples/rest/auth/toggle_keep_funding.py similarity index 89% rename from examples/rest/authenticated/toggle_keep_funding.py rename to examples/rest/auth/toggle_keep_funding.py index 4f924ae..e1fbb78 100644 --- a/examples/rest/authenticated/toggle_keep_funding.py +++ b/examples/rest/auth/toggle_keep_funding.py @@ -2,9 +2,11 @@ import os +from typing import List + from bfxapi import Client, REST_HOST -from bfxapi.rest.types import List, FundingLoan, Notification +from bfxapi.types import FundingLoan, Notification bfx = Client( rest_host=REST_HOST, diff --git a/examples/rest/merchant/submit_invoice.py b/examples/rest/merchant/submit_invoice.py index 7d7a110..446a1c3 100644 --- a/examples/rest/merchant/submit_invoice.py +++ b/examples/rest/merchant/submit_invoice.py @@ -4,7 +4,7 @@ import os from bfxapi import Client, REST_HOST -from bfxapi.rest.types import InvoiceSubmission +from bfxapi.types import InvoiceSubmission bfx = Client( rest_host=REST_HOST, diff --git a/examples/rest/public/book.py b/examples/rest/public/book.py index f716fc4..8cb11f8 100644 --- a/examples/rest/public/book.py +++ b/examples/rest/public/book.py @@ -1,8 +1,10 @@ # python -c "import examples.rest.public.book" +from typing import List + from bfxapi import Client, PUB_REST_HOST -from bfxapi.rest.types import List, TradingPairBook, TradingPairRawBook, \ +from bfxapi.types import TradingPairBook, TradingPairRawBook, \ FundingCurrencyBook, FundingCurrencyRawBook bfx = Client(rest_host=PUB_REST_HOST) diff --git a/examples/rest/public/pulse_endpoints.py b/examples/rest/public/pulse_endpoints.py index d2305ac..3784500 100644 --- a/examples/rest/public/pulse_endpoints.py +++ b/examples/rest/public/pulse_endpoints.py @@ -2,9 +2,11 @@ import datetime +from typing import List + from bfxapi import Client, PUB_REST_HOST -from bfxapi.rest.types import List, PulseMessage, PulseProfile +from bfxapi.types import PulseMessage, PulseProfile bfx = Client(rest_host=PUB_REST_HOST) diff --git a/examples/rest/public/rest_calculation_endpoints.py b/examples/rest/public/rest_calculation_endpoints.py index e66ca70..88fba15 100644 --- a/examples/rest/public/rest_calculation_endpoints.py +++ b/examples/rest/public/rest_calculation_endpoints.py @@ -2,7 +2,7 @@ from bfxapi import Client, PUB_REST_HOST -from bfxapi.rest.types import TradingMarketAveragePrice, FundingMarketAveragePrice, FxRate +from bfxapi.types import TradingMarketAveragePrice, FundingMarketAveragePrice, FxRate bfx = Client(rest_host=PUB_REST_HOST) diff --git a/examples/rest/public/trades.py b/examples/rest/public/trades.py index 5e9242a..d83ff2b 100644 --- a/examples/rest/public/trades.py +++ b/examples/rest/public/trades.py @@ -1,8 +1,10 @@ # python -c "import examples.rest.public.trades" +from typing import List + from bfxapi import Client, PUB_REST_HOST +from bfxapi.types import TradingPairTrade, FundingCurrencyTrade from bfxapi.rest.enums import Sort -from bfxapi.rest.types import List, TradingPairTrade, FundingCurrencyTrade bfx = Client(rest_host=PUB_REST_HOST) diff --git a/examples/websocket/authenticated/submit_order.py b/examples/websocket/auth/submit_order.py similarity index 95% rename from examples/websocket/authenticated/submit_order.py rename to examples/websocket/auth/submit_order.py index 5555493..4e5b8d6 100644 --- a/examples/websocket/authenticated/submit_order.py +++ b/examples/websocket/auth/submit_order.py @@ -4,7 +4,7 @@ import os from bfxapi import Client, WSS_HOST from bfxapi.enums import Error, OrderType -from bfxapi.websocket.types import Notification, Order +from bfxapi.types import Notification, Order bfx = Client( wss_host=WSS_HOST, diff --git a/examples/websocket/authenticated/wallets.py b/examples/websocket/auth/wallets.py similarity index 94% rename from examples/websocket/authenticated/wallets.py rename to examples/websocket/auth/wallets.py index 7dcb58c..259b5c0 100644 --- a/examples/websocket/authenticated/wallets.py +++ b/examples/websocket/auth/wallets.py @@ -4,7 +4,7 @@ import os from bfxapi import Client from bfxapi.enums import Error -from bfxapi.websocket.types import List, Wallet +from bfxapi.types import List, Wallet bfx = Client( api_key=os.getenv("BFX_API_KEY"), diff --git a/examples/websocket/public/derivatives_status.py b/examples/websocket/public/derivatives_status.py index 9212909..d55c492 100644 --- a/examples/websocket/public/derivatives_status.py +++ b/examples/websocket/public/derivatives_status.py @@ -1,15 +1,15 @@ # python -c "import examples.websocket.public.derivatives_status" from bfxapi import Client, PUB_WSS_HOST -from bfxapi.websocket.enums import Error, Channel -from bfxapi.websocket.types import DerivativesStatus +from bfxapi.types import DerivativesStatus +from bfxapi.websocket.subscriptions import Status -from bfxapi.websocket import subscriptions +from bfxapi.websocket.enums import Error, Channel bfx = Client(wss_host=PUB_WSS_HOST) @bfx.wss.on("derivatives_status_update") -def on_derivatives_status_update(subscription: subscriptions.Status, data: DerivativesStatus): +def on_derivatives_status_update(subscription: Status, data: DerivativesStatus): print(f"{subscription}:", data) @bfx.wss.on("wss-error") diff --git a/examples/websocket/public/order_book.py b/examples/websocket/public/order_book.py index aef1d04..ef6e31c 100644 --- a/examples/websocket/public/order_book.py +++ b/examples/websocket/public/order_book.py @@ -6,9 +6,9 @@ from typing import List from bfxapi import Client, PUB_WSS_HOST -from bfxapi.websocket import subscriptions +from bfxapi.types import TradingPairBook +from bfxapi.websocket.subscriptions import Book from bfxapi.websocket.enums import Channel, Error -from bfxapi.websocket.types import TradingPairBook class OrderBook: def __init__(self, symbols: List[str]): @@ -54,12 +54,12 @@ def on_subscribed(subscription): print(f"Subscription successful for pair <{subscription['pair']}>") @bfx.wss.on("t_book_snapshot") -def on_t_book_snapshot(subscription: subscriptions.Book, snapshot: List[TradingPairBook]): +def on_t_book_snapshot(subscription: Book, snapshot: List[TradingPairBook]): for data in snapshot: order_book.update(subscription["symbol"], data) @bfx.wss.on("t_book_update") -def on_t_book_update(subscription: subscriptions.Book, data: TradingPairBook): +def on_t_book_update(subscription: Book, data: TradingPairBook): order_book.update(subscription["symbol"], data) bfx.wss.run() diff --git a/examples/websocket/public/raw_order_book.py b/examples/websocket/public/raw_order_book.py index 07814af..33ef321 100644 --- a/examples/websocket/public/raw_order_book.py +++ b/examples/websocket/public/raw_order_book.py @@ -6,9 +6,9 @@ from typing import List from bfxapi import Client, PUB_WSS_HOST -from bfxapi.websocket import subscriptions +from bfxapi.types import TradingPairRawBook +from bfxapi.websocket.subscriptions import Book from bfxapi.websocket.enums import Channel, Error -from bfxapi.websocket.types import TradingPairRawBook class RawOrderBook: def __init__(self, symbols: List[str]): @@ -54,12 +54,12 @@ def on_subscribed(subscription): print(f"Subscription successful for pair <{subscription['pair']}>") @bfx.wss.on("t_raw_book_snapshot") -def on_t_raw_book_snapshot(subscription: subscriptions.Book, snapshot: List[TradingPairRawBook]): +def on_t_raw_book_snapshot(subscription: Book, snapshot: List[TradingPairRawBook]): for data in snapshot: raw_order_book.update(subscription["symbol"], data) @bfx.wss.on("t_raw_book_update") -def on_t_raw_book_update(subscription: subscriptions.Book, data: TradingPairRawBook): +def on_t_raw_book_update(subscription: Book, data: TradingPairRawBook): raw_order_book.update(subscription["symbol"], data) bfx.wss.run() diff --git a/examples/websocket/public/ticker.py b/examples/websocket/public/ticker.py index 8a55aa4..24c9463 100644 --- a/examples/websocket/public/ticker.py +++ b/examples/websocket/public/ticker.py @@ -2,14 +2,14 @@ from bfxapi import Client, PUB_WSS_HOST -from bfxapi.websocket import subscriptions +from bfxapi.types import TradingPairTicker +from bfxapi.websocket.subscriptions import Ticker from bfxapi.websocket.enums import Channel -from bfxapi.websocket.types import TradingPairTicker bfx = Client(wss_host=PUB_WSS_HOST) @bfx.wss.on("t_ticker_update") -def on_t_ticker_update(subscription: subscriptions.Ticker, data: TradingPairTicker): +def on_t_ticker_update(subscription: Ticker, data: TradingPairTicker): print(f"Subscription with subId: {subscription['subId']}") print(f"Data: {data}") diff --git a/examples/websocket/public/trades.py b/examples/websocket/public/trades.py index 1159ab4..e079904 100644 --- a/examples/websocket/public/trades.py +++ b/examples/websocket/public/trades.py @@ -1,19 +1,19 @@ # python -c "import examples.websocket.public.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 +from bfxapi.types import Candle, TradingPairTrade +from bfxapi.websocket.subscriptions import Candles, Trades +from bfxapi.websocket.enums import Error, Channel bfx = Client(wss_host=PUB_WSS_HOST) @bfx.wss.on("candles_update") -def on_candles_update(_sub: subscriptions.Candles, candle: Candle): +def on_candles_update(_sub: Candles, candle: Candle): print(f"New candle: {candle}") @bfx.wss.on("t_trade_execution") -def on_t_trade_execution(_sub: subscriptions.Trades, trade: TradingPairTrade): +def on_t_trade_execution(_sub: Trades, trade: TradingPairTrade): print(f"New trade: {trade}") @bfx.wss.on("wss-error") diff --git a/setup.py b/setup.py index b8484a6..fceacf7 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ setup( "Source": "https://github.com/bitfinexcom/bitfinex-api-py", }, packages=[ - "bfxapi", "bfxapi.utils", + "bfxapi", "bfxapi.utils", "bfxapi.types", "bfxapi.websocket", "bfxapi.websocket.client", "bfxapi.websocket.handlers", "bfxapi.rest", "bfxapi.rest.endpoints", "bfxapi.rest.middleware", ], From d7ce5630ef2f31522c696f0683ee28ced035469b Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 20 Apr 2023 03:49:54 +0200 Subject: [PATCH 257/265] Rewrite bfxapi.tests according to latest changes. --- bfxapi/tests/__init__.py | 14 ++++++------- ...{test_labeler.py => test_types_labeler.py} | 10 ++++----- ...fication.py => test_types_notification.py} | 8 +++---- ...rializers.py => test_types_serializers.py} | 12 ++++------- bfxapi/tests/test_websocket_serializers.py | 21 ------------------- .../websocket/client/bfx_websocket_inputs.py | 2 +- 6 files changed, 20 insertions(+), 47 deletions(-) rename bfxapi/tests/{test_labeler.py => test_types_labeler.py} (83%) rename bfxapi/tests/{test_notification.py => test_types_notification.py} (79%) rename bfxapi/tests/{test_rest_serializers.py => test_types_serializers.py} (77%) delete mode 100644 bfxapi/tests/test_websocket_serializers.py diff --git a/bfxapi/tests/__init__.py b/bfxapi/tests/__init__.py index ac76a9b..e7a6f4e 100644 --- a/bfxapi/tests/__init__.py +++ b/bfxapi/tests/__init__.py @@ -1,16 +1,14 @@ import unittest -from .test_rest_serializers import TestRestSerializers -from .test_websocket_serializers import TestWebSocketSerializers -from .test_labeler import TestLabeler -from .test_notification import TestNotification +from .test_types_labeler import TestTypesLabeler +from .test_types_notification import TestTypesNotification +from .test_types_serializers import TestTypesSerializers def suite(): return unittest.TestSuite([ - unittest.makeSuite(TestRestSerializers), - unittest.makeSuite(TestWebSocketSerializers), - unittest.makeSuite(TestLabeler), - unittest.makeSuite(TestNotification), + unittest.makeSuite(TestTypesLabeler), + unittest.makeSuite(TestTypesNotification), + unittest.makeSuite(TestTypesSerializers), ]) if __name__ == "__main__": diff --git a/bfxapi/tests/test_labeler.py b/bfxapi/tests/test_types_labeler.py similarity index 83% rename from bfxapi/tests/test_labeler.py rename to bfxapi/tests/test_types_labeler.py index c375798..90b16d9 100644 --- a/bfxapi/tests/test_labeler.py +++ b/bfxapi/tests/test_types_labeler.py @@ -3,10 +3,10 @@ import unittest from typing import Optional from dataclasses import dataclass -from ..exceptions import LabelerSerializerException -from ..labeler import _Type, generate_labeler_serializer, generate_recursive_serializer -class TestLabeler(unittest.TestCase): +from .. types.labeler import _Type, generate_labeler_serializer, generate_recursive_serializer + +class TestTypesLabeler(unittest.TestCase): def test_generate_labeler_serializer(self): @dataclass class Test(_Type): @@ -24,8 +24,8 @@ class TestLabeler(unittest.TestCase): self.assertListEqual(serializer.get_labels(), [ "A", "B", "C" ], msg="_Serializer::get_labels() should return the right list of labels.") - with self.assertRaises(LabelerSerializerException, - msg="_Serializer should raise LabelerSerializerException if given " \ + with self.assertRaises(AssertionError, + msg="_Serializer should raise an AssertionError if given " \ "fewer arguments than the serializer labels."): serializer.parse(5, 65.0, "X") diff --git a/bfxapi/tests/test_notification.py b/bfxapi/tests/test_types_notification.py similarity index 79% rename from bfxapi/tests/test_notification.py rename to bfxapi/tests/test_types_notification.py index 0d3cb59..a576d48 100644 --- a/bfxapi/tests/test_notification.py +++ b/bfxapi/tests/test_types_notification.py @@ -1,11 +1,11 @@ import unittest from dataclasses import dataclass -from ..labeler import generate_labeler_serializer -from ..notification import _Type, _Notification, Notification +from .. types.labeler import generate_labeler_serializer +from .. types.notification import _Type, _Notification, Notification -class TestNotification(unittest.TestCase): - def test_notification(self): +class TestTypesNotification(unittest.TestCase): + def test_types_notification(self): @dataclass class Test(_Type): A: int diff --git a/bfxapi/tests/test_rest_serializers.py b/bfxapi/tests/test_types_serializers.py similarity index 77% rename from bfxapi/tests/test_rest_serializers.py rename to bfxapi/tests/test_types_serializers.py index c9c1886..4c9592b 100644 --- a/bfxapi/tests/test_rest_serializers.py +++ b/bfxapi/tests/test_types_serializers.py @@ -1,13 +1,9 @@ -#pylint: disable=duplicate-code - import unittest +from .. types import serializers +from .. types.labeler import _Type -from ..labeler import _Type - -from ..rest import serializers - -class TestRestSerializers(unittest.TestCase): - def test_rest_serializers(self): +class TestTypesSerializers(unittest.TestCase): + def test_types_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 " \ diff --git a/bfxapi/tests/test_websocket_serializers.py b/bfxapi/tests/test_websocket_serializers.py deleted file mode 100644 index d379c32..0000000 --- a/bfxapi/tests/test_websocket_serializers.py +++ /dev/null @@ -1,21 +0,0 @@ -#pylint: disable=duplicate-code - -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 " \ - f"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() diff --git a/bfxapi/websocket/client/bfx_websocket_inputs.py b/bfxapi/websocket/client/bfx_websocket_inputs.py index aed658d..263d769 100644 --- a/bfxapi/websocket/client/bfx_websocket_inputs.py +++ b/bfxapi/websocket/client/bfx_websocket_inputs.py @@ -3,7 +3,7 @@ from datetime import datetime from typing import Union, Optional, List, Tuple from .. enums import OrderType, FundingOfferType -from ...utils.json_encoder import JSON +from ... types import JSON class BfxWebSocketInputs: def __init__(self, handle_websocket_input): From eeb6e975f4169bfe43c328557de170171e47c58d Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 20 Apr 2023 04:08:36 +0200 Subject: [PATCH 258/265] Fix bug in code blocks inside README.md. --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 97d4553..4c181ce 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ python3 -m pip install bitfinex-api-py==3.0.0b1 ```python from bfxapi import Client, REST_HOST -from bfxapi.rest.types import Notification, Order +from bfxapi.types import Notification, Order bfx = Client( rest_host=REST_HOST, @@ -166,7 +166,7 @@ To learn more about events and public channels, see [Listening to events](#liste ```python @bfx.wss.on("open") async def on_open(): - await bfx.wss.subscribe(Channel.TICKER, symbol="tBTCUSD") + await bfx.wss.subscribe("ticker", symbol="tBTCUSD") ``` ### Closing the connection @@ -185,7 +185,8 @@ After closing the connection, the client will emit the `disconnection` event: ```python @bfx.wss.on("disconnection") def on_disconnection(code: int, reason: str): - print(f"Closing connection with code: <{code}>. Reason: {reason}.") + if code == 1000 or code == 1001: + print("Closing the connection without errors!") ``` ## Subscribing to public channels @@ -298,7 +299,7 @@ import os from bfxapi import Client, WSS_HOST -from bfxapi.websocket.types import Notification, Order +from bfxapi.types import Notification, Order bfx = Client( wss_host=WSS_HOST, From 63ab4337100de12d05d87b997ed65c26be6c053b Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 20 Apr 2023 15:38:10 +0200 Subject: [PATCH 259/265] Apply refactoring to some relative import directives. --- bfxapi/rest/endpoints/rest_authenticated_endpoints.py | 10 +++++----- bfxapi/rest/endpoints/rest_merchant_endpoints.py | 6 +++--- bfxapi/rest/endpoints/rest_public_endpoints.py | 8 ++++---- bfxapi/rest/exceptions.py | 2 +- bfxapi/tests/test_types_labeler.py | 2 +- bfxapi/tests/test_types_notification.py | 4 ++-- bfxapi/tests/test_types_serializers.py | 4 ++-- bfxapi/types/__init__.py | 8 ++++---- bfxapi/types/dataclasses.py | 2 +- bfxapi/types/notification.py | 2 +- bfxapi/types/serializers.py | 6 +++--- bfxapi/websocket/client/bfx_websocket_inputs.py | 4 ++-- bfxapi/websocket/enums.py | 2 +- bfxapi/websocket/exceptions.py | 2 +- .../websocket/handlers/authenticated_events_handler.py | 6 +++--- bfxapi/websocket/handlers/public_channels_handler.py | 4 ++-- 16 files changed, 36 insertions(+), 36 deletions(-) diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index 0247866..b9e9ff3 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -2,11 +2,11 @@ from typing import Dict, List, Tuple, Union, Literal, Optional from decimal import Decimal from datetime import datetime -from .. middleware import Middleware +from ..middleware import Middleware -from .. enums import Sort, OrderType, FundingOfferType +from ..enums import Sort, OrderType, FundingOfferType -from ... types import JSON, Notification, \ +from ...types import JSON, Notification, \ UserInfo, LoginHistory, BalanceAvailable, \ Order, Position, Trade, \ FundingTrade, OrderTrade, Ledger, \ @@ -18,9 +18,9 @@ from ... types import JSON, Notification, \ PositionIncrease, PositionHistory, PositionSnapshot, \ PositionAudit, DerivativePositionCollateral, DerivativePositionCollateralLimits -from ... types import serializers +from ...types import serializers -from ... types.serializers import _Notification +from ...types.serializers import _Notification class RestAuthenticatedEndpoints(Middleware): def get_user_info(self) -> UserInfo: diff --git a/bfxapi/rest/endpoints/rest_merchant_endpoints.py b/bfxapi/rest/endpoints/rest_merchant_endpoints.py index ff2c19d..8055e6c 100644 --- a/bfxapi/rest/endpoints/rest_merchant_endpoints.py +++ b/bfxapi/rest/endpoints/rest_merchant_endpoints.py @@ -5,11 +5,11 @@ from typing import Callable, TypeVar, cast, \ from decimal import Decimal -from .. middleware import Middleware +from ..middleware import Middleware -from .. enums import MerchantSettingsKey +from ..enums import MerchantSettingsKey -from ... types import \ +from ...types import \ InvoiceSubmission, InvoicePage, InvoiceStats, \ CurrencyConversion, MerchantDeposit, MerchantUnlinkedDeposit diff --git a/bfxapi/rest/endpoints/rest_public_endpoints.py b/bfxapi/rest/endpoints/rest_public_endpoints.py index bb7ed2e..99cb725 100644 --- a/bfxapi/rest/endpoints/rest_public_endpoints.py +++ b/bfxapi/rest/endpoints/rest_public_endpoints.py @@ -2,11 +2,11 @@ from typing import List, Dict, Union, Literal, Optional, Any, cast from decimal import Decimal -from .. middleware import Middleware +from ..middleware import Middleware -from .. enums import Config, Sort +from ..enums import Config, Sort -from ... types import \ +from ...types import \ PlatformStatus, TradingPairTicker, FundingCurrencyTicker, \ TickersHistory, TradingPairTrade, FundingCurrencyTrade, \ TradingPairBook, FundingCurrencyBook, TradingPairRawBook, \ @@ -15,7 +15,7 @@ from ... types import \ FundingStatistic, PulseProfile, PulseMessage, \ TradingMarketAveragePrice, FundingMarketAveragePrice, FxRate -from ... types import serializers +from ...types import serializers class RestPublicEndpoints(Middleware): def conf(self, config: Config) -> Any: diff --git a/bfxapi/rest/exceptions.py b/bfxapi/rest/exceptions.py index 8ea47c3..0c506d1 100644 --- a/bfxapi/rest/exceptions.py +++ b/bfxapi/rest/exceptions.py @@ -1,4 +1,4 @@ -from .. exceptions import BfxBaseException +from ..exceptions import BfxBaseException __all__ = [ "BfxRestException", diff --git a/bfxapi/tests/test_types_labeler.py b/bfxapi/tests/test_types_labeler.py index 90b16d9..639736b 100644 --- a/bfxapi/tests/test_types_labeler.py +++ b/bfxapi/tests/test_types_labeler.py @@ -4,7 +4,7 @@ from typing import Optional from dataclasses import dataclass -from .. types.labeler import _Type, generate_labeler_serializer, generate_recursive_serializer +from ..types.labeler import _Type, generate_labeler_serializer, generate_recursive_serializer class TestTypesLabeler(unittest.TestCase): def test_generate_labeler_serializer(self): diff --git a/bfxapi/tests/test_types_notification.py b/bfxapi/tests/test_types_notification.py index a576d48..007f263 100644 --- a/bfxapi/tests/test_types_notification.py +++ b/bfxapi/tests/test_types_notification.py @@ -1,8 +1,8 @@ import unittest from dataclasses import dataclass -from .. types.labeler import generate_labeler_serializer -from .. types.notification import _Type, _Notification, Notification +from ..types.labeler import generate_labeler_serializer +from ..types.notification import _Type, _Notification, Notification class TestTypesNotification(unittest.TestCase): def test_types_notification(self): diff --git a/bfxapi/tests/test_types_serializers.py b/bfxapi/tests/test_types_serializers.py index 4c9592b..b5b2695 100644 --- a/bfxapi/tests/test_types_serializers.py +++ b/bfxapi/tests/test_types_serializers.py @@ -1,6 +1,6 @@ import unittest -from .. types import serializers -from .. types.labeler import _Type +from ..types import serializers +from ..types.labeler import _Type class TestTypesSerializers(unittest.TestCase): def test_types_serializers(self): diff --git a/bfxapi/types/__init__.py b/bfxapi/types/__init__.py index 7dcac83..ce3ef06 100644 --- a/bfxapi/types/__init__.py +++ b/bfxapi/types/__init__.py @@ -1,4 +1,4 @@ -from . dataclasses import JSON, \ +from .dataclasses import JSON, \ PlatformStatus, TradingPairTicker, FundingCurrencyTicker, \ TickersHistory, TradingPairTrade, FundingCurrencyTrade, \ TradingPairBook, FundingCurrencyBook, TradingPairRawBook, \ @@ -7,7 +7,7 @@ from . dataclasses import JSON, \ FundingStatistic, PulseProfile, PulseMessage, \ TradingMarketAveragePrice, FundingMarketAveragePrice, FxRate -from . dataclasses import \ +from .dataclasses import \ UserInfo, LoginHistory, BalanceAvailable, \ Order, Position, Trade, \ FundingTrade, OrderTrade, Ledger, \ @@ -19,8 +19,8 @@ from . dataclasses import \ PositionIncrease, PositionHistory, PositionSnapshot, \ PositionAudit, DerivativePositionCollateral, DerivativePositionCollateralLimits -from . dataclasses import \ +from .dataclasses import \ InvoiceSubmission, InvoicePage, InvoiceStats, \ CurrencyConversion, MerchantDeposit, MerchantUnlinkedDeposit -from . notification import Notification +from .notification import Notification diff --git a/bfxapi/types/dataclasses.py b/bfxapi/types/dataclasses.py index e81d08c..264de42 100644 --- a/bfxapi/types/dataclasses.py +++ b/bfxapi/types/dataclasses.py @@ -3,7 +3,7 @@ from typing import Union, Type, \ from dataclasses import dataclass -from . labeler import _Type, partial, compose +from .labeler import _Type, partial, compose JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] diff --git a/bfxapi/types/notification.py b/bfxapi/types/notification.py index 87d37cd..ae02259 100644 --- a/bfxapi/types/notification.py +++ b/bfxapi/types/notification.py @@ -1,6 +1,6 @@ from typing import List, Optional, Any, Generic, TypeVar, cast from dataclasses import dataclass -from . labeler import _Type, _Serializer +from .labeler import _Type, _Serializer T = TypeVar("T") diff --git a/bfxapi/types/serializers.py b/bfxapi/types/serializers.py index 353915f..f853ce4 100644 --- a/bfxapi/types/serializers.py +++ b/bfxapi/types/serializers.py @@ -1,10 +1,10 @@ -from . import dataclasses +from .import dataclasses -from . labeler import \ +from .labeler import \ generate_labeler_serializer, generate_recursive_serializer #pylint: disable-next=unused-import -from . notification import _Notification +from .notification import _Notification __serializers__ = [ "PlatformStatus", "TradingPairTicker", "FundingCurrencyTicker", diff --git a/bfxapi/websocket/client/bfx_websocket_inputs.py b/bfxapi/websocket/client/bfx_websocket_inputs.py index 263d769..960f886 100644 --- a/bfxapi/websocket/client/bfx_websocket_inputs.py +++ b/bfxapi/websocket/client/bfx_websocket_inputs.py @@ -2,8 +2,8 @@ from decimal import Decimal from datetime import datetime from typing import Union, Optional, List, Tuple -from .. enums import OrderType, FundingOfferType -from ... types import JSON +from ..enums import OrderType, FundingOfferType +from ...types import JSON class BfxWebSocketInputs: def __init__(self, handle_websocket_input): diff --git a/bfxapi/websocket/enums.py b/bfxapi/websocket/enums.py index 2ff57ca..227bf69 100644 --- a/bfxapi/websocket/enums.py +++ b/bfxapi/websocket/enums.py @@ -1,5 +1,5 @@ #pylint: disable-next=wildcard-import,unused-wildcard-import -from .. enums import * +from ..enums import * class Channel(str, Enum): TICKER = "ticker" diff --git a/bfxapi/websocket/exceptions.py b/bfxapi/websocket/exceptions.py index cf229bc..22917be 100644 --- a/bfxapi/websocket/exceptions.py +++ b/bfxapi/websocket/exceptions.py @@ -1,4 +1,4 @@ -from .. exceptions import BfxBaseException +from ..exceptions import BfxBaseException __all__ = [ "BfxWebSocketException", diff --git a/bfxapi/websocket/handlers/authenticated_events_handler.py b/bfxapi/websocket/handlers/authenticated_events_handler.py index 2e86be7..ec811b5 100644 --- a/bfxapi/websocket/handlers/authenticated_events_handler.py +++ b/bfxapi/websocket/handlers/authenticated_events_handler.py @@ -1,8 +1,8 @@ -from .. exceptions import HandlerNotFound +from ..exceptions import HandlerNotFound -from ... types import serializers +from ...types import serializers -from ... types.serializers import _Notification +from ...types.serializers import _Notification class AuthenticatedEventsHandler: __once_abbreviations = { diff --git a/bfxapi/websocket/handlers/public_channels_handler.py b/bfxapi/websocket/handlers/public_channels_handler.py index 6f4b0dd..d8d76c7 100644 --- a/bfxapi/websocket/handlers/public_channels_handler.py +++ b/bfxapi/websocket/handlers/public_channels_handler.py @@ -1,6 +1,6 @@ -from .. exceptions import HandlerNotFound +from ..exceptions import HandlerNotFound -from ... types import serializers +from ...types import serializers class PublicChannelsHandler: ONCE_PER_SUBSCRIPTION_EVENTS = [ From 03a03c610763b1adbfff393ddb59dd57bfc39e39 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 20 Apr 2023 15:49:46 +0200 Subject: [PATCH 260/265] Delete HandlerNotFound exception (from bfxapi.websocket.exceptions). --- bfxapi/websocket/exceptions.py | 7 ------- bfxapi/websocket/handlers/authenticated_events_handler.py | 4 ---- bfxapi/websocket/handlers/public_channels_handler.py | 4 ---- 3 files changed, 15 deletions(-) diff --git a/bfxapi/websocket/exceptions.py b/bfxapi/websocket/exceptions.py index 22917be..e47a1f0 100644 --- a/bfxapi/websocket/exceptions.py +++ b/bfxapi/websocket/exceptions.py @@ -10,7 +10,6 @@ __all__ = [ "WebSocketAuthenticationRequired", "InvalidAuthenticationCredentials", "EventNotSupported", - "HandlerNotFound", "OutdatedClientVersion" ] @@ -54,12 +53,6 @@ class EventNotSupported(BfxWebSocketException): This error indicates a failed attempt to subscribe to an event not supported by the BfxWebSocketClient. """ - -class HandlerNotFound(BfxWebSocketException): - """ - This error indicates that a handler was not found for an incoming message. - """ - class OutdatedClientVersion(BfxWebSocketException): """ This error indicates a mismatch between the client version and the server WSS version. diff --git a/bfxapi/websocket/handlers/authenticated_events_handler.py b/bfxapi/websocket/handlers/authenticated_events_handler.py index ec811b5..b3e1e12 100644 --- a/bfxapi/websocket/handlers/authenticated_events_handler.py +++ b/bfxapi/websocket/handlers/authenticated_events_handler.py @@ -1,5 +1,3 @@ -from ..exceptions import HandlerNotFound - from ...types import serializers from ...types.serializers import _Notification @@ -60,8 +58,6 @@ class AuthenticatedEventsHandler: return self.event_emitter.emit(event, serializer.parse(*stream)) - raise HandlerNotFound(f"No handler found for event of type <{abbrevation}>.") - def __notification(self, stream): event, serializer = "notification", _Notification(serializer=None) diff --git a/bfxapi/websocket/handlers/public_channels_handler.py b/bfxapi/websocket/handlers/public_channels_handler.py index d8d76c7..f32fe14 100644 --- a/bfxapi/websocket/handlers/public_channels_handler.py +++ b/bfxapi/websocket/handlers/public_channels_handler.py @@ -1,5 +1,3 @@ -from ..exceptions import HandlerNotFound - from ...types import serializers class PublicChannelsHandler: @@ -37,8 +35,6 @@ class PublicChannelsHandler: if (channel := subscription["channel"]) and channel in self.__handlers.keys(): return self.__handlers[channel](_clear(subscription, "event", "channel", "chanId"), *stream) - raise HandlerNotFound(f"No handler found for channel <{subscription['channel']}>.") - def __emit(self, event, sub, data): sub_id, should_emit_event = sub["subId"], True From ad5f323ff521d301d6e37e0bedef7577eae559af Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Thu, 20 Apr 2023 15:54:43 +0200 Subject: [PATCH 261/265] Fix small bug in examples/websocket/auth/wallets.py. --- examples/websocket/auth/wallets.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/websocket/auth/wallets.py b/examples/websocket/auth/wallets.py index 259b5c0..1773a3a 100644 --- a/examples/websocket/auth/wallets.py +++ b/examples/websocket/auth/wallets.py @@ -2,9 +2,11 @@ import os +from typing import List + from bfxapi import Client from bfxapi.enums import Error -from bfxapi.types import List, Wallet +from bfxapi.types import Wallet bfx = Client( api_key=os.getenv("BFX_API_KEY"), From c99ac502e6cf89d3da2c776608b1a81ec16de0a8 Mon Sep 17 00:00:00 2001 From: vigan-abd Date: Wed, 26 Apr 2023 16:59:10 +0200 Subject: [PATCH 262/265] license fix --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b8484a6..5119e62 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ setup( "Intended Audience :: Developers", "Topic :: Software Development :: Build Tools", - "License :: OSI Approved :: Apache-2.0", + "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", From 65eba2ad92cfdea20de2c08e7c9ecbd8ec7ae2f1 Mon Sep 17 00:00:00 2001 From: vigan-abd Date: Wed, 26 Apr 2023 17:00:15 +0200 Subject: [PATCH 263/265] exclude generated MANIFEST --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6137a9d..1556a49 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ __pycache__ dist venv !.gitkeep +MANIFEST From 353b433247719a05a4c192c376a0a30c70bc1e6e Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Wed, 26 Apr 2023 17:06:19 +0200 Subject: [PATCH 264/265] Update package version to 3.0.0b2. --- bfxapi/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bfxapi/version.py b/bfxapi/version.py index 3996ce8..c9e4186 100644 --- a/bfxapi/version.py +++ b/bfxapi/version.py @@ -1 +1 @@ -__version__ = "3.0.0b1" +__version__ = "3.0.0b2" From 62db74c2089f442b57dc4eb8d0f5d21e47638b55 Mon Sep 17 00:00:00 2001 From: Davide Casale Date: Fri, 2 Jun 2023 18:53:49 +0200 Subject: [PATCH 265/265] Switch from branch v3-beta to master in .github/workflows/bitfinex-api-py-ci.yml. --- .github/workflows/bitfinex-api-py-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/bitfinex-api-py-ci.yml b/.github/workflows/bitfinex-api-py-ci.yml index 295ce9f..bee0232 100644 --- a/.github/workflows/bitfinex-api-py-ci.yml +++ b/.github/workflows/bitfinex-api-py-ci.yml @@ -3,10 +3,10 @@ name: bitfinex-api-py-ci on: push: branches: - - v3-beta + - master pull_request: branches: - - v3-beta + - master permissions: contents: read