macdでトレード

# はじめに

移動平均線のゴールデンクロスよりもシグナルが早く出るということで人気のテクニカル指標macd。このmacdによる売買はテクニカル分析を紹介するサイトで数多く取り上げられています。

前回、<a href="https://qiita.com/SaturdayInThePark/items/38bc30044cb31270f2b5">macdには騙しが多く使えないのではないか</a> ということを検証してみましたが、この検証における売買方針は、macdゴールデンクロスの翌日寄り買いし、10日後に売るというものでした。この10日という期間にあまり根拠はなく説得力がなかったので、今回はmacdゴールデンクロスの翌日寄り買いデッドクロスが出るまでホールドし、デッドクロスの翌日寄り売りの売買方針ではどうなるかを検証してみたいと思います。

チャートの薄赤の期間買い持ちしているようなかんじですね。この例では、ホールド中上がり続けていることもあるけど、売りシグナルが出るのが遅くて売り時を逃しているのが多い印象です。

![1414.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/483341/196f4bc6-a7a1-5d29-be3b-24722541a332.png)

# macdゴールデンクロスで買いデッドクロスで売るとどうなるかの先行研究

実は、macdゴールデンクロスで買いデッドクロスで売るとどうなるかは、すでに検証されています。

<a href="https://www.systemtrade-kabu.com/macd/">MACDは有効か？【用語解説と統計データ公開】</a>西村剛さんの記事にきれいにまとめられています。

以下、抜粋。

~~~
検証対象：日経平均採用銘柄（225種銘柄）
検証期間：2000/01/01～2020/09/30
1銘柄当たりの投資金額：20万円
【買い条件】・MACD（12日、26日）がシグナル（9日）を上抜けした銘柄
⇒上記を満たした翌日に成行で買い
【売り条件】・MACD（12日、26日）がシグナル（9日）を下抜け
⇒上記を満たした翌日に成行で売り

勝率： 40.13 ％
勝ち数： 18,561 回
負け数： 27,691 回
引き分け数： 676 回

平均損益（円）： 546 円　　平均損益（率）： 0.27 ％
平均利益（円）： 13,762 円　　平均利益（率）： 6.88 ％
平均損失（円）： -8,299 円　　平均損失（率）： -4.15 ％

合計損益（円）： 25,635,459 円　　合計損益（率）： 12,815.52 ％
合計利益（円）： 255,437,850 円　　合計利益（率）： 127,723.44 ％
合計損失（円）： -229,802,391 円　　合計損失（率）： -114,907.92 ％

Profit Factor： 1.112
平均保持日数： 17.29 日

勝率は40％と低いものの、平均損益はプラスとなっています。
損益の推移を確認すると、一部期間を除いて、おおむね右肩上がりの推移となっています。
以上から、MACDは、統計的に有効なテクニカル指標と判断できるでしょう。
~~~

これとは別の記事、拙作<a href="https://qiita.com/SaturdayInThePark/items/38bc30044cb31270f2b5">「騙しに騙されるMACDテクニカル分析」</a> での検証結果。

~~~
検証対象：TOPIX500採用銘柄
検証期間：2015年1月1日～2020年1月20日
結論
ゴールデンクロスの60.3%が騙しであった。
平均損益率： 0.3795%
~~~

勝率＝100％ー騙しの割合<br/>
と解釈すると、西村剛さんの記事のゴールデンクロス買いの勝率40.13 ％と結果が一致しています。

拙作ではゴールデンクロスの10日後に反対売買、西村剛さんの記事ではデッドクロスで売りの違いはあるものの、平均損益率も近い値となっています。


# macdゴールデンクロス売買の追試

あらためて、以下の条件でmacdゴールデンクロスで買いデッドクロスで売りの追試をしてみます。

~~~
検証対象：TOPIX500採用銘柄）
検証期間：2015/01/05～2020/01/20
【買い条件】・MACD（12日、26日）がシグナル（9日）を上抜けした銘柄
⇒上記を満たした翌日に成行で買い
【売り条件】・MACD（12日、26日）がシグナル（9日）を下抜け
⇒上記を満たした翌日に成行で売り
~~~

検証データは<a href="https://github.com/etekichi98/data_topix500/blob/main/data_topix500_20200120.zip">コチラ。</a>

検証コードは以下。本記事はpythonコードの巧みさでマウントをとるためのものではないので、コードは雑です。（というか、そもそも巧みなコードなど書けない。）

~~~python:test_macd_trading.py
# -*- coding: utf-8 -*-
"""
macd-signalゴールデンクロスで買い、デッドクロスで売り
"""
import glob
import os
import math
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

DATA_PATH = "../data/"

def calc_macd(df, es, el, sg):
    macd = pd.DataFrame()
    macd['ema_s'] = df['Close'].ewm(span=es).mean()
    macd['ema_l'] = df['Close'].ewm(span=el).mean()
    macd['macd'] = macd['ema_s'] - macd['ema_l']
    macd['signal'] = macd['macd'].ewm(span=sg).mean()
    macd['diff'] = macd['macd'] - macd['signal']
    f_plus = lambda x: x if x > 0 else 0
    f_minus = lambda x: x if x < 0 else 0
    macd['diff+'] = macd['diff'].map(f_plus)
    macd['diff-'] = macd['diff'].map(f_minus)
    return macd

def calc_gc(shorts, longs):
    # ゴールデンクロス検出
    gcs = []
    short0 = shorts[0]
    long0 = longs[0]
    for i in range(len(shorts)):
        short = shorts[i]
        long = longs[i]
        if not math.isnan(short0) and not math.isnan(long0) and not math.isnan(short) and not math.isnan(long):
            if (short0-long0)<0 and (short-long)>0:
                gcs.append(shorts.index[i])
        short0 = short
        long0 = long
    return gcs

def calc_gains(df, trades):
    gains = []
    if len(trades)==0:
        return gains
    for trade in trades:
        buy = df['Open'][trade[0]]
        sell = df['Open'][trade[1]]
        gain = sell/buy - 1.0
        gains.append(gain)
    return gains

def make_macd_trades(df, macd):
    # macd-signalゴールデンクロスで買い
    # macd-signalデッドクロスで売り
    macd_gcs = calc_gc(macd['macd'], macd['signal'])
    macd_dcs = calc_gc(macd['signal'], macd['macd'])

    trades = []
    hold = False    
    for i in range(len(macd)-1):
        if macd.index[i] in macd_gcs: # 買いシグナル
            hold = True
            buy_date = macd.index[i+1]
        elif macd.index[i] in macd_dcs: # 売りシグナル
            if hold:
                sell_date = macd.index[i+1]
                trades.append([buy_date, sell_date])
                hold = False
    if hold:
        sell_date = macd.index[-1]
        trades.append([buy_date, sell_date])
    return trades

def walk_around(term=1300):
    all_gains = []
    for i in range(1, 10):
        csv_files = glob.glob(DATA_PATH+'{}/*.csv'.format(i*1000))
        for csv_file in csv_files:
            filename = os.path.basename(csv_file)
            code = int(filename[0:4])
            code_dir = int(code/1000)*1000
            input_path = DATA_PATH + str(code_dir) + "/" + str(code) + ".csv"
            df = pd.read_csv(input_path, header=None,
                         names=['Date','Open','High','Low','Close','Volume'],
                         encoding='UTF-8')
            macd = calc_macd(df, 12, 26, 9)
            trades = make_macd_trades(df, macd)
            gains = calc_gains(df, trades)
            all_gains.extend(gains)
    return all_gains

def test1(term=100):
    all_gains = walk_around(term=term)

    wins = [ gain for gain in all_gains if gain>0 ]
    loses = [ gain for gain in all_gains if gain<0 ]
    draws = [ gain for gain in all_gains if gain==0 ]

    print("wins: {} {}".format(len(wins), np.mean(wins)))
    print("loses: {} {}".format(len(loses), np.mean(loses)))
    print("draws: {} {}".format(len(draws), np.mean(draws)))

    plt.hist(all_gains, bins=100)
    mean = np.mean(all_gains)
    print(mean)

def main():
    test1(term=1300)

if __name__ == '__main__':
    main()
~~~

実行結果

~~~
wins: 9601 0.06514704638000195
loses: 14642 -0.03605581596854157
draws: 211 0.0
0.003989021627668806
~~~

先行研究の結果フォーマットにあわせると、

~~~
勝率： 9601/(9601+14642+211) = 39.26 ％
勝ち数： 9601 回
負け数： 14642 回
引き分け数： 211 回

平均損益（率）： 0.3989 ％
平均利益（率）： 6.514 ％
平均損失（率）： -3.605 ％

Profit Factor： 6.514*9601/(3.605*14642)=62,502.51/52,784.41=1.1841
~~~

対象銘柄や検証期間は異なるものの、西村剛さんの記事の結果とほぼ同じになりました。

# 素朴な疑問

さて、ここからが本題です。

これまでの検証で、確かに、macdゴールデンクロスで買いデッドクロスで売りを繰り返すと、着実に利益が積み上がることが判りました。

ここで１つ素朴な疑問。過去20年では確かにそうなったのだろうけど、そもそも、検証期間2000/01/01～2020/09/30の20年で日経平均は、19002円から27444円に44.4%に上がっている訳ですから、単に日経平均連動投信を買ってその間寝かせているだけで相当の利益が出た訳です。macdゴールデンクロス売買はその利益より大きいのでしょうか？

そこで、なるべく条件を合わせてmacd売買と日経平均連動投信寝かせ売買のパフォーマンスを比較してみます。西村剛さんの記事の結果を使用します。

パフォーマンスを比較のため、総損益率を計算してみます。総損益率は、検証期間のすべての売買の損益率の合計で、全対象銘柄を一定額投資したとき、総損益率×投資額が総損益になります。

macd売買の場合は、毎回一定金額づつ（例えば100万円とか）売買するものとします（実際はこのような売買はできないけど）。macd売買で、対象期間中平均で何銘柄数分をホールドしていたかを計算し、その銘柄数分×100万円分の日経平均連動投信を購入したとして、パフォーマンスを比較します。


macd売買の総損益率

~~~
検証期間：2000/01/01～2020/09/30の7305日+270日=7575日間

1回のmacd売買における、1日当たりの損益率：損益率／検証期間＝0.27％／7575
売買回数：勝ち数＋負け数＋引き分け数＝18,561＋27,691＋676＝46,928回
のべ投資日数：平均保持日数×売買回数＝17.29日×46,928回＝811385日
1日当たりの売買回数＝売買回数／検証期間＝46,928回／7575日＝6.195回 (※1)
1日当たりの投資回数：のべ投資日数／検証期間＝811385／7575＝107.11回 (※2)

総損益率＝Σ(1日当たりの損益率)＝(Σ損益率)／7575＝0.27％×46928／7575＝126.705／7575

売買手数料（概算）を考慮すると、
SBI証券の例100万円で往復1280円（0.128％）
総損益率＝Σ(1日当たりの損益率)＝(Σ損益率)／7575 = (0.27％ー0.128％)×46928／7575＝66.638／7575

~~~

日経平均売買の総損益率

~~~
検証期間：2000/01/01～2020/09/30の7575日間
この間で、日経平均は、19002から27444に44.4%上昇している。

一日当たりの損益率＝0.444／7575

macd売買と投資規模を合わせる。macd売買では、1日平均107.11銘柄を (※2) ホールドしたのだから、

Σ(1日当たりの損益率) = 107.11×0.444／7575＝47.557／7575

~~~

売買手数料を考慮しなければ、、macd売買の方が126.705／47.557＝2.66倍パフォーマンスが良い

売買手数料を考慮すると、macd売買の方が66.638／47.557＝1.40倍パフォーマンスが良い

20年間せっせとmacdゴールデンクロス売買を繰り返すと、日経平均連動投信を買って20年間寝かせておくよりも1.40倍成績が良かった！この倍率を多いとみるか少ないとみるかは悩ましいところですが、macd売買は毎日平均6.195回 (※1)、往復だと12.39回 の売買をしなければならないので、その労力を考慮すると、そんなにおいしい訳ではないような。

# まとめ

西村剛さんの記事では、平均損益はプラスで、損益の推移はおおむね右肩上がりだから、MACDは統計的に有効なテクニカル指標であると結論付けています。
それは事実ではありますが、検証期間で対象銘柄は指数で44%上がっている訳で、検証期間に日経平均連動投信を寝かせておいた場合とパフォーマンスを条件をあわせて比較すると、1.4倍良いだけにすぎない。または1.4倍も良いと見るべきなのか。

# おまけ

最初の図を描いたpythonコードを載せておきます。macd信者様にとってはそれなりに有益かも。私はmacd信者ではなくなりましたので、もはや不要となりました。

コードをコピペしてすぐ動くようにするため、calc_macd(), calc_gc()のコードは先のソースと重複しています。

~~~python:test_macd_chart.py
# -*- coding: utf-8 -*-
"""
macdチャート
"""
import math
import mplfinance as mpf
import numpy as np
import pandas as pd

DATA_PATH = "../data/"

def calc_macd(df, es, el, sg):
    macd = pd.DataFrame()
    macd['ema_s'] = df['Close'].ewm(span=es).mean()
    macd['ema_l'] = df['Close'].ewm(span=el).mean()
    macd['macd'] = macd['ema_s'] - macd['ema_l']
    macd['signal'] = macd['macd'].ewm(span=sg).mean()
    macd['diff'] = macd['macd'] - macd['signal']
    f_plus = lambda x: x if x > 0 else 0
    f_minus = lambda x: x if x < 0 else 0
    macd['diff+'] = macd['diff'].map(f_plus)
    macd['diff-'] = macd['diff'].map(f_minus)
    return macd

def calc_gc(shorts, longs):
    # ゴールデンクロス検出
    gcs = []
    short0 = shorts[0]
    long0 = longs[0]
    for i in range(len(shorts)):
        short = shorts[i]
        long = longs[i]
        if not math.isnan(short0) and not math.isnan(long0) and not math.isnan(short) and not math.isnan(long):
            if (short0-long0)<0 and (short-long)>0:
                gcs.append(shorts.index[i])
        short0 = short
        long0 = long
    return gcs

def make_macd_trades(df, macd):
    # macd-signalゴールデンクロスで買い
    # macd-signalデッドクロスで売り
    macd_gcs = calc_gc(macd['macd'], macd['signal'])
    macd_dcs = calc_gc(macd['signal'], macd['macd'])

    trades = []
    hold = False    
    for i in range(len(macd)-1):
        if macd.index[i] in macd_gcs: # 買いシグナル
            hold = True
            buy_date = macd.index[i+1]
        elif macd.index[i] in macd_dcs: # 売りシグナル
            if hold:
                sell_date = macd.index[i+1]
                trades.append([buy_date, sell_date])
                hold = False
    if hold:
        sell_date = macd.index[-1]
        trades.append([buy_date, sell_date])
    return trades

def calc_ylim(ser):
    max1 = ser.max()
    min1 = ser.min()
    if max1>0 and min1<0:
        max2 = max(max1, -min1)
        return max2, -max2
    elif max1>0 and min1>=0:
        return max1, 0
    else:
        return 0, min1
    
def draw_macd_chart(df):
    macd = calc_macd(df, 12, 26, 9)
    trades = make_macd_trades(df, macd)
    mh, ml = calc_ylim(macd['macd'])
    sh, sl = calc_ylim(macd['signal'])
    
    hold_line = [np.nan for i in range(len(df))]
    vlines = []
    vcolors = []
    for trade in trades:
        n = len(df['Close'][trade[0]:trade[1]].index)
        x0 = df.index.get_loc(trade[0])
        x1 = x0 + n
        y0 = df['Open'][trade[0]]
        y1 = df['Open'][trade[1]]
        a = (y1-y0)/n
        b = (x1*y0-x0*y1)/n
        for j in range(n-1):
            vlines.append(df['Close'][trade[0]:trade[1]].index[j])
            vcolors.append('r')
            y = a * (x0+j) + b
            hold_line[x0+j] = y
    macd['hold_line'] = hold_line
    vlines1 = dict(vlines=vlines,colors=vcolors,alpha=0.1, linewidths=2.5)    
    add_plot  = [
        mpf.make_addplot(macd[['hold_line']], color='r'),
        mpf.make_addplot((macd[['macd', 'signal']]), panel=1, ylim=[min(ml, sl), max(mh, sh)]),
        mpf.make_addplot((macd['diff+']), type='bar', color='r', panel=1, ylim=[min(ml, sl), max(mh, sh)]),
        mpf.make_addplot((macd['diff-']), type='bar', color='b', panel=1, ylim=[min(ml, sl), max(mh, sh)]),
    ]
    mpf.plot(df, type='candle', addplot=add_plot,
             vlines=vlines1, volume=False)    
    return

def main():
    code = 1414
    term = 120
    code_dir = int(code/1000)*1000
    input_path = DATA_PATH + str(code_dir) + "/" + str(code) + ".csv"
    df = pd.read_csv(input_path, header=None,
                 names=['Date','Open','High','Low','Close','Volume'],
                 encoding='UTF-8')
    df['Date'] = pd.to_datetime(df['Date'])
    df = df.set_index("Date")
    df = df.tail(term)
    draw_macd_chart(df)

if __name__ == '__main__':
    main()
~~~