Table of Contents
交易是一件充滿挑戰的事,許多全職交易員的年度績效甚至無法超越大盤。這讓我開始思考,是否有一個簡單卻能有效擊敗大盤的交易策略。
在台灣股市中,有一個有趣的現象:當一些股票大幅上漲時,總有人開始討論是否應該追進,擔心一進場就可能被套牢在高點,但又害怕錯過了進場的機會。
因此,我設計了一個簡單的選股策略,並結合不同週期的動能進行回測,來探討這個策略的績效表現,提供讀者作為參考。
在選股過程中,我們希望避免選到成交量過小、容易被大戶或法人操控導致劇烈波動的股票。為了減少這類風險,我選擇了當時市值前 200 大的公司作為股票池,確保所選股票具備較高的流動性與市場深度。
選定股票池後,我們針對每檔個股計算過去一個季度(時間長度可根據需求調整)的收盤價,來評估其中長期動能,並從中挑選動能表現最強的股票。這樣的選股方法有助於捕捉市場中的強勢股趨勢。
• 每個月的第一個交易日買入前期動能最高的股票,並在月底賣出。
• 本文提供了可調整的止盈和止損代碼,讀者可根據需求自行調整使用。
此策略設計的目的是模擬在不需要長時間盯盤且不受當日股價波動干擾的情況下進行投資。我們會在每月的月底先計算出前期動能,並在每月的第一個交易日收盤前一分鐘買入動能最高的股票,再於每月的最後一個交易日收盤前一分鐘賣出。此外,策略中會設定滑價功能,以模擬下單時股價的摩擦成本對交易的影響。這樣的設計既簡單又有效,適合不希望頻繁操作的投資者。
本文使用 MacOS 14.6.1 作業系統,並以 Visual Studio Code (Vscode) 作為主要編輯器進行分析與撰寫。
# 載入常用套件
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# 設定環境變數(TEJ金鑰)
import tejapi
import os
os.environ['TEJAPI_BASE'] = 'https://api.tej.com.tw'
os.environ['TEJAPI_KEY'] = 'your_key'
# 回測系統需要的套件
from zipline.sources.TEJ_Api_Data import get_universe
from zipline.data import bundles
from zipline.sources.TEJ_Api_Data import get_Benchmark_Return
from zipline.pipeline.mixins import SingleInputMixin
from zipline.pipeline.data import TWEquityPricing
from zipline.pipeline.factors import CustomFactor
from zipline.pipeline import Pipeline
from zipline.TQresearch.tej_pipeline import run_pipeline
from zipline import run_algorithm
在選擇回測的時間點時,選擇當時市值前 200 大的公司作為股票池(避免前視偏誤),以確保所選股票具備較高的流動性與市場深度。在此步驟中,可以檢查股票池是否包含具有不喜歡特性的股票,並根據需求進行適當調整。
pool = get_universe(start = '2018-01-01',
end = '2018-08-01',
mkt_bd_e = ['TSE'], # 已上市之股票
stktp_e = 'Common Stock'
)
import TejToolAPI
cap_data = TejToolAPI.get_history_data(start = '2018-01-03',
end = '2018-01-03',
ticker = pool,
columns = ['Market_Cap_Dollars'])
top_200_cap = cap_data.sort_values(by='Market_Cap_Dollars', ascending=False).head(200)['coid'].tolist()
top_200_cap
資料期間從 2018-06-01 至 2024-08-15,並導入上述 200 檔股票的價量資料與加權股價報酬指數 ( IR0001 ) 作為績效比較基準。
# 設定回測期間與起始時間,'start' 設定為 '2018-06-01',以確保能計算 2019-01-01 的動能
# (起始時間依據所需的動能計算長度設定,回測期間為 2019/01/01 ~ 2024/08/01)
start = '2018-06-01'
end = '2024-08-15'
# 設定環境變數以指定回測時間範圍與選定的股票池
os.environ['mdate'] = start + ' ' + end
os.environ['ticker'] = ' '.join(top_200_cap) + ' ' + 'IR0001'
# 執行資料擷取
!zipline ingest -b tquant
有了價量資料後,我們可以利用 CustomFactor 函式來建構我們需要的動能指標。此函式會根據每支股票的指定時間長度(window_length)來計算動能,透過收盤價的變化計算出該股票的動能。
from zipline.pipeline.filters import StaticAssets
class Momentum(CustomFactor):
inputs = [TWEquityPricing.close]
# 設定不同時間跨度的動能計算,這裡以一個季度, 63天為例
window_length = 63
def compute(self, today, assets, out, close):
# 計算動能:以收盤價變化計算報酬率,並四捨五入至小數點後五位
return_rate = (((close[-1] - close[0]) / close[0]) * 100).round(5)
out[:] = return_rate
在這段程式,Momentum()
繼承了 CustomFactor 並設定計算動能的邏輯。window_length 指定計算期間的長度(例如 63 天),動能透過首尾收盤價的變化來計算,並將結果輸出。這樣的方式讓我們能輕鬆計算出每支股票在指定期間內的動能。
Pipeline()
提供使用者快速處理多檔標的的量化指標與價量資料的功能,於本次案例我們用以處理:
# 重新定義開始及結束日期,用於回測期間
start = '2019-01-01'
end = '2024-08-15'
start_dt, end_dt = pd.Timestamp(start, tz='utc'), pd.Timestamp(end, tz='utc')
bundle = bundles.load('tquant')
benchmark_asset = bundle.asset_finder.lookup_symbol('IR0001', as_of_date=None)
def make_pipeline():
# 使用自定義的動能指標
mom = Momentum()
# 取得最新的收盤價
curr_price = TWEquityPricing.close.latest
return Pipeline(
columns={
'curr_price': curr_price,
'Momentum': mom
},
# 過濾掉基準指數,僅篩選個股
screen=~StaticAssets([benchmark_asset])
)
my_pipeline = run_pipeline(make_pipeline(), start_dt, end_dt)
dates = my_pipeline.index.get_level_values(0).unique()
my_pipeline
initialize()
函式用於定義交易開始前的每日交易環境,與此例中我們設置:
from zipline.finance import slippage, commission
from zipline.api import *
from zipline.api import set_slippage, set_commission, set_benchmark, attach_pipeline, order, order_target, symbol, pipeline_output, record
def initialize(context):
context.holding_stock = False
context.target = None
context.stock_buy_price = 0
set_slippage(slippage.FixedBasisPointsSlippage(basis_points=50.0, volume_limit=1))
set_commission(commission.PerShare(cost=0.001425 + 0.003 / 2))
attach_pipeline(make_pipeline(), 'mystrats')
set_benchmark(symbol('IR0001'))
handle_data() 是構建交易策略的重要函式,它會在回測期間的每個交易日被呼叫。這個函式的主要任務是設定交易策略、執行下單以及紀錄交易資訊。
如同在文章開頭所提到的,我們的策略只需根據日期來判斷是否為當月的月初或月末,並根據當時的動能值來決定要買入哪檔股票。
策略總結:
• 進場條件:當日期為每個月的第一個交易日,且選定的股票動能最高時進場。
• 出場條件:在每個月的最後一個交易日,賣出已持有的股票。此外,也可根據自定義的止盈止損條件提前出場。
def handle_data(context, data):
# Pipeline 輸出表格(例如第一天是 2019-01-02,表格的 index 是股票代碼,columns 則為股價和設定的指標,和上一段落的 my_pipeline 一樣)
out_dir = pipeline_output('mystrats')
out_dir = pipeline_output('mystrats')
# 獲得今天日期
current_date = context.get_datetime()
current_year = current_date.year
current_month = current_date.month
current_day = current_date.day
# 找到目前日期,在資料裡面這個年月份的第一天和最後一天
dates_info = dates[(dates.month == current_month) & (dates.year == current_year)]
min_day = dates_info.min().day
max_day = dates_info.max().day
# 每月第一天買入股票
if not context.holding_stock :
if current_day == min_day:
# 選擇第 n 大的股票
# top_10_momentum = out_dir['Momentum'].nlargest(10)
# context.target = top_10_momentum.index[n]
context.target = out_dir['Momentum'].idxmax()
context.stock_buy_price = out_dir.loc[(context.target), 'curr_price']
available_cash = context.portfolio.cash
num_shares = int(available_cash // context.stock_buy_price)
order(context.target, num_shares)
context.holding_stock = True
# 若是月底則賣出,或者根據設定的止盈止損條件提前賣出
elif context.holding_stock:
stock_price = out_dir.loc[(context.target), 'curr_price']
# 止損或止盈條件(可選)
# if (current_day != max_day) and ((stock_price - context.stock_buy_price) / context.stock_buy_price) < -0.1:
# order_target(context.target, 0)
# context.holding_stock = False
# context.target = None
# context.stock_buy_price = 0
# if (current_day != max_day) and ((stock_price - context.stock_buy_price) / context.stock_buy_price) > 0.5:
# order_target(context.target, 0)
# context.holding_stock = False
# context.target = None
# context.stock_buy_price = 0
# 每月最後一天賣出持有股票
if (current_day == max_day):
order_target(context.target, 0)
context.holding_stock = False
context.target = None
context.stock_buy_price = 0
else:
pass
def analyze(context, perf):
pass
使用 run_algorithm() 來執行上述設定的動能策略,資料集使用 tquant,初始資金設定為 100,000 元。執行過程中,輸出的 results 包含每日績效和交易明細。
在進行繪圖時,為了避免字體錯誤,先進行字體設定:
# 設定字體,避免字體錯誤
plt.rcParams['font.family'] = 'Arial Unicode MS'
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS']
# 執行回測
results = run_algorithm(
start = start_dt,
end = end_dt,
initialize=initialize,
bundle='tquant',
analyze=analyze,
capital_base=1e7,
handle_data = handle_data
)
results
from pyfolio.utils import extract_rets_pos_txn_from_zipline
import pyfolio as pf
# 從 results 資料表中取出 returns, positions & transactions
returns, positions, transactions = extract_rets_pos_txn_from_zipline(results)
benchmark_rets = results.benchmark_return # 取出 benchmark 的報酬率
# 繪製 Pyfolio 中提供的所有圖表
pf.tears.create_full_tear_sheet(returns=returns,
positions=positions,
transactions=transactions,
benchmark_rets=benchmark_rets
)
從上圖數據中可以看出,這個策略的年化報酬率達到 64.018%,但同時伴隨著 63.715% 的高波動度,顯示出策略雖有潛力帶來高額收益,但風險同樣不小。夏普比率為 1.09,表明策略的風險調整後回報雖然正向,但並非特別優異;然而,索提諾比率為 1.7,顯示出該策略在管理下行風險方面相對有效。因此,這個策略適合追求高回報且能承受較大波動的投資者,對於保守型投資者則可能不太合適。
在回檔比率圖中,可以看到此策略的波動程度非常大,並且時常會有很大的回檔,最高則在近期2024/08台股重挫,此策略的回檔來到50%。
從年化報酬和月報酬的圖表可以看到,雖然有近 40% 的月份月報酬為負,即便如此,平均月報酬仍為正數。然而,這個策略的缺點在於波動較大,資金能夠翻倍的主要原因是 2021 年期間捕捉到了陽明海運的飆漲。我認為這正是這個策略的價值所在:即使經歷了多次虧損,透過抓住幾次大的趨勢性機會,依然能實現資本的顯著增長。
最後,我們來探討這個策略是否真的是一個「高風險策略」。這張圖表展示了在波動性對照的情況下,策略與大盤的累積報酬率對比,可以用來評估策略在相同風險水平下的相對表現。在 2021 年 6 月之前,策略的表現雖然不如大盤,但當捕捉到一次高額的獲利機會後,表現迅速翻身。更值得注意的是,儘管大盤在 2022 年呈現下跌趨勢,該策略卻能保持穩定,成功避開了這波跌幅。因此,我認為,儘管這個策略的波動度較高,但整體的表現優勢依然勝過大盤。
在上面的部分,我們已經將無止盈、無止損的策略結果展示出,接著我們將分析,只有止盈、只有止損以及同時設置止盈止損,分析為何報酬率會有不同的變化
從上圖可以看出,當我們將盈利限制在 30% 後提前出場,原本讓無限制策略報酬大幅飆升的那段收益顯著落後,因此自 2021 年 6 月之後,兩者開始呈現平行趨勢,顯示出設置止盈後雖然減少了部分回報,但仍保持了與無限制策略相似的走勢。然而,這個圖表揭示了設置止盈的初衷:我們擔心標的股票在顯著上漲後出現回檔,從而導致已獲得的利潤縮水。然而在這個案例中,我們並未看到這個策略的優點,甚至可以觀察到,如果將止盈點設置得更低(例如 20%),策略的表現反而從原本大幅領先大盤變成落後大盤,這表明在這個策略中,我們主要依靠少數幾次交易中的大額盈利來實現整體亮眼的報酬,而非依賴多次的小幅收益來累積資金增長。過度限制反而可能適得其反。
那麼如果將止盈點設置得更高呢?在止盈點為 30% 到 40% 的區間內,我們會發現相比於 30% 的標準,報酬率並沒有明顯提升,增幅有限。然而,當我們進一步將止盈點提高到 50% 時,績效反而低於 30% 和 40% 的水準。這讓我想到一個可能的原因:在我們選股策略中,某些股票的特性使其單月的表現很難超過 50%,而這些股票在上漲至 40% 左右時往往會出現回檔修正。因此,將止盈點設置過高反而導致績效下降的情況。
設置止盈確實有助於降低波動風險,但如同上述分析所顯示,止盈點的選擇對績效有顯著影響。我們在回測中可以找到最適合的止盈點,但在現實交易中,這種「最棒的止盈點」並不容易預測。儘管風險得到控制,但從圖表中可以看出,無限制策略的報酬通常高於設置止盈的策略,這意味著即使風險未被有效控制,依靠少數幾次大幅上漲的機會,策略仍能獲得可觀的收益。因此,若要在實際操作中執行此策略,我可能不會設置止盈點,因為這樣可能錯失策略的核心價值,即捕捉那些高成交量的強勢股帶來的爆發性回報。
同樣的方式,讓我們來分析止損。
在設置止損的過程中,我們可以觀察到一些有趣的現象。最初,我將止損點設置在 5%,但很快發現這樣的設定與預期的績效差距較大。原因可能在於,當一些具有較高潛力的股票大幅上漲時,往往會伴隨著較大的波動。因此,止損點設置得太緊會導致過早出場,錯失後續的上漲空間。要在合理波動內保留持股,止損點需要設得更寬鬆一些。從上圖可以看到,設置 10% 和 15% 止損點的最大差異出現在 2024 年初,當時所持有的股票雖然跌破了 10% 的止損點,但並未超過 15%。結果是,10% 的止損點在該時點觸發,導致錯失了後續回升的漲幅,而 15% 的止損點則成功保留了股票,抓住了隨後的上漲收益。
最後,我們嘗試同時設置止盈和止損的雙向出場條件。結果顯示,相比於只設置單一出場條件,這種策略有效降低了整體波動,使資產曲線更加平穩。即便如此,策略最終依然實現了優於大盤的亮麗表現。因此,對於風險承受能力較低的投資者,這種雙向出場的策略可能是一個值得考慮的選擇,因為它在控制風險的同時,依然保有不錯的收益潛力。
本次策略是一個很有趣的議題,究竟該不該「上車」,相信讀者心中都有自己的答案。即便我已將股票池範圍限定在市值前 200 大的公司,試圖降低被操控的風險,但當選出動能最大的股票時,這些股票仍往往是短期內飆漲的個股。這類股票過去一個季度的強勁表現可能已經接近尾聲,但也可能繼續享受後續的漲幅。
在這次策略中,獲得高額利潤的關鍵在於在承擔高風險的同時,成功持有了那些具備強勁上漲動能的股票。以 2021 年為例,策略受益於陽明海運的爆發性漲勢;在 2024 年,即便經歷了超過 10% 的跌幅,股票仍強勢回升,帶來可觀的獲利。我們在測試過程中嘗試了多種條件設置,例如單純止盈、單純止損,或同時設置止盈和止損。正如文章中提到的分析,有興趣的讀者可以根據自己想驗證的情境自行調整代碼,策略的主要邏輯已經完善,靈活調整即可,這樣可以驗證不同設定對策略績效的影響。
電子報訂閱