forked from kodorvan/stcs
670 lines
28 KiB
Python
670 lines
28 KiB
Python
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)
|