07.4.3.19.1 - Position-aware Flip Protection

This commit is contained in:
2026-05-10 09:48:55 +03:00
parent c3cf446143
commit 38e8472942
6 changed files with 307 additions and 11 deletions

View File

@@ -208,6 +208,7 @@ def _build_active_position_text(state) -> str:
f"<b>Доступно</b> · $ {_format_money_compact(available)}", f"<b>Доступно</b> · $ {_format_money_compact(available)}",
f"<b>Зарезервировано</b> · $ {_format_money_compact(reserved)}", f"<b>Зарезервировано</b> · $ {_format_money_compact(reserved)}",
f"<b>P&L</b> {_format_signed_usd_with_direction(pnl)}", f"<b>P&L</b> {_format_signed_usd_with_direction(pnl)}",
*_execution_block_lines(state),
"", "",
( (
f"{side_icon} <b>{_asset_symbol(state.symbol)}</b> · " f"{side_icon} <b>{_asset_symbol(state.symbol)}</b> · "

View File

@@ -92,4 +92,16 @@ class AutoTradeState:
allocated_balance_usd: float = 1000.0 allocated_balance_usd: float = 1000.0
# зафиксированный результат закрытых paper-сделок # зафиксированный результат закрытых paper-сделок
realized_pnl_usd: float = 0.0 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

View File

@@ -26,6 +26,11 @@ class _ExecutionPrice:
class ExecutionEngine: class ExecutionEngine:
_position = PositionState() _position = PositionState()
_size_precision = 5 _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: def get_position(self) -> PositionState:
return type(self)._position return type(self)._position
@@ -46,6 +51,11 @@ class ExecutionEngine:
return ExecutionDecision("NONE", False, "Сигнал ещё не готов к execution.") return ExecutionDecision("NONE", False, "Сигнал ещё не готов к execution.")
if self._should_flip_position(state): 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) return self._flip_position(state)
if state.last_signal == "BUY": if state.last_signal == "BUY":
@@ -112,6 +122,10 @@ class ExecutionEngine:
) )
self._sync_state_from_position(state) 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 = { payload = {
"execution_type": "ENTRY", "execution_type": "ENTRY",
@@ -210,6 +224,12 @@ class ExecutionEngine:
) )
self._sync_state_from_position(state) 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 = { payload = {
"execution_type": "FLIP", "execution_type": "FLIP",
@@ -333,6 +353,19 @@ class ExecutionEngine:
type(self)._position = PositionState() type(self)._position = PositionState()
self._sync_state_from_position(state) 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: if forced_reason is not None:
return ExecutionDecision( return ExecutionDecision(
@@ -431,6 +464,106 @@ class ExecutionEngine:
return False 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: def _target_side_from_signal(self, signal: str | None) -> str | None:
if signal == "BUY": if signal == "BUY":
return "LONG" return "LONG"

View File

@@ -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 - audit SCALP false flip behavior
- add position-aware signal handling - add position-aware signal handling
- prevent weak/medium signal flips - prevent weak/medium signal flips
@@ -352,6 +352,17 @@
- classify signals as ENTRY / HOLD / EXIT / FLIP - classify signals as ENTRY / HOLD / EXIT / FLIP
- tune SCALP thresholds - 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 ### 07.4.4
⏳ Grid Strategy ⏳ Grid Strategy

View File

@@ -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 #### 07.4.3.19.1 - Position-aware Flip Protection (Защита Flip-позиций) ✅
- add position-aware signal handling - добавлена проверка confidence перед flip
- prevent weak/medium signal flips - добавлено подтверждение flip по количеству повторов сигнала
- add min hold time before flip - добавлена минимальная длительность удержания позиции перед flip
- add flip cooldown - добавлена защита от flip в убыточной позиции без сильного сигнала
- add spread/slippage buffer - реализована блокировка flip через RuntimeEvent logging
- classify signals as ENTRY / HOLD / EXIT / FLIP - добавлен dedupe для повторяющихся событий flip-блокировки через _last_flip_block_key
- tune SCALP thresholds - синхронизировано execution-state после ENTRY / FLIP / CLOSE
- исправлена очистка ghost-позиций после forced exit
- стабилизирован lifecycle paper execution во время ночного runtime-тестирования
--- ---

View File

@@ -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"