ローソク足OHLCの時間足を変える

# はじめに

ローソク足などで使われる[OHLC](https://en.wikipedia.org/wiki/Open-high-low-close_chart)形式(Open High Low Closeの頭文字)で集計されたデータフレームの時間足を変えて(=ダウンサンプリング)、再度OHLC形式のデータフレーム(以下、ohlcデータ)を出力します。
以下の例では時間足を日足に変換しています。

```python
df

                     open  high  low  close
2017-11-25 00:00:00     1     2    1      2
2017-11-25 01:00:00     1     3    1      3
2017-11-25 02:00:00     4     5    4      5
2017-11-25 03:00:00     6     8    6      7
2017-11-25 04:00:00     7     8    7      7
                 ...
# ↑これが


df.resample('D').ohlc2()

# ↓こう
            open  high  low  close
2017-11-25     1    13    1     12
2017-11-26    12    16    9     13
2017-11-27    12    16    7     14
2017-11-28    13    14    3     10
2017-11-29    11    21   11     12
                 ...
```

`ohlc2()`メソッドを作成し、入力をシンプルにしたいと思います。




# 準備

## ランダムウォークデータを用意
ランダムに数値が上下するデータ(ランダムウォーク)を作ります。

```python
import numpy as np
import pandas as pd


def randomwalk(periods, start=pd.datetime.today().date(), index=None, name=None, tick=1, freq='B'):
    """periods日分だけランダムウォークを返す"""
    if not index:
        index = pd.date_range(start=start, periods=periods, freq=freq)  # 今日の日付からperiod日分の平日
    bullbear = pd.Series(tick * np.random.randint(-1, 2, periods),
                         index=index, name=name)  # tick * (-1,0,1のどれか)を吐き出すSeries
    price = bullbear.cumsum()  # 累積和
    return price


df = randomwalk(periods=10000, freq='15T')  # 15分刻みのランダムウォーク

2017-11-25 00:00:00    1
2017-11-25 00:15:00    0
2017-11-25 00:30:00    1
2017-11-25 00:45:00    2
2017-11-25 01:00:00    1
          ...
```


グラフ化するとこんな感じです。

```pyhton
df.plot()
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/113494/89157627-db68-ce73-d6b2-f0b97e2fb933.png)








## 時間足に変換
pandas.DataFrameでohlcデータを用意します。

```python
df = df.resample('H').ohlc()  # 1時間足としてダウンサンプリング

                     open  high  low  close
2017-11-25 00:00:00     1     2    1      2
2017-11-25 01:00:00     1     3    1      3
2017-11-25 02:00:00     4     5    4      5
2017-11-25 03:00:00     6     8    6      7
2017-11-25 04:00:00     7     8    7      7
                    ...
```













# ohlcデータの時間足変換
## 日足に変換(間違った方法)

このohlcデータ`df`は時間足です。これを**日足に変換したいとき、`df.resample('D').ohlc()`とやりがち**ですが、`open high low close`それぞれに対して`open high low close`を分けようとするため思ったように変換してくれません。

```python
df.resample('D').ohlc()  # やりがちなohlcデータを再度resampleしてohlcで集計

           open                high                 low                close  \
           open high low close open high low close open high low close  open   
2017-11-25    1   13   1    13    2   13   2    13    1   12   1    12     2   
2017-11-26   12   15  10    13   14   16  10    13   12   14   9    13    14   
2017-11-27   12   15   7    15   12   16   9    15   10   15   7    14    10   

           high low close  
2017-11-25   10   0     8  
2017-11-26   15   6     9  
2017-11-27   14   6     8  
2017-11-28    6 -10    -9  
2017-11-29   -6 -17    -9  

                                   ...
```










## 日足に変換(正しい方法)

これを正しく日足に変換するにはresampleメソッドで日足にしたあとに、**agg(=aggregate, 集計)メソッド**を使わなければなりません。

```python
df.resample('D').agg({'open': 'first',
                     'high': 'max',
                     'low': 'min',
                     'close': 'last'})  # ohlcを再度ohlcに集計するにはaggメソッド

            open  high  low  close
2017-11-25     1    11   -1      8
2017-11-26     8    15    5      9
2017-11-27    10    15    5      8
2017-11-28     8     8  -10     -9
2017-11-29   -10    -5  -17     -9
                 ...
```








## 日足に変換(メソッドに追加)
毎回`agg({'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last'})`を打つのも大変なので、簡単に打てるようにメソッドとして使えるようにしたいと思います。

```python
from pandas.core import resample


def ohlc2(self):  # 関数でaggを定義
    return self.agg({'open': 'first',
                     'high': 'max',
                     'low': 'min',
                     'close': 'last'})


resample.DatetimeIndexResampler.ohlc2 = ohlc2  # resampleすると生成されるクラスにohlc2メソッドを追加
```

このようにすれば`df.resample('D')`のクラス`pd.core.resample.DatetimeIndexResampler`にohlc2メソッドが追加されます。


```python
df.resample('4H').ohlc2()  # 先ほどのdfをやっぱり4時間足に変えたいとき

                     open  high  low  close
2017-11-25 00:00:00    -1     1   -4      1
2017-11-25 04:00:00     0     0   -6     -5
2017-11-25 08:00:00    -4    -3   -8     -5
2017-11-25 12:00:00    -6    -1   -6     -2
2017-11-25 16:00:00    -1    -1   -7     -7
                 ...
```







## volume(出来高)列の扱い

列目にvolume(出来高)が入っている場合、上記の関数では自動的にvolumeが省かれておりましたので、volumeを含む場合、含まない場合に場合分けして動作するように改造しました。
all関数に内包表記を組み込んで、列名に`open high low close volume`のすべてを含んでいない場合はエラーを送出するようにしました。
volumeだけ入っていない場合は改造前と同様に4本値で計算します。

```python
def ohlc2(self):
    if all(i in ['open', 'high', 'low', 'close'] for i in self.asfreq().columns):  # データに出来高が含まれないとき
        return self.agg({'open': 'first',
                         'high': 'max',
                         'low': 'min',
                         'close': 'last'})
    elif all(i in ['open', 'high', 'low', 'close', 'volume'] for i in self.asfreq().columns):  # データに出来高が含まれるとき
        return self.agg({'open': 'first',
                         'high': 'max',
                         'low': 'min',
                         'close': 'last',
                         'volume': 'sum'})
    else:  # `open high low close (volume)`の列名で構成されていない場合はエラー
        raise KeyError("columns must have ['open', 'high', 'low', 'close'(, 'volume')]")
```

より短く書くなら、columnsをまとめて、`agg`に渡す辞書に`volume`を追加する形にして以下のように書きます。

```python
# ↑と同様
def ohlc2(self):
    agdict = {'open': 'first',
              'high': 'max',
              'low': 'min',
              'close': 'last'}
    columns = list(agdict.keys())  # agdictのキー['open', 'high', 'low', 'close']
    if all(i in columns for i in self.asfreq().columns):
        pass  # ['open', 'high', 'low', 'close']のときは何もせずreturnへ
    elif all(i in columns + ['volume'] for i in self.asfreq().columns):
        agdict['volume'] = 'sum'  # columnsに加えてvolumeが入る場合はagdict追加
    else:
        raise KeyError("columns must have ['open', 'high', 'low', 'close'(, 'volume')]")
    return self.agg(agdict)
```


## columns名が大文字である場合に対応する
`pandas_reader`などで取得したデータのカラム名は頭文字が大文字('Open', 'High', 'Low', 'Close', 'Volume')なので、前のバージョンではエラーを吐いていました。

```python
KeyError("columns must have ['open', 'high', 'low', 'close'(, 'volume')]")
```

openとOpenを区別してしますためですね。

そこで、次のようにして引数となるカラム名を使って`agg`に渡す辞書を作ります。


```python
def ohlc2(self, *args, **kwargs):
    """`pd.DataFrame.resample(<TimeFrame>).ohlc2()`
    Resample method converting OHLC to OHLC
    """
    cdict = dict([(v.lower(), v) for v in self.asfreq().columns])  # (1),(2)
    try:
        agdict = {cdict['open']: 'first',
                  cdict['high']: 'max',
                  cdict['low']: 'min',
                  cdict['close']: 'last'}  # (3)
    except KeyError as e:
        raise KeyError('Columns not enough {}'.format(*e.args))  # (4)
    if 'volume' in map(lambda x: x.lower(), self.asfreq().columns):
        agdict[cdict['volume']] = 'sum'  # (5)
    return self.agg(agdict)  # (6)
```


1. `pd.DataFrame.resample.asfreq().columns`(関数内では`self.asfreq().columns`)でカラム名を取得します。
2. dict関数と内包表記でcdictに格納します。
3. agdictのキーは引数のカラム名となります
4. open, high, low, columnsの
    + 全小文字(openなど)
    + 全大文字(HIGHなど)
    + 一部大文字(Low, cLoSeなど)
    が4列ともなければエラーを吐きます。
5. volume(大文字小文字関係なし)列があれば、agdictに追加します。
6. pd.DataFrame.resampleをagdictに入れたディクショナリでまとめます。


## ipython上でいつでも使えるようにする
ipythonのstartupに登録します。

```shell-session
$ ipython profile create
```

で作成されたディレクトリに以下のファイルを保存します。

```python:~/.ipython/profile_default/startup/ohlc2.ipy
from pandas.core import resample


def ohlc2(self, *args, **kwargs):
    """`pd.DataFrame.resample(<TimeFrame>).ohlc2()`
    Resample method converting OHLC to OHLC
    """
    cdict = dict([(v.lower(), v) for v in self.asfreq().columns])
    try:
        agdict = {cdict['open']: 'first',
                  cdict['high']: 'max',
                  cdict['low']: 'min',
                  cdict['close']: 'last'}
    except KeyError as e:
        raise KeyError('Columns not enough {}'.format(*e.args))
    if 'volume' in map(lambda x: x.lower(), self.asfreq().columns):
        agdict[cdict['volume']] = 'sum'
    return self.agg(agdict)


# Add instance as `pd.DataFrame.resample('<TimeFrame>').ohlc2()`
resample.DatetimeIndexResampler.ohlc2 = ohlc2
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/113494/2997d851-3282-4fe4-bc3b-75359ff0e83f.png)


ipython上で<kbd>tab</kbd>を押せば候補として`ohlc2()`が出てくるようになります。

# 参考
* OHLCをOHLCに変換

> [stackoverflow - Converting OHLC stock data into a different timeframe with python and pandas](https://stackoverflow.com/questions/9944436/converting-ohlc-stock-data-into-a-different-timeframe-with-python-and-pandas?rq=1)

* 既存クラスに自作メソッドを追加

> [ Python Tips：既存のクラスにインスタンスメソッドを追加したい ](http://www.lifewithpython.com/2014/08/python-add-instance-methods-to-existing-class.html)