Skip to content
Published on

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

Authors
  • Name
    Twitter
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를 줍니다.

참고 자료

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