Update functions
This commit is contained in:
@@ -1,325 +1,239 @@
|
|||||||
import time
|
import asyncio
|
||||||
|
import time
|
||||||
from typing import Optional
|
import logging
|
||||||
from asyncio import Handle
|
|
||||||
|
|
||||||
from annotated_types import T
|
|
||||||
from pybit import exceptions
|
from pybit import exceptions
|
||||||
from pybit.unified_trading import HTTP
|
from pybit.unified_trading import HTTP
|
||||||
from pybit.unified_trading import WebSocket
|
|
||||||
|
|
||||||
from app.services.Bybit.functions import price_symbol
|
import app.services.Bybit.functions.price_symbol as price_symbol
|
||||||
import app.services.Bybit.functions.balance as balance_g
|
import app.services.Bybit.functions.balance as balance_g
|
||||||
import app.telegram.database.requests as rq
|
import app.telegram.database.requests as rq
|
||||||
|
|
||||||
import logging
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
|
||||||
|
|
||||||
def handle_message(message):
|
|
||||||
print(message)
|
|
||||||
|
|
||||||
async def info_access_open_deal(message, symbol, trade_mode, margin_mode, leverage, qty):
|
async def info_access_open_deal(message, symbol, trade_mode, margin_mode, leverage, qty):
|
||||||
match margin_mode:
|
human_margin_mode = 'Isolated' if margin_mode == 'ISOLATED_MARGIN' else 'Cross'
|
||||||
case 'ISOLATED_MARGIN':
|
|
||||||
margin_mode = 'Isolated'
|
|
||||||
case 'REGULAR_MARGIN':
|
|
||||||
margin_mode = 'Cross'
|
|
||||||
|
|
||||||
text = f'''Позиция была успешна открыта!
|
text = f'''Позиция была успешна открыта!
|
||||||
Торговая пара: {symbol}
|
Торговая пара: {symbol}
|
||||||
Движение: {trade_mode}
|
Движение: {trade_mode}
|
||||||
Тип-маржи: {margin_mode}
|
Тип-маржи: {human_margin_mode}
|
||||||
Кредитное плечо: {leverage}
|
Кредитное плечо: {leverage}
|
||||||
Количество: {qty}
|
Количество: {qty}
|
||||||
'''
|
'''
|
||||||
|
|
||||||
await message.answer(text=text, parse_mode='html')
|
await message.answer(text=text, parse_mode='html')
|
||||||
|
|
||||||
|
|
||||||
async def error_max_step(message):
|
async def error_max_step(message):
|
||||||
await message.answer('Сделка не была совершена, превышен лимит максимального количества ставок')
|
await message.answer('Сделка не была совершена, превышен лимит максимального количества ставок в серии.')
|
||||||
|
|
||||||
|
|
||||||
async def error_max_risk(message):
|
async def error_max_risk(message):
|
||||||
await message.answer('Сделка не была совершена, слишком высокий риск')
|
await message.answer('Сделка не была совершена, риск убытка превышает допустимый лимит.')
|
||||||
|
|
||||||
|
|
||||||
|
async def open_position(tg_id, message, side: str, margin_mode: str):
|
||||||
|
"""
|
||||||
|
Открытие позиции (торговля с мартингейлом и управлением рисками)
|
||||||
|
|
||||||
|
:param tg_id: Telegram ID пользователя
|
||||||
|
:param message: объект сообщения Telegram для ответов
|
||||||
|
:param side: 'Buy' для Long, 'Sell' для Short
|
||||||
|
:param margin_mode: 'Isolated' или 'Cross'
|
||||||
|
"""
|
||||||
|
|
||||||
async def contract_long(tg_id, message, margin_mode):
|
|
||||||
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)
|
|
||||||
|
|
||||||
data_main_stgs = await rq.get_user_main_settings(tg_id)
|
|
||||||
data_risk_management_stgs = await rq.get_user_risk_management_settings(tg_id)
|
|
||||||
|
|
||||||
match margin_mode:
|
|
||||||
case 'Isolated':
|
|
||||||
margin_mode = 'ISOLATED_MARGIN'
|
|
||||||
case 'Cross':
|
|
||||||
margin_mode = 'REGULAR_MARGIN'
|
|
||||||
|
|
||||||
client = HTTP(
|
|
||||||
api_key=api_key,
|
|
||||||
api_secret=secret_key
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
balance = 0
|
|
||||||
price = 0
|
|
||||||
|
|
||||||
balance = await balance_g.get_balance(tg_id)
|
|
||||||
price = await price_symbol.get_price(tg_id, message)
|
|
||||||
|
|
||||||
client.set_margin_mode(
|
|
||||||
setMarginMode=margin_mode # margin_type
|
|
||||||
)
|
|
||||||
|
|
||||||
martingale_factor = float(data_main_stgs['martingale_factor']) # Исправлено: было maximal_quantity
|
|
||||||
max_martingale_steps = int(data_main_stgs['maximal_quantity'])
|
|
||||||
starting_quantity = float(data_main_stgs['starting_quantity'])
|
|
||||||
max_risk_percent = float(data_risk_management_stgs['max_risk_deal'])
|
|
||||||
loss_profit = float(data_risk_management_stgs['price_loss'])
|
|
||||||
takeprofit= float(data_risk_management_stgs['price_profit'])
|
|
||||||
commission_fee = float(data_risk_management_stgs.get('commission_fee', 0))
|
|
||||||
takeProfit_raw = takeprofit
|
|
||||||
takeProfit = takeProfit_raw - commission_fee # уменьшаем TP на комиссию
|
|
||||||
|
|
||||||
if takeProfit < 0:
|
|
||||||
takeProfit = 0
|
|
||||||
|
|
||||||
# Инициализация переменных
|
|
||||||
next_quantity = starting_quantity
|
|
||||||
last_quantity = starting_quantity
|
|
||||||
realised_pnl = 0.0
|
|
||||||
|
|
||||||
current_martingale_step = 0 # Текущая ставка в серии
|
|
||||||
|
|
||||||
next_quantity = 0
|
|
||||||
realised_pnl = 0
|
|
||||||
|
|
||||||
last_quantity = starting_quantity
|
|
||||||
|
|
||||||
# Пример расчёта следующего размера позиции
|
|
||||||
try:
|
|
||||||
position_info = client.get_positions(category='linear', symbol=SYMBOL)
|
|
||||||
position = position_info['result']['list'][0] # или другой нужный индекс
|
|
||||||
|
|
||||||
realised_pnl = float(position['unrealisedPnl'])
|
|
||||||
|
|
||||||
if realised_pnl > 0:
|
|
||||||
starting_quantity = next_quantity
|
|
||||||
current_martingale_step = 0
|
|
||||||
elif not realised_pnl:
|
|
||||||
next_quantity = starting_quantity
|
|
||||||
current_martingale_step += 1
|
|
||||||
else:
|
|
||||||
current_martingale_step += 1
|
|
||||||
next_quantity = last_quantity * martingale_factor
|
|
||||||
starting_quantity = next_quantity
|
|
||||||
except Exception as e:
|
|
||||||
print("Не получены позиции")
|
|
||||||
next_quantity = starting_quantity
|
|
||||||
|
|
||||||
potential_loss = (next_quantity * float(price)) * (loss_profit / 100)
|
|
||||||
allowed_loss = float(balance) * (max_risk_percent / 100)
|
|
||||||
|
|
||||||
if current_martingale_step >= max_martingale_steps:
|
|
||||||
print("Достигнут максимум ставок в серии (8)!")
|
|
||||||
print("Торговля не продолжится")
|
|
||||||
|
|
||||||
await error_max_step(message)
|
|
||||||
else:
|
|
||||||
if potential_loss > allowed_loss:
|
|
||||||
print(f"ОШИБКА: Риск превышен!")
|
|
||||||
print(f"Ручной qty = {next_quantity} → Убыток = {potential_loss} USDT")
|
|
||||||
print(f"Разрешено = {allowed_loss} USDT (1% от баланса)")
|
|
||||||
|
|
||||||
await error_max_risk(message)
|
|
||||||
else:
|
|
||||||
print(f"Риск в допустимых пределах. Qty = {next_quantity}")
|
|
||||||
|
|
||||||
r = client.place_order(
|
|
||||||
category='linear',
|
|
||||||
symbol=SYMBOL,
|
|
||||||
side='Buy',
|
|
||||||
orderType="Market",
|
|
||||||
leverage=int(data_main_stgs['size_leverage']),
|
|
||||||
qty=next_quantity,
|
|
||||||
takeProfit=takeProfit, # TP - закрывает позицию, когда цена достигает нужного уровня
|
|
||||||
stopProfit=float(data_risk_management_stgs['price_loss']), # SL - закрывает позицию, когда убыток достигает нужного уровня
|
|
||||||
orderLinkId=f"deal_{SYMBOL}_{time.time()}"
|
|
||||||
)
|
|
||||||
|
|
||||||
await info_access_open_deal(message, SYMBOL, data_main_stgs['trading_mode'], margin_mode, data_main_stgs['size_leverage'], next_quantity)
|
|
||||||
|
|
||||||
except exceptions.InvalidRequestError as e:
|
|
||||||
logging.error(f"Неверно указана торговая пара: {e}")
|
|
||||||
await message.answer('Недостаточно баланса')
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Ошибка при совершении сделки: {e}")
|
|
||||||
await message.answer('⚠️ Ошибка при совершении сделки')
|
|
||||||
|
|
||||||
async def contract_short(tg_id, message, margin_mode):
|
|
||||||
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)
|
|
||||||
data_risk_management_stgs = await rq.get_user_risk_management_settings(tg_id)
|
|
||||||
|
|
||||||
match margin_mode:
|
|
||||||
case 'Isolated':
|
|
||||||
margin_mode = 'ISOLATED_MARGIN'
|
|
||||||
case 'Cross':
|
|
||||||
margin_mode = 'REGULAR_MARGIN'
|
|
||||||
|
|
||||||
client = HTTP(
|
|
||||||
api_key=api_key,
|
|
||||||
api_secret=secret_key
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
balance = 0
|
|
||||||
price = 0
|
|
||||||
|
|
||||||
balance = await balance_g.get_balance(tg_id)
|
|
||||||
price = await price_symbol.get_price(tg_id, message)
|
|
||||||
|
|
||||||
client.set_margin_mode(
|
|
||||||
setMarginMode=margin_mode # margin_type
|
|
||||||
)
|
|
||||||
|
|
||||||
martingale_factor = float(data_main_stgs['martingale_factor']) # Исправлено: было maximal_quantity
|
|
||||||
max_martingale_steps = int(data_main_stgs['maximal_quantity'])
|
|
||||||
starting_quantity = float(data_main_stgs['starting_quantity'])
|
|
||||||
max_risk_percent = float(data_risk_management_stgs['max_risk_deal'])
|
|
||||||
loss_profit = float(data_risk_management_stgs['price_loss'])
|
|
||||||
takeprofit = float(data_risk_management_stgs['price_profit'])
|
|
||||||
commission_fee = float(data_risk_management_stgs.get('commission_fee', 0))
|
|
||||||
takeProfit_raw = takeprofit
|
|
||||||
takeProfit = takeProfit_raw - commission_fee # уменьшаем TP на комиссию
|
|
||||||
|
|
||||||
if takeProfit < 0:
|
|
||||||
takeProfit = 0
|
|
||||||
|
|
||||||
# Инициализация переменных
|
|
||||||
next_quantity = starting_quantity
|
|
||||||
last_quantity = starting_quantity
|
|
||||||
realised_pnl = 0.0
|
|
||||||
|
|
||||||
current_martingale_step = 0 # Текущая ставка в серии
|
|
||||||
|
|
||||||
next_quantity = 0
|
|
||||||
realised_pnl = 0
|
|
||||||
|
|
||||||
last_quantity = starting_quantity
|
|
||||||
|
|
||||||
# Пример расчёта следующего размера позиции
|
|
||||||
try:
|
|
||||||
position_info = client.get_positions(category='linear', symbol=SYMBOL)
|
|
||||||
position = position_info['result']['list'][0] # или другой нужный индекс
|
|
||||||
|
|
||||||
realised_pnl = float(position['unrealisedPnl'])
|
|
||||||
|
|
||||||
if realised_pnl > 0:
|
|
||||||
starting_quantity = next_quantity
|
|
||||||
current_martingale_step = 0
|
|
||||||
elif not realised_pnl:
|
|
||||||
next_quantity = starting_quantity
|
|
||||||
current_martingale_step += 1
|
|
||||||
else:
|
|
||||||
current_martingale_step += 1
|
|
||||||
next_quantity = last_quantity * martingale_factor
|
|
||||||
starting_quantity = next_quantity
|
|
||||||
except Exception as e:
|
|
||||||
print("Не получены позиции")
|
|
||||||
next_quantity = starting_quantity
|
|
||||||
|
|
||||||
potential_loss = (next_quantity * float(price)) * (loss_profit / 100)
|
|
||||||
allowed_loss = float(balance) * (max_risk_percent / 100)
|
|
||||||
|
|
||||||
if current_martingale_step >= max_martingale_steps:
|
|
||||||
print("Достигнут максимум ставок в серии (8)!")
|
|
||||||
print("Торговля не продолжится")
|
|
||||||
|
|
||||||
await error_max_step(message)
|
|
||||||
else:
|
|
||||||
if potential_loss > allowed_loss:
|
|
||||||
print(f"ОШИБКА: Риск превышен!")
|
|
||||||
print(f"Ручной qty = {next_quantity} → Убыток = {potential_loss} USDT")
|
|
||||||
print(f"Разрешено = {allowed_loss} USDT (1% от баланса)")
|
|
||||||
|
|
||||||
await error_max_risk(message)
|
|
||||||
else:
|
|
||||||
print(f"Риск в допустимых пределах. Qty = {next_quantity}")
|
|
||||||
|
|
||||||
r = client.place_order(
|
|
||||||
category='linear',
|
|
||||||
symbol=SYMBOL,
|
|
||||||
side='Sell',
|
|
||||||
orderType="Market",
|
|
||||||
leverage=int(data_main_stgs['size_leverage']),
|
|
||||||
qty=next_quantity,
|
|
||||||
orderLinkId=f"deal_{SYMBOL}_{time.time()}"
|
|
||||||
)
|
|
||||||
|
|
||||||
await info_access_open_deal(message, SYMBOL, data_main_stgs['trading_mode'], margin_mode, data_main_stgs['size_leverage'], next_quantity)
|
|
||||||
|
|
||||||
except exceptions.InvalidRequestError as e:
|
|
||||||
logging.error(f"Error in open_deal: {e}")
|
|
||||||
await message.answer('Недостаточно баланса')
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error in open_deal: {e}")
|
|
||||||
await message.answer('⚠️ Ошибка при совершении сделки')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def open_market_order(tg_id, message, api_key, secret_key):
|
|
||||||
data_main_stgs = await rq.get_user_main_settings(tg_id)
|
|
||||||
trading_mode = data_main_stgs['trading_mode']
|
|
||||||
margin_mode = data_main_stgs.get('margin_type')
|
|
||||||
|
|
||||||
if trading_mode == 'Long':
|
|
||||||
await contract_long(tg_id, message, margin_mode)
|
|
||||||
elif trading_mode == 'Short':
|
|
||||||
await contract_short(tg_id, message, margin_mode)
|
|
||||||
elif trading_mode == 'Smart':
|
|
||||||
await message.answer("Режим Smart пока недоступен")
|
|
||||||
elif trading_mode == 'Switch':
|
|
||||||
await message.answer("Режим Switch пока недоступен")
|
|
||||||
|
|
||||||
|
|
||||||
async def open_limit_order(tg_id, message, price, api_key, secret_key):
|
|
||||||
data_main_stgs = await rq.get_user_main_settings(tg_id)
|
|
||||||
trading_mode = data_main_stgs['trading_mode']
|
|
||||||
margin_mode = data_main_stgs.get('margin_type')
|
|
||||||
order_type = await rq.get_entry_order_type(tg_id)
|
|
||||||
|
|
||||||
|
|
||||||
client = HTTP(
|
|
||||||
api_key=api_key,
|
|
||||||
api_secret=secret_key
|
|
||||||
)
|
|
||||||
|
|
||||||
symbol = await rq.get_symbol(tg_id)
|
symbol = await rq.get_symbol(tg_id)
|
||||||
qty = float(data_main_stgs['starting_quantity'])
|
|
||||||
side = 'Buy' if trading_mode == 'Long' else 'Short'
|
|
||||||
|
|
||||||
|
data_main_stgs = await rq.get_user_main_settings(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:
|
try:
|
||||||
|
balance = await balance_g.get_balance(tg_id)
|
||||||
|
price = await price_symbol.get_price(tg_id, message)
|
||||||
|
|
||||||
|
# Установка маржинального режима
|
||||||
|
client.set_margin_mode(setMarginMode=bybit_margin_mode)
|
||||||
|
|
||||||
|
martingale_factor = float(data_main_stgs['martingale_factor'])
|
||||||
|
max_martingale_steps = int(data_main_stgs['maximal_quantity'])
|
||||||
|
starting_quantity = float(data_main_stgs['starting_quantity'])
|
||||||
|
max_risk_percent = float(data_risk_stgs['max_risk_deal'])
|
||||||
|
loss_profit = float(data_risk_stgs['price_loss'])
|
||||||
|
takeprofit = float(data_risk_stgs['price_profit'])
|
||||||
|
commission_fee = float(data_risk_stgs.get('commission_fee', 0))
|
||||||
|
|
||||||
|
takeProfit = max(takeprofit - commission_fee, 0)
|
||||||
|
|
||||||
|
current_martingale_step = 0
|
||||||
|
next_quantity = starting_quantity
|
||||||
|
last_quantity = starting_quantity
|
||||||
|
realised_pnl = 0.0
|
||||||
|
|
||||||
|
# Получаем текущие открытые позиции по символу
|
||||||
|
positions_resp = client.get_positions(category='linear', symbol=symbol)
|
||||||
|
positions_list = positions_resp.get('result', {}).get('list', [])
|
||||||
|
current_martingale_step = await rq.get_martingale_step(tg_id)
|
||||||
|
if positions_list:
|
||||||
|
position = positions_list[0]
|
||||||
|
realised_pnl = float(position.get('unrealisedPnl', 0.0))
|
||||||
|
|
||||||
|
|
||||||
|
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 = starting_quantity * (martingale_factor ** current_martingale_step)
|
||||||
|
else:
|
||||||
|
# Позиция не открыта — начинаем с начальной ставки
|
||||||
|
next_quantity = starting_quantity
|
||||||
|
current_martingale_step = 0
|
||||||
|
|
||||||
|
# Проверяем риск убытка
|
||||||
|
potential_loss = next_quantity * price * (loss_profit / 100)
|
||||||
|
allowed_loss = balance * (max_risk_percent / 100)
|
||||||
|
|
||||||
|
if potential_loss > allowed_loss:
|
||||||
|
await error_max_risk(message)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Отправляем запрос на открытие ордера
|
||||||
response = client.place_order(
|
response = client.place_order(
|
||||||
category='linear',
|
category='linear',
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
side=side,
|
side=side,
|
||||||
orderType='Limit',
|
orderType="Market",
|
||||||
qty=qty,
|
qty=next_quantity,
|
||||||
price=price,
|
leverage=int(data_main_stgs['size_leverage']),
|
||||||
timeInForce='GTC',
|
takeProfit=takeProfit,
|
||||||
orderLinkId=f"order_{int(time.time())}"
|
stopLoss=loss_profit,
|
||||||
|
orderLinkId=f"deal_{symbol}_{int(time.time())}"
|
||||||
)
|
)
|
||||||
if response.get('retCode') == 0:
|
|
||||||
await message.answer(f"Limit ордер открыт: {side} {qty} {symbol} по цене {price}")
|
|
||||||
await rq.update_user_trades(tg_id, symbol=symbol, side=order_type)
|
|
||||||
|
|
||||||
|
if response.get('ret_code', -1) == 0:
|
||||||
|
await info_access_open_deal(message, symbol, data_main_stgs['trading_mode'], bybit_margin_mode,
|
||||||
|
data_main_stgs['size_leverage'], next_quantity)
|
||||||
|
await rq.update_martingale_step(tg_id, current_martingale_step)
|
||||||
else:
|
else:
|
||||||
await message.answer(f"Ошибка открытия ордера: {response.get('retMsg')}")
|
await message.answer(f"Ошибка открытия ордера: {response.get('ret_msg', 'неизвестная ошибка')}")
|
||||||
|
|
||||||
|
except exceptions.InvalidRequestError as e:
|
||||||
|
logging.error(f"InvalidRequestError: {e}")
|
||||||
|
await message.answer('Ошибка: неверно указана торговая пара или параметры.')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Ошибка при открытии лимитного ордера: {e}")
|
logging.error(f"Ошибка при совершении сделки: {e}")
|
||||||
await message.answer("Ошибка при открытии ордера")
|
await message.answer('⚠️ Ошибка при совершении сделки')
|
||||||
|
|
||||||
|
|
||||||
|
async def trading_cycle(tg_id, message):
|
||||||
|
start_time = time.time()
|
||||||
|
timer_min = await rq.get_user_timer(tg_id)
|
||||||
|
timer_sec = timer_min * 60 if timer_min else 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
if timer_sec > 0 and elapsed > timer_sec:
|
||||||
|
await message.answer("Время работы по таймеру истекло. Торговля остановлена.")
|
||||||
|
await rq.update_martingale_step(tg_id, 0)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Проверяем позиции
|
||||||
|
data_main_stgs = await rq.get_user_main_settings(tg_id)
|
||||||
|
side = 'Buy' if data_main_stgs['trading_mode'] == 'Long' else 'Sell'
|
||||||
|
margin_mode = data_main_stgs.get('margin_type', 'Isolated')
|
||||||
|
|
||||||
|
# Можно добавлять логику по PNL, стоп-лоссам, тейк-профитам
|
||||||
|
|
||||||
|
await open_position(tg_id, message, side=side, margin_mode=margin_mode)
|
||||||
|
|
||||||
|
await asyncio.sleep(10)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def get_active_positions(message, api_key, secret_key, symbol):
|
||||||
|
|
||||||
|
client = HTTP(
|
||||||
|
api_key=api_key,
|
||||||
|
api_secret=secret_key
|
||||||
|
)
|
||||||
|
instruments_resp = client.get_instruments_info(category='linear')
|
||||||
|
if instruments_resp.get('ret_code') != 0:
|
||||||
|
return []
|
||||||
|
symbols = [item['symbol'] for item in instruments_resp.get('result', {}).get('list', [])]
|
||||||
|
|
||||||
|
active_positions = []
|
||||||
|
|
||||||
|
async def fetch_positions(symbol):
|
||||||
|
try:
|
||||||
|
resp = client.get_positions(category='linear', symbol=symbol)
|
||||||
|
if resp.get('ret_code') == 0:
|
||||||
|
positions = resp.get('result', {}).get('list', [])
|
||||||
|
for pos in positions:
|
||||||
|
if pos.get('size') and float(pos['size']) > 0:
|
||||||
|
active_positions.append(pos)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Ошибка при получении позиций: {e}")
|
||||||
|
await message.answer('⚠️ Ошибка при получении позиций')
|
||||||
|
|
||||||
|
for sym in symbols:
|
||||||
|
await fetch_positions(sym)
|
||||||
|
|
||||||
|
return active_positions
|
||||||
|
|
||||||
|
|
||||||
|
async def close_user_trade(tg_id: int, symbol: str) -> bool:
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Получаем текущие открытые позиции по символу (пример для linear фьючерсов)
|
||||||
|
positions_resp = client.get_positions(category="linear", symbol=symbol)
|
||||||
|
|
||||||
|
ret_code = positions_resp.get('ret_code')
|
||||||
|
result = positions_resp.get('result')
|
||||||
|
|
||||||
|
if ret_code != 0 or not result or not result.get('list'):
|
||||||
|
return False
|
||||||
|
|
||||||
|
positions_list = result['list']
|
||||||
|
if not positions_list:
|
||||||
|
return False
|
||||||
|
|
||||||
|
position = positions_list[0]
|
||||||
|
qty = abs(float(position['size']))
|
||||||
|
side = position['side']
|
||||||
|
|
||||||
|
if qty == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Определяем сторону закрытия — противоположная открытой позиции
|
||||||
|
close_side = "Sell" if side == "Buy" else "Buy"
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = client.place_order(
|
||||||
|
category="linear",
|
||||||
|
symbol=symbol,
|
||||||
|
side=close_side,
|
||||||
|
orderType="Market",
|
||||||
|
qty=str(qty),
|
||||||
|
timeInForce="GoodTillCancel",
|
||||||
|
reduceOnly=True
|
||||||
|
)
|
||||||
|
return response['ret_code'] == 0
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Ошибка закрытия сделки {symbol} для пользователя {tg_id}: {e}")
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
@@ -1,10 +1,13 @@
|
|||||||
from aiogram import F, Router
|
import asyncio
|
||||||
|
|
||||||
|
from aiogram import F, Router
|
||||||
|
|
||||||
from app.services.Bybit.functions import Futures, func_min_qty
|
from app.services.Bybit.functions import Futures, func_min_qty
|
||||||
from app.services.Bybit.functions.Futures import open_market_order, open_limit_order
|
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
|
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
|
||||||
|
import app.telegram.Keyboards.reply_keyboards as reply_markup
|
||||||
|
from pybit.unified_trading import HTTP
|
||||||
import app.telegram.database.requests as rq
|
import app.telegram.database.requests as rq
|
||||||
from aiogram.types import Message, CallbackQuery
|
from aiogram.types import Message, CallbackQuery
|
||||||
|
|
||||||
@@ -14,13 +17,21 @@ from aiogram.fsm.context import FSMContext
|
|||||||
|
|
||||||
router_functions_bybit_trade = Router()
|
router_functions_bybit_trade = Router()
|
||||||
|
|
||||||
|
|
||||||
class state_update_symbol(StatesGroup):
|
class state_update_symbol(StatesGroup):
|
||||||
symbol = State()
|
symbol = State()
|
||||||
|
|
||||||
|
|
||||||
class state_update_entry_type(StatesGroup):
|
class state_update_entry_type(StatesGroup):
|
||||||
entry_type = State()
|
entry_type = State()
|
||||||
|
|
||||||
@router_functions_bybit_trade.callback_query(F.data.in_(['clb_start_trading', 'clb_back_to_main']))
|
|
||||||
|
class TradeSetup(StatesGroup):
|
||||||
|
waiting_for_timer = State()
|
||||||
|
waiting_for_positive_percent = 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, state: FSMContext):
|
||||||
api = await rq.get_bybit_api_key(callback.from_user.id)
|
api = await rq.get_bybit_api_key(callback.from_user.id)
|
||||||
secret = await rq.get_bybit_secret_key(callback.from_user.id)
|
secret = await rq.get_bybit_secret_key(callback.from_user.id)
|
||||||
@@ -30,7 +41,7 @@ async def clb_start_bybit_trade_message(callback: CallbackQuery, state: FSMConte
|
|||||||
symbol = await rq.get_symbol(callback.from_user.id)
|
symbol = await rq.get_symbol(callback.from_user.id)
|
||||||
|
|
||||||
text = f'''💎 Торговля на Bybit
|
text = f'''💎 Торговля на Bybit
|
||||||
|
|
||||||
⚖️ Ваш баланс (USDT): {balance}
|
⚖️ Ваш баланс (USDT): {balance}
|
||||||
📊 Текущая торговая пара: {symbol}
|
📊 Текущая торговая пара: {symbol}
|
||||||
|
|
||||||
@@ -41,16 +52,17 @@ async def clb_start_bybit_trade_message(callback: CallbackQuery, state: FSMConte
|
|||||||
'''
|
'''
|
||||||
await callback.message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup)
|
await callback.message.edit_text(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup)
|
||||||
|
|
||||||
|
|
||||||
async def start_bybit_trade_message(message, state):
|
async def start_bybit_trade_message(message, state):
|
||||||
api = await rq.get_bybit_api_key(message.from_user.id)
|
api = await rq.get_bybit_api_key(message.from_user.id)
|
||||||
secret = await rq.get_bybit_secret_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)
|
balance = await get_balance(message.from_user.id, message)
|
||||||
|
|
||||||
if balance:
|
if balance:
|
||||||
symbol = await rq.get_symbol(message.from_user.id)
|
symbol = await rq.get_symbol(message.from_user.id)
|
||||||
|
|
||||||
text = f'''💎 Торговля на Bybit
|
text = f'''💎 Торговля на Bybit
|
||||||
|
|
||||||
⚖️ Ваш баланс (USDT): {balance}
|
⚖️ Ваш баланс (USDT): {balance}
|
||||||
📊 Текущая торговая пара: {symbol}
|
📊 Текущая торговая пара: {symbol}
|
||||||
|
|
||||||
@@ -62,15 +74,18 @@ async def start_bybit_trade_message(message, state):
|
|||||||
|
|
||||||
await message.answer(text=text, parse_mode='html', reply_markup=inline_markup.trading_markup)
|
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')
|
@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):
|
||||||
await state.set_state(state_update_symbol.symbol)
|
await state.set_state(state_update_symbol.symbol)
|
||||||
|
|
||||||
await callback.message.answer(text='Укажите торговую пару заглавными буквами без пробелов и лишних символов (пример: BTCUSDT): ')
|
await callback.message.answer(
|
||||||
|
text='Укажите торговую пару заглавными буквами без пробелов и лишних символов (пример: BTCUSDT): ')
|
||||||
|
|
||||||
|
|
||||||
@router_functions_bybit_trade.message(state_update_symbol.symbol)
|
@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):
|
||||||
await state.update_data(symbol = message.text)
|
await state.update_data(symbol=message.text)
|
||||||
|
|
||||||
data = await state.get_data()
|
data = await state.get_data()
|
||||||
|
|
||||||
@@ -96,116 +111,161 @@ async def entry_order_type_callback(callback: CallbackQuery, state: FSMContext):
|
|||||||
await callback.answer("Ошибка выбора", show_alert=True)
|
await callback.answer("Ошибка выбора", show_alert=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
await state.update_data(entry_order_type=order_type)
|
|
||||||
await rq.update_entry_order_type(callback.from_user.id, order_type)
|
|
||||||
|
|
||||||
if order_type == 'Limit':
|
|
||||||
await callback.answer("Вы выбрали Limit. Введите цену для лимитного ордера.")
|
|
||||||
await callback.message.answer("Введите цену лимитного ордера:")
|
|
||||||
await state.update_data(awaiting_limit_price=True)
|
|
||||||
else:
|
|
||||||
await callback.answer("Вы выбрали Market. Нажмите кнопку ниже, чтобы открыть сделку.")
|
|
||||||
await callback.message.answer("Нажмите кнопку, чтобы открыть сделку.",
|
|
||||||
reply_markup=inline_markup.open_deal_markup)
|
|
||||||
await callback.message.delete()
|
|
||||||
|
|
||||||
|
|
||||||
@router_functions_bybit_trade.message()
|
|
||||||
async def process_limit_price_message(message: Message, state: FSMContext):
|
|
||||||
data = await state.get_data()
|
|
||||||
if not data.get('awaiting_limit_price'):
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
price = float(message.text)
|
await state.update_data(entry_order_type=order_type)
|
||||||
if price <= 0:
|
await rq.update_entry_order_type(callback.from_user.id, order_type)
|
||||||
raise ValueError()
|
await callback.answer("Тип входа в позицию был успешно обновлен")
|
||||||
except ValueError:
|
except Exception as e:
|
||||||
await message.answer("Ошибка: введите корректное положительное число для цены.")
|
await callback.message.answer("Произошла ошибка при обновлении типа входа в позицию")
|
||||||
return
|
|
||||||
|
|
||||||
await state.update_data(limit_price=price, awaiting_limit_price=False)
|
|
||||||
|
|
||||||
await message.answer(f"Цена лимитного ордера установлена: {price}. Нажмите кнопку ниже, чтобы открыть сделку.",
|
|
||||||
reply_markup=inline_markup.open_deal_markup)
|
|
||||||
|
|
||||||
|
|
||||||
@router_functions_bybit_trade.callback_query(F.data == "clb_open_deal")
|
|
||||||
async def open_deal(callback: CallbackQuery, state: FSMContext):
|
|
||||||
data_main_stgs = await rq.get_user_main_settings(callback.from_user.id)
|
|
||||||
data = await state.get_data()
|
|
||||||
order_type = await rq.get_entry_order_type(callback.from_user.id)
|
|
||||||
api = await rq.get_bybit_api_key(callback.from_user.id)
|
|
||||||
secret = await rq.get_bybit_secret_key(callback.from_user.id)
|
|
||||||
qty = data_main_stgs['starting_quantity']
|
|
||||||
qty_min = await func_min_qty.get_min_qty(callback.from_user.id, callback.message)
|
|
||||||
|
|
||||||
if qty < qty_min:
|
|
||||||
await callback.message.edit_text(f"Количество вашей ставки ({qty}) меньше минимального количества ({qty_min}) для данной торговой пары")
|
|
||||||
await callback.answer()
|
|
||||||
return
|
|
||||||
|
|
||||||
if order_type == 'Market':
|
|
||||||
await open_market_order(callback.from_user.id, callback.message, api_key=api, secret_key=secret)
|
|
||||||
await rq.update_user_trades(callback.from_user.id, symbol=data.get('symbol'), side=order_type)
|
|
||||||
elif order_type == 'Limit':
|
|
||||||
price = data.get('limit_price')
|
|
||||||
if not price:
|
|
||||||
await callback.answer("Цена для лимитного ордера не задана. Введите сначала цену.")
|
|
||||||
return
|
|
||||||
await open_limit_order(callback.from_user.id, callback.message, price, api_key=api, secret_key=secret)
|
|
||||||
else:
|
|
||||||
await callback.answer("Неизвестный тип ордера.")
|
|
||||||
|
|
||||||
await callback.message.edit_reply_markup()
|
|
||||||
await state.clear()
|
await state.clear()
|
||||||
|
|
||||||
|
|
||||||
@router_functions_bybit_trade.callback_query(F.data == "clb_my_deals")
|
@router_functions_bybit_trade.callback_query(F.data == "clb_my_deals")
|
||||||
async def show_my_trades_callback(callback: CallbackQuery):
|
async def show_my_trades_callback(callback: CallbackQuery):
|
||||||
tg_id = callback.from_user.id
|
tg_id = callback.from_user.id
|
||||||
trades = await rq.get_user_trades(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)
|
||||||
|
|
||||||
|
trades = await get_active_positions(callback.message, api_key, secret_key, symbol)
|
||||||
|
|
||||||
if not trades:
|
if not trades:
|
||||||
await callback.message.answer("У вас ещё нет сделок.")
|
await callback.message.answer("Нет активных позиций.")
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
return
|
return
|
||||||
|
|
||||||
grouped = {}
|
keyboard = inline_markup.create_trades_inline_keyboard(trades)
|
||||||
for trade in trades:
|
|
||||||
symbol = trade['symbol'] if isinstance(trade, dict) else trade.symbol
|
|
||||||
grouped.setdefault(symbol, []).append(trade)
|
|
||||||
|
|
||||||
text_response = "Ваши сделки по валютным парам:\n\n"
|
await callback.message.answer(
|
||||||
for symbol, trade_list in grouped.items():
|
"Выберите сделку из списка:",
|
||||||
text_response += f"<b>{symbol}</b>\n"
|
reply_markup=keyboard
|
||||||
for t in trade_list:
|
)
|
||||||
side = t['side'] if isinstance(t, dict) else t.side
|
|
||||||
text_response += f" - {side}\n"
|
|
||||||
text_response += "\n"
|
|
||||||
|
|
||||||
await callback.message.answer(text_response, parse_mode='html')
|
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
# @router_functions_bybit_trade.callback_query(F.data == 'clb_open_deal')
|
@router_functions_bybit_trade.callback_query(lambda c: c.data and c.data.startswith('select_trade:'))
|
||||||
# async def make_deal_bybit (callback: CallbackQuery):
|
async def on_trade_selected(callback: CallbackQuery):
|
||||||
# data_main_stgs = await rq.get_user_main_settings(callback.from_user.id)
|
symbol = callback.data.split(':')[1]
|
||||||
#
|
|
||||||
# trade_mode = data_main_stgs['trading_mode']
|
tg_id = callback.from_user.id
|
||||||
# qty = data_main_stgs['starting_quantity']
|
api_key = await rq.get_bybit_api_key(tg_id)
|
||||||
# margin_mode = data_main_stgs['margin_type']
|
secret_key = await rq.get_bybit_secret_key(tg_id)
|
||||||
qty_min = await func_min_qty.get_min_qty(callback.from_user.id, callback.message)
|
|
||||||
#
|
positions = await get_active_positions(callback.message, api_key, secret_key, symbol)
|
||||||
# if qty < qty_min:
|
|
||||||
# await callback.message.edit_text(f"Количество вашей ставки ({qty}) меньше минимального количества ({qty_min}) для данной торговой пары")
|
# Если несколько позиций по символу, можно выбрать нужную или взять первую
|
||||||
# else:
|
if not positions:
|
||||||
# match trade_mode:
|
await callback.message.answer("Позиция не найдена")
|
||||||
# case 'Long':
|
await callback.answer()
|
||||||
# await Futures.contract_long(callback.from_user.id, callback.message, margin_mode)
|
return
|
||||||
# case 'Short':
|
|
||||||
# await Futures.contract_short(callback.from_user.id, callback.message, margin_mode)
|
pos = positions[0]
|
||||||
# case 'Switch':
|
symbol = pos.get('symbol')
|
||||||
# await callback.message.edit_text('Режим Switch пока недоступен')
|
side = pos.get('side')
|
||||||
# case 'Smart':
|
entry_price = pos.get('entryPrice') # Цена открытия позиции
|
||||||
# await callback.message.edit_text('Режим Smart пока недоступен')
|
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:
|
||||||
|
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)
|
||||||
|
await open_position(tg_id, message, side=side, margin_mode=margin_mode)
|
||||||
|
|
||||||
|
|
||||||
|
# Проверяем таймер и информируем пользователя
|
||||||
|
timer_minutes = await rq.get_user_timer(tg_id)
|
||||||
|
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()
|
||||||
|
|
||||||
|
@@ -27,4 +27,6 @@ async def check_profile_message(message, username):
|
|||||||
await message.answer(f'С возвращением, {username}!', reply_markup=reply_markup.base_buttons_markup)
|
await message.answer(f'С возвращением, {username}!', reply_markup=reply_markup.base_buttons_markup)
|
||||||
|
|
||||||
async def settings_message(message):
|
async def settings_message(message):
|
||||||
await message.edit_text("Выберите что настроить", reply_markup=inline_markup.special_settings_markup)
|
await message.edit_text("Выберите что настроить", reply_markup=inline_markup.special_settings_markup)
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user