Skip to content

Commit

Permalink
Centralize and refactor qty precision and isolated balance with getters
Browse files Browse the repository at this point in the history
  • Loading branch information
Carlos Wu Fei authored and Carlos Wu Fei committed Nov 1, 2023
1 parent 3bdfaf5 commit 651eca2
Show file tree
Hide file tree
Showing 9 changed files with 69 additions and 75 deletions.
9 changes: 0 additions & 9 deletions api/account/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 3 additions & 4 deletions api/account/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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}")


4 changes: 4 additions & 0 deletions api/apis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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={}):
"""
Expand Down Expand Up @@ -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})

Expand Down
8 changes: 8 additions & 0 deletions api/bots/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]) / float(price)
check_max_borrow = self.get_max_borrow(asset, isolated_symbol=bot["pair"])
if total > check_max_borrow:
return json_response_error(f"Max margin account borrow limit reached")

self.db_collection.insert_one(bot)
resp = json_response(
{
Expand Down
43 changes: 21 additions & 22 deletions api/deals/base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from typing import List
import uuid
import requests
import numpy
Expand Down Expand Up @@ -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:
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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"])

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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(
Expand Down
1 change: 0 additions & 1 deletion api/deals/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 8 additions & 29 deletions api/deals/margin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -156,20 +155,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
Expand All @@ -179,7 +164,7 @@ def init_margin_short(self, initial_price):
self.transfer_spot_to_isolated_margin(
asset=self.active_bot.balance_to_use,
symbol=self.active_bot.pair,
amount=transfer_qty,
amount=qty,
)
except BinanceErrors as error:
if error.code == -3041:
Expand Down Expand Up @@ -385,7 +370,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
Expand Down Expand Up @@ -495,8 +480,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")
Expand Down Expand Up @@ -573,14 +562,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",
Expand Down Expand Up @@ -624,9 +605,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)
Expand Down
21 changes: 15 additions & 6 deletions api/orders/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
14 changes: 10 additions & 4 deletions api/orders/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

0 comments on commit 651eca2

Please sign in to comment.