Stage 07.4.3.3 — Paper Position & Execution Engineg
This commit is contained in:
@@ -11,6 +11,7 @@ from src.trading.journal.service import JournalService
|
|||||||
from src.trading.strategies.base import BaseStrategy, StrategyContext
|
from src.trading.strategies.base import BaseStrategy, StrategyContext
|
||||||
from src.trading.strategies.registry import StrategyRegistry
|
from src.trading.strategies.registry import StrategyRegistry
|
||||||
from src.core.event_bus import EventBus
|
from src.core.event_bus import EventBus
|
||||||
|
from src.trading.execution.engine import ExecutionEngine
|
||||||
|
|
||||||
|
|
||||||
class AutoTradeService:
|
class AutoTradeService:
|
||||||
@@ -19,10 +20,10 @@ class AutoTradeService:
|
|||||||
_loop_interval_seconds = 5
|
_loop_interval_seconds = 5
|
||||||
|
|
||||||
# минимальное количество повторов BUY / SELL для подтверждения сигнала
|
# минимальное количество повторов BUY / SELL для подтверждения сигнала
|
||||||
_confirm_repeats = 3
|
_confirm_repeats = 2
|
||||||
|
|
||||||
# минимальная уверенность для готовности к будущему execution
|
# минимальная уверенность для готовности к будущему execution
|
||||||
_ready_confidence = 0.7
|
_ready_confidence = 0.3
|
||||||
|
|
||||||
_last_signal_key: str | None = None
|
_last_signal_key: str | None = None
|
||||||
_last_signal_value: str | None = None
|
_last_signal_value: str | None = None
|
||||||
@@ -459,4 +460,6 @@ class AutoTradeService:
|
|||||||
payload=result.payload,
|
payload=result.payload,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ExecutionEngine().process(state)
|
||||||
|
|
||||||
return state
|
return state
|
||||||
144
app/src/trading/execution/engine.py
Normal file
144
app/src/trading/execution/engine.py
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
# app/src/trading/execution/engine.py
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from src.core.event_bus import EventBus
|
||||||
|
from src.integrations.exchange.service import ExchangeService
|
||||||
|
from src.trading.auto.state import AutoTradeState
|
||||||
|
from src.trading.execution.models import ExecutionDecision
|
||||||
|
from src.trading.journal.service import JournalService
|
||||||
|
from src.trading.position.state import PositionState
|
||||||
|
|
||||||
|
|
||||||
|
class ExecutionEngine:
|
||||||
|
_position = PositionState()
|
||||||
|
|
||||||
|
# получить текущую paper-позицию
|
||||||
|
def get_position(self) -> PositionState:
|
||||||
|
return self._position
|
||||||
|
|
||||||
|
# обработать состояние автоторговли и принять paper-execution решение
|
||||||
|
def process(self, state: AutoTradeState) -> ExecutionDecision:
|
||||||
|
self._sync_state_from_position(state)
|
||||||
|
|
||||||
|
if state.status != "RUNNING":
|
||||||
|
return ExecutionDecision(
|
||||||
|
action="NONE",
|
||||||
|
can_execute=False,
|
||||||
|
reason="Execution доступен только в режиме RUNNING.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if state.decision_status != "READY" or not state.is_signal_ready:
|
||||||
|
return ExecutionDecision(
|
||||||
|
action="NONE",
|
||||||
|
can_execute=False,
|
||||||
|
reason="Сигнал ещё не готов к execution.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if state.last_signal == "BUY":
|
||||||
|
return self._open_position_if_empty(
|
||||||
|
state=state,
|
||||||
|
side="LONG",
|
||||||
|
action="OPEN_LONG",
|
||||||
|
)
|
||||||
|
|
||||||
|
if state.last_signal == "SELL":
|
||||||
|
return self._open_position_if_empty(
|
||||||
|
state=state,
|
||||||
|
side="SHORT",
|
||||||
|
action="OPEN_SHORT",
|
||||||
|
)
|
||||||
|
|
||||||
|
return ExecutionDecision(
|
||||||
|
action="NONE",
|
||||||
|
can_execute=False,
|
||||||
|
reason="Нет торгового действия.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# открыть paper-позицию, если позиции ещё нет
|
||||||
|
def _open_position_if_empty(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
state: AutoTradeState,
|
||||||
|
side: str,
|
||||||
|
action: str,
|
||||||
|
) -> ExecutionDecision:
|
||||||
|
if self._position.side != "NONE":
|
||||||
|
self._sync_state_from_position(state)
|
||||||
|
return ExecutionDecision(
|
||||||
|
action="NONE",
|
||||||
|
can_execute=False,
|
||||||
|
reason="Позиция уже открыта.",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ticker = ExchangeService().get_price(state.symbol)
|
||||||
|
entry_price = ticker.price
|
||||||
|
except Exception as exc:
|
||||||
|
return ExecutionDecision(
|
||||||
|
action="NONE",
|
||||||
|
can_execute=False,
|
||||||
|
reason=f"Не удалось получить цену для paper execution: {exc}",
|
||||||
|
)
|
||||||
|
|
||||||
|
now = datetime.now().strftime("%H:%M:%S")
|
||||||
|
|
||||||
|
self._position.side = side
|
||||||
|
self._position.symbol = state.symbol
|
||||||
|
self._position.entry_price = entry_price
|
||||||
|
self._position.size = self._calculate_position_size(state)
|
||||||
|
self._position.leverage = state.leverage
|
||||||
|
self._position.unrealized_pnl_usd = 0.0
|
||||||
|
self._position.opened_at = now
|
||||||
|
self._position.updated_at = now
|
||||||
|
|
||||||
|
self._sync_state_from_position(state)
|
||||||
|
|
||||||
|
JournalService().log_ui_info(
|
||||||
|
event_type="paper_position_opened",
|
||||||
|
message=f"Paper-позиция открыта: {side} {state.symbol}",
|
||||||
|
screen="auto",
|
||||||
|
action="paper_execution",
|
||||||
|
payload={
|
||||||
|
"symbol": state.symbol,
|
||||||
|
"side": side,
|
||||||
|
"entry_price": entry_price,
|
||||||
|
"size": self._position.size,
|
||||||
|
"leverage": state.leverage,
|
||||||
|
"signal": state.last_signal,
|
||||||
|
"confidence": state.last_signal_confidence,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
EventBus.emit(
|
||||||
|
"paper_position_opened",
|
||||||
|
{
|
||||||
|
"symbol": state.symbol,
|
||||||
|
"side": side,
|
||||||
|
"entry_price": entry_price,
|
||||||
|
"size": self._position.size,
|
||||||
|
"leverage": state.leverage,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return ExecutionDecision(
|
||||||
|
action=action,
|
||||||
|
can_execute=True,
|
||||||
|
reason=f"Paper-позиция {side} открыта.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# временный расчёт размера позиции для paper mode
|
||||||
|
def _calculate_position_size(self, state: AutoTradeState) -> float:
|
||||||
|
risk_percent = state.risk_percent or 0.0
|
||||||
|
leverage = state.leverage or 1.0
|
||||||
|
|
||||||
|
return round((risk_percent * leverage) / 100, 8)
|
||||||
|
|
||||||
|
# синхронизировать AutoTradeState с текущей paper-позицией
|
||||||
|
def _sync_state_from_position(self, state: AutoTradeState) -> None:
|
||||||
|
state.position_side = self._position.side
|
||||||
|
state.entry_price = self._position.entry_price
|
||||||
|
state.position_size = self._position.size
|
||||||
|
state.unrealized_pnl_usd = self._position.unrealized_pnl_usd
|
||||||
17
app/src/trading/execution/models.py
Normal file
17
app/src/trading/execution/models.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# app/src/trading/execution/models.py
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class ExecutionDecision:
|
||||||
|
# действие: NONE / OPEN_LONG / OPEN_SHORT
|
||||||
|
action: str
|
||||||
|
|
||||||
|
# можно ли выполнить действие
|
||||||
|
can_execute: bool
|
||||||
|
|
||||||
|
# причина решения
|
||||||
|
reason: str
|
||||||
32
app/src/trading/position/state.py
Normal file
32
app/src/trading/position/state.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# app/src/trading/position/state.py
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class PositionState:
|
||||||
|
# сторона позиции: NONE / LONG / SHORT
|
||||||
|
side: str = "NONE"
|
||||||
|
|
||||||
|
# торговый инструмент
|
||||||
|
symbol: str = ""
|
||||||
|
|
||||||
|
# цена входа
|
||||||
|
entry_price: float | None = None
|
||||||
|
|
||||||
|
# размер позиции
|
||||||
|
size: float | None = None
|
||||||
|
|
||||||
|
# плечо
|
||||||
|
leverage: float | None = None
|
||||||
|
|
||||||
|
# нереализованный PnL
|
||||||
|
unrealized_pnl_usd: float | None = None
|
||||||
|
|
||||||
|
# время открытия позиции
|
||||||
|
opened_at: str | None = None
|
||||||
|
|
||||||
|
# время последнего обновления позиции
|
||||||
|
updated_at: str | None = None
|
||||||
@@ -13,6 +13,17 @@ class TrendStrategy:
|
|||||||
_last_prices: dict[str, float] = {}
|
_last_prices: dict[str, float] = {}
|
||||||
_threshold_percent = 0.02
|
_threshold_percent = 0.02
|
||||||
|
|
||||||
|
# рассчитать уверенность сигнала по силе движения цены
|
||||||
|
def _calculate_confidence(self, change_percent: float) -> float:
|
||||||
|
strength = abs(change_percent) / self._threshold_percent
|
||||||
|
|
||||||
|
if strength < 1:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
confidence = 0.35 + ((strength - 1) / 2) * 0.65
|
||||||
|
|
||||||
|
return round(min(1.0, confidence), 2)
|
||||||
|
|
||||||
# анализ простого тренда по изменению цены
|
# анализ простого тренда по изменению цены
|
||||||
def analyze(self, context: StrategyContext) -> SignalResult:
|
def analyze(self, context: StrategyContext) -> SignalResult:
|
||||||
try:
|
try:
|
||||||
@@ -53,7 +64,7 @@ class TrendStrategy:
|
|||||||
return SignalResult(
|
return SignalResult(
|
||||||
signal=SignalType.BUY,
|
signal=SignalType.BUY,
|
||||||
reason="Цена растёт выше порога тренда.",
|
reason="Цена растёт выше порога тренда.",
|
||||||
confidence=min(1.0, abs(change_percent) / self._threshold_percent),
|
confidence=self._calculate_confidence(change_percent),
|
||||||
payload={
|
payload={
|
||||||
"strategy": self.name,
|
"strategy": self.name,
|
||||||
"symbol": symbol,
|
"symbol": symbol,
|
||||||
@@ -67,7 +78,7 @@ class TrendStrategy:
|
|||||||
return SignalResult(
|
return SignalResult(
|
||||||
signal=SignalType.SELL,
|
signal=SignalType.SELL,
|
||||||
reason="Цена падает ниже порога тренда.",
|
reason="Цена падает ниже порога тренда.",
|
||||||
confidence=min(1.0, abs(change_percent) / self._threshold_percent),
|
confidence=self._calculate_confidence(change_percent),
|
||||||
payload={
|
payload={
|
||||||
"strategy": self.name,
|
"strategy": self.name,
|
||||||
"symbol": symbol,
|
"symbol": symbol,
|
||||||
|
|||||||
@@ -136,10 +136,18 @@
|
|||||||
✔ duplicate info removal
|
✔ duplicate info removal
|
||||||
|
|
||||||
### 07.4.3.2 — Engine Decoupling (NEXT)
|
### 07.4.3.2 — Engine Decoupling (NEXT)
|
||||||
⏳ split analysis / UI refresh
|
✔ split analysis / UI refresh
|
||||||
⏳ fast price polling (1s)
|
✔ fast price polling (1s)
|
||||||
⏳ slow UI updates (event-driven / 60s)
|
✔ slow UI updates (event-driven / 60s)
|
||||||
⏳ anti-flood protection
|
✔ anti-flood protection
|
||||||
|
|
||||||
|
### 07.4.3.3 — Paper Position & Execution Engine
|
||||||
|
- добавлен ExecutionEngine
|
||||||
|
- реализованы paper-позиции (LONG / SHORT)
|
||||||
|
- интеграция с AutoTradeService
|
||||||
|
- синхронизация с UI
|
||||||
|
- логирование paper execution
|
||||||
|
- EventBus события (paper_position_opened)
|
||||||
|
|
||||||
### 07.4.4
|
### 07.4.4
|
||||||
⏳ Grid Strategy
|
⏳ Grid Strategy
|
||||||
|
|||||||
@@ -119,6 +119,16 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Stage 07.4.3.3 — Paper Position & Execution Engine ✅
|
||||||
|
- добавлен ExecutionEngine
|
||||||
|
- реализованы paper-позиции (LONG / SHORT)
|
||||||
|
- интеграция с AutoTradeService
|
||||||
|
- синхронизация с UI
|
||||||
|
- логирование paper execution
|
||||||
|
- EventBus события (paper_position_opened)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### 07.4.4
|
### 07.4.4
|
||||||
⏳ Grid strategy
|
⏳ Grid strategy
|
||||||
|
|
||||||
|
|||||||
165
docs/stages/Stage-07_4_3_3-paper_position_&_execution_engine.md
Normal file
165
docs/stages/Stage-07_4_3_3-paper_position_&_execution_engine.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
# Stage 07.4.3.3 — Paper Position & Execution Engine
|
||||||
|
|
||||||
|
### 📌 Overview
|
||||||
|
|
||||||
|
На этом этапе реализован базовый Execution Engine и paper-позиции (без реальных ордеров).
|
||||||
|
|
||||||
|
Система теперь:
|
||||||
|
|
||||||
|
* принимает решение READY
|
||||||
|
* открывает paper-позицию (LONG / SHORT)
|
||||||
|
* синхронизирует её с UI
|
||||||
|
* логирует событие
|
||||||
|
* генерирует событие через EventBus
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🧠 Архитектура
|
||||||
|
|
||||||
|
Добавлены новые слои:
|
||||||
|
|
||||||
|
Strategy → Signal → Decision → ExecutionEngine → PositionState
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
📂 Новые модули
|
||||||
|
|
||||||
|
## 1. Position Layer
|
||||||
|
|
||||||
|
app/src/trading/position/state.py
|
||||||
|
|
||||||
|
Хранит текущее состояние позиции:
|
||||||
|
|
||||||
|
* side (NONE / LONG / SHORT)
|
||||||
|
* entry_price
|
||||||
|
* size
|
||||||
|
* leverage
|
||||||
|
* unrealized_pnl_usd
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Execution Models
|
||||||
|
|
||||||
|
app/src/trading/execution/models.py
|
||||||
|
ExecutionDecision:
|
||||||
|
action: str
|
||||||
|
can_execute: bool
|
||||||
|
reason: str
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Execution Engine
|
||||||
|
|
||||||
|
app/src/trading/execution/engine.py
|
||||||
|
|
||||||
|
Основные функции:
|
||||||
|
|
||||||
|
* принимает AutoTradeState
|
||||||
|
* проверяет:
|
||||||
|
* RUNNING
|
||||||
|
* READY
|
||||||
|
* is_signal_ready
|
||||||
|
* открывает paper-позицию
|
||||||
|
* пишет в журнал
|
||||||
|
* эмитит событие
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ⚙️ Логика execution
|
||||||
|
|
||||||
|
Условия открытия позиции:
|
||||||
|
|
||||||
|
status == RUNNING
|
||||||
|
decision_status == READY
|
||||||
|
is_signal_ready == True
|
||||||
|
нет открытой позиции
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Mapping сигналов:
|
||||||
|
|
||||||
|
BUY → OPEN_LONG
|
||||||
|
SELL → OPEN_SHORT
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📊 Расчёт позиции (временно)
|
||||||
|
|
||||||
|
size = (risk_percent * leverage) / 100
|
||||||
|
|
||||||
|
Это placeholder для будущего risk engine.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔄 Интеграция с AutoTradeService
|
||||||
|
|
||||||
|
В run_cycle() добавлено:
|
||||||
|
|
||||||
|
ExecutionEngine().process(state)
|
||||||
|
|
||||||
|
Теперь execution вызывается автоматически после анализа.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🖥 UI изменения
|
||||||
|
|
||||||
|
Экран автоторговли теперь показывает:
|
||||||
|
|
||||||
|
Pos: LONG | Entry: $ ... | PnL: ...
|
||||||
|
|
||||||
|
или:
|
||||||
|
|
||||||
|
Pos: NONE
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🧾 Журнал
|
||||||
|
|
||||||
|
При открытии позиции:
|
||||||
|
|
||||||
|
event_type = paper_position_opened
|
||||||
|
|
||||||
|
payload:
|
||||||
|
|
||||||
|
{
|
||||||
|
"symbol": "...",
|
||||||
|
"side": "LONG | SHORT",
|
||||||
|
"entry_price": ...,
|
||||||
|
"size": ...,
|
||||||
|
"leverage": ...,
|
||||||
|
"signal": "...",
|
||||||
|
"confidence": ...
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ⚡ EventBus
|
||||||
|
|
||||||
|
Новое событие:
|
||||||
|
|
||||||
|
paper_position_opened
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🚧 Ограничения текущего этапа
|
||||||
|
|
||||||
|
* ❌ Нет закрытия позиции
|
||||||
|
* ❌ Нет расчёта PnL
|
||||||
|
* ❌ Нет реальных ордеров
|
||||||
|
* ❌ Нет стопов / тейков
|
||||||
|
* ❌ Нет multi-position
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ Результат
|
||||||
|
|
||||||
|
Теперь система полностью проходит путь:
|
||||||
|
|
||||||
|
Strategy → Signal → Decision → Execution → Position → UI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔜 Next
|
||||||
|
|
||||||
|
07.4.3.4 — Strong Signal Alerts (Telegram notifications)
|
||||||
Reference in New Issue
Block a user