forked from kodorvan/stcs
		
	Compare commits
	
		
			129 Commits
		
	
	
		
			b77c0f7dcc
			...
			stable
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 2d7acb491e | |||
|   | d767399988 | ||
|   | 89603f0b62 | ||
|   | 14f2a9e773 | ||
|   | a43fc6a66b | ||
|   | 869458b2e1 | ||
|   | 07948d93cf | ||
| 12d1db16d3 | |||
|   | 7350c86927 | ||
| 0a369b10f2 | |||
|   | 42f0f8ddc0 | ||
|   | 3df88d07ab | ||
| 7b1a803db4 | |||
|   | ddfa3a7360 | ||
| 9fcd92cc72 | |||
|   | e61b7334a4 | ||
| 97a199f31e | |||
|   | 5ad69f3f6d | ||
|   | abad01352a | ||
|   | 720b30d681 | ||
|   | 3616e2cbd3 | ||
|   | 7d108337fa | ||
|   | 0f6e6a2168 | ||
| 951bc15957 | |||
|   | 258ed970f1 | ||
|   | a3a6509933 | ||
| 5937058899 | |||
|   | 8251938b2f | ||
| f0732607e2 | |||
|   | 458b34fcec | ||
| 56af1d8f3b | |||
|   | 4a7577b977 | ||
| 9f069df68a | |||
|   | 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 | |||
|   | 0de3b17d1d | 
| @@ -1,3 +1 @@ | |||||||
| TOKEN_TELEGRAM_BOT_1= | BOT_TOKEN=YOUR_BOT_TOKEN | ||||||
| TOKEN_TELEGRAM_BOT_2= |  | ||||||
| TOKEN_TELEGRAM_BOT_3= |  | ||||||
							
								
								
									
										215
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										215
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,13 +1,212 @@ | |||||||
| .env | # Byte-compiled / optimized / DLL files | ||||||
| !*.sample |  | ||||||
|  |  | ||||||
| __pycache__/ | __pycache__/ | ||||||
| *.pyc | *.py[codz] | ||||||
|  | *$py.class | ||||||
|  |  | ||||||
| env/ | # C extensions | ||||||
| venv/ | *.so | ||||||
| .venv/ |  | ||||||
|  | # 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 | ||||||
| /.idea | /.idea | ||||||
| /myenv | .env | ||||||
|  | .envrc | ||||||
|  | .venv | ||||||
|  | env/ | ||||||
|  | venv/ | ||||||
| myenv | 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. Клонируйте репозиторий: | 1. Клонируйте репозиторий: | ||||||
|  |  | ||||||
| @@ -41,6 +41,10 @@ git clone https://git.svoboda.works/kodorvan/stcs | |||||||
| ```bash | ```bash | ||||||
| pip install -r requirements.txt | pip install -r requirements.txt | ||||||
| ``` | ``` | ||||||
|  | или для отдельного пользователя | ||||||
|  | ```bash | ||||||
|  | sudo -u www-data /usr/bin/pip install -r requirements.txt | ||||||
|  | ``` | ||||||
|  |  | ||||||
| 3. Зарегистрируйте чат-робота и сгенерируйте ключ авторизации<br> | 3. Зарегистрируйте чат-робота и сгенерируйте ключ авторизации<br> | ||||||
| [@BotFather](https://t.me/BotFather) | [@BotFather](https://t.me/BotFather) | ||||||
| @@ -50,11 +54,41 @@ pip install -r requirements.txt | |||||||
| cp .env.sample .env | cp .env.sample .env | ||||||
| nvim .env | nvim .env | ||||||
| ``` | ``` | ||||||
|  | 5. Выполните миграции: | ||||||
|  | ```bash | ||||||
|  | alembic upgrade head | ||||||
|  | ``` | ||||||
|  |  | ||||||
| 5. Запустите бота: | 5. Запустите бота: | ||||||
|  |  | ||||||
| ```bash | ```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 | import os | ||||||
|  | from dotenv import load_dotenv, find_dotenv | ||||||
|  |  | ||||||
| env_file = find_dotenv() | env_path = find_dotenv() | ||||||
| load_dotenv(env_file) | if env_path: | ||||||
|  |     load_dotenv(env_path) | ||||||
|  |  | ||||||
| TOKEN_TG_BOT_1 = os.getenv('TOKEN_TELEGRAM_BOT_1') | BOT_TOKEN = os.getenv("BOT_TOKEN") | ||||||
| TOKEN_TG_BOT_2 = os.getenv('TOKEN_TELEGRAM_BOT_2') |  | ||||||
| TOKEN_TG_BOT_3 = os.getenv('TOKEN_TELEGRAM_BOT_3') |  | ||||||
|   | |||||||
							
								
								
									
										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__)) | current_directory = os.path.dirname(os.path.abspath(__file__)) | ||||||
| log_directory = os.path.join(current_directory, 'loggers') | 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(log_directory, exist_ok=True) | ||||||
|  | os.makedirs(error_log_directory, exist_ok=True) | ||||||
| log_filename = os.path.join(log_directory, 'app.log') | log_filename = os.path.join(log_directory, 'app.log') | ||||||
|  | error_log_filename = os.path.join(error_log_directory, 'error.log') | ||||||
|  |  | ||||||
| LOGGING_CONFIG = { | LOGGING_CONFIG = { | ||||||
|     "version": 1, |     "version": 1, | ||||||
|     "disable_existing_loggers": False, |     "disable_existing_loggers": False, | ||||||
|     "formatters": { |     "formatters": { | ||||||
|         "default": { |         "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",  # Формат даты |             "datefmt": "%Y-%m-%d %H:%M:%S",  # Формат даты | ||||||
|         }, |         }, | ||||||
|     }, |     }, | ||||||
| @@ -23,90 +26,122 @@ LOGGING_CONFIG = { | |||||||
|             "backupCount": 7,  # Количество сохраняемых архивов (0 - не сохранять) |             "backupCount": 7,  # Количество сохраняемых архивов (0 - не сохранять) | ||||||
|             "formatter": "default", |             "formatter": "default", | ||||||
|             "encoding": "utf-8", |             "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": { |         "console": { | ||||||
|             "class": "logging.StreamHandler", |             "class": "logging.StreamHandler", | ||||||
|             "formatter": "default", |             "formatter": "default", | ||||||
|  |             "level": "DEBUG", | ||||||
|         }, |         }, | ||||||
|     }, |     }, | ||||||
|     "loggers": { |     "loggers": { | ||||||
|         "main": { |         "run": { | ||||||
|             "handlers": ["console", "timed_rotating_file"], |             "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", |             "level": "DEBUG", | ||||||
|             "propagate": False, |             "propagate": False, | ||||||
|         }, |         }, | ||||||
|         "add_bybit_api": { |         "add_bybit_api": { | ||||||
|             "handlers": ["console", "timed_rotating_file"], |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|             "level": "DEBUG", |             "level": "DEBUG", | ||||||
|             "propagate": False, |             "propagate": False, | ||||||
|         }, |         }, | ||||||
|         "balance": { |         "profile_tg": { | ||||||
|             "handlers": ["console", "timed_rotating_file"], |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|             "level": "DEBUG", |             "level": "DEBUG", | ||||||
|             "propagate": False, |             "propagate": False, | ||||||
|         }, |         }, | ||||||
|         "functions": { |         "settings": { | ||||||
|             "handlers": ["console", "timed_rotating_file"], |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|             "level": "DEBUG", |             "level": "DEBUG", | ||||||
|             "propagate": False, |             "propagate": False, | ||||||
|         }, |         }, | ||||||
|         "futures": { |         "additional_settings": { | ||||||
|             "handlers": ["console", "timed_rotating_file"], |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|             "level": "DEBUG", |             "level": "DEBUG", | ||||||
|             "propagate": False, |             "propagate": False, | ||||||
|         }, |         }, | ||||||
|         "get_valid_symbol": { |         "helper_functions": { | ||||||
|             "handlers": ["console", "timed_rotating_file"], |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|             "level": "DEBUG", |             "level": "DEBUG", | ||||||
|             "propagate": False, |             "propagate": False, | ||||||
|         }, |         }, | ||||||
|         "min_qty": { |         "risk_management": { | ||||||
|             "handlers": ["console", "timed_rotating_file"], |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|             "level": "DEBUG", |             "level": "DEBUG", | ||||||
|             "propagate": False, |             "propagate": False, | ||||||
|         }, |         }, | ||||||
|         "price_symbol": { |         "start_trading": { | ||||||
|             "handlers": ["console", "timed_rotating_file"], |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|             "level": "DEBUG", |             "level": "DEBUG", | ||||||
|             "propagate": False, |             "propagate": False, | ||||||
|         }, |         }, | ||||||
|         "requests": { |         "stop_trading": { | ||||||
|             "handlers": ["console", "timed_rotating_file"], |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|             "level": "DEBUG", |             "level": "DEBUG", | ||||||
|             "propagate": False, |             "propagate": False, | ||||||
|         }, |         }, | ||||||
|         "handlers": { |         "changing_the_symbol": { | ||||||
|             "handlers": ["console", "timed_rotating_file"], |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|             "level": "DEBUG", |             "level": "DEBUG", | ||||||
|             "propagate": False, |             "propagate": False, | ||||||
|         }, |         }, | ||||||
|         "condition_settings": { |         "conditional_settings": { | ||||||
|             "handlers": ["console", "timed_rotating_file"], |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|             "level": "DEBUG", |             "level": "DEBUG", | ||||||
|             "propagate": False, |             "propagate": False, | ||||||
|         }, |         }, | ||||||
|         "main_settings": { |         "get_positions_handlers": { | ||||||
|             "handlers": ["console", "timed_rotating_file"], |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|             "level": "DEBUG", |             "level": "DEBUG", | ||||||
|             "propagate": False, |             "propagate": False, | ||||||
|         }, |         }, | ||||||
|         "risk_management_settings": { |         "close_orders": { | ||||||
|             "handlers": ["console", "timed_rotating_file"], |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|             "level": "DEBUG", |             "level": "DEBUG", | ||||||
|             "propagate": False, |             "propagate": False, | ||||||
|         }, |         }, | ||||||
|         "models": { |         "tp_sl_handlers": { | ||||||
|             "handlers": ["console", "timed_rotating_file"], |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|             "level": "DEBUG", |  | ||||||
|             "propagate": False, |  | ||||||
|         }, |  | ||||||
|         "bybit_ws": { |  | ||||||
|             "handlers": ["console", "timed_rotating_file"], |  | ||||||
|             "level": "DEBUG", |             "level": "DEBUG", | ||||||
|             "propagate": False, |             "propagate": False, | ||||||
|         }, |         }, | ||||||
|         "tasks": { |         "tasks": { | ||||||
|             "handlers": ["console", "timed_rotating_file"], |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|             "level": "DEBUG", |             "level": "DEBUG", | ||||||
|             "propagate": False, |             "propagate": False, | ||||||
|         }, |         }, | ||||||
|   | |||||||
| @@ -1,35 +0,0 @@ | |||||||
| 2025-08-23 12:57:26 - main - INFO - Bot is off |  | ||||||
| 2025-08-23 13:04:01 - main - INFO - Bot is off |  | ||||||
| 2025-08-23 13:25:04 - main - INFO - Bot is off |  | ||||||
| 2025-08-23 13:26:24 - main - INFO - Bot is off |  | ||||||
| 2025-08-23 13:28:36 - main - INFO - Bot is off |  | ||||||
| 2025-08-23 13:29:29 - main - INFO - Bot is off |  | ||||||
| 2025-08-23 13:30:48 - main - INFO - Bot is off |  | ||||||
| 2025-08-23 13:31:43 - main - INFO - Bot is off |  | ||||||
| 2025-08-23 13:33:10 - main - INFO - Bot is off |  | ||||||
| 2025-08-23 13:34:59 - main - INFO - Bot is off |  | ||||||
| 2025-08-23 13:36:15 - main - INFO - Bot is off |  | ||||||
| 2025-08-23 13:49:17 - main - INFO - Bot is off |  | ||||||
| 2025-08-23 13:50:22 - main - INFO - Bot is on |  | ||||||
| 2025-08-23 13:51:30 - main - INFO - Bot is off |  | ||||||
| 2025-08-23 13:51:37 - main - INFO - Bot is on |  | ||||||
| 2025-08-23 13:52:12 - main - INFO - Bot is off |  | ||||||
| 2025-08-23 13:57:48 - main - INFO - Bot is on |  | ||||||
| 2025-08-23 14:05:36 - main - INFO - Bot is off |  | ||||||
| 2025-08-23 14:05:43 - main - INFO - Bot is on |  | ||||||
| 2025-08-23 14:06:03 - main - INFO - Bot is off |  | ||||||
| 2025-08-23 14:06:46 - main - INFO - Bot is on |  | ||||||
| 2025-08-23 14:07:04 - requests - INFO - Bybit был успешно подключен |  | ||||||
| 2025-08-23 14:07:43 - requests - INFO - Новый пользователь был добавлен в бд |  | ||||||
| 2025-08-23 14:07:43 - requests - INFO - Основные настройки нового пользователя были заполнены |  | ||||||
| 2025-08-23 14:07:43 - requests - INFO - Риск-Менеджмент настройки нового пользователя были заполнены |  | ||||||
| 2025-08-23 14:07:43 - requests - INFO - Условные настройки нового пользователя были заполнены |  | ||||||
| 2025-08-23 14:07:43 - requests - INFO - Дополнительные настройки нового пользователя были заполнены |  | ||||||
| 2025-08-23 14:23:31 - main - INFO - Bot is off |  | ||||||
| 2025-08-23 14:23:39 - main - INFO - Bot is on |  | ||||||
| 2025-08-23 14:28:13 - main - INFO - Bot is off |  | ||||||
| 2025-08-23 14:28:19 - main - INFO - Bot is on |  | ||||||
| 2025-08-23 14:28:26 - requests - INFO - Получение риск-менеджмента настроек пользователя 899674724 |  | ||||||
| 2025-08-23 14:28:26 - requests - INFO - Получение риск-менеджмента настроек пользователя 899674724 |  | ||||||
| 2025-08-23 14:29:12 - requests - INFO - Получение риск-менеджмента настроек пользователя 899674724 |  | ||||||
| 2025-08-23 14:29:34 - main - INFO - Bot is off |  | ||||||
| @@ -4,7 +4,9 @@ aiohappyeyeballs==2.6.1 | |||||||
| aiohttp==3.12.15 | aiohttp==3.12.15 | ||||||
| aiosignal==1.4.0 | aiosignal==1.4.0 | ||||||
| aiosqlite==0.21.0 | aiosqlite==0.21.0 | ||||||
|  | alembic==1.16.5 | ||||||
| annotated-types==0.7.0 | annotated-types==0.7.0 | ||||||
|  | asyncpg==0.30.0 | ||||||
| attrs==25.3.0 | attrs==25.3.0 | ||||||
| black==25.1.0 | black==25.1.0 | ||||||
| certifi==2025.8.3 | certifi==2025.8.3 | ||||||
| @@ -20,7 +22,9 @@ greenlet==3.2.4 | |||||||
| idna==3.10 | idna==3.10 | ||||||
| isort==6.0.1 | isort==6.0.1 | ||||||
| magic-filter==1.0.12 | magic-filter==1.0.12 | ||||||
|  | Mako==1.3.10 | ||||||
| mando==0.7.1 | mando==0.7.1 | ||||||
|  | MarkupSafe==3.0.2 | ||||||
| mccabe==0.7.0 | mccabe==0.7.0 | ||||||
| multidict==6.6.4 | multidict==6.6.4 | ||||||
| mypy_extensions==1.1.0 | mypy_extensions==1.1.0 | ||||||
| @@ -29,10 +33,12 @@ packaging==25.0 | |||||||
| pathspec==0.12.1 | pathspec==0.12.1 | ||||||
| platformdirs==4.4.0 | platformdirs==4.4.0 | ||||||
| propcache==0.3.2 | propcache==0.3.2 | ||||||
|  | psycopg==3.2.10 | ||||||
|  | psycopg-binary==3.2.10 | ||||||
| pybit==5.11.0 | pybit==5.11.0 | ||||||
| pycodestyle==2.14.0 | pycodestyle==2.14.0 | ||||||
| pycryptodome==3.23.0 | pycryptodome==3.23.0 | ||||||
| pydantic==2.11.7 | pydantic==2.11.9 | ||||||
| pydantic_core==2.33.2 | pydantic_core==2.33.2 | ||||||
| pyflakes==3.4.0 | pyflakes==3.4.0 | ||||||
| python-dotenv==1.1.1 | python-dotenv==1.1.1 | ||||||
| @@ -42,7 +48,8 @@ requests==2.32.5 | |||||||
| six==1.17.0 | six==1.17.0 | ||||||
| SQLAlchemy==2.0.43 | SQLAlchemy==2.0.43 | ||||||
| typing-inspection==0.4.1 | typing-inspection==0.4.1 | ||||||
| typing_extensions==4.14.1 | typing_extensions==4.15.0 | ||||||
|  | uliweb-alembic==0.6.9 | ||||||
| urllib3==2.5.0 | urllib3==2.5.0 | ||||||
| websocket-client==1.8.0 | websocket-client==1.8.0 | ||||||
| yarl==1.20.1 | 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