ゴールデンクロス(golden cross)とデッドクロス(dead cross)は、短期と長期の移動平均線が交差するポイントを指す古典的なテクニカル指標です。本記事では、定義の整理・pandas での検出ロジック・可視化、そして「シグナルどおりに売買すれば勝てるのか」の議論まで通します。

目次

  1. 定義
  2. サンプルデータの準備
  3. SMA とクロスを検出する
  4. クロスを可視化する
  5. クロス後の N 日リターンを集計する
  6. 機械的売買の限界
  7. 「翌営業日の始値で約定」を必ず仕込む

定義

短期移動平均を SMAshort\mathrm{SMA}_{\text{short}}、長期移動平均を SMAlong\mathrm{SMA}_{\text{long}} とします。

  • ゴールデンクロス: 短期線が長期線を 下から上に 抜けた瞬間
  • デッドクロス: 短期線が長期線を 上から下に 抜けた瞬間

数式で書くと、シグナル発生日 tt は次を満たす日です。

SMAshort,t1SMAlong,t1かつSMAshort,t>SMAlong,t\mathrm{SMA}_{\text{short},\,t-1} \le \mathrm{SMA}_{\text{long},\,t-1} \quad \text{かつ} \quad \mathrm{SMA}_{\text{short},\,t} > \mathrm{SMA}_{\text{long},\,t}

(デッドクロスは不等号の向きを逆にします。)

短期・長期の組み合わせは「5 日と 25 日」「25 日と 75 日」「50 日と 200 日」など複数の慣行があります。本記事では 25 日と 75 日 で進めます。

サンプルデータの準備

ランダムウォークで終値の Series を作ります。

import numpy as np
import pandas as pd
rng = np.random.default_rng(seed=11)
n = 300
returns = rng.normal(loc=0.0007, scale=0.013, size=n)
close = pd.Series(
1500 * np.exp(np.cumsum(returns)),
index=pd.date_range("2025-08-01", periods=n, freq="B"),
name="C",
)

実データを使う場合は、J-Quants から取得した DataFrame の C 列に置き換えます。

SMA とクロスを検出する

短期・長期の SMA を計算し、両者の大小関係を 0/1 のフラグにしてから差分を取ります。

SHORT, LONG = 25, 75
df = close.to_frame()
df["sma_short"] = df["C"].rolling(SHORT).mean()
df["sma_long"] = df["C"].rolling(LONG).mean()
# 短期 > 長期 を 1、それ以外を 0 にする
df["above"] = (df["sma_short"] > df["sma_long"]).astype(int)
# diff() を取ると、+1 の日が GC、-1 の日が DC
df["cross"] = df["above"].diff()
df["gc"] = df["cross"] == 1
df["dc"] = df["cross"] == -1

diff() を使う書き方の利点は ベクトル化 できる点です。ループを書く必要がなく、数十万行でも一瞬で計算できます。

検出された日付を取り出します。

gc_dates = df.index[df["gc"]].tolist()
dc_dates = df.index[df["dc"]].tolist()
print("Golden cross:", gc_dates)
print("Dead cross:", dc_dates)

冒頭の SMA が NaN の期間は above0 扱いになるため、SMA が確定する日(75 日目以降)から判定を始めるのが安全です。

df_valid = df.dropna(subset=["sma_short", "sma_long"])
df_valid = df_valid.copy()
df_valid["above"] = (df_valid["sma_short"] > df_valid["sma_long"]).astype(int)
df_valid["cross"] = df_valid["above"].diff()
gc_dates = df_valid.index[df_valid["cross"] == 1].tolist()
dc_dates = df_valid.index[df_valid["cross"] == -1].tolist()

クロスを可視化する

クロスの位置にマーカーを重ねます。

import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(11, 5))
ax.plot(df.index, df["C"], label="C", color="black", linewidth=0.8)
ax.plot(df.index, df["sma_short"], label=f"SMA({SHORT})", color="tab:blue")
ax.plot(df.index, df["sma_long"], label=f"SMA({LONG})", color="tab:orange")
ax.scatter(gc_dates, df.loc[gc_dates, "sma_short"], marker="^", color="red", s=80, label="Golden Cross")
ax.scatter(dc_dates, df.loc[dc_dates, "sma_short"], marker="v", color="green", s=80, label="Dead Cross")
ax.set_title(f"SMA({SHORT}) / SMA({LONG}) Cross Detection")
ax.set_xlabel("Date")
ax.set_ylabel("Price")
ax.legend()
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("cross_detection.png", dpi=120)
plt.close(fig)
短期と長期 SMA の交差点にマーカーを重ねたクロス検出チャート

赤の三角がゴールデンクロス、緑の逆三角がデッドクロスです。

クロス後の N 日リターンを集計する

「ゴールデンクロスのあと N 日でどれくらい動いたか」を見ます。

def forward_return(close: pd.Series, dates: list, n: int = 20) -> pd.Series:
pos = close.index.get_indexer(dates)
pos = pos[pos != -1]
pos = pos[pos + n < len(close)]
fwd = close.values[pos + n] / close.values[pos] - 1
return pd.Series(fwd, index=close.index[pos], name=f"fwd_{n}d")
fwd_gc = forward_return(close, gc_dates, n=20)
fwd_dc = forward_return(close, dc_dates, n=20)
print(fwd_gc.describe())
print(fwd_dc.describe())

ランダムウォークで生成したサンプルでは、平均値はほぼゼロに収束します。「クロスが出た = 統計的に必ず上がる / 下がる」ではない ことを、自分の手元データで確認しておくと過信を避けられます。

機械的売買の限界

クロス検出は「ルールがシンプルで再現できる」点が長所ですが、機械的シグナルとしては次の弱点があります。

  • 遅行性: SMA は過去 N 日の平均なので、トレンド転換からシグナルまで時間差が出る
  • 横ばい相場での誤シグナル多発: 短期と長期が何度も入れ替わり、往復で取引コストが嵩む
  • N の最適化バイアス: 過去データに合うように N を選ぶと、未来データでは機能しないことが多い
  • 単一指標依存: 出来高・他指標(RSI / MACD など)を見ない判断は、根拠が一本足になる

過去のデータで良いクロス戦略を見つけたとしても、将来の成果を保証するものではありません。

「翌営業日の始値で約定」を必ず仕込む

シグナルは終値で確定するため、シグナル発生日の 当日終値で約定 すると先読みバイアス(データリーク)になります。実装上は次のように 1 日ずらします。

df_valid["position"] = df_valid["above"].shift(1)

このルールを徹底すると、バックテストの数字は地味になりますが、現実に近づきます。詳細は#11-3「移動平均クロス戦略を 5 銘柄でバックテスト」 / #10-8「データリーク・先読みバイアスを実例で防ぐ」 でも扱います。

生成AI へのプロンプト例

複数銘柄に対するクロス検出を一括で行う関数を依頼します。

入力 DataFrame は次の構造です:
- columns: Date (datetime64), Code (str), C (float)
- 1 行 1 (銘柄, 日付) のロング形式
Code ごとに SMA(25) / SMA(75) を計算し、
- 短期が長期を下から上抜けた日: cross_type = "GC"
- 上から下抜けた日: cross_type = "DC"
- それ以外: 行を残さない
を返す関数 detect_crosses(df) を書いてください。
要件:
- pandas 2.2 系
- Code ごとに日付昇順に整列してから計算
- SMA が NaN の期間は判定対象から除く
- 戻り値は Date, Code, cross_type, sma_short, sma_long の DataFrame

まとめ

  • ゴールデンクロス・デッドクロスは「短期 SMA と長期 SMA の交差」
  • pandas では (sma_short > sma_long).astype(int).diff() の値を使えばベクトル化で検出できる
  • マーカー描画は scatter で SMA 線上に置くと見やすい
  • 機械的シグナルとしては遅行性・誤検知・最適化バイアスの弱点がある
  • 約定タイミングは シグナル翌営業日 に固定して先読みを避ける