CoolFace
Apppublic

Anushka-stack-queues/FinBench

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
task3_plan.py218 linesDownload Raw Back to tasks
1"""2Task 3 (Hard): Comprehensive Financial Plan3=============================================4Agent must generate a complete, coherent financial plan covering:5- Emergency fund, insurance, debt, investments, retirement,6  tax optimization, goal timelines, rebalancing strategy.7 8This is hard because all components must be internally consistent9and appropriate for the specific client.10"""11 12from __future__ import annotations13 14 15 16from typing import Dict17from ..models import ClientProfile, MarketConditions, Reward, _SCORE_EPSILON as STRICT_SCORE_EPSILON18 19 20 21 22def _retirement_savings_needed(client: ClientProfile, market: MarketConditions) -> float:23    """Estimate minimum monthly retirement savings using simplified FV formula."""24    years = max(1, 65 - client.age)25    monthly_rate = (market.equity_expected_return * 0.6 + market.bond_expected_return * 0.4) / 1226    # Target: 25x annual expenses at retirement (4% rule)27    target = client.monthly_expenses * 12 * 2528    existing_nw = max(0, client.net_worth)29    gap = max(0, target - existing_nw)30 31    if monthly_rate == 0 or years == 0:32        return gap / (years * 12) if years > 0 else 033 34    months = years * 1235    # PMT formula: gap = PMT * [(1+r)^n - 1] / r36    fv_factor = ((1 + monthly_rate) ** months - 1) / monthly_rate37    return gap / fv_factor if fv_factor > 0 else 038 39 40# FIX #7: updated signature to accept FinbenchAction directly instead of41#         FinancialPlanAction (which doesn't exist). Reads the same fields.42# FIX (market arg): FinbenchEnvironment passes self._market which is a MarketConditions43#         object — the grader correctly receives it as such, no change needed here.44def grade(45    action,46    client: ClientProfile,47    market: MarketConditions,48) -> Reward:49    """50    Score a comprehensive financial plan 0.0–1.0.51    Components:52      - Emergency fund adequacy           (0.12)53      - Insurance recommendations         (0.08)54      - Debt payoff strategy              (0.10)55      - Investment allocation suitability (0.20)56      - Retirement savings adequacy       (0.15)57      - Tax optimization                  (0.15)58      - Goal timeline realism             (0.10)59      - Internal plan consistency         (0.10)60    """61    components: Dict[str, float] = {}62    penalties: Dict[str, float] = {}63 64    monthly_income = client.annual_income / 1265 66    # ── 1. Emergency Fund (0.12) ──────────────────────────────────────────────67    if not client.has_emergency_fund:68        ideal_months = 6 if client.dependents > 0 else 369        if action.emergency_fund_months >= ideal_months:70            components["emergency_fund"] = 0.1271        elif action.emergency_fund_months >= ideal_months * 0.5:72            components["emergency_fund"] = 0.0673        else:74            components["emergency_fund"] = 0.075    else:76        components["emergency_fund"] = 0.12  # already has one77 78    # ── 2. Insurance (0.08) ──────────────────────────────────────────────────79    if not client.has_insurance:80        ins_text = " ".join(action.insurance_recommendations).lower()81        if client.dependents > 0 and "life" in ins_text:82            components["insurance"] = 0.0883        elif any(t in ins_text for t in ["term", "disability", "health", "life"]):84            components["insurance"] = 0.0585        else:86            components["insurance"] = 0.087    else:88        components["insurance"] = 0.0889 90    # ── 3. Debt Payoff Strategy (0.10) ────────────────────────────────────────91    if client.debt_to_income_ratio > 0.36:92        debt_text = action.debt_payoff_strategy.lower()93        has_strategy = any(94            t in debt_text95            for t in ["avalanche", "snowball", "high interest", "pay off", "consolidat"]96        )97        components["debt_strategy"] = 0.10 if has_strategy else 0.0298    else:99        components["debt_strategy"] = 0.10  # no debt problem = no penalty100 101    # ── 4. Investment Allocation Suitability (0.20) ───────────────────────────102    allocs = action.investment_allocations103    total_alloc = sum(allocs.values())104 105    # Sum check106    if abs(total_alloc - 100.0) > 5:107        penalties["alloc_sum_error"] = 0.10108        components["investment_allocation"] = 0.0109    else:110        equity = allocs.get("equities", allocs.get("stocks", 0))111        bonds = allocs.get("bonds", 0)112        cash = allocs.get("cash", 0)113 114        # Expected equity range by risk115        eq_ranges = {116            "conservative": (10, 30),117            "moderate": (35, 60),118            "aggressive": (55, 85),119        }120        lo, hi = eq_ranges[client.risk_tolerance]121 122        alloc_score = 0.0123        if lo <= equity <= hi:124            alloc_score += 0.10125 126        # Age adjustment: high equity bad near retirement127        if client.age >= 60 and equity > 50:128            penalties["too_aggressive_near_retirement"] = 0.05129        else:130            alloc_score += 0.05131 132        # Cash not excessively high133        if cash <= 20:134            alloc_score += 0.05135 136        components["investment_allocation"] = min(0.20, alloc_score)137 138    # ── 5. Retirement Savings (0.15) ──────────────────────────────────────────139    needed = _retirement_savings_needed(client, market)140    agent_savings = action.retirement_monthly_savings141    income_pct = agent_savings / monthly_income if monthly_income > 0 else 0142 143    # Must save at least 10% of income for partial credit144    if income_pct >= 0.15 and agent_savings >= needed * 0.80:145        components["retirement_savings"] = 0.15146    elif income_pct >= 0.10:147        components["retirement_savings"] = 0.08148    elif income_pct >= 0.05:149        components["retirement_savings"] = 0.04150    else:151        components["retirement_savings"] = 0.0152 153    # ── 6. Tax Optimization (0.15) ────────────────────────────────────────────154    tax_text = " ".join(action.tax_optimization_strategies).lower()155    tax_score = 0.0156    tax_keywords = {157        "401k": 0.04, "ira": 0.04, "roth": 0.03,158        "tax-loss": 0.02, "hsa": 0.02,159        "municipal": 0.02, "capital gains": 0.02,160    }161    for kw, pts in tax_keywords.items():162        if kw in tax_text:163            tax_score += pts164 165    # High earners must mention tax-advantaged accounts166    if client.tax_bracket >= 0.32 and not any(167        t in tax_text for t in ["401k", "ira", "roth", "hsa"]168    ):169        penalties["missed_tax_advantaged"] = 0.05170 171    components["tax_optimization"] = min(0.15, tax_score)172 173    # ── 7. Goal Timeline Realism (0.10) ──────────────────────────────────────174    if client.goals and action.goal_timelines:175        # Check timelines are within investment horizon176        max_timeline = client.investment_horizon_years177        realistic = sum(178            1 for yrs in action.goal_timelines.values()179            if 1 <= yrs <= max_timeline + 5180        )181        timeline_score = (realistic / len(action.goal_timelines)) * 0.10182        components["goal_timelines"] = round(timeline_score, 4)183    else:184        components["goal_timelines"] = 0.05  # partial for missing185 186    # ── 8. Internal Consistency (0.10) ────────────────────────────────────────187    consistency_score = 0.10188    # Inconsistency: saving a lot but no debt payoff strategy with high debt189    if client.debt_to_income_ratio > 0.5 and agent_savings > monthly_income * 0.3:190        penalties["inconsistent_savings_vs_debt"] = 0.03191        consistency_score -= 0.03192    # Inconsistency: aggressive allocation but conservative risk tolerance193    if client.risk_tolerance == "conservative":194        equity = allocs.get("equities", allocs.get("stocks", 0))195        if equity > 40:196            penalties["allocation_risk_mismatch"] = 0.05197            consistency_score -= 0.05198    # Bonus: reasoning present199    if len(action.reasoning.strip()) >= 50:200        consistency_score = min(0.10, consistency_score + 0.02)201 202    components["consistency"] = max(0.0, round(consistency_score, 4))203 204    # ── Final Score ───────────────────────────────────────────────────────────205    total = sum(components.values()) - sum(penalties.values())206    total = max(STRICT_SCORE_EPSILON, min(1.0 - STRICT_SCORE_EPSILON, total))207 208    return Reward(209        total=round(total, 3),210        components=components,211        penalties=penalties,212        explanation=(213            f"Needed monthly retirement savings: ${needed:,.0f} | "214            f"Agent savings: ${agent_savings:,.0f} | "215            f"Tax bracket: {client.tax_bracket*100:.0f}% | "216            f"Goals: {client.goals}"217        ),218    )