matplotlibでローソク足を自作する(mplfinanceを使わない)

# やること
pythonでローソク足を表示する方法として、mplfinanceというライブラリがあります。
だいたいはこれで事足りるのですが、ローソク足チャートをsubplotとして使いたいときや、カスタマイズしたいときなど、どうすればいいかわからなかったので、自作してみることにしました。
↓こんな感じのチャートをmatplotlibで自作します。

![スクリーンショット 2021-10-05 23.33.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1640073/8c26f2f8-a312-c96f-fbea-9766d5c64522.png)


# 実装
matplotlibのAxesオブジェクトとローソク足データの入ったDataFrameを渡して、ローソク足を描画する関数を作りました。

```python
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import dates as mdates
def ohlcv_plot(ax,df):
    """
    matplotlibのAxesオブジェクトにローソク足を描画する.
    :param ax:ローソク足を描画するAxesオブジェクト.
    :param df:DataFrameオブジェクト. 必要なカラムはtimestamp,open,high,low,close,volume.
    """
    # timestampからdatetimeを作りindexにする。datetimeは日本時間を指定。
    df["datetime"]=pd.to_datetime(df["timestamp"], unit='s')
    df=df.set_index("datetime")
    df=df.tz_localize('utc').tz_convert('Asia/Tokyo')

    # ローソク足の幅を設定
    # matplotlib上でwidth=1->1日となるのでローソク足の時間軸に応じて幅を設定
    time_span=df["timestamp"].diff()[1]
    w=time_span/(24*60*60)
    
    # ローソク足
    # 陽線と陰線で色を変えるため、それぞれのindexを取得
    idx1=df.index[df["close"]>=df["open"]]
    idx0=df.index[df["close"]<df["open"]]

    # 実体
    df["body"]=df["close"]-df["open"]
    df["body"]=df["body"].abs()
    ax.bar(idx1,df.loc[idx1,"body"],width=w * (1-0.2),bottom=df.loc[idx1,"open"],linewidth=1,color="#33b066",zorder=2)
    ax.bar(idx0,df.loc[idx0,"body"],width=w * (1-0.2),bottom=df.loc[idx0,"close"],linewidth=1,color="#ff5050",zorder=2)

    # ヒゲ
    # zorderを指定して実体の後ろになるようにする。
    ax.vlines(df.index,df["low"],df["high"],linewidth=1,color="#666666",zorder=1)
    
    # 指標 (SMA,BB)
    window=20

    # SMA
    sma=df["close"].rolling(window).mean()
    ax.plot(df.index,sma,linewidth=.5)

    # ボリンジャーバンド
    window=20
    sigma=df["close"].rolling(window).std()
    bb_up=sma+2*sigma
    bb_low=sma-2*sigma
    ax.plot(df.index,bb_up,color="#FF5B11",linewidth=.5)
    ax.plot(df.index,bb_low,color="#FF5B11",linewidth=.5)
    
    # 価格目盛調整
    # グラフ下部に出来高の棒グラフを描画するので、そのためのスペースを空けるよう目盛を調整する
    ymin,ymax=df["low"].min(),df["high"].max()
    ticks=ax.get_yticks()
    margin=(len(ticks)+4)/5
    tick_span=ticks[1]-ticks[0]
    min_tick=ticks[0]-tick_span*margin
    ax.set_ylim(min_tick, ticks[-1])
    ax.set_yticks(ticks[1:])
    ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, loc: "{:,}".format(int(x))))
    plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%m-%d %H:%M")) 
    plt.xticks(rotation=30)
    ax.set_axisbelow(True)
    ax.grid(True)

    # 出来高
    axv=ax.twinx()
    axv.bar(idx1,df.loc[idx1,"volume"],width=w,color="#33c076",edgecolor="#33b066",linewidth=1)
    axv.bar(idx0,df.loc[idx0,"volume"],width=w,color="#ff6060",edgecolor="#ff5050",linewidth=1)
    # 出来高目盛が価格目盛にかぶらないよう調整
    ymax=df["volume"].max()
    axv.set_ylim(0,ymax*5)
    ytick=ymax//3
    tmp=0
    cnt=0
    while ytick-tmp>0:
        cnt+=1
        ytick-=tmp
        tmp=ytick%(10**cnt)
    axv.set_axisbelow(True)
    axv.set_yticks(np.arange(0,ymax,ytick))
    axv.tick_params(left=True,labelleft=True,right=False,labelright=False)
    axv.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, loc: "{:,}".format(int(x))))

```

# データ準備
実験用にcrypto watchからビットコインのローソク足を取得します。
以下はそのための関数。

```python
import requests
def fetch_ohlcv(symbol="btc",period=60*60):
    """
    指定銘柄,指定時間軸の未確定足含む最新1000件のローソク足のDataFrameを返す.
    ローソク足はcrypto watchのAPIから取得する.
    取引所はliquid.
    :param symobl:取得する銘柄
    :param period:ローソク足の時間軸. 秒数で指定. 60*60は1時間足. 24*60*60で1日足.
    """
    url = f"https://api.cryptowat.ch/markets/liquid/{symbol}jpy/ohlc?period={period}"
    work=requests.get(url).json()
    rows = []
    for t,o,h,l,c,v,_ in work["result"][str(period)]:
        ohlcv=[t,o,h,l,c,v]
        rows.append(ohlcv)
        rows[-1][0] -= period # crypto watchで取得するローソク足はtimestampが足確定の時刻になっている.開始時刻に直す.
    rows.sort(key=lambda x:x[0])
    df=pd.DataFrame(rows)
    df.columns=["timestamp","open","high","low","close","volume"]
    return df
df=fetch_ohlcv()
df.head()
"""
[Out]
	timestamp	open	high	low	close	volume
0	1629414000	5367967	5379197	5332839	5348282	90.498291
1	1629417600	5348276	5372000	5337235	5367460	59.795573
2	1629421200	5370233	5372000	5326985	5342500	70.199786
3	1629424800	5342282	5352499	5330417	5350665	45.362724
4	1629428400	5349575	5356597	5332056	5332901	27.142236
"""
```

# 実際に描画してみる

```python
df = fetch_ohlcv("btc")
fig = plt.figure(figsize=(7,4))
ax = fig.add_subplot(111)
ohlcv_plot(ax,df[-100:]) # 全部描画すると小さくなって見にくいので一部だけ
plt.show()
```
![スクリーンショット 2021-10-05 23.33.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1640073/8c26f2f8-a312-c96f-fbea-9766d5c64522.png)


ちなみにmplfinanceで描画すると以下のようになります。

```python
df=fetch_ohlcv("btc")
df["datetime"]=pd.to_datetime(df["timestamp"], unit='s')
df=df.set_index("datetime")
df=df.tz_localize('utc').tz_convert('Asia/Tokyo')
dfx=df[["timestamp","open","high","low","close","volume"]]
dfx.columns = ['timestamp','Open', 'High', 'Low', 'Close', 'Volume']
mpf.plot(dfx[-100:], type='candle', figratio=(5,4),volume=True, mav=(5, 25), style='yahoo')
mpf.show()
```

![スクリーンショット 2021-10-05 23.33.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1640073/46bf447e-f816-7607-2272-c6d1e4d797b1.png)


同じような感じに作ることができました。


## ohlcv_plotを使って複数の銘柄を並べて描画してみる

```python
fig=plt.figure(figsize=(14,12))

df=fetch_ohlcv("btc")
ax=plt.subplot2grid((2,2),(0,0))
ohlcv_plot(ax,df[-100:])
ax.set_title("btc",size=10)

df=fetch_ohlcv("eth")
ax=plt.subplot2grid((2,2),(1,0))
ohlcv_plot(ax,df[-100:])
ax.set_title("eth",size=10)

df=fetch_ohlcv("xrp")
ax=plt.subplot2grid((2,2),(0,1))
ohlcv_plot(ax,df[-100:])
ax.set_title("xrp",size=10)
plt.show()
```
![スクリーンショット 2021-10-05 23.33.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1640073/fee2a2af-5dba-8d8f-de52-7fad2ea21602.png)


## いい感じのグラフをつくる
ローソク足チャートに、ロング/ショートをした場所をマークしたり、損益グラフをサブプロットしたりして、それっぽい感じグラフを作ります。

```python

df = fetch_ohlcv("btc")[-300:]
df["datetime"]=pd.to_datetime(df["timestamp"], unit='s')
df=df.set_index("datetime")
df=df.tz_localize('utc').tz_convert('Asia/Tokyo')

fig = plt.figure(figsize=(16,10))
ax = plt.subplot2grid((2,1),(0,0))
ohlcv_plot(ax,df)

## 適当にロング/ショートした場所をマーク
import numpy as np
df["long"] = np.where(np.random.random(len(df))<0.9,0,1)
df["short"] = np.where(np.random.random(len(df))<0.9,0,1)
ax.scatter(df[df["long"]==1].index,df.loc[df.index[df["long"]==1],"low"]*(1-0.02),marker="^",color="r",label="long")
ax.scatter(df[df["short"]==1].index,df.loc[df.index[df["short"]==1],"high"]*(1+0.02),marker="v",color="b",label="short")
ax.legend()

ax = plt.subplot2grid((2,1),(1,0))

## 適当に損益が発生したとする
df["pnl"] = np.random.random(len(df))-0.45
df["pnl"] *= 100
ax.plot(df.index,df["pnl"].cumsum(),label="pnl")
ax.fill_between(df.index,0,df["pnl"].cumsum(),alpha=.3)
ax.legend()
plt.show()
```

![スクリーンショット 2021-10-05 23.34.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1640073/f5dcd208-0ed0-6a74-e8e5-4befaa580571.png)


時間をインデックスに指定して、はじめと最後の時刻を揃えれば上下のグラフで時間軸が揃っていい感じ。

# 追記
頑張って作りましたがmpl_financeでできるみたいです。

```.py
from mpl_finance import candlestick_ohlc
candlestick_ohlc(ax, ohlcv, width=(1/24/60)*0.7,colorup='g', colordown='r')
```
以下の記事で見つけました。

https://qiita.com/p_q/items/883fd45c4bd3eb50fcb3