Stage 07.4.3.5 — Debug commands & test mode

This commit is contained in:
2026-05-03 11:13:19 +03:00
parent af2d27761f
commit 8adfab7220
10 changed files with 917 additions and 26 deletions

View File

@@ -48,6 +48,9 @@ class Settings:
db_user: str db_user: str
db_password: str db_password: str
# Debag helper
debug_enabled: bool
# helper: demo/live mode # helper: demo/live mode
def is_demo_mode(self) -> bool: def is_demo_mode(self) -> bool:
return "demo" in self.exchange_base_url.lower() return "demo" in self.exchange_base_url.lower()
@@ -87,6 +90,7 @@ def load_settings() -> Settings:
app_env=os.getenv("APP_ENV", "dev").strip() or "dev", app_env=os.getenv("APP_ENV", "dev").strip() or "dev",
log_level=os.getenv("LOG_LEVEL", "INFO").strip().upper() or "INFO", log_level=os.getenv("LOG_LEVEL", "INFO").strip().upper() or "INFO",
tz=os.getenv("TZ", "Europe/Minsk").strip() or "Europe/Minsk", tz=os.getenv("TZ", "Europe/Minsk").strip() or "Europe/Minsk",
debug_enabled=_parse_bool(os.getenv("DEBUG_ENABLED", "false")),
# Exchange # Exchange
exchange_enabled=_parse_bool(os.getenv("EXCHANGE_ENABLED", "false")), exchange_enabled=_parse_bool(os.getenv("EXCHANGE_ENABLED", "false")),

View File

@@ -0,0 +1,124 @@
# app/src/telegram/handlers/debug.py
from __future__ import annotations
from aiogram import F, Router
from aiogram.types import Message
from src.core.config import load_settings
from src.trading.auto.runner import AutoTradeRunner
from src.trading.auto.service import AutoTradeService
from src.trading.journal.service import JournalService
from src.trading.execution.engine import ExecutionEngine
router = Router(name="debug")
def _debug_enabled() -> bool:
return load_settings().debug_enabled
@router.message(F.text.startswith("/debug_signal"))
async def debug_signal(message: Message) -> None:
if not _debug_enabled():
await message.answer("Debug mode выключен.")
return
parts = (message.text or "").split()
signal = parts[1].upper() if len(parts) > 1 else "BUY"
service = AutoTradeService()
state = service.debug_force_signal(
signal=signal,
confidence=0.9,
repeat_count=2,
reason=f"DEBUG FORCE {signal}",
)
if state.status == "OFF":
state.status = "RUNNING"
await AutoTradeRunner._handle_important_event(state)
ExecutionEngine().process(state)
AutoTradeRunner.start()
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")
async def debug_ready(message: Message) -> None:
if not _debug_enabled():
await message.answer("Debug mode выключен.")
return
service = AutoTradeService()
state = service.debug_force_signal(
signal="BUY",
confidence=0.95,
repeat_count=2,
reason="DEBUG READY BUY",
)
if state.status == "OFF":
state.status = "RUNNING"
await AutoTradeRunner._handle_important_event(state)
ExecutionEngine().process(state)
AutoTradeRunner.start()
await message.answer(
"✅ Debug READY создан\n\n"
f"Signal: {state.last_signal}\n"
f"Decision: {state.decision_status}"
)
@router.message(F.text == "/debug_state")
async def debug_state(message: Message) -> None:
if not _debug_enabled():
await message.answer("Debug mode выключен.")
return
state = AutoTradeService().get_state()
await message.answer(
"<b>Debug Auto State</b>\n\n"
f"Status: {state.status}\n"
f"Symbol: {state.symbol}\n"
f"Strategy: {state.strategy}\n"
f"Risk: {state.risk_percent}\n"
f"Leverage: {state.leverage}\n\n"
f"Signal: {state.last_signal}\n"
f"Repeats: {state.last_signal_repeat_count}\n"
f"Confidence: {state.last_signal_confidence:.2f}\n"
f"Decision: {state.decision_status}\n\n"
f"Position: {state.position_side}\n"
f"Entry: {state.entry_price}\n"
f"PnL: {state.unrealized_pnl_usd}"
)

View File

@@ -12,6 +12,7 @@ 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:
@@ -24,4 +25,5 @@ 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_router)
dispatcher.include_router(system_router) dispatcher.include_router(system_router)

View File

@@ -147,6 +147,11 @@ class AutoTradeRunner:
await asyncio.sleep(cls._analysis_interval_seconds) await asyncio.sleep(cls._analysis_interval_seconds)
@classmethod
async def process_last_event_now(cls) -> None:
state = AutoTradeService().get_state()
await cls._handle_important_event(state)
@classmethod @classmethod
async def _handle_important_event(cls, state) -> None: async def _handle_important_event(cls, state) -> None:
event_type, payload = EventBus.last_event() event_type, payload = EventBus.last_event()
@@ -178,7 +183,8 @@ class AutoTradeRunner:
alert_key = ( alert_key = (
f"{symbol}:{strategy}:{signal}:" f"{symbol}:{strategy}:{signal}:"
f"{repeat_count}:{confidence:.2f}:{state.decision_status}" f"{repeat_count}:{confidence:.2f}:"
f"{state.decision_status}:{reason}"
) )
if alert_key == cls._last_strong_alert_key: if alert_key == cls._last_strong_alert_key:

View File

@@ -32,6 +32,59 @@ class AutoTradeService:
_last_signal_payload: dict | None = None _last_signal_payload: dict | None = None
_same_signal_count = 0 _same_signal_count = 0
# debug: принудительно выставить сигнал и decision
def debug_force_signal(
self,
*,
signal: str,
confidence: float = 0.9,
repeat_count: int = 2,
reason: str = "DEBUG SIGNAL",
) -> AutoTradeState:
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
previous_decision_status = state.decision_status
state.last_signal = normalized_signal
state.last_signal_repeat_count = repeat_count
state.last_signal_confidence = confidence
state.last_signal_reason = reason
if normalized_signal == "HOLD":
state.decision_status = "WAITING"
state.decision_reason = "Debug HOLD."
state.is_signal_confirmed = False
state.is_signal_ready = False
else:
state.decision_status = "READY"
state.decision_reason = "Debug READY signal."
state.is_signal_confirmed = True
state.is_signal_ready = True
EventBus.emit(
"auto_decision_changed",
{
"previous_signal": previous_signal,
"previous_decision_status": previous_decision_status,
"decision_status": state.decision_status,
"signal": state.last_signal,
"repeat_count": state.last_signal_repeat_count,
"confidence": state.last_signal_confidence,
"symbol": state.symbol,
"strategy": state.strategy,
"leverage": state.leverage,
"reason": state.last_signal_reason,
"debug": True,
},
)
return state
# получить текущее состояние автоторговли # получить текущее состояние автоторговли
def get_state(self) -> AutoTradeState: def get_state(self) -> AutoTradeState:
if not self._state.symbol: if not self._state.symbol:

View File

@@ -17,7 +17,7 @@ class ExecutionEngine:
# получить текущую paper-позицию # получить текущую paper-позицию
def get_position(self) -> PositionState: def get_position(self) -> PositionState:
return self._position return type(self)._position
# обработать состояние автоторговли и принять paper-execution решение # обработать состояние автоторговли и принять paper-execution решение
def process(self, state: AutoTradeState) -> ExecutionDecision: def process(self, state: AutoTradeState) -> ExecutionDecision:
@@ -30,6 +30,8 @@ class ExecutionEngine:
reason="Execution доступен только в режиме RUNNING.", reason="Execution доступен только в режиме RUNNING.",
) )
self._update_unrealized_pnl(state)
if state.decision_status != "READY" or not state.is_signal_ready: if state.decision_status != "READY" or not state.is_signal_ready:
return ExecutionDecision( return ExecutionDecision(
action="NONE", action="NONE",
@@ -37,6 +39,9 @@ class ExecutionEngine:
reason="Сигнал ещё не готов к execution.", reason="Сигнал ещё не готов к execution.",
) )
if self._should_close_position(state):
return self._close_position(state)
if state.last_signal == "BUY": if state.last_signal == "BUY":
return self._open_position_if_empty( return self._open_position_if_empty(
state=state, state=state,
@@ -65,7 +70,9 @@ class ExecutionEngine:
side: str, side: str,
action: str, action: str,
) -> ExecutionDecision: ) -> ExecutionDecision:
if self._position.side != "NONE": position = type(self)._position
if position.side != "NONE":
self._sync_state_from_position(state) self._sync_state_from_position(state)
return ExecutionDecision( return ExecutionDecision(
action="NONE", action="NONE",
@@ -83,16 +90,19 @@ class ExecutionEngine:
reason=f"Не удалось получить цену для paper execution: {exc}", reason=f"Не удалось получить цену для paper execution: {exc}",
) )
now = datetime.now().strftime("%H:%M:%S") now = self._now_time()
size = self._calculate_position_size(state)
self._position.side = side type(self)._position = PositionState(
self._position.symbol = state.symbol side=side,
self._position.entry_price = entry_price symbol=state.symbol,
self._position.size = self._calculate_position_size(state) entry_price=entry_price,
self._position.leverage = state.leverage size=size,
self._position.unrealized_pnl_usd = 0.0 leverage=state.leverage,
self._position.opened_at = now unrealized_pnl_usd=0.0,
self._position.updated_at = now opened_at=now,
updated_at=now,
)
self._sync_state_from_position(state) self._sync_state_from_position(state)
@@ -105,7 +115,7 @@ class ExecutionEngine:
"symbol": state.symbol, "symbol": state.symbol,
"side": side, "side": side,
"entry_price": entry_price, "entry_price": entry_price,
"size": self._position.size, "size": size,
"leverage": state.leverage, "leverage": state.leverage,
"signal": state.last_signal, "signal": state.last_signal,
"confidence": state.last_signal_confidence, "confidence": state.last_signal_confidence,
@@ -118,7 +128,7 @@ class ExecutionEngine:
"symbol": state.symbol, "symbol": state.symbol,
"side": side, "side": side,
"entry_price": entry_price, "entry_price": entry_price,
"size": self._position.size, "size": size,
"leverage": state.leverage, "leverage": state.leverage,
}, },
) )
@@ -129,6 +139,103 @@ class ExecutionEngine:
reason=f"Paper-позиция {side} открыта.", reason=f"Paper-позиция {side} открыта.",
) )
# закрыть текущую paper-позицию
def _close_position(self, state: AutoTradeState) -> ExecutionDecision:
position = type(self)._position
if position.side == "NONE":
self._sync_state_from_position(state)
return ExecutionDecision(
action="NONE",
can_execute=False,
reason="Нет открытой позиции для закрытия.",
)
try:
ticker = ExchangeService().get_price(state.symbol)
exit_price = ticker.price
except Exception as exc:
return ExecutionDecision(
action="NONE",
can_execute=False,
reason=f"Ошибка получения цены для закрытия: {exc}",
)
pnl = self._calculate_pnl(exit_price)
JournalService().log_ui_info(
event_type="paper_position_closed",
message=f"Позиция закрыта: {position.side} {state.symbol}",
screen="auto",
action="paper_execution",
payload={
"symbol": state.symbol,
"side": position.side,
"entry_price": position.entry_price,
"exit_price": exit_price,
"size": position.size,
"leverage": position.leverage,
"pnl": pnl,
},
)
EventBus.emit(
"paper_position_closed",
{
"symbol": state.symbol,
"side": position.side,
"entry_price": position.entry_price,
"exit_price": exit_price,
"size": position.size,
"leverage": position.leverage,
"pnl": pnl,
},
)
type(self)._position = PositionState()
self._sync_state_from_position(state)
return ExecutionDecision(
action="CLOSE",
can_execute=True,
reason="Позиция закрыта.",
)
# проверить, нужно ли закрывать позицию по противоположному сигналу
def _should_close_position(self, state: AutoTradeState) -> bool:
position = type(self)._position
if position.side == "NONE":
return False
if position.side == "LONG" and state.last_signal == "SELL":
return True
if position.side == "SHORT" and state.last_signal == "BUY":
return True
return False
# обновить unrealized PnL по текущей цене
def _update_unrealized_pnl(self, state: AutoTradeState) -> None:
position = type(self)._position
if position.side == "NONE":
self._sync_state_from_position(state)
return
try:
ticker = ExchangeService().get_price(position.symbol or state.symbol)
current_price = ticker.price
except Exception:
self._sync_state_from_position(state)
return
position.unrealized_pnl_usd = self._calculate_pnl(current_price)
position.updated_at = self._now_time()
self._sync_state_from_position(state)
# временный расчёт размера позиции для paper mode # временный расчёт размера позиции для paper mode
def _calculate_position_size(self, state: AutoTradeState) -> float: def _calculate_position_size(self, state: AutoTradeState) -> float:
risk_percent = state.risk_percent or 0.0 risk_percent = state.risk_percent or 0.0
@@ -136,9 +243,30 @@ class ExecutionEngine:
return round((risk_percent * leverage) / 100, 8) return round((risk_percent * leverage) / 100, 8)
# расчёт PnL для paper-позиции
def _calculate_pnl(self, current_price: float) -> float:
position = type(self)._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
# синхронизировать AutoTradeState с текущей paper-позицией # синхронизировать AutoTradeState с текущей paper-позицией
def _sync_state_from_position(self, state: AutoTradeState) -> None: def _sync_state_from_position(self, state: AutoTradeState) -> None:
state.position_side = self._position.side position = type(self)._position
state.entry_price = self._position.entry_price
state.position_size = self._position.size state.position_side = position.side
state.unrealized_pnl_usd = self._position.unrealized_pnl_usd state.entry_price = position.entry_price
state.position_size = position.size
state.unrealized_pnl_usd = position.unrealized_pnl_usd
# текущее время для paper execution
def _now_time(self) -> str:
return datetime.now().strftime("%H:%M:%S")

View File

@@ -129,19 +129,19 @@
✔ confidence scoring ✔ confidence scoring
✔ UI integration ✔ UI integration
### 07.4.3.1 — UI Optimization #### 07.4.3.1 — UI Optimization
✔ compact auto screen ✔ compact auto screen
✔ state-based rendering (OFF / RUNNING / OBSERVING) ✔ state-based rendering (OFF / RUNNING / OBSERVING)
✔ minimal trading layout ✔ minimal trading layout
✔ duplicate info removal ✔ duplicate info removal
### 07.4.3.2 — Engine Decoupling (NEXT) #### 07.4.3.2 — Engine Decoupling (NEXT)
✔ split analysis / UI refresh ✔ split analysis / UI refresh
✔ fast price polling (1s) ✔ fast price polling (1s)
✔ slow UI updates (event-driven / 60s) ✔ slow UI updates (event-driven / 60s)
✔ anti-flood protection ✔ anti-flood protection
### 07.4.3.3 — Paper Position & Execution Engine #### 07.4.3.3 — Paper Position & Execution Engine
- добавлен ExecutionEngine - добавлен ExecutionEngine
- реализованы paper-позиции (LONG / SHORT) - реализованы paper-позиции (LONG / SHORT)
- интеграция с AutoTradeService - интеграция с AutoTradeService
@@ -149,7 +149,7 @@
- логирование paper execution - логирование paper execution
- EventBus события (paper_position_opened) - EventBus события (paper_position_opened)
### Stage 07.4.3.4 — Telegram Strong Signal Alerts #### Stage 07.4.3.4 — Telegram Strong Signal Alerts
- EventBus-driven уведомления - EventBus-driven уведомления
- Фильтрация READY сигналов - Фильтрация READY сигналов
- Поддержка BUY / SELL - Поддержка BUY / SELL
@@ -157,6 +157,15 @@
- Интеграция с Journal - Интеграция с Journal
- Runner полностью управляет Telegram-уведомлениями - Runner полностью управляет Telegram-уведомлениями
#### Stage 07.4.3.5 — Debug Commands & Test Mode ✅
- DEBUG_ENABLED env flag
- debug_force_signal API
- instant EventBus processing
- Telegram debug commands
- state inspection (/debug_state)
- journal logging for debug actions
- full pipeline testing without market dependency
### 07.4.4 ### 07.4.4
⏳ Grid Strategy ⏳ Grid Strategy

View File

@@ -96,7 +96,7 @@
--- ---
### 07.4.3.1 — UI Optimization #### 07.4.3.1 — UI Optimization
✔ компактный экран автоторговли ✔ компактный экран автоторговли
✔ разделение OFF / ACTIVE / OBSERVING ✔ разделение OFF / ACTIVE / OBSERVING
✔ убраны дубли (WAITING / HOLD и т.д.) ✔ убраны дубли (WAITING / HOLD и т.д.)
@@ -104,7 +104,7 @@
--- ---
### 07.4.3.2 — Engine Decoupling (NEXT) #### 07.4.3.2 — Engine Decoupling (NEXT)
⏳ разделение: ⏳ разделение:
- analysis loop (частый) - analysis loop (частый)
- UI loop (редкий) - UI loop (редкий)
@@ -119,7 +119,7 @@
--- ---
### Stage 07.4.3.3 — Paper Position & Execution Engine #### Stage 07.4.3.3 — Paper Position & Execution Engine
- добавлен ExecutionEngine - добавлен ExecutionEngine
- реализованы paper-позиции (LONG / SHORT) - реализованы paper-позиции (LONG / SHORT)
- интеграция с AutoTradeService - интеграция с AutoTradeService
@@ -129,7 +129,7 @@
--- ---
## Stage 07.4.3.4 — Telegram Strong Signal Alerts #### Stage 07.4.3.4 — Telegram Strong Signal Alerts
- EventBus-driven уведомления - EventBus-driven уведомления
- Фильтрация READY сигналов - Фильтрация READY сигналов
- Поддержка BUY / SELL - Поддержка BUY / SELL
@@ -144,6 +144,18 @@
--- ---
#### Stage 07.4.3.5 — Debug Commands & Test Mode ✅
- DEBUG_ENABLED env flag
- debug_force_signal API
- instant EventBus processing
- Telegram debug commands
- state inspection (/debug_state)
- journal logging for debug actions
- full pipeline testing without market dependency
---
### 07.4.4 ### 07.4.4
⏳ Grid strategy ⏳ Grid strategy

View File

@@ -0,0 +1,281 @@
# Stage 07.4.3.4 — Telegram Strong Signal Alerts via EventBus
## Цель этапа
Добавить отдельные Telegram-уведомления для сильных торговых сигналов автоторговли.
Уведомление должно отправляться только тогда, когда сигнал действительно готов к действию:
- сигнал `BUY` или `SELL`;
- `decision_status = READY`;
- `is_signal_ready = True`;
- событие пришло через `EventBus`;
- уведомление не дублируется на каждом цикле анализа.
---
## Что реализовано
### 1. EventBus-событие для READY-сигналов
В `AutoTradeService` событие `auto_decision_changed` теперь передаёт расширенный payload:
```python
EventBus.emit(
"auto_decision_changed",
{
"previous_decision_status": previous_decision_status,
"decision_status": state.decision_status,
"signal": state.last_signal,
"repeat_count": state.last_signal_repeat_count,
"confidence": state.last_signal_confidence,
"symbol": state.symbol,
"strategy": state.strategy,
"leverage": state.leverage,
"reason": state.last_signal_reason,
},
)
```
Это позволяет runner-слою принимать решение об отправке уведомления без прямой связи со стратегией.
---
### 2. AutoTradeRunner обрабатывает важные события
В `AutoTradeRunner` добавлена обработка последнего события из `EventBus`.
Runner проверяет:
```text
event_type == auto_decision_changed
decision_status == READY
signal in BUY / SELL
```
Если условия выполнены — отправляется отдельное Telegram-сообщение.
---
### 3. Отдельное Telegram-уведомление
Формат уведомления:
```text
🚨 Сильный сигнал 🟢 BUY
BTC/USD_LEVERAGE · TREND · x2
Confidence: 0.90
Repeats: 2
Причина: DEBUG FORCE BUY
```
Для `SELL` используется красный индикатор.
---
### 4. Защита от дублей
Добавлен ключ последнего уведомления:
```python
_last_strong_alert_key
```
Ключ строится из:
```text
symbol + strategy + signal + repeat_count + confidence + decision_status
```
Если такой же alert уже был отправлен — повторная отправка блокируется.
---
### 5. Логирование уведомлений
При успешной отправке уведомления пишется событие журнала:
```text
auto_strong_signal_alert_sent
```
Payload содержит:
- symbol;
- strategy;
- signal;
- repeat_count;
- confidence;
- leverage;
- reason.
---
### 6. Debug-команды для проверки
Для проверки сильных сигналов без ожидания рынка добавлены debug-команды.
Добавлен флаг:
```env
DEBUG_ENABLED=true
```
Команды:
```text
/debug_signal BUY
/debug_signal SELL
/debug_ready
/debug_state
```
При выключенном debug mode команды не выполняются:
```env
DEBUG_ENABLED=false
```
---
### 7. Мгновенная обработка debug-события
Добавлен публичный метод runner-а:
```python
AutoTradeRunner.process_last_event_now()
```
Он нужен для debug-сценариев, чтобы событие `READY BUY / SELL` было обработано сразу, а не ожидало следующего цикла автоторговли.
---
## Изменённые файлы
```text
app/src/trading/auto/service.py
app/src/trading/auto/runner.py
app/src/core/config.py
app/src/telegram/handlers/debug.py
app/src/telegram/routers.py
```
---
## Проверка
### 1. Включить debug mode
```env
DEBUG_ENABLED=true
```
### 2. Запустить приложение
```bash
python -m src.main
```
### 3. Открыть экран автоторговли
Нужно, чтобы `AutoTradeRunner` зарегистрировал `bot` и `chat_id`.
### 4. Выполнить команду
```text
/debug_signal BUY
```
Ожидаемый результат:
```text
🚨 Сильный сигнал 🟢 BUY
BTC/USD_LEVERAGE · TREND · x2
Confidence: 0.90
Repeats: 2
Причина: DEBUG FORCE BUY
```
Также приходит подтверждение debug-команды:
```text
✅ Debug signal forced
Signal: BUY
Decision: READY
Confidence: 0.90
Repeats: 2
```
### 5. Проверить журнал
В журнале должно появиться событие:
```text
auto_strong_signal_alert_sent
```
---
## Важное замечание по безопасности
Debug-команды можно оставлять в коде, но в боевом режиме они должны быть выключены:
```env
DEBUG_ENABLED=false
```
Это важно, потому что debug-команды могут вручную выставлять `READY BUY / SELL`.
---
## Ограничения текущего этапа
Пока не реализовано:
- cooldown между alert-ами;
- настройка включения/выключения alert-ов из UI;
- разные уровни сигналов;
- подписки на отдельные типы уведомлений;
- уведомления по закрытию позиции.
---
## Результат
Этап добавил полноценный механизм Telegram-уведомлений для сильных сигналов через событийную архитектуру.
Теперь цепочка работает так:
```text
Strategy
AutoTradeService
EventBus
AutoTradeRunner
Telegram alert
```
---
## Следующий этап
Рекомендуемый следующий этап:
```text
Stage 07.4.3.5 — Position lifecycle and PnL
```
Цели следующего этапа:
- закрытие paper-позиции;
- пересчёт PnL;
- логирование закрытия;
- обновление UI позиции;
- подготовка к Risk Engine.

View File

@@ -0,0 +1,272 @@
# Stage 07.4.3.5 — Debug Commands & Test Mode
## Цель этапа
Добавить безопасный механизм тестирования автоторговли без зависимости от рыночных условий.
Этап позволяет:
- принудительно генерировать сигналы BUY / SELL / READY;
- тестировать Telegram-уведомления;
- тестировать Execution Engine;
- проверять состояние автоторговли в реальном времени.
---
## Что реализовано
### 1. Debug Mode через ENV
Добавлен флаг:
```env
DEBUG_ENABLED=true
```
При значении `false`:
- debug-команды полностью отключены;
- любые попытки их вызвать возвращают сообщение "Debug mode выключен".
Это обеспечивает безопасность для production.
---
### 2. Debug API в AutoTradeService
Добавлен метод:
```python
debug_force_signal(...)
```
Позволяет вручную задать:
- signal (BUY / SELL / HOLD)
- confidence
- repeat_count
- reason
Метод:
- обновляет `AutoTradeState`;
- выставляет `decision_status = READY` (для BUY/SELL);
- эмитит событие `auto_decision_changed`.
---
### 3. Интеграция с EventBus
После debug-сигнала:
```text
AutoTradeService
EventBus.emit(...)
AutoTradeRunner
```
Это позволяет тестировать всю цепочку без изменения стратегий.
---
### 4. Мгновенная обработка событий
Добавлен метод:
```python
AutoTradeRunner.process_last_event_now()
```
Он позволяет:
- обработать событие сразу;
- не ждать следующий цикл (`run_cycle`);
- использовать debug-команды в реальном времени.
---
### 5. Telegram Debug Commands
Добавлен handler:
```text
app/src/telegram/handlers/debug.py
```
Доступные команды:
```text
/debug_signal BUY
/debug_signal SELL
/debug_ready
/debug_state
```
---
### 6. Поведение команд
#### `/debug_signal BUY`
- выставляет сигнал BUY;
- делает его READY;
- запускает обработку EventBus;
- отправляет alert (если включён этап 07.4.3.4).
---
#### `/debug_ready`
- shortcut для READY BUY;
- максимальная уверенность;
- используется для быстрого теста execution и alert.
---
#### `/debug_state`
Показывает текущее состояние:
```text
Status
Symbol
Strategy
Risk
Leverage
Signal
Repeats
Confidence
Decision
Position
Entry
PnL
```
---
### 7. Логирование
Добавлено событие:
```text
debug_signal_forced
```
Позволяет:
- отслеживать тестовые действия;
- отделять debug от реальных сигналов.
---
## Изменённые файлы
```text
app/src/core/config.py
app/src/trading/auto/service.py
app/src/trading/auto/runner.py
app/src/telegram/handlers/debug.py
app/src/telegram/routers.py
```
---
## Проверка
### 1. Включить debug
```env
DEBUG_ENABLED=true
```
---
### 2. Запустить приложение
```bash
python -m src.main
```
---
### 3. Открыть автоторговлю
Важно: чтобы runner получил bot/chat_id.
---
### 4. Выполнить команды
```text
/debug_signal BUY
/debug_signal SELL
/debug_ready
/debug_state
```
---
### 5. Ожидаемый результат
- приходит debug confirmation;
- приходит Telegram alert (если READY);
- обновляется UI;
- пишется журнал.
---
## Важное замечание
Debug-режим нельзя оставлять включённым в production:
```env
DEBUG_ENABLED=false
```
Иначе:
- можно форсировать сделки;
- можно обойти стратегию;
- нарушается безопасность.
---
## Ограничения
Пока не реализовано:
- авторизация debug-команд (по user_id);
- rate limit debug-команд;
- отдельный debug-чат;
- UI toggle debug mode.
---
## Результат
Добавлен полноценный test harness для автоторговли.
Теперь можно тестировать:
```text
Strategy → Signal → Decision → EventBus → Runner → Telegram → Execution
```
без ожидания рынка.
---
## Следующий этап
```text
Stage 07.4.3.6 — Position lifecycle & PnL
```
- закрытие позиции;
- расчёт прибыли;
- обновление UI;
- подготовка к Risk Engine.