Stage 07.4.3.8 — Telegram execution alerts
This commit is contained in:
@@ -52,6 +52,34 @@ def format_usd_amount(value: float) -> str:
|
|||||||
return f"{value:,.2f}".replace(",", " ")
|
return f"{value:,.2f}".replace(",", " ")
|
||||||
|
|
||||||
|
|
||||||
|
def format_usd_price(value: float | int | str | None) -> str:
|
||||||
|
if value is None:
|
||||||
|
return "—"
|
||||||
|
|
||||||
|
try:
|
||||||
|
return f"{float(value):,.2f}".replace(",", " ")
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return "—"
|
||||||
|
|
||||||
|
|
||||||
|
def format_usd_pnl(value: float | int | str | None) -> str:
|
||||||
|
if value is None:
|
||||||
|
return "—"
|
||||||
|
|
||||||
|
try:
|
||||||
|
pnl = float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return "—"
|
||||||
|
|
||||||
|
if pnl > 0:
|
||||||
|
return f"🟢 +{format_usd_price(pnl)} USD"
|
||||||
|
|
||||||
|
if pnl < 0:
|
||||||
|
return f"🔴 -{format_usd_price(abs(pnl))} USD"
|
||||||
|
|
||||||
|
return f"⚪ {format_usd_price(0)} USD"
|
||||||
|
|
||||||
|
|
||||||
def render_currency_line(
|
def render_currency_line(
|
||||||
*,
|
*,
|
||||||
currency: str,
|
currency: str,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from src.core.event_bus import EventBus
|
|||||||
from src.integrations.exchange.market_data_runner import MarketDataRunner
|
from src.integrations.exchange.market_data_runner import MarketDataRunner
|
||||||
from src.trading.auto.service import AutoTradeService
|
from src.trading.auto.service import AutoTradeService
|
||||||
from src.trading.journal.service import JournalService
|
from src.trading.journal.service import JournalService
|
||||||
|
from src.telegram.ui.currency_ui import format_usd_pnl, format_usd_price
|
||||||
|
|
||||||
|
|
||||||
class AutoTradeRunner:
|
class AutoTradeRunner:
|
||||||
@@ -497,10 +498,11 @@ class AutoTradeRunner:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _format_price(cls, value: object) -> str:
|
def _format_price(cls, value: object) -> str:
|
||||||
try:
|
return format_usd_price(value)
|
||||||
return f"{float(value):.2f}"
|
|
||||||
except (TypeError, ValueError):
|
@classmethod
|
||||||
return "—"
|
def _format_pnl(cls, value: object) -> str:
|
||||||
|
return format_usd_pnl(value)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _format_size(cls, value: object) -> str:
|
def _format_size(cls, value: object) -> str:
|
||||||
@@ -509,16 +511,6 @@ class AutoTradeRunner:
|
|||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return "—"
|
return "—"
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _format_pnl(cls, value: object) -> str:
|
|
||||||
try:
|
|
||||||
pnl = float(value)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return "—"
|
|
||||||
|
|
||||||
prefix = "+" if pnl > 0 else ""
|
|
||||||
return f"{prefix}{pnl:.4f} USD"
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _refresh_screen(cls, *, force: bool = False) -> None:
|
async def _refresh_screen(cls, *, force: bool = False) -> None:
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
|
|||||||
@@ -15,54 +15,31 @@ from src.trading.position.state import PositionState
|
|||||||
class ExecutionEngine:
|
class ExecutionEngine:
|
||||||
_position = PositionState()
|
_position = PositionState()
|
||||||
|
|
||||||
# получить текущую paper-позицию
|
|
||||||
def get_position(self) -> PositionState:
|
def get_position(self) -> PositionState:
|
||||||
return type(self)._position
|
return type(self)._position
|
||||||
|
|
||||||
# обработать состояние автоторговли и принять paper-execution решение
|
|
||||||
def process(self, state: AutoTradeState) -> ExecutionDecision:
|
def process(self, state: AutoTradeState) -> ExecutionDecision:
|
||||||
self._sync_state_from_position(state)
|
self._sync_state_from_position(state)
|
||||||
|
|
||||||
if state.status != "RUNNING":
|
if state.status != "RUNNING":
|
||||||
return ExecutionDecision(
|
return ExecutionDecision("NONE", False, "Execution доступен только в режиме RUNNING.")
|
||||||
action="NONE",
|
|
||||||
can_execute=False,
|
|
||||||
reason="Execution доступен только в режиме RUNNING.",
|
|
||||||
)
|
|
||||||
|
|
||||||
self._update_unrealized_pnl(state)
|
self._update_unrealized_pnl(state)
|
||||||
|
|
||||||
if state.decision_status != "READY" or not state.is_signal_ready:
|
if state.decision_status != "READY" or not state.is_signal_ready:
|
||||||
return ExecutionDecision(
|
return ExecutionDecision("NONE", False, "Сигнал ещё не готов к execution.")
|
||||||
action="NONE",
|
|
||||||
can_execute=False,
|
|
||||||
reason="Сигнал ещё не готов к execution.",
|
|
||||||
)
|
|
||||||
|
|
||||||
if self._should_close_position(state):
|
if self._should_close_position(state):
|
||||||
return self._close_position(state)
|
return self._close_position(state)
|
||||||
|
|
||||||
if state.last_signal == "BUY":
|
if state.last_signal == "BUY":
|
||||||
return self._open_position_if_empty(
|
return self._open_position_if_empty(state=state, side="LONG", action="OPEN_LONG")
|
||||||
state=state,
|
|
||||||
side="LONG",
|
|
||||||
action="OPEN_LONG",
|
|
||||||
)
|
|
||||||
|
|
||||||
if state.last_signal == "SELL":
|
if state.last_signal == "SELL":
|
||||||
return self._open_position_if_empty(
|
return self._open_position_if_empty(state=state, side="SHORT", action="OPEN_SHORT")
|
||||||
state=state,
|
|
||||||
side="SHORT",
|
|
||||||
action="OPEN_SHORT",
|
|
||||||
)
|
|
||||||
|
|
||||||
return ExecutionDecision(
|
return ExecutionDecision("NONE", False, "Нет торгового действия.")
|
||||||
action="NONE",
|
|
||||||
can_execute=False,
|
|
||||||
reason="Нет торгового действия.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# открыть paper-позицию, если позиции ещё нет
|
|
||||||
def _open_position_if_empty(
|
def _open_position_if_empty(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -74,21 +51,13 @@ class ExecutionEngine:
|
|||||||
|
|
||||||
if position.side != "NONE":
|
if position.side != "NONE":
|
||||||
self._sync_state_from_position(state)
|
self._sync_state_from_position(state)
|
||||||
return ExecutionDecision(
|
return ExecutionDecision("NONE", False, "Позиция уже открыта.")
|
||||||
action="NONE",
|
|
||||||
can_execute=False,
|
|
||||||
reason="Позиция уже открыта.",
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ticker = ExchangeService().get_price(state.symbol)
|
ticker = ExchangeService().get_price(state.symbol)
|
||||||
entry_price = ticker.price
|
entry_price = ticker.price
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return ExecutionDecision(
|
return ExecutionDecision("NONE", False, f"Не удалось получить цену для paper execution: {exc}")
|
||||||
action="NONE",
|
|
||||||
can_execute=False,
|
|
||||||
reason=f"Не удалось получить цену для paper execution: {exc}",
|
|
||||||
)
|
|
||||||
|
|
||||||
now = self._now_time()
|
now = self._now_time()
|
||||||
size = self._calculate_position_size(state)
|
size = self._calculate_position_size(state)
|
||||||
@@ -106,102 +75,82 @@ class ExecutionEngine:
|
|||||||
|
|
||||||
self._sync_state_from_position(state)
|
self._sync_state_from_position(state)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"execution_type": "ENTRY",
|
||||||
|
"action": action,
|
||||||
|
"symbol": state.symbol,
|
||||||
|
"side": side,
|
||||||
|
"entry_price": entry_price,
|
||||||
|
"size": size,
|
||||||
|
"leverage": state.leverage,
|
||||||
|
"signal": state.last_signal,
|
||||||
|
"confidence": state.last_signal_confidence,
|
||||||
|
"repeat_count": state.last_signal_repeat_count,
|
||||||
|
"reason": state.last_signal_reason,
|
||||||
|
"opened_at": now,
|
||||||
|
}
|
||||||
|
|
||||||
JournalService().log_ui_info(
|
JournalService().log_ui_info(
|
||||||
event_type="paper_position_opened",
|
event_type="paper_position_opened",
|
||||||
message=f"Paper-позиция открыта: {side} {state.symbol}",
|
message=f"Paper ENTRY открыта: {side} {state.symbol}",
|
||||||
screen="auto",
|
screen="auto",
|
||||||
action="paper_execution",
|
action="paper_execution",
|
||||||
payload={
|
payload=payload,
|
||||||
"symbol": state.symbol,
|
|
||||||
"side": side,
|
|
||||||
"entry_price": entry_price,
|
|
||||||
"size": size,
|
|
||||||
"leverage": state.leverage,
|
|
||||||
"signal": state.last_signal,
|
|
||||||
"confidence": state.last_signal_confidence,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
EventBus.emit(
|
EventBus.emit("paper_position_opened", payload)
|
||||||
"paper_position_opened",
|
|
||||||
{
|
|
||||||
"symbol": state.symbol,
|
|
||||||
"side": side,
|
|
||||||
"entry_price": entry_price,
|
|
||||||
"size": size,
|
|
||||||
"leverage": state.leverage,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return ExecutionDecision(
|
return ExecutionDecision(action, True, f"Paper ENTRY {side} открыта.")
|
||||||
action=action,
|
|
||||||
can_execute=True,
|
|
||||||
reason=f"Paper-позиция {side} открыта.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# закрыть текущую paper-позицию
|
|
||||||
def _close_position(self, state: AutoTradeState) -> ExecutionDecision:
|
def _close_position(self, state: AutoTradeState) -> ExecutionDecision:
|
||||||
position = type(self)._position
|
position = type(self)._position
|
||||||
|
|
||||||
if position.side == "NONE":
|
if position.side == "NONE":
|
||||||
self._sync_state_from_position(state)
|
self._sync_state_from_position(state)
|
||||||
return ExecutionDecision(
|
return ExecutionDecision("NONE", False, "Нет открытой позиции для закрытия.")
|
||||||
action="NONE",
|
|
||||||
can_execute=False,
|
|
||||||
reason="Нет открытой позиции для закрытия.",
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ticker = ExchangeService().get_price(state.symbol)
|
ticker = ExchangeService().get_price(state.symbol)
|
||||||
exit_price = ticker.price
|
exit_price = ticker.price
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return ExecutionDecision(
|
return ExecutionDecision("NONE", False, f"Ошибка получения цены для закрытия: {exc}")
|
||||||
action="NONE",
|
|
||||||
can_execute=False,
|
|
||||||
reason=f"Ошибка получения цены для закрытия: {exc}",
|
|
||||||
)
|
|
||||||
|
|
||||||
pnl = self._calculate_pnl(exit_price)
|
pnl = self._calculate_pnl(exit_price)
|
||||||
|
now = self._now_time()
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"execution_type": "EXIT",
|
||||||
|
"action": "CLOSE",
|
||||||
|
"symbol": state.symbol,
|
||||||
|
"side": position.side,
|
||||||
|
"entry_price": position.entry_price,
|
||||||
|
"exit_price": exit_price,
|
||||||
|
"size": position.size,
|
||||||
|
"leverage": position.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": position.opened_at,
|
||||||
|
"closed_at": now,
|
||||||
|
}
|
||||||
|
|
||||||
JournalService().log_ui_info(
|
JournalService().log_ui_info(
|
||||||
event_type="paper_position_closed",
|
event_type="paper_position_closed",
|
||||||
message=f"Позиция закрыта: {position.side} {state.symbol}",
|
message=f"Paper EXIT закрыта: {position.side} {state.symbol}",
|
||||||
screen="auto",
|
screen="auto",
|
||||||
action="paper_execution",
|
action="paper_execution",
|
||||||
payload={
|
payload=payload,
|
||||||
"symbol": state.symbol,
|
|
||||||
"side": position.side,
|
|
||||||
"entry_price": position.entry_price,
|
|
||||||
"exit_price": exit_price,
|
|
||||||
"size": position.size,
|
|
||||||
"leverage": position.leverage,
|
|
||||||
"pnl": pnl,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
EventBus.emit(
|
EventBus.emit("paper_position_closed", payload)
|
||||||
"paper_position_closed",
|
|
||||||
{
|
|
||||||
"symbol": state.symbol,
|
|
||||||
"side": position.side,
|
|
||||||
"entry_price": position.entry_price,
|
|
||||||
"exit_price": exit_price,
|
|
||||||
"size": position.size,
|
|
||||||
"leverage": position.leverage,
|
|
||||||
"pnl": pnl,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
type(self)._position = PositionState()
|
type(self)._position = PositionState()
|
||||||
self._sync_state_from_position(state)
|
self._sync_state_from_position(state)
|
||||||
|
|
||||||
return ExecutionDecision(
|
return ExecutionDecision("CLOSE", True, "Paper EXIT выполнена.")
|
||||||
action="CLOSE",
|
|
||||||
can_execute=True,
|
|
||||||
reason="Позиция закрыта.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# проверить, нужно ли закрывать позицию по противоположному сигналу
|
|
||||||
def _should_close_position(self, state: AutoTradeState) -> bool:
|
def _should_close_position(self, state: AutoTradeState) -> bool:
|
||||||
position = type(self)._position
|
position = type(self)._position
|
||||||
|
|
||||||
@@ -216,7 +165,6 @@ class ExecutionEngine:
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# обновить unrealized PnL по текущей цене
|
|
||||||
def _update_unrealized_pnl(self, state: AutoTradeState) -> None:
|
def _update_unrealized_pnl(self, state: AutoTradeState) -> None:
|
||||||
position = type(self)._position
|
position = type(self)._position
|
||||||
|
|
||||||
@@ -236,14 +184,11 @@ class ExecutionEngine:
|
|||||||
|
|
||||||
self._sync_state_from_position(state)
|
self._sync_state_from_position(state)
|
||||||
|
|
||||||
# временный расчёт размера позиции для paper mode
|
|
||||||
def _calculate_position_size(self, state: AutoTradeState) -> float:
|
def _calculate_position_size(self, state: AutoTradeState) -> float:
|
||||||
risk_percent = state.risk_percent or 0.0
|
risk_percent = state.risk_percent or 0.0
|
||||||
leverage = state.leverage or 1.0
|
leverage = state.leverage or 1.0
|
||||||
|
|
||||||
return round((risk_percent * leverage) / 100, 8)
|
return round((risk_percent * leverage) / 100, 8)
|
||||||
|
|
||||||
# расчёт PnL для paper-позиции
|
|
||||||
def _calculate_pnl(self, current_price: float) -> float:
|
def _calculate_pnl(self, current_price: float) -> float:
|
||||||
position = type(self)._position
|
position = type(self)._position
|
||||||
|
|
||||||
@@ -258,7 +203,6 @@ class ExecutionEngine:
|
|||||||
|
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
# синхронизировать AutoTradeState с текущей paper-позицией
|
|
||||||
def _sync_state_from_position(self, state: AutoTradeState) -> None:
|
def _sync_state_from_position(self, state: AutoTradeState) -> None:
|
||||||
position = type(self)._position
|
position = type(self)._position
|
||||||
|
|
||||||
@@ -267,6 +211,5 @@ class ExecutionEngine:
|
|||||||
state.position_size = position.size
|
state.position_size = position.size
|
||||||
state.unrealized_pnl_usd = position.unrealized_pnl_usd
|
state.unrealized_pnl_usd = position.unrealized_pnl_usd
|
||||||
|
|
||||||
# текущее время для paper execution
|
|
||||||
def _now_time(self) -> str:
|
def _now_time(self) -> str:
|
||||||
return datetime.now().strftime("%H:%M:%S")
|
return datetime.now().strftime("%H:%M:%S")
|
||||||
@@ -179,6 +179,13 @@
|
|||||||
- compatible with cooldown & suppression
|
- compatible with cooldown & suppression
|
||||||
- extended debug_signal parameters
|
- extended debug_signal parameters
|
||||||
|
|
||||||
|
#### 07.4.3.8 — Telegram Execution Alerts ✅
|
||||||
|
- Telegram alerts for paper position opened
|
||||||
|
- Telegram alerts for paper position closed
|
||||||
|
- Entry / Exit / Size / PnL rendering
|
||||||
|
- readable USD formatting
|
||||||
|
- signal alerts separated from execution alerts
|
||||||
|
|
||||||
|
|
||||||
### 07.4.4
|
### 07.4.4
|
||||||
⏳ Grid Strategy
|
⏳ Grid Strategy
|
||||||
|
|||||||
@@ -161,6 +161,14 @@
|
|||||||
- compatible with cooldown & suppression
|
- compatible with cooldown & suppression
|
||||||
- extended debug_signal parameters
|
- extended debug_signal parameters
|
||||||
|
|
||||||
|
#### 07.4.3.8 — Telegram Execution Alerts ✅
|
||||||
|
|
||||||
|
- Telegram alerts for paper position opened
|
||||||
|
- Telegram alerts for paper position closed
|
||||||
|
- Entry / Exit / Size / PnL rendering
|
||||||
|
- readable USD formatting
|
||||||
|
- signal alerts separated from execution alerts
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 07.4.4
|
### 07.4.4
|
||||||
|
|||||||
258
docs/stages/stage-07_4_3_8-telegram-execution-alerts.md
Normal file
258
docs/stages/stage-07_4_3_8-telegram-execution-alerts.md
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
# Stage 07.4.3.8 — Telegram Execution Alerts
|
||||||
|
|
||||||
|
## Цель этапа
|
||||||
|
|
||||||
|
Добавить отдельные Telegram-уведомления для paper execution событий:
|
||||||
|
|
||||||
|
- открытие LONG / SHORT позиции;
|
||||||
|
- закрытие позиции;
|
||||||
|
- отображение Entry / Exit;
|
||||||
|
- отображение Size;
|
||||||
|
- отображение PnL;
|
||||||
|
- отделить signal alerts от execution alerts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что изменено
|
||||||
|
|
||||||
|
### 1. Отдельный alert-слой для execution
|
||||||
|
|
||||||
|
Теперь Telegram-уведомления разделены по смыслу:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Strong signal alert → рынок дал BUY / SELL
|
||||||
|
Execution alert → система открыла / закрыла paper-позицию
|
||||||
|
```
|
||||||
|
|
||||||
|
Это устраняет конфликт между:
|
||||||
|
|
||||||
|
```text
|
||||||
|
SELL для закрытия LONG
|
||||||
|
SELL для открытия SHORT
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Обработка EventBus событий execution
|
||||||
|
|
||||||
|
В `AutoTradeRunner` добавлена обработка событий:
|
||||||
|
|
||||||
|
```text
|
||||||
|
paper_position_opened
|
||||||
|
paper_position_closed
|
||||||
|
```
|
||||||
|
|
||||||
|
Теперь runner реагирует не только на:
|
||||||
|
|
||||||
|
```text
|
||||||
|
auto_decision_changed
|
||||||
|
```
|
||||||
|
|
||||||
|
но и на paper execution события.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Telegram alert при открытии позиции
|
||||||
|
|
||||||
|
Пример сообщения:
|
||||||
|
|
||||||
|
```text
|
||||||
|
📄 Paper position opened 🟢 LONG
|
||||||
|
|
||||||
|
BTC / USD · x2
|
||||||
|
Entry: $ 79 710.10
|
||||||
|
Size: 0.01
|
||||||
|
```
|
||||||
|
|
||||||
|
Для SHORT:
|
||||||
|
|
||||||
|
```text
|
||||||
|
📄 Paper position opened 🔴 SHORT
|
||||||
|
|
||||||
|
BTC / USD · x2
|
||||||
|
Entry: $ 79 710.10
|
||||||
|
Size: 0.01
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Telegram alert при закрытии позиции
|
||||||
|
|
||||||
|
Пример сообщения:
|
||||||
|
|
||||||
|
```text
|
||||||
|
✅ Paper position closed
|
||||||
|
|
||||||
|
LONG · BTC / USD · x2
|
||||||
|
Entry: $ 79 710.10
|
||||||
|
Exit: $ 79 720.15
|
||||||
|
Size: 0.01
|
||||||
|
|
||||||
|
PnL: 🟢 +0.10 USD
|
||||||
|
```
|
||||||
|
|
||||||
|
Для отрицательного PnL:
|
||||||
|
|
||||||
|
```text
|
||||||
|
PnL: 🔴 -0.02 USD
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Улучшено форматирование валют
|
||||||
|
|
||||||
|
Добавлены UI helper-функции:
|
||||||
|
|
||||||
|
```python
|
||||||
|
format_usd_price(...)
|
||||||
|
format_usd_pnl(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Теперь цены выводятся в читаемом формате:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ 79 710.10
|
||||||
|
```
|
||||||
|
|
||||||
|
а не:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ 79710.10
|
||||||
|
```
|
||||||
|
|
||||||
|
PnL отображается с направлением:
|
||||||
|
|
||||||
|
```text
|
||||||
|
🟢 +1.25 USD
|
||||||
|
🔴 -0.02 USD
|
||||||
|
⚪ 0.00 USD
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Dedup для execution alerts
|
||||||
|
|
||||||
|
Добавлен отдельный ключ для execution alert:
|
||||||
|
|
||||||
|
```text
|
||||||
|
event_type + symbol + side + entry_price + exit_price + size + pnl
|
||||||
|
```
|
||||||
|
|
||||||
|
Это защищает от повторной отправки одного и того же execution-события, но не блокирует новые реальные события.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Изменённые файлы
|
||||||
|
|
||||||
|
```text
|
||||||
|
app/src/trading/auto/runner.py
|
||||||
|
app/src/trading/execution/engine.py
|
||||||
|
app/src/telegram/ui/currency_ui.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Проверка
|
||||||
|
|
||||||
|
### 1. Открытие LONG
|
||||||
|
|
||||||
|
```text
|
||||||
|
/debug_signal BUY 0.95 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидаемо:
|
||||||
|
|
||||||
|
```text
|
||||||
|
🚨 HIGH · 🟢 BUY
|
||||||
|
📄 Paper position opened 🟢 LONG
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Закрытие LONG
|
||||||
|
|
||||||
|
```text
|
||||||
|
/debug_signal SELL 0.95 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидаемо:
|
||||||
|
|
||||||
|
```text
|
||||||
|
🚨 HIGH · 🔴 SELL
|
||||||
|
✅ Paper position closed
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Открытие SHORT
|
||||||
|
|
||||||
|
После закрытия LONG:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/debug_signal SELL 0.96 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидаемо:
|
||||||
|
|
||||||
|
```text
|
||||||
|
🚨 HIGH · 🔴 SELL
|
||||||
|
📄 Paper position opened 🔴 SHORT
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Закрытие SHORT
|
||||||
|
|
||||||
|
```text
|
||||||
|
/debug_signal BUY 0.96 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидаемо:
|
||||||
|
|
||||||
|
```text
|
||||||
|
🚨 HIGH · 🟢 BUY
|
||||||
|
✅ Paper position closed
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Архитектурный результат
|
||||||
|
|
||||||
|
Теперь система имеет отдельные слои:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Strategy
|
||||||
|
↓
|
||||||
|
AutoTradeService
|
||||||
|
↓
|
||||||
|
EventBus
|
||||||
|
↓
|
||||||
|
Signal Alert Layer
|
||||||
|
↓
|
||||||
|
ExecutionEngine
|
||||||
|
↓
|
||||||
|
Execution Alert Layer
|
||||||
|
```
|
||||||
|
|
||||||
|
Главное разделение:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Signal alert ≠ Execution alert
|
||||||
|
```
|
||||||
|
|
||||||
|
Это делает поведение более предсказуемым и готовит систему к следующему этапу — position flip flow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Следующий этап
|
||||||
|
|
||||||
|
```text
|
||||||
|
07.4.3.9 — Position flip flow
|
||||||
|
```
|
||||||
|
|
||||||
|
План:
|
||||||
|
|
||||||
|
- SELL при LONG → закрыть LONG и открыть SHORT в одном цикле;
|
||||||
|
- BUY при SHORT → закрыть SHORT и открыть LONG в одном цикле;
|
||||||
|
- отдельные alerts для flip-сценариев;
|
||||||
|
- подготовка к реальному execution.
|
||||||
Reference in New Issue
Block a user