diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e0b7fc..bd4b19c 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.15 2024-07-12 +### Added for new features +* `Binance`: add method `transfer_to_sub()`. See use example in `example/exch_client.py` + ## 2.1.14 2024-07-07 ### Fix * `Bybit`: `fetch_ledgers()` doubling of incoming transfers to a subaccount diff --git a/example/exch_client.py b/example/exch_client.py index bfe8a03..45b73ac 100644 --- a/example/exch_client.py +++ b/example/exch_client.py @@ -15,7 +15,7 @@ FILE_CONFIG = 'ms_cfg.toml' config = toml.load(FILE_CONFIG) EXCHANGE = config.get('exchange') -SYMBOL = 'BTCUSDT' +SYMBOL = 'BNBUSDT' async def main(_exchange, _symbol): @@ -63,8 +63,10 @@ async def main(_exchange, _symbol): # Subscribe to WSS # First you want to create all WSS task # Market stream + # noinspection PyAsyncCall asyncio.create_task(on_ticker_update(stub, client_id, _symbol, trade_id)) # User Stream + # noinspection PyAsyncCall asyncio.create_task(on_order_update(stub, client_id, _symbol, trade_id)) # Other market and user methods are used similarly: OnKlinesUpdate, OnFundsUpdate, OnOrderBookUpdate # Start WSS @@ -336,6 +338,37 @@ async def transfer2master(_stub, symbol: str, amount: str): print(f"Not sent {amount} {symbol} to main account\n,{res.result}") +async def transfer2sub(_stub, email: str, symbol: str, amount: str): + """ + Send request to transfer asset from subaccount to subaccount + Binance sub to sub only + :param _stub: + :param email: + :param symbol: + :param amount: + :return: + """ + try: + res = await _stub.transfer_to_sub( + mr.MarketRequest, + symbol=symbol, + amount=amount, + data=email + ) + except asyncio.CancelledError: + pass # Task cancellation should not be logged as an error + except GRPCError as ex: + status_code = ex.status + print(f"Exception transfer {symbol} to sub account: {status_code.name}, {ex.message}") + except Exception as _ex: + print(f"Exception transfer {symbol} to sub account: {_ex}") + else: + if res.success: + print(f"Sent {amount} {symbol} to sub account {email}") + else: + print(f"Not sent {amount} {symbol} to sub account {email}\n,{res.result}") + + # Server exception handling example for methods where it's realized async def create_limit_order(_stub, _client_id, _symbol, _id: int, buy: bool, amount: str, price: str): """ diff --git a/example/ms_cfg.toml b/example/ms_cfg.toml index 64ef527..d13716c 100755 --- a/example/ms_cfg.toml +++ b/example/ms_cfg.toml @@ -1,5 +1,5 @@ # Example parameters for exchanges_wrapper/exch_client.py -# Accounts name wold be identically accounts.name from exch_srv_cfg.toml +# Accounts name would be identically accounts.name from exch_srv_cfg.toml exchange = "Demo - Binance" # exchange = "Demo - Bitfinex" # exchange = "Demo - OKX" diff --git a/exchanges_wrapper/__init__.py b/exchanges_wrapper/__init__.py index 7e905db..7313f33 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.14" +__version__ = "2.1.15" from pathlib import Path import shutil diff --git a/exchanges_wrapper/client.py b/exchanges_wrapper/client.py index b9b0791..2c17e80 100644 --- a/exchanges_wrapper/client.py +++ b/exchanges_wrapper/client.py @@ -703,7 +703,8 @@ async def fetch_ticker_price_change_statistics(self, symbol=None): # https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md#symbol-price-ticker async def fetch_symbol_price_ticker(self, symbol=None): if symbol: - self.assert_symbol_exists(symbol) + if not (self.exchange == 'binance' and symbol == 'BNBUSDT'): + self.assert_symbol_exists(symbol) binance_res = {} elif self.exchange in ('bitfinex', 'huobi'): raise ValueError('For fetch_symbol_price_ticker() symbol parameter required') @@ -1603,25 +1604,34 @@ async def fetch_funding_wallet(self, asset=None, need_btc_valuation=None, receiv binance_res = bbt.funding_wallet(res["balance"]) return binance_res + # https://developers.binance.com/docs/sub_account/asset-management/Transfer-to-Sub-account-of-Same-Master + async def transfer_to_sub(self, email, symbol, quantity, receive_window=None): + if self.exchange == 'binance': + quantity = any2str(Decimal(quantity).quantize(Decimal('0.01234567'), rounding=ROUND_HALF_DOWN)) + params = {"toEmail": email, "asset": symbol, "amount": quantity} + if receive_window: + params["recvWindow"] = receive_window + return await self.http.send_api_call( + "/sapi/v1/sub-account/transfer/subToSub", + "POST", + signed=True, + params=params + ) + else: + raise ValueError(f"Can't implemented for {self.exchange}") + # https://binance-docs.github.io/apidocs/spot/en/#transfer-to-master-for-sub-account async def transfer_to_master(self, symbol, quantity, receive_window=None): - quantity = any2str(Decimal(quantity).quantize(Decimal('0.01234567'), rounding=ROUND_HALF_DOWN)) - + _quantity = any2str(Decimal(quantity).quantize(Decimal('0.01234567'), rounding=ROUND_HALF_DOWN)) binance_res = {} if self.exchange == 'binance': - params = {"asset": symbol, "amount": quantity} - if receive_window: - params["recvWindow"] = receive_window if self.master_email: - logger.info(f"Collect {quantity}{symbol} to {self.master_email} sub-account") - params["toEmail"] = self.master_email - binance_res = await self.http.send_api_call( - "/sapi/v1/sub-account/transfer/subToSub", - "POST", - signed=True, - params=params - ) + logger.info(f"Collect {_quantity}{symbol} to {self.master_email} sub-account") + binance_res = await self.transfer_to_sub(self.master_email, symbol, quantity, receive_window) else: + params = {"asset": symbol, "amount": _quantity} + if receive_window: + params["recvWindow"] = receive_window binance_res = await self.http.send_api_call( "/sapi/v1/sub-account/transfer/subToMaster", "POST", @@ -1636,7 +1646,7 @@ async def transfer_to_master(self, symbol, quantity, receive_window=None): "from": "exchange", "to": "exchange", "currency": symbol, - "amount": quantity, + "amount": _quantity, "email_dst": self.master_email, "tfaToken": {"method": "otp", "token": totp.now()} } @@ -1658,7 +1668,7 @@ async def transfer_to_master(self, symbol, quantity, receive_window=None): 'to-account-type': "spot", 'to-account': self.main_account_id, 'currency': symbol.lower(), - 'amount': quantity + 'amount': _quantity } res = await self.http.send_api_call( "v1/account/transfer", @@ -1670,7 +1680,7 @@ async def transfer_to_master(self, symbol, quantity, receive_window=None): elif self.exchange == 'okx': params = { "ccy": symbol, - "amt": quantity, + "amt": _quantity, "from": '18', "to": '18', "type": '3' @@ -1692,7 +1702,7 @@ async def transfer_to_master(self, symbol, quantity, receive_window=None): params = { 'transferId': str(uuid.uuid4()), 'coin': symbol, - 'amount': str(math.floor(float(quantity) * 10 ** n) / 10 ** n), + 'amount': str(math.floor(float(_quantity) * 10 ** n) / 10 ** n), 'fromMemberId': self.account_uid, 'toMemberId': self.main_account_uid, 'fromAccountType': 'UNIFIED', diff --git a/exchanges_wrapper/exch_srv.py b/exchanges_wrapper/exch_srv.py index 0357272..23c83cf 100755 --- a/exchanges_wrapper/exch_srv.py +++ b/exchanges_wrapper/exch_srv.py @@ -767,6 +767,24 @@ async def cancel_order(self, request: mr.CancelOrderRequest) -> mr.CancelOrderRe response.from_pydict(res) return response + async def transfer_to_sub(self, request: mr.MarketRequest) -> mr.SimpleResponse: + response = mr.SimpleResponse() + response.success = False + + res, _, _ = await self.send_request( + 'transfer_to_sub', + request, + rate_limit=True, + email=request.data, + symbol=request.symbol, + quantity=request.amount + ) + + if res and res.get("txnId"): + response.success = True + response.result = json.dumps(res) + return response + async def transfer_to_master(self, request: mr.MarketRequest) -> mr.SimpleResponse: response = mr.SimpleResponse() response.success = False diff --git a/exchanges_wrapper/martin/__init__.py b/exchanges_wrapper/martin/__init__.py index da59592..871e8ac 100644 --- a/exchanges_wrapper/martin/__init__.py +++ b/exchanges_wrapper/martin/__init__.py @@ -407,6 +407,7 @@ class MarketRequest(betterproto.Message): trade_id: str = betterproto.string_field(2) symbol: str = betterproto.string_field(3) amount: str = betterproto.string_field(4) + data: str = betterproto.string_field(5) @dataclass(eq=False, repr=False) @@ -916,6 +917,23 @@ async def transfer_to_master( metadata=metadata, ) + async def transfer_to_sub( + self, + market_request: "MarketRequest", + *, + timeout: Optional[float] = None, + deadline: Optional["Deadline"] = None, + metadata: Optional["MetadataLike"] = None + ) -> "SimpleResponse": + return await self._unary_unary( + "/martin.Martin/TransferToSub", + market_request, + SimpleResponse, + timeout=timeout, + deadline=deadline, + metadata=metadata, + ) + class MartinBase(ServiceBase): @@ -1056,6 +1074,11 @@ async def transfer_to_master( ) -> "SimpleResponse": raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + async def transfer_to_sub( + self, market_request: "MarketRequest" + ) -> "SimpleResponse": + raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED) + async def __rpc_cancel_all_orders( self, stream: "grpclib.server.Stream[MarketRequest, SimpleResponse]" ) -> None: @@ -1270,6 +1293,13 @@ async def __rpc_transfer_to_master( response = await self.transfer_to_master(request) await stream.send_message(response) + async def __rpc_transfer_to_sub( + self, stream: "grpclib.server.Stream[MarketRequest, SimpleResponse]" + ) -> None: + request = await stream.recv_message() + response = await self.transfer_to_sub(request) + await stream.send_message(response) + def __mapping__(self) -> Dict[str, grpclib.const.Handler]: return { "/martin.Martin/CancelAllOrders": grpclib.const.Handler( @@ -1434,4 +1464,10 @@ def __mapping__(self) -> Dict[str, grpclib.const.Handler]: MarketRequest, SimpleResponse, ), + "/martin.Martin/TransferToSub": grpclib.const.Handler( + self.__rpc_transfer_to_sub, + grpclib.const.Cardinality.UNARY_UNARY, + MarketRequest, + SimpleResponse, + ), } diff --git a/exchanges_wrapper/proto/martin.proto b/exchanges_wrapper/proto/martin.proto index 66dd1a5..8f013a3 100644 --- a/exchanges_wrapper/proto/martin.proto +++ b/exchanges_wrapper/proto/martin.proto @@ -38,6 +38,7 @@ service Martin { rpc StartStream (StartStreamRequest) returns (SimpleResponse) {} rpc StopStream (MarketRequest) returns (SimpleResponse) {} rpc TransferToMaster(MarketRequest) returns (SimpleResponse) {} + rpc TransferToSub(MarketRequest) returns (SimpleResponse) {} } message JSONResponse { @@ -368,6 +369,7 @@ message MarketRequest { string trade_id = 2; string symbol = 3; string amount = 4; + string data = 5; } message StartStreamRequest { diff --git a/pyproject.toml b/pyproject.toml index 695a67a..1419911 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,6 @@ dependencies = [ "pyotp~=2.9.0", "simplejson==3.19.2", "aiohttp==3.9.5", - "Pympler~=1.0.1", "websockets~=12.0", "expiringdict~=1.2.2", "ujson~=5.10.0", diff --git a/requirements.txt b/requirements.txt index 6620899..d1b2d8c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,6 @@ pyotp==2.9.0 simplejson==3.19.2 toml~=0.10.2 aiohttp~=3.9.5 -Pympler~=1.0.1 websockets==12.0 expiringdict~=1.2.2 ujson~=5.10.0