From 26deb861bcfdaa1a5285f3a6e64564ee60b810f6 Mon Sep 17 00:00:00 2001 From: Sergey Date: Fri, 3 Jul 2026 13:51:40 +0300 Subject: [PATCH] Execute autonomous protect action --- .../trading/execution/position_protection.py | 107 ++++++++++++++++++ app/src/trading/execution/runtime_actions.py | 29 ++++- 2 files changed, 131 insertions(+), 5 deletions(-) diff --git a/app/src/trading/execution/position_protection.py b/app/src/trading/execution/position_protection.py index a5b5b0b..9fd0cd8 100644 --- a/app/src/trading/execution/position_protection.py +++ b/app/src/trading/execution/position_protection.py @@ -196,6 +196,113 @@ class ExecutionPositionProtectionMixin(_ExecutionPositionProtectionProtocol): state.position_protection_reason = reason state.runtime_protection_updated_at = time.monotonic() + # принудительно усилить защиту позиции по запросу autonomous PROTECT + def _force_runtime_protect( + self, + state: AutoTradeState, + *, + reason: str, + ) -> bool: + position = type(self)._position + + if position.side == "NONE": + return False + + try: + current_execution = self._exit_price_for_side( + position.symbol or state.symbol, + position.side, + ) + current_price = safe_float(current_execution.price) + + if current_price is None or current_price <= 0: + return False + + except Exception: + return False + + metrics = build_position_metrics( + position, + current_price=current_price, + ) + + entry_price = safe_float(position.entry_price) + price_move_percent = safe_float(metrics.price_move_percent) + + if entry_price is None or entry_price <= 0: + return False + + if price_move_percent is None: + return False + + # PROTECT не должен ставить защиту, если позиция уже в минусе. + # В минусовой позиции protection-цена может немедленно закрыть сделку + # или создать ложное ощущение защиты. + if price_move_percent <= 0: + return False + + changed = False + + # 1. Если позиция уже в прибыли, но break-even ещё не включён — + # включаем его сразу, не дожидаясь обычного порога. + if not state.break_even_armed: + buffer_percent = 0.03 + + if position.side == "LONG": + state.break_even_price = entry_price * (1 + buffer_percent / 100) + elif position.side == "SHORT": + state.break_even_price = entry_price * (1 - buffer_percent / 100) + else: + return False + + state.break_even_armed = True + changed = True + + # 2. Если прибыль уже покрывает хотя бы небольшой запас, + # подтягиваем profit-lock ближе, чем обычные thresholds. + # Это помогает не отдавать маленькую прибыль обратно комиссии/шуму. + if price_move_percent >= 0.25: + lock_distance_percent = 0.18 + + if position.side == "LONG": + min_lock_price = entry_price * 1.0002 + dynamic_lock_price = current_price * (1 - lock_distance_percent / 100) + lock_price = max(min_lock_price, dynamic_lock_price) + + previous_price = safe_float(state.profit_lock_price) + if previous_price is None or lock_price > previous_price: + state.profit_lock_active = True + state.profit_lock_price = round(lock_price, 8) + changed = True + + elif position.side == "SHORT": + min_lock_price = entry_price * 0.9998 + dynamic_lock_price = current_price * (1 + lock_distance_percent / 100) + lock_price = min(min_lock_price, dynamic_lock_price) + + previous_price = safe_float(state.profit_lock_price) + if previous_price is None or lock_price < previous_price: + state.profit_lock_active = True + state.profit_lock_price = round(lock_price, 8) + changed = True + + if not changed: + return False + + state.runtime_protection_action = "FORCED_PROTECT" + state.runtime_protection_reason = reason + state.runtime_protection_updated_at = time.monotonic() + + self._log_runtime_protection_event( + state=state, + action="FORCED_PROTECT", + reason=reason, + current_price=current_price, + metrics=metrics, + ) + + return True + def _update_break_even_protection( self, *, diff --git a/app/src/trading/execution/runtime_actions.py b/app/src/trading/execution/runtime_actions.py index d6889b8..23ac753 100644 --- a/app/src/trading/execution/runtime_actions.py +++ b/app/src/trading/execution/runtime_actions.py @@ -59,6 +59,14 @@ class _ExecutionRuntimeActionsProtocol(Protocol): ) -> ExecutionDecision: ... + def _force_runtime_protect( + self, + state: AutoTradeState, + *, + reason: str, + ) -> bool: + ... + class ExecutionRuntimeActionsMixin(_ExecutionRuntimeActionsProtocol): # ----- Runtime autonomous actions subsystem. @@ -129,15 +137,26 @@ class ExecutionRuntimeActionsMixin(_ExecutionRuntimeActionsProtocol): return ExecutionDecision(EXECUTION_ACTION_NONE, False, skip_reason) if action == AUTONOMOUS_ACTION_PROTECT: - # Пока PROTECT только логируется. - # Следующим этапом можно сделать его реальным действием: - # например, принудительно подтягивать break-even / profit-lock. + protect_reason = reason or "позиция требует защиты" + + # Теперь PROTECT — это не только лог. + # Если позиция уже в плюсе, protection layer принудительно включает + # break-even и при достаточной прибыли подтягивает profit-lock. + protected = self._force_runtime_protect( + state, + reason=protect_reason, + ) + return self._log_runtime_action( state=state, action=AUTONOMOUS_ACTION_PROTECT, - reason=reason or "позиция требует защиты", + reason=( + protect_reason + if protected + else f"{protect_reason}; protection не применён" + ), confidence=confidence, - executed=False, + executed=protected, ) if action == AUTONOMOUS_ACTION_REDUCE: