07.4.4.1.1 — Market State Human UI + HOLD Lifecycle Fix

This commit is contained in:
2026-05-10 23:20:54 +03:00
parent 8024cd9d9a
commit ef7cec68cc
14 changed files with 1209 additions and 39 deletions

View File

@@ -3,6 +3,8 @@
from __future__ import annotations
from src.integrations.exchange.service import ExchangeService
from src.trading.market_analysis.models import MarketState
from src.trading.market_analysis.service import MarketAnalysisService
from src.trading.strategies.base import StrategyContext
from src.trading.strategies.signals import SignalResult, SignalType
@@ -12,18 +14,26 @@ class TrendStrategy:
_price_window: dict[str, list[float]] = {}
# длиннее окно = меньше шума
# короткое окно оставляем как дополнительное подтверждение импульса
_window_size = 8
# общий порог изменения за окно
_threshold_percent = 0.05
# сколько движений внутри окна должно быть в сторону сигнала
_min_direction_ratio = 0.6
# основной таймфрейм анализа рынка
_market_interval = "5m"
def analyze(self, context: StrategyContext) -> SignalResult:
market = MarketAnalysisService().analyze(
context.symbol,
interval=self._market_interval,
limit=200,
)
try:
snapshot = ExchangeService().get_market_snapshot(context.symbol)
snapshot = ExchangeService().get_market_snapshot(
context.symbol,
runtime_key="auto",
)
except Exception as exc:
return SignalResult(
signal=SignalType.HOLD,
@@ -33,6 +43,7 @@ class TrendStrategy:
"strategy": self.name,
"symbol": context.symbol,
"error": str(exc),
"market_analysis": market.payload,
},
)
@@ -48,6 +59,7 @@ class TrendStrategy:
"strategy": self.name,
"symbol": symbol,
"snapshot": snapshot,
"market_analysis": market.payload,
},
)
@@ -57,15 +69,39 @@ class TrendStrategy:
if len(prices) > self._window_size:
prices.pop(0)
base_payload = {
"strategy": self.name,
"symbol": symbol,
"analysis_price": current_price,
"last_price": snapshot.get("last_price"),
"bid_price": snapshot.get("bid_price"),
"ask_price": snapshot.get("ask_price"),
"market_state": market.state.value,
"market_trend": market.trend.value,
"market_volatility": market.volatility.value,
"market_analysis_interval": market.interval,
"market_analysis_reason": market.reason,
"market_analysis": market.payload,
}
if not market.is_trade_allowed:
return SignalResult(
signal=SignalType.HOLD,
reason=f"Market filter: {market.reason}",
confidence=0.0,
payload={
**base_payload,
"market_filter_blocked": True,
},
)
if len(prices) < self._window_size:
return SignalResult(
signal=SignalType.HOLD,
reason="Недостаточно данных для анализа тренда.",
reason="Недостаточно live-данных для подтверждения TREND.",
confidence=0.0,
payload={
"strategy": self.name,
"symbol": symbol,
"price": current_price,
**base_payload,
"window_size": len(prices),
"required_window_size": self._window_size,
},
@@ -77,11 +113,10 @@ class TrendStrategy:
if first_price <= 0:
return SignalResult(
signal=SignalType.HOLD,
reason="Некорректная стартовая цена в окне.",
reason="Некорректная стартовая цена в live-окне.",
confidence=0.0,
payload={
"strategy": self.name,
"symbol": symbol,
**base_payload,
"prices": prices,
},
)
@@ -90,14 +125,9 @@ class TrendStrategy:
direction_ratio = self._direction_ratio(prices, change_percent)
payload = {
"strategy": self.name,
"symbol": symbol,
"analysis_price": last_price,
**base_payload,
"first_price": first_price,
"current_price": last_price,
"last_price": snapshot.get("last_price"),
"bid_price": snapshot.get("bid_price"),
"ask_price": snapshot.get("ask_price"),
"change_percent": round(change_percent, 5),
"direction_ratio": round(direction_ratio, 3),
"window_size": len(prices),
@@ -105,31 +135,47 @@ class TrendStrategy:
"min_direction_ratio": self._min_direction_ratio,
}
if (
change_percent >= self._threshold_percent
and direction_ratio >= self._min_direction_ratio
):
if market.state == MarketState.TREND_UP:
if (
change_percent >= self._threshold_percent
and direction_ratio >= self._min_direction_ratio
):
return SignalResult(
signal=SignalType.BUY,
reason="TREND_UP подтверждён market analysis и live-импульсом.",
confidence=self._calculate_confidence(change_percent, direction_ratio),
payload=payload,
)
return SignalResult(
signal=SignalType.BUY,
reason="Устойчивый рост цены в окне TREND.",
confidence=self._calculate_confidence(change_percent, direction_ratio),
signal=SignalType.HOLD,
reason="TREND_UP есть, но live-импульс вверх недостаточно сильный.",
confidence=0.0,
payload=payload,
)
if (
change_percent <= -self._threshold_percent
and direction_ratio >= self._min_direction_ratio
):
if market.state == MarketState.TREND_DOWN:
if (
change_percent <= -self._threshold_percent
and direction_ratio >= self._min_direction_ratio
):
return SignalResult(
signal=SignalType.SELL,
reason="TREND_DOWN подтверждён market analysis и live-импульсом.",
confidence=self._calculate_confidence(change_percent, direction_ratio),
payload=payload,
)
return SignalResult(
signal=SignalType.SELL,
reason="Устойчивое снижение цены в окне TREND.",
confidence=self._calculate_confidence(change_percent, direction_ratio),
signal=SignalType.HOLD,
reason="TREND_DOWN есть, но live-импульс вниз недостаточно сильный.",
confidence=0.0,
payload=payload,
)
return SignalResult(
signal=SignalType.HOLD,
reason="Тренд недостаточно устойчивый.",
reason=f"Market state не подходит для TREND: {market.state.value}.",
confidence=0.0,
payload=payload,
)