07.4.4.1.13 — AutoTrade Runtime Journal, Execution Refactor & Trade Analytics
This commit is contained in:
814
app/src/trading/auto/signal_runtime.py
Normal file
814
app/src/trading/auto/signal_runtime.py
Normal file
@@ -0,0 +1,814 @@
|
||||
# app/src/trading/auto/signal_runtime.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Callable, cast
|
||||
|
||||
from src.core.event_bus import EventBus
|
||||
from src.core.numbers import safe_float
|
||||
from src.core.types import JsonDict, NumericLike
|
||||
from src.integrations.exchange.service import ExchangeService
|
||||
from src.trading.auto.state import AutoTradeState
|
||||
from src.trading.journal.service import JournalService
|
||||
|
||||
|
||||
class AutoSignalRuntimeMixin:
|
||||
_loop_interval_seconds: int
|
||||
|
||||
_confirm_repeats: int
|
||||
_confirm_min_duration_seconds: int
|
||||
_ready_confidence: float
|
||||
_execution_confidence_required_score: float
|
||||
|
||||
_signal_ttl_seconds: int
|
||||
_market_analysis_ttl_seconds: int
|
||||
_last_logged_runtime_expired_key: str | None
|
||||
|
||||
_last_signal_key: str | None
|
||||
_last_signal_value: str | None
|
||||
_last_signal_reason: str
|
||||
_last_signal_confidence: float
|
||||
_last_signal_payload: JsonDict | None
|
||||
_last_signal_started_at: float | None
|
||||
_same_signal_count: int
|
||||
|
||||
# получить state из основного AutoTradeService
|
||||
def get_state(self) -> AutoTradeState:
|
||||
raise NotImplementedError
|
||||
|
||||
# сбросить runtime tracking в основном AutoTradeService
|
||||
def _reset_signal_tracking(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
# debug: принудительно выставить сигнал и decision
|
||||
def debug_force_signal(
|
||||
self,
|
||||
*,
|
||||
signal: str,
|
||||
confidence: NumericLike = 0.9,
|
||||
repeat_count: int = 2,
|
||||
reason: str = "DEBUG SIGNAL",
|
||||
) -> AutoTradeState:
|
||||
state = self.get_state()
|
||||
confidence_value = safe_float(confidence) or 0.0
|
||||
|
||||
normalized_signal = signal.strip().upper()
|
||||
if normalized_signal not in {"BUY", "SELL", "HOLD"}:
|
||||
normalized_signal = "HOLD"
|
||||
|
||||
previous_signal = state.last_signal
|
||||
previous_decision_status = state.decision_status
|
||||
|
||||
if previous_signal != normalized_signal or state.signal_started_at is None:
|
||||
state.signal_started_at = time.monotonic()
|
||||
|
||||
state.last_signal = normalized_signal
|
||||
state.last_signal_repeat_count = repeat_count
|
||||
state.last_signal_confidence = confidence_value
|
||||
state.last_signal_reason = reason
|
||||
state.signal_confirmation_seconds = self._confirm_min_duration_seconds
|
||||
state.signal_confirmation_required_seconds = self._confirm_min_duration_seconds
|
||||
state.signal_confirmation_missing_repeats = 0
|
||||
state.signal_confirmation_progress = 1.0
|
||||
state.signal_confirmation_reason = "debug confirmation"
|
||||
|
||||
if normalized_signal == "HOLD":
|
||||
state.decision_status = "WAITING"
|
||||
state.decision_reason = "Debug HOLD."
|
||||
state.is_signal_confirmed = False
|
||||
state.is_signal_ready = False
|
||||
else:
|
||||
state.decision_status = "READY"
|
||||
state.decision_reason = "Debug READY signal."
|
||||
state.is_signal_confirmed = True
|
||||
state.is_signal_ready = True
|
||||
|
||||
signal_intent = self._signal_intent(
|
||||
state=state,
|
||||
signal=state.last_signal,
|
||||
)
|
||||
|
||||
EventBus.emit(
|
||||
"auto_decision_changed",
|
||||
{
|
||||
"previous_signal": previous_signal,
|
||||
"previous_decision_status": previous_decision_status,
|
||||
"decision_status": state.decision_status,
|
||||
"signal": state.last_signal,
|
||||
"signal_intent": signal_intent,
|
||||
"repeat_count": state.last_signal_repeat_count,
|
||||
"confidence": state.last_signal_confidence,
|
||||
"symbol": state.symbol,
|
||||
"strategy": state.strategy,
|
||||
"leverage": state.leverage,
|
||||
"reason": state.last_signal_reason,
|
||||
"debug": True,
|
||||
},
|
||||
)
|
||||
|
||||
return state
|
||||
|
||||
# определить смысл сигнала с учетом открытой позиции
|
||||
def _signal_intent(self, *, state: AutoTradeState, signal: str | None) -> str:
|
||||
normalized_signal = (signal or "HOLD").upper()
|
||||
position_side = str(getattr(state, "position_side", "NONE") or "NONE").upper()
|
||||
|
||||
if normalized_signal == "HOLD":
|
||||
return "HOLD_MARKET"
|
||||
|
||||
if normalized_signal not in {"BUY", "SELL"}:
|
||||
return "NOISE"
|
||||
|
||||
if position_side == "NONE":
|
||||
return "ENTRY_CANDIDATE"
|
||||
|
||||
if position_side == "LONG" and normalized_signal == "BUY":
|
||||
return "REINFORCE_POSITION"
|
||||
|
||||
if position_side == "SHORT" and normalized_signal == "SELL":
|
||||
return "REINFORCE_POSITION"
|
||||
|
||||
if position_side == "LONG" and normalized_signal == "SELL":
|
||||
return "REVERSAL_CANDIDATE"
|
||||
|
||||
if position_side == "SHORT" and normalized_signal == "BUY":
|
||||
return "REVERSAL_CANDIDATE"
|
||||
|
||||
return "NOISE"
|
||||
|
||||
# обновить статус решения по текущему сигналу
|
||||
def _update_decision_state(
|
||||
self,
|
||||
*,
|
||||
state: AutoTradeState,
|
||||
signal: str,
|
||||
confidence: float,
|
||||
) -> None:
|
||||
state.is_signal_confirmed = False
|
||||
state.is_signal_ready = False
|
||||
state.signal_confirmation_required_seconds = self._confirm_min_duration_seconds
|
||||
|
||||
if signal == "HOLD":
|
||||
state.signal_confirmation_seconds = 0
|
||||
state.signal_confirmation_missing_repeats = self._confirm_repeats
|
||||
state.signal_confirmation_progress = 0.0
|
||||
state.signal_confirmation_reason = None
|
||||
state.decision_status = "WAITING"
|
||||
state.decision_reason = "Нет торгового направления."
|
||||
return
|
||||
|
||||
now = time.monotonic()
|
||||
|
||||
if state.signal_started_at is None:
|
||||
signal_age_seconds = 0
|
||||
else:
|
||||
signal_started = safe_float(state.signal_started_at)
|
||||
signal_age_seconds = (
|
||||
max(0, int(now - signal_started))
|
||||
if signal_started is not None
|
||||
else 0
|
||||
)
|
||||
|
||||
missing_repeats = max(0, self._confirm_repeats - self._same_signal_count)
|
||||
missing_seconds = max(
|
||||
0,
|
||||
self._confirm_min_duration_seconds - signal_age_seconds,
|
||||
)
|
||||
|
||||
repeat_progress = min(
|
||||
1.0,
|
||||
self._same_signal_count / max(1, self._confirm_repeats),
|
||||
)
|
||||
time_progress = min(
|
||||
1.0,
|
||||
signal_age_seconds / max(1, self._confirm_min_duration_seconds),
|
||||
)
|
||||
|
||||
confirmation_progress = min(repeat_progress, time_progress)
|
||||
|
||||
state.signal_confirmation_seconds = signal_age_seconds
|
||||
state.signal_confirmation_missing_repeats = missing_repeats
|
||||
state.signal_confirmation_progress = round(confirmation_progress, 3)
|
||||
|
||||
if missing_repeats > 0 or missing_seconds > 0:
|
||||
state.decision_status = "CONFIRMING"
|
||||
state.signal_confirmation_reason = (
|
||||
f"{self._same_signal_count}/{self._confirm_repeats} повторов, "
|
||||
f"{signal_age_seconds}/{self._confirm_min_duration_seconds}с"
|
||||
)
|
||||
state.decision_reason = (
|
||||
f"Сигнал {signal} подтверждается: "
|
||||
f"{self._same_signal_count}/{self._confirm_repeats} повторов, "
|
||||
f"{signal_age_seconds}/{self._confirm_min_duration_seconds}с."
|
||||
)
|
||||
return
|
||||
|
||||
state.is_signal_confirmed = True
|
||||
state.signal_confirmation_reason = "сигнал подтверждён"
|
||||
|
||||
if confidence < self._ready_confidence:
|
||||
state.decision_status = "BLOCKED"
|
||||
state.decision_reason = (
|
||||
f"Сигнал {signal} подтверждён, но уверенность низкая: "
|
||||
f"{confidence:.2f} < {self._ready_confidence:.2f}."
|
||||
)
|
||||
return
|
||||
|
||||
self._sync_execution_confidence_state(
|
||||
state=state,
|
||||
signal=signal,
|
||||
confidence=confidence,
|
||||
)
|
||||
|
||||
if (
|
||||
state.execution_confidence_score is not None
|
||||
and state.execution_confidence_score < self._execution_confidence_required_score
|
||||
):
|
||||
state.decision_status = "BLOCKED"
|
||||
state.decision_reason = (
|
||||
f"Execution confidence низкий: "
|
||||
f"{state.execution_confidence_score:.2f} < "
|
||||
f"{self._execution_confidence_required_score:.2f}."
|
||||
)
|
||||
return
|
||||
|
||||
state.is_signal_ready = True
|
||||
state.signal_confirmation_progress = 1.0
|
||||
state.decision_status = "READY"
|
||||
state.decision_reason = (
|
||||
f"Сигнал {signal} подтверждён по повторам и времени удержания."
|
||||
)
|
||||
|
||||
# записать новый сигнал и итог предыдущей серии при смене сигнала
|
||||
def _log_signal_if_changed(
|
||||
self,
|
||||
*,
|
||||
strategy_name: str,
|
||||
state: AutoTradeState,
|
||||
signal: str,
|
||||
reason: str,
|
||||
confidence: float,
|
||||
payload: JsonDict | None,
|
||||
) -> None:
|
||||
signal_key = f"{state.status}:{state.symbol}:{strategy_name}:{signal}"
|
||||
previous_signal = self._last_signal_value
|
||||
previous_count = self._same_signal_count
|
||||
is_same_signal = signal_key == self._last_signal_key
|
||||
now = time.monotonic()
|
||||
|
||||
if is_same_signal:
|
||||
self._same_signal_count += 1
|
||||
self._last_signal_reason = reason
|
||||
self._last_signal_confidence = confidence
|
||||
self._last_signal_payload = payload
|
||||
|
||||
self._update_signal_state_fields(
|
||||
state=state,
|
||||
signal=signal,
|
||||
reason=reason,
|
||||
confidence=confidence,
|
||||
)
|
||||
return
|
||||
|
||||
if previous_signal is not None and previous_signal != signal:
|
||||
if previous_count > 1:
|
||||
self._log_signal_summary(
|
||||
strategy_name=strategy_name,
|
||||
state=state,
|
||||
previous_signal=previous_signal,
|
||||
previous_count=previous_count,
|
||||
next_signal=signal,
|
||||
reason=self._last_signal_reason,
|
||||
confidence=self._last_signal_confidence,
|
||||
payload=self._last_signal_payload,
|
||||
duration_seconds=self._signal_duration_seconds(now=now),
|
||||
)
|
||||
else:
|
||||
self._log_signal_event(
|
||||
strategy_name=strategy_name,
|
||||
state=state,
|
||||
signal=previous_signal,
|
||||
reason=f"{previous_signal} завершился без серии.",
|
||||
confidence=self._last_signal_confidence,
|
||||
payload={
|
||||
"previous_signal": previous_signal,
|
||||
"next_signal": signal,
|
||||
},
|
||||
)
|
||||
|
||||
self._last_signal_key = signal_key
|
||||
self._last_signal_value = signal
|
||||
self._last_signal_reason = reason
|
||||
self._last_signal_confidence = confidence
|
||||
self._last_signal_payload = payload
|
||||
self._last_signal_started_at = now
|
||||
self._same_signal_count = 1
|
||||
|
||||
self._update_signal_state_fields(
|
||||
state=state,
|
||||
signal=signal,
|
||||
reason=reason,
|
||||
confidence=confidence,
|
||||
)
|
||||
|
||||
# рассчитать длительность текущей серии сигналов
|
||||
def _signal_duration_seconds(self, *, now: float) -> int:
|
||||
if self._last_signal_started_at is None:
|
||||
return max(0, int(self._same_signal_count * self._loop_interval_seconds))
|
||||
|
||||
return max(0, int(now - self._last_signal_started_at))
|
||||
|
||||
# отформатировать длительность для журнала
|
||||
def _format_duration(self, total_seconds: int) -> str:
|
||||
total_seconds = max(0, int(total_seconds))
|
||||
|
||||
hours = total_seconds // 3600
|
||||
minutes = (total_seconds % 3600) // 60
|
||||
seconds = total_seconds % 60
|
||||
|
||||
if hours > 0:
|
||||
return f"{hours}ч {minutes:02d}м {seconds:02d}с"
|
||||
|
||||
if minutes > 0:
|
||||
return f"{minutes}м {seconds:02d}с"
|
||||
|
||||
return f"{seconds}с"
|
||||
|
||||
# обновить поля state для экрана автоторговли
|
||||
def _update_signal_state_fields(
|
||||
self,
|
||||
*,
|
||||
state: AutoTradeState,
|
||||
signal: str,
|
||||
reason: str,
|
||||
confidence: float,
|
||||
) -> None:
|
||||
previous_signal = state.last_signal
|
||||
previous_decision_status = state.decision_status
|
||||
|
||||
if previous_signal != signal or state.signal_started_at is None:
|
||||
state.signal_started_at = time.monotonic()
|
||||
|
||||
state.last_signal = signal
|
||||
state.last_signal_repeat_count = self._same_signal_count
|
||||
state.last_signal_confidence = confidence
|
||||
state.last_signal_reason = reason
|
||||
state.signal_updated_at = time.monotonic()
|
||||
state.runtime_expired_reason = None
|
||||
state.runtime_expired_message = None
|
||||
|
||||
self._update_decision_state(
|
||||
state=state,
|
||||
signal=signal,
|
||||
confidence=confidence,
|
||||
)
|
||||
|
||||
signal_intent = self._signal_intent(
|
||||
state=state,
|
||||
signal=state.last_signal,
|
||||
)
|
||||
|
||||
if (
|
||||
previous_decision_status != state.decision_status
|
||||
and state.decision_status == "READY"
|
||||
):
|
||||
self._log_ready_signal(
|
||||
state=state,
|
||||
signal=state.last_signal,
|
||||
reason=state.last_signal_reason or reason,
|
||||
confidence=state.last_signal_confidence,
|
||||
signal_intent=signal_intent,
|
||||
)
|
||||
|
||||
if previous_signal != state.last_signal:
|
||||
EventBus.emit(
|
||||
"auto_signal_changed",
|
||||
{
|
||||
"previous_signal": previous_signal,
|
||||
"signal": state.last_signal,
|
||||
"signal_intent": signal_intent,
|
||||
"repeat_count": state.last_signal_repeat_count,
|
||||
"confidence": state.last_signal_confidence,
|
||||
},
|
||||
)
|
||||
|
||||
if previous_decision_status != state.decision_status:
|
||||
EventBus.emit(
|
||||
"auto_decision_changed",
|
||||
{
|
||||
"previous_decision_status": previous_decision_status,
|
||||
"decision_status": state.decision_status,
|
||||
"signal": state.last_signal,
|
||||
"signal_intent": signal_intent,
|
||||
"repeat_count": state.last_signal_repeat_count,
|
||||
"confidence": state.last_signal_confidence,
|
||||
"symbol": state.symbol,
|
||||
"strategy": state.strategy,
|
||||
"leverage": state.leverage,
|
||||
"reason": state.last_signal_reason,
|
||||
},
|
||||
)
|
||||
|
||||
# одиночные BUY / SELL больше не пишем в журнал как полезные события
|
||||
def _log_signal_event(
|
||||
self,
|
||||
*,
|
||||
strategy_name: str,
|
||||
state: AutoTradeState,
|
||||
signal: str,
|
||||
reason: str,
|
||||
confidence: float,
|
||||
payload: JsonDict | None,
|
||||
) -> None:
|
||||
return
|
||||
|
||||
# записать итог серии одинаковых сигналов при смене сигнала
|
||||
def _log_signal_summary(
|
||||
self,
|
||||
*,
|
||||
strategy_name: str,
|
||||
state: AutoTradeState,
|
||||
previous_signal: str,
|
||||
previous_count: int,
|
||||
next_signal: str,
|
||||
reason: str,
|
||||
confidence: float,
|
||||
payload: JsonDict | None,
|
||||
duration_seconds: int,
|
||||
) -> None:
|
||||
if previous_signal != "HOLD":
|
||||
return
|
||||
|
||||
duration_text = self._format_duration(duration_seconds)
|
||||
signal_intent = "HOLD_MARKET"
|
||||
|
||||
try:
|
||||
JournalService().log_ui_info(
|
||||
event_type="signal_summary",
|
||||
message=(
|
||||
f"HOLD длился {duration_text} и завершился сигналом {next_signal}."
|
||||
),
|
||||
screen="auto",
|
||||
action="signal_summary",
|
||||
payload={
|
||||
"strategy": strategy_name,
|
||||
"status": state.status,
|
||||
"symbol": state.symbol,
|
||||
"signal": previous_signal,
|
||||
"next_signal": next_signal,
|
||||
"signal_intent": signal_intent,
|
||||
"repeat_count": previous_count,
|
||||
"duration_seconds": duration_seconds,
|
||||
"duration_text": duration_text,
|
||||
"confidence": confidence,
|
||||
"reason": reason,
|
||||
"is_strong_signal": False,
|
||||
"is_aggregated": True,
|
||||
"payload": payload or {},
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# записать событие готовности сигнала к исполнению
|
||||
def _log_ready_signal(
|
||||
self,
|
||||
*,
|
||||
state: AutoTradeState,
|
||||
signal: str | None,
|
||||
reason: str,
|
||||
confidence: float,
|
||||
signal_intent: str,
|
||||
) -> None:
|
||||
normalized_signal = (signal or "HOLD").upper()
|
||||
if normalized_signal not in {"BUY", "SELL"}:
|
||||
return
|
||||
|
||||
snapshot = ExchangeService().get_market_snapshot(
|
||||
state.symbol,
|
||||
runtime_key="auto",
|
||||
)
|
||||
|
||||
try:
|
||||
JournalService().log_ui_info(
|
||||
event_type="signal_ready",
|
||||
message=(
|
||||
f"Сигнал {normalized_signal} подтверждён и готов к исполнению."
|
||||
),
|
||||
screen="auto",
|
||||
action="signal_ready",
|
||||
payload={
|
||||
"strategy": state.strategy,
|
||||
"status": state.status,
|
||||
"symbol": state.symbol,
|
||||
"signal": normalized_signal,
|
||||
"signal_intent": signal_intent,
|
||||
"confidence": confidence,
|
||||
"reason": reason,
|
||||
"repeat_count": state.last_signal_repeat_count,
|
||||
"position_side": state.position_side,
|
||||
"decision_status": state.decision_status,
|
||||
"is_strong_signal": confidence > self._ready_confidence,
|
||||
"is_aggregated": False,
|
||||
"confirmation_seconds": state.signal_confirmation_seconds,
|
||||
"confirmation_required_seconds": state.signal_confirmation_required_seconds,
|
||||
"confirmation_progress": state.signal_confirmation_progress,
|
||||
"bid_price": snapshot.get("bid_price"),
|
||||
"ask_price": snapshot.get("ask_price"),
|
||||
"last_price": snapshot.get("last_price"),
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# сбросить устаревшие signal / market runtime данные
|
||||
def _expire_runtime_if_needed(self, state: AutoTradeState) -> None:
|
||||
now = time.monotonic()
|
||||
|
||||
signal_updated_at = getattr(state, "signal_updated_at", None)
|
||||
if signal_updated_at is not None:
|
||||
signal_updated = safe_float(signal_updated_at)
|
||||
if signal_updated is None:
|
||||
return
|
||||
|
||||
signal_age = now - signal_updated
|
||||
if signal_age > self._signal_ttl_seconds:
|
||||
previous_signal = state.last_signal
|
||||
|
||||
self._reset_signal_tracking()
|
||||
|
||||
state.runtime_expired_reason = "SIGNAL_TTL_EXPIRED"
|
||||
state.runtime_expired_message = "сигнал устарел и был сброшен"
|
||||
|
||||
self._log_runtime_expired_if_changed(
|
||||
state=state,
|
||||
reason="SIGNAL_TTL_EXPIRED",
|
||||
message="Сигнал устарел и был сброшен.",
|
||||
payload={
|
||||
"previous_signal": previous_signal,
|
||||
"signal_age_seconds": int(signal_age),
|
||||
"signal_ttl_seconds": self._signal_ttl_seconds,
|
||||
},
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
market_updated_at = getattr(state, "market_analysis_updated_at", None)
|
||||
if market_updated_at is not None:
|
||||
market_updated = safe_float(market_updated_at)
|
||||
|
||||
if market_updated is None:
|
||||
return
|
||||
|
||||
market_age = now - market_updated
|
||||
|
||||
if market_age > self._market_analysis_ttl_seconds:
|
||||
state.market_state = None
|
||||
state.market_trend = None
|
||||
state.market_volatility = None
|
||||
state.market_analysis_interval = None
|
||||
state.market_analysis_reason = None
|
||||
state.market_analysis_updated_at = None
|
||||
state.entry_block_reason = None
|
||||
state.entry_block_message = None
|
||||
state.market_trend_strength = None
|
||||
state.market_trend_quality = None
|
||||
state.market_phase = None
|
||||
state.market_phase_direction = None
|
||||
state.market_trend_gap_percent = None
|
||||
state.market_trend_consistency = None
|
||||
state.market_trend_efficiency = None
|
||||
state.trend_quality_score = None
|
||||
state.ema_distance_atr_ratio = None
|
||||
state.ema_distance_state = None
|
||||
state.entry_timing_state = None
|
||||
state.entry_timing_reason = None
|
||||
state.ema_fast_slope_percent = None
|
||||
state.ema_slow_slope_percent = None
|
||||
state.candle_noise_score = None
|
||||
state.price_position_score = None
|
||||
state.htf_interval = None
|
||||
state.htf_atr_percent = None
|
||||
state.htf_atr_percent_baseline = None
|
||||
state.htf_volatility_ratio = None
|
||||
state.htf_volatility = None
|
||||
state.momentum_state = None
|
||||
state.momentum_direction = None
|
||||
state.momentum_change_percent = None
|
||||
state.momentum_strength = None
|
||||
state.breakout_level = None
|
||||
state.breakout_distance_percent = None
|
||||
state.breakout_reason = None
|
||||
state.runtime_expired_reason = "MARKET_ANALYSIS_TTL_EXPIRED"
|
||||
state.runtime_expired_message = "анализ рынка устарел"
|
||||
|
||||
self._log_runtime_expired_if_changed(
|
||||
state=state,
|
||||
reason="MARKET_ANALYSIS_TTL_EXPIRED",
|
||||
message="Анализ рынка устарел и был сброшен.",
|
||||
payload={
|
||||
"market_age_seconds": int(market_age),
|
||||
"market_analysis_ttl_seconds": self._market_analysis_ttl_seconds,
|
||||
},
|
||||
)
|
||||
|
||||
# записать событие устаревания runtime данных
|
||||
def _log_runtime_expired_if_changed(
|
||||
self,
|
||||
*,
|
||||
state: AutoTradeState,
|
||||
reason: str,
|
||||
message: str,
|
||||
payload: JsonDict,
|
||||
) -> None:
|
||||
key = f"{state.status}:{state.symbol}:{state.strategy}:{reason}"
|
||||
|
||||
if key == type(self)._last_logged_runtime_expired_key:
|
||||
return
|
||||
|
||||
type(self)._last_logged_runtime_expired_key = key
|
||||
|
||||
try:
|
||||
JournalService().log_ui_warning(
|
||||
event_type="runtime_expired",
|
||||
message=message,
|
||||
screen="auto",
|
||||
action="runtime_expiration",
|
||||
payload={
|
||||
**payload,
|
||||
"symbol": state.symbol,
|
||||
"strategy": state.strategy,
|
||||
"status": state.status,
|
||||
"runtime_expired_reason": reason,
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# синхронизировать итоговый execution confidence
|
||||
def _sync_execution_confidence_state(
|
||||
self,
|
||||
*,
|
||||
state: AutoTradeState,
|
||||
signal: str,
|
||||
confidence: float,
|
||||
) -> None:
|
||||
if signal not in {"BUY", "SELL"}:
|
||||
state.execution_confidence_score = None
|
||||
state.execution_confidence_level = None
|
||||
state.execution_confidence_required_score = self._execution_confidence_required_score
|
||||
state.execution_confidence_reason = None
|
||||
state.execution_confidence_factors = None
|
||||
return
|
||||
|
||||
signal_score = self._clamp_score(confidence)
|
||||
confirmation_score = self._clamp_score(state.signal_confirmation_progress)
|
||||
market_score = self._market_confidence_score(state)
|
||||
execution_quality_confidence_score = cast(
|
||||
Callable[[AutoTradeState], float],
|
||||
getattr(self, "_execution_quality_confidence_score"),
|
||||
)
|
||||
execution_score = execution_quality_confidence_score(state)
|
||||
|
||||
score = (
|
||||
signal_score * 0.35
|
||||
+ confirmation_score * 0.20
|
||||
+ market_score * 0.25
|
||||
+ execution_score * 0.20
|
||||
)
|
||||
|
||||
score = round(self._clamp_score(score), 3)
|
||||
|
||||
state.execution_confidence_score = score
|
||||
state.execution_confidence_required_score = self._execution_confidence_required_score
|
||||
state.execution_confidence_level = self._execution_confidence_level(score)
|
||||
state.execution_confidence_reason = self._execution_confidence_reason(state)
|
||||
state.execution_confidence_factors = {
|
||||
"signal_score": round(signal_score, 3),
|
||||
"confirmation_score": round(confirmation_score, 3),
|
||||
"market_score": round(market_score, 3),
|
||||
"execution_score": round(execution_score, 3),
|
||||
"required_score": self._execution_confidence_required_score,
|
||||
"market_state": state.market_state,
|
||||
"market_trend": state.market_trend,
|
||||
"market_trend_strength": state.market_trend_strength,
|
||||
"market_trend_quality": state.market_trend_quality,
|
||||
"market_phase": state.market_phase,
|
||||
"execution_quality": state.execution_quality,
|
||||
"execution_quality_reason": state.execution_quality_reason,
|
||||
"spread_percent": state.spread_percent,
|
||||
"momentum_state": getattr(state, "momentum_state", None),
|
||||
"momentum_direction": getattr(state, "momentum_direction", None),
|
||||
"momentum_change_percent": getattr(state, "momentum_change_percent", None),
|
||||
"momentum_strength": getattr(state, "momentum_strength", None),
|
||||
"breakout_level": getattr(state, "breakout_level", None),
|
||||
"breakout_distance_percent": getattr(state, "breakout_distance_percent", None),
|
||||
"breakout_reason": getattr(state, "breakout_reason", None),
|
||||
}
|
||||
|
||||
# рассчитать market confidence для итогового execution confidence
|
||||
def _market_confidence_score(self, state: AutoTradeState) -> float:
|
||||
market_state = state.market_state
|
||||
strength = state.market_trend_strength
|
||||
quality = state.market_trend_quality
|
||||
phase = state.market_phase
|
||||
ema_distance_state = state.ema_distance_state
|
||||
entry_timing_state = state.entry_timing_state
|
||||
trend_quality_score = safe_float(state.trend_quality_score)
|
||||
|
||||
if market_state in {
|
||||
"HIGH_VOLATILITY",
|
||||
"LOW_VOLATILITY",
|
||||
"RANGE",
|
||||
"UNKNOWN",
|
||||
None,
|
||||
"",
|
||||
}:
|
||||
return 0.25
|
||||
|
||||
score = 0.65
|
||||
|
||||
if strength == "STRONG":
|
||||
score += 0.2
|
||||
elif strength == "NORMAL":
|
||||
score += 0.1
|
||||
elif strength == "WEAK":
|
||||
score -= 0.25
|
||||
|
||||
if quality == "CLEAN":
|
||||
score += 0.12
|
||||
elif quality == "NORMAL":
|
||||
score += 0.04
|
||||
elif quality == "NOISY":
|
||||
score -= 0.25
|
||||
|
||||
if phase == "IMPULSE":
|
||||
score += 0.1
|
||||
elif phase == "PULLBACK":
|
||||
score -= 0.25
|
||||
elif phase in {"RANGE", "SQUEEZE"}:
|
||||
score -= 0.3
|
||||
|
||||
if ema_distance_state == "HEALTHY":
|
||||
score += 0.08
|
||||
elif ema_distance_state == "EXTENDED":
|
||||
score -= 0.08
|
||||
elif ema_distance_state == "COMPRESSED":
|
||||
score -= 0.18
|
||||
elif ema_distance_state == "OVEREXTENDED":
|
||||
score -= 0.35
|
||||
|
||||
if entry_timing_state == "NORMAL":
|
||||
score += 0.08
|
||||
elif entry_timing_state == "EARLY":
|
||||
score -= 0.05
|
||||
elif entry_timing_state == "LATE":
|
||||
score -= 0.2
|
||||
elif entry_timing_state == "CHASING":
|
||||
score -= 0.35
|
||||
|
||||
if trend_quality_score is not None:
|
||||
if trend_quality_score >= 0.7:
|
||||
score += 0.08
|
||||
elif trend_quality_score < 0.45:
|
||||
score -= 0.15
|
||||
|
||||
return self._clamp_score(score)
|
||||
|
||||
# определить уровень execution confidence
|
||||
def _execution_confidence_level(self, score: float) -> str:
|
||||
if score >= 0.75:
|
||||
return "HIGH"
|
||||
|
||||
if score >= self._execution_confidence_required_score:
|
||||
return "NORMAL"
|
||||
|
||||
return "LOW"
|
||||
|
||||
# сформировать причину execution confidence
|
||||
def _execution_confidence_reason(self, state: AutoTradeState) -> str:
|
||||
score = state.execution_confidence_score
|
||||
|
||||
if score is None:
|
||||
return "execution confidence не рассчитан"
|
||||
|
||||
if score < self._execution_confidence_required_score:
|
||||
return "низкая совокупная уверенность входа"
|
||||
|
||||
if state.execution_confidence_level == "HIGH":
|
||||
return "высокая совокупная уверенность входа"
|
||||
|
||||
return "достаточная совокупная уверенность входа"
|
||||
|
||||
# ограничить score диапазоном 0.0..1.0
|
||||
def _clamp_score(self, value: NumericLike | None) -> float:
|
||||
if value is None:
|
||||
return 0.0
|
||||
|
||||
numeric = safe_float(value)
|
||||
|
||||
if numeric is None:
|
||||
return 0.0
|
||||
|
||||
return max(0.0, min(1.0, numeric))
|
||||
Reference in New Issue
Block a user