mirror of https://github.com/ghostfolio/ghostfolio
2 changed files with 284 additions and 0 deletions
@ -0,0 +1,67 @@ |
|||||
|
import sys |
||||
|
import os |
||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'tools')) |
||||
|
from family_planner import plan_family_finances |
||||
|
|
||||
|
|
||||
|
def test_family_plan_austin_one_child(): |
||||
|
result = plan_family_finances( |
||||
|
current_city="Austin", |
||||
|
annual_income=120000, |
||||
|
portfolio_value=94000, |
||||
|
num_planned_children=1, |
||||
|
) |
||||
|
assert result is not None |
||||
|
assert result["monthly_cost_breakdown"]["childcare_monthly"] > 0 |
||||
|
assert result["monthly_cost_breakdown"]["total_new_monthly_costs"] > 1000 |
||||
|
assert "honest_assessment" in result |
||||
|
assert "alternatives" in result |
||||
|
assert "is_feasible" in result["income_impact"] |
||||
|
|
||||
|
|
||||
|
def test_family_plan_two_children(): |
||||
|
result = plan_family_finances( |
||||
|
current_city="Austin", |
||||
|
annual_income=120000, |
||||
|
partner_income=80000, |
||||
|
num_planned_children=2, |
||||
|
) |
||||
|
one_child = plan_family_finances("Austin", 120000, num_planned_children=1) |
||||
|
assert ( |
||||
|
result["monthly_cost_breakdown"]["childcare_monthly"] |
||||
|
> one_child["monthly_cost_breakdown"]["childcare_monthly"] |
||||
|
) |
||||
|
|
||||
|
|
||||
|
def test_family_plan_partner_reduces_hours(): |
||||
|
result = plan_family_finances( |
||||
|
current_city="Austin", |
||||
|
annual_income=150000, |
||||
|
partner_income=80000, |
||||
|
partner_work_reduction=0.5, |
||||
|
num_planned_children=1, |
||||
|
) |
||||
|
assert result["monthly_cost_breakdown"]["income_reduction_monthly"] > 0 |
||||
|
|
||||
|
|
||||
|
def test_family_plan_international_cheaper(): |
||||
|
result_berlin = plan_family_finances("Berlin", 120000, num_planned_children=1) |
||||
|
result_austin = plan_family_finances("Austin", 120000, num_planned_children=1) |
||||
|
assert ( |
||||
|
result_berlin["monthly_cost_breakdown"]["childcare_monthly"] |
||||
|
< result_austin["monthly_cost_breakdown"]["childcare_monthly"] |
||||
|
) |
||||
|
|
||||
|
|
||||
|
def test_family_plan_shows_alternatives(): |
||||
|
result = plan_family_finances("Austin", 120000, num_planned_children=1) |
||||
|
assert "alternatives" in result |
||||
|
# Austin should suggest Williamson County as cheaper |
||||
|
assert len(result["alternatives"]) > 0 |
||||
|
|
||||
|
|
||||
|
def test_family_plan_what_helps(): |
||||
|
result = plan_family_finances("Austin", 80000, num_planned_children=1) |
||||
|
assert "what_helps" in result |
||||
|
assert len(result["what_helps"]) > 0 |
||||
|
assert "disclaimer" in result |
||||
@ -0,0 +1,217 @@ |
|||||
|
""" |
||||
|
Family Financial Planner |
||||
|
Models the financial impact of having children for any city worldwide. |
||||
|
Source: US Dept of Labor + Care.com 2024 averages |
||||
|
""" |
||||
|
|
||||
|
CHILDCARE_ANNUAL = { |
||||
|
"san francisco": 31000, "san-francisco": 31000, |
||||
|
"seattle": 26000, |
||||
|
"new york": 29000, "new-york": 29000, "nyc": 29000, |
||||
|
"boston": 28000, |
||||
|
"washington dc": 27000, "dc": 27000, |
||||
|
"los angeles": 24000, "la": 24000, |
||||
|
"chicago": 22000, |
||||
|
"portland": 22000, |
||||
|
"denver": 20000, |
||||
|
"minneapolis": 20000, |
||||
|
"austin": 18000, |
||||
|
"travis county": 18000, |
||||
|
"atlanta": 18000, |
||||
|
"miami": 19000, |
||||
|
"nashville": 17000, |
||||
|
"raleigh": 17000, |
||||
|
"williamson county": 16000, |
||||
|
"round rock": 16000, |
||||
|
"cedar park": 16000, |
||||
|
"dallas": 16000, |
||||
|
"houston": 16000, |
||||
|
"hays county": 15000, |
||||
|
"san marcos": 15000, |
||||
|
"kyle": 15000, |
||||
|
"phoenix": 15000, |
||||
|
"bastrop county": 14000, |
||||
|
"caldwell county": 13000, |
||||
|
"lockhart": 13000, |
||||
|
"london": 22000, |
||||
|
"toronto": 18000, |
||||
|
"sydney": 16000, |
||||
|
"berlin": 4000, |
||||
|
"paris": 5000, |
||||
|
"tokyo": 8000, |
||||
|
"amsterdam": 6000, |
||||
|
"stockholm": 5000, |
||||
|
"default": 18000, |
||||
|
} |
||||
|
|
||||
|
|
||||
|
def _estimate_monthly_take_home(annual_salary: float, city: str = "") -> float: |
||||
|
if annual_salary <= 44725: |
||||
|
federal = 0.12 |
||||
|
elif annual_salary <= 95375: |
||||
|
federal = 0.22 |
||||
|
elif annual_salary <= 200000: |
||||
|
federal = 0.24 |
||||
|
else: |
||||
|
federal = 0.32 |
||||
|
fica = 0.0765 |
||||
|
no_tax = [ |
||||
|
"tx", "wa", "fl", "nv", "tn", "wy", "sd", "ak", |
||||
|
"texas", "washington", "florida", "austin", "seattle", |
||||
|
"dallas", "houston", "nashville", "miami", |
||||
|
] |
||||
|
state = 0.0 if any(s in city.lower() for s in no_tax) else 0.05 |
||||
|
return (annual_salary * (1 - federal - fica - state)) / 12 |
||||
|
|
||||
|
|
||||
|
def plan_family_finances( |
||||
|
current_city: str, |
||||
|
annual_income: float, |
||||
|
partner_income: float = 0, |
||||
|
portfolio_value: float = 0, |
||||
|
num_planned_children: int = 1, |
||||
|
timeline_years: int = 5, |
||||
|
partner_work_reduction: float = 0.0, |
||||
|
) -> dict: |
||||
|
"""Model the financial impact of having children.""" |
||||
|
|
||||
|
city_lower = current_city.lower() |
||||
|
|
||||
|
# Step 1: Get childcare cost |
||||
|
annual_childcare = CHILDCARE_ANNUAL.get("default", 18000) |
||||
|
for key in CHILDCARE_ANNUAL: |
||||
|
if key in city_lower or city_lower in key: |
||||
|
annual_childcare = CHILDCARE_ANNUAL[key] |
||||
|
break |
||||
|
monthly_childcare = (annual_childcare / 12) * num_planned_children |
||||
|
|
||||
|
# Step 2: Get median rent for city |
||||
|
RENT_LOOKUP = { |
||||
|
"austin": 2100, "travis": 2100, |
||||
|
"williamson": 1995, "round rock": 1995, |
||||
|
"hays": 1937, "san marcos": 1937, |
||||
|
"bastrop": 1860, "caldwell": 1750, |
||||
|
"seattle": 2400, "san francisco": 3200, |
||||
|
"new york": 3800, "boston": 3100, |
||||
|
"denver": 1900, "chicago": 1850, |
||||
|
"miami": 2800, "nashville": 1800, |
||||
|
"los angeles": 2900, "dallas": 1700, |
||||
|
"london": 2800, "tokyo": 1800, |
||||
|
"berlin": 1600, "paris": 2200, |
||||
|
} |
||||
|
rent = 2000 |
||||
|
for key, val in RENT_LOOKUP.items(): |
||||
|
if key in city_lower: |
||||
|
rent = val |
||||
|
break |
||||
|
|
||||
|
# Step 3: Calculate income |
||||
|
total_income = annual_income + partner_income |
||||
|
reduced_partner = partner_income * (1 - partner_work_reduction) |
||||
|
effective_income = annual_income + reduced_partner |
||||
|
income_reduction = partner_income - reduced_partner |
||||
|
|
||||
|
# Step 4: Monthly financials |
||||
|
take_home_before = _estimate_monthly_take_home(total_income, current_city) |
||||
|
take_home_after = _estimate_monthly_take_home(effective_income, current_city) |
||||
|
|
||||
|
food_clothing = 800 * num_planned_children |
||||
|
healthcare = 300 * num_planned_children |
||||
|
family_rent = rent * 1.3 |
||||
|
|
||||
|
total_new_costs = monthly_childcare + food_clothing + healthcare |
||||
|
|
||||
|
surplus_before = take_home_before - (rent * 1.8) |
||||
|
surplus_after = take_home_after - (family_rent * 1.6) - total_new_costs |
||||
|
|
||||
|
# Step 5: Income needed to maintain current surplus |
||||
|
current_surplus = take_home_before - (rent * 1.8) |
||||
|
income_needed = effective_income |
||||
|
test_income = effective_income |
||||
|
while True: |
||||
|
test_take_home = _estimate_monthly_take_home(test_income, current_city) |
||||
|
test_surplus = test_take_home - (family_rent * 1.6) - total_new_costs |
||||
|
if test_surplus >= current_surplus or test_income > 500000: |
||||
|
income_needed = test_income |
||||
|
break |
||||
|
test_income += 5000 |
||||
|
|
||||
|
# Step 6: Alternatives (cheaper nearby option for Austin users) |
||||
|
alternatives = [] |
||||
|
if "austin" in city_lower or "travis" in city_lower: |
||||
|
wilco_childcare = CHILDCARE_ANNUAL.get("williamson county", 16000) |
||||
|
wilco_rent = 1995 |
||||
|
savings = ( |
||||
|
(annual_childcare - wilco_childcare) / 12 |
||||
|
+ (rent - wilco_rent) |
||||
|
) |
||||
|
if savings > 0: |
||||
|
alternatives.append({ |
||||
|
"option": "Move to Williamson County", |
||||
|
"monthly_savings": round(savings), |
||||
|
"note": f"Saves ~${savings:,.0f}/mo vs Austin City", |
||||
|
}) |
||||
|
|
||||
|
# Step 7: International comparison |
||||
|
intl = { |
||||
|
"austin": annual_childcare, |
||||
|
"berlin": CHILDCARE_ANNUAL["berlin"], |
||||
|
"paris": CHILDCARE_ANNUAL["paris"], |
||||
|
"stockholm": CHILDCARE_ANNUAL.get("stockholm", 5000), |
||||
|
"note": "Western Europe has heavily subsidized childcare", |
||||
|
} |
||||
|
|
||||
|
# Honest assessment |
||||
|
if surplus_after > 0: |
||||
|
honest_assessment = ( |
||||
|
f"Having {num_planned_children} child(ren) in {current_city} adds " |
||||
|
f"~${round(total_new_costs):,}/mo in costs. " |
||||
|
f"You would have ${round(surplus_after):,}/mo surplus after family expenses — " |
||||
|
f"this is financially feasible." |
||||
|
) |
||||
|
else: |
||||
|
shortfall = abs(round(surplus_after)) |
||||
|
honest_assessment = ( |
||||
|
f"Having {num_planned_children} child(ren) in {current_city} adds " |
||||
|
f"~${round(total_new_costs):,}/mo in costs. " |
||||
|
f"Your current income leaves a ${shortfall:,}/mo shortfall after family expenses. " |
||||
|
f"You'd need ~${income_needed:,}/yr combined income to maintain your current lifestyle." |
||||
|
) |
||||
|
|
||||
|
return { |
||||
|
"family_plan": { |
||||
|
"city": current_city, |
||||
|
"num_children": num_planned_children, |
||||
|
"timeline_years": timeline_years, |
||||
|
}, |
||||
|
"monthly_cost_breakdown": { |
||||
|
"childcare_monthly": round(monthly_childcare), |
||||
|
"food_clothing_misc": round(food_clothing), |
||||
|
"healthcare_increase": round(healthcare), |
||||
|
"housing_increase_for_space": round(family_rent - rent), |
||||
|
"total_new_monthly_costs": round(total_new_costs), |
||||
|
"income_reduction_monthly": round(income_reduction / 12), |
||||
|
}, |
||||
|
"income_impact": { |
||||
|
"take_home_before_kids": round(take_home_before), |
||||
|
"take_home_after_kids": round(take_home_after), |
||||
|
"monthly_surplus_before": round(surplus_before), |
||||
|
"monthly_surplus_after": round(surplus_after), |
||||
|
"is_feasible": surplus_after > 0, |
||||
|
"income_needed_to_maintain_surplus": round(income_needed), |
||||
|
}, |
||||
|
"honest_assessment": honest_assessment, |
||||
|
"alternatives": alternatives, |
||||
|
"what_helps": [ |
||||
|
f"Family member childcare eliminates ${round(monthly_childcare):,}/mo in costs", |
||||
|
"Nanny share splits childcare cost ~50%", |
||||
|
"Employer childcare benefits (check your benefits package)", |
||||
|
"Dependent Care FSA saves taxes on up to $5,000/yr", |
||||
|
], |
||||
|
"international_comparison": intl, |
||||
|
"disclaimer": ( |
||||
|
"Cost estimates vary significantly by provider. " |
||||
|
"Actual costs depend on childcare type and location." |
||||
|
), |
||||
|
"data_source": "US Dept of Labor + Care.com 2024 averages", |
||||
|
} |
||||
Loading…
Reference in new issue