diff --git a/app/services/Bybit/functions/Futures.py b/app/services/Bybit/functions/Futures.py index 11d15b4..d4ab4fe 100644 --- a/app/services/Bybit/functions/Futures.py +++ b/app/services/Bybit/functions/Futures.py @@ -1,52 +1,67 @@ import asyncio +import functools 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") -async def info_access_open_deal(message, symbol, trade_mode, margin_mode, leverage, qty): +active_start_tasks = {} +active_close_tasks = {} + + +def safe_float(val): + try: + if val is None or val == '': + return 0.0 + return float(val) + except (ValueError, TypeError): + 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): human_margin_mode = 'Isolated' if margin_mode == 'ISOLATED_MARGIN' else 'Cross' - text = f'''Позиция была успешна открыта! + text = f'''{'Позиция была успешна открыта' if order_type == 'Market' else 'Лимитный ордер установлен'}! Торговая пара: {symbol} +Цена входа: {entry_price if order_type == 'Market' else round(limit_price, 5)} Движение: {trade_mode} Тип-маржи: {human_margin_mode} -Кредитное плечо: {leverage} +Кредитное плечо: {leverage}x Количество: {qty} +Тейк-профит: {round(tp, 5)} +Стоп-лосс: {round(sl, 5)} ''' - await message.answer(text=text, parse_mode='html') + await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.create_close_deal_markup(symbol)) async def error_max_step(message): - await message.answer('Сделка не была совершена, превышен лимит максимального количества ставок в серии.') + logger.error('Сделка не была совершена, превышен лимит максимального количества ставок в серии.') + await message.answer('Сделка не была совершена, превышен лимит максимального количества ставок в серии.', + reply_markup=inline_markup.back_to_main) async def error_max_risk(message): - await message.answer('Сделка не была совершена, риск убытка превышает допустимый лимит.') + logger.error('Сделка не была совершена, риск убытка превышает допустимый лимит.') + await message.answer('Сделка не была совершена, риск убытка превышает допустимый лимит.', + reply_markup=inline_markup.back_to_main) -async def open_position(tg_id, message, side: str, margin_mode: str): - """ - Открытие позиции (торговля с мартингейлом и управлением рисками) - - :param tg_id: Telegram ID пользователя - :param message: объект сообщения Telegram для ответов - :param side: 'Buy' для Long, 'Sell' для Short - :param margin_mode: 'Isolated' или 'Cross' - """ - +async def open_position(tg_id, message, side: str, margin_mode: str, tpsl_mode='Full'): 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', 'Market') + 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) @@ -55,192 +70,346 @@ async def open_position(tg_id, message, side: str, margin_mode: str): 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 = float(data_main_stgs['martingale_factor']) - max_martingale_steps = int(data_main_stgs['maximal_quantity']) - starting_quantity = float(data_main_stgs['starting_quantity']) - max_risk_percent = float(data_risk_stgs['max_risk_deal']) - loss_profit = float(data_risk_stgs['price_loss']) - takeprofit = float(data_risk_stgs['price_profit']) - commission_fee = float(data_risk_stgs.get('commission_fee', 0)) + 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')) - takeProfit = max(takeprofit - commission_fee, 0) + 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')) + commission_fee = safe_float(data_risk_stgs.get('commission_fee', 0)) + 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 - last_quantity = starting_quantity realised_pnl = 0.0 - # Получаем текущие открытые позиции по символу - positions_resp = client.get_positions(category='linear', symbol=symbol) - positions_list = positions_resp.get('result', {}).get('list', []) - current_martingale_step = await rq.get_martingale_step(tg_id) + current_martingale = await rq.get_martingale_step(tg_id) + current_martingale_step = int(current_martingale) + if positions_list: - position = positions_list[0] - realised_pnl = float(position.get('unrealisedPnl', 0.0)) - - 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 = starting_quantity * (martingale_factor ** current_martingale_step) + next_quantity = float(starting_quantity) * (float(martingale_factor) ** current_martingale_step) else: - # Позиция не открыта — начинаем с начальной ставки next_quantity = starting_quantity current_martingale_step = 0 - # Проверяем риск убытка - potential_loss = next_quantity * price * (loss_profit / 100) - allowed_loss = balance * (max_risk_percent / 100) + 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"Пожалуйста, увеличьте количество позиций.") + 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=next_quantity, - leverage=int(data_main_stgs['size_leverage']), - price=limit_price if order_type == 'Limit' else None, - takeProfit=takeProfit, - stopLoss=loss_profit, + orderType=order_type, # Market или Limit + 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('ret_code', -1) == 0: - await info_access_open_deal(message, symbol, data_main_stgs['trading_mode'], bybit_margin_mode, - data_main_stgs['size_leverage'], next_quantity) + if response.get('retCode', -1) == 0: + await info_access_open_deal(message, symbol, data_main_stgs.get('trading_mode', ''), + bybit_margin_mode, + data_main_stgs.get('size_leverage', 1), next_quantity, take_profit_price, + stop_loss_price, entry_price, limit_price, order_type=order_type) await rq.update_martingale_step(tg_id, current_martingale_step) + return True else: - await message.answer(f"Ошибка открытия ордера: {response.get('ret_msg', 'неизвестная ошибка')}") + 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('Ошибка: неверно указана торговая пара или параметры.') + 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): - start_time = time.time() - timer_min = await rq.get_user_timer(tg_id) - timer_sec = timer_min * 60 if timer_min else 0 + 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 - while True: - elapsed = time.time() - start_time - if timer_sec > 0 and elapsed > timer_sec: - await message.answer("Время работы по таймеру истекло. Торговля остановлена.") - await rq.update_martingale_step(tg_id, 0) - break + 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['trading_mode'] == 'Long' else 'Sell' + side = 'Buy' if data_main_stgs.get('trading_mode', '') == 'Long' else 'Sell' margin_mode = data_main_stgs.get('margin_type', 'Isolated') - # Можно добавлять логику по PNL, стоп-лоссам, тейк-профитам - await open_position(tg_id, message, side=side, margin_mode=margin_mode) - - await asyncio.sleep(10) + except asyncio.CancelledError: + logger.info(f"Торговый цикл для пользователя {tg_id} был отменён.") +async def fetch_positions_async(client, symbol): + loop = asyncio.get_running_loop() + # запускаем блокирующий вызов get_positions в отдельном потоке + return await loop.run_in_executor(None, functools.partial(client.get_positions, category='linear', symbol=symbol)) -async def get_active_positions(message, api_key, secret_key, symbol): +async def get_active_positions(message, api_key, secret_key): client = HTTP( api_key=api_key, api_secret=secret_key ) instruments_resp = client.get_instruments_info(category='linear') - if instruments_resp.get('ret_code') != 0: + if instruments_resp.get('retCode') != 0: return [] symbols = [item['symbol'] for item in instruments_resp.get('result', {}).get('list', [])] active_positions = [] - async def fetch_positions(symbol): + for sym in symbols: try: - resp = client.get_positions(category='linear', symbol=symbol) - if resp.get('ret_code') == 0: + resp = await fetch_positions_async(client, sym) + if resp.get('retCode') == 0: positions = resp.get('result', {}).get('list', []) for pos in positions: - if pos.get('size') and float(pos['size']) > 0: + if pos.get('size') and safe_float(pos['size']) > 0: active_positions.append(pos) except Exception as e: logger.error(f"Ошибка при получении позиций: {e}") - await message.answer('⚠️ Ошибка при получении позиций') - - for sym in symbols: - await fetch_positions(sym) + await message.answer('⚠️ Ошибка при получении позиций', reply_markup=inline_markup.back_to_main) return active_positions -async def close_user_trade(tg_id: int, symbol: str) -> bool: - 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) - - # Получаем текущие открытые позиции по символу (пример для linear фьючерсов) - positions_resp = client.get_positions(category="linear", symbol=symbol) - - ret_code = positions_resp.get('ret_code') - result = positions_resp.get('result') - - if ret_code != 0 or not result or not result.get('list'): - return False - - positions_list = result['list'] - if not positions_list: - return False - - position = positions_list[0] - qty = abs(float(position['size'])) - side = position['side'] - - if qty == 0: - return False - - # Определяем сторону закрытия — противоположная открытой позиции - close_side = "Sell" if side == "Buy" else "Buy" - +async def close_user_trade(tg_id: int, symbol: str, message=None) -> bool: try: - response = client.place_order( - category="linear", - symbol=symbol, - side=close_side, - orderType="Market", - qty=str(qty), - timeInForce="GoodTillCancel", - reduceOnly=True - ) - return response['ret_code'] == 0 - except Exception as e: - logger.error(f"Ошибка закрытия сделки {symbol} для пользователя {tg_id}: {e}") + 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) + # Получаем текущие открытые позиции + 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 + + # Получаем настройки пользователя + 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) + 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', {}) + # поддержать оба варианта: result это dict с key 'list', или list + 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)) + + 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( + category="linear", + symbol=symbol, + side=close_side, + orderType="Market", + qty=str(qty), + timeInForce="GTC", + reduceOnly=True + ) + if place_resp.get('retCode', -1) == 0: + if message: + pnl = (current_price - entry_price) * qty if side == "Buy" else (entry_price - current_price) * qty + pnl_percent = (pnl / (entry_price * qty)) * 100 if entry_price * qty > 0 else 0 + text = (f"Сделка {symbol} успешно закрыта.\n" + f"Цена входа: {entry_price if entry_price else limit_price}\n" + f"Цена закрытия: {current_price}\n" + f"Результат: {pnl:.4f} USDT ({pnl_percent:.2f}%)") + await message.answer(text, reply_markup=inline_markup.back_to_main) + 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): + try: + await asyncio.sleep(delay_sec) + result = await close_user_trade(tg_id, symbol) + 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) + 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) \ No newline at end of file + return abs(negative_percent) diff --git a/app/services/Bybit/functions/functions.py b/app/services/Bybit/functions/functions.py index 021d411..fa80407 100644 --- a/app/services/Bybit/functions/functions.py +++ b/app/services/Bybit/functions/functions.py @@ -1,15 +1,18 @@ import asyncio import logging.config from aiogram import F, Router + from logger_helper.logger_helper import LOGGING_CONFIG -from app.services.Bybit.functions import Futures, min_qty -from app.services.Bybit.functions.Futures import open_position, close_user_trade, get_active_positions, trading_cycle + +from app.services.Bybit.functions.Futures import close_user_trade, get_active_positions, close_trade_after_delay, \ + active_close_tasks, active_start_tasks, trading_cycle, open_position from app.services.Bybit.functions.balance import get_balance import app.telegram.Keyboards.inline_keyboards as inline_markup -import app.telegram.Keyboards.reply_keyboards as reply_markup + from pybit.unified_trading import HTTP import app.telegram.database.requests as rq from aiogram.types import Message, CallbackQuery +from app.services.Bybit.functions.price_symbol import get_price # FSM - Механизм состояния from aiogram.fsm.state import State, StatesGroup @@ -35,16 +38,19 @@ 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'])) async def clb_start_bybit_trade_message(callback: CallbackQuery, state: FSMContext): - api = await rq.get_bybit_api_key(callback.from_user.id) - secret = await rq.get_bybit_secret_key(callback.from_user.id) balance = await get_balance(callback.from_user.id, callback.message) + price = await get_price(callback.from_user.id) if balance: symbol = await rq.get_symbol(callback.from_user.id) @@ -53,6 +59,7 @@ async def clb_start_bybit_trade_message(callback: CallbackQuery, state: FSMConte ⚖️ Ваш баланс (USDT): {balance} 📊 Текущая торговая пара: {symbol} +$$$ Цена: {price} Как начать торговлю? @@ -63,10 +70,9 @@ async def clb_start_bybit_trade_message(callback: CallbackQuery, state: FSMConte await callback.message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup) -async def start_bybit_trade_message(message, state): - api = await rq.get_bybit_api_key(message.from_user.id) - secret = await rq.get_bybit_secret_key(message.from_user.id) +async def start_bybit_trade_message(message: Message, state: FSMContext): balance = await get_balance(message.from_user.id, message) + price = await get_price(message.from_user.id) if balance: symbol = await rq.get_symbol(message.from_user.id) @@ -75,6 +81,7 @@ async def start_bybit_trade_message(message, state): ⚖️ Ваш баланс (USDT): {balance} 📊 Текущая торговая пара: {symbol} +$$$ Цена: {price} Как начать торговлю? @@ -138,7 +145,7 @@ async def entry_order_type_callback(callback: CallbackQuery, state: FSMContext): await state.update_data(entry_order_type=order_type) await rq.update_entry_order_type(callback.from_user.id, order_type) await callback.message.answer(f"Выбран тип входа в позицию: {order_type}", - reply_markup=inline_markup.start_trading_markup) + reply_markup=inline_markup.start_trading_markup) await callback.answer() except Exception as e: logger.error(f"Произошла ошибка при обновлении типа входа в позицию: {e}") @@ -152,14 +159,14 @@ async def set_limit_price(message: Message, state: FSMContext): try: price = float(message.text) if price <= 0: - await message.answer("Цена должна быть положительным числом. Попробуйте снова.", reply_markup=inline_markup.cancel) + await message.answer("Цена должна быть положительным числом. Попробуйте снова.", + reply_markup=inline_markup.cancel) return except ValueError: await message.answer("Некорректный формат цены. Введите число.", reply_markup=inline_markup.cancel) return await state.update_data(entry_order_type='Limit', limit_price=price) - data = await state.get_data() await rq.update_entry_order_type(message.from_user.id, 'Limit') await rq.update_limit_price(message.from_user.id, price) @@ -168,82 +175,61 @@ async def set_limit_price(message: Message, state: FSMContext): await state.clear() - @router_functions_bybit_trade.callback_query(F.data == "clb_my_deals") async def show_my_trades_callback(callback: CallbackQuery): - tg_id = callback.from_user.id + await callback.answer() # сразу отвечаем Telegram, освобождаем callback - 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) + 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, symbol) + trades = await get_active_positions(callback.message, api_key, secret_key) - if not trades: - await callback.message.answer("Нет активных позиций.") - await callback.answer() - return + if not trades: + await callback.message.answer("Нет активных позиций.") + return - keyboard = inline_markup.create_trades_inline_keyboard(trades) + keyboard = inline_markup.create_trades_inline_keyboard(trades) - await callback.message.answer( - "Выберите сделку из списка:", - reply_markup=keyboard - ) - await callback.answer() + 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): - symbol = callback.data.split(':')[1] - - 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, symbol) - - # Если несколько позиций по символу, можно выбрать нужную или взять первую - if not positions: - await callback.message.answer("Позиция не найдена") - await callback.answer() - return - - pos = positions[0] - symbol = pos.get('symbol') - side = pos.get('side') - entry_price = pos.get('entryPrice') # Цена открытия позиции - current_price = pos.get('price') # Текущая цена (если есть) - - text = (f"Информация по позиции:\n" - f"Название: {symbol}\n" - f"Направление: {side}\n" - f"Цена покупки: {entry_price}\n" - f"Текущая цена: {current_price if current_price else 'N/A'}") - - keyboard = inline_markup.create_close_deal_markup(symbol) - - await callback.message.answer(text, reply_markup=keyboard) 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) -@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_deal:")) -async def close_trade_callback(callback: CallbackQuery): - symbol = callback.data.split(':')[1] - tg_id = callback.from_user.id + positions = await get_active_positions(callback.message, api_key, secret_key) - result = await close_user_trade(tg_id, symbol) + if not positions: + await callback.message.answer("Позиция не найдена") + return - if result: - await callback.message.answer(f"Сделка {symbol} успешно закрыта.") - else: - await callback.message.answer(f"Не удалось закрыть сделку {symbol}.") + 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')}") - await callback.answer() + 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") -async def start_trading_process(callback: CallbackQuery, state: FSMContext): +async def start_trading_process(callback: CallbackQuery): tg_id = callback.from_user.id message = callback.message @@ -302,31 +288,122 @@ async def start_trading_process(callback: CallbackQuery, state: FSMContext): # Сообщаем о начале торговли await message.answer("Начинаю торговлю с использованием текущих настроек...") - # Открываем позицию (вызывает Futures.open_position) - success = await open_position(tg_id, message, side=side, margin_mode=margin_mode) - if not success: - await message.answer('⚠️ Ошибка при совершении сделки', reply_markup=inline_markup.back_to_main) - return - - - # Проверяем таймер и информируем пользователя - timer_data = await rq.get_user_timer(tg_id) - timer_minutes = timer_data.get('timer') if isinstance(timer_data, dict) else timer_data - if timer_minutes and timer_minutes > 0: - await message.answer(f"Торговля будет работать по таймеру: {timer_minutes} мин.") - asyncio.create_task(trading_cycle(tg_id, message)) + if isinstance(timer_data, dict): + timer_minute = timer_data.get('timer_minutes', 0) else: - await message.answer( - "Торговля начата без ограничения по времени. Для остановки нажмите кнопку 'Закрыть сделку'.", - reply_markup=inline_markup.create_close_deal_markup(symbol) - ) + 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: + old_task = active_start_tasks.get(tg_id) + if old_task: + old_task.cancel() + # можно ждать завершения старой задачи, если в async функции + task = asyncio.create_task(trading_cycle(tg_id, message, symbol)) + active_start_tasks[tg_id] = task + await message.answer(f"Торговля начнётся через {timer_minute} мин. Для отмены нажмите кнопку ниже.", + reply_markup=inline_markup.cancel_start_markup) + await rq.update_user_timer(tg_id, minutes=0) + else: + await open_position(tg_id, message, side=side, margin_mode=margin_mode) 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_start_tasks.get(tg_id) + if task: + task.cancel() + del active_start_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(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:")) +async def close_trade_callback(callback: CallbackQuery): + symbol = callback.data.split(':')[1] + tg_id = callback.from_user.id + + result = await close_user_trade(tg_id, symbol) + + if result: + await callback.message.answer(f"Сделка {symbol} успешно закрыта.", reply_markup=inline_markup.back_to_main) + else: + await callback.message.answer(f"Не удалось закрыть сделку {symbol}.") + + await callback.answer() + + +@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): + symbol = callback.data.split(":")[1] + await state.update_data(symbol=symbol) + await state.set_state(CloseTradeTimerState.waiting_for_delay) + await callback.message.answer("Введите задержку в минутах до закрытия сделки (например, 60):") + await callback.answer() + + +@router_functions_bybit_trade.message(CloseTradeTimerState.waiting_for_delay) +async def process_close_delay(message: Message, state: FSMContext): + try: + delay_minutes = int(message.text.strip()) + if delay_minutes <= 0: + await message.answer("Введите положительное число.") + return + except ValueError: + await message.answer("Некорректный ввод. Введите число в минутах.") + return + + data = await state.get_data() + symbol = data.get("symbol") + tg_id = message.from_user.id + + delay = delay_minutes * 60 + + # Отменяем предыдущую задачу, если есть + if tg_id in active_close_tasks: + active_close_tasks[tg_id].cancel() + + task = asyncio.create_task(close_trade_after_delay(tg_id, message, symbol, delay)) + active_close_tasks[tg_id] = task + + await message.answer(f"Закрытие сделки {symbol} запланировано через {delay} секунд.", + reply_markup=inline_markup.cancel_start_markup) + await state.clear() + + +@router_functions_bybit_trade.callback_query(F.data == "clb_change_martingale_reset") +async def reset_martingale(callback: CallbackQuery): + await callback.answer() + tg_id = callback.from_user.id + await rq.update_martingale_step(tg_id, 0) + await callback.message.answer("Сброс шагов мартингейла выполнен. Торговля начнется заново с начального объема.", + reply_markup=inline_markup.back_to_main) + + @router_functions_bybit_trade.callback_query(F.data == "clb_cancel") async def cancel(callback: CallbackQuery, state: FSMContext): await state.clear() await callback.message.answer("Отменено!", reply_markup=inline_markup.back_to_main) - await callback.answer() \ No newline at end of file + await callback.answer()