Files
dzentra_bot/app/src/telegram/handlers/debug.py

669 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (
"<b>🧪 Debug commands</b>\n\n"
"<b>Isolated Debug Runtime:</b>\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"
"<b>Isolated Debug Execution:</b>\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"
"<b>Legacy aliases:</b>\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 (
"<b>[DEBUG] Auto State</b>\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"<b>Signal</b>\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"<b>Risk</b>\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"<b>Position</b>\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"