2
0
forked from kodorvan/stcs

Added the ability to open a deal at a trigger price

This commit is contained in:
algizn97
2025-09-19 14:42:46 +05:00
parent 887b46c1d4
commit 2fb8cb4acb
2 changed files with 66 additions and 91 deletions

View File

@@ -1,6 +1,7 @@
import asyncio import asyncio
import logging.config import logging.config
import time import time
import json
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.services.Bybit.functions.price_symbol as price_symbol
import app.telegram.database.requests as rq import app.telegram.database.requests as rq
@@ -106,6 +107,7 @@ def format_order_details_position(data):
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", "")
trigger_price = msg.get("triggerPrice", "N/A")
movement = "" movement = ""
if side.lower() == "buy": if side.lower() == "buy":
@@ -149,6 +151,28 @@ def format_order_details_position(data):
f"Движение: {movement}\n" f"Движение: {movement}\n"
) )
return text return text
elif order_status.lower() == "untriggered":
text = (
f"Условный ордер создан:\n"
f"Торговая пара: {symbol}\n"
f"Цена: {price:.6f}\n"
f"Триггер цена: {trigger_price}\n"
f"Количество: {qty}\n"
f"Тип ордера: {order_type}\n"
f"Движение: {movement}\n"
)
return text
elif order_status.lower() == "deactivated":
text = (
f"Условный ордер отменен:\n"
f"Торговая пара: {symbol}\n"
f"Цена: {price:.6f}\n"
f"Триггер цена: {trigger_price}\n"
f"Количество: {qty}\n"
f"Тип ордера: {order_type}\n"
f"Движение: {movement}\n"
)
return text
return None return None
@@ -200,6 +224,7 @@ async def handle_execution_message(message, msg):
""" """
tg_id = message.from_user.id tg_id = message.from_user.id
data = msg.get("data", [{}])[0] data = msg.get("data", [{}])[0]
logger.info("исполнео:", json.dumps(msg, indent=4))
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)
@@ -275,7 +300,7 @@ async def handle_order_message(message, msg: dict) -> None:
Обработчик сообщений об исполнении ордера. Обработчик сообщений об исполнении ордера.
Логирует событие и проверяет условия для мартингейла и TP. Логирует событие и проверяет условия для мартингейла и TP.
""" """
# logger.info(f"Исполнен ордер:\n{json.dumps(msg, indent=4, ensure_ascii=False)}") logger.info(f"Исполнен ордер:\n{json.dumps(msg, indent=4, ensure_ascii=False)}")
trade_info = format_order_details_position(msg) trade_info = format_order_details_position(msg)
@@ -322,7 +347,7 @@ async def open_position(
bybit_margin_mode = ( bybit_margin_mode = (
"ISOLATED_MARGIN" if margin_mode == "Isolated" else "REGULAR_MARGIN" "ISOLATED_MARGIN" if margin_mode == "Isolated" else "REGULAR_MARGIN"
) )
trigger_price = await rq.get_trigger_price(tg_id)
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)
@@ -426,18 +451,33 @@ async def open_position(
if bybit_margin_mode == "ISOLATED_MARGIN": if bybit_margin_mode == "ISOLATED_MARGIN":
# Открываем позицию # Открываем позицию
if trigger_price and float(trigger_price) > 0:
response = client.place_order(
category="linear",
symbol=symbol,
side=side,
orderType="Stop" if order_type == "Conditional" else order_type,
qty=str(quantity),
price=(str(limit_price) if order_type == "Limit" and limit_price else None),
triggerPrice=str(trigger_price),
triggerBy="LastPrice",
triggerDirection=2 if side == "Buy" else 1,
timeInForce="GTC",
orderLinkId=f"deal_{symbol}_{int(time.time())}",
)
else:
# Обычный ордер, без триггера
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=( price=(str(limit_price) if order_type == "Limit" and limit_price else None),
str(limit_price) if order_type == "Limit" and limit_price else None
),
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
if response.get("retCode", -1) != 0: if response.get("retCode", -1) != 0:
@@ -544,6 +584,9 @@ async def open_position(
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,
triggerPrice=str(trigger_price) if trigger_price and float(trigger_price) > 0 else None,
triggerBy="LastPrice" if trigger_price and float(trigger_price) > 0 else None,
triggerDirection=2 if side == "Buy" else 1 if trigger_price and float(trigger_price) > 0 else None,
timeInForce="GTC", timeInForce="GTC",
orderLinkId=f"deal_{symbol}_{int(time.time())}", orderLinkId=f"deal_{symbol}_{int(time.time())}",
) )
@@ -724,20 +767,20 @@ 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( response = client.get_open_orders(
category="linear", settleCoin="USDT", orderType="Limit" category="linear", settleCoin="USDT", orderType="Limit" and "Market"
) )
orders = response.get("result", {}).get("list", []) orders = response.get("result", {}).get("list", [])
limit_orders = [order for order in orders if order.get("orderType") == "Limit"] limit_orders = [order for order in orders]
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( await message.answer(
"❗️ У вас нет активных лимитных ордеров.", "❗️ У вас нет активных ордеров.",
reply_markup=inline_markup.back_to_main, reply_markup=inline_markup.back_to_main,
) )
return return
@@ -752,12 +795,12 @@ async def get_active_orders_by_symbol(tg_id, symbol, message):
limit_orders = [ limit_orders = [
order order
for order in active_orders.get("result", {}).get("list", []) for order in active_orders.get("result", {}).get("list", [])
if order.get("orderType") == "Limit"
] ]
logger.info(limit_orders)
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
@@ -769,9 +812,8 @@ async def get_active_orders_by_symbol(tg_id, symbol, message):
f"Тип ордера: {order.get('orderType')}\n" f"Тип ордера: {order.get('orderType')}\n"
f"Сторона: {order.get('side')}\n" f"Сторона: {order.get('side')}\n"
f"Цена: {order.get('price')}\n" f"Цена: {order.get('price')}\n"
f"Триггер цена: {order.get('triggerPrice')}\n"
f"Количество: {order.get('qty')}\n" f"Количество: {order.get('qty')}\n"
f"Тейк-профит: {order.get('takeProfit')}\n"
f"Стоп-лосс: {order.get('stopLoss')}\n"
) )
texts.append(text) texts.append(text)

View File

@@ -10,7 +10,6 @@ from app.services.Bybit.functions.Futures import (close_user_trade, set_take_pro
get_active_positions_by_symbol, get_active_orders_by_symbol, get_active_positions_by_symbol, get_active_orders_by_symbol,
get_active_positions, get_active_orders, cancel_all_tp_sl_orders, get_active_positions, get_active_orders, cancel_all_tp_sl_orders,
open_position, close_trade_after_delay, safe_float, open_position, close_trade_after_delay, safe_float,
calculate_total_budget, get_bybit_client,
) )
from app.services.Bybit.functions.balance import get_balance from app.services.Bybit.functions.balance import get_balance
import app.telegram.Keyboards.inline_keyboards as inline_markup import app.telegram.Keyboards.inline_keyboards as inline_markup
@@ -18,9 +17,9 @@ import app.telegram.Keyboards.inline_keyboards as inline_markup
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.price_symbol import get_price
import app.services.Bybit.functions.balance as balance_g # import app.services.Bybit.functions.balance as balance_g
from app.states.States import (state_update_entry_type, state_update_symbol, state_limit_price, from app.states.States import (state_update_symbol,
SetTP_SL_State, CloseTradeTimerState) SetTP_SL_State, CloseTradeTimerState)
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
@@ -121,72 +120,6 @@ async def update_symbol_for_trade(message: Message, state: FSMContext) -> None:
await state.clear() await state.clear()
@router_functions_bybit_trade.callback_query(F.data == 'clb_update_entry_type')
async def update_entry_type_message(callback: CallbackQuery, state: FSMContext) -> None:
"""
Запрашивает у пользователя тип входа в позицию (Market или Limit).
"""
await state.set_state(state_update_entry_type.entry_type)
await callback.message.answer("Выберите тип входа в позицию:", reply_markup=inline_markup.entry_order_type_markup)
await callback.answer()
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith('entry_order_type:'))
async def entry_order_type_callback(callback: CallbackQuery, state: FSMContext) -> None:
"""
Обработка выбора типа входа в позицию.
Если Limit, запрашивает цену лимитного ордера.
Если Market — обновляет настройки.
"""
order_type = callback.data.split(':')[1]
if order_type not in ['Market', 'Limit']:
await callback.answer("Ошибка выбора", show_alert=True)
return
if order_type == 'Limit':
await state.set_state(state_limit_price.price)
await callback.message.answer("Введите цену:", reply_markup=inline_markup.cancel)
await callback.answer()
return
try:
await state.update_data(entry_order_type=order_type)
await rq.update_entry_order_type(callback.from_user.id, order_type)
await callback.message.answer("Выбран тип входа в позицию по текущей цене:",
reply_markup=inline_markup.start_trading_markup)
await callback.answer()
except Exception as e:
logger.error("Произошла ошибка при обновлении типа входа в позицию: %s", e)
await callback.message.answer("Произошла ошибка при обновлении типа входа в позицию",
reply_markup=inline_markup.back_to_main)
await state.clear()
@router_functions_bybit_trade.message(state_limit_price.price)
async def set_limit_price(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод цены лимитного ордера, проверяет формат и сохраняет настройки.
"""
try:
price = float(message.text)
if price <= 0:
await message.answer("Цена должна быть положительным числом. Попробуйте снова.",
reply_markup=inline_markup.cancel)
return
except ValueError:
await message.answer("Некорректный формат цены. Введите число.", reply_markup=inline_markup.cancel)
return
await state.update_data(entry_order_type='Limit', limit_price=price)
await rq.update_entry_order_type(message.from_user.id, 'Limit')
await rq.update_limit_price(message.from_user.id, price)
await message.answer(f"Триггер цена установлена: {price}", reply_markup=inline_markup.start_trading_markup)
await state.clear()
@router_functions_bybit_trade.callback_query(F.data == "clb_start_chatbot_trading") @router_functions_bybit_trade.callback_query(F.data == "clb_start_chatbot_trading")
async def start_trading_process(callback: CallbackQuery) -> None: async def start_trading_process(callback: CallbackQuery) -> None:
""" """