diff --git a/app/src/telegram/handlers/auto/_ui_old_working.py b/app/src/telegram/handlers/auto/_ui_old_working.py
deleted file mode 100644
index 9636a10..0000000
--- a/app/src/telegram/handlers/auto/_ui_old_working.py
+++ /dev/null
@@ -1,280 +0,0 @@
-# app/src/telegram/handlers/auto/ui.py
-
-from __future__ import annotations
-
-from aiogram.types import InlineKeyboardMarkup
-from aiogram.utils.keyboard import InlineKeyboardBuilder
-
-from src.integrations.exchange.service import ExchangeService
-from src.telegram.ui.common import mode_line
-from src.telegram.ui.currency_ui import format_usd_amount
-from src.trading.auto.service import AutoTradeService
-
-
-PAPER_BALANCE_USD = 1000.0
-
-def strategy_label(strategy: str | None) -> str:
- mapping = {
- "TREND": "📈 Trend Following",
- "GRID": "🧩 Grid Trading",
- "SCALP": "⚡ Scalping",
- }
- return mapping.get(strategy or "", "—")
-
-
-def status_label(status: str) -> str:
- mapping = {
- "OFF": "⚪ Выключена",
- "OBSERVING": "👀 Наблюдение",
- "RUNNING": "🟢 Активна",
- }
- return mapping.get(status, status)
-
-
-def signal_label(signal: str | None) -> str:
- mapping = {
- "BUY": "🟢 BUY",
- "SELL": "🔴 SELL",
- "HOLD": "🟡 HOLD",
- }
- return mapping.get(signal or "", "—")
-
-
-def decision_label(status: str) -> str:
- mapping = {
- "WAITING": "🟡 Ожидание",
- "CONFIRMING": "🟠 Подтверждение",
- "READY": "🟢 Готово к входу",
- "BLOCKED": "🔴 Заблокировано",
- }
- return mapping.get(status, status)
-
-
-def value_or_dash(value: object) -> str:
- if value is None:
- return "—"
- return str(value)
-
-
-def price_or_dash(value: float | None) -> str:
- if value is None:
- return "—"
- return f"{value:.2f}"
-
-
-def market_price_or_dash(symbol: str | None) -> str:
- if not symbol:
- return "—"
-
- try:
- ticker = ExchangeService().get_price(symbol)
- return f"$ {format_usd_amount(ticker.price)}"
- except Exception:
- return "—"
-
-
-def usd_or_dash(value: float | None) -> str:
- if value is None:
- return "—"
- return f"{value:.2f} USD"
-
-
-def size_or_dash(value: float | None) -> str:
- if value is None:
- return "—"
- return f"{value:.8f}".rstrip("0").rstrip(".")
-
-
-def leverage_or_dash(value: float | None) -> str:
- if value is None:
- return "—"
- return f"{value:.1f}x"
-
-
-def format_symbol(symbol: str | None) -> str:
- if not symbol:
- return "—"
-
- base_symbol = symbol.split("_", 1)[0]
- parts = base_symbol.split("/", 1)
-
- if len(parts) == 2:
- return f"{parts[0]} / {parts[1]}"
-
- return base_symbol
-
-
-def compact_strategy(strategy: str | None) -> str:
- if not strategy:
- return "—"
- return strategy.upper()
-
-
-def compact_leverage(value: float | None) -> str:
- if value is None:
- return "—"
- return f"x{value:g}"
-
-
-def is_auto_configured(state) -> bool:
- return bool(
- state.symbol
- and state.strategy
- and state.risk_percent is not None
- )
-
-
-def context_line(state) -> str:
- symbol = format_symbol(state.symbol)
- strategy = compact_strategy(state.strategy)
- leverage = compact_leverage(state.leverage)
-
- if leverage == "—":
- return f"{symbol} · {strategy}"
-
- return f"{symbol} · {strategy} · {leverage}"
-
-
-def auto_keyboard() -> InlineKeyboardMarkup:
- builder = InlineKeyboardBuilder()
-
- builder.button(text="▶️ Start", callback_data="auto:start")
- builder.button(text="👀 Watch", callback_data="auto:observe")
- builder.button(text="🛑 Stop", callback_data="auto:stop")
- builder.button(text="🛠️ Настройки", callback_data="settings:auto")
- builder.button(text="⚠️ Risk", callback_data="auto:risk")
-
- builder.adjust(3, 2)
- return builder.as_markup()
-
-
-def risk_settings_line(state) -> str:
- sl = f"{state.stop_loss_percent:g}%" if state.stop_loss_percent is not None else "off"
- tp = f"{state.take_profit_percent:g}%" if state.take_profit_percent is not None else "off"
- max_loss = f"{state.max_loss_usd:g} USD" if state.max_loss_usd is not None else "off"
-
- return f"Controls: SL {sl} · TP {tp} · ML {max_loss}"
-
-
-def target_risk_usd_line(state) -> str:
- if state.risk_percent is None:
- return "Target Risk: —"
-
- risk_usd = PAPER_BALANCE_USD * (state.risk_percent / 100)
- return f"Target Risk: {risk_usd:.2f} USD"
-
-
-def estimated_size_line(state) -> str:
- if (
- state.risk_percent is None
- or state.risk_percent <= 0
- or state.stop_loss_percent is None
- or state.stop_loss_percent <= 0
- ):
- return "Est. Size: —"
-
- try:
- ticker = ExchangeService().get_price(state.symbol)
- price = ticker.price
- except Exception:
- return "Est. Size: —"
-
- if price <= 0:
- return "Est. Size: —"
-
- target_risk_usd = PAPER_BALANCE_USD * (state.risk_percent / 100)
- stop_loss_distance_usd = price * (state.stop_loss_percent / 100)
-
- if stop_loss_distance_usd <= 0:
- return "Est. Size: —"
-
- size = target_risk_usd / stop_loss_distance_usd
- return f"Est. Size: {size:.8f}".rstrip("0").rstrip(".")
-
-
-def position_size_line(state) -> str:
- if state.position_size is None or state.position_size == 0:
- return "Position Size: —"
-
- return f"Position Size: {state.position_size:.8f}".rstrip("0").rstrip(".")
-
-
-def actual_risk_usd_line(state) -> str:
- if (
- state.position_side == "NONE"
- or state.entry_price is None
- or state.position_size is None
- or state.stop_loss_percent is None
- or state.stop_loss_percent <= 0
- ):
- return "Actual Risk: —"
-
- stop_loss_distance_usd = state.entry_price * (state.stop_loss_percent / 100)
- actual_risk_usd = stop_loss_distance_usd * state.position_size
-
- return f"Actual Risk: {actual_risk_usd:.2f} USD"
-
-
-def build_auto_text() -> str:
- service = AutoTradeService()
- state = service.get_state()
-
- account_mode = "DEMO" if "DEMO" in mode_line().upper() else "LIVE"
- risk = f"{state.risk_percent:.1f}%" if state.risk_percent is not None else "—"
- configured = is_auto_configured(state)
- price = market_price_or_dash(state.symbol)
-
- status_line = {
- "OFF": "⚪ Off",
- "OBSERVING": "👀 Watch",
- "RUNNING": "🟢 On",
- }.get(state.status, state.status)
-
- header = (
- f"🤖 Автоторговля · {status_line}\n"
- f"🔸 {account_mode} аккаунт\n\n"
- )
-
- if state.status == "OFF":
- if not configured:
- return (
- f"{header}"
- "⚠️ Не настроена\n"
- "Настрой параметры"
- )
-
- return (
- f"{header}"
- f"{context_line(state)}\n"
- f"Price: {price}\n"
- f"Position Risk: {risk}\n"
- f"{target_risk_usd_line(state)}\n"
- f"{estimated_size_line(state)}\n"
- f"{risk_settings_line(state)}"
- )
-
- position_line = (
- f"Pos: {value_or_dash(state.position_side)} | "
- f"PnL: {usd_or_dash(state.unrealized_pnl_usd)}"
- )
-
- if state.position_side != "NONE" and state.entry_price is not None:
- position_line = (
- f"Pos: {value_or_dash(state.position_side)} | "
- f"Entry: $ {price_or_dash(state.entry_price)} | "
- f"PnL: {usd_or_dash(state.unrealized_pnl_usd)}"
- )
-
- return (
- f"{header}"
- f"{context_line(state)}\n"
- f"Price: {price}\n\n"
- f"{signal_label(state.last_signal)} ×{state.last_signal_repeat_count} "
- f"· {state.decision_status}\n\n"
- f"{position_line}\n"
- f"Position Risk: {risk}\n"
- f"{target_risk_usd_line(state)}\n"
- f"{position_size_line(state)}\n"
- f"{actual_risk_usd_line(state)}\n"
- f"{risk_settings_line(state)}"
- )
\ No newline at end of file
diff --git a/app/src/telegram/handlers/auto/ui.py b/app/src/telegram/handlers/auto/ui.py
index a569f08..cc15e1e 100644
--- a/app/src/telegram/handlers/auto/ui.py
+++ b/app/src/telegram/handlers/auto/ui.py
@@ -10,7 +10,6 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from src.integrations.exchange.service import ExchangeService
from src.telegram.ui.common import mode_line
-from src.telegram.ui.currency_ui import format_usd_amount
from src.trading.auto.service import AutoTradeService
@@ -76,9 +75,9 @@ def _build_not_configured_text(state) -> str:
"🤖 Автоторговля ⚪ Не настроена",
_account_mode_line(),
"",
- f"{symbol_icon} Актив · {_asset_symbol(state.symbol)}\n"
- f"{strategy_icon} Стратегия · {_required_value(_strategy_short(state.strategy))}\n"
- f"{risk_icon} Риск на сделку · {_required_value(_risk_percent_text(state))}\n"
+ f"{symbol_icon} Актив · {_asset_symbol(state.symbol)}",
+ f"{strategy_icon} Стратегия · {_required_value(_strategy_short(state.strategy))}",
+ f"{risk_icon} Риск · {_required_value(_risk_percent_text(state))}",
]
strategy = (state.strategy or "").upper()
@@ -91,8 +90,7 @@ def _build_not_configured_text(state) -> str:
)
sl_icon = "" if sl_value != "⏤" else "⚠️"
-
- parts.append(f"{sl_icon} SL · {sl_value}")
+ parts.append(f"{sl_icon} SL · {sl_value}")
parts.extend([
"",
@@ -109,7 +107,6 @@ def _build_stopped_without_position_text(state) -> str:
estimated_size = _estimated_size(state, price)
rr_line = _risk_reward_line(state)
-
risk_line = _risk_summary_line(
state,
estimated_size,
@@ -120,14 +117,14 @@ def _build_stopped_without_position_text(state) -> str:
f"🤖 Автоторговля {_status_text(state.status)}",
_account_mode_line(),
"",
- f"Доступно · $ {_format_money_compact(available)}\n",
- "🧾 Подготовка ордера",
+ f"Доступно 💰 {_format_money_compact(available)}",
"",
+ "Подготовка ордера 🧾",
_order_header_line(state),
- f"Цена · {_format_usd_or_dash(price)}",
+ f"Цена · {_format_plain_or_dash(price)}",
_estimated_size_text(state, price),
_max_reserved_line(state, price),
- f"Риск · {_risk_percent_text(state)} ($ {_format_money_compact(_target_risk_usd(state))})",
+ f"Риск · {_format_money_compact(_target_risk_usd(state))}",
]
if rr_line or risk_line:
@@ -157,8 +154,8 @@ def _build_waiting_text(state) -> str:
signal_lines = [
_signal_line(state),
- _market_state_line(state),
- _market_diagnostics_line(state),
+ _signal_confirmation_line(state),
+ _market_semantic_line(state),
_entry_block_line(state),
_execution_quality_line(state),
*_signal_confidence_lines(state),
@@ -171,17 +168,21 @@ def _build_waiting_text(state) -> str:
f"🤖 Автоторговля {_status_text(state.status)}",
_account_mode_line(),
"",
- f"Доступно · $ {_format_money_compact(available)}",
+ f"Доступно 💰 {_format_money_compact(available)}",
+ ]
+
+ if signal_lines:
+ parts.extend(["", *signal_lines])
+
+ parts.extend([
"",
- *signal_lines,
- "",
- "🧾 Подготовка ордера",
+ "Подготовка ордера 🧾",
_order_header_line(state),
- f"{_price_label_for_signal(state)} · {_format_usd_or_dash(price)}",
+ f"{_price_label_for_signal(state)} · {_format_plain_or_dash(price)}",
_estimated_size_text(state, price),
_max_reserved_line(state, price),
- f"Риск · {_risk_percent_text(state)} ($ {_format_money_compact(_target_risk_usd(state))})",
- ]
+ f"Риск · {_format_money_compact(_target_risk_usd(state))}",
+ ])
if rr_line or risk_line:
parts.append("")
@@ -210,29 +211,40 @@ def _build_active_position_text(state) -> str:
side_icon = "🟢" if state.position_side == "LONG" else "🔴"
+ market_lines = [
+ _market_semantic_line(state),
+ _execution_quality_line(state),
+ *_execution_block_lines(state),
+ ]
+ market_lines = [line for line in market_lines if line]
+
parts = [
f"🤖 Автоторговля {_status_text(state.status)}",
_account_mode_line(),
"",
- f"Доступно · $ {_format_money_compact(available)}",
- f"Зарезервировано · $ {_format_money_compact(reserved)}",
- f"P&L {_format_signed_usd_with_direction(pnl)}",
- _market_state_line(state),
- _execution_quality_line(state),
- *_execution_block_lines(state),
+ f"Доступно 💰 {_format_money_compact(available)}",
+ f"Маржа · {_format_money_compact(reserved)}",
+ f"P&L {_format_signed_plain_with_direction(pnl)}",
+ ]
+
+ if market_lines:
+ parts.extend(["", *market_lines])
+
+ parts.extend([
"",
(
- f"{side_icon} {_asset_symbol(state.symbol)} · "
+ f"{side_icon} {_asset_symbol(state.symbol)} · "
f"{_strategy_short(state.strategy)} · "
- f"{state.position_side} {_leverage_text(state.leverage)}"
+ f"{state.position_side} {_leverage_text(state.leverage)}"
),
"",
- f"Количество · {_format_crypto_size(size)} ⇢ $ {_format_money_compact(notional)}",
- f"Цена входа · $ {_format_money(state.entry_price)}",
- f"Текущая цена · {_format_usd_or_dash(current_price)}",
+ f"Размер · {_format_crypto_size(size)}",
+ f"Позиция · {_format_money_compact(notional)}",
+ f"Вход · {_format_plain_or_dash(state.entry_price)}",
+ f"Цена · {_format_plain_or_dash(price_for_calc)}",
"",
"⚠️ Комиссии не учтены",
- ]
+ ])
if rr_line or risk_line:
parts.append("")
@@ -246,63 +258,72 @@ def _build_active_position_text(state) -> str:
return "\n".join(parts)
-def _market_state_line(state) -> str:
+def _market_semantic_line(state) -> str:
market_state = getattr(state, "market_state", None)
-
- labels = {
- "TREND_UP": "📈 Тренд · Вверх",
- "TREND_DOWN": "📉 Тренд · Вниз",
- "RANGE": "🟰 Рынок · Флэт",
- "HIGH_VOLATILITY": "⚠️ Рынок · Высокая волатильность",
- "LOW_VOLATILITY": "🟰 Рынок · Низкая активность",
- "UNKNOWN": "⏳ Рынок · Идёт анализ",
- None: "⏳ Рынок · Идёт анализ",
- }
-
- return labels.get(market_state, "⏳ Рынок · Идёт анализ")
-
-
-def _market_diagnostics_line(state) -> str:
+ trend = getattr(state, "market_trend", None)
strength = getattr(state, "market_trend_strength", None)
quality = getattr(state, "market_trend_quality", None)
phase = getattr(state, "market_phase", None)
- if not strength and not quality and not phase:
- return ""
+ if market_state in {None, "UNKNOWN"}:
+ return "⏳ Рынок · анализ"
- strength_labels = {
- "WEAK": "слабый",
- "NORMAL": "нормальный",
- "STRONG": "сильный",
- }
+ if market_state == "HIGH_VOLATILITY":
+ return "⚠️ Рынок · перегрев"
- quality_labels = {
- "CLEAN": "чистый",
- "NOISY": "шумный",
- }
+ if market_state == "LOW_VOLATILITY" or phase == "SQUEEZE":
+ return "🟦 Рынок · сжатие"
- phase_labels = {
- "IMPULSE": "импульс",
- "PULLBACK": "откат",
- "RANGE": "флэт",
- "SQUEEZE": "сжатие",
- }
+ if market_state == "RANGE" or phase == "RANGE":
+ return "🟰 Рынок · флэт"
- parts = []
+ if phase == "PULLBACK":
+ if trend == "UP":
+ return "↘️ Рынок · коррекция"
- if strength in strength_labels:
- parts.append(strength_labels[strength])
+ if trend == "DOWN":
+ return "↗️ Рынок · откат вверх"
- if quality in quality_labels:
- parts.append(quality_labels[quality])
+ return "↔️ Рынок · откат"
- if phase in phase_labels:
- parts.append(phase_labels[phase])
+ if quality == "NOISY":
+ if trend == "UP":
+ return "⚠️ Рынок · шумный рост"
- if not parts:
- return ""
+ if trend == "DOWN":
+ return "⚠️ Рынок · шумное снижение"
- return f"Анализ · {' · '.join(parts)}"
+ return "⚠️ Рынок · шум"
+
+ if strength == "WEAK":
+ if trend == "UP":
+ return "🟡 Рынок · слабый рост"
+
+ if trend == "DOWN":
+ return "🟡 Рынок · слабое снижение"
+
+ return "🟡 Рынок · слабое движение"
+
+ if phase == "IMPULSE":
+ if trend == "UP" and strength == "STRONG":
+ return "⚡ Рынок · сильный рост"
+
+ if trend == "DOWN" and strength == "STRONG":
+ return "⚡ Рынок · сильное снижение"
+
+ if trend == "UP":
+ return "📈 Рынок · рост"
+
+ if trend == "DOWN":
+ return "📉 Рынок · снижение"
+
+ if trend == "UP":
+ return "📈 Рынок · рост"
+
+ if trend == "DOWN":
+ return "📉 Рынок · снижение"
+
+ return "⏳ Рынок · анализ"
def _compact_entry_block_message(message: str) -> str:
@@ -328,15 +349,8 @@ def _entry_block_line(state) -> str:
return ""
compact_message = _compact_entry_block_message(str(message))
- signal = (state.last_signal or "HOLD").upper()
- if signal == "HOLD":
- return f"Условие · {compact_message}"
-
- if signal in {"BUY", "SELL"}:
- return f"Вход · {compact_message}"
-
- return ""
+ return f"🧩 Фильтр · {compact_message}"
def _execution_quality_line(state) -> str:
@@ -352,10 +366,10 @@ def _execution_quality_line(state) -> str:
return ""
if reason == "WIDE_SPREAD" and spread_percent is not None:
- return f"⚠️ Рынок · spread {_format_percent(spread_percent)}"
+ return f"⚠️ Вход · spread {_format_percent(spread_percent)}"
if reason == "AGING_SNAPSHOT" and age_seconds is not None:
- return f"⚠️ Рынок · данные стареют ({age_seconds:.1f}с)"
+ return f"⚠️ Вход · данные стареют {age_seconds:.1f}с"
if reason == "STALE_SNAPSHOT":
return "⛔ Вход · рынок неактуален"
@@ -364,7 +378,7 @@ def _execution_quality_line(state) -> str:
return f"⛔ Вход · высокий spread {_format_percent(spread_percent)}"
if reason == "SNAPSHOT_UNAVAILABLE":
- return "⚠️ Рынок · нет depth snapshot"
+ return "⚠️ Вход · нет стакана"
if reason == "SNAPSHOT_ERROR":
return "⛔ Вход · нет данных рынка"
@@ -374,7 +388,7 @@ def _execution_quality_line(state) -> str:
if not message:
return ""
- return f"⚠️ Рынок · {message}"
+ return f"⚠️ Вход · {message}"
def _execution_block_lines(state) -> list[str]:
@@ -392,9 +406,7 @@ def _execution_block_lines(state) -> list[str]:
adjustment = getattr(state, "execution_size_adjustment_reason", None)
if adjustment == "MARGIN_LIMIT":
- lines.append(
- "Позиция ограничена настройкой Max Reserved."
- )
+ lines.append("Позиция ограничена настройкой Max Reserved.")
return lines
@@ -428,18 +440,16 @@ def _max_reserved_line(state, price: float | None = None) -> str:
size = _estimated_size(state, price)
if size is None or price is None or price <= 0:
- return "Собственные средства · —"
+ return "Маржа · —"
leverage = state.leverage or 1.0
if leverage <= 0:
- return "Собственные средства · —"
+ return "Маржа · —"
position_size_usd = size * price
own_funds_usd = position_size_usd / leverage
- return (
- f"Собственные средства · $ {_format_money_compact(own_funds_usd)}"
- )
+ return f"Маржа · {_format_money_compact(own_funds_usd)}"
def _market_snapshot(symbol: str | None) -> dict[str, object] | None:
@@ -532,13 +542,13 @@ def _estimated_size(state, price: float | None) -> float | None:
def _estimated_size_text(state, price: float | None) -> str:
size = _estimated_size(state, price)
if size is None or price is None:
- return "Количество · —"
+ return "Размер · —\nПозиция · —"
notional = size * price
return (
- f"Количество · {_format_crypto_size(size)}\n"
- f"Размер позиции · $ {_format_money_compact(notional)}"
+ f"Размер · {_format_crypto_size(size)}\n"
+ f"Позиция · {_format_money_compact(notional)}"
)
@@ -569,9 +579,9 @@ def _risk_summary_line(
)
items = [
- f"SL {sl}" if sl else "SL off",
- f"TP {tp}" if tp else "TP off",
- f"ML {ml}" if ml else "ML off",
+ f"SL {sl}" if sl else "SL off",
+ f"TP {tp}" if tp else "TP off",
+ f"ML {ml}" if ml else "ML off",
]
return " | ".join(items)
@@ -585,19 +595,19 @@ def _risk_loss_text(
entry_price: float | None,
) -> str:
if fixed_loss is not None:
- return f"-$ {_format_money_compact(abs(fixed_loss))}"
+ return f"-{_format_money_compact(abs(fixed_loss))}"
if percent is None:
return ""
if size is None or size <= 0 or entry_price is None or entry_price <= 0:
loss = _target_loss_by_percent_stub(percent)
- return f"-$ {_format_money_compact(loss)}" if loss is not None else ""
+ return f"-{_format_money_compact(loss)}" if loss is not None else ""
move = entry_price * (percent / 100)
loss = move * size
- return f"-$ {_format_money_compact(loss)}"
+ return f"-{_format_money_compact(loss)}"
def _risk_profit_text(
@@ -615,7 +625,7 @@ def _risk_profit_text(
move = entry_price * (percent / 100)
profit = move * size
- return f"+$ {_format_money_compact(profit)}"
+ return f"+{_format_money_compact(profit)}"
def _target_loss_by_percent_stub(percent: float | None) -> float | None:
@@ -635,7 +645,7 @@ def _risk_reward_line(state) -> str:
return ""
ratio = state.take_profit_percent / state.stop_loss_percent
- return f"R:R = 1 : {_format_ratio_value(ratio)}"
+ return f"R:R = 1 : {_format_ratio_value(ratio)}"
def _order_header_line(state) -> str:
@@ -645,14 +655,14 @@ def _order_header_line(state) -> str:
return (
f"🟢 {_asset_symbol(state.symbol)} · "
f"{_strategy_short(state.strategy)} · "
- f"LONG {_leverage_text(state.leverage)}"
+ f"LONG {_leverage_text(state.leverage)}"
)
if signal == "SELL":
return (
f"🔴 {_asset_symbol(state.symbol)} · "
f"{_strategy_short(state.strategy)} · "
- f"SHORT {_leverage_text(state.leverage)}"
+ f"SHORT {_leverage_text(state.leverage)}"
)
return (
@@ -666,7 +676,7 @@ def _price_label_for_signal(state) -> str:
signal = (state.last_signal or "").upper()
if signal in {"BUY", "SELL"}:
- return "Цена входа"
+ return "Вход"
return "Цена"
@@ -674,15 +684,52 @@ def _price_label_for_signal(state) -> str:
def _signal_line(state) -> str:
signal = (state.last_signal or "HOLD").upper()
+ signal_text = f"Сигнал {_signal_icon(signal)} {signal}"
+
if signal in {"BUY", "SELL"} and (
state.decision_status == "READY"
or getattr(state, "is_signal_ready", False)
):
- return f"Сигнал {_signal_icon(signal)} {signal} · READY"
+ return f"{signal_text} · READY"
duration = _signal_duration_text(state)
- return f"Сигнал {_signal_icon(signal)} {signal} · {duration}"
+ return f"{signal_text} · {duration}"
+
+
+def _signal_confirmation_line(state) -> str:
+ signal = (state.last_signal or "HOLD").upper()
+
+ if signal not in {"BUY", "SELL"}:
+ return ""
+
+ status = getattr(state, "decision_status", None)
+
+ seconds = int(getattr(state, "signal_confirmation_seconds", 0) or 0)
+ required_seconds = int(
+ getattr(state, "signal_confirmation_required_seconds", 10) or 10
+ )
+ repeats = int(getattr(state, "last_signal_repeat_count", 0) or 0)
+
+ missing_repeats = int(
+ getattr(state, "signal_confirmation_missing_repeats", 0) or 0
+ )
+ required_repeats = repeats + missing_repeats
+
+ if status == "READY" or getattr(state, "is_signal_ready", False):
+ return "✅ Подтверждение · готово"
+
+ if status == "BLOCKED":
+ return "⛔ Подтверждение · заблокировано"
+
+ if status == "CONFIRMING":
+ return (
+ f"⏳ Подтверждение · "
+ f"{repeats}/{required_repeats} · "
+ f"{seconds}/{required_seconds}с"
+ )
+
+ return ""
def _signal_duration_text(state) -> str:
@@ -768,6 +815,7 @@ def _strategy_short(strategy: str | None) -> str:
def _leverage_text(value: float | None) -> str:
if value is None:
return "x—"
+
return f"x{value:g}"
@@ -802,6 +850,7 @@ def _signal_icon(signal: str | None) -> str:
"SELL": "🔴",
"HOLD": "🟡",
}
+
return mapping.get(signal or "", "")
@@ -817,6 +866,7 @@ def _round_size(value: float | int | None) -> float | None:
def _format_crypto_size(value: float | int | None) -> str:
rounded = _round_size(value)
+
if rounded is None:
return "—"
@@ -835,13 +885,6 @@ def _format_percent(value: float | int | None) -> str:
return f"{number:.2f}".rstrip("0").rstrip(".") + "%"
-def _format_money(value: float | int | None) -> str:
- if value is None:
- return "—"
-
- return format_usd_amount(float(value))
-
-
def _format_money_compact(value: float | int | None) -> str:
if value is None:
return "—"
@@ -854,11 +897,15 @@ def _format_money_compact(value: float | int | None) -> str:
return f"{number:,.2f}".replace(",", " ").rstrip("0").rstrip(".")
-def _format_usd_or_dash(value: float | None) -> str:
+def _format_plain_or_dash(value: float | int | None) -> str:
if value is None:
return "—"
- return f"$ {_format_money(value)}"
+ return _format_money_compact(value)
+
+
+def _format_usd_or_dash(value: float | None) -> str:
+ return _format_plain_or_dash(value)
def _format_signed_usd(value: float | int | None) -> str:
@@ -868,24 +915,28 @@ def _format_signed_usd(value: float | int | None) -> str:
amount = float(value)
if amount > 0:
- return f"+$ {_format_money_compact(amount)}"
+ return f"+{_format_money_compact(amount)}"
if amount < 0:
- return f"−$ {_format_money_compact(abs(amount))}"
+ return f"−{_format_money_compact(abs(amount))}"
- return "$ 0"
+ return "0"
def _format_signed_usd_with_direction(value: float | int | None) -> str:
+ return _format_signed_plain_with_direction(value)
+
+
+def _format_signed_plain_with_direction(value: float | int | None) -> str:
if value is None:
return "—"
amount = float(value)
if amount > 0:
- return f"🟢 +$ {_format_money_compact(amount)}"
+ return f"🟢 +{_format_money_compact(amount)}"
if amount < 0:
- return f"🔴 −$ {_format_money_compact(abs(amount))}"
+ return f"🔴 −{_format_money_compact(abs(amount))}"
- return "$ 0"
\ No newline at end of file
+ return "0"
\ No newline at end of file
diff --git a/app/src/trading/auto/service.py b/app/src/trading/auto/service.py
index 3f05f55..a0042a0 100644
--- a/app/src/trading/auto/service.py
+++ b/app/src/trading/auto/service.py
@@ -24,6 +24,9 @@ class AutoTradeService:
# минимальное количество повторов BUY / SELL для подтверждения сигнала
_confirm_repeats = 2
+ # минимальное время удержания BUY / SELL сигнала для подтверждения
+ _confirm_min_duration_seconds = 10
+
# минимальная уверенность для готовности к будущему execution
_ready_confidence = 0.3
@@ -114,6 +117,11 @@ class AutoTradeService:
state.last_signal_repeat_count = repeat_count
state.last_signal_confidence = confidence
state.last_signal_reason = reason
+ state.signal_confirmation_seconds = self._confirm_min_duration_seconds
+ state.signal_confirmation_required_seconds = self._confirm_min_duration_seconds
+ state.signal_confirmation_missing_repeats = 0
+ state.signal_confirmation_progress = 1.0
+ state.signal_confirmation_reason = "debug confirmation"
if normalized_signal == "HOLD":
state.decision_status = "WAITING"
@@ -359,6 +367,11 @@ class AutoTradeService:
state.decision_reason = None
state.is_signal_confirmed = False
state.is_signal_ready = False
+ state.signal_confirmation_seconds = 0
+ state.signal_confirmation_required_seconds = self._confirm_min_duration_seconds
+ state.signal_confirmation_missing_repeats = self._confirm_repeats
+ state.signal_confirmation_progress = 0.0
+ state.signal_confirmation_reason = None
state.execution_block_reason = None
state.signal_started_at = None
state.signal_updated_at = None
@@ -436,20 +449,60 @@ class AutoTradeService:
state.is_signal_confirmed = False
state.is_signal_ready = False
+ state.signal_confirmation_required_seconds = self._confirm_min_duration_seconds
+
if signal == "HOLD":
+ state.signal_confirmation_seconds = 0
+ state.signal_confirmation_missing_repeats = self._confirm_repeats
+ state.signal_confirmation_progress = 0.0
+ state.signal_confirmation_reason = None
state.decision_status = "WAITING"
state.decision_reason = "Нет торгового направления."
return
- if self._same_signal_count < self._confirm_repeats:
+ now = time.monotonic()
+
+ if state.signal_started_at is None:
+ signal_age_seconds = 0
+ else:
+ signal_age_seconds = max(0, int(now - float(state.signal_started_at)))
+
+ missing_repeats = max(0, self._confirm_repeats - self._same_signal_count)
+ missing_seconds = max(
+ 0,
+ self._confirm_min_duration_seconds - signal_age_seconds,
+ )
+
+ repeat_progress = min(
+ 1.0,
+ self._same_signal_count / max(1, self._confirm_repeats),
+ )
+ time_progress = min(
+ 1.0,
+ signal_age_seconds / max(1, self._confirm_min_duration_seconds),
+ )
+
+ confirmation_progress = min(repeat_progress, time_progress)
+
+ state.signal_confirmation_seconds = signal_age_seconds
+ state.signal_confirmation_missing_repeats = missing_repeats
+ state.signal_confirmation_progress = round(confirmation_progress, 3)
+
+ if missing_repeats > 0 or missing_seconds > 0:
state.decision_status = "CONFIRMING"
+ state.signal_confirmation_reason = (
+ f"{self._same_signal_count}/{self._confirm_repeats} повторов, "
+ f"{signal_age_seconds}/{self._confirm_min_duration_seconds}с"
+ )
state.decision_reason = (
f"Сигнал {signal} подтверждается: "
- f"{self._same_signal_count}/{self._confirm_repeats} повторов."
+ f"{self._same_signal_count}/{self._confirm_repeats} повторов, "
+ f"{signal_age_seconds}/{self._confirm_min_duration_seconds}с."
)
return
state.is_signal_confirmed = True
+ state.signal_confirmation_reason = "сигнал подтверждён"
if confidence < self._ready_confidence:
state.decision_status = "BLOCKED"
@@ -460,9 +513,10 @@ class AutoTradeService:
return
state.is_signal_ready = True
+ state.signal_confirmation_progress = 1.0
state.decision_status = "READY"
state.decision_reason = (
- f"Сигнал {signal} подтверждён и готов к будущему execution."
+ f"Сигнал {signal} подтверждён по повторам и времени удержания."
)
# записать новый сигнал и итог предыдущей серии при смене сигнала
@@ -728,6 +782,9 @@ class AutoTradeService:
"decision_status": state.decision_status,
"is_strong_signal": confidence > self._ready_confidence,
"is_aggregated": False,
+ "confirmation_seconds": state.signal_confirmation_seconds,
+ "confirmation_required_seconds": state.signal_confirmation_required_seconds,
+ "confirmation_progress": state.signal_confirmation_progress,
},
)
except Exception:
diff --git a/app/src/trading/auto/state.py b/app/src/trading/auto/state.py
index d40fcfe..14e2cbb 100644
--- a/app/src/trading/auto/state.py
+++ b/app/src/trading/auto/state.py
@@ -164,4 +164,19 @@ class AutoTradeState:
execution_quality_message: str | None = None
# признак деградации runtime market data
- market_runtime_degraded: bool = False
\ No newline at end of file
+ market_runtime_degraded: bool = False
+
+ # сколько секунд текущий BUY / SELL сигнал удерживается
+ signal_confirmation_seconds: int = 0
+
+ # сколько секунд нужно удерживать BUY / SELL сигнал для подтверждения
+ signal_confirmation_required_seconds: int = 10
+
+ # сколько повторов ещё не хватает до подтверждения
+ signal_confirmation_missing_repeats: int = 0
+
+ # прогресс подтверждения сигнала от 0.0 до 1.0
+ signal_confirmation_progress: float = 0.0
+
+ # человекочитаемая причина текущего confirmation status
+ signal_confirmation_reason: str | None = None
\ No newline at end of file
diff --git a/docs/roadmap/master-roadmap.md b/docs/roadmap/master-roadmap.md
index 39b768e..c65310d 100644
--- a/docs/roadmap/master-roadmap.md
+++ b/docs/roadmap/master-roadmap.md
@@ -683,6 +683,108 @@
- подготовлена база для semantic entry filters
- подготовлена база для более точного TREND execution
+#### 07.4.4.1.9.1 ✅ Market Semantic Runtime Layer
+- добавлен semantic runtime layer поверх adaptive market diagnostics
+- добавлена единая semantic-интерпретация market_state / trend / strength / quality / phase
+- добавлена функция `_market_semantic_line()` в Telegram UI
+- `_market_semantic_line()` заменяет связку `_market_state_line()` и `_market_diagnostics_line()`
+- UI больше не показывает раздельно технические признаки тренда и фазы
+- UI теперь показывает одно итоговое смысловое состояние рынка
+- добавлена semantic-нормализация TREND_UP состояний
+- добавлена semantic-нормализация TREND_DOWN состояний
+- добавлена semantic-нормализация RANGE состояния
+- добавлена semantic-нормализация LOW_VOLATILITY / SQUEEZE состояния
+- добавлена semantic-нормализация HIGH_VOLATILITY состояния
+- добавлена semantic-нормализация UNKNOWN состояния
+- добавлено состояние `⚡ Рынок · сильный рост`
+- добавлено состояние `⚡ Рынок · сильное снижение`
+- добавлено состояние `📈 Рынок · рост`
+- добавлено состояние `📉 Рынок · снижение`
+- добавлено состояние `🟡 Рынок · слабый рост`
+- добавлено состояние `🟡 Рынок · слабое снижение`
+- добавлено состояние `⚠️ Рынок · шумный рост`
+- добавлено состояние `⚠️ Рынок · шумное снижение`
+- добавлено состояние `↘️ Рынок · коррекция`
+- добавлено состояние `↗️ Рынок · откат вверх`
+- добавлено состояние `🟰 Рынок · флэт`
+- добавлено состояние `🟦 Рынок · сжатие`
+- добавлено состояние `⚠️ Рынок · перегрев`
+- добавлено состояние `⏳ Рынок · анализ`
+- добавлен приоритет semantic-состояний рынка
+- HIGH_VOLATILITY получает приоритет над обычным трендом
+- LOW_VOLATILITY / SQUEEZE отображается как semantic-сжатие
+- RANGE отображается как semantic-флэт
+- PULLBACK отображается как коррекция или откат вверх
+- NOISY trend отображается как шумный рост или шумное снижение
+- WEAK trend отображается как слабый рост или слабое снижение
+- STRONG + CLEAN + IMPULSE отображается как сильный рост или сильное снижение
+- NORMAL + CLEAN + IMPULSE отображается как рост или снижение
+- fallback по направлению тренда сохранён
+- execution quality не смешивается с market semantic layer
+- строки `Вход · spread`, `Вход · нет стакана`, `Вход · нет данных рынка` остались отдельными
+- строка `Фильтр` осталась отдельной для причин HOLD
+- экран ожидания AutoTrade переведён на semantic market line
+- экран активной позиции AutoTrade переведён на semantic market line
+- убрано дублирование `Тренд` + `Фаза` в UI
+- экран автоторговли стал короче
+- экран автоторговли стал понятнее для runtime-наблюдения
+- HOLD diagnostics стали более смысловыми
+- market state теперь отображается как торговый смысл, а не как raw enum
+- подготовлена база для semantic entry filters
+- подготовлена база для market regime scoring
+- подготовлена база для adaptive threshold tuning
+- подготовлена база для confidence scoring по состоянию рынка
+- подготовлена база для объяснимого AutoTrade decision screen
+
+#### 07.4.4.1.9.2 ✅ Signal Confirmation Runtime
+- добавлен Signal Confirmation Runtime layer
+- confirmation runtime вынесен в отдельный stateful lifecycle
+- BUY / SELL больше не переходят мгновенно в READY
+- добавлена runtime-фаза CONFIRMING
+- confirmation теперь требует repeat consistency
+- confirmation теперь требует time persistence
+- добавлен minimum signal lifetime filter
+- добавлен `_confirm_min_duration_seconds`
+- BUY / SELL теперь обязаны удерживаться во времени
+- runtime теперь анализирует signal persistence
+- runtime теперь анализирует directional continuity
+- runtime теперь анализирует signal lifetime
+- добавлен confirmation progress runtime
+- добавлен `signal_confirmation_seconds`
+- добавлен `signal_confirmation_required_seconds`
+- добавлен `signal_confirmation_missing_repeats`
+- добавлен `signal_confirmation_progress`
+- добавлен `signal_confirmation_reason`
+- confirmation progress теперь рассчитывается по repeat progress
+- confirmation progress теперь рассчитывается по time progress
+- BUY / SELL проходят lifecycle HOLD → CONFIRMING → READY
+- HOLD теперь сбрасывает confirmation progress
+- HOLD теперь очищает confirmation runtime state
+- READY теперь требует repeats и удержания сигнала
+- добавлена защита от micro-breakout noise
+- добавлена защита от single candle reversal
+- добавлена защита от volatility spikes
+- TREND runtime стал устойчивее к ложным импульсам
+- улучшено распознавание устойчивого directional movement
+- улучшена фильтрация краткосрочного рыночного шума
+- Telegram UI получил runtime confirmation line
+- UI показывает `⏳ Подтверждение`
+- UI показывает `✅ Подтверждение · готово`
+- confirmation line отображается только для BUY / SELL
+- HOLD больше не показывает confirmation line
+- confirmation runtime интегрирован в decision lifecycle
+- decision_status теперь полноценно использует CONFIRMING state
+- signal confirmation runtime синхронизирован с READY state
+- debug_force_signal обновляет confirmation runtime state
+- signal_ready journal events расширены confirmation analytics
+- signal runtime стал более stateful
+- AutoTrade приблизился к professional signal confirmation flow
+- подготовлена база для adaptive confirmation thresholds
+- подготовлена база для probabilistic signal scoring
+- подготовлена база для multi-timeframe confirmation
+- подготовлена база для direction persistence engine
+- подготовлена база для execution scoring system
+- подготовлена база для probabilistic signal engine
---
diff --git a/docs/roadmap/stage-07-auto-trading-roadmap.md b/docs/roadmap/stage-07-auto-trading-roadmap.md
index 9347801..01bd784 100644
--- a/docs/roadmap/stage-07-auto-trading-roadmap.md
+++ b/docs/roadmap/stage-07-auto-trading-roadmap.md
@@ -659,6 +659,109 @@
- подготовлена база для semantic entry filters
- подготовлена база для более точного TREND execution
+#### 07.4.4.1.9.1 ✅ Market Semantic Runtime Layer
+- добавлен semantic runtime layer поверх adaptive market diagnostics
+- добавлена единая semantic-интерпретация market_state / trend / strength / quality / phase
+- добавлена функция `_market_semantic_line()` в Telegram UI
+- `_market_semantic_line()` заменяет связку `_market_state_line()` и `_market_diagnostics_line()`
+- UI больше не показывает раздельно технические признаки тренда и фазы
+- UI теперь показывает одно итоговое смысловое состояние рынка
+- добавлена semantic-нормализация TREND_UP состояний
+- добавлена semantic-нормализация TREND_DOWN состояний
+- добавлена semantic-нормализация RANGE состояния
+- добавлена semantic-нормализация LOW_VOLATILITY / SQUEEZE состояния
+- добавлена semantic-нормализация HIGH_VOLATILITY состояния
+- добавлена semantic-нормализация UNKNOWN состояния
+- добавлено состояние `⚡ Рынок · сильный рост`
+- добавлено состояние `⚡ Рынок · сильное снижение`
+- добавлено состояние `📈 Рынок · рост`
+- добавлено состояние `📉 Рынок · снижение`
+- добавлено состояние `🟡 Рынок · слабый рост`
+- добавлено состояние `🟡 Рынок · слабое снижение`
+- добавлено состояние `⚠️ Рынок · шумный рост`
+- добавлено состояние `⚠️ Рынок · шумное снижение`
+- добавлено состояние `↘️ Рынок · коррекция`
+- добавлено состояние `↗️ Рынок · откат вверх`
+- добавлено состояние `🟰 Рынок · флэт`
+- добавлено состояние `🟦 Рынок · сжатие`
+- добавлено состояние `⚠️ Рынок · перегрев`
+- добавлено состояние `⏳ Рынок · анализ`
+- добавлен приоритет semantic-состояний рынка
+- HIGH_VOLATILITY получает приоритет над обычным трендом
+- LOW_VOLATILITY / SQUEEZE отображается как semantic-сжатие
+- RANGE отображается как semantic-флэт
+- PULLBACK отображается как коррекция или откат вверх
+- NOISY trend отображается как шумный рост или шумное снижение
+- WEAK trend отображается как слабый рост или слабое снижение
+- STRONG + CLEAN + IMPULSE отображается как сильный рост или сильное снижение
+- NORMAL + CLEAN + IMPULSE отображается как рост или снижение
+- fallback по направлению тренда сохранён
+- execution quality не смешивается с market semantic layer
+- строки `Вход · spread`, `Вход · нет стакана`, `Вход · нет данных рынка` остались отдельными
+- строка `Фильтр` осталась отдельной для причин HOLD
+- экран ожидания AutoTrade переведён на semantic market line
+- экран активной позиции AutoTrade переведён на semantic market line
+- убрано дублирование `Тренд` + `Фаза` в UI
+- экран автоторговли стал короче
+- экран автоторговли стал понятнее для runtime-наблюдения
+- HOLD diagnostics стали более смысловыми
+- market state теперь отображается как торговый смысл, а не как raw enum
+- подготовлена база для semantic entry filters
+- подготовлена база для market regime scoring
+- подготовлена база для adaptive threshold tuning
+- подготовлена база для confidence scoring по состоянию рынка
+- подготовлена база для объяснимого AutoTrade decision screen
+
+#### 07.4.4.1.9.2 ✅ Signal Confirmation Runtime
+- добавлен Signal Confirmation Runtime layer
+- confirmation runtime вынесен в отдельный stateful lifecycle
+- BUY / SELL больше не переходят мгновенно в READY
+- добавлена runtime-фаза CONFIRMING
+- confirmation теперь требует repeat consistency
+- confirmation теперь требует time persistence
+- добавлен minimum signal lifetime filter
+- добавлен `_confirm_min_duration_seconds`
+- BUY / SELL теперь обязаны удерживаться во времени
+- runtime теперь анализирует signal persistence
+- runtime теперь анализирует directional continuity
+- runtime теперь анализирует signal lifetime
+- добавлен confirmation progress runtime
+- добавлен `signal_confirmation_seconds`
+- добавлен `signal_confirmation_required_seconds`
+- добавлен `signal_confirmation_missing_repeats`
+- добавлен `signal_confirmation_progress`
+- добавлен `signal_confirmation_reason`
+- confirmation progress теперь рассчитывается по repeat progress
+- confirmation progress теперь рассчитывается по time progress
+- BUY / SELL проходят lifecycle HOLD → CONFIRMING → READY
+- HOLD теперь сбрасывает confirmation progress
+- HOLD теперь очищает confirmation runtime state
+- READY теперь требует repeats и удержания сигнала
+- добавлена защита от micro-breakout noise
+- добавлена защита от single candle reversal
+- добавлена защита от volatility spikes
+- TREND runtime стал устойчивее к ложным импульсам
+- улучшено распознавание устойчивого directional movement
+- улучшена фильтрация краткосрочного рыночного шума
+- Telegram UI получил runtime confirmation line
+- UI показывает `⏳ Подтверждение`
+- UI показывает `✅ Подтверждение · готово`
+- confirmation line отображается только для BUY / SELL
+- HOLD больше не показывает confirmation line
+- confirmation runtime интегрирован в decision lifecycle
+- decision_status теперь полноценно использует CONFIRMING state
+- signal confirmation runtime синхронизирован с READY state
+- debug_force_signal обновляет confirmation runtime state
+- signal_ready journal events расширены confirmation analytics
+- signal runtime стал более stateful
+- AutoTrade приблизился к professional signal confirmation flow
+- подготовлена база для adaptive confirmation thresholds
+- подготовлена база для probabilistic signal scoring
+- подготовлена база для multi-timeframe confirmation
+- подготовлена база для direction persistence engine
+- подготовлена база для execution scoring system
+- подготовлена база для probabilistic signal engine
+
---
### 07.4.5
diff --git a/docs/stages/stage-07_4_4_1_9_1-market_semantic_runtime_layer.md b/docs/stages/stage-07_4_4_1_9_1-market_semantic_runtime_layer.md
new file mode 100644
index 0000000..6b634b5
--- /dev/null
+++ b/docs/stages/stage-07_4_4_1_9_1-market_semantic_runtime_layer.md
@@ -0,0 +1,452 @@
+# 07.4.4.1.9.1 Market Semantic Runtime Layer
+
+## Что сделано
+
+Добавлен Market Semantic Runtime Layer — слой, который переводит технические состояния market analytics в понятные конечные semantic-состояния для UI и дальнейших runtime-фильтров.
+
+До этого этапа система уже умела рассчитывать расширенную диагностику рынка:
+
+- market_state
+- market_trend
+- market_volatility
+- market_trend_strength
+- market_trend_quality
+- market_phase
+- market_trend_gap_percent
+- market_trend_consistency
+
+Но UI отображал эти признаки раздельно:
+
+- Тренд · Вверх
+- Фаза · коррекция
+- Качество · шум
+- Сила · слабая
+
+Это было информативно, но могло путать, потому что одно и то же состояние рынка раскладывалось на несколько строк.
+
+На этом этапе добавлен semantic layer, который собирает runtime-признаки в одну итоговую смысловую строку.
+
+Теперь вместо набора технических признаков экран показывает одно конечное состояние рынка:
+
+```text
+⚡ Рынок · сильный рост
+```
+
+или:
+
+```text
+↘️ Рынок · коррекция
+```
+
+или:
+
+```text
+⚠️ Рынок · шумный рост
+```
+
+## Главная цель этапа
+
+Цель этапа — сделать market analytics не просто технической диагностикой, а понятным semantic runtime-состоянием.
+
+Система теперь отвечает не только на вопрос:
+
+```text
+Какие технические признаки у рынка?
+```
+
+но и на вопрос:
+
+```text
+Что это состояние рынка значит для автоторговли прямо сейчас?
+```
+
+## Что изменилось в аналитике
+
+### 1. Добавлена semantic-нормализация market state
+
+Ранее MarketAnalysisService и TrendStrategy передавали в UI набор независимых признаков:
+
+```text
+TREND_UP
+STRONG
+CLEAN
+IMPULSE
+```
+
+Теперь UI получает возможность интерпретировать эту комбинацию как единое смысловое состояние:
+
+```text
+⚡ Рынок · сильный рост
+```
+
+То есть слой не заменяет аналитику, а делает её результат удобным для принятия решения.
+
+### 2. Введена единая строка состояния рынка
+
+В Telegram UI добавлена функция:
+
+```python
+_market_semantic_line(state)
+```
+
+Она заменяет старую пару строк:
+
+```python
+_market_state_line(state)
+_market_diagnostics_line(state)
+```
+
+Теперь вместо двух раздельных строк формируется одна semantic-строка.
+
+Было:
+
+```text
+📈 Тренд · Вверх
+⚡ Импульс · сильный
+```
+
+Стало:
+
+```text
+⚡ Рынок · сильный рост
+```
+
+Было:
+
+```text
+📈 Тренд · Вверх
+↘️ Фаза · коррекция
+```
+
+Стало:
+
+```text
+↘️ Рынок · коррекция
+```
+
+Было:
+
+```text
+📉 Тренд · Вниз
+⚠️ Качество · шум
+```
+
+Стало:
+
+```text
+⚠️ Рынок · шумное снижение
+```
+
+### 3. Убрано дублирование смыслов в UI
+
+До этапа состояние рынка могло одновременно отображаться как:
+
+```text
+📈 Тренд · Вверх
+↘️ Фаза · коррекция
+```
+
+или:
+
+```text
+🟰 Рынок · Флэт
+```
+
+и это смешивало уровни анализа:
+
+- направление
+- фазу
+- качество
+- силу
+- применимость для входа
+
+После внедрения semantic layer UI показывает итоговую интерпретацию, а не набор технических слоёв.
+
+### 4. Добавлена приоритетность market semantics
+
+При формировании semantic-состояния используется приоритет:
+
+1. UNKNOWN
+2. HIGH_VOLATILITY
+3. LOW_VOLATILITY / SQUEEZE
+4. RANGE
+5. PULLBACK
+6. NOISY
+7. WEAK
+8. STRONG IMPULSE
+9. NORMAL IMPULSE
+10. fallback по направлению тренда
+
+Это важно, потому что одно состояние может содержать несколько признаков одновременно.
+
+Например:
+
+```text
+TREND_UP + NOISY + PULLBACK
+```
+
+В этом случае важнее показать:
+
+```text
+↘️ Рынок · коррекция
+```
+
+а не просто:
+
+```text
+⚠️ Рынок · шумный рост
+```
+
+Потому что pullback сильнее влияет на вход TREND-стратегии.
+
+### 5. Улучшена читаемость TREND-аналитики
+
+Для стратегии TREND semantic layer особенно важен, потому что стратегия работает только тогда, когда рынок не просто направленный, а достаточно качественный для входа.
+
+Теперь пользователь видит не внутреннюю механику:
+
+```text
+trend=UP
+strength=WEAK
+quality=CLEAN
+phase=RANGE
+```
+
+а итог:
+
+```text
+🟡 Рынок · слабый рост
+```
+
+или:
+
+```text
+⚠️ Рынок · шумный рост
+```
+
+или:
+
+```text
+↘️ Рынок · коррекция
+```
+
+Это упрощает понимание причин HOLD.
+
+## Поддерживаемые semantic-состояния рынка
+
+### Рост
+
+```text
+⚡ Рынок · сильный рост
+📈 Рынок · рост
+🟡 Рынок · слабый рост
+⚠️ Рынок · шумный рост
+↘️ Рынок · коррекция
+```
+
+### Снижение
+
+```text
+⚡ Рынок · сильное снижение
+📉 Рынок · снижение
+🟡 Рынок · слабое снижение
+⚠️ Рынок · шумное снижение
+↗️ Рынок · откат вверх
+```
+
+### Нейтральные состояния
+
+```text
+🟰 Рынок · флэт
+🟦 Рынок · сжатие
+⚠️ Рынок · перегрев
+⏳ Рынок · анализ
+```
+
+## Как semantic-состояния связаны с TREND-стратегией
+
+### Потенциально рабочие состояния
+
+Эти состояния могут быть пригодны для входа, если live-импульс подтвердит направление:
+
+```text
+⚡ Рынок · сильный рост
+📈 Рынок · рост
+⚡ Рынок · сильное снижение
+📉 Рынок · снижение
+```
+
+### Блокирующие или HOLD-состояния
+
+Эти состояния не являются подходящими для TREND-входа:
+
+```text
+🟡 Рынок · слабый рост
+🟡 Рынок · слабое снижение
+⚠️ Рынок · шумный рост
+⚠️ Рынок · шумное снижение
+↘️ Рынок · коррекция
+↗️ Рынок · откат вверх
+🟰 Рынок · флэт
+🟦 Рынок · сжатие
+⚠️ Рынок · перегрев
+⏳ Рынок · анализ
+```
+
+Важно: semantic line сама по себе не открывает и не блокирует сделку.
+
+Решение принимает TrendStrategy и ExecutionEngine.
+
+Но semantic line теперь корректно показывает пользователю смысл этого решения.
+
+## Что изменилось в Telegram UI
+
+### Было
+
+UI показывал технические строки:
+
+```text
+📈 Тренд · Вверх
+↘️ Фаза · коррекция
+🧩 Фильтр · откат
+⚠️ Вход · spread 0.12%
+```
+
+### Стало
+
+UI показывает одну смысловую строку рынка:
+
+```text
+↘️ Рынок · коррекция
+🧩 Фильтр · откат
+⚠️ Вход · spread 0.12%
+```
+
+Или в рабочем сценарии:
+
+```text
+⚡ Рынок · сильный рост
+```
+
+Если дополнительных блокировок нет, лишние строки не отображаются.
+
+## Разделение ответственности строк UI
+
+После этапа строки разделены так:
+
+### Сигнал
+
+Показывает текущий торговый сигнал стратегии:
+
+```text
+Сигнал 🟡 HOLD · 27с
+Сигнал 🟢 BUY · READY
+Сигнал 🔴 SELL · READY
+```
+
+### Рынок
+
+Показывает semantic-состояние market analytics:
+
+```text
+⚡ Рынок · сильный рост
+↘️ Рынок · коррекция
+🟰 Рынок · флэт
+```
+
+### Фильтр
+
+Показывает причину, почему стратегия не даёт вход:
+
+```text
+🧩 Фильтр · слабый тренд
+🧩 Фильтр · шумный тренд
+🧩 Фильтр · откат
+```
+
+### Вход
+
+Показывает проблемы именно execution/runtime качества:
+
+```text
+⚠️ Вход · spread 0.12%
+⛔ Вход · высокий spread 0.18%
+⚠️ Вход · нет стакана
+⛔ Вход · нет данных рынка
+```
+
+## Что было исправлено
+
+- убрано дублирование строк market state и diagnostics
+- устранена неоднозначность между “Тренд”, “Рынок”, “Фаза”, “Качество”
+- semantic-состояние рынка теперь отображается одной строкой
+- UI стал стабильнее и короче
+- экран автоторговли стал легче читать
+- состояния рынка стали ближе к смыслу торгового решения
+- HOLD-причины стали понятнее
+- состояние “рынок готов” больше не перегружает UI лишними строками
+- execution quality остался отдельным от market semantics
+
+## Что осталось отдельным от semantic layer
+
+Market Semantic Runtime Layer не заменяет execution quality.
+
+То есть строки вида:
+
+```text
+⚠️ Вход · spread 0.12%
+⛔ Вход · высокий spread 0.18%
+```
+
+остаются отдельными, потому что они относятся не к анализу направления рынка, а к качеству входа и возможности исполнения.
+
+Это правильное разделение:
+
+- `Рынок` — что происходит с market structure
+- `Фильтр` — почему стратегия не входит
+- `Вход` — можно ли технически исполнить вход
+
+## Проверка
+
+После внедрения нужно проверить:
+
+```bash
+python -m compileall src
+```
+
+Runtime-проверка:
+
+- экран автоторговли открывается
+- Auto screen refresh не падает
+- строка `Рынок` отображается одной semantic-строкой
+- нет одновременного дубля `Тренд · Вверх` + `Фаза · ...`
+- HOLD timer продолжает обновляться
+- при слабом тренде отображается `🟡 Рынок · слабый рост/снижение`
+- при шумном тренде отображается `⚠️ Рынок · шумный рост/снижение`
+- при откате отображается `↘️ Рынок · коррекция` или `↗️ Рынок · откат вверх`
+- при флэте отображается `🟰 Рынок · флэт`
+- при squeeze отображается `🟦 Рынок · сжатие`
+- при высокой волатильности отображается `⚠️ Рынок · перегрев`
+- execution quality продолжает отображаться отдельной строкой `Вход`
+- spread warning не смешивается со строкой `Рынок`
+- active position screen тоже использует semantic market line
+
+## Результат
+
+Этап завершил переход от raw market diagnostics к semantic runtime-представлению.
+
+Теперь система имеет три уровня:
+
+1. MarketAnalysisService рассчитывает технические признаки.
+2. TrendStrategy использует признаки для HOLD / BUY / SELL.
+3. Telegram UI показывает человеку итоговое semantic-состояние рынка.
+
+Это подготовило базу для следующих этапов:
+
+- semantic entry filters
+- adaptive threshold tuning
+- market regime scoring
+- confidence scoring по semantic-состояниям
+- более точного TREND execution
+- future strategy arbitration
+- объяснимого AutoTrade decision screen
\ No newline at end of file
diff --git a/docs/stages/stage-07_4_4_1_9_2-signal_confirmation_runtime.md b/docs/stages/stage-07_4_4_1_9_2-signal_confirmation_runtime.md
new file mode 100644
index 0000000..eab5427
--- /dev/null
+++ b/docs/stages/stage-07_4_4_1_9_2-signal_confirmation_runtime.md
@@ -0,0 +1,212 @@
+# 07.4.4.1.9.2 Signal Confirmation Runtime
+
+## Что сделано
+
+Добавлен Signal Confirmation Runtime — отдельный runtime-слой подтверждения торгового сигнала, который находится между raw BUY/SELL сигналом стратегии и итоговым READY-состоянием AutoTrade.
+
+До этого этапа система подтверждала сигнал только количеством одинаковых повторов:
+
+```text
+BUY → BUY
+```
+
+или:
+
+```text
+SELL → SELL
+```
+
+После достижения `_confirm_repeats` сигнал сразу переходил в:
+
+```text
+READY
+```
+
+Теперь подтверждение сигнала стало полноценным runtime-процессом, который требует:
+
+1. повторяемости сигнала
+2. удержания сигнала во времени
+
+---
+
+# Главная цель этапа
+
+Цель этапа — научить AutoTrade отличать:
+
+```text
+кратковременный рыночный шум
+```
+
+от:
+
+```text
+устойчивого directional impulse
+```
+
+Теперь система оценивает не только:
+
+```text
+Что сигнал появился
+```
+
+но и:
+
+```text
+Насколько стабильно рынок удерживает это направление
+```
+
+---
+
+# Что изменилось в аналитике
+
+## 1. Подтверждение стало time-aware
+
+Ранее confirmation runtime был только repeat-based:
+
+```text
+BUY → BUY → READY
+```
+
+Теперь confirmation стал:
+
+```text
+BUY
+→ BUY удерживается
+→ BUY повторяется
+→ BUY живёт достаточно долго
+→ READY
+```
+
+---
+
+## 2. Добавлен runtime-анализ устойчивости сигнала
+
+Signal Confirmation Runtime теперь анализирует:
+
+- длительность жизни BUY/SELL
+- стабильность направления
+- количество повторов
+- consistency сигнала во времени
+
+---
+
+## 3. Добавлен signal lifetime tracking
+
+В `AutoTradeState` добавлены:
+
+```python
+signal_confirmation_seconds
+signal_confirmation_required_seconds
+signal_confirmation_missing_repeats
+signal_confirmation_progress
+signal_confirmation_reason
+```
+
+---
+
+## 4. BUY/SELL теперь проходят confirmation phase
+
+Ранее жизненный цикл был:
+
+```text
+HOLD
+→ BUY
+→ READY
+```
+
+Теперь:
+
+```text
+HOLD
+→ BUY
+→ CONFIRMING
+→ READY
+```
+
+или:
+
+```text
+HOLD
+→ SELL
+→ CONFIRMING
+→ READY
+```
+
+---
+
+## 5. Добавлен signal persistence filter
+
+Теперь сигнал обязан удерживаться минимум:
+
+```python
+_confirm_min_duration_seconds = 10
+```
+
+Даже если repeats уже подтверждены — READY не наступит, пока сигнал не проживёт достаточное время.
+
+---
+
+# Что изменилось в Telegram UI
+
+## Добавлена runtime-строка подтверждения сигнала
+
+Для BUY/SELL теперь отображается:
+
+```text
+⏳ Подтверждение · 1/2 · 4/10с
+```
+
+или:
+
+```text
+✅ Подтверждение · готово
+```
+
+## HOLD больше не показывает confirmation line
+
+HOLD отображается как раньше:
+
+```text
+Сигнал 🟡 HOLD · 54с
+```
+
+без confirmation runtime строки.
+
+---
+
+# Проверка
+
+После внедрения:
+
+```bash
+python -m compileall src
+```
+
+Runtime-проверка:
+
+- HOLD не показывает confirmation line
+- BUY показывает `⏳ Подтверждение`
+- SELL показывает `⏳ Подтверждение`
+- READY показывает `✅ Подтверждение · готово`
+- READY не появляется мгновенно
+- READY требует repeats
+- READY требует удержания сигнала
+- HOLD сбрасывает confirmation progress
+
+---
+
+# Результат
+
+Этап завершил переход от:
+
+```text
+repeat-based signal confirmation
+```
+
+к:
+
+```text
+stateful runtime confirmation engine
+```
+
+Теперь AutoTrade анализирует не только факт сигнала, но и устойчивость направления во времени.