12 Commits

Author SHA1 Message Date
algizn97
72ed35ccf8 The price of the trading pair has been removed, and the trade cancellation button has been removed. The text has been corrected 2025-10-10 14:00:58 +05:00
algizn97
b890df9af8 When choosing a coin, the leverage is set to the maximum possible for this coin, also SL in accordance with the leverage. The verification range from 1 to 100 has been removed, now the verification is within the acceptable values from the exchange 2025-10-10 14:00:42 +05:00
algizn97
e40fa91125 Added the ability to summarize all commissions within a series.Minor bugs have been fixed 2025-10-10 14:00:28 +05:00
algizn97
3ec9d00650 The currency of the coin is treason on USDT, unnecessary parameters are removed 2025-10-10 14:00:01 +05:00
algizn97
e792130332 Unnecessary buttons have been removed, the buttons of the trading mode and the direction of the first transaction of the series have been moved. 2025-10-10 13:59:47 +05:00
algizn97
1a1a5a727f When adjusting the leverage, the SL changes according to the criteria. In place of the position mode, there is now a trading mode. All unnecessary functions are also removed. 2025-10-10 13:59:24 +05:00
algizn97
9f9a79bf81 The function allows you to write the number 0 2025-10-10 13:59:04 +05:00
algizn97
58397c4723 The stop trading button has been removed. 2025-10-10 13:58:23 +05:00
algizn97
6bfb816d2a Fixed percentages of TP and SL from integers to floats 2025-10-10 13:57:29 +05:00
algizn97
fb82f365f2 The trading mode has been moved to the main settings, Position mode, limit order and conditional order have been removed. The number of bids has been renamed to the base rate. The choice of the direction of the first transaction has been moved to the main settings 2025-10-10 13:57:13 +05:00
algizn97
0945be242a Added the addition of a common commission 2025-10-10 13:56:43 +05:00
algizn97
4663888190 TP AND SL have been converted to float. Switch control has been moved to the main settings, Removed unnecessary parameters 2025-10-10 13:55:42 +05:00
53 changed files with 1793 additions and 1583 deletions

View File

@@ -1 +1,6 @@
BOT_TOKEN=YOUR_BOT_TOKEN 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

6
.gitignore vendored
View File

@@ -146,9 +146,6 @@ myenv
ENV/ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
/logger_helper/loggers
/app/bybit/logger_bybit/loggers
*.db
# Spyder project settings # Spyder project settings
.spyderproject .spyderproject
.spyproject .spyproject
@@ -210,6 +207,3 @@ cython_debug/
marimo/_static/ marimo/_static/
marimo/_lsp/ marimo/_lsp/
__marimo__/ __marimo__/
stcs_venv
venv

View File

@@ -1,11 +1,6 @@
# Crypto Trading Telegram Bot by [KODORVAN](https://git.svoboda.works/kodorvan) Crypto Trading Telegram Bot
Автоматизированный торговый помощник для работы с криптовалютной биржей Bybit на основе стратегии мартингейла.<br> Этот бот — автоматизированный торговый помощник для работы с криптовалютной биржей Bybit на основе стратегии мартингейла. Он позволяет торговать бессрочными контрактами с управлением рисками, тейк-профитами, стоп-лоссами и кредитным плечом.
Он позволяет торговать бессрочными контрактами с управлением рисками, тейк-профитами, стоп-лоссами и кредитным плечом.
**Разработано командой [КОДОРВАНЬ](https://git.svoboda.works/kodorvan)**<br>
_Мы окажем полное содействие и поддержку, пишите в телеграм: https://t.me/kodorvan_
## Основные возможности ## Основные возможности
@@ -59,17 +54,8 @@ sudo -u www-data /usr/bin/pip install -r requirements.txt
cp .env.sample .env cp .env.sample .env
nvim .env nvim .env
``` ```
5. Выполните миграции:
```bash
alembic upgrade head
```
6. Убедитесь в том, что установлен redis и открыт порт 6789 5. Запустите бота:
```bash
sudo ufw allow 6789
```
7. Запустите бота:
```bash ```bash
python run.py python run.py
@@ -124,4 +110,4 @@ sudo service stcs status
- Бот требует аккуратной настройки параметров риска. - Бот требует аккуратной настройки параметров риска.
- Храните API ключи в безопасности, избегайте публикации. - Храните API ключи в безопасности, избегайте публикации.

View File

@@ -84,7 +84,7 @@ path_separator = os
# database URL. This is consumed by the user-maintained env.py script only. # database URL. This is consumed by the user-maintained env.py script only.
# other means of configuring database URLs may be customized within the env.py # other means of configuring database URLs may be customized within the env.py
# file. # file.
sqlalchemy.url = sqlite+aiosqlite:///./database/stcs.db sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks] [post_write_hooks]

View File

@@ -1,29 +1,73 @@
import asyncio import asyncio
from logging.config import fileConfig from logging.config import fileConfig
from sqlalchemy import pool from sqlalchemy import pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
from config import DATABASE_URL
config = context.config config = context.config
config.set_main_option('sqlalchemy.url', DATABASE_URL)
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None: if config.config_file_name is not None:
fileConfig(config.config_file_name) fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from database.models import Base from database.models import Base
target_metadata = Base.metadata target_metadata = Base.metadata
def do_run_migrations(connection): # other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
context.configure( context.configure(
connection=connection, url=DATABASE_URL,
target_metadata=target_metadata, target_metadata=target_metadata,
compare_type=True, literal_binds=True,
dialect_opts={"paramstyle": "named"},
) )
with context.begin_transaction(): with context.begin_transaction():
context.run_migrations() context.run_migrations()
async def run_async_migrations():
def do_run_migrations(connection: Connection) -> None:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
async def run_async_migrations() -> None:
"""In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = async_engine_from_config( connectable = async_engine_from_config(
config.get_section(config.config_ini_section), config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.", prefix="sqlalchemy.",
poolclass=pool.NullPool, poolclass=pool.NullPool,
) )
@@ -33,20 +77,13 @@ async def run_async_migrations():
await connectable.dispose() await connectable.dispose()
def run_migrations_offline():
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online(): def run_migrations_online() -> None:
"""Run migrations in 'online' mode."""
asyncio.run(run_async_migrations()) asyncio.run(run_async_migrations())
if context.is_offline_mode(): if context.is_offline_mode():
run_migrations_offline() run_migrations_offline()
else: else:

View File

@@ -0,0 +1,36 @@
"""updated user deals table
Revision ID: 09db71875980
Revises: 77197715747c
Create Date: 2025-09-29 12:57:39.943294
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '09db71875980'
down_revision: Union[str, Sequence[str], None] = '77197715747c'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user_deals', sa.Column('order_quantity', sa.Float(), nullable=True))
op.create_unique_constraint('uq_user_symbol', 'user_deals', ['user_id', 'symbol'])
op.drop_column('user_deals', 'quantity')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user_deals', sa.Column('quantity', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
op.drop_constraint('uq_user_symbol', 'user_deals', type_='unique')
op.drop_column('user_deals', 'order_quantity')
# ### end Alembic commands ###

View File

@@ -0,0 +1,40 @@
"""Added conditional_order_type
Revision ID: 0eed68eddcdb
Revises: 70094ba27e80
Create Date: 2025-09-24 13:47:23.282807
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '0eed68eddcdb'
down_revision: Union[str, Sequence[str], None] = '70094ba27e80'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user_additional_settings', sa.Column('conditional_order_type', sa.String(), nullable=False))
op.add_column('user_additional_settings', sa.Column('limit_price', sa.Float(), nullable=False))
op.add_column('user_additional_settings', sa.Column('trigger_price', sa.Float(), nullable=False))
op.drop_column('user_conditional_settings', 'trigger_price')
op.drop_column('user_conditional_settings', 'limit_price')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user_conditional_settings', sa.Column('limit_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=False))
op.add_column('user_conditional_settings', sa.Column('trigger_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=False))
op.drop_column('user_additional_settings', 'trigger_price')
op.drop_column('user_additional_settings', 'limit_price')
op.drop_column('user_additional_settings', 'conditional_order_type')
# ### end Alembic commands ###

View File

@@ -0,0 +1,34 @@
"""Added fee for user auto trading
Revision ID: 10bf073c71f9
Revises: 2b9572b49ecd
Create Date: 2025-10-02 17:52:05.235523
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '10bf073c71f9'
down_revision: Union[str, Sequence[str], None] = '2b9572b49ecd'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user_auto_trading', sa.Column('fee', sa.Float(), nullable=True))
op.drop_column('user_deals', 'fee')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user_deals', sa.Column('fee', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
op.drop_column('user_auto_trading', 'fee')
# ### end Alembic commands ###

View File

@@ -0,0 +1,34 @@
"""Added side for user auto trading
Revision ID: 2b9572b49ecd
Revises: ef342b38e17b
Create Date: 2025-10-02 17:21:20.904797
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '2b9572b49ecd'
down_revision: Union[str, Sequence[str], None] = 'ef342b38e17b'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user_auto_trading', sa.Column('side', sa.String(), nullable=True))
op.drop_constraint(op.f('uq_user_auto_trading_symbol'), 'user_auto_trading', type_='unique')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_unique_constraint(op.f('uq_user_auto_trading_symbol'), 'user_auto_trading', ['user_id', 'symbol'], postgresql_nulls_not_distinct=False)
op.drop_column('user_auto_trading', 'side')
# ### end Alembic commands ###

View File

@@ -0,0 +1,32 @@
"""update last side the conditional data
Revision ID: 3534adf891fc
Revises: ef38c90eed55
Create Date: 2025-09-30 08:39:02.971158
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '3534adf891fc'
down_revision: Union[str, Sequence[str], None] = 'ef38c90eed55'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@@ -0,0 +1,38 @@
"""Updated martingale factor
Revision ID: 42c66cfe8d4e
Revises: 45977e9d8558
Create Date: 2025-09-22 17:17:39.779979
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '42c66cfe8d4e'
down_revision: Union[str, Sequence[str], None] = '45977e9d8558'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('user_additional_settings', 'martingale_factor',
existing_type=sa.INTEGER(),
type_=sa.Float(),
existing_nullable=False)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('user_additional_settings', 'martingale_factor',
existing_type=sa.Float(),
type_=sa.INTEGER(),
existing_nullable=False)
# ### end Alembic commands ###

View File

@@ -0,0 +1,38 @@
"""Updated order quantity
Revision ID: 45977e9d8558
Revises: fd8581c0cc87
Create Date: 2025-09-22 16:59:40.415398
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '45977e9d8558'
down_revision: Union[str, Sequence[str], None] = 'fd8581c0cc87'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('user_additional_settings', 'order_quantity',
existing_type=sa.INTEGER(),
type_=sa.Float(),
existing_nullable=False)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('user_additional_settings', 'order_quantity',
existing_type=sa.Float(),
type_=sa.INTEGER(),
existing_nullable=False)
# ### end Alembic commands ###

View File

@@ -0,0 +1,40 @@
"""Create User Conditional Setting
Revision ID: 70094ba27e80
Revises: 42c66cfe8d4e
Create Date: 2025-09-23 16:47:07.161544
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '70094ba27e80'
down_revision: Union[str, Sequence[str], None] = '42c66cfe8d4e'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('user_conditional_settings',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('limit_price', sa.Float(), nullable=False),
sa.Column('trigger_price', sa.Float(), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id')
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('user_conditional_settings')
# ### end Alembic commands ###

View File

@@ -0,0 +1,42 @@
"""added user_auto_trading table
Revision ID: 73a00faa4f7f
Revises: 968f8121104f
Create Date: 2025-10-01 12:30:21.830851
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '73a00faa4f7f'
down_revision: Union[str, Sequence[str], None] = '968f8121104f'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('user_auto_trading',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('symbol', sa.String(), nullable=True),
sa.Column('auto_trading', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id', 'symbol', name='uq_user_auto_trading_symbol')
)
op.drop_column('user_conditional_settings', 'auto_trading')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user_conditional_settings', sa.Column('auto_trading', sa.BOOLEAN(), autoincrement=False, nullable=True))
op.drop_table('user_auto_trading')
# ### end Alembic commands ###

View File

@@ -0,0 +1,32 @@
"""deleted position_idx for user deals table
Revision ID: 77197715747c
Revises: 8f1476c68efa
Create Date: 2025-09-29 12:20:18.928995
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '77197715747c'
down_revision: Union[str, Sequence[str], None] = '8f1476c68efa'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('user_deals', 'position_idx')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user_deals', sa.Column('position_idx', sa.INTEGER(), autoincrement=False, nullable=True))
# ### end Alembic commands ###

View File

@@ -0,0 +1,32 @@
"""Updated Deals
Revision ID: 863d6215e1eb
Revises: f00a94ccdf01
Create Date: 2025-09-28 23:13:39.484468
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '863d6215e1eb'
down_revision: Union[str, Sequence[str], None] = 'f00a94ccdf01'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user_deals', sa.Column('margin_type', sa.String(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('user_deals', 'margin_type')
# ### end Alembic commands ###

View File

@@ -0,0 +1,34 @@
"""added position_idx for user deals table
Revision ID: 8f1476c68efa
Revises: 863d6215e1eb
Create Date: 2025-09-29 11:40:46.512160
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '8f1476c68efa'
down_revision: Union[str, Sequence[str], None] = '863d6215e1eb'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user_deals', sa.Column('position_idx', sa.Integer(), nullable=True))
op.drop_constraint(op.f('user_deals_user_id_key'), 'user_deals', type_='unique')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_unique_constraint(op.f('user_deals_user_id_key'), 'user_deals', ['user_id'], postgresql_nulls_not_distinct=False)
op.drop_column('user_deals', 'position_idx')
# ### end Alembic commands ###

View File

@@ -0,0 +1,36 @@
"""updated user_deals and user_conditional_settings
Revision ID: 968f8121104f
Revises: dbffe818030c
Create Date: 2025-10-01 11:45:49.073865
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '968f8121104f'
down_revision: Union[str, Sequence[str], None] = 'dbffe818030c'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user_conditional_settings', sa.Column('auto_trading', sa.Boolean(), nullable=True))
op.drop_column('user_deals', 'commission_fee')
op.drop_column('user_deals', 'auto_trading')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user_deals', sa.Column('auto_trading', sa.BOOLEAN(), autoincrement=False, nullable=True))
op.add_column('user_deals', sa.Column('commission_fee', sa.VARCHAR(), autoincrement=False, nullable=True))
op.drop_column('user_conditional_settings', 'auto_trading')
# ### end Alembic commands ###

View File

@@ -0,0 +1,60 @@
"""Updated UserDeals
Revision ID: acbcc95de48d
Revises: ccdc5764eb4f
Create Date: 2025-09-28 16:57:28.384116
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'acbcc95de48d'
down_revision: Union[str, Sequence[str], None] = 'ccdc5764eb4f'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user_conditional_settings', sa.Column('auto_trading', sa.String(), nullable=False))
op.add_column('user_deals', sa.Column('trading_type', sa.String(), nullable=True))
op.add_column('user_deals', sa.Column('conditional_order_type', sa.String(), nullable=True))
op.add_column('user_deals', sa.Column('take_profit_percent', sa.Integer(), nullable=True))
op.add_column('user_deals', sa.Column('stop_loss_percent', sa.Integer(), nullable=True))
op.add_column('user_deals', sa.Column('max_risk_percent', sa.Integer(), nullable=True))
op.add_column('user_deals', sa.Column('commission_fee', sa.String(), nullable=True))
op.add_column('user_deals', sa.Column('switch_side_mode', sa.String(), nullable=True))
op.drop_index(op.f('ix_user_deals_deal_series_id'), table_name='user_deals')
op.drop_column('user_deals', 'take_profit')
op.drop_column('user_deals', 'deal_series_id')
op.drop_column('user_deals', 'price')
op.drop_column('user_deals', 'exec_fee')
op.drop_column('user_deals', 'stop_loss')
op.drop_column('user_deals', 'closed_size')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user_deals', sa.Column('closed_size', sa.VARCHAR(), autoincrement=False, nullable=True))
op.add_column('user_deals', sa.Column('stop_loss', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
op.add_column('user_deals', sa.Column('exec_fee', sa.VARCHAR(), autoincrement=False, nullable=True))
op.add_column('user_deals', sa.Column('price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
op.add_column('user_deals', sa.Column('deal_series_id', sa.INTEGER(), autoincrement=False, nullable=True))
op.add_column('user_deals', sa.Column('take_profit', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
op.create_index(op.f('ix_user_deals_deal_series_id'), 'user_deals', ['deal_series_id'], unique=False)
op.drop_column('user_deals', 'switch_side_mode')
op.drop_column('user_deals', 'commission_fee')
op.drop_column('user_deals', 'max_risk_percent')
op.drop_column('user_deals', 'stop_loss_percent')
op.drop_column('user_deals', 'take_profit_percent')
op.drop_column('user_deals', 'conditional_order_type')
op.drop_column('user_deals', 'trading_type')
op.drop_column('user_conditional_settings', 'auto_trading')
# ### end Alembic commands ###

View File

@@ -0,0 +1,79 @@
"""unnecessary data has been deleted
Revision ID: c710f4e2259c
Revises: 10bf073c71f9
Create Date: 2025-10-09 14:17:32.632574
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'c710f4e2259c'
down_revision: Union[str, Sequence[str], None] = '10bf073c71f9'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user_additional_settings',
sa.Column('switch_side', sa.Boolean(), nullable=False, server_default=sa.false()))
op.drop_column('user_additional_settings', 'leverage_to_buy')
op.drop_column('user_additional_settings', 'order_type')
op.drop_column('user_additional_settings', 'limit_price')
op.drop_column('user_additional_settings', 'leverage_to_sell')
op.drop_column('user_additional_settings', 'conditional_order_type')
op.add_column('user_auto_trading', sa.Column('total_fee', sa.Float(), nullable=True))
op.drop_column('user_auto_trading', 'side')
op.drop_column('user_deals', 'switch_side_mode')
op.drop_column('user_deals', 'leverage_to_buy')
op.drop_column('user_deals', 'order_type')
op.drop_column('user_deals', 'limit_price')
op.drop_column('user_deals', 'max_risk_percent')
op.drop_column('user_deals', 'leverage_to_sell')
op.drop_column('user_deals', 'conditional_order_type')
op.alter_column('user_risk_management', 'take_profit_percent',
existing_type=sa.INTEGER(),
type_=sa.Float(),
existing_nullable=False)
op.alter_column('user_risk_management', 'stop_loss_percent',
existing_type=sa.INTEGER(),
type_=sa.Float(),
existing_nullable=False)
op.drop_column('user_risk_management', 'max_risk_percent')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user_risk_management', sa.Column('max_risk_percent', sa.INTEGER(), autoincrement=False, nullable=False))
op.alter_column('user_risk_management', 'stop_loss_percent',
existing_type=sa.Float(),
type_=sa.INTEGER(),
existing_nullable=False)
op.alter_column('user_risk_management', 'take_profit_percent',
existing_type=sa.Float(),
type_=sa.INTEGER(),
existing_nullable=False)
op.add_column('user_deals', sa.Column('conditional_order_type', sa.VARCHAR(), autoincrement=False, nullable=True))
op.add_column('user_deals', sa.Column('leverage_to_sell', sa.VARCHAR(), autoincrement=False, nullable=True))
op.add_column('user_deals', sa.Column('max_risk_percent', sa.INTEGER(), autoincrement=False, nullable=True))
op.add_column('user_deals', sa.Column('limit_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
op.add_column('user_deals', sa.Column('order_type', sa.VARCHAR(), autoincrement=False, nullable=True))
op.add_column('user_deals', sa.Column('leverage_to_buy', sa.VARCHAR(), autoincrement=False, nullable=True))
op.add_column('user_deals', sa.Column('switch_side_mode', sa.BOOLEAN(), autoincrement=False, nullable=True))
op.add_column('user_auto_trading', sa.Column('side', sa.VARCHAR(), autoincrement=False, nullable=True))
op.drop_column('user_auto_trading', 'total_fee')
op.add_column('user_additional_settings', sa.Column('conditional_order_type', sa.VARCHAR(), autoincrement=False, nullable=False))
op.add_column('user_additional_settings', sa.Column('leverage_to_sell', sa.VARCHAR(), autoincrement=False, nullable=False))
op.add_column('user_additional_settings', sa.Column('limit_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=False))
op.add_column('user_additional_settings', sa.Column('order_type', sa.VARCHAR(), server_default=sa.text("'Market'::character varying"), autoincrement=False, nullable=False))
op.add_column('user_additional_settings', sa.Column('leverage_to_buy', sa.VARCHAR(), autoincrement=False, nullable=False))
op.drop_column('user_additional_settings', 'switch_side')
# ### end Alembic commands ###

View File

@@ -0,0 +1,38 @@
"""Fixed auto_trade
Revision ID: c98b9dc36d15
Revises: acbcc95de48d
Create Date: 2025-09-28 21:33:08.319232
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'c98b9dc36d15'
down_revision: Union[str, Sequence[str], None] = 'acbcc95de48d'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('user_conditional_settings', 'auto_trading',
existing_type=sa.VARCHAR(),
type_=sa.Boolean(),
existing_nullable=False)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('user_conditional_settings', 'auto_trading',
existing_type=sa.Boolean(),
type_=sa.VARCHAR(),
existing_nullable=False)
# ### end Alembic commands ###

View File

@@ -0,0 +1,50 @@
"""Added UserDeals
Revision ID: ccdc5764eb4f
Revises: 0eed68eddcdb
Create Date: 2025-09-25 22:39:17.246594
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'ccdc5764eb4f'
down_revision: Union[str, Sequence[str], None] = '0eed68eddcdb'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user_conditional_settings', sa.Column('timer_start', sa.Integer(), nullable=False))
op.add_column('user_conditional_settings', sa.Column('timer_end', sa.Integer(), nullable=False))
op.add_column('user_deals', sa.Column('trade_mode', sa.String(), nullable=True))
op.add_column('user_deals', sa.Column('order_type', sa.String(), nullable=True))
op.add_column('user_deals', sa.Column('leverage', sa.String(), nullable=True))
op.add_column('user_deals', sa.Column('leverage_to_buy', sa.String(), nullable=True))
op.add_column('user_deals', sa.Column('leverage_to_sell', sa.String(), nullable=True))
op.add_column('user_deals', sa.Column('closed_side', sa.String(), nullable=True))
op.add_column('user_deals', sa.Column('martingale_factor', sa.Float(), nullable=True))
op.add_column('user_deals', sa.Column('max_bets_in_series', sa.Integer(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('user_deals', 'max_bets_in_series')
op.drop_column('user_deals', 'martingale_factor')
op.drop_column('user_deals', 'closed_side')
op.drop_column('user_deals', 'leverage_to_sell')
op.drop_column('user_deals', 'leverage_to_buy')
op.drop_column('user_deals', 'leverage')
op.drop_column('user_deals', 'order_type')
op.drop_column('user_deals', 'trade_mode')
op.drop_column('user_conditional_settings', 'timer_end')
op.drop_column('user_conditional_settings', 'timer_start')
# ### end Alembic commands ###

View File

@@ -0,0 +1,34 @@
"""added limit and trigger price for user deals
Revision ID: d3c85bad8c98
Revises: 09db71875980
Create Date: 2025-09-29 16:50:36.818798
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'd3c85bad8c98'
down_revision: Union[str, Sequence[str], None] = '09db71875980'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user_deals', sa.Column('limit_price', sa.Float(), nullable=True))
op.add_column('user_deals', sa.Column('trigger_price', sa.Float(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('user_deals', 'trigger_price')
op.drop_column('user_deals', 'limit_price')
# ### end Alembic commands ###

View File

@@ -0,0 +1,40 @@
"""added last_side and auto_trading for user_deals
Revision ID: dbffe818030c
Revises: 3534adf891fc
Create Date: 2025-10-01 09:29:55.554101
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'dbffe818030c'
down_revision: Union[str, Sequence[str], None] = '3534adf891fc'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('user_conditional_settings', 'last_side')
op.drop_column('user_conditional_settings', 'auto_trading')
op.add_column('user_deals', sa.Column('last_side', sa.String(), nullable=True))
op.add_column('user_deals', sa.Column('auto_trading', sa.Boolean(), nullable=True))
op.drop_column('user_deals', 'side')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user_deals', sa.Column('side', sa.VARCHAR(), autoincrement=False, nullable=True))
op.drop_column('user_deals', 'auto_trading')
op.drop_column('user_deals', 'last_side')
op.add_column('user_conditional_settings', sa.Column('auto_trading', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=False))
op.add_column('user_conditional_settings', sa.Column('last_side', sa.VARCHAR(), autoincrement=False, nullable=False))
# ### end Alembic commands ###

View File

@@ -0,0 +1,32 @@
"""added fee user deals
Revision ID: ef342b38e17b
Revises: 73a00faa4f7f
Create Date: 2025-10-02 15:10:25.456983
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'ef342b38e17b'
down_revision: Union[str, Sequence[str], None] = '73a00faa4f7f'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user_deals', sa.Column('fee', sa.Float(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('user_deals', 'fee')
# ### end Alembic commands ###

View File

@@ -0,0 +1,38 @@
"""added last side the conditional data
Revision ID: ef38c90eed55
Revises: d3c85bad8c98
Create Date: 2025-09-30 08:33:23.415545
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'ef38c90eed55'
down_revision: Union[str, Sequence[str], None] = 'd3c85bad8c98'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
op.add_column('user_conditional_settings', sa.Column('last_side', sa.String(), nullable=True))
# Обновляем все существующие строки значением по умолчанию
op.execute(
"UPDATE user_conditional_settings SET last_side = 'default_value' WHERE last_side IS NULL"
)
# Устанавливаем ограничение NOT NULL
op.alter_column('user_conditional_settings', 'last_side', nullable=False)
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('user_conditional_settings', 'last_side')
# ### end Alembic commands ###

View File

@@ -0,0 +1,40 @@
"""Updated Deals
Revision ID: f00a94ccdf01
Revises: c98b9dc36d15
Create Date: 2025-09-28 22:25:00.092196
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'f00a94ccdf01'
down_revision: Union[str, Sequence[str], None] = 'c98b9dc36d15'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('user_deals', 'switch_side_mode',
existing_type=sa.VARCHAR(),
type_=sa.Boolean(),
existing_nullable=True)
op.create_unique_constraint(None, 'user_deals', ['user_id'])
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'user_deals', type_='unique')
op.alter_column('user_deals', 'switch_side_mode',
existing_type=sa.Boolean(),
type_=sa.VARCHAR(),
existing_nullable=True)
# ### end Alembic commands ###

View File

@@ -1,8 +1,8 @@
"""initial """Updated leverage
Revision ID: f6e7eb3f25c0 Revision ID: fd8581c0cc87
Revises: Revises: bb586fa9bcd2
Create Date: 2025-11-12 22:53:02.189445 Create Date: 2025-09-22 15:13:21.487402
""" """
from typing import Sequence, Union from typing import Sequence, Union
@@ -12,7 +12,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = 'f6e7eb3f25c0' revision: str = 'fd8581c0cc87'
down_revision: Union[str, Sequence[str], None] = None down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None

View File

@@ -15,7 +15,7 @@ async def get_bybit_client(tg_id: int) -> HTTP | None:
""" """
try: try:
api_key, api_secret = await rq.get_user_api(tg_id=tg_id) api_key, api_secret = await rq.get_user_api(tg_id=tg_id)
return HTTP(demo=True, api_key=api_key, api_secret=api_secret) return HTTP(api_key=api_key, api_secret=api_secret)
except Exception as e: except Exception as e:
logger.error("Error getting bybit client for user %s: %s", tg_id, e) logger.error("Error getting bybit client for user %s: %s", tg_id, e)
return None return None

View File

@@ -7,25 +7,25 @@ logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("close_positions") logger = logging.getLogger("close_positions")
async def close_position_by_symbol( async def close_position(
tg_id: int, symbol: str tg_id: int, symbol: str, side: str, position_idx: int, qty: float
) -> bool: ) -> bool:
""" """
Closes all positions Closes all positions
:param tg_id: Telegram user ID :param tg_id: Telegram user ID
:param symbol: symbol :param symbol: symbol
:param side: side
:param position_idx: position index
:param qty: quantity
:return: bool :return: bool
""" """
try: try:
client = await get_bybit_client(tg_id) client = await get_bybit_client(tg_id)
response = client.get_positions( if side == "Buy":
category="linear", symbol=symbol r_side = "Sell"
) else:
positions = response.get("result", {}).get("list", []) r_side = "Buy"
r_side = "Sell" if positions[0].get("side") == "Buy" else "Buy"
qty = positions[0].get("size")
position_idx = positions[0].get("positionIdx")
response = client.place_order( response = client.place_order(
category="linear", category="linear",
@@ -37,16 +37,16 @@ async def close_position_by_symbol(
positionIdx=position_idx, positionIdx=position_idx,
) )
if response["retCode"] == 0: if response["retCode"] == 0:
logger.info("Positions closed for %s for user %s", symbol, tg_id) logger.info("All positions closed for %s for user %s", symbol, tg_id)
return True return True
else: else:
logger.error( logger.error(
"Error closing position for %s for user %s", symbol, tg_id "Error closing all positions for %s for user %s", symbol, tg_id
) )
return False return False
except Exception as e: except Exception as e:
logger.error( logger.error(
"Error closing positions for %s for user %s: %s", symbol, tg_id, e "Error closing all positions for %s for user %s: %s", symbol, tg_id, e
) )
return False return False

View File

@@ -38,7 +38,7 @@ async def get_active_positions(tg_id: int) -> list | None:
return None return None
async def get_active_positions_by_symbol(tg_id: int, symbol: str): async def get_active_positions_by_symbol(tg_id: int, symbol: str) -> dict | None:
""" """
Get active positions for a user by symbol Get active positions for a user by symbol
""" """
@@ -62,12 +62,8 @@ async def get_active_positions_by_symbol(tg_id: int, symbol: str):
) )
return None return None
except Exception as e: except Exception as e:
errors = str(e) logger.error("Error getting active positions for user %s: %s", tg_id, e)
if errors.startswith("Permission denied, please check your API key permissions"): return None
return "Invalid API key permissions"
else:
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: async def get_active_orders(tg_id: int) -> list | None:

View File

@@ -10,27 +10,26 @@ from app.bybit.get_functions.get_tickers import get_tickers
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
from app.bybit.set_functions.set_leverage import set_leverage from app.bybit.set_functions.set_leverage import set_leverage
from app.bybit.set_functions.set_margin_mode import set_margin_mode 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 get_liquidation_price, safe_float
from app.helper_functions import safe_float
logging.config.dictConfig(LOGGING_CONFIG) logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("open_positions") logger = logging.getLogger("open_positions")
async def start_trading_cycle( async def start_trading_cycle(tg_id: int) -> str | None:
tg_id: int
) -> str | None:
""" """
Start trading cycle Start trading cycle
:param tg_id: Telegram user ID :param tg_id: Telegram user ID
""" """
try: try:
client = await get_bybit_client(tg_id=tg_id)
symbol = await rq.get_user_symbol(tg_id=tg_id) symbol = await rq.get_user_symbol(tg_id=tg_id)
additional_data = await rq.get_user_additional_settings(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) risk_management_data = await rq.get_user_risk_management(tg_id=tg_id)
commission_fee = risk_management_data.commission_fee
user_deals_data = await rq.get_user_deal_by_symbol(tg_id=tg_id, symbol=symbol)
trade_mode = additional_data.trade_mode trade_mode = additional_data.trade_mode
switch_side = additional_data.switch_side switch_side = additional_data.switch_side
side = additional_data.side
margin_type = additional_data.margin_type margin_type = additional_data.margin_type
leverage = additional_data.leverage leverage = additional_data.leverage
order_quantity = additional_data.order_quantity order_quantity = additional_data.order_quantity
@@ -39,103 +38,53 @@ async def start_trading_cycle(
max_bets_in_series = additional_data.max_bets_in_series max_bets_in_series = additional_data.max_bets_in_series
take_profit_percent = risk_management_data.take_profit_percent take_profit_percent = risk_management_data.take_profit_percent
stop_loss_percent = risk_management_data.stop_loss_percent stop_loss_percent = risk_management_data.stop_loss_percent
commission_fee = risk_management_data.commission_fee
commission_place = risk_management_data.commission_place get_side = "Buy"
if user_deals_data:
get_side = user_deals_data.last_side or "Buy"
if trade_mode == "Switch": if trade_mode == "Switch":
side = side if switch_side == "По направлению":
side = get_side
else:
if get_side == "Buy":
side = "Sell"
else:
side = "Buy"
else: else:
if trade_mode == "Long": if trade_mode == "Long":
side = "Buy" side = "Buy"
else: else:
side = "Sell" side = "Sell"
await set_switch_position_mode( # Get fee rates
tg_id=tg_id, fee_info = client.get_fee_rates(category="linear", symbol=symbol)
symbol=symbol,
mode=0)
await rq.set_user_deal( # Check if commission fee is enabled
tg_id=tg_id, commission_fee_percent = 0.0
symbol=symbol, if commission_fee == "Yes_commission_fee":
current_step=1, commission_fee_percent = safe_float(
current_series=1, fee_info["result"]["list"][0]["takerFeeRate"]
trade_mode=trade_mode, )
side_mode=switch_side,
margin_type=margin_type,
leverage=leverage,
order_quantity=order_quantity,
trigger_price=trigger_price,
martingale_factor=martingale_factor,
max_bets_in_series=max_bets_in_series,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
base_quantity=order_quantity,
commission_fee=commission_fee,
commission_place=commission_place,
pnl_series=0
)
await rq.set_total_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, total_fee=0
)
await rq.set_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, fee=0
)
res = await open_positions( get_ticker = await get_tickers(tg_id, symbol=symbol)
tg_id=tg_id, price_symbol = safe_float(get_ticker.get("lastPrice")) or 0
symbol=symbol, instruments_info = await get_instruments_info(tg_id=tg_id, symbol=symbol)
side=side, qty_step_str = instruments_info.get("lotSizeFilter").get("qtyStep")
order_quantity=order_quantity, qty_step = safe_float(qty_step_str)
trigger_price=trigger_price, qty = safe_float(order_quantity) / safe_float(price_symbol)
leverage=leverage decimals = abs(int(round(math.log10(qty_step))))
) qty_formatted = math.floor(qty / qty_step) * qty_step
qty_formatted = round(qty_formatted, decimals)
if res == "OK": if trigger_price > 0:
return "OK" po_trigger_price = str(trigger_price)
return ( else:
res po_trigger_price = None
if res
in {
"Limit price is out min price",
"Limit price is out max price",
"Risk is too high for this trade",
"estimated will trigger liq",
"ab not enough for new order",
"InvalidRequestError",
"Order does not meet minimum order value",
"position idx not match position mode",
"Qty invalid",
"The number of contracts exceeds maximum limit allowed",
"The number of contracts exceeds minimum limit allowed",
"Order placement failed as your position may exceed the max",
"Permission denied, please check your API key permissions"
}
else None
)
except Exception as e: price_for_cals = trigger_price if po_trigger_price is not None else price_symbol
logger.error("Error in start_trading: %s", e) total_commission = price_for_cals * qty_formatted * commission_fee_percent
return None
async def trading_cycle_profit(
tg_id: int, symbol: str, side: str) -> str | None:
try:
user_deals_data = await rq.get_user_deal_by_symbol(tg_id=tg_id, symbol=symbol)
trade_mode = user_deals_data.trade_mode
margin_type = user_deals_data.margin_type
leverage = user_deals_data.leverage
trigger_price = 0
take_profit_percent = user_deals_data.take_profit_percent
stop_loss_percent = user_deals_data.stop_loss_percent
max_bets_in_series = user_deals_data.max_bets_in_series
martingale_factor = user_deals_data.martingale_factor
side_mode = user_deals_data.side_mode
base_quantity = user_deals_data.base_quantity
current_series = user_deals_data.current_series
commission_fee = user_deals_data.commission_fee
commission_place = user_deals_data.commission_place
await set_margin_mode(tg_id=tg_id, margin_mode=margin_type) await set_margin_mode(tg_id=tg_id, margin_mode=margin_type)
await set_leverage( await set_leverage(
@@ -144,74 +93,67 @@ async def trading_cycle_profit(
leverage=leverage, leverage=leverage,
) )
if trade_mode == "Switch":
if side_mode == "Противоположно":
s_side = "Sell" if side == "Buy" else "Buy"
else:
s_side = side
else:
s_side = side
next_series = current_series + 1
await rq.set_user_deal(
tg_id=tg_id,
symbol=symbol,
current_step=1,
current_series=next_series,
trade_mode=trade_mode,
side_mode=side_mode,
margin_type=margin_type,
leverage=leverage,
order_quantity=base_quantity,
trigger_price=trigger_price,
martingale_factor=martingale_factor,
max_bets_in_series=max_bets_in_series,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
base_quantity=base_quantity,
commission_fee=commission_fee,
commission_place=commission_place,
pnl_series=0
)
res = await open_positions( res = await open_positions(
tg_id=tg_id, tg_id=tg_id,
symbol=symbol, symbol=symbol,
side=s_side, side=side,
order_quantity=base_quantity, order_quantity=order_quantity,
trigger_price=trigger_price, trigger_price=trigger_price,
leverage=leverage margin_type=margin_type,
leverage=leverage,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
commission_fee_percent=total_commission,
) )
if res == "OK": 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,
order_quantity=order_quantity,
trigger_price=trigger_price,
martingale_factor=martingale_factor,
max_bets_in_series=max_bets_in_series,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
base_quantity=order_quantity,
)
return "OK" return "OK"
return ( return (
res res
if res if res
in { in {
"Risk is too high for this trade", "Limit price is out min price",
"ab not enough for new order", "Limit price is out max price",
"InvalidRequestError", "Risk is too high for this trade",
"The number of contracts exceeds maximum limit allowed", "estimated will trigger liq",
"Order placement failed as your position may exceed the max", "ab not enough for new order",
} "InvalidRequestError",
"Order does not meet minimum order value",
"position idx not match position mode",
"Qty invalid",
"The number of contracts exceeds maximum limit allowed",
"The number of contracts exceeds minimum limit allowed",
}
else None else None
) )
except Exception as e: except Exception as e:
logger.error("Error in trading_cycle_profit: %s", e) logger.error("Error in start_trading: %s", e)
return None return None
async def trading_cycle( async def trading_cycle(tg_id: int, symbol: str, reverse_side: str) -> str | None:
tg_id: int, symbol: str, side: str,
) -> str | None:
try: try:
user_deals_data = await rq.get_user_deal_by_symbol(tg_id=tg_id, symbol=symbol) user_deals_data = await rq.get_user_deal_by_symbol(tg_id=tg_id, symbol=symbol)
user_auto_trading_data = await rq.get_user_auto_trading(tg_id=tg_id, symbol=symbol) user_auto_trading_data = await rq.get_user_auto_trading(
commission_fee = user_deals_data.commission_fee tg_id=tg_id, symbol=symbol
commission_place = user_deals_data.commission_place )
total_fee = user_auto_trading_data.total_fee total_fee = user_auto_trading_data.total_fee
trade_mode = user_deals_data.trade_mode trade_mode = user_deals_data.trade_mode
margin_type = user_deals_data.margin_type margin_type = user_deals_data.margin_type
@@ -224,14 +166,6 @@ async def trading_cycle(
current_step = user_deals_data.current_step current_step = user_deals_data.current_step
order_quantity = user_deals_data.order_quantity order_quantity = user_deals_data.order_quantity
base_quantity = user_deals_data.base_quantity base_quantity = user_deals_data.base_quantity
side_mode = user_deals_data.side_mode
current_series = user_deals_data.current_series
pnl_series = user_deals_data.pnl_series
next_quantity = safe_float(order_quantity) * (
safe_float(martingale_factor)
)
current_step += 1
await set_margin_mode(tg_id=tg_id, margin_mode=margin_type) await set_margin_mode(tg_id=tg_id, margin_mode=margin_type)
await set_leverage( await set_leverage(
@@ -239,62 +173,64 @@ async def trading_cycle(
symbol=symbol, symbol=symbol,
leverage=leverage, leverage=leverage,
) )
total_quantity = next_quantity
if commission_fee == "Yes_commission_fee": if reverse_side == "Buy":
if commission_place == "Commission_for_qty": real_side = "Sell"
total_quantity = next_quantity + total_fee else:
real_side = "Buy"
side = real_side
if trade_mode == "Switch": if trade_mode == "Switch":
if side == "Buy": side = "Sell" if real_side == "Buy" else "Buy"
r_side = "Sell"
else:
r_side = "Buy"
else:
r_side = side
await rq.set_user_deal( next_quantity = safe_float(order_quantity) * (safe_float(martingale_factor))
tg_id=tg_id, current_step += 1
symbol=symbol,
current_step=current_step, if max_bets_in_series < current_step:
current_series=current_series, return "Max bets in series"
trade_mode=trade_mode,
side_mode=side_mode,
margin_type=margin_type,
leverage=leverage,
order_quantity=next_quantity,
trigger_price=trigger_price,
martingale_factor=martingale_factor,
max_bets_in_series=max_bets_in_series,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
base_quantity=base_quantity,
commission_fee=commission_fee,
commission_place=commission_place,
pnl_series=pnl_series
)
res = await open_positions( res = await open_positions(
tg_id=tg_id, tg_id=tg_id,
symbol=symbol, symbol=symbol,
side=r_side, side=side,
order_quantity=total_quantity, order_quantity=next_quantity,
trigger_price=trigger_price, trigger_price=trigger_price,
leverage=leverage margin_type=margin_type,
leverage=leverage,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
commission_fee_percent=total_fee,
) )
if res == "OK": 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,
order_quantity=next_quantity,
trigger_price=trigger_price,
martingale_factor=martingale_factor,
max_bets_in_series=max_bets_in_series,
take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent,
base_quantity=base_quantity,
)
return "OK" return "OK"
return ( return (
res res
if res if res
in { in {
"Risk is too high for this trade", "Risk is too high for this trade",
"ab not enough for new order", "ab not enough for new order",
"InvalidRequestError", "InvalidRequestError",
"The number of contracts exceeds maximum limit allowed", "The number of contracts exceeds maximum limit allowed",
"Order placement failed as your position may exceed the max", }
}
else None else None
) )
@@ -304,29 +240,28 @@ async def trading_cycle(
async def open_positions( async def open_positions(
tg_id: int, tg_id: int,
side: str, side: str,
symbol: str, symbol: str,
order_quantity: float, order_quantity: float,
trigger_price: float, trigger_price: float,
leverage: str margin_type: str,
leverage: str,
take_profit_percent: float,
stop_loss_percent: float,
commission_fee_percent: float,
) -> str | None: ) -> str | None:
try: try:
client = await get_bybit_client(tg_id=tg_id) client = await get_bybit_client(tg_id=tg_id)
get_ticker = await get_tickers(tg_id, symbol=symbol) get_ticker = await get_tickers(tg_id, symbol=symbol)
price_symbol = safe_float(get_ticker.get("lastPrice")) or 0
if get_ticker is None:
price_symbol = 0
else:
price_symbol = safe_float(get_ticker.get("lastPrice"))
instruments_info = await get_instruments_info(tg_id=tg_id, symbol=symbol) instruments_info = await get_instruments_info(tg_id=tg_id, symbol=symbol)
qty_step_str = instruments_info.get("lotSizeFilter").get("qtyStep") qty_step_str = instruments_info.get("lotSizeFilter").get("qtyStep")
qty_step = safe_float(qty_step_str) qty_step = safe_float(qty_step_str)
qty = (safe_float(order_quantity) * safe_float(leverage)) / safe_float(price_symbol) qty = safe_float(order_quantity) / safe_float(price_symbol)
decimals = abs(int(round(math.log10(qty_step)))) decimals = abs(int(round(math.log10(qty_step))))
qty_format = math.floor(qty / qty_step) * qty_step qty_formatted = math.floor(qty / qty_step) * qty_step
qty_formatted = round(qty_format, decimals) qty_formatted = round(qty_formatted, decimals)
if trigger_price > 0: if trigger_price > 0:
po_trigger_price = str(trigger_price) po_trigger_price = str(trigger_price)
@@ -335,6 +270,51 @@ async def open_positions(
po_trigger_price = None po_trigger_price = None
trigger_direction = None trigger_direction = None
get_leverage = safe_float(leverage)
price_for_cals = trigger_price if po_trigger_price is not None else price_symbol
tp_multiplier = 1 + (take_profit_percent / 100)
if commission_fee_percent > 0:
tp_multiplier += commission_fee_percent
if margin_type == "ISOLATED_MARGIN":
liq_long, liq_short = await get_liquidation_price(
tg_id=tg_id,
entry_price=price_for_cals,
symbol=symbol,
leverage=get_leverage,
)
if (liq_long > 0 or liq_short > 0) and price_for_cals > 0:
if side == "Buy":
base_tp = price_for_cals + (price_for_cals - liq_long)
take_profit_price = base_tp + commission_fee_percent
else:
base_tp = price_for_cals - (liq_short - price_for_cals)
take_profit_price = base_tp - commission_fee_percent
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_cals * tp_multiplier
stop_loss_price = price_for_cals * (1 - stop_loss_percent / 100)
else:
take_profit_price = price_for_cals * (
1 - (take_profit_percent / 100) - commission_fee_percent
)
stop_loss_price = trigger_price * (1 + stop_loss_percent / 100)
take_profit_price = max(take_profit_price, 0)
stop_loss_price = max(stop_loss_price, 0)
logger.info("Take profit price: %s", take_profit_price)
logger.info("Stop loss price: %s", stop_loss_price)
logger.info("Commission fee percent: %s", commission_fee_percent)
# Place order # Place order
order_params = { order_params = {
"category": "linear", "category": "linear",
@@ -347,6 +327,9 @@ async def open_positions(
"triggerBy": "LastPrice", "triggerBy": "LastPrice",
"timeInForce": "GTC", "timeInForce": "GTC",
"positionIdx": 0, "positionIdx": 0,
"tpslMode": "Full",
"takeProfit": str(take_profit_price) if take_profit_price else None,
"stopLoss": str(stop_loss_price) if stop_loss_price else None,
} }
response = client.place_order(**order_params) response = client.place_order(**order_params)
@@ -368,8 +351,6 @@ async def open_positions(
"Qty invalid": "Qty invalid", "Qty invalid": "Qty invalid",
"The number of contracts exceeds maximum limit allowed": "The number of contracts exceeds maximum limit allowed", "The number of contracts exceeds maximum limit allowed": "The number of contracts exceeds maximum limit allowed",
"The number of contracts exceeds minimum limit allowed": "The number of contracts exceeds minimum limit allowed", "The number of contracts exceeds minimum limit allowed": "The number of contracts exceeds minimum limit allowed",
"Order placement failed as your position may exceed the max": "Order placement failed as your position may exceed the max",
"Permission denied, please check your API key permissions": "Permission denied, please check your API key permissions"
} }
for key, msg in known_errors.items(): for key, msg in known_errors.items():
if key in error_text: if key in error_text:
@@ -379,5 +360,5 @@ async def open_positions(
return "InvalidRequestError" return "InvalidRequestError"
except Exception as e: except Exception as e:
logger.error("Error opening position for user %s: %s", tg_id, e, exc_info=True) logger.error("Error opening position for user %s: %s", tg_id, e)
return None return None

View File

@@ -21,23 +21,19 @@ async def user_profile_bybit(tg_id: int, message: Message, state: FSMContext) ->
if wallet: if wallet:
balance = wallet.get("totalWalletBalance", "0") balance = wallet.get("totalWalletBalance", "0")
symbol = await rq.get_user_symbol(tg_id=tg_id) symbol = await rq.get_user_symbol(tg_id=tg_id)
if symbol is None: await message.answer(
await rq.set_user_symbol(tg_id=tg_id, symbol="BTCUSDT") text=f"💎Ваш профиль:\n\n"
await user_profile_bybit(tg_id=tg_id, message=message, state=state) f"⚖️ Баланс: {float(balance):,.2f} USD\n"
else: f"📊Торговая пара: {symbol}\n\n"
await message.answer( f"Краткая инструкция:\n"
text=f"💎Ваш профиль:\n\n" f"1. Укажите торговую пару (например: BTCUSDT).\n"
f"⚖️ Баланс: {float(balance):,.2f} USD\n" f"2. В настройках выставьте все необходимые параметры.\n"
f"📊Торговая пара: {symbol}\n\n" f"3. Нажмите кнопку 'Начать торговлю'.\n",
f"Краткая инструкция:\n" reply_markup=kbi.main_menu,
f"1. Укажите торговую пару (например: BTCUSDT).\n" )
f"2. В настройках выставьте все необходимые параметры.\n"
f"3. Нажмите кнопку 'Начать торговлю'.\n",
reply_markup=kbi.main_menu,
)
else: else:
await message.answer( await message.answer(
text="Ошибка при подключении к платформе. Проверьте корректность и разрешения API ключа и добавьте повторно.", text="Ошибка при подключении, повторите попытку",
reply_markup=kbi.connect_the_platform, reply_markup=kbi.connect_the_platform,
) )
logger.error("Error processing user profile for user %s", tg_id) logger.error("Error processing user profile for user %s", tg_id)

View File

@@ -13,7 +13,7 @@ async def set_tp_sl_for_position(
take_profit_price: float, take_profit_price: float,
stop_loss_price: float, stop_loss_price: float,
position_idx: int, position_idx: int,
) -> bool | str: ) -> bool:
""" """
Set take profit and stop loss for a symbol. Set take profit and stop loss for a symbol.
:param tg_id: Telegram user ID :param tg_id: Telegram user ID
@@ -21,17 +21,15 @@ async def set_tp_sl_for_position(
:param take_profit_price: Take profit price :param take_profit_price: Take profit price
:param stop_loss_price: Stop loss price :param stop_loss_price: Stop loss price
:param position_idx: Position index :param position_idx: Position index
:return: bool or str :return: bool
""" """
try: try:
client = await get_bybit_client(tg_id) client = await get_bybit_client(tg_id)
take_profit = round(take_profit_price, 6) if take_profit_price is not None else None
stop_loss = round(stop_loss_price, 6) if stop_loss_price is not None else None
resp = client.set_trading_stop( resp = client.set_trading_stop(
category="linear", category="linear",
symbol=symbol, symbol=symbol,
takeProfit=str(take_profit) if take_profit is not None else None, takeProfit=str(round(take_profit_price, 5)),
stopLoss=str(stop_loss) if stop_loss is not None else None, stopLoss=str(round(stop_loss_price, 5)),
positionIdx=position_idx, positionIdx=position_idx,
tpslMode="Full", tpslMode="Full",
) )
@@ -40,18 +38,8 @@ async def set_tp_sl_for_position(
logger.info("TP/SL for %s has been set", symbol) logger.info("TP/SL for %s has been set", symbol)
return True return True
else: else:
error_msg = resp.get("retMsg") logger.error("Error setting TP/SL for %s: %s", symbol, resp.get("retMsg"))
if "not modified" in error_msg.lower():
logger.info("TP/SL for %s not modified: %s", symbol, error_msg)
return "not modified"
else:
logger.error("Error setting TP/SL for %s: %s", symbol, error_msg)
return False
except Exception as e:
error_msg = str(e)
if "not modified" in error_msg.lower():
logger.info("TP/SL for %s not modified: %s", symbol, error_msg)
return "not modified"
else:
logger.error("Error set TP/SL for %s: %s", symbol, e)
return False return False
except Exception as e:
logger.error("Error setting TP/SL for %s: %s", symbol, e)
return False

View File

@@ -1,13 +1,10 @@
import logging.config import logging.config
import math
import json
import app.telegram.keyboards.inline as kbi import app.telegram.keyboards.inline as kbi
import database.request as rq import database.request as rq
from app.bybit.get_functions.get_instruments_info import get_instruments_info
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
from app.bybit.open_positions import trading_cycle, trading_cycle_profit from app.bybit.open_positions import trading_cycle
from app.bybit.set_functions.set_tp_sl import set_tp_sl_for_position from app.helper_functions import format_value, safe_float
from app.helper_functions import format_value, safe_float, truncate_float
logging.config.dictConfig(LOGGING_CONFIG) logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("telegram_message_handler") logger = logging.getLogger("telegram_message_handler")
@@ -15,351 +12,172 @@ logger = logging.getLogger("telegram_message_handler")
class TelegramMessageHandler: class TelegramMessageHandler:
def __init__(self, telegram_bot): def __init__(self, telegram_bot):
"""Initialize the TelegramMessageHandler class."""
self.telegram_bot = telegram_bot self.telegram_bot = telegram_bot
async def format_position_update(self, message):
pass
async def format_order_update(self, message, tg_id): async def format_order_update(self, message, tg_id):
"""Handle order updates."""
try: try:
# logger.info("Order update: %s", json.dumps(message)) order_data = message.get("data", [{}])[0]
user_additional_data = await rq.get_user_additional_settings(tg_id=tg_id) symbol = format_value(order_data.get("symbol"))
trigger_price = safe_float(user_additional_data.trigger_price) qty = format_value(order_data.get("qty"))
if trigger_price > 0: side = format_value(order_data.get("side"))
order_data = message.get("data", [{}])[0] side_rus = (
symbol = format_value(order_data.get("symbol")) "Покупка"
side = format_value(order_data.get("side")) if side == "Buy"
side_rus = ( else "Продажа" if side == "Sell" else "Нет данных"
"Покупка" )
if side == "Buy" order_status = format_value(order_data.get("orderStatus"))
else "Продажа" if side == "Sell" else "Нет данных" price = format_value(order_data.get("price"))
) trigger_price = format_value(order_data.get("triggerPrice"))
order_status = format_value(order_data.get("orderStatus")) take_profit = format_value(order_data.get("takeProfit"))
tr_price = format_value(order_data.get("triggerPrice")) stop_loss = format_value(order_data.get("stopLoss"))
status_map = { status_map = {
"Untriggered": "Условный ордер выставлен", "Untriggered": "Условный ордер выставлен",
} }
if order_status == "Filled" or order_status not in status_map: if order_status == "Filled" or order_status not in status_map:
return None return None
text = ( text = (
f"Торговая пара: {symbol}\n" f"Торговая пара: {symbol}\n"
f"Движение: {side_rus}\n" f"Количество: {qty}\n"
) f"Движение: {side_rus}\n"
if tr_price and tr_price != "Нет данных": )
text += f"Триггер цена: {tr_price}\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( await self.telegram_bot.send_message(
chat_id=tg_id, text=text, reply_markup=kbi.profile_bybit chat_id=tg_id, text=text, reply_markup=kbi.profile_bybit
) )
await rq.set_trigger_price(tg_id=tg_id, trigger_price=0)
except Exception as e: except Exception as e:
logger.error("Error in format_order_update: %s", e) logger.error("Error in format_order_update: %s", e)
async def format_execution_update(self, message, tg_id): async def format_execution_update(self, message, tg_id):
"""Handle execution updates without duplicate processing."""
try: try:
# logger.info("Execution update: %s", json.dumps(message))
execution = message.get("data", [{}])[0] execution = message.get("data", [{}])[0]
exec_type = format_value(execution.get("execType")) closed_size = format_value(execution.get("closedSize"))
if exec_type == "Trade" or exec_type == "BustTrade": symbol = format_value(execution.get("symbol"))
closed_size = format_value(execution.get("closedSize")) exec_price = format_value(execution.get("execPrice"))
symbol = format_value(execution.get("symbol")) exec_fee = format_value(execution.get("execFee"))
exec_price = format_value(execution.get("execPrice")) side = format_value(execution.get("side"))
exec_qty = format_value(execution.get("execQty")) side_rus = (
exec_fees = format_value(execution.get("execFee")) "Покупка"
fee_rate = format_value(execution.get("feeRate")) if side == "Buy"
side = format_value(execution.get("side")) else "Продажа" if side == "Sell" else "Нет данных"
exec_pnl = format_value(execution.get("execPnl")) )
stop_order_type = format_value(execution.get("stopOrderType"))
create_type = format_value(execution.get("createType"))
user_auto_trading = await rq.get_user_auto_trading( if safe_float(closed_size) == 0:
tg_id=tg_id, symbol=symbol await rq.set_fee_user_auto_trading(
) tg_id=tg_id, symbol=symbol, fee=safe_float(exec_fee)
auto_trading = (
user_auto_trading.auto_trading if user_auto_trading else False
) )
if auto_trading: user_auto_trading = await rq.get_user_auto_trading(
side_rus = ( tg_id=tg_id, symbol=symbol
"Покупка" )
if side == "Buy"
else "Продажа" if side == "Sell" else "Нет данных"
)
if safe_float(exec_fees) == 0:
exec_fee = safe_float(exec_price) * safe_float(exec_qty) * safe_float(
fee_rate
)
else:
exec_fee = safe_float(exec_fees)
if safe_float(closed_size) == 0: get_total_fee = user_auto_trading.total_fee
await rq.set_fee_user_auto_trading( total_fee = safe_float(exec_fee) + safe_float(get_total_fee)
tg_id=tg_id, symbol=symbol, fee=safe_float(exec_fee) await rq.set_total_fee_user_auto_trading(
) tg_id=tg_id, symbol=symbol, total_fee=total_fee
)
get_total_fee = 0 if user_auto_trading is not None and user_auto_trading.fee is not None:
fee = user_auto_trading.fee
else:
fee = 0
if user_auto_trading is not None: exec_pnl = format_value(execution.get("execPnl"))
get_total_fee = user_auto_trading.total_fee risk_management_data = await rq.get_user_risk_management(tg_id=tg_id)
commission_fee = risk_management_data.commission_fee
total_fee = safe_float(exec_fee) + safe_float(get_total_fee) if commission_fee == "Yes_commission_fee":
ex_pnl = safe_float(exec_pnl) total_pnl = safe_float(exec_pnl) - safe_float(exec_fee) - fee
else:
total_pnl = safe_float(exec_pnl)
header = ( header = (
"Сделка закрыта:" if safe_float(closed_size) > 0 else "Сделка открыта:" "Сделка закрыта:" if safe_float(closed_size) > 0 else "Сделка открыта:"
) )
text = f"{header}\n" f"Торговая пара: {symbol}\n" text = f"{header}\n" f"Торговая пара: {symbol}\n"
user_deals_data = await rq.get_user_deal_by_symbol(
tg_id=tg_id, symbol=symbol
)
commission_fee = user_deals_data.commission_fee user_deals_data = await rq.get_user_deal_by_symbol(
commission_place = user_deals_data.commission_place tg_id=tg_id, symbol=symbol
current_series = user_deals_data.current_series )
current_step = user_deals_data.current_step exec_bet = user_deals_data.order_quantity
order_quantity = user_deals_data.order_quantity base_quantity = user_deals_data.base_quantity
pnl_series = user_deals_data.pnl_series
take_profit_percent = user_deals_data.take_profit_percent
stop_loss_percent = user_deals_data.stop_loss_percent
leverage = safe_float(user_deals_data.leverage)
fee = safe_float(user_auto_trading.fee)
total_pnl = safe_float(exec_pnl) - safe_float(exec_fee) - fee
if commission_fee == "Yes_commission_fee": text += (
if commission_place == "Commission_for_qty": f"Цена исполнения: {exec_price}\n"
total_quantity = safe_float(order_quantity) + safe_float( f"Текущая ставка: {exec_bet}\n"
total_fee f"Движение: {side_rus}\n"
) * 2 f"Комиссия за сделку: {exec_fee}\n"
else: )
total_quantity = safe_float(order_quantity)
else:
total_quantity = safe_float(order_quantity)
if user_deals_data is not None and safe_float(closed_size) == 0: if safe_float(closed_size) > 0:
await rq.set_total_fee_user_auto_trading( text += f"\nРеализованная прибыль: {total_pnl:.7f}\n"
tg_id=tg_id, symbol=symbol, total_fee=total_fee
)
text += f"Текущая ставка: {total_quantity:.2f} USDT\n"
text += f"Серия №: {current_series}\n"
text += f"Сделка №: {current_step}\n"
text += ( await self.telegram_bot.send_message(
f"Цена исполнения: {exec_price}\n" chat_id=tg_id, text=text, reply_markup=kbi.profile_bybit
f"Комиссия: {exec_fee:.8f}\n" )
)
if safe_float(closed_size) == 0: auto_trading = (
instruments_info = await get_instruments_info(tg_id=tg_id, symbol=symbol) user_auto_trading.auto_trading if user_auto_trading else False
qty_step_str = instruments_info.get("lotSizeFilter").get("qtyStep") )
qty_step = safe_float(qty_step_str) user_symbols = user_auto_trading.symbol if user_auto_trading else None
qty = (safe_float(order_quantity) * safe_float(leverage)) / safe_float(exec_price)
decimals = abs(int(round(math.log10(qty_step))))
qty_format = math.floor(qty / qty_step) * qty_step
qty_formatted = round(qty_format, decimals)
total_commission = 0
if commission_fee == "Yes_commission_fee":
if commission_place == "Commission_for_tp":
total_commission = safe_float(total_fee) / qty_formatted
if side == "Buy":
take_profit_price = safe_float(exec_price) * (
1 + take_profit_percent / 100) + total_commission
stop_loss_price = safe_float(exec_price) * (1 - stop_loss_percent / 100)
else:
take_profit_price = safe_float(exec_price) * (
1 - take_profit_percent / 100) - total_commission
stop_loss_price = safe_float(exec_price) * (1 + stop_loss_percent / 100)
ress = await set_tp_sl_for_position(tg_id=tg_id,
symbol=symbol,
take_profit_price=take_profit_price,
stop_loss_price=stop_loss_price,
position_idx=0)
if ress or ress == "not modified":
take_profit_truncated = await truncate_float(take_profit_price, 6)
stop_loss_truncated = await truncate_float(stop_loss_price, 6)
text += (f"Движение: {side_rus}\n"
f"Тейк-профит: {take_profit_truncated}\n"
f"Стоп-лосс: {stop_loss_truncated}\n"
)
else:
text += (f"Движение: {side_rus}\n"
"Не удалось установить ТП и СЛ\n")
elif safe_float(closed_size) > 0 and auto_trading:
new_pnl = safe_float(pnl_series) + total_pnl
await rq.set_pnl_series_by_symbol(
tg_id=tg_id, symbol=symbol, pnl_series=new_pnl)
text += f"\nПрибыль без комиссии: {ex_pnl:.4f}\n"
text += f"Реализованная прибыль: {total_pnl:.4f}\n"
text += f"Прибыль серии: {safe_float(new_pnl):.4f}\n"
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( await self.telegram_bot.send_message(
chat_id=tg_id, text=text, reply_markup=kbi.profile_bybit chat_id=tg_id, text=profit_text, reply_markup=kbi.profile_bybit
)
await rq.set_auto_trading(
tg_id=tg_id, symbol=symbol, auto_trading=False
) )
if stop_order_type == "TakeProfit": await rq.set_total_fee_user_auto_trading(
profit_text = "📈 Начинаю новую серию с базовой ставки\n" tg_id=tg_id, symbol=symbol, total_fee=0
await self.telegram_bot.send_message( )
chat_id=tg_id, text=profit_text, reply_markup=kbi.profile_bybit await rq.set_fee_user_auto_trading(
) tg_id=tg_id, symbol=symbol, fee=0
)
await rq.set_order_quantity(
tg_id=message.from_user.id, order_quantity=base_quantity
)
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
)
if side == "Buy": if res == "OK":
r_side = "Sell" pass
else: else:
r_side = "Buy" errors = {
"Max bets in series": "❗️ Максимальное количество сделок в серии достигнуто",
await rq.set_last_side_by_symbol( "Risk is too high for this trade": "❗️ Риск сделки слишком высок для продолжения",
tg_id=tg_id, symbol=symbol, last_side=r_side) "ab not enough for new order": "❗️ Недостаточно средств для продолжения торговли",
await rq.set_total_fee_user_auto_trading( "InvalidRequestError": "❗️ Недостаточно средств для размещения нового ордера с заданным количеством и плечом.",
tg_id=tg_id, symbol=symbol, total_fee=0 "The number of contracts exceeds maximum limit allowed": "❗️ Количество контрактов превышает допустимое максимальное количество контрактов",
) }
await rq.set_fee_user_auto_trading( error_text = errors.get(
tg_id=tg_id, symbol=symbol, fee=0 res, "❗️ Не удалось открыть новую сделку"
)
await rq.set_pnl_series_by_symbol(tg_id=tg_id, symbol=symbol, pnl_series=0)
res = await trading_cycle_profit(
tg_id=tg_id, symbol=symbol, side=r_side
)
if res == "OK":
pass
else:
errors = {
"Risk is too high for this trade": "❗️ Риск сделки слишком высок для продолжения",
"ab not enough for new order": "❗️ Недостаточно средств для продолжения торговли",
"InvalidRequestError": "❗️ Недостаточно средств для размещения нового ордера с заданным количеством и плечом.",
"The number of contracts exceeds maximum limit allowed": "❗️ Превышен максимальный лимит ставки",
"Order placement failed as your position may exceed the max": "❗️ Превышен максимальный лимит ставки",
}
error_text = errors.get(
res, "❗️ Не удалось открыть новую сделку"
)
await rq.set_auto_trading(
tg_id=tg_id, symbol=symbol, auto_trading=False
)
await rq.set_total_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, total_fee=0
)
await rq.set_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, fee=0
)
await self.telegram_bot.send_message(
chat_id=tg_id,
text=error_text,
reply_markup=kbi.profile_bybit,
)
elif stop_order_type == "StopLoss" or exec_type == "BustTrade":
current_step = user_deals_data.current_step
max_bets_in_series = user_deals_data.max_bets_in_series
current_step += 1
if max_bets_in_series < current_step:
text_series = ("\n❗️ Максимальное количество сделок в серии достигнуто.\n"
"📈 Начинаю новую серию с базовой ставки\n")
await self.telegram_bot.send_message(
chat_id=tg_id, text=text_series
)
if side == "Buy":
r_side = "Sell"
else:
r_side = "Buy"
await rq.set_last_side_by_symbol(
tg_id=tg_id, symbol=symbol, last_side=r_side)
await rq.set_total_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, total_fee=0
)
await rq.set_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, fee=0
)
await rq.set_pnl_series_by_symbol(tg_id=tg_id, symbol=symbol, pnl_series=0)
res = await trading_cycle_profit(
tg_id=tg_id, symbol=symbol, side=r_side
)
if res == "OK":
pass
else:
errors = {
"Risk is too high for this trade": "❗️ Риск сделки слишком высок для продолжения",
"ab not enough for new order": "❗️ Недостаточно средств для продолжения торговли",
"InvalidRequestError": "❗️ Недостаточно средств для размещения нового ордера с заданным количеством и плечом.",
"The number of contracts exceeds maximum limit allowed": "❗️ Превышен максимальный лимит ставки",
"Order placement failed as your position may exceed the max": "❗️ Превышен максимальный лимит ставки",
}
error_text = errors.get(
res, "❗️ Не удалось открыть новую сделку"
)
await rq.set_auto_trading(
tg_id=tg_id, symbol=symbol, auto_trading=False
)
await rq.set_total_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, total_fee=0
)
await rq.set_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, fee=0
)
await self.telegram_bot.send_message(
chat_id=tg_id,
text=error_text,
reply_markup=kbi.profile_bybit,
)
else:
open_order_text = "\n❗️ Открываю новую сделку с увеличенной ставкой.\n"
await self.telegram_bot.send_message(
chat_id=tg_id, text=open_order_text
)
if side == "Buy":
r_side = "Sell"
else:
r_side = "Buy"
res = await trading_cycle(
tg_id=tg_id, symbol=symbol, side=r_side
)
if res == "OK":
pass
else:
errors = {
"Risk is too high for this trade": "❗️ Риск сделки слишком высок для продолжения",
"ab not enough for new order": "❗️ Недостаточно средств для продолжения торговли",
"InvalidRequestError": "❗️ Недостаточно средств для размещения нового ордера с заданным количеством и плечом.",
"The number of contracts exceeds maximum limit allowed": "❗️ Превышен максимальный лимит ставки",
"Order placement failed as your position may exceed the max": "❗️ Превышен максимальный лимит ставки с текущим плечом",
}
error_text = errors.get(
res, "❗️ Не удалось открыть новую сделку"
)
await rq.set_auto_trading(
tg_id=tg_id, symbol=symbol, auto_trading=False
)
await rq.set_total_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, total_fee=0
)
await rq.set_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, fee=0
)
await self.telegram_bot.send_message(
chat_id=tg_id,
text=error_text,
reply_markup=kbi.profile_bybit,
)
elif create_type == "CreateByClosing":
await self.telegram_bot.send_message(
chat_id=tg_id,
text=f"❗️ Торговля для {symbol} остановлена",
reply_markup=kbi.profile_bybit,
) )
await rq.set_auto_trading( await rq.set_auto_trading(
tg_id=tg_id, symbol=symbol, auto_trading=False tg_id=tg_id, symbol=symbol, auto_trading=False
@@ -371,9 +189,11 @@ class TelegramMessageHandler:
await rq.set_fee_user_auto_trading( await rq.set_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, fee=0 tg_id=tg_id, symbol=symbol, fee=0
) )
logger.info("Stop trading for symbol: %s, create_type: %s, stop_order_type: %s: %s", await self.telegram_bot.send_message(
symbol, create_type, stop_order_type, tg_id) chat_id=tg_id,
else: text=error_text,
logger.info("Execution update: %s", json.dumps(message)) reply_markup=kbi.profile_bybit,
)
except Exception as e: except Exception as e:
logger.error("Error in telegram_message_handler: %s", e, exc_info=True) logger.error("Error in telegram_message_handler: %s", e)

View File

@@ -1,5 +1,4 @@
import asyncio import asyncio
from collections import deque
import logging.config import logging.config
from pybit.unified_trading import WebSocket from pybit.unified_trading import WebSocket
@@ -12,178 +11,110 @@ logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("web_socket") logger = logging.getLogger("web_socket")
class CustomWebSocket(WebSocket):
"""Custom WebSocket wrapper with enhanced error handling."""
def _on_error(self, error):
logger.error(f"WebSocket error: {error}")
return super()._on_error(error)
def _on_close(self):
logger.warning("WebSocket connection closed")
super()._on_close()
class WebSocketBot: class WebSocketBot:
""" """
Manages multiple Bybit private WebSocket connections for Telegram users. Class to handle WebSocket connections and messages.
Uses queue-based message processing to handle thread-safe async calls.
""" """
def __init__(self, telegram_bot): def __init__(self, telegram_bot):
""" """Initialize the TradingBot class."""
Initialize WebSocketBot.
Args:
telegram_bot: Telegram bot instance for message handling
"""
self.telegram_bot = telegram_bot self.telegram_bot = telegram_bot
self.user_sockets = {} self.ws_private = None
self.user_messages = {} self.user_messages = {}
self.user_sockets = {}
self.user_keys = {} self.user_keys = {}
self.loop = None self.loop = None
self.message_handler = TelegramMessageHandler(telegram_bot) self.message_handler = TelegramMessageHandler(telegram_bot)
self.order_queues = {} # {tg_id: deque}
self.execution_queues = {} # {tg_id: deque}
self.processing_tasks = {} # {tg_id: task}
async def run_user_check_loop(self): async def run_user_check_loop(self):
"""Main loop that continuously checks users and maintains connections.""" """Run a loop to check for users and connect them to the WebSocket."""
self.loop = asyncio.get_running_loop() self.loop = asyncio.get_running_loop()
logger.info("Starting WebSocket user check loop")
while True: while True:
try: users = await WebSocketBot.get_users_from_db()
users = await WebSocketBot.get_users_from_db() for user in users:
for user in users: tg_id = user.tg_id
tg_id = user.tg_id api_key, api_secret = await rq.get_user_api(tg_id=tg_id)
api_key, api_secret = await rq.get_user_api(tg_id=tg_id)
if not api_key or not api_secret: if not api_key or not api_secret:
continue continue
keys_stored = self.user_keys.get(tg_id) keys_stored = self.user_keys.get(tg_id)
socket_exists = tg_id in self.user_sockets if tg_id in self.user_sockets and keys_stored == (api_key, api_secret):
continue
if socket_exists and keys_stored == (api_key, api_secret): if tg_id in self.user_sockets:
continue 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)
if socket_exists: success = await self.try_connect_user(api_key, api_secret, tg_id)
await self.close_user_socket(tg_id) if success:
self.user_keys[tg_id] = (api_key, api_secret)
success = await self.try_connect_user(api_key, api_secret, tg_id) self.user_messages.setdefault(
if success: tg_id, {"position": None, "order": None, "execution": None}
self.user_keys[tg_id] = (api_key, api_secret) )
self.user_messages.setdefault( logger.info("User %s connected to WebSocket", tg_id)
tg_id, {"position": None, "order": None, "execution": None} else:
) await asyncio.sleep(30)
logger.info("User %s successfully connected", tg_id)
except Exception as e:
logger.error("Error in user check loop: %s", e)
await asyncio.sleep(10) 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): async def try_connect_user(self, api_key, api_secret, tg_id):
""" """Try to connect a user to the WebSocket."""
Create and setup WebSocket streams with thread-safe queues.
"""
try: try:
ws = CustomWebSocket( self.ws_private = WebSocket(
demo=True,
testnet=False, testnet=False,
channel_type="private", channel_type="private",
api_key=api_key, api_key=api_key,
api_secret=api_secret api_secret=api_secret,
) )
self.user_sockets[tg_id] = ws self.user_sockets[tg_id] = self.ws_private
# Connect to the WebSocket private channel
self.order_queues[tg_id] = deque() # Handle position updates
self.execution_queues[tg_id] = deque() self.ws_private.position_stream(
lambda msg: self.loop.call_soon_threadsafe(
self.processing_tasks[tg_id] = asyncio.create_task( asyncio.create_task, self.handle_position_update(msg)
self._process_order_queue(tg_id) )
) )
self.processing_tasks[tg_id + 1] = asyncio.create_task( # Handle order updates
self._process_execution_queue(tg_id) 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)
)
) )
def order_callback(msg):
self.order_queues[tg_id].append(msg)
def execution_callback(msg):
self.execution_queues[tg_id].append(msg)
ws.order_stream(order_callback)
ws.execution_stream(execution_callback)
logger.info("WebSocket streams configured for user %s", tg_id)
return True return True
except Exception as e: except Exception as e:
logger.error("Error connecting user %s: %s", tg_id, e) logger.error("Error connecting user %s: %s", tg_id, e)
self.user_sockets.pop(tg_id, None)
return False return False
async def _process_order_queue(self, tg_id): async def handle_position_update(self, message):
"""Continuously process order queue for user.""" """Handle position updates."""
while tg_id in self.user_sockets: await self.message_handler.format_position_update(message)
try:
if self.order_queues[tg_id]:
msg = self.order_queues[tg_id].popleft()
await self.handle_order_update(msg, tg_id)
except Exception as e:
logger.error("Error processing order queue %s: %s", tg_id, e)
await asyncio.sleep(0.01)
async def _process_execution_queue(self, tg_id):
"""Continuously process execution queue for user."""
while tg_id in self.user_sockets:
try:
if self.execution_queues[tg_id]:
msg = self.execution_queues[tg_id].popleft()
await self.handle_execution_update(msg, tg_id)
except Exception as e:
logger.error("Error processing execution queue %s: %s", tg_id, e)
await asyncio.sleep(0.01)
async def close_user_socket(self, tg_id):
"""Gracefully close user connection."""
if tg_id in self.user_sockets:
self.user_sockets.pop(tg_id, None)
for key in (tg_id, tg_id + 1):
task = self.processing_tasks.pop(key, None)
if task and not task.done():
task.cancel()
self.order_queues.pop(tg_id, None)
self.execution_queues.pop(tg_id, None)
self.user_messages.pop(tg_id, None)
self.user_keys.pop(tg_id, None)
logger.info("Cleaned up user %s", tg_id)
async def handle_order_update(self, message, tg_id): async def handle_order_update(self, message, tg_id):
"""Process order updates.""" """Handle order updates."""
try: await self.message_handler.format_order_update(message, tg_id)
await self.message_handler.format_order_update(message, tg_id)
except Exception as e:
logger.error("Error handling order update for %s: %s", tg_id, e)
async def handle_execution_update(self, message, tg_id): async def handle_execution_update(self, message, tg_id):
"""Process execution updates.""" """Handle execution updates."""
try: await self.message_handler.format_execution_update(message, tg_id)
await self.message_handler.format_execution_update(message, tg_id)
except Exception as e:
logger.error("Error handling execution update for %s: %s", tg_id, e)
@staticmethod @staticmethod
async def get_users_from_db(): async def get_users_from_db():
"""Fetch all users from database.""" """Get all users from the database."""
try: return await rq.get_users()
return await rq.get_users()
except Exception as e:
logger.error("Error getting users from DB: %s", e)
return []

View File

@@ -134,7 +134,7 @@ def check_limit_price(limit_price, min_price, max_price) -> str | None:
async def get_liquidation_price( async def get_liquidation_price(
tg_id: int, symbol: str, entry_price: float, leverage: float tg_id: int, symbol: str, entry_price: float, leverage: float
) -> tuple[float, float]: ) -> tuple[float, float]:
""" """
Function to get liquidation price Function to get liquidation price
@@ -158,7 +158,7 @@ async def get_liquidation_price(
async def calculate_total_budget( async def calculate_total_budget(
quantity, martingale_factor, max_steps quantity, martingale_factor, max_steps, commission_fee_percent
) -> float: ) -> float:
""" """
Calculate the total budget for a series of trading steps. Calculate the total budget for a series of trading steps.
@@ -167,21 +167,20 @@ async def calculate_total_budget(
quantity (float): The initial quantity of the asset. quantity (float): The initial quantity of the asset.
martingale_factor (float): The factor by which the quantity is multiplied for each step. martingale_factor (float): The factor by which the quantity is multiplied for each step.
max_steps (int): The maximum number of trading steps. max_steps (int): The maximum number of trading steps.
commission_fee_percent (float): The commission fee percentage.
Returns: Returns:
float: The total budget for the series of trading steps. float: The total budget for the series of trading steps.
""" """
total = 0 total = 0
for step in range(max_steps): for step in range(max_steps):
set_quantity = quantity * (martingale_factor**step) set_quantity = quantity * (martingale_factor ** step)
if commission_fee_percent == 0:
r_quantity = set_quantity # 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 total += r_quantity
return total return total
async def truncate_float(f, decimals=4):
factor = 10 ** decimals
return int(f * factor) / factor

View File

@@ -6,11 +6,10 @@ from aiogram.types import CallbackQuery, Message
import app.telegram.keyboards.inline as kbi import app.telegram.keyboards.inline as kbi
import database.request as rq import database.request as rq
from app.bybit.get_functions.get_tickers import get_tickers
from app.bybit.get_functions.get_instruments_info import get_instruments_info from app.bybit.get_functions.get_instruments_info import get_instruments_info
from app.bybit.get_functions.get_tickers import get_tickers
from app.bybit.profile_bybit import user_profile_bybit from app.bybit.profile_bybit import user_profile_bybit
from app.bybit.set_functions.set_leverage import set_leverage from app.bybit.set_functions.set_leverage import set_leverage
from app.bybit.set_functions.set_margin_mode import set_margin_mode from app.bybit.set_functions.set_margin_mode import set_margin_mode
from app.helper_functions import safe_float from app.helper_functions import safe_float
from app.telegram.states.states import ChangingTheSymbolState from app.telegram.states.states import ChangingTheSymbolState
@@ -99,7 +98,9 @@ async def set_symbol(message: Message, state: FSMContext) -> None:
) )
return return
instruments_info = await get_instruments_info(tg_id=message.from_user.id, symbol=symbol) instruments_info = await get_instruments_info(
tg_id=message.from_user.id, symbol=symbol
)
max_leverage = instruments_info.get("leverageFilter").get("maxLeverage") max_leverage = instruments_info.get("leverageFilter").get("maxLeverage")
req = await rq.set_user_symbol(tg_id=message.from_user.id, symbol=symbol) req = await rq.set_user_symbol(tg_id=message.from_user.id, symbol=symbol)
@@ -121,11 +122,10 @@ async def set_symbol(message: Message, state: FSMContext) -> None:
) )
await rq.set_leverage(tg_id=message.from_user.id, leverage=str(max_leverage)) await rq.set_leverage(tg_id=message.from_user.id, leverage=str(max_leverage))
risk_percent = 10 / safe_float(max_leverage) risk_percent = 100 / safe_float(max_leverage)
await rq.set_stop_loss_percent( await rq.set_stop_loss_percent(
tg_id=message.from_user.id, stop_loss_percent=risk_percent) tg_id=message.from_user.id, stop_loss_percent=risk_percent
await rq.set_take_profit_percent( )
tg_id=message.from_user.id, take_profit_percent=risk_percent)
await rq.set_trigger_price(tg_id=message.from_user.id, trigger_price=0) await rq.set_trigger_price(tg_id=message.from_user.id, trigger_price=0)
await rq.set_order_quantity(tg_id=message.from_user.id, order_quantity=1.0) await rq.set_order_quantity(tg_id=message.from_user.id, order_quantity=1.0)

View File

@@ -4,6 +4,9 @@ from aiogram import Router
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery 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 from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG) logging.config.dictConfig(LOGGING_CONFIG)
@@ -25,6 +28,31 @@ async def close_position_handler(
:return: None :return: None
""" """
try: 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( logger.debug(
"Command close_position processed successfully for user: %s", "Command close_position processed successfully for user: %s",
callback_query.from_user.id, callback_query.from_user.id,
@@ -53,6 +81,19 @@ async def cancel_order_handler(
:return: None :return: None
""" """
try: 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( logger.debug(
"Command close_order processed successfully for user: %s", "Command close_order processed successfully for user: %s",
callback_query.from_user.id, callback_query.from_user.id,

View File

@@ -53,7 +53,7 @@ async def cmd_start(message: Message, state: FSMContext) -> None:
await rq.create_user_conditional_settings(tg_id=tg_id) await rq.create_user_conditional_settings(tg_id=tg_id)
await message.answer( await message.answer(
text=f"Добро пожаловать, {full_name}!\n\n" text=f"Добро пожаловать, {full_name}!\n\n"
"Чат-робот для трейдинга - ваш надежный помощник для анализа рынка и принятия взвешенных решений.😉", "Чат-робот для трейдинга - ваш надежный помощник для анализа рынка и принятия взвешенных решений.😉",
reply_markup=kbi.connect_the_platform, reply_markup=kbi.connect_the_platform,
) )
logger.debug( logger.debug(
@@ -85,19 +85,7 @@ async def cmd_to_main(message: Message, state: FSMContext) -> None:
None: Exceptions are caught and logged internally. None: Exceptions are caught and logged internally.
""" """
try: try:
await state.clear() await user_profile_tg(tg_id=message.from_user.id, message=message)
user = await rq.get_user(tg_id=message.from_user.id)
if user:
await user_profile_tg(tg_id=message.from_user.id, message=message)
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 user_profile_tg(tg_id=message.from_user.id, message=message)
logger.debug( logger.debug(
"Command to_profile_tg processed successfully for user: %s", "Command to_profile_tg processed successfully for user: %s",
message.from_user.id, message.from_user.id,
@@ -129,21 +117,9 @@ async def profile_bybit(message: Message, state: FSMContext) -> None:
""" """
try: try:
await state.clear() await state.clear()
user = await rq.get_user(tg_id=message.from_user.id) await user_profile_bybit(
if user: tg_id=message.from_user.id, message=message, state=state
await user_profile_bybit( )
tg_id=message.from_user.id, message=message, state=state
)
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 user_profile_bybit(
tg_id=message.from_user.id, message=message, state=state)
logger.debug( logger.debug(
"Command to_profile_bybit processed successfully for user: %s", "Command to_profile_bybit processed successfully for user: %s",
message.from_user.id, message.from_user.id,
@@ -158,7 +134,7 @@ async def profile_bybit(message: Message, state: FSMContext) -> None:
@router_handlers_main.callback_query(F.data == "profile_bybit") @router_handlers_main.callback_query(F.data == "profile_bybit")
async def profile_bybit_callback( async def profile_bybit_callback(
callback_query: CallbackQuery, state: FSMContext callback_query: CallbackQuery, state: FSMContext
) -> None: ) -> None:
""" """
Handle callback query with data "profile_bybit". Handle callback query with data "profile_bybit".
@@ -174,31 +150,15 @@ async def profile_bybit_callback(
""" """
try: try:
await state.clear() await state.clear()
user = await rq.get_user(tg_id=callback_query.from_user.id) await user_profile_bybit(
tg_id=callback_query.from_user.id,
if user: message=callback_query.message,
await user_profile_bybit( state=state,
tg_id=callback_query.from_user.id, )
message=callback_query.message, logger.debug(
state=state, "Callback profile_bybit processed successfully for user: %s",
) callback_query.from_user.id,
logger.debug( )
"Callback profile_bybit 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.set_user_symbol(tg_id=callback_query.from_user.id, symbol="BTCUSDT")
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 user_profile_bybit(
tg_id=callback_query.from_user.id,
message=callback_query.message,
state=state,
)
await callback_query.answer() await callback_query.answer()
except Exception as e: except Exception as e:
logger.error( logger.error(
@@ -319,10 +279,10 @@ async def cmd_help(message: Message, state: FSMContext) -> None:
await state.clear() await state.clear()
await message.answer( await message.answer(
text="Используйте одну из следующих команд:\n" text="Используйте одну из следующих команд:\n"
"/start - Запустить бота\n" "/start - Запустить бота\n"
"/profile - Профиль\n" "/profile - Профиль\n"
"/bybit - Панель Bybit\n" "/bybit - Панель Bybit\n"
"/connect - Подключиться к платформе\n", "/connect - Подключиться к платформе\n",
reply_markup=kbr.profile, reply_markup=kbr.profile,
) )
logger.debug( logger.debug(
@@ -418,4 +378,4 @@ async def cmd_cancel(callback_query: CallbackQuery, state: FSMContext) -> None:
e, e,
) )
finally: finally:
await state.clear() await state.clear()

View File

@@ -7,7 +7,6 @@ from aiogram.types import CallbackQuery, Message
import app.telegram.keyboards.inline as kbi import app.telegram.keyboards.inline as kbi
import database.request as rq import database.request as rq
from app.bybit.get_functions.get_instruments_info import get_instruments_info from app.bybit.get_functions.get_instruments_info import get_instruments_info
from app.bybit.get_functions.get_positions import get_active_positions_by_symbol, get_active_orders_by_symbol
from app.bybit.set_functions.set_leverage import set_leverage from app.bybit.set_functions.set_leverage import set_leverage
from app.bybit.set_functions.set_margin_mode import set_margin_mode from app.bybit.set_functions.set_margin_mode import set_margin_mode
from app.helper_functions import is_int, is_number, safe_float from app.helper_functions import is_int, is_number, safe_float
@@ -22,7 +21,7 @@ router_additional_settings = Router(name="additional_settings")
@router_additional_settings.callback_query(F.data == "trade_mode") @router_additional_settings.callback_query(F.data == "trade_mode")
async def settings_for_trade_mode( async def settings_for_trade_mode(
callback_query: CallbackQuery, state: FSMContext callback_query: CallbackQuery, state: FSMContext
) -> None: ) -> None:
""" """
Handles the 'trade_mode' callback query. Handles the 'trade_mode' callback query.
@@ -41,9 +40,9 @@ async def settings_for_trade_mode(
await state.clear() await state.clear()
await callback_query.message.edit_text( await callback_query.message.edit_text(
text="Выберите режим торговли:\n\n" text="Выберите режим торговли:\n\n"
"Лонг - все сделки серии открываются на покупку.\n" "Лонг - все сделки серии открываются на покупку.\n"
"Шорт - все сделки серии открываются на продажу.\n" "Шорт - все сделки серии открываются на продажу.\n"
"Свитч - направление каждой сделки в рамках серии меняется попеременно.\n", "Свитч - направление каждой сделки серии меняется по переменно.\n",
reply_markup=kbi.trade_mode, reply_markup=kbi.trade_mode,
) )
logger.debug( logger.debug(
@@ -105,10 +104,10 @@ async def trade_mode(callback_query: CallbackQuery, state: FSMContext) -> None:
await state.clear() await state.clear()
@router_additional_settings.callback_query(F.data == "switch_side_second") @router_additional_settings.callback_query(F.data == "switch_side_start")
async def switch_side_second(callback_query: CallbackQuery, state: FSMContext) -> None: async def switch_side_start(callback_query: CallbackQuery, state: FSMContext) -> None:
""" """
Handles the 'switch_side_second' callback query. Handles the 'switch_side_start' callback query.
Clears the current FSM state, edits the message text to display the switch side start message, Clears the current FSM state, edits the message text to display the switch side start message,
and shows an inline keyboard for selection. and shows an inline keyboard for selection.
@@ -123,13 +122,13 @@ async def switch_side_second(callback_query: CallbackQuery, state: FSMContext) -
try: try:
await state.clear() await state.clear()
await callback_query.message.edit_text( await callback_query.message.edit_text(
text="Выберите направление первой сделки последующих серии:\n\n" text="Выберите направление первой сделки серии:\n\n"
"По направлению - сделка открывается в направлении последней сделки предыдущей серии.\n" "По направлению - сделка открывается в направлении последней сделки предыдущей серии.\n"
"Противоположно - сделка открывается в противоположном направлении последней сделки предыдущей серии.\n", "Противоположно - сделка открывается в противоположном направлении последней сделки предыдущей серии.\n",
reply_markup=kbi.switch_side, reply_markup=kbi.switch_side,
) )
logger.debug( logger.debug(
"Command switch_side_second processed successfully for user: %s", "Command switch_side_start processed successfully for user: %s",
callback_query.from_user.id, callback_query.from_user.id,
) )
except Exception as e: except Exception as e:
@@ -137,13 +136,15 @@ async def switch_side_second(callback_query: CallbackQuery, state: FSMContext) -
text="Произошла ошибка. Пожалуйста, попробуйте позже." text="Произошла ошибка. Пожалуйста, попробуйте позже."
) )
logger.error( logger.error(
"Error processing command switch_side_second for user %s: %s", "Error processing command switch_side_start for user %s: %s",
callback_query.from_user.id, callback_query.from_user.id,
e, e,
) )
@router_additional_settings.callback_query(lambda c: c.data == "switch_direction" or c.data == "switch_opposite") @router_additional_settings.callback_query(
lambda c: c.data == "switch_direction" or c.data == "switch_opposite"
)
async def switch_side_handler(callback_query: CallbackQuery, state: FSMContext) -> None: async def switch_side_handler(callback_query: CallbackQuery, state: FSMContext) -> None:
""" """
Handles callback queries related to switch side selection. Handles callback queries related to switch side selection.
@@ -193,92 +194,9 @@ async def switch_side_handler(callback_query: CallbackQuery, state: FSMContext)
await state.clear() await state.clear()
@router_additional_settings.callback_query(F.data == "switch_side_start")
async def switch_side_start(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handles the 'switch_side_start' callback query.
Clears the current FSM state, edits the message text to display the switch side second message,
and shows an inline keyboard for selection.
Args:
callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard.
state (FSMContext): Finite State Machine context for the current user session.
Logs:
Success or error messages with user identification.
"""
try:
await state.clear()
await callback_query.message.edit_text(
text="Выберите направление первой сделки:\n\n", reply_markup=kbi.side_for_switch
)
logger.debug(
"Command switch_side_start processed successfully for user: %s",
callback_query.from_user.id,
)
except Exception as e:
await callback_query.answer(
text="Произошла ошибка. Пожалуйста, попробуйте позже."
)
logger.error(
"Error processing command switch_side_start for user %s: %s",
callback_query.from_user.id,
e,
)
@router_additional_settings.callback_query(lambda c: c.data == "buy_switch" or c.data == "sell_switch")
async def switch_side_handler_2(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handles callback queries related to switch side selection.
Updates FSM context with selected switch side and persists the choice in database.
Sends an acknowledgement to user and clears FSM state afterward.
Args:
callback_query (CallbackQuery): Incoming callback query indicating selected switch side.
state (FSMContext): Finite State Machine context for the current user session.
Logs:
Success or error messages with user identification.
"""
try:
if callback_query.data == "sell_switch":
side = "Sell"
side_rus = "Продажа"
else:
side = "Buy"
side_rus = "Покупка"
req = await rq.set_side(
tg_id=callback_query.from_user.id, side=side
)
if not req:
await callback_query.answer(
text="Произошла ошибка при смене направления. Пожалуйста, попробуйте позже."
)
return
await callback_query.answer(text=f"Выбрано: {side_rus}")
logger.debug(
"Switch side changed successfully for user: %s", callback_query.from_user.id
)
except Exception as e:
await callback_query.answer(text="Произошла ошибка при смене направления.")
logger.error(
"Error processing set switch_side for user %s: %s",
callback_query.from_user.id,
e,
)
finally:
await state.clear()
@router_additional_settings.callback_query(F.data == "margin_type") @router_additional_settings.callback_query(F.data == "margin_type")
async def settings_for_margin_type( async def settings_for_margin_type(
callback_query: CallbackQuery, state: FSMContext callback_query: CallbackQuery, state: FSMContext
) -> None: ) -> None:
""" """
Handles the 'margin_type' callback query. Handles the 'margin_type' callback query.
@@ -295,41 +213,10 @@ async def settings_for_margin_type(
""" """
try: try:
await state.clear() await state.clear()
symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id)
deals = await get_active_positions_by_symbol(
tg_id=callback_query.from_user.id, symbol=symbol
)
if deals == "Invalid API key permissions":
await callback_query.answer(
text="API ключ не имеет достаточных прав для смены маржи",
)
return
position = next((d for d in deals if d.get("symbol") == symbol), None)
if position:
size = position.get("size", 0)
else:
size = 0
if safe_float(size) > 0:
await callback_query.answer(
text="У вас есть активная позиция по текущей паре",
)
return
orders = await get_active_orders_by_symbol(
tg_id=callback_query.from_user.id, symbol=symbol)
if orders is not None:
await callback_query.answer(
text="У вас есть активный ордер по текущей паре",
)
return
await callback_query.message.edit_text( await callback_query.message.edit_text(
text="Выберите тип маржи:\n\n" text="Выберите тип маржи:\n\n"
"Примечание: Если у вас есть открытые позиции, то маржа примениться ко всем позициям", "Примечание: Если у вас есть открытые позиции, то маржа примениться ко всем позициям",
reply_markup=kbi.margin_type reply_markup=kbi.margin_type,
) )
logger.debug( logger.debug(
"Command margin_type processed successfully for user: %s", "Command margin_type processed successfully for user: %s",
@@ -618,12 +505,12 @@ async def set_leverage_handler(message: Message, state: FSMContext) -> None:
if instruments_info is not None: if instruments_info is not None:
min_leverage = ( min_leverage = (
safe_float(instruments_info.get("leverageFilter").get("minLeverage")) safe_float(instruments_info.get("leverageFilter").get("minLeverage"))
or 1 or 1
) )
max_leverage = ( max_leverage = (
safe_float(instruments_info.get("leverageFilter").get("maxLeverage")) safe_float(instruments_info.get("leverageFilter").get("maxLeverage"))
or 100 or 100
) )
if leverage_float > max_leverage or leverage_float < min_leverage: if leverage_float > max_leverage or leverage_float < min_leverage:
@@ -666,11 +553,10 @@ async def set_leverage_handler(message: Message, state: FSMContext) -> None:
text=f"Кредитное плечо успешно установлено на {leverage_float}", text=f"Кредитное плечо успешно установлено на {leverage_float}",
reply_markup=kbi.back_to_additional_settings, reply_markup=kbi.back_to_additional_settings,
) )
risk_percent = 10 / safe_float(leverage_float) risk_percent = 100 / safe_float(leverage_float)
await rq.set_stop_loss_percent( await rq.set_stop_loss_percent(
tg_id=message.from_user.id, stop_loss_percent=risk_percent) tg_id=message.from_user.id, stop_loss_percent=risk_percent
await rq.set_take_profit_percent( )
tg_id=message.from_user.id, take_profit_percent=risk_percent)
logger.info( logger.info(
"User %s set leverage: %s", message.from_user.id, leverage_float "User %s set leverage: %s", message.from_user.id, leverage_float
) )
@@ -682,19 +568,10 @@ async def set_leverage_handler(message: Message, state: FSMContext) -> None:
await state.clear() await state.clear()
except Exception as e: except Exception as e:
errors_text = str(e) await message.answer(
known_errors = { text="Произошла ошибка при установке кредитного плеча. Пожалуйста, попробуйте позже.",
"Permission denied, please check your API key permissions": "API ключ не имеет достаточных прав для установки кредитного плеча" reply_markup=kbi.back_to_additional_settings,
)
}
for key, msg in known_errors.items():
if key in errors_text:
await message.answer(msg, reply_markup=kbi.back_to_additional_settings)
else:
await message.answer(
text="Произошла ошибка при установке кредитного плеча. Пожалуйста, попробуйте позже.",
reply_markup=kbi.back_to_additional_settings,
)
logger.error( logger.error(
"Error processing command leverage for user %s: %s", message.from_user.id, e "Error processing command leverage for user %s: %s", message.from_user.id, e
) )
@@ -893,9 +770,14 @@ async def set_martingale_factor(message: Message, state: FSMContext) -> None:
martingale_factor_value_float = safe_float(martingale_factor_value) martingale_factor_value_float = safe_float(martingale_factor_value)
if martingale_factor_value_float < 0.1 or martingale_factor_value_float > 10: if martingale_factor_value_float < 0.1 or martingale_factor_value_float > 10:
await message.answer(text="Ошибка: коэффициент мартингейла должен быть в диапазоне от 0.1 до 10") await message.answer(
logger.debug("User %s input invalid (not in range 0.1 to 10): %s", message.from_user.id, text="Ошибка: коэффициент мартингейла должен быть в диапазоне от 0.1 до 10"
martingale_factor_value_float) )
logger.debug(
"User %s input invalid (not in range 0.1 to 10): %s",
message.from_user.id,
martingale_factor_value_float,
)
return return
req = await rq.set_martingale_factor( req = await rq.set_martingale_factor(
@@ -1004,7 +886,10 @@ async def set_max_bets_in_series(message: Message, state: FSMContext) -> None:
) )
return return
if safe_float(max_bets_in_series_value) < 1 or safe_float(max_bets_in_series_value) > 100: if (
safe_float(max_bets_in_series_value) < 1
or safe_float(max_bets_in_series_value) > 100
):
await message.answer( await message.answer(
"Ошибка: число должно быть в диапазоне от 1 до 100.", "Ошибка: число должно быть в диапазоне от 1 до 100.",
reply_markup=kbi.back_to_additional_settings, reply_markup=kbi.back_to_additional_settings,

View File

@@ -98,7 +98,10 @@ async def set_take_profit_percent(message: Message, state: FSMContext) -> None:
) )
return return
if safe_float(take_profit_percent_value) < 0.1 or safe_float(take_profit_percent_value) > 100: if (
safe_float(take_profit_percent_value) < 1
or safe_float(take_profit_percent_value) > 100
):
await message.answer( await message.answer(
text="Ошибка: введите число от 1 до 100.", text="Ошибка: введите число от 1 до 100.",
reply_markup=kbi.back_to_risk_management, reply_markup=kbi.back_to_risk_management,
@@ -219,7 +222,10 @@ async def set_stop_loss_percent(message: Message, state: FSMContext) -> None:
) )
return return
if safe_float(stop_loss_percent_value) < 0.1 or safe_float(stop_loss_percent_value) > 100: if (
safe_float(stop_loss_percent_value) < 1
or safe_float(stop_loss_percent_value) > 100
):
await message.answer( await message.answer(
text="Ошибка: введите число от 1 до 100.", text="Ошибка: введите число от 1 до 100.",
reply_markup=kbi.back_to_risk_management, reply_markup=kbi.back_to_risk_management,
@@ -232,7 +238,8 @@ async def set_stop_loss_percent(message: Message, state: FSMContext) -> None:
return return
req = await rq.set_stop_loss_percent( req = await rq.set_stop_loss_percent(
tg_id=message.from_user.id, stop_loss_percent=safe_float(stop_loss_percent_value) tg_id=message.from_user.id,
stop_loss_percent=safe_float(stop_loss_percent_value),
) )
if req: if req:
@@ -341,83 +348,3 @@ async def set_commission_fee(callback_query: CallbackQuery, state: FSMContext) -
) )
finally: finally:
await state.clear() await state.clear()
@router_risk_management.callback_query(F.data == "compensation_commission")
async def compensation_commission(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handles the 'compensation_commission' callback query.
Clears the current FSM state, edits the message text to display the compensation commission options,
and shows an inline keyboard for selection.
Args:
callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard.
state (FSMContext): Finite State Machine context for the current user session.
Logs:
Success or error messages with user identification.
"""
try:
await state.clear()
msg = await callback_query.message.edit_text(
text="Выберите за счет чего будет происходить компенсация комиссии: ",
reply_markup=kbi.commission_place,
)
await state.update_data(prompt_message_id=msg.message_id)
logger.debug(
"Command compensation_commission processed successfully for user: %s",
callback_query.from_user.id,
)
except Exception as e:
await callback_query.answer(
text="Произошла ошибка. Пожалуйста, попробуйте позже."
)
logger.error(
"Error processing command compensation_commission for user %s: %s",
callback_query.from_user.id,
e,
)
@router_risk_management.callback_query(
lambda c: c.data in ["Commission_for_qty", "Commission_for_tp"]
)
async def set_compensation_commission(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handles user input for setting the compensation commission.
Updates FSM context with the selected option and persists the choice in database.
Sends an acknowledgement to user and clears FSM state afterward.
Args:
callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard.
state (FSMContext): Finite State Machine context for the current user session.
Logs:
Success or error messages with user identification.
"""
try:
req = await rq.set_commission_place(
tg_id=callback_query.from_user.id, commission_place=callback_query.data
)
if not req:
await callback_query.answer(
text="Произошла ошибка при установке компенсации комиссии. Пожалуйста, попробуйте позже."
)
return
if callback_query.data == "Commission_for_qty":
await callback_query.answer(text="Комиссия компенсируется по ставке.")
else:
await callback_query.answer(text="Комиссия компенсируется по тейк-профиту.")
except Exception as e:
logger.error(
"Error processing command compensation_commission for user %s: %s",
callback_query.from_user.id,
e,
)
finally:
await state.clear()

View File

@@ -6,7 +6,7 @@ from aiogram.types import CallbackQuery
import app.telegram.keyboards.inline as kbi import app.telegram.keyboards.inline as kbi
import database.request as rq import database.request as rq
from app.bybit import get_bybit_client
from app.helper_functions import calculate_total_budget, safe_float from app.helper_functions import calculate_total_budget, safe_float
from logger_helper.logger_helper import LOGGING_CONFIG from logger_helper.logger_helper import LOGGING_CONFIG
@@ -25,6 +25,7 @@ async def additional_settings(callback_query: CallbackQuery, state: FSMContext)
try: try:
await state.clear() await state.clear()
tg_id = callback_query.from_user.id 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) additional_data = await rq.get_user_additional_settings(tg_id=tg_id)
if not additional_data: if not additional_data:
@@ -62,29 +63,34 @@ async def additional_settings(callback_query: CallbackQuery, state: FSMContext)
max_bets = additional_data.max_bets_in_series max_bets = additional_data.max_bets_in_series
quantity = f(additional_data.order_quantity) quantity = f(additional_data.order_quantity)
trigger_price = f(additional_data.trigger_price) or 0 trigger_price = f(additional_data.trigger_price) or 0
side = additional_data.side 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)
side_map = { if commission_fee == "Yes_commission_fee":
"Buy": "Лонг", commission_fee_percent = safe_float(
"Sell": "Шорт", fee_info["result"]["list"][0]["takerFeeRate"]
} )
side_rus = side_map.get(side, side)
else:
commission_fee_percent = 0.0
switch_side_mode = "" switch_side_mode = ""
side = ""
if trade_mode == "Switch": if trade_mode == "Switch":
side = f"- Направление первой сделки: {side_rus}\n" switch_side_mode = f"- Направление первой сделки: {switch_side}\n"
switch_side_mode = f"- Направление первой сделки последующих серии: {switch_side}\n"
quantity_price = quantity * trigger_price
total_commission = quantity_price * commission_fee_percent
total_budget = await calculate_total_budget( total_budget = await calculate_total_budget(
quantity=quantity, quantity=quantity,
martingale_factor=martingale, martingale_factor=martingale,
max_steps=max_bets, max_steps=max_bets,
commission_fee_percent=total_commission,
) )
text = ( text = (
f"Основные настройки:\n\n" f"Основные настройки:\n\n"
f"- Режим торговли: {trade_mode_rus}\n" f"- Режим торговли: {trade_mode_rus}\n"
f"{side}"
f"{switch_side_mode}" f"{switch_side_mode}"
f"- Тип маржи: {margin_type_rus}\n" f"- Тип маржи: {margin_type_rus}\n"
f"- Размер кредитного плеча: {leverage:.2f}\n" f"- Размер кредитного плеча: {leverage:.2f}\n"
@@ -92,7 +98,7 @@ async def additional_settings(callback_query: CallbackQuery, state: FSMContext)
f"- Коэффициент мартингейла: {martingale:.2f}\n" f"- Коэффициент мартингейла: {martingale:.2f}\n"
f"- Триггер цена: {trigger_price:.4f} USDT\n" f"- Триггер цена: {trigger_price:.4f} USDT\n"
f"- Максимальное кол-во ставок в серии: {max_bets}\n\n" f"- Максимальное кол-во ставок в серии: {max_bets}\n\n"
f"- Бюджет серии: {total_budget:.2f} USDT\n" f"- Бюджет серии: {total_budget:.4f} USDT\n"
) )
keyboard = kbi.get_additional_settings_keyboard(mode=trade_mode) keyboard = kbi.get_additional_settings_keyboard(mode=trade_mode)
@@ -130,17 +136,12 @@ async def risk_management(callback_query: CallbackQuery, state: FSMContext) -> N
commission_fee_rus = ( commission_fee_rus = (
"Да" if commission_fee == "Yes_commission_fee" else "Нет" "Да" if commission_fee == "Yes_commission_fee" else "Нет"
) )
commission_place = risk_management_data.commission_place
commission_place_rus = (
"Ставке" if commission_place == "Commission_for_qty" else "Тейк-профиту"
)
await callback_query.message.edit_text( await callback_query.message.edit_text(
text=f"Риск-менеджмент:\n\n" text=f"Риск-менеджмент:\n\n"
f"- Процент изменения цены для фиксации прибыли: {take_profit_percent:.2f}%\n" f"- Процент изменения цены для фиксации прибыли: {take_profit_percent}%\n"
f"- Процент изменения цены для фиксации убытка: {stop_loss_percent:.2f}%\n\n" f"- Процент изменения цены для фиксации убытка: {stop_loss_percent}%\n\n"
f"- Комиссия биржи для расчета прибыли: {commission_fee_rus}\n\n" f"- Комиссия биржи для расчета прибыли: {commission_fee_rus}\n\n",
f"- Компенсация комиссии по: {commission_place_rus}",
reply_markup=kbi.risk_management, reply_markup=kbi.risk_management,
) )
logger.debug( logger.debug(
@@ -177,9 +178,11 @@ async def conditions(callback_query: CallbackQuery, state: FSMContext) -> None:
) )
if conditional_settings_data: if conditional_settings_data:
start_timer = conditional_settings_data.timer_start or 0 start_timer = conditional_settings_data.timer_start or 0
stop_timer = conditional_settings_data.timer_end or 0
await callback_query.message.edit_text( await callback_query.message.edit_text(
text="Условия торговли:\n\n" text="Условия торговли:\n\n"
f"- Таймер для старта: {start_timer} мин.\n", f"- Таймер для старта: {start_timer} мин.\n"
f"- Таймер для остановки: {stop_timer} мин.\n",
reply_markup=kbi.conditions, reply_markup=kbi.conditions,
) )
logger.debug( logger.debug(

View File

@@ -7,13 +7,10 @@ from aiogram.types import CallbackQuery
import app.telegram.keyboards.inline as kbi import app.telegram.keyboards.inline as kbi
import database.request as rq import database.request as rq
from app.bybit.get_functions.get_positions import get_active_positions_by_symbol, get_active_orders_by_symbol from app.bybit.get_functions.get_positions import get_active_positions_by_symbol
from app.bybit.open_positions import start_trading_cycle from app.bybit.open_positions import start_trading_cycle
from app.helper_functions import safe_float from app.helper_functions import safe_float
from app.telegram.tasks.tasks import ( from app.telegram.tasks.tasks import add_start_task_merged, cancel_start_task_merged
add_start_task_merged,
cancel_start_task_merged
)
from logger_helper.logger_helper import LOGGING_CONFIG from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG) logging.config.dictConfig(LOGGING_CONFIG)
@@ -33,17 +30,10 @@ async def start_trading(callback_query: CallbackQuery, state: FSMContext) -> Non
""" """
try: try:
await state.clear() await state.clear()
tg_id = callback_query.from_user.id
symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id) symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id)
deals = await get_active_positions_by_symbol( deals = await get_active_positions_by_symbol(
tg_id=callback_query.from_user.id, symbol=symbol tg_id=callback_query.from_user.id, symbol=symbol
) )
if deals == "Invalid API key permissions":
await callback_query.answer(
text="API ключ не имеет достаточных прав для запуска торговли",
)
return
position = next((d for d in deals if d.get("symbol") == symbol), None) position = next((d for d in deals if d.get("symbol") == symbol), None)
if position: if position:
@@ -53,16 +43,7 @@ async def start_trading(callback_query: CallbackQuery, state: FSMContext) -> Non
if safe_float(size) > 0: if safe_float(size) > 0:
await callback_query.answer( await callback_query.answer(
text="У вас есть активная позиция по текущей паре", text="У вас есть активная позиция",
)
return
orders = await get_active_orders_by_symbol(
tg_id=callback_query.from_user.id, symbol=symbol)
if orders is not None:
await callback_query.answer(
text="У вас есть активный ордер по текущей паре",
) )
return return
@@ -89,35 +70,22 @@ async def start_trading(callback_query: CallbackQuery, state: FSMContext) -> Non
symbol=symbol, symbol=symbol,
auto_trading=True, auto_trading=True,
) )
await rq.set_total_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, total_fee=0
)
await rq.set_fee_user_auto_trading(
tg_id=tg_id, symbol=symbol, fee=0
)
res = await start_trading_cycle( res = await start_trading_cycle(
tg_id=callback_query.from_user.id, tg_id=callback_query.from_user.id,
) )
error_messages = { error_messages = {
"Limit price is out min price": "Цена лимитного ордера меньше допустимого", "Limit price is out min price": "Цена лимитного ордера меньше минимального",
"Limit price is out max price": "Цена лимитного ордера больше допустимого", "Limit price is out max price": "Цена лимитного ордера больше максимального",
"Risk is too high for this trade": "Риск сделки превышает допустимый убыток", "Risk is too high for this trade": "Риск сделки превышает допустимый убыток",
"estimated will trigger liq": "Лимитный ордер может вызвать мгновенную ликвидацию. " "estimated will trigger liq": "Лимитный ордер может вызвать мгновенную ликвидацию. Проверьте параметры ордера.",
"Проверьте параметры ордера.",
"ab not enough for new order": "Недостаточно средств для создания нового ордера", "ab not enough for new order": "Недостаточно средств для создания нового ордера",
"InvalidRequestError": "Произошла ошибка при запуске торговли.", "InvalidRequestError": "Произошла ошибка при запуске торговли.",
"Order does not meet minimum order value": "Сумма ставки меньше допустимого для запуска торговли. " "Order does not meet minimum order value": "Сумма ордера не достаточна для запуска торговли",
"Увеличьте ставку, чтобы запустить торговлю", "position idx not match position mode": "Ошибка режима позиции для данного инструмента",
"position idx not match position mode": "Измените режим позиции, чтобы запустить торговлю", "Qty invalid": "Некорректное значение ордера для данного инструмента",
"Qty invalid": "Некорректное значение ставки для данного инструмента", "The number of contracts exceeds maximum limit allowed": "️️Количество контрактов превышает допустимое максимальное количество контрактов",
"The number of contracts exceeds maximum limit allowed": "Превышен максимальный лимит ставки", "The number of contracts exceeds minimum limit allowed": "Количество контрактов превышает допустимое минимальное количество контрактов",
"The number of contracts exceeds minimum limit allowed": "️️Лимит ставки меньше минимально допустимого",
"Order placement failed as your position may exceed the max":
"Не удалось разместить ордер, так как ваша позиция может превышать максимальный лимит."
"Пожалуйста, уменьшите кредитное плечо, чтобы увеличить максимальное значение",
"Permission denied, please check your API key permissions": "API ключ не имеет достаточных прав для запуска торговли"
} }
if res == "OK": if res == "OK":
@@ -139,18 +107,9 @@ async def start_trading(callback_query: CallbackQuery, state: FSMContext) -> Non
await add_start_task_merged(user_id=callback_query.from_user.id, task=task) await add_start_task_merged(user_id=callback_query.from_user.id, task=task)
except Exception as e: except Exception as e:
error_text = str(e) await callback_query.answer(text="Произошла ошибка при запуске торговли")
known_errors = {
"Permission denied, please check your API key permissions": "API ключ не имеет достаточных прав для запуска торговли"
}
for key, msg in known_errors.items():
if key in error_text:
await callback_query.answer(msg)
else:
await callback_query.answer(text="Произошла ошибка при запуске торговли")
logger.error( logger.error(
"Error processing command start_trading for user %s: %s", "Error processing command long for user %s: %s",
callback_query.from_user.id, callback_query.from_user.id,
e, e,
) )
@@ -158,11 +117,9 @@ async def start_trading(callback_query: CallbackQuery, state: FSMContext) -> Non
logger.error("Cancelled timer for user %s", callback_query.from_user.id) logger.error("Cancelled timer for user %s", callback_query.from_user.id)
@router_start_trading.callback_query( @router_start_trading.callback_query(lambda c: c.data == "cancel_timer_merged")
lambda c: c.data == "cancel_timer_merged"
)
async def cancel_start_trading( async def cancel_start_trading(
callback_query: CallbackQuery, state: FSMContext callback_query: CallbackQuery, state: FSMContext
) -> None: ) -> None:
""" """
Handles the "cancel_timer" callback query. Handles the "cancel_timer" callback query.

View File

@@ -5,7 +5,6 @@ from aiogram import F, Router
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery from aiogram.types import CallbackQuery
from app.bybit.close_positions import close_position_by_symbol
import app.telegram.keyboards.inline as kbi import app.telegram.keyboards.inline as kbi
import database.request as rq import database.request as rq
from app.telegram.tasks.tasks import add_stop_task, cancel_stop_task from app.telegram.tasks.tasks import add_stop_task, cancel_stop_task
@@ -28,7 +27,6 @@ async def stop_all_trading(callback_query: CallbackQuery, state: FSMContext):
tg_id=callback_query.from_user.id tg_id=callback_query.from_user.id
) )
timer_end = conditional_data.timer_end timer_end = conditional_data.timer_end
symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id)
async def delay_start(): async def delay_start():
if timer_end > 0: if timer_end > 0:
@@ -39,15 +37,30 @@ async def stop_all_trading(callback_query: CallbackQuery, state: FSMContext):
await rq.set_stop_timer(tg_id=callback_query.from_user.id, timer_end=0) await rq.set_stop_timer(tg_id=callback_query.from_user.id, timer_end=0)
await asyncio.sleep(timer_end * 60) await asyncio.sleep(timer_end * 60)
await close_position_by_symbol( user_auto_trading_list = await rq.get_all_user_auto_trading(
tg_id=callback_query.from_user.id, symbol=symbol) tg_id=callback_query.from_user.id
await rq.set_auto_trading(
tg_id=callback_query.from_user.id,
symbol=symbol,
auto_trading=False,
) )
await callback_query.message.edit_text(text=f"Торговля для {symbol} остановлена", reply_markup=kbi.profile_bybit)
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
req = await rq.set_auto_trading(
tg_id=callback_query.from_user.id,
symbol=symbol,
auto_trading=False,
)
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()) task = asyncio.create_task(delay_start())
await add_stop_task(user_id=callback_query.from_user.id, task=task) await add_stop_task(user_id=callback_query.from_user.id, task=task)
@@ -81,4 +94,4 @@ async def cancel_stop_trading(callback_query: CallbackQuery, state: FSMContext):
"Error processing command cancel_timer_stop for user %s: %s", "Error processing command cancel_timer_stop for user %s: %s",
callback_query.from_user.id, callback_query.from_user.id,
e, e,
) )

View File

@@ -36,7 +36,6 @@ main_menu = InlineKeyboardMarkup(
) )
], ],
[InlineKeyboardButton(text="Начать торговлю", callback_data="start_trading")], [InlineKeyboardButton(text="Начать торговлю", callback_data="start_trading")],
[InlineKeyboardButton(text="Остановить торговлю", callback_data="stop_trading")],
] ]
) )
@@ -62,8 +61,7 @@ main_settings = InlineKeyboardMarkup(
# additional_settings # additional_settings
def get_additional_settings_keyboard(mode: str def get_additional_settings_keyboard(mode: str) -> InlineKeyboardMarkup:
) -> InlineKeyboardMarkup:
""" """
Create keyboard for additional settings Create keyboard for additional settings
:param mode: Trade mode :param mode: Trade mode
@@ -78,26 +76,23 @@ def get_additional_settings_keyboard(mode: str
InlineKeyboardButton( InlineKeyboardButton(
text="Размер кредитного плеча", callback_data="leverage" text="Размер кредитного плеча", callback_data="leverage"
), ),
InlineKeyboardButton( InlineKeyboardButton(text="Базовая ставка", callback_data="order_quantity"),
text="Базовая ставка", callback_data="order_quantity"),
], ],
[ [
InlineKeyboardButton( InlineKeyboardButton(
text="Коэффициент мартингейла", callback_data="martingale_factor" text="Коэффициент мартингейла", callback_data="martingale_factor"
), ),
InlineKeyboardButton(text="Триггер цена", callback_data="trigger_price" InlineKeyboardButton(text="Триггер цена", callback_data="trigger_price"),
),
], ],
] ]
if mode == "Switch": if mode == "Switch":
buttons.append( buttons.append(
[InlineKeyboardButton(text="Направление первой сделки первой серии", callback_data="switch_side_start")] [
) InlineKeyboardButton(
buttons.append( text="Направление первой сделки", callback_data="switch_side_start"
[InlineKeyboardButton(text="Направление первой сделки последующих серии", callback_data="switch_side_second")] )
]
) )
buttons.append( buttons.append(
@@ -121,9 +116,7 @@ def get_additional_settings_keyboard(mode: str
trade_mode = InlineKeyboardMarkup( trade_mode = InlineKeyboardMarkup(
inline_keyboard=[ inline_keyboard=[
[ [
InlineKeyboardButton( InlineKeyboardButton(text="Лонг", callback_data="Long"),
text="Лонг", callback_data="Long"
),
InlineKeyboardButton(text="Шорт", callback_data="Short"), InlineKeyboardButton(text="Шорт", callback_data="Short"),
InlineKeyboardButton(text="Свитч", callback_data="Switch"), InlineKeyboardButton(text="Свитч", callback_data="Switch"),
], ],
@@ -151,19 +144,6 @@ switch_side = InlineKeyboardMarkup(
] ]
) )
side_for_switch = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="Лонг", callback_data="buy_switch"),
InlineKeyboardButton(text="Шорт", callback_data="sell_switch"),
],
[
InlineKeyboardButton(text="Назад", callback_data="additional_settings"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
margin_type = InlineKeyboardMarkup( margin_type = InlineKeyboardMarkup(
inline_keyboard=[ inline_keyboard=[
[ [
@@ -205,12 +185,9 @@ risk_management = InlineKeyboardMarkup(
InlineKeyboardButton( InlineKeyboardButton(
text="Тейк-профит", callback_data="take_profit_percent" text="Тейк-профит", callback_data="take_profit_percent"
), ),
InlineKeyboardButton( InlineKeyboardButton(text="Стоп-лосс", callback_data="stop_loss_percent"),
text="Стоп-лосс", callback_data="stop_loss_percent"
),
], ],
[InlineKeyboardButton(text="Комиссия биржи", callback_data="commission_fee")], [InlineKeyboardButton(text="Комиссия биржи", callback_data="commission_fee")],
[InlineKeyboardButton(text="Компенсация комиссии", callback_data="compensation_commission")],
[ [
InlineKeyboardButton(text="Назад", callback_data="main_settings"), InlineKeyboardButton(text="Назад", callback_data="main_settings"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"), InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
@@ -240,25 +217,14 @@ commission_fee = InlineKeyboardMarkup(
] ]
) )
commission_place = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="По ставке", callback_data="Commission_for_qty"),
InlineKeyboardButton(text="По тейк-профиту", callback_data="Commission_for_tp"),
],
[
InlineKeyboardButton(text="Назад", callback_data="risk_management"),
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
],
]
)
# conditions # conditions
conditions = InlineKeyboardMarkup( conditions = InlineKeyboardMarkup(
inline_keyboard=[ inline_keyboard=[
[ [
InlineKeyboardButton(text="Таймер для старта", callback_data="start_timer"), InlineKeyboardButton(text="Таймер для старта", callback_data="start_timer"),
InlineKeyboardButton(
text="Таймер для остановки", callback_data="stop_timer"
),
], ],
[ [
InlineKeyboardButton(text="Назад", callback_data="main_settings"), InlineKeyboardButton(text="Назад", callback_data="main_settings"),

View File

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

View File

@@ -1,8 +1,31 @@
import os import os
from dotenv import load_dotenv, find_dotenv from dotenv import load_dotenv, find_dotenv
import logging.config
from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("config")
env_path = find_dotenv() env_path = find_dotenv()
if env_path: if env_path:
load_dotenv(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") 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}"

View File

@@ -1,44 +1,26 @@
from database.models import Base, User, UserAdditionalSettings, UserApi, UserConditionalSettings, UserDeals, \
UserRiskManagement, UserSymbol
import logging.config import logging.config
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from sqlalchemy import event from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine, AsyncSession
from pathlib import Path
from database.models import Base
from config import DATABASE_URL
from logger_helper.logger_helper import LOGGING_CONFIG from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG) logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("database") logger = logging.getLogger("database")
BASE_DIR = Path(__file__).parent.resolve() async_engine = create_async_engine(DATABASE_URL, echo=False)
BASE_DIR.mkdir(parents=True, exist_ok=True)
DATABASE_URL = f"sqlite+aiosqlite:///{BASE_DIR / 'stcs.db'}"
async_engine = create_async_engine(
DATABASE_URL,
echo=False,
connect_args={"check_same_thread": False}
)
@event.listens_for(async_engine.sync_engine, "connect")
def _enable_foreign_keys(dbapi_connection, connection_record):
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
async_session = async_sessionmaker( async_session = async_sessionmaker(
async_engine, async_engine, class_=AsyncSession, expire_on_commit=False
class_=AsyncSession,
expire_on_commit=False
) )
async def init_db(): async def init_db():
try: try:
async with async_engine.begin() as conn: async with async_engine.begin() as conn:
await conn.run_sync(lambda sync_conn: Base.metadata.create_all(bind=sync_conn, checkfirst=True)) await conn.run_sync(Base.metadata.create_all)
logger.info("Database initialized.") logger.info("Database initialized.")
except Exception as e: except Exception as e:
logger.error("Database initialization failed: %s", e) logger.error("Database initialization failed: %s", e, exc_info=True)

View File

@@ -1,6 +1,15 @@
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.asyncio import AsyncAttrs from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy import Column, ForeignKey, Integer, String, Float, Boolean, UniqueConstraint from sqlalchemy import (
Column,
ForeignKey,
Integer,
String,
BigInteger,
Float,
Boolean,
UniqueConstraint,
)
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
Base = declarative_base(cls=AsyncAttrs) Base = declarative_base(cls=AsyncAttrs)
@@ -8,61 +17,77 @@ Base = declarative_base(cls=AsyncAttrs)
class User(Base): class User(Base):
"""User model.""" """User model."""
__tablename__ = "users" __tablename__ = "users"
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
tg_id = Column(Integer, nullable=False, unique=True) tg_id = Column(BigInteger, nullable=False, unique=True)
username = Column(String, nullable=False) username = Column(String, nullable=False)
user_api = relationship("UserApi", user_api = relationship(
back_populates="user", "UserApi",
cascade="all, delete-orphan", back_populates="user",
passive_deletes=True, cascade="all, delete-orphan",
uselist=False) passive_deletes=True,
uselist=False,
)
user_symbol = relationship("UserSymbol", user_symbol = relationship(
back_populates="user", "UserSymbol",
cascade="all, delete-orphan", back_populates="user",
passive_deletes=True, cascade="all, delete-orphan",
uselist=False) passive_deletes=True,
uselist=False,
)
user_additional_settings = relationship("UserAdditionalSettings", user_additional_settings = relationship(
back_populates="user", "UserAdditionalSettings",
cascade="all, delete-orphan", back_populates="user",
passive_deletes=True, cascade="all, delete-orphan",
uselist=False) passive_deletes=True,
uselist=False,
)
user_risk_management = relationship("UserRiskManagement", user_risk_management = relationship(
back_populates="user", "UserRiskManagement",
cascade="all, delete-orphan", back_populates="user",
passive_deletes=True, cascade="all, delete-orphan",
uselist=False) passive_deletes=True,
uselist=False,
)
user_conditional_settings = relationship("UserConditionalSettings", user_conditional_settings = relationship(
back_populates="user", "UserConditionalSettings",
cascade="all, delete-orphan", back_populates="user",
passive_deletes=True, cascade="all, delete-orphan",
uselist=False) passive_deletes=True,
uselist=False,
)
user_deals = relationship("UserDeals", user_deals = relationship(
back_populates="user", "UserDeals",
cascade="all, delete-orphan", back_populates="user",
passive_deletes=True) cascade="all, delete-orphan",
passive_deletes=True,
)
user_auto_trading = relationship("UserAutoTrading", user_auto_trading = relationship(
back_populates="user", "UserAutoTrading",
cascade="all, delete-orphan", back_populates="user",
passive_deletes=True) cascade="all, delete-orphan",
passive_deletes=True,
)
class UserApi(Base): class UserApi(Base):
"""User API model.""" """User API model."""
__tablename__ = "user_api" __tablename__ = "user_api"
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, user_id = Column(
ForeignKey("users.id", ondelete="CASCADE"), Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, unique=True
nullable=False, unique=True) )
api_key = Column(String, nullable=False) api_key = Column(String, nullable=False)
api_secret = Column(String, nullable=False) api_secret = Column(String, nullable=False)
@@ -71,12 +96,13 @@ class UserApi(Base):
class UserSymbol(Base): class UserSymbol(Base):
"""User symbol model.""" """User symbol model."""
__tablename__ = "user_symbol" __tablename__ = "user_symbol"
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, user_id = Column(
ForeignKey("users.id", ondelete="CASCADE"), Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, unique=True
nullable=False, unique=True) )
symbol = Column(String, nullable=False, default="BTCUSDT") symbol = Column(String, nullable=False, default="BTCUSDT")
user = relationship("User", back_populates="user_symbol") user = relationship("User", back_populates="user_symbol")
@@ -84,15 +110,15 @@ class UserSymbol(Base):
class UserAdditionalSettings(Base): class UserAdditionalSettings(Base):
"""User additional settings model.""" """User additional settings model."""
__tablename__ = "user_additional_settings" __tablename__ = "user_additional_settings"
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, user_id = Column(
ForeignKey("users.id", ondelete="CASCADE"), Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, unique=True
nullable=False, unique=True) )
trade_mode = Column(String, nullable=False, default="Merged_Single") trade_mode = Column(String, nullable=False, default="Merged_Single")
switch_side = Column(String, nullable=False, default="По направлению") switch_side = Column(String, nullable=False, default="По направлению")
side = Column(String, nullable=False, default="Buy")
trigger_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") margin_type = Column(String, nullable=False, default="ISOLATED_MARGIN")
leverage = Column(String, nullable=False, default="10") leverage = Column(String, nullable=False, default="10")
@@ -105,28 +131,29 @@ class UserAdditionalSettings(Base):
class UserRiskManagement(Base): class UserRiskManagement(Base):
"""User risk management model.""" """User risk management model."""
__tablename__ = "user_risk_management" __tablename__ = "user_risk_management"
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, user_id = Column(
ForeignKey("users.id", ondelete="CASCADE"), Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, unique=True
nullable=False, unique=True) )
take_profit_percent = Column(Float, nullable=False, default=1) take_profit_percent = Column(Float, nullable=False, default=1)
stop_loss_percent = Column(Float, nullable=False, default=1) stop_loss_percent = Column(Float, nullable=False, default=1)
commission_fee = Column(String, nullable=False, default="Yes_commission_fee") commission_fee = Column(String, nullable=False, default="Yes_commission_fee")
commission_place = Column(String, nullable=False, default="Commission_for_qty")
user = relationship("User", back_populates="user_risk_management") user = relationship("User", back_populates="user_risk_management")
class UserConditionalSettings(Base): class UserConditionalSettings(Base):
"""User conditional settings model.""" """User conditional settings model."""
__tablename__ = "user_conditional_settings" __tablename__ = "user_conditional_settings"
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, user_id = Column(
ForeignKey("users.id", ondelete="CASCADE"), Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, unique=True
nullable=False, unique=True) )
timer_start = Column(Integer, nullable=False, default=0) timer_start = Column(Integer, nullable=False, default=0)
timer_end = Column(Integer, nullable=False, default=0) timer_end = Column(Integer, nullable=False, default=0)
@@ -136,16 +163,16 @@ class UserConditionalSettings(Base):
class UserDeals(Base): class UserDeals(Base):
"""User deals model.""" """User deals model."""
__tablename__ = "user_deals" __tablename__ = "user_deals"
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, user_id = Column(
ForeignKey("users.id", ondelete="CASCADE"), Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False
nullable=False) )
current_step = Column(Integer, nullable=True) current_step = Column(Integer, nullable=True)
symbol = Column(String, nullable=True) symbol = Column(String, nullable=True)
trade_mode = Column(String, nullable=True) trade_mode = Column(String, nullable=True)
side_mode = Column(String, nullable=True)
base_quantity = Column(Float, nullable=True) base_quantity = Column(Float, nullable=True)
margin_type = Column(String, nullable=True) margin_type = Column(String, nullable=True)
leverage = Column(String, nullable=True) leverage = Column(String, nullable=True)
@@ -154,31 +181,24 @@ class UserDeals(Base):
order_quantity = Column(Float, nullable=True) order_quantity = Column(Float, nullable=True)
martingale_factor = Column(Float, nullable=True) martingale_factor = Column(Float, nullable=True)
max_bets_in_series = Column(Integer, nullable=True) max_bets_in_series = Column(Integer, nullable=True)
take_profit_percent = Column(Float, nullable=True) take_profit_percent = Column(Integer, nullable=True)
stop_loss_percent = Column(Float, nullable=True) stop_loss_percent = Column(Integer, nullable=True)
trigger_price = Column(Float, nullable=True) trigger_price = Column(Float, nullable=True)
current_series = Column(Integer, nullable=True)
commission_fee = Column(String, nullable=True)
commission_place = Column(String, nullable=True)
pnl_series = Column(Float, nullable=True)
take_profit = Column(Float, nullable=False, default=0.0)
stop_loss = Column(Float, nullable=False, default=0.0)
user = relationship("User", back_populates="user_deals") user = relationship("User", back_populates="user_deals")
__table_args__ = ( __table_args__ = (UniqueConstraint("user_id", "symbol", name="uq_user_symbol"),)
UniqueConstraint('user_id', 'symbol', name='uq_user_symbol'),
)
class UserAutoTrading(Base): class UserAutoTrading(Base):
"""User auto trading model.""" """User auto trading model."""
__tablename__ = "user_auto_trading" __tablename__ = "user_auto_trading"
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, user_id = Column(
ForeignKey("users.id", ondelete="CASCADE"), Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False
nullable=False) )
symbol = Column(String, nullable=True) symbol = Column(String, nullable=True)
auto_trading = Column(Boolean, nullable=True) auto_trading = Column(Boolean, nullable=True)
fee = Column(Float, nullable=True) fee = Column(Float, nullable=True)

View File

@@ -86,7 +86,7 @@ async def set_user_api(tg_id: int, api_key: str, api_secret: str) -> bool:
else: else:
# Creating new record # Creating new record
user_api = UserApi( user_api = UserApi(
user_id=user.id, api_key=api_key, api_secret=api_secret user=user, api_key=api_key, api_secret=api_secret
) )
session.add(user_api) session.add(user_api)
@@ -141,7 +141,7 @@ async def set_user_symbol(tg_id: int, symbol: str) -> bool:
# Creating new record # Creating new record
user_symbol = UserSymbol( user_symbol = UserSymbol(
symbol=symbol, symbol=symbol,
user_id=user.id, user=user,
) )
session.add(user_symbol) session.add(user_symbol)
@@ -197,11 +197,9 @@ async def create_user_additional_settings(tg_id: int) -> None:
# Create the user additional settings # Create the user additional settings
user_additional_settings = UserAdditionalSettings( user_additional_settings = UserAdditionalSettings(
user_id=user.id, user=user,
trade_mode="Long", # Default value trade_mode="Long", # Default value
switch_side="По направлению", switch_side="По направлению",
side="Buy",
trigger_price=0.0,
margin_type="ISOLATED_MARGIN", margin_type="ISOLATED_MARGIN",
leverage="10", leverage="10",
order_quantity=1.0, order_quantity=1.0,
@@ -267,7 +265,7 @@ async def set_trade_mode(tg_id: int, trade_mode: str) -> bool:
# Creating new record # Creating new record
user_additional_settings = UserAdditionalSettings( user_additional_settings = UserAdditionalSettings(
trade_mode=trade_mode, trade_mode=trade_mode,
user_id=user.id, user=user,
) )
session.add(user_additional_settings) session.add(user_additional_settings)
@@ -306,7 +304,7 @@ async def set_margin_type(tg_id: int, margin_type: str) -> bool:
# Creating new record # Creating new record
user_additional_settings = UserAdditionalSettings( user_additional_settings = UserAdditionalSettings(
margin_type=margin_type, margin_type=margin_type,
user_id=user.id, user=user,
) )
session.add(user_additional_settings) session.add(user_additional_settings)
@@ -345,7 +343,7 @@ async def set_switch_side(tg_id: int, switch_side: str) -> bool:
# Creating new record # Creating new record
user_additional_settings = UserAdditionalSettings( user_additional_settings = UserAdditionalSettings(
switch_side=switch_side, switch_side=switch_side,
user_id=user.id, user=user,
) )
session.add(user_additional_settings) session.add(user_additional_settings)
@@ -360,45 +358,6 @@ async def set_switch_side(tg_id: int, switch_side: str) -> bool:
return False return False
async def set_side(tg_id: int, side: str) -> bool:
"""
Set side for a user in the database.
:param tg_id: Telegram user ID
:param side: "BUY" or "SELL"
:return: True if successful, False otherwise
"""
try:
async with async_session() as session:
result = await session.execute(
select(User)
.options(joinedload(User.user_additional_settings))
.filter_by(tg_id=tg_id)
)
user = result.scalars().first()
if user:
if user.user_additional_settings:
# Updating existing record
user.user_additional_settings.side = side
else:
# Creating new record
user_additional_settings = UserAdditionalSettings(
side=side,
user_id=user.id,
)
session.add(user_additional_settings)
await session.commit()
logger.info("User side updated for user: %s", tg_id)
return True
else:
logger.error("User not found with tg_id: %s", tg_id)
return False
except Exception as e:
logger.error("Error adding/updating user side for user %s: %s", tg_id, e)
return False
async def set_leverage(tg_id: int, leverage: str) -> bool: async def set_leverage(tg_id: int, leverage: str) -> bool:
""" """
Set leverage for a user in the database. Set leverage for a user in the database.
@@ -423,7 +382,7 @@ async def set_leverage(tg_id: int, leverage: str) -> bool:
# Creating new record # Creating new record
user_additional_settings = UserAdditionalSettings( user_additional_settings = UserAdditionalSettings(
leverage=leverage, leverage=leverage,
user_id=user.id, user=user,
) )
session.add(user_additional_settings) session.add(user_additional_settings)
@@ -462,7 +421,7 @@ async def set_order_quantity(tg_id: int, order_quantity: float) -> bool:
# Creating new record # Creating new record
user_additional_settings = UserAdditionalSettings( user_additional_settings = UserAdditionalSettings(
order_quantity=order_quantity, order_quantity=order_quantity,
user_id=user.id, user=user,
) )
session.add(user_additional_settings) session.add(user_additional_settings)
@@ -503,7 +462,7 @@ async def set_martingale_factor(tg_id: int, martingale_factor: float) -> bool:
# Creating new record # Creating new record
user_additional_settings = UserAdditionalSettings( user_additional_settings = UserAdditionalSettings(
martingale_factor=martingale_factor, martingale_factor=martingale_factor,
user_id=user.id, user=user,
) )
session.add(user_additional_settings) session.add(user_additional_settings)
@@ -546,7 +505,7 @@ async def set_max_bets_in_series(tg_id: int, max_bets_in_series: int) -> bool:
# Creating new record # Creating new record
user_additional_settings = UserAdditionalSettings( user_additional_settings = UserAdditionalSettings(
max_bets_in_series=max_bets_in_series, max_bets_in_series=max_bets_in_series,
user_id=user.id, user=user,
) )
session.add(user_additional_settings) session.add(user_additional_settings)
@@ -587,7 +546,7 @@ async def set_trigger_price(tg_id: int, trigger_price: float) -> bool:
# Creating new record # Creating new record
user_additional_settings = UserAdditionalSettings( user_additional_settings = UserAdditionalSettings(
trigger_price=trigger_price, trigger_price=trigger_price,
user_id=user.id, user=user,
) )
session.add(user_additional_settings) session.add(user_additional_settings)
@@ -627,11 +586,10 @@ async def create_user_risk_management(tg_id: int) -> None:
# Create the user risk management # Create the user risk management
user_risk_management = UserRiskManagement( user_risk_management = UserRiskManagement(
user_id=user.id, user=user,
take_profit_percent=1.0, take_profit_percent=1.0,
stop_loss_percent=1.0, stop_loss_percent=1.0,
commission_fee="Yes_commission_fee", commission_fee="Yes_commission_fee",
commission_place="Commission_for_qty"
) )
session.add(user_risk_management) session.add(user_risk_management)
await session.commit() await session.commit()
@@ -692,7 +650,7 @@ async def set_take_profit_percent(tg_id: int, take_profit_percent: float) -> boo
# Creating new record # Creating new record
user_risk_management = UserRiskManagement( user_risk_management = UserRiskManagement(
take_profit_percent=take_profit_percent, take_profit_percent=take_profit_percent,
user_id=user.id, user=user,
) )
session.add(user_risk_management) session.add(user_risk_management)
@@ -733,7 +691,7 @@ async def set_stop_loss_percent(tg_id: int, stop_loss_percent: float) -> bool:
# Creating new record # Creating new record
user_risk_management = UserRiskManagement( user_risk_management = UserRiskManagement(
stop_loss_percent=stop_loss_percent, stop_loss_percent=stop_loss_percent,
user_id=user.id, user=user,
) )
session.add(user_risk_management) session.add(user_risk_management)
@@ -774,7 +732,7 @@ async def set_commission_fee(tg_id: int, commission_fee: str) -> bool:
# Creating new record # Creating new record
user_risk_management = UserRiskManagement( user_risk_management = UserRiskManagement(
commission_fee=commission_fee, commission_fee=commission_fee,
user_id=user.id, user=user,
) )
session.add(user_risk_management) session.add(user_risk_management)
@@ -791,47 +749,6 @@ async def set_commission_fee(tg_id: int, commission_fee: str) -> bool:
return False return False
async def set_commission_place(tg_id: int, commission_place: str) -> bool:
"""
Set commission place for a user in the database.
:param tg_id: Telegram user ID
:param commission_place: Commission place
:return: True if successful, False otherwise
"""
try:
async with async_session() as session:
result = await session.execute(
select(User)
.options(joinedload(User.user_risk_management))
.filter_by(tg_id=tg_id)
)
user = result.scalars().first()
if user:
if user.user_risk_management:
# Updating existing record
user.user_risk_management.commission_place = commission_place
else:
# Creating new record
user_risk_management = UserRiskManagement(
commission_place=commission_place,
user_id=user.id,
)
session.add(user_risk_management)
await session.commit()
logger.info("User commission place updated for user: %s", tg_id)
return True
else:
logger.error("User not found with tg_id: %s", tg_id)
return False
except Exception as e:
logger.error(
"Error adding/updating user commission place for user %s: %s", tg_id, e
)
return False
# USER CONDITIONAL SETTINGS # USER CONDITIONAL SETTINGS
@@ -855,7 +772,7 @@ async def create_user_conditional_settings(tg_id: int) -> None:
# Create the user conditional settings # Create the user conditional settings
user_conditional_settings = UserConditionalSettings( user_conditional_settings = UserConditionalSettings(
user_id=user.id, user=user,
timer_start=0, timer_start=0,
timer_end=0, timer_end=0,
) )
@@ -920,7 +837,7 @@ async def set_start_timer(tg_id: int, timer_start: int) -> bool:
# Creating new record # Creating new record
user_conditional_settings = UserConditionalSettings( user_conditional_settings = UserConditionalSettings(
timer_start=timer_start, timer_start=timer_start,
user_id=user.id, user=user,
) )
session.add(user_conditional_settings) session.add(user_conditional_settings)
@@ -959,7 +876,7 @@ async def set_stop_timer(tg_id: int, timer_end: int) -> bool:
# Creating new record # Creating new record
user_conditional_settings = UserConditionalSettings( user_conditional_settings = UserConditionalSettings(
timer_end=timer_end, timer_end=timer_end,
user_id=user.id, user=user,
) )
session.add(user_conditional_settings) session.add(user_conditional_settings)
@@ -976,33 +893,28 @@ async def set_stop_timer(tg_id: int, timer_end: int) -> bool:
# USER DEALS # USER DEALS
async def set_user_deal( async def set_user_deal(
tg_id: int, tg_id: int,
symbol: str, symbol: str,
current_step: int, last_side: str,
current_series: int, current_step: int,
trade_mode: str, trade_mode: str,
side_mode: str, margin_type: str,
margin_type: str, leverage: str,
leverage: str, order_quantity: float,
order_quantity: float, trigger_price: float,
trigger_price: float, martingale_factor: float,
martingale_factor: float, max_bets_in_series: int,
max_bets_in_series: int, take_profit_percent: int,
take_profit_percent: int, stop_loss_percent: int,
stop_loss_percent: int, base_quantity: float,
base_quantity: float,
commission_fee: str,
commission_place: str,
pnl_series: float
): ):
""" """
Set the user deal in the database. Set the user deal in the database.
:param tg_id: Telegram user ID :param tg_id: Telegram user ID
:param symbol: Symbol :param symbol: Symbol
:param last_side: Last side
:param current_step: Current step :param current_step: Current step
:param current_series: Current series
:param trade_mode: Trade mode :param trade_mode: Trade mode
:param side_mode: Side mode
:param margin_type: Margin type :param margin_type: Margin type
:param leverage: Leverage :param leverage: Leverage
:param order_quantity: Order quantity :param order_quantity: Order quantity
@@ -1012,9 +924,6 @@ async def set_user_deal(
:param take_profit_percent: Take profit percent :param take_profit_percent: Take profit percent
:param stop_loss_percent: Stop loss percent :param stop_loss_percent: Stop loss percent
:param base_quantity: Base quantity :param base_quantity: Base quantity
:param commission_fee: Commission fee
:param commission_place: Commission place
:param pnl_series: PNL series
:return: bool :return: bool
""" """
try: try:
@@ -1032,10 +941,9 @@ async def set_user_deal(
if deal: if deal:
# Updating existing record # Updating existing record
deal.last_side = last_side
deal.current_step = current_step deal.current_step = current_step
deal.current_series = current_series
deal.trade_mode = trade_mode deal.trade_mode = trade_mode
deal.side_mode = side_mode
deal.margin_type = margin_type deal.margin_type = margin_type
deal.leverage = leverage deal.leverage = leverage
deal.order_quantity = order_quantity deal.order_quantity = order_quantity
@@ -1045,18 +953,14 @@ async def set_user_deal(
deal.take_profit_percent = take_profit_percent deal.take_profit_percent = take_profit_percent
deal.stop_loss_percent = stop_loss_percent deal.stop_loss_percent = stop_loss_percent
deal.base_quantity = base_quantity deal.base_quantity = base_quantity
deal.commission_fee = commission_fee
deal.commission_place = commission_place
deal.pnl_series = pnl_series
else: else:
# Creating new record # Creating new record
new_deal = UserDeals( new_deal = UserDeals(
user_id=user.id, user=user,
symbol=symbol, symbol=symbol,
last_side=last_side,
current_step=current_step, current_step=current_step,
current_series=current_series,
trade_mode=trade_mode, trade_mode=trade_mode,
side_mode=side_mode,
margin_type=margin_type, margin_type=margin_type,
leverage=leverage, leverage=leverage,
order_quantity=order_quantity, order_quantity=order_quantity,
@@ -1066,9 +970,6 @@ async def set_user_deal(
take_profit_percent=take_profit_percent, take_profit_percent=take_profit_percent,
stop_loss_percent=stop_loss_percent, stop_loss_percent=stop_loss_percent,
base_quantity=base_quantity, base_quantity=base_quantity,
commission_fee=commission_fee,
commission_place=commission_place,
pnl_series=pnl_series
) )
session.add(new_deal) session.add(new_deal)
@@ -1077,7 +978,9 @@ async def set_user_deal(
return True return True
except Exception as e: except Exception as e:
logger.error("Error setting user deal for user %s and symbol %s: %s", tg_id, symbol, e) logger.error(
"Error setting user deal for user %s and symbol %s: %s", tg_id, symbol, e
)
return False return False
@@ -1097,7 +1000,9 @@ async def get_user_deal_by_symbol(tg_id: int, symbol: str):
deal = result_deal.scalars().first() deal = result_deal.scalars().first()
return deal return deal
except Exception as e: except Exception as e:
logger.error("Error getting deal for user %s and symbol %s: %s", tg_id, symbol, e) logger.error(
"Error getting deal for user %s and symbol %s: %s", tg_id, symbol, e
)
return None return None
@@ -1139,131 +1044,26 @@ async def set_fee_user_deal_by_symbol(tg_id: int, symbol: str, fee: float):
if record: if record:
record.fee = fee record.fee = fee
else: else:
logger.error(f"User deal with user_id={user.id} and symbol={symbol} not found") logger.error(
f"User deal with user_id={user.id} and symbol={symbol} not found"
)
return False return False
await session.commit() await session.commit()
logger.info("Set fee for user %s and symbol %s", tg_id, symbol) logger.info("Set fee for user %s and symbol %s", tg_id, symbol)
return True return True
except Exception as e: except Exception as e:
logger.error("Error setting user deal fee for user %s and symbol %s: %s", tg_id, symbol, e) logger.error(
return False "Error setting user deal fee for user %s and symbol %s: %s",
tg_id,
symbol,
async def set_last_side_by_symbol(tg_id: int, symbol: str, last_side: str): e,
"""Set last side for a user deal by symbol in the database.""" )
try:
async with async_session() as session:
result = await session.execute(select(User).filter_by(tg_id=tg_id))
user = result.scalars().first()
if user is None:
logger.error(f"User with tg_id={tg_id} not found")
return False
result = await session.execute(
select(UserDeals).filter_by(user_id=user.id, symbol=symbol)
)
record = result.scalars().first()
if record:
record.last_side = last_side
else:
logger.error(f"User deal with user_id={user.id} and symbol={symbol} not found")
return False
await session.commit()
logger.info("Set last side for user %s and symbol %s", tg_id, symbol)
return True
except Exception as e:
logger.error("Error setting user deal last side for user %s and symbol %s: %s", tg_id, symbol, e)
return False
async def set_current_series(tg_id: int, symbol: str, current_series: int):
"""Set current series for a user deal by symbol in the database."""
try:
async with async_session() as session:
result = await session.execute(select(User).filter_by(tg_id=tg_id))
user = result.scalars().first()
if user is None:
logger.error(f"User with tg_id={tg_id} not found")
return False
result = await session.execute(
select(UserDeals).filter_by(user_id=user.id, symbol=symbol)
)
record = result.scalars().first()
if record:
record.current_series = current_series
else:
logger.error(f"User deal with user_id={user.id} and symbol={symbol} not found")
return False
await session.commit()
logger.info("Set current series for user %s and symbol %s", tg_id, symbol)
return True
except Exception as e:
logger.error("Error setting user deal current series for user %s and symbol %s: %s", tg_id, symbol, e)
return False
async def set_tp_sl_by_symbol(tg_id: int, symbol: str, tp: float, sl: float):
"""Set tp and sl for a user deal by symbol in the database."""
try:
async with async_session() as session:
result = await session.execute(select(User).filter_by(tg_id=tg_id))
user = result.scalars().first()
if user is None:
logger.error(f"User with tg_id={tg_id} not found")
return False
result = await session.execute(
select(UserDeals).filter_by(user_id=user.id, symbol=symbol)
)
record = result.scalars().first()
if record:
record.take_profit = tp
record.stop_loss = sl
else:
logger.error(f"User deal with user_id={user.id} and symbol={symbol} not found")
return False
await session.commit()
logger.info("Set tp and sl for user %s and symbol %s", tg_id, symbol)
return True
except Exception as e:
logger.error("Error setting user deal tp and sl for user %s and symbol %s: %s", tg_id, symbol, e)
return False
async def set_pnl_series_by_symbol(tg_id: int, symbol: str, pnl_series: float):
"""Set pnl series for a user deal by symbol in the database."""
try:
async with async_session() as session:
result = await session.execute(select(User).filter_by(tg_id=tg_id))
user = result.scalars().first()
if user is None:
logger.error(f"User with tg_id={tg_id} not found")
return False
result = await session.execute(
select(UserDeals).filter_by(user_id=user.id, symbol=symbol)
)
record = result.scalars().first()
if record:
record.pnl_series = pnl_series
else:
logger.error(f"User deal with user_id={user.id} and symbol={symbol} not found")
return False
await session.commit()
logger.info("Set pnl series for user %s and symbol %s", tg_id, symbol)
return True
except Exception as e:
logger.error("Error setting user deal pnl series for user %s and symbol %s: %s", tg_id, symbol, e)
return False return False
# USER AUTO TRADING # USER AUTO TRADING
async def get_all_user_auto_trading(tg_id: int): async def get_all_user_auto_trading(tg_id: int):
"""Get all user auto trading from the database asynchronously.""" """Get all user auto trading from the database asynchronously."""
try: try:
@@ -1298,7 +1098,9 @@ async def get_user_auto_trading(tg_id: int, symbol: str):
auto_trading = result_auto_trading.scalars().first() auto_trading = result_auto_trading.scalars().first()
return auto_trading return auto_trading
except Exception as e: except Exception as e:
logger.error("Error getting auto trading for user %s and symbol %s: %s", tg_id, symbol, e) logger.error(
"Error getting auto trading for user %s and symbol %s: %s", tg_id, symbol, e
)
return None return None
@@ -1332,10 +1134,17 @@ async def set_auto_trading(tg_id: int, symbol: str, auto_trading: bool) -> bool:
) )
session.add(new_record) session.add(new_record)
await session.commit() await session.commit()
logger.info("Set auto_trading=%s for user %s and symbol %s", auto_trading, tg_id, symbol) logger.info(
"Set auto_trading=%s for user %s and symbol %s",
auto_trading,
tg_id,
symbol,
)
return True return True
except Exception as e: except Exception as e:
logger.error("Error setting auto_trading for user %s and symbol %s: %s", tg_id, symbol, e) logger.error(
"Error setting auto_trading for user %s and symbol %s: %s", tg_id, symbol, e
)
return False return False
@@ -1373,11 +1182,18 @@ async def set_fee_user_auto_trading(tg_id: int, symbol: str, fee: float) -> bool
logger.info("Set fee for user %s and symbol %s", tg_id, symbol) logger.info("Set fee for user %s and symbol %s", tg_id, symbol)
return True return True
except Exception as e: except Exception as e:
logger.error("Error setting user auto trading fee for user %s and symbol %s: %s", tg_id, symbol, e) logger.error(
"Error setting user auto trading fee for user %s and symbol %s: %s",
tg_id,
symbol,
e,
)
return False return False
async def set_total_fee_user_auto_trading(tg_id: int, symbol: str, total_fee: float) -> bool: async def set_total_fee_user_auto_trading(
tg_id: int, symbol: str, total_fee: float
) -> bool:
""" """
Set the total fee for a user auto trading in the database. Set the total fee for a user auto trading in the database.
:param tg_id: Telegram user ID :param tg_id: Telegram user ID
@@ -1411,5 +1227,10 @@ async def set_total_fee_user_auto_trading(tg_id: int, symbol: str, total_fee: fl
logger.info("Set total fee for user %s and symbol %s", tg_id, symbol) logger.info("Set total fee for user %s and symbol %s", tg_id, symbol)
return True return True
except Exception as e: except Exception as e:
logger.error("Error setting user auto trading total fee for user %s and symbol %s: %s", tg_id, symbol, e) logger.error(
"Error setting user auto trading total fee for user %s and symbol %s: %s",
tg_id,
symbol,
e,
)
return False return False

View File

@@ -8,7 +8,7 @@ After=syslog.target network-online.target
ExecStart=sudo -u www-data /usr/bin/python3 /var/www/stcs/BybitBot_API.py ExecStart=sudo -u www-data /usr/bin/python3 /var/www/stcs/BybitBot_API.py
PIDFile=/var/run/python/stcs.pid PIDFile=/var/run/python/stcs.pid
RemainAfterExit=no RemainAfterExit=no
RuntimeMaxSec=604800s RuntimeMaxSec=3600s
Restart=always Restart=always
RestartSec=5s RestartSec=5s

4
run.py
View File

@@ -5,10 +5,10 @@ import logging.config
from aiogram import Bot, Dispatcher from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.redis import RedisStorage from aiogram.fsm.storage.redis import RedisStorage
from database import init_db
from app.bybit.web_socket import WebSocketBot from app.bybit.web_socket import WebSocketBot
from app.telegram.handlers import router from app.telegram.handlers import router
from config import BOT_TOKEN from config import BOT_TOKEN
from database import init_db
from logger_helper.logger_helper import LOGGING_CONFIG from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG) logging.config.dictConfig(LOGGING_CONFIG)
@@ -31,6 +31,7 @@ async def main():
dp = Dispatcher(storage=storage) dp = Dispatcher(storage=storage)
dp.include_router(router) dp.include_router(router)
web_socket = WebSocketBot(telegram_bot=bot) web_socket = WebSocketBot(telegram_bot=bot)
await web_socket.clear_user_sockets()
ws_task = asyncio.create_task(web_socket.run_user_check_loop()) ws_task = asyncio.create_task(web_socket.run_user_check_loop())
tg_task = asyncio.create_task(dp.start_polling(bot)) tg_task = asyncio.create_task(dp.start_polling(bot))
@@ -45,6 +46,7 @@ async def main():
with contextlib.suppress(asyncio.CancelledError): with contextlib.suppress(asyncio.CancelledError):
await ws_task await ws_task
await tg_task await tg_task
await web_socket.clear_user_sockets()
except Exception as e: except Exception as e:
logger.error("Bot stopped with error: %s", e) logger.error("Bot stopped with error: %s", e)