Stage 07.4.3.15 — Isolated debug runtime and debug auto screen
This commit is contained in:
@@ -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"
|
||||
161
app/src/telegram/handlers/debug_auto/main.py
Normal file
161
app/src/telegram/handlers/debug_auto/main.py
Normal 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.")
|
||||
228
app/src/telegram/handlers/debug_auto/ui.py
Normal file
228
app/src/telegram/handlers/debug_auto/ui.py
Normal 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"
|
||||
@@ -3,6 +3,8 @@
|
||||
from aiogram import Dispatcher
|
||||
|
||||
from src.telegram.handlers.auto import router as auto_router
|
||||
from src.telegram.handlers.debug import router as debug_router
|
||||
from src.telegram.handlers.debug_auto.main import router as debug_auto_router
|
||||
from src.telegram.handlers.home import router as home_router
|
||||
from src.telegram.handlers.journal import router as journal_router
|
||||
from src.telegram.handlers.market import router as market_router
|
||||
@@ -12,7 +14,6 @@ from src.telegram.handlers.start import router as start_router
|
||||
from src.telegram.handlers.system import router as system_router
|
||||
from src.telegram.handlers.trade.main import router as trade_main_router
|
||||
from src.telegram.handlers.trade.new_order import router as trade_new_order_router
|
||||
from src.telegram.handlers.debug import router as debug_router
|
||||
|
||||
|
||||
def setup_routers(dispatcher: Dispatcher) -> None:
|
||||
@@ -25,5 +26,6 @@ def setup_routers(dispatcher: Dispatcher) -> None:
|
||||
dispatcher.include_router(trade_new_order_router)
|
||||
dispatcher.include_router(auto_router)
|
||||
dispatcher.include_router(journal_router)
|
||||
dispatcher.include_router(debug_auto_router)
|
||||
dispatcher.include_router(debug_router)
|
||||
dispatcher.include_router(system_router)
|
||||
Reference in New Issue
Block a user