07.4.4.1.12 — Position Health & Runtime Risk Layer

This commit is contained in:
2026-05-21 19:32:55 +03:00
parent 06ea376cb5
commit f9a25e7671
10 changed files with 3068 additions and 67 deletions

View File

@@ -41,21 +41,21 @@ def auto_keyboard() -> InlineKeyboardMarkup:
status = (state.status or "").upper()
if status == "OFF":
builder.button(text="▶️ Start", callback_data="auto:start")
builder.button(text="👀 Watch", callback_data="auto:observe")
builder.button(text="▶️ Запустить", callback_data="auto:start")
builder.button(text="👀 Наблюдать", callback_data="auto:observe")
elif status == "RUNNING":
builder.button(text="👀 Watch", callback_data="auto:observe")
builder.button(text="🛑 Stop", callback_data="auto:stop")
builder.button(text="👀 Наблюдать", callback_data="auto:observe")
builder.button(text="🛑 Остановить", callback_data="auto:stop")
elif status == "OBSERVING":
builder.button(text="▶️ Start", callback_data="auto:start")
builder.button(text="🛑 Stop", callback_data="auto:stop")
builder.button(text="▶️ Запустить", callback_data="auto:start")
builder.button(text="🛑 Остановить", callback_data="auto:stop")
else:
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="auto:start")
builder.button(text="👀 Наблюдать", callback_data="auto:observe")
builder.button(text="🛑 Остановить", callback_data="auto:stop")
builder.button(text="🛠️ Настройки", callback_data="settings:auto")
builder.button(text="🧯 Защита", callback_data="auto:risk")
@@ -192,7 +192,7 @@ def _settings_risk_percent_line(state) -> str:
ml = (
f"-{_format_money_compact(abs(state.max_loss_usd))}"
if state.max_loss_usd is not None
else "off"
else "выкл"
)
return f"SL {sl} · TP {tp} · ML {ml}"
@@ -205,15 +205,10 @@ def _build_waiting_text(state) -> str:
estimated_size = _estimated_size(state, price)
cycle_trades = int(getattr(state, "cycle_closed_trades", 0) or 0)
cycle_pnl = float(
getattr(state, "cycle_realized_pnl_usd", 0.0) or 0.0
)
cycle_pnl = float(getattr(state, "cycle_realized_pnl_usd", 0.0) or 0.0)
parts = [
(
f"{_status_text(state)}"
),
f"{_status_text(state)}",
_account_mode_line(),
"",
f"Доступно 💰 {_format_money_compact(available)}",
@@ -222,10 +217,7 @@ def _build_waiting_text(state) -> str:
if cycle_trades > 0:
parts.extend([
"",
(
f"🔄 {_cycle_number_text(state)} · "
f"{cycle_trades} {_trade_word(cycle_trades)}"
),
f"🔄 {_cycle_number_text(state)} · {cycle_trades} {_trade_word(cycle_trades)}",
_format_pnl_line(cycle_pnl),
])
@@ -236,37 +228,50 @@ def _build_waiting_text(state) -> str:
parts.extend([
"",
_signal_line(state),
"",
])
execution_runtime_line = _execution_runtime_line(state)
if execution_runtime_line:
parts.extend([
"",
execution_runtime_line,
])
block_title = (
"Прогноз сделки 🔮"
if state.status == "OBSERVING"
else "Подготовка ордера 🧾"
)
parts.extend([
order_lines = [
"",
block_title,
_order_header_line(state),
f"Цена · {_format_plain_or_dash(price)}",
_estimated_size_text(state, price),
_max_reserved_line(state, price),
_effective_risk_line(state),
]
execution_confidence_line = _execution_confidence_line(state)
if execution_confidence_line:
order_lines.append(execution_confidence_line)
order_lines.append(
_risk_summary_line(
state,
estimated_size,
entry_price_override=price,
),
])
)
)
parts.extend(order_lines)
adjustment_visible = _adaptive_adjustment_visible(state)
if adjustment_visible:
reason = getattr(state, "adaptive_size_reason", "") or ""
multiplier = float(
getattr(state, "adaptive_size_multiplier", 1.0) or 1.0
)
multiplier = float(getattr(state, "adaptive_size_multiplier", 1.0) or 1.0)
parts.extend([
"",
@@ -280,6 +285,50 @@ def _build_waiting_text(state) -> str:
return "\n".join(parts)
def _execution_runtime_line(state) -> str:
quality = str(
getattr(state, "execution_quality", "") or ""
).upper()
reason = str(
getattr(state, "execution_quality_reason", "") or ""
).upper()
freshness = _execution_freshness_text(state)
if quality == "GOOD":
return ""
if quality == "WARNING":
if reason == "WIDE_SPREAD":
return f"Исполнение ⚠️ Повышенный spread · {freshness}"
if reason == "AGING_SNAPSHOT":
return f"Исполнение ⚠️ Snapshot стареет · {freshness}"
if reason == "SNAPSHOT_UNAVAILABLE":
return f"Исполнение ⚠️ Нет стакана · {freshness}"
return f"Исполнение ⚠️ Предупреждение · {freshness}"
if quality == "BLOCKED":
if reason == "MARKET_CLOSED":
return "Исполнение ⏸️ Рынок закрыт"
if reason == "STALE_SNAPSHOT":
return f"Исполнение 🔴 Snapshot устарел · {freshness}"
if reason == "HIGH_SPREAD":
return f"Исполнение 🔴 Высокий spread · {freshness}"
if reason == "SNAPSHOT_ERROR":
return "Исполнение 🔴 Нет данных рынка"
return f"Исполнение 🔴 Заблокировано · {freshness}"
return ""
def _build_active_position_text(state) -> str:
current_price = _current_price(state.symbol)
price_for_calc = current_price or state.entry_price or 0.0
@@ -356,9 +405,14 @@ def _build_active_position_text(state) -> str:
),
f"Объём · {_format_usd_compact(notional)}",
_format_pnl_line(pnl),
_risk_summary_line(state, size),
])
execution_runtime_line = _execution_runtime_line(state)
if execution_runtime_line:
parts.append(execution_runtime_line)
parts.append(_risk_summary_line(state, size))
if pnl < 0:
reason_block = _position_warning_reason(state)
@@ -528,6 +582,40 @@ def _effective_risk_line(state) -> str:
return f"Риск · {_format_usd_compact(_target_risk_usd(state))}"
def _execution_confidence_line(state) -> str:
score = safe_float(
getattr(state, "execution_confidence_score", None)
)
if score is None:
return ""
level = str(
getattr(state, "execution_confidence_level", "") or ""
).upper()
if level == "HIGH":
icon = "🟢"
level_text = "Высокая"
elif level == "NORMAL":
icon = "🟡"
level_text = "Нормальная"
elif level == "LOW":
icon = "🔴"
level_text = "Низкая"
else:
icon = ""
level_text = "Неизвестно"
return (
f"Уверенность исполнения "
f"{icon} {level_text} · {score:.2f}"
)
def _estimated_size(state, price: float | None) -> float | None:
if (
price is None
@@ -642,13 +730,13 @@ def _risk_summary_line(
key, value = enabled[0]
if key == "SL":
return f"Stop Loss -{_format_usd_compact(value)}"
return f"Stop Loss {_format_usd_compact(value)}"
if key == "TP":
return f"Take Profit +{_format_usd_compact(value)}"
if key == "ML":
return f"Max Loss -{_format_usd_compact(value)}"
return f"Макс. убыток {_format_usd_compact(value)}"
items: list[str] = []
@@ -662,7 +750,7 @@ def _risk_summary_line(
items.append(f"ML -{_format_usd_compact(ml_value)}")
if not items:
return "SL off · TP off · ML off"
return "SL выкл · TP выкл · ML выкл"
return " · ".join(items)
@@ -739,7 +827,7 @@ def _signal_text(signal: str) -> str:
mapping = {
"BUY": "Long",
"SELL": "Short",
"HOLD": "Hold",
"HOLD": "Ожидание",
}
return mapping.get(signal.upper(), signal.title())
@@ -757,7 +845,7 @@ def _signal_line(state) -> str:
state.decision_status == "READY"
or getattr(state, "is_signal_ready", False)
):
return f"{signal_text} · READY"
return f"{signal_text} · готов"
duration = _signal_duration_text(state)
@@ -845,6 +933,21 @@ def _signal_duration_text(state) -> str:
return f"{seconds}с"
def _execution_freshness_text(state) -> str:
freshness = str(
getattr(state, "execution_price_freshness", "") or ""
).upper()
mapping = {
"FRESH": "данные свежие",
"AGING": "данные стареют",
"STALE": "данные устарели",
"UNKNOWN": "нет данных",
}
return mapping.get(freshness, "нет данных")
def _status_text(state) -> str:
runtime = _cycle_runtime_text(state)
@@ -1012,7 +1115,7 @@ def _format_crypto_size(value: float | int | None) -> str:
def _format_percent(value: float | int | None) -> str:
if value is None:
return "off"
return "выкл"
number = float(value)
@@ -1085,24 +1188,21 @@ def _adaptive_adjustment_visible(state) -> bool:
return _adaptive_size_active(state) and _show_adaptive_banner(state)
def _short_adaptive_reason(reason: str, multiplier: float | None = None) -> str:
if multiplier is not None:
try:
value = float(multiplier)
except (TypeError, ValueError):
value = 1.0
def _short_adaptive_reason(
reason: str,
multiplier: float | None = None,
) -> str:
value = safe_float(multiplier)
if value is not None:
if value < 0.15:
return "Вход почти заблокирован"
if value < 0.40:
return "Размер сильно уменьшен"
return "Вход заблокирован"
if value < 0.75:
return "Размер уменьшен"
return "Риск снижен"
if value < 1.0:
return "Небольшая коррекция"
return "Размер уменьшен"
if value > 1.05:
return "Размер увеличен"
@@ -1122,6 +1222,6 @@ def _short_adaptive_reason(reason: str, multiplier: float | None = None) -> str:
return "Низкая уверенность"
if not reason:
return "Размер изменён"
return "Размер скорректирован"
return reason[:1].upper() + reason[1:]