07.4.4.1.12 — Position Health & Runtime Risk Layer
This commit is contained in:
@@ -526,6 +526,12 @@ class AutoTradeService:
|
||||
state.execution_quality = None
|
||||
state.execution_quality_reason = None
|
||||
state.execution_quality_message = None
|
||||
state.execution_price_source = None
|
||||
state.execution_price_age_seconds = None
|
||||
state.execution_bid_price = None
|
||||
state.execution_ask_price = None
|
||||
state.execution_last_price = None
|
||||
state.execution_price_freshness = None
|
||||
state.execution_confidence_score = None
|
||||
state.execution_confidence_level = None
|
||||
state.execution_confidence_required_score = self._execution_confidence_required_score
|
||||
@@ -579,6 +585,49 @@ class AutoTradeService:
|
||||
state.snapshot_age_seconds = None
|
||||
state.spread_percent = None
|
||||
|
||||
state.position_pnl_percent = None
|
||||
state.position_hold_seconds = None
|
||||
state.position_pressure = None
|
||||
state.position_health_score = None
|
||||
state.position_health_status = None
|
||||
state.position_health_reason = None
|
||||
state.position_risk_level = None
|
||||
state.position_risk_reason = None
|
||||
state.position_trend_alignment = None
|
||||
state.position_adverse_momentum = False
|
||||
state.position_exit_pressure = None
|
||||
|
||||
state.position_lifecycle_stage = None
|
||||
state.position_hold_quality = None
|
||||
state.position_decay_state = None
|
||||
state.position_exit_confidence = None
|
||||
state.position_exit_signal = None
|
||||
state.position_intelligence_reason = None
|
||||
state.position_recommended_action = None
|
||||
|
||||
state.position_peak_pnl_usd = None
|
||||
state.position_peak_pnl_percent = None
|
||||
state.position_mfe_percent = None
|
||||
state.position_mae_percent = None
|
||||
state.position_fatigue_score = None
|
||||
state.position_fatigue_state = None
|
||||
state.position_giveback_percent = None
|
||||
state.position_conviction_state = None
|
||||
state.position_exit_urgency = None
|
||||
state.position_reversal_risk = None
|
||||
|
||||
state.autonomous_action = None
|
||||
state.autonomous_action_reason = None
|
||||
state.autonomous_action_confidence = None
|
||||
state.autonomous_protection_required = False
|
||||
state.autonomous_reduce_required = False
|
||||
state.autonomous_exit_required = False
|
||||
state.autonomous_last_action = None
|
||||
state.autonomous_last_action_reason = None
|
||||
state.autonomous_last_action_at = None
|
||||
|
||||
state.last_loss_monotonic_at = None
|
||||
|
||||
# собрать контекст для стратегии
|
||||
def _build_strategy_context(self) -> StrategyContext:
|
||||
state = self.get_state()
|
||||
@@ -1394,6 +1443,11 @@ class AutoTradeService:
|
||||
is_fresh = bool(snapshot.get("is_fresh", False))
|
||||
source = str(snapshot.get("source") or "")
|
||||
|
||||
self._sync_execution_pricing_state(
|
||||
state,
|
||||
snapshot,
|
||||
)
|
||||
|
||||
state.snapshot_age_seconds = age_seconds
|
||||
state.spread_percent = self._spread_percent(
|
||||
bid_price=bid_price,
|
||||
@@ -1490,6 +1544,767 @@ class AutoTradeService:
|
||||
|
||||
return round((spread / mid_price) * 100, 5)
|
||||
|
||||
def _sync_execution_pricing_state(
|
||||
self,
|
||||
state: AutoTradeState,
|
||||
snapshot: JsonDict,
|
||||
) -> None:
|
||||
age_seconds = safe_float(snapshot.get("age_seconds"))
|
||||
|
||||
state.execution_price_source = str(snapshot.get("source") or "")
|
||||
state.execution_price_age_seconds = age_seconds
|
||||
state.execution_bid_price = safe_float(snapshot.get("bid_price"))
|
||||
state.execution_ask_price = safe_float(snapshot.get("ask_price"))
|
||||
state.execution_last_price = safe_float(snapshot.get("last_price"))
|
||||
|
||||
if age_seconds is None:
|
||||
state.execution_price_freshness = "UNKNOWN"
|
||||
elif age_seconds <= 1:
|
||||
state.execution_price_freshness = "FRESH"
|
||||
elif age_seconds <= self._warning_snapshot_age_seconds:
|
||||
state.execution_price_freshness = "AGING"
|
||||
else:
|
||||
state.execution_price_freshness = "STALE"
|
||||
|
||||
def _sync_position_health_state(self, state: AutoTradeState) -> None:
|
||||
if state.position_side == "NONE" or state.entry_price is None:
|
||||
state.position_pnl_percent = None
|
||||
state.position_hold_seconds = None
|
||||
state.position_pressure = None
|
||||
state.position_health_score = None
|
||||
state.position_health_status = None
|
||||
state.position_health_reason = None
|
||||
state.position_risk_level = None
|
||||
state.position_risk_reason = None
|
||||
state.position_trend_alignment = None
|
||||
state.position_adverse_momentum = False
|
||||
state.position_exit_pressure = None
|
||||
return
|
||||
|
||||
pnl_percent = self._position_pnl_percent(state)
|
||||
hold_seconds = self._position_hold_seconds(state)
|
||||
trend_alignment = self._position_trend_alignment(state)
|
||||
adverse_momentum = self._has_adverse_position_momentum(state)
|
||||
|
||||
pressure = self._position_pressure(
|
||||
state=state,
|
||||
pnl_percent=pnl_percent,
|
||||
)
|
||||
|
||||
health_score = self._position_health_score(
|
||||
state=state,
|
||||
pnl_percent=pnl_percent,
|
||||
trend_alignment=trend_alignment,
|
||||
adverse_momentum=adverse_momentum,
|
||||
)
|
||||
|
||||
risk_level, risk_reason = self._position_risk_level(
|
||||
state=state,
|
||||
pnl_percent=pnl_percent,
|
||||
trend_alignment=trend_alignment,
|
||||
adverse_momentum=adverse_momentum,
|
||||
)
|
||||
|
||||
state.position_pnl_percent = pnl_percent
|
||||
state.position_hold_seconds = hold_seconds
|
||||
state.position_pressure = pressure
|
||||
state.position_health_score = health_score
|
||||
state.position_health_status = self._position_health_status(health_score)
|
||||
state.position_health_reason = self._position_health_reason(
|
||||
pressure=pressure,
|
||||
trend_alignment=trend_alignment,
|
||||
adverse_momentum=adverse_momentum,
|
||||
)
|
||||
state.position_risk_level = risk_level
|
||||
state.position_risk_reason = risk_reason
|
||||
state.position_trend_alignment = trend_alignment
|
||||
state.position_adverse_momentum = adverse_momentum
|
||||
state.position_exit_pressure = self._position_exit_pressure(
|
||||
state=state,
|
||||
pnl_percent=pnl_percent,
|
||||
risk_level=risk_level,
|
||||
)
|
||||
|
||||
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 _position_pressure(
|
||||
self,
|
||||
*,
|
||||
state: AutoTradeState,
|
||||
pnl_percent: float | None,
|
||||
) -> str:
|
||||
pnl = safe_float(state.unrealized_pnl_usd) or 0.0
|
||||
|
||||
if pnl_percent is None:
|
||||
if pnl < 0:
|
||||
return "LOSS"
|
||||
if pnl > 0:
|
||||
return "PROFIT"
|
||||
return "FLAT"
|
||||
|
||||
if pnl_percent <= -0.8:
|
||||
return "HIGH_LOSS"
|
||||
|
||||
if pnl_percent <= -0.3:
|
||||
return "LOSS"
|
||||
|
||||
if pnl_percent >= 0.8:
|
||||
return "STRONG_PROFIT"
|
||||
|
||||
if pnl_percent >= 0.3:
|
||||
return "PROFIT"
|
||||
|
||||
return "FLAT"
|
||||
|
||||
def _position_trend_alignment(self, state: AutoTradeState) -> str:
|
||||
side = str(state.position_side or "NONE").upper()
|
||||
market_state = str(state.market_state or "").upper()
|
||||
trend = str(state.market_trend or "").upper()
|
||||
|
||||
if side == "NONE":
|
||||
return "NONE"
|
||||
|
||||
if side == "LONG":
|
||||
if market_state == "TREND_UP" or trend == "UP":
|
||||
return "ALIGNED"
|
||||
if market_state == "TREND_DOWN" or trend == "DOWN":
|
||||
return "AGAINST"
|
||||
|
||||
if side == "SHORT":
|
||||
if market_state == "TREND_DOWN" or trend == "DOWN":
|
||||
return "ALIGNED"
|
||||
if market_state == "TREND_UP" or trend == "UP":
|
||||
return "AGAINST"
|
||||
|
||||
return "NEUTRAL"
|
||||
|
||||
def _has_adverse_position_momentum(self, state: AutoTradeState) -> bool:
|
||||
side = str(state.position_side or "NONE").upper()
|
||||
momentum_direction = str(state.momentum_direction or "").upper()
|
||||
momentum_state = str(state.momentum_state or "").upper()
|
||||
|
||||
if side == "LONG":
|
||||
return (
|
||||
momentum_direction == "DOWN"
|
||||
or momentum_state in {"MOMENTUM_DOWN", "BREAKOUT_DOWN"}
|
||||
)
|
||||
|
||||
if side == "SHORT":
|
||||
return (
|
||||
momentum_direction == "UP"
|
||||
or momentum_state in {"MOMENTUM_UP", "BREAKOUT_UP"}
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
def _position_health_score(
|
||||
self,
|
||||
*,
|
||||
state: AutoTradeState,
|
||||
pnl_percent: float | None,
|
||||
trend_alignment: str,
|
||||
adverse_momentum: bool,
|
||||
) -> int:
|
||||
score = 100
|
||||
|
||||
if pnl_percent is not None:
|
||||
if pnl_percent <= -1.0:
|
||||
score -= 35
|
||||
elif pnl_percent <= -0.5:
|
||||
score -= 22
|
||||
elif pnl_percent < 0:
|
||||
score -= 10
|
||||
elif pnl_percent >= 0.8:
|
||||
score += 5
|
||||
|
||||
if trend_alignment == "AGAINST":
|
||||
score -= 25
|
||||
elif trend_alignment == "NEUTRAL":
|
||||
score -= 8
|
||||
|
||||
if adverse_momentum:
|
||||
score -= 20
|
||||
|
||||
if state.execution_quality == "BLOCKED":
|
||||
score -= 15
|
||||
elif state.execution_quality == "WARNING":
|
||||
score -= 8
|
||||
|
||||
if state.market_runtime_degraded:
|
||||
score -= 10
|
||||
|
||||
return max(0, min(100, score))
|
||||
|
||||
def _position_health_status(self, score: int | None) -> str:
|
||||
if score is None:
|
||||
return "UNKNOWN"
|
||||
|
||||
if score >= 80:
|
||||
return "HEALTHY"
|
||||
|
||||
if score >= 55:
|
||||
return "WATCH"
|
||||
|
||||
if score >= 35:
|
||||
return "PRESSURE"
|
||||
|
||||
return "DANGER"
|
||||
|
||||
def _position_health_reason(
|
||||
self,
|
||||
*,
|
||||
pressure: str,
|
||||
trend_alignment: str,
|
||||
adverse_momentum: bool,
|
||||
) -> str:
|
||||
if trend_alignment == "AGAINST" and adverse_momentum:
|
||||
return "тренд и momentum против позиции"
|
||||
|
||||
if trend_alignment == "AGAINST":
|
||||
return "тренд против позиции"
|
||||
|
||||
if adverse_momentum:
|
||||
return "momentum против позиции"
|
||||
|
||||
if pressure in {"HIGH_LOSS", "LOSS"}:
|
||||
return "позиция под давлением"
|
||||
|
||||
if pressure in {"PROFIT", "STRONG_PROFIT"}:
|
||||
return "позиция в прибыли"
|
||||
|
||||
return "позиция стабильна"
|
||||
|
||||
def _position_risk_level(
|
||||
self,
|
||||
*,
|
||||
state: AutoTradeState,
|
||||
pnl_percent: float | None,
|
||||
trend_alignment: str,
|
||||
adverse_momentum: bool,
|
||||
) -> tuple[str, str]:
|
||||
if state.execution_quality == "BLOCKED":
|
||||
return "HIGH", "исполнение заблокировано"
|
||||
|
||||
if pnl_percent is not None and pnl_percent <= -1.0:
|
||||
return "HIGH", "сильная просадка позиции"
|
||||
|
||||
if trend_alignment == "AGAINST" and adverse_momentum:
|
||||
return "HIGH", "рынок движется против позиции"
|
||||
|
||||
if pnl_percent is not None and pnl_percent < 0:
|
||||
if trend_alignment == "AGAINST" or adverse_momentum:
|
||||
return "ELEVATED", "убыток усиливается рыночным контекстом"
|
||||
|
||||
return "MODERATE", "позиция в минусе"
|
||||
|
||||
if adverse_momentum:
|
||||
return "MODERATE", "momentum против позиции"
|
||||
|
||||
return "LOW", "критичных рисков нет"
|
||||
|
||||
def _position_exit_pressure(
|
||||
self,
|
||||
*,
|
||||
state: AutoTradeState,
|
||||
pnl_percent: float | None,
|
||||
risk_level: str,
|
||||
) -> str:
|
||||
if risk_level == "HIGH":
|
||||
return "HIGH"
|
||||
|
||||
if risk_level == "ELEVATED":
|
||||
return "WATCH"
|
||||
|
||||
if pnl_percent is not None and pnl_percent <= -0.5:
|
||||
return "WATCH"
|
||||
|
||||
return "LOW"
|
||||
|
||||
def _sync_position_intelligence_state(self, state: AutoTradeState) -> None:
|
||||
if state.position_side == "NONE" or state.entry_price is None:
|
||||
state.position_lifecycle_stage = None
|
||||
state.position_hold_quality = None
|
||||
state.position_decay_state = None
|
||||
state.position_exit_confidence = None
|
||||
state.position_exit_signal = None
|
||||
state.position_intelligence_reason = None
|
||||
state.position_recommended_action = None
|
||||
state.position_peak_pnl_usd = None
|
||||
state.position_peak_pnl_percent = None
|
||||
state.position_mfe_percent = None
|
||||
state.position_mae_percent = None
|
||||
state.position_fatigue_score = None
|
||||
state.position_fatigue_state = None
|
||||
state.position_giveback_percent = None
|
||||
state.position_conviction_state = None
|
||||
state.position_exit_urgency = None
|
||||
state.position_reversal_risk = None
|
||||
return
|
||||
|
||||
lifecycle_stage = self._position_lifecycle_stage(state)
|
||||
hold_quality = self._position_hold_quality(state)
|
||||
decay_state = self._position_decay_state(state)
|
||||
|
||||
self._sync_advanced_position_analytics(
|
||||
state=state,
|
||||
lifecycle_stage=lifecycle_stage,
|
||||
hold_quality=hold_quality,
|
||||
decay_state=decay_state,
|
||||
)
|
||||
|
||||
exit_confidence = self._position_exit_confidence(
|
||||
state=state,
|
||||
hold_quality=hold_quality,
|
||||
decay_state=decay_state,
|
||||
)
|
||||
|
||||
exit_signal = self._position_exit_signal(exit_confidence)
|
||||
|
||||
state.position_lifecycle_stage = lifecycle_stage
|
||||
state.position_hold_quality = hold_quality
|
||||
state.position_decay_state = decay_state
|
||||
state.position_exit_confidence = exit_confidence
|
||||
state.position_exit_signal = exit_signal
|
||||
state.position_intelligence_reason = self._position_intelligence_reason(
|
||||
state=state,
|
||||
hold_quality=hold_quality,
|
||||
decay_state=decay_state,
|
||||
exit_signal=exit_signal,
|
||||
)
|
||||
state.position_recommended_action = self._position_recommended_action(
|
||||
exit_signal
|
||||
)
|
||||
|
||||
def _position_lifecycle_stage(self, state: AutoTradeState) -> str:
|
||||
hold_seconds = state.position_hold_seconds
|
||||
|
||||
if hold_seconds is None:
|
||||
return "UNKNOWN"
|
||||
|
||||
if hold_seconds < 60:
|
||||
return "NEW"
|
||||
|
||||
if hold_seconds < 300:
|
||||
return "ACTIVE"
|
||||
|
||||
if hold_seconds < 900:
|
||||
return "MATURE"
|
||||
|
||||
return "AGED"
|
||||
|
||||
def _position_hold_quality(self, state: AutoTradeState) -> str:
|
||||
health_status = str(state.position_health_status or "").upper()
|
||||
pressure = str(state.position_pressure or "").upper()
|
||||
trend_alignment = str(state.position_trend_alignment or "").upper()
|
||||
|
||||
if health_status == "DANGER":
|
||||
return "BAD"
|
||||
|
||||
if pressure == "HIGH_LOSS":
|
||||
return "BAD"
|
||||
|
||||
if trend_alignment == "AGAINST" and state.position_adverse_momentum:
|
||||
return "BAD"
|
||||
|
||||
if health_status == "PRESSURE":
|
||||
return "WEAK"
|
||||
|
||||
if pressure == "LOSS":
|
||||
return "WEAK"
|
||||
|
||||
if pressure in {"PROFIT", "STRONG_PROFIT"} and trend_alignment == "ALIGNED":
|
||||
return "GOOD"
|
||||
|
||||
if health_status == "HEALTHY":
|
||||
return "GOOD"
|
||||
|
||||
return "NEUTRAL"
|
||||
|
||||
def _position_decay_state(self, state: AutoTradeState) -> str:
|
||||
pressure = str(state.position_pressure or "").upper()
|
||||
trend_alignment = str(state.position_trend_alignment or "").upper()
|
||||
lifecycle = str(state.position_lifecycle_stage or "").upper()
|
||||
|
||||
if pressure in {"HIGH_LOSS", "LOSS"} and state.position_adverse_momentum:
|
||||
return "ACCELERATING_LOSS"
|
||||
|
||||
if trend_alignment == "AGAINST" and state.position_adverse_momentum:
|
||||
return "CONTEXT_DECAY"
|
||||
|
||||
if pressure == "PROFIT" and state.position_adverse_momentum:
|
||||
return "PROFIT_DECAY"
|
||||
|
||||
if lifecycle == "AGED" and pressure == "FLAT":
|
||||
return "TIME_DECAY"
|
||||
|
||||
return "NONE"
|
||||
|
||||
def _position_exit_confidence(
|
||||
self,
|
||||
*,
|
||||
state: AutoTradeState,
|
||||
hold_quality: str,
|
||||
decay_state: str,
|
||||
) -> float:
|
||||
score = 0.0
|
||||
|
||||
risk_level = str(state.position_risk_level or "").upper()
|
||||
exit_pressure = str(state.position_exit_pressure or "").upper()
|
||||
|
||||
if risk_level == "HIGH":
|
||||
score += 0.45
|
||||
elif risk_level == "ELEVATED":
|
||||
score += 0.30
|
||||
elif risk_level == "MODERATE":
|
||||
score += 0.15
|
||||
|
||||
if exit_pressure == "HIGH":
|
||||
score += 0.30
|
||||
elif exit_pressure == "WATCH":
|
||||
score += 0.15
|
||||
|
||||
if hold_quality == "BAD":
|
||||
score += 0.25
|
||||
elif hold_quality == "WEAK":
|
||||
score += 0.15
|
||||
|
||||
if decay_state in {"ACCELERATING_LOSS", "CONTEXT_DECAY"}:
|
||||
score += 0.25
|
||||
elif decay_state in {"PROFIT_DECAY", "TIME_DECAY"}:
|
||||
score += 0.15
|
||||
|
||||
if state.execution_quality == "BLOCKED":
|
||||
score += 0.10
|
||||
|
||||
return round(max(0.0, min(1.0, score)), 3)
|
||||
|
||||
def _position_exit_signal(self, exit_confidence: float | None) -> str:
|
||||
if exit_confidence is None:
|
||||
return "NONE"
|
||||
|
||||
if exit_confidence >= 0.75:
|
||||
return "EXIT"
|
||||
|
||||
if exit_confidence >= 0.50:
|
||||
return "REDUCE_OR_PROTECT"
|
||||
|
||||
if exit_confidence >= 0.30:
|
||||
return "WATCH"
|
||||
|
||||
return "HOLD"
|
||||
|
||||
def _position_intelligence_reason(
|
||||
self,
|
||||
*,
|
||||
state: AutoTradeState,
|
||||
hold_quality: str,
|
||||
decay_state: str,
|
||||
exit_signal: str,
|
||||
) -> str:
|
||||
if exit_signal == "EXIT":
|
||||
return "позиция требует выхода"
|
||||
|
||||
if exit_signal == "REDUCE_OR_PROTECT":
|
||||
return "позицию нужно защитить или уменьшить"
|
||||
|
||||
if decay_state != "NONE":
|
||||
return "качество удержания ухудшается"
|
||||
|
||||
if hold_quality == "GOOD":
|
||||
return "позицию можно удерживать"
|
||||
|
||||
if hold_quality == "WEAK":
|
||||
return "позиция требует наблюдения"
|
||||
|
||||
return "критичных признаков выхода нет"
|
||||
|
||||
def _position_recommended_action(self, exit_signal: str | None) -> str:
|
||||
if exit_signal == "EXIT":
|
||||
return "CLOSE"
|
||||
|
||||
if exit_signal == "REDUCE_OR_PROTECT":
|
||||
return "PROTECT"
|
||||
|
||||
if exit_signal == "WATCH":
|
||||
return "WATCH"
|
||||
|
||||
return "HOLD"
|
||||
|
||||
def _sync_advanced_position_analytics(
|
||||
self,
|
||||
*,
|
||||
state: AutoTradeState,
|
||||
lifecycle_stage: str,
|
||||
hold_quality: str,
|
||||
decay_state: str,
|
||||
) -> None:
|
||||
pnl = safe_float(state.unrealized_pnl_usd)
|
||||
pnl_percent = safe_float(state.position_pnl_percent)
|
||||
|
||||
peak_pnl = safe_float(state.position_peak_pnl_usd)
|
||||
peak_pnl_percent = safe_float(state.position_peak_pnl_percent)
|
||||
|
||||
if pnl is not None:
|
||||
if peak_pnl is None or pnl > peak_pnl:
|
||||
state.position_peak_pnl_usd = pnl
|
||||
|
||||
if pnl_percent is not None:
|
||||
if peak_pnl_percent is None or pnl_percent > peak_pnl_percent:
|
||||
state.position_peak_pnl_percent = pnl_percent
|
||||
|
||||
state.position_mfe_percent = self._position_mfe_percent(state)
|
||||
state.position_mae_percent = self._position_mae_percent(state)
|
||||
state.position_giveback_percent = self._position_giveback_percent(state)
|
||||
|
||||
fatigue_score = self._position_fatigue_score(
|
||||
state=state,
|
||||
lifecycle_stage=lifecycle_stage,
|
||||
hold_quality=hold_quality,
|
||||
decay_state=decay_state,
|
||||
)
|
||||
|
||||
state.position_fatigue_score = fatigue_score
|
||||
state.position_fatigue_state = self._position_fatigue_state(fatigue_score)
|
||||
state.position_conviction_state = self._position_conviction_state(state)
|
||||
state.position_exit_urgency = self._position_exit_urgency(state)
|
||||
state.position_reversal_risk = self._position_reversal_risk(state)
|
||||
|
||||
def _position_mfe_percent(self, state: AutoTradeState) -> float | None:
|
||||
peak = safe_float(state.position_peak_pnl_percent)
|
||||
|
||||
if peak is None:
|
||||
return None
|
||||
|
||||
return round(max(0.0, peak), 4)
|
||||
|
||||
def _position_mae_percent(self, state: AutoTradeState) -> float | None:
|
||||
current = safe_float(state.position_pnl_percent)
|
||||
|
||||
if current is None:
|
||||
return None
|
||||
|
||||
return round(min(0.0, current), 4)
|
||||
|
||||
def _position_giveback_percent(self, state: AutoTradeState) -> float | None:
|
||||
peak = safe_float(state.position_peak_pnl_percent)
|
||||
current = safe_float(state.position_pnl_percent)
|
||||
|
||||
if peak is None or current is None:
|
||||
return None
|
||||
|
||||
if peak <= 0:
|
||||
return 0.0
|
||||
|
||||
giveback = peak - current
|
||||
|
||||
if giveback <= 0:
|
||||
return 0.0
|
||||
|
||||
return round((giveback / peak) * 100, 2)
|
||||
|
||||
def _position_fatigue_score(
|
||||
self,
|
||||
*,
|
||||
state: AutoTradeState,
|
||||
lifecycle_stage: str,
|
||||
hold_quality: str,
|
||||
decay_state: str,
|
||||
) -> float:
|
||||
score = 0.0
|
||||
|
||||
giveback = safe_float(state.position_giveback_percent) or 0.0
|
||||
hold_seconds = safe_float(state.position_hold_seconds) or 0.0
|
||||
|
||||
if lifecycle_stage == "AGED":
|
||||
score += 0.25
|
||||
elif lifecycle_stage == "MATURE":
|
||||
score += 0.15
|
||||
|
||||
if hold_quality == "BAD":
|
||||
score += 0.30
|
||||
elif hold_quality == "WEAK":
|
||||
score += 0.18
|
||||
|
||||
if decay_state in {"ACCELERATING_LOSS", "CONTEXT_DECAY"}:
|
||||
score += 0.30
|
||||
elif decay_state in {"PROFIT_DECAY", "TIME_DECAY"}:
|
||||
score += 0.18
|
||||
|
||||
if giveback >= 70:
|
||||
score += 0.30
|
||||
elif giveback >= 45:
|
||||
score += 0.20
|
||||
elif giveback >= 25:
|
||||
score += 0.10
|
||||
|
||||
if hold_seconds >= 1800:
|
||||
score += 0.15
|
||||
elif hold_seconds >= 900:
|
||||
score += 0.08
|
||||
|
||||
if state.position_adverse_momentum:
|
||||
score += 0.15
|
||||
|
||||
return round(max(0.0, min(1.0, score)), 3)
|
||||
|
||||
def _position_fatigue_state(self, score: float | None) -> str:
|
||||
value = safe_float(score)
|
||||
|
||||
if value is None:
|
||||
return "UNKNOWN"
|
||||
|
||||
if value >= 0.75:
|
||||
return "EXHAUSTED"
|
||||
|
||||
if value >= 0.50:
|
||||
return "TIRED"
|
||||
|
||||
if value >= 0.25:
|
||||
return "WATCH"
|
||||
|
||||
return "FRESH"
|
||||
|
||||
def _position_conviction_state(self, state: AutoTradeState) -> str:
|
||||
health = str(state.position_health_status or "").upper()
|
||||
fatigue = str(state.position_fatigue_state or "").upper()
|
||||
alignment = str(state.position_trend_alignment or "").upper()
|
||||
|
||||
if health == "DANGER" or fatigue == "EXHAUSTED":
|
||||
return "BROKEN"
|
||||
|
||||
if alignment == "AGAINST" or fatigue == "TIRED":
|
||||
return "WEAKENING"
|
||||
|
||||
if health == "HEALTHY" and alignment == "ALIGNED":
|
||||
return "STRONG"
|
||||
|
||||
return "NEUTRAL"
|
||||
|
||||
def _position_exit_urgency(self, state: AutoTradeState) -> str:
|
||||
exit_signal = str(state.position_exit_signal or "").upper()
|
||||
fatigue = str(state.position_fatigue_state or "").upper()
|
||||
risk = str(state.position_risk_level or "").upper()
|
||||
|
||||
if exit_signal == "EXIT" or risk == "HIGH":
|
||||
return "IMMEDIATE"
|
||||
|
||||
if fatigue == "EXHAUSTED":
|
||||
return "HIGH"
|
||||
|
||||
if exit_signal == "REDUCE_OR_PROTECT" or fatigue == "TIRED":
|
||||
return "MEDIUM"
|
||||
|
||||
if exit_signal == "WATCH":
|
||||
return "LOW"
|
||||
|
||||
return "NONE"
|
||||
|
||||
def _position_reversal_risk(self, state: AutoTradeState) -> str:
|
||||
giveback = safe_float(state.position_giveback_percent) or 0.0
|
||||
fatigue = str(state.position_fatigue_state or "").upper()
|
||||
adverse = bool(state.position_adverse_momentum)
|
||||
|
||||
if adverse and giveback >= 45:
|
||||
return "HIGH"
|
||||
|
||||
if fatigue in {"TIRED", "EXHAUSTED"} and giveback >= 25:
|
||||
return "ELEVATED"
|
||||
|
||||
if adverse:
|
||||
return "MODERATE"
|
||||
|
||||
return "LOW"
|
||||
|
||||
def _sync_autonomous_trade_management(
|
||||
self,
|
||||
state: AutoTradeState,
|
||||
) -> None:
|
||||
if state.position_side == "NONE":
|
||||
state.autonomous_action = None
|
||||
state.autonomous_action_reason = None
|
||||
state.autonomous_action_confidence = None
|
||||
state.autonomous_protection_required = False
|
||||
state.autonomous_reduce_required = False
|
||||
state.autonomous_exit_required = False
|
||||
return
|
||||
|
||||
exit_signal = str(state.position_exit_signal or "HOLD").upper()
|
||||
exit_confidence = safe_float(state.position_exit_confidence) or 0.0
|
||||
|
||||
action = "HOLD"
|
||||
reason = "позиция удерживается"
|
||||
|
||||
protect_required = False
|
||||
reduce_required = False
|
||||
exit_required = False
|
||||
|
||||
if exit_signal == "WATCH":
|
||||
action = "WATCH"
|
||||
reason = "позиция требует наблюдения"
|
||||
|
||||
elif exit_signal == "REDUCE_OR_PROTECT":
|
||||
if state.position_pressure in {"HIGH_LOSS", "LOSS"}:
|
||||
action = "REDUCE"
|
||||
reduce_required = True
|
||||
reason = "позиция должна быть уменьшена"
|
||||
else:
|
||||
action = "PROTECT"
|
||||
protect_required = True
|
||||
reason = "позиция требует защиты"
|
||||
|
||||
elif exit_signal == "EXIT":
|
||||
action = "EXIT"
|
||||
exit_required = True
|
||||
reason = "позиция требует закрытия"
|
||||
|
||||
if (
|
||||
state.position_adverse_momentum
|
||||
and state.position_trend_alignment == "AGAINST"
|
||||
and exit_confidence >= 0.65
|
||||
):
|
||||
action = "EXIT"
|
||||
exit_required = True
|
||||
reason = "рынок агрессивно движется против позиции"
|
||||
|
||||
state.autonomous_action = action
|
||||
state.autonomous_action_reason = reason
|
||||
state.autonomous_action_confidence = exit_confidence
|
||||
state.autonomous_protection_required = protect_required
|
||||
state.autonomous_reduce_required = reduce_required
|
||||
state.autonomous_exit_required = exit_required
|
||||
|
||||
def _log_execution_quality_if_changed(
|
||||
self,
|
||||
*,
|
||||
@@ -1831,6 +2646,13 @@ class AutoTradeService:
|
||||
if state.execution_quality != "BLOCKED":
|
||||
ExecutionEngine().process(state)
|
||||
|
||||
self._sync_position_health_state(state)
|
||||
self._sync_position_intelligence_state(state)
|
||||
self._sync_autonomous_trade_management(state)
|
||||
|
||||
if state.execution_quality != "BLOCKED":
|
||||
ExecutionEngine().process_runtime_action(state)
|
||||
|
||||
self._sync_execution_semantic_state(state)
|
||||
|
||||
return state
|
||||
Reference in New Issue
Block a user