forked from kodorvan/stcs
		
	Compare commits
	
		
			174 Commits
		
	
	
		
			b3119c6ee1
			...
			stable
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 88c358b90e | |||
| 
						 | 
					3ba32660ea | ||
| 
						 | 
					e0167ea406 | ||
| 
						 | 
					dbf0a30d54 | ||
| a7a23a4662 | |||
| 
						 | 
					78f21e6718 | ||
| 
						 | 
					6416bd6dc9 | ||
| 
						 | 
					29c168e31d | ||
| 245cadf650 | |||
| 
						 | 
					e043a2429f | ||
| 
						 | 
					a8119d2811 | ||
| 
						 | 
					7e4c936ef5 | ||
| 
						 | 
					d8866af185 | ||
| 8d32439a15 | |||
| 
						 | 
					9497cca3e0 | ||
| 690d793e8c | |||
| 
						 | 
					a0ef48810a | ||
| ab752b5dd8 | |||
| 
						 | 
					a2164853d9 | ||
| 
						 | 
					92bb052151 | ||
| ca7bd5c795 | |||
| 
						 | 
					62e923cefa | ||
| 
						 | 
					faae2475c1 | ||
| 
						 | 
					3ef8eae997 | ||
| 
						 | 
					2bebada215 | ||
| 
						 | 
					8be1636279 | ||
| 
						 | 
					b756aadf26 | ||
| 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 | |||
| 15e248d7d7 | |||
| cdb745d55a | |||
| c46a4cb0b7 | 
@@ -1,3 +1 @@
 | 
			
		||||
TOKEN_TELEGRAM_BOT_1=
 | 
			
		||||
TOKEN_TELEGRAM_BOT_2=
 | 
			
		||||
TOKEN_TELEGRAM_BOT_3=
 | 
			
		||||
BOT_TOKEN=YOUR_BOT_TOKEN
 | 
			
		||||
							
								
								
									
										215
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										215
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,13 +1,212 @@
 | 
			
		||||
.env
 | 
			
		||||
!*.sample
 | 
			
		||||
 | 
			
		||||
# Byte-compiled / optimized / DLL files
 | 
			
		||||
__pycache__/
 | 
			
		||||
*.pyc
 | 
			
		||||
*.py[codz]
 | 
			
		||||
*$py.class
 | 
			
		||||
 | 
			
		||||
env/
 | 
			
		||||
venv/
 | 
			
		||||
.venv/
 | 
			
		||||
# C extensions
 | 
			
		||||
*.so
 | 
			
		||||
 | 
			
		||||
# Distribution / packaging
 | 
			
		||||
.Python
 | 
			
		||||
build/
 | 
			
		||||
develop-eggs/
 | 
			
		||||
dist/
 | 
			
		||||
downloads/
 | 
			
		||||
eggs/
 | 
			
		||||
.eggs/
 | 
			
		||||
lib/
 | 
			
		||||
lib64/
 | 
			
		||||
parts/
 | 
			
		||||
sdist/
 | 
			
		||||
var/
 | 
			
		||||
wheels/
 | 
			
		||||
share/python-wheels/
 | 
			
		||||
*.egg-info/
 | 
			
		||||
.installed.cfg
 | 
			
		||||
*.egg
 | 
			
		||||
MANIFEST
 | 
			
		||||
 | 
			
		||||
# PyInstaller
 | 
			
		||||
#  Usually these files are written by a python script from a template
 | 
			
		||||
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
 | 
			
		||||
*.manifest
 | 
			
		||||
*.spec
 | 
			
		||||
 | 
			
		||||
# Installer logs
 | 
			
		||||
pip-log.txt
 | 
			
		||||
pip-delete-this-directory.txt
 | 
			
		||||
 | 
			
		||||
# Unit test / coverage reports
 | 
			
		||||
htmlcov/
 | 
			
		||||
.tox/
 | 
			
		||||
.nox/
 | 
			
		||||
.coverage
 | 
			
		||||
.coverage.*
 | 
			
		||||
.cache
 | 
			
		||||
nosetests.xml
 | 
			
		||||
coverage.xml
 | 
			
		||||
*.cover
 | 
			
		||||
*.py.cover
 | 
			
		||||
.hypothesis/
 | 
			
		||||
.pytest_cache/
 | 
			
		||||
cover/
 | 
			
		||||
 | 
			
		||||
# Translations
 | 
			
		||||
*.mo
 | 
			
		||||
*.pot
 | 
			
		||||
 | 
			
		||||
# Django stuff:
 | 
			
		||||
*.log
 | 
			
		||||
local_settings.py
 | 
			
		||||
db.sqlite3
 | 
			
		||||
db.sqlite3-journal
 | 
			
		||||
 | 
			
		||||
# Flask stuff:
 | 
			
		||||
instance/
 | 
			
		||||
.webassets-cache
 | 
			
		||||
 | 
			
		||||
# Scrapy stuff:
 | 
			
		||||
.scrapy
 | 
			
		||||
 | 
			
		||||
# Sphinx documentation
 | 
			
		||||
docs/_build/
 | 
			
		||||
 | 
			
		||||
# PyBuilder
 | 
			
		||||
.pybuilder/
 | 
			
		||||
target/
 | 
			
		||||
 | 
			
		||||
# Jupyter Notebook
 | 
			
		||||
.ipynb_checkpoints
 | 
			
		||||
 | 
			
		||||
# IPython
 | 
			
		||||
profile_default/
 | 
			
		||||
ipython_config.py
 | 
			
		||||
 | 
			
		||||
# pyenv
 | 
			
		||||
#   For a library or package, you might want to ignore these files since the code is
 | 
			
		||||
#   intended to run in multiple environments; otherwise, check them in:
 | 
			
		||||
# .python-version
 | 
			
		||||
 | 
			
		||||
# pipenv
 | 
			
		||||
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
 | 
			
		||||
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
 | 
			
		||||
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
 | 
			
		||||
#   install all needed dependencies.
 | 
			
		||||
#Pipfile.lock
 | 
			
		||||
 | 
			
		||||
# UV
 | 
			
		||||
#   Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
 | 
			
		||||
#   This is especially recommended for binary packages to ensure reproducibility, and is more
 | 
			
		||||
#   commonly ignored for libraries.
 | 
			
		||||
#uv.lock
 | 
			
		||||
 | 
			
		||||
# poetry
 | 
			
		||||
#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
 | 
			
		||||
#   This is especially recommended for binary packages to ensure reproducibility, and is more
 | 
			
		||||
#   commonly ignored for libraries.
 | 
			
		||||
#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
 | 
			
		||||
#poetry.lock
 | 
			
		||||
#poetry.toml
 | 
			
		||||
 | 
			
		||||
# pdm
 | 
			
		||||
#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
 | 
			
		||||
#   pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
 | 
			
		||||
#   https://pdm-project.org/en/latest/usage/project/#working-with-version-control
 | 
			
		||||
#pdm.lock
 | 
			
		||||
#pdm.toml
 | 
			
		||||
.pdm-python
 | 
			
		||||
.pdm-build/
 | 
			
		||||
 | 
			
		||||
# pixi
 | 
			
		||||
#   Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
 | 
			
		||||
#pixi.lock
 | 
			
		||||
#   Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
 | 
			
		||||
#   in the .venv directory. It is recommended not to include this directory in version control.
 | 
			
		||||
.pixi
 | 
			
		||||
 | 
			
		||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
 | 
			
		||||
__pypackages__/
 | 
			
		||||
 | 
			
		||||
# Celery stuff
 | 
			
		||||
celerybeat-schedule
 | 
			
		||||
celerybeat.pid
 | 
			
		||||
 | 
			
		||||
# SageMath parsed files
 | 
			
		||||
*.sage.py
 | 
			
		||||
 | 
			
		||||
# Environments
 | 
			
		||||
.idea
 | 
			
		||||
/.idea
 | 
			
		||||
/myenv
 | 
			
		||||
.env
 | 
			
		||||
.envrc
 | 
			
		||||
.venv
 | 
			
		||||
env/
 | 
			
		||||
venv/
 | 
			
		||||
myenv
 | 
			
		||||
ENV/
 | 
			
		||||
env.bak/
 | 
			
		||||
venv.bak/
 | 
			
		||||
/logger_helper/loggers
 | 
			
		||||
/app/bybit/logger_bybit/loggers
 | 
			
		||||
*.db
 | 
			
		||||
# Spyder project settings
 | 
			
		||||
.spyderproject
 | 
			
		||||
.spyproject
 | 
			
		||||
 | 
			
		||||
# Rope project settings
 | 
			
		||||
.ropeproject
 | 
			
		||||
 | 
			
		||||
# mkdocs documentation
 | 
			
		||||
/site
 | 
			
		||||
 | 
			
		||||
# mypy
 | 
			
		||||
.mypy_cache/
 | 
			
		||||
.dmypy.json
 | 
			
		||||
dmypy.json
 | 
			
		||||
 | 
			
		||||
# Pyre type checker
 | 
			
		||||
.pyre/
 | 
			
		||||
 | 
			
		||||
# pytype static type analyzer
 | 
			
		||||
.pytype/
 | 
			
		||||
 | 
			
		||||
# Cython debug symbols
 | 
			
		||||
cython_debug/
 | 
			
		||||
 | 
			
		||||
# PyCharm
 | 
			
		||||
#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
 | 
			
		||||
#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
 | 
			
		||||
#  and can be added to the global gitignore or merged into this file.  For a more nuclear
 | 
			
		||||
#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
 | 
			
		||||
#.idea/
 | 
			
		||||
 | 
			
		||||
# Abstra
 | 
			
		||||
# Abstra is an AI-powered process automation framework.
 | 
			
		||||
# Ignore directories containing user credentials, local state, and settings.
 | 
			
		||||
# Learn more at https://abstra.io/docs
 | 
			
		||||
.abstra/
 | 
			
		||||
 | 
			
		||||
# Visual Studio Code
 | 
			
		||||
#  Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore 
 | 
			
		||||
#  that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
 | 
			
		||||
#  and can be added to the global gitignore or merged into this file. However, if you prefer, 
 | 
			
		||||
#  you could uncomment the following to ignore the entire vscode folder
 | 
			
		||||
# .vscode/
 | 
			
		||||
 | 
			
		||||
# Ruff stuff:
 | 
			
		||||
.ruff_cache/
 | 
			
		||||
 | 
			
		||||
# PyPI configuration file
 | 
			
		||||
.pypirc
 | 
			
		||||
 | 
			
		||||
# Cursor
 | 
			
		||||
#  Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
 | 
			
		||||
#  exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
 | 
			
		||||
#  refer to https://docs.cursor.com/context/ignore-files
 | 
			
		||||
.cursorignore
 | 
			
		||||
.cursorindexingignore
 | 
			
		||||
 | 
			
		||||
# Marimo
 | 
			
		||||
marimo/_static/
 | 
			
		||||
marimo/_lsp/
 | 
			
		||||
__marimo__/
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								.idea/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.idea/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
								
							@@ -1,5 +0,0 @@
 | 
			
		||||
# Default ignored files
 | 
			
		||||
/shelf/
 | 
			
		||||
/workspace.xml
 | 
			
		||||
# Editor-based HTTP Client requests
 | 
			
		||||
/httpRequests/
 | 
			
		||||
							
								
								
									
										610
									
								
								.idea/dbnavigator.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										610
									
								
								.idea/dbnavigator.xml
									
									
									
										generated
									
									
									
								
							@@ -1,610 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project version="4">
 | 
			
		||||
  <component name="DBNavigator.Project.DDLFileAttachmentManager">
 | 
			
		||||
    <mappings />
 | 
			
		||||
    <preferences />
 | 
			
		||||
  </component>
 | 
			
		||||
  <component name="DBNavigator.Project.DataEditorManager">
 | 
			
		||||
    <record-view-column-sorting-type value="BY_INDEX" />
 | 
			
		||||
    <value-preview-text-wrapping value="false" />
 | 
			
		||||
    <value-preview-pinned value="false" />
 | 
			
		||||
  </component>
 | 
			
		||||
  <component name="DBNavigator.Project.DatabaseAssistantManager">
 | 
			
		||||
    <assistants>
 | 
			
		||||
      <assistant-state connection-id="539567a8-9e28-4dbe-b015-5c44ee3cfda5" default-profile-name="" selected-conversation-id="" assistant-type="GENERIC" availability="UNAVAILABLE" acknowledgement="NONE">
 | 
			
		||||
        <conversations />
 | 
			
		||||
      </assistant-state>
 | 
			
		||||
      <assistant-state connection-id="0f4bac5e-2797-4db3-bad2-53cbf8cd2b20" default-profile-name="" selected-conversation-id="" assistant-type="GENERIC" availability="UNAVAILABLE" acknowledgement="NONE">
 | 
			
		||||
        <conversations />
 | 
			
		||||
      </assistant-state>
 | 
			
		||||
      <assistant-state connection-id="be96bc1e-ca0e-4a7f-a163-9127fdee15c6" default-profile-name="" selected-conversation-id="" assistant-type="GENERIC" availability="UNAVAILABLE" acknowledgement="NONE">
 | 
			
		||||
        <conversations />
 | 
			
		||||
      </assistant-state>
 | 
			
		||||
      <assistant-state connection-id="8d8de23e-fc36-46b8-a593-c47740aca91b" default-profile-name="" selected-conversation-id="" assistant-type="GENERIC" availability="UNAVAILABLE" acknowledgement="NONE">
 | 
			
		||||
        <conversations />
 | 
			
		||||
      </assistant-state>
 | 
			
		||||
      <assistant-state connection-id="d0ab0625-5cb0-46d3-9aa4-4d4b02bd8890" default-profile-name="" selected-conversation-id="" assistant-type="GENERIC" availability="UNAVAILABLE" acknowledgement="NONE">
 | 
			
		||||
        <conversations />
 | 
			
		||||
      </assistant-state>
 | 
			
		||||
      <assistant-state connection-id="4a35d891-e512-4ca8-85c4-6f6647deefbc" default-profile-name="" selected-conversation-id="" assistant-type="GENERIC" availability="UNAVAILABLE" acknowledgement="NONE">
 | 
			
		||||
        <conversations />
 | 
			
		||||
      </assistant-state>
 | 
			
		||||
      <assistant-state connection-id="431aa026-563e-40e3-a8f7-50e01f1f1af7" default-profile-name="" selected-conversation-id="" assistant-type="GENERIC" availability="UNAVAILABLE" acknowledgement="NONE">
 | 
			
		||||
        <conversations />
 | 
			
		||||
      </assistant-state>
 | 
			
		||||
      <assistant-state connection-id="f5fd2325-996b-4e52-a9f8-b1dcee5d149d" default-profile-name="" selected-conversation-id="" assistant-type="GENERIC" availability="UNAVAILABLE" acknowledgement="NONE">
 | 
			
		||||
        <conversations />
 | 
			
		||||
      </assistant-state>
 | 
			
		||||
    </assistants>
 | 
			
		||||
  </component>
 | 
			
		||||
  <component name="DBNavigator.Project.DatabaseBrowserManager">
 | 
			
		||||
    <autoscroll-to-editor value="false" />
 | 
			
		||||
    <autoscroll-from-editor value="true" />
 | 
			
		||||
    <show-object-properties value="false" />
 | 
			
		||||
    <loaded-nodes />
 | 
			
		||||
  </component>
 | 
			
		||||
  <component name="DBNavigator.Project.DatabaseConsoleManager">
 | 
			
		||||
    <connection id="0f4bac5e-2797-4db3-bad2-53cbf8cd2b20">
 | 
			
		||||
      <console name="Connection" type="STANDARD" schema="main" session="Main" />
 | 
			
		||||
    </connection>
 | 
			
		||||
  </component>
 | 
			
		||||
  <component name="DBNavigator.Project.DatabaseEditorStateManager">
 | 
			
		||||
    <last-used-providers />
 | 
			
		||||
  </component>
 | 
			
		||||
  <component name="DBNavigator.Project.DatabaseFileManager">
 | 
			
		||||
    <open-files />
 | 
			
		||||
  </component>
 | 
			
		||||
  <component name="DBNavigator.Project.DatabaseSessionManager">
 | 
			
		||||
    <connection id="0f4bac5e-2797-4db3-bad2-53cbf8cd2b20" />
 | 
			
		||||
  </component>
 | 
			
		||||
  <component name="DBNavigator.Project.DatasetFilterManager">
 | 
			
		||||
    <filter-actions connection-id="0f4bac5e-2797-4db3-bad2-53cbf8cd2b20" dataset="main.user_bybit_api" active-filter-id="EMPTY_FILTER" />
 | 
			
		||||
  </component>
 | 
			
		||||
  <component name="DBNavigator.Project.ExecutionManager">
 | 
			
		||||
    <retain-sticky-names value="false" />
 | 
			
		||||
  </component>
 | 
			
		||||
  <component name="DBNavigator.Project.MethodExecutionManager">
 | 
			
		||||
    <method-browser />
 | 
			
		||||
    <execution-history>
 | 
			
		||||
      <group-entries value="true" />
 | 
			
		||||
      <execution-inputs />
 | 
			
		||||
    </execution-history>
 | 
			
		||||
    <execution-variables-cache />
 | 
			
		||||
  </component>
 | 
			
		||||
  <component name="DBNavigator.Project.ObjectQuickFilterManager">
 | 
			
		||||
    <last-used-operator value="EQUAL" />
 | 
			
		||||
    <filters />
 | 
			
		||||
  </component>
 | 
			
		||||
  <component name="DBNavigator.Project.Settings">
 | 
			
		||||
    <connections>
 | 
			
		||||
      <connection source-id="" id="0f4bac5e-2797-4db3-bad2-53cbf8cd2b20" active="true" signed="true">
 | 
			
		||||
        <database>
 | 
			
		||||
          <name value="Connection" />
 | 
			
		||||
          <description value="" />
 | 
			
		||||
          <database-type value="SQLITE" />
 | 
			
		||||
          <config-type value="BASIC" />
 | 
			
		||||
          <database-version value="3.5" />
 | 
			
		||||
          <driver-source value="BUNDLED" />
 | 
			
		||||
          <driver-library value="" />
 | 
			
		||||
          <driver value="" />
 | 
			
		||||
          <url-type value="FILE" />
 | 
			
		||||
          <host value="" />
 | 
			
		||||
          <port value="" />
 | 
			
		||||
          <database value="" />
 | 
			
		||||
          <tns-folder value="" />
 | 
			
		||||
          <tns-profile value="" />
 | 
			
		||||
          <server-type value="" />
 | 
			
		||||
          <protocol value="" />
 | 
			
		||||
          <url-parameters />
 | 
			
		||||
          <files>
 | 
			
		||||
            <file path="C:\Users\Algiz_N\PycharmProjects\stcs\db.sqlite3" schema="main" />
 | 
			
		||||
          </files>
 | 
			
		||||
          <type value="NONE" />
 | 
			
		||||
          <user value="" />
 | 
			
		||||
          <token-type value="" />
 | 
			
		||||
          <token-config-file value="" />
 | 
			
		||||
          <token-profile value="" />
 | 
			
		||||
          <session-user value="" />
 | 
			
		||||
        </database>
 | 
			
		||||
        <properties>
 | 
			
		||||
          <auto-commit value="true" />
 | 
			
		||||
        </properties>
 | 
			
		||||
        <ssh-settings>
 | 
			
		||||
          <active value="false" />
 | 
			
		||||
          <proxy-host value="" />
 | 
			
		||||
          <proxy-port value="22" />
 | 
			
		||||
          <proxy-user value="" />
 | 
			
		||||
          <auth-type value="PASSWORD" />
 | 
			
		||||
          <key-file value="" />
 | 
			
		||||
        </ssh-settings>
 | 
			
		||||
        <ssl-settings>
 | 
			
		||||
          <active value="false" />
 | 
			
		||||
          <certificate-authority-file value="" />
 | 
			
		||||
          <client-certificate-file value="" />
 | 
			
		||||
          <client-key-file value="" />
 | 
			
		||||
        </ssl-settings>
 | 
			
		||||
        <details>
 | 
			
		||||
          <charset value="UTF-8" />
 | 
			
		||||
          <session-management value="true" />
 | 
			
		||||
          <ddl-file-binding value="true" />
 | 
			
		||||
          <database-logging value="true" />
 | 
			
		||||
          <connect-automatically value="true" />
 | 
			
		||||
          <restore-workspace value="true" />
 | 
			
		||||
          <restore-workspace-deep value="false" />
 | 
			
		||||
          <environment-type value="default" />
 | 
			
		||||
          <connectivity-timeout value="30" />
 | 
			
		||||
          <idle-time-to-disconnect value="30" />
 | 
			
		||||
          <idle-time-to-disconnect-pool value="5" />
 | 
			
		||||
          <credential-expiry-time value="10" />
 | 
			
		||||
          <max-connection-pool-size value="7" />
 | 
			
		||||
          <alternative-statement-delimiter value="" />
 | 
			
		||||
        </details>
 | 
			
		||||
        <debugger>
 | 
			
		||||
          <compile-dependencies value="true" />
 | 
			
		||||
          <tcp-driver-tunneling value="false" />
 | 
			
		||||
          <tcp-host-address value="" />
 | 
			
		||||
          <tcp-port-from value="4000" />
 | 
			
		||||
          <tcp-port-to value="4999" />
 | 
			
		||||
          <debugger-type value="JDBC" />
 | 
			
		||||
        </debugger>
 | 
			
		||||
        <object-filters hide-empty-schemas="false" hide-pseudo-columns="false" hide-audit-columns="false">
 | 
			
		||||
          <object-filters />
 | 
			
		||||
          <object-type-filter use-master-settings="true">
 | 
			
		||||
            <object-type name="SCHEMA" enabled="true" />
 | 
			
		||||
            <object-type name="USER" enabled="true" />
 | 
			
		||||
            <object-type name="ROLE" enabled="true" />
 | 
			
		||||
            <object-type name="PRIVILEGE" enabled="true" />
 | 
			
		||||
            <object-type name="CHARSET" enabled="true" />
 | 
			
		||||
            <object-type name="TABLE" enabled="true" />
 | 
			
		||||
            <object-type name="VIEW" enabled="true" />
 | 
			
		||||
            <object-type name="JSON_VIEW" enabled="true" />
 | 
			
		||||
            <object-type name="MATERIALIZED_VIEW" enabled="true" />
 | 
			
		||||
            <object-type name="NESTED_TABLE" enabled="true" />
 | 
			
		||||
            <object-type name="COLUMN" enabled="true" />
 | 
			
		||||
            <object-type name="INDEX" enabled="true" />
 | 
			
		||||
            <object-type name="CONSTRAINT" enabled="true" />
 | 
			
		||||
            <object-type name="DATASET_TRIGGER" enabled="true" />
 | 
			
		||||
            <object-type name="DATABASE_TRIGGER" enabled="true" />
 | 
			
		||||
            <object-type name="SYNONYM" enabled="true" />
 | 
			
		||||
            <object-type name="SEQUENCE" enabled="true" />
 | 
			
		||||
            <object-type name="PROCEDURE" enabled="true" />
 | 
			
		||||
            <object-type name="FUNCTION" enabled="true" />
 | 
			
		||||
            <object-type name="PACKAGE" enabled="true" />
 | 
			
		||||
            <object-type name="TYPE" enabled="true" />
 | 
			
		||||
            <object-type name="TYPE_ATTRIBUTE" enabled="true" />
 | 
			
		||||
            <object-type name="ARGUMENT" enabled="true" />
 | 
			
		||||
            <object-type name="JAVA_CLASS" enabled="true" />
 | 
			
		||||
            <object-type name="JAVA_FIELD" enabled="true" />
 | 
			
		||||
            <object-type name="JAVA_METHOD" enabled="true" />
 | 
			
		||||
            <object-type name="JAVA_RESOURCE" enabled="true" />
 | 
			
		||||
            <object-type name="DIMENSION" enabled="true" />
 | 
			
		||||
            <object-type name="CLUSTER" enabled="true" />
 | 
			
		||||
            <object-type name="DBLINK" enabled="true" />
 | 
			
		||||
            <object-type name="CREDENTIAL" enabled="true" />
 | 
			
		||||
            <object-type name="AI_PROFILE" enabled="true" />
 | 
			
		||||
          </object-type-filter>
 | 
			
		||||
        </object-filters>
 | 
			
		||||
      </connection>
 | 
			
		||||
    </connections>
 | 
			
		||||
    <browser-settings>
 | 
			
		||||
      <general>
 | 
			
		||||
        <display-mode value="TABBED" />
 | 
			
		||||
        <navigation-history-size value="100" />
 | 
			
		||||
        <show-object-details value="false" />
 | 
			
		||||
        <enable-sticky-paths value="true" />
 | 
			
		||||
        <enable-quick-filters value="false" />
 | 
			
		||||
      </general>
 | 
			
		||||
      <filters>
 | 
			
		||||
        <object-type-filter>
 | 
			
		||||
          <object-type name="SCHEMA" enabled="true" />
 | 
			
		||||
          <object-type name="USER" enabled="true" />
 | 
			
		||||
          <object-type name="ROLE" enabled="true" />
 | 
			
		||||
          <object-type name="PRIVILEGE" enabled="true" />
 | 
			
		||||
          <object-type name="CHARSET" enabled="true" />
 | 
			
		||||
          <object-type name="TABLE" enabled="true" />
 | 
			
		||||
          <object-type name="VIEW" enabled="true" />
 | 
			
		||||
          <object-type name="JSON_VIEW" enabled="true" />
 | 
			
		||||
          <object-type name="MATERIALIZED_VIEW" enabled="true" />
 | 
			
		||||
          <object-type name="NESTED_TABLE" enabled="true" />
 | 
			
		||||
          <object-type name="COLUMN" enabled="true" />
 | 
			
		||||
          <object-type name="INDEX" enabled="true" />
 | 
			
		||||
          <object-type name="CONSTRAINT" enabled="true" />
 | 
			
		||||
          <object-type name="DATASET_TRIGGER" enabled="true" />
 | 
			
		||||
          <object-type name="DATABASE_TRIGGER" enabled="true" />
 | 
			
		||||
          <object-type name="SYNONYM" enabled="true" />
 | 
			
		||||
          <object-type name="SEQUENCE" enabled="true" />
 | 
			
		||||
          <object-type name="PROCEDURE" enabled="true" />
 | 
			
		||||
          <object-type name="FUNCTION" enabled="true" />
 | 
			
		||||
          <object-type name="PACKAGE" enabled="true" />
 | 
			
		||||
          <object-type name="TYPE" enabled="true" />
 | 
			
		||||
          <object-type name="TYPE_ATTRIBUTE" enabled="true" />
 | 
			
		||||
          <object-type name="ARGUMENT" enabled="true" />
 | 
			
		||||
          <object-type name="JAVA_CLASS" enabled="true" />
 | 
			
		||||
          <object-type name="JAVA_FIELD" enabled="true" />
 | 
			
		||||
          <object-type name="JAVA_METHOD" enabled="true" />
 | 
			
		||||
          <object-type name="JAVA_RESOURCE" enabled="true" />
 | 
			
		||||
          <object-type name="DIMENSION" enabled="true" />
 | 
			
		||||
          <object-type name="CLUSTER" enabled="true" />
 | 
			
		||||
          <object-type name="DBLINK" enabled="true" />
 | 
			
		||||
          <object-type name="CREDENTIAL" enabled="true" />
 | 
			
		||||
          <object-type name="AI_PROFILE" enabled="true" />
 | 
			
		||||
        </object-type-filter>
 | 
			
		||||
      </filters>
 | 
			
		||||
      <sorting>
 | 
			
		||||
        <object-type name="COLUMN" sorting-type="NAME" />
 | 
			
		||||
        <object-type name="FUNCTION" sorting-type="NAME" />
 | 
			
		||||
        <object-type name="PROCEDURE" sorting-type="NAME" />
 | 
			
		||||
        <object-type name="ARGUMENT" sorting-type="POSITION" />
 | 
			
		||||
        <object-type name="TYPE ATTRIBUTE" sorting-type="POSITION" />
 | 
			
		||||
      </sorting>
 | 
			
		||||
      <default-editors>
 | 
			
		||||
        <object-type name="VIEW" editor-type="SELECTION" />
 | 
			
		||||
        <object-type name="PACKAGE" editor-type="SELECTION" />
 | 
			
		||||
        <object-type name="TYPE" editor-type="SELECTION" />
 | 
			
		||||
      </default-editors>
 | 
			
		||||
    </browser-settings>
 | 
			
		||||
    <navigation-settings>
 | 
			
		||||
      <lookup-filters>
 | 
			
		||||
        <lookup-objects>
 | 
			
		||||
          <object-type name="SCHEMA" enabled="true" />
 | 
			
		||||
          <object-type name="USER" enabled="false" />
 | 
			
		||||
          <object-type name="ROLE" enabled="false" />
 | 
			
		||||
          <object-type name="PRIVILEGE" enabled="false" />
 | 
			
		||||
          <object-type name="CHARSET" enabled="false" />
 | 
			
		||||
          <object-type name="TABLE" enabled="true" />
 | 
			
		||||
          <object-type name="VIEW" enabled="true" />
 | 
			
		||||
          <object-type name="JSON VIEW" enabled="true" />
 | 
			
		||||
          <object-type name="MATERIALIZED VIEW" enabled="true" />
 | 
			
		||||
          <object-type name="INDEX" enabled="true" />
 | 
			
		||||
          <object-type name="CONSTRAINT" enabled="true" />
 | 
			
		||||
          <object-type name="DATASET TRIGGER" enabled="true" />
 | 
			
		||||
          <object-type name="DATABASE TRIGGER" enabled="true" />
 | 
			
		||||
          <object-type name="SYNONYM" enabled="false" />
 | 
			
		||||
          <object-type name="SEQUENCE" enabled="true" />
 | 
			
		||||
          <object-type name="PROCEDURE" enabled="true" />
 | 
			
		||||
          <object-type name="FUNCTION" enabled="true" />
 | 
			
		||||
          <object-type name="PACKAGE" enabled="true" />
 | 
			
		||||
          <object-type name="TYPE" enabled="true" />
 | 
			
		||||
          <object-type name="JAVA CLASS" enabled="true" />
 | 
			
		||||
          <object-type name="INNER CLASS" enabled="true" />
 | 
			
		||||
          <object-type name="JAVA FIELD" enabled="true" />
 | 
			
		||||
          <object-type name="JAVA METHOD" enabled="true" />
 | 
			
		||||
          <object-type name="JAVA PARAMETER" enabled="true" />
 | 
			
		||||
          <object-type name="JAVA RESOURCE" enabled="true" />
 | 
			
		||||
          <object-type name="DIMENSION" enabled="false" />
 | 
			
		||||
          <object-type name="CLUSTER" enabled="false" />
 | 
			
		||||
          <object-type name="DBLINK" enabled="false" />
 | 
			
		||||
          <object-type name="CREDENTIAL" enabled="false" />
 | 
			
		||||
        </lookup-objects>
 | 
			
		||||
        <force-database-load value="false" />
 | 
			
		||||
        <prompt-connection-selection value="true" />
 | 
			
		||||
        <prompt-schema-selection value="true" />
 | 
			
		||||
      </lookup-filters>
 | 
			
		||||
    </navigation-settings>
 | 
			
		||||
    <dataset-grid-settings>
 | 
			
		||||
      <general>
 | 
			
		||||
        <enable-zooming value="true" />
 | 
			
		||||
        <enable-column-tooltip value="true" />
 | 
			
		||||
      </general>
 | 
			
		||||
      <sorting>
 | 
			
		||||
        <nulls-first value="true" />
 | 
			
		||||
        <max-sorting-columns value="4" />
 | 
			
		||||
      </sorting>
 | 
			
		||||
      <audit-columns>
 | 
			
		||||
        <column-names value="" />
 | 
			
		||||
        <visible value="true" />
 | 
			
		||||
        <editable value="false" />
 | 
			
		||||
      </audit-columns>
 | 
			
		||||
    </dataset-grid-settings>
 | 
			
		||||
    <dataset-editor-settings>
 | 
			
		||||
      <text-editor-popup>
 | 
			
		||||
        <active value="false" />
 | 
			
		||||
        <active-if-empty value="false" />
 | 
			
		||||
        <data-length-threshold value="100" />
 | 
			
		||||
        <popup-delay value="1000" />
 | 
			
		||||
      </text-editor-popup>
 | 
			
		||||
      <values-actions-popup>
 | 
			
		||||
        <show-popup-button value="true" />
 | 
			
		||||
        <element-count-threshold value="1000" />
 | 
			
		||||
        <data-length-threshold value="250" />
 | 
			
		||||
      </values-actions-popup>
 | 
			
		||||
      <general>
 | 
			
		||||
        <fetch-block-size value="100" />
 | 
			
		||||
        <fetch-timeout value="30" />
 | 
			
		||||
        <trim-whitespaces value="true" />
 | 
			
		||||
        <convert-empty-strings-to-null value="true" />
 | 
			
		||||
        <select-content-on-cell-edit value="true" />
 | 
			
		||||
        <large-value-preview-active value="true" />
 | 
			
		||||
      </general>
 | 
			
		||||
      <filters>
 | 
			
		||||
        <prompt-filter-dialog value="true" />
 | 
			
		||||
        <default-filter-type value="BASIC" />
 | 
			
		||||
      </filters>
 | 
			
		||||
      <qualified-text-editor text-length-threshold="300">
 | 
			
		||||
        <content-types>
 | 
			
		||||
          <content-type name="Text" enabled="true" />
 | 
			
		||||
          <content-type name="Properties" enabled="true" />
 | 
			
		||||
          <content-type name="XML" enabled="true" />
 | 
			
		||||
          <content-type name="DTD" enabled="true" />
 | 
			
		||||
          <content-type name="HTML" enabled="true" />
 | 
			
		||||
          <content-type name="XHTML" enabled="true" />
 | 
			
		||||
          <content-type name="SQL" enabled="true" />
 | 
			
		||||
          <content-type name="PL/SQL" enabled="true" />
 | 
			
		||||
          <content-type name="JSON" enabled="true" />
 | 
			
		||||
          <content-type name="JSON5" enabled="true" />
 | 
			
		||||
          <content-type name="YAML" enabled="true" />
 | 
			
		||||
        </content-types>
 | 
			
		||||
      </qualified-text-editor>
 | 
			
		||||
      <record-navigation>
 | 
			
		||||
        <navigation-target value="VIEWER" />
 | 
			
		||||
      </record-navigation>
 | 
			
		||||
    </dataset-editor-settings>
 | 
			
		||||
    <code-editor-settings>
 | 
			
		||||
      <general>
 | 
			
		||||
        <show-object-navigation-gutter value="false" />
 | 
			
		||||
        <show-spec-declaration-navigation-gutter value="true" />
 | 
			
		||||
        <enable-spellchecking value="true" />
 | 
			
		||||
        <enable-reference-spellchecking value="false" />
 | 
			
		||||
      </general>
 | 
			
		||||
      <confirmations>
 | 
			
		||||
        <save-changes value="false" />
 | 
			
		||||
        <revert-changes value="true" />
 | 
			
		||||
        <exit-on-changes value="ASK" />
 | 
			
		||||
      </confirmations>
 | 
			
		||||
    </code-editor-settings>
 | 
			
		||||
    <code-completion-settings>
 | 
			
		||||
      <filters>
 | 
			
		||||
        <basic-filter>
 | 
			
		||||
          <filter-element type="RESERVED_WORD" id="keyword" selected="true" />
 | 
			
		||||
          <filter-element type="RESERVED_WORD" id="function" selected="true" />
 | 
			
		||||
          <filter-element type="RESERVED_WORD" id="parameter" selected="true" />
 | 
			
		||||
          <filter-element type="RESERVED_WORD" id="datatype" selected="true" />
 | 
			
		||||
          <filter-element type="RESERVED_WORD" id="exception" selected="true" />
 | 
			
		||||
          <filter-element type="OBJECT" id="schema" selected="true" />
 | 
			
		||||
          <filter-element type="OBJECT" id="role" selected="true" />
 | 
			
		||||
          <filter-element type="OBJECT" id="user" selected="true" />
 | 
			
		||||
          <filter-element type="OBJECT" id="privilege" selected="true" />
 | 
			
		||||
          <user-schema>
 | 
			
		||||
            <filter-element type="OBJECT" id="table" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="view" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="json view" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="materialized view" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="index" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="constraint" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="trigger" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="synonym" selected="false" />
 | 
			
		||||
            <filter-element type="OBJECT" id="sequence" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="procedure" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="function" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="package" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="type" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="dimension" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="cluster" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="dblink" selected="true" />
 | 
			
		||||
          </user-schema>
 | 
			
		||||
          <public-schema>
 | 
			
		||||
            <filter-element type="OBJECT" id="table" selected="false" />
 | 
			
		||||
            <filter-element type="OBJECT" id="view" selected="false" />
 | 
			
		||||
            <filter-element type="OBJECT" id="json view" selected="false" />
 | 
			
		||||
            <filter-element type="OBJECT" id="materialized view" selected="false" />
 | 
			
		||||
            <filter-element type="OBJECT" id="index" selected="false" />
 | 
			
		||||
            <filter-element type="OBJECT" id="constraint" selected="false" />
 | 
			
		||||
            <filter-element type="OBJECT" id="trigger" selected="false" />
 | 
			
		||||
            <filter-element type="OBJECT" id="synonym" selected="false" />
 | 
			
		||||
            <filter-element type="OBJECT" id="sequence" selected="false" />
 | 
			
		||||
            <filter-element type="OBJECT" id="procedure" selected="false" />
 | 
			
		||||
            <filter-element type="OBJECT" id="function" selected="false" />
 | 
			
		||||
            <filter-element type="OBJECT" id="package" selected="false" />
 | 
			
		||||
            <filter-element type="OBJECT" id="type" selected="false" />
 | 
			
		||||
            <filter-element type="OBJECT" id="dimension" selected="false" />
 | 
			
		||||
            <filter-element type="OBJECT" id="cluster" selected="false" />
 | 
			
		||||
            <filter-element type="OBJECT" id="dblink" selected="false" />
 | 
			
		||||
          </public-schema>
 | 
			
		||||
          <any-schema>
 | 
			
		||||
            <filter-element type="OBJECT" id="table" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="view" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="json view" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="materialized view" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="index" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="constraint" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="trigger" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="synonym" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="sequence" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="procedure" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="function" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="package" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="type" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="dimension" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="cluster" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="dblink" selected="true" />
 | 
			
		||||
          </any-schema>
 | 
			
		||||
        </basic-filter>
 | 
			
		||||
        <extended-filter>
 | 
			
		||||
          <filter-element type="RESERVED_WORD" id="keyword" selected="true" />
 | 
			
		||||
          <filter-element type="RESERVED_WORD" id="function" selected="true" />
 | 
			
		||||
          <filter-element type="RESERVED_WORD" id="parameter" selected="true" />
 | 
			
		||||
          <filter-element type="RESERVED_WORD" id="datatype" selected="true" />
 | 
			
		||||
          <filter-element type="RESERVED_WORD" id="exception" selected="true" />
 | 
			
		||||
          <filter-element type="OBJECT" id="schema" selected="true" />
 | 
			
		||||
          <filter-element type="OBJECT" id="user" selected="true" />
 | 
			
		||||
          <filter-element type="OBJECT" id="role" selected="true" />
 | 
			
		||||
          <filter-element type="OBJECT" id="privilege" selected="true" />
 | 
			
		||||
          <user-schema>
 | 
			
		||||
            <filter-element type="OBJECT" id="table" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="view" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="json view" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="materialized view" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="index" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="constraint" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="trigger" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="synonym" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="sequence" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="procedure" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="function" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="package" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="type" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="dimension" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="cluster" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="dblink" selected="true" />
 | 
			
		||||
          </user-schema>
 | 
			
		||||
          <public-schema>
 | 
			
		||||
            <filter-element type="OBJECT" id="table" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="view" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="json view" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="materialized view" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="index" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="constraint" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="trigger" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="synonym" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="sequence" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="procedure" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="function" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="package" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="type" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="dimension" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="cluster" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="dblink" selected="true" />
 | 
			
		||||
          </public-schema>
 | 
			
		||||
          <any-schema>
 | 
			
		||||
            <filter-element type="OBJECT" id="table" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="view" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="json view" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="materialized view" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="index" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="constraint" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="trigger" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="synonym" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="sequence" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="procedure" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="function" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="package" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="type" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="dimension" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="cluster" selected="true" />
 | 
			
		||||
            <filter-element type="OBJECT" id="dblink" selected="true" />
 | 
			
		||||
          </any-schema>
 | 
			
		||||
        </extended-filter>
 | 
			
		||||
      </filters>
 | 
			
		||||
      <sorting enabled="true">
 | 
			
		||||
        <sorting-element type="RESERVED_WORD" id="keyword" />
 | 
			
		||||
        <sorting-element type="RESERVED_WORD" id="datatype" />
 | 
			
		||||
        <sorting-element type="OBJECT" id="column" />
 | 
			
		||||
        <sorting-element type="OBJECT" id="table" />
 | 
			
		||||
        <sorting-element type="OBJECT" id="view" />
 | 
			
		||||
        <sorting-element type="OBJECT" id="json view" />
 | 
			
		||||
        <sorting-element type="OBJECT" id="materialized view" />
 | 
			
		||||
        <sorting-element type="OBJECT" id="index" />
 | 
			
		||||
        <sorting-element type="OBJECT" id="constraint" />
 | 
			
		||||
        <sorting-element type="OBJECT" id="trigger" />
 | 
			
		||||
        <sorting-element type="OBJECT" id="synonym" />
 | 
			
		||||
        <sorting-element type="OBJECT" id="sequence" />
 | 
			
		||||
        <sorting-element type="OBJECT" id="procedure" />
 | 
			
		||||
        <sorting-element type="OBJECT" id="function" />
 | 
			
		||||
        <sorting-element type="OBJECT" id="package" />
 | 
			
		||||
        <sorting-element type="OBJECT" id="type" />
 | 
			
		||||
        <sorting-element type="OBJECT" id="dimension" />
 | 
			
		||||
        <sorting-element type="OBJECT" id="cluster" />
 | 
			
		||||
        <sorting-element type="OBJECT" id="dblink" />
 | 
			
		||||
        <sorting-element type="OBJECT" id="schema" />
 | 
			
		||||
        <sorting-element type="OBJECT" id="role" />
 | 
			
		||||
        <sorting-element type="OBJECT" id="user" />
 | 
			
		||||
        <sorting-element type="RESERVED_WORD" id="function" />
 | 
			
		||||
        <sorting-element type="RESERVED_WORD" id="parameter" />
 | 
			
		||||
      </sorting>
 | 
			
		||||
      <format>
 | 
			
		||||
        <enforce-code-style-case value="true" />
 | 
			
		||||
      </format>
 | 
			
		||||
    </code-completion-settings>
 | 
			
		||||
    <execution-engine-settings>
 | 
			
		||||
      <statement-execution>
 | 
			
		||||
        <fetch-block-size value="100" />
 | 
			
		||||
        <execution-timeout value="20" />
 | 
			
		||||
        <debug-execution-timeout value="600" />
 | 
			
		||||
        <focus-result value="false" />
 | 
			
		||||
        <prompt-execution value="false" />
 | 
			
		||||
      </statement-execution>
 | 
			
		||||
      <script-execution>
 | 
			
		||||
        <command-line-interfaces />
 | 
			
		||||
        <execution-timeout value="300" />
 | 
			
		||||
      </script-execution>
 | 
			
		||||
      <method-execution>
 | 
			
		||||
        <execution-timeout value="30" />
 | 
			
		||||
        <debug-execution-timeout value="600" />
 | 
			
		||||
        <parameter-history-size value="10" />
 | 
			
		||||
      </method-execution>
 | 
			
		||||
    </execution-engine-settings>
 | 
			
		||||
    <operation-settings>
 | 
			
		||||
      <transactions>
 | 
			
		||||
        <uncommitted-changes>
 | 
			
		||||
          <on-project-close value="ASK" />
 | 
			
		||||
          <on-disconnect value="ASK" />
 | 
			
		||||
          <on-autocommit-toggle value="ASK" />
 | 
			
		||||
        </uncommitted-changes>
 | 
			
		||||
        <multiple-uncommitted-changes>
 | 
			
		||||
          <on-commit value="ASK" />
 | 
			
		||||
          <on-rollback value="ASK" />
 | 
			
		||||
        </multiple-uncommitted-changes>
 | 
			
		||||
      </transactions>
 | 
			
		||||
      <session-browser>
 | 
			
		||||
        <disconnect-session value="ASK" />
 | 
			
		||||
        <kill-session value="ASK" />
 | 
			
		||||
        <reload-on-filter-change value="false" />
 | 
			
		||||
      </session-browser>
 | 
			
		||||
      <compiler>
 | 
			
		||||
        <compile-type value="KEEP" />
 | 
			
		||||
        <compile-dependencies value="ASK" />
 | 
			
		||||
        <always-show-controls value="false" />
 | 
			
		||||
      </compiler>
 | 
			
		||||
    </operation-settings>
 | 
			
		||||
    <ddl-file-settings>
 | 
			
		||||
      <extensions>
 | 
			
		||||
        <mapping file-type-id="VIEW" extensions="vw" />
 | 
			
		||||
        <mapping file-type-id="TRIGGER" extensions="trg" />
 | 
			
		||||
        <mapping file-type-id="PROCEDURE" extensions="prc" />
 | 
			
		||||
        <mapping file-type-id="FUNCTION" extensions="fnc" />
 | 
			
		||||
        <mapping file-type-id="PACKAGE" extensions="pkg" />
 | 
			
		||||
        <mapping file-type-id="PACKAGE_SPEC" extensions="pks" />
 | 
			
		||||
        <mapping file-type-id="PACKAGE_BODY" extensions="pkb" />
 | 
			
		||||
        <mapping file-type-id="TYPE" extensions="tpe" />
 | 
			
		||||
        <mapping file-type-id="TYPE_SPEC" extensions="tps" />
 | 
			
		||||
        <mapping file-type-id="TYPE_BODY" extensions="tpb" />
 | 
			
		||||
        <mapping file-type-id="JAVA_SOURCE" extensions="sql" />
 | 
			
		||||
      </extensions>
 | 
			
		||||
      <general>
 | 
			
		||||
        <lookup-ddl-files value="true" />
 | 
			
		||||
        <create-ddl-files value="false" />
 | 
			
		||||
        <synchronize-ddl-files value="true" />
 | 
			
		||||
        <use-qualified-names value="false" />
 | 
			
		||||
        <make-scripts-rerunnable value="true" />
 | 
			
		||||
      </general>
 | 
			
		||||
    </ddl-file-settings>
 | 
			
		||||
    <assistant-settings>
 | 
			
		||||
      <credential-settings>
 | 
			
		||||
        <credentials />
 | 
			
		||||
      </credential-settings>
 | 
			
		||||
    </assistant-settings>
 | 
			
		||||
    <general-settings>
 | 
			
		||||
      <regional-settings>
 | 
			
		||||
        <date-format value="MEDIUM" />
 | 
			
		||||
        <number-format value="UNGROUPED" />
 | 
			
		||||
        <locale value="SYSTEM_DEFAULT" />
 | 
			
		||||
        <use-custom-formats value="false" />
 | 
			
		||||
      </regional-settings>
 | 
			
		||||
      <environment>
 | 
			
		||||
        <environment-types>
 | 
			
		||||
          <environment-type id="development" name="Development" description="Development environment" color="-2430209/-12296320" readonly-code="false" readonly-data="false" />
 | 
			
		||||
          <environment-type id="integration" name="Integration" description="Integration environment" color="-2621494/-12163514" readonly-code="true" readonly-data="false" />
 | 
			
		||||
          <environment-type id="production" name="Production" description="Productive environment" color="-11574/-10271420" readonly-code="true" readonly-data="true" />
 | 
			
		||||
          <environment-type id="other" name="Other" description="" color="-1576/-10724543" readonly-code="false" readonly-data="false" />
 | 
			
		||||
        </environment-types>
 | 
			
		||||
        <visibility-settings>
 | 
			
		||||
          <connection-tabs value="true" />
 | 
			
		||||
          <dialog-headers value="true" />
 | 
			
		||||
          <object-editor-tabs value="true" />
 | 
			
		||||
          <script-editor-tabs value="false" />
 | 
			
		||||
          <execution-result-tabs value="true" />
 | 
			
		||||
        </visibility-settings>
 | 
			
		||||
      </environment>
 | 
			
		||||
    </general-settings>
 | 
			
		||||
  </component>
 | 
			
		||||
</project>
 | 
			
		||||
							
								
								
									
										44
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										44
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
								
							@@ -1,44 +0,0 @@
 | 
			
		||||
<component name="InspectionProjectProfileManager">
 | 
			
		||||
  <profile version="1.0">
 | 
			
		||||
    <option name="myName" value="Project Default" />
 | 
			
		||||
    <inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true">
 | 
			
		||||
      <option name="myValues">
 | 
			
		||||
        <value>
 | 
			
		||||
          <list size="7">
 | 
			
		||||
            <item index="0" class="java.lang.String" itemvalue="nobr" />
 | 
			
		||||
            <item index="1" class="java.lang.String" itemvalue="noembed" />
 | 
			
		||||
            <item index="2" class="java.lang.String" itemvalue="comment" />
 | 
			
		||||
            <item index="3" class="java.lang.String" itemvalue="noscript" />
 | 
			
		||||
            <item index="4" class="java.lang.String" itemvalue="embed" />
 | 
			
		||||
            <item index="5" class="java.lang.String" itemvalue="script" />
 | 
			
		||||
            <item index="6" class="java.lang.String" itemvalue="span" />
 | 
			
		||||
          </list>
 | 
			
		||||
        </value>
 | 
			
		||||
      </option>
 | 
			
		||||
      <option name="myCustomValuesEnabled" value="true" />
 | 
			
		||||
    </inspection_tool>
 | 
			
		||||
    <inspection_tool class="PyCompatibilityInspection" enabled="false" level="WARNING" enabled_by_default="false">
 | 
			
		||||
      <option name="ourVersions">
 | 
			
		||||
        <value>
 | 
			
		||||
          <list size="1">
 | 
			
		||||
            <item index="0" class="java.lang.String" itemvalue="3.13" />
 | 
			
		||||
          </list>
 | 
			
		||||
        </value>
 | 
			
		||||
      </option>
 | 
			
		||||
    </inspection_tool>
 | 
			
		||||
    <inspection_tool class="PyShadowingBuiltinsInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
 | 
			
		||||
      <option name="ignoredNames">
 | 
			
		||||
        <list>
 | 
			
		||||
          <option value="format" />
 | 
			
		||||
        </list>
 | 
			
		||||
      </option>
 | 
			
		||||
    </inspection_tool>
 | 
			
		||||
    <inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
 | 
			
		||||
      <option name="ignoredIdentifiers">
 | 
			
		||||
        <list>
 | 
			
		||||
          <option value="type.*" />
 | 
			
		||||
        </list>
 | 
			
		||||
      </option>
 | 
			
		||||
    </inspection_tool>
 | 
			
		||||
  </profile>
 | 
			
		||||
</component>
 | 
			
		||||
							
								
								
									
										6
									
								
								.idea/inspectionProfiles/profiles_settings.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								.idea/inspectionProfiles/profiles_settings.xml
									
									
									
										generated
									
									
									
								
							@@ -1,6 +0,0 @@
 | 
			
		||||
<component name="InspectionProjectProfileManager">
 | 
			
		||||
  <settings>
 | 
			
		||||
    <option name="USE_PROJECT_PROFILE" value="false" />
 | 
			
		||||
    <version value="1.0" />
 | 
			
		||||
  </settings>
 | 
			
		||||
</component>
 | 
			
		||||
							
								
								
									
										7
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										7
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
								
							@@ -1,7 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project version="4">
 | 
			
		||||
  <component name="Black">
 | 
			
		||||
    <option name="sdkName" value="Python 3.13" />
 | 
			
		||||
  </component>
 | 
			
		||||
  <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 virtualenv at C:\Users\Algiz_N\PycharmProjects\stcs\.venv" project-jdk-type="Python SDK" />
 | 
			
		||||
</project>
 | 
			
		||||
							
								
								
									
										8
									
								
								.idea/modules.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								.idea/modules.xml
									
									
									
										generated
									
									
									
								
							@@ -1,8 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project version="4">
 | 
			
		||||
  <component name="ProjectModuleManager">
 | 
			
		||||
    <modules>
 | 
			
		||||
      <module fileurl="file://$PROJECT_DIR$/.idea/stcs.iml" filepath="$PROJECT_DIR$/.idea/stcs.iml" />
 | 
			
		||||
    </modules>
 | 
			
		||||
  </component>
 | 
			
		||||
</project>
 | 
			
		||||
							
								
								
									
										8
									
								
								.idea/stcs.iml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								.idea/stcs.iml
									
									
									
										generated
									
									
									
								
							@@ -1,8 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<module type="PYTHON_MODULE" version="4">
 | 
			
		||||
  <component name="NewModuleRootManager">
 | 
			
		||||
    <content url="file://$MODULE_DIR$" />
 | 
			
		||||
    <orderEntry type="jdk" jdkName="Python 3.13 virtualenv at C:\Users\Algiz_N\PycharmProjects\stcs\.venv" jdkType="Python SDK" />
 | 
			
		||||
    <orderEntry type="sourceFolder" forTests="false" />
 | 
			
		||||
  </component>
 | 
			
		||||
</module>
 | 
			
		||||
							
								
								
									
										6
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
								
							@@ -1,6 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project version="4">
 | 
			
		||||
  <component name="VcsDirectoryMappings">
 | 
			
		||||
    <mapping directory="" vcs="Git" />
 | 
			
		||||
  </component>
 | 
			
		||||
</project>
 | 
			
		||||
@@ -1,52 +0,0 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import logging.config
 | 
			
		||||
from aiogram import Bot, Dispatcher
 | 
			
		||||
from aiogram.fsm.storage.redis import RedisStorage
 | 
			
		||||
from app.services.Bybit.functions.bybit_ws import get_or_create_event_loop, set_event_loop
 | 
			
		||||
from app.telegram.database.models import async_main
 | 
			
		||||
from app.telegram.handlers.handlers import router
 | 
			
		||||
from app.telegram.functions.main_settings.settings import router_main_settings
 | 
			
		||||
from app.telegram.functions.risk_management_settings.settings import router_risk_management_settings
 | 
			
		||||
from app.telegram.functions.condition_settings.settings import condition_settings_router
 | 
			
		||||
from app.services.Bybit.functions.Add_Bybit_API import router_register_bybit_api
 | 
			
		||||
from app.services.Bybit.functions.functions import router_functions_bybit_trade
 | 
			
		||||
 | 
			
		||||
from logger_helper.logger_helper import LOGGING_CONFIG
 | 
			
		||||
from config import TOKEN_TG_BOT_1
 | 
			
		||||
 | 
			
		||||
logging.config.dictConfig(LOGGING_CONFIG)
 | 
			
		||||
logger = logging.getLogger("main")
 | 
			
		||||
 | 
			
		||||
storage = RedisStorage.from_url("redis://localhost:6379/0")
 | 
			
		||||
bot = Bot(token=TOKEN_TG_BOT_1)
 | 
			
		||||
dp = Dispatcher(storage=storage)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def main() -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Основная асинхронная функция запуска бота:
 | 
			
		||||
    """
 | 
			
		||||
    loop = get_or_create_event_loop()
 | 
			
		||||
    set_event_loop(loop)
 | 
			
		||||
 | 
			
		||||
    await async_main()
 | 
			
		||||
 | 
			
		||||
    dp.include_router(router)
 | 
			
		||||
    dp.include_router(router_main_settings)
 | 
			
		||||
    dp.include_router(router_risk_management_settings)
 | 
			
		||||
    dp.include_router(condition_settings_router)
 | 
			
		||||
    dp.include_router(router_register_bybit_api)
 | 
			
		||||
    dp.include_router(router_functions_bybit_trade)
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        await dp.start_polling(bot)
 | 
			
		||||
    except asyncio.CancelledError:
 | 
			
		||||
        logger.info("Bot is off")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    try:
 | 
			
		||||
        logger.info("Bot is on")
 | 
			
		||||
        asyncio.run(main())
 | 
			
		||||
    except KeyboardInterrupt:
 | 
			
		||||
        logger.info("Bot is off")
 | 
			
		||||
@@ -1,68 +0,0 @@
 | 
			
		||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
 | 
			
		||||
  <PropertyGroup>
 | 
			
		||||
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
 | 
			
		||||
    <SchemaVersion>2.0</SchemaVersion>
 | 
			
		||||
    <ProjectGuid>bc1d7460-d8ca-4977-a249-0f6d6cc2375a</ProjectGuid>
 | 
			
		||||
    <ProjectHome>.</ProjectHome>
 | 
			
		||||
    <StartupFile>BibytBot_API.py</StartupFile>
 | 
			
		||||
    <SearchPath>
 | 
			
		||||
    </SearchPath>
 | 
			
		||||
    <WorkingDirectory>.</WorkingDirectory>
 | 
			
		||||
    <OutputPath>.</OutputPath>
 | 
			
		||||
    <Name>BibytBot_API</Name>
 | 
			
		||||
    <RootNamespace>BibytBot_API</RootNamespace>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
 | 
			
		||||
    <DebugSymbols>true</DebugSymbols>
 | 
			
		||||
    <EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
  <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
 | 
			
		||||
    <DebugSymbols>true</DebugSymbols>
 | 
			
		||||
    <EnableUnmanagedDebugging>false</EnableUnmanagedDebugging>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
    <Compile Include="app\services\Bybit\functions\Add_Bybit_API.py" />
 | 
			
		||||
    <Compile Include="app\services\Bybit\functions\balance.py" />
 | 
			
		||||
    <Compile Include="app\services\Bybit\functions\functions.py" />
 | 
			
		||||
    <Compile Include="app\services\Bybit\functions\func_min_qty.py" />
 | 
			
		||||
    <Compile Include="app\services\Bybit\functions\Futures.py" />
 | 
			
		||||
    <Compile Include="app\services\Bybit\functions\price_symbol.py" />
 | 
			
		||||
    <Compile Include="app\telegram\functions\additional_settings\settings.py" />
 | 
			
		||||
    <Compile Include="app\telegram\functions\condition_settings\settings.py" />
 | 
			
		||||
    <Compile Include="app\telegram\functions\functions.py" />
 | 
			
		||||
    <Compile Include="app\telegram\database\models.py" />
 | 
			
		||||
    <Compile Include="app\telegram\database\requests.py" />
 | 
			
		||||
    <Compile Include="app\telegram\functions\main_settings\settings.py" />
 | 
			
		||||
    <Compile Include="app\telegram\functions\risk_management_settings\settings.py" />
 | 
			
		||||
    <Compile Include="app\telegram\handlers\handlers.py" />
 | 
			
		||||
    <Compile Include="app\telegram\Keyboards\inline_keyboards.py" />
 | 
			
		||||
    <Compile Include="app\telegram\Keyboards\reply_keyboards.py" />
 | 
			
		||||
    <Compile Include="app\telegram\logs.py" />
 | 
			
		||||
    <Compile Include="BibytBot_API.py" />
 | 
			
		||||
    <Compile Include="config.py" />
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
    <Folder Include="app\" />
 | 
			
		||||
    <Folder Include="app\services\Bybit\" />
 | 
			
		||||
    <Folder Include="app\services\" />
 | 
			
		||||
    <Folder Include="app\services\Bybit\functions\" />
 | 
			
		||||
    <Folder Include="app\telegram\database\" />
 | 
			
		||||
    <Folder Include="app\telegram\functions\condition_settings\" />
 | 
			
		||||
    <Folder Include="app\telegram\functions\additional_settings\" />
 | 
			
		||||
    <Folder Include="app\telegram\functions\risk_management_settings\" />
 | 
			
		||||
    <Folder Include="app\telegram\handlers\" />
 | 
			
		||||
    <Folder Include="app\telegram\Keyboards\" />
 | 
			
		||||
    <Folder Include="app\telegram\functions\main_settings\" />
 | 
			
		||||
    <Folder Include="app\telegram\functions\" />
 | 
			
		||||
    <Folder Include="app\telegram\" />
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
  <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.targets" />
 | 
			
		||||
  <!-- Uncomment the CoreCompile target to enable the Build command in
 | 
			
		||||
       Visual Studio and specify your pre- and post-build commands in
 | 
			
		||||
       the BeforeBuild and AfterBuild targets below. -->
 | 
			
		||||
  <!--<Target Name="CoreCompile" />-->
 | 
			
		||||
  <Target Name="BeforeBuild">
 | 
			
		||||
  </Target>
 | 
			
		||||
  <Target Name="AfterBuild">
 | 
			
		||||
  </Target>
 | 
			
		||||
</Project>
 | 
			
		||||
@@ -1,23 +0,0 @@
 | 
			
		||||
 | 
			
		||||
Microsoft Visual Studio Solution File, Format Version 12.00
 | 
			
		||||
# Visual Studio Version 17
 | 
			
		||||
VisualStudioVersion = 17.13.35825.156 d17.13
 | 
			
		||||
MinimumVisualStudioVersion = 10.0.40219.1
 | 
			
		||||
Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "BibytBot_API", "BibytBot_API.pyproj", "{BC1D7460-D8CA-4977-A249-0F6D6CC2375A}"
 | 
			
		||||
EndProject
 | 
			
		||||
Global
 | 
			
		||||
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 | 
			
		||||
		Debug|Any CPU = Debug|Any CPU
 | 
			
		||||
		Release|Any CPU = Release|Any CPU
 | 
			
		||||
	EndGlobalSection
 | 
			
		||||
	GlobalSection(ProjectConfigurationPlatforms) = postSolution
 | 
			
		||||
		{BC1D7460-D8CA-4977-A249-0F6D6CC2375A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 | 
			
		||||
		{BC1D7460-D8CA-4977-A249-0F6D6CC2375A}.Release|Any CPU.ActiveCfg = Release|Any CPU
 | 
			
		||||
	EndGlobalSection
 | 
			
		||||
	GlobalSection(SolutionProperties) = preSolution
 | 
			
		||||
		HideSolutionNode = FALSE
 | 
			
		||||
	EndGlobalSection
 | 
			
		||||
	GlobalSection(ExtensibilityGlobals) = postSolution
 | 
			
		||||
		SolutionGuid = {9AF00E9A-19FB-4146-96C0-B86C8B1E02C0}
 | 
			
		||||
	EndGlobalSection
 | 
			
		||||
EndGlobal
 | 
			
		||||
							
								
								
									
										55
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										55
									
								
								README.md
									
									
									
									
									
								
							@@ -27,26 +27,69 @@ Crypto Trading Telegram Bot
 | 
			
		||||
- Хранение пользовательских настроек и статистики в базе данных.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
##Установка и запуск
 | 
			
		||||
## Установка
 | 
			
		||||
 | 
			
		||||
1. Клонируйте репозиторий:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
git clone <URL_репозитория>
 | 
			
		||||
 | 
			
		||||
```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)
 | 
			
		||||
 | 
			
		||||
3. Создайте файл .env и настройте переменные окружения.
 | 
			
		||||
4. Создайте файл .env и настройте переменные окружения
 | 
			
		||||
```bash
 | 
			
		||||
cp .env.sample .env
 | 
			
		||||
nvim .env
 | 
			
		||||
```
 | 
			
		||||
5. Выполните миграции:
 | 
			
		||||
```bash
 | 
			
		||||
alembic upgrade head
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
5. Запустите бота:
 | 
			
		||||
 | 
			
		||||
4. Запустите бота:
 | 
			
		||||
```bash
 | 
			
		||||
python run.py
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
python BybitBot_API.py
 | 
			
		||||
## Настройка автономной работы
 | 
			
		||||
1. Создаём файл конфигурации SystemD
 | 
			
		||||
```bash
 | 
			
		||||
sudo cp examples/systemd/stcs.service /etc/systemd/system/
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
2. Настраиваем его
 | 
			
		||||
```bash
 | 
			
		||||
nvim /etc/systemd/system/stcs.service
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
3. Добавляем в автозапуск
 | 
			
		||||
```bash
 | 
			
		||||
sudo systemctl enable stcs
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
4. Запускаем
 | 
			
		||||
```bash
 | 
			
		||||
sudo service stcs start
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
5. Проверяем
 | 
			
		||||
```bash
 | 
			
		||||
sudo service stcs status
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Настройки пользователя
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										147
									
								
								alembic.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								alembic.ini
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,147 @@
 | 
			
		||||
# A generic, single database configuration.
 | 
			
		||||
 | 
			
		||||
[alembic]
 | 
			
		||||
# path to migration scripts.
 | 
			
		||||
# this is typically a path given in POSIX (e.g. forward slashes)
 | 
			
		||||
# format, relative to the token %(here)s which refers to the location of this
 | 
			
		||||
# ini file
 | 
			
		||||
script_location = %(here)s/alembic
 | 
			
		||||
 | 
			
		||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
 | 
			
		||||
# Uncomment the line below if you want the files to be prepended with date and time
 | 
			
		||||
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
 | 
			
		||||
# for all available tokens
 | 
			
		||||
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
 | 
			
		||||
 | 
			
		||||
# sys.path path, will be prepended to sys.path if present.
 | 
			
		||||
# defaults to the current working directory.  for multiple paths, the path separator
 | 
			
		||||
# is defined by "path_separator" below.
 | 
			
		||||
prepend_sys_path = .
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# timezone to use when rendering the date within the migration file
 | 
			
		||||
# as well as the filename.
 | 
			
		||||
# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
 | 
			
		||||
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
 | 
			
		||||
# string value is passed to ZoneInfo()
 | 
			
		||||
# leave blank for localtime
 | 
			
		||||
# timezone =
 | 
			
		||||
 | 
			
		||||
# max length of characters to apply to the "slug" field
 | 
			
		||||
# truncate_slug_length = 40
 | 
			
		||||
 | 
			
		||||
# set to 'true' to run the environment during
 | 
			
		||||
# the 'revision' command, regardless of autogenerate
 | 
			
		||||
# revision_environment = false
 | 
			
		||||
 | 
			
		||||
# set to 'true' to allow .pyc and .pyo files without
 | 
			
		||||
# a source .py file to be detected as revisions in the
 | 
			
		||||
# versions/ directory
 | 
			
		||||
# sourceless = false
 | 
			
		||||
 | 
			
		||||
# version location specification; This defaults
 | 
			
		||||
# to <script_location>/versions.  When using multiple version
 | 
			
		||||
# directories, initial revisions must be specified with --version-path.
 | 
			
		||||
# The path separator used here should be the separator specified by "path_separator"
 | 
			
		||||
# below.
 | 
			
		||||
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
 | 
			
		||||
 | 
			
		||||
# path_separator; This indicates what character is used to split lists of file
 | 
			
		||||
# paths, including version_locations and prepend_sys_path within configparser
 | 
			
		||||
# files such as alembic.ini.
 | 
			
		||||
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
 | 
			
		||||
# to provide os-dependent path splitting.
 | 
			
		||||
#
 | 
			
		||||
# Note that in order to support legacy alembic.ini files, this default does NOT
 | 
			
		||||
# take place if path_separator is not present in alembic.ini.  If this
 | 
			
		||||
# option is omitted entirely, fallback logic is as follows:
 | 
			
		||||
#
 | 
			
		||||
# 1. Parsing of the version_locations option falls back to using the legacy
 | 
			
		||||
#    "version_path_separator" key, which if absent then falls back to the legacy
 | 
			
		||||
#    behavior of splitting on spaces and/or commas.
 | 
			
		||||
# 2. Parsing of the prepend_sys_path option falls back to the legacy
 | 
			
		||||
#    behavior of splitting on spaces, commas, or colons.
 | 
			
		||||
#
 | 
			
		||||
# Valid values for path_separator are:
 | 
			
		||||
#
 | 
			
		||||
# path_separator = :
 | 
			
		||||
# path_separator = ;
 | 
			
		||||
# path_separator = space
 | 
			
		||||
# path_separator = newline
 | 
			
		||||
#
 | 
			
		||||
# Use os.pathsep. Default configuration used for new projects.
 | 
			
		||||
path_separator = os
 | 
			
		||||
 | 
			
		||||
# set to 'true' to search source files recursively
 | 
			
		||||
# in each "version_locations" directory
 | 
			
		||||
# new in Alembic version 1.10
 | 
			
		||||
# recursive_version_locations = false
 | 
			
		||||
 | 
			
		||||
# the output encoding used when revision files
 | 
			
		||||
# are written from script.py.mako
 | 
			
		||||
# output_encoding = utf-8
 | 
			
		||||
 | 
			
		||||
# database URL.  This is consumed by the user-maintained env.py script only.
 | 
			
		||||
# other means of configuring database URLs may be customized within the env.py
 | 
			
		||||
# file.
 | 
			
		||||
sqlalchemy.url = sqlite+aiosqlite:///./database/db/stcs.db
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
[post_write_hooks]
 | 
			
		||||
# post_write_hooks defines scripts or Python functions that are run
 | 
			
		||||
# on newly generated revision scripts.  See the documentation for further
 | 
			
		||||
# detail and examples
 | 
			
		||||
 | 
			
		||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
 | 
			
		||||
# hooks = black
 | 
			
		||||
# black.type = console_scripts
 | 
			
		||||
# black.entrypoint = black
 | 
			
		||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
 | 
			
		||||
 | 
			
		||||
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
 | 
			
		||||
# hooks = ruff
 | 
			
		||||
# ruff.type = module
 | 
			
		||||
# ruff.module = ruff
 | 
			
		||||
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
 | 
			
		||||
 | 
			
		||||
# Alternatively, use the exec runner to execute a binary found on your PATH
 | 
			
		||||
# hooks = ruff
 | 
			
		||||
# ruff.type = exec
 | 
			
		||||
# ruff.executable = ruff
 | 
			
		||||
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
 | 
			
		||||
 | 
			
		||||
# Logging configuration.  This is also consumed by the user-maintained
 | 
			
		||||
# env.py script only.
 | 
			
		||||
[loggers]
 | 
			
		||||
keys = root,sqlalchemy,alembic
 | 
			
		||||
 | 
			
		||||
[handlers]
 | 
			
		||||
keys = console
 | 
			
		||||
 | 
			
		||||
[formatters]
 | 
			
		||||
keys = generic
 | 
			
		||||
 | 
			
		||||
[logger_root]
 | 
			
		||||
level = WARNING
 | 
			
		||||
handlers = console
 | 
			
		||||
qualname =
 | 
			
		||||
 | 
			
		||||
[logger_sqlalchemy]
 | 
			
		||||
level = WARNING
 | 
			
		||||
handlers =
 | 
			
		||||
qualname = sqlalchemy.engine
 | 
			
		||||
 | 
			
		||||
[logger_alembic]
 | 
			
		||||
level = INFO
 | 
			
		||||
handlers =
 | 
			
		||||
qualname = alembic
 | 
			
		||||
 | 
			
		||||
[handler_console]
 | 
			
		||||
class = StreamHandler
 | 
			
		||||
args = (sys.stderr,)
 | 
			
		||||
level = NOTSET
 | 
			
		||||
formatter = generic
 | 
			
		||||
 | 
			
		||||
[formatter_generic]
 | 
			
		||||
format = %(levelname)-5.5s [%(name)s] %(message)s
 | 
			
		||||
datefmt = %H:%M:%S
 | 
			
		||||
							
								
								
									
										1
									
								
								alembic/README
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								alembic/README
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
Generic single-database configuration.
 | 
			
		||||
							
								
								
									
										53
									
								
								alembic/env.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								alembic/env.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
from logging.config import fileConfig
 | 
			
		||||
from sqlalchemy import pool
 | 
			
		||||
from sqlalchemy.ext.asyncio import async_engine_from_config
 | 
			
		||||
from alembic import context
 | 
			
		||||
 | 
			
		||||
config = context.config
 | 
			
		||||
 | 
			
		||||
if config.config_file_name is not None:
 | 
			
		||||
    fileConfig(config.config_file_name)
 | 
			
		||||
 | 
			
		||||
from database.models import Base
 | 
			
		||||
target_metadata = Base.metadata
 | 
			
		||||
 | 
			
		||||
def do_run_migrations(connection):
 | 
			
		||||
    context.configure(
 | 
			
		||||
        connection=connection,
 | 
			
		||||
        target_metadata=target_metadata,
 | 
			
		||||
        compare_type=True,
 | 
			
		||||
    )
 | 
			
		||||
    with context.begin_transaction():
 | 
			
		||||
        context.run_migrations()
 | 
			
		||||
 | 
			
		||||
async def run_async_migrations():
 | 
			
		||||
    connectable = async_engine_from_config(
 | 
			
		||||
        config.get_section(config.config_ini_section),
 | 
			
		||||
        prefix="sqlalchemy.",
 | 
			
		||||
        poolclass=pool.NullPool,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    async with connectable.connect() as connection:
 | 
			
		||||
        await connection.run_sync(do_run_migrations)
 | 
			
		||||
 | 
			
		||||
    await connectable.dispose()
 | 
			
		||||
 | 
			
		||||
def run_migrations_offline():
 | 
			
		||||
    url = config.get_main_option("sqlalchemy.url")
 | 
			
		||||
    context.configure(
 | 
			
		||||
        url=url,
 | 
			
		||||
        target_metadata=target_metadata,
 | 
			
		||||
        literal_binds=True,
 | 
			
		||||
        dialect_opts={"paramstyle": "named"},
 | 
			
		||||
    )
 | 
			
		||||
    with context.begin_transaction():
 | 
			
		||||
        context.run_migrations()
 | 
			
		||||
 | 
			
		||||
def run_migrations_online():
 | 
			
		||||
    asyncio.run(run_async_migrations())
 | 
			
		||||
 | 
			
		||||
if context.is_offline_mode():
 | 
			
		||||
    run_migrations_offline()
 | 
			
		||||
else:
 | 
			
		||||
    run_migrations_online()
 | 
			
		||||
							
								
								
									
										28
									
								
								alembic/script.py.mako
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								alembic/script.py.mako
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
"""${message}
 | 
			
		||||
 | 
			
		||||
Revision ID: ${up_revision}
 | 
			
		||||
Revises: ${down_revision | comma,n}
 | 
			
		||||
Create Date: ${create_date}
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from typing import Sequence, Union
 | 
			
		||||
 | 
			
		||||
from alembic import op
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
${imports if imports else ""}
 | 
			
		||||
 | 
			
		||||
# revision identifiers, used by Alembic.
 | 
			
		||||
revision: str = ${repr(up_revision)}
 | 
			
		||||
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
 | 
			
		||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
 | 
			
		||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upgrade() -> None:
 | 
			
		||||
    """Upgrade schema."""
 | 
			
		||||
    ${upgrades if upgrades else "pass"}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def downgrade() -> None:
 | 
			
		||||
    """Downgrade schema."""
 | 
			
		||||
    ${downgrades if downgrades else "pass"}
 | 
			
		||||
							
								
								
									
										32
									
								
								alembic/versions/0ee52ab23e66_added_column_current_series.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								alembic/versions/0ee52ab23e66_added_column_current_series.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
"""Added column current_series
 | 
			
		||||
 | 
			
		||||
Revision ID: 0ee52ab23e66
 | 
			
		||||
Revises: e5d612e44563
 | 
			
		||||
Create Date: 2025-10-26 11:48:48.055031
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from typing import Sequence, Union
 | 
			
		||||
 | 
			
		||||
from alembic import op
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# revision identifiers, used by Alembic.
 | 
			
		||||
revision: str = '0ee52ab23e66'
 | 
			
		||||
down_revision: Union[str, Sequence[str], None] = 'e5d612e44563'
 | 
			
		||||
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('current_series', sa.Integer(), nullable=True))
 | 
			
		||||
    # ### end Alembic commands ###
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def downgrade() -> None:
 | 
			
		||||
    """Downgrade schema."""
 | 
			
		||||
    # ### commands auto generated by Alembic - please adjust! ###
 | 
			
		||||
    op.drop_column('user_deals', 'current_series')
 | 
			
		||||
    # ### end Alembic commands ###
 | 
			
		||||
							
								
								
									
										44
									
								
								alembic/versions/3fca121b7554_added_tp_sl_and_pnl.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								alembic/versions/3fca121b7554_added_tp_sl_and_pnl.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
"""Added TP_SL and PNL
 | 
			
		||||
 | 
			
		||||
Revision ID: 3fca121b7554
 | 
			
		||||
Revises: adf3d2991896
 | 
			
		||||
Create Date: 2025-10-29 11:07:45.350771
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from typing import Sequence, Union
 | 
			
		||||
 | 
			
		||||
from alembic import op
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
from sqlalchemy import inspect
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# revision identifiers, used by Alembic.
 | 
			
		||||
revision: str = '3fca121b7554'
 | 
			
		||||
down_revision: Union[str, Sequence[str], None] = 'adf3d2991896'
 | 
			
		||||
branch_labels: Union[str, Sequence[str], None] = None
 | 
			
		||||
depends_on: Union[str, Sequence[str], None] = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upgrade() -> None:
 | 
			
		||||
    """Upgrade schema."""
 | 
			
		||||
    conn = op.get_bind()
 | 
			
		||||
    inspector = inspect(conn)
 | 
			
		||||
 | 
			
		||||
    columns = [col['name'] for col in inspector.get_columns('user_deals')]
 | 
			
		||||
 | 
			
		||||
    if 'pnl_series' not in columns:
 | 
			
		||||
        op.add_column('user_deals', sa.Column('pnl_series', sa.Float(), nullable=True))
 | 
			
		||||
    if 'take_profit' not in columns:
 | 
			
		||||
        op.add_column('user_deals', sa.Column('take_profit', sa.Boolean(), nullable=False, server_default=sa.false()))
 | 
			
		||||
    if 'stop_loss' not in columns:
 | 
			
		||||
        op.add_column('user_deals', sa.Column('stop_loss', sa.Boolean(), nullable=False, server_default=sa.false()))
 | 
			
		||||
    # ### end Alembic commands ###
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def downgrade() -> None:
 | 
			
		||||
    """Downgrade schema."""
 | 
			
		||||
    # ### commands auto generated by Alembic - please adjust! ###
 | 
			
		||||
    op.drop_column('user_deals', 'stop_loss')
 | 
			
		||||
    op.drop_column('user_deals', 'take_profit')
 | 
			
		||||
    op.drop_column('user_deals', 'pnl_series')
 | 
			
		||||
    # ### end Alembic commands ###
 | 
			
		||||
							
								
								
									
										49
									
								
								alembic/versions/8329c0994b26_fixed_tp_sl_type.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								alembic/versions/8329c0994b26_fixed_tp_sl_type.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
			
		||||
"""Fixed TP_SL type
 | 
			
		||||
 | 
			
		||||
Revision ID: 8329c0994b26
 | 
			
		||||
Revises: 3fca121b7554
 | 
			
		||||
Create Date: 2025-10-29 13:07:52.161139
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from typing import Sequence, Union
 | 
			
		||||
 | 
			
		||||
from alembic import op
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# revision identifiers, used by Alembic.
 | 
			
		||||
revision: str = '8329c0994b26'
 | 
			
		||||
down_revision: Union[str, Sequence[str], None] = '3fca121b7554'
 | 
			
		||||
branch_labels: Union[str, Sequence[str], None] = None
 | 
			
		||||
depends_on: Union[str, Sequence[str], None] = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upgrade():
 | 
			
		||||
    with op.batch_alter_table('user_deals', recreate='always') as batch_op:
 | 
			
		||||
        # Добавляем новую колонку с нужным типом
 | 
			
		||||
        batch_op.add_column(sa.Column('take_profit_new', sa.Float(), nullable=False, server_default='0'))
 | 
			
		||||
 | 
			
		||||
    # После закрытия batch создается и переименовывается таблица.
 | 
			
		||||
    # Теперь мы можем обновить данные.
 | 
			
		||||
    op.execute(
 | 
			
		||||
        "UPDATE user_deals SET take_profit_new = CAST(take_profit AS FLOAT)"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    with op.batch_alter_table('user_deals', recreate='always') as batch_op:
 | 
			
		||||
        # Удаляем старую колонку
 | 
			
		||||
        batch_op.drop_column('take_profit')
 | 
			
		||||
        # Меняем имя новой колонки на старое
 | 
			
		||||
        batch_op.alter_column('take_profit_new', new_column_name='take_profit')
 | 
			
		||||
 | 
			
		||||
def downgrade():
 | 
			
		||||
    # Аналогично, но в обратном порядке и типе
 | 
			
		||||
    with op.batch_alter_table('user_deals', recreate='always') as batch_op:
 | 
			
		||||
        batch_op.add_column(sa.Column('take_profit_old', sa.Boolean(), nullable=False, server_default='0'))
 | 
			
		||||
 | 
			
		||||
    op.execute(
 | 
			
		||||
        "UPDATE user_deals SET take_profit_old = CAST(take_profit AS BOOLEAN)"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    with op.batch_alter_table('user_deals', recreate='always') as batch_op:
 | 
			
		||||
        batch_op.drop_column('take_profit')
 | 
			
		||||
        batch_op.alter_column('take_profit_old', new_column_name='take_profit')
 | 
			
		||||
@@ -0,0 +1,45 @@
 | 
			
		||||
"""Added column commission_place
 | 
			
		||||
 | 
			
		||||
Revision ID: adf3d2991896
 | 
			
		||||
Revises: 0ee52ab23e66
 | 
			
		||||
Create Date: 2025-10-26 13:37:33.662318
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from typing import Sequence, Union
 | 
			
		||||
 | 
			
		||||
from alembic import op
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
from sqlalchemy import inspect
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# revision identifiers, used by Alembic.
 | 
			
		||||
revision: str = 'adf3d2991896'
 | 
			
		||||
down_revision: Union[str, Sequence[str], None] = '0ee52ab23e66'
 | 
			
		||||
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! ###
 | 
			
		||||
    bind = op.get_bind()
 | 
			
		||||
    inspector = inspect(bind)
 | 
			
		||||
    columns_user_deals = [col['name'] for col in inspector.get_columns('user_deals')]
 | 
			
		||||
    if 'commission_fee' not in columns_user_deals:
 | 
			
		||||
        op.add_column('user_deals', sa.Column('commission_fee', sa.String(), server_default='', nullable=True))
 | 
			
		||||
    if 'commission_place' not in columns_user_deals:
 | 
			
		||||
        op.add_column('user_deals', sa.Column('commission_place', sa.String(), server_default='', nullable=True))
 | 
			
		||||
 | 
			
		||||
    columns_user_risk_mgmt = [col['name'] for col in inspector.get_columns('user_risk_management')]
 | 
			
		||||
    if 'commission_place' not in columns_user_risk_mgmt:
 | 
			
		||||
        op.add_column('user_risk_management',
 | 
			
		||||
                      sa.Column('commission_place', sa.String(), server_default='', nullable=False))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def downgrade() -> None:
 | 
			
		||||
    """Downgrade schema."""
 | 
			
		||||
    # ### commands auto generated by Alembic - please adjust! ###
 | 
			
		||||
    op.drop_column('user_risk_management', 'commission_place')
 | 
			
		||||
    op.drop_column('user_deals', 'commission_place')
 | 
			
		||||
    op.drop_column('user_deals', 'commission_fee')
 | 
			
		||||
    # ### end Alembic commands ###
 | 
			
		||||
@@ -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(demo=True, 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,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										379
									
								
								app/bybit/open_positions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										379
									
								
								app/bybit/open_positions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,379 @@
 | 
			
		||||
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
 | 
			
		||||
        commission_fee = risk_management_data.commission_fee
 | 
			
		||||
        commission_place = risk_management_data.commission_place
 | 
			
		||||
 | 
			
		||||
        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 rq.set_user_deal(
 | 
			
		||||
            tg_id=tg_id,
 | 
			
		||||
            symbol=symbol,
 | 
			
		||||
            current_step=1,
 | 
			
		||||
            current_series=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,
 | 
			
		||||
            commission_fee=commission_fee,
 | 
			
		||||
            commission_place=commission_place,
 | 
			
		||||
            pnl_series=0
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        res = await open_positions(
 | 
			
		||||
            tg_id=tg_id,
 | 
			
		||||
            symbol=symbol,
 | 
			
		||||
            side=side,
 | 
			
		||||
            order_quantity=order_quantity,
 | 
			
		||||
            trigger_price=trigger_price,
 | 
			
		||||
            leverage=leverage
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if res == "OK":
 | 
			
		||||
            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",
 | 
			
		||||
                   "Order placement failed as your position may exceed the max",
 | 
			
		||||
               }
 | 
			
		||||
            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)
 | 
			
		||||
        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
 | 
			
		||||
        current_series = user_deals_data.current_series
 | 
			
		||||
        commission_fee = user_deals_data.commission_fee
 | 
			
		||||
        commission_place = user_deals_data.commission_place
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
        next_series = current_series + 1
 | 
			
		||||
 | 
			
		||||
        await rq.set_user_deal(
 | 
			
		||||
            tg_id=tg_id,
 | 
			
		||||
            symbol=symbol,
 | 
			
		||||
            current_step=1,
 | 
			
		||||
            current_series=next_series,
 | 
			
		||||
            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,
 | 
			
		||||
            commission_fee=commission_fee,
 | 
			
		||||
            commission_place=commission_place,
 | 
			
		||||
            pnl_series=0
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        res = await open_positions(
 | 
			
		||||
            tg_id=tg_id,
 | 
			
		||||
            symbol=symbol,
 | 
			
		||||
            side=s_side,
 | 
			
		||||
            order_quantity=base_quantity,
 | 
			
		||||
            trigger_price=trigger_price,
 | 
			
		||||
            leverage=leverage
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if res == "OK":
 | 
			
		||||
            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",
 | 
			
		||||
                   "Order placement failed as your position may exceed the max",
 | 
			
		||||
               }
 | 
			
		||||
            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)
 | 
			
		||||
        commission_fee = user_deals_data.commission_fee
 | 
			
		||||
        commission_place = user_deals_data.commission_place
 | 
			
		||||
        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
 | 
			
		||||
        current_series = user_deals_data.current_series
 | 
			
		||||
        pnl_series = user_deals_data.pnl_series
 | 
			
		||||
 | 
			
		||||
        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,
 | 
			
		||||
        )
 | 
			
		||||
        total_quantity = next_quantity
 | 
			
		||||
        if commission_fee == "Yes_commission_fee":
 | 
			
		||||
            if commission_place == "Commission_for_qty":
 | 
			
		||||
                total_quantity = next_quantity + total_fee
 | 
			
		||||
 | 
			
		||||
        if trade_mode == "Switch":
 | 
			
		||||
            if side == "Buy":
 | 
			
		||||
                r_side = "Sell"
 | 
			
		||||
            else:
 | 
			
		||||
                r_side = "Buy"
 | 
			
		||||
        else:
 | 
			
		||||
            r_side = side
 | 
			
		||||
 | 
			
		||||
        await rq.set_user_deal(
 | 
			
		||||
            tg_id=tg_id,
 | 
			
		||||
            symbol=symbol,
 | 
			
		||||
            current_step=current_step,
 | 
			
		||||
            current_series=current_series,
 | 
			
		||||
            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,
 | 
			
		||||
            commission_fee=commission_fee,
 | 
			
		||||
            commission_place=commission_place,
 | 
			
		||||
            pnl_series=pnl_series
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        res = await open_positions(
 | 
			
		||||
            tg_id=tg_id,
 | 
			
		||||
            symbol=symbol,
 | 
			
		||||
            side=r_side,
 | 
			
		||||
            order_quantity=total_quantity,
 | 
			
		||||
            trigger_price=trigger_price,
 | 
			
		||||
            leverage=leverage
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if res == "OK":
 | 
			
		||||
            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",
 | 
			
		||||
                   "Order placement failed as your position may exceed the max",
 | 
			
		||||
               }
 | 
			
		||||
            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,
 | 
			
		||||
        leverage: str
 | 
			
		||||
) -> str | None:
 | 
			
		||||
    try:
 | 
			
		||||
        client = await get_bybit_client(tg_id=tg_id)
 | 
			
		||||
        get_ticker = await get_tickers(tg_id, symbol=symbol)
 | 
			
		||||
 | 
			
		||||
        if get_ticker is None:
 | 
			
		||||
            price_symbol = 0
 | 
			
		||||
        else:
 | 
			
		||||
            price_symbol = safe_float(get_ticker.get("lastPrice"))
 | 
			
		||||
 | 
			
		||||
        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_format = math.floor(qty / qty_step) * qty_step
 | 
			
		||||
        qty_formatted = round(qty_format, 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
 | 
			
		||||
 | 
			
		||||
        # 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",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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",
 | 
			
		||||
            "Order placement failed as your position may exceed the max": "Order placement failed as your position may exceed the max",
 | 
			
		||||
        }
 | 
			
		||||
        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
 | 
			
		||||
							
								
								
									
										47
									
								
								app/bybit/set_functions/set_tp_sl.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								app/bybit/set_functions/set_tp_sl.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
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)
 | 
			
		||||
        take_profit = round(take_profit_price, 6) if take_profit_price is not None else None
 | 
			
		||||
        stop_loss = round(stop_loss_price, 6) if stop_loss_price is not None else None
 | 
			
		||||
        resp = client.set_trading_stop(
 | 
			
		||||
            category="linear",
 | 
			
		||||
            symbol=symbol,
 | 
			
		||||
            takeProfit=str(take_profit) if take_profit is not None else None,
 | 
			
		||||
            stopLoss=str(stop_loss) if stop_loss is not None else None,
 | 
			
		||||
            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
 | 
			
		||||
							
								
								
									
										345
									
								
								app/bybit/telegram_message_handler.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										345
									
								
								app/bybit/telegram_message_handler.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,345 @@
 | 
			
		||||
import logging.config
 | 
			
		||||
import math
 | 
			
		||||
# import json
 | 
			
		||||
import app.telegram.keyboards.inline as kbi
 | 
			
		||||
import database.request as rq
 | 
			
		||||
from app.bybit.get_functions.get_instruments_info import get_instruments_info
 | 
			
		||||
from app.bybit.get_functions.get_positions import get_active_positions_by_symbol
 | 
			
		||||
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
 | 
			
		||||
from app.bybit.open_positions import trading_cycle, trading_cycle_profit
 | 
			
		||||
from app.bybit.set_functions.set_tp_sl import set_tp_sl_for_position
 | 
			
		||||
from app.helper_functions import format_value, safe_float, truncate_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_order_update(self, message, tg_id):
 | 
			
		||||
        try:
 | 
			
		||||
            user_additional_data = await rq.get_user_additional_settings(tg_id=tg_id)
 | 
			
		||||
            trigger_price = safe_float(user_additional_data.trigger_price)
 | 
			
		||||
            if trigger_price > 0:
 | 
			
		||||
                order_data = message.get("data", [{}])[0]
 | 
			
		||||
                symbol = format_value(order_data.get("symbol"))
 | 
			
		||||
                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"))
 | 
			
		||||
                tr_price = format_value(order_data.get("triggerPrice"))
 | 
			
		||||
 | 
			
		||||
                status_map = {
 | 
			
		||||
                    "Untriggered": "Условный ордер выставлен",
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if order_status == "Filled" or order_status not in status_map:
 | 
			
		||||
                    return None
 | 
			
		||||
 | 
			
		||||
                text = (
 | 
			
		||||
                    f"Торговая пара: {symbol}\n"
 | 
			
		||||
                    f"Движение: {side_rus}\n"
 | 
			
		||||
                )
 | 
			
		||||
                if tr_price and tr_price != "Нет данных":
 | 
			
		||||
                    text += f"Триггер цена: {tr_price}\n"
 | 
			
		||||
 | 
			
		||||
                await self.telegram_bot.send_message(
 | 
			
		||||
                    chat_id=tg_id, text=text, reply_markup=kbi.profile_bybit
 | 
			
		||||
                )
 | 
			
		||||
                await rq.set_trigger_price(tg_id=tg_id, trigger_price=0)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error("Error in format_order_update: %s", e)
 | 
			
		||||
 | 
			
		||||
    async def format_execution_update(self, message, tg_id):
 | 
			
		||||
        try:
 | 
			
		||||
            # logger.info("Execution update: %s", json.dumps(message))
 | 
			
		||||
            execution = message.get("data", [{}])[0]
 | 
			
		||||
            exec_type = format_value(execution.get("execType"))
 | 
			
		||||
            if exec_type == "Trade" or exec_type == "BustTrade":
 | 
			
		||||
                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"))
 | 
			
		||||
 | 
			
		||||
                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
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                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)
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                get_total_fee = 0
 | 
			
		||||
 | 
			
		||||
                if user_auto_trading is not None:
 | 
			
		||||
                    get_total_fee = user_auto_trading.total_fee
 | 
			
		||||
 | 
			
		||||
                total_fee = safe_float(exec_fee) + safe_float(get_total_fee)
 | 
			
		||||
 | 
			
		||||
                exec_pnl = format_value(execution.get("execPnl"))
 | 
			
		||||
                ex_pnl = safe_float(exec_pnl)
 | 
			
		||||
                pnl = safe_float(exec_pnl)
 | 
			
		||||
 | 
			
		||||
                header = (
 | 
			
		||||
                    "Сделка закрыта:" if safe_float(closed_size) > 0 else "Сделка открыта:"
 | 
			
		||||
                )
 | 
			
		||||
                text = f"{header}\n" f"Торговая пара: {symbol}\n"
 | 
			
		||||
                user_deals_data = await rq.get_user_deal_by_symbol(
 | 
			
		||||
                    tg_id=tg_id, symbol=symbol
 | 
			
		||||
                )
 | 
			
		||||
                if user_deals_data is None:
 | 
			
		||||
                    commission_fee = "Yes_commission_fee"
 | 
			
		||||
                    commission_place = "Commission_for_qty"
 | 
			
		||||
 | 
			
		||||
                else:
 | 
			
		||||
                    commission_fee = user_deals_data.commission_fee or "Yes_commission_fee"
 | 
			
		||||
                    commission_place = user_deals_data.commission_place or "Commission_for_qty"
 | 
			
		||||
 | 
			
		||||
                current_series = user_deals_data.current_series
 | 
			
		||||
                current_step = user_deals_data.current_step
 | 
			
		||||
                order_quantity = user_deals_data.order_quantity
 | 
			
		||||
                pnl_series = user_deals_data.pnl_series
 | 
			
		||||
                margin_type = user_deals_data.margin_type
 | 
			
		||||
                take_profit_percent = user_deals_data.take_profit_percent
 | 
			
		||||
                stop_loss_percent = user_deals_data.stop_loss_percent
 | 
			
		||||
                fee = safe_float(user_auto_trading.fee)
 | 
			
		||||
                total_pnl = safe_float(exec_pnl) - safe_float(exec_fee) - fee
 | 
			
		||||
                leverage = safe_float(user_deals_data.leverage)
 | 
			
		||||
 | 
			
		||||
                if commission_fee == "Yes_commission_fee":
 | 
			
		||||
                    if commission_place == "Commission_for_qty":
 | 
			
		||||
                        total_quantity = safe_float(order_quantity) + safe_float(
 | 
			
		||||
                            total_fee
 | 
			
		||||
                        ) * 2
 | 
			
		||||
                    else:
 | 
			
		||||
                        total_quantity = safe_float(order_quantity)
 | 
			
		||||
                else:
 | 
			
		||||
                    total_quantity = safe_float(order_quantity)
 | 
			
		||||
 | 
			
		||||
                if user_deals_data is not None and auto_trading and safe_float(closed_size) == 0:
 | 
			
		||||
                    await rq.set_total_fee_user_auto_trading(
 | 
			
		||||
                        tg_id=tg_id, symbol=symbol, total_fee=total_fee
 | 
			
		||||
                    )
 | 
			
		||||
                    text += f"Текущая ставка: {total_quantity:.2f} USDT\n"
 | 
			
		||||
                    text += f"Серия №: {current_series}\n"
 | 
			
		||||
                    text += f"Сделка №: {current_step}\n"
 | 
			
		||||
 | 
			
		||||
                text += (
 | 
			
		||||
                    f"Цена исполнения: {exec_price}\n"
 | 
			
		||||
                    f"Комиссия: {exec_fee:.8f}\n"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                if safe_float(closed_size) == 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(exec_price)
 | 
			
		||||
                    decimals = abs(int(round(math.log10(qty_step))))
 | 
			
		||||
                    qty_format = math.floor(qty / qty_step) * qty_step
 | 
			
		||||
                    qty_formatted = round(qty_format, decimals)
 | 
			
		||||
                    total_commission = 0
 | 
			
		||||
 | 
			
		||||
                    if commission_fee == "Yes_commission_fee":
 | 
			
		||||
                        if commission_place == "Commission_for_tp":
 | 
			
		||||
                            total_commission = safe_float(total_fee) / qty_formatted
 | 
			
		||||
 | 
			
		||||
                    if margin_type == "ISOLATED_MARGIN":
 | 
			
		||||
                        if side == "Buy":
 | 
			
		||||
                            take_profit_price = safe_float(exec_price) * (
 | 
			
		||||
                                    1 + take_profit_percent / 100) + total_commission
 | 
			
		||||
                            stop_loss_price = None
 | 
			
		||||
                        else:
 | 
			
		||||
                            take_profit_price = safe_float(exec_price) * (
 | 
			
		||||
                                    1 - take_profit_percent / 100) - total_commission
 | 
			
		||||
                            stop_loss_price = None
 | 
			
		||||
                    else:
 | 
			
		||||
                        if side == "Buy":
 | 
			
		||||
                            take_profit_price = safe_float(exec_price) * (
 | 
			
		||||
                                    1 + take_profit_percent / 100) + total_commission
 | 
			
		||||
                            stop_loss_price = safe_float(exec_price) * (1 - stop_loss_percent / 100)
 | 
			
		||||
                        else:
 | 
			
		||||
                            take_profit_price = safe_float(exec_price) * (
 | 
			
		||||
                                    1 - take_profit_percent / 100) - total_commission
 | 
			
		||||
                            stop_loss_price = safe_float(exec_price) * (1 + stop_loss_percent / 100)
 | 
			
		||||
 | 
			
		||||
                        take_profit_price = max(take_profit_price, 0)
 | 
			
		||||
                        stop_loss_price = max(stop_loss_price, 0)
 | 
			
		||||
 | 
			
		||||
                    ress = await set_tp_sl_for_position(tg_id=tg_id,
 | 
			
		||||
                                                        symbol=symbol,
 | 
			
		||||
                                                        take_profit_price=take_profit_price,
 | 
			
		||||
                                                        stop_loss_price=stop_loss_price,
 | 
			
		||||
                                                        position_idx=0)
 | 
			
		||||
                    if ress:
 | 
			
		||||
                        take_profit_truncated = await truncate_float(take_profit_price, 6)
 | 
			
		||||
                        text += (f"Движение: {side_rus}\n"
 | 
			
		||||
                                 f"Тейк-профит: {take_profit_truncated}\n"
 | 
			
		||||
                                 )
 | 
			
		||||
                        if stop_loss_price is not None:
 | 
			
		||||
                            stop_loss_truncated = await truncate_float(stop_loss_price, 6)
 | 
			
		||||
                        else:
 | 
			
		||||
                            stop_loss_truncated = None
 | 
			
		||||
 | 
			
		||||
                        if stop_loss_truncated is not None:
 | 
			
		||||
                            text += f"Стоп-лосс: {stop_loss_truncated}\n"
 | 
			
		||||
                        else:
 | 
			
		||||
                            deals = await get_active_positions_by_symbol(
 | 
			
		||||
                                tg_id=tg_id, symbol=symbol
 | 
			
		||||
                            )
 | 
			
		||||
                            position = next((d for d in deals if d.get("symbol") == symbol), None)
 | 
			
		||||
 | 
			
		||||
                            if position:
 | 
			
		||||
                                liq_price = position.get("liqPrice", 0)
 | 
			
		||||
                                text += f"Цена ликвидации: {liq_price}\n"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                    else:
 | 
			
		||||
                        text += (f"Движение: {side_rus}\n"
 | 
			
		||||
                                 "Не удалось установить ТП и СЛ\n")
 | 
			
		||||
 | 
			
		||||
                else:
 | 
			
		||||
                    if auto_trading:
 | 
			
		||||
                        new_pnl = safe_float(pnl_series) + total_pnl
 | 
			
		||||
                        await rq.set_pnl_series_by_symbol(
 | 
			
		||||
                            tg_id=tg_id, symbol=symbol, pnl_series=new_pnl)
 | 
			
		||||
                        text += f"\nПрибыль без комиссии: {ex_pnl:.4f}\n"
 | 
			
		||||
                        text += f"Реализованная прибыль: {total_pnl:.4f}\n"
 | 
			
		||||
                        text += f"Прибыль серии: {safe_float(new_pnl):.4f}\n"
 | 
			
		||||
                    else:
 | 
			
		||||
                        text += f"\nПрибыль без комиссии: {ex_pnl:.4f}\n"
 | 
			
		||||
                        text += f"Реализованная прибыль: {total_pnl:.4f}\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(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
 | 
			
		||||
                        )
 | 
			
		||||
                        await rq.set_pnl_series_by_symbol(tg_id=tg_id, symbol=symbol, pnl_series=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": "❗️ Превышен максимальный лимит ставки",
 | 
			
		||||
                                "Order placement failed as your position may exceed the max": "❗️ Превышен максимальный лимит ставки",
 | 
			
		||||
                            }
 | 
			
		||||
                            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": "❗️ Превышен максимальный лимит ставки",
 | 
			
		||||
                                "Order placement failed as your position may exceed the max": "❗️ Превышен максимальный лимит ставки",
 | 
			
		||||
                            }
 | 
			
		||||
                            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, exc_info=True)
 | 
			
		||||
							
								
								
									
										114
									
								
								app/bybit/web_socket.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								app/bybit/web_socket.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,114 @@
 | 
			
		||||
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(5)
 | 
			
		||||
                    await self.try_connect_user(api_key, api_secret, tg_id)
 | 
			
		||||
 | 
			
		||||
            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(
 | 
			
		||||
                demo=True,
 | 
			
		||||
                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 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_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 without duplicate processing."""
 | 
			
		||||
        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()
 | 
			
		||||
							
								
								
									
										187
									
								
								app/helper_functions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								app/helper_functions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,187 @@
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def truncate_float(f, decimals=4):
 | 
			
		||||
    factor = 10 ** decimals
 | 
			
		||||
    return int(f * factor) / factor
 | 
			
		||||
@@ -1,111 +0,0 @@
 | 
			
		||||
from aiogram import F, Router
 | 
			
		||||
import logging.config
 | 
			
		||||
 | 
			
		||||
from app.services.Bybit.functions.functions import start_bybit_trade_message
 | 
			
		||||
from logger_helper.logger_helper import LOGGING_CONFIG
 | 
			
		||||
import app.telegram.Keyboards.inline_keyboards as inline_markup
 | 
			
		||||
import app.telegram.Keyboards.reply_keyboards as reply_markup
 | 
			
		||||
 | 
			
		||||
import app.telegram.functions.main_settings.settings as func_main_settings
 | 
			
		||||
import app.telegram.functions.risk_management_settings.settings as func_rmanagement_settings
 | 
			
		||||
import app.telegram.functions.condition_settings.settings as func_condition_settings
 | 
			
		||||
import app.telegram.functions.additional_settings.settings as func_additional_settings
 | 
			
		||||
 | 
			
		||||
import app.telegram.database.requests as rq
 | 
			
		||||
from aiogram.types import Message, CallbackQuery
 | 
			
		||||
 | 
			
		||||
from app.states.States import state_reg_bybit_api
 | 
			
		||||
from aiogram.fsm.context import FSMContext
 | 
			
		||||
 | 
			
		||||
logging.config.dictConfig(LOGGING_CONFIG)
 | 
			
		||||
logger = logging.getLogger("add_bybit_api")
 | 
			
		||||
 | 
			
		||||
router_register_bybit_api = Router()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_register_bybit_api.callback_query(F.data == 'clb_new_user_connect_bybit_api_message')
 | 
			
		||||
async def info_for_bybit_api_message(callback: CallbackQuery) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Отвечает пользователю подробной инструкцией по подключению аккаунта Bybit.
 | 
			
		||||
    Показывает как создать API ключ и передать его чат-боту.
 | 
			
		||||
    """
 | 
			
		||||
    text = '''<b>Подключение Bybit аккаунта</b>
 | 
			
		||||
    
 | 
			
		||||
<b>1. Зарегистрируйтесь или войдите в свой аккаунт на Bybit (https://www.bybit.com/).</b>
 | 
			
		||||
<b>2. В личном кабинете выберите раздел API. </b>  
 | 
			
		||||
<b>3. Создание нового API ключа</b>  
 | 
			
		||||
   - Нажмите кнопку Create New Key (Создать новый ключ).
 | 
			
		||||
   - Выберите системно-сгенерированный ключ.
 | 
			
		||||
   - Укажите название API ключа (любое).  
 | 
			
		||||
   - Выберите права доступа для торговли (Trade).  
 | 
			
		||||
   - Можно ограничить доступ по IP для безопасности.
 | 
			
		||||
<b>4. Подтверждение создания</b>  
 | 
			
		||||
   - Подтвердите создание ключа.
 | 
			
		||||
   - Отправьте чат-роботу.
 | 
			
		||||
 | 
			
		||||
<b>Важно: сохраните отдельно API Key и Secret Key в надежном месте. Secret ключ отображается только один раз. </b>            
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    await callback.message.answer(text=text, parse_mode='html', reply_markup=inline_markup.connect_bybit_api_markup)
 | 
			
		||||
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_register_bybit_api.callback_query(F.data == 'clb_new_user_connect_bybit_api')
 | 
			
		||||
async def add_api_key_message(callback: CallbackQuery, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Инициирует процесс добавления API ключа.
 | 
			
		||||
    Переводит пользователя в состояние ожидания ввода API Key.
 | 
			
		||||
    """
 | 
			
		||||
    await state.set_state(state_reg_bybit_api.api_key)
 | 
			
		||||
 | 
			
		||||
    text = 'Отправьте KEY_API ниже: '
 | 
			
		||||
 | 
			
		||||
    await callback.message.answer(text=text)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_register_bybit_api.message(state_reg_bybit_api.api_key)
 | 
			
		||||
async def add_api_key_and_message_for_secret_key(message: Message, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Сохраняет API Key во временное состояние FSM,
 | 
			
		||||
    затем запрашивает у пользователя ввод Secret Key.
 | 
			
		||||
    """
 | 
			
		||||
    await state.update_data(api_key=message.text)
 | 
			
		||||
 | 
			
		||||
    text = 'Отправьте SECRET_KEY ниже'
 | 
			
		||||
 | 
			
		||||
    await message.answer(text=text)
 | 
			
		||||
 | 
			
		||||
    await state.set_state(state_reg_bybit_api.secret_key)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_register_bybit_api.message(state_reg_bybit_api.secret_key)
 | 
			
		||||
async def add_secret_key(message: Message, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Сохраняет Secret Key и финализирует регистрацию,
 | 
			
		||||
    обновляет базу данных, устанавливает символ пользователя и очищает состояние.
 | 
			
		||||
    """
 | 
			
		||||
    await state.update_data(secret_key=message.text)
 | 
			
		||||
 | 
			
		||||
    data = await state.get_data()
 | 
			
		||||
    user = await rq.check_user(message.from_user.id)
 | 
			
		||||
 | 
			
		||||
    await rq.upsert_api_keys(message.from_user.id, data['api_key'], data['secret_key'])
 | 
			
		||||
    await rq.set_new_user_symbol(message.from_user.id)
 | 
			
		||||
 | 
			
		||||
    await state.clear()
 | 
			
		||||
 | 
			
		||||
    await message.answer('Данные добавлены.',
 | 
			
		||||
                         reply_markup=reply_markup.base_buttons_markup)
 | 
			
		||||
 | 
			
		||||
    if user:
 | 
			
		||||
        await start_bybit_trade_message(message)
 | 
			
		||||
    else:
 | 
			
		||||
        await rq.save_tg_id_new_user(message.from_user.id)
 | 
			
		||||
 | 
			
		||||
        await func_main_settings.reg_new_user_default_main_settings(message.from_user.id, message)
 | 
			
		||||
        await func_rmanagement_settings.reg_new_user_default_risk_management_settings(message.from_user.id,
 | 
			
		||||
                                                                                      message)
 | 
			
		||||
        await func_condition_settings.reg_new_user_default_condition_settings(message.from_user.id)
 | 
			
		||||
        await func_additional_settings.reg_new_user_default_additional_settings(message.from_user.id, message)
 | 
			
		||||
        await start_bybit_trade_message(message)
 | 
			
		||||
@@ -1,874 +0,0 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import logging.config
 | 
			
		||||
import time
 | 
			
		||||
 | 
			
		||||
import app.services.Bybit.functions.balance as balance_g
 | 
			
		||||
import app.services.Bybit.functions.price_symbol as price_symbol
 | 
			
		||||
import app.telegram.database.requests as rq
 | 
			
		||||
import app.telegram.Keyboards.inline_keyboards as inline_markup
 | 
			
		||||
from logger_helper.logger_helper import LOGGING_CONFIG
 | 
			
		||||
from pybit import exceptions
 | 
			
		||||
from pybit.unified_trading import HTTP
 | 
			
		||||
 | 
			
		||||
logging.config.dictConfig(LOGGING_CONFIG)
 | 
			
		||||
logger = logging.getLogger("futures")
 | 
			
		||||
 | 
			
		||||
processed_trade_ids = set()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_bybit_client(tg_id):
 | 
			
		||||
    """
 | 
			
		||||
    Асинхронно получает экземпляр клиента Bybit.
 | 
			
		||||
 | 
			
		||||
    :param tg_id: int - ID пользователя Telegram
 | 
			
		||||
    :return: HTTP - экземпляр клиента Bybit
 | 
			
		||||
    """
 | 
			
		||||
    api_key = await rq.get_bybit_api_key(tg_id)
 | 
			
		||||
    secret_key = await rq.get_bybit_secret_key(tg_id)
 | 
			
		||||
    return HTTP(api_key=api_key, api_secret=secret_key)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def safe_float(val) -> float:
 | 
			
		||||
    """
 | 
			
		||||
    Безопасное преобразование значения в float.
 | 
			
		||||
    Возвращает 0.0, если значение None, пустое или некорректное.
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        if val is None or val == "":
 | 
			
		||||
            return 0.0
 | 
			
		||||
        return float(val)
 | 
			
		||||
    except (ValueError, TypeError):
 | 
			
		||||
        logger.error("Некорректное значение для преобразования в float", exc_info=True)
 | 
			
		||||
        return 0.0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def format_trade_details_position(data, commission_fee):
 | 
			
		||||
    """
 | 
			
		||||
    Форматирует информацию о сделке в виде строки.
 | 
			
		||||
    """
 | 
			
		||||
    msg = data.get("data", [{}])[0]
 | 
			
		||||
 | 
			
		||||
    closed_size = safe_float(msg.get("closedSize", 0))
 | 
			
		||||
    symbol = msg.get("symbol", "N/A")
 | 
			
		||||
    entry_price = safe_float(msg.get("execPrice", 0))
 | 
			
		||||
    qty = safe_float(msg.get("execQty", 0))
 | 
			
		||||
    order_type = msg.get("orderType", "N/A")
 | 
			
		||||
    side = msg.get("side", "")
 | 
			
		||||
    commission = safe_float(msg.get("execFee", 0))
 | 
			
		||||
    pnl = safe_float(msg.get("execPnl", 0))
 | 
			
		||||
 | 
			
		||||
    if commission_fee == "Да":
 | 
			
		||||
        pnl -= commission
 | 
			
		||||
 | 
			
		||||
    movement = ""
 | 
			
		||||
    if side.lower() == "buy":
 | 
			
		||||
        movement = "Покупка"
 | 
			
		||||
    elif side.lower() == "sell":
 | 
			
		||||
        movement = "Продажа"
 | 
			
		||||
    else:
 | 
			
		||||
        movement = side
 | 
			
		||||
 | 
			
		||||
    if closed_size > 0:
 | 
			
		||||
        return (
 | 
			
		||||
            f"Сделка закрыта:\n"
 | 
			
		||||
            f"Торговая пара: {symbol}\n"
 | 
			
		||||
            f"Цена исполнения: {entry_price:.6f}\n"
 | 
			
		||||
            f"Количество: {qty}\n"
 | 
			
		||||
            f"Закрыто позиций: {closed_size}\n"
 | 
			
		||||
            f"Тип ордера: {order_type}\n"
 | 
			
		||||
            f"Движение: {movement}\n"
 | 
			
		||||
            f"Комиссия за сделку: {commission:.6f}\n"
 | 
			
		||||
            f"Реализованная прибыль: {pnl:.6f} USDT"
 | 
			
		||||
        )
 | 
			
		||||
    if order_type == "Market":
 | 
			
		||||
        return (
 | 
			
		||||
            f"Сделка открыта:\n"
 | 
			
		||||
            f"Торговая пара: {symbol}\n"
 | 
			
		||||
            f"Цена исполнения: {entry_price:.6f}\n"
 | 
			
		||||
            f"Количество: {qty}\n"
 | 
			
		||||
            f"Тип ордера: {order_type}\n"
 | 
			
		||||
            f"Движение: {movement}\n"
 | 
			
		||||
            f"Комиссия за сделку: {commission:.6f}"
 | 
			
		||||
        )
 | 
			
		||||
    return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def format_order_details_position(data):
 | 
			
		||||
    """
 | 
			
		||||
    Форматирует информацию об ордере в виде строки.
 | 
			
		||||
    """
 | 
			
		||||
    msg = data.get("data", [{}])[0]
 | 
			
		||||
    price = safe_float(msg.get("price", 0))
 | 
			
		||||
    qty = safe_float(msg.get("qty", 0))
 | 
			
		||||
    cum_exec_qty = safe_float(msg.get("cumExecQty", 0))
 | 
			
		||||
    cum_exec_fee = safe_float(msg.get("cumExecFee", 0))
 | 
			
		||||
    take_profit = safe_float(msg.get("takeProfit", 0))
 | 
			
		||||
    stop_loss = safe_float(msg.get("stopLoss", 0))
 | 
			
		||||
    order_status = msg.get("orderStatus", "N/A")
 | 
			
		||||
    symbol = msg.get("symbol", "N/A")
 | 
			
		||||
    order_type = msg.get("orderType", "N/A")
 | 
			
		||||
    side = msg.get("side", "")
 | 
			
		||||
 | 
			
		||||
    movement = ""
 | 
			
		||||
    if side.lower() == "buy":
 | 
			
		||||
        movement = "Покупка"
 | 
			
		||||
    elif side.lower() == "sell":
 | 
			
		||||
        movement = "Продажа"
 | 
			
		||||
    else:
 | 
			
		||||
        movement = side
 | 
			
		||||
 | 
			
		||||
    if order_status.lower() == "filled" and order_type.lower() == "limit":
 | 
			
		||||
        text = (
 | 
			
		||||
            f"Ордер исполнен:\n"
 | 
			
		||||
            f"Торговая пара: {symbol}\n"
 | 
			
		||||
            f"Цена исполнения: {price:.6f}\n"
 | 
			
		||||
            f"Количество: {qty}\n"
 | 
			
		||||
            f"Исполнено позиций: {cum_exec_qty}\n"
 | 
			
		||||
            f"Тип ордера: {order_type}\n"
 | 
			
		||||
            f"Движение: {movement}\n"
 | 
			
		||||
            f"Тейк-профит: {take_profit:.6f}\n"
 | 
			
		||||
            f"Стоп-лосс: {stop_loss:.6f}\n"
 | 
			
		||||
            f"Комиссия за сделку: {cum_exec_fee:.6f}\n"
 | 
			
		||||
        )
 | 
			
		||||
        return text
 | 
			
		||||
 | 
			
		||||
    elif order_status.lower() == "new":
 | 
			
		||||
        text = (
 | 
			
		||||
            f"Ордер создан:\n"
 | 
			
		||||
            f"Торговая пара: {symbol}\n"
 | 
			
		||||
            f"Цена: {price:.6f}\n"
 | 
			
		||||
            f"Количество: {qty}\n"
 | 
			
		||||
            f"Тип ордера: {order_type}\n"
 | 
			
		||||
            f"Движение: {movement}\n"
 | 
			
		||||
            f"Тейк-профит: {take_profit:.6f}\n"
 | 
			
		||||
            f"Стоп-лосс: {stop_loss:.6f}\n"
 | 
			
		||||
        )
 | 
			
		||||
        return text
 | 
			
		||||
 | 
			
		||||
    elif order_status.lower() == "cancelled":
 | 
			
		||||
        text = (
 | 
			
		||||
            f"Ордер отменен:\n"
 | 
			
		||||
            f"Торговая пара: {symbol}\n"
 | 
			
		||||
            f"Цена: {price:.6f}\n"
 | 
			
		||||
            f"Количество: {qty}\n"
 | 
			
		||||
            f"Тип ордера: {order_type}\n"
 | 
			
		||||
            f"Движение: {movement}\n"
 | 
			
		||||
            f"Тейк-профит: {take_profit:.6f}\n"
 | 
			
		||||
            f"Стоп-лосс: {stop_loss:.6f}\n"
 | 
			
		||||
        )
 | 
			
		||||
        return text
 | 
			
		||||
    return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def parse_pnl_from_msg(msg) -> float:
 | 
			
		||||
    """
 | 
			
		||||
    Извлекает реализованную прибыль/убыток из сообщения.
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        data = msg.get("data", [{}])[0]
 | 
			
		||||
        return float(data.get("execPnl", 0))
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error("Ошибка при извлечении реализованной прибыли: %s", e)
 | 
			
		||||
        return 0.0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def calculate_total_budget(starting_quantity, martingale_factor, max_steps, commission_fee_percent, leverage, current_price):
 | 
			
		||||
    """
 | 
			
		||||
    Вычисляет общий бюджет серии ставок с учётом цены пары, комиссии и кредитного плеча.
 | 
			
		||||
 | 
			
		||||
    Параметры:
 | 
			
		||||
    - starting_quantity_usdt: стартовый размер ставки в долларах (USD)
 | 
			
		||||
    - martingale_factor: множитель увеличения ставки при каждом проигрыше
 | 
			
		||||
    - max_steps: максимальное количество шагов удвоения ставки
 | 
			
		||||
    - commission_fee_percent: процент комиссии на одну операцию (открытие или закрытие)
 | 
			
		||||
    - leverage: кредитное плечо
 | 
			
		||||
    - current_price: текущая цена актива (например BTCUSDT)
 | 
			
		||||
 | 
			
		||||
    Возвращает:
 | 
			
		||||
    - общий бюджет в долларах, который необходимо иметь на счету
 | 
			
		||||
    """
 | 
			
		||||
    total = 0
 | 
			
		||||
    for step in range(max_steps):
 | 
			
		||||
        quantity = starting_quantity * (martingale_factor ** step)  # размер ставки на текущем шаге в USDT
 | 
			
		||||
 | 
			
		||||
        # Переводим ставку из USDT в количество актива по текущей цене
 | 
			
		||||
        quantity_in_asset = quantity / current_price
 | 
			
		||||
 | 
			
		||||
        # Учитываем комиссию за вход и выход (умножаем на 2)
 | 
			
		||||
        quantity_with_fee = quantity * (1 + 2 * commission_fee_percent / 100)
 | 
			
		||||
 | 
			
		||||
        # Учитываем кредитное плечо - реальные собственные вложения меньше
 | 
			
		||||
        effective_quantity = quantity_with_fee / leverage
 | 
			
		||||
 | 
			
		||||
        total += effective_quantity
 | 
			
		||||
 | 
			
		||||
    # Возвращаем бюджет в USDT
 | 
			
		||||
    total_usdt = total * current_price
 | 
			
		||||
    return total_usdt
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def handle_execution_message(message, msg):
 | 
			
		||||
    """
 | 
			
		||||
    Обработчик сообщений об исполнении сделки.
 | 
			
		||||
    Логирует событие и проверяет условия для мартингейла и TP.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    tg_id = message.from_user.id
 | 
			
		||||
    data = msg.get("data", [{}])[0]
 | 
			
		||||
    data_main_risk_stgs = await rq.get_user_risk_management_settings(tg_id)
 | 
			
		||||
    commission_fee = data_main_risk_stgs.get("commission_fee", "ДА")
 | 
			
		||||
    pnl = parse_pnl_from_msg(msg)
 | 
			
		||||
    data_main_stgs = await rq.get_user_main_settings(tg_id)
 | 
			
		||||
    symbol = data.get("symbol")
 | 
			
		||||
    trading_mode = data_main_stgs.get("trading_mode", "Long")
 | 
			
		||||
    trigger = await rq.get_for_registration_trigger(tg_id)
 | 
			
		||||
    margin_mode = data_main_stgs.get("margin_type", "Isolated")
 | 
			
		||||
    starting_quantity = safe_float(data_main_stgs.get("starting_quantity"))
 | 
			
		||||
    martingale_factor = safe_float(data_main_stgs.get("martingale_factor"))
 | 
			
		||||
    closed_size = safe_float(data.get("closedSize", 0))
 | 
			
		||||
    commission = safe_float(data.get("execFee", 0))
 | 
			
		||||
 | 
			
		||||
    if commission_fee == "Да":
 | 
			
		||||
        pnl -= commission
 | 
			
		||||
 | 
			
		||||
    trade_info = format_trade_details_position(
 | 
			
		||||
        data=msg,
 | 
			
		||||
        commission_fee=commission_fee
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if trade_info:
 | 
			
		||||
        await message.answer(f"{trade_info}", reply_markup=inline_markup.back_to_main)
 | 
			
		||||
 | 
			
		||||
    if closed_size == 0:
 | 
			
		||||
        side = data.get("side", "")
 | 
			
		||||
 | 
			
		||||
        if side.lower() == "buy":
 | 
			
		||||
            await rq.set_last_series_info(tg_id, last_side="Buy")
 | 
			
		||||
        elif side.lower() == "sell":
 | 
			
		||||
            await rq.set_last_series_info(tg_id, last_side="Sell")
 | 
			
		||||
 | 
			
		||||
    if trigger == "Автоматический" and closed_size > 0:
 | 
			
		||||
        if pnl < 0:
 | 
			
		||||
 | 
			
		||||
            if trading_mode == 'Switch':
 | 
			
		||||
                side = data_main_stgs.get("last_side")
 | 
			
		||||
            else:
 | 
			
		||||
                side = "Buy" if trading_mode == "Long" else "Sell"
 | 
			
		||||
 | 
			
		||||
            current_martingale = await rq.get_martingale_step(tg_id)
 | 
			
		||||
            current_martingale_step = int(current_martingale)
 | 
			
		||||
            current_martingale += 1
 | 
			
		||||
            next_quantity = float(starting_quantity) * (
 | 
			
		||||
                    float(martingale_factor) ** current_martingale_step
 | 
			
		||||
            )
 | 
			
		||||
            await rq.update_martingale_step(tg_id, current_martingale)
 | 
			
		||||
            await message.answer(
 | 
			
		||||
                f"❗️ Сделка закрылась в минус, открываю новую сделку с увеличенной ставкой.\n"
 | 
			
		||||
            )
 | 
			
		||||
            await open_position(
 | 
			
		||||
                tg_id,
 | 
			
		||||
                message,
 | 
			
		||||
                side=side,
 | 
			
		||||
                margin_mode=margin_mode,
 | 
			
		||||
                symbol=symbol,
 | 
			
		||||
                quantity=next_quantity,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        elif pnl > 0:
 | 
			
		||||
            await rq.update_martingale_step(tg_id, 0)
 | 
			
		||||
            await message.answer(
 | 
			
		||||
                "❗️ Прибыль достигнута, шаг мартингейла сброшен."
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def handle_order_message(message, msg: dict) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Обработчик сообщений об исполнении ордера.
 | 
			
		||||
    Логирует событие и проверяет условия для мартингейла и TP.
 | 
			
		||||
    """
 | 
			
		||||
    # logger.info(f"Исполнен ордер:\n{json.dumps(msg, indent=4, ensure_ascii=False)}")
 | 
			
		||||
 | 
			
		||||
    trade_info = format_order_details_position(msg)
 | 
			
		||||
 | 
			
		||||
    if trade_info:
 | 
			
		||||
        await message.answer(f"{trade_info}", reply_markup=inline_markup.back_to_main)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def error_max_step(message) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Сообщение об ошибке превышения максимального количества шагов мартингейла.
 | 
			
		||||
    """
 | 
			
		||||
    logger.error(
 | 
			
		||||
        "Сделка не была совершена, превышен лимит максимального количества ставок в серии."
 | 
			
		||||
    )
 | 
			
		||||
    await message.answer(
 | 
			
		||||
        "Сделка не была совершена, превышен лимит максимального количества ставок в серии.",
 | 
			
		||||
        reply_markup=inline_markup.back_to_main,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def error_max_risk(message) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Сообщение об ошибке превышения риск-лимита сделки.
 | 
			
		||||
    """
 | 
			
		||||
    logger.error("Сделка не была совершена, риск убытка превышает допустимый лимит.")
 | 
			
		||||
    await message.answer(
 | 
			
		||||
        "Сделка не была совершена, риск убытка превышает допустимый лимит.",
 | 
			
		||||
        reply_markup=inline_markup.back_to_main,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def open_position(
 | 
			
		||||
        tg_id, message, side: str, margin_mode: str, symbol, quantity, tpsl_mode="Full"
 | 
			
		||||
):
 | 
			
		||||
    """
 | 
			
		||||
    Открывает позицию на Bybit с учётом настроек пользователя, маржи, размера лота, платформы и риска.
 | 
			
		||||
 | 
			
		||||
    Возвращает True при успехе, False при ошибках открытия ордера, None при исключениях.
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        client = await get_bybit_client(tg_id)
 | 
			
		||||
        data_main_stgs = await rq.get_user_main_settings(tg_id)
 | 
			
		||||
        order_type = data_main_stgs.get("entry_order_type")
 | 
			
		||||
        bybit_margin_mode = (
 | 
			
		||||
            "ISOLATED_MARGIN" if margin_mode == "Isolated" else "REGULAR_MARGIN"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        limit_price = None
 | 
			
		||||
        if order_type == "Limit":
 | 
			
		||||
            limit_price = await rq.get_limit_price(tg_id)
 | 
			
		||||
        data_risk_stgs = await rq.get_user_risk_management_settings(tg_id)
 | 
			
		||||
 | 
			
		||||
        price = await price_symbol.get_price(tg_id, symbol=symbol)
 | 
			
		||||
        entry_price = safe_float(price)
 | 
			
		||||
        leverage = safe_float(data_main_stgs.get("size_leverage", 1))
 | 
			
		||||
 | 
			
		||||
        max_martingale_steps = int(data_main_stgs.get("maximal_quantity", 0))
 | 
			
		||||
        current_martingale = await rq.get_martingale_step(tg_id)
 | 
			
		||||
        max_risk_percent = safe_float(data_risk_stgs.get("max_risk_deal"))
 | 
			
		||||
        loss_profit = safe_float(data_risk_stgs.get("price_loss"))
 | 
			
		||||
        commission_fee = data_risk_stgs.get("commission_fee")
 | 
			
		||||
        starting_quantity = safe_float(data_main_stgs.get('starting_quantity'))
 | 
			
		||||
        martingale_factor = safe_float(data_main_stgs.get('martingale_factor'))
 | 
			
		||||
        fee_info = client.get_fee_rates(category='linear', symbol=symbol)
 | 
			
		||||
        instruments_resp = client.get_instruments_info(category="linear", symbol=symbol)
 | 
			
		||||
        instrument = instruments_resp.get("result", {}).get("list", [])
 | 
			
		||||
 | 
			
		||||
        if commission_fee == "Да":
 | 
			
		||||
            commission_fee_percent = safe_float(fee_info['result']['list'][0]['takerFeeRate'])
 | 
			
		||||
        else:
 | 
			
		||||
            commission_fee_percent = 0.0
 | 
			
		||||
 | 
			
		||||
        total_budget = await calculate_total_budget(
 | 
			
		||||
            starting_quantity=starting_quantity,
 | 
			
		||||
            martingale_factor=martingale_factor,
 | 
			
		||||
            max_steps=max_martingale_steps,
 | 
			
		||||
            commission_fee_percent=commission_fee_percent,
 | 
			
		||||
            leverage=leverage,
 | 
			
		||||
            current_price=entry_price,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        balance = await balance_g.get_balance(tg_id, message)
 | 
			
		||||
        if safe_float(balance) < total_budget:
 | 
			
		||||
            logger.error(
 | 
			
		||||
                f"Недостаточно средств для серии из {max_martingale_steps} шагов с текущими параметрами. "
 | 
			
		||||
                f"Требуемый бюджет: {total_budget:.2f} USDT, доступно: {balance} USDT."
 | 
			
		||||
            )
 | 
			
		||||
            await message.answer(
 | 
			
		||||
                f"Недостаточно средств для серии из {max_martingale_steps} шагов с текущими параметрами. "
 | 
			
		||||
                f"Требуемый бюджет: {total_budget:.2f} USDT, доступно: {balance} USDT.",
 | 
			
		||||
                reply_markup=inline_markup.back_to_main,
 | 
			
		||||
            )
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if order_type == "Limit" and limit_price:
 | 
			
		||||
            price_for_calc = limit_price
 | 
			
		||||
        else:
 | 
			
		||||
            price_for_calc = entry_price
 | 
			
		||||
 | 
			
		||||
        potential_loss = safe_float(quantity) * price_for_calc * (loss_profit / 100)
 | 
			
		||||
        adjusted_loss = potential_loss / leverage
 | 
			
		||||
        allowed_loss = safe_float(balance) * (max_risk_percent / 100)
 | 
			
		||||
 | 
			
		||||
        if adjusted_loss > allowed_loss:
 | 
			
		||||
            await error_max_risk(message)
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if max_martingale_steps < current_martingale:
 | 
			
		||||
            await error_max_step(message)
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        client.set_margin_mode(setMarginMode=bybit_margin_mode)
 | 
			
		||||
        max_leverage = safe_float(instrument[0].get("leverageFilter", {}).get("maxLeverage", 0))
 | 
			
		||||
 | 
			
		||||
        if safe_float(leverage) > max_leverage:
 | 
			
		||||
            await message.answer(
 | 
			
		||||
                f"Запрошенное кредитное плечо {leverage} превышает максимальное {max_leverage} для {symbol}. "
 | 
			
		||||
                f"Устанавливаю максимальное.",
 | 
			
		||||
                reply_markup=inline_markup.back_to_main,
 | 
			
		||||
            )
 | 
			
		||||
            logger.info(
 | 
			
		||||
                f"Запрошенное кредитное плечо {leverage} превышает максимальное {max_leverage} для {symbol}. Устанавливаю максимальное.")
 | 
			
		||||
            leverage_to_set = max_leverage
 | 
			
		||||
        else:
 | 
			
		||||
            leverage_to_set = safe_float(leverage)
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            client.set_leverage(
 | 
			
		||||
                category="linear",
 | 
			
		||||
                symbol=symbol,
 | 
			
		||||
                buyLeverage=str(leverage_to_set),
 | 
			
		||||
                sellLeverage=str(leverage_to_set),
 | 
			
		||||
            )
 | 
			
		||||
            logger.info(f"Set leverage to {leverage_to_set} for {symbol}")
 | 
			
		||||
        except exceptions.InvalidRequestError as e:
 | 
			
		||||
            if "110043" in str(e):
 | 
			
		||||
                logger.info(f"Leverage already set to {leverage} for {symbol}")
 | 
			
		||||
            else:
 | 
			
		||||
                raise e
 | 
			
		||||
 | 
			
		||||
        if instruments_resp.get("retCode") == 0:
 | 
			
		||||
            instrument_info = instruments_resp.get("result", {}).get("list", [])
 | 
			
		||||
            if instrument_info:
 | 
			
		||||
                instrument_info = instrument_info[0]
 | 
			
		||||
                min_notional_value = float(instrument_info.get("lotSizeFilter", {}).get("minNotionalValue", 0))
 | 
			
		||||
                min_order_value = min_notional_value
 | 
			
		||||
            else:
 | 
			
		||||
                min_order_value = 5.0
 | 
			
		||||
 | 
			
		||||
        order_value = float(quantity) * price_for_calc
 | 
			
		||||
        if order_value < min_order_value:
 | 
			
		||||
            logger.error(
 | 
			
		||||
                f"Сумма ордера слишком мала: {order_value:.2f} USDT. "
 | 
			
		||||
                f"Минимум для торговли — {min_order_value} USDT. "
 | 
			
		||||
                f"Пожалуйста, увеличьте количество позиций."
 | 
			
		||||
            )
 | 
			
		||||
            await message.answer(
 | 
			
		||||
                f"Сумма ордера слишком мала: {order_value:.2f} USDT. "
 | 
			
		||||
                f"Минимум для торговли — {min_order_value} USDT. "
 | 
			
		||||
                f"Пожалуйста, увеличьте количество позиций.",
 | 
			
		||||
                reply_markup=inline_markup.back_to_main,
 | 
			
		||||
            )
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        if bybit_margin_mode == "ISOLATED_MARGIN":
 | 
			
		||||
            # Открываем позицию
 | 
			
		||||
            response = client.place_order(
 | 
			
		||||
                category="linear",
 | 
			
		||||
                symbol=symbol,
 | 
			
		||||
                side=side,
 | 
			
		||||
                orderType=order_type,
 | 
			
		||||
                qty=str(quantity),
 | 
			
		||||
                price=(
 | 
			
		||||
                    str(limit_price) if order_type == "Limit" and limit_price else None
 | 
			
		||||
                ),
 | 
			
		||||
                timeInForce="GTC",
 | 
			
		||||
                orderLinkId=f"deal_{symbol}_{int(time.time())}",
 | 
			
		||||
            )
 | 
			
		||||
            if response.get("retCode", -1) != 0:
 | 
			
		||||
                logger.error(f"Ошибка открытия ордера: {response}")
 | 
			
		||||
                await message.answer(
 | 
			
		||||
                    f"Ошибка открытия ордера", reply_markup=inline_markup.back_to_main
 | 
			
		||||
                )
 | 
			
		||||
                return False
 | 
			
		||||
 | 
			
		||||
            # Получаем цену ликвидации
 | 
			
		||||
            positions = client.get_positions(category="linear", symbol=symbol)
 | 
			
		||||
            pos = positions.get("result", {}).get("list", [{}])[0]
 | 
			
		||||
            avg_price = float(pos.get("avgPrice", 0))
 | 
			
		||||
            liq_price = safe_float(pos.get("liqPrice", 0))
 | 
			
		||||
 | 
			
		||||
            if liq_price > 0 and avg_price > 0:
 | 
			
		||||
                if side.lower() == "buy":
 | 
			
		||||
                    take_profit_price = avg_price + (avg_price - liq_price)
 | 
			
		||||
                else:
 | 
			
		||||
                    take_profit_price = avg_price - (liq_price - avg_price)
 | 
			
		||||
 | 
			
		||||
                take_profit_price = max(take_profit_price, 0)
 | 
			
		||||
 | 
			
		||||
                try:
 | 
			
		||||
                    try:
 | 
			
		||||
                        client.set_tp_sl_mode(
 | 
			
		||||
                            symbol=symbol, category="linear", tpSlMode="Full"
 | 
			
		||||
                        )
 | 
			
		||||
                    except exceptions.InvalidRequestError as e:
 | 
			
		||||
                        if "same tp sl mode" in str(e):
 | 
			
		||||
                            logger.info("Режим TP/SL уже установлен - пропускаем")
 | 
			
		||||
                        else:
 | 
			
		||||
                            raise
 | 
			
		||||
                    resp = client.set_trading_stop(
 | 
			
		||||
                        category="linear",
 | 
			
		||||
                        symbol=symbol,
 | 
			
		||||
                        takeProfit=str(round(take_profit_price, 5)),
 | 
			
		||||
                        tpTriggerBy="LastPrice",
 | 
			
		||||
                        slTriggerBy="LastPrice",
 | 
			
		||||
                        positionIdx=0,
 | 
			
		||||
                        reduceOnly=False,
 | 
			
		||||
                        tpslMode=tpsl_mode,
 | 
			
		||||
                    )
 | 
			
		||||
                except Exception as e:
 | 
			
		||||
                    logger.error(f"Ошибка установки TP/SL: {e}")
 | 
			
		||||
                    await message.answer(
 | 
			
		||||
                        "Ошибка при установке Take Profit и Stop Loss.",
 | 
			
		||||
                        reply_markup=inline_markup.back_to_main,
 | 
			
		||||
                    )
 | 
			
		||||
                    return False
 | 
			
		||||
            else:
 | 
			
		||||
                logger.warning("Не удалось получить цену ликвидации для позиции")
 | 
			
		||||
 | 
			
		||||
        else:  # REGULAR_MARGIN
 | 
			
		||||
            try:
 | 
			
		||||
                client.set_tp_sl_mode(symbol=symbol, category="linear", tpSlMode="Full")
 | 
			
		||||
            except exceptions.InvalidRequestError as e:
 | 
			
		||||
                if "same tp sl mode" in str(e):
 | 
			
		||||
                    logger.info("Режим TP/SL уже установлен - пропускаем")
 | 
			
		||||
                else:
 | 
			
		||||
                    raise
 | 
			
		||||
 | 
			
		||||
            if order_type == "Market":
 | 
			
		||||
                base_price = entry_price
 | 
			
		||||
            else:
 | 
			
		||||
                base_price = limit_price
 | 
			
		||||
 | 
			
		||||
            if side.lower() == "buy":
 | 
			
		||||
                take_profit_price = base_price * (1 + loss_profit / 100)
 | 
			
		||||
                stop_loss_price = base_price * (1 - loss_profit / 100)
 | 
			
		||||
            else:
 | 
			
		||||
                take_profit_price = base_price * (1 - loss_profit / 100)
 | 
			
		||||
                stop_loss_price = base_price * (1 + loss_profit / 100)
 | 
			
		||||
 | 
			
		||||
            take_profit_price = max(take_profit_price, 0)
 | 
			
		||||
            stop_loss_price = max(stop_loss_price, 0)
 | 
			
		||||
 | 
			
		||||
            if tpsl_mode == "Full":
 | 
			
		||||
                tp_order_type = "Market"
 | 
			
		||||
                sl_order_type = "Market"
 | 
			
		||||
                tp_limit_price = None
 | 
			
		||||
                sl_limit_price = None
 | 
			
		||||
            else:  # Partial
 | 
			
		||||
                tp_order_type = "Limit"
 | 
			
		||||
                sl_order_type = "Limit"
 | 
			
		||||
                tp_limit_price = take_profit_price
 | 
			
		||||
                sl_limit_price = stop_loss_price
 | 
			
		||||
 | 
			
		||||
            response = client.place_order(
 | 
			
		||||
                category="linear",
 | 
			
		||||
                symbol=symbol,
 | 
			
		||||
                side=side,
 | 
			
		||||
                orderType=order_type,
 | 
			
		||||
                qty=str(quantity),
 | 
			
		||||
                price=(
 | 
			
		||||
                    str(limit_price) if order_type == "Limit" and limit_price else None
 | 
			
		||||
                ),
 | 
			
		||||
                takeProfit=str(take_profit_price),
 | 
			
		||||
                tpOrderType=tp_order_type,
 | 
			
		||||
                tpLimitPrice=str(tp_limit_price) if tp_limit_price else None,
 | 
			
		||||
                stopLoss=str(stop_loss_price),
 | 
			
		||||
                slOrderType=sl_order_type,
 | 
			
		||||
                slLimitPrice=str(sl_limit_price) if sl_limit_price else None,
 | 
			
		||||
                tpslMode=tpsl_mode,
 | 
			
		||||
                timeInForce="GTC",
 | 
			
		||||
                orderLinkId=f"deal_{symbol}_{int(time.time())}",
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            if response.get("retCode", -1) == 0:
 | 
			
		||||
                return True
 | 
			
		||||
            else:
 | 
			
		||||
                logger.error(f"Ошибка открытия ордера: {response}")
 | 
			
		||||
                await message.answer(
 | 
			
		||||
                    f"Ошибка открытия ордера", reply_markup=inline_markup.back_to_main
 | 
			
		||||
                )
 | 
			
		||||
                return False
 | 
			
		||||
 | 
			
		||||
        return None
 | 
			
		||||
    except exceptions.InvalidRequestError as e:
 | 
			
		||||
        logger.error("InvalidRequestError: %s", e)
 | 
			
		||||
        error_text = str(e)
 | 
			
		||||
        if "estimated will trigger liq" in error_text:
 | 
			
		||||
            await message.answer(
 | 
			
		||||
                "Лимитный ордер может вызвать мгновенную ликвидацию. Проверьте параметры ордера.",
 | 
			
		||||
                reply_markup=inline_markup.back_to_main,
 | 
			
		||||
            )
 | 
			
		||||
        elif "ab not enough for new order" in error_text:
 | 
			
		||||
            await message.answer("Недостаточно средств для нового ордера",
 | 
			
		||||
                                 reply_markup=inline_markup.back_to_main)
 | 
			
		||||
        else:
 | 
			
		||||
            logger.error("Ошибка при совершении сделки: %s", e)
 | 
			
		||||
            await message.answer(
 | 
			
		||||
                "Недостаточно средств для размещения нового ордера с заданным количеством и плечом.",
 | 
			
		||||
                reply_markup=inline_markup.back_to_main,
 | 
			
		||||
            )
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error("Ошибка при совершении сделки: %s", e)
 | 
			
		||||
        await message.answer(
 | 
			
		||||
            "Возникла ошибка при попытке открыть позицию.",
 | 
			
		||||
            reply_markup=inline_markup.back_to_main,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def set_take_profit_stop_loss(
 | 
			
		||||
        tg_id: int,
 | 
			
		||||
        message,
 | 
			
		||||
        take_profit_price: float,
 | 
			
		||||
        stop_loss_price: float,
 | 
			
		||||
        tpsl_mode="Full",
 | 
			
		||||
):
 | 
			
		||||
    """
 | 
			
		||||
    Устанавливает уровни Take Profit и Stop Loss для открытой позиции.
 | 
			
		||||
    """
 | 
			
		||||
    symbol = await rq.get_symbol(tg_id)
 | 
			
		||||
    client = await get_bybit_client(tg_id)
 | 
			
		||||
    await cancel_all_tp_sl_orders(tg_id, symbol)
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        try:
 | 
			
		||||
            client.set_tp_sl_mode(symbol=symbol, category="linear", tpSlMode=tpsl_mode)
 | 
			
		||||
        except exceptions.InvalidRequestError as e:
 | 
			
		||||
            if "same tp sl mode" in str(e).lower():
 | 
			
		||||
                logger.info("Режим TP/SL уже установлен для %s - пропускаем", symbol)
 | 
			
		||||
            else:
 | 
			
		||||
                raise
 | 
			
		||||
 | 
			
		||||
        resp = client.set_trading_stop(
 | 
			
		||||
            category="linear",
 | 
			
		||||
            symbol=symbol,
 | 
			
		||||
            takeProfit=str(round(take_profit_price, 5)),
 | 
			
		||||
            stopLoss=str(round(stop_loss_price, 5)),
 | 
			
		||||
            tpTriggerBy="LastPrice",
 | 
			
		||||
            slTriggerBy="LastPrice",
 | 
			
		||||
            positionIdx=0,
 | 
			
		||||
            reduceOnly=False,
 | 
			
		||||
            tpslMode=tpsl_mode,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if resp.get("retCode") != 0:
 | 
			
		||||
            await message.answer(
 | 
			
		||||
                f"Ошибка обновления TP/SL: {resp.get('retMsg')}",
 | 
			
		||||
                reply_markup=inline_markup.back_to_main,
 | 
			
		||||
            )
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        await message.answer(
 | 
			
		||||
            f"ТП и СЛ успешно установлены:\nТейк-профит: {take_profit_price:.5f}\nСтоп-лосс: {stop_loss_price:.5f}",
 | 
			
		||||
            reply_markup=inline_markup.back_to_main,
 | 
			
		||||
        )
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error(f"Ошибка установки TP/SL для {symbol}: {e}", exc_info=True)
 | 
			
		||||
        await message.answer(
 | 
			
		||||
            "Произошла ошибка при установке TP и SL.",
 | 
			
		||||
            reply_markup=inline_markup.back_to_main,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def cancel_all_tp_sl_orders(tg_id, symbol):
 | 
			
		||||
    """
 | 
			
		||||
    Отменяет лимитные ордера для указанного символа.
 | 
			
		||||
    """
 | 
			
		||||
    client = await get_bybit_client(tg_id)
 | 
			
		||||
    last_response = None
 | 
			
		||||
    try:
 | 
			
		||||
        orders_resp = client.get_open_orders(category="linear", symbol=symbol)
 | 
			
		||||
        orders = orders_resp.get("result", {}).get("list", [])
 | 
			
		||||
 | 
			
		||||
        for order in orders:
 | 
			
		||||
            order_id = order.get("orderId")
 | 
			
		||||
            order_symbol = order.get("symbol")
 | 
			
		||||
            cancel_resp = client.cancel_order(
 | 
			
		||||
                category="linear", symbol=symbol, orderId=order_id
 | 
			
		||||
            )
 | 
			
		||||
            if cancel_resp.get("retCode") != 0:
 | 
			
		||||
                logger.warning(
 | 
			
		||||
                    f"Не удалось отменить ордер {order_id}: {cancel_resp.get('retMsg')}"
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                last_response = order_symbol
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error(f"Ошибка при отмене ордера: {e}")
 | 
			
		||||
 | 
			
		||||
    return last_response
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_active_positions(tg_id, message):
 | 
			
		||||
    """
 | 
			
		||||
    Показывает активные позиции пользователя.
 | 
			
		||||
    """
 | 
			
		||||
    client = await get_bybit_client(tg_id)
 | 
			
		||||
    active_positions = client.get_positions(category="linear", settleCoin="USDT")
 | 
			
		||||
    positions = active_positions.get("result", {}).get("list", [])
 | 
			
		||||
    active_symbols = [
 | 
			
		||||
        pos.get("symbol") for pos in positions if float(pos.get("size", 0)) > 0
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    if active_symbols:
 | 
			
		||||
        await message.answer(
 | 
			
		||||
            "📈 Ваши активные позиции:",
 | 
			
		||||
            reply_markup=inline_markup.create_trades_inline_keyboard(active_symbols),
 | 
			
		||||
        )
 | 
			
		||||
    else:
 | 
			
		||||
        await message.answer(
 | 
			
		||||
            "❗️ У вас нет активных позиций.", reply_markup=inline_markup.back_to_main
 | 
			
		||||
        )
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_active_positions_by_symbol(tg_id, symbol, message):
 | 
			
		||||
    """
 | 
			
		||||
    Показывает активные позиции пользователя по символу.
 | 
			
		||||
    """
 | 
			
		||||
    client = await get_bybit_client(tg_id)
 | 
			
		||||
    active_positions = client.get_positions(category="linear", symbol=symbol)
 | 
			
		||||
    positions = active_positions.get("result", {}).get("list", [])
 | 
			
		||||
    pos = positions[0] if positions else None
 | 
			
		||||
 | 
			
		||||
    if float(pos.get("size", 0)) == 0:
 | 
			
		||||
        await message.answer(
 | 
			
		||||
            "❗️ У вас нет активных позиций.", reply_markup=inline_markup.back_to_main
 | 
			
		||||
        )
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    text = (
 | 
			
		||||
        f"Торговая пара: {pos.get('symbol')}\n"
 | 
			
		||||
        f"Цена входа: {pos.get('avgPrice')}\n"
 | 
			
		||||
        f"Движение: {pos.get('side')}\n"
 | 
			
		||||
        f"Кредитное плечо: {pos.get('leverage')}x\n"
 | 
			
		||||
        f"Количество: {pos.get('size')}\n"
 | 
			
		||||
        f"Тейк-профит: {pos.get('takeProfit')}\n"
 | 
			
		||||
        f"Стоп-лосс: {pos.get('stopLoss')}\n"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    await message.answer(
 | 
			
		||||
        text, reply_markup=inline_markup.create_close_deal_markup(symbol)
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_active_orders(tg_id, message):
 | 
			
		||||
    """
 | 
			
		||||
    Показывает активные лимитные ордера пользователя.
 | 
			
		||||
    """
 | 
			
		||||
    client = await get_bybit_client(tg_id)
 | 
			
		||||
    response = client.get_open_orders(
 | 
			
		||||
        category="linear", settleCoin="USDT", orderType="Limit"
 | 
			
		||||
    )
 | 
			
		||||
    orders = response.get("result", {}).get("list", [])
 | 
			
		||||
    limit_orders = [order for order in orders if order.get("orderType") == "Limit"]
 | 
			
		||||
 | 
			
		||||
    if limit_orders:
 | 
			
		||||
        symbols = [order["symbol"] for order in limit_orders]
 | 
			
		||||
        await message.answer(
 | 
			
		||||
            "📈 Ваши активные лимитные ордера:",
 | 
			
		||||
            reply_markup=inline_markup.create_trades_inline_keyboard_limits(symbols),
 | 
			
		||||
        )
 | 
			
		||||
    else:
 | 
			
		||||
        await message.answer(
 | 
			
		||||
            "❗️ У вас нет активных лимитных ордеров.",
 | 
			
		||||
            reply_markup=inline_markup.back_to_main,
 | 
			
		||||
        )
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_active_orders_by_symbol(tg_id, symbol, message):
 | 
			
		||||
    """
 | 
			
		||||
    Показывает активные лимитные ордера пользователя по символу.
 | 
			
		||||
    """
 | 
			
		||||
    client = await get_bybit_client(tg_id)
 | 
			
		||||
    active_orders = client.get_open_orders(category="linear", symbol=symbol)
 | 
			
		||||
    limit_orders = [
 | 
			
		||||
        order
 | 
			
		||||
        for order in active_orders.get("result", {}).get("list", [])
 | 
			
		||||
        if order.get("orderType") == "Limit"
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    if not limit_orders:
 | 
			
		||||
        await message.answer(
 | 
			
		||||
            "Нет активных лимитных ордеров по данной торговой паре.",
 | 
			
		||||
            reply_markup=inline_markup.back_to_main,
 | 
			
		||||
        )
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    texts = []
 | 
			
		||||
    for order in limit_orders:
 | 
			
		||||
        text = (
 | 
			
		||||
            f"Торговая пара: {order.get('symbol')}\n"
 | 
			
		||||
            f"Тип ордера: {order.get('orderType')}\n"
 | 
			
		||||
            f"Сторона: {order.get('side')}\n"
 | 
			
		||||
            f"Цена: {order.get('price')}\n"
 | 
			
		||||
            f"Количество: {order.get('qty')}\n"
 | 
			
		||||
            f"Тейк-профит: {order.get('takeProfit')}\n"
 | 
			
		||||
            f"Стоп-лосс: {order.get('stopLoss')}\n"
 | 
			
		||||
        )
 | 
			
		||||
        texts.append(text)
 | 
			
		||||
 | 
			
		||||
    await message.answer(
 | 
			
		||||
        "\n\n".join(texts), reply_markup=inline_markup.create_close_limit_markup(symbol)
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def close_user_trade(tg_id: int, symbol: str):
 | 
			
		||||
    """
 | 
			
		||||
    Закрывает открытые позиции пользователя по символу рыночным ордером.
 | 
			
		||||
    Возвращает True при успехе, False при ошибках.
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        client = await get_bybit_client(tg_id)
 | 
			
		||||
        positions_resp = client.get_positions(category="linear", symbol=symbol)
 | 
			
		||||
 | 
			
		||||
        if positions_resp.get("retCode") != 0:
 | 
			
		||||
            return False
 | 
			
		||||
        positions_list = positions_resp.get("result", {}).get("list", [])
 | 
			
		||||
        if not positions_list:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        position = positions_list[0]
 | 
			
		||||
        qty = abs(safe_float(position.get("size")))
 | 
			
		||||
        side = position.get("side")
 | 
			
		||||
        if qty == 0:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        close_side = "Sell" if side == "Buy" else "Buy"
 | 
			
		||||
 | 
			
		||||
        place_resp = client.place_order(
 | 
			
		||||
            category="linear",
 | 
			
		||||
            symbol=symbol,
 | 
			
		||||
            side=close_side,
 | 
			
		||||
            orderType="Market",
 | 
			
		||||
            qty=str(qty),
 | 
			
		||||
            timeInForce="GTC",
 | 
			
		||||
            reduceOnly=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if place_resp.get("retCode") == 0:
 | 
			
		||||
            return True
 | 
			
		||||
        else:
 | 
			
		||||
            return False
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error(
 | 
			
		||||
            f"Ошибка закрытия сделки {symbol} для пользователя {tg_id}: {e}",
 | 
			
		||||
            exc_info=True,
 | 
			
		||||
        )
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def close_trade_after_delay(tg_id: int, message, symbol: str, delay_sec: int):
 | 
			
		||||
    """
 | 
			
		||||
    Закрывает сделку пользователя после задержки delay_sec секунд.
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        await asyncio.sleep(delay_sec)
 | 
			
		||||
        result = await close_user_trade(tg_id, symbol)
 | 
			
		||||
        if result:
 | 
			
		||||
            await message.answer(
 | 
			
		||||
                f"Сделка {symbol} успешно закрыта по таймеру."
 | 
			
		||||
            )
 | 
			
		||||
            logger.info(f"Сделка {symbol} успешно закрыта по таймеру.")
 | 
			
		||||
        else:
 | 
			
		||||
            await message.answer(
 | 
			
		||||
                f"Не удалось закрыть сделку {symbol} по таймеру.",
 | 
			
		||||
                reply_markup=inline_markup.back_to_main,
 | 
			
		||||
            )
 | 
			
		||||
            logger.error(f"Не удалось закрыть сделку {symbol} по таймеру.")
 | 
			
		||||
    except asyncio.CancelledError:
 | 
			
		||||
        await message.answer(
 | 
			
		||||
            f"Закрытие сделки {symbol} по таймеру отменено.",
 | 
			
		||||
            reply_markup=inline_markup.back_to_main,
 | 
			
		||||
        )
 | 
			
		||||
        logger.info(f"Закрытие сделки {symbol} по таймеру отменено.")
 | 
			
		||||
@@ -1,52 +0,0 @@
 | 
			
		||||
import app.telegram.database.requests as rq
 | 
			
		||||
import app.telegram.Keyboards.inline_keyboards as inline_markup
 | 
			
		||||
import logging.config
 | 
			
		||||
from logger_helper.logger_helper import LOGGING_CONFIG
 | 
			
		||||
from pybit.unified_trading import HTTP
 | 
			
		||||
 | 
			
		||||
logging.config.dictConfig(LOGGING_CONFIG)
 | 
			
		||||
logger = logging.getLogger("balance")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_balance(tg_id: int, message) -> float:
 | 
			
		||||
    """
 | 
			
		||||
    Асинхронно получает общий баланс пользователя на Bybit.
 | 
			
		||||
 | 
			
		||||
    Процедура:
 | 
			
		||||
    - Получает API ключ и секрет пользователя из базы данных.
 | 
			
		||||
    - Если ключи не заданы, отправляет пользователю сообщение с предложением подключить платформу.
 | 
			
		||||
    - Создает клиент Bybit с ключами.
 | 
			
		||||
    - Запрашивает общий баланс по типу аккаунта UNIFIED.
 | 
			
		||||
    - Если ответ успешен, возвращает баланс в виде float.
 | 
			
		||||
    - При ошибках API или исключениях логирует ошибку и уведомляет пользователя.
 | 
			
		||||
 | 
			
		||||
    :param tg_id: int - идентификатор пользователя Telegram
 | 
			
		||||
    :param message: объект сообщения для отправки ответов пользователю
 | 
			
		||||
    :return: float - общий баланс пользователя; 0 при ошибке или отсутствии ключей
 | 
			
		||||
    """
 | 
			
		||||
    api_key = await rq.get_bybit_api_key(tg_id)
 | 
			
		||||
    secret_key = await rq.get_bybit_secret_key(tg_id)
 | 
			
		||||
 | 
			
		||||
    client = HTTP(
 | 
			
		||||
        api_key=api_key,
 | 
			
		||||
        api_secret=secret_key
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if api_key is None or secret_key is None:
 | 
			
		||||
        await message.answer('⚠️ Подключите платформу для торговли',
 | 
			
		||||
                             reply_markup=inline_markup.connect_bybit_api_message)
 | 
			
		||||
        return 0
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        response = client.get_wallet_balance(accountType='UNIFIED')
 | 
			
		||||
        if response['retCode'] == 0:
 | 
			
		||||
            total_balance = response['result']['list'][0].get('totalWalletBalance', '0')
 | 
			
		||||
            return total_balance
 | 
			
		||||
        else:
 | 
			
		||||
            logger.error(f"Ошибка API: {response.get('retMsg')}")
 | 
			
		||||
            await message.answer(f"⚠️ Ошибка API: {response.get('retMsg')}")
 | 
			
		||||
            return 0
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error(f"Ошибка при получении общего баланса: {e}")
 | 
			
		||||
        await message.answer('Ошибка при подключении, повторите попытку', reply_markup=inline_markup.connect_bybit_api_message)
 | 
			
		||||
        return 0
 | 
			
		||||
@@ -1,115 +0,0 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import logging.config
 | 
			
		||||
 | 
			
		||||
from pybit.unified_trading import WebSocket
 | 
			
		||||
from websocket import WebSocketConnectionClosedException
 | 
			
		||||
from logger_helper.logger_helper import LOGGING_CONFIG
 | 
			
		||||
import app.telegram.database.requests as rq
 | 
			
		||||
 | 
			
		||||
logging.config.dictConfig(LOGGING_CONFIG)
 | 
			
		||||
logger = logging.getLogger("bybit_ws")
 | 
			
		||||
 | 
			
		||||
event_loop = None  # Сюда нужно будет установить event loop из основного приложения
 | 
			
		||||
active_ws_tasks = {}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def on_ws_error(ws, error):
 | 
			
		||||
    logger.error(f"WebSocket internal error: {error}")
 | 
			
		||||
    # Запланировать переподключение через event loop
 | 
			
		||||
    if event_loop:
 | 
			
		||||
        asyncio.run_coroutine_threadsafe(reconnect_ws(ws), event_loop)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def on_ws_close(ws, close_status_code, close_msg):
 | 
			
		||||
    logger.warning(f"WebSocket closed: {close_status_code} - {close_msg}")
 | 
			
		||||
    # Запланировать переподключение через event loop
 | 
			
		||||
    if event_loop:
 | 
			
		||||
        asyncio.run_coroutine_threadsafe(reconnect_ws(ws), event_loop)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def reconnect_ws(ws):
 | 
			
		||||
    logger.info("Запускаем переподключение WebSocket...")
 | 
			
		||||
    await asyncio.sleep(5)
 | 
			
		||||
    try:
 | 
			
		||||
        await ws.run_forever()
 | 
			
		||||
    except WebSocketConnectionClosedException:
 | 
			
		||||
        logger.info("WebSocket переподключение успешно завершено.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_or_create_event_loop() -> asyncio.AbstractEventLoop:
 | 
			
		||||
    """
 | 
			
		||||
    Возвращает текущий активный цикл событий asyncio или создает новый, если его нет.
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        return asyncio.get_running_loop()
 | 
			
		||||
    except RuntimeError:
 | 
			
		||||
        loop = asyncio.new_event_loop()
 | 
			
		||||
        asyncio.set_event_loop(loop)
 | 
			
		||||
        return loop
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def set_event_loop(loop: asyncio.AbstractEventLoop):
 | 
			
		||||
    global event_loop
 | 
			
		||||
    event_loop = loop
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def run_ws_for_user(tg_id, message) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Запускает WebSocket Bybit для пользователя с указанным tg_id.
 | 
			
		||||
    """
 | 
			
		||||
    if tg_id not in active_ws_tasks or active_ws_tasks[tg_id].done():
 | 
			
		||||
        api_key = await rq.get_bybit_api_key(tg_id)
 | 
			
		||||
        api_secret = await rq.get_bybit_secret_key(tg_id)
 | 
			
		||||
        # Запускаем WebSocket как асинхронную задачу
 | 
			
		||||
        active_ws_tasks[tg_id] = asyncio.create_task(
 | 
			
		||||
            start_execution_ws(api_key, api_secret, message)
 | 
			
		||||
        )
 | 
			
		||||
        logger.info(f"WebSocket для пользователя {tg_id} запущен.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def on_order_callback(message, msg):
 | 
			
		||||
    if event_loop is not None:
 | 
			
		||||
        from app.services.Bybit.functions.Futures import handle_order_message
 | 
			
		||||
        asyncio.run_coroutine_threadsafe(handle_order_message(message, msg), event_loop)
 | 
			
		||||
        logger.info("Callback выполнен.")
 | 
			
		||||
    else:
 | 
			
		||||
        logger.error("Event loop не установлен, callback пропущен.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def on_execution_callback(message, ws_msg):
 | 
			
		||||
    if event_loop is not None:
 | 
			
		||||
        from app.services.Bybit.functions.Futures import handle_execution_message
 | 
			
		||||
        asyncio.run_coroutine_threadsafe(handle_execution_message(message, ws_msg), event_loop)
 | 
			
		||||
        logger.info("Callback выполнен.")
 | 
			
		||||
    else:
 | 
			
		||||
        logger.error("Event loop не установлен, callback пропущен.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def start_execution_ws(api_key: str, api_secret: str, message):
 | 
			
		||||
    """
 | 
			
		||||
    Запускает и поддерживает WebSocket подключение для исполнения сделок.
 | 
			
		||||
    Реконнект при потерях соединения.
 | 
			
		||||
    """
 | 
			
		||||
    reconnect_delay = 5
 | 
			
		||||
    while True:
 | 
			
		||||
        try:
 | 
			
		||||
            if not api_key or not api_secret:
 | 
			
		||||
                logger.error("API_KEY и API_SECRET должны быть указаны для подключения к приватным каналам.")
 | 
			
		||||
                await asyncio.sleep(reconnect_delay)
 | 
			
		||||
                continue
 | 
			
		||||
            ws = WebSocket(api_key=api_key, api_secret=api_secret, testnet=False, channel_type="private")
 | 
			
		||||
 | 
			
		||||
            ws.on_error = on_ws_error
 | 
			
		||||
            ws.on_close = on_ws_close
 | 
			
		||||
 | 
			
		||||
            ws.subscribe("order", lambda ws_msg: on_order_callback(message, ws_msg))
 | 
			
		||||
            ws.subscribe("execution", lambda ws_msg: on_execution_callback(message, ws_msg))
 | 
			
		||||
 | 
			
		||||
            while True:
 | 
			
		||||
                await asyncio.sleep(1)  # Поддержание активности
 | 
			
		||||
        except WebSocketConnectionClosedException:
 | 
			
		||||
            logger.warning("WebSocket закрыт, переподключение через 5 секунд...")
 | 
			
		||||
            await asyncio.sleep(reconnect_delay)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(f"Ошибка WebSocket: {e}")
 | 
			
		||||
            await asyncio.sleep(reconnect_delay)
 | 
			
		||||
@@ -1,559 +0,0 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import logging.config
 | 
			
		||||
from aiogram import F, Router
 | 
			
		||||
 | 
			
		||||
from app.services.Bybit.functions.bybit_ws import run_ws_for_user
 | 
			
		||||
from app.telegram.functions.main_settings.settings import main_settings_message
 | 
			
		||||
from logger_helper.logger_helper import LOGGING_CONFIG
 | 
			
		||||
 | 
			
		||||
from app.services.Bybit.functions.Futures import (close_user_trade, set_take_profit_stop_loss, \
 | 
			
		||||
                                                  get_active_positions_by_symbol, get_active_orders_by_symbol,
 | 
			
		||||
                                                  get_active_positions, get_active_orders, cancel_all_tp_sl_orders,
 | 
			
		||||
                                                  open_position, close_trade_after_delay, safe_float,
 | 
			
		||||
                                                  )
 | 
			
		||||
from app.services.Bybit.functions.balance import get_balance
 | 
			
		||||
import app.telegram.Keyboards.inline_keyboards as inline_markup
 | 
			
		||||
 | 
			
		||||
import app.telegram.database.requests as rq
 | 
			
		||||
from aiogram.types import Message, CallbackQuery
 | 
			
		||||
from app.services.Bybit.functions.price_symbol import get_price
 | 
			
		||||
 | 
			
		||||
from app.states.States import (state_update_entry_type, state_update_symbol, state_limit_price,
 | 
			
		||||
                               SetTP_SL_State, CloseTradeTimerState)
 | 
			
		||||
from aiogram.fsm.context import FSMContext
 | 
			
		||||
 | 
			
		||||
from app.services.Bybit.functions.get_valid_symbol import get_valid_symbols
 | 
			
		||||
 | 
			
		||||
logging.config.dictConfig(LOGGING_CONFIG)
 | 
			
		||||
logger = logging.getLogger("functions")
 | 
			
		||||
 | 
			
		||||
router_functions_bybit_trade = Router()
 | 
			
		||||
 | 
			
		||||
user_trade_tasks = {}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.callback_query(F.data.in_(['clb_start_trading', 'clb_back_to_main', 'back_to_main']))
 | 
			
		||||
async def clb_start_bybit_trade_message(callback: CallbackQuery) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Обработка нажатия кнопок запуска торговли или возврата в главное меню.
 | 
			
		||||
    Отправляет информацию о балансе, символе, цене и инструкциях по торговле.
 | 
			
		||||
    """
 | 
			
		||||
    user_id = callback.from_user.id
 | 
			
		||||
    balance = await get_balance(user_id, callback.message)
 | 
			
		||||
 | 
			
		||||
    if balance:
 | 
			
		||||
        symbol = await rq.get_symbol(user_id)
 | 
			
		||||
        price = await get_price(user_id, symbol=symbol)
 | 
			
		||||
 | 
			
		||||
        text = (
 | 
			
		||||
            f"💎 Торговля на Bybit\n\n"
 | 
			
		||||
            f"⚖️ Ваш баланс (USDT): {float(balance):.2f}\n"
 | 
			
		||||
            f"📊 Текущая торговая пара: {symbol}\n"
 | 
			
		||||
            f"$$$ Цена: {price}\n\n"
 | 
			
		||||
            "Как начать торговлю?\n\n"
 | 
			
		||||
            "1️⃣ Проверьте и тщательно настройте все параметры в вашем профиле.\n"
 | 
			
		||||
            "2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).\n"
 | 
			
		||||
            "3️⃣ Нажмите кнопку 'Начать торговать'.\n"
 | 
			
		||||
        )
 | 
			
		||||
        await callback.message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def start_bybit_trade_message(message: Message) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Отправляет пользователю информацию о балансе, символе и текущей цене,
 | 
			
		||||
    вместе с инструкциями по началу торговли.
 | 
			
		||||
    """
 | 
			
		||||
    balance = await get_balance(message.from_user.id, message)
 | 
			
		||||
    tg_id = message.from_user.id
 | 
			
		||||
 | 
			
		||||
    if balance:
 | 
			
		||||
        await run_ws_for_user(tg_id, message)
 | 
			
		||||
        symbol = await rq.get_symbol(message.from_user.id)
 | 
			
		||||
        price = await get_price(message.from_user.id, symbol=symbol)
 | 
			
		||||
 | 
			
		||||
        text = (
 | 
			
		||||
            f"💎 Торговля на Bybit\n\n"
 | 
			
		||||
            f"⚖️ Ваш баланс (USDT): {balance}\n"
 | 
			
		||||
            f"📊 Текущая торговая пара: {symbol}\n"
 | 
			
		||||
            f"$$$ Цена: {price}\n\n"
 | 
			
		||||
            "Как начать торговлю?\n\n"
 | 
			
		||||
            "1️⃣ Проверьте и тщательно настройте все параметры в вашем профиле.\n"
 | 
			
		||||
            "2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).\n"
 | 
			
		||||
            "3️⃣ Нажмите кнопку 'Начать торговать'.\n"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.callback_query(F.data == 'clb_update_trading_pair')
 | 
			
		||||
async def update_symbol_for_trade_message(callback: CallbackQuery, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Начинает процедуру обновления торговой пары, переводит пользователя в состояние ожидания пары.
 | 
			
		||||
    """
 | 
			
		||||
    await state.set_state(state_update_symbol.symbol)
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
    await callback.message.answer(
 | 
			
		||||
        text='Укажите торговую пару заглавными буквами без пробелов и лишних символов (пример: BTCUSDT): ',
 | 
			
		||||
        reply_markup=inline_markup.cancel)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.message(state_update_symbol.symbol)
 | 
			
		||||
async def update_symbol_for_trade(message: Message, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Обрабатывает ввод торговой пары пользователем и проверяет её валидность.
 | 
			
		||||
    При успешном обновлении сохранит пару и отправит обновлённую информацию.
 | 
			
		||||
    """
 | 
			
		||||
    user_input = message.text.strip().upper()
 | 
			
		||||
    exists = await get_valid_symbols(message.from_user.id, user_input)
 | 
			
		||||
 | 
			
		||||
    if not exists:
 | 
			
		||||
        await message.answer("Введена некорректная торговая пара или такой пары нет в списке. Попробуйте снова.")
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    await state.update_data(symbol=message.text)
 | 
			
		||||
    await message.answer('Пара была успешно обновлена')
 | 
			
		||||
    await rq.update_symbol(message.from_user.id, user_input)
 | 
			
		||||
    await start_bybit_trade_message(message)
 | 
			
		||||
 | 
			
		||||
    await state.clear()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.callback_query(F.data == 'clb_update_entry_type')
 | 
			
		||||
async def update_entry_type_message(callback: CallbackQuery, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Запрашивает у пользователя тип входа в позицию (Market или Limit).
 | 
			
		||||
    """
 | 
			
		||||
    await state.set_state(state_update_entry_type.entry_type)
 | 
			
		||||
    await callback.message.answer("Выберите тип входа в позицию:", reply_markup=inline_markup.entry_order_type_markup)
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith('entry_order_type:'))
 | 
			
		||||
async def entry_order_type_callback(callback: CallbackQuery, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Обработка выбора типа входа в позицию.
 | 
			
		||||
    Если Limit, запрашивает цену лимитного ордера.
 | 
			
		||||
    Если Market — обновляет настройки.
 | 
			
		||||
    """
 | 
			
		||||
    order_type = callback.data.split(':')[1]
 | 
			
		||||
 | 
			
		||||
    if order_type not in ['Market', 'Limit']:
 | 
			
		||||
        await callback.answer("Ошибка выбора", show_alert=True)
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    if order_type == 'Limit':
 | 
			
		||||
        await state.set_state(state_limit_price.price)
 | 
			
		||||
        await callback.message.answer("Введите цену лимитного ордера:", reply_markup=inline_markup.cancel)
 | 
			
		||||
        await callback.answer()
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        await state.update_data(entry_order_type=order_type)
 | 
			
		||||
        await rq.update_entry_order_type(callback.from_user.id, order_type)
 | 
			
		||||
        await callback.message.answer(f"Выбран тип входа в позицию: {order_type}",
 | 
			
		||||
                                      reply_markup=inline_markup.start_trading_markup)
 | 
			
		||||
        await callback.answer()
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error(f"Произошла ошибка при обновлении типа входа в позицию: {e}")
 | 
			
		||||
        await callback.message.answer("Произошла ошибка при обновлении типа входа в позицию",
 | 
			
		||||
                                      reply_markup=inline_markup.back_to_main)
 | 
			
		||||
    await state.clear()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.message(state_limit_price.price)
 | 
			
		||||
async def set_limit_price(message: Message, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Обрабатывает ввод цены лимитного ордера, проверяет формат и сохраняет настройки.
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        price = float(message.text)
 | 
			
		||||
        if price <= 0:
 | 
			
		||||
            await message.answer("Цена должна быть положительным числом. Попробуйте снова.",
 | 
			
		||||
                                 reply_markup=inline_markup.cancel)
 | 
			
		||||
            return
 | 
			
		||||
    except ValueError:
 | 
			
		||||
        await message.answer("Некорректный формат цены. Введите число.", reply_markup=inline_markup.cancel)
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    await state.update_data(entry_order_type='Limit', limit_price=price)
 | 
			
		||||
 | 
			
		||||
    await rq.update_entry_order_type(message.from_user.id, 'Limit')
 | 
			
		||||
    await rq.update_limit_price(message.from_user.id, price)
 | 
			
		||||
 | 
			
		||||
    await message.answer(f"Цена лимитного ордера установлена: {price}", reply_markup=inline_markup.start_trading_markup)
 | 
			
		||||
    await state.clear()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.callback_query(F.data == "clb_start_chatbot_trading")
 | 
			
		||||
async def start_trading_process(callback: CallbackQuery) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Запускает торговый цикл в выбранном режиме Long/Short.
 | 
			
		||||
    Проверяет API-ключи, режим торговли, маржинальный режим и открытые позиции,
 | 
			
		||||
    затем запускает торговый цикл с задержкой или без неё.
 | 
			
		||||
    """
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
    tg_id = callback.from_user.id
 | 
			
		||||
    message = callback.message
 | 
			
		||||
    data_main_stgs = await rq.get_user_main_settings(tg_id)
 | 
			
		||||
    symbol = await rq.get_symbol(tg_id)
 | 
			
		||||
    margin_mode = data_main_stgs.get('margin_type', 'Isolated')
 | 
			
		||||
    trading_mode = data_main_stgs.get('trading_mode')
 | 
			
		||||
    starting_quantity = safe_float(data_main_stgs.get('starting_quantity'))
 | 
			
		||||
    switch_state = data_main_stgs.get("switch_state", "По направлению")
 | 
			
		||||
 | 
			
		||||
    if trading_mode == 'Switch':
 | 
			
		||||
        if switch_state == "По направлению":
 | 
			
		||||
            side = data_main_stgs.get("last_side")
 | 
			
		||||
        else:
 | 
			
		||||
            side = data_main_stgs.get("last_side")
 | 
			
		||||
            if side.lower() == "buy":
 | 
			
		||||
                side = "Sell"
 | 
			
		||||
            else:
 | 
			
		||||
                side = "Buy"
 | 
			
		||||
    else:
 | 
			
		||||
        if trading_mode == 'Long':
 | 
			
		||||
            side = 'Buy'
 | 
			
		||||
        elif trading_mode == 'Short':
 | 
			
		||||
            side = 'Sell'
 | 
			
		||||
        else:
 | 
			
		||||
            await message.answer(f"Режим торговли '{trading_mode}' пока не поддерживается.",
 | 
			
		||||
                                 reply_markup=inline_markup.back_to_main)
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
    await message.answer("Начинаю торговлю с использованием текущих настроек...")
 | 
			
		||||
 | 
			
		||||
    timer_data = await rq.get_user_timer(tg_id)
 | 
			
		||||
    if isinstance(timer_data, dict):
 | 
			
		||||
        timer_minute = timer_data.get('timer_minutes', 0)
 | 
			
		||||
    else:
 | 
			
		||||
        timer_minute = timer_data or 0
 | 
			
		||||
 | 
			
		||||
    if timer_minute > 0:
 | 
			
		||||
        await message.answer(f"Торговля начнётся через {timer_minute} мин.", reply_markup=inline_markup.cancel_start)
 | 
			
		||||
 | 
			
		||||
        async def delay_start():
 | 
			
		||||
            try:
 | 
			
		||||
                await asyncio.sleep(timer_minute * 60)
 | 
			
		||||
                await open_position(tg_id, message, side, margin_mode, symbol=symbol, quantity=starting_quantity)
 | 
			
		||||
                await rq.update_user_timer(tg_id, minutes=0)
 | 
			
		||||
            except asyncio.exceptions.CancelledError:
 | 
			
		||||
                logger.exception(f"Торговый цикл для пользователя {tg_id} был отменён.")
 | 
			
		||||
                raise
 | 
			
		||||
 | 
			
		||||
        task = asyncio.create_task(delay_start())
 | 
			
		||||
        user_trade_tasks[tg_id] = task
 | 
			
		||||
    else:
 | 
			
		||||
        await open_position(tg_id, message, side, margin_mode, symbol=symbol, quantity=starting_quantity)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.callback_query(F.data == "clb_cancel_start")
 | 
			
		||||
async def cancel_start_trading(callback: CallbackQuery):
 | 
			
		||||
    tg_id = callback.from_user.id
 | 
			
		||||
    task = user_trade_tasks.get(tg_id)
 | 
			
		||||
    if task and not task.done():
 | 
			
		||||
        task.cancel()
 | 
			
		||||
        try:
 | 
			
		||||
            await task
 | 
			
		||||
        except asyncio.CancelledError:
 | 
			
		||||
            pass
 | 
			
		||||
        user_trade_tasks.pop(tg_id, None)
 | 
			
		||||
        await rq.update_user_timer(tg_id, minutes=0)
 | 
			
		||||
        await callback.message.answer("Запуск торговли отменён.", reply_markup=inline_markup.back_to_main)
 | 
			
		||||
        await callback.message.edit_reply_markup(reply_markup=None)
 | 
			
		||||
    else:
 | 
			
		||||
        await callback.answer("Нет запланированной задачи запуска.", show_alert=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.callback_query(F.data == "clb_my_deals")
 | 
			
		||||
async def show_my_trades(callback: CallbackQuery) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Отображает пользователю выбор типа сделки по текущей торговой паре.
 | 
			
		||||
    """
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
    try:
 | 
			
		||||
        await callback.message.answer(f"Выберите тип сделки:",
 | 
			
		||||
                                      reply_markup=inline_markup.my_deals_select_markup)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error(f"Произошла ошибка при выборе типа сделки: {e}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.callback_query(F.data == "clb_open_deals")
 | 
			
		||||
async def show_my_trades_callback(callback: CallbackQuery):
 | 
			
		||||
    """
 | 
			
		||||
    Показывает открытые позиции пользователя.
 | 
			
		||||
    """
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        await get_active_positions(callback.from_user.id, message=callback.message)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error(f"Произошла ошибка при выборе сделки: {e}")
 | 
			
		||||
        await callback.message.answer("Произошла ошибка при выборе сделки", reply_markup=inline_markup.back_to_main)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("show_deal_"))
 | 
			
		||||
async def show_deal_callback(callback_query: CallbackQuery) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Показывает сделку пользователя по символу.
 | 
			
		||||
    """
 | 
			
		||||
    await callback_query.answer()
 | 
			
		||||
    try:
 | 
			
		||||
        symbol = callback_query.data[len("show_deal_"):]
 | 
			
		||||
        await rq.update_symbol(callback_query.from_user.id, symbol)
 | 
			
		||||
        tg_id = callback_query.from_user.id
 | 
			
		||||
        await get_active_positions_by_symbol(tg_id, symbol, message=callback_query.message)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error(f"Произошла ошибка при выборе сделки: {e}")
 | 
			
		||||
        await callback_query.message.answer("Произошла ошибка при выборе сделки",
 | 
			
		||||
                                            reply_markup=inline_markup.back_to_main)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.callback_query(F.data == "clb_open_orders")
 | 
			
		||||
async def show_my_orders_callback(callback: CallbackQuery) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Показывает открытые позиции пользователя по символу.
 | 
			
		||||
    """
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        await get_active_orders(callback.from_user.id, message=callback.message)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error(f"Произошла ошибка при выборе ордера: {e}")
 | 
			
		||||
        await callback.message.answer("Произошла ошибка при выборе ордера", reply_markup=inline_markup.back_to_main)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("show_limit_"))
 | 
			
		||||
async def show_limit_callback(callback_query: CallbackQuery) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Показывает сделку пользователя по символу.
 | 
			
		||||
    """
 | 
			
		||||
    await callback_query.answer()
 | 
			
		||||
    try:
 | 
			
		||||
        symbol = callback_query.data[len("show_limit_"):]
 | 
			
		||||
        await rq.update_symbol(callback_query.from_user.id, symbol)
 | 
			
		||||
        tg_id = callback_query.from_user.id
 | 
			
		||||
        await get_active_orders_by_symbol(tg_id, symbol, message=callback_query.message)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error(f"Произошла ошибка при выборе сделки: {e}")
 | 
			
		||||
        await callback_query.message.answer("Произошла ошибка при выборе сделки",
 | 
			
		||||
                                            reply_markup=inline_markup.back_to_main)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.callback_query(F.data == "clb_set_tp_sl")
 | 
			
		||||
async def set_tp_sl(callback: CallbackQuery, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Запускает процесс установки Take Profit и Stop Loss.
 | 
			
		||||
    """
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
    await state.set_state(SetTP_SL_State.waiting_for_take_profit)
 | 
			
		||||
    await callback.message.answer("Введите значение Take Profit (в цене, например 26000.5):",
 | 
			
		||||
                                  reply_markup=inline_markup.cancel)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.message(SetTP_SL_State.waiting_for_take_profit)
 | 
			
		||||
async def process_take_profit(message: Message, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Обрабатывает ввод значения Take Profit и запрашивает Stop Loss.
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        tp = float(message.text.strip())
 | 
			
		||||
        if tp <= 0:
 | 
			
		||||
            await message.answer("Значение Take Profit должно быть положительным числом. Попробуйте снова.",
 | 
			
		||||
                                 reply_markup=inline_markup.cancel)
 | 
			
		||||
            return
 | 
			
		||||
    except ValueError:
 | 
			
		||||
        await message.answer("Некорректный ввод. Пожалуйста, введите число для Take Profit.",
 | 
			
		||||
                             reply_markup=inline_markup.cancel)
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    await state.update_data(take_profit=tp)
 | 
			
		||||
    await state.set_state(SetTP_SL_State.waiting_for_stop_loss)
 | 
			
		||||
    await message.answer("Введите значение Stop Loss (в цене, например 24500.3):", reply_markup=inline_markup.cancel)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.message(SetTP_SL_State.waiting_for_stop_loss)
 | 
			
		||||
async def process_stop_loss(message: Message, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Обрабатывает ввод значения Stop Loss и завершает процесс установки TP/SL.
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        sl = float(message.text.strip())
 | 
			
		||||
        if sl <= 0:
 | 
			
		||||
            await message.answer("Значение Stop Loss должно быть положительным числом. Попробуйте снова.",
 | 
			
		||||
                                 reply_markup=inline_markup.cancel)
 | 
			
		||||
            return
 | 
			
		||||
    except ValueError:
 | 
			
		||||
        await message.answer("Некорректный ввод. Пожалуйста, введите число для Stop Loss.",
 | 
			
		||||
                             reply_markup=inline_markup.cancel)
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    data = await state.get_data()
 | 
			
		||||
    tp = data.get("take_profit")
 | 
			
		||||
 | 
			
		||||
    if tp is None:
 | 
			
		||||
        await message.answer("Ошибка, не найдено значение Take Profit. Попробуйте снова.")
 | 
			
		||||
        await state.clear()
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    tg_id = message.from_user.id
 | 
			
		||||
 | 
			
		||||
    await set_take_profit_stop_loss(tg_id, message, take_profit_price=tp, stop_loss_price=sl)
 | 
			
		||||
 | 
			
		||||
    await state.clear()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_deal:"))
 | 
			
		||||
async def close_trade_callback(callback: CallbackQuery) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Закрывает сделку пользователя по символу.
 | 
			
		||||
    """
 | 
			
		||||
    symbol = callback.data.split(':')[1]
 | 
			
		||||
    tg_id = callback.from_user.id
 | 
			
		||||
 | 
			
		||||
    result = await close_user_trade(tg_id, symbol)
 | 
			
		||||
 | 
			
		||||
    if result:
 | 
			
		||||
        logger.info(f"Сделка {symbol} успешно закрыта.")
 | 
			
		||||
    else:
 | 
			
		||||
        logger.error(f"Не удалось закрыть сделку {symbol}.")
 | 
			
		||||
        await callback.message.answer(f"Не удалось закрыть сделку {symbol}.")
 | 
			
		||||
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_limit:"))
 | 
			
		||||
async def close_trade_callback(callback: CallbackQuery) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Закрывает ордера пользователя по символу.
 | 
			
		||||
    """
 | 
			
		||||
    symbol = callback.data.split(':')[1]
 | 
			
		||||
    tg_id = callback.from_user.id
 | 
			
		||||
 | 
			
		||||
    result = await cancel_all_tp_sl_orders(tg_id, symbol)
 | 
			
		||||
 | 
			
		||||
    if result:
 | 
			
		||||
        logger.info(f"Ордер {result} успешно закрыт.")
 | 
			
		||||
    else:
 | 
			
		||||
        await callback.message.answer(f"Не удалось закрыть ордер {result}.")
 | 
			
		||||
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_deal_by_timer:"))
 | 
			
		||||
async def ask_close_delay(callback: CallbackQuery, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Запускает диалог с пользователем для задания задержки перед закрытием сделки.
 | 
			
		||||
    """
 | 
			
		||||
    symbol = callback.data.split(":")[1]
 | 
			
		||||
    await state.update_data(symbol=symbol)
 | 
			
		||||
    await state.set_state(CloseTradeTimerState.waiting_for_delay)
 | 
			
		||||
    await callback.message.answer("Введите задержку в минутах до закрытия сделки:",
 | 
			
		||||
                                  reply_markup=inline_markup.cancel)
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.message(CloseTradeTimerState.waiting_for_delay)
 | 
			
		||||
async def process_close_delay(message: Message, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Обрабатывает ввод закрытия сделки с задержкой.
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        delay_minutes = int(message.text.strip())
 | 
			
		||||
        if delay_minutes <= 0:
 | 
			
		||||
            await message.answer("Введите положительное число.")
 | 
			
		||||
            return
 | 
			
		||||
    except ValueError:
 | 
			
		||||
        await message.answer("Некорректный ввод. Введите число в минутах.")
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    data = await state.get_data()
 | 
			
		||||
    symbol = data.get("symbol")
 | 
			
		||||
 | 
			
		||||
    delay = delay_minutes * 60
 | 
			
		||||
    await message.answer(f"Закрытие сделки {symbol} запланировано через {delay_minutes} мин.",
 | 
			
		||||
                         reply_markup=inline_markup.back_to_main)
 | 
			
		||||
    await close_trade_after_delay(message.from_user.id, message, symbol, delay)
 | 
			
		||||
    await state.clear()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.callback_query(F.data == "clb_change_martingale_reset")
 | 
			
		||||
async def reset_martingale(callback: CallbackQuery) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Сбрасывает шаги мартингейла пользователя.
 | 
			
		||||
    """
 | 
			
		||||
    tg_id = callback.from_user.id
 | 
			
		||||
    await rq.update_martingale_step(tg_id, 1)
 | 
			
		||||
    await callback.answer("Сброс шагов выполнен.")
 | 
			
		||||
    await main_settings_message(tg_id, callback.message)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.callback_query(F.data == "clb_stop_trading")
 | 
			
		||||
async def confirm_stop_trading(callback: CallbackQuery):
 | 
			
		||||
    """
 | 
			
		||||
    Предлагает пользователю выбрать вариант подтверждение остановки торговли.
 | 
			
		||||
    """
 | 
			
		||||
    await callback.message.answer(
 | 
			
		||||
        "Выберите вариант остановки торговли:", reply_markup=inline_markup.stop_choice_markup
 | 
			
		||||
    )
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.callback_query(F.data == "stop_immediately")
 | 
			
		||||
async def stop_immediately(callback: CallbackQuery):
 | 
			
		||||
    """
 | 
			
		||||
    Останавливает торговлю немедленно.
 | 
			
		||||
    """
 | 
			
		||||
    tg_id = callback.from_user.id
 | 
			
		||||
 | 
			
		||||
    await rq.update_trigger(tg_id, "Ручной")
 | 
			
		||||
    await callback.message.answer("Автоматическая торговля остановлена.", reply_markup=inline_markup.back_to_main)
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.callback_query(F.data == "stop_with_timer")
 | 
			
		||||
async def stop_with_timer_start(callback: CallbackQuery, state: FSMContext):
 | 
			
		||||
    """
 | 
			
		||||
    Запускает диалог с пользователем для задания задержки до остановки торговли.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    await state.set_state(CloseTradeTimerState.waiting_for_trade)
 | 
			
		||||
    await callback.message.answer("Введите задержку в минутах до остановки торговли:",
 | 
			
		||||
                                  reply_markup=inline_markup.cancel)
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.message(CloseTradeTimerState.waiting_for_trade)
 | 
			
		||||
async def process_stop_delay(message: Message, state: FSMContext):
 | 
			
		||||
    """
 | 
			
		||||
    Обрабатывает ввод задержки и запускает задачу остановки торговли с задержкой.
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        delay_minutes = int(message.text.strip())
 | 
			
		||||
        if delay_minutes <= 0:
 | 
			
		||||
            await message.answer("Введите положительное число минут.")
 | 
			
		||||
            return
 | 
			
		||||
    except ValueError:
 | 
			
		||||
        await message.answer("Некорректный формат. Введите число в минутах.")
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    tg_id = message.from_user.id
 | 
			
		||||
    delay_seconds = delay_minutes * 60
 | 
			
		||||
 | 
			
		||||
    await message.answer(f"Торговля будет остановлена через {delay_minutes} минут.",
 | 
			
		||||
                         reply_markup=inline_markup.back_to_main)
 | 
			
		||||
    await asyncio.sleep(delay_seconds)
 | 
			
		||||
    await rq.update_trigger(tg_id, "Ручной")
 | 
			
		||||
    await message.answer("Автоматическая торговля остановлена.", reply_markup=inline_markup.back_to_main)
 | 
			
		||||
 | 
			
		||||
    await state.clear()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_functions_bybit_trade.callback_query(F.data == "clb_cancel")
 | 
			
		||||
async def cancel(callback: CallbackQuery, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Отменяет текущее состояние FSM и сообщает пользователю об отмене.
 | 
			
		||||
    """
 | 
			
		||||
    await state.clear()
 | 
			
		||||
    await callback.message.answer("Отменено!", reply_markup=inline_markup.back_to_main)
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
@@ -1,40 +0,0 @@
 | 
			
		||||
import logging.config
 | 
			
		||||
from pybit.unified_trading import HTTP
 | 
			
		||||
import app.telegram.database.requests as rq
 | 
			
		||||
from logger_helper.logger_helper import LOGGING_CONFIG
 | 
			
		||||
 | 
			
		||||
logging.config.dictConfig(LOGGING_CONFIG)
 | 
			
		||||
logger = logging.getLogger("get_valid_symbol")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_valid_symbols(user_id: int, symbol: str) -> bool:
 | 
			
		||||
    """
 | 
			
		||||
    Проверяет существование торговой пары на Bybit в категории 'linear'.
 | 
			
		||||
 | 
			
		||||
    Эта функция получает API-ключи пользователя из базы данных и
 | 
			
		||||
    с помощью Bybit API проверяет наличие данного символа в списке
 | 
			
		||||
    торговых инструментов категории 'linear'.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        user_id (int): Идентификатор пользователя Telegram.
 | 
			
		||||
        symbol (str): Торговый символ (валютная пара), например "BTCUSDT".
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        bool: Возвращает True, если торговая пара существует, иначе False.
 | 
			
		||||
 | 
			
		||||
    Raises:
 | 
			
		||||
        Исключения подавляются и вызывается False, если произошла ошибка запроса к API.
 | 
			
		||||
    """
 | 
			
		||||
    api_key = await rq.get_bybit_api_key(user_id)
 | 
			
		||||
    secret_key = await rq.get_bybit_secret_key(user_id)
 | 
			
		||||
    client = HTTP(api_key=api_key, api_secret=secret_key)
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        resp = client.get_instruments_info(category='linear', symbol=symbol)
 | 
			
		||||
        # Проверка наличия результата и непустого списка инструментов
 | 
			
		||||
        if resp.get('retCode') == 0 and resp.get('result') and resp['result'].get('list'):
 | 
			
		||||
            return len(resp['result']['list']) > 0
 | 
			
		||||
        return False
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logging.error(f"Ошибка при получении списка инструментов: {e}")
 | 
			
		||||
        return False
 | 
			
		||||
@@ -1,52 +0,0 @@
 | 
			
		||||
import math
 | 
			
		||||
import logging.config
 | 
			
		||||
from app.services.Bybit.functions.price_symbol import get_price
 | 
			
		||||
import app.telegram.database.requests as rq
 | 
			
		||||
from logger_helper.logger_helper import LOGGING_CONFIG
 | 
			
		||||
from pybit.unified_trading import HTTP
 | 
			
		||||
 | 
			
		||||
logging.config.dictConfig(LOGGING_CONFIG)
 | 
			
		||||
logger = logging.getLogger("min_qty")
 | 
			
		||||
 | 
			
		||||
def round_up_qty(value: float, step: float) -> float:
 | 
			
		||||
    """
 | 
			
		||||
    Округление value вверх до ближайшего кратного step.
 | 
			
		||||
    """
 | 
			
		||||
    return math.ceil(value / step) * step
 | 
			
		||||
 | 
			
		||||
async def get_min_qty(tg_id: int) -> float:
 | 
			
		||||
    """
 | 
			
		||||
    Получает минимальный объем (количество) ордера для символа пользователя на Bybit,
 | 
			
		||||
    округленное с учетом шага количества qtyStep.
 | 
			
		||||
 | 
			
		||||
    :param tg_id: int - идентификатор пользователя Telegram
 | 
			
		||||
    :return: float - минимальное количество лота для ордера
 | 
			
		||||
    """
 | 
			
		||||
    api_key = await rq.get_bybit_api_key(tg_id)
 | 
			
		||||
    secret_key = await rq.get_bybit_secret_key(tg_id)
 | 
			
		||||
    symbol = await rq.get_symbol(tg_id)
 | 
			
		||||
 | 
			
		||||
    client = HTTP(api_key=api_key, api_secret=secret_key)
 | 
			
		||||
 | 
			
		||||
    price = await get_price(tg_id, symbol=symbol)
 | 
			
		||||
 | 
			
		||||
    response = client.get_instruments_info(symbol=symbol, category='linear')
 | 
			
		||||
 | 
			
		||||
    instrument = response['result'][0]
 | 
			
		||||
    lot_size_filter = instrument.get('lotSizeFilter', {})
 | 
			
		||||
 | 
			
		||||
    min_order_qty = float(lot_size_filter.get('minOrderQty', 0))
 | 
			
		||||
    min_notional_value = float(lot_size_filter.get('minNotionalValue', 0))
 | 
			
		||||
    qty_step = float(lot_size_filter.get('qtyStep', 1))
 | 
			
		||||
 | 
			
		||||
    calculated_qty = (5 / price) * 1.1
 | 
			
		||||
 | 
			
		||||
    min_qty = max(min_order_qty, calculated_qty)
 | 
			
		||||
 | 
			
		||||
    min_qty_rounded = round_up_qty(min_qty, qty_step)
 | 
			
		||||
 | 
			
		||||
    logger.debug(f"tg_id={tg_id}: price={price}, min_order_qty={min_order_qty}, "
 | 
			
		||||
                 f"min_notional_value={min_notional_value}, qty_step={qty_step}, "
 | 
			
		||||
                 f"calculated_qty={calculated_qty}, min_qty_rounded={min_qty_rounded}")
 | 
			
		||||
 | 
			
		||||
    return min_qty_rounded
 | 
			
		||||
@@ -1,32 +0,0 @@
 | 
			
		||||
import app.telegram.database.requests as rq
 | 
			
		||||
import logging.config
 | 
			
		||||
from logger_helper.logger_helper import LOGGING_CONFIG
 | 
			
		||||
from pybit import exceptions
 | 
			
		||||
from pybit.unified_trading import HTTP
 | 
			
		||||
 | 
			
		||||
logging.config.dictConfig(LOGGING_CONFIG)
 | 
			
		||||
logger = logging.getLogger("price_symbol")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_price(tg_id: int, symbol: str) -> float:
 | 
			
		||||
    """
 | 
			
		||||
    Асинхронно получает текущую цену символа пользователя на Bybit.
 | 
			
		||||
 | 
			
		||||
    :param tg_id: int - ID пользователя Telegram
 | 
			
		||||
    :return: float - текущая цена символа
 | 
			
		||||
    """
 | 
			
		||||
    api_key = await rq.get_bybit_api_key(tg_id)
 | 
			
		||||
    secret_key = await rq.get_bybit_secret_key(tg_id)
 | 
			
		||||
 | 
			
		||||
    client = HTTP(
 | 
			
		||||
        api_key=api_key,
 | 
			
		||||
        api_secret=secret_key
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        price = float(
 | 
			
		||||
            client.get_tickers(category='linear', symbol=symbol).get('result').get('list')[0].get('ask1Price'))
 | 
			
		||||
        return price
 | 
			
		||||
    except exceptions.InvalidRequestError as e:
 | 
			
		||||
        logger.error(f"Ошибка при получении цены: {e}")
 | 
			
		||||
        return 1.0
 | 
			
		||||
@@ -1,69 +0,0 @@
 | 
			
		||||
from aiogram.fsm.state import State, StatesGroup
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class state_update_symbol(StatesGroup):
 | 
			
		||||
    """FSM состояние для обновления торгового символа."""
 | 
			
		||||
    symbol = State()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class state_update_entry_type(StatesGroup):
 | 
			
		||||
    """FSM состояние для обновления типа входа."""
 | 
			
		||||
    entry_type = State()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TradeSetup(StatesGroup):
 | 
			
		||||
    """FSM состояния для настройки торговли с таймером и процентом."""
 | 
			
		||||
    waiting_for_timer = State()
 | 
			
		||||
    waiting_for_positive_percent = State()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class state_limit_price(StatesGroup):
 | 
			
		||||
    """FSM состояние для установки лимита."""
 | 
			
		||||
    price = State()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CloseTradeTimerState(StatesGroup):
 | 
			
		||||
    """FSM состояние ожидания задержки перед закрытием сделки."""
 | 
			
		||||
    waiting_for_delay = State()
 | 
			
		||||
    waiting_for_trade = State()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SetTP_SL_State(StatesGroup):
 | 
			
		||||
    """FSM состояние для установки TP и SL."""
 | 
			
		||||
    waiting_for_take_profit = State()
 | 
			
		||||
    waiting_for_stop_loss = State()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class update_risk_management_settings(StatesGroup):
 | 
			
		||||
    """FSM состояние для обновления настроек управления рисками."""
 | 
			
		||||
    price_profit = State()
 | 
			
		||||
    price_loss = State()
 | 
			
		||||
    max_risk_deal = State()
 | 
			
		||||
    commission_fee = State()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class state_reg_bybit_api(StatesGroup):
 | 
			
		||||
    """FSM состояние для регистрации API Bybit."""
 | 
			
		||||
    api_key = State()
 | 
			
		||||
    secret_key = State()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class condition_settings(StatesGroup):
 | 
			
		||||
    """FSM состояние для настройки условий трейдинга."""
 | 
			
		||||
    trigger = State()
 | 
			
		||||
    timer = State()
 | 
			
		||||
    volatilty = State()
 | 
			
		||||
    volume = State()
 | 
			
		||||
    integration = State()
 | 
			
		||||
    use_tv_signal = State()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class update_main_settings(StatesGroup):
 | 
			
		||||
    """FSM состояние для обновления основных настройок."""
 | 
			
		||||
    trading_mode = State()
 | 
			
		||||
    size_leverage = State()
 | 
			
		||||
    margin_type = State()
 | 
			
		||||
    martingale_factor = State()
 | 
			
		||||
    starting_quantity = State()
 | 
			
		||||
    maximal_quantity = State()
 | 
			
		||||
    switch_mode_enabled = State()
 | 
			
		||||
@@ -1,217 +0,0 @@
 | 
			
		||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
 | 
			
		||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
 | 
			
		||||
 | 
			
		||||
start_markup = InlineKeyboardMarkup(inline_keyboard=[
 | 
			
		||||
    [InlineKeyboardButton(text="🔥 Начать торговлю", callback_data="clb_start_chatbot_message")]
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
settings_markup = InlineKeyboardMarkup(inline_keyboard=[
 | 
			
		||||
    [InlineKeyboardButton(text="Запуск", callback_data='clb_start_trading')]
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
cancel_start = InlineKeyboardMarkup(inline_keyboard=[
 | 
			
		||||
    [InlineKeyboardButton(text="Отменить запуск", callback_data="clb_cancel_start")]
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
back_btn_list_settings = [InlineKeyboardButton(text="Назад",
 | 
			
		||||
                                               callback_data='clb_back_to_special_settings_message')]  # Кнопка для возврата к списку каталога настроек
 | 
			
		||||
back_btn_list_settings_markup = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Назад",
 | 
			
		||||
                                                                                            callback_data='clb_back_to_special_settings_message')]])  # Клавиатура для возврата к списку каталога настроек
 | 
			
		||||
back_btn_to_main = [InlineKeyboardButton(text="На главную", callback_data='clb_back_to_main')]
 | 
			
		||||
 | 
			
		||||
back_btn_profile = [InlineKeyboardButton(text="Назад", callback_data='clb_start_chatbot_message')]
 | 
			
		||||
 | 
			
		||||
connect_bybit_api_message = InlineKeyboardMarkup(inline_keyboard=[
 | 
			
		||||
    [InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api_message')]
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
special_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
 | 
			
		||||
    [InlineKeyboardButton(text="Основные настройки", callback_data='clb_change_main_settings'),
 | 
			
		||||
     InlineKeyboardButton(text="Риск-менеджмент", callback_data='clb_change_risk_management_settings')],
 | 
			
		||||
 | 
			
		||||
    [InlineKeyboardButton(text="Условия запуска", callback_data='clb_change_condition_settings')],
 | 
			
		||||
     # InlineKeyboardButton(text="Дополнительные параметры", callback_data='clb_change_additional_settings')],
 | 
			
		||||
    [InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api_message')],
 | 
			
		||||
    back_btn_to_main
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
connect_bybit_api_markup = InlineKeyboardMarkup(inline_keyboard=[
 | 
			
		||||
    [InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api')]
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
trading_markup = InlineKeyboardMarkup(inline_keyboard=[
 | 
			
		||||
    [InlineKeyboardButton(text="Настройки", callback_data='clb_settings_message')],
 | 
			
		||||
    [InlineKeyboardButton(text="Мои сделки", callback_data='clb_my_deals')],
 | 
			
		||||
    [InlineKeyboardButton(text="Указать торговую пару", callback_data='clb_update_trading_pair')],
 | 
			
		||||
    [InlineKeyboardButton(text="Начать торговать", callback_data='clb_update_entry_type')],
 | 
			
		||||
    [InlineKeyboardButton(text="Остановить торговлю", callback_data='clb_stop_trading')],
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
start_trading_markup = InlineKeyboardMarkup(inline_keyboard=[
 | 
			
		||||
    [InlineKeyboardButton(text="Начать торговлю", callback_data="clb_start_chatbot_trading")],
 | 
			
		||||
    [InlineKeyboardButton(text="На главную", callback_data='back_to_main')],
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
cancel = InlineKeyboardMarkup(inline_keyboard=[
 | 
			
		||||
    [InlineKeyboardButton(text="Отменить", callback_data="clb_cancel")]
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
entry_order_type_markup = InlineKeyboardMarkup(
 | 
			
		||||
    inline_keyboard=[
 | 
			
		||||
        [
 | 
			
		||||
            InlineKeyboardButton(text="Market (текущая цена)", callback_data="entry_order_type:Market"),
 | 
			
		||||
            InlineKeyboardButton(text="Limit (фиксированная цена)", callback_data="entry_order_type:Limit"),
 | 
			
		||||
        ], back_btn_to_main
 | 
			
		||||
    ]
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
back_to_main = InlineKeyboardMarkup(inline_keyboard=[
 | 
			
		||||
    [InlineKeyboardButton(text="На главную", callback_data='back_to_main')],
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
main_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
 | 
			
		||||
    [InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_trading_mode'),
 | 
			
		||||
     InlineKeyboardButton(text='Состояние свитча', callback_data='clb_change_switch_state'),
 | 
			
		||||
     InlineKeyboardButton(text='Тип маржи', callback_data='clb_change_margin_type')],
 | 
			
		||||
 | 
			
		||||
    [InlineKeyboardButton(text='Размер кредитного плеча', callback_data='clb_change_size_leverage'),
 | 
			
		||||
     InlineKeyboardButton(text='Начальная ставка', callback_data='clb_change_starting_quantity')],
 | 
			
		||||
 | 
			
		||||
    [InlineKeyboardButton(text='Коэффициент Мартингейла', callback_data='clb_change_martingale_factor'),
 | 
			
		||||
     InlineKeyboardButton(text='Сбросить шаги Мартингейла', callback_data='clb_change_martingale_reset')],
 | 
			
		||||
     [InlineKeyboardButton(text='Максимальное кол-во ставок', callback_data='clb_change_maximum_quantity')],
 | 
			
		||||
 | 
			
		||||
    back_btn_list_settings,
 | 
			
		||||
    back_btn_to_main
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
risk_management_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
 | 
			
		||||
    [InlineKeyboardButton(text='Изм. цены прибыли', callback_data='clb_change_price_profit'),
 | 
			
		||||
     InlineKeyboardButton(text='Изм. цены убытков', callback_data='clb_change_price_loss')],
 | 
			
		||||
 | 
			
		||||
    [InlineKeyboardButton(text='Макс. риск на сделку', callback_data='clb_change_max_risk_deal')],
 | 
			
		||||
    [InlineKeyboardButton(text='Учитывать комиссию биржи (Да/Нет)', callback_data='commission_fee')],
 | 
			
		||||
 | 
			
		||||
    back_btn_list_settings,
 | 
			
		||||
    back_btn_to_main
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
condition_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
 | 
			
		||||
    [InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_mode'),
 | 
			
		||||
     InlineKeyboardButton(text='Таймер', callback_data='clb_change_timer')],
 | 
			
		||||
    #
 | 
			
		||||
    # [InlineKeyboardButton(text='Фильтр волатильности', callback_data='clb_change_filter_volatility'),
 | 
			
		||||
    #  InlineKeyboardButton(text='Внешние сигналы', callback_data='clb_change_external_cues')],
 | 
			
		||||
    #
 | 
			
		||||
    # [InlineKeyboardButton(text='Сигналы TradingView', callback_data='clb_change_tradingview_cues'),
 | 
			
		||||
    #  InlineKeyboardButton(text='Webhook URL', callback_data='clb_change_webhook')],
 | 
			
		||||
    #
 | 
			
		||||
    # [InlineKeyboardButton(text='AI - аналитика', callback_data='clb_change_ai_analytics')],
 | 
			
		||||
 | 
			
		||||
    back_btn_list_settings,
 | 
			
		||||
    back_btn_to_main
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
additional_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
 | 
			
		||||
    [InlineKeyboardButton(text='Сохранить шаблон', callback_data='clb_change_save_pattern'),
 | 
			
		||||
     InlineKeyboardButton(text='Автозапуск', callback_data='clb_change_auto_start')],
 | 
			
		||||
 | 
			
		||||
    [InlineKeyboardButton(text='Уведомления', callback_data='clb_change_notifications')],
 | 
			
		||||
 | 
			
		||||
    back_btn_list_settings,
 | 
			
		||||
    back_btn_to_main
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
trading_mode_markup = InlineKeyboardMarkup(inline_keyboard=[
 | 
			
		||||
    [InlineKeyboardButton(text="Лонг", callback_data="trade_mode_long"),
 | 
			
		||||
     InlineKeyboardButton(text="Шорт", callback_data="trade_mode_short"),
 | 
			
		||||
     InlineKeyboardButton(text="Свитч", callback_data="trade_mode_switch")],
 | 
			
		||||
     # InlineKeyboardButton(text="Смарт", callback_data="trade_mode_smart")],
 | 
			
		||||
 | 
			
		||||
    back_btn_list_settings,
 | 
			
		||||
    back_btn_to_main
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
margin_type_markup = InlineKeyboardMarkup(inline_keyboard=[
 | 
			
		||||
    [InlineKeyboardButton(text="Изолированный", callback_data="margin_type_isolated"),
 | 
			
		||||
     InlineKeyboardButton(text="Кросс", callback_data="margin_type_cross")],
 | 
			
		||||
 | 
			
		||||
    back_btn_list_settings
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
trigger_markup = InlineKeyboardMarkup(inline_keyboard=[  # ИЗМЕНИТЬ НА INLINE
 | 
			
		||||
    [InlineKeyboardButton(text='Ручной', callback_data="clb_trigger_manual")],
 | 
			
		||||
     # [InlineKeyboardButton(text='TradingView', callback_data="clb_trigger_tradingview")],
 | 
			
		||||
    [InlineKeyboardButton(text="Автоматический", callback_data="clb_trigger_auto")],
 | 
			
		||||
    back_btn_list_settings,
 | 
			
		||||
    back_btn_to_main
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
buttons_yes_no_markup = InlineKeyboardMarkup(inline_keyboard=[
 | 
			
		||||
    [InlineKeyboardButton(text='Да', callback_data="clb_yes"),
 | 
			
		||||
     InlineKeyboardButton(text='Нет', callback_data="clb_no")],
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
buttons_on_off_markup = InlineKeyboardMarkup(inline_keyboard=[  # ИЗМЕНИТЬ НА INLINE
 | 
			
		||||
    [InlineKeyboardButton(text='Включить', callback_data="clb_on"),
 | 
			
		||||
     InlineKeyboardButton(text='Выключить', callback_data="clb_off")]
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
my_deals_select_markup = InlineKeyboardMarkup(inline_keyboard=[
 | 
			
		||||
    [InlineKeyboardButton(text='Открытые сделки', callback_data="clb_open_deals"),
 | 
			
		||||
     InlineKeyboardButton(text='Лимитные ордера', callback_data="clb_open_orders")],
 | 
			
		||||
    back_btn_to_main
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
def create_trades_inline_keyboard(trades):
 | 
			
		||||
    builder = InlineKeyboardBuilder()
 | 
			
		||||
    for trade in trades:
 | 
			
		||||
        builder.button(text=trade, callback_data=f"show_deal_{trade}")
 | 
			
		||||
    builder.adjust(2)
 | 
			
		||||
    return builder.as_markup()
 | 
			
		||||
 | 
			
		||||
def create_trades_inline_keyboard_limits(trades):
 | 
			
		||||
    builder = InlineKeyboardBuilder()
 | 
			
		||||
    for trade in trades:
 | 
			
		||||
        builder.button(text=trade, callback_data=f"show_limit_{trade}")
 | 
			
		||||
    builder.adjust(2)
 | 
			
		||||
    return builder.as_markup()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_close_deal_markup(symbol: str) -> InlineKeyboardMarkup:
 | 
			
		||||
    return InlineKeyboardMarkup(inline_keyboard=[
 | 
			
		||||
        [InlineKeyboardButton(text="Закрыть сделку", callback_data=f"close_deal:{symbol}")],
 | 
			
		||||
        [InlineKeyboardButton(text="Закрыть по таймеру", callback_data=f"close_deal_by_timer:{symbol}")],
 | 
			
		||||
        [InlineKeyboardButton(text="Установить TP/SL", callback_data="clb_set_tp_sl")],
 | 
			
		||||
        back_btn_to_main
 | 
			
		||||
    ])
 | 
			
		||||
 | 
			
		||||
def create_close_limit_markup(symbol: str) -> InlineKeyboardMarkup:
 | 
			
		||||
    return InlineKeyboardMarkup(inline_keyboard=[
 | 
			
		||||
        [InlineKeyboardButton(text="Закрыть лимитный ордер", callback_data=f"close_limit:{symbol}")],
 | 
			
		||||
        back_btn_to_main
 | 
			
		||||
    ])
 | 
			
		||||
 | 
			
		||||
timer_markup = InlineKeyboardMarkup(inline_keyboard=[
 | 
			
		||||
    [InlineKeyboardButton(text="Установить таймер", callback_data="clb_set_timer")],
 | 
			
		||||
    [InlineKeyboardButton(text="Удалить таймер", callback_data="clb_delete_timer")],
 | 
			
		||||
    back_btn_to_main
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
stop_choice_markup = InlineKeyboardMarkup(
 | 
			
		||||
    inline_keyboard=[
 | 
			
		||||
        [
 | 
			
		||||
            InlineKeyboardButton(text="Остановить сразу", callback_data="stop_immediately"),
 | 
			
		||||
            InlineKeyboardButton(text="Остановить по таймеру", callback_data="stop_with_timer"),
 | 
			
		||||
        ]
 | 
			
		||||
    ]
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
switch_state_markup = InlineKeyboardMarkup(inline_keyboard=[
 | 
			
		||||
    [InlineKeyboardButton(text='По направлению', callback_data="clb_long_switch"),
 | 
			
		||||
     InlineKeyboardButton(text='Против направления', callback_data="clb_short_switch")],
 | 
			
		||||
])
 | 
			
		||||
@@ -1,6 +0,0 @@
 | 
			
		||||
from aiogram.types import ReplyKeyboardMarkup, KeyboardButton
 | 
			
		||||
 | 
			
		||||
base_buttons_markup = ReplyKeyboardMarkup(keyboard=[
 | 
			
		||||
    [KeyboardButton(text="👤 Профиль")],    
 | 
			
		||||
    # [KeyboardButton(text="Настройки")]         
 | 
			
		||||
], resize_keyboard=True, one_time_keyboard=False)
 | 
			
		||||
							
								
								
									
										0
									
								
								app/telegram/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/telegram/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -1,306 +0,0 @@
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
import logging.config
 | 
			
		||||
from sqlalchemy.sql.sqltypes import DateTime, Numeric
 | 
			
		||||
 | 
			
		||||
from sqlalchemy import BigInteger, Boolean, Integer, String, ForeignKey
 | 
			
		||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
 | 
			
		||||
from sqlalchemy.ext.asyncio import AsyncAttrs, async_sessionmaker, create_async_engine
 | 
			
		||||
from logger_helper.logger_helper import LOGGING_CONFIG
 | 
			
		||||
from sqlalchemy import select, insert
 | 
			
		||||
 | 
			
		||||
logging.config.dictConfig(LOGGING_CONFIG)
 | 
			
		||||
logger = logging.getLogger("models")
 | 
			
		||||
 | 
			
		||||
engine = create_async_engine(url='sqlite+aiosqlite:///db.sqlite3')
 | 
			
		||||
 | 
			
		||||
async_session = async_sessionmaker(engine)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Base(AsyncAttrs, DeclarativeBase):
 | 
			
		||||
    """Базовый класс для declarative моделей SQLAlchemy с поддержкой async."""
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class User_Telegram_Id(Base):
 | 
			
		||||
    """
 | 
			
		||||
    Модель таблицы user_telegram_id.
 | 
			
		||||
 | 
			
		||||
    Хранит идентификаторы Telegram пользователей.
 | 
			
		||||
 | 
			
		||||
    Атрибуты:
 | 
			
		||||
        id (int): Внутренний первичный ключ записи.
 | 
			
		||||
        tg_id (int): Уникальный идентификатор пользователя Telegram.
 | 
			
		||||
    """
 | 
			
		||||
    __tablename__ = 'user_telegram_id'
 | 
			
		||||
 | 
			
		||||
    id: Mapped[int] = mapped_column(primary_key=True)
 | 
			
		||||
 | 
			
		||||
    tg_id = mapped_column(BigInteger)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class User_Bybit_API(Base):
 | 
			
		||||
    """
 | 
			
		||||
    Модель таблицы user_bybit_api.
 | 
			
		||||
 | 
			
		||||
    Хранит API ключи и секреты Bybit для каждого Telegram пользователя.
 | 
			
		||||
 | 
			
		||||
    Атрибуты:
 | 
			
		||||
        id (int): Внутренний первичный ключ записи.
 | 
			
		||||
        tg_id (int): Внешний ключ на Telegram пользователя (user_telegram_id.tg_id).
 | 
			
		||||
        api_key (str): API ключ Bybit (уникальный для пользователя).
 | 
			
		||||
        secret_key (str): Секретный ключ Bybit (уникальный для пользователя).
 | 
			
		||||
    """
 | 
			
		||||
    __tablename__ = 'user_bybit_api'
 | 
			
		||||
 | 
			
		||||
    id: Mapped[int] = mapped_column(primary_key=True)
 | 
			
		||||
 | 
			
		||||
    tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
 | 
			
		||||
 | 
			
		||||
    api_key = mapped_column(String(18), unique=True, nullable=True)
 | 
			
		||||
    secret_key = mapped_column(String(36), unique=True, nullable=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class User_Symbol(Base):
 | 
			
		||||
    """
 | 
			
		||||
    Модель таблицы user_main_settings.
 | 
			
		||||
 | 
			
		||||
    Хранит основные настройки торговли для пользователя.
 | 
			
		||||
    """
 | 
			
		||||
    __tablename__ = 'user_symbols'
 | 
			
		||||
 | 
			
		||||
    id: Mapped[int] = mapped_column(primary_key=True)
 | 
			
		||||
 | 
			
		||||
    tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
 | 
			
		||||
 | 
			
		||||
    symbol = mapped_column(String(18), default='PENGUUSDT')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Trading_Mode(Base):
 | 
			
		||||
    """
 | 
			
		||||
    Справочник доступных режимов торговли.
 | 
			
		||||
 | 
			
		||||
    Атрибуты:
 | 
			
		||||
        id (int): Первичный ключ.
 | 
			
		||||
        mode (str): Уникальный режим (например, 'Long', 'Short', 'Switch).
 | 
			
		||||
    """
 | 
			
		||||
    __tablename__ = 'trading_modes'
 | 
			
		||||
 | 
			
		||||
    id: Mapped[int] = mapped_column(primary_key=True)
 | 
			
		||||
 | 
			
		||||
    mode = mapped_column(String(10), unique=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Margin_type(Base):
 | 
			
		||||
    """
 | 
			
		||||
    Справочник типов маржинальной торговли.
 | 
			
		||||
 | 
			
		||||
    Атрибуты:
 | 
			
		||||
        id (int): Первичный ключ.
 | 
			
		||||
        type (str): Тип маржи (например, 'Isolated', 'Cross').
 | 
			
		||||
    """
 | 
			
		||||
    __tablename__ = 'margin_types'
 | 
			
		||||
 | 
			
		||||
    id: Mapped[int] = mapped_column(primary_key=True)
 | 
			
		||||
 | 
			
		||||
    type = mapped_column(String(15), unique=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Trigger(Base):
 | 
			
		||||
    """
 | 
			
		||||
    Справочник триггеров для сделок.
 | 
			
		||||
 | 
			
		||||
    Атрибуты:
 | 
			
		||||
        id (int): Первичный ключ.
 | 
			
		||||
    """
 | 
			
		||||
    __tablename__ = 'triggers'
 | 
			
		||||
 | 
			
		||||
    id: Mapped[int] = mapped_column(primary_key=True)
 | 
			
		||||
 | 
			
		||||
    trigger_price = mapped_column(Integer(), default=0)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class User_Main_Settings(Base):
 | 
			
		||||
    """
 | 
			
		||||
    Основные настройки пользователя для торговли.
 | 
			
		||||
 | 
			
		||||
    Атрибуты:
 | 
			
		||||
        id (int): Первичный ключ.
 | 
			
		||||
        tg_id (int): Внешний ключ на Telegram пользователя.
 | 
			
		||||
        trading_mode (str): Режим торговли, FK на trading_modes.mode.
 | 
			
		||||
        margin_type (str): Тип маржи, FK на margin_types.type.
 | 
			
		||||
        size_leverage (int): Кредитное плечо.
 | 
			
		||||
        starting_quantity (int): Начальный объем позиции.
 | 
			
		||||
        martingale_factor (int): Коэффициент мартингейла.
 | 
			
		||||
        martingale_step (int): Текущий шаг мартингейла.
 | 
			
		||||
        maximal_quantity (int): Максимальное число шагов мартингейла.
 | 
			
		||||
        entry_order_type (str): Тип ордера входа (Market/Limit).
 | 
			
		||||
        limit_order_price (Optional[str]): Цена лимитного ордера, если есть.
 | 
			
		||||
        last_side (str): Последняя сторона ордера.
 | 
			
		||||
    """
 | 
			
		||||
    __tablename__ = 'user_main_settings'
 | 
			
		||||
 | 
			
		||||
    id: Mapped[int] = mapped_column(primary_key=True)
 | 
			
		||||
 | 
			
		||||
    tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
 | 
			
		||||
 | 
			
		||||
    trading_mode = mapped_column(ForeignKey("trading_modes.mode"))
 | 
			
		||||
    margin_type = mapped_column(ForeignKey("margin_types.type"))
 | 
			
		||||
    switch_state = mapped_column(String(10), default='По направлению')
 | 
			
		||||
    size_leverage = mapped_column(Integer(), default=1)
 | 
			
		||||
    starting_quantity = mapped_column(Integer(), default=1)
 | 
			
		||||
    martingale_factor = mapped_column(Integer(), default=1)
 | 
			
		||||
    martingale_step = mapped_column(Integer(), default=1)
 | 
			
		||||
    maximal_quantity = mapped_column(Integer(), default=10)
 | 
			
		||||
    entry_order_type = mapped_column(String(10), default='Market')
 | 
			
		||||
    limit_order_price = mapped_column(Numeric(18, 15), nullable=True)
 | 
			
		||||
    last_side = mapped_column(String(10), default='Buy')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class User_Risk_Management_Settings(Base):
 | 
			
		||||
    """
 | 
			
		||||
    Настройки управления рисками пользователя.
 | 
			
		||||
 | 
			
		||||
    Атрибуты:
 | 
			
		||||
        id (int): Первичный ключ.
 | 
			
		||||
        tg_id (int): Внешний ключ на Telegram пользователя.
 | 
			
		||||
        price_profit (int): Процент прибыли для трейда.
 | 
			
		||||
        price_loss (int): Процент убытка для трейда.
 | 
			
		||||
        max_risk_deal (int): Максимально допустимый риск по сделке в процентах.
 | 
			
		||||
        commission_fee (str): Учитывать ли комиссию в расчетах ("Да"/"Нет").
 | 
			
		||||
    """
 | 
			
		||||
    __tablename__ = 'user_risk_management_settings'
 | 
			
		||||
 | 
			
		||||
    id: Mapped[int] = mapped_column(primary_key=True)
 | 
			
		||||
 | 
			
		||||
    tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
 | 
			
		||||
 | 
			
		||||
    price_profit = mapped_column(Integer(), default=1)
 | 
			
		||||
    price_loss = mapped_column(Integer(), default=1)
 | 
			
		||||
    max_risk_deal = mapped_column(Integer(), default=100)
 | 
			
		||||
    commission_fee = mapped_column(String(), default="Да")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class User_Condition_Settings(Base):
 | 
			
		||||
    """
 | 
			
		||||
    Дополнительные пользовательские условия для торговли.
 | 
			
		||||
 | 
			
		||||
    Атрибуты:
 | 
			
		||||
        id (int): Первичный ключ.
 | 
			
		||||
        tg_id (int): Внешний ключ на Telegram пользователя.
 | 
			
		||||
        trigger (str): Тип триггера, FK на triggers.trigger.
 | 
			
		||||
        filter_time (str): Временной фильтр.
 | 
			
		||||
        filter_volatility (bool): Фильтр по волатильности.
 | 
			
		||||
        external_cues (bool): Внешние сигналы.
 | 
			
		||||
        tradingview_cues (bool): Сигналы TradingView.
 | 
			
		||||
        webhook (str): URL webhook.
 | 
			
		||||
        ai_analytics (bool): Использование AI для аналитики.
 | 
			
		||||
    """
 | 
			
		||||
    __tablename__ = 'user_condition_settings'
 | 
			
		||||
 | 
			
		||||
    id: Mapped[int] = mapped_column(primary_key=True)
 | 
			
		||||
 | 
			
		||||
    tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
 | 
			
		||||
 | 
			
		||||
    trigger = mapped_column(String(15), default='Автоматический')
 | 
			
		||||
    filter_time = mapped_column(String(25), default='???')
 | 
			
		||||
    filter_volatility = mapped_column(Boolean, default=False)
 | 
			
		||||
    external_cues = mapped_column(Boolean, default=False)
 | 
			
		||||
    tradingview_cues = mapped_column(Boolean, default=False)
 | 
			
		||||
    webhook = mapped_column(String(40), default='')
 | 
			
		||||
    ai_analytics = mapped_column(Boolean, default=False)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class User_Additional_Settings(Base):
 | 
			
		||||
    """
 | 
			
		||||
    Прочие дополнительные настройки пользователя.
 | 
			
		||||
 | 
			
		||||
    Атрибуты:
 | 
			
		||||
        id (int): Первичный ключ.
 | 
			
		||||
        tg_id (int): Внешний ключ на Telegram пользователя.
 | 
			
		||||
        pattern_save (bool): Сохранять ли шаблоны.
 | 
			
		||||
        autostart (bool): Автоматический запуск.
 | 
			
		||||
        notifications (bool): Получение уведомлений.
 | 
			
		||||
    """
 | 
			
		||||
    __tablename__ = 'user_additional_settings'
 | 
			
		||||
 | 
			
		||||
    id: Mapped[int] = mapped_column(primary_key=True)
 | 
			
		||||
 | 
			
		||||
    tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
 | 
			
		||||
 | 
			
		||||
    pattern_save = mapped_column(Boolean, default=False)
 | 
			
		||||
    autostart = mapped_column(Boolean, default=False)
 | 
			
		||||
    notifications = mapped_column(Boolean, default=False)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class USER_DEALS(Base):
 | 
			
		||||
    """
 | 
			
		||||
    Таблица сделок пользователя.
 | 
			
		||||
 | 
			
		||||
    Атрибуты:
 | 
			
		||||
        id (int): Первичный ключ.
 | 
			
		||||
        tg_id (int): Внешний ключ на Telegram пользователя.
 | 
			
		||||
        symbol (str): Торговая пара.
 | 
			
		||||
        side (str): Направление сделки (Buy/Sell).
 | 
			
		||||
        open_price (int): Цена открытия.
 | 
			
		||||
        positive_percent (int): Процент доходности.
 | 
			
		||||
    """
 | 
			
		||||
    __tablename__ = 'user_deals'
 | 
			
		||||
 | 
			
		||||
    id: Mapped[int] = mapped_column(primary_key=True)
 | 
			
		||||
 | 
			
		||||
    tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
 | 
			
		||||
 | 
			
		||||
    symbol = mapped_column(String(18), default='PENGUUSDT')
 | 
			
		||||
    side = mapped_column(String(10), nullable=False)
 | 
			
		||||
    open_price = mapped_column(Integer(), nullable=False)
 | 
			
		||||
    positive_percent = mapped_column(Integer(), nullable=False)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserTimer(Base):
 | 
			
		||||
    """
 | 
			
		||||
    Таймер пользователя для отсроченного запуска сделок.
 | 
			
		||||
 | 
			
		||||
    Атрибуты:
 | 
			
		||||
        id (int): Первичный ключ.
 | 
			
		||||
        tg_id (int): Внешний ключ на Telegram пользователя.
 | 
			
		||||
        timer_minutes (int): Количество минут таймера.
 | 
			
		||||
        timer_start (datetime): Время начала таймера.
 | 
			
		||||
        timer_end (Optional[datetime]): Время окончания таймера (если установлено).
 | 
			
		||||
    """
 | 
			
		||||
    __tablename__ = 'user_timers'
 | 
			
		||||
 | 
			
		||||
    id: Mapped[int] = mapped_column(primary_key=True)
 | 
			
		||||
    tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
 | 
			
		||||
    timer_minutes = mapped_column(Integer, nullable=False, default=0)
 | 
			
		||||
    timer_start = mapped_column(DateTime, default=datetime.utcnow)
 | 
			
		||||
    timer_end = mapped_column(DateTime, nullable=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def async_main():
 | 
			
		||||
    """
 | 
			
		||||
    Асинхронное создание всех таблиц и заполнение справочников начальными данными.
 | 
			
		||||
    """
 | 
			
		||||
    async with engine.begin() as conn:
 | 
			
		||||
        await conn.run_sync(Base.metadata.create_all)
 | 
			
		||||
 | 
			
		||||
        # Заполнение таблиц
 | 
			
		||||
        modes = ['Long', 'Short', 'Switch', 'Smart']
 | 
			
		||||
        for mode in modes:
 | 
			
		||||
            result = await conn.execute(select(Trading_Mode).where(Trading_Mode.mode == mode))
 | 
			
		||||
            if not result.first():
 | 
			
		||||
                logger.info("Заполение таблицы режима торговли")
 | 
			
		||||
                await conn.execute(Trading_Mode.__table__.insert().values(mode=mode))
 | 
			
		||||
 | 
			
		||||
        types = ['Isolated', 'Cross']
 | 
			
		||||
        for type in types:
 | 
			
		||||
            result = await conn.execute(select(Margin_type).where(Margin_type.type == type))
 | 
			
		||||
            if not result.first():
 | 
			
		||||
                logger.info("Заполение таблицы типов маржи")
 | 
			
		||||
                await conn.execute(Margin_type.__table__.insert().values(type=type))
 | 
			
		||||
 | 
			
		||||
        last_side = ['Buy', 'Sell']
 | 
			
		||||
        for side in last_side:
 | 
			
		||||
            result = await conn.execute(select(User_Main_Settings).where(User_Main_Settings.last_side == side))
 | 
			
		||||
            if not result.first():
 | 
			
		||||
                logger.info("Заполение таблицы последнего направления")
 | 
			
		||||
                await conn.execute(User_Main_Settings.__table__.insert().values(last_side=side))
 | 
			
		||||
@@ -1,585 +0,0 @@
 | 
			
		||||
import logging.config
 | 
			
		||||
 | 
			
		||||
from logger_helper.logger_helper import LOGGING_CONFIG
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
from app.telegram.database.models import (
 | 
			
		||||
    async_session,
 | 
			
		||||
    User_Telegram_Id as UTi,
 | 
			
		||||
    User_Main_Settings as UMS,
 | 
			
		||||
    User_Bybit_API as UBA,
 | 
			
		||||
    User_Symbol,
 | 
			
		||||
    User_Risk_Management_Settings as URMS,
 | 
			
		||||
    User_Condition_Settings as UCS,
 | 
			
		||||
    User_Additional_Settings as UAS,
 | 
			
		||||
    Trading_Mode,
 | 
			
		||||
    Margin_type,
 | 
			
		||||
    Trigger,
 | 
			
		||||
    USER_DEALS,
 | 
			
		||||
    UserTimer,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from sqlalchemy import select, update
 | 
			
		||||
 | 
			
		||||
logging.config.dictConfig(LOGGING_CONFIG)
 | 
			
		||||
logger = logging.getLogger("requests")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# --- Функции сохранения в БД ---
 | 
			
		||||
 | 
			
		||||
async def save_tg_id_new_user(tg_id) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Сохраняет Telegram ID нового пользователя в базу, если такого ещё нет.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        tg_id (int): Telegram ID пользователя.
 | 
			
		||||
    """
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        user = await session.scalar(select(UTi).where(UTi.tg_id == tg_id))
 | 
			
		||||
 | 
			
		||||
        if not user:
 | 
			
		||||
            session.add(UTi(tg_id=tg_id))
 | 
			
		||||
 | 
			
		||||
            logger.info("Новый пользователь был добавлен в бд %s", tg_id)
 | 
			
		||||
 | 
			
		||||
        await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def set_new_user_bybit_api(tg_id) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Создаёт запись API пользователя Bybit, если её ещё нет.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        tg_id (int): Telegram ID пользователя.
 | 
			
		||||
    """
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        user = await session.scalar(select(UBA).where(UBA.tg_id == tg_id))
 | 
			
		||||
 | 
			
		||||
        if not user:
 | 
			
		||||
            session.add(UBA(tg_id=tg_id))
 | 
			
		||||
        await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def set_new_user_symbol(tg_id) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Создаёт запись торгового символа пользователя, если её нет.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        tg_id (int): Telegram ID пользователя.
 | 
			
		||||
    """
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        user = await session.scalar(select(User_Symbol).where(User_Symbol.tg_id == tg_id))
 | 
			
		||||
 | 
			
		||||
        if not user:
 | 
			
		||||
            session.add(User_Symbol(tg_id=tg_id))
 | 
			
		||||
 | 
			
		||||
            logger.info(f"Symbol был успешно добавлен %s", tg_id)
 | 
			
		||||
 | 
			
		||||
        await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def set_new_user_default_main_settings(tg_id, trading_mode, margin_type) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Создаёт основные настройки пользователя по умолчанию.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        tg_id (int): Telegram ID пользователя.
 | 
			
		||||
        trading_mode (str): Режим торговли.
 | 
			
		||||
        margin_type (str): Тип маржи.
 | 
			
		||||
    """
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        settings = await session.scalar(select(UMS).where(UMS.tg_id == tg_id))
 | 
			
		||||
 | 
			
		||||
        if not settings:
 | 
			
		||||
            session.add(UMS(
 | 
			
		||||
                tg_id=tg_id,
 | 
			
		||||
                trading_mode=trading_mode,
 | 
			
		||||
                margin_type=margin_type,
 | 
			
		||||
            ))
 | 
			
		||||
 | 
			
		||||
            logger.info("Основные настройки нового пользователя были заполнены%s", tg_id)
 | 
			
		||||
 | 
			
		||||
        await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def set_new_user_default_risk_management_settings(tg_id) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Создаёт настройки риск-менеджмента по умолчанию.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        tg_id (int): Telegram ID пользователя.
 | 
			
		||||
    """
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        settings = await session.scalar(select(URMS).where(URMS.tg_id == tg_id))
 | 
			
		||||
 | 
			
		||||
        if not settings:
 | 
			
		||||
            session.add(URMS(
 | 
			
		||||
                tg_id=tg_id
 | 
			
		||||
            ))
 | 
			
		||||
 | 
			
		||||
            logger.info("Риск-Менеджмент настройки нового пользователя были заполнены %s", tg_id)
 | 
			
		||||
 | 
			
		||||
        await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def set_new_user_default_condition_settings(tg_id, trigger) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Создаёт условные настройки по умолчанию.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        tg_id (int): Telegram ID пользователя.
 | 
			
		||||
        trigger (Any): Значение триггера по умолчанию.
 | 
			
		||||
    """
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        settings = await session.scalar(select(UCS).where(UCS.tg_id == tg_id))
 | 
			
		||||
 | 
			
		||||
        if not settings:
 | 
			
		||||
            session.add(UCS(
 | 
			
		||||
                tg_id=tg_id,
 | 
			
		||||
                trigger=trigger
 | 
			
		||||
            ))
 | 
			
		||||
 | 
			
		||||
            logger.info("Условные настройки нового пользователя были заполнены %s", tg_id)
 | 
			
		||||
 | 
			
		||||
        await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def set_new_user_default_additional_settings(tg_id) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Создаёт дополнительные настройки по умолчанию.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        tg_id (int): Telegram ID пользователя.
 | 
			
		||||
    """
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        settings = await session.scalar(select(UAS).where(UAS.tg_id == tg_id))
 | 
			
		||||
 | 
			
		||||
        if not settings:
 | 
			
		||||
            session.add(UAS(
 | 
			
		||||
                tg_id=tg_id,
 | 
			
		||||
            ))
 | 
			
		||||
 | 
			
		||||
            logger.info("Дополнительные настройки нового пользователя были заполнены %s", tg_id)
 | 
			
		||||
 | 
			
		||||
        await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# --- Функции получения данных из БД ---
 | 
			
		||||
 | 
			
		||||
async def check_user(tg_id):
 | 
			
		||||
    """
 | 
			
		||||
    Проверяет наличие пользователя в базе.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        tg_id (int): Telegram ID пользователя.
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        Optional[UTi]: Пользователь или None.
 | 
			
		||||
    """
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        user = await session.scalar(select(UTi).where(UTi.tg_id == tg_id))
 | 
			
		||||
        return user
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_bybit_api_key(tg_id):
 | 
			
		||||
    """Получить API ключ Bybit пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        api_key = await session.scalar(select(UBA.api_key).where(UBA.tg_id == tg_id))
 | 
			
		||||
        return api_key
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_bybit_secret_key(tg_id):
 | 
			
		||||
    """Получить секретный ключ Bybit пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        secret_key = await session.scalar(select(UBA.secret_key).where(UBA.tg_id == tg_id))
 | 
			
		||||
        return secret_key
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_symbol(tg_id):
 | 
			
		||||
    """Получить символ пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        symbol = await session.scalar(select(User_Symbol.symbol).where(User_Symbol.tg_id == tg_id))
 | 
			
		||||
        return symbol
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_user_trades(tg_id):
 | 
			
		||||
    """Получить сделки пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        query = select(USER_DEALS.symbol, USER_DEALS.side).where(USER_DEALS.tg_id == tg_id)
 | 
			
		||||
        result = await session.execute(query)
 | 
			
		||||
        trades = result.all()
 | 
			
		||||
        return trades
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_entry_order_type(tg_id: object) -> str | None | Any:
 | 
			
		||||
    """Получить тип входного ордера пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        order_type = await session.scalar(
 | 
			
		||||
            select(UMS.entry_order_type).where(UMS.tg_id == tg_id)
 | 
			
		||||
        )
 | 
			
		||||
        # Если в базе не установлен тип — возвращаем значение по умолчанию
 | 
			
		||||
        return order_type or 'Market'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# --- Функции обновления данных ---
 | 
			
		||||
 | 
			
		||||
async def update_user_trades(tg_id, **kwargs):
 | 
			
		||||
    """Обновить сделки пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        query = update(USER_DEALS).where(USER_DEALS.tg_id == tg_id).values(**kwargs)
 | 
			
		||||
        await session.execute(query)
 | 
			
		||||
        await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def update_symbol(tg_id: int, symbol: str) -> None:
 | 
			
		||||
    """Обновить торговый символ пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        await session.execute(update(User_Symbol).where(User_Symbol.tg_id == tg_id).values(symbol=symbol))
 | 
			
		||||
        await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def upsert_api_keys(tg_id: int, api_key: str, secret_key: str) -> None:
 | 
			
		||||
    """Обновить API ключ пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        result = await session.execute(select(UBA).where(UBA.tg_id == tg_id))
 | 
			
		||||
        user = result.scalars().first()
 | 
			
		||||
        if user:
 | 
			
		||||
            if api_key is not None:
 | 
			
		||||
                user.api_key = api_key
 | 
			
		||||
            if secret_key is not None:
 | 
			
		||||
                user.secret_key = secret_key
 | 
			
		||||
            logger.info(f"Обновлены ключи для пользователя {tg_id}")
 | 
			
		||||
        else:
 | 
			
		||||
            new_user = UBA(tg_id=tg_id, api_key=api_key, secret_key=secret_key)
 | 
			
		||||
            session.add(new_user)
 | 
			
		||||
            logger.info(f"Добавлен новый пользователь {tg_id} с ключами")
 | 
			
		||||
        await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# --- Более мелкие обновления и запросы по настройкам ---
 | 
			
		||||
 | 
			
		||||
async def update_trade_mode_user(tg_id, trading_mode) -> None:
 | 
			
		||||
    """Обновить режим торговли пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.mode == trading_mode))
 | 
			
		||||
 | 
			
		||||
        if mode:
 | 
			
		||||
            logger.info("Изменён торговый режим для пользователя %s", tg_id)
 | 
			
		||||
            await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(trading_mode=mode))
 | 
			
		||||
 | 
			
		||||
            await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def delete_user_trade(tg_id: int, symbol: str):
 | 
			
		||||
    """Удалить сделку пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        await session.execute(
 | 
			
		||||
            USER_DEALS.__table__.delete().where(
 | 
			
		||||
                (USER_DEALS.tg_id == tg_id) & (USER_DEALS.symbol == symbol)
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_for_registration_trading_mode():
 | 
			
		||||
    """Получить режим торговли по умолчанию."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.id == 1))
 | 
			
		||||
        return mode
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_for_registration_margin_type():
 | 
			
		||||
    """Получить тип маржи по умолчанию."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        type = await session.scalar(select(Margin_type.type).where(Margin_type.id == 1))
 | 
			
		||||
        return type
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_for_registration_trigger(tg_id):
 | 
			
		||||
    """Получить триггер по умолчанию."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        trigger = await session.scalar(select(UCS.trigger).where(tg_id == tg_id))
 | 
			
		||||
        return trigger
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_user_main_settings(tg_id):
 | 
			
		||||
    """Получить основные настройки пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        user = await session.scalar(select(UMS).where(UMS.tg_id == tg_id))
 | 
			
		||||
        if user:
 | 
			
		||||
            data = {
 | 
			
		||||
                'trading_mode': user.trading_mode,
 | 
			
		||||
                'margin_type': user.margin_type,
 | 
			
		||||
                'switch_state': user.switch_state,
 | 
			
		||||
                'size_leverage': user.size_leverage,
 | 
			
		||||
                'starting_quantity': user.starting_quantity,
 | 
			
		||||
                'martingale_factor': user.martingale_factor,
 | 
			
		||||
                'maximal_quantity': user.maximal_quantity,
 | 
			
		||||
                'entry_order_type': user.entry_order_type,
 | 
			
		||||
                'limit_order_price': user.limit_order_price,
 | 
			
		||||
                'martingale_step': user.martingale_step,
 | 
			
		||||
                'last_side': user.last_side,
 | 
			
		||||
            }
 | 
			
		||||
            return data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_user_risk_management_settings(tg_id):
 | 
			
		||||
    """Получить риск-менеджмента настройки пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        user = await session.scalar(select(URMS).where(URMS.tg_id == tg_id))
 | 
			
		||||
 | 
			
		||||
        if user:
 | 
			
		||||
            logger.info("Получение риск-менеджмента настроек пользователя %s", tg_id)
 | 
			
		||||
 | 
			
		||||
            price_profit = await session.scalar(select(URMS.price_profit).where(URMS.tg_id == tg_id))
 | 
			
		||||
            price_loss = await session.scalar(select(URMS.price_loss).where(URMS.tg_id == tg_id))
 | 
			
		||||
            max_risk_deal = await session.scalar(select(URMS.max_risk_deal).where(URMS.tg_id == tg_id))
 | 
			
		||||
            commission_fee = await session.scalar(select(URMS.commission_fee).where(URMS.tg_id == tg_id))
 | 
			
		||||
 | 
			
		||||
            data = {
 | 
			
		||||
                'price_profit': price_profit,
 | 
			
		||||
                'price_loss': price_loss,
 | 
			
		||||
                'max_risk_deal': max_risk_deal,
 | 
			
		||||
                'commission_fee': commission_fee,
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def update_margin_type(tg_id, margin_type) -> None:
 | 
			
		||||
    """Обновить тип маржи пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        type = await session.scalar(select(Margin_type.type).where(Margin_type.type == margin_type))
 | 
			
		||||
 | 
			
		||||
        if type:
 | 
			
		||||
            logger.info("Изменен тип маржи %s", tg_id)
 | 
			
		||||
            await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(margin_type=type))
 | 
			
		||||
 | 
			
		||||
            await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def update_size_leverange(tg_id, num):
 | 
			
		||||
    """Обновить размер левеража пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(size_leverage=num))
 | 
			
		||||
 | 
			
		||||
        await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def update_starting_quantity(tg_id, num):
 | 
			
		||||
    """Обновить размер левеража пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(starting_quantity=num))
 | 
			
		||||
 | 
			
		||||
        await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def update_martingale_factor(tg_id, num):
 | 
			
		||||
    """Обновить размер левеража пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(martingale_factor=num))
 | 
			
		||||
 | 
			
		||||
        await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def update_maximal_quantity(tg_id, num):
 | 
			
		||||
    """Обновить размер левеража пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(maximal_quantity=num))
 | 
			
		||||
 | 
			
		||||
        await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# ОБНОВЛЕНИЕ НАСТРОЕК РИСК-МЕНЕДЖМЕНТА
 | 
			
		||||
 | 
			
		||||
async def update_price_profit(tg_id, num):
 | 
			
		||||
    """Обновить цену тейк-профита (прибыль) пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(price_profit=num))
 | 
			
		||||
 | 
			
		||||
        await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def update_price_loss(tg_id, num):
 | 
			
		||||
    """Обновить цену тейк-лосса (убыток) пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(price_loss=num))
 | 
			
		||||
 | 
			
		||||
        await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def update_max_risk_deal(tg_id, num):
 | 
			
		||||
    """Обновить максимальную сумму риска пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(max_risk_deal=num))
 | 
			
		||||
 | 
			
		||||
        await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def update_entry_order_type(tg_id, order_type):
 | 
			
		||||
    """Обновить тип входного ордера пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        await session.execute(
 | 
			
		||||
            update(UMS)
 | 
			
		||||
            .where(UMS.tg_id == tg_id)
 | 
			
		||||
            .values(entry_order_type=order_type)
 | 
			
		||||
        )
 | 
			
		||||
        await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_limit_price(tg_id):
 | 
			
		||||
    """Получить лимитную цену пользователя как float, либо None."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        result = await session.execute(
 | 
			
		||||
            select(UMS.limit_order_price)
 | 
			
		||||
            .where(UMS.tg_id == tg_id)
 | 
			
		||||
        )
 | 
			
		||||
        price = result.scalar_one_or_none()
 | 
			
		||||
        if price:
 | 
			
		||||
            try:
 | 
			
		||||
                return float(price)
 | 
			
		||||
            except ValueError:
 | 
			
		||||
                return None
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def update_limit_price(tg_id, price):
 | 
			
		||||
    """Обновить лимитную цену пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        await session.execute(
 | 
			
		||||
            update(UMS)
 | 
			
		||||
            .where(UMS.tg_id == tg_id)
 | 
			
		||||
            .values(limit_order_price=str(price))
 | 
			
		||||
        )
 | 
			
		||||
        await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def update_commission_fee(tg_id, num):
 | 
			
		||||
    """Обновить комиссию пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(commission_fee=num))
 | 
			
		||||
 | 
			
		||||
        await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_user_timer(tg_id):
 | 
			
		||||
    """Получить данные о таймере пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        result = await session.execute(select(UserTimer).where(UserTimer.tg_id == tg_id))
 | 
			
		||||
        user_timer = result.scalars().first()
 | 
			
		||||
 | 
			
		||||
        if not user_timer:
 | 
			
		||||
            logging.info(f"No timer found for user {tg_id}")
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        timer_minutes = user_timer.timer_minutes
 | 
			
		||||
        timer_start = user_timer.timer_start
 | 
			
		||||
        timer_end = user_timer.timer_end
 | 
			
		||||
 | 
			
		||||
        logging.info(f"Timer data for tg_id={tg_id}: "
 | 
			
		||||
                     f"timer_minutes={timer_minutes}, "
 | 
			
		||||
                     f"timer_start={timer_start}, "
 | 
			
		||||
                     f"timer_end={timer_end}")
 | 
			
		||||
 | 
			
		||||
        remaining = None
 | 
			
		||||
        if timer_end:
 | 
			
		||||
            remaining = max(0, int((timer_end - datetime.utcnow()).total_seconds() // 60))
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            "timer_minutes": timer_minutes,
 | 
			
		||||
            "timer_start": timer_start,
 | 
			
		||||
            "timer_end": timer_end,
 | 
			
		||||
            "remaining_minutes": remaining
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def update_user_timer(tg_id, minutes: int):
 | 
			
		||||
    """Обновить данные о таймере пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        try:
 | 
			
		||||
            timer_start = None
 | 
			
		||||
            timer_end = None
 | 
			
		||||
 | 
			
		||||
            if minutes > 0:
 | 
			
		||||
                timer_start = datetime.utcnow()
 | 
			
		||||
                timer_end = timer_start + timedelta(minutes=minutes)
 | 
			
		||||
 | 
			
		||||
            result = await session.execute(select(UserTimer).where(UserTimer.tg_id == tg_id))
 | 
			
		||||
            user_timer = result.scalars().first()
 | 
			
		||||
 | 
			
		||||
            if user_timer:
 | 
			
		||||
                user_timer.timer_minutes = minutes
 | 
			
		||||
                user_timer.timer_start = timer_start
 | 
			
		||||
                user_timer.timer_end = timer_end
 | 
			
		||||
            else:
 | 
			
		||||
                user_timer = UserTimer(
 | 
			
		||||
                    tg_id=tg_id,
 | 
			
		||||
                    timer_minutes=minutes,
 | 
			
		||||
                    timer_start=timer_start,
 | 
			
		||||
                    timer_end=timer_end
 | 
			
		||||
                )
 | 
			
		||||
                session.add(user_timer)
 | 
			
		||||
 | 
			
		||||
            await session.commit()
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logging.error(f"Ошибка обновления таймера пользователя {tg_id}: {e}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_martingale_step(tg_id):
 | 
			
		||||
    """Получить шаг мартингейла пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        result = await session.execute(select(UMS).where(UMS.tg_id == tg_id))
 | 
			
		||||
        user_settings = result.scalars().first()
 | 
			
		||||
        return user_settings.martingale_step
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def update_martingale_step(tg_id, step):
 | 
			
		||||
    """Обновить шаг мартингейла пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(martingale_step=step))
 | 
			
		||||
 | 
			
		||||
        await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def update_switch_mode_enabled(tg_id, switch_mode):
 | 
			
		||||
    """Обновить режим переключения пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(switch_mode_enabled=switch_mode))
 | 
			
		||||
 | 
			
		||||
        await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def update_switch_state(tg_id, switch_state):
 | 
			
		||||
    """Обновить состояние переключения пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(switch_state=switch_state))
 | 
			
		||||
 | 
			
		||||
        await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def update_trigger(tg_id, trigger):
 | 
			
		||||
    """Обновить триггер пользователя."""
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        await session.execute(update(UCS).where(UCS.tg_id == tg_id).values(trigger=trigger))
 | 
			
		||||
 | 
			
		||||
        await session.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def set_last_series_info(tg_id: int, last_side: str):
 | 
			
		||||
    async with async_session() as session:
 | 
			
		||||
        async with session.begin():
 | 
			
		||||
            # Обновляем запись
 | 
			
		||||
            result = await session.execute(
 | 
			
		||||
                update(UMS)
 | 
			
		||||
                .where(UMS.tg_id == tg_id)
 | 
			
		||||
                .values(last_side=last_side)
 | 
			
		||||
            )
 | 
			
		||||
            if result.rowcount == 0:
 | 
			
		||||
                # Если запись не существует, создаём новую
 | 
			
		||||
                new_entry = UMS(
 | 
			
		||||
                    tg_id=tg_id,
 | 
			
		||||
                    last_side=last_side,
 | 
			
		||||
                )
 | 
			
		||||
                session.add(new_entry)
 | 
			
		||||
        await session.commit()
 | 
			
		||||
@@ -1,38 +0,0 @@
 | 
			
		||||
import app.telegram.Keyboards.inline_keyboards as inline_markup
 | 
			
		||||
 | 
			
		||||
import app.telegram.database.requests as rq
 | 
			
		||||
 | 
			
		||||
async def reg_new_user_default_additional_settings(id, message):
 | 
			
		||||
    tg_id = id
 | 
			
		||||
 | 
			
		||||
    await rq.set_new_user_default_additional_settings(tg_id)
 | 
			
		||||
 | 
			
		||||
async def main_settings_message(id, message):
 | 
			
		||||
    text = '''<b>Дополнительные параметры</b>
 | 
			
		||||
 | 
			
		||||
<b>- Сохранить как шаблон стратегии:</b> да / нет  
 | 
			
		||||
<b>- Автозапуск после сохранения:</b> да / нет  
 | 
			
		||||
<b>- Уведомления в Telegram:</b> включено / отключено '''
 | 
			
		||||
 | 
			
		||||
    await message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.additional_settings_markup)
 | 
			
		||||
 | 
			
		||||
async def save_pattern_message(message, state):
 | 
			
		||||
    text = '''<b>Сохранение шаблона</b>
 | 
			
		||||
                                                                                          
 | 
			
		||||
    Описание... '''
 | 
			
		||||
 | 
			
		||||
    await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup)
 | 
			
		||||
 | 
			
		||||
async def auto_start_message(message, state):
 | 
			
		||||
    text = '''<b>Автозапуск</b>
 | 
			
		||||
 | 
			
		||||
    Описание... '''
 | 
			
		||||
 | 
			
		||||
    await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup)
 | 
			
		||||
 | 
			
		||||
async def notifications_message(message, state):
 | 
			
		||||
    text = '''<b>Уведомления</b>                                                                
 | 
			
		||||
 | 
			
		||||
    Описание... '''
 | 
			
		||||
 | 
			
		||||
    await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_on_off_markup)                                                                                                                                
 | 
			
		||||
@@ -1,143 +0,0 @@
 | 
			
		||||
import logging.config
 | 
			
		||||
import app.telegram.Keyboards.inline_keyboards as inline_markup
 | 
			
		||||
from aiogram import Router, F
 | 
			
		||||
from aiogram.types import Message, CallbackQuery
 | 
			
		||||
from aiogram.fsm.context import FSMContext
 | 
			
		||||
import app.telegram.database.requests as rq
 | 
			
		||||
from app.states.States import condition_settings
 | 
			
		||||
 | 
			
		||||
from logger_helper.logger_helper import LOGGING_CONFIG
 | 
			
		||||
 | 
			
		||||
logging.config.dictConfig(LOGGING_CONFIG)
 | 
			
		||||
logger = logging.getLogger("condition_settings")
 | 
			
		||||
 | 
			
		||||
condition_settings_router = Router()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def reg_new_user_default_condition_settings(id):
 | 
			
		||||
    tg_id = id
 | 
			
		||||
 | 
			
		||||
    trigger = await rq.get_for_registration_trigger(tg_id)
 | 
			
		||||
 | 
			
		||||
    await rq.set_new_user_default_condition_settings(tg_id, trigger)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def main_settings_message(id, message):
 | 
			
		||||
 | 
			
		||||
    tg_id = id
 | 
			
		||||
    trigger = await rq.get_for_registration_trigger(tg_id)
 | 
			
		||||
    text = f""" <b>Условия запуска</b>
 | 
			
		||||
 | 
			
		||||
<b>- Режим торговли:</b>  {trigger}
 | 
			
		||||
<b>- Таймер: </b> установить таймер / удалить таймер
 | 
			
		||||
"""
 | 
			
		||||
    await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.condition_settings_markup)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def trigger_message(id, message, state: FSMContext):
 | 
			
		||||
    await state.set_state(condition_settings.trigger)
 | 
			
		||||
    text = '''
 | 
			
		||||
<b>- Автоматический:</b> торговля будет происходить в рамках серии ставок.
 | 
			
		||||
<b>- Ручной:</b> торговля будет происходить только в ручном режиме.
 | 
			
		||||
<em>- Выберите тип триггера:</em>'''
 | 
			
		||||
 | 
			
		||||
    await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trigger_markup)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@condition_settings_router.callback_query(F.data == "clb_trigger_manual")
 | 
			
		||||
async def trigger_manual_callback(callback: CallbackQuery, state: FSMContext):
 | 
			
		||||
    await state.set_state(condition_settings.trigger)
 | 
			
		||||
    await rq.update_trigger(tg_id=callback.from_user.id, trigger="Ручной")
 | 
			
		||||
    await main_settings_message(callback.from_user.id, callback.message)
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@condition_settings_router.callback_query(F.data == "clb_trigger_auto")
 | 
			
		||||
async def trigger_manual_callback(callback: CallbackQuery, state: FSMContext):
 | 
			
		||||
    await state.set_state(condition_settings.trigger)
 | 
			
		||||
    await rq.update_trigger(tg_id=callback.from_user.id, trigger="Автоматический")
 | 
			
		||||
    await main_settings_message(callback.from_user.id, callback.message)
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
async def timer_message(id, message: Message, state: FSMContext):
 | 
			
		||||
    await state.set_state(condition_settings.timer)
 | 
			
		||||
 | 
			
		||||
    timer_info = await rq.get_user_timer(id)
 | 
			
		||||
    if timer_info is None:
 | 
			
		||||
        await message.answer("Таймер не установлен.", reply_markup=inline_markup.timer_markup)
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    await message.answer(
 | 
			
		||||
        f"Таймер установлен на: {timer_info['timer_minutes']} мин\n",
 | 
			
		||||
        reply_markup=inline_markup.timer_markup
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@condition_settings_router.callback_query(F.data == "clb_set_timer")
 | 
			
		||||
async def set_timer_callback(callback: CallbackQuery, state: FSMContext):
 | 
			
		||||
    await state.set_state(condition_settings.timer)  # состояние для ввода времени
 | 
			
		||||
    await callback.message.answer("Введите время работы в минутах (например, 60):", reply_markup=inline_markup.cancel)
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@condition_settings_router.message(condition_settings.timer)
 | 
			
		||||
async def process_timer_input(message: Message, state: FSMContext):
 | 
			
		||||
    try:
 | 
			
		||||
        minutes = int(message.text)
 | 
			
		||||
        if minutes <= 0:
 | 
			
		||||
            await message.reply("Введите число больше нуля.")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        await rq.update_user_timer(message.from_user.id, minutes)
 | 
			
		||||
        logger.info("Timer set for user %s: %s minutes", message.from_user.id, minutes)
 | 
			
		||||
        await timer_message(message.from_user.id, message, state)
 | 
			
		||||
        await state.clear()
 | 
			
		||||
    except ValueError:
 | 
			
		||||
        await message.reply("Пожалуйста, введите корректное число.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@condition_settings_router.callback_query(F.data == "clb_delete_timer")
 | 
			
		||||
async def delete_timer_callback(callback: CallbackQuery, state: FSMContext):
 | 
			
		||||
    await state.clear()
 | 
			
		||||
    await rq.update_user_timer(callback.from_user.id, 0)
 | 
			
		||||
    logger.info("Timer deleted for user %s", callback.from_user.id)
 | 
			
		||||
    await timer_message(callback.from_user.id, callback.message, state)
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def filter_volatility_message(message, state):
 | 
			
		||||
    text = '''Фильтр волатильности
 | 
			
		||||
 | 
			
		||||
    Описание... '''
 | 
			
		||||
 | 
			
		||||
    await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_on_off_markup)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def external_cues_message(message, state):
 | 
			
		||||
    text = '''<b>Внешние сигналы</b>
 | 
			
		||||
 | 
			
		||||
    Описание... '''
 | 
			
		||||
 | 
			
		||||
    await message.answer(text=text, parse_mode='html', reply_markup=None)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def trading_cues_message(message, state):
 | 
			
		||||
    text = '''<b>Использование сигналов</b>
 | 
			
		||||
 | 
			
		||||
    Описание... '''
 | 
			
		||||
 | 
			
		||||
    await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def webhook_message(message, state):
 | 
			
		||||
    text = '''Скиньте ссылку на <b>webhook</b> (если есть trading view): '''
 | 
			
		||||
 | 
			
		||||
    await message.answer(text=text, parse_mode='html')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def ai_analytics_message(message, state):
 | 
			
		||||
    text = '''<b>ИИ - Аналитика</b> 
 | 
			
		||||
 | 
			
		||||
    Описание... '''
 | 
			
		||||
 | 
			
		||||
    await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup)
 | 
			
		||||
@@ -1,29 +0,0 @@
 | 
			
		||||
import app.telegram.Keyboards.inline_keyboards as inline_markup
 | 
			
		||||
import app.telegram.Keyboards.reply_keyboards as reply_markup
 | 
			
		||||
 | 
			
		||||
async def start_message(message):
 | 
			
		||||
    username = ''
 | 
			
		||||
    
 | 
			
		||||
    if message.from_user.first_name == None:
 | 
			
		||||
        username = message.from_user.last_name
 | 
			
		||||
    elif message.from_user.last_name == None:
 | 
			
		||||
        username = message.from_user.first_name
 | 
			
		||||
    else:
 | 
			
		||||
        username = f'{message.from_user.first_name} {message.from_user.last_name}'
 | 
			
		||||
    await message.answer(f""" Привет <b>{username}</b>! 👋""", parse_mode='html')
 | 
			
		||||
    await message.answer("Добро пожаловать в чат-робот для автоматизации трейдинга — вашего надежного помощника для анализа рынка и принятия взвешенных решений.",
 | 
			
		||||
                         parse_mode='html', reply_markup=inline_markup.start_markup)
 | 
			
		||||
 | 
			
		||||
async def profile_message(username, message):
 | 
			
		||||
    await message.answer(f""" <b>@{username}</b>
 | 
			
		||||
 | 
			
		||||
Баланс  
 | 
			
		||||
⭐️ 0
 | 
			
		||||
 | 
			
		||||
""", parse_mode='html', reply_markup=inline_markup.settings_markup)
 | 
			
		||||
 | 
			
		||||
async def check_profile_message(message, username):
 | 
			
		||||
    await message.answer(f'С возвращением, {username}!', reply_markup=reply_markup.base_buttons_markup)
 | 
			
		||||
    
 | 
			
		||||
async def settings_message(message):
 | 
			
		||||
    await message.edit_text("Выберите что настроить", reply_markup=inline_markup.special_settings_markup)
 | 
			
		||||
@@ -1,369 +0,0 @@
 | 
			
		||||
from aiogram import Router
 | 
			
		||||
import logging.config
 | 
			
		||||
import app.telegram.Keyboards.inline_keyboards as inline_markup
 | 
			
		||||
 | 
			
		||||
from pybit.unified_trading import HTTP
 | 
			
		||||
import app.telegram.database.requests as rq
 | 
			
		||||
from aiogram.types import Message, CallbackQuery
 | 
			
		||||
 | 
			
		||||
from app.services.Bybit.functions.price_symbol import get_price
 | 
			
		||||
from app.services.Bybit.functions.Futures import safe_float, calculate_total_budget, get_bybit_client
 | 
			
		||||
from app.states.States import update_main_settings
 | 
			
		||||
from logger_helper.logger_helper import LOGGING_CONFIG
 | 
			
		||||
 | 
			
		||||
logging.config.dictConfig(LOGGING_CONFIG)
 | 
			
		||||
logger = logging.getLogger("main_settings")
 | 
			
		||||
 | 
			
		||||
router_main_settings = Router()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def reg_new_user_default_main_settings(id, message):
 | 
			
		||||
    tg_id = id
 | 
			
		||||
 | 
			
		||||
    trading_mode = await rq.get_for_registration_trading_mode()
 | 
			
		||||
    margin_type = await rq.get_for_registration_margin_type()
 | 
			
		||||
 | 
			
		||||
    await rq.set_new_user_default_main_settings(tg_id, trading_mode, margin_type)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def main_settings_message(id, message):
 | 
			
		||||
    try:
 | 
			
		||||
        data = await rq.get_user_main_settings(id)
 | 
			
		||||
        tg_id = id
 | 
			
		||||
 | 
			
		||||
        data_main_stgs = await rq.get_user_main_settings(id)
 | 
			
		||||
        data_risk_stgs = await rq.get_user_risk_management_settings(id)
 | 
			
		||||
        client = await get_bybit_client(tg_id)
 | 
			
		||||
        symbol = await rq.get_symbol(tg_id)
 | 
			
		||||
        max_martingale_steps = (data_main_stgs or {}).get('maximal_quantity', 0)
 | 
			
		||||
        commission_fee = (data_risk_stgs or {}).get('commission_fee')
 | 
			
		||||
        starting_quantity = safe_float((data_main_stgs or {}).get('starting_quantity'))
 | 
			
		||||
        martingale_factor = safe_float((data_main_stgs or {}).get('martingale_factor'))
 | 
			
		||||
        fee_info = client.get_fee_rates(category='linear', symbol=symbol)
 | 
			
		||||
        leverage = safe_float((data_main_stgs or {}).get('size_leverage'))
 | 
			
		||||
        price = await get_price(tg_id, symbol=symbol)
 | 
			
		||||
        entry_price = safe_float(price)
 | 
			
		||||
 | 
			
		||||
        if commission_fee == "Да":
 | 
			
		||||
            commission_fee_percent = safe_float(fee_info['result']['list'][0]['takerFeeRate'])
 | 
			
		||||
        else:
 | 
			
		||||
            commission_fee_percent = 0.0
 | 
			
		||||
 | 
			
		||||
        total_budget = await calculate_total_budget(
 | 
			
		||||
            starting_quantity=starting_quantity,
 | 
			
		||||
            martingale_factor=martingale_factor,
 | 
			
		||||
            max_steps=max_martingale_steps,
 | 
			
		||||
            commission_fee_percent=commission_fee_percent,
 | 
			
		||||
            leverage=leverage,
 | 
			
		||||
            current_price=entry_price,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        await message.answer(f"""<b>Основные настройки</b>
 | 
			
		||||
         
 | 
			
		||||
    <b>- Режим торговли:</b> {data['trading_mode']}
 | 
			
		||||
    <b>- Состояние свитча:</b> {data['switch_state']}
 | 
			
		||||
    <b>- Направление последней сделки:</b> {data['last_side']}
 | 
			
		||||
    <b>- Тип маржи:</b> {data['margin_type']}
 | 
			
		||||
    <b>- Размер кредитного плеча:</b> х{data['size_leverage']}
 | 
			
		||||
    <b>- Начальная ставка:</b> {data['starting_quantity']}
 | 
			
		||||
    <b>- Коэффициент мартингейла:</b> {data['martingale_factor']}
 | 
			
		||||
    <b>- Текущий шаг:</b> {data['martingale_step']}
 | 
			
		||||
    <b>- Максимальное количество ставок в серии:</b> {data['maximal_quantity']}   
 | 
			
		||||
    
 | 
			
		||||
    <b>- Требуемый бюджет:</b> {total_budget:.2f} USDT
 | 
			
		||||
    """, parse_mode='html', reply_markup=inline_markup.main_settings_markup)
 | 
			
		||||
    except PermissionError as e:
 | 
			
		||||
        logger.error("Authenticated endpoints require keys: %s", e)
 | 
			
		||||
        await message.answer("Вы не авторизованы.", reply_markup=inline_markup.connect_bybit_api_message)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def trading_mode_message(message, state):
 | 
			
		||||
    await state.set_state(update_main_settings.trading_mode)
 | 
			
		||||
 | 
			
		||||
    await message.edit_text("""<b>Режим торговли</b>
 | 
			
		||||
 | 
			
		||||
<b>Лонг</b> — стратегия, ориентированная на покупку актива с целью заработать на повышении его стоимости.
 | 
			
		||||
 | 
			
		||||
<b>Шорт</b> — метод продажи активов, взятых в кредит, чтобы получить прибыль от снижения цены.
 | 
			
		||||
 | 
			
		||||
<b>Свитч</b> — динамическое переключение между торговыми режимами для максимизации эффективности.
 | 
			
		||||
    
 | 
			
		||||
<em>Выберите ниже для изменений:</em>    
 | 
			
		||||
""", parse_mode='html', reply_markup=inline_markup.trading_mode_markup)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_main_settings.callback_query(update_main_settings.trading_mode)
 | 
			
		||||
async def state_trading_mode(callback: CallbackQuery, state):
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
    id = callback.from_user.id
 | 
			
		||||
    data_settings = await rq.get_user_main_settings(id)
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        match callback.data:
 | 
			
		||||
            case 'trade_mode_long':
 | 
			
		||||
                await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Long")
 | 
			
		||||
                await rq.update_trade_mode_user(id, 'Long')
 | 
			
		||||
                await main_settings_message(id, callback.message)
 | 
			
		||||
 | 
			
		||||
                await state.clear()
 | 
			
		||||
            case 'trade_mode_short':
 | 
			
		||||
                await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Short")
 | 
			
		||||
                await rq.update_trade_mode_user(id, 'Short')
 | 
			
		||||
                await main_settings_message(id, callback.message)
 | 
			
		||||
 | 
			
		||||
                await state.clear()
 | 
			
		||||
 | 
			
		||||
            case 'trade_mode_switch':
 | 
			
		||||
                await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Switch")
 | 
			
		||||
                await rq.update_trade_mode_user(id, 'Switch')
 | 
			
		||||
                await main_settings_message(id, callback.message)
 | 
			
		||||
 | 
			
		||||
                await state.clear()
 | 
			
		||||
 | 
			
		||||
            case 'trade_mode_smart':
 | 
			
		||||
                await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Smart")
 | 
			
		||||
                await rq.update_trade_mode_user(id, 'Smart')
 | 
			
		||||
                await main_settings_message(id, callback.message)
 | 
			
		||||
 | 
			
		||||
                await state.clear()
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error(e)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def switch_mode_enabled_message(message, state):
 | 
			
		||||
    await state.set_state(update_main_settings.switch_mode_enabled)
 | 
			
		||||
 | 
			
		||||
    await message.edit_text(
 | 
			
		||||
        f"""<b> Состояние свитча</b>
 | 
			
		||||
        
 | 
			
		||||
        <b>По направлению</b> - по направлению последней сделки предыдущей серии
 | 
			
		||||
        <b>Против направления</b> - против направления последней сделки предыдущей серии
 | 
			
		||||
        
 | 
			
		||||
        <em>По умолчанию при первом запуске бота, направление сделки установлено на "Buy".</em>
 | 
			
		||||
        <em>Выберите ниже для изменений:</em>""", parse_mode='html',
 | 
			
		||||
        reply_markup=inline_markup.switch_state_markup)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_main_settings.callback_query(lambda c: c.data in ["clb_long_switch", "clb_short_switch"])
 | 
			
		||||
async def state_switch_mode_enabled(callback: CallbackQuery, state):
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
    tg_id = callback.from_user.id
 | 
			
		||||
    val = "По направлению" if callback.data == "clb_long_switch" else "Против направления"
 | 
			
		||||
    if val == "По направлению":
 | 
			
		||||
        await rq.update_switch_state(tg_id, "По направлению")
 | 
			
		||||
        await callback.message.answer(f"Состояние свитча: {val}")
 | 
			
		||||
        await main_settings_message(tg_id, callback.message)
 | 
			
		||||
    else:
 | 
			
		||||
        await rq.update_switch_state(tg_id, "Против направления")
 | 
			
		||||
        await callback.message.answer(f"Состояние свитча: {val}")
 | 
			
		||||
        await main_settings_message(tg_id, callback.message)
 | 
			
		||||
    await state.clear()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def size_leverage_message(message, state):
 | 
			
		||||
    await state.set_state(update_main_settings.size_leverage)
 | 
			
		||||
 | 
			
		||||
    await message.edit_text("Введите размер <b>кредитного плеча</b> (от 1 до 100): ", parse_mode='html',
 | 
			
		||||
                            reply_markup=inline_markup.back_btn_list_settings_markup)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_main_settings.message(update_main_settings.size_leverage)
 | 
			
		||||
async def state_size_leverage(message: Message, state):
 | 
			
		||||
    try:
 | 
			
		||||
        leverage = float(message.text)
 | 
			
		||||
        if leverage <= 0:
 | 
			
		||||
            raise ValueError("Неверное значение")
 | 
			
		||||
    except ValueError:
 | 
			
		||||
        await message.answer(
 | 
			
		||||
            "Ошибка: пожалуйста, введите положительное число для кредитного плеча."
 | 
			
		||||
            "\nПопробуйте снова."
 | 
			
		||||
        )
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    await state.update_data(size_leverage=message.text)
 | 
			
		||||
 | 
			
		||||
    data = await state.get_data()
 | 
			
		||||
    tg_id = message.from_user.id
 | 
			
		||||
    symbol = await rq.get_symbol(tg_id)
 | 
			
		||||
    leverage = data['size_leverage']
 | 
			
		||||
    client = await get_bybit_client(tg_id)
 | 
			
		||||
 | 
			
		||||
    instruments_resp = client.get_instruments_info(category="linear", symbol=symbol)
 | 
			
		||||
    info = instruments_resp.get("result", {}).get("list", [])
 | 
			
		||||
 | 
			
		||||
    max_leverage = safe_float(info[0].get("leverageFilter", {}).get("maxLeverage", 0))
 | 
			
		||||
 | 
			
		||||
    if safe_float(leverage) > max_leverage:
 | 
			
		||||
        await message.answer(
 | 
			
		||||
            f"Запрошенное кредитное плечо {leverage} превышает максимальное {max_leverage} для {symbol}. "
 | 
			
		||||
            f"Устанавливаю максимальное.",
 | 
			
		||||
            reply_markup=inline_markup.back_to_main,
 | 
			
		||||
        )
 | 
			
		||||
        logger.info(
 | 
			
		||||
            f"Запрошенное кредитное плечо {leverage} превышает максимальное {max_leverage} для {symbol}. Устанавливаю максимальное.")
 | 
			
		||||
 | 
			
		||||
        await rq.update_size_leverange(message.from_user.id, max_leverage)
 | 
			
		||||
        await main_settings_message(message.from_user.id, message)
 | 
			
		||||
        await state.clear()
 | 
			
		||||
    else:
 | 
			
		||||
        await message.answer(f"✅ Изменено: {leverage}")
 | 
			
		||||
        await rq.update_size_leverange(message.from_user.id, safe_float(leverage))
 | 
			
		||||
        await main_settings_message(message.from_user.id, message)
 | 
			
		||||
        await state.clear()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def martingale_factor_message(message, state):
 | 
			
		||||
    await state.set_state(update_main_settings.martingale_factor)
 | 
			
		||||
 | 
			
		||||
    await message.edit_text("Введите <b>коэффициент Мартингейла:</b>", parse_mode='html',
 | 
			
		||||
                            reply_markup=inline_markup.back_btn_list_settings_markup)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_main_settings.message(update_main_settings.martingale_factor)
 | 
			
		||||
async def state_martingale_factor(message: Message, state):
 | 
			
		||||
    await state.update_data(martingale_factor=message.text)
 | 
			
		||||
 | 
			
		||||
    data = await state.get_data()
 | 
			
		||||
    data_settings = await rq.get_user_main_settings(message.from_user.id)
 | 
			
		||||
 | 
			
		||||
    if data['martingale_factor'].isdigit() and int(data['martingale_factor']) <= 100:
 | 
			
		||||
        await message.answer(f"✅ Изменено: {data_settings['martingale_factor']} → {data['martingale_factor']}")
 | 
			
		||||
 | 
			
		||||
        await rq.update_martingale_factor(message.from_user.id, data['martingale_factor'])
 | 
			
		||||
        await main_settings_message(message.from_user.id, message)
 | 
			
		||||
 | 
			
		||||
        await state.clear()
 | 
			
		||||
    else:
 | 
			
		||||
        await message.answer(
 | 
			
		||||
            f'⛔️ Ошибка: ваше значение ({data['martingale_factor']}) или выше лимита (100) или вы вводите неверные символы')
 | 
			
		||||
 | 
			
		||||
        await main_settings_message(message.from_user.id, message)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def margin_type_message(message, state):
 | 
			
		||||
    await state.set_state(update_main_settings.margin_type)
 | 
			
		||||
 | 
			
		||||
    await message.edit_text("""<b>Тип маржи</b>
 | 
			
		||||
 | 
			
		||||
<b>Изолированная маржа</b>  
 | 
			
		||||
Этот тип маржи позволяет ограничить риск конкретной позиции. 
 | 
			
		||||
При использовании изолированной маржи вы выделяете определённую сумму средств только для одной позиции. 
 | 
			
		||||
Если позиция начинает приносить убытки, ваши потери ограничиваются этой суммой, 
 | 
			
		||||
и остальные средства на счёте не затрагиваются.
 | 
			
		||||
 | 
			
		||||
<b>Кросс-маржа</b>  
 | 
			
		||||
Кросс-маржа объединяет весь маржинальный баланс на счёте и использует все доступные средства для поддержания открытых позиций. 
 | 
			
		||||
В случае убытков средства с других позиций или баланса автоматически покрывают дефицит, 
 | 
			
		||||
снижая риск ликвидации, но увеличивая общий риск потери капитала.
 | 
			
		||||
 | 
			
		||||
<em>Выберите ниже для изменений:</em>
 | 
			
		||||
""", parse_mode='html', reply_markup=inline_markup.margin_type_markup)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_main_settings.callback_query(update_main_settings.margin_type)
 | 
			
		||||
async def state_margin_type(callback: CallbackQuery, state):
 | 
			
		||||
    callback_data = callback.data
 | 
			
		||||
    if callback_data in ['margin_type_isolated', 'margin_type_cross']:
 | 
			
		||||
        tg_id = callback.from_user.id
 | 
			
		||||
        api_key = await rq.get_bybit_api_key(tg_id)
 | 
			
		||||
        secret_key = await rq.get_bybit_secret_key(tg_id)
 | 
			
		||||
        data_settings = await rq.get_user_main_settings(tg_id)
 | 
			
		||||
        symbol = await rq.get_symbol(tg_id)
 | 
			
		||||
        client = HTTP(api_key=api_key, api_secret=secret_key)
 | 
			
		||||
        try:
 | 
			
		||||
            active_positions = client.get_positions(category='linear', settleCoin="USDT")
 | 
			
		||||
 | 
			
		||||
            positions = active_positions.get('result', {}).get('list', [])
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(f"error: {e}")
 | 
			
		||||
            positions = []
 | 
			
		||||
 | 
			
		||||
        for pos in positions:
 | 
			
		||||
            size = pos.get('size')
 | 
			
		||||
            if float(size) > 0:
 | 
			
		||||
                await callback.answer(
 | 
			
		||||
                    "⚠️ Маржинальный режим нельзя менять при открытой позиции"
 | 
			
		||||
                )
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            match callback.data:
 | 
			
		||||
                case 'margin_type_isolated':
 | 
			
		||||
                    await callback.answer()
 | 
			
		||||
                    await callback.message.answer(f"✅ Изменено: {data_settings['margin_type']} → Isolated")
 | 
			
		||||
 | 
			
		||||
                    await rq.update_margin_type(tg_id, 'Isolated')
 | 
			
		||||
                    await main_settings_message(tg_id, callback.message)
 | 
			
		||||
 | 
			
		||||
                    await state.clear()
 | 
			
		||||
                case 'margin_type_cross':
 | 
			
		||||
                    await callback.answer()
 | 
			
		||||
                    await callback.message.answer(f"✅ Изменено: {data_settings['margin_type']} → Cross")
 | 
			
		||||
 | 
			
		||||
                    await rq.update_margin_type(tg_id, 'Cross')
 | 
			
		||||
                    await main_settings_message(tg_id, callback.message)
 | 
			
		||||
 | 
			
		||||
                    await state.clear()
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(f"error: {e}")
 | 
			
		||||
    else:
 | 
			
		||||
        await callback.answer()
 | 
			
		||||
        await main_settings_message(callback.from_user.id, callback.message)
 | 
			
		||||
 | 
			
		||||
        await state.clear()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def starting_quantity_message(message, state):
 | 
			
		||||
    await state.set_state(update_main_settings.starting_quantity)
 | 
			
		||||
 | 
			
		||||
    await message.edit_text("Введите <b>начальную ставку:</b>", parse_mode='html',
 | 
			
		||||
                            reply_markup=inline_markup.back_btn_list_settings_markup)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_main_settings.message(update_main_settings.starting_quantity)
 | 
			
		||||
async def state_starting_quantity(message: Message, state):
 | 
			
		||||
    await state.update_data(starting_quantity=message.text)
 | 
			
		||||
 | 
			
		||||
    data = await state.get_data()
 | 
			
		||||
    data_settings = await rq.get_user_main_settings(message.from_user.id)
 | 
			
		||||
 | 
			
		||||
    if data['starting_quantity'].isdigit():
 | 
			
		||||
        await message.answer(f"✅ Изменено: {data_settings['starting_quantity']} → {data['starting_quantity']}")
 | 
			
		||||
 | 
			
		||||
        await rq.update_starting_quantity(message.from_user.id, data['starting_quantity'])
 | 
			
		||||
        await main_settings_message(message.from_user.id, message)
 | 
			
		||||
 | 
			
		||||
        await state.clear()
 | 
			
		||||
    else:
 | 
			
		||||
        await message.answer(f'⛔️ Ошибка: вы вводите неверные символы')
 | 
			
		||||
 | 
			
		||||
        await main_settings_message(message.from_user.id, message)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def maximum_quantity_message(message, state):
 | 
			
		||||
    await state.set_state(update_main_settings.maximal_quantity)
 | 
			
		||||
 | 
			
		||||
    await message.edit_text("Введите <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()
 | 
			
		||||
    data_settings = await rq.get_user_main_settings(message.from_user.id)
 | 
			
		||||
 | 
			
		||||
    if data['maximal_quantity'].isdigit() and int(data['maximal_quantity']) <= 100:
 | 
			
		||||
        await message.answer(f"✅ Изменено: {data_settings['maximal_quantity']} → {data['maximal_quantity']}")
 | 
			
		||||
 | 
			
		||||
        await rq.update_maximal_quantity(message.from_user.id, data['maximal_quantity'])
 | 
			
		||||
        await main_settings_message(message.from_user.id, message)
 | 
			
		||||
 | 
			
		||||
        await state.clear()
 | 
			
		||||
    else:
 | 
			
		||||
        await message.answer(
 | 
			
		||||
            f'⛔️ Ошибка: ваше значение ({data['maximal_quantity']}) или выше лимита (100) или вы вводите неверные символы')
 | 
			
		||||
 | 
			
		||||
        await main_settings_message(message.from_user.id, message)
 | 
			
		||||
							
								
								
									
										27
									
								
								app/telegram/functions/profile_tg.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								app/telegram/functions/profile_tg.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
import logging.config
 | 
			
		||||
 | 
			
		||||
from aiogram.types import Message
 | 
			
		||||
 | 
			
		||||
import app.telegram.keyboards.reply as kbr
 | 
			
		||||
import database.request as rq
 | 
			
		||||
from logger_helper.logger_helper import LOGGING_CONFIG
 | 
			
		||||
 | 
			
		||||
logging.config.dictConfig(LOGGING_CONFIG)
 | 
			
		||||
logger = logging.getLogger("profile_tg")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def user_profile_tg(tg_id: int, message: Message) -> None:
 | 
			
		||||
    try:
 | 
			
		||||
        user = await rq.get_user(tg_id)
 | 
			
		||||
        if user:
 | 
			
		||||
            await message.answer(
 | 
			
		||||
                text="💎Ваш профиль:\n\n" "⚖️ Баланс: 0\n", reply_markup=kbr.profile
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            await rq.create_user(tg_id=tg_id, username=user.username)
 | 
			
		||||
            await rq.set_user_symbol(tg_id=tg_id, symbol="BTCUSDT")
 | 
			
		||||
            await rq.create_user_additional_settings(tg_id=tg_id)
 | 
			
		||||
            await rq.create_user_risk_management(tg_id=tg_id)
 | 
			
		||||
            await user_profile_tg(tg_id=tg_id, message=message)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error("Error processing user profile: %s", e)
 | 
			
		||||
@@ -1,157 +0,0 @@
 | 
			
		||||
from aiogram import Router
 | 
			
		||||
import app.telegram.Keyboards.inline_keyboards as inline_markup
 | 
			
		||||
import logging.config
 | 
			
		||||
import app.telegram.database.requests as rq
 | 
			
		||||
from aiogram.types import Message, CallbackQuery
 | 
			
		||||
 | 
			
		||||
from app.states.States import update_risk_management_settings
 | 
			
		||||
 | 
			
		||||
from logger_helper.logger_helper import LOGGING_CONFIG
 | 
			
		||||
 | 
			
		||||
logging.config.dictConfig(LOGGING_CONFIG)
 | 
			
		||||
logger = logging.getLogger("risk_management_settings")
 | 
			
		||||
 | 
			
		||||
router_risk_management_settings = Router()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def reg_new_user_default_risk_management_settings(id, message):
 | 
			
		||||
    tg_id = id
 | 
			
		||||
 | 
			
		||||
    await rq.set_new_user_default_risk_management_settings(tg_id)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def main_settings_message(id, message):
 | 
			
		||||
    data = await rq.get_user_risk_management_settings(id)
 | 
			
		||||
 | 
			
		||||
    text = f"""<b>Риск менеджмент</b>,
 | 
			
		||||
 | 
			
		||||
    <b>- Процент изменения цены для фиксации прибыли:</b> {data.get('price_profit', 0)}%
 | 
			
		||||
    <b>- Процент изменения цены для фиксации убытков:</b> {data.get('price_loss', 0)}%
 | 
			
		||||
    <b>- Максимальный риск на сделку (в % от баланса):</b> {data.get('max_risk_deal', 0)}%
 | 
			
		||||
    <b>- Комиссия биржи для расчета прибыли:</b> {data.get('commission_fee', "Да")}
 | 
			
		||||
    """
 | 
			
		||||
    await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.risk_management_settings_markup)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def price_profit_message(message, state):
 | 
			
		||||
    await state.set_state(update_risk_management_settings.price_profit)
 | 
			
		||||
 | 
			
		||||
    text = 'Введите число изменения цены для фиксации прибыли: '
 | 
			
		||||
 | 
			
		||||
    await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.cancel)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_risk_management_settings.message(update_risk_management_settings.price_profit)
 | 
			
		||||
async def state_price_profit(message: Message, state):
 | 
			
		||||
    await state.update_data(price_profit=message.text)
 | 
			
		||||
 | 
			
		||||
    data = await state.get_data()
 | 
			
		||||
    data_settings = await rq.get_user_risk_management_settings(message.from_user.id)
 | 
			
		||||
 | 
			
		||||
    if data['price_profit'].isdigit() and int(data['price_profit']) <= 100:
 | 
			
		||||
        await message.answer(f"✅ Изменено: {data_settings['price_profit']}% → {data['price_profit']}%")
 | 
			
		||||
 | 
			
		||||
        await rq.update_price_profit(message.from_user.id, data['price_profit'])
 | 
			
		||||
        await main_settings_message(message.from_user.id, message)
 | 
			
		||||
 | 
			
		||||
        await state.clear()
 | 
			
		||||
    else:
 | 
			
		||||
        await message.answer(
 | 
			
		||||
            f'⛔️ Ошибка: ваше значение ({data['price_profit']}%) или выше лимита (100) или вы вводите неверные символы')
 | 
			
		||||
 | 
			
		||||
        await main_settings_message(message.from_user.id, message)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def price_loss_message(message, state):
 | 
			
		||||
    await state.set_state(update_risk_management_settings.price_loss)
 | 
			
		||||
 | 
			
		||||
    text = 'Введите число изменения цены для фиксации убытков: '
 | 
			
		||||
 | 
			
		||||
    await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.cancel)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_risk_management_settings.message(update_risk_management_settings.price_loss)
 | 
			
		||||
async def state_price_loss(message: Message, state):
 | 
			
		||||
    await state.update_data(price_loss=message.text)
 | 
			
		||||
 | 
			
		||||
    data = await state.get_data()
 | 
			
		||||
    data_settings = await rq.get_user_risk_management_settings(message.from_user.id)
 | 
			
		||||
 | 
			
		||||
    if data['price_loss'].isdigit() and int(data['price_loss']) <= 100:
 | 
			
		||||
        new_price_loss = int(data['price_loss'])
 | 
			
		||||
        old_price_loss = int(data_settings.get('price_loss', 0))
 | 
			
		||||
 | 
			
		||||
        current_price_profit = data_settings.get('price_profit')
 | 
			
		||||
        # Пробуем перевести price_profit в число, если это возможно
 | 
			
		||||
        try:
 | 
			
		||||
            current_price_profit_num = int(current_price_profit)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error(e)
 | 
			
		||||
            current_price_profit_num = 0
 | 
			
		||||
 | 
			
		||||
        # Флаг, если price_profit изначально равен 0 или совпадает со старым стоп-лоссом
 | 
			
		||||
        should_update_profit = (current_price_profit_num == 0) or (current_price_profit_num == abs(old_price_loss))
 | 
			
		||||
 | 
			
		||||
        # Обновляем стоп-лосс
 | 
			
		||||
        await rq.update_price_loss(message.from_user.id, new_price_loss)
 | 
			
		||||
 | 
			
		||||
        # Если нужно, меняем тейк-профит
 | 
			
		||||
        if should_update_profit:
 | 
			
		||||
            new_price_profit = abs(new_price_loss)
 | 
			
		||||
            await rq.update_price_profit(message.from_user.id, new_price_profit)
 | 
			
		||||
            await message.answer(f"✅ Стоп-лосс изменён: {old_price_loss}% → {new_price_loss}%\n"
 | 
			
		||||
                                 f"Тейк-профит автоматически установлен в: {new_price_profit}%")
 | 
			
		||||
        else:
 | 
			
		||||
            await message.answer(f"✅ Стоп-лосс изменён: {old_price_loss}% → {new_price_loss}%")
 | 
			
		||||
 | 
			
		||||
        await main_settings_message(message.from_user.id, message)
 | 
			
		||||
        await state.clear()
 | 
			
		||||
    else:
 | 
			
		||||
        await message.answer(
 | 
			
		||||
            f'⛔️ Ошибка: ваше значение ({data["price_loss"]}%) выше лимита (100) или содержит неверные символы')
 | 
			
		||||
        await main_settings_message(message.from_user.id, message)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def max_risk_deal_message(message, state):
 | 
			
		||||
    await state.set_state(update_risk_management_settings.max_risk_deal)
 | 
			
		||||
 | 
			
		||||
    text = 'Введите число (процент от баланса) для изменения максимального риска на сделку: '
 | 
			
		||||
 | 
			
		||||
    await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.cancel)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_risk_management_settings.message(update_risk_management_settings.max_risk_deal)
 | 
			
		||||
async def state_max_risk_deal(message: Message, state):
 | 
			
		||||
    await state.update_data(max_risk_deal=message.text)
 | 
			
		||||
 | 
			
		||||
    data = await state.get_data()
 | 
			
		||||
    data_settings = await rq.get_user_risk_management_settings(message.from_user.id)
 | 
			
		||||
 | 
			
		||||
    if data['max_risk_deal'].isdigit() and int(data['max_risk_deal']) <= 100:
 | 
			
		||||
        await message.answer(f"✅ Изменено: {data_settings['max_risk_deal']}% → {data['max_risk_deal']}%")
 | 
			
		||||
 | 
			
		||||
        await rq.update_max_risk_deal(message.from_user.id, data['max_risk_deal'])
 | 
			
		||||
        await main_settings_message(message.from_user.id, message)
 | 
			
		||||
 | 
			
		||||
        await state.clear()
 | 
			
		||||
    else:
 | 
			
		||||
        await message.answer(
 | 
			
		||||
            f'⛔️ Ошибка: ваше значение ({data['max_risk_deal']}%) или выше лимита (100) или вы вводите неверные символы')
 | 
			
		||||
 | 
			
		||||
        await main_settings_message(message.from_user.id, message)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def commission_fee_message(message, state):
 | 
			
		||||
    await state.set_state(update_risk_management_settings.commission_fee)
 | 
			
		||||
    await message.answer(text="Хотите учитывать комиссию биржи:", parse_mode='html',
 | 
			
		||||
                         reply_markup=inline_markup.buttons_yes_no_markup)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_risk_management_settings.callback_query(lambda c: c.data in ["clb_yes", "clb_no"])
 | 
			
		||||
async def process_commission_fee_callback(callback: CallbackQuery, state):
 | 
			
		||||
    val = "Да" if callback.data == "clb_yes" else "Нет"
 | 
			
		||||
    await rq.update_commission_fee(callback.from_user.id, val)
 | 
			
		||||
    await callback.message.answer(f"✅ Изменено: {val}")
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
    await main_settings_message(callback.from_user.id, callback.message)
 | 
			
		||||
    await state.clear()
 | 
			
		||||
							
								
								
									
										32
									
								
								app/telegram/handlers/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								app/telegram/handlers/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
__all__ = "router"
 | 
			
		||||
 | 
			
		||||
from aiogram import Router
 | 
			
		||||
 | 
			
		||||
from app.telegram.handlers.add_bybit_api import router_add_bybit_api
 | 
			
		||||
from app.telegram.handlers.changing_the_symbol import router_changing_the_symbol
 | 
			
		||||
from app.telegram.handlers.close_orders import router_close_orders
 | 
			
		||||
from app.telegram.handlers.common import router_common
 | 
			
		||||
from app.telegram.handlers.get_positions_handlers import router_get_positions_handlers
 | 
			
		||||
from app.telegram.handlers.handlers_main import router_handlers_main
 | 
			
		||||
from app.telegram.handlers.main_settings import router_main_settings
 | 
			
		||||
from app.telegram.handlers.settings import router_settings
 | 
			
		||||
from app.telegram.handlers.start_trading import router_start_trading
 | 
			
		||||
from app.telegram.handlers.stop_trading import router_stop_trading
 | 
			
		||||
from app.telegram.handlers.tp_sl_handlers import router_tp_sl_handlers
 | 
			
		||||
 | 
			
		||||
router = Router(name=__name__)
 | 
			
		||||
 | 
			
		||||
router.include_router(router_handlers_main)
 | 
			
		||||
router.include_router(router_add_bybit_api)
 | 
			
		||||
router.include_router(router_settings)
 | 
			
		||||
router.include_router(router_main_settings)
 | 
			
		||||
router.include_router(router_changing_the_symbol)
 | 
			
		||||
router.include_router(router_get_positions_handlers)
 | 
			
		||||
router.include_router(router_start_trading)
 | 
			
		||||
router.include_router(router_stop_trading)
 | 
			
		||||
router.include_router(router_close_orders)
 | 
			
		||||
router.include_router(router_tp_sl_handlers)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Do not add anything below this router
 | 
			
		||||
router.include_router(router_common)
 | 
			
		||||
							
								
								
									
										150
									
								
								app/telegram/handlers/add_bybit_api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								app/telegram/handlers/add_bybit_api.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,150 @@
 | 
			
		||||
import logging.config
 | 
			
		||||
 | 
			
		||||
from aiogram import F, Router
 | 
			
		||||
from aiogram.fsm.context import FSMContext
 | 
			
		||||
from aiogram.types import CallbackQuery, Message
 | 
			
		||||
 | 
			
		||||
import app.telegram.keyboards.inline as kbi
 | 
			
		||||
import app.telegram.keyboards.reply as kbr
 | 
			
		||||
import database.request as rq
 | 
			
		||||
from app.bybit.profile_bybit import user_profile_bybit
 | 
			
		||||
from app.telegram.states.states import AddBybitApiState
 | 
			
		||||
from logger_helper.logger_helper import LOGGING_CONFIG
 | 
			
		||||
 | 
			
		||||
logging.config.dictConfig(LOGGING_CONFIG)
 | 
			
		||||
logger = logging.getLogger("add_bybit_api")
 | 
			
		||||
 | 
			
		||||
router_add_bybit_api = Router(name="add_bybit_api")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_add_bybit_api.callback_query(F.data == "connect_platform")
 | 
			
		||||
async def connect_platform(callback: CallbackQuery, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Handles the callback query to initiate Bybit platform connection.
 | 
			
		||||
    Sends instructions on how to create and provide API keys to the bot.
 | 
			
		||||
 | 
			
		||||
    :param callback: CallbackQuery object triggered by user interaction.
 | 
			
		||||
    :param state: FSMContext object to manage state data.
 | 
			
		||||
    :return: None
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        await state.clear()
 | 
			
		||||
        await callback.answer()
 | 
			
		||||
        user = await rq.get_user(tg_id=callback.from_user.id)
 | 
			
		||||
        if user:
 | 
			
		||||
            await callback.message.answer(
 | 
			
		||||
                text=(
 | 
			
		||||
                    "Подключение Bybit аккаунта \n\n"
 | 
			
		||||
                    "1. Зарегистрируйтесь или войдите в свой аккаунт на Bybit по ссылке: "
 | 
			
		||||
                    "[Перейти на Bybit](https://www.bybit.com/invite?ref=YME83OJ).\n"
 | 
			
		||||
                    "2. В личном кабинете выберите раздел API. \n"
 | 
			
		||||
                    "3. Создание нового API ключа\n"
 | 
			
		||||
                    "   - Нажмите кнопку Create New Key (Создать новый ключ).\n"
 | 
			
		||||
                    "   - Выберите системно-сгенерированный ключ.\n"
 | 
			
		||||
                    "   - Укажите название API ключа (любое).  \n"
 | 
			
		||||
                    "   - Выберите права доступа для торговли (Trade).  \n"
 | 
			
		||||
                    "   - Можно ограничить доступ по IP для безопасности.\n"
 | 
			
		||||
                    "4. Подтверждение создания\n"
 | 
			
		||||
                    "   - Подтвердите создание ключа.\n"
 | 
			
		||||
                    "   - Отправьте чат-роботу.\n\n"
 | 
			
		||||
                    "Важно: сохраните отдельно API Key и Secret Key в надежном месте. Secret ключ отображается только один раз."
 | 
			
		||||
                ),
 | 
			
		||||
                parse_mode="Markdown",
 | 
			
		||||
                reply_markup=kbi.add_bybit_api,
 | 
			
		||||
                disable_web_page_preview=True,
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            await rq.create_user(
 | 
			
		||||
                tg_id=callback.from_user.id, username=callback.from_user.username
 | 
			
		||||
            )
 | 
			
		||||
            await rq.set_user_symbol(tg_id=callback.from_user.id, symbol="BTCUSDT")
 | 
			
		||||
            await rq.create_user_additional_settings(tg_id=callback.from_user.id)
 | 
			
		||||
            await rq.create_user_risk_management(tg_id=callback.from_user.id)
 | 
			
		||||
            await rq.create_user_conditional_settings(tg_id=callback.from_user.id)
 | 
			
		||||
            await connect_platform(callback=callback, state=state)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error("Error adding bybit API for user %s: %s", callback.from_user.id, e)
 | 
			
		||||
        await callback.message.answer(
 | 
			
		||||
            text="Произошла ошибка. Пожалуйста, попробуйте позже."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_add_bybit_api.callback_query(F.data == "add_bybit_api")
 | 
			
		||||
async def process_api_key(callback: CallbackQuery, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Starts the FSM flow to add Bybit API keys.
 | 
			
		||||
    Sets the FSM state to prompt user to enter API Key.
 | 
			
		||||
 | 
			
		||||
    :param callback: CallbackQuery object.
 | 
			
		||||
    :param state: FSMContext for managing user state.
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        await state.clear()
 | 
			
		||||
        await state.set_state(AddBybitApiState.api_key_state)
 | 
			
		||||
        await callback.answer()
 | 
			
		||||
        await callback.message.answer(text="Введите API Key:")
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error("Error adding bybit API for user %s: %s", callback.from_user.id, e)
 | 
			
		||||
        await callback.message.answer(
 | 
			
		||||
            text="Произошла ошибка. Пожалуйста, попробуйте позже."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_add_bybit_api.message(AddBybitApiState.api_key_state)
 | 
			
		||||
async def process_secret_key(message: Message, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Receives the API Key input from the user, stores it in FSM context,
 | 
			
		||||
    then sets state to collect Secret Key.
 | 
			
		||||
 | 
			
		||||
    :param message: Message object with user's input.
 | 
			
		||||
    :param state: FSMContext for managing user state.
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        api_key = message.text
 | 
			
		||||
        await state.update_data(api_key=api_key)
 | 
			
		||||
        await state.set_state(AddBybitApiState.api_secret_state)
 | 
			
		||||
        await message.answer(text="Введите Secret Key:")
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error("Error adding bybit API for user %s: %s", message.from_user.id, e)
 | 
			
		||||
        await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_add_bybit_api.message(AddBybitApiState.api_secret_state)
 | 
			
		||||
async def add_bybit_api(message: Message, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Receives the Secret Key input, stores it, saves both API keys in the database,
 | 
			
		||||
    clears FSM state and confirms success to the user.
 | 
			
		||||
 | 
			
		||||
    :param message: Message object with user's input.
 | 
			
		||||
    :param state: FSMContext for managing user state.
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        api_secret = message.text
 | 
			
		||||
        api_key = (await state.get_data()).get("api_key")
 | 
			
		||||
        await state.update_data(api_secret=api_secret)
 | 
			
		||||
 | 
			
		||||
        if not api_key or not api_secret:
 | 
			
		||||
            await message.answer("Введите корректные данные.")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        result = await rq.set_user_api(
 | 
			
		||||
            tg_id=message.from_user.id, api_key=api_key, api_secret=api_secret
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if result:
 | 
			
		||||
            await message.answer(text="Данные добавлены.", reply_markup=kbr.profile)
 | 
			
		||||
            await user_profile_bybit(
 | 
			
		||||
                tg_id=message.from_user.id, message=message, state=state
 | 
			
		||||
            )
 | 
			
		||||
            logger.debug(
 | 
			
		||||
                "Bybit API added successfully for user: %s", message.from_user.id
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.")
 | 
			
		||||
            logger.error(
 | 
			
		||||
                "Error adding bybit API for user %s: %s", message.from_user.id, result
 | 
			
		||||
            )
 | 
			
		||||
        await state.clear()
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error("Error adding bybit API for user %s: %s", message.from_user.id, e)
 | 
			
		||||
        await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.")
 | 
			
		||||
							
								
								
									
										135
									
								
								app/telegram/handlers/changing_the_symbol.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								app/telegram/handlers/changing_the_symbol.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,135 @@
 | 
			
		||||
import logging.config
 | 
			
		||||
 | 
			
		||||
from aiogram import F, Router
 | 
			
		||||
from aiogram.fsm.context import FSMContext
 | 
			
		||||
from aiogram.types import CallbackQuery, Message
 | 
			
		||||
 | 
			
		||||
import app.telegram.keyboards.inline as kbi
 | 
			
		||||
import database.request as rq
 | 
			
		||||
from app.bybit.get_functions.get_tickers import get_tickers
 | 
			
		||||
from app.bybit.get_functions.get_instruments_info import get_instruments_info
 | 
			
		||||
from app.bybit.profile_bybit import user_profile_bybit
 | 
			
		||||
from app.bybit.set_functions.set_leverage import set_leverage
 | 
			
		||||
 | 
			
		||||
from app.bybit.set_functions.set_margin_mode import set_margin_mode
 | 
			
		||||
from app.helper_functions import safe_float
 | 
			
		||||
from app.telegram.states.states import ChangingTheSymbolState
 | 
			
		||||
from logger_helper.logger_helper import LOGGING_CONFIG
 | 
			
		||||
 | 
			
		||||
logging.config.dictConfig(LOGGING_CONFIG)
 | 
			
		||||
logger = logging.getLogger("changing_the_symbol")
 | 
			
		||||
 | 
			
		||||
router_changing_the_symbol = Router(name="changing_the_symbol")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_changing_the_symbol.callback_query(F.data == "change_symbol")
 | 
			
		||||
async def change_symbol(callback_query: CallbackQuery, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Handler for the "change_symbol" command.
 | 
			
		||||
    Sends a message with available symbols to choose from.
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        await state.clear()
 | 
			
		||||
        await state.set_state(ChangingTheSymbolState.symbol_state)
 | 
			
		||||
        msg = await callback_query.message.edit_text(
 | 
			
		||||
            text="Выберите название инструмента без лишних символов (например: BTCUSDT):",
 | 
			
		||||
            reply_markup=kbi.symbol,
 | 
			
		||||
        )
 | 
			
		||||
        await state.update_data(prompt_message_id=msg.message_id)
 | 
			
		||||
        logger.debug(
 | 
			
		||||
            "Command change_symbol processed successfully for user: %s",
 | 
			
		||||
            callback_query.from_user.id,
 | 
			
		||||
        )
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        await callback_query.answer(
 | 
			
		||||
            text="Произошла ошибка. Пожалуйста, попробуйте позже."
 | 
			
		||||
        )
 | 
			
		||||
        logger.error(
 | 
			
		||||
            "Error processing command change_symbol for user %s: %s",
 | 
			
		||||
            callback_query.from_user.id,
 | 
			
		||||
            e,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_changing_the_symbol.message(ChangingTheSymbolState.symbol_state)
 | 
			
		||||
async def set_symbol(message: Message, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Handler for user input for setting the symbol.
 | 
			
		||||
 | 
			
		||||
    Updates FSM context with the selected symbol and persists the choice in database.
 | 
			
		||||
    Sends an acknowledgement to user and clears FSM state afterward.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        message (Message): Incoming message from user containing the selected symbol.
 | 
			
		||||
        state (FSMContext): Finite State Machine context for the current user session.
 | 
			
		||||
 | 
			
		||||
    Logs:
 | 
			
		||||
        Success or error messages with user identification.
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        try:
 | 
			
		||||
            data = await state.get_data()
 | 
			
		||||
            if "prompt_message_id" in data:
 | 
			
		||||
                prompt_message_id = data["prompt_message_id"]
 | 
			
		||||
                await message.bot.delete_message(
 | 
			
		||||
                    chat_id=message.chat.id, message_id=prompt_message_id
 | 
			
		||||
                )
 | 
			
		||||
            await message.delete()
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            if "message to delete not found" in str(e).lower():
 | 
			
		||||
                pass  # Ignore this error
 | 
			
		||||
            else:
 | 
			
		||||
                raise e
 | 
			
		||||
 | 
			
		||||
        symbol = message.text.upper()
 | 
			
		||||
        additional_settings = await rq.get_user_additional_settings(
 | 
			
		||||
            tg_id=message.from_user.id
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if not additional_settings:
 | 
			
		||||
            await rq.create_user_additional_settings(tg_id=message.from_user.id)
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        margin_type = additional_settings.margin_type or "ISOLATED_MARGIN"
 | 
			
		||||
        ticker = await get_tickers(tg_id=message.from_user.id, symbol=symbol)
 | 
			
		||||
 | 
			
		||||
        if ticker is None:
 | 
			
		||||
            await message.answer(
 | 
			
		||||
                text=f"Инструмент {symbol} не найден.", reply_markup=kbi.symbol
 | 
			
		||||
            )
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        instruments_info = await get_instruments_info(tg_id=message.from_user.id, symbol=symbol)
 | 
			
		||||
        max_leverage = instruments_info.get("leverageFilter").get("maxLeverage")
 | 
			
		||||
        req = await rq.set_user_symbol(tg_id=message.from_user.id, symbol=symbol)
 | 
			
		||||
 | 
			
		||||
        if not req:
 | 
			
		||||
            await message.answer(
 | 
			
		||||
                text="Произошла ошибка при установке инструмента.",
 | 
			
		||||
                reply_markup=kbi.symbol,
 | 
			
		||||
            )
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        await user_profile_bybit(
 | 
			
		||||
            tg_id=message.from_user.id, message=message, state=state
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        await set_margin_mode(tg_id=message.from_user.id, margin_mode=margin_type)
 | 
			
		||||
 | 
			
		||||
        await set_leverage(
 | 
			
		||||
            tg_id=message.from_user.id, symbol=symbol, leverage=str(max_leverage)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        await rq.set_leverage(tg_id=message.from_user.id, leverage=str(max_leverage))
 | 
			
		||||
        risk_percent = 10 / safe_float(max_leverage)
 | 
			
		||||
        await rq.set_stop_loss_percent(
 | 
			
		||||
            tg_id=message.from_user.id, stop_loss_percent=risk_percent)
 | 
			
		||||
        await rq.set_take_profit_percent(
 | 
			
		||||
            tg_id=message.from_user.id, take_profit_percent=risk_percent)
 | 
			
		||||
        await rq.set_trigger_price(tg_id=message.from_user.id, trigger_price=0)
 | 
			
		||||
        await rq.set_order_quantity(tg_id=message.from_user.id, order_quantity=1.0)
 | 
			
		||||
 | 
			
		||||
        await state.clear()
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.")
 | 
			
		||||
        logger.error("Error setting symbol for user %s: %s", message.from_user.id, e)
 | 
			
		||||
							
								
								
									
										68
									
								
								app/telegram/handlers/close_orders.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								app/telegram/handlers/close_orders.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,68 @@
 | 
			
		||||
import logging.config
 | 
			
		||||
 | 
			
		||||
from aiogram import Router
 | 
			
		||||
from aiogram.fsm.context import FSMContext
 | 
			
		||||
from aiogram.types import CallbackQuery
 | 
			
		||||
 | 
			
		||||
from logger_helper.logger_helper import LOGGING_CONFIG
 | 
			
		||||
 | 
			
		||||
logging.config.dictConfig(LOGGING_CONFIG)
 | 
			
		||||
logger = logging.getLogger("close_orders")
 | 
			
		||||
 | 
			
		||||
router_close_orders = Router(name="close_orders")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_close_orders.callback_query(
 | 
			
		||||
    lambda c: c.data and c.data.startswith("close_position_")
 | 
			
		||||
)
 | 
			
		||||
async def close_position_handler(
 | 
			
		||||
    callback_query: CallbackQuery, state: FSMContext
 | 
			
		||||
) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Close a position.
 | 
			
		||||
    :param callback_query: Incoming callback query from Telegram inline keyboard.
 | 
			
		||||
    :param state: Finite State Machine context for the current user session.
 | 
			
		||||
    :return: None
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        logger.debug(
 | 
			
		||||
            "Command close_position processed successfully for user: %s",
 | 
			
		||||
            callback_query.from_user.id,
 | 
			
		||||
        )
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        await callback_query.answer(text="Произошла ошибка при закрытии позиции.")
 | 
			
		||||
        logger.error(
 | 
			
		||||
            "Error processing command close_position for user %s: %s",
 | 
			
		||||
            callback_query.from_user.id,
 | 
			
		||||
            e,
 | 
			
		||||
        )
 | 
			
		||||
    finally:
 | 
			
		||||
        await state.clear()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_close_orders.callback_query(
 | 
			
		||||
    lambda c: c.data and c.data.startswith("close_order_")
 | 
			
		||||
)
 | 
			
		||||
async def cancel_order_handler(
 | 
			
		||||
    callback_query: CallbackQuery, state: FSMContext
 | 
			
		||||
) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Cancel an order.
 | 
			
		||||
    :param callback_query: Incoming callback query from Telegram inline keyboard.
 | 
			
		||||
    :param state: Finite State Machine context for the current user session.
 | 
			
		||||
    :return: None
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        logger.debug(
 | 
			
		||||
            "Command close_order processed successfully for user: %s",
 | 
			
		||||
            callback_query.from_user.id,
 | 
			
		||||
        )
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        await callback_query.answer(text="Произошла ошибка при закрытии ордера.")
 | 
			
		||||
        logger.error(
 | 
			
		||||
            "Error processing command close_order for user %s: %s",
 | 
			
		||||
            callback_query.from_user.id,
 | 
			
		||||
            e,
 | 
			
		||||
        )
 | 
			
		||||
    finally:
 | 
			
		||||
        await state.clear()
 | 
			
		||||
							
								
								
									
										50
									
								
								app/telegram/handlers/common.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								app/telegram/handlers/common.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
			
		||||
import logging.config
 | 
			
		||||
 | 
			
		||||
from aiogram import Router
 | 
			
		||||
from aiogram.fsm.context import FSMContext
 | 
			
		||||
from aiogram.types import Message
 | 
			
		||||
 | 
			
		||||
from logger_helper.logger_helper import LOGGING_CONFIG
 | 
			
		||||
 | 
			
		||||
logging.config.dictConfig(LOGGING_CONFIG)
 | 
			
		||||
logger = logging.getLogger("common")
 | 
			
		||||
 | 
			
		||||
router_common = Router(name="common")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_common.message()
 | 
			
		||||
async def unknown_message(message: Message, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Handle unexpected or unrecognized messages.
 | 
			
		||||
    Clears FSM state and informs the user about available commands.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        message (types.Message): Incoming message object.
 | 
			
		||||
        state (FSMContext): Current FSM context.
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        None
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        await message.answer(
 | 
			
		||||
            text="Извините, я вас не понял. "
 | 
			
		||||
            "Пожалуйста, используйте одну из следующих команд:\n"
 | 
			
		||||
            "/start - Запустить бота\n"
 | 
			
		||||
            "/profile - Профиль\n"
 | 
			
		||||
            "/bybit - Панель Bybit\n"
 | 
			
		||||
            "/help - Получить помощь\n"
 | 
			
		||||
        )
 | 
			
		||||
        logger.debug(
 | 
			
		||||
            "Received unknown message from user %s: %s",
 | 
			
		||||
            message.from_user.id,
 | 
			
		||||
            message.text,
 | 
			
		||||
        )
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error(
 | 
			
		||||
            "Error handling unknown message for user %s: %s", message.from_user.id, e
 | 
			
		||||
        )
 | 
			
		||||
        await message.answer(
 | 
			
		||||
            text="Произошла ошибка при обработке вашего сообщения. Пожалуйста, попробуйте позже."
 | 
			
		||||
        )
 | 
			
		||||
    finally:
 | 
			
		||||
        await state.clear()
 | 
			
		||||
							
								
								
									
										314
									
								
								app/telegram/handlers/get_positions_handlers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										314
									
								
								app/telegram/handlers/get_positions_handlers.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,314 @@
 | 
			
		||||
import logging.config
 | 
			
		||||
 | 
			
		||||
from aiogram import F, Router
 | 
			
		||||
from aiogram.fsm.context import FSMContext
 | 
			
		||||
from aiogram.types import CallbackQuery
 | 
			
		||||
 | 
			
		||||
import app.telegram.keyboards.inline as kbi
 | 
			
		||||
from app.bybit.get_functions.get_positions import (
 | 
			
		||||
    get_active_orders,
 | 
			
		||||
    get_active_orders_by_symbol,
 | 
			
		||||
    get_active_positions,
 | 
			
		||||
    get_active_positions_by_symbol,
 | 
			
		||||
)
 | 
			
		||||
from logger_helper.logger_helper import LOGGING_CONFIG
 | 
			
		||||
 | 
			
		||||
logging.config.dictConfig(LOGGING_CONFIG)
 | 
			
		||||
logger = logging.getLogger("get_positions_handlers")
 | 
			
		||||
 | 
			
		||||
router_get_positions_handlers = Router(name="get_positions_handlers")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_get_positions_handlers.callback_query(F.data == "my_deals")
 | 
			
		||||
async def get_positions_handlers(
 | 
			
		||||
    callback_query: CallbackQuery, state: FSMContext
 | 
			
		||||
) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Gets the user's active positions.
 | 
			
		||||
    :param callback_query: CallbackQuery object.
 | 
			
		||||
    :param state: FSMContext
 | 
			
		||||
    :return: None
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        await state.clear()
 | 
			
		||||
        await callback_query.message.edit_text(
 | 
			
		||||
            text="Выберите какие сделки вы хотите посмотреть:",
 | 
			
		||||
            reply_markup=kbi.change_position,
 | 
			
		||||
        )
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error("Error in get_positions_handler: %s", e)
 | 
			
		||||
        await callback_query.answer(text="Произошла ошибка при получении сделок.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_get_positions_handlers.callback_query(F.data == "change_position")
 | 
			
		||||
async def get_positions_handler(
 | 
			
		||||
    callback_query: CallbackQuery, state: FSMContext
 | 
			
		||||
) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Gets the user's active positions.
 | 
			
		||||
    :param callback_query: CallbackQuery object.
 | 
			
		||||
    :param state: FSMContext
 | 
			
		||||
    :return: None
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        res = await get_active_positions(tg_id=callback_query.from_user.id)
 | 
			
		||||
 | 
			
		||||
        if res is None:
 | 
			
		||||
            await callback_query.answer(
 | 
			
		||||
                text="Произошла ошибка при получении активных позиций."
 | 
			
		||||
            )
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if res == ["No active positions found"]:
 | 
			
		||||
            await callback_query.answer(text="Нет активных позиций.")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        active_positions = [pos for pos in res if float(pos.get("size", 0)) > 0]
 | 
			
		||||
 | 
			
		||||
        if not active_positions:
 | 
			
		||||
            await callback_query.answer(text="Нет активных позиций.")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        active_symbols_sides = [
 | 
			
		||||
            (pos.get("symbol"), pos.get("side")) for pos in active_positions
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        await callback_query.message.edit_text(
 | 
			
		||||
            text="Ваши активные позиции:",
 | 
			
		||||
            reply_markup=kbi.create_active_positions_keyboard(
 | 
			
		||||
                symbols=active_symbols_sides
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error("Error in get_positions_handler: %s", e)
 | 
			
		||||
        await callback_query.answer(
 | 
			
		||||
            text="Произошла ошибка при получении активных позиций."
 | 
			
		||||
        )
 | 
			
		||||
    finally:
 | 
			
		||||
        await state.clear()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_get_positions_handlers.callback_query(
 | 
			
		||||
    lambda c: c.data.startswith("get_position_")
 | 
			
		||||
)
 | 
			
		||||
async def get_position_handler(callback_query: CallbackQuery, state: FSMContext):
 | 
			
		||||
    try:
 | 
			
		||||
        data = callback_query.data
 | 
			
		||||
        parts = data.split("_")
 | 
			
		||||
        symbol = parts[2]
 | 
			
		||||
        get_side = parts[3]
 | 
			
		||||
        res = await get_active_positions_by_symbol(
 | 
			
		||||
            tg_id=callback_query.from_user.id, symbol=symbol
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if res is None:
 | 
			
		||||
            await callback_query.answer(
 | 
			
		||||
                text="Произошла ошибка при получении активных позиций."
 | 
			
		||||
            )
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        position = next((pos for pos in res if pos.get("side") == get_side), None)
 | 
			
		||||
 | 
			
		||||
        if position:
 | 
			
		||||
            side = position.get("side")
 | 
			
		||||
            symbol = position.get("symbol") or "Нет данных"
 | 
			
		||||
            avg_price = position.get("avgPrice") or "Нет данных"
 | 
			
		||||
            size = position.get("size") or "Нет данных"
 | 
			
		||||
            take_profit = position.get("takeProfit") or "Нет данных"
 | 
			
		||||
            stop_loss = position.get("stopLoss") or "Нет данных"
 | 
			
		||||
            position_idx = position.get("positionIdx") or "Нет данных"
 | 
			
		||||
            liq_price = position.get("liqPrice") or "Нет данных"
 | 
			
		||||
        else:
 | 
			
		||||
            side = "Нет данных"
 | 
			
		||||
            symbol = "Нет данных"
 | 
			
		||||
            avg_price = "Нет данных"
 | 
			
		||||
            size = "Нет данных"
 | 
			
		||||
            take_profit = "Нет данных"
 | 
			
		||||
            stop_loss = "Нет данных"
 | 
			
		||||
            position_idx = "Нет данных"
 | 
			
		||||
            liq_price = "Нет данных"
 | 
			
		||||
 | 
			
		||||
        side_rus = (
 | 
			
		||||
            "Покупка"
 | 
			
		||||
            if side == "Buy"
 | 
			
		||||
            else "Продажа" if side == "Sell" else "Нет данных"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        position_idx_rus = (
 | 
			
		||||
            "Односторонний"
 | 
			
		||||
            if position_idx == 0
 | 
			
		||||
            else (
 | 
			
		||||
                "Покупка в режиме хеджирования"
 | 
			
		||||
                if position_idx == 1
 | 
			
		||||
                else (
 | 
			
		||||
                    "Продажа в режиме хеджирования"
 | 
			
		||||
                    if position_idx == 2
 | 
			
		||||
                    else "Нет данных"
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        text_lines = [
 | 
			
		||||
            f"Торговая пара: {symbol}",
 | 
			
		||||
            f"Режим позиции: {position_idx_rus}",
 | 
			
		||||
            f"Цена входа: {avg_price}",
 | 
			
		||||
            f"Количество: {size}",
 | 
			
		||||
            f"Движение: {side_rus}",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        if take_profit and take_profit != "Нет данных":
 | 
			
		||||
            text_lines.append(f"Тейк-профит: {take_profit}")
 | 
			
		||||
        if stop_loss and stop_loss != "Нет данных":
 | 
			
		||||
            text_lines.append(f"Стоп-лосс: {stop_loss}")
 | 
			
		||||
        if liq_price and liq_price != "Нет данных":
 | 
			
		||||
            text_lines.append(f"Цена ликвидации: {liq_price}")
 | 
			
		||||
 | 
			
		||||
        text = "\n".join(text_lines)
 | 
			
		||||
 | 
			
		||||
        await callback_query.message.edit_text(
 | 
			
		||||
            text=text,
 | 
			
		||||
            reply_markup=kbi.make_close_position_keyboard(
 | 
			
		||||
                symbol_pos=symbol, side=side, position_idx=position_idx, qty=size
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error("Error in get_position_handler: %s", e)
 | 
			
		||||
        await callback_query.answer(
 | 
			
		||||
            text="Произошла ошибка при получении активных позиций."
 | 
			
		||||
        )
 | 
			
		||||
    finally:
 | 
			
		||||
        await state.clear()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_get_positions_handlers.callback_query(F.data == "open_orders")
 | 
			
		||||
async def get_open_orders_handler(
 | 
			
		||||
    callback_query: CallbackQuery, state: FSMContext
 | 
			
		||||
) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Gets the user's open orders.
 | 
			
		||||
    :param callback_query: CallbackQuery object.
 | 
			
		||||
    :param state: FSMContext
 | 
			
		||||
    :return: None
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        res = await get_active_orders(tg_id=callback_query.from_user.id)
 | 
			
		||||
 | 
			
		||||
        if res is None:
 | 
			
		||||
            await callback_query.answer(
 | 
			
		||||
                text="Произошла ошибка при получении активных ордеров."
 | 
			
		||||
            )
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if res == ["No active orders found"]:
 | 
			
		||||
            await callback_query.answer(text="Нет активных ордеров.")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        active_positions = [pos for pos in res if pos.get("orderStatus", 0) == "New"]
 | 
			
		||||
 | 
			
		||||
        if not active_positions:
 | 
			
		||||
            await callback_query.answer(text="Нет активных ордеров.")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        active_orders_sides = [
 | 
			
		||||
            (pos.get("symbol"), pos.get("side")) for pos in active_positions
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        await callback_query.message.edit_text(
 | 
			
		||||
            text="Ваши активные ордера:",
 | 
			
		||||
            reply_markup=kbi.create_active_orders_keyboard(orders=active_orders_sides),
 | 
			
		||||
        )
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error("Error in get_open_orders_handler: %s", e)
 | 
			
		||||
        await callback_query.answer(
 | 
			
		||||
            text="Произошла ошибка при получении активных ордеров."
 | 
			
		||||
        )
 | 
			
		||||
    finally:
 | 
			
		||||
        await state.clear()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_get_positions_handlers.callback_query(lambda c: c.data.startswith("get_order_"))
 | 
			
		||||
async def get_order_handler(callback_query: CallbackQuery, state: FSMContext):
 | 
			
		||||
    try:
 | 
			
		||||
        data = callback_query.data
 | 
			
		||||
        parts = data.split("_")
 | 
			
		||||
        symbol = parts[2]
 | 
			
		||||
        get_side = parts[3]
 | 
			
		||||
        res = await get_active_orders_by_symbol(
 | 
			
		||||
            tg_id=callback_query.from_user.id, symbol=symbol
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if res is None:
 | 
			
		||||
            await callback_query.answer(
 | 
			
		||||
                text="Произошла ошибка при получении активных ордеров."
 | 
			
		||||
            )
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        orders = next((pos for pos in res if pos.get("side") == get_side), None)
 | 
			
		||||
 | 
			
		||||
        if orders:
 | 
			
		||||
            side = orders.get("side")
 | 
			
		||||
            symbol = orders.get("symbol")
 | 
			
		||||
            price = orders.get("price")
 | 
			
		||||
            qty = orders.get("qty")
 | 
			
		||||
            order_type = orders.get("orderType")
 | 
			
		||||
            trigger_price = orders.get("triggerPrice")
 | 
			
		||||
            take_profit = orders.get("takeProfit")
 | 
			
		||||
            stop_loss = orders.get("stopLoss")
 | 
			
		||||
            order_id = orders.get("orderId")
 | 
			
		||||
        else:
 | 
			
		||||
            side = "Нет данных"
 | 
			
		||||
            symbol = "Нет данных"
 | 
			
		||||
            price = "Нет данных"
 | 
			
		||||
            qty = "Нет данных"
 | 
			
		||||
            order_type = "Нет данных"
 | 
			
		||||
            trigger_price = "Нет данных"
 | 
			
		||||
            take_profit = "Нет данных"
 | 
			
		||||
            stop_loss = "Нет данных"
 | 
			
		||||
            order_id = "Нет данных"
 | 
			
		||||
 | 
			
		||||
        side_rus = (
 | 
			
		||||
            "Покупка"
 | 
			
		||||
            if side == "Buy"
 | 
			
		||||
            else "Продажа" if side == "Sell" else "Нет данных"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        order_type_rus = (
 | 
			
		||||
            "Рыночный"
 | 
			
		||||
            if order_type == "Market"
 | 
			
		||||
            else "Лимитный" if order_type == "Limit" else "Нет данных"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        text_lines = [
 | 
			
		||||
            f"Торговая пара: {symbol}",
 | 
			
		||||
            f"Количество: {qty}",
 | 
			
		||||
            f"Движение: {side_rus}",
 | 
			
		||||
            f"Тип ордера: {order_type_rus}",
 | 
			
		||||
        ]
 | 
			
		||||
        if price:
 | 
			
		||||
            text_lines.append(f"Цена: {price}")
 | 
			
		||||
 | 
			
		||||
        if trigger_price and trigger_price != "Нет данных":
 | 
			
		||||
            text_lines.append(f"Триггер цена: {trigger_price}")
 | 
			
		||||
 | 
			
		||||
        if take_profit and take_profit != "Нет данных":
 | 
			
		||||
            text_lines.append(f"Тейк-профит: {take_profit}")
 | 
			
		||||
 | 
			
		||||
        if stop_loss and stop_loss != "Нет данных":
 | 
			
		||||
            text_lines.append(f"Стоп-лосс: {stop_loss}")
 | 
			
		||||
 | 
			
		||||
        text = "\n".join(text_lines)
 | 
			
		||||
 | 
			
		||||
        await callback_query.message.edit_text(
 | 
			
		||||
            text=text,
 | 
			
		||||
            reply_markup=kbi.make_close_orders_keyboard(
 | 
			
		||||
                symbol_order=symbol, order_id=order_id
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error("Error in get_order_handler: %s", e)
 | 
			
		||||
        await callback_query.answer(
 | 
			
		||||
            text="Произошла ошибка при получении активных ордеров."
 | 
			
		||||
        )
 | 
			
		||||
    finally:
 | 
			
		||||
        await state.clear()
 | 
			
		||||
@@ -1,316 +0,0 @@
 | 
			
		||||
import logging.config
 | 
			
		||||
 | 
			
		||||
from aiogram import F, Router
 | 
			
		||||
from aiogram.filters import CommandStart, Command
 | 
			
		||||
from aiogram.types import Message, CallbackQuery
 | 
			
		||||
from aiogram.fsm.context import FSMContext
 | 
			
		||||
 | 
			
		||||
import app.telegram.functions.functions as func
 | 
			
		||||
import app.telegram.functions.main_settings.settings as func_main_settings
 | 
			
		||||
import app.telegram.functions.risk_management_settings.settings as func_rmanagement_settings
 | 
			
		||||
import app.telegram.functions.condition_settings.settings as func_condition_settings
 | 
			
		||||
import app.telegram.functions.additional_settings.settings as func_additional_settings
 | 
			
		||||
 | 
			
		||||
import app.telegram.database.requests as rq
 | 
			
		||||
 | 
			
		||||
from app.services.Bybit.functions.balance import get_balance
 | 
			
		||||
from app.services.Bybit.functions.bybit_ws import run_ws_for_user
 | 
			
		||||
 | 
			
		||||
from logger_helper.logger_helper import LOGGING_CONFIG
 | 
			
		||||
 | 
			
		||||
logging.config.dictConfig(LOGGING_CONFIG)
 | 
			
		||||
logger = logging.getLogger("handlers")
 | 
			
		||||
 | 
			
		||||
router = Router()
 | 
			
		||||
 | 
			
		||||
@router.message(Command("start"))
 | 
			
		||||
@router.message(CommandStart())
 | 
			
		||||
async def start_message(message: Message) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Обработчик команды /start.
 | 
			
		||||
    Инициализирует нового пользователя в БД.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        message (Message): Входящее сообщение с командой /start.
 | 
			
		||||
    """
 | 
			
		||||
    await rq.set_new_user_bybit_api(message.from_user.id)
 | 
			
		||||
    await func.start_message(message)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router.message(Command("profile"))
 | 
			
		||||
@router.message(F.text == "👤 Профиль")
 | 
			
		||||
async def profile_message(message: Message) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Обработчик кнопки 'Профиль'.
 | 
			
		||||
    Проверяет существование пользователя и отображает профиль.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        message (Message): Сообщение с текстом кнопки.
 | 
			
		||||
    """
 | 
			
		||||
    user = await rq.check_user(message.from_user.id)
 | 
			
		||||
    tg_id = message.from_user.id
 | 
			
		||||
    balance = await get_balance(message.from_user.id, message)
 | 
			
		||||
    if user and balance:
 | 
			
		||||
        await run_ws_for_user(tg_id, message)
 | 
			
		||||
        await func.profile_message(message.from_user.username, message)
 | 
			
		||||
    else:
 | 
			
		||||
        await rq.save_tg_id_new_user(message.from_user.id)
 | 
			
		||||
        await func_main_settings.reg_new_user_default_main_settings(message.from_user.id, message)
 | 
			
		||||
        await func_rmanagement_settings.reg_new_user_default_risk_management_settings(message.from_user.id, message)
 | 
			
		||||
        await func_condition_settings.reg_new_user_default_condition_settings(message.from_user.id)
 | 
			
		||||
        await func_additional_settings.reg_new_user_default_additional_settings(message.from_user.id, message)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router.callback_query(F.data == "clb_start_chatbot_message")
 | 
			
		||||
async def clb_profile_msg(callback: CallbackQuery) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Обработчик колбэка 'clb_start_chatbot_message'.
 | 
			
		||||
    Если пользователь есть в БД — показывает профиль,
 | 
			
		||||
    иначе регистрирует нового пользователя и инициализирует настройки.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        callback (CallbackQuery): Полученный колбэк.
 | 
			
		||||
    """
 | 
			
		||||
    tg_id = callback.from_user.id
 | 
			
		||||
    message = callback.message
 | 
			
		||||
    user = await rq.check_user(callback.from_user.id)
 | 
			
		||||
    balance = await get_balance(callback.from_user.id, callback.message)
 | 
			
		||||
    first_name = callback.from_user.first_name or ""
 | 
			
		||||
    last_name = callback.from_user.last_name or ""
 | 
			
		||||
    username = f"{first_name} {last_name}".strip() or callback.from_user.username or "Пользователь"
 | 
			
		||||
 | 
			
		||||
    if user and balance:
 | 
			
		||||
        await run_ws_for_user(tg_id, message)
 | 
			
		||||
        await func.profile_message(callback.from_user.username, callback.message)
 | 
			
		||||
    else:
 | 
			
		||||
        await rq.save_tg_id_new_user(callback.from_user.id)
 | 
			
		||||
 | 
			
		||||
        await func_main_settings.reg_new_user_default_main_settings(callback.from_user.id, callback.message)
 | 
			
		||||
        await func_rmanagement_settings.reg_new_user_default_risk_management_settings(callback.from_user.id,
 | 
			
		||||
                                                                                      callback.message)
 | 
			
		||||
        await func_condition_settings.reg_new_user_default_condition_settings(callback.from_user.id)
 | 
			
		||||
        await func_additional_settings.reg_new_user_default_additional_settings(callback.from_user.id, callback.message)
 | 
			
		||||
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router.callback_query(F.data == "clb_settings_message")
 | 
			
		||||
async def clb_settings_msg(callback: CallbackQuery) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Показать главное меню настроек.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        callback (CallbackQuery): полученный колбэк.
 | 
			
		||||
    """
 | 
			
		||||
    await func.settings_message(callback.message)
 | 
			
		||||
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router.callback_query(F.data == "clb_back_to_special_settings_message")
 | 
			
		||||
async def clb_back_to_settings_msg(callback: CallbackQuery) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Вернуть пользователя к меню специальных настроек.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        callback (CallbackQuery): полученный колбэк.
 | 
			
		||||
    """
 | 
			
		||||
    await func.settings_message(callback.message)
 | 
			
		||||
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router.callback_query(F.data == "clb_change_main_settings")
 | 
			
		||||
async def clb_change_main_settings_message(callback: CallbackQuery) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Открыть меню изменения главных настроек.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        callback (CallbackQuery): полученный колбэк.
 | 
			
		||||
    """
 | 
			
		||||
    await func_main_settings.main_settings_message(callback.from_user.id, callback.message)
 | 
			
		||||
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router.callback_query(F.data == "clb_change_risk_management_settings")
 | 
			
		||||
async def clb_change_risk_management_message(callback: CallbackQuery) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Открыть меню изменения настроек управления рисками.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        callback (CallbackQuery): полученный колбэк.
 | 
			
		||||
    """
 | 
			
		||||
    await func_rmanagement_settings.main_settings_message(callback.from_user.id, callback.message)
 | 
			
		||||
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router.callback_query(F.data == "clb_change_condition_settings")
 | 
			
		||||
async def clb_change_condition_message(callback: CallbackQuery) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Открыть меню изменения настроек условий.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        callback (CallbackQuery): полученный колбэк.
 | 
			
		||||
    """
 | 
			
		||||
    await func_condition_settings.main_settings_message(callback.from_user.id, callback.message)
 | 
			
		||||
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router.callback_query(F.data == "clb_change_additional_settings")
 | 
			
		||||
async def clb_change_additional_message(callback: CallbackQuery) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Открыть меню изменения дополнительных настроек.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        callback (CallbackQuery): полученный колбэк.
 | 
			
		||||
    """
 | 
			
		||||
    await func_additional_settings.main_settings_message(callback.from_user.id, callback.message)
 | 
			
		||||
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Конкретные настройки каталогов
 | 
			
		||||
list_main_settings = ['clb_change_trading_mode',
 | 
			
		||||
                      'clb_change_switch_state',
 | 
			
		||||
                      'clb_change_margin_type',
 | 
			
		||||
                      'clb_change_size_leverage',
 | 
			
		||||
                      'clb_change_starting_quantity',
 | 
			
		||||
                      'clb_change_martingale_factor',
 | 
			
		||||
                      'clb_change_maximum_quantity'
 | 
			
		||||
                      ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router.callback_query(F.data.in_(list_main_settings))
 | 
			
		||||
async def clb_main_settings_msg(callback: CallbackQuery, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Обработчик колбэков изменения главных настроек с dispatch через match-case.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        callback (CallbackQuery): полученный колбэк.
 | 
			
		||||
        state (FSMContext): текущее состояние FSM.
 | 
			
		||||
    """
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        match callback.data:
 | 
			
		||||
            case 'clb_change_trading_mode':
 | 
			
		||||
                await func_main_settings.trading_mode_message(callback.message, state)
 | 
			
		||||
            case 'clb_change_switch_state':
 | 
			
		||||
                await func_main_settings.switch_mode_enabled_message(callback.message, state)
 | 
			
		||||
            case 'clb_change_margin_type':
 | 
			
		||||
                await func_main_settings.margin_type_message(callback.message, state)
 | 
			
		||||
            case 'clb_change_size_leverage':
 | 
			
		||||
                await func_main_settings.size_leverage_message(callback.message, state)
 | 
			
		||||
            case 'clb_change_starting_quantity':
 | 
			
		||||
                await func_main_settings.starting_quantity_message(callback.message, state)
 | 
			
		||||
            case 'clb_change_martingale_factor':
 | 
			
		||||
                await func_main_settings.martingale_factor_message(callback.message, state)
 | 
			
		||||
            case 'clb_change_maximum_quantity':
 | 
			
		||||
                await func_main_settings.maximum_quantity_message(callback.message, state)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error(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',
 | 
			
		||||
                                 'commission_fee',
 | 
			
		||||
                                 ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router.callback_query(F.data.in_(list_risk_management_settings))
 | 
			
		||||
async def clb_risk_management_settings_msg(callback: CallbackQuery, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Обработчик изменений настроек управления рисками.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        callback (CallbackQuery): полученный колбэк.
 | 
			
		||||
        state (FSMContext): текущее состояние FSM.
 | 
			
		||||
    """
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        match callback.data:
 | 
			
		||||
            case 'clb_change_price_profit':
 | 
			
		||||
                await func_rmanagement_settings.price_profit_message(callback.message, state)
 | 
			
		||||
            case 'clb_change_price_loss':
 | 
			
		||||
                await func_rmanagement_settings.price_loss_message(callback.message, state)
 | 
			
		||||
            case 'clb_change_max_risk_deal':
 | 
			
		||||
                await func_rmanagement_settings.max_risk_deal_message(callback.message, state)
 | 
			
		||||
            case 'commission_fee':
 | 
			
		||||
                await func_rmanagement_settings.commission_fee_message(callback.message, state)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error(f"Error callback in risk_management match-case: {e}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
list_condition_settings = ['clb_change_mode',
 | 
			
		||||
                           'clb_change_timer',
 | 
			
		||||
                           'clb_change_filter_volatility',
 | 
			
		||||
                           'clb_change_external_cues',
 | 
			
		||||
                           'clb_change_tradingview_cues',
 | 
			
		||||
                           'clb_change_webhook',
 | 
			
		||||
                           'clb_change_ai_analytics'
 | 
			
		||||
                           ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router.callback_query(F.data.in_(list_condition_settings))
 | 
			
		||||
async def clb_condition_settings_msg(callback: CallbackQuery, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Обработчик изменений настроек условий трейдинга.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        callback (CallbackQuery): полученный колбэк.
 | 
			
		||||
        state (FSMContext): текущее состояние FSM.
 | 
			
		||||
    """
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        match callback.data:
 | 
			
		||||
            case 'clb_change_mode':
 | 
			
		||||
                await func_condition_settings.trigger_message(callback.from_user.id, callback.message, state)
 | 
			
		||||
            case 'clb_change_timer':
 | 
			
		||||
                await func_condition_settings.timer_message(callback.from_user.id, callback.message, state)
 | 
			
		||||
            case 'clb_change_filter_volatility':
 | 
			
		||||
                await func_condition_settings.filter_volatility_message(callback.message, state)
 | 
			
		||||
            case 'clb_change_external_cues':
 | 
			
		||||
                await func_condition_settings.external_cues_message(callback.message, state)
 | 
			
		||||
            case 'clb_change_tradingview_cues':
 | 
			
		||||
                await func_condition_settings.trading_cues_message(callback.message, state)
 | 
			
		||||
            case 'clb_change_webhook':
 | 
			
		||||
                await func_condition_settings.webhook_message(callback.message, state)
 | 
			
		||||
            case 'clb_change_ai_analytics':
 | 
			
		||||
                await func_condition_settings.ai_analytics_message(callback.message, state)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error(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) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Обработчик дополнительных настроек бота.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        callback (CallbackQuery): полученный колбэк.
 | 
			
		||||
        state (FSMContext): текущее состояние FSM.
 | 
			
		||||
    """
 | 
			
		||||
    await callback.answer()
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        match callback.data:
 | 
			
		||||
            case 'clb_change_save_pattern':
 | 
			
		||||
                await func_additional_settings.save_pattern_message(callback.message, state)
 | 
			
		||||
            case 'clb_change_auto_start':
 | 
			
		||||
                await func_additional_settings.auto_start_message(callback.message, state)
 | 
			
		||||
            case 'clb_change_notifications':
 | 
			
		||||
                await func_additional_settings.notifications_message(callback.message, state)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error(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,
 | 
			
		||||
        )
 | 
			
		||||
							
								
								
									
										423
									
								
								app/telegram/handlers/main_settings/risk_management.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										423
									
								
								app/telegram/handlers/main_settings/risk_management.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,423 @@
 | 
			
		||||
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()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_risk_management.callback_query(F.data == "compensation_commission")
 | 
			
		||||
async def compensation_commission(callback_query: CallbackQuery, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Handles the 'compensation_commission' callback query.
 | 
			
		||||
 | 
			
		||||
    Clears the current FSM state, edits the message text to display the compensation commission 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()
 | 
			
		||||
        msg = await callback_query.message.edit_text(
 | 
			
		||||
            text="Выберите за счет чего будет происходить компенсация комиссии: ",
 | 
			
		||||
            reply_markup=kbi.commission_place,
 | 
			
		||||
        )
 | 
			
		||||
        await state.update_data(prompt_message_id=msg.message_id)
 | 
			
		||||
        logger.debug(
 | 
			
		||||
            "Command compensation_commission processed successfully for user: %s",
 | 
			
		||||
            callback_query.from_user.id,
 | 
			
		||||
        )
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        await callback_query.answer(
 | 
			
		||||
            text="Произошла ошибка. Пожалуйста, попробуйте позже."
 | 
			
		||||
        )
 | 
			
		||||
        logger.error(
 | 
			
		||||
            "Error processing command compensation_commission for user %s: %s",
 | 
			
		||||
            callback_query.from_user.id,
 | 
			
		||||
            e,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@router_risk_management.callback_query(
 | 
			
		||||
    lambda c: c.data in ["Commission_for_qty", "Commission_for_tp"]
 | 
			
		||||
)
 | 
			
		||||
async def set_compensation_commission(callback_query: CallbackQuery, state: FSMContext) -> None:
 | 
			
		||||
    """
 | 
			
		||||
    Handles user input for setting the compensation commission.
 | 
			
		||||
 | 
			
		||||
    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_place(
 | 
			
		||||
            tg_id=callback_query.from_user.id, commission_place=callback_query.data
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if not req:
 | 
			
		||||
            await callback_query.answer(
 | 
			
		||||
                text="Произошла ошибка при установке компенсации комиссии. Пожалуйста, попробуйте позже."
 | 
			
		||||
            )
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if callback_query.data == "Commission_for_qty":
 | 
			
		||||
            await callback_query.answer(text="Комиссия компенсируется по ставке.")
 | 
			
		||||
        else:
 | 
			
		||||
            await callback_query.answer(text="Комиссия компенсируется по тейк-профиту.")
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error(
 | 
			
		||||
            "Error processing command compensation_commission for user %s: %s",
 | 
			
		||||
            callback_query.from_user.id,
 | 
			
		||||
            e,
 | 
			
		||||
        )
 | 
			
		||||
    finally:
 | 
			
		||||
        await state.clear()
 | 
			
		||||
							
								
								
									
										203
									
								
								app/telegram/handlers/settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								app/telegram/handlers/settings.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,203 @@
 | 
			
		||||
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 "Нет"
 | 
			
		||||
            )
 | 
			
		||||
            commission_place = risk_management_data.commission_place
 | 
			
		||||
            commission_place_rus = (
 | 
			
		||||
                "Ставке" if commission_place == "Commission_for_qty" 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"
 | 
			
		||||
                     f"- Компенсация комиссии по: {commission_place_rus}",
 | 
			
		||||
                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,
 | 
			
		||||
        )
 | 
			
		||||
							
								
								
									
										170
									
								
								app/telegram/handlers/start_trading.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								app/telegram/handlers/start_trading.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,170 @@
 | 
			
		||||
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": "️️Лимит ставки меньше минимально допустимого",
 | 
			
		||||
                "Order placement failed as your position may exceed the max":
 | 
			
		||||
                    "Не удалось разместить ордер, так как ваша позиция может превышать максимальный лимит."
 | 
			
		||||
                    "Пожалуйста, уменьшите кредитное плечо, чтобы увеличить максимальное значение"
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            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,
 | 
			
		||||
        )
 | 
			
		||||
							
								
								
									
										84
									
								
								app/telegram/handlers/stop_trading.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								app/telegram/handlers/stop_trading.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,84 @@
 | 
			
		||||
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)
 | 
			
		||||
 | 
			
		||||
            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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        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)
 | 
			
		||||
							
								
								
									
										413
									
								
								app/telegram/keyboards/inline.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										413
									
								
								app/telegram/keyboards/inline.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,413 @@
 | 
			
		||||
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="compensation_commission")],
 | 
			
		||||
        [
 | 
			
		||||
            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"),
 | 
			
		||||
        ],
 | 
			
		||||
    ]
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
commission_place = InlineKeyboardMarkup(
 | 
			
		||||
    inline_keyboard=[
 | 
			
		||||
        [
 | 
			
		||||
            InlineKeyboardButton(text="По ставке", callback_data="Commission_for_qty"),
 | 
			
		||||
            InlineKeyboardButton(text="По тейк-профиту", callback_data="Commission_for_tp"),
 | 
			
		||||
        ],
 | 
			
		||||
        [
 | 
			
		||||
            InlineKeyboardButton(text="Назад", callback_data="risk_management"),
 | 
			
		||||
            InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
 | 
			
		||||
        ],
 | 
			
		||||
    ]
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
# conditions
 | 
			
		||||
conditions = InlineKeyboardMarkup(
 | 
			
		||||
    inline_keyboard=[
 | 
			
		||||
        [
 | 
			
		||||
            InlineKeyboardButton(text="Таймер для старта", callback_data="start_timer"),
 | 
			
		||||
        ],
 | 
			
		||||
        [
 | 
			
		||||
            InlineKeyboardButton(text="Назад", callback_data="main_settings"),
 | 
			
		||||
            InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
 | 
			
		||||
        ],
 | 
			
		||||
    ]
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
back_to_conditions = InlineKeyboardMarkup(
 | 
			
		||||
    inline_keyboard=[
 | 
			
		||||
        [
 | 
			
		||||
            InlineKeyboardButton(text="Назад", callback_data="conditional_settings"),
 | 
			
		||||
            InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
 | 
			
		||||
        ],
 | 
			
		||||
    ]
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
# SYMBOL
 | 
			
		||||
symbol = InlineKeyboardMarkup(
 | 
			
		||||
    inline_keyboard=[
 | 
			
		||||
        [
 | 
			
		||||
            InlineKeyboardButton(text="Назад", callback_data="profile_bybit"),
 | 
			
		||||
        ],
 | 
			
		||||
    ]
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
# POSITION
 | 
			
		||||
 | 
			
		||||
change_position = InlineKeyboardMarkup(
 | 
			
		||||
    inline_keyboard=[
 | 
			
		||||
        [
 | 
			
		||||
            InlineKeyboardButton(text="Позиции", callback_data="change_position"),
 | 
			
		||||
            InlineKeyboardButton(text="Открытые ордера", callback_data="open_orders"),
 | 
			
		||||
        ],
 | 
			
		||||
        [InlineKeyboardButton(text="Назад", callback_data="profile_bybit")],
 | 
			
		||||
    ]
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_active_positions_keyboard(symbols: list):
 | 
			
		||||
    builder = InlineKeyboardBuilder()
 | 
			
		||||
    for sym, side in symbols:
 | 
			
		||||
        builder.button(text=f"{sym}:{side}", callback_data=f"get_position_{sym}_{side}")
 | 
			
		||||
    builder.button(text="Назад", callback_data="my_deals")
 | 
			
		||||
    builder.button(text="На главную", callback_data="profile_bybit")
 | 
			
		||||
    builder.adjust(2)
 | 
			
		||||
    return builder.as_markup()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def make_close_position_keyboard(
 | 
			
		||||
    symbol_pos: str, side: str, position_idx: int, qty: int
 | 
			
		||||
):
 | 
			
		||||
    return InlineKeyboardMarkup(
 | 
			
		||||
        inline_keyboard=[
 | 
			
		||||
            [
 | 
			
		||||
                InlineKeyboardButton(
 | 
			
		||||
                    text="Закрыть позицию",
 | 
			
		||||
                    callback_data=f"close_position_{symbol_pos}_{side}_{position_idx}_{qty}",
 | 
			
		||||
                )
 | 
			
		||||
            ],
 | 
			
		||||
            [
 | 
			
		||||
                InlineKeyboardButton(
 | 
			
		||||
                    text="Установить TP/SL",
 | 
			
		||||
                    callback_data=f"pos_tp_sl_{symbol_pos}_{position_idx}",
 | 
			
		||||
                )
 | 
			
		||||
            ],
 | 
			
		||||
            [
 | 
			
		||||
                InlineKeyboardButton(text="Назад", callback_data="change_position"),
 | 
			
		||||
                InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
 | 
			
		||||
            ],
 | 
			
		||||
        ]
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_active_orders_keyboard(orders: list):
 | 
			
		||||
    builder = InlineKeyboardBuilder()
 | 
			
		||||
    for order, side in orders:
 | 
			
		||||
        builder.button(text=f"{order}", callback_data=f"get_order_{order}_{side}")
 | 
			
		||||
    builder.button(text="Назад", callback_data="my_deals")
 | 
			
		||||
    builder.button(text="На главную", callback_data="profile_bybit")
 | 
			
		||||
    builder.adjust(2)
 | 
			
		||||
    return builder.as_markup()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def make_close_orders_keyboard(symbol_order: str, order_id: str):
 | 
			
		||||
    return InlineKeyboardMarkup(
 | 
			
		||||
        inline_keyboard=[
 | 
			
		||||
            [
 | 
			
		||||
                InlineKeyboardButton(
 | 
			
		||||
                    text="Закрыть ордер",
 | 
			
		||||
                    callback_data=f"close_order_{symbol_order}_{order_id}",
 | 
			
		||||
                )
 | 
			
		||||
            ],
 | 
			
		||||
            [
 | 
			
		||||
                InlineKeyboardButton(text="Назад", callback_data="open_orders"),
 | 
			
		||||
                InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
 | 
			
		||||
            ],
 | 
			
		||||
        ]
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# START TRADING
 | 
			
		||||
 | 
			
		||||
back_to_start_trading = InlineKeyboardMarkup(
 | 
			
		||||
    inline_keyboard=[
 | 
			
		||||
        [
 | 
			
		||||
            InlineKeyboardButton(text="Назад", callback_data="start_trading"),
 | 
			
		||||
            InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
 | 
			
		||||
        ],
 | 
			
		||||
    ]
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
cancel_timer_merged = InlineKeyboardMarkup(
 | 
			
		||||
    inline_keyboard=[
 | 
			
		||||
        [
 | 
			
		||||
            InlineKeyboardButton(
 | 
			
		||||
                text="Отменить таймер", callback_data="cancel_timer_merged"
 | 
			
		||||
            )
 | 
			
		||||
        ],
 | 
			
		||||
        [
 | 
			
		||||
            InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
 | 
			
		||||
        ],
 | 
			
		||||
    ]
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
cancel_timer_switch = InlineKeyboardMarkup(
 | 
			
		||||
    inline_keyboard=[
 | 
			
		||||
        [
 | 
			
		||||
            InlineKeyboardButton(
 | 
			
		||||
                text="Отменить таймер", callback_data="cancel_timer_switch"
 | 
			
		||||
            )
 | 
			
		||||
        ],
 | 
			
		||||
        [
 | 
			
		||||
            InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
 | 
			
		||||
        ],
 | 
			
		||||
    ]
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
# STOP TRADING
 | 
			
		||||
 | 
			
		||||
cancel_timer_stop = InlineKeyboardMarkup(
 | 
			
		||||
    inline_keyboard=[
 | 
			
		||||
        [
 | 
			
		||||
            InlineKeyboardButton(
 | 
			
		||||
                text="Отменить таймер", callback_data="cancel_timer_stop"
 | 
			
		||||
            )
 | 
			
		||||
        ],
 | 
			
		||||
        [
 | 
			
		||||
            InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
 | 
			
		||||
        ],
 | 
			
		||||
    ]
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										11
									
								
								app/telegram/keyboards/reply.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								app/telegram/keyboards/reply.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
from aiogram.types import KeyboardButton, ReplyKeyboardMarkup
 | 
			
		||||
 | 
			
		||||
profile = ReplyKeyboardMarkup(
 | 
			
		||||
    keyboard=[
 | 
			
		||||
        [KeyboardButton(text="Панель Bybit"), KeyboardButton(text="Профиль")],
 | 
			
		||||
        [KeyboardButton(text="Подключить платформу Bybit")],
 | 
			
		||||
    ],
 | 
			
		||||
    resize_keyboard=True,
 | 
			
		||||
    one_time_keyboard=True,
 | 
			
		||||
    input_field_placeholder="Выберите пункт меню...",
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										0
									
								
								app/telegram/states/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/telegram/states/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										51
									
								
								app/telegram/states/states.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								app/telegram/states/states.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
from aiogram.fsm.state import State, StatesGroup
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AddBybitApiState(StatesGroup):
 | 
			
		||||
    """States for adding Bybit API keys."""
 | 
			
		||||
 | 
			
		||||
    api_key_state = State()
 | 
			
		||||
    api_secret_state = State()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AdditionalSettingsState(StatesGroup):
 | 
			
		||||
    """States for additional settings."""
 | 
			
		||||
 | 
			
		||||
    leverage_state = State()
 | 
			
		||||
    leverage_to_buy_state = State()
 | 
			
		||||
    leverage_to_sell_state = State()
 | 
			
		||||
    quantity_state = State()
 | 
			
		||||
    martingale_factor_state = State()
 | 
			
		||||
    max_bets_in_series_state = State()
 | 
			
		||||
    limit_price_state = State()
 | 
			
		||||
    trigger_price_state = State()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RiskManagementState(StatesGroup):
 | 
			
		||||
    """States for risk management."""
 | 
			
		||||
 | 
			
		||||
    take_profit_percent_state = State()
 | 
			
		||||
    stop_loss_percent_state = State()
 | 
			
		||||
    max_risk_percent_state = State()
 | 
			
		||||
    commission_fee_state = State()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ConditionalSettingsState(StatesGroup):
 | 
			
		||||
    """States for conditional settings."""
 | 
			
		||||
 | 
			
		||||
    start_timer_state = State()
 | 
			
		||||
    stop_timer_state = State()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ChangingTheSymbolState(StatesGroup):
 | 
			
		||||
    """States for changing the symbol."""
 | 
			
		||||
 | 
			
		||||
    symbol_state = State()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SetTradingStopState(StatesGroup):
 | 
			
		||||
    """States for setting a trading stop."""
 | 
			
		||||
 | 
			
		||||
    symbol_state = State()
 | 
			
		||||
    take_profit_state = State()
 | 
			
		||||
    stop_loss_state = State()
 | 
			
		||||
							
								
								
									
										0
									
								
								app/telegram/tasks/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/telegram/tasks/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										77
									
								
								app/telegram/tasks/tasks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								app/telegram/tasks/tasks.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,77 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import logging.config
 | 
			
		||||
 | 
			
		||||
from logger_helper.logger_helper import LOGGING_CONFIG
 | 
			
		||||
 | 
			
		||||
logging.config.dictConfig(LOGGING_CONFIG)
 | 
			
		||||
logger = logging.getLogger("tasks")
 | 
			
		||||
 | 
			
		||||
user_start_tasks_merged = {}
 | 
			
		||||
user_start_tasks_switch = {}
 | 
			
		||||
user_stop_tasks = {}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def add_start_task_merged(user_id: int, task: asyncio.Task):
 | 
			
		||||
    """Add task to user_start_tasks dict"""
 | 
			
		||||
    if user_id in user_start_tasks_merged:
 | 
			
		||||
        old_task = user_start_tasks_merged[user_id]
 | 
			
		||||
        if not old_task.done():
 | 
			
		||||
            old_task.cancel()
 | 
			
		||||
            try:
 | 
			
		||||
                await old_task
 | 
			
		||||
            except asyncio.CancelledError:
 | 
			
		||||
                pass
 | 
			
		||||
    user_start_tasks_merged[user_id] = task
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def add_start_task_switch(user_id: int, task: asyncio.Task):
 | 
			
		||||
    """Add task to user_start_tasks dict"""
 | 
			
		||||
    if user_id in user_start_tasks_switch:
 | 
			
		||||
        old_task = user_start_tasks_switch[user_id]
 | 
			
		||||
        if not old_task.done():
 | 
			
		||||
            old_task.cancel()
 | 
			
		||||
            try:
 | 
			
		||||
                await old_task
 | 
			
		||||
            except asyncio.CancelledError:
 | 
			
		||||
                pass
 | 
			
		||||
    user_start_tasks_switch[user_id] = task
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def add_stop_task(user_id: int, task: asyncio.Task):
 | 
			
		||||
    """Add task to user_stop_tasks dict"""
 | 
			
		||||
    if user_id in user_stop_tasks:
 | 
			
		||||
        old_task = user_stop_tasks[user_id]
 | 
			
		||||
        if not old_task.done():
 | 
			
		||||
            old_task.cancel()
 | 
			
		||||
            try:
 | 
			
		||||
                await old_task
 | 
			
		||||
            except asyncio.CancelledError:
 | 
			
		||||
                pass
 | 
			
		||||
    user_stop_tasks[user_id] = task
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def cancel_start_task_merged(user_id: int):
 | 
			
		||||
    """Cancel task from user_start_tasks dict"""
 | 
			
		||||
    if user_id in user_start_tasks_merged:
 | 
			
		||||
        task = user_start_tasks_merged[user_id]
 | 
			
		||||
        if not task.done():
 | 
			
		||||
            task.cancel()
 | 
			
		||||
        del user_start_tasks_merged[user_id]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def cancel_start_task_switch(user_id: int):
 | 
			
		||||
    """Cancel task from user_start_tasks dict"""
 | 
			
		||||
    if user_id in user_start_tasks_switch:
 | 
			
		||||
        task = user_start_tasks_switch[user_id]
 | 
			
		||||
        if not task.done():
 | 
			
		||||
            task.cancel()
 | 
			
		||||
        del user_start_tasks_switch[user_id]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def cancel_stop_task(user_id: int):
 | 
			
		||||
    """Cancel task from user_stop_tasks dict"""
 | 
			
		||||
    if user_id in user_stop_tasks:
 | 
			
		||||
        task = user_stop_tasks[user_id]
 | 
			
		||||
        if not task.done():
 | 
			
		||||
            task.cancel()
 | 
			
		||||
        del user_stop_tasks[user_id]
 | 
			
		||||
							
								
								
									
										11
									
								
								config.py
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								config.py
									
									
									
									
									
								
							@@ -1,9 +1,8 @@
 | 
			
		||||
from dotenv import load_dotenv, find_dotenv
 | 
			
		||||
import os
 | 
			
		||||
from dotenv import load_dotenv, find_dotenv
 | 
			
		||||
 | 
			
		||||
env_file = find_dotenv()
 | 
			
		||||
load_dotenv(env_file)
 | 
			
		||||
env_path = find_dotenv()
 | 
			
		||||
if env_path:
 | 
			
		||||
    load_dotenv(env_path)
 | 
			
		||||
 | 
			
		||||
TOKEN_TG_BOT_1 = os.getenv('TOKEN_TELEGRAM_BOT_1')
 | 
			
		||||
TOKEN_TG_BOT_2 = os.getenv('TOKEN_TELEGRAM_BOT_2')
 | 
			
		||||
TOKEN_TG_BOT_3 = os.getenv('TOKEN_TELEGRAM_BOT_3')
 | 
			
		||||
BOT_TOKEN = os.getenv("BOT_TOKEN")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										45
									
								
								database/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								database/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
from database.models import Base, User, UserAdditionalSettings, UserApi, UserConditionalSettings, UserDeals, \
 | 
			
		||||
    UserRiskManagement, UserSymbol
 | 
			
		||||
import logging.config
 | 
			
		||||
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
 | 
			
		||||
from sqlalchemy import event
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from logger_helper.logger_helper import LOGGING_CONFIG
 | 
			
		||||
 | 
			
		||||
logging.config.dictConfig(LOGGING_CONFIG)
 | 
			
		||||
logger = logging.getLogger("database")
 | 
			
		||||
 | 
			
		||||
BASE_DIR = Path(__file__).parent.resolve()
 | 
			
		||||
DATA_DIR = BASE_DIR / "db"
 | 
			
		||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
 | 
			
		||||
 | 
			
		||||
DATABASE_URL = f"sqlite+aiosqlite:///{DATA_DIR / 'stcs.db'}"
 | 
			
		||||
 | 
			
		||||
async_engine = create_async_engine(
 | 
			
		||||
    DATABASE_URL,
 | 
			
		||||
    echo=False,
 | 
			
		||||
    connect_args={"check_same_thread": False}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@event.listens_for(async_engine.sync_engine, "connect")
 | 
			
		||||
def _enable_foreign_keys(dbapi_connection, connection_record):
 | 
			
		||||
    cursor = dbapi_connection.cursor()
 | 
			
		||||
    cursor.execute("PRAGMA foreign_keys=ON")
 | 
			
		||||
    cursor.close()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async_session = async_sessionmaker(
 | 
			
		||||
    async_engine,
 | 
			
		||||
    class_=AsyncSession,
 | 
			
		||||
    expire_on_commit=False
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def init_db():
 | 
			
		||||
    try:
 | 
			
		||||
        async with async_engine.begin() as conn:
 | 
			
		||||
            await conn.run_sync(Base.metadata.create_all)
 | 
			
		||||
        logger.info("Database initialized.")
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error("Database initialization failed: %s", e)
 | 
			
		||||
							
								
								
									
										187
									
								
								database/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								database/models.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,187 @@
 | 
			
		||||
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")
 | 
			
		||||
    commission_place = Column(String, nullable=False, default="Commission_for_qty")
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
    current_series = Column(Integer, nullable=True)
 | 
			
		||||
    commission_fee = Column(String, nullable=True)
 | 
			
		||||
    commission_place = Column(String, nullable=True)
 | 
			
		||||
    pnl_series = Column(Float, nullable=True)
 | 
			
		||||
    take_profit = Column(Float, nullable=False, default=0.0)
 | 
			
		||||
    stop_loss = Column(Float, nullable=False, default=0.0)
 | 
			
		||||
 | 
			
		||||
    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")
 | 
			
		||||
							
								
								
									
										1415
									
								
								database/request.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1415
									
								
								database/request.py
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								db.sqlite3
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								db.sqlite3
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										17
									
								
								examples/systemd/stcs.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								examples/systemd/stcs.service
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
[Unit]
 | 
			
		||||
Description=Telegram chat-robot: @stcs_cryptobot
 | 
			
		||||
 | 
			
		||||
Wants=network.target
 | 
			
		||||
After=syslog.target network-online.target
 | 
			
		||||
 | 
			
		||||
[Service]
 | 
			
		||||
ExecStart=sudo -u www-data /usr/bin/python3 /var/www/stcs/BybitBot_API.py
 | 
			
		||||
PIDFile=/var/run/python/stcs.pid
 | 
			
		||||
RemainAfterExit=no
 | 
			
		||||
RuntimeMaxSec=3600s
 | 
			
		||||
Restart=always
 | 
			
		||||
RestartSec=5s
 | 
			
		||||
 | 
			
		||||
[Install]
 | 
			
		||||
WantedBy=multi-user.target
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										0
									
								
								logger_helper/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								logger_helper/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -2,15 +2,18 @@ import os
 | 
			
		||||
 | 
			
		||||
current_directory = os.path.dirname(os.path.abspath(__file__))
 | 
			
		||||
log_directory = os.path.join(current_directory, 'loggers')
 | 
			
		||||
error_log_directory = os.path.join(log_directory, 'errors')
 | 
			
		||||
os.makedirs(log_directory, exist_ok=True)
 | 
			
		||||
os.makedirs(error_log_directory, exist_ok=True)
 | 
			
		||||
log_filename = os.path.join(log_directory, 'app.log')
 | 
			
		||||
error_log_filename = os.path.join(error_log_directory, 'error.log')
 | 
			
		||||
 | 
			
		||||
LOGGING_CONFIG = {
 | 
			
		||||
    "version": 1,
 | 
			
		||||
    "disable_existing_loggers": False,
 | 
			
		||||
    "formatters": {
 | 
			
		||||
        "default": {
 | 
			
		||||
            "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
 | 
			
		||||
            "format": "TELEGRAM: %(asctime)s - %(name)s - %(levelname)s - %(message)s",
 | 
			
		||||
            "datefmt": "%Y-%m-%d %H:%M:%S",  # Формат даты
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
@@ -23,90 +26,122 @@ LOGGING_CONFIG = {
 | 
			
		||||
            "backupCount": 7,  # Количество сохраняемых архивов (0 - не сохранять)
 | 
			
		||||
            "formatter": "default",
 | 
			
		||||
            "encoding": "utf-8",
 | 
			
		||||
            "level": "DEBUG",
 | 
			
		||||
        },
 | 
			
		||||
        "error_file": {
 | 
			
		||||
            "class": "logging.handlers.TimedRotatingFileHandler",
 | 
			
		||||
            "filename": error_log_filename,
 | 
			
		||||
            "when": "midnight",
 | 
			
		||||
            "interval": 1,
 | 
			
		||||
            "backupCount": 30,
 | 
			
		||||
            "formatter": "default",
 | 
			
		||||
            "encoding": "utf-8",
 | 
			
		||||
            "level": "ERROR",
 | 
			
		||||
        },
 | 
			
		||||
        "console": {
 | 
			
		||||
            "class": "logging.StreamHandler",
 | 
			
		||||
            "formatter": "default",
 | 
			
		||||
            "level": "DEBUG",
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    "loggers": {
 | 
			
		||||
        "main": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file"],
 | 
			
		||||
        "run": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file", "error_file"],
 | 
			
		||||
            "level": "DEBUG",
 | 
			
		||||
            "propagate": False,
 | 
			
		||||
        },
 | 
			
		||||
        "config": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file", "error_file"],
 | 
			
		||||
            "level": "DEBUG",
 | 
			
		||||
            "propagate": False,
 | 
			
		||||
        },
 | 
			
		||||
        "common": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file", "error_file"],
 | 
			
		||||
            "level": "DEBUG",
 | 
			
		||||
            "propagate": False,
 | 
			
		||||
        },
 | 
			
		||||
        "handlers_main": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file", "error_file"],
 | 
			
		||||
            "level": "DEBUG",
 | 
			
		||||
            "propagate": False,
 | 
			
		||||
        },
 | 
			
		||||
        "database": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file", "error_file"],
 | 
			
		||||
            "level": "DEBUG",
 | 
			
		||||
            "propagate": False,
 | 
			
		||||
        },
 | 
			
		||||
        "request": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file", "error_file"],
 | 
			
		||||
            "level": "DEBUG",
 | 
			
		||||
            "propagate": False,
 | 
			
		||||
        },
 | 
			
		||||
        "add_bybit_api": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file"],
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file", "error_file"],
 | 
			
		||||
            "level": "DEBUG",
 | 
			
		||||
            "propagate": False,
 | 
			
		||||
        },
 | 
			
		||||
        "balance": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file"],
 | 
			
		||||
        "profile_tg": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file", "error_file"],
 | 
			
		||||
            "level": "DEBUG",
 | 
			
		||||
            "propagate": False,
 | 
			
		||||
        },
 | 
			
		||||
        "functions": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file"],
 | 
			
		||||
        "settings": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file", "error_file"],
 | 
			
		||||
            "level": "DEBUG",
 | 
			
		||||
            "propagate": False,
 | 
			
		||||
        },
 | 
			
		||||
        "futures": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file"],
 | 
			
		||||
        "additional_settings": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file", "error_file"],
 | 
			
		||||
            "level": "DEBUG",
 | 
			
		||||
            "propagate": False,
 | 
			
		||||
        },
 | 
			
		||||
        "get_valid_symbol": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file"],
 | 
			
		||||
        "helper_functions": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file", "error_file"],
 | 
			
		||||
            "level": "DEBUG",
 | 
			
		||||
            "propagate": False,
 | 
			
		||||
        },
 | 
			
		||||
        "min_qty": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file"],
 | 
			
		||||
        "risk_management": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file", "error_file"],
 | 
			
		||||
            "level": "DEBUG",
 | 
			
		||||
            "propagate": False,
 | 
			
		||||
        },
 | 
			
		||||
        "price_symbol": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file"],
 | 
			
		||||
        "start_trading": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file", "error_file"],
 | 
			
		||||
            "level": "DEBUG",
 | 
			
		||||
            "propagate": False,
 | 
			
		||||
        },
 | 
			
		||||
        "requests": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file"],
 | 
			
		||||
        "stop_trading": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file", "error_file"],
 | 
			
		||||
            "level": "DEBUG",
 | 
			
		||||
            "propagate": False,
 | 
			
		||||
        },
 | 
			
		||||
        "handlers": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file"],
 | 
			
		||||
        "changing_the_symbol": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file", "error_file"],
 | 
			
		||||
            "level": "DEBUG",
 | 
			
		||||
            "propagate": False,
 | 
			
		||||
        },
 | 
			
		||||
        "condition_settings": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file"],
 | 
			
		||||
        "conditional_settings": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file", "error_file"],
 | 
			
		||||
            "level": "DEBUG",
 | 
			
		||||
            "propagate": False,
 | 
			
		||||
        },
 | 
			
		||||
        "main_settings": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file"],
 | 
			
		||||
        "get_positions_handlers": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file", "error_file"],
 | 
			
		||||
            "level": "DEBUG",
 | 
			
		||||
            "propagate": False,
 | 
			
		||||
        },
 | 
			
		||||
        "risk_management_settings": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file"],
 | 
			
		||||
        "close_orders": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file", "error_file"],
 | 
			
		||||
            "level": "DEBUG",
 | 
			
		||||
            "propagate": False,
 | 
			
		||||
        },
 | 
			
		||||
        "models": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file"],
 | 
			
		||||
            "level": "DEBUG",
 | 
			
		||||
            "propagate": False,
 | 
			
		||||
        },
 | 
			
		||||
        "bybit_ws": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file"],
 | 
			
		||||
        "tp_sl_handlers": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file", "error_file"],
 | 
			
		||||
            "level": "DEBUG",
 | 
			
		||||
            "propagate": False,
 | 
			
		||||
        },
 | 
			
		||||
        "tasks": {
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file"],
 | 
			
		||||
            "handlers": ["console", "timed_rotating_file", "error_file"],
 | 
			
		||||
            "level": "DEBUG",
 | 
			
		||||
            "propagate": False,
 | 
			
		||||
        },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,35 +0,0 @@
 | 
			
		||||
2025-08-23 12:57:26 - main - INFO - Bot is off
 | 
			
		||||
2025-08-23 13:04:01 - main - INFO - Bot is off
 | 
			
		||||
2025-08-23 13:25:04 - main - INFO - Bot is off
 | 
			
		||||
2025-08-23 13:26:24 - main - INFO - Bot is off
 | 
			
		||||
2025-08-23 13:28:36 - main - INFO - Bot is off
 | 
			
		||||
2025-08-23 13:29:29 - main - INFO - Bot is off
 | 
			
		||||
2025-08-23 13:30:48 - main - INFO - Bot is off
 | 
			
		||||
2025-08-23 13:31:43 - main - INFO - Bot is off
 | 
			
		||||
2025-08-23 13:33:10 - main - INFO - Bot is off
 | 
			
		||||
2025-08-23 13:34:59 - main - INFO - Bot is off
 | 
			
		||||
2025-08-23 13:36:15 - main - INFO - Bot is off
 | 
			
		||||
2025-08-23 13:49:17 - main - INFO - Bot is off
 | 
			
		||||
2025-08-23 13:50:22 - main - INFO - Bot is on
 | 
			
		||||
2025-08-23 13:51:30 - main - INFO - Bot is off
 | 
			
		||||
2025-08-23 13:51:37 - main - INFO - Bot is on
 | 
			
		||||
2025-08-23 13:52:12 - main - INFO - Bot is off
 | 
			
		||||
2025-08-23 13:57:48 - main - INFO - Bot is on
 | 
			
		||||
2025-08-23 14:05:36 - main - INFO - Bot is off
 | 
			
		||||
2025-08-23 14:05:43 - main - INFO - Bot is on
 | 
			
		||||
2025-08-23 14:06:03 - main - INFO - Bot is off
 | 
			
		||||
2025-08-23 14:06:46 - main - INFO - Bot is on
 | 
			
		||||
2025-08-23 14:07:04 - requests - INFO - Bybit был успешно подключен
 | 
			
		||||
2025-08-23 14:07:43 - requests - INFO - Новый пользователь был добавлен в бд
 | 
			
		||||
2025-08-23 14:07:43 - requests - INFO - Основные настройки нового пользователя были заполнены
 | 
			
		||||
2025-08-23 14:07:43 - requests - INFO - Риск-Менеджмент настройки нового пользователя были заполнены
 | 
			
		||||
2025-08-23 14:07:43 - requests - INFO - Условные настройки нового пользователя были заполнены
 | 
			
		||||
2025-08-23 14:07:43 - requests - INFO - Дополнительные настройки нового пользователя были заполнены
 | 
			
		||||
2025-08-23 14:23:31 - main - INFO - Bot is off
 | 
			
		||||
2025-08-23 14:23:39 - main - INFO - Bot is on
 | 
			
		||||
2025-08-23 14:28:13 - main - INFO - Bot is off
 | 
			
		||||
2025-08-23 14:28:19 - main - INFO - Bot is on
 | 
			
		||||
2025-08-23 14:28:26 - requests - INFO - Получение риск-менеджмента настроек пользователя 899674724
 | 
			
		||||
2025-08-23 14:28:26 - requests - INFO - Получение риск-менеджмента настроек пользователя 899674724
 | 
			
		||||
2025-08-23 14:29:12 - requests - INFO - Получение риск-менеджмента настроек пользователя 899674724
 | 
			
		||||
2025-08-23 14:29:34 - main - INFO - Bot is off
 | 
			
		||||
@@ -4,7 +4,9 @@ aiohappyeyeballs==2.6.1
 | 
			
		||||
aiohttp==3.12.15
 | 
			
		||||
aiosignal==1.4.0
 | 
			
		||||
aiosqlite==0.21.0
 | 
			
		||||
alembic==1.16.5
 | 
			
		||||
annotated-types==0.7.0
 | 
			
		||||
asyncpg==0.30.0
 | 
			
		||||
attrs==25.3.0
 | 
			
		||||
black==25.1.0
 | 
			
		||||
certifi==2025.8.3
 | 
			
		||||
@@ -20,7 +22,9 @@ greenlet==3.2.4
 | 
			
		||||
idna==3.10
 | 
			
		||||
isort==6.0.1
 | 
			
		||||
magic-filter==1.0.12
 | 
			
		||||
Mako==1.3.10
 | 
			
		||||
mando==0.7.1
 | 
			
		||||
MarkupSafe==3.0.2
 | 
			
		||||
mccabe==0.7.0
 | 
			
		||||
multidict==6.6.4
 | 
			
		||||
mypy_extensions==1.1.0
 | 
			
		||||
@@ -29,10 +33,12 @@ packaging==25.0
 | 
			
		||||
pathspec==0.12.1
 | 
			
		||||
platformdirs==4.4.0
 | 
			
		||||
propcache==0.3.2
 | 
			
		||||
psycopg==3.2.10
 | 
			
		||||
psycopg-binary==3.2.10
 | 
			
		||||
pybit==5.11.0
 | 
			
		||||
pycodestyle==2.14.0
 | 
			
		||||
pycryptodome==3.23.0
 | 
			
		||||
pydantic==2.11.7
 | 
			
		||||
pydantic==2.11.9
 | 
			
		||||
pydantic_core==2.33.2
 | 
			
		||||
pyflakes==3.4.0
 | 
			
		||||
python-dotenv==1.1.1
 | 
			
		||||
@@ -42,7 +48,8 @@ requests==2.32.5
 | 
			
		||||
six==1.17.0
 | 
			
		||||
SQLAlchemy==2.0.43
 | 
			
		||||
typing-inspection==0.4.1
 | 
			
		||||
typing_extensions==4.14.1
 | 
			
		||||
typing_extensions==4.15.0
 | 
			
		||||
uliweb-alembic==0.6.9
 | 
			
		||||
urllib3==2.5.0
 | 
			
		||||
websocket-client==1.8.0
 | 
			
		||||
yarl==1.20.1
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										55
									
								
								run.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								run.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import contextlib
 | 
			
		||||
import logging.config
 | 
			
		||||
 | 
			
		||||
from aiogram import Bot, Dispatcher
 | 
			
		||||
from aiogram.fsm.storage.redis import RedisStorage
 | 
			
		||||
 | 
			
		||||
from database import init_db
 | 
			
		||||
from app.bybit.web_socket import WebSocketBot
 | 
			
		||||
from app.telegram.handlers import router
 | 
			
		||||
from config import BOT_TOKEN
 | 
			
		||||
from logger_helper.logger_helper import LOGGING_CONFIG
 | 
			
		||||
 | 
			
		||||
logging.config.dictConfig(LOGGING_CONFIG)
 | 
			
		||||
logger = logging.getLogger("run")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def main():
 | 
			
		||||
    """
 | 
			
		||||
    The main function of launching the bot.
 | 
			
		||||
 | 
			
		||||
    Performs database initialization, creation of bot and dispatcher objects,
 | 
			
		||||
    then it triggers the long polling event.
 | 
			
		||||
 | 
			
		||||
    Logs important events and errors.
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        await init_db()
 | 
			
		||||
        bot = Bot(token=BOT_TOKEN)
 | 
			
		||||
        storage = RedisStorage.from_url("redis://localhost:6379")
 | 
			
		||||
        dp = Dispatcher(storage=storage)
 | 
			
		||||
        dp.include_router(router)
 | 
			
		||||
        web_socket = WebSocketBot(telegram_bot=bot)
 | 
			
		||||
        await web_socket.clear_user_sockets()
 | 
			
		||||
        ws_task = asyncio.create_task(web_socket.run_user_check_loop())
 | 
			
		||||
        tg_task = asyncio.create_task(dp.start_polling(bot))
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            logger.info("Bot started")
 | 
			
		||||
            await asyncio.gather(ws_task, tg_task)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            logger.error("Bot stopped with error: %s", e)
 | 
			
		||||
        finally:
 | 
			
		||||
            for task in (ws_task, tg_task):
 | 
			
		||||
                task.cancel()
 | 
			
		||||
            with contextlib.suppress(asyncio.CancelledError):
 | 
			
		||||
                await ws_task
 | 
			
		||||
                await tg_task
 | 
			
		||||
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        logger.error("Bot stopped with error: %s", e)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    asyncio.run(main())
 | 
			
		||||
		Reference in New Issue
	
	Block a user