Stage 07.4.3.9 — Position flip flow

This commit is contained in:
2026-05-05 08:39:05 +03:00
parent 1253cda003
commit 8dd6298712
5 changed files with 232 additions and 5 deletions

View File

@@ -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"<b>🔁 Paper position flipped {old_icon} {old_side}"
f"{new_icon} {new_side}</b>\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 "<b>📄 Paper execution event</b>"
@classmethod

View File

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