diff --git a/app/src/core/event_titles.py b/app/src/core/event_titles.py
index 6182fa3..0a09ec9 100644
--- a/app/src/core/event_titles.py
+++ b/app/src/core/event_titles.py
@@ -31,6 +31,9 @@ EVENT_TITLES = {
"market_stream_disconnected": "Рынок",
"market_symbol_changed": "Рынок",
+ # Мониторинг позиций
+ "entry_blocked": "Вход в позицию",
+
# Журнал
"journal_exported": "Журнал",
"journal_export_error": "Журнал",
diff --git a/app/src/telegram/handlers/auto/ui.py b/app/src/telegram/handlers/auto/ui.py
index dcfa6c5..08de59e 100644
--- a/app/src/telegram/handlers/auto/ui.py
+++ b/app/src/telegram/handlers/auto/ui.py
@@ -155,16 +155,23 @@ def _build_waiting_text(state) -> str:
entry_price_override=price,
)
+ signal_lines = [
+ _signal_line(state),
+ _market_state_line(state),
+ _entry_block_line(state),
+ *_signal_confidence_lines(state),
+ *_execution_block_lines(state),
+ ]
+
+ signal_lines = [line for line in signal_lines if line]
+
parts = [
f"🤖 Автоторговля {_status_text(state.status)}",
_account_mode_line(),
"",
f"Доступно · $ {_format_money_compact(available)}",
"",
- _signal_line(state),
- _market_state_line(state),
- *_signal_confidence_lines(state),
- *_execution_block_lines(state),
+ *signal_lines,
"",
"🧾 Подготовка ордера",
"",
@@ -253,16 +260,28 @@ def _market_state_line(state) -> str:
return labels.get(market_state, "⏳ Рынок · Идёт анализ")
+def _entry_block_line(state) -> str:
+ message = getattr(state, "entry_block_message", None)
+
+ if not message:
+ return ""
+
+ return f"Вход в позицию · {message}"
+
+
def _execution_block_lines(state) -> list[str]:
lines: list[str] = []
reason = getattr(state, "execution_block_reason", None)
if reason:
- lines.append(f"Blocked · {reason}")
+ lines.append(f"Исполнение · {reason}")
adjustment = getattr(state, "execution_size_adjustment_reason", None)
+
if adjustment == "MARGIN_LIMIT":
- lines.append("Size adjusted by Max Reserved")
+ lines.append(
+ "Позиция ограничена настройкой Max Reserved."
+ )
return lines
diff --git a/app/src/trading/auto/service.py b/app/src/trading/auto/service.py
index 068d674..4fbb717 100644
--- a/app/src/trading/auto/service.py
+++ b/app/src/trading/auto/service.py
@@ -35,6 +35,7 @@ class AutoTradeService:
_last_logged_market_state: str | None = None
_last_logged_market_trend: str | None = None
_last_logged_market_volatility: str | None = None
+ _last_logged_entry_block_reason: str | None = None
_same_signal_count = 0
# debug: принудительно выставить сигнал и decision
@@ -663,6 +664,8 @@ class AutoTradeService:
state.market_volatility = payload.get("market_volatility")
state.market_analysis_interval = payload.get("market_analysis_interval")
state.market_analysis_reason = payload.get("market_analysis_reason")
+ state.entry_block_reason = payload.get("entry_block_reason")
+ state.entry_block_message = payload.get("entry_block_message")
self._log_market_state_if_changed(
state=state,
@@ -672,6 +675,48 @@ class AutoTradeService:
previous_market_volatility=previous_market_volatility,
)
+ self._log_entry_block_if_changed(
+ state=state,
+ payload=payload,
+ )
+
+ def _log_entry_block_if_changed(
+ self,
+ *,
+ state: AutoTradeState,
+ payload: dict,
+ ) -> None:
+ reason = state.entry_block_reason
+ message = state.entry_block_message
+
+ if not reason or not message:
+ return
+
+ key = f"{state.status}:{state.symbol}:{state.strategy}:{reason}:{message}"
+
+ if key == type(self)._last_logged_entry_block_reason:
+ return
+
+ type(self)._last_logged_entry_block_reason = key
+
+ try:
+ JournalService().log_ui_info(
+ event_type="entry_blocked",
+ message=f"Вход в позицию не выполнен: {message}.",
+ screen="auto",
+ action="entry_diagnostics",
+ payload={
+ **payload,
+ "entry_block_reason": reason,
+ "entry_block_message": message,
+ "symbol": state.symbol,
+ "strategy": state.strategy,
+ "status": state.status,
+ },
+ )
+ except Exception:
+ pass
+
def _log_market_state_if_changed(
self,
*,
diff --git a/app/src/trading/auto/state.py b/app/src/trading/auto/state.py
index 46154eb..60c07aa 100644
--- a/app/src/trading/auto/state.py
+++ b/app/src/trading/auto/state.py
@@ -119,4 +119,10 @@ class AutoTradeState:
market_analysis_interval: str | None = None
# объяснение последнего анализа рынка
- market_analysis_reason: str | None = None
\ No newline at end of file
+ market_analysis_reason: str | None = None
+
+ # код причины, почему вход в позицию сейчас не выполнен
+ entry_block_reason: str | None = None
+
+ # человекочитаемое объяснение причины не входа
+ entry_block_message: str | None = None
\ No newline at end of file
diff --git a/app/src/trading/strategies/trend.py b/app/src/trading/strategies/trend.py
index a15940a..244cf5d 100644
--- a/app/src/trading/strategies/trend.py
+++ b/app/src/trading/strategies/trend.py
@@ -92,6 +92,8 @@ class TrendStrategy:
payload={
**base_payload,
"market_filter_blocked": True,
+ "entry_block_reason": "MARKET_FILTER_BLOCKED",
+ "entry_block_message": "рынок сейчас не подходит для входа",
},
)
diff --git a/docs/roadmap/master-roadmap.md b/docs/roadmap/master-roadmap.md
index 17ae59c..3ee9742 100644
--- a/docs/roadmap/master-roadmap.md
+++ b/docs/roadmap/master-roadmap.md
@@ -451,6 +451,22 @@
- CSV / XLSX export очищен от эмодзи
- журнал подготовлен к централизованному event_titles.py и future filters/search layer
+#### 07.4.4.1.4 ✅ Entry Decision Diagnostics Layer
+- добавлен диагностический слой причин не входа в позицию
+- AutoTradeState расширен entry_block_reason и entry_block_message
+- TrendStrategy начала передавать причины HOLD в payload
+- добавлены entry_block_reason к market filter / live data / weak impulse сценариям
+- AutoTradeService синхронизирует entry diagnostics в runtime state
+- добавлено событие entry_blocked для журнала
+- журнал пишет причины не входа только при изменении причины
+- добавлена защита от spam logging одинаковых HOLD-причин
+- Auto UI показывает строку Вход в позицию · причина
+- strategy diagnostics отделены от execution diagnostics
+- execution UI приведён к human-readable стилю
+- добавлен EVENT_TITLES mapping для entry_blocked
+- подготовлена база для анализа частоты причин отказа от входа
+- подготовлена база для adaptive thresholds и настройки чувствительности стратегии
+
---
### 07.4.5
diff --git a/docs/roadmap/stage-07-auto-trading-roadmap.md b/docs/roadmap/stage-07-auto-trading-roadmap.md
index 738be68..5dfb1ee 100644
--- a/docs/roadmap/stage-07-auto-trading-roadmap.md
+++ b/docs/roadmap/stage-07-auto-trading-roadmap.md
@@ -427,6 +427,22 @@
- CSV / XLSX export очищен от эмодзи
- журнал подготовлен к централизованному event_titles.py и future filters/search layer
+#### 07.4.4.1.4 ✅ Entry Decision Diagnostics Layer
+- добавлен диагностический слой причин не входа в позицию
+- AutoTradeState расширен entry_block_reason и entry_block_message
+- TrendStrategy начала передавать причины HOLD в payload
+- добавлены entry_block_reason к market filter / live data / weak impulse сценариям
+- AutoTradeService синхронизирует entry diagnostics в runtime state
+- добавлено событие entry_blocked для журнала
+- журнал пишет причины не входа только при изменении причины
+- добавлена защита от spam logging одинаковых HOLD-причин
+- Auto UI показывает строку Вход в позицию · причина
+- strategy diagnostics отделены от execution diagnostics
+- execution UI приведён к human-readable стилю
+- добавлен EVENT_TITLES mapping для entry_blocked
+- подготовлена база для анализа частоты причин отказа от входа
+- подготовлена база для adaptive thresholds и настройки чувствительности стратегии
+
---
### 07.4.5
diff --git a/docs/stages/stage-07_4_4_1_4-entry_decision_diagnostics_layer.md b/docs/stages/stage-07_4_4_1_4-entry_decision_diagnostics_layer.md
new file mode 100644
index 0000000..52c74d3
--- /dev/null
+++ b/docs/stages/stage-07_4_4_1_4-entry_decision_diagnostics_layer.md
@@ -0,0 +1,342 @@
+# 07.4.4.1.4 — Entry Decision Diagnostics Layer
+
+## Цель этапа
+
+Добавить диагностический слой, который объясняет, почему автоторговля не входит в позицию, даже если рынок движется.
+
+До этого этапа пользователь видел в интерфейсе в основном `HOLD`, но не всегда понимал причину:
+
+- рынок не подходит для входа
+- live-импульс слишком слабый
+- недостаточно live-данных
+- сигнал ещё не подтверждён
+- execution не готов открыть позицию
+- размер позиции ограничен настройками защиты
+
+Этап сделал поведение автоторговли более прозрачным и подготовил систему к дальнейшей настройке стратегии.
+
+---
+
+# Что было реализовано
+
+## 1. Добавлен Entry Decision Diagnostics Layer
+
+В систему добавлен отдельный слой диагностики входа в позицию.
+
+Теперь стратегия не просто возвращает `HOLD`, а дополнительно передаёт причину, почему вход не выполнен.
+
+Это важно, потому что `HOLD` может означать разные ситуации:
+
+- безопасное ожидание
+- рынок без выраженного направления
+- слабый импульс
+- недостаточно live-данных
+- неподходящая волатильность
+- техническая ошибка получения рыночных данных
+
+После этапа эти причины стали видимыми в runtime state, UI и журнале.
+
+---
+
+# 2. Добавлены поля диагностики входа в AutoTradeState
+
+В `AutoTradeState` добавлены новые поля:
+
+- `entry_block_reason`
+- `entry_block_message`
+
+`entry_block_reason` хранит технический код причины.
+
+Примеры:
+
+- `MARKET_FILTER_BLOCKED`
+- `LIVE_DATA_WAITING`
+- `WEAK_UP_IMPULSE`
+- `WEAK_DOWN_IMPULSE`
+- `INVALID_PRICE`
+- `SNAPSHOT_ERROR`
+
+`entry_block_message` хранит человекочитаемое объяснение.
+
+Примеры:
+
+- «рынок сейчас не подходит для входа»
+- «недостаточно live-данных для подтверждения»
+- «слабый импульс вверх»
+- «слабый импульс вниз»
+
+---
+
+# 3. TrendStrategy начала отдавать причины отказа от входа
+
+В `TrendStrategy` причины HOLD были расширены диагностическими payload-полями.
+
+Теперь, когда стратегия не даёт BUY или SELL, она передаёт:
+
+- почему вход не разрешён
+- какой market state был в момент решения
+- какой live-импульс был рассчитан
+- сколько live-цен накоплено
+- какой threshold не был пройден
+- какой direction ratio был получен
+
+Это особенно важно для анализа ситуаций, когда визуально рынок движется, но бот всё равно не входит в позицию.
+
+Например, рынок может расти последние несколько свечей, но стратегия всё ещё может не входить, если:
+
+- общий market state не подтверждает тренд
+- live-импульс ниже порога
+- движение недостаточно устойчивое
+- подтверждение сигнала ещё не накоплено
+
+---
+
+# 4. AutoTradeService синхронизирует entry diagnostics
+
+`AutoTradeService` теперь забирает `entry_block_reason` и `entry_block_message` из payload стратегии и сохраняет их в текущем состоянии автоторговли.
+
+Это позволило использовать одну и ту же диагностику сразу в нескольких местах:
+
+- Auto UI
+- журнал
+- future analytics layer
+- future filters/search layer
+- future AI explanation layer
+
+---
+
+# 5. Добавлено журналирование причин не входа
+
+Добавлено событие:
+
+- `entry_blocked`
+
+Журнал теперь фиксирует не только готовые сигналы и открытие позиций, но и важные причины, почему позиция не была открыта.
+
+Формат сообщения:
+
+```text
+[DEMO] Вход в позицию не выполнен: слабый импульс вверх.
+```
+
+или:
+
+```text
+[DEMO] Вход в позицию не выполнен: рынок сейчас не подходит для входа.
+```
+
+Это позволяет оставить автоторговлю работать долго и потом понять, почему за период не было сделок.
+
+---
+
+# 6. Добавлена защита от spam logging
+
+Причины не входа не пишутся в журнал на каждом цикле.
+
+Система пишет событие только тогда, когда причина изменилась.
+
+Например, если бот 30 минут подряд не входит из-за слабого импульса, журнал не будет получать одинаковые записи каждые 5 секунд.
+
+Если причина изменилась, например:
+
+- было: слабый импульс вверх
+- стало: рынок без выраженного направления
+
+тогда будет записано новое событие.
+
+Это сохраняет журнал полезным и не превращает его в поток повторяющихся HOLD-сообщений.
+
+---
+
+# 7. Auto UI начал показывать причину не входа
+
+На экране автоторговли добавлена строка диагностики:
+
+```text
+Вход в позицию · слабый импульс вверх
+```
+
+или:
+
+```text
+Вход в позицию · рынок сейчас не подходит для входа
+```
+
+Теперь пользователь видит не просто `HOLD`, а понимает, почему бот ждёт.
+
+---
+
+# 8. Strategy diagnostics отделены от execution diagnostics
+
+В UI теперь логически разделены два разных типа причин.
+
+## Entry diagnostics
+
+Отвечает на вопрос:
+
+```text
+Почему стратегия не хочет входить в позицию?
+```
+
+Примеры:
+
+- рынок не подходит
+- слабый импульс
+- недостаточно данных
+
+## Execution diagnostics
+
+Отвечает на вопрос:
+
+```text
+Почему позиция не может быть технически открыта?
+```
+
+Примеры:
+
+- ограничение Max Reserved
+- execution block
+- невозможность рассчитать размер позиции
+
+Это разделение важно, потому что раньше разные причины могли восприниматься как одна проблема.
+
+---
+
+# 9. UI-тексты приведены к единому human-readable стилю
+
+Устаревшие технические строки были приведены к более понятному виду.
+
+Например:
+
+- `Blocked · ...` заменено на `Исполнение · ...`
+- `Size adjusted by Max Reserved` заменено на `Позиция ограничена настройкой Max Reserved.`
+- `Вход не выполнен` уточнено до `Вход в позицию не выполнен`
+
+Это делает интерфейс понятнее для пользователя, который не обязан знать внутреннюю архитектуру execution layer.
+
+---
+
+# 10. Добавлен event title для entry diagnostics
+
+В общий mapping заголовков событий добавлен event title:
+
+```python
+"entry_blocked": "Вход"
+```
+
+Так журнал сохраняет короткую структуру:
+
+```text
+Вход | [DEMO] Вход в позицию не выполнен: слабый импульс вверх.
+```
+
+Заголовок остаётся коротким, а смысл раскрывается в сообщении.
+
+---
+
+# Изменения в архитектуре
+
+## Strategy Layer
+
+`TrendStrategy` теперь не только формирует торговый сигнал, но и объясняет причины отказа от входа.
+
+Добавлены диагностические payload-поля:
+
+- `entry_block_reason`
+- `entry_block_message`
+
+---
+
+## Auto Runtime Layer
+
+`AutoTradeService` теперь синхронизирует entry diagnostics в runtime state и журнал.
+
+Добавлено антиспам-логирование причин не входа.
+
+---
+
+## Auto State Layer
+
+`AutoTradeState` расширен полями диагностики входа:
+
+- `entry_block_reason`
+- `entry_block_message`
+
+---
+
+## Auto UI Layer
+
+На экран автоторговли добавлено отображение причины не входа.
+
+Также execution diagnostics приведён к более понятному стилю.
+
+---
+
+## Journal Layer
+
+Добавлено событие:
+
+- `entry_blocked`
+
+Событие пишет только изменение причины отказа от входа, а не каждый HOLD-цикл.
+
+---
+
+# Что изменилось для пользователя
+
+Пользователь теперь видит не просто:
+
+```text
+Сигнал HOLD
+```
+
+А получает объяснение:
+
+```text
+Сигнал HOLD
+Вход в позицию · слабый импульс вверх
+```
+
+или:
+
+```text
+Сигнал HOLD
+Вход в позицию · рынок сейчас не подходит для входа
+```
+
+Также пользователь может открыть журнал и увидеть, почему бот не входил в позицию в течение длительного времени.
+
+Это особенно важно для paper trading и подготовки к реальной торговле.
+
+---
+
+# Почему это важно для стратегии
+
+До этого этапа было трудно отличить нормальное ожидание от проблемы.
+
+Теперь можно понять:
+
+- стратегия слишком строгая
+- market filter слишком часто блокирует вход
+- live threshold слишком высокий
+- direction ratio слишком требовательный
+- не хватает накопленных live-данных
+- execution layer блокирует уже готовый сигнал
+
+Это создаёт основу для дальнейшей настройки торговой стратегии не на ощущениях, а по фактическим причинам отказа от входа.
+
+---
+
+# Что подготовлено дальше
+
+Этап подготовил основу для следующих работ:
+
+- анализ частоты причин отказа от входа
+- adaptive thresholds
+- настройка чувствительности TrendStrategy
+- очистка `_price_window` при смене актива
+- signal aging / signal reset
+- multi-symbol runtime isolation
+- entry diagnostics filters в журнале
+- статистика HOLD-причин
+- AI-комментарии по причинам не входа