Add flip execution diagnostics

This commit is contained in:
2026-07-03 12:21:07 +03:00
parent c608072b7b
commit 4d58fcf2bf
2 changed files with 90 additions and 1 deletions

View File

@@ -52,6 +52,7 @@ class ExecutionEngine(
_flip_cooldown_seconds = 45 _flip_cooldown_seconds = 45
_loss_flip_confidence = 0.75 _loss_flip_confidence = 0.75
_last_flip_block_key: str | None = None _last_flip_block_key: str | None = None
_last_flip_diagnostic_key: str | None = None
_runtime_action_cooldown_seconds = 30 _runtime_action_cooldown_seconds = 30
_last_runtime_action_key: str | None = None _last_runtime_action_key: str | None = None
_emergency_halt_drawdown_usd = 250.0 _emergency_halt_drawdown_usd = 250.0
@@ -107,6 +108,18 @@ class ExecutionEngine(
if protection_decision is not None: if protection_decision is not None:
return protection_decision return protection_decision
# Flip diagnostics before READY / supervisor.
# Это не меняет торговую логику: только фиксирует, что противоположный
# сигнал появился при уже открытой позиции.
flip_requested = self._should_flip_position(state)
if flip_requested:
self._log_flip_diagnostic(
state=state,
stage="FLIP_REQUESTED",
reason="opposite signal while position is open",
)
# Signal readiness validation # Signal readiness validation
if state.decision_status != EXECUTION_DECISION_READY or not state.is_signal_ready: if state.decision_status != EXECUTION_DECISION_READY or not state.is_signal_ready:
reason = ( reason = (
@@ -115,6 +128,13 @@ class ExecutionEngine(
f"ready={state.is_signal_ready})." f"ready={state.is_signal_ready})."
) )
if flip_requested:
self._log_flip_diagnostic(
state=state,
stage="FLIP_BLOCKED_BY_READY",
reason=reason,
)
return self._skip_execution( return self._skip_execution(
state, state,
reason, reason,
@@ -123,6 +143,13 @@ class ExecutionEngine(
# Execution supervisor # Execution supervisor
supervisor_decision = self._process_execution_supervisor(state) supervisor_decision = self._process_execution_supervisor(state)
if supervisor_decision is not None: if supervisor_decision is not None:
if flip_requested:
self._log_flip_diagnostic(
state=state,
stage="FLIP_BLOCKED_BY_SUPERVISOR",
reason=supervisor_decision.reason,
)
return supervisor_decision return supervisor_decision
# Existing position validation # Existing position validation
@@ -143,7 +170,7 @@ class ExecutionEngine(
) )
# Position flip # Position flip
if self._should_flip_position(state): if flip_requested:
flip_block_reason = self._flip_block_reason(state) flip_block_reason = self._flip_block_reason(state)
if flip_block_reason is not None: if flip_block_reason is not None:

View File

@@ -58,6 +58,7 @@ class _ExecutionFlipProtocol(Protocol):
_flip_cooldown_seconds: int _flip_cooldown_seconds: int
_loss_flip_confidence: float _loss_flip_confidence: float
_last_flip_block_key: str | None _last_flip_block_key: str | None
_last_flip_diagnostic_key: str | None
def _create_trade_id(self, state: AutoTradeState, side: str) -> str: def _create_trade_id(self, state: AutoTradeState, side: str) -> str:
... ...
@@ -111,6 +112,67 @@ class _ExecutionFlipProtocol(Protocol):
class ExecutionFlipMixin(_ExecutionFlipProtocol): class ExecutionFlipMixin(_ExecutionFlipProtocol):
# ---------- Diagnostics ----------
# записать диагностическое событие по пути flip без изменения торговой логики
def _log_flip_diagnostic(
self,
*,
state: AutoTradeState,
stage: str,
reason: str,
) -> None:
position = type(self)._position
confidence = safe_float(state.last_signal_confidence) or 0.0
repeat_count = int(safe_float(state.last_signal_repeat_count) or 0)
key = (
f"{stage}:"
f"{state.symbol}:"
f"{position.side}:"
f"{state.last_signal}:"
f"{repeat_count}:"
f"{confidence:.2f}:"
f"{reason}"
)
if key == type(self)._last_flip_diagnostic_key:
return
type(self)._last_flip_diagnostic_key = key
payload = {
"execution_type": "FLIP_DIAGNOSTIC",
"stage": stage,
"reason": reason,
"symbol": state.symbol,
"position_side": position.side,
"signal": state.last_signal,
"signal_confidence": confidence,
"signal_repeat_count": repeat_count,
"decision_status": state.decision_status,
"is_signal_ready": state.is_signal_ready,
"is_signal_confirmed": state.is_signal_confirmed,
"execution_confidence_score": state.execution_confidence_score,
"execution_confidence_required_score": (
state.execution_confidence_required_score
),
"execution_block_reason": state.execution_block_reason,
"entry_block_reason": state.entry_block_reason,
"entry_block_message": state.entry_block_message,
"unrealized_pnl_usd": state.unrealized_pnl_usd,
**build_market_context_payload(state),
}
JournalService().log_ui_info(
event_type="position_flip_diagnostic",
message=f"Flip diagnostic: {stage} · {reason}",
screen="auto",
action="paper_execution",
payload=payload,
)
EventBus.emit("paper_flip_diagnostic", payload)
# ---------- Payload builders ---------- # ---------- Payload builders ----------
# собрать payload отказа flip без изменения состояния # собрать payload отказа flip без изменения состояния
def _build_flip_rejected_payload( def _build_flip_rejected_payload(