麥克.墨菲高科技股投資風險評估法則
本文重點摘要
- 文章難度:★★☆☆☆
- 高科技股選股策略。
- 透過 TQuant Lab 回測麥克.墨菲策略,觀察此策略因子績效。
前言
隨著高科技產業迅速發展,其股票也常成為市場焦點。然而,高科技股雖具備成長潛力,卻同時伴隨著極高的波動性與投資風險。投資人在追求高報酬的同時,若未妥善評估風險,容易面臨重大虧損。因此,如何有效衡量與控管高科技股的下跌風險,成為投資決策中不可忽視的一環。
如何計算因子 & 透過因子進行篩選
選股
在TQuant Lab中,我們使用電子工業當我們股票池再進行篩選,可以針對使用者需求調整自己需要的科技業類別。
計算因子,下跌風險值&偏離值
以下資料使用欄位皆取自於Tquant Lab資料集
A :每股營業額 : 近12月累計營收_千元/流通在外股數
B : 每股帳面價值 : 股東權益總計_Q / 流通在外股數
C : 過去3年平均稅前盈餘成長率 : 稅前淨利成長率,取序號1的Q欄位,然後用pivot轉為矩陣後直接rolling(12).mean
D : 過去4季每股盈餘 : 每股淨值_TTM
E : 下跌風險值 (DRV) = ( A + 1.5 * B + C * D * 1/3) / 3
- 如果 C 或是 D 為負數,則用0代替
偏離值 : ( 股價 - 下跌風險值 ) / 股價
偏離值與風險的關係 : 正數代表具有大跌風險,正數越大風險越大,小於0表示可以投資,負值越大越安全。
交易邏輯
- 交易頻率:每月 15 日進行交易,若當天非交易日,則移至下一個開市日。
- 取消所有未成交訂單,避免影響新交易。
- 清空本期未被選擇的持股,續抱本期集上一期皆被選擇的股票,節省手續費。
- 選出「偏離值最小的前 40 檔股票」 作為買進標的。
資金分配
- 每次進場資金 = 100% 總資金
- 每檔股票分配資金為等資金分配
載入套件
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 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 import pandas as pd from zipline.pipeline.data import Column, DataSet from zipline.pipeline.loaders.frame import DataFrameLoader from zipline.pipeline import Pipeline from zipline.pipeline.engine import SimplePipelineEngine from zipline.pipeline.domain import TW_EQUITIES from zipline.pipeline.factors import CustomFactor
取得股票池
在選擇回測的時間點時,選擇電子工業。在此步驟中,可以檢查股票池是否包含具有不喜歡特性的股票,並根據需求進行適當調整。
pool = get_universe(start = '2018-01-01', end = '2024-12-31', mkt_bd_e = ['TSE', 'OTC', 'TIB'], # 已上市之股票 stktp_e = 'Common Stock', # 普通股 main_ind_c = 'M2300 電子工業,OTC23 OTC 電子類') # general industry 可篩掉金融產業
取得資料&整理資料
透過 tejapi 獲取稅前淨利成長率,進而去計算上述數據C (過去3年平均稅前盈餘成長率)
data_r404 = tejapi.get('TWN/AINVFQ1', mdate = {'gte':'2015-01-01', 'lte': '2024-12-31'}, coid = pool ,opts = {'columns':['coid','mdate','r404' , 'no' , 'key3']}, chinese_column_name = True,paginate = True) data_r404 = data_r404[(data_r404['序號'] == '001') & (data_r404['期間別'] == 'Q')] pivot_data_r404 = data_r404.pivot(index='年/月', columns='公司', values='稅前淨利成長率') # 確保 index 是 datetime 格式 pivot_data_r404.index = pd.to_datetime(pivot_data_r404.index) pivot_data_r404 = pivot_data_r404.sort_index() # 滾動12筆平均 rolling_mean = pivot_data_r404.rolling(window=12, min_periods=12).mean() # 只保留每年 12 月 1 日 annual_rolling_mean = rolling_mean[rolling_mean.index.strftime('%m-%d') == '12-01'] # 刪除 2015 和 2016 年的 row(只保留 2017-12-01 起的) annual_rolling_mean = annual_rolling_mean[annual_rolling_mean.index.year >= 2017] annual_rolling_mean = annual_rolling_mean.dropna(axis=1)
接著透過 TejToolAPI 將其餘資料也一併抓齊,接著就能將資料作合併
columns = ['近12月累計營收_千元', 'Outstanding_Shares_1000_Shares', '股東權益總計', '每股淨值' , '收盤價' , '調整係數'] start_dt = pd.Timestamp('2015-01-01', tz = 'UTC') end_dt = pd.Timestamp('2024-12-31', tz = "UTC") data = TejToolAPI.get_history_data(start = start_dt, end = end_dt, ticker = pool, fin_type = ['A' , 'Q' , 'TTM'], columns = columns, transfer_to_chinese = True) # 將 pivot table 攤平為 long format rolling_long = annual_rolling_mean.copy() rolling_long = rolling_long.stack().reset_index() # 重命名欄位 rolling_long.columns = ["年/月", "股票代碼", "過去3年平均稅前盈餘成長率"] # 建立 year 欄 rolling_long["year"] = rolling_long["年/月"].dt.year rolling_long = rolling_long.drop(columns=["年/月"]) # 如果不需要年/月就刪掉 # 先從 data 中取出年份 data["year"] = pd.to_datetime(data["日期"]).dt.year # 合併:依據 股票代碼 和 年份 merged_data = pd.merge(data, rolling_long, how="left", on=["股票代碼", "year"])
有了merged_data就能計算下跌風險值以及偏離值
data = merged_data data['日期'] = pd.to_datetime(data['日期']) # 確保日期格式正確 data['月份'] = data['日期'].dt.to_period('M') # 轉換成月份 (YYYY-MM) data['每股營業額'] = data['近12月累計營收_千元'] / data['流通在外股數_千股']#A data['每股帳面價值'] = data['股東權益總計_Q'] / data['流通在外股數_千股']#B data['年度'] = data['日期'].dt.year # 取平均,確保三者都是平等的權重 #C已經完成 data['過去4季每股盈餘'] = data['每股淨值_TTM']#D # 計算 C × D data['C_D'] = data['過去3年平均稅前盈餘成長率'] * data['過去4季每股盈餘'] # 若 C × D 為負數,則改為 0 data.loc[data['C_D'] < 0, 'C_D'] = 0 # 計算 E(下跌風險值) data['下跌風險值'] = (data['每股營業額'] + data['每股帳面價值'] * 1.5 + data['C_D'] * (1/3)) / 3 # 刪除不必要的中間變數 data.drop(columns=['C_D'], inplace=True) data['偏離值'] = (data['收盤價'] * data['調整係數'] - data['下跌風險值']) / (data['收盤價'] * data['調整係數'])
經過一系列的計算後我們得到偏離值,接著就能把資料匯入,詳細程式碼可以參考相關連結
建立 CustomDataset 函式 & 建立 Pipeline 函式
這段程式碼的架構是透過 Zipline 的 Pipeline 系統,自訂一個名為 Bias_Value 的資料欄位(CustomDataset),將事先處理好的偏離值資料載入後,建立一個選股策略(Pipeline),挑選偏離值為負且最小的前 10 檔股票作為多頭標的,最後透過 SimplePipelineEngine 執行整體流程,回傳指定期間內的選股結果。
from zipline.pipeline.data.dataset import Column, DataSet from zipline.pipeline.loaders.frame import DataFrameLoader transform_data = data5.unstack('SID') transform_data # 確保索引為 UTC fixed_transform_data = transform_data.copy() # 如果索引已經有時區,則用 tz_convert 轉換 if fixed_transform_data.index.tz is not None: fixed_transform_data.index = fixed_transform_data.index.tz_convert('UTC') else: fixed_transform_data.index = fixed_transform_data.index.tz_localize('UTC') class CustomDataset(DataSet): Bias_Value = Column(dtype=float) domain = TW_EQUITIES # 建立 DataFrameLoader Custom_loader = { CustomDataset.Bias_Value: DataFrameLoader(CustomDataset.Bias_Value, fixed_transform_data['偏離值']) } from zipline.pipeline.data import EquityPricing def choose_loader(column): if column.name in EquityPricing.column_names: return pricing_loader elif column.name in CustomDataset.column_names: return Custom_loader[column] else: raise Exception('Column not available') engine = SimplePipelineEngine(get_loader = choose_loader, asset_finder = bundle.asset_finder, default_domain = TW_EQUITIES) from zipline.pipeline import Pipeline def compute_signals_debug(): bias = CustomDataset.Bias_Value.latest return Pipeline(columns={'偏離值(G)': bias}) pipeline_debug = engine.run_pipeline(compute_signals_debug(), start_dt, end_dt) # 篩選出 偏離值\(G\)
不是 NaN 的行 valid_pipeline_debug = pipeline_debug[pipeline_debug['偏離值(G)'].notna()] def compute_signals(): # **篩選出負的偏離值** signals = CustomDataset.Bias_Value.latest negative_bias_filter = signals < 0 # 只選偏離值為負的 # **選擇最負的前 40 檔** longs = signals.bottom(40, mask=negative_bias_filter) # 取最小的 40 檔(偏離值最負) return Pipeline(columns={ 'signals': signals, # 直接存偏離值 'longs': longs # 選最負的前 40 檔作為買進標的 }) pipeline_result = engine.run_pipeline(compute_signals(), start_dt, end_dt)
建立 Initialize 函式
initialize() 函式用於定義交易開始前的每日交易環境,與此例中我們設置:
def initialize(context): """ Called once at the start of the algorithm. """ context.universe = assets context.tradeday = tradeday context.set_benchmark(symbol('IR0001')) context.longs = [] context.shorts = [] # 交易成本 #set_commission(commission.PerDollar(cost=commission_cost)) set_commission(commission.Custom_TW_Commission()) set_slippage(slippage.TW_Slippage( spread = 1.0,volume_limit=0.8)) # schedule_function schedule_function(func=rebalance, date_rule=date_rules.every_day(), time_rule=time_rules.market_open) schedule_function(func=record_vars, date_rule=date_rules.every_day(), time_rule=time_rules.market_close) pipeline = compute_signals() attach_pipeline(pipeline, 'signals')
建立 rebalance 函式
這段程式碼是 Zipline 策略中的再平衡函數,功能如下:
在指定交易日內執行,取消所有未成交訂單,根據 context.trades 中訊號為 1 的股票作為做多標的。這些股票會等資金分配。同時,賣出不再符合條件的持股,對於新選及續抱的股票則下單調整到新目標權重,完成投資組合的動態調整。
from collections import defaultdict def rebalance(context, data): """ 1. 只在 context.tradeday 裡的日期執行 2. 取消所有未成交訂單 3. 從 context.trades 取出 long_candidates(signal == 1) 4. 用偏離值絕對值計算每檔股票的新目標權重 5. 賣掉「失選」的(signal != 1 & 目前持有) 6. 對新進 + 重疊的,每檔都 order_target_percent 到它的新目標權重 """ today = get_datetime().strftime('%Y-%m-%d') if today not in context.tradeday: return # 2. 取消所有未成交訂單 for asset, orders in get_open_orders().items(): for o in orders: cancel_order(o) # 3. 這期要做多的股票 long_candidates = [ stock for stock, signal in context.trades.items() if signal == 1 ] # 沒有候選股,就全部清空 if not long_candidates: for pos in context.portfolio.positions.values(): if pos.amount > 0: order_target_percent(pos.asset, 0) return # 4. 計算偏離值絕對值權重 abs_signal = context.output.loc[long_candidates, 'signals'].abs() total_bias = abs_signal.sum() target_weights = { stock: (1 / len(long_candidates)) for stock in long_candidates } # 5. 現有持股 held = [ pos.asset for pos in context.portfolio.positions.values() if pos.amount > 0 ] # 只賣掉「失選」的 dropouts = set(held) - set(long_candidates) for stock in dropouts: order_target_percent(stock, 0) # 6. 新進 + 重疊 的都調整到新目標權重 # order_target_percent 會自動「加減部位到達目標比例」 for stock, w in target_weights.items(): order_target_percent(stock, w)
回測策略
使用 run_algorithm() 來執行上述設定的動能策略,資料集使用 tquant,初始資金設定為 1,000,000 元。執行過程中,輸出的 results 包含每日績效和交易明細。
在進行繪圖時,為了避免字體錯誤,先進行字體設定:
from zipline import run_algorithm from zipline.utils.calendar_utils import get_calendar capital_base = 1e6 calendar_name = 'TEJ' start_dt = pd.Timestamp(start, tz = 'UTC') end_dt = pd.Timestamp(end, tz = "UTC") # Running a Backtest results = run_algorithm(start=start_dt, end=end_dt, initialize=initialize, before_trading_start=before_trading_start, capital_base=capital_base, data_frequency='daily', analyze=analyze, bundle=bundle_name, trading_calendar=get_calendar(calendar_name), custom_loader=Custom_loader)
利用 Pyfolio 進行績效評估(無止盈、無止損)
import pyfolio as pf from pyfolio.utils import extract_rets_pos_txn_from_zipline from pyfolio.plotting import (plot_perf_stats, show_perf_stats, plot_rolling_beta, plot_rolling_returns, plot_rolling_sharpe, plot_drawdown_periods, plot_drawdown_underwater) from pyfolio.tears import * from pyfolio.timeseries import (perf_stats, extract_interesting_date_ranges, sharpe_ratio, sortino_ratio) import empyrical returns, positions, transactions = pf.utils.extract_rets_pos_txn_from_zipline(results) pf.tears.create_full_tear_sheet(returns, positions=positions, benchmark_rets=results['benchmark_return'], transactions=transactions)
績效指標 / 策略 大盤(Benchmark) 麥克莫非投資策略 年化報酬率 16.275% 15.767% 累積報酬率 177.386% 169.287% 年化波動度 17.279% 19.856% 夏普值 0.96 0.84 卡瑪比率 0.57 0.45 期間最大回撤 -28.553% -35.045% 從整體績效指標來看,雖然麥克莫非投資策略的年化報酬率(15.767%)與累積報酬率(169.287%)僅略低於大盤(分別為16.275%與177.386%),且在風險控制方面表現相對較弱。該策略的年化波動度為19.856%,高於大盤的17.279%,夏普值為0.84,亦低於大盤的0.96,顯示在承擔更高風險的情況下,其單位風險報酬不如大盤。此外,該策略的期間最大回撤達 -35.045%,亦大於大盤的 -28.553%,代表策略在市場波動期間面臨較大資本損失風險。整體而言,麥克莫非策略雖具備一定報酬能力,但在風險調整後的績效與資金防禦能力上,仍略遜於大盤表現。
從年化報酬圖顯示策略在多數年度均有不錯表現,尤其在 2023 年年報酬超過 40%,顯示策略在特定市場環境下具備強勁的獲利能力;雖然 2018 與 2022 年出現負報酬,這部分可以視情況設定止盈止損。
結論
本策略聚焦於高科技產業,從中篩選出偏離值最小的前 40 檔個股,偏離值越低代表該股票與基準價格的落差越小,反映其價格相對穩定、下跌風險較低。科技業雖具有高成長潛力,但波動性大、風險高,因此透過此策略能有效辨識出在高風險環境中相對穩健的投資標的。從回測結果來看,策略報酬接近市場,還展現出良好的風險控制能力,但是相對也因為風險的降低,篩選掉了一些飆股的可能性,導致有時大盤在漲反而此策略的選股沒有明顯向上趨勢。
歡迎投資朋友參考,之後也會持續介紹TEJ資料庫來建構各式指標,並回測指標績效,所以歡迎對各種交易回測有興趣的讀者,選購TQuant Lab的相關方案,用高品質的資料庫,建構出適合自己的交易策略。
溫馨提醒,本次分析僅供參考,不代表任何商品或投資上建議。
【TQuant Lab 回測系統】解決你的量化金融痛點
全方位提供交易回測所需工具
留言 0