From fc50cadabf915c74a6dab501b2d364694d76522b Mon Sep 17 00:00:00 2001 From: Sergey Date: Tue, 12 May 2026 10:57:31 +0300 Subject: [PATCH] 07.4.4.1.9.2 Signal Confirmation Runtime --- .../telegram/handlers/auto/_ui_old_working.py | 280 ----------- app/src/telegram/handlers/auto/ui.py | 303 +++++++----- app/src/trading/auto/service.py | 63 ++- app/src/trading/auto/state.py | 17 +- docs/roadmap/master-roadmap.md | 102 ++++ docs/roadmap/stage-07-auto-trading-roadmap.md | 103 ++++ ...4_4_1_9_1-market_semantic_runtime_layer.md | 452 ++++++++++++++++++ ...7_4_4_1_9_2-signal_confirmation_runtime.md | 212 ++++++++ 8 files changed, 1122 insertions(+), 410 deletions(-) delete mode 100644 app/src/telegram/handlers/auto/_ui_old_working.py create mode 100644 docs/stages/stage-07_4_4_1_9_1-market_semantic_runtime_layer.md create mode 100644 docs/stages/stage-07_4_4_1_9_2-signal_confirmation_runtime.md 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 анализирует не только факт сигнала, но и устойчивость направления во времени.