diff --git a/.env.sample b/.env.sample index 2637330..1885292 100644 --- a/.env.sample +++ b/.env.sample @@ -1,3 +1,6 @@ -TOKEN_TELEGRAM_BOT_1= -TOKEN_TELEGRAM_BOT_2= -TOKEN_TELEGRAM_BOT_3= +BOT_TOKEN=YOUR_BOT_TOKEN +DB_USER=your_username +DB_PASS=your_password +DB_HOST=your_host +DB_PORT=your_port +DB_NAME=your_database \ No newline at end of file diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..8fc57a6 --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 130 +ignore = E501 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 98a8880..8fb70e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,212 @@ -.env -!*.sample - +# Byte-compiled / optimized / DLL files __pycache__/ -*.pyc +*.py[codz] +*$py.class -env/ -venv/ -.venv/ +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +#poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +#pdm.lock +#pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +#pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments .idea /.idea -/myenv +.env +.envrc +.venv +env/ +venv/ myenv +ENV/ +env.bak/ +venv.bak/ +/alembic/versions +/alembic +alembic.ini +# Spyder project settings +.spyderproject +.spyproject -*.sqlite3 +# Rope project settings +.ropeproject -*.log +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Cursor +# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to +# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data +# refer to https://docs.cursor.com/context/ignore-files +.cursorignore +.cursorindexingignore + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ diff --git a/BybitBot_API.py b/BybitBot_API.py deleted file mode 100644 index 0f844e5..0000000 --- a/BybitBot_API.py +++ /dev/null @@ -1,51 +0,0 @@ -import asyncio -import logging.config -from aiogram import Bot, Dispatcher - -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") - -bot = Bot(token=TOKEN_TG_BOT_1) -dp = Dispatcher() - - -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") diff --git a/BybitBot_API.pyproj b/BybitBot_API.pyproj deleted file mode 100644 index 595308b..0000000 --- a/BybitBot_API.pyproj +++ /dev/null @@ -1,68 +0,0 @@ - - - Debug - 2.0 - bc1d7460-d8ca-4977-a249-0f6d6cc2375a - . - BibytBot_API.py - - - . - . - BibytBot_API - BibytBot_API - - - true - false - - - true - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/BybitBot_API.sln b/BybitBot_API.sln deleted file mode 100644 index e90ff6b..0000000 --- a/BybitBot_API.sln +++ /dev/null @@ -1,23 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.13.35825.156 d17.13 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "BibytBot_API", "BibytBot_API.pyproj", "{BC1D7460-D8CA-4977-A249-0F6D6CC2375A}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {BC1D7460-D8CA-4977-A249-0F6D6CC2375A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BC1D7460-D8CA-4977-A249-0F6D6CC2375A}.Release|Any CPU.ActiveCfg = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {9AF00E9A-19FB-4146-96C0-B86C8B1E02C0} - EndGlobalSection -EndGlobal diff --git a/README.md b/README.md index 6c8adf8..70599b7 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ nvim .env 5. Запустите бота: ```bash -python BybitBot_API.py +python run.py ``` ## Настройка автономной работы diff --git a/data/__init__.py b/app/__init__.py similarity index 100% rename from data/__init__.py rename to app/__init__.py diff --git a/app/bybit/__init__.py b/app/bybit/__init__.py new file mode 100644 index 0000000..7b4197e --- /dev/null +++ b/app/bybit/__init__.py @@ -0,0 +1,21 @@ +import logging.config + +from pybit.unified_trading import HTTP + +from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG +from database import request as rq + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("bybit") + + +async def get_bybit_client(tg_id: int) -> HTTP | None: + """ + Get bybit client + """ + try: + api_key, api_secret = await rq.get_user_api(tg_id=tg_id) + return HTTP(api_key=api_key, api_secret=api_secret) + except Exception as e: + logger.error("Error getting bybit client for user %s: %s", tg_id, e) + return None diff --git a/app/bybit/close_positions.py b/app/bybit/close_positions.py new file mode 100644 index 0000000..47a9687 --- /dev/null +++ b/app/bybit/close_positions.py @@ -0,0 +1,100 @@ +import logging.config + +from app.bybit import get_bybit_client +from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("close_positions") + + +async def close_position( + tg_id: int, symbol: str, side: str, position_idx: int, qty: float +) -> bool: + """ + Closes all positions + :param tg_id: Telegram user ID + :param symbol: symbol + :param side: side + :param position_idx: position index + :param qty: quantity + :return: bool + """ + try: + client = await get_bybit_client(tg_id) + + if side == "Buy": + r_side = "Sell" + else: + r_side = "Buy" + + response = client.place_order( + category="linear", + symbol=symbol, + side=r_side, + orderType="Market", + qty=qty, + timeInForce="GTC", + positionIdx=position_idx, + ) + if response["retCode"] == 0: + logger.info("All positions closed for %s for user %s", symbol, tg_id) + return True + else: + logger.error( + "Error closing all positions for %s for user %s", symbol, tg_id + ) + return False + except Exception as e: + logger.error( + "Error closing all positions for %s for user %s: %s", symbol, tg_id, e + ) + return False + + +async def cancel_order(tg_id: int, symbol: str, order_id: str) -> bool: + """ + Cancel order by order id + """ + try: + client = await get_bybit_client(tg_id) + + cancel_resp = client.cancel_order( + category="linear", symbol=symbol, orderId=order_id + ) + + if cancel_resp.get("retCode") == 0: + return True + else: + logger.error( + "Error canceling order for user %s: %s", + tg_id, + cancel_resp.get("retMsg"), + ) + return False + except Exception as e: + logger.error("Error canceling order for user %s: %s", tg_id, e) + return False + + +async def cancel_all_orders(tg_id: int) -> bool: + """ + Cancel all open orders + """ + try: + client = await get_bybit_client(tg_id) + cancel_resp = client.cancel_all_orders(category="linear", settleCoin="USDT") + + if cancel_resp.get("retCode") == 0: + logger.info("All orders canceled for user %s", tg_id) + return True + else: + logger.error( + "Error canceling order for user %s: %s", + tg_id, + cancel_resp.get("retMsg"), + ) + return False + + except Exception as e: + logger.error("Error canceling order for user %s: %s", tg_id, e) + return False diff --git a/app/bybit/get_functions/__init__.py b/app/bybit/get_functions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/bybit/get_functions/get_balance.py b/app/bybit/get_functions/get_balance.py new file mode 100644 index 0000000..4d8f4e1 --- /dev/null +++ b/app/bybit/get_functions/get_balance.py @@ -0,0 +1,28 @@ +import logging.config + +from app.bybit import get_bybit_client +from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("get_balance") + + +async def get_balance(tg_id: int) -> bool | dict: + """ + Get balance bybit + """ + client = await get_bybit_client(tg_id=tg_id) + + try: + response = client.get_wallet_balance(accountType="UNIFIED") + if response["retCode"] == 0: + info = response["result"]["list"][0] + return info + else: + logger.error( + "Error getting balance for user %s: %s", tg_id, response.get("retMsg") + ) + return False + except Exception as e: + logger.error("Error connecting to Bybit for user %s: %s", tg_id, e) + return False diff --git a/app/bybit/get_functions/get_instruments_info.py b/app/bybit/get_functions/get_instruments_info.py new file mode 100644 index 0000000..0207bef --- /dev/null +++ b/app/bybit/get_functions/get_instruments_info.py @@ -0,0 +1,28 @@ +import logging.config + +from app.bybit import get_bybit_client +from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("get_instruments_info") + + +async def get_instruments_info(tg_id: int, symbol: str) -> dict | None: + """ + Get instruments info + :param tg_id: int - User ID + :param symbol: str - Symbol + :return: dict - Instruments info + """ + try: + client = await get_bybit_client(tg_id=tg_id) + response = client.get_instruments_info(category="linear", symbol=symbol) + if response["retCode"] == 0: + logger.info("Instruments info for user: %s", tg_id) + return response["result"]["list"][0] + else: + logger.error("Error getting price: %s", tg_id) + return None + except Exception as e: + logger.error("Error connecting to Bybit for user %s: %s", tg_id, e) + return None diff --git a/app/bybit/get_functions/get_positions.py b/app/bybit/get_functions/get_positions.py new file mode 100644 index 0000000..1f6606a --- /dev/null +++ b/app/bybit/get_functions/get_positions.py @@ -0,0 +1,129 @@ +import logging.config + +from app.bybit import get_bybit_client +from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("get_positions") + + +async def get_active_positions(tg_id: int) -> list | None: + """ + Get active positions for a user + """ + try: + client = await get_bybit_client(tg_id) + response = client.get_positions(category="linear", settleCoin="USDT") + + if response["retCode"] == 0: + positions = response.get("result", {}).get("list", []) + active_symbols = [ + pos.get("symbol") for pos in positions if float(pos.get("size", 0)) > 0 + ] + if active_symbols: + logger.info("Active positions for user: %s", tg_id) + return positions + else: + logger.warning("No active positions found for user: %s", tg_id) + return ["No active positions found"] + else: + logger.error( + "Error getting active positions for user %s: %s", + tg_id, + response["retMsg"], + ) + return None + except Exception as e: + logger.error("Error getting active positions for user %s: %s", tg_id, e) + return None + + +async def get_active_positions_by_symbol(tg_id: int, symbol: str) -> dict | None: + """ + Get active positions for a user by symbol + """ + try: + client = await get_bybit_client(tg_id) + response = client.get_positions(category="linear", symbol=symbol) + + if response["retCode"] == 0: + positions = response.get("result", {}).get("list", []) + if positions: + logger.info("Active positions for user: %s", tg_id) + return positions + else: + logger.warning("No active positions found for user: %s", tg_id) + return None + else: + logger.error( + "Error getting active positions for user %s: %s", + tg_id, + response["retMsg"], + ) + return None + except Exception as e: + logger.error("Error getting active positions for user %s: %s", tg_id, e) + return None + + +async def get_active_orders(tg_id: int) -> list | None: + """ + Get active orders + """ + try: + client = await get_bybit_client(tg_id) + response = client.get_open_orders( + category="linear", + settleCoin="USDT", + limit=50, + ) + + if response["retCode"] == 0: + orders = response.get("result", {}).get("list", []) + active_orders = [ + pos.get("symbol") for pos in orders if float(pos.get("qty", 0)) > 0 + ] + if active_orders: + logger.info("Active orders for user: %s", tg_id) + return orders + else: + logger.warning("No active orders found for user: %s", tg_id) + return ["No active orders found"] + else: + logger.error( + "Error getting active orders for user %s: %s", tg_id, response["retMsg"] + ) + return None + except Exception as e: + logger.error("Error getting active orders for user %s: %s", tg_id, e) + return None + + +async def get_active_orders_by_symbol(tg_id: int, symbol: str) -> dict | None: + """ + Get active orders by symbol + """ + try: + client = await get_bybit_client(tg_id) + response = client.get_open_orders( + category="linear", + symbol=symbol, + limit=50, + ) + + if response["retCode"] == 0: + orders = response.get("result", {}).get("list", []) + if orders: + logger.info("Active orders for user: %s", tg_id) + return orders + else: + logger.warning("No active orders found for user: %s", tg_id) + return None + else: + logger.error( + "Error getting active orders for user %s: %s", tg_id, response["retMsg"] + ) + return None + except Exception as e: + logger.error("Error getting active orders for user %s: %s", tg_id, e) + return None diff --git a/app/bybit/get_functions/get_tickers.py b/app/bybit/get_functions/get_tickers.py new file mode 100644 index 0000000..2e75b64 --- /dev/null +++ b/app/bybit/get_functions/get_tickers.py @@ -0,0 +1,35 @@ +import logging.config + +from app.bybit import get_bybit_client +from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("get_tickers") + + +async def get_tickers(tg_id: int, symbol: str) -> dict | None: + """ + Get tickers + :param tg_id: int Telegram ID + :param symbol: str Symbol + :return: dict + """ + try: + client = await get_bybit_client(tg_id=tg_id) + response = client.get_tickers(category="linear", symbol=symbol) + if response["retCode"] == 0: + tickers = response["result"]["list"] + # USDT quoteCoin + usdt_tickers = [t for t in tickers if t.get("symbol", "").endswith("USDT")] + if usdt_tickers: + logger.info("USDT tickers for user: %s", tg_id) + return usdt_tickers[0] + else: + logger.warning("No USDT tickers found for user: %s", tg_id) + return None + else: + logger.error("Error getting price: %s", tg_id) + return None + except Exception as e: + logger.error("Error connecting to Bybit for user %s: %s", tg_id, e) + return None diff --git a/app/bybit/logger_bybit/__init__.py b/app/bybit/logger_bybit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/bybit/logger_bybit/logger_bybit.py b/app/bybit/logger_bybit/logger_bybit.py new file mode 100644 index 0000000..7e43b91 --- /dev/null +++ b/app/bybit/logger_bybit/logger_bybit.py @@ -0,0 +1,129 @@ +import os + +current_directory = os.path.dirname(os.path.abspath(__file__)) +log_directory = os.path.join(current_directory, "loggers") +error_log_directory = os.path.join(log_directory, "errors") +os.makedirs(log_directory, exist_ok=True) +os.makedirs(error_log_directory, exist_ok=True) +log_filename = os.path.join(log_directory, "app.log") +error_log_filename = os.path.join(error_log_directory, "error.log") + +LOGGING_CONFIG = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "default": { + "format": "BYBIT: %(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", + "level": "DEBUG", + }, + "error_file": { + "class": "logging.handlers.TimedRotatingFileHandler", + "filename": error_log_filename, + "when": "midnight", + "interval": 1, + "backupCount": 30, + "formatter": "default", + "encoding": "utf-8", + "level": "ERROR", + }, + "console": { + "class": "logging.StreamHandler", + "formatter": "default", + "level": "DEBUG", + }, + }, + "loggers": { + "profile_bybit": { + "handlers": ["console", "timed_rotating_file", "error_file"], + "level": "DEBUG", + "propagate": False, + }, + "get_balance": { + "handlers": ["console", "timed_rotating_file", "error_file"], + "level": "DEBUG", + "propagate": False, + }, + "price_symbol": { + "handlers": ["console", "timed_rotating_file", "error_file"], + "level": "DEBUG", + "propagate": False, + }, + "bybit": { + "handlers": ["console", "timed_rotating_file", "error_file"], + "level": "DEBUG", + "propagate": False, + }, + "web_socket": { + "handlers": ["console", "timed_rotating_file", "error_file"], + "level": "DEBUG", + "propagate": False, + }, + "get_tickers": { + "handlers": ["console", "timed_rotating_file", "error_file"], + "level": "DEBUG", + "propagate": False, + }, + "set_margin_mode": { + "handlers": ["console", "timed_rotating_file", "error_file"], + "level": "DEBUG", + "propagate": False, + }, + "set_switch_margin_mode": { + "handlers": ["console", "timed_rotating_file", "error_file"], + "level": "DEBUG", + "propagate": False, + }, + "set_switch_position_mode": { + "handlers": ["console", "timed_rotating_file", "error_file"], + "level": "DEBUG", + "propagate": False, + }, + "set_leverage": { + "handlers": ["console", "timed_rotating_file", "error_file"], + "level": "DEBUG", + "propagate": False, + }, + "get_instruments_info": { + "handlers": ["console", "timed_rotating_file", "error_file"], + "level": "DEBUG", + "propagate": False, + }, + "get_positions": { + "handlers": ["console", "timed_rotating_file", "error_file"], + "level": "DEBUG", + "propagate": False, + }, + "open_positions": { + "handlers": ["console", "timed_rotating_file", "error_file"], + "level": "DEBUG", + "propagate": False, + }, + "close_positions": { + "handlers": ["console", "timed_rotating_file", "error_file"], + "level": "DEBUG", + "propagate": False, + }, + "telegram_message_handler": { + "handlers": ["console", "timed_rotating_file", "error_file"], + "level": "DEBUG", + "propagate": False, + }, + "set_tp_sl": { + "handlers": ["console", "timed_rotating_file", "error_file"], + "level": "DEBUG", + "propagate": False, + }, + }, +} diff --git a/app/bybit/open_positions.py b/app/bybit/open_positions.py new file mode 100644 index 0000000..5e5eed5 --- /dev/null +++ b/app/bybit/open_positions.py @@ -0,0 +1,443 @@ +import logging.config + +from pybit.exceptions import InvalidRequestError + +import database.request as rq +from app.bybit import get_bybit_client +from app.bybit.get_functions.get_balance import get_balance +from app.bybit.get_functions.get_instruments_info import get_instruments_info +from app.bybit.get_functions.get_tickers import get_tickers +from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG +from app.bybit.set_functions.set_leverage import ( + set_leverage, + set_leverage_to_buy_and_sell, +) +from app.bybit.set_functions.set_margin_mode import set_margin_mode +from app.bybit.set_functions.set_switch_position_mode import set_switch_position_mode +from app.helper_functions import check_limit_price, get_liquidation_price, safe_float + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("open_positions") + + +async def start_trading_cycle( + tg_id: int, side: str, switch_side_mode: bool +) -> str | None: + """ + Start trading cycle + :param tg_id: Telegram user ID + :param side: Buy or Sell + :param switch_side_mode: switch_side_mode + """ + try: + symbol = await rq.get_user_symbol(tg_id=tg_id) + additional_data = await rq.get_user_additional_settings(tg_id=tg_id) + risk_management_data = await rq.get_user_risk_management(tg_id=tg_id) + + trade_mode = additional_data.trade_mode + margin_type = additional_data.margin_type + leverage = additional_data.leverage + leverage_to_buy = additional_data.leverage_to_buy + leverage_to_sell = additional_data.leverage_to_sell + order_type = additional_data.order_type + conditional_order_type = additional_data.conditional_order_type + order_quantity = additional_data.order_quantity + limit_price = additional_data.limit_price + trigger_price = additional_data.trigger_price + martingale_factor = additional_data.martingale_factor + max_bets_in_series = additional_data.max_bets_in_series + take_profit_percent = risk_management_data.take_profit_percent + stop_loss_percent = risk_management_data.stop_loss_percent + max_risk_percent = risk_management_data.max_risk_percent + + mode = 3 if trade_mode == "Both_Sides" else 0 + await set_switch_position_mode(tg_id=tg_id, symbol=symbol, mode=mode) + await set_margin_mode(tg_id=tg_id, margin_mode=margin_type) + if trade_mode == "Both_Sides" and margin_type == "ISOLATED_MARGIN": + await set_leverage_to_buy_and_sell( + tg_id=tg_id, + symbol=symbol, + leverage_to_buy=leverage_to_buy, + leverage_to_sell=leverage_to_sell, + ) + else: + await set_leverage( + tg_id=tg_id, + symbol=symbol, + leverage=leverage, + ) + + res = await open_positions( + tg_id=tg_id, + symbol=symbol, + side=side, + order_type=order_type, + conditional_order_type=conditional_order_type, + order_quantity=order_quantity, + limit_price=limit_price, + trigger_price=trigger_price, + trade_mode=trade_mode, + margin_type=margin_type, + leverage=leverage, + leverage_to_buy=leverage_to_buy, + leverage_to_sell=leverage_to_sell, + take_profit_percent=take_profit_percent, + stop_loss_percent=stop_loss_percent, + max_risk_percent=max_risk_percent, + ) + + if res == "OK": + await rq.set_user_deal( + tg_id=tg_id, + symbol=symbol, + last_side=side, + current_step=1, + trade_mode=trade_mode, + margin_type=margin_type, + leverage=leverage, + leverage_to_buy=leverage_to_buy, + leverage_to_sell=leverage_to_sell, + order_type="Market", + conditional_order_type=conditional_order_type, + order_quantity=order_quantity, + limit_price=limit_price, + trigger_price=trigger_price, + martingale_factor=martingale_factor, + max_bets_in_series=max_bets_in_series, + take_profit_percent=take_profit_percent, + stop_loss_percent=stop_loss_percent, + max_risk_percent=max_risk_percent, + switch_side_mode=switch_side_mode, + ) + return "OK" + return ( + res + if res + in { + "Limit price is out min price", + "Limit price is out max price", + "Risk is too high for this trade", + "estimated will trigger liq", + "ab not enough for new order", + "InvalidRequestError", + "Order does not meet minimum order value", + "position idx not match position mode", + "Qty invalid", + "The number of contracts exceeds maximum limit allowed", + } + else None + ) + + except Exception as e: + logger.error("Error in start_trading: %s", e) + return None + + +async def trading_cycle( + tg_id: int, symbol: str, reverse_side: str, size: str +) -> str | None: + try: + user_deals_data = await rq.get_user_deal_by_symbol(tg_id=tg_id, symbol=symbol) + trade_mode = user_deals_data.trade_mode + order_type = user_deals_data.order_type + conditional_order_type = user_deals_data.conditional_order_type + margin_type = user_deals_data.margin_type + leverage = user_deals_data.leverage + leverage_to_buy = user_deals_data.leverage_to_buy + leverage_to_sell = user_deals_data.leverage_to_sell + limit_price = user_deals_data.limit_price + trigger_price = user_deals_data.trigger_price + take_profit_percent = user_deals_data.take_profit_percent + stop_loss_percent = user_deals_data.stop_loss_percent + max_risk_percent = user_deals_data.max_risk_percent + max_bets_in_series = user_deals_data.max_bets_in_series + martingale_factor = user_deals_data.martingale_factor + current_step = user_deals_data.current_step + switch_side_mode = user_deals_data.switch_side_mode + + mode = 3 if trade_mode == "Both_Sides" else 0 + await set_switch_position_mode(tg_id=tg_id, symbol=symbol, mode=mode) + await set_margin_mode(tg_id=tg_id, margin_mode=margin_type) + if trade_mode == "Both_Sides" and margin_type == "ISOLATED_MARGIN": + await set_leverage_to_buy_and_sell( + tg_id=tg_id, + symbol=symbol, + leverage_to_buy=leverage_to_buy, + leverage_to_sell=leverage_to_sell, + ) + else: + await set_leverage( + tg_id=tg_id, + symbol=symbol, + leverage=leverage, + ) + + if reverse_side == "Buy": + real_side = "Sell" + else: + real_side = "Buy" + + side = real_side + + if switch_side_mode: + side = "Sell" if real_side == "Buy" else "Buy" + + next_quantity = safe_float(size) * ( + safe_float(martingale_factor) ** current_step + ) + current_step += 1 + + if max_bets_in_series < current_step: + return "Max bets in series" + + res = await open_positions( + tg_id=tg_id, + symbol=symbol, + side=side, + order_type="Market", + conditional_order_type=conditional_order_type, + order_quantity=next_quantity, + limit_price=limit_price, + trigger_price=trigger_price, + trade_mode=trade_mode, + margin_type=margin_type, + leverage=leverage, + leverage_to_buy=leverage_to_buy, + leverage_to_sell=leverage_to_sell, + take_profit_percent=take_profit_percent, + stop_loss_percent=stop_loss_percent, + max_risk_percent=max_risk_percent, + ) + + if res == "OK": + await rq.set_user_deal( + tg_id=tg_id, + symbol=symbol, + last_side=side, + current_step=current_step, + trade_mode=trade_mode, + margin_type=margin_type, + leverage=leverage, + leverage_to_buy=leverage_to_buy, + leverage_to_sell=leverage_to_sell, + order_type=order_type, + conditional_order_type=conditional_order_type, + order_quantity=next_quantity, + limit_price=limit_price, + trigger_price=trigger_price, + martingale_factor=martingale_factor, + max_bets_in_series=max_bets_in_series, + take_profit_percent=take_profit_percent, + stop_loss_percent=stop_loss_percent, + max_risk_percent=max_risk_percent, + switch_side_mode=switch_side_mode, + ) + return "OK" + + return ( + res + if res + in { + "Risk is too high for this trade", + "ab not enough for new order", + "InvalidRequestError", + "The number of contracts exceeds maximum limit allowed", + } + else None + ) + + except Exception as e: + logger.error("Error in trading_cycle: %s", e) + return None + + +async def open_positions( + tg_id: int, + side: str, + symbol: str, + order_type: str, + conditional_order_type: str, + order_quantity: float, + limit_price: float, + trigger_price: float, + trade_mode: str, + margin_type: str, + leverage: str, + leverage_to_buy: str, + leverage_to_sell: str, + take_profit_percent: float, + stop_loss_percent: float, + max_risk_percent: float, +) -> str | None: + try: + client = await get_bybit_client(tg_id=tg_id) + + risk_management_data = await rq.get_user_risk_management(tg_id=tg_id) + commission_fee = risk_management_data.commission_fee + wallet = await get_balance(tg_id=tg_id) + user_balance = wallet.get("totalWalletBalance", 0) + instruments_resp = await get_instruments_info(tg_id=tg_id, symbol=symbol) + get_order_prices = instruments_resp.get("priceFilter") + min_price = safe_float(get_order_prices.get("minPrice")) + max_price = safe_float(get_order_prices.get("maxPrice")) + get_ticker = await get_tickers(tg_id, symbol=symbol) + price_symbol = safe_float(get_ticker.get("lastPrice")) or 0 + + if order_type == "Conditional": + po_trigger_price = str(trigger_price) + trigger_direction = 1 if trigger_price > price_symbol else 2 + if conditional_order_type == "Limit": + error = check_limit_price(limit_price, min_price, max_price) + if error in { + "Limit price is out min price", + "Limit price is out max price", + }: + return error + + order_type = "Limit" + price_for_calc = limit_price + tpsl_mode = "Partial" + else: + order_type = "Market" + price_for_calc = trigger_price + tpsl_mode = "Full" + else: + if order_type == "Limit": + error = check_limit_price(limit_price, min_price, max_price) + if error in { + "Limit price is out min price", + "Limit price is out max price", + }: + return error + + price_for_calc = limit_price + tpsl_mode = "Partial" + else: + order_type = "Market" + price_for_calc = price_symbol + tpsl_mode = "Full" + po_trigger_price = None + trigger_direction = None + + if trade_mode == "Both_Sides": + po_position_idx = 1 if side == "Buy" else 2 + if margin_type == "ISOLATED_MARGIN": + get_leverage = safe_float( + leverage_to_buy if side == "Buy" else leverage_to_sell + ) + else: + get_leverage = safe_float(leverage) + else: + po_position_idx = 0 + get_leverage = safe_float(leverage) + + potential_loss = ( + safe_float(order_quantity) + * safe_float(price_for_calc) + * (stop_loss_percent / 100) + ) + adjusted_loss = potential_loss / get_leverage + allowed_loss = safe_float(user_balance) * (max_risk_percent / 100) + + if adjusted_loss > allowed_loss: + return "Risk is too high for this trade" + + # Get fee rates + fee_info = client.get_fee_rates(category="linear", symbol=symbol) + + # Check if commission fee is enabled + commission_fee_percent = 0.0 + if commission_fee == "Yes_commission_fee": + commission_fee_percent = safe_float( + fee_info["result"]["list"][0]["takerFeeRate"] + ) + + total_commission = price_for_calc * order_quantity * commission_fee_percent + tp_multiplier = 1 + (take_profit_percent / 100) + if total_commission > 0: + tp_multiplier += total_commission + + if margin_type == "ISOLATED_MARGIN": + liq_long, liq_short = await get_liquidation_price( + tg_id=tg_id, + entry_price=price_for_calc, + symbol=symbol, + leverage=get_leverage, + ) + + if (liq_long > 0 or liq_short > 0) and price_for_calc > 0: + if side == "Buy": + base_tp = price_for_calc + (price_for_calc - liq_long) + take_profit_price = base_tp + total_commission + else: + base_tp = price_for_calc - (liq_short - price_for_calc) + take_profit_price = base_tp - total_commission + take_profit_price = max(take_profit_price, 0) + else: + take_profit_price = None + + stop_loss_price = None + else: + if side == "Buy": + take_profit_price = price_for_calc * tp_multiplier + stop_loss_price = price_for_calc * (1 - stop_loss_percent / 100) + else: + take_profit_price = price_for_calc * ( + 1 - (take_profit_percent / 100) - total_commission + ) + stop_loss_price = price_for_calc * (1 + stop_loss_percent / 100) + + take_profit_price = max(take_profit_price, 0) + stop_loss_price = max(stop_loss_price, 0) + + # Place order + order_params = { + "category": "linear", + "symbol": symbol, + "side": side, + "orderType": order_type, + "qty": str(order_quantity), + "triggerDirection": trigger_direction, + "triggerPrice": po_trigger_price, + "triggerBy": "LastPrice", + "timeInForce": "GTC", + "positionIdx": po_position_idx, + "tpslMode": tpsl_mode, + "takeProfit": str(take_profit_price) if take_profit_price else None, + "stopLoss": str(stop_loss_price) if stop_loss_price else None, + } + + if order_type == "Conditional": + if conditional_order_type == "Limit": + order_params["price"] = str(limit_price) + if order_type == "Limit": + order_params["price"] = str(limit_price) + + response = client.place_order(**order_params) + + if response["retCode"] == 0: + logger.info("Position opened for user: %s", tg_id) + return "OK" + + logger.error("Error opening position for user: %s", tg_id) + return None + + except InvalidRequestError as e: + error_text = str(e) + known_errors = { + "Order does not meet minimum order value": "Order does not meet minimum order value", + "estimated will trigger liq": "estimated will trigger liq", + "ab not enough for new order": "ab not enough for new order", + "position idx not match position mode": "position idx not match position mode", + "Qty invalid": "Qty invalid", + } + for key, msg in known_errors.items(): + if key in error_text: + logger.error(msg) + return msg + logger.error("InvalidRequestError: %s", e) + return "InvalidRequestError" + + except Exception as e: + logger.error("Error opening position for user %s: %s", tg_id, e) + return None diff --git a/app/bybit/profile_bybit.py b/app/bybit/profile_bybit.py new file mode 100644 index 0000000..3ec82a7 --- /dev/null +++ b/app/bybit/profile_bybit.py @@ -0,0 +1,45 @@ +import logging.config + +from aiogram.fsm.context import FSMContext +from aiogram.types import Message + +import app.telegram.keyboards.inline as kbi +import database.request as rq +from app.bybit.get_functions.get_balance import get_balance +from app.bybit.get_functions.get_tickers import get_tickers +from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("profile_bybit") + + +async def user_profile_bybit(tg_id: int, message: Message, state: FSMContext) -> None: + """Get user profile bybit""" + try: + await state.clear() + wallet = await get_balance(tg_id=tg_id) + + if wallet: + balance = wallet.get("totalWalletBalance", "0") + symbol = await rq.get_user_symbol(tg_id=tg_id) + get_tickers_info = await get_tickers(tg_id=tg_id, symbol=symbol) + price_symbol = get_tickers_info.get("lastPrice") or 0 + await message.answer( + text=f"💎Ваш профиль Bybit:\n\n" + f"⚖️ Баланс: {float(balance):,.2f} USD\n" + f"📊Торговая пара: {symbol}\n" + f"$$$ Цена: {float(price_symbol):,.4f}\n\n" + f"Краткая инструкция:\n" + f"1. Укажите торговую пару (например: BTCUSDT).\n" + f"2. В настройках выставьте все необходимые параметры.\n" + f"3. Нажмите кнопку 'Начать торговлю' и выберите режим торговли.\n", + reply_markup=kbi.main_menu, + ) + else: + await message.answer( + text="Ошибка при подключении, повторите попытку", + reply_markup=kbi.connect_the_platform, + ) + logger.error("Error processing user profile for user %s", tg_id) + except Exception as e: + logger.error("Error processing user profile for user %s: %s", tg_id, e) diff --git a/app/bybit/set_functions/__init__.py b/app/bybit/set_functions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/bybit/set_functions/set_leverage.py b/app/bybit/set_functions/set_leverage.py new file mode 100644 index 0000000..4e5580a --- /dev/null +++ b/app/bybit/set_functions/set_leverage.py @@ -0,0 +1,96 @@ +import logging.config + +from pybit import exceptions + +from app.bybit import get_bybit_client +from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("set_leverage") + + +async def set_leverage(tg_id: int, symbol: str, leverage: str) -> bool: + """ + Set leverage + :param tg_id: int - User ID + :param symbol: str - Symbol + :param leverage: str - Leverage + :return: bool + """ + try: + client = await get_bybit_client(tg_id=tg_id) + response = client.set_leverage( + category="linear", + symbol=symbol, + buyLeverage=str(leverage), + sellLeverage=str(leverage), + ) + if response["retCode"] == 0: + logger.info( + "Leverage set to %s for user: %s", + leverage, + tg_id, + ) + return True + else: + logger.error("Error setting leverage: %s", response["retMsg"]) + return False + except exceptions.InvalidRequestError as e: + if "110043" in str(e): + logger.debug( + "Leverage set to %s for user: %s", + leverage, + tg_id, + ) + return True + else: + raise + except Exception as e: + logger.error("Error connecting to Bybit for user %s: %s", tg_id, e) + return False + + +async def set_leverage_to_buy_and_sell( + tg_id: int, symbol: str, leverage_to_buy: str, leverage_to_sell: str +) -> bool: + """ + Set leverage to buy and sell + :param tg_id: int - User ID + :param symbol: str - Symbol + :param leverage_to_buy: str - Leverage to buy + :param leverage_to_sell: str - Leverage to sell + :return: bool + """ + try: + client = await get_bybit_client(tg_id=tg_id) + response = client.set_leverage( + category="linear", + symbol=symbol, + buyLeverage=str(leverage_to_buy), + sellLeverage=str(leverage_to_sell), + ) + if response["retCode"] == 0: + logger.info( + "Leverage set to %s and %s for user: %s", + leverage_to_buy, + leverage_to_sell, + tg_id, + ) + return True + else: + logger.error("Error setting leverage for buy and sell for user: %s", tg_id) + return False + except exceptions.InvalidRequestError as e: + if "110043" in str(e): + logger.debug( + "Leverage set to %s and %s for user: %s", + leverage_to_buy, + leverage_to_sell, + tg_id, + ) + return True + else: + raise + except Exception as e: + logger.error("Error connecting to Bybit for user %s: %s", tg_id, e) + return False diff --git a/app/bybit/set_functions/set_margin_mode.py b/app/bybit/set_functions/set_margin_mode.py new file mode 100644 index 0000000..6357995 --- /dev/null +++ b/app/bybit/set_functions/set_margin_mode.py @@ -0,0 +1,28 @@ +import logging.config + +from app.bybit import get_bybit_client +from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("set_margin_mode") + + +async def set_margin_mode(tg_id: int, margin_mode: str) -> bool: + """ + Set margin mode + :param tg_id: int - User ID + :param margin_mode: str - Margin mode + :return: bool + """ + try: + client = await get_bybit_client(tg_id=tg_id) + response = client.set_margin_mode(setMarginMode=margin_mode) + if response["retCode"] == 0: + logger.info("Margin mode set to %s for user: %s", margin_mode, tg_id) + return True + else: + logger.error("Error setting margin mode: %s", tg_id) + return False + except Exception as e: + logger.error("Error connecting to Bybit for user %s: %s", tg_id, e) + return False diff --git a/app/bybit/set_functions/set_switch_position_mode.py b/app/bybit/set_functions/set_switch_position_mode.py new file mode 100644 index 0000000..046bedb --- /dev/null +++ b/app/bybit/set_functions/set_switch_position_mode.py @@ -0,0 +1,54 @@ +import logging.config + +from app.bybit import get_bybit_client +from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("set_switch_position_mode") + + +async def set_switch_position_mode(tg_id: int, symbol: str, mode: int) -> str | bool: + """ + Set switch position mode + :param tg_id: int - User ID + :param symbol: str - Symbol + :param mode: int - Mode + :return: bool + """ + try: + client = await get_bybit_client(tg_id=tg_id) + response = client.switch_position_mode( + category="linear", + symbol=symbol, + mode=mode, + ) + if response["retCode"] == 0: + logger.info("Switch position mode set successfully") + return True + else: + logger.error("Error setting switch position mode for user: %s", tg_id) + return False + except Exception as e: + if str(e).startswith("Position mode is not modified"): + logger.debug( + "Position mode is not modified for user: %s", + tg_id, + ) + return True + if str(e).startswith( + "You have an existing position, so position mode cannot be switched" + ): + logger.debug( + "You have an existing position, so position mode cannot be switched for user: %s", + tg_id, + ) + return "You have an existing position, so position mode cannot be switched" + if str(e).startswith("Open orders exist, so you cannot change position mode"): + logger.debug( + "Open orders exist, so you cannot change position mode for user: %s", + tg_id, + ) + return "Open orders exist, so you cannot change position mode" + else: + logger.error("Error connecting to Bybit for user %s: %s", tg_id, e) + return False diff --git a/app/bybit/set_functions/set_tp_sl.py b/app/bybit/set_functions/set_tp_sl.py new file mode 100644 index 0000000..eded48f --- /dev/null +++ b/app/bybit/set_functions/set_tp_sl.py @@ -0,0 +1,45 @@ +import logging.config + +from app.bybit import get_bybit_client +from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("set_tp_sl") + + +async def set_tp_sl_for_position( + tg_id: int, + symbol: str, + take_profit_price: float, + stop_loss_price: float, + position_idx: int, +) -> bool: + """ + Set take profit and stop loss for a symbol. + :param tg_id: Telegram user ID + :param symbol: Symbol to set take profit and stop loss for + :param take_profit_price: Take profit price + :param stop_loss_price: Stop loss price + :param position_idx: Position index + :return: bool + """ + try: + client = await get_bybit_client(tg_id) + resp = client.set_trading_stop( + category="linear", + symbol=symbol, + takeProfit=str(round(take_profit_price, 5)), + stopLoss=str(round(stop_loss_price, 5)), + positionIdx=position_idx, + tpslMode="Full", + ) + + if resp.get("retCode") == 0: + logger.info("TP/SL for %s has been set", symbol) + return True + else: + logger.error("Error setting TP/SL for %s: %s", symbol, resp.get("retMsg")) + return False + except Exception as e: + logger.error("Error setting TP/SL for %s: %s", symbol, e) + return False diff --git a/app/bybit/telegram_message_handler.py b/app/bybit/telegram_message_handler.py new file mode 100644 index 0000000..6ac14f1 --- /dev/null +++ b/app/bybit/telegram_message_handler.py @@ -0,0 +1,237 @@ +import logging.config + +import app.telegram.keyboards.inline as kbi +import database.request as rq +from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG +from app.bybit.open_positions import trading_cycle +from app.helper_functions import format_value, safe_float, safe_int + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("telegram_message_handler") + + +class TelegramMessageHandler: + def __init__(self, telegram_bot): + self.telegram_bot = telegram_bot + + async def format_position_update(self, message): + pass + + async def format_order_update(self, message, tg_id): + try: + order_data = message.get("data", [{}])[0] + symbol = format_value(order_data.get("symbol")) + qty = format_value(order_data.get("qty")) + order_type = format_value(order_data.get("orderType")) + order_type_rus = ( + "Рыночный" + if order_type == "Market" + else "Лимитный" if order_type == "Limit" else "Нет данных" + ) + side = format_value(order_data.get("side")) + side_rus = ( + "Покупка" + if side == "Buy" + else "Продажа" if side == "Sell" else "Нет данных" + ) + order_status = format_value(order_data.get("orderStatus")) + price = format_value(order_data.get("price")) + trigger_price = format_value(order_data.get("triggerPrice")) + take_profit = format_value(order_data.get("takeProfit")) + stop_loss = format_value(order_data.get("stopLoss")) + + position_idx = safe_int(order_data.get("positionIdx")) + position_idx_rus = ( + "Односторонний" + if position_idx == 0 + else ( + "Покупка в режиме хеджирования" + if position_idx == 1 + else ( + "Продажа в режиме хеджирования" + if position_idx == 2 + else "Нет данных" + ) + ) + ) + + status_map = { + "New": "Ордер создан", + "Cancelled": "Ордер отменен", + "Deactivated": "Ордер деактивирован", + "Untriggered": "Условный ордер выставлен", + } + + if order_status == "Filled" or order_status not in status_map: + return None + + status_text = status_map[order_status] + + text = ( + f"{status_text}:\n" + f"Торговая пара: {symbol}\n" + f"Режим позиции: {position_idx_rus}\n" + f"Количество: {qty}\n" + f"Тип ордера: {order_type_rus}\n" + f"Движение: {side_rus}\n" + ) + if price and price != "0": + text += f"Цена: {price}\n" + if take_profit and take_profit != "Нет данных": + text += f"Тейк-профит: {take_profit}\n" + if stop_loss and stop_loss != "Нет данных": + text += f"Стоп-лосс: {stop_loss}\n" + if trigger_price and trigger_price != "Нет данных": + text += f"Триггер цена: {trigger_price}\n" + + await self.telegram_bot.send_message( + chat_id=tg_id, text=text, reply_markup=kbi.profile_bybit + ) + except Exception as e: + logger.error("Error in format_order_update: %s", e) + + async def format_execution_update(self, message, tg_id): + try: + execution = message.get("data", [{}])[0] + closed_size = format_value(execution.get("closedSize")) + symbol = format_value(execution.get("symbol")) + exec_price = format_value(execution.get("execPrice")) + exec_fee = format_value(execution.get("execFee")) + exec_qty = format_value(execution.get("execQty")) + order_type = format_value(execution.get("orderType")) + order_type_rus = ( + "Рыночный" + if order_type == "Market" + else "Лимитный" if order_type == "Limit" else "Нет данных" + ) + side = format_value(execution.get("side")) + side_rus = ( + "Покупка" + if side == "Buy" + else "Продажа" if side == "Sell" else "Нет данных" + ) + + if safe_float(closed_size) == 0: + await rq.set_fee_user_auto_trading( + tg_id=tg_id, symbol=symbol, side=side, fee=safe_float(exec_fee) + ) + if side == "Buy": + res_side = "Sell" + else: + res_side = "Buy" + user_auto_trading = await rq.get_user_auto_trading( + tg_id=tg_id, symbol=symbol, side=res_side + ) + + if user_auto_trading is not None and user_auto_trading.fee is not None: + fee = user_auto_trading.fee + else: + fee = 0 + + exec_pnl = format_value(execution.get("execPnl")) + risk_management_data = await rq.get_user_risk_management(tg_id=tg_id) + commission_fee = risk_management_data.commission_fee + + if commission_fee == "Yes_commission_fee": + total_pnl = safe_float(exec_pnl) - safe_float(exec_fee) - fee + else: + total_pnl = safe_float(exec_pnl) + + header = ( + "Сделка закрыта:" if safe_float(closed_size) > 0 else "Сделка открыта:" + ) + text = f"{header}\n" f"Торговая пара: {symbol}\n" + + if safe_float(closed_size) > 0: + text += f"Количество закрытых сделок: {closed_size}\n" + + text += ( + f"Цена исполнения: {exec_price}\n" + f"Количество исполненных сделок: {exec_qty}\n" + f"Тип ордера: {order_type_rus}\n" + f"Движение: {side_rus}\n" + f"Комиссия за сделку: {exec_fee}\n" + ) + + if safe_float(closed_size) > 0: + text += f"\nРеализованная прибыль: {total_pnl:.7f}\n" + + await self.telegram_bot.send_message( + chat_id=tg_id, text=text, reply_markup=kbi.profile_bybit + ) + + auto_trading = ( + user_auto_trading.auto_trading if user_auto_trading else False + ) + user_symbols = user_auto_trading.symbol if user_auto_trading else None + + if ( + auto_trading + and safe_float(closed_size) > 0 + and user_symbols is not None + ): + if safe_float(total_pnl) > 0: + profit_text = "📈 Прибыль достигнута\n" + await self.telegram_bot.send_message( + chat_id=tg_id, text=profit_text, reply_markup=kbi.profile_bybit + ) + if side == "Buy": + r_side = "Sell" + else: + r_side = "Buy" + await rq.set_auto_trading( + tg_id=tg_id, symbol=symbol, auto_trading=False, side=r_side + ) + user_deals_data = await rq.get_user_deal_by_symbol( + tg_id=tg_id, symbol=symbol + ) + if user_deals_data and user_deals_data.switch_side_mode: + await rq.set_auto_trading( + tg_id=tg_id, symbol=symbol, auto_trading=False, side=side + ) + else: + open_order_text = "\n❗️ Сделка закрылась в минус, открываю новую сделку с увеличенной ставкой.\n" + await self.telegram_bot.send_message( + chat_id=tg_id, text=open_order_text + ) + res = await trading_cycle( + tg_id=tg_id, symbol=symbol, reverse_side=side, size=closed_size + ) + + if res == "OK": + pass + else: + errors = { + "Max bets in series": "❗️ Максимальное количество сделок в серии достигнуто", + "Risk is too high for this trade": "❗️ Риск сделки слишком высок для продолжения", + "ab not enough for new order": "❗️ Недостаточно средств для продолжения торговли", + "InvalidRequestError": "❗️ Недостаточно средств для размещения нового ордера с заданным количеством и плечом.", + "The number of contracts exceeds maximum limit allowed": "❗️ Количество контрактов превышает допустимое максимальное количество контрактов", + } + error_text = errors.get( + res, "❗️ Не удалось открыть новую сделку" + ) + if side == "Buy": + r_side = "Sell" + else: + r_side = "Buy" + await rq.set_auto_trading( + tg_id=tg_id, symbol=symbol, auto_trading=False, side=r_side + ) + user_deals_data = await rq.get_user_deal_by_symbol( + tg_id=tg_id, symbol=symbol + ) + if user_deals_data and user_deals_data.switch_side_mode: + await rq.set_auto_trading( + tg_id=tg_id, + symbol=symbol, + auto_trading=False, + side=side, + ) + await self.telegram_bot.send_message( + chat_id=tg_id, + text=error_text, + reply_markup=kbi.profile_bybit, + ) + except Exception as e: + logger.error("Error in telegram_message_handler: %s", e) diff --git a/app/bybit/web_socket.py b/app/bybit/web_socket.py new file mode 100644 index 0000000..b89fb6e --- /dev/null +++ b/app/bybit/web_socket.py @@ -0,0 +1,120 @@ +import asyncio +import logging.config + +from pybit.unified_trading import WebSocket + +import database.request as rq +from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG +from app.bybit.telegram_message_handler import TelegramMessageHandler + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("web_socket") + + +class WebSocketBot: + """ + Class to handle WebSocket connections and messages. + """ + + def __init__(self, telegram_bot): + """Initialize the TradingBot class.""" + self.telegram_bot = telegram_bot + self.ws_private = None + self.user_messages = {} + self.user_sockets = {} + self.user_keys = {} + self.loop = None + self.message_handler = TelegramMessageHandler(telegram_bot) + + async def run_user_check_loop(self): + """Run a loop to check for users and connect them to the WebSocket.""" + self.loop = asyncio.get_running_loop() + while True: + users = await WebSocketBot.get_users_from_db() + for user in users: + tg_id = user.tg_id + api_key, api_secret = await rq.get_user_api(tg_id=tg_id) + + if not api_key or not api_secret: + continue + + keys_stored = self.user_keys.get(tg_id) + if tg_id in self.user_sockets and keys_stored == (api_key, api_secret): + continue + + if tg_id in self.user_sockets: + self.user_sockets.clear() + self.user_messages.clear() + self.user_keys.clear() + logger.info("Closed old websocket for user %s due to key change", tg_id) + + success = await self.try_connect_user(api_key, api_secret, tg_id) + if success: + self.user_keys[tg_id] = (api_key, api_secret) + self.user_messages.setdefault( + tg_id, {"position": None, "order": None, "execution": None} + ) + logger.info("User %s connected to WebSocket", tg_id) + else: + await asyncio.sleep(30) + + await asyncio.sleep(10) + + async def clear_user_sockets(self): + """Clear the user_sockets and user_messages dictionaries.""" + self.user_sockets.clear() + self.user_messages.clear() + self.user_keys.clear() + logger.info("Cleared user_sockets") + + async def try_connect_user(self, api_key, api_secret, tg_id): + """Try to connect a user to the WebSocket.""" + try: + self.ws_private = WebSocket( + testnet=False, + channel_type="private", + api_key=api_key, + api_secret=api_secret, + ) + + self.user_sockets[tg_id] = self.ws_private + # Connect to the WebSocket private channel + # Handle position updates + self.ws_private.position_stream( + lambda msg: self.loop.call_soon_threadsafe( + asyncio.create_task, self.handle_position_update(msg) + ) + ) + # Handle order updates + self.ws_private.order_stream( + lambda msg: self.loop.call_soon_threadsafe( + asyncio.create_task, self.handle_order_update(msg, tg_id) + ) + ) + # Handle execution updates + self.ws_private.execution_stream( + lambda msg: self.loop.call_soon_threadsafe( + asyncio.create_task, self.handle_execution_update(msg, tg_id) + ) + ) + return True + except Exception as e: + logger.error("Error connecting user %s: %s", tg_id, e) + return False + + async def handle_position_update(self, message): + """Handle position updates.""" + await self.message_handler.format_position_update(message) + + async def handle_order_update(self, message, tg_id): + """Handle order updates.""" + await self.message_handler.format_order_update(message, tg_id) + + async def handle_execution_update(self, message, tg_id): + """Handle execution updates.""" + await self.message_handler.format_execution_update(message, tg_id) + + @staticmethod + async def get_users_from_db(): + """Get all users from the database.""" + return await rq.get_users() diff --git a/app/helper_functions.py b/app/helper_functions.py new file mode 100644 index 0000000..d12f42f --- /dev/null +++ b/app/helper_functions.py @@ -0,0 +1,186 @@ +import logging.config + +from app.bybit import get_bybit_client +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("helper_functions") + + +def safe_float(val) -> float: + """ + Function to safely convert string to float + """ + try: + if val is None or val == "": + return 0.0 + return float(val) + except (ValueError, TypeError): + logger.error("Error converting value to float: %s", val) + return 0.0 + + +def is_number(value: str) -> bool: + """ + Checks if a given string represents a number. + + Args: + value (str): The string to check. + + Returns: + bool: True if the string represents a number, False otherwise. + """ + try: + # Convert the string to a float + num = float(value) + # Check if the number is positive + if num <= 0: + return False + # Check if the string contains "+" or "-" + if "+" in value or "-" in value: + return False + # Check if the string contains only digits + allowed_chars = set("0123456789.") + if not all(ch in allowed_chars for ch in value): + return False + return True + except ValueError: + return False + + +def is_int(value: str) -> bool: + """ + Checks if a given string represents an integer. + + Args: + value (str): The string to check. + + Returns: + bool: True if the string represents an integer, False otherwise. + """ + # Check if the string contains only digits + if not value.isdigit(): + return False + # Convert the string to an integer + num = int(value) + return num > 0 + + +def is_int_for_timer(value: str) -> bool | int: + """ + Checks if a given string represents an integer for timer. + + Args: + value (str): The string to check. + + Returns: + bool: True if the string represents an integer, False otherwise. + """ + # Check if the string contains only digits + try: + num = int(value) + + if num >= 0: + return num + else: + return False + except ValueError: + return False + + +def get_base_currency(symbol: str) -> str: + """ + Extracts the base currency from a symbol string. + + Args: + symbol (str): The symbol string to extract the base currency from. + + Returns: + str: The base currency extracted from the symbol string. + """ + if symbol.endswith("USDT"): + return symbol[:-4] + return symbol + + +def safe_int(value, default=0) -> int: + """ + Integer conversion with default value. + """ + try: + return int(value) + except (ValueError, TypeError): + return default + + +def format_value(value) -> str: + """ + Function to format value + """ + if not value or value.strip() == "": + return "Нет данных" + return value + + +def check_limit_price(limit_price, min_price, max_price) -> str | None: + """ + Function to check limit price + """ + if limit_price < min_price: + return "Limit price is out min price" + if limit_price > max_price: + return "Limit price is out max price" + return None + + +async def get_liquidation_price( + tg_id: int, symbol: str, entry_price: float, leverage: float +) -> tuple[float, float]: + """ + Function to get liquidation price + """ + try: + client = await get_bybit_client(tg_id=tg_id) + get_risk_info = client.get_risk_limit(category="linear", symbol=symbol) + risk_list = get_risk_info.get("result", {}).get("list", []) + risk_level = risk_list[0] if risk_list else {} + maintenance_margin_rate = safe_float(risk_level.get("maintenanceMargin")) + + liq_price_long = entry_price * (1 - 1 / leverage + maintenance_margin_rate) + liq_price_short = entry_price * (1 + 1 / leverage - maintenance_margin_rate) + + liq_price = liq_price_long, liq_price_short + + return liq_price + except Exception as e: + logger.error("Error getting liquidation price: %s", e) + return 0, 0 + + +async def calculate_total_budget( + quantity, martingale_factor, max_steps, commission_fee_percent +) -> float: + """ + Calculate the total budget for a series of trading steps. + + Args: + quantity (float): The initial quantity of the asset. + martingale_factor (float): The factor by which the quantity is multiplied for each step. + max_steps (int): The maximum number of trading steps. + commission_fee_percent (float): The commission fee percentage. + + Returns: + float: The total budget for the series of trading steps. + """ + total = 0 + for step in range(max_steps): + set_quantity = quantity * (martingale_factor**step) + if commission_fee_percent == 0: + # Commission fee is not added to the position size + r_quantity = set_quantity + else: + # Commission fee is added to the position size + r_quantity = set_quantity * (1 + 2 * commission_fee_percent) + + total += r_quantity + return total diff --git a/app/services/Bybit/functions/Add_Bybit_API.py b/app/services/Bybit/functions/Add_Bybit_API.py deleted file mode 100644 index e425446..0000000 --- a/app/services/Bybit/functions/Add_Bybit_API.py +++ /dev/null @@ -1,111 +0,0 @@ -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 = '''Подключение Bybit аккаунта - -1. Зарегистрируйтесь или войдите в свой аккаунт на Bybit (https://www.bybit.com/). -2. В личном кабинете выберите раздел API. -3. Создание нового API ключа - - Нажмите кнопку Create New Key (Создать новый ключ). - - Выберите системно-сгенерированный ключ. - - Укажите название API ключа (любое). - - Выберите права доступа для торговли (Trade). - - Можно ограничить доступ по IP для безопасности. -4. Подтверждение создания - - Подтвердите создание ключа. - - Отправьте чат-роботу. - -Важно: сохраните отдельно API Key и Secret Key в надежном месте. Secret ключ отображается только один раз. - ''' - - 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) \ No newline at end of file diff --git a/app/services/Bybit/functions/Futures.py b/app/services/Bybit/functions/Futures.py deleted file mode 100644 index 7a314ac..0000000 --- a/app/services/Bybit/functions/Futures.py +++ /dev/null @@ -1,896 +0,0 @@ -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("orderQty", 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: - text = ( - 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" - ) - return text - if order_type == "Market": - text = ( - f"Сделка открыта:\n" - f"Торговая пара: {symbol}\n" - f"Цена исполнения: {entry_price:.6f}\n" - f"Количество: {qty}\n" - f"Тип ордера: {order_type}\n" - f"Движение: {movement}\n" - f"Комиссия за сделку: {commission:.6f}" - ) - return text - 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)) - order_status = msg.get("orderStatus", "N/A") - symbol = msg.get("symbol", "N/A") - order_type = msg.get("orderType", "N/A") - side = msg.get("side", "") - trigger_price = msg.get("triggerPrice", "N/A") - - 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"Комиссия за сделку: {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" - ) - 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" - ) - return text - elif order_status.lower() == "untriggered": - text = ( - f"Условный ордер создан:\n" - f"Торговая пара: {symbol}\n" - f"Цена: {price:.6f}\n" - f"Триггер цена: {trigger_price}\n" - f"Количество: {qty}\n" - f"Тип ордера: {order_type}\n" - f"Движение: {movement}\n" - ) - return text - elif order_status.lower() == "deactivated": - text = ( - f"Условный ордер отменен:\n" - f"Торговая пара: {symbol}\n" - f"Цена: {price:.6f}\n" - f"Триггер цена: {trigger_price}\n" - f"Количество: {qty}\n" - f"Тип ордера: {order_type}\n" - f"Движение: {movement}\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): - """ - Вычисляет общий бюджет серии ставок с учётом цены пары, комиссии и кредитного плеча. - - Параметры: - - starting_quantity_usdt: стартовый размер ставки в долларах (USD) - - martingale_factor: множитель увеличения ставки при каждом проигрыше - - max_steps: максимальное количество шагов удвоения ставки - - commission_fee_percent: процент комиссии на одну операцию (открытие или закрытие) - - leverage: кредитное плечо - - current_price: текущая цена актива (например BTCUSDT) - - Возвращает: - - общий бюджет в долларах, который необходимо иметь на счету - """ - total = 0 - for step in range(max_steps): - base_quantity = starting_quantity * (martingale_factor ** step) - if commission_fee_percent == 0: - # Комиссия уже включена в сумму ставки, поэтому реальный размер позиции меньше - quantity = base_quantity / (1 + commission_fee_percent) - else: - # Комиссию добавляем сверху - quantity = base_quantity * (1 + commission_fee_percent) - - total += quantity - return total - - -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 data is not None: - await rq.update_trigger_price(tg_id, 0.0) - - 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 trading_mode == 'Switch': - side = data_main_stgs.get("last_side") - else: - side = "Buy" if trading_mode == "Long" else "Sell" - - if pnl < 0: - - 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 rq.update_starting_quantity(tg_id=tg_id, num=next_quantity) - 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, 1) - num = data_main_stgs.get("base_quantity") - await rq.update_starting_quantity(tg_id=tg_id, num=num) - 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)}") - data = msg.get("data", [{}])[0] - tg_id = message.from_user.id - if data is not None: - await rq.update_trigger_price(tg_id, 0.0) - 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" - ) - trigger_price = await rq.get_trigger_price(tg_id) - 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") - 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 - - if commission_fee_percent > 0: - # Добавляем к тейк-профиту процент комиссии - tp_multiplier = 1 + (loss_profit / 100) + commission_fee_percent - else: - tp_multiplier = 1 + (loss_profit / 100) - - - if order_type == "Limit" and limit_price: - price_for_calc = limit_price - else: - price_for_calc = entry_price - - balance = await balance_g.get_balance(tg_id, message) - 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_to_set} 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": - # Открываем позицию - if trigger_price and float(trigger_price) > 0: - response = client.place_order( - category="linear", - symbol=symbol, - side=side, - orderType="Stop" if order_type == "Conditional" else order_type, - qty=str(quantity), - price=(str(limit_price) if order_type == "Limit" and limit_price else None), - triggerPrice=str(trigger_price), - triggerBy="LastPrice", - triggerDirection=2 if side == "Buy" else 1, - timeInForce="GTC", - orderLinkId=f"deal_{symbol}_{int(time.time())}", - ) - else: - # Обычный ордер, без триггера - 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: - return True - 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": - base_tp = avg_price + (avg_price - liq_price) - take_profit_price = base_tp * (1 + commission_fee_percent) - else: - base_tp = avg_price - (liq_price - avg_price) - take_profit_price = base_tp * (1 - commission_fee_percent) - - 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 - 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 * tp_multiplier - stop_loss_price = base_price * (1 - loss_profit / 100) - else: - take_profit_price = base_price * (1 - (loss_profit / 100) - (commission_fee_percent)) - 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, - triggerPrice=str(trigger_price) if trigger_price and float(trigger_price) > 0 else None, - triggerBy="LastPrice" if trigger_price and float(trigger_price) > 0 else None, - triggerDirection=2 if side == "Buy" else 1 if trigger_price and float(trigger_price) > 0 else None, - 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" and "Market" - ) - orders = response.get("result", {}).get("list", []) - limit_orders = [order for order in orders] - - 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 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('triggerPrice')}\n" - f"Количество: {order.get('qty')}\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} по таймеру отменено.") diff --git a/app/services/Bybit/functions/balance.py b/app/services/Bybit/functions/balance.py deleted file mode 100644 index 35093d4..0000000 --- a/app/services/Bybit/functions/balance.py +++ /dev/null @@ -1,52 +0,0 @@ -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 diff --git a/app/services/Bybit/functions/bybit_ws.py b/app/services/Bybit/functions/bybit_ws.py deleted file mode 100644 index 5eea159..0000000 --- a/app/services/Bybit/functions/bybit_ws.py +++ /dev/null @@ -1,115 +0,0 @@ -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) diff --git a/app/services/Bybit/functions/functions.py b/app/services/Bybit/functions/functions.py deleted file mode 100644 index 6f6b817..0000000 --- a/app/services/Bybit/functions/functions.py +++ /dev/null @@ -1,540 +0,0 @@ -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 -# import app.services.Bybit.functions.balance as balance_g - -from app.states.States import (state_update_symbol, - 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): {float(balance):.2f}\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_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) - # data_risk_stgs = await rq.get_user_risk_management_settings(tg_id) - # client = await get_bybit_client(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", "По направлению") - # martingale_factor = safe_float(data_main_stgs.get('martingale_factor')) - # max_martingale_steps = int(data_main_stgs.get("maximal_quantity", 0)) - # commission_fee = data_risk_stgs.get("commission_fee") - # fee_info = client.get_fee_rates(category='linear', symbol=symbol) - - - 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 - - # 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, - # ) - - # 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 - - await message.answer("Начинаю торговлю с использованием текущих настроек...") - await rq.update_trigger(tg_id=tg_id, trigger="Автоматический") - - 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 rq.update_trigger(tg_id, "Ручной") - 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("Выберите тип сделки:", - reply_markup=inline_markup.my_deals_select_markup) - except Exception as e: - logger.error("Произошла ошибка при выборе типа сделки: %s", 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("Произошла ошибка при выборе сделки: %s", 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("Произошла ошибка при выборе сделки: %s", 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("Произошла ошибка при выборе ордера: %s", 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("Произошла ошибка при выборе сделки: %s", 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 - symbol = await rq.get_symbol(tg_id) - - await close_user_trade(tg_id, symbol) - await rq.update_trigger(tg_id, "Ручной") - await rq.update_martingale_step(tg_id, 1) - - 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) - - symbol = await rq.get_symbol(tg_id) - - await close_user_trade(tg_id, symbol) - await rq.update_trigger(tg_id, "Ручной") - await rq.update_martingale_step(tg_id, 1) - 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 и сообщает пользователю об отмене. - """ - try: - await state.clear() - await callback.message.answer("Отменено!", reply_markup=inline_markup.back_to_main) - await callback.answer() - except Exception as e: - logger.error("Ошибка при обработке отмены: %s", e) diff --git a/app/services/Bybit/functions/get_valid_symbol.py b/app/services/Bybit/functions/get_valid_symbol.py deleted file mode 100644 index 1a8d830..0000000 --- a/app/services/Bybit/functions/get_valid_symbol.py +++ /dev/null @@ -1,40 +0,0 @@ -import logging.config -from pybit.unified_trading import HTTP -import app.telegram.database.requests as rq -from logger_helper.logger_helper import LOGGING_CONFIG - -logging.config.dictConfig(LOGGING_CONFIG) -logger = logging.getLogger("get_valid_symbol") - - -async def get_valid_symbols(user_id: int, symbol: str) -> bool: - """ - Проверяет существование торговой пары на Bybit в категории 'linear'. - - Эта функция получает API-ключи пользователя из базы данных и - с помощью Bybit API проверяет наличие данного символа в списке - торговых инструментов категории 'linear'. - - Args: - user_id (int): Идентификатор пользователя Telegram. - symbol (str): Торговый символ (валютная пара), например "BTCUSDT". - - Returns: - bool: Возвращает True, если торговая пара существует, иначе False. - - Raises: - Исключения подавляются и вызывается False, если произошла ошибка запроса к API. - """ - api_key = await rq.get_bybit_api_key(user_id) - secret_key = await rq.get_bybit_secret_key(user_id) - client = HTTP(api_key=api_key, api_secret=secret_key) - - try: - resp = client.get_instruments_info(category='linear', symbol=symbol) - # Проверка наличия результата и непустого списка инструментов - if resp.get('retCode') == 0 and resp.get('result') and resp['result'].get('list'): - return len(resp['result']['list']) > 0 - return False - except Exception as e: - logging.error(f"Ошибка при получении списка инструментов: {e}") - return False diff --git a/app/services/Bybit/functions/min_qty.py b/app/services/Bybit/functions/min_qty.py deleted file mode 100644 index 51faa53..0000000 --- a/app/services/Bybit/functions/min_qty.py +++ /dev/null @@ -1,52 +0,0 @@ -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 diff --git a/app/services/Bybit/functions/price_symbol.py b/app/services/Bybit/functions/price_symbol.py deleted file mode 100644 index d86737f..0000000 --- a/app/services/Bybit/functions/price_symbol.py +++ /dev/null @@ -1,32 +0,0 @@ -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 diff --git a/app/states/States.py b/app/states/States.py deleted file mode 100644 index ba211fa..0000000 --- a/app/states/States.py +++ /dev/null @@ -1,72 +0,0 @@ -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 state_trigger_price(StatesGroup): - """FSM состояние для установки лимита.""" - price = State() - -class CloseTradeTimerState(StatesGroup): - """FSM состояние ожидания задержки перед закрытием сделки.""" - waiting_for_delay = State() - waiting_for_trade = State() - - -class SetTP_SL_State(StatesGroup): - """FSM состояние для установки TP и SL.""" - waiting_for_take_profit = State() - waiting_for_stop_loss = State() - - -class update_risk_management_settings(StatesGroup): - """FSM состояние для обновления настроек управления рисками.""" - price_profit = State() - price_loss = State() - max_risk_deal = State() - commission_fee = State() - - -class state_reg_bybit_api(StatesGroup): - """FSM состояние для регистрации API Bybit.""" - api_key = State() - secret_key = State() - - -class condition_settings(StatesGroup): - """FSM состояние для настройки условий трейдинга.""" - trigger = State() - timer = State() - volatilty = State() - volume = State() - integration = State() - use_tv_signal = State() - - -class update_main_settings(StatesGroup): - """FSM состояние для обновления основных настройок.""" - trading_mode = State() - size_leverage = State() - margin_type = State() - martingale_factor = State() - starting_quantity = State() - maximal_quantity = State() - switch_mode_enabled = State() \ No newline at end of file diff --git a/app/telegram/Keyboards/inline_keyboards.py b/app/telegram/Keyboards/inline_keyboards.py deleted file mode 100644 index 023c36a..0000000 --- a/app/telegram/Keyboards/inline_keyboards.py +++ /dev/null @@ -1,224 +0,0 @@ -from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup -from aiogram.utils.keyboard import InlineKeyboardBuilder - -start_markup = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text="🔥 Начать торговлю", callback_data="clb_start_chatbot_message")] -]) - -settings_markup = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text="Запуск", callback_data='clb_start_trading')] -]) - -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')], - [InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api_message')], - back_btn_to_main -]) - -connect_bybit_api_markup = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api')] -]) - -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_start_chatbot_trading')], - [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="Маркет", callback_data="entry_order_type:Market"), - InlineKeyboardButton(text="Лимит", 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_martingale_reset')], - [InlineKeyboardButton(text='Максимальное кол-во ставок', callback_data='clb_change_maximum_quantity')], - - 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='commission_fee')], - - back_btn_list_settings, - back_btn_to_main -]) - -condition_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text='Таймер', callback_data='clb_change_timer'), - InlineKeyboardButton(text='Тип позиции', callback_data='clb_update_entry_type')], - [InlineKeyboardButton(text='Триггер цена', callback_data='clb_change_trigger_price'), - InlineKeyboardButton(text='Лимит цена', callback_data='clb_change_limit_price')], - # - # [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_to_main -]) - -back_to_condition_settings = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text='Назад', callback_data='clb_change_condition_settings')], - back_btn_to_main -]) - -additional_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text='Сохранить шаблон', callback_data='clb_change_save_pattern'), - InlineKeyboardButton(text='Автозапуск', callback_data='clb_change_auto_start')], - - [InlineKeyboardButton(text='Уведомления', callback_data='clb_change_notifications')], - - 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_switch")], - # InlineKeyboardButton(text="Смарт", callback_data="trade_mode_smart")], - - back_btn_list_settings, - back_btn_to_main -]) - -margin_type_markup = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text="Изолированный", callback_data="margin_type_isolated"), - InlineKeyboardButton(text="Кросс", callback_data="margin_type_cross")], - - back_btn_list_settings -]) - -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")], -]) diff --git a/app/telegram/Keyboards/reply_keyboards.py b/app/telegram/Keyboards/reply_keyboards.py deleted file mode 100644 index df7c56c..0000000 --- a/app/telegram/Keyboards/reply_keyboards.py +++ /dev/null @@ -1,6 +0,0 @@ -from aiogram.types import ReplyKeyboardMarkup, KeyboardButton - -base_buttons_markup = ReplyKeyboardMarkup(keyboard=[ - [KeyboardButton(text="👤 Профиль")], - # [KeyboardButton(text="Настройки")] -], resize_keyboard=True, one_time_keyboard=False) \ No newline at end of file diff --git a/app/telegram/__init__.py b/app/telegram/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/telegram/database/models.py b/app/telegram/database/models.py deleted file mode 100644 index 73c56cd..0000000 --- a/app/telegram/database/models.py +++ /dev/null @@ -1,316 +0,0 @@ -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:///data/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_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"), 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) - base_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, default=0) - trigger_price = mapped_column(Numeric(18, 15), nullable=True, default=0) - last_side = mapped_column(String(10), default='Buy') - trading_start_stop = mapped_column(Integer(), default=0) - - -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"), 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=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"), unique=True, nullable=True) - - 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) - tradingview_cues = mapped_column(Boolean, default=False) - 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"), 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) - - # Заполнение таблиц - modes = ['Long', 'Short', 'Switch', 'Smart'] - for mode in modes: - result = await conn.execute(select(Trading_Mode).where(Trading_Mode.mode == mode)) - if not result.first(): - logger.info("Заполение таблицы режима торговли") - await conn.execute(Trading_Mode.__table__.insert().values(mode=mode)) - - types = ['Isolated', 'Cross'] - for type in types: - result = await conn.execute(select(Margin_type).where(Margin_type.type == type)) - if not result.first(): - logger.info("Заполение таблицы типов маржи") - await conn.execute(Margin_type.__table__.insert().values(type=type)) - - 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(User_Main_Settings.__table__.insert().values(last_side=side)) - - order_type = ['Limit', 'Market'] - for typ in order_type: - result = await conn.execute(select(User_Main_Settings).where(User_Main_Settings.entry_order_type == typ)) - if not result.first(): - logger.info("Заполение таблицы типов ордеров") - await conn.execute(User_Main_Settings.__table__.insert().values(entry_order_type=typ)) diff --git a/app/telegram/database/requests.py b/app/telegram/database/requests.py deleted file mode 100644 index f796e1a..0000000 --- a/app/telegram/database/requests.py +++ /dev/null @@ -1,621 +0,0 @@ -import logging.config - -from logger_helper.logger_helper import LOGGING_CONFIG -from datetime import datetime, timedelta -from typing import Any - -from app.telegram.database.models import ( - async_session, - User_Telegram_Id as UTi, - User_Main_Settings as UMS, - User_Bybit_API as UBA, - User_Symbol, - User_Risk_Management_Settings as URMS, - User_Condition_Settings as UCS, - User_Additional_Settings as UAS, - Trading_Mode, - Margin_type, - Trigger, - USER_DEALS, - UserTimer, -) - -from sqlalchemy import select, update - -logging.config.dictConfig(LOGGING_CONFIG) -logger = logging.getLogger("requests") - - -# --- Функции сохранения в БД --- - -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("Новый пользователь был добавлен в бд %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)) - - if not settings: - session.add(UMS( - tg_id=tg_id, - trading_mode=trading_mode, - margin_type=margin_type, - )) - - 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)) - - if not settings: - session.add(URMS( - tg_id=tg_id - )) - - logger.info("Риск-Менеджмент настройки нового пользователя были заполнены %s", tg_id) - - await session.commit() - - -async def set_new_user_default_condition_settings(tg_id, trigger) -> None: - """ - Создаёт условные настройки по умолчанию. - - Args: - tg_id (int): Telegram ID пользователя. - trigger (Any): Значение триггера по умолчанию. - """ - async with async_session() as session: - settings = await session.scalar(select(UCS).where(UCS.tg_id == tg_id)) - - if not settings: - session.add(UCS( - tg_id=tg_id, - trigger=trigger - )) - - 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)) - - if not settings: - session.add(UAS( - tg_id=tg_id, - )) - - logger.info("Дополнительные настройки нового пользователя были заполнены %s", tg_id) - - await session.commit() - - -# --- Функции получения данных из БД --- - -async def check_user(tg_id): - """ - Проверяет наличие пользователя в базе. - - Args: - tg_id (int): Telegram ID пользователя. - - Returns: - Optional[UTi]: Пользователь или None. - """ - async with async_session() as session: - user = await session.scalar(select(UTi).where(UTi.tg_id == tg_id)) - return user - - -async def get_bybit_api_key(tg_id): - """Получить API ключ Bybit пользователя.""" - async with async_session() as session: - api_key = await session.scalar(select(UBA.api_key).where(UBA.tg_id == tg_id)) - return api_key - - -async def get_bybit_secret_key(tg_id): - """Получить секретный ключ Bybit пользователя.""" - async with async_session() as session: - secret_key = await session.scalar(select(UBA.secret_key).where(UBA.tg_id == tg_id)) - return secret_key - - -async def get_symbol(tg_id): - """Получить символ пользователя.""" - async with async_session() as session: - symbol = await session.scalar(select(User_Symbol.symbol).where(User_Symbol.tg_id == tg_id)) - return symbol - - -async def get_user_trades(tg_id): - """Получить сделки пользователя.""" - async with async_session() as session: - query = select(USER_DEALS.symbol, USER_DEALS.side).where(USER_DEALS.tg_id == tg_id) - result = await session.execute(query) - 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: - 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() - - -# --- Более мелкие обновления и запросы по настройкам --- - -async def update_trade_mode_user(tg_id, trading_mode) -> None: - """Обновить режим торговли пользователя.""" - async with async_session() as session: - mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.mode == trading_mode)) - - if mode: - logger.info("Изменён торговый режим для пользователя %s", tg_id) - await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(trading_mode=mode)) - - await session.commit() - - -async def delete_user_trade(tg_id: int, symbol: str): - """Удалить сделку пользователя.""" - async with async_session() as session: - await session.execute( - USER_DEALS.__table__.delete().where( - (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, - 'trigger_price': user.trigger_price, - 'martingale_step': user.martingale_step, - 'last_side': user.last_side, - 'trading_start_stop': user.trading_start_stop, - 'base_quantity': user.base_quantity, - } - return data - - -async def get_user_risk_management_settings(tg_id): - """Получить риск-менеджмента настройки пользователя.""" - async with async_session() as session: - user = await session.scalar(select(URMS).where(URMS.tg_id == tg_id)) - - if user: - logger.info("Получение риск-менеджмента настроек пользователя %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("Изменен тип маржи %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.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.commit() - - -async def update_base_quantity(tg_id, num): - """Обновить размер следующей ставки пользователя.""" - async with async_session() as session: - await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(base_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.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.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 update_trigger_price(tg_id, price): - """Обновить условную цену пользователя.""" - async with async_session() as session: - await session.execute( - update(UMS) - .where(UMS.tg_id == tg_id) - .values(trigger_price=str(price)) - ) - await session.commit() - -async def get_trigger_price(tg_id): - """Получить условную цену пользователя как float, либо None.""" - async with async_session() as session: - result = await session.execute( - select(UMS.trigger_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 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() diff --git a/app/telegram/functions/additional_settings/settings.py b/app/telegram/functions/additional_settings/settings.py deleted file mode 100644 index 5090d0b..0000000 --- a/app/telegram/functions/additional_settings/settings.py +++ /dev/null @@ -1,38 +0,0 @@ -import app.telegram.Keyboards.inline_keyboards as inline_markup - -import app.telegram.database.requests as rq - -async def reg_new_user_default_additional_settings(id, message): - tg_id = id - - await rq.set_new_user_default_additional_settings(tg_id) - -async def main_settings_message(id, message): - text = '''Дополнительные параметры - -- Сохранить как шаблон стратегии: да / нет -- Автозапуск после сохранения: да / нет -- Уведомления в Telegram: включено / отключено ''' - - await message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.additional_settings_markup) - -async def save_pattern_message(message, state): - text = '''Сохранение шаблона - - Описание... ''' - - await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup) - -async def auto_start_message(message, state): - text = '''Автозапуск - - Описание... ''' - - await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup) - -async def notifications_message(message, state): - text = '''Уведомления - - Описание... ''' - - await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_on_off_markup) \ No newline at end of file diff --git a/app/telegram/functions/condition_settings/settings.py b/app/telegram/functions/condition_settings/settings.py deleted file mode 100644 index bfc9eec..0000000 --- a/app/telegram/functions/condition_settings/settings.py +++ /dev/null @@ -1,208 +0,0 @@ -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 -from app.states.States import state_limit_price, state_update_entry_type, state_trigger_price -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(tg_id) - - await rq.set_new_user_default_condition_settings(tg_id, trigger) - - -async def main_settings_message(id, message): - data = await rq.get_user_main_settings(id) - entry_order_type = data['entry_order_type'] - - if entry_order_type == "Market": - entry_order_type_rus = "Маркет" - elif entry_order_type == "Limit": - entry_order_type_rus = "Лимит" - else: - entry_order_type_rus = "Условный" - - trigger_price = data['trigger_price'] or 0.0 - limit_price = data['limit_order_price'] or 0.0 - - text = f""" Условия запуска -- Таймер: установить таймер / удалить таймер -- Тип позиции: {entry_order_type_rus} -- Триггер цена: {trigger_price:,.4f} -- Лимит цена: {limit_price:,.4f} -""" - await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.condition_settings_markup) - - -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() - - -@condition_settings_router.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() - - -@condition_settings_router.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': - order_type_rus = 'Лимит' - else: - order_type_rus = 'Маркет' - 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_rus}", - reply_markup=inline_markup.back_to_condition_settings) - await callback.answer() - except Exception as e: - logger.error("Произошла ошибка при обновлении типа входа в позицию: %s", e) - await callback.message.answer("Произошла ошибка при обновлении типа входа в позицию", - reply_markup=inline_markup.back_to_condition_settings) - await state.clear() - - -@condition_settings_router.callback_query(F.data == 'clb_change_limit_price') -async def set_limit_price_callback(callback: CallbackQuery, state: FSMContext) -> None: - await state.set_state(state_limit_price.price) - await callback.message.answer("Введите цену лимитного ордера:", reply_markup=inline_markup.cancel) - await callback.answer() - - -@condition_settings_router.message(state_limit_price.price) -async def process_limit_price_input(message: Message, state: FSMContext) -> None: - try: - price = float(message.text) - await state.update_data(price=price) - await rq.update_limit_price(tg_id=message.from_user.id, price=price) - await message.answer(f"Цена лимитного ордера установлена: {price}", reply_markup=inline_markup.back_to_condition_settings) - await state.clear() - except ValueError: - await message.reply("Пожалуйста, введите корректную цену.") - - -@condition_settings_router.callback_query(F.data == 'clb_change_trigger_price') -async def change_trigger_price_callback(callback: CallbackQuery, state: FSMContext) -> None: - await state.set_state(state_trigger_price.price) - await callback.message.answer("Введите цену триггера:", reply_markup=inline_markup.cancel) - await callback.answer() - - -@condition_settings_router.message(state_trigger_price.price) -async def process_trigger_price_input(message: Message, state: FSMContext) -> None: - try: - price = float(message.text) - await state.update_data(price=price) - await rq.update_trigger_price(tg_id=message.from_user.id, price=price) - await message.answer(f"Цена триггера установлена: {price}", reply_markup=inline_markup.back_to_condition_settings) - await state.clear() - except ValueError: - await message.reply("Пожалуйста, введите корректную цену.") - - - -async def filter_volatility_message(message, state): - text = '''Фильтр волатильности - - Описание... ''' - - await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_on_off_markup) - - -async def external_cues_message(message, state): - text = '''Внешние сигналы - - Описание... ''' - - await message.answer(text=text, parse_mode='html', reply_markup=None) - - -async def trading_cues_message(message, state): - text = '''Использование сигналов - - Описание... ''' - - await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup) - - -async def webhook_message(message, state): - text = '''Скиньте ссылку на webhook (если есть trading view): ''' - - await message.answer(text=text, parse_mode='html') - - -async def ai_analytics_message(message, state): - text = '''ИИ - Аналитика - - Описание... ''' - - await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup) diff --git a/app/telegram/functions/functions.py b/app/telegram/functions/functions.py deleted file mode 100644 index 19ab77b..0000000 --- a/app/telegram/functions/functions.py +++ /dev/null @@ -1,29 +0,0 @@ -import app.telegram.Keyboards.inline_keyboards as inline_markup -import app.telegram.Keyboards.reply_keyboards as reply_markup - -async def start_message(message): - username = '' - - 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""" Привет {username}! 👋""", parse_mode='html') - await message.answer("Добро пожаловать в чат-робот для автоматизации трейдинга — вашего надежного помощника для анализа рынка и принятия взвешенных решений.", - parse_mode='html', reply_markup=inline_markup.start_markup) - -async def profile_message(username, message): - await message.answer(f""" @{username} - -Баланс -⭐️ 0 - -""", parse_mode='html', reply_markup=inline_markup.settings_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) \ No newline at end of file diff --git a/app/telegram/functions/main_settings/settings.py b/app/telegram/functions/main_settings/settings.py deleted file mode 100644 index 92ee946..0000000 --- a/app/telegram/functions/main_settings/settings.py +++ /dev/null @@ -1,348 +0,0 @@ -from aiogram import Router -import logging.config -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.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() - - -async def reg_new_user_default_main_settings(id, message): - tg_id = id - - trading_mode = await rq.get_for_registration_trading_mode() - margin_type = await rq.get_for_registration_margin_type() - - await rq.set_new_user_default_main_settings(tg_id, trading_mode, margin_type) - - -async def main_settings_message(id, message): - try: - data = await rq.get_user_main_settings(id) - tg_id = id - - 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) - - 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, - ) - - await message.answer(f"""Основные настройки - - - Режим торговли: {data['trading_mode']} - - Состояние свитча: {data['switch_state']} - - Направление последней сделки: {data['last_side']} - - Тип маржи: {data['margin_type']} - - Размер кредитного плеча: х{data['size_leverage']} - - Ставка: {data['starting_quantity']} - - Коэффициент мартингейла: {data['martingale_factor']} - - Текущий шаг: {data['martingale_step']} - - Максимальное количество ставок в серии: {data['maximal_quantity']} - - - Требуемый бюджет: {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) - - -async def trading_mode_message(message, state): - await state.set_state(update_main_settings.trading_mode) - - await message.edit_text("""Режим торговли - -Лонг — стратегия, ориентированная на покупку актива с целью заработать на повышении его стоимости. - -Шорт — метод продажи активов, взятых в кредит, чтобы получить прибыль от снижения цены. - -Свитч — динамическое переключение между торговыми режимами для максимизации эффективности. - -Выберите ниже для изменений: -""", 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() - - id = callback.from_user.id - data_settings = await rq.get_user_main_settings(id) - - 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) - - await state.clear() - 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) - - await state.clear() - - case 'trade_mode_switch': - await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Switch") - await rq.update_trade_mode_user(id, 'Switch') - await main_settings_message(id, callback.message) - - await state.clear() - - case 'trade_mode_smart': - await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Smart") - await rq.update_trade_mode_user(id, 'Smart') - await main_settings_message(id, callback.message) - - await state.clear() - except Exception as e: - logger.error("Ошибка при обновлении режима торговли: %s", e) - - - -async def switch_mode_enabled_message(message, state): - await state.set_state(update_main_settings.switch_mode_enabled) - - await message.edit_text( - f""" Состояние свитча - - По направлению - по направлению последней сделки предыдущей серии - Против направления - против направления последней сделки предыдущей серии - - По умолчанию при первом запуске бота, направление сделки установлено на "Buy". - Выберите ниже для изменений:""", 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("Введите размер кредитного плеча (от 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): - 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) - - 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 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("Введите коэффициент Мартингейла:", 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) - - 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) - - await state.clear() - else: - val = data['martingale_factor'] - await message.answer( - f"⛔️ Ошибка: ваше значение ({val}) или выше лимита (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) - - await message.edit_text("""Тип маржи - -Изолированная маржа -Этот тип маржи позволяет ограничить риск конкретной позиции. -При использовании изолированной маржи вы выделяете определённую сумму средств только для одной позиции. -Если позиция начинает приносить убытки, ваши потери ограничиваются этой суммой, -и остальные средства на счёте не затрагиваются. - -Кросс-маржа -Кросс-маржа объединяет весь маржинальный баланс на счёте и использует все доступные средства для поддержания открытых позиций. -В случае убытков средства с других позиций или баланса автоматически покрывают дефицит, -снижая риск ликвидации, но увеличивая общий риск потери капитала. - -Выберите ниже для изменений: -""", 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): - callback_data = callback.data - if callback_data in ['margin_type_isolated', 'margin_type_cross']: - tg_id = callback.from_user.id - data_settings = await rq.get_user_main_settings(tg_id) - - try: - match callback.data: - case 'margin_type_isolated': - await callback.answer() - await callback.message.answer(f"✅ Изменено: {data_settings['margin_type']} → Isolated") - - await rq.update_margin_type(tg_id, 'Isolated') - await main_settings_message(tg_id, callback.message) - - 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("Ошибка при изменении типа маржи: %s", 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("Введите ставку:", 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) - - 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']}") - - await rq.update_starting_quantity(message.from_user.id, data['starting_quantity']) - await rq.update_base_quantity(tg_id=message.from_user.id, num=data['starting_quantity']) - await main_settings_message(message.from_user.id, message) - - await state.clear() - else: - await message.answer("⛔️ Ошибка: вы вводите неверные символы") - - 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("Введите максимальное количество серии ставок:", - 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) - - 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) - - await state.clear() - else: - val = data['maximal_quantity'] - await message.answer( - f'⛔️ Ошибка: ваше значение ({val}) или выше лимита (100) или вы вводите неверные символы') - logger.error(f'⛔️ Ошибка: ваше значение ({val}) или выше лимита (100) или вы вводите неверные символы') - - await main_settings_message(message.from_user.id, message) \ No newline at end of file diff --git a/app/telegram/functions/profile_tg.py b/app/telegram/functions/profile_tg.py new file mode 100644 index 0000000..5202c75 --- /dev/null +++ b/app/telegram/functions/profile_tg.py @@ -0,0 +1,27 @@ +import logging.config + +from aiogram.types import Message + +import app.telegram.keyboards.reply as kbr +import database.request as rq +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("profile_tg") + + +async def user_profile_tg(tg_id: int, message: Message) -> None: + try: + user = await rq.get_user(tg_id) + if user: + await message.answer( + text="💎Ваш профиль:\n\n" "⚖️ Баланс: 0\n", reply_markup=kbr.profile + ) + else: + await rq.create_user(tg_id=tg_id, username=user.username) + await rq.set_user_symbol(tg_id=tg_id, symbol="BTCUSDT") + await rq.create_user_additional_settings(tg_id=tg_id) + await rq.create_user_risk_management(tg_id=tg_id) + await user_profile_tg(tg_id=tg_id, message=message) + except Exception as e: + logger.error("Error processing user profile: %s", e) diff --git a/app/telegram/functions/risk_management_settings/settings.py b/app/telegram/functions/risk_management_settings/settings.py deleted file mode 100644 index 6594e37..0000000 --- a/app/telegram/functions/risk_management_settings/settings.py +++ /dev/null @@ -1,160 +0,0 @@ -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 - - await rq.set_new_user_default_risk_management_settings(tg_id) - - -async def main_settings_message(id, message): - data = await rq.get_user_risk_management_settings(id) - - text = f"""Риск менеджмент, - - - Процент изменения цены для фиксации прибыли: {data.get('price_profit', 0)}% - - Процент изменения цены для фиксации убытков: {data.get('price_loss', 0)}% - - Максимальный риск на сделку (в % от баланса): {data.get('max_risk_deal', 0)}% - - Комиссия биржи для расчета прибыли: {data.get('commission_fee', "Да")} - """ - await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.risk_management_settings_markup) - - -async def price_profit_message(message, state): - await state.set_state(update_risk_management_settings.price_profit) - - text = 'Введите число изменения цены для фиксации прибыли: ' - - 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: - val = data['price_profit'] - await message.answer( - f'⛔️ Ошибка: ваше значение ({val}%) или выше лимита (100) или вы вводите неверные символы') - - await main_settings_message(message.from_user.id, message) - - -async def price_loss_message(message, state): - 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: - val = data['price_loss'] - await message.answer( - f'⛔️ Ошибка: ваше значение ({val}%) выше лимита (100) или содержит неверные символы') - await main_settings_message(message.from_user.id, message) - - -async def max_risk_deal_message(message, state): - await state.set_state(update_risk_management_settings.max_risk_deal) - - text = 'Введите число (процент от баланса) для изменения максимального риска на сделку: ' - - 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: - val = data['max_risk_deal'] - await message.answer( - f'⛔️ Ошибка: ваше значение ({val}%) или выше лимита (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() \ No newline at end of file diff --git a/app/telegram/handlers/__init__.py b/app/telegram/handlers/__init__.py new file mode 100644 index 0000000..4c527e7 --- /dev/null +++ b/app/telegram/handlers/__init__.py @@ -0,0 +1,32 @@ +__all__ = "router" + +from aiogram import Router + +from app.telegram.handlers.add_bybit_api import router_add_bybit_api +from app.telegram.handlers.changing_the_symbol import router_changing_the_symbol +from app.telegram.handlers.close_orders import router_close_orders +from app.telegram.handlers.common import router_common +from app.telegram.handlers.get_positions_handlers import router_get_positions_handlers +from app.telegram.handlers.handlers_main import router_handlers_main +from app.telegram.handlers.main_settings import router_main_settings +from app.telegram.handlers.settings import router_settings +from app.telegram.handlers.start_trading import router_start_trading +from app.telegram.handlers.stop_trading import router_stop_trading +from app.telegram.handlers.tp_sl_handlers import router_tp_sl_handlers + +router = Router(name=__name__) + +router.include_router(router_handlers_main) +router.include_router(router_add_bybit_api) +router.include_router(router_settings) +router.include_router(router_main_settings) +router.include_router(router_changing_the_symbol) +router.include_router(router_get_positions_handlers) +router.include_router(router_start_trading) +router.include_router(router_stop_trading) +router.include_router(router_close_orders) +router.include_router(router_tp_sl_handlers) + + +# Do not add anything below this router +router.include_router(router_common) diff --git a/app/telegram/handlers/add_bybit_api.py b/app/telegram/handlers/add_bybit_api.py new file mode 100644 index 0000000..c658d3f --- /dev/null +++ b/app/telegram/handlers/add_bybit_api.py @@ -0,0 +1,150 @@ +import logging.config + +from aiogram import F, Router +from aiogram.fsm.context import FSMContext +from aiogram.types import CallbackQuery, Message + +import app.telegram.keyboards.inline as kbi +import app.telegram.keyboards.reply as kbr +import database.request as rq +from app.bybit.profile_bybit import user_profile_bybit +from app.telegram.states.states import AddBybitApiState +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("add_bybit_api") + +router_add_bybit_api = Router(name="add_bybit_api") + + +@router_add_bybit_api.callback_query(F.data == "connect_platform") +async def connect_platform(callback: CallbackQuery, state: FSMContext) -> None: + """ + Handles the callback query to initiate Bybit platform connection. + Sends instructions on how to create and provide API keys to the bot. + + :param callback: CallbackQuery object triggered by user interaction. + :param state: FSMContext object to manage state data. + :return: None + """ + try: + await state.clear() + await callback.answer() + user = await rq.get_user(tg_id=callback.from_user.id) + if user: + await callback.message.answer( + text=( + "Подключение Bybit аккаунта \n\n" + "1. Зарегистрируйтесь или войдите в свой аккаунт на Bybit по ссылке: " + "[Перейти на Bybit](https://www.bybit.com/invite?ref=YME83OJ).\n" + "2. В личном кабинете выберите раздел API. \n" + "3. Создание нового API ключа\n" + " - Нажмите кнопку Create New Key (Создать новый ключ).\n" + " - Выберите системно-сгенерированный ключ.\n" + " - Укажите название API ключа (любое). \n" + " - Выберите права доступа для торговли (Trade). \n" + " - Можно ограничить доступ по IP для безопасности.\n" + "4. Подтверждение создания\n" + " - Подтвердите создание ключа.\n" + " - Отправьте чат-роботу.\n\n" + "Важно: сохраните отдельно API Key и Secret Key в надежном месте. Secret ключ отображается только один раз." + ), + parse_mode="Markdown", + reply_markup=kbi.add_bybit_api, + disable_web_page_preview=True, + ) + else: + await rq.create_user( + tg_id=callback.from_user.id, username=callback.from_user.username + ) + await rq.set_user_symbol(tg_id=callback.from_user.id, symbol="BTCUSDT") + await rq.create_user_additional_settings(tg_id=callback.from_user.id) + await rq.create_user_risk_management(tg_id=callback.from_user.id) + await rq.create_user_conditional_settings(tg_id=callback.from_user.id) + await connect_platform(callback=callback, state=state) + except Exception as e: + logger.error("Error adding bybit API for user %s: %s", callback.from_user.id, e) + await callback.message.answer( + text="Произошла ошибка. Пожалуйста, попробуйте позже." + ) + + +@router_add_bybit_api.callback_query(F.data == "add_bybit_api") +async def process_api_key(callback: CallbackQuery, state: FSMContext) -> None: + """ + Starts the FSM flow to add Bybit API keys. + Sets the FSM state to prompt user to enter API Key. + + :param callback: CallbackQuery object. + :param state: FSMContext for managing user state. + """ + try: + await state.clear() + await state.set_state(AddBybitApiState.api_key_state) + await callback.answer() + await callback.message.answer(text="Введите API Key:") + except Exception as e: + logger.error("Error adding bybit API for user %s: %s", callback.from_user.id, e) + await callback.message.answer( + text="Произошла ошибка. Пожалуйста, попробуйте позже." + ) + + +@router_add_bybit_api.message(AddBybitApiState.api_key_state) +async def process_secret_key(message: Message, state: FSMContext) -> None: + """ + Receives the API Key input from the user, stores it in FSM context, + then sets state to collect Secret Key. + + :param message: Message object with user's input. + :param state: FSMContext for managing user state. + """ + try: + api_key = message.text + await state.update_data(api_key=api_key) + await state.set_state(AddBybitApiState.api_secret_state) + await message.answer(text="Введите Secret Key:") + except Exception as e: + logger.error("Error adding bybit API for user %s: %s", message.from_user.id, e) + await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.") + + +@router_add_bybit_api.message(AddBybitApiState.api_secret_state) +async def add_bybit_api(message: Message, state: FSMContext) -> None: + """ + Receives the Secret Key input, stores it, saves both API keys in the database, + clears FSM state and confirms success to the user. + + :param message: Message object with user's input. + :param state: FSMContext for managing user state. + """ + try: + api_secret = message.text + api_key = (await state.get_data()).get("api_key") + await state.update_data(api_secret=api_secret) + + if not api_key or not api_secret: + await message.answer("Введите корректные данные.") + return + + result = await rq.set_user_api( + tg_id=message.from_user.id, api_key=api_key, api_secret=api_secret + ) + + if result: + await message.answer(text="Данные добавлены.", reply_markup=kbr.profile) + await user_profile_bybit( + tg_id=message.from_user.id, message=message, state=state + ) + logger.debug( + "Bybit API added successfully for user: %s", message.from_user.id + ) + else: + await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.") + logger.error( + "Error adding bybit API for user %s: %s", message.from_user.id, result + ) + await state.clear() + except Exception as e: + logger.error("Error adding bybit API for user %s: %s", message.from_user.id, e) + await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.") diff --git a/app/telegram/handlers/changing_the_symbol.py b/app/telegram/handlers/changing_the_symbol.py new file mode 100644 index 0000000..f13d9cb --- /dev/null +++ b/app/telegram/handlers/changing_the_symbol.py @@ -0,0 +1,164 @@ +import logging.config + +from aiogram import F, Router +from aiogram.fsm.context import FSMContext +from aiogram.types import CallbackQuery, Message + +import app.telegram.keyboards.inline as kbi +import database.request as rq +from app.bybit.get_functions.get_tickers import get_tickers +from app.bybit.profile_bybit import user_profile_bybit +from app.bybit.set_functions.set_leverage import ( + set_leverage, + set_leverage_to_buy_and_sell, +) +from app.bybit.set_functions.set_margin_mode import set_margin_mode +from app.bybit.set_functions.set_switch_position_mode import set_switch_position_mode +from app.telegram.states.states import ChangingTheSymbolState +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("changing_the_symbol") + +router_changing_the_symbol = Router(name="changing_the_symbol") + + +@router_changing_the_symbol.callback_query(F.data == "change_symbol") +async def change_symbol(callback_query: CallbackQuery, state: FSMContext) -> None: + """ + Handler for the "change_symbol" command. + Sends a message with available symbols to choose from. + """ + try: + await state.clear() + await state.set_state(ChangingTheSymbolState.symbol_state) + msg = await callback_query.message.edit_text( + text="Выберите название инструмента без лишних символов (например: BTCUSDT):", + reply_markup=kbi.symbol, + ) + await state.update_data(prompt_message_id=msg.message_id) + logger.debug( + "Command change_symbol processed successfully for user: %s", + callback_query.from_user.id, + ) + except Exception as e: + await callback_query.answer( + text="Произошла ошибка. Пожалуйста, попробуйте позже." + ) + logger.error( + "Error processing command change_symbol for user %s: %s", + callback_query.from_user.id, + e, + ) + + +@router_changing_the_symbol.message(ChangingTheSymbolState.symbol_state) +async def set_symbol(message: Message, state: FSMContext) -> None: + """ + Handler for user input for setting the symbol. + + Updates FSM context with the selected symbol and persists the choice in database. + Sends an acknowledgement to user and clears FSM state afterward. + + Args: + message (Message): Incoming message from user containing the selected symbol. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + try: + data = await state.get_data() + if "prompt_message_id" in data: + prompt_message_id = data["prompt_message_id"] + await message.bot.delete_message( + chat_id=message.chat.id, message_id=prompt_message_id + ) + await message.delete() + except Exception as e: + if "message to delete not found" in str(e).lower(): + pass # Ignore this error + else: + raise e + + symbol = message.text.upper() + additional_settings = await rq.get_user_additional_settings( + tg_id=message.from_user.id + ) + + if not additional_settings: + await rq.create_user_additional_settings(tg_id=message.from_user.id) + return + + trade_mode = additional_settings.trade_mode or "Merged_Single" + mode = 0 if trade_mode == "Merged_Single" else 3 + margin_type = additional_settings.margin_type or "ISOLATED_MARGIN" + leverage = "10" + leverage_to_buy = "10" + leverage_to_sell = "10" + ticker = await get_tickers(tg_id=message.from_user.id, symbol=symbol) + + if ticker is None: + await message.answer( + text=f"Инструмент {symbol} не найден.", reply_markup=kbi.symbol + ) + return + + req = await rq.set_user_symbol(tg_id=message.from_user.id, symbol=symbol) + + if not req: + await message.answer( + text="Произошла ошибка при установке инструмента.", + reply_markup=kbi.symbol, + ) + return + + await user_profile_bybit( + tg_id=message.from_user.id, message=message, state=state + ) + + res = await set_switch_position_mode( + tg_id=message.from_user.id, symbol=symbol, mode=mode + ) + if res == "You have an existing position, so position mode cannot be switched": + if mode == 0: + mode = 3 + else: + mode = 0 + await set_switch_position_mode( + tg_id=message.from_user.id, symbol=symbol, mode=mode + ) + if trade_mode == "Merged_Single": + trade_mode = "Both_Sides" + else: + trade_mode = "Merged_Single" + await rq.set_trade_mode(tg_id=message.from_user.id, trade_mode=trade_mode) + + await set_margin_mode(tg_id=message.from_user.id, margin_mode=margin_type) + + if margin_type == "ISOLATED_MARGIN": + await set_leverage_to_buy_and_sell( + tg_id=message.from_user.id, + symbol=symbol, + leverage_to_buy=str(leverage_to_buy), + leverage_to_sell=str(leverage_to_sell), + ) + else: + await set_leverage( + tg_id=message.from_user.id, symbol=symbol, leverage=str(leverage) + ) + + await rq.set_leverage(tg_id=message.from_user.id, leverage=str(leverage)) + await rq.set_leverage_to_buy_and_sell( + tg_id=message.from_user.id, + leverage_to_buy=str(leverage_to_buy), + leverage_to_sell=str(leverage_to_sell), + ) + await rq.set_limit_price(tg_id=message.from_user.id, limit_price=0) + await rq.set_trigger_price(tg_id=message.from_user.id, trigger_price=0) + + await state.clear() + except Exception as e: + await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.") + logger.error("Error setting symbol for user %s: %s", message.from_user.id, e) diff --git a/app/telegram/handlers/close_orders.py b/app/telegram/handlers/close_orders.py new file mode 100644 index 0000000..887ecdc --- /dev/null +++ b/app/telegram/handlers/close_orders.py @@ -0,0 +1,109 @@ +import logging.config + +from aiogram import Router +from aiogram.fsm.context import FSMContext +from aiogram.types import CallbackQuery + +import database.request as rq +from app.bybit.close_positions import cancel_order, close_position +from app.helper_functions import safe_float +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("close_orders") + +router_close_orders = Router(name="close_orders") + + +@router_close_orders.callback_query( + lambda c: c.data and c.data.startswith("close_position_") +) +async def close_position_handler( + callback_query: CallbackQuery, state: FSMContext +) -> None: + """ + Close a position. + :param callback_query: Incoming callback query from Telegram inline keyboard. + :param state: Finite State Machine context for the current user session. + :return: None + """ + try: + data = callback_query.data + parts = data.split("_") + symbol = parts[2] + side = parts[3] + position_idx = int(parts[4]) + qty = safe_float(parts[5]) + await rq.set_auto_trading( + tg_id=callback_query.from_user.id, + symbol=symbol, + auto_trading=False, + side=side, + ) + res = await close_position( + tg_id=callback_query.from_user.id, + symbol=symbol, + side=side, + position_idx=position_idx, + qty=qty, + ) + + if not res: + await callback_query.answer(text="Произошла ошибка при закрытии позиции.") + return + + await callback_query.answer(text="Позиция успешно закрыта.") + logger.debug( + "Command close_position processed successfully for user: %s", + callback_query.from_user.id, + ) + except Exception as e: + await callback_query.answer(text="Произошла ошибка при закрытии позиции.") + logger.error( + "Error processing command close_position for user %s: %s", + callback_query.from_user.id, + e, + ) + finally: + await state.clear() + + +@router_close_orders.callback_query( + lambda c: c.data and c.data.startswith("close_order_") +) +async def cancel_order_handler( + callback_query: CallbackQuery, state: FSMContext +) -> None: + """ + Cancel an order. + :param callback_query: Incoming callback query from Telegram inline keyboard. + :param state: Finite State Machine context for the current user session. + :return: None + """ + try: + data = callback_query.data + parts = data.split("_") + symbol = parts[2] + order_id = parts[3] + res = await cancel_order( + tg_id=callback_query.from_user.id, symbol=symbol, order_id=order_id + ) + + if not res: + await callback_query.answer(text="Произошла ошибка при закрытии ордера.") + return + + await callback_query.answer(text="Ордер успешно закрыт.") + logger.debug( + "Command close_order processed successfully for user: %s", + callback_query.from_user.id, + ) + except Exception as e: + await callback_query.answer(text="Произошла ошибка при закрытии ордера.") + logger.error( + "Error processing command close_order for user %s: %s", + callback_query.from_user.id, + e, + ) + finally: + await state.clear() diff --git a/app/telegram/handlers/common.py b/app/telegram/handlers/common.py new file mode 100644 index 0000000..df1b323 --- /dev/null +++ b/app/telegram/handlers/common.py @@ -0,0 +1,50 @@ +import logging.config + +from aiogram import Router +from aiogram.fsm.context import FSMContext +from aiogram.types import Message + +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("common") + +router_common = Router(name="common") + + +@router_common.message() +async def unknown_message(message: Message, state: FSMContext) -> None: + """ + Handle unexpected or unrecognized messages. + Clears FSM state and informs the user about available commands. + + Args: + message (types.Message): Incoming message object. + state (FSMContext): Current FSM context. + + Returns: + None + """ + try: + await message.answer( + text="Извините, я вас не понял. " + "Пожалуйста, используйте одну из следующих команд:\n" + "/start - Запустить бота\n" + "/profile - Профиль\n" + "/bybit - Панель Bybit\n" + "/help - Получить помощь\n" + ) + logger.debug( + "Received unknown message from user %s: %s", + message.from_user.id, + message.text, + ) + except Exception as e: + logger.error( + "Error handling unknown message for user %s: %s", message.from_user.id, e + ) + await message.answer( + text="Произошла ошибка при обработке вашего сообщения. Пожалуйста, попробуйте позже." + ) + finally: + await state.clear() diff --git a/app/telegram/handlers/get_positions_handlers.py b/app/telegram/handlers/get_positions_handlers.py new file mode 100644 index 0000000..59263b6 --- /dev/null +++ b/app/telegram/handlers/get_positions_handlers.py @@ -0,0 +1,314 @@ +import logging.config + +from aiogram import F, Router +from aiogram.fsm.context import FSMContext +from aiogram.types import CallbackQuery + +import app.telegram.keyboards.inline as kbi +from app.bybit.get_functions.get_positions import ( + get_active_orders, + get_active_orders_by_symbol, + get_active_positions, + get_active_positions_by_symbol, +) +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("get_positions_handlers") + +router_get_positions_handlers = Router(name="get_positions_handlers") + + +@router_get_positions_handlers.callback_query(F.data == "my_deals") +async def get_positions_handlers( + callback_query: CallbackQuery, state: FSMContext +) -> None: + """ + Gets the user's active positions. + :param callback_query: CallbackQuery object. + :param state: FSMContext + :return: None + """ + try: + await state.clear() + await callback_query.message.edit_text( + text="Выберите какие сделки вы хотите посмотреть:", + reply_markup=kbi.change_position, + ) + except Exception as e: + logger.error("Error in get_positions_handler: %s", e) + await callback_query.answer(text="Произошла ошибка при получении сделок.") + + +@router_get_positions_handlers.callback_query(F.data == "change_position") +async def get_positions_handler( + callback_query: CallbackQuery, state: FSMContext +) -> None: + """ + Gets the user's active positions. + :param callback_query: CallbackQuery object. + :param state: FSMContext + :return: None + """ + try: + res = await get_active_positions(tg_id=callback_query.from_user.id) + + if res is None: + await callback_query.answer( + text="Произошла ошибка при получении активных позиций." + ) + return + + if res == ["No active positions found"]: + await callback_query.answer(text="Нет активных позиций.") + return + + active_positions = [pos for pos in res if float(pos.get("size", 0)) > 0] + + if not active_positions: + await callback_query.answer(text="Нет активных позиций.") + return + + active_symbols_sides = [ + (pos.get("symbol"), pos.get("side")) for pos in active_positions + ] + + await callback_query.message.edit_text( + text="Ваши активные позиции:", + reply_markup=kbi.create_active_positions_keyboard( + symbols=active_symbols_sides + ), + ) + except Exception as e: + logger.error("Error in get_positions_handler: %s", e) + await callback_query.answer( + text="Произошла ошибка при получении активных позиций." + ) + finally: + await state.clear() + + +@router_get_positions_handlers.callback_query( + lambda c: c.data.startswith("get_position_") +) +async def get_position_handler(callback_query: CallbackQuery, state: FSMContext): + try: + data = callback_query.data + parts = data.split("_") + symbol = parts[2] + get_side = parts[3] + res = await get_active_positions_by_symbol( + tg_id=callback_query.from_user.id, symbol=symbol + ) + + if res is None: + await callback_query.answer( + text="Произошла ошибка при получении активных позиций." + ) + return + + position = next((pos for pos in res if pos.get("side") == get_side), None) + + if position: + side = position.get("side") + symbol = position.get("symbol") or "Нет данных" + avg_price = position.get("avgPrice") or "Нет данных" + size = position.get("size") or "Нет данных" + take_profit = position.get("takeProfit") or "Нет данных" + stop_loss = position.get("stopLoss") or "Нет данных" + position_idx = position.get("positionIdx") or "Нет данных" + liq_price = position.get("liqPrice") or "Нет данных" + else: + side = "Нет данных" + symbol = "Нет данных" + avg_price = "Нет данных" + size = "Нет данных" + take_profit = "Нет данных" + stop_loss = "Нет данных" + position_idx = "Нет данных" + liq_price = "Нет данных" + + side_rus = ( + "Покупка" + if side == "Buy" + else "Продажа" if side == "Sell" else "Нет данных" + ) + + position_idx_rus = ( + "Односторонний" + if position_idx == 0 + else ( + "Покупка в режиме хеджирования" + if position_idx == 1 + else ( + "Продажа в режиме хеджирования" + if position_idx == 2 + else "Нет данных" + ) + ) + ) + + text_lines = [ + f"Торговая пара: {symbol}", + f"Режим позиции: {position_idx_rus}", + f"Цена входа: {avg_price}", + f"Количество: {size}", + f"Движение: {side_rus}", + ] + + if take_profit and take_profit != "Нет данных": + text_lines.append(f"Тейк-профит: {take_profit}") + if stop_loss and stop_loss != "Нет данных": + text_lines.append(f"Стоп-лосс: {stop_loss}") + if liq_price and liq_price != "Нет данных": + text_lines.append(f"Цена ликвидации: {liq_price}") + + text = "\n".join(text_lines) + + await callback_query.message.edit_text( + text=text, + reply_markup=kbi.make_close_position_keyboard( + symbol_pos=symbol, side=side, position_idx=position_idx, qty=size + ), + ) + + except Exception as e: + logger.error("Error in get_position_handler: %s", e) + await callback_query.answer( + text="Произошла ошибка при получении активных позиций." + ) + finally: + await state.clear() + + +@router_get_positions_handlers.callback_query(F.data == "open_orders") +async def get_open_orders_handler( + callback_query: CallbackQuery, state: FSMContext +) -> None: + """ + Gets the user's open orders. + :param callback_query: CallbackQuery object. + :param state: FSMContext + :return: None + """ + try: + res = await get_active_orders(tg_id=callback_query.from_user.id) + + if res is None: + await callback_query.answer( + text="Произошла ошибка при получении активных ордеров." + ) + return + + if res == ["No active orders found"]: + await callback_query.answer(text="Нет активных ордеров.") + return + + active_positions = [pos for pos in res if pos.get("orderStatus", 0) == "New"] + + if not active_positions: + await callback_query.answer(text="Нет активных ордеров.") + return + + active_orders_sides = [ + (pos.get("symbol"), pos.get("side")) for pos in active_positions + ] + + await callback_query.message.edit_text( + text="Ваши активные ордера:", + reply_markup=kbi.create_active_orders_keyboard(orders=active_orders_sides), + ) + except Exception as e: + logger.error("Error in get_open_orders_handler: %s", e) + await callback_query.answer( + text="Произошла ошибка при получении активных ордеров." + ) + finally: + await state.clear() + + +@router_get_positions_handlers.callback_query(lambda c: c.data.startswith("get_order_")) +async def get_order_handler(callback_query: CallbackQuery, state: FSMContext): + try: + data = callback_query.data + parts = data.split("_") + symbol = parts[2] + get_side = parts[3] + res = await get_active_orders_by_symbol( + tg_id=callback_query.from_user.id, symbol=symbol + ) + + if res is None: + await callback_query.answer( + text="Произошла ошибка при получении активных ордеров." + ) + return + + orders = next((pos for pos in res if pos.get("side") == get_side), None) + + if orders: + side = orders.get("side") + symbol = orders.get("symbol") + price = orders.get("price") + qty = orders.get("qty") + order_type = orders.get("orderType") + trigger_price = orders.get("triggerPrice") + take_profit = orders.get("takeProfit") + stop_loss = orders.get("stopLoss") + order_id = orders.get("orderId") + else: + side = "Нет данных" + symbol = "Нет данных" + price = "Нет данных" + qty = "Нет данных" + order_type = "Нет данных" + trigger_price = "Нет данных" + take_profit = "Нет данных" + stop_loss = "Нет данных" + order_id = "Нет данных" + + side_rus = ( + "Покупка" + if side == "Buy" + else "Продажа" if side == "Sell" else "Нет данных" + ) + + order_type_rus = ( + "Рыночный" + if order_type == "Market" + else "Лимитный" if order_type == "Limit" else "Нет данных" + ) + + text_lines = [ + f"Торговая пара: {symbol}", + f"Количество: {qty}", + f"Движение: {side_rus}", + f"Тип ордера: {order_type_rus}", + ] + if price: + text_lines.append(f"Цена: {price}") + + if trigger_price and trigger_price != "Нет данных": + text_lines.append(f"Триггер цена: {trigger_price}") + + if take_profit and take_profit != "Нет данных": + text_lines.append(f"Тейк-профит: {take_profit}") + + if stop_loss and stop_loss != "Нет данных": + text_lines.append(f"Стоп-лосс: {stop_loss}") + + text = "\n".join(text_lines) + + await callback_query.message.edit_text( + text=text, + reply_markup=kbi.make_close_orders_keyboard( + symbol_order=symbol, order_id=order_id + ), + ) + except Exception as e: + logger.error("Error in get_order_handler: %s", e) + await callback_query.answer( + text="Произошла ошибка при получении активных ордеров." + ) + finally: + await state.clear() diff --git a/app/telegram/handlers/handlers.py b/app/telegram/handlers/handlers.py deleted file mode 100644 index f2de4e5..0000000 --- a/app/telegram/handlers/handlers.py +++ /dev/null @@ -1,316 +0,0 @@ -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 -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 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) -> 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) -> None: - """ - Обработчик кнопки 'Профиль'. - Проверяет существование пользователя и отображает профиль. - - Args: - message (Message): Сообщение с текстом кнопки. - """ - user = await rq.check_user(message.from_user.id) - tg_id = message.from_user.id - balance = await get_balance(message.from_user.id, message) - if user and balance: - await run_ws_for_user(tg_id, message) - await func.profile_message(message.from_user.username, message) - 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.callback_query(F.data == "clb_start_chatbot_message") -async def clb_profile_msg(callback: CallbackQuery) -> None: - """ - Обработчик колбэка 'clb_start_chatbot_message'. - Если пользователь есть в БД — показывает профиль, - иначе регистрирует нового пользователя и инициализирует настройки. - - 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 "Пользователь" - - 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 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 == "clb_settings_message") -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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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': - await func_main_settings.size_leverage_message(callback.message, state) - case 'clb_change_starting_quantity': - await func_main_settings.starting_quantity_message(callback.message, state) - case 'clb_change_martingale_factor': - await func_main_settings.martingale_factor_message(callback.message, state) - case 'clb_change_maximum_quantity': - await func_main_settings.maximum_quantity_message(callback.message, state) - except Exception as e: - logger.error("Error callback in main_settings match-case: %s", e) - - -list_risk_management_settings = ['clb_change_price_profit', - 'clb_change_price_loss', - 'clb_change_max_risk_deal', - 'commission_fee', - ] - - -@router.callback_query(F.data.in_(list_risk_management_settings)) -async def clb_risk_management_settings_msg(callback: CallbackQuery, state: FSMContext) -> None: - """ - Обработчик изменений настроек управления рисками. - - Args: - callback (CallbackQuery): полученный колбэк. - state (FSMContext): текущее состояние FSM. - """ - await callback.answer() - - try: - match callback.data: - case 'clb_change_price_profit': - await func_rmanagement_settings.price_profit_message(callback.message, state) - case 'clb_change_price_loss': - 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: - logger.error("Error callback in risk_management match-case: %s", e) - - -list_condition_settings = ['clb_change_mode', - 'clb_change_timer', - 'clb_change_filter_volatility', - 'clb_change_external_cues', - 'clb_change_tradingview_cues', - 'clb_change_webhook', - 'clb_change_ai_analytics' - ] - - -@router.callback_query(F.data.in_(list_condition_settings)) -async def clb_condition_settings_msg(callback: CallbackQuery, state: FSMContext) -> None: - """ - Обработчик изменений настроек условий трейдинга. - - Args: - callback (CallbackQuery): полученный колбэк. - state (FSMContext): текущее состояние FSM. - """ - await callback.answer() - - try: - match callback.data: - 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': - await func_condition_settings.external_cues_message(callback.message, state) - case 'clb_change_tradingview_cues': - await func_condition_settings.trading_cues_message(callback.message, state) - case 'clb_change_webhook': - await func_condition_settings.webhook_message(callback.message, state) - case 'clb_change_ai_analytics': - await func_condition_settings.ai_analytics_message(callback.message, state) - except Exception as e: - logger.error("Error callback in main_settings match-case: %s", e) - - -list_additional_settings = ['clb_change_save_pattern', - 'clb_change_auto_start', - 'clb_change_notifications', - ] - - -@router.callback_query(F.data.in_(list_additional_settings)) -async def clb_additional_settings_msg(callback: CallbackQuery, state: FSMContext) -> None: - """ - Обработчик дополнительных настроек бота. - - Args: - callback (CallbackQuery): полученный колбэк. - state (FSMContext): текущее состояние FSM. - """ - await callback.answer() - - try: - match callback.data: - case 'clb_change_save_pattern': - await func_additional_settings.save_pattern_message(callback.message, state) - case 'clb_change_auto_start': - await func_additional_settings.auto_start_message(callback.message, state) - case 'clb_change_notifications': - await func_additional_settings.notifications_message(callback.message, state) - except Exception as e: - logger.error("Error callback in additional_settings match-case: %s", e) \ No newline at end of file diff --git a/app/telegram/handlers/handlers_main.py b/app/telegram/handlers/handlers_main.py new file mode 100644 index 0000000..5efb478 --- /dev/null +++ b/app/telegram/handlers/handlers_main.py @@ -0,0 +1,381 @@ +import logging.config + +from aiogram import F, Router +from aiogram.filters import Command +from aiogram.fsm.context import FSMContext +from aiogram.types import CallbackQuery, Message + +import app.telegram.keyboards.inline as kbi +import app.telegram.keyboards.reply as kbr +import database.request as rq +from app.bybit.profile_bybit import user_profile_bybit +from app.telegram.functions.profile_tg import user_profile_tg +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("handlers_main") + +router_handlers_main = Router(name="handlers_main") + + +@router_handlers_main.message(Command("start", "hello")) +@router_handlers_main.message(F.text.lower() == "привет") +async def cmd_start(message: Message, state: FSMContext) -> None: + """ + Handle the /start or /hello commands and the text message "привет". + + Checks if the user exists in the database and sends a user profile or creates a new user + with default settings and greeting message. + + Args: + message (Message): Incoming Telegram message object. + state (FSMContext): FSMContext for managing user state. + + Raises: + None: Exceptions are caught and logged internally. + """ + tg_id = message.from_user.id + username = message.from_user.username + full_name = message.from_user.full_name + user = await rq.get_user(tg_id) + try: + if user: + await user_profile_tg(tg_id=message.from_user.id, message=message) + logger.debug( + "Command start processed successfully for user: %s", + message.from_user.id, + ) + else: + await rq.create_user(tg_id=tg_id, username=username) + await rq.set_user_symbol(tg_id=tg_id, symbol="BTCUSDT") + await rq.create_user_additional_settings(tg_id=tg_id) + await rq.create_user_risk_management(tg_id=tg_id) + await rq.create_user_conditional_settings(tg_id=tg_id) + await message.answer( + text=f"Добро пожаловать, {full_name}!\n\n" + "PHANTOM TRADING - ваш надежный помощник для автоматизации трейдинга😉", + reply_markup=kbi.connect_the_platform, + ) + logger.debug( + "Command start processed successfully for user: %s", + message.from_user.id, + ) + except Exception as e: + logger.error( + "Error processing command start for user %s: %s", message.from_user.id, e + ) + await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.") + finally: + await state.clear() + + +@router_handlers_main.message(Command("profile")) +@router_handlers_main.message(F.text == "Профиль") +async def cmd_to_main(message: Message, state: FSMContext) -> None: + """ + Handle the /profile command or text "Профиль". + + Clears the current FSM state and sends the Telegram user profile. + + Args: + message (Message): Incoming Telegram message object. + state (FSMContext): FSM state context. + + Raises: + None: Exceptions are caught and logged internally. + """ + try: + await user_profile_tg(tg_id=message.from_user.id, message=message) + logger.debug( + "Command to_profile_tg processed successfully for user: %s", + message.from_user.id, + ) + except Exception as e: + logger.error( + "Error processing command to_profile_tg for user %s: %s", + message.from_user.id, + e, + ) + finally: + await state.clear() + + +@router_handlers_main.message(Command("bybit")) +@router_handlers_main.message(F.text == "Панель Bybit") +async def profile_bybit(message: Message, state: FSMContext) -> None: + """ + Handle the /bybit command or text "Панель Bybit". + + Clears FSM state and sends Bybit trading panel profile. + + Args: + message (Message): Incoming Telegram message object. + state (FSMContext): FSM state context. + + Raises: + None: Exceptions are caught and logged internally. + """ + try: + await state.clear() + await user_profile_bybit( + tg_id=message.from_user.id, message=message, state=state + ) + logger.debug( + "Command to_profile_bybit processed successfully for user: %s", + message.from_user.id, + ) + except Exception as e: + logger.error( + "Error processing command to_profile_bybit for user %s: %s", + message.from_user.id, + e, + ) + + +@router_handlers_main.callback_query(F.data == "profile_bybit") +async def profile_bybit_callback( + callback_query: CallbackQuery, state: FSMContext +) -> None: + """ + Handle callback query with data "profile_bybit". + + Clears FSM state and sends the Bybit profile in response. + + Args: + callback_query (CallbackQuery): Callback query object from Telegram. + state (FSMContext): FSM state context. + + Raises: + None: Exceptions are caught and logged internally. + """ + try: + await state.clear() + await user_profile_bybit( + tg_id=callback_query.from_user.id, + message=callback_query.message, + state=state, + ) + logger.debug( + "Callback profile_bybit processed successfully for user: %s", + callback_query.from_user.id, + ) + await callback_query.answer() + except Exception as e: + logger.error( + "Error processing callback profile_bybit for user %s: %s", + callback_query.from_user.id, + e, + ) + + +@router_handlers_main.callback_query(F.data == "main_settings") +async def settings(callback_query: CallbackQuery, state: FSMContext) -> None: + """ + Handle callback query with data "main_settings". + + Clears FSM state and edits the message to show main settings options. + + Args: + callback_query (CallbackQuery): Callback query object. + state (FSMContext): FSM state context. + + Raises: + None: Exceptions are caught and logged internally. + """ + try: + await state.clear() + msg = await callback_query.message.edit_text( + text="Выберите, что вы хотите настроить:", reply_markup=kbi.main_settings + ) + await state.update_data(prompt_message_id=msg.message_id) + logger.debug( + "Command settings processed successfully for user: %s", + callback_query.from_user.id, + ) + except Exception as e: + logger.error( + "Error processing command settings for user %s: %s", + callback_query.from_user.id, + e, + ) + + +@router_handlers_main.message(Command("connect")) +@router_handlers_main.message(F.text == "Подключить платформу Bybit") +async def cmd_connect(message: Message, state: FSMContext) -> None: + """ + Handle the /connect command or text "Подключить платформу Bybit". + + Clears FSM state and sends a connection message. + + Args: + message (Message): Incoming Telegram message object. + state (FSMContext): FSM state context. + + Raises: + None: Exceptions are caught and logged internally. + """ + try: + await state.clear() + user = await rq.get_user(tg_id=message.from_user.id) + if user: + await message.answer( + text=( + "Подключение Bybit аккаунта \n\n" + "1. Зарегистрируйтесь или войдите в свой аккаунт на Bybit по ссылке: " + "[Перейти на Bybit](https://www.bybit.com/invite?ref=YME83OJ).\n" + "2. В личном кабинете выберите раздел API. \n" + "3. Создание нового API ключа\n" + " - Нажмите кнопку Create New Key (Создать новый ключ).\n" + " - Выберите системно-сгенерированный ключ.\n" + " - Укажите название API ключа (любое). \n" + " - Выберите права доступа для торговли (Trade). \n" + " - Можно ограничить доступ по IP для безопасности.\n" + "4. Подтверждение создания\n" + " - Подтвердите создание ключа.\n" + " - Отправьте чат-роботу.\n\n" + "Важно: сохраните отдельно API Key и Secret Key в надежном месте. Secret ключ отображается только один раз." + ), + parse_mode="Markdown", + reply_markup=kbi.add_bybit_api, + disable_web_page_preview=True, + ) + else: + await rq.create_user( + tg_id=message.from_user.id, username=message.from_user.username + ) + await rq.set_user_symbol(tg_id=message.from_user.id, symbol="BTCUSDT") + await rq.create_user_additional_settings(tg_id=message.from_user.id) + await rq.create_user_risk_management(tg_id=message.from_user.id) + await rq.create_user_conditional_settings(tg_id=message.from_user.id) + await cmd_connect(message=message, state=state) + logger.debug( + "Command connect processed successfully for user: %s", + message.from_user.id, + ) + except Exception as e: + logger.error( + "Error processing command connect for user %s: %s", + message.from_user.id, + e, + ) + + +@router_handlers_main.message(Command("help")) +async def cmd_help(message: Message, state: FSMContext) -> None: + """ + Handle the /help command. + + Clears FSM state and sends a help message with available commands and reply keyboard. + + Args: + message (Message): Incoming Telegram message object. + state (FSMContext): FSM state context. + + Raises: + None: Exceptions are caught and logged internally. + """ + try: + await state.clear() + await message.answer( + text="Используйте одну из следующих команд:\n" + "/start - Запустить бота\n" + "/profile - Профиль\n" + "/bybit - Панель Bybit\n" + "/connect - Подключиться к платформе\n", + reply_markup=kbr.profile, + ) + logger.debug( + "Command help processed successfully for user: %s", + message.from_user.id, + ) + except Exception as e: + logger.error( + "Error processing command help for user %s: %s", message.from_user.id, e + ) + await message.answer( + text="Произошла ошибка. Пожалуйста, попробуйте позже.", + reply_markup=kbr.profile, + ) + + +@router_handlers_main.message(Command("cancel")) +@router_handlers_main.message( + lambda message: message.text.casefold() in ["cancel", "отмена"] +) +async def cmd_cancel_handler(message: Message, state: FSMContext) -> None: + """ + Handle /cancel command or text 'cancel'/'отмена'. + + If there is an active FSM state, clears it and informs the user. + Otherwise, informs that no operation was in progress. + + Args: + message (Message): Incoming Telegram message object. + state (FSMContext): FSM state context. + + Raises: + None: Exceptions are caught and logged internally. + """ + current_state = await state.get_state() + + if current_state is None: + await message.reply( + text="Хорошо, но ничего не происходило.", reply_markup=kbr.profile + ) + logger.debug( + "Cancel command received but no active state for user %s.", + message.from_user.id, + ) + return + + try: + await state.clear() + await message.reply(text="Команда отменена.", reply_markup=kbr.profile) + logger.debug( + "Command cancel executed successfully. State cleared for user %s.", + message.from_user.id, + ) + except Exception as e: + logger.error( + "Error while cancelling command for user %s: %s", message.from_user.id, e + ) + await message.answer( + text="Произошла ошибка при отмене. Пожалуйста, попробуйте позже.", + reply_markup=kbr.profile, + ) + + +@router_handlers_main.callback_query(F.data == "cancel") +async def cmd_cancel(callback_query: CallbackQuery, state: FSMContext) -> None: + """ + Handle callback query with data "cancel". + + Clears the FSM state and sends a cancellation message. + + Args: + callback_query (CallbackQuery): Callback query object. + state (FSMContext): FSM state context. + + Raises: + None: Exceptions are caught and logged internally. + """ + try: + await callback_query.message.delete() + await user_profile_bybit( + tg_id=callback_query.from_user.id, + message=callback_query.message, + state=state, + ) + logger.debug( + "Command cancel processed successfully for user: %s", + callback_query.from_user.id, + ) + except Exception as e: + logger.error( + "Error processing command cancel for user %s: %s", + callback_query.from_user.id, + e, + ) + finally: + await state.clear() \ No newline at end of file diff --git a/app/telegram/handlers/main_settings/__init__.py b/app/telegram/handlers/main_settings/__init__.py new file mode 100644 index 0000000..c1a330e --- /dev/null +++ b/app/telegram/handlers/main_settings/__init__.py @@ -0,0 +1,17 @@ +__all__ = "router" + +from aiogram import Router + +from app.telegram.handlers.main_settings.additional_settings import ( + router_additional_settings, +) +from app.telegram.handlers.main_settings.conditional_settings import ( + router_conditional_settings, +) +from app.telegram.handlers.main_settings.risk_management import router_risk_management + +router_main_settings = Router(name=__name__) + +router_main_settings.include_router(router_additional_settings) +router_main_settings.include_router(router_risk_management) +router_main_settings.include_router(router_conditional_settings) diff --git a/app/telegram/handlers/main_settings/additional_settings.py b/app/telegram/handlers/main_settings/additional_settings.py new file mode 100644 index 0000000..1d1dd4e --- /dev/null +++ b/app/telegram/handlers/main_settings/additional_settings.py @@ -0,0 +1,1556 @@ +import logging.config + +from aiogram import F, Router +from aiogram.fsm.context import FSMContext +from aiogram.types import CallbackQuery, Message + +import app.telegram.keyboards.inline as kbi +import database.request as rq +from app.bybit.get_functions.get_instruments_info import get_instruments_info +from app.bybit.get_functions.get_tickers import get_tickers +from app.bybit.set_functions.set_leverage import ( + set_leverage, + set_leverage_to_buy_and_sell, +) +from app.bybit.set_functions.set_margin_mode import set_margin_mode +from app.bybit.set_functions.set_switch_position_mode import set_switch_position_mode +from app.helper_functions import get_base_currency, is_int, is_number, safe_float +from app.telegram.states.states import AdditionalSettingsState +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("additional_settings") + +router_additional_settings = Router(name="additional_settings") + + +@router_additional_settings.callback_query(F.data == "trade_mode") +async def settings_for_trade_mode( + callback_query: CallbackQuery, state: FSMContext +) -> None: + """ + Handles the 'trade_mode' callback query. + + Clears the current FSM state, edits the message text to display trade mode options + with explanation for 'Long' and 'Short' modes, and shows an inline keyboard for selection. + + Args: + callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + await state.clear() + await callback_query.message.edit_text( + text="Выберите режим позиции:\n\n" + "Односторонний режим — возможно удержание Лонг или же Шорт позиции в контракте.\n\n" + "Хеджирование — возможно удержание обеих Лонг и Шорт позиций в контракте одновременно.", + reply_markup=kbi.trade_mode, + ) + logger.debug( + "Command trade_mode processed successfully for user: %s", + callback_query.from_user.id, + ) + + except Exception as e: + await callback_query.answer( + text="Произошла ошибка. Пожалуйста, попробуйте позже." + ) + logger.error( + "Error processing command trade_mode for user %s: %s", + callback_query.from_user.id, + e, + ) + + +@router_additional_settings.callback_query( + lambda c: c.data == "Merged_Single" or c.data == "Both_Sides" +) +async def trade_mode(callback_query: CallbackQuery, state: FSMContext) -> None: + """ + Handles callback queries related to trade mode selection. + + Updates FSM context with selected trade mode and persists the choice in database. + Sends an acknowledgement to user and clears FSM state afterward. + + Args: + callback_query (CallbackQuery): Incoming callback query indicating selected trade mode. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id) + additional_settings = await rq.get_user_additional_settings( + tg_id=callback_query.from_user.id + ) + get_leverage = additional_settings.leverage or "10" + get_leverage_to_buy = additional_settings.leverage_to_buy or "10" + get_leverage_to_sell = additional_settings.leverage_to_sell or "10" + leverage_to_float = safe_float(get_leverage) + leverage_to_buy_float = safe_float(get_leverage_to_buy) + leverage_to_sell_float = safe_float(get_leverage_to_sell) + margin_type = additional_settings.margin_type or "ISOLATED_MARGIN" + mode = 0 if callback_query.data.startswith("Merged_Single") else 3 + response = await set_switch_position_mode( + tg_id=callback_query.from_user.id, symbol=symbol, mode=mode + ) + + if not response: + await callback_query.answer( + text="Произошла ошибка при обновлении режима позиции." + ) + return + + req = await rq.set_trade_mode( + tg_id=callback_query.from_user.id, trade_mode=callback_query.data + ) + if not req: + await callback_query.answer( + text="Произошла ошибка при обновлении режима позиции." + ) + return + + if ( + response + == "You have an existing position, so position mode cannot be switched" + ): + await callback_query.answer( + text="У вас уже есть позиция по паре, " + "поэтому режим позиции не может быть изменен." + ) + return + + if response == "Open orders exist, so you cannot change position mode": + await callback_query.answer( + text="У вас есть открытые ордера, " + "поэтому режим позиции не может быть изменен." + ) + return + + if callback_query.data.startswith("Merged_Single"): + await callback_query.answer(text="Выбран режим позиции: Односторонний") + await set_leverage( + tg_id=callback_query.from_user.id, + symbol=symbol, + leverage=str(leverage_to_float), + ) + + elif callback_query.data.startswith("Both_Sides"): + await callback_query.answer(text="Выбран режим позиции: Хеджирование") + if margin_type == "ISOLATED_MARGIN": + await set_leverage_to_buy_and_sell( + tg_id=callback_query.from_user.id, + symbol=symbol, + leverage_to_buy=str(leverage_to_buy_float), + leverage_to_sell=str(leverage_to_sell_float), + ) + else: + await set_leverage( + tg_id=callback_query.from_user.id, + symbol=symbol, + leverage=str(leverage_to_float), + ) + + except Exception as e: + await callback_query.answer(text="Произошла ошибка при смене режима позиции.") + logger.error( + "Error processing set trade_mode for user %s: %s", + callback_query.from_user.id, + e, + ) + finally: + await state.clear() + + +@router_additional_settings.callback_query(F.data == "margin_type") +async def settings_for_margin_type( + callback_query: CallbackQuery, state: FSMContext +) -> None: + """ + Handles the 'margin_type' callback query. + + Clears the current FSM state, edits the message text to display margin type options, + and shows an inline keyboard for selection. + + Args: + callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + await state.clear() + await callback_query.message.edit_text( + text="Выберите тип маржи:\n\n" + "Примечание: Если у вас есть открытые позиции, то маржа примениться ко всем позициям", + reply_markup=kbi.margin_type + ) + logger.debug( + "Command margin_type processed successfully for user: %s", + callback_query.from_user.id, + ) + except Exception as e: + await callback_query.answer( + text="Произошла ошибка. Пожалуйста, попробуйте позже." + ) + logger.error( + "Error processing command margin_type for user %s: %s", + callback_query.from_user.id, + e, + ) + + +@router_additional_settings.callback_query( + lambda c: c.data == "ISOLATED_MARGIN" or c.data == "REGULAR_MARGIN" +) +async def set_margin_type(callback_query: CallbackQuery, state: FSMContext) -> None: + """ + Handles callback queries starting with 'Isolated' or 'Cross'. + + Updates FSM context with selected margin type and persists the choice in database. + Sends an acknowledgement to user and clears FSM state afterward. + + Args: + callback_query (CallbackQuery): Incoming callback query indicating selected margin type. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id) + additional_settings = await rq.get_user_additional_settings( + tg_id=callback_query.from_user.id + ) + get_leverage = additional_settings.leverage or "10" + get_leverage_to_buy = additional_settings.leverage_to_buy or "10" + get_leverage_to_sell = additional_settings.leverage_to_sell or "10" + leverage_to_float = safe_float(get_leverage) + leverage_to_buy_float = safe_float(get_leverage_to_buy) + leverage_to_sell_float = safe_float(get_leverage_to_sell) + bybit_margin_mode = callback_query.data + response = await set_margin_mode( + tg_id=callback_query.from_user.id, margin_mode=bybit_margin_mode + ) + + if not response: + await callback_query.answer( + text="Произошла ошибка при установке типа маржи" + ) + return + + req = await rq.set_margin_type( + tg_id=callback_query.from_user.id, margin_type=callback_query.data + ) + + if not req: + await callback_query.answer( + text="Произошла ошибка при установке типа маржи" + ) + return + + if callback_query.data.startswith("ISOLATED_MARGIN"): + await callback_query.answer(text="Выбран тип маржи: Изолированная") + await set_leverage_to_buy_and_sell( + tg_id=callback_query.from_user.id, + symbol=symbol, + leverage_to_buy=str(leverage_to_buy_float), + leverage_to_sell=str(leverage_to_sell_float), + ) + elif callback_query.data.startswith("REGULAR_MARGIN"): + await callback_query.answer(text="Выбран тип маржи: Кросс") + await set_leverage( + tg_id=callback_query.from_user.id, + symbol=symbol, + leverage=str(leverage_to_float), + ) + else: + await callback_query.answer( + text="Произошла ошибка при установке типа маржи" + ) + + except Exception as e: + await callback_query.answer(text="Произошла ошибка при установке типа маржи") + logger.error( + "Error processing command margin_type for user %s: %s", + callback_query.from_user.id, + e, + ) + finally: + await state.clear() + + +@router_additional_settings.callback_query(F.data == "order_type") +async def settings_for_order_type( + callback_query: CallbackQuery, state: FSMContext +) -> None: + """ + Handles the 'order_type' callback query. + + Clears the current FSM state, edits the message text to display order type options, + and shows an inline keyboard for selection. + + Args: + callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + await state.clear() + await callback_query.message.edit_text( + text="Выберите тип ордера:\n\n" + "Рыночный ордер - исполняется немедленно по лучшей доступной цене.\n\n" + "Лимитный ордер - это ордер на покупку или продажу по указанной цене или лучше.\n\n" + "Условный ордер - активируются при достижении триггерной цены.", + reply_markup=kbi.order_type, + ) + logger.debug( + "Command order_type processed successfully for user: %s", + callback_query.from_user.id, + ) + except Exception as e: + await callback_query.answer( + text="Произошла ошибка. Пожалуйста, попробуйте позже." + ) + logger.error( + "Error processing command order_type for user %s: %s", + callback_query.from_user.id, + e, + ) + + +@router_additional_settings.callback_query( + lambda c: c.data == "Market" or c.data == "Limit" or c.data == "Conditional" +) +async def set_order_type(callback_query: CallbackQuery, state: FSMContext) -> None: + """ + Handles callback queries starting with 'Market', 'Limit', or 'Conditional'. + + Updates FSM context with selected order type and persists the choice in database. + Sends an acknowledgement to user and clears FSM state afterward. + + Args: + callback_query (CallbackQuery): Incoming callback query indicating selected order type. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + await state.update_data(order_type=callback_query.data) + req = await rq.set_order_type( + tg_id=callback_query.from_user.id, order_type=callback_query.data + ) + + if not req: + await callback_query.answer( + text="Произошла ошибка при установке типа ордера" + ) + return + + if callback_query.data.startswith("Market"): + await callback_query.answer(text="Выбран тип ордера: Рыночный") + elif callback_query.data.startswith("Limit"): + await callback_query.answer(text="Выбран тип ордера: Лимитный") + elif callback_query.data.startswith("Conditional"): + await callback_query.answer(text="Выбран тип ордера: Условный") + else: + await callback_query.answer( + text="Произошла ошибка при установке типа ордера" + ) + + except Exception as e: + await callback_query.answer(text="Произошла ошибка при установке типа ордера") + logger.error( + "Error processing command order_type for user %s: %s", + callback_query.from_user.id, + e, + ) + finally: + await state.clear() + + +@router_additional_settings.callback_query(F.data == "conditional_order_type") +async def settings_for_conditional_order_type( + callback_query: CallbackQuery, state: FSMContext +) -> None: + """ + Handles the 'conditional_order_type' callback query. + + Clears the current FSM state, edits the message text to display conditional order type options, + and shows an inline keyboard for selection. + + Args: + callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + await state.clear() + await callback_query.message.edit_text( + text="Выберите тип условного ордера:\n\n" + "Рыночный ордер - исполняется немедленно по лучшей доступной цене при достижении триггерной цены.\n\n" + "Лимитный ордер - это ордер на покупку или продажу по указанной цене или лучше.\n\n", + reply_markup=kbi.conditional_order_type, + ) + logger.debug( + "Command conditional_order_type processed successfully for user: %s", + callback_query.from_user.id, + ) + except Exception as e: + await callback_query.answer( + text="Произошла ошибка. Пожалуйста, попробуйте позже." + ) + logger.error( + "Error processing command conditional_order_type for user %s: %s", + callback_query.from_user.id, + e, + ) + + +@router_additional_settings.callback_query( + lambda c: c.data == "set_market" or c.data == "set_limit" +) +async def conditional_order_type( + callback_query: CallbackQuery, state: FSMContext +) -> None: + """ + Handles callback queries starting with 'set_market' or 'set_limit'. + + Updates FSM context with selected conditional order type and persists the choice in database. + Sends an acknowledgement to user and clears FSM state afterward. + + Args: + callback_query (CallbackQuery): Incoming callback query indicating selected conditional order type. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + await state.update_data(conditional_order_type=callback_query.data) + + if callback_query.data.startswith("set_market"): + await callback_query.answer(text="Выбран тип условного ордера: Рыночный") + order_type = "Market" + elif callback_query.data.startswith("set_limit"): + await callback_query.answer(text="Выбран тип условного ордера: Лимитный") + order_type = "Limit" + else: + await callback_query.answer( + text="Произошла ошибка при обновлении типа условного ордера" + ) + return + + req = await rq.set_conditional_order_type( + tg_id=callback_query.from_user.id, conditional_order_type=order_type + ) + + if not req: + await callback_query.answer( + text="Произошла ошибка при обновлении типа условного ордера" + ) + return + except Exception as e: + await callback_query.answer( + text="Произошла ошибка при обновлении типа условного ордера." + ) + logger.error( + "Error processing conditional_order_type for user %s: %s", + callback_query.from_user.id, + e, + ) + finally: + await state.clear() + + +@router_additional_settings.callback_query(F.data == "limit_price") +async def limit_price(callback_query: CallbackQuery, state: FSMContext) -> None: + """ + Handles the 'limit_price' callback query. + + Clears the current FSM state, edits the message text to display the limit price options, + and shows an inline keyboard for selection. + + Args: + callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + await state.clear() + await callback_query.message.edit_text( + text="Выберите цену лимита:\n\n" + "1. Установить цену - указать цену\n" + "2. Последняя цена - использовать последнюю цену\n", + reply_markup=kbi.change_limit_price, + ) + logger.debug( + "Command limit_price processed successfully for user: %s", + callback_query.from_user.id, + ) + except Exception as e: + await callback_query.answer( + text="Произошла ошибка. Пожалуйста, попробуйте позже." + ) + logger.error( + "Error processing command limit_price for user %s: %s", + callback_query.from_user.id, + e, + ) + + +@router_additional_settings.callback_query(lambda c: c.data == "last_price") +async def last_price(callback_query: CallbackQuery, state: FSMContext) -> None: + """ + Handles the 'last_price' callback query. + + Clears the current FSM state, edits the message text to display the last price option, + and shows an inline keyboard for selection. + + Args: + callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id) + get_tickers_info = await get_tickers( + tg_id=callback_query.from_user.id, symbol=symbol + ) + if get_tickers_info is None: + await callback_query.answer( + text="Произошла ошибка при установке цены лимита." + ) + return + + mark_price = get_tickers_info.get("lastPrice") or 0 + req = await rq.set_limit_price( + tg_id=callback_query.from_user.id, limit_price=safe_float(mark_price) + ) + if req: + await callback_query.answer( + text=f"Цена лимита установлена на последнюю цену: {mark_price}" + ) + else: + await callback_query.answer( + text="Произошла ошибка при установке цены лимита." + ) + + except Exception as e: + await callback_query.answer(text="Произошла ошибка при установке цены лимита.") + logger.error( + "Error processing last_price for user %s: %s", + callback_query.from_user.id, + e, + ) + finally: + await state.clear() + + +@router_additional_settings.callback_query(lambda c: c.data == "set_limit_price") +async def set_limit_price_handler( + callback_query: CallbackQuery, state: FSMContext +) -> None: + """ + Handles the 'set_limit_price_handler' callback query. + + Clears the current FSM state, edits the message text to prompt for the limit price, + and shows an inline keyboard for input. + + Args: + callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + await state.clear() + await state.set_state(AdditionalSettingsState.limit_price_state) + await callback_query.answer() + await state.update_data(prompt_message_id=callback_query.message.message_id) + msg = await callback_query.message.edit_text( + text="Введите цену:", reply_markup=kbi.back_to_change_limit_price + ) + await state.update_data(prompt_message_id=msg.message_id) + logger.debug( + "Command set_limit_price processed successfully for user: %s", + callback_query.from_user.id, + ) + except Exception as e: + await callback_query.answer( + text="Произошла ошибка. Пожалуйста, попробуйте позже." + ) + logger.error( + "Error processing command set_limit_price for user %s: %s", + callback_query.from_user.id, + e, + ) + + +@router_additional_settings.message(AdditionalSettingsState.limit_price_state) +async def set_limit_price(message: Message, state: FSMContext) -> None: + """ + Handles user input for setting the limit price. + + Updates FSM context with the selected limit price and persists the choice in database. + Sends an acknowledgement to user and clears FSM state afterward. + + Args: + message (Message): Incoming message from user containing the selected limit price. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + try: + data = await state.get_data() + if "prompt_message_id" in data: + prompt_message_id = data["prompt_message_id"] + await message.bot.delete_message( + chat_id=message.chat.id, message_id=prompt_message_id + ) + await message.delete() + except Exception as e: + if "message to delete not found" in str(e).lower(): + pass # Ignore this error + else: + raise e + + limit_price_value = message.text + + if not is_number(limit_price_value): + await message.answer( + "Ошибка: введите валидное число.", + reply_markup=kbi.back_to_additional_settings, + ) + logger.debug( + "User %s input invalid (not an valid number): %s", + message.from_user.id, + limit_price_value, + ) + return + + req = await rq.set_limit_price( + tg_id=message.from_user.id, limit_price=safe_float(limit_price_value) + ) + if req: + await message.answer( + text=f"Цена лимита установлена на: {limit_price_value}", + reply_markup=kbi.back_to_additional_settings, + ) + else: + await message.answer( + text="Произошла ошибка при установке цены лимита.", + reply_markup=kbi.back_to_change_limit_price, + ) + + await state.clear() + except Exception as e: + await message.answer( + text="Произошла ошибка при установке цены лимита.", + reply_markup=kbi.back_to_additional_settings, + ) + logger.error( + "Error processing set_limit_price for user %s: %s", + message.from_user.id, + e, + ) + + +@router_additional_settings.callback_query(lambda c: c.data == "trigger_price") +async def trigger_price(callback_query: CallbackQuery, state: FSMContext) -> None: + """ + Handles the 'trigger_price' callback query. + + Clears the current FSM state, edits the message text to prompt for the trigger price, + and shows an inline keyboard for input. + + Args: + callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + await state.clear() + await state.set_state(AdditionalSettingsState.trigger_price_state) + await callback_query.answer() + await state.update_data(prompt_message_id=callback_query.message.message_id) + msg = await callback_query.message.edit_text( + text="Введите цену:", reply_markup=kbi.back_to_additional_settings + ) + await state.update_data(prompt_message_id=msg.message_id) + logger.debug( + "Command trigger_price processed successfully for user: %s", + callback_query.from_user.id, + ) + except Exception as e: + await callback_query.answer( + text="Произошла ошибка. Пожалуйста, попробуйте позже." + ) + logger.error( + "Error processing command trigger_price for user %s: %s", + callback_query.from_user.id, + e, + ) + + +@router_additional_settings.message(AdditionalSettingsState.trigger_price_state) +async def set_trigger_price(message: Message, state: FSMContext) -> None: + """ + Handles user input for setting the trigger price. + + Updates FSM context with the selected trigger price and persists the choice in database. + Sends an acknowledgement to user and clears FSM state afterward. + + Args: + message (Message): Incoming message from user containing the selected trigger price. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + try: + data = await state.get_data() + if "prompt_message_id" in data: + prompt_message_id = data["prompt_message_id"] + await message.bot.delete_message( + chat_id=message.chat.id, message_id=prompt_message_id + ) + await message.delete() + except Exception as e: + if "message to delete not found" in str(e).lower(): + pass # Ignore this error + else: + raise e + + trigger_price_value = message.text + + if not is_number(trigger_price_value): + await message.answer( + "Ошибка: введите валидное число.", + reply_markup=kbi.back_to_additional_settings, + ) + logger.debug( + "User %s input invalid (not an valid number): %s", + message.from_user.id, + trigger_price_value, + ) + return + + req = await rq.set_trigger_price( + tg_id=message.from_user.id, trigger_price=safe_float(trigger_price_value) + ) + if req: + await message.answer( + text=f"Цена триггера установлена на: {trigger_price_value}", + reply_markup=kbi.back_to_additional_settings, + ) + else: + await message.answer( + text="Произошла ошибка при установке цены триггера.", + reply_markup=kbi.back_to_additional_settings, + ) + + await state.clear() + except Exception as e: + await message.answer( + text="Произошла ошибка при установке цены триггера.", + reply_markup=kbi.back_to_additional_settings, + ) + logger.error( + "Error processing set_trigger_price for user %s: %s", + message.from_user.id, + e, + ) + + +@router_additional_settings.callback_query(F.data == "leverage") +async def leverage_to_buy(callback_query: CallbackQuery, state: FSMContext) -> None: + """ + Handles the 'leverage' callback query. + + Clears the current FSM state, edits the message text to display the leverage options, + and shows an inline keyboard for selection. + + Args: + callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + await state.clear() + await callback_query.answer() + additional_settings = await rq.get_user_additional_settings( + callback_query.from_user.id + ) + get_trade_mode = additional_settings.trade_mode or "Both_Sides" + get_margin_type = additional_settings.margin_type or "ISOLATED_MARGIN" + if get_trade_mode == "Both_Sides" and get_margin_type == "ISOLATED_MARGIN": + await state.set_state(AdditionalSettingsState.leverage_to_buy_state) + msg = await callback_query.message.edit_text( + text="Введите размер кредитного плеча для Лонг:", + reply_markup=kbi.back_to_additional_settings, + ) + await state.update_data(prompt_message_id=msg.message_id) + else: + await state.set_state(AdditionalSettingsState.leverage_state) + msg = await callback_query.message.edit_text( + text="Введите размер кредитного плеча:", + reply_markup=kbi.back_to_additional_settings, + ) + await state.update_data(prompt_message_id=msg.message_id) + logger.debug( + "Command leverage processed successfully for user: %s", + callback_query.from_user.id, + ) + except Exception as e: + await callback_query.answer( + text="Произошла ошибка. Пожалуйста, попробуйте позже." + ) + logger.error( + "Error processing command leverage for user %s: %s", + callback_query.from_user.id, + e, + ) + + +@router_additional_settings.message(AdditionalSettingsState.leverage_state) +async def leverage(message: Message, state: FSMContext) -> None: + """ + Handles user input for setting the leverage. + + Updates FSM context with the selected leverage and persists the choice in database. + Sends an acknowledgement to user and clears FSM state afterward. + + Args: + message (Message): Incoming message from user containing the selected leverage. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + try: + data = await state.get_data() + if "prompt_message_id" in data: + prompt_message_id = data["prompt_message_id"] + await message.bot.delete_message( + chat_id=message.chat.id, message_id=prompt_message_id + ) + await message.delete() + except Exception as e: + if "message to delete not found" in str(e).lower(): + pass # Ignore this error + else: + raise e + + get_leverage = message.text + tg_id = message.from_user.id + if not is_number(get_leverage): + await message.answer( + "Ошибка: введите валидное число.", + reply_markup=kbi.back_to_additional_settings, + ) + logger.debug( + "User %s input invalid (not an valid number): %s", + message.from_user.id, + get_leverage, + ) + return + + leverage_float = safe_float(get_leverage) + if leverage_float < 1 or leverage_float > 100: + await message.answer( + text="Ошибка: число должно быть от 1 до 100.", + reply_markup=kbi.back_to_additional_settings, + ) + logger.debug( + "User %s input invalid (out of range): %s", + message.from_user.id, + leverage_float, + ) + return + + symbol = await rq.get_user_symbol(tg_id=tg_id) + instruments_info = await get_instruments_info(tg_id=tg_id, symbol=symbol) + + if instruments_info is not None: + min_leverage = ( + safe_float(instruments_info.get("leverageFilter").get("minLeverage")) + or 1 + ) + max_leverage = ( + safe_float(instruments_info.get("leverageFilter").get("maxLeverage")) + or 100 + ) + + if leverage_float > max_leverage or leverage_float < min_leverage: + await message.answer( + text=f"Кредитное плечо должно быть от {min_leverage} до {max_leverage}", + reply_markup=kbi.back_to_additional_settings, + ) + logger.info( + "User %s input invalid (out of range): %s, %s, %s: %s", + message.from_user.id, + symbol, + min_leverage, + max_leverage, + leverage_float, + ) + return + else: + await message.answer( + text="Произошла ошибка. Пожалуйста, попробуйте позже.", + reply_markup=kbi.back_to_additional_settings, + ) + + response = await set_leverage( + tg_id=message.from_user.id, symbol=symbol, leverage=str(leverage_float) + ) + + if not response: + await message.answer( + text="Невозможно установить кредитное плечо для текущего режима торговли.", + reply_markup=kbi.back_to_additional_settings, + ) + return + + req_leverage = await rq.set_leverage( + tg_id=message.from_user.id, leverage=str(leverage_float) + ) + req_leverage_to_buy_and_sell = await rq.set_leverage_to_buy_and_sell( + tg_id=message.from_user.id, + leverage_to_buy=str(leverage_float), + leverage_to_sell=str(leverage_float), + ) + if req_leverage and req_leverage_to_buy_and_sell: + await message.answer( + text=f"Кредитное плечо успешно установлено на {leverage_float}", + reply_markup=kbi.back_to_additional_settings, + ) + logger.info( + "User %s set leverage: %s", message.from_user.id, leverage_float + ) + else: + await message.answer( + text="Произошла ошибка при установке кредитного плеча. Пожалуйста, попробуйте позже.", + reply_markup=kbi.back_to_additional_settings, + ) + + await state.clear() + except Exception as e: + await message.answer( + text="Произошла ошибка при установке кредитного плеча. Пожалуйста, попробуйте позже.", + reply_markup=kbi.back_to_additional_settings, + ) + logger.error( + "Error processing command leverage for user %s: %s", message.from_user.id, e + ) + + +@router_additional_settings.message(AdditionalSettingsState.leverage_to_buy_state) +async def set_leverage_to_buy(message: Message, state: FSMContext) -> None: + """ + Handles user input for setting the leverage to buy. + + Updates FSM context with the selected leverage to buy and persists the choice in database. + Sends an acknowledgement to user and clears FSM state afterward. + + Args: + message (Message): Incoming message from user containing the selected leverage to buy. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + try: + data = await state.get_data() + if "prompt_message_id" in data: + prompt_message_id = data["prompt_message_id"] + await message.bot.delete_message( + chat_id=message.chat.id, message_id=prompt_message_id + ) + await message.delete() + except Exception as e: + if "message to delete not found" in str(e).lower(): + pass # Ignore this error + else: + raise e + + get_leverage_to_buy = message.text + tg_id = message.from_user.id + + if not is_number(get_leverage_to_buy): + await message.answer( + "Ошибка: введите валидное число.", + reply_markup=kbi.back_to_additional_settings, + ) + logger.debug( + "User %s input invalid (not an valid number): %s", + message.from_user.id, + get_leverage_to_buy, + ) + return + + leverage_to_buy_float = safe_float(get_leverage_to_buy) + if leverage_to_buy_float < 1 or leverage_to_buy_float > 100: + await message.answer( + text="Ошибка: число должно быть от 1 до 100.", + reply_markup=kbi.back_to_additional_settings, + ) + logger.debug( + "User %s input invalid (out of range): %s", + message.from_user.id, + get_leverage_to_buy, + ) + return + + symbol = await rq.get_user_symbol(tg_id=tg_id) + instruments_info = await get_instruments_info(tg_id=tg_id, symbol=symbol) + + if instruments_info is not None: + max_leverage = safe_float( + instruments_info.get("leverageFilter").get("maxLeverage") + ) + if leverage_to_buy_float > max_leverage: + await message.answer( + text=f"Кредитное плечо {leverage_to_buy_float} превышает максимальное {max_leverage} для {symbol}", + reply_markup=kbi.back_to_additional_settings, + ) + logger.info( + "The requested leverage %s exceeds the maximum %s for %s for user: %s: %s", + leverage_to_buy_float, + max_leverage, + symbol, + message.from_user.id, + ) + return + else: + await message.answer( + text="Произошла ошибка при установке кредитного плеча. Пожалуйста, попробуйте позже.", + reply_markup=kbi.back_to_additional_settings, + ) + logger.error( + "Error processing command leverage_to_buy for user %s", + message.from_user.id, + ) + + await state.update_data(leverage_to_buy=leverage_to_buy_float) + await state.set_state(AdditionalSettingsState.leverage_to_sell_state) + msg = await message.answer( + text="Введите размер кредитного плеча для Шорт:", + reply_markup=kbi.back_to_additional_settings, + ) + await state.update_data(prompt_message_id=msg.message_id) + logger.debug( + "Command leverage_to_buy processed successfully for user: %s", + message.from_user.id, + ) + except Exception as e: + await message.answer( + text="Произошла ошибка при установке кредитного плеча.. Пожалуйста, попробуйте позже.", + reply_markup=kbi.back_to_additional_settings, + ) + logger.error( + "Error processing command leverage_to_buy for user %s: %s", + message.from_user.id, + e, + ) + + +@router_additional_settings.message(AdditionalSettingsState.leverage_to_sell_state) +async def set_leverage_to_sell(message: Message, state: FSMContext) -> None: + """ + Handles user input for setting the leverage to sell. + + Updates FSM context with the selected leverage and persists the choice in database. + Sends an acknowledgement to user and clears FSM state afterward. + + Args: + message (Message): Incoming message from user containing the selected leverage. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + try: + data = await state.get_data() + if "prompt_message_id" in data: + prompt_message_id = data["prompt_message_id"] + await message.bot.delete_message( + chat_id=message.chat.id, message_id=prompt_message_id + ) + await message.delete() + except Exception as e: + if "message to delete not found" in str(e).lower(): + pass # Ignore this error + else: + raise e + + get_leverage_to_sell = message.text + get_leverage_to_buy = (await state.get_data()).get("leverage_to_buy") + tg_id = message.from_user.id + + if not is_number(get_leverage_to_sell): + await message.answer( + "Ошибка: введите валидное число.", + reply_markup=kbi.back_to_additional_settings, + ) + logger.debug( + "User %s input invalid (not an valid number): %s", + message.from_user.id, + leverage_to_buy or get_leverage_to_sell, + ) + return + + leverage_to_buy_float = safe_float(get_leverage_to_buy) + leverage_to_sell_float = safe_float(get_leverage_to_sell) + if leverage_to_sell_float < 1 or leverage_to_sell_float > 100: + await message.answer( + text="Ошибка: число должно быть от 1 до 100.", + reply_markup=kbi.back_to_additional_settings, + ) + logger.debug( + "User %s input invalid (out of range): %s", + message.from_user.id, + get_leverage_to_sell, + ) + return + + symbol = await rq.get_user_symbol(tg_id=tg_id) + instruments_info = await get_instruments_info(tg_id=tg_id, symbol=symbol) + + if instruments_info is not None: + min_leverage = safe_float( + instruments_info.get("leverageFilter").get("minLeverage") + ) + if leverage_to_sell_float < min_leverage: + await message.answer( + text=f"Кредитное плечо {leverage_to_sell_float} ниже минимального {min_leverage} для {symbol}", + reply_markup=kbi.back_to_additional_settings, + ) + logger.info( + "The requested leverage %s is below the minimum %s for %s for user: %s", + leverage_to_sell_float, + min_leverage, + symbol, + message.from_user.id, + ) + return + else: + await message.answer( + text="Произошла ошибка при установке кредитного плеча. Пожалуйста, попробуйте позже.", + reply_markup=kbi.back_to_additional_settings, + ) + + response = await set_leverage_to_buy_and_sell( + tg_id=message.from_user.id, + symbol=symbol, + leverage_to_buy=str(leverage_to_buy_float), + leverage_to_sell=str(leverage_to_sell_float), + ) + if not response: + await message.answer( + text="Невозможно установить кредитное плечо для текущего режима торговли.", + reply_markup=kbi.back_to_additional_settings, + ) + return + + req = await rq.set_leverage_to_buy_and_sell( + tg_id=message.from_user.id, + leverage_to_buy=str(leverage_to_buy_float), + leverage_to_sell=str(leverage_to_sell_float), + ) + + if req: + await message.answer( + text=f"Размер кредитного плеча установлен на {leverage_to_buy_float} для Лонга " + f"и {leverage_to_sell_float} для Шорта", + reply_markup=kbi.back_to_additional_settings, + ) + else: + await message.answer( + text="Произошла ошибка при установке кредитного плеча. Пожалуйста, попробуйте позже.", + reply_markup=kbi.back_to_additional_settings, + ) + + await state.clear() + except Exception as e: + await message.answer( + text="Произошла ошибка при установке кредитного плеча. Пожалуйста, попробуйте позже.", + reply_markup=kbi.back_to_additional_settings, + ) + logger.error( + "Error processing command set_leverage for user %s: %s", + message.from_user.id, + e, + ) + + +@router_additional_settings.callback_query(F.data == "order_quantity") +async def order_quantity(callback_query: CallbackQuery, state: FSMContext) -> None: + """ + Handles the 'order_quantity' callback query. + + Clears the current FSM state, edits the message text to display the order quantity options, + and shows an inline keyboard for selection. + + Args: + callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + await state.clear() + await state.set_state(AdditionalSettingsState.quantity_state) + symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id) + name_symbol = get_base_currency(symbol) + msg = await callback_query.message.edit_text( + text=f"Укажите размер для ордера в следующей валюте: {name_symbol}.", + reply_markup=kbi.back_to_additional_settings, + ) + await state.update_data(prompt_message_id=msg.message_id) + logger.debug( + "Command order_quantity processed successfully for user: %s", + callback_query.from_user.id, + ) + except Exception as e: + await callback_query.answer( + text="Произошла ошибка. Пожалуйста, попробуйте позже." + ) + logger.error( + "Error processing command order_quantity for user %s: %s", + callback_query.from_user.id, + e, + ) + + +@router_additional_settings.message(AdditionalSettingsState.quantity_state) +async def set_order_quantity(message: Message, state: FSMContext) -> None: + """ + Handles user input for setting the order quantity. + + Updates FSM context with the selected order quantity and persists the choice in database. + Sends an acknowledgement to user and clears FSM state afterward. + + Args: + message (Message): Incoming message from user containing the selected order quantity. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + try: + data = await state.get_data() + if "prompt_message_id" in data: + prompt_message_id = data["prompt_message_id"] + await message.bot.delete_message( + chat_id=message.chat.id, message_id=prompt_message_id + ) + await message.delete() + except Exception as e: + if "message to delete not found" in str(e).lower(): + pass # Ignore this error + else: + raise e + + order_quantity_value = message.text + + if not is_number(order_quantity_value): + await message.answer( + "Ошибка: введите валидное число.", + reply_markup=kbi.back_to_additional_settings, + ) + logger.debug( + "User %s input invalid (not an valid number): %s", + message.from_user.id, + order_quantity_value, + ) + return + + quantity = safe_float(order_quantity_value) + symbol = await rq.get_user_symbol(tg_id=message.from_user.id) + instruments_info = await get_instruments_info( + tg_id=message.from_user.id, symbol=symbol + ) + + if instruments_info is not None: + max_order_qty = safe_float( + instruments_info.get("lotSizeFilter").get("maxOrderQty") + ) + min_order_qty = safe_float( + instruments_info.get("lotSizeFilter").get("minOrderQty") + ) + + if quantity < min_order_qty or quantity > max_order_qty: + await message.answer( + text=f"Количество ордера должно быть от {min_order_qty} до {max_order_qty}", + reply_markup=kbi.back_to_additional_settings, + ) + return + + req = await rq.set_order_quantity( + tg_id=message.from_user.id, order_quantity=quantity + ) + + if req: + await message.answer( + text=f"Количество ордера установлено на {message.text}", + reply_markup=kbi.back_to_additional_settings, + ) + else: + await message.answer( + text="Произошла ошибка при установке кол-ва ордера. Пожалуйста, попробуйте позже.", + reply_markup=kbi.back_to_additional_settings, + ) + + await state.clear() + except Exception as e: + await message.answer( + text="Произошла ошибка при установке кол-ва ордера. Пожалуйста, попробуйте позже.", + reply_markup=kbi.back_to_additional_settings, + ) + logger.error("Error processing command set_order_quantity: %s", e) + + +@router_additional_settings.callback_query(F.data == "martingale_factor") +async def martingale_factor(callback_query: CallbackQuery, state: FSMContext) -> None: + """ + Handles the 'martingale_factor' callback query. + + Clears the current FSM state, edits the message text to display the martingale factor options, + and shows an inline keyboard for selection. + + Args: + callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + await state.clear() + await state.set_state(AdditionalSettingsState.martingale_factor_state) + msg = await callback_query.message.edit_text( + text="Введите коэффициент мартингейла:", + reply_markup=kbi.back_to_additional_settings, + ) + await state.update_data(prompt_message_id=msg.message_id) + logger.debug( + "Command martingale_factor processed successfully for user: %s", + callback_query.from_user.id, + ) + except Exception as e: + await callback_query.answer( + text="Произошла ошибка. Пожалуйста, попробуйте позже." + ) + logger.error( + "Error processing command martingale_factor for user %s: %s", + callback_query.from_user.id, + e, + ) + + +@router_additional_settings.message(AdditionalSettingsState.martingale_factor_state) +async def set_martingale_factor(message: Message, state: FSMContext) -> None: + """ + Handles user input for setting the martingale factor. + + Updates FSM context with the selected martingale factor and persists the choice in database. + Sends an acknowledgement to user and clears FSM state afterward. + + Args: + message (Message): Incoming message from user containing the selected martingale factor. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + try: + data = await state.get_data() + if "prompt_message_id" in data: + prompt_message_id = data["prompt_message_id"] + await message.bot.delete_message( + chat_id=message.chat.id, message_id=prompt_message_id + ) + await message.delete() + except Exception as e: + if "message to delete not found" in str(e).lower(): + pass # Ignore this error + else: + raise e + + martingale_factor_value = message.text + + if not is_number(martingale_factor_value): + await message.answer( + "Ошибка: введите валидное число.", + reply_markup=kbi.back_to_additional_settings, + ) + logger.debug( + "User %s input invalid (not an valid number): %s", + message.from_user.id, + martingale_factor_value, + ) + return + + martingale_factor_value_float = safe_float(martingale_factor_value) + req = await rq.set_martingale_factor( + tg_id=message.from_user.id, martingale_factor=martingale_factor_value_float + ) + + if req: + await message.answer( + text=f"Коэффициент мартингейла установлен на {message.text}", + reply_markup=kbi.back_to_additional_settings, + ) + else: + await message.answer( + text="Произошла ошибка при установке коэффициента мартингейла. Пожалуйста, попробуйте позже.", + reply_markup=kbi.back_to_additional_settings, + ) + + await state.clear() + except Exception as e: + await message.answer( + text="Произошла ошибка при установке коэффициента мартингейла. Пожалуйста, попробуйте позже.", + reply_markup=kbi.back_to_additional_settings, + ) + logger.error("Error processing command set_martingale_factor: %s", e) + + +@router_additional_settings.callback_query(F.data == "max_bets_in_series") +async def max_bets_in_series(callback_query: CallbackQuery, state: FSMContext) -> None: + """ + Handles the 'max_bets_in_series' callback query. + + Clears the current FSM state, edits the message text to display the max bets in series options, + and shows an inline keyboard for selection. + + Args: + callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + await state.clear() + await state.set_state(AdditionalSettingsState.max_bets_in_series_state) + msg = await callback_query.message.edit_text( + text="Введите максимальное количество ставок в серии:", + reply_markup=kbi.back_to_additional_settings, + ) + await state.update_data(prompt_message_id=msg.message_id) + logger.debug( + "Command max_bets_in_series processed successfully for user: %s", + callback_query.from_user.id, + ) + except Exception as e: + await callback_query.answer( + text="Произошла ошибка. Пожалуйста, попробуйте позже." + ) + logger.error( + "Error processing command max_bets_in_series for user %s: %s", + callback_query.from_user.id, + e, + ) + + +@router_additional_settings.message(AdditionalSettingsState.max_bets_in_series_state) +async def set_max_bets_in_series(message: Message, state: FSMContext) -> None: + """ + Handles user input for setting the max bets in series. + + Updates FSM context with the selected max steps and persists the choice in database. + Sends an acknowledgement to user and clears FSM state afterward. + + Args: + message (Message): Incoming message from user containing the selected max bets in series. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + try: + data = await state.get_data() + if "prompt_message_id" in data: + prompt_message_id = data["prompt_message_id"] + await message.bot.delete_message( + chat_id=message.chat.id, message_id=prompt_message_id + ) + await message.delete() + except Exception as e: + if "message to delete not found" in str(e).lower(): + pass # Ignore this error + else: + raise e + + max_bets_in_series_value = message.text + + if not is_int(max_bets_in_series_value): + await message.answer( + "Ошибка: введите валидное число.", + reply_markup=kbi.back_to_additional_settings, + ) + logger.debug( + "User %s input invalid (not an valid number): %s", + message.from_user.id, + max_bets_in_series_value, + ) + return + + req = await rq.set_max_bets_in_series( + tg_id=message.from_user.id, max_bets_in_series=int(max_bets_in_series_value) + ) + + if req: + await message.answer( + text=f"Максимальное количество шагов установлено на {message.text}", + reply_markup=kbi.back_to_additional_settings, + ) + else: + await message.answer( + text="Произошла ошибка при установке максимального количества шагов. Пожалуйста, попробуйте позже.", + reply_markup=kbi.back_to_additional_settings, + ) + + await state.clear() + except Exception as e: + await message.answer( + text="Произошла ошибка при установке максимального количества шагов. Пожалуйста, попробуйте позже.", + reply_markup=kbi.back_to_additional_settings, + ) + logger.error( + "Error processing command set_max_bets_in_series for user %s: %s", + message.from_user.id, + e, + ) diff --git a/app/telegram/handlers/main_settings/conditional_settings.py b/app/telegram/handlers/main_settings/conditional_settings.py new file mode 100644 index 0000000..2366513 --- /dev/null +++ b/app/telegram/handlers/main_settings/conditional_settings.py @@ -0,0 +1,174 @@ +import logging.config + +from aiogram import Router +from aiogram.fsm.context import FSMContext +from aiogram.types import CallbackQuery, Message + +import app.telegram.keyboards.inline as kbi +import database.request as rq +from app.helper_functions import is_int_for_timer +from app.telegram.states.states import ConditionalSettingsState +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("conditional_settings") + +router_conditional_settings = Router(name="conditional_settings") + + +@router_conditional_settings.callback_query( + lambda c: c.data == "start_timer" or c.data == "stop_timer" +) +async def timer(callback_query: CallbackQuery, state: FSMContext): + """ + Handles callback queries starting with 'start_timer' or 'stop_timer'. + """ + try: + await state.clear() + if callback_query.data == "start_timer": + await state.set_state(ConditionalSettingsState.start_timer_state) + msg = await callback_query.message.edit_text( + "Введите время в минутах для старта торговли:", + reply_markup=kbi.back_to_conditions, + ) + await state.update_data(prompt_message_id=msg.message_id) + elif callback_query.data == "stop_timer": + await state.set_state(ConditionalSettingsState.stop_timer_state) + msg = await callback_query.message.edit_text( + "Введите время в минутах для остановки торговли:", + reply_markup=kbi.back_to_conditions, + ) + await state.update_data(prompt_message_id=msg.message_id) + else: + await callback_query.answer( + text="Произошла ошибка. Пожалуйста, попробуйте позже." + ) + except Exception as e: + await callback_query.answer( + text="Произошла ошибка. Пожалуйста, попробуйте позже." + ) + logger.error( + "Error processing command timer for user %s: %s", + callback_query.from_user.id, + e, + ) + + +@router_conditional_settings.message(ConditionalSettingsState.start_timer_state) +async def start_timer(message: Message, state: FSMContext): + """ + Handles the start_timer state of the Finite State Machine. + """ + try: + try: + data = await state.get_data() + if "prompt_message_id" in data: + prompt_message_id = data["prompt_message_id"] + await message.bot.delete_message( + chat_id=message.chat.id, message_id=prompt_message_id + ) + await message.delete() + except Exception as e: + if "message to delete not found" in str(e).lower(): + pass # Ignore this error + else: + raise e + + get_start_timer = message.text + value = is_int_for_timer(get_start_timer) + + if value is False: + await message.answer( + "Ошибка: введите валидное число.", + reply_markup=kbi.back_to_conditions, + ) + logger.debug( + "User %s input invalid (not an valid number): %s", + message.from_user.id, + get_start_timer, + ) + return + + req = await rq.set_start_timer( + tg_id=message.from_user.id, timer_start=int(get_start_timer) + ) + + if req: + await message.answer( + "Таймер успешно установлен.", + reply_markup=kbi.back_to_conditions, + ) + else: + await message.answer( + "Произошла ошибка. Пожалуйста, попробуйте позже.", + reply_markup=kbi.back_to_conditions, + ) + + await state.clear() + except Exception as e: + await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.") + logger.error( + "Error processing command start_timer for user %s: %s", + message.from_user.id, + e, + ) + + +@router_conditional_settings.message(ConditionalSettingsState.stop_timer_state) +async def stop_timer(message: Message, state: FSMContext): + """ + Handles the stop_timer state of the Finite State Machine. + """ + try: + try: + data = await state.get_data() + if "prompt_message_id" in data: + prompt_message_id = data["prompt_message_id"] + await message.bot.delete_message( + chat_id=message.chat.id, message_id=prompt_message_id + ) + await message.delete() + except Exception as e: + if "message to delete not found" in str(e).lower(): + pass # Ignore this error + else: + raise e + + get_stop_timer = message.text + value = is_int_for_timer(get_stop_timer) + + if value is False: + await message.answer( + "Ошибка: введите валидное число.", + reply_markup=kbi.back_to_conditions, + ) + logger.debug( + "User %s input invalid (not an valid number): %s", + message.from_user.id, + get_stop_timer, + ) + return + + req = await rq.set_stop_timer( + tg_id=message.from_user.id, timer_end=int(get_stop_timer) + ) + + if req: + await message.answer( + "Таймер успешно установлен.", + reply_markup=kbi.back_to_conditions, + ) + else: + await message.answer( + "Произошла ошибка. Пожалуйста, попробуйте позже.", + reply_markup=kbi.back_to_conditions, + ) + + await state.clear() + except Exception as e: + await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.") + logger.error( + "Error processing command stop_timer for user %s: %s", + message.from_user.id, + e, + ) diff --git a/app/telegram/handlers/main_settings/risk_management.py b/app/telegram/handlers/main_settings/risk_management.py new file mode 100644 index 0000000..ad17564 --- /dev/null +++ b/app/telegram/handlers/main_settings/risk_management.py @@ -0,0 +1,467 @@ +import logging.config + +from aiogram import F, Router +from aiogram.fsm.context import FSMContext +from aiogram.types import CallbackQuery, Message + +import app.telegram.keyboards.inline as kbi +import database.request as rq +from app.helper_functions import is_int +from app.telegram.states.states import RiskManagementState +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("risk_management") + +router_risk_management = Router(name="risk_management") + + +@router_risk_management.callback_query(F.data == "take_profit_percent") +async def take_profit_percent(callback_query: CallbackQuery, state: FSMContext) -> None: + """ + Handles the 'profit_price_change' callback query. + + Clears the current FSM state, edits the message text to display the take profit percent options, + and shows an inline keyboard for selection. + + Args: + callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + await state.clear() + await state.set_state(RiskManagementState.take_profit_percent_state) + msg = await callback_query.message.edit_text( + text="Введите процент изменения цены для фиксации прибыли: ", + reply_markup=kbi.back_to_risk_management, + ) + await state.update_data(prompt_message_id=msg.message_id) + logger.debug( + "Command profit_price_change processed successfully for user: %s", + callback_query.from_user.id, + ) + except Exception as e: + await callback_query.answer( + text="Произошла ошибка. Пожалуйста, попробуйте позже." + ) + logger.error( + "Error processing command profit_price_change for user %s: %s", + callback_query.from_user.id, + e, + ) + + +@router_risk_management.message(RiskManagementState.take_profit_percent_state) +async def set_take_profit_percent(message: Message, state: FSMContext) -> None: + """ + Handles user input for setting the take profit percentage. + + Updates FSM context with the selected percentage and persists the choice in database. + Sends an acknowledgement to user and clears FSM state afterward. + + Args: + message (Message): Incoming message from user containing the take profit percentage. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + try: + data = await state.get_data() + if "prompt_message_id" in data: + prompt_message_id = data["prompt_message_id"] + await message.bot.delete_message( + chat_id=message.chat.id, message_id=prompt_message_id + ) + await message.delete() + except Exception as e: + if "message to delete not found" in str(e).lower(): + pass # Ignore this error + else: + raise e + + take_profit_percent_value = message.text + + if not is_int(take_profit_percent_value): + await message.answer( + text="Ошибка: введите валидное число.", + reply_markup=kbi.back_to_risk_management, + ) + logger.debug( + "User %s input invalid (not an valid number): %s", + message.from_user.id, + take_profit_percent_value, + ) + return + + if int(take_profit_percent_value) < 1 or int(take_profit_percent_value) > 100: + await message.answer( + text="Ошибка: введите число от 1 до 100.", + reply_markup=kbi.back_to_risk_management, + ) + logger.debug( + "User %s input invalid (not an valid number): %s", + message.from_user.id, + take_profit_percent_value, + ) + return + + req = await rq.set_take_profit_percent( + tg_id=message.from_user.id, + take_profit_percent=int(take_profit_percent_value), + ) + + if req: + await message.answer( + text=f"Процент изменения цены для фиксации прибыли " + f"установлен на {take_profit_percent_value}%.", + reply_markup=kbi.back_to_risk_management, + ) + else: + await message.answer( + text="Произошла ошибка при установке процента изменения цены для фиксации прибыли. " + "Пожалуйста, попробуйте позже.", + reply_markup=kbi.back_to_risk_management, + ) + + await state.clear() + except Exception as e: + logger.error( + "Error processing command profit_price_change for user %s: %s", + message.from_user.id, + e, + ) + + +@router_risk_management.callback_query(F.data == "stop_loss_percent") +async def stop_loss_percent(callback_query: CallbackQuery, state: FSMContext) -> None: + """ + Handles the 'stop_loss_percent' callback query. + + Clears the current FSM state, edits the message text to display the stop loss percentage options, + and shows an inline keyboard for selection. + + Args: + callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + await state.clear() + await state.set_state(RiskManagementState.stop_loss_percent_state) + msg = await callback_query.message.edit_text( + text="Введите процент изменения цены для фиксации убытка: ", + reply_markup=kbi.back_to_risk_management, + ) + await state.update_data(prompt_message_id=msg.message_id) + logger.debug( + "Command stop_loss_percent processed successfully for user: %s", + callback_query.from_user.id, + ) + except Exception as e: + await callback_query.answer( + text="Произошла ошибка. Пожалуйста, попробуйте позже." + ) + logger.error( + "Error processing command stop_loss_percent for user %s: %s", + callback_query.from_user.id, + e, + ) + + +@router_risk_management.message(RiskManagementState.stop_loss_percent_state) +async def set_stop_loss_percent(message: Message, state: FSMContext) -> None: + """ + Handles user input for setting the stop loss percentage. + + Updates FSM context with the selected percentage and persists the choice in database. + Sends an acknowledgement to user and clears FSM state afterward. + + Args: + message (Message): Incoming message from user containing the stop loss percentage. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + try: + data = await state.get_data() + if "prompt_message_id" in data: + prompt_message_id = data["prompt_message_id"] + await message.bot.delete_message( + chat_id=message.chat.id, message_id=prompt_message_id + ) + await message.delete() + except Exception as e: + if "message to delete not found" in str(e).lower(): + pass # Ignore this error + else: + raise e + + stop_loss_percent_value = message.text + + if not is_int(stop_loss_percent_value): + await message.answer( + text="Ошибка: введите валидное число.", + reply_markup=kbi.back_to_risk_management, + ) + logger.debug( + "User %s input invalid (not an valid number): %s", + message.from_user.id, + stop_loss_percent_value, + ) + return + + if int(stop_loss_percent_value) < 1 or int(stop_loss_percent_value) > 100: + await message.answer( + text="Ошибка: введите число от 1 до 100.", + reply_markup=kbi.back_to_risk_management, + ) + logger.debug( + "User %s input invalid (not an valid number): %s", + message.from_user.id, + stop_loss_percent_value, + ) + return + + req = await rq.set_stop_loss_percent( + tg_id=message.from_user.id, stop_loss_percent=int(stop_loss_percent_value) + ) + + if req: + await message.answer( + text=f"Процент изменения цены для фиксации убытка " + f"установлен на {stop_loss_percent_value}%.", + reply_markup=kbi.back_to_risk_management, + ) + else: + await message.answer( + text="Произошла ошибка при установке процента изменения цены для фиксации убытка. " + "Пожалуйста, попробуйте позже.", + reply_markup=kbi.back_to_risk_management, + ) + + await state.clear() + except Exception as e: + await message.answer( + text="Произошла ошибка при установке процента изменения цены для фиксации убытка. " + "Пожалуйста, попробуйте позже.", + reply_markup=kbi.back_to_risk_management, + ) + logger.error( + "Error processing command stop_loss_percent for user %s: %s", + message.from_user.id, + e, + ) + + +@router_risk_management.callback_query(F.data == "max_risk_percent") +async def max_risk_percent(callback_query: CallbackQuery, state: FSMContext) -> None: + """ + Handles the 'max_risk_percent' callback query. + + Clears the current FSM state, edits the message text to display the maximum risk percentage options, + and shows an inline keyboard for selection. + + Args: + callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + await state.clear() + await state.set_state(RiskManagementState.max_risk_percent_state) + msg = await callback_query.message.edit_text( + text="Введите максимальный процент риска: ", + reply_markup=kbi.back_to_risk_management, + ) + await state.update_data(prompt_message_id=msg.message_id) + logger.debug( + "Command max_risk_percent processed successfully for user: %s", + callback_query.from_user.id, + ) + except Exception as e: + await callback_query.answer( + text="Произошла ошибка. Пожалуйста, попробуйте позже." + ) + logger.error( + "Error processing command max_risk_percent for user %s: %s", + callback_query.from_user.id, + e, + ) + + +@router_risk_management.message(RiskManagementState.max_risk_percent_state) +async def set_max_risk_percent(message: Message, state: FSMContext) -> None: + """ + Handles user input for setting the maximum risk percentage. + + Updates FSM context with the selected percentage and persists the choice in database. + Sends an acknowledgement to user and clears FSM state afterward. + + Args: + message (Message): Incoming message from user containing the maximum risk percentage. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + try: + data = await state.get_data() + if "prompt_message_id" in data: + prompt_message_id = data["prompt_message_id"] + await message.bot.delete_message( + chat_id=message.chat.id, message_id=prompt_message_id + ) + await message.delete() + except Exception as e: + if "message to delete not found" in str(e).lower(): + pass # Ignore this error + else: + raise e + + max_risk_percent_value = message.text + + if not is_int(max_risk_percent_value): + await message.answer( + text="Ошибка: введите валидное число.", + reply_markup=kbi.back_to_risk_management, + ) + logger.debug( + "User %s input invalid (not an valid number): %s", + message.from_user.id, + max_risk_percent_value, + ) + return + + if int(max_risk_percent_value) < 1 or int(max_risk_percent_value) > 100: + await message.answer( + text="Ошибка: введите число от 1 до 100.", + reply_markup=kbi.back_to_risk_management, + ) + logger.debug( + "User %s input invalid (not an valid number): %s", + message.from_user.id, + max_risk_percent_value, + ) + return + + req = await rq.set_max_risk_percent( + tg_id=message.from_user.id, max_risk_percent=int(max_risk_percent_value) + ) + + if req: + await message.answer( + text=f"Максимальный процент риска установлен на {max_risk_percent_value}%.", + reply_markup=kbi.back_to_risk_management, + ) + else: + await message.answer( + text="Произошла ошибка при установке максимального процента риска. " + "Пожалуйста, попробуйте позже.", + reply_markup=kbi.back_to_risk_management, + ) + + await state.clear() + except Exception as e: + await message.answer( + text="Произошла ошибка при установке максимального процента риска. " + "Пожалуйста, попробуйте позже.", + reply_markup=kbi.back_to_risk_management, + ) + logger.error( + "Error processing command max_risk_percent for user %s: %s", + message.from_user.id, + e, + ) + + +@router_risk_management.callback_query(F.data == "commission_fee") +async def commission_fee(callback_query: CallbackQuery, state: FSMContext) -> None: + """ + Handles the 'commission_fee' callback query. + + Clears the current FSM state, edits the message text to display the commission fee options, + and shows an inline keyboard for selection. + + Args: + callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + await state.clear() + await state.set_state(RiskManagementState.commission_fee_state) + msg = await callback_query.message.edit_text( + text="Учитывать комиссию биржи для расчета прибыли?: ", + reply_markup=kbi.commission_fee, + ) + await state.update_data(prompt_message_id=msg.message_id) + logger.debug( + "Command commission_fee processed successfully for user: %s", + callback_query.from_user.id, + ) + except Exception as e: + await callback_query.answer( + text="Произошла ошибка. Пожалуйста, попробуйте позже." + ) + logger.error( + "Error processing command commission_fee for user %s: %s", + callback_query.from_user.id, + e, + ) + + +@router_risk_management.callback_query( + lambda c: c.data in ["Yes_commission_fee", "No_commission_fee"] +) +async def set_commission_fee(callback_query: CallbackQuery, state: FSMContext) -> None: + """ + Handles user input for setting the commission fee. + + Updates FSM context with the selected option and persists the choice in database. + Sends an acknowledgement to user and clears FSM state afterward. + + Args: + callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. + state (FSMContext): Finite State Machine context for the current user session. + + Logs: + Success or error messages with user identification. + """ + try: + req = await rq.set_commission_fee( + tg_id=callback_query.from_user.id, commission_fee=callback_query.data + ) + + if not req: + await callback_query.answer( + text="Произошла ошибка при установке комиссии биржи. Пожалуйста, попробуйте позже." + ) + return + + if callback_query.data == "Yes_commission_fee": + await callback_query.answer(text="Комиссия биржи учитывается.") + else: + await callback_query.answer(text="Комиссия биржи не учитывается.") + + except Exception as e: + logger.error( + "Error processing command commission_fee for user %s: %s", + callback_query.from_user.id, + e, + ) + finally: + await state.clear() diff --git a/app/telegram/handlers/settings.py b/app/telegram/handlers/settings.py new file mode 100644 index 0000000..b687e0a --- /dev/null +++ b/app/telegram/handlers/settings.py @@ -0,0 +1,278 @@ +import logging.config + +from aiogram import F, Router +from aiogram.fsm.context import FSMContext +from aiogram.types import CallbackQuery + +import app.telegram.keyboards.inline as kbi +import database.request as rq +from app.bybit import get_bybit_client +from app.bybit.get_functions.get_tickers import get_tickers +from app.helper_functions import calculate_total_budget, get_base_currency, safe_float +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("settings") + +router_settings = Router(name="settings") + + +@router_settings.callback_query(F.data == "additional_settings") +async def additional_settings(callback_query: CallbackQuery, state: FSMContext) -> None: + """ + Handler for the "additional_settings" command. + Sends a message with additional settings options. + """ + try: + await state.clear() + tg_id = callback_query.from_user.id + symbol = await rq.get_user_symbol(tg_id=tg_id) + additional_data = await rq.get_user_additional_settings(tg_id=tg_id) + + if not additional_data: + await rq.create_user( + tg_id=tg_id, username=callback_query.from_user.username + ) + await rq.create_user_additional_settings(tg_id=tg_id) + await rq.create_user_risk_management(tg_id=tg_id) + await rq.create_user_conditional_settings(tg_id=tg_id) + await additional_settings(callback_query=callback_query, state=state) + return + + trade_mode_map = { + "Merged_Single": "Односторонний режим", + "Both_Sides": "Хеджирование", + } + margin_type_map = { + "ISOLATED_MARGIN": "Изолированная", + "REGULAR_MARGIN": "Кросс", + } + order_type_map = {"Market": "Рыночный", "Limit": "Лимитный"} + + trade_mode = additional_data.trade_mode or "" + margin_type = additional_data.margin_type or "" + order_type = additional_data.order_type or "" + + trade_mode_rus = trade_mode_map.get(trade_mode, trade_mode) + margin_type_rus = margin_type_map.get(margin_type, margin_type) + order_type_rus = order_type_map.get(order_type, "Условный") + + def f(x): + return safe_float(x) + + leverage = f(additional_data.leverage) + leverage_to_buy = f(additional_data.leverage_to_buy) + leverage_to_sell = f(additional_data.leverage_to_sell) + martingale = f(additional_data.martingale_factor) + max_bets = additional_data.max_bets_in_series + quantity = f(additional_data.order_quantity) + limit_price = f(additional_data.limit_price) + trigger_price = f(additional_data.trigger_price) or 0 + + tickers = await get_tickers(tg_id=tg_id, symbol=symbol) + price_symbol = safe_float(tickers.get("lastPrice")) or 0 + bid = f(tickers.get("bid1Price")) or 0 + ask = f(tickers.get("ask1Price")) or 0 + + sym = get_base_currency(symbol) + + if trade_mode == "Merged_Single": + leverage_str = f"{leverage:.2f}x" + else: + if margin_type == "ISOLATED_MARGIN": + leverage_str = f"{leverage_to_buy:.2f}x:{leverage_to_sell:.2f}x" + else: + leverage_str = f"{leverage:.2f}x" + + conditional_order_type = additional_data.conditional_order_type or "" + conditional_order_type_rus = ( + "Лимитный" + if conditional_order_type == "Limit" + else ( + "Рыночный" + if conditional_order_type == "Market" + else conditional_order_type + ) + ) + + conditional_order_type_text = ( + f"- Тип условного ордера: {conditional_order_type_rus}\n" + if order_type == "Conditional" + else "" + ) + + limit_price_text = "" + trigger_price_text = "" + + if order_type == "Limit": + limit_price_text = f"- Цена лимитного ордера: {limit_price:.4f} USDT\n" + elif order_type == "Conditional": + if conditional_order_type == "Limit": + limit_price_text = f"- Цена лимитного ордера: {limit_price:.4f} USDT\n" + trigger_price_text = f"- Триггер цена: {trigger_price:.4f} USDT\n" + + risk_management_data = await rq.get_user_risk_management(tg_id=tg_id) + commission_fee = risk_management_data.commission_fee + client = await get_bybit_client(tg_id=tg_id) + fee_info = client.get_fee_rates(category="linear", symbol=symbol) + + if commission_fee == "Yes_commission_fee": + commission_fee_percent = safe_float( + fee_info["result"]["list"][0]["takerFeeRate"] + ) + + else: + commission_fee_percent = 0.0 + + if order_type == "Conditional": + if conditional_order_type == "Limit": + entry_price = limit_price + ask_price = limit_price + bid_price = limit_price + else: + ask_price = trigger_price + bid_price = trigger_price + entry_price = trigger_price + else: + if order_type == "Limit": + entry_price = limit_price + ask_price = limit_price + bid_price = limit_price + else: + entry_price = price_symbol + ask_price = ask + bid_price = bid + + durability_buy = quantity * bid_price + durability_sell = quantity * ask_price + quantity_price = quantity * entry_price + total_commission = quantity_price * commission_fee_percent + total_budget = await calculate_total_budget( + quantity=durability_buy, + martingale_factor=martingale, + max_steps=max_bets, + commission_fee_percent=total_commission, + ) + text = ( + f"Основные настройки:\n\n" + f"- Режим позиции: {trade_mode_rus}\n" + f"- Тип маржи: {margin_type_rus}\n" + f"- Размер кредитного плеча: {leverage_str}\n" + f"- Тип ордера: {order_type_rus}\n" + f"- Количество ордера: {quantity} {sym}\n" + f"- Коэффициент мартингейла: {martingale:.2f}\n" + f"{conditional_order_type_text}" + f"{trigger_price_text}" + f"{limit_price_text}" + f"- Максимальное кол-во ставок в серии: {max_bets}\n\n" + f"- Стоимость: {durability_buy:.2f}/{durability_sell:.2f} USDT\n" + f"- Рекомендуемый бюджет: {total_budget:.4f} USDT\n" + ) + + keyboard = kbi.get_additional_settings_keyboard( + current_order_type=order_type, conditional_order=conditional_order_type + ) + await callback_query.message.edit_text(text=text, reply_markup=keyboard) + logger.debug( + "Command additional_settings processed successfully for user: %s", tg_id + ) + except Exception as e: + await callback_query.message.edit_text( + text="Произошла ошибка. Пожалуйста, попробуйте ещё раз.", + reply_markup=kbi.profile_bybit, + ) + logger.error( + "Error processing command additional_settings for user %s: %s", + callback_query.from_user.id, + e, + ) + + +@router_settings.callback_query(F.data == "risk_management") +async def risk_management(callback_query: CallbackQuery, state: FSMContext) -> None: + """ + Handler for the "risk_management" command. + Sends a message with risk management options. + """ + try: + await state.clear() + risk_management_data = await rq.get_user_risk_management( + tg_id=callback_query.from_user.id + ) + if risk_management_data: + take_profit_percent = risk_management_data.take_profit_percent or "" + stop_loss_percent = risk_management_data.stop_loss_percent or "" + max_risk_percent = risk_management_data.max_risk_percent or "" + commission_fee = risk_management_data.commission_fee or "" + commission_fee_rus = ( + "Да" if commission_fee == "Yes_commission_fee" else "Нет" + ) + + await callback_query.message.edit_text( + text=f"Риск-менеджмент:\n\n" + f"- Процент изменения цены для фиксации прибыли: {take_profit_percent}%\n" + f"- Процент изменения цены для фиксации убытка: {stop_loss_percent}%\n\n" + f"- Максимальный риск на сделку (в % от баланса): {max_risk_percent}%\n\n" + f"- Комиссия биржи для расчета прибыли: {commission_fee_rus}\n\n", + reply_markup=kbi.risk_management, + ) + logger.debug( + "Command main_settings processed successfully for user: %s", + callback_query.from_user.id, + ) + else: + await rq.create_user( + tg_id=callback_query.from_user.id, + username=callback_query.from_user.username, + ) + await rq.create_user_additional_settings(tg_id=callback_query.from_user.id) + await rq.create_user_risk_management(tg_id=callback_query.from_user.id) + await rq.create_user_conditional_settings(tg_id=callback_query.from_user.id) + await risk_management(callback_query=callback_query, state=state) + except Exception as e: + logger.error( + "Error processing command main_settings for user %s: %s", + callback_query.from_user.id, + e, + ) + + +@router_settings.callback_query(F.data == "conditional_settings") +async def conditions(callback_query: CallbackQuery, state: FSMContext) -> None: + """ + Handler for the "conditions" command. + Sends a message with trading conditions options. + """ + try: + await state.clear() + conditional_settings_data = await rq.get_user_conditional_settings( + tg_id=callback_query.from_user.id + ) + if conditional_settings_data: + start_timer = conditional_settings_data.timer_start or 0 + stop_timer = conditional_settings_data.timer_end or 0 + await callback_query.message.edit_text( + text="Условия торговли:\n\n" + f"- Таймер для старта: {start_timer} мин.\n" + f"- Таймер для остановки: {stop_timer} мин.\n", + reply_markup=kbi.conditions, + ) + logger.debug( + "Command main_settings processed successfully for user: %s", + callback_query.from_user.id, + ) + else: + await rq.create_user( + tg_id=callback_query.from_user.id, + username=callback_query.from_user.username, + ) + await rq.create_user_additional_settings(tg_id=callback_query.from_user.id) + await rq.create_user_risk_management(tg_id=callback_query.from_user.id) + await rq.create_user_conditional_settings(tg_id=callback_query.from_user.id) + await conditions(callback_query=callback_query, state=state) + except Exception as e: + logger.error( + "Error processing command main_settings for user %s: %s", + callback_query.from_user.id, + e, + ) diff --git a/app/telegram/handlers/start_trading.py b/app/telegram/handlers/start_trading.py new file mode 100644 index 0000000..351bb33 --- /dev/null +++ b/app/telegram/handlers/start_trading.py @@ -0,0 +1,414 @@ +import asyncio +import logging.config + +from aiogram import F, Router +from aiogram.fsm.context import FSMContext +from aiogram.types import CallbackQuery + +import app.telegram.keyboards.inline as kbi +import database.request as rq +from app.bybit.get_functions.get_positions import get_active_positions_by_symbol +from app.bybit.open_positions import start_trading_cycle +from app.helper_functions import safe_float +from app.telegram.tasks.tasks import ( + add_start_task_merged, + add_start_task_switch, + cancel_start_task_merged, + cancel_start_task_switch, +) +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("start_trading") + +router_start_trading = Router(name="start_trading") + + +@router_start_trading.callback_query(F.data == "start_trading") +async def start_trading(callback_query: CallbackQuery, state: FSMContext) -> None: + """ + Handles the "start_trading" callback query. + Clears the FSM state and sends a message to the user to select the trading mode. + :param callback_query: Message + :param state: FSMContext + :return: None + """ + try: + await state.clear() + additional_data = await rq.get_user_additional_settings( + tg_id=callback_query.from_user.id + ) + trade_mode = additional_data.trade_mode + symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id) + deals = await get_active_positions_by_symbol( + tg_id=callback_query.from_user.id, symbol=symbol + ) + position = next((d for d in deals if d.get("symbol") == symbol), None) + if position: + size = position.get("size", 0) + position_idx = position.get("positionIdx") + else: + size = 0 + position_idx = None + + if position_idx != 0 and safe_float(size) > 0 and trade_mode == "Merged_Single": + await callback_query.answer( + text="У вас есть активная позиция в режиме хеджирования. " + "Открытие сделки в одностороннем режиме невозможно.", + ) + return + + if position_idx == 0 and safe_float(size) > 0 and trade_mode == "Both_Sides": + await callback_query.answer( + text="У вас есть активная позиция в одностороннем режиме. " + "Открытие сделки в режиме хеджирования невозможно.", + ) + return + + if trade_mode == "Merged_Single": + await callback_query.message.edit_text( + text="Выберите режим торговли:\n\n" + "Лонг - все сделки серии открываются на покупку.\n" + "Шорт - все сделки серии открываются на продажу.\n" + "Свитч - направление каждой сделки серии меняется по переменно.\n", + reply_markup=kbi.merged_start_trading, + ) + else: # trade_mode == "Both_Sides": + await callback_query.message.edit_text( + text="Выберите режим торговли:\n\n" + "Лонг - все сделки открываются на покупку.\n" + "Шорт - все сделки открываются на продажу.\n", + reply_markup=kbi.both_start_trading, + ) + logger.debug( + "Command start_trading processed successfully for user: %s", + callback_query.from_user.id, + ) + except Exception as e: + await callback_query.answer(text="Произошла ошибка при запуске торговли") + logger.error( + "Error processing command start_trading for user %s: %s", + callback_query.from_user.id, + e, + ) + + +@router_start_trading.callback_query(lambda c: c.data == "long" or c.data == "short") +async def start_trading_long(callback_query: CallbackQuery, state: FSMContext) -> None: + """ + Handles the "long" or "short" callback query. + Clears the FSM state and starts the trading cycle. + :param callback_query: Message + :param state: FSMContext + :return: None + """ + try: + if callback_query.data == "long": + side = "Buy" + elif callback_query.data == "short": + side = "Sell" + else: + await callback_query.answer(text="Произошла ошибка при запуске торговли") + return + + symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id) + deals = await get_active_positions_by_symbol( + tg_id=callback_query.from_user.id, symbol=symbol + ) + position = next((d for d in deals if d.get("symbol") == symbol), None) + if position: + size = position.get("size", 0) + position_idx = position.get("positionIdx") + else: + size = 0 + position_idx = None + + if position_idx == 0 and safe_float(size) > 0: + await callback_query.answer( + text="Торговля уже запущена в одностороннем режиме для данного инструмента" + ) + return + + conditional_data = await rq.get_user_conditional_settings( + tg_id=callback_query.from_user.id + ) + timer_start = conditional_data.timer_start + + cancel_start_task_merged(user_id=callback_query.from_user.id) + + async def delay_start(): + if timer_start > 0: + await callback_query.message.edit_text( + text=f"Торговля будет запущена с задержкой {timer_start} мин.", + reply_markup=kbi.cancel_timer_merged, + ) + await rq.set_start_timer( + tg_id=callback_query.from_user.id, timer_start=0 + ) + await asyncio.sleep(timer_start * 60) + + await rq.set_auto_trading( + tg_id=callback_query.from_user.id, + symbol=symbol, + auto_trading=True, + side=side, + ) + res = await start_trading_cycle( + tg_id=callback_query.from_user.id, + side=side, + switch_side_mode=False, + ) + + error_messages = { + "Limit price is out min price": "Цена лимитного ордера меньше минимального", + "Limit price is out max price": "Цена лимитного ордера больше максимального", + "Risk is too high for this trade": "Риск сделки превышает допустимый убыток", + "estimated will trigger liq": "Лимитный ордер может вызвать мгновенную ликвидацию. Проверьте параметры ордера.", + "ab not enough for new order": "Недостаточно средств для создания нового ордера", + "InvalidRequestError": "Произошла ошибка при запуске торговли.", + "Order does not meet minimum order value": "Сумма ордера не достаточна для запуска торговли", + "position idx not match position mode": "Торговля уже запущена в режиме хеджирования на продажу для данного инструмента", + "Qty invalid": "Некорректное значение ордера для данного инструмента", + "The number of contracts exceeds maximum limit allowed": "️️Количество контрактов превышает допустимое максимальное количество контрактов", + } + + if res == "OK": + await callback_query.message.edit_text(text="Торговля запущена") + await state.clear() + else: + await rq.set_auto_trading( + tg_id=callback_query.from_user.id, + symbol=symbol, + auto_trading=False, + side=side, + ) + text = error_messages.get(res, "Произошла ошибка при запуске торговли") + await callback_query.message.edit_text( + text=text, reply_markup=kbi.profile_bybit + ) + + await callback_query.message.edit_text("Запуск торговли...") + task = asyncio.create_task(delay_start()) + await add_start_task_merged(user_id=callback_query.from_user.id, task=task) + + except Exception as e: + await callback_query.answer(text="Произошла ошибка при запуске торговли") + logger.error( + "Error processing command long for user %s: %s", + callback_query.from_user.id, + e, + ) + except asyncio.CancelledError: + logger.error("Cancelled timer for user %s", callback_query.from_user.id) + + +@router_start_trading.callback_query(lambda c: c.data == "switch") +async def start_trading_switch( + callback_query: CallbackQuery, state: FSMContext +) -> None: + """ + Handles the "switch" callback query. + Clears the FSM state and sends a message to the user to select the switch side. + :param callback_query: Message + :param state: FSMContext + :return: None + """ + try: + await state.clear() + await callback_query.message.edit_text( + text="Выберите направление первой сделки серии:\n\n" + "Лонг - открывается первая сделка на покупку.\n" + "Шорт - открывается первая сделка на продажу.\n" + "По направлению - сделка открывается в направлении последней сделки предыдущей серии.\n" + "Противоположно - сделка открывается в противоположном направлении последней сделки предыдущей серии.\n", + reply_markup=kbi.switch_side, + ) + except Exception as e: + await callback_query.answer(text="Произошла ошибка при запуске торговли") + logger.error( + "Error processing command start trading switch for user %s: %s", + callback_query.from_user.id, + e, + ) + + +@router_start_trading.callback_query( + lambda c: c.data + in {"switch_long", "switch_short", "switch_direction", "switch_opposite"} +) +async def start_switch(callback_query: CallbackQuery, state: FSMContext) -> None: + """ + Starts the trading cycle with the selected side. + :param callback_query: + :param state: + :return: + """ + try: + symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id) + user_deals_data = await rq.get_user_deal_by_symbol( + tg_id=callback_query.from_user.id, symbol=symbol + ) + + get_side = "Buy" + + if user_deals_data: + get_side = user_deals_data.last_side or "Buy" + + if callback_query.data == "switch_long": + side = "Buy" + elif callback_query.data == "switch_short": + side = "Sell" + elif callback_query.data == "switch_direction": + side = get_side + elif callback_query.data == "switch_opposite": + if get_side == "Buy": + side = "Sell" + else: + side = "Buy" + else: + await callback_query.answer(text="Произошла ошибка при запуске торговли") + return + + deals = await get_active_positions_by_symbol( + tg_id=callback_query.from_user.id, symbol=symbol + ) + position = next((d for d in deals if d.get("symbol") == symbol), None) + if position: + size = position.get("size", 0) + position_idx = position.get("positionIdx") + else: + size = 0 + position_idx = None + + if position_idx == 1 and safe_float(size) > 0 and side == "Buy": + await callback_query.answer( + text="Торговля уже запущена в режиме хеджирования на покупку для данного инструмента" + ) + return + + if position_idx == 2 and safe_float(size) > 0 and side == "Sell": + await callback_query.answer( + text="Торговля уже запущена в режиме хеджирования на продажу для данного инструмента" + ) + return + + conditional_data = await rq.get_user_conditional_settings( + tg_id=callback_query.from_user.id + ) + timer_start = conditional_data.timer_start + + cancel_start_task_switch(user_id=callback_query.from_user.id) + + async def delay_start(): + if timer_start > 0: + await callback_query.message.edit_text( + text=f"Торговля будет запущена с задержкой {timer_start} мин.", + reply_markup=kbi.cancel_timer_switch, + ) + await rq.set_start_timer( + tg_id=callback_query.from_user.id, timer_start=0 + ) + await asyncio.sleep(timer_start * 60) + await rq.set_auto_trading( + tg_id=callback_query.from_user.id, + symbol=symbol, + auto_trading=True, + side=side, + ) + if side == "Buy": + r_side = "Sell" + else: + r_side = "Buy" + await rq.set_auto_trading( + tg_id=callback_query.from_user.id, + symbol=symbol, + auto_trading=True, + side=r_side, + ) + res = await start_trading_cycle( + tg_id=callback_query.from_user.id, + side=side, + switch_side_mode=True, + ) + + error_messages = { + "Limit price is out min price": "Цена лимитного ордера меньше минимального", + "Limit price is out max price": "Цена лимитного ордера больше максимального", + "Risk is too high for this trade": "Риск сделки превышает допустимый убыток", + "estimated will trigger liq": "Лимитный ордер может вызвать мгновенную ликвидацию. Проверьте параметры ордера.", + "ab not enough for new order": "Недостаточно средств для создания нового ордера", + "InvalidRequestError": "Произошла ошибка при запуске торговли.", + "Order does not meet minimum order value": "Сумма ордера не достаточна для запуска торговли", + "position idx not match position mode": "Торговля уже запущена в режиме хеджирования на продажу для данного инструмента", + "Qty invalid": "Некорректное значение ордера для данного инструмента", + "The number of contracts exceeds maximum limit allowed": "️ ️️Количество контрактов превышает допустимое максимальное количество контрактов", + } + + if res == "OK": + await callback_query.message.edit_text(text="Торговля запущена") + await state.clear() + else: + await rq.set_auto_trading( + tg_id=callback_query.from_user.id, + symbol=symbol, + auto_trading=False, + side=side, + ) + if side == "Buy": + r_side = "Sell" + else: + r_side = "Buy" + await rq.set_auto_trading( + tg_id=callback_query.from_user.id, + symbol=symbol, + auto_trading=False, + side=r_side, + ) + text = error_messages.get(res, "Произошла ошибка при запуске торговли") + await callback_query.message.edit_text( + text=text, reply_markup=kbi.profile_bybit + ) + + await callback_query.message.edit_text("Запуск торговли...") + task = asyncio.create_task(delay_start()) + await add_start_task_switch(user_id=callback_query.from_user.id, task=task) + except asyncio.CancelledError: + logger.error("Cancelled timer for user %s", callback_query.from_user.id) + except Exception as e: + await callback_query.answer(text="Произошла ошибка при запуске торговли") + logger.error( + "Error processing command start switch for user %s: %s", + callback_query.from_user.id, + e, + ) + + +@router_start_trading.callback_query( + lambda c: c.data == "cancel_timer_merged" or c.data == "cancel_timer_switch" +) +async def cancel_start_trading( + callback_query: CallbackQuery, state: FSMContext +) -> None: + """ + Handles the "cancel_timer" callback query. + Clears the FSM state and sends a message to the user to cancel the start trading process. + :param callback_query: Message + :param state: FSMContext + :return: None + """ + try: + await state.clear() + if callback_query.data == "cancel_timer_merged": + cancel_start_task_merged(user_id=callback_query.from_user.id) + elif callback_query.data == "cancel_timer_switch": + cancel_start_task_switch(user_id=callback_query.from_user.id) + await callback_query.message.edit_text( + text="Запуск торговли отменен", reply_markup=kbi.profile_bybit + ) + except Exception as e: + await callback_query.answer("Произошла ошибка при отмене запуска торговли") + logger.error( + "Error processing command cancel_timer for user %s: %s", + callback_query.from_user.id, + e, + ) diff --git a/app/telegram/handlers/stop_trading.py b/app/telegram/handlers/stop_trading.py new file mode 100644 index 0000000..e9fc6c4 --- /dev/null +++ b/app/telegram/handlers/stop_trading.py @@ -0,0 +1,99 @@ +import asyncio +import logging.config + +from aiogram import F, Router +from aiogram.fsm.context import FSMContext +from aiogram.types import CallbackQuery + +import app.telegram.keyboards.inline as kbi +import database.request as rq +from app.telegram.tasks.tasks import add_stop_task, cancel_stop_task +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("stop_trading") + +router_stop_trading = Router(name="stop_trading") + + +@router_stop_trading.callback_query(F.data == "stop_trading") +async def stop_all_trading(callback_query: CallbackQuery, state: FSMContext): + try: + await state.clear() + + cancel_stop_task(callback_query.from_user.id) + + conditional_data = await rq.get_user_conditional_settings( + tg_id=callback_query.from_user.id + ) + timer_end = conditional_data.timer_end + + async def delay_start(): + if timer_end > 0: + await callback_query.message.edit_text( + text=f"Торговля будет остановлена с задержкой {timer_end} мин.", + reply_markup=kbi.cancel_timer_stop, + ) + await rq.set_stop_timer(tg_id=callback_query.from_user.id, timer_end=0) + await asyncio.sleep(timer_end * 60) + + user_auto_trading_list = await rq.get_all_user_auto_trading( + tg_id=callback_query.from_user.id + ) + + if any(item.auto_trading for item in user_auto_trading_list): + for active_auto_trading in user_auto_trading_list: + if active_auto_trading.auto_trading: + symbol = active_auto_trading.symbol + get_side = active_auto_trading.side + req = await rq.set_auto_trading( + tg_id=callback_query.from_user.id, + symbol=symbol, + auto_trading=False, + side=get_side, + ) + if not req: + await callback_query.message.edit_text( + text="Произошла ошибка при остановке торговли", + reply_markup=kbi.profile_bybit, + ) + return + await callback_query.message.edit_text( + text="Торговля остановлена", reply_markup=kbi.profile_bybit + ) + else: + await callback_query.message.edit_text(text="Нет активной торговли") + + task = asyncio.create_task(delay_start()) + await add_stop_task(user_id=callback_query.from_user.id, task=task) + + logger.debug( + "Command stop_trading processed successfully for user: %s", + callback_query.from_user.id, + ) + except Exception as e: + await callback_query.answer(text="Произошла ошибка при остановке торговли") + logger.error( + "Error processing command stop_trading for user %s: %s", + callback_query.from_user.id, + e, + ) + + +@router_stop_trading.callback_query(F.data == "cancel_timer_stop") +async def cancel_stop_trading(callback_query: CallbackQuery, state: FSMContext): + try: + await state.clear() + cancel_stop_task(callback_query.from_user.id) + await callback_query.message.edit_text( + text="Таймер отменён.", reply_markup=kbi.profile_bybit + ) + except Exception as e: + await callback_query.answer( + text="Произошла ошибка при отмене остановки торговли" + ) + logger.error( + "Error processing command cancel_timer_stop for user %s: %s", + callback_query.from_user.id, + e, + ) diff --git a/app/telegram/handlers/tp_sl_handlers.py b/app/telegram/handlers/tp_sl_handlers.py new file mode 100644 index 0000000..8bc9c24 --- /dev/null +++ b/app/telegram/handlers/tp_sl_handlers.py @@ -0,0 +1,168 @@ +import logging.config + +from aiogram import Router +from aiogram.fsm.context import FSMContext +from aiogram.types import CallbackQuery, Message + +import app.telegram.keyboards.inline as kbi +from app.bybit.set_functions.set_tp_sl import set_tp_sl_for_position +from app.helper_functions import is_number +from app.telegram.states.states import SetTradingStopState +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("tp_sl_handlers") + +router_tp_sl_handlers = Router(name="tp_sl_handlers") + + +@router_tp_sl_handlers.callback_query(lambda c: c.data.startswith("pos_tp_sl_")) +async def set_tp_sl_handler(callback_query: CallbackQuery, state: FSMContext) -> None: + """ + Handles the 'pos_tp_sl' callback query. + + Clears the current FSM state, sets the state to 'take_profit', and prompts the user to enter the take-profit. + + Args: + callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. + state (FSMContext): Finite State Machine context for the current user session. + """ + try: + await state.clear() + data = callback_query.data + parts = data.split("_") + symbol = parts[3] + position_idx = int(parts[4]) + + await state.set_state(SetTradingStopState.take_profit_state) + await state.update_data(symbol=symbol) + await state.update_data(position_idx=position_idx) + msg = await callback_query.message.answer( + text="Введите тейк-профит:", reply_markup=kbi.cancel + ) + await state.update_data(prompt_message_id=msg.message_id) + except Exception as e: + logger.error("Error in set_tp_sl_handler: %s", e) + await callback_query.answer(text="Произошла ошибка, попробуйте позже") + + +@router_tp_sl_handlers.message(SetTradingStopState.take_profit_state) +async def set_take_profit_handler(message: Message, state: FSMContext) -> None: + """ + Handles the 'take_profit' state. + + Clears the current FSM state, sets the state to 'stop_loss', and prompts the user to enter the stop-loss. + + Args: + message (Message): Incoming message from Telegram. + state (FSMContext): Finite State Machine context for the current user session. + + Returns: + None + """ + try: + try: + data = await state.get_data() + if "prompt_message_id" in data: + prompt_message_id = data["prompt_message_id"] + await message.bot.delete_message( + chat_id=message.chat.id, message_id=prompt_message_id + ) + await message.delete() + except Exception as e: + if "message to delete not found" in str(e).lower(): + pass # Ignore this error + else: + raise e + + take_profit = message.text + + if not is_number(take_profit): + await message.answer( + "Ошибка: введите валидное число.", + reply_markup=kbi.profile_bybit, + ) + logger.debug( + "User %s input invalid (not an valid number): %s", + message.from_user.id, + take_profit, + ) + return + + await state.update_data(take_profit=take_profit) + await state.set_state(SetTradingStopState.stop_loss_state) + msg = await message.answer(text="Введите стоп-лосс:", reply_markup=kbi.cancel) + await state.update_data(prompt_message_id=msg.message_id) + except Exception as e: + logger.error("Error in set_take_profit_handler: %s", e) + await message.answer( + text="Произошла ошибка, попробуйте позже", reply_markup=kbi.profile_bybit + ) + + +@router_tp_sl_handlers.message(SetTradingStopState.stop_loss_state) +async def set_stop_loss_handler(message: Message, state: FSMContext) -> None: + """ + Handles the 'stop_loss' state. + + Clears the current FSM state, sets the state to 'take_profit', and prompts the user to enter the take-profit. + + Args: + message (Message): Incoming message from Telegram. + state (FSMContext): Finite State Machine context for the current user session. + + Returns: + None + """ + try: + try: + data = await state.get_data() + if "prompt_message_id" in data: + prompt_message_id = data["prompt_message_id"] + await message.bot.delete_message( + chat_id=message.chat.id, message_id=prompt_message_id + ) + await message.delete() + except Exception as e: + if "message to delete not found" in str(e).lower(): + pass # Ignore this error + else: + raise e + + stop_loss = message.text + + if not is_number(stop_loss): + await message.answer( + "Ошибка: введите валидное число.", + reply_markup=kbi.profile_bybit, + ) + logger.debug( + "User %s input invalid (not an valid number): %s", + message.from_user.id, + stop_loss, + ) + return + + await state.update_data(stop_loss=stop_loss) + data = await state.get_data() + symbol = data["symbol"] + take_profit = data["take_profit"] + position_idx = data["position_idx"] + res = await set_tp_sl_for_position( + tg_id=message.from_user.id, + symbol=symbol, + take_profit_price=float(take_profit), + stop_loss_price=float(stop_loss), + position_idx=position_idx, + ) + + if res: + await message.answer(text="Тейк-профит и стоп-лосс установлены.") + else: + await message.answer(text="Тейк-профит и стоп-лосс не установлены.") + await state.clear() + except Exception as e: + await message.answer( + text="Произошла ошибка, попробуйте позже", reply_markup=kbi.profile_bybit + ) + logger.error("Error in set_stop_loss_handler: %s", e) diff --git a/app/telegram/keyboards/inline.py b/app/telegram/keyboards/inline.py new file mode 100644 index 0000000..7c1f559 --- /dev/null +++ b/app/telegram/keyboards/inline.py @@ -0,0 +1,484 @@ +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from aiogram.utils.keyboard import InlineKeyboardBuilder + +connect_the_platform = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text="Подключить платформу", callback_data="connect_platform" + ) + ] + ] +) + +add_bybit_api = InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="Добавить API", callback_data="add_bybit_api")] + ] +) + +profile_bybit = InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="На главную", callback_data="profile_bybit")] + ] +) + +cancel = InlineKeyboardMarkup( + inline_keyboard=[[InlineKeyboardButton(text="Отменить", callback_data="cancel")]] +) + +main_menu = InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="Настройки", callback_data="main_settings")], + [ + InlineKeyboardButton( + text="Сменить торговую пару", callback_data="change_symbol" + ) + ], + [InlineKeyboardButton(text="Мои сделки", callback_data="my_deals")], + [InlineKeyboardButton(text="Начать торговлю", callback_data="start_trading")], + [ + InlineKeyboardButton( + text="Остановить торговлю", callback_data="stop_trading" + ) + ], + ] +) + +# MAIN SETTINGS +main_settings = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text="Основные настройки", callback_data="additional_settings" + ), + InlineKeyboardButton( + text="Риск-менеджмент", callback_data="risk_management" + ), + ], + [ + InlineKeyboardButton( + text="Условия запуска", callback_data="conditional_settings" + ) + ], + [InlineKeyboardButton(text="Назад", callback_data="profile_bybit")], + ] +) + + +# additional_settings +def get_additional_settings_keyboard( + current_order_type: str, conditional_order: str +) -> InlineKeyboardMarkup: + """ + Create keyboard for additional settings + :param current_order_type: Market, Limit or Conditional + :param conditional_order: Market or Limit + :return: InlineKeyboardMarkup + """ + buttons = [ + [ + InlineKeyboardButton(text="Режим позиции", callback_data="trade_mode"), + InlineKeyboardButton(text="Тип маржи", callback_data="margin_type"), + ], + [ + InlineKeyboardButton( + text="Размер кредитного плеча", callback_data="leverage" + ), + InlineKeyboardButton(text="Тип ордера", callback_data="order_type"), + ], + [ + InlineKeyboardButton( + text="Количество ордера", callback_data="order_quantity" + ), + InlineKeyboardButton( + text="Коэффициент мартингейла", callback_data="martingale_factor" + ), + ], + ] + + if current_order_type == "Conditional": + buttons.append( + [ + InlineKeyboardButton( + text="Тип условного ордера", callback_data="conditional_order_type" + ) + ] + ) + buttons.append( + [InlineKeyboardButton(text="Триггер цена", callback_data="trigger_price")] + ) + if conditional_order == "Limit": + buttons.append( + [ + InlineKeyboardButton( + text="Цена лимитного ордера", callback_data="limit_price" + ) + ] + ) + elif current_order_type == "Limit": + buttons.append( + [ + InlineKeyboardButton( + text="Цена лимитного ордера", callback_data="limit_price" + ) + ] + ) + + buttons.append( + [ + InlineKeyboardButton( + text="Максимальное кол-во ставок в серии", + callback_data="max_bets_in_series", + ) + ] + ) + buttons.append( + [ + InlineKeyboardButton(text="Назад", callback_data="main_settings"), + InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), + ] + ) + + return InlineKeyboardMarkup(inline_keyboard=buttons) + + +order_type = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text="Рыночный", callback_data="Market"), + InlineKeyboardButton(text="Лимитный", callback_data="Limit"), + ], + [InlineKeyboardButton(text="Условный", callback_data="Conditional")], + [ + InlineKeyboardButton(text="Назад", callback_data="additional_settings"), + InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), + ], + ] +) + +conditional_order_type = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text="Рыночный", callback_data="set_market"), + InlineKeyboardButton(text="Лимитный", callback_data="set_limit"), + ], + [ + InlineKeyboardButton(text="Назад", callback_data="additional_settings"), + InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), + ], + ] +) + +trade_mode = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text="Односторонний режим", callback_data="Merged_Single" + ), + InlineKeyboardButton(text="Хеджирование", callback_data="Both_Sides"), + ], + [ + InlineKeyboardButton(text="Назад", callback_data="additional_settings"), + InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), + ], + ] +) + +margin_type = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text="Изолированная", callback_data="ISOLATED_MARGIN"), + InlineKeyboardButton(text="Кросс", callback_data="REGULAR_MARGIN"), + ], + [ + InlineKeyboardButton(text="Назад", callback_data="additional_settings"), + InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), + ], + ] +) + +back_to_additional_settings = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text="Назад", callback_data="additional_settings"), + InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), + ], + ] +) + +change_limit_price = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text="Установить цену", callback_data="set_limit_price" + ), + InlineKeyboardButton(text="Последняя цена", callback_data="last_price"), + ], + [ + InlineKeyboardButton(text="Назад", callback_data="additional_settings"), + InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), + ], + ] +) + +back_to_change_limit_price = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text="Назад", callback_data="limit_price"), + InlineKeyboardButton( + text="Основные настройки", callback_data="additional_settings" + ), + ], + [InlineKeyboardButton(text="На главную", callback_data="profile_bybit")], + ] +) + +# risk_management +risk_management = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text="Изм. цены прибыли", callback_data="take_profit_percent" + ), + InlineKeyboardButton( + text="Изм. цены убытка", callback_data="stop_loss_percent" + ), + ], + [ + InlineKeyboardButton( + text="Максимальный риск", callback_data="max_risk_percent" + ) + ], + [InlineKeyboardButton(text="Комиссия биржи", callback_data="commission_fee")], + [ + InlineKeyboardButton(text="Назад", callback_data="main_settings"), + InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), + ], + ] +) + +back_to_risk_management = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text="Назад", callback_data="risk_management"), + InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), + ], + ] +) + +commission_fee = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text="Да", callback_data="Yes_commission_fee"), + InlineKeyboardButton(text="Нет", callback_data="No_commission_fee"), + ], + [ + InlineKeyboardButton(text="Назад", callback_data="risk_management"), + InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), + ], + ] +) + +# conditions +conditions = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text="Таймер для старта", callback_data="start_timer"), + InlineKeyboardButton( + text="Таймер для остановки", callback_data="stop_timer" + ), + ], + [ + InlineKeyboardButton(text="Назад", callback_data="main_settings"), + InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), + ], + ] +) + +back_to_conditions = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text="Назад", callback_data="conditional_settings"), + InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), + ], + ] +) + +# SYMBOL +symbol = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text="Назад", callback_data="profile_bybit"), + ], + ] +) + +# POSITION + +change_position = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text="Позиции", callback_data="change_position"), + InlineKeyboardButton(text="Открытые ордера", callback_data="open_orders"), + ], + [InlineKeyboardButton(text="Назад", callback_data="profile_bybit")], + ] +) + + +def create_active_positions_keyboard(symbols: list): + builder = InlineKeyboardBuilder() + for sym, side in symbols: + builder.button(text=f"{sym}:{side}", callback_data=f"get_position_{sym}_{side}") + builder.button(text="Назад", callback_data="my_deals") + builder.button(text="На главную", callback_data="profile_bybit") + builder.adjust(2) + return builder.as_markup() + + +def make_close_position_keyboard( + symbol_pos: str, side: str, position_idx: int, qty: int +): + return InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text="Закрыть позицию", + callback_data=f"close_position_{symbol_pos}_{side}_{position_idx}_{qty}", + ) + ], + [ + InlineKeyboardButton( + text="Установить TP/SL", + callback_data=f"pos_tp_sl_{symbol_pos}_{position_idx}", + ) + ], + [ + InlineKeyboardButton(text="Назад", callback_data="change_position"), + InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), + ], + ] + ) + + +def create_active_orders_keyboard(orders: list): + builder = InlineKeyboardBuilder() + for order, side in orders: + builder.button(text=f"{order}", callback_data=f"get_order_{order}_{side}") + builder.button(text="Назад", callback_data="my_deals") + builder.button(text="На главную", callback_data="profile_bybit") + builder.adjust(2) + return builder.as_markup() + + +def make_close_orders_keyboard(symbol_order: str, order_id: str): + return InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text="Закрыть ордер", + callback_data=f"close_order_{symbol_order}_{order_id}", + ) + ], + [ + InlineKeyboardButton(text="Назад", callback_data="open_orders"), + InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), + ], + ] + ) + + +# START TRADING + +merged_start_trading = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text="Лонг", callback_data="long"), + InlineKeyboardButton(text="Шорт", callback_data="short"), + ], + [InlineKeyboardButton(text="Свитч", callback_data="switch")], + [InlineKeyboardButton(text="Назад", callback_data="profile_bybit")], + ] +) + +both_start_trading = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text="Лонг", callback_data="long"), + InlineKeyboardButton(text="Шорт", callback_data="short"), + ], + [InlineKeyboardButton(text="Назад", callback_data="profile_bybit")], + ] +) + +switch_side = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text="Лонг", callback_data="switch_long"), + InlineKeyboardButton(text="Шорт", callback_data="switch_short"), + ], + [ + InlineKeyboardButton( + text="По направлению", callback_data="switch_direction" + ), + InlineKeyboardButton( + text="Противоположно", callback_data="switch_opposite" + ), + ], + [ + InlineKeyboardButton(text="Назад", callback_data="start_trading"), + InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), + ], + ] +) + +back_to_start_trading = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text="Назад", callback_data="start_trading"), + InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), + ], + ] +) + +cancel_timer_merged = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text="Отменить таймер", callback_data="cancel_timer_merged" + ) + ], + [ + InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), + ], + ] +) + +cancel_timer_switch = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text="Отменить таймер", callback_data="cancel_timer_switch" + ) + ], + [ + InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), + ], + ] +) + +# STOP TRADING + +cancel_timer_stop = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text="Отменить таймер", callback_data="cancel_timer_stop" + ) + ], + [ + InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), + ], + ] +) diff --git a/app/telegram/keyboards/reply.py b/app/telegram/keyboards/reply.py new file mode 100644 index 0000000..fed4241 --- /dev/null +++ b/app/telegram/keyboards/reply.py @@ -0,0 +1,9 @@ +from aiogram.types import KeyboardButton, ReplyKeyboardMarkup + +profile = ReplyKeyboardMarkup( + keyboard=[[KeyboardButton(text="Панель Bybit"), KeyboardButton(text="Профиль")], + [KeyboardButton(text="Подключить платформу Bybit")]], + resize_keyboard=True, + one_time_keyboard=True, + input_field_placeholder="Выберите пункт меню...", +) diff --git a/app/telegram/states/__init__.py b/app/telegram/states/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/telegram/states/states.py b/app/telegram/states/states.py new file mode 100644 index 0000000..a5b2a4d --- /dev/null +++ b/app/telegram/states/states.py @@ -0,0 +1,51 @@ +from aiogram.fsm.state import State, StatesGroup + + +class AddBybitApiState(StatesGroup): + """States for adding Bybit API keys.""" + + api_key_state = State() + api_secret_state = State() + + +class AdditionalSettingsState(StatesGroup): + """States for additional settings.""" + + leverage_state = State() + leverage_to_buy_state = State() + leverage_to_sell_state = State() + quantity_state = State() + martingale_factor_state = State() + max_bets_in_series_state = State() + limit_price_state = State() + trigger_price_state = State() + + +class RiskManagementState(StatesGroup): + """States for risk management.""" + + take_profit_percent_state = State() + stop_loss_percent_state = State() + max_risk_percent_state = State() + commission_fee_state = State() + + +class ConditionalSettingsState(StatesGroup): + """States for conditional settings.""" + + start_timer_state = State() + stop_timer_state = State() + + +class ChangingTheSymbolState(StatesGroup): + """States for changing the symbol.""" + + symbol_state = State() + + +class SetTradingStopState(StatesGroup): + """States for setting a trading stop.""" + + symbol_state = State() + take_profit_state = State() + stop_loss_state = State() diff --git a/app/telegram/tasks/__init__.py b/app/telegram/tasks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/telegram/tasks/tasks.py b/app/telegram/tasks/tasks.py new file mode 100644 index 0000000..0126e51 --- /dev/null +++ b/app/telegram/tasks/tasks.py @@ -0,0 +1,77 @@ +import asyncio +import logging.config + +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("tasks") + +user_start_tasks_merged = {} +user_start_tasks_switch = {} +user_stop_tasks = {} + + +async def add_start_task_merged(user_id: int, task: asyncio.Task): + """Add task to user_start_tasks dict""" + if user_id in user_start_tasks_merged: + old_task = user_start_tasks_merged[user_id] + if not old_task.done(): + old_task.cancel() + try: + await old_task + except asyncio.CancelledError: + pass + user_start_tasks_merged[user_id] = task + + +async def add_start_task_switch(user_id: int, task: asyncio.Task): + """Add task to user_start_tasks dict""" + if user_id in user_start_tasks_switch: + old_task = user_start_tasks_switch[user_id] + if not old_task.done(): + old_task.cancel() + try: + await old_task + except asyncio.CancelledError: + pass + user_start_tasks_switch[user_id] = task + + +async def add_stop_task(user_id: int, task: asyncio.Task): + """Add task to user_stop_tasks dict""" + if user_id in user_stop_tasks: + old_task = user_stop_tasks[user_id] + if not old_task.done(): + old_task.cancel() + try: + await old_task + except asyncio.CancelledError: + pass + user_stop_tasks[user_id] = task + + +def cancel_start_task_merged(user_id: int): + """Cancel task from user_start_tasks dict""" + if user_id in user_start_tasks_merged: + task = user_start_tasks_merged[user_id] + if not task.done(): + task.cancel() + del user_start_tasks_merged[user_id] + + +def cancel_start_task_switch(user_id: int): + """Cancel task from user_start_tasks dict""" + if user_id in user_start_tasks_switch: + task = user_start_tasks_switch[user_id] + if not task.done(): + task.cancel() + del user_start_tasks_switch[user_id] + + +def cancel_stop_task(user_id: int): + """Cancel task from user_stop_tasks dict""" + if user_id in user_stop_tasks: + task = user_stop_tasks[user_id] + if not task.done(): + task.cancel() + del user_stop_tasks[user_id] diff --git a/config.py b/config.py index a55e59e..d1fc4bd 100644 --- a/config.py +++ b/config.py @@ -1,9 +1,31 @@ -from dotenv import load_dotenv, find_dotenv import os +from dotenv import load_dotenv, find_dotenv +import logging.config -env_file = find_dotenv() -load_dotenv(env_file) +from logger_helper.logger_helper import LOGGING_CONFIG -TOKEN_TG_BOT_1 = os.getenv('TOKEN_TELEGRAM_BOT_1') -TOKEN_TG_BOT_2 = os.getenv('TOKEN_TELEGRAM_BOT_2') -TOKEN_TG_BOT_3 = os.getenv('TOKEN_TELEGRAM_BOT_3') \ No newline at end of file +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("config") + +env_path = find_dotenv() + +if env_path: + load_dotenv(env_path) + logging.info(f"Loaded env from {env_path}") +else: + logging.warning(".env file not found, environment variables won't be loaded") + +BOT_TOKEN = os.getenv('BOT_TOKEN') +if not BOT_TOKEN: + logging.error("BOT_TOKEN is not set in environment variables") + +DB_USER = os.getenv('DB_USER') +DB_PASS = os.getenv('DB_PASS') +DB_HOST = os.getenv('DB_HOST') +DB_PORT = os.getenv('DB_PORT') +DB_NAME = os.getenv('DB_NAME') + +if not all([DB_USER, DB_PASS, DB_HOST, DB_PORT, DB_NAME]): + logger.error("One or more database environment variables are not set") + +DATABASE_URL = f"postgresql+asyncpg://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}" \ No newline at end of file diff --git a/database/__init__.py b/database/__init__.py new file mode 100644 index 0000000..7668755 --- /dev/null +++ b/database/__init__.py @@ -0,0 +1,24 @@ +import logging.config + +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine, AsyncSession + +from database.models import Base + +from config import DATABASE_URL +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("database") + +async_engine = create_async_engine(DATABASE_URL, echo=False) + +async_session = async_sessionmaker(async_engine, class_=AsyncSession, expire_on_commit=False) + + +async def init_db(): + try: + async with async_engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + logger.info("Database initialized.") + except Exception as e: + logger.error("Database initialization failed: %s", e) diff --git a/database/models.py b/database/models.py new file mode 100644 index 0000000..af5e5cd --- /dev/null +++ b/database/models.py @@ -0,0 +1,190 @@ +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.ext.asyncio import AsyncAttrs +from sqlalchemy import Column, ForeignKey, Integer, String, BigInteger, Float, Boolean, UniqueConstraint +from sqlalchemy.orm import relationship + +Base = declarative_base(cls=AsyncAttrs) + + +class User(Base): + """User model.""" + __tablename__ = "users" + + id = Column(Integer, primary_key=True, autoincrement=True) + tg_id = Column(BigInteger, nullable=False, unique=True) + username = Column(String, nullable=False) + + user_api = relationship("UserApi", + back_populates="user", + cascade="all, delete-orphan", + passive_deletes=True, + uselist=False) + + user_symbol = relationship("UserSymbol", + back_populates="user", + cascade="all, delete-orphan", + passive_deletes=True, + uselist=False) + + user_additional_settings = relationship("UserAdditionalSettings", + back_populates="user", + cascade="all, delete-orphan", + passive_deletes=True, + uselist=False) + + user_risk_management = relationship("UserRiskManagement", + back_populates="user", + cascade="all, delete-orphan", + passive_deletes=True, + uselist=False) + + user_conditional_settings = relationship("UserConditionalSettings", + back_populates="user", + cascade="all, delete-orphan", + passive_deletes=True, + uselist=False) + + user_deals = relationship("UserDeals", + back_populates="user", + cascade="all, delete-orphan", + passive_deletes=True) + + user_auto_trading = relationship("UserAutoTrading", + back_populates="user", + cascade="all, delete-orphan", + passive_deletes=True) + + +class UserApi(Base): + """User API model.""" + __tablename__ = "user_api" + + id = Column(Integer, primary_key=True, autoincrement=True) + user_id = Column(Integer, + ForeignKey("users.id", ondelete="CASCADE"), + nullable=False, unique=True) + api_key = Column(String, nullable=False) + api_secret = Column(String, nullable=False) + + user = relationship("User", back_populates="user_api") + + +class UserSymbol(Base): + """User symbol model.""" + __tablename__ = "user_symbol" + + id = Column(Integer, primary_key=True, autoincrement=True) + user_id = Column(Integer, + ForeignKey("users.id", ondelete="CASCADE"), + nullable=False, unique=True) + symbol = Column(String, nullable=False, default="BTCUSDT") + + user = relationship("User", back_populates="user_symbol") + + +class UserAdditionalSettings(Base): + """User additional settings model.""" + __tablename__ = "user_additional_settings" + + id = Column(Integer, primary_key=True, autoincrement=True) + user_id = Column(Integer, + ForeignKey("users.id", ondelete="CASCADE"), + nullable=False, unique=True) + trade_mode = Column(String, nullable=False, default="Merged_Single") + order_type = Column(String, nullable=False, default="Market") + conditional_order_type = Column(String, nullable=False, default="Market") + limit_price = Column(Float, nullable=False, default=0.0) + trigger_price = Column(Float, nullable=False, default=0.0) + margin_type = Column(String, nullable=False, default="ISOLATED_MARGIN") + leverage = Column(String, nullable=False, default="10") + leverage_to_buy = Column(String, nullable=False, default="10") + leverage_to_sell = Column(String, nullable=False, default="10") + order_quantity = Column(Float, nullable=False, default=5.0) + martingale_factor = Column(Float, nullable=False, default=1.0) + max_bets_in_series = Column(Integer, nullable=False, default=1) + + user = relationship("User", back_populates="user_additional_settings") + + +class UserRiskManagement(Base): + """User risk management model.""" + __tablename__ = "user_risk_management" + + id = Column(Integer, primary_key=True, autoincrement=True) + user_id = Column(Integer, + ForeignKey("users.id", ondelete="CASCADE"), + nullable=False, unique=True) + take_profit_percent = Column(Integer, nullable=False, default=1) + stop_loss_percent = Column(Integer, nullable=False, default=1) + max_risk_percent = Column(Integer, nullable=False, default=100) + commission_fee = Column(String, nullable=False, default="Yes_commission_fee") + + user = relationship("User", back_populates="user_risk_management") + + +class UserConditionalSettings(Base): + """User conditional settings model.""" + __tablename__ = "user_conditional_settings" + + id = Column(Integer, primary_key=True, autoincrement=True) + user_id = Column(Integer, + ForeignKey("users.id", ondelete="CASCADE"), + nullable=False, unique=True) + + timer_start = Column(Integer, nullable=False, default=0) + timer_end = Column(Integer, nullable=False, default=0) + + user = relationship("User", back_populates="user_conditional_settings") + + +class UserDeals(Base): + """User deals model.""" + __tablename__ = "user_deals" + + id = Column(Integer, primary_key=True, autoincrement=True) + user_id = Column(Integer, + ForeignKey("users.id", ondelete="CASCADE"), + nullable=False) + current_step = Column(Integer, nullable=True) + symbol = Column(String, nullable=True) + trade_mode = Column(String, nullable=True) + trading_type = Column(String, nullable=True) + margin_type = Column(String, nullable=True) + order_type = Column(String, nullable=True) + conditional_order_type = Column(String, nullable=True) + leverage = Column(String, nullable=True) + leverage_to_buy = Column(String, nullable=True) + leverage_to_sell = Column(String, nullable=True) + last_side = Column(String, nullable=True) + closed_side = Column(String, nullable=True) + order_quantity = Column(Float, nullable=True) + martingale_factor = Column(Float, nullable=True) + max_bets_in_series = Column(Integer, nullable=True) + take_profit_percent = Column(Integer, nullable=True) + stop_loss_percent = Column(Integer, nullable=True) + max_risk_percent = Column(Integer, nullable=True) + switch_side_mode = Column(Boolean, nullable=True) + limit_price = Column(Float, nullable=True) + trigger_price = Column(Float, nullable=True) + + user = relationship("User", back_populates="user_deals") + + __table_args__ = ( + UniqueConstraint('user_id', 'symbol', name='uq_user_symbol'), + ) + + +class UserAutoTrading(Base): + """User auto trading model.""" + __tablename__ = "user_auto_trading" + + id = Column(Integer, primary_key=True, autoincrement=True) + user_id = Column(Integer, + ForeignKey("users.id", ondelete="CASCADE"), + nullable=False) + symbol = Column(String, nullable=True) + auto_trading = Column(Boolean, nullable=True) + side = Column(String, nullable=True) + fee = Column(Float, nullable=True) + + user = relationship("User", back_populates="user_auto_trading") \ No newline at end of file diff --git a/database/request.py b/database/request.py new file mode 100644 index 0000000..78c401f --- /dev/null +++ b/database/request.py @@ -0,0 +1,1366 @@ +import logging.config + +from asyncpg.exceptions import UniqueViolationError +from logger_helper.logger_helper import LOGGING_CONFIG +from sqlalchemy import distinct, select +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import joinedload + +from database import async_session +from database.models import ( + User, + UserAdditionalSettings, + UserApi, + UserConditionalSettings, + UserDeals, + UserRiskManagement, + UserSymbol, + UserAutoTrading, +) + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("request") + + +async def create_user(tg_id: int, username: str) -> None: + """Create a new user in the database.""" + try: + existing_user = await get_user(tg_id) + if existing_user: + logger.info("User already exists: %s", tg_id) + return + async with async_session() as session: + user = User(tg_id=tg_id, username=username) + session.add(user) + await session.commit() + logger.info("User created: %s", tg_id) + + except IntegrityError as e: + if isinstance(e.orig, UniqueViolationError): + logger.info("User already exists: %s", tg_id) + else: + logger.error("Error creating user %s: %s", tg_id, e) + + +async def get_users(): + """Get all users from the database.""" + try: + async with async_session() as session: + result = await session.execute(select(User)) + return result.scalars().all() + except Exception as e: + logger.error("Error getting users: %s", e) + return [] + + +async def get_user(tg_id: int): + """Get a user from the database by Telegram ID.""" + try: + async with async_session() as session: + result = await session.execute(select(User).filter_by(tg_id=tg_id)) + return result.scalars().first() + except Exception as e: + logger.error("Error getting user for tg_id %s: %s", tg_id, e) + + +async def set_user_api(tg_id: int, api_key: str, api_secret: str) -> bool: + """ + Set API key and secret for a user in the database. + :param tg_id: Telegram user ID + :param api_key: API key + :param api_secret: API secret + :return: True if successful, False otherwise + """ + try: + async with async_session() as session: + result = await session.execute( + select(User).options(joinedload(User.user_api)).filter_by(tg_id=tg_id) + ) + user = result.scalars().first() + + if user: + if user.user_api: + # Updating existing record + user.user_api.api_key = api_key + user.user_api.api_secret = api_secret + else: + # Creating new record + user_api = UserApi( + user=user, api_key=api_key, api_secret=api_secret + ) + session.add(user_api) + + await session.commit() + logger.info("User API keys updated for user: %s", tg_id) + return True + else: + logger.error("User not found with tg_id: %s", tg_id) + return False + except Exception as e: + logger.error("Error adding/updating user API keys for user %s: %s", tg_id, e) + return False + + +async def get_user_api(tg_id: int): + """Get API key and secret for a user from the database.""" + try: + async with async_session() as session: + result = await session.execute( + select(User).options(joinedload(User.user_api)).filter_by(tg_id=tg_id) + ) + user = result.scalars().first() + if user and user.user_api: + return user.user_api.api_key, user.user_api.api_secret + return None, None + except Exception as e: + logger.error("Error getting user API for user %s: %s", tg_id, e) + return None, None + + +async def set_user_symbol(tg_id: int, symbol: str) -> bool: + """ + Set symbol for a user in the database. + :param tg_id: Telegram user ID + :param symbol: Symbol to set + :return: True if successful, False otherwise + """ + try: + async with async_session() as session: + result = await session.execute( + select(User) + .options(joinedload(User.user_symbol)) + .filter_by(tg_id=tg_id) + ) + user = result.scalars().first() + + if user: + if user.user_symbol: + # Updating existing record + user.user_symbol.symbol = symbol + else: + # Creating new record + user_symbol = UserSymbol( + symbol=symbol, + user=user, + ) + session.add(user_symbol) + + await session.commit() + logger.info("User symbol updated for user: %s", tg_id) + return True + else: + logger.error("User not found with tg_id: %s", tg_id) + return False + except Exception as e: + logger.error("Error adding/updating user symbol for user %s: %s", tg_id, e) + return False + + +async def get_user_symbol(tg_id: int): + """Get symbol for a user from the database.""" + try: + async with async_session() as session: + result = await session.execute( + select(User) + .options(joinedload(User.user_symbol)) + .filter_by(tg_id=tg_id) + ) + user = result.scalars().first() + if user and user.user_symbol: + return user.user_symbol.symbol + return None + except Exception as e: + logger.error("Error getting symbol for user %s: %s", tg_id, e) + return None + + +# USER ADDITIONAL SETTINGS + + +async def create_user_additional_settings(tg_id: int) -> None: + """Create a new user additional settings in the database.""" + try: + existing_user_additional_settings = await get_user_additional_settings(tg_id) + + if existing_user_additional_settings: + logger.info("User additional settings already exists: %s", tg_id) + return + + async with async_session() as session: + # Get the user + result = await session.execute(select(User).filter_by(tg_id=tg_id)) + user = result.scalars().first() + + if not user: + logger.error("User not found with tg_id: %s", tg_id) + return + + # Create the user additional settings + user_additional_settings = UserAdditionalSettings( + user=user, + trade_mode="Merged_Single", # Default value + order_type="Market", + conditional_order_type="Market", + margin_type="ISOLATED_MARGIN", + leverage="10", + leverage_to_buy="10", + leverage_to_sell="10", + order_quantity=5.0, + martingale_factor=1.0, + max_bets_in_series=10, + ) + session.add(user_additional_settings) + await session.commit() + logger.info("User additional settings created: %s", tg_id) + + except IntegrityError as e: + if isinstance(e.orig, UniqueViolationError): + logger.info("User additional settings already exists: %s", tg_id) + else: + logger.error( + "Error creating user additional settings for user %s: %s", tg_id, e + ) + except Exception as e: + logger.error( + "General error creating user additional settings for user %s: %s", tg_id, e + ) + + +async def get_user_additional_settings(tg_id: int): + """Get user additional settings from the database.""" + try: + async with async_session() as session: + result = await session.execute( + select(User) + .options(joinedload(User.user_additional_settings)) + .filter_by(tg_id=tg_id) + ) + user = result.scalars().first() + if user and user.user_additional_settings: + return user.user_additional_settings + return None + except Exception as e: + logger.error("Error getting user additional settings for user %s: %s", tg_id, e) + return None + + +async def set_trade_mode(tg_id: int, trade_mode: str) -> bool: + """ + Set trade mode for a user in the database. + :param tg_id: Telegram user ID + :param trade_mode: "Both_Sides" or "Merged_Single" + :return: True if successful, False otherwise + """ + try: + async with async_session() as session: + result = await session.execute( + select(User) + .options(joinedload(User.user_additional_settings)) + .filter_by(tg_id=tg_id) + ) + user = result.scalars().first() + + if user: + if user.user_additional_settings: + # Updating existing record + user.user_additional_settings.trade_mode = trade_mode + else: + # Creating new record + user_additional_settings = UserAdditionalSettings( + trade_mode=trade_mode, + user=user, + ) + session.add(user_additional_settings) + + await session.commit() + logger.info("User trade mode updated for user: %s", tg_id) + return True + else: + logger.error("User not found with tg_id: %s", tg_id) + return False + except Exception as e: + logger.error("Error adding/updating user trade mode for user %s: %s", tg_id, e) + return False + + +async def set_order_type(tg_id: int, order_type: str) -> bool: + """ + Set order type for a user in the database. + :param tg_id: Telegram user ID + :param order_type: "Market" or "Limit" + :return: True if successful, False otherwise + """ + try: + async with async_session() as session: + result = await session.execute( + select(User) + .options(joinedload(User.user_additional_settings)) + .filter_by(tg_id=tg_id) + ) + user = result.scalars().first() + + if user: + if user.user_additional_settings: + # Updating existing record + user.user_additional_settings.order_type = order_type + else: + # Creating new record + user_additional_settings = UserAdditionalSettings( + order_type=order_type, + user=user, + ) + session.add(user_additional_settings) + + await session.commit() + logger.info("User order type updated for user: %s", tg_id) + return True + else: + logger.error("User not found with tg_id: %s", tg_id) + return False + except Exception as e: + logger.error("Error adding/updating user order type for user %s: %s", tg_id, e) + return False + + +async def set_conditional_order_type(tg_id: int, conditional_order_type: str) -> bool: + """ + Set conditional order type for a user in the database. + :param tg_id: Telegram user ID + :param conditional_order_type: "Market" or "Limit" + :return: True if successful, False otherwise + """ + try: + async with async_session() as session: + result = await session.execute( + select(User) + .options(joinedload(User.user_additional_settings)) + .filter_by(tg_id=tg_id) + ) + user = result.scalars().first() + + if user: + if user.user_additional_settings: + # Updating existing record + user.user_additional_settings.conditional_order_type = ( + conditional_order_type + ) + else: + # Creating new record + user_additional_settings = UserAdditionalSettings( + conditional_order_type=conditional_order_type, + user=user, + ) + session.add(user_additional_settings) + + await session.commit() + logger.info("User conditional order type updated for user: %s", tg_id) + return True + else: + logger.error("User not found with tg_id: %s", tg_id) + return False + except Exception as e: + logger.error( + "Error adding/updating user conditional order type for user %s: %s", + tg_id, + e, + ) + return False + + +async def set_margin_type(tg_id: int, margin_type: str) -> bool: + """ + Set margin type for a user in the database. + :param tg_id: Telegram user ID + :param margin_type: "ISOLATED_MARGIN" or "REGULAR_MARGIN" + :return: True if successful, False otherwise + """ + try: + async with async_session() as session: + result = await session.execute( + select(User) + .options(joinedload(User.user_additional_settings)) + .filter_by(tg_id=tg_id) + ) + user = result.scalars().first() + + if user: + if user.user_additional_settings: + # Updating existing record + user.user_additional_settings.margin_type = margin_type + else: + # Creating new record + user_additional_settings = UserAdditionalSettings( + margin_type=margin_type, + user=user, + ) + session.add(user_additional_settings) + + await session.commit() + logger.info("User margin type updated for user: %s", tg_id) + return True + else: + logger.error("User not found with tg_id: %s", tg_id) + return False + except Exception as e: + logger.error("Error adding/updating user margin type for user %s: %s", tg_id, e) + return False + + +async def set_leverage(tg_id: int, leverage: str) -> bool: + """ + Set leverage for a user in the database. + :param tg_id: Telegram user ID + :param leverage: Leverage + :return: True if successful, False otherwise + """ + try: + async with async_session() as session: + result = await session.execute( + select(User) + .options(joinedload(User.user_additional_settings)) + .filter_by(tg_id=tg_id) + ) + user = result.scalars().first() + + if user: + if user.user_additional_settings: + # Updating existing record + user.user_additional_settings.leverage = leverage + else: + # Creating new record + user_additional_settings = UserAdditionalSettings( + leverage=leverage, + user=user, + ) + session.add(user_additional_settings) + + await session.commit() + logger.info("User leverage updated for user: %s", tg_id) + return True + else: + logger.error("User not found with tg_id: %s", tg_id) + return False + except Exception as e: + logger.error("Error adding/updating user leverage for user %s: %s", tg_id, e) + return False + + +async def set_leverage_to_buy_and_sell( + tg_id: int, leverage_to_buy: str, leverage_to_sell: str +) -> bool: + """ + Set leverage for a user in the database. + :param tg_id: Telegram user ID + :param leverage_to_buy: Leverage to buy + :param leverage_to_sell: Leverage to sell + :return: True if successful, False otherwise + """ + try: + async with async_session() as session: + result = await session.execute( + select(User) + .options(joinedload(User.user_additional_settings)) + .filter_by(tg_id=tg_id) + ) + user = result.scalars().first() + + if user: + if user.user_additional_settings: + # Updating existing record + user.user_additional_settings.leverage_to_buy = leverage_to_buy + user.user_additional_settings.leverage_to_sell = leverage_to_sell + else: + # Creating new record + user_additional_settings = UserAdditionalSettings( + leverage_to_buy=leverage_to_buy, + leverage_to_sell=leverage_to_sell, + user=user, + ) + session.add(user_additional_settings) + + await session.commit() + logger.info("User leverage updated for user: %s", tg_id) + return True + else: + logger.error("User not found with tg_id: %s", tg_id) + return False + except Exception as e: + logger.error("Error adding/updating user leverage for user %s: %s", tg_id, e) + return False + + +async def set_order_quantity(tg_id: int, order_quantity: float) -> bool: + """ + Set order quantity for a user in the database. + :param tg_id: Telegram user ID + :param order_quantity: Order quantity + :return: True if successful, False otherwise + """ + try: + async with async_session() as session: + result = await session.execute( + select(User) + .options(joinedload(User.user_additional_settings)) + .filter_by(tg_id=tg_id) + ) + user = result.scalars().first() + + if user: + if user.user_additional_settings: + # Updating existing record + user.user_additional_settings.order_quantity = order_quantity + else: + # Creating new record + user_additional_settings = UserAdditionalSettings( + order_quantity=order_quantity, + user=user, + ) + session.add(user_additional_settings) + + await session.commit() + logger.info("User order quantity updated for user: %s", tg_id) + return True + else: + logger.error("User not found with tg_id: %s", tg_id) + return False + except Exception as e: + logger.error( + "Error adding/updating user order quantity for user %s: %s", tg_id, e + ) + return False + + +async def set_martingale_factor(tg_id: int, martingale_factor: float) -> bool: + """ + Set martingale factor for a user in the database. + :param tg_id: Telegram user ID + :param martingale_factor: Martingale factor + :return: True if successful, False otherwise + """ + try: + async with async_session() as session: + result = await session.execute( + select(User) + .options(joinedload(User.user_additional_settings)) + .filter_by(tg_id=tg_id) + ) + user = result.scalars().first() + + if user: + if user.user_additional_settings: + # Updating existing record + user.user_additional_settings.martingale_factor = martingale_factor + else: + # Creating new record + user_additional_settings = UserAdditionalSettings( + martingale_factor=martingale_factor, + user=user, + ) + session.add(user_additional_settings) + + await session.commit() + logger.info("User martingale factor updated for user: %s", tg_id) + return True + else: + logger.error("User not found with tg_id: %s", tg_id) + return False + except Exception as e: + logger.error( + "Error adding/updating user martingale factor for user %s: %s", tg_id, e + ) + return False + + +async def set_max_bets_in_series(tg_id: int, max_bets_in_series: int) -> bool: + """ + Set max steps for a user in the database. + :param tg_id: Telegram user ID + :param max_bets_in_series: Max steps + :return: True if successful, False otherwise + """ + try: + async with async_session() as session: + result = await session.execute( + select(User) + .options(joinedload(User.user_additional_settings)) + .filter_by(tg_id=tg_id) + ) + user = result.scalars().first() + + if user: + if user.user_additional_settings: + # Updating existing record + user.user_additional_settings.max_bets_in_series = ( + max_bets_in_series + ) + else: + # Creating new record + user_additional_settings = UserAdditionalSettings( + max_bets_in_series=max_bets_in_series, + user=user, + ) + session.add(user_additional_settings) + + await session.commit() + logger.info("User max bets in series updated for user: %s", tg_id) + return True + else: + logger.error("User not found with tg_id: %s", tg_id) + return False + except Exception as e: + logger.error( + "Error adding/updating user max bets in series for user %s: %s", tg_id, e + ) + return False + + +async def set_limit_price(tg_id: int, limit_price: float) -> bool: + """ + Set limit price for a user in the database. + :param tg_id: + :param limit_price: + :return: bool + """ + try: + async with async_session() as session: + result = await session.execute( + select(User) + .options(joinedload(User.user_additional_settings)) + .filter_by(tg_id=tg_id) + ) + user = result.scalars().first() + + if user: + if user.user_additional_settings: + # Updating existing record + user.user_additional_settings.limit_price = limit_price + else: + # Creating new record + user_additional_settings = UserAdditionalSettings( + limit_price=limit_price, + user=user, + ) + session.add(user_additional_settings) + + await session.commit() + logger.info("User limit price updated for user: %s", tg_id) + return True + else: + logger.error("User not found with tg_id: %s", tg_id) + return False + except Exception as e: + logger.error("Error adding/updating user limit price for user %s: %s", tg_id, e) + return False + + +async def set_trigger_price(tg_id: int, trigger_price: float) -> bool: + """ + Set trigger price for a user in the database. + :param tg_id: + :param trigger_price: + :return: bool + """ + try: + async with async_session() as session: + result = await session.execute( + select(User) + .options(joinedload(User.user_additional_settings)) + .filter_by(tg_id=tg_id) + ) + user = result.scalars().first() + + if user: + if user.user_additional_settings: + # Updating existing record + user.user_additional_settings.trigger_price = trigger_price + else: + # Creating new record + user_additional_settings = UserAdditionalSettings( + trigger_price=trigger_price, + user=user, + ) + session.add(user_additional_settings) + + await session.commit() + logger.info("User trigger price updated for user: %s", tg_id) + return True + else: + logger.error("User not found with tg_id: %s", tg_id) + return False + except Exception as e: + logger.error( + "Error adding/updating user trigger price for user %s: %s", tg_id, e + ) + return False + + +# USER RISK MANAGEMENT + + +async def create_user_risk_management(tg_id: int) -> None: + """Create a new user risk management in the database.""" + try: + existing_user_risk_management = await get_user_risk_management(tg_id) + + if existing_user_risk_management: + logger.info("User risk management already exists: %s", tg_id) + return + + async with async_session() as session: + # Get the user + result = await session.execute(select(User).filter_by(tg_id=tg_id)) + user = result.scalars().first() + + if not user: + logger.error("User not found with tg_id: %s", tg_id) + return + + # Create the user risk management + user_risk_management = UserRiskManagement( + user=user, + take_profit_percent=1, + stop_loss_percent=1, + max_risk_percent=100, + commission_fee="Yes_commission_fee", + ) + session.add(user_risk_management) + await session.commit() + logger.info("User risk management created: %s", tg_id) + + except IntegrityError as e: + if isinstance(e.orig, UniqueViolationError): + logger.info("User risk management already exists: %s", tg_id) + else: + logger.error( + "Error creating user risk management for user %s: %s", tg_id, e + ) + except Exception as e: + logger.error( + "General error creating user risk management for user %s: %s", tg_id, e + ) + + +async def get_user_risk_management(tg_id: int): + """Get user risk management from the database.""" + try: + async with async_session() as session: + result = await session.execute( + select(User) + .options(joinedload(User.user_risk_management)) + .filter_by(tg_id=tg_id) + ) + user = result.scalars().first() + if user and user.user_risk_management: + return user.user_risk_management + return None + except Exception as e: + logger.error("Error getting user risk management for user %s: %s", tg_id, e) + return None + + +async def set_take_profit_percent(tg_id: int, take_profit_percent: int) -> bool: + """ + Set take profit percent for a user in the database. + :param tg_id: Telegram user ID + :param take_profit_percent: Take profit percent + :return: True if successful, False otherwise + """ + try: + async with async_session() as session: + result = await session.execute( + select(User) + .options(joinedload(User.user_risk_management)) + .filter_by(tg_id=tg_id) + ) + user = result.scalars().first() + + if user: + if user.user_risk_management: + # Updating existing record + user.user_risk_management.take_profit_percent = take_profit_percent + else: + # Creating new record + user_risk_management = UserRiskManagement( + take_profit_percent=take_profit_percent, + user=user, + ) + session.add(user_risk_management) + + await session.commit() + logger.info("User take profit percent updated for user: %s", tg_id) + return True + else: + logger.error("User not found with tg_id: %s", tg_id) + return False + except Exception as e: + logger.error( + "Error adding/updating user take profit percent for user %s: %s", tg_id, e + ) + return False + + +async def set_stop_loss_percent(tg_id: int, stop_loss_percent: int) -> bool: + """ + Set stop loss percent for a user in the database. + :param tg_id: Telegram user ID + :param stop_loss_percent: Stop loss percent + :return: True if successful, False otherwise + """ + try: + async with async_session() as session: + result = await session.execute( + select(User) + .options(joinedload(User.user_risk_management)) + .filter_by(tg_id=tg_id) + ) + user = result.scalars().first() + + if user: + if user.user_risk_management: + # Updating existing record + user.user_risk_management.stop_loss_percent = stop_loss_percent + else: + # Creating new record + user_risk_management = UserRiskManagement( + stop_loss_percent=stop_loss_percent, + user=user, + ) + session.add(user_risk_management) + + await session.commit() + logger.info("User stop loss percent updated for user: %s", tg_id) + return True + else: + logger.error("User not found with tg_id: %s", tg_id) + return False + except Exception as e: + logger.error( + "Error adding/updating user stop loss percent for user %s: %s", tg_id, e + ) + return False + + +async def set_max_risk_percent(tg_id: int, max_risk_percent: int) -> bool: + """ + Set max risk percent for a user in the database. + :param tg_id: Telegram user ID + :param max_risk_percent: Max risk percent + :return: True if successful, False otherwise + """ + try: + async with async_session() as session: + result = await session.execute( + select(User) + .options(joinedload(User.user_risk_management)) + .filter_by(tg_id=tg_id) + ) + user = result.scalars().first() + + if user: + if user.user_risk_management: + # Updating existing record + user.user_risk_management.max_risk_percent = max_risk_percent + else: + # Creating new record + user_risk_management = UserRiskManagement( + max_risk_percent=max_risk_percent, + user=user, + ) + session.add(user_risk_management) + + await session.commit() + logger.info("User max risk percent updated for user: %s", tg_id) + return True + else: + logger.error("User not found with tg_id: %s", tg_id) + return False + except Exception as e: + logger.error( + "Error adding/updating user max risk percent for user %s: %s", tg_id, e + ) + return False + + +async def set_commission_fee(tg_id: int, commission_fee: str) -> bool: + """ + Set commission fee for a user in the database. + :param tg_id: Telegram user ID + :param commission_fee: Commission fee + :return: True if successful, False otherwise + """ + try: + async with async_session() as session: + result = await session.execute( + select(User) + .options(joinedload(User.user_risk_management)) + .filter_by(tg_id=tg_id) + ) + user = result.scalars().first() + + if user: + if user.user_risk_management: + # Updating existing record + user.user_risk_management.commission_fee = commission_fee + else: + # Creating new record + user_risk_management = UserRiskManagement( + commission_fee=commission_fee, + user=user, + ) + session.add(user_risk_management) + + await session.commit() + logger.info("User commission fee updated for user: %s", tg_id) + return True + else: + logger.error("User not found with tg_id: %s", tg_id) + return False + except Exception as e: + logger.error( + "Error adding/updating user commission fee for user %s: %s", tg_id, e + ) + return False + + +# USER CONDITIONAL SETTINGS + + +async def create_user_conditional_settings(tg_id: int) -> None: + """Create a new user conditional settings in the database.""" + try: + existing_user_conditional_settings = await get_user_conditional_settings(tg_id) + + if existing_user_conditional_settings: + logger.info("User conditional settings already exists: %s", tg_id) + return + + async with async_session() as session: + # Get the user + result = await session.execute(select(User).filter_by(tg_id=tg_id)) + user = result.scalars().first() + + if not user: + logger.error("User not found with tg_id: %s", tg_id) + return + + # Create the user conditional settings + user_conditional_settings = UserConditionalSettings( + user=user, + timer_start=0, + timer_end=0, + ) + session.add(user_conditional_settings) + await session.commit() + logger.info("User conditional settings created: %s", tg_id) + + except IntegrityError as e: + if isinstance(e.orig, UniqueViolationError): + logger.info("User conditional settings already exists: %s", tg_id) + else: + logger.error( + "Error creating user conditional settings for user %s: %s", tg_id, e + ) + except Exception as e: + logger.error( + "General error creating user conditional settings for user %s: %s", tg_id, e + ) + + +async def get_user_conditional_settings(tg_id: int): + """Get user conditional settings from the database.""" + try: + async with async_session() as session: + result = await session.execute( + select(User) + .options(joinedload(User.user_conditional_settings)) + .filter_by(tg_id=tg_id) + ) + user = result.scalars().first() + if user and user.user_conditional_settings: + return user.user_conditional_settings + return None + except Exception as e: + logger.error( + "Error getting user conditional settings for user %s: %s", tg_id, e + ) + return None + + +async def set_start_timer(tg_id: int, timer_start: int) -> bool: + """ + Set the start timer for a user in the database. + :param tg_id: Telegram user ID + :param timer_start: Start timer + :return: bool + """ + try: + async with async_session() as session: + result = await session.execute( + select(User) + .options(joinedload(User.user_conditional_settings)) + .filter_by(tg_id=tg_id) + ) + user = result.scalars().first() + + if user: + if user.user_conditional_settings: + # Updating existing record + user.user_conditional_settings.timer_start = timer_start + else: + # Creating new record + user_conditional_settings = UserConditionalSettings( + timer_start=timer_start, + user=user, + ) + session.add(user_conditional_settings) + + await session.commit() + logger.info("User start timer updated for user: %s", tg_id) + return True + else: + logger.error("User not found with tg_id: %s", tg_id) + return False + except Exception as e: + logger.error("Error adding/updating user start timer for user %s: %s", tg_id, e) + return False + + +async def set_stop_timer(tg_id: int, timer_end: int) -> bool: + """ + Set the stop timer for a user in the database. + :param tg_id: Telegram user ID + :param timer_end: Stop timer + :return: bool + """ + try: + async with async_session() as session: + result = await session.execute( + select(User) + .options(joinedload(User.user_conditional_settings)) + .filter_by(tg_id=tg_id) + ) + user = result.scalars().first() + + if user: + if user.user_conditional_settings: + # Updating existing record + user.user_conditional_settings.timer_end = timer_end + else: + # Creating new record + user_conditional_settings = UserConditionalSettings( + timer_end=timer_end, + user=user, + ) + session.add(user_conditional_settings) + + await session.commit() + logger.info("User stop timer updated for user: %s", tg_id) + return True + else: + logger.error("User not found with tg_id: %s", tg_id) + return False + except Exception as e: + logger.error("Error adding/updating user stop timer for user %s: %s", tg_id, e) + return False + + +# USER DEALS +async def set_user_deal( + tg_id: int, + symbol: str, + last_side: str, + current_step: int, + trade_mode: str, + margin_type: str, + leverage: str, + leverage_to_buy: str, + leverage_to_sell: str, + order_type: str, + conditional_order_type: str, + order_quantity: float, + limit_price: float, + trigger_price: float, + martingale_factor: float, + max_bets_in_series: int, + take_profit_percent: int, + stop_loss_percent: int, + max_risk_percent: int, + switch_side_mode: bool, +): + """ + Set the user deal in the database. + :param tg_id: Telegram user ID + :param symbol: Symbol + :param last_side: Last side + :param current_step: Current step + :param trade_mode: Trade mode + :param margin_type: Margin type + :param leverage: Leverage + :param leverage_to_buy: Leverage to buy + :param leverage_to_sell: Leverage to sell + :param order_type: Order type + :param conditional_order_type: Conditional order type + :param order_quantity: Order quantity + :param limit_price: Limit price + :param trigger_price: Trigger price + :param martingale_factor: Martingale factor + :param max_bets_in_series: Max bets in series + :param take_profit_percent: Take profit percent + :param stop_loss_percent: Stop loss percent + :param max_risk_percent: Max risk percent + :param switch_side_mode: Switch side mode + :return: bool + """ + try: + async with async_session() as session: + result = await session.execute(select(User).filter_by(tg_id=tg_id)) + user = result.scalars().first() + if not user: + logger.error("User not found with tg_id: %s", tg_id) + return False + + result_deal = await session.execute( + select(UserDeals).filter_by(user_id=user.id, symbol=symbol) + ) + deal = result_deal.scalars().first() + + if deal: + # Updating existing record + deal.last_side = last_side + deal.current_step = current_step + deal.trade_mode = trade_mode + deal.margin_type = margin_type + deal.leverage = leverage + deal.leverage_to_buy = leverage_to_buy + deal.leverage_to_sell = leverage_to_sell + deal.order_type = order_type + deal.conditional_order_type = conditional_order_type + deal.order_quantity = order_quantity + deal.limit_price = limit_price + deal.trigger_price = trigger_price + deal.martingale_factor = martingale_factor + deal.max_bets_in_series = max_bets_in_series + deal.take_profit_percent = take_profit_percent + deal.stop_loss_percent = stop_loss_percent + deal.max_risk_percent = max_risk_percent + deal.switch_side_mode = switch_side_mode + else: + # Creating new record + new_deal = UserDeals( + user=user, + symbol=symbol, + last_side=last_side, + current_step=current_step, + trade_mode=trade_mode, + margin_type=margin_type, + leverage=leverage, + leverage_to_buy=leverage_to_buy, + leverage_to_sell=leverage_to_sell, + order_type=order_type, + conditional_order_type=conditional_order_type, + order_quantity=order_quantity, + limit_price=limit_price, + trigger_price=trigger_price, + martingale_factor=martingale_factor, + max_bets_in_series=max_bets_in_series, + take_profit_percent=take_profit_percent, + stop_loss_percent=stop_loss_percent, + max_risk_percent=max_risk_percent, + switch_side_mode=switch_side_mode, + ) + session.add(new_deal) + + await session.commit() + logger.info("User deals set for user %s and symbol %s", tg_id, symbol) + return True + + except Exception as e: + logger.error("Error setting user deal for user %s and symbol %s: %s", tg_id, symbol, e) + return False + + +async def get_user_deal_by_symbol(tg_id: int, symbol: str): + """Get user deal by symbol from the database asynchronously.""" + try: + async with async_session() as session: + + result_user = await session.execute(select(User).filter_by(tg_id=tg_id)) + user = result_user.scalars().first() + if not user: + return None + + result_deal = await session.execute( + select(UserDeals).filter_by(user_id=user.id, symbol=symbol) + ) + deal = result_deal.scalars().first() + return deal + except Exception as e: + logger.error("Error getting deal for user %s and symbol %s: %s", tg_id, symbol, e) + return None + + +async def get_all_symbols(tg_id: int): + """Get all symbols from the database asynchronously.""" + try: + async with async_session() as session: + + result_user = await session.execute(select(User).filter_by(tg_id=tg_id)) + user = result_user.scalars().first() + if not user: + return [] + + result_symbols = await session.execute( + select(distinct(UserDeals.symbol)).filter_by(user_id=user.id) + ) + symbols = [row[0] for row in result_symbols.all() if row[0] is not None] + return symbols + except Exception as e: + logger.error("Error getting symbols for user %s: %s", tg_id, e) + return [] + + +async def set_fee_user_deal_by_symbol(tg_id: int, symbol: str, fee: float): + """Set fee for a user deal by symbol in the database.""" + try: + async with async_session() as session: + result = await session.execute(select(User).filter_by(tg_id=tg_id)) + user = result.scalars().first() + if user is None: + logger.error(f"User with tg_id={tg_id} not found") + return False + + result = await session.execute( + select(UserDeals).filter_by(user_id=user.id, symbol=symbol) + ) + record = result.scalars().first() + + if record: + record.fee = fee + else: + logger.error(f"User deal with user_id={user.id} and symbol={symbol} not found") + return False + await session.commit() + logger.info("Set fee for user %s and symbol %s", tg_id, symbol) + return True + except Exception as e: + logger.error("Error setting user deal fee for user %s and symbol %s: %s", tg_id, symbol, e) + return False + + +# USER AUTO TRADING + +async def get_all_user_auto_trading(tg_id: int): + """Get all user auto trading from the database asynchronously.""" + try: + async with async_session() as session: + result_user = await session.execute(select(User).filter_by(tg_id=tg_id)) + user = result_user.scalars().first() + if not user: + return [] + + result_auto_trading = await session.execute( + select(UserAutoTrading).filter_by(user_id=user.id) + ) + auto_trading = result_auto_trading.scalars().all() + return auto_trading + except Exception as e: + logger.error("Error getting auto trading for user %s: %s", tg_id, e) + return [] + + +async def get_user_auto_trading(tg_id: int, symbol: str, side: str): + """Get user auto trading from the database asynchronously.""" + try: + async with async_session() as session: + result_user = await session.execute(select(User).filter_by(tg_id=tg_id)) + user = result_user.scalars().first() + if not user: + return None + + result_auto_trading = await session.execute( + select(UserAutoTrading).filter_by(user_id=user.id, symbol=symbol, side=side) + ) + auto_trading = result_auto_trading.scalars().first() + return auto_trading + except Exception as e: + logger.error("Error getting auto trading for user %s and symbol %s: %s", tg_id, symbol, e) + return None + + +async def set_auto_trading(tg_id: int, symbol: str, auto_trading: bool, side: str) -> bool: + """ + Set the auto trading for a user in the database. + :param tg_id: Telegram user ID + :param symbol: Symbol + :param auto_trading: Auto trading + :param side: Side + :return: bool + """ + try: + async with async_session() as session: + result = await session.execute(select(User).filter_by(tg_id=tg_id)) + user = result.scalars().first() + if user is None: + logger.error(f"User with tg_id={tg_id} not found") + return False + + result = await session.execute( + select(UserAutoTrading).filter_by(user_id=user.id, symbol=symbol, side=side) + ) + record = result.scalars().first() + if record: + record.auto_trading = auto_trading + else: + new_record = UserAutoTrading( + user_id=user.id, + symbol=symbol, + auto_trading=auto_trading, + side=side + ) + session.add(new_record) + await session.commit() + logger.info("Set auto_trading=%s for user %s and symbol %s", auto_trading, tg_id, symbol) + return True + except Exception as e: + logger.error("Error setting auto_trading for user %s and symbol %s: %s", tg_id, symbol, e) + return False + + +async def set_fee_user_auto_trading(tg_id: int, symbol: str, side: str, fee: float) -> bool: + """ + Set the fee for a user auto trading in the database. + :param tg_id: + :param symbol: + :param side: + :param fee: + :return: + """ + try: + async with async_session() as session: + result = await session.execute(select(User).filter_by(tg_id=tg_id)) + user = result.scalars().first() + if user is None: + logger.error(f"User with tg_id={tg_id} not found") + return False + + result = await session.execute( + select(UserAutoTrading).filter_by(user_id=user.id, symbol=symbol, side=side) + ) + record = result.scalars().first() + + if record: + record.fee = fee + else: + user_fee = UserAutoTrading( + user_id=user.id, + symbol=symbol, + fee=fee, + side=side + ) + session.add(user_fee) + await session.commit() + logger.info("Set fee for user %s and symbol %s", tg_id, symbol) + return True + except Exception as e: + logger.error("Error setting user auto trading fee for user %s and symbol %s: %s", tg_id, symbol, e) + return False diff --git a/logger_helper/__init__.py b/logger_helper/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/logger_helper/logger_helper.py b/logger_helper/logger_helper.py index 7f17351..d47e11f 100644 --- a/logger_helper/logger_helper.py +++ b/logger_helper/logger_helper.py @@ -2,15 +2,18 @@ import os current_directory = os.path.dirname(os.path.abspath(__file__)) log_directory = os.path.join(current_directory, 'loggers') +error_log_directory = os.path.join(log_directory, 'errors') os.makedirs(log_directory, exist_ok=True) +os.makedirs(error_log_directory, exist_ok=True) log_filename = os.path.join(log_directory, 'app.log') +error_log_filename = os.path.join(error_log_directory, 'error.log') LOGGING_CONFIG = { "version": 1, "disable_existing_loggers": False, "formatters": { "default": { - "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + "format": "TELEGRAM: %(asctime)s - %(name)s - %(levelname)s - %(message)s", "datefmt": "%Y-%m-%d %H:%M:%S", # Формат даты }, }, @@ -23,90 +26,122 @@ LOGGING_CONFIG = { "backupCount": 7, # Количество сохраняемых архивов (0 - не сохранять) "formatter": "default", "encoding": "utf-8", + "level": "DEBUG", + }, + "error_file": { + "class": "logging.handlers.TimedRotatingFileHandler", + "filename": error_log_filename, + "when": "midnight", + "interval": 1, + "backupCount": 30, + "formatter": "default", + "encoding": "utf-8", + "level": "ERROR", }, "console": { "class": "logging.StreamHandler", "formatter": "default", + "level": "DEBUG", }, }, "loggers": { - "main": { - "handlers": ["console", "timed_rotating_file"], + "run": { + "handlers": ["console", "timed_rotating_file", "error_file"], + "level": "DEBUG", + "propagate": False, + }, + "config": { + "handlers": ["console", "timed_rotating_file", "error_file"], + "level": "DEBUG", + "propagate": False, + }, + "common": { + "handlers": ["console", "timed_rotating_file", "error_file"], + "level": "DEBUG", + "propagate": False, + }, + "handlers_main": { + "handlers": ["console", "timed_rotating_file", "error_file"], + "level": "DEBUG", + "propagate": False, + }, + "database": { + "handlers": ["console", "timed_rotating_file", "error_file"], + "level": "DEBUG", + "propagate": False, + }, + "request": { + "handlers": ["console", "timed_rotating_file", "error_file"], "level": "DEBUG", "propagate": False, }, "add_bybit_api": { - "handlers": ["console", "timed_rotating_file"], + "handlers": ["console", "timed_rotating_file", "error_file"], "level": "DEBUG", "propagate": False, }, - "balance": { - "handlers": ["console", "timed_rotating_file"], + "profile_tg": { + "handlers": ["console", "timed_rotating_file", "error_file"], "level": "DEBUG", "propagate": False, }, - "functions": { - "handlers": ["console", "timed_rotating_file"], + "settings": { + "handlers": ["console", "timed_rotating_file", "error_file"], "level": "DEBUG", "propagate": False, }, - "futures": { - "handlers": ["console", "timed_rotating_file"], + "additional_settings": { + "handlers": ["console", "timed_rotating_file", "error_file"], "level": "DEBUG", "propagate": False, }, - "get_valid_symbol": { - "handlers": ["console", "timed_rotating_file"], + "helper_functions": { + "handlers": ["console", "timed_rotating_file", "error_file"], "level": "DEBUG", "propagate": False, }, - "min_qty": { - "handlers": ["console", "timed_rotating_file"], + "risk_management": { + "handlers": ["console", "timed_rotating_file", "error_file"], "level": "DEBUG", "propagate": False, }, - "price_symbol": { - "handlers": ["console", "timed_rotating_file"], + "start_trading": { + "handlers": ["console", "timed_rotating_file", "error_file"], "level": "DEBUG", "propagate": False, }, - "requests": { - "handlers": ["console", "timed_rotating_file"], + "stop_trading": { + "handlers": ["console", "timed_rotating_file", "error_file"], "level": "DEBUG", "propagate": False, }, - "handlers": { - "handlers": ["console", "timed_rotating_file"], + "changing_the_symbol": { + "handlers": ["console", "timed_rotating_file", "error_file"], "level": "DEBUG", "propagate": False, }, - "condition_settings": { - "handlers": ["console", "timed_rotating_file"], + "conditional_settings": { + "handlers": ["console", "timed_rotating_file", "error_file"], "level": "DEBUG", "propagate": False, }, - "main_settings": { - "handlers": ["console", "timed_rotating_file"], + "get_positions_handlers": { + "handlers": ["console", "timed_rotating_file", "error_file"], "level": "DEBUG", "propagate": False, }, - "risk_management_settings": { - "handlers": ["console", "timed_rotating_file"], + "close_orders": { + "handlers": ["console", "timed_rotating_file", "error_file"], "level": "DEBUG", "propagate": False, }, - "models": { - "handlers": ["console", "timed_rotating_file"], - "level": "DEBUG", - "propagate": False, - }, - "bybit_ws": { - "handlers": ["console", "timed_rotating_file"], + "tp_sl_handlers": { + "handlers": ["console", "timed_rotating_file", "error_file"], "level": "DEBUG", "propagate": False, }, "tasks": { - "handlers": ["console", "timed_rotating_file"], + "handlers": ["console", "timed_rotating_file", "error_file"], "level": "DEBUG", "propagate": False, }, diff --git a/requirements.txt b/requirements.txt index e3ab834..70e685d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ aiosignal==1.4.0 aiosqlite==0.21.0 alembic==1.16.5 annotated-types==0.7.0 +asyncpg==0.30.0 attrs==25.3.0 black==25.1.0 certifi==2025.8.3 @@ -32,10 +33,12 @@ packaging==25.0 pathspec==0.12.1 platformdirs==4.4.0 propcache==0.3.2 +psycopg==3.2.10 +psycopg-binary==3.2.10 pybit==5.11.0 pycodestyle==2.14.0 pycryptodome==3.23.0 -pydantic==2.11.7 +pydantic==2.11.9 pydantic_core==2.33.2 pyflakes==3.4.0 python-dotenv==1.1.1 @@ -45,7 +48,8 @@ requests==2.32.5 six==1.17.0 SQLAlchemy==2.0.43 typing-inspection==0.4.1 -typing_extensions==4.14.1 +typing_extensions==4.15.0 +uliweb-alembic==0.6.9 urllib3==2.5.0 websocket-client==1.8.0 yarl==1.20.1 diff --git a/run.py b/run.py new file mode 100644 index 0000000..8d92a6d --- /dev/null +++ b/run.py @@ -0,0 +1,56 @@ +import asyncio +import contextlib +import logging.config + +from aiogram import Bot, Dispatcher +from aiogram.fsm.storage.redis import RedisStorage + +from app.bybit.web_socket import WebSocketBot +from app.telegram.handlers import router +from config import BOT_TOKEN +from database import init_db +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("run") + + +async def main(): + """ + The main function of launching the bot. + + Performs database initialization, creation of bot and dispatcher objects, + then it triggers the long polling event. + + Logs important events and errors. + """ + try: + await init_db() + bot = Bot(token=BOT_TOKEN) + storage = RedisStorage.from_url("redis://localhost:6379") + dp = Dispatcher(storage=storage) + dp.include_router(router) + web_socket = WebSocketBot(telegram_bot=bot) + await web_socket.clear_user_sockets() + ws_task = asyncio.create_task(web_socket.run_user_check_loop()) + tg_task = asyncio.create_task(dp.start_polling(bot)) + + try: + logger.info("Bot started") + await asyncio.gather(ws_task, tg_task) + except Exception as e: + logger.error("Bot stopped with error: %s", e) + finally: + for task in (ws_task, tg_task): + task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await ws_task + await tg_task + await web_socket.clear_user_sockets() + + except Exception as e: + logger.error("Bot stopped with error: %s", e) + + +if __name__ == "__main__": + asyncio.run(main())