From 38e8472942cbe9679782b3a51698ff4b4aa6f2f3 Mon Sep 17 00:00:00 2001 From: Sergey Date: Sun, 10 May 2026 09:48:55 +0300 Subject: [PATCH] 07.4.3.19.1 - Position-aware Flip Protection --- app/src/telegram/handlers/auto/ui.py | 1 + app/src/trading/auto/state.py | 14 +- app/src/trading/execution/engine.py | 133 +++++++++++++++++ docs/roadmap/master-roadmap.md | 13 +- docs/roadmap/stage-07-auto-trading-roadmap.md | 20 +-- ...4_3_19_1-position_aware_flip_protection.md | 137 ++++++++++++++++++ 6 files changed, 307 insertions(+), 11 deletions(-) create mode 100644 docs/stages/stage-07_4_3_19_1-position_aware_flip_protection.md diff --git a/app/src/telegram/handlers/auto/ui.py b/app/src/telegram/handlers/auto/ui.py index 96801b4..8aa1640 100644 --- a/app/src/telegram/handlers/auto/ui.py +++ b/app/src/telegram/handlers/auto/ui.py @@ -208,6 +208,7 @@ def _build_active_position_text(state) -> str: f"Доступно · $ {_format_money_compact(available)}", f"Зарезервировано · $ {_format_money_compact(reserved)}", f"P&L {_format_signed_usd_with_direction(pnl)}", + *_execution_block_lines(state), "", ( f"{side_icon} {_asset_symbol(state.symbol)} · " diff --git a/app/src/trading/auto/state.py b/app/src/trading/auto/state.py index bb04d2c..c9e4cc5 100644 --- a/app/src/trading/auto/state.py +++ b/app/src/trading/auto/state.py @@ -92,4 +92,16 @@ class AutoTradeState: allocated_balance_usd: float = 1000.0 # зафиксированный результат закрытых paper-сделок - realized_pnl_usd: float = 0.0 \ No newline at end of file + realized_pnl_usd: float = 0.0 + + # последнее execution-действие + last_execution_action: str | None = None + + # последняя execution-причина + last_execution_reason: str | None = None + + # последняя причина блокировки flip + last_flip_block_reason: str | None = None + + # время последнего успешного flip + last_flip_at: str | None = None \ No newline at end of file diff --git a/app/src/trading/execution/engine.py b/app/src/trading/execution/engine.py index 3eb2419..1d2c5f2 100644 --- a/app/src/trading/execution/engine.py +++ b/app/src/trading/execution/engine.py @@ -26,6 +26,11 @@ class _ExecutionPrice: class ExecutionEngine: _position = PositionState() _size_precision = 5 + _min_flip_confidence = 0.75 + _min_flip_repeat_count = 3 + _min_flip_hold_seconds = 60 + _loss_flip_confidence = 0.9 + _last_flip_block_key: str | None = None def get_position(self) -> PositionState: return type(self)._position @@ -46,6 +51,11 @@ class ExecutionEngine: return ExecutionDecision("NONE", False, "Сигнал ещё не готов к execution.") if self._should_flip_position(state): + flip_block_reason = self._flip_block_reason(state) + + if flip_block_reason is not None: + return self._block_flip(state, flip_block_reason) + return self._flip_position(state) if state.last_signal == "BUY": @@ -112,6 +122,10 @@ class ExecutionEngine: ) self._sync_state_from_position(state) + state.execution_block_reason = None + state.last_flip_block_reason = None + state.last_execution_action = action + state.last_execution_reason = f"Paper ENTRY {side} открыта." payload = { "execution_type": "ENTRY", @@ -210,6 +224,12 @@ class ExecutionEngine: ) self._sync_state_from_position(state) + state.execution_block_reason = None + state.last_flip_block_reason = None + state.last_execution_action = f"FLIP_{old_side}_TO_{new_side}" + state.last_execution_reason = "Paper FLIP выполнен." + state.last_flip_at = now + type(self)._last_flip_block_key = None payload = { "execution_type": "FLIP", @@ -333,6 +353,19 @@ class ExecutionEngine: type(self)._position = PositionState() self._sync_state_from_position(state) + state.execution_block_reason = None + state.last_flip_block_reason = None + state.last_execution_action = ( + f"FORCE_CLOSE_{forced_reason}" + if forced_reason is not None + else "CLOSE" + ) + state.last_execution_reason = ( + f"Paper EXIT выполнена по риску: {forced_reason}." + if forced_reason is not None + else "Paper EXIT выполнена." + ) + type(self)._last_flip_block_key = None if forced_reason is not None: return ExecutionDecision( @@ -431,6 +464,106 @@ class ExecutionEngine: return False + def _flip_block_reason(self, state: AutoTradeState) -> str | None: + position = type(self)._position + + confidence = float(state.last_signal_confidence or 0.0) + repeat_count = int(state.last_signal_repeat_count or 0) + unrealized_pnl = float(state.unrealized_pnl_usd or 0.0) + hold_seconds = self._position_hold_seconds(position) + + if confidence < self._min_flip_confidence: + return ( + "Flip blocked: signal confidence " + f"{confidence:.2f} < {self._min_flip_confidence:.2f}." + ) + + if repeat_count < self._min_flip_repeat_count: + return ( + "Flip blocked: repeat count " + f"{repeat_count} < {self._min_flip_repeat_count}." + ) + + if hold_seconds is not None and hold_seconds < self._min_flip_hold_seconds: + return ( + "Flip blocked: position hold time " + f"{hold_seconds}s < {self._min_flip_hold_seconds}s." + ) + + if unrealized_pnl < 0 and confidence < self._loss_flip_confidence: + return ( + "Flip blocked: position is negative and signal is not strong enough " + f"({confidence:.2f} < {self._loss_flip_confidence:.2f})." + ) + + return None + + def _block_flip( + self, + state: AutoTradeState, + reason: str, + ) -> ExecutionDecision: + position = type(self)._position + confidence = float(state.last_signal_confidence or 0.0) + + state.execution_block_reason = reason + state.last_flip_block_reason = reason + state.last_execution_action = "FLIP_BLOCKED" + state.last_execution_reason = reason + + block_key = ( + f"{position.side}:" + f"{state.last_signal}:" + f"{state.last_signal_repeat_count}:" + f"{confidence:.2f}:" + f"{reason}" + ) + + if block_key != type(self)._last_flip_block_key: + type(self)._last_flip_block_key = block_key + + payload = { + "execution_type": "FLIP_BLOCKED", + "symbol": state.symbol, + "position_side": position.side, + "signal": state.last_signal, + "confidence": confidence, + "repeat_count": state.last_signal_repeat_count, + "reason": reason, + "unrealized_pnl_usd": state.unrealized_pnl_usd, + "opened_at": position.opened_at, + "updated_at": position.updated_at, + } + + JournalService().log_ui_info( + event_type="paper_flip_blocked", + message=f"Paper FLIP заблокирован: {reason}", + screen="auto", + action="paper_execution", + payload=payload, + ) + + EventBus.emit("paper_flip_blocked", payload) + + return ExecutionDecision("NONE", False, reason) + + def _position_hold_seconds(self, position: PositionState) -> int | None: + if not position.opened_at: + return None + + try: + opened_at = datetime.strptime(position.opened_at, "%H:%M:%S") + now = datetime.strptime(self._now_time(), "%H:%M:%S") + + seconds = int((now - opened_at).total_seconds()) + + if seconds < 0: + seconds += 24 * 60 * 60 + + return seconds + except Exception: + return None + def _target_side_from_signal(self, signal: str | None) -> str | None: if signal == "BUY": return "LONG" diff --git a/docs/roadmap/master-roadmap.md b/docs/roadmap/master-roadmap.md index 332222e..cab5e3f 100644 --- a/docs/roadmap/master-roadmap.md +++ b/docs/roadmap/master-roadmap.md @@ -342,7 +342,7 @@ --- -#### 07.4.3.19 — Strategy Audit & Signal Quality Layer +### 07.4.3.19 — Strategy Audit & Signal Quality Layer - audit SCALP false flip behavior - add position-aware signal handling - prevent weak/medium signal flips @@ -352,6 +352,17 @@ - classify signals as ENTRY / HOLD / EXIT / FLIP - tune SCALP thresholds +#### 07.4.3.19.1 - Position-aware Flip Protection +- добавлена проверка confidence перед flip +- добавлено подтверждение flip по количеству повторов сигнала +- добавлена минимальная длительность удержания позиции перед flip +- добавлена защита от flip в убыточной позиции без сильного сигнала +- реализована блокировка flip через RuntimeEvent logging +- добавлен dedupe для повторяющихся событий flip-блокировки через _last_flip_block_key +- синхронизировано execution-state после ENTRY / FLIP / CLOSE +- исправлена очистка ghost-позиций после forced exit +- стабилизирован lifecycle paper execution во время ночного runtime-тестирования + ### 07.4.4 ⏳ Grid Strategy diff --git a/docs/roadmap/stage-07-auto-trading-roadmap.md b/docs/roadmap/stage-07-auto-trading-roadmap.md index b4a99b6..1e24fe6 100644 --- a/docs/roadmap/stage-07-auto-trading-roadmap.md +++ b/docs/roadmap/stage-07-auto-trading-roadmap.md @@ -326,16 +326,18 @@ --- -#### 07.4.3.19 — Strategy Audit & Signal Quality Layer +### 07.4.3.19 — Strategy Audit & Signal Quality Layer -- audit SCALP false flip behavior -- add position-aware signal handling -- prevent weak/medium signal flips -- add min hold time before flip -- add flip cooldown -- add spread/slippage buffer -- classify signals as ENTRY / HOLD / EXIT / FLIP -- tune SCALP thresholds +#### 07.4.3.19.1 - Position-aware Flip Protection (Защита Flip-позиций) ✅ +- добавлена проверка confidence перед flip +- добавлено подтверждение flip по количеству повторов сигнала +- добавлена минимальная длительность удержания позиции перед flip +- добавлена защита от flip в убыточной позиции без сильного сигнала +- реализована блокировка flip через RuntimeEvent logging +- добавлен dedupe для повторяющихся событий flip-блокировки через _last_flip_block_key +- синхронизировано execution-state после ENTRY / FLIP / CLOSE +- исправлена очистка ghost-позиций после forced exit +- стабилизирован lifecycle paper execution во время ночного runtime-тестирования --- diff --git a/docs/stages/stage-07_4_3_19_1-position_aware_flip_protection.md b/docs/stages/stage-07_4_3_19_1-position_aware_flip_protection.md new file mode 100644 index 0000000..fb57924 --- /dev/null +++ b/docs/stages/stage-07_4_3_19_1-position_aware_flip_protection.md @@ -0,0 +1,137 @@ +# 07.4.3.19.1 — Position-aware Flip Protection + +## Цель этапа + +Добавить первый защитный слой качества execution-сигналов: запретить быстрые и слабые flip-сделки, когда уже открыта paper-позиция. + +До этапа логика была слишком простой: + + LONG + SELL = FLIP + SHORT + BUY = FLIP + +Из-за этого SCALP мог часто переворачивать позицию на рыночном шуме. + +## Что изменено + +### 1. Добавлены execution-поля в AutoTradeState + +В `app/src/trading/auto/state.py` добавлены поля: + + last_execution_action + last_execution_reason + last_flip_block_reason + last_flip_at + +Они нужны для хранения последнего execution-действия и причины блокировки flip. + +### 2. Добавлена flip-защита в ExecutionEngine + +В `app/src/trading/execution/engine.py` добавлены параметры: + + _min_flip_confidence = 0.75 + _min_flip_repeat_count = 3 + _min_flip_hold_seconds = 60 + _loss_flip_confidence = 0.9 + +Теперь flip проходит только если: + +- противоположный сигнал подтверждён; +- confidence >= 0.75; +- repeat_count >= 3; +- позиция открыта не меньше 60 секунд; +- если позиция в минусе, confidence должен быть >= 0.9. + +### 3. Добавлен paper_flip_blocked + +Если flip заблокирован, создаётся событие: + + paper_flip_blocked + +В payload пишется: + + execution_type + symbol + position_side + signal + confidence + repeat_count + reason + unrealized_pnl_usd + opened_at + updated_at + +### 4. Добавлена защита от спама одинаковых блокировок + +В `ExecutionEngine` добавлен `_last_flip_block_key`. + +Одинаковая причина блокировки с теми же параметрами не пишется в журнал каждую итерацию. + +### 5. Сброс block-state после успешных execution-событий + +После успешных событий очищаются: + + execution_block_reason + last_flip_block_reason + _last_flip_block_key + +Это сделано для: + + paper_position_opened + paper_position_flipped + paper_position_closed + +## Проверено на ночном прогоне + +Ночной прогон подтвердил: + +- paper position opened работает; +- paper flip blocked сработал при repeat_count = 2; +- flip прошёл только после repeat_count = 3; +- flip не спамил журнал повторяющимися блокировками; +- UI и position state не разошлись; +- PnL и reserved balance отображались корректно. + +Пример подтверждённой последовательности: + + paper_flip_blocked + reason: Flip blocked: repeat count 2 < 3. + + paper_position_flipped + repeat_count: 3 + +## Результат + +Execution стал position-aware на уровне flip-защиты. + +Система больше не должна мгновенно переворачивать позицию на первом слабом противоположном сигнале. + +## Следующий этап + +Следующий логичный подэтап: + + 07.4.3.19.2 — Journal Noise Reduction & Position-aware Signal Logging + +Основная цель следующего шага: + +- сократить шум в журнале; +- убрать лишние auto_screen_refresh_* события; +- не слать одинаковые same-direction strong signal alerts при уже открытой позиции; +- разделить сигналы на reversal candidate и position reinforced. + +## Roadmap update + +#### 07.4.3.19.1 — Position-aware Flip Protection +- added execution-state tracking fields +- added flip confidence gate +- added flip repeat-count gate +- added minimum position hold-time gate +- added negative-PnL flip protection +- added paper_flip_blocked journal event +- added duplicate flip-block suppression +- reset flip block state after open / flip / close +- validated flip protection with overnight paper runtime + +## Commit + + git add app/src/trading/auto/state.py app/src/trading/execution/engine.py app/src/telegram/handlers/auto/ui.py + git commit -m "07.4.3.19.1 — Position-aware Flip Protection"