【手把手系列】布林通道策略 + 回測教學
這篇我們要來介紹布林通道,使用的資料為過去一個月的一分K資料並且新增空單進場的情況,在本篇中我們使用台指期貨當作範例,打開VolTraderSample下載好course4範例檔案夾,下面我們來介紹策略
布林通道
1. 布林通道的定義為,K為標準差數量,N為過去幾筆資料:
中軌 = N時間段的簡單移動平均線
上軌 = 中軌 + K * N時間段的標準差
下軌 = 中軌 - K * N時間段的標準差
2. 圖中藍線是上軌、橘線是中軌、綠色是下軌,常用的計算方式為N = 20、K = 2,也就是說中軌的每個點都是前20筆價格的平均
3. 標準差的計算方式為N時間段的每筆數值減去平均值的平方和再除以N後開根號,計算出來的標準差代回到布林通道的公式中就可以得到上下軌
4. 理想的市場價格在上軌與下軌中間,當價格超過上軌時很有可能未來會有下跌的趨勢,反之當價格低於下軌時未來有可能會有上漲的趨勢
交易策略
1. 本篇運用的基本交易邏輯為市場收盤價超過布林通道的上軌時就在下一分開盤時進行賣出,反之如果市場收盤價格低於布林通道的下軌時下一分開盤進行買入
2. 除了第一筆交易是買或賣一口外,其餘情況都是買賣兩口,也就是說如果有買進訊號,就從原本做空買兩口變成做多,反之賣出時從做多賣兩口變做空
3. 範例中使用參數值為K=2、N=20,相當於過去20筆歷史資料的平均值以及兩個標準差來計算布林通道
4. 最後我們會示範增加止損價及濾網後的回測績效,止損的的條件是買多後市場價跌超過100點則在下一分鐘賣出兩口轉為做空,反之如果空單進場後價格上漲超過100點則在下一分鐘買進兩口轉為做多,濾網則是新增10、50、80均線,除了碰到下軌外還要符合10>20(中軌)>50>80均線的情況才做多,空單進場的條件也改為除了碰到上軌外,也要滿足10<20(中軌)<50<80均線的條件才空單進場
訂閱歷史資料
範例為1分K的策略,我們下載1分K歷史資料並保存下來,這次我們把讀取歷史資料跟回測的程式合併為一個檔案,下面講解sample中的bbandstrategy.py檔案:
1. 匯入訂閱歷史資料和回測會用到的套件
# 載入 tcoreapi
from tcoreapi_mq import *
# 載入我們提供的行情 function
from quote_functions import *
import threading
import numpy as np
import talib
import pandas as pd
import matplotlib.pyplot as plt
import mplfinance as mpf
from datetime import timedelta
2. 我們使用台指期貨的合約代碼,ktype 設定 1K,取2022/6/27早上8點45分到2022/7/26凌晨5點的歷史資料。
sD 與 eD參數值需要填寫四位年份+兩位月份+兩位日期+兩位小時,這裡需要注意的是從VolTrader取出來的歷史資料時區是UTC+0,因此要減掉8小時才能取得正確的歷史資料,設置好參數後代入GetHistory得到1分K的歷史資料
testSymbol = 'TC.F.TWF.TXF.HOT'
# 回補區間設定
# yyyymmddHH, HH 00-23
ktype = '1K'
sD = '2022062700'
eD = '2022072521'
# 顯示歷史數據
history_data = GetHistory(g_QuoteZMQ, g_QuoteSession, testSymbol, ktype, sD, eD)
3. 利用pd.to_datetime轉換成時間格式後用timedelta加8小時變成台灣時間。
mpf.plot需要的時間格式為datetime,因此我們需要將int轉換成datetime格式,先把日期跟時間合併,將日期乘以一萬倍預留時跟分的位置,再加上時間除以100倍也就是時跟分,由於分K只有到分的資料秒數預設為0,所以這裡我們不取秒數。
history_data.index = pd.to_datetime(history_data["Date"] * 10000 + history_data["Time"] / 100,
format='%Y%m%d%H%M') + timedelta(hours=8)
4. 訂閱完歷史資料就可以解訂閱歷史數據並進行登出
# 解除訂閱歷史數據
g_QuoteZMQ.UnsubHistory(g_QuoteSession, testSymbol, ktype, sD, eD)
# 登出帳號
g_QuoteZMQ.Logout(g_QuoteSession)
回測
下面我們進行回測的講解:
1. 設定各項參數、初始資金、買賣日期價格、手續費等等。
這裡要注意買多空單進場的投資報酬率分別計算,ROI紀錄每一次賣出時的投資報酬率無論多單出場或空單出場
# 設定各項初始參數及列表
# 設定起始資金
cash = 100000
# 計算策略賺錢和賠錢的次數
wincounts = 0
wintotal = 0
losscounts = 0
losstotal = 0
# 紀路起始資金與每次賣出後的現金數
cashlist = [cash]
# 買進日期及價格
buy_date = []
buyprice = []
# 賣出日期及價格
sell_date = []
sellprice = []
# 空單進場日期及價格
buyshort_date = []
buyshortprice = []
# 空單出場日期及價格
sellshort_date = []
sellshortprice = []
# 投資報酬率
ROI = []
ROI_long = []
ROI_short = []
# 買進賣出訊號點
up_markers = []
down_markers = []
# 設定每點價值(契約乘數),台指期一口兩百元
tick_price = 200
# 設定手續費
fees = 50
# 是否持倉,0代表沒有,1則是有持倉
pos = 0
# 繪圖時訊號點距離價格的位置
point = 1
2. 利用talib計算布林通道上中下軌道線的數值。
timeperiod是均線計算的時間範圍,輸入20就是前20筆資料的平均值,nbdevup和nbdevdn分別是上下軌道距離中軌道幾個標準差,我們使用常見的兩個標準差進行計算
# 計算布林通道上中下三條線
upperband, middleband, lowerband = talib.BBANDS(history_data["Close"],
timeperiod=20, nbdevup=2, nbdevdn=2, matype=0)
# 均線種類:matype 0:SMA 1:EMA 2:WMA ...
3. 計算目前價格與上下軌道的差距,判斷目前行情的位置。
我們用前市場價減去上軌以及市場價減去下軌的值,如果超過上軌的話diff的值會由負轉正,如果跌落下軌則是由正轉負
# 計算當前價格減去上軌跟下軌的值
diff = history_data["Close"] - upperband
diff2 = history_data["Close"] - lowerband
4. 判斷每個買入的時間點,並將買進時間點、價格、買進訊號記錄下來。
首先使用for迴圈走訪每一分鐘的市場價格,如果上分鐘市場價高於下軌且這分鐘市場價又低於下軌,且沒有持倉的情況下,則在下分鐘開盤進行買進
for i in range(len(history_data["Close"])):
if 1 < i < len(history_data["Close"]):
# 如果上分鐘市場價高於下軌且這分鐘市場價又低於下軌,
# 並且沒有做多的情況下,則在下分鐘開盤時買進
if diff2[i] < 0 < diff2[i - 1] and pos <= 0:
5. 空單出場、紀錄投資報酬率
考慮到買入時有兩種情況,一種是在空單進場的情況下買入兩口改為多單,另一種則是第一次進行多單進場沒有空單的情況下,如果是第一種情況我們就需要將空單出場的時間日期以及投資報酬率記錄下來
if pos == -1:
sellshort_date.append(history_data.index[i+1])
sellshortprice.append(history_data["Open"][i+1])
# 賣出後計算當前資金,1tick=200元,再扣掉買入及賣出各50元手續費
cash += (tick - history_data["Open"][i + 1]) * tick_price - 2 * fees
# 將賣出後的現金數紀錄到cashlist列表中
cashlist.append(cash)
# 投資報酬率增加賣出價格減買入價格除以初始資金
ROI_short.append((cashlist[-1] - cashlist[-2]) / cashlist[0])
ROI.append((cashlist[-1] - cashlist[-2]) / cashlist[0])
6. 多單進場
第一次買入需要扣除進場手續費並將ROI補nan,但無論是哪種情況都需要把買入時的價格、日期、買入訊號記錄下來,並且把賣出訊號補nan以及將持倉改為1
else:
# 多單進場手續費
cash -= fees
# 投資報酬率及賣出點增加nan值
ROI.append(np.nan)
# 紀錄買入時的價格
tick = history_data["Open"][i+1]
buy_date.append(history_data.index[i+1])
buyprice.append(history_data["Open"][i+1])
# 紀錄買入訊號點
up_markers.append(history_data["Low"][i+1] - point)
# 賣出點增加nan
down_markers.append(np.nan)
# 買入後有持倉設為1
pos = 1
7. 多單出場、空單進場
賣出也是類似的方法,判斷價格是否碰到下軌且沒有空單的情況下,下分鐘開盤就進行賣出,並記錄下賣出時的價格、時間點、和投資報酬率,記得賣出時買入訊號要加nan值,否則mplfinance在畫圖時沒辦法辨認出來資料大小而報錯
# 如果上分鐘市場價低於上軌且這分鐘市場價又高於上軌,
# 且在沒有做空的情況下,在下分鐘開盤賣出
elif diff[i] > 0 > diff[i - 1] and pos >= 0:
if pos == 1:
sell_date.append(history_data.index[i+1])
sellprice.append(history_data["Open"][i+1])
# 賣出後計算當前資金,1tick=200元,再扣掉買入及賣出各50元手續費
cash += (history_data["Open"][i + 1] - tick) * tick_price - 2 * fees
# 將賣出後的現金數紀錄到cashlist列表中
cashlist.append(cash)
# 投資報酬率增加賣出價格減買入價格除以初始資金
ROI_long.append((cashlist[-1] - cashlist[-2]) / cashlist[0])
ROI.append((cashlist[-1] - cashlist[-2]) / cashlist[0])
else:
# 空單進場手續費
cash -= fees
ROI.append(np.nan)
# 紀錄空單進場時的價格
tick = history_data["Open"][i]
# 買進點增加nan
up_markers.append(np.nan)
# 紀錄賣出訊號點
down_markers.append(history_data["High"][i+1] + point)
buyshort_date.append(history_data.index[i+1])
buyshortprice.append(history_data["Open"][i+1])
# 賣出後改為空單進場,將pos設為-1
pos = -1
8. mplfinance格式
其餘情況都需要將買賣訊號點跟投資報酬率補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)
9. 計算賺賠比
計算賣出價格高於買入的價格的次數,如果是空單進場的話計算方式變成買入價格高於賣出價格的次數,同時我們也將賠錢的次數保存下來,方便我們計算賺賠比
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]
for i in range(len(sellshortprice)):
if buyshortprice[i] > sellshortprice[i]:
wincounts += 1
wintotal += buyshortprice[i] - sellshortprice[i]
else:
losscounts += 1
losstotal += sellshortprice[i] - buyshortprice[i]
10. 交易紀錄
將非nan的投資報酬率、買入日期、買入價格、賣出日期和賣出價格放入pandas的資料表格中,轉置表格方便觀察交易紀錄,空單進場的情況另外放一個表格中
pd.set_option('display.unicode.ambiguous_as_wide', True)
pd.set_option('display.unicode.east_asian_width', True)
ROI_value = [x for x in ROI if np.isnan(x) == False]
d = [buy_date, buyprice, sell_date, sellprice, ROI_long]
df = pd.DataFrame(d, index=["買進日期", "買進價格", "賣出日期", "賣出價格", "投資報酬率"]).T
d2 = [buyshort_date, buyshortprice, sellshort_date, sellshortprice, ROI_short]
df2 = pd.DataFrame(d2,
index=["空單進場日期", "空單進場價格", "空單出場日期", "空單出場價格", "投資報酬率"]).T
11.輸出回測結果:最終持有資金、最終投資報酬率、勝率及賺賠比。
如果沒有交易的話最終投資報酬率跟勝率會因為不存在而報錯,所以這裡我們加入有投資報酬率的情況下才會計算最終投資報酬率跟勝率,賺賠比是平均獲利除以平均虧損,計算出來的結果是0.5535,也就是說以平均來說在虧損一元的情況下會獲利0.5535元,使用np.round取到小數點後四位數
print(df.to_string())
print(df2.to_string())
print("最終持有資金:", cash)
if ROI_value:
print("最終投資報酬率:", np.round(sum(ROI_value),4))
print("勝率:", np.round(wincounts / len(sellprice),4))
print("賺賠比:", np.round(wintotal * losscounts / (wincounts * losstotal),4))
12. 繪製圖表:圖表設定
將上中下軌道以及買入賣出訊號點和投資報酬率加入到added_plots中,再設定圖表的顏色與網格,接著畫出布林通道和投資報酬率,這裡我們加入inherit=True,可以讓K線圖的外框消失
# 想要增加的圖表
added_plots = {"上軌": mpf.make_addplot(upperband),
"中軌": mpf.make_addplot(middleband),
"下軌": mpf.make_addplot(lowerband),
"買入": mpf.make_addplot(up_markers, type='scatter', marker='^', markersize=200),
"賣出": mpf.make_addplot(down_markers, type='scatter', marker='v', markersize=200),
"ROI": mpf.make_addplot(ROI,type='scatter', panel=1)
}
# 設定圖表的顏色與網狀格格式
style = mpf.make_mpf_style(
marketcolors=mpf.make_marketcolors(up="r", down="g", inherit=True), gridcolor="gray")
# 畫布林線和成交量
fig, axes = mpf.plot(history_data, type="candle", style=style,
addplot=list(added_plots.values()),
returnfig=True)
13. 繪製圖表: 增加圖例與標籤
最後設定圖例和y軸標籤,使用plt.show將圖畫出來就可以看到下面的圖表了,最後一次空單進場後沒有平倉,總共買賣超過500次,增加濾網可以有效改善這種現象
# 設定圖例
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("ROI")
plt.show()
濾網與止損
上面回測完的結果不是很理想,不但頻繁交易甚至最終虧損六萬多元,想要改善的話需要用到濾網與止損,這兩項的方法在交易策略的第四點有說明,下面附上修改的程式碼:
1. 新增三條均線的計算,分別是10、50、80,加上原本布林通道計算出來的20均線總共四條用在判斷買賣時機,如果短均線大於長均線,也就是10日均線大於20日均線大於50日均線大於80日均線時並且低於下軌時加上沒有做多的情況下才會出現買進訊號,賣出訊號則是需要滿足長均線皆大於短均線且高於上軌加上沒有空單的情況
# 計算均線濾網
sma0 = talib.SMA(history_data["Close"], timeperiod=10)
sma1 = talib.SMA(history_data["Close"], timeperiod=50)
sma2 = talib.SMA(history_data["Close"], timeperiod=80)
2. 買進的部分從原本的一行改為同時滿足均線及布林通道的條件才觸發,這樣可以有效降低交易的頻率,另一方面如果空單進場後價格上漲超過100點(reverse_price可以自行調整)轉為買多,這樣做相當於止損,同樣套用到賣出時的判斷
reverse_price = 100
# 如果上分鐘市場價高於下軌且這分鐘市場價又低於下軌,且在短均線皆大於長均線、
# 沒有做多的情況下,或是空單價格上漲超過100點以上買進
if (diff2[i] < 0 < diff2[i - 1] and\
sma0[i] > middleband[i] > sma1[i] > sma2[i] and pos <= 0) or\
history_data["Open"][i] >= tick + reverse_price and pos == -1:
# 如果上分鐘市場價低於上軌且這分鐘市場價又高於上軌,且在短均線皆小於長均線、
# 沒有做空的情況下,或是做多後上下跌超過100點賣出
elif (diff[i] > 0 > diff[i - 1] and\
sma0[i] < middleband[i] < sma1[i] < sma2[i] and pos >= 0) or\
history_data["Open"][i] <= tick - reverse_price and pos == 1:
3. 增加濾網與止損後的回測圖形與績效如下圖所示
可以看到投資報酬率從-0.6155變成1.8885,交易次數也從500多減少到55次,可見濾網跟止損的重要性