07.4.3.18.1 — Runtime Event Skeleton Architecture
This commit is contained in:
@@ -7,22 +7,19 @@ from aiogram.client.default import DefaultBotProperties
|
|||||||
|
|
||||||
from src.bootstrap.logging import setup_logging
|
from src.bootstrap.logging import setup_logging
|
||||||
from src.core.config import load_settings
|
from src.core.config import load_settings
|
||||||
|
from src.notifications.targets import NotificationTargetRegistry
|
||||||
from src.storage.schema import init_schema
|
from src.storage.schema import init_schema
|
||||||
from src.telegram.routers import setup_routers
|
from src.telegram.routers import setup_routers
|
||||||
from src.trading.journal.service import JournalService
|
from src.trading.journal.service import JournalService
|
||||||
|
|
||||||
|
|
||||||
def create_app() -> tuple[Bot, Dispatcher]:
|
def create_app() -> tuple[Bot, Dispatcher]:
|
||||||
# загружаем настройки приложения
|
|
||||||
settings = load_settings()
|
settings = load_settings()
|
||||||
|
|
||||||
# настраиваем logging
|
|
||||||
setup_logging(settings.log_level)
|
setup_logging(settings.log_level)
|
||||||
|
|
||||||
# сервис журнала
|
|
||||||
journal = JournalService()
|
journal = JournalService()
|
||||||
|
|
||||||
# инициализация схемы БД
|
|
||||||
try:
|
try:
|
||||||
init_schema()
|
init_schema()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -40,7 +37,6 @@ def create_app() -> tuple[Bot, Dispatcher]:
|
|||||||
pass
|
pass
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# лог старта приложения
|
|
||||||
try:
|
try:
|
||||||
journal.log_info(
|
journal.log_info(
|
||||||
"app_start",
|
"app_start",
|
||||||
@@ -52,24 +48,17 @@ def create_app() -> tuple[Bot, Dispatcher]:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
# журнал не должен ломать запуск приложения
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# здесь позже можно инициализировать stream/cache сервисы:
|
|
||||||
# init_market_cache()
|
|
||||||
# init_market_stream()
|
|
||||||
# init_auto_trade_runner()
|
|
||||||
|
|
||||||
# создаем Telegram Bot
|
|
||||||
bot = Bot(
|
bot = Bot(
|
||||||
token=settings.bot_token,
|
token=settings.bot_token,
|
||||||
default=DefaultBotProperties(parse_mode=settings.bot_parse_mode),
|
default=DefaultBotProperties(parse_mode=settings.bot_parse_mode),
|
||||||
)
|
)
|
||||||
|
|
||||||
# создаем Dispatcher
|
NotificationTargetRegistry.set_bot(bot)
|
||||||
|
|
||||||
dispatcher = Dispatcher()
|
dispatcher = Dispatcher()
|
||||||
|
|
||||||
# подключаем routers
|
|
||||||
setup_routers(dispatcher)
|
setup_routers(dispatcher)
|
||||||
|
|
||||||
return bot, dispatcher
|
return bot, dispatcher
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# app/src/bootstrap/logging.py
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -7,4 +9,4 @@ def setup_logging(log_level: str) -> None:
|
|||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=getattr(logging, log_level.upper(), logging.INFO),
|
level=getattr(logging, log_level.upper(), logging.INFO),
|
||||||
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
|
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
|
||||||
)
|
)
|
||||||
9
app/src/notifications/__init__.py
Normal file
9
app/src/notifications/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# app/src/notifications/__init__.py
|
||||||
|
|
||||||
|
from src.notifications.models import NotificationMessage
|
||||||
|
from src.notifications.targets import NotificationTargetRegistry
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"NotificationMessage",
|
||||||
|
"NotificationTargetRegistry",
|
||||||
|
]
|
||||||
5
app/src/notifications/channels/__init__.py
Normal file
5
app/src/notifications/channels/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# app/src/notifications/channels/__init__.py
|
||||||
|
|
||||||
|
from src.notifications.channels.telegram import TelegramNotificationChannel
|
||||||
|
|
||||||
|
__all__ = ["TelegramNotificationChannel"]
|
||||||
62
app/src/notifications/channels/telegram.py
Normal file
62
app/src/notifications/channels/telegram.py
Normal file
@@ -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
|
||||||
23
app/src/notifications/dedupe.py
Normal file
23
app/src/notifications/dedupe.py
Normal file
@@ -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
|
||||||
14
app/src/notifications/models.py
Normal file
14
app/src/notifications/models.py
Normal file
@@ -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
|
||||||
65
app/src/notifications/service.py
Normal file
65
app/src/notifications/service.py
Normal file
@@ -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)
|
||||||
|
)
|
||||||
33
app/src/notifications/targets.py
Normal file
33
app/src/notifications/targets.py
Normal file
@@ -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
|
||||||
9
app/src/notifications/templates/__init__.py
Normal file
9
app/src/notifications/templates/__init__.py
Normal file
@@ -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",
|
||||||
|
]
|
||||||
54
app/src/notifications/templates/execution.py
Normal file
54
app/src/notifications/templates/execution.py
Normal file
@@ -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"<b>{title}</b>",
|
||||||
|
"",
|
||||||
|
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,
|
||||||
|
)
|
||||||
39
app/src/notifications/templates/signal.py
Normal file
39
app/src/notifications/templates/signal.py
Normal file
@@ -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"<b>🚨 Runtime Signal · {icon} {signal}</b>\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,
|
||||||
|
)
|
||||||
9
app/src/runtime_events/__init__.py
Normal file
9
app/src/runtime_events/__init__.py
Normal file
@@ -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",
|
||||||
|
]
|
||||||
17
app/src/runtime_events/event_types.py
Normal file
17
app/src/runtime_events/event_types.py
Normal file
@@ -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"
|
||||||
20
app/src/runtime_events/models.py
Normal file
20
app/src/runtime_events/models.py
Normal file
@@ -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))
|
||||||
19
app/src/runtime_events/publisher.py
Normal file
19
app/src/runtime_events/publisher.py
Normal file
@@ -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))
|
||||||
@@ -14,6 +14,7 @@ from src.integrations.exchange.market_data_runner import MarketDataRunner
|
|||||||
from src.trading.auto.service import AutoTradeService
|
from src.trading.auto.service import AutoTradeService
|
||||||
from src.trading.journal.service import JournalService
|
from src.trading.journal.service import JournalService
|
||||||
from src.telegram.ui.currency_ui import format_usd_pnl, format_usd_price
|
from src.telegram.ui.currency_ui import format_usd_pnl, format_usd_price
|
||||||
|
from src.notifications.targets import NotificationTargetRegistry
|
||||||
|
|
||||||
|
|
||||||
class AutoTradeRunner:
|
class AutoTradeRunner:
|
||||||
@@ -56,6 +57,11 @@ class AutoTradeRunner:
|
|||||||
cls._render_markup = render_markup
|
cls._render_markup = render_markup
|
||||||
cls._last_text = None
|
cls._last_text = None
|
||||||
|
|
||||||
|
NotificationTargetRegistry.set_default_chat(
|
||||||
|
bot=bot,
|
||||||
|
chat_id=chat_id,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def delete_registered_screen(
|
async def delete_registered_screen(
|
||||||
cls,
|
cls,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from aiogram.exceptions import TelegramBadRequest, TelegramRetryAfter
|
|||||||
|
|
||||||
from src.integrations.exchange.market_data_runner import MarketDataRunner
|
from src.integrations.exchange.market_data_runner import MarketDataRunner
|
||||||
from src.trading.debug.service import DebugTradeService
|
from src.trading.debug.service import DebugTradeService
|
||||||
|
from src.notifications.targets import NotificationTargetRegistry
|
||||||
|
|
||||||
|
|
||||||
class DebugTradeRunner:
|
class DebugTradeRunner:
|
||||||
@@ -48,6 +49,11 @@ class DebugTradeRunner:
|
|||||||
cls._render_markup = render_markup
|
cls._render_markup = render_markup
|
||||||
cls._last_text = None
|
cls._last_text = None
|
||||||
|
|
||||||
|
NotificationTargetRegistry.set_default_chat(
|
||||||
|
bot=bot,
|
||||||
|
chat_id=chat_id,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def delete_registered_screen(
|
async def delete_registered_screen(
|
||||||
cls,
|
cls,
|
||||||
|
|||||||
@@ -315,6 +315,22 @@
|
|||||||
- подготовлена архитектура для Telegram push-уведомлений
|
- подготовлена архитектура для Telegram push-уведомлений
|
||||||
- подготовлена база для runtime event notifications
|
- подготовлена база для 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
|
### 07.4.4
|
||||||
⏳ Grid Strategy
|
⏳ Grid Strategy
|
||||||
|
|
||||||
|
|||||||
@@ -299,6 +299,22 @@
|
|||||||
- подготовлена архитектура для Telegram push-уведомлений
|
- подготовлена архитектура для Telegram push-уведомлений
|
||||||
- подготовлена база для runtime event notifications
|
- подготовлена база для 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
|
### 07.4.4
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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 слой.
|
||||||
Reference in New Issue
Block a user