別人恐懼我進場:安東尼·賣利亞 的反向市場致勝術
前言
在金融市場中,「反向思考(Contrary Thinking)」是一種歷久不衰的智慧策略。它源於一個簡單但深刻的觀察:當大多數人極度樂觀時,市場往往已經過熱;當群眾陷入恐懼時,反而可能是低接良機。然而,反向操作長期以來多停留在諺語層次,缺乏具體、可量化的行動標準,導致實務上難以執行。
安東尼‧賈利亞(Anthony M. Gallea)與威廉‧巴特隆(William Patalon III)於《Contrarian Investing》一書中,首度將反向操作具體化為可執行的量化選股策略。賈利亞是所羅門美邦公司(Salomon Smith Barney)資深投資組合管理董事,帶領逾14人團隊、管理超過六億美元資產;巴特隆則為資深商業記者,曾四度獲得紐約州美聯社頒發之商業報導獎。兩人以豐富的實務與觀察經驗,構建出一套兼具理論深度與實用性的選股方法。
策略邏輯
本策略採用多層次篩選邏輯,結合技術面、籌碼面與基本面條件,從中挑選出具備中期成長潛力的股票。首先,在技術面上,篩選最近一年內收盤價低於一年內最高價的 50% 的股票,作為初步具備反轉動能潛力的候選名單。
接著,在籌碼面條件中,若董事或經理人在最近六個月內持股比例增加至少 5%,或持股比例已達 10% 以上,則視為內部人具備明確信心的標的,亦納入考量;此外,投信若出現明顯加碼行為,也具備相同效力。若籌碼面條件滿足,則進一步檢視該公司是否具備基本面優勢,要求其符合以下四項條件中的至少兩項:
市盈率(PE)低於市場同行平均市盈率的 85%
價格 / 自由現金流量比 < 1 倍
價格 / 每股淨值比 < 0.8 倍
價格 / 每股營收比 < 1 倍
選出標的股票之後, 以等權重的方式去分配總體資金,以達到分散風險的效果,不爾會將資金過度集中於單一股票中。
抓取 TEJ 資料欄位
資料代碼 close_d adjfac fld005 qfii_pct fd_pct ri 科目 收盤價 調整係數 董監持股% 外資持股率 投信持股率 常續性利益 資料代碼 shares per cscfo cscfi r307 r19 open_d 科目 流通在外股數 本益比 營運現金流 投資現金流 每股淨值 近12月每股營收(元) 開盤價
Python 資料整理程式碼展示
import pandas as pd import numpy as np import tejapi import os import matplotlib.pyplot as plt import datetime plt.rcParams['font.family'] = 'Arial' os.environ['TEJAPI_BASE'] = “your base" os.environ['TEJAPI_KEY'] = "your tej api 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 = '2015-01-01' end_date = '2025-06-30' pool = get_universe(start = start_date, end = end_date, mkt_bd_e = ['TSE', 'OTC', 'TIB'], # 已上市之股票 stktp_e = 'Common Stock') # 普通股 columns = ['Industry', 'close_d', 'adjfac', 'fld005', 'qfii_pct' , 'fd_pct' , 'ri' , 'shares','per' , 'cscfo' , 'cscfi' , 'r307' , 'r19' , 'open_d', 'pbr', 'psr_tej'] start_dt = pd.Timestamp(start_date, tz = 'UTC') end_dt = pd.Timestamp(end_date, tz = "UTC") data = TejToolAPI.get_history_data(start = start_dt, end = end_dt, ticker = pool, columns = columns, transfer_to_chinese = True) data = data.sort_values('日期') # 依股票代碼進行時間序列運算 data['one_year_max_price'] = data.groupby('股票代碼')['收盤價'] \ .transform(lambda x: x.rolling(window=252, min_periods=1).max()) data['one_year_price'] = data.groupby('股票代碼')['收盤價'] \ .transform(lambda x: x.shift(252)) data['insider_holding_change_6m'] = data.groupby('股票代碼')['董監持股%'] \ .transform(lambda x: x - x.shift(126)) data['foreign_holding_change_6m'] = data.groupby('股票代碼')['外資持股率'] \ .transform(lambda x: x - x.shift(126)) data['fund_holding_change_6m'] = data.groupby('股票代碼')['投信持股率'] \ .transform(lambda x: x - x.shift(126)) # 自由現金流與估值比率 data['FCF'] = data['營運產生現金流量_Q'] - data['投資產生現金流量_Q'] data['Price_FCF'] = data['收盤價'] / data['FCF'] # 注意:這裡假設 '股價淨值比' 是 PBR,如有誤請調整 data['Price_pbr'] = data['收盤價'] / data['股價淨值比'] # 價格營收比 data['Price_Rev'] = data['收盤價'] / data['股價營收比_TEJ'] # 依產業計算平均本益比 data['industry_pe_avg'] = data.groupby('主產業別_中文')['本益比'] \ .transform('mean') data_use = data.copy()
Python 選股邏輯以及回測架構
def compute_growth_strategy(date, data): df = data[data['日期'] == pd.to_datetime(date)].reset_index(drop=True) # 技術面條件:收盤價 > 一年內最高價 50% tech_pass = set(df[df['收盤價'] < 0.5 * df['one_year_max_price']]['股票代碼']) # 董事經理人條件 df['insider_6m_ago'] = df.groupby('股票代碼')['董監持股%'].shift(126) df['insider_growth'] = df['董監持股%'] - df['insider_6m_ago'] insider_pass = set(df[(df['insider_growth'] >= 5) | (df['董監持股%'] >= 10)]['股票代碼']) # 投信條件(附加條件) df['fund_6m_ago'] = df.groupby('股票代碼')['投信持股率'].shift(126) fund_pass = set(df[df['投信持股率'] >= df['fund_6m_ago']]['股票代碼']) # 基本面條件(如果內部人或投信有持股,才檢查基本面) df['cond_A'] = df['本益比'] < 0.85 * df['industry_pe_avg'] df['cond_B'] = df['Price_FCF'] < 1 df['cond_C'] = df['Price_pbr'] < 0.8 df['cond_D'] = df['Price_Rev'] < 1 df['fundamental_score'] = df[['cond_A', 'cond_B', 'cond_C', 'cond_D']].sum(axis=1) df['fundamental_pass'] = df['fundamental_score'] >= 2 # 只對「內部人 or 投信」有動作的股票檢查基本面 trigger_set = insider_pass | fund_pass fundamental_checked_set = set(df[df['股票代碼'].isin(trigger_set) & df['fundamental_pass']]['股票代碼']) # 最終篩選:股價增長 + (內部人或投信) + 基本面 final_selection = tech_pass & trigger_set & fundamental_checked_set print(f"下單日期:{date}, 股價年增長: {len(tech_pass)}, 董監條件: {len(insider_pass)}, 投信條件: {len(fund_pass)}, 觸發基本面: {len(trigger_set)}, 基本面通過: {len(fundamental_checked_set)}, 最終選股: {len(final_selection)}") return final_selection 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'結束匯入回測資料')
TQuant Lab 回測邏輯程式碼展示
def initialize(context, re = 120): 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 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)) 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_growth_strategy(date = backtest_date, data = data_use) 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: {round(context.account.leverage, 2)}') for i in context.order_tickers: order_target_percent(symbol(i), 1 / len(context.order_tickers)) context.i += 1 def analyze(context, perf): print(perf.columns) 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['benchmark_volatility'], label='Benchmark Volatility') axes1[1].plot(perf.index, perf['algo_volatility'], label='Strategy Volatility') axes1[1].set_title("Voloatility") axes1[1].legend() plt.tight_layout() plt.show() results = run_algorithm( start = pd.Timestamp('2018-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)
績效指標 / 策略 大盤(Benchmark) 本投資策略 年化報酬率 13.91% 14.53% 累積報酬率 156.29% 166.60% 年化波動度 17.54% 23.93% 夏普值 0.83 0.69 卡瑪比率 0.51 0.33 期間最大回撤 -27.37% -43.47%
從回測結果來看,本投資策略在 年化報酬率(14.53%)與累積報酬率(166.6%) 上略優於大盤(13.91% / 156.29%),但整體風險調整後的表現並不理想。策略的 年化波動度高達 23.93%,遠高於大盤的 17.54%,導致 夏普值僅為 0.69,低於大盤的 0.83,代表單位風險所帶來的報酬反而較差。此外,最大回撤高達 -43.47%,相較於大盤的 -27.37%,顯示該策略在市場波動時缺乏足夠的防禦能力。
策略的 卡瑪比率為 0.33,也低於大盤的 0.51,意味著其承受每一單位最大損失所換得的報酬相對偏低,這點在圖中「Excess return」長期為負的部分亦可見端倪,特別是在 2021~2024 年間策略表現明顯落後。
完整程式碼連結
歡迎投資朋友參考,之後也會持續介紹使用 TEJ 資料庫來建構各式指標,並回測指標績效,所以歡迎對各種交易回測有興趣的讀者,選購 TQuant Lab 的相關方案,用高品質的資料庫,建構出適合自己的交易策略。
溫馨提醒,本次分析僅供參考,不代表任何商品或投資上的建議。
【TQuant Lab 回測系統】解決你的量化金融痛點
全方位提供交易回測所需工具