Merge pull request 'dev' (#9) from Alex/stcs:dev into stable

Reviewed-on: #9
This commit is contained in:
2025-10-06 21:33:56 +07:00
77 changed files with 8699 additions and 4374 deletions

View File

@@ -1,3 +1,6 @@
TOKEN_TELEGRAM_BOT_1=
TOKEN_TELEGRAM_BOT_2=
TOKEN_TELEGRAM_BOT_3=
BOT_TOKEN=YOUR_BOT_TOKEN
DB_USER=your_username
DB_PASS=your_password
DB_HOST=your_host
DB_PORT=your_port
DB_NAME=your_database

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

@@ -58,7 +58,7 @@ nvim .env
5. Запустите бота:
```bash
python BybitBot_API.py
python run.py
```
## Настройка автономной работы

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

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

@@ -0,0 +1,443 @@
import logging.config
from pybit.exceptions import InvalidRequestError
import database.request as rq
from app.bybit import get_bybit_client
from app.bybit.get_functions.get_balance import get_balance
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,
set_leverage_to_buy_and_sell,
)
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 check_limit_price, get_liquidation_price, safe_float
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("open_positions")
async def start_trading_cycle(
tg_id: int, side: str, switch_side_mode: bool
) -> str | None:
"""
Start trading cycle
:param tg_id: Telegram user ID
:param side: Buy or Sell
:param switch_side_mode: switch_side_mode
"""
try:
symbol = await rq.get_user_symbol(tg_id=tg_id)
additional_data = await rq.get_user_additional_settings(tg_id=tg_id)
risk_management_data = await rq.get_user_risk_management(tg_id=tg_id)
trade_mode = additional_data.trade_mode
margin_type = additional_data.margin_type
leverage = additional_data.leverage
leverage_to_buy = additional_data.leverage_to_buy
leverage_to_sell = additional_data.leverage_to_sell
order_type = additional_data.order_type
conditional_order_type = additional_data.conditional_order_type
order_quantity = additional_data.order_quantity
limit_price = additional_data.limit_price
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
max_risk_percent = risk_management_data.max_risk_percent
mode = 3 if trade_mode == "Both_Sides" else 0
await set_switch_position_mode(tg_id=tg_id, symbol=symbol, mode=mode)
await set_margin_mode(tg_id=tg_id, margin_mode=margin_type)
if trade_mode == "Both_Sides" and margin_type == "ISOLATED_MARGIN":
await set_leverage_to_buy_and_sell(
tg_id=tg_id,
symbol=symbol,
leverage_to_buy=leverage_to_buy,
leverage_to_sell=leverage_to_sell,
)
else:
await set_leverage(
tg_id=tg_id,
symbol=symbol,
leverage=leverage,
)
res = await open_positions(
tg_id=tg_id,
symbol=symbol,
side=side,
order_type=order_type,
conditional_order_type=conditional_order_type,
order_quantity=order_quantity,
limit_price=limit_price,
trigger_price=trigger_price,
trade_mode=trade_mode,
margin_type=margin_type,
leverage=leverage,
leverage_to_buy=leverage_to_buy,
leverage_to_sell=leverage_to_sell,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
max_risk_percent=max_risk_percent,
)
if res == "OK":
await rq.set_user_deal(
tg_id=tg_id,
symbol=symbol,
last_side=side,
current_step=1,
trade_mode=trade_mode,
margin_type=margin_type,
leverage=leverage,
leverage_to_buy=leverage_to_buy,
leverage_to_sell=leverage_to_sell,
order_type="Market",
conditional_order_type=conditional_order_type,
order_quantity=order_quantity,
limit_price=limit_price,
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,
max_risk_percent=max_risk_percent,
switch_side_mode=switch_side_mode,
)
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",
}
else None
)
except Exception as e:
logger.error("Error in start_trading: %s", e)
return None
async def trading_cycle(
tg_id: int, symbol: str, reverse_side: str, size: str
) -> str | None:
try:
user_deals_data = await rq.get_user_deal_by_symbol(tg_id=tg_id, symbol=symbol)
trade_mode = user_deals_data.trade_mode
order_type = user_deals_data.order_type
conditional_order_type = user_deals_data.conditional_order_type
margin_type = user_deals_data.margin_type
leverage = user_deals_data.leverage
leverage_to_buy = user_deals_data.leverage_to_buy
leverage_to_sell = user_deals_data.leverage_to_sell
limit_price = user_deals_data.limit_price
trigger_price = user_deals_data.trigger_price
take_profit_percent = user_deals_data.take_profit_percent
stop_loss_percent = user_deals_data.stop_loss_percent
max_risk_percent = user_deals_data.max_risk_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
switch_side_mode = user_deals_data.switch_side_mode
mode = 3 if trade_mode == "Both_Sides" else 0
await set_switch_position_mode(tg_id=tg_id, symbol=symbol, mode=mode)
await set_margin_mode(tg_id=tg_id, margin_mode=margin_type)
if trade_mode == "Both_Sides" and margin_type == "ISOLATED_MARGIN":
await set_leverage_to_buy_and_sell(
tg_id=tg_id,
symbol=symbol,
leverage_to_buy=leverage_to_buy,
leverage_to_sell=leverage_to_sell,
)
else:
await set_leverage(
tg_id=tg_id,
symbol=symbol,
leverage=leverage,
)
if reverse_side == "Buy":
real_side = "Sell"
else:
real_side = "Buy"
side = real_side
if switch_side_mode:
side = "Sell" if real_side == "Buy" else "Buy"
next_quantity = safe_float(size) * (
safe_float(martingale_factor) ** current_step
)
current_step += 1
if max_bets_in_series < current_step:
return "Max bets in series"
res = await open_positions(
tg_id=tg_id,
symbol=symbol,
side=side,
order_type="Market",
conditional_order_type=conditional_order_type,
order_quantity=next_quantity,
limit_price=limit_price,
trigger_price=trigger_price,
trade_mode=trade_mode,
margin_type=margin_type,
leverage=leverage,
leverage_to_buy=leverage_to_buy,
leverage_to_sell=leverage_to_sell,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
max_risk_percent=max_risk_percent,
)
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,
margin_type=margin_type,
leverage=leverage,
leverage_to_buy=leverage_to_buy,
leverage_to_sell=leverage_to_sell,
order_type=order_type,
conditional_order_type=conditional_order_type,
order_quantity=next_quantity,
limit_price=limit_price,
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,
max_risk_percent=max_risk_percent,
switch_side_mode=switch_side_mode,
)
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_type: str,
conditional_order_type: str,
order_quantity: float,
limit_price: float,
trigger_price: float,
trade_mode: str,
margin_type: str,
leverage: str,
leverage_to_buy: str,
leverage_to_sell: str,
take_profit_percent: float,
stop_loss_percent: float,
max_risk_percent: float,
) -> str | None:
try:
client = await get_bybit_client(tg_id=tg_id)
risk_management_data = await rq.get_user_risk_management(tg_id=tg_id)
commission_fee = risk_management_data.commission_fee
wallet = await get_balance(tg_id=tg_id)
user_balance = wallet.get("totalWalletBalance", 0)
instruments_resp = await get_instruments_info(tg_id=tg_id, symbol=symbol)
get_order_prices = instruments_resp.get("priceFilter")
min_price = safe_float(get_order_prices.get("minPrice"))
max_price = safe_float(get_order_prices.get("maxPrice"))
get_ticker = await get_tickers(tg_id, symbol=symbol)
price_symbol = safe_float(get_ticker.get("lastPrice")) or 0
if order_type == "Conditional":
po_trigger_price = str(trigger_price)
trigger_direction = 1 if trigger_price > price_symbol else 2
if conditional_order_type == "Limit":
error = check_limit_price(limit_price, min_price, max_price)
if error in {
"Limit price is out min price",
"Limit price is out max price",
}:
return error
order_type = "Limit"
price_for_calc = limit_price
tpsl_mode = "Partial"
else:
order_type = "Market"
price_for_calc = trigger_price
tpsl_mode = "Full"
else:
if order_type == "Limit":
error = check_limit_price(limit_price, min_price, max_price)
if error in {
"Limit price is out min price",
"Limit price is out max price",
}:
return error
price_for_calc = limit_price
tpsl_mode = "Partial"
else:
order_type = "Market"
price_for_calc = price_symbol
tpsl_mode = "Full"
po_trigger_price = None
trigger_direction = None
if trade_mode == "Both_Sides":
po_position_idx = 1 if side == "Buy" else 2
if margin_type == "ISOLATED_MARGIN":
get_leverage = safe_float(
leverage_to_buy if side == "Buy" else leverage_to_sell
)
else:
get_leverage = safe_float(leverage)
else:
po_position_idx = 0
get_leverage = safe_float(leverage)
potential_loss = (
safe_float(order_quantity)
* safe_float(price_for_calc)
* (stop_loss_percent / 100)
)
adjusted_loss = potential_loss / get_leverage
allowed_loss = safe_float(user_balance) * (max_risk_percent / 100)
if adjusted_loss > allowed_loss:
return "Risk is too high for this trade"
# Get fee rates
fee_info = client.get_fee_rates(category="linear", symbol=symbol)
# Check if commission fee is enabled
commission_fee_percent = 0.0
if commission_fee == "Yes_commission_fee":
commission_fee_percent = safe_float(
fee_info["result"]["list"][0]["takerFeeRate"]
)
total_commission = price_for_calc * order_quantity * commission_fee_percent
tp_multiplier = 1 + (take_profit_percent / 100)
if total_commission > 0:
tp_multiplier += total_commission
if margin_type == "ISOLATED_MARGIN":
liq_long, liq_short = await get_liquidation_price(
tg_id=tg_id,
entry_price=price_for_calc,
symbol=symbol,
leverage=get_leverage,
)
if (liq_long > 0 or liq_short > 0) and price_for_calc > 0:
if side == "Buy":
base_tp = price_for_calc + (price_for_calc - liq_long)
take_profit_price = base_tp + total_commission
else:
base_tp = price_for_calc - (liq_short - price_for_calc)
take_profit_price = base_tp - total_commission
take_profit_price = max(take_profit_price, 0)
else:
take_profit_price = None
stop_loss_price = None
else:
if side == "Buy":
take_profit_price = price_for_calc * tp_multiplier
stop_loss_price = price_for_calc * (1 - stop_loss_percent / 100)
else:
take_profit_price = price_for_calc * (
1 - (take_profit_percent / 100) - total_commission
)
stop_loss_price = price_for_calc * (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": order_type,
"qty": str(order_quantity),
"triggerDirection": trigger_direction,
"triggerPrice": po_trigger_price,
"triggerBy": "LastPrice",
"timeInForce": "GTC",
"positionIdx": po_position_idx,
"tpslMode": tpsl_mode,
"takeProfit": str(take_profit_price) if take_profit_price else None,
"stopLoss": str(stop_loss_price) if stop_loss_price else None,
}
if order_type == "Conditional":
if conditional_order_type == "Limit":
order_params["price"] = str(limit_price)
if order_type == "Limit":
order_params["price"] = str(limit_price)
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",
}
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)
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.get_functions.get_tickers import get_tickers
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)
get_tickers_info = await get_tickers(tg_id=tg_id, symbol=symbol)
price_symbol = get_tickers_info.get("lastPrice") or 0
await message.answer(
text=f"💎Ваш профиль Bybit:\n\n"
f"⚖️ Баланс: {float(balance):,.2f} USD\n"
f"📊Торговая пара: {symbol}\n"
f"$$$ Цена: {float(price_symbol):,.4f}\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,237 @@
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
from app.helper_functions import format_value, safe_float, safe_int
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"))
order_type = format_value(order_data.get("orderType"))
order_type_rus = (
"Рыночный"
if order_type == "Market"
else "Лимитный" if order_type == "Limit" else "Нет данных"
)
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"))
position_idx = safe_int(order_data.get("positionIdx"))
position_idx_rus = (
"Односторонний"
if position_idx == 0
else (
"Покупка в режиме хеджирования"
if position_idx == 1
else (
"Продажа в режиме хеджирования"
if position_idx == 2
else "Нет данных"
)
)
)
status_map = {
"New": "Ордер создан",
"Cancelled": "Ордер отменен",
"Deactivated": "Ордер деактивирован",
"Untriggered": "Условный ордер выставлен",
}
if order_status == "Filled" or order_status not in status_map:
return None
status_text = status_map[order_status]
text = (
f"{status_text}:\n"
f"Торговая пара: {symbol}\n"
f"Режим позиции: {position_idx_rus}\n"
f"Количество: {qty}\n"
f"Тип ордера: {order_type_rus}\n"
f"Движение: {side_rus}\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_fee = format_value(execution.get("execFee"))
exec_qty = format_value(execution.get("execQty"))
order_type = format_value(execution.get("orderType"))
order_type_rus = (
"Рыночный"
if order_type == "Market"
else "Лимитный" if order_type == "Limit" else "Нет данных"
)
side = format_value(execution.get("side"))
side_rus = (
"Покупка"
if side == "Buy"
else "Продажа" if side == "Sell" else "Нет данных"
)
if safe_float(closed_size) == 0:
await rq.set_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, side=side, fee=safe_float(exec_fee)
)
if side == "Buy":
res_side = "Sell"
else:
res_side = "Buy"
user_auto_trading = await rq.get_user_auto_trading(
tg_id=tg_id, symbol=symbol, side=res_side
)
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"
if safe_float(closed_size) > 0:
text += f"Количество закрытых сделок: {closed_size}\n"
text += (
f"Цена исполнения: {exec_price}\n"
f"Количество исполненных сделок: {exec_qty}\n"
f"Тип ордера: {order_type_rus}\n"
f"Движение: {side_rus}\n"
f"Комиссия за сделку: {exec_fee}\n"
)
if safe_float(closed_size) > 0:
text += f"\nРеализованная прибыль: {total_pnl:.7f}\n"
await self.telegram_bot.send_message(
chat_id=tg_id, text=text, reply_markup=kbi.profile_bybit
)
auto_trading = (
user_auto_trading.auto_trading if user_auto_trading else False
)
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_auto_trading(
tg_id=tg_id, symbol=symbol, auto_trading=False, side=r_side
)
user_deals_data = await rq.get_user_deal_by_symbol(
tg_id=tg_id, symbol=symbol
)
if user_deals_data and user_deals_data.switch_side_mode:
await rq.set_auto_trading(
tg_id=tg_id, symbol=symbol, auto_trading=False, side=side
)
else:
open_order_text = "\n❗️ Сделка закрылась в минус, открываю новую сделку с увеличенной ставкой.\n"
await self.telegram_bot.send_message(
chat_id=tg_id, text=open_order_text
)
res = await trading_cycle(
tg_id=tg_id, symbol=symbol, reverse_side=side, size=closed_size
)
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, "❗️ Не удалось открыть новую сделку"
)
if side == "Buy":
r_side = "Sell"
else:
r_side = "Buy"
await rq.set_auto_trading(
tg_id=tg_id, symbol=symbol, auto_trading=False, side=r_side
)
user_deals_data = await rq.get_user_deal_by_symbol(
tg_id=tg_id, symbol=symbol
)
if user_deals_data and user_deals_data.switch_side_mode:
await rq.set_auto_trading(
tg_id=tg_id,
symbol=symbol,
auto_trading=False,
side=side,
)
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)

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

@@ -0,0 +1,120 @@
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()

186
app/helper_functions.py Normal file
View File

@@ -0,0 +1,186 @@
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, commission_fee_percent
) -> 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.
commission_fee_percent (float): The commission fee percentage.
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)
if commission_fee_percent == 0:
# Commission fee is not added to the position size
r_quantity = set_quantity
else:
# Commission fee is added to the position size
r_quantity = set_quantity * (1 + 2 * commission_fee_percent)
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,896 +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("orderQty", 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:
text = (
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"
)
return text
if order_type == "Market":
text = (
f"Сделка открыта:\n"
f"Торговая пара: {symbol}\n"
f"Цена исполнения: {entry_price:.6f}\n"
f"Количество: {qty}\n"
f"Тип ордера: {order_type}\n"
f"Движение: {movement}\n"
f"Комиссия за сделку: {commission:.6f}"
)
return text
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))
order_status = msg.get("orderStatus", "N/A")
symbol = msg.get("symbol", "N/A")
order_type = msg.get("orderType", "N/A")
side = msg.get("side", "")
trigger_price = msg.get("triggerPrice", "N/A")
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"Комиссия за сделку: {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"
)
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"
)
return text
elif order_status.lower() == "untriggered":
text = (
f"Условный ордер создан:\n"
f"Торговая пара: {symbol}\n"
f"Цена: {price:.6f}\n"
f"Триггер цена: {trigger_price}\n"
f"Количество: {qty}\n"
f"Тип ордера: {order_type}\n"
f"Движение: {movement}\n"
)
return text
elif order_status.lower() == "deactivated":
text = (
f"Условный ордер отменен:\n"
f"Торговая пара: {symbol}\n"
f"Цена: {price:.6f}\n"
f"Триггер цена: {trigger_price}\n"
f"Количество: {qty}\n"
f"Тип ордера: {order_type}\n"
f"Движение: {movement}\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):
"""
Вычисляет общий бюджет серии ставок с учётом цены пары, комиссии и кредитного плеча.
Параметры:
- starting_quantity_usdt: стартовый размер ставки в долларах (USD)
- martingale_factor: множитель увеличения ставки при каждом проигрыше
- max_steps: максимальное количество шагов удвоения ставки
- commission_fee_percent: процент комиссии на одну операцию (открытие или закрытие)
- leverage: кредитное плечо
- current_price: текущая цена актива (например BTCUSDT)
Возвращает:
- общий бюджет в долларах, который необходимо иметь на счету
"""
total = 0
for step in range(max_steps):
base_quantity = starting_quantity * (martingale_factor ** step)
if commission_fee_percent == 0:
# Комиссия уже включена в сумму ставки, поэтому реальный размер позиции меньше
quantity = base_quantity / (1 + commission_fee_percent)
else:
# Комиссию добавляем сверху
quantity = base_quantity * (1 + commission_fee_percent)
total += quantity
return total
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 data is not None:
await rq.update_trigger_price(tg_id, 0.0)
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 trading_mode == 'Switch':
side = data_main_stgs.get("last_side")
else:
side = "Buy" if trading_mode == "Long" else "Sell"
if pnl < 0:
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 rq.update_starting_quantity(tg_id=tg_id, num=next_quantity)
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, 1)
num = data_main_stgs.get("base_quantity")
await rq.update_starting_quantity(tg_id=tg_id, num=num)
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)}")
data = msg.get("data", [{}])[0]
tg_id = message.from_user.id
if data is not None:
await rq.update_trigger_price(tg_id, 0.0)
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"
)
trigger_price = await rq.get_trigger_price(tg_id)
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")
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
if commission_fee_percent > 0:
# Добавляем к тейк-профиту процент комиссии
tp_multiplier = 1 + (loss_profit / 100) + commission_fee_percent
else:
tp_multiplier = 1 + (loss_profit / 100)
if order_type == "Limit" and limit_price:
price_for_calc = limit_price
else:
price_for_calc = entry_price
balance = await balance_g.get_balance(tg_id, message)
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_to_set} 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":
# Открываем позицию
if trigger_price and float(trigger_price) > 0:
response = client.place_order(
category="linear",
symbol=symbol,
side=side,
orderType="Stop" if order_type == "Conditional" else order_type,
qty=str(quantity),
price=(str(limit_price) if order_type == "Limit" and limit_price else None),
triggerPrice=str(trigger_price),
triggerBy="LastPrice",
triggerDirection=2 if side == "Buy" else 1,
timeInForce="GTC",
orderLinkId=f"deal_{symbol}_{int(time.time())}",
)
else:
# Обычный ордер, без триггера
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:
return True
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":
base_tp = avg_price + (avg_price - liq_price)
take_profit_price = base_tp * (1 + commission_fee_percent)
else:
base_tp = avg_price - (liq_price - avg_price)
take_profit_price = base_tp * (1 - commission_fee_percent)
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
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 * tp_multiplier
stop_loss_price = base_price * (1 - loss_profit / 100)
else:
take_profit_price = base_price * (1 - (loss_profit / 100) - (commission_fee_percent))
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,
triggerPrice=str(trigger_price) if trigger_price and float(trigger_price) > 0 else None,
triggerBy="LastPrice" if trigger_price and float(trigger_price) > 0 else None,
triggerDirection=2 if side == "Buy" else 1 if trigger_price and float(trigger_price) > 0 else None,
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" and "Market"
)
orders = response.get("result", {}).get("list", [])
limit_orders = [order for order in orders]
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 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('triggerPrice')}\n"
f"Количество: {order.get('qty')}\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,540 +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
# import app.services.Bybit.functions.balance as balance_g
from app.states.States import (state_update_symbol,
SetTP_SL_State, CloseTradeTimerState)
from aiogram.fsm.context import FSMContext
from app.services.Bybit.functions.get_valid_symbol import get_valid_symbols
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("functions")
router_functions_bybit_trade = Router()
user_trade_tasks = {}
@router_functions_bybit_trade.callback_query(F.data.in_(['clb_start_trading', 'clb_back_to_main', 'back_to_main']))
async def clb_start_bybit_trade_message(callback: CallbackQuery) -> None:
"""
Обработка нажатия кнопок запуска торговли или возврата в главное меню.
Отправляет информацию о балансе, символе, цене и инструкциях по торговле.
"""
user_id = callback.from_user.id
balance = await get_balance(user_id, callback.message)
if balance:
symbol = await rq.get_symbol(user_id)
price = await get_price(user_id, symbol=symbol)
text = (
f"💎 Торговля на Bybit\n\n"
f"⚖️ Ваш баланс (USDT): {float(balance):.2f}\n"
f"📊 Текущая торговая пара: {symbol}\n"
f"$$$ Цена: {price}\n\n"
"Как начать торговлю?\n\n"
"1⃣ Проверьте и тщательно настройте все параметры в вашем профиле.\n"
"2⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).\n"
"3⃣ Нажмите кнопку 'Начать торговать'.\n"
)
await callback.message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup)
async def start_bybit_trade_message(message: Message) -> None:
"""
Отправляет пользователю информацию о балансе, символе и текущей цене,
вместе с инструкциями по началу торговли.
"""
balance = await get_balance(message.from_user.id, message)
tg_id = message.from_user.id
if balance:
await run_ws_for_user(tg_id, message)
symbol = await rq.get_symbol(message.from_user.id)
price = await get_price(message.from_user.id, symbol=symbol)
text = (
f"💎 Торговля на Bybit\n\n"
f"⚖️ Ваш баланс (USDT): {float(balance):.2f}\n"
f"📊 Текущая торговая пара: {symbol}\n"
f"$$$ Цена: {price}\n\n"
"Как начать торговлю?\n\n"
"1⃣ Проверьте и тщательно настройте все параметры в вашем профиле.\n"
"2⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).\n"
"3⃣ Нажмите кнопку 'Начать торговать'.\n"
)
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup)
@router_functions_bybit_trade.callback_query(F.data == 'clb_update_trading_pair')
async def update_symbol_for_trade_message(callback: CallbackQuery, state: FSMContext) -> None:
"""
Начинает процедуру обновления торговой пары, переводит пользователя в состояние ожидания пары.
"""
await state.set_state(state_update_symbol.symbol)
await callback.answer()
await callback.message.answer(
text='Укажите торговую пару заглавными буквами без пробелов и лишних символов (пример: BTCUSDT): ',
reply_markup=inline_markup.cancel)
@router_functions_bybit_trade.message(state_update_symbol.symbol)
async def update_symbol_for_trade(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод торговой пары пользователем и проверяет её валидность.
При успешном обновлении сохранит пару и отправит обновлённую информацию.
"""
user_input = message.text.strip().upper()
exists = await get_valid_symbols(message.from_user.id, user_input)
if not exists:
await message.answer("Введена некорректная торговая пара или такой пары нет в списке. Попробуйте снова.")
return
await state.update_data(symbol=message.text)
await message.answer('Пара была успешно обновлена')
await rq.update_symbol(message.from_user.id, user_input)
await start_bybit_trade_message(message)
await state.clear()
@router_functions_bybit_trade.callback_query(F.data == "clb_start_chatbot_trading")
async def start_trading_process(callback: CallbackQuery) -> None:
"""
Запускает торговый цикл в выбранном режиме Long/Short.
Проверяет API-ключи, режим торговли, маржинальный режим и открытые позиции,
затем запускает торговый цикл с задержкой или без неё.
"""
await callback.answer()
tg_id = callback.from_user.id
message = callback.message
data_main_stgs = await rq.get_user_main_settings(tg_id)
# data_risk_stgs = await rq.get_user_risk_management_settings(tg_id)
# client = await get_bybit_client(tg_id)
symbol = await rq.get_symbol(tg_id)
margin_mode = data_main_stgs.get('margin_type', 'Isolated')
trading_mode = data_main_stgs.get('trading_mode')
starting_quantity = safe_float(data_main_stgs.get('starting_quantity'))
switch_state = data_main_stgs.get("switch_state", "По направлению")
# martingale_factor = safe_float(data_main_stgs.get('martingale_factor'))
# max_martingale_steps = int(data_main_stgs.get("maximal_quantity", 0))
# commission_fee = data_risk_stgs.get("commission_fee")
# fee_info = client.get_fee_rates(category='linear', symbol=symbol)
if trading_mode == 'Switch':
if switch_state == "По направлению":
side = data_main_stgs.get("last_side")
else:
side = data_main_stgs.get("last_side")
if side.lower() == "buy":
side = "Sell"
else:
side = "Buy"
else:
if trading_mode == 'Long':
side = 'Buy'
elif trading_mode == 'Short':
side = 'Sell'
else:
await message.answer(f"Режим торговли '{trading_mode}' пока не поддерживается.",
reply_markup=inline_markup.back_to_main)
return
# 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,
# )
# 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
await message.answer("Начинаю торговлю с использованием текущих настроек...")
await rq.update_trigger(tg_id=tg_id, trigger="Автоматический")
timer_data = await rq.get_user_timer(tg_id)
if isinstance(timer_data, dict):
timer_minute = timer_data.get('timer_minutes', 0)
else:
timer_minute = timer_data or 0
if timer_minute > 0:
await message.answer(f"Торговля начнётся через {timer_minute} мин.", reply_markup=inline_markup.cancel_start)
async def delay_start():
try:
await asyncio.sleep(timer_minute * 60)
await open_position(tg_id, message, side, margin_mode, symbol=symbol, quantity=starting_quantity)
await rq.update_user_timer(tg_id, minutes=0)
except asyncio.exceptions.CancelledError:
logger.exception(f"Торговый цикл для пользователя {tg_id} был отменён.")
raise
task = asyncio.create_task(delay_start())
user_trade_tasks[tg_id] = task
else:
await open_position(tg_id, message, side, margin_mode, symbol=symbol, quantity=starting_quantity)
@router_functions_bybit_trade.callback_query(F.data == "clb_cancel_start")
async def cancel_start_trading(callback: CallbackQuery):
tg_id = callback.from_user.id
task = user_trade_tasks.get(tg_id)
if task and not task.done():
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
user_trade_tasks.pop(tg_id, None)
await rq.update_user_timer(tg_id, minutes=0)
await rq.update_trigger(tg_id, "Ручной")
await callback.message.answer("Запуск торговли отменён.", reply_markup=inline_markup.back_to_main)
await callback.message.edit_reply_markup(reply_markup=None)
else:
await callback.answer("Нет запланированной задачи запуска.", show_alert=True)
@router_functions_bybit_trade.callback_query(F.data == "clb_my_deals")
async def show_my_trades(callback: CallbackQuery) -> None:
"""
Отображает пользователю выбор типа сделки по текущей торговой паре.
"""
await callback.answer()
try:
await callback.message.answer("Выберите тип сделки:",
reply_markup=inline_markup.my_deals_select_markup)
except Exception as e:
logger.error("Произошла ошибка при выборе типа сделки: %s", e)
@router_functions_bybit_trade.callback_query(F.data == "clb_open_deals")
async def show_my_trades_callback(callback: CallbackQuery):
"""
Показывает открытые позиции пользователя.
"""
await callback.answer()
try:
await get_active_positions(callback.from_user.id, message=callback.message)
except Exception as e:
logger.error("Произошла ошибка при выборе сделки: %s", e)
await callback.message.answer("Произошла ошибка при выборе сделки", reply_markup=inline_markup.back_to_main)
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("show_deal_"))
async def show_deal_callback(callback_query: CallbackQuery) -> None:
"""
Показывает сделку пользователя по символу.
"""
await callback_query.answer()
try:
symbol = callback_query.data[len("show_deal_"):]
await rq.update_symbol(callback_query.from_user.id, symbol)
tg_id = callback_query.from_user.id
await get_active_positions_by_symbol(tg_id, symbol, message=callback_query.message)
except Exception as e:
logger.error("Произошла ошибка при выборе сделки: %s", e)
await callback_query.message.answer("Произошла ошибка при выборе сделки",
reply_markup=inline_markup.back_to_main)
@router_functions_bybit_trade.callback_query(F.data == "clb_open_orders")
async def show_my_orders_callback(callback: CallbackQuery) -> None:
"""
Показывает открытые позиции пользователя по символу.
"""
await callback.answer()
try:
await get_active_orders(callback.from_user.id, message=callback.message)
except Exception as e:
logger.error("Произошла ошибка при выборе ордера: %s", e)
await callback.message.answer("Произошла ошибка при выборе ордера", reply_markup=inline_markup.back_to_main)
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("show_limit_"))
async def show_limit_callback(callback_query: CallbackQuery) -> None:
"""
Показывает сделку пользователя по символу.
"""
await callback_query.answer()
try:
symbol = callback_query.data[len("show_limit_"):]
await rq.update_symbol(callback_query.from_user.id, symbol)
tg_id = callback_query.from_user.id
await get_active_orders_by_symbol(tg_id, symbol, message=callback_query.message)
except Exception as e:
logger.error("Произошла ошибка при выборе сделки: %s", e)
await callback_query.message.answer("Произошла ошибка при выборе сделки",
reply_markup=inline_markup.back_to_main)
@router_functions_bybit_trade.callback_query(F.data == "clb_set_tp_sl")
async def set_tp_sl(callback: CallbackQuery, state: FSMContext) -> None:
"""
Запускает процесс установки Take Profit и Stop Loss.
"""
await callback.answer()
await state.set_state(SetTP_SL_State.waiting_for_take_profit)
await callback.message.answer("Введите значение Take Profit (в цене, например 26000.5):",
reply_markup=inline_markup.cancel)
@router_functions_bybit_trade.message(SetTP_SL_State.waiting_for_take_profit)
async def process_take_profit(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод значения Take Profit и запрашивает Stop Loss.
"""
try:
tp = float(message.text.strip())
if tp <= 0:
await message.answer("Значение Take Profit должно быть положительным числом. Попробуйте снова.",
reply_markup=inline_markup.cancel)
return
except ValueError:
await message.answer("Некорректный ввод. Пожалуйста, введите число для Take Profit.",
reply_markup=inline_markup.cancel)
return
await state.update_data(take_profit=tp)
await state.set_state(SetTP_SL_State.waiting_for_stop_loss)
await message.answer("Введите значение Stop Loss (в цене, например 24500.3):", reply_markup=inline_markup.cancel)
@router_functions_bybit_trade.message(SetTP_SL_State.waiting_for_stop_loss)
async def process_stop_loss(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод значения Stop Loss и завершает процесс установки TP/SL.
"""
try:
sl = float(message.text.strip())
if sl <= 0:
await message.answer("Значение Stop Loss должно быть положительным числом. Попробуйте снова.",
reply_markup=inline_markup.cancel)
return
except ValueError:
await message.answer("Некорректный ввод. Пожалуйста, введите число для Stop Loss.",
reply_markup=inline_markup.cancel)
return
data = await state.get_data()
tp = data.get("take_profit")
if tp is None:
await message.answer("Ошибка, не найдено значение Take Profit. Попробуйте снова.")
await state.clear()
return
tg_id = message.from_user.id
await set_take_profit_stop_loss(tg_id, message, take_profit_price=tp, stop_loss_price=sl)
await state.clear()
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_deal:"))
async def close_trade_callback(callback: CallbackQuery) -> None:
"""
Закрывает сделку пользователя по символу.
"""
symbol = callback.data.split(':')[1]
tg_id = callback.from_user.id
result = await close_user_trade(tg_id, symbol)
if result:
logger.info(f"Сделка {symbol} успешно закрыта.")
else:
logger.error(f"Не удалось закрыть сделку {symbol}.")
await callback.message.answer(f"Не удалось закрыть сделку {symbol}.")
await callback.answer()
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_limit:"))
async def close_trade_callback(callback: CallbackQuery) -> None:
"""
Закрывает ордера пользователя по символу.
"""
symbol = callback.data.split(':')[1]
tg_id = callback.from_user.id
result = await cancel_all_tp_sl_orders(tg_id, symbol)
if result:
logger.info(f"Ордер {result} успешно закрыт.")
else:
await callback.message.answer(f"Не удалось закрыть ордер {result}.")
await callback.answer()
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_deal_by_timer:"))
async def ask_close_delay(callback: CallbackQuery, state: FSMContext) -> None:
"""
Запускает диалог с пользователем для задания задержки перед закрытием сделки.
"""
symbol = callback.data.split(":")[1]
await state.update_data(symbol=symbol)
await state.set_state(CloseTradeTimerState.waiting_for_delay)
await callback.message.answer("Введите задержку в минутах до закрытия сделки:",
reply_markup=inline_markup.cancel)
await callback.answer()
@router_functions_bybit_trade.message(CloseTradeTimerState.waiting_for_delay)
async def process_close_delay(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод закрытия сделки с задержкой.
"""
try:
delay_minutes = int(message.text.strip())
if delay_minutes <= 0:
await message.answer("Введите положительное число.")
return
except ValueError:
await message.answer("Некорректный ввод. Введите число в минутах.")
return
data = await state.get_data()
symbol = data.get("symbol")
delay = delay_minutes * 60
await message.answer(f"Закрытие сделки {symbol} запланировано через {delay_minutes} мин.",
reply_markup=inline_markup.back_to_main)
await close_trade_after_delay(message.from_user.id, message, symbol, delay)
await state.clear()
@router_functions_bybit_trade.callback_query(F.data == "clb_change_martingale_reset")
async def reset_martingale(callback: CallbackQuery) -> None:
"""
Сбрасывает шаги мартингейла пользователя.
"""
tg_id = callback.from_user.id
await rq.update_martingale_step(tg_id, 1)
await callback.answer("Сброс шагов выполнен.")
await main_settings_message(tg_id, callback.message)
@router_functions_bybit_trade.callback_query(F.data == "clb_stop_trading")
async def confirm_stop_trading(callback: CallbackQuery):
"""
Предлагает пользователю выбрать вариант подтверждение остановки торговли.
"""
await callback.message.answer(
"Выберите вариант остановки торговли:", reply_markup=inline_markup.stop_choice_markup
)
await callback.answer()
@router_functions_bybit_trade.callback_query(F.data == "stop_immediately")
async def stop_immediately(callback: CallbackQuery):
"""
Останавливает торговлю немедленно.
"""
tg_id = callback.from_user.id
symbol = await rq.get_symbol(tg_id)
await close_user_trade(tg_id, symbol)
await rq.update_trigger(tg_id, "Ручной")
await rq.update_martingale_step(tg_id, 1)
await callback.message.answer("Торговля остановлена.", reply_markup=inline_markup.back_to_main)
await callback.answer()
@router_functions_bybit_trade.callback_query(F.data == "stop_with_timer")
async def stop_with_timer_start(callback: CallbackQuery, state: FSMContext):
"""
Запускает диалог с пользователем для задания задержки до остановки торговли.
"""
await state.set_state(CloseTradeTimerState.waiting_for_trade)
await callback.message.answer("Введите задержку в минутах до остановки торговли:",
reply_markup=inline_markup.cancel)
await callback.answer()
@router_functions_bybit_trade.message(CloseTradeTimerState.waiting_for_trade)
async def process_stop_delay(message: Message, state: FSMContext):
"""
Обрабатывает ввод задержки и запускает задачу остановки торговли с задержкой.
"""
try:
delay_minutes = int(message.text.strip())
if delay_minutes <= 0:
await message.answer("Введите положительное число минут.")
return
except ValueError:
await message.answer("Некорректный формат. Введите число в минутах.")
return
tg_id = message.from_user.id
delay_seconds = delay_minutes * 60
await message.answer(f"Торговля будет остановлена через {delay_minutes} минут.",
reply_markup=inline_markup.back_to_main)
await asyncio.sleep(delay_seconds)
symbol = await rq.get_symbol(tg_id)
await close_user_trade(tg_id, symbol)
await rq.update_trigger(tg_id, "Ручной")
await rq.update_martingale_step(tg_id, 1)
await message.answer("Торговля остановлена.", reply_markup=inline_markup.back_to_main)
await state.clear()
@router_functions_bybit_trade.callback_query(F.data == "clb_cancel")
async def cancel(callback: CallbackQuery, state: FSMContext) -> None:
"""
Отменяет текущее состояние FSM и сообщает пользователю об отмене.
"""
try:
await state.clear()
await callback.message.answer("Отменено!", reply_markup=inline_markup.back_to_main)
await callback.answer()
except Exception as e:
logger.error("Ошибка при обработке отмены: %s", e)

View File

@@ -1,40 +0,0 @@
import logging.config
from pybit.unified_trading import HTTP
import app.telegram.database.requests as rq
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("get_valid_symbol")
async def get_valid_symbols(user_id: int, symbol: str) -> bool:
"""
Проверяет существование торговой пары на Bybit в категории 'linear'.
Эта функция получает API-ключи пользователя из базы данных и
с помощью Bybit API проверяет наличие данного символа в списке
торговых инструментов категории 'linear'.
Args:
user_id (int): Идентификатор пользователя Telegram.
symbol (str): Торговый символ (валютная пара), например "BTCUSDT".
Returns:
bool: Возвращает True, если торговая пара существует, иначе False.
Raises:
Исключения подавляются и вызывается False, если произошла ошибка запроса к API.
"""
api_key = await rq.get_bybit_api_key(user_id)
secret_key = await rq.get_bybit_secret_key(user_id)
client = HTTP(api_key=api_key, api_secret=secret_key)
try:
resp = client.get_instruments_info(category='linear', symbol=symbol)
# Проверка наличия результата и непустого списка инструментов
if resp.get('retCode') == 0 and resp.get('result') and resp['result'].get('list'):
return len(resp['result']['list']) > 0
return False
except Exception as e:
logging.error(f"Ошибка при получении списка инструментов: {e}")
return False

View File

@@ -1,52 +0,0 @@
import math
import logging.config
from app.services.Bybit.functions.price_symbol import get_price
import app.telegram.database.requests as rq
from logger_helper.logger_helper import LOGGING_CONFIG
from pybit.unified_trading import HTTP
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("min_qty")
def round_up_qty(value: float, step: float) -> float:
"""
Округление value вверх до ближайшего кратного step.
"""
return math.ceil(value / step) * step
async def get_min_qty(tg_id: int) -> float:
"""
Получает минимальный объем (количество) ордера для символа пользователя на Bybit,
округленное с учетом шага количества qtyStep.
:param tg_id: int - идентификатор пользователя Telegram
:return: float - минимальное количество лота для ордера
"""
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
symbol = await rq.get_symbol(tg_id)
client = HTTP(api_key=api_key, api_secret=secret_key)
price = await get_price(tg_id, symbol=symbol)
response = client.get_instruments_info(symbol=symbol, category='linear')
instrument = response['result'][0]
lot_size_filter = instrument.get('lotSizeFilter', {})
min_order_qty = float(lot_size_filter.get('minOrderQty', 0))
min_notional_value = float(lot_size_filter.get('minNotionalValue', 0))
qty_step = float(lot_size_filter.get('qtyStep', 1))
calculated_qty = (5 / price) * 1.1
min_qty = max(min_order_qty, calculated_qty)
min_qty_rounded = round_up_qty(min_qty, qty_step)
logger.debug(f"tg_id={tg_id}: price={price}, min_order_qty={min_order_qty}, "
f"min_notional_value={min_notional_value}, qty_step={qty_step}, "
f"calculated_qty={calculated_qty}, min_qty_rounded={min_qty_rounded}")
return min_qty_rounded

View File

@@ -1,32 +0,0 @@
import app.telegram.database.requests as rq
import logging.config
from logger_helper.logger_helper import LOGGING_CONFIG
from pybit import exceptions
from pybit.unified_trading import HTTP
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("price_symbol")
async def get_price(tg_id: int, symbol: str) -> float:
"""
Асинхронно получает текущую цену символа пользователя на Bybit.
:param tg_id: int - ID пользователя Telegram
:return: float - текущая цена символа
"""
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
client = HTTP(
api_key=api_key,
api_secret=secret_key
)
try:
price = float(
client.get_tickers(category='linear', symbol=symbol).get('result').get('list')[0].get('ask1Price'))
return price
except exceptions.InvalidRequestError as e:
logger.error(f"Ошибка при получении цены: {e}")
return 1.0

View File

@@ -1,72 +0,0 @@
from aiogram.fsm.state import State, StatesGroup
class state_update_symbol(StatesGroup):
"""FSM состояние для обновления торгового символа."""
symbol = State()
class state_update_entry_type(StatesGroup):
"""FSM состояние для обновления типа входа."""
entry_type = State()
class TradeSetup(StatesGroup):
"""FSM состояния для настройки торговли с таймером и процентом."""
waiting_for_timer = State()
waiting_for_positive_percent = State()
class state_limit_price(StatesGroup):
"""FSM состояние для установки лимита."""
price = State()
class state_trigger_price(StatesGroup):
"""FSM состояние для установки лимита."""
price = State()
class CloseTradeTimerState(StatesGroup):
"""FSM состояние ожидания задержки перед закрытием сделки."""
waiting_for_delay = State()
waiting_for_trade = State()
class SetTP_SL_State(StatesGroup):
"""FSM состояние для установки TP и SL."""
waiting_for_take_profit = State()
waiting_for_stop_loss = State()
class update_risk_management_settings(StatesGroup):
"""FSM состояние для обновления настроек управления рисками."""
price_profit = State()
price_loss = State()
max_risk_deal = State()
commission_fee = State()
class state_reg_bybit_api(StatesGroup):
"""FSM состояние для регистрации API Bybit."""
api_key = State()
secret_key = State()
class condition_settings(StatesGroup):
"""FSM состояние для настройки условий трейдинга."""
trigger = State()
timer = State()
volatilty = State()
volume = State()
integration = State()
use_tv_signal = State()
class update_main_settings(StatesGroup):
"""FSM состояние для обновления основных настройок."""
trading_mode = State()
size_leverage = State()
margin_type = State()
martingale_factor = State()
starting_quantity = State()
maximal_quantity = State()
switch_mode_enabled = State()

View File

@@ -1,224 +0,0 @@
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
start_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔥 Начать торговлю", callback_data="clb_start_chatbot_message")]
])
settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Запуск", callback_data='clb_start_trading')]
])
cancel_start = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Отменить запуск", callback_data="clb_cancel_start")]
])
back_btn_list_settings = [InlineKeyboardButton(text="Назад",
callback_data='clb_back_to_special_settings_message')] # Кнопка для возврата к списку каталога настроек
back_btn_list_settings_markup = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Назад",
callback_data='clb_back_to_special_settings_message')]]) # Клавиатура для возврата к списку каталога настроек
back_btn_to_main = [InlineKeyboardButton(text="На главную", callback_data='clb_back_to_main')]
back_btn_profile = [InlineKeyboardButton(text="Назад", callback_data='clb_start_chatbot_message')]
connect_bybit_api_message = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api_message')]
])
special_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Основные настройки", callback_data='clb_change_main_settings'),
InlineKeyboardButton(text="Риск-менеджмент", callback_data='clb_change_risk_management_settings')],
[InlineKeyboardButton(text="Условия запуска", callback_data='clb_change_condition_settings')],
# InlineKeyboardButton(text="Дополнительные параметры", callback_data='clb_change_additional_settings')],
[InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api_message')],
back_btn_to_main
])
connect_bybit_api_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api')]
])
trading_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Настройки", callback_data='clb_settings_message')],
[InlineKeyboardButton(text="Мои сделки", callback_data='clb_my_deals')],
[InlineKeyboardButton(text="Указать торговую пару", callback_data='clb_update_trading_pair')],
[InlineKeyboardButton(text="Начать торговать", callback_data='clb_start_chatbot_trading')],
[InlineKeyboardButton(text="Остановить торговлю", callback_data='clb_stop_trading')],
])
start_trading_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Начать торговлю", callback_data="clb_start_chatbot_trading")],
[InlineKeyboardButton(text="На главную", callback_data='back_to_main')],
])
cancel = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Отменить", callback_data="clb_cancel")]
])
entry_order_type_markup = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Маркет", callback_data="entry_order_type:Market"),
InlineKeyboardButton(text="Лимит", callback_data="entry_order_type:Limit"),
], back_btn_to_main
]
)
back_to_main = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="На главную", callback_data='back_to_main')],
])
main_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_trading_mode'),
InlineKeyboardButton(text='Состояние свитча', callback_data='clb_change_switch_state'),
InlineKeyboardButton(text='Тип маржи', callback_data='clb_change_margin_type')],
[InlineKeyboardButton(text='Размер кредитного плеча', callback_data='clb_change_size_leverage'),
InlineKeyboardButton(text='Ставка', callback_data='clb_change_starting_quantity')],
[InlineKeyboardButton(text='Коэффициент Мартингейла', callback_data='clb_change_martingale_factor'),
InlineKeyboardButton(text='Сбросить шаги Мартингейла', callback_data='clb_change_martingale_reset')],
[InlineKeyboardButton(text='Максимальное кол-во ставок', callback_data='clb_change_maximum_quantity')],
back_btn_list_settings,
back_btn_to_main
])
risk_management_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Изм. цены прибыли', callback_data='clb_change_price_profit'),
InlineKeyboardButton(text='Изм. цены убытков', callback_data='clb_change_price_loss')],
[InlineKeyboardButton(text='Макс. риск на сделку', callback_data='clb_change_max_risk_deal')],
[InlineKeyboardButton(text='Учитывать комиссию биржи (Да/Нет)', callback_data='commission_fee')],
back_btn_list_settings,
back_btn_to_main
])
condition_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Таймер', callback_data='clb_change_timer'),
InlineKeyboardButton(text='Тип позиции', callback_data='clb_update_entry_type')],
[InlineKeyboardButton(text='Триггер цена', callback_data='clb_change_trigger_price'),
InlineKeyboardButton(text='Лимит цена', callback_data='clb_change_limit_price')],
#
# [InlineKeyboardButton(text='Фильтр волатильности', callback_data='clb_change_filter_volatility'),
# InlineKeyboardButton(text='Внешние сигналы', callback_data='clb_change_external_cues')],
#
# [InlineKeyboardButton(text='Сигналы TradingView', callback_data='clb_change_tradingview_cues'),
# InlineKeyboardButton(text='Webhook URL', callback_data='clb_change_webhook')],
#
# [InlineKeyboardButton(text='AI - аналитика', callback_data='clb_change_ai_analytics')],
back_btn_list_settings,
back_btn_to_main
])
back_to_condition_settings = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Назад', callback_data='clb_change_condition_settings')],
back_btn_to_main
])
additional_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Сохранить шаблон', callback_data='clb_change_save_pattern'),
InlineKeyboardButton(text='Автозапуск', callback_data='clb_change_auto_start')],
[InlineKeyboardButton(text='Уведомления', callback_data='clb_change_notifications')],
back_btn_list_settings,
back_btn_to_main
])
trading_mode_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Лонг", callback_data="trade_mode_long"),
InlineKeyboardButton(text="Шорт", callback_data="trade_mode_short"),
InlineKeyboardButton(text="Свитч", callback_data="trade_mode_switch")],
# InlineKeyboardButton(text="Смарт", callback_data="trade_mode_smart")],
back_btn_list_settings,
back_btn_to_main
])
margin_type_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Изолированный", callback_data="margin_type_isolated"),
InlineKeyboardButton(text="Кросс", callback_data="margin_type_cross")],
back_btn_list_settings
])
trigger_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE
[InlineKeyboardButton(text='Ручной', callback_data="clb_trigger_manual")],
# [InlineKeyboardButton(text='TradingView', callback_data="clb_trigger_tradingview")],
[InlineKeyboardButton(text="Автоматический", callback_data="clb_trigger_auto")],
back_btn_list_settings,
back_btn_to_main
])
buttons_yes_no_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Да', callback_data="clb_yes"),
InlineKeyboardButton(text='Нет', callback_data="clb_no")],
])
buttons_on_off_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE
[InlineKeyboardButton(text='Включить', callback_data="clb_on"),
InlineKeyboardButton(text='Выключить', callback_data="clb_off")]
])
my_deals_select_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Позиции', callback_data="clb_open_deals"),
InlineKeyboardButton(text='Ордера', callback_data="clb_open_orders")],
back_btn_to_main
])
def create_trades_inline_keyboard(trades):
builder = InlineKeyboardBuilder()
for trade in trades:
builder.button(text=trade, callback_data=f"show_deal_{trade}")
builder.adjust(2)
return builder.as_markup()
def create_trades_inline_keyboard_limits(trades):
builder = InlineKeyboardBuilder()
for trade in trades:
builder.button(text=trade, callback_data=f"show_limit_{trade}")
builder.adjust(2)
return builder.as_markup()
def create_close_deal_markup(symbol: str) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Закрыть сделку", callback_data=f"close_deal:{symbol}")],
[InlineKeyboardButton(text="Закрыть по таймеру", callback_data=f"close_deal_by_timer:{symbol}")],
[InlineKeyboardButton(text="Установить TP/SL", callback_data="clb_set_tp_sl")],
back_btn_to_main
])
def create_close_limit_markup(symbol: str) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Закрыть ордер", callback_data=f"close_limit:{symbol}")],
back_btn_to_main
])
timer_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Установить таймер", callback_data="clb_set_timer")],
[InlineKeyboardButton(text="Удалить таймер", callback_data="clb_delete_timer")],
back_btn_to_main
])
stop_choice_markup = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Остановить сразу", callback_data="stop_immediately"),
InlineKeyboardButton(text="Остановить по таймеру", callback_data="stop_with_timer"),
]
]
)
switch_state_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='По направлению', callback_data="clb_long_switch"),
InlineKeyboardButton(text='Против направления', callback_data="clb_short_switch")],
])

View File

@@ -1,6 +0,0 @@
from aiogram.types import ReplyKeyboardMarkup, KeyboardButton
base_buttons_markup = ReplyKeyboardMarkup(keyboard=[
[KeyboardButton(text="👤 Профиль")],
# [KeyboardButton(text="Настройки")]
], resize_keyboard=True, one_time_keyboard=False)

0
app/telegram/__init__.py Normal file
View File

View File

@@ -1,316 +0,0 @@
from datetime import datetime
import logging.config
from sqlalchemy.sql.sqltypes import DateTime, Numeric
from sqlalchemy import BigInteger, Boolean, Integer, String, ForeignKey
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy.ext.asyncio import AsyncAttrs, async_sessionmaker, create_async_engine
from logger_helper.logger_helper import LOGGING_CONFIG
from sqlalchemy import select, insert
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("models")
engine = create_async_engine(url='sqlite+aiosqlite:///data/db.sqlite3')
async_session = async_sessionmaker(engine)
class Base(AsyncAttrs, DeclarativeBase):
"""Базовый класс для declarative моделей SQLAlchemy с поддержкой async."""
pass
class User_Telegram_Id(Base):
"""
Модель таблицы user_telegram_id.
Хранит идентификаторы Telegram пользователей.
Атрибуты:
id (int): Внутренний первичный ключ записи.
tg_id (int): Уникальный идентификатор пользователя Telegram.
"""
__tablename__ = 'user_telegram_id'
id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(BigInteger)
class User_Bybit_API(Base):
"""
Модель таблицы user_bybit_api.
Хранит API ключи и секреты Bybit для каждого Telegram пользователя.
Атрибуты:
id (int): Внутренний первичный ключ записи.
tg_id (int): Внешний ключ на Telegram пользователя (user_telegram_id.tg_id).
api_key (str): API ключ Bybit (уникальный для пользователя).
secret_key (str): Секретный ключ Bybit (уникальный для пользователя).
"""
__tablename__ = 'user_bybit_api'
id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
api_key = mapped_column(String(18), unique=True, nullable=True)
secret_key = mapped_column(String(36), unique=True, nullable=True)
class User_Symbol(Base):
"""
Модель таблицы user_main_settings.
Хранит основные настройки торговли для пользователя.
"""
__tablename__ = 'user_symbols'
id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
symbol = mapped_column(String(18), default='PENGUUSDT')
class Trading_Mode(Base):
"""
Справочник доступных режимов торговли.
Атрибуты:
id (int): Первичный ключ.
mode (str): Уникальный режим (например, 'Long', 'Short', 'Switch).
"""
__tablename__ = 'trading_modes'
id: Mapped[int] = mapped_column(primary_key=True)
mode = mapped_column(String(10), unique=True)
class Margin_type(Base):
"""
Справочник типов маржинальной торговли.
Атрибуты:
id (int): Первичный ключ.
type (str): Тип маржи (например, 'Isolated', 'Cross').
"""
__tablename__ = 'margin_types'
id: Mapped[int] = mapped_column(primary_key=True)
type = mapped_column(String(15), unique=True)
class Trigger(Base):
"""
Справочник триггеров для сделок.
Атрибуты:
id (int): Первичный ключ.
"""
__tablename__ = 'triggers'
id: Mapped[int] = mapped_column(primary_key=True)
trigger_price = mapped_column(Integer(), default=0)
class User_Main_Settings(Base):
"""
Основные настройки пользователя для торговли.
Атрибуты:
id (int): Первичный ключ.
tg_id (int): Внешний ключ на Telegram пользователя.
trading_mode (str): Режим торговли, FK на trading_modes.mode.
margin_type (str): Тип маржи, FK на margin_types.type.
size_leverage (int): Кредитное плечо.
starting_quantity (int): Начальный объем позиции.
martingale_factor (int): Коэффициент мартингейла.
martingale_step (int): Текущий шаг мартингейла.
maximal_quantity (int): Максимальное число шагов мартингейла.
entry_order_type (str): Тип ордера входа (Market/Limit).
limit_order_price (Optional[str]): Цена лимитного ордера, если есть.
last_side (str): Последняя сторона ордера.
"""
__tablename__ = 'user_main_settings'
id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
trading_mode = mapped_column(ForeignKey("trading_modes.mode"))
margin_type = mapped_column(ForeignKey("margin_types.type"))
switch_state = mapped_column(String(10), default='По направлению')
size_leverage = mapped_column(Integer(), default=1)
starting_quantity = mapped_column(Integer(), default=1)
base_quantity = mapped_column(Integer(), default=1)
martingale_factor = mapped_column(Integer(), default=1)
martingale_step = mapped_column(Integer(), default=1)
maximal_quantity = mapped_column(Integer(), default=10)
entry_order_type = mapped_column(String(10), default='Market')
limit_order_price = mapped_column(Numeric(18, 15), nullable=True, default=0)
trigger_price = mapped_column(Numeric(18, 15), nullable=True, default=0)
last_side = mapped_column(String(10), default='Buy')
trading_start_stop = mapped_column(Integer(), default=0)
class User_Risk_Management_Settings(Base):
"""
Настройки управления рисками пользователя.
Атрибуты:
id (int): Первичный ключ.
tg_id (int): Внешний ключ на Telegram пользователя.
price_profit (int): Процент прибыли для трейда.
price_loss (int): Процент убытка для трейда.
max_risk_deal (int): Максимально допустимый риск по сделке в процентах.
commission_fee (str): Учитывать ли комиссию в расчетах ("Да"/"Нет").
"""
__tablename__ = 'user_risk_management_settings'
id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
price_profit = mapped_column(Integer(), default=1)
price_loss = mapped_column(Integer(), default=1)
max_risk_deal = mapped_column(Integer(), default=100)
commission_fee = mapped_column(String(), default="Да")
class User_Condition_Settings(Base):
"""
Дополнительные пользовательские условия для торговли.
Атрибуты:
id (int): Первичный ключ.
tg_id (int): Внешний ключ на Telegram пользователя.
trigger (str): Тип триггера, FK на triggers.trigger.
filter_time (str): Временной фильтр.
filter_volatility (bool): Фильтр по волатильности.
external_cues (bool): Внешние сигналы.
tradingview_cues (bool): Сигналы TradingView.
webhook (str): URL webhook.
ai_analytics (bool): Использование AI для аналитики.
"""
__tablename__ = 'user_condition_settings'
id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
trigger = mapped_column(String(15), default='Автоматический')
filter_time = mapped_column(String(25), default='???')
filter_volatility = mapped_column(Boolean, default=False)
external_cues = mapped_column(Boolean, default=False)
tradingview_cues = mapped_column(Boolean, default=False)
webhook = mapped_column(String(40), default='')
ai_analytics = mapped_column(Boolean, default=False)
class User_Additional_Settings(Base):
"""
Прочие дополнительные настройки пользователя.
Атрибуты:
id (int): Первичный ключ.
tg_id (int): Внешний ключ на Telegram пользователя.
pattern_save (bool): Сохранять ли шаблоны.
autostart (bool): Автоматический запуск.
notifications (bool): Получение уведомлений.
"""
__tablename__ = 'user_additional_settings'
id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
pattern_save = mapped_column(Boolean, default=False)
autostart = mapped_column(Boolean, default=False)
notifications = mapped_column(Boolean, default=False)
class USER_DEALS(Base):
"""
Таблица сделок пользователя.
Атрибуты:
id (int): Первичный ключ.
tg_id (int): Внешний ключ на Telegram пользователя.
symbol (str): Торговая пара.
side (str): Направление сделки (Buy/Sell).
open_price (int): Цена открытия.
positive_percent (int): Процент доходности.
"""
__tablename__ = 'user_deals'
id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
symbol = mapped_column(String(18), default='PENGUUSDT')
side = mapped_column(String(10), nullable=False)
open_price = mapped_column(Integer(), nullable=False)
positive_percent = mapped_column(Integer(), nullable=False)
class UserTimer(Base):
"""
Таймер пользователя для отсроченного запуска сделок.
Атрибуты:
id (int): Первичный ключ.
tg_id (int): Внешний ключ на Telegram пользователя.
timer_minutes (int): Количество минут таймера.
timer_start (datetime): Время начала таймера.
timer_end (Optional[datetime]): Время окончания таймера (если установлено).
"""
__tablename__ = 'user_timers'
id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
timer_minutes = mapped_column(Integer, nullable=False, default=0)
timer_start = mapped_column(DateTime, default=datetime.utcnow)
timer_end = mapped_column(DateTime, nullable=True)
async def async_main():
"""
Асинхронное создание всех таблиц и заполнение справочников начальными данными.
"""
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
# Заполнение таблиц
modes = ['Long', 'Short', 'Switch', 'Smart']
for mode in modes:
result = await conn.execute(select(Trading_Mode).where(Trading_Mode.mode == mode))
if not result.first():
logger.info("Заполение таблицы режима торговли")
await conn.execute(Trading_Mode.__table__.insert().values(mode=mode))
types = ['Isolated', 'Cross']
for type in types:
result = await conn.execute(select(Margin_type).where(Margin_type.type == type))
if not result.first():
logger.info("Заполение таблицы типов маржи")
await conn.execute(Margin_type.__table__.insert().values(type=type))
last_side = ['Buy', 'Sell']
for side in last_side:
result = await conn.execute(select(User_Main_Settings).where(User_Main_Settings.last_side == side))
if not result.first():
logger.info("Заполение таблицы последнего направления")
await conn.execute(User_Main_Settings.__table__.insert().values(last_side=side))
order_type = ['Limit', 'Market']
for typ in order_type:
result = await conn.execute(select(User_Main_Settings).where(User_Main_Settings.entry_order_type == typ))
if not result.first():
logger.info("Заполение таблицы типов ордеров")
await conn.execute(User_Main_Settings.__table__.insert().values(entry_order_type=typ))

View File

@@ -1,621 +0,0 @@
import logging.config
from logger_helper.logger_helper import LOGGING_CONFIG
from datetime import datetime, timedelta
from typing import Any
from app.telegram.database.models import (
async_session,
User_Telegram_Id as UTi,
User_Main_Settings as UMS,
User_Bybit_API as UBA,
User_Symbol,
User_Risk_Management_Settings as URMS,
User_Condition_Settings as UCS,
User_Additional_Settings as UAS,
Trading_Mode,
Margin_type,
Trigger,
USER_DEALS,
UserTimer,
)
from sqlalchemy import select, update
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("requests")
# --- Функции сохранения в БД ---
async def save_tg_id_new_user(tg_id) -> None:
"""
Сохраняет Telegram ID нового пользователя в базу, если такого ещё нет.
Args:
tg_id (int): Telegram ID пользователя.
"""
async with async_session() as session:
user = await session.scalar(select(UTi).where(UTi.tg_id == tg_id))
if not user:
session.add(UTi(tg_id=tg_id))
logger.info("Новый пользователь был добавлен в бд %s", tg_id)
await session.commit()
async def set_new_user_bybit_api(tg_id) -> None:
"""
Создаёт запись API пользователя Bybit, если её ещё нет.
Args:
tg_id (int): Telegram ID пользователя.
"""
async with async_session() as session:
user = await session.scalar(select(UBA).where(UBA.tg_id == tg_id))
if not user:
session.add(UBA(tg_id=tg_id))
await session.commit()
async def set_new_user_symbol(tg_id) -> None:
"""
Создаёт запись торгового символа пользователя, если её нет.
Args:
tg_id (int): Telegram ID пользователя.
"""
async with async_session() as session:
user = await session.scalar(select(User_Symbol).where(User_Symbol.tg_id == tg_id))
if not user:
session.add(User_Symbol(tg_id=tg_id))
logger.info(f"Symbol был успешно добавлен %s", tg_id)
await session.commit()
async def set_new_user_default_main_settings(tg_id, trading_mode, margin_type) -> None:
"""
Создаёт основные настройки пользователя по умолчанию.
Args:
tg_id (int): Telegram ID пользователя.
trading_mode (str): Режим торговли.
margin_type (str): Тип маржи.
"""
async with async_session() as session:
settings = await session.scalar(select(UMS).where(UMS.tg_id == tg_id))
if not settings:
session.add(UMS(
tg_id=tg_id,
trading_mode=trading_mode,
margin_type=margin_type,
))
logger.info("Основные настройки нового пользователя были заполнены%s", tg_id)
await session.commit()
async def set_new_user_default_risk_management_settings(tg_id) -> None:
"""
Создаёт настройки риск-менеджмента по умолчанию.
Args:
tg_id (int): Telegram ID пользователя.
"""
async with async_session() as session:
settings = await session.scalar(select(URMS).where(URMS.tg_id == tg_id))
if not settings:
session.add(URMS(
tg_id=tg_id
))
logger.info("Риск-Менеджмент настройки нового пользователя были заполнены %s", tg_id)
await session.commit()
async def set_new_user_default_condition_settings(tg_id, trigger) -> None:
"""
Создаёт условные настройки по умолчанию.
Args:
tg_id (int): Telegram ID пользователя.
trigger (Any): Значение триггера по умолчанию.
"""
async with async_session() as session:
settings = await session.scalar(select(UCS).where(UCS.tg_id == tg_id))
if not settings:
session.add(UCS(
tg_id=tg_id,
trigger=trigger
))
logger.info("Условные настройки нового пользователя были заполнены %s", tg_id)
await session.commit()
async def set_new_user_default_additional_settings(tg_id) -> None:
"""
Создаёт дополнительные настройки по умолчанию.
Args:
tg_id (int): Telegram ID пользователя.
"""
async with async_session() as session:
settings = await session.scalar(select(UAS).where(UAS.tg_id == tg_id))
if not settings:
session.add(UAS(
tg_id=tg_id,
))
logger.info("Дополнительные настройки нового пользователя были заполнены %s", tg_id)
await session.commit()
# --- Функции получения данных из БД ---
async def check_user(tg_id):
"""
Проверяет наличие пользователя в базе.
Args:
tg_id (int): Telegram ID пользователя.
Returns:
Optional[UTi]: Пользователь или None.
"""
async with async_session() as session:
user = await session.scalar(select(UTi).where(UTi.tg_id == tg_id))
return user
async def get_bybit_api_key(tg_id):
"""Получить API ключ Bybit пользователя."""
async with async_session() as session:
api_key = await session.scalar(select(UBA.api_key).where(UBA.tg_id == tg_id))
return api_key
async def get_bybit_secret_key(tg_id):
"""Получить секретный ключ Bybit пользователя."""
async with async_session() as session:
secret_key = await session.scalar(select(UBA.secret_key).where(UBA.tg_id == tg_id))
return secret_key
async def get_symbol(tg_id):
"""Получить символ пользователя."""
async with async_session() as session:
symbol = await session.scalar(select(User_Symbol.symbol).where(User_Symbol.tg_id == tg_id))
return symbol
async def get_user_trades(tg_id):
"""Получить сделки пользователя."""
async with async_session() as session:
query = select(USER_DEALS.symbol, USER_DEALS.side).where(USER_DEALS.tg_id == tg_id)
result = await session.execute(query)
trades = result.all()
return trades
async def get_entry_order_type(tg_id: object) -> str | None | Any:
"""Получить тип входного ордера пользователя."""
async with async_session() as session:
order_type = await session.scalar(
select(UMS.entry_order_type).where(UMS.tg_id == tg_id)
)
# Если в базе не установлен тип — возвращаем значение по умолчанию
return order_type or 'Market'
# --- Функции обновления данных ---
async def update_user_trades(tg_id, **kwargs):
"""Обновить сделки пользователя."""
async with async_session() as session:
query = update(USER_DEALS).where(USER_DEALS.tg_id == tg_id).values(**kwargs)
await session.execute(query)
await session.commit()
async def update_symbol(tg_id: int, symbol: str) -> None:
"""Обновить торговый символ пользователя."""
async with async_session() as session:
await session.execute(update(User_Symbol).where(User_Symbol.tg_id == tg_id).values(symbol=symbol))
await session.commit()
async def upsert_api_keys(tg_id: int, api_key: str, secret_key: str) -> None:
"""Обновить API ключ пользователя."""
async with async_session() as session:
result = await session.execute(select(UBA).where(UBA.tg_id == tg_id))
user = result.scalars().first()
if user:
if api_key is not None:
user.api_key = api_key
if secret_key is not None:
user.secret_key = secret_key
logger.info(f"Обновлены ключи для пользователя {tg_id}")
else:
new_user = UBA(tg_id=tg_id, api_key=api_key, secret_key=secret_key)
session.add(new_user)
logger.info(f"Добавлен новый пользователь {tg_id} с ключами")
await session.commit()
# --- Более мелкие обновления и запросы по настройкам ---
async def update_trade_mode_user(tg_id, trading_mode) -> None:
"""Обновить режим торговли пользователя."""
async with async_session() as session:
mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.mode == trading_mode))
if mode:
logger.info("Изменён торговый режим для пользователя %s", tg_id)
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(trading_mode=mode))
await session.commit()
async def delete_user_trade(tg_id: int, symbol: str):
"""Удалить сделку пользователя."""
async with async_session() as session:
await session.execute(
USER_DEALS.__table__.delete().where(
(USER_DEALS.tg_id == tg_id) & (USER_DEALS.symbol == symbol)
)
)
await session.commit()
async def get_for_registration_trading_mode():
"""Получить режим торговли по умолчанию."""
async with async_session() as session:
mode = await session.scalar(select(Trading_Mode.mode).where(Trading_Mode.id == 1))
return mode
async def get_for_registration_margin_type():
"""Получить тип маржи по умолчанию."""
async with async_session() as session:
type = await session.scalar(select(Margin_type.type).where(Margin_type.id == 1))
return type
async def get_for_registration_trigger(tg_id):
"""Получить триггер по умолчанию."""
async with async_session() as session:
trigger = await session.scalar(select(UCS.trigger).where(tg_id == tg_id))
return trigger
async def get_user_main_settings(tg_id):
"""Получить основные настройки пользователя."""
async with async_session() as session:
user = await session.scalar(select(UMS).where(UMS.tg_id == tg_id))
if user:
data = {
'trading_mode': user.trading_mode,
'margin_type': user.margin_type,
'switch_state': user.switch_state,
'size_leverage': user.size_leverage,
'starting_quantity': user.starting_quantity,
'martingale_factor': user.martingale_factor,
'maximal_quantity': user.maximal_quantity,
'entry_order_type': user.entry_order_type,
'limit_order_price': user.limit_order_price,
'trigger_price': user.trigger_price,
'martingale_step': user.martingale_step,
'last_side': user.last_side,
'trading_start_stop': user.trading_start_stop,
'base_quantity': user.base_quantity,
}
return data
async def get_user_risk_management_settings(tg_id):
"""Получить риск-менеджмента настройки пользователя."""
async with async_session() as session:
user = await session.scalar(select(URMS).where(URMS.tg_id == tg_id))
if user:
logger.info("Получение риск-менеджмента настроек пользователя %s", tg_id)
price_profit = await session.scalar(select(URMS.price_profit).where(URMS.tg_id == tg_id))
price_loss = await session.scalar(select(URMS.price_loss).where(URMS.tg_id == tg_id))
max_risk_deal = await session.scalar(select(URMS.max_risk_deal).where(URMS.tg_id == tg_id))
commission_fee = await session.scalar(select(URMS.commission_fee).where(URMS.tg_id == tg_id))
data = {
'price_profit': price_profit,
'price_loss': price_loss,
'max_risk_deal': max_risk_deal,
'commission_fee': commission_fee,
}
return data
async def update_margin_type(tg_id, margin_type) -> None:
"""Обновить тип маржи пользователя."""
async with async_session() as session:
type = await session.scalar(select(Margin_type.type).where(Margin_type.type == margin_type))
if type:
logger.info("Изменен тип маржи %s", tg_id)
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(margin_type=type))
await session.commit()
async def update_size_leverange(tg_id, num):
"""Обновить размер левеража пользователя."""
async with async_session() as session:
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(size_leverage=num))
await session.commit()
async def update_starting_quantity(tg_id, num):
"""Обновить размер начальной ставки пользователя."""
async with async_session() as session:
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(starting_quantity=num))
await session.commit()
async def update_base_quantity(tg_id, num):
"""Обновить размер следующей ставки пользователя."""
async with async_session() as session:
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(base_quantity=num))
await session.commit()
async def update_martingale_factor(tg_id, num):
"""Обновить шаг мартингейла пользователя."""
async with async_session() as session:
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(martingale_factor=num))
await session.commit()
async def update_maximal_quantity(tg_id, num):
"""Обновить размер максимальной ставки пользователя."""
async with async_session() as session:
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(maximal_quantity=num))
await session.commit()
# ОБНОВЛЕНИЕ НАСТРОЕК РИСК-МЕНЕДЖМЕНТА
async def update_price_profit(tg_id, num):
"""Обновить цену тейк-профита (прибыль) пользователя."""
async with async_session() as session:
await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(price_profit=num))
await session.commit()
async def update_price_loss(tg_id, num):
"""Обновить цену тейк-лосса (убыток) пользователя."""
async with async_session() as session:
await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(price_loss=num))
await session.commit()
async def update_max_risk_deal(tg_id, num):
"""Обновить максимальную сумму риска пользователя."""
async with async_session() as session:
await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(max_risk_deal=num))
await session.commit()
async def update_entry_order_type(tg_id, order_type):
"""Обновить тип входного ордера пользователя."""
async with async_session() as session:
await session.execute(
update(UMS)
.where(UMS.tg_id == tg_id)
.values(entry_order_type=order_type)
)
await session.commit()
async def update_trigger_price(tg_id, price):
"""Обновить условную цену пользователя."""
async with async_session() as session:
await session.execute(
update(UMS)
.where(UMS.tg_id == tg_id)
.values(trigger_price=str(price))
)
await session.commit()
async def get_trigger_price(tg_id):
"""Получить условную цену пользователя как float, либо None."""
async with async_session() as session:
result = await session.execute(
select(UMS.trigger_price)
.where(UMS.tg_id == tg_id)
)
price = result.scalar_one_or_none()
if price:
try:
return float(price)
except ValueError:
return None
return None
async def get_limit_price(tg_id):
"""Получить лимитную цену пользователя как float, либо None."""
async with async_session() as session:
result = await session.execute(
select(UMS.limit_order_price)
.where(UMS.tg_id == tg_id)
)
price = result.scalar_one_or_none()
if price:
try:
return float(price)
except ValueError:
return None
return None
async def update_limit_price(tg_id, price):
"""Обновить лимитную цену пользователя."""
async with async_session() as session:
await session.execute(
update(UMS)
.where(UMS.tg_id == tg_id)
.values(limit_order_price=str(price))
)
await session.commit()
async def update_commission_fee(tg_id, num):
"""Обновить комиссию пользователя."""
async with async_session() as session:
await session.execute(update(URMS).where(URMS.tg_id == tg_id).values(commission_fee=num))
await session.commit()
async def get_user_timer(tg_id):
"""Получить данные о таймере пользователя."""
async with async_session() as session:
result = await session.execute(select(UserTimer).where(UserTimer.tg_id == tg_id))
user_timer = result.scalars().first()
if not user_timer:
logging.info(f"No timer found for user {tg_id}")
return None
timer_minutes = user_timer.timer_minutes
timer_start = user_timer.timer_start
timer_end = user_timer.timer_end
logging.info(f"Timer data for tg_id={tg_id}: "
f"timer_minutes={timer_minutes}, "
f"timer_start={timer_start}, "
f"timer_end={timer_end}")
remaining = None
if timer_end:
remaining = max(0, int((timer_end - datetime.utcnow()).total_seconds() // 60))
return {
"timer_minutes": timer_minutes,
"timer_start": timer_start,
"timer_end": timer_end,
"remaining_minutes": remaining
}
async def update_user_timer(tg_id, minutes: int):
"""Обновить данные о таймере пользователя."""
async with async_session() as session:
try:
timer_start = None
timer_end = None
if minutes > 0:
timer_start = datetime.utcnow()
timer_end = timer_start + timedelta(minutes=minutes)
result = await session.execute(select(UserTimer).where(UserTimer.tg_id == tg_id))
user_timer = result.scalars().first()
if user_timer:
user_timer.timer_minutes = minutes
user_timer.timer_start = timer_start
user_timer.timer_end = timer_end
else:
user_timer = UserTimer(
tg_id=tg_id,
timer_minutes=minutes,
timer_start=timer_start,
timer_end=timer_end
)
session.add(user_timer)
await session.commit()
except Exception as e:
logging.error(f"Ошибка обновления таймера пользователя {tg_id}: {e}")
async def get_martingale_step(tg_id):
"""Получить шаг мартингейла пользователя."""
async with async_session() as session:
result = await session.execute(select(UMS).where(UMS.tg_id == tg_id))
user_settings = result.scalars().first()
return user_settings.martingale_step
async def update_martingale_step(tg_id, step):
"""Обновить шаг мартингейла пользователя."""
async with async_session() as session:
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(martingale_step=step))
await session.commit()
async def update_switch_mode_enabled(tg_id, switch_mode):
"""Обновить режим переключения пользователя."""
async with async_session() as session:
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(switch_mode_enabled=switch_mode))
await session.commit()
async def update_switch_state(tg_id, switch_state):
"""Обновить состояние переключения пользователя."""
async with async_session() as session:
await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(switch_state=switch_state))
await session.commit()
async def update_trigger(tg_id, trigger):
"""Обновить триггер пользователя."""
async with async_session() as session:
await session.execute(update(UCS).where(UCS.tg_id == tg_id).values(trigger=trigger))
await session.commit()
async def set_last_series_info(tg_id: int, last_side: str):
async with async_session() as session:
async with session.begin():
# Обновляем запись
result = await session.execute(
update(UMS)
.where(UMS.tg_id == tg_id)
.values(last_side=last_side)
)
if result.rowcount == 0:
# Если запись не существует, создаём новую
new_entry = UMS(
tg_id=tg_id,
last_side=last_side,
)
session.add(new_entry)
await session.commit()

View File

@@ -1,38 +0,0 @@
import app.telegram.Keyboards.inline_keyboards as inline_markup
import app.telegram.database.requests as rq
async def reg_new_user_default_additional_settings(id, message):
tg_id = id
await rq.set_new_user_default_additional_settings(tg_id)
async def main_settings_message(id, message):
text = '''<b>Дополнительные параметры</b>
<b>- Сохранить как шаблон стратегии:</b> да / нет
<b>- Автозапуск после сохранения:</b> да / нет
<b>- Уведомления в Telegram:</b> включено / отключено '''
await message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.additional_settings_markup)
async def save_pattern_message(message, state):
text = '''<b>Сохранение шаблона</b>
Описание... '''
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup)
async def auto_start_message(message, state):
text = '''<b>Автозапуск</b>
Описание... '''
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup)
async def notifications_message(message, state):
text = '''<b>Уведомления</b>
Описание... '''
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_on_off_markup)

View File

@@ -1,208 +0,0 @@
import logging.config
import app.telegram.Keyboards.inline_keyboards as inline_markup
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery
from aiogram.fsm.context import FSMContext
import app.telegram.database.requests as rq
from app.states.States import condition_settings
from app.states.States import state_limit_price, state_update_entry_type, state_trigger_price
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("condition_settings")
condition_settings_router = Router()
async def reg_new_user_default_condition_settings(id):
tg_id = id
trigger = await rq.get_for_registration_trigger(tg_id)
await rq.set_new_user_default_condition_settings(tg_id, trigger)
async def main_settings_message(id, message):
data = await rq.get_user_main_settings(id)
entry_order_type = data['entry_order_type']
if entry_order_type == "Market":
entry_order_type_rus = "Маркет"
elif entry_order_type == "Limit":
entry_order_type_rus = "Лимит"
else:
entry_order_type_rus = "Условный"
trigger_price = data['trigger_price'] or 0.0
limit_price = data['limit_order_price'] or 0.0
text = f""" <b>Условия запуска</b>
<b>- Таймер: </b> установить таймер / удалить таймер
<b>- Тип позиции:</b> {entry_order_type_rus}
<b>- Триггер цена: </b> {trigger_price:,.4f}
<b>- Лимит цена: </b> {limit_price:,.4f}
"""
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.condition_settings_markup)
async def timer_message(id, message: Message, state: FSMContext):
await state.set_state(condition_settings.timer)
timer_info = await rq.get_user_timer(id)
if timer_info is None:
await message.answer("Таймер не установлен.", reply_markup=inline_markup.timer_markup)
return
await message.answer(
f"Таймер установлен на: {timer_info['timer_minutes']} мин\n",
reply_markup=inline_markup.timer_markup
)
@condition_settings_router.callback_query(F.data == "clb_set_timer")
async def set_timer_callback(callback: CallbackQuery, state: FSMContext):
await state.set_state(condition_settings.timer) # состояние для ввода времени
await callback.message.answer("Введите время работы в минутах (например, 60):", reply_markup=inline_markup.cancel)
await callback.answer()
@condition_settings_router.message(condition_settings.timer)
async def process_timer_input(message: Message, state: FSMContext):
try:
minutes = int(message.text)
if minutes <= 0:
await message.reply("Введите число больше нуля.")
return
await rq.update_user_timer(message.from_user.id, minutes)
logger.info("Timer set for user %s: %s minutes", message.from_user.id, minutes)
await timer_message(message.from_user.id, message, state)
await state.clear()
except ValueError:
await message.reply("Пожалуйста, введите корректное число.")
@condition_settings_router.callback_query(F.data == "clb_delete_timer")
async def delete_timer_callback(callback: CallbackQuery, state: FSMContext):
await state.clear()
await rq.update_user_timer(callback.from_user.id, 0)
logger.info("Timer deleted for user %s", callback.from_user.id)
await timer_message(callback.from_user.id, callback.message, state)
await callback.answer()
@condition_settings_router.callback_query(F.data == 'clb_update_entry_type')
async def update_entry_type_message(callback: CallbackQuery, state: FSMContext) -> None:
"""
Запрашивает у пользователя тип входа в позицию (Market или Limit).
"""
await state.set_state(state_update_entry_type.entry_type)
await callback.message.answer("Выберите тип входа в позицию:", reply_markup=inline_markup.entry_order_type_markup)
await callback.answer()
@condition_settings_router.callback_query(lambda c: c.data and c.data.startswith('entry_order_type:'))
async def entry_order_type_callback(callback: CallbackQuery, state: FSMContext) -> None:
"""
Обработка выбора типа входа в позицию.
Если Limit, запрашивает цену лимитного ордера.
Если Market — обновляет настройки.
"""
order_type = callback.data.split(':')[1]
if order_type not in ['Market', 'Limit']:
await callback.answer("Ошибка выбора", show_alert=True)
return
if order_type == 'Limit':
order_type_rus = 'Лимит'
else:
order_type_rus = 'Маркет'
try:
await state.update_data(entry_order_type=order_type)
await rq.update_entry_order_type(callback.from_user.id, order_type)
await callback.message.answer(f"Выбран тип входа {order_type_rus}",
reply_markup=inline_markup.back_to_condition_settings)
await callback.answer()
except Exception as e:
logger.error("Произошла ошибка при обновлении типа входа в позицию: %s", e)
await callback.message.answer("Произошла ошибка при обновлении типа входа в позицию",
reply_markup=inline_markup.back_to_condition_settings)
await state.clear()
@condition_settings_router.callback_query(F.data == 'clb_change_limit_price')
async def set_limit_price_callback(callback: CallbackQuery, state: FSMContext) -> None:
await state.set_state(state_limit_price.price)
await callback.message.answer("Введите цену лимитного ордера:", reply_markup=inline_markup.cancel)
await callback.answer()
@condition_settings_router.message(state_limit_price.price)
async def process_limit_price_input(message: Message, state: FSMContext) -> None:
try:
price = float(message.text)
await state.update_data(price=price)
await rq.update_limit_price(tg_id=message.from_user.id, price=price)
await message.answer(f"Цена лимитного ордера установлена: {price}", reply_markup=inline_markup.back_to_condition_settings)
await state.clear()
except ValueError:
await message.reply("Пожалуйста, введите корректную цену.")
@condition_settings_router.callback_query(F.data == 'clb_change_trigger_price')
async def change_trigger_price_callback(callback: CallbackQuery, state: FSMContext) -> None:
await state.set_state(state_trigger_price.price)
await callback.message.answer("Введите цену триггера:", reply_markup=inline_markup.cancel)
await callback.answer()
@condition_settings_router.message(state_trigger_price.price)
async def process_trigger_price_input(message: Message, state: FSMContext) -> None:
try:
price = float(message.text)
await state.update_data(price=price)
await rq.update_trigger_price(tg_id=message.from_user.id, price=price)
await message.answer(f"Цена триггера установлена: {price}", reply_markup=inline_markup.back_to_condition_settings)
await state.clear()
except ValueError:
await message.reply("Пожалуйста, введите корректную цену.")
async def filter_volatility_message(message, state):
text = '''Фильтр волатильности
Описание... '''
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_on_off_markup)
async def external_cues_message(message, state):
text = '''<b>Внешние сигналы</b>
Описание... '''
await message.answer(text=text, parse_mode='html', reply_markup=None)
async def trading_cues_message(message, state):
text = '''<b>Использование сигналов</b>
Описание... '''
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup)
async def webhook_message(message, state):
text = '''Скиньте ссылку на <b>webhook</b> (если есть trading view): '''
await message.answer(text=text, parse_mode='html')
async def ai_analytics_message(message, state):
text = '''<b>ИИ - Аналитика</b>
Описание... '''
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup)

View File

@@ -1,29 +0,0 @@
import app.telegram.Keyboards.inline_keyboards as inline_markup
import app.telegram.Keyboards.reply_keyboards as reply_markup
async def start_message(message):
username = ''
if message.from_user.first_name == None:
username = message.from_user.last_name
elif message.from_user.last_name == None:
username = message.from_user.first_name
else:
username = f'{message.from_user.first_name} {message.from_user.last_name}'
await message.answer(f""" Привет <b>{username}</b>! 👋""", parse_mode='html')
await message.answer("Добро пожаловать в чат-робот для автоматизации трейдинга — вашего надежного помощника для анализа рынка и принятия взвешенных решений.",
parse_mode='html', reply_markup=inline_markup.start_markup)
async def profile_message(username, message):
await message.answer(f""" <b>@{username}</b>
Баланс
⭐️ 0
""", parse_mode='html', reply_markup=inline_markup.settings_markup)
async def check_profile_message(message, username):
await message.answer(f'С возвращением, {username}!', reply_markup=reply_markup.base_buttons_markup)
async def settings_message(message):
await message.edit_text("Выберите что настроить", reply_markup=inline_markup.special_settings_markup)

View File

@@ -1,348 +0,0 @@
from aiogram import Router
import logging.config
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.Futures import safe_float, calculate_total_budget, get_bybit_client
from app.states.States import update_main_settings
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("main_settings")
router_main_settings = Router()
async def reg_new_user_default_main_settings(id, message):
tg_id = id
trading_mode = await rq.get_for_registration_trading_mode()
margin_type = await rq.get_for_registration_margin_type()
await rq.set_new_user_default_main_settings(tg_id, trading_mode, margin_type)
async def main_settings_message(id, message):
try:
data = await rq.get_user_main_settings(id)
tg_id = id
data_main_stgs = await rq.get_user_main_settings(id)
data_risk_stgs = await rq.get_user_risk_management_settings(id)
client = await get_bybit_client(tg_id)
symbol = await rq.get_symbol(tg_id)
max_martingale_steps = (data_main_stgs or {}).get('maximal_quantity', 0)
commission_fee = (data_risk_stgs or {}).get('commission_fee')
starting_quantity = safe_float((data_main_stgs or {}).get('starting_quantity'))
martingale_factor = safe_float((data_main_stgs or {}).get('martingale_factor'))
fee_info = client.get_fee_rates(category='linear', symbol=symbol)
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,
)
await message.answer(f"""<b>Основные настройки</b>
<b>- Режим торговли:</b> {data['trading_mode']}
<b>- Состояние свитча:</b> {data['switch_state']}
<b>- Направление последней сделки:</b> {data['last_side']}
<b>- Тип маржи:</b> {data['margin_type']}
<b>- Размер кредитного плеча:</b> х{data['size_leverage']}
<b>- Ставка:</b> {data['starting_quantity']}
<b>- Коэффициент мартингейла:</b> {data['martingale_factor']}
<b>- Текущий шаг:</b> {data['martingale_step']}
<b>- Максимальное количество ставок в серии:</b> {data['maximal_quantity']}
<b>- Требуемый бюджет:</b> {total_budget:.2f} USDT
""", parse_mode='html', reply_markup=inline_markup.main_settings_markup)
except PermissionError as e:
logger.error("Authenticated endpoints require keys: %s", e)
await message.answer("Вы не авторизованы.", reply_markup=inline_markup.connect_bybit_api_message)
async def trading_mode_message(message, state):
await state.set_state(update_main_settings.trading_mode)
await message.edit_text("""<b>Режим торговли</b>
<b>Лонг</b> — стратегия, ориентированная на покупку актива с целью заработать на повышении его стоимости.
<b>Шорт</b> — метод продажи активов, взятых в кредит, чтобы получить прибыль от снижения цены.
<b>Свитч</b> — динамическое переключение между торговыми режимами для максимизации эффективности.
<em>Выберите ниже для изменений:</em>
""", parse_mode='html', reply_markup=inline_markup.trading_mode_markup)
@router_main_settings.callback_query(update_main_settings.trading_mode)
async def state_trading_mode(callback: CallbackQuery, state):
await callback.answer()
id = callback.from_user.id
data_settings = await rq.get_user_main_settings(id)
try:
match callback.data:
case 'trade_mode_long':
await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Long")
await rq.update_trade_mode_user(id, 'Long')
await main_settings_message(id, callback.message)
await state.clear()
case 'trade_mode_short':
await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Short")
await rq.update_trade_mode_user(id, 'Short')
await main_settings_message(id, callback.message)
await state.clear()
case 'trade_mode_switch':
await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Switch")
await rq.update_trade_mode_user(id, 'Switch')
await main_settings_message(id, callback.message)
await state.clear()
case 'trade_mode_smart':
await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Smart")
await rq.update_trade_mode_user(id, 'Smart')
await main_settings_message(id, callback.message)
await state.clear()
except Exception as e:
logger.error("Ошибка при обновлении режима торговли: %s", e)
async def switch_mode_enabled_message(message, state):
await state.set_state(update_main_settings.switch_mode_enabled)
await message.edit_text(
f"""<b> Состояние свитча</b>
<b>По направлению</b> - по направлению последней сделки предыдущей серии
<b>Против направления</b> - против направления последней сделки предыдущей серии
<em>По умолчанию при первом запуске бота, направление сделки установлено на "Buy".</em>
<em>Выберите ниже для изменений:</em>""", parse_mode='html',
reply_markup=inline_markup.switch_state_markup)
@router_main_settings.callback_query(lambda c: c.data in ["clb_long_switch", "clb_short_switch"])
async def state_switch_mode_enabled(callback: CallbackQuery, state):
await callback.answer()
tg_id = callback.from_user.id
val = "По направлению" if callback.data == "clb_long_switch" else "Против направления"
if val == "По направлению":
await rq.update_switch_state(tg_id, "По направлению")
await callback.message.answer(f"Состояние свитча: {val}")
await main_settings_message(tg_id, callback.message)
else:
await rq.update_switch_state(tg_id, "Против направления")
await callback.message.answer(f"Состояние свитча: {val}")
await main_settings_message(tg_id, callback.message)
await state.clear()
async def size_leverage_message(message, state):
await state.set_state(update_main_settings.size_leverage)
await message.edit_text("Введите размер <b>кредитного плеча</b> (от 1 до 100): ", parse_mode='html',
reply_markup=inline_markup.back_btn_list_settings_markup)
@router_main_settings.message(update_main_settings.size_leverage)
async def state_size_leverage(message: Message, state):
try:
leverage = float(message.text)
if leverage <= 0:
raise ValueError("Неверное значение")
except ValueError:
await message.answer(
"Ошибка: пожалуйста, введите положительное число для кредитного плеча."
"\nПопробуйте снова."
)
return
await state.update_data(size_leverage=message.text)
data = await state.get_data()
tg_id = message.from_user.id
symbol = await rq.get_symbol(tg_id)
leverage = data['size_leverage']
client = await get_bybit_client(tg_id)
instruments_resp = client.get_instruments_info(category="linear", symbol=symbol)
info = instruments_resp.get("result", {}).get("list", [])
max_leverage = safe_float(info[0].get("leverageFilter", {}).get("maxLeverage", 0))
if safe_float(leverage) > max_leverage:
await message.answer(
f"Запрошенное кредитное плечо {leverage} превышает максимальное {max_leverage} для {symbol}. "
f"Устанавливаю максимальное.",
reply_markup=inline_markup.back_to_main,
)
logger.info(
f"Запрошенное кредитное плечо {leverage} превышает максимальное {max_leverage} для {symbol}. Устанавливаю максимальное.")
await rq.update_size_leverange(message.from_user.id, max_leverage)
await main_settings_message(message.from_user.id, message)
await state.clear()
else:
await message.answer(f"✅ Изменено: {leverage}")
await rq.update_size_leverange(message.from_user.id, safe_float(leverage))
await main_settings_message(message.from_user.id, message)
await state.clear()
async def martingale_factor_message(message, state):
await state.set_state(update_main_settings.martingale_factor)
await message.edit_text("Введите <b>коэффициент Мартингейла:</b>", parse_mode='html',
reply_markup=inline_markup.back_btn_list_settings_markup)
@router_main_settings.message(update_main_settings.martingale_factor)
async def state_martingale_factor(message: Message, state):
await state.update_data(martingale_factor=message.text)
data = await state.get_data()
data_settings = await rq.get_user_main_settings(message.from_user.id)
if data['martingale_factor'].isdigit() and int(data['martingale_factor']) <= 100:
await message.answer(f"✅ Изменено: {data_settings['martingale_factor']}{data['martingale_factor']}")
await rq.update_martingale_factor(message.from_user.id, data['martingale_factor'])
await main_settings_message(message.from_user.id, message)
await state.clear()
else:
val = data['martingale_factor']
await message.answer(
f"⛔️ Ошибка: ваше значение ({val}) или выше лимита (100) или вы вводите неверные символы")
await main_settings_message(message.from_user.id, message)
async def margin_type_message(message, state):
await state.set_state(update_main_settings.margin_type)
await message.edit_text("""<b>Тип маржи</b>
<b>Изолированная маржа</b>
Этот тип маржи позволяет ограничить риск конкретной позиции.
При использовании изолированной маржи вы выделяете определённую сумму средств только для одной позиции.
Если позиция начинает приносить убытки, ваши потери ограничиваются этой суммой,
и остальные средства на счёте не затрагиваются.
<b>Кросс-маржа</b>
Кросс-маржа объединяет весь маржинальный баланс на счёте и использует все доступные средства для поддержания открытых позиций.
В случае убытков средства с других позиций или баланса автоматически покрывают дефицит,
снижая риск ликвидации, но увеличивая общий риск потери капитала.
<em>Выберите ниже для изменений:</em>
""", parse_mode='html', reply_markup=inline_markup.margin_type_markup)
@router_main_settings.callback_query(update_main_settings.margin_type)
async def state_margin_type(callback: CallbackQuery, state):
callback_data = callback.data
if callback_data in ['margin_type_isolated', 'margin_type_cross']:
tg_id = callback.from_user.id
data_settings = await rq.get_user_main_settings(tg_id)
try:
match callback.data:
case 'margin_type_isolated':
await callback.answer()
await callback.message.answer(f"✅ Изменено: {data_settings['margin_type']} → Isolated")
await rq.update_margin_type(tg_id, 'Isolated')
await main_settings_message(tg_id, callback.message)
await state.clear()
case 'margin_type_cross':
await callback.answer()
await callback.message.answer(f"✅ Изменено: {data_settings['margin_type']} → Cross")
await rq.update_margin_type(tg_id, 'Cross')
await main_settings_message(tg_id, callback.message)
await state.clear()
except Exception as e:
logger.error("Ошибка при изменении типа маржи: %s", e)
else:
await callback.answer()
await main_settings_message(callback.from_user.id, callback.message)
await state.clear()
async def starting_quantity_message(message, state):
await state.set_state(update_main_settings.starting_quantity)
await message.edit_text("Введите <b>ставку:</b>", parse_mode='html',
reply_markup=inline_markup.back_btn_list_settings_markup)
@router_main_settings.message(update_main_settings.starting_quantity)
async def state_starting_quantity(message: Message, state):
await state.update_data(starting_quantity=message.text)
data = await state.get_data()
data_settings = await rq.get_user_main_settings(message.from_user.id)
if data['starting_quantity'].isdigit():
await message.answer(f"✅ Изменено: {data_settings['starting_quantity']}{data['starting_quantity']}")
await rq.update_starting_quantity(message.from_user.id, data['starting_quantity'])
await rq.update_base_quantity(tg_id=message.from_user.id, num=data['starting_quantity'])
await main_settings_message(message.from_user.id, message)
await state.clear()
else:
await message.answer("⛔️ Ошибка: вы вводите неверные символы")
await main_settings_message(message.from_user.id, message)
async def maximum_quantity_message(message, state):
await state.set_state(update_main_settings.maximal_quantity)
await message.edit_text("Введите максимальное количество серии ставок:",
reply_markup=inline_markup.back_btn_list_settings_markup)
@router_main_settings.message(update_main_settings.maximal_quantity)
async def state_maximal_quantity(message: Message, state):
await state.update_data(maximal_quantity=message.text)
data = await state.get_data()
data_settings = await rq.get_user_main_settings(message.from_user.id)
if data['maximal_quantity'].isdigit() and int(data['maximal_quantity']) <= 100:
await message.answer(f"✅ Изменено: {data_settings['maximal_quantity']}{data['maximal_quantity']}")
await rq.update_maximal_quantity(message.from_user.id, data['maximal_quantity'])
await main_settings_message(message.from_user.id, message)
await state.clear()
else:
val = data['maximal_quantity']
await message.answer(
f'⛔️ Ошибка: ваше значение ({val}) или выше лимита (100) или вы вводите неверные символы')
logger.error(f'⛔️ Ошибка: ваше значение ({val}) или выше лимита (100) или вы вводите неверные символы')
await main_settings_message(message.from_user.id, message)

View File

@@ -0,0 +1,27 @@
import logging.config
from aiogram.types import Message
import app.telegram.keyboards.reply as kbr
import database.request as rq
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("profile_tg")
async def user_profile_tg(tg_id: int, message: Message) -> None:
try:
user = await rq.get_user(tg_id)
if user:
await message.answer(
text="💎Ваш профиль:\n\n" "⚖️ Баланс: 0\n", reply_markup=kbr.profile
)
else:
await rq.create_user(tg_id=tg_id, username=user.username)
await rq.set_user_symbol(tg_id=tg_id, symbol="BTCUSDT")
await rq.create_user_additional_settings(tg_id=tg_id)
await rq.create_user_risk_management(tg_id=tg_id)
await user_profile_tg(tg_id=tg_id, message=message)
except Exception as e:
logger.error("Error processing user profile: %s", e)

View File

@@ -1,160 +0,0 @@
from aiogram import Router
import app.telegram.Keyboards.inline_keyboards as inline_markup
import logging.config
import app.telegram.database.requests as rq
from aiogram.types import Message, CallbackQuery
from app.states.States import update_risk_management_settings
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("risk_management_settings")
router_risk_management_settings = Router()
async def reg_new_user_default_risk_management_settings(id, message):
tg_id = id
await rq.set_new_user_default_risk_management_settings(tg_id)
async def main_settings_message(id, message):
data = await rq.get_user_risk_management_settings(id)
text = f"""<b>Риск менеджмент</b>,
<b>- Процент изменения цены для фиксации прибыли:</b> {data.get('price_profit', 0)}%
<b>- Процент изменения цены для фиксации убытков:</b> {data.get('price_loss', 0)}%
<b>- Максимальный риск на сделку (в % от баланса):</b> {data.get('max_risk_deal', 0)}%
<b>- Комиссия биржи для расчета прибыли:</b> {data.get('commission_fee', "Да")}
"""
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.risk_management_settings_markup)
async def price_profit_message(message, state):
await state.set_state(update_risk_management_settings.price_profit)
text = 'Введите число изменения цены для фиксации прибыли: '
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.cancel)
@router_risk_management_settings.message(update_risk_management_settings.price_profit)
async def state_price_profit(message: Message, state):
await state.update_data(price_profit=message.text)
data = await state.get_data()
data_settings = await rq.get_user_risk_management_settings(message.from_user.id)
if data['price_profit'].isdigit() and int(data['price_profit']) <= 100:
await message.answer(f"✅ Изменено: {data_settings['price_profit']}% → {data['price_profit']}%")
await rq.update_price_profit(message.from_user.id, data['price_profit'])
await main_settings_message(message.from_user.id, message)
await state.clear()
else:
val = data['price_profit']
await message.answer(
f'⛔️ Ошибка: ваше значение ({val}%) или выше лимита (100) или вы вводите неверные символы')
await main_settings_message(message.from_user.id, message)
async def price_loss_message(message, state):
await state.set_state(update_risk_management_settings.price_loss)
text = 'Введите число изменения цены для фиксации убытков: '
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.cancel)
@router_risk_management_settings.message(update_risk_management_settings.price_loss)
async def state_price_loss(message: Message, state):
await state.update_data(price_loss=message.text)
data = await state.get_data()
data_settings = await rq.get_user_risk_management_settings(message.from_user.id)
if data['price_loss'].isdigit() and int(data['price_loss']) <= 100:
new_price_loss = int(data['price_loss'])
old_price_loss = int(data_settings.get('price_loss', 0))
current_price_profit = data_settings.get('price_profit')
# Пробуем перевести price_profit в число, если это возможно
try:
current_price_profit_num = int(current_price_profit)
except Exception as e:
logger.error(e)
current_price_profit_num = 0
# Флаг, если price_profit изначально равен 0 или совпадает со старым стоп-лоссом
should_update_profit = (current_price_profit_num == 0) or (current_price_profit_num == abs(old_price_loss))
# Обновляем стоп-лосс
await rq.update_price_loss(message.from_user.id, new_price_loss)
# Если нужно, меняем тейк-профит
if should_update_profit:
new_price_profit = abs(new_price_loss)
await rq.update_price_profit(message.from_user.id, new_price_profit)
await message.answer(f"✅ Стоп-лосс изменён: {old_price_loss}% → {new_price_loss}%\n"
f"Тейк-профит автоматически установлен в: {new_price_profit}%")
else:
await message.answer(f"✅ Стоп-лосс изменён: {old_price_loss}% → {new_price_loss}%")
await main_settings_message(message.from_user.id, message)
await state.clear()
else:
val = data['price_loss']
await message.answer(
f'⛔️ Ошибка: ваше значение ({val}%) выше лимита (100) или содержит неверные символы')
await main_settings_message(message.from_user.id, message)
async def max_risk_deal_message(message, state):
await state.set_state(update_risk_management_settings.max_risk_deal)
text = 'Введите число (процент от баланса) для изменения максимального риска на сделку: '
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.cancel)
@router_risk_management_settings.message(update_risk_management_settings.max_risk_deal)
async def state_max_risk_deal(message: Message, state):
await state.update_data(max_risk_deal=message.text)
data = await state.get_data()
data_settings = await rq.get_user_risk_management_settings(message.from_user.id)
if data['max_risk_deal'].isdigit() and int(data['max_risk_deal']) <= 100:
await message.answer(f"✅ Изменено: {data_settings['max_risk_deal']}% → {data['max_risk_deal']}%")
await rq.update_max_risk_deal(message.from_user.id, data['max_risk_deal'])
await main_settings_message(message.from_user.id, message)
await state.clear()
else:
val = data['max_risk_deal']
await message.answer(
f'⛔️ Ошибка: ваше значение ({val}%) или выше лимита (100) или вы вводите неверные символы')
await main_settings_message(message.from_user.id, message)
async def commission_fee_message(message, state):
await state.set_state(update_risk_management_settings.commission_fee)
await message.answer(text="Хотите учитывать комиссию биржи:", parse_mode='html',
reply_markup=inline_markup.buttons_yes_no_markup)
@router_risk_management_settings.callback_query(lambda c: c.data in ["clb_yes", "clb_no"])
async def process_commission_fee_callback(callback: CallbackQuery, state):
val = "Да" if callback.data == "clb_yes" else "Нет"
await rq.update_commission_fee(callback.from_user.id, val)
await callback.message.answer(f"✅ Изменено: {val}")
await callback.answer()
await main_settings_message(callback.from_user.id, callback.message)
await state.clear()

View File

@@ -0,0 +1,32 @@
__all__ = "router"
from aiogram import Router
from app.telegram.handlers.add_bybit_api import router_add_bybit_api
from app.telegram.handlers.changing_the_symbol import router_changing_the_symbol
from app.telegram.handlers.close_orders import router_close_orders
from app.telegram.handlers.common import router_common
from app.telegram.handlers.get_positions_handlers import router_get_positions_handlers
from app.telegram.handlers.handlers_main import router_handlers_main
from app.telegram.handlers.main_settings import router_main_settings
from app.telegram.handlers.settings import router_settings
from app.telegram.handlers.start_trading import router_start_trading
from app.telegram.handlers.stop_trading import router_stop_trading
from app.telegram.handlers.tp_sl_handlers import router_tp_sl_handlers
router = Router(name=__name__)
router.include_router(router_handlers_main)
router.include_router(router_add_bybit_api)
router.include_router(router_settings)
router.include_router(router_main_settings)
router.include_router(router_changing_the_symbol)
router.include_router(router_get_positions_handlers)
router.include_router(router_start_trading)
router.include_router(router_stop_trading)
router.include_router(router_close_orders)
router.include_router(router_tp_sl_handlers)
# Do not add anything below this router
router.include_router(router_common)

View File

@@ -0,0 +1,150 @@
import logging.config
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
import app.telegram.keyboards.inline as kbi
import app.telegram.keyboards.reply as kbr
import database.request as rq
from app.bybit.profile_bybit import user_profile_bybit
from app.telegram.states.states import AddBybitApiState
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("add_bybit_api")
router_add_bybit_api = Router(name="add_bybit_api")
@router_add_bybit_api.callback_query(F.data == "connect_platform")
async def connect_platform(callback: CallbackQuery, state: FSMContext) -> None:
"""
Handles the callback query to initiate Bybit platform connection.
Sends instructions on how to create and provide API keys to the bot.
:param callback: CallbackQuery object triggered by user interaction.
:param state: FSMContext object to manage state data.
:return: None
"""
try:
await state.clear()
await callback.answer()
user = await rq.get_user(tg_id=callback.from_user.id)
if user:
await callback.message.answer(
text=(
"Подключение Bybit аккаунта \n\n"
"1. Зарегистрируйтесь или войдите в свой аккаунт на Bybit по ссылке: "
"[Перейти на Bybit](https://www.bybit.com/invite?ref=YME83OJ).\n"
"2. В личном кабинете выберите раздел API. \n"
"3. Создание нового API ключа\n"
" - Нажмите кнопку Create New Key (Создать новый ключ).\n"
" - Выберите системно-сгенерированный ключ.\n"
" - Укажите название API ключа (любое). \n"
" - Выберите права доступа для торговли (Trade). \n"
" - Можно ограничить доступ по IP для безопасности.\n"
"4. Подтверждение создания\n"
" - Подтвердите создание ключа.\n"
" - Отправьте чат-роботу.\n\n"
"Важно: сохраните отдельно API Key и Secret Key в надежном месте. Secret ключ отображается только один раз."
),
parse_mode="Markdown",
reply_markup=kbi.add_bybit_api,
disable_web_page_preview=True,
)
else:
await rq.create_user(
tg_id=callback.from_user.id, username=callback.from_user.username
)
await rq.set_user_symbol(tg_id=callback.from_user.id, symbol="BTCUSDT")
await rq.create_user_additional_settings(tg_id=callback.from_user.id)
await rq.create_user_risk_management(tg_id=callback.from_user.id)
await rq.create_user_conditional_settings(tg_id=callback.from_user.id)
await connect_platform(callback=callback, state=state)
except Exception as e:
logger.error("Error adding bybit API for user %s: %s", callback.from_user.id, e)
await callback.message.answer(
text="Произошла ошибка. Пожалуйста, попробуйте позже."
)
@router_add_bybit_api.callback_query(F.data == "add_bybit_api")
async def process_api_key(callback: CallbackQuery, state: FSMContext) -> None:
"""
Starts the FSM flow to add Bybit API keys.
Sets the FSM state to prompt user to enter API Key.
:param callback: CallbackQuery object.
:param state: FSMContext for managing user state.
"""
try:
await state.clear()
await state.set_state(AddBybitApiState.api_key_state)
await callback.answer()
await callback.message.answer(text="Введите API Key:")
except Exception as e:
logger.error("Error adding bybit API for user %s: %s", callback.from_user.id, e)
await callback.message.answer(
text="Произошла ошибка. Пожалуйста, попробуйте позже."
)
@router_add_bybit_api.message(AddBybitApiState.api_key_state)
async def process_secret_key(message: Message, state: FSMContext) -> None:
"""
Receives the API Key input from the user, stores it in FSM context,
then sets state to collect Secret Key.
:param message: Message object with user's input.
:param state: FSMContext for managing user state.
"""
try:
api_key = message.text
await state.update_data(api_key=api_key)
await state.set_state(AddBybitApiState.api_secret_state)
await message.answer(text="Введите Secret Key:")
except Exception as e:
logger.error("Error adding bybit API for user %s: %s", message.from_user.id, e)
await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.")
@router_add_bybit_api.message(AddBybitApiState.api_secret_state)
async def add_bybit_api(message: Message, state: FSMContext) -> None:
"""
Receives the Secret Key input, stores it, saves both API keys in the database,
clears FSM state and confirms success to the user.
:param message: Message object with user's input.
:param state: FSMContext for managing user state.
"""
try:
api_secret = message.text
api_key = (await state.get_data()).get("api_key")
await state.update_data(api_secret=api_secret)
if not api_key or not api_secret:
await message.answer("Введите корректные данные.")
return
result = await rq.set_user_api(
tg_id=message.from_user.id, api_key=api_key, api_secret=api_secret
)
if result:
await message.answer(text="Данные добавлены.", reply_markup=kbr.profile)
await user_profile_bybit(
tg_id=message.from_user.id, message=message, state=state
)
logger.debug(
"Bybit API added successfully for user: %s", message.from_user.id
)
else:
await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.")
logger.error(
"Error adding bybit API for user %s: %s", message.from_user.id, result
)
await state.clear()
except Exception as e:
logger.error("Error adding bybit API for user %s: %s", message.from_user.id, e)
await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.")

View File

@@ -0,0 +1,164 @@
import logging.config
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
import app.telegram.keyboards.inline as kbi
import database.request as rq
from app.bybit.get_functions.get_tickers import get_tickers
from app.bybit.profile_bybit import user_profile_bybit
from app.bybit.set_functions.set_leverage import (
set_leverage,
set_leverage_to_buy_and_sell,
)
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.telegram.states.states import ChangingTheSymbolState
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("changing_the_symbol")
router_changing_the_symbol = Router(name="changing_the_symbol")
@router_changing_the_symbol.callback_query(F.data == "change_symbol")
async def change_symbol(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handler for the "change_symbol" command.
Sends a message with available symbols to choose from.
"""
try:
await state.clear()
await state.set_state(ChangingTheSymbolState.symbol_state)
msg = await callback_query.message.edit_text(
text="Выберите название инструмента без лишних символов (например: BTCUSDT):",
reply_markup=kbi.symbol,
)
await state.update_data(prompt_message_id=msg.message_id)
logger.debug(
"Command change_symbol processed successfully for user: %s",
callback_query.from_user.id,
)
except Exception as e:
await callback_query.answer(
text="Произошла ошибка. Пожалуйста, попробуйте позже."
)
logger.error(
"Error processing command change_symbol for user %s: %s",
callback_query.from_user.id,
e,
)
@router_changing_the_symbol.message(ChangingTheSymbolState.symbol_state)
async def set_symbol(message: Message, state: FSMContext) -> None:
"""
Handler for user input for setting the symbol.
Updates FSM context with the selected symbol and persists the choice in database.
Sends an acknowledgement to user and clears FSM state afterward.
Args:
message (Message): Incoming message from user containing the selected symbol.
state (FSMContext): Finite State Machine context for the current user session.
Logs:
Success or error messages with user identification.
"""
try:
try:
data = await state.get_data()
if "prompt_message_id" in data:
prompt_message_id = data["prompt_message_id"]
await message.bot.delete_message(
chat_id=message.chat.id, message_id=prompt_message_id
)
await message.delete()
except Exception as e:
if "message to delete not found" in str(e).lower():
pass # Ignore this error
else:
raise e
symbol = message.text.upper()
additional_settings = await rq.get_user_additional_settings(
tg_id=message.from_user.id
)
if not additional_settings:
await rq.create_user_additional_settings(tg_id=message.from_user.id)
return
trade_mode = additional_settings.trade_mode or "Merged_Single"
mode = 0 if trade_mode == "Merged_Single" else 3
margin_type = additional_settings.margin_type or "ISOLATED_MARGIN"
leverage = "10"
leverage_to_buy = "10"
leverage_to_sell = "10"
ticker = await get_tickers(tg_id=message.from_user.id, symbol=symbol)
if ticker is None:
await message.answer(
text=f"Инструмент {symbol} не найден.", reply_markup=kbi.symbol
)
return
req = await rq.set_user_symbol(tg_id=message.from_user.id, symbol=symbol)
if not req:
await message.answer(
text="Произошла ошибка при установке инструмента.",
reply_markup=kbi.symbol,
)
return
await user_profile_bybit(
tg_id=message.from_user.id, message=message, state=state
)
res = await set_switch_position_mode(
tg_id=message.from_user.id, symbol=symbol, mode=mode
)
if res == "You have an existing position, so position mode cannot be switched":
if mode == 0:
mode = 3
else:
mode = 0
await set_switch_position_mode(
tg_id=message.from_user.id, symbol=symbol, mode=mode
)
if trade_mode == "Merged_Single":
trade_mode = "Both_Sides"
else:
trade_mode = "Merged_Single"
await rq.set_trade_mode(tg_id=message.from_user.id, trade_mode=trade_mode)
await set_margin_mode(tg_id=message.from_user.id, margin_mode=margin_type)
if margin_type == "ISOLATED_MARGIN":
await set_leverage_to_buy_and_sell(
tg_id=message.from_user.id,
symbol=symbol,
leverage_to_buy=str(leverage_to_buy),
leverage_to_sell=str(leverage_to_sell),
)
else:
await set_leverage(
tg_id=message.from_user.id, symbol=symbol, leverage=str(leverage)
)
await rq.set_leverage(tg_id=message.from_user.id, leverage=str(leverage))
await rq.set_leverage_to_buy_and_sell(
tg_id=message.from_user.id,
leverage_to_buy=str(leverage_to_buy),
leverage_to_sell=str(leverage_to_sell),
)
await rq.set_limit_price(tg_id=message.from_user.id, limit_price=0)
await rq.set_trigger_price(tg_id=message.from_user.id, trigger_price=0)
await state.clear()
except Exception as e:
await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.")
logger.error("Error setting symbol for user %s: %s", message.from_user.id, e)

View File

@@ -0,0 +1,109 @@
import logging.config
from aiogram import Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery
import database.request as rq
from app.bybit.close_positions import cancel_order, close_position
from app.helper_functions import safe_float
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("close_orders")
router_close_orders = Router(name="close_orders")
@router_close_orders.callback_query(
lambda c: c.data and c.data.startswith("close_position_")
)
async def close_position_handler(
callback_query: CallbackQuery, state: FSMContext
) -> None:
"""
Close a position.
:param callback_query: Incoming callback query from Telegram inline keyboard.
:param state: Finite State Machine context for the current user session.
:return: None
"""
try:
data = callback_query.data
parts = data.split("_")
symbol = parts[2]
side = parts[3]
position_idx = int(parts[4])
qty = safe_float(parts[5])
await rq.set_auto_trading(
tg_id=callback_query.from_user.id,
symbol=symbol,
auto_trading=False,
side=side,
)
res = await close_position(
tg_id=callback_query.from_user.id,
symbol=symbol,
side=side,
position_idx=position_idx,
qty=qty,
)
if not res:
await callback_query.answer(text="Произошла ошибка при закрытии позиции.")
return
await callback_query.answer(text="Позиция успешно закрыта.")
logger.debug(
"Command close_position processed successfully for user: %s",
callback_query.from_user.id,
)
except Exception as e:
await callback_query.answer(text="Произошла ошибка при закрытии позиции.")
logger.error(
"Error processing command close_position for user %s: %s",
callback_query.from_user.id,
e,
)
finally:
await state.clear()
@router_close_orders.callback_query(
lambda c: c.data and c.data.startswith("close_order_")
)
async def cancel_order_handler(
callback_query: CallbackQuery, state: FSMContext
) -> None:
"""
Cancel an order.
:param callback_query: Incoming callback query from Telegram inline keyboard.
:param state: Finite State Machine context for the current user session.
:return: None
"""
try:
data = callback_query.data
parts = data.split("_")
symbol = parts[2]
order_id = parts[3]
res = await cancel_order(
tg_id=callback_query.from_user.id, symbol=symbol, order_id=order_id
)
if not res:
await callback_query.answer(text="Произошла ошибка при закрытии ордера.")
return
await callback_query.answer(text="Ордер успешно закрыт.")
logger.debug(
"Command close_order processed successfully for user: %s",
callback_query.from_user.id,
)
except Exception as e:
await callback_query.answer(text="Произошла ошибка при закрытии ордера.")
logger.error(
"Error processing command close_order for user %s: %s",
callback_query.from_user.id,
e,
)
finally:
await state.clear()

View File

@@ -0,0 +1,50 @@
import logging.config
from aiogram import Router
from aiogram.fsm.context import FSMContext
from aiogram.types import Message
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("common")
router_common = Router(name="common")
@router_common.message()
async def unknown_message(message: Message, state: FSMContext) -> None:
"""
Handle unexpected or unrecognized messages.
Clears FSM state and informs the user about available commands.
Args:
message (types.Message): Incoming message object.
state (FSMContext): Current FSM context.
Returns:
None
"""
try:
await message.answer(
text="Извините, я вас не понял. "
"Пожалуйста, используйте одну из следующих команд:\n"
"/start - Запустить бота\n"
"/profile - Профиль\n"
"/bybit - Панель Bybit\n"
"/help - Получить помощь\n"
)
logger.debug(
"Received unknown message from user %s: %s",
message.from_user.id,
message.text,
)
except Exception as e:
logger.error(
"Error handling unknown message for user %s: %s", message.from_user.id, e
)
await message.answer(
text="Произошла ошибка при обработке вашего сообщения. Пожалуйста, попробуйте позже."
)
finally:
await state.clear()

View File

@@ -0,0 +1,314 @@
import logging.config
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery
import app.telegram.keyboards.inline as kbi
from app.bybit.get_functions.get_positions import (
get_active_orders,
get_active_orders_by_symbol,
get_active_positions,
get_active_positions_by_symbol,
)
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("get_positions_handlers")
router_get_positions_handlers = Router(name="get_positions_handlers")
@router_get_positions_handlers.callback_query(F.data == "my_deals")
async def get_positions_handlers(
callback_query: CallbackQuery, state: FSMContext
) -> None:
"""
Gets the user's active positions.
:param callback_query: CallbackQuery object.
:param state: FSMContext
:return: None
"""
try:
await state.clear()
await callback_query.message.edit_text(
text="Выберите какие сделки вы хотите посмотреть:",
reply_markup=kbi.change_position,
)
except Exception as e:
logger.error("Error in get_positions_handler: %s", e)
await callback_query.answer(text="Произошла ошибка при получении сделок.")
@router_get_positions_handlers.callback_query(F.data == "change_position")
async def get_positions_handler(
callback_query: CallbackQuery, state: FSMContext
) -> None:
"""
Gets the user's active positions.
:param callback_query: CallbackQuery object.
:param state: FSMContext
:return: None
"""
try:
res = await get_active_positions(tg_id=callback_query.from_user.id)
if res is None:
await callback_query.answer(
text="Произошла ошибка при получении активных позиций."
)
return
if res == ["No active positions found"]:
await callback_query.answer(text="Нет активных позиций.")
return
active_positions = [pos for pos in res if float(pos.get("size", 0)) > 0]
if not active_positions:
await callback_query.answer(text="Нет активных позиций.")
return
active_symbols_sides = [
(pos.get("symbol"), pos.get("side")) for pos in active_positions
]
await callback_query.message.edit_text(
text="Ваши активные позиции:",
reply_markup=kbi.create_active_positions_keyboard(
symbols=active_symbols_sides
),
)
except Exception as e:
logger.error("Error in get_positions_handler: %s", e)
await callback_query.answer(
text="Произошла ошибка при получении активных позиций."
)
finally:
await state.clear()
@router_get_positions_handlers.callback_query(
lambda c: c.data.startswith("get_position_")
)
async def get_position_handler(callback_query: CallbackQuery, state: FSMContext):
try:
data = callback_query.data
parts = data.split("_")
symbol = parts[2]
get_side = parts[3]
res = await get_active_positions_by_symbol(
tg_id=callback_query.from_user.id, symbol=symbol
)
if res is None:
await callback_query.answer(
text="Произошла ошибка при получении активных позиций."
)
return
position = next((pos for pos in res if pos.get("side") == get_side), None)
if position:
side = position.get("side")
symbol = position.get("symbol") or "Нет данных"
avg_price = position.get("avgPrice") or "Нет данных"
size = position.get("size") or "Нет данных"
take_profit = position.get("takeProfit") or "Нет данных"
stop_loss = position.get("stopLoss") or "Нет данных"
position_idx = position.get("positionIdx") or "Нет данных"
liq_price = position.get("liqPrice") or "Нет данных"
else:
side = "Нет данных"
symbol = "Нет данных"
avg_price = "Нет данных"
size = "Нет данных"
take_profit = "Нет данных"
stop_loss = "Нет данных"
position_idx = "Нет данных"
liq_price = "Нет данных"
side_rus = (
"Покупка"
if side == "Buy"
else "Продажа" if side == "Sell" else "Нет данных"
)
position_idx_rus = (
"Односторонний"
if position_idx == 0
else (
"Покупка в режиме хеджирования"
if position_idx == 1
else (
"Продажа в режиме хеджирования"
if position_idx == 2
else "Нет данных"
)
)
)
text_lines = [
f"Торговая пара: {symbol}",
f"Режим позиции: {position_idx_rus}",
f"Цена входа: {avg_price}",
f"Количество: {size}",
f"Движение: {side_rus}",
]
if take_profit and take_profit != "Нет данных":
text_lines.append(f"Тейк-профит: {take_profit}")
if stop_loss and stop_loss != "Нет данных":
text_lines.append(f"Стоп-лосс: {stop_loss}")
if liq_price and liq_price != "Нет данных":
text_lines.append(f"Цена ликвидации: {liq_price}")
text = "\n".join(text_lines)
await callback_query.message.edit_text(
text=text,
reply_markup=kbi.make_close_position_keyboard(
symbol_pos=symbol, side=side, position_idx=position_idx, qty=size
),
)
except Exception as e:
logger.error("Error in get_position_handler: %s", e)
await callback_query.answer(
text="Произошла ошибка при получении активных позиций."
)
finally:
await state.clear()
@router_get_positions_handlers.callback_query(F.data == "open_orders")
async def get_open_orders_handler(
callback_query: CallbackQuery, state: FSMContext
) -> None:
"""
Gets the user's open orders.
:param callback_query: CallbackQuery object.
:param state: FSMContext
:return: None
"""
try:
res = await get_active_orders(tg_id=callback_query.from_user.id)
if res is None:
await callback_query.answer(
text="Произошла ошибка при получении активных ордеров."
)
return
if res == ["No active orders found"]:
await callback_query.answer(text="Нет активных ордеров.")
return
active_positions = [pos for pos in res if pos.get("orderStatus", 0) == "New"]
if not active_positions:
await callback_query.answer(text="Нет активных ордеров.")
return
active_orders_sides = [
(pos.get("symbol"), pos.get("side")) for pos in active_positions
]
await callback_query.message.edit_text(
text="Ваши активные ордера:",
reply_markup=kbi.create_active_orders_keyboard(orders=active_orders_sides),
)
except Exception as e:
logger.error("Error in get_open_orders_handler: %s", e)
await callback_query.answer(
text="Произошла ошибка при получении активных ордеров."
)
finally:
await state.clear()
@router_get_positions_handlers.callback_query(lambda c: c.data.startswith("get_order_"))
async def get_order_handler(callback_query: CallbackQuery, state: FSMContext):
try:
data = callback_query.data
parts = data.split("_")
symbol = parts[2]
get_side = parts[3]
res = await get_active_orders_by_symbol(
tg_id=callback_query.from_user.id, symbol=symbol
)
if res is None:
await callback_query.answer(
text="Произошла ошибка при получении активных ордеров."
)
return
orders = next((pos for pos in res if pos.get("side") == get_side), None)
if orders:
side = orders.get("side")
symbol = orders.get("symbol")
price = orders.get("price")
qty = orders.get("qty")
order_type = orders.get("orderType")
trigger_price = orders.get("triggerPrice")
take_profit = orders.get("takeProfit")
stop_loss = orders.get("stopLoss")
order_id = orders.get("orderId")
else:
side = "Нет данных"
symbol = "Нет данных"
price = "Нет данных"
qty = "Нет данных"
order_type = "Нет данных"
trigger_price = "Нет данных"
take_profit = "Нет данных"
stop_loss = "Нет данных"
order_id = "Нет данных"
side_rus = (
"Покупка"
if side == "Buy"
else "Продажа" if side == "Sell" else "Нет данных"
)
order_type_rus = (
"Рыночный"
if order_type == "Market"
else "Лимитный" if order_type == "Limit" else "Нет данных"
)
text_lines = [
f"Торговая пара: {symbol}",
f"Количество: {qty}",
f"Движение: {side_rus}",
f"Тип ордера: {order_type_rus}",
]
if price:
text_lines.append(f"Цена: {price}")
if trigger_price and trigger_price != "Нет данных":
text_lines.append(f"Триггер цена: {trigger_price}")
if take_profit and take_profit != "Нет данных":
text_lines.append(f"Тейк-профит: {take_profit}")
if stop_loss and stop_loss != "Нет данных":
text_lines.append(f"Стоп-лосс: {stop_loss}")
text = "\n".join(text_lines)
await callback_query.message.edit_text(
text=text,
reply_markup=kbi.make_close_orders_keyboard(
symbol_order=symbol, order_id=order_id
),
)
except Exception as e:
logger.error("Error in get_order_handler: %s", e)
await callback_query.answer(
text="Произошла ошибка при получении активных ордеров."
)
finally:
await state.clear()

View File

@@ -1,316 +0,0 @@
import logging.config
from aiogram import F, Router
from aiogram.filters import CommandStart, Command
from aiogram.types import Message, CallbackQuery
from aiogram.fsm.context import FSMContext
import app.telegram.functions.functions as func
import app.telegram.functions.main_settings.settings as func_main_settings
import app.telegram.functions.risk_management_settings.settings as func_rmanagement_settings
import app.telegram.functions.condition_settings.settings as func_condition_settings
import app.telegram.functions.additional_settings.settings as func_additional_settings
import app.telegram.database.requests as rq
from app.services.Bybit.functions.balance import get_balance
from app.services.Bybit.functions.bybit_ws import run_ws_for_user
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("handlers")
router = Router()
@router.message(Command("start"))
@router.message(CommandStart())
async def start_message(message: Message) -> None:
"""
Обработчик команды /start.
Инициализирует нового пользователя в БД.
Args:
message (Message): Входящее сообщение с командой /start.
"""
await rq.set_new_user_bybit_api(message.from_user.id)
await func.start_message(message)
@router.message(Command("profile"))
@router.message(F.text == "👤 Профиль")
async def profile_message(message: Message) -> None:
"""
Обработчик кнопки 'Профиль'.
Проверяет существование пользователя и отображает профиль.
Args:
message (Message): Сообщение с текстом кнопки.
"""
user = await rq.check_user(message.from_user.id)
tg_id = message.from_user.id
balance = await get_balance(message.from_user.id, message)
if user and balance:
await run_ws_for_user(tg_id, message)
await func.profile_message(message.from_user.username, message)
else:
await rq.save_tg_id_new_user(message.from_user.id)
await func_main_settings.reg_new_user_default_main_settings(message.from_user.id, message)
await func_rmanagement_settings.reg_new_user_default_risk_management_settings(message.from_user.id, message)
await func_condition_settings.reg_new_user_default_condition_settings(message.from_user.id)
await func_additional_settings.reg_new_user_default_additional_settings(message.from_user.id, message)
@router.callback_query(F.data == "clb_start_chatbot_message")
async def clb_profile_msg(callback: CallbackQuery) -> None:
"""
Обработчик колбэка 'clb_start_chatbot_message'.
Если пользователь есть в БД — показывает профиль,
иначе регистрирует нового пользователя и инициализирует настройки.
Args:
callback (CallbackQuery): Полученный колбэк.
"""
tg_id = callback.from_user.id
message = callback.message
user = await rq.check_user(callback.from_user.id)
balance = await get_balance(callback.from_user.id, callback.message)
first_name = callback.from_user.first_name or ""
last_name = callback.from_user.last_name or ""
username = f"{first_name} {last_name}".strip() or callback.from_user.username or "Пользователь"
if user and balance:
await run_ws_for_user(tg_id, message)
await func.profile_message(callback.from_user.username, callback.message)
else:
await rq.save_tg_id_new_user(callback.from_user.id)
await func_main_settings.reg_new_user_default_main_settings(callback.from_user.id, callback.message)
await func_rmanagement_settings.reg_new_user_default_risk_management_settings(callback.from_user.id,
callback.message)
await func_condition_settings.reg_new_user_default_condition_settings(callback.from_user.id)
await func_additional_settings.reg_new_user_default_additional_settings(callback.from_user.id, callback.message)
await callback.answer()
@router.callback_query(F.data == "clb_settings_message")
async def clb_settings_msg(callback: CallbackQuery) -> None:
"""
Показать главное меню настроек.
Args:
callback (CallbackQuery): полученный колбэк.
"""
await func.settings_message(callback.message)
await callback.answer()
@router.callback_query(F.data == "clb_back_to_special_settings_message")
async def clb_back_to_settings_msg(callback: CallbackQuery) -> None:
"""
Вернуть пользователя к меню специальных настроек.
Args:
callback (CallbackQuery): полученный колбэк.
"""
await func.settings_message(callback.message)
await callback.answer()
@router.callback_query(F.data == "clb_change_main_settings")
async def clb_change_main_settings_message(callback: CallbackQuery) -> None:
"""
Открыть меню изменения главных настроек.
Args:
callback (CallbackQuery): полученный колбэк.
"""
await func_main_settings.main_settings_message(callback.from_user.id, callback.message)
await callback.answer()
@router.callback_query(F.data == "clb_change_risk_management_settings")
async def clb_change_risk_management_message(callback: CallbackQuery) -> None:
"""
Открыть меню изменения настроек управления рисками.
Args:
callback (CallbackQuery): полученный колбэк.
"""
await func_rmanagement_settings.main_settings_message(callback.from_user.id, callback.message)
await callback.answer()
@router.callback_query(F.data == "clb_change_condition_settings")
async def clb_change_condition_message(callback: CallbackQuery) -> None:
"""
Открыть меню изменения настроек условий.
Args:
callback (CallbackQuery): полученный колбэк.
"""
await func_condition_settings.main_settings_message(callback.from_user.id, callback.message)
await callback.answer()
@router.callback_query(F.data == "clb_change_additional_settings")
async def clb_change_additional_message(callback: CallbackQuery) -> None:
"""
Открыть меню изменения дополнительных настроек.
Args:
callback (CallbackQuery): полученный колбэк.
"""
await func_additional_settings.main_settings_message(callback.from_user.id, callback.message)
await callback.answer()
# Конкретные настройки каталогов
list_main_settings = ['clb_change_trading_mode',
'clb_change_switch_state',
'clb_change_margin_type',
'clb_change_size_leverage',
'clb_change_starting_quantity',
'clb_change_martingale_factor',
'clb_change_maximum_quantity'
]
@router.callback_query(F.data.in_(list_main_settings))
async def clb_main_settings_msg(callback: CallbackQuery, state: FSMContext) -> None:
"""
Обработчик колбэков изменения главных настроек с dispatch через match-case.
Args:
callback (CallbackQuery): полученный колбэк.
state (FSMContext): текущее состояние FSM.
"""
await callback.answer()
try:
match callback.data:
case 'clb_change_trading_mode':
await func_main_settings.trading_mode_message(callback.message, state)
case 'clb_change_switch_state':
await func_main_settings.switch_mode_enabled_message(callback.message, state)
case 'clb_change_margin_type':
await func_main_settings.margin_type_message(callback.message, state)
case 'clb_change_size_leverage':
await func_main_settings.size_leverage_message(callback.message, state)
case 'clb_change_starting_quantity':
await func_main_settings.starting_quantity_message(callback.message, state)
case 'clb_change_martingale_factor':
await func_main_settings.martingale_factor_message(callback.message, state)
case 'clb_change_maximum_quantity':
await func_main_settings.maximum_quantity_message(callback.message, state)
except Exception as e:
logger.error("Error callback in main_settings match-case: %s", e)
list_risk_management_settings = ['clb_change_price_profit',
'clb_change_price_loss',
'clb_change_max_risk_deal',
'commission_fee',
]
@router.callback_query(F.data.in_(list_risk_management_settings))
async def clb_risk_management_settings_msg(callback: CallbackQuery, state: FSMContext) -> None:
"""
Обработчик изменений настроек управления рисками.
Args:
callback (CallbackQuery): полученный колбэк.
state (FSMContext): текущее состояние FSM.
"""
await callback.answer()
try:
match callback.data:
case 'clb_change_price_profit':
await func_rmanagement_settings.price_profit_message(callback.message, state)
case 'clb_change_price_loss':
await func_rmanagement_settings.price_loss_message(callback.message, state)
case 'clb_change_max_risk_deal':
await func_rmanagement_settings.max_risk_deal_message(callback.message, state)
case 'commission_fee':
await func_rmanagement_settings.commission_fee_message(callback.message, state)
except Exception as e:
logger.error("Error callback in risk_management match-case: %s", e)
list_condition_settings = ['clb_change_mode',
'clb_change_timer',
'clb_change_filter_volatility',
'clb_change_external_cues',
'clb_change_tradingview_cues',
'clb_change_webhook',
'clb_change_ai_analytics'
]
@router.callback_query(F.data.in_(list_condition_settings))
async def clb_condition_settings_msg(callback: CallbackQuery, state: FSMContext) -> None:
"""
Обработчик изменений настроек условий трейдинга.
Args:
callback (CallbackQuery): полученный колбэк.
state (FSMContext): текущее состояние FSM.
"""
await callback.answer()
try:
match callback.data:
case 'clb_change_mode':
await func_condition_settings.trigger_message(callback.from_user.id, callback.message, state)
case 'clb_change_timer':
await func_condition_settings.timer_message(callback.from_user.id, callback.message, state)
case 'clb_change_filter_volatility':
await func_condition_settings.filter_volatility_message(callback.message, state)
case 'clb_change_external_cues':
await func_condition_settings.external_cues_message(callback.message, state)
case 'clb_change_tradingview_cues':
await func_condition_settings.trading_cues_message(callback.message, state)
case 'clb_change_webhook':
await func_condition_settings.webhook_message(callback.message, state)
case 'clb_change_ai_analytics':
await func_condition_settings.ai_analytics_message(callback.message, state)
except Exception as e:
logger.error("Error callback in main_settings match-case: %s", e)
list_additional_settings = ['clb_change_save_pattern',
'clb_change_auto_start',
'clb_change_notifications',
]
@router.callback_query(F.data.in_(list_additional_settings))
async def clb_additional_settings_msg(callback: CallbackQuery, state: FSMContext) -> None:
"""
Обработчик дополнительных настроек бота.
Args:
callback (CallbackQuery): полученный колбэк.
state (FSMContext): текущее состояние FSM.
"""
await callback.answer()
try:
match callback.data:
case 'clb_change_save_pattern':
await func_additional_settings.save_pattern_message(callback.message, state)
case 'clb_change_auto_start':
await func_additional_settings.auto_start_message(callback.message, state)
case 'clb_change_notifications':
await func_additional_settings.notifications_message(callback.message, state)
except Exception as e:
logger.error("Error callback in additional_settings match-case: %s", e)

View File

@@ -0,0 +1,381 @@
import logging.config
from aiogram import F, Router
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
import app.telegram.keyboards.inline as kbi
import app.telegram.keyboards.reply as kbr
import database.request as rq
from app.bybit.profile_bybit import user_profile_bybit
from app.telegram.functions.profile_tg import user_profile_tg
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("handlers_main")
router_handlers_main = Router(name="handlers_main")
@router_handlers_main.message(Command("start", "hello"))
@router_handlers_main.message(F.text.lower() == "привет")
async def cmd_start(message: Message, state: FSMContext) -> None:
"""
Handle the /start or /hello commands and the text message "привет".
Checks if the user exists in the database and sends a user profile or creates a new user
with default settings and greeting message.
Args:
message (Message): Incoming Telegram message object.
state (FSMContext): FSMContext for managing user state.
Raises:
None: Exceptions are caught and logged internally.
"""
tg_id = message.from_user.id
username = message.from_user.username
full_name = message.from_user.full_name
user = await rq.get_user(tg_id)
try:
if user:
await user_profile_tg(tg_id=message.from_user.id, message=message)
logger.debug(
"Command start processed successfully for user: %s",
message.from_user.id,
)
else:
await rq.create_user(tg_id=tg_id, username=username)
await rq.set_user_symbol(tg_id=tg_id, symbol="BTCUSDT")
await rq.create_user_additional_settings(tg_id=tg_id)
await rq.create_user_risk_management(tg_id=tg_id)
await rq.create_user_conditional_settings(tg_id=tg_id)
await message.answer(
text=f"Добро пожаловать, {full_name}!\n\n"
"PHANTOM TRADING - ваш надежный помощник для автоматизации трейдинга😉",
reply_markup=kbi.connect_the_platform,
)
logger.debug(
"Command start processed successfully for user: %s",
message.from_user.id,
)
except Exception as e:
logger.error(
"Error processing command start for user %s: %s", message.from_user.id, e
)
await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.")
finally:
await state.clear()
@router_handlers_main.message(Command("profile"))
@router_handlers_main.message(F.text == "Профиль")
async def cmd_to_main(message: Message, state: FSMContext) -> None:
"""
Handle the /profile command or text "Профиль".
Clears the current FSM state and sends the Telegram user profile.
Args:
message (Message): Incoming Telegram message object.
state (FSMContext): FSM state context.
Raises:
None: Exceptions are caught and logged internally.
"""
try:
await user_profile_tg(tg_id=message.from_user.id, message=message)
logger.debug(
"Command to_profile_tg processed successfully for user: %s",
message.from_user.id,
)
except Exception as e:
logger.error(
"Error processing command to_profile_tg for user %s: %s",
message.from_user.id,
e,
)
finally:
await state.clear()
@router_handlers_main.message(Command("bybit"))
@router_handlers_main.message(F.text == "Панель Bybit")
async def profile_bybit(message: Message, state: FSMContext) -> None:
"""
Handle the /bybit command or text "Панель Bybit".
Clears FSM state and sends Bybit trading panel profile.
Args:
message (Message): Incoming Telegram message object.
state (FSMContext): FSM state context.
Raises:
None: Exceptions are caught and logged internally.
"""
try:
await state.clear()
await user_profile_bybit(
tg_id=message.from_user.id, message=message, state=state
)
logger.debug(
"Command to_profile_bybit processed successfully for user: %s",
message.from_user.id,
)
except Exception as e:
logger.error(
"Error processing command to_profile_bybit for user %s: %s",
message.from_user.id,
e,
)
@router_handlers_main.callback_query(F.data == "profile_bybit")
async def profile_bybit_callback(
callback_query: CallbackQuery, state: FSMContext
) -> None:
"""
Handle callback query with data "profile_bybit".
Clears FSM state and sends the Bybit profile in response.
Args:
callback_query (CallbackQuery): Callback query object from Telegram.
state (FSMContext): FSM state context.
Raises:
None: Exceptions are caught and logged internally.
"""
try:
await state.clear()
await user_profile_bybit(
tg_id=callback_query.from_user.id,
message=callback_query.message,
state=state,
)
logger.debug(
"Callback profile_bybit processed successfully for user: %s",
callback_query.from_user.id,
)
await callback_query.answer()
except Exception as e:
logger.error(
"Error processing callback profile_bybit for user %s: %s",
callback_query.from_user.id,
e,
)
@router_handlers_main.callback_query(F.data == "main_settings")
async def settings(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handle callback query with data "main_settings".
Clears FSM state and edits the message to show main settings options.
Args:
callback_query (CallbackQuery): Callback query object.
state (FSMContext): FSM state context.
Raises:
None: Exceptions are caught and logged internally.
"""
try:
await state.clear()
msg = await callback_query.message.edit_text(
text="Выберите, что вы хотите настроить:", reply_markup=kbi.main_settings
)
await state.update_data(prompt_message_id=msg.message_id)
logger.debug(
"Command settings processed successfully for user: %s",
callback_query.from_user.id,
)
except Exception as e:
logger.error(
"Error processing command settings for user %s: %s",
callback_query.from_user.id,
e,
)
@router_handlers_main.message(Command("connect"))
@router_handlers_main.message(F.text == "Подключить платформу Bybit")
async def cmd_connect(message: Message, state: FSMContext) -> None:
"""
Handle the /connect command or text "Подключить платформу Bybit".
Clears FSM state and sends a connection message.
Args:
message (Message): Incoming Telegram message object.
state (FSMContext): FSM state context.
Raises:
None: Exceptions are caught and logged internally.
"""
try:
await state.clear()
user = await rq.get_user(tg_id=message.from_user.id)
if user:
await message.answer(
text=(
"Подключение Bybit аккаунта \n\n"
"1. Зарегистрируйтесь или войдите в свой аккаунт на Bybit по ссылке: "
"[Перейти на Bybit](https://www.bybit.com/invite?ref=YME83OJ).\n"
"2. В личном кабинете выберите раздел API. \n"
"3. Создание нового API ключа\n"
" - Нажмите кнопку Create New Key (Создать новый ключ).\n"
" - Выберите системно-сгенерированный ключ.\n"
" - Укажите название API ключа (любое). \n"
" - Выберите права доступа для торговли (Trade). \n"
" - Можно ограничить доступ по IP для безопасности.\n"
"4. Подтверждение создания\n"
" - Подтвердите создание ключа.\n"
" - Отправьте чат-роботу.\n\n"
"Важно: сохраните отдельно API Key и Secret Key в надежном месте. Secret ключ отображается только один раз."
),
parse_mode="Markdown",
reply_markup=kbi.add_bybit_api,
disable_web_page_preview=True,
)
else:
await rq.create_user(
tg_id=message.from_user.id, username=message.from_user.username
)
await rq.set_user_symbol(tg_id=message.from_user.id, symbol="BTCUSDT")
await rq.create_user_additional_settings(tg_id=message.from_user.id)
await rq.create_user_risk_management(tg_id=message.from_user.id)
await rq.create_user_conditional_settings(tg_id=message.from_user.id)
await cmd_connect(message=message, state=state)
logger.debug(
"Command connect processed successfully for user: %s",
message.from_user.id,
)
except Exception as e:
logger.error(
"Error processing command connect for user %s: %s",
message.from_user.id,
e,
)
@router_handlers_main.message(Command("help"))
async def cmd_help(message: Message, state: FSMContext) -> None:
"""
Handle the /help command.
Clears FSM state and sends a help message with available commands and reply keyboard.
Args:
message (Message): Incoming Telegram message object.
state (FSMContext): FSM state context.
Raises:
None: Exceptions are caught and logged internally.
"""
try:
await state.clear()
await message.answer(
text="Используйте одну из следующих команд:\n"
"/start - Запустить бота\n"
"/profile - Профиль\n"
"/bybit - Панель Bybit\n"
"/connect - Подключиться к платформе\n",
reply_markup=kbr.profile,
)
logger.debug(
"Command help processed successfully for user: %s",
message.from_user.id,
)
except Exception as e:
logger.error(
"Error processing command help for user %s: %s", message.from_user.id, e
)
await message.answer(
text="Произошла ошибка. Пожалуйста, попробуйте позже.",
reply_markup=kbr.profile,
)
@router_handlers_main.message(Command("cancel"))
@router_handlers_main.message(
lambda message: message.text.casefold() in ["cancel", "отмена"]
)
async def cmd_cancel_handler(message: Message, state: FSMContext) -> None:
"""
Handle /cancel command or text 'cancel'/'отмена'.
If there is an active FSM state, clears it and informs the user.
Otherwise, informs that no operation was in progress.
Args:
message (Message): Incoming Telegram message object.
state (FSMContext): FSM state context.
Raises:
None: Exceptions are caught and logged internally.
"""
current_state = await state.get_state()
if current_state is None:
await message.reply(
text="Хорошо, но ничего не происходило.", reply_markup=kbr.profile
)
logger.debug(
"Cancel command received but no active state for user %s.",
message.from_user.id,
)
return
try:
await state.clear()
await message.reply(text="Команда отменена.", reply_markup=kbr.profile)
logger.debug(
"Command cancel executed successfully. State cleared for user %s.",
message.from_user.id,
)
except Exception as e:
logger.error(
"Error while cancelling command for user %s: %s", message.from_user.id, e
)
await message.answer(
text="Произошла ошибка при отмене. Пожалуйста, попробуйте позже.",
reply_markup=kbr.profile,
)
@router_handlers_main.callback_query(F.data == "cancel")
async def cmd_cancel(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handle callback query with data "cancel".
Clears the FSM state and sends a cancellation message.
Args:
callback_query (CallbackQuery): Callback query object.
state (FSMContext): FSM state context.
Raises:
None: Exceptions are caught and logged internally.
"""
try:
await callback_query.message.delete()
await user_profile_bybit(
tg_id=callback_query.from_user.id,
message=callback_query.message,
state=state,
)
logger.debug(
"Command cancel processed successfully for user: %s",
callback_query.from_user.id,
)
except Exception as e:
logger.error(
"Error processing command cancel for user %s: %s",
callback_query.from_user.id,
e,
)
finally:
await state.clear()

View File

@@ -0,0 +1,17 @@
__all__ = "router"
from aiogram import Router
from app.telegram.handlers.main_settings.additional_settings import (
router_additional_settings,
)
from app.telegram.handlers.main_settings.conditional_settings import (
router_conditional_settings,
)
from app.telegram.handlers.main_settings.risk_management import router_risk_management
router_main_settings = Router(name=__name__)
router_main_settings.include_router(router_additional_settings)
router_main_settings.include_router(router_risk_management)
router_main_settings.include_router(router_conditional_settings)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,174 @@
import logging.config
from aiogram import Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
import app.telegram.keyboards.inline as kbi
import database.request as rq
from app.helper_functions import is_int_for_timer
from app.telegram.states.states import ConditionalSettingsState
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("conditional_settings")
router_conditional_settings = Router(name="conditional_settings")
@router_conditional_settings.callback_query(
lambda c: c.data == "start_timer" or c.data == "stop_timer"
)
async def timer(callback_query: CallbackQuery, state: FSMContext):
"""
Handles callback queries starting with 'start_timer' or 'stop_timer'.
"""
try:
await state.clear()
if callback_query.data == "start_timer":
await state.set_state(ConditionalSettingsState.start_timer_state)
msg = await callback_query.message.edit_text(
"Введите время в минутах для старта торговли:",
reply_markup=kbi.back_to_conditions,
)
await state.update_data(prompt_message_id=msg.message_id)
elif callback_query.data == "stop_timer":
await state.set_state(ConditionalSettingsState.stop_timer_state)
msg = await callback_query.message.edit_text(
"Введите время в минутах для остановки торговли:",
reply_markup=kbi.back_to_conditions,
)
await state.update_data(prompt_message_id=msg.message_id)
else:
await callback_query.answer(
text="Произошла ошибка. Пожалуйста, попробуйте позже."
)
except Exception as e:
await callback_query.answer(
text="Произошла ошибка. Пожалуйста, попробуйте позже."
)
logger.error(
"Error processing command timer for user %s: %s",
callback_query.from_user.id,
e,
)
@router_conditional_settings.message(ConditionalSettingsState.start_timer_state)
async def start_timer(message: Message, state: FSMContext):
"""
Handles the start_timer state of the Finite State Machine.
"""
try:
try:
data = await state.get_data()
if "prompt_message_id" in data:
prompt_message_id = data["prompt_message_id"]
await message.bot.delete_message(
chat_id=message.chat.id, message_id=prompt_message_id
)
await message.delete()
except Exception as e:
if "message to delete not found" in str(e).lower():
pass # Ignore this error
else:
raise e
get_start_timer = message.text
value = is_int_for_timer(get_start_timer)
if value is False:
await message.answer(
"Ошибка: введите валидное число.",
reply_markup=kbi.back_to_conditions,
)
logger.debug(
"User %s input invalid (not an valid number): %s",
message.from_user.id,
get_start_timer,
)
return
req = await rq.set_start_timer(
tg_id=message.from_user.id, timer_start=int(get_start_timer)
)
if req:
await message.answer(
"Таймер успешно установлен.",
reply_markup=kbi.back_to_conditions,
)
else:
await message.answer(
"Произошла ошибка. Пожалуйста, попробуйте позже.",
reply_markup=kbi.back_to_conditions,
)
await state.clear()
except Exception as e:
await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.")
logger.error(
"Error processing command start_timer for user %s: %s",
message.from_user.id,
e,
)
@router_conditional_settings.message(ConditionalSettingsState.stop_timer_state)
async def stop_timer(message: Message, state: FSMContext):
"""
Handles the stop_timer state of the Finite State Machine.
"""
try:
try:
data = await state.get_data()
if "prompt_message_id" in data:
prompt_message_id = data["prompt_message_id"]
await message.bot.delete_message(
chat_id=message.chat.id, message_id=prompt_message_id
)
await message.delete()
except Exception as e:
if "message to delete not found" in str(e).lower():
pass # Ignore this error
else:
raise e
get_stop_timer = message.text
value = is_int_for_timer(get_stop_timer)
if value is False:
await message.answer(
"Ошибка: введите валидное число.",
reply_markup=kbi.back_to_conditions,
)
logger.debug(
"User %s input invalid (not an valid number): %s",
message.from_user.id,
get_stop_timer,
)
return
req = await rq.set_stop_timer(
tg_id=message.from_user.id, timer_end=int(get_stop_timer)
)
if req:
await message.answer(
"Таймер успешно установлен.",
reply_markup=kbi.back_to_conditions,
)
else:
await message.answer(
"Произошла ошибка. Пожалуйста, попробуйте позже.",
reply_markup=kbi.back_to_conditions,
)
await state.clear()
except Exception as e:
await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.")
logger.error(
"Error processing command stop_timer for user %s: %s",
message.from_user.id,
e,
)

View File

@@ -0,0 +1,467 @@
import logging.config
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
import app.telegram.keyboards.inline as kbi
import database.request as rq
from app.helper_functions import is_int
from app.telegram.states.states import RiskManagementState
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("risk_management")
router_risk_management = Router(name="risk_management")
@router_risk_management.callback_query(F.data == "take_profit_percent")
async def take_profit_percent(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handles the 'profit_price_change' callback query.
Clears the current FSM state, edits the message text to display the take profit percent options,
and shows an inline keyboard for selection.
Args:
callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard.
state (FSMContext): Finite State Machine context for the current user session.
Logs:
Success or error messages with user identification.
"""
try:
await state.clear()
await state.set_state(RiskManagementState.take_profit_percent_state)
msg = await callback_query.message.edit_text(
text="Введите процент изменения цены для фиксации прибыли: ",
reply_markup=kbi.back_to_risk_management,
)
await state.update_data(prompt_message_id=msg.message_id)
logger.debug(
"Command profit_price_change processed successfully for user: %s",
callback_query.from_user.id,
)
except Exception as e:
await callback_query.answer(
text="Произошла ошибка. Пожалуйста, попробуйте позже."
)
logger.error(
"Error processing command profit_price_change for user %s: %s",
callback_query.from_user.id,
e,
)
@router_risk_management.message(RiskManagementState.take_profit_percent_state)
async def set_take_profit_percent(message: Message, state: FSMContext) -> None:
"""
Handles user input for setting the take profit percentage.
Updates FSM context with the selected percentage and persists the choice in database.
Sends an acknowledgement to user and clears FSM state afterward.
Args:
message (Message): Incoming message from user containing the take profit percentage.
state (FSMContext): Finite State Machine context for the current user session.
Logs:
Success or error messages with user identification.
"""
try:
try:
data = await state.get_data()
if "prompt_message_id" in data:
prompt_message_id = data["prompt_message_id"]
await message.bot.delete_message(
chat_id=message.chat.id, message_id=prompt_message_id
)
await message.delete()
except Exception as e:
if "message to delete not found" in str(e).lower():
pass # Ignore this error
else:
raise e
take_profit_percent_value = message.text
if not is_int(take_profit_percent_value):
await message.answer(
text="Ошибка: введите валидное число.",
reply_markup=kbi.back_to_risk_management,
)
logger.debug(
"User %s input invalid (not an valid number): %s",
message.from_user.id,
take_profit_percent_value,
)
return
if int(take_profit_percent_value) < 1 or int(take_profit_percent_value) > 100:
await message.answer(
text="Ошибка: введите число от 1 до 100.",
reply_markup=kbi.back_to_risk_management,
)
logger.debug(
"User %s input invalid (not an valid number): %s",
message.from_user.id,
take_profit_percent_value,
)
return
req = await rq.set_take_profit_percent(
tg_id=message.from_user.id,
take_profit_percent=int(take_profit_percent_value),
)
if req:
await message.answer(
text=f"Процент изменения цены для фиксации прибыли "
f"установлен на {take_profit_percent_value}%.",
reply_markup=kbi.back_to_risk_management,
)
else:
await message.answer(
text="Произошла ошибка при установке процента изменения цены для фиксации прибыли. "
"Пожалуйста, попробуйте позже.",
reply_markup=kbi.back_to_risk_management,
)
await state.clear()
except Exception as e:
logger.error(
"Error processing command profit_price_change for user %s: %s",
message.from_user.id,
e,
)
@router_risk_management.callback_query(F.data == "stop_loss_percent")
async def stop_loss_percent(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handles the 'stop_loss_percent' callback query.
Clears the current FSM state, edits the message text to display the stop loss percentage options,
and shows an inline keyboard for selection.
Args:
callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard.
state (FSMContext): Finite State Machine context for the current user session.
Logs:
Success or error messages with user identification.
"""
try:
await state.clear()
await state.set_state(RiskManagementState.stop_loss_percent_state)
msg = await callback_query.message.edit_text(
text="Введите процент изменения цены для фиксации убытка: ",
reply_markup=kbi.back_to_risk_management,
)
await state.update_data(prompt_message_id=msg.message_id)
logger.debug(
"Command stop_loss_percent processed successfully for user: %s",
callback_query.from_user.id,
)
except Exception as e:
await callback_query.answer(
text="Произошла ошибка. Пожалуйста, попробуйте позже."
)
logger.error(
"Error processing command stop_loss_percent for user %s: %s",
callback_query.from_user.id,
e,
)
@router_risk_management.message(RiskManagementState.stop_loss_percent_state)
async def set_stop_loss_percent(message: Message, state: FSMContext) -> None:
"""
Handles user input for setting the stop loss percentage.
Updates FSM context with the selected percentage and persists the choice in database.
Sends an acknowledgement to user and clears FSM state afterward.
Args:
message (Message): Incoming message from user containing the stop loss percentage.
state (FSMContext): Finite State Machine context for the current user session.
Logs:
Success or error messages with user identification.
"""
try:
try:
data = await state.get_data()
if "prompt_message_id" in data:
prompt_message_id = data["prompt_message_id"]
await message.bot.delete_message(
chat_id=message.chat.id, message_id=prompt_message_id
)
await message.delete()
except Exception as e:
if "message to delete not found" in str(e).lower():
pass # Ignore this error
else:
raise e
stop_loss_percent_value = message.text
if not is_int(stop_loss_percent_value):
await message.answer(
text="Ошибка: введите валидное число.",
reply_markup=kbi.back_to_risk_management,
)
logger.debug(
"User %s input invalid (not an valid number): %s",
message.from_user.id,
stop_loss_percent_value,
)
return
if int(stop_loss_percent_value) < 1 or int(stop_loss_percent_value) > 100:
await message.answer(
text="Ошибка: введите число от 1 до 100.",
reply_markup=kbi.back_to_risk_management,
)
logger.debug(
"User %s input invalid (not an valid number): %s",
message.from_user.id,
stop_loss_percent_value,
)
return
req = await rq.set_stop_loss_percent(
tg_id=message.from_user.id, stop_loss_percent=int(stop_loss_percent_value)
)
if req:
await message.answer(
text=f"Процент изменения цены для фиксации убытка "
f"установлен на {stop_loss_percent_value}%.",
reply_markup=kbi.back_to_risk_management,
)
else:
await message.answer(
text="Произошла ошибка при установке процента изменения цены для фиксации убытка. "
"Пожалуйста, попробуйте позже.",
reply_markup=kbi.back_to_risk_management,
)
await state.clear()
except Exception as e:
await message.answer(
text="Произошла ошибка при установке процента изменения цены для фиксации убытка. "
"Пожалуйста, попробуйте позже.",
reply_markup=kbi.back_to_risk_management,
)
logger.error(
"Error processing command stop_loss_percent for user %s: %s",
message.from_user.id,
e,
)
@router_risk_management.callback_query(F.data == "max_risk_percent")
async def max_risk_percent(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handles the 'max_risk_percent' callback query.
Clears the current FSM state, edits the message text to display the maximum risk percentage options,
and shows an inline keyboard for selection.
Args:
callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard.
state (FSMContext): Finite State Machine context for the current user session.
Logs:
Success or error messages with user identification.
"""
try:
await state.clear()
await state.set_state(RiskManagementState.max_risk_percent_state)
msg = await callback_query.message.edit_text(
text="Введите максимальный процент риска: ",
reply_markup=kbi.back_to_risk_management,
)
await state.update_data(prompt_message_id=msg.message_id)
logger.debug(
"Command max_risk_percent processed successfully for user: %s",
callback_query.from_user.id,
)
except Exception as e:
await callback_query.answer(
text="Произошла ошибка. Пожалуйста, попробуйте позже."
)
logger.error(
"Error processing command max_risk_percent for user %s: %s",
callback_query.from_user.id,
e,
)
@router_risk_management.message(RiskManagementState.max_risk_percent_state)
async def set_max_risk_percent(message: Message, state: FSMContext) -> None:
"""
Handles user input for setting the maximum risk percentage.
Updates FSM context with the selected percentage and persists the choice in database.
Sends an acknowledgement to user and clears FSM state afterward.
Args:
message (Message): Incoming message from user containing the maximum risk percentage.
state (FSMContext): Finite State Machine context for the current user session.
Logs:
Success or error messages with user identification.
"""
try:
try:
data = await state.get_data()
if "prompt_message_id" in data:
prompt_message_id = data["prompt_message_id"]
await message.bot.delete_message(
chat_id=message.chat.id, message_id=prompt_message_id
)
await message.delete()
except Exception as e:
if "message to delete not found" in str(e).lower():
pass # Ignore this error
else:
raise e
max_risk_percent_value = message.text
if not is_int(max_risk_percent_value):
await message.answer(
text="Ошибка: введите валидное число.",
reply_markup=kbi.back_to_risk_management,
)
logger.debug(
"User %s input invalid (not an valid number): %s",
message.from_user.id,
max_risk_percent_value,
)
return
if int(max_risk_percent_value) < 1 or int(max_risk_percent_value) > 100:
await message.answer(
text="Ошибка: введите число от 1 до 100.",
reply_markup=kbi.back_to_risk_management,
)
logger.debug(
"User %s input invalid (not an valid number): %s",
message.from_user.id,
max_risk_percent_value,
)
return
req = await rq.set_max_risk_percent(
tg_id=message.from_user.id, max_risk_percent=int(max_risk_percent_value)
)
if req:
await message.answer(
text=f"Максимальный процент риска установлен на {max_risk_percent_value}%.",
reply_markup=kbi.back_to_risk_management,
)
else:
await message.answer(
text="Произошла ошибка при установке максимального процента риска. "
"Пожалуйста, попробуйте позже.",
reply_markup=kbi.back_to_risk_management,
)
await state.clear()
except Exception as e:
await message.answer(
text="Произошла ошибка при установке максимального процента риска. "
"Пожалуйста, попробуйте позже.",
reply_markup=kbi.back_to_risk_management,
)
logger.error(
"Error processing command max_risk_percent for user %s: %s",
message.from_user.id,
e,
)
@router_risk_management.callback_query(F.data == "commission_fee")
async def commission_fee(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handles the 'commission_fee' callback query.
Clears the current FSM state, edits the message text to display the commission fee options,
and shows an inline keyboard for selection.
Args:
callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard.
state (FSMContext): Finite State Machine context for the current user session.
Logs:
Success or error messages with user identification.
"""
try:
await state.clear()
await state.set_state(RiskManagementState.commission_fee_state)
msg = await callback_query.message.edit_text(
text="Учитывать комиссию биржи для расчета прибыли?: ",
reply_markup=kbi.commission_fee,
)
await state.update_data(prompt_message_id=msg.message_id)
logger.debug(
"Command commission_fee processed successfully for user: %s",
callback_query.from_user.id,
)
except Exception as e:
await callback_query.answer(
text="Произошла ошибка. Пожалуйста, попробуйте позже."
)
logger.error(
"Error processing command commission_fee for user %s: %s",
callback_query.from_user.id,
e,
)
@router_risk_management.callback_query(
lambda c: c.data in ["Yes_commission_fee", "No_commission_fee"]
)
async def set_commission_fee(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handles user input for setting the commission fee.
Updates FSM context with the selected option and persists the choice in database.
Sends an acknowledgement to user and clears FSM state afterward.
Args:
callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard.
state (FSMContext): Finite State Machine context for the current user session.
Logs:
Success or error messages with user identification.
"""
try:
req = await rq.set_commission_fee(
tg_id=callback_query.from_user.id, commission_fee=callback_query.data
)
if not req:
await callback_query.answer(
text="Произошла ошибка при установке комиссии биржи. Пожалуйста, попробуйте позже."
)
return
if callback_query.data == "Yes_commission_fee":
await callback_query.answer(text="Комиссия биржи учитывается.")
else:
await callback_query.answer(text="Комиссия биржи не учитывается.")
except Exception as e:
logger.error(
"Error processing command commission_fee for user %s: %s",
callback_query.from_user.id,
e,
)
finally:
await state.clear()

View File

@@ -0,0 +1,278 @@
import logging.config
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery
import app.telegram.keyboards.inline as kbi
import database.request as rq
from app.bybit import get_bybit_client
from app.bybit.get_functions.get_tickers import get_tickers
from app.helper_functions import calculate_total_budget, get_base_currency, safe_float
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("settings")
router_settings = Router(name="settings")
@router_settings.callback_query(F.data == "additional_settings")
async def additional_settings(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handler for the "additional_settings" command.
Sends a message with additional settings options.
"""
try:
await state.clear()
tg_id = callback_query.from_user.id
symbol = await rq.get_user_symbol(tg_id=tg_id)
additional_data = await rq.get_user_additional_settings(tg_id=tg_id)
if not additional_data:
await rq.create_user(
tg_id=tg_id, username=callback_query.from_user.username
)
await rq.create_user_additional_settings(tg_id=tg_id)
await rq.create_user_risk_management(tg_id=tg_id)
await rq.create_user_conditional_settings(tg_id=tg_id)
await additional_settings(callback_query=callback_query, state=state)
return
trade_mode_map = {
"Merged_Single": "Односторонний режим",
"Both_Sides": "Хеджирование",
}
margin_type_map = {
"ISOLATED_MARGIN": "Изолированная",
"REGULAR_MARGIN": "Кросс",
}
order_type_map = {"Market": "Рыночный", "Limit": "Лимитный"}
trade_mode = additional_data.trade_mode or ""
margin_type = additional_data.margin_type or ""
order_type = additional_data.order_type or ""
trade_mode_rus = trade_mode_map.get(trade_mode, trade_mode)
margin_type_rus = margin_type_map.get(margin_type, margin_type)
order_type_rus = order_type_map.get(order_type, "Условный")
def f(x):
return safe_float(x)
leverage = f(additional_data.leverage)
leverage_to_buy = f(additional_data.leverage_to_buy)
leverage_to_sell = f(additional_data.leverage_to_sell)
martingale = f(additional_data.martingale_factor)
max_bets = additional_data.max_bets_in_series
quantity = f(additional_data.order_quantity)
limit_price = f(additional_data.limit_price)
trigger_price = f(additional_data.trigger_price) or 0
tickers = await get_tickers(tg_id=tg_id, symbol=symbol)
price_symbol = safe_float(tickers.get("lastPrice")) or 0
bid = f(tickers.get("bid1Price")) or 0
ask = f(tickers.get("ask1Price")) or 0
sym = get_base_currency(symbol)
if trade_mode == "Merged_Single":
leverage_str = f"{leverage:.2f}x"
else:
if margin_type == "ISOLATED_MARGIN":
leverage_str = f"{leverage_to_buy:.2f}x:{leverage_to_sell:.2f}x"
else:
leverage_str = f"{leverage:.2f}x"
conditional_order_type = additional_data.conditional_order_type or ""
conditional_order_type_rus = (
"Лимитный"
if conditional_order_type == "Limit"
else (
"Рыночный"
if conditional_order_type == "Market"
else conditional_order_type
)
)
conditional_order_type_text = (
f"- Тип условного ордера: {conditional_order_type_rus}\n"
if order_type == "Conditional"
else ""
)
limit_price_text = ""
trigger_price_text = ""
if order_type == "Limit":
limit_price_text = f"- Цена лимитного ордера: {limit_price:.4f} USDT\n"
elif order_type == "Conditional":
if conditional_order_type == "Limit":
limit_price_text = f"- Цена лимитного ордера: {limit_price:.4f} USDT\n"
trigger_price_text = f"- Триггер цена: {trigger_price:.4f} USDT\n"
risk_management_data = await rq.get_user_risk_management(tg_id=tg_id)
commission_fee = risk_management_data.commission_fee
client = await get_bybit_client(tg_id=tg_id)
fee_info = client.get_fee_rates(category="linear", symbol=symbol)
if commission_fee == "Yes_commission_fee":
commission_fee_percent = safe_float(
fee_info["result"]["list"][0]["takerFeeRate"]
)
else:
commission_fee_percent = 0.0
if order_type == "Conditional":
if conditional_order_type == "Limit":
entry_price = limit_price
ask_price = limit_price
bid_price = limit_price
else:
ask_price = trigger_price
bid_price = trigger_price
entry_price = trigger_price
else:
if order_type == "Limit":
entry_price = limit_price
ask_price = limit_price
bid_price = limit_price
else:
entry_price = price_symbol
ask_price = ask
bid_price = bid
durability_buy = quantity * bid_price
durability_sell = quantity * ask_price
quantity_price = quantity * entry_price
total_commission = quantity_price * commission_fee_percent
total_budget = await calculate_total_budget(
quantity=durability_buy,
martingale_factor=martingale,
max_steps=max_bets,
commission_fee_percent=total_commission,
)
text = (
f"Основные настройки:\n\n"
f"- Режим позиции: {trade_mode_rus}\n"
f"- Тип маржи: {margin_type_rus}\n"
f"- Размер кредитного плеча: {leverage_str}\n"
f"- Тип ордера: {order_type_rus}\n"
f"- Количество ордера: {quantity} {sym}\n"
f"- Коэффициент мартингейла: {martingale:.2f}\n"
f"{conditional_order_type_text}"
f"{trigger_price_text}"
f"{limit_price_text}"
f"- Максимальное кол-во ставок в серии: {max_bets}\n\n"
f"- Стоимость: {durability_buy:.2f}/{durability_sell:.2f} USDT\n"
f"- Рекомендуемый бюджет: {total_budget:.4f} USDT\n"
)
keyboard = kbi.get_additional_settings_keyboard(
current_order_type=order_type, conditional_order=conditional_order_type
)
await callback_query.message.edit_text(text=text, reply_markup=keyboard)
logger.debug(
"Command additional_settings processed successfully for user: %s", tg_id
)
except Exception as e:
await callback_query.message.edit_text(
text="Произошла ошибка. Пожалуйста, попробуйте ещё раз.",
reply_markup=kbi.profile_bybit,
)
logger.error(
"Error processing command additional_settings for user %s: %s",
callback_query.from_user.id,
e,
)
@router_settings.callback_query(F.data == "risk_management")
async def risk_management(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handler for the "risk_management" command.
Sends a message with risk management options.
"""
try:
await state.clear()
risk_management_data = await rq.get_user_risk_management(
tg_id=callback_query.from_user.id
)
if risk_management_data:
take_profit_percent = risk_management_data.take_profit_percent or ""
stop_loss_percent = risk_management_data.stop_loss_percent or ""
max_risk_percent = risk_management_data.max_risk_percent or ""
commission_fee = risk_management_data.commission_fee or ""
commission_fee_rus = (
"Да" if commission_fee == "Yes_commission_fee" else "Нет"
)
await callback_query.message.edit_text(
text=f"Риск-менеджмент:\n\n"
f"- Процент изменения цены для фиксации прибыли: {take_profit_percent}%\n"
f"- Процент изменения цены для фиксации убытка: {stop_loss_percent}%\n\n"
f"- Максимальный риск на сделку (в % от баланса): {max_risk_percent}%\n\n"
f"- Комиссия биржи для расчета прибыли: {commission_fee_rus}\n\n",
reply_markup=kbi.risk_management,
)
logger.debug(
"Command main_settings processed successfully for user: %s",
callback_query.from_user.id,
)
else:
await rq.create_user(
tg_id=callback_query.from_user.id,
username=callback_query.from_user.username,
)
await rq.create_user_additional_settings(tg_id=callback_query.from_user.id)
await rq.create_user_risk_management(tg_id=callback_query.from_user.id)
await rq.create_user_conditional_settings(tg_id=callback_query.from_user.id)
await risk_management(callback_query=callback_query, state=state)
except Exception as e:
logger.error(
"Error processing command main_settings for user %s: %s",
callback_query.from_user.id,
e,
)
@router_settings.callback_query(F.data == "conditional_settings")
async def conditions(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handler for the "conditions" command.
Sends a message with trading conditions options.
"""
try:
await state.clear()
conditional_settings_data = await rq.get_user_conditional_settings(
tg_id=callback_query.from_user.id
)
if conditional_settings_data:
start_timer = conditional_settings_data.timer_start or 0
stop_timer = conditional_settings_data.timer_end or 0
await callback_query.message.edit_text(
text="Условия торговли:\n\n"
f"- Таймер для старта: {start_timer} мин.\n"
f"- Таймер для остановки: {stop_timer} мин.\n",
reply_markup=kbi.conditions,
)
logger.debug(
"Command main_settings processed successfully for user: %s",
callback_query.from_user.id,
)
else:
await rq.create_user(
tg_id=callback_query.from_user.id,
username=callback_query.from_user.username,
)
await rq.create_user_additional_settings(tg_id=callback_query.from_user.id)
await rq.create_user_risk_management(tg_id=callback_query.from_user.id)
await rq.create_user_conditional_settings(tg_id=callback_query.from_user.id)
await conditions(callback_query=callback_query, state=state)
except Exception as e:
logger.error(
"Error processing command main_settings for user %s: %s",
callback_query.from_user.id,
e,
)

View File

@@ -0,0 +1,414 @@
import asyncio
import logging.config
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery
import app.telegram.keyboards.inline as kbi
import database.request as rq
from app.bybit.get_functions.get_positions import get_active_positions_by_symbol
from app.bybit.open_positions import start_trading_cycle
from app.helper_functions import safe_float
from app.telegram.tasks.tasks import (
add_start_task_merged,
add_start_task_switch,
cancel_start_task_merged,
cancel_start_task_switch,
)
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("start_trading")
router_start_trading = Router(name="start_trading")
@router_start_trading.callback_query(F.data == "start_trading")
async def start_trading(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handles the "start_trading" callback query.
Clears the FSM state and sends a message to the user to select the trading mode.
:param callback_query: Message
:param state: FSMContext
:return: None
"""
try:
await state.clear()
additional_data = await rq.get_user_additional_settings(
tg_id=callback_query.from_user.id
)
trade_mode = additional_data.trade_mode
symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id)
deals = await get_active_positions_by_symbol(
tg_id=callback_query.from_user.id, symbol=symbol
)
position = next((d for d in deals if d.get("symbol") == symbol), None)
if position:
size = position.get("size", 0)
position_idx = position.get("positionIdx")
else:
size = 0
position_idx = None
if position_idx != 0 and safe_float(size) > 0 and trade_mode == "Merged_Single":
await callback_query.answer(
text="У вас есть активная позиция в режиме хеджирования. "
"Открытие сделки в одностороннем режиме невозможно.",
)
return
if position_idx == 0 and safe_float(size) > 0 and trade_mode == "Both_Sides":
await callback_query.answer(
text="У вас есть активная позиция в одностороннем режиме. "
"Открытие сделки в режиме хеджирования невозможно.",
)
return
if trade_mode == "Merged_Single":
await callback_query.message.edit_text(
text="Выберите режим торговли:\n\n"
"Лонг - все сделки серии открываются на покупку.\n"
"Шорт - все сделки серии открываются на продажу.\n"
"Свитч - направление каждой сделки серии меняется по переменно.\n",
reply_markup=kbi.merged_start_trading,
)
else: # trade_mode == "Both_Sides":
await callback_query.message.edit_text(
text="Выберите режим торговли:\n\n"
"Лонг - все сделки открываются на покупку.\n"
"Шорт - все сделки открываются на продажу.\n",
reply_markup=kbi.both_start_trading,
)
logger.debug(
"Command start_trading processed successfully for user: %s",
callback_query.from_user.id,
)
except Exception as e:
await callback_query.answer(text="Произошла ошибка при запуске торговли")
logger.error(
"Error processing command start_trading for user %s: %s",
callback_query.from_user.id,
e,
)
@router_start_trading.callback_query(lambda c: c.data == "long" or c.data == "short")
async def start_trading_long(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handles the "long" or "short" callback query.
Clears the FSM state and starts the trading cycle.
:param callback_query: Message
:param state: FSMContext
:return: None
"""
try:
if callback_query.data == "long":
side = "Buy"
elif callback_query.data == "short":
side = "Sell"
else:
await callback_query.answer(text="Произошла ошибка при запуске торговли")
return
symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id)
deals = await get_active_positions_by_symbol(
tg_id=callback_query.from_user.id, symbol=symbol
)
position = next((d for d in deals if d.get("symbol") == symbol), None)
if position:
size = position.get("size", 0)
position_idx = position.get("positionIdx")
else:
size = 0
position_idx = None
if position_idx == 0 and safe_float(size) > 0:
await callback_query.answer(
text="Торговля уже запущена в одностороннем режиме для данного инструмента"
)
return
conditional_data = await rq.get_user_conditional_settings(
tg_id=callback_query.from_user.id
)
timer_start = conditional_data.timer_start
cancel_start_task_merged(user_id=callback_query.from_user.id)
async def delay_start():
if timer_start > 0:
await callback_query.message.edit_text(
text=f"Торговля будет запущена с задержкой {timer_start} мин.",
reply_markup=kbi.cancel_timer_merged,
)
await rq.set_start_timer(
tg_id=callback_query.from_user.id, timer_start=0
)
await asyncio.sleep(timer_start * 60)
await rq.set_auto_trading(
tg_id=callback_query.from_user.id,
symbol=symbol,
auto_trading=True,
side=side,
)
res = await start_trading_cycle(
tg_id=callback_query.from_user.id,
side=side,
switch_side_mode=False,
)
error_messages = {
"Limit price is out min price": "Цена лимитного ордера меньше минимального",
"Limit price is out max price": "Цена лимитного ордера больше максимального",
"Risk is too high for this trade": "Риск сделки превышает допустимый убыток",
"estimated will trigger liq": "Лимитный ордер может вызвать мгновенную ликвидацию. Проверьте параметры ордера.",
"ab not enough for new order": "Недостаточно средств для создания нового ордера",
"InvalidRequestError": "Произошла ошибка при запуске торговли.",
"Order does not meet minimum order value": "Сумма ордера не достаточна для запуска торговли",
"position idx not match position mode": "Торговля уже запущена в режиме хеджирования на продажу для данного инструмента",
"Qty invalid": "Некорректное значение ордера для данного инструмента",
"The number of contracts exceeds maximum limit allowed": "️️Количество контрактов превышает допустимое максимальное количество контрактов",
}
if res == "OK":
await callback_query.message.edit_text(text="Торговля запущена")
await state.clear()
else:
await rq.set_auto_trading(
tg_id=callback_query.from_user.id,
symbol=symbol,
auto_trading=False,
side=side,
)
text = error_messages.get(res, "Произошла ошибка при запуске торговли")
await callback_query.message.edit_text(
text=text, reply_markup=kbi.profile_bybit
)
await callback_query.message.edit_text("Запуск торговли...")
task = asyncio.create_task(delay_start())
await add_start_task_merged(user_id=callback_query.from_user.id, task=task)
except Exception as e:
await callback_query.answer(text="Произошла ошибка при запуске торговли")
logger.error(
"Error processing command long for user %s: %s",
callback_query.from_user.id,
e,
)
except asyncio.CancelledError:
logger.error("Cancelled timer for user %s", callback_query.from_user.id)
@router_start_trading.callback_query(lambda c: c.data == "switch")
async def start_trading_switch(
callback_query: CallbackQuery, state: FSMContext
) -> None:
"""
Handles the "switch" callback query.
Clears the FSM state and sends a message to the user to select the switch side.
:param callback_query: Message
:param state: FSMContext
:return: None
"""
try:
await state.clear()
await callback_query.message.edit_text(
text="Выберите направление первой сделки серии:\n\n"
"Лонг - открывается первая сделка на покупку.\n"
"Шорт - открывается первая сделка на продажу.\n"
"По направлению - сделка открывается в направлении последней сделки предыдущей серии.\n"
"Противоположно - сделка открывается в противоположном направлении последней сделки предыдущей серии.\n",
reply_markup=kbi.switch_side,
)
except Exception as e:
await callback_query.answer(text="Произошла ошибка при запуске торговли")
logger.error(
"Error processing command start trading switch for user %s: %s",
callback_query.from_user.id,
e,
)
@router_start_trading.callback_query(
lambda c: c.data
in {"switch_long", "switch_short", "switch_direction", "switch_opposite"}
)
async def start_switch(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Starts the trading cycle with the selected side.
:param callback_query:
:param state:
:return:
"""
try:
symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id)
user_deals_data = await rq.get_user_deal_by_symbol(
tg_id=callback_query.from_user.id, symbol=symbol
)
get_side = "Buy"
if user_deals_data:
get_side = user_deals_data.last_side or "Buy"
if callback_query.data == "switch_long":
side = "Buy"
elif callback_query.data == "switch_short":
side = "Sell"
elif callback_query.data == "switch_direction":
side = get_side
elif callback_query.data == "switch_opposite":
if get_side == "Buy":
side = "Sell"
else:
side = "Buy"
else:
await callback_query.answer(text="Произошла ошибка при запуске торговли")
return
deals = await get_active_positions_by_symbol(
tg_id=callback_query.from_user.id, symbol=symbol
)
position = next((d for d in deals if d.get("symbol") == symbol), None)
if position:
size = position.get("size", 0)
position_idx = position.get("positionIdx")
else:
size = 0
position_idx = None
if position_idx == 1 and safe_float(size) > 0 and side == "Buy":
await callback_query.answer(
text="Торговля уже запущена в режиме хеджирования на покупку для данного инструмента"
)
return
if position_idx == 2 and safe_float(size) > 0 and side == "Sell":
await callback_query.answer(
text="Торговля уже запущена в режиме хеджирования на продажу для данного инструмента"
)
return
conditional_data = await rq.get_user_conditional_settings(
tg_id=callback_query.from_user.id
)
timer_start = conditional_data.timer_start
cancel_start_task_switch(user_id=callback_query.from_user.id)
async def delay_start():
if timer_start > 0:
await callback_query.message.edit_text(
text=f"Торговля будет запущена с задержкой {timer_start} мин.",
reply_markup=kbi.cancel_timer_switch,
)
await rq.set_start_timer(
tg_id=callback_query.from_user.id, timer_start=0
)
await asyncio.sleep(timer_start * 60)
await rq.set_auto_trading(
tg_id=callback_query.from_user.id,
symbol=symbol,
auto_trading=True,
side=side,
)
if side == "Buy":
r_side = "Sell"
else:
r_side = "Buy"
await rq.set_auto_trading(
tg_id=callback_query.from_user.id,
symbol=symbol,
auto_trading=True,
side=r_side,
)
res = await start_trading_cycle(
tg_id=callback_query.from_user.id,
side=side,
switch_side_mode=True,
)
error_messages = {
"Limit price is out min price": "Цена лимитного ордера меньше минимального",
"Limit price is out max price": "Цена лимитного ордера больше максимального",
"Risk is too high for this trade": "Риск сделки превышает допустимый убыток",
"estimated will trigger liq": "Лимитный ордер может вызвать мгновенную ликвидацию. Проверьте параметры ордера.",
"ab not enough for new order": "Недостаточно средств для создания нового ордера",
"InvalidRequestError": "Произошла ошибка при запуске торговли.",
"Order does not meet minimum order value": "Сумма ордера не достаточна для запуска торговли",
"position idx not match position mode": "Торговля уже запущена в режиме хеджирования на продажу для данного инструмента",
"Qty invalid": "Некорректное значение ордера для данного инструмента",
"The number of contracts exceeds maximum limit allowed": " ️️Количество контрактов превышает допустимое максимальное количество контрактов",
}
if res == "OK":
await callback_query.message.edit_text(text="Торговля запущена")
await state.clear()
else:
await rq.set_auto_trading(
tg_id=callback_query.from_user.id,
symbol=symbol,
auto_trading=False,
side=side,
)
if side == "Buy":
r_side = "Sell"
else:
r_side = "Buy"
await rq.set_auto_trading(
tg_id=callback_query.from_user.id,
symbol=symbol,
auto_trading=False,
side=r_side,
)
text = error_messages.get(res, "Произошла ошибка при запуске торговли")
await callback_query.message.edit_text(
text=text, reply_markup=kbi.profile_bybit
)
await callback_query.message.edit_text("Запуск торговли...")
task = asyncio.create_task(delay_start())
await add_start_task_switch(user_id=callback_query.from_user.id, task=task)
except asyncio.CancelledError:
logger.error("Cancelled timer for user %s", callback_query.from_user.id)
except Exception as e:
await callback_query.answer(text="Произошла ошибка при запуске торговли")
logger.error(
"Error processing command start switch for user %s: %s",
callback_query.from_user.id,
e,
)
@router_start_trading.callback_query(
lambda c: c.data == "cancel_timer_merged" or c.data == "cancel_timer_switch"
)
async def cancel_start_trading(
callback_query: CallbackQuery, state: FSMContext
) -> None:
"""
Handles the "cancel_timer" callback query.
Clears the FSM state and sends a message to the user to cancel the start trading process.
:param callback_query: Message
:param state: FSMContext
:return: None
"""
try:
await state.clear()
if callback_query.data == "cancel_timer_merged":
cancel_start_task_merged(user_id=callback_query.from_user.id)
elif callback_query.data == "cancel_timer_switch":
cancel_start_task_switch(user_id=callback_query.from_user.id)
await callback_query.message.edit_text(
text="Запуск торговли отменен", reply_markup=kbi.profile_bybit
)
except Exception as e:
await callback_query.answer("Произошла ошибка при отмене запуска торговли")
logger.error(
"Error processing command cancel_timer for user %s: %s",
callback_query.from_user.id,
e,
)

View File

@@ -0,0 +1,99 @@
import asyncio
import logging.config
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery
import app.telegram.keyboards.inline as kbi
import database.request as rq
from app.telegram.tasks.tasks import add_stop_task, cancel_stop_task
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("stop_trading")
router_stop_trading = Router(name="stop_trading")
@router_stop_trading.callback_query(F.data == "stop_trading")
async def stop_all_trading(callback_query: CallbackQuery, state: FSMContext):
try:
await state.clear()
cancel_stop_task(callback_query.from_user.id)
conditional_data = await rq.get_user_conditional_settings(
tg_id=callback_query.from_user.id
)
timer_end = conditional_data.timer_end
async def delay_start():
if timer_end > 0:
await callback_query.message.edit_text(
text=f"Торговля будет остановлена с задержкой {timer_end} мин.",
reply_markup=kbi.cancel_timer_stop,
)
await rq.set_stop_timer(tg_id=callback_query.from_user.id, timer_end=0)
await asyncio.sleep(timer_end * 60)
user_auto_trading_list = await rq.get_all_user_auto_trading(
tg_id=callback_query.from_user.id
)
if any(item.auto_trading for item in user_auto_trading_list):
for active_auto_trading in user_auto_trading_list:
if active_auto_trading.auto_trading:
symbol = active_auto_trading.symbol
get_side = active_auto_trading.side
req = await rq.set_auto_trading(
tg_id=callback_query.from_user.id,
symbol=symbol,
auto_trading=False,
side=get_side,
)
if not req:
await callback_query.message.edit_text(
text="Произошла ошибка при остановке торговли",
reply_markup=kbi.profile_bybit,
)
return
await callback_query.message.edit_text(
text="Торговля остановлена", reply_markup=kbi.profile_bybit
)
else:
await callback_query.message.edit_text(text="Нет активной торговли")
task = asyncio.create_task(delay_start())
await add_stop_task(user_id=callback_query.from_user.id, task=task)
logger.debug(
"Command stop_trading processed successfully for user: %s",
callback_query.from_user.id,
)
except Exception as e:
await callback_query.answer(text="Произошла ошибка при остановке торговли")
logger.error(
"Error processing command stop_trading for user %s: %s",
callback_query.from_user.id,
e,
)
@router_stop_trading.callback_query(F.data == "cancel_timer_stop")
async def cancel_stop_trading(callback_query: CallbackQuery, state: FSMContext):
try:
await state.clear()
cancel_stop_task(callback_query.from_user.id)
await callback_query.message.edit_text(
text="Таймер отменён.", reply_markup=kbi.profile_bybit
)
except Exception as e:
await callback_query.answer(
text="Произошла ошибка при отмене остановки торговли"
)
logger.error(
"Error processing command cancel_timer_stop for user %s: %s",
callback_query.from_user.id,
e,
)

View File

@@ -0,0 +1,168 @@
import logging.config
from aiogram import Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
import app.telegram.keyboards.inline as kbi
from app.bybit.set_functions.set_tp_sl import set_tp_sl_for_position
from app.helper_functions import is_number
from app.telegram.states.states import SetTradingStopState
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("tp_sl_handlers")
router_tp_sl_handlers = Router(name="tp_sl_handlers")
@router_tp_sl_handlers.callback_query(lambda c: c.data.startswith("pos_tp_sl_"))
async def set_tp_sl_handler(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handles the 'pos_tp_sl' callback query.
Clears the current FSM state, sets the state to 'take_profit', and prompts the user to enter the take-profit.
Args:
callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard.
state (FSMContext): Finite State Machine context for the current user session.
"""
try:
await state.clear()
data = callback_query.data
parts = data.split("_")
symbol = parts[3]
position_idx = int(parts[4])
await state.set_state(SetTradingStopState.take_profit_state)
await state.update_data(symbol=symbol)
await state.update_data(position_idx=position_idx)
msg = await callback_query.message.answer(
text="Введите тейк-профит:", reply_markup=kbi.cancel
)
await state.update_data(prompt_message_id=msg.message_id)
except Exception as e:
logger.error("Error in set_tp_sl_handler: %s", e)
await callback_query.answer(text="Произошла ошибка, попробуйте позже")
@router_tp_sl_handlers.message(SetTradingStopState.take_profit_state)
async def set_take_profit_handler(message: Message, state: FSMContext) -> None:
"""
Handles the 'take_profit' state.
Clears the current FSM state, sets the state to 'stop_loss', and prompts the user to enter the stop-loss.
Args:
message (Message): Incoming message from Telegram.
state (FSMContext): Finite State Machine context for the current user session.
Returns:
None
"""
try:
try:
data = await state.get_data()
if "prompt_message_id" in data:
prompt_message_id = data["prompt_message_id"]
await message.bot.delete_message(
chat_id=message.chat.id, message_id=prompt_message_id
)
await message.delete()
except Exception as e:
if "message to delete not found" in str(e).lower():
pass # Ignore this error
else:
raise e
take_profit = message.text
if not is_number(take_profit):
await message.answer(
"Ошибка: введите валидное число.",
reply_markup=kbi.profile_bybit,
)
logger.debug(
"User %s input invalid (not an valid number): %s",
message.from_user.id,
take_profit,
)
return
await state.update_data(take_profit=take_profit)
await state.set_state(SetTradingStopState.stop_loss_state)
msg = await message.answer(text="Введите стоп-лосс:", reply_markup=kbi.cancel)
await state.update_data(prompt_message_id=msg.message_id)
except Exception as e:
logger.error("Error in set_take_profit_handler: %s", e)
await message.answer(
text="Произошла ошибка, попробуйте позже", reply_markup=kbi.profile_bybit
)
@router_tp_sl_handlers.message(SetTradingStopState.stop_loss_state)
async def set_stop_loss_handler(message: Message, state: FSMContext) -> None:
"""
Handles the 'stop_loss' state.
Clears the current FSM state, sets the state to 'take_profit', and prompts the user to enter the take-profit.
Args:
message (Message): Incoming message from Telegram.
state (FSMContext): Finite State Machine context for the current user session.
Returns:
None
"""
try:
try:
data = await state.get_data()
if "prompt_message_id" in data:
prompt_message_id = data["prompt_message_id"]
await message.bot.delete_message(
chat_id=message.chat.id, message_id=prompt_message_id
)
await message.delete()
except Exception as e:
if "message to delete not found" in str(e).lower():
pass # Ignore this error
else:
raise e
stop_loss = message.text
if not is_number(stop_loss):
await message.answer(
"Ошибка: введите валидное число.",
reply_markup=kbi.profile_bybit,
)
logger.debug(
"User %s input invalid (not an valid number): %s",
message.from_user.id,
stop_loss,
)
return
await state.update_data(stop_loss=stop_loss)
data = await state.get_data()
symbol = data["symbol"]
take_profit = data["take_profit"]
position_idx = data["position_idx"]
res = await set_tp_sl_for_position(
tg_id=message.from_user.id,
symbol=symbol,
take_profit_price=float(take_profit),
stop_loss_price=float(stop_loss),
position_idx=position_idx,
)
if res:
await message.answer(text="Тейк-профит и стоп-лосс установлены.")
else:
await message.answer(text="Тейк-профит и стоп-лосс не установлены.")
await state.clear()
except Exception as e:
await message.answer(
text="Произошла ошибка, попробуйте позже", reply_markup=kbi.profile_bybit
)
logger.error("Error in set_stop_loss_handler: %s", e)

View File

@@ -0,0 +1,484 @@
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
connect_the_platform = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="Подключить платформу", callback_data="connect_platform"
)
]
]
)
add_bybit_api = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="Добавить API", callback_data="add_bybit_api")]
]
)
profile_bybit = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="На главную", callback_data="profile_bybit")]
]
)
cancel = InlineKeyboardMarkup(
inline_keyboard=[[InlineKeyboardButton(text="Отменить", callback_data="cancel")]]
)
main_menu = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="Настройки", callback_data="main_settings")],
[
InlineKeyboardButton(
text="Сменить торговую пару", callback_data="change_symbol"
)
],
[InlineKeyboardButton(text="Мои сделки", callback_data="my_deals")],
[InlineKeyboardButton(text="Начать торговлю", callback_data="start_trading")],
[
InlineKeyboardButton(
text="Остановить торговлю", callback_data="stop_trading"
)
],
]
)
# MAIN SETTINGS
main_settings = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="Основные настройки", callback_data="additional_settings"
),
InlineKeyboardButton(
text="Риск-менеджмент", callback_data="risk_management"
),
],
[
InlineKeyboardButton(
text="Условия запуска", callback_data="conditional_settings"
)
],
[InlineKeyboardButton(text="Назад", callback_data="profile_bybit")],
]
)
# additional_settings
def get_additional_settings_keyboard(
current_order_type: str, conditional_order: str
) -> InlineKeyboardMarkup:
"""
Create keyboard for additional settings
:param current_order_type: Market, Limit or Conditional
:param conditional_order: Market or Limit
:return: InlineKeyboardMarkup
"""
buttons = [
[
InlineKeyboardButton(text="Режим позиции", callback_data="trade_mode"),
InlineKeyboardButton(text="Тип маржи", callback_data="margin_type"),
],
[
InlineKeyboardButton(
text="Размер кредитного плеча", callback_data="leverage"
),
InlineKeyboardButton(text="Тип ордера", callback_data="order_type"),
],
[
InlineKeyboardButton(
text="Количество ордера", callback_data="order_quantity"
),
InlineKeyboardButton(
text="Коэффициент мартингейла", callback_data="martingale_factor"
),
],
]
if current_order_type == "Conditional":
buttons.append(
[
InlineKeyboardButton(
text="Тип условного ордера", callback_data="conditional_order_type"
)
]
)
buttons.append(
[InlineKeyboardButton(text="Триггер цена", callback_data="trigger_price")]
)
if conditional_order == "Limit":
buttons.append(
[
InlineKeyboardButton(
text="Цена лимитного ордера", callback_data="limit_price"
)
]
)
elif current_order_type == "Limit":
buttons.append(
[
InlineKeyboardButton(
text="Цена лимитного ордера", callback_data="limit_price"
)
]
)
buttons.append(
[
InlineKeyboardButton(
text="Максимальное кол-во ставок в серии",
callback_data="max_bets_in_series",
)
]
)
buttons.append(
[
InlineKeyboardButton(text="Назад", callback_data="main_settings"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
]
)
return InlineKeyboardMarkup(inline_keyboard=buttons)
order_type = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Рыночный", callback_data="Market"),
InlineKeyboardButton(text="Лимитный", callback_data="Limit"),
],
[InlineKeyboardButton(text="Условный", callback_data="Conditional")],
[
InlineKeyboardButton(text="Назад", callback_data="additional_settings"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
conditional_order_type = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Рыночный", callback_data="set_market"),
InlineKeyboardButton(text="Лимитный", callback_data="set_limit"),
],
[
InlineKeyboardButton(text="Назад", callback_data="additional_settings"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
trade_mode = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="Односторонний режим", callback_data="Merged_Single"
),
InlineKeyboardButton(text="Хеджирование", callback_data="Both_Sides"),
],
[
InlineKeyboardButton(text="Назад", callback_data="additional_settings"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
margin_type = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Изолированная", callback_data="ISOLATED_MARGIN"),
InlineKeyboardButton(text="Кросс", callback_data="REGULAR_MARGIN"),
],
[
InlineKeyboardButton(text="Назад", callback_data="additional_settings"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
back_to_additional_settings = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Назад", callback_data="additional_settings"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
change_limit_price = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="Установить цену", callback_data="set_limit_price"
),
InlineKeyboardButton(text="Последняя цена", callback_data="last_price"),
],
[
InlineKeyboardButton(text="Назад", callback_data="additional_settings"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
back_to_change_limit_price = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Назад", callback_data="limit_price"),
InlineKeyboardButton(
text="Основные настройки", callback_data="additional_settings"
),
],
[InlineKeyboardButton(text="На главную", callback_data="profile_bybit")],
]
)
# risk_management
risk_management = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="Изм. цены прибыли", callback_data="take_profit_percent"
),
InlineKeyboardButton(
text="Изм. цены убытка", callback_data="stop_loss_percent"
),
],
[
InlineKeyboardButton(
text="Максимальный риск", callback_data="max_risk_percent"
)
],
[InlineKeyboardButton(text="Комиссия биржи", callback_data="commission_fee")],
[
InlineKeyboardButton(text="Назад", callback_data="main_settings"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
back_to_risk_management = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Назад", callback_data="risk_management"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
commission_fee = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Да", callback_data="Yes_commission_fee"),
InlineKeyboardButton(text="Нет", callback_data="No_commission_fee"),
],
[
InlineKeyboardButton(text="Назад", callback_data="risk_management"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
# conditions
conditions = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Таймер для старта", callback_data="start_timer"),
InlineKeyboardButton(
text="Таймер для остановки", callback_data="stop_timer"
),
],
[
InlineKeyboardButton(text="Назад", callback_data="main_settings"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
back_to_conditions = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Назад", callback_data="conditional_settings"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
# SYMBOL
symbol = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Назад", callback_data="profile_bybit"),
],
]
)
# POSITION
change_position = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Позиции", callback_data="change_position"),
InlineKeyboardButton(text="Открытые ордера", callback_data="open_orders"),
],
[InlineKeyboardButton(text="Назад", callback_data="profile_bybit")],
]
)
def create_active_positions_keyboard(symbols: list):
builder = InlineKeyboardBuilder()
for sym, side in symbols:
builder.button(text=f"{sym}:{side}", callback_data=f"get_position_{sym}_{side}")
builder.button(text="Назад", callback_data="my_deals")
builder.button(text="На главную", callback_data="profile_bybit")
builder.adjust(2)
return builder.as_markup()
def make_close_position_keyboard(
symbol_pos: str, side: str, position_idx: int, qty: int
):
return InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="Закрыть позицию",
callback_data=f"close_position_{symbol_pos}_{side}_{position_idx}_{qty}",
)
],
[
InlineKeyboardButton(
text="Установить TP/SL",
callback_data=f"pos_tp_sl_{symbol_pos}_{position_idx}",
)
],
[
InlineKeyboardButton(text="Назад", callback_data="change_position"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
def create_active_orders_keyboard(orders: list):
builder = InlineKeyboardBuilder()
for order, side in orders:
builder.button(text=f"{order}", callback_data=f"get_order_{order}_{side}")
builder.button(text="Назад", callback_data="my_deals")
builder.button(text="На главную", callback_data="profile_bybit")
builder.adjust(2)
return builder.as_markup()
def make_close_orders_keyboard(symbol_order: str, order_id: str):
return InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="Закрыть ордер",
callback_data=f"close_order_{symbol_order}_{order_id}",
)
],
[
InlineKeyboardButton(text="Назад", callback_data="open_orders"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
# START TRADING
merged_start_trading = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Лонг", callback_data="long"),
InlineKeyboardButton(text="Шорт", callback_data="short"),
],
[InlineKeyboardButton(text="Свитч", callback_data="switch")],
[InlineKeyboardButton(text="Назад", callback_data="profile_bybit")],
]
)
both_start_trading = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Лонг", callback_data="long"),
InlineKeyboardButton(text="Шорт", callback_data="short"),
],
[InlineKeyboardButton(text="Назад", callback_data="profile_bybit")],
]
)
switch_side = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Лонг", callback_data="switch_long"),
InlineKeyboardButton(text="Шорт", callback_data="switch_short"),
],
[
InlineKeyboardButton(
text="По направлению", callback_data="switch_direction"
),
InlineKeyboardButton(
text="Противоположно", callback_data="switch_opposite"
),
],
[
InlineKeyboardButton(text="Назад", callback_data="start_trading"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
back_to_start_trading = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Назад", callback_data="start_trading"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
cancel_timer_merged = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="Отменить таймер", callback_data="cancel_timer_merged"
)
],
[
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
cancel_timer_switch = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="Отменить таймер", callback_data="cancel_timer_switch"
)
],
[
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
# STOP TRADING
cancel_timer_stop = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="Отменить таймер", callback_data="cancel_timer_stop"
)
],
[
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)

View File

@@ -0,0 +1,9 @@
from aiogram.types import KeyboardButton, ReplyKeyboardMarkup
profile = ReplyKeyboardMarkup(
keyboard=[[KeyboardButton(text="Панель Bybit"), KeyboardButton(text="Профиль")],
[KeyboardButton(text="Подключить платформу Bybit")]],
resize_keyboard=True,
one_time_keyboard=True,
input_field_placeholder="Выберите пункт меню...",
)

View File

View File

@@ -0,0 +1,51 @@
from aiogram.fsm.state import State, StatesGroup
class AddBybitApiState(StatesGroup):
"""States for adding Bybit API keys."""
api_key_state = State()
api_secret_state = State()
class AdditionalSettingsState(StatesGroup):
"""States for additional settings."""
leverage_state = State()
leverage_to_buy_state = State()
leverage_to_sell_state = State()
quantity_state = State()
martingale_factor_state = State()
max_bets_in_series_state = State()
limit_price_state = State()
trigger_price_state = State()
class RiskManagementState(StatesGroup):
"""States for risk management."""
take_profit_percent_state = State()
stop_loss_percent_state = State()
max_risk_percent_state = State()
commission_fee_state = State()
class ConditionalSettingsState(StatesGroup):
"""States for conditional settings."""
start_timer_state = State()
stop_timer_state = State()
class ChangingTheSymbolState(StatesGroup):
"""States for changing the symbol."""
symbol_state = State()
class SetTradingStopState(StatesGroup):
"""States for setting a trading stop."""
symbol_state = State()
take_profit_state = State()
stop_loss_state = State()

View File

View File

@@ -0,0 +1,77 @@
import asyncio
import logging.config
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("tasks")
user_start_tasks_merged = {}
user_start_tasks_switch = {}
user_stop_tasks = {}
async def add_start_task_merged(user_id: int, task: asyncio.Task):
"""Add task to user_start_tasks dict"""
if user_id in user_start_tasks_merged:
old_task = user_start_tasks_merged[user_id]
if not old_task.done():
old_task.cancel()
try:
await old_task
except asyncio.CancelledError:
pass
user_start_tasks_merged[user_id] = task
async def add_start_task_switch(user_id: int, task: asyncio.Task):
"""Add task to user_start_tasks dict"""
if user_id in user_start_tasks_switch:
old_task = user_start_tasks_switch[user_id]
if not old_task.done():
old_task.cancel()
try:
await old_task
except asyncio.CancelledError:
pass
user_start_tasks_switch[user_id] = task
async def add_stop_task(user_id: int, task: asyncio.Task):
"""Add task to user_stop_tasks dict"""
if user_id in user_stop_tasks:
old_task = user_stop_tasks[user_id]
if not old_task.done():
old_task.cancel()
try:
await old_task
except asyncio.CancelledError:
pass
user_stop_tasks[user_id] = task
def cancel_start_task_merged(user_id: int):
"""Cancel task from user_start_tasks dict"""
if user_id in user_start_tasks_merged:
task = user_start_tasks_merged[user_id]
if not task.done():
task.cancel()
del user_start_tasks_merged[user_id]
def cancel_start_task_switch(user_id: int):
"""Cancel task from user_start_tasks dict"""
if user_id in user_start_tasks_switch:
task = user_start_tasks_switch[user_id]
if not task.done():
task.cancel()
del user_start_tasks_switch[user_id]
def cancel_stop_task(user_id: int):
"""Cancel task from user_stop_tasks dict"""
if user_id in user_stop_tasks:
task = user_stop_tasks[user_id]
if not task.done():
task.cancel()
del user_stop_tasks[user_id]

View File

@@ -1,9 +1,31 @@
from dotenv import load_dotenv, find_dotenv
import os
from dotenv import load_dotenv, find_dotenv
import logging.config
env_file = find_dotenv()
load_dotenv(env_file)
from logger_helper.logger_helper import LOGGING_CONFIG
TOKEN_TG_BOT_1 = os.getenv('TOKEN_TELEGRAM_BOT_1')
TOKEN_TG_BOT_2 = os.getenv('TOKEN_TELEGRAM_BOT_2')
TOKEN_TG_BOT_3 = os.getenv('TOKEN_TELEGRAM_BOT_3')
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("config")
env_path = find_dotenv()
if env_path:
load_dotenv(env_path)
logging.info(f"Loaded env from {env_path}")
else:
logging.warning(".env file not found, environment variables won't be loaded")
BOT_TOKEN = os.getenv('BOT_TOKEN')
if not BOT_TOKEN:
logging.error("BOT_TOKEN is not set in environment variables")
DB_USER = os.getenv('DB_USER')
DB_PASS = os.getenv('DB_PASS')
DB_HOST = os.getenv('DB_HOST')
DB_PORT = os.getenv('DB_PORT')
DB_NAME = os.getenv('DB_NAME')
if not all([DB_USER, DB_PASS, DB_HOST, DB_PORT, DB_NAME]):
logger.error("One or more database environment variables are not set")
DATABASE_URL = f"postgresql+asyncpg://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}"

24
database/__init__.py Normal file
View File

@@ -0,0 +1,24 @@
import logging.config
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine, AsyncSession
from database.models import Base
from config import DATABASE_URL
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("database")
async_engine = create_async_engine(DATABASE_URL, echo=False)
async_session = async_sessionmaker(async_engine, class_=AsyncSession, expire_on_commit=False)
async def init_db():
try:
async with async_engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
logger.info("Database initialized.")
except Exception as e:
logger.error("Database initialization failed: %s", e)

190
database/models.py Normal file
View File

@@ -0,0 +1,190 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy import Column, ForeignKey, Integer, String, BigInteger, Float, Boolean, UniqueConstraint
from sqlalchemy.orm import relationship
Base = declarative_base(cls=AsyncAttrs)
class User(Base):
"""User model."""
__tablename__ = "users"
id = Column(Integer, primary_key=True, autoincrement=True)
tg_id = Column(BigInteger, nullable=False, unique=True)
username = Column(String, nullable=False)
user_api = relationship("UserApi",
back_populates="user",
cascade="all, delete-orphan",
passive_deletes=True,
uselist=False)
user_symbol = relationship("UserSymbol",
back_populates="user",
cascade="all, delete-orphan",
passive_deletes=True,
uselist=False)
user_additional_settings = relationship("UserAdditionalSettings",
back_populates="user",
cascade="all, delete-orphan",
passive_deletes=True,
uselist=False)
user_risk_management = relationship("UserRiskManagement",
back_populates="user",
cascade="all, delete-orphan",
passive_deletes=True,
uselist=False)
user_conditional_settings = relationship("UserConditionalSettings",
back_populates="user",
cascade="all, delete-orphan",
passive_deletes=True,
uselist=False)
user_deals = relationship("UserDeals",
back_populates="user",
cascade="all, delete-orphan",
passive_deletes=True)
user_auto_trading = relationship("UserAutoTrading",
back_populates="user",
cascade="all, delete-orphan",
passive_deletes=True)
class UserApi(Base):
"""User API model."""
__tablename__ = "user_api"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer,
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False, unique=True)
api_key = Column(String, nullable=False)
api_secret = Column(String, nullable=False)
user = relationship("User", back_populates="user_api")
class UserSymbol(Base):
"""User symbol model."""
__tablename__ = "user_symbol"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer,
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False, unique=True)
symbol = Column(String, nullable=False, default="BTCUSDT")
user = relationship("User", back_populates="user_symbol")
class UserAdditionalSettings(Base):
"""User additional settings model."""
__tablename__ = "user_additional_settings"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer,
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False, unique=True)
trade_mode = Column(String, nullable=False, default="Merged_Single")
order_type = Column(String, nullable=False, default="Market")
conditional_order_type = Column(String, nullable=False, default="Market")
limit_price = Column(Float, nullable=False, default=0.0)
trigger_price = Column(Float, nullable=False, default=0.0)
margin_type = Column(String, nullable=False, default="ISOLATED_MARGIN")
leverage = Column(String, nullable=False, default="10")
leverage_to_buy = Column(String, nullable=False, default="10")
leverage_to_sell = Column(String, nullable=False, default="10")
order_quantity = Column(Float, nullable=False, default=5.0)
martingale_factor = Column(Float, nullable=False, default=1.0)
max_bets_in_series = Column(Integer, nullable=False, default=1)
user = relationship("User", back_populates="user_additional_settings")
class UserRiskManagement(Base):
"""User risk management model."""
__tablename__ = "user_risk_management"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer,
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False, unique=True)
take_profit_percent = Column(Integer, nullable=False, default=1)
stop_loss_percent = Column(Integer, nullable=False, default=1)
max_risk_percent = Column(Integer, nullable=False, default=100)
commission_fee = Column(String, nullable=False, default="Yes_commission_fee")
user = relationship("User", back_populates="user_risk_management")
class UserConditionalSettings(Base):
"""User conditional settings model."""
__tablename__ = "user_conditional_settings"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer,
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False, unique=True)
timer_start = Column(Integer, nullable=False, default=0)
timer_end = Column(Integer, nullable=False, default=0)
user = relationship("User", back_populates="user_conditional_settings")
class UserDeals(Base):
"""User deals model."""
__tablename__ = "user_deals"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer,
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False)
current_step = Column(Integer, nullable=True)
symbol = Column(String, nullable=True)
trade_mode = Column(String, nullable=True)
trading_type = Column(String, nullable=True)
margin_type = Column(String, nullable=True)
order_type = Column(String, nullable=True)
conditional_order_type = Column(String, nullable=True)
leverage = Column(String, nullable=True)
leverage_to_buy = Column(String, nullable=True)
leverage_to_sell = Column(String, nullable=True)
last_side = Column(String, nullable=True)
closed_side = Column(String, nullable=True)
order_quantity = Column(Float, nullable=True)
martingale_factor = Column(Float, nullable=True)
max_bets_in_series = Column(Integer, nullable=True)
take_profit_percent = Column(Integer, nullable=True)
stop_loss_percent = Column(Integer, nullable=True)
max_risk_percent = Column(Integer, nullable=True)
switch_side_mode = Column(Boolean, nullable=True)
limit_price = Column(Float, nullable=True)
trigger_price = Column(Float, nullable=True)
user = relationship("User", back_populates="user_deals")
__table_args__ = (
UniqueConstraint('user_id', 'symbol', name='uq_user_symbol'),
)
class UserAutoTrading(Base):
"""User auto trading model."""
__tablename__ = "user_auto_trading"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer,
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False)
symbol = Column(String, nullable=True)
auto_trading = Column(Boolean, nullable=True)
side = Column(String, nullable=True)
fee = Column(Float, nullable=True)
user = relationship("User", back_populates="user_auto_trading")

1366
database/request.py Normal file

File diff suppressed because it is too large Load Diff

View File

View File

@@ -2,15 +2,18 @@ import os
current_directory = os.path.dirname(os.path.abspath(__file__))
log_directory = os.path.join(current_directory, 'loggers')
error_log_directory = os.path.join(log_directory, 'errors')
os.makedirs(log_directory, exist_ok=True)
os.makedirs(error_log_directory, exist_ok=True)
log_filename = os.path.join(log_directory, 'app.log')
error_log_filename = os.path.join(error_log_directory, 'error.log')
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"format": "TELEGRAM: %(asctime)s - %(name)s - %(levelname)s - %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S", # Формат даты
},
},
@@ -23,90 +26,122 @@ LOGGING_CONFIG = {
"backupCount": 7, # Количество сохраняемых архивов (0 - не сохранять)
"formatter": "default",
"encoding": "utf-8",
"level": "DEBUG",
},
"error_file": {
"class": "logging.handlers.TimedRotatingFileHandler",
"filename": error_log_filename,
"when": "midnight",
"interval": 1,
"backupCount": 30,
"formatter": "default",
"encoding": "utf-8",
"level": "ERROR",
},
"console": {
"class": "logging.StreamHandler",
"formatter": "default",
"level": "DEBUG",
},
},
"loggers": {
"main": {
"handlers": ["console", "timed_rotating_file"],
"run": {
"handlers": ["console", "timed_rotating_file", "error_file"],
"level": "DEBUG",
"propagate": False,
},
"config": {
"handlers": ["console", "timed_rotating_file", "error_file"],
"level": "DEBUG",
"propagate": False,
},
"common": {
"handlers": ["console", "timed_rotating_file", "error_file"],
"level": "DEBUG",
"propagate": False,
},
"handlers_main": {
"handlers": ["console", "timed_rotating_file", "error_file"],
"level": "DEBUG",
"propagate": False,
},
"database": {
"handlers": ["console", "timed_rotating_file", "error_file"],
"level": "DEBUG",
"propagate": False,
},
"request": {
"handlers": ["console", "timed_rotating_file", "error_file"],
"level": "DEBUG",
"propagate": False,
},
"add_bybit_api": {
"handlers": ["console", "timed_rotating_file"],
"handlers": ["console", "timed_rotating_file", "error_file"],
"level": "DEBUG",
"propagate": False,
},
"balance": {
"handlers": ["console", "timed_rotating_file"],
"profile_tg": {
"handlers": ["console", "timed_rotating_file", "error_file"],
"level": "DEBUG",
"propagate": False,
},
"functions": {
"handlers": ["console", "timed_rotating_file"],
"settings": {
"handlers": ["console", "timed_rotating_file", "error_file"],
"level": "DEBUG",
"propagate": False,
},
"futures": {
"handlers": ["console", "timed_rotating_file"],
"additional_settings": {
"handlers": ["console", "timed_rotating_file", "error_file"],
"level": "DEBUG",
"propagate": False,
},
"get_valid_symbol": {
"handlers": ["console", "timed_rotating_file"],
"helper_functions": {
"handlers": ["console", "timed_rotating_file", "error_file"],
"level": "DEBUG",
"propagate": False,
},
"min_qty": {
"handlers": ["console", "timed_rotating_file"],
"risk_management": {
"handlers": ["console", "timed_rotating_file", "error_file"],
"level": "DEBUG",
"propagate": False,
},
"price_symbol": {
"handlers": ["console", "timed_rotating_file"],
"start_trading": {
"handlers": ["console", "timed_rotating_file", "error_file"],
"level": "DEBUG",
"propagate": False,
},
"requests": {
"handlers": ["console", "timed_rotating_file"],
"stop_trading": {
"handlers": ["console", "timed_rotating_file", "error_file"],
"level": "DEBUG",
"propagate": False,
},
"handlers": {
"handlers": ["console", "timed_rotating_file"],
"changing_the_symbol": {
"handlers": ["console", "timed_rotating_file", "error_file"],
"level": "DEBUG",
"propagate": False,
},
"condition_settings": {
"handlers": ["console", "timed_rotating_file"],
"conditional_settings": {
"handlers": ["console", "timed_rotating_file", "error_file"],
"level": "DEBUG",
"propagate": False,
},
"main_settings": {
"handlers": ["console", "timed_rotating_file"],
"get_positions_handlers": {
"handlers": ["console", "timed_rotating_file", "error_file"],
"level": "DEBUG",
"propagate": False,
},
"risk_management_settings": {
"handlers": ["console", "timed_rotating_file"],
"close_orders": {
"handlers": ["console", "timed_rotating_file", "error_file"],
"level": "DEBUG",
"propagate": False,
},
"models": {
"handlers": ["console", "timed_rotating_file"],
"level": "DEBUG",
"propagate": False,
},
"bybit_ws": {
"handlers": ["console", "timed_rotating_file"],
"tp_sl_handlers": {
"handlers": ["console", "timed_rotating_file", "error_file"],
"level": "DEBUG",
"propagate": False,
},
"tasks": {
"handlers": ["console", "timed_rotating_file"],
"handlers": ["console", "timed_rotating_file", "error_file"],
"level": "DEBUG",
"propagate": False,
},

View File

@@ -6,6 +6,7 @@ aiosignal==1.4.0
aiosqlite==0.21.0
alembic==1.16.5
annotated-types==0.7.0
asyncpg==0.30.0
attrs==25.3.0
black==25.1.0
certifi==2025.8.3
@@ -32,10 +33,12 @@ packaging==25.0
pathspec==0.12.1
platformdirs==4.4.0
propcache==0.3.2
psycopg==3.2.10
psycopg-binary==3.2.10
pybit==5.11.0
pycodestyle==2.14.0
pycryptodome==3.23.0
pydantic==2.11.7
pydantic==2.11.9
pydantic_core==2.33.2
pyflakes==3.4.0
python-dotenv==1.1.1
@@ -45,7 +48,8 @@ requests==2.32.5
six==1.17.0
SQLAlchemy==2.0.43
typing-inspection==0.4.1
typing_extensions==4.14.1
typing_extensions==4.15.0
uliweb-alembic==0.6.9
urllib3==2.5.0
websocket-client==1.8.0
yarl==1.20.1

56
run.py Normal file
View File

@@ -0,0 +1,56 @@
import asyncio
import contextlib
import logging.config
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.redis import RedisStorage
from app.bybit.web_socket import WebSocketBot
from app.telegram.handlers import router
from config import BOT_TOKEN
from database import init_db
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("run")
async def main():
"""
The main function of launching the bot.
Performs database initialization, creation of bot and dispatcher objects,
then it triggers the long polling event.
Logs important events and errors.
"""
try:
await init_db()
bot = Bot(token=BOT_TOKEN)
storage = RedisStorage.from_url("redis://localhost:6379")
dp = Dispatcher(storage=storage)
dp.include_router(router)
web_socket = WebSocketBot(telegram_bot=bot)
await web_socket.clear_user_sockets()
ws_task = asyncio.create_task(web_socket.run_user_check_loop())
tg_task = asyncio.create_task(dp.start_polling(bot))
try:
logger.info("Bot started")
await asyncio.gather(ws_task, tg_task)
except Exception as e:
logger.error("Bot stopped with error: %s", e)
finally:
for task in (ws_task, tg_task):
task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await ws_task
await tg_task
await web_socket.clear_user_sockets()
except Exception as e:
logger.error("Bot stopped with error: %s", e)
if __name__ == "__main__":
asyncio.run(main())