From 0c6b8952dceac85d12eda1b7782e1b2e44860fb3 Mon Sep 17 00:00:00 2001 From: Carlos Wu Fei Date: Sun, 29 Oct 2023 16:56:01 +0000 Subject: [PATCH] Centralize and refactor qty precision and isolated balance with getters --- api/account/account.py | 9 --------- api/account/assets.py | 7 +++---- api/apis.py | 4 ++++ api/bots/controllers.py | 8 ++++++++ api/deals/base.py | 43 ++++++++++++++++++++-------------------- api/deals/controllers.py | 1 - api/deals/margin.py | 39 ++++++++++-------------------------- api/market_updates.py | 11 ++++------ api/orders/controller.py | 21 ++++++++++++++------ api/orders/routes.py | 14 +++++++++---- 10 files changed, 75 insertions(+), 82 deletions(-) diff --git a/api/account/account.py b/api/account/account.py index 03cb3560d..eefae30ea 100644 --- a/api/account/account.py +++ b/api/account/account.py @@ -220,15 +220,6 @@ def min_notional_by_symbol(self, symbol, min_notional_limit="minNotional"): ) return min_notional_filter[min_notional_limit] - def get_qty_precision(self, pair): - lot_size_by_symbol = self.lot_size_by_symbol(pair, "stepSize") - qty_precision = -( - Decimal(str(lot_size_by_symbol)) - .as_tuple() - .exponent - ) - return qty_precision - def get_one_balance(self, symbol="BTC"): # Response after request data = self.bb_request(url=self.bb_balance_url) diff --git a/api/account/assets.py b/api/account/assets.py index abafd4f39..bca1ce434 100644 --- a/api/account/assets.py +++ b/api/account/assets.py @@ -16,7 +16,6 @@ class Assets(BaseDeal): def __init__(self): self.db = setup_db() self.usd_balance = 0 - self.isolated_balance = None def get_raw_balance(self, asset=None): """ @@ -389,13 +388,13 @@ def one_click_liquidation(self, pair: str): in that it contains some clean up functionality in the cases where there are are still funds in the isolated pair """ - + try: - self.margin_liquidation(pair) + self.symbol = pair + self.margin_liquidation(pair, self.qty_precision) return json_response_message(f"Successfully liquidated {pair}") except MarginLoanNotFound as error: return json_response_message(f"{error}. Successfully cleared isolated pair {pair}") except BinanceErrors as error: return json_response_error(f"Error liquidating {pair}: {error.message}") - diff --git a/api/apis.py b/api/apis.py index ea427f8d3..322815c82 100644 --- a/api/apis.py +++ b/api/apis.py @@ -53,6 +53,7 @@ class BinanceApi: margin_repay_url = f"{BASE}/sapi/v1/margin/repay" isolated_hourly_interest = f"{BASE}/sapi/v1/margin/next-hourly-interest-rate" margin_order = f"{BASE}/sapi/v1/margin/order" + max_borrow_url = f"{BASE}/sapi/v1/margin/maxBorrowable" def signed_request(self, url, method="GET", payload={}, params={}): """ @@ -117,6 +118,9 @@ def create_margin_loan(self, asset, symbol, amount, isIsolated=True): return self.signed_request(self.loan_record_url, method="POST", payload={"asset": asset, "symbol": symbol, "amount": amount, "isIsolated": isIsolated}) + def get_max_borrow(self, asset, isolated_symbol:str | None = None): + return self.signed_request(self.max_borrow_url, payload={"asset": asset, "isolatedSymbol": isolated_symbol }) + def get_margin_loan_details(self, asset: str, isolatedSymbol: str): return self.signed_request(self.loan_record_url, payload={"asset": asset, "isolatedSymbol": isolatedSymbol}) diff --git a/api/bots/controllers.py b/api/bots/controllers.py index 989970504..193acfafc 100644 --- a/api/bots/controllers.py +++ b/api/bots/controllers.py @@ -116,6 +116,14 @@ def create(self, data): bot = data.dict() bot["id"] = str(ObjectId()) + # if bot["strategy"] == "margin_short": + # asset = bot["pair"].replace(bot["balance_to_use"], "") + # # price = self.matching_engine(bot["pair"], True) + # total = float(bot["base_order_size"]) + # check_max_borrow = self.get_max_borrow(asset, isolated_symbol=bot["pair"]) + # if total > check_max_borrow["borrowLimit"]: + # return json_response_error(f"Max margin account borrow limit reached") + self.db_collection.insert_one(bot) resp = json_response( { diff --git a/api/deals/base.py b/api/deals/base.py index 59b745327..4be1c9345 100644 --- a/api/deals/base.py +++ b/api/deals/base.py @@ -1,3 +1,4 @@ +from typing import List import uuid import requests import numpy @@ -32,9 +33,8 @@ class BaseDeal(OrderController): def __init__(self, bot, db_collection_name): self.active_bot = BotSchema.parse_obj(bot) - self.isolated_balance = None - self.qty_precision = None - super().__init__() + self.symbol = self.active_bot.pair + super().__init__(symbol=self.active_bot.pair) self.db_collection = self.db[db_collection_name] def __repr__(self) -> str: @@ -46,6 +46,12 @@ def __repr__(self) -> str: def generate_id(self): return uuid.uuid4().hex + @property + def isolated_balance(self): + if not hasattr(self, "_isolated_balance"): + self._isolated_balance = self.get_isolated_balance(self.symbol) + return self._isolated_balance + def compute_qty(self, pair): """ Helper function to compute buy_price. @@ -60,7 +66,7 @@ def compute_qty(self, pair): return qty def compute_margin_buy_back( - self, pair: str, qty_precision + self, pair: str ): """ Same as compute_qty but with isolated margin balance @@ -70,9 +76,6 @@ def compute_margin_buy_back( Decimals have to be rounded up to avoid leaving "leftover" interests """ - if not self.isolated_balance: - self.isolated_balance = self.get_isolated_balance(pair) - if ( self.isolated_balance[0]["quoteAsset"]["free"] == 0 or self.isolated_balance[0]["baseAsset"]["borrowed"] == 0 @@ -81,12 +84,6 @@ def compute_margin_buy_back( qty = float(self.isolated_balance[0]["baseAsset"]["borrowed"]) + float(self.isolated_balance[0]["baseAsset"]["interest"]) + float(self.isolated_balance[0]["baseAsset"]["borrowed"]) * ESTIMATED_COMMISSIONS_RATE - # Save API calls - self.qty_precision = qty_precision - - if not self.qty_precision: - self.qty_precision = self.get_qty_precision(pair) - qty = round_numbers_ceiling(qty, self.qty_precision) free = float(self.isolated_balance[0]["baseAsset"]["free"]) @@ -227,7 +224,6 @@ def create_new_bot_streaming(self): bot_response = self.db_collection.insert_one( bot, - return_document=ReturnDocument.AFTER, ) except Exception as error: @@ -309,22 +305,25 @@ def dynamic_take_profit(self, current_bot, close_price): def margin_liquidation(self, pair: str, qty_precision=None): """ - Emulate Binance Dashboard - One click liquidation function + Emulate Binance Dashboard One click liquidation function + + Args: + - pair: a.k.a symbol, quote asset + base asset + - qty_precision: to round numbers for Binance API. Passed optionally to + reduce number of requests to avoid rate limit. """ - isolated_balance = self.get_isolated_balance(pair) + isolated_balance = self.isolated_balance base = isolated_balance[0]["baseAsset"]["asset"] quote = isolated_balance[0]["quoteAsset"]["asset"] # Check margin account balance first borrowed_amount = float(isolated_balance[0]["baseAsset"]["borrowed"]) free = float(isolated_balance[0]["baseAsset"]["free"]) buy_margin_response = None - qty_precision = self.get_qty_precision(pair) if borrowed_amount > 0: # repay_amount contains total borrowed_amount + interests + commissions for buying back # borrow amount is only the loan - repay_amount, free = self.compute_margin_buy_back(pair, qty_precision) + repay_amount, free = self.compute_margin_buy_back(pair) repay_amount = round_numbers_ceiling(repay_amount, qty_precision) if free == 0 or free < repay_amount: @@ -335,7 +334,7 @@ def margin_liquidation(self, pair: str, qty_precision=None): symbol=pair, qty=qty, ) - repay_amount, free = self.compute_margin_buy_back(pair, qty_precision) + repay_amount, free = self.compute_margin_buy_back(pair) except BinanceErrors as error: if error.code == -3041: # Not enough funds in isolated pair @@ -351,7 +350,7 @@ def margin_liquidation(self, pair: str, qty_precision=None): amount=amount_to_transfer, ) buy_margin_response = self.buy_margin_order(pair, supress_notation(transfer_diff_qty, qty_precision)) - repay_amount, free = self.compute_margin_buy_back(pair, qty_precision) + repay_amount, free = self.compute_margin_buy_back(pair) pass if error.code == -2010 or error.code == -1013: # There is already money in the base asset @@ -362,7 +361,7 @@ def margin_liquidation(self, pair: str, qty_precision=None): qty = round_numbers_ceiling(15 / price) buy_margin_response = self.buy_margin_order(pair, supress_notation(qty, qty_precision)) - repay_amount, free = self.compute_margin_buy_back(pair, qty_precision) + repay_amount, free = self.compute_margin_buy_back(pair) pass self.repay_margin_loan( diff --git a/api/deals/controllers.py b/api/deals/controllers.py index 2f6de1eb0..3c0e34a34 100644 --- a/api/deals/controllers.py +++ b/api/deals/controllers.py @@ -37,7 +37,6 @@ class CreateDealController(BaseDeal): def __init__(self, bot, db_collection="paper_trading"): # Inherit from parent class super().__init__(bot, db_collection) - self.active_bot = BotSchema.parse_obj(bot) def get_one_balance(self, symbol="BTC"): # Response after request diff --git a/api/deals/margin.py b/api/deals/margin.py index 6a6fec183..d01bff72b 100644 --- a/api/deals/margin.py +++ b/api/deals/margin.py @@ -22,7 +22,6 @@ class MarginDeal(BaseDeal): def __init__(self, bot, db_collection_name: str) -> None: # Inherit from parent class super().__init__(bot, db_collection_name) - self.isolated_balance = self.get_isolated_balance(self.active_bot.pair) def _append_errors(self, error): """ @@ -141,7 +140,7 @@ def init_margin_short(self, initial_price): try: self.enable_isolated_margin_account(symbol=self.active_bot.pair) except BinanceErrors as error: - if error.code == -11001: + if error.code == -11001 or error.code == -3052: # Isolated margin account needs to be activated with a transfer self.transfer_spot_to_isolated_margin( asset=self.active_bot.balance_to_use, @@ -149,6 +148,8 @@ def init_margin_short(self, initial_price): amount="1", ) + transfer_qty = float(self.active_bot.base_order_size) + # Given USDT amount we want to buy, # how much can we buy? qty = round_numbers_ceiling( @@ -156,20 +157,6 @@ def init_margin_short(self, initial_price): self.qty_precision, ) - # transfer quantity is base order size (what we want to invest) + stop loss to cover losses - stop_loss_price_inc = float(initial_price) * ( - 1 + (self.active_bot.stop_loss / 100) - ) - final_qty = float(stop_loss_price_inc * qty) - transfer_qty = round_numbers_ceiling( - final_qty, - self.qty_precision, - ) - - if transfer_qty == 0: - raise QuantityTooLow("Margin short quantity is too low") - - # For leftover values # or transfers to activate isolated pair # sometimes to activate an isolated pair we need to transfer sth @@ -385,7 +372,7 @@ def margin_short_base_order(self): self.init_margin_short(initial_price) order_res = self.sell_margin_order( symbol=self.active_bot.pair, - qty=self.active_bot.base_order_size, + qty=self.active_bot.deal.margin_short_base_order, ) else: # Simulate Margin sell @@ -495,8 +482,12 @@ def streaming_updates(self, close_price: str): ) try: self.execute_stop_loss() - except MarginLoanNotFound: - pass + except MarginLoanNotFound as error: + self.update_deal_logs( + f"{error.message}" + ) + self.active_bot.status = Status.error + return if ( hasattr(self.active_bot, "margin_short_reversal") @@ -573,14 +564,6 @@ def execute_stop_loss(self): self.update_deal_logs(error.message) return - except Exception as error: - self.update_deal_logs( - f"Error trying to open new stop_limit order {error}" - ) - # Continue in case of error to execute long bot - # to avoid losses - return - stop_loss_order = MarginOrderSchema( timestamp=res["transactTime"], deal_type="stop_loss", @@ -624,9 +607,7 @@ def execute_take_profit(self, price=None): - Buy back asset sold """ - qty = 1 if self.db_collection.name == "bots": - qty, free = self.compute_margin_buy_back(self.active_bot.pair, self.qty_precision) self.cancel_open_orders("take_profit") # Margin buy (buy back) diff --git a/api/market_updates.py b/api/market_updates.py index 47fbd0a9a..5eaf8831c 100644 --- a/api/market_updates.py +++ b/api/market_updates.py @@ -57,13 +57,10 @@ mu = StreamingController() mu.get_klines() -except WebSocketException as e: - if isinstance(e, WebSocketConnectionClosedException): - logging.error("Lost websocket connection") - mu = StreamingController() - mu.get_klines() - else: - logging.error(f"Websocket exception: {e}") +except WebSocketConnectionClosedException as e: + logging.error("Lost websocket connection") + mu = StreamingController() + mu.get_klines() atexit.register(lambda: scheduler.shutdown(wait=False)) diff --git a/api/orders/controller.py b/api/orders/controller.py index 7e56573d7..f0e049e1a 100644 --- a/api/orders/controller.py +++ b/api/orders/controller.py @@ -8,22 +8,31 @@ poll_percentage = 0 class OrderController(Account): - def __init__(self) -> None: + def __init__(self, symbol) -> None: super().__init__() # Always GTC and limit orders # limit/market orders will be decided by matching_engine # PRICE_FILTER decimals - self.price_precision = -1 * ( - Decimal(str(self.price_filter_by_symbol(self.active_bot.pair, "tickSize"))) + self.symbol = symbol + pass + + @property + def price_precision(self): + self._price_precision = -1 * ( + Decimal(str(self.price_filter_by_symbol(self.symbol, "tickSize"))) .as_tuple() .exponent ) - self.qty_precision = -1 * ( - Decimal(str(self.lot_size_by_symbol(self.active_bot.pair, "stepSize"))) + return self._price_precision + + @property + def qty_precision(self): + self._qty_precision = -1 * ( + Decimal(str(self.lot_size_by_symbol(self.symbol, "stepSize"))) .as_tuple() .exponent ) - pass + return self._qty_precision def sell_order(self, symbol, qty, price=None): """ diff --git a/api/orders/routes.py b/api/orders/routes.py index d6e88b7a6..1aac54204 100644 --- a/api/orders/routes.py +++ b/api/orders/routes.py @@ -8,20 +8,26 @@ @order_blueprint.post("/buy", tags=["orders"]) def create_buy_order(item: OrderParams): - return OrderController().buy_order(item) + return OrderController(symbol=item.pair).buy_order(item) @order_blueprint.post("/sell", tags=["orders"]) def create_sell_order(item: OrderParams): - return OrderController().sell_order(item) + return OrderController(symbol=item.pair).sell_order(item) @order_blueprint.get("/open", tags=["orders"]) def get_open_orders(): - return OrderController().get_open_orders() + # Symbol not required + return OrderController(symbol=None).get_open_orders() @order_blueprint.delete("/close/{symbol}/{orderid}", tags=["orders"]) def delete_order(symbol, orderid): - return OrderController().delete_order(symbol, orderid) + return OrderController(symbol=symbol).delete_order(symbol, orderid) + + +@order_blueprint.get("/margin/sell/{symbol}/{qty}", tags=["orders"]) +def margin_sell(symbol, qty): + return OrderController(symbol=symbol).sell_margin_order(symbol, qty) \ No newline at end of file