# app/src/telegram/handlers/debug.py from __future__ import annotations import math import time from aiogram import F, Router from aiogram.types import Message from src.core.config import load_settings from src.core.numbers import safe_float from src.core.types import JsonList, NumericLike 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 bool(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 " f"{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: {safe_float(state.last_signal_confidence) or 0.0:.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.", ) confidence = _parse_float(parts, index=2, default=0.9) if confidence < 0 or confidence > 1: return ( "BUY", 0.9, 2, "CONFIDENCE должен быть от 0.00 до 1.00.", ) repeat_count = _parse_int(parts, index=3, default=2) if repeat_count < 1: return ( "BUY", 0.9, 2, "REPEATS должен быть больше или равен 1.", ) return signal, confidence, repeat_count, None def _parse_int( parts: JsonList, *, index: int, default: int, ) -> int: try: value = parts[index] except (IndexError, TypeError): return default number = safe_float(value) if number is None: return default return int(number) def _parse_float( parts: JsonList, *, index: int, default: float, ) -> float: try: value = parts[index] except (IndexError, TypeError): return default number = safe_float(value) if number is None: return default return number def _signal_duration_text( state: DebugTradeState, ) -> str: started_at = safe_float(state.signal_started_at) if started_at is not None: total_seconds = max( 0, int(time.monotonic() - started_at), ) else: repeats = state.last_signal_repeat_count or 0 total_seconds = max(0, int(repeats) * 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: NumericLike | None, ) -> str: number = safe_float(value) if number is None: return "x—" return f"x{number:g}" def _format_crypto_size( value: NumericLike | None, ) -> str: number = safe_float(value) if number is None: return "—" return f"{number:.5f}".rstrip("0").rstrip(".") def _format_percent( value: NumericLike | None, ) -> str: number = safe_float(value) if number is None: return "off" if math.isclose(number, round(number), abs_tol=1e-9): return f"{int(round(number))}%" return f"{number:.2f}".rstrip("0").rstrip(".") + "%" def _format_money_compact( value: NumericLike | None, ) -> str: number = safe_float(value) if number is None: return "—" if math.isclose(number, round(number), abs_tol=1e-9): return f"{number:,.0f}".replace(",", " ") return ( f"{number:,.2f}" .replace(",", " ") .rstrip("0") .rstrip(".") ) def _format_usd_or_dash( value: NumericLike | None, ) -> str: if safe_float(value) is None: return "—" return f"$ {_format_money_compact(value)}" def _format_usd_or_off( value: NumericLike | None, ) -> str: if safe_float(value) is None: return "off" return f"$ {_format_money_compact(value)}" def _format_signed_usd( value: NumericLike | None, ) -> str: amount = safe_float(value) if amount is None: return "—" if amount > 0: return f"🟢 +$ {_format_money_compact(amount)}" if amount < 0: return f"🔴 −$ {_format_money_compact(abs(amount))}" return "$ 0"