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(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> · "
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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-тестирования
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
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