07.4.3.19.4 — Journal Runtime Standardization & Export Layer

This commit is contained in:
2026-05-10 15:26:49 +03:00
parent 1692cb4d81
commit 8024cd9d9a
13 changed files with 343 additions and 325 deletions

View File

@@ -39,8 +39,8 @@ def create_app() -> tuple[Bot, Dispatcher]:
try: try:
journal.log_info( journal.log_info(
"app_start", "app_started",
"Приложение запущено.", "Приложение запущено",
{ {
"env": settings.app_env, "env": settings.app_env,
"exchange_name": settings.exchange_name, "exchange_name": settings.exchange_name,

View File

@@ -46,13 +46,6 @@ class MarketDataRunner:
existing.screen = screen existing.screen = screen
existing.action = action existing.action = action
existing.runtime_label = runtime_label existing.runtime_label = runtime_label
cls._log_info(
existing,
"market_runner_context_updated",
"MarketDataRunner context updated.",
{"interval_seconds": interval_seconds},
)
return return
context = MarketRuntimeContext( context = MarketRuntimeContext(
@@ -69,8 +62,8 @@ class MarketDataRunner:
cls._log_info( cls._log_info(
context, context,
"market_runner_started", "market_monitor_started",
"MarketDataRunner started.", "Мониторинг рынка запущен.",
{"interval_seconds": interval_seconds}, {"interval_seconds": interval_seconds},
) )
@@ -93,8 +86,8 @@ class MarketDataRunner:
cls._log_info( cls._log_info(
context, context,
"market_runner_stopped", "market_monitor_stopped",
"MarketDataRunner stopped.", "Мониторинг рынка остановлен.",
) )
cls._runtimes.pop(runtime_key, None) cls._runtimes.pop(runtime_key, None)
@@ -107,11 +100,6 @@ class MarketDataRunner:
symbol = context.symbol_provider() symbol = context.symbol_provider()
if not symbol: if not symbol:
cls._log_warning(
context,
"market_runner_no_symbol",
"MarketDataRunner has no symbol.",
)
await asyncio.sleep(context.interval_seconds) await asyncio.sleep(context.interval_seconds)
continue continue
@@ -129,8 +117,8 @@ class MarketDataRunner:
cls._log_info( cls._log_info(
context, context,
"market_runner_symbol_changed", "market_symbol_changed",
"MarketDataRunner symbol changed.", f"Инструмент автоторговли изменён на {cache_symbol}.",
{ {
"symbol": symbol, "symbol": symbol,
"cache_symbol": cache_symbol, "cache_symbol": cache_symbol,
@@ -143,10 +131,10 @@ class MarketDataRunner:
except asyncio.CancelledError: except asyncio.CancelledError:
raise raise
except Exception as exc: except Exception as exc:
cls._log_error( cls._log_warning(
context, context,
"market_ws_error_fallback", "market_stream_disconnected",
"WebSocket market data failed. Falling back to REST.", "Поток рыночных данных отключён. Используется резервный REST-режим.",
{ {
"symbol": symbol, "symbol": symbol,
"cache_symbol": cache_symbol, "cache_symbol": cache_symbol,
@@ -165,17 +153,6 @@ class MarketDataRunner:
cache_symbol = cls._cache_symbol(symbol) cache_symbol = cls._cache_symbol(symbol)
ws_symbol = cls._ws_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 payload_count = 0
async for payload in ExchangeWebSocketClient().stream_depth( async for payload in ExchangeWebSocketClient().stream_depth(
@@ -185,8 +162,8 @@ class MarketDataRunner:
if payload_count == 0: if payload_count == 0:
cls._log_info( cls._log_info(
context, context,
"market_ws_connected", "market_stream_connected",
"Market WebSocket connected and first payload received.", "Поток рыночных данных подключён.",
{ {
"requested_symbol": symbol, "requested_symbol": symbol,
"cache_symbol": cache_symbol, "cache_symbol": cache_symbol,
@@ -200,33 +177,12 @@ class MarketDataRunner:
current_symbol = context.symbol_provider() current_symbol = context.symbol_provider()
if current_symbol and current_symbol != symbol: 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 break
best_bid = cls._extract_best_price(payload, "bids") best_bid = cls._extract_best_price(payload, "bids")
best_ask = cls._extract_best_price(payload, "asks") best_ask = cls._extract_best_price(payload, "asks")
if best_bid is None or best_ask is None: 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 continue
MarketPriceCache.set_price( MarketPriceCache.set_price(
@@ -241,30 +197,16 @@ class MarketDataRunner:
@classmethod @classmethod
async def _rest_fallback_once(cls, context: MarketRuntimeContext, symbol: str) -> None: async def _rest_fallback_once(cls, context: MarketRuntimeContext, symbol: str) -> None:
try: try:
snapshot = await asyncio.to_thread( await asyncio.to_thread(
ExchangeService().refresh_market_snapshot_cache, ExchangeService().refresh_market_snapshot_cache,
symbol, symbol,
runtime_key=context.runtime_key, 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: except Exception as exc:
cls._log_error( cls._log_error(
context, context,
"market_rest_fallback_error", "market_stream_disconnected",
"REST fallback market snapshot failed.", "Поток рыночных данных отключён. Резервный REST-режим недоступен.",
{ {
"symbol": symbol, "symbol": symbol,
"error": str(exc), "error": str(exc),

View File

@@ -16,8 +16,8 @@ class TelegramNotificationChannel:
if bot is None or chat_id is None: if bot is None or chat_id is None:
JournalService().log_warning( JournalService().log_warning(
"notification_target_missing", "notification_error",
"Telegram notification target is not registered.", "Не удалось отправить Telegram-уведомление: получатель не настроен.",
{ {
"title": message.title, "title": message.title,
"priority": message.priority, "priority": message.priority,
@@ -36,8 +36,8 @@ class TelegramNotificationChannel:
except TelegramRetryAfter as exc: except TelegramRetryAfter as exc:
JournalService().log_warning( JournalService().log_warning(
"notification_telegram_retry_after", "notification_error",
"Telegram notification delayed by retry-after.", "Не удалось отправить Telegram-уведомление: Telegram ограничил частоту отправки.",
{ {
"title": message.title, "title": message.title,
"retry_after": exc.retry_after, "retry_after": exc.retry_after,
@@ -49,8 +49,8 @@ class TelegramNotificationChannel:
except Exception as exc: except Exception as exc:
JournalService().log_error( JournalService().log_error(
"notification_telegram_error", "notification_error",
"Telegram notification failed.", "Не удалось отправить Telegram-уведомление.",
{ {
"title": message.title, "title": message.title,
"error": str(exc), "error": str(exc),

View File

@@ -7,9 +7,7 @@ from src.notifications.dedupe import NotificationDedupe
from src.notifications.models import NotificationMessage from src.notifications.models import NotificationMessage
from src.notifications.templates.execution import build_execution_notification from src.notifications.templates.execution import build_execution_notification
from src.notifications.templates.signal import build_signal_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.runtime_events.models import RuntimeEvent
from src.trading.journal.service import JournalService
class NotificationService: class NotificationService:
@@ -17,112 +15,15 @@ class NotificationService:
message = self._build_message(event) message = self._build_message(event)
if message is None: 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 return
if not NotificationDedupe.should_send(message.dedupe_key): if not NotificationDedupe.should_send(message.dedupe_key):
self._log_suppressed(event, message)
return return
sent = await TelegramNotificationChannel().send(message) await TelegramNotificationChannel().send(message)
if sent:
self._log_sent(event, message)
def _build_message(self, event: RuntimeEvent) -> NotificationMessage | None: def _build_message(self, event: RuntimeEvent) -> NotificationMessage | None:
return ( return (
build_signal_notification(event) build_signal_notification(event)
or build_execution_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,
},
) )

View File

@@ -207,13 +207,8 @@ def _log_risk_updated(action: str) -> None:
try: try:
JournalService().log_ui_info( JournalService().log_ui_info(
event_type="auto_risk_settings_updated", event_type="risk_settings_updated",
message=( message="Параметры защиты позиции обновлены.",
"Risk settings updated: "
f"SL={state.stop_loss_percent}, "
f"TP={state.take_profit_percent}, "
f"ML={state.max_loss_usd}"
),
screen="auto", screen="auto",
action=action, action=action,
payload={ payload={

View File

@@ -126,15 +126,6 @@ async def open_journal(message: Message, state: FSMContext) -> None:
chat_id=message.chat.id, 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( await _show_journal_page(
message, message,
page=1, page=1,
@@ -157,15 +148,6 @@ async def open_journal_from_monitoring(callback: CallbackQuery, state: FSMContex
keep_message_id=callback.message.message_id, 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( await _show_journal_page(
callback.message, callback.message,
page=1, page=1,
@@ -195,7 +177,7 @@ async def export_journal_csv(callback: CallbackQuery) -> None:
await callback.message.answer_document(document=document) await callback.message.answer_document(document=document)
service.log_ui_info( service.log_ui_info(
event_type="journal_export_csv_success", event_type="journal_exported",
message="Журнал экспортирован в CSV.", message="Журнал экспортирован в CSV.",
screen="journal", screen="journal",
action="export_csv", action="export_csv",
@@ -207,12 +189,13 @@ async def export_journal_csv(callback: CallbackQuery) -> None:
await callback.answer("CSV экспортирован") await callback.answer("CSV экспортирован")
except Exception as exc: except Exception as exc:
service.log_ui_error( service.log_ui_error(
event_type="journal_export_csv_error", event_type="journal_export_error",
message="Не удалось экспортировать журнал в CSV.", message="Не удалось экспортировать журнал в CSV.",
screen="journal", screen="journal",
action="export_csv", action="export_csv",
user_id=_user_id_from_callback(callback), user_id=_user_id_from_callback(callback),
chat_id=_chat_id_from_callback(callback), chat_id=_chat_id_from_callback(callback),
payload={"format": "csv"},
raw_error=str(exc), raw_error=str(exc),
) )
await callback.answer("Не удалось экспортировать CSV", show_alert=True) 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) await callback.message.answer_document(document=document)
service.log_ui_info( service.log_ui_info(
event_type="journal_export_xlsx_success", event_type="journal_exported",
message="Журнал экспортирован в Excel.", message="Журнал экспортирован в XLSX.",
screen="journal", screen="journal",
action="export_xlsx", action="export_xlsx",
user_id=_user_id_from_callback(callback), user_id=_user_id_from_callback(callback),
@@ -245,12 +228,13 @@ async def export_journal_xlsx(callback: CallbackQuery) -> None:
await callback.answer("Excel экспортирован") await callback.answer("Excel экспортирован")
except Exception as exc: except Exception as exc:
service.log_ui_error( service.log_ui_error(
event_type="journal_export_xlsx_error", event_type="journal_export_error",
message="Не удалось экспортировать журнал в Excel.", message="Не удалось экспортировать журнал в XLSX.",
screen="journal", screen="journal",
action="export_xlsx", action="export_xlsx",
user_id=_user_id_from_callback(callback), user_id=_user_id_from_callback(callback),
chat_id=_chat_id_from_callback(callback), chat_id=_chat_id_from_callback(callback),
payload={"format": "xlsx"},
raw_error=str(exc), raw_error=str(exc),
) )
await callback.answer("Не удалось экспортировать Excel", show_alert=True) await callback.answer("Не удалось экспортировать Excel", show_alert=True)

View File

@@ -21,65 +21,25 @@ LEVEL_ICONS = {
} }
EVENT_TITLES = { EVENT_TITLES = {
"auto_signal_generated": "Сигнал автоторговли", "signal_summary": "Сводка сигнала",
"auto_signal_summary": "Итог серии сигналов", "signal_ready": "Сигнал готов",
"app_start": "Запуск приложения", "position_opened": "Позиция открыта",
"system_open_alert": "Система загружена с предупреждениями", "position_closed": "Позиция закрыта",
"system_open_requested": "Открытие системы", "position_flipped": "Направление позиции изменено",
"system_open_success": "Система загружена", "position_flip_blocked": "Смена направления позиции заблокирована",
"system_retry": "Система обновлена", "risk_settings_updated": "Настройки защиты обновлены",
"market_open_requested": "Открытие рынка", "market_monitor_started": "Мониторинг рынка запущен",
"market_open_success": "Рынок загружен", "market_monitor_stopped": "Мониторинг рынка остановлен",
"market_open_error": "Ошибка открытия рынка", "market_stream_connected": "Поток рынка подключён",
"market_retry_error": "Ошибка обновления рынка", "market_stream_disconnected": "Поток рынка отключён",
"market_symbol_invalid": "Некорректный инструмент", "market_symbol_changed": "Инструмент рынка изменён",
"market_price_error": "Ошибка загрузки цены", "journal_export_error": "Ошибка экспорта журнала",
"portfolio_open_requested": "Открытие портфеля", "journal_exported": "Журнал экспортирован",
"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": "Ошибка состояния",
"journal_cleared": "Журнал очищен", "journal_cleared": "Журнал очищен",
"journal_cleared_old": "Журнал очищен по сроку", "notification_sent": "Уведомление отправлено",
"journal_export_csv_success": "Экспорт CSV", "notification_error": "Ошибка уведомления",
"journal_export_csv_error": "Ошибка экспорта CSV", "app_started": "Приложение запущено",
"journal_export_xlsx_success": "Экспорт Excel", "app_bootstrap_failed": "Ошибка запуска приложения",
"journal_export_xlsx_error": "Ошибка экспорта Excel",
"journal_open_requested": "Открытие журнала",
} }
TECH_TO_HUMAN_MESSAGES = { TECH_TO_HUMAN_MESSAGES = {
@@ -221,42 +181,18 @@ def _payload(event: dict) -> dict:
def _render_auto_signal(event: dict, created_time: str) -> list[str]: def _render_auto_signal(event: dict, created_time: str) -> list[str]:
payload = _payload(event) level = str(event.get("level", "INFO")).upper()
icon = LEVEL_ICONS.get(level, "")
signal = str(payload.get("signal", "HOLD")).upper() title = _event_title(str(event.get("event_type", "")))
strategy = str(payload.get("strategy", "AUTO")).upper() message = _humanize_message(str(event.get("message", "")))
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 += "🧠 "
lines = [ lines = [
f"{prefix}{signal_icon} <b>AUTO · {signal}</b>", f"{icon} <b>{level}</b> · {title}",
f"{created_time} · {strategy} · {symbol}", f"{created_time}",
] ]
if is_aggregated: if message:
lines.append(f"{repeat_count} {signal} подряд") lines.append(message)
if confidence > 0:
lines.append(f"Уверенность: {confidence:.2f}")
if reason:
lines.append(f"Причина: {reason}")
return lines return lines
@@ -305,7 +241,7 @@ def render(events, page, total_pages):
event_type = str(event.get("event_type", "")) 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)) lines.extend(_render_auto_signal(event, created_time))
else: else:
lines.extend(_render_default_event(event, created_time)) lines.extend(_render_default_event(event, created_time))

View File

@@ -573,9 +573,9 @@ class AutoTradeService:
try: try:
JournalService().log_ui_info( JournalService().log_ui_info(
event_type="auto_signal_summary", event_type="signal_summary",
message=( message=(
f"🟡 HOLD {duration_text} завершён сигналом {next_signal}" f"🟡 HOLD {duration_text} завершён сигналом {next_signal}."
), ),
screen="auto", screen="auto",
action="signal_summary", action="signal_summary",
@@ -614,12 +614,8 @@ class AutoTradeService:
try: try:
JournalService().log_ui_info( JournalService().log_ui_info(
event_type="auto_signal_ready", event_type="signal_ready",
message=( message=f"Сигнал {normalized_signal} готов к исполнению.",
f"Сигнал {normalized_signal} готов: "
f"{signal_intent}, confidence={confidence:.2f}, "
f"repeats={state.last_signal_repeat_count}"
),
screen="auto", screen="auto",
action="signal_ready", action="signal_ready",
payload={ payload={

View File

@@ -148,8 +148,8 @@ class ExecutionEngine:
} }
JournalService().log_ui_info( JournalService().log_ui_info(
event_type="paper_position_opened", event_type="position_opened",
message=f"Paper ENTRY открыта: {side} {state.symbol}", message=f"Позиция {side} открыта: {state.symbol}.",
screen="auto", screen="auto",
action="paper_execution", action="paper_execution",
payload=payload, payload=payload,
@@ -266,8 +266,8 @@ class ExecutionEngine:
} }
JournalService().log_ui_info( JournalService().log_ui_info(
event_type="paper_position_flipped", event_type="position_flipped",
message=f"Paper FLIP выполнен: {old_side}{new_side} {state.symbol}", message=f"Направление позиции изменено: {old_side}{new_side}.",
screen="auto", screen="auto",
action="paper_execution", action="paper_execution",
payload=payload, payload=payload,
@@ -337,13 +337,11 @@ class ExecutionEngine:
"price_updated_at": exit_execution.updated_at if exit_execution else None, "price_updated_at": exit_execution.updated_at if exit_execution else None,
} }
close_reason = forced_reason or "MANUAL"
JournalService().log_ui_info( JournalService().log_ui_info(
event_type="paper_position_closed", event_type="position_closed",
message=( message=f"Позиция {position.side} закрыта: {close_reason}.",
f"Paper EXIT закрыта по риску {forced_reason}: {position.side} {state.symbol}"
if forced_reason is not None
else f"Paper EXIT закрыта: {position.side} {state.symbol}"
),
screen="auto", screen="auto",
action="paper_execution", action="paper_execution",
payload=payload, payload=payload,
@@ -535,9 +533,9 @@ class ExecutionEngine:
"updated_at": position.updated_at, "updated_at": position.updated_at,
} }
JournalService().log_ui_info( JournalService().log_ui_warning(
event_type="paper_flip_blocked", event_type="position_flip_blocked",
message=f"Paper FLIP заблокирован: {reason}", message=f"Смена направления позиции заблокирована: {reason}",
screen="auto", screen="auto",
action="paper_execution", action="paper_execution",
payload=payload, payload=payload,

View File

@@ -279,8 +279,8 @@ class JournalService:
deleted_count = self.repository.delete_older_than_days(days) deleted_count = self.repository.delete_older_than_days(days)
self.log_ui_warning( self.log_ui_warning(
event_type="journal_cleared_old", event_type="journal_cleared",
message=f"Журнал очищен старше {days} дней.", message=f"Журнал очищен: удалены записи старше {days} дней.",
screen="journal", screen="journal",
action="clear_old", action="clear_old",
payload={ payload={

View File

@@ -384,6 +384,18 @@
- добавлено отдельное событие готового сигнала - добавлено отдельное событие готового сигнала
- подготовлена база для стандартизации журнала в 07.4.3.19.4 - подготовлена база для стандартизации журнала в 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 ### 07.4.4
⏳ Grid Strategy ⏳ Grid Strategy

View File

@@ -360,6 +360,18 @@
- добавлено отдельное событие готового сигнала - добавлено отдельное событие готового сигнала
- подготовлена база для стандартизации журнала в 07.4.3.19.4 - подготовлена база для стандартизации журнала в 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 ### 07.4.4

View 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.