Skip to content
Published on

Finance: ISA IRP ETF Automation Workflow 2026

Authors
  • Name
    Twitter
Finance: ISA IRP ETF Automation Workflow 2026

Why Use ISA and IRP for ETF Investing

Taxes are the most certain enemy of investment returns. No matter how good your returns are, after-tax real returns shrink significantly. The two most powerful tools that individual investors in Korea can legally use to reduce taxes are the ISA (Individual Savings Account) and the IRP (Individual Retirement Pension).

Integrating these two accounts into an ETF automated rebalancing workflow allows you to achieve both tax savings and rule-based investing simultaneously. As John Bogle emphasized in The Little Book of Common Sense Investing, "Reducing costs (including taxes) is the most reliable way to increase returns."

ISA System Key Summary (as of 2026)

What is ISA?

ISA is a tax-advantaged account that can hold various financial products such as deposits, funds, ETFs, and REITs within a single account. Income generated within the account receives tax-free benefits up to a certain amount.

ISA Type Comparison

ItemGeneral ISALow-Income ISABrokerage ISA
EligibilityResidents aged 19+Gross salary under 50M KRW or total income under 38M KRWResidents aged 19+
Annual contribution20M KRW/year (100M KRW total)20M KRW/year (100M KRW total)20M KRW/year (100M KRW total)
Tax-free limit2M KRW4M KRW2M KRW
Tax on excess9.9% flat rate9.9% flat rate9.9% flat rate
Mandatory holding3 years3 years3 years
Direct ETF tradingNot availableNot availableAvailable
Overseas ETFNot availableNot availableNot available (domestic listed only)

Key Point: To invest directly in ETFs, you must choose a Brokerage ISA. General/Low-Income types are trust-based and managed by the securities firm, making individual ETF trading impossible.

ISA Tax Savings Simulation

"""Compare after-tax returns between ISA and regular accounts."""


def compare_tax_effect(
    investment: int,
    annual_return: float,
    years: int,
    isa_type: str = "general",  # "general" or "disadvantaged"
) -> dict:
    """Compare after-tax returns between ISA and regular accounts.

    Args:
        investment: Annual investment amount (KRW)
        annual_return: Annual return rate (e.g., 0.08)
        years: Investment period (years)
        isa_type: ISA type ("general": General/Brokerage, "disadvantaged": Low-Income)
    """
    # ISA tax-free limit
    tax_free_limit = 2_000_000 if isa_type == "general" else 4_000_000
    isa_excess_tax_rate = 0.099  # 9.9% flat rate

    # Regular account dividend income tax
    normal_tax_rate = 0.154  # 15.4%

    # ISA account (tax-free + flat rate)
    isa_total_invested = 0
    isa_balance = 0
    for year in range(1, years + 1):
        isa_total_invested += investment
        isa_balance = (isa_balance + investment) * (1 + annual_return)

    isa_total_profit = isa_balance - isa_total_invested

    if isa_total_profit <= tax_free_limit:
        isa_tax = 0
    else:
        isa_tax = int((isa_total_profit - tax_free_limit) * isa_excess_tax_rate)

    isa_after_tax = isa_balance - isa_tax

    # Regular account (15.4% tax on annual profits)
    normal_balance = 0
    normal_total_tax = 0
    for year in range(1, years + 1):
        normal_balance += investment
        yearly_profit = normal_balance * annual_return
        yearly_tax = int(yearly_profit * normal_tax_rate)
        normal_total_tax += yearly_tax
        normal_balance = normal_balance + yearly_profit - yearly_tax

    tax_saving = normal_total_tax - isa_tax

    return {
        "Investment Period": f"{years} years",
        "Total Invested": f"₩{isa_total_invested:,}",
        "ISA Maturity Amount": f"₩{int(isa_after_tax):,}",
        "ISA Tax Paid": f"₩{isa_tax:,}",
        "Regular Account Maturity": f"₩{int(normal_balance):,}",
        "Regular Account Tax Paid": f"₩{normal_total_tax:,}",
        "Tax Savings": f"₩{tax_saving:,}",
        "Return Difference": f"₩{int(isa_after_tax - normal_balance):,}",
    }


# Scenario: 20M KRW annual investment, 8% annual return, 3 years
result = compare_tax_effect(
    investment=20_000_000,
    annual_return=0.08,
    years=3,
    isa_type="general",
)

print("=== ISA vs Regular Account Comparison ===")
for key, value in result.items():
    print(f"  {key}: {value}")

IRP System Key Summary (as of 2026)

What is IRP?

IRP is a retirement pension account that manages both severance pay and personal additional contributions. Combined with pension savings, you can receive tax deductions of up to 9 million KRW annually, and returns generated during the management period are tax-deferred.

IRP Key Figures

ItemDetails
Annual contribution18M KRW (including pension savings)
Tax deduction limit9M KRW (pension savings 6M + IRP 3M)
Tax deduction rateGross salary 55M or under: 16.5% / Over: 13.2%
Maximum tax deduction1.485M KRW (salary 55M or under) / 1.188M KRW (over)
Risky asset limit70% of accumulated funds (including equity ETFs)
Safe asset requirementAt least 30% (bond ETFs, deposits, etc.)
Withdrawal conditionPension payments after age 55 (10+ year installments)
Pension income tax3.3-5.5% (16.5% other income tax for lump sum)

Constraints When Investing in ETFs Through IRP

IRP is a pension account, so free trading is restricted.

Investable:

  • Domestically listed ETFs (equity, bond, hybrid)
  • TDF (Target Date Fund)
  • Bond/MMF ETFs (safe assets)

Not investable:

  • Leveraged/Inverse ETFs
  • ETFs with over 40% derivatives exposure
  • Overseas-listed ETFs (domestically listed overseas index ETFs are allowed)

70% Risky Asset Rule Example:

  • If IRP balance is 9M KRW: max 6.3M KRW in equity ETFs, min 2.7M KRW in bond ETFs

ISA + IRP Integrated Asset Allocation Strategy

Role Assignment by Account

To maximize tax efficiency, it's important to decide which assets go into which accounts.

Asset TypeOptimal AccountReason
High-dividend ETFsISADividend income tax-free up to 2-4M KRW
Growth stock ETFs (capital gains)ISADomestic ETF capital gains tax-free + ISA additional savings
Bond ETFsIRPMeets IRP 30% safe asset requirement + interest tax deferral
Overseas index tracking ETFsIRPDividend income tax-deferred (3.3-5.5% at pension receipt)
High-risk theme ETFsISAIRP has 70% risky asset limit constraint

Integrated Portfolio Example: Employee with 60M KRW Annual Salary

# Integrated asset allocation settings
investor:
  name: 'Kim Youngju'
  annual_salary: 60_000_000
  tax_deduction_rate: 0.132 # Over 55M KRW -> 13.2%

accounts:
  isa:
    type: 'Brokerage'
    annual_contribution: 20_000_000 # 20M KRW/year
    tax_free_limit: 2_000_000
    # Assets for ISA: dividend + growth + theme
    holdings:
      - ticker: 'TIGER US S&P500'
        weight: 0.35
        role: 'Core - US market'
      - ticker: 'KODEX 200'
        weight: 0.15
        role: 'Core - Korean market'
      - ticker: 'TIGER Semiconductor'
        weight: 0.15
        role: 'Satellite - Semiconductor'
      - ticker: 'TIGER US Dividend Dow Jones'
        weight: 0.15
        role: 'Satellite - Dividend'
      - ticker: 'KODEX Secondary Battery Industry'
        weight: 0.10
        role: 'Satellite - Secondary battery'
      - ticker: 'TIGER Gold & Silver Futures (H)'
        weight: 0.10
        role: 'Satellite - Gold'

  irp:
    annual_contribution: 9_000_000 # 9M KRW/year (tax deduction limit)
    tax_deduction: 1_188_000 # 9M x 13.2%
    risk_asset_limit: 0.70
    # Assets for IRP: overseas index + bonds (maximize tax deferral)
    holdings:
      risk_assets: # Within 70%
        - ticker: 'TIGER US S&P500'
          weight: 0.35
          role: 'Core - US market (dividend tax deferral)'
        - ticker: 'TIGER US NASDAQ100'
          weight: 0.20
          role: 'Core - Tech stock growth'
        - ticker: 'KODEX 200'
          weight: 0.15
          role: 'Core - Korean market'
      safe_assets: # At least 30%
        - ticker: 'KODEX Composite Bond (AA- and above) Active'
          weight: 0.15
          role: 'Safe asset - Domestic bonds'
        - ticker: 'TIGER US Treasury 10Y Futures'
          weight: 0.15
          role: 'Safe asset - US bonds'

  general:
    # Additional investments beyond the two accounts above
    holdings:
      - ticker: 'TIGER US S&P500'
        weight: 0.50
      - ticker: 'KODEX 200'
        weight: 0.30
      - ticker: 'TIGER US Treasury 10Y Futures'
        weight: 0.20

Automation Workflow Design

Annual Calendar and Automated Alerts

ISA and IRP have annual contribution limits and tax deduction caps, so missing deadlines means losing tax benefits.

"""ISA/IRP annual workflow automated alerts."""
from datetime import date


ANNUAL_TASKS = [
    {
        "month": 1,
        "tasks": [
            "Establish ISA annual contribution plan (1.67M KRW/month or lump sum)",
            "Establish IRP annual contribution plan (750K KRW/month or lump sum)",
            "Verify previous year tax deduction amount (year-end settlement reflection)",
            "Core ETF scheduled rebalancing",
        ],
    },
    {
        "month": 3,
        "tasks": [
            "Verify ISA Q1 contributions (target: 5M KRW cumulative)",
            "Verify IRP Q1 contributions (target: 2.25M KRW cumulative)",
            "ISA portfolio rebalancing (if drift exceeds 5%)",
        ],
    },
    {
        "month": 4,
        "tasks": [
            "Satellite ETF quarterly performance review",
            "Verify IRP risky asset ratio (within 70%)",
        ],
    },
    {
        "month": 6,
        "tasks": [
            "Verify ISA H1 cumulative contributions (target: 10M KRW)",
            "Verify IRP H1 cumulative contributions (target: 4.5M KRW)",
            "Full portfolio mid-year review",
        ],
    },
    {
        "month": 7,
        "tasks": [
            "Core ETF scheduled rebalancing (semi-annual)",
            "Satellite ETF quarterly performance review",
        ],
    },
    {
        "month": 10,
        "tasks": [
            "Check ISA annual contribution remaining amount",
            "Check IRP tax deduction limit remaining",
            "Plan contribution completion by year-end",
            "Satellite ETF quarterly review + next year strategy development",
        ],
    },
    {
        "month": 11,
        "tasks": [
            "Additional IRP contribution if below tax deduction limit",
            "Additional ISA contribution if below annual limit",
            "Utilize overseas ETF capital gains 2.5M KRW deduction (review year-end sales)",
        ],
    },
    {
        "month": 12,
        "tasks": [
            "Confirm ISA, IRP contributions completed by 12/31",
            "Summarize annual investment performance",
            "Develop next year asset allocation strategy",
            "If ISA 3-year maturity is approaching, decide on extension/termination",
        ],
    },
]


def get_current_tasks() -> list:
    """Return the task list for the current month."""
    current_month = date.today().month
    for item in ANNUAL_TASKS:
        if item["month"] == current_month:
            return item["tasks"]
    return ["No scheduled tasks this month. Perform drift monitoring only."]


def get_contribution_status(
    isa_ytd: int,
    irp_ytd: int,
    isa_annual_target: int = 20_000_000,
    irp_annual_target: int = 9_000_000,
) -> dict:
    """Check annual contribution progress."""
    today = date.today()
    year_progress = today.timetuple().tm_yday / 365

    return {
        "ISA": {
            "YTD Contributions": f"₩{isa_ytd:,}",
            "Annual Target": f"₩{isa_annual_target:,}",
            "Achievement Rate": f"{isa_ytd / isa_annual_target * 100:.1f}%",
            "Year Progress": f"{year_progress * 100:.1f}%",
            "Status": "On Track" if isa_ytd / isa_annual_target >= year_progress * 0.8 else "Behind Schedule",
        },
        "IRP": {
            "YTD Contributions": f"₩{irp_ytd:,}",
            "Annual Target": f"₩{irp_annual_target:,}",
            "Achievement Rate": f"{irp_ytd / irp_annual_target * 100:.1f}%",
            "Expected Tax Deduction": f"₩{int(min(irp_ytd, irp_annual_target) * 0.132):,}",
            "Status": "On Track" if irp_ytd / irp_annual_target >= year_progress * 0.8 else "Behind Schedule",
        },
    }


# Execution example
print("=== March Tasks ===")
for task in get_current_tasks():
    print(f"  - {task}")

print("\n=== Contribution Status ===")
status = get_contribution_status(isa_ytd=4_000_000, irp_ytd=1_500_000)
for account, info in status.items():
    print(f"\n{account}:")
    for key, value in info.items():
        print(f"  {key}: {value}")

ISA/IRP Rebalancing Execution Considerations

ISA Account Rebalancing

Selling and buying ETFs within an ISA account is free. Since these are intra-account transactions, no immediate taxation on capital gains occurs (settled in bulk at maturity).

"""Special logic for ISA account rebalancing."""


def isa_rebalance_check(
    holdings: dict,
    targets: dict,
    total_value: int,
    tax_free_used: int,
    tax_free_limit: int = 2_000_000,
) -> dict:
    """Check ISA account rebalancing feasibility and tax impact.

    Args:
        holdings: {code: {"value": current_value, "cost": purchase_cost}}
        targets: {code: target_weight}
        total_value: Total account balance
        tax_free_used: Tax-free limit already used (KRW)
        tax_free_limit: Tax-free limit (KRW)
    """
    trades = []
    total_realized_gain = 0

    for code, target_weight in targets.items():
        holding = holdings.get(code, {"value": 0, "cost": 0})
        current_weight = holding["value"] / total_value if total_value > 0 else 0
        diff_weight = target_weight - current_weight

        if abs(diff_weight) < 0.02:  # Ignore deviation under 2%
            continue

        trade_amount = int(total_value * abs(diff_weight))
        action = "BUY" if diff_weight > 0 else "SELL"

        # Calculate realized gains on sell
        realized_gain = 0
        if action == "SELL" and holding["value"] > holding["cost"]:
            gain_ratio = (holding["value"] - holding["cost"]) / holding["value"]
            realized_gain = int(trade_amount * gain_ratio)
            total_realized_gain += realized_gain

        trades.append({
            "code": code,
            "action": action,
            "amount": trade_amount,
            "realized_gain": realized_gain,
        })

    # Check tax-free limit
    remaining_tax_free = tax_free_limit - tax_free_used
    taxable_gain = max(0, total_realized_gain - remaining_tax_free)
    tax_on_excess = int(taxable_gain * 0.099)  # 9.9% flat rate

    return {
        "trades": trades,
        "total_realized_gain": total_realized_gain,
        "remaining_tax_free": remaining_tax_free,
        "taxable_gain": taxable_gain,
        "estimated_tax": tax_on_excess,
        "recommendation": (
            "Rebalancing executable (within tax-free limit)"
            if taxable_gain == 0
            else f"Tax-free limit exceeded: 9.9% tax on ₩{taxable_gain:,} (₩{tax_on_excess:,})"
        ),
    }


# Usage example
result = isa_rebalance_check(
    holdings={
        "360750": {"value": 8_000_000, "cost": 6_500_000},
        "069500": {"value": 3_000_000, "cost": 2_800_000},
        "091230": {"value": 4_000_000, "cost": 3_200_000},
    },
    targets={"360750": 0.40, "069500": 0.20, "091230": 0.20},
    total_value=15_000_000,
    tax_free_used=500_000,
)

print("=== ISA Rebalancing Check ===")
print(f"Expected realized gains: ₩{result['total_realized_gain']:,}")
print(f"Remaining tax-free limit: ₩{result['remaining_tax_free']:,}")
print(f"Verdict: {result['recommendation']}")

IRP Account Rebalancing

IRP has a 70% risky asset limit, so this ratio must be verified after rebalancing.

"""IRP account rebalancing with risky asset ratio verification."""

# Asset classification criteria
RISK_ASSET_CODES = {"360750", "133690", "069500", "091230", "305720"}
SAFE_ASSET_CODES = {"443160", "305080", "304660"}


def irp_risk_check(
    holdings: dict,
    max_risk_ratio: float = 0.70,
) -> dict:
    """Check the risky asset ratio of the IRP portfolio."""
    risk_value = sum(
        h["value_krw"] for code, h in holdings.items()
        if code in RISK_ASSET_CODES
    )
    safe_value = sum(
        h["value_krw"] for code, h in holdings.items()
        if code in SAFE_ASSET_CODES
    )
    total = risk_value + safe_value

    risk_ratio = risk_value / total if total > 0 else 0
    safe_ratio = safe_value / total if total > 0 else 0

    compliant = risk_ratio <= max_risk_ratio

    return {
        "total_value": f"₩{total:,}",
        "risk_assets": f"₩{risk_value:,} ({risk_ratio*100:.1f}%)",
        "safe_assets": f"₩{safe_value:,} ({safe_ratio*100:.1f}%)",
        "risk_limit": f"{max_risk_ratio*100:.0f}%",
        "compliant": compliant,
        "action": (
            "Risky asset ratio within limits"
            if compliant
            else f"Risky asset ratio exceeded: {risk_ratio*100:.1f}% > {max_risk_ratio*100:.0f}%. "
                 f"Need to convert ₩{int(risk_value - total * max_risk_ratio):,} to safe assets."
        ),
    }


# Usage example
irp_result = irp_risk_check({
    "360750": {"value_krw": 3_500_000},  # S&P500 (risky)
    "133690": {"value_krw": 1_800_000},  # NASDAQ100 (risky)
    "069500": {"value_krw": 1_200_000},  # KODEX200 (risky)
    "443160": {"value_krw": 1_500_000},  # Composite Bond (safe)
    "305080": {"value_krw": 1_000_000},  # US Treasury 10Y (safe)
})

print("=== IRP Risky Asset Ratio Check ===")
for key, value in irp_result.items():
    print(f"  {key}: {value}")

ISA vs IRP: Which to Invest in First -- Priority Decision

When you have limited investment funds, should you contribute to ISA or IRP first?

Investment Priority Decision Tree

1. Contribute 3-9M KRW annually to IRP (maximize tax deduction)
   - Tax deduction effect: Immediate 13.2-16.5% guaranteed return
   - Note: Cannot withdraw until age 55 (liquidity constraint)

2. Contribute up to 20M KRW annually to ISA
   - Can include funds needed within 3 years
   - Tax-free 2-4M KRW + 9.9% flat rate on excess

3. Regular account
   - Amounts exceeding ISA/IRP limits
   - Funds requiring liquidity

Optimal Allocation by Salary Level

Annual SalaryIRP ContributionISA ContributionRegular AccountAnnual Tax Savings
40M KRW9M KRW11M KRW-IRP deduction 1.485M + ISA tax-free
60M KRW9M KRW20M KRWSurplusIRP deduction 1.188M + ISA tax-free
80M KRW9M KRW20M KRWSurplusIRP deduction 1.188M + ISA tax-free
100M KRW9M KRW20M KRWSurplusIRP deduction 1.188M + ISA tax-free

Key: Regardless of salary, always fill the IRP 9M KRW. Tax deductions are a guaranteed return (13.2-16.5%) independent of investment performance. No return is more certain.

ISA Maturity Conversion Strategy

After the ISA 3-year maturity, there are two options.

Option 1: Extend Maturity

  • Keep the account and continue investing
  • Tax-free limit does not reset every 3 years (cumulative)
  • Can continue rebalancing under tax-free/flat-rate benefits

Option 2: Terminate and Transfer to IRP/Pension Savings

  • Transfer all or part of ISA maturity proceeds to IRP/Pension Savings
  • 10% of the transferred amount (up to 3M KRW) is eligible for additional tax deduction
  • Example: ISA 30M KRW maturity transferred to IRP = 3M KRW additional tax deduction = 396K-495K KRW tax savings
"""Calculate tax benefits when transferring ISA maturity to IRP."""


def isa_to_irp_benefit(
    isa_balance: int,
    max_additional_deduction: int = 3_000_000,
    tax_rate: float = 0.132,
) -> dict:
    """Calculate additional tax deduction when transferring ISA maturity to IRP."""
    eligible_amount = min(isa_balance, isa_balance)  # Assume full transfer
    deduction_base = min(int(eligible_amount * 0.1), max_additional_deduction)
    additional_tax_saving = int(deduction_base * tax_rate)

    return {
        "ISA Maturity Balance": f"₩{isa_balance:,}",
        "IRP Transfer Amount": f"₩{eligible_amount:,}",
        "Additional Deduction Base": f"₩{deduction_base:,}",
        "Additional Tax Savings": f"₩{additional_tax_saving:,}",
        "Note": "Transfer is separate from the existing IRP annual contribution limit (18M KRW)",
    }


result = isa_to_irp_benefit(isa_balance=30_000_000)
print("=== ISA to IRP Transfer Benefits ===")
for key, value in result.items():
    print(f"  {key}: {value}")

Full Workflow Automation Architecture

┌─────────────────────────────────────────────────────┐
GitHub Actions (Scheduler)Every Monday 09:00 KST -> Drift check               │
First business day of quarter -> Scheduled rebalance │
│  1st of each month -> Contribution status check       │
└────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
Python Rebalancing Engine1. Query ISA/IRP/general account balances via API2. Compare YAML config with current weights          │
3. Determine rebalancing needs per account           │
- ISA: Check tax-free limit                       │
- IRP: Check 70% risky asset limit                │
4. Generate trade orders (dry-run)└────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
Notifications (Slack / KakaoTalk)- When rebalancing needed: trade details + cost est  │
- When contributions behind: remaining + recommended │
- Tax deduction deadline approaching: Nov-Dec alert  │
└─────────────────────────────────────────────────────┘

Quiz

Q1. Which type of ISA should you choose to invest directly in ETFs? Answer: You must choose a Brokerage ISA. General/Low-Income types are trust-based and managed by the securities firm, making individual ETF trading impossible.

Q2. What is the maximum percentage of equity ETFs you can invest in through IRP?

Answer: Up to 70% of accumulated funds can be invested in risky assets (including equity ETFs). The remaining 30% or more must consist of safe assets such as bond ETFs and deposits.

Q3. How much tax deduction does an employee with a 60M KRW salary receive by contributing 9M KRW to IRP?

Answer: Since the gross salary exceeds 55M KRW, a 13.2% deduction rate applies. 9M KRW x 13.2% = 1.188M KRW tax deduction.

Q4. What additional benefit can you receive by transferring ISA maturity (3 years) to IRP?

Answer: You can receive additional tax deductions on 10% of the transferred amount (up to 3M KRW). For example, transferring 30M KRW gives a 3M KRW deduction base, resulting in 13.2% (396K KRW) or 16.5% (495K KRW) additional tax savings.

Q5. Why is it advantageous to put high-dividend ETFs in ISA rather than IRP? Answer: In ISA, dividend income is tax-free if within the tax-free limit (2-4M KRW). IRP only defers taxes and charges pension income tax of 3.3-5.5% upon receipt. If dividend amounts are within the tax-free limit, ISA has an absolute tax advantage.

Q6. Why is the IRP tax deduction described as a "guaranteed return"? Answer: Tax deductions are a confirmed 13.2-16.5% tax refund immediately upon contribution, regardless of investment performance. While stock investment returns are uncertain, tax deductions are definitively returned through year-end settlement, making them effectively the most certain return.

Q7. Why should investment priority be given to IRP over ISA? Answer: The IRP tax deduction (13.2-16.5%) is an immediately confirmed return, while ISA's tax-free benefit only has value when profits are generated. When investment funds are limited, it's rational to fill the IRP tax deduction limit (9M KRW) first and allocate the remainder to ISA.

References