From ddfa3a73601d6c56e992d40fd90c56059c43239c Mon Sep 17 00:00:00 2001 From: algizn97 Date: Wed, 22 Oct 2025 17:15:25 +0500 Subject: [PATCH] Fixed the switch trading mode, adjusted the take profit, added a trading cycle --- README.md | 4 + .../fbf4e3658310_added_side_mode_column.py | 32 +++ app/bybit/open_positions.py | 206 ++++++++++++------ app/bybit/telegram_message_handler.py | 64 +++++- app/telegram/handlers/changing_the_symbol.py | 2 + .../main_settings/additional_settings.py | 4 +- database/models.py | 1 + database/request.py | 32 +++ 8 files changed, 261 insertions(+), 84 deletions(-) create mode 100644 alembic/versions/fbf4e3658310_added_side_mode_column.py diff --git a/README.md b/README.md index 70599b7..aa43dc5 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,10 @@ sudo -u www-data /usr/bin/pip install -r requirements.txt cp .env.sample .env nvim .env ``` +5. Выполните миграции: +```bash +alembic upgrade head +``` 5. Запустите бота: diff --git a/alembic/versions/fbf4e3658310_added_side_mode_column.py b/alembic/versions/fbf4e3658310_added_side_mode_column.py new file mode 100644 index 0000000..3b63c77 --- /dev/null +++ b/alembic/versions/fbf4e3658310_added_side_mode_column.py @@ -0,0 +1,32 @@ +"""Added side_mode column + +Revision ID: fbf4e3658310 +Revises: +Create Date: 2025-10-22 13:08:02.317419 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'fbf4e3658310' +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('user_deals', sa.Column('side_mode', sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('user_deals', 'side_mode') + # ### end Alembic commands ### diff --git a/app/bybit/open_positions.py b/app/bybit/open_positions.py index 1b0c401..d4dee4a 100644 --- a/app/bybit/open_positions.py +++ b/app/bybit/open_positions.py @@ -11,14 +11,14 @@ from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG from app.bybit.set_functions.set_leverage import set_leverage from app.bybit.set_functions.set_margin_mode import set_margin_mode from app.bybit.set_functions.set_switch_position_mode import set_switch_position_mode -from app.helper_functions import get_liquidation_price, safe_float +from app.helper_functions import safe_float logging.config.dictConfig(LOGGING_CONFIG) logger = logging.getLogger("open_positions") async def start_trading_cycle( - tg_id: int + tg_id: int ) -> str | None: """ Start trading cycle @@ -93,6 +93,7 @@ async def start_trading_cycle( last_side=side, current_step=1, trade_mode=trade_mode, + side_mode=switch_side, margin_type=margin_type, leverage=leverage, order_quantity=order_quantity, @@ -107,19 +108,19 @@ async def start_trading_cycle( return ( res if res - in { - "Limit price is out min price", - "Limit price is out max price", - "Risk is too high for this trade", - "estimated will trigger liq", - "ab not enough for new order", - "InvalidRequestError", - "Order does not meet minimum order value", - "position idx not match position mode", - "Qty invalid", - "The number of contracts exceeds maximum limit allowed", - "The number of contracts exceeds minimum limit allowed" - } + in { + "Limit price is out min price", + "Limit price is out max price", + "Risk is too high for this trade", + "estimated will trigger liq", + "ab not enough for new order", + "InvalidRequestError", + "Order does not meet minimum order value", + "position idx not match position mode", + "Qty invalid", + "The number of contracts exceeds maximum limit allowed", + "The number of contracts exceeds minimum limit allowed" + } else None ) @@ -128,8 +129,90 @@ async def start_trading_cycle( return None +async def trading_cycle_profit( + tg_id: int, symbol: str, side: str) -> str | None: + try: + user_deals_data = await rq.get_user_deal_by_symbol(tg_id=tg_id, symbol=symbol) + user_auto_trading_data = await rq.get_user_auto_trading(tg_id=tg_id, symbol=symbol) + total_fee = user_auto_trading_data.total_fee + trade_mode = user_deals_data.trade_mode + margin_type = user_deals_data.margin_type + leverage = user_deals_data.leverage + trigger_price = 0 + take_profit_percent = user_deals_data.take_profit_percent + stop_loss_percent = user_deals_data.stop_loss_percent + max_bets_in_series = user_deals_data.max_bets_in_series + martingale_factor = user_deals_data.martingale_factor + side_mode = user_deals_data.side_mode + base_quantity = user_deals_data.base_quantity + + await set_margin_mode(tg_id=tg_id, margin_mode=margin_type) + await set_leverage( + tg_id=tg_id, + symbol=symbol, + leverage=leverage, + ) + + + if trade_mode == "Switch": + if side_mode == "Противоположно": + s_side = "Sell" if side == "Buy" else "Buy" + else: + s_side = side + else: + s_side = side + + res = await open_positions( + tg_id=tg_id, + symbol=symbol, + side=s_side, + order_quantity=base_quantity, + trigger_price=trigger_price, + margin_type=margin_type, + leverage=leverage, + take_profit_percent=take_profit_percent, + stop_loss_percent=stop_loss_percent, + commission_fee_percent=total_fee + ) + + if res == "OK": + await rq.set_user_deal( + tg_id=tg_id, + symbol=symbol, + last_side=side, + current_step=1, + trade_mode=trade_mode, + side_mode=side_mode, + margin_type=margin_type, + leverage=leverage, + order_quantity=base_quantity, + trigger_price=trigger_price, + martingale_factor=martingale_factor, + max_bets_in_series=max_bets_in_series, + take_profit_percent=take_profit_percent, + stop_loss_percent=stop_loss_percent, + base_quantity=base_quantity + ) + return "OK" + + return ( + res + if res + in { + "Risk is too high for this trade", + "ab not enough for new order", + "InvalidRequestError", + "The number of contracts exceeds maximum limit allowed", + } + else None + ) + except Exception as e: + logger.error("Error in trading_cycle_profit: %s", e) + return None + + async def trading_cycle( - tg_id: int, symbol: str, reverse_side: str + 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) @@ -146,23 +229,7 @@ async def trading_cycle( current_step = user_deals_data.current_step order_quantity = user_deals_data.order_quantity base_quantity = user_deals_data.base_quantity - - await set_margin_mode(tg_id=tg_id, margin_mode=margin_type) - await set_leverage( - tg_id=tg_id, - symbol=symbol, - leverage=leverage, - ) - - if reverse_side == "Buy": - real_side = "Sell" - else: - real_side = "Buy" - - side = real_side - - if trade_mode == "Switch": - side = "Sell" if real_side == "Buy" else "Buy" + side_mode = user_deals_data.side_mode next_quantity = safe_float(order_quantity) * ( safe_float(martingale_factor) @@ -172,6 +239,13 @@ async def trading_cycle( if max_bets_in_series < current_step: return "Max bets in series" + await set_margin_mode(tg_id=tg_id, margin_mode=margin_type) + await set_leverage( + tg_id=tg_id, + symbol=symbol, + leverage=leverage, + ) + res = await open_positions( tg_id=tg_id, symbol=symbol, @@ -192,6 +266,7 @@ async def trading_cycle( last_side=side, current_step=current_step, trade_mode=trade_mode, + side_mode=side_mode, margin_type=margin_type, leverage=leverage, order_quantity=next_quantity, @@ -207,12 +282,12 @@ async def trading_cycle( return ( res if res - in { - "Risk is too high for this trade", - "ab not enough for new order", - "InvalidRequestError", - "The number of contracts exceeds maximum limit allowed", - } + in { + "Risk is too high for this trade", + "ab not enough for new order", + "InvalidRequestError", + "The number of contracts exceeds maximum limit allowed", + } else None ) @@ -222,16 +297,16 @@ async def trading_cycle( async def open_positions( - tg_id: int, - side: str, - symbol: str, - order_quantity: float, - trigger_price: float, - margin_type: str, - leverage: str, - take_profit_percent: float, - stop_loss_percent: float, - commission_fee_percent: float + tg_id: int, + side: str, + symbol: str, + order_quantity: float, + trigger_price: float, + margin_type: str, + leverage: str, + take_profit_percent: float, + stop_loss_percent: float, + commission_fee_percent: float ) -> str | None: try: client = await get_bybit_client(tg_id=tg_id) @@ -252,39 +327,28 @@ async def open_positions( po_trigger_price = None trigger_direction = None - get_leverage = safe_float(leverage) - price_for_cals = trigger_price if po_trigger_price is not None else price_symbol if qty_formatted <= 0: return "Order does not meet minimum order value" if margin_type == "ISOLATED_MARGIN": - 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 / qty_formatted - else: - base_tp = price_for_cals - (liq_short - price_for_cals) - take_profit_price = base_tp - commission_fee_percent / qty_formatted - take_profit_price = max(take_profit_price, 0) + if side == "Buy": + take_profit_price = price_for_cals * ( + 1 + take_profit_percent / 100) + commission_fee_percent / qty_formatted + stop_loss_price = None else: - take_profit_price = None - - stop_loss_price = None + take_profit_price = price_for_cals * ( + 1 - take_profit_percent / 100) - commission_fee_percent / qty_formatted + stop_loss_price = None else: if side == "Buy": - take_profit_price = price_for_cals * (1 + take_profit_percent / 100) + commission_fee_percent / qty_formatted + take_profit_price = price_for_cals * ( + 1 + take_profit_percent / 100) + commission_fee_percent / qty_formatted stop_loss_price = price_for_cals * (1 - stop_loss_percent / 100) else: - take_profit_price = price_for_cals * (1 - take_profit_percent / 100) - commission_fee_percent / qty_formatted + take_profit_price = price_for_cals * ( + 1 - take_profit_percent / 100) - commission_fee_percent / qty_formatted stop_loss_price = price_for_cals * (1 + stop_loss_percent / 100) take_profit_price = max(take_profit_price, 0) diff --git a/app/bybit/telegram_message_handler.py b/app/bybit/telegram_message_handler.py index 616cff3..ff85da3 100644 --- a/app/bybit/telegram_message_handler.py +++ b/app/bybit/telegram_message_handler.py @@ -3,7 +3,7 @@ import logging.config import app.telegram.keyboards.inline as kbi import database.request as rq from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG -from app.bybit.open_positions import trading_cycle +from app.bybit.open_positions import trading_cycle, trading_cycle_profit from app.helper_functions import format_value, safe_float logging.config.dictConfig(LOGGING_CONFIG) @@ -57,7 +57,7 @@ class TelegramMessageHandler: ) if user_deals_data is not None and auto_trading: - text += f"Текущая ставка: {user_deals_data.order_quantity}\n" + text += f"Текущая ставка: {user_deals_data.order_quantity} USDT\n" else: text += f"Количество: {qty}\n" @@ -146,11 +146,12 @@ class TelegramMessageHandler: text += ( f"Цена исполнения: {exec_price}\n" - f"Движение: {side_rus}\n" f"Комиссия: {exec_fee:.8f}\n" ) - if safe_float(closed_size) > 0: + if safe_float(closed_size) == 0: + text += f"Движение: {side_rus}\n" + else: text += f"\nРеализованная прибыль: {total_pnl:.7f}\n" await self.telegram_bot.send_message( @@ -165,31 +166,70 @@ class TelegramMessageHandler: and user_symbols is not None ): if safe_float(total_pnl) > 0: - profit_text = "📈 Прибыль достигнута\n" + profit_text = "📈 Прибыль достигнута. Начинаем новую серию с базовой ставки\n" await self.telegram_bot.send_message( 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 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 ) - base_quantity = user_deals_data.base_quantity - await rq.set_order_quantity( - tg_id=tg_id, order_quantity=base_quantity + + res = await trading_cycle_profit( + tg_id=tg_id, symbol=symbol, side=r_side ) + + if res == "OK": + pass + else: + errors = { + "Max bets in series": "❗️ Максимальное количество сделок в серии достигнуто", + "Risk is too high for this trade": "❗️ Риск сделки слишком высок для продолжения", + "ab not enough for new order": "❗️ Недостаточно средств для продолжения торговли", + "InvalidRequestError": "❗️ Недостаточно средств для размещения нового ордера с заданным количеством и плечом.", + "The number of contracts exceeds maximum limit allowed": "❗️ Превышен максимальный лимит ставки", + } + error_text = errors.get( + res, "❗️ Не удалось открыть новую сделку" + ) + await rq.set_auto_trading( + tg_id=tg_id, symbol=symbol, auto_trading=False + ) + + await rq.set_total_fee_user_auto_trading( + tg_id=tg_id, symbol=symbol, total_fee=0 + ) + await rq.set_fee_user_auto_trading( + tg_id=tg_id, symbol=symbol, fee=0 + ) + await self.telegram_bot.send_message( + chat_id=tg_id, + text=error_text, + reply_markup=kbi.profile_bybit, + ) else: open_order_text = "\n❗️ Сделка закрылась в минус, открываю новую сделку с увеличенной ставкой.\n" await self.telegram_bot.send_message( chat_id=tg_id, text=open_order_text ) + + if side == "Buy": + r_side = "Sell" + else: + r_side = "Buy" + res = await trading_cycle( - tg_id=tg_id, symbol=symbol, reverse_side=side + tg_id=tg_id, symbol=symbol, side=r_side ) if res == "OK": diff --git a/app/telegram/handlers/changing_the_symbol.py b/app/telegram/handlers/changing_the_symbol.py index 16e5568..689867c 100644 --- a/app/telegram/handlers/changing_the_symbol.py +++ b/app/telegram/handlers/changing_the_symbol.py @@ -124,6 +124,8 @@ async def set_symbol(message: Message, state: FSMContext) -> None: risk_percent = 100 / safe_float(max_leverage) await rq.set_stop_loss_percent( tg_id=message.from_user.id, stop_loss_percent=risk_percent) + await rq.set_take_profit_percent( + tg_id=message.from_user.id, take_profit_percent=risk_percent) await rq.set_trigger_price(tg_id=message.from_user.id, trigger_price=0) await rq.set_order_quantity(tg_id=message.from_user.id, order_quantity=1.0) diff --git a/app/telegram/handlers/main_settings/additional_settings.py b/app/telegram/handlers/main_settings/additional_settings.py index 1f9099e..ad67eeb 100644 --- a/app/telegram/handlers/main_settings/additional_settings.py +++ b/app/telegram/handlers/main_settings/additional_settings.py @@ -43,7 +43,7 @@ async def settings_for_trade_mode( text="Выберите режим торговли:\n\n" "Лонг - все сделки серии открываются на покупку.\n" "Шорт - все сделки серии открываются на продажу.\n" - "Свитч - направление каждой сделки серии меняется по переменно.\n", + "Свитч - направление первой сделки серии меняется по переменно.\n", reply_markup=kbi.trade_mode, ) logger.debug( @@ -580,6 +580,8 @@ async def set_leverage_handler(message: Message, state: FSMContext) -> None: risk_percent = 100 / safe_float(leverage_float) await rq.set_stop_loss_percent( tg_id=message.from_user.id, stop_loss_percent=risk_percent) + await rq.set_take_profit_percent( + tg_id=message.from_user.id, take_profit_percent=risk_percent) logger.info( "User %s set leverage: %s", message.from_user.id, leverage_float ) diff --git a/database/models.py b/database/models.py index 33369c2..6dd8865 100644 --- a/database/models.py +++ b/database/models.py @@ -143,6 +143,7 @@ class UserDeals(Base): current_step = Column(Integer, nullable=True) symbol = Column(String, nullable=True) trade_mode = Column(String, nullable=True) + side_mode = Column(String, nullable=True) base_quantity = Column(Float, nullable=True) margin_type = Column(String, nullable=True) leverage = Column(String, nullable=True) diff --git a/database/request.py b/database/request.py index 6492cea..8677bf7 100644 --- a/database/request.py +++ b/database/request.py @@ -898,6 +898,7 @@ async def set_user_deal( last_side: str, current_step: int, trade_mode: str, + side_mode: str, margin_type: str, leverage: str, order_quantity: float, @@ -915,6 +916,7 @@ async def set_user_deal( :param last_side: Last side :param current_step: Current step :param trade_mode: Trade mode + :param side_mode: Side mode :param margin_type: Margin type :param leverage: Leverage :param order_quantity: Order quantity @@ -944,6 +946,7 @@ async def set_user_deal( deal.last_side = last_side deal.current_step = current_step deal.trade_mode = trade_mode + deal.side_mode = side_mode deal.margin_type = margin_type deal.leverage = leverage deal.order_quantity = order_quantity @@ -961,6 +964,7 @@ async def set_user_deal( last_side=last_side, current_step=current_step, trade_mode=trade_mode, + side_mode=side_mode, margin_type=margin_type, leverage=leverage, order_quantity=order_quantity, @@ -1050,6 +1054,34 @@ async def set_fee_user_deal_by_symbol(tg_id: int, symbol: str, fee: float): return False +async def set_last_side_by_symbol(tg_id: int, symbol: str, last_side: str): + """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 + + # USER AUTO TRADING async def get_all_user_auto_trading(tg_id: int):