Skip to content
Published on

The Complete Guide to ETF Portfolio Rebalancing — Asset Allocation and Automation for Developers

Authors
  • Name
    Twitter
ETF Portfolio Rebalancing

Introduction

When starting to invest, people focus on "which ETFs to buy," but in the long run, what determines returns is asset allocation and rebalancing. Rebalancing is the process of restoring your portfolio to its target ratios when asset weights have drifted from the goal.

This article covers rebalancing from theory to Python automation, taking a practical developer's perspective.

Why Rebalancing Is Necessary

Weight Drift Example

# Initial portfolio (target weights)
portfolio = {
    "Korean Stocks (KODEX 200)": 0.30,    # 30%
    "US Stocks (VOO)": 0.40,               # 40%
    "Bonds (TLT)": 0.20,                   # 20%
    "Gold (GLD)": 0.10                      # 10%
}

# After 1 year (stocks up, bonds down)
# Korean Stocks: 30% -> 28% (-2%)
# US Stocks:     40% -> 48% (+8%)  <- Exceeds target!
# Bonds:         20% -> 16% (-4%)  <- Below target!
# Gold:          10% -> 8%  (-2%)

# Without rebalancing:
# - Stock allocation rises to 76%, creating excessive risk exposure
# - The intended diversification effect is lost

Benefits of Rebalancing

1. Risk management: Prevents concentration in a single asset
2. Buy low/sell high: Contrarian effect of selling what has risen and buying what has fallen
3. Discipline: Prevents emotional investment decisions
4. Long-term return improvement: Better risk-adjusted returns (Sharpe ratio)

Rebalancing Strategies

1. Calendar Rebalancing (Time-Based)

# Rebalance at fixed intervals
# Pros: Simple and easy to execute
# Cons: Executes regardless of market conditions

# Quarterly rebalancing (most common)
REBALANCE_SCHEDULE = "quarterly"  # monthly, quarterly, semi-annually, annually

# Recommended frequency:
# - Monthly: When transaction costs are low (commission-free brokerages)
# - Quarterly: Suitable for most investors
# - Semi-annual/Annual: When tax efficiency is prioritized

2. Threshold Rebalancing

# Rebalance when deviation exceeds a set range from targets
# Pros: Responds immediately to large movements
# Cons: Frequent trades during extreme market volatility

THRESHOLD = 0.05  # Rebalance when 5% drift occurs

def needs_rebalancing(current_weights: dict, target_weights: dict, threshold: float) -> bool:
    """Determine if rebalancing is needed"""
    for asset, target in target_weights.items():
        current = current_weights.get(asset, 0)
        if abs(current - target) > threshold:
            return True
    return False

# Example: US stocks go from 40% -> 46% (6% drift) -> Rebalancing needed!
# Quarterly check + 5% threshold combination
# - Check portfolio every quarter
# - Rebalance if any asset drifts more than 5%
# - If no drift, wait until next quarter

def hybrid_rebalancing_check(
    current_weights: dict,
    target_weights: dict,
    threshold: float = 0.05,
    last_rebalance_date: str = None,
    max_interval_days: int = 180  # 6 months
) -> dict:
    """Hybrid rebalancing strategy"""
    from datetime import datetime, timedelta

    needs_rebal = False
    reason = ""

    # Threshold check
    for asset, target in target_weights.items():
        current = current_weights.get(asset, 0)
        drift = abs(current - target)
        if drift > threshold:
            needs_rebal = True
            reason = f"{asset}: {current:.1%} (target {target:.1%}, drift {drift:.1%})"
            break

    # Maximum interval check
    if last_rebalance_date and not needs_rebal:
        last_date = datetime.strptime(last_rebalance_date, "%Y-%m-%d")
        if (datetime.now() - last_date).days > max_interval_days:
            needs_rebal = True
            reason = f"Maximum rebalancing interval of {max_interval_days} days exceeded"

    return {
        "needs_rebalancing": needs_rebal,
        "reason": reason
    }

Python Rebalancing Calculator

Basic Calculation

def calculate_rebalancing(
    portfolio: dict,
    target_weights: dict,
    total_value: float
) -> dict:
    """Calculate rebalancing trades"""
    trades = {}

    for asset, target_weight in target_weights.items():
        current_value = portfolio.get(asset, {}).get("value", 0)
        target_value = total_value * target_weight
        diff = target_value - current_value

        trades[asset] = {
            "current_value": current_value,
            "current_weight": current_value / total_value if total_value > 0 else 0,
            "target_value": target_value,
            "target_weight": target_weight,
            "trade_amount": diff,
            "action": "Buy" if diff > 0 else "Sell" if diff < 0 else "Hold"
        }

    return trades


# Usage example
portfolio = {
    "KODEX 200": {"value": 2800000, "price": 35000},
    "VOO": {"value": 4800000, "price": 550000},
    "TLT": {"value": 1600000, "price": 85000},
    "GLD": {"value": 800000, "price": 260000}
}

target_weights = {
    "KODEX 200": 0.30,
    "VOO": 0.40,
    "TLT": 0.20,
    "GLD": 0.10
}

total_value = sum(a["value"] for a in portfolio.values())
trades = calculate_rebalancing(portfolio, target_weights, total_value)

for asset, trade in trades.items():
    if trade["action"] != "Hold":
        print(f"{asset}: {trade['action']} {abs(trade['trade_amount']):,.0f} KRW")
        print(f"  Current: {trade['current_weight']:.1%} -> Target: {trade['target_weight']:.1%}")

Cash Flow Rebalancing

def cash_flow_rebalancing(
    portfolio: dict,
    target_weights: dict,
    total_value: float,
    new_cash: float
) -> dict:
    """Rebalance using new cash (minimize selling)"""
    new_total = total_value + new_cash

    # Calculate shortfall for each asset
    shortfalls = {}
    for asset, target_weight in target_weights.items():
        current_value = portfolio.get(asset, {}).get("value", 0)
        target_value = new_total * target_weight
        shortfall = max(0, target_value - current_value)
        shortfalls[asset] = shortfall

    total_shortfall = sum(shortfalls.values())

    # Allocate new cash proportionally to shortfalls
    allocations = {}
    for asset, shortfall in shortfalls.items():
        if total_shortfall > 0:
            allocation = new_cash * (shortfall / total_shortfall)
        else:
            allocation = new_cash * target_weights[asset]
        allocations[asset] = round(allocation)

    return allocations


# Example: Additional 1 million KRW investment
allocations = cash_flow_rebalancing(portfolio, target_weights, total_value, 1000000)
print("New cash allocation:")
for asset, amount in allocations.items():
    print(f"  {asset}: Buy {amount:,} KRW")

# Adjust weights using only new cash, with no selling!

Tax-Efficient Strategies

Tax-Loss Harvesting

def tax_loss_harvesting(
    holdings: list,
    threshold_loss_pct: float = -0.05
) -> list:
    """Sell losing positions for tax savings"""
    harvest_candidates = []

    for holding in holdings:
        gain_pct = (holding["current_price"] - holding["avg_cost"]) / holding["avg_cost"]

        if gain_pct < threshold_loss_pct:
            harvest_candidates.append({
                "asset": holding["asset"],
                "loss_amount": (holding["current_price"] - holding["avg_cost"]) * holding["quantity"],
                "gain_pct": gain_pct,
                "action": "Sell and replace with similar ETF"
            })

    return harvest_candidates

# For Korea:
# - Domestic stock ETFs: Tax-free (capital gains)
# - Overseas ETFs: 22% capital gains tax (2.5 million KRW exemption)
# - Dividend income tax: 15.4%
#
# Tax-Loss Harvesting is effective for overseas ETFs

Asset Allocation Model Examples

Portfolio Models

# 1. Conservative Portfolio (stability-focused)
conservative = {
    "Korean Bonds (KOSEF 10Y Treasury)": 0.40,
    "US Bonds (TLT)": 0.20,
    "Korean Stocks (KODEX 200)": 0.15,
    "US Stocks (VOO)": 0.15,
    "Gold (GLD)": 0.10
}

# 2. Balanced Portfolio (general)
balanced = {
    "Korean Stocks (KODEX 200)": 0.25,
    "US Stocks (VOO)": 0.30,
    "Developed Markets (VEA)": 0.10,
    "Bonds (AGG)": 0.25,
    "Gold (GLD)": 0.10
}

# 3. Aggressive Portfolio (growth-focused)
aggressive = {
    "US Stocks (VOO)": 0.35,
    "NASDAQ (QQQ)": 0.20,
    "Korean Stocks (KODEX 200)": 0.15,
    "Emerging Markets (VWO)": 0.10,
    "Bonds (AGG)": 0.15,
    "Gold (GLD)": 0.05
}

# 4. All Weather - Ray Dalio
all_weather = {
    "US Stocks (VOO)": 0.30,
    "US Long-Term Bonds (TLT)": 0.40,
    "US Intermediate Bonds (IEF)": 0.15,
    "Gold (GLD)": 0.075,
    "Commodities (DBC)": 0.075
}

Automation: Rebalancing Alert Bot

#!/usr/bin/env python3
"""rebalance_checker.py - Rebalancing check script"""
import json
from datetime import datetime


def check_and_notify():
    # Portfolio data (in practice, use brokerage API or manual input)
    portfolio = {
        "KODEX 200": {"value": 2800000, "shares": 80},
        "VOO": {"value": 4800000, "shares": 8},
        "TLT": {"value": 1600000, "shares": 18},
        "GLD": {"value": 800000, "shares": 3}
    }

    target = {
        "KODEX 200": 0.30,
        "VOO": 0.40,
        "TLT": 0.20,
        "GLD": 0.10
    }

    total = sum(a["value"] for a in portfolio.values())

    # Check drift
    alerts = []
    for asset, target_w in target.items():
        current_w = portfolio[asset]["value"] / total
        drift = current_w - target_w
        if abs(drift) > 0.05:
            direction = "over" if drift > 0 else "under"
            alerts.append(
                f"Warning: {asset}: currently {current_w:.1%} (target {target_w:.1%}, {abs(drift):.1%} {direction})"
            )

    if alerts:
        message = f"Portfolio Rebalancing Alert ({datetime.now().strftime('%Y-%m-%d')})\n\n"
        message += "\n".join(alerts)
        message += f"\n\nTotal Assets: {total:,.0f} KRW"
        print(message)
        # Add Telegram/Slack notification integration here
    else:
        print("Portfolio normal - no rebalancing needed")


if __name__ == "__main__":
    check_and_notify()
# Check every Monday with crontab
# 0 9 * * 1 python3 /path/to/rebalance_checker.py

Conclusion

Key points for ETF rebalancing:

  1. Hybrid strategy recommended: Quarterly check + 5% threshold combination
  2. Leverage cash flows: Buy underweight assets with new investment funds (minimize selling)
  3. Tax efficiency: Consider Tax-Loss Harvesting for overseas ETFs
  4. Automation: Monitor drift with Python scripts
  5. Maintain discipline: Remove emotions, execute by rules

Quiz (7 Questions)

Q1. What is the core purpose of rebalancing? To restore the portfolio's asset weights to target allocation, managing risk and maintaining diversification benefits.

Q2. What is the difference between time-based vs threshold-based rebalancing? Time-based: Executes at fixed intervals. Threshold-based: Executes when deviation exceeds a set percentage.

Q3. What is the advantage of cash flow rebalancing? Adjust weights using only new cash without selling, minimizing transaction costs and taxes.

Q4. What is Tax-Loss Harvesting? A strategy of selling losing positions for tax deductions and buying similar ETFs as replacements.

Q5. Why is the bond allocation high in the All Weather portfolio? To pursue stable returns regardless of the economic environment (growth/recession x inflation/deflation).

Q6. What is the basic capital gains tax exemption for overseas ETFs in Korea? 2.5 million KRW (22% tax on the excess amount).

Q7. What is the "buy low/sell high" effect in rebalancing? By selling appreciated assets (high) and buying depreciated assets (low), contrarian investing is naturally achieved.