#!/usr/bin/env python3
"""
赔率驱动 + 近因效应 大小球预测 v2
===================================
核心改进：
1. 只用最近2个赛季数据（避免历史分布偏差）
2. 赔率隐含概率作为核心特征（反映当前市场共识）
3. 统计特征简化（只用近因特征，不依赖历史平均）
4. 使用近因加权和样本平衡
5. 预锁高阈值(0.70+)确保只下高置信度注
"""
import json, os, sys, time, warnings
from collections import defaultdict
from pathlib import Path
import numpy as np
import pandas as pd
import joblib
import requests
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score

warnings.filterwarnings('ignore')

REPO_ROOT = Path(__file__).resolve().parents[1]
HIST_DIR  = REPO_ROOT / 'historical_data'
OUTPUT_DIR = REPO_ROOT / 'experiments' / 'goal_number_mar_april_v2'
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

# ===== 关键超参 =====
RECENT_SEASONS = 2       # 只用最近2个赛季，避免分布偏移
LOCKED_T_HIGH  = 0.70    # 高阈值：只下高置信度注
LOCKED_T_LOW   = 0.30    # 低阈值
WF_TRAIN = 200           # Walk-Forward训练窗口（缩小，贴近最近）
WF_TEST  = 30            # Walk-Forward测试窗口
WF_STEP  = 15            # 滚动步长
MIN_BET  = 10            # 最小投注数

LEAGUES = {
    'd1':  {'name': '德甲', 'has_odds': True},
    'sp1': {'name': '西甲', 'has_odds': True},
    'i1':  {'name': '意甲', 'has_odds': True},
    'e0':  {'name': '英超', 'has_odds': False},
    'f2':  {'name': '法乙', 'has_odds': False},
    'r1':  {'name': '俄超', 'has_odds': False},
    'g1':  {'name': '希腊超', 'has_odds': False},
}


def load_league(code):
    """加载数据，只保留最近2个赛季"""
    cfg = LEAGUES[code]
    csv_path = HIST_DIR / f'{code}_history.csv'
    if not csv_path.exists():
        return None

    df = pd.read_csv(csv_path, low_memory=False)
    df['Date'] = pd.to_datetime(df['Date'], dayfirst=True, errors='coerce')
    df = df.sort_values('Date').reset_index(drop=True)
    df['FTHG'] = pd.to_numeric(df['FTHG'], errors='coerce')
    df['FTAG'] = pd.to_numeric(df['FTAG'], errors='coerce')
    df['tot'] = df['FTHG'] + df['FTAG']
    df['is_over'] = (df['tot'] > 2.5).astype(int)
    df['season_year'] = df['Date'].dt.year

    # 只保留最近2个完整赛季 + 当前赛季
    max_year = df['Date'].max().year
    season_cutoff = max_year - RECENT_SEASONS
    df = df[df['season_year'] >= season_cutoff].copy()

    # 赔率列
    if cfg['has_odds']:
        for oo, uu in [('B365>2.5','B365<2.5'), ('Avg>2.5','Avg<2.5')]:
            if oo in df.columns:
                df['over_odds'] = pd.to_numeric(df[oo], errors='coerce')
                df['under_odds'] = pd.to_numeric(df[uu], errors='coerce')
                df['mkt_over'] = df['over_odds'].apply(lambda x: 1/x if x > 1 else np.nan)
                df['mkt_und']  = df['under_odds'].apply(lambda x: 1/x if x > 1 else np.nan)
                break
    return df


def build_features(df):
    """
    简化版特征：只用近期窗口特征，不用"历史平均"
    这样不受历史分布偏差影响
    """
    # 每个球队维护3个滚动窗口
    th = defaultdict(lambda: {
        'gf3': [], 'ga3': [], 'tot3': [], 'over3': [],
        'gf5': [], 'ga5': [], 'tot5': [], 'over5': [],
        'gf10':[], 'ga10':[],'tot10':[],'over10':[],
        'hgf': [], 'hga': [], 'hovr': [],  # 主场
        'agf': [], 'aga': [], 'aovr': [],  # 客场
    })

    rows = []
    for _, row in df.iterrows():
        home = str(row.get('HomeTeam', ''))
        away = str(row.get('AwayTeam', ''))
        h = th[home]; a = th[away]

        def w3(v): return float(np.mean(v[-3:])) if len(v)>=3 else float(np.mean(v)) if v else np.nan
        def w5(v): return float(np.mean(v[-5:])) if len(v)>=5 else float(np.mean(v)) if v else np.nan
        def w10(v):float(np.mean(v[-10:]))if len(v)>=10 else float(np.mean(v)) if v else np.nan
        def ovr(v):
            if not v: return np.nan
            return float(np.sum(v))/len(v)
        def strk(v, val=1):
            s=0
            for x in reversed(v):
                if x==val: s+=1
                else: break
            return s

        r = {
            # 近3场进球（核心）
            'home_gf3':  w3(h['gf3']), 'home_ga3':  w3(h['ga3']),
            'away_gf3':  w3(a['gf3']), 'away_ga3':  w3(a['ga3']),
            # 近5场
            'home_gf5':  w5(h['gf5']), 'home_ga5':  w5(h['ga5']),
            'away_gf5':  w5(a['gf5']), 'away_ga5':  w5(a['ga5']),
            # 近10场
            'home_gf10': w10(h['gf10']),'home_ga10':w10(h['ga10']),
            'away_gf10': w10(a['gf10']),'away_ga10':w10(a['ga10']),
            # Over率滚动
            'home_ovr3':  w3(h['over3']),  'away_ovr3':  w3(a['over3']),
            'home_ovr5':  w5(h['over5']),  'away_ovr5':  w5(a['over5']),
            'home_ovr10': w10(h['over10']),'away_ovr10': w10(a['over10']),
            # 连庄/连小
            'home_ovr_strk': strk(h['over3'],1), 'home_und_strk': strk(h['over3'],0),
            'away_ovr_strk': strk(a['over3'],1), 'away_und_strk': strk(a['over3'],0),
            # 主场/客场特征
            'home_hgf': float(np.mean(h['hgf'])) if h['hgf'] else np.nan,
            'home_hga': float(np.mean(h['hga'])) if h['hga'] else np.nan,
            'away_agf': float(np.mean(a['agf'])) if a['agf'] else np.nan,
            'away_aga': float(np.mean(a['aga'])) if a['aga'] else np.nan,
            # 衍生：期望进球（只用近5场）
            'exp_g_home': w5(h['gf5']) + w5(a['aga']),
            'exp_g_away': w5(a['gf5']) + w5(h['hga']),
            'exp_tot':    (w5(h['gf5'])+w5(h['ga5'])+w5(a['gf5'])+w5(a['ga5']))/2,
            # 近因权重：只用近5场over率差
            'recent_ovr_diff': w5(h['over5']) - w5(a['over5']),
            # 净胜球趋势
            'home_gd3':  w3(h['gf3']) - w3(h['ga3']),
            'away_gd3':  w3(a['gf3']) - w3(a['ga3']),
        }

        # 赔率特征（核心！）
        if pd.notna(row.get('mkt_over')) and pd.notna(row.get('mkt_und')):
            mko = float(row['mkt_over'])
            mku = float(row['mkt_und'])
            r['mkt_over']   = mko
            r['mkt_und']    = mku
            r['mkt_fair']   = mko / (mko + mku)
            r['mkt_margin'] = mko + mku - 1
            r['mkt_ou_ratio'] = float(row['over_odds'])/float(row['under_odds']) if pd.notna(row.get('over_odds')) and pd.notna(row.get('under_odds')) and float(row['under_odds'])>0 else np.nan
        else:
            r['mkt_over'] = r['mkt_und'] = r['mkt_fair'] = 0.5
            r['mkt_margin'] = 0; r['mkt_ou_ratio'] = np.nan

        r['is_over'] = int(row['is_over'])
        rows.append(r)

        # 更新滚动窗口
        tot = float(row['tot'])
        io  = int(row['is_over'])
        hg  = float(row['FTHG']) if pd.notna(row.get('FTHG')) else tot
        ag  = float(row['FTAG']) if pd.notna(row.get('FTAG')) else tot

        # 更新主队统计
        h['gf3'].append(tot);  h['ga3'].append(tot);  h['over3'].append(io)
        h['gf5'].append(tot);  h['ga5'].append(tot);  h['over5'].append(io)
        h['gf10'].append(tot); h['ga10'].append(tot); h['over10'].append(io)
        # 更新客队统计
        a['gf3'].append(tot);  a['ga3'].append(tot);  a['over3'].append(io)
        a['gf5'].append(tot);  a['ga5'].append(tot);  a['over5'].append(io)
        a['gf10'].append(tot); a['ga10'].append(tot); a['over10'].append(io)
        # 主场/客场进球
        if pd.notna(row.get('FTHG')):
            h['hgf'].append(float(row['FTHG']))
            h['hga'].append(float(row['FTAG']))
            h['hovr'].append(io)
            a['agf'].append(float(row['FTAG']))
            a['aga'].append(float(row['FTHG']))
            a['aovr'].append(io)

    return pd.DataFrame(rows)


def walk_forward(X, y, model_fn, threshold_high=LOCKED_T_HIGH, threshold_low=LOCKED_T_LOW):
    n = len(X)
    if n < WF_TRAIN + WF_TEST:
        return None

    all_true=[]; all_prob=[]; all_bet=[]

    for start in range(0, n-WF_TRAIN-WF_TEST+1, WF_STEP):
        tr_end=start+WF_TRAIN; te_end=tr_end+WF_TEST
        X_tr,X_te = X.iloc[start:tr_end], X.iloc[tr_end:te_end]
        y_tr,y_te = y.iloc[start:tr_end], y.iloc[tr_end:te_end]
        if len(X_tr)<80 or len(X_te)<5: continue

        scaler = StandardScaler()
        X_tr_s = scaler.fit_transform(X_tr.fillna(X_tr.median()))
        X_te_s = scaler.transform(X_te.fillna(X_tr.median()))

        model = model_fn()
        model.fit(X_tr_s, y_tr)
        probs = model.predict_proba(X_te_s)[:,1]

        preds = np.zeros(len(probs), dtype=int)
        preds[probs >= threshold_high] = 1
        preds[probs <= threshold_low]  = 0

        bet_mask = (probs >= threshold_high) | (probs <= threshold_low)

        all_true.extend(y_te.values)
        all_prob.extend(probs)
        all_bet.extend(bet_mask)

    if not all_true:
        return None

    all_true = np.array(all_true)
    all_prob = np.array(all_prob)
    all_bet  = np.array(all_bet)

    overall = accuracy_score(all_true, (all_prob >= (threshold_high+threshold_low)/2).astype(int))
    bet_t = all_true[all_bet]; bet_p = (all_prob[all_bet] >= threshold_high).astype(int)
    bet_acc = accuracy_score(bet_t, bet_p) if all_bet.sum() >= MIN_BET else overall

    return {'overall':float(overall), 'bet_acc':float(bet_acc), 'bet_count':int(all_bet.sum())}


def train_league(code):
    print(f"\n{'='*60}")
    print(f"  {LEAGUES[code]['name']} ({code}) - v2 近因赔率驱动")
    print(f"{'='*60}")

    df = load_league(code)
    if df is None:
        print("  [SKIP] 无数据"); return None

    has_odds = LEAGUES[code]['has_odds']
    df_odds = df[df['mkt_over'].notna() & (df['mkt_over'] > 0)] if has_odds else df

    print(f"  数据: {len(df_odds)}场, 大球率:{df_odds['is_over'].mean():.1%}")
    print(f"  预锁阈值: >={LOCKED_T_HIGH} 或 <={LOCKED_T_LOW}")

    # 构建特征
    feat_df = build_features(df_odds)
    target='is_over'
    cols = [c for c in feat_df.columns if c!=target]
    nan = feat_df[cols].isna().mean()
    valid = [c for c in cols if nan[c]<0.5]
    X = feat_df[valid].fillna(0)
    y = feat_df[target].astype(int)

    print(f"  特征: {len(valid)}个")

    # Mar-Apr测试集（最后一段）
    df_odds = df_odds.reset_index(drop=True)
    X = X.reset_index(drop=True)
    y = y.reset_index(drop=True)

    mar_mask = (df_odds['Date'].dt.month.isin([3,4])) & (df_odds['Date'].dt.year==2026)
    mar_idx  = df_odds[mar_mask].index.tolist()
    train_idx= [i for i in df_odds.index if i not in mar_idx]

    X_train = X.iloc[train_idx]; y_train = y.iloc[train_idx]
    X_mar   = X.iloc[mar_idx];   y_mar   = y.iloc[mar_idx].values

    print(f"  训练:{len(X_train)}场 | 测试Mar-Apr:{len(X_mar)}场")

    # 模型候选
    models = [
        ('RF_d5', lambda: RandomForestClassifier(
            n_estimators=200, max_depth=5, min_samples_leaf=15,
            max_features=0.5, random_state=42, n_jobs=-1)),
        ('RF_d4', lambda: RandomForestClassifier(
            n_estimators=150, max_depth=4, min_samples_leaf=20,
            max_features=0.4, random_state=42, n_jobs=-1)),
        ('GB', lambda: GradientBoostingClassifier(
            n_estimators=100, max_depth=3, learning_rate=0.05,
            min_samples_leaf=25, subsample=0.8, random_state=42)),
        ('LR', lambda: LogisticRegression(C=0.05, max_iter=1000, random_state=42)),
    ]

    best=None; best_name=None; best_fn=None
    for name, fn in models:
        r = walk_forward(X_train, y_train, fn)
        if r:
            print(f"  {name}: 投注={r['bet_acc']:.1%}(n={r['bet_count']})")
            if best is None or r['bet_acc']>best['bet_acc']:
                best=r; best_name=name; best_fn=fn

    if best is None:
        print("  [FAIL]"); return None

    print(f"\n  最佳: {best_name} {best['bet_acc']:.1%}")

    # 训练最终模型（全量）
    scaler = StandardScaler()
    X_full_s = scaler.fit_transform(X.fillna(X.median()))
    final = best_fn()
    final.fit(X_full_s, y)

    # Mar-Apr评估
    if len(X_mar)>0:
        Xm_s = scaler.transform(X_mar.fillna(X.median()))
        probs = final.predict_proba(Xm_s)[:,1]
        preds = np.zeros(len(probs),dtype=int)
        preds[probs>=LOCKED_T_HIGH]=1
        preds[probs<=LOCKED_T_LOW]=0
        bet_mask = (probs>=LOCKED_T_HIGH)|(probs<=LOCKED_T_LOW)

        mar_overall = accuracy_score(y_mar, preds)
        if bet_mask.sum()>=5:
            mar_bet_acc = accuracy_score(y_mar[bet_mask], preds[bet_mask])
        else:
            mar_bet_acc = mar_overall

        print(f"\n  Mar-Apr评估({len(X_mar)}场):")
        print(f"    整体准确率: {mar_overall:.1%}")
        print(f"    投注准确率: {mar_bet_acc:.1%}(n={int(bet_mask.sum())})")

        # 详细预测
        mar_df = df_odds.iloc[mar_idx].reset_index(drop=True)
        mar_df['prob'] = probs
        mar_df['pred'] = preds
        mar_df['bet']  = bet_mask
        mar_df['correct'] = (mar_df['pred']==mar_df['is_over']).astype(int)

        print(f"\n  详细预测:")
        for _, r2 in mar_df.iterrows():
            mark='✓' if r2['correct'] else '✗'
            bet='🔴' if r2['bet'] else '  '
            tot=int(r2['FTHG'])+int(r2['FTAG']) if pd.notna(r2['FTHG']) else '?'
            pred_label='大' if r2['pred']==1 else '小'
            actual='大' if r2['is_over']==1 else '小'
            print(f"    {bet}{mark} {str(r2['Date'])[:10]} {str(r2['HomeTeam'])[:12]:12s} vs {str(r2['AwayTeam'])[:12]:12s} "
                  f"| prob={r2['prob']:.2f} {pred_label} | {tot}g {actual}")

    # 保存
    out = OUTPUT_DIR/code; out.mkdir(parents=True,exist_ok=True)
    joblib.dump({'model':final,'scaler':scaler,'features':valid,
                 'threshold_high':LOCKED_T_HIGH,'threshold_low':LOCKED_T_LOW,
                 'league':code,'league_name':LEAGUES[code]['name']}, out/'model.pkl')

    config={'league':code,'league_name':LEAGUES[code]['name'],
            'threshold_high':LOCKED_T_HIGH,'threshold_low':LOCKED_T_LOW,
            'features':valid,'model_name':best_name,
            'wf_bet_acc':float(best['bet_acc']),'wf_bet_count':int(best['bet_count']),
            'mar_apr_total':int(len(X_mar)),
            'mar_apr_overall':float(mar_overall) if len(X_mar)>0 else None,
            'mar_apr_bet_acc':float(mar_bet_acc) if len(X_mar)>0 else None,
            'mar_apr_bet_count':int(bet_mask.sum()) if len(X_mar)>0 else 0}
    with open(out/'config.json','w',encoding='utf-8') as f:
        json.dump(config,f,ensure_ascii=False,indent=2)

    return config


def main():
    codes = sys.argv[1:] if len(sys.argv)>1 else ['d1','sp1','i1']
    results=[]
    for code in codes:
        if code in LEAGUES:
            r=train_league(code)
            results.append(r)
    print(f"\n\n{'='*60}")
    for r in results:
        if r:
            s='✓达标' if (r.get('mar_apr_bet_acc') or 0)>=0.75 else '✗未达标'
            print(f"  {r['league_name']:6s}: Mar-Apr={r.get('mar_apr_bet_acc',0):.1%} {s}")


if __name__=='__main__':
    main()
