やのような指数について調べるとき、構成銘柄の一覧が要ります。多くの提供元が配信しているのは現在の構成銘柄で、を叩けば今の中身は分かります。
問題が出るのは、この一覧をそのまま使って過去にさかのぼったときです。指数の構成銘柄は、定期的に見直されて入れ替わります。業績が悪化して除外された銘柄、上場廃止になった銘柄、新しく採用された銘柄。現在の一覧には、除外された銘柄が入っていません。
現在の構成銘柄だけで過去10年の平均収益率を計算すると、10年間生き残った銘柄だけの成績になります。途中で脱落したものが除かれているので、実際の指数より良い数字が出ます。これは分析の誤りであって、データの誤りではありません。
時点ごとの構成が要る
この現象には名前が付いていて、生き残ったものだけを見てしまう偏りとして知られています。正しく扱うには、各時点での構成銘柄が必要です。2020年の分析には2020年時点の一覧、2015年には2015年時点の一覧。これを持っている提供元は多くありません。無料で手に入ることはさらに稀です。
手に入らない場合、選択肢は3つです。自分で取り続けて貯める、入替の履歴から復元する、その分析をしないと決める。3つ目は消極的に見えますが、正しくない結果を出すよりはよほど良い判断です。
| 方法 | 得られるもの | 難点 |
|---|---|---|
| 現在の一覧を取る | 今の構成 | 過去の再現ができない |
| 毎月取って貯める | 貯め始めて以降の構成 | 過去にさかのぼれない |
| 入替の履歴から復元 | 理論上は全期間 | 履歴の入手が難しい |
| 有償の提供元 | 時点ごとの構成 | 費用がかかる |
過去はさかのぼれませんが、今日から貯め始めれば、来年には1年分の履歴になります。月に1回、構成銘柄の一覧を保存するだけです。量も小さいので、迷うなら始めておくほうが得です。
貯め方は差分ではなく全件で
構成銘柄の一覧は、毎回全件をそのまま保存してください。差分だけを記録すると、ある時点の構成を知るために全部の差分を順に適用する必要があり、途中で1つ欠けると以降が全部狂います。
銘柄を表す値はであることが多く、文字列として保存するのが安全です。件数は多くても数千件なので、毎月全件保存しても量はたかが知れています。EP.05 の考え方をそのまま当てはめて、取得した日付で分けて置けば十分です。
import jsonfrom datetime import datefrom pathlib import Path
BASE = Path("data/raw/index_members")
def save_members(index_name: str, members: list[str], as_of: date) -> Path: """差分ではなく全件を残す。復元の手間がかからない。""" d = BASE / index_name d.mkdir(parents=True, exist_ok=True) path = d / f"{as_of.isoformat()}.json" path.write_text(json.dumps(sorted(members), ensure_ascii=False), encoding="utf-8") return path
def members_as_of(index_name: str, target: date) -> list[str]: """指定した日付以前で最も新しい一覧を返す。無ければ例外。""" files = sorted((BASE / index_name).glob("*.json")) usable = [p for p in files if date.fromisoformat(p.stem) <= target] if not usable: raise FileNotFoundError(f"{target} 以前の構成銘柄が保存されていない") return json.loads(usable[-1].read_text(encoding="utf-8"))上のコードは、指定した日付以前で最も新しい一覧を返します。その日ちょうどの一覧が無ければ例外にするという作り方もありますが、月1回しか取っていないなら現実的ではありません。どちらにせよ、無いときに黙って現在の一覧を返さないことが重要です。
重みも一緒に残す
構成銘柄の一覧だけでは足りない場合があります。指数は銘柄を等しく扱っているわけではなく、時価総額や株価に応じた重みが付いています。一覧だけを保存して、あとで自分の計算に使おうとすると、重みが無いことに気づきます。
重みが配信されているなら、一覧と一緒に残してください。配信されていない場合、時価総額から自分で計算することになりますが、指数側の算出規則と一致する保証はありません。上限を設けている指数もあり、単純な計算では再現できません。
保存の形としては、銘柄・重み・時点を1行ずつ並べた縦長が扱いやすいです。銘柄数が変わっても形が変わらないので、EP.12 で扱ったとも噛み合います。
import pandas as pd
def normalize_members(raw: list[dict], as_of: str, index_name: str) -> pd.DataFrame: """提供元の形を、銘柄 x 重み x 時点の縦長に揃える。""" rows = [] for item in raw: rows.append( { "index_name": index_name, "as_of": as_of, "code": str(item["code"]), # 数値にしない。先頭ゼロが落ちる "weight": item.get("weight"), # 無ければ None のまま残す } ) df = pd.DataFrame(rows) if df["code"].duplicated().any(): raise ValueError("同じ銘柄が複数行ある。取得の重複を疑う") return df指数そのものの値と、構成銘柄から自分で計算した値は一致しません。指数には算出方法の細かい規則があり、外から完全には再現できないためです。指数の値が欲しいなら指数を取る、というのが結局いちばん確実です。次回からは、公的なデータに話を移します。
構成銘柄は、地味なわりに後から取り返せない種類のデータです。価格は過去にさかのぼって取れますが、ある時点の構成は、その時点で保存しておかないと手に入りません。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。