Full sync - all projects, memory, configs
This commit is contained in:
282
tools/build-re-excel.py
Normal file
282
tools/build-re-excel.py
Normal file
@ -0,0 +1,282 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build Excel spreadsheet from ARI's real estate cost seg model"""
|
||||
import sys
|
||||
sys.path.insert(0, '/home/wdjones/.openclaw/workspace-ari/data/real-estate-model')
|
||||
from detailed_calculations import *
|
||||
import openpyxl
|
||||
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side, numbers
|
||||
from openpyxl.utils import get_column_letter
|
||||
|
||||
# Check if openpyxl available
|
||||
try:
|
||||
import openpyxl
|
||||
except ImportError:
|
||||
import subprocess
|
||||
subprocess.run([sys.executable, '-m', 'pip', 'install', 'openpyxl'], check=True)
|
||||
import openpyxl
|
||||
|
||||
results = run_portfolio_model()
|
||||
|
||||
wb = openpyxl.Workbook()
|
||||
|
||||
# Styles
|
||||
header_font = Font(bold=True, color="FFFFFF", size=11)
|
||||
header_fill = PatternFill(start_color="1a1a2e", end_color="1a1a2e", fill_type="solid")
|
||||
red_font = Font(color="FF4444", bold=True)
|
||||
green_font = Font(color="44FF44", bold=True)
|
||||
money_fmt = '#,##0'
|
||||
money_neg_fmt = '#,##0;[Red]-#,##0'
|
||||
pct_fmt = '0.0%'
|
||||
thin_border = Border(
|
||||
left=Side(style='thin'), right=Side(style='thin'),
|
||||
top=Side(style='thin'), bottom=Side(style='thin')
|
||||
)
|
||||
section_fill = PatternFill(start_color="2d2d44", end_color="2d2d44", fill_type="solid")
|
||||
section_font = Font(bold=True, color="00BFFF", size=11)
|
||||
warn_fill = PatternFill(start_color="4a1a1a", end_color="4a1a1a", fill_type="solid")
|
||||
warn_font = Font(bold=True, color="FF6666", size=12)
|
||||
|
||||
def style_header(ws, row, max_col):
|
||||
for col in range(1, max_col + 1):
|
||||
cell = ws.cell(row=row, column=col)
|
||||
cell.font = header_font
|
||||
cell.fill = header_fill
|
||||
cell.alignment = Alignment(horizontal='center', wrap_text=True)
|
||||
cell.border = thin_border
|
||||
|
||||
def style_row(ws, row, max_col):
|
||||
for col in range(1, max_col + 1):
|
||||
cell = ws.cell(row=row, column=col)
|
||||
cell.border = thin_border
|
||||
cell.alignment = Alignment(horizontal='right')
|
||||
if col == 1:
|
||||
cell.alignment = Alignment(horizontal='left')
|
||||
|
||||
def auto_width(ws):
|
||||
for col in ws.columns:
|
||||
max_len = 0
|
||||
col_letter = get_column_letter(col[0].column)
|
||||
for cell in col:
|
||||
if cell.value:
|
||||
max_len = max(max_len, len(str(cell.value)))
|
||||
ws.column_dimensions[col_letter].width = min(max_len + 4, 22)
|
||||
|
||||
# ============ SHEET 1: Portfolio Overview ============
|
||||
ws1 = wb.active
|
||||
ws1.title = "Portfolio Overview"
|
||||
ws1.sheet_properties.tabColor = "1a1a2e"
|
||||
|
||||
# Title
|
||||
ws1.merge_cells('A1:K1')
|
||||
ws1['A1'] = "REAL ESTATE COST SEGREGATION MODEL — 5-YEAR PORTFOLIO"
|
||||
ws1['A1'].font = Font(bold=True, color="00BFFF", size=14)
|
||||
|
||||
# Warning banner
|
||||
ws1.merge_cells('A3:K3')
|
||||
ws1['A3'] = "⚠️ VERDICT: NO-GO — Negative cash flow all 5 years. $0 tax benefit at >$150K AGI. $337K passive losses stranded."
|
||||
ws1['A3'].font = warn_font
|
||||
ws1['A3'].fill = warn_fill
|
||||
|
||||
# Headers
|
||||
row = 5
|
||||
headers = ['Year', 'Properties', 'Gross Rent', 'Effective Rent', 'NOI', 'Cash Flow',
|
||||
'Depreciation', 'Taxable Income', 'Cash Invested', 'Cumulative Invested', 'Cumulative Cash Flow']
|
||||
for col, h in enumerate(headers, 1):
|
||||
ws1.cell(row=row, column=col, value=h)
|
||||
style_header(ws1, row, len(headers))
|
||||
|
||||
cumulative_invested = 0
|
||||
cumulative_cf = 0
|
||||
for i, r in enumerate(results):
|
||||
row = 6 + i
|
||||
cumulative_invested += r['total_cash_invested']
|
||||
cumulative_cf += r['total_cash_flow']
|
||||
values = [
|
||||
f"Year {r['year']}", r['properties_owned'], r['total_gross_rent'],
|
||||
r['total_effective_rent'], r['total_noi'], r['total_cash_flow'],
|
||||
r['total_depreciation'], r['total_taxable_income'], r['total_cash_invested'],
|
||||
cumulative_invested, cumulative_cf
|
||||
]
|
||||
for col, v in enumerate(values, 1):
|
||||
cell = ws1.cell(row=row, column=col, value=v)
|
||||
if col >= 3:
|
||||
cell.number_format = money_neg_fmt
|
||||
if col == 6 and isinstance(v, (int, float)) and v < 0:
|
||||
cell.font = red_font
|
||||
if col == 8 and isinstance(v, (int, float)) and v < 0:
|
||||
cell.font = red_font
|
||||
style_row(ws1, row, len(headers))
|
||||
|
||||
# Totals row
|
||||
row = 11
|
||||
ws1.cell(row=row, column=1, value="5-YEAR TOTALS").font = Font(bold=True)
|
||||
ws1.cell(row=row, column=3, value=sum(r['total_gross_rent'] for r in results)).number_format = money_fmt
|
||||
ws1.cell(row=row, column=5, value=sum(r['total_noi'] for r in results)).number_format = money_fmt
|
||||
ws1.cell(row=row, column=6, value=sum(r['total_cash_flow'] for r in results))
|
||||
ws1.cell(row=row, column=6).number_format = money_neg_fmt
|
||||
ws1.cell(row=row, column=6).font = red_font
|
||||
ws1.cell(row=row, column=7, value=sum(r['total_depreciation'] for r in results)).number_format = money_fmt
|
||||
ws1.cell(row=row, column=9, value=395000).number_format = money_fmt
|
||||
style_row(ws1, row, len(headers))
|
||||
|
||||
auto_width(ws1)
|
||||
|
||||
# ============ SHEET 2: Tax Impact by AGI ============
|
||||
ws2 = wb.create_sheet("Tax Impact by AGI")
|
||||
ws2.sheet_properties.tabColor = "4a1a1a"
|
||||
|
||||
ws2.merge_cells('A1:H1')
|
||||
ws2['A1'] = "PASSIVE LOSS & TAX SAVINGS BY AGI BRACKET"
|
||||
ws2['A1'].font = Font(bold=True, color="00BFFF", size=14)
|
||||
|
||||
# Critical note
|
||||
ws2.merge_cells('A3:H4')
|
||||
ws2['A3'] = ("CRITICAL: At AGI >$150K (D J + partners likely scenario), passive losses CANNOT offset W-2 income. "
|
||||
"Losses carry forward indefinitely but only offset future passive rental income or are released upon sale. "
|
||||
"This means $0 annual tax benefit from depreciation — the entire cost seg strategy provides NO cash flow benefit.")
|
||||
ws2['A3'].font = Font(color="FF6666", size=10, italic=True)
|
||||
ws2['A3'].alignment = Alignment(wrap_text=True)
|
||||
|
||||
row = 6
|
||||
headers = ['Year', 'Passive Loss Generated',
|
||||
'Tax Savings\n(AGI <$100K)', 'Losses Carried\n(AGI <$100K)',
|
||||
'Tax Savings\n(AGI $100-150K)', 'Losses Carried\n(AGI $100-150K)',
|
||||
'Tax Savings\n(AGI >$150K)', 'Losses Carried\n(AGI >$150K)']
|
||||
for col, h in enumerate(headers, 1):
|
||||
ws2.cell(row=row, column=col, value=h)
|
||||
style_header(ws2, row, len(headers))
|
||||
|
||||
for i, r in enumerate(results):
|
||||
row = 7 + i
|
||||
values = [
|
||||
f"Year {r['year']}", abs(r['total_taxable_income']),
|
||||
r['tax_savings_scenario_a'], r['passive_losses_scenario_a'],
|
||||
r['tax_savings_scenario_b'], r['passive_losses_scenario_b'],
|
||||
r['tax_savings_scenario_c'], r['passive_losses_scenario_c']
|
||||
]
|
||||
for col, v in enumerate(values, 1):
|
||||
cell = ws2.cell(row=row, column=col, value=v)
|
||||
if col >= 2:
|
||||
cell.number_format = money_fmt
|
||||
# Highlight the >$150K columns
|
||||
if col in (7, 8) and isinstance(v, (int, float)):
|
||||
cell.font = red_font
|
||||
style_row(ws2, row, len(headers))
|
||||
|
||||
# Summary
|
||||
row = 13
|
||||
ws2.cell(row=row, column=1, value="5-YEAR TOTALS").font = Font(bold=True)
|
||||
ws2.cell(row=row, column=3, value=30000).number_format = money_fmt
|
||||
ws2.cell(row=row, column=5, value=15000).number_format = money_fmt
|
||||
ws2.cell(row=row, column=7, value=0).font = red_font
|
||||
ws2.cell(row=row, column=7).number_format = money_fmt
|
||||
ws2.cell(row=row, column=8, value=337877).font = red_font
|
||||
ws2.cell(row=row, column=8).number_format = money_fmt
|
||||
|
||||
auto_width(ws2)
|
||||
|
||||
# ============ SHEET 3: S&P 500 Comparison ============
|
||||
ws3 = wb.create_sheet("vs S&P 500")
|
||||
ws3.sheet_properties.tabColor = "1a4a1a"
|
||||
|
||||
ws3.merge_cells('A1:F1')
|
||||
ws3['A1'] = "SAME CAPITAL IN S&P 500 vs REAL ESTATE PORTFOLIO"
|
||||
ws3['A1'].font = Font(bold=True, color="00BFFF", size=14)
|
||||
|
||||
row = 3
|
||||
headers = ['Year', 'Capital Deployed', 'S&P 500 Value\n(9% avg)', 'RE Portfolio Equity', 'RE Cash Flow\n(Cumulative)', 'S&P 500 Advantage']
|
||||
for col, h in enumerate(headers, 1):
|
||||
ws3.cell(row=row, column=col, value=h)
|
||||
style_header(ws3, row, len(headers))
|
||||
|
||||
sp500_values = []
|
||||
sp500_total = 0
|
||||
cum_cf = 0
|
||||
for i, r in enumerate(results):
|
||||
row = 4 + i
|
||||
year = r['year']
|
||||
# Each year invest $79K, compounds at 9%
|
||||
sp500_total = (sp500_total * 1.09) + r['total_cash_invested']
|
||||
sp500_values.append(sp500_total)
|
||||
|
||||
# RE equity = appreciation + principal paydown (rough)
|
||||
props = r['properties_owned']
|
||||
re_equity = props * (DOWN_PAYMENT + (PURCHASE_PRICE * ((1.03 ** year) - 1))) # appreciation gain
|
||||
# Add principal paydown estimate (~$3K/yr per property in early years)
|
||||
re_equity += props * 3000 * ((year + 1) / 2)
|
||||
|
||||
cum_cf += r['total_cash_flow']
|
||||
|
||||
advantage = sp500_total - (re_equity + cum_cf)
|
||||
|
||||
values = [f"Year {year}", r['total_cash_invested'], sp500_total, re_equity, cum_cf, advantage]
|
||||
for col, v in enumerate(values, 1):
|
||||
cell = ws3.cell(row=row, column=col, value=v)
|
||||
if col >= 2:
|
||||
cell.number_format = money_neg_fmt
|
||||
if col == 6 and isinstance(v, (int, float)) and v > 0:
|
||||
cell.font = green_font
|
||||
style_row(ws3, row, len(headers))
|
||||
|
||||
auto_width(ws3)
|
||||
|
||||
# ============ SHEET 4: Property Assumptions ============
|
||||
ws4 = wb.create_sheet("Assumptions")
|
||||
ws4.sheet_properties.tabColor = "444444"
|
||||
|
||||
ws4.merge_cells('A1:C1')
|
||||
ws4['A1'] = "MODEL ASSUMPTIONS"
|
||||
ws4['A1'].font = Font(bold=True, color="00BFFF", size=14)
|
||||
|
||||
assumptions = [
|
||||
("Purchase", "", ""),
|
||||
("Purchase Price", "$345,000", ""),
|
||||
("Down Payment", "20%", "$69,000"),
|
||||
("Closing Costs", "$6,000", ""),
|
||||
("Cost Seg Study", "$4,000", "per property"),
|
||||
("Total Cash Per Property", "$79,000", ""),
|
||||
("", "", ""),
|
||||
("Financing", "", ""),
|
||||
("Loan Amount", "$276,000", ""),
|
||||
("Interest Rate", "6.25%", "30-year fixed"),
|
||||
("Monthly P&I", "$1,699.38", ""),
|
||||
("Monthly PITI", "$2,016.05", ""),
|
||||
("", "", ""),
|
||||
("Income", "", ""),
|
||||
("Monthly Rent", "$2,000", "0.58% rent-to-price ratio"),
|
||||
("Annual Increase", "4%", "Aggressive — 2-3% more realistic"),
|
||||
("Vacancy", "5%", ""),
|
||||
("Management", "0%", "Self-managed"),
|
||||
("", "", ""),
|
||||
("Expenses", "", ""),
|
||||
("Property Tax", "$2,400/yr", ""),
|
||||
("Insurance", "$1,400/yr", ""),
|
||||
("Maintenance", "$2,000/yr", "LOW — should be 1% of value = $3,450"),
|
||||
("Appreciation", "3%/yr", ""),
|
||||
("", "", ""),
|
||||
("Cost Segregation", "", ""),
|
||||
("5-Year Property", "15%", "$41,250 of $275K basis"),
|
||||
("15-Year Property", "12%", "$33,000 of $275K basis"),
|
||||
("27.5-Year Property", "73%", "$200,750 of $275K basis"),
|
||||
("Bonus Depreciation (2026)", "40%", "Declining 20%/yr from 100% in 2022"),
|
||||
("", "", ""),
|
||||
("Tax Rules", "", ""),
|
||||
("AGI >$150K", "$0 passive deduction", "Losses carry forward ONLY"),
|
||||
("AGI $100-150K", "Partial $25K deduction", "Phased out"),
|
||||
("AGI <$100K", "Full $25K deduction", "Against W-2 income"),
|
||||
]
|
||||
|
||||
for i, (label, val, note) in enumerate(assumptions):
|
||||
row = 3 + i
|
||||
ws4.cell(row=row, column=1, value=label)
|
||||
ws4.cell(row=row, column=2, value=val)
|
||||
ws4.cell(row=row, column=3, value=note).font = Font(italic=True, color="888888")
|
||||
if val == "" and note == "" and label:
|
||||
ws4.cell(row=row, column=1).font = Font(bold=True, color="00BFFF")
|
||||
|
||||
auto_width(ws4)
|
||||
|
||||
# Save
|
||||
outpath = "/home/wdjones/.openclaw/workspace/data/real-estate-cost-seg-model.xlsx"
|
||||
wb.save(outpath)
|
||||
print(f"Saved to {outpath}")
|
||||
Reference in New Issue
Block a user