Skip to content
Published on

ETFリバランス自動化ガイド:ルールベースの投資運用

Authors
  • Name
    Twitter
ETFリバランス自動化ガイド:ルールベースの投資運用

リバランスを自動化すべき理由

ポートフォリオを構成して購入するところまでは誰でもできる。問題はその後だ。6ヶ月経つと株式が上昇して比率が80%になり、債券は10%に縮小する。「もう少し上がりそう」という感情が介入すると、リバランスは永遠に先送りされる。

Vanguardの2023年の研究 The Case for Rebalancing によると、リバランスを行わなかったポートフォリオは10年後にリスク(標準偏差)が当初の設計より30-50%高くなっていた。収益が問題ではなく、リスク管理が崩壊したのだ。

自動化の目的は収益の最大化ではない。意思決定から感情を排除し、ルールを一貫して実行することが核心だ。William Bernsteinの The Intelligent Asset Allocator で強調されているように、「リバランスとは安く買って高く売る行為を機械的に繰り返すこと」である。

リバランス方式の比較:時間ベース vs 乖離ベース vs ハイブリッド

3つの方式の比較

方式トリガーメリットデメリット適している場合
時間ベース四半期/半年/年次の固定日シンプル、予測可能急変時の対応が遅い小規模ポートフォリオ、初心者
乖離ベース目標比率から±N%乖離した時市場変動に敏感に対応頻繁な売買でコスト増加変動性の高い資産保有時
ハイブリッド四半期チェック+乖離5%超過時に即時実行コストとリスクのバランスルールの複雑度が増す実戦最適、ほとんどの場合推奨

ハイブリッド方式の詳細

ハイブリッド方式は2つのルールを同時に適用する:

  1. 定期チェック:毎四半期の最初の営業日にポートフォリオ全体の比率を確認する
  2. 緊急リバランス:いずれかの資産が目標比率から±5%以上乖離した場合、即座にリバランスを実行する

例:目標比率が株式60%/債券30%/金10%のポートフォリオで、株式が66%を超えるか54%を下回ると、四半期を待たずにすぐリバランスを実行する。

ポートフォリオ設定ファイルの設計

リバランス自動化の第一歩は、投資ルールをコードとして文書化することだ。YAMLファイルで管理すれば、変更履歴をGitで追跡できる。

# portfolio_config.yaml
# 最終修正: 2026-03-04
# 変更理由: 2026年資産配分比率調整(債券比率拡大)

portfolio:
  name: '2026 長期成長型'
  owner: 'youngjukim'
  base_currency: 'KRW'
  broker: '韓国投資証券' # またはキウム、ミレアセットなど

  # 目標資産配分
  allocation:
    core:
      weight: 0.70
      assets:
        - ticker: 'TIGER 米国S&P500'
          code: '360750'
          target: 0.35
          min: 0.30
          max: 0.40
        - ticker: 'KODEX 200'
          code: '069500'
          target: 0.15
          min: 0.10
          max: 0.20
        - ticker: 'TIGER 米国債10年先物'
          code: '305080'
          target: 0.15
          min: 0.10
          max: 0.20
        - ticker: 'KODEX 総合債券(AA-以上)アクティブ'
          code: '443160'
          target: 0.05
          min: 0.02
          max: 0.08

    satellite:
      weight: 0.30
      assets:
        - ticker: 'TIGER 半導体'
          code: '091230'
          target: 0.10
          min: 0.05
          max: 0.15
        - ticker: 'KODEX 二次電池産業'
          code: '305720'
          target: 0.10
          min: 0.05
          max: 0.15
        - ticker: 'TIGER 金銀先物(H)'
          code: '319640'
          target: 0.10
          min: 0.05
          max: 0.15

  # リバランスルール
  rebalancing:
    strategy: 'hybrid'
    scheduled:
      frequency: 'quarterly'
      months: [1, 4, 7, 10] # 1月、4月、7月、10月の最初の営業日
      day: 'first_business_day'
    drift_trigger:
      threshold: 0.05 # ±5%乖離時に緊急リバランス
      check_frequency: 'weekly' # 毎週月曜日に確認
    constraints:
      min_trade_amount: 100000 # 最小取引金額10万ウォン
      avoid_ex_dividend_dates: true # 配当基準日前後の売買回避
      tax_loss_harvesting: false # 韓国では実効性が低い

  # コスト設定
  costs:
    etf_commission_rate: 0.00015 # 0.015%(オンライン証券会社基準)
    etf_tax_rate: 0.0 # 国内ETF売買差益非課税(2025年基準)
    overseas_etf_tax_rate: 0.22 # 海外ETF譲渡所得税22%
    slippage_estimate: 0.001 # 0.1%スリッページ推定

リバランスエンジンの実装(Python)

コアロジック:現在比率確認 → 乖離計算 → 売買注文生成

"""
ETFリバランスエンジン
- ポートフォリオ設定ファイルを読み込み、現在比率と目標比率の差を計算
- リバランスが必要な資産に対して売買注文を生成
"""
import yaml
from dataclasses import dataclass
from typing import Optional


@dataclass
class TradeOrder:
    """売買注文データ。"""
    ticker: str
    code: str
    action: str          # "BUY" or "SELL"
    amount_krw: int      # 売買金額(ウォン)
    reason: str          # リバランス理由

    def __str__(self):
        return (
            f"[{self.action}] {self.ticker} ({self.code}) "
            f"₩{self.amount_krw:,} - {self.reason}"
        )


def load_config(path: str = "portfolio_config.yaml") -> dict:
    """ポートフォリオ設定ファイルをロードする。"""
    with open(path, "r", encoding="utf-8") as f:
        return yaml.safe_load(f)


def get_current_holdings(broker_api) -> dict:
    """証券会社APIから現在の保有資産情報を照会する。

    Returns:
        {銘柄コード: {"value_krw": 金額, "quantity": 数量}}
    """
    # 実際の実装では韓国投資証券OpenAPI、キウムOpen API+等を使用
    # ここではサンプルデータを返す
    return {
        "360750": {"value_krw": 4_200_000, "quantity": 250},
        "069500": {"value_krw": 1_600_000, "quantity": 45},
        "305080": {"value_krw": 1_400_000, "quantity": 140},
        "443160": {"value_krw": 500_000, "quantity": 50},
        "091230": {"value_krw": 1_500_000, "quantity": 30},
        "305720": {"value_krw": 800_000, "quantity": 25},
        "319640": {"value_krw": 1_000_000, "quantity": 80},
    }


def calculate_drift(config: dict, holdings: dict) -> list[dict]:
    """各資産の目標比率に対する現在の乖離を計算する。"""
    total_value = sum(h["value_krw"] for h in holdings.values())
    if total_value == 0:
        return []

    results = []
    for section in ["core", "satellite"]:
        for asset in config["portfolio"]["allocation"][section]["assets"]:
            code = asset["code"]
            holding = holdings.get(code, {"value_krw": 0})
            current_weight = holding["value_krw"] / total_value
            target_weight = asset["target"]
            drift = current_weight - target_weight

            results.append({
                "ticker": asset["ticker"],
                "code": code,
                "target_weight": target_weight,
                "current_weight": round(current_weight, 4),
                "drift": round(drift, 4),
                "drift_pct": round(drift * 100, 2),
                "value_krw": holding["value_krw"],
                "section": section,
            })

    return results


def needs_rebalancing(
    drift_data: list[dict],
    threshold: float = 0.05,
) -> bool:
    """乖離閾値を超えた資産があるか確認する。"""
    return any(abs(d["drift"]) > threshold for d in drift_data)


def generate_orders(
    config: dict,
    drift_data: list[dict],
    total_value: int,
    min_trade: int = 100_000,
) -> list[TradeOrder]:
    """リバランスに必要な売買注文リストを生成する。"""
    orders = []

    for asset in drift_data:
        target_value = int(total_value * asset["target_weight"])
        diff = target_value - asset["value_krw"]

        # 最小取引金額未満はスキップ
        if abs(diff) < min_trade:
            continue

        action = "BUY" if diff > 0 else "SELL"
        reason = (
            f"目標 {asset['target_weight']*100:.1f}% vs "
            f"現在 {asset['current_weight']*100:.1f}% "
            f"(乖離 {asset['drift_pct']:+.2f}%p)"
        )

        orders.append(TradeOrder(
            ticker=asset["ticker"],
            code=asset["code"],
            action=action,
            amount_krw=abs(diff),
            reason=reason,
        ))

    return orders


def run_rebalancing(
    config_path: str = "portfolio_config.yaml",
    dry_run: bool = True,
) -> None:
    """リバランスの全プロセスを実行する。"""
    config = load_config(config_path)
    holdings = get_current_holdings(broker_api=None)
    total_value = sum(h["value_krw"] for h in holdings.values())

    print(f"総資産: ₩{total_value:,}")
    print(f"リバランス戦略: {config['portfolio']['rebalancing']['strategy']}")
    print()

    # 乖離計算
    drift_data = calculate_drift(config, holdings)

    print("=== 現在のポートフォリオ乖離 ===")
    for d in drift_data:
        flag = " ⚠" if abs(d["drift"]) > 0.05 else ""
        print(
            f"  {d['ticker']}: "
            f"目標 {d['target_weight']*100:.1f}% | "
            f"現在 {d['current_weight']*100:.1f}% | "
            f"乖離 {d['drift_pct']:+.2f}%p{flag}"
        )
    print()

    # リバランスの必要性判断
    threshold = config["portfolio"]["rebalancing"]["drift_trigger"]["threshold"]
    if not needs_rebalancing(drift_data, threshold):
        print("リバランス不要:すべての資産が閾値内")
        return

    # 売買注文生成
    min_trade = config["portfolio"]["rebalancing"]["constraints"]["min_trade_amount"]
    orders = generate_orders(config, drift_data, total_value, min_trade)

    print(f"=== 売買注文({len(orders)}件) ===")
    for order in orders:
        print(f"  {order}")
    print()

    # コスト推定
    commission_rate = config["portfolio"]["costs"]["etf_commission_rate"]
    total_trade = sum(o.amount_krw for o in orders)
    estimated_cost = int(total_trade * commission_rate)
    print(f"総売買金額: ₩{total_trade:,}")
    print(f"推定手数料: ₩{estimated_cost:,}")

    if dry_run:
        print("\n[DRY RUN] 実際の注文は実行されませんでした。")
    else:
        print("\n[LIVE] 注文を実行します...")
        # 実際の実装では証券会社APIを呼び出す


if __name__ == "__main__":
    run_rebalancing(dry_run=True)

実行結果の例

総資産:11,000,000
リバランス戦略: hybrid

=== 現在のポートフォリオ乖離 ===
  TIGER 米国S&P500: 目標 35.0% | 現在 38.2% | 乖離 +3.18%p
  KODEX 200: 目標 15.0% | 現在 14.5% | 乖離 -0.45%p
  TIGER 米国債10年先物: 目標 15.0% | 現在 12.7% | 乖離 -2.27%p
  KODEX 総合債券(AA-以上)アクティブ: 目標 5.0% | 現在 4.5% | 乖離 -0.45%p
  TIGER 半導体: 目標 10.0% | 現在 13.6% | 乖離 +3.64%p
  KODEX 二次電池産業: 目標 10.0% | 現在 7.3% | 乖離 -2.73%p
  TIGER 金銀先物(H): 目標 10.0% | 現在 9.1% | 乖離 -0.91%p

=== 売買注文(4件) ===
  [SELL] TIGER 米国S&P500 (360750)350,000 - 目標 35.0% vs 現在 38.2% (乖離 +3.18%p)
  [BUY] TIGER 米国債10年先物 (305080)250,000 - 目標 15.0% vs 現在 12.7% (乖離 -2.27%p)
  [SELL] TIGER 半導体 (091230)400,000 - 目標 10.0% vs 現在 13.6% (乖離 +3.64%p)
  [BUY] KODEX 二次電池産業 (305720)300,000 - 目標 10.0% vs 現在 7.3% (乖離 -2.73%p)

総売買金額:1,300,000
推定手数料:195

[DRY RUN] 実際の注文は実行されませんでした。

自動実行スケジューリング

GitHub Actionsによる週次チェックの自動化

# .github/workflows/rebalance-check.yml
name: ETFリバランスチェック

on:
  schedule:
    # 毎週月曜日 韓国時間09:00(UTC 00:00)
    - cron: '0 0 * * 1'
  workflow_dispatch: # 手動実行可能

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Python環境設定
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: 依存関係インストール
        run: pip install pyyaml requests

      - name: リバランスチェック(Dry Run)
        run: python rebalance.py --dry-run
        env:
          BROKER_APP_KEY: ${{ secrets.BROKER_APP_KEY }}
          BROKER_APP_SECRET: ${{ secrets.BROKER_APP_SECRET }}

      - name: 結果通知(Slack)
        if: always()
        run: |
          python notify.py --channel "#investment" \
            --message "週次リバランスチェック完了。詳細はGitHub Actionsログを参照。"
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Slack通知スクリプト

"""リバランスチェック結果をSlackに送信する。"""
import json
import os
import urllib.request


def send_slack_notification(
    webhook_url: str,
    channel: str,
    message: str,
    orders: list[dict] | None = None,
) -> None:
    """Slack Incoming Webhookで通知を送信する。"""
    blocks = [
        {
            "type": "header",
            "text": {
                "type": "plain_text",
                "text": "ETFリバランスチェック結果",
            },
        },
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": message,
            },
        },
    ]

    if orders:
        order_text = "\n".join(
            f"- {'買い' if o['action'] == 'BUY' else '売り'} "
            f"{o['ticker']}{o['amount']:,}"
            for o in orders
        )
        blocks.append({
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": f"*必要な売買注文:*\n{order_text}",
            },
        })

    payload = json.dumps({
        "channel": channel,
        "blocks": blocks,
    }).encode("utf-8")

    req = urllib.request.Request(
        webhook_url,
        data=payload,
        headers={"Content-Type": "application/json"},
    )
    urllib.request.urlopen(req)


if __name__ == "__main__":
    send_slack_notification(
        webhook_url=os.environ["SLACK_WEBHOOK_URL"],
        channel="#investment",
        message="すべての資産が目標比率±5%以内です。リバランス不要。",
    )

取引ログとパフォーマンス追跡

リバランスを実行した後は必ず記録を残す。後で「リバランスは効果があったか?」を検証するにはデータが必要だ。

取引ログスキーマ

"""リバランス取引ログをJSONファイルで管理する。"""
import json
from datetime import datetime
from dataclasses import dataclass, asdict


@dataclass
class RebalanceLog:
    """リバランス実行記録。"""
    date: str
    trigger: str  # "scheduled" | "drift" | "manual"
    total_portfolio_value: int
    trades: list[dict]
    total_trade_amount: int
    total_commission: int
    pre_drift: dict   # リバランス前の乖離
    post_drift: dict  # リバランス後の乖離
    notes: str = ""


def save_log(log: RebalanceLog, path: str = "rebalance_log.json"):
    """取引ログをファイルに追加する。"""
    try:
        with open(path, "r") as f:
            logs = json.load(f)
    except FileNotFoundError:
        logs = []

    logs.append(asdict(log))

    with open(path, "w", encoding="utf-8") as f:
        json.dump(logs, f, indent=2, ensure_ascii=False)


# 使用例
log = RebalanceLog(
    date="2026-03-04",
    trigger="scheduled",
    total_portfolio_value=11_000_000,
    trades=[
        {"ticker": "TIGER 米国S&P500", "action": "SELL", "amount": 350_000},
        {"ticker": "TIGER 米国債10年先物", "action": "BUY", "amount": 250_000},
    ],
    total_trade_amount=600_000,
    total_commission=90,
    pre_drift={"TIGER 米国S&P500": 3.18, "TIGER 米国債10年先物": -2.27},
    post_drift={"TIGER 米国S&P500": 0.12, "TIGER 米国債10年先物": -0.08},
    notes="2026年Q1定期リバランス。S&P500比率過多のため債券に移転。",
)
save_log(log)

四半期パフォーマンスレビューチェックリスト

四半期ごとに以下の項目を点検する。

  • リバランス実行回数と理由の確認(定期 vs 緊急)
  • 総売買コスト(手数料+スリッページ)の確認
  • リバランス前後の乖離変化の確認
  • ポートフォリオ収益率 vs ベンチマーク(KOSPI、S&P500)比較
  • 目標比率自体の妥当性検討(市場環境の変化を反映)
  • 売買ルール変更が必要な場合、YAMLファイル修正+Gitコミット

韓国投資家のための実践上の考慮事項

税金と手数料

項目国内ETF海外ETF(直接投資)
売買手数料約0.015%(オンライン)0.1-0.25%
売買差益課税非課税(2025年基準)譲渡所得税22%(250万ウォン控除)
配当所得税15.4%15%(米国源泉徴収)
為替コストなし為替スプレッド0.1-0.5%

リバランス自動化の際、国内ETF中心でポートフォリオを構成すれば、売買差益非課税の恩恵によりコストが大幅に削減される。海外ETFを直接保有している場合、年間250万ウォンの譲渡所得控除限度額を考慮して売買タイミングを調整できる。

証券会社API現況(2026年基準)

証券会社API名特徴
韓国投資証券KIS Developers Open APIREST API、最も活発なコミュニティ
キウム証券Open API+Windows専用、COM基盤
ミレアセットm.Stock Open APIREST API
NH投資証券QV Open APIREST API

Pythonで自動売買を実装するなら、韓国投資証券のKIS DevelopersがREST APIベースで最もアクセスしやすい。pykisライブラリを活用すれば、認証から注文までPythonで処理できる。

配当再投資とキャッシュフロー処理

リバランス前に必ず確認すべきこと:

  1. 配当金入金:配当が現金として貯まっていると、リバランス計算時に現金比率が過大に計上される。配当金を先に再投資するか、リバランス金額から除外する。
  2. 定期入金:毎月積立式で追加入金する場合、不足比率の資産に優先配分すれば売却なしでリバランスが可能だ(キャッシュフローリバランス)。
  3. 配当基準日:ETFの配当基準日前後1週間は価格変動が大きいため、リバランスを避けるのが望ましい。

リバランス自動化で注意すべきミス

ミス1:バックテスト結果だけで戦略を決める

バックテストで毎年リバランスが最適だったからといって、実戦でもそうとは限らない。バックテストにはスリッページ、気配値スプレッド、約定失敗が反映されない。必ずdry-runで実際の約定環境をシミュレーションすべきだ。

ミス2:少額取引まですべて実行する

乖離が0.3%の資産までリバランスすると手数料だけがかかる。最小取引金額(例:10万ウォン)を設定し、それ以下は無視する。

ミス3:ルールを感情的に変更する

「今回は半導体がもっと上がりそうだから比率を15%に上げよう」はリバランスではなく投機だ。ルール変更は必ずYAMLファイルに記録し、Gitコミットメッセージに理由を残す。最低1週間のクールダウン期間を設け、感情ではなくデータで判断する。

ミス4:税金の影響を無視する

海外ETFを保有している場合、リバランスで発生する譲渡益が年間250万ウォンを超えると22%の税金が発生する。年末にまとめてリバランスすると税金爆弾を受ける可能性がある。四半期ごとに分けて実行するか、買い中心のリバランス(キャッシュフローリバランス)が有利だ。

クイズ

Q1. リバランス自動化の核心目的は収益の最大化か? 正解:いいえ。核心目的は意思決定から感情を排除し、ルールを一貫して実行することだ。Vanguardの研究によると、リバランスの最大の効果はリスク(標準偏差)を設計レベルに維持することである。

Q2. ハイブリッドリバランス方式で「緊急リバランス」が発動される条件は? 正解:いずれかの資産が目標比率に対して±5%(または設定した閾値)以上乖離した場合、定期チェック日を待たずに即座にリバランスを実行する。

Q3. キャッシュフローリバランスとは何か、その利点は? 正解:毎月の積立式追加入金額を、不足比率の資産に優先配分する方式だ。既存保有資産を売却しないため、手数料と税金が発生しない。

Q4. バックテスト結果だけでリバランス戦略を確定してはいけない理由は? 正解:バックテストにはスリッページ、気配値スプレッド、約定失敗、リアルタイムの価格変動が反映されない。必ずdry-runで実際の約定環境をシミュレーションすべきだ。

Q5. リバランスルールをYAMLファイル+Gitで管理する理由は? 正解:ルール変更の履歴を追跡し、感情的な変更を防止するためだ。コミットメッセージに変更理由を残せば、後で「なぜこの比率に変更したのか」を確認できる。

Q6. 国内ETF中心でポートフォリオを構成するとリバランスコストが減る理由は? 正解:国内ETFは売買差益が非課税(2025年基準)のため、リバランスによる税負担がない。海外ETFは譲渡所得税22%(250万ウォン控除後)が発生し、頻繁なリバランス時にコストが増大する。

参考資料