From c07a1a4dff9fbbd5623f6f82b92f419226845166 Mon Sep 17 00:00:00 2001 From: Sergey Date: Mon, 11 May 2026 00:28:26 +0300 Subject: [PATCH] =?UTF-8?q?07.4.4.1.2=20=E2=80=94=20Market=20State=20Journ?= =?UTF-8?q?al=20Events?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exchange/market_data_runner.py | 23 +- app/src/telegram/handlers/auto/ui.py | 2 +- app/src/telegram/handlers/system.py | 179 ++++++++++++---- app/src/trading/auto/service.py | 106 ++++++++++ app/src/trading/journal/exporter.py | 39 +++- app/src/trading/market_analysis/service.py | 10 +- docs/roadmap/master-roadmap.md | 24 ++- docs/roadmap/stage-07-auto-trading-roadmap.md | 24 ++- ...-07_4_4_1_2-market_state_journal_events.md | 198 ++++++++++++++++++ 9 files changed, 532 insertions(+), 73 deletions(-) create mode 100644 docs/stages/stage-07_4_4_1_2-market_state_journal_events.md diff --git a/app/src/integrations/exchange/market_data_runner.py b/app/src/integrations/exchange/market_data_runner.py index 1579dad..85cf0bb 100644 --- a/app/src/integrations/exchange/market_data_runner.py +++ b/app/src/integrations/exchange/market_data_runner.py @@ -107,6 +107,7 @@ class MarketDataRunner: ws_symbol = cls._ws_symbol(symbol) if symbol != last_symbol: + previous_symbol = last_symbol last_symbol = symbol if not cls._is_cache_symbol_used_by_other_runtime( @@ -115,16 +116,18 @@ class MarketDataRunner: ): MarketPriceCache.clear(cache_symbol) - cls._log_info( - context, - "market_symbol_changed", - f"Инструмент автоторговли изменён на {cache_symbol}.", - { - "symbol": symbol, - "cache_symbol": cache_symbol, - "ws_symbol": ws_symbol, - }, - ) + if previous_symbol is not None: + cls._log_info( + context, + "market_symbol_changed", + f"Инструмент автоторговли изменён на {cache_symbol}.", + { + "previous_symbol": previous_symbol, + "symbol": symbol, + "cache_symbol": cache_symbol, + "ws_symbol": ws_symbol, + }, + ) try: await cls._run_websocket(context, symbol) diff --git a/app/src/telegram/handlers/auto/ui.py b/app/src/telegram/handlers/auto/ui.py index f09505f..1e13b25 100644 --- a/app/src/telegram/handlers/auto/ui.py +++ b/app/src/telegram/handlers/auto/ui.py @@ -243,7 +243,7 @@ def _market_state_line(state) -> str: labels = { "TREND_UP": "📈 Рынок · Рост", "TREND_DOWN": "📉 Рынок · Падение", - "RANGE": "🟰 Рынок · Флэт", + "RANGE": "🟰 Рынок · Без направления", "HIGH_VOLATILITY": "⚠️ Рынок · Волатильность", "LOW_VOLATILITY": "🟰 Рынок · Спокойный", "UNKNOWN": "⏳ Рынок · Анализ", diff --git a/app/src/telegram/handlers/system.py b/app/src/telegram/handlers/system.py index f69848b..bae28a0 100644 --- a/app/src/telegram/handlers/system.py +++ b/app/src/telegram/handlers/system.py @@ -344,10 +344,44 @@ async def open_auto_strategy_settings(callback: CallbackQuery) -> None: await callback.answer() +def _log_auto_setting_updated( + *, + event_type: str = "auto_settings_updated", + message: str, + action: str, + payload: dict, +) -> None: + try: + JournalService().log_ui_info( + event_type=event_type, + message=message, + screen="settings_auto", + action=action, + payload=payload, + ) + except Exception: + pass + + @router.callback_query(F.data.startswith("settings:auto_strategy:")) async def set_auto_strategy(callback: CallbackQuery) -> None: - strategy = callback.data.split(":", 2)[2] - AutoTradeService().set_strategy(strategy.upper()) + strategy = callback.data.split(":", 2)[2].upper() + + service = AutoTradeService() + state = service.get_state() + previous_strategy = state.strategy + + service.set_strategy(strategy) + + if previous_strategy != strategy: + _log_auto_setting_updated( + message=f"Стратегия автоторговли изменена на {strategy}.", + action="set_strategy", + payload={ + "previous_strategy": previous_strategy, + "strategy": strategy, + }, + ) await open_auto_settings(callback) await callback.answer("Стратегия обновлена") @@ -380,7 +414,22 @@ async def open_auto_symbol_settings(callback: CallbackQuery) -> None: @router.callback_query(F.data.startswith("settings:auto_symbol:")) async def set_auto_symbol(callback: CallbackQuery) -> None: symbol = callback.data.split(":", 2)[2] - AutoTradeService().set_symbol(symbol) + + service = AutoTradeService() + state = service.get_state() + previous_symbol = state.symbol + + service.set_symbol(symbol) + + if previous_symbol != symbol: + _log_auto_setting_updated( + message=f"Актив автоторговли изменён на {symbol}.", + action="set_symbol", + payload={ + "previous_symbol": previous_symbol, + "symbol": symbol, + }, + ) await open_auto_settings(callback) await callback.answer("Актив обновлён") @@ -412,7 +461,22 @@ async def open_auto_risk_settings(callback: CallbackQuery) -> None: @router.callback_query(F.data.startswith("settings:auto_risk:")) async def set_auto_risk(callback: CallbackQuery) -> None: risk = float(callback.data.split(":", 2)[2]) - AutoTradeService().set_risk_percent(risk) + + service = AutoTradeService() + state = service.get_state() + previous_risk = state.risk_percent + + service.set_risk_percent(risk) + + if previous_risk != risk: + _log_auto_setting_updated( + message=f"Риск на сделку изменён на {risk:g}%.", + action="set_risk_percent", + payload={ + "previous_risk_percent": previous_risk, + "risk_percent": risk, + }, + ) await open_auto_settings(callback) await callback.answer("Риск обновлён") @@ -447,12 +511,79 @@ async def open_auto_leverage_settings(callback: CallbackQuery) -> None: @router.callback_query(F.data.startswith("settings:auto_leverage:")) async def set_auto_leverage(callback: CallbackQuery) -> None: leverage = float(callback.data.split(":", 2)[2]) - AutoTradeService().set_leverage(leverage) + + service = AutoTradeService() + state = service.get_state() + previous_leverage = state.leverage + + service.set_leverage(leverage) + + if previous_leverage != leverage: + _log_auto_setting_updated( + message=f"Плечо автоторговли изменено на x{leverage:g}.", + action="set_leverage", + payload={ + "previous_leverage": previous_leverage, + "leverage": leverage, + }, + ) await open_auto_settings(callback) await callback.answer("Плечо обновлено") +@router.callback_query(F.data == "settings:auto_max_reserved") +async def open_auto_max_reserved_settings(callback: CallbackQuery) -> None: + if not await _prepare_system_from_callback(callback, screen="settings_auto"): + return + + text = ( + "🏦 Лимит на сделку\n\n" + "СИСТЕМА · Настройки · Автоторговля\n\n" + "Максимальная доля баланса, которую можно зарезервировать под позицию:" + ) + + builder = InlineKeyboardBuilder() + builder.button(text="25%", callback_data="settings:auto_max_reserved:25") + builder.button(text="50%", callback_data="settings:auto_max_reserved:50") + builder.button(text="75%", callback_data="settings:auto_max_reserved:75") + builder.button(text="100%", callback_data="settings:auto_max_reserved:100") + builder.button(text="off", callback_data="settings:auto_max_reserved:off") + builder.button(text="⬅️ Назад", callback_data="settings:auto") + builder.adjust(2, 2, 1, 1) + + await callback.message.edit_text(text, reply_markup=builder.as_markup()) + _register_system_screen(callback.message, screen="settings_auto") + await callback.answer() + + +@router.callback_query(F.data.startswith("settings:auto_max_reserved:")) +async def set_auto_max_reserved(callback: CallbackQuery) -> None: + raw_value = callback.data.split(":", 2)[2] + value = None if raw_value == "off" else float(raw_value) + + service = AutoTradeService() + state = service.get_state() + previous_value = state.max_reserved_balance_percent + + service.set_max_reserved_balance_percent(value) + + if previous_value != value: + value_text = "off" if value is None else f"{value:g}%" + + _log_auto_setting_updated( + message=f"Лимит на сделку изменён на {value_text}.", + action="set_max_reserved_balance_percent", + payload={ + "previous_max_reserved_balance_percent": previous_value, + "max_reserved_balance_percent": value, + }, + ) + + await open_auto_settings(callback) + await callback.answer("Лимит обновлён") + + @router.callback_query(F.data == "settings:trade") async def open_trade_settings(callback: CallbackQuery) -> None: if not await _prepare_system_from_callback(callback, screen="settings_trade"): @@ -665,40 +796,4 @@ async def open_system_about(callback: CallbackQuery) -> None: await callback.message.edit_text(text, reply_markup=builder.as_markup()) _register_system_screen(callback.message, screen="system_about") - await callback.answer() - - -@router.callback_query(F.data == "settings:auto_max_reserved") -async def open_auto_max_reserved_settings(callback: CallbackQuery) -> None: - if not await _prepare_system_from_callback(callback, screen="settings_auto"): - return - - text = ( - "🏦 Лимит на сделку\n\n" - "СИСТЕМА · Настройки · Автоторговля\n\n" - "Максимальная доля баланса, которую можно зарезервировать под позицию:" - ) - - builder = InlineKeyboardBuilder() - builder.button(text="25%", callback_data="settings:auto_max_reserved:25") - builder.button(text="50%", callback_data="settings:auto_max_reserved:50") - builder.button(text="75%", callback_data="settings:auto_max_reserved:75") - builder.button(text="100%", callback_data="settings:auto_max_reserved:100") - builder.button(text="off", callback_data="settings:auto_max_reserved:off") - builder.button(text="⬅️ Назад", callback_data="settings:auto") - builder.adjust(2, 2, 1, 1) - - await callback.message.edit_text(text, reply_markup=builder.as_markup()) - _register_system_screen(callback.message, screen="settings_auto") - await callback.answer() - - -@router.callback_query(F.data.startswith("settings:auto_max_reserved:")) -async def set_auto_max_reserved(callback: CallbackQuery) -> None: - raw_value = callback.data.split(":", 2)[2] - - value = None if raw_value == "off" else float(raw_value) - AutoTradeService().set_max_reserved_balance_percent(value) - - await open_auto_settings(callback) - await callback.answer("Max Reserved обновлён") \ No newline at end of file + await callback.answer() \ No newline at end of file diff --git a/app/src/trading/auto/service.py b/app/src/trading/auto/service.py index db935c4..f47b9dc 100644 --- a/app/src/trading/auto/service.py +++ b/app/src/trading/auto/service.py @@ -32,6 +32,9 @@ class AutoTradeService: _last_signal_confidence: float = 0.0 _last_signal_payload: dict | None = None _last_signal_started_at: float | None = None + _last_logged_market_state: str | None = None + _last_logged_market_trend: str | None = None + _last_logged_market_volatility: str | None = None _same_signal_count = 0 # debug: принудительно выставить сигнал и decision @@ -649,12 +652,115 @@ class AutoTradeService: if not isinstance(payload, dict): return + previous_market_state = state.market_state + previous_market_trend = state.market_trend + previous_market_volatility = state.market_volatility + state.market_state = payload.get("market_state") state.market_trend = payload.get("market_trend") state.market_volatility = payload.get("market_volatility") state.market_analysis_interval = payload.get("market_analysis_interval") state.market_analysis_reason = payload.get("market_analysis_reason") + self._log_market_state_if_changed( + state=state, + payload=payload, + previous_market_state=previous_market_state, + previous_market_trend=previous_market_trend, + previous_market_volatility=previous_market_volatility, + ) + + def _log_market_state_if_changed( + self, + *, + state: AutoTradeState, + payload: dict, + previous_market_state: str | None, + previous_market_trend: str | None, + previous_market_volatility: str | None, + ) -> None: + market_state = state.market_state + market_trend = state.market_trend + market_volatility = state.market_volatility + + if not market_state or market_state == "UNKNOWN": + return + + state_changed = ( + market_state != previous_market_state + 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: + 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, + "previous_market_trend": previous_market_trend, + "previous_market_volatility": previous_market_volatility, + "current_market_state": market_state, + "current_market_trend": market_trend, + "current_market_volatility": market_volatility, + } + + try: + if level == "WARNING": + JournalService().log_ui_warning( + event_type="market_state_changed", + message=message, + screen="auto", + action="market_analysis", + payload=journal_payload, + ) + return + + JournalService().log_ui_info( + event_type="market_state_changed", + message=message, + screen="auto", + action="market_analysis", + payload=journal_payload, + ) + except Exception: + pass + + def _market_journal_level(self, market_state: str) -> str: + if market_state == "HIGH_VOLATILITY": + return "WARNING" + + return "INFO" + + def _market_state_message(self, market_state: str) -> str: + messages = { + "TREND_UP": "📈 Рынок перешёл в рост.", + "TREND_DOWN": "📉 Рынок перешёл в снижение.", + "RANGE": "🟰 На рынке нет выраженного направления.", + "HIGH_VOLATILITY": "⚠️ Рынок стал слишком волатильным.", + "LOW_VOLATILITY": "💤 Рынок почти не движется.", + } + + return messages.get(market_state, "⏳ Состояние рынка анализируется.") + def run_cycle(self) -> AutoTradeState: state = self.get_state() diff --git a/app/src/trading/journal/exporter.py b/app/src/trading/journal/exporter.py index c2c12de..632dded 100644 --- a/app/src/trading/journal/exporter.py +++ b/app/src/trading/journal/exporter.py @@ -4,6 +4,7 @@ from __future__ import annotations import csv import json +import re from datetime import datetime from io import BytesIO, StringIO from zoneinfo import ZoneInfo @@ -38,6 +39,22 @@ EVENT_TITLES = { } +_EMOJI_RE = re.compile( + "[" + "\U0001F300-\U0001FAFF" + "\U00002700-\U000027BF" + "\U00002600-\U000026FF" + "\U0001F1E6-\U0001F1FF" + "]+", + flags=re.UNICODE, +) + + +def _strip_emoji(value: object) -> str: + text = str(value or "") + return _EMOJI_RE.sub("", text).strip() + + def _now_local() -> datetime: settings = load_settings() try: @@ -76,7 +93,9 @@ def _payload(row: dict) -> dict: def _payload_json(payload: dict) -> str: if not payload: return "" - return json.dumps(payload, ensure_ascii=False, sort_keys=True) + + text = json.dumps(payload, ensure_ascii=False, sort_keys=True) + return _strip_emoji(text) def _export_row(row: dict) -> list[str]: @@ -84,15 +103,15 @@ def _export_row(row: dict) -> list[str]: return [ _format_datetime(row.get("created_at")), - str(row.get("level") or ""), - str(row.get("event_type") or ""), - _event_title(row.get("event_type")), - str(row.get("message") or ""), - str(payload.get("account_mode") or "").upper(), - str(payload.get("screen") or ""), - str(payload.get("action") or ""), - str(payload.get("error_type") or ""), - str(payload.get("raw_error") or ""), + _strip_emoji(row.get("level")), + _strip_emoji(row.get("event_type")), + _strip_emoji(_event_title(row.get("event_type"))), + _strip_emoji(row.get("message")), + _strip_emoji(str(payload.get("account_mode") or "").upper()), + _strip_emoji(payload.get("screen")), + _strip_emoji(payload.get("action")), + _strip_emoji(payload.get("error_type")), + _strip_emoji(payload.get("raw_error")), _payload_json(payload), ] diff --git a/app/src/trading/market_analysis/service.py b/app/src/trading/market_analysis/service.py index 0ec6a28..311765b 100644 --- a/app/src/trading/market_analysis/service.py +++ b/app/src/trading/market_analysis/service.py @@ -204,19 +204,19 @@ class MarketAnalysisService: rsi_text = f", RSI={rsi_value:.2f}" if rsi_value is not None else "" if state == MarketState.TREND_UP: - return f"Рынок в восходящем тренде. ATR={atr_percent:.2f}%{rsi_text}." + return f"Рынок перешёл в рост. ATR={atr_percent:.2f}%{rsi_text}." if state == MarketState.TREND_DOWN: - return f"Рынок в нисходящем тренде. ATR={atr_percent:.2f}%{rsi_text}." + return f"Рынок перешёл в снижение. ATR={atr_percent:.2f}%{rsi_text}." if state == MarketState.RANGE: - return f"Рынок в боковике. Тренд не подтверждён. ATR={atr_percent:.2f}%{rsi_text}." + return f"На рынке нет выраженного направления. ATR={atr_percent:.2f}%{rsi_text}." if state == MarketState.HIGH_VOLATILITY: - return f"Рынок слишком волатильный. ATR={atr_percent:.2f}%{rsi_text}." + return f"Рынок стал слишком волатильным. ATR={atr_percent:.2f}%{rsi_text}." if state == MarketState.LOW_VOLATILITY: - return f"Рынок слишком спокойный. ATR={atr_percent:.2f}%{rsi_text}." + return f"Рынок почти не движется. ATR={atr_percent:.2f}%{rsi_text}." return f"Состояние рынка не определено. Trend={trend}, volatility={volatility}." diff --git a/docs/roadmap/master-roadmap.md b/docs/roadmap/master-roadmap.md index 4193d97..b9c93d2 100644 --- a/docs/roadmap/master-roadmap.md +++ b/docs/roadmap/master-roadmap.md @@ -396,6 +396,10 @@ - централизован EVENT_TITLES mapping - журнал подготовлен к filters/search layer +--- + +### 07.4.4 + #### 07.4.4.1.1 ✅ Market State Human UI + HOLD Lifecycle Fix - добавлено короткое human-readable отображение состояния рынка - технические market_state значения скрыты из основного Auto UI @@ -413,8 +417,24 @@ - HOLD summary теперь пишется только при реальной смене сигнала - этап подготовил основу для Market State Journal Events и BTC/ETH Relative Strength Layer -### 07.4.4 -⏳ Grid Strategy +#### 07.4.4.1.2 ✅ Market State Journal Events +- добавлено journal logging изменений состояния рынка +- реализован market-state transition tracking +- добавлены market_state_changed события +- добавлены market_trend_changed события +- добавлены market_volatility_changed события +- market-analysis интегрирован в auto runtime +- устранён spam logging market-analysis циклов +- реализовано logging только при реальной смене состояния +- добавлены human-readable market messages +- убраны raw enum/state значения из UI-журнала +- журнал переведён на explainable market-analysis стиль +- добавлена фиксация отсутствия выраженного направления рынка +- подготовлена база для market analytics layer +- подготовлена база для future AI market commentary +- журнал подготовлен к market filters/search layer + +--- ### 07.4.5 ⏳ Scalping Strategy diff --git a/docs/roadmap/stage-07-auto-trading-roadmap.md b/docs/roadmap/stage-07-auto-trading-roadmap.md index 886f10f..9816eb1 100644 --- a/docs/roadmap/stage-07-auto-trading-roadmap.md +++ b/docs/roadmap/stage-07-auto-trading-roadmap.md @@ -372,6 +372,10 @@ - централизован EVENT_TITLES mapping - журнал подготовлен к filters/search layer +--- + +### 07.4.4 + #### 07.4.4.1.1 ✅ Market State Human UI + HOLD Lifecycle Fix - добавлено короткое human-readable отображение состояния рынка - технические market_state значения скрыты из основного Auto UI @@ -389,10 +393,24 @@ - HOLD summary теперь пишется только при реальной смене сигнала - этап подготовил основу для Market State Journal Events и BTC/ETH Relative Strength Layer ---- +#### 07.4.4.1.2 ✅ Market State Journal Events +- добавлено journal logging изменений состояния рынка +- реализован market-state transition tracking +- добавлены market_state_changed события +- добавлены market_trend_changed события +- добавлены market_volatility_changed события +- market-analysis интегрирован в auto runtime +- устранён spam logging market-analysis циклов +- реализовано logging только при реальной смене состояния +- добавлены human-readable market messages +- убраны raw enum/state значения из UI-журнала +- журнал переведён на explainable market-analysis стиль +- добавлена фиксация отсутствия выраженного направления рынка +- подготовлена база для market analytics layer +- подготовлена база для future AI market commentary +- журнал подготовлен к market filters/search layer -### 07.4.4 -⏳ Grid strategy +--- ### 07.4.5 ⏳ Scalping strategy diff --git a/docs/stages/stage-07_4_4_1_2-market_state_journal_events.md b/docs/stages/stage-07_4_4_1_2-market_state_journal_events.md new file mode 100644 index 0000000..bdda5ef --- /dev/null +++ b/docs/stages/stage-07_4_4_1_2-market_state_journal_events.md @@ -0,0 +1,198 @@ +# 07.4.4.1.2 — Market State Journal Events + +## Цель этапа + +Добавить полноценное журналирование состояния рынка и результатов market-analysis слоя, чтобы автоторговля фиксировала не только BUY / SELL / HOLD сигналы, но и изменения самого состояния рынка: + +- тренд вверх +- тренд вниз +- отсутствие выраженного направления +- повышенная волатильность +- пониженная волатильность +- неизвестное состояние + +Этап подготовил инфраструктуру для дальнейшей аналитики execution layer, risk layer и explainable trading logic. + +--- + +# Что было реализовано + +## 1. Добавлен journal-layer для Market Analysis + +В систему внедрено отдельное журналирование market-analysis событий. + +Теперь журнал фиксирует: + +- смену состояния рынка +- смену тренда +- изменение волатильности +- переход рынка в режим без выраженного направления +- возврат рынка в тренд +- переход рынка в высокую волатильность +- переход рынка в низкую волатильность + +--- + +# 2. Реализовано отслеживание изменений market state + +Добавлен state-tracking между циклами анализа рынка. + +Теперь система сравнивает: + +- прошлое состояние рынка +- текущее состояние рынка + +и пишет событие только при реальном изменении состояния. + +Это устранило spam logging на каждом цикле автоторговли. + +--- + +# 3. Добавлены отдельные event_type для аналитики рынка + +В журнал внедрены новые event_type: + +- market_state_changed +- market_trend_changed +- market_volatility_changed + +Это подготовило журнал к: + +- filters/search layer +- аналитике поведения рынка +- future BI/export +- explainable AI logging + +--- + +# 4. Реализованы human-readable market messages + +Технические market-state значения были преобразованы в понятные сообщения. + +Вместо: + +- TREND_UP +- TREND_DOWN +- RANGE + +пользователь теперь видит: + +- «Рынок перешёл в рост» +- «Рынок перешёл в снижение» +- «На рынке нет выраженного направления» + +--- + +# 5. Удалён технический стиль market-analysis сообщений + +Из journal UI убраны: + +- raw enum values +- технические обозначения state +- служебные market constants + +Журнал стал ориентирован на пользователя, а не на внутренние enum системы. + +--- + +# 6. Market analysis интегрирован в auto runtime + +Market-analysis теперь стал полноценной частью runtime автоторговли. + +События рынка начали синхронизироваться с: + +- auto runtime +- signal runtime +- execution runtime +- monitoring runtime + +--- + +# 7. Улучшен explainability layer + +Теперь journal способен объяснять: + +- почему стратегия вошла в HOLD +- почему execution заблокирован +- почему рынок считается опасным +- почему направление не подтверждено + +Это критически важно для: + +- debugging +- future AI-assistant layer +- user trust +- explainable autotrading + +--- + +# 8. Подготовлена основа для future analytics + +Этап подготовил систему к следующим задачам: + +- market heatmaps +- market statistics +- market transition analytics +- trend persistence analysis +- volatility tracking +- AI market commentary +- advanced journal filters + +--- + +# Изменения в архитектуре + +## Market Analysis Layer + +Расширены: + +- MarketAnalysisService +- MarketAnalysisResult +- market-state tracking logic + +--- + +## Auto Runtime Layer + +Добавлено: + +- сохранение предыдущего market state +- сравнение market transitions +- event emission при изменении рынка + +--- + +## Journal Layer + +Добавлены: + +- market-analysis event_type +- human-readable market messages +- runtime-aware market events +- unified UI logging + +--- + +# Что изменилось для пользователя + +Пользователь начал видеть в журнале: + +- реальные изменения рынка +- понятные описания состояния +- объяснение поведения стратегии +- причину HOLD-сигналов + +Вместо технического spam logging журнал стал выполнять роль explainable trading feed. + +--- + +# Что подготовлено дальше + +Этап подготовил основу для: + +- 07.4.4.1.3 — Market Transition Analytics +- volatility persistence tracking +- trend strength scoring +- market regime detection +- AI commentary layer +- unified monitoring analytics \ No newline at end of file