【Python】TA-LibのMACD計算を自前でやってみる

スポンサーリンク

タイトル:【Python】TA-LibのMACD計算を自前でやってみる

Pythonには株やFXなどのテクニカル分析に便利なTA-Libと言うライブラリがあるんですけど…Pythonでしか使えないんですよね。
MacOSとかAndroid/iOSアプリで独自ツールを作ろうと思うと、それぞれの言語で自分で計算する必要が出てきます。
(まぁ、探せばそれぞれのプラットフォームでの便利ライブラリは存在すると思いますが……)

と言うわけで、各テクニカルがどんな計算で作られているかの勉強も含めて、自分で計算してみたいと思います。
当然、ライブラリを使った方が処理速度は早いと思うので、最後に速度比較もしてみます。

この記事は「MACDって言葉は知ってる」「Pythonはちょっと触ったことある」くらいの人を想定してます

スポンサーリンク

今回はMACD

今後、1つずつ指標を自前計算して行こうと考えていますが、まずはMACDを計算させます。
なぜMACDかというと……分析に有用とかそんな理由じゃなく……
計算が2つのMAの差分と言うことで、簡単でわかりやすいから! それだけです💦

ちなみに、MACDは"Moving Average Convergence/Divergence"の略らしいです。
直訳すると"移動平均の収束/発散"。つまり、移動平均が広がっているか狭まっているかが分かる指標ですね。

まずはリファレンスとしてTA-Libで計算

と言うわけで、自前計算をしていく訳ですが、正解の値は知っておかないとダメですよね。
なんかそれっぽい値が出たら完成! と言うのは流石に正確性に欠ける中途半端なものになってしまいますから。
なので、まずはTA-Libを使ってリファレンスとなる結果を作りましょう。

import pandas as pd
import numpy as np
import talib

# サンプルデータ(適当にランダムウォークで株価っぽさを演出)
close = pd.Series(np.random.randn(200).cumsum() + 100)

# TA-LibでMACD計算
macd, signal, hist = talib.MACD(close, fastperiod=12, slowperiod=26, signalperiod=9)

df = pd.DataFrame({"close": close, "macd": macd, "signal": signal, "hist": hist})

本来は対象のローソク足データからcloseを持ってきますが、ここではサンプルデータとしてランダムデータを生成しています。
次項の計算式を見ると分かりますが、最初の数十本はslowperiodsignalperiodでのEMAの計算ができないのでNaNになります。実際はMACDは26本目から、signalとhistは34本目slowperiod + signalperiod分)から計算ができるので、それまでNaNになります。
ですが、TA-LibのMACDはMACDも34本目まではNaNになるのでご注意ください。(多分、signalやhistが算出できないのにMACDだけ出しても意味がないからNaNを返しているんだと思います。)

MACDの計算式を確認

それではMACDの計算式を見てみましょう。
MACDは結構シンプルで、次の3つで構成されてます。

  • MACDライン: 短期EMA(12日) – 長期EMA(26日)
  • シグナルライン: MACDラインのEMA(9日)
  • ヒストグラム: MACDライン – シグナルライン

ここで肝になるのが次の**EMA(指数平滑移動平均)**の計算です。

EMAの計算式

MACDで使われるのは単純なMA(SMA)ではなく、最新の値の影響が出やすくなるEMA(指数平滑移動平均)が使われます。

$$\text{EMA}_i = \text{Value}_i \times \alpha + \text{EMA}_{i-1} \times (1 – \alpha)$$

$$\alpha = \frac{2}{N + 1}$$

1つ前のEMA値に(1-α)を掛けて、新しい値にαを掛けた値を足したのが新しいEMA値になります。
最初のEMA値(初期値)は、1つ前の値が存在しないので、 NN 日間の単純移動平均(SMA)を取るのが一般的のようです。

MACDを全部自分で計算

計算式も確認したところで、早速自分で計算していきましょう!
PandasにはMAとかの便利メソッドも存在しますが、それも使わず、ループと四則演算だけでEMAを実装します。
これが出来れば、Swift・Kotlin・TypeScriptなど他の言語でもそのまま実装可能となります。

def calc_ema(data, period):
    data_size = len(data)
    alpha = 2 / (period + 1)
    ema = [np.nan] * data_size

    # 先頭にNaNがある場合はオフセット
    offset = 0
    for i in range(data_size):
        if np.isnan(data[i]):
            offset += 1
        else:
            break

    # 初期値はSMA(単純移動平均)
    sum = 0
    for i in range(offset, offset + period):
        sum += data[i]
    ema[offset + period - 1] = sum / period

    # 以降はEMA計算
    for i in range(offset + period, data_size):
        ema[i] = data[i] * alpha + ema[i - 1] * (1 - alpha)

    return ema


def calc_macd(data, fastperiod=12, slowperiod=26, signalperiod=9):
    data_size = len(data)
    ema_fast = calc_ema(data, fastperiod)
    ema_slow = calc_ema(data, slowperiod)

    # MACDライン
    macd = [np.nan] * data_size
    for i in range(data_size):
        macd[i] = ema_fast[i] - ema_slow[i]
    # シグナルライン
    signal = calc_ema(macd, signalperiod)
    # ヒストグラム
    hist = [np.nan] * data_size
    for i in range(data_size):
        hist[i] = macd[i] - signal[i]

    return macd, signal, hist

# 実行
my_macd, my_signal, my_hist = calc_macd(close, fastperiod=12, slowperiod=26, signalperiod=9)
my_df = pd.DataFrame({"close": close, "macd": my_macd, "signal": my_signal, "hist": my_hist})

極力Python独自の記述も避けて、計算式の通りに実装してみました。……どうでしょう? わかりやすいかな?
先にFastとSlowの2つのEMAを計算して、その後にMACDの3つの構成を算出しています。
本来ならmacdhistの計算で、どちらかがNaNなら計算結果もNaNにする処理を明確にした方が良いのですが……Pythonでは自動的にそうなるのでそ、そこは特に意識していません。他の言語で実装する時は注意が必要かもしれません。

TA-LibのMACDと比較

一応これでMACDを自前計算できたので、TA-Libの結果を比較してみます。
比較するためのメソッドも作りました。
2つのDataFrameを比較する形で、colsで指定した項目を比較します。今はfloat数値の比較しか想定していないので、他の型は考慮していません。

# 比較 DataFrameの比較なので compare_df
def compare_df(data1, data2, cols):
    print("compare start.")
    if type(data1) is not DataFrame:
        print(f"data1 is not DataFrame. type: {type(data1)}")
        return

    if type(data2) is not DataFrame:
        print(f"data2 is not DataFrame. type: {type(data2)}")
        return

    data_size = len(data1)
    if data_size != len(data2):
        print(f"different data size data1size={data_size}, data2size={len(data2)}")
        return

    for col in cols:
        print(f"check {col}")
        col_data1 = data1[col]
        col_data2 = data2[col]
        for i in range(data_size):
            value1 = col_data1[i]
            value2 = col_data2[i]
            # 両方NaNなら一致
            if np.isnan(value1) & np.isnan(value2):
                continue
            # 片方だけNaNなら不一致
            if np.isnan(value1) | np.isnan(value2):
                print(f"{i} NG : {value1} / {value2}")
                continue
            
            # 差の確認 微妙な誤差を考慮して0.01以下の違いは無視
            if abs(value1 - value2) > 0.01:
                print(f"{i} NG : {value1} / {value2} | 差 {abs(value1 - value2)}")
                continue

    print("compare end.")

# 比較
compare_df(df, my_df, ["macd", "signal", "hist"])

さて、これを実行すると……なんと、一致しません!!!! 😱

conpare start.
check macd
25 NG : nan / 0.16042922743415033
~~~ 中略 ~~~
32 NG : nan / 0.7119618938032488
33 NG : 0.955026484343378 / 0.8788405651322648 | 差 0.07618591921111317
~~~ 中略 ~~~
61 NG : 0.029476043586910805 / 0.028767354032410708 | 差 0.0007086895545000971
check signal
33 NG : 0.6111064918875163 / 0.4482769839136075 | 差 0.16282950797390883
~~~ 中略 ~~~
46 NG : 2.2848713097371736 / 2.2594359454468558 | 差 0.025435364290317786
check hist
33 NG : 0.3439199924558617 / 0.43056358121865734 | 差 -0.08664358876279565
~~~ 中略 ~~~
69 NG : 0.03894103169848068 / 0.039399833701143944 | 差 -0.00045880200266326665
compare end.

macd25-32までは、signalを計算できないのでTA-LibではNaNを出す仕様なので問題ないとして…
どの項目でも33-60付近まで差が出ちゃってます。
データは200個なので、序盤の方だけではあるのですがちょっと気持ち悪いですね。
一応、TA-LibではEMAの初期値の扱いが違うとかいった情報もあったので探ろうとしたのですけど……解決しませんでした。😞
まぁ〜、序盤以降は一致できているので、今はこれで良しとしておきましょう。(ここを探り始めると確実に沼りそうなので)

処理時間はどのくらい違う?

最後に処理時間を比較してみます。
当然、ライブラリの方が早いとは思いますけど、どのくらい違うのか気になりますからね。
計測にはtimeitを使ってみます。

import timeit

# データは10万個用意
close = pd.Series(np.random.randn(100_000).cumsum() + 100)

# TA-Lib(1000回)
t_talib = timeit.timeit(
    lambda: talib.MACD(close, 12, 26, 9), number=1_000
)

# 自前計算(1000回)
t_manual = timeit.timeit(
    lambda: calc_macd(close, 12, 26, 9), number=1_000
)

print(f"TA-Lib:   {t_talib:.4f} sec (1,000回)")
print(f"自前計算: {t_manual:.4f} sec (1,000回)")

データ数は10万個にして、それぞれ1000回計算させた時の結果です。

TA-Lib:   1.1889 sec (1,000回)
自前計算: 271.0006 sec (1,000回)

270倍近い違いがありますね。圧倒的な遅さです。
ですが、10万データで1回の計算が0.25秒ならまぁまぁ実用範囲でしょうか?
(と言っても、マシンスペックによってはもっと遅くなるのでご注意を。ちなみに私のPCはM1 UltraのMac Studioです)

まとめ

これで、ライブラリを使わなくてもMACDを算出できたので、Python以外の言語でもすぐに対応可能になりました。
どうでしょう? EMAの算出が少し難しいかもしれませんが、比較的分かりやすかったと思います。

人によっては、ライブラリがあるのにわざわざ自分で処理をするなんて「車輪の再発明じゃん」って言われそうですけど……
個人的にはこうやって自分で紐解いて、理解して、と言う経験って必要だと思うんですよね。
今回はMACDを扱いましたが、他のテクニカルの自前計算も今後やって行こうと思います。
次は…RCIとか?(RSIはちょっと面倒そうなので……)

ではでは、また次回。

スポンサーリンク