【實戰應用】乖離率交易策略

TEJ 台灣經濟新報
TEJ-API 金融資料分析
11 min readDec 26, 2022

使用Python建立乖離率交易策略,並進行歷史回測。

Photo by Chris Liverani on Unsplash

本文關鍵字:乖離率、歷史回測、台積電

本文重點概要

文章難度:★☆☆☆☆

使用個股未還原收盤價計算N天乖離率指標,並結合N天前最低價和最高價作為進出場依據。
閱讀建議:本文主要透過乖離率和歷史高低點作為進出場判斷依據,提供讀者使用程式進行回測時可供參考的架構,若讀者欲了解其他常用的技術指標,可先行閱讀【量化分析】MACD指標回測實戰,進而對本文有更好的理解。

前言

乖離率是常見的技術指標之一,使用當前的股價與N天的移動平均價進行比較,反映出當前股價相較於過去歷史是否過高或過低。普遍來說,當股價持續高過移動平均價稱為「正乖離」;反之持續低於移動平均價則稱為「負乖離」,因此當正負乖離持續擴大時,就會被解讀為市場正發生持續性的超買或是超跌的情況,進而作為進出場的判斷依據。但單純只用乖離率容易產生過多的交易訊號,因此我們額外加上過去N天的最高和最低價作為第二層濾網。實際策略如下:

當收盤價高於過去N天最高價,同時乖離率為負值時,隔天開盤價進場建倉。

當收盤價低於過去N天最低價,同時乖離率為正值時,隔天開盤價出場平倉。

編輯環境及模組需求

本文使用Windows OS並以jupyter作為編輯器

import tejapi as tej
import pandas as pd
import numpy as np
import datetime
import matplotlib.pyplot as plt
import ffn

tej.ApiConfig.api_key = 'Your Key'

plt.rcParams['font.family'] = 'Noto Sans TC'

%matplotlib inline

資料庫使用

上市(櫃)未調整股價(日)(TWN/APRCD)

資料導入

資料期間從2015/01/05至2022/11/25,以台積電作為實例,抓取未調整的開盤價、最高價、最低價和收盤價資料。

#上市櫃公司代號

price = tej.get('TWN/APRCD',
coid='2330',
mdate={'gt': '2015-01-01', 'lt':'2022-11-28'},
opts={'columns': ['coid', 'mdate', 'close_d', 'high_d', 'low_d', 'open_d']},
chinese_column_name=True,
paginate=True)


price['年月日'] = price['年月日'].dt.date
price = price.reset_index(drop=True)

取得股價後首先計算乖離率指標,用每日的收盤價除以過去N天的移動平均價,並將之編寫為functions利於後續的參數優化。

def bias_cal(tmp ,n):

df = tmp.copy()

df['BIAS'] = ((df['收盤價(元)'] - df['收盤價(元)'].rolling(n).mean()) / df['收盤價(元)'].rolling(n).mean()).round(4)

return df

bias_cal(price, 20).tail(10)

計算好每天的乖離率後,對其進行歷史回測,同時計算交易成本為:
買入手續費0.1425%;賣出手續費0.1425% + 交易稅0.3%。同樣做成functions利於後續的參數優化。

def performance(tmp, n, init=1000000):
df = tmp.copy()

signal = 0

df['his_low'] = df['最低價(元)'].shift(n)
df['his_high'] = df['最高價(元)'].shift(n)

df.loc[0, '現金'] = init
df['進出場'] = 0

for i in range(1, len(df)):

if (df.loc[i-1, '收盤價(元)'] > df.loc[i-1, 'his_high']) & (df.loc[i-1, 'BIAS'] < 0) & (signal == 0): #前一天收盤價小於n天前最低價,且收盤乖離率<=0,則當天開盤價進場

df.loc[i, '股票'] = df.loc[i, '開盤價(元)'] * 1000
df.loc[i, '交易成本'] = round(-df.loc[i, '開盤價(元)']*1000*0.001425)
df.loc[i, '現金'] = df.loc[i-1, '現金'] - df.loc[i, '股票'] + df.loc[i, '交易成本']
df.loc[i, '進出場'] = '進場建倉'

signal = 1

elif (df.loc[i-1, '收盤價(元)'] < df.loc[i-1, 'his_low']) & (df.loc[i-1, 'BIAS'] > 0) & (signal == 1): #前一天收盤價大於n天前最高價,且收盤乖離率>=0,則當天開盤價出場

df.loc[i, '股票'] = 0
df.loc[i, '交易成本'] = round(-df.loc[i, '開盤價(元)']*1000*0.004425)
df.loc[i, '現金'] = df.loc[i-1, '現金'] + df.loc[i, '開盤價(元)']*1000 + df.loc[i, '交易成本']
df.loc[i, '進出場'] = '出場平倉'

signal = 0

elif signal == 1:

df.loc[i, '股票'] = df.loc[i, '收盤價(元)'] * 1000

df.loc[i, '現金'] = df.loc[i-1, '現金']

else:

df.loc[i, '現金'] = df.loc[i-1, '現金']

df['股票'] = df['股票'].fillna(0)
df['交易成本'] = df['交易成本'].fillna(0)
df['總權益'] = df['現金'] + df['股票']

print('總權益:', df['總權益'].iloc[-1])
print('交易成本:', df['交易成本'].sum())
print('報酬率:', '%.2f'%((df['總權益'].iloc[-1] - init)/init*100), '%')

print('年均報酬率:', '%.3f'%(df['總權益'].pct_change().mean()*252*100), '%')
print('年化標準差:', '%.3f'%(df['總權益'].pct_change().std()*np.sqrt(252)*100), '%')
print('夏普值:', '%.3f'%((df['總權益'].pct_change().mean()*252*100) / (df['總權益'].pct_change().std()*np.sqrt(252)*100)))

print('最大回撤:', '%.2f'%(((df['總權益'] / df['總權益'].cummax() - 1).cummin().iloc[-1])*100), '%')

return df

result = performance(bias_cal(price, 20), 20)

首先用一個月20天,起始金額100萬為參數來看看成果,可以看出到了最後一天只剩下90萬,其中大部分都被交易成本給耗光了,只賺到微幅的報酬1.17%。

N=20

將總權益和回撤曲線圖形化來看看成果。可以看出從2021年中開始隨著台積電一路狂奔,但近期由於升息和烏俄戰爭等多重因素,創下當前最大回撤幅度,回吐之前的獲利。

n = 20
result = performance(bias_cal(price, n), n)
fig, ax = plt.subplots(2, 1, figsize=(20, 12), sharex=True)

ax[0].plot(result['年月日'], result['總權益'])

ax[1].plot(result['年月日'], result['總權益'].to_drawdown_series().to_list(), color='c')

dd = (result['總權益'].to_drawdown_series()*100).to_list()

ax[1].fill_between(result['年月日'], np.zeros(len(result['總權益'])), dd, label='大盤 DD', color='blue', linewidth=1, alpha=0.8)

ax[0].set_title(f'參數{n}-總權益 & 回撤曲線圖', fontsize=20)
ax[1].set_xlabel('時間', fontsize=20)

ax[0].set_ylabel('累積金額', fontsize=20)
ax[1].set_ylabel('下跌幅度(%)', fontsize=20)


plt.subplots_adjust(hspace=.0)

後續我們進一步將參數進行優化找出最佳的N天,從2天一路搜尋到50天挑選其中夏普值最高的參數。

optim = []
for i in range(2, 51):
df = performance(bias_cal(price, i), i)

sharp = ((df['總權益'].pct_change().mean()*252) / (df['總權益'].pct_change().std()*np.sqrt(252)))

ret = round(((df['總權益'].iloc[-1] / df['總權益'].iloc[0]) - 1), 4)*100

optim.append((i, sharp, ret))

result1 = pd.DataFrame(optim, columns=['天數', '夏普值', '報酬率'])

result1.sort_values('夏普值', ascending=False).head(5)

在經過不同參數的優化後,發現7天的計算週期在過去回測歷史中表現最佳。

result = performance(bias_cal(price, 7), 7)

整體夏普值從0.055提升到0.826,報酬率更是大幅提升到將近40%的水平,而最大回撤也顯著的降低不少。

N=7

從權益圖中可看出,該策略在2022年初左右都還有不錯的表現,但還是不敵近期的強勢升息,創下自2015年以來的最大回撤。

結語

本次介紹的乖離率策略,屬於均值回歸的交易策略之一,當市場呈現超跌(乖離率<0)的情況且收盤價高於過去一段期間的最高價位時,判斷股價將逐漸回歸移動平均價位,因此進場做多;反之,當股價呈現超買(乖離率>0)且收盤價低於過去一段期間的最低價位時,認為股價已經漲過頭且有下跌的趨勢時,將原本做多的倉位平倉。但要注意由於本策略擁有較高頻率的交易策略,在一來一往中容易被手續費和交易稅給吃掉大量的獲利,因此建議讀者可以在此基礎上多結合其他技術指標,以優化進出場的時機點。

最後,還是要再次提醒本文所提及之標的僅供說明使用,不代表任何金融商品之推薦或建議。因此,若讀者對於建置策略、績效回測、研究實證等相關議題有興趣,歡迎選購 TEJ E Shop中的方案,具有齊全的資料庫,就能輕易的完成各種檢定。

完整程式碼

延伸閱讀

相關連結

給我們鼓勵
之後會持續分享更多財金資料庫的應用
如果你的覺得今天的文章不錯,可以幫我們在下面的
掌聲 icon 點 1下
如果覺得超讚,可以按住
掌聲 icon 不放直到 50 下
有任何想法歡迎點選
留言 icon和我們討論

--

--

TEJ 台灣經濟新報
TEJ-API 金融資料分析

TEJ 為台灣本土第一大財經資訊公司,成立於 1990 年,提供金融市場基本分析所需資訊,以及信用風險、法遵科技、資產評價、量化分析及 ESG 等解決方案及顧問服務。鑒於財務金融領域日趨多元與複雜,TEJ 結合實務與學術界的精英人才,致力於開發機器學習、人工智慧 AI 及自然語言處理 NLP 等新技術,持續提供創新服務