diff --git a/app/src/bootstrap/app_factory.py b/app/src/bootstrap/app_factory.py
index bbe0d7b..facaaaf 100644
--- a/app/src/bootstrap/app_factory.py
+++ b/app/src/bootstrap/app_factory.py
@@ -7,22 +7,19 @@ from aiogram.client.default import DefaultBotProperties
from src.bootstrap.logging import setup_logging
from src.core.config import load_settings
+from src.notifications.targets import NotificationTargetRegistry
from src.storage.schema import init_schema
from src.telegram.routers import setup_routers
from src.trading.journal.service import JournalService
def create_app() -> tuple[Bot, Dispatcher]:
- # загружаем настройки приложения
settings = load_settings()
- # настраиваем logging
setup_logging(settings.log_level)
- # сервис журнала
journal = JournalService()
- # инициализация схемы БД
try:
init_schema()
except Exception as exc:
@@ -40,7 +37,6 @@ def create_app() -> tuple[Bot, Dispatcher]:
pass
raise
- # лог старта приложения
try:
journal.log_info(
"app_start",
@@ -52,24 +48,17 @@ def create_app() -> tuple[Bot, Dispatcher]:
},
)
except Exception:
- # журнал не должен ломать запуск приложения
pass
- # здесь позже можно инициализировать stream/cache сервисы:
- # init_market_cache()
- # init_market_stream()
- # init_auto_trade_runner()
-
- # создаем Telegram Bot
bot = Bot(
token=settings.bot_token,
default=DefaultBotProperties(parse_mode=settings.bot_parse_mode),
)
- # создаем Dispatcher
+ NotificationTargetRegistry.set_bot(bot)
+
dispatcher = Dispatcher()
- # подключаем routers
setup_routers(dispatcher)
return bot, dispatcher
\ No newline at end of file
diff --git a/app/src/bootstrap/logging.py b/app/src/bootstrap/logging.py
index 86540e7..b1c1bfb 100644
--- a/app/src/bootstrap/logging.py
+++ b/app/src/bootstrap/logging.py
@@ -1,3 +1,5 @@
+# app/src/bootstrap/logging.py
+
from __future__ import annotations
import logging
@@ -7,4 +9,4 @@ def setup_logging(log_level: str) -> None:
logging.basicConfig(
level=getattr(logging, log_level.upper(), logging.INFO),
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
- )
+ )
\ No newline at end of file
diff --git a/app/src/notifications/__init__.py b/app/src/notifications/__init__.py
new file mode 100644
index 0000000..d430467
--- /dev/null
+++ b/app/src/notifications/__init__.py
@@ -0,0 +1,9 @@
+# app/src/notifications/__init__.py
+
+from src.notifications.models import NotificationMessage
+from src.notifications.targets import NotificationTargetRegistry
+
+__all__ = [
+ "NotificationMessage",
+ "NotificationTargetRegistry",
+]
\ No newline at end of file
diff --git a/app/src/notifications/channels/__init__.py b/app/src/notifications/channels/__init__.py
new file mode 100644
index 0000000..95c5dfa
--- /dev/null
+++ b/app/src/notifications/channels/__init__.py
@@ -0,0 +1,5 @@
+# app/src/notifications/channels/__init__.py
+
+from src.notifications.channels.telegram import TelegramNotificationChannel
+
+__all__ = ["TelegramNotificationChannel"]
\ No newline at end of file
diff --git a/app/src/notifications/channels/telegram.py b/app/src/notifications/channels/telegram.py
new file mode 100644
index 0000000..cefea86
--- /dev/null
+++ b/app/src/notifications/channels/telegram.py
@@ -0,0 +1,62 @@
+# app/src/notifications/channels/telegram.py
+
+from __future__ import annotations
+
+from aiogram.exceptions import TelegramRetryAfter
+
+from src.notifications.models import NotificationMessage
+from src.notifications.targets import NotificationTargetRegistry
+from src.trading.journal.service import JournalService
+
+
+class TelegramNotificationChannel:
+ async def send(self, message: NotificationMessage) -> bool:
+ bot = NotificationTargetRegistry.get_bot()
+ chat_id = NotificationTargetRegistry.get_default_chat_id()
+
+ if bot is None or chat_id is None:
+ JournalService().log_warning(
+ "notification_target_missing",
+ "Telegram notification target is not registered.",
+ {
+ "title": message.title,
+ "priority": message.priority,
+ "dedupe_key": message.dedupe_key,
+ },
+ )
+ return False
+
+ try:
+ await bot.send_message(
+ chat_id=chat_id,
+ text=message.text,
+ parse_mode=message.parse_mode,
+ )
+ return True
+
+ except TelegramRetryAfter as exc:
+ JournalService().log_warning(
+ "notification_telegram_retry_after",
+ "Telegram notification delayed by retry-after.",
+ {
+ "title": message.title,
+ "retry_after": exc.retry_after,
+ "priority": message.priority,
+ "dedupe_key": message.dedupe_key,
+ },
+ )
+ return False
+
+ except Exception as exc:
+ JournalService().log_error(
+ "notification_telegram_error",
+ "Telegram notification failed.",
+ {
+ "title": message.title,
+ "error": str(exc),
+ "error_type": type(exc).__name__,
+ "priority": message.priority,
+ "dedupe_key": message.dedupe_key,
+ },
+ )
+ return False
\ No newline at end of file
diff --git a/app/src/notifications/dedupe.py b/app/src/notifications/dedupe.py
new file mode 100644
index 0000000..1ab82fc
--- /dev/null
+++ b/app/src/notifications/dedupe.py
@@ -0,0 +1,23 @@
+# app/src/notifications/dedupe.py
+
+from __future__ import annotations
+
+import time
+
+
+class NotificationDedupe:
+ _sent_at_by_key: dict[str, float] = {}
+
+ @classmethod
+ def should_send(cls, key: str | None, *, ttl_seconds: int = 120) -> bool:
+ if not key:
+ return True
+
+ now = time.monotonic()
+ last_sent_at = cls._sent_at_by_key.get(key)
+
+ if last_sent_at is not None and now - last_sent_at < ttl_seconds:
+ return False
+
+ cls._sent_at_by_key[key] = now
+ return True
\ No newline at end of file
diff --git a/app/src/notifications/models.py b/app/src/notifications/models.py
new file mode 100644
index 0000000..0a03b4a
--- /dev/null
+++ b/app/src/notifications/models.py
@@ -0,0 +1,14 @@
+# app/src/notifications/models.py
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+
+@dataclass(slots=True)
+class NotificationMessage:
+ title: str
+ text: str
+ priority: str = "normal"
+ parse_mode: str = "HTML"
+ dedupe_key: str | None = None
\ No newline at end of file
diff --git a/app/src/notifications/service.py b/app/src/notifications/service.py
new file mode 100644
index 0000000..5de6c5b
--- /dev/null
+++ b/app/src/notifications/service.py
@@ -0,0 +1,65 @@
+# app/src/notifications/service.py
+
+from __future__ import annotations
+
+from src.notifications.channels.telegram import TelegramNotificationChannel
+from src.notifications.dedupe import NotificationDedupe
+from src.notifications.models import NotificationMessage
+from src.notifications.templates.execution import build_execution_notification
+from src.notifications.templates.signal import build_signal_notification
+from src.runtime_events.models import RuntimeEvent
+from src.trading.journal.service import JournalService
+
+
+class NotificationService:
+ async def handle_runtime_event(self, event: RuntimeEvent) -> None:
+ message = self._build_message(event)
+
+ if message is None:
+ JournalService().log_info(
+ "runtime_event_ignored",
+ "Runtime event has no notification template.",
+ {
+ "event_type": event.event_type.value,
+ "source": event.source,
+ "title": event.title,
+ "priority": event.priority,
+ "dedupe_key": event.dedupe_key,
+ },
+ )
+ return
+
+ if not NotificationDedupe.should_send(message.dedupe_key):
+ JournalService().log_info(
+ "notification_suppressed_duplicate",
+ "Duplicate notification suppressed.",
+ {
+ "event_type": event.event_type.value,
+ "source": event.source,
+ "title": event.title,
+ "priority": event.priority,
+ "dedupe_key": message.dedupe_key,
+ },
+ )
+ return
+
+ sent = await TelegramNotificationChannel().send(message)
+
+ if sent:
+ JournalService().log_info(
+ "notification_sent",
+ "Runtime notification sent.",
+ {
+ "event_type": event.event_type.value,
+ "source": event.source,
+ "title": event.title,
+ "priority": event.priority,
+ "dedupe_key": message.dedupe_key,
+ },
+ )
+
+ def _build_message(self, event: RuntimeEvent) -> NotificationMessage | None:
+ return (
+ build_signal_notification(event)
+ or build_execution_notification(event)
+ )
\ No newline at end of file
diff --git a/app/src/notifications/targets.py b/app/src/notifications/targets.py
new file mode 100644
index 0000000..e4f99a3
--- /dev/null
+++ b/app/src/notifications/targets.py
@@ -0,0 +1,33 @@
+# app/src/notifications/targets.py
+
+from __future__ import annotations
+
+from aiogram import Bot
+
+
+class NotificationTargetRegistry:
+ _bot: Bot | None = None
+ _chat_id: int | None = None
+
+ @classmethod
+ def set_bot(cls, bot: Bot) -> None:
+ cls._bot = bot
+
+ @classmethod
+ def set_default_chat(cls, *, bot: Bot | None = None, chat_id: int) -> None:
+ if bot is not None:
+ cls._bot = bot
+
+ cls._chat_id = chat_id
+
+ @classmethod
+ def get_bot(cls) -> Bot | None:
+ return cls._bot
+
+ @classmethod
+ def get_default_chat_id(cls) -> int | None:
+ return cls._chat_id
+
+ @classmethod
+ def is_ready(cls) -> bool:
+ return cls._bot is not None and cls._chat_id is not None
\ No newline at end of file
diff --git a/app/src/notifications/templates/__init__.py b/app/src/notifications/templates/__init__.py
new file mode 100644
index 0000000..1afa6f3
--- /dev/null
+++ b/app/src/notifications/templates/__init__.py
@@ -0,0 +1,9 @@
+# app/src/notifications/templates/__init__.py
+
+from src.notifications.templates.execution import build_execution_notification
+from src.notifications.templates.signal import build_signal_notification
+
+__all__ = [
+ "build_execution_notification",
+ "build_signal_notification",
+]
\ No newline at end of file
diff --git a/app/src/notifications/templates/execution.py b/app/src/notifications/templates/execution.py
new file mode 100644
index 0000000..5a2188e
--- /dev/null
+++ b/app/src/notifications/templates/execution.py
@@ -0,0 +1,54 @@
+# app/src/notifications/templates/execution.py
+
+from __future__ import annotations
+
+from src.notifications.models import NotificationMessage
+from src.runtime_events.event_types import RuntimeEventType
+from src.runtime_events.models import RuntimeEvent
+
+
+def build_execution_notification(event: RuntimeEvent) -> NotificationMessage | None:
+ if event.event_type not in {
+ RuntimeEventType.POSITION_OPENED,
+ RuntimeEventType.POSITION_CLOSED,
+ RuntimeEventType.POSITION_FLIPPED,
+ }:
+ return None
+
+ payload = event.payload
+
+ symbol = str(payload.get("symbol") or "—")
+ side = str(payload.get("side") or payload.get("new_side") or "—")
+ entry_price = payload.get("entry_price") or payload.get("new_entry_price")
+ exit_price = payload.get("exit_price")
+ pnl = payload.get("pnl")
+
+ if event.event_type == RuntimeEventType.POSITION_OPENED:
+ title = "📄 Position opened"
+ elif event.event_type == RuntimeEventType.POSITION_CLOSED:
+ title = "✅ Position closed"
+ else:
+ title = "🔁 Position flipped"
+
+ lines = [
+ f"{title}",
+ "",
+ f"{symbol} · {side}",
+ ]
+
+ if entry_price is not None:
+ lines.append(f"Entry: $ {entry_price}")
+
+ if exit_price is not None:
+ lines.append(f"Exit: $ {exit_price}")
+
+ if pnl is not None:
+ lines.append("")
+ lines.append(f"PnL: {pnl}")
+
+ return NotificationMessage(
+ title=event.title,
+ text="\n".join(lines),
+ priority=event.priority,
+ dedupe_key=event.dedupe_key,
+ )
\ No newline at end of file
diff --git a/app/src/notifications/templates/signal.py b/app/src/notifications/templates/signal.py
new file mode 100644
index 0000000..83470e4
--- /dev/null
+++ b/app/src/notifications/templates/signal.py
@@ -0,0 +1,39 @@
+# app/src/notifications/templates/signal.py
+
+from __future__ import annotations
+
+from src.notifications.models import NotificationMessage
+from src.runtime_events.event_types import RuntimeEventType
+from src.runtime_events.models import RuntimeEvent
+
+
+def build_signal_notification(event: RuntimeEvent) -> NotificationMessage | None:
+ if event.event_type != RuntimeEventType.AUTO_SIGNAL_READY:
+ return None
+
+ payload = event.payload
+
+ signal = str(payload.get("signal") or "—").upper()
+ symbol = str(payload.get("symbol") or "—")
+ strategy = str(payload.get("strategy") or "—")
+ confidence = payload.get("confidence")
+ repeat_count = payload.get("repeat_count")
+ reason = str(payload.get("reason") or "—")
+
+ icon = "🟢" if signal == "BUY" else "🔴" if signal == "SELL" else "🟡"
+
+ text = (
+ f"🚨 Runtime Signal · {icon} {signal}\n\n"
+ f"{symbol} · {strategy}\n"
+ f"Confidence: {confidence}\n"
+ f"Repeats: {repeat_count}\n\n"
+ f"Причина:\n"
+ f"{reason}"
+ )
+
+ return NotificationMessage(
+ title=event.title,
+ text=text,
+ priority=event.priority,
+ dedupe_key=event.dedupe_key,
+ )
\ No newline at end of file
diff --git a/app/src/runtime_events/__init__.py b/app/src/runtime_events/__init__.py
new file mode 100644
index 0000000..a33044b
--- /dev/null
+++ b/app/src/runtime_events/__init__.py
@@ -0,0 +1,9 @@
+# app/src/runtime_events/__init__.py
+
+from src.runtime_events.event_types import RuntimeEventType
+from src.runtime_events.models import RuntimeEvent
+
+__all__ = [
+ "RuntimeEvent",
+ "RuntimeEventType",
+]
\ No newline at end of file
diff --git a/app/src/runtime_events/event_types.py b/app/src/runtime_events/event_types.py
new file mode 100644
index 0000000..ad1ad1c
--- /dev/null
+++ b/app/src/runtime_events/event_types.py
@@ -0,0 +1,17 @@
+# app/src/runtime_events/event_types.py
+
+from __future__ import annotations
+
+from enum import Enum
+
+
+class RuntimeEventType(str, Enum):
+ AUTO_SIGNAL_READY = "auto_signal_ready"
+
+ POSITION_OPENED = "position_opened"
+ POSITION_CLOSED = "position_closed"
+ POSITION_FLIPPED = "position_flipped"
+
+ EXECUTION_BLOCKED = "execution_blocked"
+ RISK_ALERT = "risk_alert"
+ SYSTEM_ALERT = "system_alert"
\ No newline at end of file
diff --git a/app/src/runtime_events/models.py b/app/src/runtime_events/models.py
new file mode 100644
index 0000000..b3591c6
--- /dev/null
+++ b/app/src/runtime_events/models.py
@@ -0,0 +1,20 @@
+# app/src/runtime_events/models.py
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from datetime import datetime, timezone
+from typing import Any
+
+from src.runtime_events.event_types import RuntimeEventType
+
+
+@dataclass(slots=True)
+class RuntimeEvent:
+ event_type: RuntimeEventType
+ source: str
+ title: str
+ payload: dict[str, Any] = field(default_factory=dict)
+ priority: str = "normal"
+ dedupe_key: str | None = None
+ created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
\ No newline at end of file
diff --git a/app/src/runtime_events/publisher.py b/app/src/runtime_events/publisher.py
new file mode 100644
index 0000000..767137d
--- /dev/null
+++ b/app/src/runtime_events/publisher.py
@@ -0,0 +1,19 @@
+# app/src/runtime_events/publisher.py
+
+from __future__ import annotations
+
+import asyncio
+
+from src.notifications.service import NotificationService
+from src.runtime_events.models import RuntimeEvent
+
+
+class RuntimeEventPublisher:
+ @classmethod
+ def publish(cls, event: RuntimeEvent) -> None:
+ try:
+ loop = asyncio.get_running_loop()
+ except RuntimeError:
+ return
+
+ loop.create_task(NotificationService().handle_runtime_event(event))
\ No newline at end of file
diff --git a/app/src/trading/auto/runner.py b/app/src/trading/auto/runner.py
index 8cb657f..e3ffaa3 100644
--- a/app/src/trading/auto/runner.py
+++ b/app/src/trading/auto/runner.py
@@ -14,6 +14,7 @@ from src.integrations.exchange.market_data_runner import MarketDataRunner
from src.trading.auto.service import AutoTradeService
from src.trading.journal.service import JournalService
from src.telegram.ui.currency_ui import format_usd_pnl, format_usd_price
+from src.notifications.targets import NotificationTargetRegistry
class AutoTradeRunner:
@@ -56,6 +57,11 @@ class AutoTradeRunner:
cls._render_markup = render_markup
cls._last_text = None
+ NotificationTargetRegistry.set_default_chat(
+ bot=bot,
+ chat_id=chat_id,
+ )
+
@classmethod
async def delete_registered_screen(
cls,
diff --git a/app/src/trading/debug/runner.py b/app/src/trading/debug/runner.py
index d3f5ca9..737508b 100644
--- a/app/src/trading/debug/runner.py
+++ b/app/src/trading/debug/runner.py
@@ -11,6 +11,7 @@ from aiogram.exceptions import TelegramBadRequest, TelegramRetryAfter
from src.integrations.exchange.market_data_runner import MarketDataRunner
from src.trading.debug.service import DebugTradeService
+from src.notifications.targets import NotificationTargetRegistry
class DebugTradeRunner:
@@ -48,6 +49,11 @@ class DebugTradeRunner:
cls._render_markup = render_markup
cls._last_text = None
+ NotificationTargetRegistry.set_default_chat(
+ bot=bot,
+ chat_id=chat_id,
+ )
+
@classmethod
async def delete_registered_screen(
cls,
diff --git a/docs/roadmap/master-roadmap.md b/docs/roadmap/master-roadmap.md
index 6f15499..b34f107 100644
--- a/docs/roadmap/master-roadmap.md
+++ b/docs/roadmap/master-roadmap.md
@@ -315,6 +315,22 @@
- подготовлена архитектура для Telegram push-уведомлений
- подготовлена база для runtime event notifications
+#### 07.4.3.18.1 — Runtime Event Skeleton Architecture
+- добавлен слой runtime_events
+- добавлен слой notifications
+- создана модель RuntimeEvent
+- создана модель NotificationMessage
+- добавлен RuntimeEventPublisher
+- добавлен NotificationService skeleton
+- добавлен TelegramNotificationChannel
+- добавлен NotificationTargetRegistry
+- добавлена базовая дедупликация уведомлений
+- добавлены шаблоны signal/execution notifications
+- зарегистрирован Telegram bot в notification target registry
+- зарегистрирован default chat из AUTO/DEBUG runners
+- исправлен circular import в package init файлах
+- подготовлена архитектура для переноса strong signal alerts
+
### 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 495ede4..18e7a56 100644
--- a/docs/roadmap/stage-07-auto-trading-roadmap.md
+++ b/docs/roadmap/stage-07-auto-trading-roadmap.md
@@ -299,6 +299,22 @@
- подготовлена архитектура для Telegram push-уведомлений
- подготовлена база для runtime event notifications
+#### 07.4.3.18.1 — Runtime Event Skeleton Architecture
+- добавлен слой runtime_events
+- добавлен слой notifications
+- создана модель RuntimeEvent
+- создана модель NotificationMessage
+- добавлен RuntimeEventPublisher
+- добавлен NotificationService skeleton
+- добавлен TelegramNotificationChannel
+- добавлен NotificationTargetRegistry
+- добавлена базовая дедупликация уведомлений
+- добавлены шаблоны signal/execution notifications
+- зарегистрирован Telegram bot в notification target registry
+- зарегистрирован default chat из AUTO/DEBUG runners
+- исправлен circular import в package init файлах
+- подготовлена архитектура для переноса strong signal alerts
+
---
### 07.4.4
diff --git a/docs/stages/ROADMAP_UPDATE_07.4.3.17.md b/docs/stages/ROADMAP_UPDATE_07.4.3.17.md
deleted file mode 100644
index d115ff5..0000000
--- a/docs/stages/ROADMAP_UPDATE_07.4.3.17.md
+++ /dev/null
@@ -1,84 +0,0 @@
-# ROADMAP UPDATE — Этап 07.4.3.17
-
-## Завершено
-
-### Active Screen Lifecycle
-- внедрён единый lifecycle основных экранов;
-- реализовано автоматическое закрытие предыдущего экрана;
-- устранено накопление Telegram UI-сообщений;
-- унифицировано поведение всех основных экранов.
-
----
-
-## Обновлённая архитектура
-
-### ActiveScreenManager
-Теперь отвечает за:
-- регистрацию активного экрана;
-- удаление предыдущего экрана;
-- переключение UI.
-
-### LiveScreenRunner
-Теперь отвечает только за:
-- автообновление live-данных;
-- worker loop;
-- refresh Telegram message.
-
-### Trading Runtime
-Теперь полностью независим от UI:
-- AutoTradeRunner;
-- DebugTradeRunner;
-- future push-events.
-
----
-
-## Исправленные разделы
-
-### Основные экраны
-- Главная
-- Мониторинг
-- Рынок
-- Портфель
-- Журнал
-- Торговля
-- Система
-- Автоторговля
-- Debug Auto
-
----
-
-## Новое поведение системы
-
-### UI
-- всегда существует только один основной экран;
-- при открытии нового старый удаляется автоматически.
-
-### Background Runtime
-- продолжает работать независимо от UI;
-- готов к Telegram push-событиям.
-
----
-
-# Следующий этап
-
-## Planned
-
-### Telegram Event Push Layer
-Планируется внедрение:
-- уведомлений об открытии позиции;
-- уведомлений о закрытии позиции;
-- уведомлений о сигналах;
-- runtime alerts;
-- system alerts;
-- execution events.
-
----
-
-## Архитектурная готовность
-
-Система теперь готова к:
-- background runtime;
-- multi-event notifications;
-- persistent runtime state;
-- restart-safe workflows;
-- production lifecycle management.
diff --git a/docs/stages/stage-07_4_3_18_1-runtime_event_skeleton_architecture.md b/docs/stages/stage-07_4_3_18_1-runtime_event_skeleton_architecture.md
new file mode 100644
index 0000000..1f9ecc3
--- /dev/null
+++ b/docs/stages/stage-07_4_3_18_1-runtime_event_skeleton_architecture.md
@@ -0,0 +1,213 @@
+# 07.4.3.18.1 — Skeleton Architecture
+
+## Цель этапа
+
+Заложить базовую архитектуру слоя runtime-событий и Telegram-уведомлений без переноса существующей бизнес-логики уведомлений.
+
+Главная задача этапа — подготовить ядро, чтобы в следующих шагах можно было переносить уведомления из `AutoTradeRunner`, `DebugTradeRunner`, `ExecutionEngine` и других runtime-компонентов без переписывания архитектуры.
+
+## Что добавлено
+
+### Runtime Events Layer
+
+Создан новый слой:
+
+```text
+app/src/runtime_events/
+ __init__.py
+ event_types.py
+ models.py
+ publisher.py
+```
+
+Назначение слоя:
+
+```text
+runtime logic
+ ↓
+RuntimeEvent
+ ↓
+RuntimeEventPublisher
+```
+
+Этот слой отвечает за публикацию событий runtime-уровня, но не знает деталей Telegram, UI или конкретных шаблонов сообщений.
+
+### Notification Layer
+
+Создан новый слой:
+
+```text
+app/src/notifications/
+ __init__.py
+ models.py
+ service.py
+ targets.py
+ dedupe.py
+ templates/
+ __init__.py
+ signal.py
+ execution.py
+ channels/
+ __init__.py
+ telegram.py
+```
+
+Назначение слоя:
+
+```text
+RuntimeEvent
+ ↓
+NotificationService
+ ↓
+Notification template
+ ↓
+TelegramNotificationChannel
+```
+
+Этот слой отвечает за:
+- выбор шаблона уведомления;
+- дедупликацию уведомлений;
+- отправку в Telegram;
+- логирование результата доставки;
+- централизованное хранение Telegram target.
+
+## Добавленные runtime event types
+
+Добавлен enum `RuntimeEventType`:
+
+```text
+AUTO_SIGNAL_READY
+POSITION_OPENED
+POSITION_CLOSED
+POSITION_FLIPPED
+EXECUTION_BLOCKED
+RISK_ALERT
+SYSTEM_ALERT
+```
+
+На этапе 07.4.3.18.1 эти события только описаны архитектурно. Массовый перенос runtime-логики на них будет сделан следующими подэтапами.
+
+## Добавлена модель RuntimeEvent
+
+Добавлена единая модель события:
+
+```text
+RuntimeEvent
+ event_type
+ source
+ title
+ payload
+ priority
+ dedupe_key
+ created_at
+```
+
+Это будущий контракт между торговым runtime и системой уведомлений.
+
+## Добавлена модель NotificationMessage
+
+Добавлена единая модель готового уведомления:
+
+```text
+NotificationMessage
+ title
+ text
+ priority
+ parse_mode
+ dedupe_key
+```
+
+Эта модель отделяет runtime-событие от конкретного Telegram-сообщения.
+
+## Добавлен NotificationTargetRegistry
+
+Создан in-memory registry для хранения активной Telegram-цели уведомлений:
+
+```text
+NotificationTargetRegistry
+ set_bot()
+ set_default_chat()
+ get_bot()
+ get_default_chat_id()
+ is_ready()
+```
+
+Пока target хранится в памяти. В будущем его можно перенести в БД без изменения runtime-слоя.
+
+## Интеграция с app bootstrap
+
+В `app/src/bootstrap/app_factory.py` добавлена регистрация `Bot` в `NotificationTargetRegistry` сразу после создания Telegram Bot.
+
+Это даёт notification layer доступ к bot instance без прямой зависимости от UI handler-ов.
+
+## Интеграция с AUTO/DEBUG runner
+
+В `AutoTradeRunner.register_screen()` и `DebugTradeRunner.register_screen()` добавлена регистрация текущего `chat_id` как default notification target.
+
+Это позволяет будущим runtime-уведомлениям знать, куда отправлять сообщения.
+
+## Что не менялось на этом этапе
+
+На этом этапе намеренно не переносились:
+
+- strong signal alerts;
+- paper execution alerts;
+- risk alerts;
+- Telegram send_message из существующих runner-ов;
+- cooldown-логика strong signal;
+- execution alert dedupe.
+
+Эти изменения будут выполняться поэтапно, чтобы не сломать текущую рабочую логику.
+
+## Исправление circular import
+
+После первичной интеграции был найден circular import:
+
+```text
+notifications.__init__
+ → NotificationService
+ → runtime_events
+ → RuntimeEventPublisher
+ → NotificationService
+```
+
+Исправление:
+
+- из `runtime_events/__init__.py` убран экспорт `RuntimeEventPublisher`;
+- из `notifications/__init__.py` убран экспорт `NotificationService`.
+
+Теперь прямые импорты выполняются из конкретных модулей:
+
+```python
+from src.runtime_events.publisher import RuntimeEventPublisher
+from src.notifications.service import NotificationService
+```
+
+## Проверка
+
+Проверено:
+
+```bash
+python -m compileall src
+python -m src.main
+```
+
+Ожидаемое поведение после этапа:
+
+- бот запускается без ImportError;
+- основные экраны открываются;
+- текущая UI lifecycle логика не меняется;
+- новых runtime Telegram-уведомлений пока не появляется;
+- notification target регистрируется при открытии AUTO/DEBUG экранов.
+
+## Итог
+
+Этап 07.4.3.18.1 завершает подготовку архитектурного skeleton для будущего runtime notification pipeline.
+
+Следующий этап:
+
+```text
+07.4.3.18.2 — Move strong signal alerts
+```
+
+На нём strong signal alerts будут перенесены из `AutoTradeRunner` в новый runtime event / notification слой.