# app/src/telegram/handlers/debug.py
from __future__ import annotations
import math
from aiogram import F, Router
from aiogram.types import Message
from src.core.config import load_settings
from src.trading.debug.execution import DebugExecutionEngine
from src.trading.debug.service import DebugTradeService
from src.trading.debug.state import DebugTradeState
from src.trading.execution.models import ExecutionDecision
router = Router(name="debug")
def _debug_enabled() -> bool:
return load_settings().debug_enabled
def _debug_help_text() -> str:
return (
"🧪 Debug commands\n\n"
"Isolated Debug Runtime:\n"
"/debug_auto reset\n"
"/debug_auto off\n"
"/debug_auto hold 335\n"
"/debug_auto buy 12 0.74\n"
"/debug_auto buy_ready 0.88\n"
"/debug_auto sell 9 0.71\n"
"/debug_auto sell_ready 0.91\n"
"/debug_auto long\n"
"/debug_auto short\n"
"/debug_auto state\n\n"
"Isolated Debug Execution:\n"
"/debug_exec buy — открыть DEBUG LONG\n"
"/debug_exec sell — открыть DEBUG SHORT\n"
"/debug_exec flip — перевернуть DEBUG позицию\n"
"/debug_exec close — закрыть DEBUG позицию\n"
"/debug_exec process — один цикл DEBUG execution\n"
"/debug_exec update — обновить DEBUG PnL\n"
"/debug_exec state — состояние DEBUG runtime\n\n"
"Legacy aliases:\n"
"/debug_signal BUY 0.95 3\n"
"/debug_signal SELL 0.70 2\n"
"/debug_signal HOLD 0.00 1\n"
"/debug_ready\n"
"/debug_state\n\n"
"⚠️ Все команды работают в изолированном [DEBUG] runtime "
"и не меняют обычную автоторговлю."
)
@router.message(F.text == "/debug_help")
async def debug_help(message: Message) -> None:
if not _debug_enabled():
await message.answer("Debug mode выключен.")
return
await message.answer(_debug_help_text())
@router.message(F.text.startswith("/debug_auto"))
async def debug_auto(message: Message) -> None:
if not _debug_enabled():
await message.answer("Debug mode выключен.")
return
parts = (message.text or "").split()
command = parts[1].lower() if len(parts) > 1 else "help"
service = DebugTradeService()
if command in {"help", "-h", "--help"}:
await message.answer(_debug_help_text())
return
if command == "reset":
state = service.reset()
await message.answer(
"✅ [DEBUG] Runtime reset\n\n"
f"{_debug_state_text(state)}"
)
return
if command == "off":
state = service.stop()
await message.answer(
"✅ [DEBUG] Runtime stopped\n\n"
f"{_debug_state_text(state)}"
)
return
if command == "state":
state = service.get_state()
service.update_market()
await message.answer(_debug_state_text(state))
return
if command == "hold":
seconds = _parse_int(parts, index=2, default=335)
state = service.set_signal_duration(
signal="HOLD",
seconds=seconds,
confidence=0.0,
force_ready=False,
)
await message.answer(
f"✅ [DEBUG] HOLD {seconds}s\n\n"
f"{_debug_state_text(state)}"
)
return
if command == "buy":
seconds = _parse_int(parts, index=2, default=12)
confidence = _parse_float(parts, index=3, default=0.74)
state = service.set_signal_duration(
signal="BUY",
seconds=seconds,
confidence=confidence,
force_ready=False,
)
await message.answer(
f"✅ [DEBUG] BUY {seconds}s confidence={confidence:.2f}\n\n"
f"{_debug_state_text(state)}"
)
return
if command == "buy_ready":
confidence = _parse_float(parts, index=2, default=0.88)
state = service.set_signal_duration(
signal="BUY",
seconds=15,
confidence=confidence,
force_ready=True,
)
await message.answer(
f"✅ [DEBUG] BUY READY confidence={confidence:.2f}\n\n"
f"{_debug_state_text(state)}"
)
return
if command == "sell":
seconds = _parse_int(parts, index=2, default=9)
confidence = _parse_float(parts, index=3, default=0.71)
state = service.set_signal_duration(
signal="SELL",
seconds=seconds,
confidence=confidence,
force_ready=False,
)
await message.answer(
f"✅ [DEBUG] SELL {seconds}s confidence={confidence:.2f}\n\n"
f"{_debug_state_text(state)}"
)
return
if command == "sell_ready":
confidence = _parse_float(parts, index=2, default=0.91)
state = service.set_signal_duration(
signal="SELL",
seconds=15,
confidence=confidence,
force_ready=True,
)
await message.answer(
f"✅ [DEBUG] SELL READY confidence={confidence:.2f}\n\n"
f"{_debug_state_text(state)}"
)
return
if command == "long":
state, result = service.open_long()
await message.answer(_execution_result_text("OPEN LONG", state, result))
return
if command == "short":
state, result = service.open_short()
await message.answer(_execution_result_text("OPEN SHORT", state, result))
return
await message.answer(f"⛔️ Неизвестная команда: {command}\n\n{_debug_help_text()}")
@router.message(F.text.startswith("/debug_exec"))
async def debug_exec(message: Message) -> None:
if not _debug_enabled():
await message.answer("Debug mode выключен.")
return
parts = (message.text or "").split()
command = parts[1].lower() if len(parts) > 1 else "help"
service = DebugTradeService()
if command in {"help", "-h", "--help"}:
await message.answer(_debug_help_text())
return
if command == "state":
state = service.get_state()
service.update_market()
await message.answer(_debug_state_text(state))
return
if command == "buy":
state, result = service.open_long()
await message.answer(_execution_result_text("EXEC BUY / LONG", state, result))
return
if command == "sell":
state, result = service.open_short()
await message.answer(_execution_result_text("EXEC SELL / SHORT", state, result))
return
if command == "flip":
state, result = service.flip()
await message.answer(_execution_result_text("EXEC AUTO FLIP", state, result))
return
if command == "close":
state, result = service.close(reason="DEBUG_CLOSE")
await message.answer(_execution_result_text("EXEC CLOSE", state, result))
return
if command == "process":
state, result = service.process()
await message.answer(_execution_result_text("EXEC PROCESS", state, result))
return
if command == "update":
state = service.update_market()
await message.answer(
"✅ [DEBUG] Market update\n\n"
f"{_debug_state_text(state)}"
)
return
await message.answer(f"⛔️ Неизвестная команда: {command}\n\n{_debug_help_text()}")
@router.message(F.text.startswith("/debug_live"))
async def debug_live(message: Message) -> None:
if not _debug_enabled():
await message.answer("Debug mode выключен.")
return
await message.answer(
"⚠️ /debug_live отключён в рамках развязки debug runtime.\n\n"
"Теперь debug больше не вмешивается в обычную автоторговлю.\n"
"Используйте:\n\n"
"/debug_exec buy\n"
"/debug_exec sell\n"
"/debug_exec flip\n"
"/debug_exec close\n\n"
"Live-мониторинг для изолированного debug будет добавлен в следующем пакете "
"через отдельный DebugTradeRunner и отдельный Debug Auto экран."
)
@router.message(F.text.startswith("/debug_signal"))
async def debug_signal(message: Message) -> None:
if not _debug_enabled():
await message.answer("Debug mode выключен.")
return
signal, confidence, repeat_count, error = _parse_debug_signal_args(message.text)
if error is not None:
await message.answer(f"⛔️ {error}\n\n{_debug_help_text()}")
return
service = DebugTradeService()
state = service.set_signal(
signal=signal,
confidence=confidence,
repeat_count=repeat_count,
reason=f"[DEBUG] LEGACY FORCE {signal} {confidence:.2f} ×{repeat_count}",
force_ready=signal in {"BUY", "SELL"} and repeat_count >= 2,
)
await message.answer(
"✅ [DEBUG] Legacy signal forced\n\n"
f"{_debug_state_text(state)}"
)
@router.message(F.text == "/debug_ready")
async def debug_ready(message: Message) -> None:
if not _debug_enabled():
await message.answer("Debug mode выключен.")
return
service = DebugTradeService()
state = service.set_signal_duration(
signal="BUY",
seconds=15,
confidence=0.95,
force_ready=True,
)
await message.answer(
"✅ [DEBUG] Legacy READY created\n\n"
f"{_debug_state_text(state)}"
)
@router.message(F.text == "/debug_state")
async def debug_state(message: Message) -> None:
if not _debug_enabled():
await message.answer("Debug mode выключен.")
return
service = DebugTradeService()
state = service.get_state()
service.update_market()
await message.answer(_debug_state_text(state))
def _debug_state_text(state: DebugTradeState) -> str:
position = state.position
duration = _signal_duration_text(state)
pnl = position.unrealized_pnl_usd
return (
"[DEBUG] Auto State\n\n"
f"Status: {state.status}\n"
f"Symbol: {state.symbol}\n"
f"Strategy: {state.strategy}\n"
f"Risk: {state.risk_percent}\n"
f"Leverage: {state.leverage}\n"
f"Allocated: $ {_format_money_compact(state.allocated_balance_usd)}\n"
f"Realized PnL: {_format_signed_usd(state.realized_pnl_usd)}\n\n"
f"Signal\n"
f"Signal: {_signal_icon(state.last_signal)} {state.last_signal}\n"
f"Duration: {duration}\n"
f"Repeats: {state.last_signal_repeat_count}\n"
f"Confidence: {state.last_signal_confidence:.2f}\n"
f"Decision: {state.decision_status}\n"
f"Ready: {state.is_signal_ready}\n"
f"Reason: {state.last_signal_reason or '—'}\n\n"
f"Risk\n"
f"SL: {_format_percent(state.stop_loss_percent)}\n"
f"TP: {_format_percent(state.take_profit_percent)}\n"
f"ML: {_format_usd_or_off(state.max_loss_usd)}\n"
f"Max Reserved: {_format_percent(state.max_reserved_balance_percent)}\n"
f"Block: {state.execution_block_reason or '—'}\n"
f"Adjustment: {state.execution_size_adjustment_reason or '—'}\n\n"
f"Position\n"
f"Side: {position.side}\n"
f"Entry: {_format_usd_or_dash(position.entry_price)}\n"
f"Size: {_format_crypto_size(position.size)}\n"
f"Leverage: {_format_leverage(position.leverage)}\n"
f"PnL: {_format_signed_usd(pnl)}\n"
f"Opened: {position.opened_at or '—'}\n"
f"Updated: {position.updated_at or '—'}\n\n"
"Runtime: isolated [DEBUG]"
)
def _execution_result_text(
title: str,
state: DebugTradeState,
result: ExecutionDecision,
) -> str:
return (
f"✅ [DEBUG] {title}\n\n"
f"Action: {result.action}\n"
f"Can execute: {result.can_execute}\n"
f"Reason: {result.reason}\n\n"
f"{_debug_state_text(state)}"
)
def _parse_debug_signal_args(raw_text: str | None) -> tuple[str, float, int, str | None]:
parts = (raw_text or "").split()
signal = parts[1].upper() if len(parts) > 1 else "BUY"
if signal not in {"BUY", "SELL", "HOLD"}:
return "BUY", 0.9, 2, "SIGNAL должен быть BUY, SELL или HOLD."
try:
confidence = float(parts[2]) if len(parts) > 2 else 0.9
except ValueError:
return "BUY", 0.9, 2, "CONFIDENCE должен быть числом от 0.00 до 1.00."
if confidence < 0 or confidence > 1:
return "BUY", 0.9, 2, "CONFIDENCE должен быть от 0.00 до 1.00."
try:
repeat_count = int(parts[3]) if len(parts) > 3 else 2
except ValueError:
return "BUY", 0.9, 2, "REPEATS должен быть целым числом."
if repeat_count < 1:
return "BUY", 0.9, 2, "REPEATS должен быть больше или равен 1."
return signal, confidence, repeat_count, None
def _parse_int(parts: list[str], *, index: int, default: int) -> int:
try:
return int(parts[index])
except (IndexError, TypeError, ValueError):
return default
def _parse_float(parts: list[str], *, index: int, default: float) -> float:
try:
return float(parts[index])
except (IndexError, TypeError, ValueError):
return default
def _signal_duration_text(state: DebugTradeState) -> str:
started_at = state.signal_started_at
if started_at is not None:
total_seconds = max(0, int(__import__("time").monotonic() - float(started_at)))
else:
total_seconds = max(0, (state.last_signal_repeat_count or 0) * 5)
hours = total_seconds // 3600
minutes = (total_seconds % 3600) // 60
seconds = total_seconds % 60
if hours > 0:
return f"{hours}ч {minutes:02d}м"
if minutes > 0:
return f"{minutes}м {seconds:02d}с"
return f"{seconds}с"
def _signal_icon(signal: str | None) -> str:
mapping = {
"BUY": "🟢",
"SELL": "🔴",
"HOLD": "🟡",
}
return mapping.get(signal or "", "⚪")
def _format_leverage(value: float | int | None) -> str:
if value is None:
return "x—"
return f"x{float(value):g}"
def _format_crypto_size(value: float | int | None) -> str:
if value is None:
return "—"
number = float(value)
return f"{number:.5f}".rstrip("0").rstrip(".")
def _format_percent(value: float | int | None) -> str:
if value is None:
return "off"
number = float(value)
if abs(number - round(number)) < 1e-9:
return f"{int(round(number))}%"
return f"{number:.2f}".rstrip("0").rstrip(".") + "%"
def _format_money_compact(value: float | int | None) -> str:
if value is None:
return "—"
number = float(value)
if abs(number - round(number)) < 1e-9:
return f"{number:,.0f}".replace(",", " ")
return f"{number:,.2f}".replace(",", " ").rstrip("0").rstrip(".")
def _format_usd_or_dash(value: float | int | None) -> str:
if value is None:
return "—"
return f"$ {_format_money_compact(value)}"
def _format_usd_or_off(value: float | int | None) -> str:
if value is None:
return "off"
return f"$ {_format_money_compact(value)}"
def _format_signed_usd(value: float | int | None) -> str:
if value is None:
return "—"
amount = float(value)
if amount > 0:
return f"🟢 +$ {_format_money_compact(amount)}"
if amount < 0:
return f"🔴 −$ {_format_money_compact(abs(amount))}"
return "$ 0"