forked from kodorvan/stcs
		
	Compare commits
	
		
			117 Commits
		
	
	
		
			0de3b17d1d
			...
			devel
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | d767399988 | ||
|   | 89603f0b62 | ||
|   | 14f2a9e773 | ||
|   | a43fc6a66b | ||
|   | 869458b2e1 | ||
|   | 07948d93cf | ||
|   | 7350c86927 | ||
|   | 42f0f8ddc0 | ||
|   | 3df88d07ab | ||
|   | ddfa3a7360 | ||
|   | e61b7334a4 | ||
|   | 5ad69f3f6d | ||
|   | abad01352a | ||
|   | 720b30d681 | ||
|   | 3616e2cbd3 | ||
|   | 7d108337fa | ||
|   | 0f6e6a2168 | ||
|   | 258ed970f1 | ||
|   | a3a6509933 | ||
|   | 8251938b2f | ||
|   | 458b34fcec | ||
|   | 4a7577b977 | ||
|   | 6e0a170f4b | ||
|   | c7b4a08a6a | ||
|   | d0971f59b4 | ||
| b92376d2da | |||
| 630f2002d3 | |||
| 0784cbb54a | |||
| eeb7f81440 | |||
| b03d05bb75 | |||
| e0e4ad5d4b | |||
| fab8ff5040 | |||
| 8071f8c896 | |||
| 3db001bd19 | |||
| 99c59be9ed | |||
| 37b7b6effd | |||
| ee285523f2 | |||
| b426eb2136 | |||
| 2df3b8b40d | |||
| 8c08451d82 | |||
| d81a47b669 | |||
| 2cdfba3537 | |||
| c89c2ad803 | |||
| 3986989dbd | |||
| c0e40dc205 | |||
| 6c6f0dbb7b | |||
| 44c4fde036 | |||
| 21a93d47d4 | |||
| 3f43d42651 | |||
| aab05994ce | |||
| a58ebe6a46 | |||
| 1ec1f1784d | |||
| 7901af86af | |||
| fedfa00c10 | |||
|   | fc8ab19ae9 | ||
|   | 42c4660fe3 | ||
|   | fe030baef5 | ||
|   | 9d06412605 | ||
|   | 9c1f289870 | ||
|   | 3533e7e99a | ||
|   | 8114533475 | ||
|   | fcdc9d7483 | ||
|   | aa9f04c27e | ||
|   | 89ab106992 | ||
|   | ebe2d58975 | ||
|   | 09606a057b | ||
|   | a0a2fd30f0 | ||
|   | 2136de5d69 | ||
|   | dbbea16c19 | ||
| 898ff91392 | |||
|   | f5677e6e7e | ||
| 2047dd5ac6 | |||
|   | c49df2794d | ||
|   | c687811ea5 | ||
|   | 5da00dbaa1 | ||
|   | 01fe339d56 | ||
|   | 220c45d54c | ||
|   | 163f4dcba9 | ||
|   | ce5d0605de | ||
|   | 086c7c8170 | ||
|   | 8e73dcf81f | ||
|   | 057cfad675 | ||
|   | 1508629727 | ||
|   | 4adbd70948 | ||
|   | 6705bf4492 | ||
|   | 8dbc8d57f9 | ||
|   | fa782f748a | ||
|   | a1a7355dc3 | ||
|   | 9d2b049e56 | ||
|   | 3306c6e826 | ||
|   | 2666f90707 | ||
|   | bed53c0a2c | ||
|   | a9f7c4f7c4 | ||
|   | 1981510963 | ||
|   | 4f2ce0c1a4 | ||
|   | 3ae8c15007 | ||
|   | f81f63b198 | ||
|   | 97662081ce | ||
|   | e5a3de4ed8 | ||
|   | 66a566e6a3 | ||
|   | eca9d2c7c8 | ||
|   | 6d86b230ca | ||
| fec367cc1d | |||
|   | 4bbff680aa | ||
|   | 49d4bb26bf | ||
|   | 29bb6bd0a8 | ||
|   | 2fb8cb4acb | ||
|   | 887b46c1d4 | ||
|   | b074d1d8a1 | ||
| aebcc9dff2 | |||
|   | e2f9478971 | ||
|   | 4f0668970f | ||
|   | 4c9901c14a | ||
|   | 17dba19078 | ||
| 58a4c6af06 | |||
| b37b7193b2 | |||
| 05e8005ec9 | 
| @@ -1,3 +1 @@ | ||||
| TOKEN_TELEGRAM_BOT_1= | ||||
| TOKEN_TELEGRAM_BOT_2= | ||||
| TOKEN_TELEGRAM_BOT_3= | ||||
| BOT_TOKEN=YOUR_BOT_TOKEN | ||||
							
								
								
									
										215
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										215
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,13 +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/ | ||||
| /logger_helper/loggers | ||||
| /app/bybit/logger_bybit/loggers | ||||
| *.db | ||||
| # Spyder project settings | ||||
| .spyderproject | ||||
| .spyproject | ||||
|  | ||||
| # Rope project settings | ||||
| .ropeproject | ||||
|  | ||||
| # 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__/ | ||||
|   | ||||
| @@ -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") | ||||
| @@ -1,68 +0,0 @@ | ||||
| <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0"> | ||||
|   <PropertyGroup> | ||||
|     <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> | ||||
|     <SchemaVersion>2.0</SchemaVersion> | ||||
|     <ProjectGuid>bc1d7460-d8ca-4977-a249-0f6d6cc2375a</ProjectGuid> | ||||
|     <ProjectHome>.</ProjectHome> | ||||
|     <StartupFile>BibytBot_API.py</StartupFile> | ||||
|     <SearchPath> | ||||
|     </SearchPath> | ||||
|     <WorkingDirectory>.</WorkingDirectory> | ||||
|     <OutputPath>.</OutputPath> | ||||
|     <Name>BibytBot_API</Name> | ||||
|     <RootNamespace>BibytBot_API</RootNamespace> | ||||
|   </PropertyGroup> | ||||
|   <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> | ||||
|     <DebugSymbols>true</DebugSymbols> | ||||
|     <EnableUnmanagedDebugging>false</EnableUnmanagedDebugging> | ||||
|   </PropertyGroup> | ||||
|   <PropertyGroup Condition=" '$(Configuration)' == 'Release' "> | ||||
|     <DebugSymbols>true</DebugSymbols> | ||||
|     <EnableUnmanagedDebugging>false</EnableUnmanagedDebugging> | ||||
|   </PropertyGroup> | ||||
|   <ItemGroup> | ||||
|     <Compile Include="app\services\Bybit\functions\Add_Bybit_API.py" /> | ||||
|     <Compile Include="app\services\Bybit\functions\balance.py" /> | ||||
|     <Compile Include="app\services\Bybit\functions\functions.py" /> | ||||
|     <Compile Include="app\services\Bybit\functions\func_min_qty.py" /> | ||||
|     <Compile Include="app\services\Bybit\functions\Futures.py" /> | ||||
|     <Compile Include="app\services\Bybit\functions\price_symbol.py" /> | ||||
|     <Compile Include="app\telegram\functions\additional_settings\settings.py" /> | ||||
|     <Compile Include="app\telegram\functions\condition_settings\settings.py" /> | ||||
|     <Compile Include="app\telegram\functions\functions.py" /> | ||||
|     <Compile Include="app\telegram\database\models.py" /> | ||||
|     <Compile Include="app\telegram\database\requests.py" /> | ||||
|     <Compile Include="app\telegram\functions\main_settings\settings.py" /> | ||||
|     <Compile Include="app\telegram\functions\risk_management_settings\settings.py" /> | ||||
|     <Compile Include="app\telegram\handlers\handlers.py" /> | ||||
|     <Compile Include="app\telegram\Keyboards\inline_keyboards.py" /> | ||||
|     <Compile Include="app\telegram\Keyboards\reply_keyboards.py" /> | ||||
|     <Compile Include="app\telegram\logs.py" /> | ||||
|     <Compile Include="BibytBot_API.py" /> | ||||
|     <Compile Include="config.py" /> | ||||
|   </ItemGroup> | ||||
|   <ItemGroup> | ||||
|     <Folder Include="app\" /> | ||||
|     <Folder Include="app\services\Bybit\" /> | ||||
|     <Folder Include="app\services\" /> | ||||
|     <Folder Include="app\services\Bybit\functions\" /> | ||||
|     <Folder Include="app\telegram\database\" /> | ||||
|     <Folder Include="app\telegram\functions\condition_settings\" /> | ||||
|     <Folder Include="app\telegram\functions\additional_settings\" /> | ||||
|     <Folder Include="app\telegram\functions\risk_management_settings\" /> | ||||
|     <Folder Include="app\telegram\handlers\" /> | ||||
|     <Folder Include="app\telegram\Keyboards\" /> | ||||
|     <Folder Include="app\telegram\functions\main_settings\" /> | ||||
|     <Folder Include="app\telegram\functions\" /> | ||||
|     <Folder Include="app\telegram\" /> | ||||
|   </ItemGroup> | ||||
|   <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.targets" /> | ||||
|   <!-- Uncomment the CoreCompile target to enable the Build command in | ||||
|        Visual Studio and specify your pre- and post-build commands in | ||||
|        the BeforeBuild and AfterBuild targets below. --> | ||||
|   <!--<Target Name="CoreCompile" />--> | ||||
|   <Target Name="BeforeBuild"> | ||||
|   </Target> | ||||
|   <Target Name="AfterBuild"> | ||||
|   </Target> | ||||
| </Project> | ||||
| @@ -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 | ||||
							
								
								
									
										38
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										38
									
								
								README.md
									
									
									
									
									
								
							| @@ -27,7 +27,7 @@ Crypto Trading Telegram Bot | ||||
| - Хранение пользовательских настроек и статистики в базе данных. | ||||
|  | ||||
|  | ||||
| ## Установка и запуск | ||||
| ## Установка | ||||
|  | ||||
| 1. Клонируйте репозиторий: | ||||
|  | ||||
| @@ -41,6 +41,10 @@ git clone https://git.svoboda.works/kodorvan/stcs | ||||
| ```bash | ||||
| pip install -r requirements.txt | ||||
| ``` | ||||
| или для отдельного пользователя | ||||
| ```bash | ||||
| sudo -u www-data /usr/bin/pip install -r requirements.txt | ||||
| ``` | ||||
|  | ||||
| 3. Зарегистрируйте чат-робота и сгенерируйте ключ авторизации<br> | ||||
| [@BotFather](https://t.me/BotFather) | ||||
| @@ -50,11 +54,41 @@ pip install -r requirements.txt | ||||
| cp .env.sample .env | ||||
| nvim .env | ||||
| ``` | ||||
| 5. Выполните миграции: | ||||
| ```bash | ||||
| alembic upgrade head | ||||
| ``` | ||||
|  | ||||
| 5. Запустите бота: | ||||
|  | ||||
| ```bash | ||||
| python BybitBot_API.py | ||||
| python run.py | ||||
| ``` | ||||
|  | ||||
| ## Настройка автономной работы | ||||
| 1. Создаём файл конфигурации SystemD | ||||
| ```bash | ||||
| sudo cp examples/systemd/stcs.service /etc/systemd/system/ | ||||
| ``` | ||||
|  | ||||
| 2. Настраиваем его | ||||
| ```bash | ||||
| nvim /etc/systemd/system/stcs.service | ||||
| ``` | ||||
|  | ||||
| 3. Добавляем в автозапуск | ||||
| ```bash | ||||
| sudo systemctl enable stcs | ||||
| ``` | ||||
|  | ||||
| 4. Запускаем | ||||
| ```bash | ||||
| sudo service stcs start | ||||
| ``` | ||||
|  | ||||
| 5. Проверяем | ||||
| ```bash | ||||
| sudo service stcs status | ||||
| ``` | ||||
|  | ||||
| ## Настройки пользователя | ||||
|   | ||||
							
								
								
									
										147
									
								
								alembic.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								alembic.ini
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | ||||
| # A generic, single database configuration. | ||||
|  | ||||
| [alembic] | ||||
| # path to migration scripts. | ||||
| # this is typically a path given in POSIX (e.g. forward slashes) | ||||
| # format, relative to the token %(here)s which refers to the location of this | ||||
| # ini file | ||||
| script_location = %(here)s/alembic | ||||
|  | ||||
| # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s | ||||
| # Uncomment the line below if you want the files to be prepended with date and time | ||||
| # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file | ||||
| # for all available tokens | ||||
| # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s | ||||
|  | ||||
| # sys.path path, will be prepended to sys.path if present. | ||||
| # defaults to the current working directory.  for multiple paths, the path separator | ||||
| # is defined by "path_separator" below. | ||||
| prepend_sys_path = . | ||||
|  | ||||
|  | ||||
| # timezone to use when rendering the date within the migration file | ||||
| # as well as the filename. | ||||
| # If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library. | ||||
| # Any required deps can installed by adding `alembic[tz]` to the pip requirements | ||||
| # string value is passed to ZoneInfo() | ||||
| # leave blank for localtime | ||||
| # timezone = | ||||
|  | ||||
| # max length of characters to apply to the "slug" field | ||||
| # truncate_slug_length = 40 | ||||
|  | ||||
| # set to 'true' to run the environment during | ||||
| # the 'revision' command, regardless of autogenerate | ||||
| # revision_environment = false | ||||
|  | ||||
| # set to 'true' to allow .pyc and .pyo files without | ||||
| # a source .py file to be detected as revisions in the | ||||
| # versions/ directory | ||||
| # sourceless = false | ||||
|  | ||||
| # version location specification; This defaults | ||||
| # to <script_location>/versions.  When using multiple version | ||||
| # directories, initial revisions must be specified with --version-path. | ||||
| # The path separator used here should be the separator specified by "path_separator" | ||||
| # below. | ||||
| # version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions | ||||
|  | ||||
| # path_separator; This indicates what character is used to split lists of file | ||||
| # paths, including version_locations and prepend_sys_path within configparser | ||||
| # files such as alembic.ini. | ||||
| # The default rendered in new alembic.ini files is "os", which uses os.pathsep | ||||
| # to provide os-dependent path splitting. | ||||
| # | ||||
| # Note that in order to support legacy alembic.ini files, this default does NOT | ||||
| # take place if path_separator is not present in alembic.ini.  If this | ||||
| # option is omitted entirely, fallback logic is as follows: | ||||
| # | ||||
| # 1. Parsing of the version_locations option falls back to using the legacy | ||||
| #    "version_path_separator" key, which if absent then falls back to the legacy | ||||
| #    behavior of splitting on spaces and/or commas. | ||||
| # 2. Parsing of the prepend_sys_path option falls back to the legacy | ||||
| #    behavior of splitting on spaces, commas, or colons. | ||||
| # | ||||
| # Valid values for path_separator are: | ||||
| # | ||||
| # path_separator = : | ||||
| # path_separator = ; | ||||
| # path_separator = space | ||||
| # path_separator = newline | ||||
| # | ||||
| # Use os.pathsep. Default configuration used for new projects. | ||||
| path_separator = os | ||||
|  | ||||
| # set to 'true' to search source files recursively | ||||
| # in each "version_locations" directory | ||||
| # new in Alembic version 1.10 | ||||
| # recursive_version_locations = false | ||||
|  | ||||
| # the output encoding used when revision files | ||||
| # are written from script.py.mako | ||||
| # output_encoding = utf-8 | ||||
|  | ||||
| # database URL.  This is consumed by the user-maintained env.py script only. | ||||
| # other means of configuring database URLs may be customized within the env.py | ||||
| # file. | ||||
| sqlalchemy.url = sqlite+aiosqlite:///./database/db/stcs.db | ||||
|  | ||||
|  | ||||
| [post_write_hooks] | ||||
| # post_write_hooks defines scripts or Python functions that are run | ||||
| # on newly generated revision scripts.  See the documentation for further | ||||
| # detail and examples | ||||
|  | ||||
| # format using "black" - use the console_scripts runner, against the "black" entrypoint | ||||
| # hooks = black | ||||
| # black.type = console_scripts | ||||
| # black.entrypoint = black | ||||
| # black.options = -l 79 REVISION_SCRIPT_FILENAME | ||||
|  | ||||
| # lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module | ||||
| # hooks = ruff | ||||
| # ruff.type = module | ||||
| # ruff.module = ruff | ||||
| # ruff.options = check --fix REVISION_SCRIPT_FILENAME | ||||
|  | ||||
| # Alternatively, use the exec runner to execute a binary found on your PATH | ||||
| # hooks = ruff | ||||
| # ruff.type = exec | ||||
| # ruff.executable = ruff | ||||
| # ruff.options = check --fix REVISION_SCRIPT_FILENAME | ||||
|  | ||||
| # Logging configuration.  This is also consumed by the user-maintained | ||||
| # env.py script only. | ||||
| [loggers] | ||||
| keys = root,sqlalchemy,alembic | ||||
|  | ||||
| [handlers] | ||||
| keys = console | ||||
|  | ||||
| [formatters] | ||||
| keys = generic | ||||
|  | ||||
| [logger_root] | ||||
| level = WARNING | ||||
| handlers = console | ||||
| qualname = | ||||
|  | ||||
| [logger_sqlalchemy] | ||||
| level = WARNING | ||||
| handlers = | ||||
| qualname = sqlalchemy.engine | ||||
|  | ||||
| [logger_alembic] | ||||
| level = INFO | ||||
| handlers = | ||||
| qualname = alembic | ||||
|  | ||||
| [handler_console] | ||||
| class = StreamHandler | ||||
| args = (sys.stderr,) | ||||
| level = NOTSET | ||||
| formatter = generic | ||||
|  | ||||
| [formatter_generic] | ||||
| format = %(levelname)-5.5s [%(name)s] %(message)s | ||||
| datefmt = %H:%M:%S | ||||
							
								
								
									
										1
									
								
								alembic/README
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								alembic/README
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| Generic single-database configuration. | ||||
							
								
								
									
										53
									
								
								alembic/env.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								alembic/env.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| import asyncio | ||||
| from logging.config import fileConfig | ||||
| from sqlalchemy import pool | ||||
| from sqlalchemy.ext.asyncio import async_engine_from_config | ||||
| from alembic import context | ||||
|  | ||||
| config = context.config | ||||
|  | ||||
| if config.config_file_name is not None: | ||||
|     fileConfig(config.config_file_name) | ||||
|  | ||||
| from database.models import Base | ||||
| target_metadata = Base.metadata | ||||
|  | ||||
| def do_run_migrations(connection): | ||||
|     context.configure( | ||||
|         connection=connection, | ||||
|         target_metadata=target_metadata, | ||||
|         compare_type=True, | ||||
|     ) | ||||
|     with context.begin_transaction(): | ||||
|         context.run_migrations() | ||||
|  | ||||
| async def run_async_migrations(): | ||||
|     connectable = async_engine_from_config( | ||||
|         config.get_section(config.config_ini_section), | ||||
|         prefix="sqlalchemy.", | ||||
|         poolclass=pool.NullPool, | ||||
|     ) | ||||
|  | ||||
|     async with connectable.connect() as connection: | ||||
|         await connection.run_sync(do_run_migrations) | ||||
|  | ||||
|     await connectable.dispose() | ||||
|  | ||||
| def run_migrations_offline(): | ||||
|     url = config.get_main_option("sqlalchemy.url") | ||||
|     context.configure( | ||||
|         url=url, | ||||
|         target_metadata=target_metadata, | ||||
|         literal_binds=True, | ||||
|         dialect_opts={"paramstyle": "named"}, | ||||
|     ) | ||||
|     with context.begin_transaction(): | ||||
|         context.run_migrations() | ||||
|  | ||||
| def run_migrations_online(): | ||||
|     asyncio.run(run_async_migrations()) | ||||
|  | ||||
| if context.is_offline_mode(): | ||||
|     run_migrations_offline() | ||||
| else: | ||||
|     run_migrations_online() | ||||
							
								
								
									
										28
									
								
								alembic/script.py.mako
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								alembic/script.py.mako
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| """${message} | ||||
|  | ||||
| Revision ID: ${up_revision} | ||||
| Revises: ${down_revision | comma,n} | ||||
| Create Date: ${create_date} | ||||
|  | ||||
| """ | ||||
| from typing import Sequence, Union | ||||
|  | ||||
| from alembic import op | ||||
| import sqlalchemy as sa | ||||
| ${imports if imports else ""} | ||||
|  | ||||
| # revision identifiers, used by Alembic. | ||||
| revision: str = ${repr(up_revision)} | ||||
| down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)} | ||||
| branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} | ||||
| depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} | ||||
|  | ||||
|  | ||||
| def upgrade() -> None: | ||||
|     """Upgrade schema.""" | ||||
|     ${upgrades if upgrades else "pass"} | ||||
|  | ||||
|  | ||||
| def downgrade() -> None: | ||||
|     """Downgrade schema.""" | ||||
|     ${downgrades if downgrades else "pass"} | ||||
| @@ -0,0 +1,34 @@ | ||||
| """Added column side for additional_setiings | ||||
|  | ||||
| Revision ID: e5d612e44563 | ||||
| Revises: fbf4e3658310 | ||||
| Create Date: 2025-10-25 18:25:52.746250 | ||||
|  | ||||
| """ | ||||
| from typing import Sequence, Union | ||||
|  | ||||
| from alembic import op | ||||
| import sqlalchemy as sa | ||||
|  | ||||
|  | ||||
| # revision identifiers, used by Alembic. | ||||
| revision: str = 'e5d612e44563' | ||||
| down_revision: Union[str, Sequence[str], None] = 'fbf4e3658310' | ||||
| branch_labels: Union[str, Sequence[str], None] = None | ||||
| depends_on: Union[str, Sequence[str], None] = None | ||||
|  | ||||
|  | ||||
| def upgrade() -> None: | ||||
|     """Upgrade schema.""" | ||||
|     # ### commands auto generated by Alembic - please adjust! ### | ||||
|     op.add_column('user_additional_settings', | ||||
|                   sa.Column('side', sa.String(), nullable=False, server_default='') | ||||
|                   ) | ||||
|     # ### end Alembic commands ### | ||||
|  | ||||
|  | ||||
| def downgrade() -> None: | ||||
|     """Downgrade schema.""" | ||||
|     # ### commands auto generated by Alembic - please adjust! ### | ||||
|     op.drop_column('user_additional_settings', 'side') | ||||
|     # ### end Alembic commands ### | ||||
							
								
								
									
										32
									
								
								alembic/versions/fbf4e3658310_added_side_mode_column.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								alembic/versions/fbf4e3658310_added_side_mode_column.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| """Added side_mode column | ||||
|  | ||||
| Revision ID: fbf4e3658310 | ||||
| Revises:  | ||||
| Create Date: 2025-10-22 13:08:02.317419 | ||||
|  | ||||
| """ | ||||
| from typing import Sequence, Union | ||||
|  | ||||
| from alembic import op | ||||
| import sqlalchemy as sa | ||||
|  | ||||
|  | ||||
| # revision identifiers, used by Alembic. | ||||
| revision: str = 'fbf4e3658310' | ||||
| down_revision: Union[str, Sequence[str], None] = None | ||||
| branch_labels: Union[str, Sequence[str], None] = None | ||||
| depends_on: Union[str, Sequence[str], None] = None | ||||
|  | ||||
|  | ||||
| def upgrade() -> None: | ||||
|     """Upgrade schema.""" | ||||
|     # ### commands auto generated by Alembic - please adjust! ### | ||||
|     op.add_column('user_deals', sa.Column('side_mode', sa.String(), nullable=True)) | ||||
|     # ### end Alembic commands ### | ||||
|  | ||||
|  | ||||
| def downgrade() -> None: | ||||
|     """Downgrade schema.""" | ||||
|     # ### commands auto generated by Alembic - please adjust! ### | ||||
|     op.drop_column('user_deals', 'side_mode') | ||||
|     # ### end Alembic commands ### | ||||
							
								
								
									
										0
									
								
								app/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										21
									
								
								app/bybit/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/bybit/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										100
									
								
								app/bybit/close_positions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								app/bybit/close_positions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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_by_symbol( | ||||
|     tg_id: int, symbol: str | ||||
| ) -> bool: | ||||
|     """ | ||||
|     Closes all positions | ||||
|     :param tg_id: Telegram user ID | ||||
|     :param symbol: symbol | ||||
|     :return: bool | ||||
|     """ | ||||
|     try: | ||||
|         client = await get_bybit_client(tg_id) | ||||
|  | ||||
|         response = client.get_positions( | ||||
|             category="linear", symbol=symbol | ||||
|         ) | ||||
|         positions = response.get("result", {}).get("list", []) | ||||
|         r_side = "Sell" if positions[0].get("side") == "Buy" else "Buy" | ||||
|         qty = positions[0].get("size") | ||||
|         position_idx = positions[0].get("positionIdx") | ||||
|  | ||||
|         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("Positions closed for %s for user %s", symbol, tg_id) | ||||
|             return True | ||||
|         else: | ||||
|             logger.error( | ||||
|                 "Error closing position for %s for user %s", symbol, tg_id | ||||
|             ) | ||||
|             return False | ||||
|     except Exception as e: | ||||
|         logger.error( | ||||
|             "Error closing 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 | ||||
							
								
								
									
										0
									
								
								app/bybit/get_functions/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/bybit/get_functions/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										28
									
								
								app/bybit/get_functions/get_balance.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								app/bybit/get_functions/get_balance.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										28
									
								
								app/bybit/get_functions/get_instruments_info.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								app/bybit/get_functions/get_instruments_info.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										129
									
								
								app/bybit/get_functions/get_positions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								app/bybit/get_functions/get_positions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										35
									
								
								app/bybit/get_functions/get_tickers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								app/bybit/get_functions/get_tickers.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										0
									
								
								app/bybit/logger_bybit/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/bybit/logger_bybit/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										129
									
								
								app/bybit/logger_bybit/logger_bybit.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								app/bybit/logger_bybit/logger_bybit.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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, | ||||
|         }, | ||||
|     }, | ||||
| } | ||||
							
								
								
									
										401
									
								
								app/bybit/open_positions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										401
									
								
								app/bybit/open_positions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,401 @@ | ||||
| import logging.config | ||||
| import math | ||||
|  | ||||
| from pybit.exceptions import InvalidRequestError | ||||
|  | ||||
| import database.request as rq | ||||
| from app.bybit import get_bybit_client | ||||
| 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 | ||||
| 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 safe_float | ||||
|  | ||||
| logging.config.dictConfig(LOGGING_CONFIG) | ||||
| logger = logging.getLogger("open_positions") | ||||
|  | ||||
|  | ||||
| async def start_trading_cycle( | ||||
|         tg_id: int | ||||
| ) -> str | None: | ||||
|     """ | ||||
|     Start trading cycle | ||||
|     :param tg_id: Telegram user ID | ||||
|     """ | ||||
|     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 | ||||
|         switch_side = additional_data.switch_side | ||||
|         side= additional_data.side | ||||
|         margin_type = additional_data.margin_type | ||||
|         leverage = additional_data.leverage | ||||
|         order_quantity = additional_data.order_quantity | ||||
|         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 | ||||
|         total_commission = 0 | ||||
|  | ||||
|         if trade_mode == "Switch": | ||||
|             side = side | ||||
|         else: | ||||
|             if trade_mode == "Long": | ||||
|                 side = "Buy" | ||||
|             else: | ||||
|                 side = "Sell" | ||||
|  | ||||
|         await set_switch_position_mode( | ||||
|             tg_id=tg_id, | ||||
|             symbol=symbol, | ||||
|             mode=0) | ||||
|         await set_margin_mode(tg_id=tg_id, margin_mode=margin_type) | ||||
|         await set_leverage( | ||||
|             tg_id=tg_id, | ||||
|             symbol=symbol, | ||||
|             leverage=leverage, | ||||
|         ) | ||||
|  | ||||
|         res = await open_positions( | ||||
|             tg_id=tg_id, | ||||
|             symbol=symbol, | ||||
|             side=side, | ||||
|             order_quantity=order_quantity, | ||||
|             trigger_price=trigger_price, | ||||
|             margin_type=margin_type, | ||||
|             leverage=leverage, | ||||
|             take_profit_percent=take_profit_percent, | ||||
|             stop_loss_percent=stop_loss_percent, | ||||
|             commission_fee_percent=total_commission | ||||
|         ) | ||||
|  | ||||
|         if res == "OK": | ||||
|             await rq.set_user_deal( | ||||
|                 tg_id=tg_id, | ||||
|                 symbol=symbol, | ||||
|                 current_step=1, | ||||
|                 trade_mode=trade_mode, | ||||
|                 side_mode=switch_side, | ||||
|                 margin_type=margin_type, | ||||
|                 leverage=leverage, | ||||
|                 order_quantity=order_quantity, | ||||
|                 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, | ||||
|                 base_quantity=order_quantity | ||||
|             ) | ||||
|             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", | ||||
|                    "The number of contracts exceeds minimum limit allowed" | ||||
|                } | ||||
|             else None | ||||
|         ) | ||||
|  | ||||
|     except Exception as e: | ||||
|         logger.error("Error in start_trading: %s", e) | ||||
|         return None | ||||
|  | ||||
|  | ||||
| async def trading_cycle_profit( | ||||
|         tg_id: int, symbol: str, side: str) -> str | None: | ||||
|     try: | ||||
|         user_deals_data = await rq.get_user_deal_by_symbol(tg_id=tg_id, symbol=symbol) | ||||
|         user_auto_trading_data = await rq.get_user_auto_trading(tg_id=tg_id, symbol=symbol) | ||||
|         total_fee = user_auto_trading_data.total_fee | ||||
|         trade_mode = user_deals_data.trade_mode | ||||
|         margin_type = user_deals_data.margin_type | ||||
|         leverage = user_deals_data.leverage | ||||
|         trigger_price = 0 | ||||
|         take_profit_percent = user_deals_data.take_profit_percent | ||||
|         stop_loss_percent = user_deals_data.stop_loss_percent | ||||
|         max_bets_in_series = user_deals_data.max_bets_in_series | ||||
|         martingale_factor = user_deals_data.martingale_factor | ||||
|         side_mode = user_deals_data.side_mode | ||||
|         base_quantity = user_deals_data.base_quantity | ||||
|  | ||||
|         await set_margin_mode(tg_id=tg_id, margin_mode=margin_type) | ||||
|         await set_leverage( | ||||
|             tg_id=tg_id, | ||||
|             symbol=symbol, | ||||
|             leverage=leverage, | ||||
|         ) | ||||
|  | ||||
|  | ||||
|         if trade_mode == "Switch": | ||||
|             if side_mode == "Противоположно": | ||||
|                 s_side = "Sell" if side == "Buy" else "Buy" | ||||
|             else: | ||||
|                 s_side = side | ||||
|         else: | ||||
|             s_side = side | ||||
|  | ||||
|         res = await open_positions( | ||||
|             tg_id=tg_id, | ||||
|             symbol=symbol, | ||||
|             side=s_side, | ||||
|             order_quantity=base_quantity, | ||||
|             trigger_price=trigger_price, | ||||
|             margin_type=margin_type, | ||||
|             leverage=leverage, | ||||
|             take_profit_percent=take_profit_percent, | ||||
|             stop_loss_percent=stop_loss_percent, | ||||
|             commission_fee_percent=total_fee | ||||
|         ) | ||||
|  | ||||
|         if res == "OK": | ||||
|             await rq.set_user_deal( | ||||
|                 tg_id=tg_id, | ||||
|                 symbol=symbol, | ||||
|                 current_step=1, | ||||
|                 trade_mode=trade_mode, | ||||
|                 side_mode=side_mode, | ||||
|                 margin_type=margin_type, | ||||
|                 leverage=leverage, | ||||
|                 order_quantity=base_quantity, | ||||
|                 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, | ||||
|                 base_quantity=base_quantity | ||||
|             ) | ||||
|             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_profit: %s", e) | ||||
|         return None | ||||
|  | ||||
|  | ||||
| async def trading_cycle( | ||||
|         tg_id: int, symbol: str, side: str, | ||||
| ) -> str | None: | ||||
|     try: | ||||
|         user_deals_data = await rq.get_user_deal_by_symbol(tg_id=tg_id, symbol=symbol) | ||||
|         user_auto_trading_data = await rq.get_user_auto_trading(tg_id=tg_id, symbol=symbol) | ||||
|         user_risk_management_data = await rq.get_user_risk_management(tg_id=tg_id) | ||||
|         commission_fee = user_risk_management_data.commission_fee | ||||
|         total_fee = user_auto_trading_data.total_fee | ||||
|         trade_mode = user_deals_data.trade_mode | ||||
|         margin_type = user_deals_data.margin_type | ||||
|         leverage = user_deals_data.leverage | ||||
|         trigger_price = 0 | ||||
|         take_profit_percent = user_deals_data.take_profit_percent | ||||
|         stop_loss_percent = user_deals_data.stop_loss_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 | ||||
|         order_quantity = user_deals_data.order_quantity | ||||
|         base_quantity = user_deals_data.base_quantity | ||||
|         side_mode = user_deals_data.side_mode | ||||
|  | ||||
|         next_quantity = safe_float(order_quantity) * ( | ||||
|             safe_float(martingale_factor) | ||||
|         ) | ||||
|         current_step += 1 | ||||
|  | ||||
|         if max_bets_in_series < current_step: | ||||
|             return "Max bets in series" | ||||
|  | ||||
|         await set_margin_mode(tg_id=tg_id, margin_mode=margin_type) | ||||
|         await set_leverage( | ||||
|             tg_id=tg_id, | ||||
|             symbol=symbol, | ||||
|             leverage=leverage, | ||||
|         ) | ||||
|         if commission_fee == "Yes_commission_fee": | ||||
|             total_fee = total_fee | ||||
|         else: | ||||
|             total_fee = 0 | ||||
|  | ||||
|         if trade_mode == "Switch": | ||||
|             if side == "Buy": | ||||
|                 r_side = "Sell" | ||||
|             else: | ||||
|                 r_side = "Buy" | ||||
|         else: | ||||
|             r_side = side | ||||
|  | ||||
|         res = await open_positions( | ||||
|             tg_id=tg_id, | ||||
|             symbol=symbol, | ||||
|             side=r_side, | ||||
|             order_quantity=next_quantity, | ||||
|             trigger_price=trigger_price, | ||||
|             margin_type=margin_type, | ||||
|             leverage=leverage, | ||||
|             take_profit_percent=take_profit_percent, | ||||
|             stop_loss_percent=stop_loss_percent, | ||||
|             commission_fee_percent=total_fee | ||||
|         ) | ||||
|  | ||||
|         if res == "OK": | ||||
|             await rq.set_user_deal( | ||||
|                 tg_id=tg_id, | ||||
|                 symbol=symbol, | ||||
|                 current_step=current_step, | ||||
|                 trade_mode=trade_mode, | ||||
|                 side_mode=side_mode, | ||||
|                 margin_type=margin_type, | ||||
|                 leverage=leverage, | ||||
|                 order_quantity=next_quantity, | ||||
|                 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, | ||||
|                 base_quantity=base_quantity | ||||
|             ) | ||||
|             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_quantity: float, | ||||
|         trigger_price: float, | ||||
|         margin_type: str, | ||||
|         leverage: str, | ||||
|         take_profit_percent: float, | ||||
|         stop_loss_percent: float, | ||||
|         commission_fee_percent: float | ||||
| ) -> str | None: | ||||
|     try: | ||||
|         client = await get_bybit_client(tg_id=tg_id) | ||||
|         get_ticker = await get_tickers(tg_id, symbol=symbol) | ||||
|         price_symbol = safe_float(get_ticker.get("lastPrice")) or 0 | ||||
|         instruments_info = await get_instruments_info(tg_id=tg_id, symbol=symbol) | ||||
|         qty_step_str = instruments_info.get("lotSizeFilter").get("qtyStep") | ||||
|         qty_step = safe_float(qty_step_str) | ||||
|         qty = (safe_float(order_quantity) * safe_float(leverage)) / safe_float(price_symbol) | ||||
|         decimals = abs(int(round(math.log10(qty_step)))) | ||||
|         qty_formatted = math.floor(qty / qty_step) * qty_step | ||||
|         qty_formatted = round(qty_formatted, decimals) | ||||
|  | ||||
|         if trigger_price > 0: | ||||
|             po_trigger_price = str(trigger_price) | ||||
|             trigger_direction = 1 if trigger_price > price_symbol else 2 | ||||
|         else: | ||||
|             po_trigger_price = None | ||||
|             trigger_direction = None | ||||
|  | ||||
|         price_for_cals = trigger_price if po_trigger_price is not None else price_symbol | ||||
|  | ||||
|         if qty_formatted <= 0: | ||||
|             return "Order does not meet minimum order value" | ||||
|  | ||||
|         if margin_type == "ISOLATED_MARGIN": | ||||
|             if side == "Buy": | ||||
|                 take_profit_price = price_for_cals * ( | ||||
|                         1 + take_profit_percent / 100) + commission_fee_percent / qty_formatted | ||||
|                 stop_loss_price = None | ||||
|             else: | ||||
|                 take_profit_price = price_for_cals * ( | ||||
|                         1 - take_profit_percent / 100) - commission_fee_percent / qty_formatted | ||||
|                 stop_loss_price = None | ||||
|         else: | ||||
|             if side == "Buy": | ||||
|                 take_profit_price = price_for_cals * ( | ||||
|                             1 + take_profit_percent / 100) + commission_fee_percent / qty_formatted | ||||
|                 stop_loss_price = price_for_cals * (1 - stop_loss_percent / 100) | ||||
|             else: | ||||
|                 take_profit_price = price_for_cals * ( | ||||
|                             1 - take_profit_percent / 100) - commission_fee_percent / qty_formatted | ||||
|                 stop_loss_price = price_for_cals * (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": "Market", | ||||
|             "qty": str(qty_formatted), | ||||
|             "triggerDirection": trigger_direction, | ||||
|             "triggerPrice": po_trigger_price, | ||||
|             "triggerBy": "LastPrice", | ||||
|             "timeInForce": "GTC", | ||||
|             "positionIdx": 0, | ||||
|             "tpslMode": "Full", | ||||
|             "takeProfit": str(take_profit_price) if take_profit_price else None, | ||||
|             "stopLoss": str(stop_loss_price) if stop_loss_price else None, | ||||
|         } | ||||
|  | ||||
|         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", | ||||
|             "The number of contracts exceeds maximum limit allowed": "The number of contracts exceeds maximum limit allowed", | ||||
|             "The number of contracts exceeds minimum limit allowed": "The number of contracts exceeds minimum limit allowed", | ||||
|         } | ||||
|         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, exc_info=True) | ||||
|         return None | ||||
							
								
								
									
										45
									
								
								app/bybit/profile_bybit.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								app/bybit/profile_bybit.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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.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) | ||||
|             if symbol is None: | ||||
|                 await rq.set_user_symbol(tg_id=tg_id, symbol="BTCUSDT") | ||||
|                 await user_profile_bybit(tg_id=tg_id, message=message, state=state) | ||||
|             else: | ||||
|                 await message.answer( | ||||
|                     text=f"💎Ваш профиль:\n\n" | ||||
|                          f"⚖️ Баланс: {float(balance):,.2f} USD\n" | ||||
|                          f"📊Торговая пара: {symbol}\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) | ||||
							
								
								
									
										0
									
								
								app/bybit/set_functions/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/bybit/set_functions/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										96
									
								
								app/bybit/set_functions/set_leverage.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								app/bybit/set_functions/set_leverage.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										28
									
								
								app/bybit/set_functions/set_margin_mode.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								app/bybit/set_functions/set_margin_mode.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										54
									
								
								app/bybit/set_functions/set_switch_position_mode.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								app/bybit/set_functions/set_switch_position_mode.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										45
									
								
								app/bybit/set_functions/set_tp_sl.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								app/bybit/set_functions/set_tp_sl.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										256
									
								
								app/bybit/telegram_message_handler.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										256
									
								
								app/bybit/telegram_message_handler.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,256 @@ | ||||
| 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, trading_cycle_profit | ||||
| from app.helper_functions import format_value, safe_float | ||||
|  | ||||
| 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")) | ||||
|             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")) | ||||
|  | ||||
|             status_map = { | ||||
|                 "Untriggered": "Условный ордер выставлен", | ||||
|             } | ||||
|  | ||||
|             if order_status == "Filled" or order_status not in status_map: | ||||
|                 return None | ||||
|  | ||||
|             user_auto_trading = await rq.get_user_auto_trading( | ||||
|                 tg_id=tg_id, symbol=symbol | ||||
|             ) | ||||
|             auto_trading = ( | ||||
|                 user_auto_trading.auto_trading if user_auto_trading else False | ||||
|             ) | ||||
|             user_deals_data = await rq.get_user_deal_by_symbol( | ||||
|                 tg_id=tg_id, symbol=symbol | ||||
|             ) | ||||
|  | ||||
|             text = ( | ||||
|                 f"Торговая пара: {symbol}\n" | ||||
|                 f"Движение: {side_rus}\n" | ||||
|             ) | ||||
|  | ||||
|             if user_deals_data is not None and auto_trading: | ||||
|                 text += f"Текущая ставка: {user_deals_data.order_quantity} USDT\n" | ||||
|             else: | ||||
|                 text += f"Количество: {qty}\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_qty = format_value(execution.get("execQty")) | ||||
|             exec_fees = format_value(execution.get("execFee")) | ||||
|             fee_rate = format_value(execution.get("feeRate")) | ||||
|             side = format_value(execution.get("side")) | ||||
|             side_rus = ( | ||||
|                 "Покупка" | ||||
|                 if side == "Buy" | ||||
|                 else "Продажа" if side == "Sell" else "Нет данных" | ||||
|             ) | ||||
|             if safe_float(exec_fees) == 0: | ||||
|                 exec_fee = safe_float(exec_price) * safe_float(exec_qty) * safe_float( | ||||
|                     fee_rate | ||||
|                 ) | ||||
|             else: | ||||
|                 exec_fee = safe_float(exec_fees) | ||||
|  | ||||
|             if safe_float(closed_size) == 0: | ||||
|                 await rq.set_fee_user_auto_trading( | ||||
|                     tg_id=tg_id, symbol=symbol, fee=safe_float(exec_fee) | ||||
|                 ) | ||||
|  | ||||
|             user_auto_trading = await rq.get_user_auto_trading( | ||||
|                 tg_id=tg_id, symbol=symbol | ||||
|             ) | ||||
|  | ||||
|             get_total_fee = user_auto_trading.total_fee | ||||
|             total_fee = safe_float(exec_fee) + safe_float(get_total_fee) | ||||
|  | ||||
|  | ||||
|             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")) | ||||
|             total_pnl = safe_float(exec_pnl) - safe_float(exec_fee) - fee | ||||
|             header = ( | ||||
|                 "Сделка закрыта:" if safe_float(closed_size) > 0 else "Сделка открыта:" | ||||
|             ) | ||||
|             text = f"{header}\n" f"Торговая пара: {symbol}\n" | ||||
|  | ||||
|             auto_trading = ( | ||||
|                 user_auto_trading.auto_trading if user_auto_trading else False | ||||
|             ) | ||||
|             user_deals_data = await rq.get_user_deal_by_symbol( | ||||
|                 tg_id=tg_id, symbol=symbol | ||||
|             ) | ||||
|             if user_deals_data is not None and auto_trading: | ||||
|                 await rq.set_total_fee_user_auto_trading( | ||||
|                     tg_id=tg_id, symbol=symbol, total_fee=total_fee | ||||
|                 ) | ||||
|                 text += f"Текущая ставка: {user_deals_data.order_quantity} USDT\n" | ||||
|  | ||||
|             text += ( | ||||
|                 f"Цена исполнения: {exec_price}\n" | ||||
|                 f"Комиссия: {exec_fee:.8f}\n" | ||||
|             ) | ||||
|  | ||||
|             if safe_float(closed_size) == 0: | ||||
|                 text += f"Движение: {side_rus}\n" | ||||
|             else: | ||||
|                 text += f"\nРеализованная прибыль: {total_pnl:.7f}\n" | ||||
|  | ||||
|             await self.telegram_bot.send_message( | ||||
|                 chat_id=tg_id, text=text, reply_markup=kbi.profile_bybit | ||||
|             ) | ||||
|  | ||||
|             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_last_side_by_symbol( | ||||
|                         tg_id=tg_id, symbol=symbol, last_side=r_side) | ||||
|                     await rq.set_total_fee_user_auto_trading( | ||||
|                         tg_id=tg_id, symbol=symbol, total_fee=0 | ||||
|                     ) | ||||
|                     await rq.set_fee_user_auto_trading( | ||||
|                         tg_id=tg_id, symbol=symbol, fee=0 | ||||
|                     ) | ||||
|  | ||||
|                     res = await trading_cycle_profit( | ||||
|                         tg_id=tg_id, symbol=symbol, side=r_side | ||||
|                     ) | ||||
|  | ||||
|                     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, "❗️ Не удалось открыть новую сделку" | ||||
|                         ) | ||||
|                         await rq.set_auto_trading( | ||||
|                             tg_id=tg_id, symbol=symbol, auto_trading=False | ||||
|                         ) | ||||
|  | ||||
|                         await rq.set_total_fee_user_auto_trading( | ||||
|                             tg_id=tg_id, symbol=symbol, total_fee=0 | ||||
|                         ) | ||||
|                         await rq.set_fee_user_auto_trading( | ||||
|                             tg_id=tg_id, symbol=symbol, fee=0 | ||||
|                         ) | ||||
|                         await self.telegram_bot.send_message( | ||||
|                             chat_id=tg_id, | ||||
|                             text=error_text, | ||||
|                             reply_markup=kbi.profile_bybit, | ||||
|                         ) | ||||
|                 else: | ||||
|                     open_order_text = "\n❗️ Сделка закрылась в минус, открываю новую сделку с увеличенной ставкой.\n" | ||||
|                     await self.telegram_bot.send_message( | ||||
|                         chat_id=tg_id, text=open_order_text | ||||
|                     ) | ||||
|  | ||||
|                     if side == "Buy": | ||||
|                         r_side = "Sell" | ||||
|                     else: | ||||
|                         r_side = "Buy" | ||||
|  | ||||
|                     res = await trading_cycle( | ||||
|                         tg_id=tg_id, symbol=symbol, side=r_side | ||||
|                     ) | ||||
|  | ||||
|                     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, "❗️ Не удалось открыть новую сделку" | ||||
|                         ) | ||||
|                         await rq.set_auto_trading( | ||||
|                             tg_id=tg_id, symbol=symbol, auto_trading=False | ||||
|                         ) | ||||
|  | ||||
|                         await rq.set_total_fee_user_auto_trading( | ||||
|                             tg_id=tg_id, symbol=symbol, total_fee=0 | ||||
|                         ) | ||||
|                         await rq.set_fee_user_auto_trading( | ||||
|                             tg_id=tg_id, symbol=symbol, fee=0 | ||||
|                         ) | ||||
|                         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) | ||||
							
								
								
									
										122
									
								
								app/bybit/web_socket.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								app/bybit/web_socket.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | ||||
| 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() | ||||
							
								
								
									
										181
									
								
								app/helper_functions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								app/helper_functions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,181 @@ | ||||
| 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 | ||||
| ) -> 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. | ||||
|  | ||||
|     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) | ||||
|  | ||||
|         r_quantity = set_quantity | ||||
|  | ||||
|         total += r_quantity | ||||
|     return total | ||||
| @@ -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 = '''<b>Подключение Bybit аккаунта</b> | ||||
|      | ||||
| <b>1. Зарегистрируйтесь или войдите в свой аккаунт на Bybit (https://www.bybit.com/).</b> | ||||
| <b>2. В личном кабинете выберите раздел API. </b>   | ||||
| <b>3. Создание нового API ключа</b>   | ||||
|    - Нажмите кнопку Create New Key (Создать новый ключ). | ||||
|    - Выберите системно-сгенерированный ключ. | ||||
|    - Укажите название API ключа (любое).   | ||||
|    - Выберите права доступа для торговли (Trade).   | ||||
|    - Можно ограничить доступ по IP для безопасности. | ||||
| <b>4. Подтверждение создания</b>   | ||||
|    - Подтвердите создание ключа. | ||||
|    - Отправьте чат-роботу. | ||||
|  | ||||
| <b>Важно: сохраните отдельно API Key и Secret Key в надежном месте. Secret ключ отображается только один раз. </b>             | ||||
|     ''' | ||||
|  | ||||
|     await callback.message.answer(text=text, parse_mode='html', reply_markup=inline_markup.connect_bybit_api_markup) | ||||
|  | ||||
|     await callback.answer() | ||||
|  | ||||
|  | ||||
| @router_register_bybit_api.callback_query(F.data == 'clb_new_user_connect_bybit_api') | ||||
| async def add_api_key_message(callback: CallbackQuery, state: FSMContext) -> None: | ||||
|     """ | ||||
|     Инициирует процесс добавления API ключа. | ||||
|     Переводит пользователя в состояние ожидания ввода API Key. | ||||
|     """ | ||||
|     await state.set_state(state_reg_bybit_api.api_key) | ||||
|  | ||||
|     text = 'Отправьте KEY_API ниже: ' | ||||
|  | ||||
|     await callback.message.answer(text=text) | ||||
|  | ||||
|  | ||||
| @router_register_bybit_api.message(state_reg_bybit_api.api_key) | ||||
| async def add_api_key_and_message_for_secret_key(message: Message, state: FSMContext) -> None: | ||||
|     """ | ||||
|     Сохраняет API Key во временное состояние FSM, | ||||
|     затем запрашивает у пользователя ввод Secret Key. | ||||
|     """ | ||||
|     await state.update_data(api_key=message.text) | ||||
|  | ||||
|     text = 'Отправьте SECRET_KEY ниже' | ||||
|  | ||||
|     await message.answer(text=text) | ||||
|  | ||||
|     await state.set_state(state_reg_bybit_api.secret_key) | ||||
|  | ||||
|  | ||||
| @router_register_bybit_api.message(state_reg_bybit_api.secret_key) | ||||
| async def add_secret_key(message: Message, state: FSMContext) -> None: | ||||
|     """ | ||||
|     Сохраняет Secret Key и финализирует регистрацию, | ||||
|     обновляет базу данных, устанавливает символ пользователя и очищает состояние. | ||||
|     """ | ||||
|     await state.update_data(secret_key=message.text) | ||||
|  | ||||
|     data = await state.get_data() | ||||
|     user = await rq.check_user(message.from_user.id) | ||||
|  | ||||
|     await rq.upsert_api_keys(message.from_user.id, data['api_key'], data['secret_key']) | ||||
|     await rq.set_new_user_symbol(message.from_user.id) | ||||
|  | ||||
|     await state.clear() | ||||
|  | ||||
|     await message.answer('Данные добавлены.', | ||||
|                          reply_markup=reply_markup.base_buttons_markup) | ||||
|  | ||||
|     if user: | ||||
|         await start_bybit_trade_message(message) | ||||
|     else: | ||||
|         await rq.save_tg_id_new_user(message.from_user.id) | ||||
|  | ||||
|         await func_main_settings.reg_new_user_default_main_settings(message.from_user.id, message) | ||||
|         await func_rmanagement_settings.reg_new_user_default_risk_management_settings(message.from_user.id, | ||||
|                                                                                       message) | ||||
|         await func_condition_settings.reg_new_user_default_condition_settings(message.from_user.id) | ||||
|         await func_additional_settings.reg_new_user_default_additional_settings(message.from_user.id, message) | ||||
|         await start_bybit_trade_message(message) | ||||
| @@ -1,874 +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("execQty", 0)) | ||||
|     order_type = msg.get("orderType", "N/A") | ||||
|     side = msg.get("side", "") | ||||
|     commission = safe_float(msg.get("execFee", 0)) | ||||
|     pnl = safe_float(msg.get("execPnl", 0)) | ||||
|  | ||||
|     if commission_fee == "Да": | ||||
|         pnl -= commission | ||||
|  | ||||
|     movement = "" | ||||
|     if side.lower() == "buy": | ||||
|         movement = "Покупка" | ||||
|     elif side.lower() == "sell": | ||||
|         movement = "Продажа" | ||||
|     else: | ||||
|         movement = side | ||||
|  | ||||
|     if closed_size > 0: | ||||
|         return ( | ||||
|             f"Сделка закрыта:\n" | ||||
|             f"Торговая пара: {symbol}\n" | ||||
|             f"Цена исполнения: {entry_price:.6f}\n" | ||||
|             f"Количество: {qty}\n" | ||||
|             f"Закрыто позиций: {closed_size}\n" | ||||
|             f"Тип ордера: {order_type}\n" | ||||
|             f"Движение: {movement}\n" | ||||
|             f"Комиссия за сделку: {commission:.6f}\n" | ||||
|             f"Реализованная прибыль: {pnl:.6f} USDT" | ||||
|         ) | ||||
|     if order_type == "Market": | ||||
|         return ( | ||||
|             f"Сделка открыта:\n" | ||||
|             f"Торговая пара: {symbol}\n" | ||||
|             f"Цена исполнения: {entry_price:.6f}\n" | ||||
|             f"Количество: {qty}\n" | ||||
|             f"Тип ордера: {order_type}\n" | ||||
|             f"Движение: {movement}\n" | ||||
|             f"Комиссия за сделку: {commission:.6f}" | ||||
|         ) | ||||
|     return None | ||||
|  | ||||
|  | ||||
| def format_order_details_position(data): | ||||
|     """ | ||||
|     Форматирует информацию об ордере в виде строки. | ||||
|     """ | ||||
|     msg = data.get("data", [{}])[0] | ||||
|     price = safe_float(msg.get("price", 0)) | ||||
|     qty = safe_float(msg.get("qty", 0)) | ||||
|     cum_exec_qty = safe_float(msg.get("cumExecQty", 0)) | ||||
|     cum_exec_fee = safe_float(msg.get("cumExecFee", 0)) | ||||
|     take_profit = safe_float(msg.get("takeProfit", 0)) | ||||
|     stop_loss = safe_float(msg.get("stopLoss", 0)) | ||||
|     order_status = msg.get("orderStatus", "N/A") | ||||
|     symbol = msg.get("symbol", "N/A") | ||||
|     order_type = msg.get("orderType", "N/A") | ||||
|     side = msg.get("side", "") | ||||
|  | ||||
|     movement = "" | ||||
|     if side.lower() == "buy": | ||||
|         movement = "Покупка" | ||||
|     elif side.lower() == "sell": | ||||
|         movement = "Продажа" | ||||
|     else: | ||||
|         movement = side | ||||
|  | ||||
|     if order_status.lower() == "filled" and order_type.lower() == "limit": | ||||
|         text = ( | ||||
|             f"Ордер исполнен:\n" | ||||
|             f"Торговая пара: {symbol}\n" | ||||
|             f"Цена исполнения: {price:.6f}\n" | ||||
|             f"Количество: {qty}\n" | ||||
|             f"Исполнено позиций: {cum_exec_qty}\n" | ||||
|             f"Тип ордера: {order_type}\n" | ||||
|             f"Движение: {movement}\n" | ||||
|             f"Тейк-профит: {take_profit:.6f}\n" | ||||
|             f"Стоп-лосс: {stop_loss:.6f}\n" | ||||
|             f"Комиссия за сделку: {cum_exec_fee:.6f}\n" | ||||
|         ) | ||||
|         return text | ||||
|  | ||||
|     elif order_status.lower() == "new": | ||||
|         text = ( | ||||
|             f"Ордер создан:\n" | ||||
|             f"Торговая пара: {symbol}\n" | ||||
|             f"Цена: {price:.6f}\n" | ||||
|             f"Количество: {qty}\n" | ||||
|             f"Тип ордера: {order_type}\n" | ||||
|             f"Движение: {movement}\n" | ||||
|             f"Тейк-профит: {take_profit:.6f}\n" | ||||
|             f"Стоп-лосс: {stop_loss:.6f}\n" | ||||
|         ) | ||||
|         return text | ||||
|  | ||||
|     elif order_status.lower() == "cancelled": | ||||
|         text = ( | ||||
|             f"Ордер отменен:\n" | ||||
|             f"Торговая пара: {symbol}\n" | ||||
|             f"Цена: {price:.6f}\n" | ||||
|             f"Количество: {qty}\n" | ||||
|             f"Тип ордера: {order_type}\n" | ||||
|             f"Движение: {movement}\n" | ||||
|             f"Тейк-профит: {take_profit:.6f}\n" | ||||
|             f"Стоп-лосс: {stop_loss:.6f}\n" | ||||
|         ) | ||||
|         return text | ||||
|     return None | ||||
|  | ||||
|  | ||||
| def parse_pnl_from_msg(msg) -> float: | ||||
|     """ | ||||
|     Извлекает реализованную прибыль/убыток из сообщения. | ||||
|     """ | ||||
|     try: | ||||
|         data = msg.get("data", [{}])[0] | ||||
|         return float(data.get("execPnl", 0)) | ||||
|     except Exception as e: | ||||
|         logger.error("Ошибка при извлечении реализованной прибыли: %s", e) | ||||
|         return 0.0 | ||||
|  | ||||
|  | ||||
| async def calculate_total_budget(starting_quantity, martingale_factor, max_steps, commission_fee_percent, leverage, current_price): | ||||
|     """ | ||||
|     Вычисляет общий бюджет серии ставок с учётом цены пары, комиссии и кредитного плеча. | ||||
|  | ||||
|     Параметры: | ||||
|     - starting_quantity_usdt: стартовый размер ставки в долларах (USD) | ||||
|     - martingale_factor: множитель увеличения ставки при каждом проигрыше | ||||
|     - max_steps: максимальное количество шагов удвоения ставки | ||||
|     - commission_fee_percent: процент комиссии на одну операцию (открытие или закрытие) | ||||
|     - leverage: кредитное плечо | ||||
|     - current_price: текущая цена актива (например BTCUSDT) | ||||
|  | ||||
|     Возвращает: | ||||
|     - общий бюджет в долларах, который необходимо иметь на счету | ||||
|     """ | ||||
|     total = 0 | ||||
|     for step in range(max_steps): | ||||
|         quantity = starting_quantity * (martingale_factor ** step)  # размер ставки на текущем шаге в USDT | ||||
|  | ||||
|         # Переводим ставку из USDT в количество актива по текущей цене | ||||
|         quantity_in_asset = quantity / current_price | ||||
|  | ||||
|         # Учитываем комиссию за вход и выход (умножаем на 2) | ||||
|         quantity_with_fee = quantity * (1 + 2 * commission_fee_percent / 100) | ||||
|  | ||||
|         # Учитываем кредитное плечо - реальные собственные вложения меньше | ||||
|         effective_quantity = quantity_with_fee / leverage | ||||
|  | ||||
|         total += effective_quantity | ||||
|  | ||||
|     # Возвращаем бюджет в USDT | ||||
|     total_usdt = total * current_price | ||||
|     return total_usdt | ||||
|  | ||||
|  | ||||
| async def handle_execution_message(message, msg): | ||||
|     """ | ||||
|     Обработчик сообщений об исполнении сделки. | ||||
|     Логирует событие и проверяет условия для мартингейла и TP. | ||||
|     """ | ||||
|  | ||||
|     tg_id = message.from_user.id | ||||
|     data = msg.get("data", [{}])[0] | ||||
|     data_main_risk_stgs = await rq.get_user_risk_management_settings(tg_id) | ||||
|     commission_fee = data_main_risk_stgs.get("commission_fee", "ДА") | ||||
|     pnl = parse_pnl_from_msg(msg) | ||||
|     data_main_stgs = await rq.get_user_main_settings(tg_id) | ||||
|     symbol = data.get("symbol") | ||||
|     trading_mode = data_main_stgs.get("trading_mode", "Long") | ||||
|     trigger = await rq.get_for_registration_trigger(tg_id) | ||||
|     margin_mode = data_main_stgs.get("margin_type", "Isolated") | ||||
|     starting_quantity = safe_float(data_main_stgs.get("starting_quantity")) | ||||
|     martingale_factor = safe_float(data_main_stgs.get("martingale_factor")) | ||||
|     closed_size = safe_float(data.get("closedSize", 0)) | ||||
|     commission = safe_float(data.get("execFee", 0)) | ||||
|  | ||||
|     if commission_fee == "Да": | ||||
|         pnl -= commission | ||||
|  | ||||
|     trade_info = format_trade_details_position( | ||||
|         data=msg, | ||||
|         commission_fee=commission_fee | ||||
|     ) | ||||
|  | ||||
|     if trade_info: | ||||
|         await message.answer(f"{trade_info}", reply_markup=inline_markup.back_to_main) | ||||
|  | ||||
|     if closed_size == 0: | ||||
|         side = data.get("side", "") | ||||
|  | ||||
|         if side.lower() == "buy": | ||||
|             await rq.set_last_series_info(tg_id, last_side="Buy") | ||||
|         elif side.lower() == "sell": | ||||
|             await rq.set_last_series_info(tg_id, last_side="Sell") | ||||
|  | ||||
|     if trigger == "Автоматический" and closed_size > 0: | ||||
|         if pnl < 0: | ||||
|  | ||||
|             if trading_mode == 'Switch': | ||||
|                 side = data_main_stgs.get("last_side") | ||||
|             else: | ||||
|                 side = "Buy" if trading_mode == "Long" else "Sell" | ||||
|  | ||||
|             current_martingale = await rq.get_martingale_step(tg_id) | ||||
|             current_martingale_step = int(current_martingale) | ||||
|             current_martingale += 1 | ||||
|             next_quantity = float(starting_quantity) * ( | ||||
|                     float(martingale_factor) ** current_martingale_step | ||||
|             ) | ||||
|             await rq.update_martingale_step(tg_id, current_martingale) | ||||
|             await message.answer( | ||||
|                 f"❗️ Сделка закрылась в минус, открываю новую сделку с увеличенной ставкой.\n" | ||||
|             ) | ||||
|             await open_position( | ||||
|                 tg_id, | ||||
|                 message, | ||||
|                 side=side, | ||||
|                 margin_mode=margin_mode, | ||||
|                 symbol=symbol, | ||||
|                 quantity=next_quantity, | ||||
|             ) | ||||
|  | ||||
|         elif pnl > 0: | ||||
|             await rq.update_martingale_step(tg_id, 0) | ||||
|             await message.answer( | ||||
|                 "❗️ Прибыль достигнута, шаг мартингейла сброшен." | ||||
|             ) | ||||
|  | ||||
|  | ||||
| async def handle_order_message(message, msg: dict) -> None: | ||||
|     """ | ||||
|     Обработчик сообщений об исполнении ордера. | ||||
|     Логирует событие и проверяет условия для мартингейла и TP. | ||||
|     """ | ||||
|     # logger.info(f"Исполнен ордер:\n{json.dumps(msg, indent=4, ensure_ascii=False)}") | ||||
|  | ||||
|     trade_info = format_order_details_position(msg) | ||||
|  | ||||
|     if trade_info: | ||||
|         await message.answer(f"{trade_info}", reply_markup=inline_markup.back_to_main) | ||||
|  | ||||
|  | ||||
| async def error_max_step(message) -> None: | ||||
|     """ | ||||
|     Сообщение об ошибке превышения максимального количества шагов мартингейла. | ||||
|     """ | ||||
|     logger.error( | ||||
|         "Сделка не была совершена, превышен лимит максимального количества ставок в серии." | ||||
|     ) | ||||
|     await message.answer( | ||||
|         "Сделка не была совершена, превышен лимит максимального количества ставок в серии.", | ||||
|         reply_markup=inline_markup.back_to_main, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| async def error_max_risk(message) -> None: | ||||
|     """ | ||||
|     Сообщение об ошибке превышения риск-лимита сделки. | ||||
|     """ | ||||
|     logger.error("Сделка не была совершена, риск убытка превышает допустимый лимит.") | ||||
|     await message.answer( | ||||
|         "Сделка не была совершена, риск убытка превышает допустимый лимит.", | ||||
|         reply_markup=inline_markup.back_to_main, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| async def open_position( | ||||
|         tg_id, message, side: str, margin_mode: str, symbol, quantity, tpsl_mode="Full" | ||||
| ): | ||||
|     """ | ||||
|     Открывает позицию на Bybit с учётом настроек пользователя, маржи, размера лота, платформы и риска. | ||||
|  | ||||
|     Возвращает True при успехе, False при ошибках открытия ордера, None при исключениях. | ||||
|     """ | ||||
|     try: | ||||
|         client = await get_bybit_client(tg_id) | ||||
|         data_main_stgs = await rq.get_user_main_settings(tg_id) | ||||
|         order_type = data_main_stgs.get("entry_order_type") | ||||
|         bybit_margin_mode = ( | ||||
|             "ISOLATED_MARGIN" if margin_mode == "Isolated" else "REGULAR_MARGIN" | ||||
|         ) | ||||
|  | ||||
|         limit_price = None | ||||
|         if order_type == "Limit": | ||||
|             limit_price = await rq.get_limit_price(tg_id) | ||||
|         data_risk_stgs = await rq.get_user_risk_management_settings(tg_id) | ||||
|  | ||||
|         price = await price_symbol.get_price(tg_id, symbol=symbol) | ||||
|         entry_price = safe_float(price) | ||||
|         leverage = safe_float(data_main_stgs.get("size_leverage", 1)) | ||||
|  | ||||
|         max_martingale_steps = int(data_main_stgs.get("maximal_quantity", 0)) | ||||
|         current_martingale = await rq.get_martingale_step(tg_id) | ||||
|         max_risk_percent = safe_float(data_risk_stgs.get("max_risk_deal")) | ||||
|         loss_profit = safe_float(data_risk_stgs.get("price_loss")) | ||||
|         commission_fee = data_risk_stgs.get("commission_fee") | ||||
|         starting_quantity = safe_float(data_main_stgs.get('starting_quantity')) | ||||
|         martingale_factor = safe_float(data_main_stgs.get('martingale_factor')) | ||||
|         fee_info = client.get_fee_rates(category='linear', symbol=symbol) | ||||
|         instruments_resp = client.get_instruments_info(category="linear", symbol=symbol) | ||||
|         instrument = instruments_resp.get("result", {}).get("list", []) | ||||
|  | ||||
|         if commission_fee == "Да": | ||||
|             commission_fee_percent = safe_float(fee_info['result']['list'][0]['takerFeeRate']) | ||||
|         else: | ||||
|             commission_fee_percent = 0.0 | ||||
|  | ||||
|         total_budget = await calculate_total_budget( | ||||
|             starting_quantity=starting_quantity, | ||||
|             martingale_factor=martingale_factor, | ||||
|             max_steps=max_martingale_steps, | ||||
|             commission_fee_percent=commission_fee_percent, | ||||
|             leverage=leverage, | ||||
|             current_price=entry_price, | ||||
|         ) | ||||
|  | ||||
|         balance = await balance_g.get_balance(tg_id, message) | ||||
|         if safe_float(balance) < total_budget: | ||||
|             logger.error( | ||||
|                 f"Недостаточно средств для серии из {max_martingale_steps} шагов с текущими параметрами. " | ||||
|                 f"Требуемый бюджет: {total_budget:.2f} USDT, доступно: {balance} USDT." | ||||
|             ) | ||||
|             await message.answer( | ||||
|                 f"Недостаточно средств для серии из {max_martingale_steps} шагов с текущими параметрами. " | ||||
|                 f"Требуемый бюджет: {total_budget:.2f} USDT, доступно: {balance} USDT.", | ||||
|                 reply_markup=inline_markup.back_to_main, | ||||
|             ) | ||||
|             return | ||||
|  | ||||
|         if order_type == "Limit" and limit_price: | ||||
|             price_for_calc = limit_price | ||||
|         else: | ||||
|             price_for_calc = entry_price | ||||
|  | ||||
|         potential_loss = safe_float(quantity) * price_for_calc * (loss_profit / 100) | ||||
|         adjusted_loss = potential_loss / leverage | ||||
|         allowed_loss = safe_float(balance) * (max_risk_percent / 100) | ||||
|  | ||||
|         if adjusted_loss > allowed_loss: | ||||
|             await error_max_risk(message) | ||||
|             return | ||||
|  | ||||
|         if max_martingale_steps < current_martingale: | ||||
|             await error_max_step(message) | ||||
|             return | ||||
|  | ||||
|         client.set_margin_mode(setMarginMode=bybit_margin_mode) | ||||
|         max_leverage = safe_float(instrument[0].get("leverageFilter", {}).get("maxLeverage", 0)) | ||||
|  | ||||
|         if safe_float(leverage) > max_leverage: | ||||
|             await message.answer( | ||||
|                 f"Запрошенное кредитное плечо {leverage} превышает максимальное {max_leverage} для {symbol}. " | ||||
|                 f"Устанавливаю максимальное.", | ||||
|                 reply_markup=inline_markup.back_to_main, | ||||
|             ) | ||||
|             logger.info( | ||||
|                 f"Запрошенное кредитное плечо {leverage} превышает максимальное {max_leverage} для {symbol}. Устанавливаю максимальное.") | ||||
|             leverage_to_set = max_leverage | ||||
|         else: | ||||
|             leverage_to_set = safe_float(leverage) | ||||
|  | ||||
|         try: | ||||
|             client.set_leverage( | ||||
|                 category="linear", | ||||
|                 symbol=symbol, | ||||
|                 buyLeverage=str(leverage_to_set), | ||||
|                 sellLeverage=str(leverage_to_set), | ||||
|             ) | ||||
|             logger.info(f"Set leverage to {leverage_to_set} for {symbol}") | ||||
|         except exceptions.InvalidRequestError as e: | ||||
|             if "110043" in str(e): | ||||
|                 logger.info(f"Leverage already set to {leverage} for {symbol}") | ||||
|             else: | ||||
|                 raise e | ||||
|  | ||||
|         if instruments_resp.get("retCode") == 0: | ||||
|             instrument_info = instruments_resp.get("result", {}).get("list", []) | ||||
|             if instrument_info: | ||||
|                 instrument_info = instrument_info[0] | ||||
|                 min_notional_value = float(instrument_info.get("lotSizeFilter", {}).get("minNotionalValue", 0)) | ||||
|                 min_order_value = min_notional_value | ||||
|             else: | ||||
|                 min_order_value = 5.0 | ||||
|  | ||||
|         order_value = float(quantity) * price_for_calc | ||||
|         if order_value < min_order_value: | ||||
|             logger.error( | ||||
|                 f"Сумма ордера слишком мала: {order_value:.2f} USDT. " | ||||
|                 f"Минимум для торговли — {min_order_value} USDT. " | ||||
|                 f"Пожалуйста, увеличьте количество позиций." | ||||
|             ) | ||||
|             await message.answer( | ||||
|                 f"Сумма ордера слишком мала: {order_value:.2f} USDT. " | ||||
|                 f"Минимум для торговли — {min_order_value} USDT. " | ||||
|                 f"Пожалуйста, увеличьте количество позиций.", | ||||
|                 reply_markup=inline_markup.back_to_main, | ||||
|             ) | ||||
|             return False | ||||
|  | ||||
|         if bybit_margin_mode == "ISOLATED_MARGIN": | ||||
|             # Открываем позицию | ||||
|             response = client.place_order( | ||||
|                 category="linear", | ||||
|                 symbol=symbol, | ||||
|                 side=side, | ||||
|                 orderType=order_type, | ||||
|                 qty=str(quantity), | ||||
|                 price=( | ||||
|                     str(limit_price) if order_type == "Limit" and limit_price else None | ||||
|                 ), | ||||
|                 timeInForce="GTC", | ||||
|                 orderLinkId=f"deal_{symbol}_{int(time.time())}", | ||||
|             ) | ||||
|             if response.get("retCode", -1) != 0: | ||||
|                 logger.error(f"Ошибка открытия ордера: {response}") | ||||
|                 await message.answer( | ||||
|                     f"Ошибка открытия ордера", reply_markup=inline_markup.back_to_main | ||||
|                 ) | ||||
|                 return False | ||||
|  | ||||
|             # Получаем цену ликвидации | ||||
|             positions = client.get_positions(category="linear", symbol=symbol) | ||||
|             pos = positions.get("result", {}).get("list", [{}])[0] | ||||
|             avg_price = float(pos.get("avgPrice", 0)) | ||||
|             liq_price = safe_float(pos.get("liqPrice", 0)) | ||||
|  | ||||
|             if liq_price > 0 and avg_price > 0: | ||||
|                 if side.lower() == "buy": | ||||
|                     take_profit_price = avg_price + (avg_price - liq_price) | ||||
|                 else: | ||||
|                     take_profit_price = avg_price - (liq_price - avg_price) | ||||
|  | ||||
|                 take_profit_price = max(take_profit_price, 0) | ||||
|  | ||||
|                 try: | ||||
|                     try: | ||||
|                         client.set_tp_sl_mode( | ||||
|                             symbol=symbol, category="linear", tpSlMode="Full" | ||||
|                         ) | ||||
|                     except exceptions.InvalidRequestError as e: | ||||
|                         if "same tp sl mode" in str(e): | ||||
|                             logger.info("Режим TP/SL уже установлен - пропускаем") | ||||
|                         else: | ||||
|                             raise | ||||
|                     resp = client.set_trading_stop( | ||||
|                         category="linear", | ||||
|                         symbol=symbol, | ||||
|                         takeProfit=str(round(take_profit_price, 5)), | ||||
|                         tpTriggerBy="LastPrice", | ||||
|                         slTriggerBy="LastPrice", | ||||
|                         positionIdx=0, | ||||
|                         reduceOnly=False, | ||||
|                         tpslMode=tpsl_mode, | ||||
|                     ) | ||||
|                 except Exception as e: | ||||
|                     logger.error(f"Ошибка установки TP/SL: {e}") | ||||
|                     await message.answer( | ||||
|                         "Ошибка при установке Take Profit и Stop Loss.", | ||||
|                         reply_markup=inline_markup.back_to_main, | ||||
|                     ) | ||||
|                     return False | ||||
|             else: | ||||
|                 logger.warning("Не удалось получить цену ликвидации для позиции") | ||||
|  | ||||
|         else:  # REGULAR_MARGIN | ||||
|             try: | ||||
|                 client.set_tp_sl_mode(symbol=symbol, category="linear", tpSlMode="Full") | ||||
|             except exceptions.InvalidRequestError as e: | ||||
|                 if "same tp sl mode" in str(e): | ||||
|                     logger.info("Режим TP/SL уже установлен - пропускаем") | ||||
|                 else: | ||||
|                     raise | ||||
|  | ||||
|             if order_type == "Market": | ||||
|                 base_price = entry_price | ||||
|             else: | ||||
|                 base_price = limit_price | ||||
|  | ||||
|             if side.lower() == "buy": | ||||
|                 take_profit_price = base_price * (1 + loss_profit / 100) | ||||
|                 stop_loss_price = base_price * (1 - loss_profit / 100) | ||||
|             else: | ||||
|                 take_profit_price = base_price * (1 - loss_profit / 100) | ||||
|                 stop_loss_price = base_price * (1 + loss_profit / 100) | ||||
|  | ||||
|             take_profit_price = max(take_profit_price, 0) | ||||
|             stop_loss_price = max(stop_loss_price, 0) | ||||
|  | ||||
|             if tpsl_mode == "Full": | ||||
|                 tp_order_type = "Market" | ||||
|                 sl_order_type = "Market" | ||||
|                 tp_limit_price = None | ||||
|                 sl_limit_price = None | ||||
|             else:  # Partial | ||||
|                 tp_order_type = "Limit" | ||||
|                 sl_order_type = "Limit" | ||||
|                 tp_limit_price = take_profit_price | ||||
|                 sl_limit_price = stop_loss_price | ||||
|  | ||||
|             response = client.place_order( | ||||
|                 category="linear", | ||||
|                 symbol=symbol, | ||||
|                 side=side, | ||||
|                 orderType=order_type, | ||||
|                 qty=str(quantity), | ||||
|                 price=( | ||||
|                     str(limit_price) if order_type == "Limit" and limit_price else None | ||||
|                 ), | ||||
|                 takeProfit=str(take_profit_price), | ||||
|                 tpOrderType=tp_order_type, | ||||
|                 tpLimitPrice=str(tp_limit_price) if tp_limit_price else None, | ||||
|                 stopLoss=str(stop_loss_price), | ||||
|                 slOrderType=sl_order_type, | ||||
|                 slLimitPrice=str(sl_limit_price) if sl_limit_price else None, | ||||
|                 tpslMode=tpsl_mode, | ||||
|                 timeInForce="GTC", | ||||
|                 orderLinkId=f"deal_{symbol}_{int(time.time())}", | ||||
|             ) | ||||
|  | ||||
|             if response.get("retCode", -1) == 0: | ||||
|                 return True | ||||
|             else: | ||||
|                 logger.error(f"Ошибка открытия ордера: {response}") | ||||
|                 await message.answer( | ||||
|                     f"Ошибка открытия ордера", reply_markup=inline_markup.back_to_main | ||||
|                 ) | ||||
|                 return False | ||||
|  | ||||
|         return None | ||||
|     except exceptions.InvalidRequestError as e: | ||||
|         logger.error("InvalidRequestError: %s", e) | ||||
|         error_text = str(e) | ||||
|         if "estimated will trigger liq" in error_text: | ||||
|             await message.answer( | ||||
|                 "Лимитный ордер может вызвать мгновенную ликвидацию. Проверьте параметры ордера.", | ||||
|                 reply_markup=inline_markup.back_to_main, | ||||
|             ) | ||||
|         elif "ab not enough for new order" in error_text: | ||||
|             await message.answer("Недостаточно средств для нового ордера", | ||||
|                                  reply_markup=inline_markup.back_to_main) | ||||
|         else: | ||||
|             logger.error("Ошибка при совершении сделки: %s", e) | ||||
|             await message.answer( | ||||
|                 "Недостаточно средств для размещения нового ордера с заданным количеством и плечом.", | ||||
|                 reply_markup=inline_markup.back_to_main, | ||||
|             ) | ||||
|     except Exception as e: | ||||
|         logger.error("Ошибка при совершении сделки: %s", e) | ||||
|         await message.answer( | ||||
|             "Возникла ошибка при попытке открыть позицию.", | ||||
|             reply_markup=inline_markup.back_to_main, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| async def set_take_profit_stop_loss( | ||||
|         tg_id: int, | ||||
|         message, | ||||
|         take_profit_price: float, | ||||
|         stop_loss_price: float, | ||||
|         tpsl_mode="Full", | ||||
| ): | ||||
|     """ | ||||
|     Устанавливает уровни Take Profit и Stop Loss для открытой позиции. | ||||
|     """ | ||||
|     symbol = await rq.get_symbol(tg_id) | ||||
|     client = await get_bybit_client(tg_id) | ||||
|     await cancel_all_tp_sl_orders(tg_id, symbol) | ||||
|  | ||||
|     try: | ||||
|         try: | ||||
|             client.set_tp_sl_mode(symbol=symbol, category="linear", tpSlMode=tpsl_mode) | ||||
|         except exceptions.InvalidRequestError as e: | ||||
|             if "same tp sl mode" in str(e).lower(): | ||||
|                 logger.info("Режим TP/SL уже установлен для %s - пропускаем", symbol) | ||||
|             else: | ||||
|                 raise | ||||
|  | ||||
|         resp = client.set_trading_stop( | ||||
|             category="linear", | ||||
|             symbol=symbol, | ||||
|             takeProfit=str(round(take_profit_price, 5)), | ||||
|             stopLoss=str(round(stop_loss_price, 5)), | ||||
|             tpTriggerBy="LastPrice", | ||||
|             slTriggerBy="LastPrice", | ||||
|             positionIdx=0, | ||||
|             reduceOnly=False, | ||||
|             tpslMode=tpsl_mode, | ||||
|         ) | ||||
|  | ||||
|         if resp.get("retCode") != 0: | ||||
|             await message.answer( | ||||
|                 f"Ошибка обновления TP/SL: {resp.get('retMsg')}", | ||||
|                 reply_markup=inline_markup.back_to_main, | ||||
|             ) | ||||
|             return | ||||
|  | ||||
|         await message.answer( | ||||
|             f"ТП и СЛ успешно установлены:\nТейк-профит: {take_profit_price:.5f}\nСтоп-лосс: {stop_loss_price:.5f}", | ||||
|             reply_markup=inline_markup.back_to_main, | ||||
|         ) | ||||
|     except Exception as e: | ||||
|         logger.error(f"Ошибка установки TP/SL для {symbol}: {e}", exc_info=True) | ||||
|         await message.answer( | ||||
|             "Произошла ошибка при установке TP и SL.", | ||||
|             reply_markup=inline_markup.back_to_main, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| async def cancel_all_tp_sl_orders(tg_id, symbol): | ||||
|     """ | ||||
|     Отменяет лимитные ордера для указанного символа. | ||||
|     """ | ||||
|     client = await get_bybit_client(tg_id) | ||||
|     last_response = None | ||||
|     try: | ||||
|         orders_resp = client.get_open_orders(category="linear", symbol=symbol) | ||||
|         orders = orders_resp.get("result", {}).get("list", []) | ||||
|  | ||||
|         for order in orders: | ||||
|             order_id = order.get("orderId") | ||||
|             order_symbol = order.get("symbol") | ||||
|             cancel_resp = client.cancel_order( | ||||
|                 category="linear", symbol=symbol, orderId=order_id | ||||
|             ) | ||||
|             if cancel_resp.get("retCode") != 0: | ||||
|                 logger.warning( | ||||
|                     f"Не удалось отменить ордер {order_id}: {cancel_resp.get('retMsg')}" | ||||
|                 ) | ||||
|             else: | ||||
|                 last_response = order_symbol | ||||
|     except Exception as e: | ||||
|         logger.error(f"Ошибка при отмене ордера: {e}") | ||||
|  | ||||
|     return last_response | ||||
|  | ||||
|  | ||||
| async def get_active_positions(tg_id, message): | ||||
|     """ | ||||
|     Показывает активные позиции пользователя. | ||||
|     """ | ||||
|     client = await get_bybit_client(tg_id) | ||||
|     active_positions = client.get_positions(category="linear", settleCoin="USDT") | ||||
|     positions = active_positions.get("result", {}).get("list", []) | ||||
|     active_symbols = [ | ||||
|         pos.get("symbol") for pos in positions if float(pos.get("size", 0)) > 0 | ||||
|     ] | ||||
|  | ||||
|     if active_symbols: | ||||
|         await message.answer( | ||||
|             "📈 Ваши активные позиции:", | ||||
|             reply_markup=inline_markup.create_trades_inline_keyboard(active_symbols), | ||||
|         ) | ||||
|     else: | ||||
|         await message.answer( | ||||
|             "❗️ У вас нет активных позиций.", reply_markup=inline_markup.back_to_main | ||||
|         ) | ||||
|         return | ||||
|  | ||||
|  | ||||
| async def get_active_positions_by_symbol(tg_id, symbol, message): | ||||
|     """ | ||||
|     Показывает активные позиции пользователя по символу. | ||||
|     """ | ||||
|     client = await get_bybit_client(tg_id) | ||||
|     active_positions = client.get_positions(category="linear", symbol=symbol) | ||||
|     positions = active_positions.get("result", {}).get("list", []) | ||||
|     pos = positions[0] if positions else None | ||||
|  | ||||
|     if float(pos.get("size", 0)) == 0: | ||||
|         await message.answer( | ||||
|             "❗️ У вас нет активных позиций.", reply_markup=inline_markup.back_to_main | ||||
|         ) | ||||
|         return | ||||
|  | ||||
|     text = ( | ||||
|         f"Торговая пара: {pos.get('symbol')}\n" | ||||
|         f"Цена входа: {pos.get('avgPrice')}\n" | ||||
|         f"Движение: {pos.get('side')}\n" | ||||
|         f"Кредитное плечо: {pos.get('leverage')}x\n" | ||||
|         f"Количество: {pos.get('size')}\n" | ||||
|         f"Тейк-профит: {pos.get('takeProfit')}\n" | ||||
|         f"Стоп-лосс: {pos.get('stopLoss')}\n" | ||||
|     ) | ||||
|  | ||||
|     await message.answer( | ||||
|         text, reply_markup=inline_markup.create_close_deal_markup(symbol) | ||||
|     ) | ||||
|  | ||||
|  | ||||
| async def get_active_orders(tg_id, message): | ||||
|     """ | ||||
|     Показывает активные лимитные ордера пользователя. | ||||
|     """ | ||||
|     client = await get_bybit_client(tg_id) | ||||
|     response = client.get_open_orders( | ||||
|         category="linear", settleCoin="USDT", orderType="Limit" | ||||
|     ) | ||||
|     orders = response.get("result", {}).get("list", []) | ||||
|     limit_orders = [order for order in orders if order.get("orderType") == "Limit"] | ||||
|  | ||||
|     if limit_orders: | ||||
|         symbols = [order["symbol"] for order in limit_orders] | ||||
|         await message.answer( | ||||
|             "📈 Ваши активные лимитные ордера:", | ||||
|             reply_markup=inline_markup.create_trades_inline_keyboard_limits(symbols), | ||||
|         ) | ||||
|     else: | ||||
|         await message.answer( | ||||
|             "❗️ У вас нет активных лимитных ордеров.", | ||||
|             reply_markup=inline_markup.back_to_main, | ||||
|         ) | ||||
|         return | ||||
|  | ||||
|  | ||||
| async def get_active_orders_by_symbol(tg_id, symbol, message): | ||||
|     """ | ||||
|     Показывает активные лимитные ордера пользователя по символу. | ||||
|     """ | ||||
|     client = await get_bybit_client(tg_id) | ||||
|     active_orders = client.get_open_orders(category="linear", symbol=symbol) | ||||
|     limit_orders = [ | ||||
|         order | ||||
|         for order in active_orders.get("result", {}).get("list", []) | ||||
|         if order.get("orderType") == "Limit" | ||||
|     ] | ||||
|  | ||||
|     if not limit_orders: | ||||
|         await message.answer( | ||||
|             "Нет активных лимитных ордеров по данной торговой паре.", | ||||
|             reply_markup=inline_markup.back_to_main, | ||||
|         ) | ||||
|         return | ||||
|  | ||||
|     texts = [] | ||||
|     for order in limit_orders: | ||||
|         text = ( | ||||
|             f"Торговая пара: {order.get('symbol')}\n" | ||||
|             f"Тип ордера: {order.get('orderType')}\n" | ||||
|             f"Сторона: {order.get('side')}\n" | ||||
|             f"Цена: {order.get('price')}\n" | ||||
|             f"Количество: {order.get('qty')}\n" | ||||
|             f"Тейк-профит: {order.get('takeProfit')}\n" | ||||
|             f"Стоп-лосс: {order.get('stopLoss')}\n" | ||||
|         ) | ||||
|         texts.append(text) | ||||
|  | ||||
|     await message.answer( | ||||
|         "\n\n".join(texts), reply_markup=inline_markup.create_close_limit_markup(symbol) | ||||
|     ) | ||||
|  | ||||
|  | ||||
| async def close_user_trade(tg_id: int, symbol: str): | ||||
|     """ | ||||
|     Закрывает открытые позиции пользователя по символу рыночным ордером. | ||||
|     Возвращает True при успехе, False при ошибках. | ||||
|     """ | ||||
|     try: | ||||
|         client = await get_bybit_client(tg_id) | ||||
|         positions_resp = client.get_positions(category="linear", symbol=symbol) | ||||
|  | ||||
|         if positions_resp.get("retCode") != 0: | ||||
|             return False | ||||
|         positions_list = positions_resp.get("result", {}).get("list", []) | ||||
|         if not positions_list: | ||||
|             return False | ||||
|  | ||||
|         position = positions_list[0] | ||||
|         qty = abs(safe_float(position.get("size"))) | ||||
|         side = position.get("side") | ||||
|         if qty == 0: | ||||
|             return False | ||||
|  | ||||
|         close_side = "Sell" if side == "Buy" else "Buy" | ||||
|  | ||||
|         place_resp = client.place_order( | ||||
|             category="linear", | ||||
|             symbol=symbol, | ||||
|             side=close_side, | ||||
|             orderType="Market", | ||||
|             qty=str(qty), | ||||
|             timeInForce="GTC", | ||||
|             reduceOnly=True, | ||||
|         ) | ||||
|  | ||||
|         if place_resp.get("retCode") == 0: | ||||
|             return True | ||||
|         else: | ||||
|             return False | ||||
|     except Exception as e: | ||||
|         logger.error( | ||||
|             f"Ошибка закрытия сделки {symbol} для пользователя {tg_id}: {e}", | ||||
|             exc_info=True, | ||||
|         ) | ||||
|         return False | ||||
|  | ||||
|  | ||||
| async def close_trade_after_delay(tg_id: int, message, symbol: str, delay_sec: int): | ||||
|     """ | ||||
|     Закрывает сделку пользователя после задержки delay_sec секунд. | ||||
|     """ | ||||
|     try: | ||||
|         await asyncio.sleep(delay_sec) | ||||
|         result = await close_user_trade(tg_id, symbol) | ||||
|         if result: | ||||
|             await message.answer( | ||||
|                 f"Сделка {symbol} успешно закрыта по таймеру." | ||||
|             ) | ||||
|             logger.info(f"Сделка {symbol} успешно закрыта по таймеру.") | ||||
|         else: | ||||
|             await message.answer( | ||||
|                 f"Не удалось закрыть сделку {symbol} по таймеру.", | ||||
|                 reply_markup=inline_markup.back_to_main, | ||||
|             ) | ||||
|             logger.error(f"Не удалось закрыть сделку {symbol} по таймеру.") | ||||
|     except asyncio.CancelledError: | ||||
|         await message.answer( | ||||
|             f"Закрытие сделки {symbol} по таймеру отменено.", | ||||
|             reply_markup=inline_markup.back_to_main, | ||||
|         ) | ||||
|         logger.info(f"Закрытие сделки {symbol} по таймеру отменено.") | ||||
| @@ -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 | ||||
| @@ -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) | ||||
| @@ -1,562 +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 | ||||
|  | ||||
| from app.states.States import (state_update_entry_type, state_update_symbol, state_limit_price, | ||||
|                                SetTP_SL_State, CloseTradeTimerState) | ||||
| from aiogram.fsm.context import FSMContext | ||||
|  | ||||
| from app.services.Bybit.functions.get_valid_symbol import get_valid_symbols | ||||
|  | ||||
| logging.config.dictConfig(LOGGING_CONFIG) | ||||
| logger = logging.getLogger("functions") | ||||
|  | ||||
| router_functions_bybit_trade = Router() | ||||
|  | ||||
| user_trade_tasks = {} | ||||
|  | ||||
|  | ||||
| @router_functions_bybit_trade.callback_query(F.data.in_(['clb_start_trading', 'clb_back_to_main', 'back_to_main'])) | ||||
| async def clb_start_bybit_trade_message(callback: CallbackQuery) -> None: | ||||
|     """ | ||||
|     Обработка нажатия кнопок запуска торговли или возврата в главное меню. | ||||
|     Отправляет информацию о балансе, символе, цене и инструкциях по торговле. | ||||
|     """ | ||||
|     user_id = callback.from_user.id | ||||
|     balance = await get_balance(user_id, callback.message) | ||||
|  | ||||
|     if balance: | ||||
|         symbol = await rq.get_symbol(user_id) | ||||
|         price = await get_price(user_id, symbol=symbol) | ||||
|  | ||||
|         text = ( | ||||
|             f"💎 Торговля на Bybit\n\n" | ||||
|             f"⚖️ Ваш баланс (USDT): {float(balance):.2f}\n" | ||||
|             f"📊 Текущая торговая пара: {symbol}\n" | ||||
|             f"$$$ Цена: {price}\n\n" | ||||
|             "Как начать торговлю?\n\n" | ||||
|             "1️⃣ Проверьте и тщательно настройте все параметры в вашем профиле.\n" | ||||
|             "2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).\n" | ||||
|             "3️⃣ Нажмите кнопку 'Начать торговать'.\n" | ||||
|         ) | ||||
|         await callback.message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup) | ||||
|  | ||||
|  | ||||
| async def start_bybit_trade_message(message: Message) -> None: | ||||
|     """ | ||||
|     Отправляет пользователю информацию о балансе, символе и текущей цене, | ||||
|     вместе с инструкциями по началу торговли. | ||||
|     """ | ||||
|     balance = await get_balance(message.from_user.id, message) | ||||
|     tg_id = message.from_user.id | ||||
|  | ||||
|     if balance: | ||||
|         await run_ws_for_user(tg_id, message) | ||||
|         symbol = await rq.get_symbol(message.from_user.id) | ||||
|         price = await get_price(message.from_user.id, symbol=symbol) | ||||
|  | ||||
|         text = ( | ||||
|             f"💎 Торговля на Bybit\n\n" | ||||
|             f"⚖️ Ваш баланс (USDT): {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_update_entry_type') | ||||
| async def update_entry_type_message(callback: CallbackQuery, state: FSMContext) -> None: | ||||
|     """ | ||||
|     Запрашивает у пользователя тип входа в позицию (Market или Limit). | ||||
|     """ | ||||
|     await state.set_state(state_update_entry_type.entry_type) | ||||
|     await callback.message.answer("Выберите тип входа в позицию:", reply_markup=inline_markup.entry_order_type_markup) | ||||
|     await callback.answer() | ||||
|  | ||||
|  | ||||
| @router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith('entry_order_type:')) | ||||
| async def entry_order_type_callback(callback: CallbackQuery, state: FSMContext) -> None: | ||||
|     """ | ||||
|     Обработка выбора типа входа в позицию. | ||||
|     Если Limit, запрашивает цену лимитного ордера. | ||||
|     Если Market — обновляет настройки. | ||||
|     """ | ||||
|     order_type = callback.data.split(':')[1] | ||||
|  | ||||
|     if order_type not in ['Market', 'Limit']: | ||||
|         await callback.answer("Ошибка выбора", show_alert=True) | ||||
|         return | ||||
|  | ||||
|     if order_type == 'Limit': | ||||
|         await state.set_state(state_limit_price.price) | ||||
|         await callback.message.answer("Введите цену лимитного ордера:", reply_markup=inline_markup.cancel) | ||||
|         await callback.answer() | ||||
|         return | ||||
|  | ||||
|     try: | ||||
|         await state.update_data(entry_order_type=order_type) | ||||
|         await rq.update_entry_order_type(callback.from_user.id, order_type) | ||||
|         await callback.message.answer(f"Выбран тип входа в позицию: {order_type}", | ||||
|                                       reply_markup=inline_markup.start_trading_markup) | ||||
|         await callback.answer() | ||||
|     except Exception as e: | ||||
|         logger.error("Произошла ошибка при обновлении типа входа в позицию: %s", e) | ||||
|         await callback.message.answer("Произошла ошибка при обновлении типа входа в позицию", | ||||
|                                       reply_markup=inline_markup.back_to_main) | ||||
|     await state.clear() | ||||
|  | ||||
|  | ||||
| @router_functions_bybit_trade.message(state_limit_price.price) | ||||
| async def set_limit_price(message: Message, state: FSMContext) -> None: | ||||
|     """ | ||||
|     Обрабатывает ввод цены лимитного ордера, проверяет формат и сохраняет настройки. | ||||
|     """ | ||||
|     try: | ||||
|         price = float(message.text) | ||||
|         if price <= 0: | ||||
|             await message.answer("Цена должна быть положительным числом. Попробуйте снова.", | ||||
|                                  reply_markup=inline_markup.cancel) | ||||
|             return | ||||
|     except ValueError: | ||||
|         await message.answer("Некорректный формат цены. Введите число.", reply_markup=inline_markup.cancel) | ||||
|         return | ||||
|  | ||||
|     await state.update_data(entry_order_type='Limit', limit_price=price) | ||||
|  | ||||
|     await rq.update_entry_order_type(message.from_user.id, 'Limit') | ||||
|     await rq.update_limit_price(message.from_user.id, price) | ||||
|  | ||||
|     await message.answer(f"Цена лимитного ордера установлена: {price}", reply_markup=inline_markup.start_trading_markup) | ||||
|     await state.clear() | ||||
|  | ||||
|  | ||||
| @router_functions_bybit_trade.callback_query(F.data == "clb_start_chatbot_trading") | ||||
| async def start_trading_process(callback: CallbackQuery) -> None: | ||||
|     """ | ||||
|     Запускает торговый цикл в выбранном режиме Long/Short. | ||||
|     Проверяет API-ключи, режим торговли, маржинальный режим и открытые позиции, | ||||
|     затем запускает торговый цикл с задержкой или без неё. | ||||
|     """ | ||||
|     await callback.answer() | ||||
|     tg_id = callback.from_user.id | ||||
|     message = callback.message | ||||
|     data_main_stgs = await rq.get_user_main_settings(tg_id) | ||||
|     symbol = await rq.get_symbol(tg_id) | ||||
|     margin_mode = data_main_stgs.get('margin_type', 'Isolated') | ||||
|     trading_mode = data_main_stgs.get('trading_mode') | ||||
|     starting_quantity = safe_float(data_main_stgs.get('starting_quantity')) | ||||
|     switch_state = data_main_stgs.get("switch_state", "По направлению") | ||||
|  | ||||
|     if trading_mode == 'Switch': | ||||
|         if switch_state == "По направлению": | ||||
|             side = data_main_stgs.get("last_side") | ||||
|         else: | ||||
|             side = data_main_stgs.get("last_side") | ||||
|             if side.lower() == "buy": | ||||
|                 side = "Sell" | ||||
|             else: | ||||
|                 side = "Buy" | ||||
|     else: | ||||
|         if trading_mode == 'Long': | ||||
|             side = 'Buy' | ||||
|         elif trading_mode == 'Short': | ||||
|             side = 'Sell' | ||||
|         else: | ||||
|             await message.answer(f"Режим торговли '{trading_mode}' пока не поддерживается.", | ||||
|                                  reply_markup=inline_markup.back_to_main) | ||||
|             return | ||||
|  | ||||
|     await message.answer("Начинаю торговлю с использованием текущих настроек...") | ||||
|  | ||||
|     timer_data = await rq.get_user_timer(tg_id) | ||||
|     if isinstance(timer_data, dict): | ||||
|         timer_minute = timer_data.get('timer_minutes', 0) | ||||
|     else: | ||||
|         timer_minute = timer_data or 0 | ||||
|  | ||||
|     if timer_minute > 0: | ||||
|         await message.answer(f"Торговля начнётся через {timer_minute} мин.", reply_markup=inline_markup.cancel_start) | ||||
|  | ||||
|         async def delay_start(): | ||||
|             try: | ||||
|                 await asyncio.sleep(timer_minute * 60) | ||||
|                 await open_position(tg_id, message, side, margin_mode, symbol=symbol, quantity=starting_quantity) | ||||
|                 await rq.update_user_timer(tg_id, minutes=0) | ||||
|             except asyncio.exceptions.CancelledError: | ||||
|                 logger.exception(f"Торговый цикл для пользователя {tg_id} был отменён.") | ||||
|                 raise | ||||
|  | ||||
|         task = asyncio.create_task(delay_start()) | ||||
|         user_trade_tasks[tg_id] = task | ||||
|     else: | ||||
|         await open_position(tg_id, message, side, margin_mode, symbol=symbol, quantity=starting_quantity) | ||||
|  | ||||
|  | ||||
| @router_functions_bybit_trade.callback_query(F.data == "clb_cancel_start") | ||||
| async def cancel_start_trading(callback: CallbackQuery): | ||||
|     tg_id = callback.from_user.id | ||||
|     task = user_trade_tasks.get(tg_id) | ||||
|     if task and not task.done(): | ||||
|         task.cancel() | ||||
|         try: | ||||
|             await task | ||||
|         except asyncio.CancelledError: | ||||
|             pass | ||||
|         user_trade_tasks.pop(tg_id, None) | ||||
|         await rq.update_user_timer(tg_id, minutes=0) | ||||
|         await callback.message.answer("Запуск торговли отменён.", reply_markup=inline_markup.back_to_main) | ||||
|         await callback.message.edit_reply_markup(reply_markup=None) | ||||
|     else: | ||||
|         await callback.answer("Нет запланированной задачи запуска.", show_alert=True) | ||||
|  | ||||
|  | ||||
| @router_functions_bybit_trade.callback_query(F.data == "clb_my_deals") | ||||
| async def show_my_trades(callback: CallbackQuery) -> None: | ||||
|     """ | ||||
|     Отображает пользователю выбор типа сделки по текущей торговой паре. | ||||
|     """ | ||||
|     await callback.answer() | ||||
|     try: | ||||
|         await callback.message.answer("Выберите тип сделки:", | ||||
|                                       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 | ||||
|  | ||||
|     await rq.update_trigger(tg_id, "Ручной") | ||||
|     await callback.message.answer("Автоматическая торговля остановлена.", reply_markup=inline_markup.back_to_main) | ||||
|     await callback.answer() | ||||
|  | ||||
|  | ||||
| @router_functions_bybit_trade.callback_query(F.data == "stop_with_timer") | ||||
| async def stop_with_timer_start(callback: CallbackQuery, state: FSMContext): | ||||
|     """ | ||||
|     Запускает диалог с пользователем для задания задержки до остановки торговли. | ||||
|     """ | ||||
|  | ||||
|     await state.set_state(CloseTradeTimerState.waiting_for_trade) | ||||
|     await callback.message.answer("Введите задержку в минутах до остановки торговли:", | ||||
|                                   reply_markup=inline_markup.cancel) | ||||
|     await callback.answer() | ||||
|  | ||||
|  | ||||
| @router_functions_bybit_trade.message(CloseTradeTimerState.waiting_for_trade) | ||||
| async def process_stop_delay(message: Message, state: FSMContext): | ||||
|     """ | ||||
|     Обрабатывает ввод задержки и запускает задачу остановки торговли с задержкой. | ||||
|     """ | ||||
|     try: | ||||
|         delay_minutes = int(message.text.strip()) | ||||
|         if delay_minutes <= 0: | ||||
|             await message.answer("Введите положительное число минут.") | ||||
|             return | ||||
|     except ValueError: | ||||
|         await message.answer("Некорректный формат. Введите число в минутах.") | ||||
|         return | ||||
|  | ||||
|     tg_id = message.from_user.id | ||||
|     delay_seconds = delay_minutes * 60 | ||||
|  | ||||
|     await message.answer(f"Торговля будет остановлена через {delay_minutes} минут.", | ||||
|                          reply_markup=inline_markup.back_to_main) | ||||
|     await asyncio.sleep(delay_seconds) | ||||
|     await rq.update_trigger(tg_id, "Ручной") | ||||
|     await message.answer("Автоматическая торговля остановлена.", reply_markup=inline_markup.back_to_main) | ||||
|  | ||||
|     await state.clear() | ||||
|  | ||||
|  | ||||
| @router_functions_bybit_trade.callback_query(F.data == "clb_cancel") | ||||
| async def cancel(callback: CallbackQuery, state: FSMContext) -> None: | ||||
|     """ | ||||
|     Отменяет текущее состояние FSM и сообщает пользователю об отмене. | ||||
|     """ | ||||
|     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) | ||||
| @@ -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 | ||||
| @@ -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 | ||||
| @@ -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 | ||||
| @@ -1,69 +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 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() | ||||
| @@ -1,217 +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_update_entry_type')], | ||||
|     [InlineKeyboardButton(text="Остановить торговлю", callback_data='clb_stop_trading')], | ||||
| ]) | ||||
|  | ||||
| start_trading_markup = InlineKeyboardMarkup(inline_keyboard=[ | ||||
|     [InlineKeyboardButton(text="Начать торговлю", callback_data="clb_start_chatbot_trading")], | ||||
|     [InlineKeyboardButton(text="На главную", callback_data='back_to_main')], | ||||
| ]) | ||||
|  | ||||
| cancel = InlineKeyboardMarkup(inline_keyboard=[ | ||||
|     [InlineKeyboardButton(text="Отменить", callback_data="clb_cancel")] | ||||
| ]) | ||||
|  | ||||
| entry_order_type_markup = InlineKeyboardMarkup( | ||||
|     inline_keyboard=[ | ||||
|         [ | ||||
|             InlineKeyboardButton(text="Market (текущая цена)", callback_data="entry_order_type:Market"), | ||||
|             InlineKeyboardButton(text="Limit (фиксированная цена)", callback_data="entry_order_type:Limit"), | ||||
|         ], back_btn_to_main | ||||
|     ] | ||||
| ) | ||||
|  | ||||
|  | ||||
|  | ||||
| back_to_main = InlineKeyboardMarkup(inline_keyboard=[ | ||||
|     [InlineKeyboardButton(text="На главную", callback_data='back_to_main')], | ||||
| ]) | ||||
|  | ||||
| main_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ | ||||
|     [InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_trading_mode'), | ||||
|      InlineKeyboardButton(text='Состояние свитча', callback_data='clb_change_switch_state'), | ||||
|      InlineKeyboardButton(text='Тип маржи', callback_data='clb_change_margin_type')], | ||||
|  | ||||
|     [InlineKeyboardButton(text='Размер кредитного плеча', callback_data='clb_change_size_leverage'), | ||||
|      InlineKeyboardButton(text='Начальная ставка', callback_data='clb_change_starting_quantity')], | ||||
|  | ||||
|     [InlineKeyboardButton(text='Коэффициент Мартингейла', callback_data='clb_change_martingale_factor'), | ||||
|      InlineKeyboardButton(text='Сбросить шаги Мартингейла', callback_data='clb_change_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_mode'), | ||||
|      InlineKeyboardButton(text='Таймер', callback_data='clb_change_timer')], | ||||
|     # | ||||
|     # [InlineKeyboardButton(text='Фильтр волатильности', callback_data='clb_change_filter_volatility'), | ||||
|     #  InlineKeyboardButton(text='Внешние сигналы', callback_data='clb_change_external_cues')], | ||||
|     # | ||||
|     # [InlineKeyboardButton(text='Сигналы TradingView', callback_data='clb_change_tradingview_cues'), | ||||
|     #  InlineKeyboardButton(text='Webhook URL', callback_data='clb_change_webhook')], | ||||
|     # | ||||
|     # [InlineKeyboardButton(text='AI - аналитика', callback_data='clb_change_ai_analytics')], | ||||
|  | ||||
|     back_btn_list_settings, | ||||
|     back_btn_to_main | ||||
| ]) | ||||
|  | ||||
| additional_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ | ||||
|     [InlineKeyboardButton(text='Сохранить шаблон', callback_data='clb_change_save_pattern'), | ||||
|      InlineKeyboardButton(text='Автозапуск', callback_data='clb_change_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")], | ||||
| ]) | ||||
| @@ -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) | ||||
							
								
								
									
										0
									
								
								app/telegram/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/telegram/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -1,306 +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:///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) | ||||
|     martingale_factor = mapped_column(Integer(), default=1) | ||||
|     martingale_step = mapped_column(Integer(), default=1) | ||||
|     maximal_quantity = mapped_column(Integer(), default=10) | ||||
|     entry_order_type = mapped_column(String(10), default='Market') | ||||
|     limit_order_price = mapped_column(Numeric(18, 15), nullable=True) | ||||
|     last_side = mapped_column(String(10), default='Buy') | ||||
|  | ||||
|  | ||||
| class User_Risk_Management_Settings(Base): | ||||
|     """ | ||||
|     Настройки управления рисками пользователя. | ||||
|  | ||||
|     Атрибуты: | ||||
|         id (int): Первичный ключ. | ||||
|         tg_id (int): Внешний ключ на Telegram пользователя. | ||||
|         price_profit (int): Процент прибыли для трейда. | ||||
|         price_loss (int): Процент убытка для трейда. | ||||
|         max_risk_deal (int): Максимально допустимый риск по сделке в процентах. | ||||
|         commission_fee (str): Учитывать ли комиссию в расчетах ("Да"/"Нет"). | ||||
|     """ | ||||
|     __tablename__ = 'user_risk_management_settings' | ||||
|  | ||||
|     id: Mapped[int] = mapped_column(primary_key=True) | ||||
|  | ||||
|     tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), 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)) | ||||
| @@ -1,585 +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, | ||||
|                 'martingale_step': user.martingale_step, | ||||
|                 'last_side': user.last_side, | ||||
|             } | ||||
|             return data | ||||
|  | ||||
|  | ||||
| async def get_user_risk_management_settings(tg_id): | ||||
|     """Получить риск-менеджмента настройки пользователя.""" | ||||
|     async with async_session() as session: | ||||
|         user = await session.scalar(select(URMS).where(URMS.tg_id == tg_id)) | ||||
|  | ||||
|         if user: | ||||
|             logger.info("Получение риск-менеджмента настроек пользователя %s", tg_id) | ||||
|  | ||||
|             price_profit = await session.scalar(select(URMS.price_profit).where(URMS.tg_id == tg_id)) | ||||
|             price_loss = await session.scalar(select(URMS.price_loss).where(URMS.tg_id == tg_id)) | ||||
|             max_risk_deal = await session.scalar(select(URMS.max_risk_deal).where(URMS.tg_id == tg_id)) | ||||
|             commission_fee = await session.scalar(select(URMS.commission_fee).where(URMS.tg_id == tg_id)) | ||||
|  | ||||
|             data = { | ||||
|                 'price_profit': price_profit, | ||||
|                 'price_loss': price_loss, | ||||
|                 'max_risk_deal': max_risk_deal, | ||||
|                 'commission_fee': commission_fee, | ||||
|             } | ||||
|  | ||||
|             return data | ||||
|  | ||||
|  | ||||
| async def update_margin_type(tg_id, margin_type) -> None: | ||||
|     """Обновить тип маржи пользователя.""" | ||||
|     async with async_session() as session: | ||||
|         type = await session.scalar(select(Margin_type.type).where(Margin_type.type == margin_type)) | ||||
|  | ||||
|         if type: | ||||
|             logger.info("Изменен тип маржи %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_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 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() | ||||
| @@ -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 = '''<b>Дополнительные параметры</b> | ||||
|  | ||||
| <b>- Сохранить как шаблон стратегии:</b> да / нет   | ||||
| <b>- Автозапуск после сохранения:</b> да / нет   | ||||
| <b>- Уведомления в Telegram:</b> включено / отключено ''' | ||||
|  | ||||
|     await message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.additional_settings_markup) | ||||
|  | ||||
| async def save_pattern_message(message, state): | ||||
|     text = '''<b>Сохранение шаблона</b> | ||||
|                                                                                            | ||||
|     Описание... ''' | ||||
|  | ||||
|     await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup) | ||||
|  | ||||
| async def auto_start_message(message, state): | ||||
|     text = '''<b>Автозапуск</b> | ||||
|  | ||||
|     Описание... ''' | ||||
|  | ||||
|     await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup) | ||||
|  | ||||
| async def notifications_message(message, state): | ||||
|     text = '''<b>Уведомления</b>                                                                 | ||||
|  | ||||
|     Описание... ''' | ||||
|  | ||||
|     await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_on_off_markup)                                                                                                                                 | ||||
| @@ -1,143 +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 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): | ||||
|  | ||||
|     tg_id = id | ||||
|     trigger = await rq.get_for_registration_trigger(tg_id) | ||||
|     text = f""" <b>Условия запуска</b> | ||||
|  | ||||
| <b>- Режим торговли:</b>  {trigger} | ||||
| <b>- Таймер: </b> установить таймер / удалить таймер | ||||
| """ | ||||
|     await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.condition_settings_markup) | ||||
|  | ||||
|  | ||||
| async def trigger_message(id, message, state: FSMContext): | ||||
|     await state.set_state(condition_settings.trigger) | ||||
|     text = ''' | ||||
| <b>- Автоматический:</b> торговля будет происходить в рамках серии ставок. | ||||
| <b>- Ручной:</b> торговля будет происходить только в ручном режиме. | ||||
| <em>- Выберите тип триггера:</em>''' | ||||
|  | ||||
|     await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trigger_markup) | ||||
|  | ||||
|  | ||||
| @condition_settings_router.callback_query(F.data == "clb_trigger_manual") | ||||
| async def trigger_manual_callback(callback: CallbackQuery, state: FSMContext): | ||||
|     await state.set_state(condition_settings.trigger) | ||||
|     await rq.update_trigger(tg_id=callback.from_user.id, trigger="Ручной") | ||||
|     await main_settings_message(callback.from_user.id, callback.message) | ||||
|     await callback.answer() | ||||
|  | ||||
|  | ||||
| @condition_settings_router.callback_query(F.data == "clb_trigger_auto") | ||||
| async def trigger_manual_callback(callback: CallbackQuery, state: FSMContext): | ||||
|     await state.set_state(condition_settings.trigger) | ||||
|     await rq.update_trigger(tg_id=callback.from_user.id, trigger="Автоматический") | ||||
|     await main_settings_message(callback.from_user.id, callback.message) | ||||
|     await callback.answer() | ||||
|  | ||||
| async def timer_message(id, message: Message, state: FSMContext): | ||||
|     await state.set_state(condition_settings.timer) | ||||
|  | ||||
|     timer_info = await rq.get_user_timer(id) | ||||
|     if timer_info is None: | ||||
|         await message.answer("Таймер не установлен.", reply_markup=inline_markup.timer_markup) | ||||
|         return | ||||
|  | ||||
|     await message.answer( | ||||
|         f"Таймер установлен на: {timer_info['timer_minutes']} мин\n", | ||||
|         reply_markup=inline_markup.timer_markup | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @condition_settings_router.callback_query(F.data == "clb_set_timer") | ||||
| async def set_timer_callback(callback: CallbackQuery, state: FSMContext): | ||||
|     await state.set_state(condition_settings.timer)  # состояние для ввода времени | ||||
|     await callback.message.answer("Введите время работы в минутах (например, 60):", reply_markup=inline_markup.cancel) | ||||
|     await callback.answer() | ||||
|  | ||||
|  | ||||
| @condition_settings_router.message(condition_settings.timer) | ||||
| async def process_timer_input(message: Message, state: FSMContext): | ||||
|     try: | ||||
|         minutes = int(message.text) | ||||
|         if minutes <= 0: | ||||
|             await message.reply("Введите число больше нуля.") | ||||
|             return | ||||
|  | ||||
|         await rq.update_user_timer(message.from_user.id, minutes) | ||||
|         logger.info("Timer set for user %s: %s minutes", message.from_user.id, minutes) | ||||
|         await timer_message(message.from_user.id, message, state) | ||||
|         await state.clear() | ||||
|     except ValueError: | ||||
|         await message.reply("Пожалуйста, введите корректное число.") | ||||
|  | ||||
|  | ||||
| @condition_settings_router.callback_query(F.data == "clb_delete_timer") | ||||
| async def delete_timer_callback(callback: CallbackQuery, state: FSMContext): | ||||
|     await state.clear() | ||||
|     await rq.update_user_timer(callback.from_user.id, 0) | ||||
|     logger.info("Timer deleted for user %s", callback.from_user.id) | ||||
|     await timer_message(callback.from_user.id, callback.message, state) | ||||
|     await callback.answer() | ||||
|  | ||||
|  | ||||
| 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 = '''<b>Внешние сигналы</b> | ||||
|  | ||||
|     Описание... ''' | ||||
|  | ||||
|     await message.answer(text=text, parse_mode='html', reply_markup=None) | ||||
|  | ||||
|  | ||||
| async def trading_cues_message(message, state): | ||||
|     text = '''<b>Использование сигналов</b> | ||||
|  | ||||
|     Описание... ''' | ||||
|  | ||||
|     await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup) | ||||
|  | ||||
|  | ||||
| async def webhook_message(message, state): | ||||
|     text = '''Скиньте ссылку на <b>webhook</b> (если есть trading view): ''' | ||||
|  | ||||
|     await message.answer(text=text, parse_mode='html') | ||||
|  | ||||
|  | ||||
| async def ai_analytics_message(message, state): | ||||
|     text = '''<b>ИИ - Аналитика</b>  | ||||
|  | ||||
|     Описание... ''' | ||||
|  | ||||
|     await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup) | ||||
| @@ -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""" Привет <b>{username}</b>! 👋""", parse_mode='html') | ||||
|     await message.answer("Добро пожаловать в чат-робот для автоматизации трейдинга — вашего надежного помощника для анализа рынка и принятия взвешенных решений.", | ||||
|                          parse_mode='html', reply_markup=inline_markup.start_markup) | ||||
|  | ||||
| async def profile_message(username, message): | ||||
|     await message.answer(f""" <b>@{username}</b> | ||||
|  | ||||
| Баланс   | ||||
| ⭐️ 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) | ||||
| @@ -1,372 +0,0 @@ | ||||
| from aiogram import Router | ||||
| import logging.config | ||||
| import app.telegram.Keyboards.inline_keyboards as inline_markup | ||||
|  | ||||
| from pybit.unified_trading import HTTP | ||||
| import app.telegram.database.requests as rq | ||||
| from aiogram.types import Message, CallbackQuery | ||||
|  | ||||
| from app.services.Bybit.functions.price_symbol import get_price | ||||
| from app.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) | ||||
|         leverage = safe_float((data_main_stgs or {}).get('size_leverage')) | ||||
|         price = await get_price(tg_id, symbol=symbol) | ||||
|         entry_price = safe_float(price) | ||||
|  | ||||
|         if commission_fee == "Да": | ||||
|             commission_fee_percent = safe_float(fee_info['result']['list'][0]['takerFeeRate']) | ||||
|         else: | ||||
|             commission_fee_percent = 0.0 | ||||
|  | ||||
|         total_budget = await calculate_total_budget( | ||||
|             starting_quantity=starting_quantity, | ||||
|             martingale_factor=martingale_factor, | ||||
|             max_steps=max_martingale_steps, | ||||
|             commission_fee_percent=commission_fee_percent, | ||||
|             leverage=leverage, | ||||
|             current_price=entry_price, | ||||
|         ) | ||||
|  | ||||
|         await message.answer(f"""<b>Основные настройки</b> | ||||
|           | ||||
|     <b>- Режим торговли:</b> {data['trading_mode']} | ||||
|     <b>- Состояние свитча:</b> {data['switch_state']} | ||||
|     <b>- Направление последней сделки:</b> {data['last_side']} | ||||
|     <b>- Тип маржи:</b> {data['margin_type']} | ||||
|     <b>- Размер кредитного плеча:</b> х{data['size_leverage']} | ||||
|     <b>- Начальная ставка:</b> {data['starting_quantity']} | ||||
|     <b>- Коэффициент мартингейла:</b> {data['martingale_factor']} | ||||
|     <b>- Текущий шаг:</b> {data['martingale_step']} | ||||
|     <b>- Максимальное количество ставок в серии:</b> {data['maximal_quantity']}    | ||||
|      | ||||
|     <b>- Требуемый бюджет:</b> {total_budget:.2f} USDT | ||||
|     """, parse_mode='html', reply_markup=inline_markup.main_settings_markup) | ||||
|     except PermissionError as e: | ||||
|         logger.error("Authenticated endpoints require keys: %s", e) | ||||
|         await message.answer("Вы не авторизованы.", reply_markup=inline_markup.connect_bybit_api_message) | ||||
|  | ||||
|  | ||||
| async def trading_mode_message(message, state): | ||||
|     await state.set_state(update_main_settings.trading_mode) | ||||
|  | ||||
|     await message.edit_text("""<b>Режим торговли</b> | ||||
|  | ||||
| <b>Лонг</b> — стратегия, ориентированная на покупку актива с целью заработать на повышении его стоимости. | ||||
|  | ||||
| <b>Шорт</b> — метод продажи активов, взятых в кредит, чтобы получить прибыль от снижения цены. | ||||
|  | ||||
| <b>Свитч</b> — динамическое переключение между торговыми режимами для максимизации эффективности. | ||||
|      | ||||
| <em>Выберите ниже для изменений:</em>     | ||||
| """, parse_mode='html', reply_markup=inline_markup.trading_mode_markup) | ||||
|  | ||||
|  | ||||
| @router_main_settings.callback_query(update_main_settings.trading_mode) | ||||
| async def state_trading_mode(callback: CallbackQuery, state): | ||||
|     await callback.answer() | ||||
|  | ||||
|     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"""<b> Состояние свитча</b> | ||||
|          | ||||
|         <b>По направлению</b> - по направлению последней сделки предыдущей серии | ||||
|         <b>Против направления</b> - против направления последней сделки предыдущей серии | ||||
|          | ||||
|         <em>По умолчанию при первом запуске бота, направление сделки установлено на "Buy".</em> | ||||
|         <em>Выберите ниже для изменений:</em>""", parse_mode='html', | ||||
|         reply_markup=inline_markup.switch_state_markup) | ||||
|  | ||||
|  | ||||
| @router_main_settings.callback_query(lambda c: c.data in ["clb_long_switch", "clb_short_switch"]) | ||||
| async def state_switch_mode_enabled(callback: CallbackQuery, state): | ||||
|     await callback.answer() | ||||
|     tg_id = callback.from_user.id | ||||
|     val = "По направлению" if callback.data == "clb_long_switch" else "Против направления" | ||||
|     if val == "По направлению": | ||||
|         await rq.update_switch_state(tg_id, "По направлению") | ||||
|         await callback.message.answer(f"Состояние свитча: {val}") | ||||
|         await main_settings_message(tg_id, callback.message) | ||||
|     else: | ||||
|         await rq.update_switch_state(tg_id, "Против направления") | ||||
|         await callback.message.answer(f"Состояние свитча: {val}") | ||||
|         await main_settings_message(tg_id, callback.message) | ||||
|     await state.clear() | ||||
|  | ||||
|  | ||||
| async def size_leverage_message(message, state): | ||||
|     await state.set_state(update_main_settings.size_leverage) | ||||
|  | ||||
|     await message.edit_text("Введите размер <b>кредитного плеча</b> (от 1 до 100): ", parse_mode='html', | ||||
|                             reply_markup=inline_markup.back_btn_list_settings_markup) | ||||
|  | ||||
|  | ||||
| @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("Введите <b>коэффициент Мартингейла:</b>", parse_mode='html', | ||||
|                             reply_markup=inline_markup.back_btn_list_settings_markup) | ||||
|  | ||||
|  | ||||
| @router_main_settings.message(update_main_settings.martingale_factor) | ||||
| async def state_martingale_factor(message: Message, state): | ||||
|     await state.update_data(martingale_factor=message.text) | ||||
|  | ||||
|     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("""<b>Тип маржи</b> | ||||
|  | ||||
| <b>Изолированная маржа</b>   | ||||
| Этот тип маржи позволяет ограничить риск конкретной позиции.  | ||||
| При использовании изолированной маржи вы выделяете определённую сумму средств только для одной позиции.  | ||||
| Если позиция начинает приносить убытки, ваши потери ограничиваются этой суммой,  | ||||
| и остальные средства на счёте не затрагиваются. | ||||
|  | ||||
| <b>Кросс-маржа</b>   | ||||
| Кросс-маржа объединяет весь маржинальный баланс на счёте и использует все доступные средства для поддержания открытых позиций.  | ||||
| В случае убытков средства с других позиций или баланса автоматически покрывают дефицит,  | ||||
| снижая риск ликвидации, но увеличивая общий риск потери капитала. | ||||
|  | ||||
| <em>Выберите ниже для изменений:</em> | ||||
| """, parse_mode='html', reply_markup=inline_markup.margin_type_markup) | ||||
|  | ||||
|  | ||||
| @router_main_settings.callback_query(update_main_settings.margin_type) | ||||
| async def state_margin_type(callback: CallbackQuery, state): | ||||
|     callback_data = callback.data | ||||
|     if callback_data in ['margin_type_isolated', 'margin_type_cross']: | ||||
|         tg_id = callback.from_user.id | ||||
|         api_key = await rq.get_bybit_api_key(tg_id) | ||||
|         secret_key = await rq.get_bybit_secret_key(tg_id) | ||||
|         data_settings = await rq.get_user_main_settings(tg_id) | ||||
|         symbol = await rq.get_symbol(tg_id) | ||||
|         client = HTTP(api_key=api_key, api_secret=secret_key) | ||||
|         try: | ||||
|             active_positions = client.get_positions(category='linear', settleCoin="USDT") | ||||
|  | ||||
|             positions = active_positions.get('result', {}).get('list', []) | ||||
|         except Exception as e: | ||||
|             logger.error("Ошибка при получении активных позиций: %s", e) | ||||
|             positions = [] | ||||
|  | ||||
|         for pos in positions: | ||||
|             size = pos.get('size') | ||||
|             if float(size) > 0: | ||||
|                 await callback.answer( | ||||
|                     "⚠️ Маржинальный режим нельзя менять при открытой позиции" | ||||
|                 ) | ||||
|                 return | ||||
|  | ||||
|         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("Введите <b>начальную ставку:</b>", parse_mode='html', | ||||
|                             reply_markup=inline_markup.back_btn_list_settings_markup) | ||||
|  | ||||
|  | ||||
| @router_main_settings.message(update_main_settings.starting_quantity) | ||||
| async def state_starting_quantity(message: Message, state): | ||||
|     await state.update_data(starting_quantity=message.text) | ||||
|  | ||||
|     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 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) | ||||
							
								
								
									
										27
									
								
								app/telegram/functions/profile_tg.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								app/telegram/functions/profile_tg.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| @@ -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"""<b>Риск менеджмент</b>, | ||||
|  | ||||
|     <b>- Процент изменения цены для фиксации прибыли:</b> {data.get('price_profit', 0)}% | ||||
|     <b>- Процент изменения цены для фиксации убытков:</b> {data.get('price_loss', 0)}% | ||||
|     <b>- Максимальный риск на сделку (в % от баланса):</b> {data.get('max_risk_deal', 0)}% | ||||
|     <b>- Комиссия биржи для расчета прибыли:</b> {data.get('commission_fee', "Да")} | ||||
|     """ | ||||
|     await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.risk_management_settings_markup) | ||||
|  | ||||
|  | ||||
| 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() | ||||
							
								
								
									
										32
									
								
								app/telegram/handlers/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								app/telegram/handlers/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
							
								
								
									
										150
									
								
								app/telegram/handlers/add_bybit_api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								app/telegram/handlers/add_bybit_api.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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="Произошла ошибка. Пожалуйста, попробуйте позже.") | ||||
							
								
								
									
										135
									
								
								app/telegram/handlers/changing_the_symbol.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								app/telegram/handlers/changing_the_symbol.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | ||||
| 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.get_functions.get_instruments_info import get_instruments_info | ||||
| from app.bybit.profile_bybit import user_profile_bybit | ||||
| from app.bybit.set_functions.set_leverage import set_leverage | ||||
|  | ||||
| from app.bybit.set_functions.set_margin_mode import set_margin_mode | ||||
| from app.helper_functions import safe_float | ||||
| 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 | ||||
|  | ||||
|         margin_type = additional_settings.margin_type or "ISOLATED_MARGIN" | ||||
|         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 | ||||
|  | ||||
|         instruments_info = await get_instruments_info(tg_id=message.from_user.id, symbol=symbol) | ||||
|         max_leverage = instruments_info.get("leverageFilter").get("maxLeverage") | ||||
|         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 | ||||
|         ) | ||||
|  | ||||
|         await set_margin_mode(tg_id=message.from_user.id, margin_mode=margin_type) | ||||
|  | ||||
|         await set_leverage( | ||||
|             tg_id=message.from_user.id, symbol=symbol, leverage=str(max_leverage) | ||||
|         ) | ||||
|  | ||||
|         await rq.set_leverage(tg_id=message.from_user.id, leverage=str(max_leverage)) | ||||
|         risk_percent = 100 / safe_float(max_leverage) | ||||
|         await rq.set_stop_loss_percent( | ||||
|             tg_id=message.from_user.id, stop_loss_percent=risk_percent) | ||||
|         await rq.set_take_profit_percent( | ||||
|             tg_id=message.from_user.id, take_profit_percent=risk_percent) | ||||
|         await rq.set_trigger_price(tg_id=message.from_user.id, trigger_price=0) | ||||
|         await rq.set_order_quantity(tg_id=message.from_user.id, order_quantity=1.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) | ||||
							
								
								
									
										68
									
								
								app/telegram/handlers/close_orders.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								app/telegram/handlers/close_orders.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| import logging.config | ||||
|  | ||||
| from aiogram import Router | ||||
| from aiogram.fsm.context import FSMContext | ||||
| from aiogram.types import CallbackQuery | ||||
|  | ||||
| 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: | ||||
|         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: | ||||
|         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() | ||||
							
								
								
									
										50
									
								
								app/telegram/handlers/common.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								app/telegram/handlers/common.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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() | ||||
							
								
								
									
										314
									
								
								app/telegram/handlers/get_positions_handlers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										314
									
								
								app/telegram/handlers/get_positions_handlers.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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() | ||||
| @@ -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) | ||||
							
								
								
									
										381
									
								
								app/telegram/handlers/handlers_main.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										381
									
								
								app/telegram/handlers/handlers_main.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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" | ||||
|                      "Чат-робот для трейдинга - ваш надежный помощник для анализа рынка и принятия взвешенных решений.😉", | ||||
|                 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() | ||||
							
								
								
									
										17
									
								
								app/telegram/handlers/main_settings/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/telegram/handlers/main_settings/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
							
								
								
									
										1029
									
								
								app/telegram/handlers/main_settings/additional_settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1029
									
								
								app/telegram/handlers/main_settings/additional_settings.py
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										174
									
								
								app/telegram/handlers/main_settings/conditional_settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								app/telegram/handlers/main_settings/conditional_settings.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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, | ||||
|         ) | ||||
							
								
								
									
										343
									
								
								app/telegram/handlers/main_settings/risk_management.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										343
									
								
								app/telegram/handlers/main_settings/risk_management.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,343 @@ | ||||
| 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_number, safe_float | ||||
| 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_number(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 safe_float(take_profit_percent_value) < 1 or safe_float(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=safe_float(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_number(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 safe_float(stop_loss_percent_value) < 1 or safe_float(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=safe_float(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 == "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() | ||||
							
								
								
									
										198
									
								
								app/telegram/handlers/settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								app/telegram/handlers/settings.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,198 @@ | ||||
| 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.helper_functions import calculate_total_budget, 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 | ||||
|         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 = { | ||||
|             "Long": "Лонг", | ||||
|             "Short": "Шорт", | ||||
|             "Switch": "Свитч", | ||||
|         } | ||||
|         margin_type_map = { | ||||
|             "ISOLATED_MARGIN": "Изолированная", | ||||
|             "REGULAR_MARGIN": "Кросс", | ||||
|         } | ||||
|  | ||||
|         trade_mode = additional_data.trade_mode or "" | ||||
|         margin_type = additional_data.margin_type or "" | ||||
|  | ||||
|         trade_mode_rus = trade_mode_map.get(trade_mode, trade_mode) | ||||
|         margin_type_rus = margin_type_map.get(margin_type, margin_type) | ||||
|         switch_side = additional_data.switch_side | ||||
|  | ||||
|         def f(x): | ||||
|             return safe_float(x) | ||||
|  | ||||
|         leverage = f(additional_data.leverage) | ||||
|         martingale = f(additional_data.martingale_factor) | ||||
|         max_bets = additional_data.max_bets_in_series | ||||
|         quantity = f(additional_data.order_quantity) | ||||
|         trigger_price = f(additional_data.trigger_price) or 0 | ||||
|         side = additional_data.side | ||||
|  | ||||
|         side_map = { | ||||
|             "Buy": "Лонг", | ||||
|             "Sell": "Шорт", | ||||
|         } | ||||
|         side_rus = side_map.get(side, side) | ||||
|  | ||||
|         switch_side_mode = "" | ||||
|         side = "" | ||||
|         if trade_mode == "Switch": | ||||
|             side = f"- Направление первой сделки: {side_rus}\n" | ||||
|             switch_side_mode = f"- Направление первой сделки последующих серии: {switch_side}\n" | ||||
|  | ||||
|         total_budget = await calculate_total_budget( | ||||
|             quantity=quantity, | ||||
|             martingale_factor=martingale, | ||||
|             max_steps=max_bets, | ||||
|         ) | ||||
|         text = ( | ||||
|             f"Основные настройки:\n\n" | ||||
|             f"- Режим торговли: {trade_mode_rus}\n" | ||||
|             f"{side}" | ||||
|             f"{switch_side_mode}" | ||||
|             f"- Тип маржи: {margin_type_rus}\n" | ||||
|             f"- Размер кредитного плеча: {leverage:.2f}\n" | ||||
|             f"- Базовая ставка: {quantity} USDT\n" | ||||
|             f"- Коэффициент мартингейла: {martingale:.2f}\n" | ||||
|             f"- Триггер цена: {trigger_price:.4f} USDT\n" | ||||
|             f"- Максимальное кол-во ставок в серии: {max_bets}\n\n" | ||||
|             f"- Бюджет серии: {total_budget:.2f} USDT\n" | ||||
|         ) | ||||
|  | ||||
|         keyboard = kbi.get_additional_settings_keyboard(mode=trade_mode) | ||||
|         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 "" | ||||
|             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:.2f}%\n" | ||||
|                 f"- Процент изменения цены для фиксации убытка: {stop_loss_percent:.2f}%\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 | ||||
|             await callback_query.message.edit_text( | ||||
|                 text="Условия торговли:\n\n" | ||||
|                 f"- Таймер для старта: {start_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, | ||||
|         ) | ||||
							
								
								
									
										166
									
								
								app/telegram/handlers/start_trading.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								app/telegram/handlers/start_trading.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,166 @@ | ||||
| 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, get_active_orders_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, | ||||
|     cancel_start_task_merged | ||||
| ) | ||||
| 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() | ||||
|         tg_id = callback_query.from_user.id | ||||
|         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) | ||||
|         else: | ||||
|             size = 0 | ||||
|  | ||||
|         if safe_float(size) > 0: | ||||
|             await callback_query.answer( | ||||
|                 text="У вас есть активная позиция по текущей паре", | ||||
|             ) | ||||
|             return | ||||
|  | ||||
|         orders = await get_active_orders_by_symbol( | ||||
|             tg_id=callback_query.from_user.id, symbol=symbol) | ||||
|  | ||||
|         if orders is not None: | ||||
|             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, | ||||
|             ) | ||||
|             await rq.set_total_fee_user_auto_trading( | ||||
|                 tg_id=tg_id, symbol=symbol, total_fee=0 | ||||
|             ) | ||||
|             await rq.set_fee_user_auto_trading( | ||||
|                 tg_id=tg_id, symbol=symbol, fee=0 | ||||
|             ) | ||||
|             res = await start_trading_cycle( | ||||
|                 tg_id=callback_query.from_user.id, | ||||
|             ) | ||||
|  | ||||
|             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": "️️Превышен максимальный лимит ставки", | ||||
|                 "The number of contracts exceeds minimum 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, | ||||
|                 ) | ||||
|                 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 start_trading 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 == "cancel_timer_merged" | ||||
| ) | ||||
| 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) | ||||
|         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, | ||||
|         ) | ||||
							
								
								
									
										90
									
								
								app/telegram/handlers/stop_trading.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								app/telegram/handlers/stop_trading.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| import asyncio | ||||
| import logging.config | ||||
|  | ||||
| from aiogram import F, Router | ||||
| from aiogram.fsm.context import FSMContext | ||||
| from aiogram.types import CallbackQuery | ||||
|  | ||||
| from app.bybit.close_positions import close_position_by_symbol | ||||
| 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 | ||||
|         symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id) | ||||
|  | ||||
|         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 = await rq.get_user_auto_trading( | ||||
|                 tg_id=callback_query.from_user.id, symbol=symbol | ||||
|             ) | ||||
|  | ||||
|             if user_auto_trading and user_auto_trading.auto_trading: | ||||
|                 await rq.set_auto_trading( | ||||
|                     tg_id=callback_query.from_user.id, | ||||
|                     symbol=symbol, | ||||
|                     auto_trading=False, | ||||
|                 ) | ||||
|                 await close_position_by_symbol( | ||||
|                     tg_id=callback_query.from_user.id, symbol=symbol) | ||||
|                 await callback_query.message.edit_text(text=f"Торговля для {symbol} остановлена", reply_markup=kbi.profile_bybit) | ||||
|             else: | ||||
|                 await callback_query.message.edit_text(text=f"Нет активной торговли для {symbol}", reply_markup=kbi.profile_bybit) | ||||
|  | ||||
|         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, | ||||
|         ) | ||||
							
								
								
									
										168
									
								
								app/telegram/handlers/tp_sl_handlers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								app/telegram/handlers/tp_sl_handlers.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
							
								
								
									
										398
									
								
								app/telegram/keyboards/inline.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										398
									
								
								app/telegram/keyboards/inline.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,398 @@ | ||||
| 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="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(mode: str | ||||
| ) -> InlineKeyboardMarkup: | ||||
|     """ | ||||
|     Create keyboard for additional settings | ||||
|     :param mode: Trade mode | ||||
|     :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_quantity"), | ||||
|         ], | ||||
|  | ||||
|         [ | ||||
|             InlineKeyboardButton( | ||||
|                 text="Коэффициент мартингейла", callback_data="martingale_factor" | ||||
|             ), | ||||
|             InlineKeyboardButton(text="Триггер цена", callback_data="trigger_price" | ||||
|  | ||||
|             ), | ||||
|         ], | ||||
|     ] | ||||
|  | ||||
|     if mode == "Switch": | ||||
|         buttons.append( | ||||
|             [InlineKeyboardButton(text="Направление первой сделки первой серии", callback_data="switch_side_start")] | ||||
|         ) | ||||
|         buttons.append( | ||||
|             [InlineKeyboardButton(text="Направление первой сделки последующих серии", callback_data="switch_side_second")] | ||||
|         ) | ||||
|  | ||||
|     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) | ||||
|  | ||||
|  | ||||
| trade_mode = InlineKeyboardMarkup( | ||||
|     inline_keyboard=[ | ||||
|         [ | ||||
|             InlineKeyboardButton( | ||||
|                 text="Лонг", callback_data="Long" | ||||
|             ), | ||||
|             InlineKeyboardButton(text="Шорт", callback_data="Short"), | ||||
|             InlineKeyboardButton(text="Свитч", callback_data="Switch"), | ||||
|         ], | ||||
|         [ | ||||
|             InlineKeyboardButton(text="Назад", callback_data="additional_settings"), | ||||
|             InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), | ||||
|         ], | ||||
|     ] | ||||
| ) | ||||
|  | ||||
| switch_side = InlineKeyboardMarkup( | ||||
|     inline_keyboard=[ | ||||
|         [ | ||||
|             InlineKeyboardButton( | ||||
|                 text="По направлению", callback_data="switch_direction" | ||||
|             ), | ||||
|             InlineKeyboardButton( | ||||
|                 text="Противоположно", callback_data="switch_opposite" | ||||
|             ), | ||||
|         ], | ||||
|         [ | ||||
|             InlineKeyboardButton(text="Назад", callback_data="additional_settings"), | ||||
|             InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), | ||||
|         ], | ||||
|     ] | ||||
| ) | ||||
|  | ||||
| side_for_switch = InlineKeyboardMarkup( | ||||
|     inline_keyboard=[ | ||||
|         [ | ||||
|             InlineKeyboardButton(text="Лонг", callback_data="buy_switch"), | ||||
|             InlineKeyboardButton(text="Шорт", callback_data="sell_switch"), | ||||
|         ], | ||||
|         [ | ||||
|             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"), | ||||
|         ], | ||||
|     ] | ||||
| ) | ||||
|  | ||||
| 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="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="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 | ||||
|  | ||||
| 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"), | ||||
|         ], | ||||
|     ] | ||||
| ) | ||||
							
								
								
									
										11
									
								
								app/telegram/keyboards/reply.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								app/telegram/keyboards/reply.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| 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="Выберите пункт меню...", | ||||
| ) | ||||
							
								
								
									
										0
									
								
								app/telegram/states/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/telegram/states/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										51
									
								
								app/telegram/states/states.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								app/telegram/states/states.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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() | ||||
							
								
								
									
										0
									
								
								app/telegram/tasks/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/telegram/tasks/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										77
									
								
								app/telegram/tasks/tasks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								app/telegram/tasks/tasks.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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] | ||||
							
								
								
									
										11
									
								
								config.py
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								config.py
									
									
									
									
									
								
							| @@ -1,9 +1,8 @@ | ||||
| from dotenv import load_dotenv, find_dotenv | ||||
| import os | ||||
| from dotenv import load_dotenv, find_dotenv | ||||
|  | ||||
| env_file = find_dotenv() | ||||
| load_dotenv(env_file) | ||||
| env_path = find_dotenv() | ||||
| if env_path: | ||||
|     load_dotenv(env_path) | ||||
|  | ||||
| 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') | ||||
| BOT_TOKEN = os.getenv("BOT_TOKEN") | ||||
|   | ||||
							
								
								
									
										45
									
								
								database/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								database/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| from database.models import Base, User, UserAdditionalSettings, UserApi, UserConditionalSettings, UserDeals, \ | ||||
|     UserRiskManagement, UserSymbol | ||||
| import logging.config | ||||
| from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession | ||||
| from sqlalchemy import event | ||||
| from pathlib import Path | ||||
| from logger_helper.logger_helper import LOGGING_CONFIG | ||||
|  | ||||
| logging.config.dictConfig(LOGGING_CONFIG) | ||||
| logger = logging.getLogger("database") | ||||
|  | ||||
| BASE_DIR = Path(__file__).parent.resolve() | ||||
| DATA_DIR = BASE_DIR / "db" | ||||
| DATA_DIR.mkdir(parents=True, exist_ok=True) | ||||
|  | ||||
| DATABASE_URL = f"sqlite+aiosqlite:///{DATA_DIR / 'stcs.db'}" | ||||
|  | ||||
| async_engine = create_async_engine( | ||||
|     DATABASE_URL, | ||||
|     echo=False, | ||||
|     connect_args={"check_same_thread": False} | ||||
| ) | ||||
|  | ||||
|  | ||||
| @event.listens_for(async_engine.sync_engine, "connect") | ||||
| def _enable_foreign_keys(dbapi_connection, connection_record): | ||||
|     cursor = dbapi_connection.cursor() | ||||
|     cursor.execute("PRAGMA foreign_keys=ON") | ||||
|     cursor.close() | ||||
|  | ||||
|  | ||||
| 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) | ||||
							
								
								
									
										180
									
								
								database/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								database/models.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,180 @@ | ||||
| from sqlalchemy.ext.declarative import declarative_base | ||||
| from sqlalchemy.ext.asyncio import AsyncAttrs | ||||
| from sqlalchemy import Column, ForeignKey, Integer, String, 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(Integer, 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") | ||||
|     switch_side = Column(String, nullable=False, default="По направлению") | ||||
|     side = Column(String, nullable=False, default="Buy") | ||||
|     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") | ||||
|     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(Float, nullable=False, default=1) | ||||
|     stop_loss_percent = Column(Float, nullable=False, default=1) | ||||
|     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) | ||||
|     side_mode = Column(String, nullable=True) | ||||
|     base_quantity = Column(Float, nullable=True) | ||||
|     margin_type = Column(String, nullable=True) | ||||
|     leverage = 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) | ||||
|     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) | ||||
|     fee = Column(Float, nullable=True) | ||||
|     total_fee = Column(Float, nullable=True) | ||||
|  | ||||
|     user = relationship("User", back_populates="user_auto_trading") | ||||
							
								
								
									
										1270
									
								
								database/request.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1270
									
								
								database/request.py
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										17
									
								
								examples/systemd/stcs.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								examples/systemd/stcs.service
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| [Unit] | ||||
| Description=Telegram chat-robot: @stcs_cryptobot | ||||
|  | ||||
| Wants=network.target | ||||
| After=syslog.target network-online.target | ||||
|  | ||||
| [Service] | ||||
| ExecStart=sudo -u www-data /usr/bin/python3 /var/www/stcs/BybitBot_API.py | ||||
| PIDFile=/var/run/python/stcs.pid | ||||
| RemainAfterExit=no | ||||
| RuntimeMaxSec=3600s | ||||
| Restart=always | ||||
| RestartSec=5s | ||||
|  | ||||
| [Install] | ||||
| WantedBy=multi-user.target | ||||
|  | ||||
							
								
								
									
										0
									
								
								logger_helper/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								logger_helper/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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, | ||||
|         }, | ||||
|   | ||||
| @@ -1,45 +0,0 @@ | ||||
| 2025-09-12 10:09:21 - main - INFO - Bot is on | ||||
| 2025-09-12 11:34:46 - main - INFO - Bot is on | ||||
| 2025-09-12 11:34:56 - requests - INFO - Получение риск-менеджмента настроек пользователя 899674724 | ||||
| 2025-09-12 11:35:56 - requests - INFO - Получение риск-менеджмента настроек пользователя 899674724 | ||||
| 2025-09-12 11:36:40 - bybit_ws - INFO - WebSocket для пользователя 899674724 запущен. | ||||
| 2025-09-12 11:38:54 - main - INFO - Bot is on | ||||
| 2025-09-12 11:40:57 - main - INFO - Bot is on | ||||
| 2025-09-12 11:42:06 - main - INFO - Bot is on | ||||
| 2025-09-12 11:42:47 - main - INFO - Bot is on | ||||
| 2025-09-12 11:43:01 - bybit_ws - INFO - WebSocket для пользователя 899674724 запущен. | ||||
| 2025-09-12 11:46:11 - main - INFO - Bot is on | ||||
| 2025-09-12 11:46:20 - requests - INFO - Получение риск-менеджмента настроек пользователя 899674724 | ||||
| 2025-09-12 12:35:06 - main - INFO - Bot is on | ||||
| 2025-09-12 12:35:07 - models - INFO - Заполение таблицы режима торговли | ||||
| 2025-09-12 12:35:07 - models - INFO - Заполение таблицы режима торговли | ||||
| 2025-09-12 12:35:07 - models - INFO - Заполение таблицы режима торговли | ||||
| 2025-09-12 12:35:07 - models - INFO - Заполение таблицы режима торговли | ||||
| 2025-09-12 12:35:07 - models - INFO - Заполение таблицы типов маржи | ||||
| 2025-09-12 12:35:07 - models - INFO - Заполение таблицы типов маржи | ||||
| 2025-09-12 12:35:07 - models - INFO - Заполение таблицы последнего направления | ||||
| 2025-09-12 12:35:07 - models - INFO - Заполение таблицы последнего направления | ||||
| 2025-09-12 12:36:00 - main_settings - ERROR - Authenticated endpoints require keys: Authenticated endpoints require keys. | ||||
| 2025-09-12 12:36:24 - requests - INFO - Добавлен новый пользователь 899674724 с ключами | ||||
| 2025-09-12 12:36:25 - requests - INFO - Symbol был успешно добавлен 899674724 | ||||
| 2025-09-12 12:36:25 - requests - INFO - Новый пользователь был добавлен в бд 899674724 | ||||
| 2025-09-12 12:36:25 - requests - INFO - Основные настройки нового пользователя были заполнены899674724 | ||||
| 2025-09-12 12:36:25 - requests - INFO - Риск-Менеджмент настройки нового пользователя были заполнены 899674724 | ||||
| 2025-09-12 12:36:25 - requests - INFO - Условные настройки нового пользователя были заполнены 899674724 | ||||
| 2025-09-12 12:36:25 - requests - INFO - Дополнительные настройки нового пользователя были заполнены 899674724 | ||||
| 2025-09-12 12:36:26 - bybit_ws - INFO - WebSocket для пользователя 899674724 запущен. | ||||
| 2025-09-12 12:38:00 - main - INFO - Bot is on | ||||
| 2025-09-12 12:38:54 - bybit_ws - INFO - WebSocket для пользователя 899674724 запущен. | ||||
| 2025-09-12 12:40:53 - requests - INFO - Получение риск-менеджмента настроек пользователя 899674724 | ||||
| 2025-09-12 12:41:07 - requests - INFO - Получение риск-менеджмента настроек пользователя 899674724 | ||||
| 2025-09-12 13:43:39 - main - INFO - Bot is on | ||||
| 2025-09-12 18:13:40 - main - INFO - Bot is on | ||||
| 2025-09-12 18:13:52 - requests - INFO - Получение риск-менеджмента настроек пользователя 899674724 | ||||
| 2025-09-12 18:19:38 - main - INFO - Bot is on | ||||
| 2025-09-12 18:19:52 - bybit_ws - INFO - WebSocket для пользователя 899674724 запущен. | ||||
| 2025-09-12 18:20:01 - requests - INFO - Получение риск-менеджмента настроек пользователя 899674724 | ||||
| 2025-09-12 18:20:27 - requests - INFO - Получение риск-менеджмента настроек пользователя 899674724 | ||||
| 2025-09-12 18:22:37 - main - INFO - Bot is on | ||||
| 2025-09-12 18:22:37 - requests - INFO - Получение риск-менеджмента настроек пользователя 899674724 | ||||
| 2025-09-12 18:22:48 - requests - INFO - Получение риск-менеджмента настроек пользователя 899674724 | ||||
| 2025-09-12 18:23:21 - bybit_ws - INFO - WebSocket для пользователя 899674724 запущен. | ||||
| @@ -4,7 +4,9 @@ aiohappyeyeballs==2.6.1 | ||||
| aiohttp==3.12.15 | ||||
| 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 | ||||
| @@ -20,7 +22,9 @@ greenlet==3.2.4 | ||||
| idna==3.10 | ||||
| isort==6.0.1 | ||||
| magic-filter==1.0.12 | ||||
| Mako==1.3.10 | ||||
| mando==0.7.1 | ||||
| MarkupSafe==3.0.2 | ||||
| mccabe==0.7.0 | ||||
| multidict==6.6.4 | ||||
| mypy_extensions==1.1.0 | ||||
| @@ -29,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 | ||||
| @@ -42,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 | ||||
|   | ||||
							
								
								
									
										55
									
								
								run.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								run.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| import asyncio | ||||
| import contextlib | ||||
| import logging.config | ||||
|  | ||||
| from aiogram import Bot, Dispatcher | ||||
| from aiogram.fsm.storage.redis import RedisStorage | ||||
|  | ||||
| from database import init_db | ||||
| from app.bybit.web_socket import WebSocketBot | ||||
| from app.telegram.handlers import router | ||||
| from config import BOT_TOKEN | ||||
| 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 | ||||
|  | ||||
|     except Exception as e: | ||||
|         logger.error("Bot stopped with error: %s", e) | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     asyncio.run(main()) | ||||
		Reference in New Issue
	
	Block a user