diff --git a/app/src/telegram/handlers/auto/ui.py b/app/src/telegram/handlers/auto/ui.py index 440a13f..a569f08 100644 --- a/app/src/telegram/handlers/auto/ui.py +++ b/app/src/telegram/handlers/auto/ui.py @@ -158,6 +158,7 @@ def _build_waiting_text(state) -> str: signal_lines = [ _signal_line(state), _market_state_line(state), + _market_diagnostics_line(state), _entry_block_line(state), _execution_quality_line(state), *_signal_confidence_lines(state), @@ -261,6 +262,49 @@ def _market_state_line(state) -> str: return labels.get(market_state, "⏳ Рынок · Идёт анализ") +def _market_diagnostics_line(state) -> str: + strength = getattr(state, "market_trend_strength", None) + quality = getattr(state, "market_trend_quality", None) + phase = getattr(state, "market_phase", None) + + if not strength and not quality and not phase: + return "" + + strength_labels = { + "WEAK": "слабый", + "NORMAL": "нормальный", + "STRONG": "сильный", + } + + quality_labels = { + "CLEAN": "чистый", + "NOISY": "шумный", + } + + phase_labels = { + "IMPULSE": "импульс", + "PULLBACK": "откат", + "RANGE": "флэт", + "SQUEEZE": "сжатие", + } + + parts = [] + + if strength in strength_labels: + parts.append(strength_labels[strength]) + + if quality in quality_labels: + parts.append(quality_labels[quality]) + + if phase in phase_labels: + parts.append(phase_labels[phase]) + + if not parts: + return "" + + return f"Анализ · {' · '.join(parts)}" + + def _compact_entry_block_message(message: str) -> str: normalized = message.strip().lower() diff --git a/app/src/trading/auto/service.py b/app/src/trading/auto/service.py index f8d8056..3f05f55 100644 --- a/app/src/trading/auto/service.py +++ b/app/src/trading/auto/service.py @@ -378,6 +378,9 @@ class AutoTradeService: state.execution_quality_reason = None state.execution_quality_message = None state.market_runtime_degraded = False + state.market_trend_strength = None + state.market_trend_quality = None + state.market_phase = None # собрать контекст для стратегии def _build_strategy_context(self) -> StrategyContext: @@ -746,6 +749,9 @@ class AutoTradeService: state.market_state = payload.get("market_state") state.market_trend = payload.get("market_trend") state.market_volatility = payload.get("market_volatility") + state.market_trend_strength = payload.get("market_trend_strength") + state.market_trend_quality = payload.get("market_trend_quality") + state.market_phase = payload.get("market_phase") state.market_analysis_interval = payload.get("market_analysis_interval") state.market_analysis_reason = payload.get("market_analysis_reason") state.market_analysis_updated_at = time.monotonic() diff --git a/app/src/trading/auto/state.py b/app/src/trading/auto/state.py index d7c62d2..d40fcfe 100644 --- a/app/src/trading/auto/state.py +++ b/app/src/trading/auto/state.py @@ -115,6 +115,15 @@ class AutoTradeState: # волатильность: LOW / NORMAL / HIGH / UNKNOWN market_volatility: str | None = None + # сила тренда: WEAK / NORMAL / STRONG / UNKNOWN + market_trend_strength: str | None = None + + # качество тренда: CLEAN / NOISY / UNKNOWN + market_trend_quality: str | None = None + + # фаза рынка: IMPULSE / PULLBACK / RANGE / SQUEEZE / UNKNOWN + market_phase: str | None = None + # таймфрейм анализа рынка market_analysis_interval: str | None = None diff --git a/app/src/trading/market_analysis/models.py b/app/src/trading/market_analysis/models.py index 6a92928..4ec12a8 100644 --- a/app/src/trading/market_analysis/models.py +++ b/app/src/trading/market_analysis/models.py @@ -29,6 +29,27 @@ class VolatilityState(StrEnum): UNKNOWN = "UNKNOWN" +class TrendStrength(StrEnum): + WEAK = "WEAK" + NORMAL = "NORMAL" + STRONG = "STRONG" + UNKNOWN = "UNKNOWN" + + +class TrendQuality(StrEnum): + CLEAN = "CLEAN" + NOISY = "NOISY" + UNKNOWN = "UNKNOWN" + + +class MarketPhase(StrEnum): + IMPULSE = "IMPULSE" + PULLBACK = "PULLBACK" + RANGE = "RANGE" + SQUEEZE = "SQUEEZE" + UNKNOWN = "UNKNOWN" + + @dataclass(slots=True) class MarketAnalysisResult: symbol: str @@ -49,4 +70,10 @@ class MarketAnalysisResult: reason: str is_trade_allowed: bool - payload: dict \ No newline at end of file + payload: dict + + trend_strength: TrendStrength + trend_quality: TrendQuality + market_phase: MarketPhase + trend_gap_percent: float | None + trend_consistency: float | None \ No newline at end of file diff --git a/app/src/trading/market_analysis/service.py b/app/src/trading/market_analysis/service.py index 311765b..2bd4e01 100644 --- a/app/src/trading/market_analysis/service.py +++ b/app/src/trading/market_analysis/service.py @@ -1,13 +1,14 @@ -# app/src/trading/market_analysis/service.py - from __future__ import annotations from src.integrations.exchange.service import ExchangeService from src.trading.market_analysis.indicators import atr, ema, rsi from src.trading.market_analysis.models import ( MarketAnalysisResult, + MarketPhase, MarketState, TrendDirection, + TrendQuality, + TrendStrength, VolatilityState, ) @@ -24,6 +25,7 @@ class MarketAnalysisService: _high_volatility_atr_percent = 1.8 _trend_gap_percent = 0.03 + _trend_consistency_window = 20 def analyze( self, @@ -77,12 +79,29 @@ class MarketAnalysisService: ) atr_percent = (atr_value / close_price) * 100 + trend_gap_percent = self._trend_gap_percent_value( + ema_fast=ema_fast, + ema_slow=ema_slow, + ) volatility = self._classify_volatility(atr_percent) trend = self._classify_trend( ema_fast=ema_fast, ema_slow=ema_slow, ) + trend_strength = self._classify_trend_strength(trend_gap_percent) + trend_consistency = self._trend_consistency( + closes=closes, + trend=trend, + ) + trend_quality = self._classify_trend_quality(trend_consistency) + market_phase = self._classify_market_phase( + trend=trend, + volatility=volatility, + trend_strength=trend_strength, + trend_quality=trend_quality, + rsi_value=rsi_value, + ) state = self._classify_market_state( trend=trend, @@ -123,6 +142,15 @@ class MarketAnalysisService: "market_state": state.value, "trend": trend.value, "volatility": volatility.value, + "market_trend_strength": trend_strength.value, + "market_trend_quality": trend_quality.value, + "market_phase": market_phase.value, + "market_trend_gap_percent": round(trend_gap_percent, 5) + if trend_gap_percent is not None + else None, + "market_trend_consistency": round(trend_consistency, 3) + if trend_consistency is not None + else None, "close_price": close_price, "ema_fast_period": self._fast_ema_period, "ema_slow_period": self._slow_ema_period, @@ -136,18 +164,37 @@ class MarketAnalysisService: "candles_count": len(candles), "is_trade_allowed": is_trade_allowed, }, + trend_strength=trend_strength, + trend_quality=trend_quality, + market_phase=market_phase, + trend_gap_percent=trend_gap_percent, + trend_consistency=trend_consistency, ) + def _trend_gap_percent_value( + self, + *, + ema_fast: float, + ema_slow: float, + ) -> float | None: + if ema_slow <= 0: + return None + + return ((ema_fast - ema_slow) / ema_slow) * 100 + def _classify_trend( self, *, ema_fast: float, ema_slow: float, ) -> TrendDirection: - if ema_slow <= 0: - return TrendDirection.UNKNOWN + gap_percent = self._trend_gap_percent_value( + ema_fast=ema_fast, + ema_slow=ema_slow, + ) - gap_percent = ((ema_fast - ema_slow) / ema_slow) * 100 + if gap_percent is None: + return TrendDirection.UNKNOWN if gap_percent >= self._trend_gap_percent: return TrendDirection.UP @@ -157,6 +204,99 @@ class MarketAnalysisService: return TrendDirection.FLAT + def _classify_trend_strength( + self, + trend_gap_percent: float | None, + ) -> TrendStrength: + if trend_gap_percent is None: + return TrendStrength.UNKNOWN + + gap = abs(trend_gap_percent) + + if gap < 0.08: + return TrendStrength.WEAK + + if gap < 0.25: + return TrendStrength.NORMAL + + return TrendStrength.STRONG + + def _trend_consistency( + self, + *, + closes: list[float], + trend: TrendDirection, + ) -> float | None: + if len(closes) < 2: + return None + + window = closes[-self._trend_consistency_window :] + if len(window) < 2: + return None + + up_moves = 0 + down_moves = 0 + + for previous_price, current_price in zip(window, window[1:]): + if current_price > previous_price: + up_moves += 1 + elif current_price < previous_price: + down_moves += 1 + + total_moves = max(1, len(window) - 1) + + if trend == TrendDirection.UP: + return up_moves / total_moves + + if trend == TrendDirection.DOWN: + return down_moves / total_moves + + return None + + def _classify_trend_quality( + self, + trend_consistency: float | None, + ) -> TrendQuality: + if trend_consistency is None: + return TrendQuality.UNKNOWN + + if trend_consistency >= 0.6: + return TrendQuality.CLEAN + + return TrendQuality.NOISY + + def _classify_market_phase( + self, + *, + trend: TrendDirection, + volatility: VolatilityState, + trend_strength: TrendStrength, + trend_quality: TrendQuality, + rsi_value: float | None, + ) -> MarketPhase: + if volatility == VolatilityState.LOW: + return MarketPhase.SQUEEZE + + if trend == TrendDirection.FLAT: + return MarketPhase.RANGE + + if trend not in {TrendDirection.UP, TrendDirection.DOWN}: + return MarketPhase.UNKNOWN + + if trend_strength == TrendStrength.WEAK: + return MarketPhase.RANGE + + if trend_quality == TrendQuality.NOISY: + return MarketPhase.PULLBACK + + if trend == TrendDirection.UP and rsi_value is not None and rsi_value < 45: + return MarketPhase.PULLBACK + + if trend == TrendDirection.DOWN and rsi_value is not None and rsi_value > 55: + return MarketPhase.PULLBACK + + return MarketPhase.IMPULSE + def _classify_volatility(self, atr_percent: float) -> VolatilityState: if atr_percent <= 0: return VolatilityState.UNKNOWN @@ -249,8 +389,18 @@ class MarketAnalysisService: "market_state": MarketState.UNKNOWN.value, "trend": TrendDirection.UNKNOWN.value, "volatility": VolatilityState.UNKNOWN.value, + "market_trend_strength": TrendStrength.UNKNOWN.value, + "market_trend_quality": TrendQuality.UNKNOWN.value, + "market_phase": MarketPhase.UNKNOWN.value, + "market_trend_gap_percent": None, + "market_trend_consistency": None, "candles_count": candles_count, "is_trade_allowed": False, "reason": reason, }, + trend_strength=TrendStrength.UNKNOWN, + trend_quality=TrendQuality.UNKNOWN, + market_phase=MarketPhase.UNKNOWN, + trend_gap_percent=None, + trend_consistency=None, ) \ No newline at end of file diff --git a/app/src/trading/strategies/trend.py b/app/src/trading/strategies/trend.py index ca8db06..fb58159 100644 --- a/app/src/trading/strategies/trend.py +++ b/app/src/trading/strategies/trend.py @@ -5,7 +5,12 @@ from __future__ import annotations import time from src.integrations.exchange.service import ExchangeService -from src.trading.market_analysis.models import MarketState +from src.trading.market_analysis.models import ( + MarketPhase, + MarketState, + TrendQuality, + TrendStrength, +) from src.trading.market_analysis.service import MarketAnalysisService from src.trading.strategies.base import StrategyContext from src.trading.strategies.signals import SignalResult, SignalType @@ -116,6 +121,11 @@ class TrendStrategy: "market_analysis_interval": market.interval, "market_analysis_reason": market.reason, "market_analysis": market.payload, + "market_trend_strength": market.trend_strength.value, + "market_trend_quality": market.trend_quality.value, + "market_phase": market.market_phase.value, + "market_trend_gap_percent": market.trend_gap_percent, + "market_trend_consistency": market.trend_consistency, "runtime_window_ttl_seconds": self._window_ttl_seconds, "runtime_window_size": len(prices), } @@ -133,6 +143,42 @@ class TrendStrategy: }, ) + if market.trend_strength == TrendStrength.WEAK: + return SignalResult( + signal=SignalType.HOLD, + reason="TREND есть, но сила тренда слабая.", + confidence=0.0, + payload={ + **base_payload, + "entry_block_reason": "WEAK_MARKET_TREND", + "entry_block_message": "слабый тренд", + }, + ) + + 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, + reason="TREND есть, но рынок находится в откате.", + confidence=0.0, + payload={ + **base_payload, + "entry_block_reason": "MARKET_PULLBACK", + "entry_block_message": "откат", + }, + ) + if len(prices) < self._window_size: return SignalResult( signal=SignalType.HOLD, diff --git a/docs/roadmap/master-roadmap.md b/docs/roadmap/master-roadmap.md index 49447e7..39b768e 100644 --- a/docs/roadmap/master-roadmap.md +++ b/docs/roadmap/master-roadmap.md @@ -635,6 +635,55 @@ - подготовлена база для volatility-aware spread thresholds - подготовлена база для adaptive execution quality model +#### 07.4.4.1.9 ✅ Adaptive Market Diagnostics Layer +- добавлен расширенный слой диагностики рынка +- добавлены enum-модели TrendStrength, TrendQuality и MarketPhase +- MarketAnalysisResult расширен полями trend_strength, trend_quality и market_phase +- MarketAnalysisResult расширен полями trend_gap_percent и trend_consistency +- MarketAnalysisService получил расчёт EMA gap в процентах +- добавлен анализ силы тренда по EMA gap +- добавлена классификация WEAK / NORMAL / STRONG trend +- добавлен анализ trend consistency по последним свечам +- добавлена классификация CLEAN / NOISY trend +- добавлена классификация market phase +- добавлены фазы IMPULSE / PULLBACK / RANGE / SQUEEZE / UNKNOWN +- LOW volatility теперь интерпретируется как SQUEEZE phase +- FLAT trend теперь интерпретируется как RANGE phase +- слабый тренд теперь может блокировать TREND вход +- шумный тренд теперь может блокировать TREND вход +- откат внутри тренда теперь может блокировать TREND вход +- TrendStrategy получила поддержку adaptive market diagnostics +- TrendStrategy пробрасывает market_trend_strength в payload +- TrendStrategy пробрасывает market_trend_quality в payload +- TrendStrategy пробрасывает market_phase в payload +- TrendStrategy пробрасывает market_trend_gap_percent в payload +- TrendStrategy пробрасывает market_trend_consistency в payload +- добавлен HOLD reason WEAK_MARKET_TREND +- добавлен HOLD reason NOISY_MARKET_TREND +- добавлен HOLD reason MARKET_PULLBACK +- AutoTradeState расширен market_trend_strength +- AutoTradeState расширен market_trend_quality +- AutoTradeState расширен market_phase +- reset runtime очищает новые market diagnostics поля +- sync market analysis обновляет новые market diagnostics поля +- Telegram UI получил строку расширенной аналитики +- Telegram UI отображает силу тренда +- Telegram UI отображает качество тренда +- Telegram UI отображает фазу рынка +- HOLD diagnostics стали точнее +- причина HOLD теперь показывает слабый тренд +- причина HOLD теперь показывает шумный тренд +- причина HOLD теперь показывает откат +- исправлен auto_run_cycle_error после расширения MarketAnalysisResult +- исправлено зависание market state в “Идёт анализ” +- подтверждена работа live runtime после расширения аналитики +- подготовлена база для Market Semantic Runtime Layer +- подготовлена база для compact semantic UI labels +- подготовлена база для adaptive thresholds +- подготовлена база для semantic entry filters +- подготовлена база для более точного TREND execution + + --- ### 07.4.5 diff --git a/docs/roadmap/stage-07-auto-trading-roadmap.md b/docs/roadmap/stage-07-auto-trading-roadmap.md index 32f93f5..9347801 100644 --- a/docs/roadmap/stage-07-auto-trading-roadmap.md +++ b/docs/roadmap/stage-07-auto-trading-roadmap.md @@ -611,6 +611,54 @@ - подготовлена база для volatility-aware spread thresholds - подготовлена база для adaptive execution quality model +#### 07.4.4.1.9 ✅ Adaptive Market Diagnostics Layer +- добавлен расширенный слой диагностики рынка +- добавлены enum-модели TrendStrength, TrendQuality и MarketPhase +- MarketAnalysisResult расширен полями trend_strength, trend_quality и market_phase +- MarketAnalysisResult расширен полями trend_gap_percent и trend_consistency +- MarketAnalysisService получил расчёт EMA gap в процентах +- добавлен анализ силы тренда по EMA gap +- добавлена классификация WEAK / NORMAL / STRONG trend +- добавлен анализ trend consistency по последним свечам +- добавлена классификация CLEAN / NOISY trend +- добавлена классификация market phase +- добавлены фазы IMPULSE / PULLBACK / RANGE / SQUEEZE / UNKNOWN +- LOW volatility теперь интерпретируется как SQUEEZE phase +- FLAT trend теперь интерпретируется как RANGE phase +- слабый тренд теперь может блокировать TREND вход +- шумный тренд теперь может блокировать TREND вход +- откат внутри тренда теперь может блокировать TREND вход +- TrendStrategy получила поддержку adaptive market diagnostics +- TrendStrategy пробрасывает market_trend_strength в payload +- TrendStrategy пробрасывает market_trend_quality в payload +- TrendStrategy пробрасывает market_phase в payload +- TrendStrategy пробрасывает market_trend_gap_percent в payload +- TrendStrategy пробрасывает market_trend_consistency в payload +- добавлен HOLD reason WEAK_MARKET_TREND +- добавлен HOLD reason NOISY_MARKET_TREND +- добавлен HOLD reason MARKET_PULLBACK +- AutoTradeState расширен market_trend_strength +- AutoTradeState расширен market_trend_quality +- AutoTradeState расширен market_phase +- reset runtime очищает новые market diagnostics поля +- sync market analysis обновляет новые market diagnostics поля +- Telegram UI получил строку расширенной аналитики +- Telegram UI отображает силу тренда +- Telegram UI отображает качество тренда +- Telegram UI отображает фазу рынка +- HOLD diagnostics стали точнее +- причина HOLD теперь показывает слабый тренд +- причина HOLD теперь показывает шумный тренд +- причина HOLD теперь показывает откат +- исправлен auto_run_cycle_error после расширения MarketAnalysisResult +- исправлено зависание market state в “Идёт анализ” +- подтверждена работа live runtime после расширения аналитики +- подготовлена база для Market Semantic Runtime Layer +- подготовлена база для compact semantic UI labels +- подготовлена база для adaptive thresholds +- подготовлена база для semantic entry filters +- подготовлена база для более точного TREND execution + --- ### 07.4.5 diff --git a/docs/stages/07.4.4.1.6_signal_aging_and_runtime_expiration.md b/docs/stages/stage-07_4_4_1_6-signal_aging_and_runtime_expiration.md similarity index 100% rename from docs/stages/07.4.4.1.6_signal_aging_and_runtime_expiration.md rename to docs/stages/stage-07_4_4_1_6-signal_aging_and_runtime_expiration.md diff --git a/docs/stages/stage-07_4_4_1_9-adaptive_market_diagnostics_layer.md b/docs/stages/stage-07_4_4_1_9-adaptive_market_diagnostics_layer.md new file mode 100644 index 0000000..6cdea27 --- /dev/null +++ b/docs/stages/stage-07_4_4_1_9-adaptive_market_diagnostics_layer.md @@ -0,0 +1,114 @@ +# 07.4.4.1.9 Adaptive Market Diagnostics Layer + +## Что сделано + +Добавлен расширенный слой диагностики рынка поверх базового Market State Engine. + +Теперь система анализирует не только направление рынка: + +- TREND_UP +- TREND_DOWN +- RANGE +- HIGH_VOLATILITY +- LOW_VOLATILITY + +но и дополнительные характеристики тренда: + +- сила тренда +- качество тренда +- текущая фаза рынка +- процентный разрыв EMA +- consistency движения + +## Новые сущности + +Добавлены новые enum-модели: + +- TrendStrength + - WEAK + - NORMAL + - STRONG + - UNKNOWN + +- TrendQuality + - CLEAN + - NOISY + - UNKNOWN + +- MarketPhase + - IMPULSE + - PULLBACK + - RANGE + - SQUEEZE + - UNKNOWN + +## MarketAnalysisResult + +MarketAnalysisResult расширен новыми полями: + +- trend_strength +- trend_quality +- market_phase +- trend_gap_percent +- trend_consistency + +## TrendStrategy + +TrendStrategy теперь получает расширенную аналитику из MarketAnalysisService и пробрасывает её в payload: + +- market_trend_strength +- market_trend_quality +- market_phase +- market_trend_gap_percent +- market_trend_consistency + +Добавлены дополнительные HOLD-фильтры: + +- WEAK_MARKET_TREND +- NOISY_MARKET_TREND +- MARKET_PULLBACK + +## Telegram UI + +В UI добавлена строка расширенной аналитики: + +Анализ · сильный · шумный · откат + +или: + +Анализ · нормальный · чистый · импульс + +## Что исправлено + +Исправлена ошибка auto_run_cycle_error после расширения MarketAnalysisResult. + +После исправления: + +- run_cycle больше не падает +- рынок больше не зависает в состоянии “Идёт анализ” +- Telegram UI снова получает актуальную market diagnostics +- HOLD timer продолжает работать + +## Проверка + +Команды: + +python -m compileall src + +Runtime-проверка: + +- автоторговля запускается +- экран обновляется автоматически +- HOLD timer растёт +- market state отображается корректно +- строка Анализ появляется +- ошибки auto_run_cycle_error отсутствуют + +## Результат + +Этап подготовил базу для: + +- Market Semantic Runtime Layer +- semantic UI labels +- adaptive thresholds +- semantic entry filters