tidyquantでバックテスト(複数パラメーター最適化)
前回の記事に引き続きバックテストを進めていきます。
今回は複数のパラメータによる最適化をテストしてみたいと思います。2本のEMAを計算し、それぞれのパラメータを変化させてパフォーマンスを最大化します。
まずtq_mutate()
で2本のEMAを計算します。
require(tidyquant) require(tidyverse) data(FANG) term1 <- FANG %>% group_by(symbol) %>% tq_mutate(select = adjusted, mutate_fun = EMA, n = 10, col_rename = "ema1") %>% tq_mutate(select = adjusted, mutate_fun = EMA, n = 20, col_rename = "ema2")
計算結果を可視化して確認しておきます。
term1 %>% ggplot(aes(date, adjusted)) + geom_line() + geom_line(aes(date, ema1), colour = "red") + geom_line(aes(date, ema2), colour = "blue") + theme_tq() + facet_wrap(.~symbol, scale = "free")
次に前回のテストと同様にストラテジーを関数化します。
strategy_ema_cross <- function(param1, param2){ term1 <- FANG %>% group_by(symbol) %>% tq_mutate(select = adjusted, mutate_fun = EMA, n = param1, col_rename = "ema1") %>% tq_mutate(select = adjusted, mutate_fun = EMA, n = param2, col_rename = "ema2") term2 <- term1 %>% tq_mutate(select = adjusted, mutate_fun = ROC, col_rename = "roc") term3 <- term2 %>% mutate(sig = lag(if_else(ema1 > ema2, 1, -1)), ret = roc * sig) term4 <- term3 %>% drop_na %>% mutate(eq = cumsum(ret)) %>% select(symbol, date, eq) }
次に最適化用のパラメータセットを用意します。expand.grid()
は与えられた変数について総当たりのテーブルを計算します。
param_set <- expand.grid( param1 = seq(10, 50, by = 10), param2 = seq(20, 60, by = 10) )
purrr::pmap()
を使ってストラテジーにパラメータセットを適用します。
opt_ret <- param_set %>% pmap(strategy_ema_cross) opt_ret <- opt_ret %>% set_names(str_c("param", seq_len(25))) opt_ret <- opt_ret %>% bind_rows(.id = "param")
計算結果を可視化します。
opt_ret %>% filter(symbol == "FB") %>% ggplot(aes(x = date, y = eq, colour = param)) + geom_line() + theme_tq() + geom_hline(yintercept = 0, colour = "darkgray") + scale_color_tq()
結果はかなり見辛くなりますが、明確な傾向が表れました。この傾向が何を意味するのか探っていきます。
確認のためパラメータごとのパフォーマンスを可視化します。
opt_ret %>% filter(symbol == "FB") %>% ggplot(aes(x = date, y = eq, fill = param)) + geom_area() + theme_tq() + scale_fill_tq() + facet_wrap(.~param)
極端にパフォーマンスの悪いパラメータが複数あることが分かります。
分析のためパラメータとパフォーマンスのデータをsummaries
で集約して単純化します。
perf_tbl <- opt_ret %>% filter(symbol == "FB") %>% group_by(param) %>% summarise(perf = sum(eq))
次に使用したパラメータセットの変数XとYの差を示すデータを作成します。
param_tbl <- param_set %>% mutate( diff = param2 - param1, param = as.factor(str_c("param", seq_len(25))) )
両者を比較検討してみましょう。評価しやすいように結果をランキング形式で並べ替えます。
まずパラメータごとのパフォーマンスを確認します。
perf_tbl %>% mutate( param = as.factor(param), param = fct_reorder(param, perf) ) %>% ggplot(aes(param, perf, fill = param)) + geom_bar(stat = "identity") + coord_flip() + theme_tq() + scale_fill_tq()
次にパラメータごとの2つの値の差を確認します。
param_tbl %>% mutate(lab = fct_reorder(lab, diff)) %>% ggplot(aes(lab, diff, fill = lab)) + geom_bar(stat = "identity") + coord_flip() + theme_tq() + scale_fill_tq()
param_tbl
の中には負の値を取るものがありますが、これはストラテジーのシグナルを反転させることを意味します。つまり「短期MAが長期MAを上回ったらショート」というシグナルです。
さらにパラメータ値の差とパフォーマンスの関係を可視化します。
left_join(perf_tbl, param_tbl, .by = param) %>% select(perf, diff, param) %>% ggplot(aes(diff, perf, fill = param)) + geom_bar(colour = "black", stat = "identity", position = "dodge") + theme_tq() + scale_fill_tq()
上記のプロットから次のことが読み取れます。
- 必ずしも2つのパラメータの差の大きさ=リターンとはならない。
- パフォーマンスの高いパラメータセットに差の値が負となるものはない
- パラメータの差が0となるセットは例外なくリターンが悪化
この結果から対象の銘柄におけるストラテジーの挙動について「2つのパラメータ値の差が正の値を取り、推定区間内で中間的な値を取るパラメータセットについてリターンが高い傾向にある」ことが分かります。
またパラメータ値の差が負の値を取る、または値が0のものについてはパフォーマンスが悪化しています。
グラフは明かに与えられたパラメータ外のファクターがパフォーマンスに影響を及ぼしていることを示唆しているため、さらに詳細な分析が必要といえます。
パラメータ間に差がないものについて極端にパフォーマンスが悪くなっているのは、このストラテジーのコードだと条件が偽となる場合常にショートしていることになるからです。
本来このようなパラメータについては除外して考えるべきですが、今回はテストの意味も兼ねてそのまま計算しました。