174 lines
6.0 KiB
Python
174 lines
6.0 KiB
Python
# app/src/trading/execution/engine.py
|
||
|
||
from __future__ import annotations
|
||
|
||
from src.trading.auto.state import AutoTradeState
|
||
from src.trading.execution.models import ExecutionDecision
|
||
from src.trading.position.state import PositionState
|
||
from src.trading.execution.pricing import ExecutionPricingMixin
|
||
from src.trading.execution.position_runtime import ExecutionPositionRuntimeMixin
|
||
from src.trading.execution.position_exit_decision import ExecutionPositionExitDecisionMixin
|
||
from src.trading.execution.position_protection import ExecutionPositionProtectionMixin
|
||
from src.trading.execution.supervisor import ExecutionSupervisorMixin
|
||
from src.trading.execution.sizing import ExecutionSizingMixin
|
||
from src.trading.execution.risk_close import ExecutionRiskCloseMixin
|
||
from src.trading.execution.flip import ExecutionFlipMixin
|
||
from src.trading.execution.position_actions import ExecutionPositionActionsMixin
|
||
from src.trading.execution.runtime_actions import ExecutionRuntimeActionsMixin
|
||
from src.trading.execution.calculations import ExecutionCalculationsMixin
|
||
from src.trading.execution.resets import ExecutionResetsMixin
|
||
from src.trading.execution.constants import (
|
||
EXECUTION_ACTION_NONE,
|
||
EXECUTION_ACTION_OPEN_LONG,
|
||
EXECUTION_ACTION_OPEN_SHORT,
|
||
EXECUTION_DECISION_READY,
|
||
EXECUTION_STATUS_RUNNING,
|
||
POSITION_SIDE_LONG,
|
||
POSITION_SIDE_SHORT,
|
||
SIGNAL_BUY,
|
||
SIGNAL_SELL,
|
||
)
|
||
|
||
|
||
class ExecutionEngine(
|
||
ExecutionCalculationsMixin,
|
||
ExecutionResetsMixin,
|
||
ExecutionPricingMixin,
|
||
ExecutionPositionRuntimeMixin,
|
||
ExecutionPositionExitDecisionMixin,
|
||
ExecutionSizingMixin,
|
||
ExecutionPositionActionsMixin,
|
||
ExecutionPositionProtectionMixin,
|
||
ExecutionSupervisorMixin,
|
||
ExecutionRiskCloseMixin,
|
||
ExecutionFlipMixin,
|
||
ExecutionRuntimeActionsMixin,
|
||
):
|
||
_position = PositionState()
|
||
_size_precision = 5
|
||
_min_flip_confidence = 0.65
|
||
_min_flip_repeat_count = 2
|
||
_min_flip_hold_seconds = 20
|
||
_flip_cooldown_seconds = 45
|
||
_loss_flip_confidence = 0.75
|
||
_last_flip_block_key: str | None = None
|
||
_runtime_action_cooldown_seconds = 30
|
||
_last_runtime_action_key: str | None = None
|
||
_emergency_halt_drawdown_usd = 250.0
|
||
_emergency_halt_loss_streak = 5
|
||
|
||
_execution_cooldown_after_loss_seconds = 90
|
||
|
||
_max_execution_snapshot_age_seconds = 5
|
||
|
||
_degraded_market_block_states = {
|
||
"CHAOTIC",
|
||
"LIQUIDITY_VOID",
|
||
}
|
||
|
||
_conflict_execution_block = True
|
||
|
||
_last_supervisor_block_key: str | None = None
|
||
|
||
# вернуть ExecutionDecision без выполнения торгового действия
|
||
def _skip_execution(
|
||
self,
|
||
state: AutoTradeState,
|
||
reason: str,
|
||
) -> ExecutionDecision:
|
||
state.last_execution_action = EXECUTION_ACTION_NONE
|
||
state.last_execution_reason = reason
|
||
|
||
return ExecutionDecision(
|
||
EXECUTION_ACTION_NONE,
|
||
False,
|
||
reason,
|
||
)
|
||
|
||
def process(self, state: AutoTradeState) -> ExecutionDecision:
|
||
# Synchronize runtime state
|
||
self._sync_state_from_position(state)
|
||
|
||
if state.status != EXECUTION_STATUS_RUNNING:
|
||
return self._skip_execution(
|
||
state,
|
||
"Execution доступен только в режиме RUNNING.",
|
||
)
|
||
|
||
self._update_unrealized_pnl(state)
|
||
|
||
# Emergency risk management
|
||
risk_decision = self._risk_close_decision(state)
|
||
if risk_decision is not None:
|
||
return risk_decision
|
||
|
||
# Runtime position protection
|
||
protection_decision = self._process_runtime_protection(state)
|
||
if protection_decision is not None:
|
||
return protection_decision
|
||
|
||
# Signal readiness validation
|
||
if state.decision_status != EXECUTION_DECISION_READY or not state.is_signal_ready:
|
||
reason = (
|
||
f"Execution ожидает READY "
|
||
f"(decision={state.decision_status}, "
|
||
f"ready={state.is_signal_ready})."
|
||
)
|
||
|
||
return self._skip_execution(
|
||
state,
|
||
reason,
|
||
)
|
||
|
||
# Execution supervisor
|
||
supervisor_decision = self._process_execution_supervisor(state)
|
||
if supervisor_decision is not None:
|
||
return supervisor_decision
|
||
|
||
# Existing position validation
|
||
position = type(self)._position
|
||
|
||
# Не пытаемся повторно открыть позицию в ту же сторону.
|
||
# Сигнал остаётся валидным для UI/Telegram, но execution не дублируется.
|
||
if position.side == POSITION_SIDE_LONG and state.last_signal == SIGNAL_BUY:
|
||
return ExecutionDecision(
|
||
EXECUTION_ACTION_NONE,
|
||
False,
|
||
"Сигнал BUY совпадает с уже открытой LONG позицией.",
|
||
)
|
||
|
||
if position.side == POSITION_SIDE_SHORT and state.last_signal == SIGNAL_SELL:
|
||
return ExecutionDecision(
|
||
EXECUTION_ACTION_NONE,
|
||
False,
|
||
"Сигнал SELL совпадает с уже открытой SHORT позицией.",
|
||
)
|
||
|
||
# Position flip
|
||
if self._should_flip_position(state):
|
||
flip_block_reason = self._flip_block_reason(state)
|
||
|
||
if flip_block_reason is not None:
|
||
return self._block_flip(state, flip_block_reason)
|
||
|
||
return self._flip_position(state)
|
||
|
||
# New position opening
|
||
if state.last_signal == SIGNAL_BUY:
|
||
return self._open_position_if_empty(
|
||
state=state,
|
||
side=POSITION_SIDE_LONG,
|
||
action=EXECUTION_ACTION_OPEN_LONG,
|
||
)
|
||
|
||
if state.last_signal == SIGNAL_SELL:
|
||
return self._open_position_if_empty(
|
||
state=state,
|
||
side=POSITION_SIDE_SHORT,
|
||
action=EXECUTION_ACTION_OPEN_SHORT,
|
||
)
|
||
|
||
return self._skip_execution(
|
||
state,
|
||
"Нет торгового действия.",
|
||
) |