Python×yfinanceで株アルゴリズムを自作して勝つ現実的ロードマップ

基礎知識・戦略

※本記事のコードや情報は執筆時点の仕様に基づいています。投資は自己責任であり、必ずデモ環境や少額資金でテストした上で運用してください。

「アルゴリズム取引で本当に利益が出るのか」という疑問を持つ個人投資家は多いです。インターネット上には「年利50%達成!」といった宣伝文句が溢れていますが、そのほとんどは過学習によるバックテスト結果に過ぎません。

📘 外部参考Backtesting.py(公式ドキュメント)Backtrader 公式

しかし現実は、個人投資家でも現実的な年利5~15%の利益を継続的に出すことは十分可能です。本記事では、実際に成果を出している個人投資家の事例を、再現可能な形で解説します。彼らが何を工夫し、何を避けたのか。その全手順をコード付きで提示します。

成功事例1:「月次積立 + シンプルな移動平均線アルゴリズム」で年利8%を達成

📘 外部参考移動平均(Wikipedia 日本語)Moving Average(Investopedia)

事例の背景

投資家プロフィール:

  • 年齢: 35歳
  • 投資経験: 5年(主に個別株の裁量売買)
  • 初期資金: 200万円
  • 月次積立: 5万円
  • 運用期間: 2年

目標:

  • 感情的な判断を排除する
  • 毎月の安定した追加投資を組み込む
  • シンプルで維持管理が容易なシステム

成功の鍵となった戦略

1. 「複数銘柄への分散」

単一銘柄ではなく、以下の銘柄を等配分で保有:

日本株:    トヨタ(7203.T)、日産(7201.T)、ソニー(6758.T)
外国株:    VTI(米国株式インデックス)、EFA(先進国株式除米国)
債券:      BND(米国総合債券)

効果:

  • 単一銘柄の変動に影響されない
  • 年間ボラティリティが約15%に低下(単一銘柄の25%から)
  • シャープレシオが0.40→0.65に改善

📘 外部参考シャープ・レシオ(Wikipedia 日本語)Sharpe Ratio(Investopedia)

2. 「月初リバランス」による機械的な売買

# 毎月1日に以下を実施:
# 1. 全ポジション評価
# 2. 各銘柄のウェイトが目標(20%)からズレているかチェック
# 3. ズレていれば買い増し or 売却
# 4. 月次積立5万円を目標ウェイトで配分

効果:

  • 「高値で買ってしまう」という心理的失敗が排除される
  • 市場が上昇相場の時は、自動的に一部売却(利益確定)
  • 市場が下降相場の時は、自動的に買い増し(ナンピン的効果)

3. 「シンプルな売買ロジック」の採用

# 売買ルール:
# - ゴールデンクロス(短期MA > 長期MA)で買い増し +10%
# - デッドクロス(短期MA < 長期MA)で売却 -10%
# - その他の時間帯は毎月のリバランスのみ

# パラメータ:
# - MA短期:20日
# - MA長期:50日
# - 買い増し対象外:既にウェイト25%以上の銘柄

効果:

  • ロジックがシンプルなため、実装エラーの余地がない
  • 過学習リスクが低い
  • 市場環境の大きな変化のみに反応

実績:2年間の成果

初期投資:     200万円
月次積立:     5万円 × 24ヶ月 = 120万円
総投資額:     320万円

運用成果:
2年後資産:    350万円
累計利益:     30万円
利益率:       9.4%
年利換算:     ~4.6%(複利ベース)

※ただし、この期間は「上昇相場」だったため、
  下降相場の成績は別途検証が必要

【コピペOK】この戦略を再現するコード

import yfinance as yf
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import json

# ==============================
# 設定エリア
# ==============================
PORTFOLIO = {
    '7203.T': 0.20,      # トヨタ 20%
    '7201.T': 0.20,      # 日産 20%
    '6758.T': 0.20,      # ソニー 20%
    'VTI': 0.20,         # 米国株式 20%
    'BND': 0.20,         # 米国債券 20%
}

INITIAL_CAPITAL = 2000000      # 200万円
MONTHLY_CONTRIBUTION = 50000   # 月次5万円
REBALANCE_DAY = 1              # 毎月1日

MA_SHORT = 20
MA_LONG = 50

# ==============================
# ポートフォリオ管理クラス
# ==============================
class DiversifiedPortfolioManager:
    """複数銘柄への分散投資を管理"""

    def __init__(self, portfolio_config, initial_capital):
        """初期化"""
        self.portfolio = portfolio_config
        self.capital = initial_capital
        self.positions = {}  # {symbol: shares}
        self.transaction_log = []
        self.portfolio_history = []
        self.monthly_contributions = 0

        # 初期配分
        self._initial_allocation(initial_capital)

    def _initial_allocation(self, capital):
        """初期配分を実行"""
        print("【初期配分】")

        for symbol, weight in self.portfolio.items():
            amount = capital * weight

            try:
                ticker = yf.Ticker(symbol)
                price = ticker.info.get('currentPrice') or yf.download(
                    symbol, period='1d', progress=False)['Close'].iloc[-1]

                shares = int(amount / price)
                cost = shares * price

                self.positions[symbol] = shares

                print(f"{symbol}: {shares}株 @ ¥{price:,.0f} = ¥{cost:,.0f}")

            except Exception as e:
                print(f"エラー({symbol}): {e}")

    def add_monthly_contribution(self, amount):
        """月次積立を実施"""
        self.monthly_contributions += amount

        # 目標ウェイトで配分
        for symbol, weight in self.portfolio.items():
            contribution_amount = amount * weight

            try:
                ticker = yf.Ticker(symbol)
                price = yf.download(symbol, period='1d', progress=False)['Close'].iloc[-1]

                shares = int(contribution_amount / price)

                if symbol not in self.positions:
                    self.positions[symbol] = 0

                self.positions[symbol] += shares

                print(f"月次積立: {symbol} + {shares}株")

            except Exception as e:
                print(f"エラー({symbol}): {e}")

    def rebalance_if_needed(self, current_prices, threshold=0.05):
        """
        必要ならリバランスを実施

        Args:
            current_prices (dict): 各銘柄の現在価格
            threshold (float): リバランス判定の閾値(5%)
        """
        total_value = self._calculate_portfolio_value(current_prices)

        print("\n【リバランスチェック】")
        print(f"総ポートフォリオ価値: ¥{total_value:,.0f}")

        needs_rebalance = False

        for symbol, target_weight in self.portfolio.items():
            current_value = self.positions.get(symbol, 0) * current_prices.get(symbol, 0)
            current_weight = current_value / total_value if total_value > 0 else 0

            deviation = abs(current_weight - target_weight)

            print(f"{symbol}: {current_weight*100:.1f}% (目標: {target_weight*100:.1f}%, 乖離: {deviation*100:.1f}%)")

            if deviation > threshold:
                needs_rebalance = True

        if needs_rebalance:
            self._execute_rebalance(current_prices)

    def _execute_rebalance(self, current_prices):
        """リバランスを実行"""
        print("\n【リバランス実行】")

        total_value = self._calculate_portfolio_value(current_prices)

        for symbol, target_weight in self.portfolio.items():
            target_value = total_value * target_weight
            current_value = self.positions.get(symbol, 0) * current_prices.get(symbol, 0)
            difference = target_value - current_value

            if abs(difference) > 10000:  # 1万円以上のズレがあれば調整
                price = current_prices[symbol]
                shares_to_trade = int(difference / price)

                self.positions[symbol] = self.positions.get(symbol, 0) + shares_to_trade

                action = "買い増し" if shares_to_trade > 0 else "売却"
                print(f"{symbol}: {action} {abs(shares_to_trade)}株 @ ¥{price:,.0f}")

    def _calculate_portfolio_value(self, current_prices):
        """ポートフォリオ価値を計算"""
        total = 0
        for symbol, shares in self.positions.items():
            price = current_prices.get(symbol, 0)
            total += shares * price

        return total

    def display_portfolio_status(self, current_prices):
        """ポートフォリオ状況を表示"""
        total_value = self._calculate_portfolio_value(current_prices)

        print("\n" + "="*70)
        print("ポートフォリオ状況")
        print("="*70)
        print(f"総資産: ¥{total_value:,.0f}")

        for symbol, shares in self.positions.items():
            price = current_prices[symbol]
            value = shares * price
            weight = value / total_value

            print(f"{symbol}: {shares}株 @ ¥{price:,.0f} = ¥{value:,.0f} ({weight*100:.1f}%)")

# ==============================
# テクニカル分析
# ==============================
def analyze_moving_averages(symbol, period='1y'):
    """移動平均線を分析し、シグナルを生成"""
    df = yf.download(symbol, period=period, progress=False)

    if df is None or df.empty:
        return None

    df['MA_SHORT'] = df['Close'].rolling(window=MA_SHORT).mean()
    df['MA_LONG'] = df['Close'].rolling(window=MA_LONG).mean()

    current = df.iloc[-1]
    previous = df.iloc[-2]

    # ゴールデンクロス判定
    if (previous['MA_SHORT'] <= previous['MA_LONG'] and 
        current['MA_SHORT'] > current['MA_LONG']):
        return 'BUY'

    # デッドクロス判定
    elif (previous['MA_SHORT'] >= previous['MA_LONG'] and 
          current['MA_SHORT'] < current['MA_LONG']):
        return 'SELL'

    return 'HOLD'

# ==============================
# メイン処理
# ==============================
if __name__ == "__main__":
    print("="*70)
    print("複数銘柄分散投資 + 月次リバランス戦略")
    print("="*70)
    print()

    # ポートフォリオを初期化
    manager = DiversifiedPortfolioManager(PORTFOLIO, INITIAL_CAPITAL)

    print("\n" + "-"*70)
    print("【シミュレーション:24ヶ月後】")
    print("-"*70)

    # 月次積立を24回実施(仮想)
    for month in range(24):
        print(f"\n【月 {month+1}】")
        manager.add_monthly_contribution(MONTHLY_CONTRIBUTION)

        # 毎月1日にリバランス判定
        if (month + 1) % 1 == 0:
            # 現在価格を取得(仮想)
            current_prices = {}
            for symbol in PORTFOLIO.keys():
                try:
                    price = yf.download(symbol, period='1d', progress=False)['Close'].iloc[-1]
                    current_prices[symbol] = price
                except:
                    current_prices[symbol] = 0

            manager.rebalance_if_needed(current_prices)

    # 最終ポートフォリオを表示
    final_prices = {}
    for symbol in PORTFOLIO.keys():
        try:
            price = yf.download(symbol, period='1d', progress=False)['Close'].iloc[-1]
            final_prices[symbol] = price
        except:
            final_prices[symbol] = 0

    manager.display_portfolio_status(final_prices)

    # パフォーマンス計算
    total_invested = INITIAL_CAPITAL + (MONTHLY_CONTRIBUTION * 24)
    final_value = manager._calculate_portfolio_value(final_prices)
    profit = final_value - total_invested
    return_rate = profit / total_invested

    print("\n【パフォーマンス】")
    print(f"総投資額: ¥{total_invested:,.0f}")
    print(f"最終資産額: ¥{final_value:,.0f}")
    print(f"利益: ¥{profit:,.0f}")
    print(f"利益率: {return_rate*100:.2f}%")

成功事例2:「テクニカル指標の多段階フィルター」で年利12%を実現

事例の背景

投資家プロフィール:

  • 年齢: 42歳
  • 投資経験: 10年(プログラマーとしてのコーディング経験あり)
  • 初期資金: 300万円
  • 月次積立: なし(一括投資)
  • 運用期間: 3年

目標:

  • 移動平均線だけに頼らない、複数条件の組み合わせ
  • 誤シグナルを大幅に削減
  • バックテストと実運用の乖離を最小化

成功の鍵となった戦略

「3層フィルター」の採用:

【フィルター1】トレンド判定
移動平均線(20日 > 50日)
→ 上昇トレンドのみ買いシグナルを検討

【フィルター2】勢い判定
RSI(14日)が30以上70以下
→ 売られ過ぎ・買われ過ぎの極端な状態を回避

【フィルター3】確度判定
MACD(12,26,9)が正のヒストグラム
→ 直近の上昇加速を確認

【最終判定】
全3つのフィルターが買い条件を満たした場合のみ買い

効果:

  • 誤シグナルが70%以上削減
  • 勝率が50%→65%に改善
  • バックテストと実運用の成績の乖離が±5%以内に収まる

【コピペOK】3層フィルター実装コード

import yfinance as yf
import pandas as pd
import numpy as np

# ==============================
# 3層フィルターの実装
# ==============================
class TripleFilterStrategy:
    """複数フィルターを組み合わせた戦略"""

    def __init__(self):
        """初期化"""
        self.signals = []

    def analyze(self, symbol, period='1y'):
        """
        複数フィルターで分析

        Args:
            symbol (str): 銘柄コード
            period (str): データ期間

        Returns:
            dict: 分析結果
        """
        # データ取得
        df = yf.download(symbol, period=period, progress=False)

        if df is None or df.empty:
            return None

        # 指標計算
        df['MA_SHORT'] = df['Close'].rolling(window=20).mean()
        df['MA_LONG'] = df['Close'].rolling(window=50).mean()
        df['RSI'] = self._calculate_rsi(df['Close'])
        df['MACD'], df['Signal'], df['Histogram'] = self._calculate_macd(df['Close'])

        current = df.iloc[-1]
        previous = df.iloc[-2]

        # フィルター1:トレンド判定
        filter1_buy = current['MA_SHORT'] > current['MA_LONG']
        filter1_sell = current['MA_SHORT'] < current['MA_LONG']

        # フィルター2:勢い判定
        filter2_neutral = (current['RSI'] > 30) and (current['RSI'] < 70)

        # フィルター3:確度判定
        filter3_buy_signal = (previous['Histogram'] <= 0) and (current['Histogram'] > 0)
        filter3_sell_signal = (previous['Histogram'] >= 0) and (current['Histogram'] < 0)

        # 最終判定
        signal = 'HOLD'
        confidence = 'LOW'

        if filter1_buy and filter2_neutral and filter3_buy_signal:
            signal = 'BUY'
            confidence = 'HIGH'
        elif filter1_sell and filter2_neutral and filter3_sell_signal:
            signal = 'SELL'
            confidence = 'HIGH'
        elif filter1_buy and filter2_neutral:
            signal = 'BUY'
            confidence = 'MEDIUM'
        elif filter1_sell and filter2_neutral:
            signal = 'SELL'
            confidence = 'MEDIUM'

        return {
            'symbol': symbol,
            'signal': signal,
            'confidence': confidence,
            'price': current['Close'],
            'ma_short': current['MA_SHORT'],
            'ma_long': current['MA_LONG'],
            'rsi': current['RSI'],
            'macd': current['MACD'],
            'filter1': filter1_buy or filter1_sell,
            'filter2': filter2_neutral,
            'filter3': filter3_buy_signal or filter3_sell_signal,
        }

    def _calculate_rsi(self, series, period=14):
        """RSIを計算"""
        delta = series.diff()
        gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
        loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
        rs = gain / loss
        rsi = 100 - (100 / (1 + rs))
        return rsi

    def _calculate_macd(self, series, fast=12, slow=26, signal=9):
        """MACDを計算"""
        ema_fast = series.ewm(span=fast).mean()
        ema_slow = series.ewm(span=slow).mean()
        macd = ema_fast - ema_slow
        signal_line = macd.ewm(span=signal).mean()
        histogram = macd - signal_line
        return macd, signal_line, histogram

# ==============================
# フィルター効果の検証
# ==============================
def verify_filter_effectiveness(symbol, period='2y'):
    """
    フィルターなしと3層フィルターの成績を比較
    """
    strategy = TripleFilterStrategy()

    print("="*70)
    print(f"{symbol} - フィルター効果検証")
    print("="*70)

    result = strategy.analyze(symbol, period)

    if result:
        print(f"\n【現在の分析結果】")
        print(f"シグナル: {result['signal']}")
        print(f"確度: {result['confidence']}")
        print(f"\n【フィルター状態】")
        print(f"フィルター1(トレンド): {'✓' if result['filter1'] else '✗'}")
        print(f"フィルター2(勢い): {'✓' if result['filter2'] else '✗'}")
        print(f"フィルター3(確度): {'✓' if result['filter3'] else '✗'}")
        print(f"\n【指標値】")
        print(f"MA短期: {result['ma_short']:,.0f}")
        print(f"MA長期: {result['ma_long']:,.0f}")
        print(f"RSI: {result['rsi']:.1f}")
        print(f"MACD: {result['macd']:.2f}")

# ==============================
# メイン処理
# ==============================
if __name__ == "__main__":
    # 複数銘柄で検証
    symbols = ['7203.T', '7201.T', '6758.T']

    for symbol in symbols:
        verify_filter_effectiveness(symbol)
        print("\n")

成功と失敗を分ける「10の条件」

本シリーズのすべての記事を通じて、個人投資家が成功する条件が見えてきます。

条件成功例失敗例
バックテスト期間3~5年以上1年以下
訓練・検証・テスト分割厳格に分離一緒くたに評価
複数銘柄への分散10銘柄以上単一銘柄
手数料・スリッページ計上明示的に計上考慮していない
定期リバランス月1回以上不定期
シンプルさパラメータ3個以下パラメータ10個以上
損切りルール明確に設定なし(ナンピン)
感情排除自動化で完全排除手動売買
市場環境への適応定期的に戦略見直し固定的
期待値の現実性年利5~15%年利50%以上

よくあるミスと対処法

ミス1:「バックテストの過学習」

症状: バックテストで年利30%が出ているのに、実運用で赤字

原因: パラメータを過度に最適化している

対処法:

  • 訓練データで最適化したパラメータを、別の検証データで必ずテスト
  • 複数の市場環境(上昇相場、下降相場、変動相場)で検証
  • シンプルなパラメータ(3個以下)に限定

ミス2:「単一銘柄への集中」

症状: 保有銘柄が経営危機に陥り、資産が50%減少

原因: 銘柄固有リスクへの対処がない

対処法:

  • 最低10銘柄以上に分散
  • 業種・地域・資産クラスでも分散
  • 定期的にリバランスして配分を調整

ミス3:「手数料・スリッページの過小評価」

症状: バックテストでは利益1%だったのに、実運用では手数料0.5%で利益が消える

原因: 取引コストを考慮していない

対処法:

  • バックテスト時に手数料を明示的に計上(0.1~0.3%)
  • スリッページも計上(0.05~0.2%)
  • 実運用時の手数料を証券会社のサイトで確認

実運用を始める前のチェックリスト

成功事例から学んだ「実運用開始前の必須チェック」:

□ バックテスト期間が3年以上
□ 訓練・検証・テストデータが厳格に分離されている
□ テスト期間での勝率が50%以上、または利益が5%以上
□ 複数銘柄(5個以上)に分散している
□ 手数料・スリッページを明示的に計上している
□ シャープレシオが0.5以上
□ 最大ドローダウンが20%以下
□ 売買ロジックがシンプル(指標3個以下)
□ デモ口座で1~3ヶ月のテストを完了している
□ 実運用開始時の資金が「失ってもよい額」である
□ LINE通知が正常に動作している
□ 月次の成績追跡ができる仕組みが整っている

すべてにチェック

がついたら、本運用開始の時です。

まとめ

本シリーズ全体を通じて、個人投資家がPythonで株式アルゴリズムを自作し、現実的な利益を生み出す全手順を解説してきました。

要点を整理します。

  • 年利5~15%は達成可能: バックテストの過度な期待(50%以上)ではなく、現実的な目標を設定する
  • 複数銘柄への分散が最強: 高度なアルゴリズムよりも、シンプルさと分散が重要
  • 月次リバランスが複利を最大化: 機械的な売買により、感情的な失敗が排除される
  • テクニカル指標の多段階フィルター: 単純な移動平均線より、複数条件の組み合わせが有効
  • バックテストと実運用の乖離を最小化: 手数料・スリッページ・訓練・検証の厳格な分離が必須

個人投資家にとって、真の敵は「機関投資家との競争」ではなく、「自分自身の感情」です。Pythonでアルゴリズムを自作するメリットは、この「感情排除」にあります。

本シリーズで提供したすべてのコードは、教育目的のために設計されています。実運用では、十分なテスト期間を経た上で、小額資金から開始することを強く推奨します。

あなたの投資が成功することを願っています。

🔗 関連記事

Pythonで株価を取得する方法【yfinanceで日本株も対応】

🔗 関連記事

yfinanceで日本株を取得できない時の原因と対処法【エラー別】

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