07.4.4.1.11 — Advanced Trend Quality & EMA Distance Layer

This commit is contained in:
2026-05-20 21:15:00 +03:00
parent 2c75f95b46
commit 06ea376cb5
36 changed files with 6260 additions and 2092 deletions

View File

@@ -8,6 +8,8 @@ from datetime import datetime
from src.core.config import load_settings
from src.core.event_bus import EventBus
from src.core.numbers import safe_float
from src.core.types import JsonDict, NumericLike
from src.trading.auto.state import AutoTradeState
from src.trading.execution.engine import ExecutionEngine
from src.trading.journal.service import JournalService
@@ -40,7 +42,7 @@ class AutoTradeService:
_last_signal_value: str | None = None
_last_signal_reason: str = ""
_last_signal_confidence: float = 0.0
_last_signal_payload: dict | None = None
_last_signal_payload: JsonDict | None = None
_last_signal_started_at: float | None = None
_last_logged_market_state: str | None = None
_last_logged_market_trend: str | None = None
@@ -50,46 +52,145 @@ class AutoTradeService:
_max_snapshot_age_seconds = 5.0
_warning_snapshot_age_seconds = 2.0
_spread_warning_enter_percent = 0.08
_spread_warning_exit_percent = 0.06
_spread_block_enter_percent = 0.15
_spread_block_exit_percent = 0.12
_spread_thresholds_by_asset: dict[str, dict[str, float]] = {
"BTC": {
"warning_enter": 0.08,
"warning_exit": 0.06,
"block_enter": 0.15,
"block_exit": 0.12,
},
"ETH": {
"warning_enter": 0.10,
"warning_exit": 0.08,
"block_enter": 0.18,
"block_exit": 0.15,
},
"LTC": {
"warning_enter": 0.18,
"warning_exit": 0.14,
"block_enter": 0.35,
"block_exit": 0.28,
},
"XRP": {
"warning_enter": 0.20,
"warning_exit": 0.16,
"block_enter": 0.40,
"block_exit": 0.32,
},
}
_default_spread_thresholds: dict[str, float] = {
"warning_enter": 0.12,
"warning_exit": 0.09,
"block_enter": 0.25,
"block_exit": 0.20,
}
_last_logged_execution_quality_key: str | None = None
def _asset_symbol(self, symbol: str | None) -> str:
if not symbol:
return ""
base = str(symbol).split("_", 1)[0].upper()
if "/" in base:
return base.split("/", 1)[0]
for suffix in ("USDT", "USD", "EUR", "BTC"):
if base.endswith(suffix) and len(base) > len(suffix):
return base[: -len(suffix)]
return base
def _spread_thresholds(self, symbol: str | None) -> dict[str, float]:
asset = self._asset_symbol(symbol)
return self._spread_thresholds_by_asset.get(
asset,
self._default_spread_thresholds,
)
def _sync_market_availability_state(self, state: AutoTradeState) -> bool:
status = ExchangeService().get_symbol_market_status(state.symbol)
is_open = bool(status.get("is_open"))
market_status = str(status.get("status") or "UNKNOWN")
message = str(status.get("message") or "")
state.market_is_open = is_open
state.market_status = market_status
state.market_status_message = message
state.market_status_updated_at = time.monotonic()
if is_open:
if state.execution_quality_reason == "MARKET_CLOSED":
state.execution_quality = None
state.execution_quality_reason = None
state.execution_quality_message = None
state.execution_block_reason = None
state.market_runtime_degraded = False
return True
state.execution_quality = "BLOCKED"
state.execution_quality_reason = "MARKET_CLOSED"
state.execution_quality_message = "рынок закрыт"
state.execution_block_reason = "рынок закрыт"
state.market_runtime_degraded = True
state.entry_block_reason = "MARKET_CLOSED"
state.entry_block_message = "рынок закрыт"
state.decision_status = "WAITING"
state.decision_reason = message or "Рынок закрыт."
state.is_signal_confirmed = False
state.is_signal_ready = False
return False
def _spread_execution_quality(
self,
*,
state: AutoTradeState,
spread_percent: float | None,
spread_percent: NumericLike | None,
) -> tuple[str | None, str | None, str | None, bool]:
if spread_percent is None:
spread = safe_float(spread_percent)
if spread is None:
return None, None, None, False
thresholds = self._spread_thresholds(state.symbol)
warning_enter = thresholds["warning_enter"]
warning_exit = thresholds["warning_exit"]
block_enter = thresholds["block_enter"]
block_exit = thresholds["block_exit"]
previous_quality = state.execution_quality
previous_reason = state.execution_quality_reason
if previous_quality == "BLOCKED" and previous_reason == "HIGH_SPREAD":
if spread_percent > self._spread_block_exit_percent:
if spread > block_exit:
return "BLOCKED", "HIGH_SPREAD", "высокий spread", False
if spread_percent > self._spread_warning_exit_percent:
if spread > warning_exit:
return "WARNING", "WIDE_SPREAD", "spread повышен", False
return "GOOD", "MARKET_OK", "рынок готов", False
if previous_quality == "WARNING" and previous_reason == "WIDE_SPREAD":
if spread_percent >= self._spread_block_enter_percent:
if spread >= block_enter:
return "BLOCKED", "HIGH_SPREAD", "высокий spread", False
if spread_percent > self._spread_warning_exit_percent:
if spread > warning_exit:
return "WARNING", "WIDE_SPREAD", "spread повышен", False
return "GOOD", "MARKET_OK", "рынок готов", False
if spread_percent >= self._spread_block_enter_percent:
if spread >= block_enter:
return "BLOCKED", "HIGH_SPREAD", "высокий spread", False
if spread_percent >= self._spread_warning_enter_percent:
if spread >= warning_enter:
return "WARNING", "WIDE_SPREAD", "spread повышен", False
return "GOOD", "MARKET_OK", "рынок готов", False
@@ -99,11 +200,12 @@ class AutoTradeService:
self,
*,
signal: str,
confidence: float = 0.9,
confidence: NumericLike = 0.9,
repeat_count: int = 2,
reason: str = "DEBUG SIGNAL",
) -> AutoTradeState:
state = self.get_state()
confidence_value = safe_float(confidence) or 0.0
normalized_signal = signal.strip().upper()
if normalized_signal not in {"BUY", "SELL", "HOLD"}:
@@ -117,7 +219,7 @@ class AutoTradeService:
state.last_signal = normalized_signal
state.last_signal_repeat_count = repeat_count
state.last_signal_confidence = confidence
state.last_signal_confidence = confidence_value
state.last_signal_reason = reason
state.signal_confirmation_seconds = self._confirm_min_duration_seconds
state.signal_confirmation_required_seconds = self._confirm_min_duration_seconds
@@ -162,13 +264,15 @@ class AutoTradeService:
return state
# установить капитал, выделенный под автоторговлю
def set_allocated_balance_usd(self, value: float) -> AutoTradeState:
def set_allocated_balance_usd(self, value: NumericLike) -> AutoTradeState:
state = self.get_state()
if value <= 0:
value = 1000.0
numeric_value = safe_float(value)
state.allocated_balance_usd = value
if numeric_value is None or numeric_value <= 0:
numeric_value = 1000.0
state.allocated_balance_usd = numeric_value
state.execution_block_reason = None
state.execution_size_adjustment_reason = None
return state
@@ -231,6 +335,10 @@ class AutoTradeService:
state.status = "RUNNING"
self._reset_signal_tracking()
state.cycle_realized_pnl_usd = 0.0
state.cycle_closed_trades = 0
state.cycle_winning_trades = 0
state.cycle_started_at = time.monotonic()
state.cycle_number = int(getattr(state, "cycle_number", 0) or 0) + 1
state.last_flip_old_side = None
state.last_flip_new_side = None
state.last_flip_pnl_usd = None
@@ -268,6 +376,9 @@ class AutoTradeService:
if previous_status == "OFF":
state.cycle_realized_pnl_usd = 0.0
state.cycle_closed_trades = 0
state.cycle_winning_trades = 0
state.cycle_started_at = time.monotonic()
state.last_flip_old_side = None
state.last_flip_new_side = None
state.last_flip_pnl_usd = None
@@ -288,6 +399,10 @@ class AutoTradeService:
state.status = "OFF"
state.cycle_realized_pnl_usd = 0.0
state.cycle_closed_trades = 0
state.cycle_winning_trades = 0
state.cycle_started_at = None
state.adaptive_size_changed_at = None
state.last_flip_old_side = None
state.last_flip_new_side = None
state.last_flip_pnl_usd = None
@@ -333,39 +448,39 @@ class AutoTradeService:
return state
# установить риск
def set_risk_percent(self, risk_percent: float) -> AutoTradeState:
def set_risk_percent(self, risk_percent: NumericLike) -> AutoTradeState:
state = self.get_state()
state.risk_percent = risk_percent
state.risk_percent = safe_float(risk_percent)
return state
# установить плечо
def set_leverage(self, leverage: float) -> AutoTradeState:
def set_leverage(self, leverage: NumericLike) -> AutoTradeState:
state = self.get_state()
state.leverage = leverage
state.leverage = safe_float(leverage)
return state
# установить stop loss в %
def set_stop_loss_percent(self, value: float | None) -> AutoTradeState:
def set_stop_loss_percent(self, value: NumericLike | None) -> AutoTradeState:
state = self.get_state()
state.stop_loss_percent = value
state.stop_loss_percent = safe_float(value)
return state
# установить take profit в %
def set_take_profit_percent(self, value: float | None) -> AutoTradeState:
def set_take_profit_percent(self, value: NumericLike | None) -> AutoTradeState:
state = self.get_state()
state.take_profit_percent = value
state.take_profit_percent = safe_float(value)
return state
# установить max loss в USD
def set_max_loss_usd(self, value: float | None) -> AutoTradeState:
def set_max_loss_usd(self, value: NumericLike | None) -> AutoTradeState:
state = self.get_state()
state.max_loss_usd = value
state.max_loss_usd = safe_float(value)
return state
# установить максимальное использование баланса под маржу
def set_max_reserved_balance_percent(self, value: float | None) -> AutoTradeState:
def set_max_reserved_balance_percent(self, value: NumericLike | None) -> AutoTradeState:
state = self.get_state()
state.max_reserved_balance_percent = value
state.max_reserved_balance_percent = safe_float(value)
state.execution_block_reason = None
return state
@@ -380,6 +495,7 @@ class AutoTradeService:
self._same_signal_count = 0
state = self.get_state()
state.adaptive_size_base = None
state.adaptive_size_final = None
state.adaptive_size_multiplier = None
@@ -387,6 +503,7 @@ class AutoTradeService:
state.adaptive_size_factors = None
state.effective_risk_percent = None
state.effective_target_risk_usd = None
state.last_signal_repeat_count = 0
state.last_signal_confidence = 0.0
state.last_signal_reason = None
@@ -399,6 +516,9 @@ class AutoTradeService:
state.signal_confirmation_missing_repeats = self._confirm_repeats
state.signal_confirmation_progress = 0.0
state.signal_confirmation_reason = None
state.signal_started_at = None
state.signal_updated_at = None
state.execution_block_reason = None
state.execution_semantic_status = None
state.execution_semantic_message = None
@@ -411,8 +531,7 @@ class AutoTradeService:
state.execution_confidence_required_score = self._execution_confidence_required_score
state.execution_confidence_reason = None
state.execution_confidence_factors = None
state.signal_started_at = None
state.signal_updated_at = None
state.market_state = None
state.market_trend = None
state.market_volatility = None
@@ -424,8 +543,29 @@ class AutoTradeService:
state.market_trend_quality = None
state.market_phase = None
state.market_phase_direction = None
state.market_trend_gap_percent = None
state.market_trend_consistency = None
state.market_trend_efficiency = None
state.trend_quality_score = None
state.ema_distance_atr_ratio = None
state.ema_distance_state = None
state.entry_timing_state = None
state.entry_timing_reason = None
state.ema_fast_slope_percent = None
state.ema_slow_slope_percent = None
state.candle_noise_score = None
state.price_position_score = None
state.htf_interval = None
state.htf_atr_percent = None
state.htf_atr_percent_baseline = None
state.htf_volatility_ratio = None
state.htf_volatility = None
state.entry_block_reason = None
state.entry_block_message = None
state.momentum_state = None
state.momentum_direction = None
state.momentum_change_percent = None
@@ -433,6 +573,7 @@ class AutoTradeService:
state.breakout_level = None
state.breakout_distance_percent = None
state.breakout_reason = None
state.runtime_expired_reason = None
state.runtime_expired_message = None
state.snapshot_age_seconds = None
@@ -508,7 +649,12 @@ class AutoTradeService:
if state.signal_started_at is None:
signal_age_seconds = 0
else:
signal_age_seconds = max(0, int(now - float(state.signal_started_at)))
signal_started = safe_float(state.signal_started_at)
signal_age_seconds = (
max(0, int(now - signal_started))
if signal_started is not None
else 0
)
missing_repeats = max(0, self._confirm_repeats - self._same_signal_count)
missing_seconds = max(
@@ -589,7 +735,7 @@ class AutoTradeService:
signal: str,
reason: str,
confidence: float,
payload: dict | None,
payload: JsonDict | None,
) -> None:
signal_key = f"{state.status}:{state.symbol}:{strategy_name}:{signal}"
previous_signal = self._last_signal_value
@@ -757,7 +903,7 @@ class AutoTradeService:
signal: str,
reason: str,
confidence: float,
payload: dict | None,
payload: JsonDict | None,
) -> None:
return
@@ -772,7 +918,7 @@ class AutoTradeService:
next_signal: str,
reason: str,
confidence: float,
payload: dict | None,
payload: JsonDict | None,
duration_seconds: int,
) -> None:
if previous_signal != "HOLD":
@@ -822,6 +968,11 @@ class AutoTradeService:
if normalized_signal not in {"BUY", "SELL"}:
return
snapshot = ExchangeService().get_market_snapshot(
state.symbol,
runtime_key="auto",
)
try:
JournalService().log_ui_info(
event_type="signal_ready",
@@ -846,6 +997,9 @@ class AutoTradeService:
"confirmation_seconds": state.signal_confirmation_seconds,
"confirmation_required_seconds": state.signal_confirmation_required_seconds,
"confirmation_progress": state.signal_confirmation_progress,
"bid_price": snapshot.get("bid_price"),
"ask_price": snapshot.get("ask_price"),
"last_price": snapshot.get("last_price"),
},
)
except Exception:
@@ -855,7 +1009,7 @@ class AutoTradeService:
self,
*,
state: AutoTradeState,
payload: dict | None,
payload: JsonDict | None,
) -> None:
if not isinstance(payload, dict):
return
@@ -864,25 +1018,42 @@ class AutoTradeService:
previous_market_trend = state.market_trend
previous_market_volatility = state.market_volatility
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_phase_direction = payload.get("market_phase_direction")
state.market_analysis_interval = payload.get("market_analysis_interval")
state.market_analysis_reason = payload.get("market_analysis_reason")
state.momentum_state = payload.get("momentum_state")
state.momentum_direction = payload.get("momentum_direction")
state.momentum_change_percent = payload.get("momentum_change_percent")
state.momentum_strength = payload.get("momentum_strength")
state.breakout_level = payload.get("breakout_level")
state.breakout_distance_percent = payload.get("breakout_distance_percent")
state.breakout_reason = payload.get("breakout_reason")
state.market_state = str(payload.get("market_state") or "")
state.market_trend = str(payload.get("trend") or payload.get("market_trend") or "")
state.market_volatility = str(payload.get("volatility") or payload.get("market_volatility") or "")
state.market_trend_strength = str(payload.get("market_trend_strength") or "")
state.market_trend_quality = str(payload.get("market_trend_quality") or "")
state.market_phase = str(payload.get("market_phase") or "")
state.market_phase_direction = str(payload.get("market_phase_direction") or "")
state.market_trend_gap_percent = safe_float(payload.get("market_trend_gap_percent"))
state.market_trend_consistency = safe_float(payload.get("market_trend_consistency"))
state.market_trend_efficiency = safe_float(payload.get("market_trend_efficiency"))
state.trend_quality_score = safe_float(payload.get("trend_quality_score"))
state.ema_distance_atr_ratio = safe_float(payload.get("ema_distance_atr_ratio"))
state.ema_distance_state = str(payload.get("ema_distance_state") or "")
state.entry_timing_state = str(payload.get("entry_timing_state") or "")
state.entry_timing_reason = str(payload.get("entry_timing_reason") or "")
state.ema_fast_slope_percent = safe_float(payload.get("ema_fast_slope_percent"))
state.ema_slow_slope_percent = safe_float(payload.get("ema_slow_slope_percent"))
state.candle_noise_score = safe_float(payload.get("candle_noise_score"))
state.price_position_score = safe_float(payload.get("price_position_score"))
state.htf_interval = str(payload.get("htf_interval") or "")
state.htf_atr_percent = safe_float(payload.get("htf_atr_percent"))
state.htf_atr_percent_baseline = safe_float(payload.get("htf_atr_percent_baseline"))
state.htf_volatility_ratio = safe_float(payload.get("htf_volatility_ratio"))
state.htf_volatility = str(payload.get("htf_volatility") or "")
state.market_analysis_interval = str(payload.get("interval") or payload.get("market_analysis_interval") or "")
state.market_analysis_reason = str(payload.get("reason") or payload.get("market_analysis_reason") or "")
state.momentum_state = str(payload.get("momentum_state") or "")
state.momentum_direction = str(payload.get("momentum_direction") or "")
state.momentum_change_percent = safe_float(payload.get("momentum_change_percent"))
state.momentum_strength = safe_float(payload.get("momentum_strength"))
state.breakout_level = safe_float(payload.get("breakout_level"))
state.breakout_distance_percent = safe_float(payload.get("breakout_distance_percent"))
state.breakout_reason = str(payload.get("breakout_reason") or "")
state.market_analysis_updated_at = time.monotonic()
state.entry_block_reason = payload.get("entry_block_reason")
state.entry_block_message = payload.get("entry_block_message")
state.entry_block_reason = str(payload.get("entry_block_reason") or "")
state.entry_block_message = str(payload.get("entry_block_message") or "")
self._log_market_state_if_changed(
state=state,
@@ -901,7 +1072,7 @@ class AutoTradeService:
self,
*,
state: AutoTradeState,
payload: dict,
payload: JsonDict,
) -> None:
reason = state.entry_block_reason
message = state.entry_block_message
@@ -938,7 +1109,7 @@ class AutoTradeService:
self,
*,
state: AutoTradeState,
payload: dict,
payload: JsonDict,
previous_market_state: str | None,
previous_market_trend: str | None,
previous_market_volatility: str | None,
@@ -1003,7 +1174,7 @@ class AutoTradeService:
event_type: str,
market_state: str,
message: str,
payload: dict,
payload: JsonDict,
) -> None:
level = self._market_journal_level(market_state)
@@ -1034,7 +1205,7 @@ class AutoTradeService:
return messages.get(str(market_volatility or ""), "Волатильность не определена.")
def _market_journal_level(self, market_state: str) -> str:
def _market_journal_level(self, market_state: str | None) -> str:
if market_state == "HIGH_VOLATILITY":
return "WARNING"
@@ -1056,8 +1227,11 @@ class AutoTradeService:
signal_updated_at = getattr(state, "signal_updated_at", None)
if signal_updated_at is not None:
signal_age = now - float(signal_updated_at)
signal_updated = safe_float(signal_updated_at)
if signal_updated is None:
return
signal_age = now - signal_updated
if signal_age > self._signal_ttl_seconds:
previous_signal = state.last_signal
@@ -1081,7 +1255,12 @@ class AutoTradeService:
market_updated_at = getattr(state, "market_analysis_updated_at", None)
if market_updated_at is not None:
market_age = now - float(market_updated_at)
market_updated = safe_float(market_updated_at)
if market_updated is None:
return
market_age = now - market_updated
if market_age > self._market_analysis_ttl_seconds:
state.market_state = None
@@ -1096,7 +1275,23 @@ class AutoTradeService:
state.market_trend_quality = None
state.market_phase = None
state.market_phase_direction = None
state.market_trend_gap_percent = None
state.market_trend_consistency = None
state.market_trend_efficiency = None
state.trend_quality_score = None
state.ema_distance_atr_ratio = None
state.ema_distance_state = None
state.entry_timing_state = None
state.entry_timing_reason = None
state.ema_fast_slope_percent = None
state.ema_slow_slope_percent = None
state.candle_noise_score = None
state.price_position_score = None
state.htf_interval = None
state.htf_atr_percent = None
state.htf_atr_percent_baseline = None
state.htf_volatility_ratio = None
state.htf_volatility = None
state.momentum_state = None
state.momentum_direction = None
state.momentum_change_percent = None
@@ -1123,7 +1318,7 @@ class AutoTradeService:
state: AutoTradeState,
reason: str,
message: str,
payload: dict,
payload: JsonDict,
) -> None:
key = f"{state.status}:{state.symbol}:{state.strategy}:{reason}"
@@ -1159,7 +1354,7 @@ class AutoTradeService:
fallback_price = None
try:
fallback_price = float(
fallback_price = safe_float(
ExchangeService().get_price(
state.symbol,
runtime_key="auto",
@@ -1192,10 +1387,10 @@ class AutoTradeService:
)
return
bid_price = self._safe_float(snapshot.get("bid_price"))
ask_price = self._safe_float(snapshot.get("ask_price"))
last_price = self._safe_float(snapshot.get("last_price"))
age_seconds = self._safe_float(snapshot.get("age_seconds"))
bid_price = safe_float(snapshot.get("bid_price"))
ask_price = safe_float(snapshot.get("ask_price"))
last_price = safe_float(snapshot.get("last_price"))
age_seconds = safe_float(snapshot.get("age_seconds"))
is_fresh = bool(snapshot.get("is_fresh", False))
source = str(snapshot.get("source") or "")
@@ -1240,6 +1435,8 @@ class AutoTradeService:
elif state.execution_block_reason == state.execution_quality_message:
state.execution_block_reason = None
spread_thresholds = self._spread_thresholds(state.symbol)
self._log_execution_quality_if_changed(
state=state,
payload={
@@ -1258,49 +1455,46 @@ class AutoTradeService:
"market_runtime_degraded": state.market_runtime_degraded,
"max_snapshot_age_seconds": self._max_snapshot_age_seconds,
"warning_snapshot_age_seconds": self._warning_snapshot_age_seconds,
"spread_warning_enter_percent": self._spread_warning_enter_percent,
"spread_warning_exit_percent": self._spread_warning_exit_percent,
"spread_block_enter_percent": self._spread_block_enter_percent,
"spread_block_exit_percent": self._spread_block_exit_percent,
"spread_asset": self._asset_symbol(state.symbol),
"spread_warning_enter_percent": spread_thresholds["warning_enter"],
"spread_warning_exit_percent": spread_thresholds["warning_exit"],
"spread_block_enter_percent": spread_thresholds["block_enter"],
"spread_block_exit_percent": spread_thresholds["block_exit"],
},
)
def _spread_percent(
self,
*,
bid_price: float | None,
ask_price: float | None,
bid_price: NumericLike | None,
ask_price: NumericLike | None,
) -> float | None:
if bid_price is None or ask_price is None:
bid = safe_float(bid_price)
ask = safe_float(ask_price)
if bid is None or ask is None:
return None
if bid_price <= 0 or ask_price <= 0:
if bid <= 0 or ask <= 0:
return None
mid_price = (bid_price + ask_price) / 2
mid_price = (bid + ask) / 2
if mid_price <= 0:
return None
spread = ask_price - bid_price
spread = ask - bid
if spread < 0:
return None
return round((spread / mid_price) * 100, 5)
def _safe_float(self, value: object) -> float | None:
if value is None:
return None
try:
return float(value)
except (TypeError, ValueError):
return None
def _log_execution_quality_if_changed(
self,
*,
state: AutoTradeState,
payload: dict,
payload: JsonDict,
) -> None:
quality = state.execution_quality
reason = state.execution_quality_reason
@@ -1408,8 +1602,18 @@ class AutoTradeService:
strength = state.market_trend_strength
quality = state.market_trend_quality
phase = state.market_phase
ema_distance_state = state.ema_distance_state
entry_timing_state = state.entry_timing_state
trend_quality_score = safe_float(state.trend_quality_score)
if market_state in {"HIGH_VOLATILITY", "LOW_VOLATILITY", "RANGE", "UNKNOWN", None}:
if market_state in {
"HIGH_VOLATILITY",
"LOW_VOLATILITY",
"RANGE",
"UNKNOWN",
None,
"",
}:
return 0.25
score = 0.65
@@ -1422,7 +1626,9 @@ class AutoTradeService:
score -= 0.25
if quality == "CLEAN":
score += 0.1
score += 0.12
elif quality == "NORMAL":
score += 0.04
elif quality == "NOISY":
score -= 0.25
@@ -1433,6 +1639,30 @@ class AutoTradeService:
elif phase in {"RANGE", "SQUEEZE"}:
score -= 0.3
if ema_distance_state == "HEALTHY":
score += 0.08
elif ema_distance_state == "EXTENDED":
score -= 0.08
elif ema_distance_state == "COMPRESSED":
score -= 0.18
elif ema_distance_state == "OVEREXTENDED":
score -= 0.35
if entry_timing_state == "NORMAL":
score += 0.08
elif entry_timing_state == "EARLY":
score -= 0.05
elif entry_timing_state == "LATE":
score -= 0.2
elif entry_timing_state == "CHASING":
score -= 0.35
if trend_quality_score is not None:
if trend_quality_score >= 0.7:
score += 0.08
elif trend_quality_score < 0.45:
score -= 0.15
return self._clamp_score(score)
def _execution_quality_confidence_score(self, state: AutoTradeState) -> float:
@@ -1482,11 +1712,16 @@ class AutoTradeService:
return "достаточная совокупная уверенность входа"
def _clamp_score(self, value: float | int | None) -> float:
def _clamp_score(self, value: NumericLike | None) -> float:
if value is None:
return 0.0
return max(0.0, min(1.0, float(value)))
numeric = safe_float(value)
if numeric is None:
return 0.0
return max(0.0, min(1.0, numeric))
def _sync_execution_semantic_state(self, state: AutoTradeState) -> None:
if state.execution_quality == "BLOCKED":
@@ -1541,6 +1776,9 @@ class AutoTradeService:
def _execution_block_semantic_message(self, state: AutoTradeState) -> str:
reason = state.execution_quality_reason
if reason == "MARKET_CLOSED":
return "⏸️ Исполнение · рынок закрыт"
if reason == "STALE_SNAPSHOT":
return "⛔ Исполнение · рынок неактуален"
@@ -1561,6 +1799,11 @@ class AutoTradeService:
if state.status == "OFF":
return state
if not self._sync_market_availability_state(state):
state.last_check_at = datetime.now().strftime("%H:%M:%S")
self._sync_execution_semantic_state(state)
return state
self._expire_runtime_if_needed(state)
strategy = self._get_strategy()