mirror of https://github.com/ghostfolio/ghostfolio
Browse Source
- New agent/tools/wealth_bridge.py with 3 registered agent tools:
* calculate_down_payment_power(): maps portfolio value to 7 Austin markets
(or any cities) with can_afford_full/conservative/safe + monthly payment estimates
* calculate_job_offer_affordability(): COL-adjusted salary comparison for any
two cities worldwide using ACTRIS COL index (Austin) + Teleport API (global)
* get_portfolio_real_estate_summary(): reads live Ghostfolio portfolio + runs
down payment analysis in one call
- graph.py: add wealth_bridge imports + 4 new query_type routes:
wealth_down_payment, wealth_job_offer, wealth_global_city, wealth_portfolio_summary
- graph.py: add _extract_salary, _extract_offer_city, _extract_current_city helpers
- Mortgage formula: 30yr @ 6.95%, 20% down, ×1.25 for PITI
Made-with: Cursor
pull/6453/head
2 changed files with 676 additions and 0 deletions
@ -0,0 +1,530 @@ |
|||||
|
""" |
||||
|
Wealth Bridge Tool — AgentForge integration |
||||
|
============================================ |
||||
|
Bridges live Ghostfolio portfolio data with real estate purchasing power. |
||||
|
|
||||
|
Three capabilities: |
||||
|
1. calculate_down_payment_power(portfolio_value, target_cities) |
||||
|
— which markets can your portfolio fund a 20% down payment? |
||||
|
2. calculate_job_offer_affordability(offer_salary, offer_city, |
||||
|
current_salary, current_city) |
||||
|
— is the job offer a real raise in purchasing power terms? |
||||
|
3. get_portfolio_real_estate_summary(target_cities) |
||||
|
— master function: reads live portfolio then runs down payment calc |
||||
|
|
||||
|
Data routing: |
||||
|
Austin TX areas → _MOCK_SNAPSHOTS in real_estate.py (real ACTRIS data) |
||||
|
All other cities → teleport_api.py (live Teleport API + fallback) |
||||
|
|
||||
|
Mortgage assumption: 30-year fixed at 6.95%, 20% down, payment × 1.25 |
||||
|
for estimated taxes + insurance. |
||||
|
""" |
||||
|
|
||||
|
import asyncio |
||||
|
from typing import Optional |
||||
|
|
||||
|
from agent.tools.real_estate import _MOCK_SNAPSHOTS, _normalize_city |
||||
|
from agent.tools.teleport_api import ( |
||||
|
HARDCODED_FALLBACK, |
||||
|
_is_austin_area, |
||||
|
get_city_housing_data, |
||||
|
) |
||||
|
|
||||
|
# --------------------------------------------------------------------------- |
||||
|
# COL index values for Austin TX sub-markets (ACTRIS coverage areas) |
||||
|
# National average = 100; lower = more affordable |
||||
|
# --------------------------------------------------------------------------- |
||||
|
|
||||
|
_AUSTIN_COL_INDEX: dict[str, float] = { |
||||
|
"austin": 95.4, |
||||
|
"travis_county": 95.4, |
||||
|
"austin_msa": 91.0, |
||||
|
"williamson_county": 88.2, |
||||
|
"hays_county": 82.1, |
||||
|
"bastrop_county": 78.3, |
||||
|
"caldwell_county": 71.2, |
||||
|
} |
||||
|
|
||||
|
# State income tax lookup (no tax = not in this dict) |
||||
|
_STATE_INCOME_TAX: dict[str, float] = { |
||||
|
"CA": 0.093, "NY": 0.0685, "OR": 0.099, "MN": 0.0985, |
||||
|
"NJ": 0.1075, "VT": 0.0875, "DC": 0.0895, "HI": 0.11, |
||||
|
"ME": 0.0715, "IA": 0.0575, "SC": 0.07, "CT": 0.0699, |
||||
|
"WI": 0.0765, "MA": 0.05, "IL": 0.0495, "IN": 0.032, |
||||
|
"MI": 0.0425, "GA": 0.055, "NC": 0.0499, "VA": 0.0575, |
||||
|
"MD": 0.0575, "CO": 0.044, "AZ": 0.025, "UT": 0.0485, |
||||
|
"KS": 0.057, "MO": 0.0495, "OH": 0.0399, "PA": 0.0307, |
||||
|
"NM": 0.059, "LA": 0.06, "MS": 0.05, "AL": 0.05, |
||||
|
"AR": 0.044, "NE": 0.0664, "ID": 0.058, "MT": 0.069, |
||||
|
"ND": 0.029, "OK": 0.0475, "KY": 0.045, |
||||
|
} |
||||
|
|
||||
|
_NO_INCOME_TAX_STATES = {"TX", "WA", "FL", "NV", "WY", "SD", "AK", "NH", "TN"} |
||||
|
|
||||
|
# Default target markets (all 7 Austin ACTRIS areas) |
||||
|
_DEFAULT_AUSTIN_MARKETS = [ |
||||
|
"austin", "travis_county", "williamson_county", |
||||
|
"hays_county", "bastrop_county", "caldwell_county", "austin_msa", |
||||
|
] |
||||
|
|
||||
|
# Mortgage constants |
||||
|
_MORTGAGE_RATE = 0.0695 |
||||
|
_MORTGAGE_TERM = 360 # 30 years in months |
||||
|
_TAX_INSURANCE_MULTIPLIER = 1.25 |
||||
|
|
||||
|
|
||||
|
# --------------------------------------------------------------------------- |
||||
|
# Internal helpers |
||||
|
# --------------------------------------------------------------------------- |
||||
|
|
||||
|
def _monthly_payment(price: float) -> float: |
||||
|
"""Calculates estimated total monthly payment (PITI) at 6.95% 30yr, 20% down.""" |
||||
|
principal = price * 0.80 |
||||
|
r = _MORTGAGE_RATE / 12 |
||||
|
n = _MORTGAGE_TERM |
||||
|
base_payment = principal * (r * (1 + r) ** n) / ((1 + r) ** n - 1) |
||||
|
return round(base_payment * _TAX_INSURANCE_MULTIPLIER, 0) |
||||
|
|
||||
|
|
||||
|
def _get_austin_data(city_key: str) -> Optional[dict]: |
||||
|
"""Reads ACTRIS snapshot for a given city_key from real_estate._MOCK_SNAPSHOTS.""" |
||||
|
return _MOCK_SNAPSHOTS.get(city_key) |
||||
|
|
||||
|
|
||||
|
def _resolve_city_data_sync(city_name: str) -> tuple[str, dict]: |
||||
|
""" |
||||
|
Synchronously resolves city data for wealth bridge calculations. |
||||
|
Returns (display_name, data_dict). |
||||
|
|
||||
|
Austin areas: uses _MOCK_SNAPSHOTS (real ACTRIS data, sync) |
||||
|
Other cities: uses HARDCODED_FALLBACK (sync) or generic estimate |
||||
|
""" |
||||
|
if _is_austin_area(city_name): |
||||
|
city_key = _normalize_city(city_name) |
||||
|
snap = _MOCK_SNAPSHOTS.get(city_key, {}) |
||||
|
display = snap.get("city", city_name) |
||||
|
return display, snap |
||||
|
|
||||
|
# Non-Austin: look up in fallback dict |
||||
|
lower = city_name.lower().strip() |
||||
|
slug_guess = lower.replace(" ", "-") |
||||
|
# Direct slug match |
||||
|
if slug_guess in HARDCODED_FALLBACK: |
||||
|
return HARDCODED_FALLBACK[slug_guess]["city"], dict(HARDCODED_FALLBACK[slug_guess]) |
||||
|
# Partial match |
||||
|
for slug, data in HARDCODED_FALLBACK.items(): |
||||
|
if lower in data["city"].lower() or slug.replace("-", " ") in lower: |
||||
|
return data["city"], dict(data) |
||||
|
# Generic |
||||
|
return city_name, { |
||||
|
"city": city_name, |
||||
|
"median_price": 500_000, |
||||
|
"MedianRentMonthly": 2000, |
||||
|
"col_index": 100.0, |
||||
|
"AffordabilityScore": 5.0, |
||||
|
"data_source": "Estimate (city not in database)", |
||||
|
} |
||||
|
|
||||
|
|
||||
|
def _col_index_for_city(city_name: str, city_data: dict) -> float: |
||||
|
""" |
||||
|
Returns the COL index for a city (100 = US average). |
||||
|
Austin areas: use _AUSTIN_COL_INDEX table. |
||||
|
Others: use col_index from city_data, or derive from Teleport col_score. |
||||
|
""" |
||||
|
if _is_austin_area(city_name): |
||||
|
city_key = _normalize_city(city_name) |
||||
|
return _AUSTIN_COL_INDEX.get(city_key, 95.4) |
||||
|
|
||||
|
if "col_index" in city_data: |
||||
|
return float(city_data["col_index"]) |
||||
|
|
||||
|
# Derive from Teleport col_score (0–10): 10=cheap, 0=expensive |
||||
|
col_score = city_data.get("col_score", 5.0) |
||||
|
return round((10.0 - col_score) * 18.0 + 20.0, 1) |
||||
|
|
||||
|
|
||||
|
def _state_tax_note(city_a: str, city_b: str) -> str: |
||||
|
"""Builds a human-readable state income tax note for two cities.""" |
||||
|
def _state_code(city: str) -> Optional[str]: |
||||
|
# Simple heuristics from known cities |
||||
|
city_lower = city.lower() |
||||
|
state_map = { |
||||
|
"tx": "TX", "austin": "TX", "dallas": "TX", "houston": "TX", "san antonio": "TX", |
||||
|
"wa": "WA", "seattle": "WA", |
||||
|
"fl": "FL", "miami": "FL", "orlando": "FL", "tampa": "FL", |
||||
|
"nv": "NV", "las vegas": "NV", |
||||
|
"ca": "CA", "san francisco": "CA", "los angeles": "CA", "san diego": "CA", |
||||
|
"ny": "NY", "new york": "NY", "brooklyn": "NY", |
||||
|
"co": "CO", "denver": "CO", |
||||
|
"il": "IL", "chicago": "IL", |
||||
|
"ma": "MA", "boston": "MA", |
||||
|
"tn": "TN", "nashville": "TN", |
||||
|
"ga": "GA", "atlanta": "GA", |
||||
|
"or": "OR", "portland": "OR", |
||||
|
"az": "AZ", "phoenix": "AZ", |
||||
|
"mn": "MN", "minneapolis": "MN", |
||||
|
"nc": "NC", "charlotte": "NC", |
||||
|
"va": "VA", "arlington": "VA", |
||||
|
} |
||||
|
for keyword, code in state_map.items(): |
||||
|
if keyword in city_lower: |
||||
|
return code |
||||
|
return None |
||||
|
|
||||
|
state_a = _state_code(city_a) |
||||
|
state_b = _state_code(city_b) |
||||
|
|
||||
|
if not state_a or not state_b: |
||||
|
return "State income tax comparison not available for one or both cities." |
||||
|
|
||||
|
a_notax = state_a in _NO_INCOME_TAX_STATES |
||||
|
b_notax = state_b in _NO_INCOME_TAX_STATES |
||||
|
|
||||
|
if a_notax and b_notax: |
||||
|
return ( |
||||
|
f"Both {state_a} and {state_b} have no state income tax, " |
||||
|
"so this does not affect the comparison." |
||||
|
) |
||||
|
if a_notax and not b_notax: |
||||
|
rate = _STATE_INCOME_TAX.get(state_b, 0.05) * 100 |
||||
|
return ( |
||||
|
f"{state_a} has no state income tax. {state_b} charges ~{rate:.1f}% — " |
||||
|
"this makes the offer worth even less than the purchasing power calculation shows." |
||||
|
) |
||||
|
if not a_notax and b_notax: |
||||
|
rate = _STATE_INCOME_TAX.get(state_a, 0.05) * 100 |
||||
|
return ( |
||||
|
f"{state_b} has no state income tax vs {state_a}'s ~{rate:.1f}% — " |
||||
|
"the move actually improves your after-tax position beyond the COL calculation." |
||||
|
) |
||||
|
rate_a = _STATE_INCOME_TAX.get(state_a, 0.05) * 100 |
||||
|
rate_b = _STATE_INCOME_TAX.get(state_b, 0.05) * 100 |
||||
|
return ( |
||||
|
f"{state_a} taxes income at ~{rate_a:.1f}%, {state_b} at ~{rate_b:.1f}%. " |
||||
|
"Factor this into your take-home pay comparison." |
||||
|
) |
||||
|
|
||||
|
|
||||
|
# --------------------------------------------------------------------------- |
||||
|
# Public tool functions |
||||
|
# --------------------------------------------------------------------------- |
||||
|
|
||||
|
def calculate_down_payment_power( |
||||
|
portfolio_value: float, |
||||
|
target_cities: Optional[list[str]] = None, |
||||
|
) -> dict: |
||||
|
""" |
||||
|
Calculates which housing markets a portfolio can fund at 20% down. |
||||
|
|
||||
|
Scenarios: |
||||
|
full = portfolio_value (liquidate everything) |
||||
|
conservative = portfolio_value * 0.80 (keep 20% buffer) |
||||
|
safe = portfolio_value * 0.60 (maintain diversification) |
||||
|
|
||||
|
Args: |
||||
|
portfolio_value: Total liquid portfolio value in USD. |
||||
|
target_cities: List of city names. Defaults to all 7 Austin ACTRIS markets. |
||||
|
|
||||
|
Returns: |
||||
|
Dict with portfolio_value, down_payment_scenarios, markets list, |
||||
|
top_recommendation, mortgage_assumptions, data_source. |
||||
|
""" |
||||
|
full = portfolio_value |
||||
|
conservative = round(portfolio_value * 0.80, 2) |
||||
|
safe = round(portfolio_value * 0.60, 2) |
||||
|
|
||||
|
if target_cities is None: |
||||
|
# Default: all 7 Austin ACTRIS markets (from _MOCK_SNAPSHOTS directly) |
||||
|
city_keys = _DEFAULT_AUSTIN_MARKETS |
||||
|
markets_data = [ |
||||
|
(key, _MOCK_SNAPSHOTS[key]) |
||||
|
for key in city_keys |
||||
|
if key in _MOCK_SNAPSHOTS |
||||
|
] |
||||
|
else: |
||||
|
markets_data = [] |
||||
|
for city in target_cities: |
||||
|
display, data = _resolve_city_data_sync(city) |
||||
|
markets_data.append((city, data)) |
||||
|
|
||||
|
markets_out = [] |
||||
|
affordable_markets = [] |
||||
|
|
||||
|
for city_ref, snap in markets_data: |
||||
|
price = snap.get("median_price") or snap.get("ListPrice") or 500_000 |
||||
|
rent = snap.get("MedianRentMonthly") or snap.get("median_rent") or 0 |
||||
|
area_name = snap.get("region") or snap.get("city") or city_ref |
||||
|
ds = snap.get("data_source", "estimate") |
||||
|
|
||||
|
required_down_20 = round(price * 0.20, 2) |
||||
|
can_full = full >= required_down_20 |
||||
|
can_conservative = conservative >= required_down_20 |
||||
|
can_safe = safe >= required_down_20 |
||||
|
|
||||
|
monthly_payment = _monthly_payment(price) |
||||
|
rent_vs_buy_diff = round(monthly_payment - rent, 0) if rent else None |
||||
|
rent_vs_buy_verdict = ( |
||||
|
f"Buying costs ${abs(rent_vs_buy_diff):,.0f}/mo " |
||||
|
f"{'more' if rent_vs_buy_diff > 0 else 'less'} than renting" |
||||
|
if rent_vs_buy_diff is not None else "Rental data unavailable" |
||||
|
) |
||||
|
|
||||
|
# Simple break-even: closing costs ~3% + transaction costs ~6% = ~9% of price |
||||
|
# Break even = (9% of price) / monthly_savings_vs_rent |
||||
|
monthly_savings = -(rent_vs_buy_diff or 1) |
||||
|
if monthly_savings > 0 and rent_vs_buy_diff is not None: |
||||
|
break_even_years = round((price * 0.09) / (monthly_savings * 12), 1) |
||||
|
elif rent_vs_buy_diff is not None and rent_vs_buy_diff <= 0: |
||||
|
break_even_years = 0.0 # Already cheaper to buy |
||||
|
else: |
||||
|
break_even_years = None |
||||
|
|
||||
|
entry = { |
||||
|
"area": area_name, |
||||
|
"city_ref": city_ref, |
||||
|
"median_price": price, |
||||
|
"required_down_20pct": required_down_20, |
||||
|
"can_afford_full": can_full, |
||||
|
"can_afford_conservative": can_conservative, |
||||
|
"can_afford_safe": can_safe, |
||||
|
"monthly_payment_estimate": int(monthly_payment), |
||||
|
"median_rent": rent, |
||||
|
"rent_vs_buy_monthly_diff": rent_vs_buy_diff, |
||||
|
"rent_vs_buy_verdict": rent_vs_buy_verdict, |
||||
|
"break_even_years": break_even_years, |
||||
|
"data_source": ds, |
||||
|
} |
||||
|
markets_out.append(entry) |
||||
|
|
||||
|
if can_full: |
||||
|
affordable_markets.append(area_name) |
||||
|
|
||||
|
# Build recommendation |
||||
|
max_home_price = round(portfolio_value / 0.20, 0) |
||||
|
affordable_conservatively = [ |
||||
|
m for m in markets_out if m["can_afford_conservative"] |
||||
|
] |
||||
|
|
||||
|
if not affordable_markets and not affordable_conservatively: |
||||
|
recommendation = ( |
||||
|
f"Your ${portfolio_value:,.0f} portfolio covers 20% down on homes up to " |
||||
|
f"${max_home_price:,.0f}. None of the target markets fall below this threshold. " |
||||
|
"Consider markets in Caldwell County ($237,491) or increase savings before buying." |
||||
|
) |
||||
|
else: |
||||
|
reachable_names = [m["area"] for m in affordable_conservatively] or affordable_markets |
||||
|
recommendation = ( |
||||
|
f"Your ${portfolio_value:,.0f} portfolio could fund a 20% down payment on " |
||||
|
f"homes up to ${max_home_price:,.0f}. " |
||||
|
+ ( |
||||
|
f"Reachable markets (conservatively): {', '.join(reachable_names[:3])}." |
||||
|
if reachable_names else |
||||
|
f"Reachable with full liquidation: {', '.join(affordable_markets[:3])}." |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
return { |
||||
|
"portfolio_value": portfolio_value, |
||||
|
"down_payment_scenarios": { |
||||
|
"full": full, |
||||
|
"conservative": conservative, |
||||
|
"safe": safe, |
||||
|
}, |
||||
|
"markets": markets_out, |
||||
|
"top_recommendation": recommendation, |
||||
|
"mortgage_assumptions": { |
||||
|
"rate": _MORTGAGE_RATE, |
||||
|
"term_years": 30, |
||||
|
"down_payment_pct": 20, |
||||
|
"disclaimer": "Rate is an estimate (6.95% 30yr fixed). Verify with lender.", |
||||
|
}, |
||||
|
"data_source": ( |
||||
|
"ACTRIS/Unlock MLS Jan 2026 (Austin areas) + Teleport API (other cities)" |
||||
|
), |
||||
|
} |
||||
|
|
||||
|
|
||||
|
async def calculate_job_offer_affordability( |
||||
|
offer_salary: float, |
||||
|
offer_city: str, |
||||
|
current_salary: float, |
||||
|
current_city: str, |
||||
|
) -> dict: |
||||
|
""" |
||||
|
Determines whether a job offer is a real raise in purchasing power terms. |
||||
|
|
||||
|
Works for any two cities worldwide: |
||||
|
- Austin TX areas: uses ACTRIS COL index (real data) |
||||
|
- All other cities: uses Teleport API live data (or fallback) |
||||
|
|
||||
|
Args: |
||||
|
offer_salary: Gross annual salary of the new offer, in USD. |
||||
|
offer_city: Destination city for the offer. |
||||
|
current_salary: Current gross annual salary, in USD. |
||||
|
current_city: Current city of residence. |
||||
|
|
||||
|
Returns: |
||||
|
Full comparison dict including adjusted purchasing power, verdict, |
||||
|
break-even salary, state tax note, and housing cost comparison. |
||||
|
""" |
||||
|
# Fetch city data (async for Teleport; sync-cached for Austin) |
||||
|
async def _get_data(city: str) -> tuple[float, dict]: |
||||
|
if _is_austin_area(city): |
||||
|
city_key = _normalize_city(city) |
||||
|
snap = _MOCK_SNAPSHOTS.get(city_key, {}) |
||||
|
col = _AUSTIN_COL_INDEX.get(city_key, 95.4) |
||||
|
return col, snap |
||||
|
data = await get_city_housing_data(city) |
||||
|
col = _col_index_for_city(city, data) |
||||
|
return col, data |
||||
|
|
||||
|
current_col, current_data = await _get_data(current_city) |
||||
|
offer_col, offer_data = await _get_data(offer_city) |
||||
|
|
||||
|
# Core purchasing power calculation |
||||
|
adjusted_offer = round(offer_salary * (current_col / offer_col), 2) |
||||
|
real_raise = round(adjusted_offer - current_salary, 2) |
||||
|
is_real_raise = real_raise > 0 |
||||
|
pct_change = round((real_raise / current_salary), 4) if current_salary > 0 else 0.0 |
||||
|
breakeven_salary = round(current_salary * (offer_col / current_col), 2) |
||||
|
|
||||
|
current_city_display = current_data.get("city") or current_city |
||||
|
offer_city_display = offer_data.get("city") or offer_city |
||||
|
|
||||
|
# Housing comparison |
||||
|
current_rent = ( |
||||
|
current_data.get("MedianRentMonthly") |
||||
|
or current_data.get("median_rent") |
||||
|
or 0 |
||||
|
) |
||||
|
offer_rent = ( |
||||
|
offer_data.get("MedianRentMonthly") |
||||
|
or offer_data.get("median_rent") |
||||
|
or 0 |
||||
|
) |
||||
|
rent_diff = round(offer_rent - current_rent, 0) if current_rent and offer_rent else None |
||||
|
|
||||
|
# Verdict |
||||
|
if is_real_raise: |
||||
|
verdict = ( |
||||
|
f"The {offer_city_display} offer is a REAL raise. " |
||||
|
f"${offer_salary:,.0f}/yr in {offer_city_display} is worth " |
||||
|
f"${adjusted_offer:,.0f} in {current_city_display} purchasing power terms — " |
||||
|
f"a genuine ${real_raise:,.0f} improvement ({pct_change * 100:.1f}%)." |
||||
|
) |
||||
|
else: |
||||
|
verdict = ( |
||||
|
f"Despite looking like a ${offer_salary - current_salary:,.0f} raise, " |
||||
|
f"the {offer_city_display} offer is worth ${abs(real_raise):,.0f} LESS " |
||||
|
f"than your {current_city_display} salary in real purchasing power. " |
||||
|
f"You would need ${breakeven_salary:,.0f} in {offer_city_display} " |
||||
|
f"to match your current lifestyle." |
||||
|
) |
||||
|
|
||||
|
tax_note = _state_tax_note(current_city, offer_city) |
||||
|
|
||||
|
return { |
||||
|
"current_salary": current_salary, |
||||
|
"current_city": current_city_display, |
||||
|
"current_col_index": current_col, |
||||
|
"offer_salary": offer_salary, |
||||
|
"offer_city": offer_city_display, |
||||
|
"offer_col_index": offer_col, |
||||
|
"adjusted_offer_in_current_city_terms": adjusted_offer, |
||||
|
"real_raise_amount": real_raise, |
||||
|
"is_real_raise": is_real_raise, |
||||
|
"percentage_real_change": pct_change, |
||||
|
"breakeven_salary_needed": breakeven_salary, |
||||
|
"verdict": verdict, |
||||
|
"tax_note": tax_note, |
||||
|
"housing_comparison": { |
||||
|
"current_city_median_rent": current_rent, |
||||
|
"offer_city_median_rent": offer_rent, |
||||
|
"monthly_rent_difference": rent_diff, |
||||
|
}, |
||||
|
"data_source": ( |
||||
|
f"ACTRIS MLS (Austin areas) + Teleport API ({offer_city_display})" |
||||
|
), |
||||
|
"offer_data_source": offer_data.get("data_source", ""), |
||||
|
"current_data_source": current_data.get("data_source", ""), |
||||
|
} |
||||
|
|
||||
|
|
||||
|
async def get_portfolio_real_estate_summary( |
||||
|
target_cities: Optional[list[str]] = None, |
||||
|
) -> dict: |
||||
|
""" |
||||
|
Master function: reads live Ghostfolio portfolio then runs down payment analysis. |
||||
|
|
||||
|
Steps: |
||||
|
1. Calls portfolio_analysis to get total portfolio value and top holdings |
||||
|
2. Passes total to calculate_down_payment_power() with target_cities |
||||
|
3. Returns combined result with quick plain-English answer |
||||
|
|
||||
|
Args: |
||||
|
target_cities: Optional list of cities to analyze. Defaults to all Austin markets. |
||||
|
|
||||
|
Returns: |
||||
|
Combined dict with portfolio_summary, down_payment_analysis, quick_answer. |
||||
|
""" |
||||
|
from agent.tools.portfolio import portfolio_analysis |
||||
|
|
||||
|
portfolio_result = await portfolio_analysis() |
||||
|
|
||||
|
portfolio_value = 0.0 |
||||
|
top_holdings: list[str] = [] |
||||
|
portfolio_error: Optional[str] = None |
||||
|
|
||||
|
if portfolio_result.get("success"): |
||||
|
summary = portfolio_result.get("result", {}).get("summary", {}) |
||||
|
portfolio_value = summary.get("total_current_value_usd", 0.0) |
||||
|
holdings = portfolio_result.get("result", {}).get("holdings", []) |
||||
|
top_holdings = [ |
||||
|
h.get("symbol") or h.get("name", "") |
||||
|
for h in holdings[:5] |
||||
|
if h.get("symbol") or h.get("name") |
||||
|
] |
||||
|
else: |
||||
|
portfolio_error = portfolio_result.get("error", {}).get("message", "Unknown error") |
||||
|
|
||||
|
down_payment_analysis = calculate_down_payment_power(portfolio_value, target_cities) |
||||
|
|
||||
|
max_home_price = round(portfolio_value / 0.20, 0) if portfolio_value > 0 else 0 |
||||
|
affordable = [ |
||||
|
m for m in down_payment_analysis["markets"] |
||||
|
if m["can_afford_conservative"] |
||||
|
] |
||||
|
|
||||
|
if portfolio_value == 0.0: |
||||
|
quick_answer = ( |
||||
|
"Could not retrieve portfolio value. " |
||||
|
+ (f"Error: {portfolio_error}" if portfolio_error else "") |
||||
|
) |
||||
|
elif affordable: |
||||
|
names = [m["area"] for m in affordable[:3]] |
||||
|
quick_answer = ( |
||||
|
f"Your ${portfolio_value:,.0f} portfolio could fund a 20% down payment " |
||||
|
f"on homes up to ${max_home_price:,.0f}. " |
||||
|
f"Reachable markets: {', '.join(names)}." |
||||
|
) |
||||
|
else: |
||||
|
quick_answer = ( |
||||
|
f"Your ${portfolio_value:,.0f} portfolio covers 20% down on homes up to " |
||||
|
f"${max_home_price:,.0f}. Check Caldwell County at $237,491 — " |
||||
|
"most affordable in the Austin area." |
||||
|
) |
||||
|
|
||||
|
return { |
||||
|
"portfolio_summary": { |
||||
|
"total_value": portfolio_value, |
||||
|
"top_holdings": top_holdings, |
||||
|
"portfolio_error": portfolio_error, |
||||
|
"allocation_note": ( |
||||
|
"Liquidating portfolio would trigger capital gains taxes — " |
||||
|
"consult a financial advisor before using investments for a down payment." |
||||
|
), |
||||
|
}, |
||||
|
"down_payment_analysis": down_payment_analysis, |
||||
|
"quick_answer": quick_answer, |
||||
|
} |
||||
Loading…
Reference in new issue