Skip to content
Published on

Core-Satellite ETF Automated Rebalancing

Authors
  • Name
    Twitter
Core-Satellite ETF Automated Rebalancing

What is the Core-Satellite Strategy

Core-Satellite is an asset allocation strategy that divides the portfolio into two roles. The Core consists of low-cost index ETFs that track the overall market, pursuing stable returns. The Satellite consists of specific sector, theme, and style ETFs, aiming for excess returns (alpha).

This strategy sits between 100% index investing and 100% active investing. It is also the essence of the personal investor asset allocation proposed by David Swensen (Yale endowment manager) in Unconventional Success.

Core Principles of Core-Satellite:

  • Core weight 60-80%, Satellite weight 20-40%
  • Core is rarely touched (rebalancing 1-2 times per year)
  • Satellites can be swapped based on market conditions (reviewed quarterly)
  • Keep total portfolio expense ratio (TER) below 0.3%

Separating the Roles of Core and Satellite

Core: Capturing Market Beta

The core's objective is simple: obtaining market returns at the lowest possible cost. It doesn't try to "beat the market."

Core ETF Selection Criteria:

  1. Expense ratio 0.1% or lower (fees are a guaranteed negative return)
  2. AUM of 100 billion KRW or more (ensuring liquidity)
  3. Minimal Tracking Error (low divergence from the index)
  4. Dividend reinvestment structure (prefer Total Return index tracking)

Core ETF Candidates Available in Korea (as of 2026):

ETFCodeTracking IndexExpenseAUMPurpose
TIGER US S&P500360750S&P 5000.07%6.5T KRWUS large-cap
KODEX US S&P500TR379800S&P 500 TR0.05%3.2T KRWUS large-cap (dividend reinvest)
KODEX 200069500KOSPI 2000.05%5.8T KRWKorean large-cap
TIGER US NASDAQ100133690NASDAQ-1000.07%4.1T KRWUS tech stocks
KODEX US Treasury Ultra 30Y Futures (H)304660US Treasury 30Y0.09%280B KRWUS long-term bonds
TIGER US Treasury 10Y Futures305080US Treasury 10Y0.09%1.2T KRWUS mid-term bonds
KODEX Composite Bond (AA- and above) Active443160Korean bonds0.05%350B KRWDomestic bonds

Satellite: Pursuing Themes and Alpha

Satellites concentrate on specific sectors, regions, or styles to seek excess returns beyond the market. Unlike core holdings, they can be swapped, and if conviction is low, their weight can be reduced or held in cash.

Types of Satellite ETFs:

TypeExamplesCharacteristicsRisk Level
SectorSemiconductors, batteries, biotechFocused on specific industryHigh
ThemeAI, robotics, clean energyFollowing growth trendsHigh
RegionIndia, Japan, VietnamSpecific country growthMedium-High
StyleHigh dividend, value, small-capFactor-based investingMedium
AlternativeGold, commodities, REITsLow correlation, diversifiedMedium

Satellite Replacement Decision Criteria:

  • Is the investment thesis still valid?
  • What was the performance vs benchmark last quarter?
  • Has a similar ETF with lower fees been launched?
  • Has the correlation within the portfolio become excessively high?

Portfolio Design in Practice: 3 Scenarios

Scenario 1: Stable Growth (Conservative Investor)

# Target: 6-8% annual return, max drawdown within 15%
# For: 10-15 years before retirement, principal preservation priority
portfolio:
  name: 'Stable Growth'
  core_weight: 0.80
  satellite_weight: 0.20

  core:
    - ticker: 'KODEX US S&P500TR'
      weight: 0.30
      role: 'US market beta'
    - ticker: 'KODEX 200'
      weight: 0.15
      role: 'Korean market beta'
    - ticker: 'TIGER US Treasury 10Y Futures'
      weight: 0.20
      role: 'Defense during rate declines'
    - ticker: 'KODEX Composite Bond (AA- and above) Active'
      weight: 0.15
      role: 'Stable interest income'

  satellite:
    - ticker: 'TIGER Gold & Silver Futures (H)'
      weight: 0.10
      role: 'Inflation hedge'
    - ticker: 'TIGER US Dividend Dow Jones'
      weight: 0.10
      role: 'Dividend income'

  expected:
    annual_return: '6-8%'
    max_drawdown: '-15%'
    total_expense_ratio: '0.07%'

Scenario 2: Growth-Oriented (30s Office Worker)

# Target: 10-12% annual return, willing to accept 25% max drawdown
# For: 15+ year investment horizon, aggressive growth
portfolio:
  name: 'Growth-Oriented'
  core_weight: 0.65
  satellite_weight: 0.35

  core:
    - ticker: 'TIGER US S&P500'
      weight: 0.35
      role: 'US market beta'
    - ticker: 'TIGER US NASDAQ100'
      weight: 0.15
      role: 'Tech stock growth'
    - ticker: 'KODEX 200'
      weight: 0.10
      role: 'Korean market beta'
    - ticker: 'TIGER US Treasury 10Y Futures'
      weight: 0.05
      role: 'Minimum bond allocation'

  satellite:
    - ticker: 'TIGER Semiconductor'
      weight: 0.12
      role: 'Semiconductor cycle alpha'
    - ticker: 'KODEX Secondary Battery Industry'
      weight: 0.08
      role: 'Secondary battery growth'
    - ticker: 'TIGER Fn Semiconductor TOP10'
      weight: 0.08
      role: 'Korean semiconductor concentration'
    - ticker: 'TIGER India Nifty50'
      weight: 0.07
      role: 'India market growth'

  expected:
    annual_return: '10-12%'
    max_drawdown: '-25%'
    total_expense_ratio: '0.15%'

Scenario 3: All-Weather (Volatility Minimization)

# Target: Pursue positive returns in any market environment
# For: Investors who extremely dislike volatility
# Reference: Variation of Ray Dalio's All Weather strategy
portfolio:
  name: 'All-Weather'
  core_weight: 0.85
  satellite_weight: 0.15

  core:
    - ticker: 'TIGER US S&P500'
      weight: 0.25
      role: 'Returns during economic growth'
    - ticker: 'TIGER US Treasury 10Y Futures'
      weight: 0.25
      role: 'Recession defense'
    - ticker: 'KODEX US Treasury Ultra 30Y Futures (H)'
      weight: 0.15
      role: 'Deflation defense'
    - ticker: 'KODEX Composite Bond (AA- and above) Active'
      weight: 0.10
      role: 'Stable interest'
    - ticker: 'TIGER Gold & Silver Futures (H)'
      weight: 0.10
      role: 'Inflation hedge'

  satellite:
    - ticker: 'KODEX 200'
      weight: 0.10
      role: 'Korean market exposure'
    - ticker: 'TIGER Crude Oil Futures Enhanced (H)'
      weight: 0.05
      role: 'Commodity inflation response'

  expected:
    annual_return: '5-7%'
    max_drawdown: '-10%'
    total_expense_ratio: '0.09%'

Implementing Automated Rebalancing

Separate Rebalancing for Core vs Satellite

Core and satellite have different rebalancing cycles and criteria. They should not be combined under a single rule.

"""
Core-Satellite Separate Rebalancing Engine
- Core: Twice a year + emergency when drift exceeds 5%
- Satellite: Quarterly + emergency when drift exceeds 7%
"""
from dataclasses import dataclass


@dataclass
class RebalanceRule:
    """Rebalancing rules by asset type."""
    section: str            # "core" or "satellite"
    scheduled_frequency: str  # "semi-annual" or "quarterly"
    drift_threshold: float    # Drift threshold
    min_trade_krw: int        # Minimum trade amount


RULES = {
    "core": RebalanceRule(
        section="core",
        scheduled_frequency="semi-annual",
        drift_threshold=0.05,
        min_trade_krw=200_000,
    ),
    "satellite": RebalanceRule(
        section="satellite",
        scheduled_frequency="quarterly",
        drift_threshold=0.07,
        min_trade_krw=100_000,
    ),
}


def check_section(
    section: str,
    holdings: dict,
    targets: list[dict],
    total_value: int,
) -> dict:
    """Determine if a specific section needs rebalancing."""
    rule = RULES[section]
    drift_alerts = []

    for target in targets:
        code = target["code"]
        current_value = holdings.get(code, {}).get("value_krw", 0)
        current_weight = current_value / total_value if total_value > 0 else 0
        target_weight = target["weight"]
        drift = abs(current_weight - target_weight)

        if drift > rule.drift_threshold:
            drift_alerts.append({
                "ticker": target["ticker"],
                "code": code,
                "target": target_weight,
                "current": round(current_weight, 4),
                "drift": round(drift, 4),
                "action_needed": True,
            })

    return {
        "section": section,
        "rule": rule,
        "needs_rebalancing": len(drift_alerts) > 0,
        "alerts": drift_alerts,
    }


def run_core_satellite_rebalance(portfolio_config: dict, holdings: dict):
    """Execute rebalancing for core and satellite separately."""
    total_value = sum(h["value_krw"] for h in holdings.values())

    print(f"Total assets: ₩{total_value:,}\n")

    for section in ["core", "satellite"]:
        targets = portfolio_config[section]
        result = check_section(section, holdings, targets, total_value)

        section_label = "Core" if section == "core" else "Satellite"
        print(f"=== {section_label} Check ===")
        print(f"Rebalancing frequency: {result['rule'].scheduled_frequency}")
        print(f"Drift threshold: ±{result['rule'].drift_threshold * 100}%")

        if result["needs_rebalancing"]:
            print(f"Status: Rebalancing needed")
            for alert in result["alerts"]:
                print(
                    f"  - {alert['ticker']}: "
                    f"Target {alert['target']*100:.1f}% / "
                    f"Current {alert['current']*100:.1f}% / "
                    f"Drift {alert['drift']*100:.1f}%p"
                )
        else:
            print("Status: Within normal range")
        print()


# Usage example
config = {
    "core": [
        {"ticker": "TIGER US S&P500", "code": "360750", "weight": 0.35},
        {"ticker": "KODEX 200", "code": "069500", "weight": 0.15},
        {"ticker": "TIGER US Treasury 10Y Futures", "code": "305080", "weight": 0.15},
    ],
    "satellite": [
        {"ticker": "TIGER Semiconductor", "code": "091230", "weight": 0.12},
        {"ticker": "KODEX Secondary Battery Industry", "code": "305720", "weight": 0.08},
        {"ticker": "TIGER India Nifty50", "code": "453810", "weight": 0.07},
    ],
}

holdings = {
    "360750": {"value_krw": 4_500_000},
    "069500": {"value_krw": 1_500_000},
    "305080": {"value_krw": 1_200_000},
    "091230": {"value_krw": 1_800_000},
    "305720": {"value_krw": 600_000},
    "453810": {"value_krw": 400_000},
}

run_core_satellite_rebalance(config, holdings)

Satellite Replacement Logic

Satellites are reviewed quarterly for performance, and holdings with weakening investment theses are replaced.

"""Satellite ETF performance review and replacement decision."""

def evaluate_satellite(
    ticker: str,
    quarter_return: float,
    benchmark_return: float,
    thesis_valid: bool,
    cheaper_alternative: str | None = None,
) -> dict:
    """Determine whether a satellite ETF should be replaced."""
    excess_return = quarter_return - benchmark_return

    recommendation = "HOLD"
    reasons = []

    # If the investment thesis is invalidated
    if not thesis_valid:
        recommendation = "REPLACE"
        reasons.append("Investment thesis is no longer valid")

    # If underperforming benchmark for 3 consecutive quarters
    if excess_return < -0.05:
        recommendation = "REVIEW"
        reasons.append(
            f"Underperforming benchmark by {excess_return*100:.1f}%p"
        )

    # If a cheaper alternative exists
    if cheaper_alternative:
        reasons.append(f"Lower-fee alternative available: {cheaper_alternative}")

    return {
        "ticker": ticker,
        "quarter_return": f"{quarter_return*100:.1f}%",
        "excess_return": f"{excess_return*100:.1f}%p",
        "recommendation": recommendation,
        "reasons": reasons,
    }


# Quarterly review example
reviews = [
    evaluate_satellite(
        ticker="TIGER Semiconductor",
        quarter_return=0.12,
        benchmark_return=0.08,
        thesis_valid=True,
    ),
    evaluate_satellite(
        ticker="KODEX Secondary Battery Industry",
        quarter_return=-0.03,
        benchmark_return=0.08,
        thesis_valid=True,
    ),
    evaluate_satellite(
        ticker="TIGER India Nifty50",
        quarter_return=0.06,
        benchmark_return=0.08,
        thesis_valid=True,
        cheaper_alternative="KODEX India Nifty50 (Synthetic)",
    ),
]

print("=== Satellite Quarterly Review ===")
for r in reviews:
    print(f"\n{r['ticker']}")
    print(f"  Quarterly return: {r['quarter_return']}")
    print(f"  Excess return: {r['excess_return']}")
    print(f"  Verdict: {r['recommendation']}")
    if r["reasons"]:
        for reason in r["reasons"]:
            print(f"  - {reason}")

Correlation Management: Maintaining Diversification Benefits

One pitfall of the core-satellite strategy is when satellites become highly correlated with each other. Holding both a semiconductor ETF and a NASDAQ100 ETF simultaneously effectively results in excessive concentration in tech stocks.

Correlation Monitoring

"""Calculate ETF correlations within a portfolio."""
import numpy as np


def check_correlation(returns_matrix: dict, threshold: float = 0.8) -> list:
    """Find ETF pairs with high correlation.

    Args:
        returns_matrix: {ticker: [daily return list]}
        threshold: Warning threshold for correlation coefficient

    Returns:
        List of ETF pairs with correlation exceeding the threshold
    """
    tickers = list(returns_matrix.keys())
    data = np.array([returns_matrix[t] for t in tickers])
    corr = np.corrcoef(data)

    high_corr_pairs = []
    for i in range(len(tickers)):
        for j in range(i + 1, len(tickers)):
            if abs(corr[i][j]) > threshold:
                high_corr_pairs.append({
                    "pair": (tickers[i], tickers[j]),
                    "correlation": round(corr[i][j], 3),
                    "warning": "Weakened diversification - consider reducing one position",
                })

    return high_corr_pairs


# Example: 60-day daily returns (simulation)
np.random.seed(42)
market = np.random.normal(0.0005, 0.01, 60)

sample_returns = {
    "S&P500": market + np.random.normal(0, 0.002, 60),
    "NASDAQ100": market * 1.3 + np.random.normal(0, 0.003, 60),
    "Semiconductor": market * 1.5 + np.random.normal(0, 0.005, 60),
    "US Treasury 10Y": -market * 0.3 + np.random.normal(0, 0.003, 60),
    "Gold": np.random.normal(0.0002, 0.008, 60),
}

alerts = check_correlation(sample_returns, threshold=0.7)

print("=== Correlation Alerts ===")
for alert in alerts:
    print(f"  {alert['pair'][0]} <-> {alert['pair'][1]}: "
          f"Correlation {alert['correlation']}")
    print(f"  -> {alert['warning']}")

Core-Satellite Annual Operations Calendar

MonthCoreSatelliteOther
JanuaryScheduled rebalancingQuarterly review + annual strategyAnnual return settlement, tax review
Feb-MarDrift monitoring-Ex-dividend date check
April-Quarterly reviewISA/IRP contribution check
May-JunDrift monitoring-Mid-year review
JulyScheduled rebalancingQuarterly reviewH2 strategy adjustment
Aug-SepDrift monitoring--
October-Quarterly review + next year planISA/IRP year-end contribution plan
Nov-DecDrift monitoring-Tax-optimized trading (overseas ETF)

Quiz

Q1. What is the role of the core in a core-satellite strategy? Answer: To capture overall market returns (beta) at low cost. It doesn't try to beat the market, but pursues stable returns through index ETFs.

Q2. Why do core and satellite have different rebalancing cycles? Answer: Core follows a long-term holding principle, so semi-annual scheduled rebalancing is sufficient. Satellites can be swapped based on market conditions, so they are reviewed quarterly. Applying management cycles appropriate to each character reduces unnecessary trades.

Q3. What is the most important criterion for deciding whether to replace a satellite ETF?

Answer: Whether the investment thesis is still valid. Replacing based only on short-term underperformance leads to emotional trading, while holding despite an invalidated thesis expands losses.

Q4. Why is high correlation between ETFs in a portfolio a problem? Answer: Holding multiple highly correlated assets eliminates diversification benefits. For example, holding S&P500 + NASDAQ100 + Semiconductor ETFs simultaneously means all three drop together during a tech stock decline, causing significant damage to the entire portfolio.

Q5. Why does the All-Weather portfolio hold stocks, bonds, and gold simultaneously?

Answer: It is designed so that some assets generate returns in any environment: economic growth (stocks rise), recession (bonds rise), inflation (gold rises), deflation (long-term bonds rise). This is the core principle of Ray Dalio's All Weather strategy.

Q6. Why is expense ratio (TER) the most important factor when selecting core ETFs?

Answer: Core ETFs are held long-term (10+ years), so expenses compound over time. The difference between 0.05% and 0.5% in expense ratio creates approximately 9% return difference over 20 years. Since core tracks market returns, lower expenses mean higher real returns.

References