大盘强弱指标回测实战

Photo by Tyler Prahm on Unsplash

 

本文重点概要

文章难度:★★☆☆☆
以大盘强弱指标为核心,做均线交叉的投资策略

阅读建议:本文透过各式技术指标以及严格的多空筛选条件来判断股票的强弱势家数,以此计算出大盘的多空指标,并利用简单的均线交叉策略,以视觉化的方式判断交易讯号以及买卖点位,并透过最佳化来得到报酬率最佳的参数。如果对回测或是技术指标不了解的读者,欢迎先行阅读[量化分析(二)]-技术分析简介与回测,可以详细理解回测的执行流程。

前言

近年很常听到俗称的”拉积盘”,其代表的意思是透过权值股的上涨,如:台积电、联发科等,带动的大盘指数上涨,但其余股票下跌的情形,会导致仅观察大盘涨跌的投资人,会有错误的判断,因此本文想透过严格的多空筛选标准构建出的强弱指标,转化为均线的方式,简单的设计一个均线交叉的投资策略,并使用最佳化的方式来寻找报酬率最好的参数。本文先采用以下策略进行回测,后续再透过最佳化调整:

1. 进场条件:日三线空指标20日均线大于日三线多指标20日均线
2. 出场条件:日三线多指标20日均线大于日三线空指标20日均线

编辑环境及模组需求

本文使用Mac OS 并以jupyter作为编辑器

import pandas as pd
import numpy as np
import tejapi
import matplotlib.pyplot as plt
import matplotlib.transforms as transforms
from matplotlib.font_manager import FontProperties
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS'] # 解决MAC电脑 plot中文问题
plt.rcParams['axes.unicode_minus'] = False
tejapi.ApiConfig.api_key = "YOUR KEY"
tejapi.ApiConfig.ignoretz = True

资料库使用:

证券属性资料表(TWN/ANPRCSTD)
未调整(日)技术指标(TWN/AVIEW1)
期货资料库(TWN/AFUTR)

资料导入:

由于资料量较大,分成多次从api抓取

data=tejapi.get('TWN/ANPRCSTD' ,chinese_column_name=True )
select=data["上市别"].unique()
select=select[1:3]
condition=(data["上市别"].isin(select))&(data["证券种类名称"]=="普通股")
data=data[condition]
twid=data["证券码"].to_list()  #取得上市柜股票证券码
df = tejapi.get('TWN/AVIEW1', #从TEJ api捞取所需要的资料
                  chinese_column_name = True,
                  paginate = True,
                  mdate = {'gt':"2022-01-01"},
                  coid=twid,
                  opts={'columns':['coid','mdate','close_d','kval','dval', 'dif','macd','a10ma']})
df1 = tejapi.get('TWN/AVIEW1', #从TEJ api捞取所需要的资料
                  chinese_column_name = True,
                  paginate = True,
                  mdate = {'gt':'2021-01-01', 'lt':'2022-01-01'},
                  coid=twid,
                  opts={'columns':['coid','mdate','close_d','kval','dval', 'dif','macd','a10ma']})
df2 = tejapi.get('TWN/AVIEW1', #从TEJ api捞取所需要的资料
                  chinese_column_name = True,
                  paginate = True,
                  mdate = {'gt':'2020-01-01', 'lt':'2021-01-01'},
                  coid=twid,
                  opts={'columns':['coid','mdate','close_d','kval','dval', 'dif','macd','a10ma']})
Y9999 = tejapi.get('TWN/AFUTR', #从TEJ api捞取所需要的资料
                  chinese_column_name = True,
                  paginate = True,
                  coid = 'ZTXA', 
                  mdate = {'gt':'2020-01-01', 'lt':'2022-08-31'},
                  opts={'columns':['coid','mdate','close_d']})
df3 = pd.merge(df2,df1,how='outer')
df4 = pd.merge(df3, df, how='outer') #将抓下来的技术指标合起来
df4.set_index(df4["日期"], inplace=True) 
df4.drop(columns={'日期'}, inplace=True)
df4
df4资料表
df4资料表

将符合日绝对走多跟走空的股票筛选出来
走多条件:日K>日D , 日差离值>日MACD,日收盘价>10日移动平均
走空条件:日K<日D , 日差离值<日MACD,日收盘价<10日移动平均

condition1 = (df4["K值"] > df4["D值"]) &(df4["差离值"]> df4["MACD"]) & (df4["收盘价"] > df4["10日移动平均"])#日三线多指标的条件
condition2 = (df4["K值"] < df4["D值"]) &(df4["差离值"] < df4["MACD"]) & (df4["收盘价"] < df4["10日移动平均"])#日三线空指标的条件
long = df4[condition1] #把符合条件的筛选出来
short = df4[condition2]

用符合走多条件的股票数除以样本数乘以100计算出日三线多指标,以及用符合走空条件的股票数除以样本数乘以100计算出日三线空指标,并合并起来。

long1 = ((long.groupby('日期')['证券码'].count())/(df4.groupby('日期')['证券码'].count()))*100 #计算出日三线多指标
short1 =((short.groupby('日期')['证券码'].count())/(df4.groupby('日期')['证券码'].count()))*100 #计算出日三线空指标
long1.name = '日三线多指标' #重新命名
short1.name = '日三线空指标'
result = pd.concat([long1,short1], axis=1) #把两表合并起来
result
result结果
result结果

将大盘的收盘价与多空指标也合并起来,计算出日三线多空指标20日均线。

result1 = result.merge(Y9999[['日期', '收盘价(元)']], on='日期')
result1['日多20ma'] = result1['日三线多指标'].rolling(20).mean()
result1['日空20ma'] = result1['日三线空指标'].rolling(20).mean()
result1.set_index(result1['日期'], inplace=True)
result1.drop(columns = {'日期'}, inplace=True)

将计算出的指标与大盘收盘价绘图出来观察状况,从绘图出的结果来看可以发现日三线的多空指标,较适合用来作为逆势策略使用,因此在前面的进出场条件就是参考绘图结果来设定的。

fig, ax1= plt.subplots(figsize =(20,16))
plt.plot(result1.index , result1['日多20ma'],lw=1.5, label = '日三线多指标')       
plt.plot(result1.index , result1['日空20ma'],lw=1.5, label = '日三线空指标')  
plt.xlabel('日期',fontsize=15)
plt.ylabel('点数', fontsize=15)
plt.xticks(fontsize=15)
plt.yticks(fontsize=15)
plt.title('日三线多空与大盘指数',  fontsize=20)
plt.legend(loc=1, fontsize=15)
ax2 = ax1.twinx() #跟第一张ax1的x轴一样
plt.plot(result1.index, result1['收盘价(元)'] , lw=1.5, color='r', label='大盘')
plt.ylabel('股价', fontsize=15)
plt.yticks(fontsize=15)
plt.legend(loc=2, fontsize=15)
plt.gcf().autofmt_xdate() #让x轴的时间轴比较宽松、漂亮
plt.show()
Plt呈现画面
Plt呈现画面

参考[量化分析(十五)]量能回测实战的回测系统建立以及进出场点位的绘图方式,将进出场讯号写进函式里,并将几个常用的绩效指标也写进去,像是:交易次数、平均报酬率、累积报酬率、胜率以及买进持有报酬率等,方便投资人评断策略结果好坏,也可以根据需要做改写,其中n1跟n2为多空指标均线的参数,后续可以根据最佳化做调整。

def buysell(data,n1,n2):
    data =data.copy()
    buy=[]
    sell=[]
    hold=0
    data['日多ma'] = data['日三线多指标'].rolling(n1).mean()
    data['日空ma'] = data['日三线空指标'].rolling(n2).mean()
    data.dropna(inplace=True)
    for i in range(len(data)):
    
        if  data["日空ma"][i] > data["日多ma"][i]:
            sell.append(np.nan)
            if hold !=1:
                buy.append(data["收盘价"][i])
                
                hold = 1
            else: 
                buy.append(np.nan)
        elif data["日多ma"][i] > data["日空ma"][i] :
            buy.append(np.nan)
            if hold !=0:
                sell.append(data["收盘价"][i])
                hold = 0
            else:
                sell.append(np.nan)
        else:
            buy.append(np.nan)
            sell.append(np.nan)
    a=(buy,sell)
        
    data['Buy_Signal_Price']=a[0]
    data['Sell_Signal_Price']=a[1]
    data["买卖股数1"]=data['Buy_Signal_Price'].apply(lambda x : 1 if x >0 else 0)
    data["买卖股数2"]=data['Sell_Signal_Price'].apply(lambda x : -1 if x >0 else 0  )
    data["买卖股数"]=data["买卖股数1"]+ data["买卖股数2"]
    
    b = data[['Buy_Signal_Price']].dropna()
    c = data[['Sell_Signal_Price']].dropna()
    d = pd.DataFrame(index=c.index, columns = {"Buy_Signal_Price", 'Sell_Signal_Price'})
    for i in range(len(d.index)):
        d["Buy_Signal_Price"][i] = b["Buy_Signal_Price"][i]
        d['Sell_Signal_Price'][i] = c['Sell_Signal_Price'][i]
    d['hold_return'] = (d['Sell_Signal_Price']-d['Buy_Signal_Price'])/d['Buy_Signal_Price']
    win_rate = ((np.where(d['hold_return']>0, 1, 0).sum())/len(d.index))*100
    hold_ret = (d['hold_return'].sum())*100
    avg_ret = (d['hold_return'].mean())*100
    std = (d['hold_return'].std())*100
    
    final_equity = 10000
    for i in range(len(d.index)):
        final_equity = final_equity*(d['hold_return'][i]+1)
    cul_ret = ((final_equity-10000)/10000)*100
    y9999 = ((data['收盘价'][-1] - data['收盘价'][0])/data['收盘价'][0])*100
    
    #Visually show the stock buy and sell signal
    plt.figure(figsize=(20,16))
    # ^ = shift + 6
    plt.scatter(data.index,data['Buy_Signal_Price'],color='red', label='Buy',marker='^',alpha=1)
    #小写的v
    plt.scatter(data.index,data['Sell_Signal_Price'],color='green', label='Sell',marker='v',alpha=1)
    plt.plot(data['收盘价'], label='Close Price', alpha=0.35)
    plt.title('买卖讯号', fontsize=20)
    #字斜45度角
    plt.xticks(rotation=45)
    plt.xlabel('Date', fontsize=15)  
    plt.ylabel('Price',fontsize=15)
    plt.yticks(fontsize=15)
    plt.xticks(fontsize=15)
    plt.legend(fontsize=15)
    plt.grid()
    plt.gcf().autofmt_xdate()
    return print(" 持有期间报酬:",hold_ret,"n","平均报酬:",avg_ret,"n",'交易次数:',len(d.index),'n',"胜率:",win_rate,"n","标准差:",std,'n','权益总额:',final_equity,'n','累积报酬率:',cul_ret,'n','buy&hold:',y9999), plt.show()

从回测结果可以看到,这个策略的累积报酬率约为16.57%左右,输给买进持有策略28.95%,可见这个参数的结果并不理想,因此后续加入最佳化的方法来测试新的参数。

buysell(result1,20,20)
绩效报酬
绩效报酬
Buy & Sell 进出场图
Buy & Sell 进出场图

最佳化参数方法,透过设定参数区间来检视不同参数的累积报酬率,挑选其中最高的作为最适参数。

def optimal(data1,n1:range, n2:range):
    set_0 = 0
    for i in n1:
        for j in n2:
            data =data1.copy()
            buy=[]
            sell=[]
            hold=0
            data['日多ma'] = data['日三线多指标'].rolling(i).mean()
            data['日空ma'] = data['日三线空指标'].rolling(j).mean()
            data.dropna(inplace=True)
            for k in range(len(data)):
if  data["日空ma"][k] > data["日多ma"][k]:
                    sell.append(np.nan)
                    if hold !=1:
                        buy.append(data["收盘价"][k])
hold = 1
                    else: 
                        buy.append(np.nan)
                elif data["日多ma"][k] > data["日空ma"][k]:
                    buy.append(np.nan)
                    if hold !=0:
                        sell.append(data["收盘价"][k])
                        hold = 0
                    else:
                        sell.append(np.nan)
                else:
                    buy.append(np.nan)
                    sell.append(np.nan)
            a=(buy,sell)
data['Buy_Signal_Price']=a[0]
            data['Sell_Signal_Price']=a[1]
            data["买卖股数1"]=data['Buy_Signal_Price'].apply(lambda x : 1 if x >0 else 0)
            data["买卖股数2"]=data['Sell_Signal_Price'].apply(lambda x : -1 if x >0 else 0  )
            data["买卖股数"]=data["买卖股数1"]+ data["买卖股数2"]
b = data[['Buy_Signal_Price']].dropna()
            c = data[['Sell_Signal_Price']].dropna()
            d = pd.DataFrame(index=c.index, columns = {"Buy_Signal_Price", 'Sell_Signal_Price'})
            for l in range(len(d.index)):
                d["Buy_Signal_Price"][l] = b["Buy_Signal_Price"][l]
                d['Sell_Signal_Price'][l] = c['Sell_Signal_Price'][l]
            d['hold_return'] = (d['Sell_Signal_Price']-d['Buy_Signal_Price'])/d['Buy_Signal_Price']
final_equity = 10000
            for m in range(len(d.index)):
                final_equity = final_equity*(d['hold_return'][m]+1)
            cul_ret = ((final_equity-10000)/10000)*100
            if cul_ret>= set_0:
                set_0 = cul_ret
                n1_new = i
                n2_new = j
    return print(' n1:',n1_new,'n', 'n2:', n2_new,"n", '累积报酬:', set_0)

设定n1跟n2的范围为0到30来寻找累积报酬率最高的参数,可以看到最好的参数为n1=19, n2=13。

optimal(result1, n1=range(0,30), n2 =range(0,30))
绩效报酬
绩效报酬

再用新参数重新计算一次回测结果以及进出场点位,发现持有期间报酬到达到36.55%赢过buy&hold的 28.95%

buysell(result1,19,13)
绩效报酬
绩效报酬
Buy & Sell 进出场图
Buy & Sell 进出场图

结论

我们能看到在一开始设定的参数所做的均线交错策略,回测期间的累积报酬率仅12.75%,输了买进持有策略将近一倍之多,可以看到在2020/4到2021/4这一年的大反弹期间,我们的策略并没有吃到太多的涨幅,因此透过参数最佳化的方式来找寻最佳参数,经过最佳化后的结果可以看到,累积报酬率在回测期间为40.48%,赢过买进持有策略,可以看到2020/4到2021/4的这段涨幅也都有参与到,胜率也有69%左右,但有几点需要注意的是:

文章中回测的结果尚未考虑手续费的计算,因此实际交易结果还是需要考虑手续费。

最后,还是要再次提醒本文所提及之标的仅供说明使用,不代表任何金融商品之推荐或建议。因此,若读者对于建置策略、绩效回测、研究实证等相关议题有兴趣,欢迎选购 TEJ E Shop中的方案,具有齐全的资料库,就能轻易的完成各种检定。

完整程式码

延伸阅读

相关连结

返回总览页