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.