The entire database has been changed to PostgresSQL. The entire code has been updated.
This commit is contained in:
21
app/bybit/__init__.py
Normal file
21
app/bybit/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import logging.config
|
||||
|
||||
from pybit.unified_trading import HTTP
|
||||
|
||||
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
|
||||
from database import request as rq
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("bybit")
|
||||
|
||||
|
||||
async def get_bybit_client(tg_id: int) -> HTTP | None:
|
||||
"""
|
||||
Get bybit client
|
||||
"""
|
||||
try:
|
||||
api_key, api_secret = await rq.get_user_api(tg_id=tg_id)
|
||||
return HTTP(api_key=api_key, api_secret=api_secret)
|
||||
except Exception as e:
|
||||
logger.error("Error getting bybit client for user %s: %s", tg_id, e)
|
||||
return None
|
101
app/bybit/close_positions.py
Normal file
101
app/bybit/close_positions.py
Normal file
@@ -0,0 +1,101 @@
|
||||
import logging.config
|
||||
|
||||
from app.bybit import get_bybit_client
|
||||
from app.bybit.get_functions.get_positions import get_active_positions_by_symbol
|
||||
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("close_positions")
|
||||
|
||||
|
||||
async def close_position(tg_id: int, symbol: str) -> bool:
|
||||
"""
|
||||
Closes position
|
||||
:param tg_id: Telegram user ID
|
||||
:param symbol: symbol
|
||||
:return: bool
|
||||
"""
|
||||
try:
|
||||
client = await get_bybit_client(tg_id)
|
||||
active_positions = await get_active_positions_by_symbol(tg_id, symbol)
|
||||
side = active_positions.get("side")
|
||||
size = active_positions.get("size")
|
||||
position_idx = active_positions.get("positionIdx")
|
||||
|
||||
if side == "Buy":
|
||||
side = "Sell"
|
||||
elif side == "Sell":
|
||||
side = "Buy"
|
||||
|
||||
response = client.place_order(
|
||||
category="linear",
|
||||
symbol=symbol,
|
||||
side=side,
|
||||
orderType="Market",
|
||||
qty=size,
|
||||
timeInForce="GTC",
|
||||
positionIdx=position_idx,
|
||||
)
|
||||
if response["retCode"] == 0:
|
||||
logger.info("Position closed for %s for user %s", symbol, tg_id)
|
||||
return True
|
||||
else:
|
||||
logger.error("Error closing position for %s for user %s", symbol, tg_id)
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error("Error closing position for %s for user %s: %s", symbol, tg_id, e)
|
||||
return False
|
||||
|
||||
|
||||
async def cancel_order(tg_id, symbol) -> bool:
|
||||
"""
|
||||
Cancel order by order id
|
||||
"""
|
||||
try:
|
||||
client = await get_bybit_client(tg_id)
|
||||
orders_resp = client.get_open_orders(category="linear", symbol=symbol)
|
||||
orders = orders_resp.get("result", {}).get("list", [])
|
||||
|
||||
for order in orders:
|
||||
order_id = order.get("orderId")
|
||||
cancel_resp = client.cancel_order(
|
||||
category="linear", symbol=symbol, orderId=order_id
|
||||
)
|
||||
|
||||
if cancel_resp.get("retCode") == 0:
|
||||
return True
|
||||
else:
|
||||
logger.error(
|
||||
"Error canceling order for user %s: %s",
|
||||
tg_id,
|
||||
cancel_resp.get("retMsg"),
|
||||
)
|
||||
return False
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error("Error canceling order for user %s: %s", tg_id, e)
|
||||
return False
|
||||
|
||||
|
||||
async def cancel_all_orders(tg_id: int) -> bool:
|
||||
"""
|
||||
Cancel all open orders
|
||||
"""
|
||||
try:
|
||||
client = await get_bybit_client(tg_id)
|
||||
cancel_resp = client.cancel_all_orders(category="linear", settleCoin="USDT")
|
||||
|
||||
if cancel_resp.get("retCode") == 0:
|
||||
logger.info("All orders canceled for user %s", tg_id)
|
||||
return True
|
||||
else:
|
||||
logger.error(
|
||||
"Error canceling order for user %s: %s",
|
||||
tg_id,
|
||||
cancel_resp.get("retMsg"),
|
||||
)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error canceling order for user %s: %s", tg_id, e)
|
||||
return False
|
0
app/bybit/get_functions/__init__.py
Normal file
0
app/bybit/get_functions/__init__.py
Normal file
28
app/bybit/get_functions/get_balance.py
Normal file
28
app/bybit/get_functions/get_balance.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import logging.config
|
||||
|
||||
from app.bybit import get_bybit_client
|
||||
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("get_balance")
|
||||
|
||||
|
||||
async def get_balance(tg_id: int) -> bool | dict:
|
||||
"""
|
||||
Get balance bybit
|
||||
"""
|
||||
client = await get_bybit_client(tg_id=tg_id)
|
||||
|
||||
try:
|
||||
response = client.get_wallet_balance(accountType="UNIFIED")
|
||||
if response["retCode"] == 0:
|
||||
info = response["result"]["list"][0]
|
||||
return info
|
||||
else:
|
||||
logger.error(
|
||||
"Error getting balance for user %s: %s", tg_id, response.get("retMsg")
|
||||
)
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error("Error connecting to Bybit for user %s: %s", tg_id, e)
|
||||
return False
|
28
app/bybit/get_functions/get_instruments_info.py
Normal file
28
app/bybit/get_functions/get_instruments_info.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import logging.config
|
||||
|
||||
from app.bybit import get_bybit_client
|
||||
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("get_instruments_info")
|
||||
|
||||
|
||||
async def get_instruments_info(tg_id: int, symbol: str) -> dict | None:
|
||||
"""
|
||||
Get instruments info
|
||||
:param tg_id: int - User ID
|
||||
:param symbol: str - Symbol
|
||||
:return: dict - Instruments info
|
||||
"""
|
||||
try:
|
||||
client = await get_bybit_client(tg_id=tg_id)
|
||||
response = client.get_instruments_info(category="linear", symbol=symbol)
|
||||
if response["retCode"] == 0:
|
||||
logger.info("Instruments info for user: %s", tg_id)
|
||||
return response["result"]["list"][0]
|
||||
else:
|
||||
logger.error("Error getting price: %s", tg_id)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error("Error connecting to Bybit for user %s: %s", tg_id, e)
|
||||
return None
|
129
app/bybit/get_functions/get_positions.py
Normal file
129
app/bybit/get_functions/get_positions.py
Normal file
@@ -0,0 +1,129 @@
|
||||
import logging.config
|
||||
|
||||
from app.bybit import get_bybit_client
|
||||
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("get_positions")
|
||||
|
||||
|
||||
async def get_active_positions(tg_id: int) -> list | None:
|
||||
"""
|
||||
Get active positions for a user
|
||||
"""
|
||||
try:
|
||||
client = await get_bybit_client(tg_id)
|
||||
response = client.get_positions(category="linear", settleCoin="USDT")
|
||||
|
||||
if response["retCode"] == 0:
|
||||
positions = response.get("result", {}).get("list", [])
|
||||
active_symbols = [
|
||||
pos.get("symbol") for pos in positions if float(pos.get("size", 0)) > 0
|
||||
]
|
||||
if active_symbols:
|
||||
logger.info("Active positions for user: %s", tg_id)
|
||||
return active_symbols
|
||||
else:
|
||||
logger.warning("No active positions found for user: %s", tg_id)
|
||||
return ["No active positions found"]
|
||||
else:
|
||||
logger.error(
|
||||
"Error getting active positions for user %s: %s",
|
||||
tg_id,
|
||||
response["retMsg"],
|
||||
)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error("Error getting active positions for user %s: %s", tg_id, e)
|
||||
return None
|
||||
|
||||
|
||||
async def get_active_positions_by_symbol(tg_id: int, symbol: str) -> dict | None:
|
||||
"""
|
||||
Get active positions for a user by symbol
|
||||
"""
|
||||
try:
|
||||
client = await get_bybit_client(tg_id)
|
||||
response = client.get_positions(category="linear", symbol=symbol)
|
||||
|
||||
if response["retCode"] == 0:
|
||||
positions = response.get("result", {}).get("list", [])
|
||||
if positions:
|
||||
logger.info("Active positions for user: %s", tg_id)
|
||||
return positions[0]
|
||||
else:
|
||||
logger.warning("No active positions found for user: %s", tg_id)
|
||||
return None
|
||||
else:
|
||||
logger.error(
|
||||
"Error getting active positions for user %s: %s",
|
||||
tg_id,
|
||||
response["retMsg"],
|
||||
)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error("Error getting active positions for user %s: %s", tg_id, e)
|
||||
return None
|
||||
|
||||
|
||||
async def get_active_orders(tg_id: int) -> list | None:
|
||||
"""
|
||||
Get active orders
|
||||
"""
|
||||
try:
|
||||
client = await get_bybit_client(tg_id)
|
||||
response = client.get_open_orders(
|
||||
category="linear",
|
||||
settleCoin="USDT",
|
||||
limit=50,
|
||||
)
|
||||
|
||||
if response["retCode"] == 0:
|
||||
orders = response.get("result", {}).get("list", [])
|
||||
active_orders = [
|
||||
pos.get("symbol") for pos in orders if float(pos.get("qty", 0)) > 0
|
||||
]
|
||||
if active_orders:
|
||||
logger.info("Active orders for user: %s", tg_id)
|
||||
return active_orders
|
||||
else:
|
||||
logger.warning("No active orders found for user: %s", tg_id)
|
||||
return ["No active orders found"]
|
||||
else:
|
||||
logger.error(
|
||||
"Error getting active orders for user %s: %s", tg_id, response["retMsg"]
|
||||
)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error("Error getting active orders for user %s: %s", tg_id, e)
|
||||
return None
|
||||
|
||||
|
||||
async def get_active_orders_by_symbol(tg_id: int, symbol: str) -> dict | None:
|
||||
"""
|
||||
Get active orders by symbol
|
||||
"""
|
||||
try:
|
||||
client = await get_bybit_client(tg_id)
|
||||
response = client.get_open_orders(
|
||||
category="linear",
|
||||
symbol=symbol,
|
||||
limit=50,
|
||||
)
|
||||
|
||||
if response["retCode"] == 0:
|
||||
orders = response.get("result", {}).get("list", [])
|
||||
if orders:
|
||||
logger.info("Active orders for user: %s", tg_id)
|
||||
return orders[0]
|
||||
else:
|
||||
logger.warning("No active orders found for user: %s", tg_id)
|
||||
return None
|
||||
else:
|
||||
logger.error(
|
||||
"Error getting active orders for user %s: %s", tg_id, response["retMsg"]
|
||||
)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error("Error getting active orders for user %s: %s", tg_id, e)
|
||||
return None
|
35
app/bybit/get_functions/get_tickers.py
Normal file
35
app/bybit/get_functions/get_tickers.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import logging.config
|
||||
|
||||
from app.bybit import get_bybit_client
|
||||
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("get_tickers")
|
||||
|
||||
|
||||
async def get_tickers(tg_id: int, symbol: str) -> dict | None:
|
||||
"""
|
||||
Get tickers
|
||||
:param tg_id: int Telegram ID
|
||||
:param symbol: str Symbol
|
||||
:return: dict
|
||||
"""
|
||||
try:
|
||||
client = await get_bybit_client(tg_id=tg_id)
|
||||
response = client.get_tickers(category="linear", symbol=symbol)
|
||||
if response["retCode"] == 0:
|
||||
tickers = response["result"]["list"]
|
||||
# USDT quoteCoin
|
||||
usdt_tickers = [t for t in tickers if t.get("symbol", "").endswith("USDT")]
|
||||
if usdt_tickers:
|
||||
logger.info("USDT tickers for user: %s", tg_id)
|
||||
return usdt_tickers[0]
|
||||
else:
|
||||
logger.warning("No USDT tickers found for user: %s", tg_id)
|
||||
return None
|
||||
else:
|
||||
logger.error("Error getting price: %s", tg_id)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error("Error connecting to Bybit for user %s: %s", tg_id, e)
|
||||
return None
|
0
app/bybit/logger_bybit/__init__.py
Normal file
0
app/bybit/logger_bybit/__init__.py
Normal file
129
app/bybit/logger_bybit/logger_bybit.py
Normal file
129
app/bybit/logger_bybit/logger_bybit.py
Normal file
@@ -0,0 +1,129 @@
|
||||
import os
|
||||
|
||||
current_directory = os.path.dirname(os.path.abspath(__file__))
|
||||
log_directory = os.path.join(current_directory, "loggers")
|
||||
error_log_directory = os.path.join(log_directory, "errors")
|
||||
os.makedirs(log_directory, exist_ok=True)
|
||||
os.makedirs(error_log_directory, exist_ok=True)
|
||||
log_filename = os.path.join(log_directory, "app.log")
|
||||
error_log_filename = os.path.join(error_log_directory, "error.log")
|
||||
|
||||
LOGGING_CONFIG = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"default": {
|
||||
"format": "BYBIT: %(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
"datefmt": "%Y-%m-%d %H:%M:%S", # Формат даты
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"timed_rotating_file": {
|
||||
"class": "logging.handlers.TimedRotatingFileHandler",
|
||||
"filename": log_filename,
|
||||
"when": "midnight", # Время ротации (каждую полночь)
|
||||
"interval": 1, # Интервал в днях
|
||||
"backupCount": 7, # Количество сохраняемых архивов (0 - не сохранять)
|
||||
"formatter": "default",
|
||||
"encoding": "utf-8",
|
||||
"level": "DEBUG",
|
||||
},
|
||||
"error_file": {
|
||||
"class": "logging.handlers.TimedRotatingFileHandler",
|
||||
"filename": error_log_filename,
|
||||
"when": "midnight",
|
||||
"interval": 1,
|
||||
"backupCount": 30,
|
||||
"formatter": "default",
|
||||
"encoding": "utf-8",
|
||||
"level": "ERROR",
|
||||
},
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "default",
|
||||
"level": "DEBUG",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"profile_bybit": {
|
||||
"handlers": ["console", "timed_rotating_file", "error_file"],
|
||||
"level": "DEBUG",
|
||||
"propagate": False,
|
||||
},
|
||||
"get_balance": {
|
||||
"handlers": ["console", "timed_rotating_file", "error_file"],
|
||||
"level": "DEBUG",
|
||||
"propagate": False,
|
||||
},
|
||||
"price_symbol": {
|
||||
"handlers": ["console", "timed_rotating_file", "error_file"],
|
||||
"level": "DEBUG",
|
||||
"propagate": False,
|
||||
},
|
||||
"bybit": {
|
||||
"handlers": ["console", "timed_rotating_file", "error_file"],
|
||||
"level": "DEBUG",
|
||||
"propagate": False,
|
||||
},
|
||||
"web_socket": {
|
||||
"handlers": ["console", "timed_rotating_file", "error_file"],
|
||||
"level": "DEBUG",
|
||||
"propagate": False,
|
||||
},
|
||||
"get_tickers": {
|
||||
"handlers": ["console", "timed_rotating_file", "error_file"],
|
||||
"level": "DEBUG",
|
||||
"propagate": False,
|
||||
},
|
||||
"set_margin_mode": {
|
||||
"handlers": ["console", "timed_rotating_file", "error_file"],
|
||||
"level": "DEBUG",
|
||||
"propagate": False,
|
||||
},
|
||||
"set_switch_margin_mode": {
|
||||
"handlers": ["console", "timed_rotating_file", "error_file"],
|
||||
"level": "DEBUG",
|
||||
"propagate": False,
|
||||
},
|
||||
"set_switch_position_mode": {
|
||||
"handlers": ["console", "timed_rotating_file", "error_file"],
|
||||
"level": "DEBUG",
|
||||
"propagate": False,
|
||||
},
|
||||
"set_leverage": {
|
||||
"handlers": ["console", "timed_rotating_file", "error_file"],
|
||||
"level": "DEBUG",
|
||||
"propagate": False,
|
||||
},
|
||||
"get_instruments_info": {
|
||||
"handlers": ["console", "timed_rotating_file", "error_file"],
|
||||
"level": "DEBUG",
|
||||
"propagate": False,
|
||||
},
|
||||
"get_positions": {
|
||||
"handlers": ["console", "timed_rotating_file", "error_file"],
|
||||
"level": "DEBUG",
|
||||
"propagate": False,
|
||||
},
|
||||
"open_positions": {
|
||||
"handlers": ["console", "timed_rotating_file", "error_file"],
|
||||
"level": "DEBUG",
|
||||
"propagate": False,
|
||||
},
|
||||
"close_positions": {
|
||||
"handlers": ["console", "timed_rotating_file", "error_file"],
|
||||
"level": "DEBUG",
|
||||
"propagate": False,
|
||||
},
|
||||
"telegram_message_handler": {
|
||||
"handlers": ["console", "timed_rotating_file", "error_file"],
|
||||
"level": "DEBUG",
|
||||
"propagate": False,
|
||||
},
|
||||
"set_tp_sl": {
|
||||
"handlers": ["console", "timed_rotating_file", "error_file"],
|
||||
"level": "DEBUG",
|
||||
"propagate": False,
|
||||
},
|
||||
},
|
||||
}
|
434
app/bybit/open_positions.py
Normal file
434
app/bybit/open_positions.py
Normal file
@@ -0,0 +1,434 @@
|
||||
import logging.config
|
||||
|
||||
from pybit.exceptions import InvalidRequestError
|
||||
|
||||
import database.request as rq
|
||||
from app.bybit import get_bybit_client
|
||||
from app.bybit.get_functions.get_balance import get_balance
|
||||
from app.bybit.get_functions.get_instruments_info import get_instruments_info
|
||||
from app.bybit.get_functions.get_tickers import get_tickers
|
||||
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
|
||||
from app.bybit.set_functions.set_leverage import (
|
||||
set_leverage,
|
||||
set_leverage_to_buy_and_sell,
|
||||
)
|
||||
from app.bybit.set_functions.set_margin_mode import set_margin_mode
|
||||
from app.bybit.set_functions.set_switch_position_mode import set_switch_position_mode
|
||||
from app.helper_functions import check_limit_price, get_liquidation_price, safe_float
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("open_positions")
|
||||
|
||||
|
||||
async def start_trading_cycle(
|
||||
tg_id: int, side: str, switch_side_mode: bool
|
||||
) -> str | None:
|
||||
"""
|
||||
Start trading cycle
|
||||
:param tg_id: Telegram user ID
|
||||
:param side: Buy or Sell
|
||||
:param switch_side_mode: switch_side_mode
|
||||
"""
|
||||
try:
|
||||
symbol = await rq.get_user_symbol(tg_id=tg_id)
|
||||
additional_data = await rq.get_user_additional_settings(tg_id=tg_id)
|
||||
risk_management_data = await rq.get_user_risk_management(tg_id=tg_id)
|
||||
|
||||
trade_mode = additional_data.trade_mode
|
||||
margin_type = additional_data.margin_type
|
||||
leverage = additional_data.leverage
|
||||
leverage_to_buy = additional_data.leverage_to_buy
|
||||
leverage_to_sell = additional_data.leverage_to_sell
|
||||
order_type = additional_data.order_type
|
||||
conditional_order_type = additional_data.conditional_order_type
|
||||
order_quantity = additional_data.order_quantity
|
||||
limit_price = additional_data.limit_price
|
||||
trigger_price = additional_data.trigger_price
|
||||
martingale_factor = additional_data.martingale_factor
|
||||
max_bets_in_series = additional_data.max_bets_in_series
|
||||
take_profit_percent = risk_management_data.take_profit_percent
|
||||
stop_loss_percent = risk_management_data.stop_loss_percent
|
||||
max_risk_percent = risk_management_data.max_risk_percent
|
||||
|
||||
mode = 0 if trade_mode == "Merged_Single" else 3
|
||||
await set_switch_position_mode(tg_id=tg_id, symbol=symbol, mode=mode)
|
||||
await set_margin_mode(tg_id=tg_id, margin_mode=margin_type)
|
||||
await set_leverage(
|
||||
tg_id=tg_id,
|
||||
symbol=symbol,
|
||||
leverage=leverage,
|
||||
)
|
||||
await set_leverage_to_buy_and_sell(
|
||||
tg_id=tg_id,
|
||||
symbol=symbol,
|
||||
leverage_to_buy=leverage_to_buy,
|
||||
leverage_to_sell=leverage_to_sell,
|
||||
)
|
||||
|
||||
res = await open_positions(
|
||||
tg_id=tg_id,
|
||||
symbol=symbol,
|
||||
side=side,
|
||||
order_type=order_type,
|
||||
conditional_order_type=conditional_order_type,
|
||||
order_quantity=order_quantity,
|
||||
limit_price=limit_price,
|
||||
trigger_price=trigger_price,
|
||||
trade_mode=trade_mode,
|
||||
margin_type=margin_type,
|
||||
leverage=leverage,
|
||||
leverage_to_buy=leverage_to_buy,
|
||||
leverage_to_sell=leverage_to_sell,
|
||||
take_profit_percent=take_profit_percent,
|
||||
stop_loss_percent=stop_loss_percent,
|
||||
max_risk_percent=max_risk_percent,
|
||||
)
|
||||
|
||||
if res == "OK":
|
||||
await rq.set_user_deal(
|
||||
tg_id=tg_id,
|
||||
symbol=symbol,
|
||||
last_side=side,
|
||||
current_step=1,
|
||||
trade_mode=trade_mode,
|
||||
margin_type=margin_type,
|
||||
leverage=leverage,
|
||||
leverage_to_buy=leverage_to_buy,
|
||||
leverage_to_sell=leverage_to_sell,
|
||||
order_type="Market",
|
||||
conditional_order_type=conditional_order_type,
|
||||
order_quantity=order_quantity,
|
||||
limit_price=limit_price,
|
||||
trigger_price=trigger_price,
|
||||
martingale_factor=martingale_factor,
|
||||
max_bets_in_series=max_bets_in_series,
|
||||
take_profit_percent=take_profit_percent,
|
||||
stop_loss_percent=stop_loss_percent,
|
||||
max_risk_percent=max_risk_percent,
|
||||
switch_side_mode=switch_side_mode,
|
||||
)
|
||||
return "OK"
|
||||
return (
|
||||
res
|
||||
if res
|
||||
in {
|
||||
"Limit price is out min price",
|
||||
"Limit price is out max price",
|
||||
"Risk is too high for this trade",
|
||||
"estimated will trigger liq",
|
||||
"ab not enough for new order",
|
||||
"InvalidRequestError",
|
||||
"Order does not meet minimum order value",
|
||||
"position idx not match position mode",
|
||||
"Qty invalid",
|
||||
}
|
||||
else None
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error in start_trading: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
async def trading_cycle(tg_id: int, symbol: str, reverse_side: str) -> str | None:
|
||||
try:
|
||||
user_deals_data = await rq.get_user_deal_by_symbol(tg_id=tg_id, symbol=symbol)
|
||||
trade_mode = user_deals_data.trade_mode
|
||||
order_type = user_deals_data.order_type
|
||||
order_quantity = user_deals_data.order_quantity
|
||||
conditional_order_type = user_deals_data.conditional_order_type
|
||||
margin_type = user_deals_data.margin_type
|
||||
leverage = user_deals_data.leverage
|
||||
leverage_to_buy = user_deals_data.leverage_to_buy
|
||||
leverage_to_sell = user_deals_data.leverage_to_sell
|
||||
limit_price = user_deals_data.limit_price
|
||||
trigger_price = user_deals_data.trigger_price
|
||||
take_profit_percent = user_deals_data.take_profit_percent
|
||||
stop_loss_percent = user_deals_data.stop_loss_percent
|
||||
max_risk_percent = user_deals_data.max_risk_percent
|
||||
max_bets_in_series = user_deals_data.max_bets_in_series
|
||||
martingale_factor = user_deals_data.martingale_factor
|
||||
current_step = user_deals_data.current_step
|
||||
switch_side_mode = user_deals_data.switch_side_mode
|
||||
|
||||
mode = 0 if trade_mode == "Merged_Single" else 3
|
||||
await set_switch_position_mode(tg_id=tg_id, symbol=symbol, mode=mode)
|
||||
await set_margin_mode(tg_id=tg_id, margin_mode=margin_type)
|
||||
await set_leverage(
|
||||
tg_id=tg_id,
|
||||
symbol=symbol,
|
||||
leverage=leverage,
|
||||
)
|
||||
await set_leverage_to_buy_and_sell(
|
||||
tg_id=tg_id,
|
||||
symbol=symbol,
|
||||
leverage_to_buy=leverage_to_buy,
|
||||
leverage_to_sell=leverage_to_sell,
|
||||
)
|
||||
|
||||
if reverse_side == "Buy":
|
||||
real_side = "Sell"
|
||||
else:
|
||||
real_side = "Buy"
|
||||
|
||||
side = real_side
|
||||
|
||||
if switch_side_mode:
|
||||
side = "Sell" if real_side == "Buy" else "Buy"
|
||||
|
||||
next_quantity = safe_float(order_quantity) * (
|
||||
safe_float(martingale_factor) ** current_step
|
||||
)
|
||||
current_step += 1
|
||||
|
||||
if max_bets_in_series < current_step:
|
||||
return "Max bets in series"
|
||||
|
||||
res = await open_positions(
|
||||
tg_id=tg_id,
|
||||
symbol=symbol,
|
||||
side=side,
|
||||
order_type="Market",
|
||||
conditional_order_type=conditional_order_type,
|
||||
order_quantity=next_quantity,
|
||||
limit_price=limit_price,
|
||||
trigger_price=trigger_price,
|
||||
trade_mode=trade_mode,
|
||||
margin_type=margin_type,
|
||||
leverage=leverage,
|
||||
leverage_to_buy=leverage_to_buy,
|
||||
leverage_to_sell=leverage_to_sell,
|
||||
take_profit_percent=take_profit_percent,
|
||||
stop_loss_percent=stop_loss_percent,
|
||||
max_risk_percent=max_risk_percent,
|
||||
)
|
||||
|
||||
if res == "OK":
|
||||
await rq.set_user_deal(
|
||||
tg_id=tg_id,
|
||||
symbol=symbol,
|
||||
last_side=side,
|
||||
current_step=current_step,
|
||||
trade_mode=trade_mode,
|
||||
margin_type=margin_type,
|
||||
leverage=leverage,
|
||||
leverage_to_buy=leverage_to_buy,
|
||||
leverage_to_sell=leverage_to_sell,
|
||||
order_type=order_type,
|
||||
conditional_order_type=conditional_order_type,
|
||||
order_quantity=order_quantity,
|
||||
limit_price=limit_price,
|
||||
trigger_price=trigger_price,
|
||||
martingale_factor=martingale_factor,
|
||||
max_bets_in_series=max_bets_in_series,
|
||||
take_profit_percent=take_profit_percent,
|
||||
stop_loss_percent=stop_loss_percent,
|
||||
max_risk_percent=max_risk_percent,
|
||||
switch_side_mode=switch_side_mode,
|
||||
)
|
||||
return "OK"
|
||||
|
||||
return (
|
||||
res
|
||||
if res
|
||||
in {
|
||||
"Risk is too high for this trade",
|
||||
"ab not enough for new order",
|
||||
"InvalidRequestError",
|
||||
}
|
||||
else None
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error in trading_cycle: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
async def open_positions(
|
||||
tg_id: int,
|
||||
side: str,
|
||||
symbol: str,
|
||||
order_type: str,
|
||||
conditional_order_type: str,
|
||||
order_quantity: float,
|
||||
limit_price: float,
|
||||
trigger_price: float,
|
||||
trade_mode: str,
|
||||
margin_type: str,
|
||||
leverage: float,
|
||||
leverage_to_buy: float,
|
||||
leverage_to_sell: float,
|
||||
take_profit_percent: float,
|
||||
stop_loss_percent: float,
|
||||
max_risk_percent: float,
|
||||
) -> str | None:
|
||||
try:
|
||||
client = await get_bybit_client(tg_id=tg_id)
|
||||
|
||||
risk_management_data = await rq.get_user_risk_management(tg_id=tg_id)
|
||||
commission_fee = risk_management_data.commission_fee
|
||||
wallet = await get_balance(tg_id=tg_id)
|
||||
user_balance = wallet.get("totalWalletBalance", 0)
|
||||
instruments_resp = await get_instruments_info(tg_id=tg_id, symbol=symbol)
|
||||
get_order_prices = instruments_resp.get("priceFilter")
|
||||
min_price = safe_float(get_order_prices.get("minPrice"))
|
||||
max_price = safe_float(get_order_prices.get("maxPrice"))
|
||||
get_ticker = await get_tickers(tg_id, symbol=symbol)
|
||||
price_symbol = safe_float(get_ticker.get("lastPrice")) or 0
|
||||
bid = safe_float(get_ticker.get("bid1Price")) or 0
|
||||
ask = safe_float(get_ticker.get("ask1Price")) or 0
|
||||
|
||||
if order_type == "Conditional":
|
||||
po_trigger_price = str(trigger_price)
|
||||
trigger_direction = 1 if trigger_price > price_symbol else 2
|
||||
if conditional_order_type == "Limit":
|
||||
error = check_limit_price(limit_price, min_price, max_price)
|
||||
if error in {
|
||||
"Limit price is out min price",
|
||||
"Limit price is out max price",
|
||||
}:
|
||||
return error
|
||||
|
||||
order_type = "Limit"
|
||||
price_for_calc = limit_price
|
||||
tpsl_mode = "Partial"
|
||||
else:
|
||||
order_type = "Market"
|
||||
price_for_calc = trigger_price
|
||||
tpsl_mode = "Full"
|
||||
else:
|
||||
if order_type == "Limit":
|
||||
error = check_limit_price(limit_price, min_price, max_price)
|
||||
if error in {
|
||||
"Limit price is out min price",
|
||||
"Limit price is out max price",
|
||||
}:
|
||||
return error
|
||||
|
||||
price_for_calc = limit_price
|
||||
tpsl_mode = "Partial"
|
||||
else:
|
||||
order_type = "Market"
|
||||
price_for_calc = ask if side == "Buy" else bid
|
||||
tpsl_mode = "Full"
|
||||
po_trigger_price = None
|
||||
trigger_direction = None
|
||||
|
||||
if trade_mode == "Both_Sides":
|
||||
po_position_idx = 1 if side == "Buy" else 2
|
||||
leverage = safe_float(
|
||||
leverage_to_buy if side == "Buy" else leverage_to_sell
|
||||
)
|
||||
else:
|
||||
po_position_idx = 0
|
||||
leverage = safe_float(leverage)
|
||||
|
||||
potential_loss = (
|
||||
safe_float(order_quantity)
|
||||
* safe_float(price_for_calc)
|
||||
* (stop_loss_percent / 100)
|
||||
)
|
||||
adjusted_loss = potential_loss / leverage
|
||||
allowed_loss = safe_float(user_balance) * (max_risk_percent / 100)
|
||||
|
||||
if adjusted_loss > allowed_loss:
|
||||
return "Risk is too high for this trade"
|
||||
|
||||
# Get fee rates
|
||||
fee_info = client.get_fee_rates(category="linear", symbol=symbol)
|
||||
|
||||
# Check if commission fee is enabled
|
||||
commission_fee_percent = 0.0
|
||||
if commission_fee == "Yes_commission_fee":
|
||||
commission_fee_percent = safe_float(
|
||||
fee_info["result"]["list"][0]["takerFeeRate"]
|
||||
)
|
||||
|
||||
total_commission = price_for_calc * order_quantity * commission_fee_percent
|
||||
tp_multiplier = 1 + (take_profit_percent / 100)
|
||||
if total_commission > 0:
|
||||
tp_multiplier += total_commission
|
||||
|
||||
if margin_type == "ISOLATED_MARGIN":
|
||||
liq_long, liq_short = await get_liquidation_price(
|
||||
tg_id=tg_id,
|
||||
entry_price=price_for_calc,
|
||||
symbol=symbol,
|
||||
order_quantity=order_quantity,
|
||||
)
|
||||
|
||||
if liq_long > 0 or liq_short > 0 and price_for_calc > 0:
|
||||
if side.lower() == "buy":
|
||||
base_tp = price_for_calc + (price_for_calc - liq_long)
|
||||
take_profit_price = base_tp + total_commission
|
||||
else:
|
||||
base_tp = price_for_calc - (liq_short - price_for_calc)
|
||||
take_profit_price = base_tp - total_commission
|
||||
take_profit_price = max(take_profit_price, 0)
|
||||
else:
|
||||
take_profit_price = None
|
||||
|
||||
stop_loss_price = None
|
||||
else:
|
||||
if side.lower() == "buy":
|
||||
take_profit_price = price_for_calc * tp_multiplier
|
||||
stop_loss_price = price_for_calc * (1 - stop_loss_percent / 100)
|
||||
else:
|
||||
take_profit_price = price_for_calc * (
|
||||
1 - (take_profit_percent / 100) - total_commission
|
||||
)
|
||||
stop_loss_price = price_for_calc * (1 + stop_loss_percent / 100)
|
||||
|
||||
take_profit_price = max(take_profit_price, 0)
|
||||
stop_loss_price = max(stop_loss_price, 0)
|
||||
|
||||
order_params = {
|
||||
"category": "linear",
|
||||
"symbol": symbol,
|
||||
"side": side,
|
||||
"orderType": order_type,
|
||||
"qty": str(order_quantity),
|
||||
"triggerDirection": trigger_direction,
|
||||
"triggerPrice": po_trigger_price,
|
||||
"triggerBy": "LastPrice",
|
||||
"timeInForce": "GTC",
|
||||
"positionIdx": po_position_idx,
|
||||
"tpslMode": tpsl_mode,
|
||||
"takeProfit": str(take_profit_price) if take_profit_price else None,
|
||||
"stopLoss": str(stop_loss_price) if stop_loss_price else None,
|
||||
}
|
||||
|
||||
if order_type == "Conditional":
|
||||
if conditional_order_type == "Limit":
|
||||
order_params["price"] = str(limit_price)
|
||||
if order_type == "Limit":
|
||||
order_params["price"] = str(limit_price)
|
||||
|
||||
response = client.place_order(**order_params)
|
||||
|
||||
if response["retCode"] == 0:
|
||||
logger.info("Position opened for user: %s", tg_id)
|
||||
return "OK"
|
||||
|
||||
logger.error("Error opening position for user: %s", tg_id)
|
||||
return None
|
||||
|
||||
except InvalidRequestError as e:
|
||||
error_text = str(e)
|
||||
known_errors = {
|
||||
"Order does not meet minimum order value": "Order does not meet minimum order value",
|
||||
"estimated will trigger liq": "estimated will trigger liq",
|
||||
"ab not enough for new order": "ab not enough for new order",
|
||||
"position idx not match position mode": "position idx not match position mode",
|
||||
"Qty invalid": "Qty invalid",
|
||||
}
|
||||
for key, msg in known_errors.items():
|
||||
if key in error_text:
|
||||
logger.error(msg)
|
||||
return msg
|
||||
logger.error("InvalidRequestError: %s", e)
|
||||
return "InvalidRequestError"
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error opening position for user %s: %s", tg_id, e)
|
||||
return None
|
41
app/bybit/profile_bybit.py
Normal file
41
app/bybit/profile_bybit.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import logging.config
|
||||
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message
|
||||
|
||||
import app.telegram.keyboards.inline as kbi
|
||||
import database.request as rq
|
||||
from app.bybit.get_functions.get_balance import get_balance
|
||||
from app.bybit.get_functions.get_tickers import get_tickers
|
||||
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("profile_bybit")
|
||||
|
||||
|
||||
async def user_profile_bybit(tg_id: int, message: Message, state: FSMContext) -> None:
|
||||
"""Get user profile bybit"""
|
||||
try:
|
||||
await state.clear()
|
||||
wallet = await get_balance(tg_id=tg_id)
|
||||
|
||||
if wallet:
|
||||
balance = wallet.get("totalWalletBalance", "0")
|
||||
symbol = await rq.get_user_symbol(tg_id=tg_id)
|
||||
get_tickers_info = await get_tickers(tg_id=tg_id, symbol=symbol)
|
||||
price_symbol = get_tickers_info.get("lastPrice") or 0
|
||||
await message.answer(
|
||||
text=f"💎Ваш профиль Bybit:\n\n"
|
||||
f"⚖️ Баланс: {float(balance):,.2f} USD\n"
|
||||
f"📊Торговая пара: {symbol}\n"
|
||||
f"$$$ Цена: {float(price_symbol):,.4f}\n",
|
||||
reply_markup=kbi.main_menu,
|
||||
)
|
||||
else:
|
||||
await message.answer(
|
||||
text="Ошибка при подключении, повторите попытку",
|
||||
reply_markup=kbi.connect_the_platform,
|
||||
)
|
||||
logger.error("Error processing user profile for user %s", tg_id)
|
||||
except Exception as e:
|
||||
logger.error("Error processing user profile for user %s: %s", tg_id, e)
|
0
app/bybit/set_functions/__init__.py
Normal file
0
app/bybit/set_functions/__init__.py
Normal file
96
app/bybit/set_functions/set_leverage.py
Normal file
96
app/bybit/set_functions/set_leverage.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import logging.config
|
||||
|
||||
from pybit import exceptions
|
||||
|
||||
from app.bybit import get_bybit_client
|
||||
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("set_leverage")
|
||||
|
||||
|
||||
async def set_leverage(tg_id: int, symbol: str, leverage: str) -> bool:
|
||||
"""
|
||||
Set leverage
|
||||
:param tg_id: int - User ID
|
||||
:param symbol: str - Symbol
|
||||
:param leverage: str - Leverage
|
||||
:return: bool
|
||||
"""
|
||||
try:
|
||||
client = await get_bybit_client(tg_id=tg_id)
|
||||
response = client.set_leverage(
|
||||
category="linear",
|
||||
symbol=symbol,
|
||||
buyLeverage=str(leverage),
|
||||
sellLeverage=str(leverage),
|
||||
)
|
||||
if response["retCode"] == 0:
|
||||
logger.info(
|
||||
"Leverage set to %s for user: %s",
|
||||
leverage,
|
||||
tg_id,
|
||||
)
|
||||
return True
|
||||
else:
|
||||
logger.error("Error setting leverage: %s", response["retMsg"])
|
||||
return False
|
||||
except exceptions.InvalidRequestError as e:
|
||||
if "110043" in str(e):
|
||||
logger.debug(
|
||||
"Leverage set to %s for user: %s",
|
||||
leverage,
|
||||
tg_id,
|
||||
)
|
||||
return True
|
||||
else:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Error connecting to Bybit for user %s: %s", tg_id, e)
|
||||
return False
|
||||
|
||||
|
||||
async def set_leverage_to_buy_and_sell(
|
||||
tg_id: int, symbol: str, leverage_to_buy: str, leverage_to_sell: str
|
||||
) -> bool:
|
||||
"""
|
||||
Set leverage to buy and sell
|
||||
:param tg_id: int - User ID
|
||||
:param symbol: str - Symbol
|
||||
:param leverage_to_buy: str - Leverage to buy
|
||||
:param leverage_to_sell: str - Leverage to sell
|
||||
:return: bool
|
||||
"""
|
||||
try:
|
||||
client = await get_bybit_client(tg_id=tg_id)
|
||||
response = client.set_leverage(
|
||||
category="linear",
|
||||
symbol=symbol,
|
||||
buyLeverage=str(leverage_to_buy),
|
||||
sellLeverage=str(leverage_to_sell),
|
||||
)
|
||||
if response["retCode"] == 0:
|
||||
logger.info(
|
||||
"Leverage set to %s and %s for user: %s",
|
||||
leverage_to_buy,
|
||||
leverage_to_sell,
|
||||
tg_id,
|
||||
)
|
||||
return True
|
||||
else:
|
||||
logger.error("Error setting leverage for buy and sell for user: %s", tg_id)
|
||||
return False
|
||||
except exceptions.InvalidRequestError as e:
|
||||
if "110043" in str(e):
|
||||
logger.debug(
|
||||
"Leverage set to %s and %s for user: %s",
|
||||
leverage_to_buy,
|
||||
leverage_to_sell,
|
||||
tg_id,
|
||||
)
|
||||
return True
|
||||
else:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Error connecting to Bybit for user %s: %s", tg_id, e)
|
||||
return False
|
28
app/bybit/set_functions/set_margin_mode.py
Normal file
28
app/bybit/set_functions/set_margin_mode.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import logging.config
|
||||
|
||||
from app.bybit import get_bybit_client
|
||||
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("set_margin_mode")
|
||||
|
||||
|
||||
async def set_margin_mode(tg_id: int, margin_mode: str) -> bool:
|
||||
"""
|
||||
Set margin mode
|
||||
:param tg_id: int - User ID
|
||||
:param margin_mode: str - Margin mode
|
||||
:return: bool
|
||||
"""
|
||||
try:
|
||||
client = await get_bybit_client(tg_id=tg_id)
|
||||
response = client.set_margin_mode(setMarginMode=margin_mode)
|
||||
if response["retCode"] == 0:
|
||||
logger.info("Margin mode set to %s for user: %s", margin_mode, tg_id)
|
||||
return True
|
||||
else:
|
||||
logger.error("Error setting margin mode: %s", tg_id)
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error("Error connecting to Bybit for user %s: %s", tg_id, e)
|
||||
return False
|
48
app/bybit/set_functions/set_switch_position_mode.py
Normal file
48
app/bybit/set_functions/set_switch_position_mode.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import logging.config
|
||||
|
||||
from app.bybit import get_bybit_client
|
||||
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("set_switch_position_mode")
|
||||
|
||||
|
||||
async def set_switch_position_mode(tg_id: int, symbol: str, mode: int) -> str | bool:
|
||||
"""
|
||||
Set switch position mode
|
||||
:param tg_id: int - User ID
|
||||
:param symbol: str - Symbol
|
||||
:param mode: int - Mode
|
||||
:return: bool
|
||||
"""
|
||||
try:
|
||||
client = await get_bybit_client(tg_id=tg_id)
|
||||
response = client.switch_position_mode(
|
||||
category="linear",
|
||||
symbol=symbol,
|
||||
mode=mode,
|
||||
)
|
||||
if response["retCode"] == 0:
|
||||
logger.info("Switch position mode set successfully")
|
||||
return True
|
||||
else:
|
||||
logger.error("Error setting switch position mode for user: %s", tg_id)
|
||||
return False
|
||||
except Exception as e:
|
||||
if str(e).startswith("Position mode is not modified"):
|
||||
logger.debug(
|
||||
"Position mode is not modified for user: %s",
|
||||
tg_id,
|
||||
)
|
||||
return True
|
||||
if str(e).startswith(
|
||||
"You have an existing position, so position mode cannot be switched"
|
||||
):
|
||||
logger.debug(
|
||||
"You have an existing position, so position mode cannot be switched for user: %s",
|
||||
tg_id,
|
||||
)
|
||||
return "You have an existing position, so position mode cannot be switched"
|
||||
else:
|
||||
logger.error("Error connecting to Bybit for user %s: %s", tg_id, e)
|
||||
return False
|
48
app/bybit/set_functions/set_tp_sl.py
Normal file
48
app/bybit/set_functions/set_tp_sl.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import logging.config
|
||||
|
||||
from app.bybit import get_bybit_client
|
||||
from app.bybit.get_functions.get_positions import get_active_positions_by_symbol
|
||||
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("set_tp_sl")
|
||||
|
||||
|
||||
async def set_tp_sl_for_position(
|
||||
tg_id: int,
|
||||
symbol: str,
|
||||
take_profit_price: float,
|
||||
stop_loss_price: float,
|
||||
) -> bool:
|
||||
"""
|
||||
Set take profit and stop loss for a symbol.
|
||||
:param tg_id: Telegram user ID
|
||||
:param symbol: Symbol to set take profit and stop loss for
|
||||
:param take_profit_price: Take profit price
|
||||
:param stop_loss_price: Stop loss price
|
||||
:return: bool
|
||||
"""
|
||||
try:
|
||||
client = await get_bybit_client(tg_id)
|
||||
user_positions = await get_active_positions_by_symbol(
|
||||
tg_id=tg_id, symbol=symbol
|
||||
)
|
||||
position_idx = user_positions.get("positionIdx")
|
||||
resp = client.set_trading_stop(
|
||||
category="linear",
|
||||
symbol=symbol,
|
||||
takeProfit=str(round(take_profit_price, 5)),
|
||||
stopLoss=str(round(stop_loss_price, 5)),
|
||||
positionIdx=position_idx,
|
||||
tpslMode="Full",
|
||||
)
|
||||
|
||||
if resp.get("retCode") == 0:
|
||||
logger.info("TP/SL for %s has been set", symbol)
|
||||
return True
|
||||
else:
|
||||
logger.error("Error setting TP/SL for %s: %s", symbol, resp.get("retMsg"))
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error("Error setting TP/SL for %s: %s", symbol, e)
|
||||
return False
|
192
app/bybit/telegram_message_handler.py
Normal file
192
app/bybit/telegram_message_handler.py
Normal file
@@ -0,0 +1,192 @@
|
||||
import logging.config
|
||||
|
||||
import app.telegram.keyboards.inline as kbi
|
||||
import database.request as rq
|
||||
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
|
||||
from app.bybit.open_positions import trading_cycle
|
||||
from app.helper_functions import format_value, safe_float, safe_int
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("telegram_message_handler")
|
||||
|
||||
|
||||
class TelegramMessageHandler:
|
||||
def __init__(self, telegram_bot):
|
||||
self.telegram_bot = telegram_bot
|
||||
|
||||
async def format_position_update(self, message):
|
||||
pass
|
||||
|
||||
async def format_order_update(self, message, tg_id):
|
||||
try:
|
||||
order_data = message.get("data", [{}])[0]
|
||||
symbol = format_value(order_data.get("symbol"))
|
||||
qty = format_value(order_data.get("qty"))
|
||||
order_type = format_value(order_data.get("orderType"))
|
||||
order_type_rus = (
|
||||
"Рыночный"
|
||||
if order_type == "Market"
|
||||
else "Лимитный" if order_type == "Limit" else "Нет данных"
|
||||
)
|
||||
side = format_value(order_data.get("side"))
|
||||
side_rus = (
|
||||
"Покупка"
|
||||
if side == "Buy"
|
||||
else "Продажа" if side == "Sell" else "Нет данных"
|
||||
)
|
||||
order_status = format_value(order_data.get("orderStatus"))
|
||||
price = format_value(order_data.get("price"))
|
||||
trigger_price = format_value(order_data.get("triggerPrice"))
|
||||
take_profit = format_value(order_data.get("takeProfit"))
|
||||
stop_loss = format_value(order_data.get("stopLoss"))
|
||||
|
||||
position_idx = safe_int(order_data.get("positionIdx"))
|
||||
position_idx_rus = (
|
||||
"Односторонний"
|
||||
if position_idx == 0
|
||||
else (
|
||||
"Покупка в режиме хеджирования"
|
||||
if position_idx == 1
|
||||
else (
|
||||
"Продажа в режиме хеджирования"
|
||||
if position_idx == 2
|
||||
else "Нет данных"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
status_map = {
|
||||
"New": "Ордер создан",
|
||||
"Cancelled": "Ордер отменен",
|
||||
"Deactivated": "Ордер деактивирован",
|
||||
"Untriggered": "Условный ордер выставлен",
|
||||
}
|
||||
|
||||
if order_status == "Filled" or order_status not in status_map:
|
||||
return None
|
||||
|
||||
status_text = status_map[order_status]
|
||||
|
||||
text = (
|
||||
f"{status_text}:\n"
|
||||
f"Торговая пара: {symbol}\n"
|
||||
f"Цена: {price}\n"
|
||||
f"Режим позиции: {position_idx_rus}\n"
|
||||
f"Количество: {qty}\n"
|
||||
f"Тип ордера: {order_type_rus}\n"
|
||||
f"Движение: {side_rus}\n"
|
||||
f"Триггер цена: {trigger_price}\n"
|
||||
f"Тейк-профит: {take_profit}\n"
|
||||
f"Стоп-лосс: {stop_loss}\n"
|
||||
)
|
||||
|
||||
await self.telegram_bot.send_message(
|
||||
chat_id=tg_id, text=text, reply_markup=kbi.profile_bybit
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Error in format_order_update: %s", e)
|
||||
|
||||
async def format_execution_update(self, message, tg_id):
|
||||
try:
|
||||
execution = message.get("data", [{}])[0]
|
||||
closed_size = format_value(execution.get("closedSize"))
|
||||
symbol = format_value(execution.get("symbol"))
|
||||
exec_price = format_value(execution.get("execPrice"))
|
||||
exec_fee = format_value(execution.get("execFee"))
|
||||
exec_qty = format_value(execution.get("execQty"))
|
||||
order_type = format_value(execution.get("orderType"))
|
||||
order_type_rus = (
|
||||
"Рыночный"
|
||||
if order_type == "Market"
|
||||
else "Лимитный" if order_type == "Limit" else "Нет данных"
|
||||
)
|
||||
side = format_value(execution.get("side"))
|
||||
side_rus = (
|
||||
"Покупка"
|
||||
if side == "Buy"
|
||||
else "Продажа" if side == "Sell" else "Нет данных"
|
||||
)
|
||||
exec_pnl = format_value(execution.get("execPnl"))
|
||||
risk_management_data = await rq.get_user_risk_management(tg_id=tg_id)
|
||||
commission_fee = risk_management_data.commission_fee
|
||||
|
||||
if commission_fee == "Yes_commission_fee":
|
||||
total_pnl = safe_float(exec_pnl) - safe_float(exec_fee)
|
||||
else:
|
||||
total_pnl = safe_float(exec_pnl)
|
||||
|
||||
header = (
|
||||
"Сделка закрыта:" if safe_float(closed_size) > 0 else "Сделка открыта:"
|
||||
)
|
||||
text = f"{header}\n" f"Торговая пара: {symbol}\n"
|
||||
|
||||
if safe_float(closed_size) > 0:
|
||||
text += f"Количество закрытых сделок: {closed_size}\n"
|
||||
|
||||
text += (
|
||||
f"Цена исполнения: {exec_price}\n"
|
||||
f"Количество исполненных сделок: {exec_qty}\n"
|
||||
f"Тип ордера: {order_type_rus}\n"
|
||||
f"Движение: {side_rus}\n"
|
||||
f"Комиссия за сделку: {exec_fee:.4f}\n"
|
||||
)
|
||||
|
||||
if safe_float(closed_size) > 0:
|
||||
text += f"\nРеализованная прибыль: {total_pnl:.4f}\n"
|
||||
|
||||
await self.telegram_bot.send_message(
|
||||
chat_id=tg_id, text=text, reply_markup=kbi.profile_bybit
|
||||
)
|
||||
|
||||
user_auto_trading = await rq.get_user_auto_trading(
|
||||
tg_id=tg_id, symbol=symbol
|
||||
)
|
||||
auto_trading = (
|
||||
user_auto_trading.auto_trading if user_auto_trading else False
|
||||
)
|
||||
user_symbols = await rq.get_user_deal_by_symbol(tg_id=tg_id, symbol=symbol)
|
||||
|
||||
if (
|
||||
auto_trading
|
||||
and safe_float(closed_size) > 0
|
||||
and user_symbols is not None
|
||||
):
|
||||
if safe_float(total_pnl) > 0:
|
||||
await rq.set_auto_trading(
|
||||
tg_id=tg_id, symbol=symbol, auto_trading=False
|
||||
)
|
||||
profit_text = "📈 Прибыль достигнута\n"
|
||||
await self.telegram_bot.send_message(
|
||||
chat_id=tg_id, text=profit_text, reply_markup=kbi.profile_bybit
|
||||
)
|
||||
else:
|
||||
open_order_text = "\n❗️ Сделка закрылась в минус, открываю новую сделку с увеличенной ставкой.\n"
|
||||
await self.telegram_bot.send_message(
|
||||
chat_id=tg_id, text=open_order_text
|
||||
)
|
||||
res = await trading_cycle(
|
||||
tg_id=tg_id, symbol=symbol, reverse_side=side
|
||||
)
|
||||
|
||||
if res == "OK":
|
||||
pass
|
||||
else:
|
||||
errors = {
|
||||
"Max bets in series": "❗️ Максимальное количество сделок в серии достигнуто",
|
||||
"Risk is too high for this trade": "❗️ Риск сделки слишком высок для продолжения",
|
||||
"ab not enough for new order": "❗️ Недостаточно средств для продолжения торговли",
|
||||
"InvalidRequestError": "❗️ Недостаточно средств для размещения нового ордера с заданным количеством и плечом.",
|
||||
}
|
||||
error_text = errors.get(
|
||||
res, "❗️ Не удалось открыть новую сделку"
|
||||
)
|
||||
await rq.set_auto_trading(
|
||||
tg_id=tg_id, symbol=symbol, auto_trading=False
|
||||
)
|
||||
await self.telegram_bot.send_message(
|
||||
chat_id=tg_id,
|
||||
text=error_text,
|
||||
reply_markup=kbi.profile_bybit,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Error in telegram_message_handler: %s", e)
|
110
app/bybit/web_socket.py
Normal file
110
app/bybit/web_socket.py
Normal file
@@ -0,0 +1,110 @@
|
||||
import asyncio
|
||||
import logging.config
|
||||
|
||||
from pybit.unified_trading import WebSocket
|
||||
|
||||
import database.request as rq
|
||||
from app.bybit.logger_bybit.logger_bybit import LOGGING_CONFIG
|
||||
from app.bybit.telegram_message_handler import TelegramMessageHandler
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("web_socket")
|
||||
|
||||
|
||||
class WebSocketBot:
|
||||
"""
|
||||
Class to handle WebSocket connections and messages.
|
||||
"""
|
||||
|
||||
def __init__(self, telegram_bot):
|
||||
"""Initialize the TradingBot class."""
|
||||
self.telegram_bot = telegram_bot
|
||||
self.ws_private = None
|
||||
self.user_messages = {}
|
||||
self.user_sockets = {}
|
||||
self.loop = None
|
||||
self.message_handler = TelegramMessageHandler(telegram_bot)
|
||||
|
||||
async def run_user_check_loop(self):
|
||||
"""Run a loop to check for users and connect them to the WebSocket."""
|
||||
self.loop = asyncio.get_running_loop()
|
||||
while True:
|
||||
users = await WebSocketBot.get_users_from_db()
|
||||
for user in users:
|
||||
tg_id = user.tg_id
|
||||
api_key, api_secret = await rq.get_user_api(tg_id=tg_id)
|
||||
|
||||
if not api_key or not api_secret:
|
||||
continue
|
||||
|
||||
if tg_id in self.user_sockets:
|
||||
continue
|
||||
|
||||
success = await self.try_connect_user(api_key, api_secret, tg_id)
|
||||
if success:
|
||||
self.user_messages.setdefault(
|
||||
tg_id, {"position": None, "order": None, "execution": None}
|
||||
)
|
||||
else:
|
||||
await asyncio.sleep(30)
|
||||
|
||||
await asyncio.sleep(10)
|
||||
|
||||
async def clear_user_sockets(self):
|
||||
"""Clear the user_sockets and user_messages dictionaries."""
|
||||
self.user_sockets.clear()
|
||||
self.user_messages.clear()
|
||||
logger.info("Cleared user_sockets and user_messages on shutdown")
|
||||
|
||||
async def try_connect_user(self, api_key, api_secret, tg_id):
|
||||
"""Try to connect a user to the WebSocket."""
|
||||
try:
|
||||
self.ws_private = WebSocket(
|
||||
testnet=False,
|
||||
channel_type="private",
|
||||
api_key=api_key,
|
||||
api_secret=api_secret,
|
||||
restart_on_error=True,
|
||||
)
|
||||
|
||||
self.user_sockets[tg_id] = self.ws_private
|
||||
# Connect to the WebSocket private channel
|
||||
# Handle position updates
|
||||
self.ws_private.position_stream(
|
||||
lambda msg: self.loop.call_soon_threadsafe(
|
||||
asyncio.create_task, self.handle_position_update(msg)
|
||||
)
|
||||
)
|
||||
# Handle order updates
|
||||
self.ws_private.order_stream(
|
||||
lambda msg: self.loop.call_soon_threadsafe(
|
||||
asyncio.create_task, self.handle_order_update(msg, tg_id)
|
||||
)
|
||||
)
|
||||
# Handle execution updates
|
||||
self.ws_private.execution_stream(
|
||||
lambda msg: self.loop.call_soon_threadsafe(
|
||||
asyncio.create_task, self.handle_execution_update(msg, tg_id)
|
||||
)
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("Error connecting user %s: %s", tg_id, e)
|
||||
return False
|
||||
|
||||
async def handle_position_update(self, message):
|
||||
"""Handle position updates."""
|
||||
await self.message_handler.format_position_update(message)
|
||||
|
||||
async def handle_order_update(self, message, tg_id):
|
||||
"""Handle order updates."""
|
||||
await self.message_handler.format_order_update(message, tg_id)
|
||||
|
||||
async def handle_execution_update(self, message, tg_id):
|
||||
"""Handle execution updates."""
|
||||
await self.message_handler.format_execution_update(message, tg_id)
|
||||
|
||||
@staticmethod
|
||||
async def get_users_from_db():
|
||||
"""Get all users from the database."""
|
||||
return await rq.get_users()
|
Reference in New Issue
Block a user