07.4.3.19.3 — Strategy Noise Filter & Signal Intent Layer

This commit is contained in:
2026-05-10 12:53:43 +03:00
parent e72e2e51db
commit 1692cb4d81
4 changed files with 288 additions and 48 deletions

View File

@@ -7,12 +7,12 @@ import time
from datetime import datetime
from src.core.config import load_settings
from src.core.event_bus import EventBus
from src.trading.auto.state import AutoTradeState
from src.trading.execution.engine import ExecutionEngine
from src.trading.journal.service import JournalService
from src.trading.strategies.base import BaseStrategy, StrategyContext
from src.trading.strategies.registry import StrategyRegistry
from src.core.event_bus import EventBus
from src.trading.execution.engine import ExecutionEngine
class AutoTradeService:
@@ -31,6 +31,7 @@ class AutoTradeService:
_last_signal_reason: str = ""
_last_signal_confidence: float = 0.0
_last_signal_payload: dict | None = None
_last_signal_started_at: float | None = None
_same_signal_count = 0
# debug: принудительно выставить сигнал и decision
@@ -70,6 +71,11 @@ class AutoTradeService:
state.is_signal_confirmed = True
state.is_signal_ready = True
signal_intent = self._signal_intent(
state=state,
signal=state.last_signal,
)
EventBus.emit(
"auto_decision_changed",
{
@@ -77,6 +83,7 @@ class AutoTradeService:
"previous_decision_status": previous_decision_status,
"decision_status": state.decision_status,
"signal": state.last_signal,
"signal_intent": signal_intent,
"repeat_count": state.last_signal_repeat_count,
"confidence": state.last_signal_confidence,
"symbol": state.symbol,
@@ -160,6 +167,7 @@ class AutoTradeService:
self._reset_signal_tracking()
state.last_signal = "HOLD"
state.signal_started_at = time.monotonic()
EventBus.emit(
"auto_status_changed",
{
@@ -272,6 +280,7 @@ class AutoTradeService:
self._last_signal_reason = ""
self._last_signal_confidence = 0.0
self._last_signal_payload = None
self._last_signal_started_at = None
self._same_signal_count = 0
state = self.get_state()
@@ -300,6 +309,34 @@ class AutoTradeService:
state = self.get_state()
return StrategyRegistry.get(state.strategy)
# определить смысл сигнала с учетом открытой позиции
def _signal_intent(self, *, state: AutoTradeState, signal: str | None) -> str:
normalized_signal = (signal or "HOLD").upper()
position_side = str(getattr(state, "position_side", "NONE") or "NONE").upper()
if normalized_signal == "HOLD":
return "HOLD_MARKET"
if normalized_signal not in {"BUY", "SELL"}:
return "NOISE"
if position_side == "NONE":
return "ENTRY_CANDIDATE"
if position_side == "LONG" and normalized_signal == "BUY":
return "REINFORCE_POSITION"
if position_side == "SHORT" and normalized_signal == "SELL":
return "REINFORCE_POSITION"
if position_side == "LONG" and normalized_signal == "SELL":
return "REVERSAL_CANDIDATE"
if position_side == "SHORT" and normalized_signal == "BUY":
return "REVERSAL_CANDIDATE"
return "NOISE"
# обновить статус решения по текущему сигналу
def _update_decision_state(
self,
@@ -355,6 +392,7 @@ class AutoTradeService:
previous_signal = self._last_signal_value
previous_count = self._same_signal_count
is_same_signal = signal_key == self._last_signal_key
now = time.monotonic()
if is_same_signal:
self._same_signal_count += 1
@@ -377,6 +415,7 @@ class AutoTradeService:
reason=self._last_signal_reason,
confidence=self._last_signal_confidence,
payload=self._last_signal_payload,
duration_seconds=self._signal_duration_seconds(now=now),
)
else:
self._log_signal_event(
@@ -396,6 +435,7 @@ class AutoTradeService:
self._last_signal_reason = reason
self._last_signal_confidence = confidence
self._last_signal_payload = payload
self._last_signal_started_at = now
self._same_signal_count = 1
self._update_signal_state_fields(
@@ -405,6 +445,27 @@ class AutoTradeService:
confidence=confidence,
)
def _signal_duration_seconds(self, *, now: float) -> int:
if self._last_signal_started_at is None:
return max(0, int(self._same_signal_count * self._loop_interval_seconds))
return max(0, int(now - self._last_signal_started_at))
def _format_duration(self, total_seconds: int) -> str:
total_seconds = max(0, int(total_seconds))
hours = total_seconds // 3600
minutes = (total_seconds % 3600) // 60
seconds = total_seconds % 60
if hours > 0:
return f"{hours}ч {minutes:02d}м {seconds:02d}с"
if minutes > 0:
return f"{minutes}м {seconds:02d}с"
return f"{seconds}с"
# обновить поля state для экрана автоторговли
def _update_signal_state_fields(
self,
@@ -431,12 +492,30 @@ class AutoTradeService:
confidence=confidence,
)
signal_intent = self._signal_intent(
state=state,
signal=state.last_signal,
)
if (
previous_decision_status != state.decision_status
and state.decision_status == "READY"
):
self._log_ready_signal(
state=state,
signal=state.last_signal,
reason=state.last_signal_reason or reason,
confidence=state.last_signal_confidence,
signal_intent=signal_intent,
)
if previous_signal != state.last_signal:
EventBus.emit(
"auto_signal_changed",
{
"previous_signal": previous_signal,
"signal": state.last_signal,
"signal_intent": signal_intent,
"repeat_count": state.last_signal_repeat_count,
"confidence": state.last_signal_confidence,
},
@@ -449,6 +528,7 @@ class AutoTradeService:
"previous_decision_status": previous_decision_status,
"decision_status": state.decision_status,
"signal": state.last_signal,
"signal_intent": signal_intent,
"repeat_count": state.last_signal_repeat_count,
"confidence": state.last_signal_confidence,
"symbol": state.symbol,
@@ -458,7 +538,7 @@ class AutoTradeService:
},
)
# записать одиночный сигнал в журнал
# одиночные BUY / SELL больше не пишем в журнал как полезные события
def _log_signal_event(
self,
*,
@@ -469,34 +549,7 @@ class AutoTradeService:
confidence: float,
payload: dict | None,
) -> None:
emoji_map = {
"BUY": "🟢",
"SELL": "🔴",
"HOLD": "🟡",
}
emoji = emoji_map.get(signal, "")
try:
JournalService().log_ui_info(
event_type="auto_signal_generated",
message=f"{emoji} Сигнал автоторговли {signal}: {reason}",
screen="auto",
action="run_cycle",
payload={
"strategy": strategy_name,
"status": state.status,
"symbol": state.symbol,
"signal": signal,
"confidence": confidence,
"reason": reason,
"repeat_count": 1,
"is_strong_signal": confidence > self._ready_confidence,
"is_aggregated": False,
"payload": payload or {},
},
)
except Exception:
pass
return
# записать итог серии одинаковых сигналов при смене сигнала
def _log_signal_summary(
@@ -510,20 +563,19 @@ class AutoTradeService:
reason: str,
confidence: float,
payload: dict | None,
duration_seconds: int,
) -> None:
emoji_map = {
"BUY": "🟢",
"SELL": "🔴",
"HOLD": "🟡",
}
emoji = emoji_map.get(previous_signal, "")
if previous_signal != "HOLD":
return
duration_text = self._format_duration(duration_seconds)
signal_intent = "HOLD_MARKET"
try:
JournalService().log_ui_info(
event_type="auto_signal_summary",
message=(
f"{emoji} {previous_count} {previous_signal} подряд "
f"до смены на {next_signal}"
f"🟡 HOLD {duration_text} завершён сигналом {next_signal}"
),
screen="auto",
action="signal_summary",
@@ -533,10 +585,13 @@ class AutoTradeService:
"symbol": state.symbol,
"signal": previous_signal,
"next_signal": next_signal,
"signal_intent": signal_intent,
"repeat_count": previous_count,
"duration_seconds": duration_seconds,
"duration_text": duration_text,
"confidence": confidence,
"reason": reason,
"is_strong_signal": confidence > self._ready_confidence,
"is_strong_signal": False,
"is_aggregated": True,
"payload": payload or {},
},
@@ -544,6 +599,47 @@ class AutoTradeService:
except Exception:
pass
def _log_ready_signal(
self,
*,
state: AutoTradeState,
signal: str | None,
reason: str,
confidence: float,
signal_intent: str,
) -> None:
normalized_signal = (signal or "HOLD").upper()
if normalized_signal not in {"BUY", "SELL"}:
return
try:
JournalService().log_ui_info(
event_type="auto_signal_ready",
message=(
f"Сигнал {normalized_signal} готов: "
f"{signal_intent}, confidence={confidence:.2f}, "
f"repeats={state.last_signal_repeat_count}"
),
screen="auto",
action="signal_ready",
payload={
"strategy": state.strategy,
"status": state.status,
"symbol": state.symbol,
"signal": normalized_signal,
"signal_intent": signal_intent,
"confidence": confidence,
"reason": reason,
"repeat_count": state.last_signal_repeat_count,
"position_side": state.position_side,
"decision_status": state.decision_status,
"is_strong_signal": confidence > self._ready_confidence,
"is_aggregated": False,
},
)
except Exception:
pass
# выполнить один цикл анализа рынка
def run_cycle(self) -> AutoTradeState:
state = self.get_state()

View File

@@ -374,6 +374,16 @@
- сохранена обработка противоположных сигналов как reversal / flip candidates
- подготовлена база для Signal Intent Layer в следующем этапе
#### 07.4.3.19.3 ✅ Strategy Noise Filter & Signal Intent Layer
- убрано журналирование одиночных BUY / SELL без серии
- HOLD-серии переведены с repeat-count на duration формат
- добавлен формат 🟡 HOLD 5м 36с завершён сигналом SELL
- добавлен signal_intent в payload сигналов
- добавлены intent-типы ENTRY_CANDIDATE, REVERSAL_CANDIDATE, REINFORCE_POSITION, HOLD_MARKET, NOISE
- добавлена position-aware интерпретация сигналов
- добавлено отдельное событие готового сигнала
- подготовлена база для стандартизации журнала в 07.4.3.19.4
### 07.4.4
⏳ Grid Strategy

View File

@@ -350,6 +350,16 @@
- сохранена обработка противоположных сигналов как reversal / flip candidates
- подготовлена база для Signal Intent Layer в следующем этапе
#### 07.4.3.19.3 ✅ Strategy Noise Filter & Signal Intent Layer
- убрано журналирование одиночных BUY / SELL без серии
- HOLD-серии переведены с repeat-count на duration формат
- добавлен формат 🟡 HOLD 5м 36с завершён сигналом SELL
- добавлен signal_intent в payload сигналов
- добавлены intent-типы ENTRY_CANDIDATE, REVERSAL_CANDIDATE, REINFORCE_POSITION, HOLD_MARKET, NOISE
- добавлена position-aware интерпретация сигналов
- добавлено отдельное событие готового сигнала
- подготовлена база для стандартизации журнала в 07.4.3.19.4
---
### 07.4.4

View File

@@ -0,0 +1,124 @@
# 07.4.3.19.3 — Strategy Noise Filter & Signal Intent Layer
## Статус
Этап завершён.
## Цель этапа
Цель этапа — снизить шум от краткосрочных стратегических сигналов и добавить смысловой слой signal intent, чтобы журнал фиксировал не каждый сырой BUY / SELL / HOLD, а только полезные runtime-переходы.
До этого этапа журнал уже был очищен от UI refresh spam, но оставались частые записи вида:
- BUY завершился без серии
- SELL завершился без серии
- 21 HOLD подряд до смены на SELL
Такие записи были полезны на этапе разработки, но плохо подходили для runtime-аудита.
## Что изменено
### 1. Убрано журналирование одиночных BUY / SELL
Одиночные BUY / SELL, которые не дошли до серии и не стали READY-сигналом, больше не пишутся в журнал как полезные события.
Это снижает шум от микросигналов SCALP-стратегии, когда стратегия на один цикл выдаёт BUY или SELL, а затем возвращается в HOLD.
### 2. HOLD-серии теперь логируются по длительности
Сводка HOLD теперь формируется по времени, а не по количеству повторов.
Новый формат сообщения:
- 🟡 HOLD 5м 36с завершён сигналом SELL
Такой формат понятнее, потому что не зависит напрямую от внутреннего интервала цикла.
### 3. Добавлен signal intent
В payload сигнальных событий добавлен смысловой intent:
- ENTRY_CANDIDATE
- REVERSAL_CANDIDATE
- REINFORCE_POSITION
- HOLD_MARKET
- NOISE
Intent рассчитывается с учётом текущей позиции.
### 4. Добавлена position-aware интерпретация сигналов
Новая логика интерпретации:
- LONG + BUY → REINFORCE_POSITION
- SHORT + SELL → REINFORCE_POSITION
- LONG + SELL → REVERSAL_CANDIDATE
- SHORT + BUY → REVERSAL_CANDIDATE
- NONE + BUY / SELL → ENTRY_CANDIDATE
- HOLD → HOLD_MARKET
Это подготавливает систему к следующему уровню фильтрации стратегий и execution-логики.
### 5. Добавлено отдельное событие готового сигнала
Когда сигнал доходит до READY, в журнал пишется отдельное событие:
- auto_signal_ready
В payload добавляются:
- signal
- signal_intent
- confidence
- repeat_count
- position_side
- decision_status
- strategy
- symbol
## Что больше не пишется в журнал
Из журнала убраны как регулярные runtime-события:
- одиночный BUY без серии
- одиночный SELL без серии
- BUY завершился без серии
- SELL завершился без серии
- HOLD → HOLD
Эти события могут быть возвращены позже как debug-level telemetry, но не должны попадать в основной journal-аудит.
## Что остаётся в журнале
После этапа журнал должен содержать только полезные signal/runtime события:
- HOLD duration summary
- READY signal
- execution events
- blocked flip
- notification errors
- market/runtime events
- journal/system critical events
## Основной изменённый файл
- app/src/trading/auto/service.py
## Проверка
После правок нужно выполнить:
python -m compileall src
python -m src.main
После запуска проверить журнал:
1. В журнале больше нет частых записей «BUY завершился без серии» и «SELL завершился без серии».
2. HOLD-серии отображаются по времени.
3. READY-сигналы содержат signal_intent в payload.
4. Execution events продолжают приходить как раньше.
5. Flip protection из этапа 07.4.3.19.1 не сломан.
## Итог
Этап подготовил основу для дальнейшей стандартизации журнала и будущего Signal Intent Layer. Система стала меньше реагировать на шумовые микросигналы и начала фиксировать не только сырой сигнал, но и его смысл относительно текущей позиции.