【証券会社API徹底比較】株アルゴリズム自作に使えるのはどこ?Python対応の現実的ロードマップ

準備・環境構築

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

「株式投資で資産を増やしたい」という願いは誰もが持っていますが、その実現方法は人によって大きく異なります。ロボアドバイザーに任せるのか、自分で運用するのか、それとも両者を組み合わせるのか。本シリーズを通じて、個人投資家が「自分専用のアルゴリズム」を構築できることを示してきました。

しかし、アルゴリズムを「作る」ことと、それを「長期間安定的に運用する」ことは別問題です。バックテストで素晴らしい成績が出ても、実運用で同じ成果が得られるとは限りません。また、運用開始から数年経つと、市場環境の変化に対応して戦略を調整する必要が生じます。

本記事では、単なる「アルゴリズムの構築」ではなく、「自動化されたシステムで資産を長期運用する」という全体的なフレームワークを提示します。データ分析から売買シグナル生成、リバランス管理、パフォーマンス追跡、そして戦略の動的調整まで、実務的な資産運用システムの完全な設計図を示します。

長期資産運用において重要な5つの原則

Pythonでアルゴリズムを開発する前に、長期資産運用の本質的な原則を理解することが重要です。

原則1:「複利」と「時間」が最強の武器

投資において最も強力なのは、手数料0.1%の効率化ではなく、時間です。以下のシミュレーションをご覧ください。

初期投資:100万円
年間リターン:5%(税引き前)

年数 | 手数料0% | 手数料1% | 差分
-----|---------|---------|-----
10年 | 163万  | 145万  | -18万
20年 | 265万  | 210万  | -55万
30年 | 432万  | 305万  | -127万
40年 | 704万  | 442万  | -262万

この表から分かる通り、時間が長くなるほど、わずかな手数料が複利で蓄積され、莫大な機会損失を生み出します。個人でアルゴリズムを構築するメリットは、まさにこの手数料ゼロを30年~40年という長期間維持することにあります。

原則2:「勝つ」のではなく「負けない」ことが重要

多くの投資家が誤解していることが「勝率の高さ」です。実際には、以下のような現実があります。

条件期待値
勝率50%、勝つ時の利益+1%、負ける時の損失-1%0%(コストで赤字)
勝率50%、勝つ時の利益+1.5%、負ける時の損失-1%+0.25%(取引コスト控除後)
勝率45%、勝つ時の利益+1%、負ける時の損失-0.5%+0.20%(取引コスト控除後)

重要なのは「どれだけ負けを小さくするか」です。損失を0.5%に抑えれば、勝率が50%でなくても、期待値は正になります。

原則3:「分散」は無料のリスク削減

1つの銘柄に集中投資することのリスクは計り知れません。一方、複数銘柄に分散することで、同じリターン期待値でリスクを削減できます。これは「無料」です。

ポートフォリオ期待リターンボラティリティシャープレシオ
単一銘柄5%25%0.20
10銘柄均等配分5%12%0.42
20銘柄均等配分5%9%0.56
業種別+資産別5%7%0.71

原則4:「リバランス」が複利を最大化させる

ポートフォリオを構築した後は、定期的なリバランスが重要です。これは「売買」ではなく、資産配分を目標比率に戻すだけですが、長期的には大きなリターン向上につながります。

原則5:「感情」を排除することが最大の敵

多くの個人投資家が負ける理由は「アルゴリズムが悪い」のではなく、「感情的な判断が介入する」からです。アルゴリズムを自動化することの最大のメリットは、この感情排除にあります。

【コピペOK】長期資産運用システムの完全実装

では、以上の原則に基づいた「長期資産運用システム」を実装します。このシステムは、複数銘柄の監視、売買シグナル生成、自動リバランス、パフォーマンス追跡までを統合しています。

import yfinance as yf
import pandas as pd
import numpy as np
import json
import logging
from datetime import datetime, timedelta
from pathlib import Path
import requests

# ==============================
# ロギング設定
# ==============================
logging.basicConfig(
    level=logging.INFO,
    format='[%(asctime)s] %(levelname)s: %(message)s',
    handlers=[
        logging.FileHandler('asset_management.log'),
        logging.StreamHandler()
    ]
)

# ==============================
# システム設定
# ==============================
class AssetManagementConfig:
    """資産運用システム設定"""

    # ポートフォリオ構成(目標配分)
    PORTFOLIO = {
        'JP_STOCKS': {
            'symbols': ['7203.T', '7201.T', '8058.T', '9984.T', '6758.T'],
            'target_weight': 0.40,
            'rebalance_threshold': 0.05,
        },
        'FOREIGN_STOCKS': {
            'symbols': ['VTI', 'EFA', 'SCHF'],
            'target_weight': 0.40,
            'rebalance_threshold': 0.05,
        },
        'BONDS': {
            'symbols': ['BND', 'AGG'],
            'target_weight': 0.15,
            'rebalance_threshold': 0.05,
        },
        'CASH': {
            'target_weight': 0.05,
        },
    }

    # 運用パラメータ
    INITIAL_CAPITAL = 1000000         # 初期資金100万円
    MONTHLY_CONTRIBUTION = 50000      # 月次積立額

    # テクニカル指標パラメータ
    MA_SHORT = 20
    MA_LONG = 50
    RSI_PERIOD = 14

    # リバランス設定
    REBALANCE_FREQUENCY = 30          # 日数(30日ごと)
    REBALANCE_DAY_OF_MONTH = 1        # 月初にリバランス

    # LINE通知設定
    LINE_TOKEN = "YOUR_LINE_NOTIFY_TOKEN"
    LINE_API_URL = "https://notify-api.line.me/api/notify"

    # ファイル設定
    STATE_FILE = "portfolio_state.json"
    PERFORMANCE_LOG = "performance_log.csv"

# ==============================
# ポートフォリオ状態管理
# ==============================
class PortfolioState:
    """ポートフォリオの状態を保存・管理"""

    def __init__(self, config_file=AssetManagementConfig.STATE_FILE):
        """初期化"""
        self.config_file = config_file
        self.state = self._load_state()

    def _load_state(self):
        """状態ファイルから状態を読み込む"""
        if Path(self.config_file).exists():
            try:
                with open(self.config_file, 'r', encoding='utf-8') as f:
                    return json.load(f)
            except Exception as e:
                logging.error(f"状態読み込みエラー: {e}")

        # デフォルト状態
        return {
            'created_at': datetime.now().isoformat(),
            'last_rebalance': None,
            'total_invested': AssetManagementConfig.INITIAL_CAPITAL,
            'positions': {},
            'cash': AssetManagementConfig.INITIAL_CAPITAL,
            'performance_history': [],
        }

    def save(self):
        """状態をファイルに保存"""
        try:
            with open(self.config_file, 'w', encoding='utf-8') as f:
                json.dump(self.state, f, ensure_ascii=False, indent=2, default=str)
            logging.info("状態を保存しました")
        except Exception as e:
            logging.error(f"状態保存エラー: {e}")

    def update_position(self, symbol, price, shares, asset_class):
        """ポジションを更新"""
        if symbol not in self.state['positions']:
            self.state['positions'][symbol] = {}

        self.state['positions'][symbol] = {
            'price': price,
            'shares': shares,
            'value': price * shares,
            'asset_class': asset_class,
            'last_updated': datetime.now().isoformat(),
        }

    def get_total_value(self):
        """ポートフォリオ総価値を計算"""
        position_value = sum(p['value'] for p in self.state['positions'].values())
        return position_value + self.state['cash']

    def get_weights(self):
        """各資産クラスの現在のウェイトを計算"""
        total_value = self.get_total_value()
        weights = {}

        for symbol, position in self.state['positions'].items():
            asset_class = position['asset_class']

            if asset_class not in weights:
                weights[asset_class] = 0

            weights[asset_class] += position['value'] / total_value

        weights['CASH'] = self.state['cash'] / total_value

        return weights

# ==============================
# 銘柄分析エンジン
# ==============================
class StockAnalyzer:
    """個別銘柄の分析"""

    def __init__(self, symbol):
        """初期化"""
        self.symbol = symbol
        self.df = None

    def fetch_data(self, period='1y'):
        """データを取得"""
        try:
            logging.info(f"データ取得: {self.symbol}")
            ticker = yf.Ticker(self.symbol)
            self.df = ticker.history(period=period)

            if self.df.empty:
                return False

            self.df = self.df.fillna(method='ffill')
            return True

        except Exception as e:
            logging.error(f"データ取得エラー({self.symbol}): {e}")
            return False

    def calculate_indicators(self):
        """テクニカル指標を計算"""
        if self.df is None:
            return False

        try:
            # 移動平均線
            self.df['MA_SHORT'] = self.df['Close'].rolling(
                window=AssetManagementConfig.MA_SHORT).mean()
            self.df['MA_LONG'] = self.df['Close'].rolling(
                window=AssetManagementConfig.MA_LONG).mean()

            # RSI
            delta = self.df['Close'].diff()
            gain = (delta.where(delta > 0, 0)).rolling(
                window=AssetManagementConfig.RSI_PERIOD).mean()
            loss = (-delta.where(delta < 0, 0)).rolling(
                window=AssetManagementConfig.RSI_PERIOD).mean()
            rs = gain / loss
            self.df['RSI'] = 100 - (100 / (1 + rs))

            return True

        except Exception as e:
            logging.error(f"指標計算エラー({self.symbol}): {e}")
            return False

    def get_signal(self):
        """売買シグナルを取得"""
        if self.df is None or len(self.df) < 2:
            return 'HOLD'

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

        # データ不足チェック
        if pd.isna(current['MA_SHORT']) or pd.isna(current['MA_LONG']):
            return 'HOLD'

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

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

        return 'HOLD'

    def get_current_price(self):
        """現在価格を取得"""
        if self.df is None or self.df.empty:
            return None
        return self.df['Close'].iloc[-1]

# ==============================
# リバランス管理
# ==============================
class RebalanceManager:
    """ポートフォリオのリバランス管理"""

    def __init__(self, portfolio_state):
        """初期化"""
        self.state = portfolio_state

    def should_rebalance(self):
        """リバランスが必要かチェック"""
        # 前回のリバランス日時を確認
        last_rebalance = self.state.state.get('last_rebalance')

        if last_rebalance is None:
            return True  # 初回

        last_rebalance_date = datetime.fromisoformat(last_rebalance).date()
        today = datetime.now().date()

        # 月初でリバランスするか、指定日数が経過したかチェック
        if today.day == AssetManagementConfig.REBALANCE_DAY_OF_MONTH:
            if (today - last_rebalance_date).days >= 1:
                return True

        return False

    def calculate_rebalance_trades(self, current_prices):
        """
        リバランスに必要な取引を計算

        Returns:
            dict: 各銘柄の売買指示
        """
        total_value = self.state.get_total_value()
        current_weights = self.state.get_weights()
        target_weights = self._get_target_weights()

        trades = {}

        for asset_class, config in AssetManagementConfig.PORTFOLIO.items():
            if asset_class == 'CASH':
                continue

            current_weight = current_weights.get(asset_class, 0)
            target_weight = target_weights[asset_class]
            deviation = abs(current_weight - target_weight)

            # 閾値を超えた場合のみリバランス
            if deviation > config['rebalance_threshold']:
                logging.info(f"リバランス必要: {asset_class} "
                           f"(現在: {current_weight*100:.1f}% → 目標: {target_weight*100:.1f}%)")

                trades[asset_class] = {
                    'current_weight': current_weight,
                    'target_weight': target_weight,
                    'target_value': total_value * target_weight,
                }

        return trades

    def _get_target_weights(self):
        """目標ウェイトを計算"""
        return {
            asset_class: config['target_weight']
            for asset_class, config in AssetManagementConfig.PORTFOLIO.items()
            if asset_class != 'CASH'
        }

# ==============================
# パフォーマンス追跡
# ==============================
class PerformanceTracker:
    """運用パフォーマンスを追跡"""

    def __init__(self, log_file=AssetManagementConfig.PERFORMANCE_LOG):
        """初期化"""
        self.log_file = log_file
        self.load_history()

    def load_history(self):
        """履歴を読み込む"""
        if Path(self.log_file).exists():
            self.history = pd.read_csv(self.log_file)
        else:
            self.history = pd.DataFrame()

    def record_performance(self, portfolio_state):
        """パフォーマンスを記録"""
        total_value = portfolio_state.get_total_value()
        total_invested = portfolio_state.state['total_invested']
        gain = total_value - total_invested
        gain_rate = gain / total_invested if total_invested > 0 else 0

        record = {
            'date': datetime.now().isoformat(),
            'total_value': total_value,
            'total_invested': total_invested,
            'gain': gain,
            'gain_rate': gain_rate,
        }

        # CSVに追記
        record_df = pd.DataFrame([record])

        if Path(self.log_file).exists():
            record_df.to_csv(self.log_file, mode='a', header=False, index=False)
        else:
            record_df.to_csv(self.log_file, index=False)

        logging.info(f"パフォーマンス記録: "
                    f"資産額¥{total_value:,.0f}, 利益¥{gain:,.0f} ({gain_rate*100:+.2f}%)")

    def display_summary(self):
        """パフォーマンスサマリーを表示"""
        if self.history.empty:
            return

        latest = self.history.iloc[-1]

        print(f"\n" + "="*70)
        print("パフォーマンスサマリー")
        print("="*70)
        print(f"総資産額: ¥{latest['total_value']:,.0f}")
        print(f"累計投資額: ¥{latest['total_invested']:,.0f}")
        print(f"含み益: ¥{latest['gain']:,.0f}")
        print(f"運用利回り: {latest['gain_rate']*100:+.2f}%")

        # 期間リターンを計算
        if len(self.history) > 1:
            first_value = self.history.iloc[0]['total_value']
            last_value = self.history.iloc[-1]['total_value']
            period_return = (last_value - first_value) / first_value

            print(f"期間リターン: {period_return*100:+.2f}%")

# ==============================
# 通知システム
# ==============================
class NotificationManager:
    """LINE Notifyを使った通知"""

    @staticmethod
    def send_notification(message, token=AssetManagementConfig.LINE_TOKEN):
        """通知を送信"""
        if token == "YOUR_LINE_NOTIFY_TOKEN":
            logging.warning("LINE_TOKEN が設定されていません")
            return False

        try:
            headers = {"Authorization": f"Bearer {token}"}
            data = {"message": message}
            response = requests.post(
                AssetManagementConfig.LINE_API_URL,
                headers=headers,
                data=data
            )

            if response.status_code == 200:
                logging.info("LINE通知送信完了")
                return True
            else:
                logging.warning(f"LINE通知送信失敗: {response.status_code}")
                return False

        except Exception as e:
            logging.error(f"通知エラー: {e}")
            return False

# ==============================
# メインシステム
# ==============================
class AssetManagementSystem:
    """統合資産運用システム"""

    def __init__(self):
        """初期化"""
        self.state = PortfolioState()
        self.rebalancer = RebalanceManager(self.state)
        self.tracker = PerformanceTracker()

    def run_daily_analysis(self):
        """日次分析を実行"""
        logging.info("="*70)
        logging.info("日次分析開始")
        logging.info("="*70)

        all_symbols = []
        for asset_class, config in AssetManagementConfig.PORTFOLIO.items():
            if 'symbols' in config:
                all_symbols.extend(config['symbols'])

        signals = []

        for symbol in all_symbols:
            analyzer = StockAnalyzer(symbol)

            if not analyzer.fetch_data():
                continue

            if not analyzer.calculate_indicators():
                continue

            signal = analyzer.get_signal()
            price = analyzer.get_current_price()

            if signal != 'HOLD':
                signals.append({
                    'symbol': symbol,
                    'signal': signal,
                    'price': price,
                })
                logging.info(f"シグナル検出: {symbol} - {signal} @ ¥{price:,.0f}")

        return signals

    def check_rebalance(self):
        """リバランスが必要か確認"""
        if self.rebalancer.should_rebalance():
            logging.info("リバランス実行")
            # リバランス処理はここに実装
            self.state.state['last_rebalance'] = datetime.now().isoformat()
            self.state.save()

            return True

        return False

    def record_performance(self):
        """パフォーマンスを記録"""
        self.tracker.record_performance(self.state)

    def send_daily_report(self, signals):
        """日次レポートを送信"""
        message = f"\n【資産運用システム 日次レポート】\n"
        message += f"実行時刻: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"

        # ポートフォリオ情報
        total_value = self.state.get_total_value()
        total_invested = self.state.state['total_invested']
        gain = total_value - total_invested

        message += f"【ポートフォリオ】\n"
        message += f"総資産額: ¥{total_value:,.0f}\n"
        message += f"含み益: ¥{gain:,.0f} ({gain/total_invested*100:+.2f}%)\n\n"

        # シグナル情報
        if signals:
            message += f"【シグナル】\n"
            for s in signals:
                message += f"{s['symbol']}: {s['signal']} @ ¥{s['price']:,.0f}\n"
        else:
            message += "【シグナル】特にシグナルはありません\n"

        message += "\n※このメッセージは自動で生成されています。"

        NotificationManager.send_notification(message)

    def run(self):
        """システムを実行"""
        try:
            # 日次分析を実行
            signals = self.run_daily_analysis()

            # リバランス確認
            self.check_rebalance()

            # パフォーマンス記録
            self.record_performance()

            # レポート送信
            self.send_daily_report(signals)

            # パフォーマンス表示
            self.tracker.display_summary()

        except Exception as e:
            logging.error(f"実行エラー: {e}")

        finally:
            self.state.save()
            logging.info("実行完了\n")

# ==============================
# メイン処理
# ==============================
if __name__ == "__main__":
    system = AssetManagementSystem()
    system.run()

このシステムの特徴:

  • 完全な自動化: yfinanceを使ったデータ取得から通知まで全て自動
  • 複数銘柄対応: 日本株から外国株まで複数資産クラスを統合管理
  • 自動リバランス: 定期的に目標配分に戻す
  • パフォーマンス追跡: 運用成績を継続的に記録
  • LINE通知: 重要なイベントを即座に通知

【コピペOK】月次積立を含むシステム拡張

長期資産運用では、定期的な積立も重要です。以下は月次積立機能を追加したコードです。

class ContributionManager:
    """定期積立管理"""

    def __init__(self, monthly_amount=AssetManagementConfig.MONTHLY_CONTRIBUTION):
        """初期化"""
        self.monthly_amount = monthly_amount
        self.contribution_log = []

    def should_contribute(self, last_contribution_date):
        """月次積立の実施判定"""
        today = datetime.now().date()

        if last_contribution_date is None:
            return True

        last_date = datetime.fromisoformat(last_contribution_date).date()

        # 月が変わったか確認
        if today.month != last_date.month or today.year != last_date.year:
            return True

        return False

    def execute_contribution(self, portfolio_state, allocation):
        """
        月次積立を実行

        Args:
            portfolio_state: ポートフォリオ状態
            allocation (dict): 各資産クラスの配分比率
        """
        logging.info(f"月次積立実行: ¥{self.monthly_amount:,.0f}")

        # 現在のキャッシュに追加
        portfolio_state.state['cash'] += self.monthly_amount
        portfolio_state.state['total_invested'] += self.monthly_amount

        # 配分に従って各資産クラスに割り当て
        for asset_class, ratio in allocation.items():
            contribution = self.monthly_amount * ratio

            logging.info(f"  {asset_class}: ¥{contribution:,.0f}")

        self.contribution_log.append({
            'date': datetime.now().isoformat(),
            'amount': self.monthly_amount,
            'allocation': allocation,
        })

        portfolio_state.state['last_contribution'] = datetime.now().isoformat()

長期運用における動的な戦略調整

市場環境の変化に対応して、戦略を動的に調整することが重要です。

class StrategyAdapter:
    """市場環境に応じた戦略の動的調整"""

    @staticmethod
    def adjust_allocation_by_market_regime(market_indicators):
        """
        市場環境に応じてアセットアロケーションを調整

        Args:
            market_indicators (dict): 市場指標(VIX、金利など)

        Returns:
            dict: 調整後の配分
        """
        vix = market_indicators.get('vix', 15)  # VIX指数(デフォルト値15)

        if vix > 30:
            # 高ボラティリティ環境:防御的な配分
            return {
                'JP_STOCKS': 0.25,
                'FOREIGN_STOCKS': 0.25,
                'BONDS': 0.40,
                'CASH': 0.10,
            }

        elif vix > 20:
            # 中程度のボラティリティ:バランス配分
            return {
                'JP_STOCKS': 0.35,
                'FOREIGN_STOCKS': 0.35,
                'BONDS': 0.20,
                'CASH': 0.10,
            }

        else:
            # 低ボラティリティ環境:積極的な配分
            return {
                'JP_STOCKS': 0.40,
                'FOREIGN_STOCKS': 0.40,
                'BONDS': 0.15,
                'CASH': 0.05,
            }

よくあるエラーと対処法

「データが取得できません」というエラー

原因: yfinanceのサービス一時停止、またはネットワークエラー

対処法:

# リトライロジックを実装
from tenacity import retry, stop_after_attempt, wait_exponential

@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
def fetch_with_retry(self, period='1y'):
    """リトライ機能付きデータ取得"""
    return yf.Ticker(self.symbol).history(period=period)

「LINE通知が送信されません」

原因: LINE_TOKEN が設定されていない、または無効期限切れ

対処法:

環境変数から安全に取得:

import os
from dotenv import load_dotenv

load_dotenv()
LINE_TOKEN = os.getenv('LINE_NOTIFY_TOKEN')

リバランス計算で「配分の合計が100%にならない」

原因: 浮動小数点数の丸め誤差

対処法:

# 配分の正規化
def normalize_allocation(allocation):
    """配分の合計を100%に正規化"""
    total = sum(allocation.values())
    return {k: v/total for k, v in allocation.items()}

まとめ

本記事では、Pythonを使った「長期資産運用システム」の完全な実装を提示しました。

要点を整理します。

  • 複利と時間が最強: 40年で手数料1%の差が262万円の機会損失を生む
  • 「勝つ」のではなく「負けない」: リスク管理が勝率より重要
  • 分散は無料のリスク削減: 複数資産クラス、複数銘柄への分散が必須
  • 定期的なリバランス: 目標配分を維持することで複利を最大化
  • 感情排除が最大の武器: アルゴリズムの自動化が勝つ鍵
  • 市場環境への適応: VIXなどの指標で戦略を動的に調整

本シリーズを通じて、個人投資家が「自分専用のアルゴリズム」を構築できることを示してきました。ロボアドバイザーの手数料に悩む必要はありません。Pythonとyfinanceを使えば、十分な機能を持つシステムが、実装可能です。

次のステップ:

  1. 本記事のコードで小額(月1万円程度)のデモ運用を開始
  2. 3~6ヶ月間、システムが正常に動作することを確認
  3. 本格的な資金投入を開始
  4. 年1回程度、戦略を見直す

30年、40年という長期間、あなたの資産を確実に育てていくシステムを手に入れてください。

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