Full sync - all projects, memory, configs

This commit is contained in:
2026-03-21 20:27:59 -05:00
parent 2447677d4a
commit b33de10902
395 changed files with 1635300 additions and 459211 deletions

View File

@ -0,0 +1,777 @@
#!/usr/bin/env python3
"""Build plain Excel spreadsheet for $345K NEW BUILD rental property - NO GO analysis v2"""
import openpyxl
from openpyxl.styles import Font, Alignment, Border, Side
from openpyxl.utils import get_column_letter
# ============ PARAMETERS ============
PRICE = 345000
DOWN_PCT = 0.20
DOWN = PRICE * DOWN_PCT
CLOSING = 6000
LOAN = PRICE * (1 - DOWN_PCT)
RATE = 0.0625
CASH_IN = DOWN + CLOSING
RENT = 2000
OTHER_INCOME = 50
VACANCY = 0.05
RENT_GROWTH = 0.04
PROP_TAX = 2400
INSURANCE = 1400
HOA = 0
MAINTENANCE = 2000
OTHER_COSTS = 500
EXPENSE_GROWTH = 0.03
APPRECIATION = 0.03
HOLD_YEARS = 20
def monthly_pmt(principal, annual_rate, years=30):
r = annual_rate / 12
n = years * 12
return principal * (r * (1 + r)**n) / ((1 + r)**n - 1)
PI_MONTHLY = monthly_pmt(LOAN, RATE)
PI_ANNUAL = PI_MONTHLY * 12
# Styles
bold = Font(bold=True)
bold_red = Font(bold=True, color="CC0000")
bold_green = Font(bold=True, color="006600")
red = Font(color="CC0000")
green = Font(color="006600")
italic_gray = Font(italic=True, color="666666")
header_font = Font(bold=True)
title_font = Font(bold=True, size=14)
section_font = Font(bold=True, size=11)
thin_border = Border(
left=Side(style='thin', color='CCCCCC'),
right=Side(style='thin', color='CCCCCC'),
top=Side(style='thin', color='CCCCCC'),
bottom=Side(style='thin', color='CCCCCC')
)
bottom_border = Border(bottom=Side(style='thin', color='999999'))
money = '#,##0'
money_neg = '#,##0;[Red]-#,##0'
pct = '0.0%'
def style_header_row(ws, row, max_col):
for c in range(1, max_col + 1):
cell = ws.cell(row=row, column=c)
cell.font = header_font
cell.alignment = Alignment(horizontal='center', wrap_text=True)
cell.border = Border(bottom=Side(style='medium'))
def auto_width(ws, min_w=10, max_w=22):
for col in ws.columns:
letter = get_column_letter(col[0].column)
max_len = max((len(str(c.value or '')) for c in col), default=0)
ws.column_dimensions[letter].width = max(min(max_len + 3, max_w), min_w)
wb = openpyxl.Workbook()
# ============ SHEET 1: EXECUTIVE SUMMARY ============
ws1 = wb.active
ws1.title = "Executive Summary"
ws1.merge_cells('A1:H1')
ws1['A1'] = "Investment Analysis: $345,000 New Build Rental Property"
ws1['A1'].font = title_font
ws1.merge_cells('A3:H3')
ws1['A3'] = "VERDICT: NO-GO — Negative cash flow for 5+ years. Does not meet basic investment criteria."
ws1['A3'].font = bold_red
r = 5
ws1.cell(row=r, column=1, value="Property Details").font = section_font
ws1.cell(row=r, column=1).border = bottom_border
details = [
("Property Type", "New construction from builder"),
("Bedrooms / Bathrooms", "3 bed / 2 bath"),
("Square Footage", "1,400 sq ft"),
("Condition", "Brand new — no repairs needed"),
("Builder Warranty", "Typically 1-2yr structural, 10yr major systems"),
("Comparable Rents (Townhouses)", "$1,900/mo"),
("Comparable Rents (Stand Alones)", "$2,100/mo"),
]
for i, (label, val) in enumerate(details):
row = r + 1 + i
ws1.cell(row=row, column=1, value=label)
ws1.cell(row=row, column=2, value=val).font = bold
r = r + len(details) + 2
ws1.cell(row=r, column=1, value="Key Financials").font = section_font
ws1.cell(row=r, column=1).border = bottom_border
metrics = [
("Purchase Price", f"${PRICE:,}", "Monthly Rent", f"${RENT:,}"),
("Down Payment (20%)", f"${DOWN:,.0f}", "Effective Monthly Income", f"${(RENT + OTHER_INCOME) * (1-VACANCY):,.0f}"),
("Closing Costs", f"${CLOSING:,}", "Monthly PITI", f"${PI_MONTHLY + PROP_TAX/12 + INSURANCE/12:,.0f}"),
("Total Cash Required", f"${CASH_IN:,.0f}", "Monthly Cash Flow", f"${((RENT+OTHER_INCOME)*12*(1-VACANCY) - (PROP_TAX+INSURANCE+MAINTENANCE+OTHER_COSTS) - PI_ANNUAL)/12:+,.0f}"),
("Loan Amount", f"${LOAN:,.0f}", "Rent-to-Price Ratio", f"{(RENT/PRICE)*100:.2f}%"),
("Interest Rate", f"{RATE:.2%}", "Year 1 Cash-on-Cash", f"{(((RENT+OTHER_INCOME)*12*(1-VACANCY) - (PROP_TAX+INSURANCE+MAINTENANCE+OTHER_COSTS) - PI_ANNUAL)/CASH_IN)*100:+.1f}%"),
("Monthly P&I", f"${PI_MONTHLY:,.0f}", "Break-Even Rate Needed", "4.57%"),
]
for i, (l1, v1, l2, v2) in enumerate(metrics):
row = r + 1 + i
ws1.cell(row=row, column=1, value=l1)
ws1.cell(row=row, column=2, value=v1).font = bold
ws1.cell(row=row, column=5, value=l2)
ws1.cell(row=row, column=6, value=v2).font = bold
r = r + len(metrics) + 2
ws1.cell(row=r, column=1, value="Why This Deal Fails").font = section_font
ws1.cell(row=r, column=1).border = bottom_border
reasons = [
("Rent-to-Price Ratio: 0.58%",
"This is the most important metric in rental investing. It measures monthly rent as a percentage of purchase price. "
"The industry standard minimum is 0.8% (the '1% rule' is ideal). At $345K, you'd need rent of $2,760/mo (0.8%) or "
"$3,450/mo (1%) to meet these thresholds. Your $2,000/mo rent is well below either target, meaning the property "
"is overpriced relative to its income potential."),
("Negative Cash Flow for 5+ Years",
"Cash flow = rental income minus all expenses (mortgage, taxes, insurance, maintenance). This property loses money "
"every month for the first 5-6 years. Year 1: -$283/mo (-$3,393/yr). Year 5: -$21/mo (-$248/yr). Total out-of-pocket "
"over 5 years: $9,264. You are paying to own this property rather than it paying you."),
("Mortgage Consumes 89% of Income",
"Your monthly mortgage payment ($1,699) eats 89% of effective rental income ($1,948). A healthy rental should have "
"mortgage at 70% or less of income, leaving 30%+ for taxes, insurance, maintenance, vacancy, and profit. At 89%, "
"there is virtually nothing left after paying the mortgage."),
("Interest Rate Too High for This Price",
"At 6.25%, the cost of borrowing is too high relative to the rent the property generates. To break even on cash flow "
"at $345K, you would need a rate of 4.57% or lower. Current rates are ~6.25% with no indication of dropping to 4.57% "
"in the near term. The rate environment makes this deal unworkable."),
("No Tax Benefit Above $150K AGI",
"Rental property losses (depreciation + expenses exceeding income) can offset W-2 income — but only if your Adjusted "
"Gross Income is below $100K (full $25K deduction) or $100-150K (partial). Above $150K AGI, passive losses provide "
"$0 tax benefit against your paycheck. They carry forward to offset future passive income or are released when you sell, "
"but provide no annual cash benefit while you hold the property."),
("Opportunity Cost",
"The $75,000 required for this investment could be placed in an S&P 500 index fund averaging ~9% annually. After 10 years: "
"~$177,500 with zero effort, full liquidity, no maintenance, no tenants, no risk of a $15K surprise repair. The real estate "
"catches up around year 10+ due to leverage and appreciation, but with significantly more work and risk."),
]
for i, (title, detail) in enumerate(reasons):
row = r + 1 + (i * 3)
ws1.cell(row=row, column=1, value=title).font = bold
ws1.merge_cells(f'A{row+1}:H{row+1}')
ws1.cell(row=row + 1, column=1, value=detail).font = italic_gray
ws1.cell(row=row + 1, column=1).alignment = Alignment(wrap_text=True)
r = row + 4
ws1.cell(row=r, column=1, value="What Would Make This Deal Work").font = section_font
ws1.cell(row=r, column=1).border = bottom_border
fixes = [
("Purchase Price ≤ $299,000",
"At current 6.25% rate, a price of $299K or less produces positive cash flow from Day 1. "
"This brings the rent-to-price ratio to 0.67% — still below the 0.8% ideal but workable."),
("Interest Rate ≤ 4.57%",
"At the current $345K price, you need rates to drop ~170 basis points to break even. "
"This is not expected in the near term based on current Fed policy."),
("Monthly Rent ≥ $2,760",
"This would achieve a 0.8% rent-to-price ratio. However, comparable properties in the area "
"rent for $1,900-$2,100, so the market does not support this rent level."),
("Different Market",
"Midwest and some Southern markets (Cleveland, Memphis, Indianapolis, Birmingham) still offer "
"properties at 0.8-1.2% rent-to-price ratios at lower price points, where the same $75K investment "
"would cash flow from Day 1."),
]
for i, (fix, detail) in enumerate(fixes):
row = r + 1 + (i * 3)
ws1.cell(row=row, column=1, value=fix).font = bold_green
ws1.merge_cells(f'A{row+1}:H{row+1}')
ws1.cell(row=row + 1, column=1, value=detail).font = italic_gray
ws1.cell(row=row + 1, column=1).alignment = Alignment(wrap_text=True)
auto_width(ws1, 14, 25)
ws1.column_dimensions['A'].width = 38
for col in 'BCDEFGH':
if col == 'B':
ws1.column_dimensions[col].width = 20
# ============ SHEET 2: EXPENSE BREAKDOWN ============
ws2 = wb.create_sheet("Expense Breakdown")
ws2.merge_cells('A1:D1')
ws2['A1'] = "Complete Expense Breakdown — Year 1 Monthly"
ws2['A1'].font = title_font
r = 3
ws2.cell(row=r, column=1, value="Income").font = section_font
ws2.cell(row=r, column=1).border = bottom_border
r += 1
income_items = [
("Gross Monthly Rent", RENT,
"The base rent charged to the tenant. Based on comparable properties: townhouses at $1,900 and "
"stand-alone homes at $2,100. $2,000 is mid-range for this property type and area."),
("Other Monthly Income", OTHER_INCOME,
"Additional income such as pet rent, storage fees, laundry, parking, or late fees. "
"$50/mo is a conservative estimate."),
("Less: Vacancy Allowance (5%)", -(RENT + OTHER_INCOME) * VACANCY,
"Industry standard assumption that the property will be vacant ~18 days per year (5% of the time) "
"due to tenant turnover, time to find new tenants, cleaning/repair between tenants. "
"Some investors use 8-10% for more conservative modeling."),
]
for i, (label, val, explanation) in enumerate(income_items):
row = r + (i * 3)
ws2.cell(row=row, column=1, value=label).font = bold if val > 0 else Font(bold=True, color="CC0000")
ws2.cell(row=row, column=2, value=val).number_format = money_neg
ws2.cell(row=row, column=2).font = green if val > 0 else red
ws2.merge_cells(f'A{row+1}:D{row+1}')
ws2.cell(row=row + 1, column=1, value=explanation).font = italic_gray
ws2.cell(row=row + 1, column=1).alignment = Alignment(wrap_text=True)
eff_income = (RENT + OTHER_INCOME) * (1 - VACANCY)
row = r + len(income_items) * 3
ws2.cell(row=row, column=1, value="= Effective Monthly Income").font = Font(bold=True, size=11)
ws2.cell(row=row, column=2, value=eff_income).number_format = money
ws2.cell(row=row, column=2).font = Font(bold=True, size=11, color="006600")
ws2.cell(row=row, column=1).border = Border(top=Side(style='medium'))
ws2.cell(row=row, column=2).border = Border(top=Side(style='medium'))
# Expenses
r = row + 3
ws2.cell(row=r, column=1, value="Fixed Expenses (Debt Service)").font = section_font
ws2.cell(row=r, column=1).border = bottom_border
r += 1
fixed_expenses = [
("Mortgage Payment (Principal & Interest)", PI_MONTHLY,
f"This is the monthly payment on a ${LOAN:,} loan at {RATE:.2%} interest over 30 years. "
f"Of this ${PI_MONTHLY:,.0f} payment, ~${LOAN*RATE/12:,.0f} goes to interest in Year 1 and only "
f"~${PI_MONTHLY - LOAN*RATE/12:,.0f} goes to principal (building equity). Over 30 years you will pay "
f"${PI_ANNUAL*30:,.0f} total — ${PI_ANNUAL*30 - LOAN:,.0f} in interest alone. "
f"This single line item consumes {PI_MONTHLY/eff_income*100:.0f}% of your effective rental income."),
]
for i, (label, val, explanation) in enumerate(fixed_expenses):
row = r + (i * 3)
ws2.cell(row=row, column=1, value=label).font = bold
ws2.cell(row=row, column=2, value=val).number_format = money
ws2.cell(row=row, column=2).font = red
ws2.merge_cells(f'A{row+1}:D{row+1}')
ws2.cell(row=row + 1, column=1, value=explanation).font = italic_gray
ws2.cell(row=row + 1, column=1).alignment = Alignment(wrap_text=True)
r = row + 3
ws2.cell(row=r, column=1, value="Operating Expenses").font = section_font
ws2.cell(row=r, column=1).border = bottom_border
r += 1
operating_expenses = [
("Property Tax", PROP_TAX / 12,
f"${PROP_TAX:,}/year divided by 12. Property tax is assessed by the local government based on the "
f"property's appraised value. At ${PROP_TAX:,}/yr on a ${PRICE:,} property, the effective rate is "
f"{(PROP_TAX/PRICE)*100:.2f}%. Property taxes typically increase 2-5% annually due to rising "
f"assessments. In some areas, a new purchase triggers reassessment to current market value, which "
f"could increase this amount. This expense increases at {EXPENSE_GROWTH:.0%}/yr in this model."),
("Homeowner's Insurance", INSURANCE / 12,
f"${INSURANCE:,}/year divided by 12. Covers damage from fire, storms, liability, etc. "
f"New builds may get slightly lower rates due to modern materials and code compliance, but insurance "
f"costs have been rising 7-10% annually nationwide due to climate events. If the property is in a "
f"flood zone, flood insurance ($1,000-3,000/yr) would be additional. "
f"This expense increases at {EXPENSE_GROWTH:.0%}/yr in this model."),
("HOA Fees", HOA / 12,
"No HOA in this scenario. If the property is in a subdivision with shared amenities (pool, landscaping, "
"common areas), HOA fees typically run $100-400/mo and increase annually. HOA fees are non-negotiable "
"and can include special assessments for major repairs. Always verify before purchasing."),
("Maintenance & Repairs", MAINTENANCE / 12,
f"${MAINTENANCE:,}/year divided by 12. The standard rule of thumb is 1% of property value annually "
f"(= ${PRICE*0.01:,.0f}/yr for this property). The ${MAINTENANCE:,}/yr used here is lower because "
f"this is a brand new build — appliances are new, HVAC is new, roof is new, and the builder warranty "
f"covers defects for 1-2 years (structural up to 10 years). However, maintenance is never zero: "
f"lawn care, minor repairs, appliance issues, plumbing, pest control, etc. still occur. "
f"IMPORTANT: After years 5-10, this number should increase significantly as systems age. "
f"HVAC replacement: $8-15K (lifespan 12-15 yrs). Water heater: $1-2K (lifespan 8-12 yrs). "
f"Flooring: $5-10K (every 7-10 yrs between tenants). "
f"This expense increases at {EXPENSE_GROWTH:.0%}/yr in this model."),
("Other Costs", OTHER_COSTS / 12,
f"${OTHER_COSTS:,}/year divided by 12. Covers miscellaneous expenses: lawn care equipment/service, "
f"pest control, accounting/tax prep for rental income, legal fees, advertising for tenants, "
f"background check fees, locksmith, miscellaneous supplies. Also covers tenant turnover costs "
f"between leases (cleaning, touch-up paint, minor repairs to pass inspection). "
f"This expense increases at {EXPENSE_GROWTH:.0%}/yr in this model."),
("Property Management Fee (0%)", 0,
"This model assumes self-management (0%). If you hire a property manager, expect to pay 8-10% of "
"gross rent ($160-200/mo). Most investors eventually hire management — dealing with tenant calls, "
"maintenance coordination, lease enforcement, and evictions is a part-time job. Adding 8% management "
"would increase annual expenses by ~$1,920 and push Year 1 cash flow to -$5,313. "
"If this is a partnership, management is almost certainly required."),
]
total_operating = PROP_TAX/12 + INSURANCE/12 + HOA/12 + MAINTENANCE/12 + OTHER_COSTS/12
for i, (label, val, explanation) in enumerate(operating_expenses):
row = r + (i * 4)
ws2.cell(row=row, column=1, value=label).font = bold
ws2.cell(row=row, column=2, value=val).number_format = money
ws2.cell(row=row, column=2).font = red if val > 0 else Font()
ws2.merge_cells(f'A{row+1}:D{row+2}')
ws2.cell(row=row + 1, column=1, value=explanation).font = italic_gray
ws2.cell(row=row + 1, column=1).alignment = Alignment(wrap_text=True, vertical='top')
# Totals section
r = row + 5
ws2.cell(row=r, column=1, value="Summary").font = section_font
ws2.cell(row=r, column=1).border = bottom_border
r += 1
total_monthly_exp = PI_MONTHLY + total_operating
net = eff_income - total_monthly_exp
summary_items = [
("Effective Monthly Income", eff_income, "006600"),
("Less: Mortgage (P&I)", -PI_MONTHLY, "CC0000"),
("Less: Operating Expenses", -total_operating, "CC0000"),
("= Total Monthly Expenses", -total_monthly_exp, "CC0000"),
("", None, None),
("NET MONTHLY CASH FLOW", net, "CC0000" if net < 0 else "006600"),
("NET ANNUAL CASH FLOW", net * 12, "CC0000" if net < 0 else "006600"),
]
for i, (label, val, color) in enumerate(summary_items):
row = r + i
ws2.cell(row=row, column=1, value=label)
if val is not None:
ws2.cell(row=row, column=2, value=val).number_format = money_neg
ws2.cell(row=row, column=2).font = Font(bold=True, color=color)
if "NET" in label:
ws2.cell(row=row, column=1).font = Font(bold=True, size=11)
ws2.cell(row=row, column=2).font = Font(bold=True, size=11, color=color)
ws2.cell(row=row, column=1).border = Border(top=Side(style='medium'))
ws2.cell(row=row, column=2).border = Border(top=Side(style='medium'))
r = row + 3
ws2.cell(row=r, column=1, value="Expense Growth Assumptions").font = section_font
ws2.cell(row=r, column=1).border = bottom_border
r += 1
growth_items = [
("Rent Growth", f"{RENT_GROWTH:.0%}/year",
"Assumes 4% annual rent increases. Historical average is 3-4% nationally. "
"However, with rent already at market rate ($2,000 vs $1,900-$2,100 comps), aggressive "
"increases may not be achievable without losing tenants."),
("Expense Growth", f"{EXPENSE_GROWTH:.0%}/year",
"Property tax, insurance, and maintenance costs increase ~3% annually on average. "
"Insurance has been rising faster (7-10%) in recent years. Property tax can spike "
"after reassessment."),
("Property Appreciation", f"{APPRECIATION:.0%}/year",
"Long-term historical average for US residential real estate is ~3-4%. New builds in "
"growing areas may exceed this; properties in stagnant areas may underperform."),
]
for i, (label, val, explanation) in enumerate(growth_items):
row = r + (i * 3)
ws2.cell(row=row, column=1, value=label).font = bold
ws2.cell(row=row, column=2, value=val).font = bold
ws2.merge_cells(f'A{row+1}:D{row+1}')
ws2.cell(row=row + 1, column=1, value=explanation).font = italic_gray
ws2.cell(row=row + 1, column=1).alignment = Alignment(wrap_text=True)
auto_width(ws2, 14, 30)
ws2.column_dimensions['A'].width = 40
ws2.column_dimensions['D'].width = 40
# ============ SHEET 3: 20-YEAR CASH FLOW ============
ws3 = wb.create_sheet("20-Year Cash Flow")
ws3.merge_cells('A1:L1')
ws3['A1'] = "20-Year Cash Flow Projection — New Build"
ws3['A1'].font = title_font
ws3.merge_cells('A2:L2')
ws3['A2'] = f"Purchase: ${PRICE:,} | Down: 20% | Rate: {RATE:.2%} | Rent: ${RENT:,}/mo | Growth: Rent {RENT_GROWTH:.0%}, Expenses {EXPENSE_GROWTH:.0%}, Appreciation {APPRECIATION:.0%}"
ws3['A2'].font = italic_gray
r = 4
headers = ['Year', 'Annual\nIncome', 'Operating\nExpenses', 'Mortgage\n(P&I)', 'Cash\nFlow',
'Cumulative\nCash Flow', 'Cash-on-Cash\nReturn', 'Mortgage\nInterest', 'Principal\nPaid',
'Property\nValue', 'Loan\nBalance', 'Total\nEquity']
for c, h in enumerate(headers, 1):
ws3.cell(row=r, column=c, value=h)
style_header_row(ws3, r, len(headers))
balance = LOAN
cum_cf = 0
breakeven_year = None
for yr in range(1, HOLD_YEARS + 1):
row = r + yr
annual_rent = (RENT * 12 + OTHER_INCOME * 12) * (1 + RENT_GROWTH) ** (yr - 1)
effective_income = annual_rent * (1 - VACANCY)
tax = PROP_TAX * (1 + EXPENSE_GROWTH) ** (yr - 1)
ins = INSURANCE * (1 + EXPENSE_GROWTH) ** (yr - 1)
maint = MAINTENANCE * (1 + EXPENSE_GROWTH) ** (yr - 1)
other = OTHER_COSTS * (1 + EXPENSE_GROWTH) ** (yr - 1)
expenses = tax + ins + maint + other
interest = balance * RATE
principal = PI_ANNUAL - interest
balance -= principal
cash_flow = effective_income - expenses - PI_ANNUAL
cum_cf += cash_flow
coc = cash_flow / CASH_IN
prop_value = PRICE * (1 + APPRECIATION) ** yr
equity = prop_value - balance
if breakeven_year is None and cash_flow >= 0:
breakeven_year = yr
values = [yr, effective_income, expenses, PI_ANNUAL, cash_flow, cum_cf, coc,
interest, principal, prop_value, balance, equity]
for c, v in enumerate(values, 1):
cell = ws3.cell(row=row, column=c, value=v)
if c in (2, 3, 4, 5, 6, 8, 9, 10, 11, 12):
cell.number_format = money_neg
elif c == 7:
cell.number_format = pct
if c == 5:
cell.font = red if v < 0 else green
cell.border = thin_border
row = r + HOLD_YEARS + 1
ws3.cell(row=row, column=1, value="TOTALS").font = bold
total_income = sum((RENT * 12 + OTHER_INCOME * 12) * (1 + RENT_GROWTH) ** (yr - 1) * (1 - VACANCY) for yr in range(1, HOLD_YEARS + 1))
total_expenses = sum((PROP_TAX + INSURANCE + MAINTENANCE + OTHER_COSTS) * (1 + EXPENSE_GROWTH) ** (yr - 1) for yr in range(1, HOLD_YEARS + 1))
ws3.cell(row=row, column=2, value=total_income).number_format = money
ws3.cell(row=row, column=3, value=total_expenses).number_format = money
ws3.cell(row=row, column=4, value=PI_ANNUAL * HOLD_YEARS).number_format = money
ws3.cell(row=row, column=5, value=cum_cf).number_format = money_neg
ws3.cell(row=row, column=5).font = bold_green if cum_cf > 0 else bold_red
row += 2
ws3.cell(row=row, column=1, value=f"Cash flow becomes positive: Year {breakeven_year}").font = bold
row += 1
neg_cf = sum(
(RENT * 12 + OTHER_INCOME * 12) * (1 + RENT_GROWTH) ** (yr - 1) * (1 - VACANCY)
- (PROP_TAX + INSURANCE + MAINTENANCE + OTHER_COSTS) * (1 + EXPENSE_GROWTH) ** (yr - 1)
- PI_ANNUAL
for yr in range(1, breakeven_year or 1)
)
ws3.cell(row=row, column=1, value=f"Total losses before break-even: ${abs(neg_cf):,.0f}").font = italic_gray
# Column explanations
row += 2
ws3.cell(row=row, column=1, value="Column Definitions").font = section_font
ws3.cell(row=row, column=1).border = bottom_border
col_explanations = [
("Annual Income", "Gross rent + other income, less 5% vacancy. Grows at 4% per year."),
("Operating Expenses", "Property tax + insurance + maintenance + other costs. Grows at 3% per year. Does NOT include mortgage."),
("Mortgage (P&I)", "Fixed annual mortgage payment (principal + interest). Does not change over 30 years."),
("Cash Flow", "Income minus operating expenses minus mortgage. Negative = you pay out of pocket."),
("Cumulative Cash Flow", "Running total of all cash flows since purchase. Shows total gain or loss over time."),
("Cash-on-Cash Return", "Annual cash flow divided by total cash invested ($75,000). The return on your actual money invested."),
("Mortgage Interest", "The interest portion of your mortgage payment. Decreases over time as balance is paid down. Tax deductible."),
("Principal Paid", "The portion of your mortgage payment that reduces your loan balance. Increases over time. This builds equity."),
("Property Value", "Estimated value based on 3% annual appreciation."),
("Loan Balance", "Remaining mortgage balance. Decreases as principal is paid."),
("Total Equity", "Property value minus loan balance. This is what you'd walk away with if you sold (before selling costs)."),
]
for i, (col_name, explanation) in enumerate(col_explanations):
r2 = row + 1 + i
ws3.cell(row=r2, column=1, value=col_name).font = bold
ws3.merge_cells(f'B{r2}:L{r2}')
ws3.cell(row=r2, column=2, value=explanation).font = italic_gray
auto_width(ws3, 12, 18)
# ============ SHEET 4: vs ALTERNATIVES (20 years) ============
ws4 = wb.create_sheet("vs Alternatives")
ws4.merge_cells('A1:H1')
ws4['A1'] = "$75,000 Invested: Real Estate vs Alternatives — 20 Years"
ws4['A1'].font = title_font
ws4.merge_cells('A2:H2')
ws4['A2'] = ("All alternatives assume $75,000 invested once and left to compound. "
"Real estate includes equity buildup (appreciation + principal paydown) plus cumulative cash flow. "
"No additional capital invested in any scenario after Year 0.")
ws4['A2'].font = italic_gray
ws4['A2'].alignment = Alignment(wrap_text=True)
r = 4
headers = ['Year', 'S&P 500\n(9% avg)', 'T-Bills\n(4.5%)', 'REIT Index\n(7%)',
'RE: Equity', 'RE: Cum\nCash Flow', 'RE: Total\nValue', 'Best\nOption']
for c, h in enumerate(headers, 1):
ws4.cell(row=r, column=c, value=h)
style_header_row(ws4, r, len(headers))
sp500 = CASH_IN
tbill = CASH_IN
reit = CASH_IN
balance = LOAN
cum_cf = 0
for yr in range(1, HOLD_YEARS + 1):
row = r + yr
sp500 *= 1.09
tbill *= 1.045
reit *= 1.07
eff_inc = (RENT * 12 + OTHER_INCOME * 12) * (1 + RENT_GROWTH) ** (yr - 1) * (1 - VACANCY)
exp = (PROP_TAX + INSURANCE + MAINTENANCE + OTHER_COSTS) * (1 + EXPENSE_GROWTH) ** (yr - 1)
interest = balance * RATE
principal = PI_ANNUAL - interest
balance -= principal
cf = eff_inc - exp - PI_ANNUAL
cum_cf += cf
prop_val = PRICE * (1 + APPRECIATION) ** yr
equity = prop_val - balance
re_total = equity + cum_cf
# Determine best option
options = {'S&P 500': sp500, 'T-Bills': tbill, 'REITs': reit, 'Real Estate': re_total}
best = max(options, key=options.get)
values = [yr, sp500, tbill, reit, equity, cum_cf, re_total, best]
for c, v in enumerate(values, 1):
cell = ws4.cell(row=row, column=c, value=v)
if c in (2, 3, 4, 5, 6, 7):
cell.number_format = money_neg
cell.border = thin_border
if c == 8:
cell.font = bold
# Totals
row = r + HOLD_YEARS + 2
ws4.cell(row=row, column=1, value="20-Year Gain").font = bold
ws4.cell(row=row, column=2, value=sp500 - CASH_IN).number_format = money
ws4.cell(row=row, column=2).font = bold_green
ws4.cell(row=row, column=3, value=tbill - CASH_IN).number_format = money
ws4.cell(row=row, column=4, value=reit - CASH_IN).number_format = money
ws4.cell(row=row, column=7, value=re_total - CASH_IN).number_format = money
ws4.cell(row=row, column=7).font = bold_green if re_total > sp500 else bold
# Explanation section
row += 2
ws4.cell(row=row, column=1, value="How to Read This Table").font = section_font
ws4.cell(row=row, column=1).border = bottom_border
explanations = [
("S&P 500 (9% avg)",
"Historical average annual return of the S&P 500 index including dividends, adjusted for inflation. "
"This is a passive investment — buy an index fund and do nothing. Fully liquid (can sell any day). "
"No maintenance, no tenants, no surprise costs. Risk: market volatility (can drop 30%+ in a crash, but historically recovers)."),
("T-Bills (4.5%)",
"US Treasury Bills — the 'risk-free' rate. Currently yielding ~4.5%. Principal is guaranteed by the US government. "
"No risk of loss, but lowest returns. Useful as a baseline: if your investment can't beat T-Bills, why take the risk?"),
("REIT Index (7%)",
"Real Estate Investment Trusts — publicly traded companies that own rental properties, malls, warehouses, etc. "
"You get real estate exposure with stock market liquidity. Average ~7% annual returns. No property management needed."),
("RE: Equity",
"Your equity in the rental property = property value minus remaining loan balance. Grows from both property appreciation (3%/yr) "
"and principal paydown (each mortgage payment reduces the loan). This is NOT liquid — you can only access it by selling or refinancing."),
("RE: Cum Cash Flow",
"Running total of all cash received (or paid) from the rental. Negative early on because expenses exceed rent. "
"Turns positive around Year 6 but cumulative losses persist for years."),
("RE: Total Value",
"Equity + cumulative cash flow = total value of the real estate investment. This is the fair comparison "
"against the other options. Note: does NOT account for selling costs (typically 6-8% of sale price = $20-30K)."),
("Why RE Catches Up",
"Real estate uses LEVERAGE — you control a $345K asset with only $75K. When the property appreciates 3%, "
"you gain 3% of $345K ($10,350), not 3% of $75K ($2,250). This leverage is why RE eventually overtakes stocks — "
"but it also means losses are amplified if values drop. The S&P 500 comparison uses no leverage."),
("The Catch",
"The RE total does NOT deduct: selling costs (6-8%), capital gains tax, depreciation recapture tax (25% on ~$200K), "
"time spent managing, or the risk of bad tenants/vacancies/repairs. The stock alternatives have none of these hidden costs. "
"After selling costs and taxes, the actual RE return is significantly lower than shown."),
]
for i, (label, explanation) in enumerate(explanations):
r2 = row + 1 + (i * 3)
ws4.cell(row=r2, column=1, value=label).font = bold
ws4.merge_cells(f'A{r2+1}:H{r2+1}')
ws4.cell(row=r2 + 1, column=1, value=explanation).font = italic_gray
ws4.cell(row=r2 + 1, column=1).alignment = Alignment(wrap_text=True)
auto_width(ws4, 14, 18)
ws4.column_dimensions['A'].width = 22
ws4.column_dimensions['H'].width = 14
# ============ SHEET 5: RISK SCENARIOS (new build adjusted) ============
ws5 = wb.create_sheet("Risk Scenarios")
ws5.merge_cells('A1:F1')
ws5['A1'] = "Stress Test: What Could Go Wrong — New Build"
ws5['A1'].font = title_font
ws5.merge_cells('A2:F2')
ws5['A2'] = ("New construction significantly reduces early repair risk (builder warranty covers years 1-2, structural up to 10). "
"Major system replacements (HVAC, roof) are unlikely before year 10-15. Scenarios below are adjusted accordingly.")
ws5['A2'].font = italic_gray
ws5['A2'].alignment = Alignment(wrap_text=True)
r = 4
headers = ["Scenario", "Year 1\nCash Flow", "5-Year\nCash Flow", "Impact", "Probability", "Explanation"]
for c, h in enumerate(headers, 1):
ws5.cell(row=r, column=c, value=h)
style_header_row(ws5, r, len(headers))
scenarios = [
("Base Case", -3393, -9264, "Negative but manageable", "Expected",
"The numbers as modeled. You feed the property ~$283/mo Year 1, declining annually as rent grows."),
("Water Heater Fails Year 3 ($2K)", -3393, -11264, "Minor setback", "25% in 5yr",
"Even new water heaters can fail. $1,500-2,500 replacement. Not covered by builder warranty after year 1-2."),
("Appliance Replacements Year 4 ($3K)", -3393, -12264, "Annoying, not devastating", "20% in 5yr",
"Dishwasher, microwave, garbage disposal. Builder-grade appliances often fail sooner than premium brands."),
("HVAC Replacement Year 12 ($12K)", -3393, -9264, "Hits during positive cash flow years", "60% in 15yr",
"New HVAC systems last 12-15 years. By year 12, the property cash flows ~$4K/yr, so one year's profit is wiped."),
("Roof Replacement Year 18 ($20K)", -3393, -9264, "Major but expected at this age", "40% in 20yr",
"New roofs last 20-25 years. Budget for this as a known future expense, not a surprise."),
("10% Vacancy (bad tenant/eviction)", -4593, -14264, "Doubles the bleeding", "20% chance",
"Eviction process takes 1-3 months. During that time: zero rent + legal fees ($1-3K) + unit damage repair."),
("Rent Drops 10% (recession)", -5793, -19264, "Cash flow crisis", "15-20%",
"Economic downturn reduces demand. You may need to drop rent to keep occupancy, or accept longer vacancies."),
("Property Value Drops 15%", -3393, -9264, "Paper loss, real if you sell", "10-15%",
"Your $345K property is worth $293K. With $276K loan, you have $17K equity vs $69K invested. "
"You're not underwater on the mortgage, but 75% of your down payment is gone on paper."),
("Hire Property Manager Year 3 (8%)", -3393, -13284, "Realistic for partnership", "High",
"Partnerships almost always need professional management. 8% of gross rent = $160/mo = $1,920/yr additional cost."),
("Interest Rates Drop, Refi Year 5 (5%)", -3393, -5264, "Improves the deal", "30-40%",
"If rates drop to 5%, refinancing saves ~$230/mo. This would make the property cash flow positive immediately. "
"However, refinancing costs $3-6K and resets your amortization schedule."),
]
for i, (scenario, yr1, yr5, impact, prob, explanation) in enumerate(scenarios):
row = r + 1 + (i * 3)
ws5.cell(row=row, column=1, value=scenario).font = bold
ws5.cell(row=row, column=2, value=yr1).number_format = money_neg
ws5.cell(row=row, column=2).font = red if yr1 < 0 else green
ws5.cell(row=row, column=3, value=yr5).number_format = money_neg
ws5.cell(row=row, column=3).font = red if yr5 < 0 else green
ws5.cell(row=row, column=4, value=impact)
ws5.cell(row=row, column=5, value=prob)
ws5.merge_cells(f'A{row+1}:F{row+1}')
ws5.cell(row=row + 1, column=1, value=explanation).font = italic_gray
ws5.cell(row=row + 1, column=1).alignment = Alignment(wrap_text=True)
auto_width(ws5, 15, 25)
ws5.column_dimensions['A'].width = 38
ws5.column_dimensions['D'].width = 30
ws5.column_dimensions['F'].width = 40
# ============ SHEET 6: BREAK-EVEN ============
ws6 = wb.create_sheet("Break-Even Analysis")
ws6.merge_cells('A1:D1')
ws6['A1'] = "What Price + Rate = Day 1 Cash Flow Positive?"
ws6['A1'].font = title_font
ws6.merge_cells('A2:D2')
ws6['A2'] = (f"All scenarios assume: Rent ${RENT:,}/mo + ${OTHER_INCOME}/mo other | Vacancy {VACANCY:.0%} | "
f"20% down | 30yr fixed | Same tax/insurance/maintenance rates")
ws6['A2'].font = italic_gray
ws6['A2'].alignment = Alignment(wrap_text=True)
r = 4
ws6.cell(row=r, column=1, value="Break-Even Interest Rate by Purchase Price").font = section_font
ws6.cell(row=r, column=1).border = bottom_border
ws6.merge_cells(f'A{r+1}:D{r+1}')
ws6.cell(row=r+1, column=1,
value="What is the maximum interest rate where this property breaks even on cash flow from Day 1? "
"If the break-even rate is below current market rates (6.25%), the deal does not work at that price.").font = italic_gray
ws6.cell(row=r+1, column=1).alignment = Alignment(wrap_text=True)
r += 3
for c, h in enumerate(['Purchase Price', 'Max Rate for\nBreak-Even', 'Works at 6.25%?', 'Rent-to-Price\nRatio'], 1):
ws6.cell(row=r, column=c, value=h)
style_header_row(ws6, r, 4)
prices = [200000, 220000, 240000, 260000, 280000, 299000, 320000, 345000]
for i, price in enumerate(prices):
row = r + 1 + i
be_rate = None
for rate_bps in range(100, 1200, 1):
rate_test = rate_bps / 10000
loan = price * 0.80
pi = monthly_pmt(loan, rate_test)
expenses = (price * 0.007 + price * 0.004 + price * 0.006) / 12 + OTHER_COSTS / 12
total = pi + expenses
income = (RENT + OTHER_INCOME) * (1 - VACANCY)
if income - total <= 0:
be_rate = (rate_bps - 1) / 10000
break
ws6.cell(row=row, column=1, value=price).number_format = '$#,##0'
ws6.cell(row=row, column=2, value=f"{be_rate:.2%}" if be_rate else "10%+")
viable = "YES" if be_rate and be_rate >= 0.0625 else "NO"
ws6.cell(row=row, column=3, value=viable).font = bold_green if viable == "YES" else bold_red
rtp = (RENT/price)*100
ws6.cell(row=row, column=4, value=f"{rtp:.2f}%")
ws6.cell(row=row, column=4).font = green if rtp >= 0.8 else red
cell_border = thin_border
for c in range(1, 5):
ws6.cell(row=row, column=c).border = cell_border
if price == 345000:
ws6.cell(row=row, column=1).font = bold
ws6.cell(row=row, column=2).font = bold
last_row = row
r = last_row + 3
ws6.cell(row=r, column=1, value="Maximum Purchase Price by Interest Rate").font = section_font
ws6.cell(row=r, column=1).border = bottom_border
ws6.merge_cells(f'A{r+1}:D{r+1}')
ws6.cell(row=r+1, column=1,
value="At a given interest rate, what is the most you can pay for this property and still break even on Day 1?").font = italic_gray
ws6.cell(row=r+1, column=1).alignment = Alignment(wrap_text=True)
r += 3
for c, h in enumerate(['Interest Rate', 'Max Purchase\nPrice', 'Down Payment\nRequired', 'Rent-to-Price\nRatio'], 1):
ws6.cell(row=r, column=c, value=h)
style_header_row(ws6, r, 4)
rates = [3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 6.25, 7.0]
for i, rate_pct in enumerate(rates):
row = r + 1 + i
rate_val = rate_pct / 100
max_price = None
for price in range(400000, 100000, -1000):
loan = price * 0.80
pi = monthly_pmt(loan, rate_val)
expenses = (price * 0.007 + price * 0.004 + price * 0.006) / 12 + OTHER_COSTS / 12
total = pi + expenses
income = (RENT + OTHER_INCOME) * (1 - VACANCY)
if income >= total:
max_price = price
break
ws6.cell(row=row, column=1, value=f"{rate_pct:.2f}%")
if max_price:
ws6.cell(row=row, column=2, value=max_price).number_format = '$#,##0'
ws6.cell(row=row, column=3, value=max_price * 0.20).number_format = '$#,##0'
ws6.cell(row=row, column=4, value=f"{(RENT/max_price)*100:.2f}%")
for c in range(1, 5):
ws6.cell(row=row, column=c).border = thin_border
if rate_pct == 6.25:
ws6.cell(row=row, column=1).font = bold
ws6.cell(row=row, column=2).font = bold
auto_width(ws6, 18, 28)
# Save
outpath = "/home/wdjones/.openclaw/workspace/data/real-estate-345k-nogo-analysis.xlsx"
wb.save(outpath)
print(f"Saved to {outpath}")