- Authors
- Name
- Introduction
- ETF Basics
- Asset Allocation Strategies
- Python Backtesting
- Dollar Cost Averaging (DCA)
- Tax and Cost Considerations
- Automatic Rebalancing Alerts
- Investment Principles for Developers
- Quiz
- Conclusion
- References

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.