因子挖掘:基於遺傳演算法的多因子選股策略

本文將詳細分析一個基於遺傳演算法(GA)的因子挖掘系統,該系統整合了價量資料和財報資料,並使用夏普比率作為適應度函數。

一、系統架構概述

本系統採用遺傳演算法進行因子挖掘,主要包含以下核心組件:

  • GA選股器(GAStockSelection):負責整個遺傳演算法的流程控制,包括族群初始化、選擇、交配和突變等操作
  • 因子池優化機制:管理因子庫,包括因子的添加、刪除和權重調整
  • 適應度評估系統:計算每個個體的適應度值,使用夏普比率作為主要評估指標
  • 遺傳操作實現:包含選擇、交配和突變等遺傳操作的核心實現

二、資料處理與特徵工程

1. 資料來源

系統整合了兩大類資料:

  • 價量資料
    • 價格資料:收盤價、開盤價、最高價、最低價
    • 成交量資料:日成交量、成交金額
    • 技術指標:RSI、MACD、KD、布林通道等
    • 市場情緒指標:融資融券、外資持股等
  • 財報資料
    • 財務比率:ROE、ROA、負債比率等
    • 成長性指標:營收成長率、EPS成長率等
    • 營運效率:存貨週轉率、應收帳款週轉率等
    • 現金流量指標:營業現金流量、自由現金流量等

2. 資料預處理

在進行因子挖掘前,系統會進行以下預處理:

  • 異常值檢測與處理
    • 使用3倍標準差法識別異常值
    • 使用IQR(四分位距)方法處理極端值
    • 對異常值進行縮尾處理(Winsorization)
  • 標準化處理
    • Z-score標準化:適用於正態分佈的資料
    • Min-Max標準化:適用於有界資料
    • Robust標準化:適用於有異常值的資料
  • 特徵選擇
    • 使用相關係數矩陣去除高度相關的特徵

三、遺傳演算法實現

1. 染色體設計

染色體採用二進制編碼,每個基因代表一個因子的選擇狀態:

  • 1:選擇該因子,表示該因子被納入投資組合
  • 0:不選擇該因子,表示該因子被排除
染色體長度等於因子庫中的因子數量,例如如果有100個因子,則染色體長度為100位元。

2. 適應度函數

使用夏普比率作為適應度函數,計算公式如下:


# 適應度計算核心邏輯
def calculate_sharpe_ratio(returns, risk_free_rate=0.02):
    """計算給定收益率序列的年化夏普比率"""
    # 計算年化收益率 (假設一年252個交易日)
    annual_return = np.mean(returns) * 252
    
    # 計算年化波動率
    annual_volatility = np.std(returns) * np.sqrt(252)
    
    # 檢查波動率是否為零或非常接近零,避免除以零錯誤
    if annual_volatility < 1e-8:
        # 如果波動率為零,通常意味著沒有風險,夏普比率無意義或可視為零
        return 0.0 
    
    # 計算夏普比率
    sharpe_ratio = (annual_return - risk_free_rate) / annual_volatility
    return sharpe_ratio

# 投資組合收益計算範例 (假設 returns 是因子收益率, chromosome 是因子權重)
# portfolio_returns = np.dot(returns, chromosome) 

# 計算適應度 (夏普比率)
# fitness = calculate_sharpe_ratio(portfolio_returns)
                    
夏普比率衡量的是每單位風險所獲得的超額收益,數值越高表示風險調整後的收益越好。

3. 遺傳操作

3.1 選擇機制

採用輪盤式選擇(Roulette Wheel Selection):

  • 根據適應度值計算選擇機率:
    
    # 計算選擇機率
    def calculate_selection_probability(fitness_values):
        """根據適應度值計算輪盤式選擇的機率"""
        # 處理負適應度值
        min_fitness = min(fitness_values)
        if min_fitness < 0:
            # 使用線性變換將所有適應度值轉為正值 (加上一個小數避免全為0)
            fitness_values = [f - min_fitness + 1e-6 for f in fitness_values] 
        
        # 計算總適應度
        total_fitness = sum(fitness_values)
        
        # 檢查總適應度是否為零或非常接近零
        if total_fitness < 1e-8:
            # 如果所有適應度都接近零,則賦予相等機率
            num_individuals = len(fitness_values)
            # 避免除以零
            if num_individuals == 0:
                return []
            return [1.0 / num_individuals] * num_individuals
            
        # 計算選擇機率
        probabilities = [f / total_fitness for f in fitness_values]
        return probabilities
                                
  • 處理負適應度值的情況:使用線性變換將所有適應度值轉為正值
  • 確保選擇的公平性:使用累積機率進行選擇,避免選擇偏差

3.2 交配操作

實現單點交配策略:


# 單點交配實現
def single_point_crossover(parent1, parent2):
    """執行單點交配操作,生成兩個子代"""
    # 確保父代長度相同且不為空
    if len(parent1) != len(parent2) or len(parent1) == 0:
        raise ValueError("父代染色體長度必須相同且不為零")
    
    # 隨機選擇交叉點 (不包含第一個和最後一個位置)
    # 如果染色體長度只有1,則無法進行交配,直接回傳原父代
    if len(parent1) < 2:
        return parent1.copy(), parent2.copy()
        
    crossover_point = np.random.randint(1, len(parent1))
    
    # 產生子代
    # 子代1 = 父代1的前半部分 + 父代2的後半部分
    child1 = np.concatenate([parent1[:crossover_point], parent2[crossover_point:]])
    # 子代2 = 父代2的前半部分 + 父代1的後半部分
    child2 = np.concatenate([parent2[:crossover_point], parent1[crossover_point:]])
    
    return child1, child2
                    
  • 隨機選擇交叉點:在染色體長度範圍內隨機選擇一個位置
  • 交換父代染色體的部分基因:在交叉點處交換兩個父代的基因
  • 產生兩個子代:每個交配操作產生兩個新的子代

3.3 突變操作

以特定機率進行基因突變:


# 突變操作實現
def mutation(chromosome, mutation_rate=0.02):
    """對染色體的每個基因以指定機率進行突變 (0變1, 1變0)"""
    # 檢查突變率是否有效
    if not 0 <= mutation_rate <= 1:
        raise ValueError("突變率必須介於 0 和 1 之間")
        
    # 複製染色體以避免修改原始個體
    mutated_chromosome = chromosome.copy()
    
    # 對每個基因進行突變檢查
    for i in range(len(mutated_chromosome)):
        if np.random.random() < mutation_rate:
            # 翻轉基因值 (0 變成 1, 1 變成 0)
            mutated_chromosome[i] = 1 - mutated_chromosome[i]
            
    return mutated_chromosome
                    
  • 突變率可調(預設0.02):控制基因突變的頻率
  • 包含有效性檢查:確保突變後的染色體仍然有效
  • 確保突變後仍符合約束條件:檢查最小和最大選取因子數的限制

四、優化策略

1. 早停機制

引入早停機制避免過度擬合:


# 早停機制實現
def early_stopping(best_fitness_history, patience=10, min_delta=0.001):
    """檢查是否滿足早停條件"
    
    Args:
        best_fitness_history (list): 記錄每代最佳適應度值的列表
        patience (int): 連續多少代無顯著改善時觸發早停
        min_delta (float): 判定為顯著改善的最小閾值
        
    Returns:
        bool: True表示滿足早停條件,False則否
    """
    # 如果記錄的歷史不足 patience 期,則不停止
    if len(best_fitness_history) < patience:
        return False
        
    # 獲取最近 patience 期的最佳適應度值
    recent_history = best_fitness_history[-patience:]
    
    # 檢查最近 patience 期內是否有顯著改善
    # 計算這段期間的最大值和最小值之差
    improvement = max(recent_history) - min(recent_history)
    
    # 如果改善程度小於設定的最小閾值,則觸發早停
    return improvement < min_delta
                    
  • 設置patience參數:控制連續多少代無改善時停止
  • 監控適應度改善情況:記錄每代的最佳適應度值
  • 當連續多代無改善時停止:避免無謂的計算

2. 約束條件

系統實現了多種約束條件:


# 約束條件檢查
def check_constraints(chromosome, min_pick=3, max_pick=10, forced_indices=None):
    """檢查染色體是否滿足設定的約束條件"
    
    Args:
        chromosome (np.array): 需要檢查的染色體 (二進制)
        min_pick (int): 允許選擇的最少因子數量
        max_pick (int): 允許選擇的最大因子數量
        forced_indices (list, optional): 必須選擇的因子索引列表. Defaults to None.
        
    Returns:
        bool: True表示滿足所有約束條件,False則否
    """
    # 計算選取的因子數量 (染色體中 1 的數量)
    num_picked = np.sum(chromosome)
    
    # 檢查是否在最小和最大選取數量之間
    if not min_pick <= num_picked <= max_pick:
        return False
        
    # 檢查是否強制選擇了指定的因子
    if forced_indices is not None:
        for index in forced_indices:
            # 確保索引有效
            if index < 0 or index >= len(chromosome):
                print(f"警告:強制選擇的索引 {index} 超出範圍")
                continue # 或者可以返回 False 或拋出錯誤
            # 檢查對應位置的基因是否為 1
            if chromosome[index] != 1:
                return False
                
    # 所有檢查通過
    return True
                    
  • 最小選取因子數(min_pick):確保投資組合的分散性
  • 最大選取因子數(max_pick):控制投資組合的複雜度
  • 強制選擇特定因子(forced_indices):確保重要因子被納入

五、效能優化

1. 向量化運算

使用numpy進行高效運算:


# 向量化適應度計算
def batch_fitness_calculation(population, returns):
    """使用向量化操作批量計算族群中每個個體的適應度 (夏普比率)"""
    # population: (N, M) N個個體, M個因子
    # returns: (T, M) T個時間點, M個因子
    
    # 計算每個個體在每個時間點的組合收益率
    # (T, M) x (M, N) -> (T, N)
    portfolio_returns_per_individual = np.dot(returns, population.T)
    
    # 對每個個體 (每一列) 計算夏普比率
    # 注意:需要確保 calculate_sharpe_ratio 函數在此處可用
    sharpe_ratios = np.array([calculate_sharpe_ratio(ind_returns) 
                              for ind_returns in portfolio_returns_per_individual.T])
                              
    return sharpe_ratios
                    
  • 矩陣運算替代循環:使用numpy的矩陣運算提高效率
  • 批量處理適應度計算:一次計算整個族群的適應度
  • 優化記憶體使用:使用稀疏矩陣存儲大規模資料

2. 並行計算

實現族群評估的並行化:


# 並行適應度計算
# 注意:需要導入 multiprocessing 模組
from multiprocessing import Pool
import numpy as np # 假設已導入 numpy

def parallel_fitness_calculation(population, returns, n_processes=4):
    """使用多進程並行計算族群的適應度"""
    # population: (N, M) N個個體, M個因子
    # returns: (T, M) T個時間點, M個因子
    
    # 確保進程數量有效
    if n_processes <= 0:
        n_processes = 1 # 至少使用一個進程
    
    try:
        # 將族群分割成大致相等的塊,數量等於進程數
        # 使用 array_split 以應對無法均分的情況
        chunks = np.array_split(population, n_processes)
        
        # 創建進程池
        with Pool(processes=n_processes) as pool:
            # 為每個塊準備參數列表
            # 注意:假設 batch_fitness_calculation 函數已定義且可用
            args_list = [(chunk, returns) for chunk in chunks]
            
            # 使用 starmap 並行執行 batch_fitness_calculation
            # starmap 會將參數列表中的每個元組解包後傳遞給目標函數
            results_list = pool.starmap(batch_fitness_calculation, args_list)
            
        # 合併來自不同進程的結果
        all_sharpe_ratios = np.concatenate(results_list)
        
        return all_sharpe_ratios
        
    except Exception as e:
        print(f"並行計算過程中發生錯誤: {e}")
        # 可以選擇返回空列表、None 或重新拋出異常
        return np.array([]) 
                    
  • 多進程處理適應度計算:利用多核CPU提高計算效率
  • 優化計算資源利用:根據CPU核心數動態調整進程數
  • 提升運算效率:減少計算時間,提高系統響應速度

六、實作建議

1. 參數調優

建議的參數設置:

  • 族群大小:因子數量的2-3倍,確保足夠的遺傳多樣性
  • 突變率:1%-5%,平衡探索和開發
  • 早停參數:代數的10%-20%,避免過度擬合

2. 風險控制

建議加入的風險控制機制:

  • 最大回撤限制:控制投資組合的最大損失
  • 波動率控制:限制投資組合的波動範圍
  • 換手率限制:控制交易頻率,降低交易成本

七、未來改進方向

1. 多目標優化

可以考慮加入:

  • 收益-風險平衡:同時優化多個目標函數
  • 交易成本考慮:將交易成本納入適應度計算
  • 流動性限制:考慮市場衝擊成本

2. 動態調整機制

建議實現:

  • 自適應突變率:根據收斂情況動態調整突變率
  • 動態交叉策略:根據種群多樣性調整交叉方式
  • 局部搜索機制:在收斂後進行局部優化

八、研究成果展示

1. 挖掘出的優質因子範例

以下是系統挖掘出的部分高CAGR因子,按照CAGR值大小排序:

排名 因子表達式 CAGR 使用指標
1 signedpower(max(daily_EMA5, truediv(Neutralization(overnight_EMA5, Volume), Lower_shadow)), 20) 684.918 daily_EMA5, overnight_EMA5, Volume, Lower_shadow
2 truediv(signedpower(BPROE_Quantile, 30), delay(intraday_range, 3)) 555.596 BPROE_Quantile, intraday_range
3 signedpower(ts_product(BPROE_Quantile, 2), 20) 496.884 BPROE_Quantile
4 ts_min(signedpower(BPROE_Quantile, 60), 3) 435.778 BPROE_Quantile
5 ts_product(truediv(truediv(GapReturn, Lower_shadow), Lower_shadow), 3) 408.737 GapReturn, Lower_shadow

2. 因子分析

從上述挖掘結果中,我們可以觀察到以下幾個重要特點:

  • 價量因子的重要性:排名第一的因子結合了日內EMA、隔夜EMA和成交量等價量指標,價量因子還是有其重要性。
  • BPROE的預測力:多個高fitness因子都使用了BPROE指標,證實了基本面因子的重要性。
  • 技術指標組合:系統成功發掘出多個技術指標的有效組合,如GapReturn和Lower_shadow的比值關係。
  • 時序特徵:許多高fitness因子都包含時序運算(如ts_product、ts_min等),說明價格動量和趨勢特徵的重要性。

3. 實務應用建議

基於以上發現,在實際應用中建議:

  • 重視價量數據的收集和處理,特別是日內價格和成交量數據
  • 將基本面指標(如BPROE)與技術指標結合,以提升預測效果
  • 在因子設計時多考慮時序特徵,不要僅局限於橫截面特徵
  • 注意因子之間的相關性,避免過度依賴單一類型的指標
這些因子範例展示了遺傳演算法在因子挖掘中的強大能力,特別是在發現複雜的非線性關係方面。然而,在實際應用時仍需要考慮因子的穩定性、交易成本等實務因素。
總結:本系統通過遺傳演算法有效地在龐大的因子空間中搜索最優組合,結合了價量資料和財報資料,使用夏普比率作為適應度函數,實現了高效且穩健的因子挖掘過程。系統的模組化設計和優化策略使其具有良好的可擴展性和實用性。

九、回測結果分析

1. 績效指標總覽

樣本期間 CAGR(%) Sharpe Calmar MDD(%) 單利MDD(%) 樣本勝率(%) 周勝率(%) 月勝率(%) 年勝率(%) 盈虧比 總賺賠比 預期報酬(bps) 樣本數
樣本內 110.15 8.24 8.48 -12.98 -2.61 70.27 81.45 99.07 100.0 1.67 3.98 44.00 2223
樣本外 34.13 5.25 2.43 -14.06 -6.23 63.06 71.82 89.47 100.0 1.43 2.53 17.38 1527
全樣本 181.88 7.04 12.94 -14.06 -2.61 67.33 77.47 95.11 100.0 1.59 3.35 61.46 3750

2. 策略收益曲線

因子挖掘策略收益曲線

圖1:因子挖掘策略2010-2024年收益曲線(橙色線),紅色垂直線表示樣本內外分界點

3. 策略表現分析

3.1 收益性分析

  • 樣本內表現優異
    • 年化報酬率(CAGR)達到110.15%
    • 夏普比率高達8.24,顯示風險調整後收益表現極佳
    • Calmar比率8.48,說明相對於最大回撤的收益效率很高
  • 樣本外仍維持穩健
    • 雖然CAGR降至34.13%,但仍維持可觀的收益水平
    • 夏普比率5.25,顯示策略在樣本外期間仍保持良好的風險調整後收益
    • 收益曲線呈現穩定上升趨勢,體現策略的持續性

3.2 風險控制

  • 回撤控制得宜
    • 樣本內最大回撤僅-12.98%
    • 樣本外最大回撤-14.06%,與樣本內相近,顯示風險特徵的一致性

3.3 勝率與穩定性

  • 高勝率特徵
    • 樣本內勝率70.27%,樣本外仍維持63.06%的高勝率
    • 盈虧比分別為1.67和1.43,顯示單筆交易的獲利金額普遍大於虧損金額
    • 各時間週期(周、月、年)的勝率都保持在較高水平,體現策略的時間穩定性

4. 策略特點總結

  • 強健性:樣本外表現雖有所下降但仍維持優異水準,顯示策略具有良好的rubustness
  • 風險控制:最大回撤控制在15%以內,且單利回撤更小,顯示策略具有良好的風險管理能力
  • 穩定性:高勝率、理想盈虧比以及各時間週期的穩定表現,都支持策略的可持續性