diff --git a/app/src/telegram/handlers/auto/ui.py b/app/src/telegram/handlers/auto/ui.py
index 75d7e0b..440a13f 100644
--- a/app/src/telegram/handlers/auto/ui.py
+++ b/app/src/telegram/handlers/auto/ui.py
@@ -159,6 +159,7 @@ def _build_waiting_text(state) -> str:
_signal_line(state),
_market_state_line(state),
_entry_block_line(state),
+ _execution_quality_line(state),
*_signal_confidence_lines(state),
*_execution_block_lines(state),
]
@@ -173,8 +174,7 @@ def _build_waiting_text(state) -> str:
"",
*signal_lines,
"",
- "🧾 Подготовка ордера",
- "",
+ "🧾 Подготовка ордера",
_order_header_line(state),
f"{_price_label_for_signal(state)} · {_format_usd_or_dash(price)}",
_estimated_size_text(state, price),
@@ -217,6 +217,7 @@ def _build_active_position_text(state) -> str:
f"Зарезервировано · $ {_format_money_compact(reserved)}",
f"P&L {_format_signed_usd_with_direction(pnl)}",
_market_state_line(state),
+ _execution_quality_line(state),
*_execution_block_lines(state),
"",
(
@@ -286,7 +287,7 @@ def _entry_block_line(state) -> str:
signal = (state.last_signal or "HOLD").upper()
if signal == "HOLD":
- return f"Ожидание · {compact_message}"
+ return f"Условие · {compact_message}"
if signal in {"BUY", "SELL"}:
return f"Вход · {compact_message}"
@@ -294,12 +295,55 @@ def _entry_block_line(state) -> str:
return ""
+def _execution_quality_line(state) -> str:
+ quality = getattr(state, "execution_quality", None)
+ reason = getattr(state, "execution_quality_reason", None)
+ spread_percent = getattr(state, "spread_percent", None)
+ age_seconds = getattr(state, "snapshot_age_seconds", None)
+
+ if not quality:
+ return ""
+
+ if quality == "GOOD":
+ return ""
+
+ if reason == "WIDE_SPREAD" and spread_percent is not None:
+ return f"⚠️ Рынок · spread {_format_percent(spread_percent)}"
+
+ if reason == "AGING_SNAPSHOT" and age_seconds is not None:
+ return f"⚠️ Рынок · данные стареют ({age_seconds:.1f}с)"
+
+ if reason == "STALE_SNAPSHOT":
+ return "⛔ Вход · рынок неактуален"
+
+ if reason == "HIGH_SPREAD" and spread_percent is not None:
+ return f"⛔ Вход · высокий spread {_format_percent(spread_percent)}"
+
+ if reason == "SNAPSHOT_UNAVAILABLE":
+ return "⚠️ Рынок · нет depth snapshot"
+
+ if reason == "SNAPSHOT_ERROR":
+ return "⛔ Вход · нет данных рынка"
+
+ message = getattr(state, "execution_quality_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"Исполнение · {reason}")
+ if reason and reason not in {
+ "высокий spread",
+ "spread повышен",
+ "snapshot устарел",
+ "рынок неактуален",
+ }:
+ lines.append(f"Вход · {reason}")
adjustment = getattr(state, "execution_size_adjustment_reason", None)
diff --git a/app/src/trading/auto/service.py b/app/src/trading/auto/service.py
index 32e6371..0db3b30 100644
--- a/app/src/trading/auto/service.py
+++ b/app/src/trading/auto/service.py
@@ -13,6 +13,7 @@ from src.trading.execution.engine import ExecutionEngine
from src.trading.journal.service import JournalService
from src.trading.strategies.base import BaseStrategy, StrategyContext
from src.trading.strategies.registry import StrategyRegistry
+from src.integrations.exchange.service import ExchangeService
class AutoTradeService:
@@ -42,6 +43,12 @@ class AutoTradeService:
_last_logged_entry_block_reason: str | None = None
_same_signal_count = 0
+ _max_snapshot_age_seconds = 5.0
+ _warning_snapshot_age_seconds = 2.0
+ _max_spread_percent = 0.15
+ _warning_spread_percent = 0.08
+ _last_logged_execution_quality_key: str | None = None
+
# debug: принудительно выставить сигнал и decision
def debug_force_signal(
self,
@@ -325,6 +332,12 @@ class AutoTradeService:
state.entry_block_message = None
state.runtime_expired_reason = None
state.runtime_expired_message = None
+ state.snapshot_age_seconds = None
+ state.spread_percent = None
+ state.execution_quality = None
+ state.execution_quality_reason = None
+ state.execution_quality_message = None
+ state.market_runtime_degraded = False
# собрать контекст для стратегии
def _build_strategy_context(self) -> StrategyContext:
@@ -952,6 +965,203 @@ class AutoTradeService:
except Exception:
pass
+ def _sync_execution_quality_state(self, state: AutoTradeState) -> None:
+ try:
+ snapshot = ExchangeService().get_market_snapshot(
+ state.symbol,
+ runtime_key="auto",
+ )
+ except Exception as exc:
+ fallback_price = None
+
+ try:
+ fallback_price = float(
+ ExchangeService().get_price(
+ state.symbol,
+ runtime_key="auto",
+ ).price
+ )
+ except Exception:
+ pass
+
+ state.snapshot_age_seconds = None
+ state.spread_percent = None
+
+ if fallback_price is not None and fallback_price > 0:
+ state.execution_quality = "WARNING"
+ state.execution_quality_reason = "SNAPSHOT_UNAVAILABLE"
+ state.execution_quality_message = "нет depth snapshot"
+ state.market_runtime_degraded = True
+ else:
+ state.execution_quality = "BLOCKED"
+ state.execution_quality_reason = "SNAPSHOT_ERROR"
+ state.execution_quality_message = "нет market data"
+ state.market_runtime_degraded = True
+
+ self._log_execution_quality_if_changed(
+ state=state,
+ payload={
+ "error": str(exc),
+ "error_type": type(exc).__name__,
+ "fallback_price_available": fallback_price is not None,
+ },
+ )
+ return
+
+ bid_price = self._safe_float(snapshot.get("bid_price"))
+ ask_price = self._safe_float(snapshot.get("ask_price"))
+ last_price = self._safe_float(snapshot.get("last_price"))
+ age_seconds = self._safe_float(snapshot.get("age_seconds"))
+ is_fresh = bool(snapshot.get("is_fresh", False))
+ source = str(snapshot.get("source") or "")
+
+ state.snapshot_age_seconds = age_seconds
+ state.spread_percent = self._spread_percent(
+ bid_price=bid_price,
+ ask_price=ask_price,
+ )
+
+ if age_seconds is not None and age_seconds > self._max_snapshot_age_seconds:
+ state.execution_quality = "BLOCKED"
+ state.execution_quality_reason = "STALE_SNAPSHOT"
+ state.execution_quality_message = "snapshot устарел"
+ state.market_runtime_degraded = True
+
+ elif state.spread_percent is not None and state.spread_percent > self._max_spread_percent:
+ state.execution_quality = "BLOCKED"
+ state.execution_quality_reason = "HIGH_SPREAD"
+ state.execution_quality_message = "высокий spread"
+ state.market_runtime_degraded = False
+
+ elif age_seconds is not None and age_seconds > self._warning_snapshot_age_seconds:
+ state.execution_quality = "WARNING"
+ state.execution_quality_reason = "AGING_SNAPSHOT"
+ state.execution_quality_message = "snapshot стареет"
+ state.market_runtime_degraded = not is_fresh
+
+ elif state.spread_percent is not None and state.spread_percent > self._warning_spread_percent:
+ state.execution_quality = "WARNING"
+ state.execution_quality_reason = "WIDE_SPREAD"
+ state.execution_quality_message = "spread повышен"
+ state.market_runtime_degraded = False
+
+ else:
+ state.execution_quality = "GOOD"
+ state.execution_quality_reason = "MARKET_OK"
+ state.execution_quality_message = "рынок готов"
+ state.market_runtime_degraded = False
+
+ if state.execution_quality == "BLOCKED":
+ state.execution_block_reason = state.execution_quality_message
+
+ elif state.execution_block_reason == state.execution_quality_message:
+ state.execution_block_reason = None
+
+ self._log_execution_quality_if_changed(
+ state=state,
+ payload={
+ "symbol": state.symbol,
+ "strategy": state.strategy,
+ "bid_price": bid_price,
+ "ask_price": ask_price,
+ "last_price": last_price,
+ "snapshot_age_seconds": age_seconds,
+ "spread_percent": state.spread_percent,
+ "is_fresh": is_fresh,
+ "source": source,
+ "execution_quality": state.execution_quality,
+ "execution_quality_reason": state.execution_quality_reason,
+ "execution_quality_message": state.execution_quality_message,
+ "market_runtime_degraded": state.market_runtime_degraded,
+ "max_snapshot_age_seconds": self._max_snapshot_age_seconds,
+ "warning_snapshot_age_seconds": self._warning_snapshot_age_seconds,
+ "max_spread_percent": self._max_spread_percent,
+ "warning_spread_percent": self._warning_spread_percent,
+ },
+ )
+
+ def _spread_percent(
+ self,
+ *,
+ bid_price: float | None,
+ ask_price: float | None,
+ ) -> float | None:
+ if bid_price is None or ask_price is None:
+ return None
+
+ if bid_price <= 0 or ask_price <= 0:
+ return None
+
+ mid_price = (bid_price + ask_price) / 2
+ if mid_price <= 0:
+ return None
+
+ spread = ask_price - bid_price
+ if spread < 0:
+ return None
+
+ return round((spread / mid_price) * 100, 5)
+
+ def _safe_float(self, value: object) -> float | None:
+ if value is None:
+ return None
+
+ try:
+ return float(value)
+ except (TypeError, ValueError):
+ return None
+
+ def _log_execution_quality_if_changed(
+ self,
+ *,
+ state: AutoTradeState,
+ payload: dict,
+ ) -> None:
+ quality = state.execution_quality
+ reason = state.execution_quality_reason
+ message = state.execution_quality_message
+
+ if not quality or not reason or not message:
+ return
+
+ key = f"{state.status}:{state.symbol}:{state.strategy}:{quality}:{reason}:{message}"
+
+ if key == type(self)._last_logged_execution_quality_key:
+ return
+
+ type(self)._last_logged_execution_quality_key = key
+
+ if quality == "GOOD":
+ return
+
+ try:
+ log_payload = {
+ **payload,
+ "status": state.status,
+ "symbol": state.symbol,
+ "strategy": state.strategy,
+ }
+
+ if quality == "BLOCKED":
+ JournalService().log_ui_warning(
+ event_type="execution_quality_changed",
+ message=f"Качество исполнения: {message}.",
+ screen="auto",
+ action="execution_quality",
+ payload=log_payload,
+ )
+ return
+
+ JournalService().log_ui_info(
+ event_type="execution_quality_changed",
+ message=f"Качество исполнения: {message}.",
+ screen="auto",
+ action="execution_quality",
+ payload=log_payload,
+ )
+ except Exception:
+ pass
+
def run_cycle(self) -> AutoTradeState:
state = self.get_state()
@@ -969,6 +1179,8 @@ class AutoTradeService:
payload=result.payload,
)
+ self._sync_execution_quality_state(state)
+
state.last_check_at = datetime.now().strftime("%H:%M:%S")
self._log_signal_if_changed(
@@ -980,6 +1192,7 @@ class AutoTradeService:
payload=result.payload,
)
- ExecutionEngine().process(state)
+ if state.execution_quality != "BLOCKED":
+ ExecutionEngine().process(state)
return state
\ No newline at end of file
diff --git a/app/src/trading/auto/state.py b/app/src/trading/auto/state.py
index 099fca2..d7c62d2 100644
--- a/app/src/trading/auto/state.py
+++ b/app/src/trading/auto/state.py
@@ -137,4 +137,22 @@ class AutoTradeState:
runtime_expired_reason: str | None = None
# человекочитаемое сообщение runtime expiration
- runtime_expired_message: str | None = None
\ No newline at end of file
+ runtime_expired_message: str | None = None
+
+ # возраст последнего market snapshot в секундах
+ snapshot_age_seconds: float | None = None
+
+ # spread между bid/ask в %
+ spread_percent: float | None = None
+
+ # качество рынка для исполнения: GOOD / WARNING / BLOCKED / UNKNOWN
+ execution_quality: str | None = None
+
+ # код причины качества исполнения
+ execution_quality_reason: str | None = None
+
+ # человекочитаемое объяснение качества исполнения
+ execution_quality_message: str | None = None
+
+ # признак деградации runtime market data
+ market_runtime_degraded: bool = False
\ No newline at end of file
diff --git a/docs/roadmap/master-roadmap.md b/docs/roadmap/master-roadmap.md
index 33dc0c3..ddecc9d 100644
--- a/docs/roadmap/master-roadmap.md
+++ b/docs/roadmap/master-roadmap.md
@@ -575,6 +575,44 @@
- подготовлена база для advanced market diagnostics
- подготовлена база для multi-timeframe analysis
+#### 07.4.4.1.8 ✅ Execution Freshness & Market Quality Layer
+- добавлен слой execution freshness diagnostics
+- добавлен слой market quality diagnostics
+- AutoTradeState расширен execution quality полями
+- добавлены execution_quality и execution_quality_reason
+- добавлены execution_quality_message, spread_percent и snapshot_age_seconds
+- AutoTradeService начал синхронизировать execution quality в runtime state
+- добавлена проверка наличия market snapshot
+- добавлена диагностика SNAPSHOT_ERROR
+- добавлена диагностика SNAPSHOT_UNAVAILABLE
+- добавлена диагностика AGING_SNAPSHOT
+- добавлена диагностика STALE_SNAPSHOT
+- stale snapshot теперь блокирует вход
+- aging snapshot теперь отображается как warning
+- нормальный snapshot age больше не засоряет Telegram UI
+- age отображается только для AGING_SNAPSHOT / STALE_SNAPSHOT сценариев
+- добавлен расчёт bid/ask spread
+- добавлен spread warning layer
+- добавлен spread block layer
+- высокий spread теперь блокирует вход
+- повышенный spread теперь отображается как market warning
+- execution diagnostics отделены от strategy entry diagnostics
+- UI разделяет Условие, Рынок и Вход
+- термин Исполнение заменён на более короткий Вход
+- `нет market data` заменено на `нет данных рынка`
+- Telegram UI получил строку market quality warning
+- Telegram UI получил строку execution block по качеству рынка
+- wide spread отображается компактно как `Рынок · spread ...`
+- high spread отображается как `Вход · высокий spread ...`
+- устранено дублирование age при нормальном snapshot
+- добавлена база для spread-aware execution
+- добавлена база для stale snapshot protection
+- добавлена база для slippage protection
+- добавлена база для execution quality analytics
+- добавлена база для instrument quality scoring
+- выявлена необходимость Spread Hysteresis Layer
+- подготовлен следующий этап 07.4.4.1.8.1 Spread Hysteresis Layer
+
---
### 07.4.5
diff --git a/docs/roadmap/stage-07-auto-trading-roadmap.md b/docs/roadmap/stage-07-auto-trading-roadmap.md
index 2b5c57c..074a558 100644
--- a/docs/roadmap/stage-07-auto-trading-roadmap.md
+++ b/docs/roadmap/stage-07-auto-trading-roadmap.md
@@ -551,6 +551,44 @@
- подготовлена база для advanced market diagnostics
- подготовлена база для multi-timeframe analysis
+#### 07.4.4.1.8 ✅ Execution Freshness & Market Quality Layer
+- добавлен слой execution freshness diagnostics
+- добавлен слой market quality diagnostics
+- AutoTradeState расширен execution quality полями
+- добавлены execution_quality и execution_quality_reason
+- добавлены execution_quality_message, spread_percent и snapshot_age_seconds
+- AutoTradeService начал синхронизировать execution quality в runtime state
+- добавлена проверка наличия market snapshot
+- добавлена диагностика SNAPSHOT_ERROR
+- добавлена диагностика SNAPSHOT_UNAVAILABLE
+- добавлена диагностика AGING_SNAPSHOT
+- добавлена диагностика STALE_SNAPSHOT
+- stale snapshot теперь блокирует вход
+- aging snapshot теперь отображается как warning
+- нормальный snapshot age больше не засоряет Telegram UI
+- age отображается только для AGING_SNAPSHOT / STALE_SNAPSHOT сценариев
+- добавлен расчёт bid/ask spread
+- добавлен spread warning layer
+- добавлен spread block layer
+- высокий spread теперь блокирует вход
+- повышенный spread теперь отображается как market warning
+- execution diagnostics отделены от strategy entry diagnostics
+- UI разделяет Условие, Рынок и Вход
+- термин Исполнение заменён на более короткий Вход
+- `нет market data` заменено на `нет данных рынка`
+- Telegram UI получил строку market quality warning
+- Telegram UI получил строку execution block по качеству рынка
+- wide spread отображается компактно как `Рынок · spread ...`
+- high spread отображается как `Вход · высокий spread ...`
+- устранено дублирование age при нормальном snapshot
+- добавлена база для spread-aware execution
+- добавлена база для stale snapshot protection
+- добавлена база для slippage protection
+- добавлена база для execution quality analytics
+- добавлена база для instrument quality scoring
+- выявлена необходимость Spread Hysteresis Layer
+- подготовлен следующий этап 07.4.4.1.8.1 Spread Hysteresis Layer
+
---
### 07.4.5
diff --git a/docs/stages/stage-07_4_4_1_6-signal_aging_and_runtime_expiration.md b/docs/stages/07.4.4.1.6_signal_aging_and_runtime_expiration.md
similarity index 100%
rename from docs/stages/stage-07_4_4_1_6-signal_aging_and_runtime_expiration.md
rename to docs/stages/07.4.4.1.6_signal_aging_and_runtime_expiration.md
diff --git a/docs/stages/stage-07_4_4_1_8-execution_freshness_and_market_quality_layer.md b/docs/stages/stage-07_4_4_1_8-execution_freshness_and_market_quality_layer.md
new file mode 100644
index 0000000..08b8021
--- /dev/null
+++ b/docs/stages/stage-07_4_4_1_8-execution_freshness_and_market_quality_layer.md
@@ -0,0 +1,663 @@
+# 07.4.4.1.8 — Execution Freshness & Market Quality Layer
+
+## Статус
+
+Этап завершён.
+
+## Цель этапа
+
+Добавить отдельный слой проверки качества рынка перед входом в позицию.
+
+Этап 07.4.4.1.7 уже перевёл автоторговлю на live market runtime, websocket snapshots, REST fallback и bid/ask-aware анализ. Но после этого оставалась важная проблема: стратегия могла видеть корректный тренд и live-импульс, но сама возможность безопасного исполнения сделки ещё не оценивалась отдельно.
+
+Этап 07.4.4.1.8 добавляет execution freshness и market quality diagnostics:
+
+- проверку наличия market snapshot;
+- проверку свежести snapshot;
+- проверку возраста bid/ask данных;
+- расчёт spread;
+- разделение warning и blocking состояний;
+- отображение качества рынка в Telegram UI;
+- блокировку входа при плохих execution-условиях;
+- human-readable диагностику причин, почему вход сейчас небезопасен.
+
+---
+
+# Что было до этапа
+
+До внедрения этого слоя система уже умела:
+
+- получать live bid/ask через MarketDataRunner;
+- хранить snapshot в MarketPriceCache;
+- использовать market snapshot в TrendStrategy;
+- показывать причину HOLD;
+- защищать runtime loop от падения.
+
+Но execution layer ещё не отвечал на вопросы:
+
+```text
+А можно ли прямо сейчас безопасно открыть позицию?
+```
+
+Например, могли возникать ситуации:
+
+```text
+Сигнал готов
+Рынок трендовый
+Импульс подтверждён
+```
+
+но при этом:
+
+- bid/ask snapshot устарел;
+- spread стал слишком широким;
+- websocket lagging;
+- REST fallback дал данные, но они уже начали стареть;
+- market data временно недоступны.
+
+До этого такие ситуации были видны только косвенно или не были видны в UI совсем.
+
+---
+
+# Что внедрено
+
+## 1. Execution quality layer
+
+В runtime автоторговли добавлен отдельный слой оценки качества исполнения.
+
+Теперь система анализирует не только торговый сигнал, но и качество рынка для входа:
+
+```text
+Strategy signal
+↓
+Market analysis
+↓
+Live impulse diagnostics
+↓
+Execution freshness & market quality
+↓
+Execution decision
+```
+
+Это отделяет две разные сущности:
+
+- стратегия может быть права по направлению;
+- рынок может быть плохим для исполнения.
+
+---
+
+## 2. Snapshot availability check
+
+Добавлена проверка наличия market snapshot.
+
+Если snapshot отсутствует, система не должна считать вход безопасным.
+
+Диагностика:
+
+```text
+⛔ Вход · нет данных рынка
+```
+
+Это означает, что в текущий момент нет достаточных данных bid/ask для безопасной оценки цены входа.
+
+---
+
+## 3. Snapshot age tracking
+
+Система начала учитывать возраст market snapshot.
+
+`age` — это возраст последнего market snapshot в секундах.
+
+Пример:
+
+```text
+age 0.6с
+```
+
+означает, что последние bid/ask данные были получены 0.6 секунды назад.
+
+Это важно, потому что при автоторговле нельзя входить по данным, которые выглядят валидными, но фактически уже устарели.
+
+---
+
+## 4. AGING_SNAPSHOT
+
+Добавлено промежуточное состояние:
+
+```text
+AGING_SNAPSHOT
+```
+
+Оно означает:
+
+```text
+snapshot ещё можно использовать, но он начинает стареть
+```
+
+Это warning-состояние, а не обязательно блокировка.
+
+UI может показать:
+
+```text
+⚠️ Рынок · данные стареют (2.8с)
+```
+
+Смысл:
+
+- market runtime жив;
+- данные ещё есть;
+- но задержка уже повышена;
+- вход становится менее надёжным.
+
+---
+
+## 5. STALE_SNAPSHOT
+
+Добавлено критическое состояние:
+
+```text
+STALE_SNAPSHOT
+```
+
+Оно означает:
+
+```text
+snapshot уже слишком старый для безопасного входа
+```
+
+В этом случае execution должен быть заблокирован.
+
+UI:
+
+```text
+⛔ Вход · рынок неактуален
+```
+
+Смысл:
+
+- последние bid/ask данные могли устареть;
+- текущая цена могла измениться;
+- риск slippage повышен;
+- вход по такой цене небезопасен.
+
+---
+
+## 6. Spread calculation
+
+Добавлен расчёт spread между bid и ask.
+
+Формула:
+
+```text
+spread_percent = ((ask - bid) / mid_price) * 100
+mid_price = (ask + bid) / 2
+```
+
+Теперь система понимает, насколько дорого входить в рынок прямо сейчас.
+
+---
+
+## 7. Spread warning layer
+
+Добавлено warning-состояние для повышенного spread.
+
+UI:
+
+```text
+⚠️ Рынок · spread 0.1%
+```
+
+Это означает:
+
+- вход ещё не обязательно запрещён;
+- но рынок стал менее качественным;
+- bid/ask разъехались;
+- скрытая стоимость входа выросла.
+
+Для торговли это значит:
+
+- хуже цена входа;
+- выше вероятность мгновенного минуса после открытия;
+- выше риск slippage;
+- слабая ликвидность по текущему инструменту.
+
+---
+
+## 8. Spread block layer
+
+Добавлено blocking-состояние для слишком высокого spread.
+
+UI:
+
+```text
+⛔ Вход · высокий spread 0.17%
+```
+
+Это означает:
+
+```text
+сигнал может быть валидным, но вход сейчас запрещён из-за плохих рыночных условий
+```
+
+Такой слой особенно важен для менее ликвидных инструментов, например LTC, где spread может быстро расширяться.
+
+---
+
+## 9. Разделение WARNING и BLOCKED
+
+Слой качества рынка разделён на два уровня:
+
+```text
+GOOD — рынок нормальный
+WARNING — рынок ухудшился, но вход ещё может быть допустим
+BLOCKED — вход запрещён
+```
+
+Это важно, потому что не каждый широкий spread должен немедленно блокировать систему.
+
+Пример:
+
+```text
+⚠️ Рынок · spread 0.1%
+```
+
+это warning.
+
+А:
+
+```text
+⛔ Вход · высокий spread 0.17%
+```
+
+это уже block.
+
+---
+
+# Что изменилось в AutoTradeState
+
+В состояние автоторговли добавлены поля execution quality diagnostics.
+
+Они нужны, чтобы UI, execution engine и journal могли видеть одну и ту же runtime-оценку рынка.
+
+Логически добавлены поля такого уровня:
+
+```python
+execution_quality
+execution_quality_reason
+execution_quality_message
+spread_percent
+snapshot_age_seconds
+```
+
+Их назначение:
+
+- `execution_quality` — общий статус качества рынка;
+- `execution_quality_reason` — машинный код причины;
+- `execution_quality_message` — короткое human-readable сообщение;
+- `spread_percent` — текущий spread в процентах;
+- `snapshot_age_seconds` — возраст market snapshot.
+
+---
+
+# Что изменилось в AutoTradeService
+
+## 1. Добавлена синхронизация execution quality
+
+В AutoTradeService добавлен слой синхронизации execution quality state.
+
+Он получает market snapshot, рассчитывает качество рынка и сохраняет результат в AutoTradeState.
+
+Теперь state содержит не только:
+
+```text
+last_signal
+market_state
+entry_block_reason
+```
+
+но и:
+
+```text
+execution_quality
+execution_quality_reason
+spread_percent
+snapshot_age_seconds
+```
+
+---
+
+## 2. Execution quality теперь обновляется в runtime cycle
+
+В `run_cycle()` execution quality проверяется каждый цикл автоторговли.
+
+Это значит, что Telegram UI может показывать актуальное состояние рынка даже когда стратегия остаётся в HOLD.
+
+---
+
+## 3. Execution block reason получил market-quality причины
+
+Если рынок плохой для входа, state получает execution block reason.
+
+Например:
+
+```text
+высокий spread
+нет данных рынка
+рынок неактуален
+```
+
+Это не strategy diagnostics, а именно execution diagnostics.
+
+---
+
+# Что изменилось в Telegram UI
+
+## 1. Добавлена строка качества рынка
+
+В UI появилась отдельная строка:
+
+```text
+⚠️ Рынок · spread 0.1%
+```
+
+Она показывает warning по качеству рынка.
+
+---
+
+## 2. Добавлена строка блокировки входа
+
+Если execution quality блокирует вход, UI показывает:
+
+```text
+⛔ Вход · высокий spread 0.17%
+```
+
+Это означает, что проблема не в стратегии, а в условиях исполнения.
+
+---
+
+## 3. Убрана лишняя детализация age при нормальном snapshot
+
+Ранее строка могла выглядеть слишком длинно:
+
+```text
+⚠️ Рынок · spread повышен (spread 0.12%, age 0.8с)
+```
+
+После правки нормальный age не показывается.
+
+Теперь:
+
+```text
+⚠️ Рынок · spread 0.12%
+```
+
+Age показывается только когда он важен:
+
+```text
+⚠️ Рынок · данные стареют (2.8с)
+⛔ Вход · рынок неактуален
+```
+
+---
+
+## 4. Терминология UI приведена к единому стилю
+
+Используются короткие роли строк:
+
+```text
+Сигнал — сырой сигнал стратегии
+Тренд — состояние рынка
+Условие — почему стратегия пока не дала вход
+Рынок — качество рыночных данных / spread warning
+Вход — блокировка входа в позицию
+```
+
+Это сделало UI компактнее и понятнее.
+
+---
+
+# Улучшения аналитики
+
+## 1. Strategy diagnostics отделены от execution diagnostics
+
+Теперь система различает:
+
+```text
+Условие · слабый импульс
+```
+
+и:
+
+```text
+⛔ Вход · высокий spread
+```
+
+Первое означает:
+
+```text
+стратегия пока не видит достаточного импульса
+```
+
+Второе означает:
+
+```text
+даже если сигнал появится, вход сейчас небезопасен
+```
+
+Это критично для будущей аналитики, потому что причины отсутствия сделки теперь можно классифицировать отдельно.
+
+---
+
+## 2. Добавлена аналитика spread как качества рынка
+
+Spread теперь рассматривается не как техническая деталь стакана, а как полноценный market quality metric.
+
+Это позволяет в будущем строить аналитику:
+
+- какие активы чаще имеют высокий spread;
+- в какие часы execution quality ухудшается;
+- какие сигналы были заблокированы из-за плохой ликвидности;
+- где стратегия была права, но рынок был плохой для входа.
+
+---
+
+## 3. Добавлена аналитика freshness
+
+Возраст snapshot теперь является отдельной метрикой.
+
+Это позволяет отличать:
+
+```text
+рынок плохой из-за spread
+```
+
+от:
+
+```text
+рынок плохой из-за старых данных
+```
+
+Для live autotrading это разные причины риска.
+
+---
+
+## 4. Подготовка к slippage protection
+
+Spread diagnostics создаёт основу для будущей защиты от slippage.
+
+Если spread растёт, то реальная цена входа может заметно отличаться от ожидаемой.
+
+Теперь система может заранее остановить вход до того, как execution откроет позицию по плохой цене.
+
+---
+
+## 5. Подготовка к instrument quality scoring
+
+На примере LTC стало видно, что разные активы имеют разную норму spread.
+
+Этап подготовил основу для future scoring:
+
+```text
+BTC — stricter spread thresholds
+LTC — wider normal spread range
+XRP — отдельная ликвидность
+ETH — отдельный профиль
+```
+
+Это будет важно для adaptive thresholds и per-symbol execution profiles.
+
+---
+
+## 6. Подготовка к hysteresis
+
+В ходе проверки стало видно, что spread может колебаться около порога.
+
+Из-за этого UI может переключаться между:
+
+```text
+⚠️ Рынок · spread 0.1%
+```
+
+и:
+
+```text
+⛔ Вход · высокий spread 0.17%
+```
+
+Это не ошибка, а следствие отсутствия hysteresis.
+
+Следующий этап:
+
+```text
+07.4.4.1.8.1 — Spread Hysteresis Layer
+```
+
+должен добавить зоны стабилизации, чтобы UI и execution state не мигали на границе порогов.
+
+---
+
+# Что было проверено
+
+Проверено на live UI:
+
+- market quality line отображается;
+- spread warning отображается компактно;
+- высокий spread блокирует вход;
+- age не показывается при нормальном snapshot;
+- snapshot age остаётся доступен для AGING / STALE сценариев;
+- терминология `Исполнение` заменена на более короткое `Вход`;
+- `нет market data` заменено на `нет данных рынка`;
+- strategy diagnostics и execution diagnostics отображаются разными строками;
+- HOLD timer продолжает работать как runtime heartbeat;
+- live screen обновляется после правок runner lifecycle;
+- LTC корректно показывает spread diagnostics.
+
+---
+
+# Примеры UI после этапа
+
+## Spread warning
+
+```text
+Сигнал 🟡 HOLD · 8м 13с
+📈 Тренд · Вверх
+Условие · слабый импульс
+⚠️ Рынок · spread 0.1%
+```
+
+## Spread block
+
+```text
+Сигнал 🟡 HOLD · 54с
+📈 Тренд · Вверх
+Условие · слабый импульс
+⛔ Вход · высокий spread 0.17%
+```
+
+## Нет данных рынка
+
+```text
+⛔ Вход · нет данных рынка
+```
+
+## Устаревший рынок
+
+```text
+⛔ Вход · рынок неактуален
+```
+
+---
+
+# Что подготовлено для следующих этапов
+
+Этап подготовил базу для:
+
+```text
+Spread Hysteresis Layer
+per-symbol spread thresholds
+adaptive execution quality
+slippage protection
+liquidity quality scoring
+instrument quality profiles
+market quality analytics
+execution block analytics
+freshness-aware execution engine
+spread-aware signal confidence
+```
+
+---
+
+# Основные изменённые файлы
+
+- `app/src/trading/auto/service.py`
+- `app/src/trading/auto/state.py`
+- `app/src/trading/market_analysis/service.py`
+- `app/src/trading/market_analysis/models.py`
+- `app/src/integrations/exchange/models.py`
+- `app/src/telegram/handlers/auto/ui.py`
+- `app/src/integrations/exchange/market_data_runner.py`
+- `app/src/integrations/exchange/ws_client.py`
+
+---
+
+# Проверка перед commit
+
+Рекомендуемая проверка:
+
+```bash
+python -m compileall src
+python -m src.main
+```
+
+После запуска проверить:
+
+1. Экран автоторговли обновляется автоматически.
+2. HOLD timer продолжает тикать.
+3. Market state отображается корректно.
+4. При нормальном snapshot age не отображается.
+5. При warning spread показывается строка `⚠️ Рынок · spread ...`.
+6. При block spread показывается строка `⛔ Вход · высокий spread ...`.
+7. Сообщение `нет market data` больше не используется.
+8. Вместо него отображается `⛔ Вход · нет данных рынка`.
+9. Strategy diagnostics отображаются через `Условие`.
+10. Execution block diagnostics отображаются через `Вход`.
+
+---
+
+# Итог этапа
+
+Этап 07.4.4.1.8 добавил важный слой между стратегией и исполнением.
+
+Теперь автоторговля оценивает не только направление рынка и силу сигнала, но и качество текущего рынка для входа.
+
+Система стала безопаснее:
+
+- не входит без актуальных данных;
+- видит возраст snapshot;
+- оценивает spread;
+- предупреждает о плохом рынке;
+- блокирует вход при плохом execution quality;
+- показывает пользователю понятную причину.
+
+Это подготовило автоторговлю к следующему уровню: hysteresis, adaptive execution thresholds и per-symbol market quality profiles.