Stage 07.4.3.15 — Isolated debug runtime and debug auto screen

This commit is contained in:
2026-05-09 09:17:34 +03:00
parent df76490783
commit 71cf206e32
13 changed files with 1875 additions and 492 deletions

View File

@@ -3,19 +3,15 @@
from __future__ import annotations
import math
import time
from datetime import datetime
from aiogram import F, Router
from aiogram.types import Message
from src.core.config import load_settings
from src.integrations.exchange.service import ExchangeService
from src.trading.auto.runner import AutoTradeRunner
from src.trading.auto.service import AutoTradeService
from src.trading.execution.engine import ExecutionEngine
from src.trading.journal.service import JournalService
from src.trading.position.state import PositionState
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")
@@ -28,7 +24,8 @@ def _debug_enabled() -> bool:
def _debug_help_text() -> str:
return (
"<b>🧪 Debug commands</b>\n\n"
"<b>Auto UI states:</b>\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"
@@ -37,29 +34,23 @@ def _debug_help_text() -> str:
"/debug_auto sell_ready 0.91\n"
"/debug_auto long\n"
"/debug_auto short\n"
"/debug_auto reset\n"
"/debug_auto state\n\n"
"<b>Paper execution:</b>\n"
"/debug_exec buy — открыть LONG\n"
"/debug_exec sell — открыть SHORT\n"
"/debug_exec flip — перевернуть текущую позицию\n"
"/debug_exec flip_buy — перевернуть в LONG\n"
"/debug_exec flip_sell — перевернуть в SHORT\n"
"/debug_exec close — закрыть позицию\n"
"/debug_exec state — состояние позиции\n\n"
"<b>Live paper test:</b>\n"
"/debug_live buy — открыть LONG и запустить мониторинг\n"
"/debug_live sell — открыть SHORT и запустить мониторинг\n"
"/debug_live flip — перевернуть текущую позицию и продолжить мониторинг\n"
"/debug_live close — закрыть позицию\n"
"/debug_live stop — остановить мониторинг, позицию не закрывать\n"
"/debug_live state — состояние live paper test\n\n"
"<b>Legacy:</b>\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"
"/debug_state\n\n"
"⚠️ Все команды работают в изолированном [DEBUG] runtime "
"и не меняют обычную автоторговлю."
)
@@ -81,137 +72,122 @@ async def debug_auto(message: Message) -> None:
parts = (message.text or "").split()
command = parts[1].lower() if len(parts) > 1 else "help"
service = AutoTradeService()
state = service.get_state()
service = DebugTradeService()
if command in {"help", "-h", "--help"}:
await message.answer(_debug_help_text())
return
if command == "off":
_clear_debug_position(state)
state.status = "OFF"
state.decision_status = "WAITING"
state.last_signal = "HOLD"
state.last_signal_confidence = 0.0
state.last_signal_repeat_count = 1
state.is_signal_confirmed = False
state.is_signal_ready = False
_set_signal_started_at(state)
await _refresh_auto_screen()
await message.answer("✅ Debug Auto: OFF")
if command == "reset":
state = service.reset()
await message.answer(
"✅ [DEBUG] Runtime reset\n\n"
f"{_debug_state_text(state)}"
)
return
if command == "reset":
_clear_debug_position(state)
state.status = "RUNNING"
state.decision_status = "WAITING"
state.decision_reason = None
state.last_signal = "HOLD"
state.last_signal_reason = "DEBUG RESET HOLD"
state.last_signal_confidence = 0.0
state.last_signal_repeat_count = 1
state.is_signal_confirmed = False
state.is_signal_ready = False
state.execution_block_reason = None
state.execution_size_adjustment_reason = None
_set_signal_started_at(state)
await _refresh_auto_screen()
await message.answer("✅ Debug Auto: reset to RUNNING HOLD")
if command == "off":
state = service.stop()
await message.answer(
"✅ [DEBUG] Runtime stopped\n\n"
f"{_debug_state_text(state)}"
)
return
if command == "state":
_sync_state_from_position(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)
_clear_debug_position(state)
_set_signal_state(
state=state,
state = service.set_signal_duration(
signal="HOLD",
seconds=seconds,
confidence=0.0,
decision_status="WAITING",
ready=False,
force_ready=False,
)
await message.answer(
f"✅ [DEBUG] HOLD {seconds}s\n\n"
f"{_debug_state_text(state)}"
)
await _refresh_auto_screen()
await message.answer(f"✅ Debug Auto: HOLD {seconds}s")
return
if command == "buy":
seconds = _parse_int(parts, index=2, default=12)
confidence = _parse_float(parts, index=3, default=0.74)
_clear_debug_position(state)
_set_signal_state(
state=state,
state = service.set_signal_duration(
signal="BUY",
seconds=seconds,
confidence=confidence,
decision_status="CONFIRMING",
ready=False,
force_ready=False,
)
await message.answer(
f"✅ [DEBUG] BUY {seconds}s confidence={confidence:.2f}\n\n"
f"{_debug_state_text(state)}"
)
await _refresh_auto_screen()
await message.answer(f"✅ Debug Auto: BUY {seconds}s confidence={confidence:.2f}")
return
if command == "buy_ready":
confidence = _parse_float(parts, index=2, default=0.88)
_clear_debug_position(state)
_set_signal_state(
state=state,
state = service.set_signal_duration(
signal="BUY",
seconds=15,
confidence=confidence,
decision_status="READY",
ready=True,
force_ready=True,
)
await message.answer(
f"✅ [DEBUG] BUY READY confidence={confidence:.2f}\n\n"
f"{_debug_state_text(state)}"
)
await _refresh_auto_screen()
await message.answer(f"✅ Debug Auto: BUY READY confidence={confidence:.2f}")
return
if command == "sell":
seconds = _parse_int(parts, index=2, default=9)
confidence = _parse_float(parts, index=3, default=0.71)
_clear_debug_position(state)
_set_signal_state(
state=state,
state = service.set_signal_duration(
signal="SELL",
seconds=seconds,
confidence=confidence,
decision_status="CONFIRMING",
ready=False,
force_ready=False,
)
await message.answer(
f"✅ [DEBUG] SELL {seconds}s confidence={confidence:.2f}\n\n"
f"{_debug_state_text(state)}"
)
await _refresh_auto_screen()
await message.answer(f"✅ Debug Auto: SELL {seconds}s confidence={confidence:.2f}")
return
if command == "sell_ready":
confidence = _parse_float(parts, index=2, default=0.91)
_clear_debug_position(state)
_set_signal_state(
state=state,
state = service.set_signal_duration(
signal="SELL",
seconds=15,
confidence=confidence,
decision_status="READY",
ready=True,
force_ready=True,
)
await message.answer(
f"✅ [DEBUG] SELL READY confidence={confidence:.2f}\n\n"
f"{_debug_state_text(state)}"
)
await _refresh_auto_screen()
await message.answer(f"✅ Debug Auto: SELL READY confidence={confidence:.2f}")
return
if command == "long":
_set_debug_position(state=state, side="LONG")
await _refresh_auto_screen()
await message.answer("✅ Debug Auto: active LONG position")
state, result = service.open_long()
await message.answer(_execution_result_text("OPEN LONG", state, result))
return
if command == "short":
_set_debug_position(state=state, side="SHORT")
await _refresh_auto_screen()
await message.answer("✅ Debug Auto: active SHORT position")
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()}")
@@ -226,72 +202,49 @@ async def debug_exec(message: Message) -> None:
parts = (message.text or "").split()
command = parts[1].lower() if len(parts) > 1 else "help"
service = AutoTradeService()
state = service.get_state()
engine = ExecutionEngine()
service = DebugTradeService()
if command in {"help", "-h", "--help"}:
await message.answer(_debug_help_text())
return
if command == "state":
_sync_state_from_position(state)
state = service.get_state()
service.update_market()
await message.answer(_debug_state_text(state))
return
if command == "buy":
_prepare_ready_signal(state=state, signal="BUY", confidence=0.95)
result = engine.process(state)
await _after_debug_execution()
await message.answer(_execution_result_text("BUY execution", result, state))
state, result = service.open_long()
await message.answer(_execution_result_text("EXEC BUY / LONG", state, result))
return
if command == "sell":
_prepare_ready_signal(state=state, signal="SELL", confidence=0.95)
result = engine.process(state)
await _after_debug_execution()
await message.answer(_execution_result_text("SELL execution", result, state))
state, result = service.open_short()
await message.answer(_execution_result_text("EXEC SELL / SHORT", state, result))
return
if command == "flip":
position = engine.get_position()
current_side = position.side or state.position_side or "NONE"
if current_side == "LONG":
target_signal = "SELL"
elif current_side == "SHORT":
target_signal = "BUY"
else:
await message.answer(
"⛔️ Flip невозможен: нет открытой позиции.\n\n"
"Сначала выполните /debug_exec buy или /debug_exec sell."
)
return
_prepare_ready_signal(state=state, signal=target_signal, confidence=0.95)
result = engine.process(state)
await _after_debug_execution()
await message.answer(_execution_result_text("AUTO FLIP execution", result, state))
return
if command == "flip_buy":
_prepare_ready_signal(state=state, signal="BUY", confidence=0.95)
result = engine.process(state)
await _after_debug_execution()
await message.answer(_execution_result_text("FLIP to LONG execution", result, state))
return
if command == "flip_sell":
_prepare_ready_signal(state=state, signal="SELL", confidence=0.95)
result = engine.process(state)
await _after_debug_execution()
await message.answer(_execution_result_text("FLIP to SHORT execution", result, state))
state, result = service.flip()
await message.answer(_execution_result_text("EXEC AUTO FLIP", state, result))
return
if command == "close":
result = engine._close_position(state, forced_reason="DEBUG_CLOSE")
await _after_debug_execution()
await message.answer(_execution_result_text("CLOSE execution", result, state))
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()}")
@@ -303,285 +256,132 @@ async def debug_live(message: Message) -> None:
await message.answer("Debug mode выключен.")
return
parts = (message.text or "").split()
command = parts[1].lower() if len(parts) > 1 else "help"
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 экран."
)
service = AutoTradeService()
@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()
engine = ExecutionEngine()
service.update_market()
if command in {"help", "-h", "--help"}:
await message.answer(_debug_help_text())
return
if command == "buy":
_prepare_ready_signal(state=state, signal="BUY", confidence=0.95)
result = engine.process(state)
await _start_live_monitoring()
await message.answer(_execution_result_text("LIVE BUY execution", result, state))
return
if command == "sell":
_prepare_ready_signal(state=state, signal="SELL", confidence=0.95)
result = engine.process(state)
await _start_live_monitoring()
await message.answer(_execution_result_text("LIVE SELL execution", result, state))
return
if command == "flip":
position = engine.get_position()
current_side = position.side or state.position_side or "NONE"
if current_side == "LONG":
target_signal = "SELL"
elif current_side == "SHORT":
target_signal = "BUY"
else:
await message.answer(
"⛔️ Live flip невозможен: нет открытой позиции.\n\n"
"Сначала выполните /debug_live buy или /debug_live sell."
)
return
_prepare_ready_signal(state=state, signal=target_signal, confidence=0.95)
result = engine.process(state)
await _start_live_monitoring()
await message.answer(_execution_result_text("LIVE AUTO FLIP execution", result, state))
return
if command == "close":
result = engine._close_position(state, forced_reason="DEBUG_LIVE_CLOSE")
await _after_debug_execution()
await message.answer(_execution_result_text("LIVE CLOSE execution", result, state))
return
if command == "stop":
AutoTradeRunner.stop()
await _refresh_auto_screen()
await message.answer("✅ Debug live stopped. Позиция не закрыта.")
return
if command == "state":
_sync_state_from_position(state)
await message.answer(_debug_state_text(state))
return
await message.answer(f"⛔️ Неизвестная команда: {command}\n\n{_debug_help_text()}")
await message.answer(_debug_state_text(state))
def _prepare_ready_signal(*, state, signal: str, confidence: float) -> None:
state.status = "RUNNING"
state.last_signal = signal
state.last_signal_confidence = max(0.0, min(1.0, confidence))
state.last_signal_repeat_count = 3
state.last_signal_reason = f"DEBUG EXEC {signal}"
state.decision_status = "READY"
state.decision_reason = "DEBUG EXEC READY"
state.is_signal_confirmed = True
state.is_signal_ready = True
state.execution_block_reason = None
state.execution_size_adjustment_reason = None
_set_signal_started_at(state)
def _debug_state_text(state: DebugTradeState) -> str:
position = state.position
def _set_signal_state(
*,
state,
signal: str,
seconds: int,
confidence: float,
decision_status: str,
ready: bool,
) -> None:
state.status = "RUNNING"
state.last_signal = signal
state.last_signal_confidence = max(0.0, min(1.0, confidence))
state.last_signal_repeat_count = _seconds_to_repeats(seconds)
state.last_signal_reason = f"DEBUG {signal} {seconds}s"
state.decision_status = decision_status
state.decision_reason = f"DEBUG {decision_status}"
state.is_signal_confirmed = ready
state.is_signal_ready = ready
state.execution_block_reason = None
state.execution_size_adjustment_reason = None
_set_signal_started_at(state, seconds_ago=seconds)
def _set_signal_started_at(state, *, seconds_ago: int = 0) -> None:
if hasattr(state, "signal_started_at"):
state.signal_started_at = time.monotonic() - max(0, seconds_ago)
def _set_debug_position(*, state, side: str) -> None:
state.status = "RUNNING"
state.last_signal = "BUY" if side == "LONG" else "SELL"
state.last_signal_confidence = 0.90
state.last_signal_repeat_count = 3
state.decision_status = "READY"
state.is_signal_confirmed = True
state.is_signal_ready = True
_set_signal_started_at(state, seconds_ago=15)
entry_price = _debug_entry_price(state.symbol, side)
size = _debug_size_for_notional(entry_price, notional=1000.0)
now = datetime.now().strftime("%H:%M:%S")
position = PositionState(
side=side,
symbol=state.symbol,
entry_price=entry_price,
size=size,
leverage=state.leverage,
unrealized_pnl_usd=0.0,
opened_at=now,
updated_at=now,
)
ExecutionEngine._position = position
_sync_state_from_position(state)
def _clear_debug_position(state) -> None:
ExecutionEngine._position = PositionState()
state.position_side = "NONE"
state.entry_price = None
state.position_size = None
state.unrealized_pnl_usd = None
def _sync_state_from_position(state) -> None:
position = ExecutionEngine().get_position()
state.position_side = position.side
state.entry_price = position.entry_price
state.position_size = position.size
state.unrealized_pnl_usd = position.unrealized_pnl_usd
def _debug_entry_price(symbol: str, side: str) -> float:
try:
snapshot = ExchangeService().get_market_snapshot(symbol)
if side == "LONG":
return float(snapshot.get("ask_price") or snapshot.get("last_price"))
if side == "SHORT":
return float(snapshot.get("bid_price") or snapshot.get("last_price"))
return float(snapshot.get("last_price"))
except Exception:
return 100000.0
def _debug_size_for_notional(entry_price: float, *, notional: float) -> float:
if entry_price <= 0:
return 0.0
value = notional / entry_price
factor = 10**5
return math.floor(value * factor) / factor
def _seconds_to_repeats(seconds: int) -> int:
return max(1, math.ceil(max(0, seconds) / 5))
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
async def _refresh_auto_screen() -> None:
AutoTradeRunner.set_current_screen("auto")
AutoTradeRunner._last_text = None
await AutoTradeRunner._refresh_screen(force=True)
async def _start_live_monitoring() -> None:
state = AutoTradeService().get_state()
state.status = "RUNNING"
_sync_state_from_position(state)
AutoTradeRunner.set_current_screen("auto")
AutoTradeRunner._last_text = None
await AutoTradeRunner.process_last_event_now()
await _refresh_auto_screen()
AutoTradeRunner.start()
async def _after_debug_execution() -> None:
state = AutoTradeService().get_state()
_sync_state_from_position(state)
AutoTradeRunner.set_current_screen("auto")
AutoTradeRunner._last_text = None
await AutoTradeRunner.process_last_event_now()
await _refresh_auto_screen()
def _execution_result_text(title: str, result, state) -> str:
_sync_state_from_position(state)
duration = _signal_duration_text(state)
pnl = position.unrealized_pnl_usd
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"Signal: {state.last_signal}\n"
f"Decision: {state.decision_status}\n\n"
f"Position: {state.position_side}\n"
f"Entry: {state.entry_price}\n"
f"Size: {state.position_size}\n"
f"PnL: {state.unrealized_pnl_usd}"
)
def _debug_state_text(state) -> str:
runner_task_running = (
AutoTradeRunner._task is not None
and not AutoTradeRunner._task.done()
)
return (
"<b>Debug Auto State</b>\n\n"
"<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\n"
f"Signal: {state.last_signal}\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: {state.last_signal_confidence:.2f}\n"
f"Decision: {state.decision_status}\n"
f"Ready: {state.is_signal_ready}\n"
f"Signal started at: {getattr(state, 'signal_started_at', None)}\n\n"
f"<b>Runner</b>\n"
f"Screen: {AutoTradeRunner._current_screen}\n"
f"Chat ID: {AutoTradeRunner._chat_id}\n"
f"Message ID: {AutoTradeRunner._message_id}\n"
f"Has bot: {AutoTradeRunner._bot is not None}\n"
f"Has render_text: {AutoTradeRunner._render_text is not None}\n"
f"Task running: {runner_task_running}\n\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: {state.position_side}\n"
f"Entry: {state.entry_price}\n"
f"Size: {state.position_size}\n"
f"PnL: {state.unrealized_pnl_usd}"
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)}"
)
@@ -611,91 +411,113 @@ def _parse_debug_signal_args(raw_text: str | None) -> tuple[str, float, int, str
return signal, confidence, repeat_count, None
@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 = AutoTradeService()
state = service.debug_force_signal(
signal=signal,
confidence=confidence,
repeat_count=repeat_count,
reason=f"DEBUG FORCE {signal} {confidence:.2f} ×{repeat_count}",
)
if state.status == "OFF":
state.status = "RUNNING"
_set_signal_started_at(state)
await _refresh_auto_screen()
JournalService().log_ui_info(
event_type="debug_signal_forced",
message=f"Debug-сигнал принудительно установлен: {signal}.",
screen="debug",
action="debug_signal",
user_id=message.from_user.id if message.from_user else None,
chat_id=message.chat.id,
payload={
"signal": state.last_signal,
"decision_status": state.decision_status,
"confidence": state.last_signal_confidence,
"repeat_count": state.last_signal_repeat_count,
},
)
await message.answer(
"✅ Debug signal forced\n\n"
f"Signal: {state.last_signal}\n"
f"Decision: {state.decision_status}\n"
f"Confidence: {state.last_signal_confidence:.2f}\n"
f"Repeats: {state.last_signal_repeat_count}"
)
def _parse_int(parts: list[str], *, index: int, default: int) -> int:
try:
return int(parts[index])
except (IndexError, TypeError, ValueError):
return default
@router.message(F.text == "/debug_ready")
async def debug_ready(message: Message) -> None:
if not _debug_enabled():
await message.answer("Debug mode выключен.")
return
service = AutoTradeService()
state = service.get_state()
_clear_debug_position(state)
_set_signal_state(
state=state,
signal="BUY",
seconds=15,
confidence=0.95,
decision_status="READY",
ready=True,
)
await _refresh_auto_screen()
await message.answer(
"✅ Debug READY создан\n\n"
f"Signal: {state.last_signal}\n"
f"Decision: {state.decision_status}\n"
f"Confidence: {state.last_signal_confidence:.2f}"
)
def _parse_float(parts: list[str], *, index: int, default: float) -> float:
try:
return float(parts[index])
except (IndexError, TypeError, ValueError):
return default
@router.message(F.text == "/debug_state")
async def debug_state(message: Message) -> None:
if not _debug_enabled():
await message.answer("Debug mode выключен.")
return
def _signal_duration_text(state: DebugTradeState) -> str:
started_at = state.signal_started_at
state = AutoTradeService().get_state()
_sync_state_from_position(state)
await message.answer(_debug_state_text(state))
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"

View File

@@ -0,0 +1,161 @@
# app/src/telegram/handlers/debug_auto/main.py
from __future__ import annotations
from aiogram import F, Router
from aiogram.exceptions import TelegramBadRequest
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
from src.telegram.handlers.debug_auto.ui import (
build_debug_auto_text,
debug_auto_keyboard,
)
from src.trading.debug.runner import DebugTradeRunner
from src.trading.debug.service import DebugTradeService
router = Router(name="debug_auto")
async def render_debug_auto_screen(
target_message: Message,
*,
edit_mode: bool,
) -> None:
text = build_debug_auto_text()
if edit_mode:
try:
await target_message.edit_text(
text,
reply_markup=debug_auto_keyboard(),
)
except TelegramBadRequest as exc:
if "message is not modified" not in str(exc).lower():
raise
DebugTradeRunner.register_screen(
bot=target_message.bot,
chat_id=target_message.chat.id,
message_id=target_message.message_id,
render_text=build_debug_auto_text,
render_markup=debug_auto_keyboard,
)
return
sent_message = await target_message.answer(
text,
reply_markup=debug_auto_keyboard(),
)
DebugTradeRunner.register_screen(
bot=sent_message.bot,
chat_id=sent_message.chat.id,
message_id=sent_message.message_id,
render_text=build_debug_auto_text,
render_markup=debug_auto_keyboard,
)
@router.message(F.text.in_({"🧪 Debug Auto", "Debug Auto", "/debug_auto_screen"}))
async def open_debug_auto(message: Message, state: FSMContext) -> None:
await state.clear()
DebugTradeRunner.set_current_screen("debug_auto")
await DebugTradeRunner.delete_registered_screen(
bot=message.bot,
chat_id=message.chat.id,
)
await render_debug_auto_screen(message, edit_mode=False)
@router.callback_query(F.data == "debug_auto:start")
async def debug_auto_start(callback: CallbackQuery) -> None:
service = DebugTradeService()
state = service.get_state()
state.status = "RUNNING"
DebugTradeRunner.set_current_screen("debug_auto")
DebugTradeRunner.start()
if callback.message is not None:
await render_debug_auto_screen(callback.message, edit_mode=True)
await callback.answer("[DEBUG] Мониторинг запущен.")
@router.callback_query(F.data == "debug_auto:stop")
async def debug_auto_stop(callback: CallbackQuery) -> None:
DebugTradeRunner.stop()
DebugTradeService().stop()
if callback.message is not None:
await render_debug_auto_screen(callback.message, edit_mode=True)
await callback.answer("[DEBUG] Мониторинг остановлен.")
@router.callback_query(F.data == "debug_auto:long")
async def debug_auto_long(callback: CallbackQuery) -> None:
service = DebugTradeService()
_, result = service.open_long()
DebugTradeRunner.set_current_screen("debug_auto")
DebugTradeRunner.start()
if callback.message is not None:
await render_debug_auto_screen(callback.message, edit_mode=True)
await callback.answer(result.reason, show_alert=False)
@router.callback_query(F.data == "debug_auto:short")
async def debug_auto_short(callback: CallbackQuery) -> None:
service = DebugTradeService()
_, result = service.open_short()
DebugTradeRunner.set_current_screen("debug_auto")
DebugTradeRunner.start()
if callback.message is not None:
await render_debug_auto_screen(callback.message, edit_mode=True)
await callback.answer(result.reason, show_alert=False)
@router.callback_query(F.data == "debug_auto:flip")
async def debug_auto_flip(callback: CallbackQuery) -> None:
service = DebugTradeService()
_, result = service.flip()
DebugTradeRunner.set_current_screen("debug_auto")
DebugTradeRunner.start()
if callback.message is not None:
await render_debug_auto_screen(callback.message, edit_mode=True)
await callback.answer(result.reason, show_alert=False)
@router.callback_query(F.data == "debug_auto:close")
async def debug_auto_close(callback: CallbackQuery) -> None:
service = DebugTradeService()
_, result = service.close(reason="DEBUG_SCREEN_CLOSE")
if callback.message is not None:
await render_debug_auto_screen(callback.message, edit_mode=True)
await callback.answer(result.reason, show_alert=False)
@router.callback_query(F.data == "debug_auto:reset")
async def debug_auto_reset(callback: CallbackQuery) -> None:
DebugTradeService().reset()
if callback.message is not None:
await render_debug_auto_screen(callback.message, edit_mode=True)
await callback.answer("[DEBUG] Runtime reset.")

View File

@@ -0,0 +1,228 @@
# app/src/telegram/handlers/debug_auto/ui.py
from __future__ import annotations
import time
from aiogram.types import InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from src.trading.debug.service import DebugTradeService
def debug_auto_keyboard() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(text="▶️ Start", callback_data="debug_auto:start")
builder.button(text="🛑 Stop", callback_data="debug_auto:stop")
builder.button(text="🟢 LONG", callback_data="debug_auto:long")
builder.button(text="🔴 SHORT", callback_data="debug_auto:short")
builder.button(text="🔁 Flip", callback_data="debug_auto:flip")
builder.button(text="❌ Close", callback_data="debug_auto:close")
builder.button(text="🔄 Reset", callback_data="debug_auto:reset")
builder.adjust(2, 3, 2)
return builder.as_markup()
def build_debug_auto_text() -> str:
state = DebugTradeService().get_state()
position = state.position
parts = [
"🧪 <b>[DEBUG] Автоторговля</b>",
"",
f"<b>Status</b> · {state.status}",
f"<b>Актив</b> · {_asset_symbol(state.symbol)}",
f"<b>Стратегия</b> · {state.strategy or ''}",
f"<b>Баланс</b> · $ {_format_money_compact(state.allocated_balance_usd)}",
f"<b>Realized PnL</b> · {_format_signed_usd(state.realized_pnl_usd)}",
"",
_signal_line(state),
]
if state.last_signal != "HOLD":
parts.append(f"<b>Уверенность</b> · {state.last_signal_confidence:.2f}")
parts.extend(
[
f"<b>Decision</b> · {state.decision_status}",
"",
"<b>Risk</b>",
f"SL · {_format_percent(state.stop_loss_percent)}",
f"TP · {_format_percent(state.take_profit_percent)}",
f"ML · {_format_usd_or_off(state.max_loss_usd)}",
f"Max Reserved · {_format_percent(state.max_reserved_balance_percent)}",
]
)
if state.execution_block_reason or state.execution_size_adjustment_reason:
parts.extend(
[
"",
f"Blocked · {state.execution_block_reason or ''}",
f"Adjusted · {state.execution_size_adjustment_reason or ''}",
]
)
parts.append("")
if position.side == "NONE":
parts.extend(
[
"📭 <b>Позиция не открыта</b>",
"",
"Debug runtime изолирован от обычной автоторговли.",
]
)
return "\n".join(parts)
side_icon = "🟢" if position.side == "LONG" else "🔴"
notional = None
if position.entry_price is not None and position.size is not None:
notional = position.entry_price * position.size
reserved = None
if notional is not None and position.leverage and position.leverage > 0:
reserved = notional / position.leverage
parts.extend(
[
f"{side_icon} <b>{position.side}</b> · {_asset_symbol(position.symbol)} · {_leverage_text(position.leverage)}",
"",
f"<b>Entry</b> · {_format_usd_or_dash(position.entry_price)}",
f"<b>Size</b> · {_format_crypto_size(position.size)}",
f"<b>Notional</b> · {_format_usd_or_dash(notional)}",
f"<b>Reserved</b> · {_format_usd_or_dash(reserved)}",
f"<b>PnL</b> · {_format_signed_usd(position.unrealized_pnl_usd)}",
f"<b>Opened</b> · {position.opened_at or ''}",
f"<b>Updated</b> · {position.updated_at or ''}",
"",
"Debug runtime изолирован от обычной автоторговли.",
]
)
return "\n".join(parts)
def _signal_line(state) -> str:
signal = state.last_signal or "HOLD"
if signal in {"BUY", "SELL"} and state.is_signal_ready:
return f"<b>Сигнал</b> {_signal_icon(signal)} {signal} · READY"
return f"<b>Сигнал</b> {_signal_icon(signal)} {signal} · {_signal_duration_text(state)}"
def _signal_duration_text(state) -> str:
started_at = state.signal_started_at
if started_at is not None:
total_seconds = max(0, int(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 _asset_symbol(symbol: str | None) -> str:
if not symbol:
return ""
base = symbol.split("_", 1)[0].upper()
if "/" in base:
return base.split("/", 1)[0]
for suffix in ("USDT", "USD", "EUR", "BTC"):
if base.endswith(suffix) and len(base) > len(suffix):
return base[: -len(suffix)]
return base
def _leverage_text(value: float | int | None) -> str:
if value is None:
return "x—"
return f"x{float(value):g}"
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_crypto_size(value: float | int | None) -> str:
if value is None:
return ""
return f"{float(value):.5f}".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"