diff --git a/app/src/trading/auto/runner.py b/app/src/trading/auto/runner.py
index dfa4480..0a0b3c3 100644
--- a/app/src/trading/auto/runner.py
+++ b/app/src/trading/auto/runner.py
@@ -170,7 +170,11 @@ class AutoTradeRunner:
await cls._send_strong_signal_alert(state=state, payload=payload)
return
- if event_type in {"paper_position_opened", "paper_position_closed"}:
+ if event_type in {
+ "paper_position_opened",
+ "paper_position_closed",
+ "paper_position_flipped",
+ }:
await cls._send_execution_alert(
state=state,
event_type=event_type,
@@ -330,12 +334,16 @@ class AutoTradeRunner:
f"{event_type}:"
f"{payload.get('symbol')}:"
f"{payload.get('side')}:"
+ f"{payload.get('old_side')}:"
+ f"{payload.get('new_side')}:"
f"{payload.get('entry_price')}:"
f"{payload.get('exit_price')}:"
+ f"{payload.get('new_entry_price')}:"
f"{payload.get('size')}:"
+ f"{payload.get('old_size')}:"
+ f"{payload.get('new_size')}:"
f"{payload.get('pnl')}"
)
-
@classmethod
def _build_execution_alert_text(
cls,
@@ -378,6 +386,32 @@ class AutoTradeRunner:
f"PnL: {pnl}"
)
+ if event_type == "paper_position_flipped":
+ old_side = str(payload.get("old_side") or "—")
+ new_side = str(payload.get("new_side") or side or "—")
+
+ entry_price = cls._format_price(payload.get("entry_price"))
+ exit_price = cls._format_price(payload.get("exit_price"))
+ new_entry_price = cls._format_price(payload.get("new_entry_price"))
+ old_size = cls._format_size(payload.get("old_size"))
+ new_size = cls._format_size(payload.get("new_size"))
+ pnl = cls._format_pnl(payload.get("pnl"))
+
+ old_icon = "🟢" if old_side == "LONG" else "🔴"
+ new_icon = "🟢" if new_side == "LONG" else "🔴"
+
+ return (
+ f"🔁 Paper position flipped {old_icon} {old_side} → "
+ f"{new_icon} {new_side}\n\n"
+ f"{symbol_text} · {leverage_text}\n\n"
+ f"Old entry: $ {entry_price}\n"
+ f"Exit: $ {exit_price}\n"
+ f"Old size: {old_size}\n\n"
+ f"New entry: $ {new_entry_price}\n"
+ f"New size: {new_size}\n\n"
+ f"PnL: {pnl}"
+ )
+
return "📄 Paper execution event"
@classmethod
diff --git a/app/src/trading/execution/engine.py b/app/src/trading/execution/engine.py
index 0099a3d..a5e5579 100644
--- a/app/src/trading/execution/engine.py
+++ b/app/src/trading/execution/engine.py
@@ -29,8 +29,8 @@ class ExecutionEngine:
if state.decision_status != "READY" or not state.is_signal_ready:
return ExecutionDecision("NONE", False, "Сигнал ещё не готов к execution.")
- if self._should_close_position(state):
- return self._close_position(state)
+ if self._should_flip_position(state):
+ return self._flip_position(state)
if state.last_signal == "BUY":
return self._open_position_if_empty(state=state, side="LONG", action="OPEN_LONG")
@@ -102,6 +102,87 @@ class ExecutionEngine:
return ExecutionDecision(action, True, f"Paper ENTRY {side} открыта.")
+ def _flip_position(self, state: AutoTradeState) -> ExecutionDecision:
+ position = type(self)._position
+
+ if position.side == "NONE":
+ self._sync_state_from_position(state)
+ return ExecutionDecision("NONE", False, "Нет позиции для flip.")
+
+ new_side = self._target_side_from_signal(state.last_signal)
+ if new_side is None:
+ return ExecutionDecision("NONE", False, "Нет направления для flip.")
+
+ try:
+ ticker = ExchangeService().get_price(state.symbol)
+ flip_price = ticker.price
+ except Exception as exc:
+ return ExecutionDecision("NONE", False, f"Ошибка получения цены для flip: {exc}")
+
+ now = self._now_time()
+ pnl = self._calculate_pnl(flip_price)
+ new_size = self._calculate_position_size(state)
+
+ old_side = position.side
+ old_entry_price = position.entry_price
+ old_size = position.size
+ old_leverage = position.leverage
+ old_opened_at = position.opened_at
+
+ type(self)._position = PositionState(
+ side=new_side,
+ symbol=state.symbol,
+ entry_price=flip_price,
+ size=new_size,
+ leverage=state.leverage,
+ unrealized_pnl_usd=0.0,
+ opened_at=now,
+ updated_at=now,
+ )
+
+ self._sync_state_from_position(state)
+
+ payload = {
+ "execution_type": "FLIP",
+ "action": f"FLIP_{old_side}_TO_{new_side}",
+ "symbol": state.symbol,
+ "old_side": old_side,
+ "new_side": new_side,
+ "side": new_side,
+ "entry_price": old_entry_price,
+ "exit_price": flip_price,
+ "new_entry_price": flip_price,
+ "old_size": old_size,
+ "new_size": new_size,
+ "size": new_size,
+ "old_leverage": old_leverage,
+ "leverage": state.leverage,
+ "pnl": pnl,
+ "signal": state.last_signal,
+ "confidence": state.last_signal_confidence,
+ "repeat_count": state.last_signal_repeat_count,
+ "reason": state.last_signal_reason,
+ "opened_at": old_opened_at,
+ "closed_at": now,
+ "new_opened_at": now,
+ }
+
+ JournalService().log_ui_info(
+ event_type="paper_position_flipped",
+ message=f"Paper FLIP выполнен: {old_side} → {new_side} {state.symbol}",
+ screen="auto",
+ action="paper_execution",
+ payload=payload,
+ )
+
+ EventBus.emit("paper_position_flipped", payload)
+
+ return ExecutionDecision(
+ f"FLIP_{old_side}_TO_{new_side}",
+ True,
+ f"Paper FLIP выполнен: {old_side} → {new_side}.",
+ )
+
def _close_position(self, state: AutoTradeState) -> ExecutionDecision:
position = type(self)._position
@@ -151,7 +232,7 @@ class ExecutionEngine:
return ExecutionDecision("CLOSE", True, "Paper EXIT выполнена.")
- def _should_close_position(self, state: AutoTradeState) -> bool:
+ def _should_flip_position(self, state: AutoTradeState) -> bool:
position = type(self)._position
if position.side == "NONE":
@@ -165,6 +246,15 @@ class ExecutionEngine:
return False
+ def _target_side_from_signal(self, signal: str | None) -> str | None:
+ if signal == "BUY":
+ return "LONG"
+
+ if signal == "SELL":
+ return "SHORT"
+
+ return None
+
def _update_unrealized_pnl(self, state: AutoTradeState) -> None:
position = type(self)._position
diff --git a/docs/roadmap/master-roadmap.md b/docs/roadmap/master-roadmap.md
index ce28020..294b17c 100644
--- a/docs/roadmap/master-roadmap.md
+++ b/docs/roadmap/master-roadmap.md
@@ -186,6 +186,12 @@
- readable USD formatting
- signal alerts separated from execution alerts
+#### 07.4.3.9 — Position flip flow ✅
+- instant LONG ↔ SHORT reversal (FLIP)
+- new EventBus event: paper_position_flipped
+- unified execution alert for flip
+- improved execution realism (no idle gap)
+
### 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 9fc061c..9fe0cb1 100644
--- a/docs/roadmap/stage-07-auto-trading-roadmap.md
+++ b/docs/roadmap/stage-07-auto-trading-roadmap.md
@@ -169,6 +169,13 @@
- readable USD formatting
- signal alerts separated from execution alerts
+#### 07.4.3.9 — Position flip flow ✅
+
+- instant LONG ↔ SHORT reversal (FLIP)
+- new EventBus event: paper_position_flipped
+- unified execution alert for flip
+- improved execution realism (no idle gap)
+
---
### 07.4.4
diff --git a/docs/stages/07_4_3_9_position_flip_flow.md b/docs/stages/07_4_3_9_position_flip_flow.md
new file mode 100644
index 0000000..fdddc3b
--- /dev/null
+++ b/docs/stages/07_4_3_9_position_flip_flow.md
@@ -0,0 +1,90 @@
+# Stage 07.4.3.9 --- Position Flip Flow
+
+## Overview
+
+This stage introduces **position flip logic** into the execution engine.
+
+Instead of: - CLOSE → WAIT → OPEN
+
+We now support: - **FLIP (instant reversal)**
+
+------------------------------------------------------------------------
+
+## Behavior
+
+### Before
+
+- LONG + SELL → CLOSE
+- Next cycle → OPEN SHORT
+
+### Now
+
+- LONG + SELL → **FLIP → SHORT (same cycle)**
+- SHORT + BUY → **FLIP → LONG (same cycle)**
+
+------------------------------------------------------------------------
+
+## Execution Types
+
+ Type Description
+ ------- -------------------------------------------------
+ ENTRY Opening new position
+ EXIT Closing position
+ FLIP Closing + opening opposite position in one step
+
+------------------------------------------------------------------------
+
+## EventBus Events
+
+### New event
+
+- `paper_position_flipped`
+
+Payload includes: - old_side - new_side - entry_price - exit_price -
+new_entry_price - pnl - sizes and leverage
+
+------------------------------------------------------------------------
+
+## Telegram Alerts
+
+### Flip alert
+
+Example:
+
+🔁 Paper position flipped 🟢 LONG → 🔴 SHORT
+
+Includes: - old entry - exit - new entry - pnl
+
+------------------------------------------------------------------------
+
+## Benefits
+
+- Faster reaction to signals
+- No idle state between positions
+- Cleaner execution logic
+- More realistic trading simulation
+
+------------------------------------------------------------------------
+
+## Testing
+
+ /debug_signal BUY 0.95 3
+ /debug_signal SELL 0.95 3
+ /debug_signal BUY 0.95 3
+
+Expected: - ENTRY - FLIP - FLIP
+
+------------------------------------------------------------------------
+
+## Notes
+
+- Flip happens only when:
+ - position exists
+ - opposite signal is READY
+- Execution remains **paper-only**
+
+------------------------------------------------------------------------
+
+## Next Stage
+
+07.4.3.10 --- Risk & Position Control