【機械学習で株予測・作業編①】yfinanceで株価データ取得

株予測

こちらの動画の書き起こし記事になります。

どうもこんにちは、AIジョーです!

前回は、「株予測・理論編」と題して、
日経400銘柄を対象にした機械学習によるロング・ショート戦略についてご紹介しました。
(……と言いつつ、実はまだ理論編の台本を書き上げていません。すみません!)

さて、今回はその続きとして、
yfinance を使った株価データの取得方法に取り組んでいきます。

実際に手を動かしながら、データを集めるところからスタートしていきましょう!

始める前に・・・

マイドライブ
└── ai-joe
  └── monex400
    ├── data
    │  └── 400銘柄.csv
    └── src
       └── 000_monex400_kline.ipynb

Googleドライブ上にプロジェクト用のフォルダ構成を作っていきます。

まず、マイドライブ直下に、新しいフォルダ ai-joe を作成します。

次に、ai-joe フォルダの中に、monex400 という名前のフォルダを作ります。

続いて、monex400 の中に、datasrc という二つのフォルダを作成します。

data フォルダの中には、400銘柄.csv を配置します。
これは、日経400に採用されている銘柄について、
インターネットで調べた証券コードをまとめたCSVファイルです。

src フォルダの中には、000_monex400_kline.ipynb を配置します。ここに今回のソースコードを記述します。

これで、フォルダとファイルの構成は完成です。

データを扱う準備

import pandas as pd
from datetime import datetime
import pytz

japan = pytz.timezone("Asia/Tokyo")
print(f"開始時刻: {datetime.now(japan)}")

D = f"/content/drive/MyDrive/ai-joe/monex400/data"

まず最初のブロックでは、
データを扱う準備と、スクリプトの開始時刻の表示を行います。

import pandas as pd
from datetime import datetime
import pytz

はじめに、必要なライブラリをインポートします。
ここでは、データを扱うために pandas、日時の操作に使う datetime、タイムゾーンを扱うための pytz を読み込んでいます。

japan = pytz.timezone("Asia/Tokyo")
print(f"開始時刻: {datetime.now(japan)}")

次に、日本のタイムゾーン(Asia/Tokyo)を設定し、スクリプトの開始時刻をコンソールに表示します。
これは、プログラムの実行時間を記録するのに便利な工夫です。

D = f"/content/drive/MyDrive/ai-joe/monex400/data"

最後に、D という変数にデータの保存先パスを設定しています。
ここでは、Google Drive上の ai-joe/monex400/data フォルダを指定しており、
後続のデータ読み込みや保存処理でこのパスを使う予定です。

このブロックで、データ分析を始めるための環境が整いました。

日経400銘柄一覧データを読み込む

monex = pd.read_csv(f"{D}/400銘柄.csv")
monex.rename(columns={"証券コード": "symbol"}, inplace=True)
monex["symbol"] = monex["symbol"].astype(str)
monex

続いて、扱う銘柄リストを読み込みます。

monex = pd.read_csv(f"{D}/400銘柄.csv")

まず、pandasread_csv関数を使って、
先ほど設定したパス D 以下にある「400銘柄.csv」というファイルを読み込みます。

ちなみに、これはネットで「日経400一覧」というキーワードで検索して自ら収集したものです。
(ちょっと面倒だった・・・。)

monex.rename(columns={"証券コード": "symbol"}, inplace=True)

次に、読み込んだデータの中で、
証券コード」という列の名前を、英語表記の「symbol」に変更しています。
これにより、今後の分析やプログラム処理で、コードがシンプルで扱いやすくなります。

それから、ちょっと補足です。

実は、個人的に日経400以外に米国株など他のアセットも同じように解析していて。
そのときに、銘柄を識別するための名前を「symbol」に統一して扱うようにしました。

だから、今回ここで「必ずsymbolにしなきゃいけない」というわけではありません。

monex["symbol"] = monex["symbol"].astype(str)

さらに、「symbol」列のデータ型を文字列(str型)に変換しています。
これは、あとでラベルエンコーディングするための準備(だったような?)です。

monex

最後に、整形したmonexデータフレーム全体を表示し、
データの中身を確認しています。

このブロックで、銘柄データの読み込みと整形が完了しました。

(そういえば、普段マネックス使ってるからmonexにしちゃったけど、monexじゃなくても良かった・・・。まあ、この際、細かいことは気にしない。)

データを保存するためのフォルダを作成する

import os

os.makedirs("kline", exist_ok=True)

次に、プログラム内でデータを保存するためのフォルダを作成します。

まず、osモジュールをインポートしています。
これは、ファイルやフォルダの操作を行うための標準ライブラリです。

続いて、os.makedirs関数を使って、
kline」という名前のフォルダを作成しています。

ここで指定しているexist_ok=Trueというオプションは、
もしすでに同じ名前のフォルダが存在していても、エラーを出さずにスルーする、という意味です。

これにより、毎回プログラムを実行しても、
安心してフォルダ作成処理を走らせることができます。

このブロックで、データの保存先となるフォルダが準備できました。

いよいよ、株価データの取得!

import yfinance as yf
from tqdm import tqdm
import time
from datetime import timedelta

err_list = []
for i in tqdm(range(len(monex))):
    time.sleep(0.5)

    row = monex.iloc[i]
    symbol = row["symbol"]

    # --------------------------------------
    # 特定シンボル変換
    # --------------------------------------
    # 特定のシンボルを変換する辞書
    symbol_map = {
    }

    # シンボルを変換(該当する場合のみ変更)
    symbol = symbol_map.get(symbol, symbol)

    # --------------------------------------
    # 該当データなしの場合、スキップ
    # --------------------------------------
    if row["symbol"] in []:
        continue

    # --------------------------------------
    # データ取得・保存
    # --------------------------------------
    days_ago = datetime.today() - timedelta(days=3000)
    start_date = days_ago.strftime("%Y-%m-%d")

    df = yf.download(symbol+".T", start=start_date, interval="1d", progress=False)

    is_error = True
    if len(df) == 0:
        if is_error:
            print()
            print("ERROR!", i, row["symbol"], symbol)
            err_list.append({"index": i, "symbol": row["symbol"], "symbol": symbol})
            # break
    else:
        is_error = False

    if not is_error:
        file_name = row['symbol'].replace(" ", "_")
        df.columns = ["Close","High","Low","Open","Volume"]
        df.to_csv(f"kline/{file_name}.csv")

さあ、いよいよ株価データの取得です!

データ取得と進行管理に使うライブラリのインポート

import yfinance as yf
from tqdm import tqdm
import time
from datetime import timedelta

まず、データ取得と進行管理に使うライブラリをインポートします。

import yfinance as yf

まず、yfinanceライブラリをインポートします。
これは、Yahoo Financeから株価データを取得できる非常に便利なライブラリです。
これを使うことで、過去の株価情報を簡単にダウンロードできます。

from tqdm import tqdm

次に、tqdmをインポートします。
tqdmは、ループ処理の進捗状況をプログレスバーで表示するためのライブラリです。
データを大量に取得する場合でも、処理の進み具合が一目でわかるようになります。

import time

さらに、Python標準ライブラリのtimeモジュールもインポートします。
これは、一時停止(スリープ)や、実行時間の測定に使います。

from datetime import timedelta

最後に、timedeltaをインポートします。
timedelta日付に対して一定期間の加算・減算をするために使います。

実際の銘柄ごとの処理(for文)

err_list = []
for i in tqdm(range(len(monex))):
    time.sleep(0.5)

    row = monex.iloc[i]
    symbol = row["symbol"]

ここからは、実際に銘柄ごとの処理を行っていきます。

err_list = []

まず最初に、err_listという空のリストを作成しています。
これは、後でエラーが発生した銘柄を記録するためのリストです。

for i in tqdm(range(len(monex))):

次に、forループを使って、
銘柄リストmonexの全行に対して1件ずつ処理を行う準備をしています。(monexって名前が気になる・・・)

range(len(monex))で、銘柄の件数分だけループを回します。
さらに、ループの進行状況がひと目でわかるように、
tqdmを使ってプログレスバーを表示しています。

    time.sleep(0.5)

ループの中では、まずtime.sleep(0.5)を実行して、
処理を0.5秒だけ一時停止しています。
これは、アクセス過多を防ぐための工夫で、
特に外部サイト(今回はYahoo Finance)に連続でリクエストを送る場合に重要なマナーです。

    row = monex.iloc[i]
    symbol = row["symbol"]

次に、現在対象となっている行データ(銘柄情報)をrowという変数に格納し、
そこからsymbol(証券コード)を取り出しています。

この準備によって、
このあと各銘柄に対して個別のデータ取得や処理ができるようになります。

1銘柄ぶんの株価データを取得

    days_ago = datetime.today() - timedelta(days=3000)
    start_date = days_ago.strftime("%Y-%m-%d")

    df = yf.download(symbol+".T", start=start_date, interval="1d", progress=False)

次に、株価データを取得するための準備をしていきます。

まずは、過去3,000日前、つまりおよそ8年ちょっと前の日付を計算しています。

    days_ago = datetime.today() - timedelta(days=3000)
    start_date = days_ago.strftime("%Y-%m-%d")

ここで求めた start_date を使って、
yfinanceライブラリから株価データをダウンロードします。

    df = yf.download(symbol+".T", start=start_date, interval="1d", progress=False)

このとき、銘柄コードには「.T」を付けて、東京証券取引所のティッカー形式に合わせています。
また、取得する間隔は「1日ごと」、つまり日足データに設定しています。
progress=False にしているのは、ダウンロード中に進捗バーを表示しないようにするためです。
処理をすっきり見せるための小さな工夫ですね。

エラーチェック

    is_error = True
    if len(df) == 0:
        if is_error:
            print()
            print("ERROR!", i, row["symbol"], symbol)
            err_list.append({"index": i, "symbol": row["symbol"], "symbol": symbol})
            # break
    else:
        is_error = False

続いて、
先ほど取得した株価データdfが正常かどうかをチェックします。

    is_error = True

まず、is_errorTrueに設定して、
初期状態ではエラー前提で考えます。

    if len(df) == 0:

次に、もしdfの行数、つまり取得したデータ件数が0件だった場合は、
データの取得に失敗したと判断します。

        print()
        print("ERROR!", i, row["symbol"], symbol)
        err_list.append({"index": i, "symbol": row["symbol"], "symbol": symbol})

このとき、エラーの詳細をコンソールに表示します。先頭のprint()tqdmのプログレスバーの右に表示されると見づらいので、行の先頭文字にエラーが表示されるよう改行をいれてます。

エラーの表示内容は、

  • i(銘柄リスト上のインデックス番号)
  • row["symbol"](元データ上の証券コード)
  • symbol(”.T“付きのリクエスト用コード)

といった情報です。

さらに、失敗した銘柄の情報をerr_listに記録します。
これにより、あとでどの銘柄でエラーが発生したか一覧で確認できるようになります。

    else:
        is_error = False

一方、データが1件以上取得できていた場合は、
is_errorFalseにして、正常終了として扱います。

株価データをCSVファイル保存

    if not is_error:
        file_name = row['symbol'].replace(" ", "_")
        df.columns = ["Close","High","Low","Open","Volume"]
        df.to_csv(f"kline/{file_name}.csv")

ここでは、
先ほどのエラーチェックで問題がなかった銘柄だけ、
株価データをCSVファイルとして保存します。

    if not is_error:

まず、if not is_error:という条件で、
エラーがなかった銘柄だけ処理を続行します。

        file_name = row['symbol'].replace(" ", "_")

続いて、ファイル名を作成します。
銘柄コードの中に空白が含まれている場合に備えて、
replace(" ", "_")を使い、空白をアンダースコアに置き換えます。
これで、ファイル名として安全な形式になります。(Numeraiか何かの名残・・・。)

        df.columns = ["Close","High","Low","Open","Volume"]

次に、データフレームdfの列名を整理します。
もともとYahoo Financeから取得したデータは、
列名がシステム的なものだったりバラつきがあるので、
ここであらためて、

  • Close(終値)
  • High(高値)
  • Low(安値)
  • Open(始値)
  • Volume(出来高)

の5つに統一しています。(OHLCVの順じゃないのが違和感・・・)

        df.to_csv(f"kline/{file_name}.csv")

最後に、整形したデータを、
klineフォルダの中にCSVファイルとして保存します。
ファイル名は、作成したfile_nameを使い、
拡張子は.csvです。

このブロックによって、
銘柄ごとの株価データをきれいに保存することができました。

エラー情報をCSVに保存

print("len(err_list): ", len(err_list))
print()
print(err_list)
pd.DataFrame(err_list).to_csv("err_list.csv")

最後に、これまでの処理でエラーが発生した銘柄の情報をまとめます。

print("len(err_list): ", len(err_list))

まず、print("len(err_list): ", len(err_list))で、
エラーが発生した銘柄数、つまりerr_listに記録された件数を表示します。
これで、全体のうちどれくらい失敗したかが一目でわかります。

print()
print(err_list)

続いて、
print(err_list)で、
エラーになった銘柄それぞれの情報を一覧表示します。
インデックス番号と銘柄コードの対応も、ここで確認できます。

pd.DataFrame(err_list).to_csv("err_list.csv")

さらに、pandasを使って、
err_listをデータフレームに変換し、
その内容をerr_list.csvという名前でCSVファイルに保存しています。

これにより、後から

  • どの銘柄で問題が起きたか
  • どのリクエストコードに失敗したか

をまとめてチェックできるようになります。

このブロックで、エラー管理が完了し、後続の分析や再取得もスムーズに進められる準備ができました。

データを圧縮してGoogle Driveに保存

!zip -qr kline400.zip kline
!cp err_list.csv {D}
!cp kline400.zip {D}

print(f"終了時刻: {datetime.now(japan)}")

最後の処理として、
取得したデータをまとめて保存し、作業の終了を記録します。

!zip -qr kline400.zip kline

まず、!zip -qr kline400.zip klineで、
これまで保存してきた各銘柄のCSVファイルをひとつにまとめ、
kline400.zipという名前のZIPファイルに圧縮しています。

ここで使っているオプションは、

  • -q(quiet):ログ出力を控えめにする
  • -r(recursive):フォルダ内のすべてのファイルを対象にする

という設定です。
これで、たくさんの銘柄ファイルをコンパクトにまとめることができます。

!cp err_list.csv {D}

次に、!cp err_list.csv {D}で、
エラー情報をまとめたerr_list.csvファイルを、
作業ディレクトリDにコピーしています。
これにより、エラーリストもまとめて管理できます。

!cp kline400.zip {D}

同様に、!cp kline400.zip {D}で、
圧縮したデータファイルもDにコピーします。
これで、Google Drive上にすべての成果物が安全に保存されました。

print(f"終了時刻: {datetime.now(japan)}")

最後に、
print(f"終了時刻: {datetime.now(japan)}")を実行して、
スクリプトの終了時刻を日本時間で記録します。

このブロックで、データ収集から保存までの一連の作業が完了しました。

まとめ

これで、yfinanceを使って株価データを取得し、Google Driveに保存するところまで完了しました!

次回は、今回保存したデータをもとに、
特徴量の作成に取り組んでいきます。

コメント

タイトルとURLをコピーしました