Skip to content

Commit

Permalink
Fix reversal bot creation
Browse files Browse the repository at this point in the history
  • Loading branch information
Carlos Wu Fei authored and Carlos Wu Fei committed Jan 19, 2024
1 parent df3d141 commit 601a8fe
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 170 deletions.
143 changes: 113 additions & 30 deletions api/deals/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from typing import List
import uuid
import requests
import numpy
Expand All @@ -11,21 +10,17 @@
from pymongo import ReturnDocument
from tools.round_numbers import round_numbers, supress_notation
from tools.handle_error import handle_binance_errors, encode_json
from tools.exceptions import BinanceErrors, MarginLoanNotFound
from tools.exceptions import BinanceErrors, DealCreationError, MarginLoanNotFound
from scipy.stats import linregress
from tools.round_numbers import round_numbers_ceiling
from tools.enum_definitions import Status, Strategy
from bson.objectid import ObjectId
from deals.schema import DealSchema, OrderSchema


# To be removed one day when commission endpoint found that provides this value
ESTIMATED_COMMISSIONS_RATE = 0.0075

class DealCreationError(Exception):
pass

class StreamingSaveError(Exception):
pass

class BaseDeal(OrderController):
"""
Base Deal class to share with CreateDealController and MarginDeal
Expand Down Expand Up @@ -61,9 +56,7 @@ def compute_qty(self, pair):
qty = round_numbers(balance, self.qty_precision)
return qty

def compute_margin_buy_back(
self, pair: str
):
def compute_margin_buy_back(self, pair: str):
"""
Same as compute_qty but with isolated margin balance
Expand All @@ -78,7 +71,12 @@ def compute_margin_buy_back(
):
return None

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
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
)

qty = round_numbers_ceiling(qty, self.qty_precision)
free = float(self.isolated_balance[0]["baseAsset"]["free"])
Expand Down Expand Up @@ -128,7 +126,6 @@ def update_deal_logs(self, msg):
{"id": self.active_bot.id},
{"$push": {"errors": msg}},
)
self.save_bot_streaming()
return response

def replace_order(self, cancel_order_id):
Expand All @@ -155,7 +152,11 @@ def close_open_orders(self, symbol):
open_orders = self.signed_request(self.open_orders, payload={"symbol": symbol})
for order in open_orders:
if order["status"] == "NEW":
self.signed_request(self.order_url, method="DELETE", payload={"symbol": symbol, "orderId": order["orderId"]})
self.signed_request(
self.order_url,
method="DELETE",
payload={"symbol": symbol, "orderId": order["orderId"]},
)
return True
return False

Expand Down Expand Up @@ -206,20 +207,98 @@ def create_new_bot_streaming(self):
it returns the saved bot, thus called streaming, it's
specifically for streaming saves
"""
self.active_bot.id = str(ObjectId())
self.active_bot.orders = []
self.active_bot.errors = []
self.active_bot.created_at = time() * 1000
self.active_bot.updated_at = time() * 1000
self.active_bot.status = Status.inactive
self.active_bot.deal = DealSchema()

bot = encode_json(self.active_bot)
self.db_collection.insert_one(bot)
new_bot = self.db_collection.find_one({"id": bot["id"]})
bot_class = BotSchema.parse_obj(new_bot)

return bot_class

def base_order(self):
"""
Required initial order to trigger long strategy bot.
Other orders require this to execute,
therefore should fail if not successful
1. Initial base purchase
2. Set take_profit
"""

pair = self.active_bot.pair

# Long position does not need qty in take_profit
# initial price with 1 qty should return first match
price = float(self.matching_engine(pair, True))
qty = round_numbers(
(float(self.active_bot.base_order_size) / float(price)),
self.qty_precision,
)
# setup stop_loss_price
stop_loss_price = 0
if float(self.active_bot.stop_loss) > 0:
stop_loss_price = price - (price * (float(self.active_bot.stop_loss) / 100))

if self.db_collection.name == "paper_trading":
res = self.simulate_order(
pair, supress_notation(price, self.price_precision), qty, "BUY"
)
else:
res = self.buy_order(
symbol=pair,
qty=qty,
price=supress_notation(price, self.price_precision),
)

order_data = OrderSchema(
timestamp=res["transactTime"],
order_id=res["orderId"],
deal_type="base_order",
pair=res["symbol"],
order_side=res["side"],
order_type=res["type"],
price=res["price"],
qty=res["origQty"],
fills=res["fills"],
time_in_force=res["timeInForce"],
status=res["status"],
)

self.active_bot.orders.append(order_data)
tp_price = float(res["price"]) * 1 + (float(self.active_bot.take_profit) / 100)

self.active_bot.deal = DealSchema(
buy_timestamp=res["transactTime"],
buy_price=res["price"],
buy_total_qty=res["origQty"],
current_price=res["price"],
take_profit_price=tp_price,
stop_loss_price=stop_loss_price,
)

# Activate bot
self.active_bot.status = Status.active

bot = encode_json(self.active_bot)
bot["id"] = self.generate_id()
if "_id" in bot:
bot.pop("_id")
bot.pop("_id") # _id is what causes conflict not id

bot_response = self.db_collection.insert_one(
bot,
document = self.db_collection.find_one_and_update(
{"id": self.active_bot.id},
{"$set": bot},
return_document=ReturnDocument.AFTER,
)

return bot_response.inserted_id
return document

def dynamic_take_profit(self, current_bot, close_price):

self.active_bot = BotSchema.parse_obj(current_bot)

params = {
Expand All @@ -245,8 +324,7 @@ def dynamic_take_profit(self, current_bot, close_price):
self.active_bot.deal.trailling_stop_loss_price > 0
and self.active_bot.deal.trailling_stop_loss_price
> self.active_bot.deal.base_order_price
and float(close_price)
> self.active_bot.deal.trailling_stop_loss_price
and float(close_price) > self.active_bot.deal.trailling_stop_loss_price
and (
(self.active_bot.strategy == "long" and slope > 0)
or (self.active_bot.strategy == "margin_short" and slope < 0)
Expand All @@ -267,9 +345,11 @@ def dynamic_take_profit(self, current_bot, close_price):
)
# deal.sd comparison will prevent it from making trailling_stop_loss too big
# and thus losing all the gains
if new_trailling_stop_loss_price > float(
self.active_bot.deal.buy_price
) and sd < self.active_bot.deal.sd:
if (
new_trailling_stop_loss_price
> float(self.active_bot.deal.buy_price)
and sd < self.active_bot.deal.sd
):
self.active_bot.trailling_deviation = volatility * 100
self.active_bot.deal.trailling_stop_loss_price = float(
close_price
Expand Down Expand Up @@ -327,15 +407,17 @@ def margin_liquidation(self, pair: str, qty_precision=None):
# transfer from wallet
transfer_diff_qty = round_numbers_ceiling(repay_amount - free)
available_balance = self.get_one_balance(quote)
amount_to_transfer = 15 # Min amount
amount_to_transfer = 15 # Min amount
if available_balance < 15:
amount_to_transfer = available_balance
self.transfer_spot_to_isolated_margin(
asset=quote,
symbol=pair,
amount=amount_to_transfer,
)
buy_margin_response = self.buy_margin_order(pair, supress_notation(transfer_diff_qty, qty_precision))
buy_margin_response = self.buy_margin_order(
pair, supress_notation(transfer_diff_qty, qty_precision)
)
repay_amount, free = self.compute_margin_buy_back(pair)
pass
if error.code == -2010 or error.code == -1013:
Expand All @@ -346,7 +428,9 @@ def margin_liquidation(self, pair: str, qty_precision=None):
if usdt_notional < 15:
qty = round_numbers_ceiling(15 / price)

buy_margin_response = self.buy_margin_order(pair, supress_notation(qty, qty_precision))
buy_margin_response = self.buy_margin_order(
pair, supress_notation(qty, qty_precision)
)
repay_amount, free = self.compute_margin_buy_back(pair)
pass

Expand All @@ -373,7 +457,7 @@ def margin_liquidation(self, pair: str, qty_precision=None):
symbol=pair,
amount=self.isolated_balance[0]["baseAsset"]["free"],
)

if borrowed_amount == 0:
# Funds are transferred back by now,
# disabling pair should be done by cronjob,
Expand All @@ -384,4 +468,3 @@ def margin_liquidation(self, pair: str, qty_precision=None):
raise MarginLoanNotFound("Isolated margin loan already liquidated")

return buy_margin_response

77 changes: 0 additions & 77 deletions api/deals/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from deals.base import BaseDeal
from deals.margin import MarginDeal
from deals.models import BinanceOrderModel
from deals.schema import DealSchema, OrderSchema
from pymongo import ReturnDocument
from tools.enum_definitions import Status
from tools.exceptions import (
Expand Down Expand Up @@ -59,82 +58,6 @@ def compute_qty(self, pair):
qty = round_numbers(balance, self.qty_precision)
return qty

def base_order(self):
"""
Required initial order to trigger bot.
Other orders require this to execute,
therefore should fail if not successful
1. Initial base purchase
2. Set take_profit
"""

pair = self.active_bot.pair

# Long position does not need qty in take_profit
# initial price with 1 qty should return first match
price = float(self.matching_engine(pair, True))
qty = round_numbers(
(float(self.active_bot.base_order_size) / float(price)),
self.qty_precision,
)
# setup stop_loss_price
stop_loss_price = 0
if float(self.active_bot.stop_loss) > 0:
stop_loss_price = price - (price * (float(self.active_bot.stop_loss) / 100))

if self.db_collection.name == "paper_trading":
res = self.simulate_order(
pair, supress_notation(price, self.price_precision), qty, "BUY"
)
else:
res = self.buy_order(
symbol=pair,
qty=qty,
price=supress_notation(price, self.price_precision),
)

order_data = OrderSchema(
timestamp=res["transactTime"],
order_id=res["orderId"],
deal_type="base_order",
pair=res["symbol"],
order_side=res["side"],
order_type=res["type"],
price=res["price"],
qty=res["origQty"],
fills=res["fills"],
time_in_force=res["timeInForce"],
status=res["status"],
)

self.active_bot.orders.append(order_data)
tp_price = float(res["price"]) * 1 + (float(self.active_bot.take_profit) / 100)

self.active_bot.deal = DealSchema(
buy_timestamp=res["transactTime"],
buy_price=res["price"],
buy_total_qty=res["origQty"],
current_price=res["price"],
take_profit_price=tp_price,
stop_loss_price=stop_loss_price,
)

# Activate bot
self.active_bot.status = Status.active

bot = encode_json(self.active_bot)
if "_id" in bot:
bot.pop("_id") # _id is what causes conflict not id

document = self.db_collection.find_one_and_update(
{"id": self.active_bot.id},
{"$set": bot},
return_document=ReturnDocument.AFTER,
)

return document

def take_profit_order(self) -> BotSchema:
"""
take profit order (Binance take_profit)
Expand Down
35 changes: 4 additions & 31 deletions api/deals/margin.py
Original file line number Diff line number Diff line change
Expand Up @@ -683,38 +683,11 @@ def switch_to_long_bot(self, new_base_order_price):
3. Create deal
"""
self.update_deal_logs("Resetting bot for long strategy...")
new_id = self.create_new_bot_streaming()
self.active_bot.id = new_id

# Reset bot to prepare for new activation
base_order = next(
(
bo_deal
for bo_deal in self.active_bot.orders
if bo_deal.deal_type == "base_order"
),
None,
)
# start from current stop_loss_price which is where the bot switched to long strategy
tp_price = new_base_order_price * (
1 + (float(self.active_bot.take_profit) / 100)
)
if float(self.active_bot.stop_loss) > 0:
stop_loss_price = new_base_order_price - (
new_base_order_price * (float(self.active_bot.stop_loss) / 100)
)
else:
stop_loss_price = 0

self.active_bot.deal = DealSchema(
buy_timestamp=base_order.timestamp,
buy_price=new_base_order_price,
buy_total_qty=base_order.qty,
take_profit_price=tp_price,
stop_loss_price=stop_loss_price,
)
self.active_bot.strategy = Strategy.long
self.active_bot.status = Status.active
self.active_bot = self.create_new_bot_streaming()

bot = self.base_order()
self.active_bot = BotSchema.parse_obj(bot)

# Keep bot up to date in the DB
# this avoid unsyched bots when errors ocurr in other functions
Expand Down
Loading

0 comments on commit 601a8fe

Please sign in to comment.