如何建立nba投注模型

建立NBA投注模型需要四個核心步驟:數據收集特徵工程演算法建模盤口校正。透過使用 Python 的 nba_api 抓取官方數據,並納入主客場、背靠背出賽與進階數據(如真實命中率、進階防守數據)訓練模型,即可量化評估勝率與讓分盤。

1. 數據收集 (Data Collection)

  • 資料來源:使用 Python 的 nba_api 套件或 Basketball Reference 取得歷史與即時的賽事數據。
  • 關鍵數據維度
    • 球隊數據:進攻效率(ORTG)、防守效率(DRTG)、場均節奏(Pace)、真實命中率(TS%)。
    • 情境變數:主客場戰績、休息天數(例如是否為 Back-to-Back 背靠背賽程)、移動距離與時差。
    • 傷病名單:主力球員的缺陣對球隊勝率有決定性影響。

2. 特徵工程 (Feature Engineering)

將原始數據轉換為具備預測力的指標:

  • 淨勝分差 (Net Rating):衡量兩隊攻防實力的絕對標準。
  • 場均節奏標準化:由於各隊節奏不同,比較數據時需透過標準化來評估真實效率。
  • Net Rest (休息優勢):計算雙方休息天數的差距(例如:對手剛打完客場背靠背,而主隊已休息三天)。

3. 演算法建模 (Modeling)

根據預測目標選擇適合的機器學習模型 Scikit Learn

  • 邏輯迴歸 (Logistic Regression):最適合用於預測兩隊的勝負機率 (Win Probability)
  • 隨機森林 (Random Forest) 或 XGBoost:適合納入大量情境變數(球員傷病、休息天數)來預測總分 (Over/Under)讓分盤 (Point Spread) 的勝負。
  • 評估模型時,重點觀察交叉驗證的準確率,避免過度擬合 (Overfitting)。

4. 盤口校正與資金管理 (Odds & Bankroll Management)

  • 對標盤口:將模型預測的分數與國際盤口(或台灣運彩盤口)進行比較。當模型預測的讓分與莊家開出的盤口存在顯著差異(即 Value Bet 價值投注)時,才是進場時機 運用卷積神經網路模型預測NBA 籃球競賽勝負之研究
  • 資金控管:嚴格執行凱利公式 (Kelly Criterion) 或固定比例投注,避免單場重壓。

這是一份使用 Python 的 nba_api 套件抓取歷史數據,並透過 scikit-learn 建立邏輯迴歸勝負預測模型的完整程式碼框架:

1. 安裝必要套件

請先在終端機安裝所需的 Python 函式庫:

pip install nba_api pandas scikit-learn

2. Python 實作程式碼

import time
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score, classification_report
from nba_api.stats.endpoints import leaguegamefinder

# ==========================================
# 步驟一:使用 nba_api 抓取歷史賽事數據
# ==========================================
print("正在從 NBA API 抓取數據...")

# 抓取最近幾個賽季的常規賽數據
game_finder = leaguegamefinder.LeagueGameFinder(
    player_or_team_abbreviation='T',  # T 代表 Team (球隊)
    season_type_playoffs='Regular Season'
)
games = game_finder.get_data_frames()[0]

# 篩選出 NBA 的比賽 (排除 G-League 或夏季聯賽)
games = games[games['リーグ_ID'] == '00'] if 'リーグ_ID' in games.columns else games[games['TEAM_ID'].astype(str).str.startswith('161')]
# 確保欄位名稱正確,一般為 LEAGUE_ID
games = games[games['LEAGUE_ID'] == '00']

# 轉換日期格式並排序
games['GAME_DATE'] = pd.to_datetime(games['GAME_DATE'])
games = games.sort_values('GAME_DATE')

print(f"成功抓取 {len(games)} 筆球隊賽事記錄。")

# ==========================================
# 步驟二:特徵工程 (計算滾動平均數據)
# ==========================================
# 由於投注時不能使用當場比賽的結果,必須使用「賽前的近況數據」
print("正在計算滾動平均特徵 (Rolling Averages)...")

# 定義我們關心的基礎統計量
features_to_roll = ['PTS', 'FGM', 'FGA', 'FG_PCT', 'FG3M', 'TOV', 'REB']

# 計算每隊過去 5 場比賽的平均表現 (不包含當場比賽,所以要 shift)
rolled_games = []
for team_id, team_df in games.groupby('TEAM_ID'):
    team_df = team_df.sort_values('GAME_DATE')
    rolling = team_df[features_to_roll].shift(1).rolling(window=5).mean()
    rolling.columns = [f'{col}_ROLL5' for col in features_to_roll]
    
    # 合併滾動特徵與原始基礎資訊
    combined = pd.concat([team_df[['GAME_ID', 'GAME_DATE', 'TEAM_ID', 'WL', 'MATCHUP']], rolling], axis=1)
    rolled_games.append(combined)

df_features = pd.concat(rolled_games).dropna()

# ==========================================
# 步驟三:合併主客場數據,建構單場對決矩陣
# ==========================================
# NBA API 中,一場比賽會有兩筆記錄(主隊一筆、客隊一筆)
# 我們透過 GAME_ID 將兩者合併為一列

# 主隊記錄 (MATCHUP 包含 'vs.')
home_games = df_features[df_features['MATCHUP'].str.contains('vs.')].copy()
# 客隊記錄 (MATCHUP 包含 '@')
away_games = df_features[df_features['MATCHUP'].str.contains('@')].copy()

# 合併主客場
model_df = pd.merge(
    home_games, 
    away_games, 
    on='GAME_ID', 
    suffixes=('_HOME', '_AWAY')
)

# 建立目標標籤:主隊贏 = 1,主隊輸 = 0
model_df['HOME_WIN'] = model_df['WL_HOME'].apply(lambda x: 1 if x == 'W' else 0)

# ==========================================
# 步驟四:模型訓練與評估
# ==========================================
# 特徵清單:主隊近況 vs 客隊近況
feature_cols = (
    [f'{col}_ROLL5_HOME' for col in features_to_roll] + 
    [f'{col}_ROLL5_AWAY' for col in features_to_roll]
)

X = model_df[feature_cols]
y = model_df['HOME_WIN']

# 切分訓練集與測試集 (拿最後 20% 的比賽當測試)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, shuffle=False)

# 數據標準化
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# 初始化並訓練邏輯迴歸模型
model = LogisticRegression()
model.fit(X_train_scaled, y_train)

# 預測與評估
y_pred = model.predict(X_test_scaled)
y_pred_proba = model.predict_proba(X_test_scaled)[:, 1] # 取得主隊獲勝的精確機率

print("\n=== 模型評估報告 ===")
print(f"預測準確率 (Accuracy): {accuracy_score(y_test, y_pred):.2%}")
print("\n詳細分類報告:")
print(classification_report(y_test, y_pred))

# ==========================================
# 步驟五:應用範例 (查看最新比賽的預測機率)
# ==========================================
print("\n=== 預測範例展示 ===")
sample_results = pd.DataFrame({
    'GAME_ID': model_df.loc[X_test.index, 'GAME_ID'],
    'MATCHUP': model_df.loc[X_test.index, 'MATCHUP_HOME'],
    'HOME_WIN_PROBABILITY': y_pred_proba,
    'ACTUAL_RESULT': y_test
}).tail(5)

print(sample_results.to_string(index=False))

3. 如何使用這個機率來找「價值投注 (Value Bet)」?

當模型跑出 HOME_WIN_PROBABILITY(主隊勝率)後,您可以將其轉換為模型隱含賠率,公式為:
\(\text{模型賠率}=\frac{1}{\text{主隊勝率}}\)

  • 範例
    • 模型預測塞爾提克主場勝率為 75% → 模型合理賠率應為 1 / 0.75 = 1.33。
    • 如果此時台灣運彩或國際盤口開出的塞爾提克獨贏賠率是 1.50
    • 結論:莊家低估了塞爾提克的勝率(莊家認為勝率僅 1/1.50 = 66.7%),這就是一個具備正期望值的價值投注點

將目標改為預測讓分盤 (Spread)總分大小 (Over/Under)

預測讓分盤 (Spread)總分大小 (Over/Under) 需要將模型從「分類」轉為「迴歸 (Regression)」,核心邏輯是直接預測兩隊的精確得分,再拿預測總分與分差去對標莊家盤口。

以下是將上述 Python 架構升級為得分預測模型(使用 Ridge 脊迴歸或 XGBoost 概念)的完整程式碼:

1. 預測邏輯

  • 總分 (Over/Under) = 預測主隊得分 + 預測客隊得分
  • 分差 (Spread) = 預測主隊得分 – 預測客隊得分

2. Python 升級程式碼

import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import Ridge
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_absolute_error, mean_squared_error
from nba_api.stats.endpoints import leaguegamefinder

# ==========================================
# 步驟一:數據抓取與基本滾動特徵(同前,聚焦進攻與節奏)
# ==========================================
print("正在抓取與處理數據...")
game_finder = leaguegamefinder.LeagueGameFinder(player_or_team_abbreviation='T', season_type_playoffs='Regular Season')
games = game_finder.get_data_frames()[0]
games = games[games['LEAGUE_ID'] == '00']
games['GAME_DATE'] = pd.to_datetime(games['GAME_DATE'])
games = games.sort_values('GAME_DATE')

# 預測得分的關鍵滾動指標:得分、投籃命中率、三分命中率、失誤
features_to_roll = ['PTS', 'FG_PCT', 'FG3_PCT', 'TOV']

rolled_games = []
for team_id, team_df in games.groupby('TEAM_ID'):
    team_df = team_df.sort_values('GAME_DATE')
    # 計算過去 5 場平均
    rolling = team_df[features_to_roll].shift(1).rolling(window=5).mean()
    rolling.columns = [f'{col}_ROLL5' for col in features_to_roll]
    
    # 同時加入過去 5 場的「失分平均」(對手得分),衡量防守
    opp_pts_rolling = team_df['PTS'].shift(1).rolling(window=5).mean()
    rolling['OPP_PTS_ROLL5'] = opp_pts_rolling
    
    combined = pd.concat([team_df[['GAME_ID', 'GAME_DATE', 'TEAM_ID', 'MATCHUP', 'PTS']], rolling], axis=1)
    rolled_games.append(combined)

df_features = pd.concat(rolled_games).dropna()

# 合併主客場
home_games = df_features[df_features['MATCHUP'].str.contains('vs.')].copy()
away_games = df_features[df_features['MATCHUP'].str.contains('@')].copy()

model_df = pd.merge(home_games, away_games, on='GAME_ID', suffixes=('_HOME', '_AWAY'))

# ==========================================
# 步驟二:定義迴歸目標 (實際得分)
# ==========================================
# 實際總分與實際分差(主減客)
model_df['ACTUAL_TOTAL'] = model_df['PTS_HOME'] + model_df['PTS_AWAY']
model_df['ACTUAL_SPREAD'] = model_df['PTS_HOME'] - model_df['PTS_AWAY']

# 特徵欄位
feature_cols = (
    [f'{col}_ROLL5_HOME' for col in features_to_roll] + ['OPP_PTS_ROLL5_HOME'] +
    [f'{col}_ROLL5_AWAY' for col in features_to_roll] + ['OPP_PTS_ROLL5_AWAY']
)

X = model_df[feature_cols]
y_home = model_df['PTS_HOME']  # 目標一:主隊得分
y_away = model_df['PTS_AWAY']  # 目標二:客隊得分

# 切分訓練與測試集
X_train, X_test, y_train_home, y_test_home = train_test_split(X, y_home, test_size=0.2, random_state=42, shuffle=False)
_, _, y_train_away, y_test_away = train_test_split(X, y_away, test_size=0.2, random_state=42, shuffle=False)

# 標準化
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# ==========================================
# 步驟三:訓練兩個獨立的得分預測模型
# ==========================================
model_home = Ridge(alpha=1.0)
model_away = Ridge(alpha=1.0)

model_home.fit(X_train_scaled, y_train_home)
model_away.fit(X_train_scaled, y_train_away)

# 預測測試集得分
pred_home = model_home.predict(X_test_scaled)
pred_away = model_away.predict(X_test_scaled)

# 計算模型預測的總分與讓分
pred_total = pred_home + pred_away
pred_spread = pred_home - pred_away

# 實際結果
actual_total = model_df.loc[X_test.index, 'ACTUAL_TOTAL']
actual_spread = model_df.loc[X_test.index, 'ACTUAL_SPREAD']

print("\n=== 模型精確度評估 (平均絕對誤差 MAE) ===")
print(f"總分預測平均誤差: {mean_absolute_error(actual_total, pred_total):.2f} 分")
print(f"分差預測平均誤差: {mean_absolute_error(actual_spread, pred_spread):.2f} 分")

3. 如何比對盤口進行投注?

當您獲得 pred_spread(預測分差)與 pred_total(預測總分)後,直接對標運彩盤口:

💡 讓分盤 (Spread) 應用

  • 情境:模型預測湖人對勇士,分差為 +5.5(預測湖人贏 5.5 分)。
  • 盤口:莊家開出湖人 -2.5(湖人讓 2.5 分)。
  • 決策:模型認為湖人實力大幅領先盤口,具有 3 分 的優勢價值(5.5 – 2.5),此時推薦投注 湖人讓分勝

💡 總分大小 (Over/Under) 應用

  • 情境:模型預測兩隊總分為 232.5 分。
  • 盤口:莊家開出大小分門檻為 226.5 分。
  • 決策:模型預測比盤口高出 6 分,存在顯著價值,推薦投注 大分 (Over)

4. 進階優化提示(如何讓模型更準?)

目前模型僅使用基礎的滾動平均,若要提高實戰準確率,必須引入:

  1. Pace (節奏) 修正:兩隊進攻效率高,但如果今晚預期節奏極慢,總分就會開小。
  2. 傷病權重 (Player Impact):如果球隊明星中鋒(如 Embiid 或 Jokic)缺陣,需手動或自動將該隊預測得分扣減 5-8 分。

接下來,您想了解如何抓取並加入球隊節奏 (Pace)進攻/防守效率 (ORTG/DRTG) 指標,還是如何建立自動比對即時盤口的警報機制?

抓取並加入球隊節奏 (Pace)進攻/防守效率 (ORTG/DRTG) 指標並建立自動比對即時盤口的警報機制

1. 抓取進階指標與模型訓練

進階指標的好處在於它們獨立於比賽節奏。例如,ORTG 代表「每百回合得分」,這能精準反映球隊的真實進攻火力,而不會受到比賽打得快或慢的干擾。

import time
import pandas as pd
import numpy as np
from sklearn.linear_model import Ridge
from sklearn.preprocessing import StandardScaler
# 導入 nba_api 進階數據端點
from nba_api.stats.endpoints import leaguedashteamstats, leaguegamefinder

# ==========================================
# 步驟一:抓取進階效率指標 (ORTG / DRTG / PACE)
# ==========================================
print("正在抓取 NBA 團隊進階數據...")

# 抓取指定賽季的每隊進階數據(以 2025-26 賽季為例)
advanced_stats = leaguedashteamstats.LeagueDashTeamStats(
    measure_type_detailed_defense='Advanced',
    season='2025-26',
    season_type_all_star='Regular Season'
).get_data_frames()[0]

# 篩選我們需要的關鍵特徵:進攻效率、防守效率、淨效率、節奏
advanced_df = advanced_stats[['TEAM_ID', 'TEAM_NAME', 'OFF_RATING', 'DEF_RATING', 'NET_RATING', 'PACE']]
print("成功建立進階指標對照表。")

# ==========================================
# 步驟二:整合進常規常規賽程模型中
# ==========================================
# 抓取賽事歷史
game_finder = leaguegamefinder.LeagueGameFinder(player_or_team_abbreviation='T', season_type_playoffs='Regular Season')
games = game_finder.get_data_frames()[0]
games = games[games['LEAGUE_ID'] == '00']

# 將進階指標 Mapping 回每一場比賽的球隊
games_with_adv = pd.merge(games, advanced_df, on='TEAM_ID', how='left')

# 計算包含進階指標的 5 場滾動平均 (Rolling Averages)
features_to_roll = ['OFF_RATING', 'DEF_RATING', 'PACE', 'PTS']
rolled_games = []

for team_id, team_df in games_with_adv.sort_values('GAME_DATE').groupby('TEAM_ID'):
    rolling = team_df[features_to_roll].shift(1).rolling(window=5).mean()
    rolling.columns = [f'{col}_ROLL5' for col in features_to_roll]
    combined = pd.concat([team_df[['GAME_ID', 'GAME_DATE', 'TEAM_ID', 'MATCHUP', 'PTS']], rolling], axis=1)
    rolled_games.append(combined)

df_features = pd.concat(rolled_games).dropna()

# 合併主客場對決矩陣
home_games = df_features[df_features['MATCHUP'].str.contains('vs.')].copy()
away_games = df_features[df_features['MATCHUP'].str.contains('@')].copy()
model_df = pd.merge(home_games, away_games, on='GAME_ID', suffixes=('_HOME', '_AWAY'))

# 訓練模型(此處省略標準的分割與 fit 步驟,同前文 Ridge 迴歸模型)
# 假設我們已經訓練好兩個模型:model_home_pts 和 model_away_pts

2. 即時盤口比對與自動警報機制

當模型能夠基於進階數據預測出今日比賽的「精確得分」後,我們需要寫一個腳本:定時抓取即時盤口 → 計算模型與盤口的差距(Edge) → 觸發通報

下方程式碼展示如何設定這個監控循環,並整合常用的 Telegram Bot 警報(您也可以換成 LINE Notify):

import requests
import json

# 設定您的 Telegram Bot Token 與 Chat ID (用於接收警報)
TELEGRAM_TOKEN = "YOUR_BOT_TOKEN"
CHAT_ID = "YOUR_CHAT_ID"

def send_telegram_alert(message):
    """發送即時訊息到手機 Telegram"""
    url = f"https://telegram.org{TELEGRAM_TOKEN}/sendMessage"
    payload = {"chat_id": CHAT_ID, "text": message, "parse_mode": "Markdown"}
    try:
        requests.post(url, json=payload)
    except Exception as e:
        print(f"警報發送失敗: {e}")

# ==========================================
# 步驟三:模擬抓取今日即時盤口 (Odds API)
# ==========================================
# 備註:實務上可串接 The Odds API, Odds API 或是台灣運彩網頁爬蟲
def fetch_live_odds():
    """
    模擬從盤口 API 獲取的即時數據
    格式:球隊、莊家開出的讓分 (Spread)、大小分 (Total)
    """
    return [
        {
            "game_id": "99901",
            "home_team": "LAL", "away_team": "BOS",
            "bookie_spread": -3.5,  # 莊家盤口:主隊讓 3.5 分
            "bookie_total": 224.5   # 莊家盤口:大小分 224.5 分
        },
        {
            "game_id": "99902",
            "home_team": "GSW", "away_team": "PHX",
            "bookie_spread": 1.5,   # 莊家盤口:主隊受讓 1.5 分
            "bookie_total": 231.0
        }
    ]

# ==========================================
# 步驟四:自動比對與核心警報邏輯
# ==========================================
def run_odds_monitor(model_home, model_away, scaler):
    print("🚨 啟動即時盤口監控警報系統...")
    
    # 1. 獲取最新即時盤口
    live_games = fetch_live_odds()
    
    # 定義觸發警報的最小優勢門檻(例如:模型與盤口相差 3.0 分以上才投注)
    SPREAD_THRESHOLD = 3.0
    TOTAL_THRESHOLD = 4.5
    
    for game in live_games:
        # 2. 丟入今日兩隊的最新進階滾動數據進行預測
        # (實務上此處需帶入當天兩隊最新特徵向量 X_today)
        # 這裡用假數據模擬模型預測結果:
        pred_home_score = 115.2
        pred_away_score = 108.4
        
        # 計算模型預測的讓分與總分
        pred_spread = pred_home_score - pred_away_score # +6.8 (主贏 6.8 分)
        pred_total = pred_home_score + pred_away_score   # 223.6 分
        
        # 3. 比對讓分盤 (Spread)
        # 莊家開主讓 3.5 (等於 -3.5),模型認為主贏 6.8,相差 3.3 分
        spread_edge = pred_spread - (-game['bookie_spread']) 
        
        if abs(spread_edge) >= SPREAD_THRESHOLD:
            recommendation = f"🔥 【讓分價值投資提示】\n賽事:{game['home_team']} vs {game['away_team']}\n"
            recommendation += f"莊家讓分:{game['bookie_spread']}\n模型預測分差:{pred_spread:+.1f}\n"
            recommendation += f"💡 建議:投注 【{game['home_team'] if spread_edge > 0 else game['away_team']} 讓分盤】 (優勢值: {abs(spread_edge):.1f} 分)\n"
            
            print(recommendation)
            send_telegram_alert(recommendation)
            
        # 4. 比對總分大小盤 (Over/Under)
        total_edge = pred_total - game['bookie_total']
        
        if abs(total_edge) >= TOTAL_THRESHOLD:
            alert_type = "大分 (OVER)" if total_edge > 0 else "小分 (UNDER)"
            total_rec = f"📊 【大小分價值投資提示】\n賽事:{game['home_team']} vs {game['away_team']}\n"
            total_rec += f"莊家總分盤:{game['bookie_total']}\n模型預測總分:{pred_total:.1f}\n"
            total_rec += f"💡 建議:投注 【{alert_type}】 (優勢值: {abs(total_edge):.1f} 分)\n"
            
            print(total_rec)
            send_telegram_alert(total_rec)

# 執行監控
# run_odds_monitor(model_home, model_away, scaler)

3. 實戰營運的最佳工作流 (Workflow)

要讓這個系統自動運作,建議的架構配置如下:

[ 雲端排程 (Cron Job) ]
       │
       ▼ (每日早上 8:00 觸發)
[ Python 腳本 ] ───► 1. 透過 nba_api 更新昨晚最新數據,重訓更新模型參數
       │
       ▼ (每 10 分鐘輪詢一次)
[ 即時盤口 API ] ──► 2. 下載最新國際/台灣運彩即時盤口
       │
       ▼ 
[ 核心比對模組 ] ──► 3. 模型預測值 vs 即時盤口 > 門檻值?
       │
       ▼ (若滿足條件)
[ 手機警報通報 ] ──► 4. Telegram / LINE 自動推播,提醒您手動或自動下注

使用The Odds API平台盤口作為基準並加入自動化定時執行Windows 工作排程器教學

第一步:獲取 The Odds API 金鑰

  1. 前往 The Odds API 官網 註冊一個免費帳號 [The Odds API]。
  2. 登入後即可在個人控制台取得專屬的 API_KEY(免費方案每月提供 500 次點數) [The Odds API]。

第二步:撰寫 Python 盤口比對腳本

請將下方完整程式碼儲存為 nba_odds_monitor.py。此腳本會直接調用 The Odds API 的最新 NBA 數據,並與模型預測進行比對,當發現具備價值的場次時,將透過 Telegram 自動發送通知。

import os
import requests
import pandas as pd
from datetime import datetime

# ==========================================
# 核心參數設定
# ==========================================
THE_ODDS_API_KEY = "您的_THE_ODDS_API_金鑰"
TELEGRAM_TOKEN = "您的_TELEGRAM_BOT_TOKEN"
CHAT_ID = "您的_TELEGRAM_CHAT_ID"

# 警報觸發門檻(模型預測與莊家盤口相差幾分以上才發通知)
SPREAD_THRESHOLD = 3.0  # 讓分盤差距大於 3 分
TOTAL_THRESHOLD = 4.5   # 總分盤差距大於 4.5 分

def send_telegram_alert(message):
    """發送即時訊息至手機 Telegram"""
    url = f"https://telegram.org{TELEGRAM_TOKEN}/sendMessage"
    payload = {"chat_id": CHAT_ID, "text": message, "parse_mode": "Markdown"}
    try:
        requests.post(url, json=payload, timeout=10)
    except Exception as e:
        print(f"發送警報失敗: {e}")

def fetch_live_odds():
    """從 The Odds API 抓取美國/歐洲主流莊家的 NBA 即時讓分與總分盤口"""
    url = "https://the-odds-api.com"
    params = {
        'apiKey': THE_ODDS_API_KEY,
        'regions': 'us',          # 'us' 包含 Bet365, DraftKings 等;亦可改為 'eu'
        'markets': 'spreads,totals',
        'oddsFormat': 'decimal'
    }
    
    try:
        response = requests.get(url, params=params, timeout=15)
        if response.status_code != 200:
            print(f"API 請求失敗,錯誤碼: {response.status_code}")
            return []
        return response.json()
    except Exception as e:
        print(f"網絡請求異常: {e}")
        return []

def run_analysis():
    print(f"[{datetime.now()}] 啟動 NBA 盤口價值分析...")
    games_data = fetch_live_odds()
    
    for game in games_data:
        home_team = game['home_team']
        away_team = game['away_team']
        
        # 尋找目標莊家(此處以 bet365 為例,亦可更換為 lowvig 或 pinnacle)
        bookmaker = next((b for b in game['bookmakers'] if b['key'].lower() == 'bet365'), None)
        if not bookmaker:
            continue
            
        bookie_spread = None
        bookie_total = None
        
        # 解析該莊家的讓分盤與總分盤
        for market in bookmaker['markets']:
            if market['key'] == 'spreads':
                home_outcome = next((o for o in market['outcomes'] if o['name'] == home_team), None)
                if home_outcome:
                    bookie_spread = home_outcome['point']
            elif market['key'] == 'totals':
                over_outcome = next((o for o in market['outcomes'] if o['name'].lower() == 'over'), None)
                if over_outcome:
                    bookie_total = over_outcome['point']
        
        # ----------------------------------------------------
        # 模擬模型預測(實務上請在此引入您訓練好的模型與今日最新滾動數據)
        # ----------------------------------------------------
        pred_home_score = 114.5
        pred_away_score = 108.0
        pred_spread = pred_home_score - pred_away_score  # 模型預測分差(主隊贏 6.5 分)
        pred_total = pred_home_score + pred_away_score    # 模型預測總分(222.5 分)
        
        # 比對讓分盤
        if bookie_spread is not None:
            # API 的讓分為負數代表主隊讓分。此處轉換為實際分數差距進行比較
            actual_bookie_spread = -bookie_spread 
            spread_edge = pred_spread - actual_bookie_spread
            
            if abs(spread_edge) >= SPREAD_THRESHOLD:
                target_side = home_team if spread_edge > 0 else away_team
                msg = f"🔥 *【讓分盤價值提示】*\n賽事:{away_team} @ {home_team}\n"
                msg += f"莊家盤口:主隊 {bookie_spread:+}\n模型預測分差:{pred_spread:+.1f}\n"
                msg += f"🎯 推薦投注:【{target_side} 讓分】(優勢值: {abs(spread_edge):.1f} 分)"
                print(msg)
                send_telegram_alert(msg)
                
        # 比對總分大小盤
        if bookie_total is not None:
            total_edge = pred_total - bookie_total
            if abs(total_edge) >= TOTAL_THRESHOLD:
                ou_type = "大分 (OVER)" if total_edge > 0 else "小分 (UNDER)"
                msg = f"📊 *【大小分價值提示】*\n賽事:{away_team} @ {home_team}\n"
                msg += f"莊家總分盤:{bookie_total}\n模型預測總分:{pred_total:.1f}\n"
                msg += f"🎯 推薦投注:【{ou_type}】(優勢值: {abs(total_edge):.1f} 分)"
                print(msg)
                send_telegram_alert(msg)

if __name__ == "__main__":
    run_analysis()

第三步:建立 Windows 批次檔 (.bat)

Windows 工作排程器直接呼叫 .py 檔案容易因環境變數配置錯誤而失效,因此建議透過一個 .bat 檔來執行導向:

  1. 在 Python 腳本所在的同一個資料夾內,點擊滑鼠右鍵新建一個文字檔案,將其命名為 run_nba_monitor.bat
  2. 編輯該檔案並寫入以下內容(請根據您電腦的實際路徑進行修改):
@echo off
cd /d "C:\您的專案路徑\nba_model"
"C:\Users\您的用戶名\AppData\Local\Programs\Python\Python310\python.exe" nba_odds_monitor.py
exit

(提示:若不確定 python.exe 的精確路徑,可在 Windows 命令提示字元中輸入 where python 查詢)

第四步:設定 Windows 工作排程器 (Task Scheduler)

為了配合 NBA 盤口隨時間劇烈波動的特性,最理想的配置是每天早上 8 點觸發,且每隔 30 分鐘自動重跑一次

  1. 開啟工具:按下鍵盤上的 Win + R 鍵,輸入 taskschd.msc 並按下回車,打開「工作排程器」。
  2. 建立基本工作:在右側操作面板中,點擊 「建立基本工作…」
  3. 命名工作:在名稱欄位輸入 NBA_Live_Odds_Monitor,點擊下一步。
  4. 觸發程序:選擇 「每天」,點擊下一步。
  5. 設定啟動時間:將開始時間設定為今天的 「上午 08:00:00」,間隔設定為 1 天,點擊下一步。
  6. 選取動作:選擇 「啟動程式」,點擊下一步。
  7. 瀏覽腳本位置
    • 在「程式或指令碼」欄位點擊瀏覽,選取剛剛建好的 run_nba_monitor.bat
    • 重要關鍵:在「開始位置 (選填)」欄位,務必填入該批次檔所在的資料夾路徑(例如:C:\您的專案路徑\nba_model\,結尾請補上斜線),點擊下一步。
  8. 進階配置屬性:勾選頁面底部的 「當我點擊 [完成] 時,開啟這項工作的屬性對話方塊」,隨後點擊完成。
  9. 設定每 30 分鐘重複執行
    • 在新跳出的屬性視窗中,切換至 「觸發程序」 索引標籤。
    • 選取您剛設定的每日觸發程序,點擊下方的 「編輯…」
    • 在進階設定區塊中,勾選 「重複工作每:」,在下拉選單中手動輸入或選取 30 分鐘
    • 將右側的「持續時間」從 1 小時修改為 1 天
    • 點擊確定退出所有視窗。

只要您的電腦保持開機並連接網路,系統就會在每天球賽開打前至進行中,每半小時自動抓取盤口、比對模型,並在第一時間將具有正期望值的投注選項推播至您的手機。


透過 API 抓取每日最新傷病名單 (Injury Report) 並與模型得分自動增減連動並加入凱利公式 (Kelly Criterion) 來讓警報訊息自動算出該場比賽推薦下注的資金比例

1. 核心邏輯設計

  • 傷病扣分機制:建立明星球員的「分數價值表」(例如:Jokic 缺陣 = 球隊預測得分 -7.5 分)。
  • 凱利公式公式

2. 升級版 Python 實作程式碼

請將以下程式碼儲存並取代原本的 nba_odds_monitor.py 內容:

import requests
import numpy as np
import pandas as pd
from datetime import datetime

# ==========================================
# 1. 核心參數與 API 設定
# ==========================================
THE_ODDS_API_KEY = "您的_THE_ODDS_API_金鑰"
TELEGRAM_TOKEN = "您的_TELEGRAM_BOT_TOKEN"
CHAT_ID = "您的_TELEGRAM_CHAT_ID"

# 資金管理設定
TOTAL_BANKROLL = 100000  # 您的總下注資金總額 (例如 10 萬台幣)
KELLY_FRACTION = 0.25    # 1/4 凱利公式,降低波動風險

# 2026 賽季核心球員傷病價值對照表 (可根據 PER 或 WARP 指標滾動調整)
PLAYER_VALUE_MAP = {
    'Nikola Jokic': 7.5,
    'Luka Doncic': 7.0,
    'Giannis Antetokounmpo': 6.5,
    'Shai Gilgeous-Alexander': 6.0,
    'Jayson Tatum': 5.5,
    'Joel Embiid': 7.0,
    'Stephen Curry': 5.0
}

def send_telegram_alert(message):
    url = f"https://telegram.org{TELEGRAM_TOKEN}/sendMessage"
    payload = {"chat_id": CHAT_ID, "text": message, "parse_mode": "Markdown"}
    try: requests.post(url, json=payload, timeout=10)
    except Exception as e: print(f"發送警報失敗: {e}")

# ==========================================
# 2. 抓取今日最新傷病名單 (模擬即時 API)
# ==========================================
def fetch_today_injuries():
    """
    實務上可串接彈性的體育新聞 API 或使用 nba_api 監控球員狀態。
    此處模擬今日官方公佈的缺陣名單 (Status: Out)。
    """
    return [
        {'player_name': 'Nikola Jokic', 'team': 'DEN', 'status': 'Out'},
        {'player_name': 'Stephen Curry', 'team': 'GSW', 'status': 'Questionable'} # 僅計算確定的 Out
    ]

# ==========================================
# 3. 核心數學工具:分差轉勝率 & 凱利公式
# ==========================================
def spread_to_win_probability(predicted_spread):
    """
    根據歷史數據,將預測分差(主隊分數 - 客隊分數)轉換為勝率。
    使用 Logistic 函數逼近:分差每多 1 分,勝率隨之提高。
    """
    # 歷史統計公式:勝率 = 1 / (1 + e^(-0.15 * 分差))
    # 若 pred_spread = +5 (主贏5分),主隊勝率約為 68%
    home_win_prob = 1 / (1 + np.exp(-0.15 * predicted_spread))
    return home_win_prob

def calculate_kelly_size(win_prob, bookie_odds):
    """
    標準凱利公式計算
    win_prob: 模型評估勝率 (0~1)
    bookie_odds: 莊家歐洲盤賠率 (如 1.95)
    """
    b = bookie_odds - 1  # 淨賠率
    q = 1 - win_prob     # 輸球機率
    
    if b <= 0: return 0.0
    
    # 凱利公式: f* = (bp - q) / b
    f_star = (b * win_prob - q) / b
    
    # 僅在期望值為正時推薦下注
    if f_star > 0:
        return f_star * KELLY_FRACTION # 乘上分注控制係數
    return 0.0

# ==========================================
# 4. 自動化比對與風控主程式
# ==========================================
def run_advanced_analysis():
    print(f"[{datetime.now()}] 啟動包含傷病修正與凱利風控的監控系統...")
    
    # 抓取即時盤口
    odds_url = "https://the-odds-api.com"
    params = {'apiKey': THE_ODDS_API_KEY, 'regions': 'us', 'markets': 'spreads', 'oddsFormat': 'decimal'}
    
    try:
        response = requests.get(odds_url, params=params, timeout=15)
        games_data = response.json()
    except Exception as e:
        print(f"盤口抓取異常: {e}")
        return

    # 載入今日傷病
    injury_list = fetch_today_injuries()
    out_players = [i['player_name'] for i in injury_list if i['status'] == 'Out']

    for game in games_data:
        home_team = game['home_team']
        away_team = game['away_team']
        
        # 尋找目標莊家 Bet365 獨贏與讓分盤
        bookmaker = next((b for b in game['bookmakers'] if b['key'].lower() == 'bet365'), None)
        if not bookmaker: continue
            
        bookie_spread = None
        home_odds = 1.95  # 模擬莊家即時賠率,實務上可從 h2h 或 spreads 盤口中解析對應賠率
        
        for market in bookmaker['markets']:
            if market['key'] == 'spreads':
                home_outcome = next((o for o in market['outcomes'] if o['name'] == home_team), None)
                if home_outcome:
                    bookie_spread = home_outcome['point']
                    home_odds = home_outcome['price'] # 取得莊家開出的精確賠率
        
        if bookie_spread is None: continue
            
        # ----------------------------------------------------
        # 模型原始得分預測 (基準分)
        # ----------------------------------------------------
        base_home_score = 116.0
        base_away_score = 110.0
        
        # ----------------------------------------------------
        # 自動化傷病調整連動
        # ----------------------------------------------------
        # 檢查是否有對照表中的明星球員今晚缺陣
        # 這裡模擬:假設 Nikola Jokic 屬於客隊且今晚缺陣
        for player in out_players:
            if player in PLAYER_VALUE_MAP:
                # 實務上需判斷球員屬於哪一隊,此處以模擬邏輯示範
                if player == 'Nikola Jokic':  # 假設 Jokic 效力客隊
                    base_away_score -= PLAYER_VALUE_MAP[player]
                    print(f"⚠️ 傷病警報: {player} 缺陣,客隊得分下修 {PLAYER_VALUE_MAP[player]} 分")

        # 計算修正後的預測數據
        pred_spread = base_home_score - base_away_score  # 調整後的預測分差
        
        # ----------------------------------------------------
        # 勝率轉換與凱利公式資金計算
        # ----------------------------------------------------
        # 計算相對於莊家讓分盤口,模型預估的「贏盤勝率」
        # 例如:莊家開主隊讓 3.5 分,模型認為主隊能贏 13.5 分,則優勢分差為 +10 分
        advantage_margin = pred_spread - (-bookie_spread)
        win_probability = spread_to_win_probability(advantage_margin)
        
        # 帶入凱利公式計算下注比例
        bet_ratio = calculate_kelly_size(win_probability, home_odds)
        
        # ----------------------------------------------------
        # 滿足價值門檻,發送含資金建議的警報
        # ----------------------------------------------------
        if bet_ratio > 0.02:  # 下注比例大於 2% 總資金才觸發警報,避免微小機率噪音
            suggested_amount = TOTAL_BANKROLL * bet_ratio
            
            msg = f"🚨 *【高價值投注+自動風控提示】*\n"
            msg += f"賽事:{away_team} @ {home_team}\n"
            msg += f" Bet365 讓分盤口:主隊 {bookie_spread:+}\n"
            msg += f" 傷病修正預測分差:{pred_spread:+.1f} 分\n"
            msg += f" 評估赢盤勝率:{win_probability:.1%}\n\n"
            msg += f"💰 *【資金分配建議】*\n"
            msg += f" 建議下注比例:*{bet_ratio:.2%}* (1/4 凱利風控)\n"
            msg += f" 建議下注金額:*${suggested_amount:,.0f}* TWD / 單位"
            
            print(msg)
            send_telegram_alert(msg)

if __name__ == "__main__":
    run_advanced_analysis()

3. 實戰營運與優化建議

當您成功在 Windows 工作排程器中執行這套系統後,請注意以下優化維護重點:

  1. 傷病名單更新時間差:NBA 官方的傷病報告(Injury Report)通常在比賽當天每小時整點更新一次。因此將 Windows 排程器設定為每 30 或 60 分鐘執行一次,可以最快抓到因明星球員突發缺陣(例如賽前熱身臨時不打)導致的盤口受讓變動紅利。
  2. 賠率即時變動 b:The Odds API 的 price 欄位會隨莊家調整而變動,程式中 calculate_kelly_size 會自動讀取最新賠率。如果莊家因傷病迅速更改盤口(例如盤口從 -3.5 變成 -8.5),advantage_margin 會縮小,凱利公式算出的下注金額也會自動減少或不推薦下注,實現自動風控迴路

建立一個本地的SQLite資料庫紀錄歷史投注用來檢驗模型長期的實際勝率與回測並將模型擴展到季後賽

第一步:建立 SQLite 資料庫與自動紀錄模組

SQLite 是 Python 內建的輕量級資料庫,不需額外安裝即可使用。我們建立一個名為 nba_betting.db 的檔案,用來儲存每筆投注建議,並在賽後自動核對真實賽果。

請將此資料庫模組整合進您先前的盤口監控腳本中:

import sqlite3
import pandas as pd
from datetime import datetime

DB_NAME = "nba_betting.db"

def init_db():
    """初始化 SQLite 資料庫,建立歷史投注紀錄表"""
    conn = sqlite3.connect(DB_NAME)
    cursor = conn.cursor()
    
    # 建立投注紀錄表
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS bet_history (
            game_id TEXT PRIMARY KEY,
            game_date TEXT,
            home_team TEXT,
            away_team TEXT,
            season_type TEXT,        -- 'Regular' 或 'Playoffs'
            bookie_spread REAL,       -- 莊家讓分 (主隊)
            pred_spread REAL,         -- 模型預測分差
            win_prob REAL,            -- 模型評估勝率
            bet_side TEXT,            -- 投注方 (Home_Spread / Away_Spread)
            bet_ratio REAL,           -- 凱利計算下注比例
            actual_home_score INTEGER,-- 賽後更新:主隊實際得分
            actual_away_score INTEGER,-- 賽後更新:客隊實際得分
            bet_result TEXT           -- 賽後更新:'Win' / 'Lose' / 'Push' (走水)
        )
    ''')
    conn.commit()
    conn.close()

def log_bet_suggestion(game_id, home, away, is_playoffs, bookie_line, pred_line, prob, side, ratio):
    """當系統觸發警報時,自動將投注建議寫入資料庫"""
    conn = sqlite3.connect(DB_NAME)
    cursor = conn.cursor()
    
    season_type = 'Playoffs' if is_playoffs else 'Regular'
    date_str = datetime.now().strftime('%Y-%m-%d')
    
    try:
        cursor.execute('''
            INSERT INTO bet_history 
            (game_id, game_date, home_team, away_team, season_type, bookie_spread, pred_spread, win_prob, bet_side, bet_ratio)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        ''', (game_id, date_str, home, away, season_type, bookie_line, pred_line, prob, side, ratio))
        conn.commit()
    except sqlite3.IntegrityError:
        # 若該場比賽已存在,則不重複寫入
        pass
    finally:
        conn.close()

第二步:自動化賽後核對與回測分析

您需要建立另一個獨立腳本(例如設定在每日下午 2 點執行),透過 nba_api 抓取昨晚的最終比分,自動更新資料庫並計算長期勝率淨利潤

from nba_api.stats.endpoints import leaguegamefinder

def update_past_game_results():
    """抓取最新賽果,自動核對資料庫中尚未結算的投注"""
    conn = sqlite3.connect(DB_NAME)
    df_bets = pd.read_sql_query("SELECT * FROM bet_history WHERE bet_result IS NULL", conn)
    
    if df_bets.empty:
        print("所有投注皆已結算完畢。")
        conn.close()
        return

    print(f"正在結算 {len(df_bets)} 筆未完賽賽事...")
    
    # 抓取最新 NBA 賽果
    game_finder = leaguegamefinder.LeagueGameFinder(player_or_team_abbreviation='T', league_id_nullable='00')
    games = game_finder.get_data_frames()[0]
    
    cursor = conn.cursor()
    
    for _, row in df_bets.iterrows():
        g_id = row['game_id']
        # 尋找該場比賽主隊的記錄
        match_game = games[(games['GAME_ID'] == g_id) & (games['MATCHUP'].str.contains('vs.'))]
        
        if not match_game.empty:
            actual_home = int(match_game['PTS'].values[0])
            # 透過隨後的客隊數據獲取客隊得分
            opp_game = games[(games['GAME_ID'] == g_id) & (games['MATCHUP'].str.contains('@'))]
            actual_away = int(opp_game['PTS'].values[0])
            
            # 計算實際讓分盤結果
            actual_diff = actual_home - actual_away
            bookie_line = row['bookie_spread'] # 負數代表主讓
            
            # 判定輸贏邏輯
            result = 'Push'
            if row['bet_side'] == 'Home_Spread':
                # 主隊加上讓分後若大於客隊分數則贏盤
                if actual_home + bookie_line > actual_away: result = 'Win'
                elif actual_home + bookie_line < actual_away: result = 'Lose'
            else:
                # 客隊若受讓
                if actual_home + bookie_line < actual_away: result = 'Win'
                elif actual_home + bookie_line > actual_away: result = 'Lose'
            
            # 更新資料庫
            cursor.execute('''
                UPDATE bet_history 
                SET actual_home_score = ?, actual_away_score = ?, bet_result = ?
                WHERE game_id = ?
            ''', (actual_home, actual_away, result, g_id))
            
    conn.commit()
    conn.close()
    print("賽果結算更新成功。")

def calculate_roi_report():
    """回測統計功能:一鍵生成勝率與資金損益報告"""
    conn = sqlite3.connect(DB_NAME)
    df = pd.read_sql_query("SELECT * FROM bet_history WHERE bet_result IS NOT NULL", conn)
    conn.close()
    
    if df.empty:
        print("尚無結算數據可供回測。")
        return
        
    total_bets = len(df)
    wins = len(df[df['bet_result'] == 'Win'])
    losses = len(df[df['bet_result'] == 'Lose'])
    pushes = len(df[df['bet_result'] == 'Push'])
    
    win_rate = wins / (wins + losses) if (wins + losses) > 0 else 0
    
    print("\n" + "="*30)
    print(f"📊 模型長期歷史回測報告 ({datetime.now().strftime('%Y-%m-%d')})")
    print("="*30)
    print(f"總下注場次: {total_bets} 場 (勝: {wins} / 敗: {losses} / 走水: {pushes})")
    print(f"真實盤口勝率: {win_rate:.2%}")
    print(f"常規賽勝率: {len(df[(df['season_type']=='Regular') & (df['bet_result']=='Win')]) / max(1, len(df[df['season_type']=='Regular'])):.2%}")
    print(f"季後賽勝率: {len(df[(df['season_type']=='Playoffs') & (df['bet_result']=='Win')]) / max(1, len(df[df['season_type']=='Playoffs'])):.2%}")
    print("="*30)

第三步:將模型擴展至季後賽(Playoffs)參數修正

常規賽模型若直接套用在季後賽,通常會遭遇毀滅性的失準。這是因為常規賽看重深度與體能,而季後賽看重巨星上限與戰術方針。擴展至季後賽時,您必須在程式碼中加入以下三個核心微調參數:

1. 節奏下修參數(Pace Reduction Factor)

  • 現象:季後賽各隊防守強度拉滿,半場陣地戰增加,吹判尺度變寬,比賽回合數(Pace)會顯著低於常規賽。
  • 程式修正
# 在季後賽期間,將模型預測的雙方 Pace 滾動平均值自動扣減 3% 到 5%
if is_playoffs:
    predicted_pace = predicted_pace * 0.96 

3. 縮減輪替權重與巨星上場時間(Rotation Tightening)

  • 現象:常規賽球隊會使用 10-12 人輪替,而季後賽教練團會把輪替縮減到 7-8 人,這意味著板凳深度(Bench Depth)數據在季後賽幾乎失效,明星球員的上場時間會從 34 分鐘暴增到 42 分鐘。
  • 程式修正
    • 在計算特徵工程時,不要使用全隊的滾動平均得分。
    • 改為計算前 7 大核心球員(Top 7 Rotation)的進攻與防守效率(ORTG/DRTG)作為主要預測變數。

💡 實戰下一步

  1. 首次執行:請先單獨運行 init_db() 來在本地建立您的資料庫檔案。
  2. 季後賽開打時:在抓取盤口時,若發現 The Odds API 帶入的 season_type 轉為季後賽,或日期已進入 4-6 月,請在主腳本中將 is_playoffs 變數設為 True,全面啟動防守下修與主場加成。

發佈留言