#!/usr/bin/env python3
"""
亚盘预测模型 v2 - 高置信度策略（清洁版）
使用2023-2025年历史数据训练，目标找出85%+准确率的投注子集
"""
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.ensemble import ExtraTreesClassifier, RandomForestClassifier, HistGradientBoostingClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score

warnings.filterwarnings('ignore')

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

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

FEAT = [
    'bm_home_prob','bm_away_prob','bm_edge','bm_conf','bm_balance','bm_dir',
    'ahh','ahh_abs','ah_strong','ah_away',
    'h_gf_avg','h_ga_avg','h_tot_avg','h_ov_rate',
    'a_gf_avg','a_ga_avg','a_tot_avg','a_ov_rate',
    'h_ah_rate','a_ah_rate',
    'h_gf_l3','a_gf_l3','h_ov5','a_ov5',
    'h_strk','a_strk',
    'form_diff','eg_diff','att_diff','def_diff','home_adv',
    'bm_vs_h','bm_vs_a','bm_vs_lg',
    'lg_ah_rate','lg_tot','lg_ov',
    'mkt_align','dow','month',
]


def pr(odds):
    return 1.0 / float(odds) if odds and float(odds) > 1 else np.nan

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

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

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

def stdv(v):
    return float(np.std(v, ddof=0)) if len(v) >= 2 else 0.0

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


def load_data(code):
    hp = HIST / f'{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-01-01') & (df['Date'] <= '2025-12-31')]
    df = df.dropna(subset=['FTHG','FTAG','AHh','B365AHH','B365AHA'])
    df = df[df['B365AHH'] > 1].copy()
    df['gd'] = df['FTHG'].astype(float) - df['FTAG'].astype(float)
    df['gd_ah'] = df['gd'] + df['AHh'].astype(float)
    df['push'] = df['gd_ah'].abs() < 0.01
    df = df[~df['push']].reset_index(drop=True)
    df['target'] = (df['gd_ah'] > 0).astype(int)

    # 庄家亚盘概率
    df['bm_ph'] = df['B365AHH'].apply(pr)
    df['bm_pa'] = df['B365AHA'].apply(pr)
    ov = df['bm_ph'] + df['bm_pa']
    df['bm_ph_n'] = df['bm_ph'] / ov
    df['bm_pa_n'] = df['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['bm_edge'] = df['bm_ph_n'] - 0.5
    df['bm_bal'] = (df['B365AHH'].astype(float) - df['B365AHA'].astype(float)).abs()
    return df


def feat_at(row, ts, lh, hm, aw):
    """提取特征：用历史数据，不含当前行"""
    h = ts.get(hm, {'gf':[],'ga':[],'tot':[],'ov':[],'ah':[],'aw':[],
                    'hg':[],'ha':[],'ht':[],'ho':[]})
    a = ts.get(aw, {'gf':[],'ga':[],'tot':[],'ov':[],'ah':[],'aw':[],
                    'hg':[],'ha':[],'ht':[],'ho':[]})
    l = lh

    bm_ph = float(row['bm_ph_n'])
    bm_pa = float(row['bm_pa_n'])
    bm_dir = int(row['bm_dir'])
    ahh = float(row['AHh'])

    h_ah = wr(h.get('ah', [])[-10:])
    a_ah = wr(a.get('aw', [])[-10:])
    l_ah = wr(l.get('ah', [])[-50:])

    h_gf = avg5(h.get('gf', []))
    a_gf = avg5(a.get('gf', []))
    h_ga = avg5(h.get('ga', []))
    a_ga = avg5(a.get('ga', []))

    return {
        'bm_home_prob': bm_ph,
        'bm_away_prob': bm_pa,
        'bm_edge': bm_ph - 0.5,
        'bm_conf': abs(bm_ph - 0.5) * 2,
        'bm_balance': float(row['bm_bal']),
        'bm_dir': bm_dir,
        'ahh': ahh,
        'ahh_abs': abs(ahh),
        'ah_strong': 1.0 if abs(ahh) >= 1.0 else 0.0,
        'ah_away': 1.0 if ahh > 0 else 0.0,
        'h_gf_avg': h_gf,
        'h_ga_avg': h_ga,
        'h_tot_avg': avg5(h.get('tot', [])),
        'h_ov_rate': wr(h.get('ov', [])[-10:]),
        'a_gf_avg': a_gf,
        'a_ga_avg': a_ga,
        'a_tot_avg': avg5(a.get('tot', [])),
        'a_ov_rate': wr(a.get('ov', [])[-10:]),
        'h_ah_rate': h_ah,
        'a_ah_rate': a_ah,
        'h_gf_l3': avg3(h.get('gf', [])),
        'a_gf_l3': avg3(a.get('gf', [])),
        'h_ov5': wr(h.get('ov', [])[-5:]),
        'a_ov5': wr(a.get('ov', [])[-5:]),
        'h_strk': strk(h.get('ah', []) if h.get('ah') else [], 1),
        'a_strk': strk(a.get('aw', []) if a.get('aw') else [], 1),
        'form_diff': wr(h.get('ov', [])[-5:]) - wr(a.get('ov', [])[-5:]),
        'eg_diff': h_gf - a_ga,
        'att_diff': h_gf - a_gf,
        'def_diff': a_ga - h_ga,
        'home_adv': h_gf - a_ga + 0.3,
        'bm_vs_h': float(bm_dir) - h_ah,
        'bm_vs_a': float(1 - bm_dir) - a_ah,
        'bm_vs_lg': bm_ph - l_ah if l_ah > 0 else 0.0,
        'lg_ah_rate': l_ah,
        'lg_tot': float(np.mean(l.get('tot', [2.5]))) if l.get('tot') else 2.5,
        'lg_ov': wr(l.get('ov', [])[-50:]),
        'mkt_align': 1.0 if ((bm_ph > 0.5) == (h_ah > 0.5)) else 0.0,
        'dow': row['Date'].dayofweek if hasattr(row['Date'], 'dayofweek') else 3,
        'month': row['Date'].month if hasattr(row['Date'], 'month') else 6,
    }


def upd(ts, lh, hm, aw, hg, ag, ahh):
    """用比赛结果更新历史"""
    tot = hg + ag
    ov = int(tot > 2.5)
    gd_ah = hg - ag + ahh
    ah_hw = int(gd_ah > 0)

    def _up(d, gf, ga, t, o, ah_v):
        d['gf'].append(gf); d['ga'].append(ga)
        d['tot'].append(t); d['ov'].append(o)
        if ah_v == 'home': d.get('ah', []).append(1)
        elif ah_v == 'away': d.get('aw', []).append(1)

    _up(ts[hm], hg, ag, tot, ov, 'home')
    _up(ts[aw], ag, hg, tot, ov, 'away')
    lh['tot'].append(tot); lh['ov'].append(ov); lh['ah'].append(ah_hw)


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

    # 滚动训练
    ts = defaultdict(lambda: {'gf':[],'ga':[],'tot':[],'ov':[],'ah':[],'aw':[]})
    lh = {'tot':[],'ov':[],'ah':[]}

    # 预热 + 构建数据集
    Xl, yl, info = [], [], []
    WARM = 10
    for i, row in df.iterrows():
        hm = str(row['HomeTeam']); aw = str(row['AwayTeam'])
        hg = float(row['FTHG']); ag = float(row['FTAG']); ahh = float(row['AHh'])
        if i < WARM:
            upd(ts, lh, hm, aw, hg, ag, ahh)
            continue
        f = feat_at(row, ts, lh, hm, aw)
        Xl.append(f); yl.append(int(row['target']))
        info.append({'bm_prob': float(row['bm_ph_n']), 'bm_dir': int(row['bm_dir']), 'ahh': ahh})
        upd(ts, lh, hm, aw, hg, ag, ahh)

    if len(Xl) < 150:
        print(f" 样本不足{len(Xl)}"); return None

    X = pd.DataFrame(Xl)[FEAT].fillna(0)
    y = np.array(yl)

    sp = int(len(X) * 0.8)
    Xtr, Xte = X.iloc[:sp], X.iloc[sp:]
    ytr, yte = y[:sp], y[sp:]
    itr, ite = info[:sp], info[sp:]
    print(f" 训练{len(Xtr)} 测试{len(Xte)}")

    # 训练
    best_a, best_n, best_m, best_s = 0, '', None, None
    for nm, clf in {
        'ET':  ExtraTreesClassifier(n_estimators=500, max_depth=7, min_samples_leaf=8, random_state=42, n_jobs=-1),
        'HGB': HistGradientBoostingClassifier(max_depth=6, max_iter=300, learning_rate=0.03, random_state=42),
        'RF':  RandomForestClassifier(n_estimators=500, max_depth=6, min_samples_leaf=15, random_state=42, n_jobs=-1),
    }.items():
        sc = StandardScaler()
        clf.fit(sc.fit_transform(Xtr), ytr)
        a = accuracy_score(yte, clf.predict(sc.transform(Xte)))
        print(f"  {nm}: {a:.1%}")
        if a > best_a: best_a, best_n, best_m, best_s = a, nm, clf, sc

    bm_a = accuracy_score(yte, np.array([x['bm_dir'] for x in ite]))
    print(f"  庄家基准: {bm_a:.1%}  提升: {(best_a-bm_a)*100:+.1f}pp")

    # 特征重要性
    fi = pd.Series(best_m.feature_importances_, FEAT).sort_values(ascending=False) if hasattr(best_m, 'feature_importances_') else pd.Series()
    print(f"  Top5: {list(fi.head(5).index)}")

    # =============================================
    # 策略分析
    # =============================================
    probs = best_m.predict_proba(best_s.transform(Xte))
    bm_dirs = np.array([x['bm_dir'] for x in ite])
    mod_dirs = (probs[:,1] >= 0.5).astype(int)

    print(f"\n  {'策略':<45} {'n':>4} {'准确率':>8}")
    print(f"  {'-'*60}")
    print(f"  {'基准-纯庄家方向':<45} {len(yte):>4} {bm_a:>8.1%}")
    print(f"  {'基准-纯模型方向':<45} {len(yte):>4} {best_a:>8.1%}")

    best_st = []
    # 置信度阈值
    for mc in [0.55, 0.60, 0.65, 0.70, 0.75, 0.80]:
        for agree in [False, True]:
            for bmc in [0.0, 0.30, 0.50]:
                mask = ((probs[:,1] >= mc) | (probs[:,0] >= mc))
                if agree: mask = mask & (bm_dirs == mod_dirs)
                if bmc > 0:
                    bmc_arr = np.array([abs(x['bm_prob']-0.5)*2 for x in ite])
                    mask = mask & (bmc_arr >= bmc)
                n = mask.sum()
                if n < 8: continue
                a = accuracy_score(yte[mask], mod_dirs[mask])
                lb = f"conf>{mc:.0%}" + ("+一致" if agree else "") + (f"+bmC>{bmc:.0%}" if bmc > 0 else "")
                fl = " ✅" if a >= 0.85 else (" ⚠️" if a >= 0.70 else "")
                print(f"  {lb:<45} {n:>4} {a:>8.1%}{fl}")
                best_st.append((lb, n, a))

    # 极端概率
    for mp in [0.85, 0.88, 0.90, 0.92]:
        for agree in [False, True]:
            mask = ((probs[:,1] >= mp) | (probs[:,0] >= mp))
            if agree: mask = mask & (bm_dirs == mod_dirs)
            n = mask.sum()
            if n < 3: continue
            a = accuracy_score(yte[mask], mod_dirs[mask])
            lb = f"极端prob>{mp:.0%}" + ("+一致" if agree else "")
            fl = " ✅" if a >= 0.85 else (" ⚠️" if a >= 0.70 else "")
            print(f"  {lb:<45} {n:>4} {a:>8.1%}{fl}")
            best_st.append((lb, n, a))

    # 保存
    joblib.dump(best_m, OUT / f'{code}_model.pkl')
    joblib.dump(best_s, OUT / f'{code}_scaler.pkl')
    cfg = {
        'league': code, 'league_name': LNAME.get(code,''),
        'accuracy': float(best_a), 'bookmaker_acc': float(bm_a),
        'model': best_n, 'features': FEAT,
        'warmup': WARM, 'train': int(sp), 'test': int(len(Xte)),
        'trained': datetime.now().isoformat(),
    }
    with open(OUT / f'{code}_config.json', 'w') as f:
        json.dump(cfg, f, indent=2, ensure_ascii=False)
    print(f"  → {code}_model.pkl ({best_n},{best_a:.1%})")
    return cfg


def main():
    print("=" * 70)
    print("亚盘模型 v2 | 高置信度策略 | 目标: 85%+ | 2023-2025数据")
    print("=" * 70)
    rs = {}
    for c in sorted(LNAME.keys()):
        r = train(c)
        if r: rs[c] = r

    print("\n" + "=" * 70)
    print("汇总")
    print("=" * 70)
    print(f"\n{'联赛':<8} {'模型':<6} {'准确率':>8} {'庄家':>8} {'提升':>8}")
    print("-" * 45)
    for c, cfg in sorted(rs.items(), key=lambda x: x[1]['accuracy'], reverse=True):
        lift = cfg['accuracy'] - cfg['bookmaker_acc']
        print(f"{cfg['league_name']:<8} {cfg['model']:<6} {cfg['accuracy']:>8.1%} {cfg['bookmaker_acc']:>8.1%} {'+'if lift>0 else''}{lift:.1%}")
    if rs:
        avg = np.mean([v['accuracy'] for v in rs.values()])
        avg_b = np.mean([v['bookmaker_acc'] for v in rs.values()])
        print("-" * 45)
        print(f"{'平均':<8} {'':<6} {avg:>8.1%} {avg_b:>8.1%} {'+'if avg>avg_b else''}{(avg-avg_b):.1%}")
    print(f"\n模型: {OUT}/")


if __name__ == '__main__':
    main()
