Relax early autonomous exit guard
This commit is contained in:
@@ -48,26 +48,31 @@ class _ExecutionRuntimeActionsProtocol(Protocol):
|
|||||||
def _sync_state_from_position(
|
def _sync_state_from_position(
|
||||||
self,
|
self,
|
||||||
state: AutoTradeState,
|
state: AutoTradeState,
|
||||||
) -> None: ...
|
) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
def _close_position(
|
def _close_position(
|
||||||
self,
|
self,
|
||||||
state: AutoTradeState,
|
state: AutoTradeState,
|
||||||
*,
|
*,
|
||||||
forced_reason: str | None = None,
|
forced_reason: str | None = None,
|
||||||
) -> ExecutionDecision: ...
|
) -> ExecutionDecision:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
class ExecutionRuntimeActionsMixin(
|
class ExecutionRuntimeActionsMixin(_ExecutionRuntimeActionsProtocol):
|
||||||
_ExecutionRuntimeActionsProtocol
|
# ----- Runtime autonomous actions subsystem.
|
||||||
):
|
|
||||||
# ----- Runtime autonomous actions subsystem.
|
|
||||||
# Отвечает за:
|
# Отвечает за:
|
||||||
# - runtime EXIT
|
# - autonomous EXIT;
|
||||||
# - runtime REDUCE
|
# - autonomous PROTECT;
|
||||||
# - runtime PROTECT
|
# - autonomous REDUCE;
|
||||||
# - cooldown runtime действий
|
# - cooldown runtime действий;
|
||||||
# - runtime logging
|
# - runtime logging.
|
||||||
|
#
|
||||||
|
# Важно:
|
||||||
|
# На текущем этапе PROTECT и REDUCE пока НЕ исполняют реальное действие.
|
||||||
|
# Они логируются как диагностические runtime-сигналы.
|
||||||
|
# Реальное закрытие позиции сейчас делает только AUTONOMOUS_ACTION_EXIT.
|
||||||
|
|
||||||
_runtime_action_cooldown_seconds = RUNTIME_ACTION_COOLDOWN_SECONDS
|
_runtime_action_cooldown_seconds = RUNTIME_ACTION_COOLDOWN_SECONDS
|
||||||
_last_runtime_action_key: str | None = None
|
_last_runtime_action_key: str | None = None
|
||||||
@@ -78,6 +83,10 @@ class ExecutionRuntimeActionsMixin(
|
|||||||
state: AutoTradeState,
|
state: AutoTradeState,
|
||||||
) -> ExecutionDecision:
|
) -> ExecutionDecision:
|
||||||
# Главный runtime action processor.
|
# Главный runtime action processor.
|
||||||
|
#
|
||||||
|
# Этот метод вызывается после основного engine.process().
|
||||||
|
# Если позиция открыта и autonomous_management выставил EXIT,
|
||||||
|
# здесь позиция может быть реально закрыта.
|
||||||
|
|
||||||
self._sync_state_from_position(state)
|
self._sync_state_from_position(state)
|
||||||
|
|
||||||
@@ -120,6 +129,9 @@ class ExecutionRuntimeActionsMixin(
|
|||||||
return ExecutionDecision(EXECUTION_ACTION_NONE, False, skip_reason)
|
return ExecutionDecision(EXECUTION_ACTION_NONE, False, skip_reason)
|
||||||
|
|
||||||
if action == AUTONOMOUS_ACTION_PROTECT:
|
if action == AUTONOMOUS_ACTION_PROTECT:
|
||||||
|
# Пока PROTECT только логируется.
|
||||||
|
# Следующим этапом можно сделать его реальным действием:
|
||||||
|
# например, принудительно подтягивать break-even / profit-lock.
|
||||||
return self._log_runtime_action(
|
return self._log_runtime_action(
|
||||||
state=state,
|
state=state,
|
||||||
action=AUTONOMOUS_ACTION_PROTECT,
|
action=AUTONOMOUS_ACTION_PROTECT,
|
||||||
@@ -129,6 +141,9 @@ class ExecutionRuntimeActionsMixin(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if action == AUTONOMOUS_ACTION_REDUCE:
|
if action == AUTONOMOUS_ACTION_REDUCE:
|
||||||
|
# Пока REDUCE только логируется.
|
||||||
|
# Если partial close не реализован, лучше позже перевести REDUCE
|
||||||
|
# в PROTECT или EXIT, чтобы не было иллюзии действия.
|
||||||
return self._log_runtime_action(
|
return self._log_runtime_action(
|
||||||
state=state,
|
state=state,
|
||||||
action=AUTONOMOUS_ACTION_REDUCE,
|
action=AUTONOMOUS_ACTION_REDUCE,
|
||||||
@@ -138,24 +153,13 @@ class ExecutionRuntimeActionsMixin(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if action == AUTONOMOUS_ACTION_EXIT:
|
if action == AUTONOMOUS_ACTION_EXIT:
|
||||||
if self._early_exit_guard_active(state):
|
early_guard_reason = self._early_exit_guard_block_reason(state)
|
||||||
hold_seconds = safe_float(
|
|
||||||
getattr(state, "position_hold_seconds", None)
|
|
||||||
) or 0.0
|
|
||||||
|
|
||||||
thresholds = get_position_exit_thresholds(
|
|
||||||
getattr(state, "symbol", None)
|
|
||||||
)
|
|
||||||
|
|
||||||
min_hold = thresholds["min_hold"]
|
|
||||||
|
|
||||||
|
if early_guard_reason is not None:
|
||||||
return self._log_runtime_action(
|
return self._log_runtime_action(
|
||||||
state=state,
|
state=state,
|
||||||
action=AUTONOMOUS_ACTION_EXIT_BLOCKED,
|
action=AUTONOMOUS_ACTION_EXIT_BLOCKED,
|
||||||
reason=(
|
reason=early_guard_reason,
|
||||||
"early exit guard: позиция ещё слишком новая для закрытия "
|
|
||||||
f"({hold_seconds:.0f}s < {min_hold:.0f}s)"
|
|
||||||
),
|
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
executed=False,
|
executed=False,
|
||||||
cooldown_action=None,
|
cooldown_action=None,
|
||||||
@@ -207,6 +211,9 @@ class ExecutionRuntimeActionsMixin(
|
|||||||
action: str,
|
action: str,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
# Проверка cooldown runtime action.
|
# Проверка cooldown runtime action.
|
||||||
|
# Cooldown нужен, чтобы один и тот же runtime action не спамил
|
||||||
|
# журнал и EventBus на каждом цикле.
|
||||||
|
|
||||||
ts = safe_float(
|
ts = safe_float(
|
||||||
getattr(state, "autonomous_last_action_at", None)
|
getattr(state, "autonomous_last_action_at", None)
|
||||||
)
|
)
|
||||||
@@ -225,6 +232,7 @@ class ExecutionRuntimeActionsMixin(
|
|||||||
time.monotonic() - ts
|
time.monotonic() - ts
|
||||||
) < self._runtime_action_cooldown_seconds
|
) < self._runtime_action_cooldown_seconds
|
||||||
|
|
||||||
|
# ----- PAYLOAD -----
|
||||||
def _build_runtime_action_payload(
|
def _build_runtime_action_payload(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -261,21 +269,24 @@ class ExecutionRuntimeActionsMixin(
|
|||||||
"unrealized_pnl_usd": state.unrealized_pnl_usd,
|
"unrealized_pnl_usd": state.unrealized_pnl_usd,
|
||||||
"position_pnl_percent": state.position_pnl_percent,
|
"position_pnl_percent": state.position_pnl_percent,
|
||||||
|
|
||||||
|
# ---------- Health / intelligence ----------
|
||||||
**build_position_health_payload(state),
|
**build_position_health_payload(state),
|
||||||
"position_pressure": state.position_pressure,
|
"position_pressure": state.position_pressure,
|
||||||
"position_exit_pressure": state.position_exit_pressure,
|
"position_exit_pressure": state.position_exit_pressure,
|
||||||
|
|
||||||
**build_full_position_intelligence_payload(state),
|
**build_full_position_intelligence_payload(state),
|
||||||
|
|
||||||
|
# ---------- Autonomous ----------
|
||||||
**build_autonomous_payload(state),
|
**build_autonomous_payload(state),
|
||||||
"autonomous_last_action": state.autonomous_last_action,
|
"autonomous_last_action": state.autonomous_last_action,
|
||||||
"autonomous_last_action_reason": state.autonomous_last_action_reason,
|
"autonomous_last_action_reason": state.autonomous_last_action_reason,
|
||||||
|
|
||||||
|
# ---------- Protection / market / execution ----------
|
||||||
**build_runtime_protection_payload(state),
|
**build_runtime_protection_payload(state),
|
||||||
**build_market_context_payload(state),
|
**build_market_context_payload(state),
|
||||||
**build_execution_quality_payload(state),
|
**build_execution_quality_payload(state),
|
||||||
}
|
}
|
||||||
|
|
||||||
# ----- LOGGING -----
|
# ----- LOGGING -----
|
||||||
def _log_runtime_action(
|
def _log_runtime_action(
|
||||||
self,
|
self,
|
||||||
@@ -288,6 +299,11 @@ class ExecutionRuntimeActionsMixin(
|
|||||||
cooldown_action: str | None = None,
|
cooldown_action: str | None = None,
|
||||||
) -> ExecutionDecision:
|
) -> ExecutionDecision:
|
||||||
# Runtime action logging + deduplication.
|
# Runtime action logging + deduplication.
|
||||||
|
# Даже если действие не исполняется, payload помогает понять:
|
||||||
|
# - почему runtime action появился;
|
||||||
|
# - почему он был заблокирован;
|
||||||
|
# - какие были position health / semantics / market context.
|
||||||
|
|
||||||
position = type(self)._position
|
position = type(self)._position
|
||||||
trade_id = position.trade_id or state.current_trade_id
|
trade_id = position.trade_id or state.current_trade_id
|
||||||
|
|
||||||
@@ -341,12 +357,23 @@ class ExecutionRuntimeActionsMixin(
|
|||||||
reason,
|
reason,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _early_exit_guard_active(self, state: AutoTradeState) -> bool:
|
# ----- EARLY EXIT GUARD -----
|
||||||
|
def _early_exit_guard_block_reason(self, state: AutoTradeState) -> str | None:
|
||||||
|
# Early exit guard защищает от слишком раннего закрытия позиции
|
||||||
|
# на обычном шуме/спреде/первой волне после входа.
|
||||||
|
#
|
||||||
|
# Но раньше он блокировал выход почти всегда до min_hold,
|
||||||
|
# пока убыток не доходил до hard_loss.
|
||||||
|
#
|
||||||
|
# Новая логика:
|
||||||
|
# - обычный ранний шум всё ещё блокируется;
|
||||||
|
# - реальное ухудшение позиции guard больше НЕ блокирует.
|
||||||
|
|
||||||
hold_seconds = safe_float(getattr(state, "position_hold_seconds", None))
|
hold_seconds = safe_float(getattr(state, "position_hold_seconds", None))
|
||||||
pnl_percent = safe_float(getattr(state, "position_pnl_percent", None))
|
pnl_percent = safe_float(getattr(state, "position_pnl_percent", None))
|
||||||
|
|
||||||
if hold_seconds is None or pnl_percent is None:
|
if hold_seconds is None or pnl_percent is None:
|
||||||
return False
|
return None
|
||||||
|
|
||||||
thresholds = get_position_exit_thresholds(
|
thresholds = get_position_exit_thresholds(
|
||||||
getattr(state, "symbol", None)
|
getattr(state, "symbol", None)
|
||||||
@@ -356,10 +383,77 @@ class ExecutionRuntimeActionsMixin(
|
|||||||
hard_loss = thresholds["hard_loss"]
|
hard_loss = thresholds["hard_loss"]
|
||||||
|
|
||||||
if hold_seconds >= min_hold:
|
if hold_seconds >= min_hold:
|
||||||
return False
|
return None
|
||||||
|
|
||||||
# Если просадка уже критическая — guard не мешает защите.
|
# Если просадка уже критическая — guard не мешает защите.
|
||||||
if pnl_percent <= hard_loss:
|
if pnl_percent <= hard_loss:
|
||||||
return False
|
return None
|
||||||
|
|
||||||
return True
|
bypass_reason = self._early_exit_guard_bypass_reason(state)
|
||||||
|
|
||||||
|
if bypass_reason is not None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return (
|
||||||
|
"early exit guard: позиция ещё слишком новая для закрытия "
|
||||||
|
f"({hold_seconds:.0f}s < {min_hold:.0f}s)"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _early_exit_guard_bypass_reason(self, state: AutoTradeState) -> str | None:
|
||||||
|
# Причины, при которых ранний выход нужно разрешить.
|
||||||
|
# Это не делает выход автоматическим само по себе:
|
||||||
|
# action всё равно должен быть AUTONOMOUS_ACTION_EXIT,
|
||||||
|
# а confidence должен пройти RUNTIME_EXIT_CONFIDENCE_THRESHOLD.
|
||||||
|
|
||||||
|
adverse_momentum = bool(
|
||||||
|
getattr(state, "position_adverse_momentum", False)
|
||||||
|
)
|
||||||
|
|
||||||
|
trend_alignment = str(
|
||||||
|
getattr(state, "position_trend_alignment", "") or ""
|
||||||
|
).upper()
|
||||||
|
|
||||||
|
risk_level = str(
|
||||||
|
getattr(state, "position_risk_level", "") or ""
|
||||||
|
).upper()
|
||||||
|
|
||||||
|
conviction_state = str(
|
||||||
|
getattr(state, "position_conviction_state", "") or ""
|
||||||
|
).upper()
|
||||||
|
|
||||||
|
stall_state = str(
|
||||||
|
getattr(state, "position_stall_state", "") or ""
|
||||||
|
).upper()
|
||||||
|
|
||||||
|
exit_urgency = str(
|
||||||
|
getattr(state, "position_exit_urgency", "") or ""
|
||||||
|
).upper()
|
||||||
|
|
||||||
|
decay_state = str(
|
||||||
|
getattr(state, "position_decay_state", "") or ""
|
||||||
|
).upper()
|
||||||
|
|
||||||
|
if risk_level in {"HIGH", "ELEVATED"}:
|
||||||
|
return f"early exit allowed: position risk is {risk_level}"
|
||||||
|
|
||||||
|
if adverse_momentum and trend_alignment == "AGAINST":
|
||||||
|
return "early exit allowed: trend and momentum are against position"
|
||||||
|
|
||||||
|
if conviction_state == "BROKEN":
|
||||||
|
return "early exit allowed: position conviction is broken"
|
||||||
|
|
||||||
|
if stall_state == "ADVERSE_STALLED":
|
||||||
|
return "early exit allowed: position is adverse stalled"
|
||||||
|
|
||||||
|
if exit_urgency in {"IMMEDIATE", "HIGH"}:
|
||||||
|
return f"early exit allowed: exit urgency is {exit_urgency}"
|
||||||
|
|
||||||
|
if decay_state in {"ACCELERATING_LOSS", "CONTEXT_DECAY"}:
|
||||||
|
return f"early exit allowed: position decay is {decay_state}"
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Старый публичный helper оставляем для совместимости,
|
||||||
|
# если где-то ещё в коде он вызывается напрямую.
|
||||||
|
def _early_exit_guard_active(self, state: AutoTradeState) -> bool:
|
||||||
|
return self._early_exit_guard_block_reason(state) is not None
|
||||||
Reference in New Issue
Block a user