132
164

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Yahoo Finance USから株価をダウンロードしてみた

132
Last updated at Posted at 2019-01-15

米国の株式投資環境は、個人投資家にとって世界でも特に利用しやすい環境の一つであると考えています。歴史的に見ても株価データの取得は比較的容易であり、その代表的なサービスが米国版Yahoo Financeでした。特に、APIなどを利用して過去の株価データを取得できる環境は、個人による金融データ分析を大きく前進させました。

しかし、2017年前後から従来のデータ取得方法が利用できなくなるなど、無料で過去の株価データを取得する環境には変化が生じています。2017年以降、このような環境の変化に対応しながら、無料で株価データを取得する方法を紹介してきました。

現在、Yahoo Financeの料金案内では、過去データのCSVダウンロードが有料プランの機能として掲載されています。また、日本からの特定の利用環境では、この有料プランを契約できませんでした。無料で長期間の米国株価データを安定して取得できるサービスも、以前より限られてきています。

そこで本稿では、米国版Yahoo FinanceのWebページをブラウザに保存し、その保存ファイルからPythonを使って株価データを抽出する方法を紹介します。

方法は2段階に分かれます。まず

のホームページに行き、必要な銘柄コードを入力し、銘柄ページにあるHistorical Dataをクリックします。次にもっとも左の期間選択のタグをクリックしMAXをクリックします。また、必要に応じて期間を変更します。表示期間を設定してデータが読み込まれたことを確認した後、ブラウザのページ保存機能を使ってHTMLまたはMHTML形式で保存します。

つぎに会話型AIにこのファイルを読み込ませ、「添付のファイルから株価データを抽出してエクセルファイルとして保存して下さい」というと保存されたWebページを解析するPythonプログラムを生成し、株価表を抽出してくれます。

この方法ではたくさんのデータを取得したい場合は、時間がかかってしまうので、会話型AIにファイル名を指定して株価データを抽出するコードを作ってもらえます。 この方法は比較的汎用性が高く、列名や表の形式に軽微な変更が生じた場合にも、コードを修正することで対応しやすいという特徴があります。ただし、Yahoo Financeのページ構造が大きく変更された場合には、プログラムの修正が必要になります。

つぎにコードはChatGPTに作ってもらったコードです。

from pathlib import Path

#script = r''' #!/usr/bin/env python3

"""
Yahoo Finance の Historical Data ページを保存した
HTML / MHTML ファイルから株価表を抽出して CSV に保存します。

使い方:
    python extract_yahoo_history.py input.mhtml
    python extract_yahoo_history.py input.mhtml -o output.csv
"""

from __future__ import annotations

import argparse
import re
from email import policy
from email.parser import BytesParser
from io import StringIO
from pathlib import Path


STANDARD_COLUMNS = [
    "Date", "Open", "High", "Low", "Close", "Adj Close", "Volume"
]


def read_saved_page(path: Path) -> str:
    """HTML または MHTML からHTML本文を取り出す。"""
    suffix = path.suffix.lower()

    if suffix in {".mhtml", ".mht"}:
        with path.open("rb") as f:
            message = BytesParser(policy=policy.default).parse(f)

        html_parts: list[str] = []

        if message.is_multipart():
            for part in message.walk():
                if part.get_content_type() == "text/html":
                    try:
                        html_parts.append(part.get_content())
                    except Exception:
                        payload = part.get_payload(decode=True)
                        if payload:
                            charset = part.get_content_charset() or "utf-8"
                            html_parts.append(
                                payload.decode(charset, errors="replace")
                            )
        elif message.get_content_type() == "text/html":
            html_parts.append(message.get_content())

        if not html_parts:
            raise ValueError("MHTML内に text/html 部分が見つかりません。")

        # 通常は最も長いHTML部分が本体
        return max(html_parts, key=len)

    # 通常のHTML
    raw = path.read_bytes()
    for encoding in ("utf-8", "utf-8-sig", "cp932", "latin-1"):
        try:
            return raw.decode(encoding)
        except UnicodeDecodeError:
            continue

    return raw.decode("utf-8", errors="replace")


def flatten_column(column: object) -> str:
    """MultiIndexを含む列名を単純な文字列にする。"""
    if isinstance(column, tuple):
        parts = [
            str(x).strip()
            for x in column
            if str(x).strip() and not str(x).startswith("Unnamed")
        ]
        return " ".join(parts)
    return str(column).strip()


def normalize_column_name(name: object) -> str | None:
    """
    Yahoo側の説明付き列名を標準名へ変換する。
    例:
      'Close Close price adjusted for splits.' -> 'Close'
      'Adj Close Adjusted close price ...'     -> 'Adj Close'
    """
    text = flatten_column(name)
    text = re.sub(r"\s+", " ", text).strip()
    lower = text.lower()

    if lower.startswith("date"):
        return "Date"
    if lower.startswith("adj close") or lower.startswith("adjusted close"):
        return "Adj Close"
    if lower.startswith("open"):
        return "Open"
    if lower.startswith("high"):
        return "High"
    if lower.startswith("low"):
        return "Low"
    if lower.startswith("close"):
        return "Close"
    if lower.startswith("volume"):
        return "Volume"

    return None


def choose_history_table(tables: list[pd.DataFrame]) -> pd.DataFrame:
    """Historical Dataらしい表を選ぶ。"""
    candidates: list[tuple[int, int, pd.DataFrame]] = []

    for table in tables:
        mapped = [normalize_column_name(c) for c in table.columns]
        names = {x for x in mapped if x is not None}

        score = len(names.intersection(STANDARD_COLUMNS))

        # Date, Close, Volumeを含む表を優先
        if {"Date", "Close", "Volume"}.issubset(names):
            candidates.append((score, len(table), table))

    if not candidates:
        available = [
            [flatten_column(c) for c in table.columns]
            for table in tables
        ]
        raise ValueError(
            "Historical Data表を特定できませんでした。\n"
            f"検出された表の列名: {available}"
        )

    # 列一致数、行数の順に最大の表を採用
    candidates.sort(key=lambda x: (x[0], x[1]), reverse=True)
    return candidates[0][2].copy()


def clean_history_table(table: pd.DataFrame) -> pd.DataFrame:
    """列名、日付、数値を整形する。"""
    rename_map: dict[object, str] = {}

    for col in table.columns:
        normalized = normalize_column_name(col)
        if normalized:
            rename_map[col] = normalized

    df = table.rename(columns=rename_map)

    existing = [c for c in STANDARD_COLUMNS if c in df.columns]
    df = df.loc[:, existing].copy()

    if "Date" not in df or "Close" not in df:
        raise ValueError("Date列またはClose列がありません。")

    # 日付を変換。配当・株式分割などのイベント行も日付自体は有効なので、
    # 後段で価格列が空の行を除外する。
    df["Date"] = pd.to_datetime(df["Date"], errors="coerce")

    numeric_columns = [
        c for c in ["Open", "High", "Low", "Close", "Adj Close", "Volume"]
        if c in df.columns
    ]

    for col in numeric_columns:
        cleaned = (
            df[col]
            .astype(str)
            .str.replace(",", "", regex=False)
            .str.replace(r"[^\d.\-]", "", regex=True)
            .replace({"": pd.NA, "-": pd.NA, ".": pd.NA})
        )
        df[col] = pd.to_numeric(cleaned, errors="coerce")

    # 株価行のみ残す
    price_columns = [c for c in ["Open", "High", "Low", "Close"] if c in df]
    df = df.dropna(subset=["Date"])
    if price_columns:
        df = df.dropna(subset=price_columns, how="all")

    # 重複日を除去し、古い順に並べる
    df = (
        df.drop_duplicates(subset=["Date"], keep="first")
        .sort_values("Date")
        .reset_index(drop=True)
    )

    # 標準列順にそろえる
    ordered = [c for c in STANDARD_COLUMNS if c in df.columns]
    return df.loc[:, ordered]


def extract_history(input_path: Path) -> pd.DataFrame:
    html = read_saved_page(input_path)

    try:
        tables = pd.read_html(StringIO(html))
    except ValueError as exc:
        raise ValueError(
            "保存ページ内にHTML表が見つかりませんでした。"
            "Yahoo側がデータをJavaScript内部だけに保持している可能性があります。"
        ) from exc

    history = choose_history_table(tables)
    return clean_history_table(history)



ナスダック100のETFであるQQQのデータを保存します。長期的な投資を目的とする場合にはAdj Closeの方が適切です。通常の終値Closeよりも、分割と配当を調整したAdj Closeの方が適しています。Yahoo Financeも調整後終値を分割と配当分配を調整した価格として説明しています。

from pathlib import Path
import pandas as pd
import numpy as np

file_name="qqq.mhtml"
input_file = Path(file_name)
output_file = input_file.with_suffix(".csv")

df = extract_history(input_file)
df.to_csv(output_file,index=False)
df = df.set_index("Date")
np.log(df.Close).plot()

image.png

プログラムコードの構造

main()

└─ extract_history()

├─ read_saved_page()

├─ pd.read_html()

├─ choose_history_table()
│ └─ normalize_column_name()
│ └─ flatten_column()

└─ clean_history_table()
└─ normalize_column_name()
└─ flatten_column()
役割の階層で整理すると、次のようになります。

関数 役割の水準
flatten_column() 列名を文字列化する小さな補助関数
normalize_column_name() 列名を標準化する補助関数
read_saved_page() ファイルを読み込む関数
choose_history_table() 株価表を選ぶ関数
clean_history_table() 株価表を整形する関数
extract_history() 抽出処理全体を統括する関数
main() 入出力を含むプログラム全体を管理する関数

以後の分析では、必要な過去株価データは、この方法などを用いてあらかじめCSV形式で保存されているものとして扱います。

US yahoo! financeのデータ構成

  • Open:始値
  • High:高値
  • Low:安値
  • Close:終値 - 分割調整後
  • Volume:出来高
  • Adj Close:配当込み分割調整後株価

株価データの可視化

代表的な米国ETF(上場投資信託)

 ETFとは上場投資信託のことで手数料の点で株式と同じ手数料で売買ができるために、通常の投資信託よりも費用の面で有利です。

 では実際にダウンロードしてみましょう。

%matplotlib inline 
import matplotlib.pyplot as plt #描画ライブラリ
import pandas as pd
import numpy as np
import pandas_datareader.data as web
import datetime
ticker = 'usmv'
filename = f"{ticker}.csv"
df = pd.read_csv(filename,
    index_col="Date",
    parse_dates=["Date"]
)
np.log(df.Close).plot()
plt.show()

image.png

 ダウンロードした株価は米国の上場投資信託(ETF)のリスクパリティ-最小分散ポートフォリオ戦略を用いたusmvです。

 このETFは安定した収益の獲得を目指し、最小分散ポートフォリオというアルゴリズムを用いて運用されています。

 ポートフォリの詳細はつぎURLよりExcel形式でダウンロード可能です。

ダウジョーンズ

ticker = 'dia'
filename = f"{ticker}.csv"
df = pd.read_csv(filename,
    index_col="Date",
    parse_dates=["Date"]
)
np.log(df.Close).plot()
plt.show()

image.png

ナスダック100

ticker = 'qqq'
filename = f"{ticker}.csv"
df = pd.read_csv(filename,
    index_col="Date",
    parse_dates=["Date"]
)
np.log(df.Close).plot()
plt.show()

image.png

 QQQはInvescoが運用するナスダック100連動のETFですが、そのポートフォリオの詳細はつぎのURLよりExcel形式でダウンロード可能です。

日経平均株価

 日本の代表的な株価指数である日経平均株価(日経225)もダウンロード可能です。1988年4月8日から4本値が取得できます。それ以前は終値のみです。

ticker = '^N225'
filename = f"{ticker}.csv"
df = pd.read_csv(filename,
    index_col="Date",
    parse_dates=["Date"]
)
np.log(df.Close).plot()
plt.show()

image.png

長期のデータに関してはFREDから入手します。FREDは米国セントルイス連銀が運営する経済データバンクです。1991年から運営されています。

ticker = 'NIKKEI225'
n225 = web.DataReader(ticker, "fred","1949/5/16")
n225.plot()
plt.show()

image.png

長期のデータに関しては対数をとることをお勧めします。Python3ではじめるシステムトレード: システムトレードにおける対数の役割

np.log(n225).plot()
plt.show()

image.png
アベノミクス(2013年)以降の指数をダウンロードしてみましょう。

n225.loc['2013':].plot()
plt.show()

image.png

np.log(n225.loc['2013':]).plot()
plt.show()

image.png

米国株投資の日本円でリターン

最近、日本株が安定した値上がりを示しているために、米国株とのパフォーマンスの比較が出ます。多くは日経225の日本円でのリターンと米国株のドル建てのリターンを比べています。ここでは米国株投資を日本円のリターンに直して示してみます。

ナスダック

日経平均とNASDAQに投資した際の円建てのリターンを為替リスクのヘッジなしで比べたときの結果を示します。

FRED から USD/JPY のデータを取得

start_date = "1971/2/5"
end_date = datetime.datetime.now()
jpy = web.DataReader("DEXJPUS", "fred", start_date, end_date)

# NaNを削除(必要なら)
jpy = jpy.dropna()

# jpy.index も pd.Timestamp 型に変換
jpy.index = pd.to_datetime(jpy.index)

ndx = web.DataReader("NASDAQCOM", "fred", start_date, end_date)
# データをフィルタリング
jpy = jpy[jpy.index >= pd.Timestamp(start_date)]
ndx = ndx[ndx.index >= pd.Timestamp("1971-02-05")]

# データフレームを作成して結合
df = pd.DataFrame({'NDX_USD': ndx.NASDAQCOM, 'DEXJPUS': jpy.DEXJPUS}).dropna()

# ndxの価格を日本円に換算
df['NDX_JPY']= (df['NDX_USD'] * df['DEXJPUS'])
df=df.dropna()

# 正規化して対数スケールでプロット
ax=np.log(df.NDX_JPY/df.NDX_JPY.loc[start_date]).plot()
np.log(n225.loc[start_date:] / n225.loc[start_date]).plot(ax=ax)
plt.show()

image.png

主な株価指数の構成銘柄

代表的なナスダック100指数を構成する銘柄

 アメリカ経済を牽引する銘柄群です。

NDX=pd.read_csv("nasdaq.csv",header=None)
NDX.head(30)

image.png

nasdaq.csvは自分で作ることができます。https://en.wikipedia.org/wiki/Nasdaq-100
にナスダック100のリストがありますから、それをエクセルにペーストして、銘柄コードだけをCSVファイルとして落とせば完了です。1年に一度、見直しがあるので、入れ替えは頻繁に起こると思ってください。

代表的なダウ平均株価指数採用銘柄

「ダウ平均」、「ニューヨーク・ダウ」、「ニューヨーク平均株価」などとして親しまれている当指数は、チャールズ・ダウにより考え出され、1884年から算出されています。発表当初は鉄道株が中心でした。19世紀末には変わりゆく経済の姿を受け、1896年から新たなダウ工業株平均の算出が始まりました。その後ダウ工業株30種平均株価を構成する銘柄は時代の流れに合わせて入れ替えが行われています。今現在もっとも古くから採用されている銘柄はエクソンモービルで1928年から採用されています。

現在の構成銘柄は、wikiのページから銘柄リストをエクセルにペーストしてティッカーの部分だけを残してdjia.Csvファイルとして保存します。

DJIA=pd.read_csv("djia.csv",header=None)
DJIA.head()

for x in DJIA.iloc[:,0]:
    ticker = yf.Ticker(x)
    tsd = ticker.history(period="1y")
len(DJIA)

image.png

代表的なS&P株

現在の構成銘柄は、wikiのページから銘柄リストをエクセルにペーストしてティッカーの部分だけを残してsp500.CSVファイルとして保存します。

SP500=pd.read_csv("sp500.csv",header=None)
SP500.head()

image.png

代表的な株価指数ETF

ETF=['DIA','SPY','QQQ','IBB','XLV','IWM','EEM','EFA','XLP','XLY','ITB','XLU','XLF',
     'VGT','VT','FDN','IWO','IWN','IYF','XLK','XOP','USMV','BAB','GLD',
    'VNQ','SCHH','IYR','XLRE','AGG','BND','LQD','VCSH','VCIT','JNK']

代表的なバイオ・ヘルス株

代表的なバイオ・ヘルス株の銘柄を得るために代表的なバイオ株へ投資しているETFを参考に銘柄を絞り込みます。まず、IBBを参考にします。IBBのホームページに行きます。
https://www.ishares.com/us/products/239699/ishares-biotechnology-etf
つぎにholdingをクリックすると、投資銘柄リストをダウンロードできるボタンがあります。
image.png
左下のDetailed Holdings and Analyticsのボタンを押すと関連ファイルがダウンロードホルダーにダウンロードできます。
IBB_holdings.csv
エクセルでファイルを開いてティカーコードだけを残して、IBB.csvとして保存します。

ticker = 'IBB'
filename = f"{ticker}.csv"
df = pd.read_csv(filename,
    index_col="Date",
    parse_dates=["Date"]
)
np.log(df.Close).plot()

image.png

代表的な暗号通貨

ccurrency=["BTC-USD","XRP-USD","ETH-USD","LTC-USD","BCH-USD","BNB-USD",
       "EOS-USD","USDT-USD","LINK-USD","TRX-USD","ADA-USD",
       "XLM-USD","XMR-USD","DASH-USD","NEO-USD","IOT-USD",
       "VEN-USD","ETC-USD","XEM-USD","ZEC-USD","XRB-USD","QTUM-USD",
       "BTG-USD","BAT-USD","DOGE-USD"]
Bitcoin USD BTC-USD
Ripple USD XRP-USD
Ethereum USD ETH-USD
Litecoin USD LTC-USD
Bitcoin Cash / BCCUSD BCH-USD
Binance Coin USD BNB-USD
EOS USD EOS-USD
Tether USD USDT-USD
ChainLink USD LINK-USD
Tronix USD TRX-USD
Cardano USD ADA-USD
Stellar USD XLM-USD
Monero USD XMR-USD
DigitalCash USD DASH-USD
NEO USD NEO-USD
IOTA USD IOT-USD
Vechain USD VEN-USD
Ethereum Classic USD ETC-USD
NEM USD XEM-USD
ZCash USD ZEC-USD
Nano USD XRB-USD
QTUM USD QTUM-USD
Bitcoin Gold USD BTG-USD
Basic Attention Token USD BAT-USD
Dogecoin USD DOGE-USD

さて、重要なことをまだ説明していません。それはデータを分析することで何が得られるのかという点です。多くの人は何か理論的な確証を得て効率的で効果的な良い投資ができるのではと考えています。しかし、実はそのような理論も事実もありません。あるのは株価の過去の動きを知ることができるということだけです。この過去の株価の動きが手元にあれば、株取引の疑似体験ができます。どのようなときに買い、どのようなときに売るとどうかるのかということを経験できます。まずは疑似トレードを繰り返し、自分の目的を達成できる取引方法を見つけることです。その際に、株価の上昇が予測できたので買うことにしたとします。その後、目標とした利益が上がるまで損失が出ることなく株価が上がり続けることは通常は起こりません。つまり、ある期間にわたり、損失がでることは避けられません。取引方法を見つけた後に、確認することは、あなた自身のリスク許容度をつかむことです。株式を売らずに持ち続けることは簡単なことではありません。株価が下落すれば、すぐに売りたくなってしまいます。でもそれでは効率的な投資にはなりません。そこであなたがどの程度の下落に耐えられるのかをまずつかむ必要があります。そうすれば、株価が上がり、目標とした利益がでるまで持ち続けることができます。

参考

Python3ではじめるシステムトレード【第2版】環境構築と売買戦略

「画像をクリックしていただくとpanrollingのホームページから書籍を購入していただけます。

「画像をクリックしていただくとpanrollingのホームページから書籍を購入していただけます。」

001725072026 79345

132
164
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
132
164

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?