CoolFace
Apppublic

Anushka-stack-queues/FinBench

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
task1_allocation.py131 linesDownload Raw Back to tasks
1"""2Task 1 (Easy): Portfolio Allocation3====================================4Given a client profile, allocate the portfolio correctly across asset classes.5Grader checks suitability based on risk tolerance, age, investment horizon.6"""7 8from __future__ import annotations9 10 11 12from typing import Dict, Tuple13from ..models import ClientProfile, Reward, _SCORE_EPSILON as STRICT_SCORE_EPSILON14 15 16# ── Ideal allocation ranges per risk profile ─────────────────────────────────17# Each: {asset_class: (min%, max%, ideal%)}18ALLOCATION_PROFILES: Dict[str, Dict[str, Tuple[float, float, float]]] = {19    "conservative": {20        "bonds":            (40, 70, 55),21        "equities":         (10, 30, 20),22        "cash":             (10, 30, 15),23        "real_estate":      (0,  15, 5),24        "commodities":      (0,  10, 5),25    },26    "moderate": {27        "bonds":            (20, 45, 35),28        "equities":         (35, 60, 50),29        "cash":             (5,  15, 8),30        "real_estate":      (0,  15, 5),31        "commodities":      (0,  10, 2),32    },33    "aggressive": {34        "bonds":            (0,  20, 10),35        "equities":         (55, 85, 70),36        "cash":             (0,  10, 5),37        "real_estate":      (0,  20, 10),38        "commodities":      (0,  15, 5),39    },40}41 42AGE_BOND_RULE_BONUS = 5.0   # extra bond % for every 10 years over 5043 44 45# FIX #7 (task grader signature): graders previously accepted task-specific action46# sub-classes (AllocationAction etc.) that don't exist. Updated to accept FinbenchAction47# directly — only the relevant fields are read, so this is fully backward compatible.48def grade(action, client: ClientProfile) -> Reward:49    """50    Score a portfolio allocation 0.0–1.0.51    Partial credit for:52      - Allocations within acceptable ranges      (0.50)53      - Sum-to-100 correctness                    (0.15)54      - Age-appropriate bond weighting            (0.15)55      - Emergency fund prerequisite awareness     (0.10)56      - Reasoning quality (non-empty)             (0.10)57    """58    components: Dict[str, float] = {}59    penalties: Dict[str, float] = {}60 61    allocs = action.allocations62    profile = ALLOCATION_PROFILES[client.risk_tolerance]63 64    # 1. Allocations within acceptable ranges (0.50)65    known_assets = set(profile.keys())66    in_range_count = 067    total_checked = len(known_assets)68 69    for asset, (lo, hi, _ideal) in profile.items():70        pct = allocs.get(asset, 0.0)71        if lo <= pct <= hi:72            in_range_count += 173 74    range_score = (in_range_count / total_checked) * 0.5075    components["in_range"] = round(range_score, 4)76 77    # 2. Allocations sum to 100 (±2 tolerance) (0.15)78    total_alloc = sum(allocs.values())79    if abs(total_alloc - 100.0) <= 2.0:80        components["sum_to_100"] = 0.1581    elif abs(total_alloc - 100.0) <= 10.0:82        components["sum_to_100"] = 0.0783    else:84        components["sum_to_100"] = 0.085        penalties["allocation_sum_error"] = abs(total_alloc - 100.0)86 87    # 3. Age-appropriate bond allocation (0.15)88    bond_alloc = allocs.get("bonds", 0.0)89    # FIX #8: removed dead variable `_ideal_profile_bond` (was computed but never used).90    #         Read the ideal directly from the profile tuple.91    age_adjusted_ideal = profile["bonds"][2]  # ideal% is index 292    if client.age > 50:93        age_adjusted_ideal += ((client.age - 50) / 10) * AGE_BOND_RULE_BONUS94 95    age_adjusted_ideal = min(age_adjusted_ideal, 80)  # cap96    bond_error = abs(bond_alloc - age_adjusted_ideal)97    if bond_error <= 5:98        components["age_appropriate_bonds"] = 0.1599    elif bond_error <= 15:100        components["age_appropriate_bonds"] = 0.08101    else:102        components["age_appropriate_bonds"] = 0.0103 104    # 4. Emergency fund prerequisite awareness (0.10)105    cash_alloc = allocs.get("cash", 0.0)106    if not client.has_emergency_fund:107        if cash_alloc >= 15:108            components["emergency_fund_awareness"] = 0.10109        elif cash_alloc >= 8:110            components["emergency_fund_awareness"] = 0.05111        else:112            components["emergency_fund_awareness"] = 0.0113    else:114        components["emergency_fund_awareness"] = 0.10  # full marks if fund exists115 116    # 5. Reasoning quality (0.10)117    components["reasoning"] = 0.10 if len(action.reasoning.strip()) >= 30 else 0.04118 119    total = sum(components.values()) - sum(penalties.values())120    total = max(STRICT_SCORE_EPSILON, min(1.0 - STRICT_SCORE_EPSILON, total))121 122    return Reward(123        total=round(total, 3),124        components=components,125        penalties=penalties,126        explanation=(127            f"Risk profile: {client.risk_tolerance} | Age: {client.age} | "128            f"Bond ideal: {age_adjusted_ideal:.1f}% | Agent bond: {bond_alloc}% | "129            f"Alloc sum: {total_alloc:.1f}%"130        ),131    )