07.4.4.1.12 — Position Health & Runtime Risk Layer
This commit is contained in:
@@ -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:]
|
||||
Reference in New Issue
Block a user