07.4.4.1.9.2 Signal Confirmation Runtime

This commit is contained in:
2026-05-12 10:57:31 +03:00
parent 5325ea3855
commit fc50cadabf
8 changed files with 1122 additions and 410 deletions

View File

@@ -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"<b>🤖 Автоторговля · {status_line}</b>\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)}"
)

View File

@@ -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} <b>Актив</b> · {_asset_symbol(state.symbol)}\n"
f"{strategy_icon} <b>Стратегия</b> · {_required_value(_strategy_short(state.strategy))}\n"
f"{risk_icon} <b>Риск на сделку</b> · {_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} <b>SL</b> · {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"<b>Доступно</b> · $ {_format_money_compact(available)}\n",
"🧾 <b>Подготовка ордера</b>",
f"Доступно 💰 {_format_money_compact(available)}",
"",
"Подготовка ордера 🧾",
_order_header_line(state),
f"<b>Цена</b> · {_format_usd_or_dash(price)}",
f"Цена · {_format_plain_or_dash(price)}",
_estimated_size_text(state, price),
_max_reserved_line(state, price),
f"<b>Риск</b> · {_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"<b>Доступно</b> · $ {_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"<b>{_price_label_for_signal(state)}</b> · {_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"<b>Риск</b> · {_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"<b>Доступно</b> · $ {_format_money_compact(available)}",
f"<b>Зарезервировано</b> · $ {_format_money_compact(reserved)}",
f"<b>P&L</b> {_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} <b>{_asset_symbol(state.symbol)}</b> · "
f"{side_icon} {_asset_symbol(state.symbol)} · "
f"{_strategy_short(state.strategy)} · "
f"<b>{state.position_side}</b> {_leverage_text(state.leverage)}"
f"{state.position_side} {_leverage_text(state.leverage)}"
),
"",
f"<b>Количество</b> · {_format_crypto_size(size)} ⇢ $ {_format_money_compact(notional)}",
f"<b>Цена входа</b> · $ {_format_money(state.entry_price)}",
f"<b>Текущая цена</b> · {_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 "<b>Собственные средства</b> · —"
return "Маржа · —"
leverage = state.leverage or 1.0
if leverage <= 0:
return "<b>Собственные средства</b> · —"
return "Маржа · —"
position_size_usd = size * price
own_funds_usd = position_size_usd / leverage
return (
f"<b>Собственные средства</b> · $ {_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 "<b>Количество</b> · —"
return "Размер · —\nПозиция · —"
notional = size * price
return (
f"<b>Количество</b> · {_format_crypto_size(size)}\n"
f"<b>Размер позиции</b> · $ {_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"<b>SL</b> {sl}" if sl else "<b>SL</b> off",
f"<b>TP</b> {tp}" if tp else "<b>TP</b> off",
f"<b>ML</b> {ml}" if ml else "<b>ML</b> 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"<b>R:R</b> = 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"🟢 <b>{_asset_symbol(state.symbol)}</b> · "
f"{_strategy_short(state.strategy)} · "
f"<b>LONG</b> {_leverage_text(state.leverage)}"
f"LONG {_leverage_text(state.leverage)}"
)
if signal == "SELL":
return (
f"🔴 <b>{_asset_symbol(state.symbol)}</b> · "
f"{_strategy_short(state.strategy)} · "
f"<b>SHORT</b> {_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"
return "0"

View File

@@ -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:

View File

@@ -164,4 +164,19 @@ class AutoTradeState:
execution_quality_message: str | None = None
# признак деградации runtime market data
market_runtime_degraded: bool = False
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

View File

@@ -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
---

View File

@@ -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

View File

@@ -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

View File

@@ -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 анализирует не только факт сигнала, но и устойчивость направления во времени.