[Python / pandas] ハル移動平均（Hull Moving Average）をcolaboratory上で描画

# 概要

ハル移動平均（Hull Moving Average）というものをたまたま知ったので、Pythonを使って描画してみようと思い立って書いた記事。


# ハル移動平均（Hull Moving Average）とは

Oandaの解説を見ていただきたい！

https://www.oanda.jp/lab-education/technical_analysis/moving_average/hull/#:~:text=%E3%83%8F%E3%83%AB%E7%A7%BB%E5%8B%95%E5%B9%B3%E5%9D%87%E7%B7%9A%E3%81%AF,%E3%81%AB%E3%82%88%E3%82%8A%E9%96%8B%E7%99%BA%E3%81%95%E3%82%8C%E3%81%BE%E3%81%97%E3%81%9F%E3%80%82

計算方法も上記記事に載っています！(^ワ^*)


# 動作環境

Google Colaboratory (2023/01/12 現在は動作します)


# ソースコード

## 1. モジュール類

`mplfinance`は、ローソク足を描画するために使います。
colaboratoryにはinstallされていないので、installしてからimportします。

```python:colaboratory
!pip install -q mplfinance

import math  # 端数切捨てするために使う

import matplotlib.pyplot as plt
import mplfinance.original_flavor as mpf
import numpy as np
import pandas as pd
```

## 2. データ読み込み

以下のような`DataFrame`を用意してください。
`open` `high` `low` `close` `time`の5列があれば他はいらないです。
最低でも200行ぐらいあれば、いい感じに見えると思います。

```python:colaboratory
df = pd.read_csv("/content/230112_GBP_JPY_500days_M15.csv")
df.head()

	open	high	low     close	volume	complete  time
0	151.100	151.134	151.092	151.130	89      True      2021-08-30 04:45:00
1	151.128	151.139	151.098	151.119	111     True      2021-08-30 05:00:00
2	151.116	151.120	151.070	151.079	100     True      2021-08-30 05:15:00
3	151.082	151.098	151.068	151.084	83      True      2021-08-30 05:30:00
4	151.086	151.090	151.070	151.089	117     True      2021-08-30 05:45:00
```

## 3. ハル移動平均の計算

前記Oandaの記事を読むと、加重ハル移動平均を計算するためには`WMA（加重移動平均）`を計算する必要があるようですね。

WMAの計算方法は、以下の記事を参考にさせていただきました。

https://bigdata-tools.com/moving-average/#2Weighted_Moving_Average_WMA

```python:colaboratory
# ---------------------------------------------------------
# ハル移動平均の計算処理
# ---------------------------------------------------------
def wma_of(price_series: pd.Series, window_size: int = 9) -> pd.Series:
    """
    指定されたwindow_sizeのwma（加重移動平均）を計算

      ※記事を参考にして書いた関数
    """
    def wma(rolled_price_series: pd.Series) -> pd.Series:
        """
        rollingされた窓分だけのseriesに対する処理
        """
        weight = np.arange(len(rolled_price_series)) + 1
        wma = np.sum(weight * rolled_price_series) / weight.sum()
        return wma

    wma_result: pd.Series = price_series.rolling(window_size).apply(wma, raw=True).round(3)
    return wma_result


def hull_ma(close_series: pd.Series, window: int = 9) -> pd.Series:
    """
    ハル移動平均を計算
    """
    # n / 2
    half_of_window_size: int = window // 2
    # n の平方根
    sqrt_of_window_size: int = math.floor(np.sqrt(window))

    # A: n÷2日WMAを2倍したもの
    double_wma_of_half_window_size: pd.Series = wma_of(close_series, half_of_window_size) * 2
    # B: n日WMA
    wma_of_window_size: pd.Series = wma_of(close_series, window)

    # A - Bの結果を、nの平方根日WMAで均す
    return wma_of(
        double_wma_of_half_window_size - wma_of_window_size,  # A-B
        sqrt_of_window_size
    )
```

上記処理の動作確認をしておきます。
試しにwindow_sizeを20本にして算出してみます。

```python:colaboratory
hull_ma_20: pd.Series = hull_ma(df["close"], 20)
hull_ma_20

0            NaN
1            NaN
2            NaN
3            NaN
4            NaN
          ...   
34232    160.080
34233    160.097
34234    160.112
34235    160.119
34236    160.120
Name: close, Length: 34237, dtype: float64
```

計算するだけで良ければ、ここまでで終了ですよ。

## 4. 描画

本質的ではないんですけど、ここが一番手間がかかりますよね。
せっかくなので、Oandaの記事に表示されていたように、上昇時と下降時の色を変えておきましょう。


```python:colaboratory
# 量が多すぎると描画時間がかかりすぎるので、末尾300行のみを抽出しておく
target_df = df.iloc[-300:]

hull_ma_20: pd.Series = hull_ma(df["close"], 20)
target_hull_ma = hull_ma_20.iloc[-300:]


# ---------------------------------------------------------
# ローソク足の描画
# ---------------------------------------------------------
figure, (axis1) = plt.subplots(1, 1, figsize=(20,10))
mpf.candlestick2_ohlc(
    axis1,
    opens=target_df["open"],
    highs=target_df["high"],
    lows=target_df["low"],
    closes=target_df["close"],
    width=0.6,
    colorup="#77d879",
    colordown="#db3f3f",
)


# ---------------------------------------------------------
# ハル移動平均の描画
# ---------------------------------------------------------
# 1つ前の値より大きければTrue、小さければFalseが入っているSeriesを作成
hull_up_index: pd.Series = target_hull_ma > target_hull_ma.shift(1)

# ハル移動平均のseriesを up中 と down中 それぞれのseriesに分離して描画
hull_ma_only_up = target_hull_ma.copy()
hull_ma_only_down = target_hull_ma.copy()
hull_ma_only_up[~hull_up_index] = None  # up中の値を残し、down中の値を None で置き換え
hull_ma_only_down[hull_up_index] = None  # down中の値を残し、up中の値を None で置き換え

plt.plot(target_df["time"], hull_ma_only_up, "-", label="hull_ma_20_up", linewidth=2)
plt.plot(target_df["time"], hull_ma_only_down, "-", label="hull_ma_20_down", linewidth=2)


# ---------------------------------------------------------
# 目盛り類と凡例情報の描画
# ---------------------------------------------------------
def prepare_xticks(sr_time) -> np.ndarray:
    """
    xticks(x軸の目盛り)を全部表示すると目障りなので、量を24分の1個にするための間引き処理
    """
    num_break_xticks_into = 24
    xticks_size = int(len(sr_time) / num_break_xticks_into)
    xticks_index = range(0, len(sr_time), xticks_size) if xticks_size > 0 else []
    return xticks_index

xticks = prepare_xticks(target_df["time"])
plt.xticks(xticks, rotation=75)
plt.xlabel("time")
plt.ylabel("price")
plt.legend()


# ---------------------------------------------------------
# 上記描画対象をcolaboratory上に表示
# ---------------------------------------------------------
plt.show()
```

![hull.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327407/bcd8b753-067b-1193-fa98-9d26680183bc.png)

でけたヾ(\*´∀｀\*)ﾉ

ハル移動平均の描画については、上昇中と下降中で色を変えてるので、ソースコードがちょっとややこしいですね。
でも、普通に描画するだけなら描画処理を記事に書く旨味があまり無いと思ったので、敢えて色を変えてみました。


# 終わりに

ハル移動平均やWMAの計算方法に間違いがあったら、ご指摘いただけるとありがたいです！


# 参考資料

## Hull Moving Averageとは

Oandaによる解説記事

https://www.oanda.jp/lab-education/technical_analysis/moving_average/hull/

## WMA（加重移動平均）の計算方法

こちらの記事を参考にしました

https://bigdata-tools.com/moving-average/#2Weighted_Moving_Average_WMA

## matplotlib使い方

公式ドキュメント

https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html

## ハル移動平均の上昇/下降の切り替わりポイントで色を変えるために

matplotlib.pyplot.plot関数では、`None`が描画されないことが以下の記事で紹介されています

https://qiita.com/_Alignof/items/2200bc4448a861b826e5