미국주식 · 해외주식 양도세 · 250만원 공제 · 실측 재현 페이지

미국주식 양도세,
12월에 이렇게 줄이세요

해외주식 양도세는 1년 동안 판 이익에서 250만원을 빼고 남은 만큼에 22%(지방세 포함)가 붙습니다. 이 250만원은 그해에 안 쓰면 사라집니다. 그래서 매년 12월, 그해 실현 이익이 250만원이 될 만큼만 팔고 그날 다시 사면 얼마나 달라지는지 실제 가격으로 계산했습니다. S&P500 ETF(SPY) 종가 × 원/달러 환율 · 2016년 1월 4일 1,000만원 · 2026년 10월 2일 전부 매도 · 사고팔 때마다 편도 0.25%.

672만원끝까지 그냥 둔 계좌가 판 날 낸 양도세
242만원12월마다 팔고 다시 산 계좌의 양도세 (리셋한 해는 0원)
343만원수수료까지 빼고 손에 더 남은 돈

12월에 할 일 그대로

순서할 일
112월 15일이 지난 첫 거래일(결제일이 그해 안에 들어오는 날까지), 증권사 앱에서 올해 해외주식 실현 손익을 확인
2그 실현 이익이 250만원이 될 만큼만 가진 미국주식을 판다
3판 그날 같은 종목을 판 돈만큼 다시 산다 — 주식 수는 거의 그대로, 세금 계산에 쓰는 산 값만 올라간다
4판 돈의 10분의 1도 이익이 아니면(1,000만원어치 팔아 이익이 100만원도 안 되면) 그해는 건너뛴다 — 왕복 수수료가 아낀 세금보다 커질 수 있다

사례 — 2016년 1월 1,000만원, 해마다 한 일

2016-01-04 에 두 계좌 모두 1,000만원어치 삼
2016-12-15  리셋: 1,119.7만원어치 팔고 다시 삼 · 실현 이익 116.9만원 · 그해 양도세 0원
2017-12-15  건너뜀: 다 팔아도 이익 79.9만원 / 판 돈 1,199.8만원 (10분의 1 미만)
2018-12-17  건너뜀: 다 팔아도 이익 76.1만원 / 판 돈 1,196.0만원 (10분의 1 미만)
2019-12-16  리셋: 900.6만원어치 팔고 다시 삼 · 실현 이익 250.0만원 · 그해 양도세 0원
2020-12-15  리셋: 975.2만원어치 팔고 다시 삼 · 실현 이익 250.0만원 · 그해 양도세 0원
2021-12-15  리셋: 769.1만원어치 팔고 다시 삼 · 실현 이익 250.0만원 · 그해 양도세 0원
2022-12-15  리셋: 1,198.3만원어치 팔고 다시 삼 · 실현 이익 250.0만원 · 그해 양도세 0원
2023-12-15  리셋: 1,739.6만원어치 팔고 다시 삼 · 실현 이익 250.0만원 · 그해 양도세 0원
2024-12-16  리셋: 603.6만원어치 팔고 다시 삼 · 실현 이익 250.0만원 · 그해 양도세 0원
2025-12-15  리셋: 511.8만원어치 팔고 다시 삼 · 실현 이익 250.0만원 · 그해 양도세 0원
계좌판 돈양도세손에 쥔 돈
끝까지 그냥 둠4,303만원672만원3,631만원
12월마다 팔고 다시 삼4,216만원242만원3,975만원

판 돈이 조금 적은 것은 해마다 사고판 수수료 때문입니다. 그래도 양도세가 430만원 줄어 손에 쥔 돈은 343만원 더 많았습니다.

다른 때 시작했어도

1993년부터 S&P500 10년 계좌를 시작 달만 바꿔 286번 계산했습니다. 12월마다 판 쪽이 더 남은 것 231번 · 똑같은 것 51번 · 진 것 4번입니다. 진 경우는 넷 다 이익이 작아 원래 양도세가 0원이던 경우이고, 잃은 돈은 수수료(가장 크게 5.3만원)였습니다. 나머지 조건은 아래 출력 전문에 있습니다.

한계·주의 — 먼저 읽어 주세요

받아가기

돌리는 법: zip 을 풀고 python run_ustax.py

그대로 돌린 출력

■ 사례 · S&P500(SPY) 2016년 1월 1,000만원 → 2026-10-02 전부 매도 · 편도 0.25% · 10분의 1 문턱
  2016-01-04 에 두 계좌 모두 1,000만원어치 삼
  2016-12-15  리셋: 1,119.7만원어치 팔고 다시 삼 · 실현 이익 116.9만원 · 그해 양도세 0원
  2017-12-15  건너뜀: 다 팔아도 이익 79.9만원 / 판 돈 1,199.8만원 (10분의 1 미만)
  2018-12-17  건너뜀: 다 팔아도 이익 76.1만원 / 판 돈 1,196.0만원 (10분의 1 미만)
  2019-12-16  리셋: 900.6만원어치 팔고 다시 삼 · 실현 이익 250.0만원 · 그해 양도세 0원
  2020-12-15  리셋: 975.2만원어치 팔고 다시 삼 · 실현 이익 250.0만원 · 그해 양도세 0원
  2021-12-15  리셋: 769.1만원어치 팔고 다시 삼 · 실현 이익 250.0만원 · 그해 양도세 0원
  2022-12-15  리셋: 1,198.3만원어치 팔고 다시 삼 · 실현 이익 250.0만원 · 그해 양도세 0원
  2023-12-15  리셋: 1,739.6만원어치 팔고 다시 삼 · 실현 이익 250.0만원 · 그해 양도세 0원
  2024-12-16  리셋: 603.6만원어치 팔고 다시 삼 · 실현 이익 250.0만원 · 그해 양도세 0원
  2025-12-15  리셋: 511.8만원어치 팔고 다시 삼 · 실현 이익 250.0만원 · 그해 양도세 0원
  그냥 둔 계좌   판 돈 4,303만원 · 양도세 672만원 · 손에 쥔 돈 3,631만원
  12월마다 리셋  판 돈 4,216만원 · 양도세 242만원 · 손에 쥔 돈 3,975만원
  차이: 양도세 430만원 덜 냄 · 손에 쥔 돈 343만원 더

■ 같은 사례, 조건을 바꾸면
  편도 0.1%          손에 쥔 돈 차이 385만원 (양도세 674만원 → 256만원)
  10분의 1 문턱 없이 해마다 손에 쥔 돈 차이 299만원 (양도세 672만원 → 244만원)

■ 나스닥100(QQQ) 같은 사례
  그냥 둔 계좌   판 돈 7,693만원 · 양도세 1,418만원 · 손에 쥔 돈 6,276만원
  12월마다 리셋  판 돈 7,623만원 · 양도세 962만원 · 손에 쥔 돈 6,661만원
  차이: 양도세 456만원 덜 냄 · 손에 쥔 돈 385만원 더

■ 시작 달만 바꿔 다시 (편도 0.25% · 10분의 1 문턱) — 리셋이 더 남음 / 똑같음 / 짐 · 가장 크게 진 금액
  S&P500 1,000만원 10년         286번 중 더 남음 231 · 똑같음 51 · 짐 4 · 가장 크게 진 금액 -5.3만원
  S&P500 3억원 20년             166번 중 더 남음 166 · 똑같음 0 · 짐 0 · 가장 크게 진 금액 248.8만원
  S&P500 3억원 20년 · 문턱 없이     166번 중 더 남음 157 · 똑같음 0 · 짐 9 · 가장 크게 진 금액 -1,443.2만원
  S&P500 매달 100만원 10년        286번 중 더 남음 236 · 똑같음 41 · 짐 9 · 가장 크게 진 금액 -22.0만원
  나스닥100 1,000만원 10년         212번 중 더 남음 179 · 똑같음 19 · 짐 14 · 가장 크게 진 금액 -8.6만원

끝.

재현 코드 전문

# -*- coding: utf-8 -*-
"""미국주식 양도세 — 매년 12월 이익 250만원어치만 팔고 그날 다시 사기(리셋) 대 끝까지 그냥 두기.
AlgoLab · 2026-11-01 · 표준 라이브러리만 씁니다.  돌리는 법: python run_ustax.py

가정(영상·설명란과 같음)
 · 가격 = 미국 ETF 종가(배당 제외) × 그날 원/달러 환율(FRED DEXKOUS, 없는 날은 직전 값). 자료는 2026-10-02 까지 얼림.
 · 수수료 편도 0.25%(살 때 값에 더하고, 팔 때 값에서 뺌). 소수점 수량.
 · 양도세 = 22% × max(0, 그해 실현 이익 − 250만원). 취득가는 먼저 산 것부터(선입선출).
 · 리셋: 해마다 12월 15일 이후 첫 거래일, 실현 이익이 250만원이 될 만큼만 먼저 산 주식부터 팔고 그날 판 돈으로 다시 산다.
         판 돈의 10분의 1도 이익이 아니면 그해는 건너뛴다. 마지막으로 다 파는 해에는 리셋하지 않는다(공제를 마지막 매도가 쓴다).
 · 마지막 날 두 계좌 모두 전부 팔고 양도세를 낸 뒤 손에 쥔 돈을 견준다.
"""
import bisect, csv, datetime as dt, os, sys

try:
    sys.stdout.reconfigure(encoding="utf-8")
except Exception:
    pass
HERE = os.path.dirname(os.path.abspath(__file__))
TAX, DED = 0.22, 250.0          # 세율 · 공제(만원)


def load(sym):
    def rd(f, col):
        out = {}
        with open(os.path.join(HERE, "data", f), encoding="utf-8") as fh:
            r = csv.reader(fh); next(r)
            for row in r:
                try:
                    out[dt.date.fromisoformat(row[0])] = float(row[col])
                except (ValueError, IndexError):
                    pass
        return out
    px, fx = rd(sym + ".csv", 1), rd("DEXKOUS.csv", 1)
    fd = sorted(fx)
    D, P = [], []
    for d in sorted(px):
        i = bisect.bisect_right(fd, d) - 1
        if i >= 0:
            D.append(d); P.append(px[d] * fx[fd[i]] / 1e4)   # 1주 값(만원)
    return D, P


class Acct:
    def __init__(self, C, M):
        self.C, self.M, self.lots, self.cash = C, M, [], 0.0

    def buy(self, money, p):
        money += self.cash
        unit = p * (1 + self.C)
        sh = money / unit
        if sh > 0:
            self.lots.append([sh, unit])
        self.cash = money - sh * unit

    def gain(self, p):
        return sum(sh * (p * (1 - self.C) - b) for sh, b in self.lots)

    def reset(self, p):
        """반환 (판 돈, 실현 이익, 했나)"""
        if self.gain(p) <= 0:
            return 0.0, 0.0, False
        realized = proceeds = sold = 0.0
        keep, lots = [], [l[:] for l in self.lots]
        for i, (sh, b) in enumerate(lots):
            if realized >= DED:
                keep.extend(lots[i:]); break
            g = p * (1 - self.C) - b
            if g > 0 and realized + sh * g > DED:
                q = (DED - realized) / g
                realized += q * g; proceeds += q * p * (1 - self.C); sold += q
                keep.append([sh - q, b]); keep.extend(lots[i + 1:]); break
            realized += sh * g; proceeds += sh * p * (1 - self.C); sold += sh
        if sold <= 0 or realized <= 0:
            return 0.0, 0.0, False
        if realized / (sold * p) < self.M:
            return sold * p, realized, False
        self.lots = keep
        self.buy(proceeds, p)
        return sold * p, realized, True

    def settle(self, p):
        val = sum(sh for sh, _ in self.lots) * p * (1 - self.C) + self.cash
        tax = TAX * max(0.0, self.gain(p) - DED)
        return val, tax


def reset_days(D):
    out = {}
    for y in sorted({d.year for d in D}):
        k = bisect.bisect_left(D, dt.date(y, 12, 15))
        if k < len(D) and D[k].year == y:
            out[k] = y
    return out


def case(D, P, start, amt=1000.0, C=0.0025, M=0.10, show=False):
    a, z = bisect.bisect_left(D, start), len(D) - 1
    H, R = Acct(C, M), Acct(C, M)
    H.buy(amt, P[a]); R.buy(amt, P[a])
    if show:
        print(f"  {D[a]} 에 두 계좌 모두 {amt:,.0f}만원어치 삼")
    for y in range(D[a].year, D[z].year):
        k = bisect.bisect_left(D, dt.date(y, 12, 15))
        sold, real, did = R.reset(P[k])
        if show:
            if did:
                print(f"  {D[k]}  리셋: {sold:,.1f}만원어치 팔고 다시 삼 · 실현 이익 {real:,.1f}만원 · 그해 양도세 0원")
            elif sold > 0:
                print(f"  {D[k]}  건너뜀: 다 팔아도 이익 {real:,.1f}만원 / 판 돈 {sold:,.1f}만원 (10분의 1 미만)")
            else:
                print(f"  {D[k]}  건너뜀: 이익 없음")
    vh, th = H.settle(P[z]); vr, tr = R.settle(P[z])
    return dict(끝=D[z], 판값_보유=vh, 세금_보유=th, 세후_보유=vh - th, 판값_리셋=vr, 세금_리셋=tr, 세후_리셋=vr - tr)


def windows(D, P, mode, amt, years, C=0.0025, M=0.10):
    first = {}
    for i, d in enumerate(D):
        first.setdefault((d.year, d.month), i)
    ml = sorted(first)
    rs = reset_days(D)
    H = years * 12
    res = []
    for i0 in range(len(ml) - H):
        (y0, m0), (y1, m1) = ml[i0], ml[i0 + H]
        if (y1 * 12 + m1) - (y0 * 12 + m0) != H:
            continue
        a, z = first[ml[i0]], first[ml[i0 + H]]
        zy = D[z].year
        contrib = {a: amt} if mode == "일시금" else {first[ml[i0 + j]]: amt for j in range(H)}
        ev = sorted(set(contrib) | {k for k in rs if a <= k < z and rs[k] != zy})
        A, B = Acct(C, M), Acct(C, M)
        for k in ev:
            if k in contrib:
                A.buy(contrib[k], P[k]); B.buy(contrib[k], P[k])
            if k in rs and rs[k] != zy:
                B.reset(P[k])
        vh, th = A.settle(P[z]); vr, tr = B.settle(P[z])
        res.append((vr - tr) - (vh - th))
    win = sum(1 for d in res if d > 0); lose = sum(1 for d in res if d < 0)
    return len(res), win, len(res) - win - lose, lose, min(res)


def man(x):
    x = round(x)
    return f"{x // 10000}억 {x % 10000:,}만원" if x >= 10000 else f"{x:,}만원"


def show_case(name, D, P, **kw):
    r = case(D, P, dt.date(2016, 1, 1), **kw)
    print(f"  그냥 둔 계좌   판 돈 {man(r['판값_보유'])} · 양도세 {man(r['세금_보유'])} · 손에 쥔 돈 {man(r['세후_보유'])}")
    print(f"  12월마다 리셋  판 돈 {man(r['판값_리셋'])} · 양도세 {man(r['세금_리셋'])} · 손에 쥔 돈 {man(r['세후_리셋'])}")
    print(f"  차이: 양도세 {man(r['세금_보유'] - r['세금_리셋'])} 덜 냄 · 손에 쥔 돈 {man(r['세후_리셋'] - r['세후_보유'])} 더")
    return r


if __name__ == "__main__":
    SPY, QQQ = load("SPY"), load("QQQ")
    print("■ 사례 · S&P500(SPY) 2016년 1월 1,000만원 → 2026-10-02 전부 매도 · 편도 0.25% · 10분의 1 문턱")
    case(*SPY, dt.date(2016, 1, 1), show=True)
    show_case("SPY", *SPY)
    print()
    print("■ 같은 사례, 조건을 바꾸면")
    for lab, kw in (("편도 0.1%", dict(C=0.001)), ("10분의 1 문턱 없이 해마다", dict(M=0.0))):
        r = case(*SPY, dt.date(2016, 1, 1), **kw)
        print(f"  {lab:<16} 손에 쥔 돈 차이 {man(r['세후_리셋'] - r['세후_보유'])} (양도세 {man(r['세금_보유'])} → {man(r['세금_리셋'])})")
    print()
    print("■ 나스닥100(QQQ) 같은 사례")
    show_case("QQQ", *QQQ)
    print()
    print("■ 시작 달만 바꿔 다시 (편도 0.25% · 10분의 1 문턱) — 리셋이 더 남음 / 똑같음 / 짐 · 가장 크게 진 금액")
    for lab, S, mode, amt, yrs, kw in (
            ("S&P500 1,000만원 10년", SPY, "일시금", 1000, 10, {}),
            ("S&P500 3억원 20년", SPY, "일시금", 30000, 20, {}),
            ("S&P500 3억원 20년 · 문턱 없이", SPY, "일시금", 30000, 20, dict(M=0.0)),
            ("S&P500 매달 100만원 10년", SPY, "적립", 100, 10, {}),
            ("나스닥100 1,000만원 10년", QQQ, "일시금", 1000, 10, {})):
        n, w, t, l, worst = windows(*S, mode, amt, yrs, **kw)
        print(f"  {lab:<26} {n}번 중 더 남음 {w} · 똑같음 {t} · 짐 {l} · 가장 크게 진 금액 {worst:,.1f}만원")
    print()
    print("끝.")

알고랩 Strategy Lab — 전략을 검증하고, 검증한 것만 자동매매로 옮깁니다. 해마다 12월 날짜와 금액을 챙기는 일은 사람보다 프로그램이 잘 지킵니다. 알고랩 자동매매 살펴보기 · 전략 검증 전체 보기

과거 데이터를 돌려 본 결과이며 미래 수익을 보장하지 않습니다. 투자 판단과 책임은 본인에게 있습니다.