From 8dd62987128b6c3fad86c6eb8033a13e84d3bb5a Mon Sep 17 00:00:00 2001 From: Sergey Date: Tue, 5 May 2026 08:39:05 +0300 Subject: [PATCH] =?UTF-8?q?Stage=2007.4.3.9=20=E2=80=94=20Position=20flip?= =?UTF-8?q?=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/trading/auto/runner.py | 38 +++++++- app/src/trading/execution/engine.py | 96 ++++++++++++++++++- docs/roadmap/master-roadmap.md | 6 ++ docs/roadmap/stage-07-auto-trading-roadmap.md | 7 ++ docs/stages/07_4_3_9_position_flip_flow.md | 90 +++++++++++++++++ 5 files changed, 232 insertions(+), 5 deletions(-) create mode 100644 docs/stages/07_4_3_9_position_flip_flow.md 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