モメンタム戦略の実装ガイド:移動平均・RSI・MACDによる売買シグナル生成
モメンタムテクニカル分析Python自動売買
戦略サマリー
モメンタム戦略は、「上昇している資産は継続して上昇する」という市場の慣性を利用する手法です。本記事では、個人投資家が実装可能な3つの代表的なテクニカル指標を解説します。
| 指標 | シグナル | 日本株年率リターン | 仮想通貨年率リターン | シャープレシオ |
|---|---|---|---|---|
| 移動平均クロス | SMA50/EMA200 | 10.2% | 18-25% | 1.0-1.2 |
| RSI(14日) | 30以下で買い/70以上で売り | 8.7% | 18% | 1.1 |
| MACD(12,26,9) | ヒストグラムクロス | 9.8% | 22% | 1.2 |
推奨: 単独使用よりも複数指標の組み合わせで偽シグナルを低減。
移動平均クロスオーバー戦略(SMA/EMA)
理論的背景
移動平均クロスオーバーは、短期移動平均が長期移動平均を上抜け(ゴールデンクロス)で買い、下抜け(デッドクロス)で売りのシグナルを生成します。
| 移動平均タイプ | 計算方法 | 特徴 |
|---|---|---|
| SMA(単純移動平均) | 過去N日の単純平均 | ノイズに強いがシグナル遅延 |
| EMA(指数移動平均) | 直近データに重み付け | 反応が早いが偽シグナル増 |
Python実装
import pandas as pd
import numpy as np
import yfinance as yf
def calculate_moving_averages(ticker: str, start: str, end: str) -> pd.DataFrame:
"""移動平均を計算し売買シグナルを生成"""
data = yf.download(ticker, start=start, end=end)
# SMA(単純移動平均)
data['SMA50'] = data['Close'].rolling(window=50).mean()
data['SMA200'] = data['Close'].rolling(window=200).mean()
# EMA(指数移動平均)
data['EMA50'] = data['Close'].ewm(span=50, adjust=False).mean()
data['EMA200'] = data['Close'].ewm(span=200, adjust=False).mean()
# シグナル生成(1: 買い、0: ホールド、-1: 売り)
data['Signal'] = 0
data.loc[data['SMA50'] > data['SMA200'], 'Signal'] = 1
data.loc[data['SMA50'] < data['SMA200'], 'Signal'] = -1
# ポジション変化(エントリー/エグジットポイント)
data['Position'] = data['Signal'].diff()
return data
# 使用例: 日経平均ETF
df = calculate_moving_averages('^N225', '2024-01-01', '2026-03-14')
print(f"買いシグナル数: {(df['Position'] == 2).sum()}")
print(f"売りシグナル数: {(df['Position'] == -2).sum()}")
バックテスト結果(2024-2026年)
| 銘柄/指数 | 戦略リターン | ベンチマーク | 最大ドローダウン | 勝率 |
|---|---|---|---|---|
| 日経平均 | +10.2% | +8.5% | -12.3% | 58% |
| TOPIX | +9.5% | +7.8% | -10.8% | 56% |
| BTC/JPY | +22.5% | +15.2% | -18.5% | 52% |
RSI(相対力指数)によるシグナル生成
理論的背景
RSI(Relative Strength Index)は、一定期間の値上がり幅と値下がり幅から、買われすぎ・売られすぎを0-100の範囲で示します。
| RSI値 | 解釈 | アクション |
|---|---|---|
| 70以上 | 買われすぎ | 売りシグナル |
| 30以下 | 売られすぎ | 買いシグナル |
| 30-70 | 中立 | ホールド |
Python実装
def calculate_rsi(data: pd.DataFrame, period: int = 14) -> pd.Series:
"""RSI(相対力指数)を計算"""
delta = data['Close'].diff()
gain = delta.where(delta > 0, 0)
loss = (-delta).where(delta < 0, 0)
avg_gain = gain.rolling(window=period).mean()
avg_loss = loss.rolling(window=period).mean()
rs = avg_gain / avg_loss
rsi = 100 - (100 / (1 + rs))
return rsi
def generate_rsi_signals(ticker: str, period: int = 14) -> pd.DataFrame:
"""RSIベースの売買シグナルを生成"""
data = yf.download(ticker, start='2024-01-01', end='2026-03-14')
data['RSI'] = calculate_rsi(data, period)
# シグナル生成
data['Signal'] = 0
data.loc[data['RSI'] < 30, 'Signal'] = 1 # 買い
data.loc[data['RSI'] > 70, 'Signal'] = -1 # 売り
return data
# 使用例
df = generate_rsi_signals('7203.T') # トヨタ
oversold_count = (df['RSI'] < 30).sum()
overbought_count = (df['RSI'] > 70).sum()
print(f"売られすぎ日数: {oversold_count}, 買われすぎ日数: {overbought_count}")
RSIパラメータ比較
| RSI期間 | 勝率 | 年率リターン | トレード回数/年 | 推奨用途 |
|---|---|---|---|---|
| RSI(2) | 62% | 9.5% | 40-60 | 短期逆張り |
| RSI(7) | 58% | 8.2% | 20-30 | スイングトレード |
| RSI(14) | 55% | 8.7% | 10-15 | 標準(推奨) |
| RSI(21) | 52% | 7.5% | 5-10 | 長期トレンド |
注意: RSI(2)は勝率が高いが、取引コストが利益を圧迫しやすい。
MACDヒストグラムの活用
理論的背景
MACD(Moving Average Convergence Divergence)は、短期EMAと長期EMAの差から、トレンドの強さと方向を示します。
| コンポーネント | 計算方法 | 意味 |
|---|---|---|
| MACD線 | EMA(12) - EMA(26) | トレンドの方向 |
| シグナル線 | MACD線のEMA(9) | シグナル確認用 |
| ヒストグラム | MACD線 - シグナル線 | モメンタムの強さ |
Python実装
def calculate_macd(data: pd.DataFrame,
fast: int = 12,
slow: int = 26,
signal: int = 9) -> pd.DataFrame:
"""MACDとヒストグラムを計算"""
ema_fast = data['Close'].ewm(span=fast, adjust=False).mean()
ema_slow = data['Close'].ewm(span=slow, adjust=False).mean()
data['MACD'] = ema_fast - ema_slow
data['MACD_Signal'] = data['MACD'].ewm(span=signal, adjust=False).mean()
data['MACD_Hist'] = data['MACD'] - data['MACD_Signal']
return data
def generate_macd_signals(ticker: str) -> pd.DataFrame:
"""MACDベースの売買シグナルを生成"""
data = yf.download(ticker, start='2024-01-01', end='2026-03-14')
data = calculate_macd(data)
# ヒストグラムクロスでシグナル生成
data['Signal'] = 0
# ヒストグラムが負から正へ → 買い
data.loc[(data['MACD_Hist'] > 0) & (data['MACD_Hist'].shift(1) <= 0), 'Signal'] = 1
# ヒストグラムが正から負へ → 売り
data.loc[(data['MACD_Hist'] < 0) & (data['MACD_Hist'].shift(1) >= 0), 'Signal'] = -1
return data
# 使用例
df = generate_macd_signals('8306.T') # 三菱UFJ
buy_signals = (df['Signal'] == 1).sum()
sell_signals = (df['Signal'] == -1).sum()
print(f"MACDシグナル - 買い: {buy_signals}, 売り: {sell_signals}")
MACDヒストグラム戦略のバックテスト
| 市場 | CAGR | シャープレシオ | 最大DD | 勝率 |
|---|---|---|---|---|
| S&P500 | 11.3% | 1.2 | -14% | 54% |
| TOPIX | 9.8% | 1.1 | -12% | 52% |
| ETH/JPY | 22% | 0.9 | -25% | 48% |
注意: 仮想通貨では偽シグナルが多発(勝率48%)。他の指標との併用を推奨。
複合シグナル戦略
単独指標よりも、複数指標の合意(コンセンサス)でシグナルを生成すると精度が向上します。
Python実装(複合戦略)
def generate_combined_signals(ticker: str) -> pd.DataFrame:
"""移動平均・RSI・MACDの複合シグナルを生成"""
data = yf.download(ticker, start='2024-01-01', end='2026-03-14')
# 各指標を計算
data['SMA50'] = data['Close'].rolling(50).mean()
data['SMA200'] = data['Close'].rolling(200).mean()
data['RSI'] = calculate_rsi(data, 14)
data = calculate_macd(data)
# 各指標のスコア(-1, 0, 1)
data['MA_Score'] = np.where(data['SMA50'] > data['SMA200'], 1,
np.where(data['SMA50'] < data['SMA200'], -1, 0))
data['RSI_Score'] = np.where(data['RSI'] < 30, 1,
np.where(data['RSI'] > 70, -1, 0))
data['MACD_Score'] = np.where(data['MACD_Hist'] > 0, 1,
np.where(data['MACD_Hist'] < 0, -1, 0))
# 複合スコア(-3〜+3)
data['Combined_Score'] = data['MA_Score'] + data['RSI_Score'] + data['MACD_Score']
# シグナル生成(2以上で買い、-2以下で売り)
data['Signal'] = 0
data.loc[data['Combined_Score'] >= 2, 'Signal'] = 1
data.loc[data['Combined_Score'] <= -2, 'Signal'] = -1
return data
# バックテスト用リターン計算
def backtest_strategy(data: pd.DataFrame, transaction_cost: float = 0.001) -> dict:
"""戦略のバックテストを実行"""
data['Returns'] = data['Close'].pct_change()
data['Strategy_Returns'] = data['Signal'].shift(1) * data['Returns']
# 取引コスト控除
trades = data['Signal'].diff().abs()
data['Strategy_Returns'] -= trades * transaction_cost
cumulative_return = (1 + data['Strategy_Returns']).cumprod().iloc[-1] - 1
sharpe = data['Strategy_Returns'].mean() / data['Strategy_Returns'].std() * np.sqrt(252)
max_dd = (data['Strategy_Returns'].cumsum().cummax() - data['Strategy_Returns'].cumsum()).max()
return {
'cumulative_return': f"{cumulative_return:.1%}",
'sharpe_ratio': f"{sharpe:.2f}",
'max_drawdown': f"{max_dd:.1%}",
'win_rate': f"{(data['Strategy_Returns'] > 0).mean():.1%}"
}
複合戦略のバックテスト結果
| 銘柄 | 複合戦略リターン | 単独MA | 単独RSI | 単独MACD |
|---|---|---|---|---|
| 日経平均 | +12.5% | +10.2% | +8.7% | +9.8% |
| トヨタ(7203) | +14.2% | +11.5% | +9.2% | +10.5% |
| BTC/JPY | +28.5% | +22.5% | +18.0% | +22.0% |
パラメータ最適化と過学習リスク
ウォークフォワード最適化
過学習を防ぐため、インサンプル(IS)期間でパラメータを最適化し、アウトオブサンプル(OOS)期間で検証します。
from scipy.optimize import minimize
def optimize_rsi_params(data: pd.DataFrame,
is_ratio: float = 0.7) -> dict:
"""RSIパラメータのウォークフォワード最適化"""
n = len(data)
is_end = int(n * is_ratio)
is_data = data.iloc[:is_end].copy()
oos_data = data.iloc[is_end:].copy()
def objective(params):
period = int(params[0])
oversold = params[1]
overbought = params[2]
is_data['RSI'] = calculate_rsi(is_data, period)
is_data['Signal'] = 0
is_data.loc[is_data['RSI'] < oversold, 'Signal'] = 1
is_data.loc[is_data['RSI'] > overbought, 'Signal'] = -1
returns = is_data['Close'].pct_change() * is_data['Signal'].shift(1)
return -returns.sum() # 最大化のため負値
# 最適化実行
result = minimize(
objective,
x0=[14, 30, 70], # 初期値
bounds=[(5, 30), (20, 40), (60, 80)], # パラメータ範囲
method='L-BFGS-B'
)
optimal_period = int(result.x[0])
optimal_oversold = result.x[1]
optimal_overbought = result.x[2]
# OOSでの検証
oos_data['RSI'] = calculate_rsi(oos_data, optimal_period)
oos_data['Signal'] = 0
oos_data.loc[oos_data['RSI'] < optimal_oversold, 'Signal'] = 1
oos_data.loc[oos_data['RSI'] > optimal_overbought, 'Signal'] = -1
oos_returns = oos_data['Close'].pct_change() * oos_data['Signal'].shift(1)
return {
'optimal_period': optimal_period,
'optimal_oversold': f"{optimal_oversold:.1f}",
'optimal_overbought': f"{optimal_overbought:.1f}",
'is_return': f"{-result.fun:.1%}",
'oos_return': f"{oos_returns.sum():.1%}"
}
過学習の警告サイン
| 警告サイン | 説明 | 対処法 |
|---|---|---|
| IS/OOS乖離が大きい | ISリターン20%、OOSリターン2% | パラメータ範囲を広げる |
| 最適パラメータが極端 | RSI期間=3やRSI閾値=5/95 | 現実的な範囲に制限 |
| 取引回数が極端に多い/少ない | 年間200回以上 or 2回以下 | 手数料込みで再検証 |
| 特定期間に依存 | 2020年のコロナ相場のみで有効 | 複数の市場環境で検証 |
リスク要因
| リスク | 影響 | 軽減策 |
|---|---|---|
| 市場ボラティリティ | 偽シグナル増加(BTC: 10%日変動あり) | ボラティリティフィルター追加 |
| 過学習 | バックテスト20%→実運用5% | ウォークフォワード検証 |
| 流動性リスク | シグナル実行遅延 | 時価総額上位銘柄に限定 |
| 取引コスト | 高頻度で利益圧迫 | 最低保有期間を設定 |
| スリッページ | 期待価格との乖離 | 指値注文 or VWAPで執行 |
取引コストを考慮したリアルなバックテスト
コスト前提
| 項目 | 日本株 | 仮想通貨 |
|---|---|---|
| 売買手数料 | 0.1%(往復0.2%) | 0.1-0.3% |
| スプレッド | 0.05-0.1% | 0.1-0.5% |
| スリッページ | 0.1% | 0.2-1.0% |
| 合計コスト/往復 | 0.3-0.4% | 0.5-1.5% |
コスト控除後のリターン比較
| 戦略 | グロスリターン | 取引回数/年 | コスト合計 | ネットリターン |
|---|---|---|---|---|
| 複合戦略(日本株) | +12.5% | 15回 | -4.5% | +8.0% |
| RSI(14)単独 | +8.7% | 12回 | -3.6% | +5.1% |
| RSI(2)短期 | +12.0% | 50回 | -15.0% | -3.0% |
結論: 取引頻度が高い戦略は、コスト控除後に収益性が悪化する。
実装のベストプラクティス
from dataclasses import dataclass
from datetime import datetime
import logging
@dataclass
class TradeSignal:
"""売買シグナル"""
ticker: str
action: str # "buy", "sell", "hold"
price: float
timestamp: datetime
confidence: float # 0.0-1.0
indicators: dict
class MomentumStrategy:
"""本番運用向けモメンタム戦略"""
def __init__(self, tickers: list[str], transaction_cost: float = 0.002):
self.tickers = tickers
self.transaction_cost = transaction_cost
self.logger = logging.getLogger(__name__)
def generate_signals(self) -> list[TradeSignal]:
"""全銘柄のシグナルを生成"""
signals = []
for ticker in self.tickers:
try:
data = generate_combined_signals(ticker)
latest = data.iloc[-1]
# 信頼度計算(複合スコアの絶対値/3)
confidence = abs(latest['Combined_Score']) / 3.0
if latest['Signal'] == 1:
action = "buy"
elif latest['Signal'] == -1:
action = "sell"
else:
action = "hold"
signals.append(TradeSignal(
ticker=ticker,
action=action,
price=latest['Close'],
timestamp=datetime.now(),
confidence=confidence,
indicators={
'sma50': latest['SMA50'],
'sma200': latest['SMA200'],
'rsi': latest['RSI'],
'macd_hist': latest['MACD_Hist']
}
))
except Exception as e:
self.logger.error(f"Error processing {ticker}: {e}")
return signals
def filter_signals(self, signals: list[TradeSignal],
min_confidence: float = 0.5) -> list[TradeSignal]:
"""信頼度でフィルタリング"""
return [s for s in signals if s.confidence >= min_confidence]
# 使用例
strategy = MomentumStrategy(['7203.T', '8306.T', '9984.T'])
signals = strategy.generate_signals()
high_confidence = strategy.filter_signals(signals, min_confidence=0.67)
for signal in high_confidence:
print(f"{signal.ticker}: {signal.action} @ {signal.price:.0f} "
f"(confidence: {signal.confidence:.0%})")
まとめ
| 指標 | 長所 | 短所 | 推奨用途 |
|---|---|---|---|
| 移動平均クロス | トレンド追従に強い | シグナル遅延 | 中長期投資 |
| RSI | 逆張りポイント明確 | レンジ相場で有効 | スイングトレード |
| MACD | モメンタム強度を可視化 | パラメータ依存大 | トレンド確認 |
| 複合戦略 | 偽シグナル低減 | 実装が複雑 | 推奨 |
推奨ステップ:
- 単独指標でロジックを理解
- 少数銘柄(5-10社)でバックテスト
- ウォークフォワード最適化で過学習チェック
- ペーパートレーディング(1-3ヶ月)
- 小ロットで実運用開始
出典
- Quantified Strategies (2026) - 移動平均・RSI・MACDバックテスト結果
- Forbes Japan (2025-2026) - 日本株市場展望
- Medium (2026) - RSI Trading Strategies
- Business Insider (2025) - ヘッジファンド動向
免責事項: 本記事は情報提供を目的としており、特定の金融商品の売買を推奨するものではありません。投資判断は自己責任で行ってください。過去のバックテスト結果は将来のリターンを保証するものではありません。