Compare commits

..

101 Commits

Author SHA1 Message Date
82d875136b deleted deprecated 2025-09-12 09:33:16 +07:00
28ead61112 Обновить README.md 2025-09-12 09:32:27 +07:00
688fc7a8ab Merge pull request 'develop' (#4) from Alex/stcs:develop into stable
Reviewed-on: #4
2025-09-12 09:28:08 +07:00
07666fa984 Merge branch 'stable' into develop 2025-09-12 09:27:52 +07:00
algizn97
b3119c6ee1 Fixed martingale step 2025-09-11 12:50:43 +05:00
algizn97
da16a267e4 Fixed 2025-09-11 12:15:19 +05:00
algizn97
babbcbd1fc The timer start function has been updated 2025-09-11 12:02:47 +05:00
algizn97
f42940f847 Added timer deletion button 2025-09-11 10:57:21 +05:00
algizn97
3ff146a1b9 Fixed 2025-09-10 20:30:51 +05:00
algizn97
93865a1b16 Fixed 2025-09-10 17:34:41 +05:00
algizn97
44f9b05001 Fixed 2025-09-10 15:28:08 +05:00
algizn97
02279d19ae Fixed 2025-09-09 11:40:51 +05:00
algizn97
cf581dc485 Fixed 2025-09-09 10:24:01 +05:00
15e248d7d7 Ещё исправления markdown 2025-09-07 20:57:25 +07:00
cdb745d55a Исправление markdown 2025-09-07 20:56:42 +07:00
c46a4cb0b7 Merge pull request 'develop' (#3) from Alex/stcs:develop into stable
Reviewed-on: #3
2025-09-07 20:55:40 +07:00
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
algizn97
dd63f4c015 added 2025-08-23 14:41:23 +05:00
algizn97
6267663015 Added States.py 2025-08-23 14:34:38 +05:00
algizn97
89ea511072 Update 2025-08-23 14:34:19 +05:00
algizn97
afe61ea7d6 Added documentation 2025-08-23 14:33:33 +05:00
algizn97
2da06481f7 Update 2025-08-23 14:32:11 +05:00
algizn97
6c3f13f372 Update 2025-08-23 14:32:04 +05:00
algizn97
1ec9732607 Update loggers 2025-08-23 14:31:45 +05:00
algizn97
c7da20d577 Update 2025-08-23 14:31:09 +05:00
algizn97
8a2497bcac Added logger_helper.py 2025-08-23 14:30:48 +05:00
algizn97
0746490786 Delete func_min_qty.py and logs.py 2025-08-23 14:30:24 +05:00
algizn97
f895c19b14 fixed the method of getting the timer value. 2025-08-22 16:57:59 +05:00
algizn97
c4b35be053 Updated the risk management function 2025-08-22 16:49:17 +05:00
algizn97
54667db29b Added a martingale step 2025-08-22 16:48:17 +05:00
algizn97
12a00a1f3a Added a martingale step 2025-08-22 16:48:09 +05:00
algizn97
6ec99dc9a7 Update functions 2025-08-22 16:47:33 +05:00
algizn97
812920f46d The timer function has been updated 2025-08-22 13:56:56 +05:00
algizn97
cd7180c3d7 Added the function of getting and updating data in the database for the UserTimer table 2025-08-22 13:55:50 +05:00
algizn97
fc381ae6c2 Added timer 2025-08-22 13:54:13 +05:00
algizn97
8ab308d4b9 Added keyboards 2025-08-22 13:53:48 +05:00
algizn97
0c4204fb6e Added UserTimer 2025-08-22 13:53:17 +05:00
algizn97
c9c6a5b7f0 Fixed filename 2025-08-22 13:52:50 +05:00
algizn97
4ca6e1fb2c Added dp 2025-08-22 13:52:31 +05:00
algizn97
1a20c1a9d2 Added keyboard 2025-08-22 13:52:20 +05:00
algizn97
eaf8458835 Update 2025-08-21 16:10:01 +05:00
algizn97
f8cedf4cb4 Fixed 2025-08-21 16:09:32 +05:00
algizn97
d0577f163b Added get user trades 2025-08-21 14:22:54 +05:00
algizn97
8293c44864 Added USER DEALS 2025-08-21 14:22:34 +05:00
algizn97
c1a9f16faa Added commission_fee 2025-08-21 13:47:19 +05:00
algizn97
99b51d4cc0 Added limit and market orders functions 2025-08-21 13:46:12 +05:00
algizn97
b46d8d7af9 Update handlers.py 2025-08-21 13:41:58 +05:00
algizn97
c8b0dad7c2 Added commission_fee and order_type functions 2025-08-21 13:39:43 +05:00
algizn97
7f53caaac6 Added commission_fee and entry_order_type 2025-08-21 13:38:50 +05:00
algizn97
fe1c0b16ce Added reply keyboard (base_buttons_markup) 2025-08-21 13:37:58 +05:00
algizn97
4ebe7399ba Update function 2025-08-21 13:36:11 +05:00
algizn97
2dc639d59a Added market order and limit order 2025-08-21 13:35:42 +05:00
algizn97
a6ba949061 Added buttons To the main page, Open a deal, My Deals and select the input type 2025-08-21 13:34:56 +05:00
algizn97
de7b5ce557 Update 2025-08-21 13:23:17 +05:00
algizn97
cb7d4b1f66 Fixed the letter 2025-08-20 17:51:06 +05:00
f26df3b1a4 Merge pull request 'Merge pull STCS' (#2) from Pixe1lz/stcs:stable into stable
Reviewed-on: #2
2025-07-30 14:32:49 +07:00
Kirill Strelnikov
1997b9d1c0 The message about the Bybit profile output has been corrected when clicking the "Start" button, and messages regarding the requirement to connect the platform and incorrect API entries have been added. Messages about successful and erroneous changes to user settings have been added. 2025-07-22 16:08:23 +07:00
Kirill Strelnikov
e555bfa8fb The text of the welcome message has been changed, the text of the risk management settings has been changed, the default value for "maximum_trade_risk" in the database has been changed from 1 to 100. The message upon re-clicking the "Start Trading" button has been removed. 2025-07-22 12:54:23 +07:00
Kirill Strelnikov
0fecb5dab7 Merge branch 'stable' of https://git.svoboda.works/Pixe1lz/stcs into stable 2025-07-21 16:58:38 +07:00
Kirill Strelnikov
242be34a97 fix app/telegram/handlers/handlers.py, fix app/telegram/functions/functions.py || change two buttons "Начать торговлю" || fix text registration message, fix text profile || delete app/services/Bybit/config.py || delete comments 2025-07-21 16:55:30 +07:00
Бездельник бездельников
2da3c282d1 merge upstream 2025-07-21 15:33:27 +07:00
Kirill Strelnikov
fb0c14aba1 Update Readme.md 2025-07-21 15:05:50 +07:00
Kirill Strelnikov
ed67ed78c0 version 1 STCS 2025-07-21 13:40:33 +07:00
31 changed files with 3804 additions and 428 deletions

View File

@@ -1 +1,3 @@
TOKEN_TELEGRAM_BOT=
TOKEN_TELEGRAM_BOT_1=
TOKEN_TELEGRAM_BOT_2=
TOKEN_TELEGRAM_BOT_3=

7
.gitignore vendored
View File

@@ -7,6 +7,7 @@ __pycache__/
env/
venv/
.venv/
requirements.txt
.idea
/.idea
/myenv
myenv

View File

@@ -1,31 +0,0 @@
import asyncio
from aiogram import Bot, Dispatcher
from aiogram.filters import Command, CommandStart
from aiogram.types import Message
from app.telegram.database.models import async_main
from app.telegram.handlers.handlers import router # Для вызова событий
from app.telegram.functions.main_settings.settings import router_main_settings # Для вызова событий
from config import TOKEN_TG_BOT
from app.telegram.logs import logger
bot = Bot(token=TOKEN_TG_BOT)
dp = Dispatcher()
async def main():
await async_main()
dp.include_router(router)
dp.include_router(router_main_settings)
await dp.start_polling(bot)
if __name__ == '__main__':
try:
asyncio.run(main())
except KeyboardInterrupt:
print("Bot is off")

52
BybitBot_API.py Normal file
View File

@@ -0,0 +1,52 @@
import asyncio
import logging.config
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.redis import RedisStorage
from app.services.Bybit.functions.bybit_ws import get_or_create_event_loop, set_event_loop
from app.telegram.database.models import async_main
from app.telegram.handlers.handlers import router
from app.telegram.functions.main_settings.settings import router_main_settings
from app.telegram.functions.risk_management_settings.settings import router_risk_management_settings
from app.telegram.functions.condition_settings.settings import condition_settings_router
from app.services.Bybit.functions.Add_Bybit_API import router_register_bybit_api
from app.services.Bybit.functions.functions import router_functions_bybit_trade
from logger_helper.logger_helper import LOGGING_CONFIG
from config import TOKEN_TG_BOT_1
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("main")
storage = RedisStorage.from_url("redis://localhost:6379/0")
bot = Bot(token=TOKEN_TG_BOT_1)
dp = Dispatcher(storage=storage)
async def main() -> None:
"""
Основная асинхронная функция запуска бота:
"""
loop = get_or_create_event_loop()
set_event_loop(loop)
await async_main()
dp.include_router(router)
dp.include_router(router_main_settings)
dp.include_router(router_risk_management_settings)
dp.include_router(condition_settings_router)
dp.include_router(router_register_bybit_api)
dp.include_router(router_functions_bybit_trade)
try:
await dp.start_polling(bot)
except asyncio.CancelledError:
logger.info("Bot is off")
if __name__ == '__main__':
try:
logger.info("Bot is on")
asyncio.run(main())
except KeyboardInterrupt:
logger.info("Bot is off")

View File

@@ -21,6 +21,12 @@
<EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
</PropertyGroup>
<ItemGroup>
<Compile Include="app\services\Bybit\functions\Add_Bybit_API.py" />
<Compile Include="app\services\Bybit\functions\balance.py" />
<Compile Include="app\services\Bybit\functions\functions.py" />
<Compile Include="app\services\Bybit\functions\func_min_qty.py" />
<Compile Include="app\services\Bybit\functions\Futures.py" />
<Compile Include="app\services\Bybit\functions\price_symbol.py" />
<Compile Include="app\telegram\functions\additional_settings\settings.py" />
<Compile Include="app\telegram\functions\condition_settings\settings.py" />
<Compile Include="app\telegram\functions\functions.py" />
@@ -37,6 +43,9 @@
</ItemGroup>
<ItemGroup>
<Folder Include="app\" />
<Folder Include="app\services\Bybit\" />
<Folder Include="app\services\" />
<Folder Include="app\services\Bybit\functions\" />
<Folder Include="app\telegram\database\" />
<Folder Include="app\telegram\functions\condition_settings\" />
<Folder Include="app\telegram\functions\additional_settings\" />

View File

@@ -1 +1,83 @@
abobus
Crypto Trading Telegram Bot
Этот бот — автоматизированный торговый помощник для работы с криптовалютной биржей Bybit на основе стратегии мартингейла. Он позволяет торговать бессрочными контрактами с управлением рисками, тейк-профитами, стоп-лоссами и кредитным плечом.
## Основные возможности
- Поддержка работы с биржей Bybit через официальный API.
- Открытие и закрытие позиций по выбранным торговым парам.
- Поддержка рыночных и лимитных ордеров.
- Установка уровней тейк-профита (TP) и стоп-лосса (SL).
- Управление кредитным плечом (leverage).
- Реализация стратегии мартингейла с настройками шага, коэффициента и лимитов.
- Контроль максимального риска на сделку по балансу пользователя.
- Обработка ошибок API, логирование событий и информирование пользователя.
- Таймеры для отложенного открытия и закрытия сделок.
- Интерактивное меню и ввод настроек через Telegram.
- Хранение пользовательских настроек и статистики в базе данных.
## Установка и запуск
1. Клонируйте репозиторий:
```bash
git clone https://git.svoboda.works/kodorvan/stcs
```
2. Установите зависимости:
```bash
pip install -r requirements.txt
```
3. Зарегистрируйте чат-робота и сгенерируйте ключ авторизации<br>
[@BotFather](https://t.me/BotFather)
4. Создайте файл .env и настройте переменные окружения
```bash
cp .env.sample .env
nvim .env
```
5. Запустите бота:
```bash
python BybitBot_API.py
```
## Настройки пользователя
- Кредитное плечо (например, 15x)
- Торговая пара (например, DOGEUSDT, BTCUSDT)
- Начальное количество для сделок
- Тип ордера (Market или Limit)
- Уровни Take Profit и Stop Loss (в процентах или цене)
- Коэффициент мартингейла и максимальное количество шагов
- Максимально допустимый риск на одну сделку (% от баланса)
- Таймеры для старта и закрытия сделок
## Безопасность и риски
- Бот требует аккуратной настройки параметров риска.
- Храните API ключи в безопасности, избегайте публикации.

View File

@@ -0,0 +1,111 @@
from aiogram import F, Router
import logging.config
from app.services.Bybit.functions.functions import start_bybit_trade_message
from logger_helper.logger_helper import LOGGING_CONFIG
import app.telegram.Keyboards.inline_keyboards as inline_markup
import app.telegram.Keyboards.reply_keyboards as reply_markup
import app.telegram.functions.main_settings.settings as func_main_settings
import app.telegram.functions.risk_management_settings.settings as func_rmanagement_settings
import app.telegram.functions.condition_settings.settings as func_condition_settings
import app.telegram.functions.additional_settings.settings as func_additional_settings
import app.telegram.database.requests as rq
from aiogram.types import Message, CallbackQuery
from app.states.States import state_reg_bybit_api
from aiogram.fsm.context import FSMContext
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("add_bybit_api")
router_register_bybit_api = Router()
@router_register_bybit_api.callback_query(F.data == 'clb_new_user_connect_bybit_api_message')
async def info_for_bybit_api_message(callback: CallbackQuery) -> None:
"""
Отвечает пользователю подробной инструкцией по подключению аккаунта Bybit.
Показывает как создать API ключ и передать его чат-боту.
"""
text = '''<b>Подключение Bybit аккаунта</b>
<b>1. Зарегистрируйтесь или войдите в свой аккаунт на Bybit (https://www.bybit.com/).</b>
<b>2. В личном кабинете выберите раздел API. </b>
<b>3. Создание нового API ключа</b>
- Нажмите кнопку Create New Key (Создать новый ключ).
- Выберите системно-сгенерированный ключ.
- Укажите название API ключа (любое).
- Выберите права доступа для торговли (Trade).
- Можно ограничить доступ по IP для безопасности.
<b>4. Подтверждение создания</b>
- Подтвердите создание ключа.
- Отправьте чат-роботу.
<b>Важно: сохраните отдельно API Key и Secret Key в надежном месте. Secret ключ отображается только один раз. </b>
'''
await callback.message.answer(text=text, parse_mode='html', reply_markup=inline_markup.connect_bybit_api_markup)
await callback.answer()
@router_register_bybit_api.callback_query(F.data == 'clb_new_user_connect_bybit_api')
async def add_api_key_message(callback: CallbackQuery, state: FSMContext) -> None:
"""
Инициирует процесс добавления API ключа.
Переводит пользователя в состояние ожидания ввода API Key.
"""
await state.set_state(state_reg_bybit_api.api_key)
text = 'Отправьте KEY_API ниже: '
await callback.message.answer(text=text)
@router_register_bybit_api.message(state_reg_bybit_api.api_key)
async def add_api_key_and_message_for_secret_key(message: Message, state: FSMContext) -> None:
"""
Сохраняет API Key во временное состояние FSM,
затем запрашивает у пользователя ввод Secret Key.
"""
await state.update_data(api_key=message.text)
text = 'Отправьте SECRET_KEY ниже'
await message.answer(text=text)
await state.set_state(state_reg_bybit_api.secret_key)
@router_register_bybit_api.message(state_reg_bybit_api.secret_key)
async def add_secret_key(message: Message, state: FSMContext) -> None:
"""
Сохраняет Secret Key и финализирует регистрацию,
обновляет базу данных, устанавливает символ пользователя и очищает состояние.
"""
await state.update_data(secret_key=message.text)
data = await state.get_data()
user = await rq.check_user(message.from_user.id)
await rq.upsert_api_keys(message.from_user.id, data['api_key'], data['secret_key'])
await rq.set_new_user_symbol(message.from_user.id)
await state.clear()
await message.answer('Данные добавлены.',
reply_markup=reply_markup.base_buttons_markup)
if user:
await start_bybit_trade_message(message)
else:
await rq.save_tg_id_new_user(message.from_user.id)
await func_main_settings.reg_new_user_default_main_settings(message.from_user.id, message)
await func_rmanagement_settings.reg_new_user_default_risk_management_settings(message.from_user.id,
message)
await func_condition_settings.reg_new_user_default_condition_settings(message.from_user.id)
await func_additional_settings.reg_new_user_default_additional_settings(message.from_user.id, message)
await start_bybit_trade_message(message)

View File

@@ -0,0 +1,874 @@
import asyncio
import logging.config
import time
import app.services.Bybit.functions.balance as balance_g
import app.services.Bybit.functions.price_symbol as price_symbol
import app.telegram.database.requests as rq
import app.telegram.Keyboards.inline_keyboards as inline_markup
from logger_helper.logger_helper import LOGGING_CONFIG
from pybit import exceptions
from pybit.unified_trading import HTTP
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("futures")
processed_trade_ids = set()
async def get_bybit_client(tg_id):
"""
Асинхронно получает экземпляр клиента Bybit.
:param tg_id: int - ID пользователя Telegram
:return: HTTP - экземпляр клиента Bybit
"""
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
return HTTP(api_key=api_key, api_secret=secret_key)
def safe_float(val) -> float:
"""
Безопасное преобразование значения в float.
Возвращает 0.0, если значение None, пустое или некорректное.
"""
try:
if val is None or val == "":
return 0.0
return float(val)
except (ValueError, TypeError):
logger.error("Некорректное значение для преобразования в float", exc_info=True)
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"
)
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"
)
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"
)
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("Ошибка при извлечении реализованной прибыли: %s", e)
return 0.0
async def calculate_total_budget(starting_quantity, martingale_factor, max_steps, commission_fee_percent, leverage, current_price):
"""
Вычисляет общий бюджет серии ставок с учётом цены пары, комиссии и кредитного плеча.
Параметры:
- starting_quantity_usdt: стартовый размер ставки в долларах (USD)
- martingale_factor: множитель увеличения ставки при каждом проигрыше
- max_steps: максимальное количество шагов удвоения ставки
- commission_fee_percent: процент комиссии на одну операцию (открытие или закрытие)
- leverage: кредитное плечо
- current_price: текущая цена актива (например BTCUSDT)
Возвращает:
- общий бюджет в долларах, который необходимо иметь на счету
"""
total = 0
for step in range(max_steps):
quantity = starting_quantity * (martingale_factor ** step) # размер ставки на текущем шаге в USDT
# Переводим ставку из USDT в количество актива по текущей цене
quantity_in_asset = quantity / current_price
# Учитываем комиссию за вход и выход (умножаем на 2)
quantity_with_fee = quantity * (1 + 2 * commission_fee_percent / 100)
# Учитываем кредитное плечо - реальные собственные вложения меньше
effective_quantity = quantity_with_fee / leverage
total += effective_quantity
# Возвращаем бюджет в USDT
total_usdt = total * current_price
return total_usdt
async def handle_execution_message(message, msg):
"""
Обработчик сообщений об исполнении сделки.
Логирует событие и проверяет условия для мартингейла и TP.
"""
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)
symbol = data.get("symbol")
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"))
martingale_factor = safe_float(data_main_stgs.get("martingale_factor"))
closed_size = safe_float(data.get("closedSize", 0))
commission = safe_float(data.get("execFee", 0))
if commission_fee == "Да":
pnl -= commission
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)
if closed_size == 0:
side = data.get("side", "")
if side.lower() == "buy":
await rq.set_last_series_info(tg_id, last_side="Buy")
elif side.lower() == "sell":
await rq.set_last_series_info(tg_id, last_side="Sell")
if trigger == "Автоматический" and closed_size > 0:
if pnl < 0:
if trading_mode == 'Switch':
side = data_main_stgs.get("last_side")
else:
side = "Buy" if trading_mode == "Long" else "Sell"
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 message.answer(
f"❗️ Сделка закрылась в минус, открываю новую сделку с увеличенной ставкой.\n"
)
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(
"❗️ Прибыль достигнута, шаг мартингейла сброшен."
)
async def handle_order_message(message, msg: dict) -> None:
"""
Обработчик сообщений об исполнении ордера.
Логирует событие и проверяет условия для мартингейла и TP.
"""
# logger.info(f"Исполнен ордер:\n{json.dumps(msg, indent=4, ensure_ascii=False)}")
trade_info = format_order_details_position(msg)
if trade_info:
await message.answer(f"{trade_info}", reply_markup=inline_markup.back_to_main)
async def error_max_step(message) -> None:
"""
Сообщение об ошибке превышения максимального количества шагов мартингейла.
"""
logger.error(
"Сделка не была совершена, превышен лимит максимального количества ставок в серии."
)
await message.answer(
"Сделка не была совершена, превышен лимит максимального количества ставок в серии.",
reply_markup=inline_markup.back_to_main,
)
async def error_max_risk(message) -> None:
"""
Сообщение об ошибке превышения риск-лимита сделки.
"""
logger.error("Сделка не была совершена, риск убытка превышает допустимый лимит.")
await message.answer(
"Сделка не была совершена, риск убытка превышает допустимый лимит.",
reply_markup=inline_markup.back_to_main,
)
async def open_position(
tg_id, message, side: str, margin_mode: str, symbol, quantity, tpsl_mode="Full"
):
"""
Открывает позицию на Bybit с учётом настроек пользователя, маржи, размера лота, платформы и риска.
Возвращает True при успехе, False при ошибках открытия ордера, None при исключениях.
"""
try:
client = await get_bybit_client(tg_id)
data_main_stgs = await rq.get_user_main_settings(tg_id)
order_type = data_main_stgs.get("entry_order_type")
bybit_margin_mode = (
"ISOLATED_MARGIN" if margin_mode == "Isolated" else "REGULAR_MARGIN"
)
limit_price = None
if order_type == "Limit":
limit_price = await rq.get_limit_price(tg_id)
data_risk_stgs = await rq.get_user_risk_management_settings(tg_id)
price = await price_symbol.get_price(tg_id, symbol=symbol)
entry_price = safe_float(price)
leverage = safe_float(data_main_stgs.get("size_leverage", 1))
max_martingale_steps = int(data_main_stgs.get("maximal_quantity", 0))
current_martingale = await rq.get_martingale_step(tg_id)
max_risk_percent = safe_float(data_risk_stgs.get("max_risk_deal"))
loss_profit = safe_float(data_risk_stgs.get("price_loss"))
commission_fee = data_risk_stgs.get("commission_fee")
starting_quantity = safe_float(data_main_stgs.get('starting_quantity'))
martingale_factor = safe_float(data_main_stgs.get('martingale_factor'))
fee_info = client.get_fee_rates(category='linear', symbol=symbol)
instruments_resp = client.get_instruments_info(category="linear", symbol=symbol)
instrument = instruments_resp.get("result", {}).get("list", [])
if commission_fee == "Да":
commission_fee_percent = safe_float(fee_info['result']['list'][0]['takerFeeRate'])
else:
commission_fee_percent = 0.0
total_budget = await calculate_total_budget(
starting_quantity=starting_quantity,
martingale_factor=martingale_factor,
max_steps=max_martingale_steps,
commission_fee_percent=commission_fee_percent,
leverage=leverage,
current_price=entry_price,
)
balance = await balance_g.get_balance(tg_id, message)
if safe_float(balance) < total_budget:
logger.error(
f"Недостаточно средств для серии из {max_martingale_steps} шагов с текущими параметрами. "
f"Требуемый бюджет: {total_budget:.2f} USDT, доступно: {balance} USDT."
)
await message.answer(
f"Недостаточно средств для серии из {max_martingale_steps} шагов с текущими параметрами. "
f"Требуемый бюджет: {total_budget:.2f} USDT, доступно: {balance} USDT.",
reply_markup=inline_markup.back_to_main,
)
return
if order_type == "Limit" and limit_price:
price_for_calc = limit_price
else:
price_for_calc = entry_price
potential_loss = safe_float(quantity) * price_for_calc * (loss_profit / 100)
adjusted_loss = potential_loss / leverage
allowed_loss = safe_float(balance) * (max_risk_percent / 100)
if adjusted_loss > allowed_loss:
await error_max_risk(message)
return
if max_martingale_steps < current_martingale:
await error_max_step(message)
return
client.set_margin_mode(setMarginMode=bybit_margin_mode)
max_leverage = safe_float(instrument[0].get("leverageFilter", {}).get("maxLeverage", 0))
if safe_float(leverage) > max_leverage:
await message.answer(
f"Запрошенное кредитное плечо {leverage} превышает максимальное {max_leverage} для {symbol}. "
f"Устанавливаю максимальное.",
reply_markup=inline_markup.back_to_main,
)
logger.info(
f"Запрошенное кредитное плечо {leverage} превышает максимальное {max_leverage} для {symbol}. Устанавливаю максимальное.")
leverage_to_set = max_leverage
else:
leverage_to_set = safe_float(leverage)
try:
client.set_leverage(
category="linear",
symbol=symbol,
buyLeverage=str(leverage_to_set),
sellLeverage=str(leverage_to_set),
)
logger.info(f"Set leverage to {leverage_to_set} for {symbol}")
except exceptions.InvalidRequestError as e:
if "110043" in str(e):
logger.info(f"Leverage already set to {leverage} for {symbol}")
else:
raise e
if instruments_resp.get("retCode") == 0:
instrument_info = instruments_resp.get("result", {}).get("list", [])
if instrument_info:
instrument_info = instrument_info[0]
min_notional_value = float(instrument_info.get("lotSizeFilter", {}).get("minNotionalValue", 0))
min_order_value = min_notional_value
else:
min_order_value = 5.0
order_value = float(quantity) * price_for_calc
if order_value < min_order_value:
logger.error(
f"Сумма ордера слишком мала: {order_value:.2f} USDT. "
f"Минимум для торговли — {min_order_value} USDT. "
f"Пожалуйста, увеличьте количество позиций."
)
await message.answer(
f"Сумма ордера слишком мала: {order_value:.2f} USDT. "
f"Минимум для торговли — {min_order_value} USDT. "
f"Пожалуйста, увеличьте количество позиций.",
reply_markup=inline_markup.back_to_main,
)
return False
if bybit_margin_mode == "ISOLATED_MARGIN":
# Открываем позицию
response = client.place_order(
category="linear",
symbol=symbol,
side=side,
orderType=order_type,
qty=str(quantity),
price=(
str(limit_price) if order_type == "Limit" and limit_price else None
),
timeInForce="GTC",
orderLinkId=f"deal_{symbol}_{int(time.time())}",
)
if response.get("retCode", -1) != 0:
logger.error(f"Ошибка открытия ордера: {response}")
await message.answer(
f"Ошибка открытия ордера", reply_markup=inline_markup.back_to_main
)
return False
# Получаем цену ликвидации
positions = client.get_positions(category="linear", symbol=symbol)
pos = positions.get("result", {}).get("list", [{}])[0]
avg_price = float(pos.get("avgPrice", 0))
liq_price = safe_float(pos.get("liqPrice", 0))
if liq_price > 0 and avg_price > 0:
if side.lower() == "buy":
take_profit_price = avg_price + (avg_price - liq_price)
else:
take_profit_price = avg_price - (liq_price - avg_price)
take_profit_price = max(take_profit_price, 0)
try:
try:
client.set_tp_sl_mode(
symbol=symbol, category="linear", tpSlMode="Full"
)
except exceptions.InvalidRequestError as e:
if "same tp sl mode" in str(e):
logger.info("Режим TP/SL уже установлен - пропускаем")
else:
raise
resp = client.set_trading_stop(
category="linear",
symbol=symbol,
takeProfit=str(round(take_profit_price, 5)),
tpTriggerBy="LastPrice",
slTriggerBy="LastPrice",
positionIdx=0,
reduceOnly=False,
tpslMode=tpsl_mode,
)
except Exception as e:
logger.error(f"Ошибка установки TP/SL: {e}")
await message.answer(
"Ошибка при установке Take Profit и Stop Loss.",
reply_markup=inline_markup.back_to_main,
)
return False
else:
logger.warning("Не удалось получить цену ликвидации для позиции")
else: # REGULAR_MARGIN
try:
client.set_tp_sl_mode(symbol=symbol, category="linear", tpSlMode="Full")
except exceptions.InvalidRequestError as e:
if "same tp sl mode" in str(e):
logger.info("Режим TP/SL уже установлен - пропускаем")
else:
raise
if order_type == "Market":
base_price = entry_price
else:
base_price = limit_price
if side.lower() == "buy":
take_profit_price = base_price * (1 + loss_profit / 100)
stop_loss_price = base_price * (1 - loss_profit / 100)
else:
take_profit_price = base_price * (1 - loss_profit / 100)
stop_loss_price = base_price * (1 + loss_profit / 100)
take_profit_price = max(take_profit_price, 0)
stop_loss_price = max(stop_loss_price, 0)
if tpsl_mode == "Full":
tp_order_type = "Market"
sl_order_type = "Market"
tp_limit_price = None
sl_limit_price = None
else: # Partial
tp_order_type = "Limit"
sl_order_type = "Limit"
tp_limit_price = take_profit_price
sl_limit_price = stop_loss_price
response = client.place_order(
category="linear",
symbol=symbol,
side=side,
orderType=order_type,
qty=str(quantity),
price=(
str(limit_price) if order_type == "Limit" and limit_price else None
),
takeProfit=str(take_profit_price),
tpOrderType=tp_order_type,
tpLimitPrice=str(tp_limit_price) if tp_limit_price else None,
stopLoss=str(stop_loss_price),
slOrderType=sl_order_type,
slLimitPrice=str(sl_limit_price) if sl_limit_price else None,
tpslMode=tpsl_mode,
timeInForce="GTC",
orderLinkId=f"deal_{symbol}_{int(time.time())}",
)
if response.get("retCode", -1) == 0:
return True
else:
logger.error(f"Ошибка открытия ордера: {response}")
await message.answer(
f"Ошибка открытия ордера", reply_markup=inline_markup.back_to_main
)
return False
return None
except exceptions.InvalidRequestError as e:
logger.error("InvalidRequestError: %s", e)
error_text = str(e)
if "estimated will trigger liq" in error_text:
await message.answer(
"Лимитный ордер может вызвать мгновенную ликвидацию. Проверьте параметры ордера.",
reply_markup=inline_markup.back_to_main,
)
elif "ab not enough for new order" in error_text:
await message.answer("Недостаточно средств для нового ордера",
reply_markup=inline_markup.back_to_main)
else:
logger.error("Ошибка при совершении сделки: %s", e)
await message.answer(
"Недостаточно средств для размещения нового ордера с заданным количеством и плечом.",
reply_markup=inline_markup.back_to_main,
)
except Exception as e:
logger.error("Ошибка при совершении сделки: %s", e)
await message.answer(
"Возникла ошибка при попытке открыть позицию.",
reply_markup=inline_markup.back_to_main,
)
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)
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("Режим TP/SL уже установлен для %s - пропускаем", 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:
return False
position = positions_list[0]
qty = abs(safe_float(position.get("size")))
side = position.get("side")
if qty == 0:
return False
close_side = "Sell" if side == "Buy" else "Buy"
place_resp = client.place_order(
category="linear",
symbol=symbol,
side=close_side,
orderType="Market",
qty=str(qty),
timeInForce="GTC",
reduceOnly=True,
)
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
async def close_trade_after_delay(tg_id: int, message, symbol: str, delay_sec: int):
"""
Закрывает сделку пользователя после задержки delay_sec секунд.
"""
try:
await asyncio.sleep(delay_sec)
result = await close_user_trade(tg_id, symbol)
if result:
await message.answer(
f"Сделка {symbol} успешно закрыта по таймеру."
)
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

@@ -0,0 +1,52 @@
import app.telegram.database.requests as rq
import app.telegram.Keyboards.inline_keyboards as inline_markup
import logging.config
from logger_helper.logger_helper import LOGGING_CONFIG
from pybit.unified_trading import HTTP
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("balance")
async def get_balance(tg_id: int, message) -> float:
"""
Асинхронно получает общий баланс пользователя на Bybit.
Процедура:
- Получает API ключ и секрет пользователя из базы данных.
- Если ключи не заданы, отправляет пользователю сообщение с предложением подключить платформу.
- Создает клиент Bybit с ключами.
- Запрашивает общий баланс по типу аккаунта UNIFIED.
- Если ответ успешен, возвращает баланс в виде float.
- При ошибках API или исключениях логирует ошибку и уведомляет пользователя.
:param tg_id: int - идентификатор пользователя Telegram
:param message: объект сообщения для отправки ответов пользователю
:return: float - общий баланс пользователя; 0 при ошибке или отсутствии ключей
"""
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
client = HTTP(
api_key=api_key,
api_secret=secret_key
)
if api_key is None or secret_key is None:
await message.answer('⚠️ Подключите платформу для торговли',
reply_markup=inline_markup.connect_bybit_api_message)
return 0
try:
response = client.get_wallet_balance(accountType='UNIFIED')
if response['retCode'] == 0:
total_balance = response['result']['list'][0].get('totalWalletBalance', '0')
return total_balance
else:
logger.error(f"Ошибка API: {response.get('retMsg')}")
await message.answer(f"⚠️ Ошибка API: {response.get('retMsg')}")
return 0
except Exception as e:
logger.error(f"Ошибка при получении общего баланса: {e}")
await message.answer('Ошибка при подключении, повторите попытку', reply_markup=inline_markup.connect_bybit_api_message)
return 0

View File

@@ -0,0 +1,115 @@
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 on_ws_error(ws, error):
logger.error(f"WebSocket internal error: {error}")
# Запланировать переподключение через event loop
if event_loop:
asyncio.run_coroutine_threadsafe(reconnect_ws(ws), event_loop)
def on_ws_close(ws, close_status_code, close_msg):
logger.warning(f"WebSocket closed: {close_status_code} - {close_msg}")
# Запланировать переподключение через event loop
if event_loop:
asyncio.run_coroutine_threadsafe(reconnect_ws(ws), event_loop)
async def reconnect_ws(ws):
logger.info("Запускаем переподключение WebSocket...")
await asyncio.sleep(5)
try:
await ws.run_forever()
except WebSocketConnectionClosedException:
logger.info("WebSocket переподключение успешно завершено.")
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)
logger.info("Callback выполнен.")
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)
logger.info("Callback выполнен.")
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.on_error = on_ws_error
ws.on_close = on_ws_close
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

@@ -0,0 +1,559 @@
import asyncio
import logging.config
from aiogram import F, Router
from app.services.Bybit.functions.bybit_ws import run_ws_for_user
from app.telegram.functions.main_settings.settings import main_settings_message
from logger_helper.logger_helper import LOGGING_CONFIG
from app.services.Bybit.functions.Futures import (close_user_trade, set_take_profit_stop_loss, \
get_active_positions_by_symbol, get_active_orders_by_symbol,
get_active_positions, get_active_orders, cancel_all_tp_sl_orders,
open_position, close_trade_after_delay, safe_float,
)
from app.services.Bybit.functions.balance import get_balance
import app.telegram.Keyboards.inline_keyboards as inline_markup
import app.telegram.database.requests as rq
from aiogram.types import Message, CallbackQuery
from app.services.Bybit.functions.price_symbol import get_price
from app.states.States import (state_update_entry_type, state_update_symbol, state_limit_price,
SetTP_SL_State, CloseTradeTimerState)
from aiogram.fsm.context import FSMContext
from app.services.Bybit.functions.get_valid_symbol import get_valid_symbols
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("functions")
router_functions_bybit_trade = Router()
user_trade_tasks = {}
@router_functions_bybit_trade.callback_query(F.data.in_(['clb_start_trading', 'clb_back_to_main', 'back_to_main']))
async def clb_start_bybit_trade_message(callback: CallbackQuery) -> None:
"""
Обработка нажатия кнопок запуска торговли или возврата в главное меню.
Отправляет информацию о балансе, символе, цене и инструкциях по торговле.
"""
user_id = callback.from_user.id
balance = await get_balance(user_id, callback.message)
if balance:
symbol = await rq.get_symbol(user_id)
price = await get_price(user_id, symbol=symbol)
text = (
f"💎 Торговля на Bybit\n\n"
f"⚖️ Ваш баланс (USDT): {float(balance):.2f}\n"
f"📊 Текущая торговая пара: {symbol}\n"
f"$$$ Цена: {price}\n\n"
"Как начать торговлю?\n\n"
"1⃣ Проверьте и тщательно настройте все параметры в вашем профиле.\n"
"2⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).\n"
"3⃣ Нажмите кнопку 'Начать торговать'.\n"
)
await callback.message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup)
async def start_bybit_trade_message(message: Message) -> None:
"""
Отправляет пользователю информацию о балансе, символе и текущей цене,
вместе с инструкциями по началу торговли.
"""
balance = await get_balance(message.from_user.id, message)
tg_id = message.from_user.id
if balance:
await run_ws_for_user(tg_id, message)
symbol = await rq.get_symbol(message.from_user.id)
price = await get_price(message.from_user.id, symbol=symbol)
text = (
f"💎 Торговля на Bybit\n\n"
f"⚖️ Ваш баланс (USDT): {balance}\n"
f"📊 Текущая торговая пара: {symbol}\n"
f"$$$ Цена: {price}\n\n"
"Как начать торговлю?\n\n"
"1⃣ Проверьте и тщательно настройте все параметры в вашем профиле.\n"
"2⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).\n"
"3⃣ Нажмите кнопку 'Начать торговать'.\n"
)
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup)
@router_functions_bybit_trade.callback_query(F.data == 'clb_update_trading_pair')
async def update_symbol_for_trade_message(callback: CallbackQuery, state: FSMContext) -> None:
"""
Начинает процедуру обновления торговой пары, переводит пользователя в состояние ожидания пары.
"""
await state.set_state(state_update_symbol.symbol)
await callback.answer()
await callback.message.answer(
text='Укажите торговую пару заглавными буквами без пробелов и лишних символов (пример: BTCUSDT): ',
reply_markup=inline_markup.cancel)
@router_functions_bybit_trade.message(state_update_symbol.symbol)
async def update_symbol_for_trade(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод торговой пары пользователем и проверяет её валидность.
При успешном обновлении сохранит пару и отправит обновлённую информацию.
"""
user_input = message.text.strip().upper()
exists = await get_valid_symbols(message.from_user.id, user_input)
if not exists:
await message.answer("Введена некорректная торговая пара или такой пары нет в списке. Попробуйте снова.")
return
await state.update_data(symbol=message.text)
await message.answer('Пара была успешно обновлена')
await rq.update_symbol(message.from_user.id, user_input)
await start_bybit_trade_message(message)
await state.clear()
@router_functions_bybit_trade.callback_query(F.data == 'clb_update_entry_type')
async def update_entry_type_message(callback: CallbackQuery, state: FSMContext) -> None:
"""
Запрашивает у пользователя тип входа в позицию (Market или Limit).
"""
await state.set_state(state_update_entry_type.entry_type)
await callback.message.answer("Выберите тип входа в позицию:", reply_markup=inline_markup.entry_order_type_markup)
await callback.answer()
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith('entry_order_type:'))
async def entry_order_type_callback(callback: CallbackQuery, state: FSMContext) -> None:
"""
Обработка выбора типа входа в позицию.
Если Limit, запрашивает цену лимитного ордера.
Если Market — обновляет настройки.
"""
order_type = callback.data.split(':')[1]
if order_type not in ['Market', 'Limit']:
await callback.answer("Ошибка выбора", show_alert=True)
return
if order_type == 'Limit':
await state.set_state(state_limit_price.price)
await callback.message.answer("Введите цену лимитного ордера:", reply_markup=inline_markup.cancel)
await callback.answer()
return
try:
await state.update_data(entry_order_type=order_type)
await rq.update_entry_order_type(callback.from_user.id, order_type)
await callback.message.answer(f"Выбран тип входа в позицию: {order_type}",
reply_markup=inline_markup.start_trading_markup)
await callback.answer()
except Exception as e:
logger.error(f"Произошла ошибка при обновлении типа входа в позицию: {e}")
await callback.message.answer("Произошла ошибка при обновлении типа входа в позицию",
reply_markup=inline_markup.back_to_main)
await state.clear()
@router_functions_bybit_trade.message(state_limit_price.price)
async def set_limit_price(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод цены лимитного ордера, проверяет формат и сохраняет настройки.
"""
try:
price = float(message.text)
if price <= 0:
await message.answer("Цена должна быть положительным числом. Попробуйте снова.",
reply_markup=inline_markup.cancel)
return
except ValueError:
await message.answer("Некорректный формат цены. Введите число.", reply_markup=inline_markup.cancel)
return
await state.update_data(entry_order_type='Limit', limit_price=price)
await rq.update_entry_order_type(message.from_user.id, 'Limit')
await rq.update_limit_price(message.from_user.id, price)
await message.answer(f"Цена лимитного ордера установлена: {price}", reply_markup=inline_markup.start_trading_markup)
await state.clear()
@router_functions_bybit_trade.callback_query(F.data == "clb_start_chatbot_trading")
async def start_trading_process(callback: CallbackQuery) -> None:
"""
Запускает торговый цикл в выбранном режиме Long/Short.
Проверяет API-ключи, режим торговли, маржинальный режим и открытые позиции,
затем запускает торговый цикл с задержкой или без неё.
"""
await callback.answer()
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')
starting_quantity = safe_float(data_main_stgs.get('starting_quantity'))
switch_state = data_main_stgs.get("switch_state", "По направлению")
if trading_mode == 'Switch':
if switch_state == "По направлению":
side = data_main_stgs.get("last_side")
else:
side = data_main_stgs.get("last_side")
if side.lower() == "buy":
side = "Sell"
else:
side = "Buy"
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)
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 message.answer(f"Торговля начнётся через {timer_minute} мин.", reply_markup=inline_markup.cancel_start)
async def delay_start():
try:
await asyncio.sleep(timer_minute * 60)
await open_position(tg_id, message, side, margin_mode, symbol=symbol, quantity=starting_quantity)
await rq.update_user_timer(tg_id, minutes=0)
except asyncio.exceptions.CancelledError:
logger.exception(f"Торговый цикл для пользователя {tg_id} был отменён.")
raise
task = asyncio.create_task(delay_start())
user_trade_tasks[tg_id] = task
else:
await open_position(tg_id, message, side, margin_mode, symbol=symbol, quantity=starting_quantity)
@router_functions_bybit_trade.callback_query(F.data == "clb_cancel_start")
async def cancel_start_trading(callback: CallbackQuery):
tg_id = callback.from_user.id
task = user_trade_tasks.get(tg_id)
if task and not task.done():
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
user_trade_tasks.pop(tg_id, None)
await rq.update_user_timer(tg_id, minutes=0)
await callback.message.answer("Запуск торговли отменён.", reply_markup=inline_markup.back_to_main)
await callback.message.edit_reply_markup(reply_markup=None)
else:
await callback.answer("Нет запланированной задачи запуска.", show_alert=True)
@router_functions_bybit_trade.callback_query(F.data == "clb_my_deals")
async def show_my_trades(callback: CallbackQuery) -> None:
"""
Отображает пользователю выбор типа сделки по текущей торговой паре.
"""
await callback.answer()
try:
await callback.message.answer(f"Выберите тип сделки:",
reply_markup=inline_markup.my_deals_select_markup)
except Exception as e:
logger.error(f"Произошла ошибка при выборе типа сделки: {e}")
@router_functions_bybit_trade.callback_query(F.data == "clb_open_deals")
async def show_my_trades_callback(callback: CallbackQuery):
"""
Показывает открытые позиции пользователя.
"""
await callback.answer()
try:
await get_active_positions(callback.from_user.id, message=callback.message)
except Exception as e:
logger.error(f"Произошла ошибка при выборе сделки: {e}")
await callback.message.answer("Произошла ошибка при выборе сделки", reply_markup=inline_markup.back_to_main)
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("show_deal_"))
async def show_deal_callback(callback_query: CallbackQuery) -> None:
"""
Показывает сделку пользователя по символу.
"""
await callback_query.answer()
try:
symbol = callback_query.data[len("show_deal_"):]
await rq.update_symbol(callback_query.from_user.id, symbol)
tg_id = callback_query.from_user.id
await get_active_positions_by_symbol(tg_id, symbol, message=callback_query.message)
except Exception as e:
logger.error(f"Произошла ошибка при выборе сделки: {e}")
await callback_query.message.answer("Произошла ошибка при выборе сделки",
reply_markup=inline_markup.back_to_main)
@router_functions_bybit_trade.callback_query(F.data == "clb_open_orders")
async def show_my_orders_callback(callback: CallbackQuery) -> None:
"""
Показывает открытые позиции пользователя по символу.
"""
await callback.answer()
try:
await get_active_orders(callback.from_user.id, message=callback.message)
except Exception as e:
logger.error(f"Произошла ошибка при выборе ордера: {e}")
await callback.message.answer("Произошла ошибка при выборе ордера", reply_markup=inline_markup.back_to_main)
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("show_limit_"))
async def show_limit_callback(callback_query: CallbackQuery) -> None:
"""
Показывает сделку пользователя по символу.
"""
await callback_query.answer()
try:
symbol = callback_query.data[len("show_limit_"):]
await rq.update_symbol(callback_query.from_user.id, symbol)
tg_id = callback_query.from_user.id
await get_active_orders_by_symbol(tg_id, symbol, message=callback_query.message)
except Exception as e:
logger.error(f"Произошла ошибка при выборе сделки: {e}")
await callback_query.message.answer("Произошла ошибка при выборе сделки",
reply_markup=inline_markup.back_to_main)
@router_functions_bybit_trade.callback_query(F.data == "clb_set_tp_sl")
async def set_tp_sl(callback: CallbackQuery, state: FSMContext) -> None:
"""
Запускает процесс установки Take Profit и Stop Loss.
"""
await callback.answer()
await state.set_state(SetTP_SL_State.waiting_for_take_profit)
await callback.message.answer("Введите значение Take Profit (в цене, например 26000.5):",
reply_markup=inline_markup.cancel)
@router_functions_bybit_trade.message(SetTP_SL_State.waiting_for_take_profit)
async def process_take_profit(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод значения Take Profit и запрашивает Stop Loss.
"""
try:
tp = float(message.text.strip())
if tp <= 0:
await message.answer("Значение Take Profit должно быть положительным числом. Попробуйте снова.",
reply_markup=inline_markup.cancel)
return
except ValueError:
await message.answer("Некорректный ввод. Пожалуйста, введите число для Take Profit.",
reply_markup=inline_markup.cancel)
return
await state.update_data(take_profit=tp)
await state.set_state(SetTP_SL_State.waiting_for_stop_loss)
await message.answer("Введите значение Stop Loss (в цене, например 24500.3):", reply_markup=inline_markup.cancel)
@router_functions_bybit_trade.message(SetTP_SL_State.waiting_for_stop_loss)
async def process_stop_loss(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод значения Stop Loss и завершает процесс установки TP/SL.
"""
try:
sl = float(message.text.strip())
if sl <= 0:
await message.answer("Значение Stop Loss должно быть положительным числом. Попробуйте снова.",
reply_markup=inline_markup.cancel)
return
except ValueError:
await message.answer("Некорректный ввод. Пожалуйста, введите число для Stop Loss.",
reply_markup=inline_markup.cancel)
return
data = await state.get_data()
tp = data.get("take_profit")
if tp is None:
await message.answer("Ошибка, не найдено значение Take Profit. Попробуйте снова.")
await state.clear()
return
tg_id = message.from_user.id
await set_take_profit_stop_loss(tg_id, message, take_profit_price=tp, stop_loss_price=sl)
await state.clear()
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_deal:"))
async def close_trade_callback(callback: CallbackQuery) -> None:
"""
Закрывает сделку пользователя по символу.
"""
symbol = callback.data.split(':')[1]
tg_id = callback.from_user.id
result = await close_user_trade(tg_id, symbol)
if result:
logger.info(f"Сделка {symbol} успешно закрыта.")
else:
logger.error(f"Не удалось закрыть сделку {symbol}.")
await callback.message.answer(f"Не удалось закрыть сделку {symbol}.")
await callback.answer()
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_limit:"))
async def close_trade_callback(callback: CallbackQuery) -> None:
"""
Закрывает ордера пользователя по символу.
"""
symbol = callback.data.split(':')[1]
tg_id = callback.from_user.id
result = await cancel_all_tp_sl_orders(tg_id, symbol)
if result:
logger.info(f"Ордер {result} успешно закрыт.")
else:
await callback.message.answer(f"Не удалось закрыть ордер {result}.")
await callback.answer()
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_deal_by_timer:"))
async def ask_close_delay(callback: CallbackQuery, state: FSMContext) -> None:
"""
Запускает диалог с пользователем для задания задержки перед закрытием сделки.
"""
symbol = callback.data.split(":")[1]
await state.update_data(symbol=symbol)
await state.set_state(CloseTradeTimerState.waiting_for_delay)
await callback.message.answer("Введите задержку в минутах до закрытия сделки:",
reply_markup=inline_markup.cancel)
await callback.answer()
@router_functions_bybit_trade.message(CloseTradeTimerState.waiting_for_delay)
async def process_close_delay(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод закрытия сделки с задержкой.
"""
try:
delay_minutes = int(message.text.strip())
if delay_minutes <= 0:
await message.answer("Введите положительное число.")
return
except ValueError:
await message.answer("Некорректный ввод. Введите число в минутах.")
return
data = await state.get_data()
symbol = data.get("symbol")
delay = delay_minutes * 60
await message.answer(f"Закрытие сделки {symbol} запланировано через {delay_minutes} мин.",
reply_markup=inline_markup.back_to_main)
await close_trade_after_delay(message.from_user.id, message, symbol, delay)
await state.clear()
@router_functions_bybit_trade.callback_query(F.data == "clb_change_martingale_reset")
async def reset_martingale(callback: CallbackQuery) -> None:
"""
Сбрасывает шаги мартингейла пользователя.
"""
tg_id = callback.from_user.id
await rq.update_martingale_step(tg_id, 1)
await callback.answer("Сброс шагов выполнен.")
await main_settings_message(tg_id, callback.message)
@router_functions_bybit_trade.callback_query(F.data == "clb_stop_trading")
async def confirm_stop_trading(callback: CallbackQuery):
"""
Предлагает пользователю выбрать вариант подтверждение остановки торговли.
"""
await callback.message.answer(
"Выберите вариант остановки торговли:", reply_markup=inline_markup.stop_choice_markup
)
await callback.answer()
@router_functions_bybit_trade.callback_query(F.data == "stop_immediately")
async def stop_immediately(callback: CallbackQuery):
"""
Останавливает торговлю немедленно.
"""
tg_id = callback.from_user.id
await rq.update_trigger(tg_id, "Ручной")
await callback.message.answer("Автоматическая торговля остановлена.", reply_markup=inline_markup.back_to_main)
await callback.answer()
@router_functions_bybit_trade.callback_query(F.data == "stop_with_timer")
async def stop_with_timer_start(callback: CallbackQuery, state: FSMContext):
"""
Запускает диалог с пользователем для задания задержки до остановки торговли.
"""
await state.set_state(CloseTradeTimerState.waiting_for_trade)
await callback.message.answer("Введите задержку в минутах до остановки торговли:",
reply_markup=inline_markup.cancel)
await callback.answer()
@router_functions_bybit_trade.message(CloseTradeTimerState.waiting_for_trade)
async def process_stop_delay(message: Message, state: FSMContext):
"""
Обрабатывает ввод задержки и запускает задачу остановки торговли с задержкой.
"""
try:
delay_minutes = int(message.text.strip())
if delay_minutes <= 0:
await message.answer("Введите положительное число минут.")
return
except ValueError:
await message.answer("Некорректный формат. Введите число в минутах.")
return
tg_id = message.from_user.id
delay_seconds = delay_minutes * 60
await message.answer(f"Торговля будет остановлена через {delay_minutes} минут.",
reply_markup=inline_markup.back_to_main)
await asyncio.sleep(delay_seconds)
await rq.update_trigger(tg_id, "Ручной")
await message.answer("Автоматическая торговля остановлена.", reply_markup=inline_markup.back_to_main)
await state.clear()
@router_functions_bybit_trade.callback_query(F.data == "clb_cancel")
async def cancel(callback: CallbackQuery, state: FSMContext) -> None:
"""
Отменяет текущее состояние FSM и сообщает пользователю об отмене.
"""
await state.clear()
await callback.message.answer("Отменено!", reply_markup=inline_markup.back_to_main)
await callback.answer()

View File

@@ -0,0 +1,40 @@
import logging.config
from pybit.unified_trading import HTTP
import app.telegram.database.requests as rq
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("get_valid_symbol")
async def get_valid_symbols(user_id: int, symbol: str) -> bool:
"""
Проверяет существование торговой пары на Bybit в категории 'linear'.
Эта функция получает API-ключи пользователя из базы данных и
с помощью Bybit API проверяет наличие данного символа в списке
торговых инструментов категории 'linear'.
Args:
user_id (int): Идентификатор пользователя Telegram.
symbol (str): Торговый символ (валютная пара), например "BTCUSDT".
Returns:
bool: Возвращает True, если торговая пара существует, иначе False.
Raises:
Исключения подавляются и вызывается False, если произошла ошибка запроса к API.
"""
api_key = await rq.get_bybit_api_key(user_id)
secret_key = await rq.get_bybit_secret_key(user_id)
client = HTTP(api_key=api_key, api_secret=secret_key)
try:
resp = client.get_instruments_info(category='linear', symbol=symbol)
# Проверка наличия результата и непустого списка инструментов
if resp.get('retCode') == 0 and resp.get('result') and resp['result'].get('list'):
return len(resp['result']['list']) > 0
return False
except Exception as e:
logging.error(f"Ошибка при получении списка инструментов: {e}")
return False

View File

@@ -0,0 +1,52 @@
import math
import logging.config
from app.services.Bybit.functions.price_symbol import get_price
import app.telegram.database.requests as rq
from logger_helper.logger_helper import LOGGING_CONFIG
from pybit.unified_trading import HTTP
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("min_qty")
def round_up_qty(value: float, step: float) -> float:
"""
Округление value вверх до ближайшего кратного step.
"""
return math.ceil(value / step) * step
async def get_min_qty(tg_id: int) -> float:
"""
Получает минимальный объем (количество) ордера для символа пользователя на Bybit,
округленное с учетом шага количества qtyStep.
:param tg_id: int - идентификатор пользователя Telegram
:return: float - минимальное количество лота для ордера
"""
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
symbol = await rq.get_symbol(tg_id)
client = HTTP(api_key=api_key, api_secret=secret_key)
price = await get_price(tg_id, symbol=symbol)
response = client.get_instruments_info(symbol=symbol, category='linear')
instrument = response['result'][0]
lot_size_filter = instrument.get('lotSizeFilter', {})
min_order_qty = float(lot_size_filter.get('minOrderQty', 0))
min_notional_value = float(lot_size_filter.get('minNotionalValue', 0))
qty_step = float(lot_size_filter.get('qtyStep', 1))
calculated_qty = (5 / price) * 1.1
min_qty = max(min_order_qty, calculated_qty)
min_qty_rounded = round_up_qty(min_qty, qty_step)
logger.debug(f"tg_id={tg_id}: price={price}, min_order_qty={min_order_qty}, "
f"min_notional_value={min_notional_value}, qty_step={qty_step}, "
f"calculated_qty={calculated_qty}, min_qty_rounded={min_qty_rounded}")
return min_qty_rounded

View File

@@ -0,0 +1,32 @@
import app.telegram.database.requests as rq
import logging.config
from logger_helper.logger_helper import LOGGING_CONFIG
from pybit import exceptions
from pybit.unified_trading import HTTP
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("price_symbol")
async def get_price(tg_id: int, symbol: str) -> float:
"""
Асинхронно получает текущую цену символа пользователя на Bybit.
:param tg_id: int - ID пользователя Telegram
:return: float - текущая цена символа
"""
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
client = HTTP(
api_key=api_key,
api_secret=secret_key
)
try:
price = float(
client.get_tickers(category='linear', symbol=symbol).get('result').get('list')[0].get('ask1Price'))
return price
except exceptions.InvalidRequestError as e:
logger.error(f"Ошибка при получении цены: {e}")
return 1.0

69
app/states/States.py Normal file
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,64 +1,119 @@
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
start_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Зарегистрироваться", callback_data="callback_registration")],
[InlineKeyboardButton(text="Авторизоваться", callback_data="callback_autorisation")]
[InlineKeyboardButton(text="🔥 Начать торговлю", callback_data="clb_start_chatbot_message")]
])
settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Настройки", callback_data='clb_settings_message')]
[InlineKeyboardButton(text="Запуск", callback_data='clb_start_trading')]
])
back_btn_profile = [InlineKeyboardButton(text="Назад", callback_data='callback_autorisation')]
cancel_start = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Отменить запуск", callback_data="clb_cancel_start")]
])
back_btn_list_settings = [InlineKeyboardButton(text="Назад",
callback_data='clb_back_to_special_settings_message')] # Кнопка для возврата к списку каталога настроек
back_btn_list_settings_markup = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Назад",
callback_data='clb_back_to_special_settings_message')]]) # Клавиатура для возврата к списку каталога настроек
back_btn_to_main = [InlineKeyboardButton(text="На главную", callback_data='clb_back_to_main')]
back_btn_profile = [InlineKeyboardButton(text="Назад", callback_data='clb_start_chatbot_message')]
connect_bybit_api_message = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api_message')]
])
special_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Основные настройки", callback_data='clb_change_main_settings'),
InlineKeyboardButton(text="Риск-менеджмент", callback_data='clb_change_risk_management_settings')],
[InlineKeyboardButton(text="Условия запуска", callback_data='clb_change_condition_settings'),
InlineKeyboardButton(text="Дополнительные параметры", callback_data='clb_change_additional_settings')],
back_btn_profile
[InlineKeyboardButton(text="Условия запуска", callback_data='clb_change_condition_settings')],
# InlineKeyboardButton(text="Дополнительные параметры", callback_data='clb_change_additional_settings')],
[InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api_message')],
back_btn_to_main
])
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')]]) # Клавиатура для возврата к списку каталога настроек
connect_bybit_api_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api')]
])
trading_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Настройки", callback_data='clb_settings_message')],
[InlineKeyboardButton(text="Мои сделки", callback_data='clb_my_deals')],
[InlineKeyboardButton(text="Указать торговую пару", callback_data='clb_update_trading_pair')],
[InlineKeyboardButton(text="Начать торговать", callback_data='clb_update_entry_type')],
[InlineKeyboardButton(text="Остановить торговлю", callback_data='clb_stop_trading')],
])
start_trading_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Начать торговлю", callback_data="clb_start_chatbot_trading")],
[InlineKeyboardButton(text="На главную", callback_data='back_to_main')],
])
cancel = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Отменить", callback_data="clb_cancel")]
])
entry_order_type_markup = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Market (текущая цена)", callback_data="entry_order_type:Market"),
InlineKeyboardButton(text="Limit (фиксированная цена)", callback_data="entry_order_type:Limit"),
], back_btn_to_main
]
)
back_to_main = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="На главную", callback_data='back_to_main')],
])
main_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_trading_mode'),
InlineKeyboardButton(text='Состояние свитча', callback_data='clb_change_switch_state'),
InlineKeyboardButton(text='Тип маржи', callback_data='clb_change_margin_type')],
[InlineKeyboardButton(text='Размер кредитного плеча', callback_data='clb_change_size_leverage'),
InlineKeyboardButton(text='Начальная ставка', callback_data='clb_change_starting_quantity')],
[InlineKeyboardButton(text='Коэффициент Мартингейла', callback_data='clb_change_martingale_factor'),
InlineKeyboardButton(text='Максимльное кол-во ставок', callback_data='clb_change_maximum_quantity')],
InlineKeyboardButton(text='Сбросить шаги Мартингейла', callback_data='clb_change_martingale_reset')],
[InlineKeyboardButton(text='Максимальное кол-во ставок', callback_data='clb_change_maximum_quantity')],
back_btn_list_settings
back_btn_list_settings,
back_btn_to_main
])
risk_management_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Изм. цены прибыли', callback_data='clb_change_price_profit'),
InlineKeyboardButton(text='Изм. цены убытков', callback_data='clb_change_price_loss')],
[InlineKeyboardButton(text='Иакс. риск на сделку', callback_data='clb_change_max_risk_deal')],
[InlineKeyboardButton(text='Макс. риск на сделку', callback_data='clb_change_max_risk_deal')],
[InlineKeyboardButton(text='Учитывать комиссию биржи (Да/Нет)', callback_data='commission_fee')],
back_btn_list_settings
back_btn_list_settings,
back_btn_to_main
])
condition_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Триггер', callback_data='clb_change_trigger'),
InlineKeyboardButton(text='Фильтр времени', callback_data='clb_change_filter_time')],
[InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_mode'),
InlineKeyboardButton(text='Таймер', callback_data='clb_change_timer')],
#
# [InlineKeyboardButton(text='Фильтр волатильности', callback_data='clb_change_filter_volatility'),
# InlineKeyboardButton(text='Внешние сигналы', callback_data='clb_change_external_cues')],
#
# [InlineKeyboardButton(text='Сигналы TradingView', callback_data='clb_change_tradingview_cues'),
# InlineKeyboardButton(text='Webhook URL', callback_data='clb_change_webhook')],
#
# [InlineKeyboardButton(text='AI - аналитика', callback_data='clb_change_ai_analytics')],
[InlineKeyboardButton(text='Фильтр волатильности', callback_data='clb_change_filter_volatility'),
InlineKeyboardButton(text='Внешние сигналы', callback_data='clb_change_external_cues')],
[InlineKeyboardButton(text='Сигналы TradingView', callback_data='clb_change_tradingview_cues'),
InlineKeyboardButton(text='Webhook URL', callback_data='clb_change_webhook')],
[InlineKeyboardButton(text='AI - аналитика', callback_data='clb_change_ai_analytics')],
back_btn_list_settings
back_btn_list_settings,
back_btn_to_main
])
additional_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
@@ -67,22 +122,96 @@ additional_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Уведомления', callback_data='clb_change_notifications')],
back_btn_list_settings
back_btn_list_settings,
back_btn_to_main
])
trading_mode_markup = InlineKeyboardMarkup(inline_keyboard=[
[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_switch"),
InlineKeyboardButton(text="Смарт", callback_data="trade_mode_smart")],
back_btn_list_settings
back_btn_list_settings,
back_btn_to_main
])
margin_type_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Изолированный", callback_data="margin_type_isolated"),
InlineKeyboardButton(text="Кросс", callback_data="margin_type_cross")],
InlineKeyboardButton(text="Кросс", callback_data="margin_type_cross")],
back_btn_list_settings
])
trigger_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE
[InlineKeyboardButton(text='Ручной', callback_data="clb_trigger_manual")],
# [InlineKeyboardButton(text='TradingView', callback_data="clb_trigger_tradingview")],
[InlineKeyboardButton(text="Автоматический", callback_data="clb_trigger_auto")],
back_btn_list_settings,
back_btn_to_main
])
buttons_yes_no_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Да', callback_data="clb_yes"),
InlineKeyboardButton(text='Нет', callback_data="clb_no")],
])
buttons_on_off_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE
[InlineKeyboardButton(text='Включить', callback_data="clb_on"),
InlineKeyboardButton(text='Выключить', callback_data="clb_off")]
])
my_deals_select_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Открытые сделки', callback_data="clb_open_deals"),
InlineKeyboardButton(text='Лимитные ордера', callback_data="clb_open_orders")],
back_btn_to_main
])
def create_trades_inline_keyboard(trades):
builder = InlineKeyboardBuilder()
for trade in trades:
builder.button(text=trade, callback_data=f"show_deal_{trade}")
builder.adjust(2)
return builder.as_markup()
def create_trades_inline_keyboard_limits(trades):
builder = InlineKeyboardBuilder()
for trade in trades:
builder.button(text=trade, callback_data=f"show_limit_{trade}")
builder.adjust(2)
return builder.as_markup()
def create_close_deal_markup(symbol: str) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Закрыть сделку", callback_data=f"close_deal:{symbol}")],
[InlineKeyboardButton(text="Закрыть по таймеру", callback_data=f"close_deal_by_timer:{symbol}")],
[InlineKeyboardButton(text="Установить TP/SL", callback_data="clb_set_tp_sl")],
back_btn_to_main
])
def create_close_limit_markup(symbol: str) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Закрыть лимитный ордер", callback_data=f"close_limit:{symbol}")],
back_btn_to_main
])
timer_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Установить таймер", callback_data="clb_set_timer")],
[InlineKeyboardButton(text="Удалить таймер", callback_data="clb_delete_timer")],
back_btn_to_main
])
stop_choice_markup = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Остановить сразу", callback_data="stop_immediately"),
InlineKeyboardButton(text="Остановить по таймеру", callback_data="stop_with_timer"),
]
]
)
switch_state_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='По направлению', callback_data="clb_long_switch"),
InlineKeyboardButton(text='Против направления', callback_data="clb_short_switch")],
])

View File

@@ -3,17 +3,4 @@
base_buttons_markup = ReplyKeyboardMarkup(keyboard=[
[KeyboardButton(text="👤 Профиль")],
# [KeyboardButton(text="Настройки")]
], resize_keyboard=True)
trigger_markup = ReplyKeyboardMarkup(keyboard=[ # ИЗМЕНИТЬ НА INLINE
[KeyboardButton(text='Ручной'), KeyboardButton(text='TradingView')],
[KeyboardButton(text="Автоматический")]
], resize_keyboard=True)
buttons_yes_no_markup = ReplyKeyboardMarkup(keyboard=[ # ИЗМЕНИТЬ НА INLINE
[KeyboardButton(text='Да'), KeyboardButton(text='Нет')]
], resize_keyboard=True)
buttons_on_off_markup = ReplyKeyboardMarkup(keyboard=[ # ИЗМЕНИТЬ НА INLINE
[KeyboardButton(text='Включить'), KeyboardButton(text='Выключить')]
], resize_keyboard=True)
], resize_keyboard=True, one_time_keyboard=False)

View File

@@ -1,80 +1,207 @@
import logging
logger = logging.getLogger(__name__)
from datetime import datetime
import logging.config
from sqlalchemy.sql.sqltypes import DateTime, Numeric
from sqlalchemy import BigInteger, Boolean, Integer, String, ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy.ext.asyncio import AsyncAttrs, async_sessionmaker, create_async_engine
from logger_helper.logger_helper import LOGGING_CONFIG
from sqlalchemy import select, insert
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("models")
engine = create_async_engine(url='sqlite+aiosqlite:///db.sqlite3')
async_session = async_sessionmaker(engine)
class Base(AsyncAttrs, DeclarativeBase):
"""Базовый класс для declarative моделей SQLAlchemy с поддержкой async."""
pass
class User_Telegram_Id(Base):
"""
Модель таблицы user_telegram_id.
Хранит идентификаторы Telegram пользователей.
Атрибуты:
id (int): Внутренний первичный ключ записи.
tg_id (int): Уникальный идентификатор пользователя Telegram.
"""
__tablename__ = 'user_telegram_id'
id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(BigInteger)
class User_Bybit_API(Base):
"""
Модель таблицы user_bybit_api.
Хранит API ключи и секреты Bybit для каждого Telegram пользователя.
Атрибуты:
id (int): Внутренний первичный ключ записи.
tg_id (int): Внешний ключ на Telegram пользователя (user_telegram_id.tg_id).
api_key (str): API ключ Bybit (уникальный для пользователя).
secret_key (str): Секретный ключ Bybit (уникальный для пользователя).
"""
__tablename__ = 'user_bybit_api'
id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
api_key = mapped_column(String(18), unique=True, nullable=True)
secret_key = mapped_column(String(36), unique=True, nullable=True)
class User_Symbol(Base):
"""
Модель таблицы user_main_settings.
Хранит основные настройки торговли для пользователя.
"""
__tablename__ = 'user_symbols'
id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
symbol = mapped_column(String(18), default='PENGUUSDT')
class Trading_Mode(Base):
"""
Справочник доступных режимов торговли.
Атрибуты:
id (int): Первичный ключ.
mode (str): Уникальный режим (например, 'Long', 'Short', 'Switch).
"""
__tablename__ = 'trading_modes'
id: Mapped[int] = mapped_column(primary_key=True)
mode = mapped_column(String(10), unique=True)
class Margin_type(Base):
"""
Справочник типов маржинальной торговли.
Атрибуты:
id (int): Первичный ключ.
type (str): Тип маржи (например, 'Isolated', 'Cross').
"""
__tablename__ = 'margin_types'
id: Mapped[int] = mapped_column(primary_key=True)
type = mapped_column(String(15), unique=True)
class Trigger(Base):
"""
Справочник триггеров для сделок.
Атрибуты:
id (int): Первичный ключ.
"""
__tablename__ = 'triggers'
id: Mapped[int] = mapped_column(primary_key=True)
trigger = mapped_column(String(15), unique=True)
trigger_price = mapped_column(Integer(), default=0)
class User_Main_Settings(Base):
"""
Основные настройки пользователя для торговли.
Атрибуты:
id (int): Первичный ключ.
tg_id (int): Внешний ключ на Telegram пользователя.
trading_mode (str): Режим торговли, FK на trading_modes.mode.
margin_type (str): Тип маржи, FK на margin_types.type.
size_leverage (int): Кредитное плечо.
starting_quantity (int): Начальный объем позиции.
martingale_factor (int): Коэффициент мартингейла.
martingale_step (int): Текущий шаг мартингейла.
maximal_quantity (int): Максимальное число шагов мартингейла.
entry_order_type (str): Тип ордера входа (Market/Limit).
limit_order_price (Optional[str]): Цена лимитного ордера, если есть.
last_side (str): Последняя сторона ордера.
"""
__tablename__ = 'user_main_settings'
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"), unique=True, nullable=True)
trading_mode = mapped_column(ForeignKey("trading_modes.mode"))
margin_type = mapped_column(ForeignKey("margin_types.type"))
switch_state = mapped_column(String(10), default='По направлению')
size_leverage = mapped_column(Integer(), default=1)
starting_quantity = mapped_column(Integer(), default=1)
martingale_factor = mapped_column(Integer(), default=1)
martingale_step = mapped_column(Integer(), default=1)
maximal_quantity = mapped_column(Integer(), default=10)
entry_order_type = mapped_column(String(10), default='Market')
limit_order_price = mapped_column(Numeric(18, 15), nullable=True)
last_side = mapped_column(String(10), default='Buy')
class User_Risk_Management_Settings(Base):
"""
Настройки управления рисками пользователя.
Атрибуты:
id (int): Первичный ключ.
tg_id (int): Внешний ключ на Telegram пользователя.
price_profit (int): Процент прибыли для трейда.
price_loss (int): Процент убытка для трейда.
max_risk_deal (int): Максимально допустимый риск по сделке в процентах.
commission_fee (str): Учитывать ли комиссию в расчетах ("Да"/"Нет").
"""
__tablename__ = 'user_risk_management_settings'
id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"))
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
price_profit = mapped_column(Integer(), default=1)
price_loss = mapped_column(Integer(), default=1)
max_risk_deal = mapped_column(Integer(), default=1)
max_risk_deal = mapped_column(Integer(), default=100)
commission_fee = mapped_column(String(), default="Да")
class User_Condition_Settings(Base):
"""
Дополнительные пользовательские условия для торговли.
Атрибуты:
id (int): Первичный ключ.
tg_id (int): Внешний ключ на Telegram пользователя.
trigger (str): Тип триггера, FK на triggers.trigger.
filter_time (str): Временной фильтр.
filter_volatility (bool): Фильтр по волатильности.
external_cues (bool): Внешние сигналы.
tradingview_cues (bool): Сигналы TradingView.
webhook (str): URL webhook.
ai_analytics (bool): Использование AI для аналитики.
"""
__tablename__ = 'user_condition_settings'
id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"))
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
trigger = mapped_column(ForeignKey("triggers.trigger"))
trigger = mapped_column(String(15), default='Автоматический')
filter_time = mapped_column(String(25), default='???')
filter_volatility = mapped_column(Boolean, default=False)
external_cues = mapped_column(Boolean, default=False)
@@ -82,18 +209,77 @@ class User_Condition_Settings(Base):
webhook = mapped_column(String(40), default='')
ai_analytics = mapped_column(Boolean, default=False)
class User_Additional_Settings(Base):
"""
Прочие дополнительные настройки пользователя.
Атрибуты:
id (int): Первичный ключ.
tg_id (int): Внешний ключ на Telegram пользователя.
pattern_save (bool): Сохранять ли шаблоны.
autostart (bool): Автоматический запуск.
notifications (bool): Получение уведомлений.
"""
__tablename__ = 'user_additional_settings'
id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"))
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
pattern_save = mapped_column(Boolean, default=False)
autostart = mapped_column(Boolean, default=False)
notifications = mapped_column(Boolean, default=False)
class USER_DEALS(Base):
"""
Таблица сделок пользователя.
Атрибуты:
id (int): Первичный ключ.
tg_id (int): Внешний ключ на Telegram пользователя.
symbol (str): Торговая пара.
side (str): Направление сделки (Buy/Sell).
open_price (int): Цена открытия.
positive_percent (int): Процент доходности.
"""
__tablename__ = 'user_deals'
id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
symbol = mapped_column(String(18), default='PENGUUSDT')
side = mapped_column(String(10), nullable=False)
open_price = mapped_column(Integer(), nullable=False)
positive_percent = mapped_column(Integer(), nullable=False)
class UserTimer(Base):
"""
Таймер пользователя для отсроченного запуска сделок.
Атрибуты:
id (int): Первичный ключ.
tg_id (int): Внешний ключ на Telegram пользователя.
timer_minutes (int): Количество минут таймера.
timer_start (datetime): Время начала таймера.
timer_end (Optional[datetime]): Время окончания таймера (если установлено).
"""
__tablename__ = 'user_timers'
id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
timer_minutes = mapped_column(Integer, nullable=False, default=0)
timer_start = mapped_column(DateTime, default=datetime.utcnow)
timer_end = mapped_column(DateTime, nullable=True)
async def async_main():
"""
Асинхронное создание всех таблиц и заполнение справочников начальными данными.
"""
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
@@ -105,16 +291,16 @@ async def async_main():
logger.info("Заполение таблицы режима торговли")
await conn.execute(Trading_Mode.__table__.insert().values(mode=mode))
types = ['Изолированный', 'Кросс']
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))
last_side = ['Buy', 'Sell']
for side in last_side:
result = await conn.execute(select(User_Main_Settings).where(User_Main_Settings.last_side == side))
if not result.first():
logger.info("Заполение таблицы триггеров")
await conn.execute(Trigger.__table__.insert().values(trigger=trigger))
logger.info("Заполение таблицы последнего направления")
await conn.execute(User_Main_Settings.__table__.insert().values(last_side=side))

View File

@@ -1,33 +1,93 @@
import logging
logger = logging.getLogger(__name__)
import logging.config
from app.telegram.database.models import async_session
from app.telegram.database.models import User_Telegram_Id as UTi
from app.telegram.database.models import User_Main_Settings as UMS
from app.telegram.database.models import User_Risk_Management_Settings as URMS
from app.telegram.database.models import User_Condition_Settings as UCS
from app.telegram.database.models import User_Additional_Settings as UAS
from app.telegram.database.models import Trading_Mode
from app.telegram.database.models import Margin_type
from app.telegram.database.models import Trigger
from logger_helper.logger_helper import LOGGING_CONFIG
from datetime import datetime, timedelta
from typing import Any
import app.telegram.functions.functions as func # functions
from app.telegram.database.models import (
async_session,
User_Telegram_Id as UTi,
User_Main_Settings as UMS,
User_Bybit_API as UBA,
User_Symbol,
User_Risk_Management_Settings as URMS,
User_Condition_Settings as UCS,
User_Additional_Settings as UAS,
Trading_Mode,
Margin_type,
Trigger,
USER_DEALS,
UserTimer,
)
from sqlalchemy import select, delete, update
from sqlalchemy import select, update
# SET_DB
async def save_tg_id_new_user(tg_id):
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("requests")
# --- Функции сохранения в БД ---
async def save_tg_id_new_user(tg_id) -> None:
"""
Сохраняет Telegram ID нового пользователя в базу, если такого ещё нет.
Args:
tg_id (int): Telegram ID пользователя.
"""
async with async_session() as session:
user = await session.scalar(select(UTi).where(UTi.tg_id == tg_id))
if not user:
session.add(UTi(tg_id=tg_id))
logger.info("Новый пользователь был добавлен в бд")
logger.info("Новый пользователь был добавлен в бд %s", tg_id)
await session.commit()
async def set_new_user_bybit_api(tg_id) -> None:
"""
Создаёт запись API пользователя Bybit, если её ещё нет.
Args:
tg_id (int): Telegram ID пользователя.
"""
async with async_session() as session:
user = await session.scalar(select(UBA).where(UBA.tg_id == tg_id))
if not user:
session.add(UBA(tg_id=tg_id))
await session.commit()
async def set_new_user_symbol(tg_id) -> None:
"""
Создаёт запись торгового символа пользователя, если её нет.
Args:
tg_id (int): Telegram ID пользователя.
"""
async with async_session() as session:
user = await session.scalar(select(User_Symbol).where(User_Symbol.tg_id == tg_id))
if not user:
session.add(User_Symbol(tg_id=tg_id))
logger.info(f"Symbol был успешно добавлен %s", tg_id)
await session.commit()
async def set_new_user_default_main_settings(tg_id, trading_mode, margin_type) -> None:
"""
Создаёт основные настройки пользователя по умолчанию.
Args:
tg_id (int): Telegram ID пользователя.
trading_mode (str): Режим торговли.
margin_type (str): Тип маржи.
"""
async with async_session() as session:
settings = await session.scalar(select(UMS).where(UMS.tg_id == tg_id))
@@ -38,11 +98,18 @@ async def set_new_user_default_main_settings(tg_id, trading_mode, margin_type) -
margin_type=margin_type,
))
logger.info("Основные настройки нового пользователя были заполнены")
logger.info("Основные настройки нового пользователя были заполнены%s", tg_id)
await session.commit()
async def set_new_user_default_risk_management_settings(tg_id) -> None:
"""
Создаёт настройки риск-менеджмента по умолчанию.
Args:
tg_id (int): Telegram ID пользователя.
"""
async with async_session() as session:
settings = await session.scalar(select(URMS).where(URMS.tg_id == tg_id))
@@ -51,11 +118,19 @@ async def set_new_user_default_risk_management_settings(tg_id) -> None:
tg_id=tg_id
))
logger.info("Риск-Менеджмент настройки нового пользователя были заполнены")
logger.info("Риск-Менеджмент настройки нового пользователя были заполнены %s", tg_id)
await session.commit()
async def set_new_user_default_condition_settings(tg_id, trigger) -> None:
"""
Создаёт условные настройки по умолчанию.
Args:
tg_id (int): Telegram ID пользователя.
trigger (Any): Значение триггера по умолчанию.
"""
async with async_session() as session:
settings = await session.scalar(select(UCS).where(UCS.tg_id == tg_id))
@@ -65,11 +140,18 @@ async def set_new_user_default_condition_settings(tg_id, trigger) -> None:
trigger=trigger
))
logger.info("Условные настройки нового пользователя были заполнены")
logger.info("Условные настройки нового пользователя были заполнены %s", tg_id)
await session.commit()
async def set_new_user_default_additional_settings(tg_id) -> None:
"""
Создаёт дополнительные настройки по умолчанию.
Args:
tg_id (int): Telegram ID пользователя.
"""
async with async_session() as session:
settings = await session.scalar(select(UAS).where(UAS.tg_id == tg_id))
@@ -78,97 +160,426 @@ async def set_new_user_default_additional_settings(tg_id) -> None:
tg_id=tg_id,
))
logger.info("Дополнительные настройки нового пользователя были заполнены")
logger.info("Дополнительные настройки нового пользователя были заполнены %s", tg_id)
await session.commit()
# GET_DB
# --- Функции получения данных из БД ---
async def check_user(tg_id):
"""
Проверяет наличие пользователя в базе.
Args:
tg_id (int): Telegram ID пользователя.
Returns:
Optional[UTi]: Пользователь или None.
"""
async with async_session() as session:
user = await session.scalar(select(UTi).where(UTi.tg_id == tg_id))
return user
async def get_for_registration_trading_mode():
async with async_session() as session:
mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.id == 1))
return mode
async def get_for_registration_margin_type():
async def get_bybit_api_key(tg_id):
"""Получить API ключ Bybit пользователя."""
async with async_session() as session:
type = await session.scalar(select(Margin_type.type).where(Margin_type.id == 1))
return type
api_key = await session.scalar(select(UBA.api_key).where(UBA.tg_id == tg_id))
return api_key
async def get_for_registration_trigger():
async def get_bybit_secret_key(tg_id):
"""Получить секретный ключ Bybit пользователя."""
async with async_session() as session:
trigger = await session.scalar(select(Trigger.trigger).where(Trigger.id == 1))
return trigger
secret_key = await session.scalar(select(UBA.secret_key).where(UBA.tg_id == tg_id))
return secret_key
async def get_user_main_settings(tg_id):
async def get_symbol(tg_id):
"""Получить символ пользователя."""
async with async_session() as session:
user = await session.scalar(select(UMS).where(UMS.tg_id == tg_id))
symbol = await session.scalar(select(User_Symbol.symbol).where(User_Symbol.tg_id == tg_id))
return symbol
async def get_user_trades(tg_id):
"""Получить сделки пользователя."""
async with async_session() as session:
query = select(USER_DEALS.symbol, USER_DEALS.side).where(USER_DEALS.tg_id == tg_id)
result = await session.execute(query)
trades = result.all()
return trades
async def get_entry_order_type(tg_id: object) -> str | None | Any:
"""Получить тип входного ордера пользователя."""
async with async_session() as session:
order_type = await session.scalar(
select(UMS.entry_order_type).where(UMS.tg_id == tg_id)
)
# Если в базе не установлен тип — возвращаем значение по умолчанию
return order_type or 'Market'
# --- Функции обновления данных ---
async def update_user_trades(tg_id, **kwargs):
"""Обновить сделки пользователя."""
async with async_session() as session:
query = update(USER_DEALS).where(USER_DEALS.tg_id == tg_id).values(**kwargs)
await session.execute(query)
await session.commit()
async def update_symbol(tg_id: int, symbol: str) -> None:
"""Обновить торговый символ пользователя."""
async with async_session() as session:
await session.execute(update(User_Symbol).where(User_Symbol.tg_id == tg_id).values(symbol=symbol))
await session.commit()
async def upsert_api_keys(tg_id: int, api_key: str, secret_key: str) -> None:
"""Обновить API ключ пользователя."""
async with async_session() as session:
result = await session.execute(select(UBA).where(UBA.tg_id == tg_id))
user = result.scalars().first()
if user:
logger.info("Получение основных настроек пользователя")
if api_key is not None:
user.api_key = api_key
if secret_key is not None:
user.secret_key = secret_key
logger.info(f"Обновлены ключи для пользователя {tg_id}")
else:
new_user = UBA(tg_id=tg_id, api_key=api_key, secret_key=secret_key)
session.add(new_user)
logger.info(f"Добавлен новый пользователь {tg_id} с ключами")
await session.commit()
trading_mode = await session.scalar(select(UMS.trading_mode).where(UMS.tg_id == tg_id))
margin_mode = await session.scalar(select(UMS.margin_type).where(UMS.tg_id == tg_id))
size_leverage = await session.scalar(select(UMS.size_leverage).where(UMS.tg_id == tg_id))
starting_quantity = await session.scalar(select(UMS.starting_quantity).where(UMS.tg_id == tg_id))
martingale_factor = await session.scalar(select(UMS.martingale_factor).where(UMS.tg_id == tg_id))
maximal_quantity = await session.scalar(select(UMS.maximal_quantity).where(UMS.tg_id == tg_id))
data = {
'trading_mode': trading_mode,
'margin_type': margin_mode,
'size_leverage': size_leverage,
'starting_quantity': starting_quantity,
'martingale_factor': martingale_factor,
'maximal_quantity': maximal_quantity
}
# --- Более мелкие обновления и запросы по настройкам ---
return data
# UPDATE_DB
async def update_trade_mode_user(tg_id, trading_mode) -> None:
"""Обновить режим торговли пользователя."""
async with async_session() as session:
mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.mode == trading_mode))
if mode:
logger.info("Изменен трейд мод")
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(trading_mode = mode))
logger.info("Изменён торговый режим для пользователя %s", tg_id)
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(trading_mode=mode))
await session.commit()
async def delete_user_trade(tg_id: int, symbol: str):
"""Удалить сделку пользователя."""
async with async_session() as session:
await session.execute(
USER_DEALS.__table__.delete().where(
(USER_DEALS.tg_id == tg_id) & (USER_DEALS.symbol == symbol)
)
)
await session.commit()
async def get_for_registration_trading_mode():
"""Получить режим торговли по умолчанию."""
async with async_session() as session:
mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.id == 1))
return mode
async def get_for_registration_margin_type():
"""Получить тип маржи по умолчанию."""
async with async_session() as session:
type = await session.scalar(select(Margin_type.type).where(Margin_type.id == 1))
return type
async def get_for_registration_trigger(tg_id):
"""Получить триггер по умолчанию."""
async with async_session() as session:
trigger = await session.scalar(select(UCS.trigger).where(tg_id == tg_id))
return trigger
async def get_user_main_settings(tg_id):
"""Получить основные настройки пользователя."""
async with async_session() as session:
user = await session.scalar(select(UMS).where(UMS.tg_id == tg_id))
if user:
data = {
'trading_mode': user.trading_mode,
'margin_type': user.margin_type,
'switch_state': user.switch_state,
'size_leverage': user.size_leverage,
'starting_quantity': user.starting_quantity,
'martingale_factor': user.martingale_factor,
'maximal_quantity': user.maximal_quantity,
'entry_order_type': user.entry_order_type,
'limit_order_price': user.limit_order_price,
'martingale_step': user.martingale_step,
'last_side': user.last_side,
}
return data
async def get_user_risk_management_settings(tg_id):
"""Получить риск-менеджмента настройки пользователя."""
async with async_session() as session:
user = await session.scalar(select(URMS).where(URMS.tg_id == tg_id))
if user:
logger.info("Получение риск-менеджмента настроек пользователя %s", tg_id)
price_profit = await session.scalar(select(URMS.price_profit).where(URMS.tg_id == tg_id))
price_loss = await session.scalar(select(URMS.price_loss).where(URMS.tg_id == tg_id))
max_risk_deal = await session.scalar(select(URMS.max_risk_deal).where(URMS.tg_id == tg_id))
commission_fee = await session.scalar(select(URMS.commission_fee).where(URMS.tg_id == tg_id))
data = {
'price_profit': price_profit,
'price_loss': price_loss,
'max_risk_deal': max_risk_deal,
'commission_fee': commission_fee,
}
return data
async def update_margin_type(tg_id, margin_type) -> None:
"""Обновить тип маржи пользователя."""
async with async_session() as session:
type = await session.scalar(select(Margin_type.type).where(Margin_type.type == margin_type))
if type:
logger.info("Изменен тип маржи")
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(margin_type = type))
logger.info("Изменен тип маржи %s", tg_id)
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(margin_type=type))
await session.commit()
async def update_size_leverange(tg_id, num):
"""Обновить размер левеража пользователя."""
async with async_session() as session:
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(size_leverage = num))
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(size_leverage=num))
await session.commit()
async def update_starting_quantity(tg_id, num):
"""Обновить размер левеража пользователя."""
async with async_session() as session:
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(starting_quantity = num))
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(starting_quantity=num))
await session.commit()
async def update_martingale_factor(tg_id, num):
"""Обновить размер левеража пользователя."""
async with async_session() as session:
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(martingale_factor = num))
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(martingale_factor=num))
await session.commit()
async def update_maximal_quantity(tg_id, num):
"""Обновить размер левеража пользователя."""
async with async_session() as session:
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(maximal_quantity = num))
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(maximal_quantity=num))
await session.commit()
# ОБНОВЛЕНИЕ НАСТРОЕК РИСК-МЕНЕДЖМЕНТА
async def update_price_profit(tg_id, num):
"""Обновить цену тейк-профита (прибыль) пользователя."""
async with async_session() as session:
await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(price_profit=num))
await session.commit()
async def update_price_loss(tg_id, num):
"""Обновить цену тейк-лосса (убыток) пользователя."""
async with async_session() as session:
await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(price_loss=num))
await session.commit()
async def update_max_risk_deal(tg_id, num):
"""Обновить максимальную сумму риска пользователя."""
async with async_session() as session:
await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(max_risk_deal=num))
await session.commit()
async def update_entry_order_type(tg_id, order_type):
"""Обновить тип входного ордера пользователя."""
async with async_session() as session:
await session.execute(
update(UMS)
.where(UMS.tg_id == tg_id)
.values(entry_order_type=order_type)
)
await session.commit()
async def get_limit_price(tg_id):
"""Получить лимитную цену пользователя как float, либо None."""
async with async_session() as session:
result = await session.execute(
select(UMS.limit_order_price)
.where(UMS.tg_id == tg_id)
)
price = result.scalar_one_or_none()
if price:
try:
return float(price)
except ValueError:
return None
return None
async def update_limit_price(tg_id, price):
"""Обновить лимитную цену пользователя."""
async with async_session() as session:
await session.execute(
update(UMS)
.where(UMS.tg_id == tg_id)
.values(limit_order_price=str(price))
)
await session.commit()
async def update_commission_fee(tg_id, num):
"""Обновить комиссию пользователя."""
async with async_session() as session:
await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(commission_fee=num))
await session.commit()
async def get_user_timer(tg_id):
"""Получить данные о таймере пользователя."""
async with async_session() as session:
result = await session.execute(select(UserTimer).where(UserTimer.tg_id == tg_id))
user_timer = result.scalars().first()
if not user_timer:
logging.info(f"No timer found for user {tg_id}")
return None
timer_minutes = user_timer.timer_minutes
timer_start = user_timer.timer_start
timer_end = user_timer.timer_end
logging.info(f"Timer data for tg_id={tg_id}: "
f"timer_minutes={timer_minutes}, "
f"timer_start={timer_start}, "
f"timer_end={timer_end}")
remaining = None
if timer_end:
remaining = max(0, int((timer_end - datetime.utcnow()).total_seconds() // 60))
return {
"timer_minutes": timer_minutes,
"timer_start": timer_start,
"timer_end": timer_end,
"remaining_minutes": remaining
}
async def update_user_timer(tg_id, minutes: int):
"""Обновить данные о таймере пользователя."""
async with async_session() as session:
try:
timer_start = None
timer_end = None
if minutes > 0:
timer_start = datetime.utcnow()
timer_end = timer_start + timedelta(minutes=minutes)
result = await session.execute(select(UserTimer).where(UserTimer.tg_id == tg_id))
user_timer = result.scalars().first()
if user_timer:
user_timer.timer_minutes = minutes
user_timer.timer_start = timer_start
user_timer.timer_end = timer_end
else:
user_timer = UserTimer(
tg_id=tg_id,
timer_minutes=minutes,
timer_start=timer_start,
timer_end=timer_end
)
session.add(user_timer)
await session.commit()
except Exception as e:
logging.error(f"Ошибка обновления таймера пользователя {tg_id}: {e}")
async def get_martingale_step(tg_id):
"""Получить шаг мартингейла пользователя."""
async with async_session() as session:
result = await session.execute(select(UMS).where(UMS.tg_id == tg_id))
user_settings = result.scalars().first()
return user_settings.martingale_step
async def update_martingale_step(tg_id, step):
"""Обновить шаг мартингейла пользователя."""
async with async_session() as session:
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(martingale_step=step))
await session.commit()
async def update_switch_mode_enabled(tg_id, switch_mode):
"""Обновить режим переключения пользователя."""
async with async_session() as session:
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(switch_mode_enabled=switch_mode))
await session.commit()
async def update_switch_state(tg_id, switch_state):
"""Обновить состояние переключения пользователя."""
async with async_session() as session:
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(switch_state=switch_state))
await session.commit()
async def update_trigger(tg_id, trigger):
"""Обновить триггер пользователя."""
async with async_session() as session:
await session.execute(update(UCS).where(UCS.tg_id == tg_id).values(trigger=trigger))
await session.commit()
async def set_last_series_info(tg_id: int, last_side: str):
async with async_session() as session:
async with session.begin():
# Обновляем запись
result = await session.execute(
update(UMS)
.where(UMS.tg_id == tg_id)
.values(last_side=last_side)
)
if result.rowcount == 0:
# Если запись не существует, создаём новую
new_entry = UMS(
tg_id=tg_id,
last_side=last_side,
)
session.add(new_entry)
await session.commit()

View File

@@ -1,5 +1,4 @@
import app.telegram.Keyboards.inline_keyboards as inline_markup
import app.telegram.Keyboards.reply_keyboards as reply_markup
import app.telegram.database.requests as rq
@@ -8,34 +7,32 @@ async def reg_new_user_default_additional_settings(id, message):
await rq.set_new_user_default_additional_settings(tg_id)
async def main_settings_message(message):
async def main_settings_message(id, message):
text = '''<b>Дополнительные параметры</b>
Сохранить как шаблон стратегии: да / нет
<b>- Сохранить как шаблон стратегии:</b> да / нет
<b>- Автозапуск после сохранения:</b> да / нет
<b>- Уведомления в Telegram:</b> включено / отключено '''
Автозапуск после сохранения: да / нет
Уведомления в Telegram: включено / отключено '''
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.additional_settings_markup)
await message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.additional_settings_markup)
async def save_pattern_message(message, state):
text = '''<b>Сохранение шаблона</b>
Описание... '''
await message.answer(text=text, parse_mode='html', reply_markup=reply_markup.buttons_yes_no_markup)
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup)
async def auto_start_message(message, state):
text = '''<b>Автозапуск</b>
Описание... '''
await message.answer(text=text, parse_mode='html', reply_markup=reply_markup.buttons_yes_no_markup)
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup)
async def notifications_message(message, state):
text = '''<b>Уведомления</b>
Описание... '''
await message.answer(text=text, parse_mode='html', reply_markup=reply_markup.buttons_on_off_markup)
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_on_off_markup)

View File

@@ -1,57 +1,117 @@
import app.telegram.Keyboards.inline_keyboards as inline_markup
import app.telegram.Keyboards.reply_keyboards as reply_markup
import logging.config
import app.telegram.Keyboards.inline_keyboards as inline_markup
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery
from aiogram.fsm.context import FSMContext
import app.telegram.database.requests as rq
from app.states.States import condition_settings
async def reg_new_user_default_condition_settings(id, message):
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("condition_settings")
condition_settings_router = Router()
async def reg_new_user_default_condition_settings(id):
tg_id = id
trigger = await rq.get_for_registration_trigger()
trigger = await rq.get_for_registration_trigger(tg_id)
await rq.set_new_user_default_condition_settings(tg_id, trigger)
async def main_settings_message(message):
text = """ <b>Условия запуска</b>
Триггер: Ручной запуск / Сигнал TradingView / Полностью автоматический
async def main_settings_message(id, message):
Фильтр времени: диапазон по дням недели и времени суток
Фильтр волатильности / объёма: включить/отключить
Интеграции и внешние сигналы:
Использовать сигналы TradingView: да / нет
Использовать AI-аналитику от ChatGPT: да / нет
Webhook URL для сигналов (если используется TradingView).
tg_id = id
trigger = await rq.get_for_registration_trigger(tg_id)
text = f""" <b>Условия запуска</b>
<b>- Режим торговли:</b> {trigger}
<b>- Таймер: </b> установить таймер / удалить таймер
"""
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.condition_settings_markup)
async def trigger_message(message, state):
text = '''Триггер
Описание ручного запуска, сигналов, автоматического режима '''
async def trigger_message(id, message, state: FSMContext):
await state.set_state(condition_settings.trigger)
text = '''
<b>- Автоматический:</b> торговля будет происходить в рамках серии ставок.
<b>- Ручной:</b> торговля будет происходить только в ручном режиме.
<em>- Выберите тип триггера:</em>'''
await message.answer(text=text, parse_mode='html', reply_markup=reply_markup.trigger_markup)
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trigger_markup)
async def filter_time_message(message, state):
text = '''Фильтр времени
???
'''
@condition_settings_router.callback_query(F.data == "clb_trigger_manual")
async def trigger_manual_callback(callback: CallbackQuery, state: FSMContext):
await state.set_state(condition_settings.trigger)
await rq.update_trigger(tg_id=callback.from_user.id, trigger="Ручной")
await main_settings_message(callback.from_user.id, callback.message)
await callback.answer()
@condition_settings_router.callback_query(F.data == "clb_trigger_auto")
async def trigger_manual_callback(callback: CallbackQuery, state: FSMContext):
await state.set_state(condition_settings.trigger)
await rq.update_trigger(tg_id=callback.from_user.id, trigger="Автоматический")
await main_settings_message(callback.from_user.id, callback.message)
await callback.answer()
async def timer_message(id, message: Message, state: FSMContext):
await state.set_state(condition_settings.timer)
timer_info = await rq.get_user_timer(id)
if timer_info is None:
await message.answer("Таймер не установлен.", reply_markup=inline_markup.timer_markup)
return
await message.answer(
f"Таймер установлен на: {timer_info['timer_minutes']} мин\n",
reply_markup=inline_markup.timer_markup
)
@condition_settings_router.callback_query(F.data == "clb_set_timer")
async def set_timer_callback(callback: CallbackQuery, state: FSMContext):
await state.set_state(condition_settings.timer) # состояние для ввода времени
await callback.message.answer("Введите время работы в минутах (например, 60):", reply_markup=inline_markup.cancel)
await callback.answer()
@condition_settings_router.message(condition_settings.timer)
async def process_timer_input(message: Message, state: FSMContext):
try:
minutes = int(message.text)
if minutes <= 0:
await message.reply("Введите число больше нуля.")
return
await rq.update_user_timer(message.from_user.id, minutes)
logger.info("Timer set for user %s: %s minutes", message.from_user.id, minutes)
await timer_message(message.from_user.id, message, state)
await state.clear()
except ValueError:
await message.reply("Пожалуйста, введите корректное число.")
@condition_settings_router.callback_query(F.data == "clb_delete_timer")
async def delete_timer_callback(callback: CallbackQuery, state: FSMContext):
await state.clear()
await rq.update_user_timer(callback.from_user.id, 0)
logger.info("Timer deleted for user %s", callback.from_user.id)
await timer_message(callback.from_user.id, callback.message, state)
await callback.answer()
await message.answer(text=text)
async def filter_volatility_message(message, state):
text = '''Фильтр волатильности
Описание... '''
await message.answer(text=text, parse_mode='html', reply_markup=reply_markup.buttons_on_off_markup)
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_on_off_markup)
async def external_cues_message(message, state):
text = '''<b>Внешние сигналы</b>
@@ -60,23 +120,24 @@ async def external_cues_message(message, state):
await message.answer(text=text, parse_mode='html', reply_markup=None)
async def trading_cues_message(message, state):
text = '''<b>Использование сигналов</b>
Описание... '''
await message.answer(text=text, parse_mode='html', reply_markup=reply_markup.buttons_yes_no_markup)
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup)
async def webhook_message(message, state):
text = '''Скиньте ссылку на <b>webhook</b> (если есть trading view): '''
await message.answer(text=text, parse_mode='html')
async def ai_analytics_message(message, state):
text = '''<b>ИИ - Аналитика</b>
Описание... '''
await message.answer(text=text, parse_mode='html', reply_markup=reply_markup.buttons_yes_no_markup)
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup)

View File

@@ -2,33 +2,28 @@
import app.telegram.Keyboards.reply_keyboards as reply_markup
async def start_message(message):
await message.answer(f""" Привет <b>{message.from_user.username}</b>! 👋
username = ''
Добро пожаловать в бот по трейдингу на Bibyt — вашего надежного помощника для анализа рынка и принятия взвешенных решений. Здесь вы получите:
<b>
📊 Анализ текущих трендов
📈 Инструменты для прогнозирования и оценки рисков
⚡️ Сигналы и рекомендации по сделкам
🔔 Уведомления о важных изменениях и новостях
</b>
Просто отправляйте интересующий вас инструмент или команду, и бот быстро предоставит актуальную информацию и аналитику.
Начнем торговать умно и эффективно вместе! 🚀
""", parse_mode='html', reply_markup=inline_markup.start_markup)
if message.from_user.first_name == None:
username = message.from_user.last_name
elif message.from_user.last_name == None:
username = message.from_user.first_name
else:
username = f'{message.from_user.first_name} {message.from_user.last_name}'
await message.answer(f""" Привет <b>{username}</b>! 👋""", parse_mode='html')
await message.answer("Добро пожаловать в чат-робот для автоматизации трейдинга — вашего надежного помощника для анализа рынка и принятия взвешенных решений.",
parse_mode='html', reply_markup=inline_markup.start_markup)
async def profile_message(username, message):
await message.answer(f""" <b>{username}</b>
await message.answer(f""" <b>@{username}</b>
Баланс:
Баланс
⭐️ 0
Описание:
Активный трейдер на платформе Bibyt с индивидуальной стратегией и аналитикой в реальном времени. Постоянно улучшает навыки и следит за рыночными тенденциями для максимальной прибыли.
""", parse_mode='html', reply_markup=inline_markup.settings_markup)
async def check_profile_message(message):
await message.answer(f'Добро пожаловать {message.from_user.username}!', reply_markup=reply_markup.base_buttons_markup)
async def check_profile_message(message, username):
await message.answer(f'С возвращением, {username}!', reply_markup=reply_markup.base_buttons_markup)
async def settings_message(message):
await message.edit_text("Выберите что настроить", reply_markup=inline_markup.special_settings_markup)

View File

@@ -1,23 +1,21 @@
from aiogram import Router
import logging.config
import app.telegram.Keyboards.inline_keyboards as inline_markup
import app.telegram.Keyboards.reply_keyboards as reply_markup
from pybit.unified_trading import HTTP
import app.telegram.database.requests as rq
from aiogram.types import Message, CallbackQuery
# FSM - Механизм состояния
from aiogram.fsm.state import State, StatesGroup
from app.services.Bybit.functions.price_symbol import get_price
from app.services.Bybit.functions.Futures import safe_float, calculate_total_budget, get_bybit_client
from app.states.States import update_main_settings
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("main_settings")
router_main_settings = Router()
class update_main_settings(StatesGroup):
trading_mode = State() # +
size_leverage = State() #
margin_type = State() #
martingale_factor = State() #
starting_quantity = State() #
maximal_quantity = State() #
async def reg_new_user_default_main_settings(id, message):
tg_id = id
@@ -28,22 +26,56 @@ async def reg_new_user_default_main_settings(id, message):
await rq.set_new_user_default_main_settings(tg_id, trading_mode, margin_type)
async def main_settings_message(id, message, state):
data = await rq.get_user_main_settings(id)
async def main_settings_message(id, message):
try:
data = await rq.get_user_main_settings(id)
tg_id = id
await message.answer(f"""<b>Основные настройки</b>
Режим торговли: <b>{data['trading_mode']}</b>
data_main_stgs = await rq.get_user_main_settings(id)
data_risk_stgs = await rq.get_user_risk_management_settings(id)
client = await get_bybit_client(tg_id)
symbol = await rq.get_symbol(tg_id)
max_martingale_steps = (data_main_stgs or {}).get('maximal_quantity', 0)
commission_fee = (data_risk_stgs or {}).get('commission_fee')
starting_quantity = safe_float((data_main_stgs or {}).get('starting_quantity'))
martingale_factor = safe_float((data_main_stgs or {}).get('martingale_factor'))
fee_info = client.get_fee_rates(category='linear', symbol=symbol)
leverage = safe_float((data_main_stgs or {}).get('size_leverage'))
price = await get_price(tg_id, symbol=symbol)
entry_price = safe_float(price)
Тип маржи: <b>{data['margin_type']}</b>
if commission_fee == "Да":
commission_fee_percent = safe_float(fee_info['result']['list'][0]['takerFeeRate'])
else:
commission_fee_percent = 0.0
Размер кредитного плеча: <b>х{data['size_leverage']}</b>
total_budget = await calculate_total_budget(
starting_quantity=starting_quantity,
martingale_factor=martingale_factor,
max_steps=max_martingale_steps,
commission_fee_percent=commission_fee_percent,
leverage=leverage,
current_price=entry_price,
)
Начальная ставка: <b>{data['starting_quantity']}</b>
await message.answer(f"""<b>Основные настройки</b>
Коэффициент мартингейла: <b>{data['martingale_factor']}</b>
<b>- Режим торговли:</b> {data['trading_mode']}
<b>- Состояние свитча:</b> {data['switch_state']}
<b>- Направление последней сделки:</b> {data['last_side']}
<b>- Тип маржи:</b> {data['margin_type']}
<b>- Размер кредитного плеча:</b> х{data['size_leverage']}
<b>- Начальная ставка:</b> {data['starting_quantity']}
<b>- Коэффициент мартингейла:</b> {data['martingale_factor']}
<b>- Текущий шаг:</b> {data['martingale_step']}
<b>- Максимальное количество ставок в серии:</b> {data['maximal_quantity']}
<b>- Требуемый бюджет:</b> {total_budget:.2f} USDT
""", parse_mode='html', reply_markup=inline_markup.main_settings_markup)
except PermissionError as e:
logger.error("Authenticated endpoints require keys: %s", e)
await message.answer("Вы не авторизованы.", reply_markup=inline_markup.connect_bybit_api_message)
Максимальное количесиво ставок в серии: <b>{data['maximal_quantity']}</b>
""", parse_mode='html', reply_markup=inline_markup.main_settings_markup)
async def trading_mode_message(message, state):
await state.set_state(update_main_settings.trading_mode)
@@ -54,82 +86,160 @@ async def trading_mode_message(message, state):
<b>Шорт</b> — метод продажи активов, взятых в кредит, чтобы получить прибыль от снижения цены.
<b>Смарт</b> — автоматизированный режим, который подбирает оптимальную стратегию в зависимости от текущих рыночных условий.
<b>Свитч</b> — динамическое переключение между торговыми режимами для максимизации эффективности.
<em>Выберите ниже для изменений:</em>
""", parse_mode='html', reply_markup=inline_markup.trading_mode_markup)
@router_main_settings.callback_query(update_main_settings.trading_mode)
async def state_trading_mode(callback: CallbackQuery, state):
await callback.answer()
await callback.answer()
id = callback.from_user.id
print(f"sdljfngdjklfg ## {callback.data}")
id = callback.from_user.id
data_settings = await rq.get_user_main_settings(id)
try:
match callback.data:
case 'trade_mode_long':
try:
match callback.data:
case 'trade_mode_long':
await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Long")
await rq.update_trade_mode_user(id, 'Long')
await main_settings_message(id, callback.message, state)
await main_settings_message(id, callback.message)
await state.clear()
case 'trade_mode_short':
case 'trade_mode_short':
await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Short")
await rq.update_trade_mode_user(id, 'Short')
await main_settings_message(id, callback.message, state)
await main_settings_message(id, callback.message)
await state.clear()
case 'trade_mode_switch':
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 main_settings_message(id, callback.message)
await state.clear()
case 'trade_mode_smart':
await rq.update_trade_mode_user(id, 'Smart')
await main_settings_message(id, callback.message, state)
await state.clear()
except Exception as e:
print(f"error: {e}")
case 'trade_mode_smart':
await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Smart")
await rq.update_trade_mode_user(id, 'Smart')
await main_settings_message(id, callback.message)
async def size_leverage_message (message, state):
await state.clear()
except Exception as e:
logger.error(e)
async def switch_mode_enabled_message(message, state):
await state.set_state(update_main_settings.switch_mode_enabled)
await message.edit_text(
f"""<b> Состояние свитча</b>
<b>По направлению</b> - по направлению последней сделки предыдущей серии
<b>Против направления</b> - против направления последней сделки предыдущей серии
<em>По умолчанию при первом запуске бота, направление сделки установлено на "Buy".</em>
<em>Выберите ниже для изменений:</em>""", parse_mode='html',
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 = "По направлению" if callback.data == "clb_long_switch" else "Против направления"
if val == "По направлению":
await rq.update_switch_state(tg_id, "По направлению")
await callback.message.answer(f"Состояние свитча: {val}")
await main_settings_message(tg_id, callback.message)
else:
await rq.update_switch_state(tg_id, "Против направления")
await callback.message.answer(f"Состояние свитча: {val}")
await main_settings_message(tg_id, callback.message)
await state.clear()
async def size_leverage_message(message, state):
await state.set_state(update_main_settings.size_leverage)
await message.edit_text("Введите размер <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)
async def state_size_leverage(message: Message, state):
await state.update_data(size_leverage = message.text)
try:
leverage = float(message.text)
if leverage <= 0:
raise ValueError("Неверное значение")
except ValueError:
await message.answer(
"Ошибка: пожалуйста, введите положительное число для кредитного плеча."
"\nПопробуйте снова."
)
return
await state.update_data(size_leverage=message.text)
data = await state.get_data()
tg_id = message.from_user.id
symbol = await rq.get_symbol(tg_id)
leverage = data['size_leverage']
client = await get_bybit_client(tg_id)
if data['size_leverage'].isdigit() and int(data['size_leverage']) <= 100:
await rq.update_size_leverange(message.from_user.id, data['size_leverage'])
await main_settings_message(message.from_user.id, message, state)
instruments_resp = client.get_instruments_info(category="linear", symbol=symbol)
info = instruments_resp.get("result", {}).get("list", [])
max_leverage = safe_float(info[0].get("leverageFilter", {}).get("maxLeverage", 0))
if safe_float(leverage) > max_leverage:
await message.answer(
f"Запрошенное кредитное плечо {leverage} превышает максимальное {max_leverage} для {symbol}. "
f"Устанавливаю максимальное.",
reply_markup=inline_markup.back_to_main,
)
logger.info(
f"Запрошенное кредитное плечо {leverage} превышает максимальное {max_leverage} для {symbol}. Устанавливаю максимальное.")
await rq.update_size_leverange(message.from_user.id, max_leverage)
await main_settings_message(message.from_user.id, message)
await state.clear()
else:
await main_settings_message(message.from_user.id, message, state)
await message.answer(f"✅ Изменено: {leverage}")
await rq.update_size_leverange(message.from_user.id, safe_float(leverage))
await main_settings_message(message.from_user.id, message)
await state.clear()
async def martingale_factor_message(message, state):
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)
async def state_martingale_factor(message: Message, state):
await state.update_data(martingale_factor = message.text)
await state.update_data(martingale_factor=message.text)
data = await state.get_data()
data_settings = await rq.get_user_main_settings(message.from_user.id)
if data['martingale_factor'].isdigit() and int(data['martingale_factor']) <= 100:
await message.answer(f"✅ Изменено: {data_settings['martingale_factor']}{data['martingale_factor']}")
await rq.update_martingale_factor(message.from_user.id, data['martingale_factor'])
await main_settings_message(message.from_user.id, message, state)
await main_settings_message(message.from_user.id, message)
await state.clear()
else:
await main_settings_message(message.from_user.id, message, state)
await message.answer(
f'⛔️ Ошибка: ваше значение ({data['martingale_factor']}) или выше лимита (100) или вы вводите неверные символы')
await main_settings_message(message.from_user.id, message)
async def margin_type_message(message, state):
await state.set_state(update_main_settings.margin_type)
@@ -150,62 +260,110 @@ async def margin_type_message(message, state):
<em>Выберите ниже для изменений:</em>
""", parse_mode='html', reply_markup=inline_markup.margin_type_markup)
@router_main_settings.callback_query(update_main_settings.margin_type)
async def state_margin_type(callback: CallbackQuery, state):
await callback.answer()
callback_data = callback.data
if callback_data in ['margin_type_isolated', 'margin_type_cross']:
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)
symbol = await rq.get_symbol(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
print(f"sdljfngdjklfg ## {callback.data}")
positions = active_positions.get('result', {}).get('list', [])
except Exception as e:
logger.error(f"error: {e}")
positions = []
try:
match callback.data:
case 'margin_type_isolated':
await rq.update_margin_type(id, 'Изолированный')
await main_settings_message(id, callback.message, state)
for pos in positions:
size = pos.get('size')
if float(size) > 0:
await callback.answer(
"⚠️ Маржинальный режим нельзя менять при открытой позиции"
)
return
await state.clear()
case 'margin_type_cross':
await rq.update_margin_type(id, 'Кросс')
await main_settings_message(id, callback.message, state)
try:
match callback.data:
case 'margin_type_isolated':
await callback.answer()
await callback.message.answer(f"✅ Изменено: {data_settings['margin_type']} → Isolated")
await state.clear()
except Exception as e:
print(f"error: {e}")
await rq.update_margin_type(tg_id, 'Isolated')
await main_settings_message(tg_id, callback.message)
async def starting_quantity_message (message, state):
await state.clear()
case 'margin_type_cross':
await callback.answer()
await callback.message.answer(f"✅ Изменено: {data_settings['margin_type']} → Cross")
await rq.update_margin_type(tg_id, 'Cross')
await main_settings_message(tg_id, callback.message)
await state.clear()
except Exception as e:
logger.error(f"error: {e}")
else:
await callback.answer()
await main_settings_message(callback.from_user.id, callback.message)
await state.clear()
async def starting_quantity_message(message, state):
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)
async def state_starting_quantity(message: Message, state):
await state.update_data(starting_quantity = message.text)
await state.update_data(starting_quantity=message.text)
data = await state.get_data()
data_settings = await rq.get_user_main_settings(message.from_user.id)
if data['starting_quantity'].isdigit():
await message.answer(f"✅ Изменено: {data_settings['starting_quantity']}{data['starting_quantity']}")
if data['starting_quantity'].isdigit() and int(data['starting_quantity']) <= 100:
await rq.update_starting_quantity(message.from_user.id, data['starting_quantity'])
await main_settings_message(message.from_user.id, message, state)
await main_settings_message(message.from_user.id, message)
await state.clear()
else:
await main_settings_message(message.from_user.id, message, state)
await message.answer(f'⛔️ Ошибка: вы вводите неверные символы')
await main_settings_message(message.from_user.id, message)
async def maximum_quantity_message(message, state):
await state.set_state(update_main_settings.maximal_quantity)
await message.edit_text("Введите <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)
async def state_maximal_quantity(message: Message, state):
await state.update_data(maximal_quantity = message.text)
await state.update_data(maximal_quantity=message.text)
data = await state.get_data()
data_settings = await rq.get_user_main_settings(message.from_user.id)
if data['maximal_quantity'].isdigit() and int(data['maximal_quantity']) <= 100:
await message.answer(f"✅ Изменено: {data_settings['maximal_quantity']}{data['maximal_quantity']}")
await rq.update_maximal_quantity(message.from_user.id, data['maximal_quantity'])
await main_settings_message(message.from_user.id, message, state)
await main_settings_message(message.from_user.id, message)
await state.clear()
else:
await main_settings_message(message.from_user.id, message, state)
await message.answer(
f'⛔️ Ошибка: ваше значение ({data['maximal_quantity']}) или выше лимита (100) или вы вводите неверные символы')
await main_settings_message(message.from_user.id, message)

View File

@@ -1,7 +1,18 @@
import app.telegram.Keyboards.inline_keyboards as inline_markup
import app.telegram.Keyboards.reply_keyboards as reply_markup
from aiogram import Router
import app.telegram.Keyboards.inline_keyboards as inline_markup
import logging.config
import app.telegram.database.requests as rq
from aiogram.types import Message, CallbackQuery
from app.states.States import update_risk_management_settings
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("risk_management_settings")
router_risk_management_settings = Router()
async def reg_new_user_default_risk_management_settings(id, message):
tg_id = id
@@ -9,29 +20,138 @@ async def reg_new_user_default_risk_management_settings(id, message):
await rq.set_new_user_default_risk_management_settings(tg_id)
async def main_settings_message(message):
async def main_settings_message(id, message):
data = await rq.get_user_risk_management_settings(id)
text = f"""<b>Риск менеджмент</b>,
Процент изменения цены для фиксации прибыли: 0
Процент изменения цены для фиксации убытков: 0
Максимальный риск на сделку (в % от баланса): 0
<b>- Процент изменения цены для фиксации прибыли:</b> {data.get('price_profit', 0)}%
<b>- Процент изменения цены для фиксации убытков:</b> {data.get('price_loss', 0)}%
<b>- Максимальный риск на сделку (в % от баланса):</b> {data.get('max_risk_deal', 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.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.risk_management_settings_markup)
async def price_profit_message(message, state):
text = 'Введите число изменения цены для фиксации прибыли: '
await state.set_state(update_risk_management_settings.price_profit)
text = 'Введите число изменения цены для фиксации прибыли: '
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.cancel)
@router_risk_management_settings.message(update_risk_management_settings.price_profit)
async def state_price_profit(message: Message, state):
await state.update_data(price_profit=message.text)
data = await state.get_data()
data_settings = await rq.get_user_risk_management_settings(message.from_user.id)
if data['price_profit'].isdigit() and int(data['price_profit']) <= 100:
await message.answer(f"✅ Изменено: {data_settings['price_profit']}% → {data['price_profit']}%")
await rq.update_price_profit(message.from_user.id, data['price_profit'])
await main_settings_message(message.from_user.id, message)
await state.clear()
else:
await message.answer(
f'⛔️ Ошибка: ваше значение ({data['price_profit']}%) или выше лимита (100) или вы вводите неверные символы')
await main_settings_message(message.from_user.id, message)
await message.answer(text=text, parse_mode='html', reply_markup=None)
async def price_loss_message(message, state):
text = 'Введите число изменения цены для фиксации убытков: '
await state.set_state(update_risk_management_settings.price_loss)
text = 'Введите число изменения цены для фиксации убытков: '
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.cancel)
@router_risk_management_settings.message(update_risk_management_settings.price_loss)
async def state_price_loss(message: Message, state):
await state.update_data(price_loss=message.text)
data = await state.get_data()
data_settings = await rq.get_user_risk_management_settings(message.from_user.id)
if data['price_loss'].isdigit() and int(data['price_loss']) <= 100:
new_price_loss = int(data['price_loss'])
old_price_loss = int(data_settings.get('price_loss', 0))
current_price_profit = data_settings.get('price_profit')
# Пробуем перевести price_profit в число, если это возможно
try:
current_price_profit_num = int(current_price_profit)
except Exception as e:
logger.error(e)
current_price_profit_num = 0
# Флаг, если price_profit изначально равен 0 или совпадает со старым стоп-лоссом
should_update_profit = (current_price_profit_num == 0) or (current_price_profit_num == abs(old_price_loss))
# Обновляем стоп-лосс
await rq.update_price_loss(message.from_user.id, new_price_loss)
# Если нужно, меняем тейк-профит
if should_update_profit:
new_price_profit = abs(new_price_loss)
await rq.update_price_profit(message.from_user.id, new_price_profit)
await message.answer(f"✅ Стоп-лосс изменён: {old_price_loss}% → {new_price_loss}%\n"
f"Тейк-профит автоматически установлен в: {new_price_profit}%")
else:
await message.answer(f"✅ Стоп-лосс изменён: {old_price_loss}% → {new_price_loss}%")
await main_settings_message(message.from_user.id, message)
await state.clear()
else:
await message.answer(
f'⛔️ Ошибка: ваше значение ({data["price_loss"]}%) выше лимита (100) или содержит неверные символы')
await main_settings_message(message.from_user.id, message)
await message.answer(text=text, parse_mode='html', reply_markup=None)
async def max_risk_deal_message(message, state):
text = 'Введите число (процент от баланса) для изменения максимального риска на сделку: '
await state.set_state(update_risk_management_settings.max_risk_deal)
await message.answer(text=text, parse_mode='html', reply_markup=None)
text = 'Введите число (процент от баланса) для изменения максимального риска на сделку: '
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)
async def state_max_risk_deal(message: Message, state):
await state.update_data(max_risk_deal=message.text)
data = await state.get_data()
data_settings = await rq.get_user_risk_management_settings(message.from_user.id)
if data['max_risk_deal'].isdigit() and int(data['max_risk_deal']) <= 100:
await message.answer(f"✅ Изменено: {data_settings['max_risk_deal']}% → {data['max_risk_deal']}%")
await rq.update_max_risk_deal(message.from_user.id, data['max_risk_deal'])
await main_settings_message(message.from_user.id, message)
await state.clear()
else:
await message.answer(
f'⛔️ Ошибка: ваше значение ({data['max_risk_deal']}%) или выше лимита (100) или вы вводите неверные символы')
await main_settings_message(message.from_user.id, message)
async def commission_fee_message(message, state):
await state.set_state(update_risk_management_settings.commission_fee)
await message.answer(text="Хотите учитывать комиссию биржи:", parse_mode='html',
reply_markup=inline_markup.buttons_yes_no_markup)
@router_risk_management_settings.callback_query(lambda c: c.data in ["clb_yes", "clb_no"])
async def process_commission_fee_callback(callback: CallbackQuery, state):
val = "Да" if callback.data == "clb_yes" else "Нет"
await rq.update_commission_fee(callback.from_user.id, val)
await callback.message.answer(f"✅ Изменено: {val}")
await callback.answer()
await main_settings_message(callback.from_user.id, callback.message)
await state.clear()

View File

@@ -1,114 +1,205 @@
import logging
import logging.config
from aiogram import F, Router
from aiogram.filters import CommandStart, Command
from aiogram.types import Message, CallbackQuery
from aiogram.fsm.context import FSMContext
import app.telegram.functions.functions as func # functions
import app.telegram.functions.functions as func
import app.telegram.functions.main_settings.settings as func_main_settings
import app.telegram.functions.risk_management_settings.settings as func_rmanagement_settings
import app.telegram.functions.condition_settings.settings as func_condition_settings
import app.telegram.functions.additional_settings.settings as func_additional_settings
import app.telegram.database.requests as rq
import app.telegram.Keyboards.inline_keyboards as inline_markup
import app.telegram.Keyboards.reply_keyboards as reply_markup
from app.services.Bybit.functions.balance import get_balance
from app.services.Bybit.functions.bybit_ws import run_ws_for_user
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("handlers")
router = Router()
@router.message(Command("start"))
@router.message(CommandStart())
async def start_message(message: Message):
async def start_message(message: Message) -> None:
"""
Обработчик команды /start.
Инициализирует нового пользователя в БД.
Args:
message (Message): Входящее сообщение с командой /start.
"""
await rq.set_new_user_bybit_api(message.from_user.id)
await func.start_message(message)
@router.message(Command("profile"))
@router.message(F.text == "👤 Профиль")
async def profile_message(message: Message):
user = await rq.check_user(message.from_user.id)
async def profile_message(message: Message) -> None:
"""
Обработчик кнопки 'Профиль'.
Проверяет существование пользователя и отображает профиль.
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)
else:
await rq.save_tg_id_new_user(message.from_user.id)
await func_main_settings.reg_new_user_default_main_settings(message.from_user.id, message)
await func_rmanagement_settings.reg_new_user_default_risk_management_settings(message.from_user.id, message)
await func_condition_settings.reg_new_user_default_condition_settings(message.from_user.id)
await func_additional_settings.reg_new_user_default_additional_settings(message.from_user.id, message)
@router.message(F.text == "Настройки")
async def settings_msg(message: Message):
user = await rq.check_user(message.from_user.id)
if user:
await func.settings_message(message)
@router.callback_query(F.data == "clb_start_chatbot_message")
async def clb_profile_msg(callback: CallbackQuery) -> None:
"""
Обработчик колбэка 'clb_start_chatbot_message'.
Если пользователь есть в БД — показывает профиль,
иначе регистрирует нового пользователя и инициализирует настройки.
@router.callback_query(F.data == "callback_registration")
async def clb_func_reg (callback: CallbackQuery):
await rq.save_tg_id_new_user(callback.from_user.id)
print(callback.from_user.id)
Args:
callback (CallbackQuery): Полученный колбэк.
"""
tg_id = callback.from_user.id
message = callback.message
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 "Пользователь"
await func_main_settings.reg_new_user_default_main_settings(callback.from_user.id, callback.message)
await func_rmanagement_settings.reg_new_user_default_risk_management_settings(callback.from_user.id, callback.message)
await func_condition_settings.reg_new_user_default_condition_settings(callback.from_user.id, callback.message)
await func_additional_settings.reg_new_user_default_additional_settings(callback.from_user.id, callback.message)
if user and balance:
await run_ws_for_user(tg_id, message)
await func.profile_message(callback.from_user.username, callback.message)
else:
await rq.save_tg_id_new_user(callback.from_user.id)
await callback.message.answer(f'Регистрация прошла успешно, здравствуйте {callback.from_user.username}!', reply_markup=reply_markup.base_buttons_markup)
await func_main_settings.reg_new_user_default_main_settings(callback.from_user.id, callback.message)
await func_rmanagement_settings.reg_new_user_default_risk_management_settings(callback.from_user.id,
callback.message)
await func_condition_settings.reg_new_user_default_condition_settings(callback.from_user.id)
await func_additional_settings.reg_new_user_default_additional_settings(callback.from_user.id, callback.message)
await callback.answer()
@router.callback_query(F.data == "callback_autorisation")
async def clb_func_reg (callback: CallbackQuery):
user = await rq.check_user(callback.from_user.id)
if user:
await func.profile_message(callback.from_user.username, callback.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 callback.answer()
@router.callback_query(F.data == "clb_back_to_special_settings_message")
async def clb_back_to_settings_msg(callback: CallbackQuery):
async def clb_back_to_settings_msg(callback: CallbackQuery) -> None:
"""
Вернуть пользователя к меню специальных настроек.
Args:
callback (CallbackQuery): полученный колбэк.
"""
await func.settings_message(callback.message)
await callback.answer()
@router.callback_query(F.data == "clb_change_main_settings")
async def clb_change_main_settings_message(callback: CallbackQuery, state: FSMContext):
await func_main_settings.main_settings_message(callback.from_user.id, callback.message, state)
async def clb_change_main_settings_message(callback: CallbackQuery) -> None:
"""
Открыть меню изменения главных настроек.
Args:
callback (CallbackQuery): полученный колбэк.
"""
await func_main_settings.main_settings_message(callback.from_user.id, callback.message)
await callback.answer()
@router.callback_query(F.data == "clb_change_risk_management_settings")
async def clb_change_risk_management_message(callback: CallbackQuery, state: FSMContext):
await func_rmanagement_settings.main_settings_message(callback.from_user.id, callback.message, state)
async def clb_change_risk_management_message(callback: CallbackQuery) -> None:
"""
Открыть меню изменения настроек управления рисками.
Args:
callback (CallbackQuery): полученный колбэк.
"""
await func_rmanagement_settings.main_settings_message(callback.from_user.id, callback.message)
await callback.answer()
@router.callback_query(F.data == "clb_change_condition_settings")
async def clb_change_condition_message(callback: CallbackQuery, state: FSMContext):
await func_condition_settings.main_settings_message(callback.from_user.id, callback.message, state)
async def clb_change_condition_message(callback: CallbackQuery) -> None:
"""
Открыть меню изменения настроек условий.
Args:
callback (CallbackQuery): полученный колбэк.
"""
await func_condition_settings.main_settings_message(callback.from_user.id, callback.message)
await callback.answer()
@router.callback_query(F.data == "clb_change_additional_settings")
async def clb_change_additional_message(callback: CallbackQuery, state: FSMContext):
await func_additional_settings.main_settings_message(callback.from_user.id, callback.message, state)
async def clb_change_additional_message(callback: CallbackQuery) -> None:
"""
Открыть меню изменения дополнительных настроек.
Args:
callback (CallbackQuery): полученный колбэк.
"""
await func_additional_settings.main_settings_message(callback.from_user.id, callback.message)
await callback.answer()
# Конкретные настройки каталогов
# Конкретные настройки каталогов
list_main_settings = ['clb_change_trading_mode',
'clb_change_switch_state',
'clb_change_margin_type',
'clb_change_size_leverage',
'clb_change_starting_quantity',
'clb_change_martingale_factor',
'clb_change_maximum_quantity'
]
]
@router.callback_query(F.data.in_(list_main_settings))
async def clb_main_settings_msg(callback: CallbackQuery, state: FSMContext):
async def clb_main_settings_msg(callback: CallbackQuery, state: FSMContext) -> None:
"""
Обработчик колбэков изменения главных настроек с dispatch через match-case.
Args:
callback (CallbackQuery): полученный колбэк.
state (FSMContext): текущее состояние FSM.
"""
await callback.answer()
try:
match callback.data:
case 'clb_change_trading_mode':
await func_main_settings.trading_mode_message(callback.message, state)
case 'clb_change_switch_state':
await func_main_settings.switch_mode_enabled_message(callback.message, state)
case 'clb_change_margin_type':
await func_main_settings.margin_type_message(callback.message, state)
case 'clb_change_size_leverage':
@@ -120,15 +211,25 @@ async def clb_main_settings_msg(callback: CallbackQuery, state: FSMContext):
case 'clb_change_maximum_quantity':
await func_main_settings.maximum_quantity_message(callback.message, state)
except Exception as e:
logging.error(f"Error callback in main_settings match-case: {e}")
logger.error(f"Error callback in main_settings match-case: {e}")
list_risk_management_settings = ['clb_change_price_profit',
'clb_change_price_loss',
'clb_change_max_risk_deal',
]
'clb_change_price_loss',
'clb_change_max_risk_deal',
'commission_fee',
]
@router.callback_query(F.data.in_(list_risk_management_settings))
async def clb_risk_management_settings_msg(callback: CallbackQuery, state: FSMContext):
async def clb_risk_management_settings_msg(callback: CallbackQuery, state: FSMContext) -> None:
"""
Обработчик изменений настроек управления рисками.
Args:
callback (CallbackQuery): полученный колбэк.
state (FSMContext): текущее состояние FSM.
"""
await callback.answer()
try:
@@ -139,28 +240,39 @@ async def clb_risk_management_settings_msg(callback: CallbackQuery, state: FSMCo
await func_rmanagement_settings.price_loss_message(callback.message, state)
case 'clb_change_max_risk_deal':
await func_rmanagement_settings.max_risk_deal_message(callback.message, state)
case 'commission_fee':
await func_rmanagement_settings.commission_fee_message(callback.message, state)
except Exception as e:
logging.error(f"Error callback in risk_management match-case: {e}")
logger.error(f"Error callback in risk_management match-case: {e}")
list_condition_settings = ['clb_change_trigger',
'clb_change_filter_time',
list_condition_settings = ['clb_change_mode',
'clb_change_timer',
'clb_change_filter_volatility',
'clb_change_external_cues',
'clb_change_tradingview_cues',
'clb_change_webhook',
'clb_change_ai_analytics'
]
]
@router.callback_query(F.data.in_(list_condition_settings))
async def clb_condition_settings_msg(callback: CallbackQuery, state: FSMContext):
async def clb_condition_settings_msg(callback: CallbackQuery, state: FSMContext) -> None:
"""
Обработчик изменений настроек условий трейдинга.
Args:
callback (CallbackQuery): полученный колбэк.
state (FSMContext): текущее состояние FSM.
"""
await callback.answer()
try:
match callback.data:
case 'clb_change_trigger':
await func_condition_settings.trigger_message(callback.message, state)
case 'clb_change_filter_time':
await func_condition_settings.filter_time_message(callback.message, state)
case 'clb_change_mode':
await func_condition_settings.trigger_message(callback.from_user.id, callback.message, state)
case 'clb_change_timer':
await func_condition_settings.timer_message(callback.from_user.id, callback.message, state)
case 'clb_change_filter_volatility':
await func_condition_settings.filter_volatility_message(callback.message, state)
case 'clb_change_external_cues':
@@ -172,15 +284,24 @@ async def clb_condition_settings_msg(callback: CallbackQuery, state: FSMContext)
case 'clb_change_ai_analytics':
await func_condition_settings.ai_analytics_message(callback.message, state)
except Exception as e:
logging.error(f"Error callback in main_settings match-case: {e}")
logger.error(f"Error callback in main_settings match-case: {e}")
list_additional_settings = ['clb_change_save_pattern',
'clb_change_auto_start',
'clb_change_notifications',
]
'clb_change_auto_start',
'clb_change_notifications',
]
@router.callback_query(F.data.in_(list_additional_settings))
async def clb_additional_settings_msg(callback: CallbackQuery, state: FSMContext):
async def clb_additional_settings_msg(callback: CallbackQuery, state: FSMContext) -> None:
"""
Обработчик дополнительных настроек бота.
Args:
callback (CallbackQuery): полученный колбэк.
state (FSMContext): текущее состояние FSM.
"""
await callback.answer()
try:
@@ -192,4 +313,4 @@ async def clb_additional_settings_msg(callback: CallbackQuery, state: FSMContext
case 'clb_change_notifications':
await func_additional_settings.notifications_message(callback.message, state)
except Exception as e:
logging.error(f"Error callback in additional_settings match-case: {e}")
logger.error(f"Error callback in additional_settings match-case: {e}")

View File

@@ -1,8 +0,0 @@
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

View File

@@ -1,6 +1,9 @@
from dotenv import load_dotenv
from dotenv import load_dotenv, find_dotenv
import os
load_dotenv('.env')
env_file = find_dotenv()
load_dotenv(env_file)
TOKEN_TG_BOT = os.getenv('TOKEN_TELEGRAM_BOT')
TOKEN_TG_BOT_1 = os.getenv('TOKEN_TELEGRAM_BOT_1')
TOKEN_TG_BOT_2 = os.getenv('TOKEN_TELEGRAM_BOT_2')
TOKEN_TG_BOT_3 = os.getenv('TOKEN_TELEGRAM_BOT_3')

View File

@@ -0,0 +1,114 @@
import os
current_directory = os.path.dirname(os.path.abspath(__file__))
log_directory = os.path.join(current_directory, 'loggers')
os.makedirs(log_directory, exist_ok=True)
log_filename = os.path.join(log_directory, 'app.log')
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S", # Формат даты
},
},
"handlers": {
"timed_rotating_file": {
"class": "logging.handlers.TimedRotatingFileHandler",
"filename": log_filename,
"when": "midnight", # Время ротации (каждую полночь)
"interval": 1, # Интервал в днях
"backupCount": 7, # Количество сохраняемых архивов (0 - не сохранять)
"formatter": "default",
"encoding": "utf-8",
},
"console": {
"class": "logging.StreamHandler",
"formatter": "default",
},
},
"loggers": {
"main": {
"handlers": ["console", "timed_rotating_file"],
"level": "DEBUG",
"propagate": False,
},
"add_bybit_api": {
"handlers": ["console", "timed_rotating_file"],
"level": "DEBUG",
"propagate": False,
},
"balance": {
"handlers": ["console", "timed_rotating_file"],
"level": "DEBUG",
"propagate": False,
},
"functions": {
"handlers": ["console", "timed_rotating_file"],
"level": "DEBUG",
"propagate": False,
},
"futures": {
"handlers": ["console", "timed_rotating_file"],
"level": "DEBUG",
"propagate": False,
},
"get_valid_symbol": {
"handlers": ["console", "timed_rotating_file"],
"level": "DEBUG",
"propagate": False,
},
"min_qty": {
"handlers": ["console", "timed_rotating_file"],
"level": "DEBUG",
"propagate": False,
},
"price_symbol": {
"handlers": ["console", "timed_rotating_file"],
"level": "DEBUG",
"propagate": False,
},
"requests": {
"handlers": ["console", "timed_rotating_file"],
"level": "DEBUG",
"propagate": False,
},
"handlers": {
"handlers": ["console", "timed_rotating_file"],
"level": "DEBUG",
"propagate": False,
},
"condition_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

@@ -0,0 +1,35 @@
2025-08-23 12:57:26 - main - INFO - Bot is off
2025-08-23 13:04:01 - main - INFO - Bot is off
2025-08-23 13:25:04 - main - INFO - Bot is off
2025-08-23 13:26:24 - main - INFO - Bot is off
2025-08-23 13:28:36 - main - INFO - Bot is off
2025-08-23 13:29:29 - main - INFO - Bot is off
2025-08-23 13:30:48 - main - INFO - Bot is off
2025-08-23 13:31:43 - main - INFO - Bot is off
2025-08-23 13:33:10 - main - INFO - Bot is off
2025-08-23 13:34:59 - main - INFO - Bot is off
2025-08-23 13:36:15 - main - INFO - Bot is off
2025-08-23 13:49:17 - main - INFO - Bot is off
2025-08-23 13:50:22 - main - INFO - Bot is on
2025-08-23 13:51:30 - main - INFO - Bot is off
2025-08-23 13:51:37 - main - INFO - Bot is on
2025-08-23 13:52:12 - main - INFO - Bot is off
2025-08-23 13:57:48 - main - INFO - Bot is on
2025-08-23 14:05:36 - main - INFO - Bot is off
2025-08-23 14:05:43 - main - INFO - Bot is on
2025-08-23 14:06:03 - main - INFO - Bot is off
2025-08-23 14:06:46 - main - INFO - Bot is on
2025-08-23 14:07:04 - requests - INFO - Bybit был успешно подключен
2025-08-23 14:07:43 - requests - INFO - Новый пользователь был добавлен в бд
2025-08-23 14:07:43 - requests - INFO - Основные настройки нового пользователя были заполнены
2025-08-23 14:07:43 - requests - INFO - Риск-Менеджмент настройки нового пользователя были заполнены
2025-08-23 14:07:43 - requests - INFO - Условные настройки нового пользователя были заполнены
2025-08-23 14:07:43 - requests - INFO - Дополнительные настройки нового пользователя были заполнены
2025-08-23 14:23:31 - main - INFO - Bot is off
2025-08-23 14:23:39 - main - INFO - Bot is on
2025-08-23 14:28:13 - main - INFO - Bot is off
2025-08-23 14:28:19 - main - INFO - Bot is on
2025-08-23 14:28:26 - requests - INFO - Получение риск-менеджмента настроек пользователя 899674724
2025-08-23 14:28:26 - requests - INFO - Получение риск-менеджмента настроек пользователя 899674724
2025-08-23 14:29:12 - requests - INFO - Получение риск-менеджмента настроек пользователя 899674724
2025-08-23 14:29:34 - main - INFO - Bot is off

48
requirements.txt Normal file
View File

@@ -0,0 +1,48 @@
aiofiles==24.1.0
aiogram==3.22.0
aiohappyeyeballs==2.6.1
aiohttp==3.12.15
aiosignal==1.4.0
aiosqlite==0.21.0
annotated-types==0.7.0
attrs==25.3.0
black==25.1.0
certifi==2025.8.3
charset-normalizer==3.4.3
click==8.2.1
colorama==0.4.6
dotenv==0.9.9
flake8==7.3.0
flake8-bugbear==24.12.12
flake8-pie==0.16.0
frozenlist==1.7.0
greenlet==3.2.4
idna==3.10
isort==6.0.1
magic-filter==1.0.12
mando==0.7.1
mccabe==0.7.0
multidict==6.6.4
mypy_extensions==1.1.0
nest-asyncio==1.6.0
packaging==25.0
pathspec==0.12.1
platformdirs==4.4.0
propcache==0.3.2
pybit==5.11.0
pycodestyle==2.14.0
pycryptodome==3.23.0
pydantic==2.11.7
pydantic_core==2.33.2
pyflakes==3.4.0
python-dotenv==1.1.1
radon==6.0.1
redis==6.4.0
requests==2.32.5
six==1.17.0
SQLAlchemy==2.0.43
typing-inspection==0.4.1
typing_extensions==4.14.1
urllib3==2.5.0
websocket-client==1.8.0
yarl==1.20.1