配当金カレンダーを自動生成する(HTMLCalendar × SQLite)
Python経験者向けの投資分析シリーズ第9回。第8回で貯めた配当の入金実績を読み込み、12か月ぶんのHTMLカレンダーと月別グラフを書き出すツールを作ります。権利確定日と支払日の違い、月末の丸め、yfinanceで日本株の支払日が取れない話まで実行結果つきで。
1. この記事のゴールと連載の位置づけ
本記事は「Python経験者が、自分の投資ポートフォリオを管理・可視化するアプリを作る」連載の第9回です。第8回で配当の入金を貯める側を作ったので、今回はその読む側。貯めた dividends.db を入力にして、1年ぶんの入金カレンダー(HTML1枚)と月別の棒グラフを書き出します。
--demo で出力した dividend_calendar_2026.html(上部の赤い帯は --demo 時だけ入る注意書き。銘柄名・金額はすべて架空)証券口座のアプリでも入金履歴は見られます。それでも自分で作る値打ちがあるのは、口座をまたいだ1枚にならないからです。A証券とB証券に分かれた保有を1つのカレンダーで並べる、去年の実績と今年の見込みを同じ表で見る——データが自分のDBにあるからこそ、見せ方を自分で決められます。(口座の列は第8回のスキーマに無いので、カレンダー上で口座別に色分けはされません。区別したいなら銘柄名を「アルファ工業(NISA)」のように登録して分けます。)
第8回との住み分け
配当の記録そのものは第8回が本体です。入力フォーム・テーブル設計・金額の型・銘柄マスタはPythonで配当金管理ツールを作る(tkinter × SQLite)で作りました。本記事はそこに貯まった実績を読み出して並べ替えるだけで、第8回の dividends.db には1行も書き込みません(書き込むのは --demo を付けたときだけで、対象は常に dividends_demo.db です)。読み書きを分けておくと、表示を作り直してもデータが壊れません。
dividends.db の入金実績を読み、指定した年の12か月ぶんをHTMLカレンダーと棒グラフにするところまで。その年の実績が無い月には前年の同じ月の入金を写した「見込み」を薄い色で置きます(判定は月単位なので、過ぎた月でも実績が入っていなければ見込みが残ります)。本文は要点のコードだけで、完成版は dividend_calendar.py(443行)にあります。
作り方だけ急ぐなら7章(HTMLCalendarの継承)→9章(動かす)だけでも要点は追えます。2〜6章は、そこでたいてい踏む地雷の先回りです。
カレンダーとグラフを作る処理(4〜9章)は完全にローカルで、外部との通信はありません。通信するのは3章のyfinanceを使う部分だけ、送るのは銘柄コードだけで dividends.db の中身は送信しません。3章は使わなくてもツールは完成します(配布コードでも --fetch-ex-date を付けたときにしか動きません)。
本文のコード・出力・画面写真は Windows 11 / Python 3.12.10 / SQLite 3.49.1 / matplotlib 3.11.1 / pandas 3.0.3 / yfinance 1.4.1 で 2026年9月3日に実行して確認したものです。画面写真は生成されたHTMLをブラウザで開いて撮ったもので、合成や加筆はありません。サンプルの銘柄名・株数・入金額はすべて架空(アルファ工業・ブラボー商事などは実在の企業・団体とは一切関係のない架空名です)。3章のyfinanceの出力だけは取得日時点の実データです。
2. カレンダーに載せる「日付」を先に4つに分ける
コードを書く前に決めることが1つ。そのカレンダーは、どの日付を載せるのか。配当まわりには似た日付が4つあり、混ぜたまま作ると1〜3か月ぶんまるごとズレたカレンダーができあがります。
| 日付 | 意味 | 例(権利確定日が2026-03-31の場合) |
|---|---|---|
| 権利付最終日 | 権利確定日の2営業日前。この日の取引時間中まで | 2026-03-27(金) |
| 権利落ち日 | 権利確定日の1営業日前。yfinanceが返すのはこの日 | 2026-03-30(月) |
| 権利確定日(基準日) | 会社が定める日。この日の株主名簿に載っていると配当を受け取れる | 2026-03-31(火) |
| 支払日(入金日) | 実際にお金が振り込まれる日。本記事のカレンダーが載せるのはこれ | 権利確定日の2〜3か月後が目安 |
「権利確定日の2営業日前が『権利付最終日』、1営業日前が『権利落ち日』となります」——金融経済教育推進機構(J-FLEC)の用語解説にこう書かれています。上の例では3/28が土曜、3/29が日曜なので、2営業日前は3/27(金)。
証券会社の説明では「権利確定日を含む3営業日前までに買う」という数え方をします(auカブコム証券のお知らせ)。上の例なら3/31・3/30・3/27 と数えて3日目が3/27。J-FLECの「権利確定日の2営業日前」と指している日は同じで、権利確定日を数に含めるかどうかが違うだけ。読み比べるときは必ず具体的な日付に直してください。曖昧なまま計算式を書くと、まるまる1日ずれます。
営業日の計算は、あえてツールに入れない
権利付最終日を正しく出すには東証の休業日カレンダーが要ります。土日を除くだけでは足りず、標準ライブラリだけでは持てないデータです。
それでも外部ライブラリを足さなかったのは、1日ずれたときの損が大きいから。本連載は保有データの可視化が主題で、売買のタイミングを案内するものではありません。カレンダーが載せるのは入金日だけ、と決めておくと迷いません。
日本株と米国株ではルールが違う
英語圏の dividend calendar のコードをそのまま日本株に当てると合いません。米国は2024年5月28日のT+1移行にあわせてFINRA Rule 11140(b)(1)が改正され、権利落ち日は基準日が営業日ならその基準日そのもの(FINRA Rule 11140)。日本株の「基準日の1営業日前」とは定義が1日ずれます。
3. つまずき実演:yfinanceの配当データは入金日ではない
第8回の最後に「配当の自動取得は次回に」と書いた宿題がここです。「1株配当くらい自動で取れるだろう」——取れます。ただし取れる値と、載せたい値が違う。
以下のコードは 第1回で使ったyfinance 経由で Yahoo! Finance に問い合わせます。送るのは銘柄コードだけで、手元のDBの中身は送りません。
import yfinance as yf # yfinance 1.4.1・2026-09-03 実行
t = yf.Ticker("7203.T")
print(t.calendar) # 次回の予定
print(t.dividends.tail(3)) # 過去の配当
{'Ex-Dividend Date': datetime.date(2026, 9, 29),
'Earnings Date': [datetime.date(2026, 11, 5)], ...}
Date
2025-03-28 09:00:00+09:00 50.0
2025-09-29 09:00:00+09:00 45.0
2026-03-30 09:00:00+09:00 50.0
Name: Dividends, dtype: float64
見どころは2つ。
1つめ。dividends のインデックスは権利落ち日です。2026-03-30 は2章の表で権利落ち日として計算した日そのもので、権利確定日(3/31)でも支払日でもない。値も1株あたりの税引前の額です。家計簿に入れたいのは「いつ・いくら振り込まれたか」なので、この日付を入金日として扱うと月が2〜3か月ぶん手前にずれます。タイムゾーン付き(Asia/Tokyo・時刻は寄付の09:00)でもあるので、日付だけ使うなら t.dividends.index.tz_localize(None) のようにインデックス側で落としてから丸めます(値は float の Series なので .dt は使えません)。
2つめ。calendar の辞書に 'Dividend Date'(支払日)のキーが無い。9432.T でも同じで、返るのは 'Ex-Dividend Date' だけ。t.info にも dividendDate はありません。
米国株と並べると違いがはっきりする
print(yf.Ticker("AAPL").calendar) # 比較用・2026-09-03 実行
{'Dividend Date': datetime.date(2026, 8, 13),
'Ex-Dividend Date': datetime.date(2026, 8, 10), ...}
米国株には 'Dividend Date' が入っていました。キーがあるかどうかは銘柄しだいで、cal['Dividend Date'] と直接書けば日本株では KeyError。cal.get("Dividend Date") で取り、None が返る前提で書きます。
試した日本株2銘柄では、支払日を返すフィールドが見つかりませんでした(2026-09-03・yfinance 1.4.1)。将来のバージョンやデータ提供元の変更で変わる可能性はありますが、「取れない前提」で設計するほうが壊れません。本記事のカレンダーは手入力した入金実績を正とし、yfinanceは突き合わせに使うだけ。支払日の一次情報は各社の決算短信の「配当金支払開始予定日」です(SBI証券のFAQ)。
配布コードでは、この突き合わせを --fetch-ex-date オプションに分けてあります。
> python dividend_calendar.py --fetch-ex-date 7203.T
[7203.T] Ex-Dividend Date(権利落ち日): 2026-09-29
[7203.T] Dividend Date(支払日) : None
-> 支払日は取得できません。(以下、「権利確定日から2〜3か月後」の目安を
権利落ち日に当てはめた 2026-11-29 〜 2026-12-29 頃という参考値と注意書きが続く)
この章の出力だけは架空値ではなく、2026年9月3日に取得した実データです。銘柄コードは実在のものですが、推奨銘柄ではありません。yfinance はPyPIの説明で「調査・教育目的を意図している」「Yahoo! finance の API は個人利用のみを意図している」(当サイト訳)と明示しており、値の正確性は保証されません。利用条件は使う前に確認してください(番外編にまとめています)。正はご自身の取引報告書です。
4. つまずき実演:3月31日の3か月後は6月31日にならない
支払月を「◯か月後」で動かそうとすると、最初の1歩で止まります。素直に書くとこうなる。
import datetime
d = datetime.date(2026, 3, 31)
print(d.replace(month=6)) # 3か月後のつもり
ValueError: day is out of range for month
6月31日が無いので落ちます。たちが悪いのはいつも落ちるわけではない点で、同じコードが3月30日なら通る。テストデータに月末が無ければ気づけません。
逃げ道は3つ。python-dateutil の relativedelta を入れる、pandas の DateOffset を使う、自分で丸める。本記事は3つめ。必要なのが月の足し算だけなので、そのためにインストールを増やすのは割に合いません。
def add_months(d, months):
"""d の months か月後。存在しない日はその月の末日に丸める。
date.replace(month=...) だと 2026-03-31 の3か月後で
ValueError: day is out of range for month になる。
"""
total = d.month - 1 + months
year = d.year + total // 12
month = total % 12 + 1
last_day = calendar.monthrange(year, month)[1] # (1日の曜日, その月の日数)
return date(year, month, min(d.day, last_day))
for d, n in [(date(2026, 3, 31), 3), (date(2026, 11, 30), 3), (date(2028, 2, 29), 12)]:
print(f"{d} の {n}か月後 = {add_months(d, n)}")
2026-03-31 の 3か月後 = 2026-06-30
2026-11-30 の 3か月後 = 2027-02-28
2028-02-29 の 12か月後 = 2029-02-28
閏年の2月29日から1年後も、平年の2月28日に収まりました。うるう年判定を自分で書かずに済むのが calendar.monthrange() の効きどころです。
monthrange() が返すのは「初日と末日」ではないcalendar.monthrange(2026, 6) は (calendar.MONDAY, 30) を返します。1つめはその月の1日の曜日(0=月曜)、2つめがその月の日数。「(初日, 末日)」だと思って [0] を使うと、日付のつもりで曜日番号を掴みます。末日は [1]。
pandasを使うなら、単数形と複数形に注意
すでにpandasを入れているなら DateOffset でも構いません。月末は自動で丸めてくれます(pandas 3.0.3 で確認)。
import pandas as pd
print(pd.Timestamp("2026-03-31") + pd.DateOffset(months=3)) # Timestamp('2026-06-30 00:00:00')
print(pd.Timestamp("2026-05-15") + pd.DateOffset(month=3)) # Timestamp('2026-03-15 00:00:00')
危ないのは2行目。months(複数形)は加算、month(単数形)は置換で、5月15日が3月15日に「戻り」ます。s が1文字あるかないかで意味が反転するのにエラーは出ません(公式リファレンスにある "Works exactly like the keyword argument form of relativedelta." のとおりの仕様)。
5. DBから入金の実績を月別に取り出す
入力は第8回のテーブル。dividends(入金1件=1行)と stocks(銘柄マスタ)を結合して必要な列だけ取ります。
SELECT d.paid_on, d.ticker, s.name, d.shares, d.net_sen
FROM dividends d JOIN stocks s ON s.ticker = d.ticker
ORDER BY d.paid_on, d.ticker
paid_on は 'YYYY-MM-DD' 固定の文字列、net_sen は実際の入金額を銭(円×100)の整数で持つ形。どちらも第8回で決めたもので、書式が揃っているから ORDER BY がそのまま日付順になり、整数だから合計が誤差なく一致します。
月で集計するとき、年を落とすと静かに壊れる
SQLiteで月を取り出すなら strftime()。ここに罠が2つあります。
# 断片です。:memory: の dividends に3行(2026-03-25 / 03-31 / 2027-03-25)だけ入れた状態
print(con.execute("SELECT strftime('%m', paid_on), SUM(net_sen) "
"FROM dividends GROUP BY 1").fetchall())
print(con.execute("SELECT strftime('%Y-%m', paid_on), SUM(net_sen) "
"FROM dividends GROUP BY 1 ORDER BY 1").fetchall())
[('03', 1992000)]
[('2026-03', 1251000), ('2027-03', 741000)]
1つめ。返るのはゼロ埋めの文字列で、'03' なので if month == 3: は一致しません。
2つめのほうが重い。'%m' だけでグループ化すると、2026年3月と2027年3月が合算されます。上の 1992000 は2年ぶんの合計で、エラーも警告も出ません。カレンダーは「その年の12か月」を描くので、年別の集計では '%Y-%m' が必須です。
逆に '%m' 単独が正しい場面もあります。銘柄ごとの入金月のパターン——年をまたいで「この銘柄は6月と12月に入る」と数えたいとき。配布コードはここだけ GROUP BY d.ticker, strftime('%m', d.paid_on) です。
銘柄ごとの入金月パターン(過去の実績・年をまたいで集計)
アルファ工業: 6月(2件) 12月(1件)
チャーリー電機: 6月(2件)
デルタ食品: 3月(2件) 6月(2件) 9月(1件) 12月(1件)
ブラボー商事: 3月(2件) 9月(1件)
年2回の銘柄も四半期ごとに入る銘柄も、実績を数えれば見分けがつきます。「日本株は年2回」と決め打ちしないための材料でもある(中間配当は定款に定めのある会社が行える制度で、実施しない会社もあります)。
pandasで月次にするなら、版差を先に確認する
本ツールはpandasを使いません。集計をSQLite側で済ませられるうえ、月次まわりは版で書き方が変わるからです。手元の pandas 3.0.3 では、次の2行がどちらも ValueError になりました。
s.resample("M").mean() # s は日次の Series
pd.Period("2026-03", freq="ME")
resample と date_range は "ME"、Period 系は逆に "M" が正しい。一律置換すると to_period() が壊れます(メッセージはFAQ末尾に)。tz 付きのインデックスを月に丸めるときは .tz_localize(None) を先に書くこと。どのタイムゾーンで月を決めたかが曖昧だと、月末の配当が1か月ずれます。
6. 支払月を「推定」する — 限界を先に書く
実績だけを並べると、カレンダーの後ろ半分は空白になります。知りたいのは「これから何月にいくら入りそうか」なので、ここを埋めたい。ただし埋め方の性格を先に決めないと、予測を出すツールになってしまう。採る方式は1つだけで、前年の入金を1年後の同じ日付へそのまま写す。マスに乗る日付は前年の入金日をずらしただけで、その日に入るという意味は一切ありません(見るのは月です)。会社の配当予想も、権利確定日からの月数計算も使いません。
def project_next_year(payments, year):
"""前年の入金を year に写して「見込み」を作る。
同じ銘柄・同じ月に year の実績がすでにあれば作らない(実績が優先)。
金額は前年と同額を置いただけの見込みで、入金の確約ではない。
"""
actual_keys = {(t, d.month) for d, t, *_ in payments if d.year == year}
projected = []
for d, ticker, name, shares, net_sen in payments:
if d.year != year - 1:
continue
moved = add_months(d, 12)
if (ticker, moved.month) in actual_keys:
continue
projected.append((moved, ticker, name, shares, net_sen))
return projected
add_months(d, 12) を挟むのは4章の理由から。2月29日の記録があると、単純な replace(year=...) は翌年で落ちます。
もう1つ効いているのが actual_keys の判定。実績が入った月には見込みを置かないので、6月の入金を記録した瞬間に6月の見込みが消えます。これが無いと「入金済みなのに合計が倍になる」という気づきにくいバグになります。
この推定が当たらない場面
| 起きること | 推定への影響 |
|---|---|
| 増配・減配 | 金額が変わる。前年と同額を置いた見込みは外れる |
| 無配・配当の見送り | 入金そのものが無くなる |
| 年2回から四半期配当への変更 | 入金月が増える。前年の実績には現れない |
| 決算期の変更 | 権利確定月ごと動くので、月がまるごとずれる |
| 株数を売買で変えた | 前年の株数で計算した見込みが実態と合わない |
| 実績が1回しかない銘柄 | 周期を判断する材料が足りない |
だから画面上でも実績と見込みを同じ色で塗りません。7章のカレンダーでは青が実績、薄いオレンジが見込みで、マスの中にも「見込」と書き入れます。見た目で区別がつかない推定値は、いつのまにか事実として読まれますから。
7. HTMLCalendarを継承して自分のカレンダーを作る
カレンダーの升目を自前で組む必要はありません。標準ライブラリの calendar にある HTMLCalendar が、月や年のテーブルをそのままHTMLで吐きます。
import calendar
print(calendar.HTMLCalendar().formatmonth(2026, 3))
<table border="0" ... class="month">
<tr><th colspan="7" class="month">March 2026</th></tr>
<tr><th class="mon">Mon</th>...</tr>
各セルに mon〜sun、空きマスに noday のクラスが付く——CSSの取っ掛かりが最初から用意されています。週の始まりは既定で月曜、日曜始まりにしたければ calendar.HTMLCalendar(calendar.SUNDAY) と渡します。
差し替えるのは4つのメソッドだけ
やりたいのは「日付のマスに銘柄と金額を入れる」こと。継承して formatday() を上書きします。ところが署名が formatday(self, day, weekday)——受け取るのは日と曜日だけで、年と月が渡ってきません。date(2026, 3, 30) をキーにイベントを引きたいのに、何年何月かを知る手段がない。月の描画に入る手前で年月を覚えておきます。
class DividendCalendar(calendar.HTMLCalendar):
"""日付のマスに入金の実績と見込みを差し込む HTMLCalendar。"""
def __init__(self, events, firstweekday=calendar.MONDAY):
super().__init__(firstweekday)
self.events = events
self._year = None
def formatmonth(self, theyear, themonth, withyear=True):
# formatday には年月が渡らないので、ここで覚えておく
self._year, self._month = theyear, themonth
return super().formatmonth(theyear, themonth, withyear)
def formatmonthname(self, theyear, themonth, withyear=True):
# calendar.month_name は英語(ロケール未設定なら 'March')なのでベタ書きする
label = f"{theyear}年{themonth}月" if withyear else f"{themonth}月"
return f'<tr><th colspan="7" class="month">{label}</th></tr>\n'
formatyear() は内部で formatmonth() を12回呼ぶので、1か所置いておけば全月に効きます。曜日の見出しも同じ要領で、formatweekday() を "月火水木金土日" から1文字返すだけの実装に差し替えました。
locale は要らないcalendar.month_name[3] は Windows の日本語環境でも 'March' を返します。Pythonは起動時に setlocale をしないので、既定はCロケール(英語)のまま。locale.setlocale() はプロセス全体に効く副作用があり、Windowsでは "Japanese_Japan.932" のような指定も要ります。f"{m}月" とベタ書きするのが環境を問わず確実。ちなみに calendar.month_name は13要素で0番が空文字で、for で回すと13回まわります。
日付のマスを書き換える
def formatday(self, day, weekday):
if day == 0:
return '<td class="noday"> </td>' # 前後の月にはみ出したマス
items = self.events.get(date(self._year, self._month, day), [])
css = self.cssclasses[weekday]
... # 入金の無い日は日付だけ返す
css += " has-div" if any(a for _n, _s, a in items) else " has-plan"
parts = [f'<span class="dnum">{day}</span>']
for name, net_sen, is_actual in items:
mark = "" if is_actual else "見込"
klass = "ev" if is_actual else "ev plan"
parts.append(f'<span class="{klass}">{html.escape(name)}<b>'
f'{yen(net_sen)}円{mark}</b></span>')
return f'<td class="{css}">' + "".join(parts) + "</td>"
実績があれば has-div、見込みだけなら has-plan のクラスを足し、色はCSS側で分けます。銘柄名はhtml.escape() を通してから差し込む。自分で入れた名前でも & が1つ混ざれば表示が崩れるので、外に出す文字は素通しにしません。
formatyearpage() はスタイル付きのHTMLを返さない
1年ぶんを1ファイルにするなら formatyearpage() が使えそうに見えます。返るのは bytes の完全なHTML。ただし<style> は入っていません。
page = calendar.HTMLCalendar().formatyearpage(2026)
print(type(page), b"<style" in page) # <class 'bytes'> False
入っているのは calendar.css への link タグ1行だけ。そのファイルを別に用意しないと、装飾のない素のテーブルが表示されます。1枚で完結させたい本記事では使わず、テーブル部分だけを返す cal.formatyear(year, width=3) をf-stringで自前のHTMLに差し込み、<style> も同じファイルに書きました(width=3 は横に3か月ずつ=4段×3列に12か月が収まる)。集計表と注意書きを上に足せるのも、この形にした理由です。calendar モジュールそのものの入門はカレンダー表示ツールで扱っています。
この記事で効いたのは HTMLCalendar の使い方より、継承して差し替える場所を見つける読み方でした。formatday が年月を受け取らないと分かった時点で設計が決まる。標準ライブラリを読んで使う感覚を鍛えるならEffective Python 第3版が近道です(当サイトのおすすめ本ランキング総合4位・全572ページ)。リンク先は当サイトの書籍紹介ページで、アフィリエイト広告を含みます。
8. 月別キャッシュフローを1枚のグラフにする
カレンダーは日付の粒度で見る道具です。「今年はどの月に寄っているか」を掴むには12本の棒に落としたほうが速い。実績と見込みを積み上げて塗り分けます。
def render_chart(year, actual, planned, out_png):
import matplotlib
matplotlib.use("Agg") # 画面を出さずPNGに書く
import matplotlib.pyplot as plt
months = list(range(1, 13))
a = [actual[m] / 100 for m in months]
p = [planned[m] / 100 for m in months]
with plt.rc_context({"font.family": "Meiryo"}):
fig, ax = plt.subplots(figsize=(9, 4.2), dpi=110)
ax.bar(months, a, color="#2c5f8a", label="実績")
ax.bar(months, p, bottom=a, color="#f0c987", hatch="//",
edgecolor="#b8860b", label="見込み(推定)")
実装で効いているのは2点。
matplotlib.use("Agg") を先に呼ぶ。ウィンドウを開かずファイルだけ書くバックエンドなので、GUIのない環境やタスクスケジューラからでも同じ絵が出ます。pyplot をimportする前に指定するのが決まりです。
12か月ぶんのリストを先に作る。actual と planned は要素13個のリスト(0番は使わない)で、1件も無い月には 0 が入ります。DBの行だけをループしてグラフを組むと入金の無い月が軸から消えて11本のグラフになり、「どの月に寄っているか」の判断が狂う。枠を先に12個作って、あとから埋める——カレンダーもグラフも同じです。
日本語フォントの指定(rc_context で Meiryo を当てる方法)は第7回のMatplotlibで円グラフを作るにあります。円グラフから棒グラフに替えたのは見たいものが「構成比」ではなく「時間の並び」だから。色の濃さで金額を表すヒートマップにしなかったのは、濃い色が「良い」と読まれるためです。棒グラフなら高さがそのまま金額で、意味が1つに定まります。
9. 組み立てて動かす
部品をつないだのが dividend_calendar.py(443行)。第8回の dividend_app.py と同じフォルダに置いて実行します。
> python dividend_calendar.py --year 2026
銘柄ごとの入金月パターン(過去の実績・年をまたいで集計)
...(5章と同じ一覧)
2026年 実績 38,130円 / 見込み 25,810円
カレンダーを書き出しました: ...\dividend_calendar_2026.html
グラフを書き出しました: ...\dividend_monthly_2026.png
主なオプションはこれだけ。
| オプション | 働き |
|---|---|
--year 2026 | 出力する年(既定は今年) |
--db パス | 読み込むDB(既定はスクリプトと同じフォルダの dividends.db) |
--out / --chart | 出力先のHTML・PNG |
--no-chart | PNGを作らない。matplotlibが無い環境でもHTMLは出る |
--demo | 架空データのサンプルDBを作って動かす(dividends_demo.db)。第8回のDBが無くても試せる。作り直すのはこのファイルだけで、--db との併用はエラーで止まります(自分の記録用DBを消す事故が起きないように) |
--fetch-ex-date | ここだけ外部と通信する(3章) |
--demo で作られるDBは第8回と同じスキーマなので、中身を自分の記録に入れ替えればそのまま使えます。
dividend_calendar_2026.html には保有銘柄と入金額が書き込まれます。共有ドライブ・クラウドの同期フォルダ・Webサーバーの公開ディレクトリには置かないでください(生成されるHTMLの冒頭にも同じ注意を入れてあります)。手元だけで完結できるのが自作の実利なので、そこを自分で崩さないように。
第8回のアプリからボタン1つで呼ぶ
毎回コマンドを打つのが面倒なら、第8回のGUIから呼べます。第8回の配布コードは書き換えず、次の断片をクラス内のメソッドとして足すだけ(dividend_calendar.py は同じフォルダに置きます)。
import webbrowser
import dividend_calendar as dc # 断片です。そのままでは動きません
def export_calendar(self):
year = date.today().year
events, actual, planned = dc.build_events(dc.load_payments(DB_PATH), year)
out = Path(__file__).with_name(f"dividend_calendar_{year}.html")
out.write_text(dc.render_html(year, events, actual, planned, str(date.today())),
encoding="utf-8")
webbrowser.open(out.as_uri()) # 既定のブラウザで開く
あとは ttk.Button(frame, text="カレンダー出力", command=self.export_calendar) を置くだけ。モジュールとしてimportできる形に書いておくと、別のアプリに載せ替えられます。実行部分を if __name__ == "__main__": の内側に閉じ込めるのはそのためです。
10. つまずき一覧と、既製サービスとの使い分け
自分のデータを入れると出てくる問題です。上の3つはエラーが出ませんから、気づくきっかけがありません。
| つまずき | 何が起きるか | 対処 |
|---|---|---|
strftime('%m') で年別に集計する | 2026年3月と2027年3月が合算される | '%Y-%m' で年込みにする(5章) |
| DBの行だけでループしてマスを作る | 入金の無い月がカレンダーとグラフから消える | 12か月の枠を先に作って埋める(8章) |
| 実績のある月にも見込みを置く | 入金済みの月が二重計上される | 実績のある(銘柄, 月)を除外する(6章) |
date.replace(month=...) で月を足す | ValueError: day is out of range for month | monthrange() で末日に丸める(4章) |
cal['Dividend Date'] と直接書く | 日本株では KeyError | .get() で取り None 前提で書く(3章) |
formatyearpage() をそのまま使う | CSSが当たらず素のテーブルになる | formatyear() を自前のHTMLに埋める(7章) |
既製の配当カレンダーで足りるか
配当のカレンダーは既製のものが山ほどあります。何が違うのかをはっきりさせておきます。
| 見る観点 | 配当カレンダーのサイト | 証券会社のアプリ | 本記事の自作 |
|---|---|---|---|
| 出てくる金額 | 1株あたりの配当 | その口座の受取額 | 手入力した実際の入金額 |
| 並ぶ日付 | 権利確定日・権利落ち日 | 入金日(実績) | 入金日(実績+見込み) |
| 対象の銘柄 | 上場銘柄ぜんぶ | その口座で買った銘柄 | DBに入れた銘柄(口座をまたげる・色分けは無し) |
| 先の月 | 権利確定の予定は分かる | サービスにより異なる | 過去実績からの見込み |
| 手間 | かからない | かからない | 入力とコードが要る |
| 向いている使い方 | これから買う銘柄を調べる | 1つの口座で完結している | 複数口座を1枚にまとめたい |
銘柄を調べる用途なら既製サイトのほうが速く、正確です。自作が勝てるのは「自分がすでに持っているぶんが、いつ・いくら入るか」を1枚にまとめるところだけ。入力の手間を払う価値があるかは口座の数しだいで、1口座で完結しているなら証券会社の画面で足ります。
次回:条件で銘柄を絞り込む
第10回では配当利回りでの絞り込みを扱う予定です。連載の全体像は投資×Python シリーズ一覧にあります。
本連載は「自分の資産を自分で管理・可視化するツールを、Pythonの学習題材として自作する」ことが目的です。特定銘柄の売買や投資手法を勧めるものではなく、記事中の配当金額・株数・銘柄名はすべて架空の入力例です。当サイトは投資助言業(金融商品取引業)の登録を行っておらず、税務相談にも応じられません。税制の適用や確定申告の要否は国税庁の情報か税理士にご確認ください。投資判断はご自身の責任で(詳細は免責事項)。
11. よくある質問(FAQ)
Q. カレンダーに出た「見込み」の月と、実際の入金日がずれました。
ずれます。本ツールの見込みは前年の入金日を1年後の同じ日付に写しただけの推定で、会社の発表にもとづく予定ではありません。正確な日付は各社の決算短信「配当金支払開始予定日」が一次情報です。実際に入金されたら第8回のアプリで記録してください。実績が入った月の見込みは自動で消えます(6章)。
Q. 米国株にも使えますか?
入金の実績をDBに入れてあれば動きます(日付は paid_on、金額は net_sen=銭の整数を見るだけなので、外貨建ての配当は円換算後の入金額を入れてください。銘柄は第8回と同じく stocks に登録しておく必要があります)。ただし四半期配当なら年4回ぶんの実績が要るのと、権利落ち日のルールが日本株と違う点は前提が変わります(2章)。yfinanceの dividends も America/New_York のタイムゾーン付きで返ります(5章)。
Q. jpholiday を入れれば権利付最終日も計算できますか?
祝日を除いた営業日は数えられますが、東証の休業日と日本の祝日は完全に同じではありません(年末年始の扱いなど)。1日ずれると意味が変わる用途なので、本記事は機能に入れませんでした。必要なら取引所が公表する休業日カレンダーを一次情報として持つのが確実です。
Q. 生成したHTMLを人に見せても大丈夫ですか?
中身は自分の保有銘柄と入金額です。共有フォルダ・クラウド同期・Webサーバーの公開ディレクトリには置かないでください。見せるなら render_html() の集計表を組み立てている部分を書き換えて金額を伏せた版を作るほうが安全です。
Q. このツールは外部と通信しますか?
カレンダーとグラフを作るだけなら通信しません。読むのは手元の dividends.db、書き出すのも同じフォルダのHTMLとPNGです。通信するのは --fetch-ex-date を付けたときだけで、送るのは銘柄コードのみ(第8回のアプリ自体も通信しません)。
Q. 第8回のDBが無いのですが、試せますか?
python dividend_calendar.py --demo --year 2026 で架空データのサンプルDBが作られ、そのままカレンダーとグラフが出ます。スキーマは第8回と同じなので、動きを確かめてから自分の記録に入れ替えられます。サンプルの銘柄名・株数・金額はすべて架空です。
Q. df.resample("M") が ValueError になります。
pandas 2.2 で "M" が非推奨になり、3.0 でエラーになりました。resample や date_range では "ME"。ただしPeriod 系は逆で "M" が正しく "ME" がエラー("for Period, please use 'M' instead of 'ME'")。一律に置換すると to_period() が壊れます(5章)。
まずは完成版 dividend_calendar.py を保存し、python dividend_calendar.py --demo --year 2026 で架空データのカレンダーを出してみてください。記録する側がまだなら第8回の配当金管理ツール、データを取る側は第1回のyfinanceで株価データを取得する、sqlite3 を含む標準ライブラリの地図はPythonライブラリ一覧にあります。連載の全体像は投資×Python シリーズ一覧で、続きが出たときに気づけるよう、この一覧ページをブックマークしておくのが確実です。更新は新着記事のRSSでも配信しています。
本記事は、生成AIを活用して下書きし、運営者が内容を確認・編集したうえで公開しています。掲載しているコード・出力・画面写真は Windows 11 / Python 3.12.10 / SQLite 3.49.1 / matplotlib 3.11.1 / pandas 3.0.3 / yfinance 1.4.1 で 2026年9月3日に実行して確認したものです。サンプルの銘柄名・株数・配当金額はすべて架空の入力例で、実在の企業や運用実績とは関係がありません(3章のyfinanceの出力のみ取得時点の実データで、銘柄コードは実在しますが推奨銘柄ではありません)。本記事はプログラミングの情報提供を目的としたもので、銘柄の売買・投資手法の推奨でも税務上の助言でもありません。投資判断はご自身の責任で。AIの利用方針は免責事項をご覧ください。