1
0
forked from kodorvan/stcs

Compare commits

...

39 Commits

Author SHA1 Message Date
algizn97
058ba09c03 Fixed 2025-09-03 21:33:08 +05:00
algizn97
dd53e5a14a Fixed 2025-09-03 21:28:12 +05:00
algizn97
3bd6b7363c Fixed 2025-08-31 11:47:13 +05:00
algizn97
2ee8c9916f Fixed 2025-08-30 16:29:56 +05:00
algizn97
3462078a47 Fixed 2025-08-29 11:44:24 +05:00
algizn97
8715b32139 Fixed 2025-08-29 11:43:52 +05:00
algizn97
4245e165bf Updated 2025-08-29 11:43:11 +05:00
algizn97
f4ff128236 Added trigger function 2025-08-29 11:42:55 +05:00
algizn97
f09fe1d70b Added new request 2025-08-29 11:42:03 +05:00
algizn97
4f774160b3 Updated States.py 2025-08-29 11:41:37 +05:00
algizn97
f6130c0b8c Deleted tasks.py 2025-08-29 11:41:18 +05:00
algizn97
e05b214a8a Added the ability to get a list of open trades and limit orders, as well as their closures (previously it was possible only for the selected pair) 2025-08-28 11:25:48 +05:00
algizn97
704249d0af Fixed and updated tasks 2025-08-27 14:28:53 +05:00
algizn97
bf44b481e9 Added new handlers for tasks 2025-08-27 14:28:23 +05:00
algizn97
02fa03c824 Added documentation, added event_loop for tasks, added WebSocket 2025-08-27 13:28:44 +05:00
algizn97
4406003a6e Fixed 2025-08-27 13:28:33 +05:00
algizn97
3c282975c1 Updated 2025-08-27 12:56:32 +05:00
algizn97
aec8fea628 Updated 2025-08-27 12:56:22 +05:00
algizn97
78b76b4aa6 Updated README.md 2025-08-27 12:56:01 +05:00
algizn97
91cfdbc37b Added loggers 2025-08-27 12:55:43 +05:00
algizn97
f822220c40 Added documentation and update functions 2025-08-27 12:55:21 +05:00
algizn97
9032957631 Added documentation, added websocket and tasks 2025-08-27 12:54:45 +05:00
algizn97
a140e0eb6f Added documentation 2025-08-27 12:53:35 +05:00
algizn97
511b08e8e5 Added WebSocket 2025-08-27 12:52:33 +05:00
algizn97
50afefeb5f Update 2025-08-26 19:36:55 +05:00
algizn97
07df16dbe9 Added new functions and documentation 2025-08-26 19:36:11 +05:00
algizn97
8bc4c634fe Update 2025-08-26 19:35:40 +05:00
algizn97
7c48336a62 Update 2025-08-26 19:35:01 +05:00
algizn97
fd279f0562 Added loggers for models 2025-08-26 19:34:47 +05:00
algizn97
43e62fdeff Update keyboards 2025-08-26 19:11:53 +05:00
algizn97
f23bda38f4 Added requirements.txt 2025-08-26 19:11:23 +05:00
algizn97
29a5df0b1a Added tasks 2025-08-26 19:11:07 +05:00
algizn97
2597615630 Added states 2025-08-26 19:10:57 +05:00
algizn97
d29b4465ad added documentation 2025-08-26 19:10:43 +05:00
algizn97
73f0c67564 updated timer functions 2025-08-25 17:08:33 +05:00
algizn97
554166eeaf update 2025-08-25 17:08:14 +05:00
algizn97
964c0a09b8 Added keyboard 2025-08-25 17:08:04 +05:00
algizn97
61979653e0 Updated 2025-08-25 17:07:50 +05:00
algizn97
39b8d17498 Updated timer functions 2025-08-25 17:07:41 +05:00
23 changed files with 2179 additions and 786 deletions

2
.gitignore vendored
View File

@@ -7,3 +7,5 @@ __pycache__/
env/ env/
venv/ venv/
.venv/ .venv/
.idea
/.idea

View File

@@ -1,11 +1,9 @@
import asyncio import asyncio
import logging.config import logging.config
from aiogram import Bot, Dispatcher 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.database.models import async_main
from app.telegram.handlers.handlers import router from app.telegram.handlers.handlers import router
from app.telegram.functions.main_settings.settings import router_main_settings 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.risk_management_settings.settings import router_risk_management_settings
@@ -14,7 +12,7 @@ from app.services.Bybit.functions.Add_Bybit_API import router_register_bybit_api
from app.services.Bybit.functions.functions import router_functions_bybit_trade from app.services.Bybit.functions.functions import router_functions_bybit_trade
from logger_helper.logger_helper import LOGGING_CONFIG from logger_helper.logger_helper import LOGGING_CONFIG
from config import TOKEN_TG_BOT_1, TOKEN_TG_BOT_2, TOKEN_TG_BOT_3 from config import TOKEN_TG_BOT_1
logging.config.dictConfig(LOGGING_CONFIG) logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("main") logger = logging.getLogger("main")
@@ -22,7 +20,14 @@ logger = logging.getLogger("main")
bot = Bot(token=TOKEN_TG_BOT_1) bot = Bot(token=TOKEN_TG_BOT_1)
dp = Dispatcher() dp = Dispatcher()
async def main():
async def main() -> None:
"""
Основная асинхронная функция запуска бота:
"""
loop = get_or_create_event_loop()
set_event_loop(loop)
await async_main() await async_main()
dp.include_router(router) dp.include_router(router)
@@ -34,6 +39,7 @@ async def main():
await dp.start_polling(bot) await dp.start_polling(bot)
if __name__ == '__main__': if __name__ == '__main__':
try: try:
logger.info("Bot is on") logger.info("Bot is on")

View File

@@ -1,20 +1,74 @@
# Чат-робот STCS Crypto Trading Telegram Bot
__
**Функционал:** Этот бот — автоматизированный торговый помощник для работы с криптовалютной биржей Bybit на основе стратегии мартингейла. Он позволяет торговать бессрочными контрактами с управлением рисками, тейк-профитами, стоп-лоссами и кредитным плечом.
+ **Настройки**
+ Основные параметры *(Настроен, работает)* ##Основные возможности
+ Режим торговли Лонг/Шорт *(настроены)*, Switch/Smart *(не настроены)*
+ Тип маржи: Изолированная / Кросс *(настроено)* - Поддержка работы с биржей Bybit через официальный API.
+ Размер кредитного плеча: от x1 до x100 *(настроено)*
+ Начальная ставка: числовое значение *(настроено)* - Открытие и закрытие позиций по выбранным торговым парам.
+ Коэффициент мартингейла: число *(настроено)*
+ Максимальное количество ставок в серии: число *(настроено)* - Поддержка рыночных и лимитных ордеров.
+ Риск-менеджмент (Настроен, работает)
+ Процент изменения цены для фиксации прибыли (TP%): число *(настроено)* - Установка уровней тейк-профита (TP) и стоп-лосса (SL).
+ Процент изменения цены для фиксации убытков (SL%): число (пример: 1%) *(настроено)*
+ Максимальный риск на сделку (в % от баланса): число (опционально) *(настроено)* - Управление кредитным плечом (leverage).
+ Условия запуска *(Не настроен)*
+ Дополнительные параметры *(Не настроен)* - Реализация стратегии мартингейла с настройками шага, коэффициента и лимитов.
+ Подключение Bybit *(настроено)*
+ Информация о правильном получении и сохранении Bybit-API keys *(настроено)* - Контроль максимального риска на сделку по балансу пользователя.
- Обработка ошибок API, логирование событий и информирование пользователя.
- Таймеры для отложенного открытия и закрытия сделок.
- Интерактивное меню и ввод настроек через Telegram.
- Хранение пользовательских настроек и статистики в базе данных.
##Установка и запуск
1. Клонируйте репозиторий:
git clone <URL_репозитория>
2. Установите зависимости:
pip install -r requirements.txt
3. Создайте файл .env и настройте переменные окружения.
4. Запустите бота:
python BybitBot_API.py
##Настройки пользователя
- Кредитное плечо (например, 15x)
- Торговая пара (например, DOGEUSDT, BTCUSDT)
- Начальное количество для сделок
- Тип ордера (Market или Limit)
- Уровни Take Profit и Stop Loss (в процентах или цене)
- Коэффициент мартингейла и максимальное количество шагов
- Максимально допустимый риск на одну сделку (% от баланса)
- Таймеры для старта и закрытия сделок
##Безопасность и риски
- Бот требует аккуратной настройки параметров риска.
- Храните API ключи в безопасности, избегайте публикации.

View File

@@ -7,8 +7,7 @@ import app.telegram.Keyboards.reply_keyboards as reply_markup
import app.telegram.database.requests as rq import app.telegram.database.requests as rq
from aiogram.types import Message, CallbackQuery from aiogram.types import Message, CallbackQuery
# FSM - Механизм состояния from app.states.States import state_reg_bybit_api
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
logging.config.dictConfig(LOGGING_CONFIG) logging.config.dictConfig(LOGGING_CONFIG)
@@ -16,12 +15,13 @@ logger = logging.getLogger("add_bybit_api")
router_register_bybit_api = Router() 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') @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 = '''<b>Подключение Bybit аккаунта</b> text = '''<b>Подключение Bybit аккаунта</b>
<b>1. Зарегистрируйтесь или войдите в свой аккаунт на Bybit (https://www.bybit.com/).</b> <b>1. Зарегистрируйтесь или войдите в свой аккаунт на Bybit (https://www.bybit.com/).</b>
@@ -43,17 +43,27 @@ async def info_for_bybit_api_message(callback: CallbackQuery):
await callback.answer() await callback.answer()
@router_register_bybit_api.callback_query(F.data == 'clb_new_user_connect_bybit_api') @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) await state.set_state(state_reg_bybit_api.api_key)
text = 'Отправьте KEY_API ниже: ' text = 'Отправьте KEY_API ниже: '
await callback.message.answer(text=text) await callback.message.answer(text=text)
@router_register_bybit_api.message(state_reg_bybit_api.api_key) @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): async def add_api_key_and_message_for_secret_key(message: Message, state: FSMContext) -> None:
await state.update_data(api_key = message.text) """
Сохраняет API Key во временное состояние FSM,
затем запрашивает у пользователя ввод Secret Key.
"""
await state.update_data(api_key=message.text)
text = 'Отправьте SECRET_KEY ниже' text = 'Отправьте SECRET_KEY ниже'
@@ -61,9 +71,14 @@ async def add_api_key_and_message_for_secret_key(message: Message, state: FSMCon
await state.set_state(state_reg_bybit_api.secret_key) await state.set_state(state_reg_bybit_api.secret_key)
@router_register_bybit_api.message(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): async def add_secret_key(message: Message, state: FSMContext) -> None:
await state.update_data(secret_key = message.text) """
Сохраняет Secret Key и финализирует регистрацию,
обновляет базу данных, устанавливает символ пользователя и очищает состояние.
"""
await state.update_data(secret_key=message.text)
data = await state.get_data() data = await state.get_data()
@@ -73,6 +88,5 @@ async def add_secret_key(message: Message, state: FSMContext):
await state.clear() await state.clear()
await message.answer('Данные добавлены, нажмите на профиль и начните торговлю!', reply_markup=reply_markup.base_buttons_markup) await message.answer('Данные добавлены, нажмите на профиль и начните торговлю!',
reply_markup=reply_markup.base_buttons_markup)

View File

@@ -1,5 +1,7 @@
import asyncio import asyncio
import json
import time import time
import logging.config import logging.config
from pybit import exceptions from pybit import exceptions
from pybit.unified_trading import HTTP from pybit.unified_trading import HTTP
@@ -7,240 +9,720 @@ from logger_helper.logger_helper import LOGGING_CONFIG
import app.services.Bybit.functions.price_symbol as price_symbol import app.services.Bybit.functions.price_symbol as price_symbol
import app.services.Bybit.functions.balance as balance_g import app.services.Bybit.functions.balance as balance_g
import app.telegram.database.requests as rq import app.telegram.database.requests as rq
import app.telegram.Keyboards.inline_keyboards as inline_markup
logging.config.dictConfig(LOGGING_CONFIG) logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("futures") logger = logging.getLogger("futures")
async def info_access_open_deal(message, symbol, trade_mode, margin_mode, leverage, qty): processed_trade_ids = set()
human_margin_mode = 'Isolated' if margin_mode == 'ISOLATED_MARGIN' else 'Cross'
text = f'''Позиция была успешна открыта!
Торговая пара: {symbol}
Движение: {trade_mode}
Тип-маржи: {human_margin_mode}
Кредитное плечо: {leverage}
Количество: {qty}
'''
await message.answer(text=text, parse_mode='html')
async def error_max_step(message): async def get_bybit_client(tg_id):
await message.answer('Сделка не была совершена, превышен лимит максимального количества ставок в серии.')
async def error_max_risk(message):
await message.answer('Сделка не была совершена, риск убытка превышает допустимый лимит.')
async def open_position(tg_id, message, side: str, margin_mode: str):
""" """
Открытие позиции (торговля с мартингейлом и управлением рисками) Асинхронно получает экземпляр клиента Bybit.
:param tg_id: Telegram ID пользователя :param tg_id: int - ID пользователя Telegram
:param message: объект сообщения Telegram для ответов :return: HTTP - экземпляр клиента Bybit
:param side: 'Buy' для Long, 'Sell' для Short
:param margin_mode: 'Isolated' или 'Cross'
""" """
api_key = await rq.get_bybit_api_key(tg_id) api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id) secret_key = await rq.get_bybit_secret_key(tg_id)
symbol = await rq.get_symbol(tg_id) 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_main_stgs = await rq.get_user_main_settings(tg_id)
order_type = data_main_stgs.get('entry_order_type', 'Market') symbol = data.get('symbol')
switch_mode = data_main_stgs.get('switch_mode', 'Включено')
trading_mode = data_main_stgs.get('trading_mode', 'Long')
trigger = await rq.get_for_registration_trigger(tg_id)
margin_mode = data_main_stgs.get('margin_type', 'Isolated')
starting_quantity = safe_float(data_main_stgs.get('starting_quantity'))
trade_info = format_trade_details_position(data=msg, commission_fee=commission_fee)
if trade_info:
await message.answer(f"{trade_info}", reply_markup=inline_markup.back_to_main)
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'
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 limit_price = None
if order_type == 'Limit': if order_type == 'Limit':
limit_price = await rq.get_limit_price(tg_id) limit_price = await rq.get_limit_price(tg_id)
data_risk_stgs = await rq.get_user_risk_management_settings(tg_id) data_risk_stgs = await rq.get_user_risk_management_settings(tg_id)
bybit_margin_mode = 'ISOLATED_MARGIN' if margin_mode == 'Isolated' else 'REGULAR_MARGIN'
client = HTTP(api_key=api_key, api_secret=secret_key)
try:
balance = await balance_g.get_balance(tg_id, message) balance = await balance_g.get_balance(tg_id, message)
price = await price_symbol.get_price(tg_id) price = await price_symbol.get_price(tg_id, symbol=symbol)
entry_price = safe_float(price)
# Установка маржинального режима max_martingale_steps = int(data_main_stgs.get('maximal_quantity', 0))
client.set_margin_mode(setMarginMode=bybit_margin_mode) current_martingale = await rq.get_martingale_step(tg_id)
max_risk_percent = safe_float(data_risk_stgs.get('max_risk_deal'))
martingale_factor = float(data_main_stgs['martingale_factor']) loss_profit = safe_float(data_risk_stgs.get('price_loss'))
max_martingale_steps = int(data_main_stgs['maximal_quantity'])
starting_quantity = float(data_main_stgs['starting_quantity'])
max_risk_percent = float(data_risk_stgs['max_risk_deal'])
loss_profit = float(data_risk_stgs['price_loss'])
takeprofit = float(data_risk_stgs['price_profit'])
commission_fee = float(data_risk_stgs.get('commission_fee', 0))
takeProfit = max(takeprofit - commission_fee, 0)
current_martingale_step = 0
next_quantity = starting_quantity
last_quantity = starting_quantity
realised_pnl = 0.0
# Получаем текущие открытые позиции по символу
positions_resp = client.get_positions(category='linear', symbol=symbol)
positions_list = positions_resp.get('result', {}).get('list', [])
current_martingale_step = await rq.get_martingale_step(tg_id)
if positions_list:
position = positions_list[0]
realised_pnl = float(position.get('unrealisedPnl', 0.0))
if realised_pnl > 0:
current_martingale_step = 0
next_quantity = starting_quantity
if order_type == 'Limit' and limit_price:
price_for_calc = limit_price
else: else:
current_martingale_step += 1 price_for_calc = entry_price
if current_martingale_step > max_martingale_steps:
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) await error_max_step(message)
return return
next_quantity = starting_quantity * (martingale_factor ** current_martingale_step)
else:
# Позиция не открыта — начинаем с начальной ставки
next_quantity = starting_quantity
current_martingale_step = 0
# Проверяем риск убытка
potential_loss = next_quantity * price * (loss_profit / 100)
allowed_loss = balance * (max_risk_percent / 100)
if potential_loss > allowed_loss: if potential_loss > allowed_loss:
await error_max_risk(message) await error_max_risk(message)
return 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( response = client.place_order(
category='linear', category='linear',
symbol=symbol, symbol=symbol,
side=side, side=side,
orderType=order_type, orderType=order_type,
qty=next_quantity, qty=str(quantity),
leverage=int(data_main_stgs['size_leverage']), price=str(limit_price) if order_type == 'Limit' and limit_price else None,
price=limit_price if order_type == 'Limit' else None, timeInForce='GTC',
takeProfit=takeProfit, orderLinkId=f"deal_{symbol}_{int(time.time())}"
stopLoss=loss_profit, )
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())}" orderLinkId=f"deal_{symbol}_{int(time.time())}"
) )
if response.get('ret_code', -1) == 0: if response.get('retCode', -1) == 0:
await info_access_open_deal(message, symbol, data_main_stgs['trading_mode'], bybit_margin_mode, return True
data_main_stgs['size_leverage'], next_quantity)
await rq.update_martingale_step(tg_id, current_martingale_step)
else: else:
await message.answer(f"Ошибка открытия ордера: {response.get('ret_msg', 'неизвестная ошибка')}") logger.error(f"Ошибка открытия ордера: {response}")
await message.answer(f"Ошибка открытия ордера", reply_markup=inline_markup.back_to_main)
except exceptions.InvalidRequestError as e:
logger.error(f"InvalidRequestError: {e}")
await message.answer('Ошибка: неверно указана торговая пара или параметры.')
except Exception as e:
logger.error(f"Ошибка при совершении сделки: {e}")
async def trading_cycle(tg_id, message):
start_time = time.time()
timer_min = await rq.get_user_timer(tg_id)
timer_sec = timer_min * 60 if timer_min else 0
while True:
elapsed = time.time() - start_time
if timer_sec > 0 and elapsed > timer_sec:
await message.answer("Время работы по таймеру истекло. Торговля остановлена.")
await rq.update_martingale_step(tg_id, 0)
break
# Проверяем позиции
data_main_stgs = await rq.get_user_main_settings(tg_id)
side = 'Buy' if data_main_stgs['trading_mode'] == 'Long' else 'Sell'
margin_mode = data_main_stgs.get('margin_type', 'Isolated')
# Можно добавлять логику по PNL, стоп-лоссам, тейк-профитам
await open_position(tg_id, message, side=side, margin_mode=margin_mode)
await asyncio.sleep(10)
async def get_active_positions(message, api_key, secret_key, symbol):
client = HTTP(
api_key=api_key,
api_secret=secret_key
)
instruments_resp = client.get_instruments_info(category='linear')
if instruments_resp.get('ret_code') != 0:
return []
symbols = [item['symbol'] for item in instruments_resp.get('result', {}).get('list', [])]
active_positions = []
async def fetch_positions(symbol):
try:
resp = client.get_positions(category='linear', symbol=symbol)
if resp.get('ret_code') == 0:
positions = resp.get('result', {}).get('list', [])
for pos in positions:
if pos.get('size') and float(pos['size']) > 0:
active_positions.append(pos)
except Exception as e:
logger.error(f"Ошибка при получении позиций: {e}")
await message.answer('⚠️ Ошибка при получении позиций')
for sym in symbols:
await fetch_positions(sym)
return active_positions
async def close_user_trade(tg_id: int, symbol: str) -> bool:
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
client = HTTP(api_key=api_key, api_secret=secret_key)
# Получаем текущие открытые позиции по символу (пример для linear фьючерсов)
positions_resp = client.get_positions(category="linear", symbol=symbol)
ret_code = positions_resp.get('ret_code')
result = positions_resp.get('result')
if ret_code != 0 or not result or not result.get('list'):
return False return False
positions_list = result['list'] 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
)
if resp.get('retCode') != 0:
await message.answer(f"Ошибка обновления TP/SL: {resp.get('retMsg')}",
reply_markup=inline_markup.back_to_main)
return
await message.answer(
f"ТП и СЛ успешно установлены:\nТейк-профит: {take_profit_price:.5f}\nСтоп-лосс: {stop_loss_price:.5f}",
reply_markup=inline_markup.back_to_main)
except Exception as e:
logger.error(f"Ошибка установки TP/SL для {symbol}: {e}", exc_info=True)
await message.answer("Произошла ошибка при установке TP и SL.", reply_markup=inline_markup.back_to_main)
async def cancel_all_tp_sl_orders(tg_id, symbol):
"""
Отменяет лимитные ордера для указанного символа.
"""
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', [])
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}")
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"
)
await message.answer(text, reply_markup=inline_markup.create_close_deal_markup(symbol))
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: if not positions_list:
return False return False
position = positions_list[0] position = positions_list[0]
qty = abs(float(position['size'])) qty = abs(safe_float(position.get('size')))
side = position['side'] side = position.get('side')
if qty == 0: if qty == 0:
return False return False
# Определяем сторону закрытия — противоположная открытой позиции
close_side = "Sell" if side == "Buy" else "Buy" close_side = "Sell" if side == "Buy" else "Buy"
try: place_resp = client.place_order(
response = client.place_order(
category="linear", category="linear",
symbol=symbol, symbol=symbol,
side=close_side, side=close_side,
orderType="Market", orderType="Market",
qty=str(qty), qty=str(qty),
timeInForce="GoodTillCancel", timeInForce="GTC",
reduceOnly=True reduceOnly=True
) )
return response['ret_code'] == 0
except Exception as e:
logger.error(f"Ошибка закрытия сделки {symbol} для пользователя {tg_id}: {e}")
if place_resp.get('retCode') == 0:
return True
else:
return False
except Exception as e:
logger.error(f"Ошибка закрытия сделки {symbol} для пользователя {tg_id}: {e}", exc_info=True)
return False return False
def get_positive_percent(negative_percent: float, manual_positive_percent: float | None) -> float: async def close_trade_after_delay(tg_id: int, message, symbol: str, delay_sec: int):
if manual_positive_percent and manual_positive_percent > 0: """
return manual_positive_percent Закрывает сделку пользователя после задержки delay_sec секунд.
return abs(negative_percent) """
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} по таймеру отменено.")

View File

@@ -34,7 +34,7 @@ async def get_balance(tg_id: int, message) -> float:
if api_key == 'None' or secret_key == 'None': if api_key == 'None' or secret_key == 'None':
await message.answer('⚠️ Подключите платформу для торговли', await message.answer('⚠️ Подключите платформу для торговли',
reply_markup=inline_markup.connect_bybit_api_markup) reply_markup=inline_markup.connect_bybit_api_message)
return 0 return 0
try: try:

View File

@@ -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)

View File

@@ -1,18 +1,24 @@
import asyncio import asyncio
import logging.config import logging.config
from aiogram import F, Router from aiogram import F, Router
from app.telegram.functions.main_settings.settings import main_settings_message
from logger_helper.logger_helper import LOGGING_CONFIG from logger_helper.logger_helper import LOGGING_CONFIG
from app.services.Bybit.functions import Futures, min_qty
from app.services.Bybit.functions.Futures import open_position, close_user_trade, get_active_positions, trading_cycle from app.services.Bybit.functions.Futures import (close_user_trade, 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 from app.services.Bybit.functions.balance import get_balance
import app.telegram.Keyboards.inline_keyboards as inline_markup import app.telegram.Keyboards.inline_keyboards as inline_markup
import app.telegram.Keyboards.reply_keyboards as reply_markup
from pybit.unified_trading import HTTP
import app.telegram.database.requests as rq import app.telegram.database.requests as rq
from aiogram.types import Message, CallbackQuery from aiogram.types import Message, CallbackQuery
from app.services.Bybit.functions.price_symbol import get_price
# FSM - Механизм состояния from app.states.States import (state_update_entry_type, state_update_symbol, state_limit_price,
from aiogram.fsm.state import State, StatesGroup SetTP_SL_State, CloseTradeTimerState)
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from app.services.Bybit.functions.get_valid_symbol import get_valid_symbols from app.services.Bybit.functions.get_valid_symbol import get_valid_symbols
@@ -23,71 +29,62 @@ logger = logging.getLogger("functions")
router_functions_bybit_trade = Router() router_functions_bybit_trade = Router()
class state_update_symbol(StatesGroup):
symbol = State()
class state_update_entry_type(StatesGroup):
entry_type = State()
class TradeSetup(StatesGroup):
waiting_for_timer = State()
waiting_for_positive_percent = State()
class state_limit_price(StatesGroup):
price = State()
@router_functions_bybit_trade.callback_query(F.data.in_(['clb_start_trading', 'clb_back_to_main', 'back_to_main'])) @router_functions_bybit_trade.callback_query(F.data.in_(['clb_start_trading', 'clb_back_to_main', 'back_to_main']))
async def clb_start_bybit_trade_message(callback: CallbackQuery, state: FSMContext): async def clb_start_bybit_trade_message(callback: CallbackQuery) -> None:
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) Отправляет информацию о балансе, символе, цене и инструкциях по торговле.
"""
user_id = callback.from_user.id
balance = await get_balance(user_id, callback.message)
if balance: 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 text = (
f"💎 Торговля на Bybit\n\n"
⚖️ Ваш баланс (USDT): {balance} f"⚖️ Ваш баланс (USDT): {float(balance):.2f}\n"
📊 Текущая торговая пара: {symbol} f"📊 Текущая торговая пара: {symbol}\n"
f"$$$ Цена: {price}\n\n"
Как начать торговлю? "Как начать торговлю?\n\n"
"1⃣ Проверьте и тщательно настройте все параметры в вашем профиле.\n"
1⃣ Проверьте и тщательно настройте все параметры в вашем профиле. "2⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).\n"
2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT). "3️⃣ Нажмите кнопку 'Начать торговать'.\n"
3⃣ Нажмите кнопку 'Выбрать тип входа' и после нажмите начать торговлю. )
'''
await callback.message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup) await callback.message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup)
async def start_bybit_trade_message(message, state): async def start_bybit_trade_message(message: Message) -> None:
api = await rq.get_bybit_api_key(message.from_user.id) """
secret = await rq.get_bybit_secret_key(message.from_user.id) Отправляет пользователю информацию о балансе, символе и текущей цене,
вместе с инструкциями по началу торговли.
"""
balance = await get_balance(message.from_user.id, message) balance = await get_balance(message.from_user.id, message)
if balance: if balance:
symbol = await rq.get_symbol(message.from_user.id) symbol = await rq.get_symbol(message.from_user.id)
price = await get_price(message.from_user.id, symbol=symbol)
text = f'''💎 Торговля на Bybit text = (
f"💎 Торговля на Bybit\n\n"
⚖️ Ваш баланс (USDT): {balance} f"⚖️ Ваш баланс (USDT): {balance}\n"
📊 Текущая торговая пара: {symbol} f"📊 Текущая торговая пара: {symbol}\n"
f"$$$ Цена: {price}\n\n"
Как начать торговлю? "Как начать торговлю?\n\n"
"1⃣ Проверьте и тщательно настройте все параметры в вашем профиле.\n"
1⃣ Проверьте и тщательно настройте все параметры в вашем профиле. "2⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).\n"
2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT). "3️⃣ Нажмите кнопку 'Начать торговать'.\n"
3⃣ Нажмите кнопку 'Выбрать тип входа' и после нажмите начать торговлю. )
'''
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup) await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup)
@router_functions_bybit_trade.callback_query(F.data == 'clb_update_trading_pair') @router_functions_bybit_trade.callback_query(F.data == 'clb_update_trading_pair')
async def update_symbol_for_trade_message(callback: CallbackQuery, state: FSMContext): async def update_symbol_for_trade_message(callback: CallbackQuery, state: FSMContext) -> None:
"""
Начинает процедуру обновления торговой пары, переводит пользователя в состояние ожидания пары.
"""
await state.set_state(state_update_symbol.symbol) await state.set_state(state_update_symbol.symbol)
await callback.message.answer( await callback.message.answer(
@@ -96,7 +93,11 @@ async def update_symbol_for_trade_message(callback: CallbackQuery, state: FSMCon
@router_functions_bybit_trade.message(state_update_symbol.symbol) @router_functions_bybit_trade.message(state_update_symbol.symbol)
async def update_symbol_for_trade(message: Message, state: FSMContext): async def update_symbol_for_trade(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод торговой пары пользователем и проверяет её валидность.
При успешном обновлении сохранит пару и отправит обновлённую информацию.
"""
user_input = message.text.strip().upper() user_input = message.text.strip().upper()
exists = await get_valid_symbols(message.from_user.id, user_input) exists = await get_valid_symbols(message.from_user.id, user_input)
@@ -108,20 +109,28 @@ async def update_symbol_for_trade(message: Message, state: FSMContext):
await state.update_data(symbol=message.text) await state.update_data(symbol=message.text)
await message.answer('Пара была успешно обновлена') await message.answer('Пара была успешно обновлена')
await rq.update_symbol(message.from_user.id, user_input) await rq.update_symbol(message.from_user.id, user_input)
await start_bybit_trade_message(message, state) await start_bybit_trade_message(message)
await state.clear() await state.clear()
@router_functions_bybit_trade.callback_query(F.data == 'clb_update_entry_type') @router_functions_bybit_trade.callback_query(F.data == 'clb_update_entry_type')
async def update_entry_type_message(callback: CallbackQuery, state: FSMContext): async def update_entry_type_message(callback: CallbackQuery, state: FSMContext) -> None:
"""
Запрашивает у пользователя тип входа в позицию (Market или Limit).
"""
await state.set_state(state_update_entry_type.entry_type) await state.set_state(state_update_entry_type.entry_type)
await callback.message.answer("Выберите тип входа в позицию:", reply_markup=inline_markup.entry_order_type_markup) await callback.message.answer("Выберите тип входа в позицию:", reply_markup=inline_markup.entry_order_type_markup)
await callback.answer() await callback.answer()
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith('entry_order_type:')) @router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith('entry_order_type:'))
async def entry_order_type_callback(callback: CallbackQuery, state: FSMContext): async def entry_order_type_callback(callback: CallbackQuery, state: FSMContext) -> None:
"""
Обработка выбора типа входа в позицию.
Если Limit, запрашивает цену лимитного ордера.
Если Market — обновляет настройки.
"""
order_type = callback.data.split(':')[1] order_type = callback.data.split(':')[1]
if order_type not in ['Market', 'Limit']: if order_type not in ['Market', 'Limit']:
@@ -148,18 +157,21 @@ async def entry_order_type_callback(callback: CallbackQuery, state: FSMContext):
@router_functions_bybit_trade.message(state_limit_price.price) @router_functions_bybit_trade.message(state_limit_price.price)
async def set_limit_price(message: Message, state: FSMContext): async def set_limit_price(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод цены лимитного ордера, проверяет формат и сохраняет настройки.
"""
try: try:
price = float(message.text) price = float(message.text)
if price <= 0: if price <= 0:
await message.answer("Цена должна быть положительным числом. Попробуйте снова.", reply_markup=inline_markup.cancel) await message.answer("Цена должна быть положительным числом. Попробуйте снова.",
reply_markup=inline_markup.cancel)
return return
except ValueError: except ValueError:
await message.answer("Некорректный формат цены. Введите число.", reply_markup=inline_markup.cancel) await message.answer("Некорректный формат цены. Введите число.", reply_markup=inline_markup.cancel)
return return
await state.update_data(entry_order_type='Limit', limit_price=price) await state.update_data(entry_order_type='Limit', limit_price=price)
data = await state.get_data()
await rq.update_entry_order_type(message.from_user.id, 'Limit') await rq.update_entry_order_type(message.from_user.id, 'Limit')
await rq.update_limit_price(message.from_user.id, price) await rq.update_limit_price(message.from_user.id, price)
@@ -168,165 +180,344 @@ async def set_limit_price(message: Message, state: FSMContext):
await state.clear() 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:
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") @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): async def show_my_trades_callback(callback: CallbackQuery):
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)
symbol = await rq.get_symbol(tg_id)
trades = await get_active_positions(callback.message, api_key, secret_key, symbol)
if not trades:
await callback.message.answer("Нет активных позиций.")
await callback.answer() 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 return
keyboard = inline_markup.create_trades_inline_keyboard(trades) await state.update_data(take_profit=tp)
await state.set_state(SetTP_SL_State.waiting_for_stop_loss)
await callback.message.answer( await message.answer("Введите значение Stop Loss (в цене, например 24500.3):", reply_markup=inline_markup.cancel)
"Выберите сделку из списка:",
reply_markup=keyboard
)
await callback.answer()
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith('select_trade:')) @router_functions_bybit_trade.message(SetTP_SL_State.waiting_for_stop_loss)
async def on_trade_selected(callback: CallbackQuery): async def process_stop_loss(message: Message, state: FSMContext) -> None:
symbol = callback.data.split(':')[1] """
Обрабатывает ввод значения Stop Loss и завершает процесс установки TP/SL.
tg_id = callback.from_user.id """
api_key = await rq.get_bybit_api_key(tg_id) try:
secret_key = await rq.get_bybit_secret_key(tg_id) sl = float(message.text.strip())
if sl <= 0:
positions = await get_active_positions(callback.message, api_key, secret_key, symbol) await message.answer("Значение Stop Loss должно быть положительным числом. Попробуйте снова.",
reply_markup=inline_markup.cancel)
# Если несколько позиций по символу, можно выбрать нужную или взять первую return
if not positions: except ValueError:
await callback.message.answer("Позиция не найдена") await message.answer("Некорректный ввод. Пожалуйста, введите число для Stop Loss.",
await callback.answer() reply_markup=inline_markup.cancel)
return return
pos = positions[0] data = await state.get_data()
symbol = pos.get('symbol') tp = data.get("take_profit")
side = pos.get('side')
entry_price = pos.get('entryPrice') # Цена открытия позиции
current_price = pos.get('price') # Текущая цена (если есть)
text = (f"Информация по позиции:\n" if tp is None:
f"Название: {symbol}\n" await message.answer("Ошибка, не найдено значение Take Profit. Попробуйте снова.")
f"Направление: {side}\n" await state.clear()
f"Цена покупки: {entry_price}\n" return
f"Текущая цена: {current_price if current_price else 'N/A'}")
keyboard = inline_markup.create_close_deal_markup(symbol) tg_id = message.from_user.id
await callback.message.answer(text, reply_markup=keyboard) await set_take_profit_stop_loss(tg_id, message, take_profit_price=tp, stop_loss_price=sl)
await callback.answer()
await state.clear()
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_deal:")) @router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_deal:"))
async def close_trade_callback(callback: CallbackQuery): async def close_trade_callback(callback: CallbackQuery) -> None:
"""
Закрывает сделку пользователя по символу.
"""
symbol = callback.data.split(':')[1] symbol = callback.data.split(':')[1]
tg_id = callback.from_user.id tg_id = callback.from_user.id
result = await close_user_trade(tg_id, symbol) result = await close_user_trade(tg_id, symbol)
if result: if result:
await callback.message.answer(f"Сделка {symbol} успешно закрыта.") logger.info(f"Сделка {symbol} успешно закрыта.")
else: else:
logger.error(f"Не удалось закрыть сделку {symbol}.")
await callback.message.answer(f"Не удалось закрыть сделку {symbol}.") await callback.message.answer(f"Не удалось закрыть сделку {symbol}.")
await callback.answer() await callback.answer()
@router_functions_bybit_trade.callback_query(F.data == "clb_start_chatbot_trading") @router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_limit:"))
async def start_trading_process(callback: CallbackQuery, state: FSMContext): async def close_trade_callback(callback: CallbackQuery) -> None:
"""
Закрывает ордера пользователя по символу.
"""
symbol = callback.data.split(':')[1]
tg_id = callback.from_user.id tg_id = callback.from_user.id
message = callback.message
# Получаем настройки пользователя result = await cancel_all_tp_sl_orders(tg_id, symbol)
data_main_stgs = await rq.get_user_main_settings(tg_id)
api_key = await rq.get_bybit_api_key(tg_id) if result:
secret_key = await rq.get_bybit_secret_key(tg_id) logger.info(f"Ордер {result} успешно закрыт.")
symbol = await rq.get_symbol(tg_id) else:
margin_mode = data_main_stgs.get('margin_type', 'Isolated') await callback.message.answer(f"Не удалось закрыть ордер {result}.")
trading_mode = data_main_stgs.get('trading_mode')
# Проверка API ключей
if not api_key or not secret_key:
await message.answer("❗️ У вас не настроены API ключи для Bybit.")
await callback.answer() await callback.answer()
return
# Проверка режима торговли
if trading_mode not in ['Long', 'Short', 'Smart', 'Switch']: @router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_deal_by_timer:"))
await message.answer(f"❗️ Некорректный торговый режим: {trading_mode}") 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() await callback.answer()
return
# Проверка допустимости маржинального режима
if margin_mode not in ['Isolated', 'Cross']:
margin_mode = 'Isolated'
# Проверяем открытые позиции и маржинальный режим @router_functions_bybit_trade.message(CloseTradeTimerState.waiting_for_delay)
client = HTTP(api_key=api_key, api_secret=secret_key) async def process_close_delay(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод закрытия сделки с задержкой.
"""
try: try:
positions_resp = client.get_positions(category='linear', symbol=symbol) delay_minutes = int(message.text.strip())
positions = positions_resp.get('result', {}).get('list', []) if delay_minutes <= 0:
except Exception as e: await message.answer("Введите положительное число.")
logger.error(f"Ошибка при получении позиций: {e}") return
positions = [] except ValueError:
await message.answer("Некорректный ввод. Введите число в минутах.")
for pos in positions:
size = pos.get('size')
existing_margin_mode = pos.get('margin_mode')
if size and float(size) > 0 and existing_margin_mode and existing_margin_mode != margin_mode:
await callback.answer(
f"⚠️ Маржинальный режим нельзя менять при открытой позиции "
f"(текущий режим: {existing_margin_mode})", show_alert=True)
return return
# Определяем сторону для открытия позиции data = await state.get_data()
if trading_mode == 'Long': symbol = data.get("symbol")
side = 'Buy'
elif trading_mode == 'Short':
side = 'Sell'
else:
await message.answer(f"Режим торговли '{trading_mode}' пока не поддерживается.")
await callback.answer()
return
# Сообщаем о начале торговли delay = delay_minutes * 60
await message.answer("Начинаю торговлю с использованием текущих настроек...") await message.answer(f"Закрытие сделки {symbol} запланировано через {delay_minutes} мин.")
await close_trade_after_delay(message.from_user.id, message, symbol, delay)
# Открываем позицию (вызывает Futures.open_position) await state.clear()
success = await open_position(tg_id, message, side=side, margin_mode=margin_mode)
if not success:
await message.answer('⚠️ Ошибка при совершении сделки', reply_markup=inline_markup.back_to_main)
return
# Проверяем таймер и информируем пользователя @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)
timer_data = await rq.get_user_timer(tg_id)
timer_minutes = timer_data.get('timer') if isinstance(timer_data, dict) else timer_data @router_functions_bybit_trade.callback_query(F.data == "clb_stop_trading")
if timer_minutes and timer_minutes > 0: async def confirm_stop_trading(callback: CallbackQuery):
await message.answer(f"Торговля будет работать по таймеру: {timer_minutes} мин.") """
asyncio.create_task(trading_cycle(tg_id, message)) Предлагает пользователю выбрать вариант подтверждение остановки торговли.
else: """
await message.answer( await callback.message.answer(
"Торговля начата без ограничения по времени. Для остановки нажмите кнопку 'Закрыть сделку'.", "Выберите вариант остановки торговли:", reply_markup=inline_markup.stop_choice_markup
reply_markup=inline_markup.create_close_deal_markup(symbol)
) )
await callback.answer() 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") @router_functions_bybit_trade.callback_query(F.data == "clb_cancel")
async def cancel(callback: CallbackQuery, state: FSMContext): async def cancel(callback: CallbackQuery, state: FSMContext) -> None:
"""
Отменяет текущее состояние FSM и сообщает пользователю об отмене.
"""
await state.clear() await state.clear()
await callback.message.answer("Отменено!", reply_markup=inline_markup.back_to_main) await callback.message.answer("Отменено!", reply_markup=inline_markup.back_to_main)
await callback.answer() await callback.answer()

View File

@@ -28,7 +28,7 @@ async def get_min_qty(tg_id: int) -> float:
client = HTTP(api_key=api_key, api_secret=secret_key) client = HTTP(api_key=api_key, api_secret=secret_key)
price = await get_price(tg_id) price = await get_price(tg_id, symbol=symbol)
response = client.get_instruments_info(symbol=symbol, category='linear') response = client.get_instruments_info(symbol=symbol, category='linear')

View File

@@ -8,7 +8,7 @@ logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("price_symbol") logger = logging.getLogger("price_symbol")
async def get_price(tg_id: int) -> float: async def get_price(tg_id: int, symbol: str) -> float:
""" """
Асинхронно получает текущую цену символа пользователя на Bybit. Асинхронно получает текущую цену символа пользователя на Bybit.
@@ -17,7 +17,6 @@ async def get_price(tg_id: int) -> float:
""" """
api_key = await rq.get_bybit_api_key(tg_id) api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id) secret_key = await rq.get_bybit_secret_key(tg_id)
symbol = await rq.get_symbol(tg_id)
client = HTTP( client = HTTP(
api_key=api_key, api_key=api_key,

View File

@@ -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()

View File

@@ -1,28 +1,38 @@
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
start_markup = InlineKeyboardMarkup(inline_keyboard=[ start_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔥 Начать торговлю", callback_data="clb_start_chatbot_message")] [InlineKeyboardButton(text="🔥 Начать торговлю", callback_data="clb_start_chatbot_message")]
]) ])
settings_markup = InlineKeyboardMarkup(inline_keyboard=[ settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Настройки", callback_data='clb_settings_message')],
[InlineKeyboardButton(text="Запуск", callback_data='clb_start_trading')] [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')] 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=[ 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_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="Дополнительные параметры", callback_data='clb_change_additional_settings')],
[InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api_message')], [InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api_message')],
back_btn_to_main
back_btn_profile
]) ])
connect_bybit_api_markup = InlineKeyboardMarkup(inline_keyboard=[ connect_bybit_api_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api')] [InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api')]
]) ])
@@ -31,12 +41,14 @@ trading_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Настройки", callback_data='clb_settings_message')], [InlineKeyboardButton(text="Настройки", callback_data='clb_settings_message')],
[InlineKeyboardButton(text="Мои сделки", callback_data='clb_my_deals')], [InlineKeyboardButton(text="Мои сделки", callback_data='clb_my_deals')],
[InlineKeyboardButton(text="Указать торговую пару", callback_data='clb_update_trading_pair')], [InlineKeyboardButton(text="Указать торговую пару", callback_data='clb_update_trading_pair')],
[InlineKeyboardButton(text="Выбрать тип входа", callback_data='clb_update_entry_type')], [InlineKeyboardButton(text="Начать торговать", callback_data='clb_update_entry_type')],
[InlineKeyboardButton(text="Остановить торговлю", callback_data='clb_stop_trading')],
]) ])
start_trading_markup = InlineKeyboardMarkup(inline_keyboard=[ start_trading_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="На главную", callback_data='back_to_main')], [InlineKeyboardButton(text="На главную", callback_data='back_to_main')],
[InlineKeyboardButton(text="Начать торговлю", callback_data="clb_start_chatbot_trading")], [InlineKeyboardButton(text="Начать торговлю", callback_data="clb_start_chatbot_trading")],
[InlineKeyboardButton(text="Установить таймер", callback_data="clb_set_timer")],
]) ])
@@ -49,15 +61,11 @@ entry_order_type_markup = InlineKeyboardMarkup(
[ [
InlineKeyboardButton(text="Market (текущая цена)", callback_data="entry_order_type:Market"), InlineKeyboardButton(text="Market (текущая цена)", callback_data="entry_order_type:Market"),
InlineKeyboardButton(text="Limit (фиксированная цена)", callback_data="entry_order_type:Limit"), InlineKeyboardButton(text="Limit (фиксированная цена)", callback_data="entry_order_type:Limit"),
], ], back_btn_to_main
] ]
) )
back_btn_list_settings = [InlineKeyboardButton(text="Назад",
callback_data='clb_back_to_special_settings_message')] # Кнопка для возврата к списку каталога настроек
back_btn_list_settings_markup = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Назад",
callback_data='clb_back_to_special_settings_message')]]) # Клавиатура для возврата к списку каталога настроек
back_btn_to_main = [InlineKeyboardButton(text="На главную", callback_data='clb_back_to_main')]
back_to_main = InlineKeyboardMarkup(inline_keyboard=[ back_to_main = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="На главную", callback_data='back_to_main')], [InlineKeyboardButton(text="На главную", callback_data='back_to_main')],
@@ -65,13 +73,15 @@ back_to_main = InlineKeyboardMarkup(inline_keyboard=[
main_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ 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_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_starting_quantity')],
[InlineKeyboardButton(text='Коэффициент Мартингейла', callback_data='clb_change_martingale_factor'), [InlineKeyboardButton(text='Коэффициент Мартингейла', callback_data='clb_change_martingale_factor'),
InlineKeyboardButton(text='Максимальное кол-во ставок', callback_data='clb_change_maximum_quantity')], 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 back_btn_to_main
@@ -82,14 +92,14 @@ risk_management_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
InlineKeyboardButton(text='Изм. цены убытков', callback_data='clb_change_price_loss')], 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')], [InlineKeyboardButton(text='Учитывать комиссию биржи (Да/Нет)', callback_data='commission_fee')],
back_btn_list_settings, back_btn_list_settings,
back_btn_to_main back_btn_to_main
]) ])
condition_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ condition_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Триггер', callback_data='clb_change_trigger'), [InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_mode'),
InlineKeyboardButton(text='Таймер', callback_data='clb_change_timer')], InlineKeyboardButton(text='Таймер', callback_data='clb_change_timer')],
[InlineKeyboardButton(text='Фильтр волатильности', callback_data='clb_change_filter_volatility'), [InlineKeyboardButton(text='Фильтр волатильности', callback_data='clb_change_filter_volatility'),
@@ -116,9 +126,7 @@ additional_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
trading_mode_markup = InlineKeyboardMarkup(inline_keyboard=[ trading_mode_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Лонг", callback_data="trade_mode_long"), [InlineKeyboardButton(text="Лонг", callback_data="trade_mode_long"),
InlineKeyboardButton(text="Шорт", callback_data="trade_mode_short")], InlineKeyboardButton(text="Шорт", callback_data="trade_mode_short"),
[InlineKeyboardButton(text="Свитч", callback_data="trade_mode_switch"),
InlineKeyboardButton(text="Смарт", callback_data="trade_mode_smart")], InlineKeyboardButton(text="Смарт", callback_data="trade_mode_smart")],
back_btn_list_settings, back_btn_list_settings,
@@ -134,14 +142,16 @@ margin_type_markup = InlineKeyboardMarkup(inline_keyboard=[
]) ])
trigger_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE trigger_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE
[InlineKeyboardButton(text='Ручной', callback_data="clb_trigger_ruchnoy"), [InlineKeyboardButton(text='Ручной', callback_data="clb_trigger_manual")],
InlineKeyboardButton(text='TradingView', callback_data="clb_trigger_tradingview")], # [InlineKeyboardButton(text='TradingView', callback_data="clb_trigger_tradingview")],
[InlineKeyboardButton(text="Автоматический", callback_data="clb_trigger_auto")] [InlineKeyboardButton(text="Автоматический", callback_data="clb_trigger_auto")],
back_btn_list_settings,
back_btn_to_main
]) ])
buttons_yes_no_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE buttons_yes_no_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Да', callback_data="clb_yes"), [InlineKeyboardButton(text='Да', callback_data="clb_yes"),
InlineKeyboardButton(text='Нет', callback_data="clb_yes")] InlineKeyboardButton(text='Нет', callback_data="clb_no")],
]) ])
buttons_on_off_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE buttons_on_off_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE
@@ -149,26 +159,63 @@ buttons_on_off_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТ
InlineKeyboardButton(text='Выключить', callback_data="clb_off")] 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): def create_trades_inline_keyboard(trades):
buttons = [] builder = InlineKeyboardBuilder()
for trade in trades: for trade in trades:
symbol = trade['symbol'] if isinstance(trade, dict) else trade.symbol builder.button(text=trade, callback_data=f"show_deal_{trade}")
buttons.append([ builder.adjust(2)
InlineKeyboardButton(text=f"{symbol}", callback_data=f"show_deal_{symbol}") return builder.as_markup()
])
return InlineKeyboardMarkup(inline_keyboard=buttons) 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: def create_close_deal_markup(symbol: str) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[ return InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Закрыть сделку", callback_data=f"close_deal:{symbol}")], [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 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=[ timer_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Установить таймер", callback_data="clb_set_timer")], [InlineKeyboardButton(text="Установить таймер", callback_data="clb_set_timer")],
[InlineKeyboardButton(text="Остановить таймер", callback_data="clb_stop_timer")],
back_btn_to_main 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")],
])

View File

@@ -3,4 +3,4 @@
base_buttons_markup = ReplyKeyboardMarkup(keyboard=[ base_buttons_markup = ReplyKeyboardMarkup(keyboard=[
[KeyboardButton(text="👤 Профиль")], [KeyboardButton(text="👤 Профиль")],
# [KeyboardButton(text="Настройки")] # [KeyboardButton(text="Настройки")]
], resize_keyboard=True) ], resize_keyboard=True, one_time_keyboard=False)

View File

@@ -1,31 +1,55 @@
import logging from datetime import datetime
from datetime import datetime import logging.config
from sqlalchemy.sql.sqltypes import DateTime, Numeric
from sqlalchemy.sql.sqltypes import DateTime
logger = logging.getLogger(__name__)
from sqlalchemy import BigInteger, Boolean, Integer, String, ForeignKey from sqlalchemy import BigInteger, Boolean, Integer, String, ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy.ext.asyncio import AsyncAttrs, async_sessionmaker, create_async_engine from sqlalchemy.ext.asyncio import AsyncAttrs, async_sessionmaker, create_async_engine
from logger_helper.logger_helper import LOGGING_CONFIG
from sqlalchemy import select, insert from sqlalchemy import select, insert
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("models")
engine = create_async_engine(url='sqlite+aiosqlite:///db.sqlite3') engine = create_async_engine(url='sqlite+aiosqlite:///db.sqlite3')
async_session = async_sessionmaker(engine) async_session = async_sessionmaker(engine)
class Base(AsyncAttrs, DeclarativeBase): class Base(AsyncAttrs, DeclarativeBase):
"""Базовый класс для declarative моделей SQLAlchemy с поддержкой async."""
pass pass
class User_Telegram_Id(Base): class User_Telegram_Id(Base):
"""
Модель таблицы user_telegram_id.
Хранит идентификаторы Telegram пользователей.
Атрибуты:
id (int): Внутренний первичный ключ записи.
tg_id (int): Уникальный идентификатор пользователя Telegram.
"""
__tablename__ = 'user_telegram_id' __tablename__ = 'user_telegram_id'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(BigInteger) tg_id = mapped_column(BigInteger)
class User_Bybit_API(Base): 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' __tablename__ = 'user_bybit_api'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
@@ -35,7 +59,26 @@ class User_Bybit_API(Base):
api_key = mapped_column(String(18), default='None') api_key = mapped_column(String(18), default='None')
secret_key = mapped_column(String(36), default='None') secret_key = mapped_column(String(36), default='None')
class User_Symbol(Base): 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' __tablename__ = 'user_symbols'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
@@ -44,28 +87,68 @@ class User_Symbol(Base):
symbol = mapped_column(String(18), default='PENGUUSDT') symbol = mapped_column(String(18), default='PENGUUSDT')
class Trading_Mode(Base): class Trading_Mode(Base):
"""
Справочник доступных режимов торговли.
Атрибуты:
id (int): Первичный ключ.
mode (str): Уникальный режим (например, 'Long', 'Short').
"""
__tablename__ = 'trading_modes' __tablename__ = 'trading_modes'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
mode = mapped_column(String(10), unique=True) mode = mapped_column(String(10), unique=True)
class Margin_type(Base): class Margin_type(Base):
"""
Справочник типов маржинальной торговли.
Атрибуты:
id (int): Первичный ключ.
type (str): Тип маржи (например, 'Isolated', 'Cross').
"""
__tablename__ = 'margin_types' __tablename__ = 'margin_types'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
type = mapped_column(String(15), unique=True) type = mapped_column(String(15), unique=True)
class Trigger(Base): class Trigger(Base):
"""
Справочник триггеров для сделок.
Атрибуты:
id (int): Первичный ключ..
"""
__tablename__ = 'triggers' __tablename__ = 'triggers'
id: Mapped[int] = mapped_column(primary_key=True) 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): 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' __tablename__ = 'user_main_settings'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
@@ -74,16 +157,29 @@ class User_Main_Settings(Base):
trading_mode = mapped_column(ForeignKey("trading_modes.mode")) trading_mode = mapped_column(ForeignKey("trading_modes.mode"))
margin_type = mapped_column(ForeignKey("margin_types.type")) 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) size_leverage = mapped_column(Integer(), default=1)
starting_quantity = mapped_column(Integer(), default=1) starting_quantity = mapped_column(Integer(), default=1)
martingale_factor = mapped_column(Integer(), default=1) martingale_factor = mapped_column(Integer(), default=1)
martingale_step = mapped_column(Integer(), default=1) martingale_step = mapped_column(Integer(), default=0)
maximal_quantity = mapped_column(Integer(), default=10) maximal_quantity = mapped_column(Integer(), default=10)
entry_order_type = mapped_column(String(10), default='Market') entry_order_type = mapped_column(String(10), default='Market')
limit_order_price = mapped_column(String(20), nullable=True) limit_order_price = mapped_column(Numeric(18, 15), nullable=True)
class User_Risk_Management_Settings(Base): 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' __tablename__ = 'user_risk_management_settings'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
@@ -93,16 +189,31 @@ class User_Risk_Management_Settings(Base):
price_profit = mapped_column(Integer(), default=1) price_profit = mapped_column(Integer(), default=1)
price_loss = mapped_column(Integer(), default=1) price_loss = mapped_column(Integer(), default=1)
max_risk_deal = mapped_column(Integer(), default=100) max_risk_deal = mapped_column(Integer(), default=100)
commission_fee = mapped_column(Integer(), default=0) commission_fee = mapped_column(String(), default="Да")
class User_Condition_Settings(Base): 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' __tablename__ = 'user_condition_settings'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) 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_time = mapped_column(String(25), default='???')
filter_volatility = mapped_column(Boolean, default=False) filter_volatility = mapped_column(Boolean, default=False)
external_cues = mapped_column(Boolean, default=False) external_cues = mapped_column(Boolean, default=False)
@@ -110,7 +221,18 @@ class User_Condition_Settings(Base):
webhook = mapped_column(String(40), default='') webhook = mapped_column(String(40), default='')
ai_analytics = mapped_column(Boolean, default=False) ai_analytics = mapped_column(Boolean, default=False)
class User_Additional_Settings(Base): class User_Additional_Settings(Base):
"""
Прочие дополнительные настройки пользователя.
Атрибуты:
id (int): Первичный ключ.
tg_id (int): Внешний ключ на Telegram пользователя.
pattern_save (bool): Сохранять ли шаблоны.
autostart (bool): Автоматический запуск.
notifications (bool): Получение уведомлений.
"""
__tablename__ = 'user_additional_settings' __tablename__ = 'user_additional_settings'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
@@ -121,34 +243,19 @@ class User_Additional_Settings(Base):
autostart = mapped_column(Boolean, default=False) autostart = mapped_column(Boolean, default=False)
notifications = mapped_column(Boolean, default=False) notifications = mapped_column(Boolean, default=False)
async def async_main():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
# Заполнение таблиц
modes = ['Long', 'Short', 'Switch', 'Smart']
for mode in modes:
result = await conn.execute(select(Trading_Mode).where(Trading_Mode.mode == mode))
if not result.first():
logger.info("Заполение таблицы режима торговли")
await conn.execute(Trading_Mode.__table__.insert().values(mode=mode))
types = ['Isolated', 'Cross']
for type in types:
result = await conn.execute(select(Margin_type).where(Margin_type.type == type))
if not result.first():
logger.info("Заполение таблицы типов маржи")
await conn.execute(Margin_type.__table__.insert().values(type=type))
triggers = ['Ручной', 'Автоматический', 'TradingView']
for trigger in triggers:
result = await conn.execute(select(Trigger).where(Trigger.trigger == trigger))
if not result.first():
logger.info("Заполение таблицы триггеров")
await conn.execute(Trigger.__table__.insert().values(trigger=trigger))
class USER_DEALS(Base): 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' __tablename__ = 'user_deals'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
@@ -162,6 +269,16 @@ class USER_DEALS(Base):
class UserTimer(Base): class UserTimer(Base):
"""
Таймер пользователя для отсроченного запуска сделок.
Атрибуты:
id (int): Первичный ключ.
tg_id (int): Внешний ключ на Telegram пользователя.
timer_minutes (int): Количество минут таймера.
timer_start (datetime): Время начала таймера.
timer_end (Optional[datetime]): Время окончания таймера (если установлено).
"""
__tablename__ = 'user_timers' __tablename__ = 'user_timers'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
@@ -169,3 +286,26 @@ class UserTimer(Base):
timer_minutes = mapped_column(Integer, nullable=False, default=0) timer_minutes = mapped_column(Integer, nullable=False, default=0)
timer_start = mapped_column(DateTime, default=datetime.utcnow) timer_start = mapped_column(DateTime, default=datetime.utcnow)
timer_end = mapped_column(DateTime, nullable=True) 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', 'Smart']
for mode in modes:
result = await conn.execute(select(Trading_Mode).where(Trading_Mode.mode == mode))
if not result.first():
logger.info("Заполение таблицы режима торговли")
await conn.execute(Trading_Mode.__table__.insert().values(mode=mode))
types = ['Isolated', 'Cross']
for type in types:
result = await conn.execute(select(Margin_type).where(Margin_type.type == type))
if not result.first():
logger.info("Заполение таблицы типов маржи")
await conn.execute(Margin_type.__table__.insert().values(type=type))

View File

@@ -1,31 +1,40 @@
import logging.config import logging.config
from logger_helper.logger_helper import LOGGING_CONFIG from logger_helper.logger_helper import LOGGING_CONFIG
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any from typing import Any
from app.telegram.database.models import (
async_session,
User_Telegram_Id as UTi,
User_Main_Settings as UMS,
User_Bybit_API as UBA,
User_Symbol,
User_Risk_Management_Settings as URMS,
User_Condition_Settings as UCS,
User_Additional_Settings as UAS,
Trading_Mode,
Margin_type,
Trigger,
USER_DEALS,
UserTimer,
)
from sqlalchemy import select, update
logging.config.dictConfig(LOGGING_CONFIG) logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("requests") logger = logging.getLogger("requests")
from app.telegram.database.models import async_session
from app.telegram.database.models import User_Telegram_Id as UTi
from app.telegram.database.models import User_Main_Settings as UMS
from app.telegram.database.models import User_Bybit_API as UBA
from app.telegram.database.models import User_Symbol
from app.telegram.database.models import User_Risk_Management_Settings as URMS
from app.telegram.database.models import User_Condition_Settings as UCS
from app.telegram.database.models import User_Additional_Settings as UAS
from app.telegram.database.models import Trading_Mode
from app.telegram.database.models import Margin_type
from app.telegram.database.models import Trigger
from app.telegram.database.models import USER_DEALS, UserTimer
import app.telegram.functions.functions as func # functions # --- Функции сохранения в БД ---
from sqlalchemy import select, delete, update async def save_tg_id_new_user(tg_id) -> None:
"""
Сохраняет Telegram ID нового пользователя в базу, если такого ещё нет.
Args:
# SET_DB tg_id (int): Telegram ID пользователя.
async def save_tg_id_new_user(tg_id): """
async with async_session() as session: async with async_session() as session:
user = await session.scalar(select(UTi).where(UTi.tg_id == tg_id)) user = await session.scalar(select(UTi).where(UTi.tg_id == tg_id))
@@ -37,26 +46,33 @@ async def save_tg_id_new_user(tg_id):
await session.commit() 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: async with async_session() as session:
user = await session.scalar(select(UBA).where(UBA.tg_id == tg_id)) user = await session.scalar(select(UBA).where(UBA.tg_id == tg_id))
if not user: if not user:
session.add(UBA( session.add(UBA(tg_id=tg_id))
tg_id=tg_id,
))
await session.commit() 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: async with async_session() as session:
user = await session.scalar(select(User_Symbol).where(User_Symbol.tg_id == tg_id)) user = await session.scalar(select(User_Symbol).where(User_Symbol.tg_id == tg_id))
if not user: if not user:
session.add(User_Symbol( session.add(User_Symbol(tg_id=tg_id))
tg_id=tg_id
))
logger.info(f"Symbol был успешно добавлен %s", tg_id) logger.info(f"Symbol был успешно добавлен %s", tg_id)
@@ -64,6 +80,14 @@ async def set_new_user_symbol(tg_id):
async def set_new_user_default_main_settings(tg_id, trading_mode, margin_type) -> None: 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: async with async_session() as session:
settings = await session.scalar(select(UMS).where(UMS.tg_id == tg_id)) settings = await session.scalar(select(UMS).where(UMS.tg_id == tg_id))
@@ -80,6 +104,12 @@ async def set_new_user_default_main_settings(tg_id, trading_mode, margin_type) -
async def set_new_user_default_risk_management_settings(tg_id) -> None: async def set_new_user_default_risk_management_settings(tg_id) -> None:
"""
Создаёт настройки риск-менеджмента по умолчанию.
Args:
tg_id (int): Telegram ID пользователя.
"""
async with async_session() as session: async with async_session() as session:
settings = await session.scalar(select(URMS).where(URMS.tg_id == tg_id)) settings = await session.scalar(select(URMS).where(URMS.tg_id == tg_id))
@@ -94,6 +124,13 @@ async def set_new_user_default_risk_management_settings(tg_id) -> None:
async def set_new_user_default_condition_settings(tg_id, trigger) -> None: 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: async with async_session() as session:
settings = await session.scalar(select(UCS).where(UCS.tg_id == tg_id)) settings = await session.scalar(select(UCS).where(UCS.tg_id == tg_id))
@@ -109,6 +146,12 @@ async def set_new_user_default_condition_settings(tg_id, trigger) -> None:
async def set_new_user_default_additional_settings(tg_id) -> None: async def set_new_user_default_additional_settings(tg_id) -> None:
"""
Создаёт дополнительные настройки по умолчанию.
Args:
tg_id (int): Telegram ID пользователя.
"""
async with async_session() as session: async with async_session() as session:
settings = await session.scalar(select(UAS).where(UAS.tg_id == tg_id)) settings = await session.scalar(select(UAS).where(UAS.tg_id == tg_id))
@@ -122,32 +165,46 @@ async def set_new_user_default_additional_settings(tg_id) -> None:
await session.commit() await session.commit()
# GET_DB # --- Функции получения данных из БД ---
async def check_user(tg_id): async def check_user(tg_id):
"""
Проверяет наличие пользователя в базе.
Args:
tg_id (int): Telegram ID пользователя.
Returns:
Optional[UTi]: Пользователь или None.
"""
async with async_session() as session: async with async_session() as session:
user = await session.scalar(select(UTi).where(UTi.tg_id == tg_id)) user = await session.scalar(select(UTi).where(UTi.tg_id == tg_id))
return user return user
async def get_bybit_api_key(tg_id): async def get_bybit_api_key(tg_id):
"""Получить API ключ Bybit пользователя."""
async with async_session() as session: async with async_session() as session:
api_key = await session.scalar(select(UBA.api_key).where(UBA.tg_id == tg_id)) api_key = await session.scalar(select(UBA.api_key).where(UBA.tg_id == tg_id))
return api_key return api_key
async def get_bybit_secret_key(tg_id): async def get_bybit_secret_key(tg_id):
"""Получить секретный ключ Bybit пользователя."""
async with async_session() as session: async with async_session() as session:
secret_key = await session.scalar(select(UBA.secret_key).where(UBA.tg_id == tg_id)) secret_key = await session.scalar(select(UBA.secret_key).where(UBA.tg_id == tg_id))
return secret_key return secret_key
async def get_symbol(tg_id): async def get_symbol(tg_id):
"""Получить символ пользователя."""
async with async_session() as session: async with async_session() as session:
symbol = await session.scalar(select(User_Symbol.symbol).where(User_Symbol.tg_id == tg_id)) symbol = await session.scalar(select(User_Symbol.symbol).where(User_Symbol.tg_id == tg_id))
return symbol return symbol
async def get_user_trades(tg_id): async def get_user_trades(tg_id):
"""Получить сделки пользователя."""
async with async_session() as session: async with async_session() as session:
query = select(USER_DEALS.symbol, USER_DEALS.side).where(USER_DEALS.tg_id == tg_id) query = select(USER_DEALS.symbol, USER_DEALS.side).where(USER_DEALS.tg_id == tg_id)
result = await session.execute(query) result = await session.execute(query)
@@ -155,14 +212,63 @@ async def get_user_trades(tg_id):
return trades return trades
async def get_entry_order_type(tg_id: object) -> str | None | Any:
"""Получить тип входного ордера пользователя."""
async with async_session() as session:
order_type = await session.scalar(
select(UMS.entry_order_type).where(UMS.tg_id == tg_id)
)
# Если в базе не установлен тип — возвращаем значение по умолчанию
return order_type or 'Market'
# --- Функции обновления данных ---
async def update_user_trades(tg_id, **kwargs): async def update_user_trades(tg_id, **kwargs):
"""Обновить сделки пользователя."""
async with async_session() as session: async with async_session() as session:
query = update(USER_DEALS).where(USER_DEALS.tg_id == tg_id).values(**kwargs) query = update(USER_DEALS).where(USER_DEALS.tg_id == tg_id).values(**kwargs)
await session.execute(query) await session.execute(query)
await session.commit() await session.commit()
async def update_symbol(tg_id: int, symbol: str) -> None:
"""Обновить торговый символ пользователя."""
async with async_session() as session:
await session.execute(update(User_Symbol).where(User_Symbol.tg_id == tg_id).values(symbol=symbol))
await session.commit()
async def update_api_key(tg_id: int, api: str) -> None:
"""Обновить API ключ пользователя."""
async with async_session() as session:
await session.execute(update(UBA).where(UBA.tg_id == tg_id).values(api_key=api))
await session.commit()
async def update_secret_key(tg_id: int, api: str) -> None:
"""Обновить секретный ключ пользователя."""
async with async_session() as session:
await session.execute(update(UBA).where(UBA.tg_id == tg_id).values(secret_key=api))
await session.commit()
# --- Более мелкие обновления и запросы по настройкам ---
async def update_trade_mode_user(tg_id, trading_mode) -> None:
"""Обновить режим торговли пользователя."""
async with async_session() as session:
mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.mode == trading_mode))
if mode:
logger.info("Изменён торговый режим для пользователя %s", tg_id)
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(trading_mode=mode))
await session.commit()
async def delete_user_trade(tg_id: int, symbol: str): async def delete_user_trade(tg_id: int, symbol: str):
"""Удалить сделку пользователя."""
async with async_session() as session: async with async_session() as session:
await session.execute( await session.execute(
USER_DEALS.__table__.delete().where( USER_DEALS.__table__.delete().where(
@@ -173,24 +279,28 @@ async def delete_user_trade(tg_id: int, symbol: str):
async def get_for_registration_trading_mode(): async def get_for_registration_trading_mode():
"""Получить режим торговли по умолчанию."""
async with async_session() as session: async with async_session() as session:
mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.id == 1)) mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.id == 1))
return mode return mode
async def get_for_registration_margin_type(): async def get_for_registration_margin_type():
"""Получить тип маржи по умолчанию."""
async with async_session() as session: async with async_session() as session:
type = await session.scalar(select(Margin_type.type).where(Margin_type.id == 1)) type = await session.scalar(select(Margin_type.type).where(Margin_type.id == 1))
return type return type
async def get_for_registration_trigger(): async def get_for_registration_trigger(tg_id):
"""Получить триггер по умолчанию."""
async with async_session() as session: async with async_session() as session:
trigger = await session.scalar(select(Trigger.trigger).where(Trigger.id == 1)) trigger = await session.scalar(select(UCS.trigger).where(tg_id == tg_id))
return trigger return trigger
async def get_user_main_settings(tg_id): async def get_user_main_settings(tg_id):
"""Получить основные настройки пользователя."""
async with async_session() as session: async with async_session() as session:
user = await session.scalar(select(UMS).where(UMS.tg_id == tg_id)) user = await session.scalar(select(UMS).where(UMS.tg_id == tg_id))
@@ -199,24 +309,35 @@ async def get_user_main_settings(tg_id):
trading_mode = await session.scalar(select(UMS.trading_mode).where(UMS.tg_id == 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)) 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)) 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)) 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)) 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)) 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 = { data = {
'trading_mode': trading_mode, 'trading_mode': trading_mode,
'margin_type': margin_mode, 'margin_type': margin_mode,
'switch_mode_enabled': switch_mode_enabled,
'switch_state': switch_state,
'size_leverage': size_leverage, 'size_leverage': size_leverage,
'starting_quantity': starting_quantity, 'starting_quantity': starting_quantity,
'martingale_factor': martingale_factor, 'martingale_factor': martingale_factor,
'maximal_quantity': maximal_quantity 'maximal_quantity': maximal_quantity,
'entry_order_type': entry_order_type,
'limit_order_price': limit_order_price,
'martingale_step': martingale_step,
} }
return data return data
async def get_user_risk_management_settings(tg_id): async def get_user_risk_management_settings(tg_id):
"""Получить риск-менеджмента настройки пользователя."""
async with async_session() as session: async with async_session() as session:
user = await session.scalar(select(URMS).where(URMS.tg_id == tg_id)) user = await session.scalar(select(URMS).where(URMS.tg_id == tg_id))
@@ -238,41 +359,8 @@ async def get_user_risk_management_settings(tg_id):
return data return data
# UPDATE_SYMBOL
async def update_symbol(tg_id, symbol) -> None:
async with async_session() as session:
await session.execute(update(User_Symbol).where(User_Symbol.tg_id == tg_id).values(symbol=symbol))
await session.commit()
async def update_api_key(tg_id, api):
async with async_session() as session:
api_key = await session.execute(update(UBA).where(UBA.tg_id == tg_id).values(api_key=api))
await session.commit()
async def update_secret_key(tg_id, api):
async with async_session() as session:
secret_key = await session.execute(update(UBA).where(UBA.tg_id == tg_id).values(secret_key=api))
await session.commit()
# UPDATE_MAIN_SETTINGS_DB
async def update_trade_mode_user(tg_id, trading_mode) -> None:
async with async_session() as session:
mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.mode == trading_mode))
if mode:
logger.info("Изменен трейд мод %s", tg_id)
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(trading_mode=mode))
await session.commit()
async def update_margin_type(tg_id, margin_type) -> None: async def update_margin_type(tg_id, margin_type) -> None:
"""Обновить тип маржи пользователя."""
async with async_session() as session: async with async_session() as session:
type = await session.scalar(select(Margin_type.type).where(Margin_type.type == margin_type)) type = await session.scalar(select(Margin_type.type).where(Margin_type.type == margin_type))
@@ -284,6 +372,7 @@ async def update_margin_type(tg_id, margin_type) -> None:
async def update_size_leverange(tg_id, num): async def update_size_leverange(tg_id, num):
"""Обновить размер левеража пользователя."""
async with async_session() as session: 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))
@@ -291,6 +380,7 @@ async def update_size_leverange(tg_id, num):
async def update_starting_quantity(tg_id, num): async def update_starting_quantity(tg_id, num):
"""Обновить размер левеража пользователя."""
async with async_session() as session: 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))
@@ -298,6 +388,7 @@ async def update_starting_quantity(tg_id, num):
async def update_martingale_factor(tg_id, num): async def update_martingale_factor(tg_id, num):
"""Обновить размер левеража пользователя."""
async with async_session() as session: 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))
@@ -305,15 +396,17 @@ async def update_martingale_factor(tg_id, num):
async def update_maximal_quantity(tg_id, num): async def update_maximal_quantity(tg_id, num):
"""Обновить размер левеража пользователя."""
async with async_session() as session: 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() await session.commit()
# UPDATE_RISK_MANAGEMENT_SETTINGS_DB # ОБНОВЛЕНИЕ НАСТРОЕК РИСК-МЕНЕДЖМЕНТА
async def update_price_profit(tg_id, num): async def update_price_profit(tg_id, num):
"""Обновить цену тейк-профита (прибыль) пользователя."""
async with async_session() as session: 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))
@@ -321,6 +414,7 @@ async def update_price_profit(tg_id, num):
async def update_price_loss(tg_id, num): async def update_price_loss(tg_id, num):
"""Обновить цену тейк-лосса (убыток) пользователя."""
async with async_session() as session: 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))
@@ -328,6 +422,7 @@ async def update_price_loss(tg_id, num):
async def update_max_risk_deal(tg_id, num): async def update_max_risk_deal(tg_id, num):
"""Обновить максимальную сумму риска пользователя."""
async with async_session() as session: 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))
@@ -335,6 +430,7 @@ async def update_max_risk_deal(tg_id, num):
async def update_entry_order_type(tg_id, order_type): async def update_entry_order_type(tg_id, order_type):
"""Обновить тип входного ордера пользователя."""
async with async_session() as session: async with async_session() as session:
await session.execute( await session.execute(
update(UMS) update(UMS)
@@ -344,16 +440,8 @@ async def update_entry_order_type(tg_id, order_type):
await session.commit() await session.commit()
async def get_entry_order_type(tg_id: object) -> str | None | Any:
async with async_session() as session:
order_type = await session.scalar(
select(UMS.entry_order_type).where(UMS.tg_id == tg_id)
)
# Если в базе не установлен тип — возвращаем значение по умолчанию
return order_type or 'Market'
async def get_limit_price(tg_id): async def get_limit_price(tg_id):
"""Получить лимитную цену пользователя как float, либо None."""
async with async_session() as session: async with async_session() as session:
result = await session.execute( result = await session.execute(
select(UMS.limit_order_price) select(UMS.limit_order_price)
@@ -369,6 +457,7 @@ async def get_limit_price(tg_id):
async def update_limit_price(tg_id, price): async def update_limit_price(tg_id, price):
"""Обновить лимитную цену пользователя."""
async with async_session() as session: async with async_session() as session:
await session.execute( await session.execute(
update(UMS) update(UMS)
@@ -379,6 +468,7 @@ async def update_limit_price(tg_id, price):
async def update_commission_fee(tg_id, num): async def update_commission_fee(tg_id, num):
"""Обновить комиссию пользователя."""
async with async_session() as session: async with async_session() as session:
await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(commission_fee=num)) await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(commission_fee=num))
@@ -386,6 +476,7 @@ async def update_commission_fee(tg_id, num):
async def get_user_timer(tg_id): async def get_user_timer(tg_id):
"""Получить данные о таймере пользователя."""
async with async_session() as session: async with async_session() as session:
result = await session.execute(select(UserTimer).where(UserTimer.tg_id == tg_id)) result = await session.execute(select(UserTimer).where(UserTimer.tg_id == tg_id))
user_timer = result.scalars().first() user_timer = result.scalars().first()
@@ -416,9 +507,9 @@ async def get_user_timer(tg_id):
async def update_user_timer(tg_id, minutes: int): async def update_user_timer(tg_id, minutes: int):
"""Обновить данные о таймере пользователя."""
async with async_session() as session: async with async_session() as session:
try: try:
async with async_session() as session:
timer_start = None timer_start = None
timer_end = None timer_end = None
@@ -448,6 +539,7 @@ async def update_user_timer(tg_id, minutes: int):
async def get_martingale_step(tg_id): async def get_martingale_step(tg_id):
"""Получить шаг мартингейла пользователя."""
async with async_session() as session: async with async_session() as session:
result = await session.execute(select(UMS).where(UMS.tg_id == tg_id)) result = await session.execute(select(UMS).where(UMS.tg_id == tg_id))
user_settings = result.scalars().first() user_settings = result.scalars().first()
@@ -455,7 +547,32 @@ async def get_martingale_step(tg_id):
async def update_martingale_step(tg_id, step): async def update_martingale_step(tg_id, step):
"""Обновить шаг мартингейла пользователя."""
async with async_session() as session: async with async_session() as session:
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(martingale_step=step)) await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(martingale_step=step))
await session.commit() 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()

View File

@@ -7,7 +7,7 @@ async def reg_new_user_default_additional_settings(id, message):
await rq.set_new_user_default_additional_settings(tg_id) 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 = '''<b>Дополнительные параметры</b> text = '''<b>Дополнительные параметры</b>
<b>- Сохранить как шаблон стратегии:</b> да / нет <b>- Сохранить как шаблон стратегии:</b> да / нет

View File

@@ -1,34 +1,34 @@
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 import Router, F
from aiogram.types import Message, CallbackQuery from aiogram.types import Message, CallbackQuery
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
import app.telegram.database.requests as rq import app.telegram.database.requests as rq
from aiogram.fsm.state import State, StatesGroup from app.states.States import condition_settings
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("condition_settings")
condition_settings_router = Router() condition_settings_router = Router()
class condition_settings(StatesGroup): async def reg_new_user_default_condition_settings(id):
trigger = State()
timer = State()
volatilty = State()
volume = State()
integration = State()
use_tv_signal = State()
async def reg_new_user_default_condition_settings(id, message):
tg_id = id 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) await rq.set_new_user_default_condition_settings(tg_id, trigger)
async def main_settings_message(id, message, state): async def main_settings_message(id, message):
text = """ <b>Условия запуска</b>
<b>- Триггер:</b> Ручной запуск / Сигнал TradingView / Полностью автоматический tg_id = id
trigger = await rq.get_for_registration_trigger(tg_id)
text = f""" <b>Условия запуска</b>
<b>- Режим торговли:</b> {trigger}
<b>- Таймер: </b> установить таймер / остановить таймер <b>- Таймер: </b> установить таймер / остановить таймер
<b>- Фильтр волатильности / объёма: </b> включить/отключить <b>- Фильтр волатильности / объёма: </b> включить/отключить
<b>- Интеграции и внешние сигналы: </b> <b>- Интеграции и внешние сигналы: </b>
@@ -39,15 +39,32 @@ async def main_settings_message(id, message, state):
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.condition_settings_markup) await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.condition_settings_markup)
async def trigger_message(message, state): async def trigger_message(id, message, state: FSMContext):
text = '''Триггер await state.set_state(condition_settings.trigger)
text = '''
Описание ручного запуска, сигналов, автоматического режима ''' <b>- Автоматический:</b> торговля будет продолжаться до условии остановки.
<b>- Ручной:</b> торговля будет происходить только в ручном режиме.
<em>- Выберите тип триггера:</em>'''
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trigger_markup) await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trigger_markup)
async def timer_message(id,message: Message, state: FSMContext): @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) await state.set_state(condition_settings.timer)
timer_info = await rq.get_user_timer(id) timer_info = await rq.get_user_timer(id)
@@ -56,8 +73,7 @@ async def timer_message(id,message: Message, state: FSMContext):
return return
await message.answer( await message.answer(
f"Таймер: {timer_info['timer_minutes']} мин\n" f"Таймер: {timer_info['timer_minutes']} мин\n",
f"Осталось: {timer_info['remaining_minutes']} мин\n",
reply_markup=inline_markup.timer_markup reply_markup=inline_markup.timer_markup
) )
@@ -77,21 +93,15 @@ async def process_timer_input(message: Message, state: FSMContext):
await message.reply("Введите число больше нуля.") await message.reply("Введите число больше нуля.")
return return
# Сохраняем в базу или память время таймера для пользователя
await rq.update_user_timer(message.from_user.id, minutes) await rq.update_user_timer(message.from_user.id, minutes)
await message.answer(f"Таймер установлен на {minutes} минут.\nНажмите кнопку 'Начать торговлю' для запуска.",
reply_markup=inline_markup.start_trading_markup)
await message.answer(f"Таймер установлен на {minutes} минут.", reply_markup=inline_markup.back_to_main)
await state.clear() await state.clear()
except ValueError: except ValueError:
await message.reply("Пожалуйста, введите корректное число.") await message.reply("Пожалуйста, введите корректное число.")
@condition_settings_router.callback_query(F.data == "clb_stop_timer")
async def stop_timer_callback(callback: CallbackQuery):
await rq.update_user_timer(callback.from_user.id, 0) # обнуляем таймер
await callback.message.answer("Таймер остановлен.", reply_markup=inline_markup.back_to_main)
await callback.answer()
async def filter_volatility_message(message, state): async def filter_volatility_message(message, state):
text = '''Фильтр волатильности text = '''Фильтр волатильности

View File

@@ -10,10 +10,9 @@ async def start_message(message):
username = message.from_user.first_name username = message.from_user.first_name
else: else:
username = f'{message.from_user.first_name} {message.from_user.last_name}' username = f'{message.from_user.first_name} {message.from_user.last_name}'
await message.answer(f""" Привет <b>{username}</b>! 👋 await message.answer(f""" Привет <b>{username}</b>! 👋""", parse_mode='html')
await message.answer("Добро пожаловать в чат-робот для автоматизации трейдинга — вашего надежного помощника для анализа рынка и принятия взвешенных решений.",
Добро пожаловать в чат-робот для автоматизации трейдинга — вашего надежного помощника для анализа рынка и принятия взвешенных решений. parse_mode='html', reply_markup=inline_markup.start_markup)
""", parse_mode='html', reply_markup=inline_markup.start_markup)
async def profile_message(username, message): async def profile_message(username, message):
await message.answer(f""" <b>@{username}</b> await message.answer(f""" <b>@{username}</b>

View File

@@ -1,23 +1,18 @@
from aiogram import Router from aiogram import Router
import logging.config
import app.telegram.Keyboards.inline_keyboards as inline_markup 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 import app.telegram.database.requests as rq
from aiogram.types import Message, CallbackQuery from aiogram.types import Message, CallbackQuery
from app.states.States import update_main_settings
from logger_helper.logger_helper import LOGGING_CONFIG
# FSM - Механизм состояния logging.config.dictConfig(LOGGING_CONFIG)
from aiogram.fsm.state import State, StatesGroup logger = logging.getLogger("main_settings")
router_main_settings = Router() 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): async def reg_new_user_default_main_settings(id, message):
tg_id = id tg_id = id
@@ -28,19 +23,23 @@ async def reg_new_user_default_main_settings(id, message):
await rq.set_new_user_default_main_settings(tg_id, trading_mode, margin_type) await rq.set_new_user_default_main_settings(tg_id, trading_mode, margin_type)
async def main_settings_message(id, message, state): async def main_settings_message(id, message):
data = await rq.get_user_main_settings(id) data = await rq.get_user_main_settings(id)
await message.answer(f"""<b>Основные настройки</b> await message.answer(f"""<b>Основные настройки</b>
<b>- Режим торговли:</b> {data['trading_mode']} <b>- Режим торговли:</b> {data['trading_mode']}
<b>- Режим свитч:</b> {data['switch_mode_enabled']}
<b>- Состояние свитча:</b> {data['switch_state']}
<b>- Тип маржи:</b> {data['margin_type']} <b>- Тип маржи:</b> {data['margin_type']}
<b>- Размер кредитного плеча:</b> х{data['size_leverage']} <b>- Размер кредитного плеча:</b> х{data['size_leverage']}
<b>- Начальная ставка:</b> {data['starting_quantity']} <b>- Начальная ставка:</b> {data['starting_quantity']}
<b>- Коэффициент мартингейла:</b> {data['martingale_factor']} <b>- Коэффициент мартингейла:</b> {data['martingale_factor']}
<b>- Количество ставок в серии:</b> {data['martingale_step']}
<b>- Максимальное количество ставок в серии:</b> {data['maximal_quantity']} <b>- Максимальное количество ставок в серии:</b> {data['maximal_quantity']}
""", parse_mode='html', reply_markup=inline_markup.main_settings_markup) """, parse_mode='html', reply_markup=inline_markup.main_settings_markup)
async def trading_mode_message(message, state): async def trading_mode_message(message, state):
await state.set_state(update_main_settings.trading_mode) await state.set_state(update_main_settings.trading_mode)
@@ -52,11 +51,10 @@ async def trading_mode_message(message, state):
<b>Смарт</b> — автоматизированный режим, который подбирает оптимальную стратегию в зависимости от текущих рыночных условий. <b>Смарт</b> — автоматизированный режим, который подбирает оптимальную стратегию в зависимости от текущих рыночных условий.
<b>Свитч</b> — динамическое переключение между торговыми режимами для максимизации эффективности.
<em>Выберите ниже для изменений:</em> <em>Выберите ниже для изменений:</em>
""", parse_mode='html', reply_markup=inline_markup.trading_mode_markup) """, parse_mode='html', reply_markup=inline_markup.trading_mode_markup)
@router_main_settings.callback_query(update_main_settings.trading_mode) @router_main_settings.callback_query(update_main_settings.trading_mode)
async def state_trading_mode(callback: CallbackQuery, state): async def state_trading_mode(callback: CallbackQuery, state):
await callback.answer() await callback.answer()
@@ -69,38 +67,84 @@ async def state_trading_mode(callback: CallbackQuery, state):
case 'trade_mode_long': case 'trade_mode_long':
await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Long") await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Long")
await rq.update_trade_mode_user(id, '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() await state.clear()
case 'trade_mode_short': case 'trade_mode_short':
await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Short") await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Short")
await rq.update_trade_mode_user(id, '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() 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 callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Smart")
await rq.update_trade_mode_user(id, '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() await state.clear()
except Exception as e: except Exception as e:
print(f"error: {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(
"""<b>Свитч</b> — динамическое переключение между торговыми режимами для максимизации эффективности.
<em>Выберите ниже для изменений:</em>""", 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 state.set_state(update_main_settings.size_leverage)
await message.edit_text("Введите размер <b>кредитного плеча</b> (от 1 до 100): ", parse_mode='html', reply_markup=inline_markup.back_btn_list_settings_markup) await message.edit_text("Введите размер <b>кредитного плеча</b> (от 1 до 100): ", parse_mode='html',
reply_markup=inline_markup.back_btn_list_settings_markup)
@router_main_settings.message(update_main_settings.size_leverage) @router_main_settings.message(update_main_settings.size_leverage)
async def state_size_leverage(message: Message, state): 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 = await state.get_data()
data_settings = await rq.get_user_main_settings(message.from_user.id) 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 message.answer(f"✅ Изменено: {data_settings['size_leverage']}{data['size_leverage']}")
await rq.update_size_leverange(message.from_user.id, 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() await state.clear()
else: 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): async def martingale_factor_message(message, state):
await state.set_state(update_main_settings.martingale_factor) await state.set_state(update_main_settings.martingale_factor)
await message.edit_text("Введите <b>коэффициент Мартингейла:</b>", parse_mode='html', reply_markup=inline_markup.back_btn_list_settings_markup) await message.edit_text("Введите <b>коэффициент Мартингейла:</b>", parse_mode='html',
reply_markup=inline_markup.back_btn_list_settings_markup)
@router_main_settings.message(update_main_settings.martingale_factor) @router_main_settings.message(update_main_settings.martingale_factor)
async def state_martingale_factor(message: Message, state): 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 = await state.get_data()
data_settings = await rq.get_user_main_settings(message.from_user.id) data_settings = await rq.get_user_main_settings(message.from_user.id)
@@ -133,13 +181,15 @@ async def state_martingale_factor(message: Message, state):
await message.answer(f"✅ Изменено: {data_settings['martingale_factor']}{data['martingale_factor']}") await message.answer(f"✅ Изменено: {data_settings['martingale_factor']}{data['martingale_factor']}")
await rq.update_martingale_factor(message.from_user.id, 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() await state.clear()
else: 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): async def margin_type_message(message, state):
await state.set_state(update_main_settings.margin_type) await state.set_state(update_main_settings.margin_type)
@@ -160,40 +210,60 @@ async def margin_type_message(message, state):
<em>Выберите ниже для изменений:</em> <em>Выберите ниже для изменений:</em>
""", parse_mode='html', reply_markup=inline_markup.margin_type_markup) """, parse_mode='html', reply_markup=inline_markup.margin_type_markup)
@router_main_settings.callback_query(update_main_settings.margin_type) @router_main_settings.callback_query(update_main_settings.margin_type)
async def state_margin_type(callback: CallbackQuery, state): 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 positions = active_positions.get('result', {}).get('list', [])
data_settings = await rq.get_user_main_settings(id) except Exception as e:
logger.error(f"error: {e}")
positions = []
for pos in positions:
size = pos.get('size')
if float(size) > 0:
await callback.answer(
"⚠️ Маржинальный режим нельзя менять при открытой позиции",
show_alert=True
)
return
try: try:
match callback.data: match callback.data:
case 'margin_type_isolated': case 'margin_type_isolated':
await callback.message.answer(f"✅ Изменено: {data_settings['margin_type']} → Isolated") await callback.message.answer(f"✅ Изменено: {data_settings['margin_type']} → Isolated")
await rq.update_margin_type(id, 'Isolated') await rq.update_margin_type(tg_id, 'Isolated')
await main_settings_message(id, callback.message, state) await main_settings_message(tg_id, callback.message)
await state.clear() await state.clear()
case 'margin_type_cross': case 'margin_type_cross':
await callback.message.answer(f"✅ Изменено: {data_settings['margin_type']} → Cross") await callback.message.answer(f"✅ Изменено: {data_settings['margin_type']} → Cross")
await rq.update_margin_type(id, 'Cross') await rq.update_margin_type(tg_id, 'Cross')
await main_settings_message(id, callback.message, state) await main_settings_message(tg_id, callback.message)
await state.clear() await state.clear()
except Exception as e: except Exception as e:
print(f"error: {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 state.set_state(update_main_settings.starting_quantity)
await message.edit_text("Введите <b>начальную ставку:</b>", parse_mode='html', reply_markup=inline_markup.back_btn_list_settings_markup) await message.edit_text("Введите <b>начальную ставку:</b>", parse_mode='html',
reply_markup=inline_markup.back_btn_list_settings_markup)
@router_main_settings.message(update_main_settings.starting_quantity) @router_main_settings.message(update_main_settings.starting_quantity)
async def state_starting_quantity(message: Message, state): 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 = await state.get_data()
data_settings = await rq.get_user_main_settings(message.from_user.id) 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 message.answer(f"✅ Изменено: {data_settings['starting_quantity']}{data['starting_quantity']}")
await rq.update_starting_quantity(message.from_user.id, 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() await state.clear()
else: else:
await message.answer(f'⛔️ Ошибка: вы вводите неверные символы') 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): async def maximum_quantity_message(message, state):
await state.set_state(update_main_settings.maximal_quantity) await state.set_state(update_main_settings.maximal_quantity)
await message.edit_text("Введите <b>максимальное количество серии ставок:</b>", parse_mode='html', reply_markup=inline_markup.back_btn_list_settings_markup) await message.edit_text("Введите <b>максимальное количество серии ставок:</b>", parse_mode='html',
reply_markup=inline_markup.back_btn_list_settings_markup)
@router_main_settings.message(update_main_settings.maximal_quantity) @router_main_settings.message(update_main_settings.maximal_quantity)
async def state_maximal_quantity(message: Message, state): 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 = await state.get_data()
data_settings = await rq.get_user_main_settings(message.from_user.id) 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 message.answer(f"✅ Изменено: {data_settings['maximal_quantity']}{data['maximal_quantity']}")
await rq.update_maximal_quantity(message.from_user.id, 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() await state.clear()
else: else:
await message.answer(f'⛔️ Ошибка: ваше значение ({data['maximal_quantity']}) или выше лимита (100) или вы вводите неверные символы') await message.answer(
f'⛔️ Ошибка: ваше значение ({data['maximal_quantity']}) или выше лимита (100) или вы вводите неверные символы')
await main_settings_message(message.from_user.id, message, state) await main_settings_message(message.from_user.id, message)

View File

@@ -1,27 +1,26 @@
from aiogram import Router from aiogram import Router
import app.telegram.Keyboards.inline_keyboards as inline_markup 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 import app.telegram.database.requests as rq
from aiogram.types import Message, CallbackQuery from aiogram.types import Message, CallbackQuery
# FSM - Механизм состояния from app.states.States import update_risk_management_settings
from aiogram.fsm.state import State, StatesGroup
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("risk_management_settings")
router_risk_management_settings = Router() router_risk_management_settings = Router()
class update_risk_management_settings(StatesGroup):
price_profit = State()
price_loss = State()
max_risk_deal = State()
commission_fee = State()
async def reg_new_user_default_risk_management_settings(id, message): async def reg_new_user_default_risk_management_settings(id, message):
tg_id = id tg_id = id
await rq.set_new_user_default_risk_management_settings(tg_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) data = await rq.get_user_risk_management_settings(id)
text = f"""<b>Риск менеджмент</b>, text = f"""<b>Риск менеджмент</b>,
@@ -29,20 +28,22 @@ async def main_settings_message(id, message, state):
<b>- Процент изменения цены для фиксации прибыли:</b> {data.get('price_profit', 0)}% <b>- Процент изменения цены для фиксации прибыли:</b> {data.get('price_profit', 0)}%
<b>- Процент изменения цены для фиксации убытков:</b> {data.get('price_loss', 0)}% <b>- Процент изменения цены для фиксации убытков:</b> {data.get('price_loss', 0)}%
<b>- Максимальный риск на сделку (в % от баланса):</b> {data.get('max_risk_deal', 0)}% <b>- Максимальный риск на сделку (в % от баланса):</b> {data.get('max_risk_deal', 0)}%
<b>- Комиссия биржи для расчета процента фиксации прибыли:</b> {data.get('commission_fee', 0)}% <b>- Комиссия биржи для расчета прибыли:</b> {data.get('commission_fee', "Да")}
""" """
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.risk_management_settings_markup) await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.risk_management_settings_markup)
async def price_profit_message(message, state): async def price_profit_message(message, state):
await state.set_state(update_risk_management_settings.price_profit) await state.set_state(update_risk_management_settings.price_profit)
text = 'Введите число изменения цены для фиксации прибыли: ' text = 'Введите число изменения цены для фиксации прибыли: '
await message.answer(text=text, parse_mode='html', reply_markup=None) await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.cancel)
@router_risk_management_settings.message(update_risk_management_settings.price_profit) @router_risk_management_settings.message(update_risk_management_settings.price_profit)
async def state_price_profit(message: Message, state): 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 = await state.get_data()
data_settings = await rq.get_user_risk_management_settings(message.from_user.id) data_settings = await rq.get_user_risk_management_settings(message.from_user.id)
@@ -51,24 +52,27 @@ async def state_price_profit(message: Message, state):
await message.answer(f"✅ Изменено: {data_settings['price_profit']}% → {data['price_profit']}%") await message.answer(f"✅ Изменено: {data_settings['price_profit']}% → {data['price_profit']}%")
await rq.update_price_profit(message.from_user.id, 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() await state.clear()
else: 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): async def price_loss_message(message, state):
await state.set_state(update_risk_management_settings.price_loss) await state.set_state(update_risk_management_settings.price_loss)
text = 'Введите число изменения цены для фиксации убытков: ' text = 'Введите число изменения цены для фиксации убытков: '
await message.answer(text=text, parse_mode='html', reply_markup=None) await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.cancel)
@router_risk_management_settings.message(update_risk_management_settings.price_loss) @router_risk_management_settings.message(update_risk_management_settings.price_loss)
async def state_price_loss(message: Message, state): 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 = await state.get_data()
data_settings = await rq.get_user_risk_management_settings(message.from_user.id) data_settings = await rq.get_user_risk_management_settings(message.from_user.id)
@@ -81,7 +85,8 @@ async def state_price_loss(message: Message, state):
# Пробуем перевести price_profit в число, если это возможно # Пробуем перевести price_profit в число, если это возможно
try: try:
current_price_profit_num = int(current_price_profit) current_price_profit_num = int(current_price_profit)
except Exception: except Exception as e:
logger.error(e)
current_price_profit_num = 0 current_price_profit_num = 0
# Флаг, если price_profit изначально равен 0 или совпадает со старым стоп-лоссом # Флаг, если price_profit изначально равен 0 или совпадает со старым стоп-лоссом
@@ -99,23 +104,25 @@ async def state_price_loss(message: Message, state):
else: else:
await message.answer(f"✅ Стоп-лосс изменён: {old_price_loss}% → {new_price_loss}%") await message.answer(f"✅ Стоп-лосс изменён: {old_price_loss}% → {new_price_loss}%")
await main_settings_message(message.from_user.id, message, state) await main_settings_message(message.from_user.id, message)
await state.clear() await state.clear()
else: else:
await message.answer( await message.answer(
f'⛔️ Ошибка: ваше значение ({data["price_loss"]}%) выше лимита (100) или содержит неверные символы') f'⛔️ Ошибка: ваше значение ({data["price_loss"]}%) выше лимита (100) или содержит неверные символы')
await main_settings_message(message.from_user.id, message, state) await main_settings_message(message.from_user.id, message)
async def max_risk_deal_message(message, state): async def max_risk_deal_message(message, state):
await state.set_state(update_risk_management_settings.max_risk_deal) await state.set_state(update_risk_management_settings.max_risk_deal)
text = 'Введите число (процент от баланса) для изменения максимального риска на сделку: ' text = 'Введите число (процент от баланса) для изменения максимального риска на сделку: '
await message.answer(text=text, parse_mode='html', reply_markup=None) await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.cancel)
@router_risk_management_settings.message(update_risk_management_settings.max_risk_deal) @router_risk_management_settings.message(update_risk_management_settings.max_risk_deal)
async def state_max_risk_deal(message: Message, state): 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 = await state.get_data()
data_settings = await rq.get_user_risk_management_settings(message.from_user.id) data_settings = await rq.get_user_risk_management_settings(message.from_user.id)
@@ -124,34 +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 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 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() await state.clear()
else: else:
await message.answer(f'⛔️ Ошибка: ваше значение ({data['max_risk_deal']}%) или выше лимита (100) или вы вводите неверные символы') await message.answer(
f'⛔️ Ошибка: ваше значение ({data['max_risk_deal']}%) или выше лимита (100) или вы вводите неверные символы')
await main_settings_message(message.from_user.id, message, state) await main_settings_message(message.from_user.id, message)
async def commission_fee_message(message, state): async def commission_fee_message(message, state):
await state.set_state(update_risk_management_settings.commission_fee) await state.set_state(update_risk_management_settings.commission_fee)
await message.answer(text="Введите процент комиссии биржи (например, 0.1):", parse_mode='html', reply_markup=None) await message.answer(text="Хотите учитывать комиссию биржи:", parse_mode='html',
reply_markup=inline_markup.buttons_yes_no_markup)
@router_risk_management_settings.message(update_risk_management_settings.commission_fee)
async def state_commission_fee(message: Message, state):
await state.update_data(commission_fee=message.text)
data = await state.get_data()
data_settings = await rq.get_user_risk_management_settings(message.from_user.id)
try: @router_risk_management_settings.callback_query(lambda c: c.data in ["clb_yes", "clb_no"])
val = float(data['commission_fee']) async def process_commission_fee_callback(callback: CallbackQuery, state):
if val < 0 or val > 100: val = "Да" if callback.data == "clb_yes" else "Нет"
raise ValueError() await rq.update_commission_fee(callback.from_user.id, val)
except Exception: await callback.message.answer(f"✅ Изменено: {val}")
await message.answer("⛔️ Ошибка: введите корректный процент комиссии от 0 до 100") await callback.answer()
return await commission_fee_message(message, state) await main_settings_message(callback.from_user.id, callback.message)
await rq.update_commission_fee(message.from_user.id, val)
await message.answer(f"✅ Изменено: {data_settings['commission_fee']}% → {data['commission_fee']}%")
await main_settings_message(message.from_user.id, message, state)
await state.clear() await state.clear()

View File

@@ -1,64 +1,76 @@
import logging.config import logging.config
from aiogram import F, Router from aiogram import F, Router
from aiogram.filters import CommandStart, Command from aiogram.filters import CommandStart
from aiogram.types import Message, CallbackQuery from aiogram.types import Message, CallbackQuery
from aiogram.fsm.context import FSMContext 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.main_settings.settings as func_main_settings
import app.telegram.functions.risk_management_settings.settings as func_rmanagement_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.condition_settings.settings as func_condition_settings
import app.telegram.functions.additional_settings.settings as func_additional_settings import app.telegram.functions.additional_settings.settings as func_additional_settings
import app.telegram.database.requests as rq 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 from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG) logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("handlers") logger = logging.getLogger("handlers")
router = Router() router = Router()
@router.message(CommandStart()) @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 rq.set_new_user_bybit_api(message.from_user.id)
await func.start_message(message) await func.start_message(message)
@router.message(F.text == "👤 Профиль") @router.message(F.text == "👤 Профиль")
async def profile_message(message: Message): async def profile_message(message: Message) -> None:
user = await rq.check_user(message.from_user.id) """
Обработчик кнопки 'Профиль'.
Проверяет существование пользователя и отображает профиль.
if user: 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) 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") @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) 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 user and balance:
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:
await func.profile_message(callback.from_user.username, callback.message) await func.profile_message(callback.from_user.username, callback.message)
else: else:
await rq.save_tg_id_new_user(callback.from_user.id) await rq.save_tg_id_new_user(callback.from_user.id)
@@ -66,61 +78,93 @@ async def clb_profile_msg(callback: CallbackQuery):
await func_main_settings.reg_new_user_default_main_settings(callback.from_user.id, callback.message) 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, await func_rmanagement_settings.reg_new_user_default_risk_management_settings(callback.from_user.id,
callback.message) callback.message)
await func_condition_settings.reg_new_user_default_condition_settings(callback.from_user.id, callback.message) await func_condition_settings.reg_new_user_default_condition_settings(callback.from_user.id)
await func_additional_settings.reg_new_user_default_additional_settings(callback.from_user.id, callback.message) await 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() await callback.answer()
# Настройки торговли
@router.callback_query(F.data == "clb_settings_message") @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 func.settings_message(callback.message)
await callback.answer() await callback.answer()
@router.callback_query(F.data == "clb_back_to_special_settings_message") @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 func.settings_message(callback.message)
await callback.answer() await callback.answer()
@router.callback_query(F.data == "clb_change_main_settings") @router.callback_query(F.data == "clb_change_main_settings")
async def clb_change_main_settings_message(callback: CallbackQuery, state: FSMContext): async def clb_change_main_settings_message(callback: CallbackQuery) -> None:
await func_main_settings.main_settings_message(callback.from_user.id, callback.message, state) """
Открыть меню изменения главных настроек.
Args:
callback (CallbackQuery): полученный колбэк.
"""
await func_main_settings.main_settings_message(callback.from_user.id, callback.message)
await callback.answer() await callback.answer()
@router.callback_query(F.data == "clb_change_risk_management_settings") @router.callback_query(F.data == "clb_change_risk_management_settings")
async def clb_change_risk_management_message(callback: CallbackQuery, state: FSMContext): async def clb_change_risk_management_message(callback: CallbackQuery) -> None:
await func_rmanagement_settings.main_settings_message(callback.from_user.id, callback.message, state) """
Открыть меню изменения настроек управления рисками.
Args:
callback (CallbackQuery): полученный колбэк.
"""
await func_rmanagement_settings.main_settings_message(callback.from_user.id, callback.message)
await callback.answer() await callback.answer()
@router.callback_query(F.data == "clb_change_condition_settings") @router.callback_query(F.data == "clb_change_condition_settings")
async def clb_change_condition_message(callback: CallbackQuery, state: FSMContext): async def clb_change_condition_message(callback: CallbackQuery) -> None:
await func_condition_settings.main_settings_message(callback.from_user.id, callback.message, state) """
Открыть меню изменения настроек условий.
Args:
callback (CallbackQuery): полученный колбэк.
"""
await func_condition_settings.main_settings_message(callback.from_user.id, callback.message)
await callback.answer() await callback.answer()
@router.callback_query(F.data == "clb_change_additional_settings") @router.callback_query(F.data == "clb_change_additional_settings")
async def clb_change_additional_message(callback: CallbackQuery, state: FSMContext): async def clb_change_additional_message(callback: CallbackQuery) -> None:
await func_additional_settings.main_settings_message(callback.from_user.id, callback.message, state) """
Открыть меню изменения дополнительных настроек.
Args:
callback (CallbackQuery): полученный колбэк.
"""
await func_additional_settings.main_settings_message(callback.from_user.id, callback.message)
await callback.answer() await callback.answer()
# Конкретные настройки каталогов # Конкретные настройки каталогов
list_main_settings = ['clb_change_trading_mode', list_main_settings = ['clb_change_trading_mode',
'clb_change_switch_mode',
'clb_change_margin_type', 'clb_change_margin_type',
'clb_change_size_leverage', 'clb_change_size_leverage',
'clb_change_starting_quantity', 'clb_change_starting_quantity',
@@ -130,13 +174,22 @@ list_main_settings = ['clb_change_trading_mode',
@router.callback_query(F.data.in_(list_main_settings)) @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() await callback.answer()
try: try:
match callback.data: match callback.data:
case 'clb_change_trading_mode': case 'clb_change_trading_mode':
await func_main_settings.trading_mode_message(callback.message, state) 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': case 'clb_change_margin_type':
await func_main_settings.margin_type_message(callback.message, state) await func_main_settings.margin_type_message(callback.message, state)
case 'clb_change_size_leverage': case 'clb_change_size_leverage':
@@ -159,7 +212,14 @@ list_risk_management_settings = ['clb_change_price_profit',
@router.callback_query(F.data.in_(list_risk_management_settings)) @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() await callback.answer()
try: try:
@@ -176,7 +236,7 @@ async def clb_risk_management_settings_msg(callback: CallbackQuery, state: FSMCo
logger.error(f"Error callback in risk_management match-case: {e}") logger.error(f"Error callback in risk_management match-case: {e}")
list_condition_settings = ['clb_change_trigger', list_condition_settings = ['clb_change_mode',
'clb_change_timer', 'clb_change_timer',
'clb_change_filter_volatility', 'clb_change_filter_volatility',
'clb_change_external_cues', 'clb_change_external_cues',
@@ -187,13 +247,20 @@ list_condition_settings = ['clb_change_trigger',
@router.callback_query(F.data.in_(list_condition_settings)) @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() await callback.answer()
try: try:
match callback.data: match callback.data:
case 'clb_change_trigger': case 'clb_change_mode':
await func_condition_settings.trigger_message(callback.message, state) await func_condition_settings.trigger_message(callback.from_user.id, callback.message, state)
case 'clb_change_timer': case 'clb_change_timer':
await func_condition_settings.timer_message(callback.from_user.id, callback.message, state) await func_condition_settings.timer_message(callback.from_user.id, callback.message, state)
case 'clb_change_filter_volatility': case 'clb_change_filter_volatility':
@@ -217,7 +284,14 @@ list_additional_settings = ['clb_change_save_pattern',
@router.callback_query(F.data.in_(list_additional_settings)) @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() await callback.answer()
try: try:

View File

@@ -80,5 +80,35 @@ LOGGING_CONFIG = {
"level": "DEBUG", "level": "DEBUG",
"propagate": False, "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,
},
}, },
} }

View File

@@ -14,6 +14,7 @@ greenlet==3.2.4
idna==3.10 idna==3.10
magic-filter==1.0.12 magic-filter==1.0.12
multidict==6.6.4 multidict==6.6.4
nest-asyncio==1.6.0
propcache==0.3.2 propcache==0.3.2
pybit==5.11.0 pybit==5.11.0
pycryptodome==3.23.0 pycryptodome==3.23.0