105 Commits

Author SHA1 Message Date
algizn97
ddfa3a7360 Fixed the switch trading mode, adjusted the take profit, added a trading cycle 2025-10-22 17:15:25 +05:00
algizn97
e61b7334a4 The formula for calculating the number of contracts by price has been changed 2025-10-21 13:59:09 +05:00
algizn97
5ad69f3f6d Fixed take profit calculation. Added a position check for the current pair when trying to change the margin. 2025-10-18 13:52:20 +05:00
algizn97
abad01352a Fixed the output 2025-10-17 11:28:57 +05:00
algizn97
720b30d681 Redundant call removed 2025-10-17 11:13:31 +05:00
algizn97
3616e2cbd3 Added verification for open orders. Adjusted responses for the user 2025-10-17 11:12:50 +05:00
algizn97
7d108337fa Fixed receiving the commission and calculating the total commission 2025-10-17 11:10:35 +05:00
algizn97
0f6e6a2168 Added position mode setting, fixed stop loss calculation 2025-10-17 11:09:21 +05:00
algizn97
258ed970f1 Fixed database creation 2025-10-12 15:08:27 +05:00
algizn97
a3a6509933 Fixed database creation 2025-10-12 15:05:50 +05:00
algizn97
8251938b2f The database has been converted to SQLite 2025-10-12 12:34:32 +05:00
algizn97
458b34fcec The instruction has been corrected 2025-10-11 14:14:27 +05:00
algizn97
4a7577b977 Added migrations for the database 2025-10-11 13:36:38 +05:00
algizn97
6e0a170f4b Merge branch 'devel' of https://git.svoboda.works/Alex/stcs into devel 2025-10-10 15:21:09 +05:00
algizn97
c7b4a08a6a Merge branch 'stable' of https://git.svoboda.works/Alex/stcs into devel 2025-10-10 15:19:31 +05:00
algizn97
d0971f59b4 Added environments 2025-10-10 14:42:47 +05:00
b92376d2da merge upstream 2025-10-10 16:35:16 +07:00
630f2002d3 Удалить alembic.ini 2025-10-10 16:24:01 +07:00
0784cbb54a Удалить alembic/versions/fd8581c0cc87_updated_leverage.py 2025-10-10 16:23:51 +07:00
eeb7f81440 Удалить alembic/versions/f00a94ccdf01_updated_deals.py 2025-10-10 16:23:47 +07:00
b03d05bb75 Удалить alembic/versions/ef38c90eed55_added_last_side_the_conditional_data.py 2025-10-10 16:23:43 +07:00
e0e4ad5d4b Удалить alembic/versions/ef342b38e17b_added_fee_user_deals.py 2025-10-10 16:23:39 +07:00
fab8ff5040 Удалить alembic/versions/dbffe818030c_added_last_side_and_auto_trading_for_.py 2025-10-10 16:23:35 +07:00
8071f8c896 Удалить alembic/versions/d3c85bad8c98_added_limit_and_trigger_price_for_user_.py 2025-10-10 16:23:30 +07:00
3db001bd19 Удалить alembic/versions/ccdc5764eb4f_added_userdeals.py 2025-10-10 16:23:26 +07:00
99c59be9ed Удалить alembic/versions/acbcc95de48d_updated_userdeals.py 2025-10-10 16:23:21 +07:00
37b7b6effd Удалить alembic/versions/c98b9dc36d15_fixed_auto_trade.py 2025-10-10 16:23:17 +07:00
ee285523f2 Удалить alembic/versions/8f1476c68efa_added_position_idx_for_user_deals_table.py 2025-10-10 16:23:12 +07:00
b426eb2136 Удалить alembic/versions/968f8121104f_updated_user_deals_and_user_conditional_.py 2025-10-10 16:23:07 +07:00
2df3b8b40d Удалить alembic/versions/c710f4e2259c_unnecessary_data_has_been_deleted.py 2025-10-10 16:23:03 +07:00
8c08451d82 Удалить alembic/versions/863d6215e1eb_updated_deals.py 2025-10-10 16:22:52 +07:00
d81a47b669 Удалить alembic/versions/77197715747c_deleted_position_idx_for_user_deals_.py 2025-10-10 16:22:48 +07:00
2cdfba3537 Удалить alembic/versions/73a00faa4f7f_added_user_auto_trading_table.py 2025-10-10 16:22:43 +07:00
c89c2ad803 Удалить alembic/versions/70094ba27e80_create_user_conditional_setting.py 2025-10-10 16:22:39 +07:00
3986989dbd Удалить alembic/versions/42c66cfe8d4e_updated_martingale_factor.py 2025-10-10 16:22:35 +07:00
c0e40dc205 Удалить alembic/versions/3534adf891fc_update_last_side_the_conditional_data.py 2025-10-10 16:22:30 +07:00
6c6f0dbb7b Удалить alembic/versions/10bf073c71f9_added_fee_for_user_auto_trading.py 2025-10-10 16:22:26 +07:00
44c4fde036 Удалить alembic/versions/45977e9d8558_updated_order_quantity.py 2025-10-10 16:22:21 +07:00
21a93d47d4 Удалить alembic/versions/2b9572b49ecd_added_side_for_user_auto_trading.py 2025-10-10 16:22:18 +07:00
3f43d42651 Удалить alembic/versions/0eed68eddcdb_added_conditional_order_type.py 2025-10-10 16:22:13 +07:00
aab05994ce Удалить alembic/versions/09db71875980_updated_user_deals_table.py 2025-10-10 16:22:08 +07:00
a58ebe6a46 Удалить alembic/README 2025-10-10 16:21:38 +07:00
1ec1f1784d Удалить alembic/script.py.mako 2025-10-10 16:21:33 +07:00
7901af86af Удалить alembic/env.py 2025-10-10 16:21:24 +07:00
fedfa00c10 Merge pull request 'devel' (#3) from devel into stable
Reviewed-on: Alex/stcs#3
2025-10-10 16:18:23 +07:00
algizn97
fc8ab19ae9 Fixed the budget calculation function 2025-10-10 14:14:46 +05:00
algizn97
42c4660fe3 The price of the trading pair has been removed, and the trade cancellation button has been removed. The text has been corrected 2025-10-10 13:31:52 +05:00
algizn97
fe030baef5 When choosing a coin, the leverage is set to the maximum possible for this coin, also SL in accordance with the leverage. The verification range from 1 to 100 has been removed, now the verification is within the acceptable values from the exchange 2025-10-10 13:30:32 +05:00
algizn97
9d06412605 Added the ability to summarize all commissions within a series.Minor bugs have been fixed 2025-10-10 13:28:12 +05:00
algizn97
9c1f289870 The currency of the coin is treason on USDT, unnecessary parameters are removed 2025-10-10 13:26:56 +05:00
algizn97
3533e7e99a Unnecessary buttons have been removed, the buttons of the trading mode and the direction of the first transaction of the series have been moved. 2025-10-10 13:25:30 +05:00
algizn97
8114533475 When adjusting the leverage, the SL changes according to the criteria. In place of the position mode, there is now a trading mode. All unnecessary functions are also removed. 2025-10-10 13:24:32 +05:00
algizn97
fcdc9d7483 The function allows you to write the number 0 2025-10-10 13:22:13 +05:00
algizn97
aa9f04c27e The stop trading button has been removed. 2025-10-10 13:21:15 +05:00
algizn97
89ab106992 Fixed percentages of TP and SL from integers to floats 2025-10-10 13:20:24 +05:00
algizn97
ebe2d58975 The trading mode has been moved to the main settings, Position mode, limit order and conditional order have been removed. The number of bids has been renamed to the base rate. The choice of the direction of the first transaction has been moved to the main settings 2025-10-10 13:18:43 +05:00
algizn97
09606a057b Added the addition of a common commission 2025-10-10 13:16:00 +05:00
algizn97
a0a2fd30f0 TP AND SL have been converted to float. Switch control has been moved to the main settings, Removed unnecessary parameters 2025-10-10 13:14:59 +05:00
algizn97
2136de5d69 Added alembic migrations 2025-10-09 14:21:54 +05:00
algizn97
dbbea16c19 Added alembic 2025-10-09 14:21:21 +05:00
898ff91392 Merge pull request 'added the exc_info flag' (#10) from Alex/stcs:dev into stable
Reviewed-on: #10
2025-10-07 12:10:58 +07:00
algizn97
f5677e6e7e added the exc_info flag 2025-10-07 09:44:02 +05:00
2047dd5ac6 Merge pull request 'dev' (#9) from Alex/stcs:dev into stable
Reviewed-on: #9
2025-10-06 21:33:56 +07:00
algizn97
c49df2794d Added startup instructions 2025-10-06 10:53:59 +05:00
algizn97
c687811ea5 Fixed websocket connected 2025-10-06 10:27:52 +05:00
algizn97
5da00dbaa1 Added connect platform 2025-10-04 11:04:08 +05:00
algizn97
01fe339d56 Updated 2025-10-04 11:03:49 +05:00
algizn97
220c45d54c Fixed 2025-10-04 09:34:11 +05:00
algizn97
163f4dcba9 Fixed get liq price 2025-10-04 09:33:56 +05:00
algizn97
ce5d0605de Fixed check auto trading 2025-10-04 09:33:44 +05:00
algizn97
086c7c8170 Fixed 2025-10-04 09:33:18 +05:00
algizn97
8e73dcf81f Fixed set margin mode 2025-10-04 09:33:09 +05:00
algizn97
057cfad675 Fixed liq price calculation 2025-10-03 14:19:40 +05:00
algizn97
1508629727 Fixed 2025-10-03 14:19:18 +05:00
algizn97
4adbd70948 Fixed close position 2025-10-03 14:18:53 +05:00
algizn97
6705bf4492 Added tasks 2025-10-03 14:18:27 +05:00
algizn97
8dbc8d57f9 Added fee 2025-10-02 15:30:20 +05:00
algizn97
fa782f748a Fixed 2025-10-02 15:28:07 +05:00
algizn97
a1a7355dc3 Fixed get liq price function 2025-10-02 15:27:33 +05:00
algizn97
9d2b049e56 Optimized 2025-10-02 12:39:48 +05:00
algizn97
3306c6e826 Fixed price 2025-10-02 12:27:04 +05:00
algizn97
2666f90707 Fixed format code 2025-10-02 12:26:55 +05:00
algizn97
bed53c0a2c Fixed commission_fee 2025-10-02 12:26:38 +05:00
algizn97
a9f7c4f7c4 Cancelled check tp sl price 2025-10-02 12:08:53 +05:00
algizn97
1981510963 Fixed check tp sl price 2025-10-01 15:50:13 +05:00
algizn97
4f2ce0c1a4 Added check tp sl price 2025-10-01 15:47:10 +05:00
algizn97
3ae8c15007 Added enviroment 2025-10-01 15:25:56 +05:00
algizn97
f81f63b198 Added loggers. Rename file. Update 2025-10-01 15:24:53 +05:00
algizn97
97662081ce The entire database has been changed to PostgresSQL. The entire code has been updated. 2025-10-01 15:23:21 +05:00
algizn97
e5a3de4ed8 Deleted 2025-10-01 15:20:18 +05:00
algizn97
66a566e6a3 Updated 2025-10-01 14:04:11 +05:00
algizn97
eca9d2c7c8 The rate is now calculated in dollar terms. 2025-09-20 10:01:26 +05:00
algizn97
6d86b230ca A floating-point bid option is now available. 2025-09-20 08:13:21 +05:00
fec367cc1d Merge pull request 'dev' (#8) from Alex/stcs:dev into stable
Reviewed-on: #8
2025-09-19 17:17:25 +07:00
algizn97
4bbff680aa Update 2025-09-19 14:57:08 +05:00
algizn97
49d4bb26bf Added trigger price 2025-09-19 14:45:34 +05:00
algizn97
29bb6bd0a8 The buttons for selecting the type of entry and setting the limit and trigger prices have been updated. 2025-09-19 14:43:42 +05:00
algizn97
2fb8cb4acb Added the ability to open a deal at a trigger price 2025-09-19 14:42:46 +05:00
algizn97
887b46c1d4 Added a condition for the price trigger 2025-09-19 14:42:12 +05:00
algizn97
b074d1d8a1 Added the ability to set a price trigger and limit 2025-09-19 14:41:18 +05:00
aebcc9dff2 Merge pull request 'dev' (#7) from Alex/stcs:dev into stable
Reviewed-on: #7
2025-09-18 22:49:55 +07:00
algizn97
e2f9478971 Fixed 2025-09-18 16:17:39 +05:00
algizn97
4f0668970f Fixed 2025-09-18 08:14:23 +05:00
algizn97
4c9901c14a Fixed 2025-09-17 20:51:30 +05:00
algizn97
17dba19078 Updated 2025-09-13 12:30:40 +05:00
83 changed files with 7598 additions and 4278 deletions

View File

@@ -1,3 +1 @@
TOKEN_TELEGRAM_BOT_1= BOT_TOKEN=YOUR_BOT_TOKEN
TOKEN_TELEGRAM_BOT_2=
TOKEN_TELEGRAM_BOT_3=

3
.flake8 Normal file
View File

@@ -0,0 +1,3 @@
[flake8]
max-line-length = 130
ignore = E501

215
.gitignore vendored
View File

@@ -1,17 +1,212 @@
.env # Byte-compiled / optimized / DLL files
!*.sample
__pycache__/ __pycache__/
*.pyc *.py[codz]
*$py.class
env/ # C extensions
venv/ *.so
.venv/
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
#poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
#pdm.lock
#pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
#pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.idea .idea
/.idea /.idea
/myenv .env
.envrc
.venv
env/
venv/
myenv myenv
ENV/
env.bak/
venv.bak/
/logger_helper/loggers
/app/bybit/logger_bybit/loggers
*.db
# Spyder project settings
.spyderproject
.spyproject
*.sqlite3 # Rope project settings
.ropeproject
*.log # 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__/

View File

@@ -1,51 +0,0 @@
import asyncio
import logging.config
from aiogram import Bot, Dispatcher
from app.services.Bybit.functions.bybit_ws import get_or_create_event_loop, set_event_loop
from app.telegram.database.models import async_main
from app.telegram.handlers.handlers import router
from app.telegram.functions.main_settings.settings import router_main_settings
from app.telegram.functions.risk_management_settings.settings import router_risk_management_settings
from app.telegram.functions.condition_settings.settings import condition_settings_router
from app.services.Bybit.functions.Add_Bybit_API import router_register_bybit_api
from app.services.Bybit.functions.functions import router_functions_bybit_trade
from logger_helper.logger_helper import LOGGING_CONFIG
from config import TOKEN_TG_BOT_1
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("main")
bot = Bot(token=TOKEN_TG_BOT_1)
dp = Dispatcher()
async def main() -> None:
"""
Основная асинхронная функция запуска бота:
"""
loop = get_or_create_event_loop()
set_event_loop(loop)
await async_main()
dp.include_router(router)
dp.include_router(router_main_settings)
dp.include_router(router_risk_management_settings)
dp.include_router(condition_settings_router)
dp.include_router(router_register_bybit_api)
dp.include_router(router_functions_bybit_trade)
try:
await dp.start_polling(bot)
except asyncio.CancelledError:
logger.info("Bot is off")
if __name__ == '__main__':
try:
logger.info("Bot is on")
asyncio.run(main())
except KeyboardInterrupt:
logger.info("Bot is off")

View File

@@ -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>

View File

@@ -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

View File

@@ -54,11 +54,15 @@ sudo -u www-data /usr/bin/pip install -r requirements.txt
cp .env.sample .env cp .env.sample .env
nvim .env nvim .env
``` ```
5. Выполните миграции:
```bash
alembic upgrade head
```
5. Запустите бота: 5. Запустите бота:
```bash ```bash
python BybitBot_API.py python run.py
``` ```
## Настройка автономной работы ## Настройка автономной работы

147
alembic.ini Normal file
View 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
View File

@@ -0,0 +1 @@
Generic single-database configuration.

53
alembic/env.py Normal file
View 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
View 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"}

View 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
View File

21
app/bybit/__init__.py Normal file
View File

@@ -0,0 +1,21 @@
import logging.config
from pybit.unified_trading import HTTP
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
from database import request as rq
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("bybit")
async def get_bybit_client(tg_id: int) -> HTTP | None:
"""
Get bybit client
"""
try:
api_key, api_secret = await rq.get_user_api(tg_id=tg_id)
return HTTP(api_key=api_key, api_secret=api_secret)
except Exception as e:
logger.error("Error getting bybit client for user %s: %s", tg_id, e)
return None

View 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(
tg_id: int, symbol: str, side: str, position_idx: int, qty: float
) -> bool:
"""
Closes all positions
:param tg_id: Telegram user ID
:param symbol: symbol
:param side: side
:param position_idx: position index
:param qty: quantity
:return: bool
"""
try:
client = await get_bybit_client(tg_id)
if side == "Buy":
r_side = "Sell"
else:
r_side = "Buy"
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("All positions closed for %s for user %s", symbol, tg_id)
return True
else:
logger.error(
"Error closing all positions for %s for user %s", symbol, tg_id
)
return False
except Exception as e:
logger.error(
"Error closing all 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

View File

View 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

View 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

View 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

View 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

View File

View 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,
},
},
}

403
app/bybit/open_positions.py Normal file
View File

@@ -0,0 +1,403 @@
import logging.config
import math
from pybit.exceptions import InvalidRequestError
import database.request as rq
from app.bybit import get_bybit_client
from app.bybit.get_functions.get_instruments_info import get_instruments_info
from app.bybit.get_functions.get_tickers import get_tickers
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
from app.bybit.set_functions.set_leverage import set_leverage
from app.bybit.set_functions.set_margin_mode import set_margin_mode
from app.bybit.set_functions.set_switch_position_mode import set_switch_position_mode
from app.helper_functions import safe_float
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("open_positions")
async def start_trading_cycle(
tg_id: int
) -> str | None:
"""
Start trading cycle
:param tg_id: Telegram user ID
"""
try:
symbol = await rq.get_user_symbol(tg_id=tg_id)
additional_data = await rq.get_user_additional_settings(tg_id=tg_id)
risk_management_data = await rq.get_user_risk_management(tg_id=tg_id)
user_deals_data = await rq.get_user_deal_by_symbol(
tg_id=tg_id, symbol=symbol
)
trade_mode = additional_data.trade_mode
switch_side = additional_data.switch_side
margin_type = additional_data.margin_type
leverage = additional_data.leverage
order_quantity = additional_data.order_quantity
trigger_price = additional_data.trigger_price
martingale_factor = additional_data.martingale_factor
max_bets_in_series = additional_data.max_bets_in_series
take_profit_percent = risk_management_data.take_profit_percent
stop_loss_percent = risk_management_data.stop_loss_percent
total_commission = 0
get_side = "Buy"
if user_deals_data:
get_side = user_deals_data.last_side or "Buy"
if trade_mode == "Switch":
if switch_side == "По направлению":
side = get_side
else:
if get_side == "Buy":
side = "Sell"
else:
side = "Buy"
else:
if trade_mode == "Long":
side = "Buy"
else:
side = "Sell"
await set_switch_position_mode(
tg_id=tg_id,
symbol=symbol,
mode=0)
await set_margin_mode(tg_id=tg_id, margin_mode=margin_type)
await set_leverage(
tg_id=tg_id,
symbol=symbol,
leverage=leverage,
)
res = await open_positions(
tg_id=tg_id,
symbol=symbol,
side=side,
order_quantity=order_quantity,
trigger_price=trigger_price,
margin_type=margin_type,
leverage=leverage,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
commission_fee_percent=total_commission
)
if res == "OK":
await rq.set_user_deal(
tg_id=tg_id,
symbol=symbol,
last_side=side,
current_step=1,
trade_mode=trade_mode,
side_mode=switch_side,
margin_type=margin_type,
leverage=leverage,
order_quantity=order_quantity,
trigger_price=trigger_price,
martingale_factor=martingale_factor,
max_bets_in_series=max_bets_in_series,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
base_quantity=order_quantity
)
return "OK"
return (
res
if res
in {
"Limit price is out min price",
"Limit price is out max price",
"Risk is too high for this trade",
"estimated will trigger liq",
"ab not enough for new order",
"InvalidRequestError",
"Order does not meet minimum order value",
"position idx not match position mode",
"Qty invalid",
"The number of contracts exceeds maximum limit allowed",
"The number of contracts exceeds minimum limit allowed"
}
else None
)
except Exception as e:
logger.error("Error in start_trading: %s", e)
return None
async def trading_cycle_profit(
tg_id: int, symbol: str, side: str) -> str | None:
try:
user_deals_data = await rq.get_user_deal_by_symbol(tg_id=tg_id, symbol=symbol)
user_auto_trading_data = await rq.get_user_auto_trading(tg_id=tg_id, symbol=symbol)
total_fee = user_auto_trading_data.total_fee
trade_mode = user_deals_data.trade_mode
margin_type = user_deals_data.margin_type
leverage = user_deals_data.leverage
trigger_price = 0
take_profit_percent = user_deals_data.take_profit_percent
stop_loss_percent = user_deals_data.stop_loss_percent
max_bets_in_series = user_deals_data.max_bets_in_series
martingale_factor = user_deals_data.martingale_factor
side_mode = user_deals_data.side_mode
base_quantity = user_deals_data.base_quantity
await set_margin_mode(tg_id=tg_id, margin_mode=margin_type)
await set_leverage(
tg_id=tg_id,
symbol=symbol,
leverage=leverage,
)
if trade_mode == "Switch":
if side_mode == "Противоположно":
s_side = "Sell" if side == "Buy" else "Buy"
else:
s_side = side
else:
s_side = side
res = await open_positions(
tg_id=tg_id,
symbol=symbol,
side=s_side,
order_quantity=base_quantity,
trigger_price=trigger_price,
margin_type=margin_type,
leverage=leverage,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
commission_fee_percent=total_fee
)
if res == "OK":
await rq.set_user_deal(
tg_id=tg_id,
symbol=symbol,
last_side=side,
current_step=1,
trade_mode=trade_mode,
side_mode=side_mode,
margin_type=margin_type,
leverage=leverage,
order_quantity=base_quantity,
trigger_price=trigger_price,
martingale_factor=martingale_factor,
max_bets_in_series=max_bets_in_series,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
base_quantity=base_quantity
)
return "OK"
return (
res
if res
in {
"Risk is too high for this trade",
"ab not enough for new order",
"InvalidRequestError",
"The number of contracts exceeds maximum limit allowed",
}
else None
)
except Exception as e:
logger.error("Error in trading_cycle_profit: %s", e)
return None
async def trading_cycle(
tg_id: int, symbol: str, side: str,
) -> str | None:
try:
user_deals_data = await rq.get_user_deal_by_symbol(tg_id=tg_id, symbol=symbol)
user_auto_trading_data = await rq.get_user_auto_trading(tg_id=tg_id, symbol=symbol)
total_fee = user_auto_trading_data.total_fee
trade_mode = user_deals_data.trade_mode
margin_type = user_deals_data.margin_type
leverage = user_deals_data.leverage
trigger_price = 0
take_profit_percent = user_deals_data.take_profit_percent
stop_loss_percent = user_deals_data.stop_loss_percent
max_bets_in_series = user_deals_data.max_bets_in_series
martingale_factor = user_deals_data.martingale_factor
current_step = user_deals_data.current_step
order_quantity = user_deals_data.order_quantity
base_quantity = user_deals_data.base_quantity
side_mode = user_deals_data.side_mode
next_quantity = safe_float(order_quantity) * (
safe_float(martingale_factor)
)
current_step += 1
if max_bets_in_series < current_step:
return "Max bets in series"
await set_margin_mode(tg_id=tg_id, margin_mode=margin_type)
await set_leverage(
tg_id=tg_id,
symbol=symbol,
leverage=leverage,
)
res = await open_positions(
tg_id=tg_id,
symbol=symbol,
side=side,
order_quantity=next_quantity,
trigger_price=trigger_price,
margin_type=margin_type,
leverage=leverage,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
commission_fee_percent=total_fee
)
if res == "OK":
await rq.set_user_deal(
tg_id=tg_id,
symbol=symbol,
last_side=side,
current_step=current_step,
trade_mode=trade_mode,
side_mode=side_mode,
margin_type=margin_type,
leverage=leverage,
order_quantity=next_quantity,
trigger_price=trigger_price,
martingale_factor=martingale_factor,
max_bets_in_series=max_bets_in_series,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
base_quantity=base_quantity
)
return "OK"
return (
res
if res
in {
"Risk is too high for this trade",
"ab not enough for new order",
"InvalidRequestError",
"The number of contracts exceeds maximum limit allowed",
}
else None
)
except Exception as e:
logger.error("Error in trading_cycle: %s", e)
return None
async def open_positions(
tg_id: int,
side: str,
symbol: str,
order_quantity: float,
trigger_price: float,
margin_type: str,
leverage: str,
take_profit_percent: float,
stop_loss_percent: float,
commission_fee_percent: float
) -> str | None:
try:
client = await get_bybit_client(tg_id=tg_id)
get_ticker = await get_tickers(tg_id, symbol=symbol)
price_symbol = safe_float(get_ticker.get("lastPrice")) or 0
instruments_info = await get_instruments_info(tg_id=tg_id, symbol=symbol)
qty_step_str = instruments_info.get("lotSizeFilter").get("qtyStep")
qty_step = safe_float(qty_step_str)
qty = (safe_float(order_quantity) * safe_float(leverage)) / safe_float(price_symbol)
decimals = abs(int(round(math.log10(qty_step))))
qty_formatted = math.floor(qty / qty_step) * qty_step
qty_formatted = round(qty_formatted, decimals)
if trigger_price > 0:
po_trigger_price = str(trigger_price)
trigger_direction = 1 if trigger_price > price_symbol else 2
else:
po_trigger_price = None
trigger_direction = None
price_for_cals = trigger_price if po_trigger_price is not None else price_symbol
if qty_formatted <= 0:
return "Order does not meet minimum order value"
if margin_type == "ISOLATED_MARGIN":
if side == "Buy":
take_profit_price = price_for_cals * (
1 + take_profit_percent / 100) + commission_fee_percent / qty_formatted
stop_loss_price = None
else:
take_profit_price = price_for_cals * (
1 - take_profit_percent / 100) - commission_fee_percent / qty_formatted
stop_loss_price = None
else:
if side == "Buy":
take_profit_price = price_for_cals * (
1 + take_profit_percent / 100) + commission_fee_percent / qty_formatted
stop_loss_price = price_for_cals * (1 - stop_loss_percent / 100)
else:
take_profit_price = price_for_cals * (
1 - take_profit_percent / 100) - commission_fee_percent / qty_formatted
stop_loss_price = price_for_cals * (1 + stop_loss_percent / 100)
take_profit_price = max(take_profit_price, 0)
stop_loss_price = max(stop_loss_price, 0)
# Place order
order_params = {
"category": "linear",
"symbol": symbol,
"side": side,
"orderType": "Market",
"qty": str(qty_formatted),
"triggerDirection": trigger_direction,
"triggerPrice": po_trigger_price,
"triggerBy": "LastPrice",
"timeInForce": "GTC",
"positionIdx": 0,
"tpslMode": "Full",
"takeProfit": str(take_profit_price) if take_profit_price else None,
"stopLoss": str(stop_loss_price) if stop_loss_price else None,
}
response = client.place_order(**order_params)
if response["retCode"] == 0:
logger.info("Position opened for user: %s", tg_id)
return "OK"
logger.error("Error opening position for user: %s", tg_id)
return None
except InvalidRequestError as e:
error_text = str(e)
known_errors = {
"Order does not meet minimum order value": "Order does not meet minimum order value",
"estimated will trigger liq": "estimated will trigger liq",
"ab not enough for new order": "ab not enough for new order",
"position idx not match position mode": "position idx not match position mode",
"Qty invalid": "Qty invalid",
"The number of contracts exceeds maximum limit allowed": "The number of contracts exceeds maximum limit allowed",
"The number of contracts exceeds minimum limit allowed": "The number of contracts exceeds minimum limit allowed",
}
for key, msg in known_errors.items():
if key in error_text:
logger.error(msg)
return msg
logger.error("InvalidRequestError: %s", e)
return "InvalidRequestError"
except Exception as e:
logger.error("Error opening position for user %s: %s", tg_id, e, exc_info=True)
return None

View 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)

View File

View 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

View 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

View 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

View File

@@ -0,0 +1,45 @@
import logging.config
from app.bybit import get_bybit_client
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("set_tp_sl")
async def set_tp_sl_for_position(
tg_id: int,
symbol: str,
take_profit_price: float,
stop_loss_price: float,
position_idx: int,
) -> bool:
"""
Set take profit and stop loss for a symbol.
:param tg_id: Telegram user ID
:param symbol: Symbol to set take profit and stop loss for
:param take_profit_price: Take profit price
:param stop_loss_price: Stop loss price
:param position_idx: Position index
:return: bool
"""
try:
client = await get_bybit_client(tg_id)
resp = client.set_trading_stop(
category="linear",
symbol=symbol,
takeProfit=str(round(take_profit_price, 5)),
stopLoss=str(round(stop_loss_price, 5)),
positionIdx=position_idx,
tpslMode="Full",
)
if resp.get("retCode") == 0:
logger.info("TP/SL for %s has been set", symbol)
return True
else:
logger.error("Error setting TP/SL for %s: %s", symbol, resp.get("retMsg"))
return False
except Exception as e:
logger.error("Error setting TP/SL for %s: %s", symbol, e)
return False

View File

@@ -0,0 +1,265 @@
import logging.config
import app.telegram.keyboards.inline as kbi
import database.request as rq
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
from app.bybit.open_positions import trading_cycle, trading_cycle_profit
from app.helper_functions import format_value, safe_float
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("telegram_message_handler")
class TelegramMessageHandler:
def __init__(self, telegram_bot):
self.telegram_bot = telegram_bot
async def format_position_update(self, message):
pass
async def format_order_update(self, message, tg_id):
try:
order_data = message.get("data", [{}])[0]
symbol = format_value(order_data.get("symbol"))
qty = format_value(order_data.get("qty"))
side = format_value(order_data.get("side"))
side_rus = (
"Покупка"
if side == "Buy"
else "Продажа" if side == "Sell" else "Нет данных"
)
order_status = format_value(order_data.get("orderStatus"))
price = format_value(order_data.get("price"))
trigger_price = format_value(order_data.get("triggerPrice"))
take_profit = format_value(order_data.get("takeProfit"))
stop_loss = format_value(order_data.get("stopLoss"))
status_map = {
"Untriggered": "Условный ордер выставлен",
}
if order_status == "Filled" or order_status not in status_map:
return None
user_auto_trading = await rq.get_user_auto_trading(
tg_id=tg_id, symbol=symbol
)
auto_trading = (
user_auto_trading.auto_trading if user_auto_trading else False
)
user_deals_data = await rq.get_user_deal_by_symbol(
tg_id=tg_id, symbol=symbol
)
text = (
f"Торговая пара: {symbol}\n"
f"Движение: {side_rus}\n"
)
if user_deals_data is not None and auto_trading:
text += f"Текущая ставка: {user_deals_data.order_quantity} USDT\n"
else:
text += f"Количество: {qty}\n"
if price and price != "0":
text += f"Цена: {price}\n"
if take_profit and take_profit != "Нет данных":
text += f"Тейк-профит: {take_profit}\n"
if stop_loss and stop_loss != "Нет данных":
text += f"Стоп-лосс: {stop_loss}\n"
if trigger_price and trigger_price != "Нет данных":
text += f"Триггер цена: {trigger_price}\n"
await self.telegram_bot.send_message(
chat_id=tg_id, text=text, reply_markup=kbi.profile_bybit
)
except Exception as e:
logger.error("Error in format_order_update: %s", e)
async def format_execution_update(self, message, tg_id):
try:
execution = message.get("data", [{}])[0]
closed_size = format_value(execution.get("closedSize"))
symbol = format_value(execution.get("symbol"))
exec_price = format_value(execution.get("execPrice"))
exec_qty = format_value(execution.get("execQty"))
exec_fees = format_value(execution.get("execFee"))
fee_rate = format_value(execution.get("feeRate"))
side = format_value(execution.get("side"))
side_rus = (
"Покупка"
if side == "Buy"
else "Продажа" if side == "Sell" else "Нет данных"
)
if safe_float(exec_fees) == 0:
exec_fee = safe_float(exec_price) * safe_float(exec_qty) * safe_float(
fee_rate
)
else:
exec_fee = safe_float(exec_fees)
if safe_float(closed_size) == 0:
await rq.set_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, fee=safe_float(exec_fee)
)
user_auto_trading = await rq.get_user_auto_trading(
tg_id=tg_id, symbol=symbol
)
get_total_fee = user_auto_trading.total_fee
total_fee = safe_float(exec_fee) + safe_float(get_total_fee)
if user_auto_trading is not None and user_auto_trading.fee is not None:
fee = user_auto_trading.fee
else:
fee = 0
exec_pnl = format_value(execution.get("execPnl"))
risk_management_data = await rq.get_user_risk_management(tg_id=tg_id)
commission_fee = risk_management_data.commission_fee
if commission_fee == "Yes_commission_fee":
total_pnl = safe_float(exec_pnl) - safe_float(exec_fee) - fee
else:
total_pnl = safe_float(exec_pnl)
header = (
"Сделка закрыта:" if safe_float(closed_size) > 0 else "Сделка открыта:"
)
text = f"{header}\n" f"Торговая пара: {symbol}\n"
auto_trading = (
user_auto_trading.auto_trading if user_auto_trading else False
)
user_deals_data = await rq.get_user_deal_by_symbol(
tg_id=tg_id, symbol=symbol
)
if user_deals_data is not None and auto_trading:
await rq.set_total_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, total_fee=total_fee
)
text += f"Текущая ставка: {user_deals_data.order_quantity} USDT\n"
else:
text += f"Количество: {exec_qty}\n"
text += (
f"Цена исполнения: {exec_price}\n"
f"Комиссия: {exec_fee:.8f}\n"
)
if safe_float(closed_size) == 0:
text += f"Движение: {side_rus}\n"
else:
text += f"\nРеализованная прибыль: {total_pnl:.7f}\n"
await self.telegram_bot.send_message(
chat_id=tg_id, text=text, reply_markup=kbi.profile_bybit
)
user_symbols = user_auto_trading.symbol if user_auto_trading else None
if (
auto_trading
and safe_float(closed_size) > 0
and user_symbols is not None
):
if safe_float(total_pnl) > 0:
profit_text = "📈 Прибыль достигнута. Начинаем новую серию с базовой ставки\n"
await self.telegram_bot.send_message(
chat_id=tg_id, text=profit_text, reply_markup=kbi.profile_bybit
)
if side == "Buy":
r_side = "Sell"
else:
r_side = "Buy"
await rq.set_last_side_by_symbol(
tg_id=tg_id, symbol=symbol, last_side=r_side)
await rq.set_total_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, total_fee=0
)
await rq.set_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, fee=0
)
res = await trading_cycle_profit(
tg_id=tg_id, symbol=symbol, side=r_side
)
if res == "OK":
pass
else:
errors = {
"Max bets in series": "❗️ Максимальное количество сделок в серии достигнуто",
"Risk is too high for this trade": "❗️ Риск сделки слишком высок для продолжения",
"ab not enough for new order": "❗️ Недостаточно средств для продолжения торговли",
"InvalidRequestError": "❗️ Недостаточно средств для размещения нового ордера с заданным количеством и плечом.",
"The number of contracts exceeds maximum limit allowed": "❗️ Превышен максимальный лимит ставки",
}
error_text = errors.get(
res, "❗️ Не удалось открыть новую сделку"
)
await rq.set_auto_trading(
tg_id=tg_id, symbol=symbol, auto_trading=False
)
await rq.set_total_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, total_fee=0
)
await rq.set_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, fee=0
)
await self.telegram_bot.send_message(
chat_id=tg_id,
text=error_text,
reply_markup=kbi.profile_bybit,
)
else:
open_order_text = "\n❗️ Сделка закрылась в минус, открываю новую сделку с увеличенной ставкой.\n"
await self.telegram_bot.send_message(
chat_id=tg_id, text=open_order_text
)
if side == "Buy":
r_side = "Sell"
else:
r_side = "Buy"
res = await trading_cycle(
tg_id=tg_id, symbol=symbol, side=r_side
)
if res == "OK":
pass
else:
errors = {
"Max bets in series": "❗️ Максимальное количество сделок в серии достигнуто",
"Risk is too high for this trade": "❗️ Риск сделки слишком высок для продолжения",
"ab not enough for new order": "❗️ Недостаточно средств для продолжения торговли",
"InvalidRequestError": "❗️ Недостаточно средств для размещения нового ордера с заданным количеством и плечом.",
"The number of contracts exceeds maximum limit allowed": "❗️ Превышен максимальный лимит ставки",
}
error_text = errors.get(
res, "❗️ Не удалось открыть новую сделку"
)
await rq.set_auto_trading(
tg_id=tg_id, symbol=symbol, auto_trading=False
)
await rq.set_total_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, total_fee=0
)
await rq.set_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, fee=0
)
await self.telegram_bot.send_message(
chat_id=tg_id,
text=error_text,
reply_markup=kbi.profile_bybit,
)
except Exception as e:
logger.error("Error in telegram_message_handler: %s", e)

122
app/bybit/web_socket.py Normal file
View File

@@ -0,0 +1,122 @@
import asyncio
import logging.config
from pybit.unified_trading import WebSocket
import database.request as rq
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
from app.bybit.telegram_message_handler import TelegramMessageHandler
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("web_socket")
class WebSocketBot:
"""
Class to handle WebSocket connections and messages.
"""
def __init__(self, telegram_bot):
"""Initialize the TradingBot class."""
self.telegram_bot = telegram_bot
self.ws_private = None
self.user_messages = {}
self.user_sockets = {}
self.user_keys = {}
self.loop = None
self.message_handler = TelegramMessageHandler(telegram_bot)
async def run_user_check_loop(self):
"""Run a loop to check for users and connect them to the WebSocket."""
self.loop = asyncio.get_running_loop()
while True:
users = await WebSocketBot.get_users_from_db()
for user in users:
tg_id = user.tg_id
api_key, api_secret = await rq.get_user_api(tg_id=tg_id)
if not api_key or not api_secret:
continue
keys_stored = self.user_keys.get(tg_id)
if tg_id in self.user_sockets and keys_stored == (api_key, api_secret):
continue
if tg_id in self.user_sockets:
self.user_sockets.clear()
self.user_messages.clear()
self.user_keys.clear()
logger.info(
"Closed old websocket for user %s due to key change", tg_id
)
success = await self.try_connect_user(api_key, api_secret, tg_id)
if success:
self.user_keys[tg_id] = (api_key, api_secret)
self.user_messages.setdefault(
tg_id, {"position": None, "order": None, "execution": None}
)
logger.info("User %s connected to WebSocket", tg_id)
else:
await asyncio.sleep(30)
await asyncio.sleep(10)
async def clear_user_sockets(self):
"""Clear the user_sockets and user_messages dictionaries."""
self.user_sockets.clear()
self.user_messages.clear()
self.user_keys.clear()
logger.info("Cleared user_sockets")
async def try_connect_user(self, api_key, api_secret, tg_id):
"""Try to connect a user to the WebSocket."""
try:
self.ws_private = WebSocket(
testnet=False,
channel_type="private",
api_key=api_key,
api_secret=api_secret,
)
self.user_sockets[tg_id] = self.ws_private
# Connect to the WebSocket private channel
# Handle position updates
self.ws_private.position_stream(
lambda msg: self.loop.call_soon_threadsafe(
asyncio.create_task, self.handle_position_update(msg)
)
)
# Handle order updates
self.ws_private.order_stream(
lambda msg: self.loop.call_soon_threadsafe(
asyncio.create_task, self.handle_order_update(msg, tg_id)
)
)
# Handle execution updates
self.ws_private.execution_stream(
lambda msg: self.loop.call_soon_threadsafe(
asyncio.create_task, self.handle_execution_update(msg, tg_id)
)
)
return True
except Exception as e:
logger.error("Error connecting user %s: %s", tg_id, e)
return False
async def handle_position_update(self, message):
"""Handle position updates."""
await self.message_handler.format_position_update(message)
async def handle_order_update(self, message, tg_id):
"""Handle order updates."""
await self.message_handler.format_order_update(message, tg_id)
async def handle_execution_update(self, message, tg_id):
"""Handle execution updates."""
await self.message_handler.format_execution_update(message, tg_id)
@staticmethod
async def get_users_from_db():
"""Get all users from the database."""
return await rq.get_users()

181
app/helper_functions.py Normal file
View File

@@ -0,0 +1,181 @@
import logging.config
from app.bybit import get_bybit_client
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("helper_functions")
def safe_float(val) -> float:
"""
Function to safely convert string to float
"""
try:
if val is None or val == "":
return 0.0
return float(val)
except (ValueError, TypeError):
logger.error("Error converting value to float: %s", val)
return 0.0
def is_number(value: str) -> bool:
"""
Checks if a given string represents a number.
Args:
value (str): The string to check.
Returns:
bool: True if the string represents a number, False otherwise.
"""
try:
# Convert the string to a float
num = float(value)
# Check if the number is positive
if num < 0:
return False
# Check if the string contains "+" or "-"
if "+" in value or "-" in value:
return False
# Check if the string contains only digits
allowed_chars = set("0123456789.")
if not all(ch in allowed_chars for ch in value):
return False
return True
except ValueError:
return False
def is_int(value: str) -> bool:
"""
Checks if a given string represents an integer.
Args:
value (str): The string to check.
Returns:
bool: True if the string represents an integer, False otherwise.
"""
# Check if the string contains only digits
if not value.isdigit():
return False
# Convert the string to an integer
num = int(value)
return num > 0
def is_int_for_timer(value: str) -> bool | int:
"""
Checks if a given string represents an integer for timer.
Args:
value (str): The string to check.
Returns:
bool: True if the string represents an integer, False otherwise.
"""
# Check if the string contains only digits
try:
num = int(value)
if num >= 0:
return num
else:
return False
except ValueError:
return False
def get_base_currency(symbol: str) -> str:
"""
Extracts the base currency from a symbol string.
Args:
symbol (str): The symbol string to extract the base currency from.
Returns:
str: The base currency extracted from the symbol string.
"""
if symbol.endswith("USDT"):
return symbol[:-4]
return symbol
def safe_int(value, default=0) -> int:
"""
Integer conversion with default value.
"""
try:
return int(value)
except (ValueError, TypeError):
return default
def format_value(value) -> str:
"""
Function to format value
"""
if not value or value.strip() == "":
return "Нет данных"
return value
def check_limit_price(limit_price, min_price, max_price) -> str | None:
"""
Function to check limit price
"""
if limit_price < min_price:
return "Limit price is out min price"
if limit_price > max_price:
return "Limit price is out max price"
return None
async def get_liquidation_price(
tg_id: int, symbol: str, entry_price: float, leverage: float
) -> tuple[float, float]:
"""
Function to get liquidation price
"""
try:
client = await get_bybit_client(tg_id=tg_id)
get_risk_info = client.get_risk_limit(category="linear", symbol=symbol)
risk_list = get_risk_info.get("result", {}).get("list", [])
risk_level = risk_list[0] if risk_list else {}
maintenance_margin_rate = safe_float(risk_level.get("maintenanceMargin"))
liq_price_long = entry_price * (1 - 1 / leverage + maintenance_margin_rate)
liq_price_short = entry_price * (1 + 1 / leverage - maintenance_margin_rate)
liq_price = liq_price_long, liq_price_short
return liq_price
except Exception as e:
logger.error("Error getting liquidation price: %s", e)
return 0, 0
async def calculate_total_budget(
quantity, martingale_factor, max_steps
) -> float:
"""
Calculate the total budget for a series of trading steps.
Args:
quantity (float): The initial quantity of the asset.
martingale_factor (float): The factor by which the quantity is multiplied for each step.
max_steps (int): The maximum number of trading steps.
Returns:
float: The total budget for the series of trading steps.
"""
total = 0
for step in range(max_steps):
set_quantity = quantity * (martingale_factor**step)
r_quantity = set_quantity
total += r_quantity
return total

View File

@@ -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)

View File

@@ -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} по таймеру отменено.")

View File

@@ -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

View File

@@ -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)

View File

@@ -1,562 +0,0 @@
import asyncio
import logging.config
from aiogram import F, Router
from app.services.Bybit.functions.bybit_ws import run_ws_for_user
from app.telegram.functions.main_settings.settings import main_settings_message
from logger_helper.logger_helper import LOGGING_CONFIG
from app.services.Bybit.functions.Futures import (close_user_trade, set_take_profit_stop_loss, \
get_active_positions_by_symbol, get_active_orders_by_symbol,
get_active_positions, get_active_orders, cancel_all_tp_sl_orders,
open_position, close_trade_after_delay, safe_float,
)
from app.services.Bybit.functions.balance import get_balance
import app.telegram.Keyboards.inline_keyboards as inline_markup
import app.telegram.database.requests as rq
from aiogram.types import Message, CallbackQuery
from app.services.Bybit.functions.price_symbol import get_price
from app.states.States import (state_update_entry_type, state_update_symbol, state_limit_price,
SetTP_SL_State, CloseTradeTimerState)
from aiogram.fsm.context import FSMContext
from app.services.Bybit.functions.get_valid_symbol import get_valid_symbols
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("functions")
router_functions_bybit_trade = Router()
user_trade_tasks = {}
@router_functions_bybit_trade.callback_query(F.data.in_(['clb_start_trading', 'clb_back_to_main', 'back_to_main']))
async def clb_start_bybit_trade_message(callback: CallbackQuery) -> None:
"""
Обработка нажатия кнопок запуска торговли или возврата в главное меню.
Отправляет информацию о балансе, символе, цене и инструкциях по торговле.
"""
user_id = callback.from_user.id
balance = await get_balance(user_id, callback.message)
if balance:
symbol = await rq.get_symbol(user_id)
price = await get_price(user_id, symbol=symbol)
text = (
f"💎 Торговля на Bybit\n\n"
f"⚖️ Ваш баланс (USDT): {float(balance):.2f}\n"
f"📊 Текущая торговая пара: {symbol}\n"
f"$$$ Цена: {price}\n\n"
"Как начать торговлю?\n\n"
"1⃣ Проверьте и тщательно настройте все параметры в вашем профиле.\n"
"2⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).\n"
"3⃣ Нажмите кнопку 'Начать торговать'.\n"
)
await callback.message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup)
async def start_bybit_trade_message(message: Message) -> None: