Stage 07.4.4.1.14 — Execution refactoring and runtime semantics

This commit is contained in:
2026-07-02 16:25:18 +03:00
parent d9e6392e28
commit e08c604ff5
72 changed files with 13626 additions and 4233 deletions

View File

@@ -2,11 +2,35 @@
from __future__ import annotations
import time
from src.core.numbers import safe_float
from src.core.types import NumericLike
from src.trading.auto.state import AutoTradeState
from src.trading.execution.constants import (
EXECUTION_QUALITY_BLOCKED,
EXECUTION_QUALITY_WARNING,
MARKET_VOLATILITY_HIGH_STATES,
POSITION_CURRENT_INTERVAL_ADVERSE_MOVE_PERCENT,
POSITION_CURRENT_INTERVAL_RISK_MOVE_PERCENT,
POSITION_EXIT_PRESSURE_LOSS_PERCENT,
POSITION_HEALTH_DANGER,
POSITION_HEALTH_HEALTHY,
POSITION_HEALTH_PNL_GOOD_PROFIT_PERCENT,
POSITION_HEALTH_PNL_HARD_LOSS_PERCENT,
POSITION_HEALTH_PNL_HIGH_PRESSURE_PERCENT,
POSITION_HEALTH_PNL_PRESSURE_PERCENT,
POSITION_HEALTH_PRESSURE,
POSITION_HEALTH_UNKNOWN,
POSITION_HEALTH_WATCH,
POSITION_MOMENTUM_STRONG,
POSITION_RISK_ELEVATED,
POSITION_RISK_HIGH,
POSITION_RISK_LOW,
POSITION_RISK_MODERATE,
POSITION_STOP_LOSS_RATIO_CRITICAL,
POSITION_STOP_LOSS_RATIO_WARNING,
POSITION_STOP_LOSS_RATIO_WATCH,
get_position_health_thresholds,
)
class AutoPositionHealthMixin:
@@ -26,8 +50,11 @@ class AutoPositionHealthMixin:
state.position_exit_pressure = None
return
pnl_percent = self._position_pnl_percent(state)
hold_seconds = self._position_hold_seconds(state)
# PnL % и время удержания больше не считаем здесь.
# Эти значения должны приходить из единого расчёта position_metrics.py
# через execution/position_runtime.py.
pnl_percent = safe_float(state.position_pnl_percent)
hold_seconds = state.position_hold_seconds
trend_alignment = self._position_trend_alignment(state)
adverse_momentum = self._has_adverse_position_momentum(state)
@@ -70,41 +97,8 @@ class AutoPositionHealthMixin:
risk_level=risk_level,
)
# рассчитать PnL позиции в процентах от notional
def _position_pnl_percent(self, state: AutoTradeState) -> float | None:
entry_price = safe_float(state.entry_price)
size = safe_float(state.position_size)
pnl = safe_float(state.unrealized_pnl_usd)
if entry_price is None or entry_price <= 0:
return None
if size is None or size <= 0:
return None
if pnl is None:
return None
notional = entry_price * size
if notional <= 0:
return None
return round((pnl / notional) * 100, 4)
# рассчитать время удержания открытой позиции
def _position_hold_seconds(self, state: AutoTradeState) -> int | None:
opened_at = getattr(state, "position_opened_monotonic_at", None)
if opened_at is None:
return None
opened = safe_float(opened_at)
if opened is None:
return None
return max(0, int(time.monotonic() - opened))
def _health_thresholds(self, state: AutoTradeState) -> dict[str, float]:
return get_position_health_thresholds(state.symbol)
# определить давление на позицию по PnL
def _position_pressure(
@@ -125,16 +119,18 @@ class AutoPositionHealthMixin:
return "FLAT"
if percent <= -0.8:
thresholds = self._health_thresholds(state)
if percent <= thresholds["high_loss"]:
return "HIGH_LOSS"
if percent <= -0.3:
if percent <= thresholds["loss"]:
return "LOSS"
if percent >= 0.8:
if percent >= thresholds["strong_profit"]:
return "STRONG_PROFIT"
if percent >= 0.3:
if percent >= thresholds["profit"]:
return "PROFIT"
return "FLAT"
@@ -144,12 +140,21 @@ class AutoPositionHealthMixin:
side = str(state.position_side or "NONE").upper()
market_state = str(state.market_state or "").upper()
trend = str(state.market_trend or "").upper()
htf_trend = str(getattr(state, "htf_trend", "") or "").upper()
htf_alignment = str(getattr(state, "htf_alignment", "") or "").upper()
if side == "NONE":
return "NONE"
# HTF AGAINST важнее локального тренда:
# если старший таймфрейм против позиции, позиция считается рискованной.
if htf_alignment == "AGAINST":
return "AGAINST"
if side == "LONG":
if market_state == "TREND_UP" or trend == "UP":
if htf_trend in {"DOWN"}:
return "NEUTRAL"
return "ALIGNED"
if market_state == "TREND_DOWN" or trend == "DOWN":
@@ -157,6 +162,8 @@ class AutoPositionHealthMixin:
if side == "SHORT":
if market_state == "TREND_DOWN" or trend == "DOWN":
if htf_trend in {"UP"}:
return "NEUTRAL"
return "ALIGNED"
if market_state == "TREND_UP" or trend == "UP":
@@ -169,17 +176,45 @@ class AutoPositionHealthMixin:
side = str(state.position_side or "NONE").upper()
momentum_direction = str(state.momentum_direction or "").upper()
momentum_state = str(state.momentum_state or "").upper()
momentum_strength = safe_float(getattr(state, "momentum_strength", None)) or 0.0
current_interval_direction = str(
getattr(state, "current_interval_direction", "") or ""
).upper()
current_interval_change_percent = safe_float(
getattr(state, "current_interval_change_percent", None)
)
current_interval_move_abs = abs(current_interval_change_percent or 0.0)
current_interval_against_long = (
current_interval_direction == "DOWN"
and current_interval_move_abs >= POSITION_CURRENT_INTERVAL_ADVERSE_MOVE_PERCENT
)
current_interval_against_short = (
current_interval_direction == "UP"
and current_interval_move_abs >= POSITION_CURRENT_INTERVAL_ADVERSE_MOVE_PERCENT
)
if side == "LONG":
return (
momentum_direction == "DOWN"
or momentum_state in {"MOMENTUM_DOWN", "BREAKOUT_DOWN"}
momentum_state in {"MOMENTUM_DOWN", "BREAKOUT_DOWN"}
or current_interval_against_long
or (
momentum_direction == "DOWN"
and momentum_strength >= POSITION_MOMENTUM_STRONG
)
)
if side == "SHORT":
return (
momentum_direction == "UP"
or momentum_state in {"MOMENTUM_UP", "BREAKOUT_UP"}
momentum_state in {"MOMENTUM_UP", "BREAKOUT_UP"}
or current_interval_against_short
or (
momentum_direction == "UP"
and momentum_strength >= POSITION_MOMENTUM_STRONG
)
)
return False
@@ -195,28 +230,68 @@ class AutoPositionHealthMixin:
) -> int:
score = 100
percent = safe_float(pnl_percent)
stop_loss_percent = safe_float(getattr(state, "stop_loss_percent", None))
htf_alignment = str(getattr(state, "htf_alignment", "") or "").upper()
market_structure = str(getattr(state, "market_structure", "") or "").upper()
market_phase = str(getattr(state, "market_phase", "") or "").upper()
trend_quality = str(getattr(state, "market_trend_quality", "") or "").upper()
volatility = str(getattr(state, "market_volatility", "") or "").upper()
if percent is not None:
if percent <= -1.0:
score -= 35
elif percent <= -0.5:
score -= 22
if percent <= POSITION_HEALTH_PNL_HARD_LOSS_PERCENT:
score -= 40
elif percent <= POSITION_HEALTH_PNL_HIGH_PRESSURE_PERCENT:
score -= 30
elif percent <= POSITION_HEALTH_PNL_PRESSURE_PERCENT:
score -= 18
elif percent < 0:
score -= 10
elif percent >= 0.8:
score -= 8
elif percent >= POSITION_HEALTH_PNL_GOOD_PROFIT_PERCENT:
score += 5
# Если позиция прошла большую часть stop loss — ухудшаем score заранее.
if stop_loss_percent is not None and stop_loss_percent > 0:
loss_ratio = abs(percent) / stop_loss_percent if percent < 0 else 0.0
if loss_ratio >= POSITION_STOP_LOSS_RATIO_CRITICAL:
score -= 25
elif loss_ratio >= POSITION_STOP_LOSS_RATIO_WARNING:
score -= 15
if trend_alignment == "AGAINST":
score -= 25
score -= 30
elif trend_alignment == "NEUTRAL":
score -= 8
score -= 10
if adverse_momentum:
score -= 20
score -= 25
if state.execution_quality == "BLOCKED":
if htf_alignment == "AGAINST":
score -= 20
elif htf_alignment == "NEUTRAL":
score -= 8
if market_structure == "MIXED":
score -= 12
elif market_structure == "LH_LL" and state.position_side == "LONG":
score -= 18
elif market_structure == "HH_HL" and state.position_side == "SHORT":
score -= 18
if market_phase in {"RANGE", "SQUEEZE"}:
score -= 10
elif market_phase == "PULLBACK" and trend_alignment != "ALIGNED":
score -= 12
if trend_quality == "NOISY":
score -= 12
if volatility in MARKET_VOLATILITY_HIGH_STATES:
score -= 12
if state.execution_quality == EXECUTION_QUALITY_BLOCKED:
score -= 15
elif state.execution_quality == "WARNING":
elif state.execution_quality == EXECUTION_QUALITY_WARNING:
score -= 8
if state.market_runtime_degraded:
@@ -227,18 +302,18 @@ class AutoPositionHealthMixin:
# классифицировать health status по score
def _position_health_status(self, score: int | None) -> str:
if score is None:
return "UNKNOWN"
return POSITION_HEALTH_UNKNOWN
if score >= 80:
return "HEALTHY"
return POSITION_HEALTH_HEALTHY
if score >= 55:
return "WATCH"
if score >= 60:
return POSITION_HEALTH_WATCH
if score >= 35:
return "PRESSURE"
if score >= 40:
return POSITION_HEALTH_PRESSURE
return "DANGER"
return POSITION_HEALTH_DANGER
# сформировать человекочитаемую причину health состояния
def _position_health_reason(
@@ -275,26 +350,86 @@ class AutoPositionHealthMixin:
adverse_momentum: bool,
) -> tuple[str, str]:
percent = safe_float(pnl_percent)
stop_loss_percent = safe_float(getattr(state, "stop_loss_percent", None))
htf_alignment = str(getattr(state, "htf_alignment", "") or "").upper()
market_structure = str(getattr(state, "market_structure", "") or "").upper()
volatility = str(getattr(state, "market_volatility", "") or "").upper()
if state.execution_quality == "BLOCKED":
return "HIGH", "исполнение заблокировано"
current_interval_direction = str(
getattr(state, "current_interval_direction", "") or ""
).upper()
current_interval_change_percent = safe_float(
getattr(state, "current_interval_change_percent", None)
)
current_interval_move_abs = abs(current_interval_change_percent or 0.0)
if percent is not None and percent <= -1.0:
return "HIGH", "сильная просадка позиции"
current_interval_against_position = (
(
state.position_side == "LONG"
and current_interval_direction == "DOWN"
)
or (
state.position_side == "SHORT"
and current_interval_direction == "UP"
)
)
if state.execution_quality == EXECUTION_QUALITY_BLOCKED:
return POSITION_RISK_HIGH, "исполнение заблокировано"
if percent is not None:
if percent <= POSITION_HEALTH_PNL_HARD_LOSS_PERCENT:
return POSITION_RISK_HIGH, "сильная просадка позиции"
if stop_loss_percent is not None and stop_loss_percent > 0 and percent < 0:
loss_ratio = abs(percent) / stop_loss_percent
if loss_ratio >= POSITION_STOP_LOSS_RATIO_CRITICAL:
return POSITION_RISK_HIGH, "позиция близко к stop loss"
if loss_ratio >= POSITION_STOP_LOSS_RATIO_WARNING:
return POSITION_RISK_ELEVATED, "позиция прошла больше половины stop loss"
if trend_alignment == "AGAINST" and adverse_momentum:
return "HIGH", "рынок движется против позиции"
return POSITION_RISK_HIGH, "рынок движется против позиции"
if htf_alignment == "AGAINST" and adverse_momentum:
return POSITION_RISK_HIGH, "старший таймфрейм и momentum против позиции"
if (
state.position_side == "LONG"
and market_structure == "LH_LL"
):
return POSITION_RISK_ELEVATED, "структура рынка против LONG"
if (
state.position_side == "SHORT"
and market_structure == "HH_HL"
):
return POSITION_RISK_ELEVATED, "структура рынка против SHORT"
if volatility in MARKET_VOLATILITY_HIGH_STATES and percent is not None and percent < 0:
return POSITION_RISK_ELEVATED, "убыток в высокой волатильности"
if percent is not None and percent < 0:
if trend_alignment == "AGAINST" or adverse_momentum:
return "ELEVATED", "убыток усиливается рыночным контекстом"
return POSITION_RISK_ELEVATED, "убыток усиливается рыночным контекстом"
return "MODERATE", "позиция в минусе"
return POSITION_RISK_MODERATE, "позиция в минусе"
if current_interval_against_position and current_interval_move_abs >= POSITION_CURRENT_INTERVAL_RISK_MOVE_PERCENT:
return POSITION_RISK_ELEVATED, "текущая 5м свеча против позиции"
if current_interval_against_position and percent is not None and percent < 0:
return POSITION_RISK_ELEVATED, "убыток усиливается текущей 5м свечой"
if adverse_momentum:
return "MODERATE", "momentum против позиции"
return POSITION_RISK_MODERATE, "momentum против позиции"
return "LOW", "критичных рисков нет"
if htf_alignment == "AGAINST":
return POSITION_RISK_MODERATE, "старший таймфрейм против позиции"
return POSITION_RISK_LOW, "критичных рисков нет"
# определить давление на выход из позиции
def _position_exit_pressure(
@@ -305,14 +440,22 @@ class AutoPositionHealthMixin:
risk_level: str,
) -> str:
percent = safe_float(pnl_percent)
stop_loss_percent = safe_float(getattr(state, "stop_loss_percent", None))
if risk_level == "HIGH":
if risk_level == POSITION_RISK_HIGH:
return "HIGH"
if risk_level == "ELEVATED":
if risk_level in {POSITION_RISK_ELEVATED, POSITION_RISK_MODERATE}:
return "WATCH"
if percent is not None and percent <= -0.5:
return "WATCH"
if percent is not None:
if percent <= POSITION_EXIT_PRESSURE_LOSS_PERCENT:
return "WATCH"
if stop_loss_percent is not None and stop_loss_percent > 0 and percent < 0:
loss_ratio = abs(percent) / stop_loss_percent
if loss_ratio >= POSITION_STOP_LOSS_RATIO_WATCH:
return "WATCH"
return "LOW"