日足のデータは扱いやすいです。1銘柄1年で250行程度、1000銘柄でも25万行なので、手元で読み込んで処理できます。ところが粒度を細かくすると、この前提が一気に崩れます。
を1分ごとに取ると、1日あたり300行前後、年間で約9万行になります。1000銘柄なら9000万行です。さらに細かいになると、約定1件ごとの記録なので、活発な銘柄では1日で数十万件に達します。
この回では、量が増えたときに何が変わるかという話と、細かい粒度になって初めて表に出てくる時刻の問題の、両方を扱います。
量が変わると、扱い方が変わる
まず取得できる期間が変わります。分足を無料で長期間配信している提供元はほとんどありません。過去1か月ぶんだけ、といった制限が普通です。長期の分足が欲しければ、取り続けて自分で貯めるか、有償の提供元を使うことになります。
「あとで必要になったら取ればいい」が通用しないという意味で、これは判断を前倒しさせます。将来使うかもしれないという理由で貯め始めるかどうかを、今決めることになる。貯めなければ二度と手に入らず、貯めれば保管の手間がかかり続けます。
保存形式も変わります。この規模で を使うと、読み込みだけで数分かかるようになります。にして日付や銘柄でしておくのが前提になります。必要な範囲だけを読めば、全体が大きくても実用に耐えます。
from pathlib import Path
import pandas as pd
def save_minutes(df: pd.DataFrame, base: Path) -> None: """date と symbol の 2 段で分けて置く。 「この銘柄のこの日」だけを読めるようにするのが目的。""" for (day, symbol), part in df.groupby(["date", "symbol"]): d = base / f"dt={day}" / f"symbol={symbol}" d.mkdir(parents=True, exist_ok=True) part.drop(columns=["date", "symbol"]).to_parquet(d / "part.parquet", index=False)
def load_minutes(base: Path, symbol: str, start: str, end: str) -> pd.DataFrame: paths = [ p for p in sorted(base.glob(f"dt=*/symbol={symbol}/part.parquet")) if start <= p.parent.parent.name[3:] <= end ] if not paths: return pd.DataFrame() return pd.concat([pd.read_parquet(p) for p in paths], ignore_index=True)処理の書き方も変わります。日足なら全部を読み込んで一度に計算できましたが、この規模だと銘柄ごと・日ごとに区切って処理する形になります。集計の途中結果を持ち回る必要が出てくるので、コードの構造自体が変わります。
その時刻は「いつ」を指すのか
分足のデータには時刻が付いていますが、その時刻が区間の始まりなのか終わりなのかは提供元によって違います。09:00 の行が「09:00 から 09:01 まで」を表すのか、「08:59 から 09:00 まで」を表すのか。
日足では気にならなかったこの違いが、分足では1本ぶんのずれとして表に出ます。異なる提供元のデータを突き合わせると、全体が1分ずれているのに、パッと見では気づけません。
取引開始時刻の扱いを見ると分かります。日本市場の場合、区間の始まりを時刻にしているなら 9:00 の行が最初になり、終わりを時刻にしているなら 9:01 が最初になります。1日ぶん取って先頭を見れば判別できます。
そして時刻にはが付きます。海外の提供元から日本株を取ると、時刻が UTC で返ることがあります。UTC のまま日付で区切ると、日本時間の午前と午後が別の日に割れます。この問題は次回に詳しく扱います。
時刻の意味は、提供元の説明に明記されていないことも多いです。書いていない場合は、実際に1日ぶん取って中身を見るのが確実です。推測で合わせると、ずれたまま気づかずに使い続けることになります。
ティックはさらに別の扱いが要る
ティックデータは約定1件ごとの記録なので、同じ時刻に複数の行が存在します。時刻を索引にして扱う前提のコードは、そのままでは動きません。時刻の重複を許す形で持つ必要があります。
また、ティックから分足を作る場合、どの範囲をどの分に入れるかを自分で決めることになります。境界の扱いを1件間違えると、始値や終値が隣の分にずれます。提供元が作った分足と自分で作った分足が合わない、という食い違いはここから生まれます。
import pandas as pd
def ticks_to_minutes(ticks: pd.DataFrame) -> pd.DataFrame: """ticks は timestamp / price / size を持つ想定。
label="left" : 09:00 の行は 09:00:00 〜 09:00:59 を表す closed="left" : 09:01:00 の約定は次の行に入れる ここを明示しないと、提供元の分足と合わない原因になる。 """ ticks = ticks.set_index("timestamp").sort_index() agg = ticks.resample("1min", label="left", closed="left").agg( open=("price", "first"), high=("price", "max"), low=("price", "min"), close=("price", "last"), volume=("size", "sum"), ) # 約定が 1 件も無い分は行ごと落とす(0 で埋めない) return agg.dropna(subset=["open"])上のコードで、約定が無い分を落としているのは意図的です。0 で埋めると「価格 0 で取引された」ことになります。出来高は 0 でよくても、価格は「無い」が正しい。埋めるかどうかは、後で何を計算するかで決めてください。
分足やティックは、扱えるようになると分かることが確かに増えます。ただし、量と定義の両方で手間が跳ね上がります。日足で足りる分析なら、無理に細かくする必要はありません。次回は、株式以外の商品を扱います。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。