2023.04.04
定量取引入門
はじめに
こんにちは、グループ研究開発本部 AI研究開発室のS.Sです。
今回は定量取引手法の基本的なトピックをいくつかかいつまんで紹介したいと思います。
資産の値動きを予測するのは難しいトピックとして知られていますが、価格は現実のさまざまな出来事に反応しつつ変わっていくことを考えると、弱いながらも規則的な動きを見せる可能性は残されています。
この記事では値動きの典型的なパターンと基本的なテクニカル指標を紹介し、パターンに基づく取引戦略のパフォーマンスについて検証します。
記事の内容は技術的な検証にとどまるもので特定の取引などを推奨するものではありません。
基本となる値動き: momentum/mean-reversion
テクニカル分析で基本となる値動きとしてmomentumとmean-reversionがあります。
momentumは直近の価格変化のトレンドが次の期間も継続するような現象のことを指し、直近プライスが上がったらその次の期間も上昇傾向が続くと考えます。
一方で、mean-reversionは価格が一度上昇しても時間が経つにつれて平均の価格に再び戻ってくるような現象のことを指します。
momentum/mean-reversionにそった取引のイメージを次の画像で示します。
momentumやmean-reversionのどちらがよく当てはまるかどうかは、対象の資産クラスや値動きをチェックするウィンドウによっても変わってきます。
株やFXでは保有期間が1ヶ月におよぶような長期の取引でmomentumに基づいて取引をすると、比較的うまくいくことが知られていますが、取引のウィンドウを短くしていくと必ずしも同じパターンが当てはまるとは限りません。
今回の記事ではmean-reversionによる短期間での取引戦略を見ていきます。
為替データでのmean-reversionによる取引
ここからの検証では取引対象をドルに対する主要国通貨とし、保有期間を1週間に固定します。
価格が平均に戻るというmean-reversionの性質を予測に使うにあたって、平均の価格を正確に決めるのは難しいので、ここでは1期前の価格に戻ると仮定して取引をしてみます。
この場合は過去1週間の資産価格が上がっていれば、次の1週間は下がると予測して資産を売る、逆に過去1週間の資産価格が上がっていれば、次の1週間は上がると予測して資産を買うというような挙動になります。
データの準備
為替の日次データはAlpha Vantageから無料で取得することができます。
ユーザー登録すると割り当てられるAPIキーを使って、pandas-datareaderで為替の日次データは次のようなコードで取得できます。
APIにはrate limitがあり、1分間に5リクエストまでとなっているので、1件取得するたびに20秒待つ処理を入れます。
import os from datetime import datetime import pandas_datareader.data as web pairs = ["USD/JPY", "EUR/USD", "EUR/JPY", "AUD/USD", "GBP/USD", "CAD/USD", "NZD/USD", "USD/CHF"] dfs = [] for col in pairs: df = web.DataReader(col, "av-forex-daily", start=datetime(2000, 1, 1), end=datetime(2023, 3, 30), api_key="api_key") df.rename_axis(["date"]).to_csv("./{}.csv".format(col.replace("/", "_"))) time.sleep(20)
必要な銘柄のデータを保存したら、再度dataframeに読み込みます。
この時、後の検証での都合がよくなるように、基軸通貨のドルがlongになるように向きを揃えておきます。
from glob import glob import pandas as pd import numpy as np df_ccy = [] for fn in glob("./ccy/*.csv"): df = pd.read_csv(fn) df["date"] = pd.to_datetime(df["date"]) df_ccy.append(df.set_index(["date"])["close"].rename(os.path.basename(fn).split(".csv")[0].replace("_", "/"))) df_ccy = pd.concat(df_ccy, axis=1) df_ccy_1w = df_ccy.resample("1W").last().pct_change() df_ccy_1w_dir = pd.concat((df_ccy_1w.filter(regex="USD/"), -df_ccy_1w.filter(regex="/USD")), axis=1) df_ccy_1w_dir.columns = df_ccy_1w_dir.columns.str.replace("USD/", "").str.replace("/USD", "")
個々の銘柄のパフォーマンス分析
複数資産を取引する場合は各資産ごとに予測を行い、毎期に全ての通貨をlongもしくはshortするように取引をします。
1期前のreturnと反対に取引した結果は次のコードで計算できます。
(-df_ccy_1w_dir.pipe(np.sign) * df_ccy_1w_dir.shift(-1))\ .assign(AVG=lambda x: x.mean(axis=1)).agg(["mean", "std", "count"])\ .T.assign(sr=lambda x: x["mean"]/x["std"]*np.sqrt(252/5))
全ての通貨で取引を行なったシミュレーション結果が次のテーブルとなります。
通貨によってはうまくいっていないものもありますが、全体的には週次で6bpのreturn, riskあたりのreturnを表すsharpe ratioは0.3くらいとまずまずの結果です。
cross-sectional mean-reversion
上記の方法で複数資産を取引すると、1期に保有するpositionにlongもしくはshortの偏りが出てしまうことがあります。
momentumの予測に基づいて上がりそうな銘柄をlong、下がりそうな銘柄をshortするcross-sectional momentumに倣って、mean-reversionに基づく予測で上がりそうな上位4銘柄をlong, 下がりそうな下位3銘柄をshortしてみるとどうなるでしょうか。計算は次のようなコードとなります。
pd.concat(( (-df_ccy_1w_dir.pipe(np.sign) * df_ccy_1w_dir.shift(-1)).mean(axis=1), ((-df_ccy_1w_dir).pipe(lambda df: 2*(df.rank(axis=1, ascending=False)<=4)-1) * df_ccy_1w_dir.shift(-1)).mean(axis=1)), axis=1, keys=["average", "cross-section"])\ .agg(["mean", "std", "count"])\ .T.assign(sr=lambda x: x["mean"]/x["std"]*np.sqrt(252/5))
計算結果が次のテーブルです。
returnは同じレベルですが、リスクあたりのリターンの大きさを示すsharpe ratioは0.1ほど改善しています。
cross-sectional mean-reversionの場合は、対ドルで予測の上位の通貨をlong・下位をshortする結果として、基軸通貨であるドルに対して中立に近いポジションとなります。
累積returnは次のようになっており、こちらの図からもcross-sectionのほうが少し安定性が高くなっているのがわかります。
テクニカル指標の導入
ここまでmean-reversionに基づく取引のパフォーマンスについて検証してきましたが、資産の値動きは時期によってトレンド or レンジ傾向の相場をいったりきたりすることが多いので、mean-reversionが得意な相場だけで取引をするようにしてみてはどうかをこのセクションでは検証してみます。
価格の時系列をもとにトレンド or レンジを判断する手がかりとなるテクニカル指標にaverage directional movement index(ADX)があり、トレンドが強いかどうかを判断する指標としては適しています。
これを取引にどう活用するかやり方は色々あると思いますが、大まかには20以下はトレンドが弱く40以上はトレンドが強いと判断できるとも言われていますので、閾値を決めてトレンドが弱い時だけに取引するとどうなるかをみてみます。
Pythonでのテクニカル指標の計算にはTA-libというライブラリを用いることができます。
このライブラリを使って全銘柄に対してADXを計算し、銘柄間の平均をとってドルに対する各通貨の値動きにトレンドが強く出ているかどうかを判断します。
OHLCが全て入ったデータを読み込み直してから、TA-libの関数を呼んでADXを計算します。
import talib df_ccy_ext = [] keys = [] for fn in glob("./ccy/*.csv"): df = pd.read_csv(fn) df["date"] = pd.to_datetime(df["date"]) df_ccy_ext.append(df.set_index(["date"])) keys.append(os.path.basename(fn).split(".csv")[0].replace("_", "/")) df_ccy_ext = pd.concat(df_ccy_ext, axis=1, keys=keys) df_ccy_ext_fill = df_ccy_ext.bfill() df_adx = [] for col in df_ccy.columns: df_adx.append(talib.ADX(df_ccy_ext_fill[col]["high"], df_ccy_ext_fill[col]["low"], df_ccy_ext_fill[col]["close"], 14).rename(col)) df_adx = pd.concat(df_adx, axis=1)
得られたテクニカル指標をもとに、トレンドが弱い時だけmean-reversionに沿って取引をした結果を計算します。
pd.concat(( (-df_ccy_1w_dir.pipe(np.sign) * df_ccy_1w_dir.shift(-1)).mean(axis=1), ((-df_ccy_1w_dir).pipe(lambda df: 2*(df.rank(axis=1, ascending=False)<=4)-1)\ * df_ccy_1w_dir.shift(-1)).mean(axis=1), ((-df_ccy_1w_dir).pipe(lambda df: 2*(df.rank(axis=1, ascending=False)<=4)-1)\ * df_ccy_1w_dir.shift(-1)).mean(axis=1)\ .loc[df_adx.mean(axis=1).resample("1W").last() < 20], ((-df_ccy_1w_dir).pipe(lambda df: 2*(df.rank(axis=1, ascending=False)<=4)-1)\ * df_ccy_1w_dir.shift(-1)).mean(axis=1)\ .loc[df_adx.mean(axis=1).resample("1W").last() < 30]), axis=1, keys=["average", "cross-section", "cross-section+(ADX<20)", "cross-section+(ADX<30)"])\ .agg(["mean", "std", "count"])\ .T.assign(sr=lambda x: x["mean"]/x["std"]*np.sqrt(252/5))
結果を次のテーブルに示します。
ADXが20以下の時だけ取引する結果をみるとよさそうにも見えますが、少し閾値を変えてみるとADXが弱いトレンドであることを示していても、必ずしもreturn/riskが高くなるかはっきりとした結論はつけにくい結果となっています。
まとめ
為替データを使った1週間という短期の取引で、mean-reversionに基づく取引戦略のパフォーマンスを簡単に検証してみたところ、まずまずのreturnが出ることがわかりました。
テクニカル指標を元にして、トレンドが弱まった局面で取引をすることでさらなるパフォーマンスの改善を目論みましたが、こちらははっきりとした改善にはつながりませんでした。
今回の記事では定量取引の基本をあっさり目で紹介しましたが、少しでも雰囲気が伝わっていれば幸いです。
最後に
グループ研究開発本部 AI研究開発室では、データサイエンティスト/機械学習エンジニアを募集しています。ビッグデータの解析業務などAI研究開発室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。皆さんのご応募をお待ちしています。
グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。
Follow @GMO_RD