Phase 1: - Real-time signal alerts (Discord/Telegram webhook) - Trading journal with entry/exit tracking - Position sizing calculator (Fixed/Kelly/ATR) Phase 2: - Pension asset allocation (DC/IRP 70% risk limit) - Drawdown monitoring with SVG gauge - Benchmark dashboard (portfolio vs KOSPI vs deposit) Phase 3: - Tax benefit simulation (Korean pension tax rules) - Correlation matrix heatmap - Parameter optimizer with grid search + overfit detection
246 lines
8.3 KiB
Python
246 lines
8.3 KiB
Python
"""
|
|
Unit tests for tax simulation service.
|
|
"""
|
|
import pytest
|
|
|
|
from app.services.tax_simulation import (
|
|
calculate_tax_deduction,
|
|
calculate_pension_tax,
|
|
simulate_accumulation,
|
|
)
|
|
|
|
|
|
class TestCalculateTaxDeduction:
|
|
def test_low_income_deduction_rate(self):
|
|
"""총급여 5,500만원 이하 → 공제율 16.5%"""
|
|
result = calculate_tax_deduction(
|
|
annual_income=40_000_000,
|
|
contribution=9_000_000,
|
|
account_type="irp",
|
|
)
|
|
assert result["deduction_rate"] == 16.5
|
|
assert result["deductible_contribution"] == 9_000_000
|
|
assert result["tax_deduction"] == 9_000_000 * 0.165
|
|
|
|
def test_high_income_deduction_rate(self):
|
|
"""총급여 5,500만원 초과 → 공제율 13.2%"""
|
|
result = calculate_tax_deduction(
|
|
annual_income=80_000_000,
|
|
contribution=9_000_000,
|
|
account_type="irp",
|
|
)
|
|
assert result["deduction_rate"] == 13.2
|
|
assert result["tax_deduction"] == 9_000_000 * 0.132
|
|
|
|
def test_boundary_income_55m(self):
|
|
"""정확히 5,500만원은 16.5% 적용"""
|
|
result = calculate_tax_deduction(
|
|
annual_income=55_000_000,
|
|
contribution=5_000_000,
|
|
account_type="irp",
|
|
)
|
|
assert result["deduction_rate"] == 16.5
|
|
|
|
def test_contribution_exceeds_limit(self):
|
|
"""납입액이 900만원 한도 초과 시 900만원까지만 공제"""
|
|
result = calculate_tax_deduction(
|
|
annual_income=40_000_000,
|
|
contribution=12_000_000,
|
|
account_type="irp",
|
|
)
|
|
assert result["deductible_contribution"] == 9_000_000
|
|
assert result["tax_deduction"] == 9_000_000 * 0.165
|
|
|
|
def test_contribution_below_limit(self):
|
|
"""납입액이 한도 미만이면 실제 납입액 기준 공제"""
|
|
result = calculate_tax_deduction(
|
|
annual_income=40_000_000,
|
|
contribution=3_000_000,
|
|
account_type="irp",
|
|
)
|
|
assert result["deductible_contribution"] == 3_000_000
|
|
assert result["tax_deduction"] == 3_000_000 * 0.165
|
|
|
|
def test_dc_account_type(self):
|
|
"""DC 계좌도 동일 한도 적용 (DC+IRP 합산 900만원)"""
|
|
result = calculate_tax_deduction(
|
|
annual_income=60_000_000,
|
|
contribution=9_000_000,
|
|
account_type="dc",
|
|
)
|
|
assert result["deduction_rate"] == 13.2
|
|
assert result["deductible_contribution"] == 9_000_000
|
|
|
|
def test_zero_contribution(self):
|
|
result = calculate_tax_deduction(
|
|
annual_income=50_000_000,
|
|
contribution=0,
|
|
account_type="irp",
|
|
)
|
|
assert result["tax_deduction"] == 0
|
|
|
|
def test_result_structure(self):
|
|
result = calculate_tax_deduction(
|
|
annual_income=50_000_000,
|
|
contribution=5_000_000,
|
|
account_type="irp",
|
|
)
|
|
assert "annual_income" in result
|
|
assert "contribution" in result
|
|
assert "account_type" in result
|
|
assert "deduction_rate" in result
|
|
assert "deductible_contribution" in result
|
|
assert "tax_deduction" in result
|
|
assert "irp_limit" in result
|
|
|
|
|
|
class TestCalculatePensionTax:
|
|
def test_pension_tax_under_70(self):
|
|
"""70세 미만 연금소득세 5.5%"""
|
|
result = calculate_pension_tax(
|
|
withdrawal_amount=10_000_000,
|
|
withdrawal_type="pension",
|
|
age=65,
|
|
)
|
|
assert result["pension_tax_rate"] == 5.5
|
|
assert result["pension_tax"] == 10_000_000 * 0.055
|
|
|
|
def test_pension_tax_70_to_79(self):
|
|
"""70~79세 연금소득세 4.4%"""
|
|
result = calculate_pension_tax(
|
|
withdrawal_amount=10_000_000,
|
|
withdrawal_type="pension",
|
|
age=75,
|
|
)
|
|
assert result["pension_tax_rate"] == 4.4
|
|
assert result["pension_tax"] == pytest.approx(10_000_000 * 0.044)
|
|
|
|
def test_pension_tax_80_and_over(self):
|
|
"""80세 이상 연금소득세 3.3%"""
|
|
result = calculate_pension_tax(
|
|
withdrawal_amount=10_000_000,
|
|
withdrawal_type="pension",
|
|
age=85,
|
|
)
|
|
assert result["pension_tax_rate"] == 3.3
|
|
assert result["pension_tax"] == pytest.approx(10_000_000 * 0.033)
|
|
|
|
def test_pension_tax_boundary_70(self):
|
|
"""정확히 70세는 4.4% 적용"""
|
|
result = calculate_pension_tax(
|
|
withdrawal_amount=10_000_000,
|
|
withdrawal_type="pension",
|
|
age=70,
|
|
)
|
|
assert result["pension_tax_rate"] == 4.4
|
|
|
|
def test_pension_tax_boundary_80(self):
|
|
"""정확히 80세는 3.3% 적용"""
|
|
result = calculate_pension_tax(
|
|
withdrawal_amount=10_000_000,
|
|
withdrawal_type="pension",
|
|
age=80,
|
|
)
|
|
assert result["pension_tax_rate"] == 3.3
|
|
|
|
def test_lump_sum_tax(self):
|
|
"""일시금 수령 시 기타소득세 16.5%"""
|
|
result = calculate_pension_tax(
|
|
withdrawal_amount=10_000_000,
|
|
withdrawal_type="lump_sum",
|
|
age=65,
|
|
)
|
|
assert result["lump_sum_tax_rate"] == 16.5
|
|
assert result["lump_sum_tax"] == 10_000_000 * 0.165
|
|
|
|
def test_comparison_shows_savings(self):
|
|
"""연금 수령이 일시금보다 세금이 적음"""
|
|
result = calculate_pension_tax(
|
|
withdrawal_amount=10_000_000,
|
|
withdrawal_type="pension",
|
|
age=65,
|
|
)
|
|
assert result["tax_saving"] > 0
|
|
assert result["tax_saving"] == result["lump_sum_tax"] - result["pension_tax"]
|
|
|
|
def test_result_structure(self):
|
|
result = calculate_pension_tax(
|
|
withdrawal_amount=10_000_000,
|
|
withdrawal_type="pension",
|
|
age=65,
|
|
)
|
|
assert "withdrawal_amount" in result
|
|
assert "pension_tax_rate" in result
|
|
assert "pension_tax" in result
|
|
assert "lump_sum_tax_rate" in result
|
|
assert "lump_sum_tax" in result
|
|
assert "tax_saving" in result
|
|
|
|
|
|
class TestSimulateAccumulation:
|
|
def test_basic_accumulation(self):
|
|
"""기본 적립 시뮬레이션"""
|
|
result = simulate_accumulation(
|
|
monthly_contribution=500_000,
|
|
years=20,
|
|
annual_return=7.0,
|
|
tax_deduction_rate=16.5,
|
|
)
|
|
assert len(result["yearly_data"]) == 20
|
|
assert result["total_contribution"] == 500_000 * 12 * 20
|
|
assert result["final_value"] > result["total_contribution"]
|
|
|
|
def test_yearly_data_structure(self):
|
|
result = simulate_accumulation(
|
|
monthly_contribution=300_000,
|
|
years=5,
|
|
annual_return=5.0,
|
|
tax_deduction_rate=13.2,
|
|
)
|
|
first_year = result["yearly_data"][0]
|
|
assert "year" in first_year
|
|
assert "contribution" in first_year
|
|
assert "cumulative_contribution" in first_year
|
|
assert "investment_value" in first_year
|
|
assert "tax_deduction" in first_year
|
|
assert "cumulative_tax_deduction" in first_year
|
|
|
|
def test_tax_deduction_accumulates(self):
|
|
result = simulate_accumulation(
|
|
monthly_contribution=500_000,
|
|
years=3,
|
|
annual_return=5.0,
|
|
tax_deduction_rate=16.5,
|
|
)
|
|
yearly = result["yearly_data"]
|
|
annual_contribution = 500_000 * 12
|
|
deductible = min(annual_contribution, 9_000_000)
|
|
expected_deduction = deductible * 0.165
|
|
assert yearly[0]["tax_deduction"] == expected_deduction
|
|
assert yearly[2]["cumulative_tax_deduction"] == pytest.approx(
|
|
expected_deduction * 3, rel=1e-6
|
|
)
|
|
|
|
def test_zero_return(self):
|
|
result = simulate_accumulation(
|
|
monthly_contribution=100_000,
|
|
years=10,
|
|
annual_return=0.0,
|
|
tax_deduction_rate=16.5,
|
|
)
|
|
assert result["final_value"] == result["total_contribution"]
|
|
assert result["total_return"] == 0
|
|
|
|
def test_result_summary(self):
|
|
result = simulate_accumulation(
|
|
monthly_contribution=500_000,
|
|
years=20,
|
|
annual_return=7.0,
|
|
tax_deduction_rate=16.5,
|
|
)
|
|
assert "total_contribution" in result
|
|
assert "final_value" in result
|
|
assert "total_return" in result
|
|
assert "total_tax_deduction" in result
|
|
assert "yearly_data" in result
|