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",