QuantXでMFIを実装してみた

#はじめに
株式会社Smart Tradeが提供している投資アルゴリズム開発プラットフォーム「QuantX」で、MFIを実装しました。
#MFIとは
MFIとはMoney Flow Indexの略で相場の中でお金が買い、売りのどちらに流れているのかを株価と出来高を元に判断する、オシレーター指標のひとつです。
##定義式
まず、高値をHigh、安値をLow、終値をClose、出来高をVolumeとします。

```math
TypicalPrice = \frac{(High + Low + Close)}{3}\\
MoneyFlow = TypicalPrice\times Volume\\
```
次に、前日比でTypicalPriceが上昇したn日のMoneyFlowの合計と、前日比でTypicalPriceが下落または変わらずだったn日のMoneyFlowの合計を求めます。n日は任意の日数ですが、一般的には14日が良く用いられているそうです。

$If \quad TypicalPrice > TypicalPrice_{[-1]}$

```math
PositiveMoneyFlow = MoneyFlow
```

$else$

```math
NegativeMoneyFlow = MoneyFlow
```

MoneyFlowIndexはPositiveMoneyFlowの合計をNegativeMoneyFlowの合計で割った値のMoneyRatioを用いて求められます。

```math
MoneyRatio_{i} = \frac{\sum_{k\,=\,i-n}^{i} PositiveMoneyFlow_{k}}{\sum_{k\,=\,i-n}^{i} NegativeMoneyFlow_{k}}\\
MoneyFlowIndex_{i} = 100 - (\frac{100}{1 + MoneyRatio_{i}})
```

#実装
##全体
```MFI.py
import pandas as pd
import talib as ta
import numpy as np

def initialize(ctx):
	ctx.configure(
		channels = {           #利用チャンネル
			"jp.stock": {
				"symbols": [
					"jp.stock.2914", #日本たばこ産業
					"jp.stock.3382", #セブン&アイ・ホールディングス
					"jp.stock.4063", #信越化学工業
					"jp.stock.4452", #花王
					"jp.stock.4502", #武田薬品工業
					"jp.stock.4503", #アステラス製薬
					"jp.stock.6098", #リクルートホールディングス
					"jp.stock.6501", #日立製作所
					"jp.stock.6752", #パナソニック
					"jp.stock.6758", #ソニー
					"jp.stock.6861", #キーエンス
					"jp.stock.6954", #ファナック
					"jp.stock.6981", #村田製作所
					"jp.stock.7203", #トヨタ自動車
					"jp.stock.7267", #本田技研工業
					"jp.stock.7751", #キヤノン
					"jp.stock.7974", #任天堂
					"jp.stock.8031", #三井物産
					"jp.stock.8058", #三菱商事
					"jp.stock.8306", #三菱UFJフィナンシャル・グループ
					"jp.stock.8316", #三井住友フィナンシャルグループ
					"jp.stock.8411", #みずほフィナンシャルグループ
					"jp.stock.8766", #東京海上ホールディングス
					"jp.stock.8802", #三菱地所
					"jp.stock.9020", #東日本旅客鉄道
					"jp.stock.9022", #東海旅客鉄道
					"jp.stock.9432", #日本電信電話
					"jp.stock.9433", #KDDI
					"jp.stock.9437", #NTTドコモ
					"jp.stock.9984", #ソフトバンクグループ
				],
				"columns": [
					#"open_price_adj",  #始値(株式分割調整後)
					"high_price_adj",   #高値(株式分割調整後)
					"low_price_adj",    #安値(株式分割調整後)
					#"close_price",     #終値
					"close_price_adj",  #終値(株式分割調整後) 
					"volume_adj",       #出来高
					#"txn_volume",      #売買代金
				]
			}
		}
	)
	#シグナル定義
	def _my_signal(data):
		#この部分に作成するアルゴの指標を書き込んで下さい。
		#必要なデータ
		cp = data["close_price_adj"].fillna(method = "ffill")
		hp = data["high_price_adj"].fillna(method = "ffill")
		lp = data["low_price_adj"].fillna(method = "ffill")
		vp = data["volume_adj"].fillna(method = "ffill")

		#データを格納する場所
		mfi = pd.DataFrame(data = 0, columns=[], index = cp.index)
		
		#TA-Libの計算
		for (sym, val) in cp.items():
		  mfi[sym] = ta.MFI(
				hp[sym].values.astype(np.double),
				lp[sym].values.astype(np.double),
				cp[sym].values.astype(np.double),
				vp[sym].values.astype(np.double),
				timeperiod = 14
			)

		#シグナル生成部分
		buy_sig = (mfi < 20)
		sell_sig = (mfi > 80)
		
		return {
			"MFI": mfi,
			"buy:sig": buy_sig,
			"sell:sig": sell_sig,
		}

	# シグナル登録
	ctx.regist_signal("my_signal", _my_signal)

def handle_signals(ctx, date, current):
	df = current.copy()

	# 買いシグナル
	df_buy = df[df["buy:sig"]]
	if not df_buy.empty:
		for (sym, val) in df_buy.iterrows(): 
			sec = ctx.getSecurity(sym)
			sec.order_target_percent(0.10, comment = "SIGNAL BUY")
	# 売りシグナル
	df_sell = df[df["sell:sig"]]
	if not df_sell.empty:
		for (sym, val) in df_sell.iterrows():
			sec = ctx.getSecurity(sym)
			sec.order_target_percent(0, comment = "SIGNAL SELL")
```
##initialize(ctx)
この関数は最初に一度だけ呼び出されます。初期化部分で今回扱う銘柄と値を決めています。TOPIX Core30を用いました。ちなみに初期資金量¥10,000,000では今回の場合、単元株価格が¥1,000,000を大きく上回るキーエンス、任天堂、東海旅客鉄道、ファナック、村田製作所は基本的に売買されません。

##_my_signal(data)
まずPandasのfillna関数を使って欠損値（NaN）を穴埋めしています。ここでは、methodにffillを指定することで前の値で埋めています。

次に変数mfiにPandasのDataFrameを代入しています。ここでは、indexにcp（終値（株式分割調整後）を穴埋めしたもの）のインデックスを用いています。

そして[TA-LibのドキュメントのMFI](https://mrjbq7.github.io/ta-lib/func_groups/momentum_indicators.html)の形式に従ってfor文を用いてmfiに必要な値を代入します。ここでは、for文の中身がcp.items()の数だけ（指定した期間の日数回）実行されます。このfor文のsymはcpのカラム、つまり指定した証券コードです。

> **MFI - Money Flow Index**
NOTE: The `MFI` function has an unstable period.
`real = MFI(high, low, close, volume, timeperiod=14)`
Learn more about the Money Flow Index at [tadoc.org](http://www.tadoc.org/indicator/MFI.htm).

シグナル生成部分ではmfiの値が20未満の時、買いシグナルを、mfiの値が80より大きい時、売りシグナルを生成しています。下に例を示しておきます。mfiは定義式からも分かるように1から100までの実数値となります。

| mfi | buy_sig | sell_sig |
|:---:|:-------:|:--------:|
| 0   | True    | False    |
| 10  | True    | False    |
| 20  | False   | False    |
| 80  | False   | False    |
| 90  | False   | True     |
| 100 | False   | True     |

ここでの、買われ過ぎのサインとしての80という値と売られ過ぎのサインとしての20という値は[StockChartsのChartSchoolのMFIについての解説](https://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:money_flow_index_mfi)を参考にしました。

> **Overbought/Oversold**
Overbought and oversold levels can be used to identify unsustainable price extremes. Typically, MFI above 80 is considered overbought and MFI below 20 is considered oversold. Strong trends can present a problem for these classic overbought and oversold levels. MFI can become overbought (>80) and prices can simply continue higher when the uptrend is strong. Conversely, MFI can become oversold (<20) and prices can simply continue lower when the downtrend is strong. Quong and Soudack recommended expanding these extremes to further qualify signals. A move above 90 is truly overbought and a move below 10 is truly oversold. Moves above 90 and below 10 are rare occurrences that suggest a price move is unsustainable. Admittedly, many stocks will trade for a long time without reaching the 90/10 extremes. 

##handle_signals(ctx, date, current)
この関数は日ごとに呼び出される関数です。これは例えば100日分のデータのバックテストをやる場合、100回呼び出される事になります。 ここで株をどの位売買するかの決定や損切り、利益確定売りを指定します。 この関数はエンジンから直接呼び出されます。

まず変数dfにcurrent（dateの当日のデータとシグナルを含んだpandas.DataFrameオブジェクト）のコピーを代入しています。

以下に例を示します。

df["buy:sig"]が以下のようなpandas.Seriesオブジェクトの時

|証券コード   |boolean|
|:-----------:|:---:|
|jp.stock.2914|False|
|jp.stock.3382|False|
|jp.stock.4063|True |
|jp.stock.4452|False|
|jp.stock.4502|False|
|jp.stock.4503|False|
|jp.stock.6098|False|
|jp.stock.6501|False|
|jp.stock.6752|True |
|jp.stock.6758|False|
|jp.stock.6861|False|
|jp.stock.6954|False|
|jp.stock.6981|False|
|jp.stock.7203|False|
|jp.stock.7267|False|
|jp.stock.7751|False|
|jp.stock.7974|False|
|jp.stock.8031|False|
|jp.stock.8058|False|
|jp.stock.8306|True |
|jp.stock.8316|True |
|jp.stock.8411|True |
|jp.stock.8766|False|
|jp.stock.8802|False|
|jp.stock.9020|True |
|jp.stock.9022|False|
|jp.stock.9432|False|
|jp.stock.9433|False|
|jp.stock.9437|False|
|jp.stock.9984|True |

df_buyは以下のようになります。（ここでは、表示の都合上split_ratio、high_price_adj、close_price_adj、low_price_adj、volume_adj、close_priceをsplit、high_a、close_a、low_a、close、vol_aと略記し、適宜四捨五入しています。）

|             |split|high_a|close_a| low_a|  vol_a| close| MFI|buy:sig|sell:sig|
|:-----------:|----:|-----:|------:|-----:|------:|-----:|---:|------:|-------:|
|jp.stock.4063|  1.0|8700.0| 8555.0|8441.0|  2145k|8555.0|18.9|   True|   False|
|jp.stock.6752|  1.0| 993.3|  974.2| 973.1| 17404k| 974.2|14.5|   True|   False|
|jp.stock.8306|  1.0| 549.0|  542.1| 537.0| 87531k| 542.1|16.2|   True|   False|
|jp.stock.8316|  1.0|3714.0| 3669.0|3644.0| 11083k|3669.0|17.2|   True|   False|
|jp.stock.8411|  1.0| 173.4|  170.8| 170.0|161852k| 170.8|17.3|   True|   False|
|jp.stock.9020|  1.0|9698.0| 9480.0|9439.0|  2653k|9480.0|16.7|   True|   False|
|jp.stock.9984|  1.0|7775.0| 7621.0|7490.0| 13158k|7621.0|13.4|   True|   False|

また、df["buy:sig"]が全てFalseの時、df_buyはEmpty DataFrameとなります。

その後、```sec = ctx.getSecurity(sym)```によって当該銘柄のSecurityオブジェクトを取得し、```sec.order_target_percent(0.10, comment = "SIGNAL BUY")```によってこの銘柄の総保有額が総資産評価額(現金+保有ポジション評価額)に対して指定の割合となるように注文を行ないます。 第一引数はamountでここには割合(例:10%なら0.10)を指定します。第二引数はcommentでバックテスト結果に表示されます。詳しくはこちらの[QuantX公式APIリファレンス](https://determined-feynman-fb5a05.netlify.com/ja/api.html#Security)を参照してください。

売りシグナルの場合についても、ほぼ同様ですが、こちらは```sec.order_target_percent(0, comment = "SIGNAL SELL")```とします。

##結果
###直近3年
![Screen Shot 2019-02-25 at 15.21.50.png](https://qiita-image-store.s3.amazonaws.com/0/276929/5c0282c3-1a61-ca34-bfb6-822282e3f3f8.png)
損益率：16.54%
MaxDrawdown：-0.178

累積損益が日経平均の変化を下回っています。

###直近1年
![Screen Shot 2019-02-25 at 15.22.17.png](https://qiita-image-store.s3.amazonaws.com/0/276929/f13f2730-d620-0cfd-295c-eb04ad7594cb.png)
損益率：1.12%
MaxDrawdown：-0.18

次に、シグナルの条件を以下のように変更したところ下のような結果になりました。

```python
buy_sig = (mfi < 10)
sell_sig = (mfi > 90)
```

###直近3年
![Screen Shot 2019-02-25 at 15.25.08.png](https://qiita-image-store.s3.amazonaws.com/0/276929/b32380b9-b538-16ec-1e8d-c0725ea54971.png)
損益率：3.91%
MaxDrawdown：-0.058

日経平均の変化に比べて累積損益の変化量がかなり小さいです。

###直近1年
シグナル回数：0回

#考察
シグナルの条件を20から10、80から90に変更したところ極端にシグナル回数が減少しました。上記の引用にもありましたが、MFIが10を下回るか90を上回るのはかなり稀な出来事のようです。シグナルの回数が少なめの指標なので、採用する銘柄の数を増やしてもう一度バックテストをしてみました。

##結果
###直近3年
![Screen Shot 2019-02-25 at 15.45.17.png](https://qiita-image-store.s3.amazonaws.com/0/276929/8751989b-7b12-54d0-68fb-9897a682249e.png)
損益率：92.94%
MaxDrawDown：-0.151

TOPIX Core30のみの場合と比べて、損益率、MaxDrowDownともに良い結果となりました。日経平均の変化と比べて累積損益が大きく上回っています。

###直近1年
![Screen Shot 2019-02-25 at 15.45.43.png](https://qiita-image-store.s3.amazonaws.com/0/276929/f23b6bed-929f-8eb1-7892-c599db2bf77c.png)
損益：-1.28%
MaxDrawDown：-0.17

TOPIX Core30のみの場合と比べて、損益率は悪化、MaxDrowDownは僅かに改善しました。

最後に、シグナルの条件を20から10、80から90に変更し、もう一度バックテストを行いました。

###直近3年
![mfi-10-90-many-3.png](https://qiita-image-store.s3.amazonaws.com/0/276929/db395433-b2f3-a460-1afa-baf9841d3da5.png)
損益：38.73%
MaxDrawDown：-0.166

シグナルの条件を変える前と比べて、損益率、MaxDrawDownともに悪い結果となりました。買われ過ぎ、売られ過ぎの条件をより厳しくすることでTOPIX Core30の場合と同様にMaxDrawDownが改善すると予想したのですが、そうはなりませんでした。

###直近1年
![mfi-10-90-many-1.png](https://qiita-image-store.s3.amazonaws.com/0/276929/edad4aa9-9692-ab7b-7a82-79bdf2b7627b.png)
損益：0.05%
MaxDrawDown：-0.2

シグナルの条件を変える前と比べて、損益率は僅かに改善しましたが、MaxDrawDownはこれまでの試行の中で一番悪い結果となりました。

#おわりに

単一の指標のみを利用してうまく利益を出そうとすること自体ナンセンスかも知れませんが、今後複数の指標を組み合わせたり独自のアルゴリズムを作るにあたってどのような指標があるか学び、どのような結果が得られるのか知ることは意味があるのではないかと思います。

下記のURLからQuantX Factory上で今回紹介したコードのバックテストを行うことが出来ます。
https://factory.quantx.io/developer/9edcc98353544184b9253c6983874c34

採用する銘柄を増やす時に、弊社インターンの小林さんの記事を参考にしました。
[今さら聞こうよ、ROE。](https://qiita.com/Ric418/items/f02cf6e15e33611dec82)

#免責注意事項
このコードや知識を使った実際の取引で生じた損益に関しては一切の責任を負いかねますので御了承下さい。