diff --git a/app/src/trading/auto/auto_lifecycle.py b/app/src/trading/auto/auto_lifecycle.py index cc65362..2dc0fc2 100644 --- a/app/src/trading/auto/auto_lifecycle.py +++ b/app/src/trading/auto/auto_lifecycle.py @@ -41,6 +41,29 @@ class AutoLifecycleMixin( _confirm_repeats: int _execution_confidence_required_score: float + # собрать payload изменения статуса автоторговли без изменения состояния + def _build_auto_status_changed_payload( + self, + *, + previous_status: str, + new_status: str, + state: AutoTradeState, + ) -> dict[str, object]: + return { + "previous_status": previous_status, + "new_status": new_status, + "symbol": state.symbol, + "strategy": state.strategy, + "cycle_number": state.cycle_number, + "risk_percent": state.risk_percent, + "leverage": state.leverage, + "allocated_balance_usd": state.allocated_balance_usd, + "stop_loss_percent": state.stop_loss_percent, + "take_profit_percent": state.take_profit_percent, + "max_loss_usd": state.max_loss_usd, + "max_reserved_balance_percent": state.max_reserved_balance_percent, + } + def _log_auto_status_changed( self, *, @@ -56,20 +79,11 @@ class AutoLifecycleMixin( message=message, screen="auto", action=action, - payload={ - "previous_status": previous_status, - "new_status": new_status, - "symbol": state.symbol, - "strategy": state.strategy, - "cycle_number": state.cycle_number, - "risk_percent": state.risk_percent, - "leverage": state.leverage, - "allocated_balance_usd": state.allocated_balance_usd, - "stop_loss_percent": state.stop_loss_percent, - "take_profit_percent": state.take_profit_percent, - "max_loss_usd": state.max_loss_usd, - "max_reserved_balance_percent": state.max_reserved_balance_percent, - }, + payload=self._build_auto_status_changed_payload( + previous_status=previous_status, + new_status=new_status, + state=state, + ), ) def set_allocated_balance_usd(self, value: NumericLike) -> AutoTradeState: diff --git a/app/src/trading/auto/autonomous_management.py b/app/src/trading/auto/autonomous_management.py index eba7a09..94d6fb4 100644 --- a/app/src/trading/auto/autonomous_management.py +++ b/app/src/trading/auto/autonomous_management.py @@ -41,7 +41,7 @@ class AutoAutonomousManagementMixin: return exit_signal = str( - state.position_exit_signal + state.position_exit_signal or POSITION_EXIT_SIGNAL_HOLD ).upper() exit_confidence = safe_float(state.position_exit_confidence) or 0.0 diff --git a/app/src/trading/auto/execution_quality.py b/app/src/trading/auto/execution_quality.py index c49e2a8..7a4902c 100644 --- a/app/src/trading/auto/execution_quality.py +++ b/app/src/trading/auto/execution_quality.py @@ -143,6 +143,29 @@ class AutoExecutionQualityMixin: return "MARKET_BREAK" + # собрать payload изменения доступности биржи без изменения состояния + def _build_exchange_availability_payload( + self, + *, + state: AutoTradeState, + status: ExchangeRuntimeStatus, + reason: str, + ) -> dict[str, object]: + return { + "status": state.status, + "symbol": state.symbol, + "strategy": state.strategy, + "exchange_status_code": status.code.value, + "exchange_reason": status.reason, + "execution_reason": reason, + "is_open": status.is_open, + "is_available": status.is_available, + "is_auth_ok": status.is_auth_ok, + "message": status.message, + "raw_status": status.raw_status, + "raw_error": status.raw_error, + } + # залогировать изменение доступности биржи/рынка def _log_exchange_availability_if_changed( self, @@ -167,20 +190,11 @@ class AutoExecutionQualityMixin: message=status.ui_line, screen="auto", action="exchange_status", - payload={ - "status": state.status, - "symbol": state.symbol, - "strategy": state.strategy, - "exchange_status_code": status.code.value, - "exchange_reason": status.reason, - "execution_reason": reason, - "is_open": status.is_open, - "is_available": status.is_available, - "is_auth_ok": status.is_auth_ok, - "message": status.message, - "raw_status": status.raw_status, - "raw_error": status.raw_error, - }, + payload=self._build_exchange_availability_payload( + state=state, + status=status, + reason=reason, + ), ) except Exception: pass @@ -437,6 +451,20 @@ class AutoExecutionQualityMixin: else: state.execution_price_freshness = "STALE" + # собрать payload изменения качества исполнения без изменения состояния + def _build_execution_quality_payload( + self, + *, + state: AutoTradeState, + payload: dict[str, object], + ) -> dict[str, object]: + return { + **payload, + "status": state.status, + "symbol": state.symbol, + "strategy": state.strategy, + } + # записать событие изменения execution quality def _log_execution_quality_if_changed( self, @@ -462,12 +490,10 @@ class AutoExecutionQualityMixin: return try: - log_payload = { - **payload, - "status": state.status, - "symbol": state.symbol, - "strategy": state.strategy, - } + log_payload = self._build_execution_quality_payload( + state=state, + payload=payload, + ) if quality == "BLOCKED": JournalService().log_ui_warning( diff --git a/app/src/trading/auto/market_runtime.py b/app/src/trading/auto/market_runtime.py index 3ced277..8fd1a1e 100644 --- a/app/src/trading/auto/market_runtime.py +++ b/app/src/trading/auto/market_runtime.py @@ -140,6 +140,60 @@ class AutoMarketRuntimeMixin: payload=payload, ) + # собрать payload entry-block события без изменения состояния + def _build_entry_block_payload( + self, + *, + state: AutoTradeState, + payload: JsonDict, + reason: str, + message: str, + key: str, + ) -> JsonDict: + return { + **payload, + "entry_block_reason": reason, + "entry_block_message": message, + "entry_block_key": key, + "entry_block_ttl_seconds": type(self)._entry_block_log_ttl_seconds, + "symbol": state.symbol, + "strategy": state.strategy, + "status": state.status, + "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, + "market_phase_direction": state.market_phase_direction, + "market_score": state.market_score, + "market_score_label": state.market_score_label, + "market_long_score": state.market_long_score, + "market_short_score": state.market_short_score, + "current_interval_change_percent": state.current_interval_change_percent, + "current_interval_direction": state.current_interval_direction, + "current_interval_label": state.current_interval_label, + "market_structure": state.market_structure, + "market_structure_reason": state.market_structure_reason, + "momentum_state": state.momentum_state, + "momentum_direction": state.momentum_direction, + "momentum_strength": state.momentum_strength, + "momentum_change_percent": state.momentum_change_percent, + "htf_market_state": state.htf_market_state, + "htf_trend": state.htf_trend, + "htf_trend_strength": state.htf_trend_strength, + "htf_trend_quality": state.htf_trend_quality, + "htf_market_phase": state.htf_market_phase, + "htf_alignment": state.htf_alignment, + "htf_confirmation_score": state.htf_confirmation_score, + "htf_reason": state.htf_reason, + "execution_quality": state.execution_quality, + "execution_quality_reason": state.execution_quality_reason, + "execution_confidence_score": state.execution_confidence_score, + "last_signal": state.last_signal, + "last_signal_confidence": state.last_signal_confidence, + "last_signal_reason": state.last_signal_reason, + } + # записать entry-block событие, если причина изменилась или истёк TTL def _log_entry_block_if_changed( self, @@ -180,53 +234,39 @@ class AutoMarketRuntimeMixin: message=f"Вход в позицию не выполнен: {message}.", screen="auto", action="entry_diagnostics", - payload={ - **payload, - "entry_block_reason": reason, - "entry_block_message": message, - "entry_block_key": key, - "entry_block_ttl_seconds": type(self)._entry_block_log_ttl_seconds, - "symbol": state.symbol, - "strategy": state.strategy, - "status": state.status, - "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, - "market_phase_direction": state.market_phase_direction, - "market_score": state.market_score, - "market_score_label": state.market_score_label, - "market_long_score": state.market_long_score, - "market_short_score": state.market_short_score, - "current_interval_change_percent": state.current_interval_change_percent, - "current_interval_direction": state.current_interval_direction, - "current_interval_label": state.current_interval_label, - "market_structure": state.market_structure, - "market_structure_reason": state.market_structure_reason, - "momentum_state": state.momentum_state, - "momentum_direction": state.momentum_direction, - "momentum_strength": state.momentum_strength, - "momentum_change_percent": state.momentum_change_percent, - "htf_market_state": state.htf_market_state, - "htf_trend": state.htf_trend, - "htf_trend_strength": state.htf_trend_strength, - "htf_trend_quality": state.htf_trend_quality, - "htf_market_phase": state.htf_market_phase, - "htf_alignment": state.htf_alignment, - "htf_confirmation_score": state.htf_confirmation_score, - "htf_reason": state.htf_reason, - "execution_quality": state.execution_quality, - "execution_quality_reason": state.execution_quality_reason, - "execution_confidence_score": state.execution_confidence_score, - "last_signal": state.last_signal, - "last_signal_confidence": state.last_signal_confidence, - "last_signal_reason": state.last_signal_reason, - }, + payload=self._build_entry_block_payload( + state=state, + payload=payload, + reason=reason, + message=message, + key=key, + ), ) except Exception: pass + # собрать payload изменения состояния рынка без изменения состояния + def _build_market_state_payload( + self, + *, + payload: JsonDict, + previous_market_state: str | None, + previous_market_trend: str | None, + previous_market_volatility: str | None, + current_market_state: str | None, + current_market_trend: str | None, + current_market_volatility: str | None, + ) -> JsonDict: + return { + **payload, + "previous_market_state": previous_market_state, + "previous_market_trend": previous_market_trend, + "previous_market_volatility": previous_market_volatility, + "current_market_state": current_market_state, + "current_market_trend": current_market_trend, + "current_market_volatility": current_market_volatility, + } + # записать market state / volatility событие, если состояние изменилось def _log_market_state_if_changed( self, @@ -269,15 +309,15 @@ class AutoMarketRuntimeMixin: if not state_changed and not volatility_changed: return - journal_payload = { - **payload, - "previous_market_state": previous_market_state, - "previous_market_trend": previous_market_trend, - "previous_market_volatility": previous_market_volatility, - "current_market_state": market_state, - "current_market_trend": market_trend, - "current_market_volatility": market_volatility, - } + journal_payload = self._build_market_state_payload( + payload=payload, + previous_market_state=previous_market_state, + previous_market_trend=previous_market_trend, + previous_market_volatility=previous_market_volatility, + current_market_state=market_state, + current_market_trend=market_trend, + current_market_volatility=market_volatility, + ) try: if state_changed: diff --git a/app/src/trading/auto/position_semantics.py b/app/src/trading/auto/position_semantics.py index 777f494..3453697 100644 --- a/app/src/trading/auto/position_semantics.py +++ b/app/src/trading/auto/position_semantics.py @@ -537,7 +537,7 @@ class AutoPositionSemanticsMixin: return "MODERATE" return "LOW" - + # определить, застряла ли позиция без нормального движения def _position_stall_state(self, state: AutoTradeState) -> tuple[str, str]: hold_seconds = safe_float(getattr(state, "position_hold_seconds", None)) or 0.0 diff --git a/app/src/trading/auto/runner.py b/app/src/trading/auto/runner.py index 697b28d..9cb22e4 100644 --- a/app/src/trading/auto/runner.py +++ b/app/src/trading/auto/runner.py @@ -388,6 +388,31 @@ class AutoTradeRunner: return False + @classmethod + def _build_position_aligned_signal_suppressed_payload( + cls, + *, + symbol: str, + strategy: str, + signal: str, + position_side: str, + confidence: float, + repeat_count: int, + reason: str | None, + ) -> JsonDict: + return { + "symbol": symbol, + "strategy": strategy, + "signal": signal, + "position_side": position_side, + "confidence": confidence, + "repeat_count": repeat_count, + "reason": reason, + "suppression_interval_seconds": ( + cls._position_aligned_signal_log_interval_seconds + ), + } + @classmethod def _log_position_aligned_signal_suppressed( cls, @@ -444,16 +469,15 @@ class AutoTradeRunner: ), screen="auto", action="signal_notification", - payload={ - "symbol": symbol, - "strategy": strategy, - "signal": signal, - "position_side": position_side, - "confidence": confidence, - "repeat_count": repeat_count, - "reason": payload.get("reason") or state.last_signal_reason, - "suppression_interval_seconds": cls._position_aligned_signal_log_interval_seconds, - }, + payload=cls._build_position_aligned_signal_suppressed_payload( + symbol=symbol, + strategy=strategy, + signal=signal, + position_side=position_side, + confidence=confidence, + repeat_count=repeat_count, + reason=payload.get("reason") or state.last_signal_reason, + ), ) except Exception: pass @@ -509,6 +533,46 @@ class AutoTradeRunner: ), } + @classmethod + def _build_runtime_signal_payload( + cls, + *, + state, + symbol: str, + strategy: str, + signal: str, + signal_intent: str, + repeat_count: int, + confidence: float, + leverage, + reason: str, + position_context: str, + is_aligned_signal: bool, + semantic_lines: list[str], + price_payload: JsonDict, + ) -> JsonDict: + return { + "execution_block_title": getattr(state, "execution_block_title", None), + "execution_block_message": getattr(state, "execution_block_message", None), + "execution_block_action": getattr(state, "execution_block_action", None), + "symbol": symbol, + "strategy": strategy, + "signal": signal, + "signal_intent": signal_intent, + "repeat_count": repeat_count, + "confidence": confidence, + "leverage": leverage, + "reason": reason, + "position_context": position_context, + "position_side": position_context, + "is_position_aligned_signal": is_aligned_signal, + "decision_status": state.decision_status, + "market_score": getattr(state, "market_score", None), + "market_score_label": getattr(state, "market_score_label", None), + "semantic_lines": semantic_lines, + **price_payload, + } + @classmethod def _publish_strong_signal_event( cls, @@ -573,32 +637,21 @@ class AutoTradeRunner: event_type=RuntimeEventType.AUTO_SIGNAL_READY, source="auto_trade_runner", title=f"Auto strong signal {signal}", - payload={ - "execution_block_title": getattr(state, "execution_block_title", None), - "execution_block_message": getattr(state, "execution_block_message", None), - "execution_block_action": getattr(state, "execution_block_action", None), - "symbol": symbol, - "strategy": strategy, - "signal": signal, - "signal_intent": signal_intent, - "repeat_count": repeat_count, - "confidence": confidence, - "leverage": leverage, - "reason": reason, - "position_context": position_context, - "position_side": position_context, - "is_position_aligned_signal": is_aligned_signal, - "decision_status": state.decision_status, - - # market_score передаём в уведомления, - # чтобы позже можно было показывать “Рынок · благоприятный · 82%” - # не только в экране, но и в событиях/алертах. - "market_score": getattr(state, "market_score", None), - "market_score_label": getattr(state, "market_score_label", None), - - "semantic_lines": semantic_lines, - **price_payload, - }, + payload=cls._build_runtime_signal_payload( + state=state, + symbol=symbol, + strategy=strategy, + signal=signal, + signal_intent=signal_intent, + repeat_count=repeat_count, + confidence=confidence, + leverage=leverage, + reason=reason, + position_context=position_context, + is_aligned_signal=is_aligned_signal, + semantic_lines=semantic_lines, + price_payload=price_payload, + ), priority=priority.lower(), dedupe_key=( f"auto_signal_ready:" @@ -612,6 +665,37 @@ class AutoTradeRunner: ) ) + @classmethod + def _build_runtime_execution_payload( + cls, + *, + state, + payload: JsonDict, + event_type: str, + symbol: str, + side: str, + old_side: str, + new_side: str, + semantic_lines: list[str], + ) -> JsonDict: + return { + **payload, + "source_event_type": event_type, + "symbol": symbol, + "side": side, + "old_side": old_side, + "new_side": new_side, + "leverage": ( + payload.get("leverage") + if payload.get("leverage") is not None + else state.leverage + ), + "strategy": state.strategy, + "market_score": getattr(state, "market_score", None), + "market_score_label": getattr(state, "market_score_label", None), + "semantic_lines": semantic_lines, + } + @classmethod def _publish_execution_event( cls, @@ -636,27 +720,16 @@ class AutoTradeRunner: event_type=runtime_event_type, source="auto_trade_runner", title=cls._execution_event_title(runtime_event_type), - payload={ - **payload, - "source_event_type": event_type, - "symbol": symbol, - "side": side, - "old_side": old_side, - "new_side": new_side, - "leverage": ( - payload.get("leverage") - if payload.get("leverage") is not None - else state.leverage - ), - "strategy": state.strategy, - - # Фиксируем market_score на момент открытия/закрытия/flip, - # чтобы журнал и уведомления показывали рыночный контекст сделки. - "market_score": getattr(state, "market_score", None), - "market_score_label": getattr(state, "market_score_label", None), - - "semantic_lines": semantic_lines, - }, + payload=cls._build_runtime_execution_payload( + state=state, + payload=payload, + event_type=event_type, + symbol=symbol, + side=side, + old_side=old_side, + new_side=new_side, + semantic_lines=semantic_lines, + ), priority="normal", dedupe_key=cls._execution_dedupe_key( runtime_event_type=runtime_event_type, diff --git a/app/src/trading/auto/signal_runtime.py b/app/src/trading/auto/signal_runtime.py index 1514850..f7c7dce 100644 --- a/app/src/trading/auto/signal_runtime.py +++ b/app/src/trading/auto/signal_runtime.py @@ -439,6 +439,39 @@ class AutoSignalRuntimeMixin: ) -> None: return + # собрать payload итогового события серии сигналов + def _build_signal_summary_payload( + self, + *, + strategy_name: str, + state: AutoTradeState, + previous_signal: str, + next_signal: str, + signal_intent: str, + previous_count: int, + duration_seconds: int, + duration_text: str, + confidence: float, + reason: str, + payload: JsonDict | None, + ) -> JsonDict: + return { + "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 {}, + } + # записать итог серии одинаковых сигналов при смене сигнала def _log_signal_summary( self, @@ -467,26 +500,172 @@ class AutoSignalRuntimeMixin: ), 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 {}, - }, + payload=self._build_signal_summary_payload( + strategy_name=strategy_name, + state=state, + previous_signal=previous_signal, + next_signal=next_signal, + signal_intent=signal_intent, + previous_count=previous_count, + duration_seconds=duration_seconds, + duration_text=duration_text, + confidence=confidence, + reason=reason, + payload=payload, + ), ) except Exception: pass + # собрать payload события готовности сигнала + def _build_ready_signal_payload( + self, + *, + state: AutoTradeState, + snapshot: JsonDict, + signal: str, + signal_intent: str, + confidence: float, + reason: str, + ) -> JsonDict: + return { + # ---------- Event ---------- + "event_type": "signal_ready", + "action": "signal_ready", + "is_aggregated": False, + "is_strong_signal": confidence > self._ready_confidence, + + # ---------- Runtime ---------- + "status": state.status, + "strategy": state.strategy, + "symbol": state.symbol, + "cycle_number": state.cycle_number, + + # ---------- Signal ---------- + "signal": signal, + "signal_intent": signal_intent, + "confidence": confidence, + "reason": reason, + "repeat_count": state.last_signal_repeat_count, + + # ---------- Confirmation ---------- + "confirmation_seconds": state.signal_confirmation_seconds, + "confirmation_required_seconds": state.signal_confirmation_required_seconds, + "confirmation_missing_repeats": state.signal_confirmation_missing_repeats, + "confirmation_progress": state.signal_confirmation_progress, + "confirmation_reason": state.signal_confirmation_reason, + + # ---------- Decision ---------- + "decision_status": state.decision_status, + "decision_reason": state.decision_reason, + "is_signal_confirmed": state.is_signal_confirmed, + "is_signal_ready": state.is_signal_ready, + + # ---------- Position Context ---------- + "position_side": state.position_side, + "entry_price": state.entry_price, + "position_size": state.position_size, + "unrealized_pnl_usd": state.unrealized_pnl_usd, + "current_trade_id": state.current_trade_id, + "current_trade_cycle_number": state.current_trade_cycle_number, + + # ---------- Risk Settings ---------- + "risk_percent": state.risk_percent, + "stop_loss_percent": state.stop_loss_percent, + "take_profit_percent": state.take_profit_percent, + "max_loss_usd": state.max_loss_usd, + "max_reserved_balance_percent": state.max_reserved_balance_percent, + "allocated_balance_usd": state.allocated_balance_usd, + "leverage": state.leverage, + + # ---------- Execution Confidence ---------- + "execution_confidence_score": state.execution_confidence_score, + "execution_confidence_level": state.execution_confidence_level, + "execution_confidence_required_score": state.execution_confidence_required_score, + "execution_confidence_reason": state.execution_confidence_reason, + "execution_confidence_factors": state.execution_confidence_factors, + + # ---------- Execution Quality ---------- + "execution_quality": state.execution_quality, + "execution_quality_reason": state.execution_quality_reason, + "execution_quality_message": state.execution_quality_message, + "spread_percent": state.spread_percent, + "snapshot_age_seconds": state.snapshot_age_seconds, + + # ---------- Live Snapshot ---------- + "bid_price": snapshot.get("bid_price"), + "ask_price": snapshot.get("ask_price"), + "last_price": snapshot.get("last_price"), + + # ---------- Market Score ---------- + "market_score": state.market_score, + "market_score_label": state.market_score_label, + "market_long_score": state.market_long_score, + "market_short_score": state.market_short_score, + + # ---------- Market ---------- + "market_state": state.market_state, + "market_trend": state.market_trend, + "market_volatility": state.market_volatility, + "market_trend_strength": state.market_trend_strength, + "market_trend_quality": state.market_trend_quality, + "market_phase": state.market_phase, + "market_phase_direction": state.market_phase_direction, + + # ---------- Candle ---------- + "last_closed_candle_change_percent": state.last_closed_candle_change_percent, + "last_closed_candle_direction": state.last_closed_candle_direction, + "current_interval_change_percent": state.current_interval_change_percent, + "current_interval_direction": state.current_interval_direction, + "current_interval_label": state.current_interval_label, + + # ---------- Structure ---------- + "market_structure": state.market_structure, + "market_structure_reason": state.market_structure_reason, + + # ---------- Trend Quality ---------- + "market_trend_gap_percent": state.market_trend_gap_percent, + "market_trend_consistency": state.market_trend_consistency, + "market_trend_efficiency": state.market_trend_efficiency, + "trend_quality_score": state.trend_quality_score, + "ema_distance_atr_ratio": state.ema_distance_atr_ratio, + "ema_distance_state": state.ema_distance_state, + "entry_timing_state": state.entry_timing_state, + "entry_timing_reason": state.entry_timing_reason, + + # ---------- Momentum / Breakout ---------- + "momentum_state": state.momentum_state, + "momentum_direction": state.momentum_direction, + "momentum_change_percent": state.momentum_change_percent, + "momentum_strength": state.momentum_strength, + "breakout_level": state.breakout_level, + "breakout_distance_percent": state.breakout_distance_percent, + "breakout_reason": state.breakout_reason, + + # ---------- HTF ---------- + "htf_interval": state.htf_interval, + "htf_atr_percent": state.htf_atr_percent, + "htf_atr_percent_baseline": state.htf_atr_percent_baseline, + "htf_volatility_ratio": state.htf_volatility_ratio, + "htf_volatility": state.htf_volatility, + "htf_market_state": state.htf_market_state, + "htf_trend": state.htf_trend, + "htf_trend_strength": state.htf_trend_strength, + "htf_trend_quality": state.htf_trend_quality, + "htf_market_phase": state.htf_market_phase, + "htf_alignment": state.htf_alignment, + "htf_confirmation_score": state.htf_confirmation_score, + "htf_reason": state.htf_reason, + + # ---------- Runtime Health ---------- + "market_runtime_degraded": state.market_runtime_degraded, + "runtime_expired_reason": state.runtime_expired_reason, + "runtime_expired_message": state.runtime_expired_message, + "market_is_open": state.market_is_open, + "market_status": state.market_status, + "market_status_message": state.market_status_message, + } + # записать событие готовности сигнала к исполнению def _log_ready_signal( self, @@ -517,143 +696,14 @@ class AutoSignalRuntimeMixin: ), screen="auto", action="signal_ready", - payload={ - # ---------- Event ---------- - "event_type": "signal_ready", - "action": "signal_ready", - "is_aggregated": False, - "is_strong_signal": confidence > self._ready_confidence, - - # ---------- Runtime ---------- - "status": state.status, - "strategy": state.strategy, - "symbol": state.symbol, - "cycle_number": state.cycle_number, - - # ---------- Signal ---------- - "signal": normalized_signal, - "signal_intent": signal_intent, - "confidence": confidence, - "reason": reason, - "repeat_count": state.last_signal_repeat_count, - - # ---------- Confirmation ---------- - "confirmation_seconds": state.signal_confirmation_seconds, - "confirmation_required_seconds": state.signal_confirmation_required_seconds, - "confirmation_missing_repeats": state.signal_confirmation_missing_repeats, - "confirmation_progress": state.signal_confirmation_progress, - "confirmation_reason": state.signal_confirmation_reason, - - # ---------- Decision ---------- - "decision_status": state.decision_status, - "decision_reason": state.decision_reason, - "is_signal_confirmed": state.is_signal_confirmed, - "is_signal_ready": state.is_signal_ready, - - # ---------- Position Context ---------- - "position_side": state.position_side, - "entry_price": state.entry_price, - "position_size": state.position_size, - "unrealized_pnl_usd": state.unrealized_pnl_usd, - "current_trade_id": state.current_trade_id, - "current_trade_cycle_number": state.current_trade_cycle_number, - - # ---------- Risk Settings ---------- - "risk_percent": state.risk_percent, - "stop_loss_percent": state.stop_loss_percent, - "take_profit_percent": state.take_profit_percent, - "max_loss_usd": state.max_loss_usd, - "max_reserved_balance_percent": state.max_reserved_balance_percent, - "allocated_balance_usd": state.allocated_balance_usd, - "leverage": state.leverage, - - # ---------- Execution Confidence ---------- - "execution_confidence_score": state.execution_confidence_score, - "execution_confidence_level": state.execution_confidence_level, - "execution_confidence_required_score": state.execution_confidence_required_score, - "execution_confidence_reason": state.execution_confidence_reason, - "execution_confidence_factors": state.execution_confidence_factors, - - # ---------- Execution Quality ---------- - "execution_quality": state.execution_quality, - "execution_quality_reason": state.execution_quality_reason, - "execution_quality_message": state.execution_quality_message, - "spread_percent": state.spread_percent, - "snapshot_age_seconds": state.snapshot_age_seconds, - - # ---------- Live Snapshot ---------- - "bid_price": snapshot.get("bid_price"), - "ask_price": snapshot.get("ask_price"), - "last_price": snapshot.get("last_price"), - - # ---------- Market Score ---------- - "market_score": state.market_score, - "market_score_label": state.market_score_label, - "market_long_score": state.market_long_score, - "market_short_score": state.market_short_score, - - # ---------- Market ---------- - "market_state": state.market_state, - "market_trend": state.market_trend, - "market_volatility": state.market_volatility, - "market_trend_strength": state.market_trend_strength, - "market_trend_quality": state.market_trend_quality, - "market_phase": state.market_phase, - "market_phase_direction": state.market_phase_direction, - - # ---------- Candle ---------- - "last_closed_candle_change_percent": state.last_closed_candle_change_percent, - "last_closed_candle_direction": state.last_closed_candle_direction, - "current_interval_change_percent": state.current_interval_change_percent, - "current_interval_direction": state.current_interval_direction, - "current_interval_label": state.current_interval_label, - - # ---------- Structure ---------- - "market_structure": state.market_structure, - "market_structure_reason": state.market_structure_reason, - - # ---------- Trend Quality ---------- - "market_trend_gap_percent": state.market_trend_gap_percent, - "market_trend_consistency": state.market_trend_consistency, - "market_trend_efficiency": state.market_trend_efficiency, - "trend_quality_score": state.trend_quality_score, - "ema_distance_atr_ratio": state.ema_distance_atr_ratio, - "ema_distance_state": state.ema_distance_state, - "entry_timing_state": state.entry_timing_state, - "entry_timing_reason": state.entry_timing_reason, - - # ---------- Momentum / Breakout ---------- - "momentum_state": state.momentum_state, - "momentum_direction": state.momentum_direction, - "momentum_change_percent": state.momentum_change_percent, - "momentum_strength": state.momentum_strength, - "breakout_level": state.breakout_level, - "breakout_distance_percent": state.breakout_distance_percent, - "breakout_reason": state.breakout_reason, - - # ---------- HTF ---------- - "htf_interval": state.htf_interval, - "htf_atr_percent": state.htf_atr_percent, - "htf_atr_percent_baseline": state.htf_atr_percent_baseline, - "htf_volatility_ratio": state.htf_volatility_ratio, - "htf_volatility": state.htf_volatility, - "htf_market_state": state.htf_market_state, - "htf_trend": state.htf_trend, - "htf_trend_strength": state.htf_trend_strength, - "htf_trend_quality": state.htf_trend_quality, - "htf_market_phase": state.htf_market_phase, - "htf_alignment": state.htf_alignment, - "htf_confirmation_score": state.htf_confirmation_score, - "htf_reason": state.htf_reason, - - # ---------- Runtime Health ---------- - "market_runtime_degraded": state.market_runtime_degraded, - "runtime_expired_reason": state.runtime_expired_reason, - "runtime_expired_message": state.runtime_expired_message, - "market_is_open": state.market_is_open, - "market_status": state.market_status, - "market_status_message": state.market_status_message, - }, + payload=self._build_ready_signal_payload( + state=state, + snapshot=snapshot, + signal=normalized_signal, + signal_intent=signal_intent, + confidence=confidence, + reason=reason, + ), ) except Exception: pass @@ -801,6 +851,22 @@ class AutoSignalRuntimeMixin: }, ) + # собрать payload события устаревания runtime данных без изменения состояния + def _build_runtime_expired_payload( + self, + *, + state: AutoTradeState, + payload: JsonDict, + reason: str, + ) -> JsonDict: + return { + **payload, + "symbol": state.symbol, + "strategy": state.strategy, + "status": state.status, + "runtime_expired_reason": reason, + } + # записать событие устаревания runtime данных def _log_runtime_expired_if_changed( self, @@ -823,17 +889,59 @@ class AutoSignalRuntimeMixin: message=message, screen="auto", action="runtime_expiration", - payload={ - **payload, - "symbol": state.symbol, - "strategy": state.strategy, - "status": state.status, - "runtime_expired_reason": reason, - }, + payload=self._build_runtime_expired_payload( + state=state, + payload=payload, + reason=reason, + ), ) except Exception: pass + # собрать диагностические факторы execution confidence + def _build_execution_confidence_factors( + self, + *, + state: AutoTradeState, + signal: str, + signal_score: float, + confirmation_score: float, + market_score: float, + execution_score: float, + ) -> JsonDict: + return { + "signal_score": round(signal_score, 3), + "confirmation_score": round(confirmation_score, 3), + "market_score": round(market_score, 3), + "market_score_raw": getattr(state, "market_score", None), + "market_score_label": getattr(state, "market_score_label", None), + "execution_score": round(execution_score, 3), + "required_score": self._execution_confidence_required_score, + "signal": signal, + "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, + "current_interval_change_percent": getattr(state, "current_interval_change_percent", None), + "current_interval_direction": getattr(state, "current_interval_direction", None), + "current_interval_label": getattr(state, "current_interval_label", None), + "market_structure": getattr(state, "market_structure", None), + "market_structure_reason": getattr(state, "market_structure_reason", None), + "htf_alignment": getattr(state, "htf_alignment", None), + "htf_confirmation_score": getattr(state, "htf_confirmation_score", None), + "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), + } + # синхронизировать итоговый execution confidence def _sync_execution_confidence_state( self, @@ -881,41 +989,14 @@ class AutoSignalRuntimeMixin: 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 здесь — направленная рыночная оценка 0.0..1.0 - # именно для текущего BUY / SELL сигнала. - # state.market_score — общая оценка рынка 0..100 без привязки к сигналу. - "market_score": round(market_score, 3), - "market_score_raw": getattr(state, "market_score", None), - "market_score_label": getattr(state, "market_score_label", None), - "execution_score": round(execution_score, 3), - "required_score": self._execution_confidence_required_score, - "signal": signal, - "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, - "current_interval_change_percent": getattr(state, "current_interval_change_percent", None), - "current_interval_direction": getattr(state, "current_interval_direction", None), - "current_interval_label": getattr(state, "current_interval_label", None), - "market_structure": getattr(state, "market_structure", None), - "market_structure_reason": getattr(state, "market_structure_reason", None), - "htf_alignment": getattr(state, "htf_alignment", None), - "htf_confirmation_score": getattr(state, "htf_confirmation_score", None), - "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), - } + state.execution_confidence_factors = self._build_execution_confidence_factors( + state=state, + signal=signal, + signal_score=signal_score, + confirmation_score=confirmation_score, + market_score=market_score, + execution_score=execution_score, + ) # рассчитать market confidence для итогового execution confidence def _market_confidence_score( diff --git a/docs/architecture/auto_refactoring.md b/docs/architecture/auto_refactoring.md new file mode 100644 index 0000000..ddf9b27 --- /dev/null +++ b/docs/architecture/auto_refactoring.md @@ -0,0 +1,495 @@ +# Auto refactoring roadmap + +Цель: безопасный поэтапный аудит и рефакторинг `app/src/trading/auto`. + +Принципы: + +- сначала аудит; +- без изменения торговой логики; +- без изменения AutoTradeState; +- без изменения ExecutionEngine; +- без изменения payload; +- без изменения EventBus / JournalService; +- каждый шаг проверяется перезапуском бота. + +## Файлы + +| Файл | Статус | Комментарий | +|---|---|---| +| __init__.py | Not audited | | +| service.py | Not audited | | +| autonomous_management.py | Not audited | | +| execution_semantic.py | Not audited | | +| market_runtime.py | Not audited | | +| position_health.py | Not audited | | +| execution_quality.py | Not audited | | +| position_semantics.py | Not audited | | +| auto_lifecycle.py | Not audited | | +| runner.py | Not audited | | +| signal_runtime.py | Not audited | | +| state.py | Not audited | | + +## Порядок аудита + +1. service.py +2. autonomous_management.py +3. execution_semantic.py +4. market_runtime.py +5. position_health.py +6. execution_quality.py +7. position_semantics.py +8. auto_lifecycle.py +9. runner.py +10. signal_runtime.py +11. state.py + +## Правила аудита + +Для каждого файла фиксируем: + +- назначение; +- размер и сложность; +- зависимости; +- что хорошо; +- что настораживает; +- безопасные улучшения; +- что нельзя менять; +- рекомендуемый следующий шаг. + +## service.py + +Статус: Completed (без изменений) + +Назначение: +- Фасад AutoTradeService. +- Хранит единый runtime AutoTradeState. +- Хранит class-level настройки auto loop, signal confirmation, TTL, execution quality и spread thresholds. + +Размер: +- Небольшой/средний. + +Связность: +- Зависит от AutoLifecycleMixin и AutoTradeState. +- Не содержит прямой торговой логики. + +Что хорошо: +- Нет JournalService/EventBus/payload. +- Нет execution-алгоритмов. +- Настройки читаются централизованно. + +Что настораживает: +- Много class-level state. +- Spread thresholds находятся прямо в service.py. + +Безопасные улучшения: +- Сейчас не требуются. + +Что нельзя менять: +- Значения thresholds. +- Class-level state. +- Названия runtime-полей. +- Наследование от AutoLifecycleMixin. + +Итог: +Файл выполняет роль конфигурационного фасада. Рефакторинг на текущем этапе не нужен. + +## autonomous_management.py + +Статус: Completed after minor cleanup + +Назначение: +- Синхронизация autonomous trade management state. +- Определяет autonomous action: HOLD / WATCH / PROTECT / REDUCE / EXIT. +- Обновляет autonomous required-флаги. + +Размер: +- Небольшой. + +Связность: +- Зависит от AutoTradeState и execution.constants. +- Не вызывает ExecutionEngine напрямую. + +Что хорошо: +- Один понятный метод. +- Нет JournalService/EventBus/payload. +- Нет прямого закрытия позиции. +- Используются константы. + +Безопасные улучшения: +- Только косметика форматирования. + +Что нельзя менять: +- Порядок выбора autonomous action. +- Thresholds confidence. +- Логику aggressive exit escalation. +- Сброс autonomous-state при отсутствии позиции. + +Итог: +Файл чистый. Логических правок не требуется. + +## execution_semantic.py + +Статус: Completed (без изменений) + +Назначение: +- Синхронизация semantic-статуса execution слоя для UI. +- Формирует execution_semantic_status/message/reason. +- Преобразует технические причины блокировки в человекочитаемые UI-сообщения. + +Размер: +- Небольшой. + +Связность: +- Зависит от AutoTradeState. +- Использует ExchangeStatusCode для совместимости с exchange status layer. + +Что хорошо: +- Нет JournalService/EventBus/payload. +- Нет торговой логики. +- Нет прямых вызовов ExecutionEngine. +- Логика UI-сообщений отделена от execution layer. + +Что настораживает: +- Строковые статусы `"BLOCKED"`, `"READY"`, `"CONFIRMING"`, `"NONE"` пока используются напрямую. +- Но сейчас это не трогаем, чтобы не менять контракт state/UI. + +Безопасные улучшения: +- Сейчас не требуются. + +Что нельзя менять: +- Приоритеты semantic status. +- Тексты UI-сообщений. +- Совместимость с ExchangeStatusCode. +- Значения execution_semantic_status. + +Итог: +Файл выполняет UI-semantic роль и не требует refactoring на текущем этапе. + +## market_runtime.py + +Статус: Audited / candidate for safe payload extraction + +Назначение: +- Синхронизация market analysis payload в AutoTradeState. +- Логирование market state / volatility изменений. +- Логирование entry-block событий. + +Размер: +- Средний. + +Связность: +- Зависит от AutoTradeState, JournalService, JsonDict. +- Не вызывает ExecutionEngine напрямую. + +Что хорошо: +- Dedupe market-событий по symbol/strategy. +- Entry-block logging имеет TTL. +- Нет торговых действий. + +Что настораживает: +- Большой inline payload внутри `_log_entry_block_if_changed()`. +- `_sync_market_analysis_state()` много полей переносит из payload в state, но это существующий mapping. + +Безопасные улучшения: +- Вынести entry-block payload в `_build_entry_block_payload()`. + +Что нельзя менять: +- Mapping payload → AutoTradeState. +- Dedupe key. +- Entry-block TTL. +- Journal event_type/action. +- Содержимое payload. + +Итог: +Файл рабочий. Первый безопасный шаг — вынести payload builder без изменения поведения. + +### market_runtime.py + +Выполнено: +- Вынесен `_build_entry_block_payload()`. +- Логика `_log_entry_block_if_changed()` стала отвечать только за dedupe и запись в Journal. +- Содержимое payload не изменено. +- Поведение полностью сохранено. + +Статус: Completed + +Выполнено: +- Вынесен `_build_entry_block_payload()`. +- Вынесен `_build_market_state_payload()`. +- Методы логирования теперь отвечают только за: + - dedupe; + - принятие решения; + - запись в Journal. +- Формирование payload полностью изолировано. +- Поведение не изменилось. + +## position_health.py + +Статус: Completed (без изменений) + +Назначение: +- Runtime health/risk оценка открытой позиции. +- Формирует pressure, health score/status/reason, risk level/reason, trend alignment, adverse momentum и exit pressure. + +Размер: +- Средний/большой. + +Связность: +- Зависит от AutoTradeState, safe_float и execution.constants. +- Использует get_position_health_thresholds(). + +Что хорошо: +- PnL и hold time не рассчитываются здесь. +- Runtime metrics приходят из execution/position_runtime.py. +- Нет JournalService/EventBus/payload. +- Методы разделены по смыслу. +- Thresholds вынесены в constants. + +Что настораживает: +- Чувствительный файл: влияет на autonomous management, runtime actions, protection и exit decision. +- Много бизнес-условий. +- Много строковых runtime states. + +Безопасные улучшения: +- Сейчас не требуются. + +Что нельзя менять: +- Health score формулу. +- Risk level порядок условий. +- Trend alignment logic. +- Adverse momentum logic. +- Exit pressure logic. +- Threshold constants. + +Итог: +Файл архитектурно понятный. На текущем safe stage оставляем без изменений. + +## execution_quality.py + +Статус: Audited / candidate for safe payload extraction + +Назначение: +- Оценка качества исполнения. +- Синхронизация exchange availability. +- Проверка свежести market snapshot. +- Проверка spread. +- Синхронизация execution pricing fields. +- Логирование execution quality изменений. +- Расчёт execution quality confidence score. + +Размер: +- Большой. + +Связность: +- Зависит от AutoTradeState, ExchangeService, ExchangeRuntimeStatus, JournalService. +- Не вызывает ExecutionEngine напрямую. + +Что хорошо: +- Exchange availability вынесена отдельно. +- Spread quality вынесен отдельно. +- Execution pricing sync вынесен отдельно. +- Confidence score вынесен отдельно. +- Нет торговых действий. + +Что настораживает: +- Inline payload в `_log_exchange_availability_if_changed()`. +- Inline payload в `_log_execution_quality_if_changed()`. +- `_sync_execution_quality_state()` большой и чувствительный. + +Безопасные улучшения: +- Вынести `_build_exchange_availability_payload()`. +- Вынести `_build_execution_quality_payload()`. + +Что нельзя менять: +- Exchange status mapping. +- Spread thresholds. +- Snapshot refresh logic. +- Fallback price behavior. +- Execution quality transitions. +- Deduplication key. +- Journal event_type/action. +- Confidence score mapping. + +Итог: +Файл рабочий. Первый безопасный этап — вынести payload builders без изменения поведения. + +### execution_quality.py + +Выполнено: +- Вынесен `_build_exchange_availability_payload()`. +- Вынесен `_build_execution_quality_payload()`. +- Логика exchange availability не менялась. +- Логика spread/snapshot quality не менялась. +- Journal payload не изменён. +- Поведение полностью сохранено. + +Статус: Completed (safe refactoring stage 1) + +## position_semantics.py + +Статус: Completed after minor cleanup + +Назначение: +- Semantic/intelligence состояние открытой позиции. +- Формирует lifecycle stage, hold quality, decay state, exit confidence, exit signal, recommended action. +- Считает advanced analytics: MFE, MAE, giveback, fatigue, conviction, reversal risk, stall state. + +Размер: +- Большой. + +Связность: +- Зависит от AutoTradeState, safe_float и execution.constants. +- Не вызывает ExecutionEngine напрямую. + +Что хорошо: +- Нет JournalService/EventBus/payload. +- Нет прямых торговых действий. +- Thresholds вынесены в constants. +- Методы разделены по смыслу. + +Что настораживает: +- Очень чувствительный файл: влияет на autonomous management, runtime actions, protection и exit decision. +- Много бизнес-условий. +- Много строковых semantic states. +- `_sync_position_semantics_state()` управляет большим количеством полей AutoTradeState. + +Безопасные улучшения: +- Только косметика форматирования. + +Что нельзя менять: +- Порядок расчёта lifecycle/advanced analytics/stall/exit confidence. +- Формулы exit confidence. +- Логику damping. +- MFE/MAE/giveback/fatigue/conviction/reversal/stall. +- Threshold constants. +- Значения semantic states. + +Итог: +Файл архитектурно понятный, но чувствительный. На текущем safe stage логических правок не требуется. + +## auto_lifecycle.py + +Статус: Audited / candidate for safe payload extraction + +Назначение: +- Lifecycle автоторговли: start / observe / stop. +- Управление background loop. +- Основной run_cycle pipeline. +- Reset signal/runtime tracking. + +Размер: +- Большой. + +Связность: +- Собирает auto mixins. +- Использует ExecutionEngine, StrategyRegistry, EventBus, JournalService. + +Что хорошо: +- run_cycle имеет понятный pipeline. +- Execution делегирован ExecutionEngine. +- Market, signal, health, semantics, autonomous management вынесены в mixins. + +Что настораживает: +- Большой `_reset_signal_tracking()`. +- Inline payload в `_log_auto_status_changed()`. +- Дубли reset-полей в start/observe/stop. + +Безопасные улучшения: +- Вынести `_build_auto_status_changed_payload()`. + +Что нельзя менять: +- Порядок run_cycle. +- start/observe/stop поведение. +- Reset signal tracking. +- EventBus события. +- Journal payload. + +Итог: +Файл рабочий. Первый безопасный шаг — вынести payload builder без изменения поведения. + +## runner.py + +Статус: Audited / candidate for safe payload extraction + +Назначение: +- Background runner автоторговли. +- Обработка EventBus событий. +- Публикация RuntimeEvent уведомлений. +- Обновление Telegram auto screen. + +Безопасные улучшения: +- Вынести payload builders для notification/journal событий. + +Что нельзя менять: +- Worker loop. +- EventBus version handling. +- Dedupe keys. +- Telegram refresh logic. +- RuntimeEvent payload. + +Выполнено: +- Вынесен `_build_position_aligned_signal_suppressed_payload()`. +- Вынесен `_build_runtime_signal_payload()`. +- Вынесен `_build_runtime_execution_payload()`. +- Payload подавленного aligned-сигнала не изменён. +- Логика dedupe/TTL не менялась. +- RuntimeEvent payload не изменялся. +- Dedupe keys не менялись. +- Telegram refresh logic не менялась. + +Статус: Completed (safe refactoring stage 1) + +## signal_runtime.py + +Статус: Audited / candidate for safe payload extraction + +Назначение: +- Signal runtime tracking. +- Signal confirmation. +- Decision state. +- Runtime expiration. +- Execution confidence. +- Market confidence. + +Безопасные улучшения: +- Вынести payload builders. +- Начать с `_build_runtime_expired_payload()`. + +Что нельзя менять: +- Confirmation logic. +- READY/BLOCKED logic. +- Execution confidence formula. +- Market confidence formula. +- Runtime TTL reset logic. +- EventBus events. + +Выполнено: +- Вынесен `_build_runtime_expired_payload()`. +- Вынесен `_build_ready_signal_payload()`. +- Вынесен `_build_signal_summary_payload()`. +- Вынесен `_build_execution_confidence_factors()`. + +Что не менялось: +- Confirmation logic. +- READY/BLOCKED logic. +- Runtime TTL reset logic. +- Execution confidence formula. +- Market confidence formula. +- EventBus events. +- Journal event_type/action. + +Статус: Completed (safe refactoring stage 1) + +### state.py + +Проверено полностью. + +Изменения не требуются. + +Причина: +- dataclass не содержит бизнес-логики; +- поля уже сгруппированы по подсистемам; +- выделение вложенных dataclass приведёт к массовым изменениям во всём проекте без архитектурной пользы. + +Статус: +Completed (no changes required) \ No newline at end of file