07.4.4.1.13 — AutoTrade Runtime Journal, Execution Refactor & Trade Analytics

This commit is contained in:
2026-05-28 10:30:54 +03:00
parent f9a25e7671
commit d9e6392e28
75 changed files with 9934 additions and 10508 deletions

View File

@@ -16,18 +16,19 @@ EVENT_TITLES = {
# Настройки
"auto_settings_updated": "Автоторговля",
"auto_status_changed": "Автоторговля",
"risk_settings_updated": "Защита",
# Аналитика рынка
"market_state_changed": "Рынок",
"market_volatility_changed": "Рынок",
# Аналитика автоторговли
"market_state_changed": "Автоторговля",
"market_volatility_changed": "Автоторговля",
# Мониторинг рынка
"market_monitor_started": "Рынок",
"market_monitor_stopped": "Рынок",
"market_stream_connected": "Рынок",
"market_stream_disconnected": "Рынок",
"market_symbol_changed": "Рынок",
# Рыночные данные runtime
"market_monitor_started": "Автоторговля",
"market_monitor_stopped": "Автоторговля",
"market_stream_connected": "Автоторговля",
"market_stream_disconnected": "Автоторговля",
"market_symbol_changed": "Автоторговля",
# Мониторинг позиций
"entry_blocked": "Вход в позицию",
@@ -61,10 +62,6 @@ EVENT_TITLES = {
"system_retry": "Система",
"system_about_opened": "Система",
"market_open_requested": "Рынок",
"market_open_success": "Рынок",
"market_open_error": "Рынок",
"portfolio_open_requested": "Портфель",
"portfolio_open_success": "Портфель",
"portfolio_open_error": "Портфель",
@@ -72,10 +69,21 @@ EVENT_TITLES = {
"exchange_request_error": "Биржа",
"exchange_auth_error": "Аккаунт",
"exchange_auth_restored": "Аккаунт",
"exchange_time_sync_error": "Время биржи",
"exchange_time_sync_restored": "Время биржи",
"balance_summary_loaded": "Баланс",
"balance_summary_error": "Баланс",
"runtime_expired": "Runtime",
"market_status_unavailable": "Автоторговля",
"market_status_restored": "Автоторговля",
"market_closed": "Автоторговля",
"market_rest_fallback_available": "Автоторговля",
"market_rest_fallback_unavailable": "Автоторговля",
}

View File

@@ -9,7 +9,9 @@ from zoneinfo import ZoneInfo
from src.core.config import load_settings
from src.core.constants import APP_NAME, APP_VERSION
from src.integrations.exchange.runtime_ui import build_runtime_exchange_alerts
from src.integrations.exchange.service import ExchangeService
from src.integrations.exchange.status import build_exchange_error_status
from src.storage.session import check_database_health
from src.trading.journal.service import JournalService
@@ -32,6 +34,96 @@ class SystemSnapshot:
components: list[ComponentStatus]
def _build_exchange_alert_components(
*,
default_symbol: str,
) -> list[ComponentStatus]:
exchange_service = ExchangeService()
try:
runtime_status = exchange_service.get_symbol_runtime_status(default_symbol)
except Exception as exc:
runtime_status = build_exchange_error_status(exc)
if not runtime_status.is_available:
return [
ComponentStatus(
name="Биржа",
state=runtime_status.ui_line,
details=runtime_status.message,
)
]
alerts = build_runtime_exchange_alerts(symbol=default_symbol)
exchange_unavailable_alert = next(
(
alert
for alert in alerts
if str(alert.get("code") or "") == "EXCHANGE_UNAVAILABLE"
),
None,
)
if exchange_unavailable_alert is not None:
return [
ComponentStatus(
name="Биржа",
state=str(
exchange_unavailable_alert.get("ui_line")
or exchange_unavailable_alert.get("title")
or "⛔️ Биржа недоступна"
),
details=str(exchange_unavailable_alert.get("details") or ""),
)
]
components: list[ComponentStatus] = [
ComponentStatus(
name="Биржа",
state="🟢",
details=runtime_status.message,
)
]
has_account_alert = False
for alert in alerts:
code = str(alert.get("code") or "")
state = str(
alert.get("ui_line")
or alert.get("title")
or "⛔️ Ошибка биржи"
)
if code == "AUTH_ERROR":
has_account_alert = True
name = "Аккаунт"
elif code == "TIME_ERROR":
name = "Время биржи"
else:
name = "Биржа"
components.append(
ComponentStatus(
name=name,
state=state,
details=str(alert.get("details") or ""),
)
)
if not has_account_alert:
components.append(
ComponentStatus(
name="Аккаунт",
state="🟢",
)
)
return components
# извлечь короткую версию PostgreSQL из строки health-check
def _extract_postgres_version(raw: str) -> str:
if not raw:
return "PostgreSQL"
@@ -43,99 +135,72 @@ def _extract_postgres_version(raw: str) -> str:
return "PostgreSQL"
def _build_exchange_status(
exchange_service: ExchangeService,
default_symbol: str,
) -> ComponentStatus:
try:
symbol_validation = exchange_service.validate_symbol(default_symbol)
except Exception as exc:
return ComponentStatus(
name="Биржа",
state="🔴",
details=_humanize_error_message(str(exc)),
)
exchange_health = exchange_service.get_health()
if exchange_health.ok and symbol_validation.is_valid:
return ComponentStatus(name="Биржа", state="🟢")
if not exchange_health.ok:
return ComponentStatus(
name="Биржа",
state="🔴",
details=_humanize_error_message(exchange_health.message or ""),
)
return ComponentStatus(
name="Биржа",
state="🔴",
details=symbol_validation.message or "Инструмент не прошёл проверку.",
)
def _build_account_status(exchange_service: ExchangeService) -> ComponentStatus:
private_auth_health = exchange_service.get_private_auth_health()
if private_auth_health.ok:
return ComponentStatus(name="Аккаунт", state="🟢")
return ComponentStatus(
name="Аккаунт",
state="🔴",
details=_humanize_error_message(private_auth_health.message or ""),
)
# проверить подключение к БД и вернуть компонент + подпись версии
def _build_database_status() -> tuple[ComponentStatus, str]:
db_ok, db_message = check_database_health()
db_label = _extract_postgres_version(db_message)
if db_ok:
return ComponentStatus(name="База данных", state="🟢"), db_label
return (
ComponentStatus(
name="База данных",
state="🟢",
),
db_label,
)
return (
ComponentStatus(
name="База данных",
state="🔴",
details=db_message or "Ошибка подключения к БД.",
state="🔴 База данных недоступна",
),
db_label,
)
# проверить доступность журнала событий
def _build_journal_status() -> ComponentStatus:
ok, message = JournalService().get_journal_health()
ok, _ = JournalService().get_journal_health()
if ok:
return ComponentStatus(name="Журнал", state="🟢")
return ComponentStatus(
name="Журнал",
state="🟢",
)
return ComponentStatus(name="Журнал", state="🔴", details=message)
return ComponentStatus(
name="Журнал",
state="🔴 Журнал недоступен",
)
# определить runtime-режим по base_url биржи
def get_runtime_mode_key() -> str:
settings = load_settings()
return "demo" if "demo" in settings.exchange_base_url.lower() else "live"
# вернуть человекочитаемую подпись runtime-режима
def get_runtime_mode_label() -> str:
return "DEMO аккаунт" if get_runtime_mode_key() == "demo" else "LIVE аккаунт"
# собрать полный snapshot системного экрана
def get_system_snapshot() -> SystemSnapshot:
settings = load_settings()
exchange_service = ExchangeService()
database_status, db_label = _build_database_status()
exchange_status = _build_exchange_status(exchange_service, settings.default_symbol)
account_status = _build_account_status(exchange_service)
journal_status = _build_journal_status()
exchange_components = _build_exchange_alert_components(
default_symbol=settings.default_symbol,
)
components = [
ComponentStatus(name="Приложение", state="🟢"),
database_status,
ComponentStatus(name="Telegram", state="🟢"),
exchange_status,
account_status,
*exchange_components,
journal_status,
]
@@ -150,19 +215,20 @@ def get_system_snapshot() -> SystemSnapshot:
)
# определить, есть ли системные предупреждения
def has_system_alerts(snapshot: SystemSnapshot) -> bool:
return any(component.state != "🟢" for component in snapshot.components)
# отрендерить одну строку компонента системы
def _render_component(component: ComponentStatus) -> str:
line = f"{component.state} {component.name}"
if component.state == "🟢":
return f"{component.state} {component.name}"
if component.state == "🟢" or not component.details:
return line
return f"{line}\n{component.details}"
return component.state
# получить текущее локальное время для подписи обновления
def _now_hhmmss() -> str:
settings = load_settings()
tz_name = settings.tz or "UTC"
@@ -175,50 +241,22 @@ def _now_hhmmss() -> str:
return local_dt.strftime("%H:%M:%S")
# собрать текст экрана "Система"
def build_system_text(*, include_updated_at: bool = False) -> str:
snapshot = get_system_snapshot()
components_block = "\n".join(
_render_component(component) for component in snapshot.components
_render_component(component)
for component in snapshot.components
)
text = (
"<b>🖥️ Система</b>\n"
f"🔸 <b>{snapshot.mode_label}</b>\n\n"
# f"⏱️ {snapshot.timezone_name}\n\n"
f"{components_block}"
)
if include_updated_at:
text += f"\n\n<i>Обновлено: {_now_hhmmss()}</i>"
return text
def _humanize_error_message(text: str) -> str:
t = text.lower()
# сеть
if "nodename nor servname" in t or "name or service not known" in t:
return "Нет связи с биржей"
if "timeout" in t or "timed out" in t:
return "Биржа не отвечает (таймаут)"
if "network error" in t or "connection error" in t:
return "Ошибка сети при обращении к бирже"
# API / доступ
if "private api error" in t:
return "Ошибка доступа к аккаунту"
if "invalid api key" in t or "api key" in t:
return "Неверный API ключ"
if "forbidden" in t or "unauthorized" in t:
return "Нет доступа к аккаунту"
# время
if "-1021" in t or "doesn't match server time" in t:
return "Ошибка времени (рассинхронизация)"
return "Не удалось получить данные с биржи"
return text