332 lines
14 KiB
Python
332 lines
14 KiB
Python
import asyncio
|
||
import logging.config
|
||
from aiogram import F, Router
|
||
from logger_helper.logger_helper import LOGGING_CONFIG
|
||
from app.services.Bybit.functions import Futures, min_qty
|
||
from app.services.Bybit.functions.Futures import open_position, close_user_trade, get_active_positions, trading_cycle
|
||
from app.services.Bybit.functions.balance import get_balance
|
||
import app.telegram.Keyboards.inline_keyboards as inline_markup
|
||
import app.telegram.Keyboards.reply_keyboards as reply_markup
|
||
from pybit.unified_trading import HTTP
|
||
import app.telegram.database.requests as rq
|
||
from aiogram.types import Message, CallbackQuery
|
||
|
||
# FSM - Механизм состояния
|
||
from aiogram.fsm.state import State, StatesGroup
|
||
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()
|
||
|
||
|
||
class state_update_symbol(StatesGroup):
|
||
symbol = State()
|
||
|
||
|
||
class state_update_entry_type(StatesGroup):
|
||
entry_type = State()
|
||
|
||
|
||
class TradeSetup(StatesGroup):
|
||
waiting_for_timer = State()
|
||
waiting_for_positive_percent = State()
|
||
|
||
class state_limit_price(StatesGroup):
|
||
price = State()
|
||
|
||
|
||
|
||
@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, state: FSMContext):
|
||
api = await rq.get_bybit_api_key(callback.from_user.id)
|
||
secret = await rq.get_bybit_secret_key(callback.from_user.id)
|
||
balance = await get_balance(callback.from_user.id, callback.message)
|
||
|
||
if balance:
|
||
symbol = await rq.get_symbol(callback.from_user.id)
|
||
|
||
text = f'''💎 Торговля на Bybit
|
||
|
||
⚖️ Ваш баланс (USDT): {balance}
|
||
📊 Текущая торговая пара: {symbol}
|
||
|
||
Как начать торговлю?
|
||
|
||
1️⃣ Проверьте и тщательно настройте все параметры в вашем профиле.
|
||
2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).
|
||
3️⃣ Нажмите кнопку 'Выбрать тип входа' и после нажмите начать торговлю.
|
||
'''
|
||
await callback.message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup)
|
||
|
||
|
||
async def start_bybit_trade_message(message, state):
|
||
api = await rq.get_bybit_api_key(message.from_user.id)
|
||
secret = await rq.get_bybit_secret_key(message.from_user.id)
|
||
balance = await get_balance(message.from_user.id, message)
|
||
|
||
if balance:
|
||
symbol = await rq.get_symbol(message.from_user.id)
|
||
|
||
text = f'''💎 Торговля на Bybit
|
||
|
||
⚖️ Ваш баланс (USDT): {balance}
|
||
📊 Текущая торговая пара: {symbol}
|
||
|
||
Как начать торговлю?
|
||
|
||
1️⃣ Проверьте и тщательно настройте все параметры в вашем профиле.
|
||
2️⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).
|
||
3️⃣ Нажмите кнопку 'Выбрать тип входа' и после нажмите начать торговлю.
|
||
'''
|
||
|
||
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):
|
||
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):
|
||
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, state)
|
||
|
||
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):
|
||
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):
|
||
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):
|
||
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)
|
||
data = await state.get_data()
|
||
|
||
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_my_deals")
|
||
async def show_my_trades_callback(callback: CallbackQuery):
|
||
tg_id = callback.from_user.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)
|
||
|
||
trades = await get_active_positions(callback.message, api_key, secret_key, symbol)
|
||
|
||
if not trades:
|
||
await callback.message.answer("Нет активных позиций.")
|
||
await callback.answer()
|
||
return
|
||
|
||
keyboard = inline_markup.create_trades_inline_keyboard(trades)
|
||
|
||
await callback.message.answer(
|
||
"Выберите сделку из списка:",
|
||
reply_markup=keyboard
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith('select_trade:'))
|
||
async def on_trade_selected(callback: CallbackQuery):
|
||
symbol = callback.data.split(':')[1]
|
||
|
||
tg_id = callback.from_user.id
|
||
api_key = await rq.get_bybit_api_key(tg_id)
|
||
secret_key = await rq.get_bybit_secret_key(tg_id)
|
||
|
||
positions = await get_active_positions(callback.message, api_key, secret_key, symbol)
|
||
|
||
# Если несколько позиций по символу, можно выбрать нужную или взять первую
|
||
if not positions:
|
||
await callback.message.answer("Позиция не найдена")
|
||
await callback.answer()
|
||
return
|
||
|
||
pos = positions[0]
|
||
symbol = pos.get('symbol')
|
||
side = pos.get('side')
|
||
entry_price = pos.get('entryPrice') # Цена открытия позиции
|
||
current_price = pos.get('price') # Текущая цена (если есть)
|
||
|
||
text = (f"Информация по позиции:\n"
|
||
f"Название: {symbol}\n"
|
||
f"Направление: {side}\n"
|
||
f"Цена покупки: {entry_price}\n"
|
||
f"Текущая цена: {current_price if current_price else 'N/A'}")
|
||
|
||
keyboard = inline_markup.create_close_deal_markup(symbol)
|
||
|
||
await callback.message.answer(text, reply_markup=keyboard)
|
||
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):
|
||
symbol = callback.data.split(':')[1]
|
||
tg_id = callback.from_user.id
|
||
|
||
result = await close_user_trade(tg_id, symbol)
|
||
|
||
if result:
|
||
await callback.message.answer(f"Сделка {symbol} успешно закрыта.")
|
||
else:
|
||
await callback.message.answer(f"Не удалось закрыть сделку {symbol}.")
|
||
|
||
await callback.answer()
|
||
|
||
|
||
@router_functions_bybit_trade.callback_query(F.data == "clb_start_chatbot_trading")
|
||
async def start_trading_process(callback: CallbackQuery, state: FSMContext):
|
||
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')
|
||
|
||
# Проверка API ключей
|
||
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}' пока не поддерживается.")
|
||
await callback.answer()
|
||
return
|
||
|
||
# Сообщаем о начале торговли
|
||
await message.answer("Начинаю торговлю с использованием текущих настроек...")
|
||
|
||
# Открываем позицию (вызывает Futures.open_position)
|
||
success = await open_position(tg_id, message, side=side, margin_mode=margin_mode)
|
||
if not success:
|
||
await message.answer('⚠️ Ошибка при совершении сделки', reply_markup=inline_markup.back_to_main)
|
||
return
|
||
|
||
|
||
# Проверяем таймер и информируем пользователя
|
||
|
||
timer_data = await rq.get_user_timer(tg_id)
|
||
timer_minutes = timer_data.get('timer') if isinstance(timer_data, dict) else timer_data
|
||
if timer_minutes and timer_minutes > 0:
|
||
await message.answer(f"Торговля будет работать по таймеру: {timer_minutes} мин.")
|
||
asyncio.create_task(trading_cycle(tg_id, message))
|
||
else:
|
||
await message.answer(
|
||
"Торговля начата без ограничения по времени. Для остановки нажмите кнопку 'Закрыть сделку'.",
|
||
reply_markup=inline_markup.create_close_deal_markup(symbol)
|
||
)
|
||
|
||
await callback.answer()
|
||
|
||
|
||
@router_functions_bybit_trade.callback_query(F.data == "clb_cancel")
|
||
async def cancel(callback: CallbackQuery, state: FSMContext):
|
||
await state.clear()
|
||
await callback.message.answer("Отменено!", reply_markup=inline_markup.back_to_main)
|
||
await callback.answer() |