From d8866af1853c6b4f4f5d79fe42266e15733f6a3b Mon Sep 17 00:00:00 2001 From: algizn97 Date: Wed, 29 Oct 2025 20:54:20 +0500 Subject: [PATCH 1/4] Added pnl, tp and sl column --- .../3fca121b7554_added_tp_sl_and_pnl.py | 44 +++++++++++++++++ .../versions/8329c0994b26_fixed_tp_sl_type.py | 49 +++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 alembic/versions/3fca121b7554_added_tp_sl_and_pnl.py create mode 100644 alembic/versions/8329c0994b26_fixed_tp_sl_type.py diff --git a/alembic/versions/3fca121b7554_added_tp_sl_and_pnl.py b/alembic/versions/3fca121b7554_added_tp_sl_and_pnl.py new file mode 100644 index 0000000..8b0d671 --- /dev/null +++ b/alembic/versions/3fca121b7554_added_tp_sl_and_pnl.py @@ -0,0 +1,44 @@ +"""Added TP_SL and PNL + +Revision ID: 3fca121b7554 +Revises: adf3d2991896 +Create Date: 2025-10-29 11:07:45.350771 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy import inspect + + +# revision identifiers, used by Alembic. +revision: str = '3fca121b7554' +down_revision: Union[str, Sequence[str], None] = 'adf3d2991896' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + conn = op.get_bind() + inspector = inspect(conn) + + columns = [col['name'] for col in inspector.get_columns('user_deals')] + + if 'pnl_series' not in columns: + op.add_column('user_deals', sa.Column('pnl_series', sa.Float(), nullable=True)) + if 'take_profit' not in columns: + op.add_column('user_deals', sa.Column('take_profit', sa.Boolean(), nullable=False, server_default=sa.false())) + if 'stop_loss' not in columns: + op.add_column('user_deals', sa.Column('stop_loss', sa.Boolean(), nullable=False, server_default=sa.false())) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('user_deals', 'stop_loss') + op.drop_column('user_deals', 'take_profit') + op.drop_column('user_deals', 'pnl_series') + # ### end Alembic commands ### diff --git a/alembic/versions/8329c0994b26_fixed_tp_sl_type.py b/alembic/versions/8329c0994b26_fixed_tp_sl_type.py new file mode 100644 index 0000000..8e51ded --- /dev/null +++ b/alembic/versions/8329c0994b26_fixed_tp_sl_type.py @@ -0,0 +1,49 @@ +"""Fixed TP_SL type + +Revision ID: 8329c0994b26 +Revises: 3fca121b7554 +Create Date: 2025-10-29 13:07:52.161139 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '8329c0994b26' +down_revision: Union[str, Sequence[str], None] = '3fca121b7554' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade(): + with op.batch_alter_table('user_deals', recreate='always') as batch_op: + # Добавляем новую колонку с нужным типом + batch_op.add_column(sa.Column('take_profit_new', sa.Float(), nullable=False, server_default='0')) + + # После закрытия batch создается и переименовывается таблица. + # Теперь мы можем обновить данные. + op.execute( + "UPDATE user_deals SET take_profit_new = CAST(take_profit AS FLOAT)" + ) + + with op.batch_alter_table('user_deals', recreate='always') as batch_op: + # Удаляем старую колонку + batch_op.drop_column('take_profit') + # Меняем имя новой колонки на старое + batch_op.alter_column('take_profit_new', new_column_name='take_profit') + +def downgrade(): + # Аналогично, но в обратном порядке и типе + with op.batch_alter_table('user_deals', recreate='always') as batch_op: + batch_op.add_column(sa.Column('take_profit_old', sa.Boolean(), nullable=False, server_default='0')) + + op.execute( + "UPDATE user_deals SET take_profit_old = CAST(take_profit AS BOOLEAN)" + ) + + with op.batch_alter_table('user_deals', recreate='always') as batch_op: + batch_op.drop_column('take_profit') + batch_op.alter_column('take_profit_old', new_column_name='take_profit') \ No newline at end of file From 7e4c936ef5b585687c3b82cb3de3159863e7afb7 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Wed, 29 Oct 2025 20:54:29 +0500 Subject: [PATCH 2/4] Added pnl, tp and sl column --- database/models.py | 3 ++ database/request.py | 67 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/database/models.py b/database/models.py index b7fa56f..24c7902 100644 --- a/database/models.py +++ b/database/models.py @@ -160,6 +160,9 @@ class UserDeals(Base): 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") diff --git a/database/request.py b/database/request.py index 38d51a1..3d34697 100644 --- a/database/request.py +++ b/database/request.py @@ -200,6 +200,8 @@ async def create_user_additional_settings(tg_id: int) -> None: user=user, trade_mode="Long", # Default value switch_side="По направлению", + side="Buy", + trigger_price=0.0, margin_type="ISOLATED_MARGIN", leverage="10", order_quantity=1.0, @@ -990,7 +992,8 @@ async def set_user_deal( stop_loss_percent: int, base_quantity: float, commission_fee: str, - commission_place: str + commission_place: str, + pnl_series: float ): """ Set the user deal in the database. @@ -1011,6 +1014,7 @@ async def set_user_deal( :param base_quantity: Base quantity :param commission_fee: Commission fee :param commission_place: Commission place + :param pnl_series: PNL series :return: bool """ try: @@ -1043,6 +1047,7 @@ async def set_user_deal( deal.base_quantity = base_quantity deal.commission_fee = commission_fee deal.commission_place = commission_place + deal.pnl_series = pnl_series else: # Creating new record new_deal = UserDeals( @@ -1062,7 +1067,8 @@ async def set_user_deal( stop_loss_percent=stop_loss_percent, base_quantity=base_quantity, commission_fee=commission_fee, - commission_place=commission_place + commission_place=commission_place, + pnl_series=pnl_series ) session.add(new_deal) @@ -1199,6 +1205,63 @@ async def set_current_series(tg_id: int, symbol: str, current_series: int): 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 + + # USER AUTO TRADING async def get_all_user_auto_trading(tg_id: int): From a8119d2811e4477cbf89b501623d96230ddd0303 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Wed, 29 Oct 2025 20:55:51 +0500 Subject: [PATCH 3/4] adjusted percentages of TP and SL --- app/helper_functions.py | 6 ++++++ app/telegram/handlers/changing_the_symbol.py | 2 +- .../main_settings/additional_settings.py | 2 +- app/telegram/handlers/stop_trading.py | 20 +++++++------------ 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/helper_functions.py b/app/helper_functions.py index 07fd74d..295153d 100644 --- a/app/helper_functions.py +++ b/app/helper_functions.py @@ -179,3 +179,9 @@ async def calculate_total_budget( total += r_quantity return total + + + +async def truncate_float(f, decimals=4): + factor = 10 ** decimals + return int(f * factor) / factor \ No newline at end of file diff --git a/app/telegram/handlers/changing_the_symbol.py b/app/telegram/handlers/changing_the_symbol.py index 689867c..1019ff2 100644 --- a/app/telegram/handlers/changing_the_symbol.py +++ b/app/telegram/handlers/changing_the_symbol.py @@ -121,7 +121,7 @@ async def set_symbol(message: Message, state: FSMContext) -> None: ) await rq.set_leverage(tg_id=message.from_user.id, leverage=str(max_leverage)) - risk_percent = 100 / safe_float(max_leverage) + risk_percent = 10 / safe_float(max_leverage) await rq.set_stop_loss_percent( tg_id=message.from_user.id, stop_loss_percent=risk_percent) await rq.set_take_profit_percent( diff --git a/app/telegram/handlers/main_settings/additional_settings.py b/app/telegram/handlers/main_settings/additional_settings.py index c792555..262ed85 100644 --- a/app/telegram/handlers/main_settings/additional_settings.py +++ b/app/telegram/handlers/main_settings/additional_settings.py @@ -660,7 +660,7 @@ async def set_leverage_handler(message: Message, state: FSMContext) -> None: text=f"Кредитное плечо успешно установлено на {leverage_float}", reply_markup=kbi.back_to_additional_settings, ) - risk_percent = 100 / safe_float(leverage_float) + risk_percent = 10 / 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( diff --git a/app/telegram/handlers/stop_trading.py b/app/telegram/handlers/stop_trading.py index 719dfa1..3ccd377 100644 --- a/app/telegram/handlers/stop_trading.py +++ b/app/telegram/handlers/stop_trading.py @@ -39,21 +39,15 @@ 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 asyncio.sleep(timer_end * 60) - user_auto_trading = await rq.get_user_auto_trading( - tg_id=callback_query.from_user.id, symbol=symbol + await rq.set_auto_trading( + tg_id=callback_query.from_user.id, + symbol=symbol, + auto_trading=False, ) + await close_position_by_symbol( + tg_id=callback_query.from_user.id, symbol=symbol) + await callback_query.message.edit_text(text=f"Торговля для {symbol} остановлена", reply_markup=kbi.profile_bybit) - if user_auto_trading and user_auto_trading.auto_trading: - await rq.set_auto_trading( - tg_id=callback_query.from_user.id, - symbol=symbol, - auto_trading=False, - ) - await close_position_by_symbol( - tg_id=callback_query.from_user.id, symbol=symbol) - await callback_query.message.edit_text(text=f"Торговля для {symbol} остановлена", reply_markup=kbi.profile_bybit) - else: - await callback_query.message.edit_text(text=f"Нет активной торговли для {symbol}", reply_markup=kbi.profile_bybit) task = asyncio.create_task(delay_start()) await add_stop_task(user_id=callback_query.from_user.id, task=task) From e043a2429fc236a4376a1afe51757e6178f626d6 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Wed, 29 Oct 2025 20:58:04 +0500 Subject: [PATCH 4/4] The logic of setting take profit and stop loss has been changed, added data for the end user --- app/bybit/open_positions.py | 69 ++--------- app/bybit/telegram_message_handler.py | 167 ++++++++++++++++---------- app/bybit/web_socket.py | 23 ++-- 3 files changed, 127 insertions(+), 132 deletions(-) diff --git a/app/bybit/open_positions.py b/app/bybit/open_positions.py index ed233d6..9ccb188 100644 --- a/app/bybit/open_positions.py +++ b/app/bybit/open_positions.py @@ -78,7 +78,8 @@ async def start_trading_cycle( stop_loss_percent=stop_loss_percent, base_quantity=order_quantity, commission_fee=commission_fee, - commission_place=commission_place + commission_place=commission_place, + pnl_series=0 ) res = await open_positions( @@ -87,10 +88,7 @@ async def start_trading_cycle( side=side, order_quantity=order_quantity, trigger_price=trigger_price, - margin_type=margin_type, - leverage=leverage, - take_profit_percent=take_profit_percent, - stop_loss_percent=stop_loss_percent, + leverage=leverage ) if res == "OK": @@ -171,7 +169,8 @@ async def trading_cycle_profit( stop_loss_percent=stop_loss_percent, base_quantity=base_quantity, commission_fee=commission_fee, - commission_place=commission_place + commission_place=commission_place, + pnl_series=0 ) res = await open_positions( @@ -180,10 +179,7 @@ async def trading_cycle_profit( 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, + leverage=leverage ) if res == "OK": @@ -227,6 +223,7 @@ async def trading_cycle( 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) @@ -272,7 +269,8 @@ async def trading_cycle( stop_loss_percent=stop_loss_percent, base_quantity=base_quantity, commission_fee=commission_fee, - commission_place=commission_place + commission_place=commission_place, + pnl_series=pnl_series ) res = await open_positions( @@ -281,10 +279,7 @@ async def trading_cycle( side=r_side, order_quantity=total_quantity, trigger_price=trigger_price, - margin_type=margin_type, - leverage=leverage, - take_profit_percent=take_profit_percent, - stop_loss_percent=stop_loss_percent, + leverage=leverage ) if res == "OK": @@ -313,18 +308,10 @@ async def open_positions( symbol: str, order_quantity: float, trigger_price: float, - margin_type: str, - leverage: str, - take_profit_percent: float, - stop_loss_percent: float + leverage: str ) -> str | None: try: client = await get_bybit_client(tg_id=tg_id) - user_deals_data = await rq.get_user_deal_by_symbol(tg_id=tg_id, symbol=symbol) - commission_fee = user_deals_data.commission_fee - commission_place = user_deals_data.commission_place - user_auto_trading_data = await rq.get_user_auto_trading(tg_id=tg_id, symbol=symbol) - total_fee = user_auto_trading_data.total_fee get_ticker = await get_tickers(tg_id, symbol=symbol) price_symbol = safe_float(get_ticker.get("lastPrice")) or 0 instruments_info = await get_instruments_info(tg_id=tg_id, symbol=symbol) @@ -342,38 +329,6 @@ async def open_positions( po_trigger_price = None trigger_direction = None - price_for_cals = trigger_price if po_trigger_price is not None else price_symbol - - if qty_formatted <= 0: - return "Order does not meet minimum order value" - - total_commission = 0 - if commission_fee == "Yes_commission_fee": - if commission_place == "Commission_for_tp": - total_commission = safe_float(total_fee) / qty_formatted - - if margin_type == "ISOLATED_MARGIN": - if side == "Buy": - take_profit_price = price_for_cals * ( - 1 + take_profit_percent / 100) + total_commission - stop_loss_price = None - else: - take_profit_price = price_for_cals * ( - 1 - take_profit_percent / 100) - total_commission - stop_loss_price = None - else: - if side == "Buy": - take_profit_price = price_for_cals * ( - 1 + take_profit_percent / 100) + total_commission - stop_loss_price = price_for_cals * (1 - stop_loss_percent / 100) - else: - take_profit_price = price_for_cals * ( - 1 - take_profit_percent / 100) - total_commission - stop_loss_price = price_for_cals * (1 + stop_loss_percent / 100) - - take_profit_price = max(take_profit_price, 0) - stop_loss_price = max(stop_loss_price, 0) - # Place order order_params = { "category": "linear", @@ -387,8 +342,6 @@ async def open_positions( "timeInForce": "GTC", "positionIdx": 0, "tpslMode": "Full", - "takeProfit": str(take_profit_price) if take_profit_price else None, - "stopLoss": str(stop_loss_price) if stop_loss_price else None, } response = client.place_order(**order_params) diff --git a/app/bybit/telegram_message_handler.py b/app/bybit/telegram_message_handler.py index 94f84d4..b5d874b 100644 --- a/app/bybit/telegram_message_handler.py +++ b/app/bybit/telegram_message_handler.py @@ -1,10 +1,13 @@ import logging.config +import math import app.telegram.keyboards.inline as kbi import database.request as rq +from app.bybit.get_functions.get_instruments_info import get_instruments_info from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG from app.bybit.open_positions import trading_cycle, trading_cycle_profit -from app.helper_functions import format_value, safe_float +from app.bybit.set_functions.set_tp_sl import set_tp_sl_for_position +from app.helper_functions import format_value, safe_float, truncate_float logging.config.dictConfig(LOGGING_CONFIG) logger = logging.getLogger("telegram_message_handler") @@ -14,65 +17,40 @@ class TelegramMessageHandler: def __init__(self, telegram_bot): self.telegram_bot = telegram_bot - async def format_position_update(self, message): - pass - async def format_order_update(self, message, tg_id): try: - order_data = message.get("data", [{}])[0] - symbol = format_value(order_data.get("symbol")) - qty = format_value(order_data.get("qty")) - side = format_value(order_data.get("side")) - side_rus = ( - "Покупка" - if side == "Buy" - else "Продажа" if side == "Sell" else "Нет данных" - ) - order_status = format_value(order_data.get("orderStatus")) - price = format_value(order_data.get("price")) - trigger_price = format_value(order_data.get("triggerPrice")) - take_profit = format_value(order_data.get("takeProfit")) - stop_loss = format_value(order_data.get("stopLoss")) + user_additional_data = await rq.get_user_additional_settings(tg_id=tg_id) + trigger_price = safe_float(user_additional_data.trigger_price) + if trigger_price > 0: + order_data = message.get("data", [{}])[0] + symbol = format_value(order_data.get("symbol")) + side = format_value(order_data.get("side")) + side_rus = ( + "Покупка" + if side == "Buy" + else "Продажа" if side == "Sell" else "Нет данных" + ) + order_status = format_value(order_data.get("orderStatus")) + tr_price = format_value(order_data.get("triggerPrice")) - status_map = { - "Untriggered": "Условный ордер выставлен", - } + status_map = { + "Untriggered": "Условный ордер выставлен", + } - if order_status == "Filled" or order_status not in status_map: - return None + if order_status == "Filled" or order_status not in status_map: + return None - user_auto_trading = await rq.get_user_auto_trading( - tg_id=tg_id, symbol=symbol - ) - auto_trading = ( - user_auto_trading.auto_trading if user_auto_trading else False - ) - user_deals_data = await rq.get_user_deal_by_symbol( - tg_id=tg_id, symbol=symbol - ) + text = ( + f"Торговая пара: {symbol}\n" + f"Движение: {side_rus}\n" + ) + if tr_price and tr_price != "Нет данных": + text += f"Триггер цена: {tr_price}\n" - text = ( - f"Торговая пара: {symbol}\n" - f"Движение: {side_rus}\n" - ) - - if user_deals_data is not None and auto_trading: - text += f"Текущая ставка: {user_deals_data.order_quantity} USDT\n" - else: - text += f"Количество: {qty}\n" - - if price and price != "0": - text += f"Цена: {price}\n" - if take_profit and take_profit != "Нет данных": - text += f"Тейк-профит: {take_profit}\n" - if stop_loss and stop_loss != "Нет данных": - text += f"Стоп-лосс: {stop_loss}\n" - if trigger_price and trigger_price != "Нет данных": - text += f"Триггер цена: {trigger_price}\n" - - await self.telegram_bot.send_message( - chat_id=tg_id, text=text, reply_markup=kbi.profile_bybit - ) + await self.telegram_bot.send_message( + chat_id=tg_id, text=text, reply_markup=kbi.profile_bybit + ) + await rq.set_trigger_price(tg_id=tg_id, trigger_price=0) except Exception as e: logger.error("Error in format_order_update: %s", e) @@ -111,6 +89,7 @@ class TelegramMessageHandler: total_fee = safe_float(exec_fee) + safe_float(get_total_fee) exec_pnl = format_value(execution.get("execPnl")) + ex_pnl = safe_float(exec_pnl) pnl = safe_float(exec_pnl) header = ( @@ -127,24 +106,33 @@ class TelegramMessageHandler: commission_fee = user_deals_data.commission_fee commission_place = user_deals_data.commission_place current_series = user_deals_data.current_series + current_step = user_deals_data.current_step + order_quantity = user_deals_data.order_quantity + pnl_series = user_deals_data.pnl_series + margin_type = user_deals_data.margin_type + take_profit_percent = user_deals_data.take_profit_percent + stop_loss_percent = user_deals_data.stop_loss_percent + fee = safe_float(user_auto_trading.fee) + total_pnl = safe_float(exec_pnl) - safe_float(exec_fee) - fee + leverage = safe_float(user_deals_data.leverage) if commission_fee == "Yes_commission_fee": if commission_place == "Commission_for_qty": - total_quantity = safe_float(user_deals_data.order_quantity) + safe_float( + total_quantity = safe_float(order_quantity) + safe_float( total_fee - ) + ) * 2 else: - total_quantity = safe_float(user_deals_data.order_quantity) + total_quantity = safe_float(order_quantity) else: - total_quantity = safe_float(user_deals_data.order_quantity) + total_quantity = safe_float(order_quantity) if user_deals_data is not None and auto_trading and safe_float(closed_size) == 0: await rq.set_total_fee_user_auto_trading( tg_id=tg_id, symbol=symbol, total_fee=total_fee ) text += f"Текущая ставка: {total_quantity:.2f} USDT\n" - text += f"Серия №: {user_deals_data.current_series}\n" - text += f"Сделка №: {user_deals_data.current_step}\n" + text += f"Серия №: {current_series}\n" + text += f"Сделка №: {current_step}\n" text += ( f"Цена исполнения: {exec_price}\n" @@ -152,9 +140,61 @@ class TelegramMessageHandler: ) if safe_float(closed_size) == 0: - text += f"Движение: {side_rus}\n" + instruments_info = await get_instruments_info(tg_id=tg_id, symbol=symbol) + qty_step_str = instruments_info.get("lotSizeFilter").get("qtyStep") + qty_step = safe_float(qty_step_str) + qty = (safe_float(order_quantity) * safe_float(leverage)) / safe_float(exec_price) + decimals = abs(int(round(math.log10(qty_step)))) + qty_format = math.floor(qty / qty_step) * qty_step + qty_formatted = round(qty_format, decimals) + total_commission = 0 + + if commission_fee == "Yes_commission_fee": + if commission_place == "Commission_for_tp": + total_commission = safe_float(total_fee) / qty_formatted + + if margin_type == "ISOLATED_MARGIN": + if side == "Buy": + take_profit_price = safe_float(exec_price) * ( + 1 + take_profit_percent / 100) + total_commission + stop_loss_price = None + else: + take_profit_price = safe_float(exec_price) * ( + 1 - take_profit_percent / 100) - total_commission + stop_loss_price = None + else: + if side == "Buy": + take_profit_price = safe_float(exec_price) * ( + 1 + take_profit_percent / 100) + total_commission + stop_loss_price = safe_float(exec_price) * (1 - stop_loss_percent / 100) + else: + take_profit_price = safe_float(exec_price) * ( + 1 - take_profit_percent / 100) - total_commission + stop_loss_price = safe_float(exec_price) * (1 + stop_loss_percent / 100) + + take_profit_price = max(take_profit_price, 0) + stop_loss_price = max(stop_loss_price, 0) + + 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) + + take_profit_truncated = await truncate_float(take_profit_price, 4) + stop_loss_truncated = await truncate_float(stop_loss_price, 4) + + text += (f"Движение: {side_rus}\n" + f"Тейк-профит: {take_profit_truncated}\n" + f"Стоп-лосс: {stop_loss_truncated}\n" + ) else: - text += f"\nПрибыль: {pnl:.7f}\n" + 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"Реализованный PNL: {total_pnl:.4f}\n" + text += f"Прибыль серии: {safe_float(new_pnl):.4f}\n" await self.telegram_bot.send_message( chat_id=tg_id, text=text, reply_markup=kbi.profile_bybit @@ -186,6 +226,7 @@ class TelegramMessageHandler: 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 @@ -263,4 +304,4 @@ class TelegramMessageHandler: ) except Exception as e: - logger.error("Error in telegram_message_handler: %s", e) + logger.error("Error in telegram_message_handler: %s", e, exc_info=True) diff --git a/app/bybit/web_socket.py b/app/bybit/web_socket.py index c937510..2690d78 100644 --- a/app/bybit/web_socket.py +++ b/app/bybit/web_socket.py @@ -25,6 +25,7 @@ class WebSocketBot: self.user_keys = {} self.loop = None self.message_handler = TelegramMessageHandler(telegram_bot) + self.last_execution_seq = {} async def run_user_check_loop(self): """Run a loop to check for users and connect them to the WebSocket.""" @@ -83,12 +84,6 @@ class WebSocketBot: self.user_sockets[tg_id] = self.ws_private # Connect to the WebSocket private channel - # Handle position updates - self.ws_private.position_stream( - lambda msg: self.loop.call_soon_threadsafe( - asyncio.create_task, self.handle_position_update(msg) - ) - ) # Handle order updates self.ws_private.order_stream( lambda msg: self.loop.call_soon_threadsafe( @@ -106,17 +101,23 @@ class WebSocketBot: logger.error("Error connecting user %s: %s", tg_id, e) return False - async def handle_position_update(self, message): - """Handle position updates.""" - await self.message_handler.format_position_update(message) - async def handle_order_update(self, message, tg_id): """Handle order updates.""" await self.message_handler.format_order_update(message, tg_id) async def handle_execution_update(self, message, tg_id): """Handle execution updates.""" - await self.message_handler.format_execution_update(message, tg_id) + data_items = message.get('data', []) + if not data_items: + return + for exec_data in data_items: + seq = exec_data.get('seq') + if tg_id not in self.last_execution_seq: + self.last_execution_seq[tg_id] = -1 + if seq <= self.last_execution_seq[tg_id]: + continue + self.last_execution_seq[tg_id] = seq + await self.message_handler.format_execution_update(message, tg_id) @staticmethod async def get_users_from_db():