Skip to content
Published on

開発者のためのETF投資戦略 — 資産配分からクオンツバックテストまで

Authors
  • Name
    Twitter
ETF Investment Strategies

はじめに

開発者は忙しいものです。一日中株式チャートを見る時間も、個別銘柄を分析する余裕もありません。しかし、お金には働いてもらう必要があります。**ETF(Exchange Traded Fund)**は個別銘柄分析なしで分散投資が可能な最適なツールであり、資産配分戦略と組み合わせれば、少ない時間で安定したリターンを追求できます。

本記事では、開発者らしくデータとコードでアプローチするETF投資戦略を解説します。

ETFの基礎

ETF vs 個別株 vs ファンド

| 特性          | 個別株        | アクティブファンド | ETF(パッシブ)    |
|--------------|-------------|-----------------|------------------|
| 分散投資      | ❌ 手動      | ✅ ファンドマネージャー | ✅ インデックス連動 |
| コスト(費用) | なし         | 12%/| 0.030.5%/|
| 取引          | リアルタイム   | 11| リアルタイム       |
| 透明性        | 高い         | 低い             | 高い              |
| 必要時間      | 多い         | 少ない           | 非常に少ない       |

コアETFユニバース

# グローバル資産配分に使用するコアETF
CORE_ETFS = {
    # 株式
    "VTI": "米国株式市場全体",
    "VXUS": "米国以外の先進国+新興国株式",
    "VWO": "新興国株式",
    "QQQ": "NASDAQ 100(テック株)",

    # 債券
    "BND": "米国総合債券",
    "TLT": "米国長期国債(20年以上)",
    "IEF": "米国中期国債(7-10年)",
    "TIP": "物価連動債券(TIPS)",

    # オルタナティブ
    "GLD": "金",
    "VNQ": "米国REIT(不動産)",
    "DBC": "コモディティ",

    # 韓国
    "KODEX200": "KOSPI 200",
    "TIGER米国S&P500": "S&P500(ウォン建て)",
    "ACE米国NASDAQ100": "NASDAQ100(ウォン建て)",
}

資産配分戦略

1. オールウェザーポートフォリオ(Ray Dalio)

ALL_WEATHER = {
    "VTI": 0.30,   # 米国株式 30%
    "TLT": 0.40,   # 長期国債 40%
    "IEF": 0.15,   # 中期国債 15%
    "GLD": 0.075,  # 金 7.5%
    "DBC": 0.075,  # コモディティ 7.5%
}
# 特徴: 経済の4つの季節(成長/後退 × インフレ/デフレ)すべてに対応
# 年平均リターン: 約7%(2005-2025)
# 最大ドローダウン: 約12%

2. パーマネントポートフォリオ(Harry Browne)

PERMANENT = {
    "VTI": 0.25,   # 株式 25%(成長期)
    "TLT": 0.25,   # 長期国債 25%(後退期)
    "GLD": 0.25,   # 金 25%(インフレ)
    "BIL": 0.25,   # 短期国債 25%(安全資産)
}
# 特徴: 極めてシンプル、どんな経済状況でも25%は輝く
# 年平均リターン: 約6%
# 最大ドローダウン: 約10%

3. 60/40 ポートフォリオ(伝統的)

CLASSIC_60_40 = {
    "VTI": 0.60,   # 株式 60%
    "BND": 0.40,   # 債券 40%
}
# 特徴: 最も伝統的でシンプル
# 年平均リターン: 約8%
# 最大ドローダウン: 約20%

4. コアサテライト戦略(Core-Satellite)

CORE_SATELLITE = {
    # コア(80%) — パッシブ、低コスト
    "VTI": 0.50,    # 米国市場全体
    "VXUS": 0.20,   # 国際株式
    "BND": 0.10,    # 債券

    # サテライト(20%) — アルファ追求
    "QQQ": 0.10,    # テック成長株
    "VNQ": 0.05,    # REIT
    "GLD": 0.05,    # 金
}
# 特徴: 市場リターン(ベータ)を確保 + 一部アルファを追求

Pythonバックテスト

データ取得

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

def get_etf_data(tickers: list, start: str = "2010-01-01") -> pd.DataFrame:
    """ETF価格データのダウンロード"""
    data = yf.download(tickers, start=start, auto_adjust=True)
    prices = data["Close"]
    returns = prices.pct_change().dropna()
    return prices, returns


# データダウンロード
tickers = ["VTI", "TLT", "IEF", "GLD", "DBC", "BND"]
prices, returns = get_etf_data(tickers)

ポートフォリオバックテスト

def backtest_portfolio(
    returns: pd.DataFrame,
    weights: dict,
    rebalance_freq: str = "Q",  # Q=四半期, M=月次, Y=年次
    initial_capital: float = 10000
) -> pd.DataFrame:
    """ポートフォリオバックテスト"""
    tickers = list(weights.keys())
    w = np.array([weights[t] for t in tickers])

    # 日次ポートフォリオリターン
    port_returns = (returns[tickers] * w).sum(axis=1)

    # リバランスシミュレーション
    if rebalance_freq:
        rebalance_dates = returns.resample(rebalance_freq).last().index

    # 累積リターン
    cumulative = (1 + port_returns).cumprod() * initial_capital

    # パフォーマンス指標の計算
    total_return = cumulative.iloc[-1] / initial_capital - 1
    annual_return = (1 + total_return) ** (252 / len(port_returns)) - 1
    annual_vol = port_returns.std() * np.sqrt(252)
    sharpe = annual_return / annual_vol
    max_dd = (cumulative / cumulative.cummax() - 1).min()

    stats = {
        "トータルリターン": f"{total_return:.1%}",
        "年平均リターン(CAGR)": f"{annual_return:.1%}",
        "年間ボラティリティ": f"{annual_vol:.1%}",
        "シャープレシオ": f"{sharpe:.2f}",
        "最大ドローダウン(MDD)": f"{max_dd:.1%}",
    }

    return cumulative, stats


# 各戦略のバックテスト
strategies = {
    "オールウェザー": ALL_WEATHER,
    "パーマネントポートフォリオ": PERMANENT,
    "60/40": CLASSIC_60_40,
}

for name, weights in strategies.items():
    cumulative, stats = backtest_portfolio(returns, weights)
    print(f"\n{'='*40}")
    print(f"  {name}")
    print(f"{'='*40}")
    for k, v in stats.items():
        print(f"  {k}: {v}")

リバランスシミュレーション

def simulate_rebalancing(
    prices: pd.DataFrame,
    weights: dict,
    initial_capital: float = 10000,
    rebalance_freq: str = "Q"
) -> pd.DataFrame:
    """実際のリバランス取引シミュレーション"""
    tickers = list(weights.keys())
    target_weights = np.array([weights[t] for t in tickers])

    # 初期ポジション
    shares = {}
    for ticker, w in weights.items():
        price = prices[ticker].iloc[0]
        shares[ticker] = (initial_capital * w) / price

    portfolio_value = []
    rebalance_log = []

    rebalance_dates = prices.resample(rebalance_freq).last().index

    for date in prices.index:
        # 現在のポートフォリオ価値
        current_value = sum(
            shares[t] * prices[t].loc[date]
            for t in tickers
        )
        portfolio_value.append(current_value)

        # リバランス日か?
        if date in rebalance_dates:
            current_weights = np.array([
                shares[t] * prices[t].loc[date] / current_value
                for t in tickers
            ])

            drift = np.abs(current_weights - target_weights).max()

            if drift > 0.05:  # 5%以上の乖離時のみリバランス
                for ticker, tw in weights.items():
                    target_value = current_value * tw
                    shares[ticker] = target_value / prices[ticker].loc[date]

                rebalance_log.append({
                    "date": date,
                    "value": current_value,
                    "drift": drift,
                })

    return pd.Series(portfolio_value, index=prices.index), rebalance_log

積立投資(DCA)

def dollar_cost_averaging(
    prices: pd.DataFrame,
    ticker: str,
    monthly_amount: float = 500000,  # 月50万ウォン
    start_date: str = "2015-01-01"
) -> dict:
    """積立投資シミュレーション"""
    monthly_prices = prices[ticker].resample("MS").first()
    monthly_prices = monthly_prices[start_date:]

    total_invested = 0
    total_shares = 0

    for date, price in monthly_prices.items():
        shares_bought = monthly_amount / price
        total_shares += shares_bought
        total_invested += monthly_amount

    final_value = total_shares * prices[ticker].iloc[-1]
    total_return = (final_value / total_invested - 1) * 100
    avg_cost = total_invested / total_shares

    return {
        "総投資額": f"{total_invested:,.0f}ウォン",
        "現在価値": f"{final_value:,.0f}ウォン",
        "リターン": f"{total_return:.1f}%",
        "平均取得単価": f"{avg_cost:,.0f}ウォン",
        "現在価格": f"{prices[ticker].iloc[-1]:,.0f}ウォン",
        "投資期間": f"{len(monthly_prices)}ヶ月",
    }

税金とコストの考慮

# 韓国ETFの税制構造
TAX_RULES = {
    "国内株式型ETF": {
        "売買差益": "非課税",
        "分配金": "15.4%配当所得税",
        "例": "KODEX200, TIGER KRX300",
    },
    "国内その他ETF": {
        "売買差益": "15.4%配当所得税(保有期間課税)",
        "分配金": "15.4%配当所得税",
        "例": "KODEX ゴールド, TIGER 米国S&P500",
    },
    "海外直接投資": {
        "売買差益": "22%譲渡所得税(250万ウォン控除後)",
        "分配金": "15%源泉徴収",
        "例": "VTI, QQQ, TLT(米国直接購入)",
    },
}

def calculate_tax(profit: float, invest_type: str) -> float:
    """税引後利益の計算"""
    if invest_type == "国内株式型":
        return profit  # 非課税
    elif invest_type == "国内その他":
        return profit * (1 - 0.154)
    elif invest_type == "海外直接":
        taxable = max(0, profit - 2500000)  # 250万ウォン控除
        tax = taxable * 0.22
        return profit - tax

自動リバランスアラート

import yfinance as yf
from datetime import datetime

def check_rebalance_needed(
    portfolio: dict,
    target_weights: dict,
    threshold: float = 0.05
) -> list:
    """リバランスの必要性チェック"""
    # 現在価格の取得
    tickers = list(portfolio.keys())
    current_prices = {}
    for t in tickers:
        data = yf.Ticker(t)
        current_prices[t] = data.info.get("regularMarketPrice", 0)

    # 現在のポートフォリオ価値
    total_value = sum(
        portfolio[t]["shares"] * current_prices[t]
        for t in tickers
    )

    # ドリフトチェック
    alerts = []
    for ticker in tickers:
        current_value = portfolio[ticker]["shares"] * current_prices[ticker]
        current_weight = current_value / total_value
        target_weight = target_weights[ticker]
        drift = abs(current_weight - target_weight)

        if drift > threshold:
            action = "買い" if current_weight < target_weight else "売り"
            amount = abs(current_weight - target_weight) * total_value

            alerts.append({
                "ticker": ticker,
                "現在比率": f"{current_weight:.1%}",
                "目標比率": f"{target_weight:.1%}",
                "ドリフト": f"{drift:.1%}",
                "アクション": f"{action} {amount:,.0f}ウォン",
            })

    return alerts


# 四半期チェック(cronジョブで実行)
alerts = check_rebalance_needed(my_portfolio, ALL_WEATHER)
if alerts:
    print("警告: リバランスが必要です!")
    for a in alerts:
        print(f"  {a['ticker']}: {a['現在比率']}{a['目標比率']} ({a['アクション']})")

開発者のための投資原則

1. 自動化せよ
   - 毎月自動振替 → 自動購入
   - リバランスアラートの自動化
   - 感情が入る余地を与えるな

2. データで判断せよ
   - バックテストなしに戦略を使うな
   - 過去のリターン ≠ 未来のリターン、だがリスク測定には有用

3. 時間を節約せよ
   - ポートフォリオチェック:四半期に1回で十分
   - ニュース/チャートを見る時間 → コーディング/学習の時間に

4. コストを削減せよ
   - 信託報酬0.1%未満のETFを選択
   - 頻繁な売買 = 税金 + 手数料の増加

5. 長期投資せよ
   - 最低10年以上の投資期間
   - マーケットタイミングを狙うな
   - "Time in the market > Timing the market"

クイズ

Q1. オールウェザーポートフォリオのコアアイデアとは?

経済の4つの季節(成長/後退 × インフレ/デフレ)それぞれに有利な資産を保有し、どんな経済状況でも安定したリターンを追求します。

Q2. リバランスの目的は?

市場変動によって目標比率から乖離したポートフォリオを元の比率に戻します。自然と「高く売って、安く買う」効果が生まれます。

Q3. DCA(ドルコスト平均法)のメリットは?

定期的に一定額を投資して取得単価を平均化します。マーケットタイミングを狙う必要がなく、感情的な投資を防止します。

Q4. 海外ETF直接投資時の譲渡所得税の計算方法は?

売買差益から250万ウォンを控除した後、22%が課税されます。例:1,000万ウォンの利益 →(1,000万-250万)× 0.22 = 165万ウォンの税金。

Q5. シャープレシオ(Sharpe Ratio)とは?

(リターン - 無リスクリターン)/ ボラティリティで、リスクあたりの超過リターンを測定します。高いほど効率的な投資です。一般的に1.0以上なら良好です。

Q6. コアサテライト(Core-Satellite)戦略とは?

資産の大部分(80%)を低コストのインデックスETF(コア)で市場リターンを確保し、一部(20%)をテーマ/セクターETF(サテライト)でアルファ(超過リターン)を追求する戦略です。

Q7. MDD(Maximum Drawdown)とは?

最大ドローダウンで、高値から安値までの最大下落率です。ポートフォリオの最悪のシナリオを示し、心理的耐久力を測る指標です。

まとめ

開発者にとってETF投資の魅力はシステム化の可能性です。戦略を決め、自動購入を設定し、四半期ごとのリバランスアラートを受け取るだけです。チャートを見つめる時間を技術学習に投資しましょう — 長期的にはその方が高いROIをもたらします。

参考資料

※ 本記事は投資勧誘ではなく、教育目的のコンテンツです。投資判断はご自身の判断と責任で行ってください。