1
0
forked from kodorvan/stcs

Added new functions and documentation

This commit is contained in:
algizn97
2025-08-26 19:36:11 +05:00
parent 8bc4c634fe
commit 07df16dbe9
2 changed files with 585 additions and 246 deletions

View File

@@ -1,9 +1,11 @@
import asyncio import asyncio
import functools import nest_asyncio
import time import time
import logging.config import logging.config
from pybit import exceptions from pybit import exceptions
from pybit.unified_trading import HTTP from pybit.unified_trading import HTTP, WebSocket
from websocket import WebSocketConnectionClosedException
from logger_helper.logger_helper import LOGGING_CONFIG from logger_helper.logger_helper import LOGGING_CONFIG
import app.services.Bybit.functions.price_symbol as price_symbol import app.services.Bybit.functions.price_symbol as price_symbol
@@ -14,47 +16,166 @@ import app.telegram.Keyboards.inline_keyboards as inline_markup
logging.config.dictConfig(LOGGING_CONFIG) logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("futures") logger = logging.getLogger("futures")
active_start_tasks = {} nest_asyncio.apply()
active_close_tasks = {}
def safe_float(val): def safe_float(val) -> float:
"""
Безопасное преобразование значения в float.
Возвращает 0.0, если значение None, пустое или некорректное.
"""
try: try:
if val is None or val == '': if val is None or val == '':
return 0.0 return 0.0
return float(val) return float(val)
except (ValueError, TypeError): except (ValueError, TypeError):
logger.error("Некорректное значение для преобразования в float")
return 0.0 return 0.0
async def info_access_open_deal(message, symbol, trade_mode, margin_mode, leverage, qty, tp, sl, entry_price, limit_price, order_type): 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"Исполнена сделка: {msg}")
await message.answer(f"Исполнена сделка: {msg}")
pnl = parse_pnl_from_msg(msg)
tg_id = message.from_user.id
data_main_stgs = await rq.get_user_main_settings(tg_id)
take_profit_percent = safe_float(data_main_stgs.get('take_profit_percent', 2))
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
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 start_execution_ws(tg_id, message) -> None:
"""
Запускает WebSocket для отслеживания исполнения сделок в режиме реального времени.
Переподключается при ошибках.
"""
api_key = await rq.get_bybit_api_key(tg_id)
api_secret = await rq.get_bybit_secret_key(tg_id)
reconnect_delay = 5
while True:
try:
ws = WebSocket(api_key=api_key,
api_secret=api_secret,
testnet=False,
channel_type="private")
async def on_execution(msg):
await handle_execution_message(message, msg)
def on_execution_sync(msg):
asyncio.create_task(on_execution(msg))
ws.execution_stream(on_execution_sync)
while True:
await asyncio.sleep(1)
except WebSocketConnectionClosedException:
logging.warning("WebSocket закрыт, переподключение через 5 секунд...")
await asyncio.sleep(reconnect_delay)
except Exception as e:
logging.error(f"Ошибка WebSocket: {e}")
await asyncio.sleep(reconnect_delay)
async def info_access_open_deal(message, symbol, trade_mode, margin_mode, leverage, qty, tp, sl, entry_price,
limit_price, order_type) -> None:
"""
Отправляет сообщение об успешном открытии позиции или выставлении лимитного ордера.
"""
human_margin_mode = 'Isolated' if margin_mode == 'ISOLATED_MARGIN' else 'Cross' human_margin_mode = 'Isolated' if margin_mode == 'ISOLATED_MARGIN' else 'Cross'
text = f'''{'Позиция была успешна открыта' if order_type == 'Market' else 'Лимитный ордер установлен'}! text = (
Торговая пара: {symbol} f"{'Позиция была успешна открыта' if order_type == 'Market' else 'Лимитный ордер установлен'}!\n"
Цена входа: {entry_price if order_type == 'Market' else round(limit_price, 5)} f"Торговая пара: {symbol}\n"
Движение: {trade_mode} f"Цена входа: {entry_price if order_type == 'Market' else round(limit_price, 5)}\n"
Тип-маржи: {human_margin_mode} f"Движение: {trade_mode}\n"
Кредитное плечо: {leverage}x f"Тип-маржи: {human_margin_mode}\n"
Количество: {qty} f"Кредитное плечо: {leverage}x\n"
Тейк-профит: {round(tp, 5)} f"Количество: {qty}\n"
Стоп-лосс: {round(sl, 5)} f"Тейк-профит: {round(tp, 5)}\n"
''' f"Стоп-лосс: {round(sl, 5)}\n"
)
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.create_close_deal_markup(symbol)) await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.create_close_deal_markup(symbol))
async def error_max_step(message): async def error_max_step(message) -> None:
"""
Сообщение об ошибке превышения максимального количества шагов мартингейла.
"""
logger.error('Сделка не была совершена, превышен лимит максимального количества ставок в серии.') logger.error('Сделка не была совершена, превышен лимит максимального количества ставок в серии.')
await message.answer('Сделка не была совершена, превышен лимит максимального количества ставок в серии.', await message.answer('Сделка не была совершена, превышен лимит максимального количества ставок в серии.',
reply_markup=inline_markup.back_to_main) reply_markup=inline_markup.back_to_main)
async def error_max_risk(message): async def error_max_risk(message) -> None:
"""
Сообщение об ошибке превышения риск-лимита сделки.
"""
logger.error('Сделка не была совершена, риск убытка превышает допустимый лимит.') logger.error('Сделка не была совершена, риск убытка превышает допустимый лимит.')
await message.answer('Сделка не была совершена, риск убытка превышает допустимый лимит.', await message.answer('Сделка не была совершена, риск убытка превышает допустимый лимит.',
reply_markup=inline_markup.back_to_main) reply_markup=inline_markup.back_to_main)
async def open_position(tg_id, message, side: str, margin_mode: str, tpsl_mode='Full'): 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) api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id) secret_key = await rq.get_bybit_secret_key(tg_id)
symbol = await rq.get_symbol(tg_id) symbol = await rq.get_symbol(tg_id)
@@ -91,7 +212,6 @@ async def open_position(tg_id, message, side: str, margin_mode: str, tpsl_mode='
max_risk_percent = safe_float(data_risk_stgs.get('max_risk_deal')) max_risk_percent = safe_float(data_risk_stgs.get('max_risk_deal'))
loss_profit = safe_float(data_risk_stgs.get('price_loss')) loss_profit = safe_float(data_risk_stgs.get('price_loss'))
take_profit = safe_float(data_risk_stgs.get('price_profit')) take_profit = safe_float(data_risk_stgs.get('price_profit'))
commission_fee = safe_float(data_risk_stgs.get('commission_fee', 0))
positions_resp = client.get_positions(category='linear', symbol=symbol) positions_resp = client.get_positions(category='linear', symbol=symbol)
positions_list = positions_resp.get('result', {}).get('list', []) positions_list = positions_resp.get('result', {}).get('list', [])
@@ -173,7 +293,7 @@ async def open_position(tg_id, message, side: str, margin_mode: str, tpsl_mode='
await message.answer( await message.answer(
f"Сумма ордера слишком мала: {order_value:.2f} USDT. " f"Сумма ордера слишком мала: {order_value:.2f} USDT. "
f"Минимум для торговли — {min_order_value} USDT. " f"Минимум для торговли — {min_order_value} USDT. "
f"Пожалуйста, увеличьте количество позиций.") f"Пожалуйста, увеличьте количество позиций.", reply_markup=inline_markup.back_to_main)
return False return False
leverage = int(data_main_stgs.get('size_leverage', 1)) leverage = int(data_main_stgs.get('size_leverage', 1))
@@ -186,7 +306,6 @@ async def open_position(tg_id, message, side: str, margin_mode: str, tpsl_mode='
) )
except exceptions.InvalidRequestError as e: except exceptions.InvalidRequestError as e:
if "110043" in str(e): if "110043" in str(e):
# Плечо уже установлено с таким значением, можем игнорировать
logger.info(f"Leverage already set to {leverage} for {symbol}") logger.info(f"Leverage already set to {leverage} for {symbol}")
else: else:
raise e raise e
@@ -206,7 +325,7 @@ async def open_position(tg_id, message, side: str, margin_mode: str, tpsl_mode='
category='linear', category='linear',
symbol=symbol, symbol=symbol,
side=side, side=side,
orderType=order_type, # Market или Limit orderType=order_type,
qty=str(next_quantity), qty=str(next_quantity),
price=str(limit_price) if order_type == 'Limit' and limit_price else None, price=str(limit_price) if order_type == 'Limit' and limit_price else None,
takeProfit=str(take_profit_price), takeProfit=str(take_profit_price),
@@ -242,6 +361,9 @@ async def open_position(tg_id, message, side: str, margin_mode: str, tpsl_mode='
async def trading_cycle(tg_id, message): async def trading_cycle(tg_id, message):
"""
Цикл торговой логики с учётом таймера пользователя.
"""
try: try:
timer_data = await rq.get_user_timer(tg_id) timer_data = await rq.get_user_timer(tg_id)
timer_min = 0 timer_min = 0
@@ -264,47 +386,210 @@ async def trading_cycle(tg_id, message):
logger.info(f"Торговый цикл для пользователя {tg_id} был отменён.") logger.info(f"Торговый цикл для пользователя {tg_id} был отменён.")
async def fetch_positions_async(client, symbol): async def set_take_profit_stop_loss(tg_id: int, message, take_profit_price: float, stop_loss_price: float,
loop = asyncio.get_running_loop() tpsl_mode='Full'):
# запускаем блокирующий вызов get_positions в отдельном потоке """
return await loop.run_in_executor(None, functools.partial(client.get_positions, category='linear', symbol=symbol)) Устанавливает уровни 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'))
async def get_active_positions(message, api_key, secret_key): limit_price = None
client = HTTP( if order_type == 'Limit':
api_key=api_key, limit_price = await rq.get_limit_price(tg_id)
api_secret=secret_key
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(client, 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
) )
instruments_resp = client.get_instruments_info(category='linear') if order_type == 'Limit' and limit_price is not None:
if instruments_resp.get('retCode') != 0: params['price'] = str(limit_price)
return []
symbols = [item['symbol'] for item in instruments_resp.get('result', {}).get('list', [])]
active_positions = [] if tpsl_mode == 'Partial':
params['tpLimitPrice'] = str(take_profit_price)
params['slLimitPrice'] = str(stop_loss_price)
for sym in symbols: response = client.place_order(**params)
try: if response.get('retCode') != 0:
resp = await fetch_positions_async(client, sym) await message.answer(f"Ошибка создания ордера с TP/SL: {response.get('retMsg')}",
if resp.get('retCode') == 0: reply_markup=inline_markup.back_to_main)
positions = resp.get('result', {}).get('list', []) return
for pos in positions:
if pos.get('size') and safe_float(pos['size']) > 0: else:
active_positions.append(pos) 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: except Exception as e:
logger.error(f"Ошибка при получении позиций: {e}") logger.error(f"Ошибка установки TP/SL для {symbol}: {e}", exc_info=True)
await message.answer('⚠️ Ошибка при получении позиций', reply_markup=inline_markup.back_to_main) await message.answer("Произошла ошибка при установке TP и SL.", reply_markup=inline_markup.back_to_main)
return active_positions
async def close_user_trade(tg_id: int, symbol: str, message=None) -> bool: async def cancel_all_tp_sl_orders(tg_id, symbol):
try: """
Отменяет все открытые ордера TP/SL для указанного символа.
"""
api_key = await rq.get_bybit_api_key(tg_id) api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id) secret_key = await rq.get_bybit_secret_key(tg_id)
client = HTTP(api_key=api_key, api_secret=secret_key) 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) positions_resp = client.get_positions(category="linear", symbol=symbol)
if positions_resp.get('retCode') != 0: if positions_resp.get('retCode') != 0:
return False return False
positions_list = positions_resp.get('result', {}).get('list', []) positions_list = positions_resp.get('result', {}).get('list', [])
@@ -318,24 +603,15 @@ async def close_user_trade(tg_id: int, symbol: str, message=None) -> bool:
if qty == 0: if qty == 0:
return False return False
# Получаем настройки пользователя
data_main_stgs = await rq.get_user_main_settings(tg_id)
order_type = data_main_stgs.get('entry_order_type')
limit_price = await rq.get_limit_price(tg_id)
# Получаем открытые ордера
orders = client.get_open_orders(category='linear', symbol=symbol) 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', []) open_orders_list = orders.get('result', {}).get('list', [])
order_id = open_orders_list[0].get('orderId') if open_orders_list else None order_id = open_orders_list[0].get('orderId') if open_orders_list else None
close_side = "Sell" if side == "Buy" else "Buy" close_side = "Sell" if side == "Buy" else "Buy"
# Получаем текущую цену
ticker_resp = client.get_tickers(category="linear", symbol=symbol) ticker_resp = client.get_tickers(category="linear", symbol=symbol)
current_price = 0.0 current_price = 0.0
if ticker_resp.get('retCode') == 0: if ticker_resp.get('retCode') == 0:
result = ticker_resp.get('result', {}) result = ticker_resp.get('result', {})
# поддержать оба варианта: result это dict с key 'list', или list
ticker_list = [] ticker_list = []
if isinstance(result, dict): if isinstance(result, dict):
ticker_list = result.get('list', []) ticker_list = result.get('list', [])
@@ -344,24 +620,6 @@ async def close_user_trade(tg_id: int, symbol: str, message=None) -> bool:
if ticker_list: if ticker_list:
current_price = float(ticker_list[0].get('lastPrice', 0.0)) current_price = float(ticker_list[0].get('lastPrice', 0.0))
if order_type == 'Limit':
# Если есть открытый лимитный ордер отменяем его
if order_id:
cancel_resp = client.cancel_order(category='linear', symbol=symbol, orderId=order_id)
if cancel_resp.get('retCode') != 0:
if message:
await message.answer("Ошибка при отмене лимитного ордера.",
reply_markup=inline_markup.back_to_main)
return False
# Можно здесь добавить логику выставления лимитного ордера на закрытие, если нужно
# В текущем коде отсутствует
if message:
await message.answer(f"Лимитный ордер отменён, позиция не закрыта автоматически.",
reply_markup=inline_markup.back_to_main)
return False
else:
# Рыночный ордер для закрытия позиции
place_resp = client.place_order( place_resp = client.place_order(
category="linear", category="linear",
symbol=symbol, symbol=symbol,
@@ -371,19 +629,39 @@ async def close_user_trade(tg_id: int, symbol: str, message=None) -> bool:
timeInForce="GTC", timeInForce="GTC",
reduceOnly=True reduceOnly=True
) )
if place_resp.get('retCode', -1) == 0: if place_resp.get('retCode', -1) == 0:
if message: 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 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 pnl_percent = (pnl / (entry_price * qty)) * 100 if entry_price * qty > 0 else 0
text = (f"Сделка {symbol} успешно закрыта.\n"
text = (
f"Сделка {symbol} успешно закрыта.\n"
f"Цена входа: {entry_price if entry_price else limit_price}\n" f"Цена входа: {entry_price if entry_price else limit_price}\n"
f"Цена закрытия: {current_price}\n" f"Цена закрытия: {current_price}\n"
f"Результат: {pnl:.4f} USDT ({pnl_percent:.2f}%)") f"Прибыль: {pnl:.4f} USDT ({pnl_percent:.2f}%)\n"
await message.answer(text, reply_markup=inline_markup.back_to_main) f"{'Включая комиссию биржи' if include_fee else 'Без учета комиссии'}"
)
await message.answer(text)
return True return True
else: else:
if message: if message:
await message.answer(f"Ошибка закрытия сделки {symbol}.", reply_markup=inline_markup.back_to_main) await message.answer(f"Ошибка закрытия сделки {symbol}.",
reply_markup=inline_markup.back_to_main)
return False return False
except Exception as e: except Exception as e:
@@ -394,9 +672,12 @@ async def close_user_trade(tg_id: int, symbol: str, message=None) -> bool:
async def close_trade_after_delay(tg_id: int, message, symbol: str, delay_sec: int): async def close_trade_after_delay(tg_id: int, message, symbol: str, delay_sec: int):
"""
Закрывает сделку пользователя после задержки delay_sec секунд.
"""
try: try:
await asyncio.sleep(delay_sec) await asyncio.sleep(delay_sec)
result = await close_user_trade(tg_id, symbol) result = await close_user_trade(tg_id, symbol, message)
if result: if result:
await message.answer(f"Сделка {symbol} успешно закрыта по таймеру.", await message.answer(f"Сделка {symbol} успешно закрыта по таймеру.",
reply_markup=inline_markup.back_to_main) reply_markup=inline_markup.back_to_main)
@@ -405,11 +686,3 @@ async def close_trade_after_delay(tg_id: int, message, symbol: str, delay_sec: i
reply_markup=inline_markup.back_to_main) reply_markup=inline_markup.back_to_main)
except asyncio.CancelledError: except asyncio.CancelledError:
await message.answer(f"Закрытие сделки {symbol} по таймеру отменено.", reply_markup=inline_markup.back_to_main) await message.answer(f"Закрытие сделки {symbol} по таймеру отменено.", reply_markup=inline_markup.back_to_main)
finally:
active_close_tasks.pop(tg_id, None)
def get_positive_percent(negative_percent: float, manual_positive_percent: float | None) -> float:
if manual_positive_percent and manual_positive_percent > 0:
return manual_positive_percent
return abs(negative_percent)

View File

@@ -2,10 +2,13 @@
import logging.config import logging.config
from aiogram import F, Router from aiogram import F, Router
from app.tasks.tasks import active_close_tasks, active_start_tasks
from logger_helper.logger_helper import LOGGING_CONFIG from logger_helper.logger_helper import LOGGING_CONFIG
from app.services.Bybit.functions.Futures import close_user_trade, get_active_positions, close_trade_after_delay, \ from app.services.Bybit.functions.Futures import (close_user_trade, close_trade_after_delay,
active_close_tasks, active_start_tasks, trading_cycle, open_position trading_cycle, open_position, set_take_profit_stop_loss, \
get_active_positions_by_symbol, get_active_orders_by_symbol,
start_execution_ws)
from app.services.Bybit.functions.balance import get_balance from app.services.Bybit.functions.balance import get_balance
import app.telegram.Keyboards.inline_keyboards as inline_markup import app.telegram.Keyboards.inline_keyboards as inline_markup
@@ -14,8 +17,8 @@ import app.telegram.database.requests as rq
from aiogram.types import Message, CallbackQuery from aiogram.types import Message, CallbackQuery
from app.services.Bybit.functions.price_symbol import get_price from app.services.Bybit.functions.price_symbol import get_price
# FSM - Механизм состояния from app.states.States import (state_update_entry_type, state_update_symbol, state_limit_price,
from aiogram.fsm.state import State, StatesGroup SetTP_SL_State, CloseTradeTimerState)
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from app.services.Bybit.functions.get_valid_symbol import get_valid_symbols from app.services.Bybit.functions.get_valid_symbol import get_valid_symbols
@@ -26,75 +29,62 @@ logger = logging.getLogger("functions")
router_functions_bybit_trade = Router() router_functions_bybit_trade = Router()
class state_update_symbol(StatesGroup):
symbol = State()
class state_update_entry_type(StatesGroup):
entry_type = State()
class TradeSetup(StatesGroup):
waiting_for_timer = State()
waiting_for_positive_percent = State()
class state_limit_price(StatesGroup):
price = State()
class CloseTradeTimerState(StatesGroup):
waiting_for_delay = State()
@router_functions_bybit_trade.callback_query(F.data.in_(['clb_start_trading', 'clb_back_to_main', 'back_to_main'])) @router_functions_bybit_trade.callback_query(F.data.in_(['clb_start_trading', 'clb_back_to_main', 'back_to_main']))
async def clb_start_bybit_trade_message(callback: CallbackQuery, state: FSMContext): async def clb_start_bybit_trade_message(callback: CallbackQuery) -> None:
"""
Обработка нажатия кнопок запуска торговли или возврата в главное меню.
Отправляет информацию о балансе, символе, цене и инструкциях по торговле.
"""
balance = await get_balance(callback.from_user.id, callback.message) balance = await get_balance(callback.from_user.id, callback.message)
price = await get_price(callback.from_user.id) price = await get_price(callback.from_user.id)
if balance: if balance:
symbol = await rq.get_symbol(callback.from_user.id) symbol = await rq.get_symbol(callback.from_user.id)
text = f'''💎 Торговля на Bybit text = (
f"💎 Торговля на Bybit\n\n"
⚖️ Ваш баланс (USDT): {balance} f"⚖️ Ваш баланс (USDT): {balance}\n"
📊 Текущая торговая пара: {symbol} f"📊 Текущая торговая пара: {symbol}\n"
$$$ Цена: {price} f"$$$ Цена: {price}\n\n"
"Как начать торговлю?\n\n"
Как начать торговлю? "1⃣ Проверьте и тщательно настройте все параметры в вашем профиле.\n"
"2⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).\n"
1⃣ Проверьте и тщательно настройте все параметры в вашем профиле. "3⃣ Нажмите кнопку 'Выбрать тип входа' и после нажмите начать торговлю.\n"
2⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT). )
3⃣ Нажмите кнопку 'Выбрать тип входа' и после нажмите начать торговлю.
'''
await callback.message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup) await callback.message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup)
asyncio.create_task(start_execution_ws(callback.from_user.id, callback.message))
async def start_bybit_trade_message(message: Message, state: FSMContext): async def start_bybit_trade_message(message: Message) -> None:
"""
Отправляет пользователю информацию о балансе, символе и текущей цене,
вместе с инструкциями по началу торговли.
"""
balance = await get_balance(message.from_user.id, message) balance = await get_balance(message.from_user.id, message)
price = await get_price(message.from_user.id) price = await get_price(message.from_user.id)
if balance: if balance:
symbol = await rq.get_symbol(message.from_user.id) symbol = await rq.get_symbol(message.from_user.id)
text = f'''💎 Торговля на Bybit text = (
f"💎 Торговля на Bybit\n\n"
⚖️ Ваш баланс (USDT): {balance} f"⚖️ Ваш баланс (USDT): {balance}\n"
📊 Текущая торговая пара: {symbol} f"📊 Текущая торговая пара: {symbol}\n"
$$$ Цена: {price} f"$$$ Цена: {price}\n\n"
"Как начать торговлю?\n\n"
Как начать торговлю? "1⃣ Проверьте и тщательно настройте все параметры в вашем профиле.\n"
"2⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).\n"
1⃣ Проверьте и тщательно настройте все параметры в вашем профиле. "3⃣ Нажмите кнопку 'Выбрать тип входа' и после нажмите начать торговлю.\n"
2⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT). )
3⃣ Нажмите кнопку 'Выбрать тип входа' и после нажмите начать торговлю.
'''
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup) await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup)
@router_functions_bybit_trade.callback_query(F.data == 'clb_update_trading_pair') @router_functions_bybit_trade.callback_query(F.data == 'clb_update_trading_pair')
async def update_symbol_for_trade_message(callback: CallbackQuery, state: FSMContext): async def update_symbol_for_trade_message(callback: CallbackQuery, state: FSMContext) -> None:
"""
Начинает процедуру обновления торговой пары, переводит пользователя в состояние ожидания пары.
"""
await state.set_state(state_update_symbol.symbol) await state.set_state(state_update_symbol.symbol)
await callback.message.answer( await callback.message.answer(
@@ -103,7 +93,11 @@ async def update_symbol_for_trade_message(callback: CallbackQuery, state: FSMCon
@router_functions_bybit_trade.message(state_update_symbol.symbol) @router_functions_bybit_trade.message(state_update_symbol.symbol)
async def update_symbol_for_trade(message: Message, state: FSMContext): async def update_symbol_for_trade(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод торговой пары пользователем и проверяет её валидность.
При успешном обновлении сохранит пару и отправит обновлённую информацию.
"""
user_input = message.text.strip().upper() user_input = message.text.strip().upper()
exists = await get_valid_symbols(message.from_user.id, user_input) exists = await get_valid_symbols(message.from_user.id, user_input)
@@ -115,20 +109,28 @@ async def update_symbol_for_trade(message: Message, state: FSMContext):
await state.update_data(symbol=message.text) await state.update_data(symbol=message.text)
await message.answer('Пара была успешно обновлена') await message.answer('Пара была успешно обновлена')
await rq.update_symbol(message.from_user.id, user_input) await rq.update_symbol(message.from_user.id, user_input)
await start_bybit_trade_message(message, state) await start_bybit_trade_message(message)
await state.clear() await state.clear()
@router_functions_bybit_trade.callback_query(F.data == 'clb_update_entry_type') @router_functions_bybit_trade.callback_query(F.data == 'clb_update_entry_type')
async def update_entry_type_message(callback: CallbackQuery, state: FSMContext): async def update_entry_type_message(callback: CallbackQuery, state: FSMContext) -> None:
"""
Запрашивает у пользователя тип входа в позицию (Market или Limit).
"""
await state.set_state(state_update_entry_type.entry_type) await state.set_state(state_update_entry_type.entry_type)
await callback.message.answer("Выберите тип входа в позицию:", reply_markup=inline_markup.entry_order_type_markup) await callback.message.answer("Выберите тип входа в позицию:", reply_markup=inline_markup.entry_order_type_markup)
await callback.answer() await callback.answer()
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith('entry_order_type:')) @router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith('entry_order_type:'))
async def entry_order_type_callback(callback: CallbackQuery, state: FSMContext): async def entry_order_type_callback(callback: CallbackQuery, state: FSMContext) -> None:
"""
Обработка выбора типа входа в позицию.
Если Limit, запрашивает цену лимитного ордера.
Если Market — обновляет настройки.
"""
order_type = callback.data.split(':')[1] order_type = callback.data.split(':')[1]
if order_type not in ['Market', 'Limit']: if order_type not in ['Market', 'Limit']:
@@ -155,7 +157,10 @@ async def entry_order_type_callback(callback: CallbackQuery, state: FSMContext):
@router_functions_bybit_trade.message(state_limit_price.price) @router_functions_bybit_trade.message(state_limit_price.price)
async def set_limit_price(message: Message, state: FSMContext): async def set_limit_price(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод цены лимитного ордера, проверяет формат и сохраняет настройки.
"""
try: try:
price = float(message.text) price = float(message.text)
if price <= 0: if price <= 0:
@@ -175,65 +180,16 @@ async def set_limit_price(message: Message, state: FSMContext):
await state.clear() await state.clear()
@router_functions_bybit_trade.callback_query(F.data == "clb_my_deals")
async def show_my_trades_callback(callback: CallbackQuery):
await callback.answer() # сразу отвечаем Telegram, освобождаем callback
async def process():
tg_id = callback.from_user.id
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
trades = await get_active_positions(callback.message, api_key, secret_key)
if not trades:
await callback.message.answer("Нет активных позиций.")
return
keyboard = inline_markup.create_trades_inline_keyboard(trades)
await callback.message.answer(
"Выберите сделку из списка:",
reply_markup=keyboard
)
asyncio.create_task(process())
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith('select_trade:'))
async def on_trade_selected(callback: CallbackQuery):
await callback.answer()
async def process():
tg_id = callback.from_user.id
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
positions = await get_active_positions(callback.message, api_key, secret_key)
if not positions:
await callback.message.answer("Позиция не найдена")
return
pos = positions[0]
text = (f"Информация по позиции:\n"
f"Название: {pos.get('symbol')}\n"
f"Направление: {pos.get('side')}\n"
f"Цена покупки: {pos.get('entryPrice')}\n"
f"Текущая цена: {pos.get('price', 'N/A')}")
keyboard = inline_markup.create_close_deal_markup(pos.get('symbol'))
await callback.message.answer(text, reply_markup=keyboard)
asyncio.create_task(process())
@router_functions_bybit_trade.callback_query(F.data == "clb_start_chatbot_trading") @router_functions_bybit_trade.callback_query(F.data == "clb_start_chatbot_trading")
async def start_trading_process(callback: CallbackQuery): async def start_trading_process(callback: CallbackQuery) -> None:
"""
Запускает торговый цикл в выбранном режиме Long/Short.
Проверяет API-ключи, режим торговли, маржинальный режим и открытые позиции,
затем запускает торговый цикл с задержкой или без неё.
"""
tg_id = callback.from_user.id tg_id = callback.from_user.id
message = callback.message message = callback.message
# Получаем настройки пользователя
data_main_stgs = await rq.get_user_main_settings(tg_id) data_main_stgs = await rq.get_user_main_settings(tg_id)
api_key = await rq.get_bybit_api_key(tg_id) api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id) secret_key = await rq.get_bybit_secret_key(tg_id)
@@ -241,24 +197,21 @@ async def start_trading_process(callback: CallbackQuery):
margin_mode = data_main_stgs.get('margin_type', 'Isolated') margin_mode = data_main_stgs.get('margin_type', 'Isolated')
trading_mode = data_main_stgs.get('trading_mode') trading_mode = data_main_stgs.get('trading_mode')
# Проверка API ключей
if not api_key or not secret_key: if not api_key or not secret_key:
await message.answer("❗️ У вас не настроены API ключи для Bybit.") await message.answer("❗️ У вас не настроены API ключи для Bybit.")
await callback.answer() await callback.answer()
return return
# Проверка режима торговли
if trading_mode not in ['Long', 'Short', 'Smart', 'Switch']: if trading_mode not in ['Long', 'Short', 'Smart', 'Switch']:
await message.answer(f"❗️ Некорректный торговый режим: {trading_mode}") await message.answer(f"❗️ Некорректный торговый режим: {trading_mode}")
await callback.answer() await callback.answer()
return return
# Проверка допустимости маржинального режима
if margin_mode not in ['Isolated', 'Cross']: if margin_mode not in ['Isolated', 'Cross']:
margin_mode = 'Isolated' margin_mode = 'Isolated'
# Проверяем открытые позиции и маржинальный режим
client = HTTP(api_key=api_key, api_secret=secret_key) client = HTTP(api_key=api_key, api_secret=secret_key)
try: try:
positions_resp = client.get_positions(category='linear', symbol=symbol) positions_resp = client.get_positions(category='linear', symbol=symbol)
positions = positions_resp.get('result', {}).get('list', []) positions = positions_resp.get('result', {}).get('list', [])
@@ -275,17 +228,16 @@ async def start_trading_process(callback: CallbackQuery):
f"(текущий режим: {existing_margin_mode})", show_alert=True) f"(текущий режим: {existing_margin_mode})", show_alert=True)
return return
# Определяем сторону для открытия позиции
if trading_mode == 'Long': if trading_mode == 'Long':
side = 'Buy' side = 'Buy'
elif trading_mode == 'Short': elif trading_mode == 'Short':
side = 'Sell' side = 'Sell'
else: else:
await message.answer(f"Режим торговли '{trading_mode}' пока не поддерживается.") await message.answer(f"Режим торговли '{trading_mode}' пока не поддерживается.",
reply_markup=inline_markup.back_to_main)
await callback.answer() await callback.answer()
return return
# Сообщаем о начале торговли
await message.answer("Начинаю торговлю с использованием текущих настроек...") await message.answer("Начинаю торговлю с использованием текущих настроек...")
timer_data = await rq.get_user_timer(tg_id) timer_data = await rq.get_user_timer(tg_id)
@@ -294,16 +246,15 @@ async def start_trading_process(callback: CallbackQuery):
else: else:
timer_minute = timer_data or 0 timer_minute = timer_data or 0
logger.info(f"Timer minutes for user {tg_id}: {timer_minute}")
symbol = await rq.get_symbol(tg_id)
if timer_minute > 0: if timer_minute > 0:
old_task = active_start_tasks.get(tg_id) old_task = active_start_tasks.get(tg_id)
if old_task: if old_task and not old_task.done():
old_task.cancel() old_task.cancel()
# можно ждать завершения старой задачи, если в async функции try:
task = asyncio.create_task(trading_cycle(tg_id, message, symbol)) await old_task
except asyncio.CancelledError:
logger.info(f"Старая задача торговли для пользователя {tg_id} отменена")
task = asyncio.create_task(trading_cycle(tg_id, message))
active_start_tasks[tg_id] = task active_start_tasks[tg_id] = task
await message.answer(f"Торговля начнётся через {timer_minute} мин. Для отмены нажмите кнопку ниже.", await message.answer(f"Торговля начнётся через {timer_minute} мин. Для отмены нажмите кнопку ниже.",
reply_markup=inline_markup.cancel_start_markup) reply_markup=inline_markup.cancel_start_markup)
@@ -314,8 +265,113 @@ async def start_trading_process(callback: CallbackQuery):
await callback.answer() await callback.answer()
@router_functions_bybit_trade.callback_query(F.data == "clb_my_deals")
async def show_my_trades(callback: CallbackQuery) -> None:
"""
Отображает пользователю выбор типа сделки по текущей торговой паре.
"""
await callback.answer()
try:
symbol = await rq.get_symbol(callback.from_user.id)
await callback.message.answer(f"Выберите тип сделки для пары {symbol}:",
reply_markup=inline_markup.my_deals_select_markup)
except Exception as e:
logger.error(f"Произошла ошибка при выборе типа сделки: {e}")
@router_functions_bybit_trade.callback_query(F.data == "clb_open_deals")
async def show_my_trades_callback(callback: CallbackQuery):
await callback.answer()
try:
await get_active_positions_by_symbol(callback.from_user.id, message=callback.message)
except Exception as e:
logger.error(f"Произошла ошибка при выборе сделки: {e}")
await callback.message.answer("Произошла ошибка при выборе сделки", reply_markup=inline_markup.back_to_main)
@router_functions_bybit_trade.callback_query(F.data == "clb_open_orders")
async def show_my_orders_callback(callback: CallbackQuery) -> None:
"""
Показывает открытые позиции пользователя по символу.
"""
await callback.answer()
try:
await get_active_orders_by_symbol(callback.from_user.id, message=callback.message)
except Exception as e:
logger.error(f"Произошла ошибка при выборе ордера: {e}")
await callback.message.answer("Произошла ошибка при выборе ордера", reply_markup=inline_markup.back_to_main)
@router_functions_bybit_trade.callback_query(F.data == "clb_set_tp_sl")
async def set_tp_sl(callback: CallbackQuery, state: FSMContext) -> None:
"""
Показывает активные ордера пользователя.
"""
await callback.answer()
await state.set_state(SetTP_SL_State.waiting_for_take_profit)
await callback.message.answer("Введите значение Take Profit (в цене, например 26000.5):",
reply_markup=inline_markup.cancel)
@router_functions_bybit_trade.message(SetTP_SL_State.waiting_for_take_profit)
async def process_take_profit(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод значения Take Profit и запрашивает Stop Loss.
"""
try:
tp = float(message.text.strip())
if tp <= 0:
await message.answer("Значение Take Profit должно быть положительным числом. Попробуйте снова.",
reply_markup=inline_markup.cancel)
return
except ValueError:
await message.answer("Некорректный ввод. Пожалуйста, введите число для Take Profit.",
reply_markup=inline_markup.cancel)
return
await state.update_data(take_profit=tp)
await state.set_state(SetTP_SL_State.waiting_for_stop_loss)
await message.answer("Введите значение Stop Loss (в цене, например 24500.3):", reply_markup=inline_markup.cancel)
@router_functions_bybit_trade.message(SetTP_SL_State.waiting_for_stop_loss)
async def process_stop_loss(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод значения Stop Loss и завершает процесс установки TP/SL.
"""
try:
sl = float(message.text.strip())
if sl <= 0:
await message.answer("Значение Stop Loss должно быть положительным числом. Попробуйте снова.",
reply_markup=inline_markup.cancel)
return
except ValueError:
await message.answer("Некорректный ввод. Пожалуйста, введите число для Stop Loss.",
reply_markup=inline_markup.cancel)
return
data = await state.get_data()
tp = data.get("take_profit")
if tp is None:
await message.answer("Ошибка, не найдено значение Take Profit. Попробуйте снова.")
await state.clear()
return
tg_id = message.from_user.id
await set_take_profit_stop_loss(tg_id, message, take_profit_price=tp, stop_loss_price=sl)
await state.clear()
@router_functions_bybit_trade.callback_query(F.data == "clb_stop_timer") @router_functions_bybit_trade.callback_query(F.data == "clb_stop_timer")
async def cancel_start_callback(callback: CallbackQuery): async def cancel_start_callback(callback: CallbackQuery) -> None:
"""
Отменяет задачу старта торговли по таймеру, если она активна.
"""
tg_id = callback.from_user.id tg_id = callback.from_user.id
task = active_start_tasks.get(tg_id) task = active_start_tasks.get(tg_id)
if task: if task:
@@ -327,45 +383,44 @@ async def cancel_start_callback(callback: CallbackQuery):
await callback.answer() await callback.answer()
@router_functions_bybit_trade.callback_query(F.data == "clb_stop_timer")
async def cancel_start_callback(callback: CallbackQuery):
tg_id = callback.from_user.id
task = active_close_tasks.get(tg_id)
if task:
task.cancel()
del active_close_tasks[tg_id]
await callback.message.answer("Таймер отменен.", reply_markup=inline_markup.back_to_main)
else:
await callback.message.answer("Нет активности для отмены.", reply_markup=inline_markup.back_to_main)
await callback.answer()
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_deal:")) @router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_deal:"))
async def close_trade_callback(callback: CallbackQuery): async def close_trade_callback(callback: CallbackQuery) -> None:
"""
Закрывает сделку пользователя по символу.
"""
symbol = callback.data.split(':')[1] symbol = callback.data.split(':')[1]
tg_id = callback.from_user.id tg_id = callback.from_user.id
result = await close_user_trade(tg_id, symbol) result = await close_user_trade(tg_id, symbol, message=callback.message)
if result: if result:
await callback.message.answer(f"Сделка {symbol} успешно закрыта.", reply_markup=inline_markup.back_to_main) await callback.message.answer("Сделка успешно закрыта.", reply_markup=inline_markup.back_to_main)
logger.info(f"Сделка {symbol} успешно закрыта.")
else: else:
logger.error(f"Не удалось закрыть сделку {symbol}.")
await callback.message.answer(f"Не удалось закрыть сделку {symbol}.") await callback.message.answer(f"Не удалось закрыть сделку {symbol}.")
await callback.answer() await callback.answer()
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_deal_by_timer:")) @router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_deal_by_timer:"))
async def ask_close_delay(callback: CallbackQuery, state: FSMContext): async def ask_close_delay(callback: CallbackQuery, state: FSMContext) -> None:
"""
Запускает диалог с пользователем для задания задержки перед закрытием сделки.
"""
symbol = callback.data.split(":")[1] symbol = callback.data.split(":")[1]
await state.update_data(symbol=symbol) await state.update_data(symbol=symbol)
await state.set_state(CloseTradeTimerState.waiting_for_delay) await state.set_state(CloseTradeTimerState.waiting_for_delay)
await callback.message.answer("Введите задержку в минутах до закрытия сделки (например, 60):") await callback.message.answer("Введите задержку в минутах до закрытия сделки (например, 60):",
reply_markup=inline_markup.cancel)
await callback.answer() await callback.answer()
@router_functions_bybit_trade.message(CloseTradeTimerState.waiting_for_delay) @router_functions_bybit_trade.message(CloseTradeTimerState.waiting_for_delay)
async def process_close_delay(message: Message, state: FSMContext): async def process_close_delay(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод задержки и запускает задачу закрытия сделки с задержкой.
"""
try: try:
delay_minutes = int(message.text.strip()) delay_minutes = int(message.text.strip())
if delay_minutes <= 0: if delay_minutes <= 0:
@@ -381,9 +436,14 @@ async def process_close_delay(message: Message, state: FSMContext):
delay = delay_minutes * 60 delay = delay_minutes * 60
# Отменяем предыдущую задачу, если есть
if tg_id in active_close_tasks: if tg_id in active_close_tasks:
active_close_tasks[tg_id].cancel() old_task = active_close_tasks[tg_id]
if not old_task.done():
old_task.cancel()
try:
await old_task
except asyncio.CancelledError:
logger.info(f"Предыдущая задача закрытия сделки пользователя {tg_id} отменена")
task = asyncio.create_task(close_trade_after_delay(tg_id, message, symbol, delay)) task = asyncio.create_task(close_trade_after_delay(tg_id, message, symbol, delay))
active_close_tasks[tg_id] = task active_close_tasks[tg_id] = task
@@ -394,16 +454,22 @@ async def process_close_delay(message: Message, state: FSMContext):
@router_functions_bybit_trade.callback_query(F.data == "clb_change_martingale_reset") @router_functions_bybit_trade.callback_query(F.data == "clb_change_martingale_reset")
async def reset_martingale(callback: CallbackQuery): async def reset_martingale(callback: CallbackQuery) -> None:
"""
Сбрасывает шаги мартингейла пользователя.
"""
await callback.answer() await callback.answer()
tg_id = callback.from_user.id tg_id = callback.from_user.id
await rq.update_martingale_step(tg_id, 0) await rq.update_martingale_step(tg_id, 0)
await callback.message.answer("Сброс шагов мартингейла выполнен. Торговля начнется заново с начального объема.", await callback.message.answer("Сброс шагов выполнен.",
reply_markup=inline_markup.back_to_main) reply_markup=inline_markup.back_to_main)
@router_functions_bybit_trade.callback_query(F.data == "clb_cancel") @router_functions_bybit_trade.callback_query(F.data == "clb_cancel")
async def cancel(callback: CallbackQuery, state: FSMContext): async def cancel(callback: CallbackQuery, state: FSMContext) -> None:
"""
Отменяет текущее состояние FSM и сообщает пользователю об отмене.
"""
await state.clear() await state.clear()
await callback.message.answer("Отменено!", reply_markup=inline_markup.back_to_main) await callback.message.answer("Отменено!", reply_markup=inline_markup.back_to_main)
await callback.answer() await callback.answer()