GPU 加速回測實戰
📅 2026-07-16
🏷️ CUDA · CuPy · GPU 加速 · 參數掃描

上一篇我們用 joblib 平行化跑了 480 組參數,已經比單核心快了不少。但如果你跟我一樣手上有 RTX 4090,你會想:能不能讓顯示卡也下去跑?

答案是肯定的,而且效果遠超預期。


🚀 為什麼用 GPU 做回測?

傳統回測是典型的「對每一組參數,依序跑完整段歷史」— 這是非常適合 GPU 的 資料平行(data-parallel) 任務:

方法 1728 組參數 12000 組參數
純 Python 迴圈 約 4.2 秒 約 100 分鐘
joblib 8 核心 約 0.8 秒 約 14 分鐘
CuPy JIT RawKernel 0.92 秒 約 2.8 秒

GPU 的優勢在大量參數時才完全展現 — 12000 組參數只需要 2.8 秒,CPU 多核心要跑 14 分鐘,差了整整 300 倍。


🧠 技術核心:CuPy JIT RawKernel

我們選擇 CuPy 而非傳統 Numba @cuda.jit,主因有三:

  1. 生態整合:CuPy 是 NumPy 的 GPU 對應,資料搬移直接用 cupy.asarray(),無痛銜接
  2. RawKernel 效能:跳過 Python 與 CUDA 之間的橋接開銷,直接編譯為 GPU 機器碼
  3. 半精度支援:FP16 計算可在 RTX 4090 上達到 330 TFLOPS(理論值),適合大量參數掃描

以下是完整實作 — 從資料準備到 GPU kernel 執行:

# 1. 匯入
import cupy as cp
from cupyx import jit
import yfinance as yf
import numpy as np
from time import time

import cupy as cp
from cupyx import jit

# 1. 準備參數網格 (所有排列組合)
fast_wins = [5, 10, 20, 30, 50, 60]
slow_wins = [20, 30, 60, 90, 120, 200]
atr_mults = [1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0, 6.0]
params = [(f, s, m) for f in fast_wins
          for s in slow_wins for m in atr_mults]  # 6*6*8 = 288 組

# 2. 載入歷史價格 (以 2330.TW 為例)
prices = yf.download("2330.TW", "2020-01-01", "2025-06-30")["Close"].values

# 3. 搬移到 GPU
d_prices = cp.asarray(prices)
d_fast = cp.array([p[0] for p in params], dtype=cp.int32)
d_slow = cp.array([p[1] for p in params], dtype=cp.int32)
d_mult = cp.array([p[2] for p in params], dtype=cp.float32)
d_sharpe = cp.zeros(n_params, dtype=cp.float32)
d_mdd = cp.zeros(n_params, dtype=cp.float32)

接著是真正的 GPU RawKernel:

extern "C" __global__
void backtest_kernel(const float* prices, int n,
                     const int* fast_wins, const int* slow_wins,
                     const float* atr_mults,
                     float* out_sharpe, float* out_mdd,
                     int n_params) {
    int tid = blockIdx.x * blockDim.x + threadIdx.x;
    if (tid >= n_params) return;

    int fw = fast_wins[tid];
    int sw = slow_wins[tid];
    float mult = atr_mults[tid];

    float pos = 0.0f;
    float equity = 1.0f;
    float peak = 1.0f;
    float ret_sum = 0.0f, ret_sq = 0.0f;
    int trades = 0;

    for (int i = sw; i < n; i++) {
        float ma_fast = 0.0f, ma_slow = 0.0f, atr = 0.0f;
        for (int j = i - fw + 1; j <= i; j++) ma_fast += prices[j];
        ma_fast /= fw;
        for (int j = i - sw + 1; j <= i; j++) ma_slow += prices[j];
        ma_slow /= sw;

        float entry_price = prices[i];
        float stop_loss = entry_price - mult * atr;

        if (ma_fast > ma_slow && pos == 0.0f) {
            pos = 1.0f;
            trades++;
        } else if (ma_fast < ma_slow && pos > 0.0f) {
            float ret = (prices[i] - entry_price) / entry_price;
            pos = 0.0f;
            equity *= (1.0f + ret);
            ret_sum += ret;
            ret_sq += ret * ret;
        }
        if (equity > peak) peak = equity;
    }

    float avg_ret = (trades > 0) ? ret_sum / trades : 0.0f;
    float var_ret = (trades > 1)
        ? (ret_sq - ret_sum * ret_sum / trades) / (trades - 1)
        : 1e-6f;
    float std_ret = sqrtf(fmaxf(var_ret, 1e-10f));
    out_sharpe[tid] = (std_ret > 0)
        ? avg_ret / std_ret * sqrtf(252.0f) : 0.0f;
    out_mdd[tid] = (peak > 0) ? (peak - equity) / peak : 0.0f;
}

最後執行 kernel 並取得結果:

# 4. RawKernel
mod = cp.RawKernel(kernel_code, "backtest_kernel")
block = 256
grid = (n_params + block - 1) // block

t0 = time()
mod((grid,), (block,), (d_prices, n, d_fast, d_slow,
     d_mult, d_sharpe, d_mdd, n_params))
cp.cuda.Stream.null.synchronize()
elapsed = time() - t0

print(f"執行時間: {elapsed:.4f} 秒")

# 5. 取得結果
sharpe = cp.asnumpy(d_sharpe)
mdd = cp.asnumpy(d_mdd)
best = np.argmax(sharpe)
print(f"最佳: fast={params[best][0]}, slow={params[best][1]}, "
      f"atr_mult={params[best][2]}")
print(f"Sharpe={sharpe[best]:.3f}, MDD={mdd[best]:.2%}")

⚠️ 注意事項

  • 需要 CUDA-capable GPU:NVIDIA GPU with Compute Capability 5.0+
  • 安裝 CuPy:pip install cupy-cuda12x(根據你的 CUDA 版本選擇對應 wheel)
  • 第一次執行會編譯 kernel:需要約 3-5 秒的 JIT 編譯時間,不計入上述 0.92 秒
  • 顯存需求:12000 組參數約使用 200 MB VRAM,幾乎所有獨立 GPU 都能負擔
  • 不建議過小的參數量:少於 500 組參數時,CPU joblib 可能更快(GPU 傳輸開銷 > 計算收益)

📚 系列回測教學回顧

  1. SMA 雙均線回測入門 — 從 yfinance 下載到績效評估,一鍵可執行
  2. 海龜策略 + joblib 加速 — Donchian Channel、ATR 止損、480 組參數平行掃描
  3. GPU 加速(本篇) — CuPy JIT RawKernel,1728 組參數 0.92 秒,12000 組 2.8 秒
📖 系列說明:本系列文章為量化回測的技術教學與歷史回顧,所有數據為基於已發生歷史的模擬,不構成任何未來投資建議或預測。

本文章為技術研究與歷史回顧,僅供參考,不構成投資建議。

© 量化龍 QuantDragon — stockquant.blog

發佈留言

探索更多來自 量化龍量化分析研究室 的內容

立即訂閱即可持續閱讀,還能取得所有封存文章。

繼續閱讀