07.4.3.19.4 — Journal Runtime Standardization & Export Layer
This commit is contained in:
@@ -39,8 +39,8 @@ def create_app() -> tuple[Bot, Dispatcher]:
|
||||
|
||||
try:
|
||||
journal.log_info(
|
||||
"app_start",
|
||||
"Приложение запущено.",
|
||||
"app_started",
|
||||
"Приложение запущено",
|
||||
{
|
||||
"env": settings.app_env,
|
||||
"exchange_name": settings.exchange_name,
|
||||
|
||||
@@ -46,13 +46,6 @@ class MarketDataRunner:
|
||||
existing.screen = screen
|
||||
existing.action = action
|
||||
existing.runtime_label = runtime_label
|
||||
|
||||
cls._log_info(
|
||||
existing,
|
||||
"market_runner_context_updated",
|
||||
"MarketDataRunner context updated.",
|
||||
{"interval_seconds": interval_seconds},
|
||||
)
|
||||
return
|
||||
|
||||
context = MarketRuntimeContext(
|
||||
@@ -69,8 +62,8 @@ class MarketDataRunner:
|
||||
|
||||
cls._log_info(
|
||||
context,
|
||||
"market_runner_started",
|
||||
"MarketDataRunner started.",
|
||||
"market_monitor_started",
|
||||
"Мониторинг рынка запущен.",
|
||||
{"interval_seconds": interval_seconds},
|
||||
)
|
||||
|
||||
@@ -93,8 +86,8 @@ class MarketDataRunner:
|
||||
|
||||
cls._log_info(
|
||||
context,
|
||||
"market_runner_stopped",
|
||||
"MarketDataRunner stopped.",
|
||||
"market_monitor_stopped",
|
||||
"Мониторинг рынка остановлен.",
|
||||
)
|
||||
|
||||
cls._runtimes.pop(runtime_key, None)
|
||||
@@ -107,11 +100,6 @@ class MarketDataRunner:
|
||||
symbol = context.symbol_provider()
|
||||
|
||||
if not symbol:
|
||||
cls._log_warning(
|
||||
context,
|
||||
"market_runner_no_symbol",
|
||||
"MarketDataRunner has no symbol.",
|
||||
)
|
||||
await asyncio.sleep(context.interval_seconds)
|
||||
continue
|
||||
|
||||
@@ -129,8 +117,8 @@ class MarketDataRunner:
|
||||
|
||||
cls._log_info(
|
||||
context,
|
||||
"market_runner_symbol_changed",
|
||||
"MarketDataRunner symbol changed.",
|
||||
"market_symbol_changed",
|
||||
f"Инструмент автоторговли изменён на {cache_symbol}.",
|
||||
{
|
||||
"symbol": symbol,
|
||||
"cache_symbol": cache_symbol,
|
||||
@@ -143,10 +131,10 @@ class MarketDataRunner:
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
cls._log_error(
|
||||
cls._log_warning(
|
||||
context,
|
||||
"market_ws_error_fallback",
|
||||
"WebSocket market data failed. Falling back to REST.",
|
||||
"market_stream_disconnected",
|
||||
"Поток рыночных данных отключён. Используется резервный REST-режим.",
|
||||
{
|
||||
"symbol": symbol,
|
||||
"cache_symbol": cache_symbol,
|
||||
@@ -165,17 +153,6 @@ class MarketDataRunner:
|
||||
cache_symbol = cls._cache_symbol(symbol)
|
||||
ws_symbol = cls._ws_symbol(symbol)
|
||||
|
||||
cls._log_info(
|
||||
context,
|
||||
"market_ws_connecting",
|
||||
"Connecting market WebSocket.",
|
||||
{
|
||||
"requested_symbol": symbol,
|
||||
"cache_symbol": cache_symbol,
|
||||
"ws_symbol": ws_symbol,
|
||||
},
|
||||
)
|
||||
|
||||
payload_count = 0
|
||||
|
||||
async for payload in ExchangeWebSocketClient().stream_depth(
|
||||
@@ -185,8 +162,8 @@ class MarketDataRunner:
|
||||
if payload_count == 0:
|
||||
cls._log_info(
|
||||
context,
|
||||
"market_ws_connected",
|
||||
"Market WebSocket connected and first payload received.",
|
||||
"market_stream_connected",
|
||||
"Поток рыночных данных подключён.",
|
||||
{
|
||||
"requested_symbol": symbol,
|
||||
"cache_symbol": cache_symbol,
|
||||
@@ -200,33 +177,12 @@ class MarketDataRunner:
|
||||
|
||||
current_symbol = context.symbol_provider()
|
||||
if current_symbol and current_symbol != symbol:
|
||||
cls._log_info(
|
||||
context,
|
||||
"market_ws_symbol_switch",
|
||||
"Market WebSocket stopped because symbol changed.",
|
||||
{
|
||||
"old_symbol": symbol,
|
||||
"new_symbol": current_symbol,
|
||||
},
|
||||
)
|
||||
break
|
||||
|
||||
best_bid = cls._extract_best_price(payload, "bids")
|
||||
best_ask = cls._extract_best_price(payload, "asks")
|
||||
|
||||
if best_bid is None or best_ask is None:
|
||||
cls._log_warning(
|
||||
context,
|
||||
"market_ws_payload_unrecognized",
|
||||
"Market WebSocket payload does not contain recognizable bids/asks.",
|
||||
{
|
||||
"requested_symbol": symbol,
|
||||
"cache_symbol": cache_symbol,
|
||||
"ws_symbol": ws_symbol,
|
||||
"payload_keys": list(payload.keys()),
|
||||
"payload_preview": cls._safe_payload_preview(payload),
|
||||
},
|
||||
)
|
||||
continue
|
||||
|
||||
MarketPriceCache.set_price(
|
||||
@@ -241,30 +197,16 @@ class MarketDataRunner:
|
||||
@classmethod
|
||||
async def _rest_fallback_once(cls, context: MarketRuntimeContext, symbol: str) -> None:
|
||||
try:
|
||||
snapshot = await asyncio.to_thread(
|
||||
await asyncio.to_thread(
|
||||
ExchangeService().refresh_market_snapshot_cache,
|
||||
symbol,
|
||||
runtime_key=context.runtime_key,
|
||||
)
|
||||
|
||||
cls._log_warning(
|
||||
context,
|
||||
"market_rest_fallback_success",
|
||||
"REST fallback market snapshot loaded.",
|
||||
{
|
||||
"symbol": symbol,
|
||||
"snapshot_symbol": snapshot.get("symbol"),
|
||||
"source": snapshot.get("source"),
|
||||
"last_price": snapshot.get("last_price"),
|
||||
"bid_price": snapshot.get("bid_price"),
|
||||
"ask_price": snapshot.get("ask_price"),
|
||||
},
|
||||
)
|
||||
except Exception as exc:
|
||||
cls._log_error(
|
||||
context,
|
||||
"market_rest_fallback_error",
|
||||
"REST fallback market snapshot failed.",
|
||||
"market_stream_disconnected",
|
||||
"Поток рыночных данных отключён. Резервный REST-режим недоступен.",
|
||||
{
|
||||
"symbol": symbol,
|
||||
"error": str(exc),
|
||||
|
||||
@@ -16,8 +16,8 @@ class TelegramNotificationChannel:
|
||||
|
||||
if bot is None or chat_id is None:
|
||||
JournalService().log_warning(
|
||||
"notification_target_missing",
|
||||
"Telegram notification target is not registered.",
|
||||
"notification_error",
|
||||
"Не удалось отправить Telegram-уведомление: получатель не настроен.",
|
||||
{
|
||||
"title": message.title,
|
||||
"priority": message.priority,
|
||||
@@ -36,8 +36,8 @@ class TelegramNotificationChannel:
|
||||
|
||||
except TelegramRetryAfter as exc:
|
||||
JournalService().log_warning(
|
||||
"notification_telegram_retry_after",
|
||||
"Telegram notification delayed by retry-after.",
|
||||
"notification_error",
|
||||
"Не удалось отправить Telegram-уведомление: Telegram ограничил частоту отправки.",
|
||||
{
|
||||
"title": message.title,
|
||||
"retry_after": exc.retry_after,
|
||||
@@ -49,8 +49,8 @@ class TelegramNotificationChannel:
|
||||
|
||||
except Exception as exc:
|
||||
JournalService().log_error(
|
||||
"notification_telegram_error",
|
||||
"Telegram notification failed.",
|
||||
"notification_error",
|
||||
"Не удалось отправить Telegram-уведомление.",
|
||||
{
|
||||
"title": message.title,
|
||||
"error": str(exc),
|
||||
|
||||
@@ -7,9 +7,7 @@ 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.event_types import RuntimeEventType
|
||||
from src.runtime_events.models import RuntimeEvent
|
||||
from src.trading.journal.service import JournalService
|
||||
|
||||
|
||||
class NotificationService:
|
||||
@@ -17,112 +15,15 @@ class NotificationService:
|
||||
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):
|
||||
self._log_suppressed(event, message)
|
||||
return
|
||||
|
||||
sent = await TelegramNotificationChannel().send(message)
|
||||
|
||||
if sent:
|
||||
self._log_sent(event, message)
|
||||
await TelegramNotificationChannel().send(message)
|
||||
|
||||
def _build_message(self, event: RuntimeEvent) -> NotificationMessage | None:
|
||||
return (
|
||||
build_signal_notification(event)
|
||||
or build_execution_notification(event)
|
||||
)
|
||||
|
||||
def _log_sent(self, event: RuntimeEvent, message: NotificationMessage) -> None:
|
||||
if event.event_type == RuntimeEventType.AUTO_SIGNAL_READY:
|
||||
signal = str(event.payload.get("signal") or "—").upper()
|
||||
|
||||
JournalService().log_ui_info(
|
||||
event_type="auto_strong_signal_alert_sent",
|
||||
message=f"Отправлено уведомление о сильном сигнале {signal}.",
|
||||
screen="auto",
|
||||
action="strong_signal_alert",
|
||||
payload={
|
||||
**event.payload,
|
||||
"runtime_event_type": event.event_type.value,
|
||||
"runtime_source": event.source,
|
||||
"priority": message.priority.upper(),
|
||||
"dedupe_key": message.dedupe_key,
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
if event.event_type in {
|
||||
RuntimeEventType.POSITION_OPENED,
|
||||
RuntimeEventType.POSITION_CLOSED,
|
||||
RuntimeEventType.POSITION_FLIPPED,
|
||||
}:
|
||||
JournalService().log_ui_info(
|
||||
event_type="auto_execution_alert_sent",
|
||||
message="Отправлено Telegram-уведомление по paper execution.",
|
||||
screen="auto",
|
||||
action="execution_alert",
|
||||
payload={
|
||||
**event.payload,
|
||||
"runtime_event_type": event.event_type.value,
|
||||
"runtime_source": event.source,
|
||||
"priority": message.priority.upper(),
|
||||
"dedupe_key": message.dedupe_key,
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
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 _log_suppressed(self, event: RuntimeEvent, message: NotificationMessage) -> None:
|
||||
if event.event_type == RuntimeEventType.AUTO_SIGNAL_READY:
|
||||
signal = str(event.payload.get("signal") or "—").upper()
|
||||
|
||||
JournalService().log_ui_info(
|
||||
event_type="auto_strong_signal_alert_suppressed",
|
||||
message=f"Повторное уведомление о сильном сигнале {signal} подавлено.",
|
||||
screen="auto",
|
||||
action="strong_signal_alert",
|
||||
payload={
|
||||
**event.payload,
|
||||
"runtime_event_type": event.event_type.value,
|
||||
"runtime_source": event.source,
|
||||
"priority": message.priority.upper(),
|
||||
"dedupe_key": message.dedupe_key,
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
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,
|
||||
},
|
||||
)
|
||||
@@ -207,13 +207,8 @@ def _log_risk_updated(action: str) -> None:
|
||||
|
||||
try:
|
||||
JournalService().log_ui_info(
|
||||
event_type="auto_risk_settings_updated",
|
||||
message=(
|
||||
"Risk settings updated: "
|
||||
f"SL={state.stop_loss_percent}, "
|
||||
f"TP={state.take_profit_percent}, "
|
||||
f"ML={state.max_loss_usd}"
|
||||
),
|
||||
event_type="risk_settings_updated",
|
||||
message="Параметры защиты позиции обновлены.",
|
||||
screen="auto",
|
||||
action=action,
|
||||
payload={
|
||||
|
||||
@@ -126,15 +126,6 @@ async def open_journal(message: Message, state: FSMContext) -> None:
|
||||
chat_id=message.chat.id,
|
||||
)
|
||||
|
||||
JournalService().log_ui_info(
|
||||
event_type="journal_open_requested",
|
||||
message="Запрошено открытие журнала.",
|
||||
screen="journal",
|
||||
action="open",
|
||||
user_id=_user_id_from_message(message),
|
||||
chat_id=_chat_id_from_message(message),
|
||||
)
|
||||
|
||||
await _show_journal_page(
|
||||
message,
|
||||
page=1,
|
||||
@@ -157,15 +148,6 @@ async def open_journal_from_monitoring(callback: CallbackQuery, state: FSMContex
|
||||
keep_message_id=callback.message.message_id,
|
||||
)
|
||||
|
||||
JournalService().log_ui_info(
|
||||
event_type="journal_open_requested",
|
||||
message="Запрошено открытие журнала из мониторинга.",
|
||||
screen="journal",
|
||||
action="open_from_monitoring",
|
||||
user_id=_user_id_from_callback(callback),
|
||||
chat_id=_chat_id_from_callback(callback),
|
||||
)
|
||||
|
||||
await _show_journal_page(
|
||||
callback.message,
|
||||
page=1,
|
||||
@@ -195,7 +177,7 @@ async def export_journal_csv(callback: CallbackQuery) -> None:
|
||||
await callback.message.answer_document(document=document)
|
||||
|
||||
service.log_ui_info(
|
||||
event_type="journal_export_csv_success",
|
||||
event_type="journal_exported",
|
||||
message="Журнал экспортирован в CSV.",
|
||||
screen="journal",
|
||||
action="export_csv",
|
||||
@@ -207,12 +189,13 @@ async def export_journal_csv(callback: CallbackQuery) -> None:
|
||||
await callback.answer("CSV экспортирован")
|
||||
except Exception as exc:
|
||||
service.log_ui_error(
|
||||
event_type="journal_export_csv_error",
|
||||
event_type="journal_export_error",
|
||||
message="Не удалось экспортировать журнал в CSV.",
|
||||
screen="journal",
|
||||
action="export_csv",
|
||||
user_id=_user_id_from_callback(callback),
|
||||
chat_id=_chat_id_from_callback(callback),
|
||||
payload={"format": "csv"},
|
||||
raw_error=str(exc),
|
||||
)
|
||||
await callback.answer("Не удалось экспортировать CSV", show_alert=True)
|
||||
@@ -233,8 +216,8 @@ async def export_journal_xlsx(callback: CallbackQuery) -> None:
|
||||
await callback.message.answer_document(document=document)
|
||||
|
||||
service.log_ui_info(
|
||||
event_type="journal_export_xlsx_success",
|
||||
message="Журнал экспортирован в Excel.",
|
||||
event_type="journal_exported",
|
||||
message="Журнал экспортирован в XLSX.",
|
||||
screen="journal",
|
||||
action="export_xlsx",
|
||||
user_id=_user_id_from_callback(callback),
|
||||
@@ -245,12 +228,13 @@ async def export_journal_xlsx(callback: CallbackQuery) -> None:
|
||||
await callback.answer("Excel экспортирован")
|
||||
except Exception as exc:
|
||||
service.log_ui_error(
|
||||
event_type="journal_export_xlsx_error",
|
||||
message="Не удалось экспортировать журнал в Excel.",
|
||||
event_type="journal_export_error",
|
||||
message="Не удалось экспортировать журнал в XLSX.",
|
||||
screen="journal",
|
||||
action="export_xlsx",
|
||||
user_id=_user_id_from_callback(callback),
|
||||
chat_id=_chat_id_from_callback(callback),
|
||||
payload={"format": "xlsx"},
|
||||
raw_error=str(exc),
|
||||
)
|
||||
await callback.answer("Не удалось экспортировать Excel", show_alert=True)
|
||||
|
||||
@@ -21,65 +21,25 @@ LEVEL_ICONS = {
|
||||
}
|
||||
|
||||
EVENT_TITLES = {
|
||||
"auto_signal_generated": "Сигнал автоторговли",
|
||||
"auto_signal_summary": "Итог серии сигналов",
|
||||
"app_start": "Запуск приложения",
|
||||
"system_open_alert": "Система загружена с предупреждениями",
|
||||
"system_open_requested": "Открытие системы",
|
||||
"system_open_success": "Система загружена",
|
||||
"system_retry": "Система обновлена",
|
||||
"market_open_requested": "Открытие рынка",
|
||||
"market_open_success": "Рынок загружен",
|
||||
"market_open_error": "Ошибка открытия рынка",
|
||||
"market_retry_error": "Ошибка обновления рынка",
|
||||
"market_symbol_invalid": "Некорректный инструмент",
|
||||
"market_price_error": "Ошибка загрузки цены",
|
||||
"portfolio_open_requested": "Открытие портфеля",
|
||||
"portfolio_open_success": "Портфель загружен",
|
||||
"portfolio_open_error": "Ошибка открытия портфеля",
|
||||
"portfolio_retry_error": "Ошибка обновления портфеля",
|
||||
"portfolio_empty": "Портфель пуст",
|
||||
"portfolio_zero_balances": "Нет активов с балансом",
|
||||
"portfolio_partial_estimate": "Частичная оценка портфеля",
|
||||
"balance_summary_loaded": "Баланс загружен",
|
||||
"balance_summary_empty": "Баланс пуст",
|
||||
"balance_summary_error": "Ошибка загрузки баланса",
|
||||
"exchange_request_error": "Ошибка запроса к бирже",
|
||||
"trade_drafts_open": "Открытие списка черновиков",
|
||||
"trade_drafts_paginate": "Переключение страницы черновиков",
|
||||
"trade_draft_open_success": "Черновик открыт",
|
||||
"trade_draft_open_not_found": "Черновик не найден",
|
||||
"trade_draft_edit_start": "Начато редактирование черновика",
|
||||
"trade_draft_edit_error": "Ошибка редактирования черновика",
|
||||
"trade_draft_edit_not_found": "Черновик не найден",
|
||||
"trade_order_create_start": "Начато создание ордера",
|
||||
"trade_order_create_start_error": "Ошибка создания ордера",
|
||||
"trade_order_create_cancelled": "Создание ордера отменено",
|
||||
"trade_order_side_selected": "Выбрана сторона ордера",
|
||||
"trade_order_side_select_error": "Ошибка выбора стороны",
|
||||
"trade_order_type_selected": "Выбран тип ордера",
|
||||
"trade_order_type_select_error": "Ошибка выбора типа",
|
||||
"trade_order_quantity_selected": "Выбрано количество",
|
||||
"trade_order_quantity_select_error": "Ошибка выбора количества",
|
||||
"trade_order_quantity_manual_open": "Ручной ввод количества",
|
||||
"trade_order_quantity_manual_success": "Количество введено",
|
||||
"trade_order_quantity_manual_error": "Ошибка ввода количества",
|
||||
"trade_order_price_selected": "Выбрана цена",
|
||||
"trade_order_price_select_error": "Ошибка выбора цены",
|
||||
"trade_order_price_manual_open": "Ручной ввод цены",
|
||||
"trade_order_price_manual_success": "Цена введена",
|
||||
"trade_order_price_manual_error": "Ошибка ввода цены",
|
||||
"trade_order_confirm_success": "Черновик сохранён",
|
||||
"trade_order_confirm_error": "Ошибка сохранения",
|
||||
"trade_order_confirm_validation_error": "Ошибка проверки",
|
||||
"trade_order_confirm_state_error": "Ошибка состояния",
|
||||
"signal_summary": "Сводка сигнала",
|
||||
"signal_ready": "Сигнал готов",
|
||||
"position_opened": "Позиция открыта",
|
||||
"position_closed": "Позиция закрыта",
|
||||
"position_flipped": "Направление позиции изменено",
|
||||
"position_flip_blocked": "Смена направления позиции заблокирована",
|
||||
"risk_settings_updated": "Настройки защиты обновлены",
|
||||
"market_monitor_started": "Мониторинг рынка запущен",
|
||||
"market_monitor_stopped": "Мониторинг рынка остановлен",
|
||||
"market_stream_connected": "Поток рынка подключён",
|
||||
"market_stream_disconnected": "Поток рынка отключён",
|
||||
"market_symbol_changed": "Инструмент рынка изменён",
|
||||
"journal_export_error": "Ошибка экспорта журнала",
|
||||
"journal_exported": "Журнал экспортирован",
|
||||
"journal_cleared": "Журнал очищен",
|
||||
"journal_cleared_old": "Журнал очищен по сроку",
|
||||
"journal_export_csv_success": "Экспорт CSV",
|
||||
"journal_export_csv_error": "Ошибка экспорта CSV",
|
||||
"journal_export_xlsx_success": "Экспорт Excel",
|
||||
"journal_export_xlsx_error": "Ошибка экспорта Excel",
|
||||
"journal_open_requested": "Открытие журнала",
|
||||
"notification_sent": "Уведомление отправлено",
|
||||
"notification_error": "Ошибка уведомления",
|
||||
"app_started": "Приложение запущено",
|
||||
"app_bootstrap_failed": "Ошибка запуска приложения",
|
||||
}
|
||||
|
||||
TECH_TO_HUMAN_MESSAGES = {
|
||||
@@ -221,42 +181,18 @@ def _payload(event: dict) -> dict:
|
||||
|
||||
|
||||
def _render_auto_signal(event: dict, created_time: str) -> list[str]:
|
||||
payload = _payload(event)
|
||||
|
||||
signal = str(payload.get("signal", "HOLD")).upper()
|
||||
strategy = str(payload.get("strategy", "AUTO")).upper()
|
||||
symbol = str(payload.get("symbol", "—"))
|
||||
reason = str(payload.get("reason", ""))
|
||||
confidence = float(payload.get("confidence", 0.0) or 0.0)
|
||||
repeat_count = int(payload.get("repeat_count", 1) or 1)
|
||||
is_strong_signal = bool(payload.get("is_strong_signal", False))
|
||||
is_aggregated = bool(payload.get("is_aggregated", False))
|
||||
|
||||
signal_icon = {
|
||||
"BUY": "🟢",
|
||||
"SELL": "🔴",
|
||||
"HOLD": "🟡",
|
||||
}.get(signal, "•")
|
||||
|
||||
prefix = ""
|
||||
if is_strong_signal:
|
||||
prefix += "📈 "
|
||||
if is_aggregated:
|
||||
prefix += "🧠 "
|
||||
level = str(event.get("level", "INFO")).upper()
|
||||
icon = LEVEL_ICONS.get(level, "•")
|
||||
title = _event_title(str(event.get("event_type", "")))
|
||||
message = _humanize_message(str(event.get("message", "")))
|
||||
|
||||
lines = [
|
||||
f"{prefix}{signal_icon} <b>AUTO · {signal}</b>",
|
||||
f"{created_time} · {strategy} · {symbol}",
|
||||
f"{icon} <b>{level}</b> · {title}",
|
||||
f"{created_time}",
|
||||
]
|
||||
|
||||
if is_aggregated:
|
||||
lines.append(f"{repeat_count} {signal} подряд")
|
||||
|
||||
if confidence > 0:
|
||||
lines.append(f"Уверенность: {confidence:.2f}")
|
||||
|
||||
if reason:
|
||||
lines.append(f"Причина: {reason}")
|
||||
if message:
|
||||
lines.append(message)
|
||||
|
||||
return lines
|
||||
|
||||
@@ -305,7 +241,7 @@ def render(events, page, total_pages):
|
||||
|
||||
event_type = str(event.get("event_type", ""))
|
||||
|
||||
if event_type in {"auto_signal_generated", "auto_signal_summary"}:
|
||||
if event_type in {"signal_summary", "signal_ready"}:
|
||||
lines.extend(_render_auto_signal(event, created_time))
|
||||
else:
|
||||
lines.extend(_render_default_event(event, created_time))
|
||||
|
||||
@@ -573,9 +573,9 @@ class AutoTradeService:
|
||||
|
||||
try:
|
||||
JournalService().log_ui_info(
|
||||
event_type="auto_signal_summary",
|
||||
event_type="signal_summary",
|
||||
message=(
|
||||
f"🟡 HOLD {duration_text} завершён сигналом {next_signal}"
|
||||
f"🟡 HOLD {duration_text} завершён сигналом {next_signal}."
|
||||
),
|
||||
screen="auto",
|
||||
action="signal_summary",
|
||||
@@ -614,12 +614,8 @@ class AutoTradeService:
|
||||
|
||||
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}"
|
||||
),
|
||||
event_type="signal_ready",
|
||||
message=f"Сигнал {normalized_signal} готов к исполнению.",
|
||||
screen="auto",
|
||||
action="signal_ready",
|
||||
payload={
|
||||
|
||||
@@ -148,8 +148,8 @@ class ExecutionEngine:
|
||||
}
|
||||
|
||||
JournalService().log_ui_info(
|
||||
event_type="paper_position_opened",
|
||||
message=f"Paper ENTRY открыта: {side} {state.symbol}",
|
||||
event_type="position_opened",
|
||||
message=f"Позиция {side} открыта: {state.symbol}.",
|
||||
screen="auto",
|
||||
action="paper_execution",
|
||||
payload=payload,
|
||||
@@ -266,8 +266,8 @@ class ExecutionEngine:
|
||||
}
|
||||
|
||||
JournalService().log_ui_info(
|
||||
event_type="paper_position_flipped",
|
||||
message=f"Paper FLIP выполнен: {old_side} → {new_side} {state.symbol}",
|
||||
event_type="position_flipped",
|
||||
message=f"Направление позиции изменено: {old_side} → {new_side}.",
|
||||
screen="auto",
|
||||
action="paper_execution",
|
||||
payload=payload,
|
||||
@@ -337,13 +337,11 @@ class ExecutionEngine:
|
||||
"price_updated_at": exit_execution.updated_at if exit_execution else None,
|
||||
}
|
||||
|
||||
close_reason = forced_reason or "MANUAL"
|
||||
|
||||
JournalService().log_ui_info(
|
||||
event_type="paper_position_closed",
|
||||
message=(
|
||||
f"Paper EXIT закрыта по риску {forced_reason}: {position.side} {state.symbol}"
|
||||
if forced_reason is not None
|
||||
else f"Paper EXIT закрыта: {position.side} {state.symbol}"
|
||||
),
|
||||
event_type="position_closed",
|
||||
message=f"Позиция {position.side} закрыта: {close_reason}.",
|
||||
screen="auto",
|
||||
action="paper_execution",
|
||||
payload=payload,
|
||||
@@ -535,9 +533,9 @@ class ExecutionEngine:
|
||||
"updated_at": position.updated_at,
|
||||
}
|
||||
|
||||
JournalService().log_ui_info(
|
||||
event_type="paper_flip_blocked",
|
||||
message=f"Paper FLIP заблокирован: {reason}",
|
||||
JournalService().log_ui_warning(
|
||||
event_type="position_flip_blocked",
|
||||
message=f"Смена направления позиции заблокирована: {reason}",
|
||||
screen="auto",
|
||||
action="paper_execution",
|
||||
payload=payload,
|
||||
|
||||
@@ -279,8 +279,8 @@ class JournalService:
|
||||
deleted_count = self.repository.delete_older_than_days(days)
|
||||
|
||||
self.log_ui_warning(
|
||||
event_type="journal_cleared_old",
|
||||
message=f"Журнал очищен старше {days} дней.",
|
||||
event_type="journal_cleared",
|
||||
message=f"Журнал очищен: удалены записи старше {days} дней.",
|
||||
screen="journal",
|
||||
action="clear_old",
|
||||
payload={
|
||||
|
||||
@@ -384,6 +384,18 @@
|
||||
- добавлено отдельное событие готового сигнала
|
||||
- подготовлена база для стандартизации журнала в 07.4.3.19.4
|
||||
|
||||
#### 07.4.3.19.4 ✅ Journal Runtime Standardization & Export Layer
|
||||
- унифицированы execution event_type
|
||||
- удалены legacy paper_* события
|
||||
- execution logging переведён в единый human-readable стиль
|
||||
- унифицированы market runtime events
|
||||
- стандартизирован export logging
|
||||
- добавлены account-aware export filename
|
||||
- добавлены [DEMO]/[LIVE] runtime prefixes
|
||||
- унифицированы risk-control journal events
|
||||
- централизован EVENT_TITLES mapping
|
||||
- журнал подготовлен к filters/search layer
|
||||
|
||||
### 07.4.4
|
||||
⏳ Grid Strategy
|
||||
|
||||
|
||||
@@ -360,6 +360,18 @@
|
||||
- добавлено отдельное событие готового сигнала
|
||||
- подготовлена база для стандартизации журнала в 07.4.3.19.4
|
||||
|
||||
#### 07.4.3.19.4 ✅ Journal Runtime Standardization & Export Layer
|
||||
- унифицированы execution event_type
|
||||
- удалены legacy paper_* события
|
||||
- execution logging переведён в единый human-readable стиль
|
||||
- унифицированы market runtime events
|
||||
- стандартизирован export logging
|
||||
- добавлены account-aware export filename
|
||||
- добавлены [DEMO]/[LIVE] runtime prefixes
|
||||
- унифицированы risk-control journal events
|
||||
- централизован EVENT_TITLES mapping
|
||||
- журнал подготовлен к filters/search layer
|
||||
|
||||
---
|
||||
|
||||
### 07.4.4
|
||||
|
||||
242
docs/stages/stage-07_4_3_19_4-journal_runtime_standardization.md
Normal file
242
docs/stages/stage-07_4_3_19_4-journal_runtime_standardization.md
Normal file
@@ -0,0 +1,242 @@
|
||||
# 07.4.3.19.4 — Journal Runtime Standardization & Export Layer
|
||||
|
||||
## Статус
|
||||
|
||||
Этап завершён.
|
||||
|
||||
## Цель этапа
|
||||
|
||||
Цель этапа — завершить стандартизацию runtime-журнала, унифицировать execution/runtime/event logging, очистить legacy-style события и подготовить журнал к следующему этапу фильтрации, поиска и аналитики.
|
||||
|
||||
После этапа 07.4.3.19.3 журнал уже содержал signal intent layer и noise filtering, но оставались:
|
||||
|
||||
- разные стили event naming
|
||||
- legacy paper_* event_type
|
||||
- смешанные runtime/runtime-ui сообщения
|
||||
- несогласованные execution-сообщения
|
||||
- разные стили export/runtime notifications
|
||||
|
||||
Этап 07.4.3.19.4 завершает переход к единому audit/runtime journal.
|
||||
|
||||
---
|
||||
|
||||
## Что изменено
|
||||
|
||||
### 1. Унифицированы execution event_type
|
||||
|
||||
Старые paper-style event_type заменены на единый runtime-style:
|
||||
|
||||
Было:
|
||||
|
||||
- paper_position_opened
|
||||
- paper_position_closed
|
||||
- paper_position_flipped
|
||||
- paper_flip_blocked
|
||||
|
||||
Стало:
|
||||
|
||||
- position_opened
|
||||
- position_closed
|
||||
- position_flipped
|
||||
- position_flip_blocked
|
||||
|
||||
Это упрощает дальнейшую фильтрацию и экспорт журнала.
|
||||
|
||||
---
|
||||
|
||||
### 2. Execution-сообщения приведены к единому стилю
|
||||
|
||||
Execution runtime-сообщения теперь используют единый human-readable стиль.
|
||||
|
||||
Примеры:
|
||||
|
||||
- Позиция LONG открыта.
|
||||
- Позиция SHORT закрыта.
|
||||
- Направление позиции изменено: LONG → SHORT.
|
||||
- Смена направления позиции заблокирована.
|
||||
|
||||
Убраны:
|
||||
|
||||
- Paper ENTRY
|
||||
- Paper EXIT
|
||||
- FLIP выполнен
|
||||
- flip blocked technical text
|
||||
|
||||
---
|
||||
|
||||
### 3. Унифицированы runtime market events
|
||||
|
||||
Market runtime logging переведён в единый monitoring-style.
|
||||
|
||||
Добавлены стандартизированные события:
|
||||
|
||||
- market_monitor_started
|
||||
- market_monitor_stopped
|
||||
- market_stream_connected
|
||||
- market_stream_disconnected
|
||||
- market_symbol_changed
|
||||
|
||||
Runtime payload теперь содержит:
|
||||
|
||||
- runtime_key
|
||||
- runtime_screen
|
||||
- runtime_label
|
||||
- cache_symbol
|
||||
- ws_symbol
|
||||
|
||||
---
|
||||
|
||||
### 4. Унифицирован журнал экспорта
|
||||
|
||||
Экспорт журнала переведён в unified export layer.
|
||||
|
||||
Добавлены:
|
||||
|
||||
- journal_exported
|
||||
- journal_export_error
|
||||
|
||||
Экспорт теперь использует единый account-aware filename:
|
||||
|
||||
- journal_demo_info_plus_YYYY-MM-DD_HH-MM-SS.csv
|
||||
- journal_live_info_plus_YYYY-MM-DD_HH-MM-SS.xlsx
|
||||
|
||||
---
|
||||
|
||||
### 5. Добавлен account-mode prefix
|
||||
|
||||
Все UI/runtime journal-сообщения теперь автоматически получают account-mode prefix:
|
||||
|
||||
- [DEMO]
|
||||
- [LIVE]
|
||||
|
||||
Prefix формируется через JournalService.
|
||||
|
||||
Это подготавливает систему к multi-runtime и multi-account support.
|
||||
|
||||
---
|
||||
|
||||
### 6. Унифицированы risk-control события
|
||||
|
||||
Risk settings logging переведён в user-oriented формат.
|
||||
|
||||
Добавлены:
|
||||
|
||||
- risk_settings_updated
|
||||
|
||||
Убраны технические debug-style risk messages.
|
||||
|
||||
---
|
||||
|
||||
### 7. Унифицированы journal UI titles
|
||||
|
||||
journal_ui.py переведён на централизованный EVENT_TITLES mapping.
|
||||
|
||||
Теперь journal renderer отображает:
|
||||
|
||||
- понятные runtime titles
|
||||
- human-readable execution names
|
||||
- единый visual style
|
||||
|
||||
---
|
||||
|
||||
## Что больше не пишется в журнал
|
||||
|
||||
Из runtime journal удалены:
|
||||
|
||||
- legacy paper_* event_type
|
||||
- flip technical spam
|
||||
- execution debug text
|
||||
- duplicate runtime-notification messages
|
||||
- mixed runtime/export wording
|
||||
- raw monitoring tool messages
|
||||
|
||||
---
|
||||
|
||||
## Что остаётся в журнале
|
||||
|
||||
После этапа журнал содержит только полезные runtime-аудит события:
|
||||
|
||||
- signal summary
|
||||
- READY signals
|
||||
- execution events
|
||||
- blocked flip
|
||||
- market runtime events
|
||||
- export events
|
||||
- notification errors
|
||||
- journal/system critical events
|
||||
|
||||
---
|
||||
|
||||
## Основные изменённые файлы
|
||||
|
||||
- app/src/trading/execution/engine.py
|
||||
- app/src/trading/auto/service.py
|
||||
- app/src/integrations/exchange/market_data_runner.py
|
||||
- app/src/telegram/handlers/journal_ui.py
|
||||
- app/src/telegram/handlers/journal.py
|
||||
- app/src/trading/journal/service.py
|
||||
- app/src/telegram/handlers/auto/risk.py
|
||||
- app/src/notifications/service.py
|
||||
- app/src/notifications/channels/telegram.py
|
||||
|
||||
---
|
||||
|
||||
## Проверка
|
||||
|
||||
После правок необходимо выполнить:
|
||||
|
||||
python -m compileall src
|
||||
python -m src.main
|
||||
|
||||
После запуска проверить:
|
||||
|
||||
1. В журнале больше нет paper_* event_type.
|
||||
2. Все execution events отображаются единообразно.
|
||||
3. Flip-blocked отображается как user-readable событие.
|
||||
4. Export CSV/XLSX работает.
|
||||
5. Journal filename содержит account mode.
|
||||
6. Market runtime использует unified titles.
|
||||
7. [DEMO]/[LIVE] prefix отображается корректно.
|
||||
8. Старые debug-style runtime messages больше не появляются.
|
||||
|
||||
---
|
||||
|
||||
## Roadmap Update
|
||||
|
||||
#### 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.3.19.4 ✅ Journal Runtime Standardization & Export Layer
|
||||
- унифицированы execution event_type
|
||||
- удалены legacy paper_* события
|
||||
- execution logging переведён в единый human-readable стиль
|
||||
- унифицированы market runtime events
|
||||
- стандартизирован export logging
|
||||
- добавлены account-aware export filename
|
||||
- добавлены [DEMO]/[LIVE] runtime prefixes
|
||||
- унифицированы risk-control journal events
|
||||
- централизован EVENT_TITLES mapping
|
||||
- журнал подготовлен к filters/search layer
|
||||
|
||||
---
|
||||
|
||||
## Итог
|
||||
|
||||
Этап завершил переход журнала от debug/runtime telemetry к полноценному runtime audit layer.
|
||||
|
||||
Система получила:
|
||||
|
||||
- единый execution logging
|
||||
- account-aware runtime journal
|
||||
- стандартизированные runtime events
|
||||
- unified export layer
|
||||
- human-readable execution events
|
||||
- подготовку к следующему этапу journal filters/search/analytics.
|
||||
|
||||
Reference in New Issue
Block a user