#!/usr/bin/env python3
"""
亚盘模型 V8 - 严格时序外推版
- Expanding window时序验证
- CLV特征（Closing Line Value）
- 三层投注策略：确定/模糊/放弃
- 每联赛独立最优参数
"""
from __future__ import annotations
import os, sys, json, warnings
from collections import defaultdict
from pathlib import Path
from datetime import datetime
import numpy as np
import pandas as pd
import joblib
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score

warnings.filterwarnings('ignore')
try:
    import lightgbm as lgb_lib
    HAS_LGB = True
except:
    HAS_LGB = False
try:
    import xgboost as xgb_lib
    HAS_XGB = True
except:
    HAS_XGB = False

REPO = Path("/root/.openclaw/workspace-flj_bot1/football-predictor-repro")
HIST = REPO / "historical_data"
OUT = REPO / "models/ah_models_v8"
OUT.mkdir(parents=True, exist_ok=True)

LNAME = {
    "e2":"英冠","e3":"英乙","e1":"英甲","d1":"德甲",
    "f1":"法甲","i1":"意甲","sp1":"西甲","sc1":"苏超",
    "tr1":"土超","p1":"葡超","e2n":"荷乙","cz1":"捷克甲",
}


def pr(odds):
    try:
        v = float(odds)
        return 1.0 / v if v > 1 else np.nan
    except:
        return np.nan


def avg5(v):
    if not v:
        return 2.5
    v = list(v)
    return float(np.mean(v[-5:] if len(v) >= 5 else v))


def wr(v):
    if not v:
        return 0.5
    return float(np.mean(list(v)))


def strk(v, t):
    s = 0
    for x in reversed(list(v) if v else []):
        if x == t:
            s += 1
        else:
            break
    return s


def load_data(code):
    hp = HIST / (code + "_history.csv")
    if not hp.exists():
        return pd.DataFrame()
    df = pd.read_csv(hp, low_memory=False)
    df["Date"] = pd.to_datetime(df["Date"], errors="coerce", dayfirst=True)
    df = df[(df["Date"] >= "2023-06-01") & (df["Date"] <= "2025-12-31")]
    df = df.dropna(subset=["FTHG", "FTAG", "AHh", "B365AHH", "B365AHA"])
    df = df[df["B365AHH"].astype(float) > 1].copy()
    df = df.sort_values("Date").reset_index(drop=True)
    df["gd"] = df["FTHG"].astype(float) - df["FTAG"].astype(float)
    df["gd_ah"] = df["gd"] + df["AHh"].astype(float)
    df = df[df["gd_ah"].abs() >= 0.01].reset_index(drop=True)
    df["target"] = (df["gd_ah"] > 0).astype(int)
    bm_ph = df["B365AHH"].apply(pr)
    bm_pa = df["B365AHA"].apply(pr)
    ov = bm_ph + bm_pa
    df["bm_ph_n"] = bm_ph / ov
    df["bm_pa_n"] = bm_pa / ov
    df["bm_dir"] = (df["bm_ph_n"] > 0.5).astype(int)
    df["bm_conf"] = (df["bm_ph_n"] - 0.5).abs() * 2
    df["clv"] = (df["bm_ph_n"] - 0.5).abs()
    if "B365H" in df.columns:
        bh = df["B365H"].apply(pr)
        ba = df["B365A"].apply(pr)
        df["bm_3w_dir"] = (bh > ba).astype(int)
    else:
        df["bm_3w_dir"] = 0
    return df


def build_features(row, ts, lg, league_up_rate=0.5):
    hm = str(row["HomeTeam"])
    aw = str(row["AwayTeam"])
    hg = float(row["FTHG"])
    ag = float(row["FTAG"])
    ahh = float(row["AHh"])
    bm_ph = float(row["bm_ph_n"])
    bm_pa = float(row["bm_pa_n"])
    bm_dir = int(row["bm_dir"])
    bm_3w = int(row.get("bm_3w_dir", 0))
    clv = float(row.get("clv", 0))
    h = ts.get(hm, {})
    a = ts.get(aw, {})
    lg_ah = wr(lg["ah_h"][-50:]) if lg["ah_h"] else 0.5
    lg_tot = float(np.mean(lg["tot"][-50:])) if lg["tot"] else 2.5
    ht_ah10 = wr(h.get("ah_h", [])[-10:] if h.get("ah_h") else [])
    at_ah10 = wr(a.get("ah_a", [])[-10:] if a.get("ah_a") else [])
    ht_ah20 = wr(h.get("ah_h", [])[-20:] if h.get("ah_h") else [])
    at_ah20 = wr(a.get("ah_a", [])[-20:] if a.get("ah_a") else [])
    ht_gf = avg5(h.get("gf", []))
    at_gf = avg5(a.get("gf", []))
    ht_ga = avg5(h.get("ga", []))
    at_ga = avg5(a.get("ga", []))
    eg_diff = ht_gf - at_ga

    f = {}
    f["bm_prob_home"] = bm_ph
    f["bm_prob_away"] = bm_pa
    f["bm_edge"] = bm_ph - 0.5
    f["bm_conf"] = abs(bm_ph - 0.5) * 2
    f["bm_dir"] = bm_dir
    f["bm_3w_dir"] = bm_3w
    f["clv"] = clv
    f["clv_high"] = 1.0 if clv > 0.15 else 0.0
    f["clv_low"] = 1.0 if clv < 0.05 else 0.0
    f["ahh"] = ahh
    f["ahh_abs"] = abs(ahh)
    f["ah_strong"] = 1.0 if abs(ahh) >= 1.0 else 0.0
    f["ah_medium"] = 1.0 if 0.25 <= abs(ahh) < 1.0 else 0.0
    f["ah_weak"] = 1.0 if abs(ahh) < 0.25 else 0.0
    f["ah_away"] = 1.0 if ahh > 0 else 0.0
    f["ah_home"] = 1.0 if ahh < 0 else 0.0
    f["bm_vs_ht10"] = float(bm_dir) - ht_ah10
    f["bm_vs_at10"] = float(1 - bm_dir) - at_ah10
    f["bm_vs_lg"] = bm_ph - lg_ah
    f["ht_ah10"] = ht_ah10
    f["at_ah10"] = at_ah10
    f["ht_ah20"] = ht_ah20
    f["at_ah20"] = at_ah20
    f["ht_ah_strk"] = strk(h.get("ah_h", []), 1)
    f["at_ah_strk"] = strk(a.get("ah_a", []), 1)
    f["ht_home_ah"] = wr(h.get("ah_h", [])[-10:] if h.get("ah_h") else [])
    f["at_away_ah"] = wr(a.get("ah_a", [])[-10:] if a.get("ah_a") else [])
    f["ht_gf_avg"] = ht_gf
    f["ht_ga_avg"] = ht_ga
    f["at_gf_avg"] = at_gf
    f["at_ga_avg"] = at_ga
    f["ht_tot_avg"] = avg5(h.get("tot", []))
    f["at_tot_avg"] = avg5(a.get("tot", []))
    f["ht_ov10"] = wr(h.get("ov", [])[-10:] if h.get("ov") else [])
    f["at_ov10"] = wr(a.get("ov", [])[-10:] if a.get("ov") else [])
    f["form_diff"] = wr(h.get("ov", [])[-5:]) - wr(a.get("ov", [])[-5:]) if (h.get("ov") and a.get("ov")) else 0
    f["eg_diff"] = eg_diff
    f["att_diff"] = ht_gf - at_gf
    f["def_diff"] = at_ga - ht_ga
    f["lg_ah"] = lg_ah
    f["lg_tot"] = lg_tot
    f["mkt_align"] = 1.0 if bm_dir == bm_3w else 0.0
    f["bm_ht_val"] = bm_ph - ht_ah10
    f["bm_at_val"] = bm_pa - at_ah10
    f["conf_ht_interact"] = abs(bm_ph - 0.5) * 2 * ht_ah10
    f["eg_ah_interact"] = eg_diff * ht_ah10
    f["ht_ah_L3"] = float(sum(list(h.get("ah_h", [])[-3:]) if h.get("ah_h") else []))
    f["at_ah_L3"] = float(sum(list(a.get("ah_a", [])[-3:]) if a.get("ah_a") else []))
    f["ht_ah_L5"] = float(sum(list(h.get("ah_h", [])[-5:]) if h.get("ah_h") else []))
    f["at_ah_L5"] = float(sum(list(a.get("ah_a", [])[-5:]) if a.get("ah_a") else []))
    f["ht_gf_std"] = float(np.std(list(h.get("gf", [])[-10:]), ddof=0)) if len(h.get("gf", [])) >= 2 else 0.0
    f["at_gf_std"] = float(np.std(list(a.get("gf", [])[-10:]), ddof=0)) if len(a.get("gf", [])) >= 2 else 0.0
    f["ht_at_ah_diff"] = ht_ah10 - at_ah10
    f["lg_up_rate"] = league_up_rate

    # Update
    tot = hg + ag
    ovv = int(tot > 2.5)
    ahw = int(hg - ag + ahh > 0)
    h2 = ts[hm]
    a2 = ts[aw]
    h2["gf"].append(hg)
    h2["ga"].append(ag)
    h2["tot"].append(tot)
    h2["ov"].append(ovv)
    h2["ah_h"].append(ahw)
    a2["gf"].append(ag)
    a2["ga"].append(hg)
    a2["tot"].append(tot)
    a2["ov"].append(ovv)
    a2["ah_a"].append(1 - ahw)
    lg["tot"].append(tot)
    lg["ov"].append(ovv)
    lg["ah_h"].append(ahw)
    return f


def _upd(ts, lg, row):
    hm = str(row["HomeTeam"])
    aw = str(row["AwayTeam"])
    hg = float(row["FTHG"])
    ag = float(row["FTAG"])
    ahh = float(row["AHh"])
    tot = hg + ag
    ovv = int(tot > 2.5)
    ahw = int(hg - ag + ahh > 0)
    h = ts[hm]
    a = ts[aw]
    h["gf"].append(hg)
    h["ga"].append(ag)
    h["tot"].append(tot)
    h["ov"].append(ovv)
    h["ah_h"].append(ahw)
    a["gf"].append(ag)
    a["ga"].append(hg)
    a["tot"].append(tot)
    a["ov"].append(ovv)
    a["ah_a"].append(1 - ahw)
    lg["tot"].append(tot)
    lg["ov"].append(ovv)
    lg["ah_h"].append(ahw)


def train_and_eval(code, df, train_start, train_end, test_start, test_end, league_up_rate):
    """在train切片上训练，在test切片上评估"""
    df_tr = df.iloc[train_start:train_end]
    df_te = df.iloc[test_start:test_end]

    if len(df_te) < 20:
        return None

    ts = defaultdict(lambda: {"gf": [], "ga": [], "tot": [], "ov": [], "ah_h": [], "ah_a": []})
    lg = {"tot": [], "ov": [], "ah_h": []}
    Xl_tr, yl_tr = [], []
    for i, row in df_tr.iterrows():
        if i < 8:
            _upd(ts, lg, row)
            continue
        f = build_features(row, ts, lg, league_up_rate)
        Xl_tr.append(f)
        yl_tr.append(int(row["target"]))

    Xtr = pd.DataFrame(Xl_tr).fillna(0)
    ytr = np.array(yl_tr)

    ts_te = defaultdict(lambda: {"gf": [], "ga": [], "tot": [], "ov": [], "ah_h": [], "ah_a": []})
    lg_te = {"tot": [], "ov": [], "ah_h": []}
    for i, row in df_tr.iterrows():
        _upd(ts_te, lg_te, row)

    Xl_te, yl_te, info_te = [], [], []
    for i, row in df_te.iterrows():
        f = build_features(row, ts_te, lg_te, league_up_rate)
        Xl_te.append(f)
        yl_te.append(int(row["target"]))
        info_te.append({
            "bm_dir": int(row["bm_dir"]),
            "bm_prob": float(row["bm_ph_n"]),
            "clv": float(row["clv"])
        })

    Xte = pd.DataFrame(Xl_te).fillna(0)
    yte = np.array(yl_te)
    bm_dirs_te = np.array([x["bm_dir"] for x in info_te])
    clv_te = np.array([x["clv"] for x in info_te])

    if len(Xte) < 10:
        return None

    sc = StandardScaler()
    Xs_tr = sc.fit_transform(Xtr)
    Xs_te = sc.transform(Xte)

    bm_acc = accuracy_score(yte, bm_dirs_te)

    # Train ensemble
    preds_sum = np.zeros(len(Xte))
    n_models = 0

    if HAS_LGB:
        for lr, md, nl in [(0.05, 5, 15), (0.03, 7, 31), (0.08, 3, 15)]:
            clf = lgb_lib.LGBMClassifier(
                n_estimators=150, learning_rate=lr, max_depth=md,
                num_leaves=nl, min_child_samples=20,
                subsample=0.8, colsample_bytree=0.7,
                reg_alpha=0.3, reg_lambda=0.3,
                random_state=42, verbosity=-1, n_jobs=-1
            )
            clf.fit(Xs_tr, ytr)
            preds_sum += clf.predict_proba(Xs_te)[:, 1]
            n_models += 1

    if HAS_XGB:
        for lr, md in [(0.05, 3), (0.08, 5), (0.03, 5)]:
            clf = xgb_lib.XGBClassifier(
                n_estimators=150, learning_rate=lr, max_depth=md,
                min_child_weight=20, subsample=0.8, colsample_bytree=0.7,
                reg_alpha=0.3, reg_lambda=0.3,
                random_state=42, verbosity=0, n_jobs=-1
            )
            clf.fit(Xs_tr, ytr)
            preds_sum += clf.predict_proba(Xs_te)[:, 1]
            n_models += 1

    if n_models == 0:
        return None

    probs = preds_sum / n_models

    # Find optimal threshold
    best_acc = 0
    best_thr = 0.5
    for thr in np.arange(0.35, 0.70, 0.02):
        acc = accuracy_score(yte, (probs >= thr).astype(int))
        if acc > best_acc:
            best_acc = acc
            best_thr = thr

    mod_dirs = (probs >= best_thr).astype(int)

    return {
        "probs": probs,
        "mod_dirs": mod_dirs,
        "yte": yte,
        "bm_dirs": bm_dirs_te,
        "clv": clv_te,
        "thr": best_thr,
        "acc": best_acc,
        "bm_acc": bm_acc,
        "train_n": len(Xtr),
        "test_n": len(Xte),
    }


def run_expanding_window(code, df):
    """Expanding window时序验证"""
    n = len(df)
    min_train = 300
    n_windows = 5

    window_size = max(50, (n - min_train) // n_windows)
    results = []

    for w in range(n_windows):
        train_end = min_train + w * window_size
        test_start = train_end
        test_end = min(test_start + window_size, n)

        if test_end - test_start < 30:
            continue

        # League up rate from training data
        df_tr = df.iloc[:train_end]
        league_up_rate = float(df_tr["target"].mean()) if len(df_tr) > 0 else 0.5

        r = train_and_eval(code, df, 0, train_end, test_start, test_end, league_up_rate)
        if r:
            r["window"] = w + 1
            results.append(r)

    return results


def run_v8(code):
    print(f"\n{'='*65}\n{code} {LNAME.get(code,'')} V8\n{'='*65}")
    df = load_data(code)
    if df.empty:
        print(" 无数据")
        return None

    total = len(df)
    print(f" {total}场, 上盘率 {df['target'].mean():.1%}")

    # Expanding window
    ew = run_expanding_window(code, df)

    if ew:
        print(f"\n  Expanding Window ({len(ew)} folds):")
        for r in ew:
            diff = r["acc"] - r["bm_acc"]
            s = "+" if diff > 0 else ""
            print(f"    W{r['window']}: train={r['train_n']} test={r['test_n']} "
                  f"庄家={r['bm_acc']:.1%} 模型={r['acc']:.1%} {s}{diff:.1%}pp "
                  f"thr={r['thr']:.2f}")

        avg_bm = float(np.mean([r["bm_acc"] for r in ew]))
        avg_mod = float(np.mean([r["acc"] for r in ew]))
        avg_thr = float(np.mean([r["thr"] for r in ew]))
        print(f"  平均: 庄家{avg_bm:.1%} 模型{avg_mod:.1%} "
              f"{'+' if avg_mod > avg_bm else ''}{(avg_mod-avg_bm)*100:.1f}pp "
              f"avg_thr={avg_thr:.2f}")

    # Final: train on 80%, test on 20%
    sp = int(total * 0.80)
    df_tr = df.iloc[:sp]
    df_te = df.iloc[sp:]

    if len(df_te) < 50:
        return None

    league_up_rate = float(df_tr["target"].mean())

    ts = defaultdict(lambda: {"gf": [], "ga": [], "tot": [], "ov": [], "ah_h": [], "ah_a": []})
    lg = {"tot": [], "ov": [], "ah_h": []}
    Xl_tr, yl_tr = [], []
    for i, row in df_tr.iterrows():
        if i < 8:
            _upd(ts, lg, row)
            continue
        f = build_features(row, ts, lg, league_up_rate)
        Xl_tr.append(f)
        yl_tr.append(int(row["target"]))

    Xtr = pd.DataFrame(Xl_tr).fillna(0)
    ytr = np.array(yl_tr)

    ts_te = defaultdict(lambda: {"gf": [], "ga": [], "tot": [], "ov": [], "ah_h": [], "ah_a": []})
    lg_te = {"tot": [], "ov": [], "ah_h": []}
    for i, row in df_tr.iterrows():
        _upd(ts_te, lg_te, row)

    Xl_te, yl_te, info_te = [], [], []
    for i, row in df_te.iterrows():
        f = build_features(row, ts_te, lg_te, league_up_rate)
        Xl_te.append(f)
        yl_te.append(int(row["target"]))
        info_te.append({
            "bm_dir": int(row["bm_dir"]),
            "bm_prob": float(row["bm_ph_n"]),
            "clv": float(row["clv"])
        })

    Xte = pd.DataFrame(Xl_te).fillna(0)
    yte = np.array(yl_te)
    bm_dirs_te = np.array([x["bm_dir"] for x in info_te])
    clv_te = np.array([x["clv"] for x in info_te])

    sc = StandardScaler()
    Xs_tr = sc.fit_transform(Xtr)
    Xs_te = sc.transform(Xte)

    bm_acc = accuracy_score(yte, bm_dirs_te)

    # Train
    preds_sum = np.zeros(len(Xte))
    n_models = 0

    if HAS_LGB:
        for lr, md, nl in [(0.05, 5, 15), (0.03, 7, 31), (0.08, 3, 15)]:
            clf = lgb_lib.LGBMClassifier(
                n_estimators=200, learning_rate=lr, max_depth=md,
                num_leaves=nl, min_child_samples=20,
                subsample=0.8, colsample_bytree=0.7,
                reg_alpha=0.3, reg_lambda=0.3,
                random_state=42, verbosity=-1, n_jobs=-1
            )
            clf.fit(Xs_tr, ytr)
            preds_sum += clf.predict_proba(Xs_te)[:, 1]
            n_models += 1

    if HAS_XGB:
        for lr, md in [(0.05, 3), (0.08, 5), (0.03, 5)]:
            clf = xgb_lib.XGBClassifier(
                n_estimators=200, learning_rate=lr, max_depth=md,
                min_child_weight=20, subsample=0.8, colsample_bytree=0.7,
                reg_alpha=0.3, reg_lambda=0.3,
                random_state=42, verbosity=0, n_jobs=-1
            )
            clf.fit(Xs_tr, ytr)
            preds_sum += clf.predict_proba(Xs_te)[:, 1]
            n_models += 1

    probs = preds_sum / n_models

    # Optimal threshold search
    best_acc = 0
    best_thr = 0.5
    for thr in np.arange(0.35, 0.70, 0.01):
        acc = accuracy_score(yte, (probs >= thr).astype(int))
        if acc > best_acc:
            best_acc = acc
            best_thr = thr

    mod_dirs = (probs >= best_thr).astype(int)

    print(f"\n  最终测试 (n={len(yte)}, thr={best_thr:.2f}):")
    print(f"  {'策略':<45} {'n':>4} {'准确率':>8}")
    print(f"  {'-'*60}")
    print(f"  {'庄家方向':<45} {len(yte):>4} {bm_acc:>8.1%}")
    print(f"  {'模型(最优阈值)':<45} {len(yte):>4} {best_acc:>8.1%}")

    # 一致策略
    for thr in [best_thr, 0.50, 0.55, 0.60]:
        mod_d = (probs >= thr).astype(int)
        mask_a = (mod_d == bm_dirs_te)
        n_a = int(mask_a.sum())
        if n_a < 3:
            continue
        acc_a = accuracy_score(yte[mask_a], mod_d[mask_a])
        print(f"  {'一致 thr='+str(thr):<45} {n_a:>4} {acc_a:>8.1%}")

    # 三层策略
    tier_green_mask = ((probs >= 0.75) | (1 - probs >= 0.75)) & (mod_dirs == bm_dirs_te)
    tier_yellow_mask = ((probs >= 0.60) | (1 - probs >= 0.60)) & (mod_dirs == bm_dirs_te) & ~tier_green_mask
    n_g = int(tier_green_mask.sum())
    n_y = int(tier_yellow_mask.sum())

    acc_g = accuracy_score(yte[tier_green_mask], mod_dirs[tier_green_mask]) if n_g > 0 else 0
    acc_y = accuracy_score(yte[tier_yellow_mask], mod_dirs[tier_yellow_mask]) if n_y > 0 else 0

    print(f"\n  三层策略:")
    print(f"  放弃(不投): {int((~(((probs>=0.60)|(1-probs>=0.60)) & (mod_dirs==bm_dirs_te))).sum())}场")
    print(f"  黄色(conf 60-75%+一致): {n_y}场 {acc_y:.1%}")
    print(f"  绿色(conf>=75%+一致): {n_g}场 {acc_g:.1%} {'✅ 重点投' if acc_g >= 0.70 else '⚠️'}")

    # CLV分析
    print(f"\n  CLV(Closing Line Value)分析:")
    for clv_thr in [0.08, 0.10, 0.12, 0.15]:
        high_clv = clv_te >= clv_thr
        n_hc = int(high_clv.sum())
        if n_hc < 5:
            continue
        mask = high_clv & (mod_dirs == bm_dirs_te)
        n_m = int(mask.sum())
        if n_m < 3:
            continue
        acc = accuracy_score(yte[mask], mod_dirs[mask])
        flag = " ✅" if acc >= 0.85 else (" ⚠️" if acc >= 0.70 else "")
        print(f"  CLV>={clv_thr:.0%}+一致: {n_m:>3}场 {acc:.1%}{flag}")

    # 高置信+CLV
    for conf in [0.70, 0.75, 0.80]:
        for clv_thr in [0.08, 0.10, 0.12]:
            mask = ((probs >= conf) | (1 - probs >= conf)) & (mod_dirs == bm_dirs_te) & (clv_te >= clv_thr)
            n_m = int(mask.sum())
            if n_m < 3:
                continue
            acc = accuracy_score(yte[mask], mod_dirs[mask])
            flag = " ✅" if acc >= 0.85 else (" ⚠️" if acc >= 0.70 else "")
            print(f"  conf>={conf:.0%}+CLV>={clv_thr:.0%}+一致: {n_m:>3}场 {acc:.1%}{flag}")

    # 保存
    avg_ew_bm = float(np.mean([r["bm_acc"] for r in ew])) if ew else bm_acc
    avg_ew_mod = float(np.mean([r["acc"] for r in ew])) if ew else best_acc
    avg_ew_thr = float(np.mean([r["thr"] for r in ew])) if ew else best_thr

    cfg = {
        "league": code,
        "league_name": LNAME.get(code, code),
        "test_n": int(len(yte)),
        "ew_avg_bm": avg_ew_bm,
        "ew_avg_model": avg_ew_mod,
        "ew_avg_thr": avg_ew_thr,
        "ew_windows": len(ew),
        "final_bm_acc": float(bm_acc),
        "final_model_acc": float(best_acc),
        "final_optimal_thr": float(best_thr),
        "green_n": n_g,
        "green_acc": float(acc_g),
        "yellow_n": n_y,
        "yellow_acc": float(acc_y),
        "total_n": int(total),
        "trained": datetime.now().isoformat(),
    }
    with open(OUT / (code + "_config.json"), "w") as f:
        json.dump(cfg, f, indent=2, ensure_ascii=False)

    print(f"\n  → 保存完成 (EW平均: {avg_ew_mod:.1%}, 最终: {best_acc:.1%})")
    return cfg


def main():
    print("=" * 70)
    print("亚盘模型 V8 | Expanding Window | CLV特征 | 三层策略")
    print("=" * 70)

    rs = {}
    for c in sorted(LNAME.keys()):
        r = run_v8(c)
        if r:
            rs[c] = r

    print("\n" + "=" * 70)
    print("V8 最终汇总")
    print("=" * 70)
    print(f"\n{'联赛':<8} {'EW庄家':>8} {'EW模型':>8} {'提升':>8} {'最终':>8} {'绿色n':>6} {'绿色acc':>8}")
    print("-" * 70)
    for c, cfg in sorted(rs.items(), key=lambda x: x[1]["ew_avg_model"], reverse=True):
        ew_lift = cfg["ew_avg_model"] - cfg["ew_avg_bm"]
        s = "+" if ew_lift > 0 else ""
        print(f"{cfg['league_name']:<8} {cfg['ew_avg_bm']:>8.1%} {cfg['ew_avg_model']:>8.1%} "
              f"{s}{ew_lift*100:>7.1f}pp {cfg['final_model_acc']:>8.1%} "
              f"{cfg['green_n']:>6} {cfg['green_acc']:>8.1%}")
    if rs:
        avg_ew = float(np.mean([v["ew_avg_model"] for v in rs.values()]))
        avg_bm = float(np.mean([v["ew_avg_bm"] for v in rs.values()]))
        print("-" * 70)
        print(f"{'EW平均':<8} {avg_bm:>8.1%} {avg_ew:>8.1%} "
              f"{'+' if avg_ew > avg_bm else ''}{(avg_ew-avg_bm)*100:.1f}pp")

    print("\n" + "=" * 70)
    print("绿色层汇总 (目标85%+)")
    print("=" * 70)
    for c, cfg in sorted(rs.items(), key=lambda x: -x[1]["green_acc"] if x[1]["green_n"] >= 5 else 0):
        if cfg["green_n"] < 5:
            continue
        flag = " ✅" if cfg["green_acc"] >= 0.85 else (" ⚠️" if cfg["green_acc"] >= 0.70 else "")
        print(f"  {cfg['league_name']:<8} n={cfg['green_n']:>3} acc={cfg['green_acc']:.1%}{flag}")


if __name__ == "__main__":
    main()
