develop #4

Merged
Arsen Mirzaev Tatyano-Muradovich merged 10 commits from Alex/stcs:develop into stable 2025-09-12 09:28:09 +07:00
12 changed files with 678 additions and 439 deletions
Showing only changes of commit cf581dc485 - Show all commits

2
.gitignore vendored
View File

@@ -9,3 +9,5 @@ venv/
.venv/ .venv/
.idea .idea
/.idea /.idea
/myenv
myenv

View File

@@ -1,5 +1,7 @@
from aiogram import F, Router from aiogram import F, Router
import logging.config import logging.config
from app.services.Bybit.functions.functions import start_bybit_trade_message
from logger_helper.logger_helper import LOGGING_CONFIG from logger_helper.logger_helper import LOGGING_CONFIG
import app.telegram.Keyboards.inline_keyboards as inline_markup import app.telegram.Keyboards.inline_keyboards as inline_markup
import app.telegram.Keyboards.reply_keyboards as reply_markup import app.telegram.Keyboards.reply_keyboards as reply_markup
@@ -82,11 +84,11 @@ async def add_secret_key(message: Message, state: FSMContext) -> None:
data = await state.get_data() data = await state.get_data()
await rq.update_api_key(message.from_user.id, data['api_key']) await rq.upsert_api_keys(message.from_user.id, data['api_key'], data['secret_key'])
await rq.update_secret_key(message.from_user.id, data['secret_key'])
await rq.set_new_user_symbol(message.from_user.id) await rq.set_new_user_symbol(message.from_user.id)
await state.clear() await state.clear()
await message.answer('Данные добавлены, нажмите на профиль и начните торговлю!', await message.answer('Данные добавлены.',
reply_markup=reply_markup.base_buttons_markup) reply_markup=reply_markup.base_buttons_markup)
await start_bybit_trade_message(message)

View File

@@ -1,15 +1,14 @@
import asyncio import asyncio
import json import logging.config
import time import time
import logging.config
from pybit import exceptions
from pybit.unified_trading import HTTP
from logger_helper.logger_helper import LOGGING_CONFIG
import app.services.Bybit.functions.price_symbol as price_symbol
import app.services.Bybit.functions.balance as balance_g import app.services.Bybit.functions.balance as balance_g
import app.services.Bybit.functions.price_symbol as price_symbol
import app.telegram.database.requests as rq import app.telegram.database.requests as rq
import app.telegram.Keyboards.inline_keyboards as inline_markup import app.telegram.Keyboards.inline_keyboards as inline_markup
from logger_helper.logger_helper import LOGGING_CONFIG
from pybit import exceptions
from pybit.unified_trading import HTTP
logging.config.dictConfig(LOGGING_CONFIG) logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("futures") logger = logging.getLogger("futures")
@@ -35,11 +34,11 @@ def safe_float(val) -> float:
Возвращает 0.0, если значение None, пустое или некорректное. Возвращает 0.0, если значение None, пустое или некорректное.
""" """
try: try:
if val is None or val == '': if val is None or val == "":
return 0.0 return 0.0
return float(val) return float(val)
except (ValueError, TypeError): except (ValueError, TypeError):
logger.error("Некорректное значение для преобразования в float") logger.error("Некорректное значение для преобразования в float", exc_info=True)
return 0.0 return 0.0
@@ -47,25 +46,25 @@ def format_trade_details_position(data, commission_fee):
""" """
Форматирует информацию о сделке в виде строки. Форматирует информацию о сделке в виде строки.
""" """
msg = data.get('data', [{}])[0] msg = data.get("data", [{}])[0]
closed_size = safe_float(msg.get('closedSize', 0)) closed_size = safe_float(msg.get("closedSize", 0))
symbol = msg.get('symbol', 'N/A') symbol = msg.get("symbol", "N/A")
entry_price = safe_float(msg.get('execPrice', 0)) entry_price = safe_float(msg.get("execPrice", 0))
qty = safe_float(msg.get('execQty', 0)) qty = safe_float(msg.get("execQty", 0))
order_type = msg.get('orderType', 'N/A') order_type = msg.get("orderType", "N/A")
side = msg.get('side', '') side = msg.get("side", "")
commission = safe_float(msg.get('execFee', 0)) commission = safe_float(msg.get("execFee", 0))
pnl = safe_float(msg.get('execPnl', 0)) pnl = safe_float(msg.get("execPnl", 0))
if commission_fee == "Да": if commission_fee == "Да":
pnl -= commission pnl -= commission
movement = '' movement = ""
if side.lower() == 'buy': if side.lower() == "buy":
movement = 'Покупка' movement = "Покупка"
elif side.lower() == 'sell': elif side.lower() == "sell":
movement = 'Продажа' movement = "Продажа"
else: else:
movement = side movement = side
@@ -81,7 +80,7 @@ def format_trade_details_position(data, commission_fee):
f"Комиссия за сделку: {commission:.6f}\n" f"Комиссия за сделку: {commission:.6f}\n"
f"Реализованная прибыль: {pnl:.6f} USDT" f"Реализованная прибыль: {pnl:.6f} USDT"
) )
if order_type == 'Market': if order_type == "Market":
return ( return (
f"Сделка открыта:\n" f"Сделка открыта:\n"
f"Торговая пара: {symbol}\n" f"Торговая пара: {symbol}\n"
@@ -98,27 +97,27 @@ def format_order_details_position(data):
""" """
Форматирует информацию об ордере в виде строки. Форматирует информацию об ордере в виде строки.
""" """
msg = data.get('data', [{}])[0] msg = data.get("data", [{}])[0]
price = safe_float(msg.get('price', 0)) price = safe_float(msg.get("price", 0))
qty = safe_float(msg.get('qty', 0)) qty = safe_float(msg.get("qty", 0))
cum_exec_qty = safe_float(msg.get('cumExecQty', 0)) cum_exec_qty = safe_float(msg.get("cumExecQty", 0))
cum_exec_fee = safe_float(msg.get('cumExecFee', 0)) cum_exec_fee = safe_float(msg.get("cumExecFee", 0))
take_profit = safe_float(msg.get('takeProfit', 0)) take_profit = safe_float(msg.get("takeProfit", 0))
stop_loss = safe_float(msg.get('stopLoss', 0)) stop_loss = safe_float(msg.get("stopLoss", 0))
order_status = msg.get('orderStatus', 'N/A') order_status = msg.get("orderStatus", "N/A")
symbol = msg.get('symbol', 'N/A') symbol = msg.get("symbol", "N/A")
order_type = msg.get('orderType', 'N/A') order_type = msg.get("orderType", "N/A")
side = msg.get('side', '') side = msg.get("side", "")
movement = '' movement = ""
if side.lower() == 'buy': if side.lower() == "buy":
movement = 'Покупка' movement = "Покупка"
elif side.lower() == 'sell': elif side.lower() == "sell":
movement = 'Продажа' movement = "Продажа"
else: else:
movement = side movement = side
if order_status.lower() == 'filled' and order_type.lower() == 'limit': if order_status.lower() == "filled" and order_type.lower() == "limit":
text = ( text = (
f"Ордер исполнен:\n" f"Ордер исполнен:\n"
f"Торговая пара: {symbol}\n" f"Торговая пара: {symbol}\n"
@@ -131,10 +130,9 @@ def format_order_details_position(data):
f"Стоп-лосс: {stop_loss:.6f}\n" f"Стоп-лосс: {stop_loss:.6f}\n"
f"Комиссия за сделку: {cum_exec_fee:.6f}\n" f"Комиссия за сделку: {cum_exec_fee:.6f}\n"
) )
logger.info(text)
return text return text
elif order_status.lower() == 'new': elif order_status.lower() == "new":
text = ( text = (
f"Ордер создан:\n" f"Ордер создан:\n"
f"Торговая пара: {symbol}\n" f"Торговая пара: {symbol}\n"
@@ -145,10 +143,9 @@ def format_order_details_position(data):
f"Тейк-профит: {take_profit:.6f}\n" f"Тейк-профит: {take_profit:.6f}\n"
f"Стоп-лосс: {stop_loss:.6f}\n" f"Стоп-лосс: {stop_loss:.6f}\n"
) )
logger.info(text)
return text return text
elif order_status.lower() == 'cancelled': elif order_status.lower() == "cancelled":
text = ( text = (
f"Ордер отменен:\n" f"Ордер отменен:\n"
f"Торговая пара: {symbol}\n" f"Торговая пара: {symbol}\n"
@@ -159,7 +156,6 @@ def format_order_details_position(data):
f"Тейк-профит: {take_profit:.6f}\n" f"Тейк-профит: {take_profit:.6f}\n"
f"Стоп-лосс: {stop_loss:.6f}\n" f"Стоп-лосс: {stop_loss:.6f}\n"
) )
logger.info(text)
return text return text
return None return None
@@ -169,66 +165,117 @@ def parse_pnl_from_msg(msg) -> float:
Извлекает реализованную прибыль/убыток из сообщения. Извлекает реализованную прибыль/убыток из сообщения.
""" """
try: try:
data = msg.get('data', [{}])[0] data = msg.get("data", [{}])[0]
return float(data.get('execPnl', 0)) return float(data.get("execPnl", 0))
except Exception as e: except Exception as e:
logger.error(f"Ошибка при извлечении реализованной прибыли: {e}") logger.error("Ошибка при извлечении реализованной прибыли: %s", e)
return 0.0 return 0.0
async def calculate_total_budget(starting_quantity, martingale_factor, max_steps, commission_fee_percent, leverage, current_price):
"""
Вычисляет общий бюджет серии ставок с учётом цены пары, комиссии и кредитного плеча.
Параметры:
- starting_quantity_usdt: стартовый размер ставки в долларах (USD)
- martingale_factor: множитель увеличения ставки при каждом проигрыше
- max_steps: максимальное количество шагов удвоения ставки
- commission_fee_percent: процент комиссии на одну операцию (открытие или закрытие)
- leverage: кредитное плечо
- current_price: текущая цена актива (например BTCUSDT)
Возвращает:
- общий бюджет в долларах, который необходимо иметь на счету
"""
total = 0
for step in range(max_steps):
quantity = starting_quantity * (martingale_factor ** step) # размер ставки на текущем шаге в USDT
# Переводим ставку из USDT в количество актива по текущей цене
quantity_in_asset = quantity / current_price
# Учитываем комиссию за вход и выход (умножаем на 2)
quantity_with_fee = quantity * (1 + 2 * commission_fee_percent / 100)
# Учитываем кредитное плечо - реальные собственные вложения меньше
effective_quantity = quantity_with_fee / leverage
total += effective_quantity
# Возвращаем бюджет в USDT
total_usdt = total * current_price
return total_usdt
async def handle_execution_message(message, msg): async def handle_execution_message(message, msg):
""" """
Обработчик сообщений об исполнении сделки. Обработчик сообщений об исполнении сделки.
Логирует событие и проверяет условия для мартингейла и TP. Логирует событие и проверяет условия для мартингейла и TP.
""" """
# logger.info(f"Исполнена сделка:\n{json.dumps(msg, indent=4, ensure_ascii=False)}")
tg_id = message.from_user.id tg_id = message.from_user.id
data = msg.get('data', [{}])[0] data = msg.get("data", [{}])[0]
data_main_risk_stgs = await rq.get_user_risk_management_settings(tg_id) data_main_risk_stgs = await rq.get_user_risk_management_settings(tg_id)
commission_fee = data_main_risk_stgs.get('commission_fee', "ДА") commission_fee = data_main_risk_stgs.get("commission_fee", "ДА")
pnl = parse_pnl_from_msg(msg) pnl = parse_pnl_from_msg(msg)
data_main_stgs = await rq.get_user_main_settings(tg_id) data_main_stgs = await rq.get_user_main_settings(tg_id)
symbol = data.get('symbol') symbol = data.get("symbol")
switch_mode = data_main_stgs.get('switch_mode', 'Включено') trading_mode = data_main_stgs.get("trading_mode", "Long")
trading_mode = data_main_stgs.get('trading_mode', 'Long')
trigger = await rq.get_for_registration_trigger(tg_id) trigger = await rq.get_for_registration_trigger(tg_id)
margin_mode = data_main_stgs.get('margin_type', 'Isolated') margin_mode = data_main_stgs.get("margin_type", "Isolated")
starting_quantity = safe_float(data_main_stgs.get('starting_quantity')) starting_quantity = safe_float(data_main_stgs.get("starting_quantity"))
martingale_factor = safe_float(data_main_stgs.get("martingale_factor"))
closed_size = safe_float(data.get("closedSize", 0))
commission = safe_float(data.get("execFee", 0))
trade_info = format_trade_details_position(data=msg, commission_fee=commission_fee) if commission_fee == "Да":
pnl -= commission
trade_info = format_trade_details_position(
data=msg,
commission_fee=commission_fee
)
if trade_info: if trade_info:
await message.answer(f"{trade_info}", reply_markup=inline_markup.back_to_main) await message.answer(f"{trade_info}", reply_markup=inline_markup.back_to_main)
side = None if closed_size == 0:
if switch_mode == 'Включено': side = data.get("side", "")
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'
if trigger == "Автоматический": if side.lower() == "buy":
await rq.set_last_series_info(tg_id, last_side="Buy")
elif side.lower() == "sell":
await rq.set_last_series_info(tg_id, last_side="Sell")
if trigger == "Автоматический" and closed_size > 0:
if pnl < 0: if pnl < 0:
martingale_factor = safe_float(data_main_stgs.get('martingale_factor'))
if trading_mode == 'Switch':
side = data_main_stgs.get("last_side")
else:
side = "Buy" if trading_mode == "Long" else "Sell"
current_martingale = await rq.get_martingale_step(tg_id) current_martingale = await rq.get_martingale_step(tg_id)
current_martingale_step = int(current_martingale) current_martingale_step = int(current_martingale)
current_martingale += 1 current_martingale += 1
next_quantity = float(starting_quantity) * (float(martingale_factor) ** current_martingale_step) next_quantity = float(starting_quantity) * (
float(martingale_factor) ** current_martingale_step
)
await rq.update_martingale_step(tg_id, current_martingale) await rq.update_martingale_step(tg_id, current_martingale)
await open_position(tg_id, message, side=side, margin_mode=margin_mode, symbol=symbol, await open_position(
quantity=next_quantity) tg_id,
message,
side=side,
margin_mode=margin_mode,
symbol=symbol,
quantity=next_quantity,
)
elif pnl > 0: elif pnl > 0:
await rq.update_martingale_step(tg_id, 0) await rq.update_martingale_step(tg_id, 0)
await message.answer("❗️ Прибыль достигнута, шаг мартингейла сброшен. " await message.answer(
"Начинаем новую серию ставок") "❗️ Прибыль достигнута, шаг мартингейла сброшен."
await open_position(tg_id, message, side=side, margin_mode=margin_mode, symbol=symbol, )
quantity=starting_quantity)
async def handle_order_message(message, msg: dict) -> None: async def handle_order_message(message, msg: dict) -> None:
@@ -248,21 +295,29 @@ async def error_max_step(message) -> None:
""" """
Сообщение об ошибке превышения максимального количества шагов мартингейла. Сообщение об ошибке превышения максимального количества шагов мартингейла.
""" """
logger.error('Сделка не была совершена, превышен лимит максимального количества ставок в серии.') logger.error(
await message.answer('Сделка не была совершена, превышен лимит максимального количества ставок в серии.', "Сделка не была совершена, превышен лимит максимального количества ставок в серии."
reply_markup=inline_markup.back_to_main) )
await message.answer(
"Сделка не была совершена, превышен лимит максимального количества ставок в серии.",
reply_markup=inline_markup.back_to_main,
)
async def error_max_risk(message) -> None: async def error_max_risk(message) -> None:
""" """
Сообщение об ошибке превышения риск-лимита сделки. Сообщение об ошибке превышения риск-лимита сделки.
""" """
logger.error('Сделка не была совершена, риск убытка превышает допустимый лимит.') logger.error("Сделка не была совершена, риск убытка превышает допустимый лимит.")
await message.answer('Сделка не была совершена, риск убытка превышает допустимый лимит.', await message.answer(
reply_markup=inline_markup.back_to_main) "Сделка не была совершена, риск убытка превышает допустимый лимит.",
reply_markup=inline_markup.back_to_main,
)
async def open_position(tg_id, message, side: str, margin_mode: str, symbol, quantity, tpsl_mode='Full'): async def open_position(
tg_id, message, side: str, margin_mode: str, symbol, quantity, tpsl_mode="Full"
):
""" """
Открывает позицию на Bybit с учётом настроек пользователя, маржи, размера лота, платформы и риска. Открывает позицию на Bybit с учётом настроек пользователя, маржи, размера лота, платформы и риска.
@@ -271,67 +326,106 @@ async def open_position(tg_id, message, side: str, margin_mode: str, symbol, qua
try: try:
client = await get_bybit_client(tg_id) client = await get_bybit_client(tg_id)
data_main_stgs = await rq.get_user_main_settings(tg_id) data_main_stgs = await rq.get_user_main_settings(tg_id)
order_type = data_main_stgs.get('entry_order_type') order_type = data_main_stgs.get("entry_order_type")
bybit_margin_mode = 'ISOLATED_MARGIN' if margin_mode == 'Isolated' else 'REGULAR_MARGIN' bybit_margin_mode = (
"ISOLATED_MARGIN" if margin_mode == "Isolated" else "REGULAR_MARGIN"
)
limit_price = None limit_price = None
if order_type == 'Limit': if order_type == "Limit":
limit_price = await rq.get_limit_price(tg_id) limit_price = await rq.get_limit_price(tg_id)
data_risk_stgs = await rq.get_user_risk_management_settings(tg_id) data_risk_stgs = await rq.get_user_risk_management_settings(tg_id)
balance = await balance_g.get_balance(tg_id, message)
price = await price_symbol.get_price(tg_id, symbol=symbol) price = await price_symbol.get_price(tg_id, symbol=symbol)
entry_price = safe_float(price) entry_price = safe_float(price)
leverage = safe_float(data_main_stgs.get("size_leverage", 1))
max_martingale_steps = int(data_main_stgs.get('maximal_quantity', 0)) max_martingale_steps = int(data_main_stgs.get("maximal_quantity", 0))
current_martingale = await rq.get_martingale_step(tg_id) current_martingale = await rq.get_martingale_step(tg_id)
max_risk_percent = safe_float(data_risk_stgs.get('max_risk_deal')) max_risk_percent = safe_float(data_risk_stgs.get("max_risk_deal"))
loss_profit = safe_float(data_risk_stgs.get('price_loss')) loss_profit = safe_float(data_risk_stgs.get("price_loss"))
commission_fee = data_risk_stgs.get("commission_fee")
starting_quantity = safe_float(data_main_stgs.get('starting_quantity'))
martingale_factor = safe_float(data_main_stgs.get('martingale_factor'))
fee_info = client.get_fee_rates(category='linear', symbol=symbol)
instruments_resp = client.get_instruments_info(category="linear", symbol=symbol)
instrument = instruments_resp.get("result", {}).get("list", [])
if order_type == 'Limit' and limit_price: if commission_fee == "Да":
commission_fee_percent = safe_float(fee_info['result']['list'][0]['takerFeeRate'])
else:
commission_fee_percent = 0.0
total_budget = await calculate_total_budget(
starting_quantity=starting_quantity,
martingale_factor=martingale_factor,
max_steps=max_martingale_steps,
commission_fee_percent=commission_fee_percent,
leverage=leverage,
current_price=entry_price,
)
balance = await balance_g.get_balance(tg_id, message)
if safe_float(balance) < total_budget:
await message.answer(
f"Недостаточно средств для серии из {max_martingale_steps} шагов с текущими параметрами. "
f"Требуемый бюджет: {total_budget:.2f} USDT, доступно: {balance} USDT.",
reply_markup=inline_markup.back_to_main,
)
return
if order_type == "Limit" and limit_price:
price_for_calc = limit_price price_for_calc = limit_price
else: else:
price_for_calc = entry_price price_for_calc = entry_price
potential_loss = safe_float(quantity) * price_for_calc * (loss_profit / 100) potential_loss = safe_float(quantity) * price_for_calc * (loss_profit / 100)
adjusted_loss = potential_loss / leverage
allowed_loss = safe_float(balance) * (max_risk_percent / 100) allowed_loss = safe_float(balance) * (max_risk_percent / 100)
if adjusted_loss > allowed_loss:
await error_max_risk(message)
return
if max_martingale_steps == current_martingale: if max_martingale_steps == current_martingale:
await error_max_step(message) await error_max_step(message)
return return
if potential_loss > allowed_loss:
await error_max_risk(message)
return
client.set_margin_mode(setMarginMode=bybit_margin_mode) client.set_margin_mode(setMarginMode=bybit_margin_mode)
max_leverage = safe_float(instrument[0].get("leverageFilter", {}).get("maxLeverage", 0))
leverage = int(data_main_stgs.get('size_leverage', 1)) if safe_float(leverage) > max_leverage:
try: await message.answer(
resp = client.set_leverage( f"Запрошенное кредитное плечо {leverage} превышает максимальное {max_leverage} для {symbol}. "
category='linear', f"Устанавливаю максимальное.",
symbol=symbol, reply_markup=inline_markup.back_to_main,
buyLeverage=str(leverage),
sellLeverage=str(leverage)
) )
logger.info(
f"Запрошенное кредитное плечо {leverage} превышает максимальное {max_leverage} для {symbol}. Устанавливаю максимальное.")
leverage_to_set = max_leverage
else:
leverage_to_set = safe_float(leverage)
try:
client.set_leverage(
category="linear",
symbol=symbol,
buyLeverage=str(leverage_to_set),
sellLeverage=str(leverage_to_set),
)
logger.info(f"Set leverage to {leverage_to_set} for {symbol}")
except exceptions.InvalidRequestError as e: except exceptions.InvalidRequestError as e:
if "110043" in str(e): if "110043" in str(e):
logger.info(f"Leverage already set to {leverage} for {symbol}") logger.info(f"Leverage already set to {leverage} for {symbol}")
else: else:
raise e raise e
instruments_resp = client.get_instruments_info(category='linear', symbol=symbol) if instruments_resp.get("retCode") == 0:
if instruments_resp.get('retCode') == 0: instrument_info = instruments_resp.get("result", {}).get("list", [])
instrument_info = instruments_resp.get('result', {}).get('list', [])
if instrument_info: if instrument_info:
instrument = instrument_info[0] instrument_info = instrument_info[0]
min_order_qty = float(instrument.get('minOrderQty', 0)) min_notional_value = float(instrument_info.get("lotSizeFilter", {}).get("minNotionalValue", 0))
min_order_value_api = float(instrument.get('minOrderValue', 0)) min_order_value = min_notional_value
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: else:
min_order_value = 5.0 min_order_value = 5.0
@@ -340,34 +434,40 @@ async def open_position(tg_id, message, side: str, margin_mode: str, symbol, qua
await message.answer( await message.answer(
f"Сумма ордера слишком мала: {order_value:.2f} USDT. " f"Сумма ордера слишком мала: {order_value:.2f} USDT. "
f"Минимум для торговли — {min_order_value} USDT. " f"Минимум для торговли — {min_order_value} USDT. "
f"Пожалуйста, увеличьте количество позиций.", reply_markup=inline_markup.back_to_main) f"Пожалуйста, увеличьте количество позиций.",
reply_markup=inline_markup.back_to_main,
)
return False return False
if bybit_margin_mode == 'ISOLATED_MARGIN': if bybit_margin_mode == "ISOLATED_MARGIN":
# Открываем позицию # Открываем позицию
response = client.place_order( response = client.place_order(
category='linear', category="linear",
symbol=symbol, symbol=symbol,
side=side, side=side,
orderType=order_type, orderType=order_type,
qty=str(quantity), qty=str(quantity),
price=str(limit_price) if order_type == 'Limit' and limit_price else None, price=(
timeInForce='GTC', str(limit_price) if order_type == "Limit" and limit_price else None
orderLinkId=f"deal_{symbol}_{int(time.time())}" ),
timeInForce="GTC",
orderLinkId=f"deal_{symbol}_{int(time.time())}",
) )
if response.get('retCode', -1) != 0: if response.get("retCode", -1) != 0:
logger.error(f"Ошибка открытия ордера: {response}") logger.error(f"Ошибка открытия ордера: {response}")
await message.answer(f"Ошибка открытия ордера", reply_markup=inline_markup.back_to_main) await message.answer(
f"Ошибка открытия ордера", reply_markup=inline_markup.back_to_main
)
return False return False
# Получаем цену ликвидации # Получаем цену ликвидации
positions = client.get_positions(category='linear', symbol=symbol) positions = client.get_positions(category="linear", symbol=symbol)
pos = positions.get('result', {}).get('list', [{}])[0] pos = positions.get("result", {}).get("list", [{}])[0]
avg_price = float(pos.get('avgPrice', 0)) avg_price = float(pos.get("avgPrice", 0))
liq_price = safe_float(pos.get('liqPrice', 0)) liq_price = safe_float(pos.get("liqPrice", 0))
if liq_price > 0 and avg_price > 0: if liq_price > 0 and avg_price > 0:
if side.lower() == 'buy': if side.lower() == "buy":
take_profit_price = avg_price + (avg_price - liq_price) take_profit_price = avg_price + (avg_price - liq_price)
else: else:
take_profit_price = avg_price - (liq_price - avg_price) take_profit_price = avg_price - (liq_price - avg_price)
@@ -376,45 +476,49 @@ async def open_position(tg_id, message, side: str, margin_mode: str, symbol, qua
try: try:
try: try:
client.set_tp_sl_mode(symbol=symbol, category='linear', tpSlMode='Full') client.set_tp_sl_mode(
symbol=symbol, category="linear", tpSlMode="Full"
)
except exceptions.InvalidRequestError as e: except exceptions.InvalidRequestError as e:
if 'same tp sl mode' in str(e): if "same tp sl mode" in str(e):
logger.info("Режим TP/SL уже установлен - пропускаем") logger.info("Режим TP/SL уже установлен - пропускаем")
else: else:
raise raise
resp = client.set_trading_stop( resp = client.set_trading_stop(
category='linear', category="linear",
symbol=symbol, symbol=symbol,
takeProfit=str(round(take_profit_price, 5)), takeProfit=str(round(take_profit_price, 5)),
tpTriggerBy='LastPrice', tpTriggerBy="LastPrice",
slTriggerBy='LastPrice', slTriggerBy="LastPrice",
positionIdx=0, positionIdx=0,
reduceOnly=False, reduceOnly=False,
tpslMode=tpsl_mode tpslMode=tpsl_mode,
) )
except Exception as e: except Exception as e:
logger.error(f"Ошибка установки TP/SL: {e}") logger.error(f"Ошибка установки TP/SL: {e}")
await message.answer('Ошибка при установке Take Profit и Stop Loss.', await message.answer(
reply_markup=inline_markup.back_to_main) "Ошибка при установке Take Profit и Stop Loss.",
reply_markup=inline_markup.back_to_main,
)
return False return False
else: else:
logger.warning("Не удалось получить цену ликвидации для позиции") logger.warning("Не удалось получить цену ликвидации для позиции")
else: # REGULAR_MARGIN else: # REGULAR_MARGIN
try: try:
client.set_tp_sl_mode(symbol=symbol, category='linear', tpSlMode='Full') client.set_tp_sl_mode(symbol=symbol, category="linear", tpSlMode="Full")
except exceptions.InvalidRequestError as e: except exceptions.InvalidRequestError as e:
if 'same tp sl mode' in str(e): if "same tp sl mode" in str(e):
logger.info("Режим TP/SL уже установлен - пропускаем") logger.info("Режим TP/SL уже установлен - пропускаем")
else: else:
raise raise
if order_type == 'Market': if order_type == "Market":
base_price = entry_price base_price = entry_price
else: else:
base_price = limit_price base_price = limit_price
if side.lower() == 'buy': if side.lower() == "buy":
take_profit_price = base_price * (1 + loss_profit / 100) take_profit_price = base_price * (1 + loss_profit / 100)
stop_loss_price = base_price * (1 - loss_profit / 100) stop_loss_price = base_price * (1 - loss_profit / 100)
else: else:
@@ -424,24 +528,26 @@ async def open_position(tg_id, message, side: str, margin_mode: str, symbol, qua
take_profit_price = max(take_profit_price, 0) take_profit_price = max(take_profit_price, 0)
stop_loss_price = max(stop_loss_price, 0) stop_loss_price = max(stop_loss_price, 0)
if tpsl_mode == 'Full': if tpsl_mode == "Full":
tp_order_type = 'Market' tp_order_type = "Market"
sl_order_type = 'Market' sl_order_type = "Market"
tp_limit_price = None tp_limit_price = None
sl_limit_price = None sl_limit_price = None
else: # Partial else: # Partial
tp_order_type = 'Limit' tp_order_type = "Limit"
sl_order_type = 'Limit' sl_order_type = "Limit"
tp_limit_price = take_profit_price tp_limit_price = take_profit_price
sl_limit_price = stop_loss_price sl_limit_price = stop_loss_price
response = client.place_order( response = client.place_order(
category='linear', category="linear",
symbol=symbol, symbol=symbol,
side=side, side=side,
orderType=order_type, orderType=order_type,
qty=str(quantity), qty=str(quantity),
price=str(limit_price) if order_type == 'Limit' and limit_price else None, price=(
str(limit_price) if order_type == "Limit" and limit_price else None
),
takeProfit=str(take_profit_price), takeProfit=str(take_profit_price),
tpOrderType=tp_order_type, tpOrderType=tp_order_type,
tpLimitPrice=str(tp_limit_price) if tp_limit_price else None, tpLimitPrice=str(tp_limit_price) if tp_limit_price else None,
@@ -449,25 +555,42 @@ async def open_position(tg_id, message, side: str, margin_mode: str, symbol, qua
slOrderType=sl_order_type, slOrderType=sl_order_type,
slLimitPrice=str(sl_limit_price) if sl_limit_price else None, slLimitPrice=str(sl_limit_price) if sl_limit_price else None,
tpslMode=tpsl_mode, tpslMode=tpsl_mode,
timeInForce='GTC', timeInForce="GTC",
orderLinkId=f"deal_{symbol}_{int(time.time())}" orderLinkId=f"deal_{symbol}_{int(time.time())}",
) )
if response.get('retCode', -1) == 0: if response.get("retCode", -1) == 0:
return True return True
else: else:
logger.error(f"Ошибка открытия ордера: {response}") logger.error(f"Ошибка открытия ордера: {response}")
await message.answer(f"Ошибка открытия ордера", reply_markup=inline_markup.back_to_main) await message.answer(
f"Ошибка открытия ордера", reply_markup=inline_markup.back_to_main
)
return False return False
return None return None
except exceptions.InvalidRequestError as e: except exceptions.InvalidRequestError as e:
logger.error(f"InvalidRequestError: {e}", exc_info=True) logger.error("InvalidRequestError: %s", e)
await message.answer('Недостаточно средств для размещения нового ордера с заданным количеством и плечом.', error_text = str(e)
reply_markup=inline_markup.back_to_main) if "estimated will trigger liq" in error_text:
await message.answer(
"Лимитный ордер может вызвать мгновенную ликвидацию. Проверьте параметры ордера.",
reply_markup=inline_markup.back_to_main,
)
elif "ab not enough for new order" in error_text:
await message.answer("Недостаточно средств для нового ордера",
reply_markup=inline_markup.back_to_main)
else:
await message.answer(
"Недостаточно средств для размещения нового ордера с заданным количеством и плечом.",
reply_markup=inline_markup.back_to_main,
)
except Exception as e: except Exception as e:
logger.error(f"Ошибка при совершении сделки: {e}", exc_info=True) logger.error("Ошибка при совершении сделки: %s", e)
await message.answer('Возникла ошибка при попытке открыть позицию.', reply_markup=inline_markup.back_to_main) await message.answer(
"Возникла ошибка при попытке открыть позицию.",
reply_markup=inline_markup.back_to_main,
)
async def trading_cycle(tg_id, message, side, margin_mode, symbol, starting_quantity): async def trading_cycle(tg_id, message, side, margin_mode, symbol, starting_quantity):
@@ -478,7 +601,7 @@ async def trading_cycle(tg_id, message, side, margin_mode, symbol, starting_quan
timer_data = await rq.get_user_timer(tg_id) timer_data = await rq.get_user_timer(tg_id)
timer_min = 0 timer_min = 0
if isinstance(timer_data, dict): if isinstance(timer_data, dict):
timer_min = timer_data.get('timer_minutes') or timer_data.get('timer') or 0 timer_min = timer_data.get("timer_minutes") or timer_data.get("timer") or 0
else: else:
timer_min = timer_data or 0 timer_min = timer_data or 0
@@ -487,66 +610,72 @@ async def trading_cycle(tg_id, message, side, margin_mode, symbol, starting_quan
if timer_sec > 0: if timer_sec > 0:
await asyncio.sleep(timer_sec) await asyncio.sleep(timer_sec)
await open_position(tg_id, message, side=side, margin_mode=margin_mode, symbol=symbol, await open_position(
quantity=starting_quantity) tg_id,
message,
side=side,
margin_mode=margin_mode,
symbol=symbol,
quantity=starting_quantity,
)
except asyncio.CancelledError: except asyncio.CancelledError:
logger.info(f"Торговый цикл для пользователя {tg_id} был отменён.", exc_info=True) 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, async def set_take_profit_stop_loss(
tpsl_mode='Full'): tg_id: int,
message,
take_profit_price: float,
stop_loss_price: float,
tpsl_mode="Full",
):
""" """
Устанавливает уровни Take Profit и Stop Loss для открытой позиции. Устанавливает уровни Take Profit и Stop Loss для открытой позиции.
""" """
symbol = await rq.get_symbol(tg_id) symbol = await rq.get_symbol(tg_id)
data_main_stgs = await rq.get_user_main_settings(tg_id)
trading_mode = data_main_stgs.get('trading_mode')
side = None
if trading_mode == 'Long':
side = 'Buy'
elif trading_mode == 'Short':
side = 'Sell'
if side is None:
await message.answer("Не удалось определить сторону сделки.")
return
client = await get_bybit_client(tg_id) client = await get_bybit_client(tg_id)
await cancel_all_tp_sl_orders(tg_id, symbol) await cancel_all_tp_sl_orders(tg_id, symbol)
try: try:
try: try:
client.set_tp_sl_mode(symbol=symbol, category='linear', tpSlMode=tpsl_mode) client.set_tp_sl_mode(symbol=symbol, category="linear", tpSlMode=tpsl_mode)
except exceptions.InvalidRequestError as e: except exceptions.InvalidRequestError as e:
if 'same tp sl mode' in str(e).lower(): if "same tp sl mode" in str(e).lower():
logger.info(f"Режим TP/SL уже установлен для {symbol}") logger.info(f"Режим TP/SL уже установлен для {symbol}")
else: else:
raise raise
resp = client.set_trading_stop( resp = client.set_trading_stop(
category='linear', category="linear",
symbol=symbol, symbol=symbol,
takeProfit=str(round(take_profit_price, 5)), takeProfit=str(round(take_profit_price, 5)),
stopLoss=str(round(stop_loss_price, 5)), stopLoss=str(round(stop_loss_price, 5)),
tpTriggerBy='LastPrice', tpTriggerBy="LastPrice",
slTriggerBy='LastPrice', slTriggerBy="LastPrice",
positionIdx=0, positionIdx=0,
reduceOnly=False, reduceOnly=False,
tpslMode=tpsl_mode tpslMode=tpsl_mode,
) )
if resp.get('retCode') != 0: if resp.get("retCode") != 0:
await message.answer(f"Ошибка обновления TP/SL: {resp.get('retMsg')}", await message.answer(
reply_markup=inline_markup.back_to_main) f"Ошибка обновления TP/SL: {resp.get('retMsg')}",
reply_markup=inline_markup.back_to_main,
)
return return
await message.answer( await message.answer(
f"ТП и СЛ успешно установлены:\nТейк-профит: {take_profit_price:.5f}\nСтоп-лосс: {stop_loss_price:.5f}", f"ТП и СЛ успешно установлены:\nТейк-профит: {take_profit_price:.5f}\nСтоп-лосс: {stop_loss_price:.5f}",
reply_markup=inline_markup.back_to_main) reply_markup=inline_markup.back_to_main,
)
except Exception as e: except Exception as e:
logger.error(f"Ошибка установки TP/SL для {symbol}: {e}", exc_info=True) logger.error(f"Ошибка установки TP/SL для {symbol}: {e}", exc_info=True)
await message.answer("Произошла ошибка при установке TP и SL.", reply_markup=inline_markup.back_to_main) await message.answer(
"Произошла ошибка при установке TP и SL.",
reply_markup=inline_markup.back_to_main,
)
async def cancel_all_tp_sl_orders(tg_id, symbol): async def cancel_all_tp_sl_orders(tg_id, symbol):
@@ -556,15 +685,19 @@ async def cancel_all_tp_sl_orders(tg_id, symbol):
client = await get_bybit_client(tg_id) client = await get_bybit_client(tg_id)
last_response = None last_response = None
try: try:
orders_resp = client.get_open_orders(category='linear', symbol=symbol) orders_resp = client.get_open_orders(category="linear", symbol=symbol)
orders = orders_resp.get('result', {}).get('list', []) orders = orders_resp.get("result", {}).get("list", [])
for order in orders: for order in orders:
order_id = order.get('orderId') order_id = order.get("orderId")
order_symbol = order.get('symbol') order_symbol = order.get("symbol")
cancel_resp = client.cancel_order(category='linear', symbol=symbol, orderId=order_id) cancel_resp = client.cancel_order(
if cancel_resp.get('retCode') != 0: category="linear", symbol=symbol, orderId=order_id
logger.warning(f"Не удалось отменить ордер {order_id}: {cancel_resp.get('retMsg')}") )
if cancel_resp.get("retCode") != 0:
logger.warning(
f"Не удалось отменить ордер {order_id}: {cancel_resp.get('retMsg')}"
)
else: else:
last_response = order_symbol last_response = order_symbol
except Exception as e: except Exception as e:
@@ -578,15 +711,21 @@ async def get_active_positions(tg_id, message):
Показывает активные позиции пользователя. Показывает активные позиции пользователя.
""" """
client = await get_bybit_client(tg_id) client = await get_bybit_client(tg_id)
active_positions = client.get_positions(category='linear', settleCoin='USDT') active_positions = client.get_positions(category="linear", settleCoin="USDT")
positions = active_positions.get('result', {}).get('list', []) positions = active_positions.get("result", {}).get("list", [])
active_symbols = [pos.get('symbol') for pos in positions if float(pos.get('size', 0)) > 0] active_symbols = [
pos.get("symbol") for pos in positions if float(pos.get("size", 0)) > 0
]
if active_symbols: if active_symbols:
await message.answer("📈 Ваши активные позиции:", await message.answer(
reply_markup=inline_markup.create_trades_inline_keyboard(active_symbols)) "📈 Ваши активные позиции:",
reply_markup=inline_markup.create_trades_inline_keyboard(active_symbols),
)
else: else:
await message.answer("❗️ У вас нет активных позиций.", reply_markup=inline_markup.back_to_main) await message.answer(
"❗️ У вас нет активных позиций.", reply_markup=inline_markup.back_to_main
)
return return
@@ -595,12 +734,14 @@ async def get_active_positions_by_symbol(tg_id, symbol, message):
Показывает активные позиции пользователя по символу. Показывает активные позиции пользователя по символу.
""" """
client = await get_bybit_client(tg_id) client = await get_bybit_client(tg_id)
active_positions = client.get_positions(category='linear', symbol=symbol) active_positions = client.get_positions(category="linear", symbol=symbol)
positions = active_positions.get('result', {}).get('list', []) positions = active_positions.get("result", {}).get("list", [])
pos = positions[0] if positions else None pos = positions[0] if positions else None
if float(pos.get('size', 0)) == 0: if float(pos.get("size", 0)) == 0:
await message.answer("❗️ У вас нет активных позиций.", reply_markup=inline_markup.back_to_main) await message.answer(
"❗️ У вас нет активных позиций.", reply_markup=inline_markup.back_to_main
)
return return
text = ( text = (
@@ -613,7 +754,9 @@ async def get_active_positions_by_symbol(tg_id, symbol, message):
f"Стоп-лосс: {pos.get('stopLoss')}\n" f"Стоп-лосс: {pos.get('stopLoss')}\n"
) )
await message.answer(text, reply_markup=inline_markup.create_close_deal_markup(symbol)) await message.answer(
text, reply_markup=inline_markup.create_close_deal_markup(symbol)
)
async def get_active_orders(tg_id, message): async def get_active_orders(tg_id, message):
@@ -621,16 +764,23 @@ async def get_active_orders(tg_id, message):
Показывает активные лимитные ордера пользователя. Показывает активные лимитные ордера пользователя.
""" """
client = await get_bybit_client(tg_id) client = await get_bybit_client(tg_id)
response = client.get_open_orders(category='linear', settleCoin='USDT', orderType='Limit') response = client.get_open_orders(
orders = response.get('result', {}).get('list', []) category="linear", settleCoin="USDT", orderType="Limit"
limit_orders = [order for order in orders if order.get('orderType') == 'Limit'] )
orders = response.get("result", {}).get("list", [])
limit_orders = [order for order in orders if order.get("orderType") == "Limit"]
if limit_orders: if limit_orders:
symbols = [order['symbol'] for order in limit_orders] symbols = [order["symbol"] for order in limit_orders]
await message.answer("📈 Ваши активные лимитные ордера:", await message.answer(
reply_markup=inline_markup.create_trades_inline_keyboard_limits(symbols)) "📈 Ваши активные лимитные ордера:",
reply_markup=inline_markup.create_trades_inline_keyboard_limits(symbols),
)
else: else:
await message.answer("❗️ У вас нет активных лимитных ордеров.", reply_markup=inline_markup.back_to_main) await message.answer(
"❗️ У вас нет активных лимитных ордеров.",
reply_markup=inline_markup.back_to_main,
)
return return
@@ -639,15 +789,18 @@ async def get_active_orders_by_symbol(tg_id, symbol, message):
Показывает активные лимитные ордера пользователя по символу. Показывает активные лимитные ордера пользователя по символу.
""" """
client = await get_bybit_client(tg_id) client = await get_bybit_client(tg_id)
active_orders = client.get_open_orders(category='linear', symbol=symbol) active_orders = client.get_open_orders(category="linear", symbol=symbol)
limit_orders = [ limit_orders = [
order for order in active_orders.get('result', {}).get('list', []) order
if order.get('orderType') == 'Limit' for order in active_orders.get("result", {}).get("list", [])
if order.get("orderType") == "Limit"
] ]
if not limit_orders: if not limit_orders:
await message.answer("Нет активных лимитных ордеров по данной торговой паре.", await message.answer(
reply_markup=inline_markup.back_to_main) "Нет активных лимитных ордеров по данной торговой паре.",
reply_markup=inline_markup.back_to_main,
)
return return
texts = [] texts = []
@@ -663,7 +816,9 @@ async def get_active_orders_by_symbol(tg_id, symbol, message):
) )
texts.append(text) texts.append(text)
await message.answer("\n\n".join(texts), reply_markup=inline_markup.create_close_limit_markup(symbol)) 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): async def close_user_trade(tg_id: int, symbol: str):
@@ -675,15 +830,15 @@ async def close_user_trade(tg_id: int, symbol: str):
client = await get_bybit_client(tg_id) client = await get_bybit_client(tg_id)
positions_resp = client.get_positions(category="linear", symbol=symbol) positions_resp = client.get_positions(category="linear", symbol=symbol)
if positions_resp.get('retCode') != 0: if positions_resp.get("retCode") != 0:
return False return False
positions_list = positions_resp.get('result', {}).get('list', []) positions_list = positions_resp.get("result", {}).get("list", [])
if not positions_list: if not positions_list:
return False return False
position = positions_list[0] position = positions_list[0]
qty = abs(safe_float(position.get('size'))) qty = abs(safe_float(position.get("size")))
side = position.get('side') side = position.get("side")
if qty == 0: if qty == 0:
return False return False
@@ -696,15 +851,18 @@ async def close_user_trade(tg_id: int, symbol: str):
orderType="Market", orderType="Market",
qty=str(qty), qty=str(qty),
timeInForce="GTC", timeInForce="GTC",
reduceOnly=True reduceOnly=True,
) )
if place_resp.get('retCode') == 0: if place_resp.get("retCode") == 0:
return True return True
else: else:
return False return False
except Exception as e: except Exception as e:
logger.error(f"Ошибка закрытия сделки {symbol} для пользователя {tg_id}: {e}", exc_info=True) logger.error(
f"Ошибка закрытия сделки {symbol} для пользователя {tg_id}: {e}",
exc_info=True,
)
return False return False
@@ -716,13 +874,19 @@ async def close_trade_after_delay(tg_id: int, message, symbol: str, delay_sec: i
await asyncio.sleep(delay_sec) await asyncio.sleep(delay_sec)
result = await close_user_trade(tg_id, symbol) result = await close_user_trade(tg_id, symbol)
if result: if result:
await message.answer(f"Сделка {symbol} успешно закрыта по таймеру.", await message.answer(
reply_markup=inline_markup.back_to_main) f"Сделка {symbol} успешно закрыта по таймеру."
)
logger.info(f"Сделка {symbol} успешно закрыта по таймеру.") logger.info(f"Сделка {symbol} успешно закрыта по таймеру.")
else: else:
await message.answer(f"Не удалось закрыть сделку {symbol} по таймеру.", await message.answer(
reply_markup=inline_markup.back_to_main) f"Не удалось закрыть сделку {symbol} по таймеру.",
reply_markup=inline_markup.back_to_main,
)
logger.error(f"Не удалось закрыть сделку {symbol} по таймеру.") logger.error(f"Не удалось закрыть сделку {symbol} по таймеру.")
except asyncio.CancelledError: except asyncio.CancelledError:
await message.answer(f"Закрытие сделки {symbol} по таймеру отменено.", reply_markup=inline_markup.back_to_main) await message.answer(
f"Закрытие сделки {symbol} по таймеру отменено.",
reply_markup=inline_markup.back_to_main,
)
logger.info(f"Закрытие сделки {symbol} по таймеру отменено.") logger.info(f"Закрытие сделки {symbol} по таймеру отменено.")

View File

@@ -32,7 +32,7 @@ async def get_balance(tg_id: int, message) -> float:
api_secret=secret_key api_secret=secret_key
) )
if api_key == 'None' or secret_key == 'None': if api_key is None or secret_key is None:
await message.answer('⚠️ Подключите платформу для торговли', await message.answer('⚠️ Подключите платформу для торговли',
reply_markup=inline_markup.connect_bybit_api_message) reply_markup=inline_markup.connect_bybit_api_message)
return 0 return 0
@@ -48,5 +48,5 @@ async def get_balance(tg_id: int, message) -> float:
return 0 return 0
except Exception as e: except Exception as e:
logger.error(f"Ошибка при получении общего баланса: {e}") logger.error(f"Ошибка при получении общего баланса: {e}")
await message.answer('⚠️ Ошибка при получении баланса') await message.answer('Ошибка при подключении, повторите попытку', reply_markup=inline_markup.connect_bybit_api_message)
return 0 return 0

View File

@@ -2,6 +2,7 @@
import logging.config import logging.config
from aiogram import F, Router from aiogram import F, Router
from app.services.Bybit.functions.bybit_ws import run_ws_for_user
from app.telegram.functions.main_settings.settings import main_settings_message from app.telegram.functions.main_settings.settings import main_settings_message
from logger_helper.logger_helper import LOGGING_CONFIG from logger_helper.logger_helper import LOGGING_CONFIG
@@ -35,10 +36,13 @@ async def clb_start_bybit_trade_message(callback: CallbackQuery) -> None:
Обработка нажатия кнопок запуска торговли или возврата в главное меню. Обработка нажатия кнопок запуска торговли или возврата в главное меню.
Отправляет информацию о балансе, символе, цене и инструкциях по торговле. Отправляет информацию о балансе, символе, цене и инструкциях по торговле.
""" """
tg_id = callback.from_user.id
message = callback.message
user_id = callback.from_user.id user_id = callback.from_user.id
balance = await get_balance(user_id, callback.message) balance = await get_balance(user_id, callback.message)
if balance: if balance:
await run_ws_for_user(tg_id, message)
symbol = await rq.get_symbol(user_id) symbol = await rq.get_symbol(user_id)
price = await get_price(user_id, symbol=symbol) price = await get_price(user_id, symbol=symbol)
@@ -61,8 +65,11 @@ async def start_bybit_trade_message(message: Message) -> None:
вместе с инструкциями по началу торговли. вместе с инструкциями по началу торговли.
""" """
balance = await get_balance(message.from_user.id, message) balance = await get_balance(message.from_user.id, message)
tg_id = message.from_user.id
await run_ws_for_user(tg_id, message)
if balance: if balance:
await run_ws_for_user(tg_id, message)
symbol = await rq.get_symbol(message.from_user.id) symbol = await rq.get_symbol(message.from_user.id)
price = await get_price(message.from_user.id, symbol=symbol) price = await get_price(message.from_user.id, symbol=symbol)
@@ -86,6 +93,7 @@ async def update_symbol_for_trade_message(callback: CallbackQuery, state: FSMCon
Начинает процедуру обновления торговой пары, переводит пользователя в состояние ожидания пары. Начинает процедуру обновления торговой пары, переводит пользователя в состояние ожидания пары.
""" """
await state.set_state(state_update_symbol.symbol) await state.set_state(state_update_symbol.symbol)
await callback.answer()
await callback.message.answer( await callback.message.answer(
text='Укажите торговую пару заглавными буквами без пробелов и лишних символов (пример: BTCUSDT): ', text='Укажите торговую пару заглавными буквами без пробелов и лишних символов (пример: BTCUSDT): ',
@@ -99,7 +107,6 @@ async def update_symbol_for_trade(message: Message, state: FSMContext) -> None:
При успешном обновлении сохранит пару и отправит обновлённую информацию. При успешном обновлении сохранит пару и отправит обновлённую информацию.
""" """
user_input = message.text.strip().upper() user_input = message.text.strip().upper()
exists = await get_valid_symbols(message.from_user.id, user_input) exists = await get_valid_symbols(message.from_user.id, user_input)
if not exists: if not exists:
@@ -189,18 +196,25 @@ async def start_trading_process(callback: CallbackQuery) -> None:
""" """
tg_id = callback.from_user.id tg_id = callback.from_user.id
message = callback.message message = callback.message
await run_ws_for_user(tg_id, message)
await callback.answer()
data_main_stgs = await rq.get_user_main_settings(tg_id) data_main_stgs = await rq.get_user_main_settings(tg_id)
symbol = await rq.get_symbol(tg_id) symbol = await rq.get_symbol(tg_id)
margin_mode = data_main_stgs.get('margin_type', 'Isolated') margin_mode = data_main_stgs.get('margin_type', 'Isolated')
trading_mode = data_main_stgs.get('trading_mode') trading_mode = data_main_stgs.get('trading_mode')
switch_mode = data_main_stgs.get('switch_mode_enabled')
starting_quantity = safe_float(data_main_stgs.get('starting_quantity')) starting_quantity = safe_float(data_main_stgs.get('starting_quantity'))
switch_state = data_main_stgs.get("switch_state", "По направлению")
side = None if trading_mode == 'Switch':
if switch_mode == 'Включено': if switch_state == "По направлению":
switch_state = data_main_stgs.get('switch_state', 'Long') side = data_main_stgs.get("last_side")
side = 'Buy' if switch_state == 'Long' else 'Sell' else:
side = data_main_stgs.get("last_side")
if side.lower() == "buy":
side = "Sell"
else:
side = "Buy"
else: else:
if trading_mode == 'Long': if trading_mode == 'Long':
side = 'Buy' side = 'Buy'
@@ -221,9 +235,9 @@ async def start_trading_process(callback: CallbackQuery) -> None:
timer_minute = timer_data or 0 timer_minute = timer_data or 0
if timer_minute > 0: if timer_minute > 0:
await message.answer(f"Торговля начнётся через {timer_minute} мин.")
await trading_cycle(tg_id, message, side=side, margin_mode=margin_mode, symbol=symbol, await trading_cycle(tg_id, message, side=side, margin_mode=margin_mode, symbol=symbol,
starting_quantity=starting_quantity) starting_quantity=starting_quantity)
await message.answer(f"Торговля начнётся через {timer_minute} мин.")
await rq.update_user_timer(tg_id, minutes=0) await rq.update_user_timer(tg_id, minutes=0)
else: else:
await open_position(tg_id, message, side, margin_mode, symbol=symbol, quantity=starting_quantity) await open_position(tg_id, message, side, margin_mode, symbol=symbol, quantity=starting_quantity)
@@ -437,7 +451,7 @@ async def process_close_delay(message: Message, state: FSMContext) -> None:
symbol = data.get("symbol") symbol = data.get("symbol")
delay = delay_minutes * 60 delay = delay_minutes * 60
await message.answer(f"Закрытие сделки {symbol} запланировано через {delay_minutes} мин.") await message.answer(f"Закрытие сделки {symbol} запланировано через {delay_minutes} мин.", reply_markup=inline_markup.back_to_main)
await close_trade_after_delay(message.from_user.id, message, symbol, delay) await close_trade_after_delay(message.from_user.id, message, symbol, delay)
await state.clear() await state.clear()
@@ -479,11 +493,11 @@ async def stop_immediately(callback: CallbackQuery):
@router_functions_bybit_trade.callback_query(F.data == "stop_with_timer") @router_functions_bybit_trade.callback_query(F.data == "stop_with_timer")
async def stop_with_timer_start(callback: CallbackQuery, state: FSMContext): async def stop_with_timer_start(callback: CallbackQuery, state: FSMContext):
""" """
Запускает диалог с пользователем для задания задержки перед остановкой торговли. Запускает диалог с пользователем для задания задержки до остановки торговли.
""" """
await state.set_state(CloseTradeTimerState.waiting_for_trade) await state.set_state(CloseTradeTimerState.waiting_for_trade)
await callback.message.answer("Введите задержку в минутах перед остановкой торговли:", await callback.message.answer("Введите задержку в минутах до остановки торговли:",
reply_markup=inline_markup.cancel) reply_markup=inline_markup.cancel)
await callback.answer() await callback.answer()
@@ -505,7 +519,7 @@ async def process_stop_delay(message: Message, state: FSMContext):
tg_id = message.from_user.id tg_id = message.from_user.id
delay_seconds = delay_minutes * 60 delay_seconds = delay_minutes * 60
await message.answer(f"Торговля будет остановлена через {delay_minutes} минут.") await message.answer(f"Торговля будет остановлена через {delay_minutes} минут.", reply_markup=inline_markup.back_to_main)
await asyncio.sleep(delay_seconds) await asyncio.sleep(delay_seconds)
await rq.update_trigger(tg_id, "Ручной") await rq.update_trigger(tg_id, "Ручной")
await message.answer("Автоматическая торговля остановлена.", reply_markup=inline_markup.back_to_main) await message.answer("Автоматическая торговля остановлена.", reply_markup=inline_markup.back_to_main)

View File

@@ -25,8 +25,8 @@ special_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Основные настройки", callback_data='clb_change_main_settings'), [InlineKeyboardButton(text="Основные настройки", callback_data='clb_change_main_settings'),
InlineKeyboardButton(text="Риск-менеджмент", callback_data='clb_change_risk_management_settings')], InlineKeyboardButton(text="Риск-менеджмент", callback_data='clb_change_risk_management_settings')],
[InlineKeyboardButton(text="Условия запуска", callback_data='clb_change_condition_settings'), [InlineKeyboardButton(text="Условия запуска", callback_data='clb_change_condition_settings')],
InlineKeyboardButton(text="Дополнительные параметры", callback_data='clb_change_additional_settings')], # InlineKeyboardButton(text="Дополнительные параметры", callback_data='clb_change_additional_settings')],
[InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api_message')], [InlineKeyboardButton(text="Подключить Bybit", callback_data='clb_new_user_connect_bybit_api_message')],
back_btn_to_main back_btn_to_main
]) ])
@@ -73,7 +73,7 @@ back_to_main = InlineKeyboardMarkup(inline_keyboard=[
main_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ main_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_trading_mode'), [InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_trading_mode'),
InlineKeyboardButton(text='Режим свитч', callback_data='clb_change_switch_mode'), InlineKeyboardButton(text='Состояние свитча', callback_data='clb_change_switch_state'),
InlineKeyboardButton(text='Тип маржи', callback_data='clb_change_margin_type')], InlineKeyboardButton(text='Тип маржи', callback_data='clb_change_margin_type')],
[InlineKeyboardButton(text='Размер кредитного плеча', callback_data='clb_change_size_leverage'), [InlineKeyboardButton(text='Размер кредитного плеча', callback_data='clb_change_size_leverage'),
@@ -101,14 +101,14 @@ risk_management_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
condition_settings_markup = InlineKeyboardMarkup(inline_keyboard=[ condition_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_mode'), [InlineKeyboardButton(text='Режим торговли', callback_data='clb_change_mode'),
InlineKeyboardButton(text='Таймер', callback_data='clb_change_timer')], InlineKeyboardButton(text='Таймер', callback_data='clb_change_timer')],
#
[InlineKeyboardButton(text='Фильтр волатильности', callback_data='clb_change_filter_volatility'), # [InlineKeyboardButton(text='Фильтр волатильности', callback_data='clb_change_filter_volatility'),
InlineKeyboardButton(text='Внешние сигналы', callback_data='clb_change_external_cues')], # InlineKeyboardButton(text='Внешние сигналы', callback_data='clb_change_external_cues')],
#
[InlineKeyboardButton(text='Сигналы TradingView', callback_data='clb_change_tradingview_cues'), # [InlineKeyboardButton(text='Сигналы TradingView', callback_data='clb_change_tradingview_cues'),
InlineKeyboardButton(text='Webhook URL', callback_data='clb_change_webhook')], # InlineKeyboardButton(text='Webhook URL', callback_data='clb_change_webhook')],
#
[InlineKeyboardButton(text='AI - аналитика', callback_data='clb_change_ai_analytics')], # [InlineKeyboardButton(text='AI - аналитика', callback_data='clb_change_ai_analytics')],
back_btn_list_settings, back_btn_list_settings,
back_btn_to_main back_btn_to_main
@@ -127,7 +127,8 @@ additional_settings_markup = InlineKeyboardMarkup(inline_keyboard=[
trading_mode_markup = InlineKeyboardMarkup(inline_keyboard=[ trading_mode_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Лонг", callback_data="trade_mode_long"), [InlineKeyboardButton(text="Лонг", callback_data="trade_mode_long"),
InlineKeyboardButton(text="Шорт", callback_data="trade_mode_short"), InlineKeyboardButton(text="Шорт", callback_data="trade_mode_short"),
InlineKeyboardButton(text="Смарт", callback_data="trade_mode_smart")], InlineKeyboardButton(text="Свитч", callback_data="trade_mode_switch")],
# InlineKeyboardButton(text="Смарт", callback_data="trade_mode_smart")],
back_btn_list_settings, back_btn_list_settings,
back_btn_to_main back_btn_to_main
@@ -137,8 +138,7 @@ margin_type_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Изолированный", callback_data="margin_type_isolated"), [InlineKeyboardButton(text="Изолированный", callback_data="margin_type_isolated"),
InlineKeyboardButton(text="Кросс", callback_data="margin_type_cross")], InlineKeyboardButton(text="Кросс", callback_data="margin_type_cross")],
back_btn_list_settings, back_btn_list_settings
back_btn_to_main
]) ])
trigger_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE trigger_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE
@@ -208,14 +208,7 @@ stop_choice_markup = InlineKeyboardMarkup(
] ]
) )
buttons_on_off_markup_for_switch = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТЬ НА INLINE
[InlineKeyboardButton(text='Включить', callback_data="clb_on_switch"),
InlineKeyboardButton(text='Выключить', callback_data="clb_off_switch")],
[InlineKeyboardButton(text="Изменить состояние", callback_data="clb_switch_state")],
back_btn_to_main
])
switch_state_markup = InlineKeyboardMarkup(inline_keyboard=[ switch_state_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Long', callback_data="clb_long_switch"), [InlineKeyboardButton(text='По направлению', callback_data="clb_long_switch"),
InlineKeyboardButton(text='Short', callback_data="clb_short_switch")], InlineKeyboardButton(text='Против направления', callback_data="clb_short_switch")],
]) ])

View File

@@ -54,10 +54,10 @@ class User_Bybit_API(Base):
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
api_key = mapped_column(String(18), default='None') api_key = mapped_column(String(18), unique=True, nullable=True)
secret_key = mapped_column(String(36), default='None') secret_key = mapped_column(String(36), unique=True, nullable=True)
class User_Symbol(Base): class User_Symbol(Base):
@@ -65,25 +65,12 @@ class User_Symbol(Base):
Модель таблицы user_main_settings. Модель таблицы user_main_settings.
Хранит основные настройки торговли для пользователя. Хранит основные настройки торговли для пользователя.
Атрибуты:
id (int): Внутренний первичный ключ записи.
tg_id (int): Внешний ключ на Telegram пользователя.
trading_mode (str): Режим торговли (ForeignKey на trading_modes.mode).
margin_type (str): Тип маржи (ForeignKey на margin_types.type).
size_leverage (int): Кредитное плечо.
starting_quantity (int): Начальный объём позиции.
martingale_factor (int): Коэффициент мартингейла.
martingale_step (int): Текущий шаг мартингейла.
maximal_quantity (int): Максимальное количество шагов мартингейла.
entry_order_type (str): Тип входа (Market или Limit).
limit_order_price (str, optional): Цена лимитного ордера, если используется.
""" """
__tablename__ = 'user_symbols' __tablename__ = 'user_symbols'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
symbol = mapped_column(String(18), default='PENGUUSDT') symbol = mapped_column(String(18), default='PENGUUSDT')
@@ -94,7 +81,7 @@ class Trading_Mode(Base):
Атрибуты: Атрибуты:
id (int): Первичный ключ. id (int): Первичный ключ.
mode (str): Уникальный режим (например, 'Long', 'Short'). mode (str): Уникальный режим (например, 'Long', 'Short', 'Switch).
""" """
__tablename__ = 'trading_modes' __tablename__ = 'trading_modes'
@@ -123,7 +110,7 @@ class Trigger(Base):
Справочник триггеров для сделок. Справочник триггеров для сделок.
Атрибуты: Атрибуты:
id (int): Первичный ключ.. id (int): Первичный ключ.
""" """
__tablename__ = 'triggers' __tablename__ = 'triggers'
@@ -148,17 +135,17 @@ class User_Main_Settings(Base):
maximal_quantity (int): Максимальное число шагов мартингейла. maximal_quantity (int): Максимальное число шагов мартингейла.
entry_order_type (str): Тип ордера входа (Market/Limit). entry_order_type (str): Тип ордера входа (Market/Limit).
limit_order_price (Optional[str]): Цена лимитного ордера, если есть. limit_order_price (Optional[str]): Цена лимитного ордера, если есть.
last_side (str): Последняя сторона ордера.
""" """
__tablename__ = 'user_main_settings' __tablename__ = 'user_main_settings'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
trading_mode = mapped_column(ForeignKey("trading_modes.mode")) trading_mode = mapped_column(ForeignKey("trading_modes.mode"))
margin_type = mapped_column(ForeignKey("margin_types.type")) margin_type = mapped_column(ForeignKey("margin_types.type"))
switch_mode_enabled = mapped_column(String(15), default="Выключен") switch_state = mapped_column(String(10), default='По направлению')
switch_state = mapped_column(String(10), default='Long')
size_leverage = mapped_column(Integer(), default=1) size_leverage = mapped_column(Integer(), default=1)
starting_quantity = mapped_column(Integer(), default=1) starting_quantity = mapped_column(Integer(), default=1)
martingale_factor = mapped_column(Integer(), default=1) martingale_factor = mapped_column(Integer(), default=1)
@@ -166,6 +153,7 @@ class User_Main_Settings(Base):
maximal_quantity = mapped_column(Integer(), default=10) maximal_quantity = mapped_column(Integer(), default=10)
entry_order_type = mapped_column(String(10), default='Market') entry_order_type = mapped_column(String(10), default='Market')
limit_order_price = mapped_column(Numeric(18, 15), nullable=True) limit_order_price = mapped_column(Numeric(18, 15), nullable=True)
last_side = mapped_column(String(10), default='Buy')
class User_Risk_Management_Settings(Base): class User_Risk_Management_Settings(Base):
@@ -184,7 +172,7 @@ class User_Risk_Management_Settings(Base):
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
price_profit = mapped_column(Integer(), default=1) price_profit = mapped_column(Integer(), default=1)
price_loss = mapped_column(Integer(), default=1) price_loss = mapped_column(Integer(), default=1)
@@ -211,9 +199,9 @@ class User_Condition_Settings(Base):
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
trigger = mapped_column(String(15), default='Ручной') trigger = mapped_column(String(15), default='Автоматический')
filter_time = mapped_column(String(25), default='???') filter_time = mapped_column(String(25), default='???')
filter_volatility = mapped_column(Boolean, default=False) filter_volatility = mapped_column(Boolean, default=False)
external_cues = mapped_column(Boolean, default=False) external_cues = mapped_column(Boolean, default=False)
@@ -237,7 +225,7 @@ class User_Additional_Settings(Base):
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
pattern_save = mapped_column(Boolean, default=False) pattern_save = mapped_column(Boolean, default=False)
autostart = mapped_column(Boolean, default=False) autostart = mapped_column(Boolean, default=False)
@@ -260,7 +248,7 @@ class USER_DEALS(Base):
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
symbol = mapped_column(String(18), default='PENGUUSDT') symbol = mapped_column(String(18), default='PENGUUSDT')
side = mapped_column(String(10), nullable=False) side = mapped_column(String(10), nullable=False)
@@ -282,7 +270,7 @@ class UserTimer(Base):
__tablename__ = 'user_timers' __tablename__ = 'user_timers'
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id")) tg_id = mapped_column(ForeignKey("user_telegram_id.tg_id"), unique=True, nullable=True)
timer_minutes = mapped_column(Integer, nullable=False, default=0) timer_minutes = mapped_column(Integer, nullable=False, default=0)
timer_start = mapped_column(DateTime, default=datetime.utcnow) timer_start = mapped_column(DateTime, default=datetime.utcnow)
timer_end = mapped_column(DateTime, nullable=True) timer_end = mapped_column(DateTime, nullable=True)
@@ -296,7 +284,7 @@ async def async_main():
await conn.run_sync(Base.metadata.create_all) await conn.run_sync(Base.metadata.create_all)
# Заполнение таблиц # Заполнение таблиц
modes = ['Long', 'Short', 'Smart'] modes = ['Long', 'Short', 'Switch', 'Smart']
for mode in modes: for mode in modes:
result = await conn.execute(select(Trading_Mode).where(Trading_Mode.mode == mode)) result = await conn.execute(select(Trading_Mode).where(Trading_Mode.mode == mode))
if not result.first(): if not result.first():
@@ -309,3 +297,10 @@ async def async_main():
if not result.first(): if not result.first():
logger.info("Заполение таблицы типов маржи") logger.info("Заполение таблицы типов маржи")
await conn.execute(Margin_type.__table__.insert().values(type=type)) await conn.execute(Margin_type.__table__.insert().values(type=type))
last_side = ['Buy', 'Sell']
for side in last_side:
result = await conn.execute(select(User_Main_Settings).where(User_Main_Settings.last_side == side))
if not result.first():
logger.info("Заполение таблицы последнего направления")
await conn.execute(User_Main_Settings.__table__.insert().values(last_side=side))

View File

@@ -239,17 +239,21 @@ async def update_symbol(tg_id: int, symbol: str) -> None:
await session.commit() await session.commit()
async def update_api_key(tg_id: int, api: str) -> None: async def upsert_api_keys(tg_id: int, api_key: str, secret_key: str) -> None:
"""Обновить API ключ пользователя.""" """Обновить API ключ пользователя."""
async with async_session() as session: async with async_session() as session:
await session.execute(update(UBA).where(UBA.tg_id == tg_id).values(api_key=api)) result = await session.execute(select(UBA).where(UBA.tg_id == tg_id))
await session.commit() user = result.scalars().first()
if user:
if api_key is not None:
async def update_secret_key(tg_id: int, api: str) -> None: user.api_key = api_key
"""Обновить секретный ключ пользователя.""" if secret_key is not None:
async with async_session() as session: user.secret_key = secret_key
await session.execute(update(UBA).where(UBA.tg_id == tg_id).values(secret_key=api)) logger.info(f"Обновлены ключи для пользователя {tg_id}")
else:
new_user = UBA(tg_id=tg_id, api_key=api_key, secret_key=secret_key)
session.add(new_user)
logger.info(f"Добавлен новый пользователь {tg_id} с ключами")
await session.commit() await session.commit()
@@ -303,36 +307,20 @@ async def get_user_main_settings(tg_id):
"""Получить основные настройки пользователя.""" """Получить основные настройки пользователя."""
async with async_session() as session: async with async_session() as session:
user = await session.scalar(select(UMS).where(UMS.tg_id == tg_id)) user = await session.scalar(select(UMS).where(UMS.tg_id == tg_id))
if user: if user:
logger.info("Получение основных настроек пользователя %s", tg_id)
trading_mode = await session.scalar(select(UMS.trading_mode).where(UMS.tg_id == tg_id))
margin_mode = await session.scalar(select(UMS.margin_type).where(UMS.tg_id == tg_id))
switch_mode_enabled = await session.scalar(select(UMS.switch_mode_enabled).where(UMS.tg_id == tg_id))
switch_state = await session.scalar(select(UMS.switch_state).where(UMS.tg_id == tg_id))
size_leverage = await session.scalar(select(UMS.size_leverage).where(UMS.tg_id == tg_id))
starting_quantity = await session.scalar(select(UMS.starting_quantity).where(UMS.tg_id == tg_id))
martingale_factor = await session.scalar(select(UMS.martingale_factor).where(UMS.tg_id == tg_id))
maximal_quantity = await session.scalar(select(UMS.maximal_quantity).where(UMS.tg_id == tg_id))
entry_order_type = await session.scalar(select(UMS.entry_order_type).where(UMS.tg_id == tg_id))
limit_order_price = await session.scalar(select(UMS.limit_order_price).where(UMS.tg_id == tg_id))
martingale_step = await session.scalar(select(UMS.martingale_step).where(UMS.tg_id == tg_id))
data = { data = {
'trading_mode': trading_mode, 'trading_mode': user.trading_mode,
'margin_type': margin_mode, 'margin_type': user.margin_type,
'switch_mode_enabled': switch_mode_enabled, 'switch_state': user.switch_state,
'switch_state': switch_state, 'size_leverage': user.size_leverage,
'size_leverage': size_leverage, 'starting_quantity': user.starting_quantity,
'starting_quantity': starting_quantity, 'martingale_factor': user.martingale_factor,
'martingale_factor': martingale_factor, 'maximal_quantity': user.maximal_quantity,
'maximal_quantity': maximal_quantity, 'entry_order_type': user.entry_order_type,
'entry_order_type': entry_order_type, 'limit_order_price': user.limit_order_price,
'limit_order_price': limit_order_price, 'martingale_step': user.martingale_step,
'martingale_step': martingale_step, 'last_side': user.last_side,
} }
return data return data
@@ -576,3 +564,22 @@ async def update_trigger(tg_id, trigger):
await session.execute(update(UCS).where(UCS.tg_id == tg_id).values(trigger=trigger)) await session.execute(update(UCS).where(UCS.tg_id == tg_id).values(trigger=trigger))
await session.commit() await session.commit()
async def set_last_series_info(tg_id: int, last_side: str):
async with async_session() as session:
async with session.begin():
# Обновляем запись
result = await session.execute(
update(UMS)
.where(UMS.tg_id == tg_id)
.values(last_side=last_side)
)
if result.rowcount == 0:
# Если запись не существует, создаём новую
new_entry = UMS(
tg_id=tg_id,
last_side=last_side,
)
session.add(new_entry)
await session.commit()

View File

@@ -30,11 +30,6 @@ async def main_settings_message(id, message):
<b>- Режим торговли:</b> {trigger} <b>- Режим торговли:</b> {trigger}
<b>- Таймер: </b> установить таймер / остановить таймер <b>- Таймер: </b> установить таймер / остановить таймер
<b>- Фильтр волатильности / объёма: </b> включить/отключить
<b>- Интеграции и внешние сигналы: </b>
<b>- Использовать сигналы TradingView:</b> да / нет
<b>- Использовать AI-аналитику от ChatGPT:</b> да / не
<b>- Webhook URL для сигналов (если используется TradingView): </b>
""" """
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.condition_settings_markup) await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.condition_settings_markup)
@@ -42,7 +37,7 @@ async def main_settings_message(id, message):
async def trigger_message(id, message, state: FSMContext): async def trigger_message(id, message, state: FSMContext):
await state.set_state(condition_settings.trigger) await state.set_state(condition_settings.trigger)
text = ''' text = '''
<b>- Автоматический:</b> торговля будет продолжаться до условии остановки. <b>- Автоматический:</b> торговля будет происходить в рамках серии ставок.
<b>- Ручной:</b> торговля будет происходить только в ручном режиме. <b>- Ручной:</b> торговля будет происходить только в ручном режиме.
<em>- Выберите тип триггера:</em>''' <em>- Выберите тип триггера:</em>'''

View File

@@ -5,6 +5,9 @@ import app.telegram.Keyboards.inline_keyboards as inline_markup
from pybit.unified_trading import HTTP from pybit.unified_trading import HTTP
import app.telegram.database.requests as rq import app.telegram.database.requests as rq
from aiogram.types import Message, CallbackQuery from aiogram.types import Message, CallbackQuery
from app.services.Bybit.functions.price_symbol import get_price
from app.services.Bybit.functions.Futures import safe_float, calculate_total_budget, get_bybit_client
from app.states.States import update_main_settings from app.states.States import update_main_settings
from logger_helper.logger_helper import LOGGING_CONFIG from logger_helper.logger_helper import LOGGING_CONFIG
@@ -24,20 +27,54 @@ async def reg_new_user_default_main_settings(id, message):
async def main_settings_message(id, message): async def main_settings_message(id, message):
data = await rq.get_user_main_settings(id) try:
data = await rq.get_user_main_settings(id)
tg_id = id
await message.answer(f"""<b>Основные настройки</b> data_main_stgs = await rq.get_user_main_settings(id)
data_risk_stgs = await rq.get_user_risk_management_settings(id)
client = await get_bybit_client(tg_id)
symbol = await rq.get_symbol(tg_id)
max_martingale_steps = (data_main_stgs or {}).get('maximal_quantity', 0)
commission_fee = (data_risk_stgs or {}).get('commission_fee')
starting_quantity = safe_float((data_main_stgs or {}).get('starting_quantity'))
martingale_factor = safe_float((data_main_stgs or {}).get('martingale_factor'))
fee_info = client.get_fee_rates(category='linear', symbol=symbol)
leverage = safe_float((data_main_stgs or {}).get('size_leverage'))
price = await get_price(tg_id, symbol=symbol)
entry_price = safe_float(price)
<b>- Режим торговли:</b> {data['trading_mode']} if commission_fee == "Да":
<b>- Режим свитч:</b> {data['switch_mode_enabled']} commission_fee_percent = safe_float(fee_info['result']['list'][0]['takerFeeRate'])
<b>- Состояние свитча:</b> {data['switch_state']} else:
<b>- Тип маржи:</b> {data['margin_type']} commission_fee_percent = 0.0
<b>- Размер кредитного плеча:</b> х{data['size_leverage']}
<b>- Начальная ставка:</b> {data['starting_quantity']} total_budget = await calculate_total_budget(
<b>- Коэффициент мартингейла:</b> {data['martingale_factor']} starting_quantity=starting_quantity,
<b>- Количество ставок в серии:</b> {data['martingale_step']} martingale_factor=martingale_factor,
<b>- Максимальное количество ставок в серии:</b> {data['maximal_quantity']} max_steps=max_martingale_steps,
""", parse_mode='html', reply_markup=inline_markup.main_settings_markup) commission_fee_percent=commission_fee_percent,
leverage=leverage,
current_price=entry_price,
)
await message.answer(f"""<b>Основные настройки</b>
<b>- Режим торговли:</b> {data['trading_mode']}
<b>- Состояние свитча:</b> {data['switch_state']}
<b>- Направление последней сделки:</b> {data['last_side']}
<b>- Тип маржи:</b> {data['margin_type']}
<b>- Размер кредитного плеча:</b> х{data['size_leverage']}
<b>- Начальная ставка:</b> {data['starting_quantity']}
<b>- Коэффициент мартингейла:</b> {data['martingale_factor']}
<b>- Количество ставок в серии:</b> {data['martingale_step']}
<b>- Максимальное количество ставок в серии:</b> {data['maximal_quantity']}
<b>- Требуемый бюджет:</b> {total_budget:.2f} USDT
""", parse_mode='html', reply_markup=inline_markup.main_settings_markup)
except PermissionError as e:
logger.error("Authenticated endpoints require keys: %s", e)
await message.answer("Вы не авторизованы.", reply_markup=inline_markup.connect_bybit_api_message)
async def trading_mode_message(message, state): async def trading_mode_message(message, state):
@@ -49,7 +86,7 @@ async def trading_mode_message(message, state):
<b>Шорт</b> — метод продажи активов, взятых в кредит, чтобы получить прибыль от снижения цены. <b>Шорт</b> — метод продажи активов, взятых в кредит, чтобы получить прибыль от снижения цены.
<b>Смарт</b> — автоматизированный режим, который подбирает оптимальную стратегию в зависимости от текущих рыночных условий. <b>Свитч</b> — динамическое переключение между торговыми режимами для максимизации эффективности.
<em>Выберите ниже для изменений:</em> <em>Выберите ниже для изменений:</em>
""", parse_mode='html', reply_markup=inline_markup.trading_mode_markup) """, parse_mode='html', reply_markup=inline_markup.trading_mode_markup)
@@ -77,12 +114,19 @@ async def state_trading_mode(callback: CallbackQuery, state):
await state.clear() await state.clear()
case 'trade_mode_switch':
await callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Switch")
await rq.update_trade_mode_user(id, 'Switch')
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 callback.message.answer(f"✅ Изменено: {data_settings['trading_mode']} → Smart")
await rq.update_trade_mode_user(id, 'Smart') await rq.update_trade_mode_user(id, 'Smart')
await main_settings_message(id, callback.message) await main_settings_message(id, callback.message)
await state.clear() await state.clear()
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
@@ -91,45 +135,27 @@ async def switch_mode_enabled_message(message, state):
await state.set_state(update_main_settings.switch_mode_enabled) await state.set_state(update_main_settings.switch_mode_enabled)
await message.edit_text( await message.edit_text(
"""<b>Свитч</b> — динамическое переключение между торговыми режимами для максимизации эффективности. f"""<b> Состояние свитча</b>
<b>По направлению</b> - по направлению последней сделки предыдущей серии
<b>Против направления</b> - против направления последней сделки предыдущей серии
<em>По умолчанию при первом запуске бота, направление сделки установлено на "Buy".</em>
<em>Выберите ниже для изменений:</em>""", parse_mode='html', <em>Выберите ниже для изменений:</em>""", parse_mode='html',
reply_markup=inline_markup.buttons_on_off_markup_for_switch) reply_markup=inline_markup.switch_state_markup)
@router_main_settings.callback_query(lambda c: c.data in ["clb_on_switch", "clb_off_switch"])
async def state_switch_mode_enabled(callback: CallbackQuery, state):
await callback.answer()
tg_id = callback.from_user.id
val = "Включить" if callback.data == "clb_on_switch" else "Выключить"
if val == "Включить":
await rq.update_switch_mode_enabled(tg_id, "Включено")
await callback.answer(f"Включено")
await main_settings_message(tg_id, callback.message)
else:
await rq.update_switch_mode_enabled(tg_id, "Выключено")
await callback.answer(f"Выключено")
await main_settings_message(tg_id, callback.message)
await state.clear()
@router_main_settings.callback_query(lambda c: c.data in ["clb_switch_state"])
async def state_switch_mode_enabled(callback: CallbackQuery):
await callback.answer()
await callback.message.answer("Выберите состояние свитча:", reply_markup=inline_markup.switch_state_markup)
@router_main_settings.callback_query(lambda c: c.data in ["clb_long_switch", "clb_short_switch"]) @router_main_settings.callback_query(lambda c: c.data in ["clb_long_switch", "clb_short_switch"])
async def state_switch_mode_enabled(callback: CallbackQuery, state): async def state_switch_mode_enabled(callback: CallbackQuery, state):
await callback.answer() await callback.answer()
tg_id = callback.from_user.id tg_id = callback.from_user.id
val = "Long" if callback.data == "clb_long_switch" else "Short" val = "По направлению" if callback.data == "clb_long_switch" else "Против направления"
if val == "Long": if val == "По направлению":
await rq.update_switch_state(tg_id, "Long") await rq.update_switch_state(tg_id, "По направлению")
await callback.message.answer(f"Состояние свитча: {val}") await callback.message.answer(f"Состояние свитча: {val}")
await main_settings_message(tg_id, callback.message) await main_settings_message(tg_id, callback.message)
else: else:
await rq.update_switch_state(tg_id, "Short") await rq.update_switch_state(tg_id, "Против направления")
await callback.message.answer(f"Состояние свитча: {val}") await callback.message.answer(f"Состояние свитча: {val}")
await main_settings_message(tg_id, callback.message) await main_settings_message(tg_id, callback.message)
await state.clear() await state.clear()
@@ -144,23 +170,47 @@ async def size_leverage_message(message, state):
@router_main_settings.message(update_main_settings.size_leverage) @router_main_settings.message(update_main_settings.size_leverage)
async def state_size_leverage(message: Message, state): async def state_size_leverage(message: Message, state):
try:
leverage = float(message.text)
if leverage <= 0:
raise ValueError("Неверное значение")
except ValueError:
await message.answer(
"Ошибка: пожалуйста, введите положительное число для кредитного плеча."
"\nПопробуйте снова."
)
return
await state.update_data(size_leverage=message.text) await state.update_data(size_leverage=message.text)
data = await state.get_data() data = await state.get_data()
data_settings = await rq.get_user_main_settings(message.from_user.id) tg_id = message.from_user.id
symbol = await rq.get_symbol(tg_id)
leverage = data['size_leverage']
client = await get_bybit_client(tg_id)
if data['size_leverage'].isdigit() and int(data['size_leverage']) <= 100: instruments_resp = client.get_instruments_info(category="linear", symbol=symbol)
await message.answer(f"✅ Изменено: {data_settings['size_leverage']}{data['size_leverage']}") info = instruments_resp.get("result", {}).get("list", [])
await rq.update_size_leverange(message.from_user.id, data['size_leverage']) max_leverage = safe_float(info[0].get("leverageFilter", {}).get("maxLeverage", 0))
if safe_float(leverage) > max_leverage:
await message.answer(
f"Запрошенное кредитное плечо {leverage} превышает максимальное {max_leverage} для {symbol}. "
f"Устанавливаю максимальное.",
reply_markup=inline_markup.back_to_main,
)
logger.info(
f"Запрошенное кредитное плечо {leverage} превышает максимальное {max_leverage} для {symbol}. Устанавливаю максимальное.")
await rq.update_size_leverange(message.from_user.id, max_leverage)
await main_settings_message(message.from_user.id, message) await main_settings_message(message.from_user.id, message)
await state.clear() await state.clear()
else: else:
await message.answer( await message.answer(f"✅ Изменено: {leverage}")
f'⛔️ Ошибка: ваше значение ({data['size_leverage']}) или выше лимита (100) или вы вводите неверные символы') await rq.update_size_leverange(message.from_user.id, safe_float(leverage))
await main_settings_message(message.from_user.id, message) await main_settings_message(message.from_user.id, message)
await state.clear()
async def martingale_factor_message(message, state): async def martingale_factor_message(message, state):
@@ -213,45 +263,55 @@ async def margin_type_message(message, state):
@router_main_settings.callback_query(update_main_settings.margin_type) @router_main_settings.callback_query(update_main_settings.margin_type)
async def state_margin_type(callback: CallbackQuery, state): async def state_margin_type(callback: CallbackQuery, state):
tg_id = callback.from_user.id callback_data = callback.data
api_key = await rq.get_bybit_api_key(tg_id) if callback_data in ['margin_type_isolated', 'margin_type_cross']:
secret_key = await rq.get_bybit_secret_key(tg_id) tg_id = callback.from_user.id
data_settings = await rq.get_user_main_settings(tg_id) api_key = await rq.get_bybit_api_key(tg_id)
client = HTTP(api_key=api_key, api_secret=secret_key) secret_key = await rq.get_bybit_secret_key(tg_id)
try: data_settings = await rq.get_user_main_settings(tg_id)
active_positions = client.get_positions(category='linear', settleCoin='USDT') symbol = await rq.get_symbol(tg_id)
client = HTTP(api_key=api_key, api_secret=secret_key)
try:
active_positions = client.get_positions(category='linear', settleCoin="USDT")
positions = active_positions.get('result', {}).get('list', []) positions = active_positions.get('result', {}).get('list', [])
except Exception as e: except Exception as e:
logger.error(f"error: {e}") logger.error(f"error: {e}")
positions = [] positions = []
for pos in positions: for pos in positions:
size = pos.get('size') size = pos.get('size')
if float(size) > 0: if float(size) > 0:
await callback.answer( await callback.answer(
"⚠️ Маржинальный режим нельзя менять при открытой позиции", "⚠️ Маржинальный режим нельзя менять при открытой позиции"
show_alert=True )
) return
return
try:
match callback.data:
case 'margin_type_isolated':
await callback.message.answer(f"✅ Изменено: {data_settings['margin_type']} → Isolated")
await rq.update_margin_type(tg_id, 'Isolated') try:
await main_settings_message(tg_id, callback.message) match callback.data:
case 'margin_type_isolated':
await callback.answer()
await callback.message.answer(f"✅ Изменено: {data_settings['margin_type']} → Isolated")
await state.clear() await rq.update_margin_type(tg_id, 'Isolated')
case 'margin_type_cross': await main_settings_message(tg_id, callback.message)
await callback.message.answer(f"✅ Изменено: {data_settings['margin_type']} → Cross")
await rq.update_margin_type(tg_id, 'Cross') await state.clear()
await main_settings_message(tg_id, callback.message) case 'margin_type_cross':
await callback.answer()
await callback.message.answer(f"✅ Изменено: {data_settings['margin_type']} → Cross")
await state.clear() await rq.update_margin_type(tg_id, 'Cross')
except Exception as e: await main_settings_message(tg_id, callback.message)
logger.error(f"error: {e}")
await state.clear()
except Exception as e:
logger.error(f"error: {e}")
else:
await callback.answer()
await main_settings_message(callback.from_user.id, callback.message)
await state.clear()
async def starting_quantity_message(message, state): async def starting_quantity_message(message, state):

View File

@@ -1,7 +1,7 @@
import logging.config import logging.config
from aiogram import F, Router from aiogram import F, Router
from aiogram.filters import CommandStart from aiogram.filters import CommandStart, Command
from aiogram.types import Message, CallbackQuery from aiogram.types import Message, CallbackQuery
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
@@ -23,7 +23,7 @@ logger = logging.getLogger("handlers")
router = Router() router = Router()
@router.message(Command("start"))
@router.message(CommandStart()) @router.message(CommandStart())
async def start_message(message: Message) -> None: async def start_message(message: Message) -> None:
""" """
@@ -37,6 +37,7 @@ async def start_message(message: Message) -> None:
await func.start_message(message) await func.start_message(message)
@router.message(Command("profile"))
@router.message(F.text == "👤 Профиль") @router.message(F.text == "👤 Профиль")
async def profile_message(message: Message) -> None: async def profile_message(message: Message) -> None:
""" """
@@ -52,6 +53,12 @@ async def profile_message(message: Message) -> None:
if user and balance: if user and balance:
await run_ws_for_user(tg_id, message) await run_ws_for_user(tg_id, message)
await func.profile_message(message.from_user.username, message) await func.profile_message(message.from_user.username, message)
else:
await rq.save_tg_id_new_user(message.from_user.id)
await func_main_settings.reg_new_user_default_main_settings(message.from_user.id, message)
await func_rmanagement_settings.reg_new_user_default_risk_management_settings(message.from_user.id, message)
await func_condition_settings.reg_new_user_default_condition_settings(message.from_user.id)
await func_additional_settings.reg_new_user_default_additional_settings(message.from_user.id, message)
@router.callback_query(F.data == "clb_start_chatbot_message") @router.callback_query(F.data == "clb_start_chatbot_message")
@@ -64,6 +71,8 @@ async def clb_profile_msg(callback: CallbackQuery) -> None:
Args: Args:
callback (CallbackQuery): Полученный колбэк. callback (CallbackQuery): Полученный колбэк.
""" """
tg_id = callback.from_user.id
message = callback.message
user = await rq.check_user(callback.from_user.id) user = await rq.check_user(callback.from_user.id)
balance = await get_balance(callback.from_user.id, callback.message) balance = await get_balance(callback.from_user.id, callback.message)
first_name = callback.from_user.first_name or "" first_name = callback.from_user.first_name or ""
@@ -71,6 +80,7 @@ async def clb_profile_msg(callback: CallbackQuery) -> None:
username = f"{first_name} {last_name}".strip() or callback.from_user.username or "Пользователь" username = f"{first_name} {last_name}".strip() or callback.from_user.username or "Пользователь"
if user and balance: if user and balance:
await run_ws_for_user(tg_id, message)
await func.profile_message(callback.from_user.username, callback.message) await func.profile_message(callback.from_user.username, callback.message)
else: else:
await rq.save_tg_id_new_user(callback.from_user.id) await rq.save_tg_id_new_user(callback.from_user.id)
@@ -164,7 +174,7 @@ async def clb_change_additional_message(callback: CallbackQuery) -> None:
# Конкретные настройки каталогов # Конкретные настройки каталогов
list_main_settings = ['clb_change_trading_mode', list_main_settings = ['clb_change_trading_mode',
'clb_change_switch_mode', 'clb_change_switch_state',
'clb_change_margin_type', 'clb_change_margin_type',
'clb_change_size_leverage', 'clb_change_size_leverage',
'clb_change_starting_quantity', 'clb_change_starting_quantity',
@@ -188,7 +198,7 @@ async def clb_main_settings_msg(callback: CallbackQuery, state: FSMContext) -> N
match callback.data: match callback.data:
case 'clb_change_trading_mode': case 'clb_change_trading_mode':
await func_main_settings.trading_mode_message(callback.message, state) await func_main_settings.trading_mode_message(callback.message, state)
case 'clb_change_switch_mode': case 'clb_change_switch_state':
await func_main_settings.switch_mode_enabled_message(callback.message, state) await func_main_settings.switch_mode_enabled_message(callback.message, state)
case 'clb_change_margin_type': case 'clb_change_margin_type':
await func_main_settings.margin_type_message(callback.message, state) await func_main_settings.margin_type_message(callback.message, state)