Stage 07.4.3.15 — Isolated debug runtime and debug auto screen
This commit is contained in:
@@ -312,6 +312,63 @@ class ExchangeService:
|
|||||||
"updated_at": self._format_exchange_time(close_time),
|
"updated_at": self._format_exchange_time(close_time),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# получить свежий market snapshot напрямую через REST, без WebSocket cache
|
||||||
|
def get_fresh_market_snapshot(self, symbol: str | None = None) -> dict[str, object]:
|
||||||
|
symbol_to_use = symbol or self.settings.default_symbol
|
||||||
|
|
||||||
|
if not self.settings.exchange_enabled:
|
||||||
|
ticker = mock_ticker_price(symbol_to_use)
|
||||||
|
return {
|
||||||
|
"symbol": ticker.symbol,
|
||||||
|
"last_price": ticker.price,
|
||||||
|
"bid_price": ticker.price,
|
||||||
|
"ask_price": ticker.price,
|
||||||
|
"updated_at": ticker.updated_at,
|
||||||
|
"source": "mock",
|
||||||
|
}
|
||||||
|
|
||||||
|
validation = self.validate_symbol(symbol_to_use)
|
||||||
|
if not validation.is_valid:
|
||||||
|
raise ExchangeError(validation.message)
|
||||||
|
|
||||||
|
client = ExchangeRestClient()
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = client.get_json(
|
||||||
|
"/api/v2/ticker/24hr",
|
||||||
|
params={"symbol": validation.normalized_symbol},
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
self._log_exchange_error(
|
||||||
|
endpoint="ticker/24hr",
|
||||||
|
exc=exc,
|
||||||
|
symbol=validation.normalized_symbol,
|
||||||
|
)
|
||||||
|
raise ExchangeError(str(exc)) from exc
|
||||||
|
|
||||||
|
last_raw = payload.get("lastPrice")
|
||||||
|
if last_raw is None:
|
||||||
|
exc = ExchangeError("Field 'lastPrice' is missing in ticker response.")
|
||||||
|
self._log_exchange_error(
|
||||||
|
endpoint="ticker/24hr",
|
||||||
|
exc=exc,
|
||||||
|
symbol=validation.normalized_symbol,
|
||||||
|
)
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
bid_raw = payload.get("bidPrice") or last_raw
|
||||||
|
ask_raw = payload.get("askPrice") or last_raw
|
||||||
|
close_time = payload.get("closeTime") or payload.get("eventTime") or ""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"symbol": validation.normalized_symbol,
|
||||||
|
"last_price": float(last_raw),
|
||||||
|
"bid_price": float(bid_raw),
|
||||||
|
"ask_price": float(ask_raw),
|
||||||
|
"updated_at": self._format_exchange_time(close_time),
|
||||||
|
"source": "fresh_rest",
|
||||||
|
}
|
||||||
|
|
||||||
def get_balance_summary(self) -> list[BalanceSummary]:
|
def get_balance_summary(self) -> list[BalanceSummary]:
|
||||||
if not self.settings.exchange_enabled:
|
if not self.settings.exchange_enabled:
|
||||||
return mock_balance_summary()
|
return mock_balance_summary()
|
||||||
|
|||||||
@@ -3,19 +3,15 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import math
|
import math
|
||||||
import time
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from aiogram import F, Router
|
from aiogram import F, Router
|
||||||
from aiogram.types import Message
|
from aiogram.types import Message
|
||||||
|
|
||||||
from src.core.config import load_settings
|
from src.core.config import load_settings
|
||||||
from src.integrations.exchange.service import ExchangeService
|
from src.trading.debug.execution import DebugExecutionEngine
|
||||||
from src.trading.auto.runner import AutoTradeRunner
|
from src.trading.debug.service import DebugTradeService
|
||||||
from src.trading.auto.service import AutoTradeService
|
from src.trading.debug.state import DebugTradeState
|
||||||
from src.trading.execution.engine import ExecutionEngine
|
from src.trading.execution.models import ExecutionDecision
|
||||||
from src.trading.journal.service import JournalService
|
|
||||||
from src.trading.position.state import PositionState
|
|
||||||
|
|
||||||
|
|
||||||
router = Router(name="debug")
|
router = Router(name="debug")
|
||||||
@@ -28,7 +24,8 @@ def _debug_enabled() -> bool:
|
|||||||
def _debug_help_text() -> str:
|
def _debug_help_text() -> str:
|
||||||
return (
|
return (
|
||||||
"<b>🧪 Debug commands</b>\n\n"
|
"<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 off\n"
|
||||||
"/debug_auto hold 335\n"
|
"/debug_auto hold 335\n"
|
||||||
"/debug_auto buy 12 0.74\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 sell_ready 0.91\n"
|
||||||
"/debug_auto long\n"
|
"/debug_auto long\n"
|
||||||
"/debug_auto short\n"
|
"/debug_auto short\n"
|
||||||
"/debug_auto reset\n"
|
|
||||||
"/debug_auto state\n\n"
|
"/debug_auto state\n\n"
|
||||||
"<b>Paper execution:</b>\n"
|
"<b>Isolated Debug Execution:</b>\n"
|
||||||
"/debug_exec buy — открыть LONG\n"
|
"/debug_exec buy — открыть DEBUG LONG\n"
|
||||||
"/debug_exec sell — открыть SHORT\n"
|
"/debug_exec sell — открыть DEBUG SHORT\n"
|
||||||
"/debug_exec flip — перевернуть текущую позицию\n"
|
"/debug_exec flip — перевернуть DEBUG позицию\n"
|
||||||
"/debug_exec flip_buy — перевернуть в LONG\n"
|
"/debug_exec close — закрыть DEBUG позицию\n"
|
||||||
"/debug_exec flip_sell — перевернуть в SHORT\n"
|
"/debug_exec process — один цикл DEBUG execution\n"
|
||||||
"/debug_exec close — закрыть позицию\n"
|
"/debug_exec update — обновить DEBUG PnL\n"
|
||||||
"/debug_exec state — состояние позиции\n\n"
|
"/debug_exec state — состояние DEBUG runtime\n\n"
|
||||||
"<b>Live paper test:</b>\n"
|
"<b>Legacy aliases:</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"
|
|
||||||
"/debug_signal BUY 0.95 3\n"
|
"/debug_signal BUY 0.95 3\n"
|
||||||
"/debug_signal SELL 0.70 2\n"
|
"/debug_signal SELL 0.70 2\n"
|
||||||
"/debug_signal HOLD 0.00 1\n"
|
"/debug_signal HOLD 0.00 1\n"
|
||||||
"/debug_ready\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()
|
parts = (message.text or "").split()
|
||||||
command = parts[1].lower() if len(parts) > 1 else "help"
|
command = parts[1].lower() if len(parts) > 1 else "help"
|
||||||
|
|
||||||
service = AutoTradeService()
|
service = DebugTradeService()
|
||||||
state = service.get_state()
|
|
||||||
|
|
||||||
if command in {"help", "-h", "--help"}:
|
if command in {"help", "-h", "--help"}:
|
||||||
await message.answer(_debug_help_text())
|
await message.answer(_debug_help_text())
|
||||||
return
|
return
|
||||||
|
|
||||||
if command == "off":
|
if command == "reset":
|
||||||
_clear_debug_position(state)
|
state = service.reset()
|
||||||
state.status = "OFF"
|
await message.answer(
|
||||||
state.decision_status = "WAITING"
|
"✅ [DEBUG] Runtime reset\n\n"
|
||||||
state.last_signal = "HOLD"
|
f"{_debug_state_text(state)}"
|
||||||
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")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if command == "reset":
|
if command == "off":
|
||||||
_clear_debug_position(state)
|
state = service.stop()
|
||||||
state.status = "RUNNING"
|
await message.answer(
|
||||||
state.decision_status = "WAITING"
|
"✅ [DEBUG] Runtime stopped\n\n"
|
||||||
state.decision_reason = None
|
f"{_debug_state_text(state)}"
|
||||||
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")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if command == "state":
|
if command == "state":
|
||||||
_sync_state_from_position(state)
|
state = service.get_state()
|
||||||
|
service.update_market()
|
||||||
await message.answer(_debug_state_text(state))
|
await message.answer(_debug_state_text(state))
|
||||||
return
|
return
|
||||||
|
|
||||||
if command == "hold":
|
if command == "hold":
|
||||||
seconds = _parse_int(parts, index=2, default=335)
|
seconds = _parse_int(parts, index=2, default=335)
|
||||||
_clear_debug_position(state)
|
state = service.set_signal_duration(
|
||||||
_set_signal_state(
|
|
||||||
state=state,
|
|
||||||
signal="HOLD",
|
signal="HOLD",
|
||||||
seconds=seconds,
|
seconds=seconds,
|
||||||
confidence=0.0,
|
confidence=0.0,
|
||||||
decision_status="WAITING",
|
force_ready=False,
|
||||||
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
|
return
|
||||||
|
|
||||||
if command == "buy":
|
if command == "buy":
|
||||||
seconds = _parse_int(parts, index=2, default=12)
|
seconds = _parse_int(parts, index=2, default=12)
|
||||||
confidence = _parse_float(parts, index=3, default=0.74)
|
confidence = _parse_float(parts, index=3, default=0.74)
|
||||||
_clear_debug_position(state)
|
|
||||||
_set_signal_state(
|
state = service.set_signal_duration(
|
||||||
state=state,
|
|
||||||
signal="BUY",
|
signal="BUY",
|
||||||
seconds=seconds,
|
seconds=seconds,
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
decision_status="CONFIRMING",
|
force_ready=False,
|
||||||
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
|
return
|
||||||
|
|
||||||
if command == "buy_ready":
|
if command == "buy_ready":
|
||||||
confidence = _parse_float(parts, index=2, default=0.88)
|
confidence = _parse_float(parts, index=2, default=0.88)
|
||||||
_clear_debug_position(state)
|
|
||||||
_set_signal_state(
|
state = service.set_signal_duration(
|
||||||
state=state,
|
|
||||||
signal="BUY",
|
signal="BUY",
|
||||||
seconds=15,
|
seconds=15,
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
decision_status="READY",
|
force_ready=True,
|
||||||
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
|
return
|
||||||
|
|
||||||
if command == "sell":
|
if command == "sell":
|
||||||
seconds = _parse_int(parts, index=2, default=9)
|
seconds = _parse_int(parts, index=2, default=9)
|
||||||
confidence = _parse_float(parts, index=3, default=0.71)
|
confidence = _parse_float(parts, index=3, default=0.71)
|
||||||
_clear_debug_position(state)
|
|
||||||
_set_signal_state(
|
state = service.set_signal_duration(
|
||||||
state=state,
|
|
||||||
signal="SELL",
|
signal="SELL",
|
||||||
seconds=seconds,
|
seconds=seconds,
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
decision_status="CONFIRMING",
|
force_ready=False,
|
||||||
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
|
return
|
||||||
|
|
||||||
if command == "sell_ready":
|
if command == "sell_ready":
|
||||||
confidence = _parse_float(parts, index=2, default=0.91)
|
confidence = _parse_float(parts, index=2, default=0.91)
|
||||||
_clear_debug_position(state)
|
|
||||||
_set_signal_state(
|
state = service.set_signal_duration(
|
||||||
state=state,
|
|
||||||
signal="SELL",
|
signal="SELL",
|
||||||
seconds=15,
|
seconds=15,
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
decision_status="READY",
|
force_ready=True,
|
||||||
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
|
return
|
||||||
|
|
||||||
if command == "long":
|
if command == "long":
|
||||||
_set_debug_position(state=state, side="LONG")
|
state, result = service.open_long()
|
||||||
await _refresh_auto_screen()
|
await message.answer(_execution_result_text("OPEN LONG", state, result))
|
||||||
await message.answer("✅ Debug Auto: active LONG position")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if command == "short":
|
if command == "short":
|
||||||
_set_debug_position(state=state, side="SHORT")
|
state, result = service.open_short()
|
||||||
await _refresh_auto_screen()
|
await message.answer(_execution_result_text("OPEN SHORT", state, result))
|
||||||
await message.answer("✅ Debug Auto: active SHORT position")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
await message.answer(f"⛔️ Неизвестная команда: {command}\n\n{_debug_help_text()}")
|
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()
|
parts = (message.text or "").split()
|
||||||
command = parts[1].lower() if len(parts) > 1 else "help"
|
command = parts[1].lower() if len(parts) > 1 else "help"
|
||||||
|
|
||||||
service = AutoTradeService()
|
service = DebugTradeService()
|
||||||
state = service.get_state()
|
|
||||||
engine = ExecutionEngine()
|
|
||||||
|
|
||||||
if command in {"help", "-h", "--help"}:
|
if command in {"help", "-h", "--help"}:
|
||||||
await message.answer(_debug_help_text())
|
await message.answer(_debug_help_text())
|
||||||
return
|
return
|
||||||
|
|
||||||
if command == "state":
|
if command == "state":
|
||||||
_sync_state_from_position(state)
|
state = service.get_state()
|
||||||
|
service.update_market()
|
||||||
await message.answer(_debug_state_text(state))
|
await message.answer(_debug_state_text(state))
|
||||||
return
|
return
|
||||||
|
|
||||||
if command == "buy":
|
if command == "buy":
|
||||||
_prepare_ready_signal(state=state, signal="BUY", confidence=0.95)
|
state, result = service.open_long()
|
||||||
result = engine.process(state)
|
await message.answer(_execution_result_text("EXEC BUY / LONG", state, result))
|
||||||
await _after_debug_execution()
|
|
||||||
await message.answer(_execution_result_text("BUY execution", result, state))
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if command == "sell":
|
if command == "sell":
|
||||||
_prepare_ready_signal(state=state, signal="SELL", confidence=0.95)
|
state, result = service.open_short()
|
||||||
result = engine.process(state)
|
await message.answer(_execution_result_text("EXEC SELL / SHORT", state, result))
|
||||||
await _after_debug_execution()
|
|
||||||
await message.answer(_execution_result_text("SELL execution", result, state))
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if command == "flip":
|
if command == "flip":
|
||||||
position = engine.get_position()
|
state, result = service.flip()
|
||||||
current_side = position.side or state.position_side or "NONE"
|
await message.answer(_execution_result_text("EXEC AUTO FLIP", state, result))
|
||||||
|
|
||||||
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))
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if command == "close":
|
if command == "close":
|
||||||
result = engine._close_position(state, forced_reason="DEBUG_CLOSE")
|
state, result = service.close(reason="DEBUG_CLOSE")
|
||||||
await _after_debug_execution()
|
await message.answer(_execution_result_text("EXEC CLOSE", state, result))
|
||||||
await message.answer(_execution_result_text("CLOSE execution", result, state))
|
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
|
return
|
||||||
|
|
||||||
await message.answer(f"⛔️ Неизвестная команда: {command}\n\n{_debug_help_text()}")
|
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 выключен.")
|
await message.answer("Debug mode выключен.")
|
||||||
return
|
return
|
||||||
|
|
||||||
parts = (message.text or "").split()
|
await message.answer(
|
||||||
command = parts[1].lower() if len(parts) > 1 else "help"
|
"⚠️ /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()
|
state = service.get_state()
|
||||||
engine = ExecutionEngine()
|
service.update_market()
|
||||||
|
|
||||||
if command in {"help", "-h", "--help"}:
|
await message.answer(_debug_state_text(state))
|
||||||
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()}")
|
|
||||||
|
|
||||||
|
|
||||||
def _prepare_ready_signal(*, state, signal: str, confidence: float) -> None:
|
def _debug_state_text(state: DebugTradeState) -> str:
|
||||||
state.status = "RUNNING"
|
position = state.position
|
||||||
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)
|
|
||||||
|
|
||||||
|
duration = _signal_duration_text(state)
|
||||||
def _set_signal_state(
|
pnl = position.unrealized_pnl_usd
|
||||||
*,
|
|
||||||
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)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
f"✅ Debug {title}\n\n"
|
"<b>[DEBUG] Auto State</b>\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"
|
|
||||||
f"Status: {state.status}\n"
|
f"Status: {state.status}\n"
|
||||||
f"Symbol: {state.symbol}\n"
|
f"Symbol: {state.symbol}\n"
|
||||||
f"Strategy: {state.strategy}\n"
|
f"Strategy: {state.strategy}\n"
|
||||||
f"Risk: {state.risk_percent}\n"
|
f"Risk: {state.risk_percent}\n"
|
||||||
f"Leverage: {state.leverage}\n\n"
|
f"Leverage: {state.leverage}\n"
|
||||||
f"Signal: {state.last_signal}\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"Repeats: {state.last_signal_repeat_count}\n"
|
||||||
f"Confidence: {state.last_signal_confidence:.2f}\n"
|
f"Confidence: {state.last_signal_confidence:.2f}\n"
|
||||||
f"Decision: {state.decision_status}\n"
|
f"Decision: {state.decision_status}\n"
|
||||||
f"Ready: {state.is_signal_ready}\n"
|
f"Ready: {state.is_signal_ready}\n"
|
||||||
f"Signal started at: {getattr(state, 'signal_started_at', None)}\n\n"
|
f"Reason: {state.last_signal_reason or '—'}\n\n"
|
||||||
f"<b>Runner</b>\n"
|
f"<b>Risk</b>\n"
|
||||||
f"Screen: {AutoTradeRunner._current_screen}\n"
|
f"SL: {_format_percent(state.stop_loss_percent)}\n"
|
||||||
f"Chat ID: {AutoTradeRunner._chat_id}\n"
|
f"TP: {_format_percent(state.take_profit_percent)}\n"
|
||||||
f"Message ID: {AutoTradeRunner._message_id}\n"
|
f"ML: {_format_usd_or_off(state.max_loss_usd)}\n"
|
||||||
f"Has bot: {AutoTradeRunner._bot is not None}\n"
|
f"Max Reserved: {_format_percent(state.max_reserved_balance_percent)}\n"
|
||||||
f"Has render_text: {AutoTradeRunner._render_text is not None}\n"
|
f"Block: {state.execution_block_reason or '—'}\n"
|
||||||
f"Task running: {runner_task_running}\n\n"
|
f"Adjustment: {state.execution_size_adjustment_reason or '—'}\n\n"
|
||||||
f"<b>Position</b>\n"
|
f"<b>Position</b>\n"
|
||||||
f"Side: {state.position_side}\n"
|
f"Side: {position.side}\n"
|
||||||
f"Entry: {state.entry_price}\n"
|
f"Entry: {_format_usd_or_dash(position.entry_price)}\n"
|
||||||
f"Size: {state.position_size}\n"
|
f"Size: {_format_crypto_size(position.size)}\n"
|
||||||
f"PnL: {state.unrealized_pnl_usd}"
|
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
|
return signal, confidence, repeat_count, None
|
||||||
|
|
||||||
|
|
||||||
@router.message(F.text.startswith("/debug_signal"))
|
def _parse_int(parts: list[str], *, index: int, default: int) -> int:
|
||||||
async def debug_signal(message: Message) -> None:
|
try:
|
||||||
if not _debug_enabled():
|
return int(parts[index])
|
||||||
await message.answer("Debug mode выключен.")
|
except (IndexError, TypeError, ValueError):
|
||||||
return
|
return default
|
||||||
|
|
||||||
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}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.message(F.text == "/debug_ready")
|
def _parse_float(parts: list[str], *, index: int, default: float) -> float:
|
||||||
async def debug_ready(message: Message) -> None:
|
try:
|
||||||
if not _debug_enabled():
|
return float(parts[index])
|
||||||
await message.answer("Debug mode выключен.")
|
except (IndexError, TypeError, ValueError):
|
||||||
return
|
return default
|
||||||
|
|
||||||
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}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.message(F.text == "/debug_state")
|
def _signal_duration_text(state: DebugTradeState) -> str:
|
||||||
async def debug_state(message: Message) -> None:
|
started_at = state.signal_started_at
|
||||||
if not _debug_enabled():
|
|
||||||
await message.answer("Debug mode выключен.")
|
|
||||||
return
|
|
||||||
|
|
||||||
state = AutoTradeService().get_state()
|
if started_at is not None:
|
||||||
_sync_state_from_position(state)
|
total_seconds = max(0, int(__import__("time").monotonic() - float(started_at)))
|
||||||
await message.answer(_debug_state_text(state))
|
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 aiogram import Dispatcher
|
||||||
|
|
||||||
from src.telegram.handlers.auto import router as auto_router
|
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.home import router as home_router
|
||||||
from src.telegram.handlers.journal import router as journal_router
|
from src.telegram.handlers.journal import router as journal_router
|
||||||
from src.telegram.handlers.market import router as market_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.system import router as system_router
|
||||||
from src.telegram.handlers.trade.main import router as trade_main_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.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:
|
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(trade_new_order_router)
|
||||||
dispatcher.include_router(auto_router)
|
dispatcher.include_router(auto_router)
|
||||||
dispatcher.include_router(journal_router)
|
dispatcher.include_router(journal_router)
|
||||||
|
dispatcher.include_router(debug_auto_router)
|
||||||
dispatcher.include_router(debug_router)
|
dispatcher.include_router(debug_router)
|
||||||
dispatcher.include_router(system_router)
|
dispatcher.include_router(system_router)
|
||||||
1
app/src/trading/debug/__init__.py
Normal file
1
app/src/trading/debug/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from __future__ import annotations
|
||||||
443
app/src/trading/debug/execution.py
Normal file
443
app/src/trading/debug/execution.py
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
# app/src/trading/debug/execution.py
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from src.integrations.exchange.service import ExchangeService
|
||||||
|
from src.trading.debug.state import DebugPositionState, DebugTradeState
|
||||||
|
from src.trading.execution.models import ExecutionDecision
|
||||||
|
|
||||||
|
|
||||||
|
class DebugExecutionEngine:
|
||||||
|
_size_precision = 5
|
||||||
|
|
||||||
|
def process(self, state: DebugTradeState) -> ExecutionDecision:
|
||||||
|
if state.status != "RUNNING":
|
||||||
|
return ExecutionDecision(
|
||||||
|
"NONE",
|
||||||
|
False,
|
||||||
|
"[DEBUG] Execution доступен только в режиме RUNNING.",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.update_unrealized_pnl(state)
|
||||||
|
|
||||||
|
risk_decision = self.risk_close_decision(state)
|
||||||
|
if risk_decision is not None:
|
||||||
|
return risk_decision
|
||||||
|
|
||||||
|
if state.decision_status != "READY" or not state.is_signal_ready:
|
||||||
|
return ExecutionDecision(
|
||||||
|
"NONE",
|
||||||
|
False,
|
||||||
|
"[DEBUG] Сигнал ещё не готов к execution.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._should_flip_position(state):
|
||||||
|
return self.flip_position(state)
|
||||||
|
|
||||||
|
if state.last_signal == "BUY":
|
||||||
|
return self.open_position_if_empty(state=state, side="LONG")
|
||||||
|
|
||||||
|
if state.last_signal == "SELL":
|
||||||
|
return self.open_position_if_empty(state=state, side="SHORT")
|
||||||
|
|
||||||
|
return ExecutionDecision("NONE", False, "[DEBUG] Нет торгового действия.")
|
||||||
|
|
||||||
|
def open_position_if_empty(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
state: DebugTradeState,
|
||||||
|
side: str,
|
||||||
|
) -> ExecutionDecision:
|
||||||
|
if state.position.side != "NONE":
|
||||||
|
return ExecutionDecision("NONE", False, "[DEBUG] Позиция уже открыта.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
entry_price = self._entry_price_for_side(state.symbol, side)
|
||||||
|
except Exception as exc:
|
||||||
|
return ExecutionDecision(
|
||||||
|
"NONE",
|
||||||
|
False,
|
||||||
|
f"[DEBUG] Не удалось получить цену входа: {exc}",
|
||||||
|
)
|
||||||
|
|
||||||
|
size = self.calculate_position_size(state, entry_price=entry_price)
|
||||||
|
|
||||||
|
if size <= 0:
|
||||||
|
return ExecutionDecision(
|
||||||
|
"NONE",
|
||||||
|
False,
|
||||||
|
"[DEBUG] Позиция не открыта: невозможно рассчитать size.",
|
||||||
|
)
|
||||||
|
|
||||||
|
size = self.adjust_size_by_margin_limit(
|
||||||
|
state=state,
|
||||||
|
entry_price=entry_price,
|
||||||
|
size=size,
|
||||||
|
)
|
||||||
|
size = self._round_size(size)
|
||||||
|
|
||||||
|
if size <= 0:
|
||||||
|
return ExecutionDecision(
|
||||||
|
"NONE",
|
||||||
|
False,
|
||||||
|
"[DEBUG] Позиция не открыта: итоговый size равен 0.",
|
||||||
|
)
|
||||||
|
|
||||||
|
now = self._now_time()
|
||||||
|
|
||||||
|
state.position = DebugPositionState(
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ExecutionDecision(
|
||||||
|
f"DEBUG_OPEN_{side}",
|
||||||
|
True,
|
||||||
|
f"[DEBUG] Paper позиция открыта: {side}.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def flip_position(self, state: DebugTradeState) -> ExecutionDecision:
|
||||||
|
position = state.position
|
||||||
|
|
||||||
|
if position.side == "NONE":
|
||||||
|
return ExecutionDecision("NONE", False, "[DEBUG] Нет позиции для flip.")
|
||||||
|
|
||||||
|
new_side = self._target_side_from_signal(state.last_signal)
|
||||||
|
if new_side is None:
|
||||||
|
return ExecutionDecision("NONE", False, "[DEBUG] Нет направления для flip.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
exit_price = self._exit_price_for_side(
|
||||||
|
position.symbol or state.symbol,
|
||||||
|
position.side,
|
||||||
|
)
|
||||||
|
new_entry_price = self._entry_price_for_side(state.symbol, new_side)
|
||||||
|
except Exception as exc:
|
||||||
|
return ExecutionDecision("NONE", False, f"[DEBUG] Ошибка цены для flip: {exc}")
|
||||||
|
|
||||||
|
pnl = self.calculate_pnl(state, exit_price)
|
||||||
|
new_size = self.calculate_position_size(state, entry_price=new_entry_price)
|
||||||
|
|
||||||
|
if new_size <= 0:
|
||||||
|
return ExecutionDecision(
|
||||||
|
"NONE",
|
||||||
|
False,
|
||||||
|
"[DEBUG] Flip отменён: невозможно рассчитать new size.",
|
||||||
|
)
|
||||||
|
|
||||||
|
new_size = self.adjust_size_by_margin_limit(
|
||||||
|
state=state,
|
||||||
|
entry_price=new_entry_price,
|
||||||
|
size=new_size,
|
||||||
|
)
|
||||||
|
new_size = self._round_size(new_size)
|
||||||
|
|
||||||
|
if new_size <= 0:
|
||||||
|
return ExecutionDecision(
|
||||||
|
"NONE",
|
||||||
|
False,
|
||||||
|
"[DEBUG] Flip отменён: итоговый new size равен 0.",
|
||||||
|
)
|
||||||
|
|
||||||
|
state.realized_pnl_usd += pnl
|
||||||
|
|
||||||
|
now = self._now_time()
|
||||||
|
old_side = position.side
|
||||||
|
|
||||||
|
state.position = DebugPositionState(
|
||||||
|
side=new_side,
|
||||||
|
symbol=state.symbol,
|
||||||
|
entry_price=new_entry_price,
|
||||||
|
size=new_size,
|
||||||
|
leverage=state.leverage,
|
||||||
|
unrealized_pnl_usd=0.0,
|
||||||
|
opened_at=now,
|
||||||
|
updated_at=now,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ExecutionDecision(
|
||||||
|
f"DEBUG_FLIP_{old_side}_TO_{new_side}",
|
||||||
|
True,
|
||||||
|
f"[DEBUG] Flip выполнен: {old_side} → {new_side}.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def close_position(
|
||||||
|
self,
|
||||||
|
state: DebugTradeState,
|
||||||
|
*,
|
||||||
|
forced_reason: str | None = None,
|
||||||
|
) -> ExecutionDecision:
|
||||||
|
position = state.position
|
||||||
|
|
||||||
|
if position.side == "NONE":
|
||||||
|
return ExecutionDecision(
|
||||||
|
"NONE",
|
||||||
|
False,
|
||||||
|
"[DEBUG] Нет открытой позиции для закрытия.",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
exit_price = self._exit_price_for_side(
|
||||||
|
position.symbol or state.symbol,
|
||||||
|
position.side,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
return ExecutionDecision("NONE", False, f"[DEBUG] Ошибка цены закрытия: {exc}")
|
||||||
|
|
||||||
|
pnl = self.calculate_pnl(state, exit_price)
|
||||||
|
state.realized_pnl_usd += pnl
|
||||||
|
|
||||||
|
old_side = position.side
|
||||||
|
state.position = DebugPositionState()
|
||||||
|
|
||||||
|
action = f"DEBUG_CLOSE_{forced_reason}" if forced_reason else "DEBUG_CLOSE"
|
||||||
|
|
||||||
|
return ExecutionDecision(
|
||||||
|
action,
|
||||||
|
True,
|
||||||
|
f"[DEBUG] Позиция закрыта: {old_side}. PnL: {pnl:.4f}",
|
||||||
|
)
|
||||||
|
|
||||||
|
def risk_close_decision(self, state: DebugTradeState) -> ExecutionDecision | None:
|
||||||
|
position = state.position
|
||||||
|
|
||||||
|
if position.side == "NONE":
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
current_price = self._exit_price_for_side(
|
||||||
|
position.symbol or state.symbol,
|
||||||
|
position.side,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
price_move_percent = self.calculate_price_move_percent(state, current_price)
|
||||||
|
unrealized_pnl = self.calculate_pnl(state, current_price)
|
||||||
|
|
||||||
|
if self._is_max_loss_hit(state, unrealized_pnl):
|
||||||
|
return self.close_position(state, forced_reason="MAX_LOSS")
|
||||||
|
|
||||||
|
if self._is_stop_loss_hit(state, price_move_percent):
|
||||||
|
return self.close_position(state, forced_reason="STOP_LOSS")
|
||||||
|
|
||||||
|
if self._is_take_profit_hit(state, price_move_percent):
|
||||||
|
return self.close_position(state, forced_reason="TAKE_PROFIT")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def update_unrealized_pnl(self, state: DebugTradeState) -> None:
|
||||||
|
position = state.position
|
||||||
|
|
||||||
|
if position.side == "NONE":
|
||||||
|
position.unrealized_pnl_usd = None
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
current_price = self._exit_price_for_side(
|
||||||
|
position.symbol or state.symbol,
|
||||||
|
position.side,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
position.unrealized_pnl_usd = self.calculate_pnl(state, current_price)
|
||||||
|
position.updated_at = self._now_time()
|
||||||
|
|
||||||
|
def calculate_position_size(
|
||||||
|
self,
|
||||||
|
state: DebugTradeState,
|
||||||
|
*,
|
||||||
|
entry_price: float | None = None,
|
||||||
|
) -> float:
|
||||||
|
if state.risk_percent is None or state.risk_percent <= 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
if state.stop_loss_percent is None or state.stop_loss_percent <= 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
price = entry_price
|
||||||
|
if price is None:
|
||||||
|
price = self._signal_entry_price(state)
|
||||||
|
|
||||||
|
if price <= 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
target_risk_usd = state.allocated_balance_usd * (state.risk_percent / 100)
|
||||||
|
stop_loss_distance_usd = price * (state.stop_loss_percent / 100)
|
||||||
|
|
||||||
|
if stop_loss_distance_usd <= 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
return self._round_size(target_risk_usd / stop_loss_distance_usd)
|
||||||
|
|
||||||
|
def adjust_size_by_margin_limit(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
state: DebugTradeState,
|
||||||
|
entry_price: float,
|
||||||
|
size: float,
|
||||||
|
) -> float:
|
||||||
|
state.execution_block_reason = None
|
||||||
|
state.execution_size_adjustment_reason = None
|
||||||
|
|
||||||
|
max_percent = state.max_reserved_balance_percent
|
||||||
|
if max_percent is None or max_percent <= 0:
|
||||||
|
return self._round_size(size)
|
||||||
|
|
||||||
|
leverage = state.leverage or 1.0
|
||||||
|
|
||||||
|
if leverage <= 0 or entry_price <= 0:
|
||||||
|
state.execution_block_reason = "[DEBUG] Invalid leverage or entry price."
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
max_reserved_usd = state.allocated_balance_usd * (max_percent / 100)
|
||||||
|
max_notional_usd = max_reserved_usd * leverage
|
||||||
|
max_size = max_notional_usd / entry_price
|
||||||
|
|
||||||
|
if size <= max_size:
|
||||||
|
return self._round_size(size)
|
||||||
|
|
||||||
|
state.execution_size_adjustment_reason = "MARGIN_LIMIT"
|
||||||
|
return self._round_size(max_size)
|
||||||
|
|
||||||
|
def calculate_price_move_percent(
|
||||||
|
self,
|
||||||
|
state: DebugTradeState,
|
||||||
|
current_price: float,
|
||||||
|
) -> float:
|
||||||
|
position = state.position
|
||||||
|
entry = position.entry_price or 0.0
|
||||||
|
|
||||||
|
if entry <= 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
if position.side == "LONG":
|
||||||
|
return round(((current_price - entry) / entry) * 100, 4)
|
||||||
|
|
||||||
|
if position.side == "SHORT":
|
||||||
|
return round(((entry - current_price) / entry) * 100, 4)
|
||||||
|
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def calculate_pnl(self, state: DebugTradeState, current_price: float) -> float:
|
||||||
|
position = state.position
|
||||||
|
|
||||||
|
entry = position.entry_price or 0.0
|
||||||
|
size = position.size or 0.0
|
||||||
|
|
||||||
|
if position.side == "LONG":
|
||||||
|
return round((current_price - entry) * size, 4)
|
||||||
|
|
||||||
|
if position.side == "SHORT":
|
||||||
|
return round((entry - current_price) * size, 4)
|
||||||
|
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def _should_flip_position(self, state: DebugTradeState) -> bool:
|
||||||
|
if state.position.side == "LONG" and state.last_signal == "SELL":
|
||||||
|
return True
|
||||||
|
|
||||||
|
if state.position.side == "SHORT" and state.last_signal == "BUY":
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _target_side_from_signal(self, signal: str | None) -> str | None:
|
||||||
|
if signal == "BUY":
|
||||||
|
return "LONG"
|
||||||
|
|
||||||
|
if signal == "SELL":
|
||||||
|
return "SHORT"
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _is_stop_loss_hit(self, state: DebugTradeState, price_move_percent: float) -> bool:
|
||||||
|
if state.stop_loss_percent is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return price_move_percent <= -abs(state.stop_loss_percent)
|
||||||
|
|
||||||
|
def _is_take_profit_hit(self, state: DebugTradeState, price_move_percent: float) -> bool:
|
||||||
|
if state.take_profit_percent is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return price_move_percent >= abs(state.take_profit_percent)
|
||||||
|
|
||||||
|
def _is_max_loss_hit(self, state: DebugTradeState, unrealized_pnl: float) -> bool:
|
||||||
|
if state.max_loss_usd is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return unrealized_pnl <= -abs(state.max_loss_usd)
|
||||||
|
|
||||||
|
def _signal_entry_price(self, state: DebugTradeState) -> float:
|
||||||
|
if state.last_signal == "BUY":
|
||||||
|
return self._entry_price_for_side(state.symbol, "LONG")
|
||||||
|
|
||||||
|
if state.last_signal == "SELL":
|
||||||
|
return self._entry_price_for_side(state.symbol, "SHORT")
|
||||||
|
|
||||||
|
return self._market_last_price(state.symbol)
|
||||||
|
|
||||||
|
def _entry_price_for_side(self, symbol: str, side: str) -> float:
|
||||||
|
snapshot = ExchangeService().get_fresh_market_snapshot(symbol)
|
||||||
|
|
||||||
|
if side == "LONG":
|
||||||
|
return self._snapshot_price(snapshot, "ask_price", "last_price")
|
||||||
|
|
||||||
|
if side == "SHORT":
|
||||||
|
return self._snapshot_price(snapshot, "bid_price", "last_price")
|
||||||
|
|
||||||
|
return self._snapshot_price(snapshot, "last_price")
|
||||||
|
|
||||||
|
def _exit_price_for_side(self, symbol: str, side: str) -> float:
|
||||||
|
snapshot = ExchangeService().get_fresh_market_snapshot(symbol)
|
||||||
|
|
||||||
|
if side == "LONG":
|
||||||
|
return self._snapshot_price(snapshot, "bid_price", "last_price")
|
||||||
|
|
||||||
|
if side == "SHORT":
|
||||||
|
return self._snapshot_price(snapshot, "ask_price", "last_price")
|
||||||
|
|
||||||
|
return self._snapshot_price(snapshot, "last_price")
|
||||||
|
|
||||||
|
def _market_last_price(self, symbol: str) -> float:
|
||||||
|
snapshot = ExchangeService().get_fresh_market_snapshot(symbol)
|
||||||
|
return self._snapshot_price(snapshot, "last_price")
|
||||||
|
|
||||||
|
def _snapshot_price(
|
||||||
|
self,
|
||||||
|
snapshot: dict[str, object],
|
||||||
|
primary_key: str,
|
||||||
|
fallback_key: str | None = None,
|
||||||
|
) -> float:
|
||||||
|
raw_price = snapshot.get(primary_key)
|
||||||
|
|
||||||
|
if raw_price is None and fallback_key is not None:
|
||||||
|
raw_price = snapshot.get(fallback_key)
|
||||||
|
|
||||||
|
if raw_price is None:
|
||||||
|
raise ValueError(f"Market snapshot price '{primary_key}' is missing.")
|
||||||
|
|
||||||
|
price = float(raw_price)
|
||||||
|
|
||||||
|
if price <= 0:
|
||||||
|
raise ValueError(f"Market snapshot price '{primary_key}' is invalid: {price}")
|
||||||
|
|
||||||
|
return price
|
||||||
|
|
||||||
|
def _round_size(self, size: float) -> float:
|
||||||
|
factor = 10 ** self._size_precision
|
||||||
|
return math.floor(float(size) * factor) / factor
|
||||||
|
|
||||||
|
def _now_time(self) -> str:
|
||||||
|
return datetime.now().strftime("%H:%M:%S")
|
||||||
170
app/src/trading/debug/runner.py
Normal file
170
app/src/trading/debug/runner.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
# app/src/trading/debug/runner.py
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from aiogram import Bot
|
||||||
|
from aiogram.exceptions import TelegramBadRequest, TelegramRetryAfter
|
||||||
|
|
||||||
|
from src.trading.debug.service import DebugTradeService
|
||||||
|
|
||||||
|
|
||||||
|
class DebugTradeRunner:
|
||||||
|
_task: asyncio.Task | None = None
|
||||||
|
|
||||||
|
_bot: Bot | None = None
|
||||||
|
_chat_id: int | None = None
|
||||||
|
_message_id: int | None = None
|
||||||
|
_render_text: Callable[[], str] | None = None
|
||||||
|
_render_markup: Callable[[], object] | None = None
|
||||||
|
|
||||||
|
_current_screen: str | None = None
|
||||||
|
|
||||||
|
_interval_seconds = 5
|
||||||
|
_last_text: str | None = None
|
||||||
|
_last_refresh_at: float = 0.0
|
||||||
|
_retry_after_until: float = 0.0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register_screen(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
bot: Bot,
|
||||||
|
chat_id: int,
|
||||||
|
message_id: int,
|
||||||
|
render_text: Callable[[], str],
|
||||||
|
render_markup: Callable[[], object],
|
||||||
|
) -> None:
|
||||||
|
cls._bot = bot
|
||||||
|
cls._chat_id = chat_id
|
||||||
|
cls._message_id = message_id
|
||||||
|
cls._render_text = render_text
|
||||||
|
cls._render_markup = render_markup
|
||||||
|
cls._last_text = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def delete_registered_screen(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
bot: Bot,
|
||||||
|
chat_id: int,
|
||||||
|
) -> None:
|
||||||
|
if cls._chat_id is None or cls._message_id is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if cls._chat_id != chat_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await bot.delete_message(
|
||||||
|
chat_id=cls._chat_id,
|
||||||
|
message_id=cls._message_id,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
cls._message_id = None
|
||||||
|
cls._render_text = None
|
||||||
|
cls._render_markup = None
|
||||||
|
cls._last_text = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set_current_screen(cls, screen: str) -> None:
|
||||||
|
cls._current_screen = screen
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def start(cls) -> None:
|
||||||
|
state = DebugTradeService().get_state()
|
||||||
|
state.status = "RUNNING"
|
||||||
|
|
||||||
|
if cls._task is not None and not cls._task.done():
|
||||||
|
return
|
||||||
|
|
||||||
|
cls._task = asyncio.create_task(cls._worker())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def stop(cls) -> None:
|
||||||
|
if cls._task is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
cls._task.cancel()
|
||||||
|
cls._task = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _worker(cls) -> None:
|
||||||
|
service = DebugTradeService()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
state = service.get_state()
|
||||||
|
|
||||||
|
if state.status == "OFF":
|
||||||
|
cls._task = None
|
||||||
|
break
|
||||||
|
|
||||||
|
service.process()
|
||||||
|
await cls.refresh_screen(force=False)
|
||||||
|
|
||||||
|
await asyncio.sleep(cls._interval_seconds)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def refresh_screen(cls, *, force: bool = False) -> None:
|
||||||
|
if cls._current_screen != "debug_auto":
|
||||||
|
return
|
||||||
|
|
||||||
|
now = time.monotonic()
|
||||||
|
|
||||||
|
if now < cls._retry_after_until:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not force and now - cls._last_refresh_at < cls._interval_seconds:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not all(
|
||||||
|
[
|
||||||
|
cls._bot,
|
||||||
|
cls._chat_id,
|
||||||
|
cls._message_id,
|
||||||
|
cls._render_text,
|
||||||
|
cls._render_markup,
|
||||||
|
]
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
text = cls._render_text()
|
||||||
|
|
||||||
|
if text == cls._last_text:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await cls._bot.edit_message_text(
|
||||||
|
chat_id=cls._chat_id,
|
||||||
|
message_id=cls._message_id,
|
||||||
|
text=text,
|
||||||
|
reply_markup=cls._render_markup(),
|
||||||
|
)
|
||||||
|
cls._last_text = text
|
||||||
|
cls._last_refresh_at = now
|
||||||
|
|
||||||
|
except TelegramRetryAfter as exc:
|
||||||
|
cls._retry_after_until = time.monotonic() + exc.retry_after + 5
|
||||||
|
|
||||||
|
except TelegramBadRequest as exc:
|
||||||
|
error_text = str(exc).lower()
|
||||||
|
|
||||||
|
if "message is not modified" in error_text:
|
||||||
|
cls._last_text = text
|
||||||
|
cls._last_refresh_at = now
|
||||||
|
return
|
||||||
|
|
||||||
|
if "message to edit not found" in error_text:
|
||||||
|
cls._message_id = None
|
||||||
|
cls._render_text = None
|
||||||
|
cls._render_markup = None
|
||||||
|
cls._last_text = None
|
||||||
|
return
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
219
app/src/trading/debug/service.py
Normal file
219
app/src/trading/debug/service.py
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
# app/src/trading/debug/service.py
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from src.core.config import load_settings
|
||||||
|
from src.trading.debug.execution import DebugExecutionEngine
|
||||||
|
from src.trading.debug.state import DebugPositionState, DebugTradeState
|
||||||
|
from src.trading.execution.models import ExecutionDecision
|
||||||
|
|
||||||
|
|
||||||
|
class DebugTradeService:
|
||||||
|
_state = DebugTradeState()
|
||||||
|
|
||||||
|
_confirm_repeats = 2
|
||||||
|
_ready_confidence = 0.3
|
||||||
|
|
||||||
|
def get_state(self) -> DebugTradeState:
|
||||||
|
if not self._state.symbol:
|
||||||
|
self._state.symbol = load_settings().default_symbol
|
||||||
|
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
def reset(self) -> DebugTradeState:
|
||||||
|
state = self.get_state()
|
||||||
|
|
||||||
|
state.status = "RUNNING"
|
||||||
|
state.last_signal = "HOLD"
|
||||||
|
state.last_signal_confidence = 0.0
|
||||||
|
state.last_signal_repeat_count = 1
|
||||||
|
state.last_signal_reason = "[DEBUG] RESET HOLD"
|
||||||
|
state.signal_started_at = time.monotonic()
|
||||||
|
|
||||||
|
state.decision_status = "WAITING"
|
||||||
|
state.decision_reason = "[DEBUG] Reset."
|
||||||
|
state.is_signal_confirmed = False
|
||||||
|
state.is_signal_ready = False
|
||||||
|
|
||||||
|
state.execution_block_reason = None
|
||||||
|
state.execution_size_adjustment_reason = None
|
||||||
|
|
||||||
|
state.position = DebugPositionState()
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
|
def stop(self) -> DebugTradeState:
|
||||||
|
state = self.get_state()
|
||||||
|
state.status = "OFF"
|
||||||
|
return state
|
||||||
|
|
||||||
|
def set_signal(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
signal: str,
|
||||||
|
confidence: float = 0.0,
|
||||||
|
repeat_count: int = 1,
|
||||||
|
reason: str | None = None,
|
||||||
|
force_ready: bool = False,
|
||||||
|
) -> DebugTradeState:
|
||||||
|
state = self.get_state()
|
||||||
|
|
||||||
|
normalized_signal = signal.strip().upper()
|
||||||
|
if normalized_signal not in {"BUY", "SELL", "HOLD"}:
|
||||||
|
normalized_signal = "HOLD"
|
||||||
|
|
||||||
|
previous_signal = state.last_signal
|
||||||
|
|
||||||
|
state.status = "RUNNING"
|
||||||
|
state.last_signal = normalized_signal
|
||||||
|
state.last_signal_confidence = max(0.0, min(1.0, confidence))
|
||||||
|
state.last_signal_repeat_count = max(1, int(repeat_count))
|
||||||
|
state.last_signal_reason = reason or f"[DEBUG] SIGNAL {normalized_signal}"
|
||||||
|
|
||||||
|
if previous_signal != normalized_signal or state.signal_started_at is None:
|
||||||
|
state.signal_started_at = time.monotonic()
|
||||||
|
|
||||||
|
self._update_decision_state(state, force_ready=force_ready)
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
|
def set_signal_duration(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
signal: str,
|
||||||
|
seconds: int,
|
||||||
|
confidence: float = 0.0,
|
||||||
|
force_ready: bool = False,
|
||||||
|
) -> DebugTradeState:
|
||||||
|
repeat_count = max(1, int(max(0, seconds) / 5))
|
||||||
|
|
||||||
|
state = self.set_signal(
|
||||||
|
signal=signal,
|
||||||
|
confidence=confidence,
|
||||||
|
repeat_count=repeat_count,
|
||||||
|
reason=f"[DEBUG] {signal.upper()} {seconds}s",
|
||||||
|
force_ready=force_ready,
|
||||||
|
)
|
||||||
|
|
||||||
|
state.signal_started_at = time.monotonic() - max(0, seconds)
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
|
def open_long(self) -> tuple[DebugTradeState, ExecutionDecision]:
|
||||||
|
state = self.set_signal(
|
||||||
|
signal="BUY",
|
||||||
|
confidence=0.95,
|
||||||
|
repeat_count=3,
|
||||||
|
reason="[DEBUG] OPEN LONG",
|
||||||
|
force_ready=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = DebugExecutionEngine().process(state)
|
||||||
|
return state, result
|
||||||
|
|
||||||
|
def open_short(self) -> tuple[DebugTradeState, ExecutionDecision]:
|
||||||
|
state = self.set_signal(
|
||||||
|
signal="SELL",
|
||||||
|
confidence=0.95,
|
||||||
|
repeat_count=3,
|
||||||
|
reason="[DEBUG] OPEN SHORT",
|
||||||
|
force_ready=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = DebugExecutionEngine().process(state)
|
||||||
|
return state, result
|
||||||
|
|
||||||
|
def flip(self) -> tuple[DebugTradeState, ExecutionDecision]:
|
||||||
|
state = self.get_state()
|
||||||
|
|
||||||
|
if state.position.side == "LONG":
|
||||||
|
target_signal = "SELL"
|
||||||
|
elif state.position.side == "SHORT":
|
||||||
|
target_signal = "BUY"
|
||||||
|
else:
|
||||||
|
return state, ExecutionDecision(
|
||||||
|
"NONE",
|
||||||
|
False,
|
||||||
|
"[DEBUG] Flip невозможен: нет открытой позиции.",
|
||||||
|
)
|
||||||
|
|
||||||
|
state = self.set_signal(
|
||||||
|
signal=target_signal,
|
||||||
|
confidence=0.95,
|
||||||
|
repeat_count=3,
|
||||||
|
reason="[DEBUG] AUTO FLIP",
|
||||||
|
force_ready=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = DebugExecutionEngine().process(state)
|
||||||
|
return state, result
|
||||||
|
|
||||||
|
def close(self, *, reason: str = "DEBUG_CLOSE") -> tuple[DebugTradeState, ExecutionDecision]:
|
||||||
|
state = self.get_state()
|
||||||
|
result = DebugExecutionEngine().close_position(state, forced_reason=reason)
|
||||||
|
return state, result
|
||||||
|
|
||||||
|
def update_market(self) -> DebugTradeState:
|
||||||
|
state = self.get_state()
|
||||||
|
|
||||||
|
state.last_check_at = datetime.now().strftime("%H:%M:%S")
|
||||||
|
|
||||||
|
if state.status != "RUNNING":
|
||||||
|
return state
|
||||||
|
|
||||||
|
DebugExecutionEngine().update_unrealized_pnl(state)
|
||||||
|
return state
|
||||||
|
|
||||||
|
def process(self) -> tuple[DebugTradeState, ExecutionDecision]:
|
||||||
|
state = self.get_state()
|
||||||
|
|
||||||
|
state.last_check_at = datetime.now().strftime("%H:%M:%S")
|
||||||
|
|
||||||
|
result = DebugExecutionEngine().process(state)
|
||||||
|
return state, result
|
||||||
|
|
||||||
|
def _update_decision_state(
|
||||||
|
self,
|
||||||
|
state: DebugTradeState,
|
||||||
|
*,
|
||||||
|
force_ready: bool = False,
|
||||||
|
) -> None:
|
||||||
|
state.is_signal_confirmed = False
|
||||||
|
state.is_signal_ready = False
|
||||||
|
|
||||||
|
if state.last_signal == "HOLD":
|
||||||
|
state.decision_status = "WAITING"
|
||||||
|
state.decision_reason = "[DEBUG] Нет торгового направления."
|
||||||
|
return
|
||||||
|
|
||||||
|
if force_ready:
|
||||||
|
state.is_signal_confirmed = True
|
||||||
|
state.is_signal_ready = True
|
||||||
|
state.decision_status = "READY"
|
||||||
|
state.decision_reason = "[DEBUG] Signal forced READY."
|
||||||
|
return
|
||||||
|
|
||||||
|
if state.last_signal_repeat_count < self._confirm_repeats:
|
||||||
|
state.decision_status = "CONFIRMING"
|
||||||
|
state.decision_reason = (
|
||||||
|
f"[DEBUG] Сигнал {state.last_signal} подтверждается: "
|
||||||
|
f"{state.last_signal_repeat_count}/{self._confirm_repeats}."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
state.is_signal_confirmed = True
|
||||||
|
|
||||||
|
if state.last_signal_confidence < self._ready_confidence:
|
||||||
|
state.decision_status = "BLOCKED"
|
||||||
|
state.decision_reason = (
|
||||||
|
f"[DEBUG] Confidence низкая: "
|
||||||
|
f"{state.last_signal_confidence:.2f} < {self._ready_confidence:.2f}."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
state.is_signal_ready = True
|
||||||
|
state.decision_status = "READY"
|
||||||
|
state.decision_reason = "[DEBUG] Signal ready."
|
||||||
57
app/src/trading/debug/state.py
Normal file
57
app/src/trading/debug/state.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# app/src/trading/debug/state.py
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class DebugPositionState:
|
||||||
|
side: str = "NONE"
|
||||||
|
symbol: str = ""
|
||||||
|
|
||||||
|
entry_price: float | None = None
|
||||||
|
size: float | None = None
|
||||||
|
leverage: float | None = None
|
||||||
|
|
||||||
|
unrealized_pnl_usd: float | None = None
|
||||||
|
|
||||||
|
opened_at: str | None = None
|
||||||
|
updated_at: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class DebugTradeState:
|
||||||
|
status: str = "OFF"
|
||||||
|
|
||||||
|
strategy: str | None = "TREND"
|
||||||
|
symbol: str = "BTC/USD_LEVERAGE"
|
||||||
|
|
||||||
|
allocated_balance_usd: float = 1000.0
|
||||||
|
realized_pnl_usd: float = 0.0
|
||||||
|
|
||||||
|
risk_percent: float | None = 1.0
|
||||||
|
leverage: float | None = 2.0
|
||||||
|
|
||||||
|
stop_loss_percent: float | None = 1.0
|
||||||
|
take_profit_percent: float | None = None
|
||||||
|
max_loss_usd: float | None = None
|
||||||
|
max_reserved_balance_percent: float | None = 50.0
|
||||||
|
|
||||||
|
last_signal: str | None = "HOLD"
|
||||||
|
last_signal_confidence: float = 0.0
|
||||||
|
last_signal_repeat_count: int = 0
|
||||||
|
last_signal_reason: str | None = None
|
||||||
|
signal_started_at: float | None = None
|
||||||
|
|
||||||
|
decision_status: str = "WAITING"
|
||||||
|
decision_reason: str | None = None
|
||||||
|
is_signal_confirmed: bool = False
|
||||||
|
is_signal_ready: bool = False
|
||||||
|
|
||||||
|
execution_block_reason: str | None = None
|
||||||
|
execution_size_adjustment_reason: str | None = None
|
||||||
|
|
||||||
|
position: DebugPositionState = field(default_factory=DebugPositionState)
|
||||||
|
|
||||||
|
last_check_at: str | None = None
|
||||||
@@ -266,6 +266,30 @@
|
|||||||
- integration testing flow for execution alerts
|
- integration testing flow for execution alerts
|
||||||
- preparation for isolated debug runtime architecture
|
- preparation for isolated debug runtime architecture
|
||||||
|
|
||||||
|
#### 07.4.3.15 — Isolated Debug Runtime & Debug Auto Screen ✅
|
||||||
|
|
||||||
|
- isolated DebugTradeState
|
||||||
|
- isolated DebugPositionState
|
||||||
|
- isolated DebugTradeService
|
||||||
|
- isolated DebugExecutionEngine
|
||||||
|
- isolated DebugTradeRunner
|
||||||
|
- separate `/debug_auto_screen`
|
||||||
|
- separate debug auto UI
|
||||||
|
- debug commands no longer mutate AutoTradeService
|
||||||
|
- debug execution no longer mutates ExecutionEngine._position
|
||||||
|
- debug runner no longer uses AutoTradeRunner
|
||||||
|
- `/debug_live` disabled as production-runtime injector
|
||||||
|
- legacy debug commands redirected to isolated debug runtime
|
||||||
|
- debug LONG / SHORT / FLIP / CLOSE sandbox flow
|
||||||
|
- debug Start / Stop / Reset controls
|
||||||
|
- debug PnL live refresh
|
||||||
|
- debug margin / reserved rendering
|
||||||
|
- debug bid / ask execution pricing
|
||||||
|
- fresh REST snapshot support for debug execution
|
||||||
|
- debug_auto router added
|
||||||
|
- ordinary 🤖 Автоторговля screen remains unchanged by debug commands
|
||||||
|
- preparation for production execution pricing layer
|
||||||
|
|
||||||
### 07.4.4
|
### 07.4.4
|
||||||
⏳ Grid Strategy
|
⏳ Grid Strategy
|
||||||
|
|
||||||
|
|||||||
@@ -253,6 +253,29 @@
|
|||||||
- integration testing flow for execution alerts
|
- integration testing flow for execution alerts
|
||||||
- preparation for isolated debug runtime architecture
|
- preparation for isolated debug runtime architecture
|
||||||
|
|
||||||
|
#### 07.4.3.15 — Isolated Debug Runtime & Debug Auto Screen ✅
|
||||||
|
- isolated DebugTradeState
|
||||||
|
- isolated DebugPositionState
|
||||||
|
- isolated DebugTradeService
|
||||||
|
- isolated DebugExecutionEngine
|
||||||
|
- isolated DebugTradeRunner
|
||||||
|
- separate `/debug_auto_screen`
|
||||||
|
- separate debug auto UI
|
||||||
|
- debug commands no longer mutate AutoTradeService
|
||||||
|
- debug execution no longer mutates ExecutionEngine._position
|
||||||
|
- debug runner no longer uses AutoTradeRunner
|
||||||
|
- `/debug_live` disabled as production-runtime injector
|
||||||
|
- legacy debug commands redirected to isolated debug runtime
|
||||||
|
- debug LONG / SHORT / FLIP / CLOSE sandbox flow
|
||||||
|
- debug Start / Stop / Reset controls
|
||||||
|
- debug PnL live refresh
|
||||||
|
- debug margin / reserved rendering
|
||||||
|
- debug bid / ask execution pricing
|
||||||
|
- fresh REST snapshot support for debug execution
|
||||||
|
- debug_auto router added
|
||||||
|
- ordinary 🤖 Автоторговля screen remains unchanged by debug commands
|
||||||
|
- preparation for production execution pricing layer
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 07.4.4
|
### 07.4.4
|
||||||
|
|||||||
176
docs/stages/stage-07_4_3_15-isolated_debug_runtime.md
Normal file
176
docs/stages/stage-07_4_3_15-isolated_debug_runtime.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# 07.4.3.15 — Isolated Debug Runtime & Debug Auto Screen
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
|
||||||
|
Изолировать debug-режим от обычной автоторговли и добавить отдельный экран:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/debug_auto_screen
|
||||||
|
```
|
||||||
|
|
||||||
|
Теперь debug-команды больше не должны изменять runtime обычной автоторговли.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что было раньше
|
||||||
|
|
||||||
|
Ранее команды:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/debug_exec
|
||||||
|
/debug_live
|
||||||
|
/debug_auto
|
||||||
|
```
|
||||||
|
|
||||||
|
работали напрямую с обычными компонентами автоторговли:
|
||||||
|
|
||||||
|
```text
|
||||||
|
AutoTradeService
|
||||||
|
ExecutionEngine
|
||||||
|
AutoTradeRunner
|
||||||
|
Auto UI
|
||||||
|
```
|
||||||
|
|
||||||
|
Это было удобно для integration testing, но могло загрязнять реальное состояние автоторговли.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что реализовано
|
||||||
|
|
||||||
|
### 1. Отдельный debug state
|
||||||
|
|
||||||
|
Добавлен отдельный debug state:
|
||||||
|
|
||||||
|
```text
|
||||||
|
app/src/trading/debug/state.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Основные структуры:
|
||||||
|
|
||||||
|
```text
|
||||||
|
DebugTradeState
|
||||||
|
DebugPositionState
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Отдельный debug execution engine
|
||||||
|
|
||||||
|
Добавлен:
|
||||||
|
|
||||||
|
```text
|
||||||
|
app/src/trading/debug/execution.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Важно:
|
||||||
|
|
||||||
|
```text
|
||||||
|
DebugExecutionEngine не трогает ExecutionEngine._position
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Отдельный debug service
|
||||||
|
|
||||||
|
Добавлен:
|
||||||
|
|
||||||
|
```text
|
||||||
|
app/src/trading/debug/service.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Важно:
|
||||||
|
|
||||||
|
```text
|
||||||
|
DebugTradeService не трогает AutoTradeService
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Переделаны debug-команды
|
||||||
|
|
||||||
|
Файл:
|
||||||
|
|
||||||
|
```text
|
||||||
|
app/src/telegram/handlers/debug.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Теперь команды работают через изолированный debug runtime.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Отключён старый /debug_live
|
||||||
|
|
||||||
|
Команда:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/debug_live
|
||||||
|
```
|
||||||
|
|
||||||
|
больше не вмешивается в обычную автоторговлю.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Отдельный debug runner
|
||||||
|
|
||||||
|
Добавлен:
|
||||||
|
|
||||||
|
```text
|
||||||
|
app/src/trading/debug/runner.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Он обновляет только debug screen:
|
||||||
|
|
||||||
|
```text
|
||||||
|
🧪 [DEBUG] Автоторговля
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Отдельный debug auto screen
|
||||||
|
|
||||||
|
Добавлены файлы:
|
||||||
|
|
||||||
|
```text
|
||||||
|
app/src/telegram/handlers/debug_auto/main.py
|
||||||
|
app/src/telegram/handlers/debug_auto/ui.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Открытие экрана:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/debug_auto_screen
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. Fresh REST snapshot для debug execution
|
||||||
|
|
||||||
|
Для debug execution добавлен fresh REST snapshot:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ExchangeService.get_fresh_market_snapshot()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Архитектурный результат
|
||||||
|
|
||||||
|
Теперь debug runtime отделён от обычной автоторговли.
|
||||||
|
|
||||||
|
Было:
|
||||||
|
|
||||||
|
```text
|
||||||
|
debug commands → AutoTradeService / ExecutionEngine / AutoTradeRunner
|
||||||
|
```
|
||||||
|
|
||||||
|
Стало:
|
||||||
|
|
||||||
|
```text
|
||||||
|
debug commands → DebugTradeService / DebugExecutionEngine / DebugTradeRunner
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Следующий этап
|
||||||
|
|
||||||
|
07.4.3.16 — Production Execution Pricing Layer
|
||||||
Reference in New Issue
Block a user