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 18, 2023
1 parent 3bdfaf5 commit 1e70634
Show file tree
Hide file tree
Showing 11 changed files with 114 additions and 93 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
13 changes: 5 additions & 8 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 @@ -363,6 +362,7 @@ async def disable_isolated_accounts(self, symbol=None):
Check and disable isolated accounts
"""
info = self.signed_request(url=self.isolated_account_url, payload={})
msg = "Disabling isolated margin account not required yet."
for item in info["assets"]:
# Liquidate price = 0 guarantees there is no loan unpaid
if float(item["liquidatePrice"]) == 0:
Expand All @@ -374,11 +374,8 @@ async def disable_isolated_accounts(self, symbol=None):

self.disable_isolated_margin_account(item["symbol"])
msg = "Sucessfully finished disabling isolated margin accounts."
else:
msg = "Disabling isolated margin account not required yet."

resp = json_response_message(msg)
return resp
return json_response_message(msg)

def one_click_liquidation(self, pair: str):
"""
Expand All @@ -389,13 +386,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"])
# 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(
{
Expand Down
24 changes: 23 additions & 1 deletion api/charts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,25 @@ def bollinguer_bands(self, df, dates):

return ma_100, ma_25, ma_7

def macd(self, df):
"""
Moving Average Convergence Divergence (MACD) indicator
https://www.alpharithms.com/calculate-macd-python-272222/
"""

k = df[4].ewm(span=12, adjust=False, min_periods=12).mean()
# Get the 12-day EMA of the closing price
d = df[4].ewm(span=26, adjust=False, min_periods=26).mean()
# Subtract the 26-day EMA from the 12-Day EMA to get the MACD
macd = k - d
# Get the 9-Day EMA of the MACD for the Trigger line
# Get the 9-Day EMA of the MACD for the Trigger line
macd_s = macd.ewm(span=9, adjust=False, min_periods=9).mean()
# Calculate the difference between the MACD - Trigger for the Convergence/Divergence value
macd_h = macd - macd_s

return macd, macd_s

def get(self, symbol, interval="15m", limit=500, start_time: float | None=None, end_time: float | None=None, stats = False):
"""
Get candlestick graph data
Expand Down Expand Up @@ -351,6 +370,7 @@ def get(self, symbol, interval="15m", limit=500, start_time: float | None=None,

trace = self.candlestick_trace(df, dates)
ma_100, ma_25, ma_7 = self.bollinguer_bands(df, dates)
macd, macd_signal = self.macd(df)

if stats:
high_price = max(df[2])
Expand Down Expand Up @@ -380,12 +400,14 @@ def get(self, symbol, interval="15m", limit=500, start_time: float | None=None,
"high_price": high_price_r,
"low_price": low_price_r,
"close_price": close_price_r,
"volume": volume_r
"volume": volume_r,
}

resp = json_response(
{
"trace": [trace, ma_100, ma_25, ma_7],
"macd": macd,
"macd_signal": macd_signal,
"interval": interval,
"volumes": volumes,
"amplitude": amplitude,
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
39 changes: 10 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 @@ -141,35 +140,23 @@ 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,
symbol=self.active_bot.pair,
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(
(float(self.active_bot.base_order_size) / float(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 Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 1e70634

Please sign in to comment.