1
0
forked from kodorvan/stcs

Added new functions and documentation

This commit is contained in:
algizn97
2025-08-26 19:36:11 +05:00
parent 8bc4c634fe
commit 07df16dbe9
2 changed files with 585 additions and 246 deletions

View File

@@ -2,10 +2,13 @@
import logging.config
from aiogram import F, Router
from app.tasks.tasks import active_close_tasks, active_start_tasks
from logger_helper.logger_helper import LOGGING_CONFIG
from app.services.Bybit.functions.Futures import close_user_trade, get_active_positions, close_trade_after_delay, \
active_close_tasks, active_start_tasks, trading_cycle, open_position
from app.services.Bybit.functions.Futures import (close_user_trade, close_trade_after_delay,
trading_cycle, open_position, set_take_profit_stop_loss, \
get_active_positions_by_symbol, get_active_orders_by_symbol,
start_execution_ws)
from app.services.Bybit.functions.balance import get_balance
import app.telegram.Keyboards.inline_keyboards as inline_markup
@@ -14,8 +17,8 @@ import app.telegram.database.requests as rq
from aiogram.types import Message, CallbackQuery
from app.services.Bybit.functions.price_symbol import get_price
# FSM - Механизм состояния
from aiogram.fsm.state import State, StatesGroup
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
@@ -26,75 +29,62 @@ 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()
class CloseTradeTimerState(StatesGroup):
waiting_for_delay = 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):
async def clb_start_bybit_trade_message(callback: CallbackQuery) -> None:
"""
Обработка нажатия кнопок запуска торговли или возврата в главное меню.
Отправляет информацию о балансе, символе, цене и инструкциях по торговле.
"""
balance = await get_balance(callback.from_user.id, callback.message)
price = await get_price(callback.from_user.id)
if balance:
symbol = await rq.get_symbol(callback.from_user.id)
text = f'''💎 Торговля на Bybit
⚖️ Ваш баланс (USDT): {balance}
📊 Текущая торговая пара: {symbol}
$$$ Цена: {price}
Как начать торговлю?
1⃣ Проверьте и тщательно настройте все параметры в вашем профиле.
2⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).
3⃣ Нажмите кнопку 'Выбрать тип входа' и после нажмите начать торговлю.
'''
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 callback.message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup)
asyncio.create_task(start_execution_ws(callback.from_user.id, callback.message))
async def start_bybit_trade_message(message: Message, state: FSMContext):
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
⚖️ Ваш баланс (USDT): {balance}
📊 Текущая торговая пара: {symbol}
$$$ Цена: {price}
Как начать торговлю?
1⃣ Проверьте и тщательно настройте все параметры в вашем профиле.
2⃣ Нажмите ниже кнопку 'Указать торговую пару' и введите торговую пару, без лишних символов (например: BTCUSDT).
3⃣ Нажмите кнопку 'Выбрать тип входа' и после нажмите начать торговлю.
'''
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):
async def update_symbol_for_trade_message(callback: CallbackQuery, state: FSMContext) -> None:
"""
Начинает процедуру обновления торговой пары, переводит пользователя в состояние ожидания пары.
"""
await state.set_state(state_update_symbol.symbol)
await callback.message.answer(
@@ -103,7 +93,11 @@ async def update_symbol_for_trade_message(callback: CallbackQuery, state: FSMCon
@router_functions_bybit_trade.message(state_update_symbol.symbol)
async def update_symbol_for_trade(message: Message, state: FSMContext):
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)
@@ -115,20 +109,28 @@ async def update_symbol_for_trade(message: Message, state: FSMContext):
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 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):
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):
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']:
@@ -155,7 +157,10 @@ async def entry_order_type_callback(callback: CallbackQuery, state: FSMContext):
@router_functions_bybit_trade.message(state_limit_price.price)
async def set_limit_price(message: Message, state: FSMContext):
async def set_limit_price(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод цены лимитного ордера, проверяет формат и сохраняет настройки.
"""
try:
price = float(message.text)
if price <= 0:
@@ -175,65 +180,16 @@ async def set_limit_price(message: Message, state: FSMContext):
await state.clear()
@router_functions_bybit_trade.callback_query(F.data == "clb_my_deals")
async def show_my_trades_callback(callback: CallbackQuery):
await callback.answer() # сразу отвечаем Telegram, освобождаем callback
async def process():
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)
trades = await get_active_positions(callback.message, api_key, secret_key)
if not trades:
await callback.message.answer("Нет активных позиций.")
return
keyboard = inline_markup.create_trades_inline_keyboard(trades)
await callback.message.answer(
"Выберите сделку из списка:",
reply_markup=keyboard
)
asyncio.create_task(process())
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith('select_trade:'))
async def on_trade_selected(callback: CallbackQuery):
await callback.answer()
async def process():
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)
if not positions:
await callback.message.answer("Позиция не найдена")
return
pos = positions[0]
text = (f"Информация по позиции:\n"
f"Название: {pos.get('symbol')}\n"
f"Направление: {pos.get('side')}\n"
f"Цена покупки: {pos.get('entryPrice')}\n"
f"Текущая цена: {pos.get('price', 'N/A')}")
keyboard = inline_markup.create_close_deal_markup(pos.get('symbol'))
await callback.message.answer(text, reply_markup=keyboard)
asyncio.create_task(process())
@router_functions_bybit_trade.callback_query(F.data == "clb_start_chatbot_trading")
async def start_trading_process(callback: CallbackQuery):
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)
@@ -241,24 +197,21 @@ async def start_trading_process(callback: CallbackQuery):
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', [])
@@ -275,17 +228,16 @@ async def start_trading_process(callback: CallbackQuery):
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 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)
@@ -294,16 +246,15 @@ async def start_trading_process(callback: CallbackQuery):
else:
timer_minute = timer_data or 0
logger.info(f"Timer minutes for user {tg_id}: {timer_minute}")
symbol = await rq.get_symbol(tg_id)
if timer_minute > 0:
old_task = active_start_tasks.get(tg_id)
if old_task:
if old_task and not old_task.done():
old_task.cancel()
# можно ждать завершения старой задачи, если в async функции
task = asyncio.create_task(trading_cycle(tg_id, message, symbol))
try:
await old_task
except asyncio.CancelledError:
logger.info(f"Старая задача торговли для пользователя {tg_id} отменена")
task = asyncio.create_task(trading_cycle(tg_id, message))
active_start_tasks[tg_id] = task
await message.answer(f"Торговля начнётся через {timer_minute} мин. Для отмены нажмите кнопку ниже.",
reply_markup=inline_markup.cancel_start_markup)
@@ -314,8 +265,113 @@ async def start_trading_process(callback: CallbackQuery):
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:
symbol = await rq.get_symbol(callback.from_user.id)
await callback.message.answer(f"Выберите тип сделки для пары {symbol}:",
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_by_symbol(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(F.data == "clb_open_orders")
async def show_my_orders_callback(callback: CallbackQuery) -> None:
"""
Показывает открытые позиции пользователя по символу.
"""
await callback.answer()
try:
await get_active_orders_by_symbol(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(F.data == "clb_set_tp_sl")
async def set_tp_sl(callback: CallbackQuery, state: FSMContext) -> None:
"""
Показывает активные ордера пользователя.
"""
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):
async def cancel_start_callback(callback: CallbackQuery) -> None:
"""
Отменяет задачу старта торговли по таймеру, если она активна.
"""
tg_id = callback.from_user.id
task = active_start_tasks.get(tg_id)
if task:
@@ -327,45 +383,44 @@ async def cancel_start_callback(callback: CallbackQuery):
await callback.answer()
@router_functions_bybit_trade.callback_query(F.data == "clb_stop_timer")
async def cancel_start_callback(callback: CallbackQuery):
tg_id = callback.from_user.id
task = active_close_tasks.get(tg_id)
if task:
task.cancel()
del active_close_tasks[tg_id]
await callback.message.answer("Таймер отменен.", reply_markup=inline_markup.back_to_main)
else:
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):
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)
result = await close_user_trade(tg_id, symbol, message=callback.message)
if result:
await callback.message.answer(f"Сделка {symbol} успешно закрыта.", reply_markup=inline_markup.back_to_main)
await callback.message.answer("Сделка успешно закрыта.", reply_markup=inline_markup.back_to_main)
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_deal_by_timer:"))
async def ask_close_delay(callback: CallbackQuery, state: FSMContext):
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):")
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):
async def process_close_delay(message: Message, state: FSMContext) -> None:
"""
Обрабатывает ввод задержки и запускает задачу закрытия сделки с задержкой.
"""
try:
delay_minutes = int(message.text.strip())
if delay_minutes <= 0:
@@ -381,9 +436,14 @@ async def process_close_delay(message: Message, state: FSMContext):
delay = delay_minutes * 60
# Отменяем предыдущую задачу, если есть
if tg_id in active_close_tasks:
active_close_tasks[tg_id].cancel()
old_task = active_close_tasks[tg_id]
if not old_task.done():
old_task.cancel()
try:
await old_task
except asyncio.CancelledError:
logger.info(f"Предыдущая задача закрытия сделки пользователя {tg_id} отменена")
task = asyncio.create_task(close_trade_after_delay(tg_id, message, symbol, delay))
active_close_tasks[tg_id] = task
@@ -394,16 +454,22 @@ async def process_close_delay(message: Message, state: FSMContext):
@router_functions_bybit_trade.callback_query(F.data == "clb_change_martingale_reset")
async def reset_martingale(callback: CallbackQuery):
async def reset_martingale(callback: CallbackQuery) -> None:
"""
Сбрасывает шаги мартингейла пользователя.
"""
await callback.answer()
tg_id = callback.from_user.id
await rq.update_martingale_step(tg_id, 0)
await callback.message.answer("Сброс шагов мартингейла выполнен. Торговля начнется заново с начального объема.",
await callback.message.answer("Сброс шагов выполнен.",
reply_markup=inline_markup.back_to_main)
@router_functions_bybit_trade.callback_query(F.data == "clb_cancel")
async def cancel(callback: CallbackQuery, state: FSMContext):
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()