月營收成長率策略應用

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中的方案,具有齊全的資料庫,就能輕易的完成各種檢定。

完整程式碼

延伸閱讀

相關連結

返回總覽頁
Procesing