譯者 | Tianyu
本文是用 Python 做交易策略回測系列文章的第四篇。上個部分介紹了以下幾個方面內容:
介紹了 zipline 回測框架,並展示了如何回測基本的策略導入自定義的數據並使用 zipline評估交易策略的表現這篇文章的目的是介紹如何基於技術分析(TA, Technical Analysis)來創建交易策略。在此引用維基百科的解釋,技術分析是指「基於對市場的歷史數據、成交價格、交易量的研究,來預測價格走勢的一套方法」。
在本文中,我會介紹如何使用流行的 Python 庫 TA-Lib 以及 zipline 回測框架來計算 TA 指標。我會創建 5 種策略,然後研究哪種策略在投資期限內表現最好。
安裝
我用到的庫有以下幾個:
pyfolio 0.9.2
numpy 1.14.6
matplotlib 3.0.0
pandas 0.22.0
json 2.0.9
empyrical 0.5.0
zipline 1.3.0
輔助函數
在構造策略之前,我要先定義幾個輔助函數(此處我只介紹其中一個,因為它是最重要的一個)。
這個函數用來設置回測的起始時間,因為我希望所有策略開始實施的時間保持一致,設置為2016年的第一天。不過,有些基於技術指標的策略需要一定數量的歷史數據,也就是所謂的 warm-up 階段。請一定記住一點,沒有任何交易決策會發生在回測期的起始時間之前。
def get_start_date(ticker, start_date, days_prior):
start_date_dt = datetime.strptime(start_date, '%Y-%m-%d')
prior_to_start_date_dt = start_date_dt - relativedelta(days=2 * days_prior)
prior_to_start_date = prior_to_start_date_dt.strftime('%Y-%m-%d')
yahoo_financials = YahooFinancials(ticker)
df = yahoo_financials.get_historical_price_data(
prior_to_start_date, start_date, 'daily')
df = pd.DataFrame(df[ticker]['prices'])['formatted_date']
if df.iloc[-1] == start_date:
days_prior += 1
new_start_date = df.iloc[-days_prior]
return new_start_date
策略
本篇文章中,我們要解決的問題如下:
投資者有 10000 元的本金投資時限為 2016-2017投資者僅投資 Tesla 的股票假設不存在交易成本,即交易佣金為零不存在做空行為(投資者只能出售他們擁有的股票)當投資者購買股票時,他們會花掉全部本金之所以選擇這段時期,是因為2018年中後的 Quandl 數據集還沒有更新,我們希望代碼可以儘可能簡化。關於如何將數據載入 zipline 的更多細節,請參考到我之前的文章。
買入和持有的策略
我們首先來看最基本的策略 —— 買入和持有。具體的思路是,我們買入一定的資產,在整個投資期間不進行任何操作。因此在投資第一天,我們使用全部本金儘可能多地購買 Tesla 的股票,接下來什麼事情都不做。
這種簡單的策略可以作為其他高級策略的基準,若某種複雜的策略相比於基準策略反而損失了更多的錢,那麼說明這種策略毫無用處。
%%zipline --start 2016-1-1 --end 2017-12-31 --capital-base 10000.0 -o buy_and_hold.pkl
# imports
from zipline.api import order_percent, symbol, record
from zipline.finance import commission
# parameters
SELECTED_STOCK = 'TSLA'
def initialize(context):
context.asset = symbol(SELECTED_STOCK)
context.has_ordered = False
context.set_commission(commission.PerShare(cost=0.0, min_trade_cost=0))
def handle_data(context, data):
# trading logic
if not context.has_ordered:
order_percent(context.asset, 1)
context.has_ordered = True
record(price=data.current(context.asset, 'price'))
接下來,我們載入有關該策略表現的 DataFrame:
buy_and_hold_results = pd.read_pickle('buy_and_hold.pkl')
這裡可能會出現 ending_cash 為負的情況,原因是我們想要買入的股份是當天收盤時計算的,於是使用的是收盤價格。然而,這筆交易是次日執行的,價格可能會發生大幅變化。在 zipline 中,交易不會因為金額不足而被拒,但我們可以通過負的餘額將其終止。我們可以想些辦法避免這種情況的發生,例如手動計算第二天要買入的股份,並考慮股價上漲等因素,以防止這種情況發生。
我們使用一個輔助函數,將該策略的細節進行可視化:投資組合的變化,交易價格序列,以及每天的收益情況。
我們還使用了另一個輔助函數來觀察策略的表現,該函數將用於最後一部分:
buy_and_hold_results = pd.read_pickle('buy_and_hold.pkl')
為了簡潔起見,我們不會展示每種策略的全部步驟,因為它們的執行方式都是一樣的。
簡單的移動平均策略
我們採用的第二種策略基於簡單的移動平均數方法(SMA, Simple Moving Average)。該策略的邏輯可以歸納為以下幾步:
當20天的 SMA 價格上升時,買入股份當20天的 SMA 價格下降時,賣掉全部股份用前19天和當天的數據計算移動平均數,次日執行交易決策這是我們第一次調用預設輔助函數的地方,計算起始日期,以使投資者能在2016年的第一個交易日制定交易決策。
get_start_date('TSLA', '2016-01-04', 19)# '2015-12-04'
在下面的策略中,我們使用修改後的日期作為起始日期:
%%zipline --start 2015-12-4 --end 2017-12-31 --capital-base 10000.0 -o simple_moving_average.pkl
# imports
from zipline.api import order_percent, record, symbol, order_target
from zipline.finance import commission
# parameters
MA_PERIODS = 20
SELECTED_STOCK = 'TSLA'
def initialize(context):
context.time = 0
context.asset = symbol(SELECTED_STOCK)
context.set_commission(commission.PerShare(cost=0.0, min_trade_cost=0))
context.has_position = False
def handle_data(context, data):
context.time += 1
record(time=context.time)
if context.time < MA_PERIODS:
return
price_history = data.history(context.asset, fields="price", bar_count=MA_PERIODS, frequency="1d")
ma = price_history.mean
# cross up
if (price_history[-2] < ma) & (price_history[-1] > ma) & (not context.has_position):
order_percent(context.asset, 1.0)
context.has_position = True
# cross down
elif (price_history[-2] > ma) & (price_history[-1] < ma) & (context.has_position):
order_target(context.asset, 0)
context.has_position = False
record(price=data.current(context.asset, 'price'),
moving_average=ma)
注意:data.current(context.asset, 『price』) 等同於 price_history[-1].
下圖展示了該策略:
下圖展示了20天的移動平均價格序列。我們還對每一次交易做了標註,即在記號之後的第一個交易日執行此筆交易。
移動平均交叉
移動平均交叉策略(Moving Average Crossover)可以看作是上一種策略的拓展版,用兩個不同規格的移動窗口來代替單個的窗口。100天的移動平均數序列中,要隔很久才會出現價格的突變,而20天的移動平均數序列發生突變的速度要快很多。
該策略的邏輯如下:
當較快的移動平均值穿越較慢的移動平均值時,我們買入股份當較慢的移動平均值穿越較快的移動平均值時,我們賣出股份一定要記住一點,在這種策略中,許多不同長度窗口的組合構成了速度不同的移動平均數。
對於該策略,我們需要另外載入100天的數據,以便於準備 warm-up 階段。
%%zipline --start 2015-8-11 --end 2017-12-31 --capital-base 10000.0 -o moving_average_crossover.pkl
# imports
from zipline.api import order_percent, record, symbol, order_target
from zipline.finance import commission
# parameters
SELECTED_STOCK = 'TSLA'
SLOW_MA_PERIODS = 100
FAST_MA_PERIODS = 20
def initialize(context):
context.time = 0
context.asset = symbol(SELECTED_STOCK)
context.set_commission(commission.PerShare(cost=0.0, min_trade_cost=0))
context.has_position = False
def handle_data(context, data):
context.time += 1
if context.time < SLOW_MA_PERIODS:
return
fast_ma = data.history(context.asset, 'price', bar_count=FAST_MA_PERIODS, frequency="1d").mean
slow_ma = data.history(context.asset, 'price', bar_count=SLOW_MA_PERIODS, frequency="1d").mean
# Trading logic
if (fast_ma > slow_ma) & (not context.has_position):
order_percent(context.asset, 1.0)
context.has_position = True
elif (fast_ma < slow_ma) & (context.has_position):
order_target(context.asset, 0)
context.has_position = False
record(price=data.current(context.asset, 'price'),
fast_ma=fast_ma,
slow_ma=slow_ma)
接下來,我們繪製了兩個移動平均價格序列。我們可以發現,該策略產生的交易行為要比 SMA 策略少得多。
移動平均線收斂差異
MACD 的全稱為 Moving Average Convergence/Divergence,即移動平均線收斂差異指標,是一種常用於股價技術分析中的指標。
MACD 由三個時間序列構成:
MACD 序列:快速(短期)和慢速(長期)的兩個指數移動平均值的差值信號序列:MACD 序列的 EMA(指數移動平均值)差異序列:MACD 序列與信號序列之間的差值MACD 的參數包括計算三個移動平均數的天數,即 MACD(a, b, c),參數 a 表示快速 EMA,b 表示慢速 EMA,c 表示 MACD 序列的 EMA。最常見的參數配置為 MACD(12, 26, 9),也是本文所採用的配置。若每周有6個工作日,這三個參數分別對應2個星期、1個月、1.5個星期。
必須記住一點,由於 MACD 是基於移動平均方法進行計算的,因此它是一種滯後指標。這就解釋了為什麼 MACD 在股市上的作用很小,它無法得出準確的價格趨勢。
該策略的基本思想如下:
當 MACD 線穿越信號線向上時,買入股份當 MACD 線穿越信號線向下時,賣出股份和之前一樣,為了準備 warm-up,我們要保證有34個歷史數據值來計算 MACD:
%%zipline --start 2015-11-12 --end 2017-12-31 --capital-base 10000.0 -o macd.pkl
# imports ----
from zipline.api import order_target, record, symbol, set_commission, order_percent
import matplotlib.pyplot as plt
import talib as ta
from zipline.finance import commission
# parameters ----
SELECTED_STOCK = 'TSLA'
#initialize the strategy
def initialize(context):
context.time = 0
context.asset = symbol(SELECTED_STOCK)
context.set_commission(commission.PerShare(cost=0.0, min_trade_cost=0))
context.has_position = False
def handle_data(context, data):
context.time += 1
if context.time < 34:
return
price_history = data.history(context.asset, fields="price", bar_count=34, frequency="1d")
macd, macdsignal, macdhist = ta.MACD(price_history, 12, 26, 9)
if (macdsignal[-1] < macd[-1]) and (not context.has_position):
order_percent(context.asset, 1.0)
context.has_position = True
if (macdsignal[-1] > macd[-1]) and (context.has_position):
order_target(context.asset, 0)
context.has_position = False
record(macd = macd[-1], macdsignal = macdsignal[-1], macdhist = macdhist[-1], price=price_history[-1])
接下來,我們繪製了 MACD 線和信號線,交叉點代表買入/賣出的信號。另外,你也可以試著用直方圖的形式來展現 MACD 差異。
相對強弱指標(RSI)
RSI 的全稱為 Relative Strength Index,即相對強弱指標,也是一種用於創建交易策略的技術指標。RSI 被看作是一種動量振蕩器,它可以估測價格變化的速度和幅度。
RSI 指標評估了股價的向上力量與向下力量的比率。若向上的力量較大,則計算出來的指標上升;若向下的力量較大,則指標下降。
RSI 的結果為0到100之間的數字,一般按14天進行計算。為生成交易信號,通常要指定 RSI 的下限為30,上限為70。也就是說,30以下在超賣區,70以上為超買區。
有時候,也可能會設定一個比較居中的值,比如在涉及到做空的策略中。我們也可以選擇更極端的閾值,如20和80。不過,這要求具備專業知識,或者在回測時嘗試。
這種策略的思想如下:
當 RSI 低於下限(30)時,買入股份當 RSI 高於上限(70)時,賣出股份%%zipline --start 2015-12-10 --end 2017-12-31 --capital-base 10000.0 -o rsi.pkl
# imports ----
from zipline.api import order_target, record, symbol, set_commission, order_percent
import matplotlib.pyplot as plt
import talib as ta
from zipline.finance import commission
# parameters ----
SELECTED_STOCK = 'TSLA'
UPPER = 70
LOWER = 30
RSI_PERIOD = 14
#initialize the strategy
def initialize(context):
context.time = 0
context.asset = symbol(SELECTED_STOCK)
context.set_commission(commission.PerShare(cost=0.0, min_trade_cost=0))
context.has_position = False
def handle_data(context, data):
context.time += 1
if context.time < RSI_PERIOD + 1:
return
price_history = data.history(context.asset, fields="price", bar_count=RSI_PERIOD+1, frequency="1d")
rsi = ta.RSI(price_history, timeperiod=RSI_PERIOD)
if rsi[-1] < LOWER and not context.has_position:
order_percent(context.asset, 1.0)
context.has_position = True
if rsi[-1] > UPPER and context.has_position:
order_target(context.asset, 0)
context.has_position = False
record(rsi=rsi[-1], price=price_history[-1], time=context.time)
下圖繪製了 RSI 指標和上、下限:
效果評估
最後一步,把所有的評估指標放入一個 DataFrame 中,然後觀察其結果。我們會發現在回測時,基於簡單移動平均方法的策略在收益方面表現最好,其夏普指數也最高,即在特定風險下,可獲得的收益最高。基於 MACD 的策略排在第二位。只有這兩種策略的表現超過了我們所設置的基準。
perf_df = pd.DataFrame({'Buy and Hold': buy_and_hold_perf,
'Simple Moving Average': sma_perf,
'Moving Average Crossover': mac_perf,
'MACD': macd_perf,
'RSI': rsi_perf})
perf_df.transpose
結論
本篇文章介紹了如何利用 zipline 和 talib 進行交易策略的回測,使用的技術指標包括移動平均數、MACD、RSI 等等。但這只是一些基礎,還有相當多更加複雜的策略。
另外,我們必須記住一點,一些在過去表現很好的策略不一定也適用於未來。
(*本文為AI科技大本營編譯文章,轉載請微信聯繫 1092722531)
◆
◆