The entire database has been changed to PostgresSQL. The entire code has been updated.
This commit is contained in:
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
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()
|
196
app/helper_functions.py
Normal file
196
app/helper_functions.py
Normal file
@@ -0,0 +1,196 @@
|
||||
import logging.config
|
||||
|
||||
from app.bybit import get_bybit_client
|
||||
from logger_helper.logger_helper import LOGGING_CONFIG
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("helper_functions")
|
||||
|
||||
|
||||
def safe_float(val) -> float:
|
||||
"""
|
||||
Function to safely convert string to float
|
||||
"""
|
||||
try:
|
||||
if val is None or val == "":
|
||||
return 0.0
|
||||
return float(val)
|
||||
except (ValueError, TypeError):
|
||||
logger.error("Error converting value to float: %s", val)
|
||||
return 0.0
|
||||
|
||||
|
||||
def is_number(value: str) -> bool:
|
||||
"""
|
||||
Checks if a given string represents a number.
|
||||
|
||||
Args:
|
||||
value (str): The string to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the string represents a number, False otherwise.
|
||||
"""
|
||||
try:
|
||||
# Convert the string to a float
|
||||
num = float(value)
|
||||
# Check if the number is positive
|
||||
if num <= 0:
|
||||
return False
|
||||
# Check if the string contains "+" or "-"
|
||||
if "+" in value or "-" in value:
|
||||
return False
|
||||
# Check if the string contains only digits
|
||||
allowed_chars = set("0123456789.")
|
||||
if not all(ch in allowed_chars for ch in value):
|
||||
return False
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def is_int(value: str) -> bool:
|
||||
"""
|
||||
Checks if a given string represents an integer.
|
||||
|
||||
Args:
|
||||
value (str): The string to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the string represents an integer, False otherwise.
|
||||
"""
|
||||
# Check if the string contains only digits
|
||||
if not value.isdigit():
|
||||
return False
|
||||
# Convert the string to an integer
|
||||
num = int(value)
|
||||
return num > 0
|
||||
|
||||
|
||||
def is_int_for_timer(value: str) -> bool | int:
|
||||
"""
|
||||
Checks if a given string represents an integer for timer.
|
||||
|
||||
Args:
|
||||
value (str): The string to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the string represents an integer, False otherwise.
|
||||
"""
|
||||
# Check if the string contains only digits
|
||||
try:
|
||||
num = int(value)
|
||||
|
||||
if num >= 0:
|
||||
return num
|
||||
else:
|
||||
return False
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def get_base_currency(symbol: str) -> str:
|
||||
"""
|
||||
Extracts the base currency from a symbol string.
|
||||
|
||||
Args:
|
||||
symbol (str): The symbol string to extract the base currency from.
|
||||
|
||||
Returns:
|
||||
str: The base currency extracted from the symbol string.
|
||||
"""
|
||||
if symbol.endswith("USDT"):
|
||||
return symbol[:-4]
|
||||
return symbol
|
||||
|
||||
|
||||
def safe_int(value, default=0) -> int:
|
||||
"""
|
||||
Integer conversion with default value.
|
||||
"""
|
||||
try:
|
||||
return int(value)
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
|
||||
def format_value(value) -> str:
|
||||
"""
|
||||
Function to format value
|
||||
"""
|
||||
if not value or value.strip() == "":
|
||||
return "Нет данных"
|
||||
return value
|
||||
|
||||
|
||||
def check_limit_price(limit_price, min_price, max_price) -> str | None:
|
||||
"""
|
||||
Function to check limit price
|
||||
"""
|
||||
if limit_price < min_price:
|
||||
return "Limit price is out min price"
|
||||
if limit_price > max_price:
|
||||
return "Limit price is out max price"
|
||||
return None
|
||||
|
||||
|
||||
async def get_liquidation_price(
|
||||
tg_id: int, symbol: str, entry_price: float, order_quantity: float
|
||||
) -> tuple[float, float]:
|
||||
"""
|
||||
Function to get liquidation price
|
||||
"""
|
||||
try:
|
||||
client = await get_bybit_client(tg_id=tg_id)
|
||||
get_risk_info = client.get_risk_limit(category="linear", symbol=symbol)
|
||||
risk_list = get_risk_info.get("result", {}).get("list", [])
|
||||
risk_level = risk_list[0] if risk_list else {}
|
||||
maint_margin_rate = safe_float(risk_level.get("maintenanceMargin"))
|
||||
maint_margin_deduction = safe_float(risk_level.get("initialMargin"))
|
||||
max_leverage = safe_float(risk_level.get("maxLeverage"))
|
||||
position_value = order_quantity / entry_price
|
||||
initial_margin = position_value / max_leverage
|
||||
maint_margin = (position_value * maint_margin_rate) - maint_margin_deduction
|
||||
|
||||
liq_price_long = order_quantity / (
|
||||
position_value + (initial_margin - maint_margin)
|
||||
)
|
||||
|
||||
liq_price_short = order_quantity / (
|
||||
position_value - (initial_margin - maint_margin)
|
||||
)
|
||||
|
||||
liq_price = liq_price_long, liq_price_short
|
||||
|
||||
return liq_price
|
||||
except Exception as e:
|
||||
logger.error("Error getting liquidation price: %s", e)
|
||||
return 0, 0
|
||||
|
||||
|
||||
async def calculate_total_budget(
|
||||
quantity, martingale_factor, max_steps, commission_fee_percent
|
||||
) -> float:
|
||||
"""
|
||||
Calculate the total budget for a series of trading steps.
|
||||
|
||||
Args:
|
||||
quantity (float): The initial quantity of the asset.
|
||||
martingale_factor (float): The factor by which the quantity is multiplied for each step.
|
||||
max_steps (int): The maximum number of trading steps.
|
||||
commission_fee_percent (float): The commission fee percentage.
|
||||
|
||||
Returns:
|
||||
float: The total budget for the series of trading steps.
|
||||
"""
|
||||
total = 0
|
||||
for step in range(max_steps):
|
||||
set_quantity = quantity * (martingale_factor**step)
|
||||
if commission_fee_percent == 0:
|
||||
# Commission fee is not added to the position size
|
||||
r_quantity = set_quantity
|
||||
else:
|
||||
# Commission fee is added to the position size
|
||||
r_quantity = set_quantity * (1 + 2 * commission_fee_percent)
|
||||
|
||||
total += r_quantity
|
||||
return total
|
0
app/telegram/__init__.py
Normal file
0
app/telegram/__init__.py
Normal file
27
app/telegram/functions/profile_tg.py
Normal file
27
app/telegram/functions/profile_tg.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import logging.config
|
||||
|
||||
from aiogram.types import Message
|
||||
|
||||
import app.telegram.keyboards.reply as kbr
|
||||
import database.request as rq
|
||||
from logger_helper.logger_helper import LOGGING_CONFIG
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("profile_tg")
|
||||
|
||||
|
||||
async def user_profile_tg(tg_id: int, message: Message) -> None:
|
||||
try:
|
||||
user = await rq.get_user(tg_id)
|
||||
if user:
|
||||
await message.answer(
|
||||
text="💎Ваш профиль:\n\n" "⚖️ Баланс: 0\n", reply_markup=kbr.profile
|
||||
)
|
||||
else:
|
||||
await rq.create_user(tg_id=tg_id, username=user.username)
|
||||
await rq.set_user_symbol(tg_id=tg_id, symbol="BTCUSDT")
|
||||
await rq.create_user_additional_settings(tg_id=tg_id)
|
||||
await rq.create_user_risk_management(tg_id=tg_id)
|
||||
await user_profile_tg(tg_id=tg_id, message=message)
|
||||
except Exception as e:
|
||||
logger.error("Error processing user profile: %s", e)
|
32
app/telegram/handlers/__init__.py
Normal file
32
app/telegram/handlers/__init__.py
Normal file
@@ -0,0 +1,32 @@
|
||||
__all__ = "router"
|
||||
|
||||
from aiogram import Router
|
||||
|
||||
from app.telegram.handlers.add_bybit_api import router_add_bybit_api
|
||||
from app.telegram.handlers.changing_the_symbol import router_changing_the_symbol
|
||||
from app.telegram.handlers.close_orders import router_close_orders
|
||||
from app.telegram.handlers.common import router_common
|
||||
from app.telegram.handlers.get_positions_handlers import router_get_positions_handlers
|
||||
from app.telegram.handlers.handlers_main import router_handlers_main
|
||||
from app.telegram.handlers.main_settings import router_main_settings
|
||||
from app.telegram.handlers.settings import router_settings
|
||||
from app.telegram.handlers.start_trading import router_start_trading
|
||||
from app.telegram.handlers.stop_trading import router_stop_trading
|
||||
from app.telegram.handlers.tp_sl_handlers import router_tp_sl_handlers
|
||||
|
||||
router = Router(name=__name__)
|
||||
|
||||
router.include_router(router_handlers_main)
|
||||
router.include_router(router_add_bybit_api)
|
||||
router.include_router(router_settings)
|
||||
router.include_router(router_main_settings)
|
||||
router.include_router(router_changing_the_symbol)
|
||||
router.include_router(router_get_positions_handlers)
|
||||
router.include_router(router_start_trading)
|
||||
router.include_router(router_stop_trading)
|
||||
router.include_router(router_close_orders)
|
||||
router.include_router(router_tp_sl_handlers)
|
||||
|
||||
|
||||
# Do not add anything below this router
|
||||
router.include_router(router_common)
|
150
app/telegram/handlers/add_bybit_api.py
Normal file
150
app/telegram/handlers/add_bybit_api.py
Normal file
@@ -0,0 +1,150 @@
|
||||
import logging.config
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
|
||||
import app.telegram.keyboards.inline as kbi
|
||||
import app.telegram.keyboards.reply as kbr
|
||||
import database.request as rq
|
||||
from app.bybit.profile_bybit import user_profile_bybit
|
||||
from app.telegram.states.states import AddBybitApiState
|
||||
from logger_helper.logger_helper import LOGGING_CONFIG
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("add_bybit_api")
|
||||
|
||||
router_add_bybit_api = Router(name="add_bybit_api")
|
||||
|
||||
|
||||
@router_add_bybit_api.callback_query(F.data == "connect_platform")
|
||||
async def connect_platform(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Handles the callback query to initiate Bybit platform connection.
|
||||
Sends instructions on how to create and provide API keys to the bot.
|
||||
|
||||
:param callback: CallbackQuery object triggered by user interaction.
|
||||
:param state: FSMContext object to manage state data.
|
||||
:return: None
|
||||
"""
|
||||
try:
|
||||
await state.clear()
|
||||
await callback.answer()
|
||||
user = await rq.get_user(tg_id=callback.from_user.id)
|
||||
if user:
|
||||
await callback.message.answer(
|
||||
text=(
|
||||
"Подключение Bybit аккаунта \n\n"
|
||||
"1. Зарегистрируйтесь или войдите в свой аккаунт на Bybit по ссылке: "
|
||||
"[Перейти на Bybit](https://www.bybit.com/invite?ref=YME83OJ).\n"
|
||||
"2. В личном кабинете выберите раздел API. \n"
|
||||
"3. Создание нового API ключа\n"
|
||||
" - Нажмите кнопку Create New Key (Создать новый ключ).\n"
|
||||
" - Выберите системно-сгенерированный ключ.\n"
|
||||
" - Укажите название API ключа (любое). \n"
|
||||
" - Выберите права доступа для торговли (Trade). \n"
|
||||
" - Можно ограничить доступ по IP для безопасности.\n"
|
||||
"4. Подтверждение создания\n"
|
||||
" - Подтвердите создание ключа.\n"
|
||||
" - Отправьте чат-роботу.\n\n"
|
||||
"Важно: сохраните отдельно API Key и Secret Key в надежном месте. Secret ключ отображается только один раз."
|
||||
),
|
||||
parse_mode="Markdown",
|
||||
reply_markup=kbi.add_bybit_api,
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
else:
|
||||
await rq.create_user(
|
||||
tg_id=callback.from_user.id, username=callback.from_user.username
|
||||
)
|
||||
await rq.set_user_symbol(tg_id=callback.from_user.id, symbol="BTCUSDT")
|
||||
await rq.create_user_additional_settings(tg_id=callback.from_user.id)
|
||||
await rq.create_user_risk_management(tg_id=callback.from_user.id)
|
||||
await rq.create_user_conditional_settings(tg_id=callback.from_user.id)
|
||||
await connect_platform(callback=callback, state=state)
|
||||
except Exception as e:
|
||||
logger.error("Error adding bybit API for user %s: %s", callback.from_user.id, e)
|
||||
await callback.message.answer(
|
||||
text="Произошла ошибка. Пожалуйста, попробуйте позже."
|
||||
)
|
||||
|
||||
|
||||
@router_add_bybit_api.callback_query(F.data == "add_bybit_api")
|
||||
async def process_api_key(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Starts the FSM flow to add Bybit API keys.
|
||||
Sets the FSM state to prompt user to enter API Key.
|
||||
|
||||
:param callback: CallbackQuery object.
|
||||
:param state: FSMContext for managing user state.
|
||||
"""
|
||||
try:
|
||||
await state.clear()
|
||||
await state.set_state(AddBybitApiState.api_key_state)
|
||||
await callback.answer()
|
||||
await callback.message.answer(text="Введите API Key:")
|
||||
except Exception as e:
|
||||
logger.error("Error adding bybit API for user %s: %s", callback.from_user.id, e)
|
||||
await callback.message.answer(
|
||||
text="Произошла ошибка. Пожалуйста, попробуйте позже."
|
||||
)
|
||||
|
||||
|
||||
@router_add_bybit_api.message(AddBybitApiState.api_key_state)
|
||||
async def process_secret_key(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Receives the API Key input from the user, stores it in FSM context,
|
||||
then sets state to collect Secret Key.
|
||||
|
||||
:param message: Message object with user's input.
|
||||
:param state: FSMContext for managing user state.
|
||||
"""
|
||||
try:
|
||||
api_key = message.text
|
||||
await state.update_data(api_key=api_key)
|
||||
await state.set_state(AddBybitApiState.api_secret_state)
|
||||
await message.answer(text="Введите Secret Key:")
|
||||
except Exception as e:
|
||||
logger.error("Error adding bybit API for user %s: %s", message.from_user.id, e)
|
||||
await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.")
|
||||
|
||||
|
||||
@router_add_bybit_api.message(AddBybitApiState.api_secret_state)
|
||||
async def add_bybit_api(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Receives the Secret Key input, stores it, saves both API keys in the database,
|
||||
clears FSM state and confirms success to the user.
|
||||
|
||||
:param message: Message object with user's input.
|
||||
:param state: FSMContext for managing user state.
|
||||
"""
|
||||
try:
|
||||
api_secret = message.text
|
||||
api_key = (await state.get_data()).get("api_key")
|
||||
await state.update_data(api_secret=api_secret)
|
||||
|
||||
if not api_key or not api_secret:
|
||||
await message.answer("Введите корректные данные.")
|
||||
return
|
||||
|
||||
result = await rq.set_user_api(
|
||||
tg_id=message.from_user.id, api_key=api_key, api_secret=api_secret
|
||||
)
|
||||
|
||||
if result:
|
||||
await message.answer(text="Данные добавлены.", reply_markup=kbr.profile)
|
||||
await user_profile_bybit(
|
||||
tg_id=message.from_user.id, message=message, state=state
|
||||
)
|
||||
logger.debug(
|
||||
"Bybit API added successfully for user: %s", message.from_user.id
|
||||
)
|
||||
else:
|
||||
await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.")
|
||||
logger.error(
|
||||
"Error adding bybit API for user %s: %s", message.from_user.id, result
|
||||
)
|
||||
await state.clear()
|
||||
except Exception as e:
|
||||
logger.error("Error adding bybit API for user %s: %s", message.from_user.id, e)
|
||||
await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.")
|
164
app/telegram/handlers/changing_the_symbol.py
Normal file
164
app/telegram/handlers/changing_the_symbol.py
Normal file
@@ -0,0 +1,164 @@
|
||||
import logging.config
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
|
||||
import app.telegram.keyboards.inline as kbi
|
||||
import database.request as rq
|
||||
from app.bybit.get_functions.get_tickers import get_tickers
|
||||
from app.bybit.profile_bybit import user_profile_bybit
|
||||
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.telegram.states.states import ChangingTheSymbolState
|
||||
from logger_helper.logger_helper import LOGGING_CONFIG
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("changing_the_symbol")
|
||||
|
||||
router_changing_the_symbol = Router(name="changing_the_symbol")
|
||||
|
||||
|
||||
@router_changing_the_symbol.callback_query(F.data == "change_symbol")
|
||||
async def change_symbol(callback_query: CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Handler for the "change_symbol" command.
|
||||
Sends a message with available symbols to choose from.
|
||||
"""
|
||||
try:
|
||||
await state.clear()
|
||||
await state.set_state(ChangingTheSymbolState.symbol_state)
|
||||
msg = await callback_query.message.edit_text(
|
||||
text="Выберите название инструмента без лишних символов (например: BTCUSDT):",
|
||||
reply_markup=kbi.symbol,
|
||||
)
|
||||
await state.update_data(prompt_message_id=msg.message_id)
|
||||
logger.debug(
|
||||
"Command change_symbol processed successfully for user: %s",
|
||||
callback_query.from_user.id,
|
||||
)
|
||||
except Exception as e:
|
||||
await callback_query.answer(
|
||||
text="Произошла ошибка. Пожалуйста, попробуйте позже."
|
||||
)
|
||||
logger.error(
|
||||
"Error processing command change_symbol for user %s: %s",
|
||||
callback_query.from_user.id,
|
||||
e,
|
||||
)
|
||||
|
||||
|
||||
@router_changing_the_symbol.message(ChangingTheSymbolState.symbol_state)
|
||||
async def set_symbol(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Handler for user input for setting the symbol.
|
||||
|
||||
Updates FSM context with the selected symbol and persists the choice in database.
|
||||
Sends an acknowledgement to user and clears FSM state afterward.
|
||||
|
||||
Args:
|
||||
message (Message): Incoming message from user containing the selected symbol.
|
||||
state (FSMContext): Finite State Machine context for the current user session.
|
||||
|
||||
Logs:
|
||||
Success or error messages with user identification.
|
||||
"""
|
||||
try:
|
||||
try:
|
||||
data = await state.get_data()
|
||||
if "prompt_message_id" in data:
|
||||
prompt_message_id = data["prompt_message_id"]
|
||||
await message.bot.delete_message(
|
||||
chat_id=message.chat.id, message_id=prompt_message_id
|
||||
)
|
||||
await message.delete()
|
||||
except Exception as e:
|
||||
if "message to delete not found" in str(e).lower():
|
||||
pass # Ignore this error
|
||||
else:
|
||||
raise e
|
||||
|
||||
symbol = message.text.upper()
|
||||
additional_settings = await rq.get_user_additional_settings(
|
||||
tg_id=message.from_user.id
|
||||
)
|
||||
|
||||
if not additional_settings:
|
||||
await rq.create_user_additional_settings(tg_id=message.from_user.id)
|
||||
return
|
||||
|
||||
trade_mode = additional_settings.trade_mode or "Merged_Single"
|
||||
mode = 0 if trade_mode == "Merged_Single" else 3
|
||||
margin_type = additional_settings.margin_type or "ISOLATED_MARGIN"
|
||||
leverage = "10"
|
||||
leverage_to_buy = "10"
|
||||
leverage_to_sell = "10"
|
||||
ticker = await get_tickers(tg_id=message.from_user.id, symbol=symbol)
|
||||
|
||||
if ticker is None:
|
||||
await message.answer(
|
||||
text=f"Инструмент {symbol} не найден.", reply_markup=kbi.symbol
|
||||
)
|
||||
return
|
||||
|
||||
req = await rq.set_user_symbol(tg_id=message.from_user.id, symbol=symbol)
|
||||
|
||||
if not req:
|
||||
await message.answer(
|
||||
text="Произошла ошибка при установке инструмента.",
|
||||
reply_markup=kbi.symbol,
|
||||
)
|
||||
return
|
||||
|
||||
await user_profile_bybit(
|
||||
tg_id=message.from_user.id, message=message, state=state
|
||||
)
|
||||
|
||||
res = await set_switch_position_mode(
|
||||
tg_id=message.from_user.id, symbol=symbol, mode=mode
|
||||
)
|
||||
if res == "You have an existing position, so position mode cannot be switched":
|
||||
if mode == 0:
|
||||
mode = 3
|
||||
else:
|
||||
mode = 0
|
||||
await set_switch_position_mode(
|
||||
tg_id=message.from_user.id, symbol=symbol, mode=mode
|
||||
)
|
||||
if trade_mode == "Merged_Single":
|
||||
trade_mode = "Both_Sides"
|
||||
else:
|
||||
trade_mode = "Merged_Single"
|
||||
await rq.set_trade_mode(tg_id=message.from_user.id, trade_mode=trade_mode)
|
||||
|
||||
await set_margin_mode(tg_id=message.from_user.id, margin_mode=margin_type)
|
||||
|
||||
if margin_type == "ISOLATED_MARGIN":
|
||||
await set_leverage_to_buy_and_sell(
|
||||
tg_id=message.from_user.id,
|
||||
symbol=symbol,
|
||||
leverage_to_buy=str(leverage_to_buy),
|
||||
leverage_to_sell=str(leverage_to_sell),
|
||||
)
|
||||
else:
|
||||
await set_leverage(
|
||||
tg_id=message.from_user.id, symbol=symbol, leverage=str(leverage)
|
||||
)
|
||||
|
||||
await rq.set_leverage(tg_id=message.from_user.id, leverage=str(leverage))
|
||||
await rq.set_leverage_to_buy_and_sell(
|
||||
tg_id=message.from_user.id,
|
||||
leverage_to_buy=str(leverage_to_buy),
|
||||
leverage_to_sell=str(leverage_to_sell),
|
||||
)
|
||||
await rq.set_limit_price(tg_id=message.from_user.id, limit_price=0)
|
||||
await rq.set_trigger_price(tg_id=message.from_user.id, trigger_price=0)
|
||||
|
||||
await state.clear()
|
||||
except Exception as e:
|
||||
await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.")
|
||||
logger.error("Error setting symbol for user %s: %s", message.from_user.id, e)
|
120
app/telegram/handlers/close_orders.py
Normal file
120
app/telegram/handlers/close_orders.py
Normal file
@@ -0,0 +1,120 @@
|
||||
import logging.config
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery
|
||||
|
||||
from app.bybit.close_positions import cancel_all_orders, cancel_order, close_position
|
||||
from logger_helper.logger_helper import LOGGING_CONFIG
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("close_orders")
|
||||
|
||||
router_close_orders = Router(name="close_orders")
|
||||
|
||||
|
||||
@router_close_orders.callback_query(
|
||||
lambda c: c.data and c.data.startswith("close_position_")
|
||||
)
|
||||
async def close_position_handler(
|
||||
callback_query: CallbackQuery, state: FSMContext
|
||||
) -> None:
|
||||
"""
|
||||
Close a position.
|
||||
:param callback_query: Incoming callback query from Telegram inline keyboard.
|
||||
:param state: Finite State Machine context for the current user session.
|
||||
:return: None
|
||||
"""
|
||||
try:
|
||||
symbol = callback_query.data.split("_", 2)[2]
|
||||
res = await close_position(tg_id=callback_query.from_user.id, symbol=symbol)
|
||||
|
||||
if not res:
|
||||
await callback_query.answer(text="Произошла ошибка при закрытии позиции.")
|
||||
return
|
||||
|
||||
await callback_query.answer(text="Позиция успешно закрыта.")
|
||||
logger.debug(
|
||||
"Command close_position processed successfully for user: %s",
|
||||
callback_query.from_user.id,
|
||||
)
|
||||
except Exception as e:
|
||||
await callback_query.answer(text="Произошла ошибка при закрытии позиции.")
|
||||
logger.error(
|
||||
"Error processing command close_position for user %s: %s",
|
||||
callback_query.from_user.id,
|
||||
e,
|
||||
)
|
||||
finally:
|
||||
await state.clear()
|
||||
|
||||
|
||||
@router_close_orders.callback_query(
|
||||
lambda c: c.data and c.data.startswith("close_order_")
|
||||
)
|
||||
async def cancel_order_handler(
|
||||
callback_query: CallbackQuery, state: FSMContext
|
||||
) -> None:
|
||||
"""
|
||||
Cancel an order.
|
||||
:param callback_query: Incoming callback query from Telegram inline keyboard.
|
||||
:param state: Finite State Machine context for the current user session.
|
||||
:return: None
|
||||
"""
|
||||
try:
|
||||
symbol = callback_query.data.split("_", 2)[2]
|
||||
res = await cancel_order(tg_id=callback_query.from_user.id, symbol=symbol)
|
||||
|
||||
if not res:
|
||||
await callback_query.answer(text="Произошла ошибка при закрытии ордера.")
|
||||
return
|
||||
|
||||
await callback_query.answer(text="Ордер успешно закрыт.")
|
||||
logger.debug(
|
||||
"Command close_order processed successfully for user: %s",
|
||||
callback_query.from_user.id,
|
||||
)
|
||||
except Exception as e:
|
||||
await callback_query.answer(text="Произошла ошибка при закрытии ордера.")
|
||||
logger.error(
|
||||
"Error processing command close_order for user %s: %s",
|
||||
callback_query.from_user.id,
|
||||
e,
|
||||
)
|
||||
finally:
|
||||
await state.clear()
|
||||
|
||||
|
||||
@router_close_orders.callback_query(F.data == "cancel_all_orders")
|
||||
async def cancel_all_orders_handler(
|
||||
callback_query: CallbackQuery, state: FSMContext
|
||||
) -> None:
|
||||
"""
|
||||
Close all open positions and orders.
|
||||
:param callback_query: Incoming callback query from Telegram inline keyboard.
|
||||
:param state: Finite State Machine context for the current user session.
|
||||
:return: None
|
||||
"""
|
||||
try:
|
||||
res = await cancel_all_orders(tg_id=callback_query.from_user.id)
|
||||
|
||||
if not res:
|
||||
await callback_query.answer(
|
||||
text="Произошла ошибка при закрытии всех ордеров."
|
||||
)
|
||||
return
|
||||
|
||||
await callback_query.answer(text="Все ордера успешно закрыты.")
|
||||
logger.debug(
|
||||
"Command close_all_orders processed successfully for user: %s",
|
||||
callback_query.from_user.id,
|
||||
)
|
||||
except Exception as e:
|
||||
await callback_query.answer(text="Произошла ошибка при закрытии всех ордеров.")
|
||||
logger.error(
|
||||
"Error processing command close_all_orders for user %s: %s",
|
||||
callback_query.from_user.id,
|
||||
e,
|
||||
)
|
||||
finally:
|
||||
await state.clear()
|
50
app/telegram/handlers/common.py
Normal file
50
app/telegram/handlers/common.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import logging.config
|
||||
|
||||
from aiogram import Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message
|
||||
|
||||
from logger_helper.logger_helper import LOGGING_CONFIG
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("common")
|
||||
|
||||
router_common = Router(name="common")
|
||||
|
||||
|
||||
@router_common.message()
|
||||
async def unknown_message(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Handle unexpected or unrecognized messages.
|
||||
Clears FSM state and informs the user about available commands.
|
||||
|
||||
Args:
|
||||
message (types.Message): Incoming message object.
|
||||
state (FSMContext): Current FSM context.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
try:
|
||||
await message.answer(
|
||||
text="Извините, я вас не понял. "
|
||||
"Пожалуйста, используйте одну из следующих команд:\n"
|
||||
"/start - Запустить бота\n"
|
||||
"/profile - Профиль\n"
|
||||
"/bybit - Панель Bybit\n"
|
||||
"/help - Получить помощь\n"
|
||||
)
|
||||
logger.debug(
|
||||
"Received unknown message from user %s: %s",
|
||||
message.from_user.id,
|
||||
message.text,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error handling unknown message for user %s: %s", message.from_user.id, e
|
||||
)
|
||||
await message.answer(
|
||||
text="Произошла ошибка при обработке вашего сообщения. Пожалуйста, попробуйте позже."
|
||||
)
|
||||
finally:
|
||||
await state.clear()
|
214
app/telegram/handlers/get_positions_handlers.py
Normal file
214
app/telegram/handlers/get_positions_handlers.py
Normal file
@@ -0,0 +1,214 @@
|
||||
import logging.config
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery
|
||||
|
||||
import app.telegram.keyboards.inline as kbi
|
||||
import database.request as rq
|
||||
from app.bybit.get_functions.get_positions import (
|
||||
get_active_orders,
|
||||
get_active_orders_by_symbol,
|
||||
get_active_positions,
|
||||
get_active_positions_by_symbol,
|
||||
)
|
||||
from logger_helper.logger_helper import LOGGING_CONFIG
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("get_positions_handlers")
|
||||
|
||||
router_get_positions_handlers = Router(name="get_positions_handlers")
|
||||
|
||||
|
||||
@router_get_positions_handlers.callback_query(F.data == "my_deals")
|
||||
async def get_positions_handlers(
|
||||
callback_query: CallbackQuery, state: FSMContext
|
||||
) -> None:
|
||||
"""
|
||||
Gets the user's active positions.
|
||||
:param callback_query: CallbackQuery object.
|
||||
:param state: FSMContext
|
||||
:return: None
|
||||
"""
|
||||
try:
|
||||
await state.clear()
|
||||
await callback_query.message.edit_text(
|
||||
text="Выберите какие сделки вы хотите посмотреть:",
|
||||
reply_markup=kbi.change_position,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Error in get_positions_handler: %s", e)
|
||||
await callback_query.answer(text="Произошла ошибка при получении сделок.")
|
||||
|
||||
|
||||
@router_get_positions_handlers.callback_query(F.data == "change_position")
|
||||
async def get_positions_handler(
|
||||
callback_query: CallbackQuery, state: FSMContext
|
||||
) -> None:
|
||||
"""
|
||||
Gets the user's active positions.
|
||||
:param callback_query: CallbackQuery object.
|
||||
:param state: FSMContext
|
||||
:return: None
|
||||
"""
|
||||
try:
|
||||
res = await get_active_positions(tg_id=callback_query.from_user.id)
|
||||
|
||||
if res is None:
|
||||
await callback_query.answer(
|
||||
text="Произошла ошибка при получении активных позиций."
|
||||
)
|
||||
return
|
||||
|
||||
if res == ["No active positions found"]:
|
||||
await callback_query.answer(text="Нет активных позиций.")
|
||||
return
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
text="Ваши активные позиции:",
|
||||
reply_markup=kbi.create_active_positions_keyboard(res),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Error in get_positions_handler: %s", e)
|
||||
await callback_query.answer(
|
||||
text="Произошла ошибка при получении активных позиций."
|
||||
)
|
||||
finally:
|
||||
await state.clear()
|
||||
|
||||
|
||||
@router_get_positions_handlers.callback_query(
|
||||
lambda c: c.data.startswith("get_position_")
|
||||
)
|
||||
async def get_position_handler(callback_query: CallbackQuery, state: FSMContext):
|
||||
try:
|
||||
symbol = callback_query.data.split("_", 2)[2]
|
||||
res = await get_active_positions_by_symbol(
|
||||
tg_id=callback_query.from_user.id, symbol=symbol
|
||||
)
|
||||
|
||||
if res is None:
|
||||
await callback_query.answer(
|
||||
text="Произошла ошибка при получении активных позиций."
|
||||
)
|
||||
return
|
||||
|
||||
symbol = res.get("symbol") or "Нет данных"
|
||||
avg_price = res.get("avgPrice") or "Нет данных"
|
||||
size = res.get("size") or "Нет данных"
|
||||
side = res.get("side") or ""
|
||||
side_rus = (
|
||||
"Покупка"
|
||||
if side == "Buy"
|
||||
else "Продажа" if side == "Sell" else "Нет данных"
|
||||
)
|
||||
take_profit = res.get("takeProfit") or "Нет данных"
|
||||
stop_loss = res.get("stopLoss") or "Нет данных"
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
text=f"Торговая пара: {symbol}\n"
|
||||
f"Цена входа: {avg_price}\n"
|
||||
f"Количество: {size}\n"
|
||||
f"Движение: {side_rus}\n"
|
||||
f"Тейк-профит: {take_profit}\n"
|
||||
f"Стоп-лосс: {stop_loss}\n",
|
||||
reply_markup=kbi.make_close_position_keyboard(symbol_pos=symbol),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error in get_position_handler: %s", e)
|
||||
await callback_query.answer(
|
||||
text="Произошла ошибка при получении активных позиций."
|
||||
)
|
||||
finally:
|
||||
await state.clear()
|
||||
|
||||
|
||||
@router_get_positions_handlers.callback_query(F.data == "open_orders")
|
||||
async def get_open_orders_handler(
|
||||
callback_query: CallbackQuery, state: FSMContext
|
||||
) -> None:
|
||||
"""
|
||||
Gets the user's open orders.
|
||||
:param callback_query: CallbackQuery object.
|
||||
:param state: FSMContext
|
||||
:return: None
|
||||
"""
|
||||
try:
|
||||
res = await get_active_orders(tg_id=callback_query.from_user.id)
|
||||
|
||||
if res is None:
|
||||
await callback_query.answer(
|
||||
text="Произошла ошибка при получении активных ордеров."
|
||||
)
|
||||
return
|
||||
|
||||
if res == ["No active orders found"]:
|
||||
await callback_query.answer(text="Нет активных ордеров.")
|
||||
return
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
text="Ваши активные ордера:",
|
||||
reply_markup=kbi.create_active_orders_keyboard(res),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Error in get_open_orders_handler: %s", e)
|
||||
await callback_query.answer(
|
||||
text="Произошла ошибка при получении активных ордеров."
|
||||
)
|
||||
finally:
|
||||
await state.clear()
|
||||
|
||||
|
||||
@router_get_positions_handlers.callback_query(lambda c: c.data.startswith("get_order_"))
|
||||
async def get_order_handler(callback_query: CallbackQuery, state: FSMContext):
|
||||
try:
|
||||
symbol = callback_query.data.split("_", 2)[2]
|
||||
res = await get_active_orders_by_symbol(
|
||||
tg_id=callback_query.from_user.id, symbol=symbol
|
||||
)
|
||||
|
||||
if res is None:
|
||||
await callback_query.answer(
|
||||
text="Произошла ошибка при получении активных ордеров."
|
||||
)
|
||||
return
|
||||
|
||||
symbol = res.get("symbol") or "Нет данных"
|
||||
price = res.get("price") or "Нет данных"
|
||||
qty = res.get("qty") or "Нет данных"
|
||||
side = res.get("side") or ""
|
||||
side_rus = (
|
||||
"Покупка"
|
||||
if side == "Buy"
|
||||
else "Продажа" if side == "Sell" else "Нет данных"
|
||||
)
|
||||
order_type = res.get("orderType") or ""
|
||||
order_type_rus = (
|
||||
"Рыночный"
|
||||
if order_type == "Market"
|
||||
else "Лимитный" if order_type == "Limit" else "Нет данных"
|
||||
)
|
||||
trigger_price = res.get("triggerPrice") or "Нет данных"
|
||||
take_profit = res.get("takeProfit") or "Нет данных"
|
||||
stop_loss = res.get("stopLoss") or "Нет данных"
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
text=f"Торговая пара: {symbol}\n"
|
||||
f"Цена: {price}\n"
|
||||
f"Количество: {qty}\n"
|
||||
f"Движение: {side_rus}\n"
|
||||
f"Тип ордера: {order_type_rus}\n"
|
||||
f"Триггер цена: {trigger_price}\n"
|
||||
f"Тейк-профит: {take_profit}\n"
|
||||
f"Стоп-лосс: {stop_loss}\n",
|
||||
reply_markup=kbi.make_close_orders_keyboard(symbol_order=symbol),
|
||||
)
|
||||
await rq.set_user_symbol(tg_id=callback_query.from_user.id, symbol=symbol)
|
||||
except Exception as e:
|
||||
logger.error("Error in get_order_handler: %s", e)
|
||||
await callback_query.answer(
|
||||
text="Произошла ошибка при получении активных ордеров."
|
||||
)
|
||||
finally:
|
||||
await state.clear()
|
319
app/telegram/handlers/handlers_main.py
Normal file
319
app/telegram/handlers/handlers_main.py
Normal file
@@ -0,0 +1,319 @@
|
||||
import logging.config
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
|
||||
import app.telegram.keyboards.inline as kbi
|
||||
import app.telegram.keyboards.reply as kbr
|
||||
import database.request as rq
|
||||
from app.bybit.profile_bybit import user_profile_bybit
|
||||
from app.telegram.functions.profile_tg import user_profile_tg
|
||||
from logger_helper.logger_helper import LOGGING_CONFIG
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("handlers_main")
|
||||
|
||||
router_handlers_main = Router(name="handlers_main")
|
||||
|
||||
|
||||
@router_handlers_main.message(Command("start", "hello"))
|
||||
@router_handlers_main.message(F.text.lower() == "привет")
|
||||
async def cmd_start(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Handle the /start or /hello commands and the text message "привет".
|
||||
|
||||
Checks if the user exists in the database and sends a user profile or creates a new user
|
||||
with default settings and greeting message.
|
||||
|
||||
Args:
|
||||
message (Message): Incoming Telegram message object.
|
||||
state (FSMContext): FSMContext for managing user state.
|
||||
|
||||
Raises:
|
||||
None: Exceptions are caught and logged internally.
|
||||
"""
|
||||
tg_id = message.from_user.id
|
||||
username = message.from_user.username
|
||||
full_name = message.from_user.full_name
|
||||
user = await rq.get_user(tg_id)
|
||||
try:
|
||||
if user:
|
||||
await user_profile_tg(tg_id=message.from_user.id, message=message)
|
||||
logger.debug(
|
||||
"Command start processed successfully for user: %s",
|
||||
message.from_user.id,
|
||||
)
|
||||
else:
|
||||
await rq.create_user(tg_id=tg_id, username=username)
|
||||
await rq.set_user_symbol(tg_id=tg_id, symbol="BTCUSDT")
|
||||
await rq.create_user_additional_settings(tg_id=tg_id)
|
||||
await rq.create_user_risk_management(tg_id=tg_id)
|
||||
await rq.create_user_conditional_settings(tg_id=tg_id)
|
||||
await message.answer(
|
||||
text=f"Добро пожаловать, {full_name}!\n\n"
|
||||
"PHANTOM TRADING - ваш надежный помощник для автоматизации трейдинга😉",
|
||||
reply_markup=kbi.connect_the_platform,
|
||||
)
|
||||
logger.debug(
|
||||
"Command start processed successfully for user: %s",
|
||||
message.from_user.id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error processing command start for user %s: %s", message.from_user.id, e
|
||||
)
|
||||
await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.")
|
||||
finally:
|
||||
await state.clear()
|
||||
|
||||
|
||||
@router_handlers_main.message(Command("profile"))
|
||||
@router_handlers_main.message(F.text == "Профиль")
|
||||
async def cmd_to_main(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Handle the /profile command or text "Профиль".
|
||||
|
||||
Clears the current FSM state and sends the Telegram user profile.
|
||||
|
||||
Args:
|
||||
message (Message): Incoming Telegram message object.
|
||||
state (FSMContext): FSM state context.
|
||||
|
||||
Raises:
|
||||
None: Exceptions are caught and logged internally.
|
||||
"""
|
||||
try:
|
||||
await user_profile_tg(tg_id=message.from_user.id, message=message)
|
||||
logger.debug(
|
||||
"Command to_profile_tg processed successfully for user: %s",
|
||||
message.from_user.id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error processing command to_profile_tg for user %s: %s",
|
||||
message.from_user.id,
|
||||
e,
|
||||
)
|
||||
finally:
|
||||
await state.clear()
|
||||
|
||||
|
||||
@router_handlers_main.message(Command("bybit"))
|
||||
@router_handlers_main.message(F.text == "Панель Bybit")
|
||||
async def profile_bybit(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Handle the /bybit command or text "Панель Bybit".
|
||||
|
||||
Clears FSM state and sends Bybit trading panel profile.
|
||||
|
||||
Args:
|
||||
message (Message): Incoming Telegram message object.
|
||||
state (FSMContext): FSM state context.
|
||||
|
||||
Raises:
|
||||
None: Exceptions are caught and logged internally.
|
||||
"""
|
||||
try:
|
||||
await state.clear()
|
||||
await user_profile_bybit(
|
||||
tg_id=message.from_user.id, message=message, state=state
|
||||
)
|
||||
logger.debug(
|
||||
"Command to_profile_bybit processed successfully for user: %s",
|
||||
message.from_user.id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error processing command to_profile_bybit for user %s: %s",
|
||||
message.from_user.id,
|
||||
e,
|
||||
)
|
||||
|
||||
|
||||
@router_handlers_main.callback_query(F.data == "profile_bybit")
|
||||
async def profile_bybit_callback(
|
||||
callback_query: CallbackQuery, state: FSMContext
|
||||
) -> None:
|
||||
"""
|
||||
Handle callback query with data "profile_bybit".
|
||||
|
||||
Clears FSM state and sends the Bybit profile in response.
|
||||
|
||||
Args:
|
||||
callback_query (CallbackQuery): Callback query object from Telegram.
|
||||
state (FSMContext): FSM state context.
|
||||
|
||||
Raises:
|
||||
None: Exceptions are caught and logged internally.
|
||||
"""
|
||||
try:
|
||||
await state.clear()
|
||||
await user_profile_bybit(
|
||||
tg_id=callback_query.from_user.id,
|
||||
message=callback_query.message,
|
||||
state=state,
|
||||
)
|
||||
logger.debug(
|
||||
"Callback profile_bybit processed successfully for user: %s",
|
||||
callback_query.from_user.id,
|
||||
)
|
||||
await callback_query.answer()
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error processing callback profile_bybit for user %s: %s",
|
||||
callback_query.from_user.id,
|
||||
e,
|
||||
)
|
||||
|
||||
|
||||
@router_handlers_main.callback_query(F.data == "main_settings")
|
||||
async def settings(callback_query: CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Handle callback query with data "main_settings".
|
||||
|
||||
Clears FSM state and edits the message to show main settings options.
|
||||
|
||||
Args:
|
||||
callback_query (CallbackQuery): Callback query object.
|
||||
state (FSMContext): FSM state context.
|
||||
|
||||
Raises:
|
||||
None: Exceptions are caught and logged internally.
|
||||
"""
|
||||
try:
|
||||
await state.clear()
|
||||
msg = await callback_query.message.edit_text(
|
||||
text="Выберите, что вы хотите настроить:", reply_markup=kbi.main_settings
|
||||
)
|
||||
await state.update_data(prompt_message_id=msg.message_id)
|
||||
logger.debug(
|
||||
"Command settings processed successfully for user: %s",
|
||||
callback_query.from_user.id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error processing command settings for user %s: %s",
|
||||
callback_query.from_user.id,
|
||||
e,
|
||||
)
|
||||
|
||||
|
||||
@router_handlers_main.message(Command("help"))
|
||||
async def cmd_help(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Handle the /help command.
|
||||
|
||||
Clears FSM state and sends a help message with available commands and reply keyboard.
|
||||
|
||||
Args:
|
||||
message (Message): Incoming Telegram message object.
|
||||
state (FSMContext): FSM state context.
|
||||
|
||||
Raises:
|
||||
None: Exceptions are caught and logged internally.
|
||||
"""
|
||||
try:
|
||||
await state.clear()
|
||||
await message.answer(
|
||||
text="Используйте одну из следующих команд:\n"
|
||||
"/start - Запустить бота\n"
|
||||
"/profile - Профиль\n"
|
||||
"/bybit - Панель Bybit\n",
|
||||
reply_markup=kbr.profile,
|
||||
)
|
||||
logger.debug(
|
||||
"Command help processed successfully for user: %s",
|
||||
message.from_user.id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error processing command help for user %s: %s", message.from_user.id, e
|
||||
)
|
||||
await message.answer(
|
||||
text="Произошла ошибка. Пожалуйста, попробуйте позже.",
|
||||
reply_markup=kbr.profile,
|
||||
)
|
||||
|
||||
|
||||
@router_handlers_main.message(Command("cancel"))
|
||||
@router_handlers_main.message(
|
||||
lambda message: message.text.casefold() in ["cancel", "отмена"]
|
||||
)
|
||||
async def cmd_cancel_handler(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Handle /cancel command or text 'cancel'/'отмена'.
|
||||
|
||||
If there is an active FSM state, clears it and informs the user.
|
||||
Otherwise, informs that no operation was in progress.
|
||||
|
||||
Args:
|
||||
message (Message): Incoming Telegram message object.
|
||||
state (FSMContext): FSM state context.
|
||||
|
||||
Raises:
|
||||
None: Exceptions are caught and logged internally.
|
||||
"""
|
||||
current_state = await state.get_state()
|
||||
|
||||
if current_state is None:
|
||||
await message.reply(
|
||||
text="Хорошо, но ничего не происходило.", reply_markup=kbr.profile
|
||||
)
|
||||
logger.debug(
|
||||
"Cancel command received but no active state for user %s.",
|
||||
message.from_user.id,
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
await state.clear()
|
||||
await message.reply(text="Команда отменена.", reply_markup=kbr.profile)
|
||||
logger.debug(
|
||||
"Command cancel executed successfully. State cleared for user %s.",
|
||||
message.from_user.id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error while cancelling command for user %s: %s", message.from_user.id, e
|
||||
)
|
||||
await message.answer(
|
||||
text="Произошла ошибка при отмене. Пожалуйста, попробуйте позже.",
|
||||
reply_markup=kbr.profile,
|
||||
)
|
||||
|
||||
|
||||
@router_handlers_main.callback_query(F.data == "cancel")
|
||||
async def cmd_cancel(callback_query: CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Handle callback query with data "cancel".
|
||||
|
||||
Clears the FSM state and sends a cancellation message.
|
||||
|
||||
Args:
|
||||
callback_query (CallbackQuery): Callback query object.
|
||||
state (FSMContext): FSM state context.
|
||||
|
||||
Raises:
|
||||
None: Exceptions are caught and logged internally.
|
||||
"""
|
||||
try:
|
||||
await callback_query.message.delete()
|
||||
await user_profile_bybit(
|
||||
tg_id=callback_query.from_user.id,
|
||||
message=callback_query.message,
|
||||
state=state,
|
||||
)
|
||||
logger.debug(
|
||||
"Command cancel processed successfully for user: %s",
|
||||
callback_query.from_user.id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error processing command cancel for user %s: %s",
|
||||
callback_query.from_user.id,
|
||||
e,
|
||||
)
|
||||
finally:
|
||||
await state.clear()
|
17
app/telegram/handlers/main_settings/__init__.py
Normal file
17
app/telegram/handlers/main_settings/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
__all__ = "router"
|
||||
|
||||
from aiogram import Router
|
||||
|
||||
from app.telegram.handlers.main_settings.additional_settings import (
|
||||
router_additional_settings,
|
||||
)
|
||||
from app.telegram.handlers.main_settings.conditional_settings import (
|
||||
router_conditional_settings,
|
||||
)
|
||||
from app.telegram.handlers.main_settings.risk_management import router_risk_management
|
||||
|
||||
router_main_settings = Router(name=__name__)
|
||||
|
||||
router_main_settings.include_router(router_additional_settings)
|
||||
router_main_settings.include_router(router_risk_management)
|
||||
router_main_settings.include_router(router_conditional_settings)
|
1547
app/telegram/handlers/main_settings/additional_settings.py
Normal file
1547
app/telegram/handlers/main_settings/additional_settings.py
Normal file
File diff suppressed because it is too large
Load Diff
174
app/telegram/handlers/main_settings/conditional_settings.py
Normal file
174
app/telegram/handlers/main_settings/conditional_settings.py
Normal file
@@ -0,0 +1,174 @@
|
||||
import logging.config
|
||||
|
||||
from aiogram import Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
|
||||
import app.telegram.keyboards.inline as kbi
|
||||
import database.request as rq
|
||||
from app.helper_functions import is_int_for_timer
|
||||
from app.telegram.states.states import ConditionalSettingsState
|
||||
from logger_helper.logger_helper import LOGGING_CONFIG
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("conditional_settings")
|
||||
|
||||
router_conditional_settings = Router(name="conditional_settings")
|
||||
|
||||
|
||||
@router_conditional_settings.callback_query(
|
||||
lambda c: c.data == "start_timer" or c.data == "stop_timer"
|
||||
)
|
||||
async def timer(callback_query: CallbackQuery, state: FSMContext):
|
||||
"""
|
||||
Handles callback queries starting with 'start_timer' or 'stop_timer'.
|
||||
"""
|
||||
try:
|
||||
await state.clear()
|
||||
if callback_query.data == "start_timer":
|
||||
await state.set_state(ConditionalSettingsState.start_timer_state)
|
||||
msg = await callback_query.message.edit_text(
|
||||
"Введите время в минутах для старта торговли:",
|
||||
reply_markup=kbi.back_to_conditions,
|
||||
)
|
||||
await state.update_data(prompt_message_id=msg.message_id)
|
||||
elif callback_query.data == "stop_timer":
|
||||
await state.set_state(ConditionalSettingsState.stop_timer_state)
|
||||
msg = await callback_query.message.edit_text(
|
||||
"Введите время в минутах для остановки торговли:",
|
||||
reply_markup=kbi.back_to_conditions,
|
||||
)
|
||||
await state.update_data(prompt_message_id=msg.message_id)
|
||||
else:
|
||||
await callback_query.answer(
|
||||
text="Произошла ошибка. Пожалуйста, попробуйте позже."
|
||||
)
|
||||
except Exception as e:
|
||||
await callback_query.answer(
|
||||
text="Произошла ошибка. Пожалуйста, попробуйте позже."
|
||||
)
|
||||
logger.error(
|
||||
"Error processing command timer for user %s: %s",
|
||||
callback_query.from_user.id,
|
||||
e,
|
||||
)
|
||||
|
||||
|
||||
@router_conditional_settings.message(ConditionalSettingsState.start_timer_state)
|
||||
async def start_timer(message: Message, state: FSMContext):
|
||||
"""
|
||||
Handles the start_timer state of the Finite State Machine.
|
||||
"""
|
||||
try:
|
||||
try:
|
||||
data = await state.get_data()
|
||||
if "prompt_message_id" in data:
|
||||
prompt_message_id = data["prompt_message_id"]
|
||||
await message.bot.delete_message(
|
||||
chat_id=message.chat.id, message_id=prompt_message_id
|
||||
)
|
||||
await message.delete()
|
||||
except Exception as e:
|
||||
if "message to delete not found" in str(e).lower():
|
||||
pass # Ignore this error
|
||||
else:
|
||||
raise e
|
||||
|
||||
get_start_timer = message.text
|
||||
value = is_int_for_timer(get_start_timer)
|
||||
|
||||
if value is False:
|
||||
await message.answer(
|
||||
"Ошибка: введите валидное число.",
|
||||
reply_markup=kbi.back_to_conditions,
|
||||
)
|
||||
logger.debug(
|
||||
"User %s input invalid (not an valid number): %s",
|
||||
message.from_user.id,
|
||||
get_start_timer,
|
||||
)
|
||||
return
|
||||
|
||||
req = await rq.set_start_timer(
|
||||
tg_id=message.from_user.id, timer_start=int(get_start_timer)
|
||||
)
|
||||
|
||||
if req:
|
||||
await message.answer(
|
||||
"Таймер успешно установлен.",
|
||||
reply_markup=kbi.back_to_conditions,
|
||||
)
|
||||
else:
|
||||
await message.answer(
|
||||
"Произошла ошибка. Пожалуйста, попробуйте позже.",
|
||||
reply_markup=kbi.back_to_conditions,
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
except Exception as e:
|
||||
await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.")
|
||||
logger.error(
|
||||
"Error processing command start_timer for user %s: %s",
|
||||
message.from_user.id,
|
||||
e,
|
||||
)
|
||||
|
||||
|
||||
@router_conditional_settings.message(ConditionalSettingsState.stop_timer_state)
|
||||
async def stop_timer(message: Message, state: FSMContext):
|
||||
"""
|
||||
Handles the stop_timer state of the Finite State Machine.
|
||||
"""
|
||||
try:
|
||||
try:
|
||||
data = await state.get_data()
|
||||
if "prompt_message_id" in data:
|
||||
prompt_message_id = data["prompt_message_id"]
|
||||
await message.bot.delete_message(
|
||||
chat_id=message.chat.id, message_id=prompt_message_id
|
||||
)
|
||||
await message.delete()
|
||||
except Exception as e:
|
||||
if "message to delete not found" in str(e).lower():
|
||||
pass # Ignore this error
|
||||
else:
|
||||
raise e
|
||||
|
||||
get_stop_timer = message.text
|
||||
value = is_int_for_timer(get_stop_timer)
|
||||
|
||||
if value is False:
|
||||
await message.answer(
|
||||
"Ошибка: введите валидное число.",
|
||||
reply_markup=kbi.back_to_conditions,
|
||||
)
|
||||
logger.debug(
|
||||
"User %s input invalid (not an valid number): %s",
|
||||
message.from_user.id,
|
||||
get_stop_timer,
|
||||
)
|
||||
return
|
||||
|
||||
req = await rq.set_stop_timer(
|
||||
tg_id=message.from_user.id, timer_end=int(get_stop_timer)
|
||||
)
|
||||
|
||||
if req:
|
||||
await message.answer(
|
||||
"Таймер успешно установлен.",
|
||||
reply_markup=kbi.back_to_conditions,
|
||||
)
|
||||
else:
|
||||
await message.answer(
|
||||
"Произошла ошибка. Пожалуйста, попробуйте позже.",
|
||||
reply_markup=kbi.back_to_conditions,
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
except Exception as e:
|
||||
await message.answer(text="Произошла ошибка. Пожалуйста, попробуйте позже.")
|
||||
logger.error(
|
||||
"Error processing command stop_timer for user %s: %s",
|
||||
message.from_user.id,
|
||||
e,
|
||||
)
|
467
app/telegram/handlers/main_settings/risk_management.py
Normal file
467
app/telegram/handlers/main_settings/risk_management.py
Normal file
@@ -0,0 +1,467 @@
|
||||
import logging.config
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
|
||||
import app.telegram.keyboards.inline as kbi
|
||||
import database.request as rq
|
||||
from app.helper_functions import is_int
|
||||
from app.telegram.states.states import RiskManagementState
|
||||
from logger_helper.logger_helper import LOGGING_CONFIG
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("risk_management")
|
||||
|
||||
router_risk_management = Router(name="risk_management")
|
||||
|
||||
|
||||
@router_risk_management.callback_query(F.data == "take_profit_percent")
|
||||
async def take_profit_percent(callback_query: CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Handles the 'profit_price_change' callback query.
|
||||
|
||||
Clears the current FSM state, edits the message text to display the take profit percent options,
|
||||
and shows an inline keyboard for selection.
|
||||
|
||||
Args:
|
||||
callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard.
|
||||
state (FSMContext): Finite State Machine context for the current user session.
|
||||
|
||||
Logs:
|
||||
Success or error messages with user identification.
|
||||
"""
|
||||
try:
|
||||
await state.clear()
|
||||
await state.set_state(RiskManagementState.take_profit_percent_state)
|
||||
msg = await callback_query.message.edit_text(
|
||||
text="Введите процент изменения цены для фиксации прибыли: ",
|
||||
reply_markup=kbi.back_to_risk_management,
|
||||
)
|
||||
await state.update_data(prompt_message_id=msg.message_id)
|
||||
logger.debug(
|
||||
"Command profit_price_change processed successfully for user: %s",
|
||||
callback_query.from_user.id,
|
||||
)
|
||||
except Exception as e:
|
||||
await callback_query.answer(
|
||||
text="Произошла ошибка. Пожалуйста, попробуйте позже."
|
||||
)
|
||||
logger.error(
|
||||
"Error processing command profit_price_change for user %s: %s",
|
||||
callback_query.from_user.id,
|
||||
e,
|
||||
)
|
||||
|
||||
|
||||
@router_risk_management.message(RiskManagementState.take_profit_percent_state)
|
||||
async def set_take_profit_percent(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Handles user input for setting the take profit percentage.
|
||||
|
||||
Updates FSM context with the selected percentage and persists the choice in database.
|
||||
Sends an acknowledgement to user and clears FSM state afterward.
|
||||
|
||||
Args:
|
||||
message (Message): Incoming message from user containing the take profit percentage.
|
||||
state (FSMContext): Finite State Machine context for the current user session.
|
||||
|
||||
Logs:
|
||||
Success or error messages with user identification.
|
||||
"""
|
||||
try:
|
||||
try:
|
||||
data = await state.get_data()
|
||||
if "prompt_message_id" in data:
|
||||
prompt_message_id = data["prompt_message_id"]
|
||||
await message.bot.delete_message(
|
||||
chat_id=message.chat.id, message_id=prompt_message_id
|
||||
)
|
||||
await message.delete()
|
||||
except Exception as e:
|
||||
if "message to delete not found" in str(e).lower():
|
||||
pass # Ignore this error
|
||||
else:
|
||||
raise e
|
||||
|
||||
take_profit_percent_value = message.text
|
||||
|
||||
if not is_int(take_profit_percent_value):
|
||||
await message.answer(
|
||||
text="Ошибка: введите валидное число.",
|
||||
reply_markup=kbi.back_to_risk_management,
|
||||
)
|
||||
logger.debug(
|
||||
"User %s input invalid (not an valid number): %s",
|
||||
message.from_user.id,
|
||||
take_profit_percent_value,
|
||||
)
|
||||
return
|
||||
|
||||
if int(take_profit_percent_value) < 1 or int(take_profit_percent_value) > 100:
|
||||
await message.answer(
|
||||
text="Ошибка: введите число от 1 до 100.",
|
||||
reply_markup=kbi.back_to_risk_management,
|
||||
)
|
||||
logger.debug(
|
||||
"User %s input invalid (not an valid number): %s",
|
||||
message.from_user.id,
|
||||
take_profit_percent_value,
|
||||
)
|
||||
return
|
||||
|
||||
req = await rq.set_take_profit_percent(
|
||||
tg_id=message.from_user.id,
|
||||
take_profit_percent=int(take_profit_percent_value),
|
||||
)
|
||||
|
||||
if req:
|
||||
await message.answer(
|
||||
text=f"Процент изменения цены для фиксации прибыли "
|
||||
f"установлен на {take_profit_percent_value}%.",
|
||||
reply_markup=kbi.back_to_risk_management,
|
||||
)
|
||||
else:
|
||||
await message.answer(
|
||||
text="Произошла ошибка при установке процента изменения цены для фиксации прибыли. "
|
||||
"Пожалуйста, попробуйте позже.",
|
||||
reply_markup=kbi.back_to_risk_management,
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error processing command profit_price_change for user %s: %s",
|
||||
message.from_user.id,
|
||||
e,
|
||||
)
|
||||
|
||||
|
||||
@router_risk_management.callback_query(F.data == "stop_loss_percent")
|
||||
async def stop_loss_percent(callback_query: CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Handles the 'stop_loss_percent' callback query.
|
||||
|
||||
Clears the current FSM state, edits the message text to display the stop loss percentage options,
|
||||
and shows an inline keyboard for selection.
|
||||
|
||||
Args:
|
||||
callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard.
|
||||
state (FSMContext): Finite State Machine context for the current user session.
|
||||
|
||||
Logs:
|
||||
Success or error messages with user identification.
|
||||
"""
|
||||
try:
|
||||
await state.clear()
|
||||
await state.set_state(RiskManagementState.stop_loss_percent_state)
|
||||
msg = await callback_query.message.edit_text(
|
||||
text="Введите процент изменения цены для фиксации убытка: ",
|
||||
reply_markup=kbi.back_to_risk_management,
|
||||
)
|
||||
await state.update_data(prompt_message_id=msg.message_id)
|
||||
logger.debug(
|
||||
"Command stop_loss_percent processed successfully for user: %s",
|
||||
callback_query.from_user.id,
|
||||
)
|
||||
except Exception as e:
|
||||
await callback_query.answer(
|
||||
text="Произошла ошибка. Пожалуйста, попробуйте позже."
|
||||
)
|
||||
logger.error(
|
||||
"Error processing command stop_loss_percent for user %s: %s",
|
||||
callback_query.from_user.id,
|
||||
e,
|
||||
)
|
||||
|
||||
|
||||
@router_risk_management.message(RiskManagementState.stop_loss_percent_state)
|
||||
async def set_stop_loss_percent(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Handles user input for setting the stop loss percentage.
|
||||
|
||||
Updates FSM context with the selected percentage and persists the choice in database.
|
||||
Sends an acknowledgement to user and clears FSM state afterward.
|
||||
|
||||
Args:
|
||||
message (Message): Incoming message from user containing the stop loss percentage.
|
||||
state (FSMContext): Finite State Machine context for the current user session.
|
||||
|
||||
Logs:
|
||||
Success or error messages with user identification.
|
||||
"""
|
||||
try:
|
||||
try:
|
||||
data = await state.get_data()
|
||||
if "prompt_message_id" in data:
|
||||
prompt_message_id = data["prompt_message_id"]
|
||||
await message.bot.delete_message(
|
||||
chat_id=message.chat.id, message_id=prompt_message_id
|
||||
)
|
||||
await message.delete()
|
||||
except Exception as e:
|
||||
if "message to delete not found" in str(e).lower():
|
||||
pass # Ignore this error
|
||||
else:
|
||||
raise e
|
||||
|
||||
stop_loss_percent_value = message.text
|
||||
|
||||
if not is_int(stop_loss_percent_value):
|
||||
await message.answer(
|
||||
text="Ошибка: введите валидное число.",
|
||||
reply_markup=kbi.back_to_risk_management,
|
||||
)
|
||||
logger.debug(
|
||||
"User %s input invalid (not an valid number): %s",
|
||||
message.from_user.id,
|
||||
stop_loss_percent_value,
|
||||
)
|
||||
return
|
||||
|
||||
if int(stop_loss_percent_value) < 1 or int(stop_loss_percent_value) > 100:
|
||||
await message.answer(
|
||||
text="Ошибка: введите число от 1 до 100.",
|
||||
reply_markup=kbi.back_to_risk_management,
|
||||
)
|
||||
logger.debug(
|
||||
"User %s input invalid (not an valid number): %s",
|
||||
message.from_user.id,
|
||||
stop_loss_percent_value,
|
||||
)
|
||||
return
|
||||
|
||||
req = await rq.set_stop_loss_percent(
|
||||
tg_id=message.from_user.id, stop_loss_percent=int(stop_loss_percent_value)
|
||||
)
|
||||
|
||||
if req:
|
||||
await message.answer(
|
||||
text=f"Процент изменения цены для фиксации убытка "
|
||||
f"установлен на {stop_loss_percent_value}%.",
|
||||
reply_markup=kbi.back_to_risk_management,
|
||||
)
|
||||
else:
|
||||
await message.answer(
|
||||
text="Произошла ошибка при установке процента изменения цены для фиксации убытка. "
|
||||
"Пожалуйста, попробуйте позже.",
|
||||
reply_markup=kbi.back_to_risk_management,
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
except Exception as e:
|
||||
await message.answer(
|
||||
text="Произошла ошибка при установке процента изменения цены для фиксации убытка. "
|
||||
"Пожалуйста, попробуйте позже.",
|
||||
reply_markup=kbi.back_to_risk_management,
|
||||
)
|
||||
logger.error(
|
||||
"Error processing command stop_loss_percent for user %s: %s",
|
||||
message.from_user.id,
|
||||
e,
|
||||
)
|
||||
|
||||
|
||||
@router_risk_management.callback_query(F.data == "max_risk_percent")
|
||||
async def max_risk_percent(callback_query: CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Handles the 'max_risk_percent' callback query.
|
||||
|
||||
Clears the current FSM state, edits the message text to display the maximum risk percentage options,
|
||||
and shows an inline keyboard for selection.
|
||||
|
||||
Args:
|
||||
callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard.
|
||||
state (FSMContext): Finite State Machine context for the current user session.
|
||||
|
||||
Logs:
|
||||
Success or error messages with user identification.
|
||||
"""
|
||||
try:
|
||||
await state.clear()
|
||||
await state.set_state(RiskManagementState.max_risk_percent_state)
|
||||
msg = await callback_query.message.edit_text(
|
||||
text="Введите максимальный процент риска: ",
|
||||
reply_markup=kbi.back_to_risk_management,
|
||||
)
|
||||
await state.update_data(prompt_message_id=msg.message_id)
|
||||
logger.debug(
|
||||
"Command max_risk_percent processed successfully for user: %s",
|
||||
callback_query.from_user.id,
|
||||
)
|
||||
except Exception as e:
|
||||
await callback_query.answer(
|
||||
text="Произошла ошибка. Пожалуйста, попробуйте позже."
|
||||
)
|
||||
logger.error(
|
||||
"Error processing command max_risk_percent for user %s: %s",
|
||||
callback_query.from_user.id,
|
||||
e,
|
||||
)
|
||||
|
||||
|
||||
@router_risk_management.message(RiskManagementState.max_risk_percent_state)
|
||||
async def set_max_risk_percent(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Handles user input for setting the maximum risk percentage.
|
||||
|
||||
Updates FSM context with the selected percentage and persists the choice in database.
|
||||
Sends an acknowledgement to user and clears FSM state afterward.
|
||||
|
||||
Args:
|
||||
message (Message): Incoming message from user containing the maximum risk percentage.
|
||||
state (FSMContext): Finite State Machine context for the current user session.
|
||||
|
||||
Logs:
|
||||
Success or error messages with user identification.
|
||||
"""
|
||||
try:
|
||||
try:
|
||||
data = await state.get_data()
|
||||
if "prompt_message_id" in data:
|
||||
prompt_message_id = data["prompt_message_id"]
|
||||
await message.bot.delete_message(
|
||||
chat_id=message.chat.id, message_id=prompt_message_id
|
||||
)
|
||||
await message.delete()
|
||||
except Exception as e:
|
||||
if "message to delete not found" in str(e).lower():
|
||||
pass # Ignore this error
|
||||
else:
|
||||
raise e
|
||||
|
||||
max_risk_percent_value = message.text
|
||||
|
||||
if not is_int(max_risk_percent_value):
|
||||
await message.answer(
|
||||
text="Ошибка: введите валидное число.",
|
||||
reply_markup=kbi.back_to_risk_management,
|
||||
)
|
||||
logger.debug(
|
||||
"User %s input invalid (not an valid number): %s",
|
||||
message.from_user.id,
|
||||
max_risk_percent_value,
|
||||
)
|
||||
return
|
||||
|
||||
if int(max_risk_percent_value) < 1 or int(max_risk_percent_value) > 100:
|
||||
await message.answer(
|
||||
text="Ошибка: введите число от 1 до 100.",
|
||||
reply_markup=kbi.back_to_risk_management,
|
||||
)
|
||||
logger.debug(
|
||||
"User %s input invalid (not an valid number): %s",
|
||||
message.from_user.id,
|
||||
max_risk_percent_value,
|
||||
)
|
||||
return
|
||||
|
||||
req = await rq.set_max_risk_percent(
|
||||
tg_id=message.from_user.id, max_risk_percent=int(max_risk_percent_value)
|
||||
)
|
||||
|
||||
if req:
|
||||
await message.answer(
|
||||
text=f"Максимальный процент риска установлен на {max_risk_percent_value}%.",
|
||||
reply_markup=kbi.back_to_risk_management,
|
||||
)
|
||||
else:
|
||||
await message.answer(
|
||||
text="Произошла ошибка при установке максимального процента риска. "
|
||||
"Пожалуйста, попробуйте позже.",
|
||||
reply_markup=kbi.back_to_risk_management,
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
except Exception as e:
|
||||
await message.answer(
|
||||
text="Произошла ошибка при установке максимального процента риска. "
|
||||
"Пожалуйста, попробуйте позже.",
|
||||
reply_markup=kbi.back_to_risk_management,
|
||||
)
|
||||
logger.error(
|
||||
"Error processing command max_risk_percent for user %s: %s",
|
||||
message.from_user.id,
|
||||
e,
|
||||
)
|
||||
|
||||
|
||||
@router_risk_management.callback_query(F.data == "commission_fee")
|
||||
async def commission_fee(callback_query: CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Handles the 'commission_fee' callback query.
|
||||
|
||||
Clears the current FSM state, edits the message text to display the commission fee options,
|
||||
and shows an inline keyboard for selection.
|
||||
|
||||
Args:
|
||||
callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard.
|
||||
state (FSMContext): Finite State Machine context for the current user session.
|
||||
|
||||
Logs:
|
||||
Success or error messages with user identification.
|
||||
"""
|
||||
try:
|
||||
await state.clear()
|
||||
await state.set_state(RiskManagementState.commission_fee_state)
|
||||
msg = await callback_query.message.edit_text(
|
||||
text="Учитывать комиссию биржи для расчета прибыли?: ",
|
||||
reply_markup=kbi.commission_fee,
|
||||
)
|
||||
await state.update_data(prompt_message_id=msg.message_id)
|
||||
logger.debug(
|
||||
"Command commission_fee processed successfully for user: %s",
|
||||
callback_query.from_user.id,
|
||||
)
|
||||
except Exception as e:
|
||||
await callback_query.answer(
|
||||
text="Произошла ошибка. Пожалуйста, попробуйте позже."
|
||||
)
|
||||
logger.error(
|
||||
"Error processing command commission_fee for user %s: %s",
|
||||
callback_query.from_user.id,
|
||||
e,
|
||||
)
|
||||
|
||||
|
||||
@router_risk_management.callback_query(
|
||||
lambda c: c.data in ["Yes_commission_fee", "No_commission_fee"]
|
||||
)
|
||||
async def set_commission_fee(callback_query: CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Handles user input for setting the commission fee.
|
||||
|
||||
Updates FSM context with the selected option and persists the choice in database.
|
||||
Sends an acknowledgement to user and clears FSM state afterward.
|
||||
|
||||
Args:
|
||||
callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard.
|
||||
state (FSMContext): Finite State Machine context for the current user session.
|
||||
|
||||
Logs:
|
||||
Success or error messages with user identification.
|
||||
"""
|
||||
try:
|
||||
req = await rq.set_commission_fee(
|
||||
tg_id=callback_query.from_user.id, commission_fee=callback_query.data
|
||||
)
|
||||
|
||||
if not req:
|
||||
await callback_query.answer(
|
||||
text="Произошла ошибка при установке комиссии биржи. Пожалуйста, попробуйте позже."
|
||||
)
|
||||
return
|
||||
|
||||
if callback_query.data == "Yes_commission_fee":
|
||||
await callback_query.answer(text="Комиссия биржи учитывается.")
|
||||
else:
|
||||
await callback_query.answer(text="Комиссия биржи не учитывается.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error processing command commission_fee for user %s: %s",
|
||||
callback_query.from_user.id,
|
||||
e,
|
||||
)
|
||||
finally:
|
||||
await state.clear()
|
278
app/telegram/handlers/settings.py
Normal file
278
app/telegram/handlers/settings.py
Normal file
@@ -0,0 +1,278 @@
|
||||
import logging.config
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery
|
||||
|
||||
import app.telegram.keyboards.inline as kbi
|
||||
import database.request as rq
|
||||
from app.bybit import get_bybit_client
|
||||
from app.bybit.get_functions.get_tickers import get_tickers
|
||||
from app.helper_functions import calculate_total_budget, get_base_currency, safe_float
|
||||
from logger_helper.logger_helper import LOGGING_CONFIG
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("settings")
|
||||
|
||||
router_settings = Router(name="settings")
|
||||
|
||||
|
||||
@router_settings.callback_query(F.data == "additional_settings")
|
||||
async def additional_settings(callback_query: CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Handler for the "additional_settings" command.
|
||||
Sends a message with additional settings options.
|
||||
"""
|
||||
try:
|
||||
await state.clear()
|
||||
tg_id = callback_query.from_user.id
|
||||
symbol = await rq.get_user_symbol(tg_id=tg_id)
|
||||
additional_data = await rq.get_user_additional_settings(tg_id=tg_id)
|
||||
|
||||
if not additional_data:
|
||||
await rq.create_user(
|
||||
tg_id=tg_id, username=callback_query.from_user.username
|
||||
)
|
||||
await rq.create_user_additional_settings(tg_id=tg_id)
|
||||
await rq.create_user_risk_management(tg_id=tg_id)
|
||||
await rq.create_user_conditional_settings(tg_id=tg_id)
|
||||
await additional_settings(callback_query=callback_query, state=state)
|
||||
return
|
||||
|
||||
trade_mode_map = {
|
||||
"Merged_Single": "Односторонний режим",
|
||||
"Both_Sides": "Хеджирование",
|
||||
}
|
||||
margin_type_map = {
|
||||
"ISOLATED_MARGIN": "Изолированная",
|
||||
"REGULAR_MARGIN": "Кросс",
|
||||
}
|
||||
order_type_map = {"Market": "Рыночный", "Limit": "Лимитный"}
|
||||
|
||||
trade_mode = additional_data.trade_mode or ""
|
||||
margin_type = additional_data.margin_type or ""
|
||||
order_type = additional_data.order_type or ""
|
||||
|
||||
trade_mode_rus = trade_mode_map.get(trade_mode, trade_mode)
|
||||
margin_type_rus = margin_type_map.get(margin_type, margin_type)
|
||||
order_type_rus = order_type_map.get(order_type, "Условный")
|
||||
|
||||
def f(x):
|
||||
return safe_float(x)
|
||||
|
||||
leverage = f(additional_data.leverage)
|
||||
leverage_to_buy = f(additional_data.leverage_to_buy)
|
||||
leverage_to_sell = f(additional_data.leverage_to_sell)
|
||||
martingale = f(additional_data.martingale_factor)
|
||||
max_bets = additional_data.max_bets_in_series
|
||||
quantity = f(additional_data.order_quantity)
|
||||
limit_price = f(additional_data.limit_price)
|
||||
trigger_price = f(additional_data.trigger_price) or 0
|
||||
|
||||
tickers = await get_tickers(tg_id=tg_id, symbol=symbol)
|
||||
price_symbol = safe_float(tickers.get("lastPrice")) or 0
|
||||
bid = f(tickers.get("bid1Price")) or 0
|
||||
ask = f(tickers.get("ask1Price")) or 0
|
||||
|
||||
sym = get_base_currency(symbol)
|
||||
|
||||
if trade_mode == "Merged_Single":
|
||||
leverage_str = f"{leverage:.2f}x"
|
||||
else:
|
||||
if margin_type == "ISOLATED_MARGIN":
|
||||
leverage_str = f"{leverage_to_buy:.2f}x:{leverage_to_sell:.2f}x"
|
||||
else:
|
||||
leverage_str = f"{leverage:.2f}x"
|
||||
|
||||
conditional_order_type = additional_data.conditional_order_type or ""
|
||||
conditional_order_type_rus = (
|
||||
"Лимитный"
|
||||
if conditional_order_type == "Limit"
|
||||
else (
|
||||
"Рыночный"
|
||||
if conditional_order_type == "Market"
|
||||
else conditional_order_type
|
||||
)
|
||||
)
|
||||
|
||||
conditional_order_type_text = (
|
||||
f"- Тип условного ордера: {conditional_order_type_rus}\n"
|
||||
if order_type == "Conditional"
|
||||
else ""
|
||||
)
|
||||
|
||||
limit_price_text = ""
|
||||
trigger_price_text = ""
|
||||
|
||||
if order_type == "Limit":
|
||||
limit_price_text = f"- Цена лимитного ордера: {limit_price:.4f} USDT\n"
|
||||
elif order_type == "Conditional":
|
||||
if conditional_order_type == "Limit":
|
||||
limit_price_text = f"- Цена лимитного ордера: {limit_price:.4f} USDT\n"
|
||||
trigger_price_text = f"- Триггер цена: {trigger_price:.4f} USDT\n"
|
||||
|
||||
risk_management_data = await rq.get_user_risk_management(tg_id=tg_id)
|
||||
commission_fee = risk_management_data.commission_fee
|
||||
client = await get_bybit_client(tg_id=tg_id)
|
||||
fee_info = client.get_fee_rates(category="linear", symbol=symbol)
|
||||
|
||||
if commission_fee == "Yes_commission_fee":
|
||||
commission_fee_percent = safe_float(
|
||||
fee_info["result"]["list"][0]["takerFeeRate"]
|
||||
)
|
||||
|
||||
else:
|
||||
commission_fee_percent = 0.0
|
||||
|
||||
if order_type == "Conditional":
|
||||
if conditional_order_type == "Limit":
|
||||
entry_price = limit_price
|
||||
ask_price = limit_price
|
||||
bid_price = limit_price
|
||||
else:
|
||||
ask_price = trigger_price
|
||||
bid_price = trigger_price
|
||||
entry_price = trigger_price
|
||||
else:
|
||||
if order_type == "Limit":
|
||||
entry_price = limit_price
|
||||
ask_price = limit_price
|
||||
bid_price = limit_price
|
||||
else:
|
||||
entry_price = price_symbol
|
||||
ask_price = ask
|
||||
bid_price = bid
|
||||
|
||||
durability_buy = quantity * bid_price
|
||||
durability_sell = quantity * ask_price
|
||||
quantity_price = quantity * entry_price
|
||||
total_commission = quantity_price * commission_fee_percent
|
||||
total_budget = await calculate_total_budget(
|
||||
quantity=durability_buy,
|
||||
martingale_factor=martingale,
|
||||
max_steps=max_bets,
|
||||
commission_fee_percent=total_commission,
|
||||
)
|
||||
text = (
|
||||
f"Основные настройки:\n\n"
|
||||
f"- Режим позиции: {trade_mode_rus}\n"
|
||||
f"- Тип маржи: {margin_type_rus}\n"
|
||||
f"- Размер кредитного плеча: {leverage_str}\n"
|
||||
f"- Тип ордера: {order_type_rus}\n"
|
||||
f"- Количество ордера: {quantity} {sym}\n"
|
||||
f"- Коэффициент мартингейла: {martingale:.2f}\n"
|
||||
f"{conditional_order_type_text}"
|
||||
f"{trigger_price_text}"
|
||||
f"{limit_price_text}"
|
||||
f"- Максимальное кол-во ставок в серии: {max_bets}\n\n"
|
||||
f"- Стоимость: {durability_buy:.2f}/{durability_sell:.2f} USDT\n"
|
||||
f"- Рекомендуемый бюджет: {total_budget:.4f} USDT\n"
|
||||
)
|
||||
|
||||
keyboard = kbi.get_additional_settings_keyboard(
|
||||
current_order_type=order_type, conditional_order=conditional_order_type
|
||||
)
|
||||
await callback_query.message.edit_text(text=text, reply_markup=keyboard)
|
||||
logger.debug(
|
||||
"Command additional_settings processed successfully for user: %s", tg_id
|
||||
)
|
||||
except Exception as e:
|
||||
await callback_query.message.edit_text(
|
||||
text="Произошла ошибка. Пожалуйста, попробуйте ещё раз.",
|
||||
reply_markup=kbi.profile_bybit,
|
||||
)
|
||||
logger.error(
|
||||
"Error processing command additional_settings for user %s: %s",
|
||||
callback_query.from_user.id,
|
||||
e,
|
||||
)
|
||||
|
||||
|
||||
@router_settings.callback_query(F.data == "risk_management")
|
||||
async def risk_management(callback_query: CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Handler for the "risk_management" command.
|
||||
Sends a message with risk management options.
|
||||
"""
|
||||
try:
|
||||
await state.clear()
|
||||
risk_management_data = await rq.get_user_risk_management(
|
||||
tg_id=callback_query.from_user.id
|
||||
)
|
||||
if risk_management_data:
|
||||
take_profit_percent = risk_management_data.take_profit_percent or ""
|
||||
stop_loss_percent = risk_management_data.stop_loss_percent or ""
|
||||
max_risk_percent = risk_management_data.max_risk_percent or ""
|
||||
commission_fee = risk_management_data.commission_fee or ""
|
||||
commission_fee_rus = (
|
||||
"Да" if commission_fee == "Yes_commission_fee" else "Нет"
|
||||
)
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
text=f"Риск-менеджмент:\n\n"
|
||||
f"- Процент изменения цены для фиксации прибыли: {take_profit_percent}%\n"
|
||||
f"- Процент изменения цены для фиксации убытка: {stop_loss_percent}%\n\n"
|
||||
f"- Максимальный риск на сделку (в % от баланса): {max_risk_percent}%\n\n"
|
||||
f"- Комиссия биржи для расчета прибыли: {commission_fee_rus}\n\n",
|
||||
reply_markup=kbi.risk_management,
|
||||
)
|
||||
logger.debug(
|
||||
"Command main_settings processed successfully for user: %s",
|
||||
callback_query.from_user.id,
|
||||
)
|
||||
else:
|
||||
await rq.create_user(
|
||||
tg_id=callback_query.from_user.id,
|
||||
username=callback_query.from_user.username,
|
||||
)
|
||||
await rq.create_user_additional_settings(tg_id=callback_query.from_user.id)
|
||||
await rq.create_user_risk_management(tg_id=callback_query.from_user.id)
|
||||
await rq.create_user_conditional_settings(tg_id=callback_query.from_user.id)
|
||||
await risk_management(callback_query=callback_query, state=state)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error processing command main_settings for user %s: %s",
|
||||
callback_query.from_user.id,
|
||||
e,
|
||||
)
|
||||
|
||||
|
||||
@router_settings.callback_query(F.data == "conditional_settings")
|
||||
async def conditions(callback_query: CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Handler for the "conditions" command.
|
||||
Sends a message with trading conditions options.
|
||||
"""
|
||||
try:
|
||||
await state.clear()
|
||||
conditional_settings_data = await rq.get_user_conditional_settings(
|
||||
tg_id=callback_query.from_user.id
|
||||
)
|
||||
if conditional_settings_data:
|
||||
start_timer = conditional_settings_data.timer_start or 0
|
||||
stop_timer = conditional_settings_data.timer_end or 0
|
||||
await callback_query.message.edit_text(
|
||||
text="Условия торговли:\n\n"
|
||||
f"- Таймер для старта: {start_timer} мин.\n"
|
||||
f"- Таймер для остановки: {stop_timer} мин.\n",
|
||||
reply_markup=kbi.conditions,
|
||||
)
|
||||
logger.debug(
|
||||
"Command main_settings processed successfully for user: %s",
|
||||
callback_query.from_user.id,
|
||||
)
|
||||
else:
|
||||
await rq.create_user(
|
||||
tg_id=callback_query.from_user.id,
|
||||
username=callback_query.from_user.username,
|
||||
)
|
||||
await rq.create_user_additional_settings(tg_id=callback_query.from_user.id)
|
||||
await rq.create_user_risk_management(tg_id=callback_query.from_user.id)
|
||||
await rq.create_user_conditional_settings(tg_id=callback_query.from_user.id)
|
||||
await conditions(callback_query=callback_query, state=state)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error processing command main_settings for user %s: %s",
|
||||
callback_query.from_user.id,
|
||||
e,
|
||||
)
|
363
app/telegram/handlers/start_trading.py
Normal file
363
app/telegram/handlers/start_trading.py
Normal file
@@ -0,0 +1,363 @@
|
||||
import asyncio
|
||||
import logging.config
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery
|
||||
|
||||
import app.telegram.keyboards.inline as kbi
|
||||
import database.request as rq
|
||||
from app.bybit.get_functions.get_positions import get_active_positions_by_symbol
|
||||
from app.bybit.open_positions import start_trading_cycle
|
||||
from logger_helper.logger_helper import LOGGING_CONFIG
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("start_trading")
|
||||
|
||||
router_start_trading = Router(name="start_trading")
|
||||
|
||||
user_trade_tasks = {}
|
||||
|
||||
|
||||
@router_start_trading.callback_query(F.data == "start_trading")
|
||||
async def start_trading(callback_query: CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Handles the "start_trading" callback query.
|
||||
Clears the FSM state and sends a message to the user to select the trading mode.
|
||||
:param callback_query: Message
|
||||
:param state: FSMContext
|
||||
:return: None
|
||||
"""
|
||||
try:
|
||||
await state.clear()
|
||||
additional_data = await rq.get_user_additional_settings(
|
||||
tg_id=callback_query.from_user.id
|
||||
)
|
||||
trade_mode = additional_data.trade_mode
|
||||
symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id)
|
||||
deals = await get_active_positions_by_symbol(
|
||||
tg_id=callback_query.from_user.id, symbol=symbol
|
||||
)
|
||||
size = deals.get("size") or 0
|
||||
position_idx = deals.get("positionIdx")
|
||||
|
||||
if position_idx != 0 and int(size) > 0 and trade_mode == "Merged_Single":
|
||||
await callback_query.answer(
|
||||
text="У вас есть активная позиция в режиме хеджирования. "
|
||||
"Открытие сделки в одностороннем режиме невозможно.",
|
||||
)
|
||||
return
|
||||
|
||||
if position_idx == 0 and int(size) > 0 and trade_mode == "Both_Sides":
|
||||
await callback_query.answer(
|
||||
text="У вас есть активная позиция в одностороннем режиме. "
|
||||
"Открытие сделки в режиме хеджирования невозможно.",
|
||||
)
|
||||
return
|
||||
|
||||
if trade_mode == "Merged_Single":
|
||||
await callback_query.message.edit_text(
|
||||
text="Выберите режим торговли:\n\n"
|
||||
"Лонг - все сделки серии открываются на покупку.\n"
|
||||
"Шорт - все сделки серии открываются на продажу.\n"
|
||||
"Свитч - направление каждой сделки серии меняется по переменно.\n",
|
||||
reply_markup=kbi.merged_start_trading,
|
||||
)
|
||||
else: # trade_mode == "Both_Sides":
|
||||
await callback_query.message.edit_text(
|
||||
text="Выберите режим торговли:\n\n"
|
||||
"Лонг - все сделки открываются на покупку.\n"
|
||||
"Шорт - все сделки открываются на продажу.\n",
|
||||
reply_markup=kbi.both_start_trading,
|
||||
)
|
||||
logger.debug(
|
||||
"Command start_trading processed successfully for user: %s",
|
||||
callback_query.from_user.id,
|
||||
)
|
||||
except Exception as e:
|
||||
await callback_query.answer(text="Произошла ошибка при запуске торговли")
|
||||
logger.error(
|
||||
"Error processing command start_trading for user %s: %s",
|
||||
callback_query.from_user.id,
|
||||
e,
|
||||
)
|
||||
|
||||
|
||||
@router_start_trading.callback_query(lambda c: c.data == "long" or c.data == "short")
|
||||
async def start_trading_long(callback_query: CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Handles the "long" or "short" callback query.
|
||||
Clears the FSM state and starts the trading cycle.
|
||||
:param callback_query: Message
|
||||
:param state: FSMContext
|
||||
:return: None
|
||||
"""
|
||||
try:
|
||||
if callback_query.data == "long":
|
||||
side = "Buy"
|
||||
elif callback_query.data == "short":
|
||||
side = "Sell"
|
||||
else:
|
||||
await callback_query.answer(text="Произошла ошибка при запуске торговли")
|
||||
return
|
||||
|
||||
symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id)
|
||||
deals = await get_active_positions_by_symbol(
|
||||
tg_id=callback_query.from_user.id, symbol=symbol
|
||||
)
|
||||
size = deals.get("size") or 0
|
||||
position_idx = deals.get("positionIdx")
|
||||
|
||||
if position_idx == 0 and int(size) > 0:
|
||||
await callback_query.answer(
|
||||
text="Торговля уже запущена в одностороннем режиме для данного инструмента"
|
||||
)
|
||||
return
|
||||
|
||||
conditional_data = await rq.get_user_conditional_settings(
|
||||
tg_id=callback_query.from_user.id
|
||||
)
|
||||
timer_start = conditional_data.timer_start
|
||||
|
||||
if callback_query.from_user.id in user_trade_tasks:
|
||||
task = user_trade_tasks[callback_query.from_user.id]
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
del user_trade_tasks[callback_query.from_user.id]
|
||||
|
||||
async def delay_start():
|
||||
if timer_start > 0:
|
||||
await callback_query.message.edit_text(
|
||||
text=f"Торговля будет запущена с задержкой {timer_start} мин.",
|
||||
reply_markup=kbi.cancel_timer,
|
||||
)
|
||||
await asyncio.sleep(timer_start * 60)
|
||||
res = await start_trading_cycle(
|
||||
tg_id=callback_query.from_user.id,
|
||||
side=side,
|
||||
switch_side_mode=False,
|
||||
)
|
||||
|
||||
error_messages = {
|
||||
"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": "Сумма ордера не sufficientдля запуска торговли",
|
||||
"position idx not match position mode": "Торговля уже запущена в режиме хеджирования на продажу для данного инструмента",
|
||||
"Qty invalid": "Некорректное значение ордера для данного инструмента",
|
||||
}
|
||||
|
||||
if res == "OK":
|
||||
await rq.set_start_timer(
|
||||
tg_id=callback_query.from_user.id, timer_start=0
|
||||
)
|
||||
await rq.set_auto_trading(
|
||||
tg_id=callback_query.from_user.id, symbol=symbol, auto_trading=True
|
||||
)
|
||||
await callback_query.answer(text="Торговля запущена")
|
||||
await state.clear()
|
||||
else:
|
||||
# Получаем сообщение из таблицы, или дефолтный текст
|
||||
text = error_messages.get(res, "Произошла ошибка при запуске торговли")
|
||||
await callback_query.answer(text=text)
|
||||
|
||||
task = asyncio.create_task(delay_start())
|
||||
user_trade_tasks[callback_query.from_user.id] = task
|
||||
|
||||
except Exception as e:
|
||||
await callback_query.answer(text="Произошла ошибка при запуске торговли")
|
||||
logger.error(
|
||||
"Error processing command long for user %s: %s",
|
||||
callback_query.from_user.id,
|
||||
e,
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
logger.error("Cancelled timer for user %s", callback_query.from_user.id)
|
||||
|
||||
|
||||
@router_start_trading.callback_query(lambda c: c.data == "switch")
|
||||
async def start_trading_switch(
|
||||
callback_query: CallbackQuery, state: FSMContext
|
||||
) -> None:
|
||||
"""
|
||||
Handles the "switch" callback query.
|
||||
Clears the FSM state and sends a message to the user to select the switch side.
|
||||
:param callback_query: Message
|
||||
:param state: FSMContext
|
||||
:return: None
|
||||
"""
|
||||
try:
|
||||
await state.clear()
|
||||
await callback_query.message.edit_text(
|
||||
text="Выберите направление первой сделки серии:\n\n"
|
||||
"Лонг - открывается первая сделка на покупку.\n"
|
||||
"Шорт - открывается первая сделка на продажу.\n"
|
||||
"По направлению - сделка открывается в направлении последней сделки предыдущей серии.\n"
|
||||
"Противоположно - сделка открывается в противоположном направлении последней сделки предыдущей серии.\n",
|
||||
reply_markup=kbi.switch_side,
|
||||
)
|
||||
except Exception as e:
|
||||
await callback_query.answer(text="Произошла ошибка при запуске торговли")
|
||||
logger.error(
|
||||
"Error processing command start trading switch for user %s: %s",
|
||||
callback_query.from_user.id,
|
||||
e,
|
||||
)
|
||||
|
||||
|
||||
@router_start_trading.callback_query(
|
||||
lambda c: c.data
|
||||
in {"switch_long", "switch_short", "switch_direction", "switch_opposite"}
|
||||
)
|
||||
async def start_switch(callback_query: CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Starts the trading cycle with the selected side.
|
||||
:param callback_query:
|
||||
:param state:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
symbol = await rq.get_user_symbol(tg_id=callback_query.from_user.id)
|
||||
user_deals_data = await rq.get_user_deal_by_symbol(
|
||||
tg_id=callback_query.from_user.id, symbol=symbol
|
||||
)
|
||||
|
||||
get_side = "Buy"
|
||||
|
||||
if user_deals_data:
|
||||
get_side = user_deals_data.last_side or "Buy"
|
||||
|
||||
if callback_query.data == "switch_long":
|
||||
side = "Buy"
|
||||
elif callback_query.data == "switch_short":
|
||||
side = "Sell"
|
||||
elif callback_query.data == "switch_direction":
|
||||
side = get_side
|
||||
elif callback_query.data == "switch_opposite":
|
||||
if get_side == "Buy":
|
||||
side = "Sell"
|
||||
else:
|
||||
side = "Buy"
|
||||
else:
|
||||
await callback_query.answer(text="Произошла ошибка при запуске торговли")
|
||||
return
|
||||
|
||||
deals = await get_active_positions_by_symbol(
|
||||
tg_id=callback_query.from_user.id, symbol=symbol
|
||||
)
|
||||
size = deals.get("size") or 0
|
||||
position_idx = deals.get("positionIdx")
|
||||
|
||||
if position_idx == 1 and int(size) > 0 and side == "Buy":
|
||||
await callback_query.answer(
|
||||
text="Торговля уже запущена в режиме хеджирования на покупку для данного инструмента"
|
||||
)
|
||||
return
|
||||
|
||||
if position_idx == 2 and int(size) > 0 and side == "Sell":
|
||||
await callback_query.answer(
|
||||
text="Торговля уже запущена в режиме хеджирования на продажу для данного инструмента"
|
||||
)
|
||||
return
|
||||
|
||||
conditional_data = await rq.get_user_conditional_settings(
|
||||
tg_id=callback_query.from_user.id
|
||||
)
|
||||
timer_start = conditional_data.timer_start
|
||||
|
||||
if callback_query.from_user.id in user_trade_tasks:
|
||||
task = user_trade_tasks[callback_query.from_user.id]
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
del user_trade_tasks[callback_query.from_user.id]
|
||||
|
||||
async def delay_start():
|
||||
if timer_start > 0:
|
||||
await callback_query.message.edit_text(
|
||||
text=f"Торговля будет запущена с задержкой {timer_start} мин.",
|
||||
reply_markup=kbi.cancel_timer,
|
||||
)
|
||||
await asyncio.sleep(timer_start * 60)
|
||||
res = await start_trading_cycle(
|
||||
tg_id=callback_query.from_user.id,
|
||||
side=side,
|
||||
switch_side_mode=True,
|
||||
)
|
||||
|
||||
error_messages = {
|
||||
"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": "Сумма ордера не sufficientдля запуска торговли",
|
||||
"position idx not match position mode": "Торговля уже запущена в режиме хеджирования на продажу для данного инструмента",
|
||||
"Qty invalid": "Некорректное значение ордера для данного инструмента",
|
||||
}
|
||||
|
||||
if res == "OK":
|
||||
await rq.set_start_timer(
|
||||
tg_id=callback_query.from_user.id, timer_start=0
|
||||
)
|
||||
await rq.set_auto_trading(
|
||||
tg_id=callback_query.from_user.id, symbol=symbol, auto_trading=True
|
||||
)
|
||||
await callback_query.answer(text="Торговля запущена")
|
||||
await state.clear()
|
||||
else:
|
||||
text = error_messages.get(res, "Произошла ошибка при запуске торговли")
|
||||
await callback_query.answer(text=text)
|
||||
|
||||
task = asyncio.create_task(delay_start())
|
||||
user_trade_tasks[callback_query.from_user.id] = task
|
||||
except asyncio.CancelledError:
|
||||
logger.error("Cancelled timer for user %s", callback_query.from_user.id)
|
||||
except Exception as e:
|
||||
await callback_query.answer(text="Произошла ошибка при запуске торговли")
|
||||
logger.error(
|
||||
"Error processing command start switch for user %s: %s",
|
||||
callback_query.from_user.id,
|
||||
e,
|
||||
)
|
||||
|
||||
|
||||
@router_start_trading.callback_query(F.data == "cancel_timer")
|
||||
async def cancel_start_trading(
|
||||
callback_query: CallbackQuery, state: FSMContext
|
||||
) -> None:
|
||||
"""
|
||||
Handles the "cancel_timer" callback query.
|
||||
Clears the FSM state and sends a message to the user to cancel the start trading process.
|
||||
:param callback_query: Message
|
||||
:param state: FSMContext
|
||||
:return: None
|
||||
"""
|
||||
try:
|
||||
task = user_trade_tasks.get(callback_query.from_user.id)
|
||||
if task and not task.done():
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
user_trade_tasks.pop(callback_query.from_user.id, None)
|
||||
await callback_query.message.edit_text(
|
||||
"Таймер отменён.", reply_markup=kbi.profile_bybit
|
||||
)
|
||||
else:
|
||||
await callback_query.message.edit_text(
|
||||
"Таймер не запущен.", reply_markup=kbi.profile_bybit
|
||||
)
|
||||
except Exception as e:
|
||||
await callback_query.answer("Произошла ошибка при отмене запуска торговли")
|
||||
logger.error(
|
||||
"Error processing command cancel_timer for user %s: %s",
|
||||
callback_query.from_user.id,
|
||||
e,
|
||||
)
|
||||
finally:
|
||||
await state.clear()
|
73
app/telegram/handlers/stop_trading.py
Normal file
73
app/telegram/handlers/stop_trading.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import asyncio
|
||||
import logging.config
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery
|
||||
|
||||
import app.telegram.keyboards.inline as kbi
|
||||
import database.request as rq
|
||||
from app.bybit.close_positions import cancel_order, close_position
|
||||
from logger_helper.logger_helper import LOGGING_CONFIG
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("stop_trading")
|
||||
|
||||
router_stop_trading = Router(name="stop_trading")
|
||||
|
||||
user_trade_tasks = {}
|
||||
|
||||
|
||||
@router_stop_trading.callback_query(F.data == "stop_trading")
|
||||
async def stop_trading(callback_query: CallbackQuery, state: FSMContext):
|
||||
try:
|
||||
await state.clear()
|
||||
|
||||
if callback_query.from_user.id in user_trade_tasks:
|
||||
task = user_trade_tasks[callback_query.from_user.id]
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
del user_trade_tasks[callback_query.from_user.id]
|
||||
|
||||
conditional_data = await rq.get_user_conditional_settings(
|
||||
tg_id=callback_query.from_user.id
|
||||
)
|
||||
timer_end = conditional_data.timer_end
|
||||
symbols = await rq.get_all_symbols(tg_id=callback_query.from_user.id)
|
||||
|
||||
async def delay_start():
|
||||
if timer_end > 0:
|
||||
await callback_query.message.edit_text(
|
||||
text=f"Торговля будет остановлена с задержкой {timer_end} мин.",
|
||||
reply_markup=kbi.cancel_timer,
|
||||
)
|
||||
await asyncio.sleep(timer_end * 60)
|
||||
|
||||
for symbol in symbols:
|
||||
auto_trading_data = await rq.get_user_auto_trading(
|
||||
tg_id=callback_query.from_user.id, symbol=symbol
|
||||
)
|
||||
if auto_trading_data is not None and auto_trading_data.auto_trading:
|
||||
await close_position(tg_id=callback_query.from_user.id, symbol=symbol)
|
||||
await cancel_order(tg_id=callback_query.from_user.id, symbol=symbol)
|
||||
await rq.set_auto_trading(
|
||||
tg_id=callback_query.from_user.id, symbol=symbol, auto_trading=False
|
||||
)
|
||||
|
||||
await callback_query.answer(text="Торговля остановлена")
|
||||
await rq.set_stop_timer(tg_id=callback_query.from_user.id, timer_end=0)
|
||||
|
||||
task = asyncio.create_task(delay_start())
|
||||
user_trade_tasks[callback_query.from_user.id] = task
|
||||
|
||||
logger.debug(
|
||||
"Command stop_trading processed successfully for user: %s",
|
||||
callback_query.from_user.id,
|
||||
)
|
||||
except Exception as e:
|
||||
await callback_query.answer(text="Произошла ошибка при остановке торговли")
|
||||
logger.error(
|
||||
"Error processing command stop_trading for user %s: %s",
|
||||
callback_query.from_user.id,
|
||||
e,
|
||||
)
|
161
app/telegram/handlers/tp_sl_handlers.py
Normal file
161
app/telegram/handlers/tp_sl_handlers.py
Normal file
@@ -0,0 +1,161 @@
|
||||
import logging.config
|
||||
|
||||
from aiogram import Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
|
||||
import app.telegram.keyboards.inline as kbi
|
||||
from app.bybit.set_functions.set_tp_sl import set_tp_sl_for_position
|
||||
from app.helper_functions import is_number
|
||||
from app.telegram.states.states import SetTradingStopState
|
||||
from logger_helper.logger_helper import LOGGING_CONFIG
|
||||
|
||||
logging.config.dictConfig(LOGGING_CONFIG)
|
||||
logger = logging.getLogger("tp_sl_handlers")
|
||||
|
||||
router_tp_sl_handlers = Router(name="tp_sl_handlers")
|
||||
|
||||
|
||||
@router_tp_sl_handlers.callback_query(lambda c: c.data.startswith("pos_tp_sl_"))
|
||||
async def set_tp_sl_handler(callback_query: CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Handles the 'pos_tp_sl' callback query.
|
||||
|
||||
Clears the current FSM state, sets the state to 'take_profit', and prompts the user to enter the take-profit.
|
||||
|
||||
Args:
|
||||
callback_query (CallbackQuery): Incoming callback query from Telegram inline keyboard.
|
||||
state (FSMContext): Finite State Machine context for the current user session.
|
||||
"""
|
||||
try:
|
||||
await state.clear()
|
||||
symbol = callback_query.data.split("_", 3)[3]
|
||||
await state.set_state(SetTradingStopState.take_profit_state)
|
||||
await state.update_data(symbol=symbol)
|
||||
msg = await callback_query.message.answer(
|
||||
text="Введите тейк-профит:", reply_markup=kbi.cancel
|
||||
)
|
||||
await state.update_data(prompt_message_id=msg.message_id)
|
||||
except Exception as e:
|
||||
logger.error("Error in set_tp_sl_handler: %s", e)
|
||||
await callback_query.answer(text="Произошла ошибка, попробуйте позже")
|
||||
|
||||
|
||||
@router_tp_sl_handlers.message(SetTradingStopState.take_profit_state)
|
||||
async def set_take_profit_handler(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Handles the 'take_profit' state.
|
||||
|
||||
Clears the current FSM state, sets the state to 'stop_loss', and prompts the user to enter the stop-loss.
|
||||
|
||||
Args:
|
||||
message (Message): Incoming message from Telegram.
|
||||
state (FSMContext): Finite State Machine context for the current user session.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
try:
|
||||
try:
|
||||
data = await state.get_data()
|
||||
if "prompt_message_id" in data:
|
||||
prompt_message_id = data["prompt_message_id"]
|
||||
await message.bot.delete_message(
|
||||
chat_id=message.chat.id, message_id=prompt_message_id
|
||||
)
|
||||
await message.delete()
|
||||
except Exception as e:
|
||||
if "message to delete not found" in str(e).lower():
|
||||
pass # Ignore this error
|
||||
else:
|
||||
raise e
|
||||
|
||||
take_profit = message.text
|
||||
|
||||
if not is_number(take_profit):
|
||||
await message.answer(
|
||||
"Ошибка: введите валидное число.",
|
||||
reply_markup=kbi.profile_bybit,
|
||||
)
|
||||
logger.debug(
|
||||
"User %s input invalid (not an valid number): %s",
|
||||
message.from_user.id,
|
||||
take_profit,
|
||||
)
|
||||
return
|
||||
|
||||
await state.update_data(take_profit=take_profit)
|
||||
await state.set_state(SetTradingStopState.stop_loss_state)
|
||||
msg = await message.answer(text="Введите стоп-лосс:", reply_markup=kbi.cancel)
|
||||
await state.update_data(prompt_message_id=msg.message_id)
|
||||
except Exception as e:
|
||||
logger.error("Error in set_take_profit_handler: %s", e)
|
||||
await message.answer(
|
||||
text="Произошла ошибка, попробуйте позже", reply_markup=kbi.profile_bybit
|
||||
)
|
||||
|
||||
|
||||
@router_tp_sl_handlers.message(SetTradingStopState.stop_loss_state)
|
||||
async def set_stop_loss_handler(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Handles the 'stop_loss' state.
|
||||
|
||||
Clears the current FSM state, sets the state to 'take_profit', and prompts the user to enter the take-profit.
|
||||
|
||||
Args:
|
||||
message (Message): Incoming message from Telegram.
|
||||
state (FSMContext): Finite State Machine context for the current user session.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
try:
|
||||
try:
|
||||
data = await state.get_data()
|
||||
if "prompt_message_id" in data:
|
||||
prompt_message_id = data["prompt_message_id"]
|
||||
await message.bot.delete_message(
|
||||
chat_id=message.chat.id, message_id=prompt_message_id
|
||||
)
|
||||
await message.delete()
|
||||
except Exception as e:
|
||||
if "message to delete not found" in str(e).lower():
|
||||
pass # Ignore this error
|
||||
else:
|
||||
raise e
|
||||
|
||||
stop_loss = message.text
|
||||
|
||||
if not is_number(stop_loss):
|
||||
await message.answer(
|
||||
"Ошибка: введите валидное число.",
|
||||
reply_markup=kbi.profile_bybit,
|
||||
)
|
||||
logger.debug(
|
||||
"User %s input invalid (not an valid number): %s",
|
||||
message.from_user.id,
|
||||
stop_loss,
|
||||
)
|
||||
return
|
||||
|
||||
await state.update_data(stop_loss=stop_loss)
|
||||
data = await state.get_data()
|
||||
symbol = data["symbol"]
|
||||
take_profit = data["take_profit"]
|
||||
res = await set_tp_sl_for_position(
|
||||
tg_id=message.from_user.id,
|
||||
symbol=symbol,
|
||||
take_profit_price=float(take_profit),
|
||||
stop_loss_price=float(stop_loss),
|
||||
)
|
||||
|
||||
if res:
|
||||
await message.answer(text="Тейк-профит и стоп-лосс установлены.")
|
||||
else:
|
||||
await message.answer(text="Тейк-профит и стоп-лосс не установлены.")
|
||||
await state.clear()
|
||||
except Exception as e:
|
||||
await message.answer(
|
||||
text="Произошла ошибка, попробуйте позже", reply_markup=kbi.profile_bybit
|
||||
)
|
||||
logger.error("Error in set_stop_loss_handler: %s", e)
|
449
app/telegram/keyboards/inline.py
Normal file
449
app/telegram/keyboards/inline.py
Normal file
@@ -0,0 +1,449 @@
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
connect_the_platform = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="Подключить платформу", callback_data="connect_platform"
|
||||
)
|
||||
]
|
||||
]
|
||||
)
|
||||
|
||||
add_bybit_api = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text="Добавить API", callback_data="add_bybit_api")]
|
||||
]
|
||||
)
|
||||
|
||||
profile_bybit = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text="На главную", callback_data="profile_bybit")]
|
||||
]
|
||||
)
|
||||
|
||||
cancel = InlineKeyboardMarkup(
|
||||
inline_keyboard=[[InlineKeyboardButton(text="Отменить", callback_data="cancel")]]
|
||||
)
|
||||
|
||||
main_menu = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text="Настройки", callback_data="main_settings")],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="Сменить торговую пару", callback_data="change_symbol"
|
||||
)
|
||||
],
|
||||
[InlineKeyboardButton(text="Мои сделки", callback_data="my_deals")],
|
||||
[InlineKeyboardButton(text="Начать торговлю", callback_data="start_trading")],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="Остановить торговлю", callback_data="stop_trading"
|
||||
)
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
# MAIN SETTINGS
|
||||
main_settings = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="Основные настройки", callback_data="additional_settings"
|
||||
),
|
||||
InlineKeyboardButton(
|
||||
text="Риск-менеджмент", callback_data="risk_management"
|
||||
),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="Условия запуска", callback_data="conditional_settings"
|
||||
)
|
||||
],
|
||||
[InlineKeyboardButton(text="Назад", callback_data="profile_bybit")],
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# additional_settings
|
||||
def get_additional_settings_keyboard(
|
||||
current_order_type: str, conditional_order: str
|
||||
) -> InlineKeyboardMarkup:
|
||||
"""
|
||||
Create keyboard for additional settings
|
||||
:param current_order_type: Market, Limit or Conditional
|
||||
:param conditional_order: Market or Limit
|
||||
:return: InlineKeyboardMarkup
|
||||
"""
|
||||
buttons = [
|
||||
[
|
||||
InlineKeyboardButton(text="Режим позиции", callback_data="trade_mode"),
|
||||
InlineKeyboardButton(text="Тип маржи", callback_data="margin_type"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="Размер кредитного плеча", callback_data="leverage"
|
||||
),
|
||||
InlineKeyboardButton(text="Тип ордера", callback_data="order_type"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="Количество ордера", callback_data="order_quantity"
|
||||
),
|
||||
InlineKeyboardButton(
|
||||
text="Коэффициент мартингейла", callback_data="martingale_factor"
|
||||
),
|
||||
],
|
||||
]
|
||||
|
||||
if current_order_type == "Conditional":
|
||||
buttons.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="Тип условного ордера", callback_data="conditional_order_type"
|
||||
)
|
||||
]
|
||||
)
|
||||
buttons.append(
|
||||
[InlineKeyboardButton(text="Триггер цена", callback_data="trigger_price")]
|
||||
)
|
||||
if conditional_order == "Limit":
|
||||
buttons.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="Цена лимитного ордера", callback_data="limit_price"
|
||||
)
|
||||
]
|
||||
)
|
||||
elif current_order_type == "Limit":
|
||||
buttons.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="Цена лимитного ордера", callback_data="limit_price"
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
buttons.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="Максимальное кол-во ставок в серии",
|
||||
callback_data="max_bets_in_series",
|
||||
)
|
||||
]
|
||||
)
|
||||
buttons.append(
|
||||
[
|
||||
InlineKeyboardButton(text="Назад", callback_data="main_settings"),
|
||||
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
|
||||
]
|
||||
)
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
|
||||
order_type = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="Рыночный", callback_data="Market"),
|
||||
InlineKeyboardButton(text="Лимитный", callback_data="Limit"),
|
||||
],
|
||||
[InlineKeyboardButton(text="Условный", callback_data="Conditional")],
|
||||
[
|
||||
InlineKeyboardButton(text="Назад", callback_data="additional_settings"),
|
||||
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
conditional_order_type = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="Рыночный", callback_data="set_market"),
|
||||
InlineKeyboardButton(text="Лимитный", callback_data="set_limit"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="Назад", callback_data="additional_settings"),
|
||||
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
trade_mode = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="Односторонний режим", callback_data="Merged_Single"
|
||||
),
|
||||
InlineKeyboardButton(text="Хеджирование", callback_data="Both_Sides"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="Назад", callback_data="additional_settings"),
|
||||
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
margin_type = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="Изолированная", callback_data="ISOLATED_MARGIN"),
|
||||
InlineKeyboardButton(text="Кросс", callback_data="REGULAR_MARGIN"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="Назад", callback_data="additional_settings"),
|
||||
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
back_to_additional_settings = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="Назад", callback_data="additional_settings"),
|
||||
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
change_limit_price = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="Установить цену", callback_data="set_limit_price"
|
||||
),
|
||||
InlineKeyboardButton(text="Последняя цена", callback_data="last_price"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="Назад", callback_data="additional_settings"),
|
||||
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
back_to_change_limit_price = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="Назад", callback_data="limit_price"),
|
||||
InlineKeyboardButton(
|
||||
text="Основные настройки", callback_data="additional_settings"
|
||||
),
|
||||
],
|
||||
[InlineKeyboardButton(text="На главную", callback_data="profile_bybit")],
|
||||
]
|
||||
)
|
||||
|
||||
# risk_management
|
||||
risk_management = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="Изм. цены прибыли", callback_data="take_profit_percent"
|
||||
),
|
||||
InlineKeyboardButton(
|
||||
text="Изм. цены убытка", callback_data="stop_loss_percent"
|
||||
),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="Максимальный риск", callback_data="max_risk_percent"
|
||||
)
|
||||
],
|
||||
[InlineKeyboardButton(text="Комиссия биржи", callback_data="commission_fee")],
|
||||
[
|
||||
InlineKeyboardButton(text="Назад", callback_data="main_settings"),
|
||||
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
back_to_risk_management = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="Назад", callback_data="risk_management"),
|
||||
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
commission_fee = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="Да", callback_data="Yes_commission_fee"),
|
||||
InlineKeyboardButton(text="Нет", callback_data="No_commission_fee"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="Назад", callback_data="risk_management"),
|
||||
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
# conditions
|
||||
conditions = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="Таймер для старта", callback_data="start_timer"),
|
||||
InlineKeyboardButton(
|
||||
text="Таймер для остановки", callback_data="stop_timer"
|
||||
),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="Назад", callback_data="main_settings"),
|
||||
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
back_to_conditions = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="Назад", callback_data="conditional_settings"),
|
||||
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
# SYMBOL
|
||||
symbol = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="Назад", callback_data="profile_bybit"),
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
# POSITION
|
||||
|
||||
change_position = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="Позиции", callback_data="change_position"),
|
||||
InlineKeyboardButton(text="Открытые ордера", callback_data="open_orders"),
|
||||
],
|
||||
[InlineKeyboardButton(text="Назад", callback_data="profile_bybit")],
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def create_active_positions_keyboard(symbols):
|
||||
builder = InlineKeyboardBuilder()
|
||||
for sym in symbols:
|
||||
builder.button(text=f"{sym}", callback_data=f"get_position_{sym}")
|
||||
builder.button(text="Назад", callback_data="my_deals")
|
||||
builder.button(text="На главную", callback_data="profile_bybit")
|
||||
builder.adjust(2)
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def make_close_position_keyboard(symbol_pos: str):
|
||||
return InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="Закрыть позицию", callback_data=f"close_position_{symbol_pos}"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="Установить TP/SL", callback_data=f"pos_tp_sl_{symbol_pos}"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="Назад", callback_data="my_deals"),
|
||||
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def create_active_orders_keyboard(orders):
|
||||
builder = InlineKeyboardBuilder()
|
||||
for order in orders:
|
||||
builder.button(text=f"{order}", callback_data=f"get_order_{order}")
|
||||
builder.button(text="Закрыть все ордера", callback_data="cancel_all_orders")
|
||||
builder.button(text="Назад", callback_data="my_deals")
|
||||
builder.button(text="На главную", callback_data="profile_bybit")
|
||||
builder.adjust(2)
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def make_close_orders_keyboard(symbol_order: str):
|
||||
return InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="Закрыть ордер", callback_data=f"close_order_{symbol_order}"
|
||||
)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="Назад", callback_data="my_deals"),
|
||||
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# START TRADING
|
||||
|
||||
merged_start_trading = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="Лонг", callback_data="long"),
|
||||
InlineKeyboardButton(text="Шорт", callback_data="short"),
|
||||
],
|
||||
[InlineKeyboardButton(text="Свитч", callback_data="switch")],
|
||||
[InlineKeyboardButton(text="Назад", callback_data="profile_bybit")],
|
||||
]
|
||||
)
|
||||
|
||||
both_start_trading = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="Лонг", callback_data="long"),
|
||||
InlineKeyboardButton(text="Шорт", callback_data="short"),
|
||||
],
|
||||
[InlineKeyboardButton(text="Назад", callback_data="profile_bybit")],
|
||||
]
|
||||
)
|
||||
|
||||
switch_side = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="Лонг", callback_data="switch_long"),
|
||||
InlineKeyboardButton(text="Шорт", callback_data="switch_short"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="По направлению", callback_data="switch_direction"
|
||||
),
|
||||
InlineKeyboardButton(
|
||||
text="Противоположно", callback_data="switch_opposite"
|
||||
),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="Назад", callback_data="start_trading"),
|
||||
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
back_to_start_trading = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="Назад", callback_data="start_trading"),
|
||||
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
cancel_timer = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text="Отменить таймер", callback_data="cancel_timer")],
|
||||
[
|
||||
InlineKeyboardButton(text="Назад", callback_data="conditions"),
|
||||
InlineKeyboardButton(text="На главную", callback_data="profile_bybit"),
|
||||
],
|
||||
]
|
||||
)
|
8
app/telegram/keyboards/reply.py
Normal file
8
app/telegram/keyboards/reply.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from aiogram.types import KeyboardButton, ReplyKeyboardMarkup
|
||||
|
||||
profile = ReplyKeyboardMarkup(
|
||||
keyboard=[[KeyboardButton(text="Панель Bybit"), KeyboardButton(text="Профиль")]],
|
||||
resize_keyboard=True,
|
||||
one_time_keyboard=True,
|
||||
input_field_placeholder="Выберите пункт меню...",
|
||||
)
|
0
app/telegram/states/__init__.py
Normal file
0
app/telegram/states/__init__.py
Normal file
51
app/telegram/states/states.py
Normal file
51
app/telegram/states/states.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
|
||||
|
||||
class AddBybitApiState(StatesGroup):
|
||||
"""States for adding Bybit API keys."""
|
||||
|
||||
api_key_state = State()
|
||||
api_secret_state = State()
|
||||
|
||||
|
||||
class AdditionalSettingsState(StatesGroup):
|
||||
"""States for additional settings."""
|
||||
|
||||
leverage_state = State()
|
||||
leverage_to_buy_state = State()
|
||||
leverage_to_sell_state = State()
|
||||
quantity_state = State()
|
||||
martingale_factor_state = State()
|
||||
max_bets_in_series_state = State()
|
||||
limit_price_state = State()
|
||||
trigger_price_state = State()
|
||||
|
||||
|
||||
class RiskManagementState(StatesGroup):
|
||||
"""States for risk management."""
|
||||
|
||||
take_profit_percent_state = State()
|
||||
stop_loss_percent_state = State()
|
||||
max_risk_percent_state = State()
|
||||
commission_fee_state = State()
|
||||
|
||||
|
||||
class ConditionalSettingsState(StatesGroup):
|
||||
"""States for conditional settings."""
|
||||
|
||||
start_timer_state = State()
|
||||
stop_timer_state = State()
|
||||
|
||||
|
||||
class ChangingTheSymbolState(StatesGroup):
|
||||
"""States for changing the symbol."""
|
||||
|
||||
symbol_state = State()
|
||||
|
||||
|
||||
class SetTradingStopState(StatesGroup):
|
||||
"""States for setting a trading stop."""
|
||||
|
||||
symbol_state = State()
|
||||
take_profit_state = State()
|
||||
stop_loss_state = State()
|
Reference in New Issue
Block a user