乖离率交易策略

使用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中的方案,具有齐全的资料库,就能轻易的完成各种检定。

完整程式码

延伸阅读

相关连结

返回总览页