diff --git a/app/src/core/event_titles.py b/app/src/core/event_titles.py new file mode 100644 index 0000000..6182fa3 --- /dev/null +++ b/app/src/core/event_titles.py @@ -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, "Событие") \ No newline at end of file diff --git a/app/src/integrations/exchange/market_data_runner.py b/app/src/integrations/exchange/market_data_runner.py index 85cf0bb..f94cf95 100644 --- a/app/src/integrations/exchange/market_data_runner.py +++ b/app/src/integrations/exchange/market_data_runner.py @@ -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 diff --git a/app/src/notifications/channels/telegram.py b/app/src/notifications/channels/telegram.py index 78370af..0ffad0f 100644 --- a/app/src/notifications/channels/telegram.py +++ b/app/src/notifications/channels/telegram.py @@ -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: diff --git a/app/src/telegram/handlers/auto/risk.py b/app/src/telegram/handlers/auto/risk.py index d4722ee..e31ed3a 100644 --- a/app/src/telegram/handlers/auto/risk.py +++ b/app/src/telegram/handlers/auto/risk.py @@ -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={ diff --git a/app/src/telegram/handlers/auto/ui.py b/app/src/telegram/handlers/auto/ui.py index 1e13b25..dcfa6c5 100644 --- a/app/src/telegram/handlers/auto/ui.py +++ b/app/src/telegram/handlers/auto/ui.py @@ -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]: diff --git a/app/src/telegram/handlers/journal_ui.py b/app/src/telegram/handlers/journal_ui.py index 1565113..8eaf2d3 100644 --- a/app/src/telegram/handlers/journal_ui.py +++ b/app/src/telegram/handlers/journal_ui.py @@ -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: diff --git a/app/src/telegram/handlers/system.py b/app/src/telegram/handlers/system.py index bae28a0..344fe02 100644 --- a/app/src/telegram/handlers/system.py +++ b/app/src/telegram/handlers/system.py @@ -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, diff --git a/app/src/trading/auto/service.py b/app/src/trading/auto/service.py index f47b9dc..068d674 100644 --- a/app/src/trading/auto/service.py +++ b/app/src/trading/auto/service.py @@ -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() diff --git a/app/src/trading/execution/engine.py b/app/src/trading/execution/engine.py index a24c957..751ef46 100644 --- a/app/src/trading/execution/engine.py +++ b/app/src/trading/execution/engine.py @@ -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, diff --git a/app/src/trading/journal/exporter.py b/app/src/trading/journal/exporter.py index 632dded..358fbaa 100644 --- a/app/src/trading/journal/exporter.py +++ b/app/src/trading/journal/exporter.py @@ -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: diff --git a/docs/roadmap/master-roadmap.md b/docs/roadmap/master-roadmap.md index b9c93d2..17ae59c 100644 --- a/docs/roadmap/master-roadmap.md +++ b/docs/roadmap/master-roadmap.md @@ -434,6 +434,23 @@ - подготовлена база для future AI market commentary - журнал подготовлен к market filters/search layer +#### 07.4.4.1.3 ✅ Journal Runtime Cleanup & Event Titles Layer +- унифицирована модель журнала: event_type / заголовок / сообщение +- заголовки событий сокращены до понятных сущностей +- подготовлен общий EVENT_TITLES mapping для journal UI и export layer +- убрано дублирование market_state_changed и market_trend_changed +- market_trend_changed исключён из обычного журналирования +- market trend сохранён в payload для аналитики и debug +- market-analysis сообщения приведены к единому формату +- Auto UI wording уточнён: падение заменено на нисходящий тренд +- сообщения настроек автоторговли приведены к короткому стилю +- символы активов очищены в journal messages: BTC вместо BTC/USD_LEVERAGE +- risk-control logging стал показывать SL / TP / ML в одном сообщении +- notification logging унифицирован через notification_sent / notification_error +- из market runtime сообщений убран лишний [AUTO] +- CSV / XLSX export очищен от эмодзи +- журнал подготовлен к централизованному event_titles.py и future filters/search layer + --- ### 07.4.5 diff --git a/docs/roadmap/stage-07-auto-trading-roadmap.md b/docs/roadmap/stage-07-auto-trading-roadmap.md index 9816eb1..738be68 100644 --- a/docs/roadmap/stage-07-auto-trading-roadmap.md +++ b/docs/roadmap/stage-07-auto-trading-roadmap.md @@ -410,6 +410,23 @@ - подготовлена база для future AI market commentary - журнал подготовлен к market filters/search layer +#### 07.4.4.1.3 ✅ Journal Runtime Cleanup & Event Titles Layer +- унифицирована модель журнала: event_type / заголовок / сообщение +- заголовки событий сокращены до понятных сущностей +- подготовлен общий EVENT_TITLES mapping для journal UI и export layer +- убрано дублирование market_state_changed и market_trend_changed +- market_trend_changed исключён из обычного журналирования +- market trend сохранён в payload для аналитики и debug +- market-analysis сообщения приведены к единому формату +- Auto UI wording уточнён: падение заменено на нисходящий тренд +- сообщения настроек автоторговли приведены к короткому стилю +- символы активов очищены в journal messages: BTC вместо BTC/USD_LEVERAGE +- risk-control logging стал показывать SL / TP / ML в одном сообщении +- notification logging унифицирован через notification_sent / notification_error +- из market runtime сообщений убран лишний [AUTO] +- CSV / XLSX export очищен от эмодзи +- журнал подготовлен к централизованному event_titles.py и future filters/search layer + --- ### 07.4.5 diff --git a/docs/stages/stage-07_4_4_1_3-journal_runtime_cleanup_and_event_titles.md b/docs/stages/stage-07_4_4_1_3-journal_runtime_cleanup_and_event_titles.md new file mode 100644 index 0000000..7e0f45d --- /dev/null +++ b/docs/stages/stage-07_4_4_1_3-journal_runtime_cleanup_and_event_titles.md @@ -0,0 +1,381 @@ +# 07.4.4.1.3 — Journal Runtime Cleanup & Event Titles Layer + +## Цель этапа + +Привести журнал runtime-событий к единому, читаемому и пригодному для анализа виду. + +После этапов Market State Engine и Market State Journal Events журнал уже начал фиксировать состояние рынка, сигналы, настройки и runtime-события. Но в экспорте и UI оставались проблемы: + +- заголовки часто дублировали сообщения; +- часть сообщений была слишком длинной; +- часть event_type отображалась технически; +- market_state_changed и market_trend_changed часто писали один и тот же смысл; +- в экспорт попадали эмодзи; +- в сообщениях встречались лишние технические маркеры вроде [AUTO]; +- торговые инструменты отображались в сыром виде, например BTC/USD_LEVERAGE; +- EVENT_TITLES дублировались в разных файлах. + +Этап 07.4.4.1.3 сделал журнал более чистым, компактным и пригодным для дальнейшей аналитики. + +--- + +# Что было реализовано + +## 1. Унифицирована логика заголовков журнала + +В журнале была закреплена новая модель: + +- `event_type` — технический код события; +- `Заголовок` — короткая сущность события; +- `Сообщение` — конкретное действие или факт. + +Теперь заголовок больше не повторяет текст сообщения. + +Пример итогового формата: + +- `auto_settings_updated` → `Автоторговля` → `Плечо изменено: x3.` +- `risk_settings_updated` → `Защита` → `Параметры защиты позиции изменены: SL=1%, TP=1%, ML=off.` +- `market_state_changed` → `Рынок` → `Состояние рынка изменено: снижение.` +- `market_stream_connected` → `Рынок` → `Поток рыночных данных подключён.` + +Такой формат стал понятнее и для пользователя, и для будущих фильтров / поиска / аналитики. + +--- + +## 2. EVENT_TITLES подготовлены к централизации + +Была согласована новая компактная карта заголовков событий. + +Ключевые группы: + +- `Сигнал` +- `Позиция` +- `Автоторговля` +- `Защита` +- `Рынок` +- `Журнал` +- `Уведомление` +- `Приложение` +- `Система` +- `Портфель` +- `Биржа` +- `Баланс` + +Также принято архитектурное решение вынести `EVENT_TITLES` в отдельный общий файл: + +```text +app/src/core/event_titles.py +``` + +Это правильнее, чем хранить заголовки в `trading/journal`, потому что события относятся не только к торговле, но и к системе, журналу, рынку, уведомлениям, портфелю и бирже. + +--- + +## 3. Убрано дублирование market_state_changed и market_trend_changed + +До правки одно изменение рынка могло писать сразу два события: + +- `market_state_changed` — «Рынок перешёл в рост» +- `market_trend_changed` — «Направление рынка изменилось на рост» + +Для пользователя это выглядело как дублирование. + +Теперь `market_trend_changed` исключён из обычного журналирования, потому что направление уже включено в market state: + +- `TREND_UP` уже означает рост; +- `TREND_DOWN` уже означает снижение; +- `RANGE` уже означает отсутствие выраженного направления. + +В журнале остаётся одно основное событие: + +```text +market_state_changed → Рынок → Состояние рынка изменено: снижение. +``` + +Это сделало market-analysis feed значительно чище. + +--- + +## 4. Аналитические market-сообщения приведены к единому стилю + +Market-analysis сообщения были переписаны в одинаковом формате: + +- `Состояние рынка изменено: рост.` +- `Состояние рынка изменено: снижение.` +- `Состояние рынка изменено: нет выраженного направления.` +- `Состояние рынка изменено: высокая волатильность.` +- `Состояние рынка изменено: низкая активность.` + +Сообщения волатильности также приведены к компактному виду: + +- `Волатильность изменена: низкая.` +- `Волатильность изменена: нормальная.` +- `Волатильность изменена: высокая.` + +Это важно для дальнейшей аналитики: сообщения теперь имеют одинаковую структуру и легче читаются в CSV / XLSX. + +--- + +## 5. Market trend оставлен в payload, но убран из отдельного журнала + +Поле `market_trend` не удалялось из состояния и payload. + +Оно по-прежнему может использоваться: + +- в UI; +- в стратегии; +- в debug; +- в будущей аналитике; +- в explainability layer. + +Но отдельное событие `market_trend_changed` больше не нужно для обычного журнала, потому что оно дублировало `market_state_changed`. + +Итог: + +- данные не потеряны; +- журнал стал чище; +- future analytics сохранила нужный контекст. + +--- + +## 6. Улучшено отображение состояния рынка в Auto UI + +Формулировка `Рынок · Падение` была признана неоднозначной. + +Она могла восприниматься как «цена прямо сейчас падает», хотя фактически отображался более широкий режим рынка по анализатору. + +Поэтому UI wording был уточнён: + +- `TREND_UP` → `Тренд · Восходящий` +- `TREND_DOWN` → `Тренд · Нисходящий` +- `RANGE` → `Тренд · Нет выраженного направления` +- `HIGH_VOLATILITY` → `Рынок · Высокая волатильность` +- `LOW_VOLATILITY` → `Рынок · Низкая активность` +- `UNKNOWN` → `Рынок · Идёт анализ` + +Это снизило риск неправильной интерпретации графика пользователем. + +--- + +## 7. Сообщения настроек автоторговли приведены к единому стилю + +Сообщения настроек были сокращены и стандартизированы. + +Теперь используется формат: + +- `Стратегия изменена: TREND.` +- `Актив изменён: BTC.` +- `Риск на сделку изменён: 1%.` +- `Плечо изменено: x3.` +- `Лимит на сделку изменён: 50%.` + +Ранее сообщения были длиннее и местами дублировали заголовок. + +--- + +## 8. Символы активов очищены для journal messages + +В пользовательских сообщениях журнала убраны технические торговые символы вида: + +```text +BTC/USD_LEVERAGE +ETH/USD_LEVERAGE +LTC/USD_LEVERAGE +XRP/USD_LEVERAGE +``` + +Теперь в сообщениях отображаются короткие имена активов: + +```text +BTC +ETH +LTC +XRP +``` + +Пример: + +```text +[DEMO] Актив изменён: BTC. +``` + +В payload при этом сохраняется полный технический symbol, поэтому аналитика и отладка не теряют точность. + +--- + +## 9. Risk-control logging приведён к единому формату + +Сообщение `Параметры защиты позиции обновлены` было заменено на более информативное: + +```text +Параметры защиты позиции изменены: SL=1%, TP=1%, ML=25 USD. +``` + +Теперь в одном сообщении видно состояние всех ключевых protective rules: + +- Stop Loss; +- Take Profit; +- Max Loss. + +Это удобно для просмотра истории настроек в журнале и экспорте. + +--- + +## 10. Уведомления приведены к единой модели событий + +Notification layer был очищен от legacy event_type. + +Теперь используется единая схема: + +- `notification_sent` +- `notification_error` + +После успешной отправки Telegram-уведомления добавлено логирование: + +```text +Telegram-уведомление отправлено. +``` + +Ошибки отправки также пишутся в один event_type `notification_error`, а детали уходят в payload. + +--- + +## 11. Убран лишний runtime-префикс [AUTO] + +Сообщения market runtime раньше выглядели так: + +```text +[DEMO] [AUTO] Поток рыночных данных подключён. +``` + +Префикс `[AUTO]` был признан избыточным, потому что контекст уже хранится в колонках `Экран`, `Действие` и payload. + +Теперь сообщение выглядит чище: + +```text +[DEMO] Поток рыночных данных подключён. +``` + +--- + +## 12. Экспорт очищен от эмодзи + +CSV / XLSX экспорт был очищен от эмодзи. + +Эмодзи могут быть удобны в Telegram UI, но в экспортных файлах они мешают: + +- сортировке; +- фильтрации; +- чтению в Excel; +- будущей BI-аналитике; +- машинной обработке. + +Теперь экспорт содержит чистые текстовые значения. + +--- + +# Изменения в архитектуре + +## Core Layer + +Подготовлено выделение общего файла: + +```text +src/core/event_titles.py +``` + +Он должен стать единым источником event-title mapping для: + +- journal UI; +- CSV export; +- XLSX export; +- будущих filters/search layer. + +--- + +## Journal UI Layer + +`journal_ui.py` подготовлен к использованию централизованного `event_title()`. + +Локальный `EVENT_TITLES` больше не должен дублироваться после вынесения в core. + +--- + +## Journal Export Layer + +`exporter.py` подготовлен к использованию того же `event_title()`. + +Это гарантирует, что UI и экспорт будут показывать одинаковые заголовки. + +--- + +## Auto Runtime Layer + +В `AutoTradeService` улучшена логика market logging: + +- убрано избыточное `market_trend_changed`; +- сохранено `market_state_changed`; +- сохранено `market_volatility_changed`; +- market trend оставлен в payload; +- сообщения приведены к единому стилю. + +--- + +## Market UI Layer + +Auto UI стал точнее отображать market state: + +- вместо «Падение» используется «Нисходящий тренд»; +- вместо «Рост» используется «Восходящий тренд»; +- вместо «Без направления» используется «Нет выраженного направления». + +--- + +# Что изменилось для пользователя + +Журнал стал выглядеть как нормальная событийная лента: + +```text +Рынок | Состояние рынка изменено: снижение. +Рынок | Волатильность изменена: нормальная. +Автоторговля | Плечо изменено: x3. +Защита | Параметры защиты позиции изменены: SL=1%, TP=1%, ML=off. +Журнал | Журнал очищен. +``` + +Пользователь теперь быстрее понимает: + +- что изменилось; +- где изменилось; +- относится ли событие к рынку, настройкам, защите, позиции или журналу; +- какой был конкретный результат. + +--- + +# Что подготовлено дальше + +Этап подготовил основу для следующих работ: + +- централизованный `event_titles.py`; +- фильтры журнала по сущностям; +- поиск по event_type; +- группировка событий по доменам; +- журнал действий пользователя; +- market transition statistics; +- аналитика частоты смены market state; +- AI-комментарии к журналу; +- подготовка runtime-журнала к реальной торговле. + +--- + +# Итог этапа + +Этап 07.4.4.1.3 завершил важную часть подготовки журнала к реальной эксплуатации. + +Журнал больше не выглядит как набор технических сообщений. Он стал ближе к операционной ленте автоторговли, где каждая строка имеет понятную структуру: + +```text +что произошло → где произошло → конкретное действие +``` + +Это важный шаг перед переходом к более серьёзной аналитике стратегий и дальнейшей подготовке к real trading.