forked from kodorvan/stcs
		
	Compare commits
	
		
			225 Commits
		
	
	
		
			0fecb5dab7
			...
			stable
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | ||
|   | b77c0f7dcc | ||
|   | 3ccfb64be8 | ||
| 13d69e2f73 | |||
|   | 751cde86f9 | ||
|   | 1b95992297 | ||
|   | d8bb3fda82 | ||
|   | 4704d4a486 | ||
|   | c7b3ae7876 | ||
|   | 6fb876ade2 | ||
| 82d875136b | |||
| 28ead61112 | |||
| 688fc7a8ab | |||
| 07666fa984 | |||
|   | b3119c6ee1 | ||
|   | da16a267e4 | ||
|   | babbcbd1fc | ||
|   | f42940f847 | ||
|   | 3ff146a1b9 | ||
|   | 93865a1b16 | ||
|   | 44f9b05001 | ||
|   | 02279d19ae | ||
|   | cf581dc485 | ||
| 15e248d7d7 | |||
| cdb745d55a | |||
| c46a4cb0b7 | |||
|   | 058ba09c03 | ||
|   | dd53e5a14a | ||
|   | 3bd6b7363c | ||
|   | 2ee8c9916f | ||
|   | 3462078a47 | ||
|   | 8715b32139 | ||
|   | 4245e165bf | ||
|   | f4ff128236 | ||
|   | f09fe1d70b | ||
|   | 4f774160b3 | ||
|   | f6130c0b8c | ||
|   | e05b214a8a | ||
|   | 704249d0af | ||
|   | bf44b481e9 | ||
|   | 02fa03c824 | ||
|   | 4406003a6e | ||
|   | 3c282975c1 | ||
|   | aec8fea628 | ||
|   | 78b76b4aa6 | ||
|   | 91cfdbc37b | ||
|   | f822220c40 | ||
|   | 9032957631 | ||
|   | a140e0eb6f | ||
|   | 511b08e8e5 | ||
|   | 50afefeb5f | ||
|   | 07df16dbe9 | ||
|   | 8bc4c634fe | ||
|   | 7c48336a62 | ||
|   | fd279f0562 | ||
|   | 43e62fdeff | ||
|   | f23bda38f4 | ||
|   | 29a5df0b1a | ||
|   | 2597615630 | ||
|   | d29b4465ad | ||
|   | 73f0c67564 | ||
|   | 554166eeaf | ||
|   | 964c0a09b8 | ||
|   | 61979653e0 | ||
|   | 39b8d17498 | ||
|   | dd63f4c015 | ||
|   | 6267663015 | ||
|   | 89ea511072 | ||
|   | afe61ea7d6 | ||
|   | 2da06481f7 | ||
|   | 6c3f13f372 | ||
|   | 1ec9732607 | ||
|   | c7da20d577 | ||
|   | 8a2497bcac | ||
|   | 0746490786 | ||
|   | f895c19b14 | ||
|   | c4b35be053 | ||
|   | 54667db29b | ||
|   | 12a00a1f3a | ||
|   | 6ec99dc9a7 | ||
|   | 812920f46d | ||
|   | cd7180c3d7 | ||
|   | fc381ae6c2 | ||
|   | 8ab308d4b9 | ||
|   | 0c4204fb6e | ||
|   | c9c6a5b7f0 | ||
|   | 4ca6e1fb2c | ||
|   | 1a20c1a9d2 | ||
|   | eaf8458835 | ||
|   | f8cedf4cb4 | ||
|   | d0577f163b | ||
|   | 8293c44864 | ||
|   | c1a9f16faa | ||
|   | 99b51d4cc0 | ||
|   | b46d8d7af9 | ||
|   | c8b0dad7c2 | ||
|   | 7f53caaac6 | ||
|   | fe1c0b16ce | ||
|   | 4ebe7399ba | ||
|   | 2dc639d59a | ||
|   | a6ba949061 | ||
|   | de7b5ce557 | ||
|   | cb7d4b1f66 | ||
| f26df3b1a4 | |||
|   | 1997b9d1c0 | ||
|   | e555bfa8fb | 
| @@ -1 +1 @@ | ||||
| TOKEN_TELEGRAM_BOT= | ||||
| BOT_TOKEN=YOUR_BOT_TOKEN | ||||
							
								
								
									
										212
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										212
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,12 +1,212 @@ | ||||
| .env | ||||
| !*.sample | ||||
|  | ||||
| # Byte-compiled / optimized / DLL files | ||||
| __pycache__/ | ||||
| *.pyc | ||||
| *.py[codz] | ||||
| *$py.class | ||||
|  | ||||
| # C extensions | ||||
| *.so | ||||
|  | ||||
| # Distribution / packaging | ||||
| .Python | ||||
| build/ | ||||
| develop-eggs/ | ||||
| dist/ | ||||
| downloads/ | ||||
| eggs/ | ||||
| .eggs/ | ||||
| lib/ | ||||
| lib64/ | ||||
| parts/ | ||||
| sdist/ | ||||
| var/ | ||||
| wheels/ | ||||
| share/python-wheels/ | ||||
| *.egg-info/ | ||||
| .installed.cfg | ||||
| *.egg | ||||
| MANIFEST | ||||
|  | ||||
| # PyInstaller | ||||
| #  Usually these files are written by a python script from a template | ||||
| #  before PyInstaller builds the exe, so as to inject date/other infos into it. | ||||
| *.manifest | ||||
| *.spec | ||||
|  | ||||
| # Installer logs | ||||
| pip-log.txt | ||||
| pip-delete-this-directory.txt | ||||
|  | ||||
| # Unit test / coverage reports | ||||
| htmlcov/ | ||||
| .tox/ | ||||
| .nox/ | ||||
| .coverage | ||||
| .coverage.* | ||||
| .cache | ||||
| nosetests.xml | ||||
| coverage.xml | ||||
| *.cover | ||||
| *.py.cover | ||||
| .hypothesis/ | ||||
| .pytest_cache/ | ||||
| cover/ | ||||
|  | ||||
| # Translations | ||||
| *.mo | ||||
| *.pot | ||||
|  | ||||
| # Django stuff: | ||||
| *.log | ||||
| local_settings.py | ||||
| db.sqlite3 | ||||
| db.sqlite3-journal | ||||
|  | ||||
| # Flask stuff: | ||||
| instance/ | ||||
| .webassets-cache | ||||
|  | ||||
| # Scrapy stuff: | ||||
| .scrapy | ||||
|  | ||||
| # Sphinx documentation | ||||
| docs/_build/ | ||||
|  | ||||
| # PyBuilder | ||||
| .pybuilder/ | ||||
| target/ | ||||
|  | ||||
| # Jupyter Notebook | ||||
| .ipynb_checkpoints | ||||
|  | ||||
| # IPython | ||||
| profile_default/ | ||||
| ipython_config.py | ||||
|  | ||||
| # pyenv | ||||
| #   For a library or package, you might want to ignore these files since the code is | ||||
| #   intended to run in multiple environments; otherwise, check them in: | ||||
| # .python-version | ||||
|  | ||||
| # pipenv | ||||
| #   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. | ||||
| #   However, in case of collaboration, if having platform-specific dependencies or dependencies | ||||
| #   having no cross-platform support, pipenv may install dependencies that don't work, or not | ||||
| #   install all needed dependencies. | ||||
| #Pipfile.lock | ||||
|  | ||||
| # UV | ||||
| #   Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. | ||||
| #   This is especially recommended for binary packages to ensure reproducibility, and is more | ||||
| #   commonly ignored for libraries. | ||||
| #uv.lock | ||||
|  | ||||
| # poetry | ||||
| #   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. | ||||
| #   This is especially recommended for binary packages to ensure reproducibility, and is more | ||||
| #   commonly ignored for libraries. | ||||
| #   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control | ||||
| #poetry.lock | ||||
| #poetry.toml | ||||
|  | ||||
| # pdm | ||||
| #   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. | ||||
| #   pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. | ||||
| #   https://pdm-project.org/en/latest/usage/project/#working-with-version-control | ||||
| #pdm.lock | ||||
| #pdm.toml | ||||
| .pdm-python | ||||
| .pdm-build/ | ||||
|  | ||||
| # pixi | ||||
| #   Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. | ||||
| #pixi.lock | ||||
| #   Pixi creates a virtual environment in the .pixi directory, just like venv module creates one | ||||
| #   in the .venv directory. It is recommended not to include this directory in version control. | ||||
| .pixi | ||||
|  | ||||
| # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm | ||||
| __pypackages__/ | ||||
|  | ||||
| # Celery stuff | ||||
| celerybeat-schedule | ||||
| celerybeat.pid | ||||
|  | ||||
| # SageMath parsed files | ||||
| *.sage.py | ||||
|  | ||||
| # Environments | ||||
| .idea | ||||
| /.idea | ||||
| .env | ||||
| .envrc | ||||
| .venv | ||||
| env/ | ||||
| venv/ | ||||
| .venv/ | ||||
| myenv | ||||
| ENV/ | ||||
| env.bak/ | ||||
| venv.bak/ | ||||
| /logger_helper/loggers | ||||
| /app/bybit/logger_bybit/loggers | ||||
| *.db | ||||
| # Spyder project settings | ||||
| .spyderproject | ||||
| .spyproject | ||||
|  | ||||
| requirements.txt | ||||
| # 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,37 +0,0 @@ | ||||
| import asyncio | ||||
|  | ||||
| from aiogram import Bot, Dispatcher | ||||
| from aiogram.filters import Command, CommandStart | ||||
| from aiogram.types import Message | ||||
|  | ||||
| from app.telegram.database.models import async_main | ||||
|  | ||||
| from app.telegram.handlers.handlers import router  | ||||
| from app.telegram.functions.main_settings.settings import router_main_settings  | ||||
| from app.telegram.functions.risk_management_settings.settings import router_risk_management_settings | ||||
| 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 config import TOKEN_TG_BOT | ||||
|  | ||||
| from app.telegram.logs import logger | ||||
|  | ||||
| bot = Bot(token=TOKEN_TG_BOT) | ||||
| dp = Dispatcher() | ||||
|  | ||||
| async def main(): | ||||
|     await async_main() | ||||
|  | ||||
|     dp.include_router(router) | ||||
|     dp.include_router(router_main_settings) | ||||
|     dp.include_router(router_risk_management_settings) | ||||
|     dp.include_router(router_register_bybit_api) | ||||
|     dp.include_router(router_functions_bybit_trade) | ||||
|  | ||||
|     await dp.start_polling(bot) | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     try: | ||||
|         asyncio.run(main()) | ||||
|     except KeyboardInterrupt: | ||||
|         print("Bot is off") | ||||
| @@ -1,69 +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\config.py" /> | ||||
|     <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 | ||||
							
								
								
									
										135
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										135
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,20 +1,117 @@ | ||||
| # Чат-робот STCS | ||||
| __ | ||||
| Crypto Trading Telegram Bot | ||||
|  | ||||
| **Функционал:** | ||||
| + **Настройки** | ||||
|   + Основные параметры *(Настроен, работает)* | ||||
|     + Режим торговли Лонг/Шорт *(настроены)*, Switch/Smart *(не настроены)* | ||||
|     + Тип маржи: Изолированная / Кросс *(настроено)* | ||||
|     + Размер кредитного плеча: от x1 до x100 *(настроено)* | ||||
|     + Начальная ставка: числовое значение *(настроено)* | ||||
|     + Коэффициент мартингейла: число *(настроено)* | ||||
|     + Максимальное количество ставок в серии: число *(настроено)* | ||||
|   + Риск-менеджмент (Настроен, работает) | ||||
|     + Процент изменения цены для фиксации прибыли (TP%): число *(настроено)* | ||||
|     + Процент изменения цены для фиксации убытков (SL%): число (пример: 1%) *(настроено)* | ||||
|     + Максимальный риск на сделку (в % от баланса): число (опционально) *(настроено)* | ||||
|    + Условия запуска *(Не настроен)* | ||||
|    + Дополнительные параметры *(Не настроен)* | ||||
|    + Подключение Bybit *(настроено)* | ||||
|     + Информация о правильном получении и сохранении Bybit-API keys *(настроено)* | ||||
| Этот бот — автоматизированный торговый помощник для работы с криптовалютной биржей Bybit на основе стратегии мартингейла. Он позволяет торговать бессрочными контрактами с управлением рисками, тейк-профитами, стоп-лоссами и кредитным плечом. | ||||
|  | ||||
| ## Основные возможности | ||||
|  | ||||
| - Поддержка работы с биржей Bybit через официальный API. | ||||
|  | ||||
| - Открытие и закрытие позиций по выбранным торговым парам. | ||||
|  | ||||
| - Поддержка рыночных и лимитных ордеров. | ||||
|  | ||||
| - Установка уровней тейк-профита (TP) и стоп-лосса (SL). | ||||
|  | ||||
| - Управление кредитным плечом (leverage). | ||||
|  | ||||
| - Реализация стратегии мартингейла с настройками шага, коэффициента и лимитов. | ||||
|  | ||||
| - Контроль максимального риска на сделку по балансу пользователя. | ||||
|  | ||||
| - Обработка ошибок API, логирование событий и информирование пользователя. | ||||
|  | ||||
| - Таймеры для отложенного открытия и закрытия сделок. | ||||
|  | ||||
| - Интерактивное меню и ввод настроек через Telegram. | ||||
|  | ||||
| - Хранение пользовательских настроек и статистики в базе данных. | ||||
|  | ||||
|  | ||||
| ## Установка | ||||
|  | ||||
| 1. Клонируйте репозиторий: | ||||
|  | ||||
|  | ||||
| ```bash | ||||
| git clone https://git.svoboda.works/kodorvan/stcs | ||||
| ``` | ||||
|  | ||||
| 2. Установите зависимости: | ||||
|  | ||||
| ```bash | ||||
| pip install -r requirements.txt | ||||
| ``` | ||||
| или для отдельного пользователя | ||||
| ```bash | ||||
| sudo -u www-data /usr/bin/pip install -r requirements.txt | ||||
| ``` | ||||
|  | ||||
| 3. Зарегистрируйте чат-робота и сгенерируйте ключ авторизации<br> | ||||
| [@BotFather](https://t.me/BotFather) | ||||
|  | ||||
| 4. Создайте файл .env и настройте переменные окружения | ||||
| ```bash | ||||
| cp .env.sample .env | ||||
| nvim .env | ||||
| ``` | ||||
| 5. Выполните миграции: | ||||
| ```bash | ||||
| alembic upgrade head | ||||
| ``` | ||||
|  | ||||
| 5. Запустите бота: | ||||
|  | ||||
| ```bash | ||||
| 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 | ||||
| ``` | ||||
|  | ||||
| ## Настройки пользователя | ||||
|  | ||||
| - Кредитное плечо (например, 15x) | ||||
|  | ||||
| - Торговая пара (например, DOGEUSDT, BTCUSDT) | ||||
|  | ||||
| - Начальное количество для сделок | ||||
|  | ||||
| - Тип ордера (Market или Limit) | ||||
|  | ||||
| - Уровни Take Profit и Stop Loss (в процентах или цене) | ||||
|  | ||||
| - Коэффициент мартингейла и максимальное количество шагов | ||||
|  | ||||
| - Максимально допустимый риск на одну сделку (% от баланса) | ||||
|  | ||||
| - Таймеры для старта и закрытия сделок | ||||
|  | ||||
|  | ||||
| ## Безопасность и риски | ||||
|  | ||||
| - Бот требует аккуратной настройки параметров риска. | ||||
|  | ||||
| - Храните API ключи в безопасности, избегайте публикации. | ||||
							
								
								
									
										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"} | ||||
							
								
								
									
										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, | ||||
|         }, | ||||
|     }, | ||||
| } | ||||
							
								
								
									
										408
									
								
								app/bybit/open_positions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										408
									
								
								app/bybit/open_positions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,408 @@ | ||||
| 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) | ||||
|         user_deals_data = await rq.get_user_deal_by_symbol( | ||||
|             tg_id=tg_id, symbol=symbol | ||||
|         ) | ||||
|         trade_mode = additional_data.trade_mode | ||||
|         switch_side = additional_data.switch_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 | ||||
|  | ||||
|         get_side = "Buy" | ||||
|  | ||||
|         if user_deals_data: | ||||
|             get_side = user_deals_data.last_side or "Buy" | ||||
|  | ||||
|         if trade_mode == "Switch": | ||||
|             if switch_side == "По направлению": | ||||
|                 side = get_side | ||||
|             else: | ||||
|                 if get_side == "Buy": | ||||
|                     side = "Sell" | ||||
|                 else: | ||||
|                     side = "Buy" | ||||
|         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) | ||||
|         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 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 | ||||
							
								
								
									
										263
									
								
								app/bybit/telegram_message_handler.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										263
									
								
								app/bybit/telegram_message_handler.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,263 @@ | ||||
| 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")) | ||||
|             risk_management_data = await rq.get_user_risk_management(tg_id=tg_id) | ||||
|             commission_fee = risk_management_data.commission_fee | ||||
|  | ||||
|             if commission_fee == "Yes_commission_fee": | ||||
|                 total_pnl = safe_float(exec_pnl) - safe_float(exec_fee) - fee | ||||
|             else: | ||||
|                 total_pnl = safe_float(exec_pnl) | ||||
|  | ||||
|             header = ( | ||||
|                 "Сделка закрыта:" if safe_float(closed_size) > 0 else "Сделка открыта:" | ||||
|             ) | ||||
|             text = f"{header}\n" f"Торговая пара: {symbol}\n" | ||||
|  | ||||
|             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,73 +0,0 @@ | ||||
| from aiogram import F, Router | ||||
|  | ||||
| import app.telegram.Keyboards.inline_keyboards as inline_markup | ||||
|  | ||||
| import app.telegram.database.requests as rq | ||||
| from aiogram.types import Message, CallbackQuery | ||||
|  | ||||
| # FSM - Механизм состояния | ||||
| from aiogram.fsm.state import State, StatesGroup | ||||
| from aiogram.fsm.context import FSMContext | ||||
|  | ||||
| router_register_bybit_api = Router() | ||||
|  | ||||
| class state_reg_bybit_api(StatesGroup): | ||||
|     api_key = State() | ||||
|     secret_key = State() | ||||
|  | ||||
| @router_register_bybit_api.callback_query(F.data == 'clb_new_user_connect_bybit_api_message') | ||||
| async def info_for_bybit_api_message(callback: CallbackQuery): | ||||
|     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):    | ||||
|     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): | ||||
|     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): | ||||
|     await state.update_data(secret_key = message.text) | ||||
|  | ||||
|     data = await state.get_data() | ||||
|  | ||||
|     await rq.update_api_key(message.from_user.id, data['api_key']) | ||||
|     await rq.update_secret_key(message.from_user.id, data['secret_key']) | ||||
|     await rq.set_new_user_symbol(message.from_user.id) | ||||
|      | ||||
|     await state.clear() | ||||
|  | ||||
|     await message.answer('Данные добавлены, нажмите на профиль и начните торговлю!') | ||||
|  | ||||
|      | ||||
| @@ -1,286 +0,0 @@ | ||||
| import time | ||||
|  | ||||
| from typing import Optional | ||||
| from asyncio import Handle | ||||
|  | ||||
| from annotated_types import T | ||||
| from pybit import exceptions | ||||
| from pybit.unified_trading import HTTP | ||||
| from pybit.unified_trading import WebSocket | ||||
|  | ||||
| from app.services.Bybit.functions import price_symbol | ||||
| import app.services.Bybit.functions.balance as balance_g | ||||
| import app.telegram.database.requests as rq | ||||
|  | ||||
| import logging | ||||
| logging.basicConfig(level=logging.DEBUG) | ||||
|  | ||||
| def handle_message(message): | ||||
|     print(message) | ||||
|  | ||||
| async def info_access_open_deal(message, symbol, trade_mode, margin_mode, leverage, qty): | ||||
|     match margin_mode: | ||||
|         case 'ISOLATED_MARGIN': | ||||
|             margin_mode = 'Isolated' | ||||
|         case 'REGULAR_MARGIN': | ||||
|             margin_mode = 'Cross' | ||||
|  | ||||
|     text = f'''Позиция была успешна открыта! | ||||
|        Торговая пара: {symbol} | ||||
|        Движение: {trade_mode} | ||||
|        Тип-маржи: {margin_mode} | ||||
|        Кредитное плечо: {leverage} | ||||
|        Количество: {qty} | ||||
|     ''' | ||||
|  | ||||
|     await message.answer(text=text, parse_mode='html') | ||||
|  | ||||
| async def error_max_step(message): | ||||
|     await message.answer('Сделка не была совершена, превышен лимит максимального количества ставок') | ||||
|  | ||||
| async def error_max_risk(message): | ||||
|     await message.answer('Сделка не была совершена, слишком высокий риск') | ||||
|  | ||||
| async def contract_long(tg_id, message, margin_mode): | ||||
|     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) | ||||
|  | ||||
|     data_main_stgs = await rq.get_user_main_settings(tg_id) | ||||
|     data_risk_management_stgs = await rq.get_user_risk_management_settings(tg_id) | ||||
|  | ||||
|     match margin_mode: | ||||
|         case 'Isolated': | ||||
|             margin_mode = 'ISOLATED_MARGIN' | ||||
|         case 'Cross': | ||||
|             margin_mode = 'REGULAR_MARGIN' | ||||
|  | ||||
|     client = HTTP( | ||||
|         api_key=api_key, | ||||
|         api_secret=secret_key | ||||
|     ) | ||||
|  | ||||
|     try:       | ||||
|         balance = 0 | ||||
|         price = 0 | ||||
|   | ||||
|         balance = await balance_g.get_balance(tg_id) | ||||
|         price = await price_symbol.get_price(tg_id, message) | ||||
|  | ||||
|         client.set_margin_mode( | ||||
|             setMarginMode=margin_mode # margin_type  | ||||
|         ) | ||||
|  | ||||
|         martingale_factor = float(data_main_stgs['martingale_factor'])  | ||||
|         max_martingale_steps = int(data_main_stgs['maximal_quantity']) | ||||
|         starting_quantity = float(data_main_stgs['starting_quantity']) | ||||
|         max_risk_percent = float(data_risk_management_stgs['max_risk_deal']) | ||||
|         loss_profit = float(data_risk_management_stgs['price_loss']) | ||||
|         takeProfit= float(data_risk_management_stgs['price_profit']) | ||||
|          | ||||
|         # Инициализация переменных | ||||
|         next_quantity = starting_quantity | ||||
|         last_quantity = starting_quantity | ||||
|         realised_pnl = 0.0 | ||||
|  | ||||
|         current_martingale_step = 0  # Текущая ставка в серии | ||||
|  | ||||
|         next_quantity = 0 | ||||
|         realised_pnl = 0 | ||||
|  | ||||
|         last_quantity = starting_quantity | ||||
|  | ||||
|         # Пример расчёта следующего размера позиции | ||||
|         try: | ||||
|             position_info = client.get_positions(category='linear', symbol=SYMBOL) | ||||
|             position = position_info['result']['list'][0]  # или другой нужный индекс | ||||
|  | ||||
|             realised_pnl = float(position['unrealisedPnl']) | ||||
|  | ||||
|             if realised_pnl > 0: | ||||
|                 print(f''' | ||||
|         ===================== | ||||
|         =====Сделка========= | ||||
|         ===уСПЕШНЕАЯ================ | ||||
|         =================== | ||||
|         ================= | ||||
|  | ||||
|             {realised_pnl} | ||||
|  | ||||
|         =============== | ||||
|         =============== | ||||
|         ============= | ||||
|         =============== | ||||
|         ============== | ||||
|         ''') | ||||
|                 starting_quantity = next_quantity | ||||
|                 current_martingale_step = 0 | ||||
|             elif not realised_pnl: | ||||
|                 next_quantity = starting_quantity | ||||
|                 current_martingale_step += 1 | ||||
|             else: | ||||
|                 current_martingale_step += 1 | ||||
|                 next_quantity = last_quantity * martingale_factor | ||||
|                 starting_quantity = next_quantity | ||||
|  | ||||
|             print(f''' | ||||
|         ======СДЕЛКА=============== | ||||
|         =====УБЫТОЧНАЯ============== | ||||
|         =================== | ||||
|         =================== | ||||
|         ================= | ||||
|  | ||||
|             {realised_pnl} | ||||
|  | ||||
|         =============== | ||||
|         =============== | ||||
|         ============= | ||||
|         =============== | ||||
|         ============== | ||||
|         ''') | ||||
|         except Exception as e: | ||||
|             print("Не получены позиции") | ||||
|             next_quantity = starting_quantity | ||||
|  | ||||
|         potential_loss = (next_quantity * float(price)) * (loss_profit / 100) | ||||
|         allowed_loss = float(balance) * (max_risk_percent / 100) | ||||
|  | ||||
|         if current_martingale_step >= max_martingale_steps: | ||||
|             print("Достигнут максимум ставок в серии (8)!") | ||||
|             print("Торговля не продолжится") | ||||
|  | ||||
|             await error_max_step(message) | ||||
|         else: | ||||
|             if potential_loss > allowed_loss: | ||||
|                 print(f"ОШИБКА: Риск превышен!") | ||||
|                 print(f"Ручной qty = {next_quantity} → Убыток = {potential_loss} USDT") | ||||
|                 print(f"Разрешено = {allowed_loss} USDT (1% от баланса)") | ||||
|  | ||||
|                 await error_max_risk(message) | ||||
|             else: | ||||
|                 print(f"Риск в допустимых пределах. Qty = {next_quantity}") | ||||
|  | ||||
|                 r = client.place_order(       | ||||
|                     category='linear', | ||||
|                     symbol=SYMBOL, | ||||
|                     side='Buy',  | ||||
|                     orderType="Market", | ||||
|                     leverage=int(data_main_stgs['size_leverage']), | ||||
|                     qty=next_quantity, | ||||
|                     takeProfit=takeProfit, # TP - закрывает позицию, когда цена достигает нужного уровня | ||||
|                     stopProfit=float(data_risk_management_stgs['price_loss']), # SL - закрывает позицию, когда убыток достигает нужного уровня | ||||
|                     orderLinkId=f"deal_{SYMBOL}_{time.time()}" | ||||
|                 ) | ||||
|  | ||||
|                 await info_access_open_deal(message, SYMBOL, data_main_stgs['trading_mode'], margin_mode, data_main_stgs['size_leverage'], next_quantity)     | ||||
|          | ||||
|     except exceptions.InvalidRequestError as e: | ||||
|         await message.answer('Недостаточно баланса') | ||||
|     except Exception as e: | ||||
|         await message.answer('Непредвиденная оишбка') | ||||
|  | ||||
| async def contract_short(tg_id, message, margin_mode): | ||||
|     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) | ||||
|  | ||||
|     data_main_stgs = await rq.get_user_main_settings(tg_id) | ||||
|     data_risk_management_stgs = await rq.get_user_risk_management_settings(tg_id) | ||||
|  | ||||
|     match margin_mode: | ||||
|         case 'Isolated': | ||||
|             margin_mode = 'ISOLATED_MARGIN' | ||||
|         case 'Cross': | ||||
|             margin_mode = 'REGULAR_MARGIN' | ||||
|  | ||||
|     client = HTTP( | ||||
|         api_key=api_key, | ||||
|         api_secret=secret_key | ||||
|     ) | ||||
|  | ||||
|     try:       | ||||
|         balance = 0 | ||||
|         price = 0 | ||||
|   | ||||
|         balance = await balance_g.get_balance(tg_id) | ||||
|         price = await price_symbol.get_price(tg_id, message) | ||||
|  | ||||
|         client.set_margin_mode( | ||||
|             setMarginMode=margin_mode # margin_type  | ||||
|         ) | ||||
|  | ||||
|         martingale_factor = float(data_main_stgs['martingale_factor'])   | ||||
|         max_martingale_steps = int(data_main_stgs['maximal_quantity']) | ||||
|         starting_quantity = float(data_main_stgs['starting_quantity']) | ||||
|         max_risk_percent = float(data_risk_management_stgs['max_risk_deal']) | ||||
|         loss_profit = float(data_risk_management_stgs['price_loss']) | ||||
|         takeProfit = float(data_risk_management_stgs['price_profit']) | ||||
|          | ||||
|         # Инициализация переменных | ||||
|         next_quantity = starting_quantity | ||||
|         last_quantity = starting_quantity | ||||
|         realised_pnl = 0.0 | ||||
|  | ||||
|         current_martingale_step = 0  # Текущая ставка в серии | ||||
|  | ||||
|         next_quantity = 0 | ||||
|         realised_pnl = 0 | ||||
|  | ||||
|         last_quantity = starting_quantity | ||||
|  | ||||
|         # Пример расчёта следующего размера позиции | ||||
|         try: | ||||
|             position_info = client.get_positions(category='linear', symbol=SYMBOL) | ||||
|             position = position_info['result']['list'][0]  # или другой нужный индекс | ||||
|  | ||||
|             realised_pnl = float(position['unrealisedPnl']) | ||||
|  | ||||
|             if realised_pnl > 0: # Прибыльная сделка | ||||
|                 starting_quantity = next_quantity | ||||
|                 current_martingale_step = 0 | ||||
|             elif not realised_pnl: | ||||
|                 next_quantity = starting_quantity | ||||
|                 current_martingale_step += 1 | ||||
|             else: # Убыточная сделка | ||||
|                 current_martingale_step += 1 | ||||
|                 next_quantity = last_quantity * martingale_factor | ||||
|                 starting_quantity = next_quantity | ||||
|  | ||||
|         except Exception as e: | ||||
|             print("Не получены позиции") | ||||
|             next_quantity = starting_quantity | ||||
|  | ||||
|         potential_loss = (next_quantity * float(price)) * (loss_profit / 100) | ||||
|         allowed_loss = float(balance) * (max_risk_percent / 100) | ||||
|  | ||||
|         if current_martingale_step >= max_martingale_steps: | ||||
|             print("Достигнут максимум ставок в серии (8)!") | ||||
|             print("Торговля не продолжится") | ||||
|  | ||||
|             await error_max_step(message) | ||||
|         else: | ||||
|             if potential_loss > allowed_loss: | ||||
|                 print(f"ОШИБКА: Риск превышен!") | ||||
|                 print(f"Ручной qty = {next_quantity} → Убыток = {potential_loss} USDT") | ||||
|                 print(f"Разрешено = {allowed_loss} USDT (1% от баланса)") | ||||
|  | ||||
|                 await error_max_risk(message) | ||||
|             else: | ||||
|                 print(f"Риск в допустимых пределах. Qty = {next_quantity}") | ||||
|  | ||||
|                 r = client.place_order(       | ||||
|                     category='linear', | ||||
|                     symbol=SYMBOL, | ||||
|                     side='Sell',  | ||||
|                     orderType="Market", | ||||
|                     leverage=int(data_main_stgs['size_leverage']), | ||||
|                     qty=next_quantity, | ||||
|                     orderLinkId=f"deal_{SYMBOL}_{time.time()}" | ||||
|                 ) | ||||
|  | ||||
|                 await info_access_open_deal(message, SYMBOL, data_main_stgs['trading_mode'], margin_mode, data_main_stgs['size_leverage'], next_quantity)    | ||||
|          | ||||
|     except exceptions.InvalidRequestError as e: | ||||
|         await message.answer('Недостаточно баланса') | ||||
|     except Exception as e: | ||||
|         await message.answer('Непредвиденная оишбка') | ||||
| @@ -1,22 +0,0 @@ | ||||
| import app.telegram.database.requests as rq | ||||
|  | ||||
| from pybit.unified_trading import HTTP | ||||
|  | ||||
| client = HTTP() | ||||
|  | ||||
| async def get_balance(tg_id, message): | ||||
|     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: | ||||
|         balance = client.get_wallet_balance(accountType='UNIFIED', coin='USDT')['result']['list'][0]['coin'][0]['walletBalance'] | ||||
|  | ||||
|         return balance | ||||
|     except Exception as e: | ||||
|         await message.answer('Баланс не был получен, подключите платформу')  | ||||
|         return 0 | ||||
| @@ -1,21 +0,0 @@ | ||||
| import app.telegram.database.requests as rq | ||||
| import app.services.Bybit.functions.price_symbol as price_s | ||||
|  | ||||
| from pybit.unified_trading import HTTP | ||||
|  | ||||
| client = HTTP() | ||||
|  | ||||
| async def get_min_qty(tg_id, message): | ||||
|     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 price_s.get_price(tg_id, message) | ||||
|     min_qty = int(5 / price * 1.1) | ||||
|  | ||||
|     return min_qty | ||||
| @@ -1,103 +0,0 @@ | ||||
| from aiogram import F, Router | ||||
|  | ||||
| from app.services.Bybit.functions import Futures, func_min_qty | ||||
| 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 | ||||
|  | ||||
| # FSM - Механизм состояния | ||||
| from aiogram.fsm.state import State, StatesGroup | ||||
| from aiogram.fsm.context import FSMContext | ||||
|  | ||||
| router_functions_bybit_trade = Router() | ||||
|  | ||||
| class state_update_symbol(StatesGroup): | ||||
|     symbol = State() | ||||
|      | ||||
| @router_functions_bybit_trade.callback_query(F.data == 'clb_start_trading') | ||||
| async def clb_start_bybit_trade_message(callback: CallbackQuery, state: FSMContext): | ||||
|     api = await rq.get_bybit_api_key(callback.from_user.id) | ||||
|     secret = await rq.get_bybit_secret_key(callback.from_user.id) | ||||
|  | ||||
|     if api and secret: | ||||
|         balance = await get_balance(callback.from_user.id, callback.message) | ||||
|         symbol = await rq.get_symbol(callback.from_user.id) | ||||
|  | ||||
|         text = f'''💎 Торговля на Bybit | ||||
|  | ||||
| ⚖️ Ваш баланс (USDT): {balance}   | ||||
| 📊 Текущая торговая пара: {symbol} | ||||
|  | ||||
| --- | ||||
|  | ||||
| Как начать торговлю? | ||||
|  | ||||
| 1️⃣ Проверьте и тщательно настройте все параметры в вашем профиле.   | ||||
| 2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару заглавными буквами, без лишних символов (например: BTCUSDT).   | ||||
| ''' | ||||
|         await callback.message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup) | ||||
|     else: | ||||
|         callback.message.answer('Перед началом работы, в настройках подключите bybit') | ||||
|  | ||||
| async def start_bybit_trade_message(message, state): | ||||
|     api = await rq.get_bybit_api_key(message.from_user.id) | ||||
|     secret = await rq.get_bybit_secret_key(message.from_user.id) | ||||
|  | ||||
|     if api and secret: | ||||
|         balance = await get_balance(message.from_user.id, message) | ||||
|         symbol = await rq.get_symbol(message.from_user.id) | ||||
|  | ||||
|         text = f'''Торговля на Bybit | ||||
|  | ||||
| <b>ваш баланс (USDT):</b> {balance} | ||||
| <b>Текущая торговая пара: </b> {symbol} | ||||
|      | ||||
| Как начать торговлю? | ||||
| 1. Внимательно проверьте и настройте все параметры в вашем профиле | ||||
| 2. Ниже нажмите 'Указать торговую пару' и отправьте торговую пару заглавными буквами, указав два актива без всяких лишних символов! (Пример: BTCUSDT) | ||||
| ''' | ||||
|  | ||||
|         await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup) | ||||
|     else: | ||||
|         await message.answer('Перед началом работы, в настройках подключите bybit') | ||||
|  | ||||
| @router_functions_bybit_trade.callback_query(F.data == 'clb_update_trading_pair') | ||||
| async def update_symbol_for_trade_message(callback: CallbackQuery, state: FSMContext): | ||||
|     await state.set_state(state_update_symbol.symbol) | ||||
|  | ||||
|     await callback.message.answer(text='Укажите торговую пару заглавными буквами без пробелов и лишних символов (пример: BTCUSDT): ') | ||||
|  | ||||
| @router_functions_bybit_trade.message(state_update_symbol.symbol) | ||||
| async def update_symbol_for_trade(message: Message, state: FSMContext): | ||||
|     await state.update_data(symbol = message.text) | ||||
|  | ||||
|     data = await state.get_data() | ||||
|  | ||||
|     await rq.update_symbol(message.from_user.id, data['symbol']) | ||||
|     await start_bybit_trade_message(message, state) | ||||
|  | ||||
|     await state.clear() | ||||
|  | ||||
| @router_functions_bybit_trade.callback_query(F.data == 'clb_open_deal') | ||||
| async def make_deal_bybit (callback: CallbackQuery): | ||||
|     data_main_stgs = await rq.get_user_main_settings(callback.from_user.id) | ||||
|  | ||||
|     trade_mode = data_main_stgs['trading_mode'] | ||||
|     qty = data_main_stgs['starting_quantity'] | ||||
|     margin_mode = data_main_stgs['margin_type']  | ||||
|     qty_min = await func_min_qty.get_min_qty(callback.from_user.id, callback.message)                                                                  | ||||
|  | ||||
|     if qty < qty_min: | ||||
|         await callback.message.edit_text(f"Количество вашей ставки ({qty}) меньше минимального количества ({qty_min}) для данной торговой пары") | ||||
|     else: | ||||
|         match trade_mode:  | ||||
|             case 'Long': | ||||
|                 await Futures.contract_long(callback.from_user.id, callback.message, margin_mode) | ||||
|             case 'Short': | ||||
|                 await Futures.contract_short(callback.from_user.id, callback.message, margin_mode) | ||||
|             case 'Switch': | ||||
|                 await callback.message.edit_text('Режим Switch пока недоступен') | ||||
|             case 'Smart': | ||||
|                 await callback.message.edit_text('Режим Smart пока недоступен') | ||||
| @@ -1,23 +0,0 @@ | ||||
| from app.services.Bybit.functions import price_symbol | ||||
| import app.telegram.database.requests as rq | ||||
|  | ||||
| from pybit.unified_trading import HTTP | ||||
|  | ||||
| client = HTTP() | ||||
|  | ||||
| async def get_min_qty(tg_id): | ||||
|     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 price_symbol(tg_id) | ||||
|     json_data = client.get_instruments_info(symbol=SYMBOL, category='linear') | ||||
|  | ||||
|     min_qty = int(5 / price * 1.1) # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> 1% <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> 5 USDT | ||||
|      | ||||
|     return min_qty | ||||
| @@ -1,24 +0,0 @@ | ||||
| import app.telegram.database.requests as rq | ||||
|  | ||||
| from pybit import exceptions | ||||
| from pybit.unified_trading import HTTP | ||||
|  | ||||
| client = HTTP() | ||||
|  | ||||
| async def get_price(tg_id, message): | ||||
|     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 | ||||
|     ) | ||||
|      | ||||
|     try: | ||||
|         price = float(client.get_tickers(category='linear', symbol=SYMBOL).get('result').get('list')[0].get('ask1Price')) | ||||
|  | ||||
|         return price | ||||
|     except exceptions.InvalidRequestError as e: | ||||
|         await message.answer('Неверно указана торговая пара') | ||||
|         return 1.0 | ||||
| @@ -1,112 +0,0 @@ | ||||
| from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup | ||||
|  | ||||
| start_markup = InlineKeyboardMarkup(inline_keyboard=[     | ||||
|     [InlineKeyboardButton(text="🔥 Начать торговлю", callback_data="clb_start_chatbot_message")]    | ||||
| ]) | ||||
|  | ||||
| settings_markup = InlineKeyboardMarkup(inline_keyboard=[ | ||||
|     [InlineKeyboardButton(text="Настройки", callback_data='clb_settings_message')], | ||||
|     [InlineKeyboardButton(text="Запуск", callback_data='clb_start_trading')] | ||||
| ]) | ||||
|  | ||||
| back_btn_profile = [InlineKeyboardButton(text="Назад", callback_data='callback_profile')] | ||||
|  | ||||
| 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_profile | ||||
| ]) | ||||
|  | ||||
| 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_update_trading_pair')],         | ||||
|     [InlineKeyboardButton(text="Совершить сделку", callback_data='clb_open_deal')]         | ||||
| ])  | ||||
|  | ||||
| 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')]]) # Клавиатура для возврата к списку каталога настроек | ||||
|  | ||||
| main_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ | ||||
|     [InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_trading_mode'),  | ||||
|      InlineKeyboardButton(text='Тип маржи', callback_data='clb_change_margin_type')], | ||||
|  | ||||
|     [InlineKeyboardButton(text='Размер кредитного плеча', callback_data='clb_change_size_leverage'),  | ||||
|      InlineKeyboardButton(text='Начальная ставка', callback_data='clb_change_starting_quantity')], | ||||
|  | ||||
|     [InlineKeyboardButton(text='Коэффициент Мартингейла', callback_data='clb_change_martingale_factor'),  | ||||
|      InlineKeyboardButton(text='Максимльное кол-во ставок', callback_data='clb_change_maximum_quantity')], | ||||
|  | ||||
|      back_btn_list_settings | ||||
| ]) | ||||
|  | ||||
| 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')], | ||||
|  | ||||
|     back_btn_list_settings | ||||
| ]) | ||||
|  | ||||
| condition_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ | ||||
|     [InlineKeyboardButton(text='Триггер', callback_data='clb_change_trigger'),  | ||||
|      InlineKeyboardButton(text='Фильтр времени', callback_data='clb_change_filter_time')], | ||||
|  | ||||
|     [InlineKeyboardButton(text='Фильтр волатильности', callback_data='clb_change_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 | ||||
| ]) | ||||
|  | ||||
| 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 | ||||
| ]) | ||||
|  | ||||
| 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 | ||||
| ]) | ||||
|  | ||||
| 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_ruchnoy"), InlineKeyboardButton(text='TradingView', callback_data="clb_trigger_tradingview")],     | ||||
|     [InlineKeyboardButton(text="Автоматический", callback_data="clb_trigger_auto")]     | ||||
| ]) | ||||
|  | ||||
| buttons_yes_no_markup = InlineKeyboardMarkup(inline_keyboard=[  # ИЗМЕНИТЬ НА INLINE | ||||
|     [InlineKeyboardButton(text='Да', callback_data="clb_yes"), InlineKeyboardButton(text='Нет', callback_data="clb_yes")]          | ||||
| ]) | ||||
|  | ||||
| buttons_on_off_markup = InlineKeyboardMarkup(inline_keyboard=[  # ИЗМЕНИТЬ НА INLINE | ||||
|     [InlineKeyboardButton(text='Включить', callback_data="clb_on"), InlineKeyboardButton(text='Выключить', callback_data="clb_off")]         | ||||
| ]) | ||||
| @@ -1,6 +0,0 @@ | ||||
| from aiogram.types import ReplyKeyboardMarkup, KeyboardButton | ||||
|  | ||||
| base_buttons_markup = ReplyKeyboardMarkup(keyboard=[ | ||||
|     [KeyboardButton(text="👤 Профиль")],     | ||||
|     # [KeyboardButton(text="Настройки")]          | ||||
| ], resize_keyboard=True) | ||||
							
								
								
									
										0
									
								
								app/telegram/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/telegram/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -1,139 +0,0 @@ | ||||
| import logging | ||||
| logger = logging.getLogger(__name__) | ||||
|  | ||||
| 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 sqlalchemy import select, insert | ||||
|  | ||||
| engine = create_async_engine(url='sqlite+aiosqlite:///db.sqlite3') | ||||
|  | ||||
| async_session = async_sessionmaker(engine) | ||||
|  | ||||
| class Base(AsyncAttrs, DeclarativeBase): | ||||
|     pass | ||||
|  | ||||
| class User_Telegram_Id(Base): | ||||
|     __tablename__ = 'user_telegram_id' | ||||
|  | ||||
|     id: Mapped[int] = mapped_column(primary_key=True) | ||||
|  | ||||
|     tg_id = mapped_column(BigInteger) | ||||
|  | ||||
| class User_Bybit_API(Base): | ||||
|     __tablename__ = 'user_bybit_api' | ||||
|  | ||||
|     id: Mapped[int] = mapped_column(primary_key=True) | ||||
|  | ||||
|     tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) | ||||
|  | ||||
|     api_key = mapped_column(String(18), default='None') | ||||
|     secret_key = mapped_column(String(36), default='None') | ||||
|  | ||||
| class User_Symbol(Base): | ||||
|     __tablename__ = 'user_symbols' | ||||
|  | ||||
|     id: Mapped[int] = mapped_column(primary_key=True) | ||||
|  | ||||
|     tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) | ||||
|  | ||||
|     symbol = mapped_column(String(18), default='PENGUUSDT') | ||||
|  | ||||
| class Trading_Mode(Base): | ||||
|     __tablename__ = 'trading_modes' | ||||
|  | ||||
|     id: Mapped[int] = mapped_column(primary_key=True) | ||||
|  | ||||
|     mode = mapped_column(String(10), unique=True) | ||||
|  | ||||
| class Margin_type(Base): | ||||
|     __tablename__ = 'margin_types' | ||||
|  | ||||
|     id: Mapped[int] = mapped_column(primary_key=True) | ||||
|  | ||||
|     type = mapped_column(String(15), unique=True) | ||||
|  | ||||
| class Trigger(Base): | ||||
|     __tablename__ = 'triggers' | ||||
|  | ||||
|     id: Mapped[int] = mapped_column(primary_key=True) | ||||
|  | ||||
|     trigger = mapped_column(String(15), unique=True) | ||||
|  | ||||
| class User_Main_Settings(Base): | ||||
|     __tablename__ = 'user_main_settings' | ||||
|  | ||||
|     id: Mapped[int] = mapped_column(primary_key=True) | ||||
|  | ||||
|     tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) | ||||
|  | ||||
|     trading_mode = mapped_column(ForeignKey("trading_modes.mode")) | ||||
|     margin_type = mapped_column(ForeignKey("margin_types.type")) | ||||
|     size_leverage = mapped_column(Integer(), default=1) | ||||
|     starting_quantity = mapped_column(Integer(), default=1) | ||||
|     martingale_factor = mapped_column(Integer(), default=1) | ||||
|     maximal_quantity = mapped_column(Integer(), default=10) | ||||
|  | ||||
| class User_Risk_Management_Settings(Base): | ||||
|     __tablename__ = 'user_risk_management_settings' | ||||
|  | ||||
|     id: Mapped[int] = mapped_column(primary_key=True) | ||||
|  | ||||
|     tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) | ||||
|  | ||||
|     price_profit = mapped_column(Integer(), default=1) | ||||
|     price_loss = mapped_column(Integer(), default=1) | ||||
|     max_risk_deal = mapped_column(Integer(), default=1) | ||||
|  | ||||
| class User_Condition_Settings(Base): | ||||
|     __tablename__ = 'user_condition_settings' | ||||
|  | ||||
|     id: Mapped[int] = mapped_column(primary_key=True) | ||||
|  | ||||
|     tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) | ||||
|  | ||||
|     trigger = mapped_column(ForeignKey("triggers.trigger")) | ||||
|     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): | ||||
|     __tablename__ = 'user_additional_settings' | ||||
|  | ||||
|     id: Mapped[int] = mapped_column(primary_key=True) | ||||
|  | ||||
|     tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) | ||||
|  | ||||
|     pattern_save = mapped_column(Boolean, default=False) | ||||
|     autostart = mapped_column(Boolean, default=False) | ||||
|     notifications = mapped_column(Boolean, default=False) | ||||
|  | ||||
| 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)) | ||||
|  | ||||
|         triggers = ['Ручной', 'Автоматический', 'TradingView'] | ||||
|         for trigger in triggers: | ||||
|             result = await conn.execute(select(Trigger).where(Trigger.trigger == trigger)) | ||||
|             if not result.first(): | ||||
|                 logger.info("Заполение таблицы триггеров") | ||||
|                 await conn.execute(Trigger.__table__.insert().values(trigger=trigger)) | ||||
| @@ -1,275 +0,0 @@ | ||||
| import logging | ||||
| logger = logging.getLogger(__name__) | ||||
|  | ||||
| from app.telegram.database.models import async_session | ||||
| from app.telegram.database.models import User_Telegram_Id as UTi | ||||
| from app.telegram.database.models import User_Main_Settings as UMS | ||||
| from app.telegram.database.models import User_Bybit_API as UBA | ||||
| from app.telegram.database.models import User_Symbol | ||||
| from app.telegram.database.models import User_Risk_Management_Settings as URMS | ||||
| from app.telegram.database.models import User_Condition_Settings as UCS  | ||||
| from app.telegram.database.models import User_Additional_Settings as UAS  | ||||
| from app.telegram.database.models import Trading_Mode | ||||
| from app.telegram.database.models import Margin_type | ||||
| from app.telegram.database.models import Trigger | ||||
|  | ||||
| import app.telegram.functions.functions as func # functions | ||||
|  | ||||
| from sqlalchemy import select, delete, update | ||||
|  | ||||
| # SET_DB | ||||
| async def save_tg_id_new_user(tg_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("Новый пользователь был добавлен в бд") | ||||
|  | ||||
|         await session.commit() | ||||
|  | ||||
| async def set_new_user_bybit_api(tg_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, | ||||
|             )) | ||||
|  | ||||
|             logger.info(f"Bybit был успешно подключен") | ||||
|  | ||||
|         await session.commit() | ||||
|  | ||||
| async def set_new_user_symbol(tg_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 был успешно добавлен") | ||||
|  | ||||
|         await session.commit() | ||||
|  | ||||
| async def set_new_user_default_main_settings(tg_id, trading_mode, margin_type) -> None: | ||||
|     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("Основные настройки нового пользователя были заполнены") | ||||
|  | ||||
|         await session.commit() | ||||
|  | ||||
| async def set_new_user_default_risk_management_settings(tg_id) -> None: | ||||
|     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("Риск-Менеджмент настройки нового пользователя были заполнены") | ||||
|  | ||||
|         await session.commit() | ||||
|  | ||||
| async def set_new_user_default_condition_settings(tg_id, trigger) -> None: | ||||
|     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("Условные настройки нового пользователя были заполнены") | ||||
|  | ||||
|         await session.commit() | ||||
|  | ||||
| async def set_new_user_default_additional_settings(tg_id) -> None: | ||||
|     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("Дополнительные настройки нового пользователя были заполнены") | ||||
|  | ||||
|         await session.commit() | ||||
|  | ||||
| # GET_DB | ||||
| async def check_user(tg_id): | ||||
|     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): | ||||
|     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): | ||||
|     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_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(): | ||||
|     async with async_session() as session: | ||||
|         trigger = await session.scalar(select(Trigger.trigger).where(Trigger.id == 1)) | ||||
|         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: | ||||
|             logger.info("Получение основных настроек пользователя") | ||||
|  | ||||
|             trading_mode = await session.scalar(select(UMS.trading_mode).where(UMS.tg_id == tg_id))  | ||||
|             margin_mode = await session.scalar(select(UMS.margin_type).where(UMS.tg_id == tg_id))  | ||||
|             size_leverage = await session.scalar(select(UMS.size_leverage).where(UMS.tg_id == tg_id))  | ||||
|             starting_quantity = await session.scalar(select(UMS.starting_quantity).where(UMS.tg_id == tg_id))  | ||||
|             martingale_factor = await session.scalar(select(UMS.martingale_factor).where(UMS.tg_id == tg_id))  | ||||
|             maximal_quantity = await session.scalar(select(UMS.maximal_quantity).where(UMS.tg_id == tg_id))  | ||||
|  | ||||
|             data = { | ||||
|                 'trading_mode': trading_mode, | ||||
|                 'margin_type': margin_mode, | ||||
|                 'size_leverage': size_leverage, | ||||
|                 'starting_quantity': starting_quantity, | ||||
|                 'martingale_factor': martingale_factor, | ||||
|                 'maximal_quantity': maximal_quantity | ||||
|             } | ||||
|  | ||||
|             return data | ||||
|  | ||||
| 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("Получение риск-менеджмента настроек пользователя") | ||||
|  | ||||
|             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)) | ||||
|  | ||||
|             data = { | ||||
|                 'price_profit': price_profit, | ||||
|                 'price_loss': price_loss, | ||||
|                 'max_risk_deal': max_risk_deal                         | ||||
|             } | ||||
|  | ||||
|             return data | ||||
|  | ||||
| #UPDATE_SYMBOL | ||||
| async def update_symbol(tg_id, symbol) -> 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 update_api_key(tg_id, api): | ||||
|     async with async_session() as session: | ||||
|         api_key = await session.execute(update(UBA).where(UBA.tg_id == tg_id).values(api_key = api)) | ||||
|  | ||||
|         await session.commit() | ||||
|  | ||||
| async def update_secret_key(tg_id, api): | ||||
|     async with async_session() as session: | ||||
|         secret_key = await session.execute(update(UBA).where(UBA.tg_id == tg_id).values(secret_key = api)) | ||||
|  | ||||
|         await session.commit() | ||||
|  | ||||
| # UPDATE_MAIN_SETTINGS_DB | ||||
| async def update_trade_mode_user(tg_id, trading_mode) -> None: | ||||
|     async with async_session() as session: | ||||
|         mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.mode == trading_mode)) | ||||
|  | ||||
|         if mode: | ||||
|             logger.info("Изменен трейд мод") | ||||
|             await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(trading_mode = mode)) | ||||
|  | ||||
|             await session.commit() | ||||
|  | ||||
| async def update_margin_type(tg_id, margin_type) -> None: | ||||
|     async with async_session() as session: | ||||
|         type = await session.scalar(select(Margin_type.type).where(Margin_type.type == margin_type)) | ||||
|  | ||||
|         if type: | ||||
|             logger.info("Изменен тип маржи") | ||||
|             await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(margin_type = type)) | ||||
|  | ||||
|             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() | ||||
|  | ||||
| # UPDATE_RISK_MANAGEMENT_SETTINGS_DB | ||||
|  | ||||
| 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() | ||||
| @@ -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, state): | ||||
|     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,73 +0,0 @@ | ||||
| import app.telegram.Keyboards.inline_keyboards as inline_markup | ||||
|  | ||||
| import app.telegram.database.requests as rq | ||||
|  | ||||
| async def reg_new_user_default_condition_settings(id, message): | ||||
|     tg_id = id | ||||
|  | ||||
|     trigger = await rq.get_for_registration_trigger() | ||||
|  | ||||
|     await rq.set_new_user_default_condition_settings(tg_id, trigger) | ||||
|  | ||||
| async def main_settings_message(id, message, state): | ||||
|     text = """ <b>Условия запуска</b> | ||||
|  | ||||
| <b>- Триггер:</b> Ручной запуск / Сигнал TradingView / Полностью автоматический  | ||||
| <b>- Фильтр времени: </b> диапазон по дням недели и времени суток   | ||||
| <b>- Фильтр волатильности / объёма: </b> включить/отключить   | ||||
| <b>- Интеграции и внешние сигналы: </b> | ||||
| <b>- Использовать сигналы TradingView:</b> да / нет | ||||
| <b>- Использовать AI-аналитику от ChatGPT:</b> да / не | ||||
| <b>- Webhook URL для сигналов (если используется TradingView): </b> | ||||
| """ | ||||
|     await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.condition_settings_markup) | ||||
|     | ||||
| async def trigger_message(message, state): | ||||
|     text = '''Триггер | ||||
|  | ||||
|     Описание ручного запуска, сигналов, автоматического режима ''' | ||||
|  | ||||
|     await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trigger_markup) | ||||
|  | ||||
| async def filter_time_message(message, state): | ||||
|     text = '''Фильтр времени | ||||
|  | ||||
|     ??? | ||||
|     ''' | ||||
|  | ||||
|     await message.answer(text=text) | ||||
|  | ||||
| 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,37 +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>! 👋 | ||||
|  | ||||
| Добро пожаловать в чат-робот по трейдингу на Bybit — вашего надежного помощника для анализа рынка и принятия взвешенных решений.  | ||||
| Здесь вы получите: | ||||
| <b> | ||||
| 📊 Анализ текущих трендов   | ||||
| 📈 Инструменты для прогнозирования и оценки рисков   | ||||
| ⚡️ Сигналы и рекомендации по сделкам   | ||||
| 🔔 Уведомления о важных изменениях и новостях | ||||
| </b> | ||||
| """, 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): | ||||
|     await message.answer(f'Добро пожаловать {message.from_user.first_name} {message.from_user.last_name}!', reply_markup=reply_markup.base_buttons_markup) | ||||
|      | ||||
| async def settings_message(message): | ||||
|     await message.edit_text("Выберите что настроить", reply_markup=inline_markup.special_settings_markup) | ||||
| @@ -1,206 +0,0 @@ | ||||
| from aiogram import Router | ||||
|  | ||||
| import app.telegram.Keyboards.inline_keyboards as inline_markup | ||||
| import app.telegram.Keyboards.reply_keyboards as reply_markup | ||||
|  | ||||
| import app.telegram.database.requests as rq | ||||
| from aiogram.types import Message, CallbackQuery | ||||
|  | ||||
| # FSM - Механизм состояния | ||||
| from aiogram.fsm.state import State, StatesGroup | ||||
|  | ||||
| router_main_settings = Router() | ||||
|  | ||||
| class update_main_settings(StatesGroup): | ||||
|     trading_mode = State()  | ||||
|     size_leverage = State()  | ||||
|     margin_type = State()  | ||||
|     martingale_factor = State()  | ||||
|     starting_quantity = State()  | ||||
|     maximal_quantity = State()  | ||||
|  | ||||
| async def reg_new_user_default_main_settings(id, message): | ||||
|     tg_id = id | ||||
|  | ||||
|     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, state): | ||||
|      data = await rq.get_user_main_settings(id) | ||||
|  | ||||
|      await message.answer(f"""<b>Основные настройки</b> | ||||
|       | ||||
| <b>- Режим торговли:</b> {data['trading_mode']} | ||||
| <b>- Тип маржи:</b> {data['margin_type']} | ||||
| <b>- Размер кредитного плеча:</b> х{data['size_leverage']} | ||||
| <b>- Начальная ставка:</b> {data['starting_quantity']} | ||||
| <b>- Коэффициент мартингейла:</b> {data['martingale_factor']} | ||||
| <b>- Максимальное количесиво ставок в серии:</b> {data['maximal_quantity']}     | ||||
| """, parse_mode='html', reply_markup=inline_markup.main_settings_markup) | ||||
|  | ||||
| async def trading_mode_message(message, state): | ||||
|     await state.set_state(update_main_settings.trading_mode) | ||||
|  | ||||
|     await message.edit_text("""<b>Режим торговли</b> | ||||
|  | ||||
| <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 | ||||
|  | ||||
|    try: | ||||
|        match callback.data: | ||||
|            case 'trade_mode_long': | ||||
|                 await rq.update_trade_mode_user(id, 'Long') | ||||
|                 await main_settings_message(id, callback.message, state) | ||||
|  | ||||
|                 await state.clear() | ||||
|            case 'trade_mode_short': | ||||
|                 await rq.update_trade_mode_user(id, 'Short') | ||||
|                 await main_settings_message(id, callback.message, state) | ||||
|  | ||||
|                 await state.clear() | ||||
|            case 'trade_mode_switch': | ||||
|                 await rq.update_trade_mode_user(id, 'Switch') | ||||
|                 await main_settings_message(id, callback.message, state) | ||||
|  | ||||
|                 await state.clear() | ||||
|            case 'trade_mode_smart': | ||||
|                await rq.update_trade_mode_user(id, 'Smart') | ||||
|                await main_settings_message(id, callback.message, state) | ||||
|  | ||||
|                await state.clear() | ||||
|    except Exception as e: | ||||
|         print(f"error: {e}") | ||||
|  | ||||
| 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): | ||||
|     await state.update_data(size_leverage = message.text) | ||||
|  | ||||
|     data = await state.get_data() | ||||
|  | ||||
|     if data['size_leverage'].isdigit() and int(data['size_leverage']) <= 100: | ||||
|         await rq.update_size_leverange(message.from_user.id, data['size_leverage']) | ||||
|         await main_settings_message(message.from_user.id, message, state) | ||||
|  | ||||
|         await state.clear() | ||||
|     else: | ||||
|         await main_settings_message(message.from_user.id, message, state)         | ||||
|  | ||||
| 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() | ||||
|  | ||||
|     if data['martingale_factor'].isdigit() and int(data['martingale_factor']) <= 100: | ||||
|         await rq.update_martingale_factor(message.from_user.id, data['martingale_factor']) | ||||
|         await main_settings_message(message.from_user.id, message, state) | ||||
|  | ||||
|         await state.clear() | ||||
|     else: | ||||
|         await main_settings_message(message.from_user.id, message, state) | ||||
|      | ||||
| 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): | ||||
|    await callback.answer() | ||||
|  | ||||
|    id = callback.from_user.id | ||||
|    print(f"sdljfngdjklfg ## {callback.data}") | ||||
|  | ||||
|    try: | ||||
|        match callback.data: | ||||
|            case 'margin_type_isolated': | ||||
|                 await rq.update_margin_type(id, 'Isolated') | ||||
|                 await main_settings_message(id, callback.message, state) | ||||
|  | ||||
|                 await state.clear() | ||||
|            case 'margin_type_cross': | ||||
|                 await rq.update_margin_type(id, 'Cross') | ||||
|                 await main_settings_message(id, callback.message, state) | ||||
|  | ||||
|                 await state.clear() | ||||
|    except Exception as e: | ||||
|         print(f"error: {e}") | ||||
|  | ||||
| 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() | ||||
|  | ||||
|     if data['starting_quantity'].isdigit(): | ||||
|         await rq.update_starting_quantity(message.from_user.id, data['starting_quantity']) | ||||
|         await main_settings_message(message.from_user.id, message, state) | ||||
|  | ||||
|         await state.clear() | ||||
|     else: | ||||
|         await main_settings_message(message.from_user.id, message, state) | ||||
|  | ||||
| async def maximum_quantity_message(message, state): | ||||
|     await state.set_state(update_main_settings.maximal_quantity) | ||||
|  | ||||
|     await message.edit_text("Введите <b>максимальное количество ставок:</b>", parse_mode='html', reply_markup=inline_markup.back_btn_list_settings_markup) | ||||
|  | ||||
| @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() | ||||
|  | ||||
|     if data['maximal_quantity'].isdigit() and int(data['maximal_quantity']) <= 100: | ||||
|         await rq.update_maximal_quantity(message.from_user.id, data['maximal_quantity']) | ||||
|         await main_settings_message(message.from_user.id, message, state) | ||||
|  | ||||
|         await state.clear() | ||||
|     else: | ||||
|         await main_settings_message(message.from_user.id, message, state) | ||||
							
								
								
									
										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,95 +0,0 @@ | ||||
| from aiogram import Router | ||||
| import app.telegram.Keyboards.inline_keyboards as inline_markup | ||||
| import app.telegram.Keyboards.reply_keyboards as reply_markup | ||||
|  | ||||
| import app.telegram.database.requests as rq | ||||
| from aiogram.types import Message, CallbackQuery | ||||
|  | ||||
| # FSM - Механизм состояния | ||||
| from aiogram.fsm.state import State, StatesGroup | ||||
|  | ||||
| router_risk_management_settings = Router() | ||||
|  | ||||
| class update_risk_management_settings(StatesGroup): | ||||
|     price_profit = State() | ||||
|     price_loss = State() | ||||
|     max_risk_deal = State() | ||||
|  | ||||
| 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, state): | ||||
|     data = await rq.get_user_risk_management_settings(id) | ||||
|  | ||||
|     text = f"""<b>Риск менеджмент</b>, | ||||
|  | ||||
| <b>- Процент изменения цены для фиксации прибыли:</b> {data['price_profit']} | ||||
| <b>- Процент изменения цены для фиксации убытков:</b> {data['price_loss']} | ||||
| <b>- Максимальный риск на сделку (в % от баланса):</b> {data['max_risk_deal']}  | ||||
| """ | ||||
|     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=None) | ||||
|  | ||||
| @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() | ||||
|  | ||||
|     if data['price_profit'].isdigit() and int(data['price_profit']) <= 100: | ||||
|         await rq.update_price_profit(message.from_user.id, data['price_profit']) | ||||
|         await main_settings_message(message.from_user.id, message, state) | ||||
|  | ||||
|         await state.clear() | ||||
|     else: | ||||
|         await main_settings_message(message.from_user.id, message, state)  | ||||
|  | ||||
| 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=None) | ||||
|  | ||||
| @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() | ||||
|  | ||||
|     if data['price_loss'].isdigit() and int(data['price_loss']) <= 100: | ||||
|         await rq.update_price_loss(message.from_user.id, data['price_loss']) | ||||
|         await main_settings_message(message.from_user.id, message, state) | ||||
|  | ||||
|         await state.clear() | ||||
|     else: | ||||
|         await main_settings_message(message.from_user.id, message, state)  | ||||
|  | ||||
| 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=None) | ||||
|  | ||||
| @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() | ||||
|  | ||||
|     if data['max_risk_deal'].isdigit() and int(data['max_risk_deal']) <= 100: | ||||
|         await rq.update_max_risk_deal(message.from_user.id, data['max_risk_deal']) | ||||
|         await main_settings_message(message.from_user.id, message, state) | ||||
|  | ||||
|         await state.clear() | ||||
|     else: | ||||
|         await main_settings_message(message.from_user.id, message, state)  | ||||
							
								
								
									
										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,207 +0,0 @@ | ||||
| import logging | ||||
|  | ||||
| from aiogram import F, Router | ||||
| from aiogram.filters import CommandStart, Command | ||||
| from aiogram.types import Message, CallbackQuery | ||||
| from aiogram.fsm.context import FSMContext | ||||
|  | ||||
| import app.telegram.functions.functions as func # functions | ||||
| import app.telegram.functions.main_settings.settings as func_main_settings | ||||
| import app.telegram.functions.risk_management_settings.settings as func_rmanagement_settings | ||||
| import app.telegram.functions.condition_settings.settings as func_condition_settings | ||||
| import app.telegram.functions.additional_settings.settings as func_additional_settings | ||||
|  | ||||
| import app.telegram.database.requests as rq | ||||
| import app.telegram.Keyboards.inline_keyboards as inline_markup | ||||
| import app.telegram.Keyboards.reply_keyboards as reply_markup | ||||
|  | ||||
| router = Router() | ||||
|  | ||||
| @router.message(CommandStart()) | ||||
| async def start_message(message: Message): | ||||
|     await rq.set_new_user_bybit_api(message.from_user.id) | ||||
|  | ||||
|     await func.start_message(message) | ||||
|  | ||||
| @router.message(F.text == "👤 Профиль") | ||||
| async def profile_message(message: Message): | ||||
|     user = await rq.check_user(message.from_user.id) | ||||
|  | ||||
|     if user: | ||||
|         await func.profile_message(message.from_user.username, message) | ||||
|  | ||||
| @router.message(F.text == "Настройки") | ||||
| async def settings_msg(message: Message): | ||||
|     user = await rq.check_user(message.from_user.id) | ||||
|  | ||||
|     if user: | ||||
|         await func.settings_message(message) | ||||
|  | ||||
| @router.callback_query(F.data == "clb_start_chatbot_message") | ||||
| async def clb_func_reg (callback: CallbackQuery): | ||||
|     user = await rq.check_user(callback.from_user.id) | ||||
|  | ||||
|     if user: | ||||
|         await callback.message.answer(f'С возвращением, {callback.from_user.username}!', reply_markup=reply_markup.base_buttons_markup) | ||||
|          | ||||
|         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, callback.message)  | ||||
|         await func_additional_settings.reg_new_user_default_additional_settings(callback.from_user.id, callback.message) | ||||
|  | ||||
|         await callback.message.answer(f'Регистрация прошла успешно, перейдите в профиль нажав на кнопку!', reply_markup=reply_markup.base_buttons_markup) | ||||
|  | ||||
|         await func.profile_message(callback.from_user.username, callback.message) | ||||
|  | ||||
|     await callback.answer() | ||||
|      | ||||
| @router.callback_query(F.data == "callback_profile") | ||||
| async def clb_profile_message (callback: CallbackQuery): | ||||
|     user = await rq.check_user(callback.from_user.id) | ||||
|  | ||||
|     if user: | ||||
|         await func.profile_message(callback.from_user.username, callback.message) | ||||
|  | ||||
|     await callback.answer() | ||||
|      | ||||
|  # Настройки торговли | ||||
| @router.callback_query(F.data == "clb_settings_message") | ||||
| async def clb_settings_msg (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): | ||||
|     await func.settings_message(callback.message) | ||||
|  | ||||
|     await callback.answer() | ||||
|  | ||||
| @router.callback_query(F.data == "clb_change_main_settings") | ||||
| async def clb_change_main_settings_message(callback: CallbackQuery, state: FSMContext): | ||||
|     await func_main_settings.main_settings_message(callback.from_user.id, callback.message, state) | ||||
|  | ||||
|     await callback.answer()  | ||||
|  | ||||
| @router.callback_query(F.data == "clb_change_risk_management_settings") | ||||
| async def clb_change_risk_management_message(callback: CallbackQuery, state: FSMContext): | ||||
|     await func_rmanagement_settings.main_settings_message(callback.from_user.id, callback.message, state) | ||||
|  | ||||
|     await callback.answer() | ||||
|  | ||||
| @router.callback_query(F.data == "clb_change_condition_settings") | ||||
| async def clb_change_condition_message(callback: CallbackQuery, state: FSMContext): | ||||
|     await func_condition_settings.main_settings_message(callback.from_user.id, callback.message, state) | ||||
|  | ||||
|     await callback.answer() | ||||
|  | ||||
| @router.callback_query(F.data == "clb_change_additional_settings") | ||||
| async def clb_change_additional_message(callback: CallbackQuery, state: FSMContext): | ||||
|     await func_additional_settings.main_settings_message(callback.from_user.id, callback.message, state) | ||||
|  | ||||
|     await callback.answer() | ||||
|  | ||||
|  # Конкретные настройки каталогов    | ||||
| list_main_settings = ['clb_change_trading_mode',  | ||||
|                       '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): | ||||
|     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_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: | ||||
|         logging.error(f"Error callback in main_settings match-case: {e}") | ||||
|  | ||||
|  | ||||
| list_risk_management_settings = ['clb_change_price_profit',  | ||||
|                       'clb_change_price_loss',  | ||||
|                       'clb_change_max_risk_deal',  | ||||
| ] | ||||
| @router.callback_query(F.data.in_(list_risk_management_settings)) | ||||
| async def clb_risk_management_settings_msg(callback: CallbackQuery, state: FSMContext): | ||||
|     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) | ||||
|     except Exception as e: | ||||
|         logging.error(f"Error callback in risk_management match-case: {e}") | ||||
|    | ||||
|          | ||||
| list_condition_settings = ['clb_change_trigger', | ||||
|                            'clb_change_filter_time', | ||||
|                            '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): | ||||
|     await callback.answer() | ||||
|      | ||||
|     try: | ||||
|         match callback.data: | ||||
|             case 'clb_change_trigger': | ||||
|                 await func_condition_settings.trigger_message(callback.message, state) | ||||
|             case 'clb_change_filter_time': | ||||
|                 await func_condition_settings.filter_time_message(callback.message, state) | ||||
|             case 'clb_change_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: | ||||
|         logging.error(f"Error callback in main_settings match-case: {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): | ||||
|     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: | ||||
|         logging.error(f"Error callback in additional_settings match-case: {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) | ||||
							
								
								
									
										946
									
								
								app/telegram/handlers/main_settings/additional_settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										946
									
								
								app/telegram/handlers/main_settings/additional_settings.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,946 @@ | ||||
| import logging.config | ||||
|  | ||||
| from aiogram import F, Router | ||||
| from aiogram.fsm.context import FSMContext | ||||
| from aiogram.types import CallbackQuery, Message | ||||
|  | ||||
| import app.telegram.keyboards.inline as kbi | ||||
| import database.request as rq | ||||
| from app.bybit.get_functions.get_instruments_info import get_instruments_info | ||||
| from app.bybit.get_functions.get_positions import get_active_positions_by_symbol, get_active_orders_by_symbol | ||||
| 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 is_int, is_number, safe_float | ||||
| from app.telegram.states.states import AdditionalSettingsState | ||||
| from logger_helper.logger_helper import LOGGING_CONFIG | ||||
|  | ||||
| logging.config.dictConfig(LOGGING_CONFIG) | ||||
| logger = logging.getLogger("additional_settings") | ||||
|  | ||||
| router_additional_settings = Router(name="additional_settings") | ||||
|  | ||||
|  | ||||
| @router_additional_settings.callback_query(F.data == "trade_mode") | ||||
| async def settings_for_trade_mode( | ||||
|         callback_query: CallbackQuery, state: FSMContext | ||||
| ) -> None: | ||||
|     """ | ||||
|     Handles the 'trade_mode' callback query. | ||||
|  | ||||
|     Clears the current FSM state, edits the message text to display trade mode options | ||||
|     with explanation for 'Long' and 'Short' modes, and shows an inline keyboard for selection. | ||||
|  | ||||
|     Args: | ||||
|         callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. | ||||
|         state (FSMContext): Finite State Machine context for the current user session. | ||||
|  | ||||
|     Logs: | ||||
|         Success or error messages with user identification. | ||||
|     """ | ||||
|     try: | ||||
|         await state.clear() | ||||
|         await callback_query.message.edit_text( | ||||
|             text="Выберите режим торговли:\n\n" | ||||
|                  "Лонг - все сделки серии открываются на покупку.\n" | ||||
|                  "Шорт - все сделки серии открываются на продажу.\n" | ||||
|                  "Свитч - направление первой сделки серии меняется по переменно.\n", | ||||
|             reply_markup=kbi.trade_mode, | ||||
|         ) | ||||
|         logger.debug( | ||||
|             "Command trade_mode processed successfully for user: %s", | ||||
|             callback_query.from_user.id, | ||||
|         ) | ||||
|  | ||||
|     except Exception as e: | ||||
|         await callback_query.answer( | ||||
|             text="Произошла ошибка. Пожалуйста, попробуйте позже." | ||||
|         ) | ||||
|         logger.error( | ||||
|             "Error processing command trade_mode for user %s: %s", | ||||
|             callback_query.from_user.id, | ||||
|             e, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @router_additional_settings.callback_query( | ||||
|     lambda c: c.data == "Long" or c.data == "Short" or c.data == "Switch" | ||||
| ) | ||||
| async def trade_mode(callback_query: CallbackQuery, state: FSMContext) -> None: | ||||
|     """ | ||||
|     Handles callback queries related to trade mode selection. | ||||
|  | ||||
|     Updates FSM context with selected trade mode and persists the choice in database. | ||||
|     Sends an acknowledgement to user and clears FSM state afterward. | ||||
|  | ||||
|     Args: | ||||
|         callback_query (CallbackQuery): Incoming callback query indicating selected trade mode. | ||||
|         state (FSMContext): Finite State Machine context for the current user session. | ||||
|  | ||||
|     Logs: | ||||
|         Success or error messages with user identification. | ||||
|     """ | ||||
|     try: | ||||
|         req = await rq.set_trade_mode( | ||||
|             tg_id=callback_query.from_user.id, trade_mode=callback_query.data | ||||
|         ) | ||||
|  | ||||
|         if not req: | ||||
|             await callback_query.answer( | ||||
|                 text="Произошла ошибка при установке режима торговли" | ||||
|             ) | ||||
|             return | ||||
|  | ||||
|         await callback_query.answer(text="Режим торговли успешно изменен") | ||||
|         logger.debug( | ||||
|             "Trade mode changed successfully for user: %s", callback_query.from_user.id | ||||
|         ) | ||||
|     except Exception as e: | ||||
|         await callback_query.answer(text="Произошла ошибка при смене режима позиции.") | ||||
|         logger.error( | ||||
|             "Error processing set trade_mode for user %s: %s", | ||||
|             callback_query.from_user.id, | ||||
|             e, | ||||
|         ) | ||||
|     finally: | ||||
|         await state.clear() | ||||
|  | ||||
|  | ||||
| @router_additional_settings.callback_query(F.data == "switch_side_start") | ||||
| async def switch_side_start(callback_query: CallbackQuery, state: FSMContext) -> None: | ||||
|     """ | ||||
|     Handles the 'switch_side_start' callback query. | ||||
|  | ||||
|     Clears the current FSM state, edits the message text to display the switch side start message, | ||||
|     and shows an inline keyboard for selection. | ||||
|  | ||||
|     Args: | ||||
|         callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. | ||||
|         state (FSMContext): Finite State Machine context for the current user session. | ||||
|  | ||||
|     Logs: | ||||
|         Success or error messages with user identification. | ||||
|     """ | ||||
|     try: | ||||
|         await state.clear() | ||||
|         await callback_query.message.edit_text( | ||||
|             text="Выберите направление первой сделки серии:\n\n" | ||||
|                  "По направлению - сделка открывается в направлении последней сделки предыдущей серии.\n" | ||||
|                  "Противоположно - сделка открывается в противоположном направлении последней сделки предыдущей серии.\n", | ||||
|             reply_markup=kbi.switch_side, | ||||
|         ) | ||||
|         logger.debug( | ||||
|             "Command switch_side_start processed successfully for user: %s", | ||||
|             callback_query.from_user.id, | ||||
|         ) | ||||
|     except Exception as e: | ||||
|         await callback_query.answer( | ||||
|             text="Произошла ошибка. Пожалуйста, попробуйте позже." | ||||
|         ) | ||||
|         logger.error( | ||||
|             "Error processing command switch_side_start for user %s: %s", | ||||
|             callback_query.from_user.id, | ||||
|             e, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @router_additional_settings.callback_query(lambda c: c.data == "switch_direction" or c.data == "switch_opposite") | ||||
| async def switch_side_handler(callback_query: CallbackQuery, state: FSMContext) -> None: | ||||
|     """ | ||||
|     Handles callback queries related to switch side selection. | ||||
|  | ||||
|     Updates FSM context with selected switch side and persists the choice in database. | ||||
|     Sends an acknowledgement to user and clears FSM state afterward. | ||||
|  | ||||
|     Args: | ||||
|         callback_query (CallbackQuery): Incoming callback query indicating selected switch side. | ||||
|         state (FSMContext): Finite State Machine context for the current user session. | ||||
|  | ||||
|     Logs: | ||||
|         Success or error messages with user identification. | ||||
|     """ | ||||
|     try: | ||||
|         if callback_query.data == "switch_direction": | ||||
|             switch_side = "По направлению" | ||||
|         elif callback_query.data == "switch_opposite": | ||||
|             switch_side = "Противоположно" | ||||
|         else: | ||||
|             switch_side = None | ||||
|  | ||||
|         req = await rq.set_switch_side( | ||||
|             tg_id=callback_query.from_user.id, switch_side=switch_side | ||||
|         ) | ||||
|  | ||||
|         if not req: | ||||
|             await callback_query.answer( | ||||
|                 text="Произошла ошибка при установке направления переключения" | ||||
|             ) | ||||
|             return | ||||
|  | ||||
|         await callback_query.answer(text=f"Выбрано: {switch_side}") | ||||
|         logger.debug( | ||||
|             "Switch side changed successfully for user: %s", callback_query.from_user.id | ||||
|         ) | ||||
|     except Exception as e: | ||||
|         await callback_query.answer( | ||||
|             text="Произошла ошибка при смене направления переключения" | ||||
|         ) | ||||
|         logger.error( | ||||
|             "Error processing set switch_side for user %s: %s", | ||||
|             callback_query.from_user.id, | ||||
|             e, | ||||
|         ) | ||||
|     finally: | ||||
|         await state.clear() | ||||
|  | ||||
|  | ||||
| @router_additional_settings.callback_query(F.data == "margin_type") | ||||
| async def settings_for_margin_type( | ||||
|         callback_query: CallbackQuery, state: FSMContext | ||||
| ) -> None: | ||||
|     """ | ||||
|     Handles the 'margin_type' callback query. | ||||
|  | ||||
|     Clears the current FSM state, edits the message text to display margin type options, | ||||
|     and shows an inline keyboard for selection. | ||||
|  | ||||
|     Args: | ||||
|         callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. | ||||
|         state (FSMContext): Finite State Machine context for the current user session. | ||||
|  | ||||
|     Logs: | ||||
|         Success or error messages with user identification. | ||||
|     """ | ||||
|     try: | ||||
|         await state.clear() | ||||
|         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 | ||||
|         await callback_query.message.edit_text( | ||||
|             text="Выберите тип маржи:\n\n" | ||||
|                  "Примечание: Если у вас есть открытые позиции, то маржа примениться ко всем позициям", | ||||
|             reply_markup=kbi.margin_type | ||||
|         ) | ||||
|         logger.debug( | ||||
|             "Command margin_type processed successfully for user: %s", | ||||
|             callback_query.from_user.id, | ||||
|         ) | ||||
|     except Exception as e: | ||||
|         await callback_query.answer( | ||||
|             text="Произошла ошибка. Пожалуйста, попробуйте позже." | ||||
|         ) | ||||
|         logger.error( | ||||
|             "Error processing command margin_type for user %s: %s", | ||||
|             callback_query.from_user.id, | ||||
|             e, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @router_additional_settings.callback_query( | ||||
|     lambda c: c.data == "ISOLATED_MARGIN" or c.data == "REGULAR_MARGIN" | ||||
| ) | ||||
| async def set_margin_type(callback_query: CallbackQuery, state: FSMContext) -> None: | ||||
|     """ | ||||
|     Handles callback queries starting with 'Isolated' or 'Cross'. | ||||
|  | ||||
|     Updates FSM context with selected margin type and persists the choice in database. | ||||
|     Sends an acknowledgement to user and clears FSM state afterward. | ||||
|  | ||||
|     Args: | ||||
|         callback_query (CallbackQuery): Incoming callback query indicating selected margin type. | ||||
|         state (FSMContext): Finite State Machine context for the current user session. | ||||
|  | ||||
|     Logs: | ||||
|         Success or error messages with user identification. | ||||
|     """ | ||||
|     try: | ||||
|         symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id) | ||||
|         additional_settings = await rq.get_user_additional_settings( | ||||
|             tg_id=callback_query.from_user.id | ||||
|         ) | ||||
|         get_leverage = additional_settings.leverage or "10" | ||||
|  | ||||
|         leverage_to_float = safe_float(get_leverage) | ||||
|         bybit_margin_mode = callback_query.data | ||||
|         response = await set_margin_mode( | ||||
|             tg_id=callback_query.from_user.id, margin_mode=bybit_margin_mode | ||||
|         ) | ||||
|  | ||||
|         if not response: | ||||
|             await callback_query.answer( | ||||
|                 text="Произошла ошибка при установке типа маржи" | ||||
|             ) | ||||
|             return | ||||
|  | ||||
|         req = await rq.set_margin_type( | ||||
|             tg_id=callback_query.from_user.id, margin_type=callback_query.data | ||||
|         ) | ||||
|  | ||||
|         if not req: | ||||
|             await callback_query.answer( | ||||
|                 text="Произошла ошибка при установке типа маржи" | ||||
|             ) | ||||
|             return | ||||
|  | ||||
|         await set_leverage( | ||||
|             tg_id=callback_query.from_user.id, | ||||
|             symbol=symbol, | ||||
|             leverage=str(leverage_to_float), | ||||
|         ) | ||||
|  | ||||
|         if callback_query.data.startswith("ISOLATED_MARGIN"): | ||||
|             await callback_query.answer(text="Выбран тип маржи: Изолированная") | ||||
|         elif callback_query.data.startswith("REGULAR_MARGIN"): | ||||
|             await callback_query.answer(text="Выбран тип маржи: Кросс") | ||||
|         else: | ||||
|             await callback_query.answer( | ||||
|                 text="Произошла ошибка при установке типа маржи" | ||||
|             ) | ||||
|  | ||||
|     except Exception as e: | ||||
|         await callback_query.answer(text="Произошла ошибка при установке типа маржи") | ||||
|         logger.error( | ||||
|             "Error processing command margin_type for user %s: %s", | ||||
|             callback_query.from_user.id, | ||||
|             e, | ||||
|         ) | ||||
|     finally: | ||||
|         await state.clear() | ||||
|  | ||||
|  | ||||
| @router_additional_settings.callback_query(lambda c: c.data == "trigger_price") | ||||
| async def trigger_price(callback_query: CallbackQuery, state: FSMContext) -> None: | ||||
|     """ | ||||
|     Handles the 'trigger_price' callback query. | ||||
|  | ||||
|     Clears the current FSM state, edits the message text to prompt for the trigger price, | ||||
|     and shows an inline keyboard for input. | ||||
|  | ||||
|     Args: | ||||
|         callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. | ||||
|         state (FSMContext): Finite State Machine context for the current user session. | ||||
|  | ||||
|     Logs: | ||||
|         Success or error messages with user identification. | ||||
|     """ | ||||
|     try: | ||||
|         await state.clear() | ||||
|         await state.set_state(AdditionalSettingsState.trigger_price_state) | ||||
|         await callback_query.answer() | ||||
|         await state.update_data(prompt_message_id=callback_query.message.message_id) | ||||
|         msg = await callback_query.message.edit_text( | ||||
|             text="Введите цену:", reply_markup=kbi.back_to_additional_settings | ||||
|         ) | ||||
|         await state.update_data(prompt_message_id=msg.message_id) | ||||
|         logger.debug( | ||||
|             "Command trigger_price processed successfully for user: %s", | ||||
|             callback_query.from_user.id, | ||||
|         ) | ||||
|     except Exception as e: | ||||
|         await callback_query.answer( | ||||
|             text="Произошла ошибка. Пожалуйста, попробуйте позже." | ||||
|         ) | ||||
|         logger.error( | ||||
|             "Error processing command trigger_price for user %s: %s", | ||||
|             callback_query.from_user.id, | ||||
|             e, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @router_additional_settings.message(AdditionalSettingsState.trigger_price_state) | ||||
| async def set_trigger_price(message: Message, state: FSMContext) -> None: | ||||
|     """ | ||||
|     Handles user input for setting the trigger price. | ||||
|  | ||||
|     Updates FSM context with the selected trigger price and persists the choice in database. | ||||
|     Sends an acknowledgement to user and clears FSM state afterward. | ||||
|  | ||||
|     Args: | ||||
|         message (Message): Incoming message from user containing the selected trigger price. | ||||
|         state (FSMContext): Finite State Machine context for the current user session. | ||||
|  | ||||
|     Logs: | ||||
|         Success or error messages with user identification. | ||||
|     """ | ||||
|     try: | ||||
|         try: | ||||
|             data = await state.get_data() | ||||
|             if "prompt_message_id" in data: | ||||
|                 prompt_message_id = data["prompt_message_id"] | ||||
|                 await message.bot.delete_message( | ||||
|                     chat_id=message.chat.id, message_id=prompt_message_id | ||||
|                 ) | ||||
|             await message.delete() | ||||
|         except Exception as e: | ||||
|             if "message to delete not found" in str(e).lower(): | ||||
|                 pass  # Ignore this error | ||||
|             else: | ||||
|                 raise e | ||||
|  | ||||
|         trigger_price_value = message.text | ||||
|  | ||||
|         if not is_number(trigger_price_value): | ||||
|             await message.answer( | ||||
|                 "Ошибка: введите валидное число.", | ||||
|                 reply_markup=kbi.back_to_additional_settings, | ||||
|             ) | ||||
|             logger.debug( | ||||
|                 "User %s input invalid (not an valid number): %s", | ||||
|                 message.from_user.id, | ||||
|                 trigger_price_value, | ||||
|             ) | ||||
|             return | ||||
|  | ||||
|         req = await rq.set_trigger_price( | ||||
|             tg_id=message.from_user.id, trigger_price=safe_float(trigger_price_value) | ||||
|         ) | ||||
|         if req: | ||||
|             await message.answer( | ||||
|                 text=f"Цена триггера установлена на: {trigger_price_value}", | ||||
|                 reply_markup=kbi.back_to_additional_settings, | ||||
|             ) | ||||
|         else: | ||||
|             await message.answer( | ||||
|                 text="Произошла ошибка при установке цены триггера.", | ||||
|                 reply_markup=kbi.back_to_additional_settings, | ||||
|             ) | ||||
|  | ||||
|         await state.clear() | ||||
|     except Exception as e: | ||||
|         await message.answer( | ||||
|             text="Произошла ошибка при установке цены триггера.", | ||||
|             reply_markup=kbi.back_to_additional_settings, | ||||
|         ) | ||||
|         logger.error( | ||||
|             "Error processing set_trigger_price for user %s: %s", | ||||
|             message.from_user.id, | ||||
|             e, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @router_additional_settings.callback_query(F.data == "leverage") | ||||
| async def leverage_handler(callback_query: CallbackQuery, state: FSMContext) -> None: | ||||
|     """ | ||||
|     Handles the 'leverage' callback query. | ||||
|  | ||||
|     Clears the current FSM state, edits the message text to display the leverage options, | ||||
|     and shows an inline keyboard for selection. | ||||
|  | ||||
|     Args: | ||||
|         callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. | ||||
|         state (FSMContext): Finite State Machine context for the current user session. | ||||
|  | ||||
|     Logs: | ||||
|         Success or error messages with user identification. | ||||
|     """ | ||||
|     try: | ||||
|         await state.clear() | ||||
|         await callback_query.answer() | ||||
|         await state.set_state(AdditionalSettingsState.leverage_state) | ||||
|         msg = await callback_query.message.edit_text( | ||||
|             text="Введите размер кредитного плеча:", | ||||
|             reply_markup=kbi.back_to_additional_settings, | ||||
|         ) | ||||
|         await state.update_data(prompt_message_id=msg.message_id) | ||||
|         logger.debug( | ||||
|             "Command leverage processed successfully for user: %s", | ||||
|             callback_query.from_user.id, | ||||
|         ) | ||||
|     except Exception as e: | ||||
|         await callback_query.answer( | ||||
|             text="Произошла ошибка. Пожалуйста, попробуйте позже." | ||||
|         ) | ||||
|         logger.error( | ||||
|             "Error processing command leverage for user %s: %s", | ||||
|             callback_query.from_user.id, | ||||
|             e, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @router_additional_settings.message(AdditionalSettingsState.leverage_state) | ||||
| async def set_leverage_handler(message: Message, state: FSMContext) -> None: | ||||
|     """ | ||||
|     Handles user input for setting the leverage. | ||||
|  | ||||
|     Updates FSM context with the selected leverage and persists the choice in database. | ||||
|     Sends an acknowledgement to user and clears FSM state afterward. | ||||
|  | ||||
|     Args: | ||||
|         message (Message): Incoming message from user containing the selected leverage. | ||||
|         state (FSMContext): Finite State Machine context for the current user session. | ||||
|  | ||||
|     Logs: | ||||
|         Success or error messages with user identification. | ||||
|     """ | ||||
|     try: | ||||
|         try: | ||||
|             data = await state.get_data() | ||||
|             if "prompt_message_id" in data: | ||||
|                 prompt_message_id = data["prompt_message_id"] | ||||
|                 await message.bot.delete_message( | ||||
|                     chat_id=message.chat.id, message_id=prompt_message_id | ||||
|                 ) | ||||
|             await message.delete() | ||||
|         except Exception as e: | ||||
|             if "message to delete not found" in str(e).lower(): | ||||
|                 pass  # Ignore this error | ||||
|             else: | ||||
|                 raise e | ||||
|  | ||||
|         get_leverage = message.text | ||||
|         tg_id = message.from_user.id | ||||
|         if not is_number(get_leverage): | ||||
|             await message.answer( | ||||
|                 "Ошибка: введите валидное число.", | ||||
|                 reply_markup=kbi.back_to_additional_settings, | ||||
|             ) | ||||
|             logger.debug( | ||||
|                 "User %s input invalid (not an valid number): %s", | ||||
|                 message.from_user.id, | ||||
|                 get_leverage, | ||||
|             ) | ||||
|             return | ||||
|  | ||||
|         leverage_float = safe_float(get_leverage) | ||||
|  | ||||
|         symbol = await rq.get_user_symbol(tg_id=tg_id) | ||||
|         instruments_info = await get_instruments_info(tg_id=tg_id, symbol=symbol) | ||||
|  | ||||
|         if instruments_info is not None: | ||||
|             min_leverage = ( | ||||
|                     safe_float(instruments_info.get("leverageFilter").get("minLeverage")) | ||||
|                     or 1 | ||||
|             ) | ||||
|             max_leverage = ( | ||||
|                     safe_float(instruments_info.get("leverageFilter").get("maxLeverage")) | ||||
|                     or 100 | ||||
|             ) | ||||
|  | ||||
|             if leverage_float > max_leverage or leverage_float < min_leverage: | ||||
|                 await message.answer( | ||||
|                     text=f"Кредитное плечо должно быть от {min_leverage} до {max_leverage}", | ||||
|                     reply_markup=kbi.back_to_additional_settings, | ||||
|                 ) | ||||
|                 logger.info( | ||||
|                     "User %s input invalid (out of range): %s, %s, %s: %s", | ||||
|                     message.from_user.id, | ||||
|                     symbol, | ||||
|                     min_leverage, | ||||
|                     max_leverage, | ||||
|                     leverage_float, | ||||
|                 ) | ||||
|                 return | ||||
|         else: | ||||
|             await message.answer( | ||||
|                 text="Произошла ошибка. Пожалуйста, попробуйте позже.", | ||||
|                 reply_markup=kbi.back_to_additional_settings, | ||||
|             ) | ||||
|  | ||||
|         response = await set_leverage( | ||||
|             tg_id=message.from_user.id, symbol=symbol, leverage=str(leverage_float) | ||||
|         ) | ||||
|  | ||||
|         if not response: | ||||
|             await message.answer( | ||||
|                 text="Невозможно установить кредитное плечо для текущего режима торговли.", | ||||
|                 reply_markup=kbi.back_to_additional_settings, | ||||
|             ) | ||||
|             return | ||||
|  | ||||
|         req_leverage = await rq.set_leverage( | ||||
|             tg_id=message.from_user.id, leverage=str(leverage_float) | ||||
|         ) | ||||
|  | ||||
|         if req_leverage: | ||||
|             await message.answer( | ||||
|                 text=f"Кредитное плечо успешно установлено на {leverage_float}", | ||||
|                 reply_markup=kbi.back_to_additional_settings, | ||||
|             ) | ||||
|             risk_percent = 100 / safe_float(leverage_float) | ||||
|             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) | ||||
|             logger.info( | ||||
|                 "User %s set leverage: %s", message.from_user.id, leverage_float | ||||
|             ) | ||||
|         else: | ||||
|             await message.answer( | ||||
|                 text="Произошла ошибка при установке кредитного плеча. Пожалуйста, попробуйте позже.", | ||||
|                 reply_markup=kbi.back_to_additional_settings, | ||||
|             ) | ||||
|  | ||||
|         await state.clear() | ||||
|     except Exception as e: | ||||
|         await message.answer( | ||||
|             text="Произошла ошибка при установке кредитного плеча. Пожалуйста, попробуйте позже.", | ||||
|             reply_markup=kbi.back_to_additional_settings, | ||||
|         ) | ||||
|         logger.error( | ||||
|             "Error processing command leverage for user %s: %s", message.from_user.id, e | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @router_additional_settings.callback_query(F.data == "order_quantity") | ||||
| async def order_quantity(callback_query: CallbackQuery, state: FSMContext) -> None: | ||||
|     """ | ||||
|     Handles the 'order_quantity' callback query. | ||||
|  | ||||
|     Clears the current FSM state, edits the message text to display the order quantity options, | ||||
|     and shows an inline keyboard for selection. | ||||
|  | ||||
|     Args: | ||||
|         callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. | ||||
|         state (FSMContext): Finite State Machine context for the current user session. | ||||
|  | ||||
|     Logs: | ||||
|         Success or error messages with user identification. | ||||
|     """ | ||||
|     try: | ||||
|         await state.clear() | ||||
|         await state.set_state(AdditionalSettingsState.quantity_state) | ||||
|         msg = await callback_query.message.edit_text( | ||||
|             text=f"Введите базовую ставку в USDT:", | ||||
|             reply_markup=kbi.back_to_additional_settings, | ||||
|         ) | ||||
|         await state.update_data(prompt_message_id=msg.message_id) | ||||
|         logger.debug( | ||||
|             "Command order_quantity processed successfully for user: %s", | ||||
|             callback_query.from_user.id, | ||||
|         ) | ||||
|     except Exception as e: | ||||
|         await callback_query.answer( | ||||
|             text="Произошла ошибка. Пожалуйста, попробуйте позже." | ||||
|         ) | ||||
|         logger.error( | ||||
|             "Error processing command order_quantity for user %s: %s", | ||||
|             callback_query.from_user.id, | ||||
|             e, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @router_additional_settings.message(AdditionalSettingsState.quantity_state) | ||||
| async def set_order_quantity(message: Message, state: FSMContext) -> None: | ||||
|     """ | ||||
|     Handles user input for setting the order quantity. | ||||
|  | ||||
|     Updates FSM context with the selected order quantity and persists the choice in database. | ||||
|     Sends an acknowledgement to user and clears FSM state afterward. | ||||
|  | ||||
|     Args: | ||||
|         message (Message): Incoming message from user containing the selected order quantity. | ||||
|         state (FSMContext): Finite State Machine context for the current user session. | ||||
|  | ||||
|     Logs: | ||||
|         Success or error messages with user identification. | ||||
|     """ | ||||
|     try: | ||||
|         try: | ||||
|             data = await state.get_data() | ||||
|             if "prompt_message_id" in data: | ||||
|                 prompt_message_id = data["prompt_message_id"] | ||||
|                 await message.bot.delete_message( | ||||
|                     chat_id=message.chat.id, message_id=prompt_message_id | ||||
|                 ) | ||||
|             await message.delete() | ||||
|         except Exception as e: | ||||
|             if "message to delete not found" in str(e).lower(): | ||||
|                 pass  # Ignore this error | ||||
|             else: | ||||
|                 raise e | ||||
|  | ||||
|         order_quantity_value = message.text | ||||
|  | ||||
|         if not is_number(order_quantity_value): | ||||
|             await message.answer( | ||||
|                 "Ошибка: введите валидное число.", | ||||
|                 reply_markup=kbi.back_to_additional_settings, | ||||
|             ) | ||||
|             logger.debug( | ||||
|                 "User %s input invalid (not an valid number): %s", | ||||
|                 message.from_user.id, | ||||
|                 order_quantity_value, | ||||
|             ) | ||||
|             return | ||||
|  | ||||
|         quantity = safe_float(order_quantity_value) | ||||
|  | ||||
|         req = await rq.set_order_quantity( | ||||
|             tg_id=message.from_user.id, order_quantity=quantity | ||||
|         ) | ||||
|  | ||||
|         if req: | ||||
|             await message.answer( | ||||
|                 text=f"Базовая ставка установлена на {message.text} USDT", | ||||
|                 reply_markup=kbi.back_to_additional_settings, | ||||
|             ) | ||||
|         else: | ||||
|             await message.answer( | ||||
|                 text="Произошла ошибка при установке кол-ва ордера. Пожалуйста, попробуйте позже.", | ||||
|                 reply_markup=kbi.back_to_additional_settings, | ||||
|             ) | ||||
|  | ||||
|         await state.clear() | ||||
|     except Exception as e: | ||||
|         await message.answer( | ||||
|             text="Произошла ошибка при установке базовой ставки. Пожалуйста, попробуйте позже.", | ||||
|             reply_markup=kbi.back_to_additional_settings, | ||||
|         ) | ||||
|         logger.error("Error processing command set_order_quantity: %s", e) | ||||
|  | ||||
|  | ||||
| @router_additional_settings.callback_query(F.data == "martingale_factor") | ||||
| async def martingale_factor(callback_query: CallbackQuery, state: FSMContext) -> None: | ||||
|     """ | ||||
|     Handles the 'martingale_factor' callback query. | ||||
|  | ||||
|     Clears the current FSM state, edits the message text to display the martingale factor options, | ||||
|     and shows an inline keyboard for selection. | ||||
|  | ||||
|     Args: | ||||
|         callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. | ||||
|         state (FSMContext): Finite State Machine context for the current user session. | ||||
|  | ||||
|     Logs: | ||||
|         Success or error messages with user identification. | ||||
|     """ | ||||
|     try: | ||||
|         await state.clear() | ||||
|         await state.set_state(AdditionalSettingsState.martingale_factor_state) | ||||
|         msg = await callback_query.message.edit_text( | ||||
|             text="Введите коэффициент мартингейла:", | ||||
|             reply_markup=kbi.back_to_additional_settings, | ||||
|         ) | ||||
|         await state.update_data(prompt_message_id=msg.message_id) | ||||
|         logger.debug( | ||||
|             "Command martingale_factor processed successfully for user: %s", | ||||
|             callback_query.from_user.id, | ||||
|         ) | ||||
|     except Exception as e: | ||||
|         await callback_query.answer( | ||||
|             text="Произошла ошибка. Пожалуйста, попробуйте позже." | ||||
|         ) | ||||
|         logger.error( | ||||
|             "Error processing command martingale_factor for user %s: %s", | ||||
|             callback_query.from_user.id, | ||||
|             e, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @router_additional_settings.message(AdditionalSettingsState.martingale_factor_state) | ||||
| async def set_martingale_factor(message: Message, state: FSMContext) -> None: | ||||
|     """ | ||||
|     Handles user input for setting the martingale factor. | ||||
|  | ||||
|     Updates FSM context with the selected martingale factor and persists the choice in database. | ||||
|     Sends an acknowledgement to user and clears FSM state afterward. | ||||
|  | ||||
|     Args: | ||||
|         message (Message): Incoming message from user containing the selected martingale factor. | ||||
|         state (FSMContext): Finite State Machine context for the current user session. | ||||
|  | ||||
|     Logs: | ||||
|         Success or error messages with user identification. | ||||
|     """ | ||||
|     try: | ||||
|         try: | ||||
|             data = await state.get_data() | ||||
|             if "prompt_message_id" in data: | ||||
|                 prompt_message_id = data["prompt_message_id"] | ||||
|                 await message.bot.delete_message( | ||||
|                     chat_id=message.chat.id, message_id=prompt_message_id | ||||
|                 ) | ||||
|             await message.delete() | ||||
|         except Exception as e: | ||||
|             if "message to delete not found" in str(e).lower(): | ||||
|                 pass  # Ignore this error | ||||
|             else: | ||||
|                 raise e | ||||
|  | ||||
|         martingale_factor_value = message.text | ||||
|  | ||||
|         if not is_number(martingale_factor_value): | ||||
|             await message.answer( | ||||
|                 "Ошибка: введите валидное число.", | ||||
|                 reply_markup=kbi.back_to_additional_settings, | ||||
|             ) | ||||
|             logger.debug( | ||||
|                 "User %s input invalid (not an valid number): %s", | ||||
|                 message.from_user.id, | ||||
|                 martingale_factor_value, | ||||
|             ) | ||||
|             return | ||||
|  | ||||
|         martingale_factor_value_float = safe_float(martingale_factor_value) | ||||
|  | ||||
|         if martingale_factor_value_float < 0.1 or martingale_factor_value_float > 10: | ||||
|             await message.answer(text="Ошибка: коэффициент мартингейла должен быть в диапазоне от 0.1 до 10") | ||||
|             logger.debug("User %s input invalid (not in range 0.1 to 10): %s", message.from_user.id, | ||||
|                          martingale_factor_value_float) | ||||
|             return | ||||
|  | ||||
|         req = await rq.set_martingale_factor( | ||||
|             tg_id=message.from_user.id, martingale_factor=martingale_factor_value_float | ||||
|         ) | ||||
|  | ||||
|         if req: | ||||
|             await message.answer( | ||||
|                 text=f"Коэффициент мартингейла установлен на {message.text}", | ||||
|                 reply_markup=kbi.back_to_additional_settings, | ||||
|             ) | ||||
|         else: | ||||
|             await message.answer( | ||||
|                 text="Произошла ошибка при установке коэффициента мартингейла. Пожалуйста, попробуйте позже.", | ||||
|                 reply_markup=kbi.back_to_additional_settings, | ||||
|             ) | ||||
|  | ||||
|         await state.clear() | ||||
|     except Exception as e: | ||||
|         await message.answer( | ||||
|             text="Произошла ошибка при установке коэффициента мартингейла. Пожалуйста, попробуйте позже.", | ||||
|             reply_markup=kbi.back_to_additional_settings, | ||||
|         ) | ||||
|         logger.error("Error processing command set_martingale_factor: %s", e) | ||||
|  | ||||
|  | ||||
| @router_additional_settings.callback_query(F.data == "max_bets_in_series") | ||||
| async def max_bets_in_series(callback_query: CallbackQuery, state: FSMContext) -> None: | ||||
|     """ | ||||
|     Handles the 'max_bets_in_series' callback query. | ||||
|  | ||||
|     Clears the current FSM state, edits the message text to display the max bets in series options, | ||||
|     and shows an inline keyboard for selection. | ||||
|  | ||||
|     Args: | ||||
|         callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. | ||||
|         state (FSMContext): Finite State Machine context for the current user session. | ||||
|  | ||||
|     Logs: | ||||
|         Success or error messages with user identification. | ||||
|     """ | ||||
|     try: | ||||
|         await state.clear() | ||||
|         await state.set_state(AdditionalSettingsState.max_bets_in_series_state) | ||||
|         msg = await callback_query.message.edit_text( | ||||
|             text="Введите максимальное количество ставок в серии:", | ||||
|             reply_markup=kbi.back_to_additional_settings, | ||||
|         ) | ||||
|         await state.update_data(prompt_message_id=msg.message_id) | ||||
|         logger.debug( | ||||
|             "Command max_bets_in_series processed successfully for user: %s", | ||||
|             callback_query.from_user.id, | ||||
|         ) | ||||
|     except Exception as e: | ||||
|         await callback_query.answer( | ||||
|             text="Произошла ошибка. Пожалуйста, попробуйте позже." | ||||
|         ) | ||||
|         logger.error( | ||||
|             "Error processing command max_bets_in_series for user %s: %s", | ||||
|             callback_query.from_user.id, | ||||
|             e, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @router_additional_settings.message(AdditionalSettingsState.max_bets_in_series_state) | ||||
| async def set_max_bets_in_series(message: Message, state: FSMContext) -> None: | ||||
|     """ | ||||
|     Handles user input for setting the max bets in series. | ||||
|  | ||||
|     Updates FSM context with the selected max steps and persists the choice in database. | ||||
|     Sends an acknowledgement to user and clears FSM state afterward. | ||||
|  | ||||
|     Args: | ||||
|         message (Message): Incoming message from user containing the selected max bets in series. | ||||
|         state (FSMContext): Finite State Machine context for the current user session. | ||||
|  | ||||
|     Logs: | ||||
|         Success or error messages with user identification. | ||||
|     """ | ||||
|     try: | ||||
|         try: | ||||
|             data = await state.get_data() | ||||
|             if "prompt_message_id" in data: | ||||
|                 prompt_message_id = data["prompt_message_id"] | ||||
|                 await message.bot.delete_message( | ||||
|                     chat_id=message.chat.id, message_id=prompt_message_id | ||||
|                 ) | ||||
|             await message.delete() | ||||
|         except Exception as e: | ||||
|             if "message to delete not found" in str(e).lower(): | ||||
|                 pass  # Ignore this error | ||||
|             else: | ||||
|                 raise e | ||||
|  | ||||
|         max_bets_in_series_value = message.text | ||||
|  | ||||
|         if not is_int(max_bets_in_series_value): | ||||
|             await message.answer( | ||||
|                 "Ошибка: введите валидное число.", | ||||
|                 reply_markup=kbi.back_to_additional_settings, | ||||
|             ) | ||||
|             logger.debug( | ||||
|                 "User %s input invalid (not an valid number): %s", | ||||
|                 message.from_user.id, | ||||
|                 max_bets_in_series_value, | ||||
|             ) | ||||
|             return | ||||
|  | ||||
|         if safe_float(max_bets_in_series_value) < 1 or safe_float(max_bets_in_series_value) > 100: | ||||
|             await message.answer( | ||||
|                 "Ошибка: число должно быть в диапазоне от 1 до 100.", | ||||
|                 reply_markup=kbi.back_to_additional_settings, | ||||
|             ) | ||||
|             logger.debug( | ||||
|                 "User %s input invalid (not in range 1 to 100): %s", | ||||
|                 message.from_user.id, | ||||
|                 max_bets_in_series_value, | ||||
|             ) | ||||
|             return | ||||
|  | ||||
|         req = await rq.set_max_bets_in_series( | ||||
|             tg_id=message.from_user.id, max_bets_in_series=int(max_bets_in_series_value) | ||||
|         ) | ||||
|  | ||||
|         if req: | ||||
|             await message.answer( | ||||
|                 text=f"Максимальное количество шагов установлено на {message.text}", | ||||
|                 reply_markup=kbi.back_to_additional_settings, | ||||
|             ) | ||||
|         else: | ||||
|             await message.answer( | ||||
|                 text="Произошла ошибка при установке максимального количества шагов. Пожалуйста, попробуйте позже.", | ||||
|                 reply_markup=kbi.back_to_additional_settings, | ||||
|             ) | ||||
|  | ||||
|         await state.clear() | ||||
|     except Exception as e: | ||||
|         await message.answer( | ||||
|             text="Произошла ошибка при установке максимального количества шагов. Пожалуйста, попробуйте позже.", | ||||
|             reply_markup=kbi.back_to_additional_settings, | ||||
|         ) | ||||
|         logger.error( | ||||
|             "Error processing command set_max_bets_in_series for user %s: %s", | ||||
|             message.from_user.id, | ||||
|             e, | ||||
|         ) | ||||
							
								
								
									
										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() | ||||
							
								
								
									
										188
									
								
								app/telegram/handlers/settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								app/telegram/handlers/settings.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,188 @@ | ||||
| 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 | ||||
|  | ||||
|         switch_side_mode = "" | ||||
|         if trade_mode == "Switch": | ||||
|             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"{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) | ||||
							
								
								
									
										382
									
								
								app/telegram/keyboards/inline.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										382
									
								
								app/telegram/keyboards/inline.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,382 @@ | ||||
| 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="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"), | ||||
|         ], | ||||
|     ] | ||||
| ) | ||||
|  | ||||
| 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="Выберите пункт меню...", | ||||
| ) | ||||
| @@ -1,8 +0,0 @@ | ||||
| import logging | ||||
|  | ||||
| logging.basicConfig( | ||||
|     level=logging.INFO, | ||||
|     format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' | ||||
| ) | ||||
|  | ||||
| logger = logging.getLogger(__name__) | ||||
							
								
								
									
										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] | ||||
| @@ -1,6 +1,8 @@ | ||||
| from dotenv import load_dotenv | ||||
| import os | ||||
| from dotenv import load_dotenv, find_dotenv | ||||
|  | ||||
| load_dotenv('.env') | ||||
| env_path = find_dotenv() | ||||
| if env_path: | ||||
|     load_dotenv(env_path) | ||||
|  | ||||
| TOKEN_TG_BOT = os.getenv('TOKEN_TELEGRAM_BOT') | ||||
| BOT_TOKEN = os.getenv("BOT_TOKEN") | ||||
|   | ||||
							
								
								
									
										45
									
								
								database/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								database/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| from database.models import Base, User, UserAdditionalSettings, UserApi, UserConditionalSettings, UserDeals, \ | ||||
|     UserRiskManagement, UserSymbol | ||||
| import logging.config | ||||
| from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession | ||||
| from sqlalchemy import event | ||||
| from pathlib import Path | ||||
| from logger_helper.logger_helper import LOGGING_CONFIG | ||||
|  | ||||
| logging.config.dictConfig(LOGGING_CONFIG) | ||||
| logger = logging.getLogger("database") | ||||
|  | ||||
| BASE_DIR = Path(__file__).parent.resolve() | ||||
| DATA_DIR = BASE_DIR / "db" | ||||
| DATA_DIR.mkdir(parents=True, exist_ok=True) | ||||
|  | ||||
| DATABASE_URL = f"sqlite+aiosqlite:///{DATA_DIR / 'stcs.db'}" | ||||
|  | ||||
| async_engine = create_async_engine( | ||||
|     DATABASE_URL, | ||||
|     echo=False, | ||||
|     connect_args={"check_same_thread": False} | ||||
| ) | ||||
|  | ||||
|  | ||||
| @event.listens_for(async_engine.sync_engine, "connect") | ||||
| def _enable_foreign_keys(dbapi_connection, connection_record): | ||||
|     cursor = dbapi_connection.cursor() | ||||
|     cursor.execute("PRAGMA foreign_keys=ON") | ||||
|     cursor.close() | ||||
|  | ||||
|  | ||||
| async_session = async_sessionmaker( | ||||
|     async_engine, | ||||
|     class_=AsyncSession, | ||||
|     expire_on_commit=False | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def init_db(): | ||||
|     try: | ||||
|         async with async_engine.begin() as conn: | ||||
|             await conn.run_sync(Base.metadata.create_all) | ||||
|         logger.info("Database initialized.") | ||||
|     except Exception as e: | ||||
|         logger.error("Database initialization failed: %s", e) | ||||
							
								
								
									
										179
									
								
								database/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								database/models.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,179 @@ | ||||
| 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="По направлению") | ||||
|     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") | ||||
							
								
								
									
										1231
									
								
								database/request.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1231
									
								
								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
									
								
							
							
								
								
									
										149
									
								
								logger_helper/logger_helper.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								logger_helper/logger_helper.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,149 @@ | ||||
| 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": "TELEGRAM: %(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": { | ||||
|         "run": { | ||||
|             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||
|             "level": "DEBUG", | ||||
|             "propagate": False, | ||||
|         }, | ||||
|         "config": { | ||||
|             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||
|             "level": "DEBUG", | ||||
|             "propagate": False, | ||||
|         }, | ||||
|         "common": { | ||||
|             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||
|             "level": "DEBUG", | ||||
|             "propagate": False, | ||||
|         }, | ||||
|         "handlers_main": { | ||||
|             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||
|             "level": "DEBUG", | ||||
|             "propagate": False, | ||||
|         }, | ||||
|         "database": { | ||||
|             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||
|             "level": "DEBUG", | ||||
|             "propagate": False, | ||||
|         }, | ||||
|         "request": { | ||||
|             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||
|             "level": "DEBUG", | ||||
|             "propagate": False, | ||||
|         }, | ||||
|         "add_bybit_api": { | ||||
|             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||
|             "level": "DEBUG", | ||||
|             "propagate": False, | ||||
|         }, | ||||
|         "profile_tg": { | ||||
|             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||
|             "level": "DEBUG", | ||||
|             "propagate": False, | ||||
|         }, | ||||
|         "settings": { | ||||
|             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||
|             "level": "DEBUG", | ||||
|             "propagate": False, | ||||
|         }, | ||||
|         "additional_settings": { | ||||
|             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||
|             "level": "DEBUG", | ||||
|             "propagate": False, | ||||
|         }, | ||||
|         "helper_functions": { | ||||
|             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||
|             "level": "DEBUG", | ||||
|             "propagate": False, | ||||
|         }, | ||||
|         "risk_management": { | ||||
|             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||
|             "level": "DEBUG", | ||||
|             "propagate": False, | ||||
|         }, | ||||
|         "start_trading": { | ||||
|             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||
|             "level": "DEBUG", | ||||
|             "propagate": False, | ||||
|         }, | ||||
|         "stop_trading": { | ||||
|             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||
|             "level": "DEBUG", | ||||
|             "propagate": False, | ||||
|         }, | ||||
|         "changing_the_symbol": { | ||||
|             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||
|             "level": "DEBUG", | ||||
|             "propagate": False, | ||||
|         }, | ||||
|         "conditional_settings": { | ||||
|             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||
|             "level": "DEBUG", | ||||
|             "propagate": False, | ||||
|         }, | ||||
|         "get_positions_handlers": { | ||||
|             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||
|             "level": "DEBUG", | ||||
|             "propagate": False, | ||||
|         }, | ||||
|         "close_orders": { | ||||
|             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||
|             "level": "DEBUG", | ||||
|             "propagate": False, | ||||
|         }, | ||||
|         "tp_sl_handlers": { | ||||
|             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||
|             "level": "DEBUG", | ||||
|             "propagate": False, | ||||
|         }, | ||||
|         "tasks": { | ||||
|             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||
|             "level": "DEBUG", | ||||
|             "propagate": False, | ||||
|         }, | ||||
|     }, | ||||
| } | ||||
							
								
								
									
										55
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| aiofiles==24.1.0 | ||||
| aiogram==3.22.0 | ||||
| aiohappyeyeballs==2.6.1 | ||||
| aiohttp==3.12.15 | ||||
| aiosignal==1.4.0 | ||||
| aiosqlite==0.21.0 | ||||
| alembic==1.16.5 | ||||
| annotated-types==0.7.0 | ||||
| asyncpg==0.30.0 | ||||
| attrs==25.3.0 | ||||
| black==25.1.0 | ||||
| certifi==2025.8.3 | ||||
| charset-normalizer==3.4.3 | ||||
| click==8.2.1 | ||||
| colorama==0.4.6 | ||||
| dotenv==0.9.9 | ||||
| flake8==7.3.0 | ||||
| flake8-bugbear==24.12.12 | ||||
| flake8-pie==0.16.0 | ||||
| frozenlist==1.7.0 | ||||
| greenlet==3.2.4 | ||||
| idna==3.10 | ||||
| isort==6.0.1 | ||||
| magic-filter==1.0.12 | ||||
| Mako==1.3.10 | ||||
| mando==0.7.1 | ||||
| MarkupSafe==3.0.2 | ||||
| mccabe==0.7.0 | ||||
| multidict==6.6.4 | ||||
| mypy_extensions==1.1.0 | ||||
| nest-asyncio==1.6.0 | ||||
| packaging==25.0 | ||||
| pathspec==0.12.1 | ||||
| platformdirs==4.4.0 | ||||
| propcache==0.3.2 | ||||
| psycopg==3.2.10 | ||||
| psycopg-binary==3.2.10 | ||||
| pybit==5.11.0 | ||||
| pycodestyle==2.14.0 | ||||
| pycryptodome==3.23.0 | ||||
| pydantic==2.11.9 | ||||
| pydantic_core==2.33.2 | ||||
| pyflakes==3.4.0 | ||||
| python-dotenv==1.1.1 | ||||
| radon==6.0.1 | ||||
| redis==6.4.0 | ||||
| requests==2.32.5 | ||||
| six==1.17.0 | ||||
| SQLAlchemy==2.0.43 | ||||
| typing-inspection==0.4.1 | ||||
| typing_extensions==4.15.0 | ||||
| uliweb-alembic==0.6.9 | ||||
| urllib3==2.5.0 | ||||
| websocket-client==1.8.0 | ||||
| yarl==1.20.1 | ||||
							
								
								
									
										55
									
								
								run.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								run.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| import asyncio | ||||
| import contextlib | ||||
| import logging.config | ||||
|  | ||||
| from aiogram import Bot, Dispatcher | ||||
| from aiogram.fsm.storage.redis import RedisStorage | ||||
|  | ||||
| from database import init_db | ||||
| from app.bybit.web_socket import WebSocketBot | ||||
| from app.telegram.handlers import router | ||||
| from config import BOT_TOKEN | ||||
| from logger_helper.logger_helper import LOGGING_CONFIG | ||||
|  | ||||
| logging.config.dictConfig(LOGGING_CONFIG) | ||||
| logger = logging.getLogger("run") | ||||
|  | ||||
|  | ||||
| async def main(): | ||||
|     """ | ||||
|     The main function of launching the bot. | ||||
|  | ||||
|     Performs database initialization, creation of bot and dispatcher objects, | ||||
|     then it triggers the long polling event. | ||||
|  | ||||
|     Logs important events and errors. | ||||
|     """ | ||||
|     try: | ||||
|         await init_db() | ||||
|         bot = Bot(token=BOT_TOKEN) | ||||
|         storage = RedisStorage.from_url("redis://localhost:6379") | ||||
|         dp = Dispatcher(storage=storage) | ||||
|         dp.include_router(router) | ||||
|         web_socket = WebSocketBot(telegram_bot=bot) | ||||
|         await web_socket.clear_user_sockets() | ||||
|         ws_task = asyncio.create_task(web_socket.run_user_check_loop()) | ||||
|         tg_task = asyncio.create_task(dp.start_polling(bot)) | ||||
|  | ||||
|         try: | ||||
|             logger.info("Bot started") | ||||
|             await asyncio.gather(ws_task, tg_task) | ||||
|         except Exception as e: | ||||
|             logger.error("Bot stopped with error: %s", e) | ||||
|         finally: | ||||
|             for task in (ws_task, tg_task): | ||||
|                 task.cancel() | ||||
|             with contextlib.suppress(asyncio.CancelledError): | ||||
|                 await ws_task | ||||
|                 await tg_task | ||||
|  | ||||
|     except Exception as e: | ||||
|         logger.error("Bot stopped with error: %s", e) | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     asyncio.run(main()) | ||||
		Reference in New Issue
	
	Block a user