Stage 07.4.4.1.15 — Runtime Payload Builders & Auto Runtime Refactoring

This commit is contained in:
2026-07-02 20:40:04 +03:00
parent 3c336b10fb
commit 2785f83260
8 changed files with 1069 additions and 340 deletions

View File

@@ -41,6 +41,29 @@ class AutoLifecycleMixin(
_confirm_repeats: int _confirm_repeats: int
_execution_confidence_required_score: float _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( def _log_auto_status_changed(
self, self,
*, *,
@@ -56,20 +79,11 @@ class AutoLifecycleMixin(
message=message, message=message,
screen="auto", screen="auto",
action=action, action=action,
payload={ payload=self._build_auto_status_changed_payload(
"previous_status": previous_status, previous_status=previous_status,
"new_status": new_status, new_status=new_status,
"symbol": state.symbol, state=state,
"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 set_allocated_balance_usd(self, value: NumericLike) -> AutoTradeState: def set_allocated_balance_usd(self, value: NumericLike) -> AutoTradeState:

View File

@@ -143,6 +143,29 @@ class AutoExecutionQualityMixin:
return "MARKET_BREAK" 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( def _log_exchange_availability_if_changed(
self, self,
@@ -167,20 +190,11 @@ class AutoExecutionQualityMixin:
message=status.ui_line, message=status.ui_line,
screen="auto", screen="auto",
action="exchange_status", action="exchange_status",
payload={ payload=self._build_exchange_availability_payload(
"status": state.status, state=state,
"symbol": state.symbol, status=status,
"strategy": state.strategy, reason=reason,
"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,
},
) )
except Exception: except Exception:
pass pass
@@ -437,6 +451,20 @@ class AutoExecutionQualityMixin:
else: else:
state.execution_price_freshness = "STALE" 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 # записать событие изменения execution quality
def _log_execution_quality_if_changed( def _log_execution_quality_if_changed(
self, self,
@@ -462,12 +490,10 @@ class AutoExecutionQualityMixin:
return return
try: try:
log_payload = { log_payload = self._build_execution_quality_payload(
**payload, state=state,
"status": state.status, payload=payload,
"symbol": state.symbol, )
"strategy": state.strategy,
}
if quality == "BLOCKED": if quality == "BLOCKED":
JournalService().log_ui_warning( JournalService().log_ui_warning(

View File

@@ -140,47 +140,17 @@ class AutoMarketRuntimeMixin:
payload=payload, payload=payload,
) )
# записать entry-block событие, если причина изменилась или истёк TTL # собрать payload entry-block события без изменения состояния
def _log_entry_block_if_changed( def _build_entry_block_payload(
self, self,
*, *,
state: AutoTradeState, state: AutoTradeState,
payload: JsonDict, payload: JsonDict,
) -> None: reason: str,
reason = state.entry_block_reason message: str,
message = state.entry_block_message key: str,
) -> JsonDict:
if not reason or not message: return {
return
now = time.monotonic()
# status специально не входит в key:
# RUNNING / OBSERVING не должны создавать дубли одной и той же причины.
key = f"{state.symbol}:{state.strategy}:{reason}:{message}"
last_logged_at = type(self)._last_logged_entry_block_at
ttl_expired = (
last_logged_at is None
or now - last_logged_at >= type(self)._entry_block_log_ttl_seconds
)
if (
key == type(self)._last_logged_entry_block_reason
and not ttl_expired
):
return
type(self)._last_logged_entry_block_reason = key
type(self)._last_logged_entry_block_at = now
try:
JournalService().log_ui_info(
event_type="entry_blocked",
message=f"Вход в позицию не выполнен: {message}.",
screen="auto",
action="entry_diagnostics",
payload={
**payload, **payload,
"entry_block_reason": reason, "entry_block_reason": reason,
"entry_block_message": message, "entry_block_message": message,
@@ -222,11 +192,81 @@ class AutoMarketRuntimeMixin:
"last_signal": state.last_signal, "last_signal": state.last_signal,
"last_signal_confidence": state.last_signal_confidence, "last_signal_confidence": state.last_signal_confidence,
"last_signal_reason": state.last_signal_reason, "last_signal_reason": state.last_signal_reason,
}, }
# записать entry-block событие, если причина изменилась или истёк TTL
def _log_entry_block_if_changed(
self,
*,
state: AutoTradeState,
payload: JsonDict,
) -> None:
reason = state.entry_block_reason
message = state.entry_block_message
if not reason or not message:
return
now = time.monotonic()
# status специально не входит в key:
# RUNNING / OBSERVING не должны создавать дубли одной и той же причины.
key = f"{state.symbol}:{state.strategy}:{reason}:{message}"
last_logged_at = type(self)._last_logged_entry_block_at
ttl_expired = (
last_logged_at is None
or now - last_logged_at >= type(self)._entry_block_log_ttl_seconds
)
if (
key == type(self)._last_logged_entry_block_reason
and not ttl_expired
):
return
type(self)._last_logged_entry_block_reason = key
type(self)._last_logged_entry_block_at = now
try:
JournalService().log_ui_info(
event_type="entry_blocked",
message=f"Вход в позицию не выполнен: {message}.",
screen="auto",
action="entry_diagnostics",
payload=self._build_entry_block_payload(
state=state,
payload=payload,
reason=reason,
message=message,
key=key,
),
) )
except Exception: except Exception:
pass 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 событие, если состояние изменилось # записать market state / volatility событие, если состояние изменилось
def _log_market_state_if_changed( def _log_market_state_if_changed(
self, self,
@@ -269,15 +309,15 @@ class AutoMarketRuntimeMixin:
if not state_changed and not volatility_changed: if not state_changed and not volatility_changed:
return return
journal_payload = { journal_payload = self._build_market_state_payload(
**payload, payload=payload,
"previous_market_state": previous_market_state, previous_market_state=previous_market_state,
"previous_market_trend": previous_market_trend, previous_market_trend=previous_market_trend,
"previous_market_volatility": previous_market_volatility, previous_market_volatility=previous_market_volatility,
"current_market_state": market_state, current_market_state=market_state,
"current_market_trend": market_trend, current_market_trend=market_trend,
"current_market_volatility": market_volatility, current_market_volatility=market_volatility,
} )
try: try:
if state_changed: if state_changed:

View File

@@ -388,6 +388,31 @@ class AutoTradeRunner:
return False 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 @classmethod
def _log_position_aligned_signal_suppressed( def _log_position_aligned_signal_suppressed(
cls, cls,
@@ -444,16 +469,15 @@ class AutoTradeRunner:
), ),
screen="auto", screen="auto",
action="signal_notification", action="signal_notification",
payload={ payload=cls._build_position_aligned_signal_suppressed_payload(
"symbol": symbol, symbol=symbol,
"strategy": strategy, strategy=strategy,
"signal": signal, signal=signal,
"position_side": position_side, position_side=position_side,
"confidence": confidence, confidence=confidence,
"repeat_count": repeat_count, repeat_count=repeat_count,
"reason": payload.get("reason") or state.last_signal_reason, reason=payload.get("reason") or state.last_signal_reason,
"suppression_interval_seconds": cls._position_aligned_signal_log_interval_seconds, ),
},
) )
except Exception: except Exception:
pass 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 @classmethod
def _publish_strong_signal_event( def _publish_strong_signal_event(
cls, cls,
@@ -573,32 +637,21 @@ class AutoTradeRunner:
event_type=RuntimeEventType.AUTO_SIGNAL_READY, event_type=RuntimeEventType.AUTO_SIGNAL_READY,
source="auto_trade_runner", source="auto_trade_runner",
title=f"Auto strong signal {signal}", title=f"Auto strong signal {signal}",
payload={ payload=cls._build_runtime_signal_payload(
"execution_block_title": getattr(state, "execution_block_title", None), state=state,
"execution_block_message": getattr(state, "execution_block_message", None), symbol=symbol,
"execution_block_action": getattr(state, "execution_block_action", None), strategy=strategy,
"symbol": symbol, signal=signal,
"strategy": strategy, signal_intent=signal_intent,
"signal": signal, repeat_count=repeat_count,
"signal_intent": signal_intent, confidence=confidence,
"repeat_count": repeat_count, leverage=leverage,
"confidence": confidence, reason=reason,
"leverage": leverage, position_context=position_context,
"reason": reason, is_aligned_signal=is_aligned_signal,
"position_context": position_context, semantic_lines=semantic_lines,
"position_side": position_context, price_payload=price_payload,
"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,
},
priority=priority.lower(), priority=priority.lower(),
dedupe_key=( dedupe_key=(
f"auto_signal_ready:" 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 @classmethod
def _publish_execution_event( def _publish_execution_event(
cls, cls,
@@ -636,27 +720,16 @@ class AutoTradeRunner:
event_type=runtime_event_type, event_type=runtime_event_type,
source="auto_trade_runner", source="auto_trade_runner",
title=cls._execution_event_title(runtime_event_type), title=cls._execution_event_title(runtime_event_type),
payload={ payload=cls._build_runtime_execution_payload(
**payload, state=state,
"source_event_type": event_type, payload=payload,
"symbol": symbol, event_type=event_type,
"side": side, symbol=symbol,
"old_side": old_side, side=side,
"new_side": new_side, old_side=old_side,
"leverage": ( new_side=new_side,
payload.get("leverage") semantic_lines=semantic_lines,
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,
},
priority="normal", priority="normal",
dedupe_key=cls._execution_dedupe_key( dedupe_key=cls._execution_dedupe_key(
runtime_event_type=runtime_event_type, runtime_event_type=runtime_event_type,

View File

@@ -439,6 +439,39 @@ class AutoSignalRuntimeMixin:
) -> None: ) -> None:
return 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( def _log_signal_summary(
self, self,
@@ -467,57 +500,35 @@ class AutoSignalRuntimeMixin:
), ),
screen="auto", screen="auto",
action="signal_summary", action="signal_summary",
payload={ payload=self._build_signal_summary_payload(
"strategy": strategy_name, strategy_name=strategy_name,
"status": state.status, state=state,
"symbol": state.symbol, previous_signal=previous_signal,
"signal": previous_signal, next_signal=next_signal,
"next_signal": next_signal, signal_intent=signal_intent,
"signal_intent": signal_intent, previous_count=previous_count,
"repeat_count": previous_count, duration_seconds=duration_seconds,
"duration_seconds": duration_seconds, duration_text=duration_text,
"duration_text": duration_text, confidence=confidence,
"confidence": confidence, reason=reason,
"reason": reason, payload=payload,
"is_strong_signal": False, ),
"is_aggregated": True,
"payload": payload or {},
},
) )
except Exception: except Exception:
pass pass
# записать событие готовности сигнала к исполнению # собрать payload события готовности сигнала
def _log_ready_signal( def _build_ready_signal_payload(
self, self,
*, *,
state: AutoTradeState, state: AutoTradeState,
signal: str | None, snapshot: JsonDict,
reason: str, signal: str,
confidence: float,
signal_intent: str, signal_intent: str,
) -> None: confidence: float,
normalized_signal = (signal or "HOLD").upper() reason: str,
if normalized_signal not in {"BUY", "SELL"}: ) -> JsonDict:
return return {
try:
snapshot = ExchangeService().get_market_snapshot(
state.symbol,
runtime_key="auto",
)
except Exception:
snapshot = {}
try:
JournalService().log_ui_info(
event_type="signal_ready",
message=(
f"Сигнал {normalized_signal} подтверждён и готов к исполнению."
),
screen="auto",
action="signal_ready",
payload={
# ---------- Event ---------- # ---------- Event ----------
"event_type": "signal_ready", "event_type": "signal_ready",
"action": "signal_ready", "action": "signal_ready",
@@ -531,7 +542,7 @@ class AutoSignalRuntimeMixin:
"cycle_number": state.cycle_number, "cycle_number": state.cycle_number,
# ---------- Signal ---------- # ---------- Signal ----------
"signal": normalized_signal, "signal": signal,
"signal_intent": signal_intent, "signal_intent": signal_intent,
"confidence": confidence, "confidence": confidence,
"reason": reason, "reason": reason,
@@ -653,7 +664,46 @@ class AutoSignalRuntimeMixin:
"market_is_open": state.market_is_open, "market_is_open": state.market_is_open,
"market_status": state.market_status, "market_status": state.market_status,
"market_status_message": state.market_status_message, "market_status_message": state.market_status_message,
}, }
# записать событие готовности сигнала к исполнению
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
try:
snapshot = ExchangeService().get_market_snapshot(
state.symbol,
runtime_key="auto",
)
except Exception:
snapshot = {}
try:
JournalService().log_ui_info(
event_type="signal_ready",
message=(
f"Сигнал {normalized_signal} подтверждён и готов к исполнению."
),
screen="auto",
action="signal_ready",
payload=self._build_ready_signal_payload(
state=state,
snapshot=snapshot,
signal=normalized_signal,
signal_intent=signal_intent,
confidence=confidence,
reason=reason,
),
) )
except Exception: except Exception:
pass 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 данных # записать событие устаревания runtime данных
def _log_runtime_expired_if_changed( def _log_runtime_expired_if_changed(
self, self,
@@ -823,17 +889,59 @@ class AutoSignalRuntimeMixin:
message=message, message=message,
screen="auto", screen="auto",
action="runtime_expiration", action="runtime_expiration",
payload={ payload=self._build_runtime_expired_payload(
**payload, state=state,
"symbol": state.symbol, payload=payload,
"strategy": state.strategy, reason=reason,
"status": state.status, ),
"runtime_expired_reason": reason,
},
) )
except Exception: except Exception:
pass 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 # синхронизировать итоговый execution confidence
def _sync_execution_confidence_state( def _sync_execution_confidence_state(
self, self,
@@ -881,41 +989,14 @@ class AutoSignalRuntimeMixin:
state.execution_confidence_required_score = self._execution_confidence_required_score state.execution_confidence_required_score = self._execution_confidence_required_score
state.execution_confidence_level = self._execution_confidence_level(score) state.execution_confidence_level = self._execution_confidence_level(score)
state.execution_confidence_reason = self._execution_confidence_reason(state) state.execution_confidence_reason = self._execution_confidence_reason(state)
state.execution_confidence_factors = { state.execution_confidence_factors = self._build_execution_confidence_factors(
"signal_score": round(signal_score, 3), state=state,
"confirmation_score": round(confirmation_score, 3), signal=signal,
# market_score здесь — направленная рыночная оценка 0.0..1.0 signal_score=signal_score,
# именно для текущего BUY / SELL сигнала. confirmation_score=confirmation_score,
# state.market_score — общая оценка рынка 0..100 без привязки к сигналу. market_score=market_score,
"market_score": round(market_score, 3), execution_score=execution_score,
"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),
}
# рассчитать market confidence для итогового execution confidence # рассчитать market confidence для итогового execution confidence
def _market_confidence_score( def _market_confidence_score(

View File

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