1
0
forked from kodorvan/stcs
Files
stcs/app/services/Bybit/functions/functions.py

564 lines
26 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import asyncio
import logging.config
from aiogram import F, Router
from app.tasks.tasks import handle_stop_close_trade, handle_start_close_trade, handle_stop_trading, handle_start_trading
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, \
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
import app.telegram.Keyboards.inline_keyboards as inline_markup
from pybit.unified_trading import HTTP
import app.telegram.database.requests as rq
from aiogram.types import Message, CallbackQuery
from app.services.Bybit.functions.price_symbol import get_price
from app.states.States import (state_update_entry_type, state_update_symbol, state_limit_price,
SetTP_SL_State, CloseTradeTimerState)
from aiogram.fsm.context import FSMContext
from app.services.Bybit.functions.get_valid_symbol import get_valid_symbols
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("functions")
router_functions_bybit_trade = Router()
@router_functions_bybit_trade.callback_query(F.data.in_(['clb_start_trading', 'clb_back_to_main', 'back_to_main']))
async def clb_start_bybit_trade_message(callback: CallbackQuery) -> None:
"""
Обработка нажатия кнопок запуска торговли или возврата в главное меню.
Отправляет информацию о балансе, символе, цене и инструкциях по торговле.
"""
user_id = callback.from_user.id
balance = await get_balance(user_id, callback.message)
price = await get_price(user_id)
if balance:
symbol = await rq.get_symbol(user_id)
text = (
f"💎 Торговля на Bybit\n\n"
f"⚖️ Ваш баланс (USDT): {float(balance):.2f}\n"
f"📊 Текущая торговая пара: {symbol}\n"
f"$$$ Цена: {price}\n\n"
"Как начать торговлю?\n\n"
"1⃣ Проверьте и тщательно настройте все параметры в вашем профиле.\n"
"2⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).\n"
"3⃣ Нажмите кнопку 'Выбрать тип входа' и после нажмите начать торговлю.\n"
)
await callback.message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup)
async def start_bybit_trade_message(message: Message) -> None:
"""
Отправляет пользователю информацию о балансе, символе и текущей цене,
вместе с инструкциями по началу торговли.
"""
balance = await get_balance(message.from_user.id, message)
price = await get_price(message.from_user.id)
if balance:
symbol = await rq.get_symbol(message.from_user.id)
text = (
f"💎 Торговля на Bybit\n\n"
f"⚖️ Ваш баланс (USDT): {balance}\n"
f"📊 Текущая торговая пара: {symbol}\n"
f"$$$ Цена: {price}\n\n"
"Как начать торговлю?\n\n"
"1⃣ Проверьте и тщательно настройте все параметры в вашем профиле.\n"
"2⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).\n"
"3⃣ Нажмите кнопку 'Выбрать тип входа' и после нажмите начать торговлю.\n"
)
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup)
@router_functions_bybit_trade.callback_query(F.data == 'clb_update_trading_pair')
async def update_symbol_for_trade_message(callback: CallbackQuery, state: FSMContext) -> None:
"""
Начинает процедуру обновления торговой пары, переводит пользователя в состояние ожидания пары.
"""
await state.set_state(state_update_symbol.symbol)
await callback.message.answer(
text='Укажите торговую пару заглавными буквами без пробелов и лишних символов (пример: BTCUSDT): ',
reply_markup=inline_markup.cancel)
@router_functions_bybit_trade.message(state_update_symbol.symbol)
async def update_symbol_for_trade(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод торговой пары пользователем и проверяет её валидность.
При успешном обновлении сохранит пару и отправит обновлённую информацию.
"""
user_input = message.text.strip().upper()
exists = await get_valid_symbols(message.from_user.id, user_input)
if not exists:
await message.answer("Введена некорректная торговая пара или такой пары нет в списке. Попробуйте снова.")
return
await state.update_data(symbol=message.text)
await message.answer('Пара была успешно обновлена')
await rq.update_symbol(message.from_user.id, user_input)
await start_bybit_trade_message(message)
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(f"Выбран тип входа в позицию: {order_type}",
reply_markup=inline_markup.start_trading_markup)
await callback.answer()
except Exception as e:
logger.error(f"Произошла ошибка при обновлении типа входа в позицию: {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")
async def start_trading_process(callback: CallbackQuery) -> None:
"""
Запускает торговый цикл в выбранном режиме Long/Short.
Проверяет API-ключи, режим торговли, маржинальный режим и открытые позиции,
затем запускает торговый цикл с задержкой или без неё.
"""
tg_id = callback.from_user.id
message = callback.message
data_main_stgs = await rq.get_user_main_settings(tg_id)
api_key = await rq.get_bybit_api_key(tg_id)
secret_key = await rq.get_bybit_secret_key(tg_id)
symbol = await rq.get_symbol(tg_id)
margin_mode = data_main_stgs.get('margin_type', 'Isolated')
trading_mode = data_main_stgs.get('trading_mode')
if not api_key or not secret_key:
await message.answer("❗️ У вас не настроены API ключи для Bybit.")
await callback.answer()
return
if trading_mode not in ['Long', 'Short', 'Smart', 'Switch']:
await message.answer(f"❗️ Некорректный торговый режим: {trading_mode}")
await callback.answer()
return
if margin_mode not in ['Isolated', 'Cross']:
margin_mode = 'Isolated'
client = HTTP(api_key=api_key, api_secret=secret_key)
try:
positions_resp = client.get_positions(category='linear', symbol=symbol)
positions = positions_resp.get('result', {}).get('list', [])
except Exception as e:
logger.error(f"Ошибка при получении позиций: {e}")
positions = []
for pos in positions:
size = pos.get('size')
existing_margin_mode = pos.get('margin_mode')
if size and float(size) > 0 and existing_margin_mode and existing_margin_mode != margin_mode:
await callback.answer(
f"⚠️ Маржинальный режим нельзя менять при открытой позиции "
f"(текущий режим: {existing_margin_mode})", show_alert=True)
return
if trading_mode == 'Long':
side = 'Buy'
elif trading_mode == 'Short':
side = 'Sell'
else:
await message.answer(f"Режим торговли '{trading_mode}' пока не поддерживается.",
reply_markup=inline_markup.back_to_main)
await callback.answer()
return
await message.answer("Начинаю торговлю с использованием текущих настроек...")
timer_data = await rq.get_user_timer(tg_id)
if isinstance(timer_data, dict):
timer_minute = timer_data.get('timer_minutes', 0)
else:
timer_minute = timer_data or 0
if timer_minute > 0:
await handle_start_trading(tg_id, message, side=side, margin_mode=margin_mode, use_timer=True)
await message.answer(f"Торговля начнётся через {timer_minute} мин. Для отмены нажмите кнопку ниже.",
reply_markup=inline_markup.cancel_start_markup)
await rq.update_user_timer(tg_id, minutes=0)
else:
await handle_start_trading(tg_id, message, side=side, margin_mode=margin_mode, use_timer=False)
await callback.answer()
@router_functions_bybit_trade.callback_query(F.data == "clb_my_deals")
async def show_my_trades(callback: CallbackQuery) -> None:
"""
Отображает пользователю выбор типа сделки по текущей торговой паре.
"""
await callback.answer()
try:
await callback.message.answer(f"Выберите тип сделки:",
reply_markup=inline_markup.my_deals_select_markup)
except Exception as e:
logger.error(f"Произошла ошибка при выборе типа сделки: {e}")
@router_functions_bybit_trade.callback_query(F.data == "clb_open_deals")
async def show_my_trades_callback(callback: CallbackQuery):
"""
Показывает открытые позиции пользователя.
"""
await callback.answer()
try:
await get_active_positions(callback.from_user.id, message=callback.message)
except Exception as e:
logger.error(f"Произошла ошибка при выборе сделки: {e}")
await callback.message.answer("Произошла ошибка при выборе сделки", reply_markup=inline_markup.back_to_main)
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("show_deal_"))
async def show_deal_callback(callback_query: CallbackQuery) -> None:
"""
Показывает сделку пользователя по символу.
"""
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")
async def show_my_orders_callback(callback: CallbackQuery) -> None:
"""
Показывает открытые позиции пользователя по символу.
"""
await callback.answer()
try:
await get_active_orders(callback.from_user.id, message=callback.message)
except Exception as e:
logger.error(f"Произошла ошибка при выборе ордера: {e}")
await callback.message.answer("Произошла ошибка при выборе ордера", reply_markup=inline_markup.back_to_main)
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("show_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")
async def set_tp_sl(callback: CallbackQuery, state: FSMContext) -> None:
"""
Запускает процесс установки Take Profit и Stop Loss.
"""
await callback.answer()
await state.set_state(SetTP_SL_State.waiting_for_take_profit)
await callback.message.answer("Введите значение Take Profit (в цене, например 26000.5):",
reply_markup=inline_markup.cancel)
@router_functions_bybit_trade.message(SetTP_SL_State.waiting_for_take_profit)
async def process_take_profit(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод значения Take Profit и запрашивает Stop Loss.
"""
try:
tp = float(message.text.strip())
if tp <= 0:
await message.answer("Значение Take Profit должно быть положительным числом. Попробуйте снова.",
reply_markup=inline_markup.cancel)
return
except ValueError:
await message.answer("Некорректный ввод. Пожалуйста, введите число для Take Profit.",
reply_markup=inline_markup.cancel)
return
await state.update_data(take_profit=tp)
await state.set_state(SetTP_SL_State.waiting_for_stop_loss)
await message.answer("Введите значение Stop Loss (в цене, например 24500.3):", reply_markup=inline_markup.cancel)
@router_functions_bybit_trade.message(SetTP_SL_State.waiting_for_stop_loss)
async def process_stop_loss(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод значения Stop Loss и завершает процесс установки TP/SL.
"""
try:
sl = float(message.text.strip())
if sl <= 0:
await message.answer("Значение Stop Loss должно быть положительным числом. Попробуйте снова.",
reply_markup=inline_markup.cancel)
return
except ValueError:
await message.answer("Некорректный ввод. Пожалуйста, введите число для Stop Loss.",
reply_markup=inline_markup.cancel)
return
data = await state.get_data()
tp = data.get("take_profit")
if tp is None:
await message.answer("Ошибка, не найдено значение Take Profit. Попробуйте снова.")
await state.clear()
return
tg_id = message.from_user.id
await set_take_profit_stop_loss(tg_id, message, take_profit_price=tp, stop_loss_price=sl)
await state.clear()
@router_functions_bybit_trade.callback_query(F.data == "clb_stop_timer")
async def cancel_start_callback(callback: CallbackQuery) -> None:
"""
Отменяет задачу старта торговли по таймеру, если она активна.
"""
tg_id = callback.from_user.id
await handle_stop_close_trade(tg_id)
await callback.message.answer("Торговля по таймеру отменена.", reply_markup=inline_markup.back_to_main)
await callback.answer()
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith("close_deal:"))
async def close_trade_callback(callback: CallbackQuery) -> None:
"""
Закрывает сделку пользователя по символу.
"""
symbol = callback.data.split(':')[1]
tg_id = callback.from_user.id
result = await close_user_trade(tg_id, symbol, message=callback.message)
if result:
await handle_stop_trading(tg_id)
logger.info(f"Сделка {symbol} успешно закрыта.")
else:
logger.error(f"Не удалось закрыть сделку {symbol}.")
await callback.message.answer(f"Не удалось закрыть сделку {symbol}.")
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:"))
async def ask_close_delay(callback: CallbackQuery, state: FSMContext) -> None:
"""
Запускает диалог с пользователем для задания задержки перед закрытием сделки.
"""
symbol = callback.data.split(":")[1]
await state.update_data(symbol=symbol)
await state.set_state(CloseTradeTimerState.waiting_for_delay)
await callback.message.answer("Введите задержку в минутах до закрытия сделки (например, 60):",
reply_markup=inline_markup.cancel)
await callback.answer()
@router_functions_bybit_trade.message(CloseTradeTimerState.waiting_for_delay)
async def process_close_delay(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод задержки и запускает задачу закрытия сделки с задержкой.
"""
try:
delay_minutes = int(message.text.strip())
if delay_minutes <= 0:
await message.answer("Введите положительное число.")
return
except ValueError:
await message.answer("Некорректный ввод. Введите число в минутах.")
return
data = await state.get_data()
symbol = data.get("symbol")
tg_id = message.from_user.id
delay = delay_minutes * 60
await handle_start_close_trade(tg_id, message, symbol, delay)
await message.answer(f"Закрытие сделки {symbol} запланировано через {delay} секунд.",
reply_markup=inline_markup.cancel_start_markup)
await state.clear()
@router_functions_bybit_trade.callback_query(F.data == "clb_change_martingale_reset")
async def reset_martingale(callback: CallbackQuery) -> None:
"""
Сбрасывает шаги мартингейла пользователя.
"""
tg_id = callback.from_user.id
await rq.update_martingale_step(tg_id, 0)
await callback.answer("Сброс шагов выполнен.")
@router_functions_bybit_trade.callback_query(F.data == "clb_stop_trading")
async def confirm_stop_trading(callback: CallbackQuery):
"""
Предлагает пользователю выбрать вариант подтверждение остановки торговли.
"""
await callback.message.answer(
"Выберите вариант остановки торговли:", reply_markup=inline_markup.stop_choice_markup
)
await callback.answer()
@router_functions_bybit_trade.callback_query(F.data == "stop_immediately")
async def stop_immediately(callback: CallbackQuery):
"""
Останавливает торговлю немедленно.
"""
tg_id = callback.from_user.id
await handle_stop_trading(tg_id, use_timer=False)
await callback.message.answer("Торговля остановлена.", reply_markup=inline_markup.back_to_main)
await callback.answer()
@router_functions_bybit_trade.callback_query(F.data == "stop_with_timer")
async def stop_with_timer_start(callback: CallbackQuery, state: FSMContext):
"""
Запускает диалог с пользователем для задания задержки перед остановкой торговли.
"""
await state.set_state(CloseTradeTimerState.waiting_for_delay)
await callback.message.answer("Введите задержку в минутах перед остановкой торговли:", reply_markup=inline_markup.cancel)
await callback.answer()
@router_functions_bybit_trade.message(CloseTradeTimerState.waiting_for_delay)
async def process_stop_delay(message: Message, state: FSMContext):
"""
Обрабатывает ввод задержки и запускает задачу остановки торговли с задержкой.
"""
try:
delay_minutes = int(message.text.strip())
if delay_minutes <= 0:
await message.answer("Введите положительное число минут.")
return
except ValueError:
await message.answer("Некорректный формат. Введите число в минутах.")
return
tg_id = message.from_user.id
delay_seconds = delay_minutes * 60
# Остановка задачи с таймером через заданную задержку
# Можно реализовать через запуск отдельной асинхронной задачи, которая через delay_seconds отменит торговый цикл
async def delayed_stop():
await asyncio.sleep(delay_seconds)
await handle_stop_trading(tg_id, use_timer=True)
await message.answer("Торговля по таймеру остановлена.")
asyncio.create_task(delayed_stop())
await message.answer(f"Торговля будет остановлена через {delay_minutes} минут.", reply_markup=inline_markup.back_to_main)
await state.clear()
@router_functions_bybit_trade.callback_query(F.data == "clb_cancel")
async def cancel(callback: CallbackQuery, state: FSMContext) -> None:
"""
Отменяет текущее состояние FSM и сообщает пользователю об отмене.
"""
await state.clear()
await callback.message.answer("Отменено!", reply_markup=inline_markup.back_to_main)
await callback.answer()