diff --git a/app/src/trading/auto/runner.py b/app/src/trading/auto/runner.py index d1b923f..63d7b1a 100644 --- a/app/src/trading/auto/runner.py +++ b/app/src/trading/auto/runner.py @@ -36,6 +36,9 @@ class AutoTradeRunner: _last_event_version: int = 0 _retry_after_until: float = 0.0 + _position_aligned_signal_log_interval_seconds = 900 + _last_position_aligned_signal_log_at_by_key: dict[str, float] = {} + @classmethod def register_screen( cls, @@ -206,6 +209,14 @@ class AutoTradeRunner: if signal not in {"BUY", "SELL"}: return + if cls._is_position_aligned_signal(state=state, signal=signal): + cls._log_position_aligned_signal_suppressed( + state=state, + payload=payload, + signal=signal, + ) + return + cls._publish_strong_signal_event(state=state, payload=payload) return @@ -221,6 +232,73 @@ class AutoTradeRunner: ) return + @classmethod + def _is_position_aligned_signal(cls, *, state, signal: str) -> bool: + position_side = str(getattr(state, "position_side", "NONE") or "NONE").upper() + + if position_side == "LONG" and signal == "BUY": + return True + + if position_side == "SHORT" and signal == "SELL": + return True + + return False + + @classmethod + def _log_position_aligned_signal_suppressed( + cls, + *, + state, + payload: dict, + signal: str, + ) -> None: + position_side = str(getattr(state, "position_side", "NONE") or "NONE").upper() + symbol = str(payload.get("symbol") or state.symbol or "—") + strategy = str(payload.get("strategy") or state.strategy or "—") + confidence = float(payload.get("confidence") or state.last_signal_confidence or 0.0) + repeat_count = int(payload.get("repeat_count") or state.last_signal_repeat_count or 0) + + log_key = ( + f"{position_side}:" + f"{symbol}:" + f"{strategy}:" + f"{signal}" + ) + + now = time.monotonic() + last_logged_at = cls._last_position_aligned_signal_log_at_by_key.get(log_key) + + if ( + last_logged_at is not None + and now - last_logged_at < cls._position_aligned_signal_log_interval_seconds + ): + return + + cls._last_position_aligned_signal_log_at_by_key[log_key] = now + + try: + JournalService().log_ui_info( + event_type="auto_position_aligned_signal_suppressed", + message=( + f"Сигнал {signal} совпадает с открытой позицией " + f"{position_side}; уведомление подавлено." + ), + screen="auto", + action="signal_notification", + payload={ + "symbol": symbol, + "strategy": strategy, + "signal": signal, + "position_side": position_side, + "confidence": confidence, + "repeat_count": repeat_count, + "reason": payload.get("reason") or state.last_signal_reason, + "suppression_interval_seconds": cls._position_aligned_signal_log_interval_seconds, + }, + ) + except Exception: + pass + @classmethod def _publish_strong_signal_event(cls, *, state, payload: dict) -> None: signal = str(payload.get("signal", "")).upper() @@ -366,29 +444,11 @@ class AutoTradeRunner: @classmethod def _log_refresh_skip(cls, reason: str, payload: dict | None = None) -> None: - try: - JournalService().log_ui_info( - event_type="auto_screen_refresh_skipped", - message=f"Auto screen refresh skipped: {reason}", - screen="auto", - action="refresh_screen", - payload=payload or {}, - ) - except Exception: - pass + return @classmethod def _log_refresh_success(cls, payload: dict | None = None) -> None: - try: - JournalService().log_ui_info( - event_type="auto_screen_refreshed", - message="Auto screen refreshed.", - screen="auto", - action="refresh_screen", - payload=payload or {}, - ) - except Exception: - pass + return @classmethod def _log_refresh_error(cls, reason: str, payload: dict | None = None) -> None: diff --git a/app/src/trading/journal/service.py b/app/src/trading/journal/service.py index 7ca4ed5..8d0be19 100644 --- a/app/src/trading/journal/service.py +++ b/app/src/trading/journal/service.py @@ -11,7 +11,7 @@ from src.storage.repositories.journal import JournalRepository from src.storage.session import check_database_health from src.trading.journal.exporter import build_csv, build_xlsx -EXPORT_LIMIT = 5000 +EXPORT_LIMIT = 10000 class JournalService: diff --git a/docs/roadmap/master-roadmap.md b/docs/roadmap/master-roadmap.md index cab5e3f..5118cdc 100644 --- a/docs/roadmap/master-roadmap.md +++ b/docs/roadmap/master-roadmap.md @@ -363,6 +363,17 @@ - исправлена очистка ghost-позиций после forced exit - стабилизирован lifecycle paper execution во время ночного runtime-тестирования +#### 07.4.3.19.2 — Journal Noise Reduction & Position-aware Signal Logg +Снижение шума журнала и position-aware signal logging ✅ +- отключено журналирование auto_screen_refresh_skipped +- отключено журналирование auto_screen_refreshed +- сохранено журналирование ошибок refresh-цикла +- добавлено подавление сигналов, совпадающих с открытой позицией +- добавлен throttled summary auto_position_aligned_signal_suppressed +- снижено количество повторных strong signal уведомлений +- сохранена обработка противоположных сигналов как reversal / flip candidates +- подготовлена база для Signal Intent Layer в следующем этапе + ### 07.4.4 ⏳ Grid Strategy diff --git a/docs/roadmap/stage-07-auto-trading-roadmap.md b/docs/roadmap/stage-07-auto-trading-roadmap.md index 1e24fe6..10a3d74 100644 --- a/docs/roadmap/stage-07-auto-trading-roadmap.md +++ b/docs/roadmap/stage-07-auto-trading-roadmap.md @@ -339,6 +339,17 @@ - исправлена очистка ghost-позиций после forced exit - стабилизирован lifecycle paper execution во время ночного runtime-тестирования +#### 07.4.3.19.2 — Journal Noise Reduction & Position-aware Signal Logg +Снижение шума журнала и position-aware signal logging ✅ +- отключено журналирование auto_screen_refresh_skipped +- отключено журналирование auto_screen_refreshed +- сохранено журналирование ошибок refresh-цикла +- добавлено подавление сигналов, совпадающих с открытой позицией +- добавлен throttled summary auto_position_aligned_signal_suppressed +- снижено количество повторных strong signal уведомлений +- сохранена обработка противоположных сигналов как reversal / flip candidates +- подготовлена база для Signal Intent Layer в следующем этапе + --- ### 07.4.4 diff --git a/docs/stages/stage-07_4_3_19_2-journal_noise_reduction_position_aware_signal_logging.md b/docs/stages/stage-07_4_3_19_2-journal_noise_reduction_position_aware_signal_logging.md new file mode 100644 index 0000000..fc54590 --- /dev/null +++ b/docs/stages/stage-07_4_3_19_2-journal_noise_reduction_position_aware_signal_logging.md @@ -0,0 +1,171 @@ +# 07.4.3.19.2 — Journal Noise Reduction & Position-aware Signal Logging + +## Статус + +Этап: `07.4.3.19.2` +Название: `Journal Noise Reduction & Position-aware Signal Logging` +Состояние: подготовлено к runtime-проверке после внедрения правок. + +## Цель этапа + +Сократить технический шум в журнале и убрать повторяющиеся уведомления о сильных сигналах, которые совпадают с уже открытой позицией. + +До этапа журнал активно заполнялся техническими событиями автообновления экрана и повторными сигналами вида: + +- `auto_screen_refresh_skipped` +- `auto_screen_refreshed` +- повторные strong signal alerts при уже открытой позиции + +После этапа журнал должен содержать меньше технических записей и больше полезных runtime-событий. + +## Что изменено + +### 1. Отключены шумные UI refresh-логи + +В `AutoTradeRunner` отключено журналирование обычных успешных и пропущенных обновлений экрана: + +- `auto_screen_refresh_skipped` +- `auto_screen_refreshed` + +Логика обновления экрана не меняется. Экран продолжает обновляться, но journal больше не заполняется техническими refresh-событиями. + +Ошибки refresh остаются в журнале: + +- `auto_screen_refresh_error` +- `telegram_retry_after` +- `telegram_bad_request` +- `unexpected_refresh_error` + +### 2. Добавлено подавление сигналов, совпадающих с открытой позицией + +Добавлена position-aware проверка перед публикацией strong signal RuntimeEvent. + +Теперь: + +- если открыта `LONG` позиция и приходит `BUY`, уведомление подавляется; +- если открыта `SHORT` позиция и приходит `SELL`, уведомление подавляется; +- противоположные сигналы не подавляются, так как они могут быть reversal / flip candidate. + +Это снижает Telegram spam и уменьшает количество повторяющихся записей в journal. + +### 3. Добавлен throttled summary в journal + +Для подавленных aligned-сигналов добавлено редкое summary-событие: + +- `auto_position_aligned_signal_suppressed` + +Оно пишется не чаще одного раза в 15 минут для связки: + +- position side +- symbol +- strategy +- signal + +Это позволяет видеть, что стратегия продолжает выдавать сигналы по направлению позиции, но без лавины одинаковых записей. + +## Новое поведение + +### Если позиции нет + +Сильный сигнал продолжает проходить в RuntimeEvent pipeline: + +- `AUTO_SIGNAL_READY` +- Telegram notification +- journal notification logs + +### Если открыта LONG позиция + +`BUY` сигнал: + +- не отправляет повторное strong signal уведомление; +- не создаёт лишний RuntimeEvent notification; +- пишет summary `auto_position_aligned_signal_suppressed` максимум раз в 15 минут. + +`SELL` сигнал: + +- не подавляется; +- может быть обработан как reversal / flip candidate; +- дальше проверяется execution-защитами из `07.4.3.19.1`. + +### Если открыта SHORT позиция + +`SELL` сигнал: + +- не отправляет повторное strong signal уведомление; +- не создаёт лишний RuntimeEvent notification; +- пишет summary `auto_position_aligned_signal_suppressed` максимум раз в 15 минут. + +`BUY` сигнал: + +- не подавляется; +- может быть обработан как reversal / flip candidate; +- дальше проверяется execution-защитами из `07.4.3.19.1`. + +## Какие файлы затронуты + +Основной файл: + +- `app/src/trading/auto/runner.py` + +Используется существующая инфраструктура: + +- `RuntimeEventPublisher` +- `RuntimeEventType.AUTO_SIGNAL_READY` +- `JournalService` +- `NotificationTargetRegistry` + +## Что проверить после запуска + +### 1. Compile check + +`python -m compileall src` + +### 2. Проверить отсутствие refresh-spam + +После запуска в journal больше не должны появляться: + +- `auto_screen_refresh_skipped` +- `auto_screen_refreshed` + +### 3. Проверить position-aligned suppression + +После открытия позиции: + +- `LONG + BUY` не должен слать повторные Telegram strong signal alerts; +- `SHORT + SELL` не должен слать повторные Telegram strong signal alerts; +- в journal должен появляться `auto_position_aligned_signal_suppressed` не чаще одного раза в 15 минут. + +### 4. Проверить reversal-кандидаты + +Противоположные сигналы должны продолжать проходить: + +- `LONG + SELL` +- `SHORT + BUY` + +Они должны идти дальше в execution pipeline и проверяться flip-защитами. + +## Ожидаемый эффект + +После этапа journal должен стать заметно чище: + +- меньше технического UI noise; +- меньше повторных signal notification events; +- меньше Telegram spam; +- лучше видны реальные execution-события: + - `paper_position_opened` + - `paper_position_closed` + - `paper_position_flipped` + - `paper_flip_blocked` + - `auto_position_aligned_signal_suppressed` + +## Следующий рекомендуемый этап + +`07.4.3.19.3 — Strategy Noise Filter & Signal Intent Layer` + +Цель следующего этапа — не только подавлять лишние уведомления, но и классифицировать смысл сигнала относительно позиции: + +- `ENTRY` +- `REINFORCE` +- `REVERSAL_CANDIDATE` +- `FLIP_ALLOWED` +- `IGNORE`