07.4.4.1.4 Entry Decision Diagnostics Layer

This commit is contained in:
2026-05-11 11:22:10 +03:00
parent b5d931bbb7
commit 363719cc8e
8 changed files with 456 additions and 7 deletions

View File

@@ -31,6 +31,9 @@ EVENT_TITLES = {
"market_stream_disconnected": "Рынок",
"market_symbol_changed": "Рынок",
# Мониторинг позиций
"entry_blocked": "Вход в позицию",
# Журнал
"journal_exported": "Журнал",
"journal_export_error": "Журнал",

View File

@@ -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"<b>Доступно</b> · $ {_format_money_compact(available)}",
"",
_signal_line(state),
_market_state_line(state),
*_signal_confidence_lines(state),
*_execution_block_lines(state),
*signal_lines,
"",
"🧾 <b>Подготовка ордера</b>",
"",
@@ -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

View File

@@ -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,
*,

View File

@@ -120,3 +120,9 @@ class AutoTradeState:
# объяснение последнего анализа рынка
market_analysis_reason: str | None = None
# код причины, почему вход в позицию сейчас не выполнен
entry_block_reason: str | None = None
# человекочитаемое объяснение причины не входа
entry_block_message: str | None = None

View File

@@ -92,6 +92,8 @@ class TrendStrategy:
payload={
**base_payload,
"market_filter_blocked": True,
"entry_block_reason": "MARKET_FILTER_BLOCKED",
"entry_block_message": "рынок сейчас не подходит для входа",
},
)

View File

@@ -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

View File

@@ -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

View File

@@ -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-комментарии по причинам не входа