Add detailed flip diagnostics
This commit is contained in:
@@ -113,7 +113,9 @@ class _ExecutionFlipProtocol(Protocol):
|
||||
|
||||
class ExecutionFlipMixin(_ExecutionFlipProtocol):
|
||||
# ---------- Diagnostics ----------
|
||||
# записать диагностическое событие по пути flip без изменения торговой логики
|
||||
# Записать диагностическое событие по пути flip без изменения торговой логики.
|
||||
# Эти события нужны, чтобы понять, где именно разворот был остановлен:
|
||||
# READY, supervisor, flip guard, price, sizing или успешное исполнение.
|
||||
def _log_flip_diagnostic(
|
||||
self,
|
||||
*,
|
||||
@@ -125,6 +127,8 @@ class ExecutionFlipMixin(_ExecutionFlipProtocol):
|
||||
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}:"
|
||||
@@ -174,7 +178,7 @@ class ExecutionFlipMixin(_ExecutionFlipProtocol):
|
||||
EventBus.emit("paper_flip_diagnostic", payload)
|
||||
|
||||
# ---------- Payload builders ----------
|
||||
# собрать payload отказа flip без изменения состояния
|
||||
# Собрать payload отказа flip без изменения состояния позиции.
|
||||
def _build_flip_rejected_payload(
|
||||
self,
|
||||
*,
|
||||
@@ -197,7 +201,7 @@ class ExecutionFlipMixin(_ExecutionFlipProtocol):
|
||||
"updated_at": position.updated_at,
|
||||
}
|
||||
|
||||
# собрать payload блокировки flip без изменения состояния
|
||||
# Собрать payload блокировки flip guard'ами без изменения состояния позиции.
|
||||
def _build_flip_blocked_payload(
|
||||
self,
|
||||
*,
|
||||
@@ -211,21 +215,20 @@ class ExecutionFlipMixin(_ExecutionFlipProtocol):
|
||||
"execution_type": EXECUTION_TYPE_FLIP_BLOCKED,
|
||||
"symbol": state.symbol,
|
||||
"position_side": position.side,
|
||||
|
||||
**build_signal_payload(
|
||||
state,
|
||||
confidence=confidence,
|
||||
reason=reason,
|
||||
),
|
||||
**build_execution_quality_payload(state),
|
||||
|
||||
"unrealized_pnl_usd": state.unrealized_pnl_usd,
|
||||
**build_market_context_payload(state),
|
||||
"opened_at": position.opened_at,
|
||||
"updated_at": position.updated_at,
|
||||
}
|
||||
|
||||
# собрать payload выполненного flip без изменения состояния
|
||||
# Собрать payload выполненного flip.
|
||||
# Здесь фиксируем и закрытую старую позицию, и параметры новой позиции.
|
||||
def _build_flip_executed_payload(
|
||||
self,
|
||||
*,
|
||||
@@ -311,6 +314,7 @@ class ExecutionFlipMixin(_ExecutionFlipProtocol):
|
||||
# ---------- Runtime protection ----------
|
||||
**build_runtime_protection_payload(state),
|
||||
|
||||
# ---------- Pricing diagnostics ----------
|
||||
"pricing": PRICING_FLIP_MODE,
|
||||
"exit_pricing_role": exit_execution.pricing_role,
|
||||
"exit_price_source": exit_execution.source,
|
||||
@@ -323,7 +327,9 @@ class ExecutionFlipMixin(_ExecutionFlipProtocol):
|
||||
}
|
||||
|
||||
# ---------- Journal helpers ----------
|
||||
# записать отказ flip execution в журнал
|
||||
# Записать отказ flip execution в журнал.
|
||||
# Reject отличается от block: reject происходит уже внутри попытки исполнения,
|
||||
# например из-за отсутствия цены или невозможности рассчитать size.
|
||||
def _log_flip_rejected(
|
||||
self,
|
||||
*,
|
||||
@@ -344,17 +350,26 @@ class ExecutionFlipMixin(_ExecutionFlipProtocol):
|
||||
)
|
||||
|
||||
# ---------- Decision helpers ----------
|
||||
# записать отказ flip и вернуть стандартное решение без исполнения
|
||||
# Записать отказ flip и вернуть стандартное решение без исполнения.
|
||||
# diagnostic_stage указывает, на каком техническом этапе flip был отклонён.
|
||||
def _reject_flip(
|
||||
self,
|
||||
*,
|
||||
state: AutoTradeState,
|
||||
reason: str,
|
||||
diagnostic_stage: str | None = None,
|
||||
) -> ExecutionDecision:
|
||||
if diagnostic_stage is not None:
|
||||
self._log_flip_diagnostic(
|
||||
state=state,
|
||||
stage=diagnostic_stage,
|
||||
reason=reason,
|
||||
)
|
||||
|
||||
self._log_flip_rejected(state=state, reason=reason)
|
||||
return ExecutionDecision(EXECUTION_ACTION_NONE, False, reason)
|
||||
|
||||
# записать блокировку flip в state, journal и event bus
|
||||
# Записать блокировку flip guard'ами в state, journal и event bus.
|
||||
def _block_flip(
|
||||
self,
|
||||
state: AutoTradeState,
|
||||
@@ -363,6 +378,14 @@ class ExecutionFlipMixin(_ExecutionFlipProtocol):
|
||||
position = type(self)._position
|
||||
confidence = safe_float(state.last_signal_confidence) or 0.0
|
||||
|
||||
# Диагностика отдельно показывает, что flip дошёл до flip.py,
|
||||
# но был остановлен именно flip-specific guard'ами.
|
||||
self._log_flip_diagnostic(
|
||||
state=state,
|
||||
stage="FLIP_BLOCKED_BY_FLIP_GUARD",
|
||||
reason=reason,
|
||||
)
|
||||
|
||||
state.execution_block_reason = reason
|
||||
state.last_flip_block_reason = reason
|
||||
state.last_execution_action = EXECUTION_ACTION_FLIP_BLOCKED
|
||||
@@ -398,7 +421,8 @@ class ExecutionFlipMixin(_ExecutionFlipProtocol):
|
||||
return ExecutionDecision(EXECUTION_ACTION_NONE, False, reason)
|
||||
|
||||
# ---------- Flip checks ----------
|
||||
# проверить, нужен ли flip позиции по текущему сигналу
|
||||
# Проверить, нужен ли flip позиции по текущему сигналу.
|
||||
# Здесь только факт противоположного сигнала, без оценки качества рынка.
|
||||
def _should_flip_position(self, state: AutoTradeState) -> bool:
|
||||
position = type(self)._position
|
||||
signal = str(state.last_signal or "").upper()
|
||||
@@ -414,7 +438,8 @@ class ExecutionFlipMixin(_ExecutionFlipProtocol):
|
||||
|
||||
return False
|
||||
|
||||
# определить причину блокировки flip, если flip сейчас опасен
|
||||
# Определить причину блокировки flip, если flip сейчас опасен.
|
||||
# Важно: пока торговую логику не меняем, только делаем её наблюдаемой.
|
||||
def _flip_block_reason(self, state: AutoTradeState) -> str | None:
|
||||
position = type(self)._position
|
||||
|
||||
@@ -423,6 +448,9 @@ class ExecutionFlipMixin(_ExecutionFlipProtocol):
|
||||
execution_confidence = safe_float(state.execution_confidence_score)
|
||||
repeat_count = int(safe_float(state.last_signal_repeat_count) or 0)
|
||||
unrealized_pnl = safe_float(state.unrealized_pnl_usd) or 0.0
|
||||
|
||||
# hold_seconds считаем через position metrics.
|
||||
# current_price пока берём entry_price, чтобы не менять текущую механику.
|
||||
metrics = build_position_metrics(
|
||||
position,
|
||||
current_price=position.entry_price,
|
||||
@@ -535,7 +563,7 @@ class ExecutionFlipMixin(_ExecutionFlipProtocol):
|
||||
|
||||
return None
|
||||
|
||||
# проверить, активен ли cooldown после последнего flip
|
||||
# Проверить, активен ли cooldown после последнего flip.
|
||||
def _flip_cooldown_active(self, state: AutoTradeState) -> bool:
|
||||
ts = safe_float(getattr(state, "last_flip_monotonic_at", None))
|
||||
|
||||
@@ -544,7 +572,7 @@ class ExecutionFlipMixin(_ExecutionFlipProtocol):
|
||||
|
||||
return (time.monotonic() - ts) < self._flip_cooldown_seconds
|
||||
|
||||
# определить сторону позиции по сигналу BUY / SELL
|
||||
# Определить сторону новой позиции по сигналу BUY / SELL.
|
||||
def _target_side_from_signal(self, signal: str | None) -> str | None:
|
||||
normalized_signal = str(signal or "").upper()
|
||||
|
||||
@@ -557,22 +585,33 @@ class ExecutionFlipMixin(_ExecutionFlipProtocol):
|
||||
return None
|
||||
|
||||
# ---------- Execution ----------
|
||||
# закрыть текущую позицию и открыть новую в противоположную сторону
|
||||
# Закрыть текущую позицию и открыть новую в противоположную сторону.
|
||||
def _flip_position(self, state: AutoTradeState) -> ExecutionDecision:
|
||||
position = type(self)._position
|
||||
|
||||
if position.side == POSITION_SIDE_NONE:
|
||||
self._sync_state_from_position(state)
|
||||
reason = "Нет позиции для flip."
|
||||
return self._reject_flip(state=state, reason=reason)
|
||||
return self._reject_flip(
|
||||
state=state,
|
||||
reason=reason,
|
||||
diagnostic_stage="FLIP_REJECTED_NO_POSITION",
|
||||
)
|
||||
|
||||
new_side = self._target_side_from_signal(state.last_signal)
|
||||
|
||||
if new_side is None:
|
||||
reason = "Нет направления для flip."
|
||||
return self._reject_flip(state=state, reason=reason)
|
||||
return self._reject_flip(
|
||||
state=state,
|
||||
reason=reason,
|
||||
diagnostic_stage="FLIP_REJECTED_NO_DIRECTION",
|
||||
)
|
||||
|
||||
try:
|
||||
# Для flip нужны две цены:
|
||||
# 1) exit price — закрытие старой позиции;
|
||||
# 2) entry price — открытие новой позиции.
|
||||
exit_execution = self._exit_price_for_side(
|
||||
position.symbol or state.symbol,
|
||||
position.side,
|
||||
@@ -586,7 +625,11 @@ class ExecutionFlipMixin(_ExecutionFlipProtocol):
|
||||
|
||||
except Exception as exc:
|
||||
reason = f"Ошибка получения цены для flip: {exc}"
|
||||
return self._reject_flip(state=state, reason=reason)
|
||||
return self._reject_flip(
|
||||
state=state,
|
||||
reason=reason,
|
||||
diagnostic_stage="FLIP_REJECTED_BY_PRICE",
|
||||
)
|
||||
|
||||
now = self._now_time()
|
||||
opened_monotonic_at = time.monotonic()
|
||||
@@ -606,7 +649,11 @@ class ExecutionFlipMixin(_ExecutionFlipProtocol):
|
||||
|
||||
if new_size <= 0:
|
||||
reason = "Flip отменён: невозможно рассчитать adaptive size."
|
||||
return self._reject_flip(state=state, reason=reason)
|
||||
return self._reject_flip(
|
||||
state=state,
|
||||
reason=reason,
|
||||
diagnostic_stage="FLIP_REJECTED_BY_SIZING",
|
||||
)
|
||||
|
||||
new_size = self._adjust_size_by_margin_limit(
|
||||
state=state,
|
||||
@@ -624,7 +671,11 @@ class ExecutionFlipMixin(_ExecutionFlipProtocol):
|
||||
|
||||
if new_size <= 0:
|
||||
reason = "Flip отменён: итоговый size равен 0."
|
||||
return self._reject_flip(state=state, reason=reason)
|
||||
return self._reject_flip(
|
||||
state=state,
|
||||
reason=reason,
|
||||
diagnostic_stage="FLIP_REJECTED_BY_SIZING",
|
||||
)
|
||||
|
||||
state.realized_pnl_usd += pnl
|
||||
state.cycle_realized_pnl_usd += pnl
|
||||
@@ -635,16 +686,14 @@ class ExecutionFlipMixin(_ExecutionFlipProtocol):
|
||||
if pnl > 0:
|
||||
state.cycle_winning_trades += 1
|
||||
|
||||
# прибыльный flip закрывает серию убытков
|
||||
# Прибыльный flip закрывает серию убытков и выключает loss cooldown.
|
||||
state.cycle_consecutive_losses = 0
|
||||
|
||||
state.loss_cooldown_active = False
|
||||
state.loss_cooldown_reason = None
|
||||
|
||||
elif pnl < 0:
|
||||
state.cycle_losing_trades += 1
|
||||
state.cycle_consecutive_losses += 1
|
||||
|
||||
state.last_loss_monotonic_at = time.monotonic()
|
||||
|
||||
if state.cycle_consecutive_losses >= EXECUTION_MAX_CONSECUTIVE_LOSSES:
|
||||
@@ -737,6 +786,13 @@ class ExecutionFlipMixin(_ExecutionFlipProtocol):
|
||||
entry_execution=entry_execution,
|
||||
)
|
||||
|
||||
# Отдельная диагностика успешного прохождения всего flip-пайплайна.
|
||||
self._log_flip_diagnostic(
|
||||
state=state,
|
||||
stage="FLIP_EXECUTED",
|
||||
reason=f"{old_side} -> {new_side}",
|
||||
)
|
||||
|
||||
JournalService().log_ui_info(
|
||||
event_type="position_flipped",
|
||||
message=f"Направление позиции изменено: {old_side} → {new_side}.",
|
||||
|
||||
Reference in New Issue
Block a user