FOMCは深夜2時開始で、子供が生まれてから完全に諦めた。でも日銀会合は昼間にある。「これならいける!」と思って去年の政策変更のタイミングでドル円をロングしたら、見事に逆方向に動いてあっさり損切り。。。結局、方向感を持たずに飛び込んだのが問題だったんだよね。
なぜ日銀会合でドル円が読みにくいのか
この失敗から「過去の日銀会合でドル円はどう動いたのか」を体系的に調べようと思った。感覚で「利上げ→円高」と思っていたけど、それが本当に成り立つのかをPythonで検証してみる。
日銀(日本銀行)は年8回の金融政策決定会合を開催する。政策金利の変更・現状維持・フォワードガイダンスの修正など、発表内容によってドル円は大きく動くことがある。ただし、「利上げだから円高」という単純なロジックが通用しないケースも多く、市場の事前予測との乖離(サプライズ度)が重要になる。
Pythonで日銀会合前後のドル円変動を集計する
まず必要なデータを用意する。日銀会合の開催日は日銀のウェブサイトで公開されているが、今回は過去データを手動でリストアップしてPythonに渡す形にする。ドル円の価格データはyfinanceで取得できる。
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from datetime import datetime, timedelta
# 過去の日銀会合開催日(主要な会合をピックアップ)
# 実際には日銀ウェブサイトから全件取得するのが望ましい
boj_meetings = [
# (日付, 政策変更の種類)
("2023-01-18", "現状維持(YCC修正議論)"),
("2023-03-10", "現状維持"),
("2023-04-28", "現状維持(YCC柔軟化示唆)"),
("2023-06-16", "現状維持"),
("2023-07-28", "YCC上限引き上げ"),
("2023-09-22", "現状維持"),
("2023-10-31", "YCC再修正"),
("2023-12-19", "現状維持"),
("2024-01-23", "現状維持"),
("2024-03-19", "マイナス金利解除・利上げ"),
("2024-04-26", "現状維持"),
("2024-06-14", "現状維持(国債買入縮小示唆)"),
("2024-07-31", "追加利上げ0.25%"),
("2024-09-20", "現状維持"),
("2024-10-31", "現状維持"),
("2024-12-19", "追加利上げ0.25%"),
("2025-01-24", "追加利上げ0.25%"),
("2025-03-19", "現状維持"),
("2025-04-30", "現状維持"),
("2025-06-17", "現状維持"),
]
# ドル円データ取得
usdjpy = yf.download("JPY=X", start="2023-01-01", end="2026-06-30", interval="1d")
usdjpy = usdjpy["Close"].squeeze()
# 各会合前後の変動を計算
results = []
for date_str, policy in boj_meetings:
meeting_date = pd.Timestamp(date_str)
# 会合日前日・当日・翌日のレート
try:
# 前日
pre_date = meeting_date - timedelta(days=1)
while pre_date not in usdjpy.index:
pre_date -= timedelta(days=1)
pre_rate = usdjpy[pre_date]
# 当日(会合発表後)
if meeting_date in usdjpy.index:
same_rate = usdjpy[meeting_date]
else:
same_rate = None
# 翌日
next_date = meeting_date + timedelta(days=1)
while next_date not in usdjpy.index:
next_date += timedelta(days=1)
next_rate = usdjpy[next_date]
change_1d = (same_rate - pre_rate) / pre_rate * 100 if same_rate else None
change_2d = (next_rate - pre_rate) / pre_rate * 100
results.append({
"date": date_str,
"policy": policy,
"pre_rate": round(float(pre_rate), 2),
"same_rate": round(float(same_rate), 2) if same_rate else None,
"next_rate": round(float(next_rate), 2),
"change_1d_pct": round(change_1d, 3) if change_1d else None,
"change_2d_pct": round(change_2d, 3),
})
except Exception as e:
print(f"{date_str} エラー: {e}")
df = pd.DataFrame(results)
print(df[["date", "policy", "pre_rate", "same_rate", "change_1d_pct", "change_2d_pct"]].to_string())
政策種類別の平均変動幅を集計する
データが集まったら、政策内容を「利上げ」「現状維持」「その他(YCC修正等)」に分類して集計してみる。
# 政策をシンプルに分類
def classify_policy(policy_str):
if "利上げ" in policy_str:
return "利上げ"
elif "現状維持" in policy_str and "修正" not in policy_str and "縮小" not in policy_str:
return "現状維持"
else:
return "その他(YCC修正等)"
df["category"] = df["policy"].apply(classify_policy)
# カテゴリ別統計
summary = df.groupby("category")["change_2d_pct"].agg(["count", "mean", "std", "min", "max"])
summary.columns = ["件数", "平均変動(%)", "標準偏差", "最小", "最大"]
summary["平均変動(%)"] = summary["平均変動(%)"].round(3)
print(summary)
# 可視化
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# 散布図:時系列でどう動いたか
ax1 = axes[0]
colors = {"利上げ": "red", "現状維持": "gray", "その他(YCC修正等)": "orange"}
for cat, group in df.groupby("category"):
ax1.scatter(pd.to_datetime(group["date"]), group["change_2d_pct"],
label=cat, color=colors[cat], s=80, alpha=0.8)
ax1.axhline(0, color="black", linewidth=0.8, linestyle="--")
ax1.set_title("日銀会合後翌日のドル円変動率")
ax1.set_ylabel("変動率 (%)")
ax1.legend()
ax1.xaxis.set_major_formatter(mdates.DateFormatter("%y/%m"))
plt.setp(ax1.xaxis.get_majorticklabels(), rotation=45)
# ボックスプロット
ax2 = axes[1]
categories = list(colors.keys())
data_by_cat = [df[df["category"] == cat]["change_2d_pct"].dropna().values for cat in categories]
bp = ax2.boxplot(data_by_cat, labels=categories, patch_artist=True)
for patch, cat in zip(bp["boxes"], categories):
patch.set_facecolor(colors[cat])
patch.set_alpha(0.6)
ax2.axhline(0, color="black", linewidth=0.8, linestyle="--")
ax2.set_title("政策種類別ドル円変動分布")
ax2.set_ylabel("変動率 (%)")
plt.setp(ax2.xaxis.get_majorticklabels(), rotation=15)
plt.tight_layout()
plt.savefig("boj_usdjpy_reaction.png", dpi=150)
plt.show()
print("グラフ保存完了")
会合前の「事前期待」をサプライズ指標で調整する
実は僕が最初に気づいた重要なポイントは「利上げ=円高じゃない」ということ。2024年3月のマイナス金利解除でも円安に動いた局面がある。これは「市場がすでに利上げを織り込んでいた」から。つまり重要なのは「結果」じゃなく「期待との差(サプライズ)」なんだよね。
簡易的なサプライズ指標として「会合前5日間のドル円動き(事前の期待織り込み)」と「会合後の実際の動き」を比較してみる。
# 会合前5日間の動き(期待の方向性)
pre_move_results = []
for _, row in df.iterrows():
meeting_date = pd.Timestamp(row["date"])
# 5営業日前
start_idx = usdjpy.index.searchsorted(meeting_date) - 5
if start_idx < 0:
continue
pre_5d_rate = usdjpy.iloc[start_idx]
pre_rate = row["pre_rate"]
pre_5d_change = (pre_rate - float(pre_5d_rate)) / float(pre_5d_rate) * 100
pre_move_results.append({
"date": row["date"],
"category": row["category"],
"pre_5d_pct": round(pre_5d_change, 3), # 会合前5日の動き(正=円安方向)
"post_pct": row["change_2d_pct"], # 会合後翌日の動き
})
df2 = pd.DataFrame(pre_move_results)
# 事前に円安期待が高かった場合(pre_5d_pct > 0)と低かった場合で分析
df2["pre_expectation"] = df2["pre_5d_pct"].apply(
lambda x: "円安期待(ドル高)" if x > 0 else "円高期待(ドル安)"
)
cross_table = df2.groupby(["category", "pre_expectation"])["post_pct"].agg(["count", "mean"]).round(3)
print(cross_table)
print("\n===")
print("※ 利上げ局面で事前に円高期待が高まっていたケースの会合後変動:")
mask = (df2["category"] == "利上げ") & (df2["pre_expectation"] == "円高期待(ドル安)")
print(df2[mask][["date", "pre_5d_pct", "post_pct"]])
分析してわかったこと
このコードを走らせてみると、いくつか面白いパターンが見えてくる。利上げ局面でも事前に「円安期待(会合で何もしないだろう)」が高まっていたケースでは、発表後に円高(ドル安)に振れる傾向があった。逆に現状維持でも事前に円高を織り込んでいたのにタカ派サプライズがあると円高継続になる。
もちろんサンプル数が少ないので統計的な有意性はないけれど、「方向を決め打ちするより、事前の市場期待と発表内容のズレを見る」という視点は確実に持てた。日銀会合で突撃トレードするより、まず過去を分析してからのほうが絶対いい。
まとめ:日銀会合トレードの前にやるべきこと
今回のポイントをまとめる。
まず、yfinanceとPythonで過去の日銀会合前後のドル円変動を一覧化できる。次に、政策内容を単純に「利上げ」「現状維持」「その他」に分けて集計するだけで、傾向が見えてくる。そして最も重要なのは「結果=価格方向」ではなく「サプライズ=事前期待との差」を意識すること。会合前5日間の動きを使って事前期待を簡易推定できる。
個人的には、このデータを見てから日銀会合トレードに対する姿勢が変わった。今は会合当日に飛びつくのをやめて、発表後30分の動きを観察してからエントリーするようにしている。次は夜間の米雇用統計とドル円の同じような分析もやってみたいと思っている。

