德伍.卻斯的成長動能選股策略:價值與動能的交會點
前言
在成長型投資領域中,德伍.卻斯(Derwood S. Chase Jr.)無疑是一位極具代表性的傳奇人物。投資生涯橫跨近半世紀的他,擁有紮實的學術背景,畢業於維吉尼亞大學,並於1954年取得哈佛大學企業管理碩士學位。1958年創立大通投資顧問公司(Chase Investment Counsel Corporation),專注於管理公司退休基金、信託基金與個人退休計畫,並不涉足與銀行相關業務。在他的領導下,公司至2001年已管理超過14億美元資產,其旗下機構大型成長股組合於1990至2001年間創下年化報酬率16.38%的亮眼成績,顯著超越同期S&P 500指數及Russell 1000 Growth指數。
1997年,德伍.卻斯進一步推出大通成長基金(Chase Growth Fund),同樣繳出優異表現。截至2001年底,該基金4年平均年報酬率達9.88%,遠高於S&P 500的5.65%,並在743檔同類型基金中名列第六,獲得晨星(Morningstar)五顆星的最高評等。
德伍.卻斯自許為一位風險意識強烈的GARP(Growth at a Reasonable Price)投資者,他的選股哲學融合數量研究、技術分析與基本面分析,並強調分散投資的重要性,認為理想的投資組合應包含35至45檔個股。透過兼具成長與穩健的策略,德伍.卻斯為投資人建立了穿越市場周期的長期回報典範。
為何德伍.卻斯如此設定選股與買進基準?
德伍.卻斯採用的選股標準,結合了企業的規模、成長性、獲利能力、估值與動能表現。他偏好總市值高於平均的大型企業,以降低流動性與營運風險;選擇過去五年盈餘成長率高、ROE 高的公司,代表企業具備穩健成長與經營效率。他也設下估值上限(預估本益比不得超過成長率兩倍),確保不會為成長支付過高溢價。同時納入股價相對強弱度,反映市場資金動向。這些條件搭配低於平均的負債比率,共同構築出一套注重「合理成長、穩健財務、順勢操作」的選股策略。
德伍.卻斯的選股邏輯
根據德伍.卻斯的投資方法,他所採用的選股邏輯結合了企業的基本面強度、成長潛力、市場表現與財務穩健性。以下為其具體條件:
總市值≧市場平均值 → 聚焦於具有穩定性與資金流動性的中大型企業,降低流動性風險。
過去五年盈餘複合成長率(EPS CAGR)≧10% → 確保公司在中長期具備穩定的獲利成長性。
近四季股東權益報酬率(ROE)≧15% → 評估企業運用資本創造價值的效率,高 ROE 代表企業經營效能良好。
近一季負債比率<市場平均值 → 篩選財務結構穩健的公司,提升整體投資組合的抗壓能力。
預估本益比(Forward P/E)≦EPS CAGR × 2 → 以成長性合理估值原則(GARP)篩選價格未被高估的標的。
近六個月股價相對強弱度(6M RS)>1 → 動能強勁,近期獲市場資金青睞。
回測基本設定
回測期間:2016-01-01 至2025-05-27
再平衡天數:30 個交易日
比較基準Benchmark:IR0001 簡稱大盤
以下程式碼抓取財務資料時使用的是資料庫代碼,以下展示代碼對應的財務欄位
coid roi mktcap r505 r104 per close_d r316 股票代碼 日報酬率 公司市值 流動比率 常續ROE 本益比 收盤價 EPS 資料庫欄位代碼對應財務資料
資料整理程式碼展示
import pandas as pd import numpy as np import tejapi import os import json import matplotlib.pyplot as plt plt.rcParams['font.family'] = 'Arial' tej_key = 'your key' tejapi.ApiConfig.api_key = tej_key os.environ['TEJAPI_BASE'] = "https://api.tej.com.tw" os.environ['TEJAPI_KEY'] = tej_key from zipline.sources.TEJ_Api_Data import get_universe import TejToolAPI from zipline.data.run_ingest import simple_ingest from zipline.api import set_slippage, set_commission, set_benchmark, symbol, record, order_target_percent from zipline.finance import commission, slippage from zipline import run_algorithm start_date = '2010-01-01'; end_date = '2025-05-27' pool = get_universe(start = start_date, end = end_date, mkt_bd_e = ['TSE', 'OTC'], stktp_e = ['Common Stock-Foreign','Common Stock']) columns = ['coid', 'roi', 'mktcap', 'r505', 'r104', 'per', 'close_d', 'r316'] start_dt = pd.Timestamp(start_date, tz = 'UTC') end_dt = pd.Timestamp(end_date, tz = "UTC") data_use = TejToolAPI.get_history_data(start = start_dt, end = end_dt, ticker = pool + ['IR0001'], fin_type = ['Q', 'TTM'], columns = columns, transfer_to_chinese = False) data_use['mdate'] = pd.to_datetime(data_use['mdate']) data_use = data_use.sort_values('mdate') data_use['avg_market_cap'] = data_use.groupby('mdate')['Market_Cap_Dollars'].transform('mean') data_use['avg_debt_ratio'] = data_use.groupby('mdate')['Liabilities_Ratio_Q'].transform('mean') data_use['EPS_5y_ago'] = data_use.groupby('coid')['Net_Income_Per_Share_Q'].shift(252 * 5) data_use['EPS_CAGR_5Y'] = ((data_use['Net_Income_Per_Share_Q'] / data_use['EPS_5y_ago']) ** (1/5)) - 1 # 以 126 個交易日作為近 6 個月(可依台股調整為 120~130 天) data_use['ret_6m'] = data_use.groupby('coid')['Close'].transform(lambda x: x / x.shift(126) - 1) # 從 df 中抽出大盤(IR0001)的每日報酬率 market_ret = data_use[data_use['coid'] == 'IR0001'][['mdate', 'ret_6m']].rename(columns={'ret_6m': 'market_ret_6m'}) # 把 market_ret 併回所有資料(用日期對齊) data_use = data_use.merge(market_ret, on='mdate', how='left') data_use['RS_ratio_6m'] = data_use['ret_6m'] / data_use['market_ret_6m'] data_use['mdate_shifted'] = data_use.groupby('coid')['mdate'].shift(1) def compute_stock(date, data): df = data[data['mdate_shifted'] == pd.to_datetime(date)].reset_index(drop = True) set_1 = set(df[df['Market_Cap_Dollars'] >= df['avg_market_cap']]['coid']) set_2 = set(df[df['EPS_CAGR_5Y'] >= .1]['coid']) set_3 = set(df[df['Net_Income_Per_Share_TTM'] >= .15]['coid']) set_4 = set(df[df['Liabilities_Ratio_Q'] < df['avg_debt_ratio']]['coid']) set_5 = set(df[(df['PER_TWSE'] <= df['EPS_CAGR_5Y']*100 * 2.0)]['coid']) set_6 = set(df[(df['RS_ratio_6m'] > 1)]['coid']) tickers = list(set_1 & set_2 & set_3 & set_4 & set_6 & set_5) print(f'set1:{len(set_1)}, set2:{len(set_2)},set3:{len(set_3)},set4:{len(set_4)},set5:{len(set_5)},set6:{len(set_6)}') sets = [len(set_1), len(set_2),len(set_3), len(set_4),len(set_5), len(set_6)] return tickers, sets
回測程式碼展示
pools = pool + ['IR0001'] start_ingest = start_date.replace('-', '') end_ingest = end_date.replace('-', '') print(f'開始匯入回測資料') simple_ingest(name = 'tquant' , tickers = pools , start_date = start_ingest , end_date = end_ingest) print(f'結束匯入回測資料') def initialize(context, re = 30): set_slippage(slippage.VolumeShareSlippage(volume_limit=1, price_impact=0.01)) set_commission(commission.Custom_TW_Commission()) set_benchmark(symbol('IR0001')) context.i = 0 context.state = False context.order_tickers = [] context.last_tickers = [] context.rebalance = re context.set1 = 0 context.set2 = 0 context.set3 = 0 context.set4 = 0 context.set5 = 0 context.set = 0 context.dic = {} def handle_data_1(context, data): # 避免前視偏誤,在篩選股票下一交易日下單 if context.state == True: for i in context.last_tickers: if i not in context.order_tickers: order_target_percent(symbol(i), 0) for i in context.order_tickers: order_target_percent(symbol(i), 1.0 / len(context.order_tickers)) context.dic[i] = data.current(symbol(i), 'price') record(p = context.dic) context.dic = {} print(f"下單日期:{data.current_dt.date()}, 擇股股票數量:{len(context.order_tickers)}, Leverage: {context.account.leverage}") context.last_tickers = context.order_tickers.copy() context.state = False backtest_date = data.current_dt.date() if context.i % context.rebalance == 0: context.state = True context.order_tickers = compute_stock(date = backtest_date, data = data_use)[0] context.set = compute_stock(date = backtest_date, data = data_use)[1] record(tickers = context.order_tickers) record(Leverage = context.account.leverage) if context.account.leverage > 1.2: print(f'{data.current_dt.date()}: Over Leverage, Leverage: {context.account.leverage}') for i in context.order_tickers: order_target_percent(symbol(i), 1 / len(context.order_tickers)) context.i += 1 lengths = [s for s in context.set] record( ticker_num = len(context.order_tickers), set1_len = lengths[0], set2_len = lengths[1], set3_len = lengths[2], set4_len = lengths[3], set5_len = lengths[4], set6_len = lengths[5] ) def analyze(context, perf): plt.style.use('ggplot') # 第一張圖:策略績效與報酬 fig1, axes1 = plt.subplots(nrows=2, ncols=1, figsize=(18, 10), sharex=False) axes1[0].plot(perf.index, perf['algorithm_period_return'], label='Strategy') axes1[0].plot(perf.index, perf['benchmark_period_return'], label='Benchmark') axes1[0].bar(perf.index, perf['algorithm_period_return'] - perf['benchmark_period_return'], label='Excess return', color='g', alpha=0.4) axes1[0].set_title("Backtest Results") axes1[0].legend() axes1[1].plot(perf.index, perf['returns'], label='Strategy') axes1[1].plot(perf.index, perf['benchmark_return'], label='Benchmark') axes1[1].set_title("Daily Returns") axes1[1].legend() plt.tight_layout() plt.show() # 第二張圖:選股結構與篩選條件 fig2, axes2 = plt.subplots(nrows=2, ncols=1, figsize=(18, 10), sharex=False) axes2[0].plot(perf.index, perf['ticker_num'], label='Ticker Number') axes2[0].set_title("Ticker Number") axes2[0].legend() axes2[1].plot(perf.index, perf['set1_len'], label='Set1: mktcap >= ave') axes2[1].plot(perf.index, perf['set2_len'], label='Set2: EPS CAGR >= 10%') axes2[1].plot(perf.index, perf['set3_len'], label='Set3: ROE >= 15%') axes2[1].plot(perf.index, perf['set4_len'], label='Set4: debt ratio < ave') axes2[1].plot(perf.index, perf['set5_len'], label='Set5: PER <= CAGR x2') axes2[1].plot(perf.index, perf['set6_len'], label='Set6: RS ratio > 1') axes2[1].set_title("Six Conditions Filtered Count") axes2[1].legend(loc='upper right') plt.tight_layout() plt.show() results = run_algorithm( start = pd.Timestamp('2016-01-01', tz = 'utc'), end = pd.Timestamp(end_date, tz = 'utc'), initialize = initialize, handle_data = handle_data_1, analyze = analyze, bundle = 'tquant', capital_base = 1e5)
績效圖表&分析
從上圖的第一張圖「累積報酬率圖表中」,我們可以看得出來策略在表現上是贏過大盤的,策略累積報酬率為 457.54% 相比於大盤只有 241.5%,展現出了良好的獲利性。我們可以從第一張圖中看出,每當大盤處於上升格局,策略報酬率容易表現得更好,也就是創造報酬綠差距的期間。但是當大盤在進行盤整或進入下跌期時,策略往往產生更大的跌幅,同時對比第二張圖「日報酬率的圖表中」,可以得出策略的波動率大部分期間是大於大盤的。這也可以從策略的 Beta 值得出,策略的 Beta 值為 1.27 但是 alpha 值僅僅只有 0.03,顯示出策略的良好獲利性其實來源於放大市場風險賺取的市場溢酬。顯示出策略的實用性應該根據市場狀態而有所調整,而不是一個可以在全期間使用的策略邏輯。
從上述這張圖來看,第一張圖顯示了策略在不同時期持有的股票個數,第二張圖顯示 了不同時期通過篩選標準的股票個數,也就是說若一隻股票同時滿足這六個篩選條件才會被選入被策略邏輯買進持有。可以看得出來,主導選股的條件集中在 set 6,也就是「近六個月股價相對強弱度(6M RS)>1」這個條件,而這個條件就是這個策略之所以被描述為動能的理由,我認為此條件也是使得策略展現高市場風險的主要原因。
另一方面,從持股數量來看,策略期間持股的數量至多不超過20支股票,這可能導致投資組合持股數量不足難以分散個股分險,造成整體績效容易受到單一個股影響,而承受較高的獨有風險。
從回測結果來看,策略在 2018、2022 與 2025 年皆經歷明顯回撤,表示市場處於下跌行情時策略會放大市場的跌幅。其中,2022 年報酬為負且最大回撤接近 -35%,為典型的系統性下跌;2025 年雖未結束年度,但已出現超過 -40% 的深度回撤,顯示市場波動劇烈,仍處於修正階段。而 2018 年則為中度回檔,跌幅約 -20%,屬於邊緣型熊市或大型修正。整體而言,策略雖表現亮眼,但在面對極端市場狀況時仍存在顯著回撤風險。
項目 德伍.卻斯投資策略 大盤 IR0001 累積報酬率 457.54% 241.54% 年化報酬率 20.84% 14.47% 年化波動度 27.79% 16.22% 夏普比率 0.82 0.92 卡瑪比率 0.49 0.53 最大回撤 - 42.61% - 27.37% Beta 1.27 0.99 Alpha 0.03 0 註:卡瑪比率計算方式為年化波動度除以期間最大回撤,用以衡量「報酬率對虧損」的比值,概念類似於風暴比,此指標的數值越高越好。
完整程式碼連結
歡迎投資朋友參考,之後也會持續介紹使用 TEJ 資料庫來建構各式指標,並回測指標績效,所以歡迎對各種交易回測有興趣的讀者,選購 TQuant Lab 的相關方案,用高品質的資料庫,建構出適合自己的交易策略。
溫馨提醒,本次分析僅供參考,不代表任何商品或投資上的建議。
【TQuant Lab 回測系統】解決你的量化金融痛點
全方位提供交易回測所需工具
留言 0