Table of Contents
文章難度:★☆☆☆☆
以均線( MA )以及其正負標準差組成布林通道的上界、下界,透過這三條線繪製出布林通道,判斷進場、出場時機。
布林通道(Bollinger Band)是由 John Bollinger 在1980 年代所發明的技術指標。布林通道是由均線和統計學的標準差概念結合而成,均線 (Moving Average),簡稱 MA,代表過去一段時間的平均成交價格,一般來說在布林中使用的時間區段為近20日;標準差 (Standard Deviation),簡稱SD,常以σ作為代號,用於表示資料中數值的分散程度。
布林通道總共包含三條線:
● 上軌:20 MA+2 倍標準差
● 中軌:20 MA
● 下軌:20 MA — 2 倍標準差
由於在長時間觀測下,標的價格的變化會呈現常態分佈 (Normal Distribution),而根據統計學原理,在常態分佈下有95%的機率,資料會分布在平均值正負兩倍標準差(μ − 2σ, μ + 2σ)之間,也稱為95%的信賴區間,而布林通道正是以上述的統計學原理作為理論依據,發展出來的技術指標。
實際策略如下:
當收盤價觸碰到上界時,視為接下來可能會下跌的訊號,以隔日開盤價拋售持有部位。
當收盤價觸碰到下界時,視為接下來有可能谷底反彈的訊號,以隔日開盤價買入一單位。當滿足上述條件時,以及滿足本金充足、已持有部位與當日收盤價低於上次買入訊號收盤價時,則繼續加碼一單位。
本文使用Mac OS並以jupyter作為編輯器
import pandas as pd
import re
import numpy as np
import tejapi
from functools import reduce
import matplotlib.pyplot as plt
from collections import defaultdict, OrderedDict
from tqdm import trange, tqdm
import plotly.express as px
import plotly.graph_objects as go
tejapi.ApiConfig.api_key = "Your Key"
tejapi.ApiConfig.ignoretz = True
資料期間從2021–06–01至2022–12–31,以友達光電(2409)作為實例,抓取未調整的收盤價、BB-Upper(20)、BB-Lower(20)資料,並以報酬指數(Y9997)作為大盤進行績效比較。
stock = tejapi.get('TWN/APRCD',
paginate = True,
coid = '2409',
mdate = {'gte':'2021-06-01', 'lte':'2022-12-31'},
opts = {
'columns':[ 'mdate', 'open_d', 'high_d', 'low_d', 'close_d', 'volume']
}
)
ta = tejapi.get('TWN/AVIEW1',
paginate = True,
coid = '2409',
mdate = {'gte':'2021-06-01', 'lte':'2022-12-31'},
opts = {
'columns':[ 'mdate', 'bbu20', 'bbma20', 'bbl20']
}
)
market = tejapi.get('TWN/APRCD',
paginate = True,
coid = "Y9997",
mdate = {'gte':'2021-06-01', 'lte':'2022-12-31'},
opts = {
'columns':[ 'mdate', 'close_d', 'volume']
}
)
#將標的價格資訊與技術指標資訊以日期進行合併
data = stock.merge(ta, on = ['mdate'])
#將大盤價格資訊進行重新命名,與標的資料區別
market.columns = ['mdate', 'close_m', 'volume_m']
#將日期設為指標
data = data.set_index('mdate')
取得股價與技術指標資料後我們首先來繪製布林通道,這裡使用plotly.express來進行圖表的繪製。其中bbu20為20日布林通道上界、bbl20為通道的下界,而close_d為收盤價。
fig = px.line(data,
x=data.index,
y=["close_d","bbu20","bbl20"],
color_discrete_sequence = px.colors.qualitative.Vivid
)
fig.show()
這裏我們定義了幾個參數
● principal:本金
● position:股票部位持有張數
● cash:現金部位持有量
● order_unit:交易單位數
principal = 500000
cash = principal
position = 0
order_unit = 0
trade_book = pd.DataFrame()
for i in range(data.shape[0] -2):
cu_time = data.index[i]
cu_close = data.loc[cu_time, 'close_d']
cu_bbl, cu_bbu = data.loc[cu_time, 'bbl20'], data.loc[cu_time, 'bbu20']
n_time = data.index[i + 1]
n_open = data['open_d'][i + 1]
if position == 0: #進場條件
if cu_close <= cu_bbl and cash >= n_open*1000:
position += 1
order_time = n_time
order_price = n_open
order_unit = 1
friction_cost = (20 if order_price*1000*0.001425 < 20 else order_price*1000*0.001425)
total_cost = -1 * order_price * 1000 - friction_cost
cash += total_cost
trade_book = trade_book.append(
pd.Series(
[
stock_id, 'Buy', order_time, 0, total_cost, order_unit, position, cash
]), ignore_index = True)
elif position > 0:
if cu_close >= cu_bbu: # 出場條件
order_unit = position
position = 0
cover_time = n_time
cover_price = n_open
friction_cost = (20 if cover_price*order_unit*1000*0.001425 < 20 else cover_price*order_unit*1000*0.001425) + cover_price*order_unit*1000*0.003
total_cost = cover_price*order_unit*1000-friction_cost
cash += total_cost
trade_book = trade_book.append(pd.Series([
stock_id, 'Sell', 0, cover_time, total_cost, -1*order_unit, position, cash
]), ignore_index=True)
elif cu_close <= cu_bbl and cu_close <= order_price and cash >= n_open*1000: #加碼條件: 碰到下界,比過去買入價格貴
order_unit = 1
order_time = n_time
order_price = n_open
position += 1
friction_cost = (20 if order_price*1000*0.001425 < 20 else order_price*1000*0.001425)
total_cost = -1 * order_price * 1000 - friction_cost
cash += total_cost
trade_book = trade_book.append(
pd.Series(
[
stock_id, 'Buy', order_time, 0, total_cost, order_unit, position, cash
]), ignore_index = True)
if position > 0: # 最後一天平倉
order_unit = position
position = 0
cover_price = data['open_d'][-1]
cover_time = data.index[-1]
friction_cost = (20 if cover_price*order_unit*1000*0.001425 < 20 else cover_price*order_unit*1000*0.001425) + cover_price*order_unit*1000*0.003
cash += cover_price*order_unit*1000-friction_cost
trade_book = trade_book.append(
pd.Series(
[
stock_id, 'Sell',0, cover_time, cover_price*order_unit*1000-friction_cost, -1*order_unit, position, cash
]), ignore_index = True)
trade_book.columns = ['Coid', 'BuyOrSell', 'BuyTime', 'SellTime', 'CashFlow','TradeUnit', 'HoldingPosition', 'CashValue']
執行完上述的交易策略,接著就來看看在這段時間內我們進行交易資訊表,製作交易資訊表方法請詳見程式碼。
● BuyTime: 買入時間點
● SellTime: 賣出時間點
● CashFlow: 現金流出入量
● TradeUnit: 買進賣出張數
● HoldingPosition: 持有部位
● CashValue: 剩餘現金量
觀察以下圖表(繪圖方法請見下方程式碼),可以發現在2021年11月到2021年12月的上升區段(圖中淺藍色區域),由於收盤價無法碰觸到布林通道下界,因此一直沒有買入持有,導致無法賺取這區段的價差。
同樣的問題也出現在連續下降波段,比如2022年4月開始的下降趨勢(圖中淺綠色區域),不斷地碰觸布林通道下界,在回漲一小段後,因布林通道上界過低容易碰觸到,所以很快就賣出掉,導致這段期間的交易為負報酬。
事實上,由於20日布林通道的遲滯性,故無法反映短期高波動的價格變化,若您所分析的股票為漲跌幅度較大者,建議縮短布林通道的期間或搭配其他觀察趨勢的指標建立交易策略。
cash = principal
data_ = data.copy()
data_ = data_.merge(trade_book_, on = 'mdate', how = 'outer').set_index('mdate')
data_ = data_.merge(market, on = 'mdate', how = 'inner').set_index('mdate')
# fillna after merge
data_['CashValue'].fillna(method = 'ffill', inplace=True)
data_['CashValue'].fillna(cash, inplace = True)
data_['TradeUnit'].fillna(0, inplace = True)
data_['HoldingPosition'] = data_['TradeUnit'].cumsum()
# Calc strategy value and return
data_["StockValue"] = [data_['open_d'][i] * data_['HoldingPosition'][i] *1000 for i in range(len(data_.index))]
data_['TotalValue'] = data_['CashValue'] + data_['StockValue']
data_['DailyValueChange'] = np.log(data_['TotalValue']) - np.log(data_['TotalValue']).shift(1)
data_['AccDailyReturn'] = (data_['TotalValue']/cash - 1) *100
# Calc BuyHold return
data_['AccBHReturn'] = (data_['open_d']/data_['open_d'][0] -1) * 100
# Calc market return
data_['AccMarketReturn'] = (data_['close_m'] / data_['close_m'][0] - 1) *100
# Calc numerical output
overallreturn = round((data_['TotalValue'][-1] / cash - 1) *100, 4) # 總績效
num_buy, num_sell = len([i for i in data_.BuyOrSell if i == "Buy"]), len([i for i in data_.BuyOrSell if i == "Sell"]) # 買入次數與賣出次數
num_trade = num_buy #交易次數
avg_hold_period, avg_return = [], []
tmp_period, tmp_return = [], []
for i in range(len(trade_book_['mdate'])):
if trade_book_['BuyOrSell'][i] == 'Buy':
tmp_period.append(trade_book_["mdate"][i])
tmp_return.append(trade_book_['CashFlow'][i])
else:
sell_date = trade_book_["mdate"][i]
sell_price = trade_book_['CashFlow'][i] / len(tmp_return)
avg_hold_period += [sell_date - j for j in tmp_period]
avg_return += [ abs(sell_price/j) -1 for j in tmp_return]
tmp_period, tmp_return = [], []
avg_hold_period_, avg_return_ = np.mean(avg_hold_period), round(np.mean(avg_return) * 100,4) #平均持有期間,平均報酬
max_win, max_loss = round(max(avg_return)*100, 4) , round(min(avg_return)*100, 4) # 最大獲利報酬,最大損失報酬
winning_rate = round(len([i for i in avg_return if i > 0]) / len(avg_return) *100, 4)#勝率
min_cash = round(min(data_['CashValue']),4) #最小現金持有量
print('總績效:', overallreturn, '%')
print('交易次數:', num_trade, '次')
print('買入次數:', num_buy, '次')
print('賣出次數:', num_sell, '次')
print('平均交易報酬:', avg_return_, '%')
print('平均持有期間:', avg_hold_period_ )
print('勝率:', winning_rate, '%' )
print('最大獲利交易報酬:', max_win, '%')
print('最大損失交易報酬:', max_loss, '%')
print('最低現金持有量:', min_cash)
透過上述的程式碼,我們能得到下列資訊,可以在一年半間,交易次數僅為29次,或許可以採用上述改進方法,以增加進場次數。
#累積報酬圖
fig = go.Figure()
fig.add_trace(go.Scatter(
x = data_.index, y = data_.AccDailyReturn, mode = 'lines', name = '交易策略'
))
fig.add_trace(go.Scatter(
x = data_.index, y = data_.AccBHReturn, mode = 'lines', name = '買進持有'
))
fig.add_trace(go.Scatter(
x = data_.index, y = data_.AccMarketReturn, mode = 'lines', name = '市場大盤'
))
fig.update_layout(
title = stock_id + '累積報酬圖', yaxis_title = '累積報酬(%)', xaxis_title = '時間'
)
fig.show()
2021後半年到2022整年,對於友達來說是整體緩步向下的趨勢。若採用買進持有的策略,到期日所累績報酬為嚴重的-40%到-50%之間,相對的,採用布林通道交易策略之下,其表現是優於買進持有的。更甚者,雖然這一年半友達的表現不如市場大盤,然而在本次策略之下,卻是優於大盤表現的。
然而單純的布林通道策略,在下滑大趨勢區段中後的回升段中,容易有過早出場的劣勢,在上升區段中,容易有極少入場的窘境;故針對股價大幅變動的個股,建議多採用其他判斷趨勢強弱的指標,加以優化自身的策略。
溫馨提醒,本次策略與標的僅供參考,不代表任何商品或投資上的建議。之後也會介紹使用TEJ資料庫來建構各式指標,並回測指標績效,所以歡迎對各種交易回測有興趣的讀者,選購TEJ E-Shop的相關方案,用高品質的資料庫,建構出適合自己的交易策略。
電子報訂閱