本次範例將以最經典的買進持有策略向您介紹 zipline 回測方法,並且介紹組建 zipline 交易策略最基礎的四大函式 - initialize
、handle_data
、analyze
、run_algorithm
。
在 zipline 中,我們使用 os
搭配 !zipline ingest
將股價資料導入到本地端。常用寫法為:
!zipline ingest -b tqant
其中 -b
為 bundle 之涵義,bundle 為股票價量資訊的載體,tquant
則是 bundle 之名稱,可由使用者自定義。在 ingest 之前,需先使用 os
設定環境變數,以方便 zipline 接收使用者所欲抓取之資產標的與年份之要求。一般而言,針對環境變數之寫法如下:
os.environ['TEJAPI_BASE'] = "https://api.tej.com.tw" ==> 導航至 tej api 網域。
os.environ['TEJAPI_KEY'] = "your key" ==> 個人 api key 以驗證身分。
os.environ['mdate'] = "20170601 20230702" ==> 欲抓取資料之日期區間,前者為起始日,後者為終止日。
os.environ['ticker'] = '2330 2337' ==> 所欲抓取股票之代碼。
import os
os.environ['TEJAPI_BASE'] = "https://api.tej.com.tw"
os.environ['TEJAPI_KEY'] = "your key"
os.environ['mdate'] = "20170601 20230702"
os.environ['ticker'] = '2330 IR0001'
!zipline ingest -b tquant
Merging daily equity files:
[2023-08-09 05:09:50.565678] INFO: zipline.data.bundles.core: Ingesting tquant.
initialize
為構建 zipline 交易策略的重要函式,會在回測開始前被呼叫一次,主要任務為設定回測環境,常見用於設定滑價或手續費。分別可以使用:
zipline.api.set_slippage
設定滑價模式,zipline 共提供四種滑價計算方法,詳請請見後續教學-zipline slippage。
zipline.api.set_commission
設定手續費模式,zipline 共提供三種手續費計算方法,詳請請見後續教學-zipline commission。
常見的寫法如下:
def initialize(context):
set_slippage(slippage.FixedSlippage())
set_commission(commission.PerShare(cost=0.00285))
除此之外,我們可以注意到 initialize 含有一個參數 context__,__context 為一個命名空間 (namespace),可以在儲存各種自定義之變數並且在每次交易日中循環呼叫。舉例來說,我們設置一個變數 (context.day = 0) 來計算交易日天數與一個變數 (context.has_ordered = False) 紀錄是否已經持有台積電股票。
def initialize(context):
context.day = 0
context.has_ordered = False
set_slippage(slippage.FixedSlippage())
set_commission(commission.PerShare(cost=0.00285))
from zipline.api import set_slippage, set_commission
from zipline.finance import slippage, commission
def initialize(context):
context.day = 0
context.has_ordered = False
set_slippage(slippage.FixedSlippage())
set_commission(commission.PerShare(cost=0.00285))
handle_data
為構建 zipline 交易策略的重要函式,會在回測開始後每天被呼叫,主要任務為設定交易策略、下單與紀錄交易資訊。
其中 handle_data
含有兩種參數 -- context , data__。__context 與上述 initialize
介紹功能相同,這裡為了記錄交易天數與否持有台積電股票,我們設定為:
def handle_data(context, data):
# 每次交易日加 1 天。
context.day += 1
# 判別是否持有台積電,請注意我們在 initialize 設定 context.has_ordered 為 False。
if not context.has_ordered:
接著我們引入交易下單函式,下單函式共有六個不同種類,請見教材中以 zipline order 開頭之文件,這裡使用最基礎的下單函式:
買進或賣出 n 股的資產標的。
加入下單函式 order(symbol("2330"),其中 symbol("2330") 就是 zipline 中的 Asset 資料型態。之後,我們會將 context.has_ordered 調整成 True,此時下個交易日就不會再度下單,更改完程式如下:
def handle_data(context, data):
context.day += 1
if not context.has_ordered:
# 下單台積電股票一張 == 1000股
order(symbol("2330", 1000)
# 設定 context.has_ordered 為 True 以避免下次交易日下單
context.has_ordered = True
最後為了記錄交易天數、是否持有部位與當日價格,我們使用 record
函式,其功能為記錄每個交易日的資訊並且在最終 run_algorithm
輸出的資料表中,以欄位型式加入所紀錄資訊。其程式編輯方式如下:
record( 欄位名稱 = 資訊)
這裡我們紀錄當天交易天數 (context.day)、是否持有部位 (context.has_ordered) 與當天收盤價格 (data.current(symbol("2330"), "close")),其中上面所提到的 data 就是在 handle_data
中的 data__,__data 主要功能為保存每天股價的價量資料並且提供呼叫,於本實例我們欲紀錄當天收盤價,於是用到 data.current()
函式。
呼叫股票的當日價量資訊。
由於我們希望記錄台積電當日收盤價格,因此程式編輯如下:
def handle_data(context, data):
context.day += 1
if not context.has_ordered:
order(symbol("2330", 1000)
context.has_ordered = True
record( # 紀錄用
trade_days = context.day,
has_ordered = context.has_ordered,
TSMC = data.current(symbol("2330"), "close")
)
from zipline.api import order, record, symbol
def handle_data(context, data):
context.day += 1
if not context.has_ordered:
order(symbol("2330"), 1000)
context.has_ordered = True
record(
trade_days = context.day,
has_ordered = context.has_ordered,
TSMC = data.current(symbol("2330"), "close")
)
analyze
主要用於回測後視覺化策略績效與風險,這裡我們以 matplotlib
繪製投組價值表與台積電股價走勢表。其中 analyze
有兩個參數 context 與 perf__,__context 就與上述相同,perf 就是最終 run_algorithm
輸出的資料表 -- __results__。我們可以提取裡面特定欄位來繪製圖表。
import matplotlib.pyplot as plt
def analyze(context, perf):
ax1 = plt.subplot(211)
perf.portfolio_value.plot(ax=ax1,title='portfolio values')
ax2 = plt.subplot(212, sharex=ax1)
perf['TSMC'].plot(ax=ax2,title='TSMC close')
plt.gcf().set_size_inches(18, 8)
plt.show()
進行策略回測。
Perf: pd.DataFrame, 內建欄位有:
benchmark_return:
當日的benchmark報酬率,若是由set_benchmark()
設定,則計算方式為(當日benchmark收盤價 / 前一日benchmark收盤價) - 1。
benchmark_period_return:
累積的benchmark日報酬率。計算方式:np.nancumprod(1 + benchmark_return
Series) - 1。
treasury_return: 當日的treasury報酬率,直接由TEJ API提供。
treasury_period_return:
累積的treasury日報酬率。計算方式:np.nancumprod(1 + treasury_return
Series) - 1。
benchmark_volatility:
benchmark日報酬率的年化波動率,至少需有兩期的報酬率才進行計算。計算方式:(benchmark_return
Series).expanding(2).std(ddof=1) * np.sqrt(252)
orders:顯示下單紀錄 每一筆下單用一個字典的方式呈現,其中:
id:虛擬單號。
dt:下單時間。
reason:None, Hold, or Reject(目前不適用)
created:建立時間。
amount:
filled:成交股數。
commission:該筆交易傭金。
stop:停損價,若有發股票股利或股票分割的情形,除權日當天會更新之前未成交訂單的stop price(new_stop = old_stop * ratio,其中ratio = 1/僅除權調整係數)。
limit:限價價,若有發股票股利或股票分割的情形,除權日當天會更新之前未成交訂單的limit price(new_limit = old_limit * ratio,其中ratio = 1/僅除權調整係數)。
stop_reached:
limit_reached:
sid(asset):下單的標的。
status:若=0表示OPEN(未完全成交),=1表示FILLED(完全成交),=2表示CANCEL(已取消)。
transactions:顯示交易紀錄
positions:顯示持有部位
longs_count:
positions
比較。shorts_count:
positions
比較。long_exposure(long_value):
positions
比較。long_exposure
和long_value
兩欄位數值一致。short_exposure(short_value):
positions
比較。short_exposure
和short_value
兩欄位數值一致。ending_exposure(ending_value):
long_exposure
+ short_exposure
starting_exposure(starting_value):
ending_exposure
。gross_leverage(leverage):
long_exposure
- short_exposure
)/portfolio_value
net_leverage:
long_exposure
+ short_exposure
)/portfolio_value
capital_used:
transaction.price
* transaction.amount
) - sum(order.commission
) + sum(earn_dividends) + sum(leftover_cash)ending_cash:
starting_cash
+capital_used
starting_cash:
ending_cash
+sum(earn_dividends),若無前一日則為capital_base
。pnl:
ending_exposure
+ ending_cash
) - (starting_exposure
+ starting_cash
)。returns:
portfolio_value
) / (前一日portfolio_value
) - 1。portfolio_value:
ending_exposure
+ ending_cash
)algorithm_period_return:
algorithm_period_return
) * ( 1 + 當日returns
) - 1。algo_volatility:
returns
Series, period='daily', alpha=2.0, annualization=None)。returns
Series.std(ddof=1) * (252 ** (1 / 2)),std為樣本標準差。returns
資料,則會回傳最後一日algo_volatility,若扣掉最後一日returns
,則回傳倒數第二日,以此類推。excess_return:
benchmark_period_return
)。algorithm_period_return
- benchmark_period_return
)。max_drawdown:
returns
Series)。sharpe:
returns
Series, risk_free=0)。returns
Series) / np.std(returns
Series,ddof=1) * np.sqrt(252)sortino:
returns
Series, required_return=0)。returns
Series中>0的數值替換成0。returns
Series)alpha:
returns
Series, factor_returns=benchmark_return
Series,risk_free=0.0)returns
Series - (當日beta
* benchmark_return
Series)beta:
returns
Series, factor_returns=benchmark_return
Series,risk_free=0.0)benchmark_return
Series, returns
Series) / Var(benchmark_return
Series)from zipline import run_algorithm
import pandas as pd
start_date = pd.Timestamp('2018-12-30',tz='utc')
end_date = pd.Timestamp('2023-05-26',tz='utc')
results = run_algorithm(start= start_date,
end=end_date,
initialize=initialize,
capital_base=1e6,
analyze=analyze,
handle_data=handle_data,
data_frequency='daily',
bundle='tquant'
)
results
period_open | period_close | short_value | long_exposure | benchmark_return | treasury_return | pnl | short_exposure | capital_used | returns | ... | treasury_period_return | algorithm_period_return | alpha | beta | sharpe | sortino | max_drawdown | max_leverage | trading_days | period_label | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
2019-01-02 13:30:00+08:00 | 2019-01-02 09:01:00+08:00 | 2019-01-02 13:30:00+08:00 | 0.0 | 0.0 | 0.0 | 0.0 | 0.00 | 0.0 | 0.00 | 0.000000 | ... | 0.0 | 0.000000 | None | None | NaN | NaN | 0.000000 | 0.000000 | 1 | 2019-01 |
2019-01-03 13:30:00+08:00 | 2019-01-03 09:01:00+08:00 | 2019-01-03 13:30:00+08:00 | 0.0 | 215500.0 | 0.0 | 0.0 | -2.85 | 0.0 | -215502.85 | -0.000003 | ... | 0.0 | -0.000003 | None | None | -11.224972 | -11.224972 | -0.000003 | 0.215501 | 2 | 2019-01 |
2019-01-04 13:30:00+08:00 | 2019-01-04 09:01:00+08:00 | 2019-01-04 13:30:00+08:00 | 0.0 | 208000.0 | 0.0 | 0.0 | -7500.00 | 0.0 | 0.00 | -0.007500 | ... | 0.0 | -0.007503 | None | None | -9.170376 | -9.168633 | -0.007503 | 0.215501 | 3 | 2019-01 |
2019-01-07 13:30:00+08:00 | 2019-01-07 09:01:00+08:00 | 2019-01-07 13:30:00+08:00 | 0.0 | 213000.0 | 0.0 | 0.0 | 5000.00 | 0.0 | 0.00 | 0.005038 | ... | 0.0 | -0.002503 | None | None | -1.893153 | -2.608781 | -0.007503 | 0.215501 | 4 | 2019-01 |
2019-01-08 13:30:00+08:00 | 2019-01-08 09:01:00+08:00 | 2019-01-08 13:30:00+08:00 | 0.0 | 211000.0 | 0.0 | 0.0 | -2000.00 | 0.0 | 0.00 | -0.002005 | ... | 0.0 | -0.004503 | None | None | -3.141155 | -4.087705 | -0.007503 | 0.215501 | 5 | 2019-01 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
2023-05-22 13:30:00+08:00 | 2023-05-22 09:01:00+08:00 | 2023-05-22 13:30:00+08:00 | 0.0 | 531000.0 | 0.0 | 0.0 | -1000.00 | 0.0 | 0.00 | -0.000734 | ... | 0.0 | 0.362247 | None | None | 0.816172 | 1.250657 | -0.202433 | 0.455182 | 1063 | 2023-05 |
2023-05-23 13:30:00+08:00 | 2023-05-23 09:01:00+08:00 | 2023-05-23 13:30:00+08:00 | 0.0 | 530000.0 | 0.0 | 0.0 | -1000.00 | 0.0 | 0.00 | -0.000734 | ... | 0.0 | 0.361247 | None | None | 0.813954 | 1.247253 | -0.202433 | 0.455182 | 1064 | 2023-05 |
2023-05-24 13:30:00+08:00 | 2023-05-24 09:01:00+08:00 | 2023-05-24 13:30:00+08:00 | 0.0 | 525000.0 | 0.0 | 0.0 | -5000.00 | 0.0 | 0.00 | -0.003673 | ... | 0.0 | 0.356247 | None | None | 0.804283 | 1.232180 | -0.202433 | 0.455182 | 1065 | 2023-05 |
2023-05-25 13:30:00+08:00 | 2023-05-25 09:01:00+08:00 | 2023-05-25 13:30:00+08:00 | 0.0 | 543000.0 | 0.0 | 0.0 | 18000.00 | 0.0 | 0.00 | 0.013272 | ... | 0.0 | 0.374247 | None | None | 0.835019 | 1.282067 | -0.202433 | 0.455182 | 1066 | 2023-05 |
2023-05-26 13:30:00+08:00 | 2023-05-26 09:01:00+08:00 | 2023-05-26 13:30:00+08:00 | 0.0 | 566000.0 | 0.0 | 0.0 | 23000.00 | 0.0 | 0.00 | 0.016736 | ... | 0.0 | 0.397247 | None | None | 0.873009 | 1.345075 | -0.202433 | 0.455182 | 1067 | 2023-05 |
1067 rows × 42 columns
我們可以發現之前使用 order
紀錄的 trade_days, has_ordered 與 TSMC 確實以欄位型式記錄在 results 表中。
results[['trade_days','has_ordered','TSMC']]
trade_days | has_ordered | TSMC | |
---|---|---|---|
2019-01-02 13:30:00+08:00 | 1 | True | 219.5 |
2019-01-03 13:30:00+08:00 | 2 | True | 215.5 |
2019-01-04 13:30:00+08:00 | 3 | True | 208.0 |
2019-01-07 13:30:00+08:00 | 4 | True | 213.0 |
2019-01-08 13:30:00+08:00 | 5 | True | 211.0 |
... | ... | ... | ... |
2023-05-22 13:30:00+08:00 | 1063 | True | 531.0 |
2023-05-23 13:30:00+08:00 | 1064 | True | 530.0 |
2023-05-24 13:30:00+08:00 | 1065 | True | 525.0 |
2023-05-25 13:30:00+08:00 | 1066 | True | 543.0 |
2023-05-26 13:30:00+08:00 | 1067 | True | 566.0 |
1067 rows × 3 columns