月营收成长率策略应用

Photo by Giorgio Tomassetti on Unsplash

 

本文重点概要

文章难度:★★☆☆☆

以月营收的月增率及年增率来作为股票进出场点味的交易策略
阅读建议:本文所建立的回测架构可以参考量化分析】大盘强弱指标回测实战里的讲解,而对于回测较不了解的读者,可以先行阅读量化分析】-技术分析简介与回测中的回测部分,可以更详细理解回测的执行流程。

前言

证券交易法第36条规定,上市柜公司应于每月十日前,公告并申报上个月的营运情形,月营收的资讯属于市场中较为特别的讯息,国外市场较少发布关于月营收的相关资料,因此尝试使用月营收相关资讯来帮助投资决策,或许会有不错的效果,因此本文利用月营收的年增率(yoy)和月增率(mom)来回测台湾上市柜公司在此策略下的报酬与胜率,由于mom的资料变动较大,因此参数选择10月平均,而yoy则采5月平均:

  1. 进场条件:mom大于10月mom平均且yoy大于5月yoy平均
  2. 出场条件:mom小于10月mom平均且yoy小于5月yoy平均

编辑环境及模组需求

本文使用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/APRCD1)
月营收盈余(TWN/ASALE)

资料导入

自2013年起,月营收资料揭露方式改成以合并为主的资料替代,因此资料期间我们选2013年4月到2021年底,为了使后面回圈程式码较简洁,将资料抓取的部分写成函式。

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()  #取得上市柜股票证券码
def get_data(code:str, id_):
    df = tejapi.get(code, #从TEJ api捞取所需要的资料
                  chinese_column_name = True,
                  paginate = True,
                  mdate = {'gt':'2013-04-01', 'lt':'2021-12-31'},
                  coid=id_,
                  opts={'columns':['coid','mdate','close_adj']})
    return df
def get_data1(code:str, id_):
    df = tejapi.get(code, #从TEJ api捞取所需要的资料
                  chinese_column_name = True,
                  paginate = True,
                  mdate = {'gt':'2013-04-01', 'lt':'2021-12-31'},
                  coid=id_,
                  opts={'columns':['coid','annd_s', 'd0003', 'd0004']})
    return df

计算出yoy的5个月平均以及mom的10个月平均,并将计算好的指标与股票收盘价合并,由于营收公布往往是收盘之后,如果设定营收发布日买进的话,会有偏误存在,因此将指标往后移动一日,设定成营收发布隔天买进,更符合实际交易情形。

df_1 = get_data('TWN/APRCD1', i)
df = get_data1('TWN/ASALE', i)
df.rename(columns={'营收发布日':'年月日','单月营收成长率%':'yoy', '单月营收与上月比%':'mom', '公司':'证券代码'}, inplace=True)
df['yoy3'] = df['yoy'].rolling(5).mean()
df['mom3'] = df['mom'].rolling(10).mean()
df2 = df_1.merge(df, on=['证券代码', '年月日'], how='outer')
df2 = df2.sort_values(by='年月日')
df2[['yoy', 'mom','yoy3','mom3']] = df2[['yoy', 'mom','yoy3',"mom3"]].shift(1)
df3 = df2.dropna()
df3.set_index(df3['年月日'], inplace=True)
df3.drop(columns={'年月日'}, inplace=True)

回测系统部分可以参考【量化分析】大盘强弱指标回测实战中的回测系统建立,以下仅将有改变的地方呈现出来,当yoy大过5月yoy平均和mom大过10月mom平均即产生买入讯号,反之则卖出,完整程式码会放在最下面。

for i in range(len(data)):
    
        if  (data["yoy"][i] > data["yoy3"][i]) & (data["mom"][i] > data["mom3"][i]):
            sell.append(np.nan)
            if hold !=1:
                buy.append(data["收盘价(元)"][i])
                
                hold = 1
            else: 
                buy.append(np.nan)
        elif (data["yoy"][i] < data["yoy3"][i]) & (data["mom"][i] < data["mom3"][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)

将前面的函式以及资料处理过程写成回圈,一次把所有上市柜的结果跑出来。

qq = pd.DataFrame()
for i in twid[:]:
    df_1 = get_data("TWN/APRCD1",i)
    df = get_data1("TWN/ASALE",i)
    df.rename(columns={'营收发布日':'年月日','单月营收成长率%':'yoy', '单月营收与上月比%':'mom', '公司':'证券代码'}, inplace=True)
    df['yoy3'] = df['yoy'].rolling(5).mean()
    df['mom3'] = df['mom'].rolling(10).mean()
    df2 = df_1.merge(df, on=['证券代码', '年月日'], how='outer')
    df2 = df2.sort_values(by='年月日')
    df2[['yoy', 'mom','yoy3','mom3']] = df2[['yoy', 'mom','yoy3',"mom3"]].shift(1)
    df3 = df2.dropna()
    df3.set_index(df3['年月日'], inplace=True)
    df3.drop(columns={'年月日'}, inplace=True)
    qq = qq.append(buysell(df3, i))
    print(i)
qq.index.name = '证券码'

跑出来的结果可以一次看到所有上市柜公司使用这个策略所获得的总报酬率以及胜率。

确认是否有缺失值,并将其删除。
确认是否有缺失值,并将其删除。
print(qq.isna().sum())
qq.dropna(inplace=True)

将所有标的的平均报酬率和胜率算出来观察,可以看到平均胜率约是52%,报酬率为66%左右。

print('胜率:',qq['胜率'].mean())
print('报酬率:',qq['报酬'].mean())
胜率、报酬率图
胜率、报酬率图

将报酬率为正及胜率大于50%的公司数计算出来,大约占了所有公司的6成左右,可见此营收策略在6成左右的上市柜公司中,都可以有胜率大于50%且报酬率为正的效果。

qq['count'] = np.where( (qq['报酬']>0) &(qq['胜率']>= 50),1,0)
(qq['count'].sum()/qq['count'].count())*100

将胜率切成五等分,并将每个胜率区间的股票数计算出来,用来后续画成圆饼图。

qq['20%'] = np.where((qq['胜率']>= 80),1,0)
qq['40%'] = np.where((qq['胜率']>= 60)& (qq['胜率']< 80),1,0)
qq['60%'] = np.where((qq['胜率']>= 40)& (qq['胜率']< 60),1,0)
qq['80%'] = np.where((qq['胜率']>= 20)& (qq['胜率']< 40),1,0)
qq['100%'] = np.where((qq['胜率']<20),1,0)
z5 = [qq['20%'].sum(),qq['40%'].sum(), qq['60%'].sum(), qq['80%'].sum(),qq['100%'].sum()]

将结果用圆饼图呈现,可以看到胜率大于60%的部分就占了约1/3,可见是一个胜率不低的策略。

plt.figure(figsize=(8,10))
plt.pie(z5,
        radius=1,
        labels=['胜率>80%','80%>胜率>60%','60%>胜率>40%','40%>胜率>20%','胜率<20%'],
        autopct='%.1f%%',    # %.1f%% 表示显示小数点一位的浮点数,后方加上百分比符号     
        pctdistance = 0.6,             
        textprops = {"fontsize" : 18})  # 文字大小)   
plt.title('月营收成长率策略胜率占比', {"fontsize":18})
plt.legend(loc = "best")
plt.axis('equal')
        
plt.show()
月营收成长率策略胜率占比
月营收成长率策略胜率占比

结论

我们能看到这个月营收成长率的策略,胜率大于50%且报酬率为正的公司就占了约60%,可见有6成的公司是有机会使用这个策略获利的,而胜率大于60%的公司也占了上市柜公司的1/3,后续还可以透过参数最佳化的方法来得到更高的报酬率和胜率,需要注意的部分是,此处的回测尚未考虑手续费的问题,实际交易结果仍须将手续费的成本加入。

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

完整程式码

延伸阅读

相关连结

返回总览页