forked from kodorvan/stcs
		
	Compare commits
	
		
			236 Commits
		
	
	
		
			0fecb5dab7
			...
			stable
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 46c890f7af | |||
|   | f10500cc79 | ||
| 2d7acb491e | |||
|   | d767399988 | ||
|   | 89603f0b62 | ||
|   | 14f2a9e773 | ||
|   | a43fc6a66b | ||
|   | 869458b2e1 | ||
|   | 07948d93cf | ||
| 12d1db16d3 | |||
|   | 7350c86927 | ||
| 0a369b10f2 | |||
|   | 42f0f8ddc0 | ||
|   | 3df88d07ab | ||
| 7b1a803db4 | |||
|   | ddfa3a7360 | ||
| 9fcd92cc72 | |||
|   | e61b7334a4 | ||
| 97a199f31e | |||
|   | 5ad69f3f6d | ||
|   | abad01352a | ||
|   | 720b30d681 | ||
|   | 3616e2cbd3 | ||
|   | 7d108337fa | ||
|   | 0f6e6a2168 | ||
| 951bc15957 | |||
|   | 258ed970f1 | ||
|   | a3a6509933 | ||
| 5937058899 | |||
|   | 8251938b2f | ||
| f0732607e2 | |||
|   | 458b34fcec | ||
| 56af1d8f3b | |||
|   | 4a7577b977 | ||
| 9f069df68a | |||
|   | 6e0a170f4b | ||
|   | c7b4a08a6a | ||
|   | d0971f59b4 | ||
| b92376d2da | |||
| 630f2002d3 | |||
| 0784cbb54a | |||
| eeb7f81440 | |||
| b03d05bb75 | |||
| e0e4ad5d4b | |||
| fab8ff5040 | |||
| 8071f8c896 | |||
| 3db001bd19 | |||
| 99c59be9ed | |||
| 37b7b6effd | |||
| ee285523f2 | |||
| b426eb2136 | |||
| 2df3b8b40d | |||
| 8c08451d82 | |||
| d81a47b669 | |||
| 2cdfba3537 | |||
| c89c2ad803 | |||
| 3986989dbd | |||
| c0e40dc205 | |||
| 6c6f0dbb7b | |||
| 44c4fde036 | |||
| 21a93d47d4 | |||
| 3f43d42651 | |||
| aab05994ce | |||
| a58ebe6a46 | |||
| 1ec1f1784d | |||
| 7901af86af | |||
| fedfa00c10 | |||
|   | fc8ab19ae9 | ||
|   | 42c4660fe3 | ||
|   | fe030baef5 | ||
|   | 9d06412605 | ||
|   | 9c1f289870 | ||
|   | 3533e7e99a | ||
|   | 8114533475 | ||
|   | fcdc9d7483 | ||
|   | aa9f04c27e | ||
|   | 89ab106992 | ||
|   | ebe2d58975 | ||
|   | 09606a057b | ||
|   | a0a2fd30f0 | ||
|   | 2136de5d69 | ||
|   | dbbea16c19 | ||
| 898ff91392 | |||
|   | f5677e6e7e | ||
| 2047dd5ac6 | |||
|   | c49df2794d | ||
|   | c687811ea5 | ||
|   | 5da00dbaa1 | ||
|   | 01fe339d56 | ||
|   | 220c45d54c | ||
|   | 163f4dcba9 | ||
|   | ce5d0605de | ||
|   | 086c7c8170 | ||
|   | 8e73dcf81f | ||
|   | 057cfad675 | ||
|   | 1508629727 | ||
|   | 4adbd70948 | ||
|   | 6705bf4492 | ||
|   | 8dbc8d57f9 | ||
|   | fa782f748a | ||
|   | a1a7355dc3 | ||
|   | 9d2b049e56 | ||
|   | 3306c6e826 | ||
|   | 2666f90707 | ||
|   | bed53c0a2c | ||
|   | a9f7c4f7c4 | ||
|   | 1981510963 | ||
|   | 4f2ce0c1a4 | ||
|   | 3ae8c15007 | ||
|   | f81f63b198 | ||
|   | 97662081ce | ||
|   | e5a3de4ed8 | ||
|   | 66a566e6a3 | ||
|   | eca9d2c7c8 | ||
|   | 6d86b230ca | ||
| fec367cc1d | |||
|   | 4bbff680aa | ||
|   | 49d4bb26bf | ||
|   | 29bb6bd0a8 | ||
|   | 2fb8cb4acb | ||
|   | 887b46c1d4 | ||
|   | b074d1d8a1 | ||
| aebcc9dff2 | |||
|   | e2f9478971 | ||
|   | 4f0668970f | ||
|   | 4c9901c14a | ||
|   | 17dba19078 | ||
| 58a4c6af06 | |||
| b37b7193b2 | |||
| 05e8005ec9 | |||
|   | 0de3b17d1d | ||
|   | 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 | # Byte-compiled / optimized / DLL files | ||||||
| !*.sample |  | ||||||
|  |  | ||||||
| __pycache__/ | __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/ | env/ | ||||||
| venv/ | 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 | ||||||
| __ |  | ||||||
|  |  | ||||||
| **Функционал:** | Этот бот — автоматизированный торговый помощник для работы с криптовалютной биржей Bybit на основе стратегии мартингейла. Он позволяет торговать бессрочными контрактами с управлением рисками, тейк-профитами, стоп-лоссами и кредитным плечом. | ||||||
| + **Настройки** |  | ||||||
|   + Основные параметры *(Настроен, работает)* | ## Основные возможности | ||||||
|     + Режим торговли Лонг/Шорт *(настроены)*, Switch/Smart *(не настроены)* |  | ||||||
|     + Тип маржи: Изолированная / Кросс *(настроено)* | - Поддержка работы с биржей Bybit через официальный API. | ||||||
|     + Размер кредитного плеча: от x1 до x100 *(настроено)* |  | ||||||
|     + Начальная ставка: числовое значение *(настроено)* | - Открытие и закрытие позиций по выбранным торговым парам. | ||||||
|     + Коэффициент мартингейла: число *(настроено)* |  | ||||||
|     + Максимальное количество ставок в серии: число *(настроено)* | - Поддержка рыночных и лимитных ордеров. | ||||||
|   + Риск-менеджмент (Настроен, работает) |  | ||||||
|     + Процент изменения цены для фиксации прибыли (TP%): число *(настроено)* | - Установка уровней тейк-профита (TP) и стоп-лосса (SL). | ||||||
|     + Процент изменения цены для фиксации убытков (SL%): число (пример: 1%) *(настроено)* |  | ||||||
|     + Максимальный риск на сделку (в % от баланса): число (опционально) *(настроено)* | - Управление кредитным плечом (leverage). | ||||||
|    + Условия запуска *(Не настроен)* |  | ||||||
|    + Дополнительные параметры *(Не настроен)* | - Реализация стратегии мартингейла с настройками шага, коэффициента и лимитов. | ||||||
|    + Подключение Bybit *(настроено)* |  | ||||||
|     + Информация о правильном получении и сохранении Bybit-API keys *(настроено)* | - Контроль максимального риска на сделку по балансу пользователя. | ||||||
|  |  | ||||||
|  | - Обработка ошибок 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"} | ||||||
| @@ -0,0 +1,34 @@ | |||||||
|  | """Added column side for additional_setiings | ||||||
|  |  | ||||||
|  | Revision ID: e5d612e44563 | ||||||
|  | Revises: fbf4e3658310 | ||||||
|  | Create Date: 2025-10-25 18:25:52.746250 | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | from typing import Sequence, Union | ||||||
|  |  | ||||||
|  | from alembic import op | ||||||
|  | import sqlalchemy as sa | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # revision identifiers, used by Alembic. | ||||||
|  | revision: str = 'e5d612e44563' | ||||||
|  | down_revision: Union[str, Sequence[str], None] = 'fbf4e3658310' | ||||||
|  | branch_labels: Union[str, Sequence[str], None] = None | ||||||
|  | depends_on: Union[str, Sequence[str], None] = None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def upgrade() -> None: | ||||||
|  |     """Upgrade schema.""" | ||||||
|  |     # ### commands auto generated by Alembic - please adjust! ### | ||||||
|  |     op.add_column('user_additional_settings', | ||||||
|  |                   sa.Column('side', sa.String(), nullable=False, server_default='') | ||||||
|  |                   ) | ||||||
|  |     # ### end Alembic commands ### | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def downgrade() -> None: | ||||||
|  |     """Downgrade schema.""" | ||||||
|  |     # ### commands auto generated by Alembic - please adjust! ### | ||||||
|  |     op.drop_column('user_additional_settings', 'side') | ||||||
|  |     # ### end Alembic commands ### | ||||||
							
								
								
									
										32
									
								
								alembic/versions/fbf4e3658310_added_side_mode_column.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								alembic/versions/fbf4e3658310_added_side_mode_column.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | """Added side_mode column | ||||||
|  |  | ||||||
|  | Revision ID: fbf4e3658310 | ||||||
|  | Revises:  | ||||||
|  | Create Date: 2025-10-22 13:08:02.317419 | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | from typing import Sequence, Union | ||||||
|  |  | ||||||
|  | from alembic import op | ||||||
|  | import sqlalchemy as sa | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # revision identifiers, used by Alembic. | ||||||
|  | revision: str = 'fbf4e3658310' | ||||||
|  | down_revision: Union[str, Sequence[str], None] = None | ||||||
|  | branch_labels: Union[str, Sequence[str], None] = None | ||||||
|  | depends_on: Union[str, Sequence[str], None] = None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def upgrade() -> None: | ||||||
|  |     """Upgrade schema.""" | ||||||
|  |     # ### commands auto generated by Alembic - please adjust! ### | ||||||
|  |     op.add_column('user_deals', sa.Column('side_mode', sa.String(), nullable=True)) | ||||||
|  |     # ### end Alembic commands ### | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def downgrade() -> None: | ||||||
|  |     """Downgrade schema.""" | ||||||
|  |     # ### commands auto generated by Alembic - please adjust! ### | ||||||
|  |     op.drop_column('user_deals', 'side_mode') | ||||||
|  |     # ### end Alembic commands ### | ||||||
							
								
								
									
										0
									
								
								app/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										21
									
								
								app/bybit/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/bybit/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | import logging.config | ||||||
|  |  | ||||||
|  | from pybit.unified_trading import HTTP | ||||||
|  |  | ||||||
|  | from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG | ||||||
|  | from database import request as rq | ||||||
|  |  | ||||||
|  | logging.config.dictConfig(LOGGING_CONFIG) | ||||||
|  | logger = logging.getLogger("bybit") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def get_bybit_client(tg_id: int) -> HTTP | None: | ||||||
|  |     """ | ||||||
|  |     Get bybit client | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         api_key, api_secret = await rq.get_user_api(tg_id=tg_id) | ||||||
|  |         return HTTP(api_key=api_key, api_secret=api_secret) | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error("Error getting bybit client for user %s: %s", tg_id, e) | ||||||
|  |         return None | ||||||
							
								
								
									
										100
									
								
								app/bybit/close_positions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								app/bybit/close_positions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | |||||||
|  | import logging.config | ||||||
|  |  | ||||||
|  | from app.bybit import get_bybit_client | ||||||
|  | from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG | ||||||
|  |  | ||||||
|  | logging.config.dictConfig(LOGGING_CONFIG) | ||||||
|  | logger = logging.getLogger("close_positions") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def close_position_by_symbol( | ||||||
|  |     tg_id: int, symbol: str | ||||||
|  | ) -> bool: | ||||||
|  |     """ | ||||||
|  |     Closes all positions | ||||||
|  |     :param tg_id: Telegram user ID | ||||||
|  |     :param symbol: symbol | ||||||
|  |     :return: bool | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         client = await get_bybit_client(tg_id) | ||||||
|  |  | ||||||
|  |         response = client.get_positions( | ||||||
|  |             category="linear", symbol=symbol | ||||||
|  |         ) | ||||||
|  |         positions = response.get("result", {}).get("list", []) | ||||||
|  |         r_side = "Sell" if positions[0].get("side") == "Buy" else "Buy" | ||||||
|  |         qty = positions[0].get("size") | ||||||
|  |         position_idx = positions[0].get("positionIdx") | ||||||
|  |  | ||||||
|  |         response = client.place_order( | ||||||
|  |             category="linear", | ||||||
|  |             symbol=symbol, | ||||||
|  |             side=r_side, | ||||||
|  |             orderType="Market", | ||||||
|  |             qty=qty, | ||||||
|  |             timeInForce="GTC", | ||||||
|  |             positionIdx=position_idx, | ||||||
|  |         ) | ||||||
|  |         if response["retCode"] == 0: | ||||||
|  |             logger.info("Positions closed for %s for user %s", symbol, tg_id) | ||||||
|  |             return True | ||||||
|  |         else: | ||||||
|  |             logger.error( | ||||||
|  |                 "Error closing position for %s for user %s", symbol, tg_id | ||||||
|  |             ) | ||||||
|  |             return False | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error( | ||||||
|  |             "Error closing positions for %s for user %s: %s", symbol, tg_id, e | ||||||
|  |         ) | ||||||
|  |         return False | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def cancel_order(tg_id: int, symbol: str, order_id: str) -> bool: | ||||||
|  |     """ | ||||||
|  |     Cancel order by order id | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         client = await get_bybit_client(tg_id) | ||||||
|  |  | ||||||
|  |         cancel_resp = client.cancel_order( | ||||||
|  |             category="linear", symbol=symbol, orderId=order_id | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         if cancel_resp.get("retCode") == 0: | ||||||
|  |             return True | ||||||
|  |         else: | ||||||
|  |             logger.error( | ||||||
|  |                 "Error canceling order for user %s: %s", | ||||||
|  |                 tg_id, | ||||||
|  |                 cancel_resp.get("retMsg"), | ||||||
|  |             ) | ||||||
|  |             return False | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error("Error canceling order for user %s: %s", tg_id, e) | ||||||
|  |         return False | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def cancel_all_orders(tg_id: int) -> bool: | ||||||
|  |     """ | ||||||
|  |     Cancel all open orders | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         client = await get_bybit_client(tg_id) | ||||||
|  |         cancel_resp = client.cancel_all_orders(category="linear", settleCoin="USDT") | ||||||
|  |  | ||||||
|  |         if cancel_resp.get("retCode") == 0: | ||||||
|  |             logger.info("All orders canceled for user %s", tg_id) | ||||||
|  |             return True | ||||||
|  |         else: | ||||||
|  |             logger.error( | ||||||
|  |                 "Error canceling order for user %s: %s", | ||||||
|  |                 tg_id, | ||||||
|  |                 cancel_resp.get("retMsg"), | ||||||
|  |             ) | ||||||
|  |             return False | ||||||
|  |  | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error("Error canceling order for user %s: %s", tg_id, e) | ||||||
|  |         return False | ||||||
							
								
								
									
										0
									
								
								app/bybit/get_functions/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/bybit/get_functions/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										28
									
								
								app/bybit/get_functions/get_balance.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								app/bybit/get_functions/get_balance.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | import logging.config | ||||||
|  |  | ||||||
|  | from app.bybit import get_bybit_client | ||||||
|  | from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG | ||||||
|  |  | ||||||
|  | logging.config.dictConfig(LOGGING_CONFIG) | ||||||
|  | logger = logging.getLogger("get_balance") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def get_balance(tg_id: int) -> bool | dict: | ||||||
|  |     """ | ||||||
|  |     Get balance bybit | ||||||
|  |     """ | ||||||
|  |     client = await get_bybit_client(tg_id=tg_id) | ||||||
|  |  | ||||||
|  |     try: | ||||||
|  |         response = client.get_wallet_balance(accountType="UNIFIED") | ||||||
|  |         if response["retCode"] == 0: | ||||||
|  |             info = response["result"]["list"][0] | ||||||
|  |             return info | ||||||
|  |         else: | ||||||
|  |             logger.error( | ||||||
|  |                 "Error getting balance for user %s: %s", tg_id, response.get("retMsg") | ||||||
|  |             ) | ||||||
|  |             return False | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error("Error connecting to Bybit for user %s: %s", tg_id, e) | ||||||
|  |         return False | ||||||
							
								
								
									
										28
									
								
								app/bybit/get_functions/get_instruments_info.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								app/bybit/get_functions/get_instruments_info.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | import logging.config | ||||||
|  |  | ||||||
|  | from app.bybit import get_bybit_client | ||||||
|  | from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG | ||||||
|  |  | ||||||
|  | logging.config.dictConfig(LOGGING_CONFIG) | ||||||
|  | logger = logging.getLogger("get_instruments_info") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def get_instruments_info(tg_id: int, symbol: str) -> dict | None: | ||||||
|  |     """ | ||||||
|  |     Get instruments info | ||||||
|  |     :param tg_id: int - User ID | ||||||
|  |     :param symbol: str - Symbol | ||||||
|  |     :return: dict - Instruments info | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         client = await get_bybit_client(tg_id=tg_id) | ||||||
|  |         response = client.get_instruments_info(category="linear", symbol=symbol) | ||||||
|  |         if response["retCode"] == 0: | ||||||
|  |             logger.info("Instruments info for user: %s", tg_id) | ||||||
|  |             return response["result"]["list"][0] | ||||||
|  |         else: | ||||||
|  |             logger.error("Error getting price: %s", tg_id) | ||||||
|  |             return None | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error("Error connecting to Bybit for user %s: %s", tg_id, e) | ||||||
|  |         return None | ||||||
							
								
								
									
										129
									
								
								app/bybit/get_functions/get_positions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								app/bybit/get_functions/get_positions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,129 @@ | |||||||
|  | import logging.config | ||||||
|  |  | ||||||
|  | from app.bybit import get_bybit_client | ||||||
|  | from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG | ||||||
|  |  | ||||||
|  | logging.config.dictConfig(LOGGING_CONFIG) | ||||||
|  | logger = logging.getLogger("get_positions") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def get_active_positions(tg_id: int) -> list | None: | ||||||
|  |     """ | ||||||
|  |     Get active positions for a user | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         client = await get_bybit_client(tg_id) | ||||||
|  |         response = client.get_positions(category="linear", settleCoin="USDT") | ||||||
|  |  | ||||||
|  |         if response["retCode"] == 0: | ||||||
|  |             positions = response.get("result", {}).get("list", []) | ||||||
|  |             active_symbols = [ | ||||||
|  |                 pos.get("symbol") for pos in positions if float(pos.get("size", 0)) > 0 | ||||||
|  |             ] | ||||||
|  |             if active_symbols: | ||||||
|  |                 logger.info("Active positions for user: %s", tg_id) | ||||||
|  |                 return positions | ||||||
|  |             else: | ||||||
|  |                 logger.warning("No active positions found for user: %s", tg_id) | ||||||
|  |                 return ["No active positions found"] | ||||||
|  |         else: | ||||||
|  |             logger.error( | ||||||
|  |                 "Error getting active positions for user %s: %s", | ||||||
|  |                 tg_id, | ||||||
|  |                 response["retMsg"], | ||||||
|  |             ) | ||||||
|  |             return None | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error("Error getting active positions for user %s: %s", tg_id, e) | ||||||
|  |         return None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def get_active_positions_by_symbol(tg_id: int, symbol: str) -> dict | None: | ||||||
|  |     """ | ||||||
|  |     Get active positions for a user by symbol | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         client = await get_bybit_client(tg_id) | ||||||
|  |         response = client.get_positions(category="linear", symbol=symbol) | ||||||
|  |  | ||||||
|  |         if response["retCode"] == 0: | ||||||
|  |             positions = response.get("result", {}).get("list", []) | ||||||
|  |             if positions: | ||||||
|  |                 logger.info("Active positions for user: %s", tg_id) | ||||||
|  |                 return positions | ||||||
|  |             else: | ||||||
|  |                 logger.warning("No active positions found for user: %s", tg_id) | ||||||
|  |                 return None | ||||||
|  |         else: | ||||||
|  |             logger.error( | ||||||
|  |                 "Error getting active positions for user %s: %s", | ||||||
|  |                 tg_id, | ||||||
|  |                 response["retMsg"], | ||||||
|  |             ) | ||||||
|  |             return None | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error("Error getting active positions for user %s: %s", tg_id, e) | ||||||
|  |         return None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def get_active_orders(tg_id: int) -> list | None: | ||||||
|  |     """ | ||||||
|  |     Get active orders | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         client = await get_bybit_client(tg_id) | ||||||
|  |         response = client.get_open_orders( | ||||||
|  |             category="linear", | ||||||
|  |             settleCoin="USDT", | ||||||
|  |             limit=50, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         if response["retCode"] == 0: | ||||||
|  |             orders = response.get("result", {}).get("list", []) | ||||||
|  |             active_orders = [ | ||||||
|  |                 pos.get("symbol") for pos in orders if float(pos.get("qty", 0)) > 0 | ||||||
|  |             ] | ||||||
|  |             if active_orders: | ||||||
|  |                 logger.info("Active orders for user: %s", tg_id) | ||||||
|  |                 return orders | ||||||
|  |             else: | ||||||
|  |                 logger.warning("No active orders found for user: %s", tg_id) | ||||||
|  |                 return ["No active orders found"] | ||||||
|  |         else: | ||||||
|  |             logger.error( | ||||||
|  |                 "Error getting active orders for user %s: %s", tg_id, response["retMsg"] | ||||||
|  |             ) | ||||||
|  |             return None | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error("Error getting active orders for user %s: %s", tg_id, e) | ||||||
|  |         return None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def get_active_orders_by_symbol(tg_id: int, symbol: str) -> dict | None: | ||||||
|  |     """ | ||||||
|  |     Get active orders by symbol | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         client = await get_bybit_client(tg_id) | ||||||
|  |         response = client.get_open_orders( | ||||||
|  |             category="linear", | ||||||
|  |             symbol=symbol, | ||||||
|  |             limit=50, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         if response["retCode"] == 0: | ||||||
|  |             orders = response.get("result", {}).get("list", []) | ||||||
|  |             if orders: | ||||||
|  |                 logger.info("Active orders for user: %s", tg_id) | ||||||
|  |                 return orders | ||||||
|  |             else: | ||||||
|  |                 logger.warning("No active orders found for user: %s", tg_id) | ||||||
|  |                 return None | ||||||
|  |         else: | ||||||
|  |             logger.error( | ||||||
|  |                 "Error getting active orders for user %s: %s", tg_id, response["retMsg"] | ||||||
|  |             ) | ||||||
|  |             return None | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error("Error getting active orders for user %s: %s", tg_id, e) | ||||||
|  |         return None | ||||||
							
								
								
									
										35
									
								
								app/bybit/get_functions/get_tickers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								app/bybit/get_functions/get_tickers.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | import logging.config | ||||||
|  |  | ||||||
|  | from app.bybit import get_bybit_client | ||||||
|  | from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG | ||||||
|  |  | ||||||
|  | logging.config.dictConfig(LOGGING_CONFIG) | ||||||
|  | logger = logging.getLogger("get_tickers") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def get_tickers(tg_id: int, symbol: str) -> dict | None: | ||||||
|  |     """ | ||||||
|  |     Get tickers | ||||||
|  |     :param tg_id: int Telegram ID | ||||||
|  |     :param symbol: str Symbol | ||||||
|  |     :return: dict | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         client = await get_bybit_client(tg_id=tg_id) | ||||||
|  |         response = client.get_tickers(category="linear", symbol=symbol) | ||||||
|  |         if response["retCode"] == 0: | ||||||
|  |             tickers = response["result"]["list"] | ||||||
|  |             # USDT quoteCoin | ||||||
|  |             usdt_tickers = [t for t in tickers if t.get("symbol", "").endswith("USDT")] | ||||||
|  |             if usdt_tickers: | ||||||
|  |                 logger.info("USDT tickers for user: %s", tg_id) | ||||||
|  |                 return usdt_tickers[0] | ||||||
|  |             else: | ||||||
|  |                 logger.warning("No USDT tickers found for user: %s", tg_id) | ||||||
|  |                 return None | ||||||
|  |         else: | ||||||
|  |             logger.error("Error getting price: %s", tg_id) | ||||||
|  |             return None | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error("Error connecting to Bybit for user %s: %s", tg_id, e) | ||||||
|  |         return None | ||||||
							
								
								
									
										0
									
								
								app/bybit/logger_bybit/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/bybit/logger_bybit/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										129
									
								
								app/bybit/logger_bybit/logger_bybit.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								app/bybit/logger_bybit/logger_bybit.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,129 @@ | |||||||
|  | import os | ||||||
|  |  | ||||||
|  | current_directory = os.path.dirname(os.path.abspath(__file__)) | ||||||
|  | log_directory = os.path.join(current_directory, "loggers") | ||||||
|  | error_log_directory = os.path.join(log_directory, "errors") | ||||||
|  | os.makedirs(log_directory, exist_ok=True) | ||||||
|  | os.makedirs(error_log_directory, exist_ok=True) | ||||||
|  | log_filename = os.path.join(log_directory, "app.log") | ||||||
|  | error_log_filename = os.path.join(error_log_directory, "error.log") | ||||||
|  |  | ||||||
|  | LOGGING_CONFIG = { | ||||||
|  |     "version": 1, | ||||||
|  |     "disable_existing_loggers": False, | ||||||
|  |     "formatters": { | ||||||
|  |         "default": { | ||||||
|  |             "format": "BYBIT: %(asctime)s - %(name)s - %(levelname)s - %(message)s", | ||||||
|  |             "datefmt": "%Y-%m-%d %H:%M:%S",  # Формат даты | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  |     "handlers": { | ||||||
|  |         "timed_rotating_file": { | ||||||
|  |             "class": "logging.handlers.TimedRotatingFileHandler", | ||||||
|  |             "filename": log_filename, | ||||||
|  |             "when": "midnight",  # Время ротации (каждую полночь) | ||||||
|  |             "interval": 1,  # Интервал в днях | ||||||
|  |             "backupCount": 7,  # Количество сохраняемых архивов (0 - не сохранять) | ||||||
|  |             "formatter": "default", | ||||||
|  |             "encoding": "utf-8", | ||||||
|  |             "level": "DEBUG", | ||||||
|  |         }, | ||||||
|  |         "error_file": { | ||||||
|  |             "class": "logging.handlers.TimedRotatingFileHandler", | ||||||
|  |             "filename": error_log_filename, | ||||||
|  |             "when": "midnight", | ||||||
|  |             "interval": 1, | ||||||
|  |             "backupCount": 30, | ||||||
|  |             "formatter": "default", | ||||||
|  |             "encoding": "utf-8", | ||||||
|  |             "level": "ERROR", | ||||||
|  |         }, | ||||||
|  |         "console": { | ||||||
|  |             "class": "logging.StreamHandler", | ||||||
|  |             "formatter": "default", | ||||||
|  |             "level": "DEBUG", | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  |     "loggers": { | ||||||
|  |         "profile_bybit": { | ||||||
|  |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|  |             "level": "DEBUG", | ||||||
|  |             "propagate": False, | ||||||
|  |         }, | ||||||
|  |         "get_balance": { | ||||||
|  |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|  |             "level": "DEBUG", | ||||||
|  |             "propagate": False, | ||||||
|  |         }, | ||||||
|  |         "price_symbol": { | ||||||
|  |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|  |             "level": "DEBUG", | ||||||
|  |             "propagate": False, | ||||||
|  |         }, | ||||||
|  |         "bybit": { | ||||||
|  |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|  |             "level": "DEBUG", | ||||||
|  |             "propagate": False, | ||||||
|  |         }, | ||||||
|  |         "web_socket": { | ||||||
|  |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|  |             "level": "DEBUG", | ||||||
|  |             "propagate": False, | ||||||
|  |         }, | ||||||
|  |         "get_tickers": { | ||||||
|  |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|  |             "level": "DEBUG", | ||||||
|  |             "propagate": False, | ||||||
|  |         }, | ||||||
|  |         "set_margin_mode": { | ||||||
|  |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|  |             "level": "DEBUG", | ||||||
|  |             "propagate": False, | ||||||
|  |         }, | ||||||
|  |         "set_switch_margin_mode": { | ||||||
|  |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|  |             "level": "DEBUG", | ||||||
|  |             "propagate": False, | ||||||
|  |         }, | ||||||
|  |         "set_switch_position_mode": { | ||||||
|  |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|  |             "level": "DEBUG", | ||||||
|  |             "propagate": False, | ||||||
|  |         }, | ||||||
|  |         "set_leverage": { | ||||||
|  |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|  |             "level": "DEBUG", | ||||||
|  |             "propagate": False, | ||||||
|  |         }, | ||||||
|  |         "get_instruments_info": { | ||||||
|  |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|  |             "level": "DEBUG", | ||||||
|  |             "propagate": False, | ||||||
|  |         }, | ||||||
|  |         "get_positions": { | ||||||
|  |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|  |             "level": "DEBUG", | ||||||
|  |             "propagate": False, | ||||||
|  |         }, | ||||||
|  |         "open_positions": { | ||||||
|  |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|  |             "level": "DEBUG", | ||||||
|  |             "propagate": False, | ||||||
|  |         }, | ||||||
|  |         "close_positions": { | ||||||
|  |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|  |             "level": "DEBUG", | ||||||
|  |             "propagate": False, | ||||||
|  |         }, | ||||||
|  |         "telegram_message_handler": { | ||||||
|  |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|  |             "level": "DEBUG", | ||||||
|  |             "propagate": False, | ||||||
|  |         }, | ||||||
|  |         "set_tp_sl": { | ||||||
|  |             "handlers": ["console", "timed_rotating_file", "error_file"], | ||||||
|  |             "level": "DEBUG", | ||||||
|  |             "propagate": False, | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  | } | ||||||
							
								
								
									
										401
									
								
								app/bybit/open_positions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										401
									
								
								app/bybit/open_positions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,401 @@ | |||||||
|  | import logging.config | ||||||
|  | import math | ||||||
|  |  | ||||||
|  | from pybit.exceptions import InvalidRequestError | ||||||
|  |  | ||||||
|  | import database.request as rq | ||||||
|  | from app.bybit import get_bybit_client | ||||||
|  | from app.bybit.get_functions.get_instruments_info import get_instruments_info | ||||||
|  | from app.bybit.get_functions.get_tickers import get_tickers | ||||||
|  | from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG | ||||||
|  | from app.bybit.set_functions.set_leverage import set_leverage | ||||||
|  | from app.bybit.set_functions.set_margin_mode import set_margin_mode | ||||||
|  | from app.bybit.set_functions.set_switch_position_mode import set_switch_position_mode | ||||||
|  | from app.helper_functions import safe_float | ||||||
|  |  | ||||||
|  | logging.config.dictConfig(LOGGING_CONFIG) | ||||||
|  | logger = logging.getLogger("open_positions") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def start_trading_cycle( | ||||||
|  |         tg_id: int | ||||||
|  | ) -> str | None: | ||||||
|  |     """ | ||||||
|  |     Start trading cycle | ||||||
|  |     :param tg_id: Telegram user ID | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         symbol = await rq.get_user_symbol(tg_id=tg_id) | ||||||
|  |         additional_data = await rq.get_user_additional_settings(tg_id=tg_id) | ||||||
|  |         risk_management_data = await rq.get_user_risk_management(tg_id=tg_id) | ||||||
|  |         trade_mode = additional_data.trade_mode | ||||||
|  |         switch_side = additional_data.switch_side | ||||||
|  |         side= additional_data.side | ||||||
|  |         margin_type = additional_data.margin_type | ||||||
|  |         leverage = additional_data.leverage | ||||||
|  |         order_quantity = additional_data.order_quantity | ||||||
|  |         trigger_price = additional_data.trigger_price | ||||||
|  |         martingale_factor = additional_data.martingale_factor | ||||||
|  |         max_bets_in_series = additional_data.max_bets_in_series | ||||||
|  |         take_profit_percent = risk_management_data.take_profit_percent | ||||||
|  |         stop_loss_percent = risk_management_data.stop_loss_percent | ||||||
|  |         total_commission = 0 | ||||||
|  |  | ||||||
|  |         if trade_mode == "Switch": | ||||||
|  |             side = side | ||||||
|  |         else: | ||||||
|  |             if trade_mode == "Long": | ||||||
|  |                 side = "Buy" | ||||||
|  |             else: | ||||||
|  |                 side = "Sell" | ||||||
|  |  | ||||||
|  |         await set_switch_position_mode( | ||||||
|  |             tg_id=tg_id, | ||||||
|  |             symbol=symbol, | ||||||
|  |             mode=0) | ||||||
|  |         await set_margin_mode(tg_id=tg_id, margin_mode=margin_type) | ||||||
|  |         await set_leverage( | ||||||
|  |             tg_id=tg_id, | ||||||
|  |             symbol=symbol, | ||||||
|  |             leverage=leverage, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         res = await open_positions( | ||||||
|  |             tg_id=tg_id, | ||||||
|  |             symbol=symbol, | ||||||
|  |             side=side, | ||||||
|  |             order_quantity=order_quantity, | ||||||
|  |             trigger_price=trigger_price, | ||||||
|  |             margin_type=margin_type, | ||||||
|  |             leverage=leverage, | ||||||
|  |             take_profit_percent=take_profit_percent, | ||||||
|  |             stop_loss_percent=stop_loss_percent, | ||||||
|  |             commission_fee_percent=total_commission | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         if res == "OK": | ||||||
|  |             await rq.set_user_deal( | ||||||
|  |                 tg_id=tg_id, | ||||||
|  |                 symbol=symbol, | ||||||
|  |                 current_step=1, | ||||||
|  |                 trade_mode=trade_mode, | ||||||
|  |                 side_mode=switch_side, | ||||||
|  |                 margin_type=margin_type, | ||||||
|  |                 leverage=leverage, | ||||||
|  |                 order_quantity=order_quantity, | ||||||
|  |                 trigger_price=trigger_price, | ||||||
|  |                 martingale_factor=martingale_factor, | ||||||
|  |                 max_bets_in_series=max_bets_in_series, | ||||||
|  |                 take_profit_percent=take_profit_percent, | ||||||
|  |                 stop_loss_percent=stop_loss_percent, | ||||||
|  |                 base_quantity=order_quantity | ||||||
|  |             ) | ||||||
|  |             return "OK" | ||||||
|  |         return ( | ||||||
|  |             res | ||||||
|  |             if res | ||||||
|  |                in { | ||||||
|  |                    "Limit price is out min price", | ||||||
|  |                    "Limit price is out max price", | ||||||
|  |                    "Risk is too high for this trade", | ||||||
|  |                    "estimated will trigger liq", | ||||||
|  |                    "ab not enough for new order", | ||||||
|  |                    "InvalidRequestError", | ||||||
|  |                    "Order does not meet minimum order value", | ||||||
|  |                    "position idx not match position mode", | ||||||
|  |                    "Qty invalid", | ||||||
|  |                    "The number of contracts exceeds maximum limit allowed", | ||||||
|  |                    "The number of contracts exceeds minimum limit allowed" | ||||||
|  |                } | ||||||
|  |             else None | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error("Error in start_trading: %s", e) | ||||||
|  |         return None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def trading_cycle_profit( | ||||||
|  |         tg_id: int, symbol: str, side: str) -> str | None: | ||||||
|  |     try: | ||||||
|  |         user_deals_data = await rq.get_user_deal_by_symbol(tg_id=tg_id, symbol=symbol) | ||||||
|  |         user_auto_trading_data = await rq.get_user_auto_trading(tg_id=tg_id, symbol=symbol) | ||||||
|  |         total_fee = user_auto_trading_data.total_fee | ||||||
|  |         trade_mode = user_deals_data.trade_mode | ||||||
|  |         margin_type = user_deals_data.margin_type | ||||||
|  |         leverage = user_deals_data.leverage | ||||||
|  |         trigger_price = 0 | ||||||
|  |         take_profit_percent = user_deals_data.take_profit_percent | ||||||
|  |         stop_loss_percent = user_deals_data.stop_loss_percent | ||||||
|  |         max_bets_in_series = user_deals_data.max_bets_in_series | ||||||
|  |         martingale_factor = user_deals_data.martingale_factor | ||||||
|  |         side_mode = user_deals_data.side_mode | ||||||
|  |         base_quantity = user_deals_data.base_quantity | ||||||
|  |  | ||||||
|  |         await set_margin_mode(tg_id=tg_id, margin_mode=margin_type) | ||||||
|  |         await set_leverage( | ||||||
|  |             tg_id=tg_id, | ||||||
|  |             symbol=symbol, | ||||||
|  |             leverage=leverage, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         if trade_mode == "Switch": | ||||||
|  |             if side_mode == "Противоположно": | ||||||
|  |                 s_side = "Sell" if side == "Buy" else "Buy" | ||||||
|  |             else: | ||||||
|  |                 s_side = side | ||||||
|  |         else: | ||||||
|  |             s_side = side | ||||||
|  |  | ||||||
|  |         res = await open_positions( | ||||||
|  |             tg_id=tg_id, | ||||||
|  |             symbol=symbol, | ||||||
|  |             side=s_side, | ||||||
|  |             order_quantity=base_quantity, | ||||||
|  |             trigger_price=trigger_price, | ||||||
|  |             margin_type=margin_type, | ||||||
|  |             leverage=leverage, | ||||||
|  |             take_profit_percent=take_profit_percent, | ||||||
|  |             stop_loss_percent=stop_loss_percent, | ||||||
|  |             commission_fee_percent=total_fee | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         if res == "OK": | ||||||
|  |             await rq.set_user_deal( | ||||||
|  |                 tg_id=tg_id, | ||||||
|  |                 symbol=symbol, | ||||||
|  |                 current_step=1, | ||||||
|  |                 trade_mode=trade_mode, | ||||||
|  |                 side_mode=side_mode, | ||||||
|  |                 margin_type=margin_type, | ||||||
|  |                 leverage=leverage, | ||||||
|  |                 order_quantity=base_quantity, | ||||||
|  |                 trigger_price=trigger_price, | ||||||
|  |                 martingale_factor=martingale_factor, | ||||||
|  |                 max_bets_in_series=max_bets_in_series, | ||||||
|  |                 take_profit_percent=take_profit_percent, | ||||||
|  |                 stop_loss_percent=stop_loss_percent, | ||||||
|  |                 base_quantity=base_quantity | ||||||
|  |             ) | ||||||
|  |             return "OK" | ||||||
|  |  | ||||||
|  |         return ( | ||||||
|  |             res | ||||||
|  |             if res | ||||||
|  |                in { | ||||||
|  |                    "Risk is too high for this trade", | ||||||
|  |                    "ab not enough for new order", | ||||||
|  |                    "InvalidRequestError", | ||||||
|  |                    "The number of contracts exceeds maximum limit allowed", | ||||||
|  |                } | ||||||
|  |             else None | ||||||
|  |         ) | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error("Error in trading_cycle_profit: %s", e) | ||||||
|  |         return None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def trading_cycle( | ||||||
|  |         tg_id: int, symbol: str, side: str, | ||||||
|  | ) -> str | None: | ||||||
|  |     try: | ||||||
|  |         user_deals_data = await rq.get_user_deal_by_symbol(tg_id=tg_id, symbol=symbol) | ||||||
|  |         user_auto_trading_data = await rq.get_user_auto_trading(tg_id=tg_id, symbol=symbol) | ||||||
|  |         user_risk_management_data = await rq.get_user_risk_management(tg_id=tg_id) | ||||||
|  |         commission_fee = user_risk_management_data.commission_fee | ||||||
|  |         total_fee = user_auto_trading_data.total_fee | ||||||
|  |         trade_mode = user_deals_data.trade_mode | ||||||
|  |         margin_type = user_deals_data.margin_type | ||||||
|  |         leverage = user_deals_data.leverage | ||||||
|  |         trigger_price = 0 | ||||||
|  |         take_profit_percent = user_deals_data.take_profit_percent | ||||||
|  |         stop_loss_percent = user_deals_data.stop_loss_percent | ||||||
|  |         max_bets_in_series = user_deals_data.max_bets_in_series | ||||||
|  |         martingale_factor = user_deals_data.martingale_factor | ||||||
|  |         current_step = user_deals_data.current_step | ||||||
|  |         order_quantity = user_deals_data.order_quantity | ||||||
|  |         base_quantity = user_deals_data.base_quantity | ||||||
|  |         side_mode = user_deals_data.side_mode | ||||||
|  |  | ||||||
|  |         next_quantity = safe_float(order_quantity) * ( | ||||||
|  |             safe_float(martingale_factor) | ||||||
|  |         ) | ||||||
|  |         current_step += 1 | ||||||
|  |  | ||||||
|  |         if max_bets_in_series < current_step: | ||||||
|  |             return "Max bets in series" | ||||||
|  |  | ||||||
|  |         await set_margin_mode(tg_id=tg_id, margin_mode=margin_type) | ||||||
|  |         await set_leverage( | ||||||
|  |             tg_id=tg_id, | ||||||
|  |             symbol=symbol, | ||||||
|  |             leverage=leverage, | ||||||
|  |         ) | ||||||
|  |         if commission_fee == "Yes_commission_fee": | ||||||
|  |             total_fee = total_fee | ||||||
|  |         else: | ||||||
|  |             total_fee = 0 | ||||||
|  |  | ||||||
|  |         if trade_mode == "Switch": | ||||||
|  |             if side == "Buy": | ||||||
|  |                 r_side = "Sell" | ||||||
|  |             else: | ||||||
|  |                 r_side = "Buy" | ||||||
|  |         else: | ||||||
|  |             r_side = side | ||||||
|  |  | ||||||
|  |         res = await open_positions( | ||||||
|  |             tg_id=tg_id, | ||||||
|  |             symbol=symbol, | ||||||
|  |             side=r_side, | ||||||
|  |             order_quantity=next_quantity, | ||||||
|  |             trigger_price=trigger_price, | ||||||
|  |             margin_type=margin_type, | ||||||
|  |             leverage=leverage, | ||||||
|  |             take_profit_percent=take_profit_percent, | ||||||
|  |             stop_loss_percent=stop_loss_percent, | ||||||
|  |             commission_fee_percent=total_fee | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         if res == "OK": | ||||||
|  |             await rq.set_user_deal( | ||||||
|  |                 tg_id=tg_id, | ||||||
|  |                 symbol=symbol, | ||||||
|  |                 current_step=current_step, | ||||||
|  |                 trade_mode=trade_mode, | ||||||
|  |                 side_mode=side_mode, | ||||||
|  |                 margin_type=margin_type, | ||||||
|  |                 leverage=leverage, | ||||||
|  |                 order_quantity=next_quantity, | ||||||
|  |                 trigger_price=trigger_price, | ||||||
|  |                 martingale_factor=martingale_factor, | ||||||
|  |                 max_bets_in_series=max_bets_in_series, | ||||||
|  |                 take_profit_percent=take_profit_percent, | ||||||
|  |                 stop_loss_percent=stop_loss_percent, | ||||||
|  |                 base_quantity=base_quantity | ||||||
|  |             ) | ||||||
|  |             return "OK" | ||||||
|  |  | ||||||
|  |         return ( | ||||||
|  |             res | ||||||
|  |             if res | ||||||
|  |                in { | ||||||
|  |                    "Risk is too high for this trade", | ||||||
|  |                    "ab not enough for new order", | ||||||
|  |                    "InvalidRequestError", | ||||||
|  |                    "The number of contracts exceeds maximum limit allowed", | ||||||
|  |                } | ||||||
|  |             else None | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error("Error in trading_cycle: %s", e) | ||||||
|  |         return None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def open_positions( | ||||||
|  |         tg_id: int, | ||||||
|  |         side: str, | ||||||
|  |         symbol: str, | ||||||
|  |         order_quantity: float, | ||||||
|  |         trigger_price: float, | ||||||
|  |         margin_type: str, | ||||||
|  |         leverage: str, | ||||||
|  |         take_profit_percent: float, | ||||||
|  |         stop_loss_percent: float, | ||||||
|  |         commission_fee_percent: float | ||||||
|  | ) -> str | None: | ||||||
|  |     try: | ||||||
|  |         client = await get_bybit_client(tg_id=tg_id) | ||||||
|  |         get_ticker = await get_tickers(tg_id, symbol=symbol) | ||||||
|  |         price_symbol = safe_float(get_ticker.get("lastPrice")) or 0 | ||||||
|  |         instruments_info = await get_instruments_info(tg_id=tg_id, symbol=symbol) | ||||||
|  |         qty_step_str = instruments_info.get("lotSizeFilter").get("qtyStep") | ||||||
|  |         qty_step = safe_float(qty_step_str) | ||||||
|  |         qty = (safe_float(order_quantity) * safe_float(leverage)) / safe_float(price_symbol) | ||||||
|  |         decimals = abs(int(round(math.log10(qty_step)))) | ||||||
|  |         qty_formatted = math.floor(qty / qty_step) * qty_step | ||||||
|  |         qty_formatted = round(qty_formatted, decimals) | ||||||
|  |  | ||||||
|  |         if trigger_price > 0: | ||||||
|  |             po_trigger_price = str(trigger_price) | ||||||
|  |             trigger_direction = 1 if trigger_price > price_symbol else 2 | ||||||
|  |         else: | ||||||
|  |             po_trigger_price = None | ||||||
|  |             trigger_direction = None | ||||||
|  |  | ||||||
|  |         price_for_cals = trigger_price if po_trigger_price is not None else price_symbol | ||||||
|  |  | ||||||
|  |         if qty_formatted <= 0: | ||||||
|  |             return "Order does not meet minimum order value" | ||||||
|  |  | ||||||
|  |         if margin_type == "ISOLATED_MARGIN": | ||||||
|  |             if side == "Buy": | ||||||
|  |                 take_profit_price = price_for_cals * ( | ||||||
|  |                         1 + take_profit_percent / 100) + commission_fee_percent / qty_formatted | ||||||
|  |                 stop_loss_price = None | ||||||
|  |             else: | ||||||
|  |                 take_profit_price = price_for_cals * ( | ||||||
|  |                         1 - take_profit_percent / 100) - commission_fee_percent / qty_formatted | ||||||
|  |                 stop_loss_price = None | ||||||
|  |         else: | ||||||
|  |             if side == "Buy": | ||||||
|  |                 take_profit_price = price_for_cals * ( | ||||||
|  |                             1 + take_profit_percent / 100) + commission_fee_percent / qty_formatted | ||||||
|  |                 stop_loss_price = price_for_cals * (1 - stop_loss_percent / 100) | ||||||
|  |             else: | ||||||
|  |                 take_profit_price = price_for_cals * ( | ||||||
|  |                             1 - take_profit_percent / 100) - commission_fee_percent / qty_formatted | ||||||
|  |                 stop_loss_price = price_for_cals * (1 + stop_loss_percent / 100) | ||||||
|  |  | ||||||
|  |             take_profit_price = max(take_profit_price, 0) | ||||||
|  |             stop_loss_price = max(stop_loss_price, 0) | ||||||
|  |  | ||||||
|  |         # Place order | ||||||
|  |         order_params = { | ||||||
|  |             "category": "linear", | ||||||
|  |             "symbol": symbol, | ||||||
|  |             "side": side, | ||||||
|  |             "orderType": "Market", | ||||||
|  |             "qty": str(qty_formatted), | ||||||
|  |             "triggerDirection": trigger_direction, | ||||||
|  |             "triggerPrice": po_trigger_price, | ||||||
|  |             "triggerBy": "LastPrice", | ||||||
|  |             "timeInForce": "GTC", | ||||||
|  |             "positionIdx": 0, | ||||||
|  |             "tpslMode": "Full", | ||||||
|  |             "takeProfit": str(take_profit_price) if take_profit_price else None, | ||||||
|  |             "stopLoss": str(stop_loss_price) if stop_loss_price else None, | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         response = client.place_order(**order_params) | ||||||
|  |  | ||||||
|  |         if response["retCode"] == 0: | ||||||
|  |             logger.info("Position opened for user: %s", tg_id) | ||||||
|  |             return "OK" | ||||||
|  |  | ||||||
|  |         logger.error("Error opening position for user: %s", tg_id) | ||||||
|  |         return None | ||||||
|  |  | ||||||
|  |     except InvalidRequestError as e: | ||||||
|  |         error_text = str(e) | ||||||
|  |         known_errors = { | ||||||
|  |             "Order does not meet minimum order value": "Order does not meet minimum order value", | ||||||
|  |             "estimated will trigger liq": "estimated will trigger liq", | ||||||
|  |             "ab not enough for new order": "ab not enough for new order", | ||||||
|  |             "position idx not match position mode": "position idx not match position mode", | ||||||
|  |             "Qty invalid": "Qty invalid", | ||||||
|  |             "The number of contracts exceeds maximum limit allowed": "The number of contracts exceeds maximum limit allowed", | ||||||
|  |             "The number of contracts exceeds minimum limit allowed": "The number of contracts exceeds minimum limit allowed", | ||||||
|  |         } | ||||||
|  |         for key, msg in known_errors.items(): | ||||||
|  |             if key in error_text: | ||||||
|  |                 logger.error(msg) | ||||||
|  |                 return msg | ||||||
|  |         logger.error("InvalidRequestError: %s", e) | ||||||
|  |         return "InvalidRequestError" | ||||||
|  |  | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error("Error opening position for user %s: %s", tg_id, e, exc_info=True) | ||||||
|  |         return None | ||||||
							
								
								
									
										45
									
								
								app/bybit/profile_bybit.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								app/bybit/profile_bybit.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | import logging.config | ||||||
|  |  | ||||||
|  | from aiogram.fsm.context import FSMContext | ||||||
|  | from aiogram.types import Message | ||||||
|  |  | ||||||
|  | import app.telegram.keyboards.inline as kbi | ||||||
|  | import database.request as rq | ||||||
|  | from app.bybit.get_functions.get_balance import get_balance | ||||||
|  | from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG | ||||||
|  |  | ||||||
|  | logging.config.dictConfig(LOGGING_CONFIG) | ||||||
|  | logger = logging.getLogger("profile_bybit") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def user_profile_bybit(tg_id: int, message: Message, state: FSMContext) -> None: | ||||||
|  |     """Get user profile bybit""" | ||||||
|  |     try: | ||||||
|  |         await state.clear() | ||||||
|  |         wallet = await get_balance(tg_id=tg_id) | ||||||
|  |  | ||||||
|  |         if wallet: | ||||||
|  |             balance = wallet.get("totalWalletBalance", "0") | ||||||
|  |             symbol = await rq.get_user_symbol(tg_id=tg_id) | ||||||
|  |             if symbol is None: | ||||||
|  |                 await rq.set_user_symbol(tg_id=tg_id, symbol="BTCUSDT") | ||||||
|  |                 await user_profile_bybit(tg_id=tg_id, message=message, state=state) | ||||||
|  |             else: | ||||||
|  |                 await message.answer( | ||||||
|  |                     text=f"💎Ваш профиль:\n\n" | ||||||
|  |                          f"⚖️ Баланс: {float(balance):,.2f} USD\n" | ||||||
|  |                          f"📊Торговая пара: {symbol}\n\n" | ||||||
|  |                          f"Краткая инструкция:\n" | ||||||
|  |                          f"1. Укажите торговую пару (например: BTCUSDT).\n" | ||||||
|  |                          f"2. В настройках выставьте все необходимые параметры.\n" | ||||||
|  |                          f"3. Нажмите кнопку 'Начать торговлю'.\n", | ||||||
|  |                     reply_markup=kbi.main_menu, | ||||||
|  |                 ) | ||||||
|  |         else: | ||||||
|  |             await message.answer( | ||||||
|  |                 text="Ошибка при подключении, повторите попытку", | ||||||
|  |                 reply_markup=kbi.connect_the_platform, | ||||||
|  |             ) | ||||||
|  |             logger.error("Error processing user profile for user %s", tg_id) | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error("Error processing user profile for user %s: %s", tg_id, e) | ||||||
							
								
								
									
										0
									
								
								app/bybit/set_functions/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/bybit/set_functions/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										96
									
								
								app/bybit/set_functions/set_leverage.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								app/bybit/set_functions/set_leverage.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | |||||||
|  | import logging.config | ||||||
|  |  | ||||||
|  | from pybit import exceptions | ||||||
|  |  | ||||||
|  | from app.bybit import get_bybit_client | ||||||
|  | from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG | ||||||
|  |  | ||||||
|  | logging.config.dictConfig(LOGGING_CONFIG) | ||||||
|  | logger = logging.getLogger("set_leverage") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def set_leverage(tg_id: int, symbol: str, leverage: str) -> bool: | ||||||
|  |     """ | ||||||
|  |     Set leverage | ||||||
|  |     :param tg_id: int - User ID | ||||||
|  |     :param symbol: str - Symbol | ||||||
|  |     :param leverage: str - Leverage | ||||||
|  |     :return: bool | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         client = await get_bybit_client(tg_id=tg_id) | ||||||
|  |         response = client.set_leverage( | ||||||
|  |             category="linear", | ||||||
|  |             symbol=symbol, | ||||||
|  |             buyLeverage=str(leverage), | ||||||
|  |             sellLeverage=str(leverage), | ||||||
|  |         ) | ||||||
|  |         if response["retCode"] == 0: | ||||||
|  |             logger.info( | ||||||
|  |                 "Leverage set to %s for user: %s", | ||||||
|  |                 leverage, | ||||||
|  |                 tg_id, | ||||||
|  |             ) | ||||||
|  |             return True | ||||||
|  |         else: | ||||||
|  |             logger.error("Error setting leverage: %s", response["retMsg"]) | ||||||
|  |             return False | ||||||
|  |     except exceptions.InvalidRequestError as e: | ||||||
|  |         if "110043" in str(e): | ||||||
|  |             logger.debug( | ||||||
|  |                 "Leverage set to %s for user: %s", | ||||||
|  |                 leverage, | ||||||
|  |                 tg_id, | ||||||
|  |             ) | ||||||
|  |             return True | ||||||
|  |         else: | ||||||
|  |             raise | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error("Error connecting to Bybit for user %s: %s", tg_id, e) | ||||||
|  |         return False | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def set_leverage_to_buy_and_sell( | ||||||
|  |     tg_id: int, symbol: str, leverage_to_buy: str, leverage_to_sell: str | ||||||
|  | ) -> bool: | ||||||
|  |     """ | ||||||
|  |     Set leverage to buy and sell | ||||||
|  |     :param tg_id: int - User ID | ||||||
|  |     :param symbol: str - Symbol | ||||||
|  |     :param leverage_to_buy: str - Leverage to buy | ||||||
|  |     :param leverage_to_sell: str - Leverage to sell | ||||||
|  |     :return: bool | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         client = await get_bybit_client(tg_id=tg_id) | ||||||
|  |         response = client.set_leverage( | ||||||
|  |             category="linear", | ||||||
|  |             symbol=symbol, | ||||||
|  |             buyLeverage=str(leverage_to_buy), | ||||||
|  |             sellLeverage=str(leverage_to_sell), | ||||||
|  |         ) | ||||||
|  |         if response["retCode"] == 0: | ||||||
|  |             logger.info( | ||||||
|  |                 "Leverage set to %s and %s for user: %s", | ||||||
|  |                 leverage_to_buy, | ||||||
|  |                 leverage_to_sell, | ||||||
|  |                 tg_id, | ||||||
|  |             ) | ||||||
|  |             return True | ||||||
|  |         else: | ||||||
|  |             logger.error("Error setting leverage for buy and sell for user: %s", tg_id) | ||||||
|  |             return False | ||||||
|  |     except exceptions.InvalidRequestError as e: | ||||||
|  |         if "110043" in str(e): | ||||||
|  |             logger.debug( | ||||||
|  |                 "Leverage set to %s and %s for user: %s", | ||||||
|  |                 leverage_to_buy, | ||||||
|  |                 leverage_to_sell, | ||||||
|  |                 tg_id, | ||||||
|  |             ) | ||||||
|  |             return True | ||||||
|  |         else: | ||||||
|  |             raise | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error("Error connecting to Bybit for user %s: %s", tg_id, e) | ||||||
|  |         return False | ||||||
							
								
								
									
										28
									
								
								app/bybit/set_functions/set_margin_mode.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								app/bybit/set_functions/set_margin_mode.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | import logging.config | ||||||
|  |  | ||||||
|  | from app.bybit import get_bybit_client | ||||||
|  | from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG | ||||||
|  |  | ||||||
|  | logging.config.dictConfig(LOGGING_CONFIG) | ||||||
|  | logger = logging.getLogger("set_margin_mode") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def set_margin_mode(tg_id: int, margin_mode: str) -> bool: | ||||||
|  |     """ | ||||||
|  |     Set margin mode | ||||||
|  |     :param tg_id: int - User ID | ||||||
|  |     :param margin_mode: str - Margin mode | ||||||
|  |     :return: bool | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         client = await get_bybit_client(tg_id=tg_id) | ||||||
|  |         response = client.set_margin_mode(setMarginMode=margin_mode) | ||||||
|  |         if response["retCode"] == 0: | ||||||
|  |             logger.info("Margin mode set to %s for user: %s", margin_mode, tg_id) | ||||||
|  |             return True | ||||||
|  |         else: | ||||||
|  |             logger.error("Error setting margin mode: %s", tg_id) | ||||||
|  |             return False | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error("Error connecting to Bybit for user %s: %s", tg_id, e) | ||||||
|  |         return False | ||||||
							
								
								
									
										54
									
								
								app/bybit/set_functions/set_switch_position_mode.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								app/bybit/set_functions/set_switch_position_mode.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | |||||||
|  | import logging.config | ||||||
|  |  | ||||||
|  | from app.bybit import get_bybit_client | ||||||
|  | from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG | ||||||
|  |  | ||||||
|  | logging.config.dictConfig(LOGGING_CONFIG) | ||||||
|  | logger = logging.getLogger("set_switch_position_mode") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def set_switch_position_mode(tg_id: int, symbol: str, mode: int) -> str | bool: | ||||||
|  |     """ | ||||||
|  |     Set switch position mode | ||||||
|  |     :param tg_id: int - User ID | ||||||
|  |     :param symbol: str - Symbol | ||||||
|  |     :param mode: int - Mode | ||||||
|  |     :return: bool | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         client = await get_bybit_client(tg_id=tg_id) | ||||||
|  |         response = client.switch_position_mode( | ||||||
|  |             category="linear", | ||||||
|  |             symbol=symbol, | ||||||
|  |             mode=mode, | ||||||
|  |         ) | ||||||
|  |         if response["retCode"] == 0: | ||||||
|  |             logger.info("Switch position mode set successfully") | ||||||
|  |             return True | ||||||
|  |         else: | ||||||
|  |             logger.error("Error setting switch position mode for user: %s", tg_id) | ||||||
|  |             return False | ||||||
|  |     except Exception as e: | ||||||
|  |         if str(e).startswith("Position mode is not modified"): | ||||||
|  |             logger.debug( | ||||||
|  |                 "Position mode is not modified for user: %s", | ||||||
|  |                 tg_id, | ||||||
|  |             ) | ||||||
|  |             return True | ||||||
|  |         if str(e).startswith( | ||||||
|  |             "You have an existing position, so position mode cannot be switched" | ||||||
|  |         ): | ||||||
|  |             logger.debug( | ||||||
|  |                 "You have an existing position, so position mode cannot be switched for user: %s", | ||||||
|  |                 tg_id, | ||||||
|  |             ) | ||||||
|  |             return "You have an existing position, so position mode cannot be switched" | ||||||
|  |         if str(e).startswith("Open orders exist, so you cannot change position mode"): | ||||||
|  |             logger.debug( | ||||||
|  |                 "Open orders exist, so you cannot change position mode for user: %s", | ||||||
|  |                 tg_id, | ||||||
|  |             ) | ||||||
|  |             return "Open orders exist, so you cannot change position mode" | ||||||
|  |         else: | ||||||
|  |             logger.error("Error connecting to Bybit for user %s: %s", tg_id, e) | ||||||
|  |             return False | ||||||
							
								
								
									
										45
									
								
								app/bybit/set_functions/set_tp_sl.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								app/bybit/set_functions/set_tp_sl.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | import logging.config | ||||||
|  |  | ||||||
|  | from app.bybit import get_bybit_client | ||||||
|  | from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG | ||||||
|  |  | ||||||
|  | logging.config.dictConfig(LOGGING_CONFIG) | ||||||
|  | logger = logging.getLogger("set_tp_sl") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def set_tp_sl_for_position( | ||||||
|  |     tg_id: int, | ||||||
|  |     symbol: str, | ||||||
|  |     take_profit_price: float, | ||||||
|  |     stop_loss_price: float, | ||||||
|  |     position_idx: int, | ||||||
|  | ) -> bool: | ||||||
|  |     """ | ||||||
|  |     Set take profit and stop loss for a symbol. | ||||||
|  |     :param tg_id: Telegram user ID | ||||||
|  |     :param symbol: Symbol to set take profit and stop loss for | ||||||
|  |     :param take_profit_price: Take profit price | ||||||
|  |     :param stop_loss_price: Stop loss price | ||||||
|  |     :param position_idx: Position index | ||||||
|  |     :return: bool | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         client = await get_bybit_client(tg_id) | ||||||
|  |         resp = client.set_trading_stop( | ||||||
|  |             category="linear", | ||||||
|  |             symbol=symbol, | ||||||
|  |             takeProfit=str(round(take_profit_price, 5)), | ||||||
|  |             stopLoss=str(round(stop_loss_price, 5)), | ||||||
|  |             positionIdx=position_idx, | ||||||
|  |             tpslMode="Full", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         if resp.get("retCode") == 0: | ||||||
|  |             logger.info("TP/SL for %s has been set", symbol) | ||||||
|  |             return True | ||||||
|  |         else: | ||||||
|  |             logger.error("Error setting TP/SL for %s: %s", symbol, resp.get("retMsg")) | ||||||
|  |             return False | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error("Error setting TP/SL for %s: %s", symbol, e) | ||||||
|  |         return False | ||||||
							
								
								
									
										256
									
								
								app/bybit/telegram_message_handler.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										256
									
								
								app/bybit/telegram_message_handler.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,256 @@ | |||||||
|  | import logging.config | ||||||
|  |  | ||||||
|  | import app.telegram.keyboards.inline as kbi | ||||||
|  | import database.request as rq | ||||||
|  | from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG | ||||||
|  | from app.bybit.open_positions import trading_cycle, trading_cycle_profit | ||||||
|  | from app.helper_functions import format_value, safe_float | ||||||
|  |  | ||||||
|  | logging.config.dictConfig(LOGGING_CONFIG) | ||||||
|  | logger = logging.getLogger("telegram_message_handler") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TelegramMessageHandler: | ||||||
|  |     def __init__(self, telegram_bot): | ||||||
|  |         self.telegram_bot = telegram_bot | ||||||
|  |  | ||||||
|  |     async def format_position_update(self, message): | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |     async def format_order_update(self, message, tg_id): | ||||||
|  |         try: | ||||||
|  |             order_data = message.get("data", [{}])[0] | ||||||
|  |             symbol = format_value(order_data.get("symbol")) | ||||||
|  |             qty = format_value(order_data.get("qty")) | ||||||
|  |             side = format_value(order_data.get("side")) | ||||||
|  |             side_rus = ( | ||||||
|  |                 "Покупка" | ||||||
|  |                 if side == "Buy" | ||||||
|  |                 else "Продажа" if side == "Sell" else "Нет данных" | ||||||
|  |             ) | ||||||
|  |             order_status = format_value(order_data.get("orderStatus")) | ||||||
|  |             price = format_value(order_data.get("price")) | ||||||
|  |             trigger_price = format_value(order_data.get("triggerPrice")) | ||||||
|  |             take_profit = format_value(order_data.get("takeProfit")) | ||||||
|  |             stop_loss = format_value(order_data.get("stopLoss")) | ||||||
|  |  | ||||||
|  |             status_map = { | ||||||
|  |                 "Untriggered": "Условный ордер выставлен", | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if order_status == "Filled" or order_status not in status_map: | ||||||
|  |                 return None | ||||||
|  |  | ||||||
|  |             user_auto_trading = await rq.get_user_auto_trading( | ||||||
|  |                 tg_id=tg_id, symbol=symbol | ||||||
|  |             ) | ||||||
|  |             auto_trading = ( | ||||||
|  |                 user_auto_trading.auto_trading if user_auto_trading else False | ||||||
|  |             ) | ||||||
|  |             user_deals_data = await rq.get_user_deal_by_symbol( | ||||||
|  |                 tg_id=tg_id, symbol=symbol | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             text = ( | ||||||
|  |                 f"Торговая пара: {symbol}\n" | ||||||
|  |                 f"Движение: {side_rus}\n" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             if user_deals_data is not None and auto_trading: | ||||||
|  |                 text += f"Текущая ставка: {user_deals_data.order_quantity} USDT\n" | ||||||
|  |             else: | ||||||
|  |                 text += f"Количество: {qty}\n" | ||||||
|  |  | ||||||
|  |             if price and price != "0": | ||||||
|  |                 text += f"Цена: {price}\n" | ||||||
|  |             if take_profit and take_profit != "Нет данных": | ||||||
|  |                 text += f"Тейк-профит: {take_profit}\n" | ||||||
|  |             if stop_loss and stop_loss != "Нет данных": | ||||||
|  |                 text += f"Стоп-лосс: {stop_loss}\n" | ||||||
|  |             if trigger_price and trigger_price != "Нет данных": | ||||||
|  |                 text += f"Триггер цена: {trigger_price}\n" | ||||||
|  |  | ||||||
|  |             await self.telegram_bot.send_message( | ||||||
|  |                 chat_id=tg_id, text=text, reply_markup=kbi.profile_bybit | ||||||
|  |             ) | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error("Error in format_order_update: %s", e) | ||||||
|  |  | ||||||
|  |     async def format_execution_update(self, message, tg_id): | ||||||
|  |         try: | ||||||
|  |             execution = message.get("data", [{}])[0] | ||||||
|  |             closed_size = format_value(execution.get("closedSize")) | ||||||
|  |             symbol = format_value(execution.get("symbol")) | ||||||
|  |             exec_price = format_value(execution.get("execPrice")) | ||||||
|  |             exec_qty = format_value(execution.get("execQty")) | ||||||
|  |             exec_fees = format_value(execution.get("execFee")) | ||||||
|  |             fee_rate = format_value(execution.get("feeRate")) | ||||||
|  |             side = format_value(execution.get("side")) | ||||||
|  |             side_rus = ( | ||||||
|  |                 "Покупка" | ||||||
|  |                 if side == "Buy" | ||||||
|  |                 else "Продажа" if side == "Sell" else "Нет данных" | ||||||
|  |             ) | ||||||
|  |             if safe_float(exec_fees) == 0: | ||||||
|  |                 exec_fee = safe_float(exec_price) * safe_float(exec_qty) * safe_float( | ||||||
|  |                     fee_rate | ||||||
|  |                 ) | ||||||
|  |             else: | ||||||
|  |                 exec_fee = safe_float(exec_fees) | ||||||
|  |  | ||||||
|  |             if safe_float(closed_size) == 0: | ||||||
|  |                 await rq.set_fee_user_auto_trading( | ||||||
|  |                     tg_id=tg_id, symbol=symbol, fee=safe_float(exec_fee) | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|  |             user_auto_trading = await rq.get_user_auto_trading( | ||||||
|  |                 tg_id=tg_id, symbol=symbol | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             get_total_fee = user_auto_trading.total_fee | ||||||
|  |             total_fee = safe_float(exec_fee) + safe_float(get_total_fee) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |             if user_auto_trading is not None and user_auto_trading.fee is not None: | ||||||
|  |                 fee = user_auto_trading.fee | ||||||
|  |             else: | ||||||
|  |                 fee = 0 | ||||||
|  |  | ||||||
|  |             exec_pnl = format_value(execution.get("execPnl")) | ||||||
|  |             total_pnl = safe_float(exec_pnl) - safe_float(exec_fee) - fee | ||||||
|  |             header = ( | ||||||
|  |                 "Сделка закрыта:" if safe_float(closed_size) > 0 else "Сделка открыта:" | ||||||
|  |             ) | ||||||
|  |             text = f"{header}\n" f"Торговая пара: {symbol}\n" | ||||||
|  |  | ||||||
|  |             auto_trading = ( | ||||||
|  |                 user_auto_trading.auto_trading if user_auto_trading else False | ||||||
|  |             ) | ||||||
|  |             user_deals_data = await rq.get_user_deal_by_symbol( | ||||||
|  |                 tg_id=tg_id, symbol=symbol | ||||||
|  |             ) | ||||||
|  |             if user_deals_data is not None and auto_trading: | ||||||
|  |                 await rq.set_total_fee_user_auto_trading( | ||||||
|  |                     tg_id=tg_id, symbol=symbol, total_fee=total_fee | ||||||
|  |                 ) | ||||||
|  |                 text += f"Текущая ставка: {user_deals_data.order_quantity} USDT\n" | ||||||
|  |  | ||||||
|  |             text += ( | ||||||
|  |                 f"Цена исполнения: {exec_price}\n" | ||||||
|  |                 f"Комиссия: {exec_fee:.8f}\n" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             if safe_float(closed_size) == 0: | ||||||
|  |                 text += f"Движение: {side_rus}\n" | ||||||
|  |             else: | ||||||
|  |                 text += f"\nРеализованная прибыль: {total_pnl:.7f}\n" | ||||||
|  |  | ||||||
|  |             await self.telegram_bot.send_message( | ||||||
|  |                 chat_id=tg_id, text=text, reply_markup=kbi.profile_bybit | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             user_symbols = user_auto_trading.symbol if user_auto_trading else None | ||||||
|  |  | ||||||
|  |             if ( | ||||||
|  |                 auto_trading | ||||||
|  |                 and safe_float(closed_size) > 0 | ||||||
|  |                 and user_symbols is not None | ||||||
|  |             ): | ||||||
|  |                 if safe_float(total_pnl) > 0: | ||||||
|  |                     profit_text = "📈 Прибыль достигнута. Начинаем новую серию с базовой ставки\n" | ||||||
|  |                     await self.telegram_bot.send_message( | ||||||
|  |                         chat_id=tg_id, text=profit_text, reply_markup=kbi.profile_bybit | ||||||
|  |                     ) | ||||||
|  |  | ||||||
|  |                     if side == "Buy": | ||||||
|  |                         r_side = "Sell" | ||||||
|  |                     else: | ||||||
|  |                         r_side = "Buy" | ||||||
|  |  | ||||||
|  |                     await rq.set_last_side_by_symbol( | ||||||
|  |                         tg_id=tg_id, symbol=symbol, last_side=r_side) | ||||||
|  |                     await rq.set_total_fee_user_auto_trading( | ||||||
|  |                         tg_id=tg_id, symbol=symbol, total_fee=0 | ||||||
|  |                     ) | ||||||
|  |                     await rq.set_fee_user_auto_trading( | ||||||
|  |                         tg_id=tg_id, symbol=symbol, fee=0 | ||||||
|  |                     ) | ||||||
|  |  | ||||||
|  |                     res = await trading_cycle_profit( | ||||||
|  |                         tg_id=tg_id, symbol=symbol, side=r_side | ||||||
|  |                     ) | ||||||
|  |  | ||||||
|  |                     if res == "OK": | ||||||
|  |                         pass | ||||||
|  |                     else: | ||||||
|  |                         errors = { | ||||||
|  |                             "Max bets in series": "❗️ Максимальное количество сделок в серии достигнуто", | ||||||
|  |                             "Risk is too high for this trade": "❗️ Риск сделки слишком высок для продолжения", | ||||||
|  |                             "ab not enough for new order": "❗️ Недостаточно средств для продолжения торговли", | ||||||
|  |                             "InvalidRequestError": "❗️ Недостаточно средств для размещения нового ордера с заданным количеством и плечом.", | ||||||
|  |                             "The number of contracts exceeds maximum limit allowed": "❗️ Превышен максимальный лимит ставки", | ||||||
|  |                         } | ||||||
|  |                         error_text = errors.get( | ||||||
|  |                             res, "❗️ Не удалось открыть новую сделку" | ||||||
|  |                         ) | ||||||
|  |                         await rq.set_auto_trading( | ||||||
|  |                             tg_id=tg_id, symbol=symbol, auto_trading=False | ||||||
|  |                         ) | ||||||
|  |  | ||||||
|  |                         await rq.set_total_fee_user_auto_trading( | ||||||
|  |                             tg_id=tg_id, symbol=symbol, total_fee=0 | ||||||
|  |                         ) | ||||||
|  |                         await rq.set_fee_user_auto_trading( | ||||||
|  |                             tg_id=tg_id, symbol=symbol, fee=0 | ||||||
|  |                         ) | ||||||
|  |                         await self.telegram_bot.send_message( | ||||||
|  |                             chat_id=tg_id, | ||||||
|  |                             text=error_text, | ||||||
|  |                             reply_markup=kbi.profile_bybit, | ||||||
|  |                         ) | ||||||
|  |                 else: | ||||||
|  |                     open_order_text = "\n❗️ Сделка закрылась в минус, открываю новую сделку с увеличенной ставкой.\n" | ||||||
|  |                     await self.telegram_bot.send_message( | ||||||
|  |                         chat_id=tg_id, text=open_order_text | ||||||
|  |                     ) | ||||||
|  |  | ||||||
|  |                     if side == "Buy": | ||||||
|  |                         r_side = "Sell" | ||||||
|  |                     else: | ||||||
|  |                         r_side = "Buy" | ||||||
|  |  | ||||||
|  |                     res = await trading_cycle( | ||||||
|  |                         tg_id=tg_id, symbol=symbol, side=r_side | ||||||
|  |                     ) | ||||||
|  |  | ||||||
|  |                     if res == "OK": | ||||||
|  |                         pass | ||||||
|  |                     else: | ||||||
|  |                         errors = { | ||||||
|  |                             "Max bets in series": "❗️ Максимальное количество сделок в серии достигнуто", | ||||||
|  |                             "Risk is too high for this trade": "❗️ Риск сделки слишком высок для продолжения", | ||||||
|  |                             "ab not enough for new order": "❗️ Недостаточно средств для продолжения торговли", | ||||||
|  |                             "InvalidRequestError": "❗️ Недостаточно средств для размещения нового ордера с заданным количеством и плечом.", | ||||||
|  |                             "The number of contracts exceeds maximum limit allowed": "❗️ Превышен максимальный лимит ставки", | ||||||
|  |                         } | ||||||
|  |                         error_text = errors.get( | ||||||
|  |                             res, "❗️ Не удалось открыть новую сделку" | ||||||
|  |                         ) | ||||||
|  |                         await rq.set_auto_trading( | ||||||
|  |                             tg_id=tg_id, symbol=symbol, auto_trading=False | ||||||
|  |                         ) | ||||||
|  |  | ||||||
|  |                         await rq.set_total_fee_user_auto_trading( | ||||||
|  |                             tg_id=tg_id, symbol=symbol, total_fee=0 | ||||||
|  |                         ) | ||||||
|  |                         await rq.set_fee_user_auto_trading( | ||||||
|  |                             tg_id=tg_id, symbol=symbol, fee=0 | ||||||
|  |                         ) | ||||||
|  |                         await self.telegram_bot.send_message( | ||||||
|  |                             chat_id=tg_id, | ||||||
|  |                             text=error_text, | ||||||
|  |                             reply_markup=kbi.profile_bybit, | ||||||
|  |                         ) | ||||||
|  |  | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error("Error in telegram_message_handler: %s", e) | ||||||
							
								
								
									
										122
									
								
								app/bybit/web_socket.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								app/bybit/web_socket.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | |||||||
|  | import asyncio | ||||||
|  | import logging.config | ||||||
|  |  | ||||||
|  | from pybit.unified_trading import WebSocket | ||||||
|  |  | ||||||
|  | import database.request as rq | ||||||
|  | from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG | ||||||
|  | from app.bybit.telegram_message_handler import TelegramMessageHandler | ||||||
|  |  | ||||||
|  | logging.config.dictConfig(LOGGING_CONFIG) | ||||||
|  | logger = logging.getLogger("web_socket") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class WebSocketBot: | ||||||
|  |     """ | ||||||
|  |     Class to handle WebSocket connections and messages. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__(self, telegram_bot): | ||||||
|  |         """Initialize the TradingBot class.""" | ||||||
|  |         self.telegram_bot = telegram_bot | ||||||
|  |         self.ws_private = None | ||||||
|  |         self.user_messages = {} | ||||||
|  |         self.user_sockets = {} | ||||||
|  |         self.user_keys = {} | ||||||
|  |         self.loop = None | ||||||
|  |         self.message_handler = TelegramMessageHandler(telegram_bot) | ||||||
|  |  | ||||||
|  |     async def run_user_check_loop(self): | ||||||
|  |         """Run a loop to check for users and connect them to the WebSocket.""" | ||||||
|  |         self.loop = asyncio.get_running_loop() | ||||||
|  |         while True: | ||||||
|  |             users = await WebSocketBot.get_users_from_db() | ||||||
|  |             for user in users: | ||||||
|  |                 tg_id = user.tg_id | ||||||
|  |                 api_key, api_secret = await rq.get_user_api(tg_id=tg_id) | ||||||
|  |  | ||||||
|  |                 if not api_key or not api_secret: | ||||||
|  |                     continue | ||||||
|  |  | ||||||
|  |                 keys_stored = self.user_keys.get(tg_id) | ||||||
|  |                 if tg_id in self.user_sockets and keys_stored == (api_key, api_secret): | ||||||
|  |                     continue | ||||||
|  |  | ||||||
|  |                 if tg_id in self.user_sockets: | ||||||
|  |                     self.user_sockets.clear() | ||||||
|  |                     self.user_messages.clear() | ||||||
|  |                     self.user_keys.clear() | ||||||
|  |                     logger.info( | ||||||
|  |                         "Closed old websocket for user %s due to key change", tg_id | ||||||
|  |                     ) | ||||||
|  |  | ||||||
|  |                 success = await self.try_connect_user(api_key, api_secret, tg_id) | ||||||
|  |                 if success: | ||||||
|  |                     self.user_keys[tg_id] = (api_key, api_secret) | ||||||
|  |                     self.user_messages.setdefault( | ||||||
|  |                         tg_id, {"position": None, "order": None, "execution": None} | ||||||
|  |                     ) | ||||||
|  |                     logger.info("User %s connected to WebSocket", tg_id) | ||||||
|  |                 else: | ||||||
|  |                     await asyncio.sleep(30) | ||||||
|  |  | ||||||
|  |             await asyncio.sleep(10) | ||||||
|  |  | ||||||
|  |     async def clear_user_sockets(self): | ||||||
|  |         """Clear the user_sockets and user_messages dictionaries.""" | ||||||
|  |         self.user_sockets.clear() | ||||||
|  |         self.user_messages.clear() | ||||||
|  |         self.user_keys.clear() | ||||||
|  |         logger.info("Cleared user_sockets") | ||||||
|  |  | ||||||
|  |     async def try_connect_user(self, api_key, api_secret, tg_id): | ||||||
|  |         """Try to connect a user to the WebSocket.""" | ||||||
|  |         try: | ||||||
|  |             self.ws_private = WebSocket( | ||||||
|  |                 testnet=False, | ||||||
|  |                 channel_type="private", | ||||||
|  |                 api_key=api_key, | ||||||
|  |                 api_secret=api_secret, | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             self.user_sockets[tg_id] = self.ws_private | ||||||
|  |             # Connect to the WebSocket private channel | ||||||
|  |             # Handle position updates | ||||||
|  |             self.ws_private.position_stream( | ||||||
|  |                 lambda msg: self.loop.call_soon_threadsafe( | ||||||
|  |                     asyncio.create_task, self.handle_position_update(msg) | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |             # Handle order updates | ||||||
|  |             self.ws_private.order_stream( | ||||||
|  |                 lambda msg: self.loop.call_soon_threadsafe( | ||||||
|  |                     asyncio.create_task, self.handle_order_update(msg, tg_id) | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |             # Handle execution updates | ||||||
|  |             self.ws_private.execution_stream( | ||||||
|  |                 lambda msg: self.loop.call_soon_threadsafe( | ||||||
|  |                     asyncio.create_task, self.handle_execution_update(msg, tg_id) | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |             return True | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error("Error connecting user %s: %s", tg_id, e) | ||||||
|  |             return False | ||||||
|  |  | ||||||
|  |     async def handle_position_update(self, message): | ||||||
|  |         """Handle position updates.""" | ||||||
|  |         await self.message_handler.format_position_update(message) | ||||||
|  |  | ||||||
|  |     async def handle_order_update(self, message, tg_id): | ||||||
|  |         """Handle order updates.""" | ||||||
|  |         await self.message_handler.format_order_update(message, tg_id) | ||||||
|  |  | ||||||
|  |     async def handle_execution_update(self, message, tg_id): | ||||||
|  |         """Handle execution updates.""" | ||||||
|  |         await self.message_handler.format_execution_update(message, tg_id) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     async def get_users_from_db(): | ||||||
|  |         """Get all users from the database.""" | ||||||
|  |         return await rq.get_users() | ||||||
							
								
								
									
										181
									
								
								app/helper_functions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								app/helper_functions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,181 @@ | |||||||
|  | import logging.config | ||||||
|  |  | ||||||
|  | from app.bybit import get_bybit_client | ||||||
|  | from logger_helper.logger_helper import LOGGING_CONFIG | ||||||
|  |  | ||||||
|  | logging.config.dictConfig(LOGGING_CONFIG) | ||||||
|  | logger = logging.getLogger("helper_functions") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def safe_float(val) -> float: | ||||||
|  |     """ | ||||||
|  |     Function to safely convert string to float | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         if val is None or val == "": | ||||||
|  |             return 0.0 | ||||||
|  |         return float(val) | ||||||
|  |     except (ValueError, TypeError): | ||||||
|  |         logger.error("Error converting value to float: %s", val) | ||||||
|  |         return 0.0 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def is_number(value: str) -> bool: | ||||||
|  |     """ | ||||||
|  |     Checks if a given string represents a number. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         value (str): The string to check. | ||||||
|  |  | ||||||
|  |     Returns: | ||||||
|  |         bool: True if the string represents a number, False otherwise. | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         # Convert the string to a float | ||||||
|  |         num = float(value) | ||||||
|  |         # Check if the number is positive | ||||||
|  |         if num < 0: | ||||||
|  |             return False | ||||||
|  |         # Check if the string contains "+" or "-" | ||||||
|  |         if "+" in value or "-" in value: | ||||||
|  |             return False | ||||||
|  |         # Check if the string contains only digits | ||||||
|  |         allowed_chars = set("0123456789.") | ||||||
|  |         if not all(ch in allowed_chars for ch in value): | ||||||
|  |             return False | ||||||
|  |         return True | ||||||
|  |     except ValueError: | ||||||
|  |         return False | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def is_int(value: str) -> bool: | ||||||
|  |     """ | ||||||
|  |     Checks if a given string represents an integer. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         value (str): The string to check. | ||||||
|  |  | ||||||
|  |     Returns: | ||||||
|  |         bool: True if the string represents an integer, False otherwise. | ||||||
|  |     """ | ||||||
|  |     # Check if the string contains only digits | ||||||
|  |     if not value.isdigit(): | ||||||
|  |         return False | ||||||
|  |     # Convert the string to an integer | ||||||
|  |     num = int(value) | ||||||
|  |     return num > 0 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def is_int_for_timer(value: str) -> bool | int: | ||||||
|  |     """ | ||||||
|  |     Checks if a given string represents an integer for timer. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         value (str): The string to check. | ||||||
|  |  | ||||||
|  |     Returns: | ||||||
|  |         bool: True if the string represents an integer, False otherwise. | ||||||
|  |     """ | ||||||
|  |     # Check if the string contains only digits | ||||||
|  |     try: | ||||||
|  |         num = int(value) | ||||||
|  |  | ||||||
|  |         if num >= 0: | ||||||
|  |             return num | ||||||
|  |         else: | ||||||
|  |             return False | ||||||
|  |     except ValueError: | ||||||
|  |         return False | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_base_currency(symbol: str) -> str: | ||||||
|  |     """ | ||||||
|  |     Extracts the base currency from a symbol string. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         symbol (str): The symbol string to extract the base currency from. | ||||||
|  |  | ||||||
|  |     Returns: | ||||||
|  |         str: The base currency extracted from the symbol string. | ||||||
|  |     """ | ||||||
|  |     if symbol.endswith("USDT"): | ||||||
|  |         return symbol[:-4] | ||||||
|  |     return symbol | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def safe_int(value, default=0) -> int: | ||||||
|  |     """ | ||||||
|  |     Integer conversion with default value. | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         return int(value) | ||||||
|  |     except (ValueError, TypeError): | ||||||
|  |         return default | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def format_value(value) -> str: | ||||||
|  |     """ | ||||||
|  |     Function to format value | ||||||
|  |     """ | ||||||
|  |     if not value or value.strip() == "": | ||||||
|  |         return "Нет данных" | ||||||
|  |     return value | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def check_limit_price(limit_price, min_price, max_price) -> str | None: | ||||||
|  |     """ | ||||||
|  |     Function to check limit price | ||||||
|  |     """ | ||||||
|  |     if limit_price < min_price: | ||||||
|  |         return "Limit price is out min price" | ||||||
|  |     if limit_price > max_price: | ||||||
|  |         return "Limit price is out max price" | ||||||
|  |     return None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def get_liquidation_price( | ||||||
|  |     tg_id: int, symbol: str, entry_price: float, leverage: float | ||||||
|  | ) -> tuple[float, float]: | ||||||
|  |     """ | ||||||
|  |     Function to get liquidation price | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         client = await get_bybit_client(tg_id=tg_id) | ||||||
|  |         get_risk_info = client.get_risk_limit(category="linear", symbol=symbol) | ||||||
|  |         risk_list = get_risk_info.get("result", {}).get("list", []) | ||||||
|  |         risk_level = risk_list[0] if risk_list else {} | ||||||
|  |         maintenance_margin_rate = safe_float(risk_level.get("maintenanceMargin")) | ||||||
|  |  | ||||||
|  |         liq_price_long = entry_price * (1 - 1 / leverage + maintenance_margin_rate) | ||||||
|  |         liq_price_short = entry_price * (1 + 1 / leverage - maintenance_margin_rate) | ||||||
|  |  | ||||||
|  |         liq_price = liq_price_long, liq_price_short | ||||||
|  |  | ||||||
|  |         return liq_price | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error("Error getting liquidation price: %s", e) | ||||||
|  |         return 0, 0 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def calculate_total_budget( | ||||||
|  |     quantity, martingale_factor, max_steps | ||||||
|  | ) -> float: | ||||||
|  |     """ | ||||||
|  |     Calculate the total budget for a series of trading steps. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         quantity (float): The initial quantity of the asset. | ||||||
|  |         martingale_factor (float): The factor by which the quantity is multiplied for each step. | ||||||
|  |         max_steps (int): The maximum number of trading steps. | ||||||
|  |  | ||||||
|  |     Returns: | ||||||
|  |         float: The total budget for the series of trading steps. | ||||||
|  |     """ | ||||||
|  |     total = 0 | ||||||
|  |     for step in range(max_steps): | ||||||
|  |         set_quantity = quantity * (martingale_factor**step) | ||||||
|  |  | ||||||
|  |         r_quantity = set_quantity | ||||||
|  |  | ||||||
|  |         total += r_quantity | ||||||
|  |     return total | ||||||
| @@ -1,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) | ||||||
							
								
								
									
										1029
									
								
								app/telegram/handlers/main_settings/additional_settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1029
									
								
								app/telegram/handlers/main_settings/additional_settings.py
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										174
									
								
								app/telegram/handlers/main_settings/conditional_settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								app/telegram/handlers/main_settings/conditional_settings.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,174 @@ | |||||||
|  | import logging.config | ||||||
|  |  | ||||||
|  | from aiogram import Router | ||||||
|  | from aiogram.fsm.context import FSMContext | ||||||
|  | from aiogram.types import CallbackQuery, Message | ||||||
|  |  | ||||||
|  | import app.telegram.keyboards.inline as kbi | ||||||
|  | import database.request as rq | ||||||
|  | from app.helper_functions import is_int_for_timer | ||||||
|  | from app.telegram.states.states import ConditionalSettingsState | ||||||
|  | from logger_helper.logger_helper import LOGGING_CONFIG | ||||||
|  |  | ||||||
|  | logging.config.dictConfig(LOGGING_CONFIG) | ||||||
|  | logger = logging.getLogger("conditional_settings") | ||||||
|  |  | ||||||
|  | router_conditional_settings = Router(name="conditional_settings") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router_conditional_settings.callback_query( | ||||||
|  |     lambda c: c.data == "start_timer" or c.data == "stop_timer" | ||||||
|  | ) | ||||||
|  | async def timer(callback_query: CallbackQuery, state: FSMContext): | ||||||
|  |     """ | ||||||
|  |     Handles callback queries starting with 'start_timer' or 'stop_timer'. | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         await state.clear() | ||||||
|  |         if callback_query.data == "start_timer": | ||||||
|  |             await state.set_state(ConditionalSettingsState.start_timer_state) | ||||||
|  |             msg = await callback_query.message.edit_text( | ||||||
|  |                 "Введите время в минутах для старта торговли:", | ||||||
|  |                 reply_markup=kbi.back_to_conditions, | ||||||
|  |             ) | ||||||
|  |             await state.update_data(prompt_message_id=msg.message_id) | ||||||
|  |         elif callback_query.data == "stop_timer": | ||||||
|  |             await state.set_state(ConditionalSettingsState.stop_timer_state) | ||||||
|  |             msg = await callback_query.message.edit_text( | ||||||
|  |                 "Введите время в минутах для остановки торговли:", | ||||||
|  |                 reply_markup=kbi.back_to_conditions, | ||||||
|  |             ) | ||||||
|  |             await state.update_data(prompt_message_id=msg.message_id) | ||||||
|  |         else: | ||||||
|  |             await callback_query.answer( | ||||||
|  |                 text="Произошла ошибка. Пожалуйста, попробуйте позже." | ||||||
|  |             ) | ||||||
|  |     except Exception as e: | ||||||
|  |         await callback_query.answer( | ||||||
|  |             text="Произошла ошибка. Пожалуйста, попробуйте позже." | ||||||
|  |         ) | ||||||
|  |         logger.error( | ||||||
|  |             "Error processing command timer for user %s: %s", | ||||||
|  |             callback_query.from_user.id, | ||||||
|  |             e, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router_conditional_settings.message(ConditionalSettingsState.start_timer_state) | ||||||
|  | async def start_timer(message: Message, state: FSMContext): | ||||||
|  |     """ | ||||||
|  |     Handles the start_timer state of the Finite State Machine. | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         try: | ||||||
|  |             data = await state.get_data() | ||||||
|  |             if "prompt_message_id" in data: | ||||||
|  |                 prompt_message_id = data["prompt_message_id"] | ||||||
|  |                 await message.bot.delete_message( | ||||||
|  |                     chat_id=message.chat.id, message_id=prompt_message_id | ||||||
|  |                 ) | ||||||
|  |             await message.delete() | ||||||
|  |         except Exception as e: | ||||||
|  |             if "message to delete not found" in str(e).lower(): | ||||||
|  |                 pass  # Ignore this error | ||||||
|  |             else: | ||||||
|  |                 raise e | ||||||
|  |  | ||||||
|  |         get_start_timer = message.text | ||||||
|  |         value = is_int_for_timer(get_start_timer) | ||||||
|  |  | ||||||
|  |         if value is False: | ||||||
|  |             await message.answer( | ||||||
|  |                 "Ошибка: введите валидное число.", | ||||||
|  |                 reply_markup=kbi.back_to_conditions, | ||||||
|  |             ) | ||||||
|  |             logger.debug( | ||||||
|  |                 "User %s input invalid (not an valid number): %s", | ||||||
|  |                 message.from_user.id, | ||||||
|  |                 get_start_timer, | ||||||
|  |             ) | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         req = await rq.set_start_timer( | ||||||
|  |             tg_id=message.from_user.id, timer_start=int(get_start_timer) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         if req: | ||||||
|  |             await message.answer( | ||||||
|  |                 "Таймер успешно установлен.", | ||||||
|  |                 reply_markup=kbi.back_to_conditions, | ||||||
|  |             ) | ||||||
|  |         else: | ||||||
|  |             await message.answer( | ||||||
|  |                 "Произошла ошибка. Пожалуйста, попробуйте позже.", | ||||||
|  |                 reply_markup=kbi.back_to_conditions, | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         await state.clear() | ||||||
|  |     except Exception as e: | ||||||
|  |         await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.") | ||||||
|  |         logger.error( | ||||||
|  |             "Error processing command start_timer for user %s: %s", | ||||||
|  |             message.from_user.id, | ||||||
|  |             e, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router_conditional_settings.message(ConditionalSettingsState.stop_timer_state) | ||||||
|  | async def stop_timer(message: Message, state: FSMContext): | ||||||
|  |     """ | ||||||
|  |     Handles the stop_timer state of the Finite State Machine. | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         try: | ||||||
|  |             data = await state.get_data() | ||||||
|  |             if "prompt_message_id" in data: | ||||||
|  |                 prompt_message_id = data["prompt_message_id"] | ||||||
|  |                 await message.bot.delete_message( | ||||||
|  |                     chat_id=message.chat.id, message_id=prompt_message_id | ||||||
|  |                 ) | ||||||
|  |             await message.delete() | ||||||
|  |         except Exception as e: | ||||||
|  |             if "message to delete not found" in str(e).lower(): | ||||||
|  |                 pass  # Ignore this error | ||||||
|  |             else: | ||||||
|  |                 raise e | ||||||
|  |  | ||||||
|  |         get_stop_timer = message.text | ||||||
|  |         value = is_int_for_timer(get_stop_timer) | ||||||
|  |  | ||||||
|  |         if value is False: | ||||||
|  |             await message.answer( | ||||||
|  |                 "Ошибка: введите валидное число.", | ||||||
|  |                 reply_markup=kbi.back_to_conditions, | ||||||
|  |             ) | ||||||
|  |             logger.debug( | ||||||
|  |                 "User %s input invalid (not an valid number): %s", | ||||||
|  |                 message.from_user.id, | ||||||
|  |                 get_stop_timer, | ||||||
|  |             ) | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         req = await rq.set_stop_timer( | ||||||
|  |             tg_id=message.from_user.id, timer_end=int(get_stop_timer) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         if req: | ||||||
|  |             await message.answer( | ||||||
|  |                 "Таймер успешно установлен.", | ||||||
|  |                 reply_markup=kbi.back_to_conditions, | ||||||
|  |             ) | ||||||
|  |         else: | ||||||
|  |             await message.answer( | ||||||
|  |                 "Произошла ошибка. Пожалуйста, попробуйте позже.", | ||||||
|  |                 reply_markup=kbi.back_to_conditions, | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         await state.clear() | ||||||
|  |     except Exception as e: | ||||||
|  |         await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.") | ||||||
|  |         logger.error( | ||||||
|  |             "Error processing command stop_timer for user %s: %s", | ||||||
|  |             message.from_user.id, | ||||||
|  |             e, | ||||||
|  |         ) | ||||||
							
								
								
									
										343
									
								
								app/telegram/handlers/main_settings/risk_management.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										343
									
								
								app/telegram/handlers/main_settings/risk_management.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,343 @@ | |||||||
|  | import logging.config | ||||||
|  |  | ||||||
|  | from aiogram import F, Router | ||||||
|  | from aiogram.fsm.context import FSMContext | ||||||
|  | from aiogram.types import CallbackQuery, Message | ||||||
|  |  | ||||||
|  | import app.telegram.keyboards.inline as kbi | ||||||
|  | import database.request as rq | ||||||
|  | from app.helper_functions import is_number, safe_float | ||||||
|  | from app.telegram.states.states import RiskManagementState | ||||||
|  | from logger_helper.logger_helper import LOGGING_CONFIG | ||||||
|  |  | ||||||
|  | logging.config.dictConfig(LOGGING_CONFIG) | ||||||
|  | logger = logging.getLogger("risk_management") | ||||||
|  |  | ||||||
|  | router_risk_management = Router(name="risk_management") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router_risk_management.callback_query(F.data == "take_profit_percent") | ||||||
|  | async def take_profit_percent(callback_query: CallbackQuery, state: FSMContext) -> None: | ||||||
|  |     """ | ||||||
|  |     Handles the 'profit_price_change' callback query. | ||||||
|  |  | ||||||
|  |     Clears the current FSM state, edits the message text to display the take profit percent options, | ||||||
|  |     and shows an inline keyboard for selection. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. | ||||||
|  |         state (FSMContext): Finite State Machine context for the current user session. | ||||||
|  |  | ||||||
|  |     Logs: | ||||||
|  |         Success or error messages with user identification. | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         await state.clear() | ||||||
|  |         await state.set_state(RiskManagementState.take_profit_percent_state) | ||||||
|  |         msg = await callback_query.message.edit_text( | ||||||
|  |             text="Введите процент изменения цены для фиксации прибыли: ", | ||||||
|  |             reply_markup=kbi.back_to_risk_management, | ||||||
|  |         ) | ||||||
|  |         await state.update_data(prompt_message_id=msg.message_id) | ||||||
|  |         logger.debug( | ||||||
|  |             "Command profit_price_change processed successfully for user: %s", | ||||||
|  |             callback_query.from_user.id, | ||||||
|  |         ) | ||||||
|  |     except Exception as e: | ||||||
|  |         await callback_query.answer( | ||||||
|  |             text="Произошла ошибка. Пожалуйста, попробуйте позже." | ||||||
|  |         ) | ||||||
|  |         logger.error( | ||||||
|  |             "Error processing command profit_price_change for user %s: %s", | ||||||
|  |             callback_query.from_user.id, | ||||||
|  |             e, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router_risk_management.message(RiskManagementState.take_profit_percent_state) | ||||||
|  | async def set_take_profit_percent(message: Message, state: FSMContext) -> None: | ||||||
|  |     """ | ||||||
|  |     Handles user input for setting the take profit percentage. | ||||||
|  |  | ||||||
|  |     Updates FSM context with the selected percentage and persists the choice in database. | ||||||
|  |     Sends an acknowledgement to user and clears FSM state afterward. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         message (Message): Incoming message from user containing the take profit percentage. | ||||||
|  |         state (FSMContext): Finite State Machine context for the current user session. | ||||||
|  |  | ||||||
|  |     Logs: | ||||||
|  |         Success or error messages with user identification. | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         try: | ||||||
|  |             data = await state.get_data() | ||||||
|  |             if "prompt_message_id" in data: | ||||||
|  |                 prompt_message_id = data["prompt_message_id"] | ||||||
|  |                 await message.bot.delete_message( | ||||||
|  |                     chat_id=message.chat.id, message_id=prompt_message_id | ||||||
|  |                 ) | ||||||
|  |             await message.delete() | ||||||
|  |         except Exception as e: | ||||||
|  |             if "message to delete not found" in str(e).lower(): | ||||||
|  |                 pass  # Ignore this error | ||||||
|  |             else: | ||||||
|  |                 raise e | ||||||
|  |  | ||||||
|  |         take_profit_percent_value = message.text | ||||||
|  |  | ||||||
|  |         if not is_number(take_profit_percent_value): | ||||||
|  |             await message.answer( | ||||||
|  |                 text="Ошибка: введите валидное число.", | ||||||
|  |                 reply_markup=kbi.back_to_risk_management, | ||||||
|  |             ) | ||||||
|  |             logger.debug( | ||||||
|  |                 "User %s input invalid (not an valid number): %s", | ||||||
|  |                 message.from_user.id, | ||||||
|  |                 take_profit_percent_value, | ||||||
|  |             ) | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         if safe_float(take_profit_percent_value) < 0.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) < 0.1 or safe_float(stop_loss_percent_value) > 100: | ||||||
|  |             await message.answer( | ||||||
|  |                 text="Ошибка: введите число от 1 до 100.", | ||||||
|  |                 reply_markup=kbi.back_to_risk_management, | ||||||
|  |             ) | ||||||
|  |             logger.debug( | ||||||
|  |                 "User %s input invalid (not an valid number): %s", | ||||||
|  |                 message.from_user.id, | ||||||
|  |                 stop_loss_percent_value, | ||||||
|  |             ) | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         req = await rq.set_stop_loss_percent( | ||||||
|  |             tg_id=message.from_user.id, stop_loss_percent=safe_float(stop_loss_percent_value) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         if req: | ||||||
|  |             await message.answer( | ||||||
|  |                 text=f"Процент изменения цены для фиксации убытка " | ||||||
|  |                 f"установлен на {stop_loss_percent_value}%.", | ||||||
|  |                 reply_markup=kbi.back_to_risk_management, | ||||||
|  |             ) | ||||||
|  |         else: | ||||||
|  |             await message.answer( | ||||||
|  |                 text="Произошла ошибка при установке процента изменения цены для фиксации убытка. " | ||||||
|  |                 "Пожалуйста, попробуйте позже.", | ||||||
|  |                 reply_markup=kbi.back_to_risk_management, | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         await state.clear() | ||||||
|  |     except Exception as e: | ||||||
|  |         await message.answer( | ||||||
|  |             text="Произошла ошибка при установке процента изменения цены для фиксации убытка. " | ||||||
|  |             "Пожалуйста, попробуйте позже.", | ||||||
|  |             reply_markup=kbi.back_to_risk_management, | ||||||
|  |         ) | ||||||
|  |         logger.error( | ||||||
|  |             "Error processing command stop_loss_percent for user %s: %s", | ||||||
|  |             message.from_user.id, | ||||||
|  |             e, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router_risk_management.callback_query(F.data == "commission_fee") | ||||||
|  | async def commission_fee(callback_query: CallbackQuery, state: FSMContext) -> None: | ||||||
|  |     """ | ||||||
|  |     Handles the 'commission_fee' callback query. | ||||||
|  |  | ||||||
|  |     Clears the current FSM state, edits the message text to display the commission fee options, | ||||||
|  |     and shows an inline keyboard for selection. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. | ||||||
|  |         state (FSMContext): Finite State Machine context for the current user session. | ||||||
|  |  | ||||||
|  |     Logs: | ||||||
|  |         Success or error messages with user identification. | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         await state.clear() | ||||||
|  |         await state.set_state(RiskManagementState.commission_fee_state) | ||||||
|  |         msg = await callback_query.message.edit_text( | ||||||
|  |             text="Учитывать комиссию биржи для расчета прибыли?: ", | ||||||
|  |             reply_markup=kbi.commission_fee, | ||||||
|  |         ) | ||||||
|  |         await state.update_data(prompt_message_id=msg.message_id) | ||||||
|  |         logger.debug( | ||||||
|  |             "Command commission_fee processed successfully for user: %s", | ||||||
|  |             callback_query.from_user.id, | ||||||
|  |         ) | ||||||
|  |     except Exception as e: | ||||||
|  |         await callback_query.answer( | ||||||
|  |             text="Произошла ошибка. Пожалуйста, попробуйте позже." | ||||||
|  |         ) | ||||||
|  |         logger.error( | ||||||
|  |             "Error processing command commission_fee for user %s: %s", | ||||||
|  |             callback_query.from_user.id, | ||||||
|  |             e, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router_risk_management.callback_query( | ||||||
|  |     lambda c: c.data in ["Yes_commission_fee", "No_commission_fee"] | ||||||
|  | ) | ||||||
|  | async def set_commission_fee(callback_query: CallbackQuery, state: FSMContext) -> None: | ||||||
|  |     """ | ||||||
|  |     Handles user input for setting the commission fee. | ||||||
|  |  | ||||||
|  |     Updates FSM context with the selected option and persists the choice in database. | ||||||
|  |     Sends an acknowledgement to user and clears FSM state afterward. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. | ||||||
|  |         state (FSMContext): Finite State Machine context for the current user session. | ||||||
|  |  | ||||||
|  |     Logs: | ||||||
|  |         Success or error messages with user identification. | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         req = await rq.set_commission_fee( | ||||||
|  |             tg_id=callback_query.from_user.id, commission_fee=callback_query.data | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         if not req: | ||||||
|  |             await callback_query.answer( | ||||||
|  |                 text="Произошла ошибка при установке комиссии биржи. Пожалуйста, попробуйте позже." | ||||||
|  |             ) | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         if callback_query.data == "Yes_commission_fee": | ||||||
|  |             await callback_query.answer(text="Комиссия биржи учитывается.") | ||||||
|  |         else: | ||||||
|  |             await callback_query.answer(text="Комиссия биржи не учитывается.") | ||||||
|  |  | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error( | ||||||
|  |             "Error processing command commission_fee for user %s: %s", | ||||||
|  |             callback_query.from_user.id, | ||||||
|  |             e, | ||||||
|  |         ) | ||||||
|  |     finally: | ||||||
|  |         await state.clear() | ||||||
							
								
								
									
										198
									
								
								app/telegram/handlers/settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								app/telegram/handlers/settings.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,198 @@ | |||||||
|  | import logging.config | ||||||
|  |  | ||||||
|  | from aiogram import F, Router | ||||||
|  | from aiogram.fsm.context import FSMContext | ||||||
|  | from aiogram.types import CallbackQuery | ||||||
|  |  | ||||||
|  | import app.telegram.keyboards.inline as kbi | ||||||
|  | import database.request as rq | ||||||
|  |  | ||||||
|  | from app.helper_functions import calculate_total_budget, safe_float | ||||||
|  | from logger_helper.logger_helper import LOGGING_CONFIG | ||||||
|  |  | ||||||
|  | logging.config.dictConfig(LOGGING_CONFIG) | ||||||
|  | logger = logging.getLogger("settings") | ||||||
|  |  | ||||||
|  | router_settings = Router(name="settings") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router_settings.callback_query(F.data == "additional_settings") | ||||||
|  | async def additional_settings(callback_query: CallbackQuery, state: FSMContext) -> None: | ||||||
|  |     """ | ||||||
|  |     Handler for the "additional_settings" command. | ||||||
|  |     Sends a message with additional settings options. | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         await state.clear() | ||||||
|  |         tg_id = callback_query.from_user.id | ||||||
|  |         additional_data = await rq.get_user_additional_settings(tg_id=tg_id) | ||||||
|  |  | ||||||
|  |         if not additional_data: | ||||||
|  |             await rq.create_user( | ||||||
|  |                 tg_id=tg_id, username=callback_query.from_user.username | ||||||
|  |             ) | ||||||
|  |             await rq.create_user_additional_settings(tg_id=tg_id) | ||||||
|  |             await rq.create_user_risk_management(tg_id=tg_id) | ||||||
|  |             await rq.create_user_conditional_settings(tg_id=tg_id) | ||||||
|  |             await additional_settings(callback_query=callback_query, state=state) | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         trade_mode_map = { | ||||||
|  |             "Long": "Лонг", | ||||||
|  |             "Short": "Шорт", | ||||||
|  |             "Switch": "Свитч", | ||||||
|  |         } | ||||||
|  |         margin_type_map = { | ||||||
|  |             "ISOLATED_MARGIN": "Изолированная", | ||||||
|  |             "REGULAR_MARGIN": "Кросс", | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         trade_mode = additional_data.trade_mode or "" | ||||||
|  |         margin_type = additional_data.margin_type or "" | ||||||
|  |  | ||||||
|  |         trade_mode_rus = trade_mode_map.get(trade_mode, trade_mode) | ||||||
|  |         margin_type_rus = margin_type_map.get(margin_type, margin_type) | ||||||
|  |         switch_side = additional_data.switch_side | ||||||
|  |  | ||||||
|  |         def f(x): | ||||||
|  |             return safe_float(x) | ||||||
|  |  | ||||||
|  |         leverage = f(additional_data.leverage) | ||||||
|  |         martingale = f(additional_data.martingale_factor) | ||||||
|  |         max_bets = additional_data.max_bets_in_series | ||||||
|  |         quantity = f(additional_data.order_quantity) | ||||||
|  |         trigger_price = f(additional_data.trigger_price) or 0 | ||||||
|  |         side = additional_data.side | ||||||
|  |  | ||||||
|  |         side_map = { | ||||||
|  |             "Buy": "Лонг", | ||||||
|  |             "Sell": "Шорт", | ||||||
|  |         } | ||||||
|  |         side_rus = side_map.get(side, side) | ||||||
|  |  | ||||||
|  |         switch_side_mode = "" | ||||||
|  |         side = "" | ||||||
|  |         if trade_mode == "Switch": | ||||||
|  |             side = f"- Направление первой сделки: {side_rus}\n" | ||||||
|  |             switch_side_mode = f"- Направление первой сделки последующих серии: {switch_side}\n" | ||||||
|  |  | ||||||
|  |         total_budget = await calculate_total_budget( | ||||||
|  |             quantity=quantity, | ||||||
|  |             martingale_factor=martingale, | ||||||
|  |             max_steps=max_bets, | ||||||
|  |         ) | ||||||
|  |         text = ( | ||||||
|  |             f"Основные настройки:\n\n" | ||||||
|  |             f"- Режим торговли: {trade_mode_rus}\n" | ||||||
|  |             f"{side}" | ||||||
|  |             f"{switch_side_mode}" | ||||||
|  |             f"- Тип маржи: {margin_type_rus}\n" | ||||||
|  |             f"- Размер кредитного плеча: {leverage:.2f}\n" | ||||||
|  |             f"- Базовая ставка: {quantity} USDT\n" | ||||||
|  |             f"- Коэффициент мартингейла: {martingale:.2f}\n" | ||||||
|  |             f"- Триггер цена: {trigger_price:.4f} USDT\n" | ||||||
|  |             f"- Максимальное кол-во ставок в серии: {max_bets}\n\n" | ||||||
|  |             f"- Бюджет серии: {total_budget:.2f} USDT\n" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         keyboard = kbi.get_additional_settings_keyboard(mode=trade_mode) | ||||||
|  |         await callback_query.message.edit_text(text=text, reply_markup=keyboard) | ||||||
|  |         logger.debug( | ||||||
|  |             "Command additional_settings processed successfully for user: %s", tg_id | ||||||
|  |         ) | ||||||
|  |     except Exception as e: | ||||||
|  |         await callback_query.message.edit_text( | ||||||
|  |             text="Произошла ошибка. Пожалуйста, попробуйте ещё раз.", | ||||||
|  |             reply_markup=kbi.profile_bybit, | ||||||
|  |         ) | ||||||
|  |         logger.error( | ||||||
|  |             "Error processing command additional_settings for user %s: %s", | ||||||
|  |             callback_query.from_user.id, | ||||||
|  |             e, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router_settings.callback_query(F.data == "risk_management") | ||||||
|  | async def risk_management(callback_query: CallbackQuery, state: FSMContext) -> None: | ||||||
|  |     """ | ||||||
|  |     Handler for the "risk_management" command. | ||||||
|  |     Sends a message with risk management options. | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         await state.clear() | ||||||
|  |         risk_management_data = await rq.get_user_risk_management( | ||||||
|  |             tg_id=callback_query.from_user.id | ||||||
|  |         ) | ||||||
|  |         if risk_management_data: | ||||||
|  |             take_profit_percent = risk_management_data.take_profit_percent or "" | ||||||
|  |             stop_loss_percent = risk_management_data.stop_loss_percent or "" | ||||||
|  |             commission_fee = risk_management_data.commission_fee or "" | ||||||
|  |             commission_fee_rus = ( | ||||||
|  |                 "Да" if commission_fee == "Yes_commission_fee" else "Нет" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             await callback_query.message.edit_text( | ||||||
|  |                 text=f"Риск-менеджмент:\n\n" | ||||||
|  |                 f"- Процент изменения цены для фиксации прибыли: {take_profit_percent:.2f}%\n" | ||||||
|  |                 f"- Процент изменения цены для фиксации убытка: {stop_loss_percent:.2f}%\n\n" | ||||||
|  |                 f"- Комиссия биржи для расчета прибыли: {commission_fee_rus}\n\n", | ||||||
|  |                 reply_markup=kbi.risk_management, | ||||||
|  |             ) | ||||||
|  |             logger.debug( | ||||||
|  |                 "Command main_settings processed successfully for user: %s", | ||||||
|  |                 callback_query.from_user.id, | ||||||
|  |             ) | ||||||
|  |         else: | ||||||
|  |             await rq.create_user( | ||||||
|  |                 tg_id=callback_query.from_user.id, | ||||||
|  |                 username=callback_query.from_user.username, | ||||||
|  |             ) | ||||||
|  |             await rq.create_user_additional_settings(tg_id=callback_query.from_user.id) | ||||||
|  |             await rq.create_user_risk_management(tg_id=callback_query.from_user.id) | ||||||
|  |             await rq.create_user_conditional_settings(tg_id=callback_query.from_user.id) | ||||||
|  |             await risk_management(callback_query=callback_query, state=state) | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error( | ||||||
|  |             "Error processing command main_settings for user %s: %s", | ||||||
|  |             callback_query.from_user.id, | ||||||
|  |             e, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router_settings.callback_query(F.data == "conditional_settings") | ||||||
|  | async def conditions(callback_query: CallbackQuery, state: FSMContext) -> None: | ||||||
|  |     """ | ||||||
|  |     Handler for the "conditions" command. | ||||||
|  |     Sends a message with trading conditions options. | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         await state.clear() | ||||||
|  |         conditional_settings_data = await rq.get_user_conditional_settings( | ||||||
|  |             tg_id=callback_query.from_user.id | ||||||
|  |         ) | ||||||
|  |         if conditional_settings_data: | ||||||
|  |             start_timer = conditional_settings_data.timer_start or 0 | ||||||
|  |             await callback_query.message.edit_text( | ||||||
|  |                 text="Условия торговли:\n\n" | ||||||
|  |                 f"- Таймер для старта: {start_timer} мин.\n", | ||||||
|  |                 reply_markup=kbi.conditions, | ||||||
|  |             ) | ||||||
|  |             logger.debug( | ||||||
|  |                 "Command main_settings processed successfully for user: %s", | ||||||
|  |                 callback_query.from_user.id, | ||||||
|  |             ) | ||||||
|  |         else: | ||||||
|  |             await rq.create_user( | ||||||
|  |                 tg_id=callback_query.from_user.id, | ||||||
|  |                 username=callback_query.from_user.username, | ||||||
|  |             ) | ||||||
|  |             await rq.create_user_additional_settings(tg_id=callback_query.from_user.id) | ||||||
|  |             await rq.create_user_risk_management(tg_id=callback_query.from_user.id) | ||||||
|  |             await rq.create_user_conditional_settings(tg_id=callback_query.from_user.id) | ||||||
|  |             await conditions(callback_query=callback_query, state=state) | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error( | ||||||
|  |             "Error processing command main_settings for user %s: %s", | ||||||
|  |             callback_query.from_user.id, | ||||||
|  |             e, | ||||||
|  |         ) | ||||||
							
								
								
									
										166
									
								
								app/telegram/handlers/start_trading.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								app/telegram/handlers/start_trading.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,166 @@ | |||||||
|  | import asyncio | ||||||
|  | import logging.config | ||||||
|  |  | ||||||
|  | from aiogram import F, Router | ||||||
|  | from aiogram.fsm.context import FSMContext | ||||||
|  | from aiogram.types import CallbackQuery | ||||||
|  |  | ||||||
|  | import app.telegram.keyboards.inline as kbi | ||||||
|  | import database.request as rq | ||||||
|  | from app.bybit.get_functions.get_positions import get_active_positions_by_symbol, get_active_orders_by_symbol | ||||||
|  | from app.bybit.open_positions import start_trading_cycle | ||||||
|  | from app.helper_functions import safe_float | ||||||
|  | from app.telegram.tasks.tasks import ( | ||||||
|  |     add_start_task_merged, | ||||||
|  |     cancel_start_task_merged | ||||||
|  | ) | ||||||
|  | from logger_helper.logger_helper import LOGGING_CONFIG | ||||||
|  |  | ||||||
|  | logging.config.dictConfig(LOGGING_CONFIG) | ||||||
|  | logger = logging.getLogger("start_trading") | ||||||
|  |  | ||||||
|  | router_start_trading = Router(name="start_trading") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router_start_trading.callback_query(F.data == "start_trading") | ||||||
|  | async def start_trading(callback_query: CallbackQuery, state: FSMContext) -> None: | ||||||
|  |     """ | ||||||
|  |     Handles the "start_trading" callback query. | ||||||
|  |     Clears the FSM state and sends a message to the user to select the trading mode. | ||||||
|  |     :param callback_query: Message | ||||||
|  |     :param state: FSMContext | ||||||
|  |     :return: None | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         await state.clear() | ||||||
|  |         tg_id = callback_query.from_user.id | ||||||
|  |         symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id) | ||||||
|  |         deals = await get_active_positions_by_symbol( | ||||||
|  |             tg_id=callback_query.from_user.id, symbol=symbol | ||||||
|  |         ) | ||||||
|  |         position = next((d for d in deals if d.get("symbol") == symbol), None) | ||||||
|  |  | ||||||
|  |         if position: | ||||||
|  |             size = position.get("size", 0) | ||||||
|  |         else: | ||||||
|  |             size = 0 | ||||||
|  |  | ||||||
|  |         if safe_float(size) > 0: | ||||||
|  |             await callback_query.answer( | ||||||
|  |                 text="У вас есть активная позиция по текущей паре", | ||||||
|  |             ) | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         orders = await get_active_orders_by_symbol( | ||||||
|  |             tg_id=callback_query.from_user.id, symbol=symbol) | ||||||
|  |  | ||||||
|  |         if orders is not None: | ||||||
|  |             await callback_query.answer( | ||||||
|  |                 text="У вас есть активный ордер по текущей паре", | ||||||
|  |             ) | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         conditional_data = await rq.get_user_conditional_settings( | ||||||
|  |             tg_id=callback_query.from_user.id | ||||||
|  |         ) | ||||||
|  |         timer_start = conditional_data.timer_start | ||||||
|  |  | ||||||
|  |         cancel_start_task_merged(user_id=callback_query.from_user.id) | ||||||
|  |  | ||||||
|  |         async def delay_start(): | ||||||
|  |             if timer_start > 0: | ||||||
|  |                 await callback_query.message.edit_text( | ||||||
|  |                     text=f"Торговля будет запущена с задержкой {timer_start} мин.", | ||||||
|  |                     reply_markup=kbi.cancel_timer_merged, | ||||||
|  |                 ) | ||||||
|  |                 await rq.set_start_timer( | ||||||
|  |                     tg_id=callback_query.from_user.id, timer_start=0 | ||||||
|  |                 ) | ||||||
|  |                 await asyncio.sleep(timer_start * 60) | ||||||
|  |  | ||||||
|  |             await rq.set_auto_trading( | ||||||
|  |                 tg_id=callback_query.from_user.id, | ||||||
|  |                 symbol=symbol, | ||||||
|  |                 auto_trading=True, | ||||||
|  |             ) | ||||||
|  |             await rq.set_total_fee_user_auto_trading( | ||||||
|  |                 tg_id=tg_id, symbol=symbol, total_fee=0 | ||||||
|  |             ) | ||||||
|  |             await rq.set_fee_user_auto_trading( | ||||||
|  |                 tg_id=tg_id, symbol=symbol, fee=0 | ||||||
|  |             ) | ||||||
|  |             res = await start_trading_cycle( | ||||||
|  |                 tg_id=callback_query.from_user.id, | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             error_messages = { | ||||||
|  |                 "Limit price is out min price": "Цена лимитного ордера меньше допустимого", | ||||||
|  |                 "Limit price is out max price": "Цена лимитного ордера больше допустимого", | ||||||
|  |                 "Risk is too high for this trade": "Риск сделки превышает допустимый убыток", | ||||||
|  |                 "estimated will trigger liq": "Лимитный ордер может вызвать мгновенную ликвидацию. Проверьте параметры ордера.", | ||||||
|  |                 "ab not enough for new order": "Недостаточно средств для создания нового ордера", | ||||||
|  |                 "InvalidRequestError": "Произошла ошибка при запуске торговли.", | ||||||
|  |                 "Order does not meet minimum order value": "Сумма ставки меньше допустимого для запуска торговли. " | ||||||
|  |                                                            "Увеличьте ставку, чтобы запустить торговлю", | ||||||
|  |                 "position idx not match position mode": "Измените режим позиции, чтобы запустить торговлю", | ||||||
|  |                 "Qty invalid": "Некорректное значение ставки для данного инструмента", | ||||||
|  |                 "The number of contracts exceeds maximum limit allowed": "️️Превышен максимальный лимит ставки", | ||||||
|  |                 "The number of contracts exceeds minimum limit allowed": "️️Лимит ставки меньше минимально допустимого", | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if res == "OK": | ||||||
|  |                 await callback_query.message.edit_text(text="Торговля запущена") | ||||||
|  |                 await state.clear() | ||||||
|  |             else: | ||||||
|  |                 await rq.set_auto_trading( | ||||||
|  |                     tg_id=callback_query.from_user.id, | ||||||
|  |                     symbol=symbol, | ||||||
|  |                     auto_trading=False, | ||||||
|  |                 ) | ||||||
|  |                 text = error_messages.get(res, "Произошла ошибка при запуске торговли") | ||||||
|  |                 await callback_query.message.edit_text( | ||||||
|  |                     text=text, reply_markup=kbi.profile_bybit | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|  |         await callback_query.message.edit_text("Запуск торговли...") | ||||||
|  |         task = asyncio.create_task(delay_start()) | ||||||
|  |         await add_start_task_merged(user_id=callback_query.from_user.id, task=task) | ||||||
|  |  | ||||||
|  |     except Exception as e: | ||||||
|  |         await callback_query.answer(text="Произошла ошибка при запуске торговли") | ||||||
|  |         logger.error( | ||||||
|  |             "Error processing command start_trading for user %s: %s", | ||||||
|  |             callback_query.from_user.id, | ||||||
|  |             e, | ||||||
|  |         ) | ||||||
|  |     except asyncio.CancelledError: | ||||||
|  |         logger.error("Cancelled timer for user %s", callback_query.from_user.id) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router_start_trading.callback_query( | ||||||
|  |     lambda c: c.data == "cancel_timer_merged" | ||||||
|  | ) | ||||||
|  | async def cancel_start_trading( | ||||||
|  |     callback_query: CallbackQuery, state: FSMContext | ||||||
|  | ) -> None: | ||||||
|  |     """ | ||||||
|  |     Handles the "cancel_timer" callback query. | ||||||
|  |     Clears the FSM state and sends a message to the user to cancel the start trading process. | ||||||
|  |     :param callback_query: Message | ||||||
|  |     :param state: FSMContext | ||||||
|  |     :return: None | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         await state.clear() | ||||||
|  |         if callback_query.data == "cancel_timer_merged": | ||||||
|  |             cancel_start_task_merged(user_id=callback_query.from_user.id) | ||||||
|  |         await callback_query.message.edit_text( | ||||||
|  |             text="Запуск торговли отменен", reply_markup=kbi.profile_bybit | ||||||
|  |         ) | ||||||
|  |     except Exception as e: | ||||||
|  |         await callback_query.answer("Произошла ошибка при отмене запуска торговли") | ||||||
|  |         logger.error( | ||||||
|  |             "Error processing command cancel_timer for user %s: %s", | ||||||
|  |             callback_query.from_user.id, | ||||||
|  |             e, | ||||||
|  |         ) | ||||||
							
								
								
									
										90
									
								
								app/telegram/handlers/stop_trading.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								app/telegram/handlers/stop_trading.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | |||||||
|  | import asyncio | ||||||
|  | import logging.config | ||||||
|  |  | ||||||
|  | from aiogram import F, Router | ||||||
|  | from aiogram.fsm.context import FSMContext | ||||||
|  | from aiogram.types import CallbackQuery | ||||||
|  |  | ||||||
|  | from app.bybit.close_positions import close_position_by_symbol | ||||||
|  | import app.telegram.keyboards.inline as kbi | ||||||
|  | import database.request as rq | ||||||
|  | from app.telegram.tasks.tasks import add_stop_task, cancel_stop_task | ||||||
|  | from logger_helper.logger_helper import LOGGING_CONFIG | ||||||
|  |  | ||||||
|  | logging.config.dictConfig(LOGGING_CONFIG) | ||||||
|  | logger = logging.getLogger("stop_trading") | ||||||
|  |  | ||||||
|  | router_stop_trading = Router(name="stop_trading") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router_stop_trading.callback_query(F.data == "stop_trading") | ||||||
|  | async def stop_all_trading(callback_query: CallbackQuery, state: FSMContext): | ||||||
|  |     try: | ||||||
|  |         await state.clear() | ||||||
|  |  | ||||||
|  |         cancel_stop_task(callback_query.from_user.id) | ||||||
|  |  | ||||||
|  |         conditional_data = await rq.get_user_conditional_settings( | ||||||
|  |             tg_id=callback_query.from_user.id | ||||||
|  |         ) | ||||||
|  |         timer_end = conditional_data.timer_end | ||||||
|  |         symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id) | ||||||
|  |  | ||||||
|  |         async def delay_start(): | ||||||
|  |             if timer_end > 0: | ||||||
|  |                 await callback_query.message.edit_text( | ||||||
|  |                     text=f"Торговля будет остановлена с задержкой {timer_end} мин.", | ||||||
|  |                     reply_markup=kbi.cancel_timer_stop, | ||||||
|  |                 ) | ||||||
|  |                 await rq.set_stop_timer(tg_id=callback_query.from_user.id, timer_end=0) | ||||||
|  |                 await asyncio.sleep(timer_end * 60) | ||||||
|  |  | ||||||
|  |             user_auto_trading = await rq.get_user_auto_trading( | ||||||
|  |                 tg_id=callback_query.from_user.id, symbol=symbol | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             if user_auto_trading and user_auto_trading.auto_trading: | ||||||
|  |                 await rq.set_auto_trading( | ||||||
|  |                     tg_id=callback_query.from_user.id, | ||||||
|  |                     symbol=symbol, | ||||||
|  |                     auto_trading=False, | ||||||
|  |                 ) | ||||||
|  |                 await close_position_by_symbol( | ||||||
|  |                     tg_id=callback_query.from_user.id, symbol=symbol) | ||||||
|  |                 await callback_query.message.edit_text(text=f"Торговля для {symbol} остановлена", reply_markup=kbi.profile_bybit) | ||||||
|  |             else: | ||||||
|  |                 await callback_query.message.edit_text(text=f"Нет активной торговли для {symbol}", reply_markup=kbi.profile_bybit) | ||||||
|  |  | ||||||
|  |         task = asyncio.create_task(delay_start()) | ||||||
|  |         await add_stop_task(user_id=callback_query.from_user.id, task=task) | ||||||
|  |  | ||||||
|  |         logger.debug( | ||||||
|  |             "Command stop_trading processed successfully for user: %s", | ||||||
|  |             callback_query.from_user.id, | ||||||
|  |         ) | ||||||
|  |     except Exception as e: | ||||||
|  |         await callback_query.answer(text="Произошла ошибка при остановке торговли") | ||||||
|  |         logger.error( | ||||||
|  |             "Error processing command stop_trading for user %s: %s", | ||||||
|  |             callback_query.from_user.id, | ||||||
|  |             e, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router_stop_trading.callback_query(F.data == "cancel_timer_stop") | ||||||
|  | async def cancel_stop_trading(callback_query: CallbackQuery, state: FSMContext): | ||||||
|  |     try: | ||||||
|  |         await state.clear() | ||||||
|  |         cancel_stop_task(callback_query.from_user.id) | ||||||
|  |         await callback_query.message.edit_text( | ||||||
|  |             text="Таймер отменён.", reply_markup=kbi.profile_bybit | ||||||
|  |         ) | ||||||
|  |     except Exception as e: | ||||||
|  |         await callback_query.answer( | ||||||
|  |             text="Произошла ошибка при отмене остановки торговли" | ||||||
|  |         ) | ||||||
|  |         logger.error( | ||||||
|  |             "Error processing command cancel_timer_stop for user %s: %s", | ||||||
|  |             callback_query.from_user.id, | ||||||
|  |             e, | ||||||
|  |         ) | ||||||
							
								
								
									
										168
									
								
								app/telegram/handlers/tp_sl_handlers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								app/telegram/handlers/tp_sl_handlers.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,168 @@ | |||||||
|  | import logging.config | ||||||
|  |  | ||||||
|  | from aiogram import Router | ||||||
|  | from aiogram.fsm.context import FSMContext | ||||||
|  | from aiogram.types import CallbackQuery, Message | ||||||
|  |  | ||||||
|  | import app.telegram.keyboards.inline as kbi | ||||||
|  | from app.bybit.set_functions.set_tp_sl import set_tp_sl_for_position | ||||||
|  | from app.helper_functions import is_number | ||||||
|  | from app.telegram.states.states import SetTradingStopState | ||||||
|  | from logger_helper.logger_helper import LOGGING_CONFIG | ||||||
|  |  | ||||||
|  | logging.config.dictConfig(LOGGING_CONFIG) | ||||||
|  | logger = logging.getLogger("tp_sl_handlers") | ||||||
|  |  | ||||||
|  | router_tp_sl_handlers = Router(name="tp_sl_handlers") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router_tp_sl_handlers.callback_query(lambda c: c.data.startswith("pos_tp_sl_")) | ||||||
|  | async def set_tp_sl_handler(callback_query: CallbackQuery, state: FSMContext) -> None: | ||||||
|  |     """ | ||||||
|  |     Handles the 'pos_tp_sl' callback query. | ||||||
|  |  | ||||||
|  |     Clears the current FSM state, sets the state to 'take_profit', and prompts the user to enter the take-profit. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard. | ||||||
|  |         state (FSMContext): Finite State Machine context for the current user session. | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         await state.clear() | ||||||
|  |         data = callback_query.data | ||||||
|  |         parts = data.split("_") | ||||||
|  |         symbol = parts[3] | ||||||
|  |         position_idx = int(parts[4]) | ||||||
|  |  | ||||||
|  |         await state.set_state(SetTradingStopState.take_profit_state) | ||||||
|  |         await state.update_data(symbol=symbol) | ||||||
|  |         await state.update_data(position_idx=position_idx) | ||||||
|  |         msg = await callback_query.message.answer( | ||||||
|  |             text="Введите тейк-профит:", reply_markup=kbi.cancel | ||||||
|  |         ) | ||||||
|  |         await state.update_data(prompt_message_id=msg.message_id) | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error("Error in set_tp_sl_handler: %s", e) | ||||||
|  |         await callback_query.answer(text="Произошла ошибка, попробуйте позже") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router_tp_sl_handlers.message(SetTradingStopState.take_profit_state) | ||||||
|  | async def set_take_profit_handler(message: Message, state: FSMContext) -> None: | ||||||
|  |     """ | ||||||
|  |     Handles the 'take_profit' state. | ||||||
|  |  | ||||||
|  |     Clears the current FSM state, sets the state to 'stop_loss', and prompts the user to enter the stop-loss. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         message (Message): Incoming message from Telegram. | ||||||
|  |         state (FSMContext): Finite State Machine context for the current user session. | ||||||
|  |  | ||||||
|  |     Returns: | ||||||
|  |         None | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         try: | ||||||
|  |             data = await state.get_data() | ||||||
|  |             if "prompt_message_id" in data: | ||||||
|  |                 prompt_message_id = data["prompt_message_id"] | ||||||
|  |                 await message.bot.delete_message( | ||||||
|  |                     chat_id=message.chat.id, message_id=prompt_message_id | ||||||
|  |                 ) | ||||||
|  |             await message.delete() | ||||||
|  |         except Exception as e: | ||||||
|  |             if "message to delete not found" in str(e).lower(): | ||||||
|  |                 pass  # Ignore this error | ||||||
|  |             else: | ||||||
|  |                 raise e | ||||||
|  |  | ||||||
|  |         take_profit = message.text | ||||||
|  |  | ||||||
|  |         if not is_number(take_profit): | ||||||
|  |             await message.answer( | ||||||
|  |                 "Ошибка: введите валидное число.", | ||||||
|  |                 reply_markup=kbi.profile_bybit, | ||||||
|  |             ) | ||||||
|  |             logger.debug( | ||||||
|  |                 "User %s input invalid (not an valid number): %s", | ||||||
|  |                 message.from_user.id, | ||||||
|  |                 take_profit, | ||||||
|  |             ) | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         await state.update_data(take_profit=take_profit) | ||||||
|  |         await state.set_state(SetTradingStopState.stop_loss_state) | ||||||
|  |         msg = await message.answer(text="Введите стоп-лосс:", reply_markup=kbi.cancel) | ||||||
|  |         await state.update_data(prompt_message_id=msg.message_id) | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error("Error in set_take_profit_handler: %s", e) | ||||||
|  |         await message.answer( | ||||||
|  |             text="Произошла ошибка, попробуйте позже", reply_markup=kbi.profile_bybit | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @router_tp_sl_handlers.message(SetTradingStopState.stop_loss_state) | ||||||
|  | async def set_stop_loss_handler(message: Message, state: FSMContext) -> None: | ||||||
|  |     """ | ||||||
|  |     Handles the 'stop_loss' state. | ||||||
|  |  | ||||||
|  |     Clears the current FSM state, sets the state to 'take_profit', and prompts the user to enter the take-profit. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         message (Message): Incoming message from Telegram. | ||||||
|  |         state (FSMContext): Finite State Machine context for the current user session. | ||||||
|  |  | ||||||
|  |     Returns: | ||||||
|  |         None | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         try: | ||||||
|  |             data = await state.get_data() | ||||||
|  |             if "prompt_message_id" in data: | ||||||
|  |                 prompt_message_id = data["prompt_message_id"] | ||||||
|  |                 await message.bot.delete_message( | ||||||
|  |                     chat_id=message.chat.id, message_id=prompt_message_id | ||||||
|  |                 ) | ||||||
|  |             await message.delete() | ||||||
|  |         except Exception as e: | ||||||
|  |             if "message to delete not found" in str(e).lower(): | ||||||
|  |                 pass  # Ignore this error | ||||||
|  |             else: | ||||||
|  |                 raise e | ||||||
|  |  | ||||||
|  |         stop_loss = message.text | ||||||
|  |  | ||||||
|  |         if not is_number(stop_loss): | ||||||
|  |             await message.answer( | ||||||
|  |                 "Ошибка: введите валидное число.", | ||||||
|  |                 reply_markup=kbi.profile_bybit, | ||||||
|  |             ) | ||||||
|  |             logger.debug( | ||||||
|  |                 "User %s input invalid (not an valid number): %s", | ||||||
|  |                 message.from_user.id, | ||||||
|  |                 stop_loss, | ||||||
|  |             ) | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         await state.update_data(stop_loss=stop_loss) | ||||||
|  |         data = await state.get_data() | ||||||
|  |         symbol = data["symbol"] | ||||||
|  |         take_profit = data["take_profit"] | ||||||
|  |         position_idx = data["position_idx"] | ||||||
|  |         res = await set_tp_sl_for_position( | ||||||
|  |             tg_id=message.from_user.id, | ||||||
|  |             symbol=symbol, | ||||||
|  |             take_profit_price=float(take_profit), | ||||||
|  |             stop_loss_price=float(stop_loss), | ||||||
|  |             position_idx=position_idx, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         if res: | ||||||
|  |             await message.answer(text="Тейк-профит и стоп-лосс установлены.") | ||||||
|  |         else: | ||||||
|  |             await message.answer(text="Тейк-профит и стоп-лосс не установлены.") | ||||||
|  |         await state.clear() | ||||||
|  |     except Exception as e: | ||||||
|  |         await message.answer( | ||||||
|  |             text="Произошла ошибка, попробуйте позже", reply_markup=kbi.profile_bybit | ||||||
|  |         ) | ||||||
|  |         logger.error("Error in set_stop_loss_handler: %s", e) | ||||||
							
								
								
									
										398
									
								
								app/telegram/keyboards/inline.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										398
									
								
								app/telegram/keyboards/inline.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,398 @@ | |||||||
|  | from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup | ||||||
|  | from aiogram.utils.keyboard import InlineKeyboardBuilder | ||||||
|  |  | ||||||
|  | connect_the_platform = InlineKeyboardMarkup( | ||||||
|  |     inline_keyboard=[ | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton( | ||||||
|  |                 text="Подключить платформу", callback_data="connect_platform" | ||||||
|  |             ) | ||||||
|  |         ] | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | add_bybit_api = InlineKeyboardMarkup( | ||||||
|  |     inline_keyboard=[ | ||||||
|  |         [InlineKeyboardButton(text="Добавить API", callback_data="add_bybit_api")] | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | profile_bybit = InlineKeyboardMarkup( | ||||||
|  |     inline_keyboard=[ | ||||||
|  |         [InlineKeyboardButton(text="На главную", callback_data="profile_bybit")] | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | cancel = InlineKeyboardMarkup( | ||||||
|  |     inline_keyboard=[[InlineKeyboardButton(text="Отменить", callback_data="cancel")]] | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | main_menu = InlineKeyboardMarkup( | ||||||
|  |     inline_keyboard=[ | ||||||
|  |         [InlineKeyboardButton(text="Настройки", callback_data="main_settings")], | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton( | ||||||
|  |                 text="Сменить торговую пару", callback_data="change_symbol" | ||||||
|  |             ) | ||||||
|  |         ], | ||||||
|  |         [InlineKeyboardButton(text="Начать торговлю", callback_data="start_trading")], | ||||||
|  |         [InlineKeyboardButton(text="Остановить торговлю", callback_data="stop_trading")], | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | # MAIN SETTINGS | ||||||
|  | main_settings = InlineKeyboardMarkup( | ||||||
|  |     inline_keyboard=[ | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton( | ||||||
|  |                 text="Основные настройки", callback_data="additional_settings" | ||||||
|  |             ), | ||||||
|  |             InlineKeyboardButton( | ||||||
|  |                 text="Риск-менеджмент", callback_data="risk_management" | ||||||
|  |             ), | ||||||
|  |         ], | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton( | ||||||
|  |                 text="Условия запуска", callback_data="conditional_settings" | ||||||
|  |             ) | ||||||
|  |         ], | ||||||
|  |         [InlineKeyboardButton(text="Назад", callback_data="profile_bybit")], | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # additional_settings | ||||||
|  | def get_additional_settings_keyboard(mode: str | ||||||
|  | ) -> InlineKeyboardMarkup: | ||||||
|  |     """ | ||||||
|  |     Create keyboard for additional settings | ||||||
|  |     :param mode: Trade mode | ||||||
|  |     :return: InlineKeyboardMarkup | ||||||
|  |     """ | ||||||
|  |     buttons = [ | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton(text="Режим торговли", callback_data="trade_mode"), | ||||||
|  |             InlineKeyboardButton(text="Тип маржи", callback_data="margin_type"), | ||||||
|  |         ], | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton( | ||||||
|  |                 text="Размер кредитного плеча", callback_data="leverage" | ||||||
|  |             ), | ||||||
|  |             InlineKeyboardButton( | ||||||
|  |                 text="Базовая ставка", callback_data="order_quantity"), | ||||||
|  |         ], | ||||||
|  |  | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton( | ||||||
|  |                 text="Коэффициент мартингейла", callback_data="martingale_factor" | ||||||
|  |             ), | ||||||
|  |             InlineKeyboardButton(text="Триггер цена", callback_data="trigger_price" | ||||||
|  |  | ||||||
|  |             ), | ||||||
|  |         ], | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     if mode == "Switch": | ||||||
|  |         buttons.append( | ||||||
|  |             [InlineKeyboardButton(text="Направление первой сделки первой серии", callback_data="switch_side_start")] | ||||||
|  |         ) | ||||||
|  |         buttons.append( | ||||||
|  |             [InlineKeyboardButton(text="Направление первой сделки последующих серии", callback_data="switch_side_second")] | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     buttons.append( | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton( | ||||||
|  |                 text="Максимальное кол-во ставок в серии", | ||||||
|  |                 callback_data="max_bets_in_series", | ||||||
|  |             ) | ||||||
|  |         ] | ||||||
|  |     ) | ||||||
|  |     buttons.append( | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton(text="Назад", callback_data="main_settings"), | ||||||
|  |             InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), | ||||||
|  |         ] | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     return InlineKeyboardMarkup(inline_keyboard=buttons) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | trade_mode = InlineKeyboardMarkup( | ||||||
|  |     inline_keyboard=[ | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton( | ||||||
|  |                 text="Лонг", callback_data="Long" | ||||||
|  |             ), | ||||||
|  |             InlineKeyboardButton(text="Шорт", callback_data="Short"), | ||||||
|  |             InlineKeyboardButton(text="Свитч", callback_data="Switch"), | ||||||
|  |         ], | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton(text="Назад", callback_data="additional_settings"), | ||||||
|  |             InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), | ||||||
|  |         ], | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | switch_side = InlineKeyboardMarkup( | ||||||
|  |     inline_keyboard=[ | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton( | ||||||
|  |                 text="По направлению", callback_data="switch_direction" | ||||||
|  |             ), | ||||||
|  |             InlineKeyboardButton( | ||||||
|  |                 text="Противоположно", callback_data="switch_opposite" | ||||||
|  |             ), | ||||||
|  |         ], | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton(text="Назад", callback_data="additional_settings"), | ||||||
|  |             InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), | ||||||
|  |         ], | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | side_for_switch = InlineKeyboardMarkup( | ||||||
|  |     inline_keyboard=[ | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton(text="Лонг", callback_data="buy_switch"), | ||||||
|  |             InlineKeyboardButton(text="Шорт", callback_data="sell_switch"), | ||||||
|  |         ], | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton(text="Назад", callback_data="additional_settings"), | ||||||
|  |             InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), | ||||||
|  |         ], | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | margin_type = InlineKeyboardMarkup( | ||||||
|  |     inline_keyboard=[ | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton(text="Изолированная", callback_data="ISOLATED_MARGIN"), | ||||||
|  |             InlineKeyboardButton(text="Кросс", callback_data="REGULAR_MARGIN"), | ||||||
|  |         ], | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton(text="Назад", callback_data="additional_settings"), | ||||||
|  |             InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), | ||||||
|  |         ], | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | back_to_additional_settings = InlineKeyboardMarkup( | ||||||
|  |     inline_keyboard=[ | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton(text="Назад", callback_data="additional_settings"), | ||||||
|  |             InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), | ||||||
|  |         ], | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | back_to_change_limit_price = InlineKeyboardMarkup( | ||||||
|  |     inline_keyboard=[ | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton(text="Назад", callback_data="limit_price"), | ||||||
|  |             InlineKeyboardButton( | ||||||
|  |                 text="Основные настройки", callback_data="additional_settings" | ||||||
|  |             ), | ||||||
|  |         ], | ||||||
|  |         [InlineKeyboardButton(text="На главную", callback_data="profile_bybit")], | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | # risk_management | ||||||
|  | risk_management = InlineKeyboardMarkup( | ||||||
|  |     inline_keyboard=[ | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton( | ||||||
|  |                 text="Тейк-профит", callback_data="take_profit_percent" | ||||||
|  |             ), | ||||||
|  |             InlineKeyboardButton( | ||||||
|  |                 text="Стоп-лосс", callback_data="stop_loss_percent" | ||||||
|  |             ), | ||||||
|  |         ], | ||||||
|  |         [InlineKeyboardButton(text="Комиссия биржи", callback_data="commission_fee")], | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton(text="Назад", callback_data="main_settings"), | ||||||
|  |             InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), | ||||||
|  |         ], | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | back_to_risk_management = InlineKeyboardMarkup( | ||||||
|  |     inline_keyboard=[ | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton(text="Назад", callback_data="risk_management"), | ||||||
|  |             InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), | ||||||
|  |         ], | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | commission_fee = InlineKeyboardMarkup( | ||||||
|  |     inline_keyboard=[ | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton(text="Да", callback_data="Yes_commission_fee"), | ||||||
|  |             InlineKeyboardButton(text="Нет", callback_data="No_commission_fee"), | ||||||
|  |         ], | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton(text="Назад", callback_data="risk_management"), | ||||||
|  |             InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), | ||||||
|  |         ], | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | # conditions | ||||||
|  | conditions = InlineKeyboardMarkup( | ||||||
|  |     inline_keyboard=[ | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton(text="Таймер для старта", callback_data="start_timer"), | ||||||
|  |         ], | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton(text="Назад", callback_data="main_settings"), | ||||||
|  |             InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), | ||||||
|  |         ], | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | back_to_conditions = InlineKeyboardMarkup( | ||||||
|  |     inline_keyboard=[ | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton(text="Назад", callback_data="conditional_settings"), | ||||||
|  |             InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), | ||||||
|  |         ], | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | # SYMBOL | ||||||
|  | symbol = InlineKeyboardMarkup( | ||||||
|  |     inline_keyboard=[ | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton(text="Назад", callback_data="profile_bybit"), | ||||||
|  |         ], | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | # POSITION | ||||||
|  |  | ||||||
|  | change_position = InlineKeyboardMarkup( | ||||||
|  |     inline_keyboard=[ | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton(text="Позиции", callback_data="change_position"), | ||||||
|  |             InlineKeyboardButton(text="Открытые ордера", callback_data="open_orders"), | ||||||
|  |         ], | ||||||
|  |         [InlineKeyboardButton(text="Назад", callback_data="profile_bybit")], | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def create_active_positions_keyboard(symbols: list): | ||||||
|  |     builder = InlineKeyboardBuilder() | ||||||
|  |     for sym, side in symbols: | ||||||
|  |         builder.button(text=f"{sym}:{side}", callback_data=f"get_position_{sym}_{side}") | ||||||
|  |     builder.button(text="Назад", callback_data="my_deals") | ||||||
|  |     builder.button(text="На главную", callback_data="profile_bybit") | ||||||
|  |     builder.adjust(2) | ||||||
|  |     return builder.as_markup() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def make_close_position_keyboard( | ||||||
|  |     symbol_pos: str, side: str, position_idx: int, qty: int | ||||||
|  | ): | ||||||
|  |     return InlineKeyboardMarkup( | ||||||
|  |         inline_keyboard=[ | ||||||
|  |             [ | ||||||
|  |                 InlineKeyboardButton( | ||||||
|  |                     text="Закрыть позицию", | ||||||
|  |                     callback_data=f"close_position_{symbol_pos}_{side}_{position_idx}_{qty}", | ||||||
|  |                 ) | ||||||
|  |             ], | ||||||
|  |             [ | ||||||
|  |                 InlineKeyboardButton( | ||||||
|  |                     text="Установить TP/SL", | ||||||
|  |                     callback_data=f"pos_tp_sl_{symbol_pos}_{position_idx}", | ||||||
|  |                 ) | ||||||
|  |             ], | ||||||
|  |             [ | ||||||
|  |                 InlineKeyboardButton(text="Назад", callback_data="change_position"), | ||||||
|  |                 InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), | ||||||
|  |             ], | ||||||
|  |         ] | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def create_active_orders_keyboard(orders: list): | ||||||
|  |     builder = InlineKeyboardBuilder() | ||||||
|  |     for order, side in orders: | ||||||
|  |         builder.button(text=f"{order}", callback_data=f"get_order_{order}_{side}") | ||||||
|  |     builder.button(text="Назад", callback_data="my_deals") | ||||||
|  |     builder.button(text="На главную", callback_data="profile_bybit") | ||||||
|  |     builder.adjust(2) | ||||||
|  |     return builder.as_markup() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def make_close_orders_keyboard(symbol_order: str, order_id: str): | ||||||
|  |     return InlineKeyboardMarkup( | ||||||
|  |         inline_keyboard=[ | ||||||
|  |             [ | ||||||
|  |                 InlineKeyboardButton( | ||||||
|  |                     text="Закрыть ордер", | ||||||
|  |                     callback_data=f"close_order_{symbol_order}_{order_id}", | ||||||
|  |                 ) | ||||||
|  |             ], | ||||||
|  |             [ | ||||||
|  |                 InlineKeyboardButton(text="Назад", callback_data="open_orders"), | ||||||
|  |                 InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), | ||||||
|  |             ], | ||||||
|  |         ] | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # START TRADING | ||||||
|  |  | ||||||
|  | back_to_start_trading = InlineKeyboardMarkup( | ||||||
|  |     inline_keyboard=[ | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton(text="Назад", callback_data="start_trading"), | ||||||
|  |             InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), | ||||||
|  |         ], | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | cancel_timer_merged = InlineKeyboardMarkup( | ||||||
|  |     inline_keyboard=[ | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton( | ||||||
|  |                 text="Отменить таймер", callback_data="cancel_timer_merged" | ||||||
|  |             ) | ||||||
|  |         ], | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), | ||||||
|  |         ], | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | cancel_timer_switch = InlineKeyboardMarkup( | ||||||
|  |     inline_keyboard=[ | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton( | ||||||
|  |                 text="Отменить таймер", callback_data="cancel_timer_switch" | ||||||
|  |             ) | ||||||
|  |         ], | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), | ||||||
|  |         ], | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | # STOP TRADING | ||||||
|  |  | ||||||
|  | cancel_timer_stop = InlineKeyboardMarkup( | ||||||
|  |     inline_keyboard=[ | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton( | ||||||
|  |                 text="Отменить таймер", callback_data="cancel_timer_stop" | ||||||
|  |             ) | ||||||
|  |         ], | ||||||
|  |         [ | ||||||
|  |             InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), | ||||||
|  |         ], | ||||||
|  |     ] | ||||||
|  | ) | ||||||
							
								
								
									
										11
									
								
								app/telegram/keyboards/reply.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								app/telegram/keyboards/reply.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | from aiogram.types import KeyboardButton, ReplyKeyboardMarkup | ||||||
|  |  | ||||||
|  | profile = ReplyKeyboardMarkup( | ||||||
|  |     keyboard=[ | ||||||
|  |         [KeyboardButton(text="Панель Bybit"), KeyboardButton(text="Профиль")], | ||||||
|  |         [KeyboardButton(text="Подключить платформу Bybit")], | ||||||
|  |     ], | ||||||
|  |     resize_keyboard=True, | ||||||
|  |     one_time_keyboard=True, | ||||||
|  |     input_field_placeholder="Выберите пункт меню...", | ||||||
|  | ) | ||||||
| @@ -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 | 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) | ||||||
							
								
								
									
										180
									
								
								database/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								database/models.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,180 @@ | |||||||
|  | from sqlalchemy.ext.declarative import declarative_base | ||||||
|  | from sqlalchemy.ext.asyncio import AsyncAttrs | ||||||
|  | from sqlalchemy import Column, ForeignKey, Integer, String, Float, Boolean, UniqueConstraint | ||||||
|  | from sqlalchemy.orm import relationship | ||||||
|  |  | ||||||
|  | Base = declarative_base(cls=AsyncAttrs) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class User(Base): | ||||||
|  |     """User model.""" | ||||||
|  |     __tablename__ = "users" | ||||||
|  |  | ||||||
|  |     id = Column(Integer, primary_key=True, autoincrement=True) | ||||||
|  |     tg_id = Column(Integer, nullable=False, unique=True) | ||||||
|  |     username = Column(String, nullable=False) | ||||||
|  |  | ||||||
|  |     user_api = relationship("UserApi", | ||||||
|  |                             back_populates="user", | ||||||
|  |                             cascade="all, delete-orphan", | ||||||
|  |                             passive_deletes=True, | ||||||
|  |                             uselist=False) | ||||||
|  |  | ||||||
|  |     user_symbol = relationship("UserSymbol", | ||||||
|  |                                back_populates="user", | ||||||
|  |                                cascade="all, delete-orphan", | ||||||
|  |                                passive_deletes=True, | ||||||
|  |                                uselist=False) | ||||||
|  |  | ||||||
|  |     user_additional_settings = relationship("UserAdditionalSettings", | ||||||
|  |                                             back_populates="user", | ||||||
|  |                                             cascade="all, delete-orphan", | ||||||
|  |                                             passive_deletes=True, | ||||||
|  |                                             uselist=False) | ||||||
|  |  | ||||||
|  |     user_risk_management = relationship("UserRiskManagement", | ||||||
|  |                                         back_populates="user", | ||||||
|  |                                         cascade="all, delete-orphan", | ||||||
|  |                                         passive_deletes=True, | ||||||
|  |                                         uselist=False) | ||||||
|  |  | ||||||
|  |     user_conditional_settings = relationship("UserConditionalSettings", | ||||||
|  |                                              back_populates="user", | ||||||
|  |                                              cascade="all, delete-orphan", | ||||||
|  |                                              passive_deletes=True, | ||||||
|  |                                              uselist=False) | ||||||
|  |  | ||||||
|  |     user_deals = relationship("UserDeals", | ||||||
|  |                               back_populates="user", | ||||||
|  |                               cascade="all, delete-orphan", | ||||||
|  |                               passive_deletes=True) | ||||||
|  |  | ||||||
|  |     user_auto_trading = relationship("UserAutoTrading", | ||||||
|  |                                      back_populates="user", | ||||||
|  |                                      cascade="all, delete-orphan", | ||||||
|  |                                      passive_deletes=True) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class UserApi(Base): | ||||||
|  |     """User API model.""" | ||||||
|  |     __tablename__ = "user_api" | ||||||
|  |  | ||||||
|  |     id = Column(Integer, primary_key=True, autoincrement=True) | ||||||
|  |     user_id = Column(Integer, | ||||||
|  |                      ForeignKey("users.id", ondelete="CASCADE"), | ||||||
|  |                      nullable=False, unique=True) | ||||||
|  |     api_key = Column(String, nullable=False) | ||||||
|  |     api_secret = Column(String, nullable=False) | ||||||
|  |  | ||||||
|  |     user = relationship("User", back_populates="user_api") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class UserSymbol(Base): | ||||||
|  |     """User symbol model.""" | ||||||
|  |     __tablename__ = "user_symbol" | ||||||
|  |  | ||||||
|  |     id = Column(Integer, primary_key=True, autoincrement=True) | ||||||
|  |     user_id = Column(Integer, | ||||||
|  |                      ForeignKey("users.id", ondelete="CASCADE"), | ||||||
|  |                      nullable=False, unique=True) | ||||||
|  |     symbol = Column(String, nullable=False, default="BTCUSDT") | ||||||
|  |  | ||||||
|  |     user = relationship("User", back_populates="user_symbol") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class UserAdditionalSettings(Base): | ||||||
|  |     """User additional settings model.""" | ||||||
|  |     __tablename__ = "user_additional_settings" | ||||||
|  |  | ||||||
|  |     id = Column(Integer, primary_key=True, autoincrement=True) | ||||||
|  |     user_id = Column(Integer, | ||||||
|  |                      ForeignKey("users.id", ondelete="CASCADE"), | ||||||
|  |                      nullable=False, unique=True) | ||||||
|  |     trade_mode = Column(String, nullable=False, default="Merged_Single") | ||||||
|  |     switch_side = Column(String, nullable=False, default="По направлению") | ||||||
|  |     side = Column(String, nullable=False, default="Buy") | ||||||
|  |     trigger_price = Column(Float, nullable=False, default=0.0) | ||||||
|  |     margin_type = Column(String, nullable=False, default="ISOLATED_MARGIN") | ||||||
|  |     leverage = Column(String, nullable=False, default="10") | ||||||
|  |     order_quantity = Column(Float, nullable=False, default=5.0) | ||||||
|  |     martingale_factor = Column(Float, nullable=False, default=1.0) | ||||||
|  |     max_bets_in_series = Column(Integer, nullable=False, default=1) | ||||||
|  |  | ||||||
|  |     user = relationship("User", back_populates="user_additional_settings") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class UserRiskManagement(Base): | ||||||
|  |     """User risk management model.""" | ||||||
|  |     __tablename__ = "user_risk_management" | ||||||
|  |  | ||||||
|  |     id = Column(Integer, primary_key=True, autoincrement=True) | ||||||
|  |     user_id = Column(Integer, | ||||||
|  |                      ForeignKey("users.id", ondelete="CASCADE"), | ||||||
|  |                      nullable=False, unique=True) | ||||||
|  |     take_profit_percent = Column(Float, nullable=False, default=1) | ||||||
|  |     stop_loss_percent = Column(Float, nullable=False, default=1) | ||||||
|  |     commission_fee = Column(String, nullable=False, default="Yes_commission_fee") | ||||||
|  |  | ||||||
|  |     user = relationship("User", back_populates="user_risk_management") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class UserConditionalSettings(Base): | ||||||
|  |     """User conditional settings model.""" | ||||||
|  |     __tablename__ = "user_conditional_settings" | ||||||
|  |  | ||||||
|  |     id = Column(Integer, primary_key=True, autoincrement=True) | ||||||
|  |     user_id = Column(Integer, | ||||||
|  |                      ForeignKey("users.id", ondelete="CASCADE"), | ||||||
|  |                      nullable=False, unique=True) | ||||||
|  |  | ||||||
|  |     timer_start = Column(Integer, nullable=False, default=0) | ||||||
|  |     timer_end = Column(Integer, nullable=False, default=0) | ||||||
|  |  | ||||||
|  |     user = relationship("User", back_populates="user_conditional_settings") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class UserDeals(Base): | ||||||
|  |     """User deals model.""" | ||||||
|  |     __tablename__ = "user_deals" | ||||||
|  |  | ||||||
|  |     id = Column(Integer, primary_key=True, autoincrement=True) | ||||||
|  |     user_id = Column(Integer, | ||||||
|  |                      ForeignKey("users.id", ondelete="CASCADE"), | ||||||
|  |                      nullable=False) | ||||||
|  |     current_step = Column(Integer, nullable=True) | ||||||
|  |     symbol = Column(String, nullable=True) | ||||||
|  |     trade_mode = Column(String, nullable=True) | ||||||
|  |     side_mode = Column(String, nullable=True) | ||||||
|  |     base_quantity = Column(Float, nullable=True) | ||||||
|  |     margin_type = Column(String, nullable=True) | ||||||
|  |     leverage = Column(String, nullable=True) | ||||||
|  |     last_side = Column(String, nullable=True) | ||||||
|  |     closed_side = Column(String, nullable=True) | ||||||
|  |     order_quantity = Column(Float, nullable=True) | ||||||
|  |     martingale_factor = Column(Float, nullable=True) | ||||||
|  |     max_bets_in_series = Column(Integer, nullable=True) | ||||||
|  |     take_profit_percent = Column(Integer, nullable=True) | ||||||
|  |     stop_loss_percent = Column(Integer, nullable=True) | ||||||
|  |     trigger_price = Column(Float, nullable=True) | ||||||
|  |  | ||||||
|  |     user = relationship("User", back_populates="user_deals") | ||||||
|  |  | ||||||
|  |     __table_args__ = ( | ||||||
|  |         UniqueConstraint('user_id', 'symbol', name='uq_user_symbol'), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class UserAutoTrading(Base): | ||||||
|  |     """User auto trading model.""" | ||||||
|  |     __tablename__ = "user_auto_trading" | ||||||
|  |  | ||||||
|  |     id = Column(Integer, primary_key=True, autoincrement=True) | ||||||
|  |     user_id = Column(Integer, | ||||||
|  |                      ForeignKey("users.id", ondelete="CASCADE"), | ||||||
|  |                      nullable=False) | ||||||
|  |     symbol = Column(String, nullable=True) | ||||||
|  |     auto_trading = Column(Boolean, nullable=True) | ||||||
|  |     fee = Column(Float, nullable=True) | ||||||
|  |     total_fee = Column(Float, nullable=True) | ||||||
|  |  | ||||||
|  |     user = relationship("User", back_populates="user_auto_trading") | ||||||
							
								
								
									
										1270
									
								
								database/request.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1270
									
								
								database/request.py
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										17
									
								
								examples/systemd/stcs.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								examples/systemd/stcs.service
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | [Unit] | ||||||
|  | Description=Telegram chat-robot: @stcs_cryptobot | ||||||
|  |  | ||||||
|  | Wants=network.target | ||||||
|  | After=syslog.target network-online.target | ||||||
|  |  | ||||||
|  | [Service] | ||||||
|  | ExecStart=sudo -u www-data /usr/bin/python3 /var/www/stcs/BybitBot_API.py | ||||||
|  | PIDFile=/var/run/python/stcs.pid | ||||||
|  | RemainAfterExit=no | ||||||
|  | RuntimeMaxSec=3600s | ||||||
|  | Restart=always | ||||||
|  | RestartSec=5s | ||||||
|  |  | ||||||
|  | [Install] | ||||||
|  | WantedBy=multi-user.target | ||||||
|  |  | ||||||
							
								
								
									
										0
									
								
								logger_helper/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								logger_helper/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										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