Table of Contents
「前視偏誤」(Look-ahead bias)指的是在歷史分析或決策過程中,不知情地使用未來或無法獲得的資料或信息的錯誤。它出現在當資訊或數據在特定時間點上是未知或不可得知的情況下,被用於做出決策或進行評估,就好像這些資訊在當時已經知道了一樣。
前視偏誤可能會導致結果扭曲和誤導,因為它違反了只使用分析時可用的資訊的原則。它可能出現在各個領域,包括金融、經濟和數據分析,並可能影響投資策略、交易系統的回測、績效評估和研究研究。
本次實作將展現金融領域中一個常見的前視偏誤 ━ 在測試交易策略時使用歷史價格或交易數據,但該數據包含未來價格變動的信息,而這些信息在當時是不可得知的。這可能導致人為膨脹的績效結果和對策略盈利能力的不切實際的期望。
本文使用 Mac 作業系統以及 Jupyter Notebook 作為編輯器。
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 api key"
tejapi.ApiConfig.ignoretz = True
資料期間從 2020–01–01 至 2023–06–30,以陽明海運 (2609) 作為實例,抓取未調整的收盤價、BB-Upper(20)、BB-Lower(20)資料,並以報酬指數(Y9997)作為大盤進行績效比較。
stock_id = "2609"
gte, lte = '2020-01-01', '2023-06-30'
stock = tejapi.get('TWN/APRCD',
paginate = True,
coid = stock_id,
mdate = {'gte':gte, 'lte':lte},
opts = {
'columns':[ 'mdate', 'open_d', 'high_d', 'low_d', 'close_d', 'volume']
}
)
ta = tejapi.get('TWN/AVIEW1',
paginate = True,
coid = stock_id,
mdate = {'gte':gte, 'lte':lte},
opts = {
'columns':[ 'mdate', 'bbu20', 'bbma20', 'bbl20']
}
)
market = tejapi.get('TWN/APRCD',
paginate = True,
coid = "Y9997",
mdate = {'gte':gte, 'lte':lte},
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()
本次實作將以陽明海運股價資料實作兩種布林通道交易策略,比較兩者差異
由於兩交易策略的差異僅僅在於交易時的單位價格不同,因此我們修改先前文章中的交易策略,將整個交易策略包裝成一個函式 bollingeband_strategy,在程式碼中增加一個 if 判斷式,以函式的輸入參數 — mode 控制條件,當 mode 等於 True 時,執行 策略 1,當mode 等於 False 時,執行 策略 2。
def bollingeband_strategy(data, principal, cash, position, order_unit, mode):
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']
if mode:
n_time = data.index[i + 1]
n_open = data['open_d'][i + 1]
else:
n_time = data.index[i]
n_open = data['close_d'][i]
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 = pd.concat([trade_book,
pd.DataFrame([stock_id, 'Buy', order_time, 0, total_cost, order_unit, position, cash])],
ignore_index = True, axis=1)
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 = pd.concat([trade_book,
pd.DataFrame([stock_id, 'Sell', 0, cover_time, total_cost, -1*order_unit, position, cash])],
ignore_index = True, axis=1)
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 = pd.concat([trade_book,
pd.DataFrame([stock_id, 'Buy', order_time, 0, total_cost, order_unit, position, cash])],
ignore_index = True, axis=1)
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 = pd.concat([trade_book,
pd.DataFrame([stock_id, 'Sell',0,cover_time, cover_price*order_unit*1000-friction_cost, -1*order_unit, position, cash])],ignore_index=True, axis=1)
trade_book = trade_book.T
trade_book.columns = ['Coid', 'BuyOrSell', 'BuyTime', 'SellTime', 'CashFlow','TradeUnit', 'HoldingPosition', 'CashValue']
return trade_book
接著,定義函式 simplify 簡化產出報表資訊,以方便閱讀。
def simplify(trade_book):
trade_book_ = trade_book.copy()
trade_book_['mdate'] = [trade_book.BuyTime[i] if trade_book.BuyTime[i] != 0 else trade_book.SellTime[i] for i in trade_book.index]
trade_book_ = trade_book_.loc[:, ['BuyOrSell', 'CashFlow', 'TradeUnit', 'HoldingPosition', 'CashValue' ,'mdate']]
return trade_book_
最後是績效計算,由於 pandas 的套件版本更新,目前最新版本已不支援 append 這個功能,因此我們小幅度修改程式碼使其能順利運行,並將程式同樣地打包成一個函式 back_test。
def back_test(principal, trade_book_, data, market):
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)
到此所有流程的程式碼架構都已經撰寫完畢,接下來就實際將資料放入模型,進行回測比較績效差異。
以隔日開盤價進行交易
principal = 500000
cash = principal
position = 0
order_unit = 0
trade_book = bollingeband_strategy(data, principal, cash, position, order_unit, True)
trade_book_ = simplify(trade_book)
back_test(principal, trade_book_, data, market)
以當日收盤價進行交易
principal = 500000
cash = principal
position = 0
order_unit = 0
trade_book_cu_close = bollingeband_strategy(data, principal, cash, position, order_unit, False)
trade_book_cu_close_ = simplify(trade_book_cu_close)
back_test(principal, trade_book_cu_close_, data, market)
觀察兩種交易策略的結果,可以發現以當日收盤價進行交易的總績效較優。股市新手在使用歷史回測資料進行回測可能會錯誤以當日收盤價作為交易價格,而忽略了在實際市場進行操作時,不可能事先得知當日收盤價格是多少的,使用交易當下尚不可知的資料進行回測,即構成了所謂的「前視偏誤」,導致回測結果產生差異,應當以隔日開盤價作為交易價格,才能反應最真實的交易情形。
本次實作透過了簡單的交易回測實作演示了在交易回測中暗藏的前視偏誤,而這樣的現象並不僅僅出現於交易回測,而是普遍的出現在金融領域中,為了避免前視偏誤,重要的是確保歷史分析或決策過程僅基於當時可用的資訊。這要求按照過去已知的方式使用歷史數據,排除任何後續不可得知的信息。意識到前瞻性偏誤並謹慎處理數據對於保持統計分析和決策過程的完整性和準確性至關重要。
溫馨提醒,本次策略與標的僅供參考,不代表任何商品或投資上的建議。之後也會介紹使用TEJ資料庫來建構各式指標,並回測指標績效,所以歡迎對各種交易回測有興趣的讀者,選購TEJ E-Shop的相關方案,用高品質的資料庫,建構出適合自己的交易策略。
電子報訂閱