Files
stcs/app/services/Bybit/functions/Futures.py
algizn97 4406003a6e Fixed
2025-08-27 13:28:33 +05:00

670 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import asyncio
import json
import time
import logging.config
from pybit import exceptions
from pybit.unified_trading import HTTP
from logger_helper.logger_helper import LOGGING_CONFIG
import app.services.Bybit.functions.price_symbol as price_symbol
import app.services.Bybit.functions.balance as balance_g
import app.telegram.database.requests as rq
import app.telegram.Keyboards.inline_keyboards as inline_markup
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("futures")
def safe_float(val) -> float:
"""
Безопасное преобразование значения в float.
Возвращает 0.0, если значение None, пустое или некорректное.
"""
try:
if val is None or val == '':
return 0.0
return float(val)
except (ValueError, TypeError):
logger.error("Некорректное значение для преобразования в float")
return 0.0
def format_trade_details_position(data, commission_fee):
"""
Форматирует информацию о сделке в виде строки.
"""
msg = data.get('data', [{}])[0]
closed_size = float(msg.get('closedSize', 0))
symbol = msg.get('symbol', 'N/A')
entry_price = float(msg.get('execPrice', 0))
qty = float(msg.get('execQty', 0))
order_type = msg.get('orderType', 'N/A')
side = msg.get('side', '')
commission = float(msg.get('execFee', 0))
pnl = float(msg.get('execPnl', 0))
if commission_fee == "Да":
if pnl >= 0:
pnl -= commission
else:
pnl -= commission
movement = ''
if side.lower() == 'buy':
movement = 'Покупка'
elif side.lower() == 'sell':
movement = 'Продажа'
else:
movement = side
if closed_size > 0:
return (
f"Сделка закрыта:\n"
f"Торговая пара: {symbol}\n"
f"Цена исполнения: {entry_price:.6f}\n"
f"Количество: {qty}\n"
f"Закрыто позиций: {closed_size}\n"
f"Тип ордера: {order_type}\n"
f"Движение: {movement}\n"
f"Комиссия за сделку: {commission:.6f}\n"
f"Реализованная прибыль: {pnl:.6f} USDT"
)
else:
return (
f"Сделка открыта:\n"
f"Торговая пара: {symbol}\n"
f"Цена исполнения: {entry_price:.6f}\n"
f"Количество: {qty}\n"
f"Тип ордера: {order_type}\n"
f"Движение: {movement}\n"
f"Комиссия за сделку: {commission:.6f}"
)
def parse_pnl_from_msg(msg) -> float:
"""
Извлекает реализованную прибыль/убыток из сообщения.
"""
try:
return float(msg.get('realisedPnl', 0))
except Exception as e:
logger.error(f"Ошибка при извлечении реализованной прибыли: {e}")
return 0.0
async def handle_execution_message(message, msg: dict) -> None:
"""
Обработчик сообщений об исполнении сделки.
Логирует событие и проверяет условия для мартингейла и TP.
"""
logger.info(f"Исполнена сделка:\n{json.dumps(msg, indent=4, ensure_ascii=False)}")
pnl = parse_pnl_from_msg(msg)
tg_id = message.from_user.id
data_main_stgs = await rq.get_user_main_settings(tg_id)
data_main_risk_stgs = await rq.get_user_risk_management_settings(tg_id)
take_profit_percent = safe_float(data_main_stgs.get('take_profit_percent', 2))
commission_fee = data_main_risk_stgs.get('commission_fee', "ДА")
symbol = await rq.get_symbol(tg_id)
api_key = await rq.get_bybit_api_key(tg_id)
api_secret = await rq.get_bybit_secret_key(tg_id)
client = HTTP(api_key=api_key, api_secret=api_secret)
positions_resp = client.get_positions(category='linear', symbol=symbol)
positions_list = positions_resp.get('result', {}).get('list', [])
position = positions_list[0] if positions_list else None
trade_info = format_trade_details_position(msg, commission_fee=commission_fee)
await message.answer(f"{trade_info}", reply_markup=inline_markup.back_to_main)
liquidation_threshold = -100
if pnl <= liquidation_threshold:
current_step = int(await rq.get_martingale_step(tg_id))
current_step += 1
await rq.update_martingale_step(tg_id, current_step)
side = 'Buy' if position and position.get('side', '').lower() == 'long' else 'Sell'
margin_mode = data_main_stgs.get('margin_type', 'Isolated')
await open_position(tg_id, message, side=side, margin_mode=margin_mode)
elif position:
entry_price = safe_float(position.get('avgPrice'))
side = position.get('side', '')
current_price = float(position.get('markPrice', 0))
if side.lower() == 'long':
take_profit_trigger_price = entry_price * (1 + take_profit_percent / 100)
if current_price >= take_profit_trigger_price:
await close_user_trade(tg_id, symbol, message)
await rq.update_martingale_step(tg_id, 0)
elif side.lower() == 'short':
take_profit_trigger_price = entry_price * (1 - take_profit_percent / 100)
if current_price <= take_profit_trigger_price:
await close_user_trade(tg_id, symbol, message)
await rq.update_martingale_step(tg_id, 0)
async def error_max_step(message) -> None:
"""
Сообщение об ошибке превышения максимального количества шагов мартингейла.
"""
logger.error('Сделка не была совершена, превышен лимит максимального количества ставок в серии.')
await message.answer('Сделка не была совершена, превышен лимит максимального количества ставок в серии.',
reply_markup=inline_markup.back_to_main)
async def error_max_risk(message) -> None:
"""
Сообщение об ошибке превышения риск-лимита сделки.
"""
logger.error('Сделка не была совершена, риск убытка превышает допустимый лимит.')
await message.answer('Сделка не была совершена, риск убытка превышает допустимый лимит.',
reply_markup=inline_markup.back_to_main)
async def open_position(tg_id, message, side: str, margin_mode: str, tpsl_mode='Full'):
"""
Открывает позицию на Bybit с учётом настроек пользователя, маржи, размера лота, платформы и риска.
Возвращает True при успехе, False при ошибках открытия ордера, None при исключениях.
"""
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
symbol = await rq.get_symbol(tg_id)
data_main_stgs = await rq.get_user_main_settings(tg_id)
order_type = data_main_stgs.get('entry_order_type')
limit_price = None
if order_type == 'Limit':
limit_price = await rq.get_limit_price(tg_id)
data_risk_stgs = await rq.get_user_risk_management_settings(tg_id)
bybit_margin_mode = 'ISOLATED_MARGIN' if margin_mode == 'Isolated' else 'REGULAR_MARGIN'
client = HTTP(api_key=api_key, api_secret=secret_key)
try:
client.set_tp_sl_mode(symbol=symbol, category='linear', tpSlMode='Full')
except exceptions.InvalidRequestError as e:
if 'same tp sl mode' in str(e):
logger.info("Режим TP/SL уже установлен - пропускаем")
else:
raise
try:
balance = await balance_g.get_balance(tg_id, message)
price = await price_symbol.get_price(tg_id)
client.set_margin_mode(setMarginMode=bybit_margin_mode)
martingale_factor = safe_float(data_main_stgs.get('martingale_factor'))
max_martingale_steps = int(data_main_stgs.get('maximal_quantity', 0))
starting_quantity = safe_float(data_main_stgs.get('starting_quantity'))
max_risk_percent = safe_float(data_risk_stgs.get('max_risk_deal'))
loss_profit = safe_float(data_risk_stgs.get('price_loss'))
take_profit = safe_float(data_risk_stgs.get('price_profit'))
positions_resp = client.get_positions(category='linear', symbol=symbol)
positions_list = positions_resp.get('result', {}).get('list', [])
if positions_list:
position = positions_list[0]
size = safe_float(position.get('size', 0))
side_pos = position.get('side', '')
if size > 0 and side_pos:
entry_price = safe_float(position.get('avgPrice', price))
else:
entry_price = price
else:
entry_price = price
if order_type == 'Market':
base_price = entry_price
else:
base_price = limit_price
if side.lower() == 'buy':
take_profit_price = base_price * (1 + take_profit / 100)
stop_loss_price = base_price * (1 - loss_profit / 100)
else:
take_profit_price = base_price * (1 - take_profit / 100)
stop_loss_price = base_price * (1 + loss_profit / 100)
take_profit_price = max(take_profit_price, 0)
stop_loss_price = max(stop_loss_price, 0)
current_martingale_step = 0
next_quantity = starting_quantity
realised_pnl = 0.0
current_martingale = await rq.get_martingale_step(tg_id)
current_martingale_step = int(current_martingale)
if positions_list:
if realised_pnl > 0:
current_martingale_step = 0
next_quantity = starting_quantity
else:
current_martingale_step += 1
if current_martingale_step > max_martingale_steps:
await error_max_step(message)
return
next_quantity = float(starting_quantity) * (float(martingale_factor) ** current_martingale_step)
else:
next_quantity = starting_quantity
current_martingale_step = 0
potential_loss = safe_float(next_quantity) * safe_float(price) * (loss_profit / 100)
allowed_loss = safe_float(balance) * (max_risk_percent / 100)
if potential_loss > allowed_loss:
await error_max_risk(message)
return
instruments_resp = client.get_instruments_info(category='linear', symbol=symbol)
if instruments_resp.get('retCode') == 0:
instrument_info = instruments_resp.get('result', {}).get('list', [])
if instrument_info:
instrument = instrument_info[0]
min_order_qty = float(instrument.get('minOrderQty', 0))
min_order_value_api = float(instrument.get('minOrderValue', 0))
if min_order_value_api == 0:
min_order_value_api = 5.0
# Рассчитываем по формуле:
min_order_value_calc = min_order_qty * price if min_order_qty > 0 else 0
# Минимальное значение из значений параметров на бирже
min_order_value = max(min_order_value_calc, min_order_value_api)
else:
min_order_value = 5.0
order_value = float(next_quantity) * float(price)
if order_value < min_order_value:
await message.answer(
f"Сумма ордера слишком мала: {order_value:.2f} USDT. "
f"Минимум для торговли — {min_order_value} USDT. "
f"Пожалуйста, увеличьте количество позиций.", reply_markup=inline_markup.back_to_main)
return False
leverage = int(data_main_stgs.get('size_leverage', 1))
try:
resp = client.set_leverage(
category='linear',
symbol=symbol,
buyLeverage=str(leverage),
sellLeverage=str(leverage)
)
except exceptions.InvalidRequestError as e:
if "110043" in str(e):
logger.info(f"Leverage already set to {leverage} for {symbol}")
else:
raise e
if tpsl_mode == 'Full':
tp_order_type = 'Market'
sl_order_type = 'Market'
tp_limit_price = None
sl_limit_price = None
else: # Partial
tp_order_type = 'Limit'
sl_order_type = 'Limit'
tp_limit_price = take_profit_price
sl_limit_price = stop_loss_price
response = client.place_order(
category='linear',
symbol=symbol,
side=side,
orderType=order_type,
qty=str(next_quantity),
price=str(limit_price) if order_type == 'Limit' and limit_price else None,
takeProfit=str(take_profit_price),
tpOrderType=tp_order_type,
tpLimitPrice=str(tp_limit_price) if tp_limit_price else None,
stopLoss=str(stop_loss_price),
slOrderType=sl_order_type,
slLimitPrice=str(sl_limit_price) if sl_limit_price else None,
tpslMode=tpsl_mode,
timeInForce='GTC',
orderLinkId=f"deal_{symbol}_{int(time.time())}"
)
if response.get('retCode', -1) == 0:
await rq.update_martingale_step(tg_id, current_martingale_step)
return True
else:
logger.error(f"Ошибка открытия ордера: {response}")
await message.answer(f"Ошибка открытия ордера", reply_markup=inline_markup.back_to_main)
return False
except exceptions.InvalidRequestError as e:
logger.error(f"InvalidRequestError: {e}")
await message.answer('Недостаточно средств для размещения нового ордера с заданным количеством и плечом.',
reply_markup=inline_markup.back_to_main)
except Exception as e:
logger.error(f"Ошибка при совершении сделки: {e}")
await message.answer('Возникла ошибка при попытке открыть позицию.', reply_markup=inline_markup.back_to_main)
async def trading_cycle(tg_id, message):
"""
Цикл торговой логики с учётом таймера пользователя.
"""
try:
timer_data = await rq.get_user_timer(tg_id)
timer_min = 0
if isinstance(timer_data, dict):
timer_min = timer_data.get('timer_minutes') or timer_data.get('timer') or 0
else:
timer_min = timer_data or 0
timer_sec = timer_min * 60
if timer_sec > 0:
await asyncio.sleep(timer_sec)
data_main_stgs = await rq.get_user_main_settings(tg_id)
side = 'Buy' if data_main_stgs.get('trading_mode', '') == 'Long' else 'Sell'
margin_mode = data_main_stgs.get('margin_type', 'Isolated')
await open_position(tg_id, message, side=side, margin_mode=margin_mode)
except asyncio.CancelledError:
logger.info(f"Торговый цикл для пользователя {tg_id} был отменён.")
async def set_take_profit_stop_loss(tg_id: int, message, take_profit_price: float, stop_loss_price: float,
tpsl_mode='Full'):
"""
Устанавливает уровни Take Profit и Stop Loss для открытой позиции.
"""
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
symbol = await rq.get_symbol(tg_id)
data_main_stgs = await rq.get_user_main_settings(tg_id)
order_type = data_main_stgs.get('entry_order_type')
starting_quantity = safe_float(data_main_stgs.get('starting_quantity'))
limit_price = None
if order_type == 'Limit':
limit_price = await rq.get_limit_price(tg_id)
data_risk_stgs = await rq.get_user_risk_management_settings(tg_id)
trading_mode = data_main_stgs.get('trading_mode')
side = None
if trading_mode == 'Long':
side = 'Buy'
elif trading_mode == 'Short':
side = 'Sell'
if side is None:
await message.answer("Не удалось определить сторону сделки.")
return
client = HTTP(api_key=api_key, api_secret=secret_key)
await cancel_all_tp_sl_orders(tg_id, symbol)
try:
try:
client.set_tp_sl_mode(symbol=symbol, category='linear', tpSlMode=tpsl_mode)
except exceptions.InvalidRequestError as e:
if 'same tp sl mode' in str(e).lower():
logger.info(f"Режим TP/SL уже установлен для {symbol}")
else:
raise
positions_resp = client.get_positions(category='linear', symbol=symbol)
positions = positions_resp.get('result', {}).get('list', [])
if not positions or abs(float(positions[0].get('size', 0))) == 0:
params = dict(
category='linear',
symbol=symbol,
side=side,
orderType=order_type,
qty=str(starting_quantity),
timeInForce='GTC',
orderLinkId=f"deal_{symbol}_{int(time.time())}",
takeProfit=str(take_profit_price),
stopLoss=str(stop_loss_price),
tpOrderType='Limit' if tpsl_mode == 'Partial' else 'Market',
slOrderType='Limit' if tpsl_mode == 'Partial' else 'Market',
tpslMode=tpsl_mode
)
if order_type == 'Limit' and limit_price is not None:
params['price'] = str(limit_price)
if tpsl_mode == 'Partial':
params['tpLimitPrice'] = str(take_profit_price)
params['slLimitPrice'] = str(stop_loss_price)
response = client.place_order(**params)
if response.get('retCode') != 0:
await message.answer(f"Ошибка создания ордера с TP/SL: {response.get('retMsg')}",
reply_markup=inline_markup.back_to_main)
return
else:
resp = client.set_trading_stop(
category='linear',
symbol=symbol,
takeProfit=str(round(take_profit_price, 5)),
stopLoss=str(round(stop_loss_price, 5)),
tpTriggerBy='LastPrice',
slTriggerBy='LastPrice',
reduceOnly=False
)
if resp.get('retCode') != 0:
await message.answer(f"Ошибка обновления TP/SL: {resp.get('retMsg')}",
reply_markup=inline_markup.back_to_main)
return
await message.answer(
f"ТП и СЛ успешно установлены:\nТейк-профит: {take_profit_price:.5f}\nСтоп-лосс: {stop_loss_price:.5f}",
reply_markup=inline_markup.back_to_main)
except Exception as e:
logger.error(f"Ошибка установки TP/SL для {symbol}: {e}", exc_info=True)
await message.answer("Произошла ошибка при установке TP и SL.", reply_markup=inline_markup.back_to_main)
async def cancel_all_tp_sl_orders(tg_id, symbol):
"""
Отменяет все открытые ордера TP/SL для указанного символа.
"""
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
client = HTTP(api_key=api_key, api_secret=secret_key)
last_response = None
try:
orders_resp = client.get_open_orders(category='linear', symbol=symbol)
orders = orders_resp.get('result', {}).get('list', [])
for order in orders:
order_id = order.get('orderId')
cancel_resp = client.cancel_order(category='linear', symbol=symbol, orderId=order_id)
last_response = cancel_resp
if cancel_resp.get('retCode') != 0:
logger.warning(f"Не удалось отменить ордер {order_id}: {cancel_resp.get('retMsg')}")
except Exception as e:
logger.error(f"Ошибка при отмене ордеров TP/SL: {e}")
return last_response
async def get_active_positions_by_symbol(tg_id, message):
"""
Показывает активные позиции пользователя по символу.
"""
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
client = HTTP(api_key=api_key, api_secret=secret_key)
symbol = await rq.get_symbol(tg_id)
active_positions = client.get_positions(category='linear', symbol=symbol)
positions = active_positions.get('result', {}).get('list', [])
pos = positions[0] if positions else None
if float(pos.get('size', 0)) == 0:
await message.answer("❗️ У вас нет активных позиций.", reply_markup=inline_markup.back_to_main)
return
text = (
f"Торговая пара: {pos.get('symbol')}\n"
f"Цена входа: {pos.get('avgPrice')}\n"
f"Движение: {pos.get('side')}\n"
f"Кредитное плечо: {pos.get('leverage')}x\n"
f"Количество: {pos.get('size')}\n"
f"Тейк-профит: {pos.get('takeProfit')}\n"
f"Стоп-лосс: {pos.get('stopLoss')}\n"
)
await message.answer(text, reply_markup=inline_markup.create_close_deal_markup(symbol))
async def get_active_orders_by_symbol(tg_id, message):
"""
Показывает активные лимитные ордера пользователя по символу.
"""
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
symbol = await rq.get_symbol(tg_id)
client = HTTP(api_key=api_key, api_secret=secret_key)
active_orders = client.get_open_orders(category='linear', symbol=symbol)
limit_orders = [
order for order in active_orders.get('result', {}).get('list', [])
if order.get('orderType') == 'Limit'
]
if not limit_orders:
await message.answer("Нет активных лимитных ордеров по данной торговой паре.",
reply_markup=inline_markup.back_to_main)
return
texts = []
for order in limit_orders:
text = (
f"Торговая пара: {order.get('symbol')}\n"
f"Тип ордера: {order.get('orderType')}\n"
f"Сторона: {order.get('side')}\n"
f"Цена: {order.get('price')}\n"
f"Количество: {order.get('qty')}\n"
f"Тейк-профит: {order.get('takeProfit')}\n"
f"Стоп-лосс: {order.get('stopLoss')}\n"
f"Кредитное плечо: {order.get('leverage')}\n"
)
texts.append(text)
await message.answer("\n\n".join(texts), reply_markup=inline_markup.create_close_deal_markup(symbol))
async def close_user_trade(tg_id: int, symbol: str, message):
"""
Закрывает открытые позиции пользователя по символу рыночным ордером.
Возвращает True при успехе, False при ошибках.
"""
try:
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
data_risk_stgs = await rq.get_user_risk_management_settings(tg_id)
limit_price = await rq.get_limit_price(tg_id)
include_fee = data_risk_stgs.get('commission_fee', 'Нет') == 'Да'
client = HTTP(api_key=api_key, api_secret=secret_key)
positions_resp = client.get_positions(category="linear", symbol=symbol)
if positions_resp.get('retCode') != 0:
return False
positions_list = positions_resp.get('result', {}).get('list', [])
if not positions_list:
return False
position = positions_list[0]
qty = abs(safe_float(position.get('size')))
side = position.get('side')
entry_price = safe_float(position.get('avgPrice'))
if qty == 0:
return False
orders = client.get_open_orders(category='linear', symbol=symbol)
cancel_resp = await cancel_all_tp_sl_orders(tg_id, symbol)
open_orders_list = orders.get('result', {}).get('list', [])
order_id = open_orders_list[0].get('orderId') if open_orders_list else None
close_side = "Sell" if side == "Buy" else "Buy"
ticker_resp = client.get_tickers(category="linear", symbol=symbol)
current_price = 0.0
if ticker_resp.get('retCode') == 0:
result = ticker_resp.get('result', {})
ticker_list = []
if isinstance(result, dict):
ticker_list = result.get('list', [])
elif isinstance(result, list):
ticker_list = result
if ticker_list:
current_price = float(ticker_list[0].get('lastPrice', 0.0))
place_resp = client.place_order(
category="linear",
symbol=symbol,
side=close_side,
orderType="Market",
qty=str(qty),
timeInForce="GTC",
reduceOnly=True
)
if place_resp.get('retCode', -1) == 0:
trade_fee = 0
try:
trades_resp = client.get_closed_pnl(category="linear", symbol=symbol)
if trades_resp.get('retCode') == 0:
trades = trades_resp.get('result', {}).get('list', [])
for trade in trades:
if trade.get('orderId') == order_id:
trade_fee += float(trade.get('execFee', 0))
except Exception as e:
logger.error(f"Ошибка при получении сделок: {e}")
trade_fee = 0
pnl = (current_price - entry_price) * qty if side == "Buy" else (entry_price - current_price) * qty
if include_fee:
pnl -= trade_fee
pnl_percent = (pnl / (entry_price * qty)) * 100 if entry_price * qty > 0 else 0
return True
else:
if message:
await message.answer(f"Ошибка закрытия сделки {symbol}.",
reply_markup=inline_markup.back_to_main)
return False
except Exception as e:
logger.error(f"Ошибка закрытия сделки {symbol} для пользователя {tg_id}: {e}", exc_info=True)
if message:
await message.answer("Произошла ошибка при закрытии сделки.", reply_markup=inline_markup.back_to_main)
return False
async def close_trade_after_delay(tg_id: int, message, symbol: str, delay_sec: int):
"""
Закрывает сделку пользователя после задержки delay_sec секунд.
"""
try:
await asyncio.sleep(delay_sec)
result = await close_user_trade(tg_id, symbol, message)
if result:
await message.answer(f"Сделка {symbol} успешно закрыта по таймеру.",
reply_markup=inline_markup.back_to_main)
else:
await message.answer(f"Не удалось закрыть сделку {symbol} по таймеру.",
reply_markup=inline_markup.back_to_main)
except asyncio.CancelledError:
await message.answer(f"Закрытие сделки {symbol} по таймеру отменено.", reply_markup=inline_markup.back_to_main)