#!/usr/bin/env python3
"""
赔率驱动的大小球预测 - 目标是3-4月预测75%+
=============================================
核心思路：
1. 用API-Football批量抓取各联赛赔率数据（扩展训练集）
2. 结合已有完整赔率的CSV数据(d1/sp1/i1)
3. 防过拟合：Walk-Forward验证 + 预锁阈值 + 特征精简
4. 对3-4月比赛进行评估
"""
import json, os, sys, time, warnings, re
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'
ODDS_CACHE   = REPO_ROOT / 'artifacts' / 'odds_api_cache'
ODDS_CACHE.mkdir(exist_ok=True)
OUTPUT_DIR   = REPO_ROOT / 'experiments' / 'goal_number_mar_april'
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

API_KEY = os.environ.get('API_FOOTBALL_KEY', '57b64e857ebeffdc156f0c41d43c7894')
BASE_URL = 'https://v3.football.api-sports.io'

# 联赛映射: code -> api_football_id, name, has_full_odds_in_csv
LEAGUES = {
    'd1':  {'id': 78,  'name': '德甲', 'has_csv_odds': True},
    'sp1': {'id': 140, 'name': '西甲', 'has_csv_odds': True},
    'i1':  {'id': 135, 'name': '意甲', 'has_csv_odds': True},
    'e0':  {'id': 39,  'name': '英超', 'has_csv_odds': False},
    'f2':  {'id': 61,  'name': '法乙', 'has_csv_odds': False},
    'r1':  {'id': 235, 'name': '俄超', 'has_csv_odds': False},
    'g1':  {'id': 197, 'name': '希腊超', 'has_csv_odds': False},
    'f1':  {'id': 61,  'name': '法甲', 'has_csv_odds': False},
    'e2':  {'id': 40,  'name': '英冠', 'has_csv_odds': False},
    'tr1': {'id': 203, 'name': '土超', 'has_csv_odds': False},
}

WF_TRAIN = 300    # Walk-Forward训练窗口
WF_TEST  = 50     # Walk-Forward测试窗口
WF_STEP  = 20     # 滚动步长
LOCKED_T = 0.60   # 预锁阈值（不要在测试集上调）
MIN_FEATURES = 12
MAX_FEATURES = 20


# ========== API工具函数 ==========
def api_get(endpoint, params, retries=2):
    url = f'{BASE_URL}/{endpoint}'
    for attempt in range(retries):
        try:
            resp = requests.get(url, params=params,
                               headers={'x-apisports-key': API_KEY},
                               timeout=20)
            return resp.json()
        except Exception as e:
            if attempt < retries - 1:
                time.sleep(2)
            else:
                print(f'    API error: {e}')
                return {}
    return {}


def parse_total_odds(bets):
    """从bookmaker数据中提取Over/Under 2.5赔率"""
    for bet in bets:
        name = str(bet.get('name', ''))
        if 'Over/Under' not in name and 'Goals Over' not in name:
            continue
        for val in bet.get('values', []):
            raw = str(val.get('value', ''))
            m = re.match(r'^(Over|Under)\s+([0-9.]+)$', raw, re.I)
            if not m:
                continue
            side, line = m.group(1).lower(), float(m.group(2))
            if abs(line - 2.5) > 0.01:
                continue
            odd = float(val.get('odd', 0))
            if odd <= 1:
                continue
            return side, odd
    return None, None


def fetch_league_odds_via_fixtures(api_id, league_code, seasons=[2023,2024,2025]):
    """
    通过遍历fixtures再查odds的方式，收集历史赔率。
    API-Football免费版对历史odds支持很差，多试几个bookmaker。
    """
    cache_file = ODDS_CACHE / f'{league_code}_api_odds.json'
    if cache_file.exists():
        print(f'  [CACHE] {league_code}')
        with open(cache_file) as f:
            return json.load(f)

    all_fixtures = []

    for season in seasons:
        print(f'  Fetching {league_code} season {season}...')
        # 获取该赛季所有fixtures
        page = 1
        season_fixtures = []
        while True:
            data = api_get('fixtures', {
                'league': api_id,
                'season': season,
                'page': page,
                'timezone': 'Asia/Shanghai',
            }, retries=2)

            fixtures = data.get('response', [])
            if not fixtures:
                break

            for f in fixtures:
                fid  = f['fixture']['id']
                date = f['fixture']['date'][:10]
                home = f['teams']['home']['name']
                away = f['teams']['away']['name']
                score = f['score']
                ft_hg = score.get('fulltime', {}).get('home')
                ft_ag = score.get('fulltime', {}).get('away')
                if ft_hg is None:
                    continue
                season_fixtures.append({
                    'fixture_id': fid, 'date': date,
                    'HomeTeam': home, 'AwayTeam': away,
                    'FTHG': ft_hg, 'FTAG': ft_ag,
                })

            pagination = data.get('paging', {})
            total_pages = pagination.get('totalPages', 1)
            if page >= total_pages:
                break
            page += 1
            time.sleep(0.3)

        all_fixtures.extend(season_fixtures)
        print(f'    {league_code}/{season}: {len(season_fixtures)} fixtures')
        time.sleep(1)

    # 对有结果的fixture查赔率（只查最近的部分，避免API限制）
    print(f'  Fetching odds for {len(all_fixtures)} {league_code} fixtures...')
    results = []
    checked = 0
    skipped = 0

    for fix in all_fixtures:
        checked += 1
        if checked % 100 == 0:
            print(f'    Progress: {checked}/{len(all_fixtures)}')

        fid = fix['fixture_id']
        over_odd = under_odd = None

        # 尝试多个bookmaker id
        for book_id in [1, 12, 6, 2, 3]:
            if over_odd is not None:
                break
            odds_data = api_get('odds', {'fixture': fid, 'book': book_id}, retries=1)
            for bp in odds_data.get('response', []):
                for bookmaker in bp.get('bookmakers', []):
                    bname = bookmaker.get('name', '')
                    for bet in bookmaker.get('bets', []):
                        side, odd = parse_total_odds([bet])
                        if side == 'over' and over_odd is None:
                            over_odd = odd
                        elif side == 'under' and under_odd is None:
                            under_odd = odd
                    if over_odd and under_odd:
                        break
                if over_odd and under_odd:
                    break

        fix['over_odds'] = over_odd
        fix['under_odds'] = under_odd

        if over_odd and under_odd:
            results.append(fix)
        else:
            skipped += 1

        if checked % 500 == 0:
            time.sleep(0.5)

    print(f'  {league_code}: checked={checked}, with_odds={len(results)}, skipped={skipped}')

    # 保存缓存
    with open(cache_file, 'w') as f:
        json.dump(results, f)

    return results


# ========== 数据构建 ==========
def load_or_fetch_league(code):
    """加载CSV数据 + API赔率数据"""
    cfg = LEAGUES[code]

    # 1. 加载历史CSV
    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['source'] = 'csv'

    # 2. 如果CSV有赔率列，提取赔率
    if cfg['has_csv_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

    # 3. 如果CSV没有赔率，尝试用API数据补充
    if not cfg['has_csv_odds']:
        api_data = fetch_league_odds_via_fixtures(
            cfg['id'], code,
            seasons=[2023, 2024, 2025]
        )

        if api_data:
            api_df = pd.DataFrame(api_data)
            api_df['date'] = pd.to_datetime(api_df['date'])
            api_df['source'] = 'api'
            api_df['FTHG'] = pd.to_numeric(api_df['FTHG'], errors='coerce')
            api_df['FTAG'] = pd.to_numeric(api_df['FTAG'], errors='coerce')
            api_df['tot'] = api_df['FTHG'] + api_df['FTAG']
            api_df['is_over'] = (api_df['tot'] > 2.5).astype(int)
            api_df['over_odds'] = pd.to_numeric(api_df['over_odds'], errors='coerce')
            api_df['under_odds'] = pd.to_numeric(api_df['under_odds'], errors='coerce')
            api_df['mkt_over'] = api_df['over_odds'].apply(lambda x: 1/x if x > 1 else np.nan)
            api_df['mkt_und']  = api_df['under_odds'].apply(lambda x: 1/x if x > 1 else np.nan)
            api_df['Date'] = api_df['date']

            # 合并（去重，以CSV优先）
            existing_keys = set(zip(df['Date'].dt.date, df['HomeTeam'], df['AwayTeam']))
            new_rows = []
            for _, row in api_df.iterrows():
                key = (row['Date'].date(), row['HomeTeam'], row['AwayTeam'])
                if key not in existing_keys:
                    new_rows.append(row)
            if new_rows:
                df = pd.concat([df, pd.DataFrame(new_rows)], ignore_index=True)
                df = df.sort_values('Date').reset_index(drop=True)

    return df


def build_features(df):
    """用expanding window构建特征（无未来泄漏）"""
    team_stats = defaultdict(lambda: {
        'gf': [], 'ga': [], 'tot': [], 'over': [],
        'home_gf': [], 'home_ga': [], 'home_over': [],
        'away_gf': [], 'away_ga': [], 'away_over': [],
        'gf3': [], 'ga3': [], 'gf5': [], 'ga5': [],
    })

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

        def avg(v, d=1.5): return float(np.mean(v)) if v else d
        def ln(v, n, d=1.5): v=list(v); return float(np.mean(v[-n:])) if len(v)>=n else (float(np.mean(v)) if v else d)
        def streak(v, val=1):
            s = 0
            for x in reversed(v):
                if x==val: s+=1
                else: break
            return s

        r = {
            'home_gf_avg':  avg(h['gf']),
            'home_ga_avg':  avg(h['ga']),
            'home_tot_avg': avg(h['tot'], 2.6),
            'home_over_rate': avg(h['over'], 0.52),
            'home_gf3':    ln(h['gf3'], 3),
            'home_gf5':    ln(h['gf5'], 5),
            'home_ga3':    ln(h['ga3'], 3),
            'home_ga5':    ln(h['ga5'], 5),
            'home_ovrstk': streak(h['over'], 1),
            'home_undrstk':streak(h['over'], 0),
            'away_gf_avg':  avg(a['gf']),
            'away_ga_avg':  avg(a['ga']),
            'away_tot_avg': avg(a['tot'], 2.6),
            'away_over_rate': avg(a['over'], 0.52),
            'away_gf3':    ln(a['gf3'], 3),
            'away_gf5':    ln(a['gf5'], 5),
            'away_ga3':    ln(a['ga3'], 3),
            'away_ga5':    ln(a['ga5'], 5),
            'away_ovrstk': streak(a['over'], 1),
            'away_undrstk':streak(a['over'], 0),
            'exp_g':       (avg(h['gf']) + avg(a['ga'])) / 2,
            'exp_tot':     (avg(h['tot'],2.6) + avg(a['tot'],2.6)) / 2,
            'over_diff':   avg(h['over'],0.52) - avg(a['over'],0.52),
            'tot_diff':    avg(h['tot'],2.6) - avg(a['tot'],2.6),
            'h2h_over':    0.52,
            'is_over':     int(row['is_over']),
        }

        # 赔率特征（最重要）
        if pd.notna(row.get('mkt_over')) and pd.notna(row.get('mkt_und')):
            r['mkt_over'] = float(row['mkt_over'])
            r['mkt_und']  = float(row['mkt_und'])
            r['mkt_margin'] = float(row['mkt_over'] + row['mkt_und'] - 1)
            r['mkt_fair_over'] = float(row['mkt_over'] / (row['mkt_over'] + row['mkt_und']))
        else:
            r['mkt_over'] = 0.5
            r['mkt_und']  = 0.5
            r['mkt_margin'] = 0.0
            r['mkt_fair_over'] = 0.5

        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

        for stat_dict, g in [(h, tot), (a, tot)]:
            stat_dict['gf'].append(g); stat_dict['ga'].append(g)
            stat_dict['tot'].append(g); stat_dict['over'].append(io)
            stat_dict['gf3'].append(g); stat_dict['ga3'].append(g)
            stat_dict['gf5'].append(g); stat_dict['ga5'].append(g)

        if pd.notna(row.get('FTHG')):
            h['home_gf'].append(float(row['FTHG']))
            h['home_ga'].append(float(row['FTAG']))
            h['home_over'].append(io)
            a['away_gf'].append(float(row['FTAG']))
            a['away_ga'].append(float(row['FTHG']))
            a['away_over'].append(io)

    return pd.DataFrame(rows)


def feature_selection(X, y, max_feat=MAX_FEATURES):
    """基于互信息的特征选择，去除冗余特征"""
    from sklearn.feature_selection import mutual_info_classif

    X_filled = X.fillna(X.median())
    mi = mutual_info_classif(X_filled, y, discrete_features=False, random_state=42)
    feat_scores = sorted(zip(X.columns, mi), key=lambda x: -x[1])

    selected = []
    for feat, score in feat_scores:
        if len(selected) >= max_feat:
            break
        if selected:
            corr = X_filled[selected].corrwith(X_filled[feat]).abs().max()
            if corr < 0.88:
                selected.append(feat)
        else:
            selected.append(feat)
    return selected


def walk_forward(X, y, model_fn, threshold):
    """Walk-Forward验证，返回整体准确率和投注准确率"""
    n = len(X)
    if n < WF_TRAIN + WF_TEST:
        return None

    all_true = []; all_probs = []; 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) < 100 or len(X_te) < 10:
            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 = (probs >= threshold).astype(int)

        all_true.extend(y_te.values)
        all_probs.extend(probs)
        all_bet.extend((probs >= threshold) | (probs <= 1-threshold))

    if not all_true:
        return None

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

    overall_acc = accuracy_score(all_true, (all_probs >= threshold).astype(int))
    bet_true = all_true[all_bet]
    bet_prob = all_probs[all_bet]
    bet_pred = (bet_prob >= threshold).astype(int)
    bet_acc = accuracy_score(bet_true, bet_pred) if len(bet_true) >= 15 else overall_acc

    return {
        'overall_acc': float(overall_acc),
        '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}) - 赔率驱动版")
    print(f"{'='*60}")

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

    print(f"  总数据: {len(df)} 场")
    print(f"  日期: {df['Date'].min().date()} ~ {df['Date'].max().date()}")
    print(f"  有赔率: {df['over_odds'].notna().sum()} 场 ({df['over_odds'].notna().mean():.0%})")
    print(f"  大球率: {df['is_over'].mean():.1%}")

    # 只用有赔率的数据训练（赔率是核心特征）
    df_odds = df[df['over_odds'].notna() & (df['over_odds'] > 1)].copy()
    if len(df_odds) < 500:
        print(f"  [SKIP] 赔率数据不足（{len(df_odds)}场），改用全部数据")
        df_odds = df.copy()
    else:
        print(f"  使用 {len(df_odds)} 场有赔率数据训练")

    print("  构建特征...")
    feat_df = build_features(df_odds)
    target = 'is_over'

    all_cols = [c for c in feat_df.columns if c != target]
    nan_ratio = feat_df[all_cols].isna().mean()
    valid_cols = [c for c in all_cols if nan_ratio[c] < 0.5]
    X_all = feat_df[valid_cols].fillna(0)
    y_all = feat_df[target].astype(int)

    # 特征选择
    split = int(len(X_all) * 0.4)
    selected = feature_selection(X_all.iloc[:split], y_all.iloc[:split])
    print(f"  精选特征({len(selected)}): {selected[:8]}...")
    X_sel = X_all[selected]

    # 分离Mar-Apr测试集（用positional indexing，避免index混淆）
    df_sel = df_odds.reset_index(drop=True)
    X_sel  = X_all[valid_cols].reset_index(drop=True)
    y_all  = y_all.reset_index(drop=True)

    mar_pos = [i for i, d in df_sel.iterrows()
               if d['Date'].month in [3, 4] and d['Date'].year == 2026]
    train_pos = [i for i in range(len(df_sel)) if i not in mar_pos]

    X_train = X_sel.iloc[train_pos].reset_index(drop=True)
    y_train = y_all.iloc[train_pos].reset_index(drop=True)
    X_mar   = X_sel.iloc[mar_pos].reset_index(drop=True)
    y_mar   = y_all.iloc[mar_pos].values  # numpy array

    print(f"\n  Walk-Forward验证 (不含Mar-Apr数据)")
    print(f"  训练: {len(X_train)} 场 | 测试(Mar-Apr): {len(X_mar)} 场")
    print(f"  预锁阈值: {LOCKED_T}")

    models = [
        ('RF_depth6', lambda: RandomForestClassifier(
            n_estimators=200, max_depth=6, min_samples_leaf=20,
            max_features=0.5, random_state=42, n_jobs=-1
        )),
        ('RF_depth8', lambda: RandomForestClassifier(
            n_estimators=200, max_depth=8, min_samples_leaf=15,
            max_features=0.6, random_state=42, n_jobs=-1
        )),
        ('GB_shallow', lambda: GradientBoostingClassifier(
            n_estimators=150, max_depth=4, learning_rate=0.05,
            min_samples_leaf=20, subsample=0.8, random_state=42
        )),
        ('LR_L2', lambda: LogisticRegression(C=0.1, max_iter=1000, random_state=42)),
    ]

    best_result = None; best_name = None; best_fn = None

    for name, fn in models:
        result = walk_forward(X_train.reset_index(drop=True),
                              y_train.reset_index(drop=True),
                              fn, LOCKED_T)
        if result:
            print(f"  {name}: "
                  f"整体={result['overall_acc']:.1%}, "
                  f"投注={result['bet_acc']:.1%}(n={result['bet_count']})")
            if best_result is None or result['bet_acc'] > best_result['bet_acc']:
                best_result = result; best_name = name; best_fn = fn

    if best_result is None:
        print("  [FAIL] 所有模型验证失败"); return None

    print(f"\n  最佳: {best_name}, 投注准确率: {best_result['bet_acc']:.1%}")

    # 最终训练（全量训练数据）
    print("  训练最终模型...")
    scaler = StandardScaler()
    X_full_s = scaler.fit_transform(X_sel.fillna(X_sel.median()))

    final_model = best_fn()
    final_model.fit(X_full_s, y_all)

    # Mar-Apr预测评估
    if len(X_mar) > 0:
        X_mar_s = scaler.transform(X_mar.fillna(X_sel.median()))
        probs_mar = final_model.predict_proba(X_mar_s)[:, 1]
        preds_mar = (probs_mar >= LOCKED_T).astype(int)
        bet_mask_mar = (probs_mar >= LOCKED_T) | (probs_mar <= 1 - LOCKED_T)
        mar_overall = accuracy_score(y_mar, preds_mar)
        bet_true = y_mar[bet_mask_mar]
        bet_pred = preds_mar[bet_mask_mar]
        mar_bet_acc = accuracy_score(bet_true, bet_pred) if bet_mask_mar.sum() >= 5 else 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_mar.sum())})")

        mar_details = df_sel.iloc[mar_pos][['Date','HomeTeam','AwayTeam','FTHG','FTAG','is_over']].copy()
        mar_details['prob'] = probs_mar
        mar_details['pred'] = preds_mar
        mar_details['bet']  = bet_mask_mar
        mar_details['correct'] = (mar_details['pred'] == mar_details['is_over']).astype(int)
        print(f"\n  Mar-Apr详细预测:")
        for _, r in mar_details.iterrows():
            mark = '✓' if r['correct'] else '✗'
            bet_mark = '🔴' if r['bet'] else '  '
            tot = int(r['FTHG'])+int(r['FTAG']) if pd.notna(r['FTHG']) else '?'
            print(f"    {bet_mark}{mark} {str(r['Date'])[:10]} {r['HomeTeam'][:12]:12s} vs {r['AwayTeam'][:12]:12s} "
                  f"| prob={r['prob']:.2f} pred={'大' if r['pred'] else '小':2s} "
                  f"| 实际={tot}g {'大' if r['is_over'] else '小'}")

    # 保存
    out_dir = OUTPUT_DIR / code
    out_dir.mkdir(parents=True, exist_ok=True)

    joblib.dump({
        'model': final_model, 'scaler': scaler,
        'features': selected, 'threshold': LOCKED_T,
        'league': code, 'league_name': LEAGUES[code]['name'],
    }, out_dir / 'model.pkl')

    config = {
        'league': code, 'league_name': LEAGUES[code]['name'],
        'threshold': LOCKED_T, 'features': selected,
        'model_name': best_name,
        'wf_overall': float(best_result['overall_acc']),
        'wf_bet_acc': float(best_result['bet_acc']),
        'wf_bet_count': int(best_result['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_mar.sum()) if len(X_mar)>0 else 0,
    }
    with open(out_dir / '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','e0','f2','r1','g1']
    results = []
    for code in codes:
        if code in LEAGUES:
            r = train_league(code)
            results.append(r)
        else:
            print(f"Unknown: {code}")
    print(f"\n\n{'='*60}")
    print("  全部完成!")
    print(f"{'='*60}")
    for r in results:
        if r:
            status = '✓达标' 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%} {status}")


if __name__ == '__main__':
    main()
