07.4.4.1.3 — Journal Runtime Cleanup & Event Titles Layer

This commit is contained in:
2026-05-11 10:20:21 +03:00
parent c07a1a4dff
commit b5d931bbb7
13 changed files with 629 additions and 127 deletions

View File

@@ -0,0 +1,85 @@
# app/src/core/event_titles.py
# app/src/core/event_titles.py
from __future__ import annotations
EVENT_TITLES = {
# Сигналы
"signal_summary": "Сигнал",
"signal_ready": "Сигнал",
# Execution
"position_opened": "Позиция",
"position_closed": "Позиция",
"position_flipped": "Позиция",
"position_flip_blocked": "Позиция",
# Настройки
"auto_settings_updated": "Автоторговля",
"risk_settings_updated": "Защита",
# Аналитика рынка
"market_state_changed": "Рынок",
"market_volatility_changed": "Рынок",
# Мониторинг рынка
"market_monitor_started": "Рынок",
"market_monitor_stopped": "Рынок",
"market_stream_connected": "Рынок",
"market_stream_disconnected": "Рынок",
"market_symbol_changed": "Рынок",
# Журнал
"journal_exported": "Журнал",
"journal_export_error": "Журнал",
"journal_cleared": "Журнал",
# Уведомления
"notification_sent": "Уведомление",
"notification_error": "Уведомление",
# Приложение
"app_started": "Приложение",
"app_bootstrap_failed": "Приложение",
# Legacy
"app_start": "Приложение",
"journal_open_requested": "Журнал",
"journal_export_csv_success": "Журнал",
"journal_export_csv_error": "Журнал",
"journal_export_xlsx_success": "Журнал",
"journal_export_xlsx_error": "Журнал",
"journal_cleared_old": "Журнал",
"system_open_requested": "Система",
"system_open_alert": "Система",
"system_open_success": "Система",
"system_retry": "Система",
"system_about_opened": "Система",
"market_open_requested": "Рынок",
"market_open_success": "Рынок",
"market_open_error": "Рынок",
"portfolio_open_requested": "Портфель",
"portfolio_open_success": "Портфель",
"portfolio_open_error": "Портфель",
"portfolio_partial_estimate": "Портфель",
"exchange_request_error": "Биржа",
"balance_summary_loaded": "Баланс",
"balance_summary_error": "Баланс",
}
def event_title(event_type: object) -> str:
value = str(event_type or "").strip()
if not value:
return "Событие"
return EVENT_TITLES.get(value, "Событие")

View File

@@ -120,7 +120,7 @@ class MarketDataRunner:
cls._log_info(
context,
"market_symbol_changed",
f"Инструмент автоторговли изменён на {cache_symbol}.",
f"Инструмент автоторговли изменён: {cache_symbol}.",
{
"previous_symbol": previous_symbol,
"symbol": symbol,
@@ -128,7 +128,6 @@ class MarketDataRunner:
"ws_symbol": ws_symbol,
},
)
try:
await cls._run_websocket(context, symbol)
except asyncio.CancelledError:
@@ -307,9 +306,6 @@ class MarketDataRunner:
@classmethod
def _message(cls, context: MarketRuntimeContext, message: str) -> str:
if context.runtime_label:
return f"{context.runtime_label} {message}"
return message
@classmethod

View File

@@ -32,6 +32,17 @@ class TelegramNotificationChannel:
text=message.text,
parse_mode=message.parse_mode,
)
JournalService().log_info(
"notification_sent",
"Telegram-уведомление отправлено.",
{
"title": message.title,
"priority": message.priority,
"dedupe_key": message.dedupe_key,
},
)
return True
except TelegramRetryAfter as exc:

View File

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

View File

@@ -241,16 +241,16 @@ def _market_state_line(state) -> str:
market_state = getattr(state, "market_state", None)
labels = {
"TREND_UP": "📈 Рынок · Рост",
"TREND_DOWN": "📉 Рынок · Падение",
"RANGE": "🟰 Рынок · Без направления",
"HIGH_VOLATILITY": "⚠️ Рынок · Волатильность",
"LOW_VOLATILITY": "🟰 Рынок · Спокойный",
"UNKNOWN": "⏳ Рынок · Анализ",
None: "⏳ Рынок · Анализ",
"TREND_UP": "📈 Тренд · Восходящий",
"TREND_DOWN": "📉 Тренд · Нисходящий",
"RANGE": "🟰 Тренд · Нет выраженного направления",
"HIGH_VOLATILITY": "⚠️ Рынок · Высокая волатильность",
"LOW_VOLATILITY": "🟰 Рынок · Низкая активность",
"UNKNOWN": "⏳ Рынок · Идёт анализ",
None: "⏳ Рынок · Идёт анализ",
}
return labels.get(market_state, "⏳ Рынок · Анализ")
return labels.get(market_state, "⏳ Рынок · Идёт анализ")
def _execution_block_lines(state) -> list[str]:

View File

@@ -9,6 +9,7 @@ from aiogram.types import InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from src.core.config import load_settings
from src.core.event_titles import event_title
PAGE_SIZE = 5
@@ -20,28 +21,6 @@ LEVEL_ICONS = {
"CRITICAL": "🆘",
}
EVENT_TITLES = {
"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": "Журнал очищен",
"notification_sent": "Уведомление отправлено",
"notification_error": "Ошибка уведомления",
"app_started": "Приложение запущено",
"app_bootstrap_failed": "Ошибка запуска приложения",
}
TECH_TO_HUMAN_MESSAGES = {
"invalid api key": "Неверный API Key.",
"unauthorized": "Нет доступа к аккаунту.",
@@ -164,7 +143,7 @@ def _time_label(dt: datetime | None, raw_value: str) -> str:
def _event_title(event_type: str) -> str:
return EVENT_TITLES.get(event_type, event_type)
return event_title(event_type)
def _humanize_message(message: str) -> str:

View File

@@ -97,15 +97,6 @@ async def _render_system_screen(
) -> None:
journal = JournalService()
journal.log_ui_info(
event_type="system_open_requested",
message="Запрошено открытие экрана системы.",
screen="system",
action=action,
user_id=user_id,
chat_id=chat_id,
)
snapshot = get_system_snapshot()
is_alert = has_system_alerts(snapshot)
@@ -363,6 +354,22 @@ def _log_auto_setting_updated(
pass
def _human_symbol(symbol: str | None) -> str:
if not symbol:
return ""
base = symbol.split("_", 1)[0].upper()
if "/" in base:
return base.split("/", 1)[0]
for suffix in ("USDT", "USD", "EUR", "BTC"):
if base.endswith(suffix) and len(base) > len(suffix):
return base[: -len(suffix)]
return base
@router.callback_query(F.data.startswith("settings:auto_strategy:"))
async def set_auto_strategy(callback: CallbackQuery) -> None:
strategy = callback.data.split(":", 2)[2].upper()
@@ -375,7 +382,7 @@ async def set_auto_strategy(callback: CallbackQuery) -> None:
if previous_strategy != strategy:
_log_auto_setting_updated(
message=f"Стратегия автоторговли изменена на {strategy}.",
message=f"Стратегия изменена: {strategy}.",
action="set_strategy",
payload={
"previous_strategy": previous_strategy,
@@ -423,7 +430,7 @@ async def set_auto_symbol(callback: CallbackQuery) -> None:
if previous_symbol != symbol:
_log_auto_setting_updated(
message=f"Актив автоторговли изменён на {symbol}.",
message=f"Актив изменён: {_human_symbol(symbol)}.",
action="set_symbol",
payload={
"previous_symbol": previous_symbol,
@@ -470,7 +477,7 @@ async def set_auto_risk(callback: CallbackQuery) -> None:
if previous_risk != risk:
_log_auto_setting_updated(
message=f"Риск на сделку изменён на {risk:g}%.",
message=f"Риск на сделку изменён: {risk:g}%.",
action="set_risk_percent",
payload={
"previous_risk_percent": previous_risk,
@@ -520,7 +527,7 @@ async def set_auto_leverage(callback: CallbackQuery) -> None:
if previous_leverage != leverage:
_log_auto_setting_updated(
message=f"Плечо автоторговли изменено на x{leverage:g}.",
message=f"Плечо изменено: x{leverage:g}.",
action="set_leverage",
payload={
"previous_leverage": previous_leverage,
@@ -572,7 +579,7 @@ async def set_auto_max_reserved(callback: CallbackQuery) -> None:
value_text = "off" if value is None else f"{value:g}%"
_log_auto_setting_updated(
message=f"Лимит на сделку изменён на {value_text}.",
message=f"Лимит на сделку изменён: {value_text}.",
action="set_max_reserved_balance_percent",
payload={
"previous_max_reserved_balance_percent": previous_value,

View File

@@ -582,7 +582,7 @@ class AutoTradeService:
JournalService().log_ui_info(
event_type="signal_summary",
message=(
f"🟡 HOLD {duration_text} завершён сигналом {next_signal}."
f"HOLD длился {duration_text} и завершился сигналом {next_signal}."
),
screen="auto",
action="signal_summary",
@@ -622,7 +622,9 @@ class AutoTradeService:
try:
JournalService().log_ui_info(
event_type="signal_ready",
message=f"Сигнал {normalized_signal} готов к исполнению.",
message=(
f"Сигнал {normalized_signal} подтверждён и готов к исполнению."
),
screen="auto",
action="signal_ready",
payload={
@@ -691,28 +693,15 @@ class AutoTradeService:
and market_state != type(self)._last_logged_market_state
)
trend_changed = (
market_trend is not None
and market_trend != previous_market_trend
and market_trend != type(self)._last_logged_market_trend
)
volatility_changed = (
market_volatility is not None
and market_volatility != previous_market_volatility
and market_volatility != type(self)._last_logged_market_volatility
)
if not state_changed and not trend_changed and not volatility_changed:
if not state_changed and not volatility_changed:
return
type(self)._last_logged_market_state = market_state
type(self)._last_logged_market_trend = market_trend
type(self)._last_logged_market_volatility = market_volatility
level = self._market_journal_level(market_state)
message = self._market_state_message(market_state)
journal_payload = {
**payload,
"previous_market_state": previous_market_state,
@@ -724,25 +713,64 @@ class AutoTradeService:
}
try:
if level == "WARNING":
JournalService().log_ui_warning(
if state_changed:
self._write_market_journal_event(
event_type="market_state_changed",
message=message,
screen="auto",
action="market_analysis",
market_state=market_state,
message=self._market_state_message(market_state),
payload=journal_payload,
)
return
JournalService().log_ui_info(
event_type="market_state_changed",
if volatility_changed:
self._write_market_journal_event(
event_type="market_volatility_changed",
market_state=market_state,
message=self._market_volatility_message(market_volatility),
payload=journal_payload,
)
except Exception:
pass
type(self)._last_logged_market_state = market_state
type(self)._last_logged_market_trend = market_trend
type(self)._last_logged_market_volatility = market_volatility
def _write_market_journal_event(
self,
*,
event_type: str,
market_state: str,
message: str,
payload: dict,
) -> None:
level = self._market_journal_level(market_state)
if level == "WARNING":
JournalService().log_ui_warning(
event_type=event_type,
message=message,
screen="auto",
action="market_analysis",
payload=journal_payload,
payload=payload,
)
except Exception:
pass
return
JournalService().log_ui_info(
event_type=event_type,
message=message,
screen="auto",
action="market_analysis",
payload=payload,
)
def _market_volatility_message(self, market_volatility: str | None) -> str:
messages = {
"LOW": "Волатильность изменена: низкая.",
"NORMAL": "Волатильность изменена: нормальная.",
"HIGH": "Волатильность изменена: высокая.",
}
return messages.get(str(market_volatility or ""), "Волатильность не определена.")
def _market_journal_level(self, market_state: str) -> str:
if market_state == "HIGH_VOLATILITY":
@@ -752,14 +780,14 @@ class AutoTradeService:
def _market_state_message(self, market_state: str) -> str:
messages = {
"TREND_UP": "📈 Рынок перешёл в рост.",
"TREND_DOWN": "📉 Рынок перешёл в снижение.",
"RANGE": "🟰 На рынке нет выраженного направления.",
"HIGH_VOLATILITY": "⚠️ Рынок стал слишком волатильным.",
"LOW_VOLATILITY": "💤 Рынок почти не движется.",
"TREND_UP": "Состояние рынка изменено: рост.",
"TREND_DOWN": "Состояние рынка изменено: снижение.",
"RANGE": "Состояние рынка изменено: нет выраженного направления.",
"HIGH_VOLATILITY": "Состояние рынка изменено: высокая волатильность.",
"LOW_VOLATILITY": "Состояние рынка изменено: низкая активность.",
}
return messages.get(market_state, "Состояние рынка анализируется.")
return messages.get(market_state, "Состояние рынка анализируется.")
def run_cycle(self) -> AutoTradeState:
state = self.get_state()

View File

@@ -125,7 +125,7 @@ class ExecutionEngine:
state.execution_block_reason = None
state.last_flip_block_reason = None
state.last_execution_action = action
state.last_execution_reason = f"Paper ENTRY {side} открыта."
state.last_execution_reason = f"Позиция {side} открыта."
payload = {
"execution_type": "ENTRY",
@@ -157,7 +157,7 @@ class ExecutionEngine:
EventBus.emit("paper_position_opened", payload)
return ExecutionDecision(action, True, f"Paper ENTRY {side} открыта.")
return ExecutionDecision(action, True, f"Позиция {side} открыта.")
def _flip_position(self, state: AutoTradeState) -> ExecutionDecision:
position = type(self)._position
@@ -227,7 +227,7 @@ class ExecutionEngine:
state.execution_block_reason = None
state.last_flip_block_reason = None
state.last_execution_action = f"FLIP_{old_side}_TO_{new_side}"
state.last_execution_reason = "Paper FLIP выполнен."
state.last_execution_reason = "Направление позиции изменено."
state.last_flip_at = now
type(self)._last_flip_block_key = None
@@ -278,7 +278,7 @@ class ExecutionEngine:
return ExecutionDecision(
f"FLIP_{old_side}_TO_{new_side}",
True,
f"Paper FLIP выполнен: {old_side}{new_side}.",
f"Направление позиции изменено: {old_side}{new_side}.",
)
def _close_position(
@@ -359,9 +359,9 @@ class ExecutionEngine:
else "CLOSE"
)
state.last_execution_reason = (
f"Paper EXIT выполнена по риску: {forced_reason}."
f"Позиция закрыта по правилу защиты: {forced_reason}."
if forced_reason is not None
else "Paper EXIT выполнена."
else "Позиция закрыта."
)
type(self)._last_flip_block_key = None
@@ -369,10 +369,10 @@ class ExecutionEngine:
return ExecutionDecision(
f"FORCE_CLOSE_{forced_reason}",
True,
f"Paper EXIT выполнена по риску: {forced_reason}.",
f"Позиция закрыта по правилу защиты: {forced_reason}.",
)
return ExecutionDecision("CLOSE", True, "Paper EXIT выполнена.")
return ExecutionDecision("CLOSE", True, "Позиция закрыта.")
def _risk_close_decision(self, state: AutoTradeState) -> ExecutionDecision | None:
position = type(self)._position
@@ -472,26 +472,26 @@ class ExecutionEngine:
if confidence < self._min_flip_confidence:
return (
"Flip blocked: signal confidence "
f"{confidence:.2f} < {self._min_flip_confidence:.2f}."
"уверенность сигнала ниже порога "
f"({confidence:.2f} < {self._min_flip_confidence:.2f})"
)
if repeat_count < self._min_flip_repeat_count:
return (
"Flip blocked: repeat count "
f"{repeat_count} < {self._min_flip_repeat_count}."
"сигнал ещё не подтверждён нужным количеством повторов "
f"({repeat_count} < {self._min_flip_repeat_count})"
)
if hold_seconds is not None and hold_seconds < self._min_flip_hold_seconds:
return (
"Flip blocked: position hold time "
f"{hold_seconds}s < {self._min_flip_hold_seconds}s."
"позиция открыта слишком недавно "
f"({hold_seconds}с < {self._min_flip_hold_seconds}с)"
)
if unrealized_pnl < 0 and confidence < self._loss_flip_confidence:
return (
"Flip blocked: position is negative and signal is not strong enough "
f"({confidence:.2f} < {self._loss_flip_confidence:.2f})."
"позиция сейчас в минусе, а сигнал недостаточно сильный "
f"({confidence:.2f} < {self._loss_flip_confidence:.2f})"
)
return None
@@ -535,7 +535,7 @@ class ExecutionEngine:
JournalService().log_ui_warning(
event_type="position_flip_blocked",
message=f"Смена направления позиции заблокирована: {reason}",
message=f"Смена направления позиции заблокирована: {reason}.",
screen="auto",
action="paper_execution",
payload=payload,

View File

@@ -13,30 +13,7 @@ from openpyxl import Workbook
from openpyxl.styles import Font
from src.core.config import load_settings
EVENT_TITLES = {
"app_start": "Запуск приложения",
"journal_open_requested": "Открытие журнала",
"journal_export_csv_success": "Экспорт CSV",
"journal_export_csv_error": "Ошибка экспорта CSV",
"journal_export_xlsx_success": "Экспорт Excel",
"journal_export_xlsx_error": "Ошибка экспорта Excel",
"journal_cleared": "Журнал очищен",
"journal_cleared_old": "Очистка старых записей",
"system_open_alert": "Система загружена с предупреждениями",
"system_open_success": "Система загружена",
"market_open_requested": "Открытие рынка",
"market_open_success": "Рынок загружен",
"market_open_error": "Ошибка открытия рынка",
"portfolio_open_requested": "Открытие портфеля",
"portfolio_open_success": "Портфель загружен",
"portfolio_open_error": "Ошибка открытия портфеля",
"portfolio_partial_estimate": "Частичная оценка портфеля",
"exchange_request_error": "Ошибка запроса к бирже",
"balance_summary_loaded": "Баланс загружен",
"balance_summary_error": "Ошибка загрузки баланса",
}
from src.core.event_titles import event_title
_EMOJI_RE = re.compile(
@@ -81,8 +58,7 @@ def _format_datetime(value: object) -> str:
def _event_title(event_type: object) -> str:
value = str(event_type or "").strip()
return EVENT_TITLES.get(value, value.replace("_", " ").strip().capitalize())
return event_title(event_type)
def _payload(row: dict) -> dict: