MACDのアルゴリズムを傾きを意識して改良してみる

#自己紹介
Qiita初投稿です。
T大経済学部で経済学を専攻しているsasakiと申します。
経済学部でありながら金融系の単位取得数0という全くの金融初心者です。
これまでは主にJuliaを使ったアルゴリズム構築や、SQLを使ったサービスの分析・DB設計などをしてきましたが、Pythonは昔短期のデータ分析インターンで使ったくらいで、久しぶりに触ることになります。
今週からSmartTrade社でインターンを始めたので、そこで学んだことを随時更新していきます。

#MACDのアルゴリズムを改良してみた
##MACDとは
MACDについての説明は、SmartTradeインターン生のkatakyoさんの記事[「QuantX FactoryでMACDのアルゴリズムを作ってみよう。」](https://qiita.com/katakyo/items/63e4c6461a0c8ac861d6)を参照してもらえると良いかと思います。
今回は、こちらで作ったMACDのアルゴリズムを、MACD線・シグナル線の傾きを利用して、よりだましに強い頑健なものへと改良してみたいと思います。
なお、改良前の単にゴールデンクロス・デッドクロスでシグナルを出すアルゴリズムのままだと、以下の結果になりました。
![スクリーンショット 2018-12-13 15.21.38.png](https://qiita-image-store.s3.amazonaws.com/0/325611/da809b5f-8431-8394-fd2f-0fb0a9b9611c.png)
シグナル回数が相当数多いのが気になりますね。ここからシグナルを出すクロスの制約を追加して、よりだましに強いものにしていきます。

##今回作ったコードについて
以下、今回書いたコードの全文です。
QuantX Factory上にコピペして動かすことができると思います。
（[QuantX Factory](https://factory.quantx.io/)とは、SmartTrade社が提供している、株式や仮想通貨などの売買ルール(アルゴリズム)の作成、販売ができるシステムトレードプラットフォームです。）

```python:macd.py
############################################################################
# Sample Algorithm
import pandas as pd
import numpy as np 
import talib as ta 

def initialize(ctx):
    # 設定
    ctx.logger.debug("initialize() called")
    ctx.configure(
      channels=
      {
        	"jp.stock": {
        		"columns": [
        			"close_price_adj"
        		],
        		"symbols": [
              "jp.stock.1414", "jp.stock.2269",
              "jp.stock.2292", "jp.stock.2371",
              "jp.stock.2379", "jp.stock.2413",
              "jp.stock.2427", "jp.stock.2702",
              "jp.stock.2782", "jp.stock.2809",
              "jp.stock.3003",
              "jp.stock.3053", 
              "jp.stock.3092", "jp.stock.3141",
              "jp.stock.3382", "jp.stock.3407",
              "jp.stock.3665", "jp.stock.3676",
              "jp.stock.4063", "jp.stock.4343",
              "jp.stock.4452", "jp.stock.4519",
              "jp.stock.4543", "jp.stock.4704",
              "jp.stock.4751", "jp.stock.4911",
              "jp.stock.4974", "jp.stock.5108",
              "jp.stock.6028", "jp.stock.6098",
              "jp.stock.6194", "jp.stock.6503",
              "jp.stock.6645", "jp.stock.6758",
              "jp.stock.6866", "jp.stock.7269",
              "jp.stock.7272", "jp.stock.7606",
              "jp.stock.7936", "jp.stock.8020",
              "jp.stock.8252", "jp.stock.8591",
              "jp.stock.8929", "jp.stock.9007",
              "jp.stock.9086", "jp.stock.9433",
              "jp.stock.9613"
              ]
        	}
        }
    )

    def _my_signal(data):
      cp = data["close_price_adj"].fillna(method='ffill')
      macd = pd.DataFrame(data=0,columns=cp.columns,index=cp.index)
      macdsignal = pd.DataFrame(data=0,columns=cp.columns,index=cp.index)
      macdhist = pd.DataFrame(data=0,columns=cp.columns,index=cp.index)
      dmacd = pd.DataFrame(data=0,columns=cp.columns,index=cp.index)
      dmacdsignal = pd.DataFrame(data=0,columns=cp.columns, index=cp.index)
      for (sym,val) in cp.items():
        macd[sym], macdsignal[sym], macdhist[sym] = ta.MACD(cp[sym].values.astype(np.double), fastperiod=9, slowperiod=26, signalperiod=9)
        dmacd[sym] = np.gradient(macd[sym])
        dmacdsignal[sym] = np.gradient(macdsignal[sym])
      
      #通常のMACD
      #buy_sig = ((macd > macdsignal) & (macd.shift(1) < macdsignal.shift(1)))
      #sell_sig = ((macd < macdsignal) & (macd.shift(1) > macdsignal.shift(1)))
      
      #改良版
      buy_sig = ((macd > macdsignal) & (macd.shift(1) < macdsignal.shift(1)) & (dmacd/dmacdsignal>5.0) & (abs(dmacdsignal) > 1.0))
      sell_sig = ((macd < macdsignal) & (macd.shift(1) > macdsignal.shift(1)) & (dmacd/dmacdsignal>5.0) & (abs(dmacdsignal) > 1.0))
      
      return {
        "macd": macd,
        "macdsignal": macdsignal,
        "hist": macdhist,
        "dmacd": dmacd,
        "dmacdsignal": dmacdsignal,
        "buy:sig": buy_sig,
        "sell:sig": sell_sig,
      }

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

def handle_signals(ctx, date, current):
    '''
    current: pd.DataFrame
    '''

    df = current.copy()

    # 買いシグナル
    df_long = df[df["buy:sig"]]
    if not df_long.empty:
      for (sym, val) in df_long.iterrows(): 
        #ctx.logger.info(val)
        sec = ctx.getSecurity(sym)
        msg = "買いシグナル"
        sec.order_target_percent(0.10, comment= msg)

    # 売りシグナル
    df_sell = df[df["sell:sig"]]
    if not df_sell.empty:
      for (sym, val) in df_sell.iterrows(): 
        sec = ctx.getSecurity(sym)
        msg = "売りシグナル"
        sec.order_target_percent(0, comment= msg)
```

##改良のポイント
###①角度のついたクロスのみ判定する
MACD線が角度をつけてシグナル線を超えていくということは、上昇あるいは下落に短期的な勢いがある証拠であり、強い売買シグナルの指標となります。
MACDとシグナルの数値勾配を計算し、クロス時の勾配比率が一定以上になったもののみを採用することで、もみ合いの中で発生するクロスを除外しました。

```python:macd.py
#MACDとシグナルの勾配を格納するDataFrameを作成
dmacd = pd.DataFrame(data=0,columns=cp.columns,index=cp.index)
dmacdsignal = pd.DataFrame(data=0,columns=cp.columns, index=cp.index)
for (sym,val) in cp.items():
  macd[sym], macdsignal[sym], macdhist[sym] = ta.MACD(cp[sym].values.astype(np.double), fastperiod=9, slowperiod=26, signalperiod=9)
  #銘柄ごとに数値勾配を計算
  dmacd[sym] = np.gradient(macd[sym])
  dmacdsignal[sym] = np.gradient(macdsignal[sym])
```

今回はMACDの傾きがシグナルの傾きの５倍以上の時のクロスのみ採用する。

```python:macd.py
buy_sig = (... & (dmacd/dmacdsignal>5.0) & ...)
```


###②シグナルが明確なトレンドを表している時のみ判定する
シグナルが水平でなく、相場が明確なトレンドを指し示している時のクロスのみ判定したいので、シグナル線の勾配の絶対値を一定以上の時のみ評価するようにする。今回は1以上を採用。

```python:macd.py
buy_sig = (... & (abs(dmacdsignal) > 1.0))
```

##結果
![スクリーンショット 2018-12-13 15.20.12.png](https://qiita-image-store.s3.amazonaws.com/0/325611/15afa3a9-959e-7e39-6e9e-bac1f1e5ce87.png)
シグナル回数がぐっと減り、損益率・シャープレシオともに改善していると思います。
ベータが依然として高いところが気になりますね。
他の指標と組み合わせることで改善していくのか、今後検討してみたいです。