From cb7d4b1f669cfc3904e99d09560d40f89dc14b7e Mon Sep 17 00:00:00 2001 From: algizn97 Date: Wed, 20 Aug 2025 17:51:06 +0500 Subject: [PATCH 01/77] Fixed the letter --- app/telegram/Keyboards/inline_keyboards.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/telegram/Keyboards/inline_keyboards.py b/app/telegram/Keyboards/inline_keyboards.py index e9c034d..c979706 100644 --- a/app/telegram/Keyboards/inline_keyboards.py +++ b/app/telegram/Keyboards/inline_keyboards.py @@ -52,7 +52,7 @@ risk_management_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text='Изм. цены прибыли', callback_data='clb_change_price_profit'), InlineKeyboardButton(text='Изм. цены убытков', callback_data='clb_change_price_loss')], - [InlineKeyboardButton(text='Иакс. риск на сделку', callback_data='clb_change_max_risk_deal')], + [InlineKeyboardButton(text='Макс. риск на сделку', callback_data='clb_change_max_risk_deal')], back_btn_list_settings ]) -- 2.50.1 From de7b5ce5573bbcce97c2fb9c672940babbc1596b Mon Sep 17 00:00:00 2001 From: algizn97 Date: Thu, 21 Aug 2025 13:23:17 +0500 Subject: [PATCH 02/77] Update --- app/telegram/Keyboards/inline_keyboards.py | 42 +++++++++++++++++----- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/app/telegram/Keyboards/inline_keyboards.py b/app/telegram/Keyboards/inline_keyboards.py index c979706..ae11135 100644 --- a/app/telegram/Keyboards/inline_keyboards.py +++ b/app/telegram/Keyboards/inline_keyboards.py @@ -28,12 +28,29 @@ connect_bybit_api_markup = InlineKeyboardMarkup(inline_keyboard=[ ]) trading_markup = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text="Указать торговую пару", callback_data='clb_update_trading_pair')], - [InlineKeyboardButton(text="Совершить сделку", callback_data='clb_open_deal')] -]) + [InlineKeyboardButton(text="Указать торговую пару", callback_data='clb_update_trading_pair')], + [InlineKeyboardButton(text="Выбрать тип входа", callback_data='clb_update_entry_type')], + # [InlineKeyboardButton(text="Совершить сделку", callback_data='clb_open_deal')] +]) + +open_deal_markup = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="Открыть сделку", callback_data="clb_open_deal")], +]) + + + +entry_order_type_markup = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text="Market (текущая цена)", callback_data="entry_order_type:Market"), + InlineKeyboardButton(text="Limit (фиксированная цена)", callback_data="entry_order_type:Limit"), + ] + ] +) back_btn_list_settings = [InlineKeyboardButton(text="Назад", callback_data='clb_back_to_special_settings_message')] # Кнопка для возврата к списку каталога настроек back_btn_list_settings_markup = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Назад", callback_data='clb_back_to_special_settings_message')]]) # Клавиатура для возврата к списку каталога настроек +back_btn_to_main = [InlineKeyboardButton(text="На главную", callback_data='clb_back_to_main')] main_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_trading_mode'), @@ -45,7 +62,8 @@ main_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text='Коэффициент Мартингейла', callback_data='clb_change_martingale_factor'), InlineKeyboardButton(text='Максимльное кол-во ставок', callback_data='clb_change_maximum_quantity')], - back_btn_list_settings + back_btn_list_settings, + back_btn_to_main ]) risk_management_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ @@ -53,8 +71,10 @@ risk_management_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ InlineKeyboardButton(text='Изм. цены убытков', callback_data='clb_change_price_loss')], [InlineKeyboardButton(text='Макс. риск на сделку', callback_data='clb_change_max_risk_deal')], + [InlineKeyboardButton(text='Комиссия биржи', callback_data='commission_fee')], - back_btn_list_settings + back_btn_list_settings, + back_btn_to_main ]) condition_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ @@ -69,7 +89,8 @@ condition_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text='AI - аналитика', callback_data='clb_change_ai_analytics')], - back_btn_list_settings + back_btn_list_settings, + back_btn_to_main ]) additional_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ @@ -78,7 +99,8 @@ additional_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text='Уведомления', callback_data='clb_change_notifications')], - back_btn_list_settings + back_btn_list_settings, + back_btn_to_main ]) trading_mode_markup = InlineKeyboardMarkup(inline_keyboard=[ @@ -88,14 +110,16 @@ trading_mode_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="Свитч", callback_data="trade_mode_switch"), InlineKeyboardButton(text="Смарт", callback_data="trade_mode_smart")], - back_btn_list_settings + back_btn_list_settings, + back_btn_to_main ]) margin_type_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="Изолированный", callback_data="margin_type_isolated"), InlineKeyboardButton(text="Кросс", callback_data="margin_type_cross")], - back_btn_list_settings + back_btn_list_settings, + back_btn_to_main ]) trigger_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE -- 2.50.1 From a6ba9490610c3854f65e39042540726d8e06edd4 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Thu, 21 Aug 2025 13:34:56 +0500 Subject: [PATCH 03/77] Added buttons To the main page, Open a deal, My Deals and select the input type --- app/telegram/Keyboards/inline_keyboards.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/telegram/Keyboards/inline_keyboards.py b/app/telegram/Keyboards/inline_keyboards.py index ae11135..b913353 100644 --- a/app/telegram/Keyboards/inline_keyboards.py +++ b/app/telegram/Keyboards/inline_keyboards.py @@ -30,7 +30,7 @@ connect_bybit_api_markup = InlineKeyboardMarkup(inline_keyboard=[ trading_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="Указать торговую пару", callback_data='clb_update_trading_pair')], [InlineKeyboardButton(text="Выбрать тип входа", callback_data='clb_update_entry_type')], - # [InlineKeyboardButton(text="Совершить сделку", callback_data='clb_open_deal')] + [InlineKeyboardButton(text="Мои сделки", callback_data='clb_my_deals')], ]) open_deal_markup = InlineKeyboardMarkup(inline_keyboard=[ -- 2.50.1 From 2dc639d59a1e2fcc4278fe9afe091d2c130aff50 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Thu, 21 Aug 2025 13:35:42 +0500 Subject: [PATCH 04/77] Added market order and limit order --- app/services/Bybit/functions/Futures.py | 74 +++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/app/services/Bybit/functions/Futures.py b/app/services/Bybit/functions/Futures.py index 1f68dc0..f8ac8bb 100644 --- a/app/services/Bybit/functions/Futures.py +++ b/app/services/Bybit/functions/Futures.py @@ -76,7 +76,13 @@ async def contract_long(tg_id, message, margin_mode): starting_quantity = float(data_main_stgs['starting_quantity']) max_risk_percent = float(data_risk_management_stgs['max_risk_deal']) loss_profit = float(data_risk_management_stgs['price_loss']) - takeProfit= float(data_risk_management_stgs['price_profit']) + takeprofit= float(data_risk_management_stgs['price_profit']) + commission_fee = float(data_risk_management_stgs.get('commission_fee', 0)) + takeProfit_raw = takeprofit + takeProfit = takeProfit_raw - commission_fee # уменьшаем TP на комиссию + + if takeProfit < 0: + takeProfit = 0 # Инициализация переменных next_quantity = starting_quantity @@ -144,9 +150,11 @@ async def contract_long(tg_id, message, margin_mode): await info_access_open_deal(message, SYMBOL, data_main_stgs['trading_mode'], margin_mode, data_main_stgs['size_leverage'], next_quantity) except exceptions.InvalidRequestError as e: + logging.error(f"Неверно указана торговая пара: {e}") await message.answer('Недостаточно баланса') except Exception as e: - await message.answer('Непредвиденная оишбка') + logging.error(f"Ошибка при совершении сделки: {e}") + await message.answer('⚠️ Ошибка при совершении сделки') async def contract_short(tg_id, message, margin_mode): api_key = await rq.get_bybit_api_key(tg_id) @@ -183,7 +191,13 @@ async def contract_short(tg_id, message, margin_mode): starting_quantity = float(data_main_stgs['starting_quantity']) max_risk_percent = float(data_risk_management_stgs['max_risk_deal']) loss_profit = float(data_risk_management_stgs['price_loss']) - takeProfit = float(data_risk_management_stgs['price_profit']) + takeprofit = float(data_risk_management_stgs['price_profit']) + commission_fee = float(data_risk_management_stgs.get('commission_fee', 0)) + takeProfit_raw = takeprofit + takeProfit = takeProfit_raw - commission_fee # уменьшаем TP на комиссию + + if takeProfit < 0: + takeProfit = 0 # Инициализация переменных next_quantity = starting_quantity @@ -249,6 +263,58 @@ async def contract_short(tg_id, message, margin_mode): await info_access_open_deal(message, SYMBOL, data_main_stgs['trading_mode'], margin_mode, data_main_stgs['size_leverage'], next_quantity) except exceptions.InvalidRequestError as e: + logging.error(f"Error in open_deal: {e}") await message.answer('Недостаточно баланса') except Exception as e: - await message.answer('Непредвиденная оишбка') \ No newline at end of file + logging.error(f"Error in open_deal: {e}") + await message.answer('⚠️ Ошибка при совершении сделки') + + + +async def open_market_order(tg_id, message, api_key, secret_key): + data_main_stgs = await rq.get_user_main_settings(tg_id) + trading_mode = data_main_stgs['trading_mode'] + margin_mode = data_main_stgs.get('margin_type') + + if trading_mode == 'Long': + await contract_long(tg_id, message, margin_mode) + elif trading_mode == 'Short': + await contract_short(tg_id, message, margin_mode) + else: + await message.answer("Неизвестный режим торговли: выберите Long или Short.") + + +async def open_limit_order(tg_id, message, price, api_key, secret_key): + data_main_stgs = await rq.get_user_main_settings(tg_id) + trading_mode = data_main_stgs['trading_mode'] + margin_mode = data_main_stgs.get('margin_type') + + + client = HTTP( + api_key=api_key, + api_secret=secret_key + ) + + symbol = await rq.get_symbol(tg_id) + qty = float(data_main_stgs['starting_quantity']) + side = 'Buy' if trading_mode == 'Long' else 'Sell' + + + try: + response = client.place_order( + category='linear', + symbol=symbol, + side=side, + orderType='Limit', + qty=qty, + price=price, + timeInForce='GTC', + orderLinkId=f"order_{int(time.time())}" + ) + if response.get('retCode') == 0: + await message.answer(f"Limit ордер открыт: {side} {qty} {symbol} по цене {price}") + else: + await message.answer(f"Ошибка открытия ордера: {response.get('retMsg')}") + except Exception as e: + logging.error(f"Ошибка при открытии лимитного ордера: {e}") + await message.answer("Ошибка при открытии ордера") \ No newline at end of file -- 2.50.1 From 4ebe7399ba73e7df52533abe76d85d7d23d8b847 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Thu, 21 Aug 2025 13:36:11 +0500 Subject: [PATCH 05/77] Update function --- app/services/Bybit/functions/balance.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/app/services/Bybit/functions/balance.py b/app/services/Bybit/functions/balance.py index db3d74d..5785325 100644 --- a/app/services/Bybit/functions/balance.py +++ b/app/services/Bybit/functions/balance.py @@ -2,7 +2,8 @@ from pybit.unified_trading import HTTP -client = HTTP() +import logging +logging.basicConfig(level=logging.DEBUG) async def get_balance(tg_id, message): api_key = await rq.get_bybit_api_key(tg_id) @@ -15,19 +16,17 @@ async def get_balance(tg_id, message): if api_key == 'None' or secret_key == 'None': await message.answer('⚠️ Подключите платформу для торговли') - return 0 + return 0 try: - check_user = client.get_wallet_balance() - - if check_user: - try: - balance = client.get_wallet_balance(accountType='UNIFIED', coin='USDT')['result']['list'][0]['coin'][0]['walletBalance'] - - return balance - except Exception as e: - await message.answer('⚠️ Ошибка при получении баланса пользователя') - return 0 + response = client.get_wallet_balance(accountType='UNIFIED') + if response['retCode'] == 0: + total_balance = response['result']['list'][0].get('totalWalletBalance', '0') + return total_balance + else: + await message.answer(f"⚠️ Ошибка API: {response.get('retMsg')}") + return 0 except Exception as e: - await message.answer('⚠️ Неверные данные API, перепроверьте их') + logging.error(f"Ошибка при получении общего баланса: {e}") + await message.answer('⚠️ Ошибка при получении баланса') return 0 \ No newline at end of file -- 2.50.1 From fe1c0b16ce252814d7f1e5e6aeb187efc0d26b24 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Thu, 21 Aug 2025 13:37:58 +0500 Subject: [PATCH 06/77] Added reply keyboard (base_buttons_markup) --- app/services/Bybit/functions/Add_Bybit_API.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/services/Bybit/functions/Add_Bybit_API.py b/app/services/Bybit/functions/Add_Bybit_API.py index 291da46..21761b0 100644 --- a/app/services/Bybit/functions/Add_Bybit_API.py +++ b/app/services/Bybit/functions/Add_Bybit_API.py @@ -1,6 +1,7 @@ from aiogram import F, Router import app.telegram.Keyboards.inline_keyboards as inline_markup +import app.telegram.Keyboards.reply_keyboards as reply_markup import app.telegram.database.requests as rq from aiogram.types import Message, CallbackQuery @@ -68,6 +69,6 @@ async def add_secret_key(message: Message, state: FSMContext): await state.clear() - await message.answer('Данные добавлены, нажмите на профиль и начните торговлю!') + await message.answer('Данные добавлены, нажмите на профиль и начните торговлю!', reply_markup=reply_markup.base_buttons_markup) \ No newline at end of file -- 2.50.1 From 7f53caaac6bf3487232cdcc2587190e2d1b6ff44 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Thu, 21 Aug 2025 13:38:50 +0500 Subject: [PATCH 07/77] Added commission_fee and entry_order_type --- app/telegram/database/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/telegram/database/models.py b/app/telegram/database/models.py index 69da607..7f475d8 100644 --- a/app/telegram/database/models.py +++ b/app/telegram/database/models.py @@ -74,6 +74,8 @@ class User_Main_Settings(Base): starting_quantity = mapped_column(Integer(), default=1) martingale_factor = mapped_column(Integer(), default=1) maximal_quantity = mapped_column(Integer(), default=10) + entry_order_type = mapped_column(String(10), default='Market') + class User_Risk_Management_Settings(Base): __tablename__ = 'user_risk_management_settings' @@ -85,6 +87,7 @@ class User_Risk_Management_Settings(Base): price_profit = mapped_column(Integer(), default=1) price_loss = mapped_column(Integer(), default=1) max_risk_deal = mapped_column(Integer(), default=100) + commission_fee = mapped_column(Integer(), default=0) class User_Condition_Settings(Base): __tablename__ = 'user_condition_settings' -- 2.50.1 From c8b0dad7c2316b541693dd0a6812bffd36fd123b Mon Sep 17 00:00:00 2001 From: algizn97 Date: Thu, 21 Aug 2025 13:39:43 +0500 Subject: [PATCH 08/77] Added commission_fee and order_type functions --- app/telegram/database/requests.py | 32 ++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/app/telegram/database/requests.py b/app/telegram/database/requests.py index 4b67b5e..9708a00 100644 --- a/app/telegram/database/requests.py +++ b/app/telegram/database/requests.py @@ -1,4 +1,6 @@ import logging +from typing import Any + logger = logging.getLogger(__name__) from app.telegram.database.models import async_session @@ -181,11 +183,13 @@ async def get_user_risk_management_settings(tg_id): price_profit = await session.scalar(select(URMS.price_profit).where(URMS.tg_id == tg_id)) price_loss = await session.scalar(select(URMS.price_loss).where(URMS.tg_id == tg_id)) max_risk_deal = await session.scalar(select(URMS.max_risk_deal).where(URMS.tg_id == tg_id)) + commission_fee = await session.scalar(select(URMS.commission_fee).where(URMS.tg_id == tg_id)) data = { 'price_profit': price_profit, 'price_loss': price_loss, - 'max_risk_deal': max_risk_deal + 'max_risk_deal': max_risk_deal, + 'commission_fee': commission_fee, } return data @@ -272,4 +276,30 @@ async def update_max_risk_deal(tg_id, num): async with async_session() as session: await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(max_risk_deal = num)) + await session.commit() + + +async def update_entry_order_type(tg_id, order_type): + async with async_session() as session: + await session.execute( + update(UMS) + .where(UMS.tg_id == tg_id) + .values(entry_order_type=order_type) + ) + await session.commit() + + +async def get_entry_order_type(tg_id: object) -> str | None | Any: + async with async_session() as session: + order_type = await session.scalar( + select(UMS.entry_order_type).where(UMS.tg_id == tg_id) + ) + # Если в базе не установлен тип — возвращаем значение по умолчанию + return order_type or 'Market' + + +async def update_commission_fee(tg_id, num): + async with async_session() as session: + await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(commission_fee = num)) + await session.commit() \ No newline at end of file -- 2.50.1 From b46d8d7af9fa734d807ad67b533ccf386f2e38fd Mon Sep 17 00:00:00 2001 From: algizn97 Date: Thu, 21 Aug 2025 13:41:58 +0500 Subject: [PATCH 09/77] Update handlers.py --- app/telegram/handlers/handlers.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/telegram/handlers/handlers.py b/app/telegram/handlers/handlers.py index 8ea7fa3..48b2a9b 100644 --- a/app/telegram/handlers/handlers.py +++ b/app/telegram/handlers/handlers.py @@ -134,7 +134,8 @@ async def clb_main_settings_msg(callback: CallbackQuery, state: FSMContext): list_risk_management_settings = ['clb_change_price_profit', 'clb_change_price_loss', - 'clb_change_max_risk_deal', + 'clb_change_max_risk_deal', + 'commission_fee', ] @router.callback_query(F.data.in_(list_risk_management_settings)) async def clb_risk_management_settings_msg(callback: CallbackQuery, state: FSMContext): @@ -148,6 +149,8 @@ async def clb_risk_management_settings_msg(callback: CallbackQuery, state: FSMCo await func_rmanagement_settings.price_loss_message(callback.message, state) case 'clb_change_max_risk_deal': await func_rmanagement_settings.max_risk_deal_message(callback.message, state) + case 'commission_fee': + await func_rmanagement_settings.commission_fee_message(callback.message, state) except Exception as e: logging.error(f"Error callback in risk_management match-case: {e}") @@ -201,4 +204,5 @@ async def clb_additional_settings_msg(callback: CallbackQuery, state: FSMContext case 'clb_change_notifications': await func_additional_settings.notifications_message(callback.message, state) except Exception as e: - logging.error(f"Error callback in additional_settings match-case: {e}") \ No newline at end of file + logging.error(f"Error callback in additional_settings match-case: {e}") + -- 2.50.1 From 99b51d4cc04594aa217b4acf53e65f304d37e651 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Thu, 21 Aug 2025 13:46:12 +0500 Subject: [PATCH 10/77] Added limit and market orders functions --- app/services/Bybit/functions/functions.py | 117 ++++++++++++++++++---- 1 file changed, 97 insertions(+), 20 deletions(-) diff --git a/app/services/Bybit/functions/functions.py b/app/services/Bybit/functions/functions.py index 86ff692..a2ca431 100644 --- a/app/services/Bybit/functions/functions.py +++ b/app/services/Bybit/functions/functions.py @@ -1,6 +1,7 @@ from aiogram import F, Router from app.services.Bybit.functions import Futures, func_min_qty +from app.services.Bybit.functions.Futures import open_market_order, open_limit_order from app.services.Bybit.functions.balance import get_balance import app.telegram.Keyboards.inline_keyboards as inline_markup @@ -15,8 +16,11 @@ router_functions_bybit_trade = Router() class state_update_symbol(StatesGroup): symbol = State() - -@router_functions_bybit_trade.callback_query(F.data == 'clb_start_trading') + +class state_update_entry_type(StatesGroup): + entry_type = State() + +@router_functions_bybit_trade.callback_query(F.data.in_(['clb_start_trading', 'clb_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) @@ -76,24 +80,97 @@ async def update_symbol_for_trade(message: Message, state: FSMContext): await state.clear() -@router_functions_bybit_trade.callback_query(F.data == 'clb_open_deal') -async def make_deal_bybit (callback: CallbackQuery): - data_main_stgs = await rq.get_user_main_settings(callback.from_user.id) - trade_mode = data_main_stgs['trading_mode'] - qty = data_main_stgs['starting_quantity'] - margin_mode = data_main_stgs['margin_type'] - qty_min = await func_min_qty.get_min_qty(callback.from_user.id, callback.message) +@router_functions_bybit_trade.callback_query(F.data == 'clb_update_entry_type') +async def update_entry_type_message(callback: CallbackQuery, state: FSMContext): + await state.set_state(state_update_entry_type.entry_type) + await callback.message.answer("Выберите тип входа в позицию:", reply_markup=inline_markup.entry_order_type_markup) + await callback.answer() - if qty < qty_min: - await callback.message.edit_text(f"Количество вашей ставки ({qty}) меньше минимального количества ({qty_min}) для данной торговой пары") + +@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): + order_type = callback.data.split(':')[1] + + if order_type not in ['Market', 'Limit']: + await callback.answer("Ошибка выбора", show_alert=True) + return + + await state.update_data(entry_order_type=order_type) + await rq.update_entry_order_type(callback.from_user.id, order_type) + + if order_type == 'Limit': + await callback.answer("Вы выбрали Limit. Введите цену для лимитного ордера.") + await callback.message.answer("Введите цену лимитного ордера:") + await state.update_data(awaiting_limit_price=True) else: - match trade_mode: - case 'Long': - await Futures.contract_long(callback.from_user.id, callback.message, margin_mode) - case 'Short': - await Futures.contract_short(callback.from_user.id, callback.message, margin_mode) - case 'Switch': - await callback.message.edit_text('Режим Switch пока недоступен') - case 'Smart': - await callback.message.edit_text('Режим Smart пока недоступен') \ No newline at end of file + await callback.answer("Вы выбрали Market. Нажмите кнопку ниже, чтобы открыть сделку.") + await callback.message.answer("Нажмите кнопку, чтобы открыть сделку.", + reply_markup=inline_markup.open_deal_markup) + await callback.message.delete() + + +@router_functions_bybit_trade.message() +async def process_limit_price_message(message: Message, state: FSMContext): + data = await state.get_data() + if not data.get('awaiting_limit_price'): + return + + try: + price = float(message.text) + if price <= 0: + raise ValueError() + except ValueError: + await message.answer("Ошибка: введите корректное положительное число для цены.") + return + + await state.update_data(limit_price=price, awaiting_limit_price=False) + + await message.answer(f"Цена лимитного ордера установлена: {price}. Нажмите кнопку ниже, чтобы открыть сделку.", + reply_markup=inline_markup.open_deal_markup) + + +@router_functions_bybit_trade.callback_query(F.data == "clb_open_deal") +async def open_deal(callback: CallbackQuery, state: FSMContext): + data = await state.get_data() + order_type = await rq.get_entry_order_type(callback.from_user.id) + api = await rq.get_bybit_api_key(callback.from_user.id) + secret = await rq.get_bybit_secret_key(callback.from_user.id) + + if order_type == 'Market': + await open_market_order(callback.from_user.id, callback.message, api_key=api, secret_key=secret) + elif order_type == 'Limit': + price = data.get('limit_price') + if not price: + await callback.answer("Цена для лимитного ордера не задана. Введите сначала цену.") + return + + await open_limit_order(callback.from_user.id, callback.message, price, api_key=api, secret_key=secret) + else: + await callback.answer("Неизвестный тип ордера.") + + await callback.message.edit_reply_markup() + await state.clear() + + +# @router_functions_bybit_trade.callback_query(F.data == 'clb_open_deal') +# async def make_deal_bybit (callback: CallbackQuery): +# data_main_stgs = await rq.get_user_main_settings(callback.from_user.id) +# +# trade_mode = data_main_stgs['trading_mode'] +# qty = data_main_stgs['starting_quantity'] +# margin_mode = data_main_stgs['margin_type'] +# qty_min = await func_min_qty.get_min_qty(callback.from_user.id, callback.message) +# +# if qty < qty_min: +# await callback.message.edit_text(f"Количество вашей ставки ({qty}) меньше минимального количества ({qty_min}) для данной торговой пары") +# else: +# match trade_mode: +# case 'Long': +# await Futures.contract_long(callback.from_user.id, callback.message, margin_mode) +# case 'Short': +# await Futures.contract_short(callback.from_user.id, callback.message, margin_mode) +# case 'Switch': +# await callback.message.edit_text('Режим Switch пока недоступен') +# case 'Smart': +# await callback.message.edit_text('Режим Smart пока недоступен') \ No newline at end of file -- 2.50.1 From c1a9f16faa71fcdbe0e937301397793153f43672 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Thu, 21 Aug 2025 13:47:19 +0500 Subject: [PATCH 11/77] Added commission_fee --- .../risk_management_settings/settings.py | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/app/telegram/functions/risk_management_settings/settings.py b/app/telegram/functions/risk_management_settings/settings.py index 859451e..86dcc82 100644 --- a/app/telegram/functions/risk_management_settings/settings.py +++ b/app/telegram/functions/risk_management_settings/settings.py @@ -14,6 +14,7 @@ class update_risk_management_settings(StatesGroup): price_profit = State() price_loss = State() max_risk_deal = State() + commission_fee = State() async def reg_new_user_default_risk_management_settings(id, message): tg_id = id @@ -25,10 +26,11 @@ async def main_settings_message(id, message, state): text = f"""Риск менеджмент, -- Процент изменения цены для фиксации прибыли: {data['price_profit']}% -- Процент изменения цены для фиксации убытков: {data['price_loss']}% -- Максимальный риск на сделку (в % от баланса): {data['max_risk_deal']}% -""" + - Процент изменения цены для фиксации прибыли: {data.get('price_profit', 0)}% + - Процент изменения цены для фиксации убытков: {data.get('price_loss', 0)}% + - Максимальный риск на сделку (в % от баланса): {data.get('max_risk_deal', 0)}% + - Комиссия биржи для расчета процента фиксации прибыли: {data.get('commission_fee', 0)}% + """ await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.risk_management_settings_markup) async def price_profit_message(message, state): @@ -107,4 +109,28 @@ async def state_max_risk_deal(message: Message, state): else: await message.answer(f'⛔️ Ошибка: ваше значение ({data['max_risk_deal']}%) или выше лимита (100) или вы вводите неверные символы') - await main_settings_message(message.from_user.id, message, state) \ No newline at end of file + await main_settings_message(message.from_user.id, message, state) + + +async def commission_fee_message(message, state): + await state.set_state(update_risk_management_settings.commission_fee) + await message.answer(text="Введите процент комиссии биржи (например, 0.1):", parse_mode='html', reply_markup=None) + +@router_risk_management_settings.message(update_risk_management_settings.commission_fee) +async def state_commission_fee(message: Message, state): + await state.update_data(commission_fee=message.text) + data = await state.get_data() + data_settings = await rq.get_user_risk_management_settings(message.from_user.id) + + try: + val = float(data['commission_fee']) + if val < 0 or val > 100: + raise ValueError() + except Exception: + await message.answer("⛔️ Ошибка: введите корректный процент комиссии от 0 до 100") + return await commission_fee_message(message, state) + + await rq.update_commission_fee(message.from_user.id, val) + await message.answer(f"✅ Изменено: {data_settings['commission_fee']}% → {data['commission_fee']}%") + await main_settings_message(message.from_user.id, message, state) + await state.clear() \ No newline at end of file -- 2.50.1 From 8293c4486464c4712fa39de13b003d6335cae521 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Thu, 21 Aug 2025 14:22:34 +0500 Subject: [PATCH 12/77] Added USER DEALS --- app/telegram/database/models.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/telegram/database/models.py b/app/telegram/database/models.py index 7f475d8..9b444d9 100644 --- a/app/telegram/database/models.py +++ b/app/telegram/database/models.py @@ -139,4 +139,16 @@ async def async_main(): result = await conn.execute(select(Trigger).where(Trigger.trigger == trigger)) if not result.first(): logger.info("Заполение таблицы триггеров") - await conn.execute(Trigger.__table__.insert().values(trigger=trigger)) \ No newline at end of file + await conn.execute(Trigger.__table__.insert().values(trigger=trigger)) + + +class USER_DEALS(Base): + __tablename__ = 'user_deals' + + id: Mapped[int] = mapped_column(primary_key=True) + + tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) + + symbol = mapped_column(String(18), default='PENGUUSDT') + open_price = mapped_column(Integer(), default=1) + side = mapped_column(String(10), nullable=False) \ No newline at end of file -- 2.50.1 From d0577f163b6f1d558dfc8ee81f3949891b039805 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Thu, 21 Aug 2025 14:22:54 +0500 Subject: [PATCH 13/77] Added get user trades --- app/telegram/database/requests.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/telegram/database/requests.py b/app/telegram/database/requests.py index 9708a00..917d983 100644 --- a/app/telegram/database/requests.py +++ b/app/telegram/database/requests.py @@ -14,6 +14,7 @@ from app.telegram.database.models import User_Additional_Settings as UAS from app.telegram.database.models import Trading_Mode from app.telegram.database.models import Margin_type from app.telegram.database.models import Trigger +from app.telegram.database.models import USER_DEALS import app.telegram.functions.functions as func # functions @@ -133,6 +134,13 @@ async def get_symbol(tg_id): symbol = await session.scalar(select(User_Symbol.symbol).where(User_Symbol.tg_id == tg_id)) return symbol +async def get_user_trades(tg_id): + async with async_session() as session: + query = select(USER_DEALS.symbol, USER_DEALS.side, USER_DEALS.open_price).where(USER_DEALS.tg_id == tg_id) + result = await session.execute(query) + trades = result.all() + return trades + async def get_for_registration_trading_mode(): async with async_session() as session: mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.id == 1)) @@ -302,4 +310,6 @@ async def update_commission_fee(tg_id, num): async with async_session() as session: await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(commission_fee = num)) - await session.commit() \ No newline at end of file + await session.commit() + + -- 2.50.1 From f8cedf4cb4a66a7f7d12286fd3939207bfcb574a Mon Sep 17 00:00:00 2001 From: algizn97 Date: Thu, 21 Aug 2025 16:09:32 +0500 Subject: [PATCH 14/77] Fixed --- app/telegram/Keyboards/inline_keyboards.py | 2 +- app/telegram/database/models.py | 1 - app/telegram/database/requests.py | 9 ++++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/telegram/Keyboards/inline_keyboards.py b/app/telegram/Keyboards/inline_keyboards.py index b913353..8dff646 100644 --- a/app/telegram/Keyboards/inline_keyboards.py +++ b/app/telegram/Keyboards/inline_keyboards.py @@ -60,7 +60,7 @@ main_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ InlineKeyboardButton(text='Начальная ставка', callback_data='clb_change_starting_quantity')], [InlineKeyboardButton(text='Коэффициент Мартингейла', callback_data='clb_change_martingale_factor'), - InlineKeyboardButton(text='Максимльное кол-во ставок', callback_data='clb_change_maximum_quantity')], + InlineKeyboardButton(text='Максимальное кол-во ставок', callback_data='clb_change_maximum_quantity')], back_btn_list_settings, back_btn_to_main diff --git a/app/telegram/database/models.py b/app/telegram/database/models.py index 9b444d9..a690214 100644 --- a/app/telegram/database/models.py +++ b/app/telegram/database/models.py @@ -150,5 +150,4 @@ class USER_DEALS(Base): tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) symbol = mapped_column(String(18), default='PENGUUSDT') - open_price = mapped_column(Integer(), default=1) side = mapped_column(String(10), nullable=False) \ No newline at end of file diff --git a/app/telegram/database/requests.py b/app/telegram/database/requests.py index 917d983..59f58de 100644 --- a/app/telegram/database/requests.py +++ b/app/telegram/database/requests.py @@ -136,11 +136,18 @@ async def get_symbol(tg_id): async def get_user_trades(tg_id): async with async_session() as session: - query = select(USER_DEALS.symbol, USER_DEALS.side, USER_DEALS.open_price).where(USER_DEALS.tg_id == tg_id) + query = select(USER_DEALS.symbol, USER_DEALS.side).where(USER_DEALS.tg_id == tg_id) result = await session.execute(query) trades = result.all() return trades + +async def update_user_trades(tg_id, symbol, side): + async with async_session() as session: + await session.execute(update(USER_DEALS).where(USER_DEALS.tg_id == tg_id).values(symbol = symbol, side = side)) + + await session.commit() + async def get_for_registration_trading_mode(): async with async_session() as session: mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.id == 1)) -- 2.50.1 From eaf84588351e6116743aff73d33e02cacae9435a Mon Sep 17 00:00:00 2001 From: algizn97 Date: Thu, 21 Aug 2025 16:10:01 +0500 Subject: [PATCH 15/77] Update --- app/services/Bybit/functions/Futures.py | 11 ++++-- app/services/Bybit/functions/functions.py | 39 ++++++++++++++++++- .../functions/main_settings/settings.py | 4 +- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/app/services/Bybit/functions/Futures.py b/app/services/Bybit/functions/Futures.py index f8ac8bb..afd28a0 100644 --- a/app/services/Bybit/functions/Futures.py +++ b/app/services/Bybit/functions/Futures.py @@ -280,14 +280,17 @@ async def open_market_order(tg_id, message, api_key, secret_key): await contract_long(tg_id, message, margin_mode) elif trading_mode == 'Short': await contract_short(tg_id, message, margin_mode) - else: - await message.answer("Неизвестный режим торговли: выберите Long или Short.") + elif trading_mode == 'Smart': + await message.answer("Режим Smart пока недоступен") + elif trading_mode == 'Switch': + await message.answer("Режим Switch пока недоступен") async def open_limit_order(tg_id, message, price, api_key, secret_key): data_main_stgs = await rq.get_user_main_settings(tg_id) trading_mode = data_main_stgs['trading_mode'] margin_mode = data_main_stgs.get('margin_type') + order_type = await rq.get_entry_order_type(tg_id) client = HTTP( @@ -297,7 +300,7 @@ async def open_limit_order(tg_id, message, price, api_key, secret_key): symbol = await rq.get_symbol(tg_id) qty = float(data_main_stgs['starting_quantity']) - side = 'Buy' if trading_mode == 'Long' else 'Sell' + side = 'Buy' if trading_mode == 'Long' else 'Short' try: @@ -313,6 +316,8 @@ async def open_limit_order(tg_id, message, price, api_key, secret_key): ) if response.get('retCode') == 0: await message.answer(f"Limit ордер открыт: {side} {qty} {symbol} по цене {price}") + await rq.update_user_trades(tg_id, symbol=symbol, side=order_type) + else: await message.answer(f"Ошибка открытия ордера: {response.get('retMsg')}") except Exception as e: diff --git a/app/services/Bybit/functions/functions.py b/app/services/Bybit/functions/functions.py index a2ca431..ac8b186 100644 --- a/app/services/Bybit/functions/functions.py +++ b/app/services/Bybit/functions/functions.py @@ -132,19 +132,27 @@ async def process_limit_price_message(message: Message, state: FSMContext): @router_functions_bybit_trade.callback_query(F.data == "clb_open_deal") async def open_deal(callback: CallbackQuery, state: FSMContext): + data_main_stgs = await rq.get_user_main_settings(callback.from_user.id) data = await state.get_data() order_type = await rq.get_entry_order_type(callback.from_user.id) api = await rq.get_bybit_api_key(callback.from_user.id) secret = await rq.get_bybit_secret_key(callback.from_user.id) + qty = data_main_stgs['starting_quantity'] + qty_min = await func_min_qty.get_min_qty(callback.from_user.id, callback.message) + + if qty < qty_min: + await callback.message.edit_text(f"Количество вашей ставки ({qty}) меньше минимального количества ({qty_min}) для данной торговой пары") + await callback.answer() + return if order_type == 'Market': await open_market_order(callback.from_user.id, callback.message, api_key=api, secret_key=secret) + await rq.update_user_trades(callback.from_user.id, symbol=data.get('symbol'), side=order_type) elif order_type == 'Limit': price = data.get('limit_price') if not price: await callback.answer("Цена для лимитного ордера не задана. Введите сначала цену.") return - await open_limit_order(callback.from_user.id, callback.message, price, api_key=api, secret_key=secret) else: await callback.answer("Неизвестный тип ордера.") @@ -153,6 +161,33 @@ async def open_deal(callback: CallbackQuery, 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 + trades = await rq.get_user_trades(tg_id) + + if not trades: + await callback.message.answer("У вас ещё нет сделок.") + await callback.answer() + return + + grouped = {} + for trade in trades: + symbol = trade['symbol'] if isinstance(trade, dict) else trade.symbol + grouped.setdefault(symbol, []).append(trade) + + text_response = "Ваши сделки по валютным парам:\n\n" + for symbol, trade_list in grouped.items(): + text_response += f"{symbol}\n" + for t in trade_list: + side = t['side'] if isinstance(t, dict) else t.side + text_response += f" - {side}\n" + text_response += "\n" + + await callback.message.answer(text_response, parse_mode='html') + await callback.answer() + + # @router_functions_bybit_trade.callback_query(F.data == 'clb_open_deal') # async def make_deal_bybit (callback: CallbackQuery): # data_main_stgs = await rq.get_user_main_settings(callback.from_user.id) @@ -160,7 +195,7 @@ async def open_deal(callback: CallbackQuery, state: FSMContext): # trade_mode = data_main_stgs['trading_mode'] # qty = data_main_stgs['starting_quantity'] # margin_mode = data_main_stgs['margin_type'] -# qty_min = await func_min_qty.get_min_qty(callback.from_user.id, callback.message) + qty_min = await func_min_qty.get_min_qty(callback.from_user.id, callback.message) # # if qty < qty_min: # await callback.message.edit_text(f"Количество вашей ставки ({qty}) меньше минимального количества ({qty_min}) для данной торговой пары") diff --git a/app/telegram/functions/main_settings/settings.py b/app/telegram/functions/main_settings/settings.py index 632c234..8ab3c03 100644 --- a/app/telegram/functions/main_settings/settings.py +++ b/app/telegram/functions/main_settings/settings.py @@ -38,7 +38,7 @@ async def main_settings_message(id, message, state): - Размер кредитного плеча: х{data['size_leverage']} - Начальная ставка: {data['starting_quantity']} - Коэффициент мартингейла: {data['martingale_factor']} -- Максимальное количесиво ставок в серии: {data['maximal_quantity']} +- Максимальное количество ставок в серии: {data['maximal_quantity']} """, parse_mode='html', reply_markup=inline_markup.main_settings_markup) async def trading_mode_message(message, state): @@ -189,7 +189,7 @@ async def state_margin_type(callback: CallbackQuery, state): async def starting_quantity_message (message, state): await state.set_state(update_main_settings.starting_quantity) - await message.edit_text("Введите началаьную ставку:", parse_mode='html', reply_markup=inline_markup.back_btn_list_settings_markup) + await message.edit_text("Введите начальную ставку:", parse_mode='html', reply_markup=inline_markup.back_btn_list_settings_markup) @router_main_settings.message(update_main_settings.starting_quantity) async def state_starting_quantity(message: Message, state): -- 2.50.1 From 1a20c1a9d25829fcfce80a63fb7ee10374b84196 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Fri, 22 Aug 2025 13:52:20 +0500 Subject: [PATCH 16/77] Added keyboard --- app/services/Bybit/functions/balance.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/services/Bybit/functions/balance.py b/app/services/Bybit/functions/balance.py index 5785325..89daef4 100644 --- a/app/services/Bybit/functions/balance.py +++ b/app/services/Bybit/functions/balance.py @@ -1,4 +1,5 @@ import app.telegram.database.requests as rq +import app.telegram.Keyboards.inline_keyboards as inline_markup from pybit.unified_trading import HTTP @@ -15,7 +16,7 @@ async def get_balance(tg_id, message): ) if api_key == 'None' or secret_key == 'None': - await message.answer('⚠️ Подключите платформу для торговли') + await message.answer('⚠️ Подключите платформу для торговли', reply_markup=inline_markup.connect_bybit_api_markup) return 0 try: -- 2.50.1 From 4ca6e1fb2ca72aa9c9a5ad77fd027b8c8bcb57d2 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Fri, 22 Aug 2025 13:52:31 +0500 Subject: [PATCH 17/77] Added dp --- BibytBot_API.py => BybitBot_API.py | 3 +++ 1 file changed, 3 insertions(+) rename BibytBot_API.py => BybitBot_API.py (89%) diff --git a/BibytBot_API.py b/BybitBot_API.py similarity index 89% rename from BibytBot_API.py rename to BybitBot_API.py index e8a3b81..6e3c9d7 100644 --- a/BibytBot_API.py +++ b/BybitBot_API.py @@ -9,9 +9,11 @@ from app.telegram.database.models import async_main from app.telegram.handlers.handlers import router from app.telegram.functions.main_settings.settings import router_main_settings from app.telegram.functions.risk_management_settings.settings import router_risk_management_settings +from app.telegram.functions.condition_settings.settings import condition_settings_router from app.services.Bybit.functions.Add_Bybit_API import router_register_bybit_api from app.services.Bybit.functions.functions import router_functions_bybit_trade + from config import TOKEN_TG_BOT from app.telegram.logs import logger @@ -25,6 +27,7 @@ async def main(): dp.include_router(router) dp.include_router(router_main_settings) dp.include_router(router_risk_management_settings) + dp.include_router(condition_settings_router) dp.include_router(router_register_bybit_api) dp.include_router(router_functions_bybit_trade) -- 2.50.1 From c9c6a5b7f0d845809e8d355da463486f375ce987 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Fri, 22 Aug 2025 13:52:50 +0500 Subject: [PATCH 18/77] Fixed filename --- BibytBot_API.pyproj => BybitBot_API.pyproj | 0 BibytBot_API.sln => BybitBot_API.sln | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename BibytBot_API.pyproj => BybitBot_API.pyproj (100%) rename BibytBot_API.sln => BybitBot_API.sln (100%) diff --git a/BibytBot_API.pyproj b/BybitBot_API.pyproj similarity index 100% rename from BibytBot_API.pyproj rename to BybitBot_API.pyproj diff --git a/BibytBot_API.sln b/BybitBot_API.sln similarity index 100% rename from BibytBot_API.sln rename to BybitBot_API.sln -- 2.50.1 From 0c4204fb6ef8b48da8c08dae29c28376501e791a Mon Sep 17 00:00:00 2001 From: algizn97 Date: Fri, 22 Aug 2025 13:53:17 +0500 Subject: [PATCH 19/77] Added UserTimer --- app/telegram/database/models.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/telegram/database/models.py b/app/telegram/database/models.py index a690214..ad6afec 100644 --- a/app/telegram/database/models.py +++ b/app/telegram/database/models.py @@ -1,4 +1,8 @@ import logging +from datetime import datetime + +from sqlalchemy.sql.sqltypes import DateTime + logger = logging.getLogger(__name__) from sqlalchemy import BigInteger, Boolean, Integer, String, ForeignKey @@ -150,4 +154,16 @@ class USER_DEALS(Base): tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) symbol = mapped_column(String(18), default='PENGUUSDT') - side = mapped_column(String(10), nullable=False) \ No newline at end of file + side = mapped_column(String(10), nullable=False) + open_price = mapped_column(Integer(), nullable=False) + positive_percent = mapped_column(Integer(), nullable=False) + + +class UserTimer(Base): + __tablename__ = 'user_timers' + + id: Mapped[int] = mapped_column(primary_key=True) + tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) + timer_minutes = mapped_column(Integer, nullable=False, default=0) + timer_start = mapped_column(DateTime, default=datetime.utcnow) + timer_end = mapped_column(DateTime, nullable=True) \ No newline at end of file -- 2.50.1 From 8ab308d4b9f1921bb9552f78dba390944541a3b9 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Fri, 22 Aug 2025 13:53:48 +0500 Subject: [PATCH 20/77] Added keyboards --- app/telegram/Keyboards/inline_keyboards.py | 97 ++++++++++++++-------- 1 file changed, 63 insertions(+), 34 deletions(-) diff --git a/app/telegram/Keyboards/inline_keyboards.py b/app/telegram/Keyboards/inline_keyboards.py index 8dff646..f4149fb 100644 --- a/app/telegram/Keyboards/inline_keyboards.py +++ b/app/telegram/Keyboards/inline_keyboards.py @@ -1,7 +1,7 @@ from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup -start_markup = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text="🔥 Начать торговлю", callback_data="clb_start_chatbot_message")] +start_markup = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="🔥 Начать торговлю", callback_data="clb_start_chatbot_message")] ]) settings_markup = InlineKeyboardMarkup(inline_keyboard=[ @@ -12,15 +12,15 @@ settings_markup = InlineKeyboardMarkup(inline_keyboard=[ back_btn_profile = [InlineKeyboardButton(text="Назад", callback_data='clb_start_chatbot_message')] special_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text="Основные настройки", callback_data='clb_change_main_settings'), + [InlineKeyboardButton(text="Основные настройки", callback_data='clb_change_main_settings'), InlineKeyboardButton(text="Риск-менеджмент", callback_data='clb_change_risk_management_settings')], - [InlineKeyboardButton(text="Условия запуска", callback_data='clb_change_condition_settings'), + [InlineKeyboardButton(text="Условия запуска", callback_data='clb_change_condition_settings'), InlineKeyboardButton(text="Дополнительные параметры", callback_data='clb_change_additional_settings')], [InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api_message')], - back_btn_profile + back_btn_profile ]) connect_bybit_api_markup = InlineKeyboardMarkup(inline_keyboard=[ @@ -28,17 +28,13 @@ connect_bybit_api_markup = InlineKeyboardMarkup(inline_keyboard=[ ]) trading_markup = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="Настройки", callback_data='clb_settings_message')], + [InlineKeyboardButton(text="Мои сделки", callback_data='clb_my_deals')], [InlineKeyboardButton(text="Указать торговую пару", callback_data='clb_update_trading_pair')], [InlineKeyboardButton(text="Выбрать тип входа", callback_data='clb_update_entry_type')], - [InlineKeyboardButton(text="Мои сделки", callback_data='clb_my_deals')], + [InlineKeyboardButton(text="Начать торговлю", callback_data="clb_start_chatbot_trading")], ]) -open_deal_markup = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text="Открыть сделку", callback_data="clb_open_deal")], -]) - - - entry_order_type_markup = InlineKeyboardMarkup( inline_keyboard=[ [ @@ -48,26 +44,32 @@ entry_order_type_markup = InlineKeyboardMarkup( ] ) -back_btn_list_settings = [InlineKeyboardButton(text="Назад", callback_data='clb_back_to_special_settings_message')] # Кнопка для возврата к списку каталога настроек -back_btn_list_settings_markup = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Назад", callback_data='clb_back_to_special_settings_message')]]) # Клавиатура для возврата к списку каталога настроек +back_btn_list_settings = [InlineKeyboardButton(text="Назад", + callback_data='clb_back_to_special_settings_message')] # Кнопка для возврата к списку каталога настроек +back_btn_list_settings_markup = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Назад", + callback_data='clb_back_to_special_settings_message')]]) # Клавиатура для возврата к списку каталога настроек back_btn_to_main = [InlineKeyboardButton(text="На главную", callback_data='clb_back_to_main')] +back_to_main = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="На главную", callback_data='back_to_main')], + ]) + main_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_trading_mode'), + [InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_trading_mode'), InlineKeyboardButton(text='Тип маржи', callback_data='clb_change_margin_type')], - [InlineKeyboardButton(text='Размер кредитного плеча', callback_data='clb_change_size_leverage'), + [InlineKeyboardButton(text='Размер кредитного плеча', callback_data='clb_change_size_leverage'), InlineKeyboardButton(text='Начальная ставка', callback_data='clb_change_starting_quantity')], - [InlineKeyboardButton(text='Коэффициент Мартингейла', callback_data='clb_change_martingale_factor'), + [InlineKeyboardButton(text='Коэффициент Мартингейла', callback_data='clb_change_martingale_factor'), InlineKeyboardButton(text='Максимальное кол-во ставок', callback_data='clb_change_maximum_quantity')], - back_btn_list_settings, + back_btn_list_settings, back_btn_to_main ]) risk_management_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text='Изм. цены прибыли', callback_data='clb_change_price_profit'), + [InlineKeyboardButton(text='Изм. цены прибыли', callback_data='clb_change_price_profit'), InlineKeyboardButton(text='Изм. цены убытков', callback_data='clb_change_price_loss')], [InlineKeyboardButton(text='Макс. риск на сделку', callback_data='clb_change_max_risk_deal')], @@ -78,15 +80,15 @@ risk_management_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ ]) condition_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text='Триггер', callback_data='clb_change_trigger'), - InlineKeyboardButton(text='Фильтр времени', callback_data='clb_change_filter_time')], + [InlineKeyboardButton(text='Триггер', callback_data='clb_change_trigger'), + InlineKeyboardButton(text='Таймер', callback_data='clb_change_timer')], - [InlineKeyboardButton(text='Фильтр волатильности', callback_data='clb_change_filter_volatility'), + [InlineKeyboardButton(text='Фильтр волатильности', callback_data='clb_change_filter_volatility'), InlineKeyboardButton(text='Внешние сигналы', callback_data='clb_change_external_cues')], - [InlineKeyboardButton(text='Сигналы TradingView', callback_data='clb_change_tradingview_cues'), + [InlineKeyboardButton(text='Сигналы TradingView', callback_data='clb_change_tradingview_cues'), InlineKeyboardButton(text='Webhook URL', callback_data='clb_change_webhook')], - + [InlineKeyboardButton(text='AI - аналитика', callback_data='clb_change_ai_analytics')], back_btn_list_settings, @@ -94,7 +96,7 @@ condition_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ ]) additional_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text='Сохранить шаблон', callback_data='clb_change_save_pattern'), + [InlineKeyboardButton(text='Сохранить шаблон', callback_data='clb_change_save_pattern'), InlineKeyboardButton(text='Автозапуск', callback_data='clb_change_auto_start')], [InlineKeyboardButton(text='Уведомления', callback_data='clb_change_notifications')], @@ -105,10 +107,10 @@ additional_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ trading_mode_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="Лонг", callback_data="trade_mode_long"), - InlineKeyboardButton(text="Шорт", callback_data="trade_mode_short")], + InlineKeyboardButton(text="Шорт", callback_data="trade_mode_short")], [InlineKeyboardButton(text="Свитч", callback_data="trade_mode_switch"), - InlineKeyboardButton(text="Смарт", callback_data="trade_mode_smart")], + InlineKeyboardButton(text="Смарт", callback_data="trade_mode_smart")], back_btn_list_settings, back_btn_to_main @@ -116,21 +118,48 @@ trading_mode_markup = InlineKeyboardMarkup(inline_keyboard=[ margin_type_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="Изолированный", callback_data="margin_type_isolated"), - InlineKeyboardButton(text="Кросс", callback_data="margin_type_cross")], + InlineKeyboardButton(text="Кросс", callback_data="margin_type_cross")], back_btn_list_settings, back_btn_to_main ]) -trigger_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE - [InlineKeyboardButton(text='Ручной', callback_data="clb_trigger_ruchnoy"), InlineKeyboardButton(text='TradingView', callback_data="clb_trigger_tradingview")], - [InlineKeyboardButton(text="Автоматический", callback_data="clb_trigger_auto")] +trigger_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE + [InlineKeyboardButton(text='Ручной', callback_data="clb_trigger_ruchnoy"), + InlineKeyboardButton(text='TradingView', callback_data="clb_trigger_tradingview")], + [InlineKeyboardButton(text="Автоматический", callback_data="clb_trigger_auto")] ]) buttons_yes_no_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE - [InlineKeyboardButton(text='Да', callback_data="clb_yes"), InlineKeyboardButton(text='Нет', callback_data="clb_yes")] + [InlineKeyboardButton(text='Да', callback_data="clb_yes"), + InlineKeyboardButton(text='Нет', callback_data="clb_yes")] ]) buttons_on_off_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE - [InlineKeyboardButton(text='Включить', callback_data="clb_on"), InlineKeyboardButton(text='Выключить', callback_data="clb_off")] -]) \ No newline at end of file + [InlineKeyboardButton(text='Включить', callback_data="clb_on"), + InlineKeyboardButton(text='Выключить', callback_data="clb_off")] +]) + + +def create_trades_inline_keyboard(trades): + buttons = [] + for trade in trades: + symbol = trade['symbol'] if isinstance(trade, dict) else trade.symbol + buttons.append([ + InlineKeyboardButton(text=f"{symbol}", callback_data=f"show_deal_{symbol}") + ]) + return InlineKeyboardMarkup(inline_keyboard=buttons) + + +def create_close_deal_markup(symbol: str) -> InlineKeyboardMarkup: + return InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="Закрыть сделку", callback_data=f"close_deal:{symbol}")], + back_btn_to_main + ]) + + +timer_markup = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="Установить таймер", callback_data="clb_set_timer")], + [InlineKeyboardButton(text="Остановить таймер", callback_data="clb_stop_timer")], + back_btn_to_main +]) -- 2.50.1 From fc381ae6c227e19a16d360a6ddb42d681ef02598 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Fri, 22 Aug 2025 13:54:13 +0500 Subject: [PATCH 21/77] Added timer --- .../functions/condition_settings/settings.py | 80 ++++++++++++++++--- 1 file changed, 69 insertions(+), 11 deletions(-) diff --git a/app/telegram/functions/condition_settings/settings.py b/app/telegram/functions/condition_settings/settings.py index 3d9e571..79fc57f 100644 --- a/app/telegram/functions/condition_settings/settings.py +++ b/app/telegram/functions/condition_settings/settings.py @@ -1,6 +1,21 @@ import app.telegram.Keyboards.inline_keyboards as inline_markup - +from aiogram import Router, F +from aiogram.types import Message, CallbackQuery +from aiogram.fsm.context import FSMContext import app.telegram.database.requests as rq +from aiogram.fsm.state import State, StatesGroup + +condition_settings_router = Router() + + +class condition_settings(StatesGroup): + trigger = State() + timer = State() + volatilty = State() + volume = State() + integration = State() + use_tv_signal = State() + async def reg_new_user_default_condition_settings(id, message): tg_id = id @@ -9,11 +24,12 @@ async def reg_new_user_default_condition_settings(id, message): await rq.set_new_user_default_condition_settings(tg_id, trigger) + async def main_settings_message(id, message, state): text = """ Условия запуска - Триггер: Ручной запуск / Сигнал TradingView / Полностью автоматический -- Фильтр времени: диапазон по дням недели и времени суток +- Таймер: установить таймер / остановить таймер - Фильтр волатильности / объёма: включить/отключить - Интеграции и внешние сигналы: - Использовать сигналы TradingView: да / нет @@ -21,7 +37,8 @@ async def main_settings_message(id, message, state): - Webhook URL для сигналов (если используется TradingView): """ await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.condition_settings_markup) - + + async def trigger_message(message, state): text = '''Триггер @@ -29,13 +46,52 @@ async def trigger_message(message, state): await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trigger_markup) -async def filter_time_message(message, state): - text = '''Фильтр времени - ??? - ''' +async def timer_message(id,message: Message, state: FSMContext): + await state.set_state(condition_settings.timer) + + timer_info = await rq.get_user_timer(id) + if timer_info is None: + await message.answer("Таймер не установлен.", reply_markup=inline_markup.timer_markup) + return + + await message.answer( + f"Таймер: {timer_info['timer_minutes']} мин\n" + f"Осталось: {timer_info['remaining_minutes']} мин\n", + reply_markup=inline_markup.timer_markup + ) + + +@condition_settings_router.callback_query(F.data == "clb_set_timer") +async def set_timer_callback(callback: CallbackQuery, state: FSMContext): + await state.set_state(condition_settings.timer) # состояние для ввода времени + await callback.message.answer("Введите время работы в минутах (например, 60):") + await callback.answer() + + +@condition_settings_router.message(condition_settings.timer) +async def process_timer_input(message: Message, state: FSMContext): + try: + minutes = int(message.text) + if minutes <= 0: + await message.reply("Введите число больше нуля.") + return + + # Сохраняем в базу или память время таймера для пользователя + await rq.update_user_timer(message.from_user.id, minutes) + + await message.answer(f"Таймер установлен на {minutes} минут.", reply_markup=inline_markup.back_to_main) + await state.clear() + except ValueError: + await message.reply("Пожалуйста, введите корректное число.") + + +@condition_settings_router.callback_query(F.data == "clb_stop_timer") +async def stop_timer_callback(callback: CallbackQuery): + await rq.update_user_timer(callback.from_user.id, 0) # обнуляем таймер + await callback.message.answer("Таймер остановлен.", reply_markup=inline_markup.back_to_main) + await callback.answer() - await message.answer(text=text) async def filter_volatility_message(message, state): text = '''Фильтр волатильности @@ -44,6 +100,7 @@ async def filter_volatility_message(message, state): await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_on_off_markup) + async def external_cues_message(message, state): text = '''Внешние сигналы @@ -51,6 +108,7 @@ async def external_cues_message(message, state): await message.answer(text=text, parse_mode='html', reply_markup=None) + async def trading_cues_message(message, state): text = '''Использование сигналов @@ -58,16 +116,16 @@ async def trading_cues_message(message, state): await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup) + async def webhook_message(message, state): text = '''Скиньте ссылку на webhook (если есть trading view): ''' await message.answer(text=text, parse_mode='html') - + + async def ai_analytics_message(message, state): text = '''ИИ - Аналитика Описание... ''' await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup) - - -- 2.50.1 From cd7180c3d7b1415c4691da191fdd2b05cb466199 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Fri, 22 Aug 2025 13:55:50 +0500 Subject: [PATCH 22/77] Added the function of getting and updating data in the database for the UserTimer table --- app/telegram/database/requests.py | 76 +++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/app/telegram/database/requests.py b/app/telegram/database/requests.py index 59f58de..00b02f7 100644 --- a/app/telegram/database/requests.py +++ b/app/telegram/database/requests.py @@ -1,4 +1,5 @@ import logging +from datetime import datetime, timedelta from typing import Any logger = logging.getLogger(__name__) @@ -14,7 +15,7 @@ from app.telegram.database.models import User_Additional_Settings as UAS from app.telegram.database.models import Trading_Mode from app.telegram.database.models import Margin_type from app.telegram.database.models import Trigger -from app.telegram.database.models import USER_DEALS +from app.telegram.database.models import USER_DEALS, UserTimer import app.telegram.functions.functions as func # functions @@ -142,10 +143,19 @@ async def get_user_trades(tg_id): return trades -async def update_user_trades(tg_id, symbol, side): +async def update_user_trades(tg_id, **kwargs): async with async_session() as session: - await session.execute(update(USER_DEALS).where(USER_DEALS.tg_id == tg_id).values(symbol = symbol, side = side)) + query = update(USER_DEALS).where(USER_DEALS.tg_id == tg_id).values(**kwargs) + await session.execute(query) + await session.commit() +async def delete_user_trade(tg_id: int, symbol: str): + async with async_session() as session: + await session.execute( + USER_DEALS.__table__.delete().where( + (USER_DEALS.tg_id == tg_id) & (USER_DEALS.symbol == symbol) + ) + ) await session.commit() async def get_for_registration_trading_mode(): @@ -319,4 +329,64 @@ async def update_commission_fee(tg_id, num): await session.commit() +async def get_user_timer(tg_id): + async with async_session() as session: + result = await session.execute(select(UserTimer).where(UserTimer.tg_id == tg_id)) + user_timer = result.scalars().first() + + if not user_timer: + logging.info(f"No timer found for user {tg_id}") + return None + + timer_minutes = user_timer.timer_minutes + timer_start = user_timer.timer_start + timer_end = user_timer.timer_end + + logging.info(f"Timer data for tg_id={tg_id}: " + f"timer_minutes={timer_minutes}, " + f"timer_start={timer_start}, " + f"timer_end={timer_end}") + + remaining = None + if timer_end: + remaining = max(0, int((timer_end - datetime.utcnow()).total_seconds() // 60)) + + return { + "timer_minutes": timer_minutes, + "timer_start": timer_start, + "timer_end": timer_end, + "remaining_minutes": remaining + } + + +async def update_user_timer(tg_id, minutes: int): + async with async_session() as session: + try: + async with async_session() as session: + timer_start = None + timer_end = None + + if minutes > 0: + timer_start = datetime.utcnow() + timer_end = timer_start + timedelta(minutes=minutes) + + result = await session.execute(select(UserTimer).where(UserTimer.tg_id == tg_id)) + user_timer = result.scalars().first() + + if user_timer: + user_timer.timer_minutes = minutes + user_timer.timer_start = timer_start + user_timer.timer_end = timer_end + else: + user_timer = UserTimer( + tg_id=tg_id, + timer_minutes=minutes, + timer_start=timer_start, + timer_end=timer_end + ) + session.add(user_timer) + + await session.commit() + except Exception as e: + logging.error(f"Ошибка обновления таймера пользователя {tg_id}: {e}") -- 2.50.1 From 812920f46d6ada279490983128072182459e16cc Mon Sep 17 00:00:00 2001 From: algizn97 Date: Fri, 22 Aug 2025 13:56:56 +0500 Subject: [PATCH 23/77] The timer function has been updated --- app/telegram/handlers/handlers.py | 89 +++++++++++++++++++------------ 1 file changed, 54 insertions(+), 35 deletions(-) diff --git a/app/telegram/handlers/handlers.py b/app/telegram/handlers/handlers.py index 48b2a9b..e1cb05f 100644 --- a/app/telegram/handlers/handlers.py +++ b/app/telegram/handlers/handlers.py @@ -5,7 +5,7 @@ from aiogram.filters import CommandStart, Command from aiogram.types import Message, CallbackQuery from aiogram.fsm.context import FSMContext -import app.telegram.functions.functions as func # functions +import app.telegram.functions.functions as func # functions import app.telegram.functions.main_settings.settings as func_main_settings import app.telegram.functions.risk_management_settings.settings as func_rmanagement_settings import app.telegram.functions.condition_settings.settings as func_condition_settings @@ -17,11 +17,13 @@ import app.telegram.Keyboards.reply_keyboards as reply_markup router = Router() + @router.message(CommandStart()) async def start_message(message: Message): await rq.set_new_user_bybit_api(message.from_user.id) await func.start_message(message) + @router.message(F.text == "👤 Профиль") async def profile_message(message: Message): user = await rq.check_user(message.from_user.id) @@ -29,6 +31,7 @@ async def profile_message(message: Message): if user: await func.profile_message(message.from_user.username, message) + @router.message(F.text == "Настройки") async def settings_msg(message: Message): user = await rq.check_user(message.from_user.id) @@ -36,27 +39,29 @@ async def settings_msg(message: Message): if user: await func.settings_message(message) + @router.callback_query(F.data == "clb_start_chatbot_message") -async def clb_profile_msg (callback: CallbackQuery): +async def clb_profile_msg(callback: CallbackQuery): user = await rq.check_user(callback.from_user.id) username = '' - - if callback.from_user.first_name == None: + + if callback.from_user.first_name == None: username = callback.from_user.last_name elif callback.from_user.last_name == None: username = callback.from_user.first_name else: username = f'{callback.from_user.first_name} {callback.from_user.last_name}' - if user: + if user: await func.profile_message(callback.from_user.username, callback.message) else: await rq.save_tg_id_new_user(callback.from_user.id) await func_main_settings.reg_new_user_default_main_settings(callback.from_user.id, callback.message) - await func_rmanagement_settings.reg_new_user_default_risk_management_settings(callback.from_user.id, callback.message) - await func_condition_settings.reg_new_user_default_condition_settings(callback.from_user.id, callback.message) + await func_rmanagement_settings.reg_new_user_default_risk_management_settings(callback.from_user.id, + callback.message) + await func_condition_settings.reg_new_user_default_condition_settings(callback.from_user.id, callback.message) await func_additional_settings.reg_new_user_default_additional_settings(callback.from_user.id, callback.message) await callback.message.answer(f'Здравствуйте, {username}!', reply_markup=reply_markup.base_buttons_markup) @@ -64,25 +69,29 @@ async def clb_profile_msg (callback: CallbackQuery): await func.profile_message(username, callback.message) await callback.answer() - - # Настройки торговли + + +# Настройки торговли @router.callback_query(F.data == "clb_settings_message") -async def clb_settings_msg (callback: CallbackQuery): +async def clb_settings_msg(callback: CallbackQuery): await func.settings_message(callback.message) await callback.answer() + @router.callback_query(F.data == "clb_back_to_special_settings_message") async def clb_back_to_settings_msg(callback: CallbackQuery): await func.settings_message(callback.message) await callback.answer() + @router.callback_query(F.data == "clb_change_main_settings") async def clb_change_main_settings_message(callback: CallbackQuery, state: FSMContext): await func_main_settings.main_settings_message(callback.from_user.id, callback.message, state) - await callback.answer() + await callback.answer() + @router.callback_query(F.data == "clb_change_risk_management_settings") async def clb_change_risk_management_message(callback: CallbackQuery, state: FSMContext): @@ -90,26 +99,31 @@ async def clb_change_risk_management_message(callback: CallbackQuery, state: FSM await callback.answer() + @router.callback_query(F.data == "clb_change_condition_settings") async def clb_change_condition_message(callback: CallbackQuery, state: FSMContext): await func_condition_settings.main_settings_message(callback.from_user.id, callback.message, state) await callback.answer() + @router.callback_query(F.data == "clb_change_additional_settings") async def clb_change_additional_message(callback: CallbackQuery, state: FSMContext): await func_additional_settings.main_settings_message(callback.from_user.id, callback.message, state) await callback.answer() - # Конкретные настройки каталогов -list_main_settings = ['clb_change_trading_mode', - 'clb_change_margin_type', - 'clb_change_size_leverage', - 'clb_change_starting_quantity', - 'clb_change_martingale_factor', + +# Конкретные настройки каталогов +list_main_settings = ['clb_change_trading_mode', + 'clb_change_margin_type', + 'clb_change_size_leverage', + 'clb_change_starting_quantity', + 'clb_change_martingale_factor', 'clb_change_maximum_quantity' -] + ] + + @router.callback_query(F.data.in_(list_main_settings)) async def clb_main_settings_msg(callback: CallbackQuery, state: FSMContext): await callback.answer() @@ -132,11 +146,13 @@ async def clb_main_settings_msg(callback: CallbackQuery, state: FSMContext): logging.error(f"Error callback in main_settings match-case: {e}") -list_risk_management_settings = ['clb_change_price_profit', - 'clb_change_price_loss', - 'clb_change_max_risk_deal', - 'commission_fee', -] +list_risk_management_settings = ['clb_change_price_profit', + 'clb_change_price_loss', + 'clb_change_max_risk_deal', + 'commission_fee', + ] + + @router.callback_query(F.data.in_(list_risk_management_settings)) async def clb_risk_management_settings_msg(callback: CallbackQuery, state: FSMContext): await callback.answer() @@ -153,26 +169,28 @@ async def clb_risk_management_settings_msg(callback: CallbackQuery, state: FSMCo await func_rmanagement_settings.commission_fee_message(callback.message, state) except Exception as e: logging.error(f"Error callback in risk_management match-case: {e}") - - + + list_condition_settings = ['clb_change_trigger', - 'clb_change_filter_time', + 'clb_change_timer', 'clb_change_filter_volatility', 'clb_change_external_cues', 'clb_change_tradingview_cues', 'clb_change_webhook', 'clb_change_ai_analytics' -] + ] + + @router.callback_query(F.data.in_(list_condition_settings)) async def clb_condition_settings_msg(callback: CallbackQuery, state: FSMContext): await callback.answer() - + try: match callback.data: case 'clb_change_trigger': await func_condition_settings.trigger_message(callback.message, state) - case 'clb_change_filter_time': - await func_condition_settings.filter_time_message(callback.message, state) + case 'clb_change_timer': + await func_condition_settings.timer_message(callback.from_user.id, callback.message, state) case 'clb_change_filter_volatility': await func_condition_settings.filter_volatility_message(callback.message, state) case 'clb_change_external_cues': @@ -187,10 +205,12 @@ async def clb_condition_settings_msg(callback: CallbackQuery, state: FSMContext) logging.error(f"Error callback in main_settings match-case: {e}") -list_additional_settings = ['clb_change_save_pattern', - 'clb_change_auto_start', - 'clb_change_notifications', -] +list_additional_settings = ['clb_change_save_pattern', + 'clb_change_auto_start', + 'clb_change_notifications', + ] + + @router.callback_query(F.data.in_(list_additional_settings)) async def clb_additional_settings_msg(callback: CallbackQuery, state: FSMContext): await callback.answer() @@ -205,4 +225,3 @@ async def clb_additional_settings_msg(callback: CallbackQuery, state: FSMContext await func_additional_settings.notifications_message(callback.message, state) except Exception as e: logging.error(f"Error callback in additional_settings match-case: {e}") - -- 2.50.1 From 6ec99dc9a7d93dc478f6acfbbad78a290b4ee0d7 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Fri, 22 Aug 2025 16:47:33 +0500 Subject: [PATCH 24/77] Update functions --- app/services/Bybit/functions/Futures.py | 506 +++++++++------------- app/services/Bybit/functions/functions.py | 272 +++++++----- app/telegram/functions/functions.py | 4 +- 3 files changed, 379 insertions(+), 403 deletions(-) diff --git a/app/services/Bybit/functions/Futures.py b/app/services/Bybit/functions/Futures.py index afd28a0..360c7ca 100644 --- a/app/services/Bybit/functions/Futures.py +++ b/app/services/Bybit/functions/Futures.py @@ -1,325 +1,239 @@ -import time - -from typing import Optional -from asyncio import Handle - -from annotated_types import T +import asyncio +import time +import logging from pybit import exceptions from pybit.unified_trading import HTTP -from pybit.unified_trading import WebSocket -from app.services.Bybit.functions import price_symbol +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 logging -logging.basicConfig(level=logging.DEBUG) - -def handle_message(message): - print(message) async def info_access_open_deal(message, symbol, trade_mode, margin_mode, leverage, qty): - match margin_mode: - case 'ISOLATED_MARGIN': - margin_mode = 'Isolated' - case 'REGULAR_MARGIN': - margin_mode = 'Cross' - + human_margin_mode = 'Isolated' if margin_mode == 'ISOLATED_MARGIN' else 'Cross' text = f'''Позиция была успешна открыта! - Торговая пара: {symbol} - Движение: {trade_mode} - Тип-маржи: {margin_mode} - Кредитное плечо: {leverage} - Количество: {qty} - ''' - +Торговая пара: {symbol} +Движение: {trade_mode} +Тип-маржи: {human_margin_mode} +Кредитное плечо: {leverage} +Количество: {qty} +''' await message.answer(text=text, parse_mode='html') + async def error_max_step(message): - await message.answer('Сделка не была совершена, превышен лимит максимального количества ставок') + await message.answer('Сделка не была совершена, превышен лимит максимального количества ставок в серии.') + async def error_max_risk(message): - await message.answer('Сделка не была совершена, слишком высокий риск') + await message.answer('Сделка не была совершена, риск убытка превышает допустимый лимит.') + + +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 contract_long(tg_id, message, margin_mode): 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) - data_risk_management_stgs = await rq.get_user_risk_management_settings(tg_id) - - match margin_mode: - case 'Isolated': - margin_mode = 'ISOLATED_MARGIN' - case 'Cross': - margin_mode = 'REGULAR_MARGIN' - - client = HTTP( - api_key=api_key, - api_secret=secret_key - ) - - try: - balance = 0 - price = 0 - - balance = await balance_g.get_balance(tg_id) - price = await price_symbol.get_price(tg_id, message) - - client.set_margin_mode( - setMarginMode=margin_mode # margin_type - ) - - martingale_factor = float(data_main_stgs['martingale_factor']) # Исправлено: было maximal_quantity - max_martingale_steps = int(data_main_stgs['maximal_quantity']) - starting_quantity = float(data_main_stgs['starting_quantity']) - max_risk_percent = float(data_risk_management_stgs['max_risk_deal']) - loss_profit = float(data_risk_management_stgs['price_loss']) - takeprofit= float(data_risk_management_stgs['price_profit']) - commission_fee = float(data_risk_management_stgs.get('commission_fee', 0)) - takeProfit_raw = takeprofit - takeProfit = takeProfit_raw - commission_fee # уменьшаем TP на комиссию - - if takeProfit < 0: - takeProfit = 0 - - # Инициализация переменных - next_quantity = starting_quantity - last_quantity = starting_quantity - realised_pnl = 0.0 - - current_martingale_step = 0 # Текущая ставка в серии - - next_quantity = 0 - realised_pnl = 0 - - last_quantity = starting_quantity - - # Пример расчёта следующего размера позиции - try: - position_info = client.get_positions(category='linear', symbol=SYMBOL) - position = position_info['result']['list'][0] # или другой нужный индекс - - realised_pnl = float(position['unrealisedPnl']) - - if realised_pnl > 0: - starting_quantity = next_quantity - current_martingale_step = 0 - elif not realised_pnl: - next_quantity = starting_quantity - current_martingale_step += 1 - else: - current_martingale_step += 1 - next_quantity = last_quantity * martingale_factor - starting_quantity = next_quantity - except Exception as e: - print("Не получены позиции") - next_quantity = starting_quantity - - potential_loss = (next_quantity * float(price)) * (loss_profit / 100) - allowed_loss = float(balance) * (max_risk_percent / 100) - - if current_martingale_step >= max_martingale_steps: - print("Достигнут максимум ставок в серии (8)!") - print("Торговля не продолжится") - - await error_max_step(message) - else: - if potential_loss > allowed_loss: - print(f"ОШИБКА: Риск превышен!") - print(f"Ручной qty = {next_quantity} → Убыток = {potential_loss} USDT") - print(f"Разрешено = {allowed_loss} USDT (1% от баланса)") - - await error_max_risk(message) - else: - print(f"Риск в допустимых пределах. Qty = {next_quantity}") - - r = client.place_order( - category='linear', - symbol=SYMBOL, - side='Buy', - orderType="Market", - leverage=int(data_main_stgs['size_leverage']), - qty=next_quantity, - takeProfit=takeProfit, # TP - закрывает позицию, когда цена достигает нужного уровня - stopProfit=float(data_risk_management_stgs['price_loss']), # SL - закрывает позицию, когда убыток достигает нужного уровня - orderLinkId=f"deal_{SYMBOL}_{time.time()}" - ) - - await info_access_open_deal(message, SYMBOL, data_main_stgs['trading_mode'], margin_mode, data_main_stgs['size_leverage'], next_quantity) - - except exceptions.InvalidRequestError as e: - logging.error(f"Неверно указана торговая пара: {e}") - await message.answer('Недостаточно баланса') - except Exception as e: - logging.error(f"Ошибка при совершении сделки: {e}") - await message.answer('⚠️ Ошибка при совершении сделки') - -async def contract_short(tg_id, message, margin_mode): - 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) - data_risk_management_stgs = await rq.get_user_risk_management_settings(tg_id) - - match margin_mode: - case 'Isolated': - margin_mode = 'ISOLATED_MARGIN' - case 'Cross': - margin_mode = 'REGULAR_MARGIN' - - client = HTTP( - api_key=api_key, - api_secret=secret_key - ) - - try: - balance = 0 - price = 0 - - balance = await balance_g.get_balance(tg_id) - price = await price_symbol.get_price(tg_id, message) - - client.set_margin_mode( - setMarginMode=margin_mode # margin_type - ) - - martingale_factor = float(data_main_stgs['martingale_factor']) # Исправлено: было maximal_quantity - max_martingale_steps = int(data_main_stgs['maximal_quantity']) - starting_quantity = float(data_main_stgs['starting_quantity']) - max_risk_percent = float(data_risk_management_stgs['max_risk_deal']) - loss_profit = float(data_risk_management_stgs['price_loss']) - takeprofit = float(data_risk_management_stgs['price_profit']) - commission_fee = float(data_risk_management_stgs.get('commission_fee', 0)) - takeProfit_raw = takeprofit - takeProfit = takeProfit_raw - commission_fee # уменьшаем TP на комиссию - - if takeProfit < 0: - takeProfit = 0 - - # Инициализация переменных - next_quantity = starting_quantity - last_quantity = starting_quantity - realised_pnl = 0.0 - - current_martingale_step = 0 # Текущая ставка в серии - - next_quantity = 0 - realised_pnl = 0 - - last_quantity = starting_quantity - - # Пример расчёта следующего размера позиции - try: - position_info = client.get_positions(category='linear', symbol=SYMBOL) - position = position_info['result']['list'][0] # или другой нужный индекс - - realised_pnl = float(position['unrealisedPnl']) - - if realised_pnl > 0: - starting_quantity = next_quantity - current_martingale_step = 0 - elif not realised_pnl: - next_quantity = starting_quantity - current_martingale_step += 1 - else: - current_martingale_step += 1 - next_quantity = last_quantity * martingale_factor - starting_quantity = next_quantity - except Exception as e: - print("Не получены позиции") - next_quantity = starting_quantity - - potential_loss = (next_quantity * float(price)) * (loss_profit / 100) - allowed_loss = float(balance) * (max_risk_percent / 100) - - if current_martingale_step >= max_martingale_steps: - print("Достигнут максимум ставок в серии (8)!") - print("Торговля не продолжится") - - await error_max_step(message) - else: - if potential_loss > allowed_loss: - print(f"ОШИБКА: Риск превышен!") - print(f"Ручной qty = {next_quantity} → Убыток = {potential_loss} USDT") - print(f"Разрешено = {allowed_loss} USDT (1% от баланса)") - - await error_max_risk(message) - else: - print(f"Риск в допустимых пределах. Qty = {next_quantity}") - - r = client.place_order( - category='linear', - symbol=SYMBOL, - side='Sell', - orderType="Market", - leverage=int(data_main_stgs['size_leverage']), - qty=next_quantity, - orderLinkId=f"deal_{SYMBOL}_{time.time()}" - ) - - await info_access_open_deal(message, SYMBOL, data_main_stgs['trading_mode'], margin_mode, data_main_stgs['size_leverage'], next_quantity) - - except exceptions.InvalidRequestError as e: - logging.error(f"Error in open_deal: {e}") - await message.answer('Недостаточно баланса') - except Exception as e: - logging.error(f"Error in open_deal: {e}") - await message.answer('⚠️ Ошибка при совершении сделки') - - - -async def open_market_order(tg_id, message, api_key, secret_key): - data_main_stgs = await rq.get_user_main_settings(tg_id) - trading_mode = data_main_stgs['trading_mode'] - margin_mode = data_main_stgs.get('margin_type') - - if trading_mode == 'Long': - await contract_long(tg_id, message, margin_mode) - elif trading_mode == 'Short': - await contract_short(tg_id, message, margin_mode) - elif trading_mode == 'Smart': - await message.answer("Режим Smart пока недоступен") - elif trading_mode == 'Switch': - await message.answer("Режим Switch пока недоступен") - - -async def open_limit_order(tg_id, message, price, api_key, secret_key): - data_main_stgs = await rq.get_user_main_settings(tg_id) - trading_mode = data_main_stgs['trading_mode'] - margin_mode = data_main_stgs.get('margin_type') - order_type = await rq.get_entry_order_type(tg_id) - - - client = HTTP( - api_key=api_key, - api_secret=secret_key - ) - symbol = await rq.get_symbol(tg_id) - qty = float(data_main_stgs['starting_quantity']) - side = 'Buy' if trading_mode == 'Long' else 'Short' + data_main_stgs = await rq.get_user_main_settings(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: + balance = await balance_g.get_balance(tg_id) + price = await price_symbol.get_price(tg_id, message) + + # Установка маржинального режима + 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)) + + takeProfit = max(takeprofit - commission_fee, 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) + 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) + else: + # Позиция не открыта — начинаем с начальной ставки + next_quantity = starting_quantity + current_martingale_step = 0 + + # Проверяем риск убытка + potential_loss = next_quantity * price * (loss_profit / 100) + allowed_loss = balance * (max_risk_percent / 100) + + if potential_loss > allowed_loss: + await error_max_risk(message) + return + + # Отправляем запрос на открытие ордера response = client.place_order( category='linear', symbol=symbol, side=side, - orderType='Limit', - qty=qty, - price=price, - timeInForce='GTC', - orderLinkId=f"order_{int(time.time())}" + orderType="Market", + qty=next_quantity, + leverage=int(data_main_stgs['size_leverage']), + takeProfit=takeProfit, + stopLoss=loss_profit, + orderLinkId=f"deal_{symbol}_{int(time.time())}" ) - if response.get('retCode') == 0: - await message.answer(f"Limit ордер открыт: {side} {qty} {symbol} по цене {price}") - await rq.update_user_trades(tg_id, symbol=symbol, side=order_type) + 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) + await rq.update_martingale_step(tg_id, current_martingale_step) else: - await message.answer(f"Ошибка открытия ордера: {response.get('retMsg')}") + await message.answer(f"Ошибка открытия ордера: {response.get('ret_msg', 'неизвестная ошибка')}") + + except exceptions.InvalidRequestError as e: + logging.error(f"InvalidRequestError: {e}") + await message.answer('Ошибка: неверно указана торговая пара или параметры.') except Exception as e: - logging.error(f"Ошибка при открытии лимитного ордера: {e}") - await message.answer("Ошибка при открытии ордера") \ No newline at end of file + logging.error(f"Ошибка при совершении сделки: {e}") + await message.answer('⚠️ Ошибка при совершении сделки') + + +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 + + 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 + + # Проверяем позиции + data_main_stgs = await rq.get_user_main_settings(tg_id) + side = 'Buy' if data_main_stgs['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) + + + +async def get_active_positions(message, api_key, secret_key, symbol): + + 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: + return [] + symbols = [item['symbol'] for item in instruments_resp.get('result', {}).get('list', [])] + + active_positions = [] + + async def fetch_positions(symbol): + try: + resp = client.get_positions(category='linear', symbol=symbol) + if resp.get('ret_code') == 0: + positions = resp.get('result', {}).get('list', []) + for pos in positions: + if pos.get('size') and float(pos['size']) > 0: + active_positions.append(pos) + except Exception as e: + logging.error(f"Ошибка при получении позиций: {e}") + await message.answer('⚠️ Ошибка при получении позиций') + + for sym in symbols: + await fetch_positions(sym) + + 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" + + 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: + logging.error(f"Ошибка закрытия сделки {symbol} для пользователя {tg_id}: {e}") + + return False + + +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 diff --git a/app/services/Bybit/functions/functions.py b/app/services/Bybit/functions/functions.py index ac8b186..ef87d6e 100644 --- a/app/services/Bybit/functions/functions.py +++ b/app/services/Bybit/functions/functions.py @@ -1,10 +1,13 @@ -from aiogram import F, Router +import asyncio + +from aiogram import F, Router from app.services.Bybit.functions import Futures, func_min_qty -from app.services.Bybit.functions.Futures import open_market_order, open_limit_order +from app.services.Bybit.functions.Futures import open_position, close_user_trade, get_active_positions, trading_cycle 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 @@ -14,13 +17,21 @@ from aiogram.fsm.context import FSMContext router_functions_bybit_trade = Router() + class state_update_symbol(StatesGroup): symbol = State() + class state_update_entry_type(StatesGroup): entry_type = State() -@router_functions_bybit_trade.callback_query(F.data.in_(['clb_start_trading', 'clb_back_to_main'])) + +class TradeSetup(StatesGroup): + waiting_for_timer = State() + waiting_for_positive_percent = 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) @@ -30,7 +41,7 @@ async def clb_start_bybit_trade_message(callback: CallbackQuery, state: FSMConte symbol = await rq.get_symbol(callback.from_user.id) text = f'''💎 Торговля на Bybit - + ⚖️ Ваш баланс (USDT): {balance} 📊 Текущая торговая пара: {symbol} @@ -41,16 +52,17 @@ 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) balance = await get_balance(message.from_user.id, message) - if balance: + if balance: symbol = await rq.get_symbol(message.from_user.id) text = f'''💎 Торговля на Bybit - + ⚖️ Ваш баланс (USDT): {balance} 📊 Текущая торговая пара: {symbol} @@ -62,15 +74,18 @@ async def start_bybit_trade_message(message, state): 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') async def update_symbol_for_trade_message(callback: CallbackQuery, state: FSMContext): await state.set_state(state_update_symbol.symbol) - await callback.message.answer(text='Укажите торговую пару заглавными буквами без пробелов и лишних символов (пример: BTCUSDT): ') + await callback.message.answer( + text='Укажите торговую пару заглавными буквами без пробелов и лишних символов (пример: BTCUSDT): ') + @router_functions_bybit_trade.message(state_update_symbol.symbol) async def update_symbol_for_trade(message: Message, state: FSMContext): - await state.update_data(symbol = message.text) + await state.update_data(symbol=message.text) data = await state.get_data() @@ -96,116 +111,161 @@ async def entry_order_type_callback(callback: CallbackQuery, state: FSMContext): await callback.answer("Ошибка выбора", show_alert=True) return - await state.update_data(entry_order_type=order_type) - await rq.update_entry_order_type(callback.from_user.id, order_type) - - if order_type == 'Limit': - await callback.answer("Вы выбрали Limit. Введите цену для лимитного ордера.") - await callback.message.answer("Введите цену лимитного ордера:") - await state.update_data(awaiting_limit_price=True) - else: - await callback.answer("Вы выбрали Market. Нажмите кнопку ниже, чтобы открыть сделку.") - await callback.message.answer("Нажмите кнопку, чтобы открыть сделку.", - reply_markup=inline_markup.open_deal_markup) - await callback.message.delete() - - -@router_functions_bybit_trade.message() -async def process_limit_price_message(message: Message, state: FSMContext): - data = await state.get_data() - if not data.get('awaiting_limit_price'): - return - try: - price = float(message.text) - if price <= 0: - raise ValueError() - except ValueError: - await message.answer("Ошибка: введите корректное положительное число для цены.") - return - - await state.update_data(limit_price=price, awaiting_limit_price=False) - - await message.answer(f"Цена лимитного ордера установлена: {price}. Нажмите кнопку ниже, чтобы открыть сделку.", - reply_markup=inline_markup.open_deal_markup) - - -@router_functions_bybit_trade.callback_query(F.data == "clb_open_deal") -async def open_deal(callback: CallbackQuery, state: FSMContext): - data_main_stgs = await rq.get_user_main_settings(callback.from_user.id) - data = await state.get_data() - order_type = await rq.get_entry_order_type(callback.from_user.id) - api = await rq.get_bybit_api_key(callback.from_user.id) - secret = await rq.get_bybit_secret_key(callback.from_user.id) - qty = data_main_stgs['starting_quantity'] - qty_min = await func_min_qty.get_min_qty(callback.from_user.id, callback.message) - - if qty < qty_min: - await callback.message.edit_text(f"Количество вашей ставки ({qty}) меньше минимального количества ({qty_min}) для данной торговой пары") - await callback.answer() - return - - if order_type == 'Market': - await open_market_order(callback.from_user.id, callback.message, api_key=api, secret_key=secret) - await rq.update_user_trades(callback.from_user.id, symbol=data.get('symbol'), side=order_type) - elif order_type == 'Limit': - price = data.get('limit_price') - if not price: - await callback.answer("Цена для лимитного ордера не задана. Введите сначала цену.") - return - await open_limit_order(callback.from_user.id, callback.message, price, api_key=api, secret_key=secret) - else: - await callback.answer("Неизвестный тип ордера.") - - await callback.message.edit_reply_markup() + await state.update_data(entry_order_type=order_type) + await rq.update_entry_order_type(callback.from_user.id, order_type) + await callback.answer("Тип входа в позицию был успешно обновлен") + except Exception as e: + await callback.message.answer("Произошла ошибка при обновлении типа входа в позицию") 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 - trades = await rq.get_user_trades(tg_id) + + 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) + + trades = await get_active_positions(callback.message, api_key, secret_key, symbol) if not trades: - await callback.message.answer("У вас ещё нет сделок.") + await callback.message.answer("Нет активных позиций.") await callback.answer() return - grouped = {} - for trade in trades: - symbol = trade['symbol'] if isinstance(trade, dict) else trade.symbol - grouped.setdefault(symbol, []).append(trade) + keyboard = inline_markup.create_trades_inline_keyboard(trades) - text_response = "Ваши сделки по валютным парам:\n\n" - for symbol, trade_list in grouped.items(): - text_response += f"{symbol}\n" - for t in trade_list: - side = t['side'] if isinstance(t, dict) else t.side - text_response += f" - {side}\n" - text_response += "\n" - - await callback.message.answer(text_response, parse_mode='html') + await callback.message.answer( + "Выберите сделку из списка:", + reply_markup=keyboard + ) await callback.answer() -# @router_functions_bybit_trade.callback_query(F.data == 'clb_open_deal') -# async def make_deal_bybit (callback: CallbackQuery): -# data_main_stgs = await rq.get_user_main_settings(callback.from_user.id) -# -# trade_mode = data_main_stgs['trading_mode'] -# qty = data_main_stgs['starting_quantity'] -# margin_mode = data_main_stgs['margin_type'] - qty_min = await func_min_qty.get_min_qty(callback.from_user.id, callback.message) -# -# if qty < qty_min: -# await callback.message.edit_text(f"Количество вашей ставки ({qty}) меньше минимального количества ({qty_min}) для данной торговой пары") -# else: -# match trade_mode: -# case 'Long': -# await Futures.contract_long(callback.from_user.id, callback.message, margin_mode) -# case 'Short': -# await Futures.contract_short(callback.from_user.id, callback.message, margin_mode) -# case 'Switch': -# await callback.message.edit_text('Режим Switch пока недоступен') -# case 'Smart': -# await callback.message.edit_text('Режим Smart пока недоступен') \ No newline at end of file +@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() + + +@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} успешно закрыта.") + else: + await callback.message.answer(f"Не удалось закрыть сделку {symbol}.") + + await callback.answer() + + +@router_functions_bybit_trade.callback_query(F.data == "clb_start_chatbot_trading") +async def start_trading_process(callback: CallbackQuery, state: FSMContext): + tg_id = callback.from_user.id + message = callback.message + + # Получаем настройки пользователя + data_main_stgs = await rq.get_user_main_settings(tg_id) + 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) + margin_mode = data_main_stgs.get('margin_type', 'Isolated') + trading_mode = data_main_stgs.get('trading_mode') + + # Проверка API ключей + if not api_key or not secret_key: + await message.answer("❗️ У вас не настроены API ключи для Bybit.") + await callback.answer() + return + + # Проверка режима торговли + if trading_mode not in ['Long', 'Short', 'Smart', 'Switch']: + await message.answer(f"❗️ Некорректный торговый режим: {trading_mode}") + await callback.answer() + return + + # Проверка допустимости маржинального режима + if margin_mode not in ['Isolated', 'Cross']: + margin_mode = 'Isolated' + + # Проверяем открытые позиции и маржинальный режим + client = HTTP(api_key=api_key, api_secret=secret_key) + try: + positions_resp = client.get_positions(category='linear', symbol=symbol) + positions = positions_resp.get('result', {}).get('list', []) + except Exception: + positions = [] + + for pos in positions: + size = pos.get('size') + existing_margin_mode = pos.get('margin_mode') + if size and float(size) > 0 and existing_margin_mode and existing_margin_mode != margin_mode: + await callback.answer( + f"⚠️ Маржинальный режим нельзя менять при открытой позиции " + f"(текущий режим: {existing_margin_mode})", show_alert=True) + return + + # Определяем сторону для открытия позиции + if trading_mode == 'Long': + side = 'Buy' + elif trading_mode == 'Short': + side = 'Sell' + else: + await message.answer(f"Режим торговли '{trading_mode}' пока не поддерживается.") + await callback.answer() + return + + # Сообщаем о начале торговли + await message.answer("Начинаю торговлю с использованием текущих настроек...") + + # Открываем позицию (вызывает Futures.open_position) + await open_position(tg_id, message, side=side, margin_mode=margin_mode) + + + # Проверяем таймер и информируем пользователя + timer_minutes = await rq.get_user_timer(tg_id) + if timer_minutes and timer_minutes > 0: + await message.answer(f"Торговля будет работать по таймеру: {timer_minutes} мин.") + asyncio.create_task(trading_cycle(tg_id, message)) + else: + await message.answer( + "Торговля начата без ограничения по времени. Для остановки нажмите кнопку 'Закрыть сделку'.", + reply_markup=inline_markup.create_close_deal_markup(symbol) + ) + + await callback.answer() + diff --git a/app/telegram/functions/functions.py b/app/telegram/functions/functions.py index 28e9134..0a1b5fe 100644 --- a/app/telegram/functions/functions.py +++ b/app/telegram/functions/functions.py @@ -27,4 +27,6 @@ async def check_profile_message(message, username): await message.answer(f'С возвращением, {username}!', reply_markup=reply_markup.base_buttons_markup) async def settings_message(message): - await message.edit_text("Выберите что настроить", reply_markup=inline_markup.special_settings_markup) \ No newline at end of file + await message.edit_text("Выберите что настроить", reply_markup=inline_markup.special_settings_markup) + + -- 2.50.1 From 12a00a1f3a722b06bafca826a314befea1347577 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Fri, 22 Aug 2025 16:48:09 +0500 Subject: [PATCH 25/77] Added a martingale step --- app/telegram/database/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/telegram/database/models.py b/app/telegram/database/models.py index ad6afec..0db91ef 100644 --- a/app/telegram/database/models.py +++ b/app/telegram/database/models.py @@ -77,6 +77,7 @@ class User_Main_Settings(Base): size_leverage = mapped_column(Integer(), default=1) starting_quantity = mapped_column(Integer(), default=1) martingale_factor = mapped_column(Integer(), default=1) + martingale_step = mapped_column(Integer(), default=1) maximal_quantity = mapped_column(Integer(), default=10) entry_order_type = mapped_column(String(10), default='Market') -- 2.50.1 From 54667db29bc9398d509785cafea10620edca4d8c Mon Sep 17 00:00:00 2001 From: algizn97 Date: Fri, 22 Aug 2025 16:48:17 +0500 Subject: [PATCH 26/77] Added a martingale step --- app/telegram/database/requests.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/telegram/database/requests.py b/app/telegram/database/requests.py index 00b02f7..8e30589 100644 --- a/app/telegram/database/requests.py +++ b/app/telegram/database/requests.py @@ -390,3 +390,15 @@ async def update_user_timer(tg_id, minutes: int): except Exception as e: logging.error(f"Ошибка обновления таймера пользователя {tg_id}: {e}") + +async def get_martingale_step(tg_id): + async with async_session() as session: + result = await session.execute(select(UMS).where(UMS.tg_id == tg_id)) + user_settings = result.scalars().first() + return user_settings.martingale_step + +async def update_martingale_step(tg_id, step): + async with async_session() as session: + await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(martingale_step = step)) + + await session.commit() \ No newline at end of file -- 2.50.1 From c4b35be0531fc7248d23e1694f45f24609c794f1 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Fri, 22 Aug 2025 16:49:17 +0500 Subject: [PATCH 27/77] Updated the risk management function --- .../risk_management_settings/settings.py | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/app/telegram/functions/risk_management_settings/settings.py b/app/telegram/functions/risk_management_settings/settings.py index 86dcc82..3a7e7ee 100644 --- a/app/telegram/functions/risk_management_settings/settings.py +++ b/app/telegram/functions/risk_management_settings/settings.py @@ -74,16 +74,37 @@ async def state_price_loss(message: Message, state): data_settings = await rq.get_user_risk_management_settings(message.from_user.id) if data['price_loss'].isdigit() and int(data['price_loss']) <= 100: - await message.answer(f"✅ Изменено: {data_settings['price_loss']}% → {data['price_loss']}%") + new_price_loss = int(data['price_loss']) + old_price_loss = int(data_settings.get('price_loss', 0)) + + current_price_profit = data_settings.get('price_profit') + # Пробуем перевести price_profit в число, если это возможно + try: + current_price_profit_num = int(current_price_profit) + except Exception: + current_price_profit_num = 0 + + # Флаг, если price_profit изначально равен 0 или совпадает со старым стоп-лоссом + should_update_profit = (current_price_profit_num == 0) or (current_price_profit_num == abs(old_price_loss)) + + # Обновляем стоп-лосс + await rq.update_price_loss(message.from_user.id, new_price_loss) + + # Если нужно, меняем тейк-профит + if should_update_profit: + new_price_profit = abs(new_price_loss) + await rq.update_price_profit(message.from_user.id, new_price_profit) + await message.answer(f"✅ Стоп-лосс изменён: {old_price_loss}% → {new_price_loss}%\n" + f"Тейк-профит автоматически установлен в: {new_price_profit}%") + else: + await message.answer(f"✅ Стоп-лосс изменён: {old_price_loss}% → {new_price_loss}%") - await rq.update_price_loss(message.from_user.id, data['price_loss']) await main_settings_message(message.from_user.id, message, state) - await state.clear() else: - await message.answer(f'⛔️ Ошибка: ваше значение ({data['price_loss']}%) или выше лимита (100) или вы вводите неверные символы') - - await main_settings_message(message.from_user.id, message, state) + await message.answer( + f'⛔️ Ошибка: ваше значение ({data["price_loss"]}%) выше лимита (100) или содержит неверные символы') + await main_settings_message(message.from_user.id, message, state) async def max_risk_deal_message(message, state): await state.set_state(update_risk_management_settings.max_risk_deal) -- 2.50.1 From f895c19b14cc8a330abfd28e8866d1c00bd918be Mon Sep 17 00:00:00 2001 From: algizn97 Date: Fri, 22 Aug 2025 16:57:59 +0500 Subject: [PATCH 28/77] fixed the method of getting the timer value. --- app/services/Bybit/functions/functions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/services/Bybit/functions/functions.py b/app/services/Bybit/functions/functions.py index ef87d6e..b5c315c 100644 --- a/app/services/Bybit/functions/functions.py +++ b/app/services/Bybit/functions/functions.py @@ -257,7 +257,9 @@ async def start_trading_process(callback: CallbackQuery, state: FSMContext): # Проверяем таймер и информируем пользователя - timer_minutes = await rq.get_user_timer(tg_id) + + 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)) -- 2.50.1 From 0746490786eb4d0fee291841fe092007b625adb8 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Sat, 23 Aug 2025 14:30:24 +0500 Subject: [PATCH 29/77] Delete func_min_qty.py and logs.py --- app/services/Bybit/functions/func_min_qty.py | 21 -------------------- app/telegram/logs.py | 8 -------- 2 files changed, 29 deletions(-) delete mode 100644 app/services/Bybit/functions/func_min_qty.py delete mode 100644 app/telegram/logs.py diff --git a/app/services/Bybit/functions/func_min_qty.py b/app/services/Bybit/functions/func_min_qty.py deleted file mode 100644 index 0544b64..0000000 --- a/app/services/Bybit/functions/func_min_qty.py +++ /dev/null @@ -1,21 +0,0 @@ -import app.telegram.database.requests as rq -import app.services.Bybit.functions.price_symbol as price_s - -from pybit.unified_trading import HTTP - -client = HTTP() - -async def get_min_qty(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 - ) - - price = await price_s.get_price(tg_id, message) - min_qty = int(5 / price * 1.1) - - return min_qty \ No newline at end of file diff --git a/app/telegram/logs.py b/app/telegram/logs.py deleted file mode 100644 index eaf7fa3..0000000 --- a/app/telegram/logs.py +++ /dev/null @@ -1,8 +0,0 @@ -import logging - -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) - -logger = logging.getLogger(__name__) \ No newline at end of file -- 2.50.1 From 8a2497bcac7905d87db9a30f585d78badd4fb482 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Sat, 23 Aug 2025 14:30:48 +0500 Subject: [PATCH 30/77] Added logger_helper.py --- logger_helper/logger_helper.py | 84 ++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 logger_helper/logger_helper.py diff --git a/logger_helper/logger_helper.py b/logger_helper/logger_helper.py new file mode 100644 index 0000000..8ff88be --- /dev/null +++ b/logger_helper/logger_helper.py @@ -0,0 +1,84 @@ +import os + +current_directory = os.path.dirname(os.path.abspath(__file__)) +log_directory = os.path.join(current_directory, 'loggers') +os.makedirs(log_directory, exist_ok=True) +log_filename = os.path.join(log_directory, 'app.log') + +LOGGING_CONFIG = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "default": { + "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + "datefmt": "%Y-%m-%d %H:%M:%S", # Формат даты + }, + }, + "handlers": { + "timed_rotating_file": { + "class": "logging.handlers.TimedRotatingFileHandler", + "filename": log_filename, + "when": "midnight", # Время ротации (каждую полночь) + "interval": 1, # Интервал в днях + "backupCount": 7, # Количество сохраняемых архивов (0 - не сохранять) + "formatter": "default", + "encoding": "utf-8", + }, + "console": { + "class": "logging.StreamHandler", + "formatter": "default", + }, + }, + "loggers": { + "main": { + "handlers": ["console", "timed_rotating_file"], + "level": "DEBUG", + "propagate": False, + }, + "add_bybit_api": { + "handlers": ["console", "timed_rotating_file"], + "level": "DEBUG", + "propagate": False, + }, + "balance": { + "handlers": ["console", "timed_rotating_file"], + "level": "DEBUG", + "propagate": False, + }, + "functions": { + "handlers": ["console", "timed_rotating_file"], + "level": "DEBUG", + "propagate": False, + }, + "futures": { + "handlers": ["console", "timed_rotating_file"], + "level": "DEBUG", + "propagate": False, + }, + "get_valid_symbol": { + "handlers": ["console", "timed_rotating_file"], + "level": "DEBUG", + "propagate": False, + }, + "min_qty": { + "handlers": ["console", "timed_rotating_file"], + "level": "DEBUG", + "propagate": False, + }, + "price_symbol": { + "handlers": ["console", "timed_rotating_file"], + "level": "DEBUG", + "propagate": False, + }, + "requests": { + "handlers": ["console", "timed_rotating_file"], + "level": "DEBUG", + "propagate": False, + }, + "handlers": { + "handlers": ["console", "timed_rotating_file"], + "level": "DEBUG", + "propagate": False, + }, + }, +} -- 2.50.1 From c7da20d577ccc306d140fff879f87b287843feea Mon Sep 17 00:00:00 2001 From: algizn97 Date: Sat, 23 Aug 2025 14:31:09 +0500 Subject: [PATCH 31/77] Update --- .env.sample | 4 +++- .gitignore | 3 --- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.env.sample b/.env.sample index f5fde1c..2637330 100644 --- a/.env.sample +++ b/.env.sample @@ -1 +1,3 @@ -TOKEN_TELEGRAM_BOT= \ No newline at end of file +TOKEN_TELEGRAM_BOT_1= +TOKEN_TELEGRAM_BOT_2= +TOKEN_TELEGRAM_BOT_3= diff --git a/.gitignore b/.gitignore index 2a4caf9..47f6ab0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,3 @@ __pycache__/ env/ venv/ .venv/ - -requirements.txt - -- 2.50.1 From 1ec9732607a6e1604d3b61a524f15b9c9ada0022 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Sat, 23 Aug 2025 14:31:45 +0500 Subject: [PATCH 32/77] Update loggers --- BybitBot_API.py | 18 +++++----- app/services/Bybit/functions/Add_Bybit_API.py | 6 +++- app/services/Bybit/functions/balance.py | 33 +++++++++++++++---- 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/BybitBot_API.py b/BybitBot_API.py index 6e3c9d7..60ef27f 100644 --- a/BybitBot_API.py +++ b/BybitBot_API.py @@ -1,24 +1,25 @@ import asyncio - +import logging.config from aiogram import Bot, Dispatcher from aiogram.filters import Command, CommandStart from aiogram.types import Message from app.telegram.database.models import async_main -from app.telegram.handlers.handlers import router -from app.telegram.functions.main_settings.settings import router_main_settings +from app.telegram.handlers.handlers import router +from app.telegram.functions.main_settings.settings import router_main_settings from app.telegram.functions.risk_management_settings.settings import router_risk_management_settings from app.telegram.functions.condition_settings.settings import condition_settings_router from app.services.Bybit.functions.Add_Bybit_API import router_register_bybit_api from app.services.Bybit.functions.functions import router_functions_bybit_trade +from logger_helper.logger_helper import LOGGING_CONFIG +from config import TOKEN_TG_BOT_1, TOKEN_TG_BOT_2, TOKEN_TG_BOT_3 -from config import TOKEN_TG_BOT +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("main") -from app.telegram.logs import logger - -bot = Bot(token=TOKEN_TG_BOT) +bot = Bot(token=TOKEN_TG_BOT_1) dp = Dispatcher() async def main(): @@ -35,6 +36,7 @@ async def main(): if __name__ == '__main__': try: + logger.info("Bot is on") asyncio.run(main()) except KeyboardInterrupt: - print("Bot is off") + logger.info("Bot is off") diff --git a/app/services/Bybit/functions/Add_Bybit_API.py b/app/services/Bybit/functions/Add_Bybit_API.py index 21761b0..5b4f614 100644 --- a/app/services/Bybit/functions/Add_Bybit_API.py +++ b/app/services/Bybit/functions/Add_Bybit_API.py @@ -1,5 +1,6 @@ from aiogram import F, Router - +import logging.config +from logger_helper.logger_helper import LOGGING_CONFIG import app.telegram.Keyboards.inline_keyboards as inline_markup import app.telegram.Keyboards.reply_keyboards as reply_markup @@ -10,6 +11,9 @@ from aiogram.types import Message, CallbackQuery from aiogram.fsm.state import State, StatesGroup from aiogram.fsm.context import FSMContext +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("add_bybit_api") + router_register_bybit_api = Router() class state_reg_bybit_api(StatesGroup): diff --git a/app/services/Bybit/functions/balance.py b/app/services/Bybit/functions/balance.py index 89daef4..3168bc0 100644 --- a/app/services/Bybit/functions/balance.py +++ b/app/services/Bybit/functions/balance.py @@ -1,12 +1,29 @@ import app.telegram.database.requests as rq import app.telegram.Keyboards.inline_keyboards as inline_markup - +import logging.config +from logger_helper.logger_helper import LOGGING_CONFIG from pybit.unified_trading import HTTP -import logging -logging.basicConfig(level=logging.DEBUG) +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("balance") -async def get_balance(tg_id, message): + +async def get_balance(tg_id: int, message) -> float: + """ + Асинхронно получает общий баланс пользователя на Bybit. + + Процедура: + - Получает API ключ и секрет пользователя из базы данных. + - Если ключи не заданы, отправляет пользователю сообщение с предложением подключить платформу. + - Создает клиент Bybit с ключами. + - Запрашивает общий баланс по типу аккаунта UNIFIED. + - Если ответ успешен, возвращает баланс в виде float. + - При ошибках API или исключениях логирует ошибку и уведомляет пользователя. + + :param tg_id: int - идентификатор пользователя Telegram + :param message: объект сообщения для отправки ответов пользователю + :return: float - общий баланс пользователя; 0 при ошибке или отсутствии ключей + """ api_key = await rq.get_bybit_api_key(tg_id) secret_key = await rq.get_bybit_secret_key(tg_id) @@ -16,7 +33,8 @@ async def get_balance(tg_id, message): ) if api_key == 'None' or secret_key == 'None': - await message.answer('⚠️ Подключите платформу для торговли', reply_markup=inline_markup.connect_bybit_api_markup) + await message.answer('⚠️ Подключите платформу для торговли', + reply_markup=inline_markup.connect_bybit_api_markup) return 0 try: @@ -25,9 +43,10 @@ async def get_balance(tg_id, message): total_balance = response['result']['list'][0].get('totalWalletBalance', '0') return total_balance else: + logger.error(f"Ошибка API: {response.get('retMsg')}") await message.answer(f"⚠️ Ошибка API: {response.get('retMsg')}") return 0 except Exception as e: - logging.error(f"Ошибка при получении общего баланса: {e}") + logger.error(f"Ошибка при получении общего баланса: {e}") await message.answer('⚠️ Ошибка при получении баланса') - return 0 \ No newline at end of file + return 0 -- 2.50.1 From 6c3f13f3727fbd1e6d3e3d4d1863fa2ed892f69f Mon Sep 17 00:00:00 2001 From: algizn97 Date: Sat, 23 Aug 2025 14:32:04 +0500 Subject: [PATCH 33/77] Update --- app/telegram/Keyboards/inline_keyboards.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/app/telegram/Keyboards/inline_keyboards.py b/app/telegram/Keyboards/inline_keyboards.py index f4149fb..8ec6f05 100644 --- a/app/telegram/Keyboards/inline_keyboards.py +++ b/app/telegram/Keyboards/inline_keyboards.py @@ -32,7 +32,16 @@ trading_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="Мои сделки", callback_data='clb_my_deals')], [InlineKeyboardButton(text="Указать торговую пару", callback_data='clb_update_trading_pair')], [InlineKeyboardButton(text="Выбрать тип входа", callback_data='clb_update_entry_type')], +]) + +start_trading_markup = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="На главную", callback_data='back_to_main')], [InlineKeyboardButton(text="Начать торговлю", callback_data="clb_start_chatbot_trading")], + +]) + +cancel = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="Отменить", callback_data="clb_cancel")] ]) entry_order_type_markup = InlineKeyboardMarkup( @@ -40,7 +49,7 @@ entry_order_type_markup = InlineKeyboardMarkup( [ InlineKeyboardButton(text="Market (текущая цена)", callback_data="entry_order_type:Market"), InlineKeyboardButton(text="Limit (фиксированная цена)", callback_data="entry_order_type:Limit"), - ] + ], ] ) @@ -51,8 +60,8 @@ back_btn_list_settings_markup = InlineKeyboardMarkup(inline_keyboard=[[InlineKey back_btn_to_main = [InlineKeyboardButton(text="На главную", callback_data='clb_back_to_main')] back_to_main = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text="На главную", callback_data='back_to_main')], - ]) + [InlineKeyboardButton(text="На главную", callback_data='back_to_main')], +]) main_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_trading_mode'), @@ -161,5 +170,5 @@ def create_close_deal_markup(symbol: str) -> InlineKeyboardMarkup: timer_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="Установить таймер", callback_data="clb_set_timer")], [InlineKeyboardButton(text="Остановить таймер", callback_data="clb_stop_timer")], - back_btn_to_main + back_btn_to_main ]) -- 2.50.1 From 2da06481f7d2b20437de000dc28f1df141a520ab Mon Sep 17 00:00:00 2001 From: algizn97 Date: Sat, 23 Aug 2025 14:32:11 +0500 Subject: [PATCH 34/77] Update --- config.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/config.py b/config.py index 230e0c2..a55e59e 100644 --- a/config.py +++ b/config.py @@ -1,6 +1,9 @@ -from dotenv import load_dotenv +from dotenv import load_dotenv, find_dotenv import os -load_dotenv('.env') +env_file = find_dotenv() +load_dotenv(env_file) -TOKEN_TG_BOT = os.getenv('TOKEN_TELEGRAM_BOT') \ No newline at end of file +TOKEN_TG_BOT_1 = os.getenv('TOKEN_TELEGRAM_BOT_1') +TOKEN_TG_BOT_2 = os.getenv('TOKEN_TELEGRAM_BOT_2') +TOKEN_TG_BOT_3 = os.getenv('TOKEN_TELEGRAM_BOT_3') \ No newline at end of file -- 2.50.1 From afe61ea7d617d3377361aac8068511c6df3e6b42 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Sat, 23 Aug 2025 14:33:33 +0500 Subject: [PATCH 35/77] Added documentation --- .../Bybit/functions/get_valid_symbol.py | 40 +++++++++++++ app/services/Bybit/functions/min_qty.py | 57 ++++++++++++++----- app/services/Bybit/functions/price_symbol.py | 27 ++++++--- 3 files changed, 101 insertions(+), 23 deletions(-) create mode 100644 app/services/Bybit/functions/get_valid_symbol.py diff --git a/app/services/Bybit/functions/get_valid_symbol.py b/app/services/Bybit/functions/get_valid_symbol.py new file mode 100644 index 0000000..1a8d830 --- /dev/null +++ b/app/services/Bybit/functions/get_valid_symbol.py @@ -0,0 +1,40 @@ +import logging.config +from pybit.unified_trading import HTTP +import app.telegram.database.requests as rq +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("get_valid_symbol") + + +async def get_valid_symbols(user_id: int, symbol: str) -> bool: + """ + Проверяет существование торговой пары на Bybit в категории 'linear'. + + Эта функция получает API-ключи пользователя из базы данных и + с помощью Bybit API проверяет наличие данного символа в списке + торговых инструментов категории 'linear'. + + Args: + user_id (int): Идентификатор пользователя Telegram. + symbol (str): Торговый символ (валютная пара), например "BTCUSDT". + + Returns: + bool: Возвращает True, если торговая пара существует, иначе False. + + Raises: + Исключения подавляются и вызывается False, если произошла ошибка запроса к API. + """ + api_key = await rq.get_bybit_api_key(user_id) + secret_key = await rq.get_bybit_secret_key(user_id) + client = HTTP(api_key=api_key, api_secret=secret_key) + + try: + resp = client.get_instruments_info(category='linear', symbol=symbol) + # Проверка наличия результата и непустого списка инструментов + if resp.get('retCode') == 0 and resp.get('result') and resp['result'].get('list'): + return len(resp['result']['list']) > 0 + return False + except Exception as e: + logging.error(f"Ошибка при получении списка инструментов: {e}") + return False diff --git a/app/services/Bybit/functions/min_qty.py b/app/services/Bybit/functions/min_qty.py index 07c1485..7cfb453 100644 --- a/app/services/Bybit/functions/min_qty.py +++ b/app/services/Bybit/functions/min_qty.py @@ -1,23 +1,52 @@ -from app.services.Bybit.functions import price_symbol +import math +import logging.config +from app.services.Bybit.functions.price_symbol import get_price import app.telegram.database.requests as rq - +from logger_helper.logger_helper import LOGGING_CONFIG from pybit.unified_trading import HTTP -client = HTTP() +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("min_qty") -async def get_min_qty(tg_id): +def round_up_qty(value: float, step: float) -> float: + """ + Округление value вверх до ближайшего кратного step. + """ + return math.ceil(value / step) * step + +async def get_min_qty(tg_id: int) -> float: + """ + Получает минимальный объем (количество) ордера для символа пользователя на Bybit, + округленное с учетом шага количества qtyStep. + + :param tg_id: int - идентификатор пользователя Telegram + :return: float - минимальное количество лота для ордера + """ 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) + symbol = await rq.get_symbol(tg_id) - client = HTTP( - api_key=api_key, - api_secret=secret_key - ) + client = HTTP(api_key=api_key, api_secret=secret_key) - price = await price_symbol(tg_id) - json_data = client.get_instruments_info(symbol=SYMBOL, category='linear') + price = await get_price(tg_id) - min_qty = int(5 / price * 1.1) # 1% 5 USDT - - return min_qty \ No newline at end of file + response = client.get_instruments_info(symbol=symbol, category='linear') + + instrument = response['result'][0] + lot_size_filter = instrument.get('lotSizeFilter', {}) + + min_order_qty = float(lot_size_filter.get('minOrderQty', 0)) + min_notional_value = float(lot_size_filter.get('minNotionalValue', 0)) + qty_step = float(lot_size_filter.get('qtyStep', 1)) + + calculated_qty = (5 / price) * 1.1 + + min_qty = max(min_order_qty, calculated_qty) + + min_qty_rounded = round_up_qty(min_qty, qty_step) + + logger.debug(f"tg_id={tg_id}: price={price}, min_order_qty={min_order_qty}, " + f"min_notional_value={min_notional_value}, qty_step={qty_step}, " + f"calculated_qty={calculated_qty}, min_qty_rounded={min_qty_rounded}") + + return min_qty_rounded diff --git a/app/services/Bybit/functions/price_symbol.py b/app/services/Bybit/functions/price_symbol.py index 8971a66..3421228 100644 --- a/app/services/Bybit/functions/price_symbol.py +++ b/app/services/Bybit/functions/price_symbol.py @@ -1,24 +1,33 @@ import app.telegram.database.requests as rq - +import logging.config +from logger_helper.logger_helper import LOGGING_CONFIG from pybit import exceptions from pybit.unified_trading import HTTP -client = HTTP() +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("price_symbol") -async def get_price(tg_id, message): + +async def get_price(tg_id: int) -> float: + """ + Асинхронно получает текущую цену символа пользователя на Bybit. + + :param tg_id: int - ID пользователя Telegram + :return: float - текущая цена символа + """ 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) + symbol = await rq.get_symbol(tg_id) client = HTTP( api_key=api_key, api_secret=secret_key ) - - try: - price = float(client.get_tickers(category='linear', symbol=SYMBOL).get('result').get('list')[0].get('ask1Price')) + try: + price = float( + client.get_tickers(category='linear', symbol=symbol).get('result').get('list')[0].get('ask1Price')) return price except exceptions.InvalidRequestError as e: - await message.answer('Неверно указана торговая пара') - return 1.0 \ No newline at end of file + logger.error(f"Ошибка при получении цены: {e}") + return 1.0 -- 2.50.1 From 89ea51107202552359cf9e31527f952df6560eb5 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Sat, 23 Aug 2025 14:34:19 +0500 Subject: [PATCH 36/77] Update --- app/services/Bybit/functions/Futures.py | 27 ++- app/services/Bybit/functions/functions.py | 87 +++++++-- app/telegram/database/models.py | 1 + app/telegram/database/requests.py | 215 ++++++++++++++-------- app/telegram/functions/functions.py | 4 +- app/telegram/handlers/handlers.py | 15 +- 6 files changed, 238 insertions(+), 111 deletions(-) diff --git a/app/services/Bybit/functions/Futures.py b/app/services/Bybit/functions/Futures.py index 360c7ca..11d15b4 100644 --- a/app/services/Bybit/functions/Futures.py +++ b/app/services/Bybit/functions/Futures.py @@ -1,13 +1,15 @@ import asyncio import time -import logging +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 +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("futures") async def info_access_open_deal(message, symbol, trade_mode, margin_mode, leverage, qty): human_margin_mode = 'Isolated' if margin_mode == 'ISOLATED_MARGIN' else 'Cross' @@ -44,6 +46,10 @@ async def open_position(tg_id, message, side: str, margin_mode: str): 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') + 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' @@ -51,8 +57,8 @@ async def open_position(tg_id, message, side: str, margin_mode: str): client = HTTP(api_key=api_key, api_secret=secret_key) try: - balance = await balance_g.get_balance(tg_id) - price = await price_symbol.get_price(tg_id, message) + 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) @@ -109,9 +115,10 @@ async def open_position(tg_id, message, side: str, margin_mode: str): category='linear', symbol=symbol, side=side, - orderType="Market", + 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, orderLinkId=f"deal_{symbol}_{int(time.time())}" @@ -125,11 +132,11 @@ async def open_position(tg_id, message, side: str, margin_mode: str): await message.answer(f"Ошибка открытия ордера: {response.get('ret_msg', 'неизвестная ошибка')}") except exceptions.InvalidRequestError as e: - logging.error(f"InvalidRequestError: {e}") + logger.error(f"InvalidRequestError: {e}") await message.answer('Ошибка: неверно указана торговая пара или параметры.') except Exception as e: - logging.error(f"Ошибка при совершении сделки: {e}") - await message.answer('⚠️ Ошибка при совершении сделки') + logger.error(f"Ошибка при совершении сделки: {e}") + async def trading_cycle(tg_id, message): @@ -179,7 +186,7 @@ async def get_active_positions(message, api_key, secret_key, symbol): if pos.get('size') and float(pos['size']) > 0: active_positions.append(pos) except Exception as e: - logging.error(f"Ошибка при получении позиций: {e}") + logger.error(f"Ошибка при получении позиций: {e}") await message.answer('⚠️ Ошибка при получении позиций') for sym in symbols: @@ -228,7 +235,7 @@ async def close_user_trade(tg_id: int, symbol: str) -> bool: ) return response['ret_code'] == 0 except Exception as e: - logging.error(f"Ошибка закрытия сделки {symbol} для пользователя {tg_id}: {e}") + logger.error(f"Ошибка закрытия сделки {symbol} для пользователя {tg_id}: {e}") return False diff --git a/app/services/Bybit/functions/functions.py b/app/services/Bybit/functions/functions.py index b5c315c..021d411 100644 --- a/app/services/Bybit/functions/functions.py +++ b/app/services/Bybit/functions/functions.py @@ -1,8 +1,8 @@ import asyncio - +import logging.config from aiogram import F, Router - -from app.services.Bybit.functions import Futures, func_min_qty +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.balance import get_balance import app.telegram.Keyboards.inline_keyboards as inline_markup @@ -15,6 +15,11 @@ from aiogram.types import Message, CallbackQuery from aiogram.fsm.state import State, StatesGroup from aiogram.fsm.context import FSMContext +from app.services.Bybit.functions.get_valid_symbol import get_valid_symbols + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("functions") + router_functions_bybit_trade = Router() @@ -30,6 +35,10 @@ class TradeSetup(StatesGroup): waiting_for_timer = State() waiting_for_positive_percent = State() +class state_limit_price(StatesGroup): + price = 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): @@ -48,7 +57,8 @@ async def clb_start_bybit_trade_message(callback: CallbackQuery, state: FSMConte Как начать торговлю? 1️⃣ Проверьте и тщательно настройте все параметры в вашем профиле. -2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару заглавными буквами, без лишних символов (например: BTCUSDT). +2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT). +3️⃣ Нажмите кнопку 'Выбрать тип входа' и после нажмите начать торговлю. ''' await callback.message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup) @@ -69,7 +79,8 @@ async def start_bybit_trade_message(message, state): Как начать торговлю? 1️⃣ Проверьте и тщательно настройте все параметры в вашем профиле. -2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару заглавными буквами, без лишних символов (например: BTCUSDT). +2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT). +3️⃣ Нажмите кнопку 'Выбрать тип входа' и после нажмите начать торговлю. ''' await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup) @@ -80,17 +91,23 @@ async def update_symbol_for_trade_message(callback: CallbackQuery, state: FSMCon await state.set_state(state_update_symbol.symbol) await callback.message.answer( - text='Укажите торговую пару заглавными буквами без пробелов и лишних символов (пример: BTCUSDT): ') + text='Укажите торговую пару заглавными буквами без пробелов и лишних символов (пример: BTCUSDT): ', + reply_markup=inline_markup.cancel) @router_functions_bybit_trade.message(state_update_symbol.symbol) async def update_symbol_for_trade(message: Message, state: FSMContext): + user_input = message.text.strip().upper() + + exists = await get_valid_symbols(message.from_user.id, user_input) + + if not exists: + await message.answer("Введена некорректная торговая пара или такой пары нет в списке. Попробуйте снова.") + return + await state.update_data(symbol=message.text) - - data = await state.get_data() - await message.answer('Пара была успешно обновлена') - await rq.update_symbol(message.from_user.id, data['symbol']) + await rq.update_symbol(message.from_user.id, user_input) await start_bybit_trade_message(message, state) await state.clear() @@ -111,15 +128,47 @@ async def entry_order_type_callback(callback: CallbackQuery, state: FSMContext): await callback.answer("Ошибка выбора", show_alert=True) return + if order_type == 'Limit': + await state.set_state(state_limit_price.price) + await callback.message.answer("Введите цену лимитного ордера:", reply_markup=inline_markup.cancel) + await callback.answer() + return + try: await state.update_data(entry_order_type=order_type) await rq.update_entry_order_type(callback.from_user.id, order_type) - await callback.answer("Тип входа в позицию был успешно обновлен") + await callback.message.answer(f"Выбран тип входа в позицию: {order_type}", + reply_markup=inline_markup.start_trading_markup) + await callback.answer() except Exception as e: - await callback.message.answer("Произошла ошибка при обновлении типа входа в позицию") + logger.error(f"Произошла ошибка при обновлении типа входа в позицию: {e}") + await callback.message.answer("Произошла ошибка при обновлении типа входа в позицию", + reply_markup=inline_markup.back_to_main) await state.clear() +@router_functions_bybit_trade.message(state_limit_price.price) +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) + 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) + + await message.answer(f"Цена лимитного ордера установлена: {price}", reply_markup=inline_markup.start_trading_markup) + 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 @@ -227,7 +276,8 @@ async def start_trading_process(callback: CallbackQuery, state: FSMContext): try: positions_resp = client.get_positions(category='linear', symbol=symbol) positions = positions_resp.get('result', {}).get('list', []) - except Exception: + except Exception as e: + logger.error(f"Ошибка при получении позиций: {e}") positions = [] for pos in positions: @@ -253,7 +303,10 @@ async def start_trading_process(callback: CallbackQuery, state: FSMContext): await message.answer("Начинаю торговлю с использованием текущих настроек...") # Открываем позицию (вызывает Futures.open_position) - await open_position(tg_id, message, side=side, margin_mode=margin_mode) + 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 # Проверяем таймер и информируем пользователя @@ -271,3 +324,9 @@ async def start_trading_process(callback: CallbackQuery, state: FSMContext): await callback.answer() + +@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 diff --git a/app/telegram/database/models.py b/app/telegram/database/models.py index 0db91ef..789a1a9 100644 --- a/app/telegram/database/models.py +++ b/app/telegram/database/models.py @@ -80,6 +80,7 @@ class User_Main_Settings(Base): martingale_step = mapped_column(Integer(), default=1) maximal_quantity = mapped_column(Integer(), default=10) entry_order_type = mapped_column(String(10), default='Market') + limit_order_price = mapped_column(String(20), nullable=True) class User_Risk_Management_Settings(Base): diff --git a/app/telegram/database/requests.py b/app/telegram/database/requests.py index 8e30589..142d281 100644 --- a/app/telegram/database/requests.py +++ b/app/telegram/database/requests.py @@ -1,8 +1,10 @@ -import logging +import logging.config +from logger_helper.logger_helper import LOGGING_CONFIG from datetime import datetime, timedelta from typing import Any -logger = logging.getLogger(__name__) +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("requests") from app.telegram.database.models import async_session from app.telegram.database.models import User_Telegram_Id as UTi @@ -10,17 +12,18 @@ from app.telegram.database.models import User_Main_Settings as UMS from app.telegram.database.models import User_Bybit_API as UBA from app.telegram.database.models import User_Symbol from app.telegram.database.models import User_Risk_Management_Settings as URMS -from app.telegram.database.models import User_Condition_Settings as UCS -from app.telegram.database.models import User_Additional_Settings as UAS +from app.telegram.database.models import User_Condition_Settings as UCS +from app.telegram.database.models import User_Additional_Settings as UAS from app.telegram.database.models import Trading_Mode from app.telegram.database.models import Margin_type from app.telegram.database.models import Trigger from app.telegram.database.models import USER_DEALS, UserTimer -import app.telegram.functions.functions as func # functions +import app.telegram.functions.functions as func # functions from sqlalchemy import select, delete, update + # SET_DB async def save_tg_id_new_user(tg_id): async with async_session() as session: @@ -29,10 +32,11 @@ async def save_tg_id_new_user(tg_id): if not user: session.add(UTi(tg_id=tg_id)) - logger.info("Новый пользователь был добавлен в бд") + logger.info("Новый пользователь был добавлен в бд %s", tg_id) await session.commit() + async def set_new_user_bybit_api(tg_id): async with async_session() as session: user = await session.scalar(select(UBA).where(UBA.tg_id == tg_id)) @@ -42,23 +46,23 @@ async def set_new_user_bybit_api(tg_id): tg_id=tg_id, )) - logger.info(f"Bybit был успешно подключен") - await session.commit() + async def set_new_user_symbol(tg_id): async with async_session() as session: user = await session.scalar(select(User_Symbol).where(User_Symbol.tg_id == tg_id)) if not user: session.add(User_Symbol( - tg_id=tg_id + tg_id=tg_id )) - logger.info(f"Symbol был успешно добавлен") + logger.info(f"Symbol был успешно добавлен %s", tg_id) await session.commit() + async def set_new_user_default_main_settings(tg_id, trading_mode, margin_type) -> None: async with async_session() as session: settings = await session.scalar(select(UMS).where(UMS.tg_id == tg_id)) @@ -67,13 +71,14 @@ async def set_new_user_default_main_settings(tg_id, trading_mode, margin_type) - session.add(UMS( tg_id=tg_id, trading_mode=trading_mode, - margin_type=margin_type, + margin_type=margin_type, )) - logger.info("Основные настройки нового пользователя были заполнены") + logger.info("Основные настройки нового пользователя были заполнены%s", tg_id) await session.commit() + async def set_new_user_default_risk_management_settings(tg_id) -> None: async with async_session() as session: settings = await session.scalar(select(URMS).where(URMS.tg_id == tg_id)) @@ -83,10 +88,11 @@ async def set_new_user_default_risk_management_settings(tg_id) -> None: tg_id=tg_id )) - logger.info("Риск-Менеджмент настройки нового пользователя были заполнены") + logger.info("Риск-Менеджмент настройки нового пользователя были заполнены %s", tg_id) await session.commit() + async def set_new_user_default_condition_settings(tg_id, trigger) -> None: async with async_session() as session: settings = await session.scalar(select(UCS).where(UCS.tg_id == tg_id)) @@ -94,13 +100,14 @@ async def set_new_user_default_condition_settings(tg_id, trigger) -> None: if not settings: session.add(UCS( tg_id=tg_id, - trigger=trigger + trigger=trigger )) - logger.info("Условные настройки нового пользователя были заполнены") + logger.info("Условные настройки нового пользователя были заполнены %s", tg_id) await session.commit() + async def set_new_user_default_additional_settings(tg_id) -> None: async with async_session() as session: settings = await session.scalar(select(UAS).where(UAS.tg_id == tg_id)) @@ -110,30 +117,35 @@ async def set_new_user_default_additional_settings(tg_id) -> None: tg_id=tg_id, )) - logger.info("Дополнительные настройки нового пользователя были заполнены") + logger.info("Дополнительные настройки нового пользователя были заполнены %s", tg_id) await session.commit() + # GET_DB async def check_user(tg_id): async with async_session() as session: user = await session.scalar(select(UTi).where(UTi.tg_id == tg_id)) - return user - + return user + + async def get_bybit_api_key(tg_id): async with async_session() as session: api_key = await session.scalar(select(UBA.api_key).where(UBA.tg_id == tg_id)) return api_key - + + async def get_bybit_secret_key(tg_id): async with async_session() as session: secret_key = await session.scalar(select(UBA.secret_key).where(UBA.tg_id == tg_id)) return secret_key - + + async def get_symbol(tg_id): async with async_session() as session: symbol = await session.scalar(select(User_Symbol.symbol).where(User_Symbol.tg_id == tg_id)) - return symbol + return symbol + async def get_user_trades(tg_id): async with async_session() as session: @@ -149,6 +161,7 @@ async def update_user_trades(tg_id, **kwargs): await session.execute(query) await session.commit() + async def delete_user_trade(tg_id: int, symbol: str): async with async_session() as session: await session.execute( @@ -158,34 +171,38 @@ async def delete_user_trade(tg_id: int, symbol: str): ) await session.commit() + async def get_for_registration_trading_mode(): async with async_session() as session: mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.id == 1)) return mode + async def get_for_registration_margin_type(): async with async_session() as session: type = await session.scalar(select(Margin_type.type).where(Margin_type.id == 1)) return type + async def get_for_registration_trigger(): async with async_session() as session: trigger = await session.scalar(select(Trigger.trigger).where(Trigger.id == 1)) return trigger + async def get_user_main_settings(tg_id): async with async_session() as session: user = await session.scalar(select(UMS).where(UMS.tg_id == tg_id)) - - if user: - logger.info("Получение основных настроек пользователя") - trading_mode = await session.scalar(select(UMS.trading_mode).where(UMS.tg_id == tg_id)) - margin_mode = await session.scalar(select(UMS.margin_type).where(UMS.tg_id == tg_id)) - size_leverage = await session.scalar(select(UMS.size_leverage).where(UMS.tg_id == tg_id)) - starting_quantity = await session.scalar(select(UMS.starting_quantity).where(UMS.tg_id == tg_id)) - martingale_factor = await session.scalar(select(UMS.martingale_factor).where(UMS.tg_id == tg_id)) - maximal_quantity = await session.scalar(select(UMS.maximal_quantity).where(UMS.tg_id == tg_id)) + if user: + logger.info("Получение основных настроек пользователя %s", tg_id) + + trading_mode = await session.scalar(select(UMS.trading_mode).where(UMS.tg_id == tg_id)) + margin_mode = await session.scalar(select(UMS.margin_type).where(UMS.tg_id == tg_id)) + size_leverage = await session.scalar(select(UMS.size_leverage).where(UMS.tg_id == tg_id)) + starting_quantity = await session.scalar(select(UMS.starting_quantity).where(UMS.tg_id == tg_id)) + martingale_factor = await session.scalar(select(UMS.martingale_factor).where(UMS.tg_id == tg_id)) + maximal_quantity = await session.scalar(select(UMS.maximal_quantity).where(UMS.tg_id == tg_id)) data = { 'trading_mode': trading_mode, @@ -198,12 +215,13 @@ async def get_user_main_settings(tg_id): return data + async def get_user_risk_management_settings(tg_id): async with async_session() as session: user = await session.scalar(select(URMS).where(URMS.tg_id == tg_id)) if user: - logger.info("Получение риск-менеджмента настроек пользователя") + logger.info("Получение риск-менеджмента настроек пользователя %s", tg_id) price_profit = await session.scalar(select(URMS.price_profit).where(URMS.tg_id == tg_id)) price_loss = await session.scalar(select(URMS.price_loss).where(URMS.tg_id == tg_id)) @@ -219,88 +237,100 @@ async def get_user_risk_management_settings(tg_id): return data -#UPDATE_SYMBOL + +# UPDATE_SYMBOL async def update_symbol(tg_id, symbol) -> None: async with async_session() as session: - await session.execute(update(User_Symbol).where(User_Symbol.tg_id == tg_id).values(symbol = symbol)) - + await session.execute(update(User_Symbol).where(User_Symbol.tg_id == tg_id).values(symbol=symbol)) + await session.commit() + async def update_api_key(tg_id, api): async with async_session() as session: - api_key = await session.execute(update(UBA).where(UBA.tg_id == tg_id).values(api_key = api)) + api_key = await session.execute(update(UBA).where(UBA.tg_id == tg_id).values(api_key=api)) await session.commit() + async def update_secret_key(tg_id, api): async with async_session() as session: - secret_key = await session.execute(update(UBA).where(UBA.tg_id == tg_id).values(secret_key = api)) + secret_key = await session.execute(update(UBA).where(UBA.tg_id == tg_id).values(secret_key=api)) await session.commit() + # UPDATE_MAIN_SETTINGS_DB async def update_trade_mode_user(tg_id, trading_mode) -> None: async with async_session() as session: mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.mode == trading_mode)) if mode: - logger.info("Изменен трейд мод") - await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(trading_mode = mode)) + logger.info("Изменен трейд мод %s", tg_id) + await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(trading_mode=mode)) await session.commit() + async def update_margin_type(tg_id, margin_type) -> None: async with async_session() as session: type = await session.scalar(select(Margin_type.type).where(Margin_type.type == margin_type)) if type: - logger.info("Изменен тип маржи") - await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(margin_type = type)) + logger.info("Изменен тип маржи %s", tg_id) + await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(margin_type=type)) await session.commit() + async def update_size_leverange(tg_id, num): async with async_session() as session: - await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(size_leverage = num)) - + await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(size_leverage=num)) + await session.commit() + async def update_starting_quantity(tg_id, num): async with async_session() as session: - await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(starting_quantity = num)) - + await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(starting_quantity=num)) + await session.commit() + async def update_martingale_factor(tg_id, num): async with async_session() as session: - await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(martingale_factor = num)) - + await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(martingale_factor=num)) + await session.commit() + async def update_maximal_quantity(tg_id, num): async with async_session() as session: - await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(maximal_quantity = num)) - + await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(maximal_quantity=num)) + await session.commit() + # UPDATE_RISK_MANAGEMENT_SETTINGS_DB async def update_price_profit(tg_id, num): async with async_session() as session: - await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(price_profit = num)) - + await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(price_profit=num)) + await session.commit() + async def update_price_loss(tg_id, num): async with async_session() as session: - await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(price_loss = num)) - + await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(price_loss=num)) + await session.commit() + async def update_max_risk_deal(tg_id, num): async with async_session() as session: - await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(max_risk_deal = num)) - + await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(max_risk_deal=num)) + await session.commit() @@ -323,40 +353,66 @@ async def get_entry_order_type(tg_id: object) -> str | None | Any: return order_type or 'Market' +async def get_limit_price(tg_id): + async with async_session() as session: + result = await session.execute( + select(UMS.limit_order_price) + .where(UMS.tg_id == tg_id) + ) + price = result.scalar_one_or_none() + if price: + try: + return float(price) + except ValueError: + return None + return None + + +async def update_limit_price(tg_id, price): + async with async_session() as session: + await session.execute( + update(UMS) + .where(UMS.tg_id == tg_id) + .values(limit_order_price=str(price)) + ) + await session.commit() + + async def update_commission_fee(tg_id, num): async with async_session() as session: - await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(commission_fee = num)) + await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(commission_fee=num)) await session.commit() + async def get_user_timer(tg_id): - async with async_session() as session: - result = await session.execute(select(UserTimer).where(UserTimer.tg_id == tg_id)) - user_timer = result.scalars().first() + async with async_session() as session: + result = await session.execute(select(UserTimer).where(UserTimer.tg_id == tg_id)) + user_timer = result.scalars().first() - if not user_timer: - logging.info(f"No timer found for user {tg_id}") - return None + if not user_timer: + logging.info(f"No timer found for user {tg_id}") + return None - timer_minutes = user_timer.timer_minutes - timer_start = user_timer.timer_start - timer_end = user_timer.timer_end + timer_minutes = user_timer.timer_minutes + timer_start = user_timer.timer_start + timer_end = user_timer.timer_end - logging.info(f"Timer data for tg_id={tg_id}: " - f"timer_minutes={timer_minutes}, " - f"timer_start={timer_start}, " - f"timer_end={timer_end}") + logging.info(f"Timer data for tg_id={tg_id}: " + f"timer_minutes={timer_minutes}, " + f"timer_start={timer_start}, " + f"timer_end={timer_end}") - remaining = None - if timer_end: - remaining = max(0, int((timer_end - datetime.utcnow()).total_seconds() // 60)) + remaining = None + if timer_end: + remaining = max(0, int((timer_end - datetime.utcnow()).total_seconds() // 60)) - return { - "timer_minutes": timer_minutes, - "timer_start": timer_start, - "timer_end": timer_end, - "remaining_minutes": remaining - } + return { + "timer_minutes": timer_minutes, + "timer_start": timer_start, + "timer_end": timer_end, + "remaining_minutes": remaining + } async def update_user_timer(tg_id, minutes: int): @@ -397,8 +453,9 @@ async def get_martingale_step(tg_id): user_settings = result.scalars().first() return user_settings.martingale_step + async def update_martingale_step(tg_id, step): async with async_session() as session: - await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(martingale_step = step)) + await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(martingale_step=step)) - await session.commit() \ No newline at end of file + await session.commit() diff --git a/app/telegram/functions/functions.py b/app/telegram/functions/functions.py index 0a1b5fe..28e9134 100644 --- a/app/telegram/functions/functions.py +++ b/app/telegram/functions/functions.py @@ -27,6 +27,4 @@ async def check_profile_message(message, username): await message.answer(f'С возвращением, {username}!', reply_markup=reply_markup.base_buttons_markup) async def settings_message(message): - await message.edit_text("Выберите что настроить", reply_markup=inline_markup.special_settings_markup) - - + await message.edit_text("Выберите что настроить", reply_markup=inline_markup.special_settings_markup) \ No newline at end of file diff --git a/app/telegram/handlers/handlers.py b/app/telegram/handlers/handlers.py index e1cb05f..fcdea72 100644 --- a/app/telegram/handlers/handlers.py +++ b/app/telegram/handlers/handlers.py @@ -1,4 +1,4 @@ -import logging +import logging.config from aiogram import F, Router from aiogram.filters import CommandStart, Command @@ -14,6 +14,11 @@ import app.telegram.functions.additional_settings.settings as func_additional_se import app.telegram.database.requests as rq import app.telegram.Keyboards.inline_keyboards as inline_markup import app.telegram.Keyboards.reply_keyboards as reply_markup +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("handlers") + router = Router() @@ -143,7 +148,7 @@ async def clb_main_settings_msg(callback: CallbackQuery, state: FSMContext): case 'clb_change_maximum_quantity': await func_main_settings.maximum_quantity_message(callback.message, state) except Exception as e: - logging.error(f"Error callback in main_settings match-case: {e}") + logger.error(f"Error callback in main_settings match-case: {e}") list_risk_management_settings = ['clb_change_price_profit', @@ -168,7 +173,7 @@ async def clb_risk_management_settings_msg(callback: CallbackQuery, state: FSMCo case 'commission_fee': await func_rmanagement_settings.commission_fee_message(callback.message, state) except Exception as e: - logging.error(f"Error callback in risk_management match-case: {e}") + logger.error(f"Error callback in risk_management match-case: {e}") list_condition_settings = ['clb_change_trigger', @@ -202,7 +207,7 @@ async def clb_condition_settings_msg(callback: CallbackQuery, state: FSMContext) case 'clb_change_ai_analytics': await func_condition_settings.ai_analytics_message(callback.message, state) except Exception as e: - logging.error(f"Error callback in main_settings match-case: {e}") + logger.error(f"Error callback in main_settings match-case: {e}") list_additional_settings = ['clb_change_save_pattern', @@ -224,4 +229,4 @@ async def clb_additional_settings_msg(callback: CallbackQuery, state: FSMContext case 'clb_change_notifications': await func_additional_settings.notifications_message(callback.message, state) except Exception as e: - logging.error(f"Error callback in additional_settings match-case: {e}") + logger.error(f"Error callback in additional_settings match-case: {e}") -- 2.50.1 From 626766301532c46cd77a54b0c4ffed4e29022b62 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Sat, 23 Aug 2025 14:34:38 +0500 Subject: [PATCH 37/77] Added States.py --- app/states/States.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/states/States.py diff --git a/app/states/States.py b/app/states/States.py new file mode 100644 index 0000000..e69de29 -- 2.50.1 From dd63f4c0159c776201ad89a1d72ca1592ff0858a Mon Sep 17 00:00:00 2001 From: algizn97 Date: Sat, 23 Aug 2025 14:41:23 +0500 Subject: [PATCH 38/77] added --- .idea/.gitignore | 5 + .idea/dbnavigator.xml | 610 ++++++++++++++++++ .idea/inspectionProfiles/Project_Default.xml | 44 ++ .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 7 + .idea/modules.xml | 8 + .idea/stcs.iml | 8 + .idea/vcs.xml | 6 + db.sqlite3 | Bin 0 -> 65536 bytes logger_helper/loggers/app.log | 35 + requirements.txt | 29 + 11 files changed, 758 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/dbnavigator.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/stcs.iml create mode 100644 .idea/vcs.xml create mode 100644 db.sqlite3 create mode 100644 logger_helper/loggers/app.log create mode 100644 requirements.txt diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/dbnavigator.xml b/.idea/dbnavigator.xml new file mode 100644 index 0000000..f8fbf64 --- /dev/null +++ b/.idea/dbnavigator.xml @@ -0,0 +1,610 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
\ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..59be94b --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,44 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..3b04bb5 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..742f0a6 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/stcs.iml b/.idea/stcs.iml new file mode 100644 index 0000000..f420542 --- /dev/null +++ b/.idea/stcs.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..4466452ceacbc88a45e143107ba35b88a640b6c2 GIT binary patch literal 65536 zcmeI*?Qh%09S86`Q86X?;asEvkpf1T8g4A0>FT6OI}}B#xQyr7mK#|vn!O=t>7=lc zM4P0YXzvsU-LO~t2Zr_a-fS2!pdGdr*dMUU?^^U#w^uFDu2=(X*sI-@Q(k)97&1<~sT6I~t&OVv!!xtu0 z?czoHNb4h%xm61df;Bcs5TqYLz-!hwoSOblt}y?TTw!+ZwV=$NUFQ9?m$NZMXOlO~ zP1i2*p<%ptOBU9P?tPp>yT+KR zz5Y6V|6*ULH7-RB#39p_%}K(D+Ul)>|996Z21^Vcjem%Ojch9VgLRWcR^tO0y9+WvYSf>Z= zdW4z{#so1kI2F6s>t(oMdd=~AL##}#TMx|OpmNCS4iYk>C_zrhPbw|yDaYeLsb8Fjb__Bj^PJf*Tlp~;3<7UMw`Hp))rswm`c-8~EF8bB5 z#}rM?ZJLf-mOVCB(y$}d?7HoD^22jQ_Y`v_+~N4@Qid2i$%frNu-(_Gg7F~85IsxU z51gUvRu4aR-$=T+tBg3C$+ni#sx~u2AKvM=q40zno^3konZb6`$vHxc8GfQZK1C5t zEv0JNEZsfZk7lE}A*#{Zw1ekYy{+_MM7_Kt#3UZqyvLipLAd+ugO^7%u_aCVv+WZtHcB9N6 zcw6E$$4&X1A=oy%B{1i@YpK>`GH8CSQV>D3iALAY#3G6291=~uPycWrogVS(nGtPi z=Aam&$2RApTO+Nr301pviMDvBHX>a$I#_y2ESgx>A>j-gXAi5FU~I@_f2@pXE0+!m zCw6dIFOf3K*>g%xaat->wab@jE7$K;e*gJ?IB)51Ct~64&pjl<{ZpI$(PgsZqBNBK zhln365P$##AOHafKmY;|fB*y_009V`ECH1&Bzf((|1Qh$Ur#nED9s&@600bZa0SG_<0uX=z1Rwx`@C^aM{68_O zOr^W?e|i01VILFrFZOqFg9QQ*fB*y_009U<00Izz00bZafhSI2Li}(4bTRyjfp^3= z8eI_o;BT~l-Tvd=!}iDRzqJ1#Za>`nMf*>Ct@fYWAGSYY;@baA`-8n*k@#1!%SZD1 zzrsEt>=X7W`|gPg5Cwt&1Rwwb2tWV=5P$##AOHafK!6aYOw-8xU(pqV9yb3^Fl8nk zo&Sg5|0n1F;Di7KAOHafKmY;|fB*y_009U<;0YDDLC=un+`9ZGze?R*&Tm~Jo2tWV= z5P$##AOHafKmY;|fWWB}n5JVSd2T8#7J8n@S-eK>z{}fB*y_ z009U<00Izz00d6Dz!c4p({jF9aX}0SG_<0uX=z1Rwwb2%JWN{{!E5>S+J~ literal 0 HcmV?d00001 diff --git a/logger_helper/loggers/app.log b/logger_helper/loggers/app.log new file mode 100644 index 0000000..eb7755d --- /dev/null +++ b/logger_helper/loggers/app.log @@ -0,0 +1,35 @@ +2025-08-23 12:57:26 - main - INFO - Bot is off +2025-08-23 13:04:01 - main - INFO - Bot is off +2025-08-23 13:25:04 - main - INFO - Bot is off +2025-08-23 13:26:24 - main - INFO - Bot is off +2025-08-23 13:28:36 - main - INFO - Bot is off +2025-08-23 13:29:29 - main - INFO - Bot is off +2025-08-23 13:30:48 - main - INFO - Bot is off +2025-08-23 13:31:43 - main - INFO - Bot is off +2025-08-23 13:33:10 - main - INFO - Bot is off +2025-08-23 13:34:59 - main - INFO - Bot is off +2025-08-23 13:36:15 - main - INFO - Bot is off +2025-08-23 13:49:17 - main - INFO - Bot is off +2025-08-23 13:50:22 - main - INFO - Bot is on +2025-08-23 13:51:30 - main - INFO - Bot is off +2025-08-23 13:51:37 - main - INFO - Bot is on +2025-08-23 13:52:12 - main - INFO - Bot is off +2025-08-23 13:57:48 - main - INFO - Bot is on +2025-08-23 14:05:36 - main - INFO - Bot is off +2025-08-23 14:05:43 - main - INFO - Bot is on +2025-08-23 14:06:03 - main - INFO - Bot is off +2025-08-23 14:06:46 - main - INFO - Bot is on +2025-08-23 14:07:04 - requests - INFO - Bybit был успешно подключен +2025-08-23 14:07:43 - requests - INFO - Новый пользователь был добавлен в бд +2025-08-23 14:07:43 - requests - INFO - Основные настройки нового пользователя были заполнены +2025-08-23 14:07:43 - requests - INFO - Риск-Менеджмент настройки нового пользователя были заполнены +2025-08-23 14:07:43 - requests - INFO - Условные настройки нового пользователя были заполнены +2025-08-23 14:07:43 - requests - INFO - Дополнительные настройки нового пользователя были заполнены +2025-08-23 14:23:31 - main - INFO - Bot is off +2025-08-23 14:23:39 - main - INFO - Bot is on +2025-08-23 14:28:13 - main - INFO - Bot is off +2025-08-23 14:28:19 - main - INFO - Bot is on +2025-08-23 14:28:26 - requests - INFO - Получение риск-менеджмента настроек пользователя 899674724 +2025-08-23 14:28:26 - requests - INFO - Получение риск-менеджмента настроек пользователя 899674724 +2025-08-23 14:29:12 - requests - INFO - Получение риск-менеджмента настроек пользователя 899674724 +2025-08-23 14:29:34 - main - INFO - Bot is off diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4e876bb --- /dev/null +++ b/requirements.txt @@ -0,0 +1,29 @@ +aiofiles==24.1.0 +aiogram==3.22.0 +aiohappyeyeballs==2.6.1 +aiohttp==3.12.15 +aiosignal==1.4.0 +aiosqlite==0.21.0 +annotated-types==0.7.0 +attrs==25.3.0 +certifi==2025.8.3 +charset-normalizer==3.4.3 +dotenv==0.9.9 +frozenlist==1.7.0 +greenlet==3.2.4 +idna==3.10 +magic-filter==1.0.12 +multidict==6.6.4 +propcache==0.3.2 +pybit==5.11.0 +pycryptodome==3.23.0 +pydantic==2.11.7 +pydantic_core==2.33.2 +python-dotenv==1.1.1 +requests==2.32.5 +SQLAlchemy==2.0.43 +typing-inspection==0.4.1 +typing_extensions==4.14.1 +urllib3==2.5.0 +websocket-client==1.8.0 +yarl==1.20.1 -- 2.50.1 From 39b8d17498523c096b7668f409e50a063ee92bc0 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Mon, 25 Aug 2025 17:07:41 +0500 Subject: [PATCH 39/77] Updated timer functions --- app/services/Bybit/functions/Futures.py | 409 +++++++++++++++------- app/services/Bybit/functions/functions.py | 247 ++++++++----- 2 files changed, 451 insertions(+), 205 deletions(-) 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() -- 2.50.1 From 61979653e063d53102c04ac9d0b078363d3a336c Mon Sep 17 00:00:00 2001 From: algizn97 Date: Mon, 25 Aug 2025 17:07:50 +0500 Subject: [PATCH 40/77] Updated --- logger_helper/logger_helper.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/logger_helper/logger_helper.py b/logger_helper/logger_helper.py index 8ff88be..cde13eb 100644 --- a/logger_helper/logger_helper.py +++ b/logger_helper/logger_helper.py @@ -80,5 +80,10 @@ LOGGING_CONFIG = { "level": "DEBUG", "propagate": False, }, + "conditions_settings": { + "handlers": ["console", "timed_rotating_file"], + "level": "DEBUG", + "propagate": False, + }, }, } -- 2.50.1 From 964c0a09b8ec5a8159323f43c03705697f0cf64a Mon Sep 17 00:00:00 2001 From: algizn97 Date: Mon, 25 Aug 2025 17:08:04 +0500 Subject: [PATCH 41/77] Added keyboard --- app/telegram/Keyboards/inline_keyboards.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/telegram/Keyboards/inline_keyboards.py b/app/telegram/Keyboards/inline_keyboards.py index 8ec6f05..c0acda3 100644 --- a/app/telegram/Keyboards/inline_keyboards.py +++ b/app/telegram/Keyboards/inline_keyboards.py @@ -71,7 +71,8 @@ main_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ InlineKeyboardButton(text='Начальная ставка', callback_data='clb_change_starting_quantity')], [InlineKeyboardButton(text='Коэффициент Мартингейла', callback_data='clb_change_martingale_factor'), - InlineKeyboardButton(text='Максимальное кол-во ставок', callback_data='clb_change_maximum_quantity')], + InlineKeyboardButton(text='Сбросить шаги Мартингейла', callback_data='clb_change_martingale_reset')], + [InlineKeyboardButton(text='Максимальное кол-во ставок', callback_data='clb_change_maximum_quantity')], back_btn_list_settings, back_btn_to_main @@ -163,12 +164,16 @@ def create_trades_inline_keyboard(trades): def create_close_deal_markup(symbol: str) -> InlineKeyboardMarkup: return InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="Закрыть сделку", callback_data=f"close_deal:{symbol}")], + [InlineKeyboardButton(text="Закрыть по таймеру", callback_data=f"close_deal_by_timer:{symbol}")], back_btn_to_main ]) timer_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="Установить таймер", callback_data="clb_set_timer")], - [InlineKeyboardButton(text="Остановить таймер", callback_data="clb_stop_timer")], back_btn_to_main ]) + +cancel_start_markup = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="Отменить запуск", callback_data="clb_stop_timer")] +]) -- 2.50.1 From 554166eeaff4d5b819f4923b46b650f7e16b904f Mon Sep 17 00:00:00 2001 From: algizn97 Date: Mon, 25 Aug 2025 17:08:14 +0500 Subject: [PATCH 42/77] update --- app/telegram/database/requests.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/telegram/database/requests.py b/app/telegram/database/requests.py index 142d281..dc86469 100644 --- a/app/telegram/database/requests.py +++ b/app/telegram/database/requests.py @@ -203,6 +203,9 @@ async def get_user_main_settings(tg_id): starting_quantity = await session.scalar(select(UMS.starting_quantity).where(UMS.tg_id == tg_id)) martingale_factor = await session.scalar(select(UMS.martingale_factor).where(UMS.tg_id == tg_id)) maximal_quantity = await session.scalar(select(UMS.maximal_quantity).where(UMS.tg_id == tg_id)) + entry_order_type = await session.scalar(select(UMS.entry_order_type).where(UMS.tg_id == tg_id)) + limit_order_price = await session.scalar(select(UMS.limit_order_price).where(UMS.tg_id == tg_id)) + martingale_step = await session.scalar(select(UMS.martingale_step).where(UMS.tg_id == tg_id)) data = { 'trading_mode': trading_mode, @@ -210,7 +213,10 @@ async def get_user_main_settings(tg_id): 'size_leverage': size_leverage, 'starting_quantity': starting_quantity, 'martingale_factor': martingale_factor, - 'maximal_quantity': maximal_quantity + 'maximal_quantity': maximal_quantity, + 'entry_order_type': entry_order_type, + 'limit_order_price': limit_order_price, + 'martingale_step': martingale_step, } return data -- 2.50.1 From 73f0c675648f9797084a3d00cd22c7479c462a42 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Mon, 25 Aug 2025 17:08:33 +0500 Subject: [PATCH 43/77] updated timer functions --- .../functions/condition_settings/settings.py | 26 ++++++++++--------- .../functions/main_settings/settings.py | 1 + 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/app/telegram/functions/condition_settings/settings.py b/app/telegram/functions/condition_settings/settings.py index 79fc57f..6b385d1 100644 --- a/app/telegram/functions/condition_settings/settings.py +++ b/app/telegram/functions/condition_settings/settings.py @@ -1,10 +1,19 @@ -import app.telegram.Keyboards.inline_keyboards as inline_markup +import asyncio +import logging.config +import app.telegram.Keyboards.inline_keyboards as inline_markup from aiogram import Router, F from aiogram.types import Message, CallbackQuery from aiogram.fsm.context import FSMContext import app.telegram.database.requests as rq from aiogram.fsm.state import State, StatesGroup +from app.services.Bybit.functions.Futures import trading_cycle + +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("condition_settings") + condition_settings_router = Router() @@ -47,7 +56,7 @@ async def trigger_message(message, state): await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trigger_markup) -async def timer_message(id,message: Message, state: FSMContext): +async def timer_message(id, message: Message, state: FSMContext): await state.set_state(condition_settings.timer) timer_info = await rq.get_user_timer(id) @@ -56,8 +65,7 @@ async def timer_message(id,message: Message, state: FSMContext): return await message.answer( - f"Таймер: {timer_info['timer_minutes']} мин\n" - f"Осталось: {timer_info['remaining_minutes']} мин\n", + f"Таймер: {timer_info['timer_minutes']} мин\n", reply_markup=inline_markup.timer_markup ) @@ -77,21 +85,15 @@ async def process_timer_input(message: Message, state: FSMContext): await message.reply("Введите число больше нуля.") return - # Сохраняем в базу или память время таймера для пользователя await rq.update_user_timer(message.from_user.id, minutes) + await message.answer(f"Таймер установлен на {minutes} минут.\nНажмите кнопку 'Начать торговлю' для запуска.", + reply_markup=inline_markup.start_trading_markup) - await message.answer(f"Таймер установлен на {minutes} минут.", reply_markup=inline_markup.back_to_main) await state.clear() except ValueError: await message.reply("Пожалуйста, введите корректное число.") -@condition_settings_router.callback_query(F.data == "clb_stop_timer") -async def stop_timer_callback(callback: CallbackQuery): - await rq.update_user_timer(callback.from_user.id, 0) # обнуляем таймер - await callback.message.answer("Таймер остановлен.", reply_markup=inline_markup.back_to_main) - await callback.answer() - async def filter_volatility_message(message, state): text = '''Фильтр волатильности diff --git a/app/telegram/functions/main_settings/settings.py b/app/telegram/functions/main_settings/settings.py index 8ab3c03..2365967 100644 --- a/app/telegram/functions/main_settings/settings.py +++ b/app/telegram/functions/main_settings/settings.py @@ -38,6 +38,7 @@ async def main_settings_message(id, message, state): - Размер кредитного плеча: х{data['size_leverage']} - Начальная ставка: {data['starting_quantity']} - Коэффициент мартингейла: {data['martingale_factor']} +- Количество ставок в серии: {data['martingale_step']} - Максимальное количество ставок в серии: {data['maximal_quantity']} """, parse_mode='html', reply_markup=inline_markup.main_settings_markup) -- 2.50.1 From d29b4465ad6165098914b0b5acc6f0641e1b1988 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Tue, 26 Aug 2025 19:10:43 +0500 Subject: [PATCH 44/77] added documentation --- app/services/Bybit/functions/Add_Bybit_API.py | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/app/services/Bybit/functions/Add_Bybit_API.py b/app/services/Bybit/functions/Add_Bybit_API.py index 5b4f614..2ee471e 100644 --- a/app/services/Bybit/functions/Add_Bybit_API.py +++ b/app/services/Bybit/functions/Add_Bybit_API.py @@ -7,8 +7,7 @@ import app.telegram.Keyboards.reply_keyboards as reply_markup import app.telegram.database.requests as rq from aiogram.types import Message, CallbackQuery -# FSM - Механизм состояния -from aiogram.fsm.state import State, StatesGroup +from app.states.States import state_reg_bybit_api from aiogram.fsm.context import FSMContext logging.config.dictConfig(LOGGING_CONFIG) @@ -16,12 +15,13 @@ logger = logging.getLogger("add_bybit_api") router_register_bybit_api = Router() -class state_reg_bybit_api(StatesGroup): - api_key = State() - secret_key = State() @router_register_bybit_api.callback_query(F.data == 'clb_new_user_connect_bybit_api_message') -async def info_for_bybit_api_message(callback: CallbackQuery): +async def info_for_bybit_api_message(callback: CallbackQuery) -> None: + """ + Отвечает пользователю подробной инструкцией по подключению аккаунта Bybit. + Показывает как создать API ключ и передать его чат-боту. + """ text = '''Подключение Bybit аккаунта 1. Зарегистрируйтесь или войдите в свой аккаунт на Bybit (https://www.bybit.com/). @@ -43,36 +43,50 @@ async def info_for_bybit_api_message(callback: CallbackQuery): await callback.answer() + @router_register_bybit_api.callback_query(F.data == 'clb_new_user_connect_bybit_api') -async def add_api_key_message(callback: CallbackQuery, state: FSMContext): +async def add_api_key_message(callback: CallbackQuery, state: FSMContext) -> None: + """ + Инициирует процесс добавления API ключа. + Переводит пользователя в состояние ожидания ввода API Key. + """ await state.set_state(state_reg_bybit_api.api_key) text = 'Отправьте KEY_API ниже: ' await callback.message.answer(text=text) + @router_register_bybit_api.message(state_reg_bybit_api.api_key) -async def add_api_key_and_message_for_secret_key(message: Message, state: FSMContext): - await state.update_data(api_key = message.text) - +async def add_api_key_and_message_for_secret_key(message: Message, state: FSMContext) -> None: + """ + Сохраняет API Key во временное состояние FSM, + затем запрашивает у пользователя ввод Secret Key. + """ + await state.update_data(api_key=message.text) + text = 'Отправьте SECRET_KEY ниже' await message.answer(text=text) await state.set_state(state_reg_bybit_api.secret_key) + @router_register_bybit_api.message(state_reg_bybit_api.secret_key) -async def add_secret_key(message: Message, state: FSMContext): - await state.update_data(secret_key = message.text) +async def add_secret_key(message: Message, state: FSMContext) -> None: + """ + Сохраняет Secret Key и финализирует регистрацию, + обновляет базу данных, устанавливает символ пользователя и очищает состояние. + """ + await state.update_data(secret_key=message.text) data = await state.get_data() await rq.update_api_key(message.from_user.id, data['api_key']) await rq.update_secret_key(message.from_user.id, data['secret_key']) await rq.set_new_user_symbol(message.from_user.id) - + await state.clear() - await message.answer('Данные добавлены, нажмите на профиль и начните торговлю!', reply_markup=reply_markup.base_buttons_markup) - - \ No newline at end of file + await message.answer('Данные добавлены, нажмите на профиль и начните торговлю!', + reply_markup=reply_markup.base_buttons_markup) -- 2.50.1 From 25976156301bf39e7b8cfe29ebdf0767270052f1 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Tue, 26 Aug 2025 19:10:57 +0500 Subject: [PATCH 45/77] Added states --- app/states/States.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/app/states/States.py b/app/states/States.py index e69de29..4ac4b7d 100644 --- a/app/states/States.py +++ b/app/states/States.py @@ -0,0 +1,39 @@ +from aiogram.fsm.state import State, StatesGroup + + +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() + + +class SetTP_SL_State(StatesGroup): + waiting_for_take_profit = State() + waiting_for_stop_loss = State() + + +class update_risk_management_settings(StatesGroup): + price_profit = State() + price_loss = State() + max_risk_deal = State() + commission_fee = State() + + +class state_reg_bybit_api(StatesGroup): + api_key = State() + secret_key = State() -- 2.50.1 From 29a5df0b1ab70c9b52f25a84673d9e7eb8d0a4dd Mon Sep 17 00:00:00 2001 From: algizn97 Date: Tue, 26 Aug 2025 19:11:07 +0500 Subject: [PATCH 46/77] Added tasks --- app/tasks/tasks.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 app/tasks/tasks.py diff --git a/app/tasks/tasks.py b/app/tasks/tasks.py new file mode 100644 index 0000000..13becce --- /dev/null +++ b/app/tasks/tasks.py @@ -0,0 +1,24 @@ +import asyncio + +from app.services.Bybit.functions.Futures import close_trade_after_delay, trading_cycle + +active_start_tasks = {} +active_close_tasks = {} + +def start_trading_cycle(tg_id, message): + task = asyncio.create_task(trading_cycle(tg_id, message)) + active_start_tasks[tg_id] = task + +def stop_trading_cycle(tg_id): + task = active_start_tasks.pop(tg_id, None) + if task: + task.cancel() + +def start_close_trade_task(tg_id, message, symbol, delay_sec): + task = asyncio.create_task(close_trade_after_delay(tg_id, message, symbol, delay_sec)) + active_close_tasks[tg_id] = task + +def stop_close_trade_task(tg_id): + task = active_close_tasks.pop(tg_id, None) + if task: + task.cancel() -- 2.50.1 From f23bda38f43dd2eb7df92dc6c2e2f75cec241c28 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Tue, 26 Aug 2025 19:11:23 +0500 Subject: [PATCH 47/77] Added requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 4e876bb..66baab5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,6 +14,7 @@ greenlet==3.2.4 idna==3.10 magic-filter==1.0.12 multidict==6.6.4 +nest-asyncio==1.6.0 propcache==0.3.2 pybit==5.11.0 pycryptodome==3.23.0 -- 2.50.1 From 43e62fdeff5c10990873ee0ed33efbc435590bff Mon Sep 17 00:00:00 2001 From: algizn97 Date: Tue, 26 Aug 2025 19:11:53 +0500 Subject: [PATCH 48/77] Update keyboards --- app/telegram/Keyboards/inline_keyboards.py | 25 ++++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/app/telegram/Keyboards/inline_keyboards.py b/app/telegram/Keyboards/inline_keyboards.py index c0acda3..39b953a 100644 --- a/app/telegram/Keyboards/inline_keyboards.py +++ b/app/telegram/Keyboards/inline_keyboards.py @@ -1,4 +1,5 @@ from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from aiogram.utils.keyboard import InlineKeyboardBuilder start_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🔥 Начать торговлю", callback_data="clb_start_chatbot_message")] @@ -37,6 +38,7 @@ trading_markup = InlineKeyboardMarkup(inline_keyboard=[ start_trading_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="На главную", callback_data='back_to_main')], [InlineKeyboardButton(text="Начать торговлю", callback_data="clb_start_chatbot_trading")], + [InlineKeyboardButton(text="Установить таймер", callback_data="clb_set_timer")], ]) @@ -83,7 +85,7 @@ risk_management_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ InlineKeyboardButton(text='Изм. цены убытков', callback_data='clb_change_price_loss')], [InlineKeyboardButton(text='Макс. риск на сделку', callback_data='clb_change_max_risk_deal')], - [InlineKeyboardButton(text='Комиссия биржи', callback_data='commission_fee')], + [InlineKeyboardButton(text='Учитывать комиссию биржи (Да/Нет)', callback_data='commission_fee')], back_btn_list_settings, back_btn_to_main @@ -140,9 +142,9 @@ trigger_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА [InlineKeyboardButton(text="Автоматический", callback_data="clb_trigger_auto")] ]) -buttons_yes_no_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE +buttons_yes_no_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text='Да', callback_data="clb_yes"), - InlineKeyboardButton(text='Нет', callback_data="clb_yes")] + InlineKeyboardButton(text='Нет', callback_data="clb_no")], ]) buttons_on_off_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE @@ -150,21 +152,26 @@ buttons_on_off_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТ InlineKeyboardButton(text='Выключить', callback_data="clb_off")] ]) +my_deals_select_markup = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text='Открытые сделки', callback_data="clb_open_deals"), + InlineKeyboardButton(text='Открытые ордера', callback_data="clb_open_orders")], + back_btn_to_main +]) def create_trades_inline_keyboard(trades): - buttons = [] + builder = InlineKeyboardBuilder() for trade in trades: symbol = trade['symbol'] if isinstance(trade, dict) else trade.symbol - buttons.append([ - InlineKeyboardButton(text=f"{symbol}", callback_data=f"show_deal_{symbol}") - ]) - return InlineKeyboardMarkup(inline_keyboard=buttons) + builder.button(text=symbol, callback_data=f"show_deal_{symbol}") + builder.adjust(2) + return builder.as_markup() def create_close_deal_markup(symbol: str) -> InlineKeyboardMarkup: return InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="Закрыть сделку", callback_data=f"close_deal:{symbol}")], [InlineKeyboardButton(text="Закрыть по таймеру", callback_data=f"close_deal_by_timer:{symbol}")], + [InlineKeyboardButton(text="Установить TP/SL", callback_data="clb_set_tp_sl")], back_btn_to_main ]) @@ -175,5 +182,5 @@ timer_markup = InlineKeyboardMarkup(inline_keyboard=[ ]) cancel_start_markup = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text="Отменить запуск", callback_data="clb_stop_timer")] + [InlineKeyboardButton(text="Отменить таймер", callback_data="clb_stop_timer")] ]) -- 2.50.1 From fd279f05625f962191c6fd0b99f2de0eed75ed55 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Tue, 26 Aug 2025 19:34:47 +0500 Subject: [PATCH 49/77] Added loggers for models --- app/telegram/database/models.py | 214 +++++++++++++++++++++++++++----- logger_helper/logger_helper.py | 5 + 2 files changed, 185 insertions(+), 34 deletions(-) diff --git a/app/telegram/database/models.py b/app/telegram/database/models.py index 789a1a9..6b6ab01 100644 --- a/app/telegram/database/models.py +++ b/app/telegram/database/models.py @@ -1,31 +1,55 @@ -import logging -from datetime import datetime - +from datetime import datetime +import logging.config from sqlalchemy.sql.sqltypes import DateTime -logger = logging.getLogger(__name__) - from sqlalchemy import BigInteger, Boolean, Integer, String, ForeignKey from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from sqlalchemy.ext.asyncio import AsyncAttrs, async_sessionmaker, create_async_engine - +from logger_helper.logger_helper import LOGGING_CONFIG from sqlalchemy import select, insert +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("models") + engine = create_async_engine(url='sqlite+aiosqlite:///db.sqlite3') async_session = async_sessionmaker(engine) + class Base(AsyncAttrs, DeclarativeBase): + """Базовый класс для declarative моделей SQLAlchemy с поддержкой async.""" pass + class User_Telegram_Id(Base): + """ + Модель таблицы user_telegram_id. + + Хранит идентификаторы Telegram пользователей. + + Атрибуты: + id (int): Внутренний первичный ключ записи. + tg_id (int): Уникальный идентификатор пользователя Telegram. + """ __tablename__ = 'user_telegram_id' id: Mapped[int] = mapped_column(primary_key=True) tg_id = mapped_column(BigInteger) + class User_Bybit_API(Base): + """ + Модель таблицы user_bybit_api. + + Хранит API ключи и секреты Bybit для каждого Telegram пользователя. + + Атрибуты: + id (int): Внутренний первичный ключ записи. + tg_id (int): Внешний ключ на Telegram пользователя (user_telegram_id.tg_id). + api_key (str): API ключ Bybit (уникальный для пользователя). + secret_key (str): Секретный ключ Bybit (уникальный для пользователя). + """ __tablename__ = 'user_bybit_api' id: Mapped[int] = mapped_column(primary_key=True) @@ -35,7 +59,26 @@ class User_Bybit_API(Base): api_key = mapped_column(String(18), default='None') secret_key = mapped_column(String(36), default='None') + class User_Symbol(Base): + """ + Модель таблицы user_main_settings. + + Хранит основные настройки торговли для пользователя. + + Атрибуты: + id (int): Внутренний первичный ключ записи. + tg_id (int): Внешний ключ на Telegram пользователя. + trading_mode (str): Режим торговли (ForeignKey на trading_modes.mode). + margin_type (str): Тип маржи (ForeignKey на margin_types.type). + size_leverage (int): Кредитное плечо. + starting_quantity (int): Начальный объём позиции. + martingale_factor (int): Коэффициент мартингейла. + martingale_step (int): Текущий шаг мартингейла. + maximal_quantity (int): Максимальное количество шагов мартингейла. + entry_order_type (str): Тип входа (Market или Limit). + limit_order_price (str, optional): Цена лимитного ордера, если используется. + """ __tablename__ = 'user_symbols' id: Mapped[int] = mapped_column(primary_key=True) @@ -44,28 +87,69 @@ class User_Symbol(Base): symbol = mapped_column(String(18), default='PENGUUSDT') + class Trading_Mode(Base): + """ + Справочник доступных режимов торговли. + + Атрибуты: + id (int): Первичный ключ. + mode (str): Уникальный режим (например, 'Long', 'Short'). + """ __tablename__ = 'trading_modes' id: Mapped[int] = mapped_column(primary_key=True) mode = mapped_column(String(10), unique=True) + class Margin_type(Base): + """ + Справочник типов маржинальной торговли. + + Атрибуты: + id (int): Первичный ключ. + type (str): Тип маржи (например, 'Isolated', 'Cross'). + """ __tablename__ = 'margin_types' id: Mapped[int] = mapped_column(primary_key=True) type = mapped_column(String(15), unique=True) + class Trigger(Base): + """ + Справочник видов триггеров для сделок. + + Атрибуты: + id (int): Первичный ключ. + trigger (str): Название триггера (например, 'Ручной', 'Автоматический'). + """ __tablename__ = 'triggers' id: Mapped[int] = mapped_column(primary_key=True) trigger = mapped_column(String(15), unique=True) + class User_Main_Settings(Base): + """ + Основные настройки пользователя для торговли. + + Атрибуты: + id (int): Первичный ключ. + tg_id (int): Внешний ключ на Telegram пользователя. + trading_mode (str): Режим торговли, FK на trading_modes.mode. + margin_type (str): Тип маржи, FK на margin_types.type. + size_leverage (int): Кредитное плечо. + starting_quantity (int): Начальный объем позиции. + martingale_factor (int): Коэффициент мартингейла. + martingale_step (int): Текущий шаг мартингейла. + maximal_quantity (int): Максимальное число шагов мартингейла. + entry_order_type (str): Тип ордера входа (Market/Limit). + limit_order_price (Optional[str]): Цена лимитного ордера, если есть. + """ __tablename__ = 'user_main_settings' id: Mapped[int] = mapped_column(primary_key=True) @@ -84,6 +168,17 @@ class User_Main_Settings(Base): class User_Risk_Management_Settings(Base): + """ + Настройки управления рисками пользователя. + + Атрибуты: + id (int): Первичный ключ. + tg_id (int): Внешний ключ на Telegram пользователя. + price_profit (int): Процент прибыли для трейда. + price_loss (int): Процент убытка для трейда. + max_risk_deal (int): Максимально допустимый риск по сделке в процентах. + commission_fee (str): Учитывать ли комиссию в расчетах ("Да"/"Нет"). + """ __tablename__ = 'user_risk_management_settings' id: Mapped[int] = mapped_column(primary_key=True) @@ -93,9 +188,24 @@ class User_Risk_Management_Settings(Base): price_profit = mapped_column(Integer(), default=1) price_loss = mapped_column(Integer(), default=1) max_risk_deal = mapped_column(Integer(), default=100) - commission_fee = mapped_column(Integer(), default=0) + commission_fee = mapped_column(String(), default="Да") + class User_Condition_Settings(Base): + """ + Дополнительные пользовательские условия для торговли. + + Атрибуты: + id (int): Первичный ключ. + tg_id (int): Внешний ключ на Telegram пользователя. + trigger (str): Тип триггера, FK на triggers.trigger. + filter_time (str): Временной фильтр. + filter_volatility (bool): Фильтр по волатильности. + external_cues (bool): Внешние сигналы. + tradingview_cues (bool): Сигналы TradingView. + webhook (str): URL webhook. + ai_analytics (bool): Использование AI для аналитики. + """ __tablename__ = 'user_condition_settings' id: Mapped[int] = mapped_column(primary_key=True) @@ -110,7 +220,18 @@ class User_Condition_Settings(Base): webhook = mapped_column(String(40), default='') ai_analytics = mapped_column(Boolean, default=False) + class User_Additional_Settings(Base): + """ + Прочие дополнительные настройки пользователя. + + Атрибуты: + id (int): Первичный ключ. + tg_id (int): Внешний ключ на Telegram пользователя. + pattern_save (bool): Сохранять ли шаблоны. + autostart (bool): Автоматический запуск. + notifications (bool): Получение уведомлений. + """ __tablename__ = 'user_additional_settings' id: Mapped[int] = mapped_column(primary_key=True) @@ -121,34 +242,19 @@ class User_Additional_Settings(Base): autostart = mapped_column(Boolean, default=False) notifications = mapped_column(Boolean, default=False) -async def async_main(): - async with engine.begin() as conn: - await conn.run_sync(Base.metadata.create_all) - - # Заполнение таблиц - modes = ['Long', 'Short', 'Switch', 'Smart'] - for mode in modes: - result = await conn.execute(select(Trading_Mode).where(Trading_Mode.mode == mode)) - if not result.first(): - logger.info("Заполение таблицы режима торговли") - await conn.execute(Trading_Mode.__table__.insert().values(mode=mode)) - - types = ['Isolated', 'Cross'] - for type in types: - result = await conn.execute(select(Margin_type).where(Margin_type.type == type)) - if not result.first(): - logger.info("Заполение таблицы типов маржи") - await conn.execute(Margin_type.__table__.insert().values(type=type)) - - triggers = ['Ручной', 'Автоматический', 'TradingView'] - for trigger in triggers: - result = await conn.execute(select(Trigger).where(Trigger.trigger == trigger)) - if not result.first(): - logger.info("Заполение таблицы триггеров") - await conn.execute(Trigger.__table__.insert().values(trigger=trigger)) - class USER_DEALS(Base): + """ + Таблица сделок пользователя. + + Атрибуты: + id (int): Первичный ключ. + tg_id (int): Внешний ключ на Telegram пользователя. + symbol (str): Торговая пара. + side (str): Направление сделки (Buy/Sell). + open_price (int): Цена открытия. + positive_percent (int): Процент доходности. + """ __tablename__ = 'user_deals' id: Mapped[int] = mapped_column(primary_key=True) @@ -162,10 +268,50 @@ class USER_DEALS(Base): class UserTimer(Base): + """ + Таймер пользователя для отсроченного запуска сделок. + + Атрибуты: + id (int): Первичный ключ. + tg_id (int): Внешний ключ на Telegram пользователя. + timer_minutes (int): Количество минут таймера. + timer_start (datetime): Время начала таймера. + timer_end (Optional[datetime]): Время окончания таймера (если установлено). + """ __tablename__ = 'user_timers' id: Mapped[int] = mapped_column(primary_key=True) tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) timer_minutes = mapped_column(Integer, nullable=False, default=0) timer_start = mapped_column(DateTime, default=datetime.utcnow) - timer_end = mapped_column(DateTime, nullable=True) \ No newline at end of file + timer_end = mapped_column(DateTime, nullable=True) + + +async def async_main(): + """ + Асинхронное создание всех таблиц и заполнение справочников начальными данными. + """ + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + # Заполнение таблиц + modes = ['Long', 'Short', 'Switch', 'Smart'] + for mode in modes: + result = await conn.execute(select(Trading_Mode).where(Trading_Mode.mode == mode)) + if not result.first(): + logger.info("Заполение таблицы режима торговли") + await conn.execute(Trading_Mode.__table__.insert().values(mode=mode)) + + types = ['Isolated', 'Cross'] + for type in types: + result = await conn.execute(select(Margin_type).where(Margin_type.type == type)) + if not result.first(): + logger.info("Заполение таблицы типов маржи") + await conn.execute(Margin_type.__table__.insert().values(type=type)) + + triggers = ['Ручной', 'Автоматический', 'TradingView'] + for trigger in triggers: + result = await conn.execute(select(Trigger).where(Trigger.trigger == trigger)) + if not result.first(): + logger.info("Заполение таблицы триггеров") + await conn.execute(Trigger.__table__.insert().values(trigger=trigger)) diff --git a/logger_helper/logger_helper.py b/logger_helper/logger_helper.py index cde13eb..8123460 100644 --- a/logger_helper/logger_helper.py +++ b/logger_helper/logger_helper.py @@ -85,5 +85,10 @@ LOGGING_CONFIG = { "level": "DEBUG", "propagate": False, }, + "models": { + "handlers": ["console", "timed_rotating_file"], + "level": "DEBUG", + "propagate": False, + }, }, } -- 2.50.1 From 7c48336a62bfee1656b35aa63327192ecd4fe40f Mon Sep 17 00:00:00 2001 From: algizn97 Date: Tue, 26 Aug 2025 19:35:01 +0500 Subject: [PATCH 50/77] Update --- .../risk_management_settings/settings.py | 78 +++++++++---------- 1 file changed, 36 insertions(+), 42 deletions(-) diff --git a/app/telegram/functions/risk_management_settings/settings.py b/app/telegram/functions/risk_management_settings/settings.py index 3a7e7ee..53d699d 100644 --- a/app/telegram/functions/risk_management_settings/settings.py +++ b/app/telegram/functions/risk_management_settings/settings.py @@ -1,27 +1,21 @@ from aiogram import Router import app.telegram.Keyboards.inline_keyboards as inline_markup -import app.telegram.Keyboards.reply_keyboards as reply_markup import app.telegram.database.requests as rq from aiogram.types import Message, CallbackQuery -# FSM - Механизм состояния -from aiogram.fsm.state import State, StatesGroup +from app.states.States import update_risk_management_settings router_risk_management_settings = Router() -class update_risk_management_settings(StatesGroup): - price_profit = State() - price_loss = State() - max_risk_deal = State() - commission_fee = State() async def reg_new_user_default_risk_management_settings(id, message): tg_id = id await rq.set_new_user_default_risk_management_settings(tg_id) -async def main_settings_message(id, message, state): + +async def main_settings_message(id, message): data = await rq.get_user_risk_management_settings(id) text = f"""Риск менеджмент, @@ -29,20 +23,22 @@ async def main_settings_message(id, message, state): - Процент изменения цены для фиксации прибыли: {data.get('price_profit', 0)}% - Процент изменения цены для фиксации убытков: {data.get('price_loss', 0)}% - Максимальный риск на сделку (в % от баланса): {data.get('max_risk_deal', 0)}% - - Комиссия биржи для расчета процента фиксации прибыли: {data.get('commission_fee', 0)}% + - Комиссия биржи для расчета прибыли: {data.get('commission_fee', "Да")} """ await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.risk_management_settings_markup) + async def price_profit_message(message, state): await state.set_state(update_risk_management_settings.price_profit) - text = 'Введите число изменения цены для фиксации прибыли: ' + text = 'Введите число изменения цены для фиксации прибыли: ' await message.answer(text=text, parse_mode='html', reply_markup=None) + @router_risk_management_settings.message(update_risk_management_settings.price_profit) async def state_price_profit(message: Message, state): - await state.update_data(price_profit = message.text) + await state.update_data(price_profit=message.text) data = await state.get_data() data_settings = await rq.get_user_risk_management_settings(message.from_user.id) @@ -51,24 +47,27 @@ async def state_price_profit(message: Message, state): await message.answer(f"✅ Изменено: {data_settings['price_profit']}% → {data['price_profit']}%") await rq.update_price_profit(message.from_user.id, data['price_profit']) - await main_settings_message(message.from_user.id, message, state) + await main_settings_message(message.from_user.id, message) await state.clear() else: - await message.answer(f'⛔️ Ошибка: ваше значение ({data['price_profit']}%) или выше лимита (100) или вы вводите неверные символы') + await message.answer( + f'⛔️ Ошибка: ваше значение ({data['price_profit']}%) или выше лимита (100) или вы вводите неверные символы') + + await main_settings_message(message.from_user.id, message) - await main_settings_message(message.from_user.id, message, state) async def price_loss_message(message, state): await state.set_state(update_risk_management_settings.price_loss) - text = 'Введите число изменения цены для фиксации убытков: ' + text = 'Введите число изменения цены для фиксации убытков: ' await message.answer(text=text, parse_mode='html', reply_markup=None) + @router_risk_management_settings.message(update_risk_management_settings.price_loss) async def state_price_loss(message: Message, state): - await state.update_data(price_loss = message.text) + await state.update_data(price_loss=message.text) data = await state.get_data() data_settings = await rq.get_user_risk_management_settings(message.from_user.id) @@ -99,23 +98,25 @@ async def state_price_loss(message: Message, state): else: await message.answer(f"✅ Стоп-лосс изменён: {old_price_loss}% → {new_price_loss}%") - await main_settings_message(message.from_user.id, message, state) + await main_settings_message(message.from_user.id, message) await state.clear() else: await message.answer( f'⛔️ Ошибка: ваше значение ({data["price_loss"]}%) выше лимита (100) или содержит неверные символы') - await main_settings_message(message.from_user.id, message, state) + await main_settings_message(message.from_user.id, message) + async def max_risk_deal_message(message, state): await state.set_state(update_risk_management_settings.max_risk_deal) - text = 'Введите число (процент от баланса) для изменения максимального риска на сделку: ' + text = 'Введите число (процент от баланса) для изменения максимального риска на сделку: ' await message.answer(text=text, parse_mode='html', reply_markup=None) + @router_risk_management_settings.message(update_risk_management_settings.max_risk_deal) async def state_max_risk_deal(message: Message, state): - await state.update_data(max_risk_deal = message.text) + await state.update_data(max_risk_deal=message.text) data = await state.get_data() data_settings = await rq.get_user_risk_management_settings(message.from_user.id) @@ -124,34 +125,27 @@ async def state_max_risk_deal(message: Message, state): await message.answer(f"✅ Изменено: {data_settings['max_risk_deal']}% → {data['max_risk_deal']}%") await rq.update_max_risk_deal(message.from_user.id, data['max_risk_deal']) - await main_settings_message(message.from_user.id, message, state) + await main_settings_message(message.from_user.id, message) await state.clear() else: - await message.answer(f'⛔️ Ошибка: ваше значение ({data['max_risk_deal']}%) или выше лимита (100) или вы вводите неверные символы') + await message.answer( + f'⛔️ Ошибка: ваше значение ({data['max_risk_deal']}%) или выше лимита (100) или вы вводите неверные символы') - await main_settings_message(message.from_user.id, message, state) + await main_settings_message(message.from_user.id, message) async def commission_fee_message(message, state): await state.set_state(update_risk_management_settings.commission_fee) - await message.answer(text="Введите процент комиссии биржи (например, 0.1):", parse_mode='html', reply_markup=None) + await message.answer(text="Хотите учитывать комиссию биржи:", parse_mode='html', + reply_markup=inline_markup.buttons_yes_no_markup) -@router_risk_management_settings.message(update_risk_management_settings.commission_fee) -async def state_commission_fee(message: Message, state): - await state.update_data(commission_fee=message.text) - data = await state.get_data() - data_settings = await rq.get_user_risk_management_settings(message.from_user.id) - try: - val = float(data['commission_fee']) - if val < 0 or val > 100: - raise ValueError() - except Exception: - await message.answer("⛔️ Ошибка: введите корректный процент комиссии от 0 до 100") - return await commission_fee_message(message, state) - - await rq.update_commission_fee(message.from_user.id, val) - await message.answer(f"✅ Изменено: {data_settings['commission_fee']}% → {data['commission_fee']}%") - await main_settings_message(message.from_user.id, message, state) - await state.clear() \ No newline at end of file +@router_risk_management_settings.callback_query(lambda c: c.data in ["clb_yes", "clb_no"]) +async def process_commission_fee_callback(callback: CallbackQuery, state): + val = "Да" if callback.data == "clb_yes" else "Нет" + await rq.update_commission_fee(callback.from_user.id, val) + await callback.message.answer(f"✅ Изменено: {val}") + await callback.answer() + await main_settings_message(callback.from_user.id, callback.message) + await state.clear() -- 2.50.1 From 8bc4c634fe273e4c24ff938cb158e7a241aafd7c Mon Sep 17 00:00:00 2001 From: algizn97 Date: Tue, 26 Aug 2025 19:35:40 +0500 Subject: [PATCH 51/77] Update --- app/telegram/handlers/handlers.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/telegram/handlers/handlers.py b/app/telegram/handlers/handlers.py index fcdea72..5c84064 100644 --- a/app/telegram/handlers/handlers.py +++ b/app/telegram/handlers/handlers.py @@ -1,18 +1,17 @@ import logging.config from aiogram import F, Router -from aiogram.filters import CommandStart, Command +from aiogram.filters import CommandStart from aiogram.types import Message, CallbackQuery from aiogram.fsm.context import FSMContext -import app.telegram.functions.functions as func # functions +import app.telegram.functions.functions as func import app.telegram.functions.main_settings.settings as func_main_settings import app.telegram.functions.risk_management_settings.settings as func_rmanagement_settings import app.telegram.functions.condition_settings.settings as func_condition_settings import app.telegram.functions.additional_settings.settings as func_additional_settings import app.telegram.database.requests as rq -import app.telegram.Keyboards.inline_keyboards as inline_markup import app.telegram.Keyboards.reply_keyboards as reply_markup from logger_helper.logger_helper import LOGGING_CONFIG @@ -99,8 +98,8 @@ async def clb_change_main_settings_message(callback: CallbackQuery, state: FSMCo @router.callback_query(F.data == "clb_change_risk_management_settings") -async def clb_change_risk_management_message(callback: CallbackQuery, state: FSMContext): - await func_rmanagement_settings.main_settings_message(callback.from_user.id, callback.message, state) +async def clb_change_risk_management_message(callback: CallbackQuery): + await func_rmanagement_settings.main_settings_message(callback.from_user.id, callback.message) await callback.answer() -- 2.50.1 From 07df16dbe93eeed4fb4da14bd2edbe657af01bc5 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Tue, 26 Aug 2025 19:36:11 +0500 Subject: [PATCH 52/77] Added new functions and documentation --- app/services/Bybit/functions/Futures.py | 463 +++++++++++++++++----- app/services/Bybit/functions/functions.py | 368 ++++++++++------- 2 files changed, 585 insertions(+), 246 deletions(-) diff --git a/app/services/Bybit/functions/Futures.py b/app/services/Bybit/functions/Futures.py index d4ab4fe..3ca6713 100644 --- a/app/services/Bybit/functions/Futures.py +++ b/app/services/Bybit/functions/Futures.py @@ -1,9 +1,11 @@ import asyncio -import functools +import nest_asyncio + import time import logging.config 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 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) logger = logging.getLogger("futures") -active_start_tasks = {} -active_close_tasks = {} +nest_asyncio.apply() -def safe_float(val): +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 -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' - 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}x -Количество: {qty} -Тейк-профит: {round(tp, 5)} -Стоп-лосс: {round(sl, 5)} -''' + text = ( + f"{'Позиция была успешна открыта' if order_type == 'Market' else 'Лимитный ордер установлен'}!\n" + f"Торговая пара: {symbol}\n" + f"Цена входа: {entry_price if order_type == 'Market' else round(limit_price, 5)}\n" + f"Движение: {trade_mode}\n" + f"Тип-маржи: {human_margin_mode}\n" + f"Кредитное плечо: {leverage}x\n" + f"Количество: {qty}\n" + 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)) -async def error_max_step(message): +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): +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) @@ -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')) 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', []) @@ -173,7 +293,7 @@ async def open_position(tg_id, message, side: str, margin_mode: str, tpsl_mode=' await message.answer( f"Сумма ордера слишком мала: {order_value:.2f} USDT. " f"Минимум для торговли — {min_order_value} USDT. " - f"Пожалуйста, увеличьте количество позиций.") + f"Пожалуйста, увеличьте количество позиций.", reply_markup=inline_markup.back_to_main) return False 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: if "110043" in str(e): - # Плечо уже установлено с таким значением, можем игнорировать logger.info(f"Leverage already set to {leverage} for {symbol}") else: raise e @@ -206,7 +325,7 @@ async def open_position(tg_id, message, side: str, margin_mode: str, tpsl_mode=' category='linear', symbol=symbol, side=side, - orderType=order_type, # Market или Limit + 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), @@ -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): + """ + Цикл торговой логики с учётом таймера пользователя. + """ try: timer_data = await rq.get_user_timer(tg_id) timer_min = 0 @@ -264,47 +386,210 @@ async def trading_cycle(tg_id, message): 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 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')) -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('retCode') != 0: - return [] - symbols = [item['symbol'] for item in instruments_resp.get('result', {}).get('list', [])] + limit_price = None + if order_type == 'Limit': + limit_price = await rq.get_limit_price(tg_id) - active_positions = [] + data_risk_stgs = await rq.get_user_risk_management_settings(tg_id) + trading_mode = data_main_stgs.get('trading_mode') - for sym in symbols: + 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: - 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 safe_float(pos['size']) > 0: - active_positions.append(pos) - except Exception as e: - logger.error(f"Ошибка при получении позиций: {e}") - await message.answer('⚠️ Ошибка при получении позиций', reply_markup=inline_markup.back_to_main) + 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 - return active_positions + 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 close_user_trade(tg_id: int, symbol: str, message=None) -> bool: +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', []) @@ -318,24 +603,15 @@ async def close_user_trade(tg_id: int, symbol: str, message=None) -> bool: 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) + 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', {}) - # поддержать оба варианта: result это dict с key 'list', или list ticker_list = [] if isinstance(result, dict): 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: 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, @@ -371,19 +629,39 @@ async def close_user_trade(tg_id: int, symbol: str, message=None) -> bool: 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) + 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 + + 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}%)\n" + f"{'Включая комиссию биржи' if include_fee else 'Без учета комиссии'}" + ) + await message.answer(text) return True else: 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 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): + """ + Закрывает сделку пользователя после задержки delay_sec секунд. + """ try: await asyncio.sleep(delay_sec) - result = await close_user_trade(tg_id, symbol) + result = await close_user_trade(tg_id, symbol, message) if result: await message.answer(f"Сделка {symbol} успешно закрыта по таймеру.", 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) 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) diff --git a/app/services/Bybit/functions/functions.py b/app/services/Bybit/functions/functions.py index fa80407..40fadc7 100644 --- a/app/services/Bybit/functions/functions.py +++ b/app/services/Bybit/functions/functions.py @@ -2,10 +2,13 @@ import logging.config 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 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.Futures import (close_user_trade, close_trade_after_delay, + 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 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 app.services.Bybit.functions.price_symbol import get_price -# FSM - Механизм состояния -from aiogram.fsm.state import State, StatesGroup +from app.states.States import (state_update_entry_type, state_update_symbol, state_limit_price, + SetTP_SL_State, CloseTradeTimerState) from aiogram.fsm.context import FSMContext 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() -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'])) -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) price = await get_price(callback.from_user.id) if balance: symbol = await rq.get_symbol(callback.from_user.id) - text = f'''💎 Торговля на Bybit - -⚖️ Ваш баланс (USDT): {balance} -📊 Текущая торговая пара: {symbol} -$$$ Цена: {price} - -Как начать торговлю? - -1️⃣ Проверьте и тщательно настройте все параметры в вашем профиле. -2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT). -3️⃣ Нажмите кнопку 'Выбрать тип входа' и после нажмите начать торговлю. -''' + text = ( + f"💎 Торговля на Bybit\n\n" + f"⚖️ Ваш баланс (USDT): {balance}\n" + f"📊 Текущая торговая пара: {symbol}\n" + f"$$$ Цена: {price}\n\n" + "Как начать торговлю?\n\n" + "1️⃣ Проверьте и тщательно настройте все параметры в вашем профиле.\n" + "2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).\n" + "3️⃣ Нажмите кнопку 'Выбрать тип входа' и после нажмите начать торговлю.\n" + ) 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) price = await get_price(message.from_user.id) if balance: symbol = await rq.get_symbol(message.from_user.id) - text = f'''💎 Торговля на Bybit - -⚖️ Ваш баланс (USDT): {balance} -📊 Текущая торговая пара: {symbol} -$$$ Цена: {price} - -Как начать торговлю? - -1️⃣ Проверьте и тщательно настройте все параметры в вашем профиле. -2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT). -3️⃣ Нажмите кнопку 'Выбрать тип входа' и после нажмите начать торговлю. -''' + text = ( + f"💎 Торговля на Bybit\n\n" + f"⚖️ Ваш баланс (USDT): {balance}\n" + f"📊 Текущая торговая пара: {symbol}\n" + f"$$$ Цена: {price}\n\n" + "Как начать торговлю?\n\n" + "1️⃣ Проверьте и тщательно настройте все параметры в вашем профиле.\n" + "2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).\n" + "3️⃣ Нажмите кнопку 'Выбрать тип входа' и после нажмите начать торговлю.\n" + ) 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') -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 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) -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() 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 message.answer('Пара была успешно обновлена') 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() @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 callback.message.answer("Выберите тип входа в позицию:", reply_markup=inline_markup.entry_order_type_markup) await callback.answer() @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] 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) -async def set_limit_price(message: Message, state: FSMContext): +async def set_limit_price(message: Message, state: FSMContext) -> None: + """ + Обрабатывает ввод цены лимитного ордера, проверяет формат и сохраняет настройки. + """ try: price = float(message.text) if price <= 0: @@ -175,65 +180,16 @@ 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): - 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") -async def start_trading_process(callback: CallbackQuery): +async def start_trading_process(callback: CallbackQuery) -> None: + """ + Запускает торговый цикл в выбранном режиме Long/Short. + Проверяет API-ключи, режим торговли, маржинальный режим и открытые позиции, + затем запускает торговый цикл с задержкой или без неё. + """ tg_id = callback.from_user.id message = callback.message - # Получаем настройки пользователя data_main_stgs = await rq.get_user_main_settings(tg_id) api_key = await rq.get_bybit_api_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') trading_mode = data_main_stgs.get('trading_mode') - # Проверка API ключей if not api_key or not secret_key: await message.answer("❗️ У вас не настроены API ключи для Bybit.") await callback.answer() return - # Проверка режима торговли if trading_mode not in ['Long', 'Short', 'Smart', 'Switch']: await message.answer(f"❗️ Некорректный торговый режим: {trading_mode}") await callback.answer() return - # Проверка допустимости маржинального режима if margin_mode not in ['Isolated', 'Cross']: margin_mode = 'Isolated' - # Проверяем открытые позиции и маржинальный режим client = HTTP(api_key=api_key, api_secret=secret_key) + try: positions_resp = client.get_positions(category='linear', symbol=symbol) 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) return - # Определяем сторону для открытия позиции if trading_mode == 'Long': side = 'Buy' elif trading_mode == 'Short': side = 'Sell' else: - await message.answer(f"Режим торговли '{trading_mode}' пока не поддерживается.") + await message.answer(f"Режим торговли '{trading_mode}' пока не поддерживается.", + reply_markup=inline_markup.back_to_main) await callback.answer() return - # Сообщаем о начале торговли await message.answer("Начинаю торговлю с использованием текущих настроек...") timer_data = await rq.get_user_timer(tg_id) @@ -294,16 +246,15 @@ async def start_trading_process(callback: CallbackQuery): else: 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: + if old_task and not old_task.done(): old_task.cancel() - # можно ждать завершения старой задачи, если в async функции - task = asyncio.create_task(trading_cycle(tg_id, message, symbol)) + try: + 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 await message.answer(f"Торговля начнётся через {timer_minute} мин. Для отмены нажмите кнопку ниже.", reply_markup=inline_markup.cancel_start_markup) @@ -314,8 +265,113 @@ async def start_trading_process(callback: CallbackQuery): 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") -async def cancel_start_callback(callback: CallbackQuery): +async def cancel_start_callback(callback: CallbackQuery) -> None: + """ + Отменяет задачу старта торговли по таймеру, если она активна. + """ tg_id = callback.from_user.id task = active_start_tasks.get(tg_id) if task: @@ -327,45 +383,44 @@ async def cancel_start_callback(callback: CallbackQuery): 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): +async def close_trade_callback(callback: CallbackQuery) -> None: + """ + Закрывает сделку пользователя по символу. + """ symbol = callback.data.split(':')[1] 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: - 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: + logger.error(f"Не удалось закрыть сделку {symbol}.") 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): +async def ask_close_delay(callback: CallbackQuery, state: FSMContext) -> None: + """ + Запускает диалог с пользователем для задания задержки перед закрытием сделки. + """ 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.message.answer("Введите задержку в минутах до закрытия сделки (например, 60):", + reply_markup=inline_markup.cancel) await callback.answer() @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: delay_minutes = int(message.text.strip()) if delay_minutes <= 0: @@ -381,9 +436,14 @@ async def process_close_delay(message: Message, state: FSMContext): delay = delay_minutes * 60 - # Отменяем предыдущую задачу, если есть 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)) 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") -async def reset_martingale(callback: CallbackQuery): +async def reset_martingale(callback: CallbackQuery) -> None: + """ + Сбрасывает шаги мартингейла пользователя. + """ await callback.answer() tg_id = callback.from_user.id await rq.update_martingale_step(tg_id, 0) - await callback.message.answer("Сброс шагов мартингейла выполнен. Торговля начнется заново с начального объема.", + 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): +async def cancel(callback: CallbackQuery, state: FSMContext) -> None: + """ + Отменяет текущее состояние FSM и сообщает пользователю об отмене. + """ await state.clear() await callback.message.answer("Отменено!", reply_markup=inline_markup.back_to_main) await callback.answer() -- 2.50.1 From 50afefeb5fc52ff1f4300cf1d27a5a2cd966d362 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Tue, 26 Aug 2025 19:36:55 +0500 Subject: [PATCH 53/77] Update --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 47f6ab0..52973aa 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ __pycache__/ env/ venv/ .venv/ +.idea +/.idea -- 2.50.1 From 511b08e8e526a424203823c63c50a30ea70680d2 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Wed, 27 Aug 2025 12:52:33 +0500 Subject: [PATCH 54/77] Added WebSocket --- app/services/Bybit/functions/bybit_ws.py | 47 ++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 app/services/Bybit/functions/bybit_ws.py diff --git a/app/services/Bybit/functions/bybit_ws.py b/app/services/Bybit/functions/bybit_ws.py new file mode 100644 index 0000000..8b8562d --- /dev/null +++ b/app/services/Bybit/functions/bybit_ws.py @@ -0,0 +1,47 @@ +import asyncio +import logging.config +from pybit.unified_trading import WebSocket +from websocket import WebSocketConnectionClosedException +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("bybit_ws") + +event_loop = None # Сюда нужно будет установить event loop из основного приложения + + +def set_event_loop(loop: asyncio.AbstractEventLoop): + global event_loop + event_loop = loop + + +def on_execution_callback(message, msg): + """ + Callback на событие исполнения сделки. + Безопасно запускает асинхронный обработчик из sync callback. + """ + if event_loop is not None: + from app.services.Bybit.functions.Futures import handle_execution_message # Импорт внутри, чтобы избежать циклических импортов + asyncio.run_coroutine_threadsafe(handle_execution_message(message, msg), event_loop) + else: + logger.error("Event loop не установлен, callback пропущен.") + + +async def start_execution_ws(api_key: str, api_secret: str, message): + """ + Запускает и поддерживает WebSocket подключение для исполнения сделок. + Реконнект при потерях соединения. + """ + reconnect_delay = 5 + while True: + try: + ws = WebSocket(api_key=api_key, api_secret=api_secret, testnet=False, channel_type="private") + ws.execution_stream(lambda msg: on_execution_callback(message, msg)) + while True: + await asyncio.sleep(1) # Поддержание активности + except WebSocketConnectionClosedException: + logger.warning("WebSocket закрыт, переподключение через 5 секунд...") + await asyncio.sleep(reconnect_delay) + except Exception as e: + logger.error(f"Ошибка WebSocket: {e}") + await asyncio.sleep(reconnect_delay) -- 2.50.1 From a140e0eb6ff1bf70b41ce8bed5ab299c5eadbd62 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Wed, 27 Aug 2025 12:53:35 +0500 Subject: [PATCH 55/77] Added documentation --- app/states/States.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/states/States.py b/app/states/States.py index 4ac4b7d..1f3122d 100644 --- a/app/states/States.py +++ b/app/states/States.py @@ -2,32 +2,39 @@ from aiogram.fsm.state import State, StatesGroup class state_update_symbol(StatesGroup): + """FSM состояние для обновления торгового символа.""" symbol = State() class state_update_entry_type(StatesGroup): + """FSM состояние для обновления типа входа.""" entry_type = State() class TradeSetup(StatesGroup): + """FSM состояния для настройки торговли с таймером и процентом.""" waiting_for_timer = State() waiting_for_positive_percent = State() class state_limit_price(StatesGroup): + """FSM состояние для установки лимита.""" price = State() class CloseTradeTimerState(StatesGroup): + """FSM состояние ожидания задержки перед закрытием сделки.""" waiting_for_delay = State() class SetTP_SL_State(StatesGroup): + """FSM состояние для установки TP и SL.""" waiting_for_take_profit = State() waiting_for_stop_loss = State() class update_risk_management_settings(StatesGroup): + """FSM состояние для обновления настроек управления рисками.""" price_profit = State() price_loss = State() max_risk_deal = State() @@ -35,5 +42,6 @@ class update_risk_management_settings(StatesGroup): class state_reg_bybit_api(StatesGroup): + """FSM состояние для регистрации API Bybit.""" api_key = State() secret_key = State() -- 2.50.1 From 90329576318436dc2992d722216e2a6837f47844 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Wed, 27 Aug 2025 12:54:45 +0500 Subject: [PATCH 56/77] Added documentation, added websocket and tasks --- app/services/Bybit/functions/Futures.py | 134 +++++------ app/services/Bybit/functions/functions.py | 62 ++--- app/telegram/database/requests.py | 266 ++++++++++++++-------- 3 files changed, 247 insertions(+), 215 deletions(-) diff --git a/app/services/Bybit/functions/Futures.py b/app/services/Bybit/functions/Futures.py index 3ca6713..a7104be 100644 --- a/app/services/Bybit/functions/Futures.py +++ b/app/services/Bybit/functions/Futures.py @@ -1,12 +1,9 @@ import asyncio -import nest_asyncio - import time +import json import logging.config from pybit import exceptions -from pybit.unified_trading import HTTP, WebSocket -from websocket import WebSocketConnectionClosedException - +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 @@ -16,8 +13,6 @@ import app.telegram.Keyboards.inline_keyboards as inline_markup logging.config.dictConfig(LOGGING_CONFIG) logger = logging.getLogger("futures") -nest_asyncio.apply() - def safe_float(val) -> float: """ @@ -33,6 +28,53 @@ def safe_float(val) -> float: return 0.0 +def format_trade_details_position(data): + """ + Форматирует информацию о сделке в виде строки. + """ + 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_fee = float(msg.get('execFee', 0)) + pnl = float(msg.get('execPnl', 0)) + + 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_fee:.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_fee:.6f}" + ) + + def parse_pnl_from_msg(msg) -> float: """ Извлекает реализованную прибыль/убыток из сообщения. @@ -49,8 +91,6 @@ 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 @@ -66,6 +106,9 @@ async def handle_execution_message(message, msg: dict) -> None: positions_list = positions_resp.get('result', {}).get('list', []) position = positions_list[0] if positions_list else None + trade_info = format_trade_details_position(msg) + await message.answer(f"{trade_info}", reply_markup=inline_markup.back_to_main) + liquidation_threshold = -100 if pnl <= liquidation_threshold: @@ -94,64 +137,6 @@ async def handle_execution_message(message, msg: dict) -> None: 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' - text = ( - f"{'Позиция была успешна открыта' if order_type == 'Market' else 'Лимитный ордер установлен'}!\n" - f"Торговая пара: {symbol}\n" - f"Цена входа: {entry_price if order_type == 'Market' else round(limit_price, 5)}\n" - f"Движение: {trade_mode}\n" - f"Тип-маржи: {human_margin_mode}\n" - f"Кредитное плечо: {leverage}x\n" - f"Количество: {qty}\n" - 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)) - - async def error_max_step(message) -> None: """ Сообщение об ошибке превышения максимального количества шагов мартингейла. @@ -340,10 +325,6 @@ async def open_position(tg_id, message, side: str, margin_mode: str, tpsl_mode=' ) 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: @@ -417,7 +398,7 @@ async def set_take_profit_stop_loss(tg_id: int, message, take_profit_price: floa return client = HTTP(api_key=api_key, api_secret=secret_key) - await cancel_all_tp_sl_orders(client, symbol) + await cancel_all_tp_sl_orders(tg_id, symbol) try: try: @@ -648,15 +629,6 @@ async def close_user_trade(tg_id: int, symbol: str, message): if include_fee: pnl -= trade_fee 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}%)\n" - f"{'Включая комиссию биржи' if include_fee else 'Без учета комиссии'}" - ) - await message.answer(text) return True else: if message: diff --git a/app/services/Bybit/functions/functions.py b/app/services/Bybit/functions/functions.py index 40fadc7..60d0098 100644 --- a/app/services/Bybit/functions/functions.py +++ b/app/services/Bybit/functions/functions.py @@ -1,14 +1,12 @@ -import asyncio -import logging.config +import logging.config from aiogram import F, Router -from app.tasks.tasks import active_close_tasks, active_start_tasks +from app.tasks.tasks import handle_stop_close_trade, handle_start_close_trade, handle_stop_trading, handle_start_trading from logger_helper.logger_helper import LOGGING_CONFIG -from app.services.Bybit.functions.Futures import (close_user_trade, close_trade_after_delay, - trading_cycle, open_position, set_take_profit_stop_loss, \ +from app.services.Bybit.functions.Futures import (close_user_trade, 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 import app.telegram.Keyboards.inline_keyboards as inline_markup @@ -35,15 +33,16 @@ async def clb_start_bybit_trade_message(callback: CallbackQuery) -> None: Обработка нажатия кнопок запуска торговли или возврата в главное меню. Отправляет информацию о балансе, символе, цене и инструкциях по торговле. """ - balance = await get_balance(callback.from_user.id, callback.message) - price = await get_price(callback.from_user.id) + user_id = callback.from_user.id + balance = await get_balance(user_id, callback.message) + price = await get_price(user_id) if balance: - symbol = await rq.get_symbol(callback.from_user.id) + symbol = await rq.get_symbol(user_id) text = ( f"💎 Торговля на Bybit\n\n" - f"⚖️ Ваш баланс (USDT): {balance}\n" + f"⚖️ Ваш баланс (USDT): {float(balance):.2f}\n" f"📊 Текущая торговая пара: {symbol}\n" f"$$$ Цена: {price}\n\n" "Как начать торговлю?\n\n" @@ -52,7 +51,6 @@ async def clb_start_bybit_trade_message(callback: CallbackQuery) -> None: "3️⃣ Нажмите кнопку 'Выбрать тип входа' и после нажмите начать торговлю.\n" ) 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) -> None: @@ -240,6 +238,7 @@ async def start_trading_process(callback: CallbackQuery) -> None: await message.answer("Начинаю торговлю с использованием текущих настроек...") + timer_data = await rq.get_user_timer(tg_id) if isinstance(timer_data, dict): timer_minute = timer_data.get('timer_minutes', 0) @@ -247,15 +246,7 @@ async def start_trading_process(callback: CallbackQuery) -> None: timer_minute = timer_data or 0 if timer_minute > 0: - old_task = active_start_tasks.get(tg_id) - if old_task and not old_task.done(): - old_task.cancel() - try: - 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 + await handle_start_trading(tg_id, message) await message.answer(f"Торговля начнётся через {timer_minute} мин. Для отмены нажмите кнопку ниже.", reply_markup=inline_markup.cancel_start_markup) await rq.update_user_timer(tg_id, minutes=0) @@ -281,6 +272,9 @@ async def show_my_trades(callback: CallbackQuery) -> None: @router_functions_bybit_trade.callback_query(F.data == "clb_open_deals") async def show_my_trades_callback(callback: CallbackQuery): + """ + Показывает открытые позиции пользователя по символу. + """ await callback.answer() try: @@ -373,13 +367,9 @@ async def cancel_start_callback(callback: CallbackQuery) -> None: Отменяет задачу старта торговли по таймеру, если она активна. """ 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 handle_stop_close_trade(tg_id) + + await callback.message.answer("Торговля по таймеру отменена.", reply_markup=inline_markup.back_to_main) await callback.answer() @@ -394,7 +384,7 @@ async def close_trade_callback(callback: CallbackQuery) -> None: result = await close_user_trade(tg_id, symbol, message=callback.message) if result: - await callback.message.answer("Сделка успешно закрыта.", reply_markup=inline_markup.back_to_main) + await handle_stop_trading(tg_id) logger.info(f"Сделка {symbol} успешно закрыта.") else: logger.error(f"Не удалось закрыть сделку {symbol}.") @@ -436,17 +426,7 @@ async def process_close_delay(message: Message, state: FSMContext) -> None: delay = delay_minutes * 60 - if tg_id in active_close_tasks: - 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)) - active_close_tasks[tg_id] = task + await handle_start_close_trade(tg_id, message, symbol, delay) await message.answer(f"Закрытие сделки {symbol} запланировано через {delay} секунд.", reply_markup=inline_markup.cancel_start_markup) @@ -458,11 +438,9 @@ async def reset_martingale(callback: CallbackQuery) -> None: """ Сбрасывает шаги мартингейла пользователя. """ - 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) + await callback.answer("Сброс шагов выполнен.") @router_functions_bybit_trade.callback_query(F.data == "clb_cancel") diff --git a/app/telegram/database/requests.py b/app/telegram/database/requests.py index dc86469..b9e2693 100644 --- a/app/telegram/database/requests.py +++ b/app/telegram/database/requests.py @@ -3,29 +3,37 @@ from logger_helper.logger_helper import LOGGING_CONFIG from datetime import datetime, timedelta from typing import Any +from app.telegram.database.models import ( + async_session, + User_Telegram_Id as UTi, + User_Main_Settings as UMS, + User_Bybit_API as UBA, + User_Symbol, + User_Risk_Management_Settings as URMS, + User_Condition_Settings as UCS, + User_Additional_Settings as UAS, + Trading_Mode, + Margin_type, + Trigger, + USER_DEALS, + UserTimer, +) + +from sqlalchemy import select, update + logging.config.dictConfig(LOGGING_CONFIG) logger = logging.getLogger("requests") -from app.telegram.database.models import async_session -from app.telegram.database.models import User_Telegram_Id as UTi -from app.telegram.database.models import User_Main_Settings as UMS -from app.telegram.database.models import User_Bybit_API as UBA -from app.telegram.database.models import User_Symbol -from app.telegram.database.models import User_Risk_Management_Settings as URMS -from app.telegram.database.models import User_Condition_Settings as UCS -from app.telegram.database.models import User_Additional_Settings as UAS -from app.telegram.database.models import Trading_Mode -from app.telegram.database.models import Margin_type -from app.telegram.database.models import Trigger -from app.telegram.database.models import USER_DEALS, UserTimer -import app.telegram.functions.functions as func # functions +# --- Функции сохранения в БД --- -from sqlalchemy import select, delete, update +async def save_tg_id_new_user(tg_id) -> None: + """ + Сохраняет Telegram ID нового пользователя в базу, если такого ещё нет. - -# SET_DB -async def save_tg_id_new_user(tg_id): + Args: + tg_id (int): Telegram ID пользователя. + """ async with async_session() as session: user = await session.scalar(select(UTi).where(UTi.tg_id == tg_id)) @@ -37,26 +45,33 @@ async def save_tg_id_new_user(tg_id): await session.commit() -async def set_new_user_bybit_api(tg_id): +async def set_new_user_bybit_api(tg_id) -> None: + """ + Создаёт запись API пользователя Bybit, если её ещё нет. + + Args: + tg_id (int): Telegram ID пользователя. + """ async with async_session() as session: user = await session.scalar(select(UBA).where(UBA.tg_id == tg_id)) if not user: - session.add(UBA( - tg_id=tg_id, - )) - + session.add(UBA(tg_id=tg_id)) await session.commit() -async def set_new_user_symbol(tg_id): +async def set_new_user_symbol(tg_id) -> None: + """ + Создаёт запись торгового символа пользователя, если её нет. + + Args: + tg_id (int): Telegram ID пользователя. + """ async with async_session() as session: user = await session.scalar(select(User_Symbol).where(User_Symbol.tg_id == tg_id)) if not user: - session.add(User_Symbol( - tg_id=tg_id - )) + session.add(User_Symbol(tg_id=tg_id)) logger.info(f"Symbol был успешно добавлен %s", tg_id) @@ -64,6 +79,14 @@ async def set_new_user_symbol(tg_id): async def set_new_user_default_main_settings(tg_id, trading_mode, margin_type) -> None: + """ + Создаёт основные настройки пользователя по умолчанию. + + Args: + tg_id (int): Telegram ID пользователя. + trading_mode (str): Режим торговли. + margin_type (str): Тип маржи. + """ async with async_session() as session: settings = await session.scalar(select(UMS).where(UMS.tg_id == tg_id)) @@ -80,6 +103,12 @@ async def set_new_user_default_main_settings(tg_id, trading_mode, margin_type) - async def set_new_user_default_risk_management_settings(tg_id) -> None: + """ + Создаёт настройки риск-менеджмента по умолчанию. + + Args: + tg_id (int): Telegram ID пользователя. + """ async with async_session() as session: settings = await session.scalar(select(URMS).where(URMS.tg_id == tg_id)) @@ -94,6 +123,13 @@ async def set_new_user_default_risk_management_settings(tg_id) -> None: async def set_new_user_default_condition_settings(tg_id, trigger) -> None: + """ + Создаёт условные настройки по умолчанию. + + Args: + tg_id (int): Telegram ID пользователя. + trigger (Any): Значение триггера по умолчанию. + """ async with async_session() as session: settings = await session.scalar(select(UCS).where(UCS.tg_id == tg_id)) @@ -109,6 +145,12 @@ async def set_new_user_default_condition_settings(tg_id, trigger) -> None: async def set_new_user_default_additional_settings(tg_id) -> None: + """ + Создаёт дополнительные настройки по умолчанию. + + Args: + tg_id (int): Telegram ID пользователя. + """ async with async_session() as session: settings = await session.scalar(select(UAS).where(UAS.tg_id == tg_id)) @@ -122,32 +164,46 @@ async def set_new_user_default_additional_settings(tg_id) -> None: await session.commit() -# GET_DB +# --- Функции получения данных из БД --- + async def check_user(tg_id): + """ + Проверяет наличие пользователя в базе. + + Args: + tg_id (int): Telegram ID пользователя. + + Returns: + Optional[UTi]: Пользователь или None. + """ async with async_session() as session: user = await session.scalar(select(UTi).where(UTi.tg_id == tg_id)) return user async def get_bybit_api_key(tg_id): + """Получить API ключ Bybit пользователя.""" async with async_session() as session: api_key = await session.scalar(select(UBA.api_key).where(UBA.tg_id == tg_id)) return api_key async def get_bybit_secret_key(tg_id): + """Получить секретный ключ Bybit пользователя.""" async with async_session() as session: secret_key = await session.scalar(select(UBA.secret_key).where(UBA.tg_id == tg_id)) return secret_key async def get_symbol(tg_id): + """Получить символ пользователя.""" async with async_session() as session: symbol = await session.scalar(select(User_Symbol.symbol).where(User_Symbol.tg_id == tg_id)) return symbol async def get_user_trades(tg_id): + """Получить сделки пользователя.""" async with async_session() as session: query = select(USER_DEALS.symbol, USER_DEALS.side).where(USER_DEALS.tg_id == tg_id) result = await session.execute(query) @@ -155,14 +211,63 @@ async def get_user_trades(tg_id): return trades +async def get_entry_order_type(tg_id: object) -> str | None | Any: + """Получить тип входного ордера пользователя.""" + async with async_session() as session: + order_type = await session.scalar( + select(UMS.entry_order_type).where(UMS.tg_id == tg_id) + ) + # Если в базе не установлен тип — возвращаем значение по умолчанию + return order_type or 'Market' + + +# --- Функции обновления данных --- + async def update_user_trades(tg_id, **kwargs): + """Обновить сделки пользователя.""" async with async_session() as session: query = update(USER_DEALS).where(USER_DEALS.tg_id == tg_id).values(**kwargs) await session.execute(query) await session.commit() +async def update_symbol(tg_id: int, symbol: str) -> None: + """Обновить торговый символ пользователя.""" + async with async_session() as session: + await session.execute(update(User_Symbol).where(User_Symbol.tg_id == tg_id).values(symbol=symbol)) + await session.commit() + + +async def update_api_key(tg_id: int, api: str) -> None: + """Обновить API ключ пользователя.""" + async with async_session() as session: + await session.execute(update(UBA).where(UBA.tg_id == tg_id).values(api_key=api)) + await session.commit() + + +async def update_secret_key(tg_id: int, api: str) -> None: + """Обновить секретный ключ пользователя.""" + async with async_session() as session: + await session.execute(update(UBA).where(UBA.tg_id == tg_id).values(secret_key=api)) + await session.commit() + + +# --- Более мелкие обновления и запросы по настройкам --- + +async def update_trade_mode_user(tg_id, trading_mode) -> None: + """Обновить режим торговли пользователя.""" + async with async_session() as session: + mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.mode == trading_mode)) + + if mode: + logger.info("Изменён торговый режим для пользователя %s", tg_id) + await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(trading_mode=mode)) + + await session.commit() + + async def delete_user_trade(tg_id: int, symbol: str): + """Удалить сделку пользователя.""" async with async_session() as session: await session.execute( USER_DEALS.__table__.delete().where( @@ -173,24 +278,28 @@ async def delete_user_trade(tg_id: int, symbol: str): async def get_for_registration_trading_mode(): + """Получить режим торговли по умолчанию.""" async with async_session() as session: mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.id == 1)) return mode async def get_for_registration_margin_type(): + """Получить тип маржи по умолчанию.""" async with async_session() as session: type = await session.scalar(select(Margin_type.type).where(Margin_type.id == 1)) return type async def get_for_registration_trigger(): + """Получить триггер по умолчанию.""" async with async_session() as session: trigger = await session.scalar(select(Trigger.trigger).where(Trigger.id == 1)) return trigger async def get_user_main_settings(tg_id): + """Получить основные настройки пользователя.""" async with async_session() as session: user = await session.scalar(select(UMS).where(UMS.tg_id == tg_id)) @@ -223,6 +332,7 @@ async def get_user_main_settings(tg_id): async def get_user_risk_management_settings(tg_id): + """Получить риск-менеджмента настройки пользователя.""" async with async_session() as session: user = await session.scalar(select(URMS).where(URMS.tg_id == tg_id)) @@ -244,41 +354,8 @@ async def get_user_risk_management_settings(tg_id): return data -# UPDATE_SYMBOL -async def update_symbol(tg_id, symbol) -> None: - async with async_session() as session: - await session.execute(update(User_Symbol).where(User_Symbol.tg_id == tg_id).values(symbol=symbol)) - - await session.commit() - - -async def update_api_key(tg_id, api): - async with async_session() as session: - api_key = await session.execute(update(UBA).where(UBA.tg_id == tg_id).values(api_key=api)) - - await session.commit() - - -async def update_secret_key(tg_id, api): - async with async_session() as session: - secret_key = await session.execute(update(UBA).where(UBA.tg_id == tg_id).values(secret_key=api)) - - await session.commit() - - -# UPDATE_MAIN_SETTINGS_DB -async def update_trade_mode_user(tg_id, trading_mode) -> None: - async with async_session() as session: - mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.mode == trading_mode)) - - if mode: - logger.info("Изменен трейд мод %s", tg_id) - await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(trading_mode=mode)) - - await session.commit() - - async def update_margin_type(tg_id, margin_type) -> None: + """Обновить тип маржи пользователя.""" async with async_session() as session: type = await session.scalar(select(Margin_type.type).where(Margin_type.type == margin_type)) @@ -290,6 +367,7 @@ async def update_margin_type(tg_id, margin_type) -> None: async def update_size_leverange(tg_id, num): + """Обновить размер левеража пользователя.""" async with async_session() as session: await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(size_leverage=num)) @@ -297,6 +375,7 @@ async def update_size_leverange(tg_id, num): async def update_starting_quantity(tg_id, num): + """Обновить размер левеража пользователя.""" async with async_session() as session: await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(starting_quantity=num)) @@ -304,6 +383,7 @@ async def update_starting_quantity(tg_id, num): async def update_martingale_factor(tg_id, num): + """Обновить размер левеража пользователя.""" async with async_session() as session: await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(martingale_factor=num)) @@ -311,15 +391,17 @@ async def update_martingale_factor(tg_id, num): async def update_maximal_quantity(tg_id, num): + """Обновить размер левеража пользователя.""" async with async_session() as session: await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(maximal_quantity=num)) await session.commit() -# UPDATE_RISK_MANAGEMENT_SETTINGS_DB +# ОБНОВЛЕНИЕ НАСТРОЕК РИСК-МЕНЕДЖМЕНТА async def update_price_profit(tg_id, num): + """Обновить цену тейк-профита (прибыль) пользователя.""" async with async_session() as session: await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(price_profit=num)) @@ -327,6 +409,7 @@ async def update_price_profit(tg_id, num): async def update_price_loss(tg_id, num): + """Обновить цену тейк-лосса (убыток) пользователя.""" async with async_session() as session: await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(price_loss=num)) @@ -334,6 +417,7 @@ async def update_price_loss(tg_id, num): async def update_max_risk_deal(tg_id, num): + """Обновить максимальную сумму риска пользователя.""" async with async_session() as session: await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(max_risk_deal=num)) @@ -341,6 +425,7 @@ async def update_max_risk_deal(tg_id, num): async def update_entry_order_type(tg_id, order_type): + """Обновить тип входного ордера пользователя.""" async with async_session() as session: await session.execute( update(UMS) @@ -350,16 +435,8 @@ async def update_entry_order_type(tg_id, order_type): await session.commit() -async def get_entry_order_type(tg_id: object) -> str | None | Any: - async with async_session() as session: - order_type = await session.scalar( - select(UMS.entry_order_type).where(UMS.tg_id == tg_id) - ) - # Если в базе не установлен тип — возвращаем значение по умолчанию - return order_type or 'Market' - - async def get_limit_price(tg_id): + """Получить лимитную цену пользователя как float, либо None.""" async with async_session() as session: result = await session.execute( select(UMS.limit_order_price) @@ -375,6 +452,7 @@ async def get_limit_price(tg_id): async def update_limit_price(tg_id, price): + """Обновить лимитную цену пользователя.""" async with async_session() as session: await session.execute( update(UMS) @@ -385,6 +463,7 @@ async def update_limit_price(tg_id, price): async def update_commission_fee(tg_id, num): + """Обновить комиссию пользователя.""" async with async_session() as session: await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(commission_fee=num)) @@ -392,6 +471,7 @@ async def update_commission_fee(tg_id, num): async def get_user_timer(tg_id): + """Получить данные о таймере пользователя.""" async with async_session() as session: result = await session.execute(select(UserTimer).where(UserTimer.tg_id == tg_id)) user_timer = result.scalars().first() @@ -422,38 +502,39 @@ async def get_user_timer(tg_id): async def update_user_timer(tg_id, minutes: int): + """Обновить данные о таймере пользователя.""" async with async_session() as session: try: - async with async_session() as session: - timer_start = None - timer_end = None + timer_start = None + timer_end = None - if minutes > 0: - timer_start = datetime.utcnow() - timer_end = timer_start + timedelta(minutes=minutes) + if minutes > 0: + timer_start = datetime.utcnow() + timer_end = timer_start + timedelta(minutes=minutes) - result = await session.execute(select(UserTimer).where(UserTimer.tg_id == tg_id)) - user_timer = result.scalars().first() + result = await session.execute(select(UserTimer).where(UserTimer.tg_id == tg_id)) + user_timer = result.scalars().first() - if user_timer: - user_timer.timer_minutes = minutes - user_timer.timer_start = timer_start - user_timer.timer_end = timer_end - else: - user_timer = UserTimer( - tg_id=tg_id, - timer_minutes=minutes, - timer_start=timer_start, - timer_end=timer_end - ) - session.add(user_timer) + if user_timer: + user_timer.timer_minutes = minutes + user_timer.timer_start = timer_start + user_timer.timer_end = timer_end + else: + user_timer = UserTimer( + tg_id=tg_id, + timer_minutes=minutes, + timer_start=timer_start, + timer_end=timer_end + ) + session.add(user_timer) - await session.commit() + await session.commit() except Exception as e: logging.error(f"Ошибка обновления таймера пользователя {tg_id}: {e}") async def get_martingale_step(tg_id): + """Получить шаг мартингейла пользователя.""" async with async_session() as session: result = await session.execute(select(UMS).where(UMS.tg_id == tg_id)) user_settings = result.scalars().first() @@ -461,6 +542,7 @@ async def get_martingale_step(tg_id): async def update_martingale_step(tg_id, step): + """Обновить шаг мартингейла пользователя.""" async with async_session() as session: await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(martingale_step=step)) -- 2.50.1 From f822220c40e74235a7ff982034c9a35b459e0330 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Wed, 27 Aug 2025 12:55:21 +0500 Subject: [PATCH 57/77] Added documentation and update functions --- app/tasks/tasks.py | 88 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 7 deletions(-) diff --git a/app/tasks/tasks.py b/app/tasks/tasks.py index 13becce..a59c859 100644 --- a/app/tasks/tasks.py +++ b/app/tasks/tasks.py @@ -1,24 +1,98 @@ import asyncio - +import logging.config +from typing import Optional from app.services.Bybit.functions.Futures import close_trade_after_delay, trading_cycle +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("tasks") active_start_tasks = {} active_close_tasks = {} -def start_trading_cycle(tg_id, message): +lock_start_tasks = asyncio.Lock() +lock_close_tasks = asyncio.Lock() + + +def start_trading_cycle(tg_id, message) -> None: + """ + Запускает асинхронную задачу торгового цикла для пользователя с указанным tg_id. + """ task = asyncio.create_task(trading_cycle(tg_id, message)) active_start_tasks[tg_id] = task -def stop_trading_cycle(tg_id): - task = active_start_tasks.pop(tg_id, None) + +def stop_trading_cycle(tg_id) -> None: + """ + Останавливает (отменяет) задачу торгового цикла для пользователя с указанным tg_id. + """ + task: Optional[asyncio.Task] = active_start_tasks.pop(tg_id, None) if task: task.cancel() -def start_close_trade_task(tg_id, message, symbol, delay_sec): + +def start_close_trade_task(tg_id, message, symbol, delay_sec) -> None: + """ + Запускает асинхронную задачу автоматического закрытия сделки после задержки. + """ task = asyncio.create_task(close_trade_after_delay(tg_id, message, symbol, delay_sec)) active_close_tasks[tg_id] = task -def stop_close_trade_task(tg_id): - task = active_close_tasks.pop(tg_id, None) + +def stop_close_trade_task(tg_id) -> None: + """ + Останавливает (отменяет) задачу автоматического закрытия сделки для пользователя. + """ + task: Optional[asyncio.Task] = active_close_tasks.pop(tg_id, None) if task: task.cancel() + + +async def handle_start_trading(tg_id: int, message): + """ + Запускает торговый цикл. Если уже есть запущенная задача, отменяет её. + """ + async with lock_start_tasks: + old_task = active_start_tasks.get(tg_id) + if old_task and not old_task.done(): + old_task.cancel() + try: + await old_task + except asyncio.CancelledError: + logger.info(f"Старая задача торговли для пользователя {tg_id} отменена") + start_trading_cycle(tg_id, message) + logger.info(f"Новая задача торговли запущена для пользователя {tg_id}") + + +async def handle_stop_trading(tg_id: int): + """ + Останавливает торговую задачу пользователя, если она активна. + """ + async with lock_start_tasks: + stop_trading_cycle(tg_id) + logger.info(f"Задача торговли остановлена для пользователя {tg_id}") + + +async def handle_start_close_trade(tg_id: int, message, symbol: str, delay_sec: int): + """ + Запускает задачу закрытия сделки с задержкой. Отменяет старую задачу, если есть. + """ + async with lock_close_tasks: + old_task = active_close_tasks.get(tg_id) + if old_task and not old_task.done(): + old_task.cancel() + try: + await old_task + except asyncio.CancelledError: + logger.info(f"Старая задача закрытия сделки пользователя {tg_id} отменена") + start_close_trade_task(tg_id, message, symbol, delay_sec) + logger.info(f"Задача закрытия сделки для {symbol} запущена с задержкой {delay_sec}s для пользователя {tg_id}") + + +async def handle_stop_close_trade(tg_id: int): + """ + Отменяет задачу закрытия сделки пользователя, если она есть. + """ + async with lock_close_tasks: + stop_close_trade_task(tg_id) + logger.info(f"Задача закрытия сделки отменена для пользователя {tg_id}") -- 2.50.1 From 91cfdbc37b996c67bc171c87c8351fb6e743d435 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Wed, 27 Aug 2025 12:55:43 +0500 Subject: [PATCH 58/77] Added loggers --- logger_helper/logger_helper.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/logger_helper/logger_helper.py b/logger_helper/logger_helper.py index 8123460..374f49e 100644 --- a/logger_helper/logger_helper.py +++ b/logger_helper/logger_helper.py @@ -90,5 +90,15 @@ LOGGING_CONFIG = { "level": "DEBUG", "propagate": False, }, + "bybit_ws": { + "handlers": ["console", "timed_rotating_file"], + "level": "DEBUG", + "propagate": False, + }, + "tasks": { + "handlers": ["console", "timed_rotating_file"], + "level": "DEBUG", + "propagate": False, + }, }, } -- 2.50.1 From 78b76b4aa6999866c60ba22429be531dc2a4f4f1 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Wed, 27 Aug 2025 12:56:01 +0500 Subject: [PATCH 59/77] Updated README.md --- README.md | 92 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 73 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 713dc9c..268ab60 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,74 @@ -# Чат-робот STCS -__ +Crypto Trading Telegram Bot -**Функционал:** -+ **Настройки** - + Основные параметры *(Настроен, работает)* - + Режим торговли Лонг/Шорт *(настроены)*, Switch/Smart *(не настроены)* - + Тип маржи: Изолированная / Кросс *(настроено)* - + Размер кредитного плеча: от x1 до x100 *(настроено)* - + Начальная ставка: числовое значение *(настроено)* - + Коэффициент мартингейла: число *(настроено)* - + Максимальное количество ставок в серии: число *(настроено)* - + Риск-менеджмент (Настроен, работает) - + Процент изменения цены для фиксации прибыли (TP%): число *(настроено)* - + Процент изменения цены для фиксации убытков (SL%): число (пример: 1%) *(настроено)* - + Максимальный риск на сделку (в % от баланса): число (опционально) *(настроено)* - + Условия запуска *(Не настроен)* - + Дополнительные параметры *(Не настроен)* - + Подключение Bybit *(настроено)* - + Информация о правильном получении и сохранении Bybit-API keys *(настроено)* +Этот бот — автоматизированный торговый помощник для работы с криптовалютной биржей Bybit на основе стратегии мартингейла. Он позволяет торговать бессрочными контрактами с управлением рисками, тейк-профитами, стоп-лоссами и кредитным плечом. + +##Основные возможности + +- Поддержка работы с биржей Bybit через официальный API. + +- Открытие и закрытие позиций по выбранным торговым парам. + +- Поддержка рыночных и лимитных ордеров. + +- Установка уровней тейк-профита (TP) и стоп-лосса (SL). + +- Управление кредитным плечом (leverage). + +- Реализация стратегии мартингейла с настройками шага, коэффициента и лимитов. + +- Контроль максимального риска на сделку по балансу пользователя. + +- Обработка ошибок API, логирование событий и информирование пользователя. + +- Таймеры для отложенного открытия и закрытия сделок. + +- Интерактивное меню и ввод настроек через Telegram. + +- Хранение пользовательских настроек и статистики в базе данных. + + +##Установка и запуск + +1. Клонируйте репозиторий: + + +git clone + + +2. Установите зависимости: + +pip install -r requirements.txt + + +3. Создайте файл .env и настройте переменные окружения. + + +4. Запустите бота: + +python BybitBot_API.py + + +##Настройки пользователя + +- Кредитное плечо (например, 15x) + +- Торговая пара (например, DOGEUSDT, BTCUSDT) + +- Начальное количество для сделок + +- Тип ордера (Market или Limit) + +- Уровни Take Profit и Stop Loss (в процентах или цене) + +- Коэффициент мартингейла и максимальное количество шагов + +- Максимально допустимый риск на одну сделку (% от баланса) + +- Таймеры для старта и закрытия сделок + + +##Безопасность и риски + +- Бот требует аккуратной настройки параметров риска. + +- Храните API ключи в безопасности, избегайте публикации. \ No newline at end of file -- 2.50.1 From aec8fea6282c4a0577f2edd77a81f1df48eff53a Mon Sep 17 00:00:00 2001 From: algizn97 Date: Wed, 27 Aug 2025 12:56:22 +0500 Subject: [PATCH 60/77] Updated --- app/telegram/handlers/handlers.py | 145 ++++++++++++++++++++++++------ 1 file changed, 120 insertions(+), 25 deletions(-) diff --git a/app/telegram/handlers/handlers.py b/app/telegram/handlers/handlers.py index 5c84064..d5a45df 100644 --- a/app/telegram/handlers/handlers.py +++ b/app/telegram/handlers/handlers.py @@ -1,5 +1,5 @@ import logging.config - +import asyncio from aiogram import F, Router from aiogram.filters import CommandStart from aiogram.types import Message, CallbackQuery @@ -13,23 +13,42 @@ import app.telegram.functions.additional_settings.settings as func_additional_se import app.telegram.database.requests as rq import app.telegram.Keyboards.reply_keyboards as reply_markup + from logger_helper.logger_helper import LOGGING_CONFIG logging.config.dictConfig(LOGGING_CONFIG) logger = logging.getLogger("handlers") - router = Router() @router.message(CommandStart()) -async def start_message(message: Message): +async def start_message(message: Message) -> None: + """ + Обработчик команды /start. + Запускает WebSocket для пользователя и инициализирует нового пользователя в БД. + + Args: + message (Message): Входящее сообщение с командой /start. + """ + from BybitBot_API import run_ws_for_user + tg_id = message.from_user.id + asyncio.create_task(run_ws_for_user(tg_id, message)) + logger.info(f"Получение event loop") + await rq.set_new_user_bybit_api(message.from_user.id) await func.start_message(message) @router.message(F.text == "👤 Профиль") -async def profile_message(message: Message): +async def profile_message(message: Message) -> None: + """ + Обработчик кнопки 'Профиль'. + Проверяет существование пользователя и отображает профиль. + + Args: + message (Message): Сообщение с текстом кнопки. + """ user = await rq.check_user(message.from_user.id) if user: @@ -37,7 +56,14 @@ async def profile_message(message: Message): @router.message(F.text == "Настройки") -async def settings_msg(message: Message): +async def settings_msg(message: Message) -> None: + """ + Обработчик кнопки 'Настройки'. + Проверяет пользователя и отображает меню настроек. + + Args: + message (Message): Сообщение с текстом кнопки. + """ user = await rq.check_user(message.from_user.id) if user: @@ -45,17 +71,20 @@ async def settings_msg(message: Message): @router.callback_query(F.data == "clb_start_chatbot_message") -async def clb_profile_msg(callback: CallbackQuery): +async def clb_profile_msg(callback: CallbackQuery) -> None: + """ + Обработчик колбэка 'clb_start_chatbot_message'. + Если пользователь есть в БД — показывает профиль, + иначе регистрирует нового пользователя и инициализирует настройки. + + Args: + callback (CallbackQuery): Полученный колбэк. + """ user = await rq.check_user(callback.from_user.id) - username = '' - - if callback.from_user.first_name == None: - username = callback.from_user.last_name - elif callback.from_user.last_name == None: - username = callback.from_user.first_name - else: - username = f'{callback.from_user.first_name} {callback.from_user.last_name}' + first_name = callback.from_user.first_name or "" + last_name = callback.from_user.last_name or "" + username = f"{first_name} {last_name}".strip() or callback.from_user.username or "Пользователь" if user: await func.profile_message(callback.from_user.username, callback.message) @@ -75,44 +104,82 @@ async def clb_profile_msg(callback: CallbackQuery): await callback.answer() -# Настройки торговли @router.callback_query(F.data == "clb_settings_message") -async def clb_settings_msg(callback: CallbackQuery): +async def clb_settings_msg(callback: CallbackQuery) -> None: + """ + Показать главное меню настроек. + + Args: + callback (CallbackQuery): полученный колбэк. + """ await func.settings_message(callback.message) await callback.answer() @router.callback_query(F.data == "clb_back_to_special_settings_message") -async def clb_back_to_settings_msg(callback: CallbackQuery): +async def clb_back_to_settings_msg(callback: CallbackQuery) -> None: + """ + Вернуть пользователя к меню специальных настроек. + + Args: + callback (CallbackQuery): полученный колбэк. + """ await func.settings_message(callback.message) await callback.answer() @router.callback_query(F.data == "clb_change_main_settings") -async def clb_change_main_settings_message(callback: CallbackQuery, state: FSMContext): +async def clb_change_main_settings_message(callback: CallbackQuery, state: FSMContext) -> None: + """ + Открыть меню изменения главных настроек. + + Args: + callback (CallbackQuery): полученный колбэк. + state (FSMContext): текущее состояние FSM. + """ await func_main_settings.main_settings_message(callback.from_user.id, callback.message, state) await callback.answer() @router.callback_query(F.data == "clb_change_risk_management_settings") -async def clb_change_risk_management_message(callback: CallbackQuery): +async def clb_change_risk_management_message(callback: CallbackQuery) -> None: + """ + Открыть меню изменения настроек управления рисками. + + Args: + callback (CallbackQuery): полученный колбэк. + """ await func_rmanagement_settings.main_settings_message(callback.from_user.id, callback.message) await callback.answer() @router.callback_query(F.data == "clb_change_condition_settings") -async def clb_change_condition_message(callback: CallbackQuery, state: FSMContext): +async def clb_change_condition_message(callback: CallbackQuery, state: FSMContext) -> None: + """ + Открыть меню изменения настроек условий. + + Args: + callback (CallbackQuery): полученный колбэк. + state (FSMContext): текущее состояние FSM. + """ await func_condition_settings.main_settings_message(callback.from_user.id, callback.message, state) await callback.answer() @router.callback_query(F.data == "clb_change_additional_settings") -async def clb_change_additional_message(callback: CallbackQuery, state: FSMContext): +async def clb_change_additional_message(callback: CallbackQuery, state: FSMContext) -> None: + """ + Открыть меню изменения дополнительных настроек. + + Args: + callback (CallbackQuery): полученный колбэк. + state (FSMContext): текущее состояние FSM. + """ await func_additional_settings.main_settings_message(callback.from_user.id, callback.message, state) await callback.answer() @@ -129,7 +196,14 @@ list_main_settings = ['clb_change_trading_mode', @router.callback_query(F.data.in_(list_main_settings)) -async def clb_main_settings_msg(callback: CallbackQuery, state: FSMContext): +async def clb_main_settings_msg(callback: CallbackQuery, state: FSMContext) -> None: + """ + Обработчик колбэков изменения главных настроек с dispatch через match-case. + + Args: + callback (CallbackQuery): полученный колбэк. + state (FSMContext): текущее состояние FSM. + """ await callback.answer() try: @@ -158,7 +232,14 @@ list_risk_management_settings = ['clb_change_price_profit', @router.callback_query(F.data.in_(list_risk_management_settings)) -async def clb_risk_management_settings_msg(callback: CallbackQuery, state: FSMContext): +async def clb_risk_management_settings_msg(callback: CallbackQuery, state: FSMContext) -> None: + """ + Обработчик изменений настроек управления рисками. + + Args: + callback (CallbackQuery): полученный колбэк. + state (FSMContext): текущее состояние FSM. + """ await callback.answer() try: @@ -186,7 +267,14 @@ list_condition_settings = ['clb_change_trigger', @router.callback_query(F.data.in_(list_condition_settings)) -async def clb_condition_settings_msg(callback: CallbackQuery, state: FSMContext): +async def clb_condition_settings_msg(callback: CallbackQuery, state: FSMContext) -> None: + """ + Обработчик изменений настроек условий трейдинга. + + Args: + callback (CallbackQuery): полученный колбэк. + state (FSMContext): текущее состояние FSM. + """ await callback.answer() try: @@ -216,7 +304,14 @@ list_additional_settings = ['clb_change_save_pattern', @router.callback_query(F.data.in_(list_additional_settings)) -async def clb_additional_settings_msg(callback: CallbackQuery, state: FSMContext): +async def clb_additional_settings_msg(callback: CallbackQuery, state: FSMContext) -> None: + """ + Обработчик дополнительных настроек бота. + + Args: + callback (CallbackQuery): полученный колбэк. + state (FSMContext): текущее состояние FSM. + """ await callback.answer() try: -- 2.50.1 From 3c282975c1414fbaea8b2eced5acd489e1806089 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Wed, 27 Aug 2025 12:56:32 +0500 Subject: [PATCH 61/77] Updated --- app/telegram/Keyboards/reply_keyboards.py | 2 +- app/telegram/functions/functions.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/telegram/Keyboards/reply_keyboards.py b/app/telegram/Keyboards/reply_keyboards.py index daf376f..0678289 100644 --- a/app/telegram/Keyboards/reply_keyboards.py +++ b/app/telegram/Keyboards/reply_keyboards.py @@ -3,4 +3,4 @@ base_buttons_markup = ReplyKeyboardMarkup(keyboard=[ [KeyboardButton(text="👤 Профиль")], # [KeyboardButton(text="Настройки")] -], resize_keyboard=True) \ No newline at end of file +], resize_keyboard=True, one_time_keyboard=True) \ No newline at end of file diff --git a/app/telegram/functions/functions.py b/app/telegram/functions/functions.py index 28e9134..d1b7b9b 100644 --- a/app/telegram/functions/functions.py +++ b/app/telegram/functions/functions.py @@ -10,10 +10,9 @@ async def start_message(message): username = message.from_user.first_name else: username = f'{message.from_user.first_name} {message.from_user.last_name}' - await message.answer(f""" Привет {username}! 👋 - -Добро пожаловать в чат-робот для автоматизации трейдинга — вашего надежного помощника для анализа рынка и принятия взвешенных решений. -""", parse_mode='html', reply_markup=inline_markup.start_markup) + await message.answer(f""" Привет {username}! 👋""", parse_mode='html', reply_markup=reply_markup.base_buttons_markup) + await message.answer("Добро пожаловать в чат-робот для автоматизации трейдинга — вашего надежного помощника для анализа рынка и принятия взвешенных решений.", + parse_mode='html', reply_markup=inline_markup.start_markup) async def profile_message(username, message): await message.answer(f""" @{username} -- 2.50.1 From 4406003a6e684bd01d09fc1b520b01123862b1a8 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Wed, 27 Aug 2025 13:28:33 +0500 Subject: [PATCH 62/77] Fixed --- app/services/Bybit/functions/Futures.py | 23 ++++++++++++++++------- app/telegram/Keyboards/reply_keyboards.py | 2 +- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/app/services/Bybit/functions/Futures.py b/app/services/Bybit/functions/Futures.py index a7104be..9bd7415 100644 --- a/app/services/Bybit/functions/Futures.py +++ b/app/services/Bybit/functions/Futures.py @@ -1,6 +1,7 @@ import asyncio -import time import json +import time + import logging.config from pybit import exceptions from pybit.unified_trading import HTTP @@ -28,7 +29,7 @@ def safe_float(val) -> float: return 0.0 -def format_trade_details_position(data): +def format_trade_details_position(data, commission_fee): """ Форматирует информацию о сделке в виде строки. """ @@ -40,9 +41,15 @@ def format_trade_details_position(data): qty = float(msg.get('execQty', 0)) order_type = msg.get('orderType', 'N/A') side = msg.get('side', '') - commission_fee = float(msg.get('execFee', 0)) + 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 = 'Покупка' @@ -60,7 +67,7 @@ def format_trade_details_position(data): f"Закрыто позиций: {closed_size}\n" f"Тип ордера: {order_type}\n" f"Движение: {movement}\n" - f"Комиссия за сделку: {commission_fee:.6f}\n" + f"Комиссия за сделку: {commission:.6f}\n" f"Реализованная прибыль: {pnl:.6f} USDT" ) else: @@ -71,7 +78,7 @@ def format_trade_details_position(data): f"Количество: {qty}\n" f"Тип ордера: {order_type}\n" f"Движение: {movement}\n" - f"Комиссия за сделку: {commission_fee:.6f}" + f"Комиссия за сделку: {commission:.6f}" ) @@ -91,13 +98,15 @@ 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) @@ -106,7 +115,7 @@ async def handle_execution_message(message, msg: dict) -> None: positions_list = positions_resp.get('result', {}).get('list', []) position = positions_list[0] if positions_list else None - trade_info = format_trade_details_position(msg) + 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 diff --git a/app/telegram/Keyboards/reply_keyboards.py b/app/telegram/Keyboards/reply_keyboards.py index 0678289..df7c56c 100644 --- a/app/telegram/Keyboards/reply_keyboards.py +++ b/app/telegram/Keyboards/reply_keyboards.py @@ -3,4 +3,4 @@ base_buttons_markup = ReplyKeyboardMarkup(keyboard=[ [KeyboardButton(text="👤 Профиль")], # [KeyboardButton(text="Настройки")] -], resize_keyboard=True, one_time_keyboard=True) \ No newline at end of file +], resize_keyboard=True, one_time_keyboard=False) \ No newline at end of file -- 2.50.1 From 02fa03c824852f0df0d6f9c7a4a7d70bdaacb443 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Wed, 27 Aug 2025 13:28:44 +0500 Subject: [PATCH 63/77] Added documentation, added event_loop for tasks, added WebSocket --- BybitBot_API.py | 43 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/BybitBot_API.py b/BybitBot_API.py index 60ef27f..954d84b 100644 --- a/BybitBot_API.py +++ b/BybitBot_API.py @@ -1,11 +1,9 @@ import asyncio import logging.config from aiogram import Bot, Dispatcher -from aiogram.filters import Command, CommandStart -from aiogram.types import Message - +import app.services.Bybit.functions.bybit_ws as bybit_ws +import app.telegram.database.requests as rq from app.telegram.database.models import async_main - from app.telegram.handlers.handlers import router from app.telegram.functions.main_settings.settings import router_main_settings from app.telegram.functions.risk_management_settings.settings import router_risk_management_settings @@ -14,7 +12,7 @@ from app.services.Bybit.functions.Add_Bybit_API import router_register_bybit_api from app.services.Bybit.functions.functions import router_functions_bybit_trade from logger_helper.logger_helper import LOGGING_CONFIG -from config import TOKEN_TG_BOT_1, TOKEN_TG_BOT_2, TOKEN_TG_BOT_3 +from config import TOKEN_TG_BOT_1 logging.config.dictConfig(LOGGING_CONFIG) logger = logging.getLogger("main") @@ -22,7 +20,39 @@ logger = logging.getLogger("main") bot = Bot(token=TOKEN_TG_BOT_1) dp = Dispatcher() -async def main(): + +def get_or_create_event_loop() -> asyncio.AbstractEventLoop: + """ + Возвращает текущий активный цикл событий asyncio или создает новый, если его нет. + """ + try: + return asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + return loop + + +loop = get_or_create_event_loop() +bybit_ws.set_event_loop(loop) + + +async def run_ws_for_user(tg_id, message) -> None: + """ + Запускает WebSocket Bybit для пользователя с указанным tg_id. + """ + + api_key = await rq.get_bybit_api_key(tg_id) + api_secret = await rq.get_bybit_secret_key(tg_id) + + await bybit_ws.start_execution_ws(api_key, api_secret, message) + + +async def main() -> None: + """ + Основная асинхронная функция запуска бота: + """ + await async_main() dp.include_router(router) @@ -34,6 +64,7 @@ async def main(): await dp.start_polling(bot) + if __name__ == '__main__': try: logger.info("Bot is on") -- 2.50.1 From bf44b481e9476aa8b8192eca09c793a01975664c Mon Sep 17 00:00:00 2001 From: algizn97 Date: Wed, 27 Aug 2025 14:28:23 +0500 Subject: [PATCH 64/77] Added new handlers for tasks --- app/services/Bybit/functions/functions.py | 68 +++++++++++++++++++++- app/telegram/Keyboards/inline_keyboards.py | 12 +++- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/app/services/Bybit/functions/functions.py b/app/services/Bybit/functions/functions.py index 60d0098..4ac7ee0 100644 --- a/app/services/Bybit/functions/functions.py +++ b/app/services/Bybit/functions/functions.py @@ -1,4 +1,5 @@ -import logging.config +import asyncio +import logging.config from aiogram import F, Router from app.tasks.tasks import handle_stop_close_trade, handle_start_close_trade, handle_stop_trading, handle_start_trading @@ -246,12 +247,12 @@ async def start_trading_process(callback: CallbackQuery) -> None: timer_minute = timer_data or 0 if timer_minute > 0: - await handle_start_trading(tg_id, message) + await handle_start_trading(tg_id, message, side=side, margin_mode=margin_mode, use_timer=True) 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 handle_start_trading(tg_id, message, side=side, margin_mode=margin_mode, use_timer=False) await callback.answer() @@ -443,6 +444,67 @@ async def reset_martingale(callback: CallbackQuery) -> None: await callback.answer("Сброс шагов выполнен.") +@router_functions_bybit_trade.callback_query(F.data == "clb_stop_trading") +async def confirm_stop_trading(callback: CallbackQuery): + """ + Предлагает пользователю выбрать вариант подтверждение остановки торговли. + """ + await callback.message.answer( + "Выберите вариант остановки торговли:", reply_markup=inline_markup.stop_choice_markup + ) + await callback.answer() + +@router_functions_bybit_trade.callback_query(F.data == "stop_immediately") +async def stop_immediately(callback: CallbackQuery): + """ + Останавливает торговлю немедленно. + """ + tg_id = callback.from_user.id + + await handle_stop_trading(tg_id, use_timer=False) + await callback.message.answer("Торговля остановлена.", reply_markup=inline_markup.back_to_main) + await callback.answer() + +@router_functions_bybit_trade.callback_query(F.data == "stop_with_timer") +async def stop_with_timer_start(callback: CallbackQuery, state: FSMContext): + """ + Запускает диалог с пользователем для задания задержки перед остановкой торговли. + """ + + await state.set_state(CloseTradeTimerState.waiting_for_delay) + await callback.message.answer("Введите задержку в минутах перед остановкой торговли:", reply_markup=inline_markup.cancel) + await callback.answer() + +@router_functions_bybit_trade.message(CloseTradeTimerState.waiting_for_delay) +async def process_stop_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 + + tg_id = message.from_user.id + delay_seconds = delay_minutes * 60 + + # Остановка задачи с таймером через заданную задержку + # Можно реализовать через запуск отдельной асинхронной задачи, которая через delay_seconds отменит торговый цикл + async def delayed_stop(): + await asyncio.sleep(delay_seconds) + await handle_stop_trading(tg_id, use_timer=True) + await message.answer("Торговля по таймеру остановлена.") + + asyncio.create_task(delayed_stop()) + + await message.answer(f"Торговля будет остановлена через {delay_minutes} минут.", reply_markup=inline_markup.back_to_main) + await state.clear() + + @router_functions_bybit_trade.callback_query(F.data == "clb_cancel") async def cancel(callback: CallbackQuery, state: FSMContext) -> None: """ diff --git a/app/telegram/Keyboards/inline_keyboards.py b/app/telegram/Keyboards/inline_keyboards.py index 39b953a..19de990 100644 --- a/app/telegram/Keyboards/inline_keyboards.py +++ b/app/telegram/Keyboards/inline_keyboards.py @@ -32,7 +32,8 @@ trading_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="Настройки", callback_data='clb_settings_message')], [InlineKeyboardButton(text="Мои сделки", callback_data='clb_my_deals')], [InlineKeyboardButton(text="Указать торговую пару", callback_data='clb_update_trading_pair')], - [InlineKeyboardButton(text="Выбрать тип входа", callback_data='clb_update_entry_type')], + [InlineKeyboardButton(text="Начать торговлю", callback_data='clb_update_entry_type')], + [InlineKeyboardButton(text="Остановить торговлю", callback_data='clb_stop_trading')], ]) start_trading_markup = InlineKeyboardMarkup(inline_keyboard=[ @@ -184,3 +185,12 @@ timer_markup = InlineKeyboardMarkup(inline_keyboard=[ cancel_start_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="Отменить таймер", callback_data="clb_stop_timer")] ]) + +stop_choice_markup = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text="Остановить сразу", callback_data="stop_immediately"), + InlineKeyboardButton(text="Остановить по таймеру", callback_data="stop_with_timer"), + ] + ] +) \ No newline at end of file -- 2.50.1 From 704249d0af980dd54e85da6c742ed22f0494ceb7 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Wed, 27 Aug 2025 14:28:53 +0500 Subject: [PATCH 65/77] Fixed and updated tasks --- app/services/Bybit/functions/Futures.py | 2 +- app/tasks/tasks.py | 59 ++++++++++++++++++------- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/app/services/Bybit/functions/Futures.py b/app/services/Bybit/functions/Futures.py index 9bd7415..4412bf4 100644 --- a/app/services/Bybit/functions/Futures.py +++ b/app/services/Bybit/functions/Futures.py @@ -98,7 +98,7 @@ async def handle_execution_message(message, msg: dict) -> None: Обработчик сообщений об исполнении сделки. Логирует событие и проверяет условия для мартингейла и TP. """ - logger.info(f"Исполнена сделка:\n{json.dumps(msg, indent=4, ensure_ascii=False)}") + # logger.info(f"Исполнена сделка:\n{json.dumps(msg, indent=4, ensure_ascii=False)}") pnl = parse_pnl_from_msg(msg) tg_id = message.from_user.id diff --git a/app/tasks/tasks.py b/app/tasks/tasks.py index a59c859..af81ad1 100644 --- a/app/tasks/tasks.py +++ b/app/tasks/tasks.py @@ -1,7 +1,7 @@ import asyncio import logging.config from typing import Optional -from app.services.Bybit.functions.Futures import close_trade_after_delay, trading_cycle +from app.services.Bybit.functions.Futures import close_trade_after_delay, trading_cycle, open_position from logger_helper.logger_helper import LOGGING_CONFIG logging.config.dictConfig(LOGGING_CONFIG) @@ -9,16 +9,17 @@ logger = logging.getLogger("tasks") active_start_tasks = {} active_close_tasks = {} +active_start_tasks_timer = {} lock_start_tasks = asyncio.Lock() lock_close_tasks = asyncio.Lock() -def start_trading_cycle(tg_id, message) -> None: +def start_trading_cycle(tg_id, message, side: str, margin_mode: str, tpsl_mode='Full') -> None: """ Запускает асинхронную задачу торгового цикла для пользователя с указанным tg_id. """ - task = asyncio.create_task(trading_cycle(tg_id, message)) + task = asyncio.create_task(open_position(tg_id, message, side, margin_mode, tpsl_mode)) active_start_tasks[tg_id] = task @@ -31,6 +32,17 @@ def stop_trading_cycle(tg_id) -> None: task.cancel() +def start_trading_for_timer(tg_id, message, side: str, margin_mode: str, tpsl_mode='Full') -> None: + # Старт с задержкой (trading_cycle) + task = asyncio.create_task(trading_cycle(tg_id, message)) + active_start_tasks_timer[tg_id] = task + +def stop_trading_for_timer(tg_id) -> None: + task: Optional[asyncio.Task] = active_start_tasks_timer.pop(tg_id, None) + if task: + task.cancel() + + def start_close_trade_task(tg_id, message, symbol, delay_sec) -> None: """ Запускает асинхронную задачу автоматического закрытия сделки после задержки. @@ -48,29 +60,44 @@ def stop_close_trade_task(tg_id) -> None: task.cancel() -async def handle_start_trading(tg_id: int, message): +async def handle_start_trading(tg_id: int, message, side: str, margin_mode: str, tpsl_mode='Full', use_timer=False): """ Запускает торговый цикл. Если уже есть запущенная задача, отменяет её. """ async with lock_start_tasks: - old_task = active_start_tasks.get(tg_id) - if old_task and not old_task.done(): - old_task.cancel() - try: - await old_task - except asyncio.CancelledError: - logger.info(f"Старая задача торговли для пользователя {tg_id} отменена") - start_trading_cycle(tg_id, message) - logger.info(f"Новая задача торговли запущена для пользователя {tg_id}") + if use_timer: + old_task = active_start_tasks_timer.get(tg_id) + if old_task and not old_task.done(): + old_task.cancel() + try: + await old_task + except asyncio.CancelledError: + logger.info(f"Старая задача торговли с таймером для пользователя {tg_id} отменена") + start_trading_for_timer(tg_id, message, side, margin_mode, tpsl_mode) + logger.info(f"Новая задача торговли с таймером запущена для пользователя {tg_id}") + else: + old_task = active_start_tasks.get(tg_id) + if old_task and not old_task.done(): + old_task.cancel() + try: + await old_task + except asyncio.CancelledError: + logger.info(f"Старая задача торговли для пользователя {tg_id} отменена") + start_trading_cycle(tg_id, message, side, margin_mode, tpsl_mode) + logger.info(f"Новая задача торговли запущена для пользователя {tg_id}") -async def handle_stop_trading(tg_id: int): +async def handle_stop_trading(tg_id: int, use_timer=False): """ Останавливает торговую задачу пользователя, если она активна. """ async with lock_start_tasks: - stop_trading_cycle(tg_id) - logger.info(f"Задача торговли остановлена для пользователя {tg_id}") + if use_timer: + stop_trading_for_timer(tg_id) + logger.info(f"Задача торговли с таймером остановлена для пользователя {tg_id}") + else: + stop_trading_cycle(tg_id) + logger.info(f"Задача торговли остановлена для пользователя {tg_id}") async def handle_start_close_trade(tg_id: int, message, symbol: str, delay_sec: int): -- 2.50.1 From e05b214a8a52681aad748f4aecba1ed18fdee190 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Thu, 28 Aug 2025 11:25:48 +0500 Subject: [PATCH 66/77] Added the ability to get a list of open trades and limit orders, as well as their closures (previously it was possible only for the selected pair) --- app/services/Bybit/functions/Futures.py | 137 ++++++++++-------- app/services/Bybit/functions/functions.py | 60 +++++++- app/telegram/Keyboards/inline_keyboards.py | 18 ++- .../risk_management_settings/settings.py | 6 +- 4 files changed, 147 insertions(+), 74 deletions(-) diff --git a/app/services/Bybit/functions/Futures.py b/app/services/Bybit/functions/Futures.py index 4412bf4..4a46099 100644 --- a/app/services/Bybit/functions/Futures.py +++ b/app/services/Bybit/functions/Futures.py @@ -14,6 +14,8 @@ import app.telegram.Keyboards.inline_keyboards as inline_markup logging.config.dictConfig(LOGGING_CONFIG) logger = logging.getLogger("futures") +processed_trade_ids = set() + def safe_float(val) -> float: """ @@ -100,6 +102,13 @@ async def handle_execution_message(message, msg: dict) -> None: """ # logger.info(f"Исполнена сделка:\n{json.dumps(msg, indent=4, ensure_ascii=False)}") + trade_id = msg.get('data', [{}])[0].get('orderId') + if trade_id in processed_trade_ids: + logger.info(f"Уже обработана сделка {trade_id}, дублирующее уведомление игнорируется") + return + + processed_trade_ids.add(trade_id) + pnl = parse_pnl_from_msg(msg) tg_id = message.from_user.id @@ -475,7 +484,7 @@ async def set_take_profit_stop_loss(tg_id: int, message, take_profit_price: floa 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) @@ -487,26 +496,49 @@ async def cancel_all_tp_sl_orders(tg_id, symbol): for order in orders: order_id = order.get('orderId') + order_symbol = order.get('symbol') 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')}") + else: + last_response = order_symbol except Exception as e: - logger.error(f"Ошибка при отмене ордеров TP/SL: {e}") + logger.error(f"Ошибка при отмене ордера: {e}") return last_response -async def get_active_positions_by_symbol(tg_id, message): +async def get_active_positions(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) + + active_positions = client.get_positions(category='linear', settleCoin='USDT') + + positions = active_positions.get('result', {}).get('list', []) + active_symbols = [pos.get('symbol') for pos in positions if float(pos.get('size', 0)) > 0] + + if active_symbols: + await message.answer("📈 Ваши активные позиции:", + reply_markup=inline_markup.create_trades_inline_keyboard(active_symbols)) + else: + await message.answer("❗️ У вас нет активных позиций.", reply_markup=inline_markup.back_to_main) + return + + +async def get_active_positions_by_symbol(tg_id, symbol, 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 @@ -526,14 +558,33 @@ async def get_active_positions_by_symbol(tg_id, message): await message.answer(text, reply_markup=inline_markup.create_close_deal_markup(symbol)) +async def get_active_orders(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) -async def get_active_orders_by_symbol(tg_id, message): + response = client.get_open_orders(category='linear', settleCoin='USDT', orderType='Limit') + orders = response.get('result', {}).get('list', []) + limit_orders = [order for order in orders if order.get('orderType') == 'Limit'] + + if limit_orders: + symbols = [order['symbol'] for order in limit_orders] + await message.answer("📈 Ваши активные лимитные ордера:", + reply_markup=inline_markup.create_trades_inline_keyboard_limits(symbols)) + else: + await message.answer("❗️ У вас нет активных лимитных ордеров.", reply_markup=inline_markup.back_to_main) + return + + +async def get_active_orders_by_symbol(tg_id, symbol, 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) @@ -561,7 +612,7 @@ async def get_active_orders_by_symbol(tg_id, message): ) texts.append(text) - await message.answer("\n\n".join(texts), reply_markup=inline_markup.create_close_deal_markup(symbol)) + await message.answer("\n\n".join(texts), reply_markup=inline_markup.create_close_limit_markup(symbol)) async def close_user_trade(tg_id: int, symbol: str, message): @@ -574,7 +625,6 @@ async def close_user_trade(tg_id: int, symbol: str, message): 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) @@ -589,66 +639,29 @@ async def close_user_trade(tg_id: int, symbol: str, message): 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 + 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') == 0: + await message.answer(f"Сделка {symbol} успешно закрыта.", reply_markup=inline_markup.back_to_main) + return True + else: + 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) + await message.answer("Произошла ошибка при закрытии сделки.", reply_markup=inline_markup.back_to_main) return False diff --git a/app/services/Bybit/functions/functions.py b/app/services/Bybit/functions/functions.py index 4ac7ee0..f4f7c1a 100644 --- a/app/services/Bybit/functions/functions.py +++ b/app/services/Bybit/functions/functions.py @@ -7,6 +7,7 @@ from logger_helper.logger_helper import LOGGING_CONFIG from app.services.Bybit.functions.Futures import (close_user_trade, open_position, set_take_profit_stop_loss, \ get_active_positions_by_symbol, get_active_orders_by_symbol, + get_active_positions, get_active_orders, cancel_all_tp_sl_orders, ) from app.services.Bybit.functions.balance import get_balance import app.telegram.Keyboards.inline_keyboards as inline_markup @@ -264,8 +265,7 @@ 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}:", + await callback.message.answer(f"Выберите тип сделки:", reply_markup=inline_markup.my_deals_select_markup) except Exception as e: logger.error(f"Произошла ошибка при выборе типа сделки: {e}") @@ -274,16 +274,30 @@ async def show_my_trades(callback: CallbackQuery) -> None: @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) + await get_active_positions(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(lambda c: c.data and c.data.startswith("show_deal_")) +async def show_deal_callback(callback_query: CallbackQuery) -> None: + """ + Показывает сделку пользователя по символу. + """ + await callback_query.answer() + try: + symbol = callback_query.data[len("show_deal_"):] + tg_id = callback_query.from_user.id + await get_active_positions_by_symbol(tg_id, symbol, message=callback_query.message) + except Exception as e: + logger.error(f"Произошла ошибка при выборе сделки: {e}") + await callback_query.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: @@ -293,16 +307,31 @@ 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) + await get_active_orders(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(lambda c: c.data and c.data.startswith("show_limit_")) +async def show_limit_callback(callback_query: CallbackQuery) -> None: + """ + Показывает сделку пользователя по символу. + """ + await callback_query.answer() + try: + symbol = callback_query.data[len("show_limit_"):] + tg_id = callback_query.from_user.id + await get_active_orders_by_symbol(tg_id, symbol, message=callback_query.message) + except Exception as e: + logger.error(f"Произошла ошибка при выборе сделки: {e}") + await callback_query.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: """ - Показывает активные ордера пользователя. + Запускает процесс установки Take Profit и Stop Loss. """ await callback.answer() await state.set_state(SetTP_SL_State.waiting_for_take_profit) @@ -394,6 +423,25 @@ async def close_trade_callback(callback: CallbackQuery) -> None: await callback.answer() +@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_limit:")) +async def close_trade_callback(callback: CallbackQuery) -> None: + """ + Закрывает ордера пользователя по символу. + """ + symbol = callback.data.split(':')[1] + tg_id = callback.from_user.id + + result = await cancel_all_tp_sl_orders(tg_id, symbol) + + if result: + await callback.message.answer(f"Ордер {result} успешно закрыт.", reply_markup=inline_markup.back_to_main) + logger.info(f"Ордер {result} успешно закрыт.") + else: + await callback.message.answer(f"Не удалось закрыть ордер {result}.") + + 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) -> None: """ diff --git a/app/telegram/Keyboards/inline_keyboards.py b/app/telegram/Keyboards/inline_keyboards.py index 19de990..140c05e 100644 --- a/app/telegram/Keyboards/inline_keyboards.py +++ b/app/telegram/Keyboards/inline_keyboards.py @@ -155,15 +155,21 @@ buttons_on_off_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТ my_deals_select_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text='Открытые сделки', callback_data="clb_open_deals"), - InlineKeyboardButton(text='Открытые ордера', callback_data="clb_open_orders")], + InlineKeyboardButton(text='Лимитные ордера', callback_data="clb_open_orders")], back_btn_to_main ]) def create_trades_inline_keyboard(trades): builder = InlineKeyboardBuilder() for trade in trades: - symbol = trade['symbol'] if isinstance(trade, dict) else trade.symbol - builder.button(text=symbol, callback_data=f"show_deal_{symbol}") + builder.button(text=trade, callback_data=f"show_deal_{trade}") + builder.adjust(2) + return builder.as_markup() + +def create_trades_inline_keyboard_limits(trades): + builder = InlineKeyboardBuilder() + for trade in trades: + builder.button(text=trade, callback_data=f"show_limit_{trade}") builder.adjust(2) return builder.as_markup() @@ -176,6 +182,12 @@ def create_close_deal_markup(symbol: str) -> InlineKeyboardMarkup: back_btn_to_main ]) +def create_close_limit_markup(symbol: str) -> InlineKeyboardMarkup: + return InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="Закрыть лимитный ордер", callback_data=f"close_limit:{symbol}")], + [InlineKeyboardButton(text="Установить TP/SL", callback_data="clb_set_tp_sl_")], + back_btn_to_main + ]) timer_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="Установить таймер", callback_data="clb_set_timer")], diff --git a/app/telegram/functions/risk_management_settings/settings.py b/app/telegram/functions/risk_management_settings/settings.py index 53d699d..f600403 100644 --- a/app/telegram/functions/risk_management_settings/settings.py +++ b/app/telegram/functions/risk_management_settings/settings.py @@ -33,7 +33,7 @@ async def price_profit_message(message, state): text = 'Введите число изменения цены для фиксации прибыли: ' - await message.answer(text=text, parse_mode='html', reply_markup=None) + await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.cancel) @router_risk_management_settings.message(update_risk_management_settings.price_profit) @@ -62,7 +62,7 @@ async def price_loss_message(message, state): text = 'Введите число изменения цены для фиксации убытков: ' - await message.answer(text=text, parse_mode='html', reply_markup=None) + await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.cancel) @router_risk_management_settings.message(update_risk_management_settings.price_loss) @@ -111,7 +111,7 @@ async def max_risk_deal_message(message, state): text = 'Введите число (процент от баланса) для изменения максимального риска на сделку: ' - await message.answer(text=text, parse_mode='html', reply_markup=None) + await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.cancel) @router_risk_management_settings.message(update_risk_management_settings.max_risk_deal) -- 2.50.1 From f6130c0b8cda9204240c206dd5c718fae4bbfdec Mon Sep 17 00:00:00 2001 From: algizn97 Date: Fri, 29 Aug 2025 11:41:18 +0500 Subject: [PATCH 67/77] Deleted tasks.py --- app/services/Bybit/functions/bybit_ws.py | 45 ++++++-- app/tasks/tasks.py | 125 ----------------------- 2 files changed, 39 insertions(+), 131 deletions(-) delete mode 100644 app/tasks/tasks.py diff --git a/app/services/Bybit/functions/bybit_ws.py b/app/services/Bybit/functions/bybit_ws.py index 8b8562d..4d0d31e 100644 --- a/app/services/Bybit/functions/bybit_ws.py +++ b/app/services/Bybit/functions/bybit_ws.py @@ -3,26 +3,53 @@ import logging.config from pybit.unified_trading import WebSocket from websocket import WebSocketConnectionClosedException from logger_helper.logger_helper import LOGGING_CONFIG +import app.telegram.database.requests as rq logging.config.dictConfig(LOGGING_CONFIG) logger = logging.getLogger("bybit_ws") event_loop = None # Сюда нужно будет установить event loop из основного приложения +def get_or_create_event_loop() -> asyncio.AbstractEventLoop: + """ + Возвращает текущий активный цикл событий asyncio или создает новый, если его нет. + """ + try: + return asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + return loop + def set_event_loop(loop: asyncio.AbstractEventLoop): global event_loop event_loop = loop -def on_execution_callback(message, msg): +async def run_ws_for_user(tg_id, message) -> None: """ - Callback на событие исполнения сделки. - Безопасно запускает асинхронный обработчик из sync callback. + Запускает WebSocket Bybit для пользователя с указанным tg_id. """ + + api_key = await rq.get_bybit_api_key(tg_id) + api_secret = await rq.get_bybit_secret_key(tg_id) + + await start_execution_ws(api_key, api_secret, message) + + +def on_order_callback(message, msg): if event_loop is not None: - from app.services.Bybit.functions.Futures import handle_execution_message # Импорт внутри, чтобы избежать циклических импортов - asyncio.run_coroutine_threadsafe(handle_execution_message(message, msg), event_loop) + from app.services.Bybit.functions.Futures import handle_order_message + asyncio.run_coroutine_threadsafe(handle_order_message(message, msg), event_loop) + else: + logger.error("Event loop не установлен, callback пропущен.") + + +def on_execution_callback(message, ws_msg): + if event_loop is not None: + from app.services.Bybit.functions.Futures import handle_execution_message + asyncio.run_coroutine_threadsafe(handle_execution_message(message, ws_msg), event_loop) else: logger.error("Event loop не установлен, callback пропущен.") @@ -35,8 +62,14 @@ async def start_execution_ws(api_key: str, api_secret: str, message): reconnect_delay = 5 while True: try: + if not api_key or not api_secret: + logger.error("API_KEY и API_SECRET должны быть указаны для подключения к приватным каналам.") + await asyncio.sleep(reconnect_delay) + continue ws = WebSocket(api_key=api_key, api_secret=api_secret, testnet=False, channel_type="private") - ws.execution_stream(lambda msg: on_execution_callback(message, msg)) + + ws.subscribe("order", lambda ws_msg: on_order_callback(message, ws_msg)) + ws.subscribe("execution", lambda ws_msg: on_execution_callback(message, ws_msg)) while True: await asyncio.sleep(1) # Поддержание активности except WebSocketConnectionClosedException: diff --git a/app/tasks/tasks.py b/app/tasks/tasks.py deleted file mode 100644 index af81ad1..0000000 --- a/app/tasks/tasks.py +++ /dev/null @@ -1,125 +0,0 @@ -import asyncio -import logging.config -from typing import Optional -from app.services.Bybit.functions.Futures import close_trade_after_delay, trading_cycle, open_position -from logger_helper.logger_helper import LOGGING_CONFIG - -logging.config.dictConfig(LOGGING_CONFIG) -logger = logging.getLogger("tasks") - -active_start_tasks = {} -active_close_tasks = {} -active_start_tasks_timer = {} - -lock_start_tasks = asyncio.Lock() -lock_close_tasks = asyncio.Lock() - - -def start_trading_cycle(tg_id, message, side: str, margin_mode: str, tpsl_mode='Full') -> None: - """ - Запускает асинхронную задачу торгового цикла для пользователя с указанным tg_id. - """ - task = asyncio.create_task(open_position(tg_id, message, side, margin_mode, tpsl_mode)) - active_start_tasks[tg_id] = task - - -def stop_trading_cycle(tg_id) -> None: - """ - Останавливает (отменяет) задачу торгового цикла для пользователя с указанным tg_id. - """ - task: Optional[asyncio.Task] = active_start_tasks.pop(tg_id, None) - if task: - task.cancel() - - -def start_trading_for_timer(tg_id, message, side: str, margin_mode: str, tpsl_mode='Full') -> None: - # Старт с задержкой (trading_cycle) - task = asyncio.create_task(trading_cycle(tg_id, message)) - active_start_tasks_timer[tg_id] = task - -def stop_trading_for_timer(tg_id) -> None: - task: Optional[asyncio.Task] = active_start_tasks_timer.pop(tg_id, None) - if task: - task.cancel() - - -def start_close_trade_task(tg_id, message, symbol, delay_sec) -> None: - """ - Запускает асинхронную задачу автоматического закрытия сделки после задержки. - """ - task = asyncio.create_task(close_trade_after_delay(tg_id, message, symbol, delay_sec)) - active_close_tasks[tg_id] = task - - -def stop_close_trade_task(tg_id) -> None: - """ - Останавливает (отменяет) задачу автоматического закрытия сделки для пользователя. - """ - task: Optional[asyncio.Task] = active_close_tasks.pop(tg_id, None) - if task: - task.cancel() - - -async def handle_start_trading(tg_id: int, message, side: str, margin_mode: str, tpsl_mode='Full', use_timer=False): - """ - Запускает торговый цикл. Если уже есть запущенная задача, отменяет её. - """ - async with lock_start_tasks: - if use_timer: - old_task = active_start_tasks_timer.get(tg_id) - if old_task and not old_task.done(): - old_task.cancel() - try: - await old_task - except asyncio.CancelledError: - logger.info(f"Старая задача торговли с таймером для пользователя {tg_id} отменена") - start_trading_for_timer(tg_id, message, side, margin_mode, tpsl_mode) - logger.info(f"Новая задача торговли с таймером запущена для пользователя {tg_id}") - else: - old_task = active_start_tasks.get(tg_id) - if old_task and not old_task.done(): - old_task.cancel() - try: - await old_task - except asyncio.CancelledError: - logger.info(f"Старая задача торговли для пользователя {tg_id} отменена") - start_trading_cycle(tg_id, message, side, margin_mode, tpsl_mode) - logger.info(f"Новая задача торговли запущена для пользователя {tg_id}") - - -async def handle_stop_trading(tg_id: int, use_timer=False): - """ - Останавливает торговую задачу пользователя, если она активна. - """ - async with lock_start_tasks: - if use_timer: - stop_trading_for_timer(tg_id) - logger.info(f"Задача торговли с таймером остановлена для пользователя {tg_id}") - else: - stop_trading_cycle(tg_id) - logger.info(f"Задача торговли остановлена для пользователя {tg_id}") - - -async def handle_start_close_trade(tg_id: int, message, symbol: str, delay_sec: int): - """ - Запускает задачу закрытия сделки с задержкой. Отменяет старую задачу, если есть. - """ - async with lock_close_tasks: - old_task = active_close_tasks.get(tg_id) - if old_task and not old_task.done(): - old_task.cancel() - try: - await old_task - except asyncio.CancelledError: - logger.info(f"Старая задача закрытия сделки пользователя {tg_id} отменена") - start_close_trade_task(tg_id, message, symbol, delay_sec) - logger.info(f"Задача закрытия сделки для {symbol} запущена с задержкой {delay_sec}s для пользователя {tg_id}") - - -async def handle_stop_close_trade(tg_id: int): - """ - Отменяет задачу закрытия сделки пользователя, если она есть. - """ - async with lock_close_tasks: - stop_close_trade_task(tg_id) - logger.info(f"Задача закрытия сделки отменена для пользователя {tg_id}") -- 2.50.1 From 4f774160b3803a8af6144b095e9afed7b6c8a40e Mon Sep 17 00:00:00 2001 From: algizn97 Date: Fri, 29 Aug 2025 11:41:37 +0500 Subject: [PATCH 68/77] Updated States.py --- app/states/States.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/states/States.py b/app/states/States.py index 1f3122d..86cc80f 100644 --- a/app/states/States.py +++ b/app/states/States.py @@ -45,3 +45,13 @@ class state_reg_bybit_api(StatesGroup): """FSM состояние для регистрации API Bybit.""" api_key = State() secret_key = State() + + +class condition_settings(StatesGroup): + """FSM состояние для настройки условий трейдинга.""" + trigger = State() + timer = State() + volatilty = State() + volume = State() + integration = State() + use_tv_signal = State() -- 2.50.1 From f09fe1d70bc487ef050fec4f60978966a59c3263 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Fri, 29 Aug 2025 11:42:03 +0500 Subject: [PATCH 69/77] Added new request --- app/telegram/database/models.py | 6 ++++-- app/telegram/database/requests.py | 32 +++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/app/telegram/database/models.py b/app/telegram/database/models.py index 6b6ab01..fdb6f6e 100644 --- a/app/telegram/database/models.py +++ b/app/telegram/database/models.py @@ -158,6 +158,8 @@ class User_Main_Settings(Base): trading_mode = mapped_column(ForeignKey("trading_modes.mode")) margin_type = mapped_column(ForeignKey("margin_types.type")) + switch_mode_enabled = mapped_column(String(15), default="Выключен") + switch_state = mapped_column(String(10), default='Long') size_leverage = mapped_column(Integer(), default=1) starting_quantity = mapped_column(Integer(), default=1) martingale_factor = mapped_column(Integer(), default=1) @@ -212,7 +214,7 @@ class User_Condition_Settings(Base): tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) - trigger = mapped_column(ForeignKey("triggers.trigger")) + trigger = mapped_column(String(15), default='Ручной') filter_time = mapped_column(String(25), default='???') filter_volatility = mapped_column(Boolean, default=False) external_cues = mapped_column(Boolean, default=False) @@ -309,7 +311,7 @@ async def async_main(): logger.info("Заполение таблицы типов маржи") await conn.execute(Margin_type.__table__.insert().values(type=type)) - triggers = ['Ручной', 'Автоматический', 'TradingView'] + triggers = ['Ручной', 'Автоматический'] for trigger in triggers: result = await conn.execute(select(Trigger).where(Trigger.trigger == trigger)) if not result.first(): diff --git a/app/telegram/database/requests.py b/app/telegram/database/requests.py index b9e2693..d0b108a 100644 --- a/app/telegram/database/requests.py +++ b/app/telegram/database/requests.py @@ -291,10 +291,10 @@ async def get_for_registration_margin_type(): return type -async def get_for_registration_trigger(): +async def get_for_registration_trigger(tg_id): """Получить триггер по умолчанию.""" async with async_session() as session: - trigger = await session.scalar(select(Trigger.trigger).where(Trigger.id == 1)) + trigger = await session.scalar(select(UCS.trigger).where(tg_id == tg_id)) return trigger @@ -308,6 +308,8 @@ async def get_user_main_settings(tg_id): trading_mode = await session.scalar(select(UMS.trading_mode).where(UMS.tg_id == tg_id)) margin_mode = await session.scalar(select(UMS.margin_type).where(UMS.tg_id == tg_id)) + switch_mode_enabled = await session.scalar(select(UMS.switch_mode_enabled).where(UMS.tg_id == tg_id)) + switch_state = await session.scalar(select(UMS.switch_state).where(UMS.tg_id == tg_id)) size_leverage = await session.scalar(select(UMS.size_leverage).where(UMS.tg_id == tg_id)) starting_quantity = await session.scalar(select(UMS.starting_quantity).where(UMS.tg_id == tg_id)) martingale_factor = await session.scalar(select(UMS.martingale_factor).where(UMS.tg_id == tg_id)) @@ -319,6 +321,8 @@ async def get_user_main_settings(tg_id): data = { 'trading_mode': trading_mode, 'margin_type': margin_mode, + 'switch_mode_enabled': switch_mode_enabled, + 'switch_state': switch_state, 'size_leverage': size_leverage, 'starting_quantity': starting_quantity, 'martingale_factor': martingale_factor, @@ -547,3 +551,27 @@ async def update_martingale_step(tg_id, step): await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(martingale_step=step)) await session.commit() + + +async def update_switch_mode_enabled(tg_id, switch_mode): + """Обновить режим переключения пользователя.""" + async with async_session() as session: + await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(switch_mode_enabled=switch_mode)) + + await session.commit() + + +async def update_switch_state(tg_id, switch_state): + """Обновить состояние переключения пользователя.""" + async with async_session() as session: + await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(switch_state=switch_state)) + + await session.commit() + + +async def update_trigger(tg_id, trigger): + """Обновить триггер пользователя.""" + async with async_session() as session: + await session.execute(update(UCS).where(UCS.tg_id == tg_id).values(trigger=trigger)) + + await session.commit() \ No newline at end of file -- 2.50.1 From f4ff1282361b3f29e9c0f2e6443b2d66b591c0e6 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Fri, 29 Aug 2025 11:42:55 +0500 Subject: [PATCH 70/77] Added trigger function --- .../functions/condition_settings/settings.py | 56 +++++++++------- .../functions/main_settings/settings.py | 66 ++++++++++++++++--- 2 files changed, 90 insertions(+), 32 deletions(-) diff --git a/app/telegram/functions/condition_settings/settings.py b/app/telegram/functions/condition_settings/settings.py index 6b385d1..32932eb 100644 --- a/app/telegram/functions/condition_settings/settings.py +++ b/app/telegram/functions/condition_settings/settings.py @@ -1,13 +1,10 @@ -import asyncio -import logging.config +import logging.config import app.telegram.Keyboards.inline_keyboards as inline_markup from aiogram import Router, F from aiogram.types import Message, CallbackQuery from aiogram.fsm.context import FSMContext import app.telegram.database.requests as rq -from aiogram.fsm.state import State, StatesGroup - -from app.services.Bybit.functions.Futures import trading_cycle +from app.states.States import condition_settings from logger_helper.logger_helper import LOGGING_CONFIG @@ -17,27 +14,21 @@ logger = logging.getLogger("condition_settings") condition_settings_router = Router() -class condition_settings(StatesGroup): - trigger = State() - timer = State() - volatilty = State() - volume = State() - integration = State() - use_tv_signal = State() - - -async def reg_new_user_default_condition_settings(id, message): +async def reg_new_user_default_condition_settings(id): tg_id = id - trigger = await rq.get_for_registration_trigger() + trigger = await rq.get_for_registration_trigger(tg_id) await rq.set_new_user_default_condition_settings(tg_id, trigger) -async def main_settings_message(id, message, state): - text = """ Условия запуска +async def main_settings_message(id, message): -- Триггер: Ручной запуск / Сигнал TradingView / Полностью автоматический + tg_id = id + trigger = await rq.get_for_registration_trigger(tg_id) + text = f""" Условия запуска + +- Триггер: {trigger} - Таймер: установить таймер / остановить таймер - Фильтр волатильности / объёма: включить/отключить - Интеграции и внешние сигналы: @@ -48,14 +39,33 @@ async def main_settings_message(id, message, state): await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.condition_settings_markup) -async def trigger_message(message, state): - text = '''Триггер - - Описание ручного запуска, сигналов, автоматического режима ''' +async def trigger_message(id, message, state: FSMContext): + await state.set_state(condition_settings.trigger) + text = ''' +- Автоматический: торговля будет продолжаться до условии остановки. +- Ручной: торговля будет происходить только в ручном режиме. +- Выберите тип триггера:''' await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trigger_markup) +@condition_settings_router.callback_query(F.data == "clb_trigger_manual") +async def trigger_manual_callback(callback: CallbackQuery, state: FSMContext): + await state.set_state(condition_settings.trigger) + await rq.update_trigger(tg_id=callback.from_user.id, trigger="Ручной") + await callback.message.answer("Триггер установлен в ручной режим.") + await main_settings_message(callback.from_user.id, callback.message) + await callback.answer() + + +@condition_settings_router.callback_query(F.data == "clb_trigger_auto") +async def trigger_manual_callback(callback: CallbackQuery, state: FSMContext): + await state.set_state(condition_settings.trigger) + await rq.update_trigger(tg_id=callback.from_user.id, trigger="Автоматический") + await callback.message.answer("Триггер установлен в автоматический режим.") + await main_settings_message(callback.from_user.id, callback.message) + await callback.answer() + async def timer_message(id, message: Message, state: FSMContext): await state.set_state(condition_settings.timer) diff --git a/app/telegram/functions/main_settings/settings.py b/app/telegram/functions/main_settings/settings.py index 2365967..90f1050 100644 --- a/app/telegram/functions/main_settings/settings.py +++ b/app/telegram/functions/main_settings/settings.py @@ -1,4 +1,4 @@ -from aiogram import Router +from aiogram import Router, F import app.telegram.Keyboards.inline_keyboards as inline_markup import app.telegram.Keyboards.reply_keyboards as reply_markup @@ -17,7 +17,8 @@ class update_main_settings(StatesGroup): margin_type = State() martingale_factor = State() starting_quantity = State() - maximal_quantity = State() + maximal_quantity = State() + switch_mode_enabled = State() async def reg_new_user_default_main_settings(id, message): tg_id = id @@ -34,6 +35,8 @@ async def main_settings_message(id, message, state): await message.answer(f"""Основные настройки - Режим торговли: {data['trading_mode']} +- Режим свитч: {data['switch_mode_enabled']} +- Состояние свитча: {data['switch_state']} - Тип маржи: {data['margin_type']} - Размер кредитного плеча: х{data['size_leverage']} - Начальная ставка: {data['starting_quantity']} @@ -52,8 +55,6 @@ async def trading_mode_message(message, state): Шорт — метод продажи активов, взятых в кредит, чтобы получить прибыль от снижения цены. Смарт — автоматизированный режим, который подбирает оптимальную стратегию в зависимости от текущих рыночных условий. - -Свитч — динамическое переключение между торговыми режимами для максимизации эффективности. Выберите ниже для изменений: """, parse_mode='html', reply_markup=inline_markup.trading_mode_markup) @@ -79,12 +80,7 @@ async def state_trading_mode(callback: CallbackQuery, state): await main_settings_message(id, callback.message, state) await state.clear() - case 'trade_mode_switch': - await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Switch") - await rq.update_trade_mode_user(id, 'Switch') - await main_settings_message(id, callback.message, state) - await state.clear() case 'trade_mode_smart': await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Smart") await rq.update_trade_mode_user(id, 'Smart') @@ -94,11 +90,63 @@ async def state_trading_mode(callback: CallbackQuery, state): except Exception as e: print(f"error: {e}") + +async def switch_mode_enabled_message(message, state): + await state.set_state(update_main_settings.switch_mode_enabled) + + await message.edit_text( + """Свитч — динамическое переключение между торговыми режимами для максимизации эффективности. + + Выберите ниже для изменений:""", parse_mode='html', reply_markup=inline_markup.buttons_on_off_markup_for_switch) + + + + +@router_main_settings.callback_query(lambda c: c.data in ["clb_on_switch", "clb_off_switch"]) +async def state_switch_mode_enabled(callback: CallbackQuery, state): + await callback.answer() + tg_id = callback.from_user.id + val = "Включить" if callback.data == "clb_on_switch" else "Выключить" + if val == "Включить": + await rq.update_switch_mode_enabled(tg_id, "Включено") + await callback.message.answer(f"Включено") + await main_settings_message(tg_id, callback.message, state) + else: + await rq.update_switch_mode_enabled(tg_id, "Выключено") + await callback.message.answer(f"Выключено") + await main_settings_message(tg_id, callback.message, state) + await state.clear() + + +@router_main_settings.callback_query(lambda c: c.data in ["clb_switch_state"]) +async def state_switch_mode_enabled(callback: CallbackQuery): + await callback.answer() + await callback.message.answer("Выберите состояние свитча:", reply_markup=inline_markup.switch_state_markup) + + +@router_main_settings.callback_query(lambda c: c.data in ["clb_long_switch", "clb_short_switch"]) +async def state_switch_mode_enabled(callback: CallbackQuery, state): + await callback.answer() + tg_id = callback.from_user.id + val = "Long" if callback.data == "clb_long_switch" else "Short" + if val == "Long": + await rq.update_switch_state(tg_id, "Long") + await callback.message.answer(f"Состояние свитча: {val}") + await main_settings_message(tg_id, callback.message, state) + else: + await rq.update_switch_state(tg_id, "Short") + await callback.message.answer(f"Состояние свитча: {val}") + await main_settings_message(tg_id, callback.message, state) + await state.clear() + + + async def size_leverage_message (message, state): await state.set_state(update_main_settings.size_leverage) await message.edit_text("Введите размер кредитного плеча (от 1 до 100): ", parse_mode='html', reply_markup=inline_markup.back_btn_list_settings_markup) + @router_main_settings.message(update_main_settings.size_leverage) async def state_size_leverage(message: Message, state): await state.update_data(size_leverage = message.text) -- 2.50.1 From 4245e165bf5a136076f910d44dae3b0e59330318 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Fri, 29 Aug 2025 11:43:11 +0500 Subject: [PATCH 71/77] Updated --- app/telegram/Keyboards/inline_keyboards.py | 51 ++++++++++++---------- app/telegram/handlers/handlers.py | 50 ++++++++------------- 2 files changed, 46 insertions(+), 55 deletions(-) diff --git a/app/telegram/Keyboards/inline_keyboards.py b/app/telegram/Keyboards/inline_keyboards.py index 140c05e..ae1cb2c 100644 --- a/app/telegram/Keyboards/inline_keyboards.py +++ b/app/telegram/Keyboards/inline_keyboards.py @@ -6,10 +6,15 @@ start_markup = InlineKeyboardMarkup(inline_keyboard=[ ]) settings_markup = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text="Настройки", callback_data='clb_settings_message')], [InlineKeyboardButton(text="Запуск", callback_data='clb_start_trading')] ]) +back_btn_list_settings = [InlineKeyboardButton(text="Назад", + callback_data='clb_back_to_special_settings_message')] # Кнопка для возврата к списку каталога настроек +back_btn_list_settings_markup = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Назад", + callback_data='clb_back_to_special_settings_message')]]) # Клавиатура для возврата к списку каталога настроек +back_btn_to_main = [InlineKeyboardButton(text="На главную", callback_data='clb_back_to_main')] + back_btn_profile = [InlineKeyboardButton(text="Назад", callback_data='clb_start_chatbot_message')] special_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ @@ -20,8 +25,6 @@ special_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ InlineKeyboardButton(text="Дополнительные параметры", callback_data='clb_change_additional_settings')], [InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api_message')], - - back_btn_profile ]) connect_bybit_api_markup = InlineKeyboardMarkup(inline_keyboard=[ @@ -32,7 +35,7 @@ trading_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="Настройки", callback_data='clb_settings_message')], [InlineKeyboardButton(text="Мои сделки", callback_data='clb_my_deals')], [InlineKeyboardButton(text="Указать торговую пару", callback_data='clb_update_trading_pair')], - [InlineKeyboardButton(text="Начать торговлю", callback_data='clb_update_entry_type')], + [InlineKeyboardButton(text="Начать торговать", callback_data='clb_update_entry_type')], [InlineKeyboardButton(text="Остановить торговлю", callback_data='clb_stop_trading')], ]) @@ -52,15 +55,11 @@ entry_order_type_markup = InlineKeyboardMarkup( [ InlineKeyboardButton(text="Market (текущая цена)", callback_data="entry_order_type:Market"), InlineKeyboardButton(text="Limit (фиксированная цена)", callback_data="entry_order_type:Limit"), - ], + ], back_btn_to_main ] ) -back_btn_list_settings = [InlineKeyboardButton(text="Назад", - callback_data='clb_back_to_special_settings_message')] # Кнопка для возврата к списку каталога настроек -back_btn_list_settings_markup = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Назад", - callback_data='clb_back_to_special_settings_message')]]) # Клавиатура для возврата к списку каталога настроек -back_btn_to_main = [InlineKeyboardButton(text="На главную", callback_data='clb_back_to_main')] + back_to_main = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="На главную", callback_data='back_to_main')], @@ -68,6 +67,7 @@ back_to_main = InlineKeyboardMarkup(inline_keyboard=[ main_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_trading_mode'), + InlineKeyboardButton(text='Режим свитч', callback_data='clb_change_switch_mode'), InlineKeyboardButton(text='Тип маржи', callback_data='clb_change_margin_type')], [InlineKeyboardButton(text='Размер кредитного плеча', callback_data='clb_change_size_leverage'), @@ -120,9 +120,7 @@ additional_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ trading_mode_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="Лонг", callback_data="trade_mode_long"), - InlineKeyboardButton(text="Шорт", callback_data="trade_mode_short")], - - [InlineKeyboardButton(text="Свитч", callback_data="trade_mode_switch"), + InlineKeyboardButton(text="Шорт", callback_data="trade_mode_short"), InlineKeyboardButton(text="Смарт", callback_data="trade_mode_smart")], back_btn_list_settings, @@ -138,9 +136,11 @@ margin_type_markup = InlineKeyboardMarkup(inline_keyboard=[ ]) trigger_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE - [InlineKeyboardButton(text='Ручной', callback_data="clb_trigger_ruchnoy"), - InlineKeyboardButton(text='TradingView', callback_data="clb_trigger_tradingview")], - [InlineKeyboardButton(text="Автоматический", callback_data="clb_trigger_auto")] + [InlineKeyboardButton(text='Ручной', callback_data="clb_trigger_manual")], + # [InlineKeyboardButton(text='TradingView', callback_data="clb_trigger_tradingview")], + [InlineKeyboardButton(text="Автоматический", callback_data="clb_trigger_auto")], + back_btn_list_settings, + back_btn_to_main ]) buttons_yes_no_markup = InlineKeyboardMarkup(inline_keyboard=[ @@ -185,7 +185,6 @@ def create_close_deal_markup(symbol: str) -> InlineKeyboardMarkup: def create_close_limit_markup(symbol: str) -> InlineKeyboardMarkup: return InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="Закрыть лимитный ордер", callback_data=f"close_limit:{symbol}")], - [InlineKeyboardButton(text="Установить TP/SL", callback_data="clb_set_tp_sl_")], back_btn_to_main ]) @@ -194,10 +193,6 @@ timer_markup = InlineKeyboardMarkup(inline_keyboard=[ back_btn_to_main ]) -cancel_start_markup = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text="Отменить таймер", callback_data="clb_stop_timer")] -]) - stop_choice_markup = InlineKeyboardMarkup( inline_keyboard=[ [ @@ -205,4 +200,16 @@ stop_choice_markup = InlineKeyboardMarkup( InlineKeyboardButton(text="Остановить по таймеру", callback_data="stop_with_timer"), ] ] -) \ No newline at end of file +) + +buttons_on_off_markup_for_switch = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE + [InlineKeyboardButton(text='Включить', callback_data="clb_on_switch"), + InlineKeyboardButton(text='Выключить', callback_data="clb_off_switch")], + [InlineKeyboardButton(text="Изменить состояние", callback_data="clb_switch_state")], + back_btn_to_main +]) + +switch_state_markup = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text='Long', callback_data="clb_long_switch"), + InlineKeyboardButton(text='Short', callback_data="clb_short_switch")], +]) \ No newline at end of file diff --git a/app/telegram/handlers/handlers.py b/app/telegram/handlers/handlers.py index d5a45df..3152ae5 100644 --- a/app/telegram/handlers/handlers.py +++ b/app/telegram/handlers/handlers.py @@ -12,7 +12,9 @@ import app.telegram.functions.condition_settings.settings as func_condition_sett import app.telegram.functions.additional_settings.settings as func_additional_settings import app.telegram.database.requests as rq -import app.telegram.Keyboards.reply_keyboards as reply_markup + +from app.services.Bybit.functions.balance import get_balance +from app.services.Bybit.functions.bybit_ws import run_ws_for_user from logger_helper.logger_helper import LOGGING_CONFIG @@ -26,16 +28,11 @@ router = Router() async def start_message(message: Message) -> None: """ Обработчик команды /start. - Запускает WebSocket для пользователя и инициализирует нового пользователя в БД. + Инициализирует нового пользователя в БД. Args: message (Message): Входящее сообщение с командой /start. """ - from BybitBot_API import run_ws_for_user - tg_id = message.from_user.id - asyncio.create_task(run_ws_for_user(tg_id, message)) - logger.info(f"Получение event loop") - await rq.set_new_user_bybit_api(message.from_user.id) await func.start_message(message) @@ -50,26 +47,14 @@ async def profile_message(message: Message) -> None: message (Message): Сообщение с текстом кнопки. """ user = await rq.check_user(message.from_user.id) - - if user: + tg_id = message.from_user.id + balance = await get_balance(message.from_user.id, message) + if user and balance: + asyncio.create_task(run_ws_for_user(tg_id, message)) + logger.info(f"Получение event loop") await func.profile_message(message.from_user.username, message) -@router.message(F.text == "Настройки") -async def settings_msg(message: Message) -> None: - """ - Обработчик кнопки 'Настройки'. - Проверяет пользователя и отображает меню настроек. - - Args: - message (Message): Сообщение с текстом кнопки. - """ - user = await rq.check_user(message.from_user.id) - - if user: - await func.settings_message(message) - - @router.callback_query(F.data == "clb_start_chatbot_message") async def clb_profile_msg(callback: CallbackQuery) -> None: """ @@ -81,12 +66,12 @@ async def clb_profile_msg(callback: CallbackQuery) -> None: callback (CallbackQuery): Полученный колбэк. """ user = await rq.check_user(callback.from_user.id) - + balance = await get_balance(callback.from_user.id, callback.message) first_name = callback.from_user.first_name or "" last_name = callback.from_user.last_name or "" username = f"{first_name} {last_name}".strip() or callback.from_user.username or "Пользователь" - if user: + if user and balance: await func.profile_message(callback.from_user.username, callback.message) else: await rq.save_tg_id_new_user(callback.from_user.id) @@ -94,13 +79,9 @@ async def clb_profile_msg(callback: CallbackQuery) -> None: await func_main_settings.reg_new_user_default_main_settings(callback.from_user.id, callback.message) await func_rmanagement_settings.reg_new_user_default_risk_management_settings(callback.from_user.id, callback.message) - await func_condition_settings.reg_new_user_default_condition_settings(callback.from_user.id, callback.message) + await func_condition_settings.reg_new_user_default_condition_settings(callback.from_user.id) await func_additional_settings.reg_new_user_default_additional_settings(callback.from_user.id, callback.message) - await callback.message.answer(f'Здравствуйте, {username}!', reply_markup=reply_markup.base_buttons_markup) - - await func.profile_message(username, callback.message) - await callback.answer() @@ -166,7 +147,7 @@ async def clb_change_condition_message(callback: CallbackQuery, state: FSMContex callback (CallbackQuery): полученный колбэк. state (FSMContext): текущее состояние FSM. """ - await func_condition_settings.main_settings_message(callback.from_user.id, callback.message, state) + await func_condition_settings.main_settings_message(callback.from_user.id, callback.message) await callback.answer() @@ -187,6 +168,7 @@ async def clb_change_additional_message(callback: CallbackQuery, state: FSMConte # Конкретные настройки каталогов list_main_settings = ['clb_change_trading_mode', + 'clb_change_switch_mode', 'clb_change_margin_type', 'clb_change_size_leverage', 'clb_change_starting_quantity', @@ -210,6 +192,8 @@ async def clb_main_settings_msg(callback: CallbackQuery, state: FSMContext) -> N match callback.data: case 'clb_change_trading_mode': await func_main_settings.trading_mode_message(callback.message, state) + case 'clb_change_switch_mode': + await func_main_settings.switch_mode_enabled_message(callback.message, state) case 'clb_change_margin_type': await func_main_settings.margin_type_message(callback.message, state) case 'clb_change_size_leverage': @@ -280,7 +264,7 @@ async def clb_condition_settings_msg(callback: CallbackQuery, state: FSMContext) try: match callback.data: case 'clb_change_trigger': - await func_condition_settings.trigger_message(callback.message, state) + await func_condition_settings.trigger_message(callback.from_user.id, callback.message, state) case 'clb_change_timer': await func_condition_settings.timer_message(callback.from_user.id, callback.message, state) case 'clb_change_filter_volatility': -- 2.50.1 From 8715b32139ae17f4b14b881af57495bc11ba5d89 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Fri, 29 Aug 2025 11:43:52 +0500 Subject: [PATCH 72/77] Fixed --- app/services/Bybit/functions/functions.py | 84 ++++++++++------------- app/telegram/functions/functions.py | 2 +- 2 files changed, 36 insertions(+), 50 deletions(-) diff --git a/app/services/Bybit/functions/functions.py b/app/services/Bybit/functions/functions.py index f4f7c1a..54b407e 100644 --- a/app/services/Bybit/functions/functions.py +++ b/app/services/Bybit/functions/functions.py @@ -2,12 +2,12 @@ import logging.config from aiogram import F, Router -from app.tasks.tasks import handle_stop_close_trade, handle_start_close_trade, handle_stop_trading, handle_start_trading from logger_helper.logger_helper import LOGGING_CONFIG -from app.services.Bybit.functions.Futures import (close_user_trade, open_position, set_take_profit_stop_loss, \ +from app.services.Bybit.functions.Futures import (close_user_trade, set_take_profit_stop_loss, \ get_active_positions_by_symbol, get_active_orders_by_symbol, get_active_positions, get_active_orders, cancel_all_tp_sl_orders, + trading_cycle, open_position, close_trade_after_delay, ) from app.services.Bybit.functions.balance import get_balance import app.telegram.Keyboards.inline_keyboards as inline_markup @@ -50,7 +50,7 @@ async def clb_start_bybit_trade_message(callback: CallbackQuery) -> None: "Как начать торговлю?\n\n" "1️⃣ Проверьте и тщательно настройте все параметры в вашем профиле.\n" "2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).\n" - "3️⃣ Нажмите кнопку 'Выбрать тип входа' и после нажмите начать торговлю.\n" + "3️⃣ Нажмите кнопку 'Начать торговать'.\n" ) await callback.message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup) @@ -74,7 +74,7 @@ async def start_bybit_trade_message(message: Message) -> None: "Как начать торговлю?\n\n" "1️⃣ Проверьте и тщательно настройте все параметры в вашем профиле.\n" "2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).\n" - "3️⃣ Нажмите кнопку 'Выбрать тип входа' и после нажмите начать торговлю.\n" + "3️⃣ Нажмите кнопку 'Начать торговать'.\n" ) await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup) @@ -124,6 +124,7 @@ async def update_entry_type_message(callback: CallbackQuery, state: FSMContext) await callback.answer() + @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) -> None: """ @@ -196,13 +197,14 @@ async def start_trading_process(callback: CallbackQuery) -> None: symbol = await rq.get_symbol(tg_id) margin_mode = data_main_stgs.get('margin_type', 'Isolated') trading_mode = data_main_stgs.get('trading_mode') + switch_mode = data_main_stgs.get('switch_mode_enabled') if not api_key or not secret_key: await message.answer("❗️ У вас не настроены API ключи для Bybit.") await callback.answer() return - if trading_mode not in ['Long', 'Short', 'Smart', 'Switch']: + if trading_mode not in ['Long', 'Short', 'Smart']: await message.answer(f"❗️ Некорректный торговый режим: {trading_mode}") await callback.answer() return @@ -228,15 +230,20 @@ async def start_trading_process(callback: CallbackQuery) -> None: f"(текущий режим: {existing_margin_mode})", show_alert=True) return - if trading_mode == 'Long': - side = 'Buy' - elif trading_mode == 'Short': - side = 'Sell' + side = None + if switch_mode == 'Включено': + switch_state = data_main_stgs.get('switch_state', 'Long') + side = 'Buy' if switch_state == 'Long' else 'Sell' else: - await message.answer(f"Режим торговли '{trading_mode}' пока не поддерживается.", - reply_markup=inline_markup.back_to_main) - await callback.answer() - return + if trading_mode == 'Long': + side = 'Buy' + elif trading_mode == 'Short': + side = 'Sell' + else: + await message.answer(f"Режим торговли '{trading_mode}' пока не поддерживается.", + reply_markup=inline_markup.back_to_main) + await callback.answer() + return await message.answer("Начинаю торговлю с использованием текущих настроек...") @@ -248,12 +255,11 @@ async def start_trading_process(callback: CallbackQuery) -> None: timer_minute = timer_data or 0 if timer_minute > 0: - await handle_start_trading(tg_id, message, side=side, margin_mode=margin_mode, use_timer=True) - await message.answer(f"Торговля начнётся через {timer_minute} мин. Для отмены нажмите кнопку ниже.", - reply_markup=inline_markup.cancel_start_markup) + await trading_cycle(tg_id, message) + await message.answer(f"Торговля начнётся через {timer_minute} мин.") await rq.update_user_timer(tg_id, minutes=0) else: - await handle_start_trading(tg_id, message, side=side, margin_mode=margin_mode, use_timer=False) + await open_position(tg_id, message, side, margin_mode) await callback.answer() @@ -292,6 +298,7 @@ async def show_deal_callback(callback_query: CallbackQuery) -> None: await callback_query.answer() try: symbol = callback_query.data[len("show_deal_"):] + await rq.update_symbol(callback_query.from_user.id, symbol) tg_id = callback_query.from_user.id await get_active_positions_by_symbol(tg_id, symbol, message=callback_query.message) except Exception as e: @@ -321,6 +328,7 @@ async def show_limit_callback(callback_query: CallbackQuery) -> None: await callback_query.answer() try: symbol = callback_query.data[len("show_limit_"):] + await rq.update_symbol(callback_query.from_user.id, symbol) tg_id = callback_query.from_user.id await get_active_orders_by_symbol(tg_id, symbol, message=callback_query.message) except Exception as e: @@ -391,18 +399,6 @@ async def process_stop_loss(message: Message, state: FSMContext) -> None: await state.clear() -@router_functions_bybit_trade.callback_query(F.data == "clb_stop_timer") -async def cancel_start_callback(callback: CallbackQuery) -> None: - """ - Отменяет задачу старта торговли по таймеру, если она активна. - """ - tg_id = callback.from_user.id - await handle_stop_close_trade(tg_id) - - 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) -> None: """ @@ -414,7 +410,6 @@ async def close_trade_callback(callback: CallbackQuery) -> None: result = await close_user_trade(tg_id, symbol, message=callback.message) if result: - await handle_stop_trading(tg_id) logger.info(f"Сделка {symbol} успешно закрыта.") else: logger.error(f"Не удалось закрыть сделку {symbol}.") @@ -434,7 +429,6 @@ async def close_trade_callback(callback: CallbackQuery) -> None: result = await cancel_all_tp_sl_orders(tg_id, symbol) if result: - await callback.message.answer(f"Ордер {result} успешно закрыт.", reply_markup=inline_markup.back_to_main) logger.info(f"Ордер {result} успешно закрыт.") else: await callback.message.answer(f"Не удалось закрыть ордер {result}.") @@ -450,7 +444,7 @@ async def ask_close_delay(callback: CallbackQuery, state: FSMContext) -> None: 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.message.answer("Введите задержку в минутах до закрытия сделки:", reply_markup=inline_markup.cancel) await callback.answer() @@ -458,7 +452,7 @@ async def ask_close_delay(callback: CallbackQuery, state: FSMContext) -> None: @router_functions_bybit_trade.message(CloseTradeTimerState.waiting_for_delay) async def process_close_delay(message: Message, state: FSMContext) -> None: """ - Обрабатывает ввод задержки и запускает задачу закрытия сделки с задержкой. + Обрабатывает ввод закрытия сделки с задержкой. """ try: delay_minutes = int(message.text.strip()) @@ -471,14 +465,10 @@ async def process_close_delay(message: Message, state: FSMContext) -> None: data = await state.get_data() symbol = data.get("symbol") - tg_id = message.from_user.id delay = delay_minutes * 60 - - await handle_start_close_trade(tg_id, message, symbol, delay) - - await message.answer(f"Закрытие сделки {symbol} запланировано через {delay} секунд.", - reply_markup=inline_markup.cancel_start_markup) + await message.answer(f"Закрытие сделки {symbol} запланировано через {delay_minutes} мин.") + await close_trade_after_delay(message.from_user.id, message, symbol, delay) await state.clear() @@ -509,7 +499,7 @@ async def stop_immediately(callback: CallbackQuery): """ tg_id = callback.from_user.id - await handle_stop_trading(tg_id, use_timer=False) + await rq.update_trigger(tg_id, "Ручной") await callback.message.answer("Торговля остановлена.", reply_markup=inline_markup.back_to_main) await callback.answer() @@ -540,16 +530,12 @@ async def process_stop_delay(message: Message, state: FSMContext): tg_id = message.from_user.id delay_seconds = delay_minutes * 60 - # Остановка задачи с таймером через заданную задержку - # Можно реализовать через запуск отдельной асинхронной задачи, которая через delay_seconds отменит торговый цикл - async def delayed_stop(): - await asyncio.sleep(delay_seconds) - await handle_stop_trading(tg_id, use_timer=True) - await message.answer("Торговля по таймеру остановлена.") + await message.answer(f"Торговля будет остановлена через {delay_minutes} минут.", + reply_markup=inline_markup.back_to_main) + await asyncio.sleep(delay_seconds) + await rq.update_trigger(tg_id, "Ручной") + await message.answer("Торговля по таймеру остановлена.") - asyncio.create_task(delayed_stop()) - - await message.answer(f"Торговля будет остановлена через {delay_minutes} минут.", reply_markup=inline_markup.back_to_main) await state.clear() diff --git a/app/telegram/functions/functions.py b/app/telegram/functions/functions.py index d1b7b9b..19ab77b 100644 --- a/app/telegram/functions/functions.py +++ b/app/telegram/functions/functions.py @@ -10,7 +10,7 @@ async def start_message(message): username = message.from_user.first_name else: username = f'{message.from_user.first_name} {message.from_user.last_name}' - await message.answer(f""" Привет {username}! 👋""", parse_mode='html', reply_markup=reply_markup.base_buttons_markup) + await message.answer(f""" Привет {username}! 👋""", parse_mode='html') await message.answer("Добро пожаловать в чат-робот для автоматизации трейдинга — вашего надежного помощника для анализа рынка и принятия взвешенных решений.", parse_mode='html', reply_markup=inline_markup.start_markup) -- 2.50.1 From 3462078a476ec42fee87749dc936f0b542102570 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Fri, 29 Aug 2025 11:44:24 +0500 Subject: [PATCH 73/77] Fixed --- BybitBot_API.py | 33 ++++----------------------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/BybitBot_API.py b/BybitBot_API.py index 954d84b..1eff3a4 100644 --- a/BybitBot_API.py +++ b/BybitBot_API.py @@ -1,8 +1,8 @@ import asyncio import logging.config from aiogram import Bot, Dispatcher -import app.services.Bybit.functions.bybit_ws as bybit_ws -import app.telegram.database.requests as rq + +from app.services.Bybit.functions.bybit_ws import get_or_create_event_loop, set_event_loop from app.telegram.database.models import async_main from app.telegram.handlers.handlers import router from app.telegram.functions.main_settings.settings import router_main_settings @@ -21,37 +21,12 @@ bot = Bot(token=TOKEN_TG_BOT_1) dp = Dispatcher() -def get_or_create_event_loop() -> asyncio.AbstractEventLoop: - """ - Возвращает текущий активный цикл событий asyncio или создает новый, если его нет. - """ - try: - return asyncio.get_running_loop() - except RuntimeError: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - return loop - - -loop = get_or_create_event_loop() -bybit_ws.set_event_loop(loop) - - -async def run_ws_for_user(tg_id, message) -> None: - """ - Запускает WebSocket Bybit для пользователя с указанным tg_id. - """ - - api_key = await rq.get_bybit_api_key(tg_id) - api_secret = await rq.get_bybit_secret_key(tg_id) - - await bybit_ws.start_execution_ws(api_key, api_secret, message) - - async def main() -> None: """ Основная асинхронная функция запуска бота: """ + loop = get_or_create_event_loop() + set_event_loop(loop) await async_main() -- 2.50.1 From 2ee8c9916f690c6606f52ac84160df8def269d4e Mon Sep 17 00:00:00 2001 From: algizn97 Date: Sat, 30 Aug 2025 16:29:56 +0500 Subject: [PATCH 74/77] Fixed --- app/services/Bybit/functions/Futures.py | 630 ++++++++++-------- app/services/Bybit/functions/bybit_ws.py | 14 +- app/services/Bybit/functions/functions.py | 65 +- app/services/Bybit/functions/min_qty.py | 2 +- app/services/Bybit/functions/price_symbol.py | 3 +- app/states/States.py | 11 + app/telegram/Keyboards/inline_keyboards.py | 3 +- app/telegram/database/models.py | 22 +- app/telegram/database/requests.py | 1 + .../functions/additional_settings/settings.py | 2 +- .../functions/condition_settings/settings.py | 4 +- app/telegram/functions/functions.py | 2 +- .../functions/main_settings/settings.py | 181 ++--- .../risk_management_settings/settings.py | 10 +- app/telegram/handlers/handlers.py | 22 +- logger_helper/logger_helper.py | 10 + 16 files changed, 523 insertions(+), 459 deletions(-) diff --git a/app/services/Bybit/functions/Futures.py b/app/services/Bybit/functions/Futures.py index 4a46099..f45d08b 100644 --- a/app/services/Bybit/functions/Futures.py +++ b/app/services/Bybit/functions/Futures.py @@ -17,6 +17,18 @@ logger = logging.getLogger("futures") processed_trade_ids = set() +async def get_bybit_client(tg_id): + """ + Асинхронно получает экземпляр клиента Bybit. + + :param tg_id: int - ID пользователя Telegram + :return: HTTP - экземпляр клиента Bybit + """ + api_key = await rq.get_bybit_api_key(tg_id) + secret_key = await rq.get_bybit_secret_key(tg_id) + return HTTP(api_key=api_key, api_secret=secret_key) + + def safe_float(val) -> float: """ Безопасное преобразование значения в float. @@ -37,20 +49,17 @@ def format_trade_details_position(data, commission_fee): """ msg = data.get('data', [{}])[0] - closed_size = float(msg.get('closedSize', 0)) + closed_size = safe_float(msg.get('closedSize', 0)) symbol = msg.get('symbol', 'N/A') - entry_price = float(msg.get('execPrice', 0)) - qty = float(msg.get('execQty', 0)) + entry_price = safe_float(msg.get('execPrice', 0)) + qty = safe_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)) + commission = safe_float(msg.get('execFee', 0)) + pnl = safe_float(msg.get('execPnl', 0)) if commission_fee == "Да": - if pnl >= 0: - pnl -= commission - else: - pnl -= commission + pnl -= commission movement = '' if side.lower() == 'buy': @@ -72,7 +81,7 @@ def format_trade_details_position(data, commission_fee): f"Комиссия за сделку: {commission:.6f}\n" f"Реализованная прибыль: {pnl:.6f} USDT" ) - else: + if order_type == 'Market': return ( f"Сделка открыта:\n" f"Торговая пара: {symbol}\n" @@ -82,6 +91,77 @@ def format_trade_details_position(data, commission_fee): f"Движение: {movement}\n" f"Комиссия за сделку: {commission:.6f}" ) + return None + + +def format_order_details_position(data): + """ + Форматирует информацию об ордере в виде строки. + """ + msg = data.get('data', [{}])[0] + price = safe_float(msg.get('price', 0)) + qty = safe_float(msg.get('qty', 0)) + cum_exec_qty = safe_float(msg.get('cumExecQty', 0)) + cum_exec_fee = safe_float(msg.get('cumExecFee', 0)) + take_profit = safe_float(msg.get('takeProfit', 0)) + stop_loss = safe_float(msg.get('stopLoss', 0)) + order_status = msg.get('orderStatus', 'N/A') + symbol = msg.get('symbol', 'N/A') + order_type = msg.get('orderType', 'N/A') + side = msg.get('side', '') + + movement = '' + if side.lower() == 'buy': + movement = 'Покупка' + elif side.lower() == 'sell': + movement = 'Продажа' + else: + movement = side + + if order_status.lower() == 'filled' and order_type.lower() == 'limit': + text = ( + f"Ордер исполнен:\n" + f"Торговая пара: {symbol}\n" + f"Цена исполнения: {price:.6f}\n" + f"Количество: {qty}\n" + f"Исполнено позиций: {cum_exec_qty}\n" + f"Тип ордера: {order_type}\n" + f"Движение: {movement}\n" + f"Тейк-профит: {take_profit:.6f}\n" + f"Стоп-лосс: {stop_loss:.6f}\n" + f"Комиссия за сделку: {cum_exec_fee:.6f}\n" + ) + logger.info(text) + return text + + elif order_status.lower() == 'new': + text = ( + f"Ордер создан:\n" + f"Торговая пара: {symbol}\n" + f"Цена: {price:.6f}\n" + f"Количество: {qty}\n" + f"Тип ордера: {order_type}\n" + f"Движение: {movement}\n" + f"Тейк-профит: {take_profit:.6f}\n" + f"Стоп-лосс: {stop_loss:.6f}\n" + ) + logger.info(text) + return text + + elif order_status.lower() == 'cancelled': + text = ( + f"Ордер отменен:\n" + f"Торговая пара: {symbol}\n" + f"Цена: {price:.6f}\n" + f"Количество: {qty}\n" + f"Тип ордера: {order_type}\n" + f"Движение: {movement}\n" + f"Тейк-профит: {take_profit:.6f}\n" + f"Стоп-лосс: {stop_loss:.6f}\n" + ) + logger.info(text) + return text + return None def parse_pnl_from_msg(msg) -> float: @@ -89,70 +169,79 @@ def parse_pnl_from_msg(msg) -> float: Извлекает реализованную прибыль/убыток из сообщения. """ try: - return float(msg.get('realisedPnl', 0)) + data = msg.get('data', [{}])[0] + return float(data.get('execPnl', 0)) except Exception as e: logger.error(f"Ошибка при извлечении реализованной прибыли: {e}") return 0.0 -async def handle_execution_message(message, msg: dict) -> None: +async def handle_execution_message(message, msg): """ Обработчик сообщений об исполнении сделки. Логирует событие и проверяет условия для мартингейла и TP. """ # logger.info(f"Исполнена сделка:\n{json.dumps(msg, indent=4, ensure_ascii=False)}") - - trade_id = msg.get('data', [{}])[0].get('orderId') - if trade_id in processed_trade_ids: - logger.info(f"Уже обработана сделка {trade_id}, дублирующее уведомление игнорируется") - return - - processed_trade_ids.add(trade_id) - - pnl = parse_pnl_from_msg(msg) tg_id = message.from_user.id - - data_main_stgs = await rq.get_user_main_settings(tg_id) + data = msg.get('data', [{}])[0] 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 + pnl = parse_pnl_from_msg(msg) + data_main_stgs = await rq.get_user_main_settings(tg_id) + symbol = data.get('symbol') + switch_mode = data_main_stgs.get('switch_mode', 'Включено') + trading_mode = data_main_stgs.get('trading_mode', 'Long') + trigger = await rq.get_for_registration_trigger(tg_id) + margin_mode = data_main_stgs.get('margin_type', 'Isolated') + starting_quantity = safe_float(data_main_stgs.get('starting_quantity')) - trade_info = format_trade_details_position(msg, commission_fee=commission_fee) - await message.answer(f"{trade_info}", reply_markup=inline_markup.back_to_main) + trade_info = format_trade_details_position(data=msg, commission_fee=commission_fee) - liquidation_threshold = -100 + if trade_info: + await message.answer(f"{trade_info}", reply_markup=inline_markup.back_to_main) - 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 = None + if switch_mode == 'Включено': + switch_state = data_main_stgs.get('switch_state', 'Long') + side = 'Buy' if switch_state == 'Long' else 'Sell' + else: + if trading_mode == 'Long': + side = 'Buy' + elif trading_mode == 'Short': + side = 'Sell' + else: + side = 'Buy' - 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) + if trigger == "Автоматический": + if pnl < 0: + martingale_factor = safe_float(data_main_stgs.get('martingale_factor')) + current_martingale = await rq.get_martingale_step(tg_id) + current_martingale_step = int(current_martingale) + current_martingale += 1 + next_quantity = float(starting_quantity) * (float(martingale_factor) ** current_martingale_step) + await rq.update_martingale_step(tg_id, current_martingale) + await open_position(tg_id, message, side=side, margin_mode=margin_mode, symbol=symbol, + quantity=next_quantity) - elif position: - entry_price = safe_float(position.get('avgPrice')) - side = position.get('side', '') - current_price = float(position.get('markPrice', 0)) + elif pnl > 0: + await rq.update_martingale_step(tg_id, 0) + await message.answer("❗️ Прибыль достигнута, шаг мартингейла сброшен. " + "Начинаем новую серию ставок") + await open_position(tg_id, message, side=side, margin_mode=margin_mode, symbol=symbol, + quantity=starting_quantity) - 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 handle_order_message(message, msg: dict) -> None: + """ + Обработчик сообщений об исполнении ордера. + Логирует событие и проверяет условия для мартингейла и TP. + """ + # logger.info(f"Исполнен ордер:\n{json.dumps(msg, indent=4, ensure_ascii=False)}") + + trade_info = format_order_details_position(msg) + + if trade_info: + await message.answer(f"{trade_info}", reply_markup=inline_markup.back_to_main) async def error_max_step(message) -> None: @@ -173,131 +262,49 @@ async def error_max_risk(message) -> None: 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, symbol, quantity, 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 + client = await get_bybit_client(tg_id) + data_main_stgs = await rq.get_user_main_settings(tg_id) + order_type = data_main_stgs.get('entry_order_type') + bybit_margin_mode = 'ISOLATED_MARGIN' if margin_mode == 'Isolated' else 'REGULAR_MARGIN' + + 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) - try: balance = await balance_g.get_balance(tg_id, message) - price = await price_symbol.get_price(tg_id) + price = await price_symbol.get_price(tg_id, symbol=symbol) + entry_price = safe_float(price) - 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')) - + current_martingale = await rq.get_martingale_step(tg_id) 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 + if order_type == 'Limit' and limit_price: + price_for_calc = limit_price else: - entry_price = price + price_for_calc = entry_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) + potential_loss = safe_float(quantity) * price_for_calc * (loss_profit / 100) allowed_loss = safe_float(balance) * (max_risk_percent / 100) + if max_martingale_steps == current_martingale: + await error_max_step(message) + return + 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 + client.set_margin_mode(setMarginMode=bybit_margin_mode) leverage = int(data_main_stgs.get('size_leverage', 1)) try: @@ -313,53 +320,157 @@ async def open_position(tg_id, message, side: str, margin_mode: str, tpsl_mode=' 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 + 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)) - 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 min_order_value_api == 0: + min_order_value_api = 5.0 + min_order_value_calc = min_order_qty * price_for_calc 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 - 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) + order_value = float(quantity) * price_for_calc + 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 + if bybit_margin_mode == 'ISOLATED_MARGIN': + # Открываем позицию + response = client.place_order( + category='linear', + symbol=symbol, + side=side, + orderType=order_type, + qty=str(quantity), + price=str(limit_price) if order_type == 'Limit' and limit_price else None, + timeInForce='GTC', + orderLinkId=f"deal_{symbol}_{int(time.time())}" + ) + if response.get('retCode', -1) != 0: + logger.error(f"Ошибка открытия ордера: {response}") + await message.answer(f"Ошибка открытия ордера", reply_markup=inline_markup.back_to_main) + return False + + # Получаем цену ликвидации + positions = client.get_positions(category='linear', symbol=symbol) + pos = positions.get('result', {}).get('list', [{}])[0] + avg_price = float(pos.get('avgPrice', 0)) + liq_price = safe_float(pos.get('liqPrice', 0)) + + if liq_price > 0 and avg_price > 0: + if side.lower() == 'buy': + take_profit_price = avg_price + (avg_price - liq_price) + else: + take_profit_price = avg_price - (liq_price - avg_price) + + take_profit_price = max(take_profit_price, 0) + + try: + 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 + resp = client.set_trading_stop( + category='linear', + symbol=symbol, + takeProfit=str(round(take_profit_price, 5)), + tpTriggerBy='LastPrice', + slTriggerBy='LastPrice', + positionIdx=0, + reduceOnly=False, + tpslMode=tpsl_mode + ) + except Exception as e: + logger.error(f"Ошибка установки TP/SL: {e}") + await message.answer('Ошибка при установке Take Profit и Stop Loss.', + reply_markup=inline_markup.back_to_main) + return False + else: + logger.warning("Не удалось получить цену ликвидации для позиции") + + else: # REGULAR_MARGIN + 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 + + if order_type == 'Market': + base_price = entry_price + else: + base_price = limit_price + + if side.lower() == 'buy': + take_profit_price = base_price * (1 + loss_profit / 100) + stop_loss_price = base_price * (1 - loss_profit / 100) + else: + take_profit_price = base_price * (1 - loss_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) + + 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(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: + return True + else: + logger.error(f"Ошибка открытия ордера: {response}") + await message.answer(f"Ошибка открытия ордера", reply_markup=inline_markup.back_to_main) + return False + + return None except exceptions.InvalidRequestError as e: - logger.error(f"InvalidRequestError: {e}") + logger.error(f"InvalidRequestError: {e}", exc_info=True) await message.answer('Недостаточно средств для размещения нового ордера с заданным количеством и плечом.', reply_markup=inline_markup.back_to_main) except Exception as e: - logger.error(f"Ошибка при совершении сделки: {e}") + logger.error(f"Ошибка при совершении сделки: {e}", exc_info=True) await message.answer('Возникла ошибка при попытке открыть позицию.', reply_markup=inline_markup.back_to_main) -async def trading_cycle(tg_id, message): +async def trading_cycle(tg_id, message, side, margin_mode, symbol, starting_quantity): """ Цикл торговой логики с учётом таймера пользователя. """ @@ -376,13 +487,10 @@ async def trading_cycle(tg_id, message): 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) + await open_position(tg_id, message, side=side, margin_mode=margin_mode, symbol=symbol, + quantity=starting_quantity) except asyncio.CancelledError: - logger.info(f"Торговый цикл для пользователя {tg_id} был отменён.") + logger.info(f"Торговый цикл для пользователя {tg_id} был отменён.", exc_info=True) async def set_take_profit_stop_loss(tg_id: int, message, take_profit_price: float, stop_loss_price: float, @@ -390,19 +498,8 @@ async def set_take_profit_stop_loss(tg_id: int, message, take_profit_price: floa """ Устанавливает уровни 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 @@ -415,7 +512,7 @@ async def set_take_profit_stop_loss(tg_id: int, message, take_profit_price: floa await message.answer("Не удалось определить сторону сделки.") return - client = HTTP(api_key=api_key, api_secret=secret_key) + client = await get_bybit_client(tg_id) await cancel_all_tp_sl_orders(tg_id, symbol) try: @@ -427,52 +524,22 @@ async def set_take_profit_stop_loss(tg_id: int, message, take_profit_price: floa else: raise - positions_resp = client.get_positions(category='linear', symbol=symbol) - positions = positions_resp.get('result', {}).get('list', []) + 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', + positionIdx=0, + reduceOnly=False, + tpslMode=tpsl_mode + ) - 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 + 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}", @@ -486,9 +553,7 @@ async def cancel_all_tp_sl_orders(tg_id, symbol): """ Отменяет лимитные ордера для указанного символа. """ - 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) + client = await get_bybit_client(tg_id) last_response = None try: orders_resp = client.get_open_orders(category='linear', symbol=symbol) @@ -512,12 +577,8 @@ async def get_active_positions(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) - + client = await get_bybit_client(tg_id) active_positions = client.get_positions(category='linear', settleCoin='USDT') - positions = active_positions.get('result', {}).get('list', []) active_symbols = [pos.get('symbol') for pos in positions if float(pos.get('size', 0)) > 0] @@ -533,12 +594,8 @@ async def get_active_positions_by_symbol(tg_id, symbol, 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) - + client = await get_bybit_client(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 @@ -558,14 +615,12 @@ async def get_active_positions_by_symbol(tg_id, symbol, message): await message.answer(text, reply_markup=inline_markup.create_close_deal_markup(symbol)) + async def get_active_orders(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) - + client = await get_bybit_client(tg_id) response = client.get_open_orders(category='linear', settleCoin='USDT', orderType='Limit') orders = response.get('result', {}).get('list', []) limit_orders = [order for order in orders if order.get('orderType') == 'Limit'] @@ -583,10 +638,7 @@ async def get_active_orders_by_symbol(tg_id, symbol, 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) - + client = await get_bybit_client(tg_id) active_orders = client.get_open_orders(category='linear', symbol=symbol) limit_orders = [ order for order in active_orders.get('result', {}).get('list', []) @@ -608,26 +660,19 @@ async def get_active_orders_by_symbol(tg_id, symbol, message): 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_limit_markup(symbol)) -async def close_user_trade(tg_id: int, symbol: str, message): +async def close_user_trade(tg_id: int, symbol: str): """ Закрывает открытые позиции пользователя по символу рыночным ордером. Возвращает 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) - - include_fee = data_risk_stgs.get('commission_fee', 'Нет') == 'Да' - client = HTTP(api_key=api_key, api_secret=secret_key) - + client = await get_bybit_client(tg_id) positions_resp = client.get_positions(category="linear", symbol=symbol) if positions_resp.get('retCode') != 0: @@ -643,6 +688,7 @@ async def close_user_trade(tg_id: int, symbol: str, message): return False close_side = "Sell" if side == "Buy" else "Buy" + place_resp = client.place_order( category="linear", symbol=symbol, @@ -654,14 +700,11 @@ async def close_user_trade(tg_id: int, symbol: str, message): ) if place_resp.get('retCode') == 0: - await message.answer(f"Сделка {symbol} успешно закрыта.", reply_markup=inline_markup.back_to_main) return True else: - 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) - await message.answer("Произошла ошибка при закрытии сделки.", reply_markup=inline_markup.back_to_main) return False @@ -671,12 +714,15 @@ async def close_trade_after_delay(tg_id: int, message, symbol: str, delay_sec: i """ try: await asyncio.sleep(delay_sec) - result = await close_user_trade(tg_id, symbol, message) + result = await close_user_trade(tg_id, symbol) if result: await message.answer(f"Сделка {symbol} успешно закрыта по таймеру.", reply_markup=inline_markup.back_to_main) + logger.info(f"Сделка {symbol} успешно закрыта по таймеру.") else: await message.answer(f"Не удалось закрыть сделку {symbol} по таймеру.", reply_markup=inline_markup.back_to_main) + logger.error(f"Не удалось закрыть сделку {symbol} по таймеру.") except asyncio.CancelledError: await message.answer(f"Закрытие сделки {symbol} по таймеру отменено.", reply_markup=inline_markup.back_to_main) + logger.info(f"Закрытие сделки {symbol} по таймеру отменено.") diff --git a/app/services/Bybit/functions/bybit_ws.py b/app/services/Bybit/functions/bybit_ws.py index 4d0d31e..961faa8 100644 --- a/app/services/Bybit/functions/bybit_ws.py +++ b/app/services/Bybit/functions/bybit_ws.py @@ -9,6 +9,7 @@ logging.config.dictConfig(LOGGING_CONFIG) logger = logging.getLogger("bybit_ws") event_loop = None # Сюда нужно будет установить event loop из основного приложения +active_ws_tasks = {} def get_or_create_event_loop() -> asyncio.AbstractEventLoop: """ @@ -31,11 +32,14 @@ async def run_ws_for_user(tg_id, message) -> None: """ Запускает WebSocket Bybit для пользователя с указанным tg_id. """ - - api_key = await rq.get_bybit_api_key(tg_id) - api_secret = await rq.get_bybit_secret_key(tg_id) - - await start_execution_ws(api_key, api_secret, message) + if tg_id not in active_ws_tasks or active_ws_tasks[tg_id].done(): + api_key = await rq.get_bybit_api_key(tg_id) + api_secret = await rq.get_bybit_secret_key(tg_id) + # Запускаем WebSocket как асинхронную задачу + active_ws_tasks[tg_id] = asyncio.create_task( + start_execution_ws(api_key, api_secret, message) + ) + logger.info(f"WebSocket для пользователя {tg_id} запущен.") def on_order_callback(message, msg): diff --git a/app/services/Bybit/functions/functions.py b/app/services/Bybit/functions/functions.py index 54b407e..3187854 100644 --- a/app/services/Bybit/functions/functions.py +++ b/app/services/Bybit/functions/functions.py @@ -2,17 +2,17 @@ import logging.config from aiogram import F, Router +from app.telegram.functions.main_settings.settings import main_settings_message from logger_helper.logger_helper import LOGGING_CONFIG from app.services.Bybit.functions.Futures import (close_user_trade, set_take_profit_stop_loss, \ get_active_positions_by_symbol, get_active_orders_by_symbol, get_active_positions, get_active_orders, cancel_all_tp_sl_orders, - trading_cycle, open_position, close_trade_after_delay, + trading_cycle, open_position, close_trade_after_delay, safe_float, ) from app.services.Bybit.functions.balance import get_balance import app.telegram.Keyboards.inline_keyboards as inline_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 @@ -37,10 +37,10 @@ async def clb_start_bybit_trade_message(callback: CallbackQuery) -> None: """ user_id = callback.from_user.id balance = await get_balance(user_id, callback.message) - price = await get_price(user_id) if balance: symbol = await rq.get_symbol(user_id) + price = await get_price(user_id, symbol=symbol) text = ( f"💎 Торговля на Bybit\n\n" @@ -61,10 +61,10 @@ async def start_bybit_trade_message(message: Message) -> None: вместе с инструкциями по началу торговли. """ 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) + price = await get_price(message.from_user.id, symbol=symbol) text = ( f"💎 Торговля на Bybit\n\n" @@ -124,7 +124,6 @@ async def update_entry_type_message(callback: CallbackQuery, state: FSMContext) await callback.answer() - @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) -> None: """ @@ -192,43 +191,11 @@ async def start_trading_process(callback: CallbackQuery) -> None: message = callback.message data_main_stgs = await rq.get_user_main_settings(tg_id) - 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) margin_mode = data_main_stgs.get('margin_type', 'Isolated') trading_mode = data_main_stgs.get('trading_mode') switch_mode = data_main_stgs.get('switch_mode_enabled') - - if not api_key or not secret_key: - await message.answer("❗️ У вас не настроены API ключи для Bybit.") - await callback.answer() - return - - if trading_mode not in ['Long', 'Short', 'Smart']: - await message.answer(f"❗️ Некорректный торговый режим: {trading_mode}") - await callback.answer() - return - - if margin_mode not in ['Isolated', 'Cross']: - margin_mode = 'Isolated' - - client = HTTP(api_key=api_key, api_secret=secret_key) - - try: - positions_resp = client.get_positions(category='linear', symbol=symbol) - positions = positions_resp.get('result', {}).get('list', []) - except Exception as e: - logger.error(f"Ошибка при получении позиций: {e}") - positions = [] - - for pos in positions: - size = pos.get('size') - existing_margin_mode = pos.get('margin_mode') - if size and float(size) > 0 and existing_margin_mode and existing_margin_mode != margin_mode: - await callback.answer( - f"⚠️ Маржинальный режим нельзя менять при открытой позиции " - f"(текущий режим: {existing_margin_mode})", show_alert=True) - return + starting_quantity = safe_float(data_main_stgs.get('starting_quantity')) side = None if switch_mode == 'Включено': @@ -247,7 +214,6 @@ async def start_trading_process(callback: CallbackQuery) -> None: await message.answer("Начинаю торговлю с использованием текущих настроек...") - timer_data = await rq.get_user_timer(tg_id) if isinstance(timer_data, dict): timer_minute = timer_data.get('timer_minutes', 0) @@ -255,11 +221,12 @@ async def start_trading_process(callback: CallbackQuery) -> None: timer_minute = timer_data or 0 if timer_minute > 0: - await trading_cycle(tg_id, message) + await trading_cycle(tg_id, message, side=side, margin_mode=margin_mode, symbol=symbol, + starting_quantity=starting_quantity) await message.answer(f"Торговля начнётся через {timer_minute} мин.") await rq.update_user_timer(tg_id, minutes=0) else: - await open_position(tg_id, message, side, margin_mode) + await open_position(tg_id, message, side, margin_mode, symbol=symbol, quantity=starting_quantity) await callback.answer() @@ -290,6 +257,7 @@ async def show_my_trades_callback(callback: CallbackQuery): logger.error(f"Произошла ошибка при выборе сделки: {e}") await callback.message.answer("Произошла ошибка при выборе сделки", reply_markup=inline_markup.back_to_main) + @router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("show_deal_")) async def show_deal_callback(callback_query: CallbackQuery) -> None: """ @@ -303,7 +271,8 @@ async def show_deal_callback(callback_query: CallbackQuery) -> None: await get_active_positions_by_symbol(tg_id, symbol, message=callback_query.message) except Exception as e: logger.error(f"Произошла ошибка при выборе сделки: {e}") - await callback_query.message.answer("Произошла ошибка при выборе сделки", reply_markup=inline_markup.back_to_main) + await callback_query.message.answer("Произошла ошибка при выборе сделки", + reply_markup=inline_markup.back_to_main) @router_functions_bybit_trade.callback_query(F.data == "clb_open_orders") @@ -333,7 +302,8 @@ async def show_limit_callback(callback_query: CallbackQuery) -> None: await get_active_orders_by_symbol(tg_id, symbol, message=callback_query.message) except Exception as e: logger.error(f"Произошла ошибка при выборе сделки: {e}") - await callback_query.message.answer("Произошла ошибка при выборе сделки", reply_markup=inline_markup.back_to_main) + await callback_query.message.answer("Произошла ошибка при выборе сделки", + reply_markup=inline_markup.back_to_main) @router_functions_bybit_trade.callback_query(F.data == "clb_set_tp_sl") @@ -407,7 +377,7 @@ async def close_trade_callback(callback: CallbackQuery) -> None: symbol = callback.data.split(':')[1] tg_id = callback.from_user.id - result = await close_user_trade(tg_id, symbol, message=callback.message) + result = await close_user_trade(tg_id, symbol) if result: logger.info(f"Сделка {symbol} успешно закрыта.") @@ -480,6 +450,7 @@ async def reset_martingale(callback: CallbackQuery) -> None: tg_id = callback.from_user.id await rq.update_martingale_step(tg_id, 0) await callback.answer("Сброс шагов выполнен.") + await main_settings_message(tg_id, callback.message) @router_functions_bybit_trade.callback_query(F.data == "clb_stop_trading") @@ -492,6 +463,7 @@ async def confirm_stop_trading(callback: CallbackQuery): ) await callback.answer() + @router_functions_bybit_trade.callback_query(F.data == "stop_immediately") async def stop_immediately(callback: CallbackQuery): """ @@ -503,6 +475,7 @@ async def stop_immediately(callback: CallbackQuery): await callback.message.answer("Торговля остановлена.", reply_markup=inline_markup.back_to_main) await callback.answer() + @router_functions_bybit_trade.callback_query(F.data == "stop_with_timer") async def stop_with_timer_start(callback: CallbackQuery, state: FSMContext): """ @@ -510,9 +483,11 @@ async def stop_with_timer_start(callback: CallbackQuery, state: FSMContext): """ await state.set_state(CloseTradeTimerState.waiting_for_delay) - await callback.message.answer("Введите задержку в минутах перед остановкой торговли:", reply_markup=inline_markup.cancel) + await callback.message.answer("Введите задержку в минутах перед остановкой торговли:", + reply_markup=inline_markup.cancel) await callback.answer() + @router_functions_bybit_trade.message(CloseTradeTimerState.waiting_for_delay) async def process_stop_delay(message: Message, state: FSMContext): """ diff --git a/app/services/Bybit/functions/min_qty.py b/app/services/Bybit/functions/min_qty.py index 7cfb453..51faa53 100644 --- a/app/services/Bybit/functions/min_qty.py +++ b/app/services/Bybit/functions/min_qty.py @@ -28,7 +28,7 @@ async def get_min_qty(tg_id: int) -> float: client = HTTP(api_key=api_key, api_secret=secret_key) - price = await get_price(tg_id) + price = await get_price(tg_id, symbol=symbol) response = client.get_instruments_info(symbol=symbol, category='linear') diff --git a/app/services/Bybit/functions/price_symbol.py b/app/services/Bybit/functions/price_symbol.py index 3421228..d86737f 100644 --- a/app/services/Bybit/functions/price_symbol.py +++ b/app/services/Bybit/functions/price_symbol.py @@ -8,7 +8,7 @@ logging.config.dictConfig(LOGGING_CONFIG) logger = logging.getLogger("price_symbol") -async def get_price(tg_id: int) -> float: +async def get_price(tg_id: int, symbol: str) -> float: """ Асинхронно получает текущую цену символа пользователя на Bybit. @@ -17,7 +17,6 @@ async def get_price(tg_id: int) -> float: """ 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, diff --git a/app/states/States.py b/app/states/States.py index 86cc80f..d18e769 100644 --- a/app/states/States.py +++ b/app/states/States.py @@ -55,3 +55,14 @@ class condition_settings(StatesGroup): volume = State() integration = State() use_tv_signal = State() + + +class update_main_settings(StatesGroup): + """FSM состояние для обновления основных настройок.""" + trading_mode = State() + size_leverage = State() + margin_type = State() + martingale_factor = State() + starting_quantity = State() + maximal_quantity = State() + switch_mode_enabled = State() \ No newline at end of file diff --git a/app/telegram/Keyboards/inline_keyboards.py b/app/telegram/Keyboards/inline_keyboards.py index ae1cb2c..f484292 100644 --- a/app/telegram/Keyboards/inline_keyboards.py +++ b/app/telegram/Keyboards/inline_keyboards.py @@ -25,6 +25,7 @@ special_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ InlineKeyboardButton(text="Дополнительные параметры", callback_data='clb_change_additional_settings')], [InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api_message')], + back_btn_to_main ]) connect_bybit_api_markup = InlineKeyboardMarkup(inline_keyboard=[ @@ -93,7 +94,7 @@ risk_management_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ ]) condition_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text='Триггер', callback_data='clb_change_trigger'), + [InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_mode'), InlineKeyboardButton(text='Таймер', callback_data='clb_change_timer')], [InlineKeyboardButton(text='Фильтр волатильности', callback_data='clb_change_filter_volatility'), diff --git a/app/telegram/database/models.py b/app/telegram/database/models.py index fdb6f6e..a7e30f8 100644 --- a/app/telegram/database/models.py +++ b/app/telegram/database/models.py @@ -1,6 +1,6 @@ from datetime import datetime import logging.config -from sqlalchemy.sql.sqltypes import DateTime +from sqlalchemy.sql.sqltypes import DateTime, Numeric from sqlalchemy import BigInteger, Boolean, Integer, String, ForeignKey from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column @@ -120,17 +120,16 @@ class Margin_type(Base): class Trigger(Base): """ - Справочник видов триггеров для сделок. + Справочник триггеров для сделок. Атрибуты: - id (int): Первичный ключ. - trigger (str): Название триггера (например, 'Ручной', 'Автоматический'). + id (int): Первичный ключ.. """ __tablename__ = 'triggers' id: Mapped[int] = mapped_column(primary_key=True) - trigger = mapped_column(String(15), unique=True) + trigger_price = mapped_column(Integer(), default=0) class User_Main_Settings(Base): @@ -163,10 +162,10 @@ class User_Main_Settings(Base): size_leverage = mapped_column(Integer(), default=1) starting_quantity = mapped_column(Integer(), default=1) martingale_factor = mapped_column(Integer(), default=1) - martingale_step = mapped_column(Integer(), default=1) + martingale_step = mapped_column(Integer(), default=0) maximal_quantity = mapped_column(Integer(), default=10) entry_order_type = mapped_column(String(10), default='Market') - limit_order_price = mapped_column(String(20), nullable=True) + limit_order_price = mapped_column(Numeric(18, 15), nullable=True) class User_Risk_Management_Settings(Base): @@ -297,7 +296,7 @@ async def async_main(): await conn.run_sync(Base.metadata.create_all) # Заполнение таблиц - modes = ['Long', 'Short', 'Switch', 'Smart'] + modes = ['Long', 'Short', 'Smart'] for mode in modes: result = await conn.execute(select(Trading_Mode).where(Trading_Mode.mode == mode)) if not result.first(): @@ -310,10 +309,3 @@ async def async_main(): if not result.first(): logger.info("Заполение таблицы типов маржи") await conn.execute(Margin_type.__table__.insert().values(type=type)) - - triggers = ['Ручной', 'Автоматический'] - for trigger in triggers: - result = await conn.execute(select(Trigger).where(Trigger.trigger == trigger)) - if not result.first(): - logger.info("Заполение таблицы триггеров") - await conn.execute(Trigger.__table__.insert().values(trigger=trigger)) diff --git a/app/telegram/database/requests.py b/app/telegram/database/requests.py index d0b108a..ad7951d 100644 --- a/app/telegram/database/requests.py +++ b/app/telegram/database/requests.py @@ -1,4 +1,5 @@ import logging.config + from logger_helper.logger_helper import LOGGING_CONFIG from datetime import datetime, timedelta from typing import Any diff --git a/app/telegram/functions/additional_settings/settings.py b/app/telegram/functions/additional_settings/settings.py index 77b6c14..5090d0b 100644 --- a/app/telegram/functions/additional_settings/settings.py +++ b/app/telegram/functions/additional_settings/settings.py @@ -7,7 +7,7 @@ async def reg_new_user_default_additional_settings(id, message): await rq.set_new_user_default_additional_settings(tg_id) -async def main_settings_message(id, message, state): +async def main_settings_message(id, message): text = '''Дополнительные параметры - Сохранить как шаблон стратегии: да / нет diff --git a/app/telegram/functions/condition_settings/settings.py b/app/telegram/functions/condition_settings/settings.py index 32932eb..a33d8c9 100644 --- a/app/telegram/functions/condition_settings/settings.py +++ b/app/telegram/functions/condition_settings/settings.py @@ -28,7 +28,7 @@ async def main_settings_message(id, message): trigger = await rq.get_for_registration_trigger(tg_id) text = f""" Условия запуска -- Триггер: {trigger} +- Режим торговли: {trigger} - Таймер: установить таймер / остановить таймер - Фильтр волатильности / объёма: включить/отключить - Интеграции и внешние сигналы: @@ -53,7 +53,6 @@ async def trigger_message(id, message, state: FSMContext): async def trigger_manual_callback(callback: CallbackQuery, state: FSMContext): await state.set_state(condition_settings.trigger) await rq.update_trigger(tg_id=callback.from_user.id, trigger="Ручной") - await callback.message.answer("Триггер установлен в ручной режим.") await main_settings_message(callback.from_user.id, callback.message) await callback.answer() @@ -62,7 +61,6 @@ async def trigger_manual_callback(callback: CallbackQuery, state: FSMContext): async def trigger_manual_callback(callback: CallbackQuery, state: FSMContext): await state.set_state(condition_settings.trigger) await rq.update_trigger(tg_id=callback.from_user.id, trigger="Автоматический") - await callback.message.answer("Триггер установлен в автоматический режим.") await main_settings_message(callback.from_user.id, callback.message) await callback.answer() diff --git a/app/telegram/functions/functions.py b/app/telegram/functions/functions.py index 19ab77b..d1b7b9b 100644 --- a/app/telegram/functions/functions.py +++ b/app/telegram/functions/functions.py @@ -10,7 +10,7 @@ async def start_message(message): username = message.from_user.first_name else: username = f'{message.from_user.first_name} {message.from_user.last_name}' - await message.answer(f""" Привет {username}! 👋""", parse_mode='html') + await message.answer(f""" Привет {username}! 👋""", parse_mode='html', reply_markup=reply_markup.base_buttons_markup) await message.answer("Добро пожаловать в чат-робот для автоматизации трейдинга — вашего надежного помощника для анализа рынка и принятия взвешенных решений.", parse_mode='html', reply_markup=inline_markup.start_markup) diff --git a/app/telegram/functions/main_settings/settings.py b/app/telegram/functions/main_settings/settings.py index 90f1050..65a08e3 100644 --- a/app/telegram/functions/main_settings/settings.py +++ b/app/telegram/functions/main_settings/settings.py @@ -1,24 +1,18 @@ -from aiogram import Router, F - +from aiogram import Router +import logging.config 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.states.States import update_main_settings +from logger_helper.logger_helper import LOGGING_CONFIG -# FSM - Механизм состояния -from aiogram.fsm.state import State, StatesGroup +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("main_settings") router_main_settings = Router() -class update_main_settings(StatesGroup): - trading_mode = State() - size_leverage = State() - margin_type = State() - martingale_factor = State() - starting_quantity = State() - maximal_quantity = State() - switch_mode_enabled = State() async def reg_new_user_default_main_settings(id, message): tg_id = id @@ -27,12 +21,12 @@ async def reg_new_user_default_main_settings(id, message): margin_type = await rq.get_for_registration_margin_type() await rq.set_new_user_default_main_settings(tg_id, trading_mode, margin_type) - -async def main_settings_message(id, message, state): - data = await rq.get_user_main_settings(id) - await message.answer(f"""Основные настройки +async def main_settings_message(id, message): + data = await rq.get_user_main_settings(id) + + await message.answer(f"""Основные настройки - Режим торговли: {data['trading_mode']} - Режим свитч: {data['switch_mode_enabled']} @@ -45,6 +39,7 @@ async def main_settings_message(id, message, state): - Максимальное количество ставок в серии: {data['maximal_quantity']} """, parse_mode='html', reply_markup=inline_markup.main_settings_markup) + async def trading_mode_message(message, state): await state.set_state(update_main_settings.trading_mode) @@ -59,36 +54,37 @@ async def trading_mode_message(message, state): Выберите ниже для изменений: """, parse_mode='html', reply_markup=inline_markup.trading_mode_markup) + @router_main_settings.callback_query(update_main_settings.trading_mode) async def state_trading_mode(callback: CallbackQuery, state): - await callback.answer() + await callback.answer() - id = callback.from_user.id - data_settings = await rq.get_user_main_settings(id) + id = callback.from_user.id + data_settings = await rq.get_user_main_settings(id) - try: - match callback.data: - case 'trade_mode_long': + try: + match callback.data: + case 'trade_mode_long': await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Long") await rq.update_trade_mode_user(id, 'Long') - await main_settings_message(id, callback.message, state) + await main_settings_message(id, callback.message) await state.clear() - case 'trade_mode_short': + case 'trade_mode_short': await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Short") await rq.update_trade_mode_user(id, 'Short') - await main_settings_message(id, callback.message, state) + await main_settings_message(id, callback.message) await state.clear() - case 'trade_mode_smart': + case 'trade_mode_smart': await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Smart") await rq.update_trade_mode_user(id, 'Smart') - await main_settings_message(id, callback.message, state) + await main_settings_message(id, callback.message) - await state.clear() - except Exception as e: - print(f"error: {e}") + await state.clear() + except Exception as e: + logger.error(e) async def switch_mode_enabled_message(message, state): @@ -97,9 +93,8 @@ async def switch_mode_enabled_message(message, state): await message.edit_text( """Свитч — динамическое переключение между торговыми режимами для максимизации эффективности. - Выберите ниже для изменений:""", parse_mode='html', reply_markup=inline_markup.buttons_on_off_markup_for_switch) - - + Выберите ниже для изменений:""", parse_mode='html', + reply_markup=inline_markup.buttons_on_off_markup_for_switch) @router_main_settings.callback_query(lambda c: c.data in ["clb_on_switch", "clb_off_switch"]) @@ -109,12 +104,12 @@ async def state_switch_mode_enabled(callback: CallbackQuery, state): val = "Включить" if callback.data == "clb_on_switch" else "Выключить" if val == "Включить": await rq.update_switch_mode_enabled(tg_id, "Включено") - await callback.message.answer(f"Включено") - await main_settings_message(tg_id, callback.message, state) + await callback.answer(f"Включено") + await main_settings_message(tg_id, callback.message) else: await rq.update_switch_mode_enabled(tg_id, "Выключено") - await callback.message.answer(f"Выключено") - await main_settings_message(tg_id, callback.message, state) + await callback.answer(f"Выключено") + await main_settings_message(tg_id, callback.message) await state.clear() @@ -132,24 +127,24 @@ async def state_switch_mode_enabled(callback: CallbackQuery, state): if val == "Long": await rq.update_switch_state(tg_id, "Long") await callback.message.answer(f"Состояние свитча: {val}") - await main_settings_message(tg_id, callback.message, state) + await main_settings_message(tg_id, callback.message) else: await rq.update_switch_state(tg_id, "Short") await callback.message.answer(f"Состояние свитча: {val}") - await main_settings_message(tg_id, callback.message, state) + await main_settings_message(tg_id, callback.message) await state.clear() - -async def size_leverage_message (message, state): +async def size_leverage_message(message, state): await state.set_state(update_main_settings.size_leverage) - await message.edit_text("Введите размер кредитного плеча (от 1 до 100): ", parse_mode='html', reply_markup=inline_markup.back_btn_list_settings_markup) + await message.edit_text("Введите размер кредитного плеча (от 1 до 100): ", parse_mode='html', + reply_markup=inline_markup.back_btn_list_settings_markup) @router_main_settings.message(update_main_settings.size_leverage) async def state_size_leverage(message: Message, state): - await state.update_data(size_leverage = message.text) + await state.update_data(size_leverage=message.text) data = await state.get_data() data_settings = await rq.get_user_main_settings(message.from_user.id) @@ -158,22 +153,26 @@ async def state_size_leverage(message: Message, state): await message.answer(f"✅ Изменено: {data_settings['size_leverage']} → {data['size_leverage']}") await rq.update_size_leverange(message.from_user.id, data['size_leverage']) - await main_settings_message(message.from_user.id, message, state) + await main_settings_message(message.from_user.id, message) await state.clear() else: - await message.answer(f'⛔️ Ошибка: ваше значение ({data['size_leverage']}) или выше лимита (100) или вы вводите неверные символы') + await message.answer( + f'⛔️ Ошибка: ваше значение ({data['size_leverage']}) или выше лимита (100) или вы вводите неверные символы') + + await main_settings_message(message.from_user.id, message) - await main_settings_message(message.from_user.id, message, state) async def martingale_factor_message(message, state): await state.set_state(update_main_settings.martingale_factor) - await message.edit_text("Введите коэффициент Мартингейла:", parse_mode='html', reply_markup=inline_markup.back_btn_list_settings_markup) - + await message.edit_text("Введите коэффициент Мартингейла:", parse_mode='html', + reply_markup=inline_markup.back_btn_list_settings_markup) + + @router_main_settings.message(update_main_settings.martingale_factor) async def state_martingale_factor(message: Message, state): - await state.update_data(martingale_factor = message.text) + await state.update_data(martingale_factor=message.text) data = await state.get_data() data_settings = await rq.get_user_main_settings(message.from_user.id) @@ -182,14 +181,16 @@ async def state_martingale_factor(message: Message, state): await message.answer(f"✅ Изменено: {data_settings['martingale_factor']} → {data['martingale_factor']}") await rq.update_martingale_factor(message.from_user.id, data['martingale_factor']) - await main_settings_message(message.from_user.id, message, state) + await main_settings_message(message.from_user.id, message) await state.clear() else: - await message.answer(f'⛔️ Ошибка: ваше значение ({data['martingale_factor']}) или выше лимита (100) или вы вводите неверные символы') + await message.answer( + f'⛔️ Ошибка: ваше значение ({data['martingale_factor']}) или выше лимита (100) или вы вводите неверные символы') + + await main_settings_message(message.from_user.id, message) + - await main_settings_message(message.from_user.id, message, state) - async def margin_type_message(message, state): await state.set_state(update_main_settings.margin_type) @@ -209,40 +210,60 @@ async def margin_type_message(message, state): Выберите ниже для изменений: """, parse_mode='html', reply_markup=inline_markup.margin_type_markup) + @router_main_settings.callback_query(update_main_settings.margin_type) async def state_margin_type(callback: CallbackQuery, state): - await callback.answer() + 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) + data_settings = await rq.get_user_main_settings(tg_id) + client = HTTP(api_key=api_key, api_secret=secret_key) + try: + active_positions = client.get_positions(category='linear', settleCoin='USDT') - id = callback.from_user.id - data_settings = await rq.get_user_main_settings(id) + positions = active_positions.get('result', {}).get('list', []) + except Exception as e: + logger.error(f"error: {e}") + positions = [] - try: - match callback.data: - case 'margin_type_isolated': + for pos in positions: + size = pos.get('size') + if float(size) > 0: + await callback.answer( + "⚠️ Маржинальный режим нельзя менять при открытой позиции", + show_alert=True + ) + return + try: + match callback.data: + case 'margin_type_isolated': await callback.message.answer(f"✅ Изменено: {data_settings['margin_type']} → Isolated") - await rq.update_margin_type(id, 'Isolated') - await main_settings_message(id, callback.message, state) + await rq.update_margin_type(tg_id, 'Isolated') + await main_settings_message(tg_id, callback.message) await state.clear() - case 'margin_type_cross': + case 'margin_type_cross': await callback.message.answer(f"✅ Изменено: {data_settings['margin_type']} → Cross") - await rq.update_margin_type(id, 'Cross') - await main_settings_message(id, callback.message, state) + await rq.update_margin_type(tg_id, 'Cross') + await main_settings_message(tg_id, callback.message) await state.clear() - except Exception as e: - print(f"error: {e}") + except Exception as e: + logger.error(f"error: {e}") -async def starting_quantity_message (message, state): + +async def starting_quantity_message(message, state): await state.set_state(update_main_settings.starting_quantity) - await message.edit_text("Введите начальную ставку:", parse_mode='html', reply_markup=inline_markup.back_btn_list_settings_markup) + await message.edit_text("Введите начальную ставку:", parse_mode='html', + reply_markup=inline_markup.back_btn_list_settings_markup) + @router_main_settings.message(update_main_settings.starting_quantity) async def state_starting_quantity(message: Message, state): - await state.update_data(starting_quantity = message.text) + await state.update_data(starting_quantity=message.text) data = await state.get_data() data_settings = await rq.get_user_main_settings(message.from_user.id) @@ -251,22 +272,25 @@ async def state_starting_quantity(message: Message, state): await message.answer(f"✅ Изменено: {data_settings['starting_quantity']} → {data['starting_quantity']}") await rq.update_starting_quantity(message.from_user.id, data['starting_quantity']) - await main_settings_message(message.from_user.id, message, state) + await main_settings_message(message.from_user.id, message) await state.clear() else: await message.answer(f'⛔️ Ошибка: вы вводите неверные символы') - await main_settings_message(message.from_user.id, message, state) + await main_settings_message(message.from_user.id, message) + async def maximum_quantity_message(message, state): await state.set_state(update_main_settings.maximal_quantity) - await message.edit_text("Введите максимальное количество серии ставок:", parse_mode='html', reply_markup=inline_markup.back_btn_list_settings_markup) + await message.edit_text("Введите максимальное количество серии ставок:", parse_mode='html', + reply_markup=inline_markup.back_btn_list_settings_markup) + @router_main_settings.message(update_main_settings.maximal_quantity) async def state_maximal_quantity(message: Message, state): - await state.update_data(maximal_quantity = message.text) + await state.update_data(maximal_quantity=message.text) data = await state.get_data() data_settings = await rq.get_user_main_settings(message.from_user.id) @@ -275,10 +299,11 @@ async def state_maximal_quantity(message: Message, state): await message.answer(f"✅ Изменено: {data_settings['maximal_quantity']} → {data['maximal_quantity']}") await rq.update_maximal_quantity(message.from_user.id, data['maximal_quantity']) - await main_settings_message(message.from_user.id, message, state) + await main_settings_message(message.from_user.id, message) await state.clear() else: - await message.answer(f'⛔️ Ошибка: ваше значение ({data['maximal_quantity']}) или выше лимита (100) или вы вводите неверные символы') - - await main_settings_message(message.from_user.id, message, state) \ No newline at end of file + await message.answer( + f'⛔️ Ошибка: ваше значение ({data['maximal_quantity']}) или выше лимита (100) или вы вводите неверные символы') + + await main_settings_message(message.from_user.id, message) diff --git a/app/telegram/functions/risk_management_settings/settings.py b/app/telegram/functions/risk_management_settings/settings.py index f600403..5d0b32d 100644 --- a/app/telegram/functions/risk_management_settings/settings.py +++ b/app/telegram/functions/risk_management_settings/settings.py @@ -1,11 +1,16 @@ from aiogram import Router import app.telegram.Keyboards.inline_keyboards as inline_markup - +import logging.config import app.telegram.database.requests as rq from aiogram.types import Message, CallbackQuery from app.states.States import update_risk_management_settings +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("risk_management_settings") + router_risk_management_settings = Router() @@ -80,7 +85,8 @@ async def state_price_loss(message: Message, state): # Пробуем перевести price_profit в число, если это возможно try: current_price_profit_num = int(current_price_profit) - except Exception: + except Exception as e: + logger.error(e) current_price_profit_num = 0 # Флаг, если price_profit изначально равен 0 или совпадает со старым стоп-лоссом diff --git a/app/telegram/handlers/handlers.py b/app/telegram/handlers/handlers.py index 3152ae5..aad0658 100644 --- a/app/telegram/handlers/handlers.py +++ b/app/telegram/handlers/handlers.py @@ -1,5 +1,5 @@ import logging.config -import asyncio + from aiogram import F, Router from aiogram.filters import CommandStart from aiogram.types import Message, CallbackQuery @@ -50,8 +50,7 @@ async def profile_message(message: Message) -> None: tg_id = message.from_user.id balance = await get_balance(message.from_user.id, message) if user and balance: - asyncio.create_task(run_ws_for_user(tg_id, message)) - logger.info(f"Получение event loop") + await run_ws_for_user(tg_id, message) await func.profile_message(message.from_user.username, message) @@ -112,15 +111,14 @@ async def clb_back_to_settings_msg(callback: CallbackQuery) -> None: @router.callback_query(F.data == "clb_change_main_settings") -async def clb_change_main_settings_message(callback: CallbackQuery, state: FSMContext) -> None: +async def clb_change_main_settings_message(callback: CallbackQuery) -> None: """ Открыть меню изменения главных настроек. Args: callback (CallbackQuery): полученный колбэк. - state (FSMContext): текущее состояние FSM. """ - await func_main_settings.main_settings_message(callback.from_user.id, callback.message, state) + await func_main_settings.main_settings_message(callback.from_user.id, callback.message) await callback.answer() @@ -139,13 +137,12 @@ async def clb_change_risk_management_message(callback: CallbackQuery) -> None: @router.callback_query(F.data == "clb_change_condition_settings") -async def clb_change_condition_message(callback: CallbackQuery, state: FSMContext) -> None: +async def clb_change_condition_message(callback: CallbackQuery) -> None: """ Открыть меню изменения настроек условий. Args: callback (CallbackQuery): полученный колбэк. - state (FSMContext): текущее состояние FSM. """ await func_condition_settings.main_settings_message(callback.from_user.id, callback.message) @@ -153,15 +150,14 @@ async def clb_change_condition_message(callback: CallbackQuery, state: FSMContex @router.callback_query(F.data == "clb_change_additional_settings") -async def clb_change_additional_message(callback: CallbackQuery, state: FSMContext) -> None: +async def clb_change_additional_message(callback: CallbackQuery) -> None: """ Открыть меню изменения дополнительных настроек. Args: callback (CallbackQuery): полученный колбэк. - state (FSMContext): текущее состояние FSM. """ - await func_additional_settings.main_settings_message(callback.from_user.id, callback.message, state) + await func_additional_settings.main_settings_message(callback.from_user.id, callback.message) await callback.answer() @@ -240,7 +236,7 @@ async def clb_risk_management_settings_msg(callback: CallbackQuery, state: FSMCo logger.error(f"Error callback in risk_management match-case: {e}") -list_condition_settings = ['clb_change_trigger', +list_condition_settings = ['clb_change_mode', 'clb_change_timer', 'clb_change_filter_volatility', 'clb_change_external_cues', @@ -263,7 +259,7 @@ async def clb_condition_settings_msg(callback: CallbackQuery, state: FSMContext) try: match callback.data: - case 'clb_change_trigger': + case 'clb_change_mode': await func_condition_settings.trigger_message(callback.from_user.id, callback.message, state) case 'clb_change_timer': await func_condition_settings.timer_message(callback.from_user.id, callback.message, state) diff --git a/logger_helper/logger_helper.py b/logger_helper/logger_helper.py index 374f49e..f28566e 100644 --- a/logger_helper/logger_helper.py +++ b/logger_helper/logger_helper.py @@ -85,6 +85,16 @@ LOGGING_CONFIG = { "level": "DEBUG", "propagate": False, }, + "main_settings": { + "handlers": ["console", "timed_rotating_file"], + "level": "DEBUG", + "propagate": False, + }, + "risk_management_settings": { + "handlers": ["console", "timed_rotating_file"], + "level": "DEBUG", + "propagate": False, + }, "models": { "handlers": ["console", "timed_rotating_file"], "level": "DEBUG", -- 2.50.1 From 3bd6b7363c0429dfa49f8433bf19b7925d6ecfa7 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Sun, 31 Aug 2025 11:47:13 +0500 Subject: [PATCH 75/77] Fixed --- app/services/Bybit/functions/functions.py | 11 +++++------ app/states/States.py | 1 + 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/services/Bybit/functions/functions.py b/app/services/Bybit/functions/functions.py index 3187854..57b6f65 100644 --- a/app/services/Bybit/functions/functions.py +++ b/app/services/Bybit/functions/functions.py @@ -472,7 +472,7 @@ async def stop_immediately(callback: CallbackQuery): tg_id = callback.from_user.id await rq.update_trigger(tg_id, "Ручной") - 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() @@ -482,13 +482,13 @@ async def stop_with_timer_start(callback: CallbackQuery, state: FSMContext): Запускает диалог с пользователем для задания задержки перед остановкой торговли. """ - await state.set_state(CloseTradeTimerState.waiting_for_delay) + await state.set_state(CloseTradeTimerState.waiting_for_trade) await callback.message.answer("Введите задержку в минутах перед остановкой торговли:", reply_markup=inline_markup.cancel) await callback.answer() -@router_functions_bybit_trade.message(CloseTradeTimerState.waiting_for_delay) +@router_functions_bybit_trade.message(CloseTradeTimerState.waiting_for_trade) async def process_stop_delay(message: Message, state: FSMContext): """ Обрабатывает ввод задержки и запускает задачу остановки торговли с задержкой. @@ -505,11 +505,10 @@ async def process_stop_delay(message: Message, state: FSMContext): tg_id = message.from_user.id delay_seconds = delay_minutes * 60 - await message.answer(f"Торговля будет остановлена через {delay_minutes} минут.", - reply_markup=inline_markup.back_to_main) + await message.answer(f"Торговля будет остановлена через {delay_minutes} минут.") await asyncio.sleep(delay_seconds) await rq.update_trigger(tg_id, "Ручной") - await message.answer("Торговля по таймеру остановлена.") + await message.answer("Автоматическая торговля остановлена.", reply_markup=inline_markup.back_to_main) await state.clear() diff --git a/app/states/States.py b/app/states/States.py index d18e769..6b9c7d1 100644 --- a/app/states/States.py +++ b/app/states/States.py @@ -25,6 +25,7 @@ class state_limit_price(StatesGroup): class CloseTradeTimerState(StatesGroup): """FSM состояние ожидания задержки перед закрытием сделки.""" waiting_for_delay = State() + waiting_for_trade = State() class SetTP_SL_State(StatesGroup): -- 2.50.1 From dd53e5a14a92338d776f7dbd5740c66b974bbf54 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Wed, 3 Sep 2025 21:28:12 +0500 Subject: [PATCH 76/77] Fixed --- app/services/Bybit/functions/balance.py | 2 +- app/telegram/Keyboards/inline_keyboards.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/services/Bybit/functions/balance.py b/app/services/Bybit/functions/balance.py index 3168bc0..7907d7f 100644 --- a/app/services/Bybit/functions/balance.py +++ b/app/services/Bybit/functions/balance.py @@ -34,7 +34,7 @@ async def get_balance(tg_id: int, message) -> float: if api_key == 'None' or secret_key == 'None': await message.answer('⚠️ Подключите платформу для торговли', - reply_markup=inline_markup.connect_bybit_api_markup) + reply_markup=inline_markup.connect_bybit_api_message) return 0 try: diff --git a/app/telegram/Keyboards/inline_keyboards.py b/app/telegram/Keyboards/inline_keyboards.py index f484292..0040b50 100644 --- a/app/telegram/Keyboards/inline_keyboards.py +++ b/app/telegram/Keyboards/inline_keyboards.py @@ -17,17 +17,22 @@ back_btn_to_main = [InlineKeyboardButton(text="На главную", callback_da back_btn_profile = [InlineKeyboardButton(text="Назад", callback_data='clb_start_chatbot_message')] +connect_bybit_api_message = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api_message')] +]) + special_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="Основные настройки", callback_data='clb_change_main_settings'), InlineKeyboardButton(text="Риск-менеджмент", callback_data='clb_change_risk_management_settings')], [InlineKeyboardButton(text="Условия запуска", callback_data='clb_change_condition_settings'), InlineKeyboardButton(text="Дополнительные параметры", callback_data='clb_change_additional_settings')], - [InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api_message')], back_btn_to_main ]) + + connect_bybit_api_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api')] ]) -- 2.50.1 From 058ba09c03de2736a1600a0a525355b094c98f0c Mon Sep 17 00:00:00 2001 From: algizn97 Date: Wed, 3 Sep 2025 21:33:08 +0500 Subject: [PATCH 77/77] Fixed --- app/telegram/functions/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/telegram/functions/functions.py b/app/telegram/functions/functions.py index d1b7b9b..19ab77b 100644 --- a/app/telegram/functions/functions.py +++ b/app/telegram/functions/functions.py @@ -10,7 +10,7 @@ async def start_message(message): username = message.from_user.first_name else: username = f'{message.from_user.first_name} {message.from_user.last_name}' - await message.answer(f""" Привет {username}! 👋""", parse_mode='html', reply_markup=reply_markup.base_buttons_markup) + await message.answer(f""" Привет {username}! 👋""", parse_mode='html') await message.answer("Добро пожаловать в чат-робот для автоматизации трейдинга — вашего надежного помощника для анализа рынка и принятия взвешенных решений.", parse_mode='html', reply_markup=inline_markup.start_markup) -- 2.50.1