07.4.4.1.1 — Market State Human UI + HOLD Lifecycle Fix
This commit is contained in:
@@ -388,7 +388,7 @@ class AutoTradeService:
|
||||
confidence: float,
|
||||
payload: dict | None,
|
||||
) -> None:
|
||||
signal_key = f"{state.status}:{state.symbol}:{strategy_name}:{signal}:{reason}"
|
||||
signal_key = f"{state.status}:{state.symbol}:{strategy_name}:{signal}"
|
||||
previous_signal = self._last_signal_value
|
||||
previous_count = self._same_signal_count
|
||||
is_same_signal = signal_key == self._last_signal_key
|
||||
@@ -396,6 +396,10 @@ class AutoTradeService:
|
||||
|
||||
if is_same_signal:
|
||||
self._same_signal_count += 1
|
||||
self._last_signal_reason = reason
|
||||
self._last_signal_confidence = confidence
|
||||
self._last_signal_payload = payload
|
||||
|
||||
self._update_signal_state_fields(
|
||||
state=state,
|
||||
signal=signal,
|
||||
@@ -404,7 +408,7 @@ class AutoTradeService:
|
||||
)
|
||||
return
|
||||
|
||||
if previous_signal is not None:
|
||||
if previous_signal is not None and previous_signal != signal:
|
||||
if previous_count > 1:
|
||||
self._log_signal_summary(
|
||||
strategy_name=strategy_name,
|
||||
@@ -636,7 +640,21 @@ class AutoTradeService:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# выполнить один цикл анализа рынка
|
||||
def _sync_market_analysis_state(
|
||||
self,
|
||||
*,
|
||||
state: AutoTradeState,
|
||||
payload: dict | None,
|
||||
) -> None:
|
||||
if not isinstance(payload, dict):
|
||||
return
|
||||
|
||||
state.market_state = payload.get("market_state")
|
||||
state.market_trend = payload.get("market_trend")
|
||||
state.market_volatility = payload.get("market_volatility")
|
||||
state.market_analysis_interval = payload.get("market_analysis_interval")
|
||||
state.market_analysis_reason = payload.get("market_analysis_reason")
|
||||
|
||||
def run_cycle(self) -> AutoTradeState:
|
||||
state = self.get_state()
|
||||
|
||||
@@ -647,6 +665,11 @@ class AutoTradeService:
|
||||
context = self._build_strategy_context()
|
||||
result = strategy.analyze(context)
|
||||
|
||||
self._sync_market_analysis_state(
|
||||
state=state,
|
||||
payload=result.payload,
|
||||
)
|
||||
|
||||
state.last_check_at = datetime.now().strftime("%H:%M:%S")
|
||||
|
||||
self._log_signal_if_changed(
|
||||
|
||||
@@ -104,4 +104,19 @@ class AutoTradeState:
|
||||
last_flip_block_reason: str | None = None
|
||||
|
||||
# время последнего успешного flip
|
||||
last_flip_at: str | None = None
|
||||
last_flip_at: str | None = None
|
||||
|
||||
# состояние рынка по Market State Engine
|
||||
market_state: str | None = None
|
||||
|
||||
# направление тренда: UP / DOWN / FLAT / UNKNOWN
|
||||
market_trend: str | None = None
|
||||
|
||||
# волатильность: LOW / NORMAL / HIGH / UNKNOWN
|
||||
market_volatility: str | None = None
|
||||
|
||||
# таймфрейм анализа рынка
|
||||
market_analysis_interval: str | None = None
|
||||
|
||||
# объяснение последнего анализа рынка
|
||||
market_analysis_reason: str | None = None
|
||||
3
app/src/trading/market_analysis/__init__.py
Normal file
3
app/src/trading/market_analysis/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# app/src/trading/market_analysis/__init__.py
|
||||
|
||||
from __future__ import annotations
|
||||
67
app/src/trading/market_analysis/indicators.py
Normal file
67
app/src/trading/market_analysis/indicators.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# app/src/trading/market_analysis/indicators.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from src.integrations.exchange.models import Kline
|
||||
|
||||
|
||||
def ema(values: list[float], period: int) -> float | None:
|
||||
if period <= 0 or len(values) < period:
|
||||
return None
|
||||
|
||||
multiplier = 2 / (period + 1)
|
||||
current = sum(values[:period]) / period
|
||||
|
||||
for value in values[period:]:
|
||||
current = (value - current) * multiplier + current
|
||||
|
||||
return current
|
||||
|
||||
|
||||
def atr(candles: list[Kline], period: int = 14) -> float | None:
|
||||
if period <= 0 or len(candles) < period + 1:
|
||||
return None
|
||||
|
||||
true_ranges: list[float] = []
|
||||
|
||||
for previous, current in zip(candles, candles[1:]):
|
||||
high_low = current.high_price - current.low_price
|
||||
high_close = abs(current.high_price - previous.close_price)
|
||||
low_close = abs(current.low_price - previous.close_price)
|
||||
|
||||
true_ranges.append(max(high_low, high_close, low_close))
|
||||
|
||||
if len(true_ranges) < period:
|
||||
return None
|
||||
|
||||
recent = true_ranges[-period:]
|
||||
return sum(recent) / period
|
||||
|
||||
|
||||
def rsi(values: list[float], period: int = 14) -> float | None:
|
||||
if period <= 0 or len(values) < period + 1:
|
||||
return None
|
||||
|
||||
gains: list[float] = []
|
||||
losses: list[float] = []
|
||||
|
||||
recent = values[-(period + 1):]
|
||||
|
||||
for previous, current in zip(recent, recent[1:]):
|
||||
change = current - previous
|
||||
|
||||
if change > 0:
|
||||
gains.append(change)
|
||||
losses.append(0.0)
|
||||
else:
|
||||
gains.append(0.0)
|
||||
losses.append(abs(change))
|
||||
|
||||
average_gain = sum(gains) / period
|
||||
average_loss = sum(losses) / period
|
||||
|
||||
if average_loss == 0:
|
||||
return 100.0
|
||||
|
||||
rs = average_gain / average_loss
|
||||
return 100 - (100 / (1 + rs))
|
||||
52
app/src/trading/market_analysis/models.py
Normal file
52
app/src/trading/market_analysis/models.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# app/src/trading/market_analysis/models.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class MarketState(StrEnum):
|
||||
TREND_UP = "TREND_UP"
|
||||
TREND_DOWN = "TREND_DOWN"
|
||||
RANGE = "RANGE"
|
||||
HIGH_VOLATILITY = "HIGH_VOLATILITY"
|
||||
LOW_VOLATILITY = "LOW_VOLATILITY"
|
||||
UNKNOWN = "UNKNOWN"
|
||||
|
||||
|
||||
class TrendDirection(StrEnum):
|
||||
UP = "UP"
|
||||
DOWN = "DOWN"
|
||||
FLAT = "FLAT"
|
||||
UNKNOWN = "UNKNOWN"
|
||||
|
||||
|
||||
class VolatilityState(StrEnum):
|
||||
LOW = "LOW"
|
||||
NORMAL = "NORMAL"
|
||||
HIGH = "HIGH"
|
||||
UNKNOWN = "UNKNOWN"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class MarketAnalysisResult:
|
||||
symbol: str
|
||||
interval: str
|
||||
|
||||
state: MarketState
|
||||
trend: TrendDirection
|
||||
volatility: VolatilityState
|
||||
|
||||
close_price: float | None
|
||||
ema_fast: float | None
|
||||
ema_slow: float | None
|
||||
atr: float | None
|
||||
atr_percent: float | None
|
||||
rsi: float | None
|
||||
|
||||
candles_count: int
|
||||
reason: str
|
||||
is_trade_allowed: bool
|
||||
|
||||
payload: dict
|
||||
256
app/src/trading/market_analysis/service.py
Normal file
256
app/src/trading/market_analysis/service.py
Normal file
@@ -0,0 +1,256 @@
|
||||
# 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,
|
||||
MarketState,
|
||||
TrendDirection,
|
||||
VolatilityState,
|
||||
)
|
||||
|
||||
|
||||
class MarketAnalysisService:
|
||||
_fast_ema_period = 20
|
||||
_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
|
||||
|
||||
def analyze(
|
||||
self,
|
||||
symbol: str,
|
||||
*,
|
||||
interval: str = "5m",
|
||||
limit: int = 200,
|
||||
) -> MarketAnalysisResult:
|
||||
try:
|
||||
batch = ExchangeService().get_klines(
|
||||
symbol=symbol,
|
||||
interval=interval,
|
||||
limit=limit,
|
||||
)
|
||||
except Exception as exc:
|
||||
return self._unknown(
|
||||
symbol=symbol,
|
||||
interval=interval,
|
||||
reason=f"Не удалось получить свечи: {exc}",
|
||||
)
|
||||
|
||||
candles = batch.candles
|
||||
closes = [item.close_price for item in candles]
|
||||
|
||||
if len(candles) < self._min_candles:
|
||||
return self._unknown(
|
||||
symbol=batch.symbol,
|
||||
interval=interval,
|
||||
reason="Недостаточно свечей для анализа рынка.",
|
||||
candles_count=len(candles),
|
||||
)
|
||||
|
||||
close_price = closes[-1] if closes else None
|
||||
ema_fast = ema(closes, self._fast_ema_period)
|
||||
ema_slow = ema(closes, self._slow_ema_period)
|
||||
atr_value = atr(candles, self._atr_period)
|
||||
rsi_value = rsi(closes, self._rsi_period)
|
||||
|
||||
if (
|
||||
close_price is None
|
||||
or close_price <= 0
|
||||
or ema_fast is None
|
||||
or ema_slow is None
|
||||
or atr_value is None
|
||||
):
|
||||
return self._unknown(
|
||||
symbol=batch.symbol,
|
||||
interval=interval,
|
||||
reason="Недостаточно данных для расчёта EMA / ATR.",
|
||||
candles_count=len(candles),
|
||||
)
|
||||
|
||||
atr_percent = (atr_value / close_price) * 100
|
||||
|
||||
volatility = self._classify_volatility(atr_percent)
|
||||
trend = self._classify_trend(
|
||||
ema_fast=ema_fast,
|
||||
ema_slow=ema_slow,
|
||||
)
|
||||
|
||||
state = self._classify_market_state(
|
||||
trend=trend,
|
||||
volatility=volatility,
|
||||
)
|
||||
|
||||
is_trade_allowed = state in {
|
||||
MarketState.TREND_UP,
|
||||
MarketState.TREND_DOWN,
|
||||
}
|
||||
|
||||
reason = self._reason(
|
||||
state=state,
|
||||
trend=trend,
|
||||
volatility=volatility,
|
||||
atr_percent=atr_percent,
|
||||
rsi_value=rsi_value,
|
||||
)
|
||||
|
||||
return MarketAnalysisResult(
|
||||
symbol=batch.symbol,
|
||||
interval=interval,
|
||||
state=state,
|
||||
trend=trend,
|
||||
volatility=volatility,
|
||||
close_price=close_price,
|
||||
ema_fast=ema_fast,
|
||||
ema_slow=ema_slow,
|
||||
atr=atr_value,
|
||||
atr_percent=atr_percent,
|
||||
rsi=rsi_value,
|
||||
candles_count=len(candles),
|
||||
reason=reason,
|
||||
is_trade_allowed=is_trade_allowed,
|
||||
payload={
|
||||
"symbol": batch.symbol,
|
||||
"interval": interval,
|
||||
"market_state": state.value,
|
||||
"trend": trend.value,
|
||||
"volatility": volatility.value,
|
||||
"close_price": close_price,
|
||||
"ema_fast_period": self._fast_ema_period,
|
||||
"ema_slow_period": self._slow_ema_period,
|
||||
"ema_fast": round(ema_fast, 8),
|
||||
"ema_slow": round(ema_slow, 8),
|
||||
"atr_period": self._atr_period,
|
||||
"atr": round(atr_value, 8),
|
||||
"atr_percent": round(atr_percent, 4),
|
||||
"rsi_period": self._rsi_period,
|
||||
"rsi": round(rsi_value, 2) if rsi_value is not None else None,
|
||||
"candles_count": len(candles),
|
||||
"is_trade_allowed": is_trade_allowed,
|
||||
},
|
||||
)
|
||||
|
||||
def _classify_trend(
|
||||
self,
|
||||
*,
|
||||
ema_fast: float,
|
||||
ema_slow: float,
|
||||
) -> TrendDirection:
|
||||
if ema_slow <= 0:
|
||||
return TrendDirection.UNKNOWN
|
||||
|
||||
gap_percent = ((ema_fast - ema_slow) / ema_slow) * 100
|
||||
|
||||
if gap_percent >= self._trend_gap_percent:
|
||||
return TrendDirection.UP
|
||||
|
||||
if gap_percent <= -self._trend_gap_percent:
|
||||
return TrendDirection.DOWN
|
||||
|
||||
return TrendDirection.FLAT
|
||||
|
||||
def _classify_volatility(self, atr_percent: float) -> VolatilityState:
|
||||
if atr_percent <= 0:
|
||||
return VolatilityState.UNKNOWN
|
||||
|
||||
if atr_percent < self._low_volatility_atr_percent:
|
||||
return VolatilityState.LOW
|
||||
|
||||
if atr_percent > self._high_volatility_atr_percent:
|
||||
return VolatilityState.HIGH
|
||||
|
||||
return VolatilityState.NORMAL
|
||||
|
||||
def _classify_market_state(
|
||||
self,
|
||||
*,
|
||||
trend: TrendDirection,
|
||||
volatility: VolatilityState,
|
||||
) -> MarketState:
|
||||
if volatility == VolatilityState.HIGH:
|
||||
return MarketState.HIGH_VOLATILITY
|
||||
|
||||
if volatility == VolatilityState.LOW:
|
||||
return MarketState.LOW_VOLATILITY
|
||||
|
||||
if trend == TrendDirection.UP:
|
||||
return MarketState.TREND_UP
|
||||
|
||||
if trend == TrendDirection.DOWN:
|
||||
return MarketState.TREND_DOWN
|
||||
|
||||
if trend == TrendDirection.FLAT:
|
||||
return MarketState.RANGE
|
||||
|
||||
return MarketState.UNKNOWN
|
||||
|
||||
def _reason(
|
||||
self,
|
||||
*,
|
||||
state: MarketState,
|
||||
trend: TrendDirection,
|
||||
volatility: VolatilityState,
|
||||
atr_percent: float,
|
||||
rsi_value: float | None,
|
||||
) -> str:
|
||||
rsi_text = f", RSI={rsi_value:.2f}" if rsi_value is not None else ""
|
||||
|
||||
if state == MarketState.TREND_UP:
|
||||
return f"Рынок в восходящем тренде. ATR={atr_percent:.2f}%{rsi_text}."
|
||||
|
||||
if state == MarketState.TREND_DOWN:
|
||||
return f"Рынок в нисходящем тренде. ATR={atr_percent:.2f}%{rsi_text}."
|
||||
|
||||
if state == MarketState.RANGE:
|
||||
return f"Рынок в боковике. Тренд не подтверждён. ATR={atr_percent:.2f}%{rsi_text}."
|
||||
|
||||
if state == MarketState.HIGH_VOLATILITY:
|
||||
return f"Рынок слишком волатильный. ATR={atr_percent:.2f}%{rsi_text}."
|
||||
|
||||
if state == MarketState.LOW_VOLATILITY:
|
||||
return f"Рынок слишком спокойный. ATR={atr_percent:.2f}%{rsi_text}."
|
||||
|
||||
return f"Состояние рынка не определено. Trend={trend}, volatility={volatility}."
|
||||
|
||||
def _unknown(
|
||||
self,
|
||||
*,
|
||||
symbol: str,
|
||||
interval: str,
|
||||
reason: str,
|
||||
candles_count: int = 0,
|
||||
) -> MarketAnalysisResult:
|
||||
return MarketAnalysisResult(
|
||||
symbol=symbol,
|
||||
interval=interval,
|
||||
state=MarketState.UNKNOWN,
|
||||
trend=TrendDirection.UNKNOWN,
|
||||
volatility=VolatilityState.UNKNOWN,
|
||||
close_price=None,
|
||||
ema_fast=None,
|
||||
ema_slow=None,
|
||||
atr=None,
|
||||
atr_percent=None,
|
||||
rsi=None,
|
||||
candles_count=candles_count,
|
||||
reason=reason,
|
||||
is_trade_allowed=False,
|
||||
payload={
|
||||
"symbol": symbol,
|
||||
"interval": interval,
|
||||
"market_state": MarketState.UNKNOWN.value,
|
||||
"trend": TrendDirection.UNKNOWN.value,
|
||||
"volatility": VolatilityState.UNKNOWN.value,
|
||||
"candles_count": candles_count,
|
||||
"is_trade_allowed": False,
|
||||
"reason": reason,
|
||||
},
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user