FinLab

00919 高股息 ETF 復刻與優化:玩轉精選高息策略

February 16, 2026

封面圖片 封面圖片

前言

市面上的高股息 ETF 百花齊放,這次要來復刻 00919 **群益台灣精選高息 ETF,**如果 0056 是台股存股族的啟蒙,00919  就像下一代改裝版。它主打「精準高息、精準卡位、精準領息」三大賣點,看似簡單,其實暗藏許多量化細節。本文帶你:

  1. 完整拆解官方邏輯,一步步復刻 00919 的選股流程。
  2. 用量化工具驗證:驗證相關性和重疊率。
  3. 再往前優化:刪掉雜訊因子、加入高效指標,打造報酬更高、回撤更低的 00919 優化版

一、00919 三大「精準」拆解

查看基金介紹發現有幾個特色

  • 精準高息:用「實際宣告」取代「預估數字」

00919 選擇鎖定「已公告現金股利」的企業,從確定的金額計算真實殖利率,進一步提升股息來源的可靠度與穩定性。


  • 精準卡位:提前布局的策略優勢

00919 採雙階段審核機制,五月、十二月雙審核,透過這種快與早並重的選股機制,達到真正「買在除息前」與「走在市場前」的投資節奏。


  • 精準領息:每一分股息都不浪費

選股時機對應企業除息時程,投資人持有期間能真正參與除息、獲取現金配息,讓每一分錢都落袋為安

名詞解釋 – 殖利率:每股現金股利 ÷ 股價。殖利率越高,代表用相對便宜的股價就能拿到較高現金回報。

二、復刻 00919:研究流程

  1. 資料擷取
  2. 邏輯拆解
    • 採樣母體 → 流動性 / 財務健全性 → 股利資訊 → 排序 → 替換規則。

三、復刻 00919 的關鍵步驟

  1. 初始採樣母體

    • 臺灣上市與上櫃普通股股票為基礎。

    • 選取發行市值前 300 大股票。

  2. 基本條件篩選

    • 日平均成交金額需高於 8,000 萬元。
    • 近四季稅後股東權益報酬率 (ROE) 皆為正數。
  3. 股利資訊篩選(五月定審限定)

    • 排除董事會尚未決定股利金額的公司。
    • 排除已除息且於審核生效日前已完成發放的股票。
  4. 排序並選取成分股

    • 五月定審:依近四季股利率排序,選出前 30 檔。
    • 十二月定審:依據預估股利率排序,選出前 30 檔。
顯示程式碼
def calculate_ranks(date, is_may_review=True):
    """計算特定審核日期的股票排名"""
    nearest_date = get_nearest_past_trading_date(date, all_trading_dates)
    if nearest_date is None:
        return pd.Series(dtype=float)  # 空序列
        
    if is_may_review:  # 5月定審
        # 計算排名 - 5月定審使用股利率
        score = (市值.rank(axis=1, pct=True) + 股利率.rank(axis=1, pct=True) + yield_ratio.rank(axis=1, pct=True))[conds & (board_cash_dividend > 0)]
    else:  # 12月定審
        # 計算排名 - 12月定審使用預估股利率
        score = (市值.rank(axis=1, pct=True) + 預估股利率.rank(axis=1, pct=True) + yield_ratio.rank(axis=1, pct=True))[conds]
        
    if nearest_date not in score.index:
        return pd.Series(dtype=float)  # 空序列
        
    return score.loc[nearest_date].dropna().rank(ascending=False, method='min')
  1. 成分股替換規則
    • 排名前 15 名直接納入成分股。
    • 既有成分股若跌出 46 名以外則剔除。
    • 排名 16 至 45 名的股票列為候補,優先保留既有成分股。
    • 十二月定審單次最多更替 8 檔股票。
顯示程式碼
# 建立空的結果DataFrame
position = pd.DataFrame(False, index=effective_dates, columns=close.columns)
 
# 處理所有調倉日期和排名數據
all_ranks = {}
valid_dates = []
 
for date in effective_dates:
    try:
        is_may_review = (date.month == 5)
        ranks = calculate_ranks(date, is_may_review)
        if not ranks.empty:
            all_ranks[date] = ranks
            valid_dates.append(date)
    except Exception as e:
        print(f"處理 {date} 時發生錯誤: {e}")
 
valid_dates = sorted(valid_dates)  # 確保日期順序
 
if not valid_dates:
    print("沒有有效的調倉日期,無法進行模擬")
else:
# 使用 Pandas 向量化處理成分股替換
    prev_components = None
    target_component_count = 30 # 設定目標成分股數量
 
    for i, date in enumerate(valid_dates):
                ranks = all_ranks[date]
 
                # 1. 排名在第15名以內者納入成分股
                top_15 = set(ranks[ranks <= 15].index)
 
                # 2. 排名16至45名為候補名單
                candidates = set(ranks[(ranks > 15) & (ranks <= 45)].index)
 
                # 暫定的成分股 (先加入前15名)
                current_components_tentative = set(top_15)
 
                # 如果不是第一次調倉
                if prev_components is not None:
                        # 加入排名16-45之間的既有成分股
                        existing_in_candidates = prev_components.intersection(candidates)
                        current_components_tentative.update(existing_in_candidates)
 
                        # 對於12月定審,額外限制最多替換8檔
                        if date.month == 12:
                                # 計算基於 Top15 + 既有候補 所得的新增股票
                                added = current_components_tentative - prev_components
 
                                # 如果新增超過8檔,需要減少替換數量
                                if len(added) > 8:
                                        # 取出新增的股票並按排名排序 (rank越小越好)
                                        added_with_rank = pd.Series({stock: ranks.get(stock, float('inf')) for stock in added})
                                        # 只保留排名最好的前8名新增的股票
                                        to_keep = set(added_with_rank.sort_values().index[:8])
                                        to_remove_due_to_limit = added - to_keep
 
                                        # 從暫定名單中移除因超過8檔限制而被剔除的股票
                                        current_components_tentative = current_components_tentative - to_remove_due_to_limit
 
                # --- 補滿至目標數量 ---
                num_needed = target_component_count - len(current_components_tentative)
                if num_needed > 0:
                        # 找出所有排名16-45,但尚未被選入的股票
                        remaining_candidates = candidates - current_components_tentative
                        if remaining_candidates: # 確保還有候選股可補
                                # 依排名排序這些候選股
                                remaining_candidates_with_rank = pd.Series({stock: ranks.get(stock, float('inf')) for stock in remaining_candidates})
                                sorted_remaining_candidates = remaining_candidates_with_rank.sort_values().index
 
                                # 選取排名最好的 num_needed 檔來補滿
                                stocks_to_add = set(sorted_remaining_candidates[:num_needed])
                                current_components_tentative.update(stocks_to_add)
                # --- 補滿邏輯結束 ---
 
                # 最終確認的成分股
                current_components = current_components_tentative
 
                # 設定成分股
                # 確保只設定存在的欄位
                valid_cols = [col for col in current_components if col in position.columns]
                position.loc[date, valid_cols] = True
 
                # 更新前一期成分股
                prev_components = current_components
 
    # 5. 前向填充空值,確保非調倉日也有持股
    position = position.loc[valid_dates]
  1. 生效日期計算
    • 審核基準日後第 5 個交易日正式生效。
顯示程式碼
def calculate_review_dates(trading_dates: pd.DatetimeIndex):
    """
    計算台灣高股息指數的審核與調倉日期
    
    台灣高股息指數規則:
    - 5月: 第17個交易日為審核基準日,審核資料截至5月第10個交易日
    - 12月: 第7個交易日為審核基準日,審核資料截至11月最後交易日
    - 生效日: 審核基準日後第5個交易日
    """
    review_dates_info = []
    start_year = trading_dates[0].year
    end_year = trading_dates[-1].year
 
    for year in range(start_year, end_year + 1):
        # 5月定審
        may_review_basis_day = get_trading_day_of_month(year, 5, 17, trading_dates)
        may_data_cutoff_day = get_trading_day_of_month(year, 5, 10, trading_dates)
        
        if may_review_basis_day and may_data_cutoff_day:
            target_effective_day_index = trading_dates.searchsorted(may_review_basis_day) + 5
            if target_effective_day_index < len(trading_dates):
                may_effective_day_target = trading_dates[target_effective_day_index]
                may_effective_day = get_nearest_future_trading_date(may_effective_day_target, trading_dates)
                if may_effective_day:
                    review_dates_info.append({
                        'year': year, 
                        'month': 5,
                        'cutoff_date': may_data_cutoff_day,
                        'basis_date': may_review_basis_day,
                        'effective_date': may_effective_day
                    })
 
        # 12月定審
        dec_review_basis_day = get_trading_day_of_month(year, 12, 7, trading_dates)
        nov_data_cutoff_day = get_last_trading_day_of_month(year, 11, trading_dates)
        
        if dec_review_basis_day and nov_data_cutoff_day:
            target_effective_day_index = trading_dates.searchsorted(dec_review_basis_day) + 5
            if target_effective_day_index < len(trading_dates):
                dec_effective_day_target = trading_dates[target_effective_day_index]
                dec_effective_day = get_nearest_future_trading_date(dec_effective_day_target, trading_dates)
                if dec_effective_day:
                    review_dates_info.append({
                        'year': year, 
                        'month': 12,
                        'cutoff_date': nov_data_cutoff_day,
                        'basis_date': dec_review_basis_day,
                        'effective_date': dec_effective_day
                    })
 
    # 按生效日期排序
    review_dates_info = sorted(review_dates_info, key=lambda x: x['effective_date'])
    # 移除無效日期
    review_dates_info = [info for info in review_dates_info if info['effective_date'] is not None]
    
    if not review_dates_info:
        raise ValueError("無法計算出任何有效的審核與生效日期,請檢查日期計算邏輯或資料範圍。")
    
    return review_dates_info

四、復刻結果

在經過一連串比對後,復刻而得的策略股池與原版 00919 報酬率曲線有高度相關。這意味著,我們的 復刻版 00919 確實能有效重現 00919 的選股結果。

相關性分析:

相關性分析 相關性分析

復刻 00919 報酬 :

復刻 00919 報酬 復刻 00919 報酬

長期持有 00919 報酬 :

長期持有 00919 報酬 長期持有 00919 報酬

我們的「復刻版 00919」與官方版本高相關,證明邏輯拆解合理。

想建立自己的策略?

用自然語言描述你的選股想法,AI 自動驗證、回測、給你答案

免費開始