Pythonで株価チャートを描画する(Matplotlib・ローソク足)
Python経験者向けの投資分析シリーズ第5回。matplotlib だけでローソク足・移動平均線・出来高を1枚にまとめたチャートを組み立てます。専用ライブラリに頼らず実体とヒゲを自分で描くので、チャートの中身がそのまま分かるようになります。
1. この記事のゴールと連載の位置づけ
本記事は「Python経験者が、自分の投資ポートフォリオを管理・可視化するアプリを作る」連載の第5回です。第4回のPythonで保有銘柄の損益を計算する(pandas)で、取引履歴から損益一覧の DataFrame を返す部品まで作りました。ここまでで扱ってきたのは、すべて数字です。
ただ、数字だけを見ていると分からないことがあります。「その価格になるまでに、どういう動き方をしたのか」です。同じ「終値3,258円」でも、じりじり上げてきた3,258円と、一度大きく下げてから戻した3,258円では、まったく違う経緯をたどっています。そして値動きの経緯は、表を眺めるより1枚のチャートにしたほうが圧倒的に速く読み取れます。連載はここから可視化の部(第5〜7回)に入ります。
今回使うのは matplotlib だけです。ローソク足を描く専用ライブラリを入れれば数行で済むところを、あえて実体(箱)とヒゲ(線)を自分で描きます。遠回りに見えますが、自作するとローソク足が「4本の値をどう配置した図形なのか」が手に触れる形で分かり、あとから配色を変える・線を足す・別のサブプロットを増やすといった改造が全部自分でできるようになります。連載後半(第13回)で Streamlit ダッシュボードに組み込むときに効いてくるのは、この「自分で描ける」状態のほうです。
OHLCV(始値・高値・安値・終値・出来高)の DataFrame を渡すと、ローソク足+移動平均線+出来高が2段に並んだチャートを返す関数を書けるようになること。第1回で取得した株価データをそのまま渡せる形にします。
完成コードだけ先に見たい方は 9章「完成コード(関数にまとめる)」へ。そのままファイルに保存して動く形で載せています(そこまでの本文は、その1行1行が何を描いているのかを確かめる道のりです)。
本文のコードと図は、Windows 11 / Python 3.12.10 / matplotlib 3.11.1 / pandas 3.0.3 / NumPy 2.4.6 の環境で 2026年8月26日に実際に実行して確認したものです。掲載している出力と画像は、上から順にコードを実行したときのそのままの結果です。登場する銘柄・価格・出来高はすべて架空で、実在の銘柄や相場を示すものではありません。
2. サンプルのOHLCVデータを用意する
チャートを描く前に、描く材料が要ります。株価チャートの材料はOHLCV——始値(Open)・高値(High)・安値(Low)・終値(Close)・出来高(Volume)の5つです。この5つが日付ごとに並んだ表があれば、ローソク足も移動平均線も出来高も全部描けます。
| 列 | 意味 | チャートのどこになるか |
|---|---|---|
Open | その日の最初に付いた値段 | 実体(箱)の上端または下端 |
High | その日の最高値 | 上ヒゲの先端 |
Low | その日の最安値 | 下ヒゲの先端 |
Close | その日の最後に付いた値段 | 実体のもう一方の端・移動平均線のもと |
Volume | その日に売買が成立した株数 | 下段の棒グラフ |
本記事では、この5列を固定シードの乱数から生成した架空データで用意します。実在の株価を使わない理由は3つあります。①実在銘柄の値動きを載せると、意図せず特定銘柄への言及になってしまう ②ネットワークがなくても手元で試せる ③同じコードを実行すれば誰でも同じ図が出るので、記事の図と自分の画面を見比べられる——とくに③は、学習用の記事では出力が一致することが確認の助けになります。
銘柄名は第4回と同じ架空の ALPHA を使います。日付は2026年4〜6月の平日から、市場が休みだった祝日(4月29日、5月4日・5日・6日)を除いた61営業日です。この「祝日を抜いた日付」が、5章のつまずきで効いてきます。
import numpy as np
import pandas as pd
def make_sample_ohlcv(seed: int = 294) -> pd.DataFrame:
"""架空銘柄ALPHAの日足OHLCVを固定シードで生成する(実在の銘柄・相場とは無関係)。"""
# 2026年4〜6月の平日から、市場が休みの祝日を除く
holidays = pd.to_datetime(["2026-04-29", "2026-05-04", "2026-05-05", "2026-05-06"])
dates = pd.bdate_range("2026-04-01", "2026-06-30").difference(holidays)
n = len(dates)
rng = np.random.default_rng(seed) # 固定シード=毎回同じ値
ret = rng.normal(0.002, 0.016, n) # 日次リターン(ランダムウォーク)
close = 2900 * np.exp(np.cumsum(ret)) # 終値
open_ = np.r_[2900.0, close[:-1]] * (1 + rng.normal(0, 0.004, n)) # 始値=前日終値±α
high = np.maximum(open_, close) * (1 + rng.uniform(0.001, 0.010, n))
low = np.minimum(open_, close) * (1 - rng.uniform(0.001, 0.010, n))
volume = rng.lognormal(np.log(1_200_000), 0.30, n) * (1 + 5 * np.abs(ret))
df = pd.DataFrame({"Open": open_, "High": high, "Low": low, "Close": close,
"Volume": volume},
index=pd.DatetimeIndex(dates, name="Date"))
df[["Open", "High", "Low", "Close"]] = df[["Open", "High", "Low", "Close"]].round(0)
df["Volume"] = df["Volume"].round(-2).astype("int64")
return df
df = make_sample_ohlcv()
print(df.head())
print()
print(f"行数: {len(df)} 期間: {df.index[0]:%Y-%m-%d} 〜 {df.index[-1]:%Y-%m-%d}")
Open High Low Close Volume
Date
2026-04-01 2888.0 2942.0 2882.0 2925.0 1183300
2026-04-02 2915.0 2924.0 2885.0 2912.0 923000
2026-04-03 2909.0 2932.0 2881.0 2909.0 1244400
2026-04-06 2910.0 2961.0 2900.0 2933.0 951800
2026-04-07 2932.0 3002.0 2910.0 2993.0 1520100
行数: 61 期間: 2026-04-01 〜 2026-06-30
列名を Open / High / Low / Close / Volume と大文字始まりにしているのは意図的です。第1回の yfinanceで株価データを取得するで history() が返す列名がこの形だからです。同じ列名にしておけば、この記事で書く描画コードは架空データでも実データでも、そのまま動きます。実データに切り替えるときは make_sample_ohlcv() の呼び出しを yf.Ticker("...").history(period="3mo") に差し替えるだけです。
描く前に、データの整合を1行で確かめる
OHLCには「高値は始値・終値より低くならない」「安値は始値・終値より高くならない」という当たり前の制約があります。ここが崩れたデータを描くと、ヒゲが実体を突き抜けた不気味な図になります。乱数で作ったデータでも、丸め処理の後にいちおう確認しておきます。
ng = df[(df["High"] < df[["Open", "Close"]].max(axis=1))
| (df["Low"] > df[["Open", "Close"]].min(axis=1))]
print(f"High/Lowの矛盾がある行: {len(ng)}")
print(f"陽線: {(df['Close'] >= df['Open']).sum()} 本 / 陰線: {(df['Close'] < df['Open']).sum()} 本")
High/Lowの矛盾がある行: 0
陽線: 33 本 / 陰線: 28 本
矛盾は0行、上げ下げもほぼ半々でした。この確認は実データを使うときこそ効きます。取得元やスクレイピング結果で High と Low を取り違えていると、ここで引っかかるからです。ただしこの検査は万能ではありません。Open と Close の取り違えは max/min の値が変わらないため気づけないので、列名の並び順も合わせて確認してください。
np.random.default_rng(294) はシードを固定しているので、同じ環境で何度実行しても同じ表・同じ図になります。ただし NumPy のバージョンが上がると乱数の生成アルゴリズムが変わる可能性があり、その場合は数値が本記事と一致しません(本記事は NumPy 2.4.6 で実行しています)。数値が違っても、この先の描画コードの動き方は変わりませんので、そのまま読み進めて大丈夫です。
3. まずは終値の折れ線から始める
いきなりローソク足に行かず、まず終値だけの折れ線を描きます。理由は2つあります。Matplotlibで図を作って保存するまでの型を先に押さえること、そして折れ線では何が分からないのかを体感してからローソク足に進むほうが、ローソク足の意味が入ってくることです。
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(10, 4.2))
ax.plot(df.index, df["Close"], color="#2563eb", linewidth=1.6)
ax.set_title("ALPHA (fictional) - Daily Close")
ax.set_ylabel("Price (JPY)")
ax.grid(True, alpha=0.3)
fig.autofmt_xdate() # 日付ラベルを斜めにして重なりを防ぐ
fig.savefig("close_line.png", dpi=150, bbox_inches="tight")
装飾を除けば subplots・plot・savefig の実質3行で図になりました。ここで押さえておきたいのは fig, ax = plt.subplots() の書き方です。plt.plot() だけでも描けますが、「図(Figure)」と「1つの描画領域(Axes)」を変数として受け取っておくと、後でサブプロットを増やすときにコードの形が変わりません。8章で出来高の段を足すとき、この書き方の効き目が出ます。
保存の savefig() には2つ引数を付けています。dpi=150 は解像度(既定は100で、ブログに載せると少しぼやけます)、bbox_inches="tight" は余白の自動トリミングです。斜めにした日付ラベルは図の枠からはみ出しがちですが、これを付けておくと切れずに収まります。画面に出したいだけなら fig.savefig(...) の代わりに plt.show() を使ってください。
さて、この折れ線から読み取れるのは「終値がどう動いたか」だけです。ある日に大きく下げてから戻して終えたのか、静かに終値まで到達したのかは、この線からは分かりません。1日の中の値動きの幅を1本の図形で表そうというのが、次のローソク足です。
4. ローソク足の構造と、Matplotlibでの自作
ローソク足は1日ぶんのOHLC(4本値)を1つの図形に押し込んだ表現です。部品は2つだけです。
- 実体(じったい): 始値と終値ではさまれた長方形。1日の「行って戻ってきた末の差」を表す
- ヒゲ: 安値から高値まで引いた縦線。実体からはみ出した部分が、その日に一時的に付けた値段
そして終値が始値より高い(上げて終えた)足を陽線、低い足を陰線と呼び、色を変えて描きます。
bar、ヒゲは vlines で描ける。陽線と陰線の違いは、実体の上下と色だけ。構造が分かると、Matplotlibで描く方法もそのまま決まります。ヒゲは ax.vlines()(安値から高値までの縦線)、実体は ax.bar()(下端 bottom から高さ height ぶんの棒)です。bar() は「0から積み上げる棒グラフ」だと思われがちですが、bottom を指定すれば宙に浮いた箱を描けます。この一点さえ知っていれば、ローソク足は数行で描けます。
UP, DOWN = "#e11d48", "#2563eb" # 陽線=赤・陰線=青(日本式)
d = df.tail(30)
colors = np.where(d["Close"] >= d["Open"], UP, DOWN)
fig, ax = plt.subplots(figsize=(10, 4.6))
# ヒゲ:安値から高値までの縦線
ax.vlines(d.index, d["Low"], d["High"], color=colors, linewidth=1)
# 実体:始値と終値ではさまれた長方形(下端 bottom から高さ height ぶん)
ax.bar(d.index, height=(d["Close"] - d["Open"]).abs(),
bottom=d[["Open", "Close"]].min(axis=1),
width=0.6, color=colors)
ax.set_title("ALPHA (fictional) - Candlestick, last 30 sessions")
ax.set_ylabel("Price (JPY)")
ax.grid(True, alpha=0.3)
fig.autofmt_xdate()
fig.savefig("candle_basic.png", dpi=150, bbox_inches="tight")
ポイントは3か所です。
- 色は
np.where()で1本ずつ決める。vlines()もbar()も、colorに色の配列を渡せば1本ごとに違う色で描いてくれます。陽線と陰線でループを分ける必要はありません - 実体の高さは
(Close - Open).abs()、下端はOpenとCloseの小さいほう。この2つで、陽線・陰線どちらも同じ式で描けます width=0.6の単位は「日」。x軸に日付を渡しているとき、Matplotlibは内部で日付を数値(1日=1.0)に変換して扱うため、0.6は「0.6日ぶんの幅」という意味になります
本記事は陽線=赤・陰線=青という日本の証券会社でよく見る配色にしました。一方、欧米のチャートは陽線=緑・陰線=赤が主流です。つまり「赤いローソク足」が意味するものが、日本と欧米で正反対になります。自作するなら定数 UP / DOWN を1か所にまとめておき、見る人に合わせて切り替えられるようにしておくと安全です。色覚特性への配慮まで考えるなら、色だけでなく実体の塗りつぶし/中抜きで区別する方法もあります。
ローソク足には mplfinance という専用ライブラリがあります。matplotlib 組織がメンテナンスしており、pip名・import名ともに mplfinance、PyPIのページで確認したところ2026年8月26日時点の最新は 0.12.10b0(2023年8月2日公開)でした。バージョン番号のとおりベータ扱いが続いています。
数行でローソク足が描けるのは大きな利点なので、「とにかく早くチャートを出したい」なら選択肢に入ります。それでも本記事が自作を選ぶのは、この連載のゴールが「自分のポートフォリオに合わせて自由に作り替えられるアプリ」だからです。ライブラリが用意していない見せ方をしたくなったとき、bar と vlines で描いてあれば、その場で足せます。なお本記事のコードは mplfinance を使わないので、インストールは不要です。
5. つまずき実演:ローソク足チャートに土日・祝日の空白が入る
前章の図をもう一度見てください。5本ごとに、うっすら空白が入っています。これは描画の失敗ではなく、x軸に日付をそのまま渡した当然の結果です。Matplotlibの日付軸は「時間が連続して流れる軸」なので、土曜と日曜の位置にもきちんと場所を確保します。そこに株価データが無いから、空白になる——それだけのことです。
ふだんは気にならない程度ですが、連休をまたぐと一気に目立ちます。2026年のゴールデンウィーク付近(4月29日、5月4日・5日・6日が休み)を拡大してみましょう。
# ⚠️ 失敗例:日付をそのままx軸に使うと、取引がない土日・祝日ぶんの空白が残る
z = df.loc["2026-04-20":"2026-05-15"]
colors = np.where(z["Close"] >= z["Open"], UP, DOWN)
fig, ax = plt.subplots(figsize=(10, 4.4))
ax.vlines(z.index, z["Low"], z["High"], color=colors, linewidth=1.2)
ax.bar(z.index, height=(z["Close"] - z["Open"]).abs(),
bottom=z[["Open", "Close"]].min(axis=1), width=0.6, color=colors)
ax.axvspan(pd.Timestamp("2026-05-02"), pd.Timestamp("2026-05-07"),
color="#94a3b8", alpha=0.25) # 休場だった期間を塗る
ax.text(pd.Timestamp("2026-05-04 12:00"), z["High"].max(), "market closed",
ha="center", va="top", fontsize=9, color="#334155")
ax.set_title("Date axis: weekends and holidays leave blank space")
ax.set_ylabel("Price (JPY)")
ax.grid(True, alpha=0.3)
fig.autofmt_xdate()
fig.savefig("candle_gap.png", dpi=150, bbox_inches="tight")
ローソク足が飛び飛びに並び、灰色に塗った連休のところは完全に空白です。ぱっと見で「データが欠けているチャート」に見えてしまうのが問題で、実際には欠けていません。市場が開いていなかっただけです。証券会社のチャートアプリでこの空白を見かけないのは、取引があった足だけを詰めて並べているためです。
解き方:連番をx座標にして、ラベルだけ日付にする
やることは単純です。x座標には「何本目か」という連番(0, 1, 2, …)を使い、目盛りのラベルだけ日付の文字列に差し替えます。取引のあった日だけが等間隔に並ぶので、空白は生まれません。
x = np.arange(len(z)) # 0,1,2,... の連番をx座標にする
colors = np.where(z["Close"] >= z["Open"], UP, DOWN)
fig, ax = plt.subplots(figsize=(10, 4.4))
ax.vlines(x, z["Low"], z["High"], color=colors, linewidth=1.2)
ax.bar(x, height=(z["Close"] - z["Open"]).abs(),
bottom=z[["Open", "Close"]].min(axis=1), width=0.6, color=colors)
ax.set_xticks(x[::2]) # 2本おきに目盛りを立て
ax.set_xticklabels(z.index[::2].strftime("%m/%d"), # ラベルだけ日付にする
rotation=45, ha="right")
ax.set_title("Index axis: sessions are evenly spaced")
ax.set_ylabel("Price (JPY)")
ax.grid(True, alpha=0.3)
fig.savefig("candle_index.png", dpi=150, bbox_inches="tight")
変えたのは、x座標に渡していた z.index を x に置き換えたことと、目盛りを手で立てたことだけ。それだけで、連休の帯も土日の隙間も消えました。ax.set_xticks() で目盛りの位置(何本目か)を決め、ax.set_xticklabels() でそこに貼る文字を決める、という2段構えになっています。[::2] で2本おきに間引いているのは、全部にラベルを付けると重なって読めなくなるからです。本数が増えたら間引く間隔を広げる——8章の完成形では61本を5本おきにしています。
この方法にはトレードオフがあります。x軸が「時間」ではなく「本数」になるため、①目盛りの日付の間隔が不揃いになる(上の図でも 05/01 の次が 05/08 に飛んでいます)②matplotlib.dates の日付フォーマッタや axvspan(日付, 日付) のような日付を直接指定する機能が使えなくなる——という2点です。休場を含む日足チャートでは連番軸が実用的ですが、「時間の流れの正しさ」が主題のグラフ(資産推移など)では日付軸のままがふさわしい場面もあります。目的で使い分けてください。
6. 移動平均線を描画して重ねる(pandasのrolling)
ローソク足だけだと、1本1本の情報量が多すぎて全体の流れが追いにくいことがあります。そこで重ねるのが移動平均線(SMA: Simple Moving Average)です。直近N日の終値を平均して線にしたもので、日々のぶれをならして流れを見やすくするための平滑化です。pandasなら rolling() 一発で計算できます。
df["SMA5"] = df["Close"].rolling(5).mean()
df["SMA25"] = df["Close"].rolling(25).mean()
print(df[["Close", "SMA5", "SMA25"]].head(6).round(1))
print()
print(df[["Close", "SMA5", "SMA25"]].tail(3).round(1))
Close SMA5 SMA25
Date
2026-04-01 2925.0 NaN NaN
2026-04-02 2912.0 NaN NaN
2026-04-03 2909.0 NaN NaN
2026-04-06 2933.0 NaN NaN
2026-04-07 2993.0 2934.4 NaN
2026-04-08 2948.0 2939.0 NaN
Close SMA5 SMA25
Date
2026-06-26 3306.0 3317.2 3186.7
2026-06-29 3308.0 3322.0 3194.1
2026-06-30 3258.0 3306.4 3199.2
最初の4行が NaN になっていることに注目してください。5日移動平均は5日ぶんの終値が揃って初めて計算できるので、5行目からしか値が出ません。25日移動平均なら最初の24行が NaN です。これはバグではなく仕様で、グラフに描くと線の描き始めが右にずれるという形で現れます(NaN の区間は描画されません)。どうしても最初から線を引きたい場合は rolling(25, min_periods=1) のように指定できますが、「25日ぶん揃っていない平均」を25日線として見せることになるので、私は既定のままにしておくほうが誠実だと考えています。
あとは折れ線として重ねるだけです。x座標は前章の連番をそのまま使います。
x = np.arange(len(df))
colors = np.where(df["Close"] >= df["Open"], UP, DOWN)
fig, ax = plt.subplots(figsize=(11, 5.0))
ax.vlines(x, df["Low"], df["High"], color=colors, linewidth=1)
ax.bar(x, height=(df["Close"] - df["Open"]).abs(),
bottom=df[["Open", "Close"]].min(axis=1), width=0.6, color=colors)
ax.plot(x, df["SMA5"], color="#f59e0b", linewidth=1.4, label="SMA 5")
ax.plot(x, df["SMA25"], color="#7c3aed", linewidth=1.4, label="SMA 25")
ax.set_xticks(x[::5])
ax.set_xticklabels(df.index[::5].strftime("%m/%d"), rotation=45, ha="right")
ax.set_title("ALPHA (fictional) - Candlestick with moving averages")
ax.set_ylabel("Price (JPY)")
ax.legend(loc="upper left")
ax.grid(True, alpha=0.3)
fig.savefig("candle_sma.png", dpi=150, bbox_inches="tight")
ローソク足の上に2本の線が乗りました。紫の25日線が図の左端から描かれていないのが、さきほどの NaN の正体です。label= を付けて ax.legend() を呼ぶと凡例が出るので、線が増えても何の線か分からなくなりません。
移動平均線には、クロスや傾きを売買の判断材料とする使い方が広く知られています。ただし本記事はあくまで描画の技術記事であり、チャートの形から相場を予測する方法や、売買のタイミングの判断は扱いません(当サイトは投資助言業の登録を行っていません)。ここで作っているのは、自分のデータを自分の目で確かめるための道具です。
7. Matplotlibのグラフで日本語が文字化けして□(豆腐)になるときの対処
ここまで、タイトルも軸ラベルもすべて英語にしてきました。理由があります。Matplotlibの既定フォント(DejaVu Sans)は日本語のグリフを持っていないからです。何も設定せずに日本語を入れると、そのまま文字化けします。試しに日本語のタイトルを付けてみます。
# ⚠️ 失敗例:既定フォント(DejaVu Sans)のままなので、日本語ラベルが□(豆腐)に文字化けする
# ⚠️ 対処は次の「手順2」— plt.rcParams["font.family"] = "Meiryo" を参照
fig, ax = plt.subplots(figsize=(6, 3))
ax.plot(np.arange(len(df)), df["Close"], color="#2563eb")
ax.set_title("株価チャート")
ax.set_ylabel("終値(円)")
fig.savefig("jp_default.png", dpi=100, bbox_inches="tight")
エラーにはなりませんが、次のような警告が出ます(実際のターミナルではファイル名と行番号が頭に付きます。以下は検証時に出た22件のうち、先頭6件の本文だけを抜き出したものです)。
UserWarning: Glyph 32066 (\N{CJK UNIFIED IDEOGRAPH-7D42}) missing from font(s) DejaVu Sans.
UserWarning: Glyph 20516 (\N{CJK UNIFIED IDEOGRAPH-5024}) missing from font(s) DejaVu Sans.
UserWarning: Glyph 65288 (\N{FULLWIDTH LEFT PARENTHESIS}) missing from font(s) DejaVu Sans.
UserWarning: Glyph 20870 (\N{CJK UNIFIED IDEOGRAPH-5186}) missing from font(s) DejaVu Sans.
UserWarning: Glyph 65289 (\N{FULLWIDTH RIGHT PARENTHESIS}) missing from font(s) DejaVu Sans.
UserWarning: Glyph 26666 (\N{CJK UNIFIED IDEOGRAPH-682A}) missing from font(s) DejaVu Sans.
そして出来上がったPNGでは、日本語の文字がすべて □(中に文字コードが小さく入った四角)に置き換わります。これが Matplotlib のつまずきとして定番の「日本語の文字化け」で、□は「豆腐」と呼ばれています。エラーで止まらないぶん、原因に気づきにくいのが厄介なところです。ちなみにこの検証では警告が22件出ました。使った日本語は11文字ぶんなのに倍あるのは、bbox_inches="tight" が余白を測るために一度描画し、保存でもう一度描画するからです(bbox_inches を外して実行したところ、11件になりました)。原因が分かっていれば驚かずに済みます。
手順1:手元にある日本語フォント名を調べる
対処は「日本語を持っているフォントを指定する」だけですが、使えるフォント名はOSと環境によって違います。ネットで見かけた名前をそのまま書いても、その環境に無ければ効きません。まず自分の環境を調べます。
from matplotlib import font_manager
names = sorted({f.name for f in font_manager.fontManager.ttflist})
print([n for n in names if any(k in n for k in ("Meiryo", "Gothic", "Hiragino", "Noto Sans CJK"))])
['BIZ UDGothic', 'BIZ UDPGothic', 'Franklin Gothic Medium', 'MS Gothic', 'MS PGothic', 'MS UI Gothic', 'Malgun Gothic', 'Malgun Gothic Semilight', 'Meiryo', 'Meiryo UI', 'Yu Gothic', 'Yu Gothic Light', 'Yu Gothic Medium', 'Yu Gothic UI', 'Yu Gothic UI Light', 'Yu Gothic UI Semibold', 'Yu Gothic UI Semilight']
この検証環境(Windows 11)では Meiryo・Yu Gothic・MS Gothic などが見つかりました。ここに出てきた名前をそのまま指定するのが確実です。macOSやLinuxでは並ぶ名前が変わるので、同じコードを自分の環境で実行して確かめてください(一覧が空なら、日本語フォント自体を入れる必要があります)。
手順2:font.family に日本語フォント名を指定して文字化けを直す
with plt.rc_context({"font.family": "Meiryo"}): # Windows。以降のコードには影響させない
fig, ax = plt.subplots(figsize=(6, 3))
ax.plot(np.arange(len(df)), df["Close"], color="#2563eb")
ax.set_title("株価チャート")
ax.set_ylabel("終値(円)")
fig.savefig("jp_meiryo.png", dpi=100, bbox_inches="tight")
print("警告なしで保存できました")
警告なしで保存できました
警告は0件になり、日本語も正しく表示されました。スクリプト全体を日本語表示にしたいなら、plt.rcParams["font.family"] = "Meiryo" をimportの直後に1行書くだけです。ここで plt.rc_context() を使ったのは、この記事の以降のコードに設定を持ち越さないためです(with を抜けると元に戻ります)。
なお本記事のチャートを英語ラベルのままにしているのは、読者がどの環境で実行しても同じ図が出るようにするためです。自分のためのツールなら、日本語ラベルのほうが読みやすいでしょう。
8. 出来高をサブプロットに足して完成形にする
最後のピースが出来高です。出来高は株数(数百万といった大きな数字)で、価格(数千円)とは桁も単位もまったく違います。同じ座標軸に描くと、片方が潰れて読めなくなります。そこで、上下2段に分けて、x軸だけを共有するというのが定番の作りです。
これを実現するのが plt.subplots() の3つの指定です。
2, 1: 2行1列に分割するsharex=True: 2つの段でx軸を共有する(拡大しても縦のズレが起きない)gridspec_kw={"height_ratios": [3, 1]}: 上段と下段の高さの比を3:1にする。hspaceで段の隙間も詰められる
from matplotlib.ticker import FuncFormatter
x = np.arange(len(df))
colors = np.where(df["Close"] >= df["Open"], UP, DOWN)
fig, (ax1, ax2) = plt.subplots(
2, 1, figsize=(11, 6.4), sharex=True,
gridspec_kw={"height_ratios": [3, 1], "hspace": 0.06},
)
# 上段:ローソク足+移動平均
ax1.vlines(x, df["Low"], df["High"], color=colors, linewidth=1)
ax1.bar(x, height=(df["Close"] - df["Open"]).abs(),
bottom=df[["Open", "Close"]].min(axis=1), width=0.6, color=colors)
ax1.plot(x, df["SMA5"], color="#f59e0b", linewidth=1.4, label="SMA 5")
ax1.plot(x, df["SMA25"], color="#7c3aed", linewidth=1.4, label="SMA 25")
ax1.set_ylabel("Price (JPY)")
ax1.yaxis.set_major_formatter(FuncFormatter(lambda v, _: f"{v:,.0f}"))
ax1.legend(loc="upper left", framealpha=0.9)
ax1.grid(True, alpha=0.3)
ax1.set_title("ALPHA (fictional) - 2026-04-01 to 2026-06-30")
# 下段:出来高
ax2.bar(x, df["Volume"], width=0.6, color=colors)
ax2.set_ylabel("Volume")
ax2.yaxis.set_major_formatter(FuncFormatter(lambda v, _: f"{v / 1e6:.1f}M"))
ax2.grid(True, alpha=0.3)
# 日付ラベルは共有x軸の下段だけに付ける
ax2.set_xticks(x[::5])
ax2.set_xticklabels(df.index[::5].strftime("%m/%d"), rotation=45, ha="right")
ax2.set_xlim(-1, len(df))
fig.savefig("stock_chart.png", dpi=150, bbox_inches="tight")
証券会社のアプリで見慣れた形に近づきました。仕上げとして入れた工夫が2つあります。
1つめは FuncFormatter による目盛りの整形です。何も指定しないと、検証環境では出来高の目盛りが 0 / 1 / 2 / 3 と表示され、軸の左上に小さく 1e6 と出るだけでした(実際の値はその百万倍)。桁を頭の中で戻しながら読むことになります。lambda v, _: f"{v / 1e6:.1f}M" を渡すと 2.0M のように百万株単位で目盛りそのものに書かれ、幅も取りません。同じ仕組みで価格側には桁区切りのカンマを入れています。目盛りの読みやすさは、チャートを毎日見るかどうかを左右します。
2つめは 出来高の棒を、その日の陽線・陰線と同じ色で塗っていることです(colors を使い回しています)。上段と下段を目で行き来しなくても、「大きく動いた日に売買も膨らんでいたか」が同じ色で追えます——ただし、その関係をどう読むか(出来高を伴う上昇か否か等)の解釈は本記事の範囲外です。
完成形をよく見ると、実体が描かれていない足が1本ある
図の左端から3本目、2026年4月3日の足を拡大すると、ヒゲの縦線だけで箱がありません。データを見ると Open も Close も 2,909円。始値と終値が同じ日(同事・どうじ)は実体の高さが0になり、ax.bar() は高さ0の棒として何も描かないのです。
実際の株価でも、値動きの小さい銘柄では珍しくありません。放っておくと「その日だけデータが無いように見える」ので、次章の完成コードでは高さが0のときだけ、ごく小さい高さを与えて横線として描くようにします。この手の細部は、実データを流し込んで初めて気づくことが多いので、先に潰しておくと後が楽です。
9. 完成コード(関数にまとめる)
ここまでのコードを、そのままファイルに保存して使える形にまとめます。OHLCVの DataFrame を渡すと Figure が返る関数にしておくと、あとでファイル保存にも画面表示にも、Streamlitへの埋め込みにも回せます。
下のコードをまるごと chart.py という名前で保存し、python chart.py を実行すると、同じフォルダに stock_chart_final.png が出ます。必要なライブラリは pip install matplotlib pandas だけです(NumPy は pandas と一緒に入ります。mplfinance は使いません)。
"""架空のOHLCVデータから、ローソク足+移動平均+出来高のチャートを描く。"""
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from matplotlib.ticker import FuncFormatter
UP, DOWN = "#e11d48", "#2563eb" # 陽線=赤・陰線=青(日本式)
def make_sample_ohlcv(seed: int = 294) -> pd.DataFrame:
"""架空銘柄ALPHAの日足OHLCVを固定シードで生成する(実在の銘柄・相場とは無関係)。"""
holidays = pd.to_datetime(["2026-04-29", "2026-05-04", "2026-05-05", "2026-05-06"])
dates = pd.bdate_range("2026-04-01", "2026-06-30").difference(holidays)
n = len(dates)
rng = np.random.default_rng(seed)
ret = rng.normal(0.002, 0.016, n)
close = 2900 * np.exp(np.cumsum(ret))
open_ = np.r_[2900.0, close[:-1]] * (1 + rng.normal(0, 0.004, n))
high = np.maximum(open_, close) * (1 + rng.uniform(0.001, 0.010, n))
low = np.minimum(open_, close) * (1 - rng.uniform(0.001, 0.010, n))
volume = rng.lognormal(np.log(1_200_000), 0.30, n) * (1 + 5 * np.abs(ret))
df = pd.DataFrame({"Open": open_, "High": high, "Low": low, "Close": close,
"Volume": volume},
index=pd.DatetimeIndex(dates, name="Date"))
df[["Open", "High", "Low", "Close"]] = df[["Open", "High", "Low", "Close"]].round(0)
df["Volume"] = df["Volume"].round(-2).astype("int64")
return df
def draw_candles(ax, x, df, width=0.6):
"""axにローソク足(ヒゲ+実体)を描く。実体が0になる足は細い線として描く。"""
colors = np.where(df["Close"] >= df["Open"], UP, DOWN)
body = (df["Close"] - df["Open"]).abs()
body = body.where(body > 0, (df["High"].max() - df["Low"].min()) * 0.002) # 同事対策
ax.vlines(x, df["Low"], df["High"], color=colors, linewidth=1)
ax.bar(x, height=body, bottom=df[["Open", "Close"]].min(axis=1),
width=width, color=colors)
return colors
def plot_stock_chart(df, sma=(5, 25), title="", tick_step=5):
"""ローソク足+移動平均+出来高の2段チャートを描いてFigureを返す。"""
x = np.arange(len(df)) # 連番軸=休場日の空白を作らない
fig, (ax1, ax2) = plt.subplots(
2, 1, figsize=(11, 6.4), sharex=True,
gridspec_kw={"height_ratios": [3, 1], "hspace": 0.06},
)
colors = draw_candles(ax1, x, df)
for window, color in zip(sma, ("#f59e0b", "#7c3aed", "#0ea5e9")):
ax1.plot(x, df["Close"].rolling(window).mean(), color=color,
linewidth=1.4, label=f"SMA {window}")
ax1.set_ylabel("Price (JPY)")
ax1.yaxis.set_major_formatter(FuncFormatter(lambda v, _: f"{v:,.0f}"))
ax1.legend(loc="upper left", framealpha=0.9)
ax1.grid(True, alpha=0.3)
ax1.set_title(title)
ax2.bar(x, df["Volume"], width=0.6, color=colors)
ax2.set_ylabel("Volume")
ax2.yaxis.set_major_formatter(FuncFormatter(lambda v, _: f"{v / 1e6:.1f}M"))
ax2.grid(True, alpha=0.3)
ax2.set_xticks(x[::tick_step])
ax2.set_xticklabels(df.index[::tick_step].strftime("%m/%d"), rotation=45, ha="right")
ax2.set_xlim(-1, len(df))
return fig
if __name__ == "__main__":
# 実運用では第1回のyfinanceで取得したDataFrameを渡す(列名はOpen/High/Low/Close/Volume)
data = make_sample_ohlcv()
fig = plot_stock_chart(data, title="ALPHA (fictional) - 2026-04-01 to 2026-06-30")
fig.savefig("stock_chart_final.png", dpi=150, bbox_inches="tight")
print(f"{len(data)}本のローソク足を描画しました: stock_chart_final.png")
61本のローソク足を描画しました: stock_chart_final.png
出力される図は8章のものとほぼ同じです。違いは、同事対策を入れた4月3日の足に細い横線が1本入ったところだけ——2枚のPNGを画素単位で比べたところ、差があったのは12×1ピクセルの範囲だけでした。
関数に切り出すときに考えた設計が3つあります。
| 設計 | そうした理由 |
|---|---|
draw_candles() を分けた | ローソク足を描く処理だけ他のグラフでも使い回せる。同事対策のような細かい修正も1か所で済む |
sma=(5, 25) をタプルで受ける | sma=(5, 25, 75) と渡せば3本目も描ける(色は3色用意してある)。呼び出し側で本数を変えられる |
savefig せず Figure を返す | 保存するか画面に出すかを呼び出し側が決められる。第13回のStreamlitでは st.pyplot(fig) にそのまま渡せる |
実データで使うときは、make_sample_ohlcv() の代わりに第1回で作った取得処理を呼ぶだけです。history() が返す DataFrame には Dividends や Stock Splits の列も付いてきますが、この関数は必要な列を名前で取り出すので、余分な列があっても問題ありません。
10. つまずきやすいポイントと次につなげる
架空データでうまく動いても、実データを流し込むと別の問題が出てきます。よく踏む落とし穴を先に共有しておきます。
| つまずき | 何が起きるか | 対処 |
|---|---|---|
列名が小文字(close) | KeyError: 'Close' で止まる | 取得元によって大小が違う。df.columns を確認して df.rename(columns=str.capitalize) などで揃える |
欠損(NaN)の行が混ざる | その足だけ実体やヒゲが描かれない | df = df.dropna(subset=["Open", "High", "Low", "Close"]) で描く前に落とす |
| 本数が多くて実体が潰れる | 1年ぶん(約240本)だと実体が細くなり、ヒゲと見分けられない | 表示期間を絞るか、figsize の横幅を広げる。width も太くできるが、連番軸では 1.0 で隣の足とくっつく |
| 日付ラベルが重なって読めない | x軸が真っ黒になる | tick_step を増やして間引く(240本なら20本おき程度) |
| ループで大量に描くとメモリを食う | RuntimeWarning(20個超のFigure)が出る | 使い終わった図は plt.close(fig) で閉じる |
とくに列名の大小は、複数のデータ源を混ぜ始めた瞬間に効いてきます。読み込んだ直後に列名を正規化する関数を1つ挟んでおくと、描画側のコードを触らずに済みます。証券会社のCSVを材料にするなら、第3回の証券会社のCSVをpandasで読み込むで扱った文字コードと型変換が先に必要です。エラーが出たときの読み解き方はPythonのよくあるエラーと対処法にまとめてあります。
Matplotlib自体の引数を詳しく調べたいときは、公式の Axes.bar リファレンスが確実です。bottom・width・color に何を渡せるかが一覧になっています。他のライブラリと合わせて全体像をつかみたいときは、当サイトのPythonライブラリ一覧もどうぞ。
今回いちばん引っかかりやすいのは、描画コードそのものよりrolling() の NaN や、実データを入れた途端に出る列名・欠損の食い違い——つまり描く前の段階です。ここは pandas のデータモデルを一度通しで押さえておくと、10章の表を毎回引かなくても自分で対処できるようになります。Pythonによるデータ分析入門 第3版は pandas の開発者本人が書いた定番書で、読み込み・集計から可視化までを Jupyter で手を動かしながら学べます。当サイトのおすすめ本ランキングでは総合5位として紹介しており、リンク先では対象レベル・全612ページというボリューム・出版社の公式ページを確認でき、そこから楽天の価格・在庫ページへ進めます。なお、リンク先は当サイトの書籍紹介ページで、アフィリエイト広告を含みますのでご了承ください。
次につなげる:静止画から、動かせるチャートへ
今回作ったのはPNGとして保存できる静止画のチャートです。手元で毎日眺めるには十分ですが、期間を切り替えたい・特定の足の値を確認したいとなると、画像では手が届きません。第6回PythonとPlotlyでポートフォリオを可視化する(ホバー・ズーム・期間選択)では Plotly を使い、マウスを乗せると内訳が出る・ドラッグで拡大できる・ボタンで期間を切り替えられるグラフを扱っています(記事内に実際に触れるデモを置いてあります)。第7回はセクター分散の円グラフで、いずれも第4回の損益一覧がそのまま材料になります。連載の全体像と公開済みの記事は投資×Python シリーズ一覧から辿れます。
本連載は「自分の資産を自分で管理・可視化するツールを、Pythonの学習題材として自作する」ことが目的です。特定銘柄の売買や投資手法を勧めるものではなく、記事中のデータもすべて描画方法を示すための架空例です。当サイトは投資助言業(金融商品取引業)の登録を行っておりません。チャートの読み方や個別の投資判断についてのご質問にはお答えできません。投資判断はご自身の責任で行ってください(詳細は免責事項をご覧ください)。
11. よくある質問(FAQ)
Matplotlibで株価チャートを描くときにつまずきやすい点をまとめました。
Q. Matplotlibだけでローソク足は描けますか? 専用ライブラリは必要ですか?
Matplotlibだけで描けます。ヒゲを ax.vlines(x, Low, High)、実体を ax.bar(x, height=(Close-Open).abs(), bottom=min(Open, Close)) で描けば、追加のライブラリは要りません。専用ライブラリとしては mplfinance があり、PyPIで確認したところ2026年8月26日時点の最新は 0.12.10b0(2023年8月公開)です。数行で描けるのが利点ですが、配色やレイアウトを自分の好みに作り替えたいなら、自作したほうが自由が利きます。
Q. ローソク足の色は、赤と青のどちらが上昇ですか?
決まった正解はなく、地域とツールで異なります。日本の証券会社のチャートでは陽線(終値が始値以上)を赤、陰線を青で描くことが多く、本記事もこれに合わせています。一方、欧米のチャートでは陽線が緑、陰線が赤が主流です。同じ「赤い足」でも意味が逆になるため、自作するときは色を定数として1か所にまとめ、必要に応じて切り替えられるようにしておくのがおすすめです。
Q. チャートに土日や祝日の空白が入ってしまいます。消せますか?
x軸に日付をそのまま渡すのをやめて、連番(np.arange(len(df)))をx座標にします。そのうえで ax.set_xticks() で目盛りの位置を決め、ax.set_xticklabels() に日付の文字列を渡せば、取引があった日だけが等間隔に並びます。引き換えに、x軸は「時間」ではなく「本数」になるため、日付の目盛り間隔は不揃いになり、matplotlib.dates の日付機能は使えなくなります。
Q. 移動平均線の最初のほうが描かれません(NaNになります)。
仕様です。rolling(25).mean() は25本ぶんのデータが揃って初めて値を返すため、先頭の24行は NaN になり、その区間は線が描かれません。線を最初から引きたい場合は rolling(25, min_periods=1) を指定できますが、本数が揃っていない平均を「25日移動平均」として見せることになる点は理解しておいてください。表示期間より少し前からデータを取得しておき、計算後に表示範囲だけを切り出すほうが、意味としては正確です。
Q. Matplotlibのグラフで日本語が文字化けして□になります。どう直せばいいですか?
Matplotlibの既定フォント(DejaVu Sans)に日本語のグリフが無いためで、Glyph 26666 ... missing from font(s) DejaVu Sans. という警告が出ます。対処は plt.rcParams["font.family"] = "Meiryo" のように、日本語を持つフォントを指定することです。フォント名は環境ごとに違うので、matplotlib.font_manager.fontManager.ttflist を一覧表示して、実際にある名前を使ってください。検証環境(Windows 11)では Meiryo・Yu Gothic・MS Gothic などが利用できました。
Q. 始値と終値が同じ日の足だけ、実体が描かれません。
実体の高さが0になり、ax.bar() が高さ0の棒を描いている(=見えない)ためです。始値と終値が同じ足は「同事(どうじ)」と呼ばれ、値動きの小さい銘柄では実際に発生します。高さが0のときだけごく小さな値に置き換えると、横線として残せます。本記事の完成コードでは body.where(body > 0, (High.max() - Low.min()) * 0.002) として、チャート全体の値幅の0.2%を最小の高さにしています。
Q. 実際の株価データでチャートを描くには、どこを変えればいいですか?
make_sample_ohlcv() の呼び出しを、第1回で作った取得処理に差し替えるだけです。yfinance の history() が返す DataFrame は列名が Open / High / Low / Close / Volume で、本記事のサンプルと同じ形をしています。Dividends などの余分な列があっても、描画関数は必要な列を名前で取り出すため影響しません。取得方法はyfinanceで株価データを取得するを参照してください。
9章の plot_stock_chart() を chart.py として保存しておけば、連載の後半でそのまま再利用できます。第7回以降は順次公開ですが、いま地続きで手を動かせるのは次の3本です。
- チャートに載せる損益の数字を作るなら → Pythonで保有銘柄の損益を計算する(pandas)(第4回)
- 同じ数字を「触れるグラフ」で見たいなら → PythonとPlotlyでポートフォリオを可視化する(第6回)
- 取得と描画をひとつのツールにまとめたいなら → Pythonで株価トラッカーを作る(yfinance+matplotlib)
連載の全体像と公開済みの記事は投資×Python シリーズ一覧にまとめてあります。続きが出たときに気づけるよう、この一覧ページをブックマークしておくのが確実です。RSSリーダーを使っているなら、更新は新着記事のRSSでも配信しているので、登録しておけば第7回の公開に自動で気づけます。
本記事は、生成AIを活用して下書きし、運営者が内容を確認・編集したうえで公開しています。掲載しているコード・出力・チャート画像は Python 3.12.10 / matplotlib 3.11.1 / pandas 3.0.3 / NumPy 2.4.6 で実際に実行して確認したものですが、登場する銘柄名・価格・出来高はすべて架空であり、実在の銘柄や相場を示すものではありません。本記事は描画技術の情報提供を目的としたもので、チャートの解釈・特定の銘柄の売買・投資手法を推奨するものではありません。投資判断はご自身の責任で行ってください。AIの利用方針は免責事項をご覧ください。