07.4.4.1.8 Execution Freshness and Market Quality Layer

This commit is contained in:
2026-05-11 20:08:29 +03:00
parent ec9904f91d
commit eb40ecc4dd
7 changed files with 1021 additions and 7 deletions

View File

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

View File

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

View File

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