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 8ac4f20
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 174 deletions.
156 changes: 122 additions & 34 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 @@ -200,26 +201,109 @@ def save_bot_streaming(self):

def create_new_bot_streaming(self):
"""
MongoDB query to save bot using Pydantic
Resets bot to initial state and saves it to DB
This function differs from usual save query in that
it returns the saved bot, thus called streaming, it's
specifically for streaming saves
This function differs from usual create_bot in that
it needs to set strategy first (reversal)
clear orders, deal and errors,
which are not required in new bots,
as they initialize with empty values
"""
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)

# notify the system to update streaming as usual bot actions
self.db.research_controller.update_one({"_id": "settings"}, {"$set": {"update_required": time()}})

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 +329,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 +350,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 +412,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 +433,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 +462,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 +473,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 8ac4f20

Please sign in to comment.