361 lines
13 KiB
Python
361 lines
13 KiB
Python
import logging.config
|
|
import math
|
|
|
|
from pybit.exceptions import InvalidRequestError
|
|
|
|
import database.request as rq
|
|
from app.bybit import get_bybit_client
|
|
from app.bybit.get_functions.get_instruments_info import get_instruments_info
|
|
from app.bybit.get_functions.get_tickers import get_tickers
|
|
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
|
|
from app.bybit.set_functions.set_leverage import set_leverage
|
|
from app.bybit.set_functions.set_margin_mode import set_margin_mode
|
|
from app.helper_functions import get_liquidation_price, safe_float
|
|
|
|
logging.config.dictConfig(LOGGING_CONFIG)
|
|
logger = logging.getLogger("open_positions")
|
|
|
|
|
|
async def start_trading_cycle(
|
|
tg_id: int
|
|
) -> str | None:
|
|
"""
|
|
Start trading cycle
|
|
:param tg_id: Telegram user ID
|
|
"""
|
|
try:
|
|
client = await get_bybit_client(tg_id=tg_id)
|
|
symbol = await rq.get_user_symbol(tg_id=tg_id)
|
|
additional_data = await rq.get_user_additional_settings(tg_id=tg_id)
|
|
risk_management_data = await rq.get_user_risk_management(tg_id=tg_id)
|
|
commission_fee = risk_management_data.commission_fee
|
|
user_deals_data = await rq.get_user_deal_by_symbol(
|
|
tg_id=tg_id, symbol=symbol
|
|
)
|
|
trade_mode = additional_data.trade_mode
|
|
switch_side = additional_data.switch_side
|
|
margin_type = additional_data.margin_type
|
|
leverage = additional_data.leverage
|
|
order_quantity = additional_data.order_quantity
|
|
trigger_price = additional_data.trigger_price
|
|
martingale_factor = additional_data.martingale_factor
|
|
max_bets_in_series = additional_data.max_bets_in_series
|
|
take_profit_percent = risk_management_data.take_profit_percent
|
|
stop_loss_percent = risk_management_data.stop_loss_percent
|
|
|
|
get_side = "Buy"
|
|
|
|
if user_deals_data:
|
|
get_side = user_deals_data.last_side or "Buy"
|
|
|
|
if trade_mode == "Switch":
|
|
if switch_side == "По направлению":
|
|
side = get_side
|
|
else:
|
|
if get_side == "Buy":
|
|
side = "Sell"
|
|
else:
|
|
side = "Buy"
|
|
else:
|
|
if trade_mode == "Long":
|
|
side = "Buy"
|
|
else:
|
|
side = "Sell"
|
|
|
|
# Get fee rates
|
|
fee_info = client.get_fee_rates(category="linear", symbol=symbol)
|
|
|
|
# Check if commission fee is enabled
|
|
commission_fee_percent = 0.0
|
|
if commission_fee == "Yes_commission_fee":
|
|
commission_fee_percent = safe_float(
|
|
fee_info["result"]["list"][0]["takerFeeRate"]
|
|
)
|
|
|
|
get_ticker = await get_tickers(tg_id, symbol=symbol)
|
|
price_symbol = safe_float(get_ticker.get("lastPrice")) or 0
|
|
instruments_info = await get_instruments_info(tg_id=tg_id, symbol=symbol)
|
|
qty_step_str = instruments_info.get("lotSizeFilter").get("qtyStep")
|
|
qty_step = safe_float(qty_step_str)
|
|
qty = safe_float(order_quantity) / safe_float(price_symbol)
|
|
decimals = abs(int(round(math.log10(qty_step))))
|
|
qty_formatted = math.floor(qty / qty_step) * qty_step
|
|
qty_formatted = round(qty_formatted, decimals)
|
|
|
|
if trigger_price > 0:
|
|
po_trigger_price = str(trigger_price)
|
|
else:
|
|
po_trigger_price = None
|
|
|
|
price_for_cals = trigger_price if po_trigger_price is not None else price_symbol
|
|
total_commission = price_for_cals * qty_formatted * commission_fee_percent
|
|
|
|
await set_margin_mode(tg_id=tg_id, margin_mode=margin_type)
|
|
await set_leverage(
|
|
tg_id=tg_id,
|
|
symbol=symbol,
|
|
leverage=leverage,
|
|
)
|
|
|
|
res = await open_positions(
|
|
tg_id=tg_id,
|
|
symbol=symbol,
|
|
side=side,
|
|
order_quantity=order_quantity,
|
|
trigger_price=trigger_price,
|
|
margin_type=margin_type,
|
|
leverage=leverage,
|
|
take_profit_percent=take_profit_percent,
|
|
stop_loss_percent=stop_loss_percent,
|
|
commission_fee_percent=total_commission
|
|
)
|
|
|
|
if res == "OK":
|
|
await rq.set_user_deal(
|
|
tg_id=tg_id,
|
|
symbol=symbol,
|
|
last_side=side,
|
|
current_step=1,
|
|
trade_mode=trade_mode,
|
|
margin_type=margin_type,
|
|
leverage=leverage,
|
|
order_quantity=order_quantity,
|
|
trigger_price=trigger_price,
|
|
martingale_factor=martingale_factor,
|
|
max_bets_in_series=max_bets_in_series,
|
|
take_profit_percent=take_profit_percent,
|
|
stop_loss_percent=stop_loss_percent,
|
|
base_quantity=order_quantity
|
|
)
|
|
return "OK"
|
|
return (
|
|
res
|
|
if res
|
|
in {
|
|
"Limit price is out min price",
|
|
"Limit price is out max price",
|
|
"Risk is too high for this trade",
|
|
"estimated will trigger liq",
|
|
"ab not enough for new order",
|
|
"InvalidRequestError",
|
|
"Order does not meet minimum order value",
|
|
"position idx not match position mode",
|
|
"Qty invalid",
|
|
"The number of contracts exceeds maximum limit allowed",
|
|
"The number of contracts exceeds minimum limit allowed"
|
|
}
|
|
else None
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error("Error in start_trading: %s", e)
|
|
return None
|
|
|
|
|
|
async def trading_cycle(
|
|
tg_id: int, symbol: str, reverse_side: str
|
|
) -> str | None:
|
|
try:
|
|
user_deals_data = await rq.get_user_deal_by_symbol(tg_id=tg_id, symbol=symbol)
|
|
user_auto_trading_data = await rq.get_user_auto_trading(tg_id=tg_id, symbol=symbol)
|
|
total_fee = user_auto_trading_data.total_fee
|
|
trade_mode = user_deals_data.trade_mode
|
|
margin_type = user_deals_data.margin_type
|
|
leverage = user_deals_data.leverage
|
|
trigger_price = 0
|
|
take_profit_percent = user_deals_data.take_profit_percent
|
|
stop_loss_percent = user_deals_data.stop_loss_percent
|
|
max_bets_in_series = user_deals_data.max_bets_in_series
|
|
martingale_factor = user_deals_data.martingale_factor
|
|
current_step = user_deals_data.current_step
|
|
order_quantity = user_deals_data.order_quantity
|
|
base_quantity = user_deals_data.base_quantity
|
|
|
|
await set_margin_mode(tg_id=tg_id, margin_mode=margin_type)
|
|
await set_leverage(
|
|
tg_id=tg_id,
|
|
symbol=symbol,
|
|
leverage=leverage,
|
|
)
|
|
|
|
if reverse_side == "Buy":
|
|
real_side = "Sell"
|
|
else:
|
|
real_side = "Buy"
|
|
|
|
side = real_side
|
|
|
|
if trade_mode == "Switch":
|
|
side = "Sell" if real_side == "Buy" else "Buy"
|
|
|
|
next_quantity = safe_float(order_quantity) * (
|
|
safe_float(martingale_factor)
|
|
)
|
|
current_step += 1
|
|
|
|
if max_bets_in_series < current_step:
|
|
return "Max bets in series"
|
|
|
|
res = await open_positions(
|
|
tg_id=tg_id,
|
|
symbol=symbol,
|
|
side=side,
|
|
order_quantity=next_quantity,
|
|
trigger_price=trigger_price,
|
|
margin_type=margin_type,
|
|
leverage=leverage,
|
|
take_profit_percent=take_profit_percent,
|
|
stop_loss_percent=stop_loss_percent,
|
|
commission_fee_percent=total_fee
|
|
)
|
|
|
|
if res == "OK":
|
|
await rq.set_user_deal(
|
|
tg_id=tg_id,
|
|
symbol=symbol,
|
|
last_side=side,
|
|
current_step=current_step,
|
|
trade_mode=trade_mode,
|
|
margin_type=margin_type,
|
|
leverage=leverage,
|
|
order_quantity=next_quantity,
|
|
trigger_price=trigger_price,
|
|
martingale_factor=martingale_factor,
|
|
max_bets_in_series=max_bets_in_series,
|
|
take_profit_percent=take_profit_percent,
|
|
stop_loss_percent=stop_loss_percent,
|
|
base_quantity=base_quantity
|
|
)
|
|
return "OK"
|
|
|
|
return (
|
|
res
|
|
if res
|
|
in {
|
|
"Risk is too high for this trade",
|
|
"ab not enough for new order",
|
|
"InvalidRequestError",
|
|
"The number of contracts exceeds maximum limit allowed",
|
|
}
|
|
else None
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error("Error in trading_cycle: %s", e)
|
|
return None
|
|
|
|
|
|
async def open_positions(
|
|
tg_id: int,
|
|
side: str,
|
|
symbol: str,
|
|
order_quantity: float,
|
|
trigger_price: float,
|
|
margin_type: str,
|
|
leverage: str,
|
|
take_profit_percent: float,
|
|
stop_loss_percent: float,
|
|
commission_fee_percent: float
|
|
) -> str | None:
|
|
try:
|
|
client = await get_bybit_client(tg_id=tg_id)
|
|
get_ticker = await get_tickers(tg_id, symbol=symbol)
|
|
price_symbol = safe_float(get_ticker.get("lastPrice")) or 0
|
|
instruments_info = await get_instruments_info(tg_id=tg_id, symbol=symbol)
|
|
qty_step_str = instruments_info.get("lotSizeFilter").get("qtyStep")
|
|
qty_step = safe_float(qty_step_str)
|
|
qty = safe_float(order_quantity) / safe_float(price_symbol)
|
|
decimals = abs(int(round(math.log10(qty_step))))
|
|
qty_formatted = math.floor(qty / qty_step) * qty_step
|
|
qty_formatted = round(qty_formatted, decimals)
|
|
|
|
if trigger_price > 0:
|
|
po_trigger_price = str(trigger_price)
|
|
trigger_direction = 1 if trigger_price > price_symbol else 2
|
|
else:
|
|
po_trigger_price = None
|
|
trigger_direction = None
|
|
|
|
get_leverage = safe_float(leverage)
|
|
|
|
price_for_cals = trigger_price if po_trigger_price is not None else price_symbol
|
|
|
|
if margin_type == "ISOLATED_MARGIN":
|
|
liq_long, liq_short = await get_liquidation_price(
|
|
tg_id=tg_id,
|
|
entry_price=price_for_cals,
|
|
symbol=symbol,
|
|
leverage=get_leverage,
|
|
)
|
|
|
|
if (liq_long > 0 or liq_short > 0) and price_for_cals > 0:
|
|
if side == "Buy":
|
|
base_tp = price_for_cals + (price_for_cals - liq_long)
|
|
take_profit_price = base_tp + commission_fee_percent
|
|
else:
|
|
base_tp = price_for_cals - (liq_short - price_for_cals)
|
|
take_profit_price = base_tp - commission_fee_percent
|
|
take_profit_price = max(take_profit_price, 0)
|
|
else:
|
|
take_profit_price = None
|
|
|
|
stop_loss_price = None
|
|
else:
|
|
if side == "Buy":
|
|
take_profit_price = price_for_cals * (1 + take_profit_percent / 100) + commission_fee_percent
|
|
stop_loss_price = price_for_cals * (1 - stop_loss_percent / 100) - commission_fee_percent
|
|
else:
|
|
take_profit_price = price_for_cals * (1 - take_profit_percent / 100) - commission_fee_percent
|
|
stop_loss_price = price_for_cals * (1 + stop_loss_percent / 100) + commission_fee_percent
|
|
|
|
take_profit_price = max(take_profit_price, 0)
|
|
stop_loss_price = max(stop_loss_price, 0)
|
|
|
|
# Place order
|
|
order_params = {
|
|
"category": "linear",
|
|
"symbol": symbol,
|
|
"side": side,
|
|
"orderType": "Market",
|
|
"qty": str(qty_formatted),
|
|
"triggerDirection": trigger_direction,
|
|
"triggerPrice": po_trigger_price,
|
|
"triggerBy": "LastPrice",
|
|
"timeInForce": "GTC",
|
|
"positionIdx": 0,
|
|
"tpslMode": "Full",
|
|
"takeProfit": str(take_profit_price) if take_profit_price else None,
|
|
"stopLoss": str(stop_loss_price) if stop_loss_price else None,
|
|
}
|
|
|
|
response = client.place_order(**order_params)
|
|
|
|
if response["retCode"] == 0:
|
|
logger.info("Position opened for user: %s", tg_id)
|
|
return "OK"
|
|
|
|
logger.error("Error opening position for user: %s", tg_id)
|
|
return None
|
|
|
|
except InvalidRequestError as e:
|
|
error_text = str(e)
|
|
known_errors = {
|
|
"Order does not meet minimum order value": "Order does not meet minimum order value",
|
|
"estimated will trigger liq": "estimated will trigger liq",
|
|
"ab not enough for new order": "ab not enough for new order",
|
|
"position idx not match position mode": "position idx not match position mode",
|
|
"Qty invalid": "Qty invalid",
|
|
"The number of contracts exceeds maximum limit allowed": "The number of contracts exceeds maximum limit allowed",
|
|
"The number of contracts exceeds minimum limit allowed": "The number of contracts exceeds minimum limit allowed",
|
|
}
|
|
for key, msg in known_errors.items():
|
|
if key in error_text:
|
|
logger.error(msg)
|
|
return msg
|
|
logger.error("InvalidRequestError: %s", e)
|
|
return "InvalidRequestError"
|
|
|
|
except Exception as e:
|
|
logger.error("Error opening position for user %s: %s", tg_id, e)
|
|
return None
|