2
0
forked from kodorvan/stcs

The trading mode has been moved to the main settings, Position mode, limit order and conditional order have been removed. The number of bids has been renamed to the base rate. The choice of the direction of the first transaction has been moved to the main settings

This commit is contained in:
algizn97
2025-10-10 13:18:43 +05:00
parent 09606a057b
commit ebe2d58975
2 changed files with 25 additions and 361 deletions

View File

@@ -7,8 +7,8 @@ from aiogram.types import CallbackQuery
import app.telegram.keyboards.inline as kbi import app.telegram.keyboards.inline as kbi
import database.request as rq import database.request as rq
from app.bybit import get_bybit_client from app.bybit import get_bybit_client
from app.bybit.get_functions.get_tickers import get_tickers
from app.helper_functions import calculate_total_budget, get_base_currency, safe_float from app.helper_functions import calculate_total_budget, safe_float
from logger_helper.logger_helper import LOGGING_CONFIG from logger_helper.logger_helper import LOGGING_CONFIG
logging.config.dictConfig(LOGGING_CONFIG) logging.config.dictConfig(LOGGING_CONFIG)
@@ -40,77 +40,30 @@ async def additional_settings(callback_query: CallbackQuery, state: FSMContext)
return return
trade_mode_map = { trade_mode_map = {
"Merged_Single": "Односторонний режим", "Long": "Лонг",
"Both_Sides": "Хеджирование", "Short": "Шорт",
"Switch": "Свитч",
} }
margin_type_map = { margin_type_map = {
"ISOLATED_MARGIN": "Изолированная", "ISOLATED_MARGIN": "Изолированная",
"REGULAR_MARGIN": "Кросс", "REGULAR_MARGIN": "Кросс",
} }
order_type_map = {"Market": "Рыночный", "Limit": "Лимитный"}
trade_mode = additional_data.trade_mode or "" trade_mode = additional_data.trade_mode or ""
margin_type = additional_data.margin_type or "" margin_type = additional_data.margin_type or ""
order_type = additional_data.order_type or ""
trade_mode_rus = trade_mode_map.get(trade_mode, trade_mode) trade_mode_rus = trade_mode_map.get(trade_mode, trade_mode)
margin_type_rus = margin_type_map.get(margin_type, margin_type) margin_type_rus = margin_type_map.get(margin_type, margin_type)
order_type_rus = order_type_map.get(order_type, "Условный") switch_side = additional_data.switch_side
def f(x): def f(x):
return safe_float(x) return safe_float(x)
leverage = f(additional_data.leverage) leverage = f(additional_data.leverage)
leverage_to_buy = f(additional_data.leverage_to_buy)
leverage_to_sell = f(additional_data.leverage_to_sell)
martingale = f(additional_data.martingale_factor) martingale = f(additional_data.martingale_factor)
max_bets = additional_data.max_bets_in_series max_bets = additional_data.max_bets_in_series
quantity = f(additional_data.order_quantity) quantity = f(additional_data.order_quantity)
limit_price = f(additional_data.limit_price)
trigger_price = f(additional_data.trigger_price) or 0 trigger_price = f(additional_data.trigger_price) or 0
tickers = await get_tickers(tg_id=tg_id, symbol=symbol)
price_symbol = safe_float(tickers.get("lastPrice")) or 0
bid = f(tickers.get("bid1Price")) or 0
ask = f(tickers.get("ask1Price")) or 0
sym = get_base_currency(symbol)
if trade_mode == "Merged_Single":
leverage_str = f"{leverage:.2f}x"
else:
if margin_type == "ISOLATED_MARGIN":
leverage_str = f"{leverage_to_buy:.2f}x:{leverage_to_sell:.2f}x"
else:
leverage_str = f"{leverage:.2f}x"
conditional_order_type = additional_data.conditional_order_type or ""
conditional_order_type_rus = (
"Лимитный"
if conditional_order_type == "Limit"
else (
"Рыночный"
if conditional_order_type == "Market"
else conditional_order_type
)
)
conditional_order_type_text = (
f"- Тип условного ордера: {conditional_order_type_rus}\n"
if order_type == "Conditional"
else ""
)
limit_price_text = ""
trigger_price_text = ""
if order_type == "Limit":
limit_price_text = f"- Цена лимитного ордера: {limit_price:.4f} USDT\n"
elif order_type == "Conditional":
if conditional_order_type == "Limit":
limit_price_text = f"- Цена лимитного ордера: {limit_price:.4f} USDT\n"
trigger_price_text = f"- Триггер цена: {trigger_price:.4f} USDT\n"
risk_management_data = await rq.get_user_risk_management(tg_id=tg_id) risk_management_data = await rq.get_user_risk_management(tg_id=tg_id)
commission_fee = risk_management_data.commission_fee commission_fee = risk_management_data.commission_fee
client = await get_bybit_client(tg_id=tg_id) client = await get_bybit_client(tg_id=tg_id)
@@ -124,54 +77,32 @@ async def additional_settings(callback_query: CallbackQuery, state: FSMContext)
else: else:
commission_fee_percent = 0.0 commission_fee_percent = 0.0
if order_type == "Conditional": switch_side_mode = ""
if conditional_order_type == "Limit": if trade_mode == "Switch":
entry_price = limit_price switch_side_mode = f"- Направление первой сделки: {switch_side}\n"
ask_price = limit_price
bid_price = limit_price
else:
ask_price = trigger_price
bid_price = trigger_price
entry_price = trigger_price
else:
if order_type == "Limit":
entry_price = limit_price
ask_price = limit_price
bid_price = limit_price
else:
entry_price = price_symbol
ask_price = ask
bid_price = bid
durability_buy = quantity * bid_price quantity_price = quantity * trigger_price
durability_sell = quantity * ask_price
quantity_price = quantity * entry_price
total_commission = quantity_price * commission_fee_percent total_commission = quantity_price * commission_fee_percent
total_budget = await calculate_total_budget( total_budget = await calculate_total_budget(
quantity=durability_buy, quantity=quantity,
martingale_factor=martingale, martingale_factor=martingale,
max_steps=max_bets, max_steps=max_bets,
commission_fee_percent=total_commission, commission_fee_percent=total_commission,
) )
text = ( text = (
f"Основные настройки:\n\n" f"Основные настройки:\n\n"
f"- Режим позиции: {trade_mode_rus}\n" f"- Режим торговли: {trade_mode_rus}\n"
f"{switch_side_mode}"
f"- Тип маржи: {margin_type_rus}\n" f"- Тип маржи: {margin_type_rus}\n"
f"- Размер кредитного плеча: {leverage_str}\n" f"- Размер кредитного плеча: {leverage:.2f}\n"
f"- Тип ордера: {order_type_rus}\n" f"- Базовая ставка: {quantity} USDT\n"
f"- Количество ордера: {quantity} {sym}\n"
f"- Коэффициент мартингейла: {martingale:.2f}\n" f"- Коэффициент мартингейла: {martingale:.2f}\n"
f"{conditional_order_type_text}" f"- Триггер цена: {trigger_price:.4f} USDT\n"
f"{trigger_price_text}"
f"{limit_price_text}"
f"- Максимальное кол-во ставок в серии: {max_bets}\n\n" f"- Максимальное кол-во ставок в серии: {max_bets}\n\n"
f"- Стоимость: {durability_buy:.2f}/{durability_sell:.2f} USDT\n" f"- Бюджет серии: {total_budget:.4f} USDT\n"
f"- Рекомендуемый бюджет: {total_budget:.4f} USDT\n"
) )
keyboard = kbi.get_additional_settings_keyboard( keyboard = kbi.get_additional_settings_keyboard(mode=trade_mode)
current_order_type=order_type, conditional_order=conditional_order_type
)
await callback_query.message.edit_text(text=text, reply_markup=keyboard) await callback_query.message.edit_text(text=text, reply_markup=keyboard)
logger.debug( logger.debug(
"Command additional_settings processed successfully for user: %s", tg_id "Command additional_settings processed successfully for user: %s", tg_id
@@ -202,7 +133,6 @@ async def risk_management(callback_query: CallbackQuery, state: FSMContext) -> N
if risk_management_data: if risk_management_data:
take_profit_percent = risk_management_data.take_profit_percent or "" take_profit_percent = risk_management_data.take_profit_percent or ""
stop_loss_percent = risk_management_data.stop_loss_percent or "" stop_loss_percent = risk_management_data.stop_loss_percent or ""
max_risk_percent = risk_management_data.max_risk_percent or ""
commission_fee = risk_management_data.commission_fee or "" commission_fee = risk_management_data.commission_fee or ""
commission_fee_rus = ( commission_fee_rus = (
"Да" if commission_fee == "Yes_commission_fee" else "Нет" "Да" if commission_fee == "Yes_commission_fee" else "Нет"
@@ -212,7 +142,6 @@ async def risk_management(callback_query: CallbackQuery, state: FSMContext) -> N
text=f"Риск-менеджмент:\n\n" text=f"Риск-менеджмент:\n\n"
f"- Процент изменения цены для фиксации прибыли: {take_profit_percent}%\n" f"- Процент изменения цены для фиксации прибыли: {take_profit_percent}%\n"
f"- Процент изменения цены для фиксации убытка: {stop_loss_percent}%\n\n" f"- Процент изменения цены для фиксации убытка: {stop_loss_percent}%\n\n"
f"- Максимальный риск на сделку (в % от баланса): {max_risk_percent}%\n\n"
f"- Комиссия биржи для расчета прибыли: {commission_fee_rus}\n\n", f"- Комиссия биржи для расчета прибыли: {commission_fee_rus}\n\n",
reply_markup=kbi.risk_management, reply_markup=kbi.risk_management,
) )

View File

@@ -12,9 +12,7 @@ from app.bybit.open_positions import start_trading_cycle
from app.helper_functions import safe_float from app.helper_functions import safe_float
from app.telegram.tasks.tasks import ( from app.telegram.tasks.tasks import (
add_start_task_merged, add_start_task_merged,
add_start_task_switch, cancel_start_task_merged
cancel_start_task_merged,
cancel_start_task_switch,
) )
from logger_helper.logger_helper import LOGGING_CONFIG from logger_helper.logger_helper import LOGGING_CONFIG
@@ -35,97 +33,20 @@ async def start_trading(callback_query: CallbackQuery, state: FSMContext) -> Non
""" """
try: try:
await state.clear() await state.clear()
additional_data = await rq.get_user_additional_settings(
tg_id=callback_query.from_user.id
)
trade_mode = additional_data.trade_mode
symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id) symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id)
deals = await get_active_positions_by_symbol( deals = await get_active_positions_by_symbol(
tg_id=callback_query.from_user.id, symbol=symbol tg_id=callback_query.from_user.id, symbol=symbol
) )
position = next((d for d in deals if d.get("symbol") == symbol), None) position = next((d for d in deals if d.get("symbol") == symbol), None)
if position: if position:
size = position.get("size", 0) size = position.get("size", 0)
position_idx = position.get("positionIdx")
else: else:
size = 0 size = 0
position_idx = None
if position_idx != 0 and safe_float(size) > 0 and trade_mode == "Merged_Single": if safe_float(size) > 0:
await callback_query.answer( await callback_query.answer(
text="У вас есть активная позиция в режиме хеджирования. " text="У вас есть активная позиция",
"Открытие сделки в одностороннем режиме невозможно.",
)
return
if position_idx == 0 and safe_float(size) > 0 and trade_mode == "Both_Sides":
await callback_query.answer(
text="У вас есть активная позиция в одностороннем режиме. "
"Открытие сделки в режиме хеджирования невозможно.",
)
return
if trade_mode == "Merged_Single":
await callback_query.message.edit_text(
text="Выберите режим торговли:\n\n"
"Лонг - все сделки серии открываются на покупку.\n"
"Шорт - все сделки серии открываются на продажу.\n"
"Свитч - направление каждой сделки серии меняется по переменно.\n",
reply_markup=kbi.merged_start_trading,
)
else: # trade_mode == "Both_Sides":
await callback_query.message.edit_text(
text="Выберите режим торговли:\n\n"
"Лонг - все сделки открываются на покупку.\n"
"Шорт - все сделки открываются на продажу.\n",
reply_markup=kbi.both_start_trading,
)
logger.debug(
"Command start_trading processed successfully for user: %s",
callback_query.from_user.id,
)
except Exception as e:
await callback_query.answer(text="Произошла ошибка при запуске торговли")
logger.error(
"Error processing command start_trading for user %s: %s",
callback_query.from_user.id,
e,
)
@router_start_trading.callback_query(lambda c: c.data == "long" or c.data == "short")
async def start_trading_long(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Handles the "long" or "short" callback query.
Clears the FSM state and starts the trading cycle.
:param callback_query: Message
:param state: FSMContext
:return: None
"""
try:
if callback_query.data == "long":
side = "Buy"
elif callback_query.data == "short":
side = "Sell"
else:
await callback_query.answer(text="Произошла ошибка при запуске торговли")
return
symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id)
deals = await get_active_positions_by_symbol(
tg_id=callback_query.from_user.id, symbol=symbol
)
position = next((d for d in deals if d.get("symbol") == symbol), None)
if position:
size = position.get("size", 0)
position_idx = position.get("positionIdx")
else:
size = 0
position_idx = None
if position_idx == 0 and safe_float(size) > 0:
await callback_query.answer(
text="Торговля уже запущена в одностороннем режиме для данного инструмента"
) )
return return
@@ -151,12 +72,9 @@ async def start_trading_long(callback_query: CallbackQuery, state: FSMContext) -
tg_id=callback_query.from_user.id, tg_id=callback_query.from_user.id,
symbol=symbol, symbol=symbol,
auto_trading=True, auto_trading=True,
side=side,
) )
res = await start_trading_cycle( res = await start_trading_cycle(
tg_id=callback_query.from_user.id, tg_id=callback_query.from_user.id,
side=side,
switch_side_mode=False,
) )
error_messages = { error_messages = {
@@ -167,9 +85,10 @@ async def start_trading_long(callback_query: CallbackQuery, state: FSMContext) -
"ab not enough for new order": "Недостаточно средств для создания нового ордера", "ab not enough for new order": "Недостаточно средств для создания нового ордера",
"InvalidRequestError": "Произошла ошибка при запуске торговли.", "InvalidRequestError": "Произошла ошибка при запуске торговли.",
"Order does not meet minimum order value": "Сумма ордера не достаточна для запуска торговли", "Order does not meet minimum order value": "Сумма ордера не достаточна для запуска торговли",
"position idx not match position mode": "Торговля уже запущена в режиме хеджирования на продажу для данного инструмента", "position idx not match position mode": "Ошибка режима позиции для данного инструмента",
"Qty invalid": "Некорректное значение ордера для данного инструмента", "Qty invalid": "Некорректное значение ордера для данного инструмента",
"The number of contracts exceeds maximum limit allowed": "️️Количество контрактов превышает допустимое максимальное количество контрактов", "The number of contracts exceeds maximum limit allowed": "️️Количество контрактов превышает допустимое максимальное количество контрактов",
"The number of contracts exceeds minimum limit allowed": "️️Количество контрактов превышает допустимое минимальное количество контрактов",
} }
if res == "OK": if res == "OK":
@@ -180,7 +99,6 @@ async def start_trading_long(callback_query: CallbackQuery, state: FSMContext) -
tg_id=callback_query.from_user.id, tg_id=callback_query.from_user.id,
symbol=symbol, symbol=symbol,
auto_trading=False, auto_trading=False,
side=side,
) )
text = error_messages.get(res, "Произошла ошибка при запуске торговли") text = error_messages.get(res, "Произошла ошибка при запуске торговли")
await callback_query.message.edit_text( await callback_query.message.edit_text(
@@ -202,189 +120,8 @@ async def start_trading_long(callback_query: CallbackQuery, state: FSMContext) -
logger.error("Cancelled timer for user %s", callback_query.from_user.id) logger.error("Cancelled timer for user %s", callback_query.from_user.id)
@router_start_trading.callback_query(lambda c: c.data == "switch")
async def start_trading_switch(
callback_query: CallbackQuery, state: FSMContext
) -> None:
"""
Handles the "switch" callback query.
Clears the FSM state and sends a message to the user to select the switch side.
:param callback_query: Message
:param state: FSMContext
:return: None
"""
try:
await state.clear()
await callback_query.message.edit_text(
text="Выберите направление первой сделки серии:\n\n"
"Лонг - открывается первая сделка на покупку.\n"
"Шорт - открывается первая сделка на продажу.\n"
"По направлению - сделка открывается в направлении последней сделки предыдущей серии.\n"
"Противоположно - сделка открывается в противоположном направлении последней сделки предыдущей серии.\n",
reply_markup=kbi.switch_side,
)
except Exception as e:
await callback_query.answer(text="Произошла ошибка при запуске торговли")
logger.error(
"Error processing command start trading switch for user %s: %s",
callback_query.from_user.id,
e,
)
@router_start_trading.callback_query( @router_start_trading.callback_query(
lambda c: c.data lambda c: c.data == "cancel_timer_merged"
in {"switch_long", "switch_short", "switch_direction", "switch_opposite"}
)
async def start_switch(callback_query: CallbackQuery, state: FSMContext) -> None:
"""
Starts the trading cycle with the selected side.
:param callback_query:
:param state:
:return:
"""
try:
symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id)
user_deals_data = await rq.get_user_deal_by_symbol(
tg_id=callback_query.from_user.id, symbol=symbol
)
get_side = "Buy"
if user_deals_data:
get_side = user_deals_data.last_side or "Buy"
if callback_query.data == "switch_long":
side = "Buy"
elif callback_query.data == "switch_short":
side = "Sell"
elif callback_query.data == "switch_direction":
side = get_side
elif callback_query.data == "switch_opposite":
if get_side == "Buy":
side = "Sell"
else:
side = "Buy"
else:
await callback_query.answer(text="Произошла ошибка при запуске торговли")
return
deals = await get_active_positions_by_symbol(
tg_id=callback_query.from_user.id, symbol=symbol
)
position = next((d for d in deals if d.get("symbol") == symbol), None)
if position:
size = position.get("size", 0)
position_idx = position.get("positionIdx")
else:
size = 0
position_idx = None
if position_idx == 1 and safe_float(size) > 0 and side == "Buy":
await callback_query.answer(
text="Торговля уже запущена в режиме хеджирования на покупку для данного инструмента"
)
return
if position_idx == 2 and safe_float(size) > 0 and side == "Sell":
await callback_query.answer(
text="Торговля уже запущена в режиме хеджирования на продажу для данного инструмента"
)
return
conditional_data = await rq.get_user_conditional_settings(
tg_id=callback_query.from_user.id
)
timer_start = conditional_data.timer_start
cancel_start_task_switch(user_id=callback_query.from_user.id)
async def delay_start():
if timer_start > 0:
await callback_query.message.edit_text(
text=f"Торговля будет запущена с задержкой {timer_start} мин.",
reply_markup=kbi.cancel_timer_switch,
)
await rq.set_start_timer(
tg_id=callback_query.from_user.id, timer_start=0
)
await asyncio.sleep(timer_start * 60)
await rq.set_auto_trading(
tg_id=callback_query.from_user.id,
symbol=symbol,
auto_trading=True,
side=side,
)
if side == "Buy":
r_side = "Sell"
else:
r_side = "Buy"
await rq.set_auto_trading(
tg_id=callback_query.from_user.id,
symbol=symbol,
auto_trading=True,
side=r_side,
)
res = await start_trading_cycle(
tg_id=callback_query.from_user.id,
side=side,
switch_side_mode=True,
)
error_messages = {
"Limit price is out min price": "Цена лимитного ордера меньше минимального",
"Limit price is out max price": "Цена лимитного ордера больше максимального",
"Risk is too high for this trade": "Риск сделки превышает допустимый убыток",
"estimated will trigger liq": "Лимитный ордер может вызвать мгновенную ликвидацию. Проверьте параметры ордера.",
"ab not enough for new order": "Недостаточно средств для создания нового ордера",
"InvalidRequestError": "Произошла ошибка при запуске торговли.",
"Order does not meet minimum order value": "Сумма ордера не достаточна для запуска торговли",
"position idx not match position mode": "Торговля уже запущена в режиме хеджирования на продажу для данного инструмента",
"Qty invalid": "Некорректное значение ордера для данного инструмента",
"The number of contracts exceeds maximum limit allowed": " ️️Количество контрактов превышает допустимое максимальное количество контрактов",
}
if res == "OK":
await callback_query.message.edit_text(text="Торговля запущена")
await state.clear()
else:
await rq.set_auto_trading(
tg_id=callback_query.from_user.id,
symbol=symbol,
auto_trading=False,
side=side,
)
if side == "Buy":
r_side = "Sell"
else:
r_side = "Buy"
await rq.set_auto_trading(
tg_id=callback_query.from_user.id,
symbol=symbol,
auto_trading=False,
side=r_side,
)
text = error_messages.get(res, "Произошла ошибка при запуске торговли")
await callback_query.message.edit_text(
text=text, reply_markup=kbi.profile_bybit
)
await callback_query.message.edit_text("Запуск торговли...")
task = asyncio.create_task(delay_start())
await add_start_task_switch(user_id=callback_query.from_user.id, task=task)
except asyncio.CancelledError:
logger.error("Cancelled timer for user %s", callback_query.from_user.id)
except Exception as e:
await callback_query.answer(text="Произошла ошибка при запуске торговли")
logger.error(
"Error processing command start switch for user %s: %s",
callback_query.from_user.id,
e,
)
@router_start_trading.callback_query(
lambda c: c.data == "cancel_timer_merged" or c.data == "cancel_timer_switch"
) )
async def cancel_start_trading( async def cancel_start_trading(
callback_query: CallbackQuery, state: FSMContext callback_query: CallbackQuery, state: FSMContext
@@ -400,8 +137,6 @@ async def cancel_start_trading(
await state.clear() await state.clear()
if callback_query.data == "cancel_timer_merged": if callback_query.data == "cancel_timer_merged":
cancel_start_task_merged(user_id=callback_query.from_user.id) cancel_start_task_merged(user_id=callback_query.from_user.id)
elif callback_query.data == "cancel_timer_switch":
cancel_start_task_switch(user_id=callback_query.from_user.id)
await callback_query.message.edit_text( await callback_query.message.edit_text(
text="Запуск торговли отменен", reply_markup=kbi.profile_bybit text="Запуск торговли отменен", reply_markup=kbi.profile_bybit
) )