From 2ee8c9916f690c6606f52ac84160df8def269d4e Mon Sep 17 00:00:00 2001 From: algizn97 Date: Sat, 30 Aug 2025 16:29:56 +0500 Subject: [PATCH] Fixed --- app/services/Bybit/functions/Futures.py | 630 ++++++++++-------- app/services/Bybit/functions/bybit_ws.py | 14 +- app/services/Bybit/functions/functions.py | 65 +- app/services/Bybit/functions/min_qty.py | 2 +- app/services/Bybit/functions/price_symbol.py | 3 +- app/states/States.py | 11 + app/telegram/Keyboards/inline_keyboards.py | 3 +- app/telegram/database/models.py | 22 +- app/telegram/database/requests.py | 1 + .../functions/additional_settings/settings.py | 2 +- .../functions/condition_settings/settings.py | 4 +- app/telegram/functions/functions.py | 2 +- .../functions/main_settings/settings.py | 181 ++--- .../risk_management_settings/settings.py | 10 +- app/telegram/handlers/handlers.py | 22 +- logger_helper/logger_helper.py | 10 + 16 files changed, 523 insertions(+), 459 deletions(-) diff --git a/app/services/Bybit/functions/Futures.py b/app/services/Bybit/functions/Futures.py index 4a46099..f45d08b 100644 --- a/app/services/Bybit/functions/Futures.py +++ b/app/services/Bybit/functions/Futures.py @@ -17,6 +17,18 @@ logger = logging.getLogger("futures") processed_trade_ids = set() +async def get_bybit_client(tg_id): + """ + Асинхронно получает экземпляр клиента Bybit. + + :param tg_id: int - ID пользователя Telegram + :return: HTTP - экземпляр клиента Bybit + """ + api_key = await rq.get_bybit_api_key(tg_id) + secret_key = await rq.get_bybit_secret_key(tg_id) + return HTTP(api_key=api_key, api_secret=secret_key) + + def safe_float(val) -> float: """ Безопасное преобразование значения в float. @@ -37,20 +49,17 @@ def format_trade_details_position(data, commission_fee): """ msg = data.get('data', [{}])[0] - closed_size = float(msg.get('closedSize', 0)) + closed_size = safe_float(msg.get('closedSize', 0)) symbol = msg.get('symbol', 'N/A') - entry_price = float(msg.get('execPrice', 0)) - qty = float(msg.get('execQty', 0)) + entry_price = safe_float(msg.get('execPrice', 0)) + qty = safe_float(msg.get('execQty', 0)) order_type = msg.get('orderType', 'N/A') side = msg.get('side', '') - commission = float(msg.get('execFee', 0)) - pnl = float(msg.get('execPnl', 0)) + commission = safe_float(msg.get('execFee', 0)) + pnl = safe_float(msg.get('execPnl', 0)) if commission_fee == "Да": - if pnl >= 0: - pnl -= commission - else: - pnl -= commission + pnl -= commission movement = '' if side.lower() == 'buy': @@ -72,7 +81,7 @@ def format_trade_details_position(data, commission_fee): f"Комиссия за сделку: {commission:.6f}\n" f"Реализованная прибыль: {pnl:.6f} USDT" ) - else: + if order_type == 'Market': return ( f"Сделка открыта:\n" f"Торговая пара: {symbol}\n" @@ -82,6 +91,77 @@ def format_trade_details_position(data, commission_fee): f"Движение: {movement}\n" f"Комиссия за сделку: {commission:.6f}" ) + return None + + +def format_order_details_position(data): + """ + Форматирует информацию об ордере в виде строки. + """ + msg = data.get('data', [{}])[0] + price = safe_float(msg.get('price', 0)) + qty = safe_float(msg.get('qty', 0)) + cum_exec_qty = safe_float(msg.get('cumExecQty', 0)) + cum_exec_fee = safe_float(msg.get('cumExecFee', 0)) + take_profit = safe_float(msg.get('takeProfit', 0)) + stop_loss = safe_float(msg.get('stopLoss', 0)) + order_status = msg.get('orderStatus', 'N/A') + symbol = msg.get('symbol', 'N/A') + order_type = msg.get('orderType', 'N/A') + side = msg.get('side', '') + + movement = '' + if side.lower() == 'buy': + movement = 'Покупка' + elif side.lower() == 'sell': + movement = 'Продажа' + else: + movement = side + + if order_status.lower() == 'filled' and order_type.lower() == 'limit': + text = ( + f"Ордер исполнен:\n" + f"Торговая пара: {symbol}\n" + f"Цена исполнения: {price:.6f}\n" + f"Количество: {qty}\n" + f"Исполнено позиций: {cum_exec_qty}\n" + f"Тип ордера: {order_type}\n" + f"Движение: {movement}\n" + f"Тейк-профит: {take_profit:.6f}\n" + f"Стоп-лосс: {stop_loss:.6f}\n" + f"Комиссия за сделку: {cum_exec_fee:.6f}\n" + ) + logger.info(text) + return text + + elif order_status.lower() == 'new': + text = ( + f"Ордер создан:\n" + f"Торговая пара: {symbol}\n" + f"Цена: {price:.6f}\n" + f"Количество: {qty}\n" + f"Тип ордера: {order_type}\n" + f"Движение: {movement}\n" + f"Тейк-профит: {take_profit:.6f}\n" + f"Стоп-лосс: {stop_loss:.6f}\n" + ) + logger.info(text) + return text + + elif order_status.lower() == 'cancelled': + text = ( + f"Ордер отменен:\n" + f"Торговая пара: {symbol}\n" + f"Цена: {price:.6f}\n" + f"Количество: {qty}\n" + f"Тип ордера: {order_type}\n" + f"Движение: {movement}\n" + f"Тейк-профит: {take_profit:.6f}\n" + f"Стоп-лосс: {stop_loss:.6f}\n" + ) + logger.info(text) + return text + return None def parse_pnl_from_msg(msg) -> float: @@ -89,70 +169,79 @@ def parse_pnl_from_msg(msg) -> float: Извлекает реализованную прибыль/убыток из сообщения. """ try: - return float(msg.get('realisedPnl', 0)) + data = msg.get('data', [{}])[0] + return float(data.get('execPnl', 0)) except Exception as e: logger.error(f"Ошибка при извлечении реализованной прибыли: {e}") return 0.0 -async def handle_execution_message(message, msg: dict) -> None: +async def handle_execution_message(message, msg): """ Обработчик сообщений об исполнении сделки. Логирует событие и проверяет условия для мартингейла и TP. """ # logger.info(f"Исполнена сделка:\n{json.dumps(msg, indent=4, ensure_ascii=False)}") - - trade_id = msg.get('data', [{}])[0].get('orderId') - if trade_id in processed_trade_ids: - logger.info(f"Уже обработана сделка {trade_id}, дублирующее уведомление игнорируется") - return - - processed_trade_ids.add(trade_id) - - pnl = parse_pnl_from_msg(msg) tg_id = message.from_user.id - - data_main_stgs = await rq.get_user_main_settings(tg_id) + data = msg.get('data', [{}])[0] data_main_risk_stgs = await rq.get_user_risk_management_settings(tg_id) - take_profit_percent = safe_float(data_main_stgs.get('take_profit_percent', 2)) commission_fee = data_main_risk_stgs.get('commission_fee', "ДА") - symbol = await rq.get_symbol(tg_id) - api_key = await rq.get_bybit_api_key(tg_id) - api_secret = await rq.get_bybit_secret_key(tg_id) - client = HTTP(api_key=api_key, api_secret=api_secret) - positions_resp = client.get_positions(category='linear', symbol=symbol) - positions_list = positions_resp.get('result', {}).get('list', []) - position = positions_list[0] if positions_list else None + pnl = parse_pnl_from_msg(msg) + data_main_stgs = await rq.get_user_main_settings(tg_id) + symbol = data.get('symbol') + switch_mode = data_main_stgs.get('switch_mode', 'Включено') + trading_mode = data_main_stgs.get('trading_mode', 'Long') + trigger = await rq.get_for_registration_trigger(tg_id) + margin_mode = data_main_stgs.get('margin_type', 'Isolated') + starting_quantity = safe_float(data_main_stgs.get('starting_quantity')) - trade_info = format_trade_details_position(msg, commission_fee=commission_fee) - await message.answer(f"{trade_info}", reply_markup=inline_markup.back_to_main) + trade_info = format_trade_details_position(data=msg, commission_fee=commission_fee) - liquidation_threshold = -100 + if trade_info: + await message.answer(f"{trade_info}", reply_markup=inline_markup.back_to_main) - if pnl <= liquidation_threshold: - current_step = int(await rq.get_martingale_step(tg_id)) - current_step += 1 - await rq.update_martingale_step(tg_id, current_step) + side = None + if switch_mode == 'Включено': + switch_state = data_main_stgs.get('switch_state', 'Long') + side = 'Buy' if switch_state == 'Long' else 'Sell' + else: + if trading_mode == 'Long': + side = 'Buy' + elif trading_mode == 'Short': + side = 'Sell' + else: + side = 'Buy' - side = 'Buy' if position and position.get('side', '').lower() == 'long' else 'Sell' - margin_mode = data_main_stgs.get('margin_type', 'Isolated') - await open_position(tg_id, message, side=side, margin_mode=margin_mode) + if trigger == "Автоматический": + if pnl < 0: + martingale_factor = safe_float(data_main_stgs.get('martingale_factor')) + current_martingale = await rq.get_martingale_step(tg_id) + current_martingale_step = int(current_martingale) + current_martingale += 1 + next_quantity = float(starting_quantity) * (float(martingale_factor) ** current_martingale_step) + await rq.update_martingale_step(tg_id, current_martingale) + await open_position(tg_id, message, side=side, margin_mode=margin_mode, symbol=symbol, + quantity=next_quantity) - elif position: - entry_price = safe_float(position.get('avgPrice')) - side = position.get('side', '') - current_price = float(position.get('markPrice', 0)) + elif pnl > 0: + await rq.update_martingale_step(tg_id, 0) + await message.answer("❗️ Прибыль достигнута, шаг мартингейла сброшен. " + "Начинаем новую серию ставок") + await open_position(tg_id, message, side=side, margin_mode=margin_mode, symbol=symbol, + quantity=starting_quantity) - if side.lower() == 'long': - take_profit_trigger_price = entry_price * (1 + take_profit_percent / 100) - if current_price >= take_profit_trigger_price: - await close_user_trade(tg_id, symbol, message) - await rq.update_martingale_step(tg_id, 0) - elif side.lower() == 'short': - take_profit_trigger_price = entry_price * (1 - take_profit_percent / 100) - if current_price <= take_profit_trigger_price: - await close_user_trade(tg_id, symbol, message) - await rq.update_martingale_step(tg_id, 0) + +async def handle_order_message(message, msg: dict) -> None: + """ + Обработчик сообщений об исполнении ордера. + Логирует событие и проверяет условия для мартингейла и TP. + """ + # logger.info(f"Исполнен ордер:\n{json.dumps(msg, indent=4, ensure_ascii=False)}") + + trade_info = format_order_details_position(msg) + + if trade_info: + await message.answer(f"{trade_info}", reply_markup=inline_markup.back_to_main) async def error_max_step(message) -> None: @@ -173,131 +262,49 @@ async def error_max_risk(message) -> None: reply_markup=inline_markup.back_to_main) -async def open_position(tg_id, message, side: str, margin_mode: str, tpsl_mode='Full'): +async def open_position(tg_id, message, side: str, margin_mode: str, symbol, quantity, tpsl_mode='Full'): """ Открывает позицию на Bybit с учётом настроек пользователя, маржи, размера лота, платформы и риска. Возвращает True при успехе, False при ошибках открытия ордера, None при исключениях. """ - api_key = await rq.get_bybit_api_key(tg_id) - secret_key = await rq.get_bybit_secret_key(tg_id) - symbol = await rq.get_symbol(tg_id) - - data_main_stgs = await rq.get_user_main_settings(tg_id) - order_type = data_main_stgs.get('entry_order_type') - - limit_price = None - if order_type == 'Limit': - limit_price = await rq.get_limit_price(tg_id) - data_risk_stgs = await rq.get_user_risk_management_settings(tg_id) - - bybit_margin_mode = 'ISOLATED_MARGIN' if margin_mode == 'Isolated' else 'REGULAR_MARGIN' - - client = HTTP(api_key=api_key, api_secret=secret_key) try: - client.set_tp_sl_mode(symbol=symbol, category='linear', tpSlMode='Full') - except exceptions.InvalidRequestError as e: - if 'same tp sl mode' in str(e): - logger.info("Режим TP/SL уже установлен - пропускаем") - else: - raise + client = await get_bybit_client(tg_id) + data_main_stgs = await rq.get_user_main_settings(tg_id) + order_type = data_main_stgs.get('entry_order_type') + bybit_margin_mode = 'ISOLATED_MARGIN' if margin_mode == 'Isolated' else 'REGULAR_MARGIN' + + limit_price = None + if order_type == 'Limit': + limit_price = await rq.get_limit_price(tg_id) + data_risk_stgs = await rq.get_user_risk_management_settings(tg_id) - try: balance = await balance_g.get_balance(tg_id, message) - price = await price_symbol.get_price(tg_id) + price = await price_symbol.get_price(tg_id, symbol=symbol) + entry_price = safe_float(price) - client.set_margin_mode(setMarginMode=bybit_margin_mode) - - martingale_factor = safe_float(data_main_stgs.get('martingale_factor')) max_martingale_steps = int(data_main_stgs.get('maximal_quantity', 0)) - starting_quantity = safe_float(data_main_stgs.get('starting_quantity')) - + current_martingale = await rq.get_martingale_step(tg_id) max_risk_percent = safe_float(data_risk_stgs.get('max_risk_deal')) loss_profit = safe_float(data_risk_stgs.get('price_loss')) - take_profit = safe_float(data_risk_stgs.get('price_profit')) - positions_resp = client.get_positions(category='linear', symbol=symbol) - positions_list = positions_resp.get('result', {}).get('list', []) - if positions_list: - position = positions_list[0] - size = safe_float(position.get('size', 0)) - side_pos = position.get('side', '') - if size > 0 and side_pos: - entry_price = safe_float(position.get('avgPrice', price)) - else: - entry_price = price + if order_type == 'Limit' and limit_price: + price_for_calc = limit_price else: - entry_price = price + price_for_calc = entry_price - if order_type == 'Market': - base_price = entry_price - else: - base_price = limit_price - - if side.lower() == 'buy': - take_profit_price = base_price * (1 + take_profit / 100) - stop_loss_price = base_price * (1 - loss_profit / 100) - else: - take_profit_price = base_price * (1 - take_profit / 100) - stop_loss_price = base_price * (1 + loss_profit / 100) - - take_profit_price = max(take_profit_price, 0) - stop_loss_price = max(stop_loss_price, 0) - - current_martingale_step = 0 - next_quantity = starting_quantity - realised_pnl = 0.0 - - current_martingale = await rq.get_martingale_step(tg_id) - current_martingale_step = int(current_martingale) - - if positions_list: - if realised_pnl > 0: - current_martingale_step = 0 - next_quantity = starting_quantity - else: - current_martingale_step += 1 - if current_martingale_step > max_martingale_steps: - await error_max_step(message) - return - next_quantity = float(starting_quantity) * (float(martingale_factor) ** current_martingale_step) - else: - next_quantity = starting_quantity - current_martingale_step = 0 - - potential_loss = safe_float(next_quantity) * safe_float(price) * (loss_profit / 100) + potential_loss = safe_float(quantity) * price_for_calc * (loss_profit / 100) allowed_loss = safe_float(balance) * (max_risk_percent / 100) + if max_martingale_steps == current_martingale: + await error_max_step(message) + return + if potential_loss > allowed_loss: await error_max_risk(message) return - instruments_resp = client.get_instruments_info(category='linear', symbol=symbol) - - if instruments_resp.get('retCode') == 0: - instrument_info = instruments_resp.get('result', {}).get('list', []) - if instrument_info: - instrument = instrument_info[0] - min_order_qty = float(instrument.get('minOrderQty', 0)) - min_order_value_api = float(instrument.get('minOrderValue', 0)) - - if min_order_value_api == 0: - min_order_value_api = 5.0 - # Рассчитываем по формуле: - min_order_value_calc = min_order_qty * price if min_order_qty > 0 else 0 - # Минимальное значение из значений параметров на бирже - min_order_value = max(min_order_value_calc, min_order_value_api) - else: - min_order_value = 5.0 - - order_value = float(next_quantity) * float(price) - - if order_value < min_order_value: - await message.answer( - f"Сумма ордера слишком мала: {order_value:.2f} USDT. " - f"Минимум для торговли — {min_order_value} USDT. " - f"Пожалуйста, увеличьте количество позиций.", reply_markup=inline_markup.back_to_main) - return False + client.set_margin_mode(setMarginMode=bybit_margin_mode) leverage = int(data_main_stgs.get('size_leverage', 1)) try: @@ -313,53 +320,157 @@ async def open_position(tg_id, message, side: str, margin_mode: str, tpsl_mode=' else: raise e - if tpsl_mode == 'Full': - tp_order_type = 'Market' - sl_order_type = 'Market' - tp_limit_price = None - sl_limit_price = None - else: # Partial - tp_order_type = 'Limit' - sl_order_type = 'Limit' - tp_limit_price = take_profit_price - sl_limit_price = stop_loss_price + instruments_resp = client.get_instruments_info(category='linear', symbol=symbol) + if instruments_resp.get('retCode') == 0: + instrument_info = instruments_resp.get('result', {}).get('list', []) + if instrument_info: + instrument = instrument_info[0] + min_order_qty = float(instrument.get('minOrderQty', 0)) + min_order_value_api = float(instrument.get('minOrderValue', 0)) - response = client.place_order( - category='linear', - symbol=symbol, - side=side, - orderType=order_type, - qty=str(next_quantity), - price=str(limit_price) if order_type == 'Limit' and limit_price else None, - takeProfit=str(take_profit_price), - tpOrderType=tp_order_type, - tpLimitPrice=str(tp_limit_price) if tp_limit_price else None, - stopLoss=str(stop_loss_price), - slOrderType=sl_order_type, - slLimitPrice=str(sl_limit_price) if sl_limit_price else None, - tpslMode=tpsl_mode, - timeInForce='GTC', - orderLinkId=f"deal_{symbol}_{int(time.time())}" - ) + if min_order_value_api == 0: + min_order_value_api = 5.0 + min_order_value_calc = min_order_qty * price_for_calc if min_order_qty > 0 else 0 + min_order_value = max(min_order_value_calc, min_order_value_api) + else: + min_order_value = 5.0 - if response.get('retCode', -1) == 0: - await rq.update_martingale_step(tg_id, current_martingale_step) - return True - else: - logger.error(f"Ошибка открытия ордера: {response}") - await message.answer(f"Ошибка открытия ордера", reply_markup=inline_markup.back_to_main) + order_value = float(quantity) * price_for_calc + if order_value < min_order_value: + await message.answer( + f"Сумма ордера слишком мала: {order_value:.2f} USDT. " + f"Минимум для торговли — {min_order_value} USDT. " + f"Пожалуйста, увеличьте количество позиций.", reply_markup=inline_markup.back_to_main) return False + if bybit_margin_mode == 'ISOLATED_MARGIN': + # Открываем позицию + response = client.place_order( + category='linear', + symbol=symbol, + side=side, + orderType=order_type, + qty=str(quantity), + price=str(limit_price) if order_type == 'Limit' and limit_price else None, + timeInForce='GTC', + orderLinkId=f"deal_{symbol}_{int(time.time())}" + ) + if response.get('retCode', -1) != 0: + logger.error(f"Ошибка открытия ордера: {response}") + await message.answer(f"Ошибка открытия ордера", reply_markup=inline_markup.back_to_main) + return False + + # Получаем цену ликвидации + positions = client.get_positions(category='linear', symbol=symbol) + pos = positions.get('result', {}).get('list', [{}])[0] + avg_price = float(pos.get('avgPrice', 0)) + liq_price = safe_float(pos.get('liqPrice', 0)) + + if liq_price > 0 and avg_price > 0: + if side.lower() == 'buy': + take_profit_price = avg_price + (avg_price - liq_price) + else: + take_profit_price = avg_price - (liq_price - avg_price) + + take_profit_price = max(take_profit_price, 0) + + try: + try: + client.set_tp_sl_mode(symbol=symbol, category='linear', tpSlMode='Full') + except exceptions.InvalidRequestError as e: + if 'same tp sl mode' in str(e): + logger.info("Режим TP/SL уже установлен - пропускаем") + else: + raise + resp = client.set_trading_stop( + category='linear', + symbol=symbol, + takeProfit=str(round(take_profit_price, 5)), + tpTriggerBy='LastPrice', + slTriggerBy='LastPrice', + positionIdx=0, + reduceOnly=False, + tpslMode=tpsl_mode + ) + except Exception as e: + logger.error(f"Ошибка установки TP/SL: {e}") + await message.answer('Ошибка при установке Take Profit и Stop Loss.', + reply_markup=inline_markup.back_to_main) + return False + else: + logger.warning("Не удалось получить цену ликвидации для позиции") + + else: # REGULAR_MARGIN + try: + client.set_tp_sl_mode(symbol=symbol, category='linear', tpSlMode='Full') + except exceptions.InvalidRequestError as e: + if 'same tp sl mode' in str(e): + logger.info("Режим TP/SL уже установлен - пропускаем") + else: + raise + + if order_type == 'Market': + base_price = entry_price + else: + base_price = limit_price + + if side.lower() == 'buy': + take_profit_price = base_price * (1 + loss_profit / 100) + stop_loss_price = base_price * (1 - loss_profit / 100) + else: + take_profit_price = base_price * (1 - loss_profit / 100) + stop_loss_price = base_price * (1 + loss_profit / 100) + + take_profit_price = max(take_profit_price, 0) + stop_loss_price = max(stop_loss_price, 0) + + if tpsl_mode == 'Full': + tp_order_type = 'Market' + sl_order_type = 'Market' + tp_limit_price = None + sl_limit_price = None + else: # Partial + tp_order_type = 'Limit' + sl_order_type = 'Limit' + tp_limit_price = take_profit_price + sl_limit_price = stop_loss_price + + response = client.place_order( + category='linear', + symbol=symbol, + side=side, + orderType=order_type, + qty=str(quantity), + price=str(limit_price) if order_type == 'Limit' and limit_price else None, + takeProfit=str(take_profit_price), + tpOrderType=tp_order_type, + tpLimitPrice=str(tp_limit_price) if tp_limit_price else None, + stopLoss=str(stop_loss_price), + slOrderType=sl_order_type, + slLimitPrice=str(sl_limit_price) if sl_limit_price else None, + tpslMode=tpsl_mode, + timeInForce='GTC', + orderLinkId=f"deal_{symbol}_{int(time.time())}" + ) + + if response.get('retCode', -1) == 0: + return True + else: + logger.error(f"Ошибка открытия ордера: {response}") + await message.answer(f"Ошибка открытия ордера", reply_markup=inline_markup.back_to_main) + return False + + return None except exceptions.InvalidRequestError as e: - logger.error(f"InvalidRequestError: {e}") + logger.error(f"InvalidRequestError: {e}", exc_info=True) await message.answer('Недостаточно средств для размещения нового ордера с заданным количеством и плечом.', reply_markup=inline_markup.back_to_main) except Exception as e: - logger.error(f"Ошибка при совершении сделки: {e}") + logger.error(f"Ошибка при совершении сделки: {e}", exc_info=True) await message.answer('Возникла ошибка при попытке открыть позицию.', reply_markup=inline_markup.back_to_main) -async def trading_cycle(tg_id, message): +async def trading_cycle(tg_id, message, side, margin_mode, symbol, starting_quantity): """ Цикл торговой логики с учётом таймера пользователя. """ @@ -376,13 +487,10 @@ async def trading_cycle(tg_id, message): if timer_sec > 0: await asyncio.sleep(timer_sec) - data_main_stgs = await rq.get_user_main_settings(tg_id) - side = 'Buy' if data_main_stgs.get('trading_mode', '') == 'Long' else 'Sell' - margin_mode = data_main_stgs.get('margin_type', 'Isolated') - - await open_position(tg_id, message, side=side, margin_mode=margin_mode) + await open_position(tg_id, message, side=side, margin_mode=margin_mode, symbol=symbol, + quantity=starting_quantity) except asyncio.CancelledError: - logger.info(f"Торговый цикл для пользователя {tg_id} был отменён.") + logger.info(f"Торговый цикл для пользователя {tg_id} был отменён.", exc_info=True) async def set_take_profit_stop_loss(tg_id: int, message, take_profit_price: float, stop_loss_price: float, @@ -390,19 +498,8 @@ async def set_take_profit_stop_loss(tg_id: int, message, take_profit_price: floa """ Устанавливает уровни Take Profit и Stop Loss для открытой позиции. """ - api_key = await rq.get_bybit_api_key(tg_id) - secret_key = await rq.get_bybit_secret_key(tg_id) symbol = await rq.get_symbol(tg_id) - data_main_stgs = await rq.get_user_main_settings(tg_id) - order_type = data_main_stgs.get('entry_order_type') - starting_quantity = safe_float(data_main_stgs.get('starting_quantity')) - - limit_price = None - if order_type == 'Limit': - limit_price = await rq.get_limit_price(tg_id) - - data_risk_stgs = await rq.get_user_risk_management_settings(tg_id) trading_mode = data_main_stgs.get('trading_mode') side = None @@ -415,7 +512,7 @@ async def set_take_profit_stop_loss(tg_id: int, message, take_profit_price: floa await message.answer("Не удалось определить сторону сделки.") return - client = HTTP(api_key=api_key, api_secret=secret_key) + client = await get_bybit_client(tg_id) await cancel_all_tp_sl_orders(tg_id, symbol) try: @@ -427,52 +524,22 @@ async def set_take_profit_stop_loss(tg_id: int, message, take_profit_price: floa else: raise - positions_resp = client.get_positions(category='linear', symbol=symbol) - positions = positions_resp.get('result', {}).get('list', []) + resp = client.set_trading_stop( + category='linear', + symbol=symbol, + takeProfit=str(round(take_profit_price, 5)), + stopLoss=str(round(stop_loss_price, 5)), + tpTriggerBy='LastPrice', + slTriggerBy='LastPrice', + positionIdx=0, + reduceOnly=False, + tpslMode=tpsl_mode + ) - if not positions or abs(float(positions[0].get('size', 0))) == 0: - params = dict( - category='linear', - symbol=symbol, - side=side, - orderType=order_type, - qty=str(starting_quantity), - timeInForce='GTC', - orderLinkId=f"deal_{symbol}_{int(time.time())}", - takeProfit=str(take_profit_price), - stopLoss=str(stop_loss_price), - tpOrderType='Limit' if tpsl_mode == 'Partial' else 'Market', - slOrderType='Limit' if tpsl_mode == 'Partial' else 'Market', - tpslMode=tpsl_mode - ) - if order_type == 'Limit' and limit_price is not None: - params['price'] = str(limit_price) - - if tpsl_mode == 'Partial': - params['tpLimitPrice'] = str(take_profit_price) - params['slLimitPrice'] = str(stop_loss_price) - - response = client.place_order(**params) - if response.get('retCode') != 0: - await message.answer(f"Ошибка создания ордера с TP/SL: {response.get('retMsg')}", - reply_markup=inline_markup.back_to_main) - return - - else: - resp = client.set_trading_stop( - category='linear', - symbol=symbol, - takeProfit=str(round(take_profit_price, 5)), - stopLoss=str(round(stop_loss_price, 5)), - tpTriggerBy='LastPrice', - slTriggerBy='LastPrice', - reduceOnly=False - ) - - if resp.get('retCode') != 0: - await message.answer(f"Ошибка обновления TP/SL: {resp.get('retMsg')}", - reply_markup=inline_markup.back_to_main) - return + if resp.get('retCode') != 0: + await message.answer(f"Ошибка обновления TP/SL: {resp.get('retMsg')}", + reply_markup=inline_markup.back_to_main) + return await message.answer( f"ТП и СЛ успешно установлены:\nТейк-профит: {take_profit_price:.5f}\nСтоп-лосс: {stop_loss_price:.5f}", @@ -486,9 +553,7 @@ async def cancel_all_tp_sl_orders(tg_id, symbol): """ Отменяет лимитные ордера для указанного символа. """ - api_key = await rq.get_bybit_api_key(tg_id) - secret_key = await rq.get_bybit_secret_key(tg_id) - client = HTTP(api_key=api_key, api_secret=secret_key) + client = await get_bybit_client(tg_id) last_response = None try: orders_resp = client.get_open_orders(category='linear', symbol=symbol) @@ -512,12 +577,8 @@ async def get_active_positions(tg_id, message): """ Показывает активные позиции пользователя. """ - api_key = await rq.get_bybit_api_key(tg_id) - secret_key = await rq.get_bybit_secret_key(tg_id) - client = HTTP(api_key=api_key, api_secret=secret_key) - + client = await get_bybit_client(tg_id) active_positions = client.get_positions(category='linear', settleCoin='USDT') - positions = active_positions.get('result', {}).get('list', []) active_symbols = [pos.get('symbol') for pos in positions if float(pos.get('size', 0)) > 0] @@ -533,12 +594,8 @@ async def get_active_positions_by_symbol(tg_id, symbol, message): """ Показывает активные позиции пользователя по символу. """ - api_key = await rq.get_bybit_api_key(tg_id) - secret_key = await rq.get_bybit_secret_key(tg_id) - client = HTTP(api_key=api_key, api_secret=secret_key) - + client = await get_bybit_client(tg_id) active_positions = client.get_positions(category='linear', symbol=symbol) - positions = active_positions.get('result', {}).get('list', []) pos = positions[0] if positions else None @@ -558,14 +615,12 @@ async def get_active_positions_by_symbol(tg_id, symbol, message): await message.answer(text, reply_markup=inline_markup.create_close_deal_markup(symbol)) + async def get_active_orders(tg_id, message): """ Показывает активные лимитные ордера пользователя. """ - api_key = await rq.get_bybit_api_key(tg_id) - secret_key = await rq.get_bybit_secret_key(tg_id) - client = HTTP(api_key=api_key, api_secret=secret_key) - + client = await get_bybit_client(tg_id) response = client.get_open_orders(category='linear', settleCoin='USDT', orderType='Limit') orders = response.get('result', {}).get('list', []) limit_orders = [order for order in orders if order.get('orderType') == 'Limit'] @@ -583,10 +638,7 @@ async def get_active_orders_by_symbol(tg_id, symbol, message): """ Показывает активные лимитные ордера пользователя по символу. """ - api_key = await rq.get_bybit_api_key(tg_id) - secret_key = await rq.get_bybit_secret_key(tg_id) - client = HTTP(api_key=api_key, api_secret=secret_key) - + client = await get_bybit_client(tg_id) active_orders = client.get_open_orders(category='linear', symbol=symbol) limit_orders = [ order for order in active_orders.get('result', {}).get('list', []) @@ -608,26 +660,19 @@ async def get_active_orders_by_symbol(tg_id, symbol, message): f"Количество: {order.get('qty')}\n" f"Тейк-профит: {order.get('takeProfit')}\n" f"Стоп-лосс: {order.get('stopLoss')}\n" - f"Кредитное плечо: {order.get('leverage')}\n" ) texts.append(text) await message.answer("\n\n".join(texts), reply_markup=inline_markup.create_close_limit_markup(symbol)) -async def close_user_trade(tg_id: int, symbol: str, message): +async def close_user_trade(tg_id: int, symbol: str): """ Закрывает открытые позиции пользователя по символу рыночным ордером. Возвращает True при успехе, False при ошибках. """ try: - api_key = await rq.get_bybit_api_key(tg_id) - secret_key = await rq.get_bybit_secret_key(tg_id) - data_risk_stgs = await rq.get_user_risk_management_settings(tg_id) - - include_fee = data_risk_stgs.get('commission_fee', 'Нет') == 'Да' - client = HTTP(api_key=api_key, api_secret=secret_key) - + client = await get_bybit_client(tg_id) positions_resp = client.get_positions(category="linear", symbol=symbol) if positions_resp.get('retCode') != 0: @@ -643,6 +688,7 @@ async def close_user_trade(tg_id: int, symbol: str, message): return False close_side = "Sell" if side == "Buy" else "Buy" + place_resp = client.place_order( category="linear", symbol=symbol, @@ -654,14 +700,11 @@ async def close_user_trade(tg_id: int, symbol: str, message): ) if place_resp.get('retCode') == 0: - await message.answer(f"Сделка {symbol} успешно закрыта.", reply_markup=inline_markup.back_to_main) return True else: - await message.answer(f"Ошибка закрытия сделки {symbol}.", reply_markup=inline_markup.back_to_main) return False except Exception as e: logger.error(f"Ошибка закрытия сделки {symbol} для пользователя {tg_id}: {e}", exc_info=True) - await message.answer("Произошла ошибка при закрытии сделки.", reply_markup=inline_markup.back_to_main) return False @@ -671,12 +714,15 @@ async def close_trade_after_delay(tg_id: int, message, symbol: str, delay_sec: i """ try: await asyncio.sleep(delay_sec) - result = await close_user_trade(tg_id, symbol, message) + result = await close_user_trade(tg_id, symbol) if result: await message.answer(f"Сделка {symbol} успешно закрыта по таймеру.", reply_markup=inline_markup.back_to_main) + logger.info(f"Сделка {symbol} успешно закрыта по таймеру.") else: await message.answer(f"Не удалось закрыть сделку {symbol} по таймеру.", reply_markup=inline_markup.back_to_main) + logger.error(f"Не удалось закрыть сделку {symbol} по таймеру.") except asyncio.CancelledError: await message.answer(f"Закрытие сделки {symbol} по таймеру отменено.", reply_markup=inline_markup.back_to_main) + logger.info(f"Закрытие сделки {symbol} по таймеру отменено.") diff --git a/app/services/Bybit/functions/bybit_ws.py b/app/services/Bybit/functions/bybit_ws.py index 4d0d31e..961faa8 100644 --- a/app/services/Bybit/functions/bybit_ws.py +++ b/app/services/Bybit/functions/bybit_ws.py @@ -9,6 +9,7 @@ logging.config.dictConfig(LOGGING_CONFIG) logger = logging.getLogger("bybit_ws") event_loop = None # Сюда нужно будет установить event loop из основного приложения +active_ws_tasks = {} def get_or_create_event_loop() -> asyncio.AbstractEventLoop: """ @@ -31,11 +32,14 @@ async def run_ws_for_user(tg_id, message) -> None: """ Запускает WebSocket Bybit для пользователя с указанным tg_id. """ - - api_key = await rq.get_bybit_api_key(tg_id) - api_secret = await rq.get_bybit_secret_key(tg_id) - - await start_execution_ws(api_key, api_secret, message) + if tg_id not in active_ws_tasks or active_ws_tasks[tg_id].done(): + api_key = await rq.get_bybit_api_key(tg_id) + api_secret = await rq.get_bybit_secret_key(tg_id) + # Запускаем WebSocket как асинхронную задачу + active_ws_tasks[tg_id] = asyncio.create_task( + start_execution_ws(api_key, api_secret, message) + ) + logger.info(f"WebSocket для пользователя {tg_id} запущен.") def on_order_callback(message, msg): diff --git a/app/services/Bybit/functions/functions.py b/app/services/Bybit/functions/functions.py index 54b407e..3187854 100644 --- a/app/services/Bybit/functions/functions.py +++ b/app/services/Bybit/functions/functions.py @@ -2,17 +2,17 @@ import logging.config from aiogram import F, Router +from app.telegram.functions.main_settings.settings import main_settings_message from logger_helper.logger_helper import LOGGING_CONFIG from app.services.Bybit.functions.Futures import (close_user_trade, set_take_profit_stop_loss, \ get_active_positions_by_symbol, get_active_orders_by_symbol, get_active_positions, get_active_orders, cancel_all_tp_sl_orders, - trading_cycle, open_position, close_trade_after_delay, + trading_cycle, open_position, close_trade_after_delay, safe_float, ) from app.services.Bybit.functions.balance import get_balance import app.telegram.Keyboards.inline_keyboards as inline_markup -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 @@ -37,10 +37,10 @@ async def clb_start_bybit_trade_message(callback: CallbackQuery) -> None: """ user_id = callback.from_user.id balance = await get_balance(user_id, callback.message) - price = await get_price(user_id) if balance: symbol = await rq.get_symbol(user_id) + price = await get_price(user_id, symbol=symbol) text = ( f"💎 Торговля на Bybit\n\n" @@ -61,10 +61,10 @@ async def start_bybit_trade_message(message: Message) -> None: вместе с инструкциями по началу торговли. """ balance = await get_balance(message.from_user.id, message) - price = await get_price(message.from_user.id) if balance: symbol = await rq.get_symbol(message.from_user.id) + price = await get_price(message.from_user.id, symbol=symbol) text = ( f"💎 Торговля на Bybit\n\n" @@ -124,7 +124,6 @@ async def update_entry_type_message(callback: CallbackQuery, state: FSMContext) await callback.answer() - @router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith('entry_order_type:')) async def entry_order_type_callback(callback: CallbackQuery, state: FSMContext) -> None: """ @@ -192,43 +191,11 @@ async def start_trading_process(callback: CallbackQuery) -> None: message = callback.message data_main_stgs = await rq.get_user_main_settings(tg_id) - api_key = await rq.get_bybit_api_key(tg_id) - secret_key = await rq.get_bybit_secret_key(tg_id) symbol = await rq.get_symbol(tg_id) margin_mode = data_main_stgs.get('margin_type', 'Isolated') trading_mode = data_main_stgs.get('trading_mode') switch_mode = data_main_stgs.get('switch_mode_enabled') - - if not api_key or not secret_key: - await message.answer("❗️ У вас не настроены API ключи для Bybit.") - await callback.answer() - return - - if trading_mode not in ['Long', 'Short', 'Smart']: - await message.answer(f"❗️ Некорректный торговый режим: {trading_mode}") - await callback.answer() - return - - if margin_mode not in ['Isolated', 'Cross']: - margin_mode = 'Isolated' - - client = HTTP(api_key=api_key, api_secret=secret_key) - - try: - positions_resp = client.get_positions(category='linear', symbol=symbol) - positions = positions_resp.get('result', {}).get('list', []) - except Exception as e: - logger.error(f"Ошибка при получении позиций: {e}") - positions = [] - - for pos in positions: - size = pos.get('size') - existing_margin_mode = pos.get('margin_mode') - if size and float(size) > 0 and existing_margin_mode and existing_margin_mode != margin_mode: - await callback.answer( - f"⚠️ Маржинальный режим нельзя менять при открытой позиции " - f"(текущий режим: {existing_margin_mode})", show_alert=True) - return + starting_quantity = safe_float(data_main_stgs.get('starting_quantity')) side = None if switch_mode == 'Включено': @@ -247,7 +214,6 @@ async def start_trading_process(callback: CallbackQuery) -> None: await message.answer("Начинаю торговлю с использованием текущих настроек...") - timer_data = await rq.get_user_timer(tg_id) if isinstance(timer_data, dict): timer_minute = timer_data.get('timer_minutes', 0) @@ -255,11 +221,12 @@ async def start_trading_process(callback: CallbackQuery) -> None: timer_minute = timer_data or 0 if timer_minute > 0: - await trading_cycle(tg_id, message) + await trading_cycle(tg_id, message, side=side, margin_mode=margin_mode, symbol=symbol, + starting_quantity=starting_quantity) await message.answer(f"Торговля начнётся через {timer_minute} мин.") await rq.update_user_timer(tg_id, minutes=0) else: - await open_position(tg_id, message, side, margin_mode) + await open_position(tg_id, message, side, margin_mode, symbol=symbol, quantity=starting_quantity) await callback.answer() @@ -290,6 +257,7 @@ async def show_my_trades_callback(callback: CallbackQuery): logger.error(f"Произошла ошибка при выборе сделки: {e}") await callback.message.answer("Произошла ошибка при выборе сделки", reply_markup=inline_markup.back_to_main) + @router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("show_deal_")) async def show_deal_callback(callback_query: CallbackQuery) -> None: """ @@ -303,7 +271,8 @@ async def show_deal_callback(callback_query: CallbackQuery) -> None: await get_active_positions_by_symbol(tg_id, symbol, message=callback_query.message) except Exception as e: logger.error(f"Произошла ошибка при выборе сделки: {e}") - await callback_query.message.answer("Произошла ошибка при выборе сделки", reply_markup=inline_markup.back_to_main) + await callback_query.message.answer("Произошла ошибка при выборе сделки", + reply_markup=inline_markup.back_to_main) @router_functions_bybit_trade.callback_query(F.data == "clb_open_orders") @@ -333,7 +302,8 @@ async def show_limit_callback(callback_query: CallbackQuery) -> None: await get_active_orders_by_symbol(tg_id, symbol, message=callback_query.message) except Exception as e: logger.error(f"Произошла ошибка при выборе сделки: {e}") - await callback_query.message.answer("Произошла ошибка при выборе сделки", reply_markup=inline_markup.back_to_main) + await callback_query.message.answer("Произошла ошибка при выборе сделки", + reply_markup=inline_markup.back_to_main) @router_functions_bybit_trade.callback_query(F.data == "clb_set_tp_sl") @@ -407,7 +377,7 @@ async def close_trade_callback(callback: CallbackQuery) -> None: symbol = callback.data.split(':')[1] tg_id = callback.from_user.id - result = await close_user_trade(tg_id, symbol, message=callback.message) + result = await close_user_trade(tg_id, symbol) if result: logger.info(f"Сделка {symbol} успешно закрыта.") @@ -480,6 +450,7 @@ async def reset_martingale(callback: CallbackQuery) -> None: tg_id = callback.from_user.id await rq.update_martingale_step(tg_id, 0) await callback.answer("Сброс шагов выполнен.") + await main_settings_message(tg_id, callback.message) @router_functions_bybit_trade.callback_query(F.data == "clb_stop_trading") @@ -492,6 +463,7 @@ async def confirm_stop_trading(callback: CallbackQuery): ) await callback.answer() + @router_functions_bybit_trade.callback_query(F.data == "stop_immediately") async def stop_immediately(callback: CallbackQuery): """ @@ -503,6 +475,7 @@ async def stop_immediately(callback: CallbackQuery): await callback.message.answer("Торговля остановлена.", reply_markup=inline_markup.back_to_main) await callback.answer() + @router_functions_bybit_trade.callback_query(F.data == "stop_with_timer") async def stop_with_timer_start(callback: CallbackQuery, state: FSMContext): """ @@ -510,9 +483,11 @@ async def stop_with_timer_start(callback: CallbackQuery, state: FSMContext): """ await state.set_state(CloseTradeTimerState.waiting_for_delay) - await callback.message.answer("Введите задержку в минутах перед остановкой торговли:", reply_markup=inline_markup.cancel) + await callback.message.answer("Введите задержку в минутах перед остановкой торговли:", + reply_markup=inline_markup.cancel) await callback.answer() + @router_functions_bybit_trade.message(CloseTradeTimerState.waiting_for_delay) async def process_stop_delay(message: Message, state: FSMContext): """ diff --git a/app/services/Bybit/functions/min_qty.py b/app/services/Bybit/functions/min_qty.py index 7cfb453..51faa53 100644 --- a/app/services/Bybit/functions/min_qty.py +++ b/app/services/Bybit/functions/min_qty.py @@ -28,7 +28,7 @@ async def get_min_qty(tg_id: int) -> float: client = HTTP(api_key=api_key, api_secret=secret_key) - price = await get_price(tg_id) + price = await get_price(tg_id, symbol=symbol) response = client.get_instruments_info(symbol=symbol, category='linear') diff --git a/app/services/Bybit/functions/price_symbol.py b/app/services/Bybit/functions/price_symbol.py index 3421228..d86737f 100644 --- a/app/services/Bybit/functions/price_symbol.py +++ b/app/services/Bybit/functions/price_symbol.py @@ -8,7 +8,7 @@ logging.config.dictConfig(LOGGING_CONFIG) logger = logging.getLogger("price_symbol") -async def get_price(tg_id: int) -> float: +async def get_price(tg_id: int, symbol: str) -> float: """ Асинхронно получает текущую цену символа пользователя на Bybit. @@ -17,7 +17,6 @@ async def get_price(tg_id: int) -> float: """ api_key = await rq.get_bybit_api_key(tg_id) secret_key = await rq.get_bybit_secret_key(tg_id) - symbol = await rq.get_symbol(tg_id) client = HTTP( api_key=api_key, diff --git a/app/states/States.py b/app/states/States.py index 86cc80f..d18e769 100644 --- a/app/states/States.py +++ b/app/states/States.py @@ -55,3 +55,14 @@ class condition_settings(StatesGroup): volume = State() integration = State() use_tv_signal = State() + + +class update_main_settings(StatesGroup): + """FSM состояние для обновления основных настройок.""" + trading_mode = State() + size_leverage = State() + margin_type = State() + martingale_factor = State() + starting_quantity = State() + maximal_quantity = State() + switch_mode_enabled = State() \ No newline at end of file diff --git a/app/telegram/Keyboards/inline_keyboards.py b/app/telegram/Keyboards/inline_keyboards.py index ae1cb2c..f484292 100644 --- a/app/telegram/Keyboards/inline_keyboards.py +++ b/app/telegram/Keyboards/inline_keyboards.py @@ -25,6 +25,7 @@ special_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ InlineKeyboardButton(text="Дополнительные параметры", callback_data='clb_change_additional_settings')], [InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api_message')], + back_btn_to_main ]) connect_bybit_api_markup = InlineKeyboardMarkup(inline_keyboard=[ @@ -93,7 +94,7 @@ risk_management_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ ]) condition_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text='Триггер', callback_data='clb_change_trigger'), + [InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_mode'), InlineKeyboardButton(text='Таймер', callback_data='clb_change_timer')], [InlineKeyboardButton(text='Фильтр волатильности', callback_data='clb_change_filter_volatility'), diff --git a/app/telegram/database/models.py b/app/telegram/database/models.py index fdb6f6e..a7e30f8 100644 --- a/app/telegram/database/models.py +++ b/app/telegram/database/models.py @@ -1,6 +1,6 @@ from datetime import datetime import logging.config -from sqlalchemy.sql.sqltypes import DateTime +from sqlalchemy.sql.sqltypes import DateTime, Numeric from sqlalchemy import BigInteger, Boolean, Integer, String, ForeignKey from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column @@ -120,17 +120,16 @@ class Margin_type(Base): class Trigger(Base): """ - Справочник видов триггеров для сделок. + Справочник триггеров для сделок. Атрибуты: - id (int): Первичный ключ. - trigger (str): Название триггера (например, 'Ручной', 'Автоматический'). + id (int): Первичный ключ.. """ __tablename__ = 'triggers' id: Mapped[int] = mapped_column(primary_key=True) - trigger = mapped_column(String(15), unique=True) + trigger_price = mapped_column(Integer(), default=0) class User_Main_Settings(Base): @@ -163,10 +162,10 @@ class User_Main_Settings(Base): size_leverage = mapped_column(Integer(), default=1) starting_quantity = mapped_column(Integer(), default=1) martingale_factor = mapped_column(Integer(), default=1) - martingale_step = mapped_column(Integer(), default=1) + martingale_step = mapped_column(Integer(), default=0) maximal_quantity = mapped_column(Integer(), default=10) entry_order_type = mapped_column(String(10), default='Market') - limit_order_price = mapped_column(String(20), nullable=True) + limit_order_price = mapped_column(Numeric(18, 15), nullable=True) class User_Risk_Management_Settings(Base): @@ -297,7 +296,7 @@ async def async_main(): await conn.run_sync(Base.metadata.create_all) # Заполнение таблиц - modes = ['Long', 'Short', 'Switch', 'Smart'] + modes = ['Long', 'Short', 'Smart'] for mode in modes: result = await conn.execute(select(Trading_Mode).where(Trading_Mode.mode == mode)) if not result.first(): @@ -310,10 +309,3 @@ async def async_main(): if not result.first(): logger.info("Заполение таблицы типов маржи") await conn.execute(Margin_type.__table__.insert().values(type=type)) - - triggers = ['Ручной', 'Автоматический'] - for trigger in triggers: - result = await conn.execute(select(Trigger).where(Trigger.trigger == trigger)) - if not result.first(): - logger.info("Заполение таблицы триггеров") - await conn.execute(Trigger.__table__.insert().values(trigger=trigger)) diff --git a/app/telegram/database/requests.py b/app/telegram/database/requests.py index d0b108a..ad7951d 100644 --- a/app/telegram/database/requests.py +++ b/app/telegram/database/requests.py @@ -1,4 +1,5 @@ import logging.config + from logger_helper.logger_helper import LOGGING_CONFIG from datetime import datetime, timedelta from typing import Any diff --git a/app/telegram/functions/additional_settings/settings.py b/app/telegram/functions/additional_settings/settings.py index 77b6c14..5090d0b 100644 --- a/app/telegram/functions/additional_settings/settings.py +++ b/app/telegram/functions/additional_settings/settings.py @@ -7,7 +7,7 @@ async def reg_new_user_default_additional_settings(id, message): await rq.set_new_user_default_additional_settings(tg_id) -async def main_settings_message(id, message, state): +async def main_settings_message(id, message): text = '''Дополнительные параметры - Сохранить как шаблон стратегии: да / нет diff --git a/app/telegram/functions/condition_settings/settings.py b/app/telegram/functions/condition_settings/settings.py index 32932eb..a33d8c9 100644 --- a/app/telegram/functions/condition_settings/settings.py +++ b/app/telegram/functions/condition_settings/settings.py @@ -28,7 +28,7 @@ async def main_settings_message(id, message): trigger = await rq.get_for_registration_trigger(tg_id) text = f""" Условия запуска -- Триггер: {trigger} +- Режим торговли: {trigger} - Таймер: установить таймер / остановить таймер - Фильтр волатильности / объёма: включить/отключить - Интеграции и внешние сигналы: @@ -53,7 +53,6 @@ async def trigger_message(id, message, state: FSMContext): 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 callback.message.answer("Триггер установлен в ручной режим.") await main_settings_message(callback.from_user.id, callback.message) await callback.answer() @@ -62,7 +61,6 @@ async def trigger_manual_callback(callback: CallbackQuery, state: FSMContext): 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 callback.message.answer("Триггер установлен в автоматический режим.") await main_settings_message(callback.from_user.id, callback.message) await callback.answer() diff --git a/app/telegram/functions/functions.py b/app/telegram/functions/functions.py index 19ab77b..d1b7b9b 100644 --- a/app/telegram/functions/functions.py +++ b/app/telegram/functions/functions.py @@ -10,7 +10,7 @@ async def start_message(message): username = message.from_user.first_name else: username = f'{message.from_user.first_name} {message.from_user.last_name}' - await message.answer(f""" Привет {username}! 👋""", parse_mode='html') + await message.answer(f""" Привет {username}! 👋""", parse_mode='html', reply_markup=reply_markup.base_buttons_markup) await message.answer("Добро пожаловать в чат-робот для автоматизации трейдинга — вашего надежного помощника для анализа рынка и принятия взвешенных решений.", parse_mode='html', reply_markup=inline_markup.start_markup) diff --git a/app/telegram/functions/main_settings/settings.py b/app/telegram/functions/main_settings/settings.py index 90f1050..65a08e3 100644 --- a/app/telegram/functions/main_settings/settings.py +++ b/app/telegram/functions/main_settings/settings.py @@ -1,24 +1,18 @@ -from aiogram import Router, F - +from aiogram import Router +import logging.config import app.telegram.Keyboards.inline_keyboards as inline_markup -import app.telegram.Keyboards.reply_keyboards as reply_markup +from pybit.unified_trading import HTTP import app.telegram.database.requests as rq from aiogram.types import Message, CallbackQuery +from app.states.States import update_main_settings +from logger_helper.logger_helper import LOGGING_CONFIG -# FSM - Механизм состояния -from aiogram.fsm.state import State, StatesGroup +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("main_settings") router_main_settings = Router() -class update_main_settings(StatesGroup): - trading_mode = State() - size_leverage = State() - margin_type = State() - martingale_factor = State() - starting_quantity = State() - maximal_quantity = State() - switch_mode_enabled = State() async def reg_new_user_default_main_settings(id, message): tg_id = id @@ -27,12 +21,12 @@ async def reg_new_user_default_main_settings(id, message): margin_type = await rq.get_for_registration_margin_type() await rq.set_new_user_default_main_settings(tg_id, trading_mode, margin_type) - -async def main_settings_message(id, message, state): - data = await rq.get_user_main_settings(id) - await message.answer(f"""Основные настройки +async def main_settings_message(id, message): + data = await rq.get_user_main_settings(id) + + await message.answer(f"""Основные настройки - Режим торговли: {data['trading_mode']} - Режим свитч: {data['switch_mode_enabled']} @@ -45,6 +39,7 @@ async def main_settings_message(id, message, state): - Максимальное количество ставок в серии: {data['maximal_quantity']} """, parse_mode='html', reply_markup=inline_markup.main_settings_markup) + async def trading_mode_message(message, state): await state.set_state(update_main_settings.trading_mode) @@ -59,36 +54,37 @@ async def trading_mode_message(message, state): Выберите ниже для изменений: """, parse_mode='html', reply_markup=inline_markup.trading_mode_markup) + @router_main_settings.callback_query(update_main_settings.trading_mode) async def state_trading_mode(callback: CallbackQuery, state): - await callback.answer() + await callback.answer() - id = callback.from_user.id - data_settings = await rq.get_user_main_settings(id) + id = callback.from_user.id + data_settings = await rq.get_user_main_settings(id) - try: - match callback.data: - case 'trade_mode_long': + try: + match callback.data: + case 'trade_mode_long': await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Long") await rq.update_trade_mode_user(id, 'Long') - await main_settings_message(id, callback.message, state) + await main_settings_message(id, callback.message) await state.clear() - case 'trade_mode_short': + case 'trade_mode_short': await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Short") await rq.update_trade_mode_user(id, 'Short') - await main_settings_message(id, callback.message, state) + await main_settings_message(id, callback.message) await state.clear() - case 'trade_mode_smart': + case 'trade_mode_smart': await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Smart") await rq.update_trade_mode_user(id, 'Smart') - await main_settings_message(id, callback.message, state) + await main_settings_message(id, callback.message) - await state.clear() - except Exception as e: - print(f"error: {e}") + await state.clear() + except Exception as e: + logger.error(e) async def switch_mode_enabled_message(message, state): @@ -97,9 +93,8 @@ async def switch_mode_enabled_message(message, state): await message.edit_text( """Свитч — динамическое переключение между торговыми режимами для максимизации эффективности. - Выберите ниже для изменений:""", parse_mode='html', reply_markup=inline_markup.buttons_on_off_markup_for_switch) - - + Выберите ниже для изменений:""", parse_mode='html', + reply_markup=inline_markup.buttons_on_off_markup_for_switch) @router_main_settings.callback_query(lambda c: c.data in ["clb_on_switch", "clb_off_switch"]) @@ -109,12 +104,12 @@ async def state_switch_mode_enabled(callback: CallbackQuery, state): val = "Включить" if callback.data == "clb_on_switch" else "Выключить" if val == "Включить": await rq.update_switch_mode_enabled(tg_id, "Включено") - await callback.message.answer(f"Включено") - await main_settings_message(tg_id, callback.message, state) + await callback.answer(f"Включено") + await main_settings_message(tg_id, callback.message) else: await rq.update_switch_mode_enabled(tg_id, "Выключено") - await callback.message.answer(f"Выключено") - await main_settings_message(tg_id, callback.message, state) + await callback.answer(f"Выключено") + await main_settings_message(tg_id, callback.message) await state.clear() @@ -132,24 +127,24 @@ async def state_switch_mode_enabled(callback: CallbackQuery, state): if val == "Long": await rq.update_switch_state(tg_id, "Long") await callback.message.answer(f"Состояние свитча: {val}") - await main_settings_message(tg_id, callback.message, state) + await main_settings_message(tg_id, callback.message) else: await rq.update_switch_state(tg_id, "Short") await callback.message.answer(f"Состояние свитча: {val}") - await main_settings_message(tg_id, callback.message, state) + await main_settings_message(tg_id, callback.message) await state.clear() - -async def size_leverage_message (message, state): +async def size_leverage_message(message, state): await state.set_state(update_main_settings.size_leverage) - await message.edit_text("Введите размер кредитного плеча (от 1 до 100): ", parse_mode='html', reply_markup=inline_markup.back_btn_list_settings_markup) + await message.edit_text("Введите размер кредитного плеча (от 1 до 100): ", parse_mode='html', + reply_markup=inline_markup.back_btn_list_settings_markup) @router_main_settings.message(update_main_settings.size_leverage) async def state_size_leverage(message: Message, state): - await state.update_data(size_leverage = message.text) + await state.update_data(size_leverage=message.text) data = await state.get_data() data_settings = await rq.get_user_main_settings(message.from_user.id) @@ -158,22 +153,26 @@ async def state_size_leverage(message: Message, state): await message.answer(f"✅ Изменено: {data_settings['size_leverage']} → {data['size_leverage']}") await rq.update_size_leverange(message.from_user.id, data['size_leverage']) - await main_settings_message(message.from_user.id, message, state) + await main_settings_message(message.from_user.id, message) await state.clear() else: - await message.answer(f'⛔️ Ошибка: ваше значение ({data['size_leverage']}) или выше лимита (100) или вы вводите неверные символы') + await message.answer( + f'⛔️ Ошибка: ваше значение ({data['size_leverage']}) или выше лимита (100) или вы вводите неверные символы') + + await main_settings_message(message.from_user.id, message) - await main_settings_message(message.from_user.id, message, state) async def martingale_factor_message(message, state): await state.set_state(update_main_settings.martingale_factor) - await message.edit_text("Введите коэффициент Мартингейла:", parse_mode='html', reply_markup=inline_markup.back_btn_list_settings_markup) - + await message.edit_text("Введите коэффициент Мартингейла:", parse_mode='html', + reply_markup=inline_markup.back_btn_list_settings_markup) + + @router_main_settings.message(update_main_settings.martingale_factor) async def state_martingale_factor(message: Message, state): - await state.update_data(martingale_factor = message.text) + await state.update_data(martingale_factor=message.text) data = await state.get_data() data_settings = await rq.get_user_main_settings(message.from_user.id) @@ -182,14 +181,16 @@ async def state_martingale_factor(message: Message, state): await message.answer(f"✅ Изменено: {data_settings['martingale_factor']} → {data['martingale_factor']}") await rq.update_martingale_factor(message.from_user.id, data['martingale_factor']) - await main_settings_message(message.from_user.id, message, state) + await main_settings_message(message.from_user.id, message) await state.clear() else: - await message.answer(f'⛔️ Ошибка: ваше значение ({data['martingale_factor']}) или выше лимита (100) или вы вводите неверные символы') + await message.answer( + f'⛔️ Ошибка: ваше значение ({data['martingale_factor']}) или выше лимита (100) или вы вводите неверные символы') + + await main_settings_message(message.from_user.id, message) + - await main_settings_message(message.from_user.id, message, state) - async def margin_type_message(message, state): await state.set_state(update_main_settings.margin_type) @@ -209,40 +210,60 @@ async def margin_type_message(message, state): Выберите ниже для изменений: """, parse_mode='html', reply_markup=inline_markup.margin_type_markup) + @router_main_settings.callback_query(update_main_settings.margin_type) async def state_margin_type(callback: CallbackQuery, state): - await callback.answer() + 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) + client = HTTP(api_key=api_key, api_secret=secret_key) + try: + active_positions = client.get_positions(category='linear', settleCoin='USDT') - id = callback.from_user.id - data_settings = await rq.get_user_main_settings(id) + positions = active_positions.get('result', {}).get('list', []) + except Exception as e: + logger.error(f"error: {e}") + positions = [] - try: - match callback.data: - case 'margin_type_isolated': + for pos in positions: + size = pos.get('size') + if float(size) > 0: + await callback.answer( + "⚠️ Маржинальный режим нельзя менять при открытой позиции", + show_alert=True + ) + return + try: + match callback.data: + case 'margin_type_isolated': await callback.message.answer(f"✅ Изменено: {data_settings['margin_type']} → Isolated") - await rq.update_margin_type(id, 'Isolated') - await main_settings_message(id, callback.message, state) + await rq.update_margin_type(tg_id, 'Isolated') + await main_settings_message(tg_id, callback.message) await state.clear() - case 'margin_type_cross': + case 'margin_type_cross': await callback.message.answer(f"✅ Изменено: {data_settings['margin_type']} → Cross") - await rq.update_margin_type(id, 'Cross') - await main_settings_message(id, callback.message, state) + await rq.update_margin_type(tg_id, 'Cross') + await main_settings_message(tg_id, callback.message) await state.clear() - except Exception as e: - print(f"error: {e}") + except Exception as e: + logger.error(f"error: {e}") -async def starting_quantity_message (message, state): + +async def starting_quantity_message(message, state): await state.set_state(update_main_settings.starting_quantity) - await message.edit_text("Введите начальную ставку:", parse_mode='html', reply_markup=inline_markup.back_btn_list_settings_markup) + await message.edit_text("Введите начальную ставку:", parse_mode='html', + reply_markup=inline_markup.back_btn_list_settings_markup) + @router_main_settings.message(update_main_settings.starting_quantity) async def state_starting_quantity(message: Message, state): - await state.update_data(starting_quantity = message.text) + await state.update_data(starting_quantity=message.text) data = await state.get_data() data_settings = await rq.get_user_main_settings(message.from_user.id) @@ -251,22 +272,25 @@ 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 main_settings_message(message.from_user.id, message, state) + await main_settings_message(message.from_user.id, message) await state.clear() else: await message.answer(f'⛔️ Ошибка: вы вводите неверные символы') - await main_settings_message(message.from_user.id, message, state) + await main_settings_message(message.from_user.id, message) + async def maximum_quantity_message(message, state): await state.set_state(update_main_settings.maximal_quantity) - await message.edit_text("Введите максимальное количество серии ставок:", parse_mode='html', reply_markup=inline_markup.back_btn_list_settings_markup) + await message.edit_text("Введите максимальное количество серии ставок:", parse_mode='html', + reply_markup=inline_markup.back_btn_list_settings_markup) + @router_main_settings.message(update_main_settings.maximal_quantity) async def state_maximal_quantity(message: Message, state): - await state.update_data(maximal_quantity = message.text) + await state.update_data(maximal_quantity=message.text) data = await state.get_data() data_settings = await rq.get_user_main_settings(message.from_user.id) @@ -275,10 +299,11 @@ async def state_maximal_quantity(message: Message, state): await message.answer(f"✅ Изменено: {data_settings['maximal_quantity']} → {data['maximal_quantity']}") await rq.update_maximal_quantity(message.from_user.id, data['maximal_quantity']) - await main_settings_message(message.from_user.id, message, state) + await main_settings_message(message.from_user.id, message) await state.clear() else: - await message.answer(f'⛔️ Ошибка: ваше значение ({data['maximal_quantity']}) или выше лимита (100) или вы вводите неверные символы') - - await main_settings_message(message.from_user.id, message, state) \ No newline at end of file + await message.answer( + f'⛔️ Ошибка: ваше значение ({data['maximal_quantity']}) или выше лимита (100) или вы вводите неверные символы') + + await main_settings_message(message.from_user.id, message) diff --git a/app/telegram/functions/risk_management_settings/settings.py b/app/telegram/functions/risk_management_settings/settings.py index f600403..5d0b32d 100644 --- a/app/telegram/functions/risk_management_settings/settings.py +++ b/app/telegram/functions/risk_management_settings/settings.py @@ -1,11 +1,16 @@ from aiogram import Router import app.telegram.Keyboards.inline_keyboards as inline_markup - +import logging.config import app.telegram.database.requests as rq from aiogram.types import Message, CallbackQuery from app.states.States import update_risk_management_settings +from logger_helper.logger_helper import LOGGING_CONFIG + +logging.config.dictConfig(LOGGING_CONFIG) +logger = logging.getLogger("risk_management_settings") + router_risk_management_settings = Router() @@ -80,7 +85,8 @@ async def state_price_loss(message: Message, state): # Пробуем перевести price_profit в число, если это возможно try: current_price_profit_num = int(current_price_profit) - except Exception: + except Exception as e: + logger.error(e) current_price_profit_num = 0 # Флаг, если price_profit изначально равен 0 или совпадает со старым стоп-лоссом diff --git a/app/telegram/handlers/handlers.py b/app/telegram/handlers/handlers.py index 3152ae5..aad0658 100644 --- a/app/telegram/handlers/handlers.py +++ b/app/telegram/handlers/handlers.py @@ -1,5 +1,5 @@ import logging.config -import asyncio + from aiogram import F, Router from aiogram.filters import CommandStart from aiogram.types import Message, CallbackQuery @@ -50,8 +50,7 @@ async def profile_message(message: Message) -> None: tg_id = message.from_user.id balance = await get_balance(message.from_user.id, message) if user and balance: - asyncio.create_task(run_ws_for_user(tg_id, message)) - logger.info(f"Получение event loop") + await run_ws_for_user(tg_id, message) await func.profile_message(message.from_user.username, message) @@ -112,15 +111,14 @@ async def clb_back_to_settings_msg(callback: CallbackQuery) -> None: @router.callback_query(F.data == "clb_change_main_settings") -async def clb_change_main_settings_message(callback: CallbackQuery, state: FSMContext) -> None: +async def clb_change_main_settings_message(callback: CallbackQuery) -> None: """ Открыть меню изменения главных настроек. Args: callback (CallbackQuery): полученный колбэк. - state (FSMContext): текущее состояние FSM. """ - await func_main_settings.main_settings_message(callback.from_user.id, callback.message, state) + await func_main_settings.main_settings_message(callback.from_user.id, callback.message) await callback.answer() @@ -139,13 +137,12 @@ async def clb_change_risk_management_message(callback: CallbackQuery) -> None: @router.callback_query(F.data == "clb_change_condition_settings") -async def clb_change_condition_message(callback: CallbackQuery, state: FSMContext) -> None: +async def clb_change_condition_message(callback: CallbackQuery) -> None: """ Открыть меню изменения настроек условий. Args: callback (CallbackQuery): полученный колбэк. - state (FSMContext): текущее состояние FSM. """ await func_condition_settings.main_settings_message(callback.from_user.id, callback.message) @@ -153,15 +150,14 @@ async def clb_change_condition_message(callback: CallbackQuery, state: FSMContex @router.callback_query(F.data == "clb_change_additional_settings") -async def clb_change_additional_message(callback: CallbackQuery, state: FSMContext) -> None: +async def clb_change_additional_message(callback: CallbackQuery) -> None: """ Открыть меню изменения дополнительных настроек. Args: callback (CallbackQuery): полученный колбэк. - state (FSMContext): текущее состояние FSM. """ - await func_additional_settings.main_settings_message(callback.from_user.id, callback.message, state) + await func_additional_settings.main_settings_message(callback.from_user.id, callback.message) await callback.answer() @@ -240,7 +236,7 @@ async def clb_risk_management_settings_msg(callback: CallbackQuery, state: FSMCo logger.error(f"Error callback in risk_management match-case: {e}") -list_condition_settings = ['clb_change_trigger', +list_condition_settings = ['clb_change_mode', 'clb_change_timer', 'clb_change_filter_volatility', 'clb_change_external_cues', @@ -263,7 +259,7 @@ async def clb_condition_settings_msg(callback: CallbackQuery, state: FSMContext) try: match callback.data: - case 'clb_change_trigger': + case 'clb_change_mode': await func_condition_settings.trigger_message(callback.from_user.id, callback.message, state) case 'clb_change_timer': await func_condition_settings.timer_message(callback.from_user.id, callback.message, state) diff --git a/logger_helper/logger_helper.py b/logger_helper/logger_helper.py index 374f49e..f28566e 100644 --- a/logger_helper/logger_helper.py +++ b/logger_helper/logger_helper.py @@ -85,6 +85,16 @@ LOGGING_CONFIG = { "level": "DEBUG", "propagate": False, }, + "main_settings": { + "handlers": ["console", "timed_rotating_file"], + "level": "DEBUG", + "propagate": False, + }, + "risk_management_settings": { + "handlers": ["console", "timed_rotating_file"], + "level": "DEBUG", + "propagate": False, + }, "models": { "handlers": ["console", "timed_rotating_file"], "level": "DEBUG",