寝不足がたたって、夜中の取引で連敗が続いていました。「夜は値動きが激しい」とはよく聞きますが、実際どれくらい激しいのかを自分で測ってみないと納得できないタチなので、Pythonで可視化してみることにしました。
結果は予想と少し違いました。「夜は動く」は正しかったのですが、「どの夜も同じように動くわけではない」ことが数字ではっきり見えたのです。しかも、動きが大きい時間帯が必ずしも稼ぎやすいわけではありませんでした。
この記事では、ドル円を東京・ロンドン・ニューヨークという3つのセッションに分けて分析します。時間を1時間刻みで見るのではなく、市場の参加者が入れ替わる区切りで捉える——この見方をすると、値動きの理由まで説明できるようになります。
※本記事のコードや情報は執筆時点の仕様に基づいています。投資は自己責任であり、必ずデモ環境や少額資金でテストした上で運用してください。
3つのセッションという枠組み
為替市場は24時間動いていますが、「常に同じ人が取引している」わけではありません。地球の自転にあわせて、主要な参加者が入れ替わっていきます。
| セッション | 日本時間 | 主な参加者 | ドル円の性格 |
|---|---|---|---|
| 東京 | 9:00〜15:00 | 日本の輸出入企業・機関投資家 | 比較的穏やか。レンジになりやすい |
| ロンドン | 16:00〜翌1:00 | 欧州勢。世界最大の取引量 | トレンドが出やすい |
| ニューヨーク | 21:00〜翌6:00 | 米国勢。経済指標の発表 | 最も荒い |
注目すべきは21時〜翌1時の重複時間帯です。ロンドンとニューヨークが同時に開いているため、この時間帯に取引量が集中します。僕がトレードしていたのは、まさにここでした。
1時間刻みで細かく見る方法もありますが、セッション単位で捉えると「なぜその時間に動くのか」まで説明できるのが利点です。単なる時間の問題ではなく、市場参加者が変わっているからだと理解できます。
それぞれのセッションで値動きの性格が違う理由も、参加者を考えれば納得できます。
- 東京: 実需(輸出入企業の決済)が中心。方向性より「必要だから両替する」取引が多い
- ロンドン: 世界最大の取引量。投機的なポジションが動き出すためトレンドが出やすい
- ニューヨーク: 米国の経済指標が集中。サプライズで一気に動く
この背景を知っていると、分析結果の解釈が変わります。「NY時間が荒い」のは偶然ではなく、指標発表という明確な理由があるわけです。であれば、指標カレンダーを確認するだけでリスクをかなり減らせることになります。
📘 外部参考:FX市場時間(東京・ロンドン・ニューヨーク) / Forex Trading Sessions(Investopedia)
データを準備する
pip install yfinance pandas numpy matplotlib
時間帯の分析には1時間足のデータが必要です。日足では時間の情報が失われてしまいます。
import yfinance as yf
import numpy as np
import pandas as pd
def load_hourly(symbol="JPY=X", period="60d"):
"""1時間足を取得し、日本時間に変換する"""
df = yf.download(symbol, period=period, interval="1h",
auto_adjust=True, progress=False)
if df.empty:
raise ValueError("データを取得できませんでした")
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
# タイムゾーンを日本時間に揃える(ここが最重要)
if df.index.tz is None:
df.index = df.index.tz_localize("UTC")
df.index = df.index.tz_convert("Asia/Tokyo")
df = df.dropna(subset=["Open", "High", "Low", "Close"])
df["hour"] = df.index.hour
df["weekday"] = df.index.dayofweek
return df
df = load_hourly()
print(f"期間: {df.index[0]:%Y-%m-%d %H:%M} 〜 {df.index[-1]:%Y-%m-%d %H:%M}")
print(f"本数: {len(df):,}")
タイムゾーンの変換が最大の落とし穴です。yfinanceが返す時刻はUTCなどのことがあり、そのまま「21時」として扱うと日本時間の6時になってしまいます。
この変換を忘れると、分析結果がまるごと9時間ずれます。「ニューヨーク時間が静か」というおかしな結論が出たら、まずここを疑ってください。
なお、無料で取得できる1時間足は直近2か月程度に限られます。もっと長期で分析したい場合は、有料のデータサービスを検討することになります。データソースの選択肢はPython株価データ取得ツール徹底比較にまとめています。
セッションごとに集計する
時刻をセッションに振り分けます。重複時間帯を独立した区分として扱うのがポイントです。
def assign_session(hour):
"""日本時間の時刻をセッションに割り当てる"""
if 9 <= hour < 15:
return "東京"
if 15 <= hour < 21:
return "ロンドン"
if 21 <= hour or hour < 1:
return "ロンドン×NY" # 重複時間帯
if 1 <= hour < 6:
return "NY後半"
return "閑散帯" # 6〜9時
df["session"] = df["hour"].apply(assign_session)
# セッションごとのボラティリティを測る
df["range_pips"] = (df["High"] - df["Low"]) * 100 # 値幅(銭→pips換算)
df["ret"] = df["Close"].pct_change() * 100 # 変化率(%)
df["abs_ret"] = df["ret"].abs()
order = ["東京", "ロンドン", "ロンドン×NY", "NY後半", "閑散帯"]
summary = df.groupby("session").agg(
本数=("Close", "count"),
平均値幅=("range_pips", "mean"),
最大値幅=("range_pips", "max"),
平均変動率=("abs_ret", "mean"),
標準偏差=("ret", "std"),
).reindex(order).round(3)
print(summary.to_string())
実行すると、ロンドン×NYの重複時間帯が突出して大きいことが分かります。東京時間の2倍前後という結果になることが多いはずです。
ここまでは「夜は動く」という通説どおりです。問題は、その先でした。
集計にあたって「値幅」と「変化率」の2つを見ているのには理由があります。
- 値幅(高値−安値): その時間内にどれだけ振れたか。逆指値が刈られるリスクに直結する
- 変化率(終値ベース): 結果としてどれだけ動いたか。損益に直結する
この2つは一致しません。大きく上下に振れたのに終値は元の位置、という時間帯があるからです。損切りを置いていれば刈られますが、終値だけ見ていると「何も起きなかった」ように見えます。
損切りの置き方を考えるなら値幅を、期待できる利益を考えるなら変化率を見る——目的によって見るべき指標が違う点は押さえておいてください。
セッションの区切り方についても補足しておきます。本記事では重複時間帯を独立した区分にしていますが、これは「最も特徴的な時間帯を埋もれさせないため」です。
ロンドンを16時〜翌1時とひとまとめにしてしまうと、穏やかな16〜20時と荒れる21時以降が平均化されて、実態が見えなくなります。分析の目的は平均を出すことではなく、違いを見つけることなので、性格が変わる境目で区切るほうが有益です。
「動く」と「稼げる」は別だった
値幅が大きいということは、利益も損失も大きくなるということです。ここを確かめるため、方向の当てやすさも測ってみました。
def directionality(group):
"""その時間帯の値動きに方向性があるかを測る"""
r = group["ret"].dropna()
if len(r) < 20:
return pd.Series({"継続率": np.nan, "平均リターン": np.nan})
# 前の足と同じ方向に動いた割合(トレンドの続きやすさ)
same_direction = (np.sign(r) == np.sign(r.shift(1))).mean()
return pd.Series({
"継続率": round(float(same_direction) * 100, 1),
"平均リターン": round(float(r.mean()), 4),
})
direction = df.groupby("session").apply(
directionality, include_groups=False).reindex(order)
result = summary.join(direction)
print(result.to_string())
ここで見るべきは「継続率」です。50%なら完全にランダム、それより高ければトレンドが続きやすい、低ければ反転しやすいことを意味します。
実際に計算すると、どのセッションでも継続率は50%前後になります。つまり「値幅は2倍でも、方向の当てやすさは変わらない」——これが僕にとって一番の発見でした。
| 東京 | ロンドン×NY | |
|---|---|---|
| 値幅 | 小さい | 約2倍 |
| 方向の当てやすさ | 約50% | 約50%(同じ) |
| 1回の損益の振れ | 小さい | 大きい |
| 判断ミスの代償 | 軽い | 重い |
この表が意味するところは明快です。ボラティリティが高い時間帯は「稼ぎやすい」のではなく「結果が早く出る」だけです。優位性がないまま値幅の大きい時間に張れば、負けも早く大きくなります。
値動きの方向が予測しにくいという点は、株式でも同じことが確認できます。詳しくは自己相関とランダムウォーク仮説をPythonで確かめてみたをご覧ください。
この結果を見て、僕は自分の連敗の理由がようやく腑に落ちました。「夜は動くから稼げる」と思って夜にトレードしていたのですが、実際に増えていたのは値幅だけで、勝率は変わっていなかったのです。
むしろ悪いことに、寝不足で判断力が落ちた状態で、最も荒い時間帯に張っていたわけです。同じ勝率なら、コンディションが悪い時間に大きく張る理由はどこにもありません。
グラフで確認する
import matplotlib.pyplot as plt
hourly = df.groupby("hour").agg(
値幅=("range_pips", "mean"),
変動率=("abs_ret", "mean"),
)
fig, ax = plt.subplots(figsize=(12, 5))
bars = ax.bar(hourly.index, hourly["値幅"], edgecolor="white")
# セッションごとに色を分ける
for h, bar in zip(hourly.index, bars):
s = assign_session(h)
color = {"東京": "#3498DB", "ロンドン": "#2ECC71",
"ロンドン×NY": "#E74C3C", "NY後半": "#E67E22",
"閑散帯": "#95A5A6"}[s]
bar.set_color(color)
ax.set_xlabel("時刻(日本時間)")
ax.set_ylabel("平均値幅")
ax.set_title("ドル円 時間帯別の値動きの大きさ")
ax.set_xticks(range(0, 24))
ax.grid(alpha=0.3, axis="y")
plt.tight_layout()
plt.savefig("session_volatility.png", dpi=120)
peak = int(hourly["値幅"].idxmax())
quiet = int(hourly["値幅"].idxmin())
print(f"最も動く時間 : {peak}時台({assign_session(peak)})")
print(f"最も静かな時間: {quiet}時台({assign_session(quiet)})")
print(f"倍率 : {hourly['値幅'].max() / hourly['値幅'].min():.1f}倍")
グラフにすると、21時から24時にかけて明確な山ができるのが視覚的に分かります。そして早朝6〜8時が最も静かです。
最も動く時間と最も静かな時間で、値幅に3〜5倍の差がつくこともあります。同じ「1時間」でも、リスクの大きさがまるで違うということです。
曜日でも違いがある
時間帯だけでなく、曜日によっても値動きの性格が変わります。特に週の始めと終わりは特徴的です。
WEEKDAYS = ["月", "火", "水", "木", "金", "土", "日"]
by_day = df.groupby("weekday").agg(
本数=("Close", "count"),
平均値幅=("range_pips", "mean"),
平均変動率=("abs_ret", "mean"),
).round(3)
by_day.index = [WEEKDAYS[i] for i in by_day.index]
print(by_day.to_string())
# セッション×曜日のクロス集計
cross = df.pivot_table(values="range_pips", index="session",
columns="weekday", aggfunc="mean").reindex(order)
cross.columns = [WEEKDAYS[c] for c in cross.columns]
print("\n--- セッション×曜日の平均値幅 ---")
print(cross.round(2).to_string())
典型的な傾向として、月曜の朝は動きが鈍く、金曜のNY時間は荒れやすいという結果が出ます。週末をまたぐポジション調整が入るためだと考えられます。
クロス集計まで見ると、「金曜のロンドン×NY」が最も荒いといった具体的な組み合わせが浮かび上がります。ここを避けるだけでも、想定外の損失は減らせます。
分析結果を運用ルールに落とす
数字が出たので、実際の行動に変換します。ここまでやらないと分析した意味がありません。
時間帯に応じてロットを変える
値幅が2倍の時間帯で同じロットを持てば、リスクも2倍です。それなら、時間帯に応じてロットを調整すればリスクを揃えられます。
def position_multiplier(hour, baseline_session="東京"):
"""時間帯に応じたロット倍率を返す(リスクを一定に保つ)"""
base = float(df[df["session"] == baseline_session]["range_pips"].mean())
current = float(df[df["hour"] == hour]["range_pips"].mean())
if current <= 0:
return 1.0
return round(base / current, 2)
print(f"{'時刻':>4s} {'セッション':<12s} {'平均値幅':>8s} {'ロット倍率':>10s}")
for h in (10, 16, 22, 23, 3, 7):
width = float(df[df["hour"] == h]["range_pips"].mean())
print(f"{h:>3d}時 {assign_session(h):<12s} {width:>8.2f} "
f"{position_multiplier(h):>10.2f}")
これで「22時にエントリーするなら、東京時間の半分のロットにする」といった調整が根拠を持ってできます。感覚で「夜だから小さめに」と決めるより、ずっと納得感があります。
ボラティリティからポジションサイズを決める考え方は、標準偏差を使ってポジションサイズを決める方法で体系的に扱っています。
損切り幅も時間帯で変える
同じ理屈で、損切り幅も時間帯に合わせるべきです。東京時間の感覚で損切りを置くと、NY時間ではただのノイズで刈られます。
def stop_width(hour, multiple=1.5):
"""その時間帯の平均値幅を基準にした損切り幅"""
width = float(df[df["hour"] == hour]["range_pips"].mean())
return round(width * multiple, 1)
for h in (10, 22, 3):
print(f"{h:>2d}時にエントリー → 損切り幅の目安 {stop_width(h)} 程度")
早朝はスプレッドに注意する
これはデータからは見えない、実務上の注意点です。早朝6〜8時は値動きが小さいぶん、スプレッドが広がりやすい時間帯でもあります。
「静かだから安全」と考えて早朝に取引すると、値幅が小さいのにコストだけ高いという最悪の条件になります。値動きの大きさとコストは、セットで考える必要があります。
| 時間帯 | 値幅 | スプレッド | 評価 |
|---|---|---|---|
| 東京 | 小 | 狭い | コスト効率は良い |
| ロンドン×NY | 大 | 狭い | 値幅は取れるがリスクも大 |
| 早朝 | 小 | 広い | 避けるべき |
分析でつまずいたところ
夏時間で1時間ずれる
欧米には夏時間があるため、ロンドンやNYの市場開始時刻が季節によって1時間ずれます。日本時間で固定して分析していると、境目の時間帯で結果が混ざります。
# 現地時間に変換して、夏時間を自動で吸収する
df["ny_hour"] = df.index.tz_convert("America/New_York").hour
df["london_hour"] = df.index.tz_convert("Europe/London").hour
# 現地時間ベースでのNY市場(9:30〜16:00)
ny_open = df[(df["ny_hour"] >= 9) & (df["ny_hour"] < 16)]
print(f"NY市場時間の平均値幅: {float(ny_open['range_pips'].mean()):.2f}")
print("※ tz_convert を使えば夏時間は自動的に調整されます")
tz_convert で現地時間に直せば、夏時間は自動的に考慮されます。厳密に分析したいなら、この方法が確実です。
データ期間が短いと結論が変わる
2か月分のデータだと、その期間にたまたま大きな指標発表があったかどうかで結果が動きます。
# 極端な値を除いても結論が変わらないか確認する
threshold = df["range_pips"].quantile(0.99)
trimmed = df[df["range_pips"] <= threshold]
print("--- 全データ ---")
print(df.groupby("session")["range_pips"].mean().reindex(order).round(2).to_string())
print("\n--- 上位1%を除外 ---")
print(trimmed.groupby("session")["range_pips"].mean().reindex(order).round(2).to_string())
除外前後で順位が変わるなら、それは「たまたま大きく動いた日」に引っ張られた結果です。傾向として信じてよいのは、除外しても変わらない部分だけだと考えてください。
週明けの窓と週末の空白
為替は土日に市場が閉まるため、月曜の朝に前週末との価格差(窓)が発生することがあります。これを1時間分の値動きとして集計すると、月曜早朝が異常に荒く見えてしまいます。
# 週明け最初の足を除外して比較する
first_of_week = df.index.to_series().dt.dayofweek.eq(0) & \
df.index.to_series().dt.hour.le(8)
without_gap = df[~first_of_week.values]
print(f"全データの月曜平均値幅 : "
f"{float(df[df['weekday'] == 0]['range_pips'].mean()):.2f}")
print(f"窓を除いた月曜平均値幅 : "
f"{float(without_gap[without_gap['weekday'] == 0]['range_pips'].mean()):.2f}")
差が大きいなら、週明けの窓が数字を押し上げていたということです。窓は通常の値動きとは性質が違うので、分けて考えるほうが実態に近くなります。
まとめ
ドル円をセッション別に分析して、夜中の取引を見直した記録をまとめました。
- 為替は参加者が入れ替わるため、セッション単位で見ると値動きの理由まで分かる
- 1時間足の分析ではタイムゾーン変換が必須。忘れると結果が9時間ずれる
- ロンドン×NYの重複時間帯は、東京時間の約2倍の値幅
- ただし方向の当てやすさはどのセッションでも約50%。動く=稼げるではない
- 対策は時間帯に応じたロットと損切り幅の調整。リスクを一定に保つ
- 早朝は値幅が小さいのにスプレッドが広い。コスト効率が最悪
- 夏時間のずれは
tz_convertで現地時間に直せば自動的に吸収できる
分析を終えて僕が変えたのは、「夜中に取引しない」ではなく「夜中はロットを半分にする」というルールでした。値幅が2倍なら、ロットを半分にすればリスクは同じです。
連敗していた原因は、時間帯そのものではなく時間帯に合わせた調整をしていなかったことでした。同じ感覚で同じロットを張り続けていれば、荒れる時間帯で負けが大きくなるのは当然です。
まずはご自身のトレード時間帯で、平均値幅を計算してみてください。「自分が普段どれくらいの荒さの中で取引しているか」を数字で知るだけでも、ロットの決め方が変わるはずです。

