Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 135 additions & 4 deletions backend/agents/deterministic/financial_reality.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,29 @@
Financial Reality Agent — Pure math, no AI.
Standard amortization formula. Deterministic output for the same inputs every time.
No imports from Dev 2's files.

Feature additions:
Feature 3: Bank FOIR (Eligibility) Underwriting Check
- Computes total FOIR = (new EMI + existing EMIs) / monthly income
- Flags breach if FOIR > 50% (RBI benchmark)

Feature 5: Property Age & Structural Depreciation Factor
- RCC building expected lifespan: 60 years
- Banks typically reduce LTV below standard 80% for buildings >20 yrs old
- Buildings >40 yrs: max LTV 50–60%; structurally risky
"""
from datetime import date
from typing import Optional, Tuple
from schemas.schemas import (
UserInput, FinancialRealityOutput, AffordabilityStatus, IndiaCostBreakdown
)
from engines.india_defaults import calculate_true_total_cost


# ---------------------------------------------------------------------------
# EMI helpers
# ---------------------------------------------------------------------------

def _calculate_emi(principal: float, annual_rate: float, tenure_years: int) -> float:
"""Standard EMI amortization formula: EMI = P * r * (1+r)^n / ((1+r)^n - 1)"""
if principal <= 0:
Expand Down Expand Up @@ -37,12 +53,105 @@ def _reverse_loan_from_emi(target_emi: float, annual_rate: float, tenure_years:
return principal


# ---------------------------------------------------------------------------
# Feature 3: FOIR Underwriting Check
# ---------------------------------------------------------------------------

FOIR_LIMIT = 0.50 # RBI benchmark; some banks use 0.55 for HNI

def _compute_foir(
emi: float,
existing_obligations: float,
monthly_income: float,
) -> Tuple[float, bool, Optional[str]]:
"""
Returns (foir_ratio, foir_breach, warning_message).

foir_ratio = (new_EMI + all_existing_EMIs) / monthly_income
"""
if monthly_income <= 0:
return 1.0, True, "Income not provided — FOIR cannot be calculated."

total_obligations = emi + existing_obligations
foir = total_obligations / monthly_income

if foir > FOIR_LIMIT:
warning = (
f"FOIR is {foir*100:.1f}% — exceeds the RBI 50% underwriting limit. "
f"Total monthly debt (₹{total_obligations:,.0f}) on income of "
f"₹{monthly_income:,.0f} will likely trigger a bank rejection. "
f"Consider reducing loan amount or increasing income documentation."
)
return round(foir, 4), True, warning

return round(foir, 4), False, None


# ---------------------------------------------------------------------------
# Feature 5: Property Age & Structural Depreciation
# ---------------------------------------------------------------------------

RCC_LIFESPAN_YEARS = 60 # Standard RCC building life expectancy
LTV_STANDARD = 0.80 # Bank standard LTV for properties < 20 yrs
LTV_MODERATE = 0.70 # 20–30 yrs old
LTV_AGED = 0.60 # 30–40 yrs old
LTV_OLD = 0.50 # > 40 yrs old


def _compute_depreciation(
construction_year: Optional[int],
) -> Tuple[Optional[int], Optional[float], bool, Optional[str]]:
"""
Returns (structural_life_remaining, ltv_recommended_pct, ltv_age_risk, warning).
"""
if construction_year is None:
return None, None, False, None

current_year = date.today().year
building_age = current_year - construction_year
life_remaining = max(0, RCC_LIFESPAN_YEARS - building_age)

if building_age < 20:
ltv_pct = LTV_STANDARD
risk = False
warning = None
elif building_age < 30:
ltv_pct = LTV_MODERATE
risk = True
warning = (
f"Building is {building_age} years old. Banks may restrict LTV to 70% "
f"(vs. standard 80%), increasing the required down payment significantly."
)
elif building_age < 40:
ltv_pct = LTV_AGED
risk = True
warning = (
f"Building is {building_age} years old — entering the aging bracket. "
f"Expect bank LTV of ~60%. Structural survey strongly recommended before purchase."
)
else:
ltv_pct = LTV_OLD
risk = True
warning = (
f"CRITICAL: Building is {building_age} years old with only "
f"~{life_remaining} years of structural life remaining. "
f"Banks typically offer max LTV of 50%. High risk of future maintenance "
f"levy, redevelopment disputes, and difficulty reselling."
)

return life_remaining, ltv_pct, risk, warning


# ---------------------------------------------------------------------------
# Main calculation
# ---------------------------------------------------------------------------

def calculate_affordability(user_input: UserInput) -> FinancialRealityOutput:
"""
Calculates the complete financial reality of a home purchase.
Pure math. Zero LLM involvement. Deterministic.
"""
# --- India cost breakdown ---
# --- India cost breakdown (now with GST slab, district stamp duty, hidden fees) ---
loan_amount = user_input.property_price - user_input.down_payment
if loan_amount < 0:
loan_amount = 0.0
Expand All @@ -53,6 +162,9 @@ def calculate_affordability(user_input: UserInput) -> FinancialRealityOutput:
property_type=user_input.property_type.value,
loan_amount=loan_amount,
area_sqft=user_input.area_sqft if user_input.area_sqft else 1000,
area_sqm=user_input.area_sqm,
district=user_input.district,
is_female_buyer=user_input.is_female_buyer,
)

# --- EMI calculation ---
Expand All @@ -70,7 +182,6 @@ def calculate_affordability(user_input: UserInput) -> FinancialRealityOutput:
monthly_surplus_after_emi = user_input.monthly_income - user_input.monthly_expenses - emi

# --- 12-month cash flow projection ---
# Shows cumulative savings each month starting from current savings
cash_flow_12_months = []
running_savings = user_input.total_savings - user_input.down_payment
savings_depletion_month = None
Expand All @@ -82,12 +193,10 @@ def calculate_affordability(user_input: UserInput) -> FinancialRealityOutput:
savings_depletion_month = month

# --- Safe and maximum property prices ---
# Safe: EMI = exactly 35% of income
target_emi_safe = user_input.monthly_income * 0.35
safe_loan = _reverse_loan_from_emi(target_emi_safe, user_input.annual_interest_rate, user_input.tenure_years)
safe_property_price = safe_loan + user_input.down_payment

# Maximum: EMI = exactly 50% of income
target_emi_max = user_input.monthly_income * 0.50
max_loan = _reverse_loan_from_emi(target_emi_max, user_input.annual_interest_rate, user_input.tenure_years)
maximum_property_price = max_loan + user_input.down_payment
Expand All @@ -100,6 +209,18 @@ def calculate_affordability(user_input: UserInput) -> FinancialRealityOutput:
else:
affordability_status = AffordabilityStatus.OVEREXTENDED

# --- Feature 3: FOIR Underwriting Check ---
foir_ratio, foir_breach, foir_warning = _compute_foir(
emi=emi,
existing_obligations=user_input.existing_emi_obligations,
monthly_income=user_input.monthly_income,
)

# --- Feature 5: Property Age & Structural Depreciation ---
structural_life_remaining, ltv_recommended_pct, ltv_age_risk, age_depreciation_warning = (
_compute_depreciation(user_input.construction_year)
)

return FinancialRealityOutput(
emi=round(emi, 2),
emi_to_income_ratio=round(emi_to_income_ratio, 4),
Expand All @@ -112,4 +233,14 @@ def calculate_affordability(user_input: UserInput) -> FinancialRealityOutput:
india_cost_breakdown=india_cost_breakdown,
loan_amount=round(loan_amount, 2),
total_interest_payable=round(total_interest_payable, 2),
# Feature 3: FOIR
foir_ratio=foir_ratio,
foir_limit=FOIR_LIMIT,
foir_breach=foir_breach,
foir_warning=foir_warning,
# Feature 5: Depreciation
structural_life_remaining=structural_life_remaining,
ltv_recommended_pct=ltv_recommended_pct,
ltv_age_risk=ltv_age_risk,
age_depreciation_warning=age_depreciation_warning,
)
67 changes: 67 additions & 0 deletions backend/engines/compute.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""
Headless Compute Engine — All deterministic math in one function call.

compute_all() runs every deterministic agent in sequence and returns the
complete bundled result. This is the function behind the /api/v1/calculate
endpoint — designed for sub-50ms execution with zero LLM involvement.

Use cases:
- Frontend real-time sliders that update as the user drags
- Comparison tools that need fast recalculation
- Batch processing without burning API quota
- Unit testing the math pipeline in isolation
"""
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from schemas.schemas import UserInput, ComputeAllOutput
from engines.india_defaults import calculate_true_total_cost
from agents.deterministic.financial_reality import calculate_affordability
from agents.deterministic.scenario_simulation import run_all_scenarios
from agents.deterministic.risk_scorer import calculate_risk_score


def compute_all(user_input: UserInput) -> ComputeAllOutput:
"""
Run every deterministic calculation and return the bundled result.

Execution order (respects dependencies):
1. india_defaults — hidden cost breakdown (independent)
2. affordability — EMI, ratios, cash flow (independent)
3. scenarios — stress tests (depends on #2)
4. risk_score — composite score (depends on #2 and #3)

Total execution time target: < 50ms on a single CPU core.
All functions are pure Python arithmetic with no I/O.
"""
loan_amount = user_input.property_price - user_input.down_payment
if loan_amount < 0:
loan_amount = 0.0

india_costs = calculate_true_total_cost(
base_price=user_input.property_price,
state=user_input.state,
property_type=user_input.property_type.value,
loan_amount=loan_amount,
area_sqft=user_input.area_sqft if user_input.area_sqft else 1000,
area_sqm=user_input.area_sqm,
district=user_input.district,
is_female_buyer=user_input.is_female_buyer,
)

financial_reality = calculate_affordability(user_input)
all_scenarios = run_all_scenarios(user_input, financial_reality)
risk_score = calculate_risk_score(
financial_reality=financial_reality,
all_scenarios=all_scenarios,
age=user_input.age,
tenure_years=user_input.tenure_years,
)

return ComputeAllOutput(
india_cost_breakdown=india_costs,
financial_reality=financial_reality,
all_scenarios=all_scenarios,
risk_score=risk_score,
)
Loading