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..52973aa 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,5 @@ __pycache__/ env/ venv/ .venv/ - -requirements.txt - +.idea +/.idea 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/BibytBot_API.py b/BybitBot_API.py similarity index 52% rename from BibytBot_API.py rename to BybitBot_API.py index e8a3b81..1eff3a4 100644 --- a/BibytBot_API.py +++ b/BybitBot_API.py @@ -1,37 +1,48 @@ import asyncio - +import logging.config from aiogram import Bot, Dispatcher -from aiogram.filters import Command, CommandStart -from aiogram.types import Message +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 +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 logger_helper.logger_helper import LOGGING_CONFIG +from config import TOKEN_TG_BOT_1 -from app.telegram.logs import logger +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("main") -bot = Bot(token=TOKEN_TG_BOT) +bot = Bot(token=TOKEN_TG_BOT_1) dp = Dispatcher() -async def main(): + +async def main() -> None: + """ + Основная асинхронная функция запуска бота: + """ + loop = get_or_create_event_loop() + set_event_loop(loop) + await async_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) await dp.start_polling(bot) + 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/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 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 diff --git a/app/services/Bybit/functions/Add_Bybit_API.py b/app/services/Bybit/functions/Add_Bybit_API.py index 291da46..2ee471e 100644 --- a/app/services/Bybit/functions/Add_Bybit_API.py +++ b/app/services/Bybit/functions/Add_Bybit_API.py @@ -1,22 +1,27 @@ 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 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) +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/). @@ -38,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('Данные добавлены, нажмите на профиль и начните торговлю!') - - \ No newline at end of file + await message.answer('Данные добавлены, нажмите на профиль и начните торговлю!', + reply_markup=reply_markup.base_buttons_markup) diff --git a/app/services/Bybit/functions/Futures.py b/app/services/Bybit/functions/Futures.py index 1f68dc0..f45d08b 100644 --- a/app/services/Bybit/functions/Futures.py +++ b/app/services/Bybit/functions/Futures.py @@ -1,254 +1,728 @@ -import time +import asyncio +import json +import time -from typing import Optional -from asyncio import Handle - -from annotated_types import T +import logging.config from pybit import exceptions from pybit.unified_trading import HTTP -from pybit.unified_trading import WebSocket - -from app.services.Bybit.functions import price_symbol +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 -import logging -logging.basicConfig(level=logging.DEBUG) +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("futures") -def handle_message(message): - print(message) +processed_trade_ids = set() -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' - text = f'''Позиция была успешна открыта! - Торговая пара: {symbol} - Движение: {trade_mode} - Тип-маржи: {margin_mode} - Кредитное плечо: {leverage} - Количество: {qty} - ''' +async def get_bybit_client(tg_id): + """ + Асинхронно получает экземпляр клиента Bybit. - await message.answer(text=text, parse_mode='html') - -async def error_max_step(message): - await message.answer('Сделка не была совершена, превышен лимит максимального количества ставок') - -async def error_max_risk(message): - await message.answer('Сделка не была совершена, слишком высокий риск') - -async def contract_long(tg_id, message, margin_mode): + :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) - SYMBOL = await rq.get_symbol(tg_id) + return HTTP(api_key=api_key, api_secret=secret_key) + +def safe_float(val) -> float: + """ + Безопасное преобразование значения в float. + Возвращает 0.0, если значение None, пустое или некорректное. + """ + try: + if val is None or val == '': + return 0.0 + return float(val) + except (ValueError, TypeError): + logger.error("Некорректное значение для преобразования в float") + return 0.0 + + +def format_trade_details_position(data, commission_fee): + """ + Форматирует информацию о сделке в виде строки. + """ + msg = data.get('data', [{}])[0] + + closed_size = safe_float(msg.get('closedSize', 0)) + symbol = msg.get('symbol', 'N/A') + 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 = safe_float(msg.get('execFee', 0)) + pnl = safe_float(msg.get('execPnl', 0)) + + if commission_fee == "Да": + pnl -= commission + + movement = '' + if side.lower() == 'buy': + movement = 'Покупка' + elif side.lower() == 'sell': + movement = 'Продажа' + else: + movement = side + + if closed_size > 0: + return ( + f"Сделка закрыта:\n" + f"Торговая пара: {symbol}\n" + f"Цена исполнения: {entry_price:.6f}\n" + f"Количество: {qty}\n" + f"Закрыто позиций: {closed_size}\n" + f"Тип ордера: {order_type}\n" + f"Движение: {movement}\n" + f"Комиссия за сделку: {commission:.6f}\n" + f"Реализованная прибыль: {pnl:.6f} USDT" + ) + if order_type == 'Market': + return ( + f"Сделка открыта:\n" + f"Торговая пара: {symbol}\n" + f"Цена исполнения: {entry_price:.6f}\n" + f"Количество: {qty}\n" + f"Тип ордера: {order_type}\n" + 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: + """ + Извлекает реализованную прибыль/убыток из сообщения. + """ + try: + 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): + """ + Обработчик сообщений об исполнении сделки. + Логирует событие и проверяет условия для мартингейла и TP. + """ + # logger.info(f"Исполнена сделка:\n{json.dumps(msg, indent=4, ensure_ascii=False)}") + tg_id = message.from_user.id + data = msg.get('data', [{}])[0] + data_main_risk_stgs = await rq.get_user_risk_management_settings(tg_id) + commission_fee = data_main_risk_stgs.get('commission_fee', "ДА") + pnl = parse_pnl_from_msg(msg) data_main_stgs = await rq.get_user_main_settings(tg_id) - data_risk_management_stgs = await rq.get_user_risk_management_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')) - match margin_mode: - case 'Isolated': - margin_mode = 'ISOLATED_MARGIN' - case 'Cross': - margin_mode = 'REGULAR_MARGIN' + trade_info = format_trade_details_position(data=msg, commission_fee=commission_fee) - client = HTTP( - api_key=api_key, - api_secret=secret_key - ) + if trade_info: + await message.answer(f"{trade_info}", reply_markup=inline_markup.back_to_main) - try: - balance = 0 - price = 0 - - balance = await balance_g.get_balance(tg_id) - price = await price_symbol.get_price(tg_id, message) + 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' - client.set_margin_mode( - setMarginMode=margin_mode # margin_type + 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 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) + + +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: + """ + Сообщение об ошибке превышения максимального количества шагов мартингейла. + """ + logger.error('Сделка не была совершена, превышен лимит максимального количества ставок в серии.') + await message.answer('Сделка не была совершена, превышен лимит максимального количества ставок в серии.', + reply_markup=inline_markup.back_to_main) + + +async def error_max_risk(message) -> None: + """ + Сообщение об ошибке превышения риск-лимита сделки. + """ + logger.error('Сделка не была совершена, риск убытка превышает допустимый лимит.') + await message.answer('Сделка не была совершена, риск убытка превышает допустимый лимит.', + reply_markup=inline_markup.back_to_main) + + +async def open_position(tg_id, message, side: str, margin_mode: str, symbol, quantity, tpsl_mode='Full'): + """ + Открывает позицию на Bybit с учётом настроек пользователя, маржи, размера лота, платформы и риска. + + Возвращает True при успехе, False при ошибках открытия ордера, None при исключениях. + """ + try: + 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) + + balance = await balance_g.get_balance(tg_id, message) + price = await price_symbol.get_price(tg_id, symbol=symbol) + entry_price = safe_float(price) + + max_martingale_steps = int(data_main_stgs.get('maximal_quantity', 0)) + 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')) + + if order_type == 'Limit' and limit_price: + price_for_calc = limit_price + else: + price_for_calc = entry_price + + 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 + + client.set_margin_mode(setMarginMode=bybit_margin_mode) + + 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 + + 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_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 + + 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}", exc_info=True) + await message.answer('Недостаточно средств для размещения нового ордера с заданным количеством и плечом.', + reply_markup=inline_markup.back_to_main) + except Exception as 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, side, margin_mode, symbol, starting_quantity): + """ + Цикл торговой логики с учётом таймера пользователя. + """ + try: + timer_data = await rq.get_user_timer(tg_id) + timer_min = 0 + if isinstance(timer_data, dict): + timer_min = timer_data.get('timer_minutes') or timer_data.get('timer') or 0 + else: + timer_min = timer_data or 0 + + timer_sec = timer_min * 60 + + if timer_sec > 0: + await asyncio.sleep(timer_sec) + + 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} был отменён.", exc_info=True) + + +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 для открытой позиции. + """ + symbol = await rq.get_symbol(tg_id) + data_main_stgs = await rq.get_user_main_settings(tg_id) + trading_mode = data_main_stgs.get('trading_mode') + + side = None + if trading_mode == 'Long': + side = 'Buy' + elif trading_mode == 'Short': + side = 'Sell' + + if side is None: + await message.answer("Не удалось определить сторону сделки.") + return + + client = await get_bybit_client(tg_id) + await cancel_all_tp_sl_orders(tg_id, symbol) + + try: + try: + client.set_tp_sl_mode(symbol=symbol, category='linear', tpSlMode=tpsl_mode) + except exceptions.InvalidRequestError as e: + if 'same tp sl mode' in str(e).lower(): + logger.info(f"Режим TP/SL уже установлен для {symbol}") + else: + raise + + 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 ) - 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']) - - # Инициализация переменных - next_quantity = starting_quantity - last_quantity = starting_quantity - realised_pnl = 0.0 + if resp.get('retCode') != 0: + await message.answer(f"Ошибка обновления TP/SL: {resp.get('retMsg')}", + reply_markup=inline_markup.back_to_main) + return - 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: - await message.answer('Недостаточно баланса') + 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: - await message.answer('Непредвиденная оишбка') + logger.error(f"Ошибка установки TP/SL для {symbol}: {e}", exc_info=True) + await message.answer("Произошла ошибка при установке TP и SL.", reply_markup=inline_markup.back_to_main) -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) +async def cancel_all_tp_sl_orders(tg_id, symbol): + """ + Отменяет лимитные ордера для указанного символа. + """ + client = await get_bybit_client(tg_id) + last_response = None + try: + orders_resp = client.get_open_orders(category='linear', symbol=symbol) + orders = orders_resp.get('result', {}).get('list', []) - match margin_mode: - case 'Isolated': - margin_mode = 'ISOLATED_MARGIN' - case 'Cross': - margin_mode = 'REGULAR_MARGIN' + 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) + 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"Ошибка при отмене ордера: {e}") - client = HTTP( - api_key=api_key, - api_secret=secret_key + return last_response + + +async def get_active_positions(tg_id, message): + """ + Показывает активные позиции пользователя. + """ + 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] + + 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): + """ + Показывает активные позиции пользователя по символу. + """ + 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 + + 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" ) - try: - balance = 0 - price = 0 - - balance = await balance_g.get_balance(tg_id) - price = await price_symbol.get_price(tg_id, message) + await message.answer(text, reply_markup=inline_markup.create_close_deal_markup(symbol)) - client.set_margin_mode( - setMarginMode=margin_mode # margin_type + +async def get_active_orders(tg_id, message): + """ + Показывает активные лимитные ордера пользователя. + """ + 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'] + + 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): + """ + Показывает активные лимитные ордера пользователя по символу. + """ + 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', []) + 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" + ) + 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): + """ + Закрывает открытые позиции пользователя по символу рыночным ордером. + Возвращает True при успехе, False при ошибках. + """ + try: + client = await get_bybit_client(tg_id) + 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') + if qty == 0: + return False + + close_side = "Sell" if side == "Buy" else "Buy" + + place_resp = client.place_order( + category="linear", + symbol=symbol, + side=close_side, + orderType="Market", + qty=str(qty), + timeInForce="GTC", + reduceOnly=True ) - 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']) - - # Инициализация переменных - 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) + if place_resp.get('retCode') == 0: + return True 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: - await message.answer('Недостаточно баланса') + return False except Exception as e: - await message.answer('Непредвиденная оишбка') \ No newline at end of file + logger.error(f"Ошибка закрытия сделки {symbol} для пользователя {tg_id}: {e}", exc_info=True) + return False + + +async def close_trade_after_delay(tg_id: int, message, symbol: str, delay_sec: int): + """ + Закрывает сделку пользователя после задержки delay_sec секунд. + """ + try: + await asyncio.sleep(delay_sec) + result = await close_user_trade(tg_id, symbol) + 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/balance.py b/app/services/Bybit/functions/balance.py index db3d74d..7907d7f 100644 --- a/app/services/Bybit/functions/balance.py +++ b/app/services/Bybit/functions/balance.py @@ -1,10 +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 -client = HTTP() +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) @@ -14,20 +33,20 @@ async def get_balance(tg_id, message): ) if api_key == 'None' or secret_key == 'None': - await message.answer('⚠️ Подключите платформу для торговли') - return 0 + await message.answer('⚠️ Подключите платформу для торговли', + reply_markup=inline_markup.connect_bybit_api_message) + 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: + logger.error(f"Ошибка API: {response.get('retMsg')}") + await message.answer(f"⚠️ Ошибка API: {response.get('retMsg')}") + return 0 except Exception as e: - await message.answer('⚠️ Неверные данные API, перепроверьте их') - return 0 \ No newline at end of file + logger.error(f"Ошибка при получении общего баланса: {e}") + await message.answer('⚠️ Ошибка при получении баланса') + return 0 diff --git a/app/services/Bybit/functions/bybit_ws.py b/app/services/Bybit/functions/bybit_ws.py new file mode 100644 index 0000000..961faa8 --- /dev/null +++ b/app/services/Bybit/functions/bybit_ws.py @@ -0,0 +1,84 @@ +import asyncio +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 из основного приложения +active_ws_tasks = {} + +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 + + +async def run_ws_for_user(tg_id, message) -> None: + """ + Запускает WebSocket Bybit для пользователя с указанным tg_id. + """ + 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): + if event_loop is not None: + 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 пропущен.") + + +async def start_execution_ws(api_key: str, api_secret: str, message): + """ + Запускает и поддерживает WebSocket подключение для исполнения сделок. + Реконнект при потерях соединения. + """ + 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.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: + logger.warning("WebSocket закрыт, переподключение через 5 секунд...") + await asyncio.sleep(reconnect_delay) + except Exception as e: + logger.error(f"Ошибка WebSocket: {e}") + await asyncio.sleep(reconnect_delay) 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/services/Bybit/functions/functions.py b/app/services/Bybit/functions/functions.py index 86ff692..57b6f65 100644 --- a/app/services/Bybit/functions/functions.py +++ b/app/services/Bybit/functions/functions.py @@ -1,99 +1,523 @@ -from aiogram import F, Router +import asyncio +import logging.config +from aiogram import F, Router -from app.services.Bybit.functions import Futures, func_min_qty +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, safe_float, + ) from app.services.Bybit.functions.balance import get_balance import app.telegram.Keyboards.inline_keyboards as inline_markup 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 + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("functions") + router_functions_bybit_trade = Router() -class state_update_symbol(StatesGroup): - symbol = State() - -@router_functions_bybit_trade.callback_query(F.data == 'clb_start_trading') -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) + +@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) -> None: + """ + Обработка нажатия кнопок запуска торговли или возврата в главное меню. + Отправляет информацию о балансе, символе, цене и инструкциях по торговле. + """ + user_id = callback.from_user.id + balance = await get_balance(user_id, callback.message) if balance: - symbol = await rq.get_symbol(callback.from_user.id) + symbol = await rq.get_symbol(user_id) + price = await get_price(user_id, symbol=symbol) - text = f'''💎 Торговля на Bybit - -⚖️ Ваш баланс (USDT): {balance} -📊 Текущая торговая пара: {symbol} - -Как начать торговлю? - -1️⃣ Проверьте и тщательно настройте все параметры в вашем профиле. -2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару заглавными буквами, без лишних символов (например: BTCUSDT). -''' + text = ( + f"💎 Торговля на Bybit\n\n" + f"⚖️ Ваш баланс (USDT): {float(balance):.2f}\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) -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) -> None: + """ + Отправляет пользователю информацию о балансе, символе и текущей цене, + вместе с инструкциями по началу торговли. + """ balance = await get_balance(message.from_user.id, message) - if balance: + if balance: symbol = await rq.get_symbol(message.from_user.id) + price = await get_price(message.from_user.id, symbol=symbol) - text = f'''💎 Торговля на Bybit - -⚖️ Ваш баланс (USDT): {balance} -📊 Текущая торговая пара: {symbol} - -Как начать торговлю? - -1️⃣ Проверьте и тщательно настройте все параметры в вашем профиле. -2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару заглавными буквами, без лишних символов (например: BTCUSDT). -''' + 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(text='Укажите торговую пару заглавными буквами без пробелов и лишних символов (пример: BTCUSDT): ') + await callback.message.answer( + 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): - await state.update_data(symbol = message.text) +async def update_symbol_for_trade(message: Message, state: FSMContext) -> None: + """ + Обрабатывает ввод торговой пары пользователем и проверяет её валидность. + При успешном обновлении сохранит пару и отправит обновлённую информацию. + """ + user_input = message.text.strip().upper() - data = await state.get_data() + 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) await message.answer('Пара была успешно обновлена') - await rq.update_symbol(message.from_user.id, data['symbol']) - await start_bybit_trade_message(message, state) + await rq.update_symbol(message.from_user.id, user_input) + await start_bybit_trade_message(message) 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) -> 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() - 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) -> None: + """ + Обработка выбора типа входа в позицию. + Если Limit, запрашивает цену лимитного ордера. + Если Market — обновляет настройки. + """ + order_type = callback.data.split(':')[1] + + if order_type not in ['Market', 'Limit']: + 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.message.answer(f"Выбран тип входа в позицию: {order_type}", + reply_markup=inline_markup.start_trading_markup) + await callback.answer() + except Exception as e: + 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) -> None: + """ + Обрабатывает ввод цены лимитного ордера, проверяет формат и сохраняет настройки. + """ + 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) + + 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_start_chatbot_trading") +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) + 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') + starting_quantity = safe_float(data_main_stgs.get('starting_quantity')) + + side = None + if switch_mode == 'Включено': + switch_state = data_main_stgs.get('switch_state', 'Long') + side = 'Buy' if switch_state == 'Long' else 'Sell' 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 + 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("Начинаю торговлю с использованием текущих настроек...") + + timer_data = await rq.get_user_timer(tg_id) + if isinstance(timer_data, dict): + timer_minute = timer_data.get('timer_minutes', 0) + else: + timer_minute = timer_data or 0 + + if timer_minute > 0: + 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, symbol=symbol, quantity=starting_quantity) + + 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: + await callback.message.answer(f"Выберите тип сделки:", + 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(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_"):] + 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: + 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: + """ + Показывает открытые позиции пользователя по символу. + """ + await callback.answer() + + try: + 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_"):] + 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: + 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) + 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(lambda c: c.data and c.data.startswith("close_deal:")) +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) + + if result: + 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_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: + 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: + """ + Запускает диалог с пользователем для задания задержки перед закрытием сделки. + """ + symbol = callback.data.split(":")[1] + await state.update_data(symbol=symbol) + 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_close_delay(message: Message, state: FSMContext) -> None: + """ + Обрабатывает ввод закрытия сделки с задержкой. + """ + 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") + + delay = delay_minutes * 60 + await message.answer(f"Закрытие сделки {symbol} запланировано через {delay_minutes} мин.") + await close_trade_after_delay(message.from_user.id, message, symbol, delay) + await state.clear() + + +@router_functions_bybit_trade.callback_query(F.data == "clb_change_martingale_reset") +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") +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 rq.update_trigger(tg_id, "Ручной") + 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_trade) + await callback.message.answer("Введите задержку в минутах перед остановкой торговли:", + reply_markup=inline_markup.cancel) + await callback.answer() + + +@router_functions_bybit_trade.message(CloseTradeTimerState.waiting_for_trade) +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 + + await message.answer(f"Торговля будет остановлена через {delay_minutes} минут.") + await asyncio.sleep(delay_seconds) + await rq.update_trigger(tg_id, "Ручной") + await message.answer("Автоматическая торговля остановлена.", 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: + """ + Отменяет текущее состояние FSM и сообщает пользователю об отмене. + """ + await state.clear() + await callback.message.answer("Отменено!", reply_markup=inline_markup.back_to_main) + await callback.answer() 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..51faa53 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, symbol=symbol) - 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..d86737f 100644 --- a/app/services/Bybit/functions/price_symbol.py +++ b/app/services/Bybit/functions/price_symbol.py @@ -1,24 +1,32 @@ 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, symbol: str) -> 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) 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 diff --git a/app/states/States.py b/app/states/States.py new file mode 100644 index 0000000..6b9c7d1 --- /dev/null +++ b/app/states/States.py @@ -0,0 +1,69 @@ +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() + waiting_for_trade = 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() + commission_fee = State() + + +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() + + +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 e9c034d..0040b50 100644 --- a/app/telegram/Keyboards/inline_keyboards.py +++ b/app/telegram/Keyboards/inline_keyboards.py @@ -1,112 +1,221 @@ 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")] +start_markup = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="🔥 Начать торговлю", callback_data="clb_start_chatbot_message")] ]) 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')] +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_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_to_main ]) + + connect_bybit_api_markup = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api')] ]) 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_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_stop_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')]]) # Клавиатура для возврата к списку каталога настроек +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")], + +]) + +cancel = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="Отменить", callback_data="clb_cancel")] +]) + +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_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_switch_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_maximum_quantity')], + [InlineKeyboardButton(text='Коэффициент Мартингейла', callback_data='clb_change_martingale_factor'), + InlineKeyboardButton(text='Сбросить шаги Мартингейла', callback_data='clb_change_martingale_reset')], + [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')], + [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=[ - [InlineKeyboardButton(text='Триггер', callback_data='clb_change_trigger'), - InlineKeyboardButton(text='Фильтр времени', callback_data='clb_change_filter_time')], + [InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_mode'), + 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 + back_btn_list_settings, + back_btn_to_main ]) 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')], - back_btn_list_settings + back_btn_list_settings, + back_btn_to_main ]) 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_smart")], - [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")], + 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 - [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_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=[ # ИЗМЕНИТЬ НА INLINE - [InlineKeyboardButton(text='Да', callback_data="clb_yes"), InlineKeyboardButton(text='Нет', callback_data="clb_yes")] +buttons_yes_no_markup = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text='Да', callback_data="clb_yes"), + InlineKeyboardButton(text='Нет', callback_data="clb_no")], ]) buttons_on_off_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE - [InlineKeyboardButton(text='Включить', callback_data="clb_on"), InlineKeyboardButton(text='Выключить', callback_data="clb_off")] + [InlineKeyboardButton(text='Включить', callback_data="clb_on"), + 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): + builder = InlineKeyboardBuilder() + for trade in trades: + 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() + + +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 + ]) + +def create_close_limit_markup(symbol: str) -> InlineKeyboardMarkup: + return InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="Закрыть лимитный ордер", callback_data=f"close_limit:{symbol}")], + back_btn_to_main + ]) + +timer_markup = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text="Установить таймер", callback_data="clb_set_timer")], + back_btn_to_main +]) + +stop_choice_markup = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text="Остановить сразу", callback_data="stop_immediately"), + InlineKeyboardButton(text="Остановить по таймеру", callback_data="stop_with_timer"), + ] + ] +) + +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/Keyboards/reply_keyboards.py b/app/telegram/Keyboards/reply_keyboards.py index daf376f..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) \ No newline at end of file +], resize_keyboard=True, one_time_keyboard=False) \ No newline at end of file diff --git a/app/telegram/database/models.py b/app/telegram/database/models.py index 69da607..a7e30f8 100644 --- a/app/telegram/database/models.py +++ b/app/telegram/database/models.py @@ -1,27 +1,55 @@ -import logging -logger = logging.getLogger(__name__) +from datetime import datetime +import logging.config +from sqlalchemy.sql.sqltypes import DateTime, Numeric 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) @@ -31,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) @@ -40,28 +87,68 @@ 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): Первичный ключ.. + """ __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): + """ + Основные настройки пользователя для торговли. + + Атрибуты: + 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) @@ -70,12 +157,29 @@ 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) + 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(Numeric(18, 15), nullable=True) + 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) @@ -85,15 +189,31 @@ 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(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) 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) @@ -101,7 +221,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) @@ -112,12 +243,60 @@ class User_Additional_Settings(Base): autostart = mapped_column(Boolean, default=False) notifications = mapped_column(Boolean, default=False) + +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) + + tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) + + symbol = mapped_column(String(18), default='PENGUUSDT') + side = mapped_column(String(10), nullable=False) + open_price = mapped_column(Integer(), nullable=False) + positive_percent = mapped_column(Integer(), nullable=False) + + +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) + + async def async_main(): + """ + Асинхронное создание всех таблиц и заполнение справочников начальными данными. + """ async with engine.begin() as conn: 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(): @@ -126,14 +305,7 @@ async def async_main(): types = ['Isolated', 'Cross'] for type in types: - result = await conn.execute(select(Margin_type).where(Margin_type.type == type)) + 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)) \ No newline at end of file diff --git a/app/telegram/database/requests.py b/app/telegram/database/requests.py index 4b67b5e..ad7951d 100644 --- a/app/telegram/database/requests.py +++ b/app/telegram/database/requests.py @@ -1,61 +1,93 @@ -import logging -logger = logging.getLogger(__name__) +import logging.config -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 logger_helper.logger_helper import LOGGING_CONFIG +from datetime import datetime, timedelta +from typing import Any -import app.telegram.functions.functions as func # functions +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, delete, update +from sqlalchemy import select, update -# SET_DB -async def save_tg_id_new_user(tg_id): +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("requests") + + +# --- Функции сохранения в БД --- + +async def save_tg_id_new_user(tg_id) -> None: + """ + Сохраняет Telegram 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)) 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 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, - )) - - logger.info(f"Bybit был успешно подключен") - + 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 был успешно добавлен") + 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: + """ + Создаёт основные настройки пользователя по умолчанию. + + 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)) @@ -63,14 +95,21 @@ 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: + """ + Создаёт настройки риск-менеджмента по умолчанию. + + Args: + tg_id (int): Telegram ID пользователя. + """ async with async_session() as session: settings = await session.scalar(select(URMS).where(URMS.tg_id == tg_id)) @@ -79,25 +118,40 @@ 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: + """ + Создаёт условные настройки по умолчанию. + + 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)) 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: + """ + Создаёт дополнительные настройки по умолчанию. + + Args: + tg_id (int): Telegram ID пользователя. + """ async with async_session() as session: settings = await session.scalar(select(UAS).where(UAS.tg_id == tg_id)) @@ -106,170 +160,419 @@ 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): + """ + Проверяет наличие пользователя в базе. + + 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 - + 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 + return symbol -async def get_for_registration_trading_mode(): + +async def get_user_trades(tg_id): + """Получить сделки пользователя.""" async with async_session() as session: - mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.id == 1)) - return mode + 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 get_for_registration_margin_type(): + +async def get_entry_order_type(tg_id: object) -> str | None | Any: + """Получить тип входного ордера пользователя.""" async with async_session() as session: - type = await session.scalar(select(Margin_type.type).where(Margin_type.id == 1)) - return type + order_type = await session.scalar( + select(UMS.entry_order_type).where(UMS.tg_id == tg_id) + ) + # Если в базе не установлен тип — возвращаем значение по умолчанию + return order_type or 'Market' -async def get_for_registration_trigger(): + +# --- Функции обновления данных --- + +async def update_user_trades(tg_id, **kwargs): + """Обновить сделки пользователя.""" 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)) - - data = { - 'trading_mode': trading_mode, - 'margin_type': margin_mode, - 'size_leverage': size_leverage, - 'starting_quantity': starting_quantity, - 'martingale_factor': martingale_factor, - 'maximal_quantity': maximal_quantity - } - - 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("Получение риск-менеджмента настроек пользователя") - - 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)) - - data = { - 'price_profit': price_profit, - 'price_loss': price_loss, - 'max_risk_deal': max_risk_deal - } - - 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)) - + query = update(USER_DEALS).where(USER_DEALS.tg_id == tg_id).values(**kwargs) + await session.execute(query) 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)) +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_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)) +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() -# UPDATE_MAIN_SETTINGS_DB + +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("Изменен трейд мод") - 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 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(): + """Получить режим торговли по умолчанию.""" + 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(tg_id): + """Получить триггер по умолчанию.""" + async with async_session() as session: + trigger = await session.scalar(select(UCS.trigger).where(tg_id == tg_id)) + 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("Получение основных настроек пользователя %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)) + 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)) + 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, + '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, + 'maximal_quantity': maximal_quantity, + 'entry_order_type': entry_order_type, + 'limit_order_price': limit_order_price, + 'martingale_step': martingale_step, + } + + 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("Получение риск-менеджмента настроек пользователя %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)) + 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, + 'commission_fee': commission_fee, + } + + return data + + 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() + + +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_limit_price(tg_id): + """Получить лимитную цену пользователя как float, либо None.""" + 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.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: + 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}") + + +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() + + +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 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 3d9e571..a33d8c9 100644 --- a/app/telegram/functions/condition_settings/settings.py +++ b/app/telegram/functions/condition_settings/settings.py @@ -1,19 +1,35 @@ -import app.telegram.Keyboards.inline_keyboards as inline_markup - +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 app.states.States import condition_settings -async def reg_new_user_default_condition_settings(id, message): +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("condition_settings") + +condition_settings_router = Router() + + +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 = """ Условия запуска -- Триггер: Ручной запуск / Сигнал TradingView / Полностью автоматический -- Фильтр времени: диапазон по дням недели и времени суток +async def main_settings_message(id, message): + + tg_id = id + trigger = await rq.get_for_registration_trigger(tg_id) + text = f""" Условия запуска + +- Режим торговли: {trigger} +- Таймер: установить таймер / остановить таймер - Фильтр волатильности / объёма: включить/отключить - Интеграции и внешние сигналы: - Использовать сигналы TradingView: да / нет @@ -21,21 +37,71 @@ 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 = '''Триггер - Описание ручного запуска, сигналов, автоматического режима ''' + +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) -async def filter_time_message(message, state): - text = '''Фильтр времени - ??? - ''' +@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 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 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) + + 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", + 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} минут.\nНажмите кнопку 'Начать торговлю' для запуска.", + reply_markup=inline_markup.start_trading_markup) + + await state.clear() + except ValueError: + await message.reply("Пожалуйста, введите корректное число.") + - await message.answer(text=text) async def filter_volatility_message(message, state): text = '''Фильтр волатильности @@ -44,6 +110,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 +118,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 +126,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) - - diff --git a/app/telegram/functions/functions.py b/app/telegram/functions/functions.py index 28e9134..19ab77b 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') + await message.answer("Добро пожаловать в чат-робот для автоматизации трейдинга — вашего надежного помощника для анализа рынка и принятия взвешенных решений.", + parse_mode='html', reply_markup=inline_markup.start_markup) async def profile_message(username, message): await message.answer(f""" @{username} diff --git a/app/telegram/functions/main_settings/settings.py b/app/telegram/functions/main_settings/settings.py index 632c234..65a08e3 100644 --- a/app/telegram/functions/main_settings/settings.py +++ b/app/telegram/functions/main_settings/settings.py @@ -1,23 +1,18 @@ 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() async def reg_new_user_default_main_settings(id, message): tg_id = id @@ -26,21 +21,25 @@ 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']} +- Состояние свитча: {data['switch_state']} - Тип маржи: {data['margin_type']} - Размер кредитного плеча: х{data['size_leverage']} - Начальная ставка: {data['starting_quantity']} - Коэффициент мартингейла: {data['martingale_factor']} -- Максимальное количесиво ставок в серии: {data['maximal_quantity']} +- Количество ставок в серии: {data['martingale_step']} +- Максимальное количество ставок в серии: {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) @@ -51,56 +50,101 @@ 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_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': + 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 size_leverage_message (message, state): + +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.answer(f"Включено") + await main_settings_message(tg_id, callback.message) + else: + await rq.update_switch_mode_enabled(tg_id, "Выключено") + await callback.answer(f"Выключено") + await main_settings_message(tg_id, callback.message) + 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) + else: + await rq.update_switch_state(tg_id, "Short") + await callback.message.answer(f"Состояние свитча: {val}") + await main_settings_message(tg_id, callback.message) + 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) + 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) @@ -109,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) @@ -133,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) @@ -160,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) @@ -202,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) @@ -226,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 859451e..5d0b32d 100644 --- a/app/telegram/functions/risk_management_settings/settings.py +++ b/app/telegram/functions/risk_management_settings/settings.py @@ -1,46 +1,49 @@ from aiogram import Router import app.telegram.Keyboards.inline_keyboards as inline_markup -import app.telegram.Keyboards.reply_keyboards as reply_markup - +import logging.config 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 + +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("risk_management_settings") router_risk_management_settings = Router() -class update_risk_management_settings(StatesGroup): - price_profit = State() - price_loss = State() - max_risk_deal = 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"""Риск менеджмент, -- Процент изменения цены для фиксации прибыли: {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', "Да")} + """ 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=inline_markup.cancel) - 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) @@ -49,50 +52,77 @@ 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=inline_markup.cancel) - 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) 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)) - await rq.update_price_loss(message.from_user.id, data['price_loss']) - await main_settings_message(message.from_user.id, message, state) + current_price_profit = data_settings.get('price_profit') + # Пробуем перевести price_profit в число, если это возможно + try: + current_price_profit_num = int(current_price_profit) + except Exception as e: + logger.error(e) + 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 main_settings_message(message.from_user.id, message) await state.clear() else: - await message.answer(f'⛔️ Ошибка: ваше значение ({data['price_loss']}%) или выше лимита (100) или вы вводите неверные символы') + await message.answer( + f'⛔️ Ошибка: ваше значение ({data["price_loss"]}%) выше лимита (100) или содержит неверные символы') + await main_settings_message(message.from_user.id, message) - 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) - text = 'Введите число (процент от баланса) для изменения максимального риска на сделку: ' + text = 'Введите число (процент от баланса) для изменения максимального риска на сделку: ' + + await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.cancel) - 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) @@ -101,10 +131,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) \ No newline at end of file + 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="Хотите учитывать комиссию биржи:", parse_mode='html', + reply_markup=inline_markup.buttons_yes_no_markup) + + +@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() diff --git a/app/telegram/handlers/handlers.py b/app/telegram/handlers/handlers.py index 8ea7fa3..aad0658 100644 --- a/app/telegram/handlers/handlers.py +++ b/app/telegram/handlers/handlers.py @@ -1,123 +1,195 @@ -import logging +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 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 + +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. + Инициализирует нового пользователя в БД. + + Args: + message (Message): Входящее сообщение с командой /start. + """ 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) - if user: +@router.message(F.text == "👤 Профиль") +async def profile_message(message: Message) -> None: + """ + Обработчик кнопки 'Профиль'. + Проверяет существование пользователя и отображает профиль. + + Args: + message (Message): Сообщение с текстом кнопки. + """ + user = await rq.check_user(message.from_user.id) + tg_id = message.from_user.id + balance = await get_balance(message.from_user.id, message) + if user and balance: + await run_ws_for_user(tg_id, message) 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) - - 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) -> None: + """ + Обработчик колбэка 'clb_start_chatbot_message'. + Если пользователь есть в БД — показывает профиль, + иначе регистрирует нового пользователя и инициализирует настройки. + + Args: + 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 "Пользователь" - 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}' - - 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) 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) 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() - - # Настройки торговли + + @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): - await func_main_settings.main_settings_message(callback.from_user.id, callback.message, state) - await callback.answer() +@router.callback_query(F.data == "clb_change_main_settings") +async def clb_change_main_settings_message(callback: CallbackQuery) -> None: + """ + Открыть меню изменения главных настроек. + + Args: + callback (CallbackQuery): полученный колбэк. + """ + await func_main_settings.main_settings_message(callback.from_user.id, callback.message) + + await callback.answer() + @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) -> 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): - await func_condition_settings.main_settings_message(callback.from_user.id, callback.message, state) +async def clb_change_condition_message(callback: CallbackQuery) -> None: + """ + Открыть меню изменения настроек условий. + + Args: + callback (CallbackQuery): полученный колбэк. + """ + await func_condition_settings.main_settings_message(callback.from_user.id, callback.message) 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) +async def clb_change_additional_message(callback: CallbackQuery) -> None: + """ + Открыть меню изменения дополнительных настроек. + + Args: + callback (CallbackQuery): полученный колбэк. + """ + await func_additional_settings.main_settings_message(callback.from_user.id, callback.message) 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_switch_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): +async def clb_main_settings_msg(callback: CallbackQuery, state: FSMContext) -> None: + """ + Обработчик колбэков изменения главных настроек с dispatch через match-case. + + Args: + callback (CallbackQuery): полученный колбэк. + state (FSMContext): текущее состояние FSM. + """ await callback.answer() try: 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': @@ -129,15 +201,25 @@ 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', + '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', -] @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: @@ -148,28 +230,39 @@ 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}") - - -list_condition_settings = ['clb_change_trigger', - 'clb_change_filter_time', + logger.error(f"Error callback in risk_management match-case: {e}") + + +list_condition_settings = ['clb_change_mode', + '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): +async def clb_condition_settings_msg(callback: CallbackQuery, state: FSMContext) -> None: + """ + Обработчик изменений настроек условий трейдинга. + + Args: + callback (CallbackQuery): полученный колбэк. + state (FSMContext): текущее состояние FSM. + """ 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_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) case 'clb_change_filter_volatility': await func_condition_settings.filter_volatility_message(callback.message, state) case 'clb_change_external_cues': @@ -181,15 +274,24 @@ 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', + '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): +async def clb_additional_settings_msg(callback: CallbackQuery, state: FSMContext) -> None: + """ + Обработчик дополнительных настроек бота. + + Args: + callback (CallbackQuery): полученный колбэк. + state (FSMContext): текущее состояние FSM. + """ await callback.answer() try: @@ -201,4 +303,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}") \ No newline at end of file + logger.error(f"Error callback in additional_settings match-case: {e}") 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 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 diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000..4466452 Binary files /dev/null and b/db.sqlite3 differ diff --git a/logger_helper/logger_helper.py b/logger_helper/logger_helper.py new file mode 100644 index 0000000..f28566e --- /dev/null +++ b/logger_helper/logger_helper.py @@ -0,0 +1,114 @@ +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, + }, + "conditions_settings": { + "handlers": ["console", "timed_rotating_file"], + "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", + "propagate": False, + }, + "bybit_ws": { + "handlers": ["console", "timed_rotating_file"], + "level": "DEBUG", + "propagate": False, + }, + "tasks": { + "handlers": ["console", "timed_rotating_file"], + "level": "DEBUG", + "propagate": False, + }, + }, +} 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..66baab5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,30 @@ +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 +nest-asyncio==1.6.0 +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