포스트

손익통산으로 세금 줄이기 — Tax-Loss Harvesting 시뮬레이션

주의: 이 기능은 모의투자 전용 교육 시뮬레이션입니다. 실제 세무 신고나 절세 전략으로 사용할 수 없습니다.

손익통산이란

한국 주식 투자에서 양도소득세는 이익이 난 종목에만 부과되지 않는다. 같은 해에 발생한 이익과 손실을 합산(통산)한 후 순이익에 과세한다.

1
2
3
4
5
6
7
8
9
10
11
12
[손익통산 전]
  NVDA 매도 이익: +1,200,000원 → 양도세 264,000원 (22%)
  삼성전자 손실: -500,000원 (아직 보유 중, 미실현)

  납부 세금: 264,000원

[손익통산 후 — 삼성전자 매도]
  NVDA 이익: +1,200,000원
  삼성전자 손실: -500,000원 (실현)
  과세표준: 700,000원 → 양도세 154,000원

  절세액: 264,000 - 154,000 = 110,000원

평가손실 중인 종목을 연말 전에 매도해 손실을 실현시키면 세금을 줄일 수 있다. 이를 Tax-Loss Harvesting이라고 한다.


구현: TaxOptimizerService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Service
class TaxOptimizerService(
    private val jdbcTemplate: JdbcTemplate,
    private val priceService: PriceService,
) {
    // 절세 후보 종목 추출
    fun findHarvestingCandidates(userId: Long): HarvestingResult {
        // 1. 올해 실현 손익 합산
        val realizedPnl = getRealizedPnl(userId)

        // 2. 현재 보유 중인 평가손실 종목 목록
        val holdings = getHoldingsWithPnl(userId)
        val losers   = holdings.filter { it.unrealizedPnl < BigDecimal.ZERO }

        if (losers.isEmpty() || realizedPnl <= BigDecimal.ZERO) {
            return HarvestingResult(
                realizedPnl = realizedPnl,
                candidates  = emptyList(),
                summary     = "손익통산 대상이 없습니다.",
            )
        }

        // 3. 각 손실 종목의 절세 효과 계산
        val candidates = losers.map { holding ->
            val harvestableAmount = holding.unrealizedPnl.abs()
            val offsettable = minOf(harvestableAmount, realizedPnl)
            val taxSaving   = offsettable.multiply(BigDecimal("0.22"))

            HarvestingCandidate(
                stockId        = holding.stockId,
                symbol         = holding.symbol,
                currentPrice   = holding.currentPrice,
                avgCost        = holding.avgCost,
                quantity       = holding.quantity,
                unrealizedPnl  = holding.unrealizedPnl,
                estimatedTaxSaving = taxSaving,
            )
        }.sortedByDescending { it.estimatedTaxSaving }

        return HarvestingResult(
            realizedPnl = realizedPnl,
            candidates  = candidates,
            summary     = buildSummary(realizedPnl, candidates),
        )
    }
}

실현 손익 계산

1
2
3
4
5
6
7
8
9
private fun getRealizedPnl(userId: Long): BigDecimal {
    return jdbcTemplate.queryForObject("""
        SELECT COALESCE(SUM(realized_pnl), 0)
        FROM fills f
        JOIN orders o ON o.id = f.order_id
        WHERE o.user_id = ?
          AND EXTRACT(YEAR FROM f.filled_at) = EXTRACT(YEAR FROM NOW())
    """, BigDecimal::class.java, userId) ?: BigDecimal.ZERO
}

매도 시뮬레이션

후보 종목을 실제 매도하면 세금이 어떻게 달라지는지 시뮬레이션한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
fun simulate(userId: Long, stockIdsToSell: List<Long>): TaxSimulationResult {
    val currentRealizedPnl = getRealizedPnl(userId)
    val holdings = getHoldingsWithPnl(userId)

    // 선택한 종목들의 손실 합산
    val additionalLoss = stockIdsToSell.sumOf { stockId ->
        holdings.find { it.stockId == stockId }?.unrealizedPnl ?: BigDecimal.ZERO
    }

    val beforePnl    = currentRealizedPnl
    val afterPnl     = currentRealizedPnl + additionalLoss  // 손실 반영
    val beforeTax    = calculateTax(beforePnl)
    val afterTax     = calculateTax(afterPnl)
    val taxSaving    = beforeTax - afterTax

    return TaxSimulationResult(
        beforeRealizedPnl = beforePnl,
        afterRealizedPnl  = afterPnl,
        beforeTax         = beforeTax,
        afterTax          = afterTax,
        estimatedSaving   = taxSaving,
        caveat            = "모의투자 시뮬레이션 결과입니다. 실제 세무 신고에 사용하지 마세요.",
    )
}

private fun calculateTax(pnl: BigDecimal): BigDecimal {
    if (pnl <= BigDecimal.ZERO) return BigDecimal.ZERO
    return pnl.multiply(BigDecimal("0.22"))  // 양도소득세 22% (단순화)
}

응답 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
  "realizedPnl": 1200000,
  "candidates": [
    {
      "symbol": "005930",
      "currentPrice": 68000,
      "avgCost": 75000,
      "quantity": 10,
      "unrealizedPnl": -70000,
      "estimatedTaxSaving": 15400
    },
    {
      "symbol": "NVDA",
      "currentPrice": 120.5,
      "avgCost": 135.0,
      "quantity": 5,
      "unrealizedPnl": -72500,
      "estimatedTaxSaving": 15950
    }
  ],
  "summary": "두 종목 모두 매도 시 예상 절세액: 31,350원 (연간 실현이익 1,200,000원 기준)",
  "caveat": "모의투자 시뮬레이션입니다. 실제 세무 신고에 사용하지 마세요."
}

한계와 고지

실제 세금 계산은 훨씬 복잡하다.

  1. 보유 기간: 단기(1년 미만)와 장기 양도세율이 다를 수 있다.
  2. 종목 유형: 상장 주식과 비상장 주식, 해외 주식 세율이 다르다.
  3. 기본공제: 연간 250만 원 기본공제 적용 여부.
  4. 지방소득세: 양도소득세의 10% 추가.

monticker는 이를 단순화해 교육 목적으로만 제공하고, 모든 응답에 면책 고지를 포함한다.


정리

  • Tax-Loss Harvesting은 평가손실 종목을 연내 매도해 손익을 통산하고 세금을 줄이는 전략이다.
  • monticker는 후보 종목을 추출하고 절세액을 시뮬레이션하는 교육 기능을 제공한다.
  • 실제 세무 신고에는 사용할 수 없으며, 모든 응답에 면책 고지를 포함한다.

다음 시리즈(Bonus)에서는 311개 테스트를 작성한 경험을 기반으로 MockK로 JdbcTemplate 목킹하기를 다룬다.

  1. 1 가격이 아니라 이벤트를 팔자 — monticker 설계 철학
  2. 2 모듈식 모놀리스를 선택한 이유 — MSA의 유혹을 거부하기
  3. 3 TimescaleDB를 시계열 DB로 고른 이유 — Hypertable과 연속 집계
  4. 4 Go goroutine으로 202개 종목 동시 수집하기 — Market Gateway 설계
  5. 5 Kafka로 시세 파이프라인 분리하기 — 토픽 설계와 at-least-once
  6. 6 Netty로 수만 연결에 시세 브로드캐스트하기 — NioEventLoopGroup 리액터 패턴
  7. 7 EMA 기반 이상 탐지 — 가격 급등과 거래량 서지 실시간 감지
  8. 8 TreeMap으로 CLOB 호가창 구현하기 — 가격/시간 우선 매칭과 슬리피지
  9. 9 주문 전 동기 리스크 게이트 설계 — VaR, 집중도, 일일손실 5가지 규칙
  10. 10 잔고를 저장하지 말고 재구성하라 — 이벤트 소싱 원장 설계
  11. 11 감정 태그 × 수익률 — 투자 습관을 데이터로 기록하기
  12. 12 룰 엔진: RSI·MACD 조건식을 JSON DSL로 — Quant Lab 설계
  13. 13 백테스트 엔진: look-ahead 없는 시뮬레이션 — Sharpe·MDD·PF 계산
  14. 14 전략 지문(SHA-256)으로 룰셋 보호하기 — 서버 사이드 실행과 역공학 방어
  15. 15 Markowitz 최적화를 솔버 없이 구현하기 — 프로젝션 경사하강법
  16. 16 Kelly Criterion: 수학적 파산 방지 베팅 비율 — Half Kelly와 백테스트 연동
  17. 17 ZigZag + 패턴 템플릿 매칭으로 차트 패턴 감지 — 헤드앤숄더·이중바닥
  18. 18 ADX로 시장 국면 분류하기 — BULL·BEAR·SIDEWAYS·HIGH_VOL
  19. 19 손익통산으로 세금 줄이기 — Tax-Loss Harvesting 시뮬레이션
  20. 20 MockK로 JdbcTemplate 목킹하기 — 311개 테스트 작성 경험
  21. 21 OpenTelemetry + Jaeger로 분산 추적 — 시세 파이프라인 지연 측정
  22. 22 Circuit Breaker로 외부 API 장애 격리 — Resilience4j + KIS·Yahoo 폴백 체인
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

댓글

아직 댓글이 없습니다