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