Help us understand the problem. What is going on with this article?

QuantXでMFIを実装してみた

はじめに

株式会社Smart Tradeが提供している投資アルゴリズム開発プラットフォーム「QuantX」で、MFIを実装しました。

MFIとは

MFIとはMoney Flow Indexの略で相場の中でお金が買い、売りのどちらに流れているのかを株価と出来高を元に判断する、オシレーター指標のひとつです。

定義式

まず、高値をHigh、安値をLow、終値をClose、出来高をVolumeとします。

TypicalPrice = \frac{(High + Low + Close)}{3}\\
MoneyFlow = TypicalPrice\times Volume\\

次に、前日比でTypicalPriceが上昇したn日のMoneyFlowの合計と、前日比でTypicalPriceが下落または変わらずだったn日のMoneyFlowの合計を求めます。n日は任意の日数ですが、一般的には14日が良く用いられているそうです。

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

PositiveMoneyFlow = MoneyFlow

$else$

NegativeMoneyFlow = MoneyFlow

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

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の形式に従って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.

シグナル生成部分では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についての解説を参考にしました。

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リファレンスを参照してください。

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

結果

直近3年

Screen Shot 2019-02-25 at 15.21.50.png
損益率:16.54%
MaxDrawdown:-0.178

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

直近1年

Screen Shot 2019-02-25 at 15.22.17.png
損益率:1.12%
MaxDrawdown:-0.18

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

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

直近3年

Screen Shot 2019-02-25 at 15.25.08.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
損益率:92.94%
MaxDrawDown:-0.151

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

直近1年

Screen Shot 2019-02-25 at 15.45.43.png
損益:-1.28%
MaxDrawDown:-0.17

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

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

直近3年

mfi-10-90-many-3.png
損益:38.73%
MaxDrawDown:-0.166

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

直近1年

mfi-10-90-many-1.png
損益:0.05%
MaxDrawDown:-0.2

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

おわりに

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

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

採用する銘柄を増やす時に、弊社インターンの小林さんの記事を参考にしました。
今さら聞こうよ、ROE。

免責注意事項

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした