"""交易监管:事件分类,频率规则,会话消息与企业微信推送."""
from __future__ import annotations

import json
import os
import threading
import uuid
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Callable, Optional

from lib.hub.hub_trades_lib import current_trading_day, parse_dt_for_trading_day

HUB_DIR = Path(__file__).resolve().parent
STATE_PATH = HUB_DIR / "hub_supervisor_state.json"

PROGRAM_RESULTS = frozenset({"止盈", "止损", "保本止盈", "移动止盈"})
MANUAL_CLOSE_RESULTS = frozenset({"手动平仓"})
HUB_CLOSE_RESULTS = frozenset({"强制清仓"})
WEAK_RESULTS = frozenset({"外部平仓", "时间平仓"})

EVENT_OPEN = "open"
EVENT_MANUAL_CLOSE = "manual_close"
EVENT_HUB_CLOSE = "hub_close"
EVENT_PROGRAM_TP = "program_tp"
EVENT_PROGRAM_SL = "program_sl"
EVENT_EXTERNAL = "external"
EVENT_FREQ_WARN = "freq_warn"

DEFAULT_SUPERVISOR = {
    "enabled": True,
    "wechat_webhook": "",
    "wechat_link_base": "http://127.0.0.1:5100/ai?mode=supervisor",
    "wechat_prefix": "【交易监管】",
    "wechat_on_program_tp_sl": True,
    "manual_close_daily_warn": 2,
    "interval_warn_minutes": 15,
    "freq_30m_count": 2,
    "reopen_after_close_minutes": 30,
}


def _now_str() -> str:
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")


def _atomic_write(path: Path, data: dict) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp = path.with_suffix(path.suffix + ".tmp")
    tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
    os.replace(tmp, path)


def _load_json(path: Path, default: dict) -> dict:
    if not path.is_file():
        return dict(default)
    try:
        loaded = json.loads(path.read_text(encoding="utf-8"))
        if isinstance(loaded, dict):
            return loaded
    except Exception:
        pass
    return dict(default)


def normalize_supervisor_settings(raw: dict | None) -> dict:
    out = dict(DEFAULT_SUPERVISOR)
    env_webhook = (os.getenv("SUPERVISOR_WECHAT_WEBHOOK") or "").strip()
    env_link = (os.getenv("SUPERVISOR_WECHAT_LINK") or "").strip()
    if env_webhook:
        out["wechat_webhook"] = env_webhook
    if env_link:
        out["wechat_link_base"] = env_link
    if not isinstance(raw, dict):
        return out
    for key in DEFAULT_SUPERVISOR:
        if key not in raw:
            continue
        val = raw.get(key)
        if key == "enabled" or key == "wechat_on_program_tp_sl":
            out[key] = bool(val)
        elif key in ("manual_close_daily_warn", "freq_30m_count"):
            try:
                out[key] = max(1, int(val))
            except (TypeError, ValueError):
                pass
        elif key in ("interval_warn_minutes", "reopen_after_close_minutes"):
            try:
                out[key] = max(1, int(val))
            except (TypeError, ValueError):
                pass
        elif isinstance(val, str):
            out[key] = val.strip()
    return out


def load_supervisor_state() -> dict:
    data = _load_json(STATE_PATH, {"version": 1, "trading_day": "", "processed": [], "positions": {}, "stats": {}})
    data.setdefault("version", 1)
    data.setdefault("processed", [])
    data.setdefault("positions", {})
    data.setdefault("stats", {})
    return data


def save_supervisor_state(data: dict) -> None:
    processed = list(data.get("processed") or [])
    if len(processed) > 500:
        processed = processed[-500:]
    data["processed"] = processed
    _atomic_write(STATE_PATH, data)


def _trade_event_id(trade: dict) -> str:
    return "|".join(
        [
            str(trade.get("account_name") or trade.get("account_key") or ""),
            str(trade.get("symbol") or ""),
            str(trade.get("closed_at") or ""),
            str(trade.get("result") or ""),
            str(trade.get("pnl_amount") or ""),
        ]
    )


def classify_close_result(result: str) -> str:
    r = (result or "").strip()
    if r in PROGRAM_RESULTS:
        if r == "止损":
            return EVENT_PROGRAM_SL
        return EVENT_PROGRAM_TP
    if r in MANUAL_CLOSE_RESULTS:
        return EVENT_MANUAL_CLOSE
    if r in HUB_CLOSE_RESULTS:
        return EVENT_HUB_CLOSE
    if r in WEAK_RESULTS:
        return EVENT_EXTERNAL
    return EVENT_EXTERNAL


def is_supervised_event(event_type: str) -> bool:
    return event_type in (EVENT_OPEN, EVENT_MANUAL_CLOSE, EVENT_HUB_CLOSE)


def is_program_event(event_type: str) -> bool:
    return event_type in (EVENT_PROGRAM_TP, EVENT_PROGRAM_SL)


def _normalize_position_symbol(sym: str) -> str:
    """统一合约名,避免 ZEC/USDT 与 ZEC/USDT:USDT 被当成两笔持仓."""
    s = (sym or "").strip().upper()
    if not s:
        return ""
    if s.endswith(":USDT") and "/" in s:
        return s.rsplit(":", 1)[0]
    return s


def _position_key(exchange_id: str, symbol: str, side: str) -> str:
    sym = _normalize_position_symbol(symbol)
    sd = (side or "long").strip().lower() or "long"
    return f"{exchange_id}|{sym}|{sd}"


def _position_contracts(pos: dict) -> float:
    for key in ("contracts", "contracts_signed", "size"):
        try:
            v = pos.get(key)
            if v is not None and v != "":
                return abs(float(v))
        except (TypeError, ValueError):
            continue
    return 0.0


def collect_position_keys(board_payload: dict | None) -> dict[str, dict]:
    out: dict[str, dict] = {}
    rows = (board_payload or {}).get("rows") or []
    for row in rows:
        if not isinstance(row, dict):
            continue
        ex_id = str(row.get("id") or row.get("key") or "")
        ex_name = str(row.get("name") or row.get("key") or ex_id)
        ag = row.get("agent") or {}
        for p in ag.get("positions") or []:
            if not isinstance(p, dict):
                continue
            if _position_contracts(p) < 1e-12:
                continue
            sym = str(p.get("symbol") or "")
            side = str(p.get("side") or "").lower() or "long"
            key = _position_key(ex_id, sym, side)
            out[key] = {
                "exchange_id": ex_id,
                "exchange_name": ex_name,
                "symbol": sym,
                "side": side,
                "contracts": _position_contracts(p),
            }
    return out


def _board_agent_snapshot_ready(board_payload: dict | None) -> bool:
    """监控板各启用账户 agent 快照已就绪(避免空板先入库导致后续持仓误判为新开)."""
    if not isinstance(board_payload, dict) or board_payload.get("ok") is False:
        return False
    rows = board_payload.get("rows") or []
    if not rows:
        return False
    seen = 0
    for row in rows:
        if not isinstance(row, dict):
            continue
        if row.get("enabled") is False:
            continue
        ag = row.get("agent")
        if not isinstance(ag, dict):
            return False
        seen += 1
    return seen > 0


def _entry_contracts(entry: dict | None) -> float:
    if not isinstance(entry, dict):
        return 0.0
    try:
        return float(entry.get("contracts") or 0)
    except (TypeError, ValueError):
        return 0.0


def detect_new_opens(
    prev_positions: dict[str, dict],
    curr_positions: dict[str, dict],
) -> list[dict]:
    """仅当某合约从空仓变为有仓时视为新开(已有持仓不加仓不算)."""
    events = []
    for key, info in curr_positions.items():
        curr_c = _entry_contracts(info)
        if curr_c < 1e-12:
            continue
        prev_c = _entry_contracts(prev_positions.get(key))
        if prev_c >= 1e-12:
            continue
        events.append({"event_type": EVENT_OPEN, "event_id": f"open:{key}:{_now_str()[:16]}", **info})
    return events


def detect_new_closes(
    prev_processed: set[str],
    closed_trades: list[dict],
) -> list[dict]:
    events = []
    for trade in closed_trades or []:
        if not isinstance(trade, dict):
            continue
        eid = _trade_event_id(trade)
        if eid in prev_processed:
            continue
        event_type = classify_close_result(str(trade.get("result") or ""))
        events.append(
            {
                "event_type": event_type,
                "event_id": f"close:{eid}",
                "account_name": trade.get("account_name"),
                "symbol": trade.get("symbol"),
                "direction": trade.get("direction"),
                "result": trade.get("result"),
                "pnl_amount": trade.get("pnl_amount"),
                "closed_at": trade.get("closed_at"),
            }
        )
    return events


def _parse_event_dt(raw: Any) -> Optional[datetime]:
    return parse_dt_for_trading_day(raw)


def _supervised_close_times(stats: dict, trading_day: str) -> list[datetime]:
    rows = (stats.get(trading_day) or {}).get("supervised_closes") or []
    out = []
    for item in rows:
        if isinstance(item, dict):
            dt = _parse_event_dt(item.get("closed_at") or item.get("at"))
        else:
            dt = _parse_event_dt(item)
        if dt:
            out.append(dt)
    out.sort()
    return out


def _record_supervised_event(stats: dict, trading_day: str, event: dict) -> None:
    day_stats = stats.setdefault(trading_day, {})
    et = str(event.get("event_type") or "")
    if et == EVENT_OPEN:
        opens = list(day_stats.get("supervised_opens") or [])
        opens.append({"at": _now_str(), "symbol": event.get("symbol")})
        day_stats["supervised_opens"] = opens[-50:]
        return
    if et not in (EVENT_MANUAL_CLOSE, EVENT_HUB_CLOSE):
        return
    closes = list(day_stats.get("supervised_closes") or [])
    closes.append(
        {
            "at": _now_str(),
            "closed_at": event.get("closed_at"),
            "event_type": et,
            "pnl_amount": event.get("pnl_amount"),
        }
    )
    day_stats["supervised_closes"] = closes[-50:]


def evaluate_frequency_warnings(
    *,
    trading_day: str,
    event: dict,
    stats: dict,
    settings: dict,
) -> list[dict]:
    if not is_supervised_event(str(event.get("event_type") or "")):
        return []
    warnings: list[dict] = []
    day_stats = stats.setdefault(trading_day, {})
    closes = _supervised_close_times(stats, trading_day)
    now = datetime.now()
    if event.get("event_type") in (EVENT_MANUAL_CLOSE, EVENT_HUB_CLOSE):
        evt_dt = _parse_event_dt(event.get("closed_at")) or now
        closes = closes + [evt_dt]
        closes.sort()
    open_count = len(day_stats.get("supervised_opens") or [])
    close_count = len(day_stats.get("supervised_closes") or [])
    if event.get("event_type") == EVENT_OPEN:
        open_count += 1
    elif event.get("event_type") in (EVENT_MANUAL_CLOSE, EVENT_HUB_CLOSE):
        close_count += 1

    interval_min = int(settings.get("interval_warn_minutes") or 15)
    daily_warn = int(settings.get("manual_close_daily_warn") or 2)
    freq_30m = int(settings.get("freq_30m_count") or 2)
    reopen_min = int(settings.get("reopen_after_close_minutes") or 30)

    if event.get("event_type") in (EVENT_MANUAL_CLOSE, EVENT_HUB_CLOSE) and len(closes) >= 2:
        prev = closes[-2]
        cur = closes[-1]
        gap = (cur - prev).total_seconds() / 60.0
        if gap < interval_min:
            warnings.append(
                {
                    "rule": "INTERVAL_SHORT",
                    "message": f"两笔手动/中控平间隔仅 {int(gap)} 分钟(阈值 {interval_min} 分钟)",
                }
            )

    recent_closes = [t for t in closes if (now - t).total_seconds() <= 30 * 60]
    if event.get("event_type") in (EVENT_MANUAL_CLOSE, EVENT_HUB_CLOSE) and len(recent_closes) >= freq_30m:
        warnings.append(
            {
                "rule": "FREQ_30M",
                "message": f"30 分钟内手动/中控平已达 {len(recent_closes)} 笔(阈值 {freq_30m} 笔)",
            }
        )

    supervised_total = open_count + close_count
    if supervised_total >= daily_warn and event.get("event_type") in (
        EVENT_MANUAL_CLOSE,
        EVENT_HUB_CLOSE,
        EVENT_OPEN,
    ):
        if close_count >= daily_warn:
            warnings.append(
                {
                    "rule": "DAILY_COUNT",
                    "message": f"今日手动/中控平 {close_count} 笔(阈值 {daily_warn} 笔),注意过度交易",
                }
            )

    if event.get("event_type") == EVENT_OPEN and closes:
        last_close = closes[-1]
        gap_open = (now - last_close).total_seconds() / 60.0
        if gap_open < reopen_min:
            warnings.append(
                {
                    "rule": "REOPEN_FAST",
                    "message": f"距上一笔手动/中控平仅 {int(gap_open)} 分钟又新开仓(阈值 {reopen_min} 分钟)",
                }
            )

    loss_streak = 0
    for item in reversed((stats.get(trading_day) or {}).get("supervised_closes") or []):
        try:
            pnl = float((item or {}).get("pnl_amount") or 0)
        except (TypeError, ValueError):
            pnl = 0.0
        if pnl < 0:
            loss_streak += 1
        else:
            break
    if event.get("event_type") in (EVENT_MANUAL_CLOSE, EVENT_HUB_CLOSE):
        try:
            pnl = float(event.get("pnl_amount") or 0)
        except (TypeError, ValueError):
            pnl = 0.0
        if pnl < 0:
            loss_streak += 1
        else:
            loss_streak = 0
    if loss_streak >= 2 and event.get("event_type") in (EVENT_MANUAL_CLOSE, EVENT_HUB_CLOSE):
        warnings.append(
            {
                "rule": "LOSS_STREAK",
                "message": f"连续 {loss_streak} 笔手动/中控亏损,先停一停",
            }
        )

    deduped = []
    seen = set()
    for w in warnings:
        key = w.get("rule")
        if key in seen:
            continue
        seen.add(key)
        deduped.append(w)
    return deduped


def event_tag(event_type: str) -> str:
    return {
        EVENT_OPEN: "监管·开仓",
        EVENT_MANUAL_CLOSE: "监管·手动平",
        EVENT_HUB_CLOSE: "监管·中控平",
        EVENT_PROGRAM_TP: "监管·程序止盈",
        EVENT_PROGRAM_SL: "监管·程序止损",
        EVENT_EXTERNAL: "监管·外部平",
        EVENT_FREQ_WARN: "监管·频率",
    }.get(event_type, "监管")


def _fmt_pnl_u(pnl: Any) -> str:
    try:
        v = float(pnl)
        sign = "+" if v > 0 else ""
        return f"{sign}{v:.4f}".rstrip("0").rstrip(".") + "U"
    except (TypeError, ValueError):
        return ""


def build_supervisor_fallback_reply(event: dict, warnings: list[dict] | None = None) -> str:
    """AI 不可用或返回空时的短评语(不展示错误文案)."""
    et = str(event.get("event_type") or "")
    sym = str(event.get("symbol") or "—")
    ex = str(event.get("exchange_name") or event.get("account_name") or "").strip()
    pnl_txt = _fmt_pnl_u(event.get("pnl_amount"))
    warn = (warnings or [])[:1]
    warn_txt = str(warn[0].get("message") or "").strip() if warn else ""

    if et == EVENT_PROGRAM_SL:
        base = f"{sym} 程序止损"
        if pnl_txt:
            base += f"({pnl_txt})"
        base += ",按计划出场是纪律.先歇一会儿,别急着马上再开."
    elif et == EVENT_PROGRAM_TP:
        base = f"{sym} 程序止盈"
        if pnl_txt:
            base += f"({pnl_txt})"
        base += ",执行不错.保持节奏,别立刻反手再开一单."
    elif et == EVENT_OPEN:
        who = f"{ex} " if ex else ""
        base = f"看到 {who}新开 {sym}.动手前确认是不是计划内,别因为上一笔情绪再开."
    elif et == EVENT_HUB_CLOSE:
        base = f"中控平了 {sym}"
        if pnl_txt:
            base += f"({pnl_txt})"
        base += "."
        base += f" {warn_txt}" if warn_txt else " 停一停,别连着手痒."
    elif et == EVENT_MANUAL_CLOSE:
        base = f"手动平了 {sym}"
        if pnl_txt:
            base += f"({pnl_txt})"
        base += "."
        base += f" {warn_txt}" if warn_txt else " 想好再开下一单."
    elif et == EVENT_FREQ_WARN:
        base = warn_txt or "今日操作偏频繁,先休息一会儿."
    else:
        base = "收到.确认是否按计划执行,别连续加码."
    return base.strip()[:320]


def build_system_message(event: dict, *, trading_day: str, warnings: list[dict] | None = None) -> str:
    tag = event_tag(str(event.get("event_type") or ""))
    ex = event.get("exchange_name") or event.get("account_name") or "—"
    sym = event.get("symbol") or "—"
    lines = [f"[{tag}] {ex} · {sym}"]
    et = event.get("event_type")
    if et == EVENT_OPEN:
        side = event.get("side") or event.get("direction") or ""
        if side:
            lines.append(f"方向:{side}")
    elif et in (EVENT_MANUAL_CLOSE, EVENT_HUB_CLOSE, EVENT_PROGRAM_TP, EVENT_PROGRAM_SL, EVENT_EXTERNAL):
        res = event.get("result") or ""
        pnl = event.get("pnl_amount")
        if pnl is not None:
            lines.append(f"结果 {res} · 盈亏 {pnl}U")
        else:
            lines.append(f"结果 {res}")
        if event.get("closed_at"):
            lines.append(f"平仓时间 {event.get('closed_at')}")
    for w in warnings or []:
        lines.append(f"⚠ {w.get('message')}")
    lines.append(f"交易日 {trading_day}")
    return "\n".join(lines)


def build_wechat_body(
    event: dict,
    *,
    trading_day: str,
    link_base: str,
    system_text: str,
) -> str:
    link = (link_base or "").strip()
    if link:
        sep = "&" if "?" in link else "?"
        link = f"{link}{sep}day={trading_day}"
    body = system_text.replace("\n", "\n")
    if link:
        body += f"\n详情:{link}"
    return body


def should_send_wechat(event: dict, settings: dict) -> bool:
    if not settings.get("enabled", True):
        return False
    webhook = (settings.get("wechat_webhook") or "").strip()
    if not webhook or "replace-me" in webhook.lower():
        return False
    et = str(event.get("event_type") or "")
    if is_program_event(et):
        return bool(settings.get("wechat_on_program_tp_sl", True))
    if et == EVENT_EXTERNAL:
        return False
    return True


def send_supervisor_wechat(
    event: dict,
    *,
    trading_day: str,
    settings: dict,
    system_text: str,
) -> bool:
    if not should_send_wechat(event, settings):
        return False
    from lib.common.wechat_notify_lib import send_wechat_webhook

    prefix = (settings.get("wechat_prefix") or "【交易监管】").strip()
    body = build_wechat_body(
        event,
        trading_day=trading_day,
        link_base=str(settings.get("wechat_link_base") or ""),
        system_text=system_text,
    )
    return bool(
        send_wechat_webhook(
            str(settings.get("wechat_webhook") or ""),
            body,
            prefix=prefix,
        )
    )


_notify_hook: Optional[Callable[[], None]] = None


def set_supervisor_notify_hook(fn: Optional[Callable[[], None]]) -> None:
    global _notify_hook
    _notify_hook = fn


def _fire_notify() -> None:
    if _notify_hook:
        try:
            _notify_hook()
        except Exception:
            pass


def process_supervisor_tick(
    dashboard_payload: dict | None,
    board_payload: dict | None,
    settings_root: dict | None,
    *,
    reset_hour: int = 8,
    ai_reply_fn: Optional[Callable[..., str]] = None,
) -> dict[str, Any]:
    """单次监管扫描:对比快照,写会话,推微信,可选 AI 评语."""
    from hub_ai.supervisor_store import (
        append_supervisor_ai_message,
        append_supervisor_system_message,
        ensure_supervisor_session,
    )

    sup_cfg = normalize_supervisor_settings((settings_root or {}).get("supervisor"))
    if not sup_cfg.get("enabled", True):
        return {"ok": True, "skipped": True, "reason": "disabled"}

    dash = dashboard_payload or {}
    trading_day = str(dash.get("trading_day") or current_trading_day(reset_hour=reset_hour))
    state = load_supervisor_state()
    if str(state.get("trading_day") or "") != trading_day:
        state = {
            "version": 1,
            "trading_day": trading_day,
            "processed": [],
            "positions": {},
            "stats": {trading_day: state.get("stats", {}).get(trading_day, {})},
            "positions_baseline_ready": False,
        }

    processed = set(str(x) for x in (state.get("processed") or []))
    stats = dict(state.get("stats") or {})
    prev_positions = dict(state.get("positions") or {})
    curr_positions = collect_position_keys(board_payload)
    closed_trades = dash.get("closed_trades") or []
    board_ready = _board_agent_snapshot_ready(board_payload)

    if not state.get("positions_baseline_ready"):
        for trade in closed_trades:
            if isinstance(trade, dict):
                processed.add(f"close:{_trade_event_id(trade)}")
        if not board_ready:
            state["trading_day"] = trading_day
            state["processed"] = list(processed)
            save_supervisor_state(state)
            return {"ok": True, "events": 0, "waiting_board": True, "trading_day": trading_day}
        state["trading_day"] = trading_day
        state["processed"] = list(processed)
        state["positions"] = curr_positions
        state["positions_baseline_ready"] = True
        state["initialized"] = True
        save_supervisor_state(state)
        return {
            "ok": True,
            "events": 0,
            "seeded": True,
            "trading_day": trading_day,
            "positions": len(curr_positions),
        }

    raw_events = detect_new_opens(prev_positions, curr_positions) + detect_new_closes(
        processed, closed_trades
    )
    if not raw_events:
        state["positions"] = curr_positions
        save_supervisor_state(state)
        return {"ok": True, "events": 0}

    session = ensure_supervisor_session(trading_day)
    session_id = str(session.get("id") or "")
    handled = 0

    for event in raw_events:
        eid = str(event.get("event_id") or uuid.uuid4().hex)
        if eid in processed:
            continue
        et = str(event.get("event_type") or "")
        if et == EVENT_EXTERNAL:
            processed.add(eid)
            continue

        warnings = evaluate_frequency_warnings(
            trading_day=trading_day,
            event=event,
            stats=stats,
            settings=sup_cfg,
        )
        if is_supervised_event(et):
            _record_supervised_event(stats, trading_day, event)

        system_text = build_system_message(event, trading_day=trading_day, warnings=warnings)
        append_supervisor_system_message(
            session_id,
            system_text,
            event_type=et,
            level="warn" if warnings else "info",
        )
        send_supervisor_wechat(
            event,
            trading_day=trading_day,
            settings=sup_cfg,
            system_text=system_text,
        )
        for w in warnings:
            warn_event = {
                "event_type": EVENT_FREQ_WARN,
                "event_id": f"warn:{eid}:{w.get('rule')}",
                **event,
                "warn_message": w.get("message"),
            }
            warn_text = f"[{event_tag(EVENT_FREQ_WARN)}] {w.get('message')}"
            append_supervisor_system_message(
                session_id,
                warn_text,
                event_type=EVENT_FREQ_WARN,
                level="warn",
            )
            send_supervisor_wechat(
                warn_event,
                trading_day=trading_day,
                settings=sup_cfg,
                system_text=warn_text,
            )

        if ai_reply_fn and et != EVENT_EXTERNAL:
            evt_snapshot = dict(event)
            evt_warnings = list(warnings)

            def _ai_bg() -> None:
                try:
                    reply = ai_reply_fn(
                        event=evt_snapshot,
                        warnings=evt_warnings,
                        trading_day=trading_day,
                        session_id=session_id,
                    )
                    from hub_ai.text_util import is_ai_error_reply

                    text = str(reply or "").strip()
                    if not text or is_ai_error_reply(text):
                        text = build_supervisor_fallback_reply(evt_snapshot, evt_warnings)
                    if text:
                        append_supervisor_ai_message(session_id, text)
                        _fire_notify()
                except Exception:
                    try:
                        fb = build_supervisor_fallback_reply(evt_snapshot, evt_warnings)
                        if fb:
                            append_supervisor_ai_message(session_id, fb)
                            _fire_notify()
                    except Exception:
                        pass

            threading.Thread(target=_ai_bg, daemon=True).start()

        processed.add(eid)
        handled += 1

    state["trading_day"] = trading_day
    state["processed"] = list(processed)
    state["positions"] = curr_positions
    state["stats"] = stats
    save_supervisor_state(state)
    if handled:
        _fire_notify()
    return {"ok": True, "events": handled, "trading_day": trading_day, "session_id": session_id}
