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

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