diff --git a/app/src/trading/auto/service.py b/app/src/trading/auto/service.py index b5c319e..eae59e7 100644 --- a/app/src/trading/auto/service.py +++ b/app/src/trading/auto/service.py @@ -11,6 +11,7 @@ from src.trading.journal.service import JournalService from src.trading.strategies.base import BaseStrategy, StrategyContext from src.trading.strategies.registry import StrategyRegistry from src.core.event_bus import EventBus +from src.trading.execution.engine import ExecutionEngine class AutoTradeService: @@ -19,10 +20,10 @@ class AutoTradeService: _loop_interval_seconds = 5 # минимальное количество повторов BUY / SELL для подтверждения сигнала - _confirm_repeats = 3 + _confirm_repeats = 2 # минимальная уверенность для готовности к будущему execution - _ready_confidence = 0.7 + _ready_confidence = 0.3 _last_signal_key: str | None = None _last_signal_value: str | None = None @@ -459,4 +460,6 @@ class AutoTradeService: payload=result.payload, ) + ExecutionEngine().process(state) + return state \ No newline at end of file diff --git a/app/src/trading/execution/engine.py b/app/src/trading/execution/engine.py new file mode 100644 index 0000000..171a56d --- /dev/null +++ b/app/src/trading/execution/engine.py @@ -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 \ No newline at end of file diff --git a/app/src/trading/execution/models.py b/app/src/trading/execution/models.py new file mode 100644 index 0000000..f6c136c --- /dev/null +++ b/app/src/trading/execution/models.py @@ -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 \ No newline at end of file diff --git a/app/src/trading/position/state.py b/app/src/trading/position/state.py new file mode 100644 index 0000000..3271e68 --- /dev/null +++ b/app/src/trading/position/state.py @@ -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 \ No newline at end of file diff --git a/app/src/trading/strategies/trend.py b/app/src/trading/strategies/trend.py index 145c2f5..ed28ef0 100644 --- a/app/src/trading/strategies/trend.py +++ b/app/src/trading/strategies/trend.py @@ -13,6 +13,17 @@ class TrendStrategy: _last_prices: dict[str, float] = {} _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: try: @@ -53,7 +64,7 @@ class TrendStrategy: return SignalResult( signal=SignalType.BUY, reason="Цена растёт выше порога тренда.", - confidence=min(1.0, abs(change_percent) / self._threshold_percent), + confidence=self._calculate_confidence(change_percent), payload={ "strategy": self.name, "symbol": symbol, @@ -67,7 +78,7 @@ class TrendStrategy: return SignalResult( signal=SignalType.SELL, reason="Цена падает ниже порога тренда.", - confidence=min(1.0, abs(change_percent) / self._threshold_percent), + confidence=self._calculate_confidence(change_percent), payload={ "strategy": self.name, "symbol": symbol, diff --git a/docs/roadmap/master-roadmap.md b/docs/roadmap/master-roadmap.md index a3caef5..2398a42 100644 --- a/docs/roadmap/master-roadmap.md +++ b/docs/roadmap/master-roadmap.md @@ -136,10 +136,18 @@ ✔ duplicate info removal ### 07.4.3.2 — Engine Decoupling (NEXT) -⏳ split analysis / UI refresh -⏳ fast price polling (1s) -⏳ slow UI updates (event-driven / 60s) -⏳ anti-flood protection +✔ split analysis / UI refresh +✔ fast price polling (1s) +✔ slow UI updates (event-driven / 60s) +✔ 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 ⏳ Grid Strategy diff --git a/docs/roadmap/stage-07-auto-trading-roadmap.md b/docs/roadmap/stage-07-auto-trading-roadmap.md index 3c78f2b..8bda882 100644 --- a/docs/roadmap/stage-07-auto-trading-roadmap.md +++ b/docs/roadmap/stage-07-auto-trading-roadmap.md @@ -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 ⏳ Grid strategy diff --git a/docs/stages/Stage-07_4_3_3-paper_position_&_execution_engine.md b/docs/stages/Stage-07_4_3_3-paper_position_&_execution_engine.md new file mode 100644 index 0000000..8f189c8 --- /dev/null +++ b/docs/stages/Stage-07_4_3_3-paper_position_&_execution_engine.md @@ -0,0 +1,188 @@ +Готово. Ниже — документация этапа 07.4.3.3 в формате .md (можешь просто сохранить как файл). + +⸻ + +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) + +: + +⸻ + +🗺 Обновление roadmap + +Добавь: + +Stage 07.4.3.3 — Paper Position & Execution Engine ✅ +- добавлен ExecutionEngine +- реализованы paper-позиции (LONG / SHORT) +- интеграция с AutoTradeService +- синхронизация с UI +- логирование paper execution +- EventBus события (paper_position_opened) + +⸻ + +Если готов — дальше сразу делаем 07.4.3.4 (уведомления в Telegram) 🚀 \ No newline at end of file