07.4.3.19.1 - Position-aware Flip Protection
This commit is contained in:
@@ -208,6 +208,7 @@ def _build_active_position_text(state) -> str:
|
||||
f"<b>Доступно</b> · $ {_format_money_compact(available)}",
|
||||
f"<b>Зарезервировано</b> · $ {_format_money_compact(reserved)}",
|
||||
f"<b>P&L</b> {_format_signed_usd_with_direction(pnl)}",
|
||||
*_execution_block_lines(state),
|
||||
"",
|
||||
(
|
||||
f"{side_icon} <b>{_asset_symbol(state.symbol)}</b> · "
|
||||
|
||||
@@ -92,4 +92,16 @@ class AutoTradeState:
|
||||
allocated_balance_usd: float = 1000.0
|
||||
|
||||
# зафиксированный результат закрытых 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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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-тестирования
|
||||
|
||||
---
|
||||
|
||||
|
||||
137
docs/stages/stage-07_4_3_19_1-position_aware_flip_protection.md
Normal file
137
docs/stages/stage-07_4_3_19_1-position_aware_flip_protection.md
Normal 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"
|
||||
Reference in New Issue
Block a user