develop #3

Open
Alex wants to merge 77 commits from Alex/stcs:develop into stable
41 changed files with 3608 additions and 682 deletions
Showing only changes of commit e05b214a8a - Show all commits

View File

@@ -14,6 +14,8 @@ import app.telegram.Keyboards.inline_keyboards as inline_markup
logging.config.dictConfig(LOGGING_CONFIG) logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("futures") logger = logging.getLogger("futures")
processed_trade_ids = set()
def safe_float(val) -> float: def safe_float(val) -> float:
""" """
@@ -100,6 +102,13 @@ async def handle_execution_message(message, msg: dict) -> None:
""" """
# 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_id = msg.get('data', [{}])[0].get('orderId')
if trade_id in processed_trade_ids:
logger.info(f"Уже обработана сделка {trade_id}, дублирующее уведомление игнорируется")
return
processed_trade_ids.add(trade_id)
pnl = parse_pnl_from_msg(msg) pnl = parse_pnl_from_msg(msg)
tg_id = message.from_user.id tg_id = message.from_user.id
@@ -475,7 +484,7 @@ async def set_take_profit_stop_loss(tg_id: int, message, take_profit_price: floa
async def cancel_all_tp_sl_orders(tg_id, symbol): async def cancel_all_tp_sl_orders(tg_id, symbol):
""" """
Отменяет все открытые ордера TP/SL для указанного символа. Отменяет лимитные ордера для указанного символа.
""" """
api_key = await rq.get_bybit_api_key(tg_id) api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id) secret_key = await rq.get_bybit_secret_key(tg_id)
@@ -487,26 +496,49 @@ async def cancel_all_tp_sl_orders(tg_id, symbol):
for order in orders: for order in orders:
order_id = order.get('orderId') order_id = order.get('orderId')
order_symbol = order.get('symbol')
cancel_resp = client.cancel_order(category='linear', symbol=symbol, orderId=order_id) cancel_resp = client.cancel_order(category='linear', symbol=symbol, orderId=order_id)
last_response = cancel_resp
if cancel_resp.get('retCode') != 0: if cancel_resp.get('retCode') != 0:
logger.warning(f"Не удалось отменить ордер {order_id}: {cancel_resp.get('retMsg')}") logger.warning(f"Не удалось отменить ордер {order_id}: {cancel_resp.get('retMsg')}")
else:
last_response = order_symbol
except Exception as e: except Exception as e:
logger.error(f"Ошибка при отмене ордеров TP/SL: {e}") logger.error(f"Ошибка при отмене ордера: {e}")
return last_response return last_response
async def get_active_positions_by_symbol(tg_id, message): async def get_active_positions(tg_id, message):
"""
Показывает активные позиции пользователя.
"""
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
client = HTTP(api_key=api_key, api_secret=secret_key)
active_positions = client.get_positions(category='linear', settleCoin='USDT')
positions = active_positions.get('result', {}).get('list', [])
active_symbols = [pos.get('symbol') for pos in positions if float(pos.get('size', 0)) > 0]
if active_symbols:
await message.answer("📈 Ваши активные позиции:",
reply_markup=inline_markup.create_trades_inline_keyboard(active_symbols))
else:
await message.answer("❗️ У вас нет активных позиций.", reply_markup=inline_markup.back_to_main)
return
async def get_active_positions_by_symbol(tg_id, symbol, message):
""" """
Показывает активные позиции пользователя по символу. Показывает активные позиции пользователя по символу.
""" """
api_key = await rq.get_bybit_api_key(tg_id) api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id) secret_key = await rq.get_bybit_secret_key(tg_id)
client = HTTP(api_key=api_key, api_secret=secret_key) client = HTTP(api_key=api_key, api_secret=secret_key)
symbol = await rq.get_symbol(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
@@ -526,14 +558,33 @@ async def get_active_positions_by_symbol(tg_id, message):
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):
"""
Показывает активные лимитные ордера пользователя.
"""
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
client = HTTP(api_key=api_key, api_secret=secret_key)
async def get_active_orders_by_symbol(tg_id, message): response = client.get_open_orders(category='linear', settleCoin='USDT', orderType='Limit')
orders = response.get('result', {}).get('list', [])
limit_orders = [order for order in orders if order.get('orderType') == 'Limit']
if limit_orders:
symbols = [order['symbol'] for order in limit_orders]
await message.answer("📈 Ваши активные лимитные ордера:",
reply_markup=inline_markup.create_trades_inline_keyboard_limits(symbols))
else:
await message.answer("❗️ У вас нет активных лимитных ордеров.", reply_markup=inline_markup.back_to_main)
return
async def get_active_orders_by_symbol(tg_id, symbol, message):
""" """
Показывает активные лимитные ордера пользователя по символу. Показывает активные лимитные ордера пользователя по символу.
""" """
api_key = await rq.get_bybit_api_key(tg_id) api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id) secret_key = await rq.get_bybit_secret_key(tg_id)
symbol = await rq.get_symbol(tg_id)
client = HTTP(api_key=api_key, api_secret=secret_key) client = HTTP(api_key=api_key, api_secret=secret_key)
active_orders = client.get_open_orders(category='linear', symbol=symbol) active_orders = client.get_open_orders(category='linear', symbol=symbol)
@@ -561,7 +612,7 @@ async def get_active_orders_by_symbol(tg_id, message):
) )
texts.append(text) texts.append(text)
await message.answer("\n\n".join(texts), reply_markup=inline_markup.create_close_deal_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, message): async def close_user_trade(tg_id: int, symbol: str, message):
@@ -574,7 +625,6 @@ async def close_user_trade(tg_id: int, symbol: str, message):
secret_key = await rq.get_bybit_secret_key(tg_id) secret_key = await rq.get_bybit_secret_key(tg_id)
data_risk_stgs = await rq.get_user_risk_management_settings(tg_id) data_risk_stgs = await rq.get_user_risk_management_settings(tg_id)
limit_price = await rq.get_limit_price(tg_id)
include_fee = data_risk_stgs.get('commission_fee', 'Нет') == 'Да' include_fee = data_risk_stgs.get('commission_fee', 'Нет') == 'Да'
client = HTTP(api_key=api_key, api_secret=secret_key) client = HTTP(api_key=api_key, api_secret=secret_key)
@@ -589,66 +639,29 @@ async def close_user_trade(tg_id: int, symbol: str, message):
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')
entry_price = safe_float(position.get('avgPrice'))
if qty == 0: if qty == 0:
return False return False
orders = client.get_open_orders(category='linear', symbol=symbol)
cancel_resp = await cancel_all_tp_sl_orders(tg_id, symbol)
open_orders_list = orders.get('result', {}).get('list', [])
order_id = open_orders_list[0].get('orderId') if open_orders_list else None
close_side = "Sell" if side == "Buy" else "Buy" close_side = "Sell" if side == "Buy" else "Buy"
ticker_resp = client.get_tickers(category="linear", symbol=symbol) place_resp = client.place_order(
current_price = 0.0 category="linear",
if ticker_resp.get('retCode') == 0: symbol=symbol,
result = ticker_resp.get('result', {}) side=close_side,
ticker_list = [] orderType="Market",
if isinstance(result, dict): qty=str(qty),
ticker_list = result.get('list', []) timeInForce="GTC",
elif isinstance(result, list): reduceOnly=True
ticker_list = result )
if ticker_list:
current_price = float(ticker_list[0].get('lastPrice', 0.0))
place_resp = client.place_order(
category="linear",
symbol=symbol,
side=close_side,
orderType="Market",
qty=str(qty),
timeInForce="GTC",
reduceOnly=True
)
if place_resp.get('retCode', -1) == 0:
trade_fee = 0
try:
trades_resp = client.get_closed_pnl(category="linear", symbol=symbol)
if trades_resp.get('retCode') == 0:
trades = trades_resp.get('result', {}).get('list', [])
for trade in trades:
if trade.get('orderId') == order_id:
trade_fee += float(trade.get('execFee', 0))
except Exception as e:
logger.error(f"Ошибка при получении сделок: {e}")
trade_fee = 0
pnl = (current_price - entry_price) * qty if side == "Buy" else (entry_price - current_price) * qty
if include_fee:
pnl -= trade_fee
pnl_percent = (pnl / (entry_price * qty)) * 100 if entry_price * qty > 0 else 0
return True
else:
if message:
await message.answer(f"Ошибка закрытия сделки {symbol}.",
reply_markup=inline_markup.back_to_main)
return False
if place_resp.get('retCode') == 0:
await message.answer(f"Сделка {symbol} успешно закрыта.", reply_markup=inline_markup.back_to_main)
return True
else:
await message.answer(f"Ошибка закрытия сделки {symbol}.", reply_markup=inline_markup.back_to_main)
return False
except Exception as e: except Exception as e:
logger.error(f"Ошибка закрытия сделки {symbol} для пользователя {tg_id}: {e}", exc_info=True) logger.error(f"Ошибка закрытия сделки {symbol} для пользователя {tg_id}: {e}", exc_info=True)
if message: await message.answer("Произошла ошибка при закрытии сделки.", reply_markup=inline_markup.back_to_main)
await message.answer("Произошла ошибка при закрытии сделки.", reply_markup=inline_markup.back_to_main)
return False return False

View File

@@ -7,6 +7,7 @@ from logger_helper.logger_helper import LOGGING_CONFIG
from app.services.Bybit.functions.Futures import (close_user_trade, open_position, set_take_profit_stop_loss, \ from app.services.Bybit.functions.Futures import (close_user_trade, open_position, set_take_profit_stop_loss, \
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,
) )
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
@@ -264,8 +265,7 @@ async def show_my_trades(callback: CallbackQuery) -> None:
""" """
await callback.answer() await callback.answer()
try: try:
symbol = await rq.get_symbol(callback.from_user.id) await callback.message.answer(f"Выберите тип сделки:",
await callback.message.answer(f"Выберите тип сделки для пары {symbol}:",
reply_markup=inline_markup.my_deals_select_markup) reply_markup=inline_markup.my_deals_select_markup)
except Exception as e: except Exception as e:
logger.error(f"Произошла ошибка при выборе типа сделки: {e}") logger.error(f"Произошла ошибка при выборе типа сделки: {e}")
@@ -274,16 +274,30 @@ async def show_my_trades(callback: CallbackQuery) -> None:
@router_functions_bybit_trade.callback_query(F.data == "clb_open_deals") @router_functions_bybit_trade.callback_query(F.data == "clb_open_deals")
async def show_my_trades_callback(callback: CallbackQuery): async def show_my_trades_callback(callback: CallbackQuery):
""" """
Показывает открытые позиции пользователя по символу. Показывает открытые позиции пользователя.
""" """
await callback.answer() await callback.answer()
try: try:
await get_active_positions_by_symbol(callback.from_user.id, message=callback.message) await get_active_positions(callback.from_user.id, message=callback.message)
except Exception as e: except Exception as e:
logger.error(f"Произошла ошибка при выборе сделки: {e}") logger.error(f"Произошла ошибка при выборе сделки: {e}")
await callback.message.answer("Произошла ошибка при выборе сделки", reply_markup=inline_markup.back_to_main) await callback.message.answer("Произошла ошибка при выборе сделки", reply_markup=inline_markup.back_to_main)
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("show_deal_"))
async def show_deal_callback(callback_query: CallbackQuery) -> None:
"""
Показывает сделку пользователя по символу.
"""
await callback_query.answer()
try:
symbol = callback_query.data[len("show_deal_"):]
tg_id = callback_query.from_user.id
await get_active_positions_by_symbol(tg_id, symbol, message=callback_query.message)
except Exception as e:
logger.error(f"Произошла ошибка при выборе сделки: {e}")
await callback_query.message.answer("Произошла ошибка при выборе сделки", reply_markup=inline_markup.back_to_main)
@router_functions_bybit_trade.callback_query(F.data == "clb_open_orders") @router_functions_bybit_trade.callback_query(F.data == "clb_open_orders")
async def show_my_orders_callback(callback: CallbackQuery) -> None: async def show_my_orders_callback(callback: CallbackQuery) -> None:
@@ -293,16 +307,31 @@ async def show_my_orders_callback(callback: CallbackQuery) -> None:
await callback.answer() await callback.answer()
try: try:
await get_active_orders_by_symbol(callback.from_user.id, message=callback.message) await get_active_orders(callback.from_user.id, message=callback.message)
except Exception as e: except Exception as e:
logger.error(f"Произошла ошибка при выборе ордера: {e}") logger.error(f"Произошла ошибка при выборе ордера: {e}")
await callback.message.answer("Произошла ошибка при выборе ордера", reply_markup=inline_markup.back_to_main) await callback.message.answer("Произошла ошибка при выборе ордера", reply_markup=inline_markup.back_to_main)
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("show_limit_"))
async def show_limit_callback(callback_query: CallbackQuery) -> None:
"""
Показывает сделку пользователя по символу.
"""
await callback_query.answer()
try:
symbol = callback_query.data[len("show_limit_"):]
tg_id = callback_query.from_user.id
await get_active_orders_by_symbol(tg_id, symbol, message=callback_query.message)
except Exception as e:
logger.error(f"Произошла ошибка при выборе сделки: {e}")
await callback_query.message.answer("Произошла ошибка при выборе сделки", reply_markup=inline_markup.back_to_main)
@router_functions_bybit_trade.callback_query(F.data == "clb_set_tp_sl") @router_functions_bybit_trade.callback_query(F.data == "clb_set_tp_sl")
async def set_tp_sl(callback: CallbackQuery, state: FSMContext) -> None: async def set_tp_sl(callback: CallbackQuery, state: FSMContext) -> None:
""" """
Показывает активные ордера пользователя. Запускает процесс установки Take Profit и Stop Loss.
""" """
await callback.answer() await callback.answer()
await state.set_state(SetTP_SL_State.waiting_for_take_profit) await state.set_state(SetTP_SL_State.waiting_for_take_profit)
@@ -394,6 +423,25 @@ async def close_trade_callback(callback: CallbackQuery) -> None:
await callback.answer() await callback.answer()
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_limit:"))
async def close_trade_callback(callback: CallbackQuery) -> None:
"""
Закрывает ордера пользователя по символу.
"""
symbol = callback.data.split(':')[1]
tg_id = callback.from_user.id
result = await cancel_all_tp_sl_orders(tg_id, symbol)
if result:
await callback.message.answer(f"Ордер {result} успешно закрыт.", reply_markup=inline_markup.back_to_main)
logger.info(f"Ордер {result} успешно закрыт.")
else:
await callback.message.answer(f"Не удалось закрыть ордер {result}.")
await callback.answer()
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_deal_by_timer:")) @router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_deal_by_timer:"))
async def ask_close_delay(callback: CallbackQuery, state: FSMContext) -> None: async def ask_close_delay(callback: CallbackQuery, state: FSMContext) -> None:
""" """

View File

@@ -155,15 +155,21 @@ buttons_on_off_markup = InlineKeyboardMarkup(inline_keyboard=[ # ИЗМЕНИТ
my_deals_select_markup = InlineKeyboardMarkup(inline_keyboard=[ my_deals_select_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Открытые сделки', callback_data="clb_open_deals"), [InlineKeyboardButton(text='Открытые сделки', callback_data="clb_open_deals"),
InlineKeyboardButton(text='Открытые ордера', callback_data="clb_open_orders")], InlineKeyboardButton(text='Лимитные ордера', callback_data="clb_open_orders")],
back_btn_to_main back_btn_to_main
]) ])
def create_trades_inline_keyboard(trades): def create_trades_inline_keyboard(trades):
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
for trade in trades: for trade in trades:
symbol = trade['symbol'] if isinstance(trade, dict) else trade.symbol builder.button(text=trade, callback_data=f"show_deal_{trade}")
builder.button(text=symbol, callback_data=f"show_deal_{symbol}") builder.adjust(2)
return builder.as_markup()
def create_trades_inline_keyboard_limits(trades):
builder = InlineKeyboardBuilder()
for trade in trades:
builder.button(text=trade, callback_data=f"show_limit_{trade}")
builder.adjust(2) builder.adjust(2)
return builder.as_markup() return builder.as_markup()
@@ -176,6 +182,12 @@ def create_close_deal_markup(symbol: str) -> InlineKeyboardMarkup:
back_btn_to_main back_btn_to_main
]) ])
def create_close_limit_markup(symbol: str) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Закрыть лимитный ордер", callback_data=f"close_limit:{symbol}")],
[InlineKeyboardButton(text="Установить TP/SL", callback_data="clb_set_tp_sl_")],
back_btn_to_main
])
timer_markup = InlineKeyboardMarkup(inline_keyboard=[ timer_markup = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Установить таймер", callback_data="clb_set_timer")], [InlineKeyboardButton(text="Установить таймер", callback_data="clb_set_timer")],

View File

@@ -33,7 +33,7 @@ async def price_profit_message(message, state):
text = 'Введите число изменения цены для фиксации прибыли: ' text = 'Введите число изменения цены для фиксации прибыли: '
await message.answer(text=text, parse_mode='html', reply_markup=None) await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.cancel)
@router_risk_management_settings.message(update_risk_management_settings.price_profit) @router_risk_management_settings.message(update_risk_management_settings.price_profit)
@@ -62,7 +62,7 @@ async def price_loss_message(message, state):
text = 'Введите число изменения цены для фиксации убытков: ' text = 'Введите число изменения цены для фиксации убытков: '
await message.answer(text=text, parse_mode='html', reply_markup=None) await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.cancel)
@router_risk_management_settings.message(update_risk_management_settings.price_loss) @router_risk_management_settings.message(update_risk_management_settings.price_loss)
@@ -111,7 +111,7 @@ async def max_risk_deal_message(message, state):
text = 'Введите число (процент от баланса) для изменения максимального риска на сделку: ' text = 'Введите число (процент от баланса) для изменения максимального риска на сделку: '
await message.answer(text=text, parse_mode='html', reply_markup=None) await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.cancel)
@router_risk_management_settings.message(update_risk_management_settings.max_risk_deal) @router_risk_management_settings.message(update_risk_management_settings.max_risk_deal)