07.4.4.1.11 — Advanced Trend Quality & EMA Distance Layer
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user