【Python】RSI×MACDの組み合わせ戦略で日本株製造業を狙う【コード付き】

子供が生まれて以来、株価チャートを眺める時間が激減した。でもその分「一目でエントリーの根拠を判断できる仕組み」が必要になって、たどり着いたのがRSIとMACDの組み合わせ戦略だった。単体で使うと「あれ、これフィルター弱くない?」ってなることが多かったので、二つ重ねてみたら意外とスッキリした。。。

なぜ僕がRSI×MACD組み合わせに行き着いたか

以前、MACDだけを使ったシンプルなゴールデンクロス戦略をバックテストしたとき、「シグナルが多すぎてほとんどノイズ」という結果に頭を抱えた。特にトヨタや日立のような製造メーカー株は、決算前後にボラが跳ねる局面が多くて、MACDのゴールデンクロスに素直についていくとやられる場面がちょくちょく出てくる。

そこで「RSIで売られすぎ・買われすぎを確認してからMACDのシグナルを使う」という二段構えを試してみた。結果的に取引頻度は下がるけど、1トレードあたりのヒット率が体感で上がった気がする(あくまで体感。バックテストのデータは後述)。

RSIとMACDのおさらい

知ってる人は飛ばしてもOKだけど、念のため整理しておく。

RSI(Relative Strength Index)は0〜100の値を取る「売られすぎ・買われすぎ」を示すオシレーター系指標。一般的に30以下が売られすぎ、70以上が買われすぎの目安とされている。計算式は「直近N日間の上昇幅の平均 ÷ (上昇幅の平均 + 下落幅の平均) × 100」で、デフォルトのNは14日。

MACD(Moving Average Convergence Divergence)はトレンド系指標で、短期EMA(12日)と長期EMA(26日)の差から計算する。さらにその9日EMA(シグナル線)との交差で買い売りを判断する。ゴールデンクロス(MACDラインがシグナル線を上抜け)→ 買いシグナル、デッドクロス → 売りシグナル、という使い方が基本。

この二つの特性をまとめると、RSIは「今の価格水準の過熱感」を測り、MACDは「トレンドの転換タイミング」を測る。つまり役割分担がきれいに分かれているから組み合わせると相性がいい。

戦略ロジックの設計

今回実装するロジックはシンプルにこう定義する:

買いエントリー条件:RSIが40以下(売られすぎ寄り)かつMACDラインがシグナル線をゴールデンクロスした翌日の始値で買い。
売りエグジット条件:RSIが65以上(やや過熱)またはMACDがデッドクロス → 翌日始値で売り。
損切り:エントリー価格から-5%到達で即日終値で強制売り。

RSI閾値を「30」じゃなくて「40」にしてるのは、日本の製造メーカー株はROEが高い優良銘柄でも突然売り込まれることがあって、30まで待ってると出遅れることが多かったから。このあたりはお好みで調整してほしい。

Pythonで実装してみる

使うライブラリは yfinancepandasnumpymatplotlib。これだけあれば動く。対象銘柄はトヨタ自動車(7203.T)で試してみた。

import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# ===== データ取得 =====
ticker = "7203.T"  # トヨタ自動車
df = yf.download(ticker, start="2022-01-01", end="2026-05-31", auto_adjust=True)
df = df[["Open", "High", "Low", "Close", "Volume"]].dropna()

# ===== RSI計算(14日) =====
def calc_rsi(series, period=14):
    delta = series.diff()
    gain = delta.clip(lower=0)
    loss = -delta.clip(upper=0)
    avg_gain = gain.ewm(com=period - 1, min_periods=period).mean()
    avg_loss = loss.ewm(com=period - 1, min_periods=period).mean()
    rs = avg_gain / avg_loss
    return 100 - (100 / (1 + rs))

df["RSI"] = calc_rsi(df["Close"])

# ===== MACD計算 =====
ema12 = df["Close"].ewm(span=12, adjust=False).mean()
ema26 = df["Close"].ewm(span=26, adjust=False).mean()
df["MACD"] = ema12 - ema26
df["Signal"] = df["MACD"].ewm(span=9, adjust=False).mean()
df["MACD_diff"] = df["MACD"] - df["Signal"]

# ===== エントリー・エグジットシグナル =====
# MACD ゴールデンクロス: 前日がMACDSignal
df["GoldenCross"] = (df["MACD_diff"].shift(1) < 0) & (df["MACD_diff"] >= 0)
df["DeadCross"]   = (df["MACD_diff"].shift(1) > 0) & (df["MACD_diff"] <= 0)

# 買いシグナル: RSI<=40 かつ ゴールデンクロス
df["BuySignal"]  = (df["RSI"] <= 40) & df["GoldenCross"]
# 売りシグナル: RSI>=65 または デッドクロス
df["SellSignal"] = (df["RSI"] >= 65) | df["DeadCross"]

# ===== バックテスト =====
capital = 1_000_000  # 初期資金100万円
position = 0
entry_price = 0
trades = []

for i in range(1, len(df)):
    row = df.iloc[i]
    prev = df.iloc[i - 1]
    price = row["Open"]  # 翌日始値で執行
    date  = df.index[i]

    if position == 0 and prev["BuySignal"]:
        # エントリー(100株単位)
        shares = int(capital * 0.95 / price / 100) * 100
        if shares > 0:
            position = shares
            entry_price = price
            capital -= shares * price

    elif position > 0:
        stop_hit = price <= entry_price * 0.95
        if prev["SellSignal"] or stop_hit:
            # エグジット
            exit_price = price
            pnl = (exit_price - entry_price) * position
            capital += position * exit_price
            trades.append({
                "date": date,
                "entry": entry_price,
                "exit": exit_price,
                "shares": position,
                "pnl": pnl,
                "stop": stop_hit
            })
            position = 0
            entry_price = 0

# まだポジションが残っている場合は最終日で決済
if position > 0:
    exit_price = df["Close"].iloc[-1]
    pnl = (exit_price - entry_price) * position
    capital += position * exit_price
    trades.append({
        "date": df.index[-1],
        "entry": entry_price,
        "exit": exit_price,
        "shares": position,
        "pnl": pnl,
        "stop": False
    })

result_df = pd.DataFrame(trades)
total_pnl  = result_df["pnl"].sum() if len(result_df) > 0 else 0
win_rate   = (result_df["pnl"] > 0).mean() * 100 if len(result_df) > 0 else 0

print(f"総取引回数: {len(result_df)}")
print(f"勝率: {win_rate:.1f}%")
print(f"総損益: {total_pnl:,.0f}円")
print(f"最終資産: {capital:,.0f}円")

このコードを実行すると、バックテスト期間(2022年〜2026年5月)でのトレード一覧と最終損益が出力される。もちろんこれは過去のデータを使ったシミュレーションなので、未来の収益を保証するものじゃないことは念押ししておく。

結果の確認と注意点

バックテストでよく起きる罠として、「スリッページ(指値と約定値のズレ)」と「手数料」を無視してしまうことがある。今回のコードは翌日の始値で約定する想定にしているが、実際には寄り付きに注文が殺到する銘柄だとそれよりも不利な価格になることも。

また、製造メーカー株は決算シーズン(1・3・7・10月頃)に大きく動くことが多い。決算発表をまたぐポジションはリスクが大きいので、決算日の数日前にはフラットにする運用も検討してほしい。

RSIの閾値(40 / 65)やMACDのパラメータ(12・26・9)は、対象銘柄や市場環境によって最適値が変わる。ただし、パラメータをいじりすぎると「過去データに過剰適応(過学習)」してしまうので、ある程度シンプルに保つのがコツ。この過学習対策については別の記事でしっかり書く予定。

まとめ

RSIで売られすぎ水準を確認してからMACDのゴールデンクロスを拾うという戦略は、シグナルが絞られる分「ここだ!」という場面に集中できる。テクニカル指標を一つ増やすだけでエントリー根拠が補強されるのは、初心者にもわかりやすいアプローチだと思う。

個人的には、この戦略はトレンドが出やすい日経平均連動型の大型製造メーカー(トヨタ・日立・デンソーあたり)との相性が良いと感じている。次はこの戦略に「決算跨ぎ回避フィルター」を追加して、どれくらい結果が変わるか試してみたい。

タイトルとURLをコピーしました