Skip to content

Commit b08a384

Browse files
committed
hopefully i can sleep now
1 parent b37ac4f commit b08a384

5 files changed

Lines changed: 284 additions & 27 deletions

File tree

0 Bytes
Binary file not shown.

app.py

Lines changed: 116 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,16 +1305,18 @@ def compute_retirement_corpus(age, monthly_income, desired_monthly_pension=None,
13051305

13061306
def compute_term_insurance_need(age, monthly_income, retirement_age=60):
13071307
"""
1308-
Calculate term insurance requirement using Human Life Value method.
1309-
Formula: (retirement_age - age) * monthly_income * 12
1308+
Calculate term insurance requirement.
1309+
Formula: 15x annual income × 1.3 (inflation buffer) = 19.5x annual income
1310+
1311+
For example: ₹30,000/month = ₹3.6 lakh/year → Required cover = ₹70.2 lakh
13101312
13111313
Returns the required term cover amount.
13121314
"""
1313-
age_val = _safe_float(age, 30)
13141315
mi = _safe_float(monthly_income, 0.0)
1315-
years_to_retirement = max(0, retirement_age - age_val)
1316+
annual_income = mi * 12
13161317

1317-
required_cover = years_to_retirement * mi * 12
1318+
# 15x annual income with 1.3x inflation buffer = 19.5x
1319+
required_cover = annual_income * 15 * 1.3
13181320
return round(required_cover, 0)
13191321

13201322
def compute_ihs(savings_percent, current_products, allocation):
@@ -3724,7 +3726,7 @@ def generate_financial_plan_pdf(q: dict, analysis: dict, output_path: str, doc_i
37243726
["Current Status", life_status],
37253727
["Current Coverage", f"Rs. {_format_indian_amount(life_cover)}" if life_cover > 0 else "Rs. 0"],
37263728
["Recommended Coverage", f"Rs. {_format_indian_amount(required_term_cover)} minimum"],
3727-
["Basis", "10x annual income"],
3729+
["Basis", "15x annual income (with 1.3x inflation buffer)"],
37283730
["Gap", f"Rs. {_format_indian_amount(max(0, required_term_cover - life_cover))}"],
37293731
]
37303732
life_table = Table(
@@ -4003,21 +4005,48 @@ def generate_financial_plan_pdf(q: dict, analysis: dict, output_path: str, doc_i
40034005
funding_pct = 100
40044006
available_for_goals = monthly_surplus
40054007

4006-
# Reality Check Summary Box
4008+
# Reality Check Summary Box - Enhanced with Existing SIP Context
40074009
story.append(Spacer(1, 12))
4008-
story.append(Paragraph("<b>Reality Check: Affordability Summary</b>", styles["h2"]))
4010+
story.append(Paragraph("<b>Reality Check: Your Actual Financial Situation</b>", styles["h2"]))
4011+
4012+
# Calculate insurance provision for proper context
4013+
term_gap = max(0, required_term_cover - life_cover)
4014+
health_gap = max(0, required_health_cover - health_cover)
4015+
client_age = _safe_float(age, 35)
4016+
term_rate_per_lakh = 500 if client_age < 35 else (700 if client_age < 45 else 1200)
4017+
health_rate_per_lakh = 2500 if client_age < 35 else (3500 if client_age < 45 else 5000)
4018+
term_premium_yearly = (term_gap / 100000) * term_rate_per_lakh if term_gap > 0 else 0
4019+
health_premium_yearly = (health_gap / 100000) * health_rate_per_lakh if health_gap > 0 else 0
4020+
monthly_insurance_provision = (term_premium_yearly + health_premium_yearly) / 12
4021+
4022+
# Calculate remaining after insurance
4023+
remaining_after_insurance = max(0, monthly_surplus - monthly_insurance_provision)
4024+
4025+
# Total investing = existing SIP + new allocations
4026+
total_investing = total_monthly_sip + remaining_after_insurance
4027+
total_investing_pct = (total_investing / total_required_sip * 100) if total_required_sip > 0 else 100
40094028

40104029
summary_rows = [
4011-
["Total Goals", f"{len(goals)}"],
4012-
["Total Required SIP", f"Rs. {total_required_sip:,.0f}/month"],
4013-
["Current SIP Commitment", f"Rs. {total_monthly_sip:,.0f}/month" if total_monthly_sip > 0 else "None"],
4014-
["Available Monthly Surplus", f"Rs. {_format_indian_amount(monthly_surplus)}"],
4015-
["Available for New Goals", f"Rs. {_format_indian_amount(available_for_goals)}"],
4016-
["Funding Capacity", f"{min(funding_pct, 100):.0f}% of requirement"],
4030+
["Monthly Income", f"Rs. {_format_indian_amount(monthly_income)}"],
4031+
["Monthly Expenses", f"Rs. {_format_indian_amount(monthly_expenses)}"],
4032+
["Monthly Surplus", f"Rs. {_format_indian_amount(monthly_surplus)}"],
4033+
["", ""], # Separator row
4034+
["Current SIP (Already Investing)", f"Rs. {total_monthly_sip:,.0f}/month" if total_monthly_sip > 0 else "None"],
40174035
]
40184036

4019-
if shortfall > 0:
4020-
summary_rows.append(["Monthly Shortfall", f"Rs. {shortfall:,.0f}/month"])
4037+
if monthly_insurance_provision > 0:
4038+
summary_rows.append(["Insurance Provision Needed", f"Rs. {monthly_insurance_provision:,.0f}/month"])
4039+
summary_rows.append(["Available for NEW Additions", f"Rs. {remaining_after_insurance:,.0f}/month"])
4040+
4041+
summary_rows.extend([
4042+
["", ""], # Separator row
4043+
["TOTAL Investing", f"Rs. {total_investing:,.0f}/month"],
4044+
["Goal Requirement", f"Rs. {total_required_sip:,.0f}/month"],
4045+
["Coverage", f"{min(total_investing_pct, 100):.1f}% of requirement"],
4046+
])
4047+
4048+
if total_required_sip > total_investing:
4049+
summary_rows.append(["Shortfall", f"Rs. {total_required_sip - total_investing:,.0f}/month"])
40214050

40224051
summary_table = Table(
40234052
summary_rows,
@@ -4026,9 +4055,9 @@ def generate_financial_plan_pdf(q: dict, analysis: dict, output_path: str, doc_i
40264055
)
40274056

40284057
# Color based on funding status
4029-
if funding_pct >= 95:
4058+
if total_investing_pct >= 95:
40304059
header_color = colors.HexColor('#2D6A4F') # Green
4031-
elif funding_pct >= 50:
4060+
elif total_investing_pct >= 50:
40324061
header_color = colors.HexColor('#E9C46A') # Yellow
40334062
else:
40344063
header_color = colors.HexColor('#9D4B45') # Red
@@ -4041,6 +4070,75 @@ def generate_financial_plan_pdf(q: dict, analysis: dict, output_path: str, doc_i
40414070
('BOTTOMPADDING', (0,0), (-1,-1), 6),
40424071
]))
40434072
story.append(summary_table)
4073+
story.append(Spacer(1, 12))
4074+
4075+
# =========================================================================
4076+
# GOAL-WISE SIP ALLOCATION TABLE
4077+
# =========================================================================
4078+
if len(goal_analysis) > 0:
4079+
story.append(Paragraph("<b>Goal-wise SIP Allocation</b>", styles["h2"]))
4080+
4081+
# Calculate per-goal allocations
4082+
goal_sip_rows = [["Goal", "Required SIP", "Allocated SIP", "Gap", "Status"]]
4083+
4084+
for ga in goal_analysis:
4085+
g_name = ga["name"][:25] # Truncate long names
4086+
required_sip = ga["required_sip"]
4087+
4088+
# Proportional allocation from available surplus
4089+
if total_required_sip > 0 and required_sip > 0:
4090+
allocated_sip = (required_sip / total_required_sip) * remaining_after_insurance
4091+
else:
4092+
allocated_sip = remaining_after_insurance / max(1, len(goal_analysis))
4093+
4094+
# Add share of existing SIP (proportionally)
4095+
if total_monthly_sip > 0 and total_required_sip > 0:
4096+
existing_sip_share = (required_sip / total_required_sip) * total_monthly_sip
4097+
allocated_sip += existing_sip_share
4098+
4099+
gap = max(0, required_sip - allocated_sip)
4100+
4101+
if gap <= 0 or (allocated_sip >= required_sip * 0.95):
4102+
status = "✓ Funded"
4103+
elif allocated_sip >= required_sip * 0.5:
4104+
status = "Partial"
4105+
else:
4106+
status = "Gap"
4107+
4108+
goal_sip_rows.append([
4109+
sanitize_pdf_text(g_name),
4110+
f"Rs. {required_sip:,.0f}",
4111+
f"Rs. {allocated_sip:,.0f}",
4112+
f"Rs. {gap:,.0f}" if gap > 0 else "-",
4113+
status
4114+
])
4115+
4116+
goal_sip_table = Table(goal_sip_rows, hAlign="LEFT", colWidths=[120, 80, 80, 80, 60])
4117+
goal_sip_table.setStyle(TableStyle([
4118+
('BACKGROUND', (0,0), (-1,0), colors.HexColor('#457B9D')),
4119+
('TEXTCOLOR', (0,0), (-1,0), colors.white),
4120+
('GRID', (0,0), (-1,-1), 0.5, colors.grey),
4121+
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
4122+
('FONTSIZE', (0,0), (-1,-1), 8),
4123+
('BOTTOMPADDING', (0,0), (-1,0), 6),
4124+
]))
4125+
story.append(goal_sip_table)
4126+
story.append(Spacer(1, 8))
4127+
4128+
# Context message
4129+
if total_monthly_sip > 0:
4130+
story.append(Paragraph(
4131+
f"<i>Note: You are already investing Rs. {total_monthly_sip:,.0f}/month through existing SIPs. "
4132+
f"After insurance provision, Rs. {remaining_after_insurance:,.0f}/month can be added. "
4133+
f"Total: Rs. {total_investing:,.0f}/month ({total_investing_pct:.1f}% of goal needs).</i>",
4134+
styles["BodyText"]
4135+
))
4136+
else:
4137+
story.append(Paragraph(
4138+
f"<i>Note: After insurance provision of Rs. {monthly_insurance_provision:,.0f}/month, "
4139+
f"Rs. {remaining_after_insurance:,.0f}/month is available for goal SIPs.</i>",
4140+
styles["BodyText"]
4141+
))
40444142

40454143
# =========================================================================
40464144
# REALISTIC ACTION PLAN - What you CAN do with what you HAVE

llm_sections.py

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,7 @@ def compute_allocation(
539539
emergency_fund_target: float = 0.0,
540540
emergency_fund_current: float = 0.0,
541541
existing_investments: float = 0.0,
542+
existing_sip_commitments: float = 0.0, # Already running SIP (counts towards total investing)
542543
) -> Dict[str, Any]:
543544
"""
544545
Compute priority-based allocation of monthly surplus with enhanced logic.
@@ -564,6 +565,7 @@ def compute_allocation(
564565
emergency_fund_target: Target emergency fund amount (6 months expenses) (NEW)
565566
emergency_fund_current: Current emergency fund amount (NEW)
566567
existing_investments: Total current investment value that can be allocated to goals (NEW)
568+
existing_sip_commitments: Already running SIPs that count towards total investing (NEW)
567569
568570
Returns:
569571
Dict with priority breakdown, per-goal table, achievement %, and bridge recommendations
@@ -751,6 +753,13 @@ def compute_allocation(
751753
# Generate bridge recommendations if shortfall exists
752754
bridge_recs = generate_bridge_recommendations(total_shortfall, remaining_for_goals_and_emergency) if total_shortfall > 0 else []
753755

756+
# Calculate total investing (existing SIP + new allocations after insurance)
757+
new_allocations_after_insurance = remaining_for_goals_and_emergency
758+
total_investing = existing_sip_commitments + new_allocations_after_insurance
759+
760+
# Calculate what % of goal requirement is being funded
761+
goal_funding_pct = (total_investing / total_ideal_sip * 100) if total_ideal_sip > 0 else 100.0
762+
754763
return {
755764
"priority_breakdown": priority_items,
756765
"monthly_surplus": round(monthly_surplus, 0),
@@ -767,7 +776,18 @@ def compute_allocation(
767776
"savings_shortfall": round(total_shortfall, 0) if total_shortfall > 0 else 0,
768777
"bridge_recommendations": bridge_recs,
769778
"existing_investments_allocation": investments_per_goal,
770-
"summary": f"With Rs. {monthly_surplus:,.0f}/month surplus: Rs. {total_insurance_sip_monthly:,.0f} for insurance SIP, Rs. {remaining_for_goals_and_emergency:,.0f} divided equally among {num_allocation_buckets} items ({goal_achievement_pct:.0f}% of goal requirement).",
779+
# NEW: Existing SIP tracking for proper report messages
780+
"existing_sip_running": round(existing_sip_commitments, 0),
781+
"new_allocations_available": round(new_allocations_after_insurance, 0),
782+
"total_investing": round(total_investing, 0),
783+
"goal_funding_percent": round(goal_funding_pct, 1),
784+
"summary": (
785+
f"With Rs. {monthly_surplus:,.0f}/month surplus: "
786+
f"Rs. {total_insurance_sip_monthly:,.0f} for insurance, "
787+
f"Rs. {remaining_for_goals_and_emergency:,.0f} remaining for goals. "
788+
+ (f"Already investing Rs. {existing_sip_commitments:,.0f}/month. " if existing_sip_commitments > 0 else "")
789+
+ f"Total investing: Rs. {total_investing:,.0f}/month ({goal_funding_pct:.1f}% of goal requirement)."
790+
),
771791
"insurance_recommendation": {
772792
"use_savings": insurance_from_savings > 0,
773793
"savings_used": round(insurance_from_savings, 0),
@@ -1179,7 +1199,7 @@ def digest(self, facts: Dict[str, Any]) -> Dict[str, Any]:
11791199
# Fallback: compute from required vs current
11801200
required_cover = _coerce_float(
11811201
(analysis.get("_diagnostics") or {}).get("requiredLifeCover"),
1182-
monthly_income * 12 * 10 # Default: 10x annual income
1202+
monthly_income * 12 * 15 * 1.3 # Default: 15x annual income with 1.3x inflation buffer
11831203
)
11841204
current_life_cover = _coerce_float(insurance.get("lifeCover"), 0.0)
11851205
term_gap = max(0, required_cover - current_life_cover)
@@ -1227,6 +1247,7 @@ def digest(self, facts: Dict[str, Any]) -> Dict[str, Any]:
12271247
emergency_fund_target=emergency_fund_target,
12281248
emergency_fund_current=emergency_fund_current,
12291249
existing_investments=existing_investments,
1250+
existing_sip_commitments=existing_sip, # Pass existing SIP for proper tracking
12301251
)
12311252

12321253
# --- Affordability Summary ---
@@ -1284,6 +1305,13 @@ def prompt(self, digest: Dict[str, Any]) -> Tuple[str, str]:
12841305
achievement_pct = priority_data.get("goal_achievement_percent", 100)
12851306
bridge_recs = priority_data.get("bridge_recommendations") or []
12861307

1308+
# Extract existing SIP tracking (NEW)
1309+
existing_sip_running = priority_data.get("existing_sip_running", 0)
1310+
new_allocations_available = priority_data.get("new_allocations_available", 0)
1311+
total_investing = priority_data.get("total_investing", 0)
1312+
goal_funding_pct = priority_data.get("goal_funding_percent", 0)
1313+
insurance_sip = priority_data.get("insurance_sip_monthly", 0)
1314+
12871315
# Extract affordability summary for budget enforcement
12881316
affordability = digest.get("affordability_summary") or {}
12891317
remaining_budget = affordability.get("remaining_budget_for_goals", 0)
@@ -1312,13 +1340,27 @@ def prompt(self, digest: Dict[str, Any]) -> Tuple[str, str]:
13121340
if constraint_violated:
13131341
user += (
13141342
"⚠️ **CRITICAL BUDGET CONSTRAINT - READ CAREFULLY:**\n"
1315-
f" - Available monthly budget for goals: Rs. {remaining_budget:,.0f}\n"
1316-
f" - Total IDEAL SIP needed (if unlimited funds): Rs. {total_ideal:,.0f}\n"
1317-
f" - Recommended SIP allocation (within budget): Rs. {total_affordable:,.0f}\n"
1318-
f" - Funding gap: Rs. {funding_gap:,.0f}/month\n"
1319-
f" - Goals can be {fundable_pct:.0f}% funded with current savings\n\n"
1320-
f"**YOU MUST RECOMMEND TOTAL SIPs OF Rs. {total_affordable:,.0f}/month OR LESS.**\n"
1321-
"**USE THE 'affordable_sip' VALUE FOR EACH GOAL, NOT 'ideal_sip'.**\n\n"
1343+
)
1344+
# Add existing SIP context if client is already investing
1345+
if existing_sip_running > 0:
1346+
user += (
1347+
f" ✓ Already investing (current SIP running): Rs. {existing_sip_running:,.0f}/month\n"
1348+
f" - Insurance provision needed: Rs. {insurance_sip:,.0f}/month\n"
1349+
f" + Available for NEW additions: Rs. {new_allocations_available:,.0f}/month\n"
1350+
f" = TOTAL investing after changes: Rs. {total_investing:,.0f}/month ({goal_funding_pct:.1f}% of goal needs)\n\n"
1351+
)
1352+
else:
1353+
user += (
1354+
f" - Available monthly budget for goals: Rs. {remaining_budget:,.0f}\n"
1355+
f" - Total IDEAL SIP needed (if unlimited funds): Rs. {total_ideal:,.0f}\n"
1356+
f" - Insurance provision needed: Rs. {insurance_sip:,.0f}/month\n"
1357+
f" - Available after insurance: Rs. {new_allocations_available:,.0f}/month\n"
1358+
)
1359+
user += (
1360+
f" - REQUIRED for all goals: Rs. {total_ideal:,.0f}/month\n"
1361+
f" - SHORTFALL: Rs. {funding_gap:,.0f}/month\n\n"
1362+
f"**DO NOT say 'no surplus remains' or 'Available: Rs. 0' unless truly zero.**\n"
1363+
f"**Acknowledge existing investments and remaining capacity accurately.**\n\n"
13221364
)
13231365

13241366
user += (

output/index.db

12 KB
Binary file not shown.

0 commit comments

Comments
 (0)