From 4c9901c14aa18a801cfd8482f94036337952ecb8 Mon Sep 17 00:00:00 2001 From: algizn97 Date: Wed, 17 Sep 2025 20:51:30 +0500 Subject: [PATCH] Fixed --- app/services/Bybit/functions/Futures.py | 82 ++++++++----------- app/services/Bybit/functions/functions.py | 51 +++++++++++- app/telegram/Keyboards/inline_keyboards.py | 2 +- app/telegram/database/models.py | 2 + app/telegram/database/requests.py | 18 +++- .../functions/condition_settings/settings.py | 32 +------- .../functions/main_settings/settings.py | 26 +----- requirements.txt | 3 + 8 files changed, 104 insertions(+), 112 deletions(-) diff --git a/app/services/Bybit/functions/Futures.py b/app/services/Bybit/functions/Futures.py index 875eef2..3d6727c 100644 --- a/app/services/Bybit/functions/Futures.py +++ b/app/services/Bybit/functions/Futures.py @@ -172,7 +172,7 @@ def parse_pnl_from_msg(msg) -> float: return 0.0 -async def calculate_total_budget(starting_quantity, martingale_factor, max_steps, commission_fee_percent, leverage, current_price): +async def calculate_total_budget(starting_quantity, martingale_factor, max_steps, commission_fee_percent): """ Вычисляет общий бюджет серии ставок с учётом цены пары, комиссии и кредитного плеча. @@ -189,22 +189,16 @@ async def calculate_total_budget(starting_quantity, martingale_factor, max_steps """ total = 0 for step in range(max_steps): - quantity = starting_quantity * (martingale_factor ** step) # размер ставки на текущем шаге в USDT + base_quantity = starting_quantity * (martingale_factor ** step) + if commission_fee_percent == 0: + # Комиссия уже включена в сумму ставки, поэтому реальный размер позиции меньше + quantity = base_quantity / (1 + commission_fee_percent) + else: + # Комиссию добавляем сверху + quantity = base_quantity * (1 + commission_fee_percent) - # Переводим ставку из USDT в количество актива по текущей цене - quantity_in_asset = quantity / current_price - - # Учитываем комиссию за вход и выход (умножаем на 2) - quantity_with_fee = quantity * (1 + 2 * commission_fee_percent / 100) - - # Учитываем кредитное плечо - реальные собственные вложения меньше - effective_quantity = quantity_with_fee / leverage - - total += effective_quantity - - # Возвращаем бюджет в USDT - total_usdt = total * current_price - return total_usdt + total += quantity + return total async def handle_execution_message(message, msg): @@ -248,12 +242,12 @@ async def handle_execution_message(message, msg): await rq.set_last_series_info(tg_id, last_side="Sell") if trigger == "Автоматический" and closed_size > 0: - if pnl < 0: + if trading_mode == 'Switch': + side = data_main_stgs.get("last_side") + else: + side = "Buy" if trading_mode == "Long" else "Sell" - if trading_mode == 'Switch': - side = data_main_stgs.get("last_side") - else: - side = "Buy" if trading_mode == "Long" else "Sell" + if pnl < 0: current_martingale = await rq.get_martingale_step(tg_id) current_martingale_step = int(current_martingale) @@ -262,6 +256,7 @@ async def handle_execution_message(message, msg): float(martingale_factor) ** current_martingale_step ) await rq.update_martingale_step(tg_id, current_martingale) + await rq.update_starting_quantity(tg_id=tg_id, num=next_quantity) await message.answer( f"❗️ Сделка закрылась в минус, открываю новую сделку с увеличенной ставкой.\n" ) @@ -276,8 +271,11 @@ async def handle_execution_message(message, msg): elif pnl > 0: await rq.update_martingale_step(tg_id, 0) + num = data_main_stgs.get("base_quantity") + await rq.update_starting_quantity(tg_id=tg_id, num=num) await message.answer( - "❗️ Прибыль достигнута, шаг мартингейла сброшен." + "❗️ Прибыль достигнута, шаг мартингейла сброшен. " + "Возврат к начальной ставке." ) @@ -348,8 +346,6 @@ async def open_position( max_risk_percent = safe_float(data_risk_stgs.get("max_risk_deal")) loss_profit = safe_float(data_risk_stgs.get("price_loss")) commission_fee = data_risk_stgs.get("commission_fee") - starting_quantity = safe_float(data_main_stgs.get('starting_quantity')) - martingale_factor = safe_float(data_main_stgs.get('martingale_factor')) fee_info = client.get_fee_rates(category='linear', symbol=symbol) instruments_resp = client.get_instruments_info(category="linear", symbol=symbol) instrument = instruments_resp.get("result", {}).get("list", []) @@ -359,33 +355,19 @@ async def open_position( else: commission_fee_percent = 0.0 - total_budget = await calculate_total_budget( - starting_quantity=starting_quantity, - martingale_factor=martingale_factor, - max_steps=max_martingale_steps, - commission_fee_percent=commission_fee_percent, - leverage=leverage, - current_price=entry_price, - ) + if commission_fee_percent > 0: + # Добавляем к тейк-профиту процент комиссии + tp_multiplier = 1 + (loss_profit / 100) + commission_fee_percent + else: + tp_multiplier = 1 + (loss_profit / 100) - balance = await balance_g.get_balance(tg_id, message) - if safe_float(balance) < total_budget: - logger.error( - f"Недостаточно средств для серии из {max_martingale_steps} шагов с текущими параметрами. " - f"Требуемый бюджет: {total_budget:.2f} USDT, доступно: {balance} USDT." - ) - await message.answer( - f"Недостаточно средств для серии из {max_martingale_steps} шагов с текущими параметрами. " - f"Требуемый бюджет: {total_budget:.2f} USDT, доступно: {balance} USDT.", - reply_markup=inline_markup.back_to_main, - ) - return if order_type == "Limit" and limit_price: price_for_calc = limit_price else: price_for_calc = entry_price + balance = await balance_g.get_balance(tg_id, message) potential_loss = safe_float(quantity) * price_for_calc * (loss_profit / 100) adjusted_loss = potential_loss / leverage allowed_loss = safe_float(balance) * (max_risk_percent / 100) @@ -465,6 +447,8 @@ async def open_position( timeInForce="GTC", orderLinkId=f"deal_{symbol}_{int(time.time())}", ) + if response.get("retCode", -1) == 0: + return True if response.get("retCode", -1) != 0: logger.error(f"Ошибка открытия ордера: {response}") await message.answer( @@ -480,9 +464,11 @@ async def open_position( if liq_price > 0 and avg_price > 0: if side.lower() == "buy": - take_profit_price = avg_price + (avg_price - liq_price) + base_tp = avg_price + (avg_price - liq_price) + take_profit_price = base_tp * (1 + commission_fee_percent) else: - take_profit_price = avg_price - (liq_price - avg_price) + base_tp = avg_price - (liq_price - avg_price) + take_profit_price = base_tp * (1 - commission_fee_percent) take_profit_price = max(take_profit_price, 0) @@ -531,10 +517,10 @@ async def open_position( base_price = limit_price if side.lower() == "buy": - take_profit_price = base_price * (1 + loss_profit / 100) + take_profit_price = base_price * tp_multiplier stop_loss_price = base_price * (1 - loss_profit / 100) else: - take_profit_price = base_price * (1 - loss_profit / 100) + take_profit_price = base_price * (1 - (loss_profit / 100) - (commission_fee_percent)) stop_loss_price = base_price * (1 + loss_profit / 100) take_profit_price = max(take_profit_price, 0) diff --git a/app/services/Bybit/functions/functions.py b/app/services/Bybit/functions/functions.py index 599d021..d0d3e63 100644 --- a/app/services/Bybit/functions/functions.py +++ b/app/services/Bybit/functions/functions.py @@ -10,6 +10,7 @@ from app.services.Bybit.functions.Futures import (close_user_trade, set_take_pro get_active_positions_by_symbol, get_active_orders_by_symbol, get_active_positions, get_active_orders, cancel_all_tp_sl_orders, open_position, close_trade_after_delay, safe_float, + calculate_total_budget, get_bybit_client, ) from app.services.Bybit.functions.balance import get_balance import app.telegram.Keyboards.inline_keyboards as inline_markup @@ -17,6 +18,7 @@ import app.telegram.Keyboards.inline_keyboards as inline_markup import app.telegram.database.requests as rq from aiogram.types import Message, CallbackQuery from app.services.Bybit.functions.price_symbol import get_price +import app.services.Bybit.functions.balance as balance_g from app.states.States import (state_update_entry_type, state_update_symbol, state_limit_price, SetTP_SL_State, CloseTradeTimerState) @@ -196,11 +198,18 @@ async def start_trading_process(callback: CallbackQuery) -> None: tg_id = callback.from_user.id message = callback.message data_main_stgs = await rq.get_user_main_settings(tg_id) + data_risk_stgs = await rq.get_user_risk_management_settings(tg_id) + client = await get_bybit_client(tg_id) symbol = await rq.get_symbol(tg_id) margin_mode = data_main_stgs.get('margin_type', 'Isolated') trading_mode = data_main_stgs.get('trading_mode') starting_quantity = safe_float(data_main_stgs.get('starting_quantity')) switch_state = data_main_stgs.get("switch_state", "По направлению") + martingale_factor = safe_float(data_main_stgs.get('martingale_factor')) + max_martingale_steps = int(data_main_stgs.get("maximal_quantity", 0)) + commission_fee = data_risk_stgs.get("commission_fee") + fee_info = client.get_fee_rates(category='linear', symbol=symbol) + if trading_mode == 'Switch': if switch_state == "По направлению": @@ -221,7 +230,33 @@ async def start_trading_process(callback: CallbackQuery) -> None: reply_markup=inline_markup.back_to_main) return + if commission_fee == "Да": + commission_fee_percent = safe_float(fee_info['result']['list'][0]['takerFeeRate']) + else: + commission_fee_percent = 0.0 + + total_budget = await calculate_total_budget( + starting_quantity=starting_quantity, + martingale_factor=martingale_factor, + max_steps=max_martingale_steps, + commission_fee_percent=commission_fee_percent, + ) + + balance = await balance_g.get_balance(tg_id, message) + if safe_float(balance) < total_budget: + logger.error( + f"Недостаточно средств для серии из {max_martingale_steps} шагов с текущими параметрами. " + f"Требуемый бюджет: {total_budget:.2f} USDT, доступно: {balance} USDT." + ) + await message.answer( + f"Недостаточно средств для серии из {max_martingale_steps} шагов с текущими параметрами. " + f"Требуемый бюджет: {total_budget:.2f} USDT, доступно: {balance} USDT.", + reply_markup=inline_markup.back_to_main, + ) + return + await message.answer("Начинаю торговлю с использованием текущих настроек...") + await rq.update_trigger(tg_id=tg_id, trigger="Автоматический") timer_data = await rq.get_user_timer(tg_id) if isinstance(timer_data, dict): @@ -259,6 +294,7 @@ async def cancel_start_trading(callback: CallbackQuery): pass user_trade_tasks.pop(tg_id, None) await rq.update_user_timer(tg_id, minutes=0) + await rq.update_trigger(tg_id, "Ручной") await callback.message.answer("Запуск торговли отменён.", reply_markup=inline_markup.back_to_main) await callback.message.edit_reply_markup(reply_markup=None) else: @@ -505,9 +541,13 @@ async def stop_immediately(callback: CallbackQuery): Останавливает торговлю немедленно. """ tg_id = callback.from_user.id + symbol = await rq.get_symbol(tg_id) + await close_user_trade(tg_id, symbol) await rq.update_trigger(tg_id, "Ручной") - await callback.message.answer("Автоматическая торговля остановлена.", reply_markup=inline_markup.back_to_main) + await rq.update_martingale_step(tg_id, 1) + + await callback.message.answer("Торговля остановлена.", reply_markup=inline_markup.back_to_main) await callback.answer() @@ -543,8 +583,13 @@ async def process_stop_delay(message: Message, state: FSMContext): await message.answer(f"Торговля будет остановлена через {delay_minutes} минут.", reply_markup=inline_markup.back_to_main) await asyncio.sleep(delay_seconds) + + symbol = await rq.get_symbol(tg_id) + + await close_user_trade(tg_id, symbol) await rq.update_trigger(tg_id, "Ручной") - await message.answer("Автоматическая торговля остановлена.", reply_markup=inline_markup.back_to_main) + await rq.update_martingale_step(tg_id, 1) + await message.answer("Торговля остановлена.", reply_markup=inline_markup.back_to_main) await state.clear() @@ -559,4 +604,4 @@ async def cancel(callback: CallbackQuery, state: FSMContext) -> None: await callback.message.answer("Отменено!", reply_markup=inline_markup.back_to_main) await callback.answer() except Exception as e: - logger.error("Ошибка при обработке отмены: %s", e) \ No newline at end of file + logger.error("Ошибка при обработке отмены: %s", e) diff --git a/app/telegram/Keyboards/inline_keyboards.py b/app/telegram/Keyboards/inline_keyboards.py index 8efe82f..75c55e8 100644 --- a/app/telegram/Keyboards/inline_keyboards.py +++ b/app/telegram/Keyboards/inline_keyboards.py @@ -62,7 +62,7 @@ entry_order_type_markup = InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton(text="Market (текущая цена)", callback_data="entry_order_type:Market"), - InlineKeyboardButton(text="Limit (фиксированная цена)", callback_data="entry_order_type:Limit"), + InlineKeyboardButton(text="Limit (триггер цена)", callback_data="entry_order_type:Limit"), ], back_btn_to_main ] ) diff --git a/app/telegram/database/models.py b/app/telegram/database/models.py index d741428..d20ad8c 100644 --- a/app/telegram/database/models.py +++ b/app/telegram/database/models.py @@ -148,12 +148,14 @@ class User_Main_Settings(Base): switch_state = mapped_column(String(10), default='По направлению') size_leverage = mapped_column(Integer(), default=1) starting_quantity = mapped_column(Integer(), default=1) + base_quantity = mapped_column(Integer(), default=1) martingale_factor = mapped_column(Integer(), default=1) martingale_step = mapped_column(Integer(), default=1) maximal_quantity = mapped_column(Integer(), default=10) entry_order_type = mapped_column(String(10), default='Market') limit_order_price = mapped_column(Numeric(18, 15), nullable=True) last_side = mapped_column(String(10), default='Buy') + trading_start_stop = mapped_column(Integer(), default=0) class User_Risk_Management_Settings(Base): diff --git a/app/telegram/database/requests.py b/app/telegram/database/requests.py index 11e70e8..fb41694 100644 --- a/app/telegram/database/requests.py +++ b/app/telegram/database/requests.py @@ -320,6 +320,8 @@ async def get_user_main_settings(tg_id): 'limit_order_price': user.limit_order_price, 'martingale_step': user.martingale_step, 'last_side': user.last_side, + 'trading_start_stop': user.trading_start_stop, + 'base_quantity': user.base_quantity, } return data @@ -368,15 +370,23 @@ async def update_size_leverange(tg_id, num): async def update_starting_quantity(tg_id, num): - """Обновить размер левеража пользователя.""" + """Обновить размер начальной ставки пользователя.""" async with async_session() as session: await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(starting_quantity=num)) await session.commit() +async def update_base_quantity(tg_id, num): + """Обновить размер следующей ставки пользователя.""" + async with async_session() as session: + await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(base_quantity=num)) + + await session.commit() + + async def update_martingale_factor(tg_id, num): - """Обновить размер левеража пользователя.""" + """Обновить шаг мартингейла пользователя.""" async with async_session() as session: await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(martingale_factor=num)) @@ -384,7 +394,7 @@ async def update_martingale_factor(tg_id, num): async def update_maximal_quantity(tg_id, num): - """Обновить размер левеража пользователя.""" + """Обновить размер максимальной ставки пользователя.""" async with async_session() as session: await session.execute(update(UMS).where(UMS.tg_id == tg_id).values(maximal_quantity=num)) @@ -582,4 +592,4 @@ async def set_last_series_info(tg_id: int, last_side: str): last_side=last_side, ) session.add(new_entry) - await session.commit() \ No newline at end of file + await session.commit() diff --git a/app/telegram/functions/condition_settings/settings.py b/app/telegram/functions/condition_settings/settings.py index 9958269..0f3116d 100644 --- a/app/telegram/functions/condition_settings/settings.py +++ b/app/telegram/functions/condition_settings/settings.py @@ -23,42 +23,12 @@ async def reg_new_user_default_condition_settings(id): async def main_settings_message(id, message): - - tg_id = id - trigger = await rq.get_for_registration_trigger(tg_id) text = f""" Условия запуска - -- Режим торговли: {trigger} - Таймер: установить таймер / удалить таймер """ await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.condition_settings_markup) -async def trigger_message(id, message, state: FSMContext): - await state.set_state(condition_settings.trigger) - text = ''' -- Автоматический: торговля будет происходить в рамках серии ставок. -- Ручной: торговля будет происходить только в ручном режиме. -- Выберите тип триггера:''' - - await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trigger_markup) - - -@condition_settings_router.callback_query(F.data == "clb_trigger_manual") -async def trigger_manual_callback(callback: CallbackQuery, state: FSMContext): - await state.set_state(condition_settings.trigger) - await rq.update_trigger(tg_id=callback.from_user.id, trigger="Ручной") - await main_settings_message(callback.from_user.id, callback.message) - await callback.answer() - - -@condition_settings_router.callback_query(F.data == "clb_trigger_auto") -async def trigger_manual_callback(callback: CallbackQuery, state: FSMContext): - await state.set_state(condition_settings.trigger) - await rq.update_trigger(tg_id=callback.from_user.id, trigger="Автоматический") - await main_settings_message(callback.from_user.id, callback.message) - await callback.answer() - async def timer_message(id, message: Message, state: FSMContext): await state.set_state(condition_settings.timer) @@ -140,4 +110,4 @@ async def ai_analytics_message(message, state): Описание... ''' - await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup) \ No newline at end of file + await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.buttons_yes_no_markup) diff --git a/app/telegram/functions/main_settings/settings.py b/app/telegram/functions/main_settings/settings.py index 94ce115..a61def0 100644 --- a/app/telegram/functions/main_settings/settings.py +++ b/app/telegram/functions/main_settings/settings.py @@ -6,7 +6,6 @@ from pybit.unified_trading import HTTP import app.telegram.database.requests as rq from aiogram.types import Message, CallbackQuery -from app.services.Bybit.functions.price_symbol import get_price from app.services.Bybit.functions.Futures import safe_float, calculate_total_budget, get_bybit_client from app.states.States import update_main_settings from logger_helper.logger_helper import LOGGING_CONFIG @@ -40,9 +39,6 @@ async def main_settings_message(id, message): starting_quantity = safe_float((data_main_stgs or {}).get('starting_quantity')) martingale_factor = safe_float((data_main_stgs or {}).get('martingale_factor')) fee_info = client.get_fee_rates(category='linear', symbol=symbol) - leverage = safe_float((data_main_stgs or {}).get('size_leverage')) - price = await get_price(tg_id, symbol=symbol) - entry_price = safe_float(price) if commission_fee == "Да": commission_fee_percent = safe_float(fee_info['result']['list'][0]['takerFeeRate']) @@ -54,8 +50,6 @@ async def main_settings_message(id, message): martingale_factor=martingale_factor, max_steps=max_martingale_steps, commission_fee_percent=commission_fee_percent, - leverage=leverage, - current_price=entry_price, ) await message.answer(f"""Основные настройки @@ -267,26 +261,7 @@ async def state_margin_type(callback: CallbackQuery, state): callback_data = callback.data if callback_data in ['margin_type_isolated', 'margin_type_cross']: tg_id = callback.from_user.id - api_key = await rq.get_bybit_api_key(tg_id) - secret_key = await rq.get_bybit_secret_key(tg_id) data_settings = await rq.get_user_main_settings(tg_id) - symbol = await rq.get_symbol(tg_id) - client = HTTP(api_key=api_key, api_secret=secret_key) - try: - active_positions = client.get_positions(category='linear', settleCoin="USDT") - - positions = active_positions.get('result', {}).get('list', []) - except Exception as e: - logger.error("Ошибка при получении активных позиций: %s", e) - positions = [] - - for pos in positions: - size = pos.get('size') - if float(size) > 0: - await callback.answer( - "⚠️ Маржинальный режим нельзя менять при открытой позиции" - ) - return try: match callback.data: @@ -333,6 +308,7 @@ async def state_starting_quantity(message: Message, state): await message.answer(f"✅ Изменено: {data_settings['starting_quantity']} → {data['starting_quantity']}") await rq.update_starting_quantity(message.from_user.id, data['starting_quantity']) + await rq.update_base_quantity(tg_id=message.from_user.id, num=data['starting_quantity']) await main_settings_message(message.from_user.id, message) await state.clear() diff --git a/requirements.txt b/requirements.txt index beef837..e3ab834 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ aiohappyeyeballs==2.6.1 aiohttp==3.12.15 aiosignal==1.4.0 aiosqlite==0.21.0 +alembic==1.16.5 annotated-types==0.7.0 attrs==25.3.0 black==25.1.0 @@ -20,7 +21,9 @@ greenlet==3.2.4 idna==3.10 isort==6.0.1 magic-filter==1.0.12 +Mako==1.3.10 mando==0.7.1 +MarkupSafe==3.0.2 mccabe==0.7.0 multidict==6.6.4 mypy_extensions==1.1.0