The entire database has been changed to PostgresSQL. The entire code has been updated.

This commit is contained in:
algizn97
2025-10-01 15:23:21 +05:00
parent e5a3de4ed8
commit 97662081ce
49 changed files with 7916 additions and 0 deletions

21
app/bybit/__init__.py Normal file
View 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

View 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

View File

View 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

View 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

View 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

View 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

View File

View 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
View 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

View 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)

View File

View 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

View 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

View 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

View 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

View 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
View 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()