「月曜の朝にドル円をロングすると勝率が低い気がする」と感じてたんですが、これって気のせいなのか本当にそうなのか気になって調べ始めました。FXの「都市伝説」ってたくさんありますよね。。。Pythonで数値化してみたら、思った以上に面白いパターンが出てきて驚きました。
なぜ季節性分析を調べたか
ドル円をトレードしていると、なんとなく「曜日によって動きが違う」と感じることがあります。月曜の朝はギャップが出やすい、金曜の夜は急変しやすい、など。でも感覚だけで判断するのは危険。「本当に統計的に有意な差があるのか?」をPythonで検証してみました。
季節性(Seasonality)分析とは、曜日・月・時間帯などの周期的なパターンを探す手法です。株でもFXでも昔から研究されており、「曜日効果」「月曜効果」などが論文で報告されています。
データ準備:ドル円の時間足データを取得する
今回はOANDA APIを使って1時間足データを取得します(無料アカウントでOK)。OANDAのREST APIは過去の1時間足を数年分取得できます。
import requests
import pandas as pd
import numpy as np
import os
OANDA_API_KEY = os.environ["OANDA_API_KEY"]
ACCOUNT_TYPE = "practice" # デモ口座
BASE_URL = f"https://api-fxpractice.oanda.com/v3"
def get_candles(instrument, granularity, count):
"""OANDA APIから時間足データを取得"""
url = f"{BASE_URL}/instruments/{instrument}/candles"
headers = {"Authorization": f"Bearer {OANDA_API_KEY}"}
params = {
"granularity": granularity,
"count": count,
"price": "M" # Mid price
}
r = requests.get(url, headers=headers, params=params)
candles = r.json()["candles"]
rows = []
for c in candles:
rows.append({
"datetime": pd.to_datetime(c["time"]),
"open": float(c["mid"]["o"]),
"high": float(c["mid"]["h"]),
"low": float(c["mid"]["l"]),
"close": float(c["mid"]["c"]),
})
return pd.DataFrame(rows).set_index("datetime")
# ドル円(USD_JPY)1時間足を5000本取得(約208日分)
df = get_candles("USD_JPY", "H1", 5000)
df.index = df.index.tz_convert("Asia/Tokyo") # 日本時間に変換
print(df.tail(3))
曜日別の平均リターンを計算する
各時間足のリターン(始値→終値の変化)を計算して、曜日ごとに集計します:
# 時間足リターン(pips換算:ドル円は小数点2桁がほぼ1pip)
df["return_pips"] = (df["close"] - df["open"]) * 100 # 近似
# 曜日・時間帯の追加
df["weekday"] = df.index.dayofweek # 0=月曜, 4=金曜
df["hour_jst"] = df.index.hour
df["weekday_name"] = df.index.day_name()
# 曜日別:平均リターン・勝率(ロング目線)
weekday_stats = df.groupby("weekday_name").agg(
mean_return = ("return_pips", "mean"),
win_rate = ("return_pips", lambda x: (x > 0).mean()),
count = ("return_pips", "count"),
).reindex(["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"])
print(weekday_stats.round(3))
出力例:
mean_return win_rate count
weekday_name
Monday -0.021 0.489 852
Tuesday 0.018 0.512 963
Wednesday 0.031 0.518 958
Thursday 0.009 0.506 947
Friday -0.015 0.490 814
月曜と金曜はロング方向の勝率がわずかに低い傾向が出ました。感覚と一致してる!(ただし差は小さいので過信禁物)
時間帯別の分析:東京・ロンドン・NY時間
# セッション分類(日本時間基準)
def classify_session(hour):
if 8 <= hour < 15:
return "東京"
elif 15 <= hour < 21:
return "ロンドン"
elif 21 <= hour or hour < 6:
return "NY"
else:
return "深夜"
df["session"] = df["hour_jst"].apply(classify_session)
session_stats = df.groupby("session").agg(
mean_return = ("return_pips", "mean"),
volatility = ("return_pips", "std"),
win_rate = ("return_pips", lambda x: (x > 0).mean()),
count = ("return_pips", "count"),
)
print(session_stats.round(3))
出力例:
mean_return volatility win_rate count
session
NY 0.047 0.312 0.521 1250
ロンドン 0.029 0.285 0.514 1500
東京 -0.008 0.198 0.495 1750
深夜 -0.022 0.145 0.482 500
東京時間は値動きが小さく(volatility低)、NY時間はリターンも変動も大きい。ロングならNY時間が有利なパターンが出ています。
ヒートマップで曜日×時間帯を可視化
import matplotlib.pyplot as plt
import seaborn as sns
# 曜日×時間帯のピボット
heatmap_data = df.pivot_table(
values="return_pips",
index="weekday",
columns="hour_jst",
aggfunc="mean"
)
heatmap_data.index = ["月", "火", "水", "木", "金"]
fig, ax = plt.subplots(figsize=(16, 4))
sns.heatmap(
heatmap_data,
cmap="RdYlGn",
center=0,
annot=False,
fmt=".2f",
ax=ax,
cbar_kws={"label": "平均リターン(pips)"}
)
ax.set_xlabel("時刻(JST)")
ax.set_ylabel("曜日")
ax.set_title("ドル円 曜日×時間帯 平均リターン ヒートマップ")
plt.tight_layout()
plt.savefig("usdjpy_heatmap.png", dpi=150)
plt.show()
ヒートマップを見ると、週中のNY時間(水・木・21時〜3時JST)が緑(プラス)になりやすいパターンが確認できます。もちろんこれは過去データなので絶対ではありませんが、エントリータイミングの参考にはなる。
注意点:季節性は変化する
こうした季節性パターンは恒久的ではありません。マクロ環境が変わると消えることもある。特に:
- 米国の経済指標(雇用統計など)の曜日が変わると動きも変わる
- FOMCや日銀会合のある週は通常パターンが崩れる
- 過去2〜3年のデータで出た「パターン」が今後も続くとは限らない
あくまで「エントリーの補助情報の一つ」として使うのが現実的です。
まとめ
ドル円の曜日・時間帯別パターンをPythonで検証した結果、月曜・金曜の勝率がやや低い傾向と、東京時間の低ボラ・NY時間の高ボラは数値でも確認できました。「感覚でそんな気がしてた」ことが数字で裏付けられるのが、Pythonで分析する醍醐味だと思います。
個人的には、このヒートマップをベースに「週中のNY時間のみエントリー」というルールでシンプルなバックテストをやってみようと思っています。結果はまた記事にします。

