#!/usr/bin/env python3
"""
亚盘模型 V9 - 精细化优化
1. 时间加权特征（近期比赛权重更高）
2. CLV多档位细分分析
3. 置信度+CLV组合策略
4. 三层投注策略（放弃/黄色/绿色）
"""
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_v9"
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 time_weighted_mean(values, days_ago, half_life=30):
    if not values:
        return 0.5
    values = list(values)
    days_ago = list(days_ago)
    n = min(len(values), len(days_ago))
    if n == 0:
        return 0.5
    values = values[-n:]
    days_ago = days_ago[-n:]
    weights = np.array([np.exp(-d / half_life) for d in days_ago])
    weights /= weights.sum()
    return float(np.average(values, weights=weights))


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
    start_date = df["Date"].min()
    df["days_since_start"] = (df["Date"] - start_date).dt.days
    return df


def build_features_v9(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))
    days_since_start = int(row.get("days_since_start", 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

    # Time-weighted AH
    ah_h_list = h.get("ah_h", [])
    ah_a_list = a.get("ah_a", [])
    days_h_list = h.get("days", [])
    days_a_list = a.get("days", [])
    if len(ah_h_list) >= 3:
        tw_ah_home = time_weighted_mean(ah_h_list[-10:], days_h_list[-10:] if days_h_list else [0]*10)
    else:
        tw_ah_home = 0.5
    if len(ah_a_list) >= 3:
        tw_ah_away = time_weighted_mean(ah_a_list[-10:], days_a_list[-10:] if days_a_list else [0]*10)
    else:
        tw_ah_away = 0.5

    # Compression
    if len(ah_h_list) >= 5:
        recent5 = wr(ah_h_list[-5:])
        recent10 = wr(ah_h_list[-10:]) if len(ah_h_list) >= 10 else recent5
        ah_compression = recent5 - recent10
    else:
        ah_compression = 0.0

    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_low"] = 1.0 if clv < 0.05 else 0.0
    f["clv_med"] = 1.0 if 0.05 <= clv < 0.10 else 0.0
    f["clv_high"] = 1.0 if 0.10 <= clv < 0.15 else 0.0
    f["clv_vhigh"] = 1.0 if clv >= 0.15 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(ah_h_list, 1)
    f["at_ah_strk"] = strk(ah_a_list, 1)
    f["ht_home_ah"] = wr(ah_h_list[-10:] if ah_h_list else [])
    f["at_away_ah"] = wr(ah_a_list[-10:] if ah_a_list else [])
    f["tw_ah_home"] = tw_ah_home
    f["tw_ah_away"] = tw_ah_away
    f["tw_ah_diff"] = tw_ah_home - tw_ah_away
    f["ah_compression"] = ah_compression
    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(ah_h_list[-3:]) if len(ah_h_list) >= 3 else 0)
    f["at_ah_L3"] = float(sum(ah_a_list[-3:]) if len(ah_a_list) >= 3 else 0)
    f["ht_ah_L5"] = float(sum(ah_h_list[-5:]) if len(ah_h_list) >= 5 else 0)
    f["at_ah_L5"] = float(sum(ah_a_list[-5:]) if len(ah_a_list) >= 5 else 0)
    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); h2["days"].append(days_since_start)
    a2["gf"].append(ag); a2["ga"].append(hg); a2["tot"].append(tot); a2["ov"].append(ovv)
    a2["ah_a"].append(1 - ahw); a2["days"].append(days_since_start)
    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)
    days_since_start = int(row.get("days_since_start", 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); h["days"].append(days_since_start)
    a["gf"].append(ag); a["ga"].append(hg); a["tot"].append(tot); a["ov"].append(ovv)
    a["ah_a"].append(1 - ahw); a["days"].append(days_since_start)
    lg["tot"].append(tot); lg["ov"].append(ovv); lg["ah_h"].append(ahw)


def expanding_window_v9(code, df, n_windows=5):
    n = len(df)
    min_train = 300
    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

        df_tr = df.iloc[:train_end]
        df_te = df.iloc[test_start:test_end]
        league_up_rate = float(df_tr["target"].mean()) if len(df_tr) > 0 else 0.5

        ts = defaultdict(lambda: {"gf": [], "ga": [], "tot": [], "ov": [], "ah_h": [], "ah_a": [], "days": []})
        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_v9(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": [], "days": []})
        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_v9(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"]), "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) < 20:
            continue

        sc = StandardScaler()
        Xs_tr = sc.fit_transform(Xtr)
        Xs_te = sc.transform(Xte)
        bm_acc = accuracy_score(yte, bm_dirs_te)

        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

        probs = preds_sum / n_models if n_models > 0 else np.full(len(Xte), 0.5)
        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

        results.append({
            "window": w + 1, "train_n": len(Xtr), "test_n": len(Xte),
            "bm_acc": bm_acc, "model_acc": best_acc, "optimal_thr": best_thr,
            "probs": probs, "yte": yte, "bm_dirs": bm_dirs_te, "clv": clv_te,
        })

    return results


def run_v9(code):
    print(f"\n{'='*65}\n{code} {LNAME.get(code,'')} V9\n{'='*65}")
    df = load_data(code)
    if df.empty:
        print(" 无数据"); return None
    total = len(df)
    print(f" {total}场, 上盘率 {df['target'].mean():.1%}")

    ew = expanding_window_v9(code, df, n_windows=5)
    if ew:
        print(f"\n  Expanding Window ({len(ew)} folds):")
        for r in ew:
            diff = r["model_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['model_acc']:.1%} {s}{diff:.1%}pp thr={r['optimal_thr']:.2f}")
        avg_bm = float(np.mean([r["bm_acc"] for r in ew]))
        avg_mod = float(np.mean([r["model_acc"] for r in ew]))
        avg_thr = float(np.mean([r["optimal_thr"] for r in ew]))
        print(f"  平均: 庄家{avg_bm:.1%} 模型{avg_mod:.1%} {'+' if avg_mod > avg_bm else ''}{(avg_mod-avg_bm)*100:.1f}pp avg_thr={avg_thr:.2f}")

    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": [], "days": []})
    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_v9(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": [], "days": []})
    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_v9(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"]), "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)

    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
    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} {len(yte):>4} {bm_acc:>8.1%}")
    print(f"  {'模型(最优阈值)':<45} {len(yte):>4} {best_acc:>8.1%}")

    # CLV分层
    print(f"\n  CLV分层分析:")
    for clv_lo, clv_hi, label in [(0, 0.05, "极低<5%"), (0.05, 0.10, "低5-10%"), (0.10, 0.15, "中10-15%"), (0.15, 1.0, "高>15%")]:
        mask = (clv_te >= clv_lo) & (clv_te < clv_hi)
        n = int(mask.sum())
        if n < 5: continue
        bm_a = accuracy_score(yte[mask], bm_dirs_te[mask])
        mod_a = accuracy_score(yte[mask], mod_dirs[mask])
        agree_n = int(np.sum(mod_dirs[mask] == bm_dirs_te[mask]))
        print(f"  {label:<12} n={n:>3} 庄家={bm_a:.1%} 模型={mod_a:.1%} 一致={agree_n/n:.0%}")

    # 置信度+CLV组合
    print(f"\n  置信度+CLV组合:")
    best_combo = None
    best_combo_acc = 0
    for conf in [0.55, 0.60, 0.65, 0.70, 0.75, 0.80]:
        for clv_lo in [0.00, 0.05, 0.08, 0.10, 0.12]:
            mask = ((probs >= conf) | (1 - probs >= conf)) & (clv_te >= clv_lo) & (mod_dirs == bm_dirs_te)
            n = int(mask.sum())
            if n < 4: continue
            a = accuracy_score(yte[mask], mod_dirs[mask])
            if a > best_combo_acc:
                best_combo_acc = a
                best_combo = (conf, clv_lo, n, a)

    # Print top combos
    combos_tried = []
    for conf in [0.60, 0.65, 0.70, 0.75, 0.80]:
        for clv_lo in [0.05, 0.08, 0.10, 0.12]:
            mask = ((probs >= conf) | (1 - probs >= conf)) & (clv_te >= clv_lo) & (mod_dirs == bm_dirs_te)
            n = int(mask.sum())
            if n < 4: continue
            a = accuracy_score(yte[mask], mod_dirs[mask])
            combos_tried.append((conf, clv_lo, n, a))

    combos_tried.sort(key=lambda x: -x[3])
    for conf, clv_lo, n, a in combos_tried[:8]:
        flag = " ✅" if a >= 0.85 else (" ⚠️" if a >= 0.70 else "")
        print(f"  conf>={conf:.0%}+CLV>={clv_lo:.0%}+一致: n={n:>3} acc={a:.1%}{flag}")

    # 三层
    tier_green = ((probs >= 0.75) | (1 - probs >= 0.75)) & (mod_dirs == bm_dirs_te)
    tier_yellow = ((probs >= 0.60) | (1 - probs >= 0.60)) & (mod_dirs == bm_dirs_te) & ~tier_green
    n_g = int(tier_green.sum())
    n_y = int(tier_yellow.sum())
    acc_g = accuracy_score(yte[tier_green], mod_dirs[tier_green]) if n_g > 0 else 0
    acc_y = accuracy_score(yte[tier_yellow], mod_dirs[tier_yellow]) 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 '⚠️'}")

    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["model_acc"] for r in ew])) if ew else best_acc

    cfg = {
        "league": code,
        "league_name": LNAME.get(code, code),
        "ew_avg_bm": avg_ew_bm,
        "ew_avg_model": avg_ew_mod,
        "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("亚盘模型 V9 | 时间加权 | CLV细分 | 置信+CLV组合")
    print("=" * 70)
    rs = {}
    for c in sorted(LNAME.keys()):
        r = run_v9(c)
        if r:
            rs[c] = r

    print("\n" + "=" * 70)
    print("V9 最终汇总")
    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("绿色层汇总")
    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()
