07.4.4.1.9.3 Market Phase Transition Fix

This commit is contained in:
2026-05-12 11:23:13 +03:00
parent fc50cadabf
commit 0dbb609b5a
7 changed files with 479 additions and 33 deletions

View File

@@ -264,6 +264,7 @@ def _market_semantic_line(state) -> str:
strength = getattr(state, "market_trend_strength", None)
quality = getattr(state, "market_trend_quality", None)
phase = getattr(state, "market_phase", None)
phase_direction = getattr(state, "market_phase_direction", None)
if market_state in {None, "UNKNOWN"}:
return "⏳ Рынок · анализ"
@@ -278,12 +279,18 @@ def _market_semantic_line(state) -> str:
return "🟰 Рынок · флэт"
if phase == "PULLBACK":
if trend == "UP":
if trend == "UP" and phase_direction == "DOWN":
return "↘️ Рынок · коррекция"
if trend == "DOWN":
if trend == "DOWN" and phase_direction == "UP":
return "↗️ Рынок · откат вверх"
if trend == "UP":
return "📈 Рынок · рост"
if trend == "DOWN":
return "📉 Рынок · снижение"
return "↔️ Рынок · откат"
if quality == "NOISY":
@@ -306,10 +313,10 @@ def _market_semantic_line(state) -> str:
if phase == "IMPULSE":
if trend == "UP" and strength == "STRONG":
return "⚡ Рынок · сильный рост"
return " Рынок · сильный рост"
if trend == "DOWN" and strength == "STRONG":
return "⚡ Рынок · сильное снижение"
return " Рынок · сильное снижение"
if trend == "UP":
return "📈 Рынок · рост"

View File

@@ -76,4 +76,8 @@ class MarketAnalysisResult:
trend_quality: TrendQuality
market_phase: MarketPhase
trend_gap_percent: float | None
trend_consistency: float | None
trend_consistency: float | None
phase_direction: TrendDirection
phase_change_percent: float | None
phase_reason: str | None

View File

@@ -1,3 +1,5 @@
# app/src/trading/market_analysis/service.py
from __future__ import annotations
from src.integrations.exchange.service import ExchangeService
@@ -18,14 +20,13 @@ class MarketAnalysisService:
_slow_ema_period = 50
_atr_period = 14
_rsi_period = 14
_min_candles = 60
_low_volatility_atr_percent = 0.05
_high_volatility_atr_percent = 1.8
_trend_gap_percent = 0.03
_trend_consistency_window = 20
_phase_window = 5
_phase_direction_threshold_percent = 0.03
def analyze(
self,
@@ -95,12 +96,20 @@ class MarketAnalysisService:
trend=trend,
)
trend_quality = self._classify_trend_quality(trend_consistency)
market_phase = self._classify_market_phase(
phase_change_percent = self._recent_change_percent(
closes=closes,
window=self._phase_window,
)
phase_direction = self._classify_phase_direction(phase_change_percent)
market_phase, phase_reason = self._classify_market_phase(
trend=trend,
volatility=volatility,
trend_strength=trend_strength,
trend_quality=trend_quality,
rsi_value=rsi_value,
phase_direction=phase_direction,
)
state = self._classify_market_state(
@@ -145,6 +154,11 @@ class MarketAnalysisService:
"market_trend_strength": trend_strength.value,
"market_trend_quality": trend_quality.value,
"market_phase": market_phase.value,
"market_phase_direction": phase_direction.value,
"market_phase_change_percent": round(phase_change_percent, 5)
if phase_change_percent is not None
else None,
"market_phase_reason": phase_reason,
"market_trend_gap_percent": round(trend_gap_percent, 5)
if trend_gap_percent is not None
else None,
@@ -169,6 +183,9 @@ class MarketAnalysisService:
market_phase=market_phase,
trend_gap_percent=trend_gap_percent,
trend_consistency=trend_consistency,
phase_direction=phase_direction,
phase_change_percent=phase_change_percent,
phase_reason=phase_reason,
)
def _trend_gap_percent_value(
@@ -265,6 +282,52 @@ class MarketAnalysisService:
return TrendQuality.NOISY
def _recent_change_percent(
self,
*,
closes: list[float],
window: int,
) -> float | None:
if window <= 0 or len(closes) < window + 1:
return None
first_price = closes[-(window + 1)]
last_price = closes[-1]
if first_price <= 0:
return None
return ((last_price - first_price) / first_price) * 100
def _classify_phase_direction(
self,
change_percent: float | None,
) -> TrendDirection:
if change_percent is None:
return TrendDirection.UNKNOWN
if change_percent >= self._phase_direction_threshold_percent:
return TrendDirection.UP
if change_percent <= -self._phase_direction_threshold_percent:
return TrendDirection.DOWN
return TrendDirection.FLAT
def _is_counter_trend_move(
self,
*,
trend: TrendDirection,
phase_direction: TrendDirection,
) -> bool:
if trend == TrendDirection.UP:
return phase_direction == TrendDirection.DOWN
if trend == TrendDirection.DOWN:
return phase_direction == TrendDirection.UP
return False
def _classify_market_phase(
self,
*,
@@ -273,29 +336,43 @@ class MarketAnalysisService:
trend_strength: TrendStrength,
trend_quality: TrendQuality,
rsi_value: float | None,
) -> MarketPhase:
phase_direction: TrendDirection,
) -> tuple[MarketPhase, str]:
if volatility == VolatilityState.LOW:
return MarketPhase.SQUEEZE
return MarketPhase.SQUEEZE, "LOW_VOLATILITY_SQUEEZE"
if trend == TrendDirection.FLAT:
return MarketPhase.RANGE
return MarketPhase.RANGE, "FLAT_TREND_RANGE"
if trend not in {TrendDirection.UP, TrendDirection.DOWN}:
return MarketPhase.UNKNOWN
return MarketPhase.UNKNOWN, "UNKNOWN_TREND"
if trend_strength == TrendStrength.WEAK:
return MarketPhase.RANGE
return MarketPhase.RANGE, "WEAK_TREND_RANGE"
if trend_quality == TrendQuality.NOISY:
return MarketPhase.PULLBACK
if self._is_counter_trend_move(
trend=trend,
phase_direction=phase_direction,
):
return MarketPhase.PULLBACK, "COUNTER_TREND_MOVE"
if trend == TrendDirection.UP and rsi_value is not None and rsi_value < 45:
return MarketPhase.PULLBACK
if (
trend == TrendDirection.UP
and rsi_value is not None
and rsi_value < 45
and phase_direction == TrendDirection.DOWN
):
return MarketPhase.PULLBACK, "UPTREND_RSI_PULLBACK_CONFIRMED_BY_PRICE"
if trend == TrendDirection.DOWN and rsi_value is not None and rsi_value > 55:
return MarketPhase.PULLBACK
if (
trend == TrendDirection.DOWN
and rsi_value is not None
and rsi_value > 55
and phase_direction == TrendDirection.UP
):
return MarketPhase.PULLBACK, "DOWNTREND_RSI_PULLBACK_CONFIRMED_BY_PRICE"
return MarketPhase.IMPULSE
return MarketPhase.IMPULSE, "WITH_TREND_OR_NEUTRAL_MOVE"
def _classify_volatility(self, atr_percent: float) -> VolatilityState:
if atr_percent <= 0:
@@ -392,6 +469,9 @@ class MarketAnalysisService:
"market_trend_strength": TrendStrength.UNKNOWN.value,
"market_trend_quality": TrendQuality.UNKNOWN.value,
"market_phase": MarketPhase.UNKNOWN.value,
"market_phase_direction": TrendDirection.UNKNOWN.value,
"market_phase_change_percent": None,
"market_phase_reason": reason,
"market_trend_gap_percent": None,
"market_trend_consistency": None,
"candles_count": candles_count,
@@ -403,4 +483,7 @@ class MarketAnalysisService:
market_phase=MarketPhase.UNKNOWN,
trend_gap_percent=None,
trend_consistency=None,
phase_direction=TrendDirection.UNKNOWN,
phase_change_percent=None,
phase_reason=reason,
)

View File

@@ -124,6 +124,9 @@ class TrendStrategy:
"market_trend_strength": market.trend_strength.value,
"market_trend_quality": market.trend_quality.value,
"market_phase": market.market_phase.value,
"market_phase_direction": market.phase_direction.value,
"market_phase_change_percent": market.phase_change_percent,
"market_phase_reason": market.phase_reason,
"market_trend_gap_percent": market.trend_gap_percent,
"market_trend_consistency": market.trend_consistency,
"runtime_window_ttl_seconds": self._window_ttl_seconds,
@@ -155,18 +158,6 @@ class TrendStrategy:
},
)
if market.trend_quality == TrendQuality.NOISY:
return SignalResult(
signal=SignalType.HOLD,
reason="TREND есть, но движение шумное.",
confidence=0.0,
payload={
**base_payload,
"entry_block_reason": "NOISY_MARKET_TREND",
"entry_block_message": "шумный тренд",
},
)
if market.market_phase == MarketPhase.PULLBACK:
return SignalResult(
signal=SignalType.HOLD,
@@ -179,6 +170,18 @@ class TrendStrategy:
},
)
if market.trend_quality == TrendQuality.NOISY:
return SignalResult(
signal=SignalType.HOLD,
reason="TREND есть, но движение шумное.",
confidence=0.0,
payload={
**base_payload,
"entry_block_reason": "NOISY_MARKET_TREND",
"entry_block_message": "шумный тренд",
},
)
if len(prices) < self._window_size:
return SignalResult(
signal=SignalType.HOLD,