07.4.4.1.9.3 Market Phase Transition Fix
This commit is contained in:
@@ -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 "📈 Рынок · рост"
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user