Files
stcs/app/services/Bybit/functions/Futures.py
2025-08-25 17:07:41 +05:00

416 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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 functools
import time
import logging.config
from pybit import exceptions
from pybit.unified_trading import HTTP
from logger_helper.logger_helper import LOGGING_CONFIG
import app.services.Bybit.functions.price_symbol as price_symbol
import app.services.Bybit.functions.balance as balance_g
import app.telegram.database.requests as rq
import app.telegram.Keyboards.inline_keyboards as inline_markup
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger("futures")
active_start_tasks = {}
active_close_tasks = {}
def safe_float(val):
try:
if val is None or val == '':
return 0.0
return float(val)
except (ValueError, TypeError):
return 0.0
async def info_access_open_deal(message, symbol, trade_mode, margin_mode, leverage, qty, tp, sl, entry_price, limit_price, order_type):
human_margin_mode = 'Isolated' if margin_mode == 'ISOLATED_MARGIN' else 'Cross'
text = f'''{'Позиция была успешна открыта' if order_type == 'Market' else 'Лимитный ордер установлен'}!
Торговая пара: {symbol}
Цена входа: {entry_price if order_type == 'Market' else round(limit_price, 5)}
Движение: {trade_mode}
Тип-маржи: {human_margin_mode}
Кредитное плечо: {leverage}x
Количество: {qty}
Тейк-профит: {round(tp, 5)}
Стоп-лосс: {round(sl, 5)}
'''
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.create_close_deal_markup(symbol))
async def error_max_step(message):
logger.error('Сделка не была совершена, превышен лимит максимального количества ставок в серии.')
await message.answer('Сделка не была совершена, превышен лимит максимального количества ставок в серии.',
reply_markup=inline_markup.back_to_main)
async def error_max_risk(message):
logger.error('Сделка не была совершена, риск убытка превышает допустимый лимит.')
await message.answer('Сделка не была совершена, риск убытка превышает допустимый лимит.',
reply_markup=inline_markup.back_to_main)
async def open_position(tg_id, message, side: str, margin_mode: str, tpsl_mode='Full'):
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)
data_main_stgs = await rq.get_user_main_settings(tg_id)
order_type = data_main_stgs.get('entry_order_type')
limit_price = None
if order_type == 'Limit':
limit_price = await rq.get_limit_price(tg_id)
data_risk_stgs = await rq.get_user_risk_management_settings(tg_id)
bybit_margin_mode = 'ISOLATED_MARGIN' if margin_mode == 'Isolated' else 'REGULAR_MARGIN'
client = HTTP(api_key=api_key, api_secret=secret_key)
try:
client.set_tp_sl_mode(symbol=symbol, category='linear', tpSlMode='Full')
except exceptions.InvalidRequestError as e:
if 'same tp sl mode' in str(e):
logger.info("Режим TP/SL уже установлен - пропускаем")
else:
raise
try:
balance = await balance_g.get_balance(tg_id, message)
price = await price_symbol.get_price(tg_id)
client.set_margin_mode(setMarginMode=bybit_margin_mode)
martingale_factor = safe_float(data_main_stgs.get('martingale_factor'))
max_martingale_steps = int(data_main_stgs.get('maximal_quantity', 0))
starting_quantity = safe_float(data_main_stgs.get('starting_quantity'))
max_risk_percent = safe_float(data_risk_stgs.get('max_risk_deal'))
loss_profit = safe_float(data_risk_stgs.get('price_loss'))
take_profit = safe_float(data_risk_stgs.get('price_profit'))
commission_fee = safe_float(data_risk_stgs.get('commission_fee', 0))
positions_resp = client.get_positions(category='linear', symbol=symbol)
positions_list = positions_resp.get('result', {}).get('list', [])
if positions_list:
position = positions_list[0]
size = safe_float(position.get('size', 0))
side_pos = position.get('side', '')
if size > 0 and side_pos:
entry_price = safe_float(position.get('avgPrice', price))
else:
entry_price = price
else:
entry_price = price
if order_type == 'Market':
base_price = entry_price
else:
base_price = limit_price
if side.lower() == 'buy':
take_profit_price = base_price * (1 + take_profit / 100)
stop_loss_price = base_price * (1 - loss_profit / 100)
else:
take_profit_price = base_price * (1 - take_profit / 100)
stop_loss_price = base_price * (1 + loss_profit / 100)
take_profit_price = max(take_profit_price, 0)
stop_loss_price = max(stop_loss_price, 0)
current_martingale_step = 0
next_quantity = starting_quantity
realised_pnl = 0.0
current_martingale = await rq.get_martingale_step(tg_id)
current_martingale_step = int(current_martingale)
if positions_list:
if realised_pnl > 0:
current_martingale_step = 0
next_quantity = starting_quantity
else:
current_martingale_step += 1
if current_martingale_step > max_martingale_steps:
await error_max_step(message)
return
next_quantity = float(starting_quantity) * (float(martingale_factor) ** current_martingale_step)
else:
next_quantity = starting_quantity
current_martingale_step = 0
potential_loss = safe_float(next_quantity) * safe_float(price) * (loss_profit / 100)
allowed_loss = safe_float(balance) * (max_risk_percent / 100)
if potential_loss > allowed_loss:
await error_max_risk(message)
return
instruments_resp = client.get_instruments_info(category='linear', symbol=symbol)
if instruments_resp.get('retCode') == 0:
instrument_info = instruments_resp.get('result', {}).get('list', [])
if instrument_info:
instrument = instrument_info[0]
min_order_qty = float(instrument.get('minOrderQty', 0))
min_order_value_api = float(instrument.get('minOrderValue', 0))
if min_order_value_api == 0:
min_order_value_api = 5.0
# Рассчитываем по формуле:
min_order_value_calc = min_order_qty * price if min_order_qty > 0 else 0
# Минимальное значение из значений параметров на бирже
min_order_value = max(min_order_value_calc, min_order_value_api)
else:
min_order_value = 5.0
order_value = float(next_quantity) * float(price)
if order_value < min_order_value:
await message.answer(
f"Сумма ордера слишком мала: {order_value:.2f} USDT. "
f"Минимум для торговли — {min_order_value} USDT. "
f"Пожалуйста, увеличьте количество позиций.")
return False
leverage = int(data_main_stgs.get('size_leverage', 1))
try:
resp = client.set_leverage(
category='linear',
symbol=symbol,
buyLeverage=str(leverage),
sellLeverage=str(leverage)
)
except exceptions.InvalidRequestError as e:
if "110043" in str(e):
# Плечо уже установлено с таким значением, можем игнорировать
logger.info(f"Leverage already set to {leverage} for {symbol}")
else:
raise e
if tpsl_mode == 'Full':
tp_order_type = 'Market'
sl_order_type = 'Market'
tp_limit_price = None
sl_limit_price = None
else: # Partial
tp_order_type = 'Limit'
sl_order_type = 'Limit'
tp_limit_price = take_profit_price
sl_limit_price = stop_loss_price
response = client.place_order(
category='linear',
symbol=symbol,
side=side,
orderType=order_type, # Market или Limit
qty=str(next_quantity),
price=str(limit_price) if order_type == 'Limit' and limit_price else None,
takeProfit=str(take_profit_price),
tpOrderType=tp_order_type,
tpLimitPrice=str(tp_limit_price) if tp_limit_price else None,
stopLoss=str(stop_loss_price),
slOrderType=sl_order_type,
slLimitPrice=str(sl_limit_price) if sl_limit_price else None,
tpslMode=tpsl_mode,
timeInForce='GTC',
orderLinkId=f"deal_{symbol}_{int(time.time())}"
)
if response.get('retCode', -1) == 0:
await info_access_open_deal(message, symbol, data_main_stgs.get('trading_mode', ''),
bybit_margin_mode,
data_main_stgs.get('size_leverage', 1), next_quantity, take_profit_price,
stop_loss_price, entry_price, limit_price, order_type=order_type)
await rq.update_martingale_step(tg_id, current_martingale_step)
return True
else:
logger.error(f"Ошибка открытия ордера: {response}")
await message.answer(f"Ошибка открытия ордера", reply_markup=inline_markup.back_to_main)
return False
except exceptions.InvalidRequestError as e:
logger.error(f"InvalidRequestError: {e}")
await message.answer('Недостаточно средств для размещения нового ордера с заданным количеством и плечом.',
reply_markup=inline_markup.back_to_main)
except Exception as e:
logger.error(f"Ошибка при совершении сделки: {e}")
await message.answer('Возникла ошибка при попытке открыть позицию.', reply_markup=inline_markup.back_to_main)
async def trading_cycle(tg_id, message):
try:
timer_data = await rq.get_user_timer(tg_id)
timer_min = 0
if isinstance(timer_data, dict):
timer_min = timer_data.get('timer_minutes') or timer_data.get('timer') or 0
else:
timer_min = timer_data or 0
timer_sec = timer_min * 60
if timer_sec > 0:
await asyncio.sleep(timer_sec)
data_main_stgs = await rq.get_user_main_settings(tg_id)
side = 'Buy' if data_main_stgs.get('trading_mode', '') == 'Long' else 'Sell'
margin_mode = data_main_stgs.get('margin_type', 'Isolated')
await open_position(tg_id, message, side=side, margin_mode=margin_mode)
except asyncio.CancelledError:
logger.info(f"Торговый цикл для пользователя {tg_id} был отменён.")
async def fetch_positions_async(client, symbol):
loop = asyncio.get_running_loop()
# запускаем блокирующий вызов get_positions в отдельном потоке
return await loop.run_in_executor(None, functools.partial(client.get_positions, category='linear', symbol=symbol))
async def get_active_positions(message, api_key, secret_key):
client = HTTP(
api_key=api_key,
api_secret=secret_key
)
instruments_resp = client.get_instruments_info(category='linear')
if instruments_resp.get('retCode') != 0:
return []
symbols = [item['symbol'] for item in instruments_resp.get('result', {}).get('list', [])]
active_positions = []
for sym in symbols:
try:
resp = await fetch_positions_async(client, sym)
if resp.get('retCode') == 0:
positions = resp.get('result', {}).get('list', [])
for pos in positions:
if pos.get('size') and safe_float(pos['size']) > 0:
active_positions.append(pos)
except Exception as e:
logger.error(f"Ошибка при получении позиций: {e}")
await message.answer('⚠️ Ошибка при получении позиций', reply_markup=inline_markup.back_to_main)
return active_positions
async def close_user_trade(tg_id: int, symbol: str, message=None) -> bool:
try:
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)
# Получаем текущие открытые позиции
positions_resp = client.get_positions(category="linear", symbol=symbol)
if positions_resp.get('retCode') != 0:
return False
positions_list = positions_resp.get('result', {}).get('list', [])
if not positions_list:
return False
position = positions_list[0]
qty = abs(safe_float(position.get('size')))
side = position.get('side')
entry_price = safe_float(position.get('avgPrice'))
if qty == 0:
return False
# Получаем настройки пользователя
data_main_stgs = await rq.get_user_main_settings(tg_id)
order_type = data_main_stgs.get('entry_order_type')
limit_price = await rq.get_limit_price(tg_id)
# Получаем открытые ордера
orders = client.get_open_orders(category='linear', symbol=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"
# Получаем текущую цену
ticker_resp = client.get_tickers(category="linear", symbol=symbol)
current_price = 0.0
if ticker_resp.get('retCode') == 0:
result = ticker_resp.get('result', {})
# поддержать оба варианта: result это dict с key 'list', или list
ticker_list = []
if isinstance(result, dict):
ticker_list = result.get('list', [])
elif isinstance(result, list):
ticker_list = result
if ticker_list:
current_price = float(ticker_list[0].get('lastPrice', 0.0))
if order_type == 'Limit':
# Если есть открытый лимитный ордер отменяем его
if order_id:
cancel_resp = client.cancel_order(category='linear', symbol=symbol, orderId=order_id)
if cancel_resp.get('retCode') != 0:
if message:
await message.answer("Ошибка при отмене лимитного ордера.",
reply_markup=inline_markup.back_to_main)
return False
# Можно здесь добавить логику выставления лимитного ордера на закрытие, если нужно
# В текущем коде отсутствует
if message:
await message.answer(f"Лимитный ордер отменён, позиция не закрыта автоматически.",
reply_markup=inline_markup.back_to_main)
return False
else:
# Рыночный ордер для закрытия позиции
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:
if message:
pnl = (current_price - entry_price) * qty if side == "Buy" else (entry_price - current_price) * qty
pnl_percent = (pnl / (entry_price * qty)) * 100 if entry_price * qty > 0 else 0
text = (f"Сделка {symbol} успешно закрыта.\n"
f"Цена входа: {entry_price if entry_price else limit_price}\n"
f"Цена закрытия: {current_price}\n"
f"Результат: {pnl:.4f} USDT ({pnl_percent:.2f}%)")
await message.answer(text, reply_markup=inline_markup.back_to_main)
return True
else:
if message:
await message.answer(f"Ошибка закрытия сделки {symbol}.", reply_markup=inline_markup.back_to_main)
return False
except Exception as e:
logger.error(f"Ошибка закрытия сделки {symbol} для пользователя {tg_id}: {e}", exc_info=True)
if message:
await message.answer("Произошла ошибка при закрытии сделки.", reply_markup=inline_markup.back_to_main)
return False
async def close_trade_after_delay(tg_id: int, message, symbol: str, delay_sec: int):
try:
await asyncio.sleep(delay_sec)
result = await close_user_trade(tg_id, symbol)
if result:
await message.answer(f"Сделка {symbol} успешно закрыта по таймеру.",
reply_markup=inline_markup.back_to_main)
else:
await message.answer(f"Не удалось закрыть сделку {symbol} по таймеру.",
reply_markup=inline_markup.back_to_main)
except asyncio.CancelledError:
await message.answer(f"Закрытие сделки {symbol} по таймеру отменено.", reply_markup=inline_markup.back_to_main)
finally:
active_close_tasks.pop(tg_id, None)
def get_positive_percent(negative_percent: float, manual_positive_percent: float | None) -> float:
if manual_positive_percent and manual_positive_percent > 0:
return manual_positive_percent
return abs(negative_percent)