Browse Source

feat: add equity unlock advisor to property tracker

Made-with: Cursor
pull/6453/head
Priyanka Punukollu 1 month ago
parent
commit
6f62abcb53
  1. 56
      agent/evals/test_equity_advisor.py
  2. 140
      agent/tools/property_tracker.py

56
agent/evals/test_equity_advisor.py

@ -0,0 +1,56 @@
import sys
import os
import asyncio
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'tools'))
from property_tracker import add_property, get_properties, analyze_equity_options
os.environ["ENABLE_REAL_ESTATE"] = "true"
def _add(address, purchase_price, current_value, mortgage_balance):
"""Helper: run async add_property and return the property dict."""
result = asyncio.run(add_property(
address=address,
purchase_price=purchase_price,
current_value=current_value,
mortgage_balance=mortgage_balance,
))
return result["result"]["property"]
def test_equity_three_options_returned():
prop = _add(
address="123 Equity Test St Austin TX",
purchase_price=400000,
current_value=520000,
mortgage_balance=380000,
)
result = analyze_equity_options(prop["id"])
assert "options" in result
assert "leave_untouched" in result["options"]
assert "cash_out_invest" in result["options"]
assert "rental_property" in result["options"]
assert result["current_equity"] == 140000
assert result["accessible_equity"] == 112000
def test_equity_math_correct():
prop = _add("Math Test Property", 300000, 450000, 200000)
result = analyze_equity_options(prop["id"])
assert result["current_equity"] == 250000
assert result["accessible_equity"] == 200000
def test_equity_recommendation_exists():
prop = _add("Rec Test", 350000, 480000, 320000)
result = analyze_equity_options(prop["id"])
assert "recommendation" in result
assert len(result["recommendation"]) > 20
assert "disclaimer" in result
def test_equity_bad_property_id():
result = analyze_equity_options("nonexistent-id-99999")
assert result is not None
assert "error" in result

140
agent/tools/property_tracker.py

@ -639,3 +639,143 @@ async def get_total_net_worth(portfolio_value: float) -> dict:
"summary": summary,
},
}
def analyze_equity_options(
property_id: str,
market_return_assumption: float = 0.07,
) -> dict:
"""Analyzes 3 options for home equity: leave untouched,
cash-out refi and invest, or use for rental property."""
db_path = os.path.join(
os.path.dirname(__file__), '..', 'data', 'properties.db'
)
try:
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
cur = conn.cursor()
cur.execute(
"SELECT * FROM properties WHERE id=? AND is_active=1",
(property_id,),
)
row = cur.fetchone()
conn.close()
except Exception as e:
return {
"error": f"Database error: {str(e)}",
"property_id": property_id,
}
if not row:
return {
"error": f"Property {property_id} not found",
"property_id": property_id,
}
current_value = row["current_value"]
mortgage_balance = row["mortgage_balance"] or 0
equity = current_value - mortgage_balance
accessible = equity * 0.80
if accessible <= 0:
return {
"property_address": row["address"],
"current_value": current_value,
"mortgage_balance": mortgage_balance,
"current_equity": equity,
"accessible_equity": 0,
"message": "Insufficient equity for cash-out options",
"options": {},
}
monthly_rate = 0.0695 / 12
n = 360
# Option A: Leave untouched
projected_value_a = current_value * (1.04 ** 10)
equity_a = projected_value_a - mortgage_balance
# Option B: Cash-out refi + invest
new_balance = mortgage_balance + accessible
new_payment = new_balance * (
monthly_rate * (1 + monthly_rate) ** n
) / ((1 + monthly_rate) ** n - 1)
old_payment = (
mortgage_balance
* (monthly_rate * (1 + monthly_rate) ** n)
/ ((1 + monthly_rate) ** n - 1)
if mortgage_balance > 0
else 0
)
payment_increase = new_payment - old_payment
invested_b = accessible * ((1 + market_return_assumption) ** 10)
home_equity_b = (current_value * 1.04 ** 10) - new_balance
total_b = home_equity_b + invested_b
# Option C: Rental property
rental_price = current_value * 0.9
rental_down = accessible
rental_mortgage_balance = rental_price - rental_down
rental_payment = (
rental_mortgage_balance
* (monthly_rate * (1 + monthly_rate) ** n)
/ ((1 + monthly_rate) ** n - 1)
if rental_mortgage_balance > 0
else 0
)
monthly_rent_income = current_value * 0.007
monthly_cash_flow = monthly_rent_income - rental_payment
ten_yr_cash_flow = monthly_cash_flow * 120
return {
"property_address": row["address"],
"current_value": current_value,
"mortgage_balance": mortgage_balance,
"current_equity": round(equity),
"accessible_equity": round(accessible),
"options": {
"leave_untouched": {
"label": "Option A — Do Nothing",
"projected_equity_10yr": round(equity_a),
"projected_home_value": round(projected_value_a),
"upside": "No risk, no new debt, steady appreciation",
"downside": "Equity is illiquid, not generating returns",
},
"cash_out_invest": {
"label": "Option B — Cash-Out Refi + Invest",
"cash_extracted": round(accessible),
"monthly_payment_increase": round(payment_increase),
"invested_value_10yr": round(invested_b),
"total_wealth_10yr": round(total_b),
"upside": "Equity working harder in the market",
"downside": "Higher payment, market risk",
},
"rental_property": {
"label": "Option C — Buy Rental Property",
"monthly_cash_flow": round(monthly_cash_flow),
"ten_year_cash_flow": round(ten_yr_cash_flow),
"upside": "Passive income + appreciation on 2 properties",
"downside": "Landlord responsibilities, vacancy risk",
},
},
"recommendation": (
f"Option B generates the most total wealth at "
f"${total_b:,.0f} by year 10 if markets perform well. "
f"Option C provides ${monthly_cash_flow:,.0f}/mo passive income. "
f"Option A is best for simplicity and certainty."
),
"disclaimer": (
"These are projections not guarantees. "
"Consult a financial advisor before refinancing."
),
"data_source": (
"Market assumptions: 4% home appreciation, "
f"{market_return_assumption * 100:.0f}% investment return, "
"6.95% mortgage rate"
),
}

Loading…
Cancel
Save