Stage 07.4.3 — trend strategy, signal aggregation and journal UX improvements
This commit is contained in:
@@ -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 '—'}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user