diff --git a/.run/exchanges-wrapper-image.run.xml b/.run/exchanges-wrapper-image.run.xml new file mode 100644 index 0000000..1639b9f --- /dev/null +++ b/.run/exchanges-wrapper-image.run.xml @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/.run/exchanges-wrapper.run.xml b/.run/exchanges-wrapper.run.xml new file mode 100644 index 0000000..81abc30 --- /dev/null +++ b/.run/exchanges-wrapper.run.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index fba887b..64b2a92 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ -## 2.1.11.post1 2024-04-xx +## 2.1.12 2024-04-30 +### Fix +* `Docker`: [#49 ImportError: cannot import name 'version' from 'exchanges_wrapper'](https://github.com/DogsTailFarmer/exchanges-wrapper/issues/49#issue-2272432093) + ### Update * `send_request`: controlling rate_limit by changing exception handling +* `Bitfinex`: sync `nonce` for connections group +* Dependency: Up requirements for crypto-ws-api==2.0.11 ## 2.1.11 2024-04-19 ### Update diff --git a/Dockerfile b/Dockerfile index c947f71..a220949 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ COPY requirements.txt requirements.txt RUN pip3 install -r requirements.txt -COPY ./exchanges_wrapper/* /home/appuser/.local/lib/python3.10/site-packages/exchanges_wrapper/ +COPY ./exchanges_wrapper /home/appuser/.local/lib/python3.10/site-packages/exchanges_wrapper/ WORKDIR "/home/appuser/.local/lib/python3.10/site-packages" diff --git a/README.md b/README.md index ec915f9..6b9aa0c 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,11 @@ pip install -U exchanges-wrapper #### Start server * Run in terminal window + ``` + exchanges-wrapper-init + ``` +and + ``` exchanges-wrapper-srv ``` diff --git a/exchanges_wrapper/__init__.py b/exchanges_wrapper/__init__.py index 44d5f18..ef188f3 100755 --- a/exchanges_wrapper/__init__.py +++ b/exchanges_wrapper/__init__.py @@ -12,7 +12,7 @@ __contact__ = "https://github.com/DogsTailFarmer" __email__ = "jerry.fedorenko@yahoo.com" __credits__ = ["https://github.com/DanyaSWorlD"] -__version__ = "2.1.11.post1" +__version__ = "2.1.12" from pathlib import Path import shutil diff --git a/exchanges_wrapper/client.py b/exchanges_wrapper/client.py index 0db105f..d08b53a 100644 --- a/exchanges_wrapper/client.py +++ b/exchanges_wrapper/client.py @@ -109,6 +109,8 @@ def __init__(self, *acc): self.ledgers_id = [] self.ts_start = {} self.tasks = set() + self.request_event = asyncio.Event() + self.request_event.set() def tasks_manage(self, coro): _t = asyncio.create_task(coro) @@ -377,24 +379,10 @@ async def fetch_exchange_info(self, symbol): # MARKET DATA ENDPOINTS # https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md#order-book - async def fetch_order_book(self, symbol, precision='P0', limit=100): + async def fetch_order_book(self, symbol, precision='P0'): self.assert_symbol(symbol) - valid_limits = [] - if self.exchange == 'binance': - valid_limits = [5, 10, 20, 50, 100, 500, 1000, 5000] - elif self.exchange == 'bitfinex': - valid_limits = [1, 25, 100] - elif self.exchange == 'huobi': - valid_limits = [5, 10, 20] - elif self.exchange == 'okx': - valid_limits = [1, 5, 10, 20, 50, 100, 400] - elif self.exchange == 'bybit': - valid_limits = range(1, 51) + limit = 1 if self.exchange in ('bitfinex', 'okx', 'bybit') else 5 binance_res = {} - if limit not in valid_limits: - raise ValueError( - f"{limit} is not a valid limit. Valid limits: {valid_limits}" - ) if self.exchange == 'binance': binance_res = await self.http.send_api_call( "/api/v3/depth", diff --git a/exchanges_wrapper/exch_srv.py b/exchanges_wrapper/exch_srv.py index f45f32a..0357272 100755 --- a/exchanges_wrapper/exch_srv.py +++ b/exchanges_wrapper/exch_srv.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- + import grpclib.exceptions -from exchanges_wrapper import __version__ as ver_ew -from crypto_ws_api import __version__ as ver_cw +from exchanges_wrapper import __version__ as VER_EW +from crypto_ws_api import __version__ as VER_CW import time import weakref import gc @@ -148,10 +149,10 @@ class Martin(mr.MartinBase): rate_limit_reached_time = None rate_limiter = None - async def rate_limit_control(self, client, _call_from='default'): - if client.client.exchange == 'bitfinex': - rate_limit_interval = REST_RATE_LIMIT_INTERVAL.get(client.client.exchange, {}).get(_call_from, 0) - ts_diff = time.time() - client.ts_rlc + async def rate_limit_control(self, exchange, ts_rlc, _call_from='default'): + if exchange == 'bitfinex': + rate_limit_interval = REST_RATE_LIMIT_INTERVAL.get(exchange, {}).get(_call_from, 0) + ts_diff = time.time() - ts_rlc if ts_diff < rate_limit_interval: sleep_duration = rate_limit_interval - ts_diff await asyncio.sleep(sleep_duration) @@ -201,7 +202,7 @@ async def open_client_connection(self, request: mr.OpenClientConnectionRequest) Martin.rate_limiter = max(Martin.rate_limiter or 0, request.rate_limiter) return mr.OpenClientConnectionId( client_id=client_id, - srv_version=f"{ver_cw}:{ver_ew}", + srv_version=f"{VER_CW}:{VER_EW}", exchange=open_client.client.exchange, real_market=open_client.real_market ) @@ -231,7 +232,10 @@ async def send_request(self, client_method_name, request, rate_limit=False, **kw if hasattr(request, 'client_order_id'): msg_header += f"({request.client_order_id}):" if rate_limit: - await self.rate_limit_control(open_client) + await self.rate_limit_control(client.exchange, open_client.ts_rlc) + if client.exchange == 'bitfinex': + await client.request_event.wait() + client.request_event.clear() try: res = await asyncio.wait_for(getattr(client, client_method_name)(**kwargs), timeout=HEARTBEAT * 60) except asyncio.exceptions.CancelledError: @@ -263,6 +267,8 @@ async def send_request(self, client_method_name, request, rate_limit=False, **kw if rate_limit: open_client.ts_rlc = time.time() return res, client, msg_header + finally: + client.request_event.set() async def fetch_server_time(self, request: mr.OpenClientConnectionId) -> mr.FetchServerTimeResponse: res, _, _ = await self.send_request('fetch_server_time', request, rate_limit=True) @@ -432,36 +438,39 @@ async def fetch_funding_wallet(self, request: mr.FetchFundingWalletRequest) -> m return response async def fetch_order_book(self, request: mr.MarketRequest) -> mr.FetchOrderBookResponse: - open_client = OpenClient.get_client(request.client_id) - client = open_client.client response = mr.FetchOrderBookResponse() - await self.rate_limit_control(open_client) - limit = 1 if client.exchange in ('bitfinex', 'okx') else 5 - res = await client.fetch_order_book(symbol=request.symbol, limit=limit) - open_client.ts_rlc = time.time() + res, _, _ = await self.send_request( + 'fetch_order_book', + request, + rate_limit=True, + symbol=request.symbol + ) + res['bids'] = [json.dumps(v) for v in res.get('bids', [])] res['asks'] = [json.dumps(v) for v in res.get('asks', [])] return response.from_pydict(res) async def fetch_symbol_price_ticker(self, request: mr.MarketRequest) -> mr.FetchSymbolPriceTickerResponse: - open_client = OpenClient.get_client(request.client_id) - client = open_client.client response = mr.FetchSymbolPriceTickerResponse() - await self.rate_limit_control(open_client) - res = await client.fetch_symbol_price_ticker(symbol=request.symbol) - open_client.ts_rlc = time.time() + res, _, _ = await self.send_request( + 'fetch_symbol_price_ticker', + request, + rate_limit=True, + symbol=request.symbol + ) return response.from_pydict(res) async def fetch_ticker_price_change_statistics( self, request: mr.MarketRequest ) -> mr.FetchTickerPriceChangeStatisticsResponse: - open_client = OpenClient.get_client(request.client_id) - client = open_client.client response = mr.FetchTickerPriceChangeStatisticsResponse() - await self.rate_limit_control(open_client) - res = await client.fetch_ticker_price_change_statistics(symbol=request.symbol) - open_client.ts_rlc = time.time() + res, _, _ = await self.send_request( + 'fetch_ticker_price_change_statistics', + request, + rate_limit=True, + symbol=request.symbol + ) return response.from_pydict(res) async def fetch_klines(self, request: mr.FetchKlinesRequest) -> mr.JsonResponse: @@ -667,15 +676,13 @@ async def on_balance_update(self, request: mr.MarketRequest) -> mr.StreamRespons _get_event_from_queue = False if client.exchange in ('bitfinex', 'huobi', 'bybit'): - await self.rate_limit_control(open_client) - try: - balances = await client.fetch_ledgers(request.symbol) - except Exception as _ex: - logger.warning(f"OnBalanceUpdate: for {open_client.name}:{request.symbol}: {_ex}") - logger.debug(f"OnBalanceUpdate: {traceback.format_exc()}") - else: - open_client.ts_rlc = time.time() - [_events.append(client.events.wrap_event(balance)) for balance in balances] + balances, _, _ = await self.send_request( + 'fetch_ledgers', + request, + rate_limit=True, + symbol=request.symbol + ) + [_events.append(client.events.wrap_event(balance)) for balance in balances] for _event in _events: if _event.asset in request.symbol: @@ -851,7 +858,7 @@ async def amain(host: str = '127.0.0.1', port: int = 50051): server = Server([Martin()]) with graceful_exit([server]): await server.start(host, port) - logger.info(f"Starting server v:{ver_cw}:{ver_ew} on {host}:{port}") + logger.info(f"Starting server v:{VER_CW}:{VER_EW} on {host}:{port}") await server.wait_closed() for oc in OpenClient.open_clients: diff --git a/pyproject.toml b/pyproject.toml index b3db409..0611ed5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ dynamic = ["version", "description"] requires-python = ">=3.9" dependencies = [ - "crypto-ws-api==2.0.10", + "crypto-ws-api==2.0.11", "grpcio==1.62.0", "pyotp~=2.9.0", "simplejson==3.19.2", diff --git a/requirements.txt b/requirements.txt index 9675089..8de6164 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -crypto-ws-api==2.0.10 +crypto-ws-api==2.0.11 pyotp==2.9.0 simplejson==3.19.2 toml~=0.10.2