Stage 07.4.3 — trend strategy, signal aggregation and journal UX improvements

This commit is contained in:
2026-05-01 11:43:26 +03:00
parent 80f29443d4
commit ec8e53c416
8 changed files with 510 additions and 20 deletions

View File

@@ -76,7 +76,9 @@ def _build_auto_text() -> str:
f"Риск: {risk}\n"
f"PnL: {state.pnl_usd:.2f} USD\n"
f"Последний анализ: {state.last_check_at or ''}\n"
f"Сигнал: {_signal_label(state.last_signal)}"
f"Сигнал: {_signal_label(state.last_signal)} · {state.last_signal_repeat_count} подряд\n"
f"Уверенность: {state.last_signal_confidence:.2f}\n"
f"Причина: {state.last_signal_reason or ''}"
)

View File

@@ -21,6 +21,8 @@ LEVEL_ICONS = {
}
EVENT_TITLES = {
"auto_signal_generated": "Сигнал автоторговли",
"auto_signal_summary": "Итог серии сигналов",
"app_start": "Запуск приложения",
"system_open_alert": "Система загружена с предупреждениями",
"system_open_requested": "Открытие системы",
@@ -165,16 +167,40 @@ def render_clear_confirm(
return "\n".join(lines)
def _normalize_datetime(value: str) -> str:
def _parse_local_datetime(value: str) -> datetime | None:
try:
settings = load_settings()
dt = datetime.fromisoformat(value)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=ZoneInfo("UTC"))
dt = dt.astimezone(ZoneInfo(settings.tz))
return dt.strftime("%Y-%m-%d %H:%M:%S")
return dt.astimezone(ZoneInfo(settings.tz))
except Exception:
return value
return None
def _date_group_label(dt: datetime | None) -> str:
if dt is None:
return "Без даты"
settings = load_settings()
today = datetime.now(ZoneInfo(settings.tz)).date()
if dt.date() == today:
return "Сегодня"
if (today - dt.date()).days == 1:
return "Вчера"
return dt.strftime("%Y-%m-%d")
def _time_label(dt: datetime | None, raw_value: str) -> str:
if dt is None:
return raw_value
return dt.strftime("%H:%M:%S")
def _event_title(event_type: str) -> str:
@@ -189,29 +215,101 @@ def _humanize_message(message: str) -> str:
return message
def _payload(event: dict) -> dict:
payload = event.get("payload")
return payload if isinstance(payload, dict) else {}
def _render_auto_signal(event: dict, created_time: str) -> list[str]:
payload = _payload(event)
signal = str(payload.get("signal", "HOLD")).upper()
strategy = str(payload.get("strategy", "AUTO")).upper()
symbol = str(payload.get("symbol", ""))
reason = str(payload.get("reason", ""))
confidence = float(payload.get("confidence", 0.0) or 0.0)
repeat_count = int(payload.get("repeat_count", 1) or 1)
is_strong_signal = bool(payload.get("is_strong_signal", False))
is_aggregated = bool(payload.get("is_aggregated", False))
signal_icon = {
"BUY": "🟢",
"SELL": "🔴",
"HOLD": "🟡",
}.get(signal, "")
prefix = ""
if is_strong_signal:
prefix += "📈 "
if is_aggregated:
prefix += "🧠 "
lines = [
f"{prefix}{signal_icon} <b>AUTO · {signal}</b>",
f"{created_time} · {strategy} · {symbol}",
]
if is_aggregated:
lines.append(f"{repeat_count} {signal} подряд")
if confidence > 0:
lines.append(f"Уверенность: {confidence:.2f}")
if reason:
lines.append(f"Причина: {reason}")
return lines
def _render_default_event(event: dict, created_time: str) -> list[str]:
level = str(event.get("level", "INFO")).upper()
icon = LEVEL_ICONS.get(level, "")
title = _event_title(str(event.get("event_type", "")))
message = _humanize_message(str(event.get("message", "")))
lines = [
f"{icon} <b>{level}</b> · {title}",
f"{created_time}",
]
if message:
lines.append(message)
return lines
def render(events, page, total_pages):
lines = [
"<b>📒 Журнал</b>",
"",
"<b>МОНИТОРИНГ</b>",
"",
"<b>Последние события:</b>",
"",
]
if not events:
lines.append("Событий пока нет.")
return "\n".join(lines)
for event in events:
level = str(event.get("level", "INFO")).upper()
icon = LEVEL_ICONS.get(level, "")
title = _event_title(str(event.get("event_type", "")))
created_at = _normalize_datetime(str(event.get("created_at", "")))
message = _humanize_message(str(event.get("message", "")))
current_group = None
for event in events:
raw_created_at = str(event.get("created_at", ""))
dt = _parse_local_datetime(raw_created_at)
group_label = _date_group_label(dt)
created_time = _time_label(dt, raw_created_at)
if group_label != current_group:
current_group = group_label
lines.append(f"<b>{group_label}</b>")
lines.append("")
event_type = str(event.get("event_type", ""))
if event_type in {"auto_signal_generated", "auto_signal_summary"}:
lines.extend(_render_auto_signal(event, created_time))
else:
lines.extend(_render_default_event(event, created_time))
lines.append(f"{icon} [ <b>{level}</b> ] <b>{title}</b>")
lines.append(f"{created_at}")
lines.append("")
return "\n".join(lines).rstrip()