大盤強弱指標回測實戰

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

完整程式碼

延伸閱讀

相關連結

返回總覽頁
Procesing