07.4.4.1.8 Execution Freshness and Market Quality Layer
This commit is contained in:
@@ -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,
|
||||
"",
|
||||
"🧾 <b>Подготовка ордера</b>",
|
||||
"",
|
||||
"🧾 Подготовка ордера",
|
||||
_order_header_line(state),
|
||||
f"<b>{_price_label_for_signal(state)}</b> · {_format_usd_or_dash(price)}",
|
||||
_estimated_size_text(state, price),
|
||||
@@ -217,6 +217,7 @@ def _build_active_position_text(state) -> str:
|
||||
f"<b>Зарезервировано</b> · $ {_format_money_compact(reserved)}",
|
||||
f"<b>P&L</b> {_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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -137,4 +137,22 @@ class AutoTradeState:
|
||||
runtime_expired_reason: str | None = None
|
||||
|
||||
# человекочитаемое сообщение runtime expiration
|
||||
runtime_expired_message: str | None = None
|
||||
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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user