07.4.4.1.6 — Signal Aging & Runtime Expiration
This commit is contained in:
@@ -76,6 +76,8 @@ EVENT_TITLES = {
|
|||||||
|
|
||||||
"balance_summary_loaded": "Баланс",
|
"balance_summary_loaded": "Баланс",
|
||||||
"balance_summary_error": "Баланс",
|
"balance_summary_error": "Баланс",
|
||||||
|
|
||||||
|
"runtime_expired": "Runtime",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -248,9 +248,9 @@ def _market_state_line(state) -> str:
|
|||||||
market_state = getattr(state, "market_state", None)
|
market_state = getattr(state, "market_state", None)
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
"TREND_UP": "📈 Тренд · Восходящий",
|
"TREND_UP": "📈 Тренд · Вверх",
|
||||||
"TREND_DOWN": "📉 Тренд · Нисходящий",
|
"TREND_DOWN": "📉 Тренд · Вниз",
|
||||||
"RANGE": "🟰 Тренд · Нет выраженного направления",
|
"RANGE": "🟰 Рынок · Флэт",
|
||||||
"HIGH_VOLATILITY": "⚠️ Рынок · Высокая волатильность",
|
"HIGH_VOLATILITY": "⚠️ Рынок · Высокая волатильность",
|
||||||
"LOW_VOLATILITY": "🟰 Рынок · Низкая активность",
|
"LOW_VOLATILITY": "🟰 Рынок · Низкая активность",
|
||||||
"UNKNOWN": "⏳ Рынок · Идёт анализ",
|
"UNKNOWN": "⏳ Рынок · Идёт анализ",
|
||||||
@@ -260,13 +260,38 @@ def _market_state_line(state) -> str:
|
|||||||
return labels.get(market_state, "⏳ Рынок · Идёт анализ")
|
return labels.get(market_state, "⏳ Рынок · Идёт анализ")
|
||||||
|
|
||||||
|
|
||||||
|
def _compact_entry_block_message(message: str) -> str:
|
||||||
|
normalized = message.strip().lower()
|
||||||
|
|
||||||
|
mapping = {
|
||||||
|
"рынок сейчас не подходит для входа": "слабый импульс",
|
||||||
|
"слабый импульс вверх": "слабый импульс",
|
||||||
|
"слабый импульс вниз": "слабый импульс",
|
||||||
|
"недостаточно live-данных": "мало данных",
|
||||||
|
"мало live-данных": "мало данных",
|
||||||
|
"высокая волатильность": "волатильность",
|
||||||
|
"низкая активность": "низкая активность",
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapping.get(normalized, message)
|
||||||
|
|
||||||
|
|
||||||
def _entry_block_line(state) -> str:
|
def _entry_block_line(state) -> str:
|
||||||
message = getattr(state, "entry_block_message", None)
|
message = getattr(state, "entry_block_message", None)
|
||||||
|
|
||||||
if not message:
|
if not message:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
return f"Вход в позицию · {message}"
|
compact_message = _compact_entry_block_message(str(message))
|
||||||
|
signal = (state.last_signal or "HOLD").upper()
|
||||||
|
|
||||||
|
if signal == "HOLD":
|
||||||
|
return f"Ожидание · {compact_message}"
|
||||||
|
|
||||||
|
if signal in {"BUY", "SELL"}:
|
||||||
|
return f"Вход · {compact_message}"
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _execution_block_lines(state) -> list[str]:
|
def _execution_block_lines(state) -> list[str]:
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ class AutoTradeService:
|
|||||||
# минимальная уверенность для готовности к будущему execution
|
# минимальная уверенность для готовности к будущему execution
|
||||||
_ready_confidence = 0.3
|
_ready_confidence = 0.3
|
||||||
|
|
||||||
|
_signal_ttl_seconds = 90
|
||||||
|
_market_analysis_ttl_seconds = 180
|
||||||
|
_last_logged_runtime_expired_key: str | None = None
|
||||||
|
|
||||||
_last_signal_key: str | None = None
|
_last_signal_key: str | None = None
|
||||||
_last_signal_value: str | None = None
|
_last_signal_value: str | None = None
|
||||||
_last_signal_reason: str = ""
|
_last_signal_reason: str = ""
|
||||||
@@ -310,13 +314,17 @@ class AutoTradeService:
|
|||||||
state.is_signal_ready = False
|
state.is_signal_ready = False
|
||||||
state.execution_block_reason = None
|
state.execution_block_reason = None
|
||||||
state.signal_started_at = None
|
state.signal_started_at = None
|
||||||
|
state.signal_updated_at = None
|
||||||
state.market_state = None
|
state.market_state = None
|
||||||
state.market_trend = None
|
state.market_trend = None
|
||||||
state.market_volatility = None
|
state.market_volatility = None
|
||||||
state.market_analysis_interval = None
|
state.market_analysis_interval = None
|
||||||
state.market_analysis_reason = None
|
state.market_analysis_reason = None
|
||||||
|
state.market_analysis_updated_at = None
|
||||||
state.entry_block_reason = None
|
state.entry_block_reason = None
|
||||||
state.entry_block_message = None
|
state.entry_block_message = None
|
||||||
|
state.runtime_expired_reason = None
|
||||||
|
state.runtime_expired_message = None
|
||||||
|
|
||||||
# собрать контекст для стратегии
|
# собрать контекст для стратегии
|
||||||
def _build_strategy_context(self) -> StrategyContext:
|
def _build_strategy_context(self) -> StrategyContext:
|
||||||
@@ -513,6 +521,9 @@ class AutoTradeService:
|
|||||||
state.last_signal_repeat_count = self._same_signal_count
|
state.last_signal_repeat_count = self._same_signal_count
|
||||||
state.last_signal_confidence = confidence
|
state.last_signal_confidence = confidence
|
||||||
state.last_signal_reason = reason
|
state.last_signal_reason = reason
|
||||||
|
state.signal_updated_at = time.monotonic()
|
||||||
|
state.runtime_expired_reason = None
|
||||||
|
state.runtime_expired_message = None
|
||||||
|
|
||||||
self._update_decision_state(
|
self._update_decision_state(
|
||||||
state=state,
|
state=state,
|
||||||
@@ -684,6 +695,7 @@ class AutoTradeService:
|
|||||||
state.market_volatility = payload.get("market_volatility")
|
state.market_volatility = payload.get("market_volatility")
|
||||||
state.market_analysis_interval = payload.get("market_analysis_interval")
|
state.market_analysis_interval = payload.get("market_analysis_interval")
|
||||||
state.market_analysis_reason = payload.get("market_analysis_reason")
|
state.market_analysis_reason = payload.get("market_analysis_reason")
|
||||||
|
state.market_analysis_updated_at = time.monotonic()
|
||||||
state.entry_block_reason = payload.get("entry_block_reason")
|
state.entry_block_reason = payload.get("entry_block_reason")
|
||||||
state.entry_block_message = payload.get("entry_block_message")
|
state.entry_block_message = payload.get("entry_block_message")
|
||||||
|
|
||||||
@@ -853,12 +865,100 @@ class AutoTradeService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
return messages.get(market_state, "Состояние рынка анализируется.")
|
return messages.get(market_state, "Состояние рынка анализируется.")
|
||||||
|
|
||||||
|
def _expire_runtime_if_needed(self, state: AutoTradeState) -> None:
|
||||||
|
now = time.monotonic()
|
||||||
|
|
||||||
|
signal_updated_at = getattr(state, "signal_updated_at", None)
|
||||||
|
if signal_updated_at is not None:
|
||||||
|
signal_age = now - float(signal_updated_at)
|
||||||
|
|
||||||
|
if signal_age > self._signal_ttl_seconds:
|
||||||
|
previous_signal = state.last_signal
|
||||||
|
|
||||||
|
self._reset_signal_tracking()
|
||||||
|
|
||||||
|
state.runtime_expired_reason = "SIGNAL_TTL_EXPIRED"
|
||||||
|
state.runtime_expired_message = "сигнал устарел и был сброшен"
|
||||||
|
|
||||||
|
self._log_runtime_expired_if_changed(
|
||||||
|
state=state,
|
||||||
|
reason="SIGNAL_TTL_EXPIRED",
|
||||||
|
message="Сигнал устарел и был сброшен.",
|
||||||
|
payload={
|
||||||
|
"previous_signal": previous_signal,
|
||||||
|
"signal_age_seconds": int(signal_age),
|
||||||
|
"signal_ttl_seconds": self._signal_ttl_seconds,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
market_updated_at = getattr(state, "market_analysis_updated_at", None)
|
||||||
|
if market_updated_at is not None:
|
||||||
|
market_age = now - float(market_updated_at)
|
||||||
|
|
||||||
|
if market_age > self._market_analysis_ttl_seconds:
|
||||||
|
state.market_state = None
|
||||||
|
state.market_trend = None
|
||||||
|
state.market_volatility = None
|
||||||
|
state.market_analysis_interval = None
|
||||||
|
state.market_analysis_reason = None
|
||||||
|
state.market_analysis_updated_at = None
|
||||||
|
state.entry_block_reason = None
|
||||||
|
state.entry_block_message = None
|
||||||
|
state.runtime_expired_reason = "MARKET_ANALYSIS_TTL_EXPIRED"
|
||||||
|
state.runtime_expired_message = "анализ рынка устарел"
|
||||||
|
|
||||||
|
self._log_runtime_expired_if_changed(
|
||||||
|
state=state,
|
||||||
|
reason="MARKET_ANALYSIS_TTL_EXPIRED",
|
||||||
|
message="Анализ рынка устарел и был сброшен.",
|
||||||
|
payload={
|
||||||
|
"market_age_seconds": int(market_age),
|
||||||
|
"market_analysis_ttl_seconds": self._market_analysis_ttl_seconds,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def _log_runtime_expired_if_changed(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
state: AutoTradeState,
|
||||||
|
reason: str,
|
||||||
|
message: str,
|
||||||
|
payload: dict,
|
||||||
|
) -> None:
|
||||||
|
key = f"{state.status}:{state.symbol}:{state.strategy}:{reason}"
|
||||||
|
|
||||||
|
if key == type(self)._last_logged_runtime_expired_key:
|
||||||
|
return
|
||||||
|
|
||||||
|
type(self)._last_logged_runtime_expired_key = key
|
||||||
|
|
||||||
|
try:
|
||||||
|
JournalService().log_ui_warning(
|
||||||
|
event_type="runtime_expired",
|
||||||
|
message=message,
|
||||||
|
screen="auto",
|
||||||
|
action="runtime_expiration",
|
||||||
|
payload={
|
||||||
|
**payload,
|
||||||
|
"symbol": state.symbol,
|
||||||
|
"strategy": state.strategy,
|
||||||
|
"status": state.status,
|
||||||
|
"runtime_expired_reason": reason,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def run_cycle(self) -> AutoTradeState:
|
def run_cycle(self) -> AutoTradeState:
|
||||||
state = self.get_state()
|
state = self.get_state()
|
||||||
|
|
||||||
if state.status == "OFF":
|
if state.status == "OFF":
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
self._expire_runtime_if_needed(state)
|
||||||
|
|
||||||
strategy = self._get_strategy()
|
strategy = self._get_strategy()
|
||||||
context = self._build_strategy_context()
|
context = self._build_strategy_context()
|
||||||
|
|||||||
@@ -125,4 +125,16 @@ class AutoTradeState:
|
|||||||
entry_block_reason: str | None = None
|
entry_block_reason: str | None = None
|
||||||
|
|
||||||
# человекочитаемое объяснение причины не входа
|
# человекочитаемое объяснение причины не входа
|
||||||
entry_block_message: str | None = None
|
entry_block_message: str | None = None
|
||||||
|
|
||||||
|
# время последнего обновления сигнала, monotonic timestamp
|
||||||
|
signal_updated_at: float | None = None
|
||||||
|
|
||||||
|
# время последнего обновления market analysis, monotonic timestamp
|
||||||
|
market_analysis_updated_at: float | None = None
|
||||||
|
|
||||||
|
# причина runtime expiration
|
||||||
|
runtime_expired_reason: str | None = None
|
||||||
|
|
||||||
|
# человекочитаемое сообщение runtime expiration
|
||||||
|
runtime_expired_message: str | None = None
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
from src.integrations.exchange.service import ExchangeService
|
from src.integrations.exchange.service import ExchangeService
|
||||||
from src.trading.strategies.base import StrategyContext
|
from src.trading.strategies.base import StrategyContext
|
||||||
from src.trading.strategies.signals import SignalResult, SignalType
|
from src.trading.strategies.signals import SignalResult, SignalType
|
||||||
@@ -11,6 +13,8 @@ class ScalpStrategy:
|
|||||||
name = "SCALP"
|
name = "SCALP"
|
||||||
|
|
||||||
_price_window: dict[str, list[float]] = {}
|
_price_window: dict[str, list[float]] = {}
|
||||||
|
_window_ttl_seconds = 30
|
||||||
|
_price_window_updated_at: dict[str, float] = {}
|
||||||
|
|
||||||
# короткое окно = быстрая реакция
|
# короткое окно = быстрая реакция
|
||||||
_window_size = 4
|
_window_size = 4
|
||||||
@@ -24,6 +28,7 @@ class ScalpStrategy:
|
|||||||
def reset_runtime(self, symbol: str | None = None) -> None:
|
def reset_runtime(self, symbol: str | None = None) -> None:
|
||||||
if symbol is None:
|
if symbol is None:
|
||||||
self._price_window.clear()
|
self._price_window.clear()
|
||||||
|
self._price_window_updated_at.clear()
|
||||||
return
|
return
|
||||||
|
|
||||||
normalized_symbol = symbol.upper()
|
normalized_symbol = symbol.upper()
|
||||||
@@ -34,6 +39,7 @@ class ScalpStrategy:
|
|||||||
|
|
||||||
for key in keys_to_delete:
|
for key in keys_to_delete:
|
||||||
self._price_window.pop(key, None)
|
self._price_window.pop(key, None)
|
||||||
|
self._price_window_updated_at.pop(key, None)
|
||||||
|
|
||||||
def analyze(self, context: StrategyContext) -> SignalResult:
|
def analyze(self, context: StrategyContext) -> SignalResult:
|
||||||
try:
|
try:
|
||||||
@@ -53,8 +59,18 @@ class ScalpStrategy:
|
|||||||
symbol = ticker.symbol
|
symbol = ticker.symbol
|
||||||
current_price = float(ticker.price)
|
current_price = float(ticker.price)
|
||||||
|
|
||||||
|
now = time.monotonic()
|
||||||
|
previous_updated_at = self._price_window_updated_at.get(symbol)
|
||||||
|
|
||||||
|
if (
|
||||||
|
previous_updated_at is not None
|
||||||
|
and now - previous_updated_at > self._window_ttl_seconds
|
||||||
|
):
|
||||||
|
self._price_window.pop(symbol, None)
|
||||||
|
|
||||||
prices = self._price_window.setdefault(symbol, [])
|
prices = self._price_window.setdefault(symbol, [])
|
||||||
prices.append(current_price)
|
prices.append(current_price)
|
||||||
|
self._price_window_updated_at[symbol] = now
|
||||||
|
|
||||||
if len(prices) > self._window_size:
|
if len(prices) > self._window_size:
|
||||||
prices.pop(0)
|
prices.pop(0)
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
from src.integrations.exchange.service import ExchangeService
|
from src.integrations.exchange.service import ExchangeService
|
||||||
from src.trading.market_analysis.models import MarketState
|
from src.trading.market_analysis.models import MarketState
|
||||||
from src.trading.market_analysis.service import MarketAnalysisService
|
from src.trading.market_analysis.service import MarketAnalysisService
|
||||||
@@ -13,6 +15,8 @@ class TrendStrategy:
|
|||||||
name = "TREND"
|
name = "TREND"
|
||||||
|
|
||||||
_price_window: dict[str, list[float]] = {}
|
_price_window: dict[str, list[float]] = {}
|
||||||
|
_window_ttl_seconds = 60
|
||||||
|
_price_window_updated_at: dict[str, float] = {}
|
||||||
|
|
||||||
# короткое окно оставляем как дополнительное подтверждение импульса
|
# короткое окно оставляем как дополнительное подтверждение импульса
|
||||||
_window_size = 8
|
_window_size = 8
|
||||||
@@ -25,6 +29,7 @@ class TrendStrategy:
|
|||||||
def reset_runtime(self, symbol: str | None = None) -> None:
|
def reset_runtime(self, symbol: str | None = None) -> None:
|
||||||
if symbol is None:
|
if symbol is None:
|
||||||
self._price_window.clear()
|
self._price_window.clear()
|
||||||
|
self._price_window_updated_at.clear()
|
||||||
return
|
return
|
||||||
|
|
||||||
normalized_symbol = symbol.upper()
|
normalized_symbol = symbol.upper()
|
||||||
@@ -35,6 +40,7 @@ class TrendStrategy:
|
|||||||
|
|
||||||
for key in keys_to_delete:
|
for key in keys_to_delete:
|
||||||
self._price_window.pop(key, None)
|
self._price_window.pop(key, None)
|
||||||
|
self._price_window_updated_at.pop(key, None)
|
||||||
|
|
||||||
def analyze(self, context: StrategyContext) -> SignalResult:
|
def analyze(self, context: StrategyContext) -> SignalResult:
|
||||||
market = MarketAnalysisService().analyze(
|
market = MarketAnalysisService().analyze(
|
||||||
@@ -77,8 +83,18 @@ class TrendStrategy:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
now = time.monotonic()
|
||||||
|
previous_updated_at = self._price_window_updated_at.get(symbol)
|
||||||
|
|
||||||
|
if (
|
||||||
|
previous_updated_at is not None
|
||||||
|
and now - previous_updated_at > self._window_ttl_seconds
|
||||||
|
):
|
||||||
|
self._price_window.pop(symbol, None)
|
||||||
|
|
||||||
prices = self._price_window.setdefault(symbol, [])
|
prices = self._price_window.setdefault(symbol, [])
|
||||||
prices.append(current_price)
|
prices.append(current_price)
|
||||||
|
self._price_window_updated_at[symbol] = now
|
||||||
|
|
||||||
if len(prices) > self._window_size:
|
if len(prices) > self._window_size:
|
||||||
prices.pop(0)
|
prices.pop(0)
|
||||||
@@ -96,6 +112,8 @@ class TrendStrategy:
|
|||||||
"market_analysis_interval": market.interval,
|
"market_analysis_interval": market.interval,
|
||||||
"market_analysis_reason": market.reason,
|
"market_analysis_reason": market.reason,
|
||||||
"market_analysis": market.payload,
|
"market_analysis": market.payload,
|
||||||
|
"runtime_window_ttl_seconds": self._window_ttl_seconds,
|
||||||
|
"runtime_window_size": len(prices),
|
||||||
}
|
}
|
||||||
|
|
||||||
if not market.is_trade_allowed:
|
if not market.is_trade_allowed:
|
||||||
|
|||||||
@@ -493,6 +493,32 @@
|
|||||||
- подготовлена база для signal aging/reset system
|
- подготовлена база для signal aging/reset system
|
||||||
- подготовлена база для adaptive runtime memory management
|
- подготовлена база для adaptive runtime memory management
|
||||||
|
|
||||||
|
#### 07.4.4.1.6 ✅ Signal Aging & Runtime Expiration
|
||||||
|
- добавлены поля signal_updated_at и market_analysis_updated_at в AutoTradeState
|
||||||
|
- добавлены runtime_expired_reason и runtime_expired_message
|
||||||
|
- внедрён TTL для signal runtime
|
||||||
|
- внедрён TTL для market analysis runtime
|
||||||
|
- добавлен runtime expiration handler в AutoTradeService
|
||||||
|
- добавлено событие runtime_expired для журнала
|
||||||
|
- добавлена защита от spam logging одинаковых runtime expiration событий
|
||||||
|
- signal tracking теперь обновляет время последнего сигнала
|
||||||
|
- market analysis sync теперь обновляет время последней аналитики
|
||||||
|
- stale signal runtime сбрасывается при превышении TTL
|
||||||
|
- stale market diagnostics очищаются при превышении TTL
|
||||||
|
- TrendStrategy получила TTL для live price window
|
||||||
|
- ScalpStrategy получила отдельный TTL для live price window
|
||||||
|
- reset_runtime теперь очищает price window и timestamp window
|
||||||
|
- предотвращено использование старых цен после runtime-паузы
|
||||||
|
- HOLD timer сохранён как индикатор живого runtime цикла
|
||||||
|
- Telegram UI переведён на компактные market state labels
|
||||||
|
- entry diagnostics в UI разделены на Ожидание и Вход
|
||||||
|
- добавлен compact mapping для длинных entry_block_message
|
||||||
|
- подтверждена корректная работа runtime lifecycle на флэт-рынке
|
||||||
|
- выявлен uncovered HOLD diagnostic scenario для следующего этапа
|
||||||
|
- подготовлена база для advanced market diagnostics layer
|
||||||
|
- подготовлена база для adaptive thresholds
|
||||||
|
- подготовлена база для signal freshness-aware execution
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 07.4.5
|
### 07.4.5
|
||||||
|
|||||||
@@ -469,6 +469,31 @@
|
|||||||
- подготовлена база для signal aging/reset system
|
- подготовлена база для signal aging/reset system
|
||||||
- подготовлена база для adaptive runtime memory management
|
- подготовлена база для adaptive runtime memory management
|
||||||
|
|
||||||
|
#### 07.4.4.1.6 ✅ Signal Aging & Runtime Expiration
|
||||||
|
- добавлены поля signal_updated_at и market_analysis_updated_at в AutoTradeState
|
||||||
|
- добавлены runtime_expired_reason и runtime_expired_message
|
||||||
|
- внедрён TTL для signal runtime
|
||||||
|
- внедрён TTL для market analysis runtime
|
||||||
|
- добавлен runtime expiration handler в AutoTradeService
|
||||||
|
- добавлено событие runtime_expired для журнала
|
||||||
|
- добавлена защита от spam logging одинаковых runtime expiration событий
|
||||||
|
- signal tracking теперь обновляет время последнего сигнала
|
||||||
|
- market analysis sync теперь обновляет время последней аналитики
|
||||||
|
- stale signal runtime сбрасывается при превышении TTL
|
||||||
|
- stale market diagnostics очищаются при превышении TTL
|
||||||
|
- TrendStrategy получила TTL для live price window
|
||||||
|
- ScalpStrategy получила отдельный TTL для live price window
|
||||||
|
- reset_runtime теперь очищает price window и timestamp window
|
||||||
|
- предотвращено использование старых цен после runtime-паузы
|
||||||
|
- HOLD timer сохранён как индикатор живого runtime цикла
|
||||||
|
- Telegram UI переведён на компактные market state labels
|
||||||
|
- entry diagnostics в UI разделены на Ожидание и Вход
|
||||||
|
- добавлен compact mapping для длинных entry_block_message
|
||||||
|
- подтверждена корректная работа runtime lifecycle на флэт-рынке
|
||||||
|
- выявлен uncovered HOLD diagnostic scenario для следующего этапа
|
||||||
|
- подготовлена база для advanced market diagnostics layer
|
||||||
|
- подготовлена база для adaptive thresholds
|
||||||
|
- подготовлена база для signal freshness-aware execution
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,261 @@
|
|||||||
|
# 07.4.4.1.6 — Signal Aging & Runtime Expiration
|
||||||
|
|
||||||
|
## Цель этапа
|
||||||
|
|
||||||
|
Добавить слой контроля возраста runtime-состояния автоторговли, чтобы сигналы, market analysis и внутренние окна стратегий не жили бесконечно и не влияли на новые решения после пауз, смены актива, смены стратегии или простоя рынка.
|
||||||
|
|
||||||
|
Этап закрывает проблему “устаревшего runtime”:
|
||||||
|
|
||||||
|
- старый BUY/SELL не должен продолжаться после долгой паузы;
|
||||||
|
- READY-состояние не должно сохраняться бесконечно;
|
||||||
|
- market diagnostics не должны визуально залипать;
|
||||||
|
- runtime-окна стратегий не должны использовать старые цены после простоя;
|
||||||
|
- HOLD lifecycle должен оставаться живым и понятным в UI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что было до этапа
|
||||||
|
|
||||||
|
До внедрения Signal Aging runtime-состояние было логически изолировано по активам и стратегиям, но не имело срока жизни.
|
||||||
|
|
||||||
|
Это означало, что:
|
||||||
|
|
||||||
|
- `_price_window` мог хранить старые цены;
|
||||||
|
- `last_signal` мог оставаться актуальным дольше, чем фактический рынок;
|
||||||
|
- `READY` мог визуально выглядеть свежим, хотя сигнал уже устарел;
|
||||||
|
- market diagnostics могли оставаться на экране после смены условий;
|
||||||
|
- пользователь видел состояние, но не всегда понимал, насколько оно свежее.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что внедрено
|
||||||
|
|
||||||
|
### 1. Signal aging fields в AutoTradeState
|
||||||
|
|
||||||
|
В состояние автоторговли добавлены поля для отслеживания возраста runtime:
|
||||||
|
|
||||||
|
```python
|
||||||
|
signal_updated_at
|
||||||
|
market_analysis_updated_at
|
||||||
|
runtime_expired_reason
|
||||||
|
runtime_expired_message
|
||||||
|
```
|
||||||
|
|
||||||
|
Теперь состояние знает:
|
||||||
|
|
||||||
|
- когда последний раз обновлялся сигнал;
|
||||||
|
- когда последний раз обновлялась аналитика рынка;
|
||||||
|
- была ли runtime-информация сброшена по expiration;
|
||||||
|
- почему она была сброшена.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Signal TTL
|
||||||
|
|
||||||
|
Добавлен TTL для сигналов:
|
||||||
|
|
||||||
|
```python
|
||||||
|
_signal_ttl_seconds = 90
|
||||||
|
```
|
||||||
|
|
||||||
|
Если сигнал живёт дольше допустимого времени без актуального обновления, runtime сбрасывается.
|
||||||
|
|
||||||
|
Это защищает от ситуаций:
|
||||||
|
|
||||||
|
```text
|
||||||
|
BUY появился
|
||||||
|
↓
|
||||||
|
бот/рынок/цикл остановился
|
||||||
|
↓
|
||||||
|
через долгое время BUY всё ещё считается актуальным
|
||||||
|
```
|
||||||
|
|
||||||
|
Теперь такой сигнал считается устаревшим и должен быть пересобран заново.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Market analysis TTL
|
||||||
|
|
||||||
|
Добавлен TTL для market diagnostics:
|
||||||
|
|
||||||
|
```python
|
||||||
|
_market_analysis_ttl_seconds = 180
|
||||||
|
```
|
||||||
|
|
||||||
|
Если анализ рынка устарел, очищаются:
|
||||||
|
|
||||||
|
```python
|
||||||
|
market_state
|
||||||
|
market_trend
|
||||||
|
market_volatility
|
||||||
|
market_analysis_interval
|
||||||
|
market_analysis_reason
|
||||||
|
entry_block_reason
|
||||||
|
entry_block_message
|
||||||
|
```
|
||||||
|
|
||||||
|
Это предотвращает визуальное залипание старого состояния рынка.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Runtime expiration handler
|
||||||
|
|
||||||
|
В AutoTradeService добавлен метод:
|
||||||
|
|
||||||
|
```python
|
||||||
|
_expire_runtime_if_needed()
|
||||||
|
```
|
||||||
|
|
||||||
|
Он проверяет возраст:
|
||||||
|
|
||||||
|
- текущего сигнала;
|
||||||
|
- последнего market analysis;
|
||||||
|
- runtime diagnostics.
|
||||||
|
|
||||||
|
При необходимости он сбрасывает устаревшие данные и подготавливает state к новой оценке рынка.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Runtime expiration logging
|
||||||
|
|
||||||
|
Добавлено событие журнала:
|
||||||
|
|
||||||
|
```python
|
||||||
|
runtime_expired
|
||||||
|
```
|
||||||
|
|
||||||
|
Оно используется для фиксации случаев, когда runtime был сброшен по TTL.
|
||||||
|
|
||||||
|
Логика защищена от spam logging одинаковых expiration-событий.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Runtime window TTL для TREND
|
||||||
|
|
||||||
|
В TrendStrategy добавлен TTL live-окна:
|
||||||
|
|
||||||
|
```python
|
||||||
|
_window_ttl_seconds = 60
|
||||||
|
_price_window_updated_at
|
||||||
|
```
|
||||||
|
|
||||||
|
Теперь live-окно TREND очищается, если между обновлениями был слишком большой разрыв.
|
||||||
|
|
||||||
|
Это защищает от анализа старых цен как будто они свежие.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Runtime window TTL для SCALP
|
||||||
|
|
||||||
|
В ScalpStrategy добавлен отдельный TTL:
|
||||||
|
|
||||||
|
```python
|
||||||
|
_window_ttl_seconds = 30
|
||||||
|
_price_window_updated_at
|
||||||
|
```
|
||||||
|
|
||||||
|
SCALP чувствительнее к времени, поэтому его окно живёт меньше.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. Runtime reset теперь очищает не только цены
|
||||||
|
|
||||||
|
`reset_runtime()` теперь очищает:
|
||||||
|
|
||||||
|
- `_price_window`;
|
||||||
|
- `_price_window_updated_at`.
|
||||||
|
|
||||||
|
Это важно, потому что сам факт времени последнего обновления тоже является runtime-состоянием.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. UI продолжает показывать HOLD duration
|
||||||
|
|
||||||
|
Строка:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Сигнал 🟡 HOLD · 27с
|
||||||
|
```
|
||||||
|
|
||||||
|
оставлена намеренно.
|
||||||
|
|
||||||
|
Это не торговый сигнал, а индикатор живого runtime-цикла:
|
||||||
|
|
||||||
|
- экран обновляется;
|
||||||
|
- HOLD не залип;
|
||||||
|
- цикл автоторговли работает;
|
||||||
|
- пользователь видит возраст текущего режима ожидания.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. Compact Telegram UI
|
||||||
|
|
||||||
|
В рамках проверки этапа UI был приведён к более короткому виду.
|
||||||
|
|
||||||
|
Market state теперь отображается компактно:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"TREND_UP": "📈 Тренд · Вверх"
|
||||||
|
"TREND_DOWN": "📉 Тренд · Вниз"
|
||||||
|
"RANGE": "🟰 Рынок · Флэт"
|
||||||
|
"HIGH_VOLATILITY": "⚠️ Рынок · Высокая волатильность"
|
||||||
|
"LOW_VOLATILITY": "🟰 Рынок · Низкая активность"
|
||||||
|
```
|
||||||
|
|
||||||
|
Entry diagnostics разделены по смыслу:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Ожидание · слабый импульс
|
||||||
|
Вход · слабый импульс
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Проверка этапа
|
||||||
|
|
||||||
|
Проверено на live UI:
|
||||||
|
|
||||||
|
- HOLD timer работает;
|
||||||
|
- market state отображается компактно;
|
||||||
|
- после runtime reset старые данные не протекают;
|
||||||
|
- UI не залипает на старом активе;
|
||||||
|
- entry diagnostics стали короче;
|
||||||
|
- runtime state не смешивается между активами;
|
||||||
|
- выбранный актив отображается корректно;
|
||||||
|
- подготовка ордера продолжает рассчитываться штатно.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что обнаружено дополнительно
|
||||||
|
|
||||||
|
На LTC был выявлен uncovered HOLD diagnostic сценарий:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Сигнал 🟡 HOLD
|
||||||
|
📉 Тренд · Вниз
|
||||||
|
```
|
||||||
|
|
||||||
|
При этом UI не показал причину, почему при нисходящем тренде не был выдан SELL.
|
||||||
|
|
||||||
|
Причина: не все HOLD-ветки TrendStrategy передают `entry_block_reason` и `entry_block_message`.
|
||||||
|
|
||||||
|
Это не блокер этапа 07.4.4.1.6, потому что относится не к aging, а к полноте диагностического слоя стратегии.
|
||||||
|
|
||||||
|
Рекомендуется вынести в следующий этап:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Advanced Market Diagnostics Layer
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Итог этапа
|
||||||
|
|
||||||
|
Этап 07.4.4.1.6 добавил фундамент runtime expiration:
|
||||||
|
|
||||||
|
- сигналы получили возраст;
|
||||||
|
- market analysis получил возраст;
|
||||||
|
- runtime-окна стратегий получили TTL;
|
||||||
|
- stale state теперь можно сбрасывать;
|
||||||
|
- UI показывает живое состояние runtime;
|
||||||
|
- система подготовлена к adaptive thresholds и расширенной диагностике входа.
|
||||||
Reference in New Issue
Block a user