Python×株式投資：都度DLはやめた─yfinanceで爆速テクニカル分析を回したい

# yfinaceデータを用いた軽量データセット（個人用）の構築
## はじめに
これまでテクニカルスクリーニングのバックテストを行う中で、  
処理速度の遅さに課題を感じていました。

特に、複数銘柄に対してループ処理を行う際、  
毎回 `yfinance` を通じて株価データを取得していたため、  
同じ銘柄でも都度ダウンロードが発生し、非効率でした。

→今のスクリーニング系の問題について書いたのがこちらの記事です。よければ参考に。

https://qiita.com/rS_alonewolf/items/4e02442524a666703660

例えば以下のようなコードが問題でした。

```python
# -----------------------------
# スクリーニング条件別ループ
# -----------------------------
    summary = []
    
    for cond in screening_conditions:
        result = []
        chart_dir = お好きなディレクトリ
        os.makedirs(chart_dir, exist_ok=True)
    
        for _, row in df_list.iterrows():
            ticker = row["Ticker"]
            df = yf.download(ticker, start="2020-12-01", end="2025-04-06")
            time.sleep(0.7)
            print(ticker)
    
            if isinstance(df.columns, pd.MultiIndex):
                df.columns = df.columns.get_level_values(0)
            if df.empty or len(df) < 75:
                continue
    
            df["MA_5"] = df["Close"].rolling(5).mean()
            df["MA_25"] = df["Close"].rolling(25).mean()
            df["MA_75"] = df["Close"].rolling(75).mean()
    
            delta = df["Close"].diff().values.flatten()
            gain = np.where(delta > 0, delta, 0)
            loss = np.where(delta < 0, -delta, 0)
            avg_gain = pd.Series(gain, index=df.index).rolling(14).mean()
            avg_loss = pd.Series(loss, index=df.index).rolling(14).mean()
            rs = avg_gain / (avg_loss + 1e-10)
            df["RSI"] = 100 - (100 / (1 + rs))
            df["MA_5_slope"] = df["MA_5"].diff(cond["slope_period"]) / cond["slope_period"]
 ##--続く--##
```

問題点は次の通りです。
| カテゴリ | 本質的な問題 |
|----------|----------------|
| データ取得 | データを何度もダウンロードしている |
| 時間待ち処理 | アクセス制限回避を `sleep` で行っている |
| 冗長ループ | 取得・計算・スクリーニングをすべて1つのループで済ませている |
| 再計算の無駄 | 指標計算を毎ループで毎銘柄に再実行している|

## 解決の方針

この課題に対応するために、  
必要最低限の株価情報のみをまとめた軽量なデータセットを構築しました。

- 対象：東証プライム全銘柄（約1600銘柄）  
- 期間：2010年～2025年  
- 項目：OHLCV（Open, High, Low, Close, Volume）  
- 形式：Parquet形式で銘柄ごとに保存

このようなデータセットを構築することで、次のようなメリットがあります。
##  ローカルに株価データを保存するメリット

| 観点 | メリット |
|------|----------|
| 速度 | 毎回ダウンロードせず、瞬時に読み込める |
| 安定性 | `yfinance` の障害や制限に左右されない |
| 拡張性 | 後から特徴量や外部データを追加できる |

また、今後導入する予定の機械学習では、学習や検証を何度も繰り返すため、株価データを毎回オンラインで取得するのは非効率かつ不安定です。再現性や実行速度を確保するためにも、ローカルに保存したデータを使うのが良いと思います。

## なぜOHLCVなのか
証券アプリなどで使用されている多くのテクニカル指標は、  
実は「OHLCV（始値・高値・安値・終値・出来高）」の5項目だけで再現可能です。

このように、分析に必要なデータを**最低限に絞って保存**しておくことで、

- 保存容量の削減  
- 読み込み速度の向上  
- 特徴量生成の柔軟性維持  

といったメリットがあると考えられます。

なお、後から必要に応じて以下のような情報も**簡単に拡張**できると思われます：

- 日付に基づく曜日・月・週番号などのカレンダー特徴量  
- 指数データ（TOPIX、日経平均、VIXなど）とのマージ  
- 財務・業種・需給・マクロデータの追加


# 実装：ローカル用の株価データセットを構築する方法

東証プライム全銘柄の日足データ（2010年〜2025年）を `.parquet` 形式で保存し、  
**最終的に1つの統合ファイルとして使えるようにする**ことを目的とします。

この統合データは、後のスクリーニングや機械学習、PCAなどの分析に有用です。

---
## 全体の処理フロー

本稿のコードは下記の項目で構成されています：

1. **データ取得**：銘柄ごとに15年分の日足データを `.parquet` で保存  
2. **バッチ処理**：保存された銘柄ファイルを 200 件ずつまとめて分割保存  
3. **統合処理**：バッチファイルを結合して、1つの巨大な `.parquet` を作成
4. **中身の確認**：データの件数、カラム数、データ型、欠損の数を確認
5. **有用性の確認**：統合データから個別銘柄を抽出・可視化する


### ① yfinanceで全銘柄の株価を `.parquet` に保存
```python
# Driveマウントとライブラリ
from google.colab import drive
drive.mount('/content/drive')

!pip install curl_cffi --quiet

import yfinance as yf
import pandas as pd
import os
from tqdm import tqdm
from curl_cffi import requests
session = requests.Session(impersonate="safari15_5")

# 保存先（必要に応じて変更）
output_dir = お好きなディレクトリ
os.makedirs(output_dir, exist_ok=True)

# 銘柄リスト読み込み(前回記事参照)
excel_path = "/content/drive/MyDrive/stock_prediction/data_j.xlsx"
df_all = pd.read_excel(excel_path)
df_all = df_all[df_all["市場・商品区分"] == "プライム（内国株式）"]
df_all["Ticker"] = df_all["コード"].astype(str).str.zfill(4) + ".T"
df_list = df_all[["Ticker", "33業種コード"]].copy()

# 各銘柄のデータを取得して parquet 形式で保存
for _, row in tqdm(df_list.iterrows(), total=len(df_list)):
    ticker = row["Ticker"]
    industry_code = row["33業種コード"]

    try:
        df = yf.download(ticker, start="2010-01-01", end="2025-05-25", auto_adjust=False, session=session)
        if df.empty or len(df) < 100:
            continue

        df.reset_index(inplace=True)
        df = df.astype({
            "Open": "float64", "High": "float64", "Low": "float64",
            "Close": "float64", "Adj Close": "float64", "Volume": "int64"
        })
        df["Date"] = pd.to_datetime(df["Date"])
        df["Ticker"] = ticker
        df["IndustryCode"] = int(industry_code)

        df.columns = [col[0] if isinstance(col, tuple) else col for col in df.columns]
        df.to_parquet(os.path.join(output_dir, f"{ticker}_OHLCV.parquet"), index=False)
        print(f"{ticker} ✅")

    except Exception as e:
        print(f"{ticker} ❌ failed: {e}")
```

 銘柄リストのエクセルファイルに関しては、2回目の財務スクリーニングの記事に紹介済。
 気になる方は、ぜひ参考に。

 https://qiita.com/rS_alonewolf/items/ba9dbbf7d6163f4435a5
 
 今回はtimesleepなしで進んだが、
 時と場合により、**時よりかなり頻繁に**
 アクセスエラーが吐き出されるので、頃合いを見て追加した方が良い。

 2025年6月8日
 `numpy`使ってないのに、ライブラリインポートしてたので削除修正
 
 
---
### ② バッチ処理

株価データは銘柄数が多く、期間も長いため、**すべてを一気に読み込むとメモリを圧迫しやすくなります**。1600銘柄を一度に統合させても、進む場合もありますが、何回かクラッシュしたことがあります。
そこで、ファイルをあらかじめ200件ずつにまとめておく（バッチ化）ことで、

- メモリ負荷を軽減  
- 並列処理や検証を柔軟に行える  
- 後の統合処理が安定する  

ことが期待できます。このコードの流れは下記の通りです。

1. 保存ディレクトリ内の `.parquet` を一覧取得  
2. 空データやエラーを除外しながら読み込み  
3. 200件ごとに `batch_0.parquet`, `batch_1.parquet`, ... として保存



```python
# 入力・出力ディレクトリ（必要に応じて変更）
input_dir = お好きなディレクトリ(↑のファイルが格納されてるところ)
batch_dir = os.path.join(input_dir, "batch_size")
os.makedirs(batch_dir, exist_ok=True)

# 既存のバッチファイルを削除
for f in os.listdir(batch_dir):
    if f.startswith("batch_") and f.endswith(".parquet"):
        os.remove(os.path.join(batch_dir, f))
print(" 古いバッチファイルを削除しました")

# .parquetファイル一覧（batch_除外）
parquet_files = sorted([
    f for f in os.listdir(input_dir)
    if f.endswith(".parquet") and not f.startswith("batch_")
])

# バッチ処理（200件ごとに保存）
batch_size = 200
for i in tqdm(range(0, len(parquet_files), batch_size), desc=" バッチ保存中"):
    batch_files = parquet_files[i:i + batch_size]
    dfs = []
    for file in batch_files:
        try:
            df = pd.read_parquet(os.path.join(input_dir, file))
            if not df.empty:
                dfs.append(df)
            else:
                print(f"⚠️ 空データスキップ: {file}")
        except Exception as e:
            print(f"❌ 読み込み失敗: {file}, 理由: {e}")

    if dfs:
        df_batch = pd.concat(dfs, ignore_index=True)
        batch_path = os.path.join(batch_dir, f"batch_{i // batch_size}.parquet")
        df_batch.to_parquet(batch_path, index=False)
        print(f"✅ batch_{i // batch_size} バッチリsaved")
    else:
        print(f"🚫 batch_{i // batch_size} はスキップ（全データ空）")
```




### ③：バッチファイルを1つの統合 `.parquet` にまとめる

前のステップで保存した `batch_0.parquet`, `batch_1.parquet`, ... といったバッチファイルを  
すべて読み込み、**1つの巨大な `.parquet` ファイル**に統合します。

これが最終的な「全銘柄・全期間データ」の完成版になります。

---

###  処理の流れ

1. `batch_size/` フォルダ内のファイルをすべて読み込む  
2. 中身を1つの DataFrame にまとめる  
3. `ticker_combined_OHLCV.parquet` として保存

---

```python
# ディレクトリ設定（前のステップと同じ場所）
batch_dir = "お好きなディレクトリ/batch_size"
output_parquet = "お好きなディレクトリ/ticker_combined_OHLCV.parquet"

# バッチファイル一覧を取得
batch_files = sorted([f for f in os.listdir(batch_dir) if f.endswith(".parquet")])

# バッチを順に読み込み＆統合
df_list = []
for file in tqdm(batch_files, desc=" 統合中"):
    path = os.path.join(batch_dir, file)
    df = pd.read_parquet(path)
    df_list.append(df)

# 結合して1つのファイルに保存
df_combined = pd.concat(df_list, ignore_index=True)
df_combined.to_parquet(output_parquet, index=False)

print(f"\n 統合完了: {output_parquet}")
```

## ④：統合済みデータの読み込みと確認

前ステップで作成した `ticker_combined_OHLCV.parquet` を読み込み、  
中身を確認して問題がないかチェックします。

- 統合後のデータが正しく読み込めるか確認  
- カラム構成、欠損、データ型、件数などを検証  


---

```python
# 日付・ディレクトリ設定（適宜変更してください）

input_dir = バッチファイルがあるところ
file_path = os.path.join(input_dir, f"ticker_combined_OHLCV.parquet")

# データ読み込み
df = pd.read_parquet(file_path, engine="pyarrow")

# 中身の確認
print("ファイル:", file_path)
print("shape:", df.shape)
print("columns:", df.columns.tolist())
print("dtypes:\n", df.dtypes)
print("null counts:\n", df.isnull().sum())
print("head:\n", df.head())

```

コードが正常に動作すれば、次のような出力結果が表示されます。
```plaintext
ファイル: .../ticker_combined_OHLCV.parquet
shape: (5683368, 9)
columns: ['Date', 'Adj Close', 'Close', 'High', 'Low', 'Open', 'Volume', 'Ticker', 'IndustryCode']
dtypes:
 Date            datetime64[ns]
 Adj Close              float64
 Close                  float64
 High                   float64
 Low                    float64
 Open                   float64
 Volume                   int64
 Ticker                  object
 IndustryCode             int64

null counts:
 全カラムで欠損値は0件（完全なデータ）

head:
        Date    Adj Close   Close   High    Low     Open    Volume  Ticker   IndustryCode
0  2010-01-04   80.097107  120.00  121.33  116.00  120.00   39000   9743.T       9050
1  2010-01-05   80.987068  121.33  121.33  121.33  121.33    1500   9743.T       9050
2  2010-01-06   78.317177  117.33  118.67  116.67  118.00   16500   9743.T       9050

```

- 件数：568万行超 → おおよそ2000銘柄×約2800営業日（想定どおり）
- カラム数：9列 → OHLCV＋識別情報（ティッカー、業種コード）
- データ型：すべて適切に指定されており、後の処理もスムーズ
- 欠損：なし（理想の状態）

### ⑤：統合データから個別銘柄を抽出・可視化する

統合済みファイルから、特定の銘柄（例：7203.T）のデータを抽出し、株価の推移を可視化ができるか確認します。

### 処理の流れ

1. 統合ファイルを読み込む  
2. `"Date"` を時系列インデックスに変換（yfinance風）  
3. 任意のティッカー（ここでは `"7203.T"`）でフィルタ  
4. 株価の推移を可視化＋リターンを計算して列追加

---

```python
import pandas as pd
import os
import matplotlib.pyplot as plt

# 設定
input_dir = お好きなディレクトリ
file_path = os.path.join(input_dir, f"ticker_combined_OHLCV.parquet")

# 統合ファイルの読み込み
df = pd.read_parquet(file_path, engine="pyarrow")
df["Date"] = pd.to_datetime(df["Date"])  # 念のため型変換
df.set_index("Date", inplace=True)       # yfinance形式っぽくする

# 特定銘柄（例：7203.T）だけを抽出して昇順に並べる
df_7203 = df[df["Ticker"] == "7203.T"].sort_index()
print(df_7203.head())

# 1日リターンを追加（Closeベース）
df_7203["Return_1d"] = df_7203["Close"].pct_change()

# 株価推移を可視化
df_7203["Close"].plot(title="7203.T - Close Price", figsize=(10, 4))
plt.grid(True)
plt.show()

```
出力例
```plaintext
             Adj Close  Close   High    Low   Open    Volume  Ticker  IndustryCode
Date                                                                              
2010-01-04  512.543579  778.0  783.0  778.0  780.0  30732500  7203.T          3700
2010-01-05  501.343903  761.0  784.0  761.0  784.0  52827500  7203.T          3700
2010-01-06  513.202271  779.0  779.0  779.0  779.0  41519000  7203.T          3700
2010-01-07  507.273041  770.0  782.0  766.0  781.0  31003500  7203.T          3700
2010-01-08  521.766663  792.0  796.0  780.0  781.0  77448500  7203.T          3700
```
出力されるグラフ
![Screenshot 2025-06-04 at 21.23.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4066570/5fce42e8-93d5-4e28-9296-eff5680921da.png)


## まとめ

本記事では、東証プライム全銘柄の株価データを `yfinance` から取得し、  
`.parquet` 形式で保存・整理する一連の処理を構築しました。

- 銘柄ごとのデータを `.parquet` で個別保存  
- メモリ効率を考慮して200件ずつバッチに分割  
- 全バッチを統合して1つのマスターファイルを作成  
- 特定銘柄の抽出・時系列処理・可視化まで確認

これにより、以後は Web アクセスなしで高速かつ安定的に株価データを活用できると考えられます。  
また、テクニカル指標の追加、スクリーニング、機械学習への応用といった後続の分析において、  
**再現性と柔軟性を両立したデータ基盤**として活用できるはずです。

 
 


 
……しかし、本当にこの統合データセットを使えば、`yfinance` を都度呼び出すよりも  
**高速に、ストレスなく、バックテストができるようになる**のでしょうか？
その検証を行わなければ、今回の工程の有用性を証明したことにはなりません。
そこで次回は、  

**「yfinance vs 自前データセット」徹底・速度対決**を行います。
果たしてこのローカルデータセットは、 

あの幻の記事『Python×株式投資：月利3〜5%を狙う自動スクリーニング戦略』で行った、  

地獄のバックテスト工程─**丸3日かかった実験**─を短縮できるのでしょうか？
結果が気になる方は、ぜひ次回もご一読ください。

---

過去記事もぜひ参考に。
地獄のバックテストの結果についての記事と、
地獄のバックテストをした後に、ネタが思いつかなくて書いた記事です。
いつも読んでいただいて、ありがとうございます。
お陰様で...
あぁ言いたいこと忘れてしまった。

https://qiita.com/rS_alonewolf/items/a8856f0cb28ae6a7da57


https://qiita.com/rS_alonewolf/items/6dbb932d5deef9429a4d