中級者向け No.077

株価チャートビューアー

銘柄コードを入力してyfinanceで株価を取得しmatplotlibで移動平均線付きチャートを表示するアプリ。

🎯 難易度: ★★★ 📦 ライブラリ: tkinter(標準ライブラリ), yfinance, matplotlib ⏱️ 制作時間: 30〜90分

1. アプリ概要

銘柄コードを入力してyfinanceで株価を取得しmatplotlibで移動平均線付きチャートを表示するアプリ。

このアプリはdataカテゴリの実践的なPythonアプリです。使用ライブラリは tkinter(標準ライブラリ)・yfinance・matplotlib、難易度は ★★★ です。

このアプリは「データ処理」カテゴリです。データ分析・処理アプリは Python の真骨頂と言える領域で、生データから意味ある可視化・要約までを一通り体験できます。tkinter(標準ライブラリ)・yfinance・matplotlib を活かして実装するこの構造は、他のアプリにも応用が効きます。

🌐
外部通信あり(Yahoo! Finance)/学習用サンプルです

このアプリは yfinance 経由で Yahoo! Finance へ通信して株価データを取得します(送るのはティッカーと期間だけ)。通信するのは「▶ 取得・表示」ボタン・入力例ボタン・Enter キーのいずれかで取得を実行したときだけで、起動しただけでは通信しません。オフラインでも起動はでき、取得に失敗するとメッセージを出して待機状態に戻ります。yfinance は Yahoo! Finance の非公式ライブラリで、取得できるデータの精度・継続性は保証されません。表示するのは取得した数値そのままで、売買のシグナルや推奨は一切出しません。投資判断はご自身で行ってください。取得したデータの扱いは Yahoo! の利用規約に従い、個人の学習・分析の範囲で使ってください(再配布は要確認)。当サイトは投資助言業の登録を行っておらず、本記事は Python の学習記録です(免責事項)。

コードを読む前に実行することをおすすめします。動いている挙動を先に把握しておくと、解説で出てくる関数や処理がどこに対応するかが頭に入りやすくなります。

チャートの元になる株価データの取り方(日本株の書き方・期間と間隔の指定・取得できないケース)は、Pythonで株価を取得する方法(yfinance)で実行結果つきに解説しています。

応用のヒントは、機能を 1 つ増やす・見た目を整える・例外を一つでも丁寧に扱う、のいずれかから始めるのがおすすめです。

株価チャートビューアー 実行画面(Windows)
実行画面(Windows)
株価チャートビューアー 実行画面(Linux Mint)
実行画面(Linux Mint)

2. 機能一覧

  • ティッカーと期間を指定して yfinance で株価(OHLCV)を取得(yf.Ticker(symbol).history(period=..., auto_adjust=True)
  • 期間は PERIODS の5種類(1ヶ月・3ヶ月・6ヶ月・1年・2年)から選択
  • チャートは「ローソク足」「折れ線」を切り替え表示(chart_var
  • MA_WINDOWS = (5, 25) の移動平均線をチェックボックスで重ね描き(データが足りない期間は描かれない)
  • 出来高を下段のサブプロットに棒グラフで表示(上昇日・下落日で色分け)
  • 「▶ 取得・表示」ボタン(_fetch())で操作
  • キー <Return> から _fetch() を実行
  • 入力例のボタン(EXAMPLE_SYMBOLS = ("7203.T", "9984.T", "AAPL", "MSFT"))を押すと、その文字列がティッカー欄に入って取得まで進む(ティッカーの書き方を確かめるための例で、銘柄の推奨ではありません)
  • ttk.Treeview による表形式の一覧表示(直近50件の日付・始値・高値・安値・終値・出来高)
  • 取得は threading.Thread の別スレッドで行い、結果は root.after() で画面へ戻す(取得中も画面が固まらない)
  • 取得失敗・データ0件は messagebox.showerror()messagebox.showwarning() で通知し、アプリは終了しない
  • ステータスバーに終値・前営業日比・取得期間・取得時刻を表示
  • ウィンドウはタイトル「株価チャートビューアー」・サイズ 1071x782 で起動

3. 事前準備・環境

ℹ️
動作確認環境

Windows 11 / Python 3.12.10 / yfinance 1.4.1 / matplotlib 3.11.1(2026-09-09 に実行して確認)/Linux Mint 22.3 は 2026-09-10 に起動を確認(macOS は未検証)

Windows 11(実機)で、ティッカー 7203.T・期間「3ヶ月」「1年」の取得と描画、存在しないティッカー、通信できない状態の3ケースを実行して確認しています(全機能の網羅テストではありません)。Linux Mint 22.3(仮想マシン)では起動して画面が表示されるところまで確認しました(株価の取得は Windows 11 での確認です)。macOS は未検証です。

  • Python 3.10 以上(確認は 3.12.10)
  • OS: Windows 11(実機で実行して確認)/Linux Mint 22.3(仮想マシンで起動を確認)
  • インターネット接続(「取得・表示」ボタン・入力例ボタン・Enter キーのいずれかで取得を実行したときだけ Yahoo! Finance と通信します)

インストールが必要なライブラリ

pip install yfinance matplotlib

yfinance のインストール時に pandas などが一緒に入ります。ティッカーの書き方(日本株は 7203.T のように .T を付ける)や、期間・間隔の指定、データが取れないケースは、Pythonで株価を取得する方法(yfinance)で実行結果つきに解説しています。

4. 完全なソースコード

💡
コードのコピー方法

右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。

📦 必要なライブラリをインストール(初回のみ)
pip install yfinance matplotlib
app077.py
"""株価チャートビューアー(tkinter + yfinance + matplotlib)

ティッカーと期間を指定して Yahoo! Finance の株価(OHLCV)を yfinance で取得し、
ローソク足/折れ線チャート・移動平均線・出来高・データ表を表示する学習用サンプル。

- 起動しただけでは通信しない(「取得・表示」ボタン・入力例ボタン・Enter キーのいずれかで取得を実行したときだけ通信する)
- 取得に失敗しても落ちない(メッセージを出して待機状態に戻る)
- 表示するのは取得した数値そのままで、売買の判断は行わない
"""
import tkinter as tk
from tkinter import ttk, messagebox
import threading
from datetime import datetime

try:
    import yfinance as yf
    import matplotlib
    matplotlib.use("TkAgg")
    from matplotlib.figure import Figure
    from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
    from matplotlib import font_manager
    import matplotlib.dates as mdates
    LIBS_AVAILABLE = True
except ImportError:
    LIBS_AVAILABLE = False

if LIBS_AVAILABLE:
    # グラフ内の日本語が豆腐(□)にならないよう、入っている和文フォントを1つ選ぶ
    _installed = {f.name for f in font_manager.fontManager.ttflist}
    for _name in ("Meiryo", "Yu Gothic", "Noto Sans CJK JP", "Noto Sans JP",
                  "Hiragino Sans", "IPAexGothic", "MS Gothic", "DejaVu Sans"):
        if _name in _installed:
            matplotlib.rcParams["font.family"] = _name
            break
    matplotlib.rcParams["axes.unicode_minus"] = False


class App077:
    """株価チャートビューアー"""

    # 画面の表示名 -> yfinance の period 文字列
    PERIODS = {"1ヶ月": "1mo", "3ヶ月": "3mo", "6ヶ月": "6mo",
               "1年": "1y", "2年": "2y"}
    MA_WINDOWS = (5, 25)
    # 動作確認用のティッカー入力例(銘柄の推奨ではない)。
    # 日本株は「証券コード + .T」、米国株はティッカーをそのまま書く書式の見本
    EXAMPLE_SYMBOLS = ("7203.T", "9984.T", "AAPL", "MSFT")
    UP_COLOR = "#26a69a"
    DOWN_COLOR = "#ef5350"
    PIP_HINT = "pip install yfinance matplotlib"

    def __init__(self, root):
        self.root = root
        self.root.title("株価チャートビューアー")
        self.root.geometry("1071x782")
        self.root.minsize(1071, 782)
        self.root.configure(bg="#1e1e1e")
        self._hist = None      # 直近に取得した DataFrame
        self._busy = False     # 取得中フラグ(二重クリック防止)
        self._build_ui()

    def _build_ui(self):
        header = tk.Frame(self.root, bg="#252526", pady=6)
        header.pack(fill=tk.X)
        tk.Label(header, text="📈 株価チャートビューアー",
                 font=("Noto Sans JP", 12, "bold"),
                 bg="#252526", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)

        if not LIBS_AVAILABLE:
            tk.Label(self.root,
                     text="⚠ yfinance / matplotlib が未インストールです ("
                          + self.PIP_HINT + ")",
                     bg="#fff3cd", fg="#856404", font=("Arial", 9),
                     anchor="w", padx=8).pack(fill=tk.X)

        # 入力バー
        search_f = tk.Frame(self.root, bg="#2d2d2d", pady=6)
        search_f.pack(fill=tk.X)

        tk.Label(search_f, text="ティッカー:", bg="#2d2d2d", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=4)
        self.symbol_var = tk.StringVar(value="7203.T")
        entry = ttk.Entry(search_f, textvariable=self.symbol_var, width=10)
        entry.pack(side=tk.LEFT, padx=4)
        entry.bind("<Return>", lambda e: self._fetch())

        tk.Label(search_f, text="期間:", bg="#2d2d2d", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.period_var = tk.StringVar(value="3ヶ月")
        ttk.Combobox(search_f, textvariable=self.period_var,
                     values=list(self.PERIODS.keys()),
                     state="readonly", width=8).pack(side=tk.LEFT)

        tk.Label(search_f, text="チャート:", bg="#2d2d2d", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
        self.chart_var = tk.StringVar(value="ローソク足")
        ttk.Combobox(search_f, textvariable=self.chart_var,
                     values=["ローソク足", "折れ線"],
                     state="readonly", width=10).pack(side=tk.LEFT)

        self.ma_var = tk.BooleanVar(value=True)
        ttk.Checkbutton(search_f, text="移動平均線(5/25)",
                        variable=self.ma_var,
                        command=self._redraw).pack(side=tk.LEFT, padx=8)

        self.fetch_btn = tk.Button(search_f, text="▶ 取得・表示",
                                   command=self._fetch,
                                   bg="#1565c0", fg="white", relief=tk.FLAT,
                                   font=("Arial", 10, "bold"), padx=12, pady=4,
                                   activebackground="#0d47a1", bd=0)
        self.fetch_btn.pack(side=tk.LEFT, padx=8)

        tk.Label(search_f,
                 text="※ Yahoo! Finance へ通信します(日本株は 7203.T のように .T を付ける)",
                 bg="#2d2d2d", fg="#888", font=("Arial", 8)).pack(side=tk.LEFT)

        # 入力例のボタン(毎回ティッカーを打たずに動作確認するためのもの)
        example_f = tk.Frame(self.root, bg="#2d2d2d", pady=2)
        example_f.pack(fill=tk.X)
        tk.Label(example_f, text="例:", bg="#2d2d2d", fg="#ccc",
                 font=("Arial", 9)).pack(side=tk.LEFT, padx=4)
        for sym in self.EXAMPLE_SYMBOLS:
            # lambda の既定引数 s=sym でループ変数をその場の値に固定する
            tk.Button(example_f, text=sym, bg="#37373d", fg="#cfd8dc",
                      relief=tk.FLAT, font=("Arial", 9), padx=6, bd=0,
                      command=lambda s=sym: (self.symbol_var.set(s),
                                             self._fetch())
                      ).pack(side=tk.LEFT, padx=2)
        tk.Label(example_f,
                 text="※ 銘柄の推奨ではなく、入力書式を確かめるための例です",
                 bg="#2d2d2d", fg="#888", font=("Arial", 8)).pack(side=tk.LEFT,
                                                                  padx=6)

        # チャートエリア
        self.chart_frame = tk.Frame(self.root, bg="#0d1117")
        self.chart_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=4)

        if LIBS_AVAILABLE:
            self.fig = Figure(figsize=(9, 5), facecolor="#0d1117")
            self.ax_price = self.fig.add_subplot(211)
            self.ax_vol = self.fig.add_subplot(212, sharex=self.ax_price)
            self.fig.tight_layout(pad=2)
            self.mpl_canvas = FigureCanvasTkAgg(self.fig, master=self.chart_frame)
            self.mpl_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
        else:
            tk.Label(self.chart_frame,
                     text="yfinance と matplotlib をインストールすると"
                          "チャートが表示されます",
                     bg="#0d1117", fg="#555", font=("Arial", 11)).pack(
                expand=True)

        # データテーブル
        cols = ("date", "open", "high", "low", "close", "volume")
        self.tree = ttk.Treeview(self.root, columns=cols, show="headings",
                                 height=5)
        for col, text, width in [
            ("date",   "日付",   90),
            ("open",   "始値",   70),
            ("high",   "高値",   70),
            ("low",    "安値",   70),
            ("close",  "終値",   70),
            ("volume", "出来高", 90),
        ]:
            self.tree.heading(col, text=text)
            self.tree.column(col, width=width, anchor="center")
        tsb = ttk.Scrollbar(self.root, orient=tk.HORIZONTAL,
                            command=self.tree.xview)
        self.tree.configure(xscrollcommand=tsb.set)
        self.tree.pack(fill=tk.X, padx=8)
        tsb.pack(fill=tk.X, padx=8)

        self.status_var = tk.StringVar(
            value="ティッカーを入力して「取得・表示」を押してください")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#252526", fg="#858585", font=("Arial", 9),
                 anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)

    def _fetch(self):
        """入力を検証し、取得をワーカースレッドへ渡す"""
        if not LIBS_AVAILABLE:
            messagebox.showerror("エラー", self.PIP_HINT)
            return
        if self._busy:
            return
        symbol = self.symbol_var.get().strip().upper()
        if not symbol:
            messagebox.showwarning("警告", "ティッカーを入力してください")
            return
        label = self.period_var.get()
        period = self.PERIODS.get(label, "3mo")
        self._busy = True
        self.fetch_btn.config(state=tk.DISABLED)
        self.status_var.set(f"{symbol} を取得中...")
        threading.Thread(target=self._load_data,
                         args=(symbol, label, period), daemon=True).start()

    def _load_data(self, symbol, label, period):
        """別スレッドで yfinance から取得(UI 操作は after 経由で本体に戻す)"""
        try:
            hist = yf.Ticker(symbol).history(period=period, auto_adjust=True)
            # 場が開いていない当日ぶんなど、値が欠けた行が混じることがあるので落とす
            hist = hist.dropna(subset=["Open", "High", "Low", "Close"])
        except Exception as e:                      # 通信断・API 変更など
            self._post(self._on_error, symbol, f"{type(e).__name__}: {e}")
            return
        self._post(self._on_loaded, symbol, label, hist)

    def _post(self, func, *args):
        """ウィンドウが閉じられていても例外を出さずに UI へ結果を返す"""
        try:
            self.root.after(0, func, *args)
        except tk.TclError:
            pass

    def _on_loaded(self, symbol, label, hist):
        self._busy = False
        self.fetch_btn.config(state=tk.NORMAL)
        if hist is None or hist.empty:
            self.status_var.set(f"{symbol}: データが取得できませんでした")
            messagebox.showwarning(
                "データなし",
                f"{symbol} のデータが空でした。\n"
                "ティッカーの綴り(日本株は 7203.T のように .T が必要)と"
                "期間を確認してください。")
            return
        self._hist = hist
        self._draw_chart(symbol, hist)
        self._fill_table(hist)
        close = float(hist["Close"].iloc[-1])
        if len(hist) >= 2:
            prev = float(hist["Close"].iloc[-2])
            chg = close - prev
            pct = chg / prev * 100 if prev else 0.0
            diff_text = f"前営業日比: {chg:+.2f} ({pct:+.2f}%)  "
        else:
            diff_text = ""
        self.status_var.set(
            f"{symbol}  {label}  {len(hist)}件  "
            f"終値: {close:,.2f}  {diff_text}"
            f"期間: {hist.index[0]:%Y-%m-%d} 〜 {hist.index[-1]:%Y-%m-%d}  "
            f"取得: {datetime.now():%H:%M:%S}")

    def _on_error(self, symbol, message):
        self._busy = False
        self.fetch_btn.config(state=tk.NORMAL)
        self.status_var.set(f"{symbol}: 取得に失敗しました - {message}")
        messagebox.showerror(
            "取得エラー",
            f"{symbol} の取得に失敗しました。\n"
            "ネットワーク接続とティッカーを確認してください。\n\n" + message)

    def _redraw(self):
        """移動平均やチャート種類の切り替え時に、取得済みデータを描き直す"""
        if self._hist is not None and LIBS_AVAILABLE:
            self._draw_chart(self.symbol_var.get().strip().upper(), self._hist)

    @staticmethod
    def _to_int(value):
        """出来高が欠損(NaN)の銘柄でも落ちないように 0 として扱う"""
        return int(value) if value == value else 0

    def _draw_chart(self, symbol, hist):
        index = hist.index
        if getattr(index, "tz", None) is not None:
            index = index.tz_localize(None)
        dates = list(index.to_pydatetime())
        opens = [float(v) for v in hist["Open"]]
        highs = [float(v) for v in hist["High"]]
        lows = [float(v) for v in hist["Low"]]
        closes = [float(v) for v in hist["Close"]]
        vols = [self._to_int(v) for v in hist["Volume"]]

        self.ax_price.clear()
        self.ax_vol.clear()

        if self.chart_var.get() == "ローソク足":
            for d, o, h, l, c in zip(dates, opens, highs, lows, closes):
                color = self.DOWN_COLOR if c < o else self.UP_COLOR
                self.ax_price.plot([d, d], [l, h], color=color, linewidth=0.8)
                self.ax_price.bar(d, max(abs(c - o), 0.01), bottom=min(o, c),
                                  color=color, width=0.6, linewidth=0)
        else:
            self.ax_price.plot(dates, closes, color="#4fc3f7", linewidth=1.2,
                               label="終値")

        # 移動平均線(データが足りない期間は自動的に描かれない)
        if self.ma_var.get():
            for window, ma_color in zip(self.MA_WINDOWS, ("#ffd54f", "#ba68c8")):
                if len(closes) >= window:
                    ma = hist["Close"].rolling(window).mean()
                    self.ax_price.plot(dates, list(ma), color=ma_color,
                                       linewidth=1.0, alpha=0.9,
                                       label=f"MA{window}")
        handles, _ = self.ax_price.get_legend_handles_labels()
        if handles:
            self.ax_price.legend(facecolor="#161b22", edgecolor="#30363d",
                                 labelcolor="#c9d1d9", fontsize=7)

        self.ax_vol.bar(dates, vols,
                        color=[self.DOWN_COLOR if c < o else self.UP_COLOR
                               for o, c in zip(opens, closes)],
                        width=0.6, linewidth=0)

        for ax in (self.ax_price, self.ax_vol):
            ax.set_facecolor("#0d1117")
            ax.tick_params(colors="#8b949e", labelsize=7)
            for spine in ax.spines.values():
                spine.set_edgecolor("#30363d")
            ax.grid(True, color="#21262d", linewidth=0.5)
            ax.xaxis.set_major_formatter(mdates.DateFormatter("%m/%d"))
            ax.xaxis.set_major_locator(mdates.AutoDateLocator())

        self.ax_price.set_title(f"{symbol} 株価チャート(Yahoo! Finance / yfinance)",
                                color="#c9d1d9", fontsize=10, pad=4)
        self.ax_price.set_ylabel("価格", color="#8b949e", fontsize=8)
        self.ax_vol.set_ylabel("出来高", color="#8b949e", fontsize=8)
        self.fig.tight_layout(pad=2)
        self.mpl_canvas.draw()

    def _fill_table(self, hist):
        self.tree.delete(*self.tree.get_children())
        tail = hist.tail(50)
        for ts, row in reversed(list(tail.iterrows())):
            self.tree.insert("", tk.END,
                             values=(f"{ts:%Y-%m-%d}",
                                     f"{row['Open']:.2f}",
                                     f"{row['High']:.2f}",
                                     f"{row['Low']:.2f}",
                                     f"{row['Close']:.2f}",
                                     f"{self._to_int(row['Volume']):,}"))


if __name__ == "__main__":
    root = tk.Tk()
    app = App077(root)
    root.mainloop()

5. コード解説

株価チャートビューアーのコードを、実際に書かれている実装に沿って解説します。

クラス設計とコンストラクタ

App077 クラスにアプリの全機能をまとめています(メソッド11個・全336行)。__init__ ではタイトル「株価チャートビューアー」とウィンドウサイズ 1071x782 を設定します。あわせて状態を保持する変数(self._histself._busy)を初期化し、最後に _build_ui() で画面を組み立てます。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

ウィジェット構成

画面は tk.Frame×4・tk.Label×10・ttk.Entryttk.Combobox×2・ttk.Checkbuttontk.Button×5(ループで生成)・ttk.Treeviewttk.Scrollbar で構成しています。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

イベント処理とボタンの接続

「▶ 取得・表示」ボタン(_fetch())を command= で接続しています。ボタンはループ内で command=lambda s=sym: (self.symbol_var.set(s), self._fetch()) の形で生成しています。既定引数でループ変数を固定するのがポイントで、これを書かないと全ボタンが最後の値で動いてしまいます。また bind("<Return>", ...) から _fetch() を呼べるようにイベントも登録しています。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

クラス定数によるデータ定義

クラスの先頭でデータを定数として定義しています:PERIODS = {'1ヶ月': '1mo', '3ヶ月': '3mo', '6ヶ月': '6mo', '1年': '1y', '2年': '2y'}MA_WINDOWS = (5, 25)。ロジックの中に直接書かず定数にまとめておくと、内容を変えたいときに1か所を書き換えるだけで済みます。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

例外処理

try-except で ExceptionImportErrortk.TclError を捕捉しています。あわせて messagebox.showerror()messagebox.showwarning() のダイアログで、ユーザーへの通知・確認を行います。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

入力例ボタンと lambda の既定引数

EXAMPLE_SYMBOLS の4件は for sym in self.EXAMPLE_SYMBOLS: でまとめてボタン化しています。中身は「日本株は証券コード+.T」「米国株はティッカーをそのまま」という入力書式の見本で、銘柄を推奨するものではありません。

ここで command=lambda: (self.symbol_var.set(sym), self._fetch()) と書くと、4つのボタンがすべて同じ銘柄を取得します。lambda の中身が実行されるのはボタンを押した時点で、そのとき参照される sym はループを回し終えた後の最後の値だからです。変数そのものを覚えていて、値を覚えているわけではない、と考えると分かりやすいです。

掲載コードの command=lambda s=sym: (self.symbol_var.set(s), self._fetch()) は、既定引数 s=sym関数を作った時点で評価される性質を使い、その周回の値を s に固定しています。動作確認環境(Windows 11 / Python 3.12.10 / yfinance 1.4.1)で 7203.TAAPL のボタンを押したときの処理を実行し、押したティッカーがそのまま入力欄に入って取得・描画まで進むことを確認しました。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

6. ステップバイステップガイド

このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。

  1. 1
    ファイルを作成する

    新しいファイルを作成して app077.py と保存します。外部ライブラリ matplotlibyfinance を使います。先に pip install matplotlib yfinance を実行してください。

  2. 2
    クラスの骨格を作る

    App077 クラスを定義し、__init__ と、末尾の root = tk.Tk()mainloop() の最小構成を書いて、まず空のウィンドウが出ることを確認します。

  3. 3
    ウィンドウを設定する

    title("株価チャートビューアー")geometry("1071x782")configure(bg="#1e1e1e")minsize(1071, 782) をコンストラクタで設定します。

  4. 4
    画面部品を並べる

    _build_ui() の中で、tk.Frame×4・tk.Label×10・ttk.Entryttk.Combobox×2・ttk.Checkbuttontk.Button×5(ループで生成)・ttk.Treeviewttk.Scrollbar を作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。

  5. 5
    イベントを接続する

    「2. 機能一覧」で挙げた各ボタンを command= で対応するメソッド(_fetch())につなぎます。ループ生成のボタンは command=lambda s=sym: (self.symbol_var.set(s), self._fetch()) の形で接続します。bind("<Return>", ...) の登録も忘れずに。

  6. 6
    中心になるメソッドを実装する

    アプリの本体である _fetch()(18行)・_draw_chart()(57行)・_on_loaded()(27行) を実装します。

  7. 7
    動作確認する

    python app077.py で起動し、各ボタンが反応すること・キー操作(<Return>)が効くことを確認します。

7. カスタマイズアイデア

基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。

💡 ダークモードを追加する

bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。

💡 クラス定数を書き換えて動きを変える

データはクラス定数(PERIODSMA_WINDOWS)にまとまっています。中身を書き換えて動きが変わることを確かめましょう。複数の定数が対応関係にある場合は、セットで整合させる必要がある点に注意してください。

💡 設定ダイアログ

フォントサイズや色などの設定をユーザーが変更できるオプションダイアログを追加しましょう。

8. よくある問題と解決法

❌「データが取得できませんでした」と表示される

原因:history() は、存在しないティッカーや対象期間にデータが無い銘柄に対して例外ではなく空の DataFrame を返します。検証環境で ZZZZZZ.XX を指定したときも、例外ではなく空データが返りました。多いのは日本株で .T を付け忘れているケースです。

解決法:日本株は 7203.T のように証券コード+.T、米国株は AAPL のようにそのまま入力してください。このアプリは空データを検知すると「データなし」ダイアログを出して待機状態に戻ります(アプリは終了しません)。

❌ 「取得エラー」が出る・取得に時間がかかる

原因:yfinance は Yahoo! Finance へインターネット経由で問い合わせます。オフライン・社内プロキシ・ファイアウォールで通信できないと、名前解決エラー(getaddrinfo failed など)になります。

解決法:ネットワーク接続を確認してください。_load_data()except Exception で例外を受け止め、例外名とメッセージをダイアログとステータスバーに表示してから待機状態に戻すので、通信できない環境でもアプリは落ちません(起動だけならオフラインでも可能です)。

❌ 以前は動いたのにデータが取れなくなった

原因:yfinance は Yahoo! Finance の非公式ライブラリです。取得元の仕様変更でライブラリ側の対応が必要になることがあり、データの精度・継続性は保証されません。

解決法:pip install -U yfinance で更新してから再実行してください。それでも直らない場合は、ライブラリ側の対応を待つことになります。

❌ グラフの日本語が □(豆腐)になる

原因:matplotlib の既定フォント(DejaVu Sans)には日本語のグリフが無いためです。

解決法:このコードは冒頭で font_manager.fontManager.ttflist を調べ、Meiryo・Yu Gothic(Windows)や Noto Sans CJK JP(Linux に多い)などが入っていれば matplotlib.rcParams["font.family"] に設定します。候補がどれも入っていない環境では豆腐のままになるので、その場合はリストに自分の環境の和文フォント名を足してください。

❌ 入力例のボタンがどれを押しても同じ銘柄になる

原因:ループでボタンを作るとき、このコードは command=lambda s=sym: (self.symbol_var.set(s), self._fetch()) のように既定引数でループ変数を固定しています。これを省略して lambda: (self.symbol_var.set(sym), self._fetch()) と書くと、lambda の中身はボタンを押した時点で評価されるため、ループ終了後の最後の値(MSFT)が全ボタンで共有されてしまいます(Python のクロージャの有名な罠です)。

解決法:掲載コードのとおり、lambda の既定引数でその時点の値を渡してください。直したかどうかは、7203.T のボタンを押してティッカー欄が 7203.T になるかで確認できます。

❌ 文字のフォントが崩れる・見た目が違う

原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。

解決法:動作には支障ありません。気になる場合は font 引数の名前を自分の OS に入っているフォントへ書き換えるか、font 引数を省略してください。

<Return> キーを押しても反応しない

原因:bind("<Return>", ...) はウィジェット単位で登録されるため、ウィンドウ(またはバインド先)にキーボードフォーカスが無いとイベントが届きません。

解決法:一度ウィンドウ内をクリックしてフォーカスを与えてから、キーを押してください。

❌ ウィンドウの大きさが画面に合わない

原因:geometry("1071x782") の固定サイズで起動するためです。

解決法:geometry()minsize() の両方の数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。ただし minsize(1071, 782) を指定しているため、起動時の大きさより小さくは縮められません(縮めると画面部品が表示されなくなるのを防ぐためです)。

9. 練習問題

アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。課題を試すには yfinance と matplotlib が要ります(pip install yfinance matplotlib)。どちらも取得済みのデータの見せ方を直す課題なので、通信は最初の1回で足ります。表示するのは取得した数値そのままで、売買の判断には使いません。

  1. 課題1:チャートの種類を変えたら通信せずに描き直す

    「チャート:」を「折れ線」に変えても、もう一度「▶ 取得・表示」を押すまで描き直されません。「移動平均線(5/25)」のチェックと同じように、その場で描き直すようにしてください。

    期待結果:1度取得したあとに「ローソク足」から「折れ線」へ変えると、通信せずに折れ線へ変わる(下の帯の 取得: 12:34:56 の時刻はそのまま)。

    合格条件

    • 1度も取得していないときに切り替えても、何も描かれずエラーにもならない
    • 「移動平均線(5/25)」のチェックは、これまでどおり効く
    • 切り替えたあとに「▶ 取得・表示」を押すと、通信して帯の 取得: の時刻が新しくなる

    ヒント①(どこを触るか): 触るのは _build_ui() の1か所です。self.chart_var をつないでいる ttk.Combobox(...) を変数で受け、bind("<<ComboboxSelected>>", lambda e: self._redraw()) を足します。
    ヒント②(使うもの): _redraw() は取得済みの self._hist があるときだけ _draw_chart() を呼ぶ作りで、「移動平均線(5/25)」の command=self._redraw と同じ入口です。通信するのは _fetch() から始まる流れだけなので、描き直しで通信は起きません。
    つまずきやすい点: ttk.Combobox(...).pack(...) と1行で書くと pack() の戻り値の None になるため、変数へ受けてから bind します。ttk.Combobox には command= が無いので、チェックボックスと同じ書き方は使えません。「期間:」の欄は取り直しが要るぶん通信が増えるので、同じ扱いにはしないでください。

  2. 課題2:描き直しのタイトルが入力欄につられないようにする

    _redraw() はグラフのタイトルに入力欄の文字を使います。取得せずにティッカーだけ打ち替えて描き直すと、古いデータのまま新しい銘柄名が出てしまいます。表示中の銘柄を覚えて使ってください。

    期待結果7203.T を取得したあとに入力欄だけ AAPL へ打ち替え、取得は押さずに移動平均のチェックを外すと、タイトルが 7203.T 株価チャート(Yahoo! Finance / yfinance) のまま変わらない。

    合格条件

    • 「▶ 取得・表示」で AAPL を取得すると、タイトルも表も帯も AAPL になる
    • 取得に失敗したときは、前に表示していたチャートとタイトルがそのまま残る
    • 1度も取得していないときにチェックを操作しても、これまでどおり何も描かれない

    ヒント①(どこを触るか): 触るのは2か所です。_on_loaded()self._hist = hist の隣に、self._symbol = symbol を足します。あとは _redraw()_draw_chart() へ渡している self.symbol_var.get().strip().upper() を、その変数へ差し替えます。
    ヒント②(使うもの): __init__self._hist = None と並べて self._symbol = None を用意しておくと、1度も取得していないときも安全です。_on_loaded() が受け取る symbol_fetch().strip().upper() 済みなので、そのまま使えます。
    つまずきやすい点: 表示している数値とタイトルの出どころが違うと、画面の銘柄名だけが先に変わってしまいます。株価を扱う画面では、この食い違いが読み違いのもとになります。_on_error() はチャートを描き直さない作りなので、取得に失敗しても前の表示が残る点はそのまま残してください。

  3. 課題3:保存機能の追加

    処理結果をファイルに保存する機能を追加しましょう。

🚀
次に挑戦するアプリ

このアプリをマスターしたら、次のアプリに挑戦しましょう。

🐛
エラーが出て動かないときは

写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。

📖
次のレベルへ進む参考書

このレベルのアプリが作れたら、入門書の次の「作るための本」へ進む時期です。実践におすすめのPython本(当サイトの参考書ランキング総合3〜4位)で自動化とコードの書き方の2冊を、その直後の用途別専門書の節でデータ分析・Web・AIの本を比較しています。