Photo from Unflash by Markus Spiske
Table of Contents
在投資的世界裡,準確的預測和系統性的分析是取得長期成功的關鍵,而能夠洞悉市場趨勢的策略家,更是投資者們爭相學習的對象。麥克.喜偉(Michael Sivy)無疑是其中的佼佼者。他以精準的市場預測和獨到的投資見解,在美國投資界樹立了卓越的聲譽。
從1987年預警美國股市大崩盤,到1995年預言股市轉機,再到1999年洞察科技股泡沫的破滅,麥克.喜偉以數十年的市場研究和實戰經驗,證明了其卓越的投資洞察力。他的作品《投資金律》更成為眾多投資者必讀的經典,透過系統性的分析和清晰的準則,為不同類型的投資者提供了實用的選股指南。
本篇文章將聚焦於麥克.喜偉的投資理念,特別是他為收益型投資者制定的四大準則:高收益率、穩健成長率、健康財務狀況和合理估值。我們將結合其經典案例,探索這些原則如何在現實投資中實現收益最大化,並為當前的投資環境提供指引。
麥克.喜偉重視公司在各種收益率的表現,因此使用了較多的收益相關資料進行股票的篩選。同時也關心公司的營運方的表現,我們將採用舉債情況以及資產的流動程度來作為首要依據,去評估公司營運方是否有將公司為實在良好的體質當中。
通過上述的五個條件進行選股,於每季最後一天交易日更換部位,最後查看麥克.喜偉(Michael Sivy)的擇股策略是否可以應用於台灣股市。
基本的模組輸入以及apikey 的設定
import pandas as pd
import numpy as np
import tejapi
import os
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = 'Arial'
tej_key ='your tejapi key'
tejapi.ApiConfig.api_key = tej_key
os.environ['TEJAPI_BASE'] = "your tej base"
os.environ['TEJAPI_KEY'] = tej_key
選擇回測時間之前就存在的股票(2019年就存在的股票),避免出現前視偏誤問題,並選擇上市股票作為研究目標
from zipline.sources.TEJ_Api_Data import get_universe
pool = get_universe(start = pd.Timestamp('2019-01-01', tz = 'UTC'),
end = pd.Timestamp('2019-12-31', tz = "UTC"),
mkt_bd_e = 'TSE', stktp_e = 'Common Stock')
關於 get_universe 用法可以參考 TQuant Lab Github 官網:https://github.com/tejtw/TQuant-Lab/blob/main/lecture/get_universe說明.ipynb
利用TEJToolAPI的get_history函數抓取需要的財務會計資料,這裡抓取麥克.喜偉(Michael Sivy)的擇股條件所需要的會計資料。
例如:本益比、流動比率、營收成長率等等。
import TejToolAPI
columns = ['Industry', '本益比', '收盤價', '流動比率', '股東權益總計', '負債總額', '營收成長率','eps','mt_div','現金股利率']
start_dt = pd.Timestamp('2015-12-29', tz = 'UTC')
end_dt = pd.Timestamp('2023-12-31', tz = "UTC")
data__ = TejToolAPI.get_history_data(start = start_dt,
end = end_dt,
ticker = pool,
fin_type = 'A', # 為累計資料
columns = columns,
transfer_to_chinese = True)
計算出每期換部位的時間(每季的最一天交易日):
通過選取台積電的資料當作樣本,來篩選出每季的最後一天交易日日期,之後會根據這個日期進行新一輪的換股以及選股。
# 找尋每年當中的12月以及6月的最後一天交易日日期
sample = data__[data__['股票代碼'] == '2330']
last_day_ = list(sample.groupby(sample['日期'].dt.year)['日期'].max())
june_data = sample[sample['日期'].dt.month == 6]
last_june_day = list(june_data.groupby(june_data['日期'].dt.year)['日期'].max())
march_data = sample[sample['日期'].dt.month == 3]
last_march_day = list(march_data.groupby(march_data['日期'].dt.year)['日期'].max())
sep_data = sample[sample['日期'].dt.month == 9]
last_sep_day = list(sep_data.groupby(sep_data['日期'].dt.year)['日期'].max())
last_day_ = last_day_ + last_june_day + last_march_day + last_sep_day
modified_day = []
for i in last_day_:
modified_day.append(i.date())
modified_day
日期輸出格式:
[datetime.date(2015, 12, 31),
datetime.date(2016, 12, 30),
datetime.date(2017, 12, 29),
datetime.date(2018, 12, 28),
datetime.date(2019, 12, 31),
datetime.date(2020, 12, 31),
datetime.date(2021, 12, 30),
datetime.date(2022, 12, 30),
datetime.date(2023, 12, 29),
datetime.date(2016, 6, 30),
datetime.date(2017, 6, 30),
datetime.date(2018, 6, 29),
datetime.date(2019, 6, 28),
datetime.date(2020, 6, 30),
datetime.date(2021, 6, 30),
datetime.date(2022, 6, 30),
datetime.date(2023, 6, 30),
datetime.date(2016, 3, 31),
datetime.date(2017, 3, 31),
datetime.date(2018, 3, 31),
datetime.date(2019, 3, 29),
datetime.date(2020, 3, 31),
datetime.date(2021, 3, 31),
先將需要的股票資料匯入浸會側系統當中,這裡除了剛剛獲取的股票池之外,會加入台股大盤(IR0001)資料當作選股策略的比較對象。回測的時間設定為2019/12/30至2024/11/31。
pools = pool + ['IR0001']
from zipline.data.run_ingest import simple_ingest
# 價量資料
simple_ingest(name = 'tquant' , tickers = pools , start_date = '20191231' , end_date = '20241131')
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
設定benchmark為台灣大盤指數(IR0001)來進行最終策略的比較,同時設定手續費以及滑價模型。
def initialize(context):
set_slippage(slippage.TW_Slippage(spread = 1 , volume_limit = 1))
# 設定為台灣股票手續費計算方法
set_commission(commission.Custom_TW_Commission(min_trade_cost=20, discount=1.0, tax = 0.003))
# 設定台灣大盤為比較基準
set_benchmark(symbol('IR0001'))
context.i = 0
context.state = False
context.order_tickers = []
context.last_tickers = []
交易策略設定為在每一年的最後一天交易日進行上一期股票的賣出,同時對下一期的股票進行買入。買入的部位比例為等權重的比例買進。
def compute_stock(date, data): # 創建一個函數,在指定的日期進行選股,輸出篩選出的股票列表。
# 提取出調整部位當日的股票資訊
df = data[data['日期'] == pd.Timestamp(date)].reset_index(drop = True)
# 本益比小於市場平均值
df['產業平均本益比'] = df.groupby('主產業別_中文')['本益比'].transform('mean')
set_1 = set(df[df['本益比'] < df['產業平均本益比']]['股票代碼'])
# 流動比例大於市場平均值
df['產業平均流動比率'] = df.groupby('主產業別_中文')['流動比率_A'].transform('mean')
set_2 = set(df[df['流動比率_A'] > df['產業平均流動比率']]['股票代碼'])
# 負債佔股東權益小於20%
df['負債佔股東權益'] = df['負債總額_A'] / df['股東權益總計_A']
set_3 = set(df[df['負債佔股東權益'] < 0.2]['股票代碼'])
# 現金股利率大於市場平均值
df['產業平均現金股利率'] = df.groupby('主產業別_中文')['現金股利率'].transform('mean')
set_4 = set(df[df['現金股利率'] > df['產業平均現金股利率']]['股票代碼'])
# 股利收益率加上獲利成長率大於10%
set_5 = set(df[df['營收成長率_A']*0.01 + df['現金股利率']*0.01 > 0.1]['股票代碼']) # 因為單位問題進行調整,將%調整為正確單位
tickers = list(set_1 & set_2 & set_3 & set_4 & set_5)
return tickers
def handle_data(context, data):
# 避免前視偏誤,在篩選股票下一交易日下單
if context.state == True:
print(f"下單日期:{data.current_dt.date()}, 擇股股票數量:{len(context.order_tickers)}")
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 / len(context.order_tickers))
curr = data.current(symbol(i), 'price')
record(price = curr, days = context.i)
context.last_tickers = context.order_tickers
context.state = False
backtest_date = data.current_dt.date()
# 查看回測時間是否符合指定日期
for idx, j in enumerate(modified_day):
if backtest_date == j:
# 調整狀態,在下一個交易下單
context.state = True
context.order_tickers = compute_stock(date = backtest_date, data = data__)
context.i += 1
def analyze(context, perf):
# plt.style.use('dark_background')
# 重置為預設樣式
plt.style.use('default')
plt.title(f'Portfolio Value')
plt.plot(perf['portfolio_value'], label='Portfolio Value')
plt.legend()
plt.show()
cumulative_returns = (1 + perf['returns']).cumprod() - 1
plt.title(f'Period Return Portfolio & Benchmark')
plt.plot(perf.index, cumulative_returns, label = 'Portfolio')
plt.plot(perf.index, perf['benchmark_period_return'], label = 'Benchmark')
plt.legend()
plt.show()
capital_base = 1e7
results = run_algorithm(
start = pd.Timestamp('20191231', tz = 'utc'),
end = pd.Timestamp('20231130', tz = 'utc'),
initialize = initialize,
handle_data = handle_data,
analyze = analyze,
bundle = 'tquant',
capital_base = capital_base)
results
回測期間設定為2019年底~2023年底,從累積報酬圖中看出選股策略的績效是略微優於大盤,表示這個策略有挑選出股市中上漲潛力較高的標的。但是在市場下行時,策略的下行幅度更加劇烈,在應對市場的系統性風險上相對缺乏韌性。從槓桿使用圖來看,只有在換股的幾天會出現槓桿的波動,其餘情況是維持在無槓桿的情況,使得策略在週轉資金上不會有過多的問題。策略年化報酬率為18.74%,略高於台灣大盤的年化報酬12-15%。
從移動波動度以及移動夏普值圖表中,可以刊到在2021年的下半年出現比較大的波動性。那段時間正好處於牛市結尾的回檔,台灣股票市場出現明顯的下跌段並且波動性較高,但策略的波動度卻明顯大於市場波動,我認為是這個選股方法是選出市場中上漲潛力較大的股票,這種股票市場處於熊市時,因為投資人的資金入場,因此叫沒有動能推升這類型股價。所以在該時間對中策略的下跌情形以及波動度都會較大,算是這個策略的市場風險。因此如果可以在整體股票市場基期較高時(牛市尾段)減少這個策略的操作,可以避開較大的波動時段。
以不同的換股頻率去進行相同邏輯的策略,我們採用每年、每半年以及每季為單位進行回測。從圖中可以看到換股頻率愈頻繁所帶來的整題績效會於高。經過篩選出的股票會在一個季度的內,股票價格反應公司的價值,因此進行高頻率的換股會對整體績效有顯著的提升。
統計量 | 3month | 6month | yearly | Benchmark |
年化報酬 | 18.74% | 16.058% | 17.635% | 14.92% |
累積報酬 | 94.499% | 78.025% | 87.581% | 71.362% |
夏普值 | 0.95 | 0.89 | 0.94 | 0.86 |
年化波動度 | 20.318% | 18.805% | 19.311% | 18.02% |
最大回撤 | -29.274% | -28.394% | -28.394% | -28.553% |
歡迎投資朋友參考,之後也會持續介紹使用 TEJ 資料庫來建構各式指標,並回測指標績效,所以歡迎對各種交易回測有興趣的讀者,選購 TQuant Lab 的相關方案,用高品質的資料庫,建構出適合自己的交易策略。
溫馨提醒,本次分析僅供參考,不代表任何商品或投資上的建議。
程式交易是什麼?程式交易教學、優缺點及常見策略懶人包 – TEJ台灣經濟新報
用 Alphalens 剖析因子表現,價量因子篇
電子報訂閱