【手把手系列】均線策略 + 回測教學

在上一篇章》達錢 Python 第一個指標(均線)中,我們已經學會如何使用talib計算均值後代入mplfinance的K線圖中畫出兩條均線,在這篇中我們要使用【黃金交叉以及死亡交叉】當作【買進賣出訊號】,再用歷史數據進行回測,用回測結果來評估我們的策略是否可行。

完整的範例程式在 VolTraderSample 的 course3 範例檔案夾。

交易策略(均線策略)

黃金交叉就是當短期均線由低往高穿過長期均線時,也就是說前天短均線的值小於長均線的值且昨天短均線大於長均線的值時,代表未來可能有上漲的趨勢,反之死亡交叉就是當期均線由高往低穿過長期均線時,代表未來可能會下跌,如果有黃金或死亡交叉時我們在隔天開盤時立刻買入或賣出,這篇範例使用五天短均線和十天長均線,所以至少需要12天的資料才能進行判斷。

歷史回測

接著我們要將策略套用到歷史數據上,利用pycharm選擇下載的資料夾的方法,可以參考達錢 Python 【行情&歷史數據】串接設定教學串接行情第2-4步驟,打開sample裡面的smastrategy.py檔案下面進行講解:

1. 這次我們需要使用numpy生成nan以及去除nan的功能所以先匯入numpy,下載步驟跟之前一樣使用pip install或在pycharm直譯器中下載都可以

import numpy as np
import talib
import pandas as pd
import matplotlib.pyplot as plt
import mplfinance as mpf

 

2. 接下來讀取歷史資料,可以參考第一篇【手把手系列】達錢 Python 【行情&歷史數據】串接設定教學,後面回測的時間範圍要在下載前進行調整

# 讀取歷史資料
history_data = pd.read_csv("history_data.csv", index_col="Date", parse_dates=True)

 

3. 設定起始資金十萬元、各項初始參數及列表,我們需要紀錄每次賣出的現金,買進日期、買進價格、賣出日期、賣出價格、賣出時的投資報酬率等等,先設置為0或空列表方便後面進行覆蓋或增加值,台指期貨契約倍率為200,買賣時手續費不會超過50,交易後改變持倉部位0代表沒有持倉1代表有持倉

# 設定各項初始參數及列表
# 設定起始資金
cash = 100000
# 計算策略賺錢的次數
wincounts = 0
# 紀路起始資金與每次賣出後的現金數
cashlist = [cash]
# 買進日期及價格
buy_date = []
buyprice = []
# 賣出日期及價格
sell_date = []
sellprice = []
# 投資報酬率
ROI = []
# 買進賣出訊號點
up_markers = []
down_markers = []
# 設定tick倍率
tick_price = 200
# 設定手續費
fees = 50
# 是否持倉,0代表沒有,1則是有持倉
pos = 0

 

4. 然後計算均線的資料,因為後面會頻繁使用,所以我們將時間週期設定為可變更參數,以後想修改只要在參數的值設定成想要的值就可以了,這邊我們使用五日均線跟十日均線為例

# 計算均線
t1 = 5
t2 = 10
globals()['sma_' + str(t1)] = talib.SMA(history_data["Close"], timeperiod=t1)
globals()['sma_' + str(t2)] = talib.SMA(history_data["Close"], timeperiod=t2)

 

5. 為了計算黃金交叉及死亡交叉時機,我們將短期均線減掉長期均線,如果差距由負轉正代表短均線的值原本小於長均線,隔天大於長均線的值,也就是黃金交叉出現,買進訊號出現隔天開盤我們進行買入,反之數值由正轉負代表死亡交叉出現,賣出訊號出現隔天開盤進行賣出

# 計算短期均線減長期均線的值
diff = globals()['sma_' + str(t1)] - globals()['sma_' + str(t2)]

 

6. 接下來我們進入策略的部分,使用for迴圈模擬每一天的情況,由於talib計算出來的均線值會自動將前幾天的數值補nan值,所以我們可以直接使用前兩天的值進行判斷,如果沒有數值的話就不會有買進賣出的訊號點,因此i從2開始迭代,實際上前11天不會發生交易,直到第12天開始才有機會買進賣出

for i in range(len(history_data["Close"])):
    if 1 < i < len(history_data["Close"]):

 

7. 買入的情況是當前天的短期均線低於長期均線時以及昨天的短期均線高於長期均線時(黃金交叉),並且當前沒有持倉的情況下就以今天的開盤價進行買入,買入時紀錄買進日期和價格,還有買進的訊號點,再將持倉更改為1,投資報酬率跟賣出訊號增加nan值,第九點會講解原因

        # 如果前天的短期均線低於長期均線時以及昨天的短期均線高於長期均線時(黃金交叉),並且當前沒有持倉的情況下就以今天的開盤價進行買入
        if diff[i - 1] > 0 > diff[i - 2] and pos == 0:
            buy_date.append(history_data.index[i])
            buyprice.append(history_data["Open"][i])

            # 開盤價-200的位置紀錄買入訊號點
            up_markers.append(history_data["Open"][i] - 200)
            # 投資報酬率及賣出點增加nan值
            ROI.append(np.nan)
            down_markers.append(np.nan)
            # 紀錄買入時的價格
            tick = history_data["Open"][i]
            # 買入後有持倉設為1
            pos = 1

 

8. 反之如果前天的短期均線高於長期均線且昨天的短期均線低於長期均線(死亡交叉),或今天是回測的最後一天,且在有持倉的情況下,今天就以開盤價的價格賣出,賣出時也要記錄賣出日期、價格以及賣出後的現金數,再將持倉數改為0,將賣出價格減去買入價格後再除以初始資金計算出來的投資報酬率增加至列表中

        # 如果前天的短期均線高於長期均線且昨天的短期均線低於長期均線(死亡交叉),或今天是最後一天,且在有持倉的情況下,今天就以開盤價的價格賣出
        elif (diff[i - 1] < 0 < diff[i - 2] or i == len(history_data["Close"]) - 1) and pos == 1:
            sell_date.append(history_data.index[i])
            sellprice.append(history_data["Open"][i])
            # 買進點增加nan
            up_markers.append(np.nan)
            # 開盤價+200的位置紀錄賣出訊號點
            down_markers.append(history_data["Open"][i] + 200)
            # 賣出後計算當前資金,1tick=200元,再扣掉買入及賣出各50元手續費
            cash += (history_data["Open"][i] - tick) * tick_price - 2 * fees
            # 將賣出後的現金數紀錄到cashlist列表中
            cashlist.append(cash)
            # 投資報酬率增加賣出價格減買入價格除以初始資金
            ROI.append((cashlist[-1] - cashlist[-2]) / cashlist[0])
            # 賣出後沒有持倉,將pos設為0
            pos = 0

 

9. 在沒有買賣時我們依然要將投資報酬率和訊號點設為np.nan,這是因為mplfinance沒辦法將不同長度的資料同時畫出,因此我們需要將三種列表都補到跟歷史資料同樣大小才能增加到圖表中

        else:
            # 其餘情況增加nan值
            ROI.append(np.nan)
            up_markers.append(np.nan)
            down_markers.append(np.nan)
    else:
        # 其餘情況增加nan值
        ROI.append(np.nan)
        up_markers.append(np.nan)
        down_markers.append(np.nan)

 

10. 使用for迴圈將有賺錢的次數跟賠錢的次數分別加總,後面計算勝率及賺賠比時方便計算

for i in range(len(sellprice)):
    if sellprice[i] > buyprice[i]:
        wincounts += 1
        wintotal += sellprice[i] - buyprice[i]
    else:
        losscounts += 1
        losstotal += buyprice[i] - sellprice[i]

 

11. 將投資報酬率非nan的值提取出來,並將買進賣出時間、價格和投資報酬率放入表格中,再將表格和最終投資報酬率、持有資金、勝率print出來

ROI_value = [x for x in ROI if np.isnan(x) == False]
d = [buy_date, buyprice, sell_date, sellprice, ROI_value]
df = pd.DataFrame(d, index=["買進日期", "買進價格", "賣出日期", "賣出價格", "投資報酬率"])
print(df.to_string())
print("最終投資報酬率:", sum(ROI_value))
print("最終持有資金:", cash)
print("勝率:", wincounts / len(sellprice))
print("賺賠比:", wintotal * losscounts / (wincounts * losstotal))

 

12. 設定想要增加的圖表,前兩項跟上一篇相同,這邊我們將收盤價去掉改成買入、賣出訊號點和投資報酬率,ROI中的panel=2代表我們指定他新增的位置為第三格,第一格為K線及均線的位置,而第二格為交易量,所以投資報酬率在最底下

# 想要增加的圖表
added_plots = {"SMA" + str(t1): mpf.make_addplot(globals()['sma_' + str(t1)]),
               "SMA" + str(t2): mpf.make_addplot(globals()['sma_' + str(t2)]),
               "Buy": mpf.make_addplot(up_markers, type='scatter', marker='^', markersize=100),
               "Sell": mpf.make_addplot(down_markers, type='scatter', marker='v', markersize=100),
               "ROI": mpf.make_addplot(ROI, type='scatter', panel=2)
               }

 

13. 設定圖表顏色與網格、畫K線和均線圖、設定圖例,這邊多加一個參數inherit=True,可以讓K線邊框消失,並且讓成交量也變成紅綠色,最後增加y座標標籤ROI

# 設定圖表的顏色與網狀格格式
style = mpf.make_mpf_style(marketcolors=mpf.make_marketcolors(up="r", down="g", inherit=True), gridcolor="gray")

# 畫K線和均線圖
fig, axes = mpf.plot(history_data, type="candle", style=style,
                     addplot=list(added_plots.values()),
                     volume=True,
                     returnfig=True)

# 設定圖例
axes[0].legend([None] * (len(added_plots) + 2))
handles = axes[0].get_legend().legendHandles
axes[0].legend(handles=handles[2:], labels=list(added_plots.keys()))

axes[0].set_ylabel("Price")
axes[2].set_ylabel("Volume")
axes[4].set_ylabel("ROI")
plt.show()

 

14. 使用plt.show畫出圖後,我們可以看到藍色箭頭是買入訊號、橘色箭頭是賣出訊號,最下面藍色的點市每次賣出時的投資報酬率

 

15. 到這裡我們已經畫好我們的策略回測結果了,投資報酬率以及買賣價格、日期都以表格的形式顯示出來,最後需要注意的是期貨是開槓桿的,所以高報酬的同時也高風險,只要改一下t1跟t2的數值,投資報酬率從三點多掉到負的都有可能,另外歷史回測的結果不一定套用的到未來的市場上,畢竟市場上什麼都有可能發生,也有可能出現歷史數據中沒有出現的情況,因此回測結果再好的策略也有虧損的風險

 

下面提供smastrategy.py完整程式碼:

* 請注意!本教學提供之範例程式僅供測試與學習使用,請勿直接使用在實盤交易。

import numpy as np
import talib
import pandas as pd
import matplotlib.pyplot as plt
import mplfinance as mpf

# 讀取歷史資料
history_data = pd.read_csv("history_data.csv", index_col="Date", parse_dates=True)

# 設定起始資金
cash = 100000

# 設定各項初始參數及列表
# 計算策略賺錢的次數
wincounts = 0
wintotal = 0
losscounts = 0
losstotal = 0
# 紀路起始資金與每次賣出後的現金數
cashlist = [cash]
# 買進日期及價格
buy_date = []
buyprice = []
# 賣出日期及價格
sell_date = []
sellprice = []
# 投資報酬率
ROI = []
# 買進賣出訊號點
up_markers = []
down_markers = []
# 設定tick倍率
tick_price = 200
# 設定手續費
fees = 50
# 是否持倉,0代表沒有,1則是有持倉
pos = 0

# 計算均線
t1 = 5
t2 = 10
globals()['sma_' + str(t1)] = talib.SMA(history_data["Close"], timeperiod=t1)
globals()['sma_' + str(t2)] = talib.SMA(history_data["Close"], timeperiod=t2)

# 計算短期均線減長期均線的值
diff = globals()['sma_' + str(t1)] - globals()['sma_' + str(t2)]

for i in range(len(history_data["Close"])):
    if 1 < i < len(history_data["Close"]):
        # 如果前天的短期均線低於長期均線時以及昨天的短期均線高於長期均線時(黃金交叉),並且當前沒有持倉的情況下就以今天的開盤價進行買入
        if diff[i - 1] > 0 > diff[i - 2] and pos == 0:
            buy_date.append(history_data.index[i])
            buyprice.append(history_data["Open"][i])

            # 開盤價-200的位置紀錄買入訊號點
            up_markers.append(history_data["Open"][i] - 200)
            # 投資報酬率及賣出點增加nan值
            ROI.append(np.nan)
            down_markers.append(np.nan)
            # 紀錄買入時的價格
            tick = history_data["Open"][i]
            # 買入後有持倉設為1
            pos = 1

        # 如果前天的短期均線高於長期均線且昨天的短期均線低於長期均線(死亡交叉),或今天是最後一天,且在有持倉的情況下,今天就以開盤價的價格賣出
        elif (diff[i - 1] < 0 < diff[i - 2] or i == len(history_data["Close"]) - 1) and pos == 1:
            sell_date.append(history_data.index[i])
            sellprice.append(history_data["Open"][i])
            # 買進點增加nan
            up_markers.append(np.nan)
            # 開盤價+200的位置紀錄賣出訊號點
            down_markers.append(history_data["Open"][i] + 200)
            # 賣出後計算當前資金,1tick=200元,再扣掉買入及賣出各50元手續費
            cash += (history_data["Open"][i] - tick) * tick_price - 2 * fees
            # 將賣出後的現金數紀錄到cashlist列表中
            cashlist.append(cash)
            # 投資報酬率增加賣出價格減買入價格除以初始資金
            ROI.append((cashlist[-1] - cashlist[-2]) / cashlist[0])
            # 賣出後沒有持倉,將pos設為0
            pos = 0
        else:
            # 其餘情況增加nan值
            ROI.append(np.nan)
            up_markers.append(np.nan)
            down_markers.append(np.nan)
    else:
        # 其餘情況增加nan值
        ROI.append(np.nan)
        up_markers.append(np.nan)
        down_markers.append(np.nan)

for i in range(len(sellprice)):
    if sellprice[i] > buyprice[i]:
        wincounts += 1
        wintotal += sellprice[i] - buyprice[i]
    else:
        losscounts += 1
        losstotal += buyprice[i] - sellprice[i]

ROI_value = [x for x in ROI if np.isnan(x) == False]
d = [buy_date, buyprice, sell_date, sellprice, ROI_value]
df = pd.DataFrame(d, index=["買進日期", "買進價格", "賣出日期", "賣出價格", "投資報酬率"])
print(df.to_string())
print("最終投資報酬率:", sum(ROI_value))
print("最終持有資金:", cash)
print("勝率:", wincounts / len(sellprice))
print("賺賠比:", wintotal * losscounts / (wincounts * losstotal))

# 想要增加的圖表
added_plots = {"SMA" + str(t1): mpf.make_addplot(globals()['sma_' + str(t1)]),
               "SMA" + str(t2): mpf.make_addplot(globals()['sma_' + str(t2)]),
               "Buy": mpf.make_addplot(up_markers, type='scatter', marker='^', markersize=100),
               "Sell": mpf.make_addplot(down_markers, type='scatter', marker='v', markersize=100),
               "ROI": mpf.make_addplot(ROI, type='scatter', panel=2)
               }
# 設定圖表的顏色與網狀格格式
style = mpf.make_mpf_style(marketcolors=mpf.make_marketcolors(up="r", down="g", inherit=True), gridcolor="gray")

# 畫K線和均線圖
fig, axes = mpf.plot(history_data, type="candle", style=style,
                     addplot=list(added_plots.values()),
                     volume=True,
                     returnfig=True)

# 設定圖例
axes[0].legend([None] * (len(added_plots) + 2))
handles = axes[0].get_legend().legendHandles
axes[0].legend(handles=handles[2:], labels=list(added_plots.keys()))

axes[0].set_ylabel("Price")
axes[2].set_ylabel("Volume")
axes[4].set_ylabel("ROI")
plt.show()