07.4.3.19.3 — Strategy Noise Filter & Signal Intent Layer
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. Система стала меньше реагировать на шумовые микросигналы и начала фиксировать не только сырой сигнал, но и его смысл относительно текущей позиции.
|
||||
Reference in New Issue
Block a user