Python pandas ヒストリカルデータのサマータイムを考慮していませんでした

以前の記事で 1 分足のヒストリカルデータから日足のヒストリカルデータを作りました。 そのときに時間を UTC+3 に一律設定していたのですが、これだと問題がありました。 修正した内容を書いておきます。

環境

  • Ubuntu 18.04 LTS
  • Python 3.6
  • pandas 0.23.0

問題

作った日足のデータを読み込んでおいて土曜日の行を表示してみます。

df[df.index.dayofweek == 5].tail()

結果です。

 open high low close volume
time
2017-12-02 112.107 112.282 112.099 112.176 2201.3
2017-12-09 113.489 113.525 113.454 113.461 3260.2
2017-12-16 112.581 112.636 112.562 112.585 3219.1
2017-12-23 113.268 113.343 113.254 113.260 3561.4
2017-12-30 112.680 112.714 112.647 112.658 5856.3

土曜日は取引がないはずなのに該当してしまいました。 これだと日足が 6 本できてしまいます。

原因

結果を見ていると、土曜日のデータがあるのはサマータイム以外の期間だけでした。

どうやらサマータイムじゃないときにも UTC+3 を設定したことで、月曜日の 1 時に始まり、金曜日の 25 時(土曜日の 1 時)に終わるようになってしまっていたようでした。

対処

サマータイムを考慮して、サマータイムのときは UTC+3 、サマータイムじゃないときは UTC+2 を設定してあげる必要があるようでした。

ここからは Jupyter Notebook のセルに入力しながら進めます。

必要なモジュールを読み込みます。

import calendar
import datetime
import functools
import pandas as pd

1 分足のヒストリカルデータを読み込む関数です。

def read(filepath):
    dtype = { 'time': str, 'open': float, 'high': float, 'low': float, 'close': float, 'volume': float }
    names = ['time', 'open', 'high', 'low', 'close', 'volume']
    df = pd.read_csv(filepath, dtype=dtype, header=0, index_col='time', names=names, parse_dates=['time'])
    return df

UTC+3 と UTC+2 を設定する関数です。

def dstdt(year, month, week, hours):
    day = [i for i in calendar.Calendar().itermonthdays2(year, month) if i[0] != 0 and i[1] == calendar.SUNDAY][week][0]
    return datetime.datetime(year, month, day, 2, 0) + datetime.timedelta(hours=-hours)

def dst(year):
    dic = {
        1918: [(4, 0, -5), (10, -1, -4)],
        2007: [(3, 1, -5), (11, 0, -4)]
    }
    key = functools.reduce(lambda x, y: x if year < y else y, dic.keys(), 0)
    if key == 0:
        return []
    return [dstdt(year, i[0], i[1], i[2]) for i in dic[key]]

def offset(df):
    data = { 'open': df['open'].values, 'high': df['high'].values, 'low': df['low'].values, 'close': df['close'].values, 'volume': df['volume'].values }
    columns = ['open', 'high', 'low', 'close', 'volume']
    l = [dst(i) for i in range(2001, 2018)]
    l2 = [(i[0] <= df.index) & (df.index < i[1]) for i in l]
    is_dst = functools.reduce(lambda x, y: x | y, l2)
    eest = df.index[is_dst] + pd.DateOffset(hours=3) # サマータイムは UTC+3
    eet = df.index[~is_dst] + pd.DateOffset(hours=2) # サマータイム以外は UTC+2
    index = pd.concat([eest.to_series(), eet.to_series()]).sort_values()
    df = pd.DataFrame(data=data, columns=columns, index=index)
    return df

サマータイムの期間を求める関数も実装しました。 最初 pandas.Timestamp の dst 関数を使ったり、 pytz の dst 関数を使ったりしたのですが、遅かったので実装することにしました。

1 分足から日足にする関数です。

def resample(df, rule):
    df2 = df.resample(rule).ohlc()
    df3 = df.resample(rule).sum()
    data = { 'open': df2['open']['open'].values, 'high': df2['high']['high'].values, 'low': df2['low']['low'].values, 'close': df2['close']['close'].values, 'volume': df3['volume'].values }
    columns = ['open', 'high', 'low', 'close', 'volume']
    df4 = pd.DataFrame(data=data, columns=columns, index=df2.index).dropna()
    return df4

rule'D' を指定すると日足、 'H' を指定すると 1 時間足、 '15T' にすると 15 分足、 '5T' にすると 5 分足になります。

上の関数をまとめて、それから csv を作る関数です。

def main(filepath, rule, decimals, to):
    df = read(filepath)
    df2 = offset(df)
    df3 = resample(df2, rule)
    df4 = df3.round({ 'open': decimals[0], 'high': decimals[0], 'low': decimals[0], 'close': decimals[0], 'volume': decimals[1] })
    df4.to_csv(to)
    return to, df4['close'].count()

filepath は入力する 1 分足のファイルのパスです。 rule は resample 関数の rule です。 decimals は小数点以下の桁数です。 価格に対する小数点以下の桁数と、出来高に対する小数点以下の桁数をセットか配列で渡すことを想定しています。 価格について、円クロスの通貨ペアは 3 、それ以外は 5 になることを想定しています。 to は作る csv のパスです。

確認

データは以前使ったのと同じものです。

Time (UTC),Open,High,Low,Close,Volume
2003.05.04 21:00:00,118.94,118.952,118.94,118.952,253
2003.05.04 21:01:00,118.961,118.967,118.958,118.967,154.6
2003.05.04 21:02:00,118.972,118.972,118.955,118.955,219.7
2003.05.04 21:03:00,118.953,118.961,118.949,118.949,309.9
2003.05.04 21:04:00,118.953,118.953,118.946,118.946,229.4
2003.05.04 21:05:00,118.952,118.954,118.944,118.944,112.2
2003.05.04 21:06:00,118.95,118.952,118.945,118.945,170.2
2003.05.04 21:07:00,118.947,118.956,118.947,118.947,124.5
2003.05.04 21:08:00,118.946,118.954,118.934,118.934,355

実行しました。

%time main(f'~/Documents/data/USDJPY_1 Min_Bid_2000.01.01_2018.01.01.csv', 'd', (3, 1), f'~/Documents/data/USDJPY_D.csv')

結果です。

CPU times: user 20.9 s, sys: 5.45 s, total: 26.4 s
Wall time: 29.8 s

[('~/Documents/data/d/USDJPY_D.csv', 3827)]

500 万行程度の 1 分足のデータを読み込んで 3,800 行程度の日足のデータを作りました。 30 秒程度かかりました。

日足のデータです。

time,open,high,low,close,volume
2003-05-05,118.94,119.046,118.461,118.603,592866.9
2003-05-06,118.591,118.751,117.29,117.5,581707.0
2003-05-07,117.456,117.83,116.052,116.303,584496.2
2003-05-08,116.311,116.969,115.94,116.823,588236.7
2003-05-09,116.835,117.612,116.794,117.151,583132.9
2003-05-12,117.286,117.286,116.304,117.025,586660.3
2003-05-13,117.053,117.528,116.338,116.561,584984.4
2003-05-14,116.554,116.837,115.557,116.16,586464.9
2003-05-15,116.173,116.525,115.282,116.515,585585.1

土曜日のデータがあるか確認しました。

df = read(f'~/Documents/data/d/USDJPY_D.csv')
df[df.index.dayofweek == 5]

結果です。

 open high low close volume
time

ヘッダーだけで行が出力されなかったので大丈夫そうでした。

Gist

Jupyter Notebook のファイルを Gist にアップしておきました。

終わり

まだ取引の環境のことをちゃんと理解できていないようでした…。

変なデータで検証を進めるところでした。 気をつけます。