Bobs_Tax_Project / tax_engine.py
rmd826's picture
Update tax_engine.py
8ae1e7c verified
# -*- coding: utf-8 -*-
"""
Consolidated tax calculation engine for the Social Security Tax Torpedo app.
Merges the best of Python_version.py (OOP structure, all 4 filing statuses)
and Tax_calc_breakdown.ipynb (evolved functions, auto-computed base_amount_cap).
All functions are importable and self-contained -- no top-level execution.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import List, Tuple, Dict, Optional
import numpy as np
import pandas as pd
Bracket = Tuple[float, float, float] # (lower_inclusive, upper_exclusive, rate)
# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class SSBThresholds:
"""
IRS provisional-income thresholds for Social Security taxation.
These differ by filing status.
Typical values:
- Single / HOH: t1=25,000 t2=34,000
- MFJ: t1=32,000 t2=44,000
- MFS (lived with spouse): effectively 0 / 0 (special rule)
"""
t1: float
t2: float
@dataclass(frozen=True)
class TaxConfig:
"""
Complete tax configuration for one filing status.
ssb_base_amount_cap is auto-computed as 0.5 * (t2 - t1).
"""
name: str
brackets: List[Bracket]
standard_deduction: float
ssb_thresholds: SSBThresholds
ssb_base_amount_cap: float = field(init=False)
def __post_init__(self):
object.__setattr__(
self,
"ssb_base_amount_cap",
0.5 * (self.ssb_thresholds.t2 - self.ssb_thresholds.t1),
)
# ---------------------------------------------------------------------------
# 2016 tax bracket data (all 4 filing statuses)
# Rates: 10%, 15%, 25%, 27.75%, 33%, 35%, 39.6%
# Note: 2016 also had a personal exemption of $4,050 per person,
# which is NOT modeled here. Only the standard deduction is used.
# ---------------------------------------------------------------------------
TAX_BRACKETS_SGL = [
(0, 9275, 0.10),
(9275, 37650, 0.15),
(37650, 91150, 0.25),
(91150, 190150, 0.2775),
(190150, 413350, 0.33),
(413350, 415050, 0.35),
(415050, float("inf"), 0.396),
]
TAX_BRACKETS_MFJ = [
(0, 18550, 0.10),
(18550, 75300, 0.15),
(75300, 151900, 0.25),
(151900, 231450, 0.2775),
(231450, 413350, 0.33),
(413350, 466950, 0.35),
(466950, float("inf"), 0.396),
]
TAX_BRACKETS_HOH = [
(0, 13250, 0.10),
(13250, 50400, 0.15),
(50400, 130150, 0.25),
(130150, 210800, 0.2775),
(210800, 413350, 0.33),
(413350, 441000, 0.35),
(441000, float("inf"), 0.396),
]
TAX_BRACKETS_MFS = [
(0, 9275, 0.10),
(9275, 37650, 0.15),
(37650, 75950, 0.25),
(75950, 115725, 0.2775),
(115725, 206675, 0.33),
(206675, 233475, 0.35),
(233475, float("inf"), 0.396),
]
# Standard deductions (2016) + personal exemptions folded in
# Personal exemption: $4,050 per person (1 for SGL/HOH/MFS, 2 for MFJ)
STD_DEDUCTION_SGL = 6300 + 4050 # $10,350
STD_DEDUCTION_MFJ = 12600 + 4050 + 4050 # $20,700
STD_DEDUCTION_HOH = 9300 + 4050 # $13,350
STD_DEDUCTION_MFS = 6300 + 4050 # $10,350
# Filing-status configurations
CFG_SGL = TaxConfig(
name="Single",
brackets=TAX_BRACKETS_SGL,
standard_deduction=STD_DEDUCTION_SGL,
ssb_thresholds=SSBThresholds(t1=25000, t2=34000),
)
CFG_MFJ = TaxConfig(
name="Married Filing Jointly",
brackets=TAX_BRACKETS_MFJ,
standard_deduction=STD_DEDUCTION_MFJ,
ssb_thresholds=SSBThresholds(t1=32000, t2=44000),
)
CFG_HOH = TaxConfig(
name="Head of Household",
brackets=TAX_BRACKETS_HOH,
standard_deduction=STD_DEDUCTION_HOH,
ssb_thresholds=SSBThresholds(t1=25000, t2=34000),
)
CFG_MFS = TaxConfig(
name="Married Filing Separately",
brackets=TAX_BRACKETS_MFS,
standard_deduction=STD_DEDUCTION_MFS,
ssb_thresholds=SSBThresholds(t1=25000, t2=34000),
)
CONFIGS: Dict[str, TaxConfig] = {
"SGL": CFG_SGL,
"MFJ": CFG_MFJ,
"HOH": CFG_HOH,
"MFS": CFG_MFS,
}
# ---------------------------------------------------------------------------
# Core tax calculation functions
# ---------------------------------------------------------------------------
def ssb_tax(other_income: float, ssb: float, cfg: TaxConfig) -> float:
"""
Taxable Social Security benefits (IRS worksheet-style).
PI = other_income + 0.5 * ssb
Tier 1 (PI <= t1): taxable = 0
Tier 2 (t1 < PI <= t2): taxable = min(0.5*(PI-t1), 0.5*ssb)
Tier 3 (PI > t2): taxable = min(0.85*ssb, base_amount + 0.85*(PI-t2))
"""
t1, t2 = cfg.ssb_thresholds.t1, cfg.ssb_thresholds.t2
pi = other_income + 0.5 * ssb
if pi <= t1:
return 0.0
if pi <= t2:
return min(0.5 * (pi - t1), 0.5 * ssb)
base_amount = min(0.5 * (t2 - t1), 0.5 * ssb)
candidate = base_amount + 0.85 * (pi - t2)
return min(0.85 * ssb, candidate)
def bracket_tax(taxable_income: float, cfg: TaxConfig) -> float:
"""Progressive tax from brackets."""
if taxable_income <= 0:
return 0.0
tax = 0.0
for lower, upper, rate in cfg.brackets:
if taxable_income <= lower:
break
amt = min(taxable_income, upper) - lower
if amt > 0:
tax += amt * rate
if taxable_income <= upper:
break
return float(tax)
def compute_baseline_tax(other_income: float, cfg: TaxConfig) -> float:
"""Tax ignoring Social Security (baseline reference)."""
taxable_income = max(0.0, other_income - cfg.standard_deduction)
return bracket_tax(taxable_income, cfg)
def tax_with_ssb(other_income: float, ssb: float, cfg: TaxConfig) -> float:
"""Total federal tax including SSB taxation."""
taxable_ssb = ssb_tax(other_income, ssb, cfg)
taxable_income = max(0.0, other_income + taxable_ssb - cfg.standard_deduction)
return bracket_tax(taxable_income, cfg)
def tax_with_ssb_detail(other_income: float, ssb: float, cfg: TaxConfig) -> Dict[str, float]:
"""
Full tax calculation returning all intermediate values.
"""
taxable_ssb_val = ssb_tax(other_income, ssb, cfg)
pi = other_income + 0.5 * ssb
agi = other_income + taxable_ssb_val
taxable_income = max(0.0, agi - cfg.standard_deduction)
tax = bracket_tax(taxable_income, cfg)
return {
"other_income": float(other_income),
"ssb": float(ssb),
"provisional_income": float(pi),
"taxable_ssb": float(taxable_ssb_val),
"pct_ssb_taxable": float(taxable_ssb_val / ssb * 100) if ssb > 0 else 0.0,
"agi": float(agi),
"standard_deduction": float(cfg.standard_deduction),
"taxable_income": float(taxable_income),
"tax": float(tax),
"effective_rate": float(tax / other_income * 100) if other_income > 0 else 0.0,
}
def bracket_marginal_rate(other_income: float, cfg: TaxConfig) -> float:
"""Marginal bracket rate (step function, ignoring SSB effects)."""
ti = float(max(0.0, other_income - cfg.standard_deduction))
for lower, upper, rate in cfg.brackets:
if ti >= lower and ti < upper:
return float(rate)
return float(cfg.brackets[-1][2])
def total_marginal_rate(
other_income: float, ssb: float, cfg: TaxConfig, delta: float = 100.0
) -> float:
"""
Total marginal tax rate via finite difference (includes SSB torpedo effect).
"""
t1 = tax_with_ssb(other_income, ssb, cfg)
t2 = tax_with_ssb(other_income + delta, ssb, cfg)
return (t2 - t1) / delta
def find_torpedo_bounds(
cfg: TaxConfig, ssb: float, x_max: float = 200000
) -> Tuple[Optional[float], Optional[float]]:
"""
Find the zero-point (where tax first > 0) and confluence point
(where taxable SSB reaches 85% of SSB).
The zero point is the highest Other Income where the marginal rate
is still 0% (i.e. tax(OI) == 0 AND tax(OI + delta) == 0), so that
it plots cleanly inside the No-Tax Zone.
Returns (zero_point, confluence_point). Either may be None.
"""
delta = 100.0 # must match the delta used for marginal-rate computation
# --- Coarse scan to bracket the transition points ---
xs = np.linspace(0, x_max, 5000)
ssb_vals = np.array([ssb_tax(x, ssb, cfg) for x in xs])
tax_vals = np.array([tax_with_ssb(x, ssb, cfg) for x in xs], dtype=float)
zero_point = None
confluence_point = None
# Find approximate zero point (first x where tax > 0)
approx_zp_lo = None
approx_zp_hi = None
prev_x = 0.0
for x, tv in zip(xs, tax_vals):
if tv > 0 and approx_zp_lo is None:
approx_zp_lo = prev_x
approx_zp_hi = x
prev_x = x
# Binary search for the precise boundary where tax transitions from 0 to >0
if approx_zp_lo is not None:
lo, hi = approx_zp_lo, approx_zp_hi
for _ in range(50): # ~15 decimal digits of precision
mid = (lo + hi) / 2
if tax_with_ssb(mid, ssb, cfg) > 0:
hi = mid
else:
lo = mid
# lo is the highest income where tax == 0
# Step back by delta so that marginal rate (tax(x+delta) - tax(x))/delta == 0
zero_point = max(0.0, lo - delta)
# Find confluence point
for x, sv in zip(xs, ssb_vals):
if zero_point is not None and sv >= 0.85 * ssb:
confluence_point = float(x)
break
return zero_point, confluence_point
def classify_zone(
other_income: float, ssb: float, cfg: TaxConfig,
zero_point: Optional[float] = None, confluence_point: Optional[float] = None,
) -> str:
"""
Classify the user's income into one of three zones:
- 'No-Tax Zone' (green) -- tax == 0
- 'High-Tax Zone' (red) -- in the torpedo
- 'Same-Old Zone' (blue) -- past the torpedo
"""
my_tax = tax_with_ssb(other_income, ssb, cfg)
if my_tax <= 0:
return "No-Tax Zone"
if confluence_point is not None and other_income >= confluence_point:
return "Same-Old Zone"
return "High-Tax Zone"
def bracket_breakdown(taxable_income: float, cfg: TaxConfig) -> pd.DataFrame:
"""DataFrame showing tax in each bracket."""
rows = []
ti = float(max(0.0, taxable_income))
for lower, upper, rate in cfg.brackets:
if ti <= lower:
amt = 0.0
else:
amt = max(0.0, min(ti, upper) - lower)
rows.append({
"lower": lower,
"upper": upper if np.isfinite(upper) else np.inf,
"rate": rate,
"taxed_amount": amt,
"tax_in_bracket": amt * rate,
})
if ti <= upper:
break
df = pd.DataFrame(rows)
df["tax_in_bracket"] = df["tax_in_bracket"].astype(float)
df["cum_tax"] = df["tax_in_bracket"].cumsum()
return df