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