Skip to content

Split View: 개발자를 위한 ETF 투자 전략 — 자산배분부터 퀀트 백테스팅까지

✨ Learn with Quiz
|

개발자를 위한 ETF 투자 전략 — 자산배분부터 퀀트 백테스팅까지

ETF Investment Strategies

들어가며

개발자는 바쁩니다. 주식 차트를 하루 종일 볼 시간도, 개별 종목을 분석할 여력도 부족합니다. 하지만 돈은 일해야 합니다. **ETF(Exchange Traded Fund)**는 개별 종목 분석 없이도 분산 투자가 가능한 최적의 도구이며, 자산배분 전략과 결합하면 적은 시간으로 안정적인 수익을 추구할 수 있습니다.

이 글에서는 개발자답게 데이터와 코드로 접근하는 ETF 투자 전략을 다룹니다.

ETF 기초

ETF vs 개별주 vs 펀드

| 특성        | 개별주      | 액티브 펀드    | ETF (패시브)  |
|------------|-----------|-------------|-------------|
| 분산 투자    | ❌ 직접    | ✅ 펀드매니저  | ✅ 인덱스 추종 |
| 보수(비용)   | 없음      | 1~2%/| 0.03~0.5%/|
| 거래        | 실시간     | 11| 실시간       |
| 투명성      | 높음      | 낮음         | 높음         |
| 필요 시간   | 많음      | 적음         | 매우 적음     |

핵심 ETF 유니버스

# 글로벌 자산배분에 사용할 핵심 ETF
CORE_ETFS = {
    # 주식
    "VTI": "미국 전체 주식시장",
    "VXUS": "미국 외 선진국+신흥국 주식",
    "VWO": "신흥국 주식",
    "QQQ": "나스닥 100 (기술주)",

    # 채권
    "BND": "미국 종합 채권",
    "TLT": "미국 장기 국채 (20년+)",
    "IEF": "미국 중기 국채 (7-10년)",
    "TIP": "물가연동채권 (TIPS)",

    # 대체자산
    "GLD": "금",
    "VNQ": "미국 리츠 (부동산)",
    "DBC": "원자재",

    # 한국
    "KODEX200": "코스피200",
    "TIGER미국S&P500": "S&P500 (원화)",
    "ACE미국나스닥100": "나스닥100 (원화)",
}

자산배분 전략

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,    # 리츠
    "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


# 분기별 체크 (크론잡으로 실행)
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. 💰 비용을 줄여라
   - ETF 보수 0.1% 미만 선택
   - 잦은 매매 = 세금 + 수수료 증가

5. 🧘 장기 투자하라
   - 최소 10년 이상 투자 기간
   - 시장 타이밍 잡으려 하지 마라
   - "Time in the market > Timing the market"

퀴즈

Q1. 올웨더 포트폴리오의 핵심 아이디어는?

경제의 4가지 계절(성장/침체 × 인플레/디플레) 각각에 유리한 자산을 보유하여, 어떤 경제 상황에서도 안정적인 수익을 추구합니다.

Q2. 리밸런싱의 목적은?

시장 변동으로 인해 목표 비중에서 벗어난 포트폴리오를 원래 비중으로 되돌립니다. 자연스럽게 "비싸게 팔고, 싸게 사는" 효과가 있습니다.

Q3. DCA(Dollar Cost Averaging)의 장점은?

정기적으로 일정 금액을 투자하여 매수 단가를 평균화합니다. 시장 타이밍을 잡을 필요가 없고, 감정적 투자를 방지합니다.

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를 줍니다.

참고 자료

※ 이 글은 투자 권유가 아닌 교육 목적의 콘텐츠입니다. 투자 결정은 본인의 판단과 책임 하에 이루어져야 합니다.

ETF Investment Strategies for Developers — From Asset Allocation to Quant Backtesting

ETF Investment Strategies

Introduction

Developers are busy. There is no time to stare at stock charts all day or the bandwidth to analyze individual stocks. But money needs to work. ETFs (Exchange Traded Funds) are the optimal tool for diversified investing without individual stock analysis, and when combined with asset allocation strategies, they enable stable returns with minimal time commitment.

In this article, we approach ETF investment strategies the developer way — with data and code.

ETF Basics

ETF vs Individual Stocks vs Funds

| Feature         | Individual Stocks | Active Funds     | ETF (Passive)     |
|----------------|-------------------|------------------|-------------------|
| Diversification | No (manual)       | Yes (fund mgr)   | Yes (index track) |
| Fees (cost)     | None              | 1-2%/year        | 0.03-0.5%/year    |
| Trading         | Real-time         | Once per day      | Real-time         |
| Transparency    | High              | Low               | High              |
| Time required   | A lot             | Little            | Very little       |

Core ETF Universe

# Core ETFs for global asset allocation
CORE_ETFS = {
    # Equities
    "VTI": "US Total Stock Market",
    "VXUS": "Non-US Developed + Emerging Markets",
    "VWO": "Emerging Markets",
    "QQQ": "NASDAQ 100 (Tech Stocks)",

    # Bonds
    "BND": "US Aggregate Bonds",
    "TLT": "US Long-Term Treasury (20+ years)",
    "IEF": "US Intermediate Treasury (7-10 years)",
    "TIP": "Treasury Inflation-Protected Securities (TIPS)",

    # Alternatives
    "GLD": "Gold",
    "VNQ": "US REITs (Real Estate)",
    "DBC": "Commodities",

    # Korea
    "KODEX200": "KOSPI 200",
    "TIGER US S&P500": "S&P 500 (KRW denominated)",
    "ACE US NASDAQ100": "NASDAQ 100 (KRW denominated)",
}

Asset Allocation Strategies

1. All Weather Portfolio (Ray Dalio)

ALL_WEATHER = {
    "VTI": 0.30,   # US Stocks 30%
    "TLT": 0.40,   # Long-Term Treasury 40%
    "IEF": 0.15,   # Intermediate Treasury 15%
    "GLD": 0.075,  # Gold 7.5%
    "DBC": 0.075,  # Commodities 7.5%
}
# Feature: Prepared for all 4 economic seasons (Growth/Recession x Inflation/Deflation)
# Average annual return: ~7% (2005-2025)
# Maximum drawdown: ~12%

2. Permanent Portfolio (Harry Browne)

PERMANENT = {
    "VTI": 0.25,   # Stocks 25% (Growth)
    "TLT": 0.25,   # Long-Term Treasury 25% (Recession)
    "GLD": 0.25,   # Gold 25% (Inflation)
    "BIL": 0.25,   # Short-Term Treasury 25% (Safe Asset)
}
# Feature: Extremely simple, 25% always shines in any economic condition
# Average annual return: ~6%
# Maximum drawdown: ~10%

3. 60/40 Portfolio (Traditional)

CLASSIC_60_40 = {
    "VTI": 0.60,   # Stocks 60%
    "BND": 0.40,   # Bonds 40%
}
# Feature: The most traditional and simple
# Average annual return: ~8%
# Maximum drawdown: ~20%

4. Core-Satellite Strategy

CORE_SATELLITE = {
    # Core (80%) — Passive, low-cost
    "VTI": 0.50,    # US Total Market
    "VXUS": 0.20,   # International Stocks
    "BND": 0.10,    # Bonds

    # Satellite (20%) — Alpha seeking
    "QQQ": 0.10,    # Tech Growth
    "VNQ": 0.05,    # REITs
    "GLD": 0.05,    # Gold
}
# Feature: Capture market returns (beta) + seek some alpha

Python Backtesting

Data Collection

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:
    """Download ETF price data"""
    data = yf.download(tickers, start=start, auto_adjust=True)
    prices = data["Close"]
    returns = prices.pct_change().dropna()
    return prices, returns


# Download data
tickers = ["VTI", "TLT", "IEF", "GLD", "DBC", "BND"]
prices, returns = get_etf_data(tickers)

Portfolio Backtest

def backtest_portfolio(
    returns: pd.DataFrame,
    weights: dict,
    rebalance_freq: str = "Q",  # Q=Quarterly, M=Monthly, Y=Yearly
    initial_capital: float = 10000
) -> pd.DataFrame:
    """Portfolio backtest"""
    tickers = list(weights.keys())
    w = np.array([weights[t] for t in tickers])

    # Daily portfolio returns
    port_returns = (returns[tickers] * w).sum(axis=1)

    # Rebalancing simulation
    if rebalance_freq:
        rebalance_dates = returns.resample(rebalance_freq).last().index

    # Cumulative returns
    cumulative = (1 + port_returns).cumprod() * initial_capital

    # Performance metrics
    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 = {
        "Total Return": f"{total_return:.1%}",
        "CAGR": f"{annual_return:.1%}",
        "Annual Volatility": f"{annual_vol:.1%}",
        "Sharpe Ratio": f"{sharpe:.2f}",
        "Maximum Drawdown (MDD)": f"{max_dd:.1%}",
    }

    return cumulative, stats


# Backtest each strategy
strategies = {
    "All Weather": ALL_WEATHER,
    "Permanent Portfolio": 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}")

Rebalancing Simulation

def simulate_rebalancing(
    prices: pd.DataFrame,
    weights: dict,
    initial_capital: float = 10000,
    rebalance_freq: str = "Q"
) -> pd.DataFrame:
    """Actual rebalancing trade simulation"""
    tickers = list(weights.keys())
    target_weights = np.array([weights[t] for t in tickers])

    # Initial positions
    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 portfolio value
        current_value = sum(
            shares[t] * prices[t].loc[date]
            for t in tickers
        )
        portfolio_value.append(current_value)

        # Is it a rebalancing date?
        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:  # Rebalance only when drift exceeds 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

Dollar Cost Averaging (DCA)

def dollar_cost_averaging(
    prices: pd.DataFrame,
    ticker: str,
    monthly_amount: float = 500000,  # 500,000 KRW per month
    start_date: str = "2015-01-01"
) -> dict:
    """Dollar cost averaging simulation"""
    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 {
        "Total Invested": f"{total_invested:,.0f} KRW",
        "Current Value": f"{final_value:,.0f} KRW",
        "Return": f"{total_return:.1f}%",
        "Average Cost Basis": f"{avg_cost:,.0f} KRW",
        "Current Price": f"{prices[ticker].iloc[-1]:,.0f} KRW",
        "Investment Period": f"{len(monthly_prices)} months",
    }

Tax and Cost Considerations

# Korean ETF tax structure
TAX_RULES = {
    "Domestic Stock ETF": {
        "Capital Gains": "Tax-free",
        "Distributions": "15.4% dividend income tax",
        "Examples": "KODEX200, TIGER KRX300",
    },
    "Domestic Other ETF": {
        "Capital Gains": "15.4% dividend income tax (holding period basis)",
        "Distributions": "15.4% dividend income tax",
        "Examples": "KODEX Gold, TIGER US S&P500",
    },
    "Direct Overseas Investment": {
        "Capital Gains": "22% capital gains tax (after 2.5 million KRW exemption)",
        "Distributions": "15% withholding tax",
        "Examples": "VTI, QQQ, TLT (direct US purchase)",
    },
}

def calculate_tax(profit: float, invest_type: str) -> float:
    """After-tax profit calculation"""
    if invest_type == "domestic_stock":
        return profit  # Tax-free
    elif invest_type == "domestic_other":
        return profit * (1 - 0.154)
    elif invest_type == "overseas_direct":
        taxable = max(0, profit - 2500000)  # 2.5 million KRW exemption
        tax = taxable * 0.22
        return profit - tax

Automatic Rebalancing Alerts

import yfinance as yf
from datetime import datetime

def check_rebalance_needed(
    portfolio: dict,
    target_weights: dict,
    threshold: float = 0.05
) -> list:
    """Check if rebalancing is needed"""
    # Get current prices
    tickers = list(portfolio.keys())
    current_prices = {}
    for t in tickers:
        data = yf.Ticker(t)
        current_prices[t] = data.info.get("regularMarketPrice", 0)

    # Current portfolio value
    total_value = sum(
        portfolio[t]["shares"] * current_prices[t]
        for t in tickers
    )

    # Drift check
    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 = "Buy" if current_weight < target_weight else "Sell"
            amount = abs(current_weight - target_weight) * total_value

            alerts.append({
                "ticker": ticker,
                "Current Weight": f"{current_weight:.1%}",
                "Target Weight": f"{target_weight:.1%}",
                "Drift": f"{drift:.1%}",
                "Action": f"{action} {amount:,.0f} KRW",
            })

    return alerts


# Quarterly check (run as cron job)
alerts = check_rebalance_needed(my_portfolio, ALL_WEATHER)
if alerts:
    print("Warning: Rebalancing needed!")
    for a in alerts:
        print(f"  {a['ticker']}: {a['Current Weight']} -> {a['Target Weight']} ({a['Action']})")

Investment Principles for Developers

1. Automate
   - Monthly auto-transfer -> auto-purchase
   - Automate rebalancing alerts
   - Leave no room for emotions to interfere

2. Decide with data
   - Never use a strategy without backtesting
   - Past returns are not equal to future returns, but useful for risk measurement

3. Save time
   - Portfolio check: once per quarter is enough
   - Time spent on news/charts -> redirect to coding/learning

4. Reduce costs
   - Choose ETFs with expense ratios under 0.1%
   - Frequent trading = more taxes + more fees

5. Invest long-term
   - Minimum 10+ year investment horizon
   - Do not try to time the market
   - "Time in the market > Timing the market"

Quiz

Q1. What is the core idea of the All Weather portfolio?

It holds assets that perform well in each of the 4 economic seasons (Growth/Recession x Inflation/Deflation), pursuing stable returns regardless of the economic environment.

Q2. What is the purpose of rebalancing?

It restores a portfolio that has drifted from target weights back to its original allocation. It naturally creates a "sell high, buy low" effect.

Q3. What are the advantages of DCA (Dollar Cost Averaging)?

By investing a fixed amount regularly, it averages the purchase price. It eliminates the need to time the market and prevents emotional investing.

Q4. How is capital gains tax calculated for direct overseas ETF investment?

22% is taxed on gains after deducting 2.5 million KRW. Example: 10 million KRW profit results in (10M - 2.5M) x 0.22 = 1.65 million KRW tax.

Q5. What is the Sharpe Ratio?

(Return - Risk-free return) / Volatility, measuring excess return per unit of risk. Higher is more efficient. Generally, 1.0 or above is considered good.

Q6. What is the Core-Satellite strategy?

A strategy where the majority of assets (80%) are in low-cost index ETFs (core) to capture market returns, while a portion (20%) is in theme/sector ETFs (satellite) to seek alpha (excess returns).

Q7. What is MDD (Maximum Drawdown)?

The maximum drawdown is the largest peak-to-trough decline. It shows the worst-case scenario for a portfolio and serves as a measure of psychological resilience.

Conclusion

For developers, the appeal of ETF investing is its systematizability. Set a strategy, configure auto-purchases, and just receive quarterly rebalancing alerts. Invest the time you would spend staring at charts into technical learning instead — in the long run, that yields a higher ROI.

References

Note: This article is educational content, not investment advice. Investment decisions should be made based on your own judgment and responsibility.