Let's value your property.
Pick the property and tell me why you need the value. Upload financials if you have them — T12 and rent roll are the most useful. If the purpose doesn't fit one of the options, leave it blank and explain in free text.
cd /vercel/sandbox && python3 -c "
import openpyxl
wb = openpyxl.load_workbook('budget.xlsx', data_only=True)
for ws in wb.worksheets:
print('SHEET:', ws.title, ws.dimensions)
"
cd /vercel/sandbox && python3 -c "
import openpyxl
wb = openpyxl.load_workbook('budget.xlsx', data_only=True)
ws = wb['Report1']
for row in ws.iter_rows(min_row=1, max_row=201, values_only=True):
if any(c is not None for c in row):
print(row)
" | head -250
cd /vercel/sandbox && python3 -c "
import openpyxl
wb = openpyxl.load_workbook('rent_roll.xlsx', data_only=True)
for ws in wb.worksheets:
print('SHEET', ws.title, ws.dimensions)
for row in ws.iter_rows(min_row=1, max_row=15, values_only=True):
print(row)
"
cd /vercel/sandbox && python3 << 'EOF'
import openpyxl, datetime
wb = openpyxl.load_workbook('rent_roll.xlsx', data_only=True)
ws = wb['Report1']
rows = list(ws.iter_rows(min_row=1, max_row=200, values_only=True))
# find header row index
for i,r in enumerate(rows):
if r[0]=='Unit':
header_idx=i
break
print("header at", header_idx)
data=[]
section=None
for r in rows[header_idx+2:]:
if r[0] is None and r[1] is None:
continue
if isinstance(r[0], str) and ('Residents' in r[0] or 'Total' in r[0] or 'Summary' in r[0]):
section = r[0]
print("SECTION:", r)
continue
data.append((section,)+r)
print(len(data))
for d in data[:5]:
print(d)
for d in data[-15:]:
print(d)
EOF
cd /vercel/sandbox && python3 << 'EOF'
import openpyxl, datetime
wb = openpyxl.load_workbook('rent_roll.xlsx', data_only=True)
ws = wb['Report1']
rows = list(ws.iter_rows(min_row=1, max_row=200, values_only=True))
for i,r in enumerate(rows):
print(i, r)
EOF
cd /vercel/sandbox && python3 << 'EOF'
import openpyxl, datetime
wb = openpyxl.load_workbook('rent_roll.xlsx', data_only=True)
ws = wb['Report1']
rows = list(ws.iter_rows(min_row=8, max_row=130, values_only=True))
units=[]
for r in rows:
unit, utype, sqft, resid, name, market, actual, secdep, otherdep, movein, leaseexp, moveout, bal = r
if unit is None: continue
status = 'VACANT' if name=='VACANT' else ('MODEL' if name=='MODEL' else 'OCCUPIED')
units.append(dict(unit=unit, utype=utype, sqft=sqft, market=market, actual=actual, movein=movein, leaseexp=leaseexp, status=status))
occ = [u for u in units if u['status']=='OCCUPIED']
vac = [u for u in units if u['status']=='VACANT']
model = [u for u in units if u['status']=='MODEL']
print("total units", len(units), "occ", len(occ), "vac", len(vac), "model", len(model))
tot_market_occ = sum(u['market'] for u in occ)
tot_actual_occ = sum(u['actual'] for u in occ)
print("occ market sum", tot_market_occ, "occ actual sum", tot_actual_occ, "gap %", (tot_market_occ-tot_actual_occ)/tot_market_occ*100)
print("avg actual rent occ", tot_actual_occ/len(occ))
print("avg market rent occ", tot_market_occ/len(occ))
tot_market_vac = sum(u['market'] for u in vac)
print("avg market rent vacant units", tot_market_vac/len(vac))
# unit mix by type
from collections import defaultdict
mix = defaultdict(lambda: {'count':0,'sqft':0,'market':0,'actual':0,'n_occ':0})
for u in units:
m = mix[u['utype']]
m['count']+=1
m['sqft']=u['sqft']
m['market']+=u['market']
if u['status']=='OCCUPIED':
m['actual']+=u['actual']
m['n_occ']+=1
for k,v in sorted(mix.items()):
print(k, v, 'avg market/unit', round(v['market']/v['count'],0))
# lease expirations in next 12 months (from 2026-09-11)
today = datetime.datetime(2026,9,11)
next12 = today + datetime.timedelta(days=365)
exp_soon = [u for u in occ if u['leaseexp'] and today <= u['leaseexp'] <= next12]
print("units w/ lease expiring in next 12mo:", len(exp_soon), "of", len(occ), "=", round(len(exp_soon)/len(occ)*100,1),"%")
avg_sqft_occ = sum(u['sqft'] for u in occ)/len(occ)
print("avg sqft occ", avg_sqft_occ)
print("avg rent/sqft actual (occ)", tot_actual_occ/sum(u['sqft'] for u in occ))
EOF
Find sale comps and rental comps for a 124-unit build-for-rent (BFR) single-family-style rental community: "Amber Pines at Fosters Ridge", 9811 Laurel Lake Dr, Conroe, TX 77384 (property_id dc89327e695af7563ae8ebd22971bae7). Built 2018, avg unit size ~1571 sqft, submarket "The Woodlands" (submarket_id 50f2ef35242cea8d1578a0514e1c2274), market Houston, TX (market_id 26420).
Load and follow the "sales-comps" skill and the "rental-comps" skill instructions exactly (query progressively wider geographies, filter/rank per the skill's criteria) to produce:
Return the two tables as structured data (property IDs, names, key metrics) plus a 3-4 sentence summary of where the subject sits in the comp distribution for both price-per-unit and rent. Note any data confidence issues (thin comp counts, stale dates).
SUBJECT: Amber Pines at Fosters Ridge (property_mfr id dc89327e695af7563ae8ebd22971bae7), 9811 Laurel Lake Dr, Conroe TX 77384. 124 units, built 2018, BUILD_FOR_RENT style, avg unit size 1,571 sqft (3-4BR only product). Current metrics: asking rent avg $2,012.72 (median $1,920), in-place rent avg $2,013.79 (median $1,995), in-place rent/sqft $1.32, asking rent/sqft $1.28, physical occupancy 86.3% (30-day avg 95.0%, occupancy down ~11pts over trailing 3 months), last recorded sale 12/14/2020 with no price captured in datamart.
METHODOLOGY: Followed sales-comps and rental-comps skill protocols — queried zipcode (77384) then Montgomery County (48339) then full Houston MSA (26420) for sales; queried county-wide property_mfr for rent comps (subject has no census_place_id, so county was the first usable geography), supplementing with a targeted BUILD_FOR_RENT style-filter pass to surface true BFR peers.
=== SALE COMPS (5-6 selected, ranked by proximity/vintage/recency; Montgomery County / Houston MSA) === Note: No BUILD_FOR_RENT-style sale transactions exist in the datamart for the Houston MSA — the sale comp set below is limited to GARDEN/LOW_RISE/MID_RISE communities of similar vintage, the closest available proxy.
Cap rates: not available in the datamart for any of these transactions. PPU range: $134,797 – $181,383/unit (avg ≈ $156,373; median ≈ $147,720). Only 1 of 6 comps (Encore at Westfork) falls inside a standard 3-year lookback; the rest are 3.5–6+ years stale — flag as a data confidence issue. No transacted BFR product exists to benchmark the subject's specific product type directly.
=== RENTAL COMPS (8 selected, ranked by proximity/BFR-style match/bedroom overlap; Montgomery County) ===
Comp-set in-place rent average ≈ $1,594; range $1,276–$2,037.
=== POSITIONING SUMMARY === Subject's in-place rent ($2,013.79) sits at or above the top of the 8-comp rental set (~26% above the $1,594 average, exceeded only by the newly-delivered, still-in-lease-up Oasis at Magnolia), reflecting its large 1,571-sqft average unit size and 3-4BR-only product mix versus comps that blend smaller 1-3BR units. On a $/sqft basis subject ($1.32 in-place) is mid-pack — lower than the newer BFR peers (Cottage Living Woodlands $1.94, Oasis $1.87) but above older/smaller-unit garden product (Centro Modern Homes $1.05, The Dominion $1.26), consistent with larger units commanding lower per-square-foot rent even at higher gross rent. Subject's physical occupancy (86.3% latest, though 95.0% on a 30-day average basis) is below every comp except lease-up outlier Oasis at Magnolia, suggesting recent move-outs or seasonal softness worth monitoring. On the sales side, subject has no recorded transaction price, but comparable-vintage 2015-2021 garden/low-rise product in the immediate submarket traded at $134,797–$181,383/unit; absent a true BFR sale comp, subject would likely price in the upper half of that band given its larger, newer, single-family-style units, though this is directional given the age of most sale comps (4 of 6 are 3.5+ years stale) and the complete absence of transacted BFR product in the MSA.
DATA CONFIDENCE ISSUES: (1) No BUILD_FOR_RENT sale comps exist in the Houston MSA dataset — sale comps are the closest available garden/low-rise proxies, not true product-type matches. (2) 5 of 6 sale comps are older than the standard 3-year lookback (up to ~5.5 years stale), reducing pricing precision. (3) No cap rate data available for any transaction. (4) One rental comp (Oasis at Magnolia) is a brand-new 2025 delivery at 56% occupancy — its rent figures reflect concession-driven lease-up pricing, not stabilized market rent. (5) Centro Modern Homes' in-place rent is based on only 3 leased units (low statistical confidence). (6) Subject's own occupancy_latest (86.3%) diverges sharply from its 30-day average (95.0%), suggesting a recent data anomaly or genuine move-out spike that should be verified before use in underwriting.
Amber Pines at Fosters Ridge — Conroe, TX (Houston MSA / The Woodlands submarket) — multifamily build-for-rent community, 124 units — valuing for acquisition underwriting.
A few things worth flagging before we lock assumptions: physical occupancy is 86.3% today (confirmed independently by your rent roll and the platform's rent index) versus 92.7% for the submarket and a 3-month-ago reading of 97.6% — a real, not seasonal, occupancy air-pocket, with 85% of occupied leases expiring in the next 12 months. Your uploaded document is a full-year 2026 budget, not a trailing-12 actual — I'm treating it as property-provided but forward-looking, not verified operating history.
Silent defaults applied below; flag anything you'd like changed in free text.
| Assumption | Value | Source |
|---|---|---|
| Other income | $432,055/yr | Uploaded 2026 budget |
| OpEx ratio | 57.2% of EGI | Uploaded 2026 budget (property-specific) |
| Expense growth | 2.5%/yr | Default |
| Hold period | 5 years (direct cap) | Default |
| RE tax basis | $980,004/yr embedded in budget OpEx (29.8% of EGI — unusually high) | Uploaded budget |
cat skills/mf-operating-statement/references/schema.md | head -200
{
"mode": "direct_cap",
"property_context": {
"units": 124,
"in_place_rent": 2110.66,
"market_rent": 2057.0,
"occupancy": 0.92,
"stabilization_status_override": "stabilized",
"recent_delivery_flag": false,
"repositioning_flag": false
},
"mf_property_financials": {
"gpr": 3232832,
"vacancy_loss": -488878,
"other_income": 432055,
"egi": 3176009,
"opex": 1880061,
"opex_ratio": null,
"noi": 1295948
},
"benchmarks": { "vacancy_rate": 0.08, "opex_ratio": 0.592 },
"valuation_inputs": {
"going_in_cap": 0.054,
"exit_cap": 0.058,
"discount_rate": 0.085,
"sale_costs_pct": 0.02,
"purchase_price": null
}
}
cd /vercel/sandbox && python3 skills/mf-operating-statement/scripts/operating.py < /tmp/req.json > /tmp/out.json && cat /tmp/out.json
{
"mode": "direct_cap",
"property_context": {
"units": 124,
"in_place_rent": 2110.66,
"market_rent": 2057.0,
"occupancy": 0.92,
"stabilization_status_override": "stabilized",
"recent_delivery_flag": false,
"repositioning_flag": false
},
"mf_property_financials": {
"gpr": 3232832,
"vacancy_loss": -436357,
"other_income": 432055,
"egi": 3228530,
"opex": 1880061,
"opex_ratio": null,
"noi": 1348469
},
"benchmarks": { "vacancy_rate": 0.08, "opex_ratio": 0.55 },
"valuation_inputs": {
"going_in_cap": 0.054,
"exit_cap": 0.058,
"discount_rate": 0.085,
"sale_costs_pct": 0.02,
"purchase_price": null
}
}
cd /vercel/sandbox && python3 skills/mf-operating-statement/scripts/operating.py < /tmp/req2.json > /tmp/out2.json && cat /tmp/out2.json
mkdir -p output && cat > /tmp/build_wb.py << 'PYEOF'
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
wb = openpyxl.Workbook()
BLUE = Font(color="0000FF")
BLACK = Font(color="000000")
GREEN = Font(color="008000")
BOLD = Font(bold=True)
HEADER_FILL = PatternFill("solid", fgColor="1F4E78")
HEADER_FONT = Font(bold=True, color="FFFFFF")
SUB_FILL = PatternFill("solid", fgColor="D9E1F2")
THIN = Side(style="thin", color="BFBFBF")
BORDER = Border(top=THIN, bottom=THIN, left=THIN, right=THIN)
def style_header(ws, row, col1, col2):
for c in range(col1, col2+1):
cell = ws.cell(row=row, column=c)
cell.fill = HEADER_FILL
cell.font = HEADER_FONT
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
def set_widths(ws, widths):
for i, w in enumerate(widths, start=1):
ws.column_dimensions[get_column_letter(i)].width = w
# ---------- Sheet 1: Assumptions ----------
ws = wb.active
ws.title = "Assumptions"
ws["A1"] = "Amber Pines at Fosters Ridge — Valuation Assumptions"
ws["A1"].font = Font(bold=True, size=14)
rows = [
("Property", "", ""),
("Units", 124, "user-provided / property data"),
("Year built", 2018, "property data"),
("Avg unit size (sqft)", 1571, "property data"),
("", "", ""),
("Income Assumptions", "", ""),
("Gross potential rent (annual, budget)", 3232832, "uploaded budget"),
("Stabilized occupancy", 0.92, "submarket benchmark (current actual 86.3%)"),
("Stabilized vacancy + collection loss (combined)", 0.135, "derived: normalized to stabilized occupancy"),
("Other income (annual)", 432055, "uploaded budget"),
("Operating expenses (annual, budget)", 1880061, "uploaded budget (incl. $980K RE tax)"),
("Market rent growth (annual)", 0.02, "default (recovery assumption; submarket T12 asking rent is -2.7%)"),
("Expense growth (annual)", 0.025, "default"),
("", "", ""),
("Valuation Assumptions", "", ""),
("Going-in cap rate", 0.054, "blend: Houston MF 5.49% / SFR-BFR proxy 5.30% (2Q26)"),
("Exit / terminal cap rate", 0.058, "default: +40bps over going-in"),
("Discount rate", 0.085, "default"),
("Hold period (years)", 5, "default"),
]
r = 3
for label, val, src in rows:
ws.cell(row=r, column=1, value=label)
if label and val != "":
c = ws.cell(row=r, column=2, value=val)
c.font = BLUE
if isinstance(val, float) and val < 1:
c.number_format = "0.0%"
elif isinstance(val, (int, float)):
c.number_format = "#,##0"
if src:
ws.cell(row=r, column=3, value=src).font = Font(italic=True, size=9, color="666666")
if label in ("Property","Income Assumptions","Valuation Assumptions"):
ws.cell(row=r, column=1).font = BOLD
r += 1
set_widths(ws, [42, 16, 55])
# named-like references (use direct cell addresses)
A = "Assumptions"
# ---------- Sheet 2: Property Fundamentals ----------
ws2 = wb.create_sheet("Fundamentals")
ws2["A1"] = "Property Fundamentals"
ws2["A1"].font = Font(bold=True, size=14)
fund_rows = [
("Units", f"='{A}'!B4"),
("Avg unit size (sqft)", f"='{A}'!B6"),
("Total rentable sqft", 194804),
("Year built", f"='{A}'!B5"),
("Submarket", "The Woodlands (Houston, TX MSA)"),
("Asset class", "Multifamily — Build-for-Rent (BFR) single-family-style"),
("Current physical occupancy (8/26/26)", 0.863),
("Submarket occupancy (latest)", 0.9266),
("T12 budgeted NOI (property-provided; not verified actual)", f"='{A}'!B12-'{A}'!B10+'{A}'!B9"),
("Last sale date", "12/14/2020"),
("Last sale price", "Not disclosed in records"),
("Current RE tax assessment (total)", 36353447),
("Current RE tax assessment (per unit)", f"=B13/'{A}'!B4"),
("Annual RE tax (embedded in OpEx)", 980004),
("RE tax as % of EGI", None),
]
r = 3
for row in fund_rows:
label = row[0]
val = row[1] if len(row) > 1 else None
ws2.cell(row=r, column=1, value=label)
if val is not None:
cell = ws2.cell(row=r, column=2, value=val)
if isinstance(val, str) and val.startswith("="):
cell.font = GREEN if A in val else BLACK
elif isinstance(val, (int, float)):
cell.font = BLUE
if label.endswith("occupancy") or "occupancy" in label.lower() or "%" in label:
cell.number_format = "0.0%"
else:
cell.number_format = "#,##0"
r += 1
set_widths(ws2, [50, 22])
# ---------- Sheet 3: Sale Comps ----------
ws3 = wb.create_sheet("Sale Comps")
ws3["A1"] = "Sale Comparables — Houston MSA / Montgomery County"
ws3["A1"].font = Font(bold=True, size=14)
headers = ["Property", "Distance (mi)", "Sale Date", "Units", "Year Built", "Sale Price", "$/Unit", "Notes"]
for i, h in enumerate(headers, start=1):
ws3.cell(row=3, column=i, value=h)
style_header(ws3, 3, 1, len(headers))
comp_data = [
("Encore at Westfork", 6.1, "6/22/2026", 222, 2017, 29925000, None, "Most recent transaction"),
("Wildwood Forest", 9.6, "1/29/2025", 252, "1997/renov 2018", 43167810, None, ""),
("Berkshire Jones Forest", 1.7, "3/31/2020", 416, 2015, 56968800, None, "Same submarket; stale (>6 yrs)"),
("Enclave at Woodland Lakes", 1.8, "11/19/2021", 340, 2017, 53870320, None, "Same submarket; stale"),
("Woodridge at Grand Central Park", 3.6, "8/30/2022", 288, 2021, 52238410, None, "Newest vintage comp; stale"),
("The Mill Apartments", 9.5, "8/18/2021", 201, 2020, 31250000, None, "Stale"),
]
r = 4
for row in comp_data:
ws3.cell(row=r, column=1, value=row[0])
ws3.cell(row=r, column=2, value=row[1]).number_format="0.0"
ws3.cell(row=r, column=3, value=row[2])
ws3.cell(row=r, column=4, value=row[3]).number_format="#,##0"
ws3.cell(row=r, column=5, value=str(row[4]))
ws3.cell(row=r, column=6, value=row[5]).number_format="$#,##0"
ppu_cell = ws3.cell(row=r, column=7, value=f"=F{r}/D{r}")
ppu_cell.number_format = "$#,##0"
ws3.cell(row=r, column=8, value=row[7])
r += 1
avg_row = r+1
ws3.cell(row=avg_row, column=1, value="Average (all comps)").font = BOLD
ws3.cell(row=avg_row, column=7, value=f"=AVERAGE(G4:G{r-1})").number_format = "$#,##0"
ws3.cell(row=avg_row, column=7).font = BOLD
ws3.cell(row=avg_row+1, column=1, value="Subject (BFR — no direct product-type comp; premium applied)").font = Font(italic=True)
set_widths(ws3, [32,13,12,9,16,14,12,32])
# ---------- Sheet 4: Rental Comps ----------
ws4 = wb.create_sheet("Rental Comps")
ws4["A1"] = "Rental Comparables — Montgomery County"
ws4["A1"].font = Font(bold=True, size=14)
headers2 = ["Property", "Distance (mi)", "Style", "Year Built", "Units", "Asking Rent", "In-Place Rent", "In-Place $/SF", "Occupancy"]
for i, h in enumerate(headers2, start=1):
ws4.cell(row=3, column=i, value=h)
style_header(ws4, 3, 1, len(headers2))
rent_data = [
("Sanctuary at Jacobs Reserve", 1.5, "LOW_RISE", 2017, 384, 1443.92, 1437.43, 1.53, 0.951),
("Cottage Living Woodlands", 1.6, "BUILD_FOR_RENT", 2022, 314, 1495.52, 1745.57, 1.94, 0.943),
("Berkshire Jones Forest", 1.7, "LOW_RISE", 2015, 416, 1328.91, 1275.94, 1.54, 0.909),
("Woodland Landings", 2.2, "GARDEN", 2013, 360, 1313.00, 1381.38, 1.30, 0.911),
("Woodridge at Grand Central Park", 3.6, "LOW_RISE", 2021, 288, 1579.02, 1487.34, 1.58, 0.903),
("Oasis at Magnolia", 3.8, "BUILD_FOR_RENT", 2025, 158, 2241.56, 2037.16, 1.87, 0.563),
("The Dominion", 3.8, "TOWNHOUSE", 2003, 238, 1603.00, 1563.73, 1.26, 0.954),
("Centro Modern Homes", 5.6, "BUILD_FOR_RENT", 2022, 84, 2040.79, 1821.67, 1.05, 0.929),
]
r = 4
for row in rent_data:
ws4.cell(row=r, column=1, value=row[0])
ws4.cell(row=r, column=2, value=row[1]).number_format="0.0"
ws4.cell(row=r, column=3, value=row[2])
ws4.cell(row=r, column=4, value=row[3])
ws4.cell(row=r, column=5, value=row[4]).number_format="#,##0"
ws4.cell(row=r, column=6, value=row[5]).number_format="$#,##0"
ws4.cell(row=r, column=7, value=row[6]).number_format="$#,##0"
ws4.cell(row=r, column=8, value=row[7]).number_format="$0.00"
ws4.cell(row=r, column=9, value=row[8]).number_format="0.0%"
r += 1
avg_row2 = r + 1
ws4.cell(row=avg_row2, column=1, value="Comp-set average").font = BOLD
ws4.cell(row=avg_row2, column=7, value=f"=AVERAGE(G4:G{r-1})").number_format="$#,##0"
ws4.cell(row=avg_row2, column=7).font = BOLD
ws4.cell(row=avg_row2+1, column=1, value="Subject (in-place, actual)")
ws4.cell(row=avg_row2+1, column=7, value=2013.79).number_format="$#,##0"
ws4.cell(row=avg_row2+1, column=7).font = BLUE
ws4.cell(row=avg_row2+1, column=8, value=1.32).number_format="$0.00"
ws4.cell(row=avg_row2+1, column=8).font = BLUE
ws4.cell(row=avg_row2+1, column=9, value=0.863).number_format="0.0%"
ws4.cell(row=avg_row2+1, column=9).font = BLUE
set_widths(ws4, [32,13,16,11,9,13,13,12,11])
wb.save("output/Amber_Pines_Valuation.xlsx")
print("saved base workbook")
PYEOF
python3 /tmp/build_wb.pyNow the income approach, sensitivity, and reconciliation sheets:
cat > /tmp/build_wb2.py << 'PYEOF'
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
wb = openpyxl.load_workbook("output/Amber_Pines_Valuation.xlsx")
BLUE = Font(color="0000FF")
BLACK = Font(color="000000")
GREEN = Font(color="008000")
BOLD = Font(bold=True)
HEADER_FILL = PatternFill("solid", fgColor="1F4E78")
HEADER_FONT = Font(bold=True, color="FFFFFF")
def style_header(ws, row, col1, col2):
for c in range(col1, col2+1):
cell = ws.cell(row=row, column=c)
cell.fill = HEADER_FILL
cell.font = HEADER_FONT
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
def set_widths(ws, widths):
for i, w in enumerate(widths, start=1):
ws.column_dimensions[get_column_letter(i)].width = w
A = "Assumptions"
# ---------- Sheet 5: Income Approach ----------
ws5 = wb.create_sheet("Income Approach")
ws5["A1"] = "Income Approach — Direct Capitalization"
ws5["A1"].font = Font(bold=True, size=14)
headers = ["Line", "Stabilized (Yr 1)"]
for i,h in enumerate(headers, start=1):
ws5.cell(row=3, column=i, value=h)
style_header(ws5, 3, 1, 2)
lines = [
("Gross potential rent", f"='{A}'!B9*(1+'{A}'!B14)"),
("Vacancy + collection loss", f"=-B4*'{A}'!B10*(1+'{A}'!B10-1)/1"), # placeholder, fixed below
("Other income", f"='{A}'!B11*(1+'{A}'!B14)"),
("Effective gross income", "=B4+B5+B6"),
("Operating expenses", f"='{A}'!B12*(1+'{A}'!B15)"),
("Net operating income", "=B7-B8"),
("Going-in cap rate", f"='{A}'!B17"),
("Direct cap value", "=B9/B10"),
("Value per unit", f"=B11/'{A}'!B4"),
("Value per sqft", f"=B11/('{A}'!B4*'{A}'!B6)"),
]
r = 4
for label, formula in lines:
ws5.cell(row=r, column=1, value=label)
c = ws5.cell(row=r, column=2, value=formula)
c.font = GREEN
if "cap rate" in label.lower():
c.number_format = "0.00%"
elif "per unit" in label.lower() or "per sqft" in label.lower():
c.number_format = "$#,##0"
else:
c.number_format = "$#,##0"
if label in ("Effective gross income", "Net operating income", "Direct cap value"):
ws5.cell(row=r, column=1).font = BOLD
c.font = Font(bold=True, color="008000")
r += 1
# fix vacancy loss formula properly: GPR * combined vacancy rate (13.5%)
ws5["B5"] = f"=-B4*'{A}'!B10"
ws5["B5"].number_format = "$#,##0"
ws5["B5"].font = GREEN
set_widths(ws5, [30, 20])
# ---------- Sheet 6: Sensitivity ----------
ws6 = wb.create_sheet("Sensitivity")
ws6["A1"] = "Sensitivity — Going-In Cap Rate vs. Stabilized NOI"
ws6["A1"].font = Font(bold=True, size=14)
ws6["A3"] = "Value ($) — rows: cap rate, columns: NOI sensitivity"
ws6["A3"].font = BOLD
# axis: cap rate rows (-50,-25,0,+25,+50 bps), NOI columns (-5%, 0%, +5%)
cap_base_ref = "'Income Approach'!B10"
noi_base_ref = "'Income Approach'!B9"
ws6["B5"] = "NOI -5%"
ws6["C5"] = "NOI Base"
ws6["D5"] = "NOI +5%"
for c in ["B5","C5","D5"]:
ws6[c].font = BOLD
ws6[c].alignment = Alignment(horizontal="center")
cap_offsets = [-0.005, -0.0025, 0, 0.0025, 0.005]
noi_mults = [0.95, 1.0, 1.05]
for i, off in enumerate(cap_offsets):
row = 6+i
ws6.cell(row=row, column=1, value=f"={cap_base_ref}+({off})")
ws6.cell(row=row, column=1).number_format = "0.00%"
ws6.cell(row=row, column=1).font = BLUE
for j, mult in enumerate(noi_mults):
col = 2+j
formula = f"=({noi_base_ref}*{mult})/$A{row}"
cell = ws6.cell(row=row, column=col, value=formula)
cell.number_format = "$#,##0"
cell.font = BLACK
set_widths(ws6, [14,16,16,16])
# ---------- Sheet 7: Reconciliation ----------
ws7 = wb.create_sheet("Reconciliation")
ws7["A1"] = "Methodology Reconciliation"
ws7["A1"].font = Font(bold=True, size=14)
headers3 = ["Methodology", "Value", "$/Unit", "Implied Cap Rate", "Role"]
for i, h in enumerate(headers3, start=1):
ws7.cell(row=3, column=i, value=h)
style_header(ws7, 3, 1, len(headers3))
ws7.cell(row=4, column=1, value="Income approach (direct cap, stabilized NOI)")
ws7.cell(row=4, column=2, value="='Income Approach'!B11").number_format="$#,##0"
ws7.cell(row=4, column=2).font = GREEN
ws7.cell(row=4, column=3, value="='Income Approach'!B12").number_format="$#,##0"
ws7.cell(row=4, column=3).font = GREEN
ws7.cell(row=4, column=4, value="='Income Approach'!B10").number_format="0.00%"
ws7.cell(row=4, column=4).font = GREEN
ws7.cell(row=4, column=5, value="Primary")
ws7.cell(row=5, column=1, value="Sales comparison (adjusted $/unit, BFR premium to garden comps)")
ws7.cell(row=5, column=2, value=173000*124).number_format="$#,##0"
ws7.cell(row=5, column=2).font = BLUE
ws7.cell(row=5, column=3, value=173000).number_format="$#,##0"
ws7.cell(row=5, column=3).font = BLUE
ws7.cell(row=5, column=4, value="=('Income Approach'!B9)/B5").number_format="0.00%"
ws7.cell(row=5, column=5, value="Supporting (thin, stale comps)")
ws7.cell(row=6, column=1, value="Replacement cost")
ws7.cell(row=6, column=2, value="n/a")
ws7.cell(row=6, column=3, value="n/a")
ws7.cell(row=6, column=4, value="n/a")
ws7.cell(row=6, column=5, value="Considered, skipped (existing 2018 asset trading below build cost is expected; not a basis flag for acquisition pricing)")
set_widths(ws7, [45,16,14,16,45])
wb.save("output/Amber_Pines_Valuation.xlsx")
print("saved sheets 5-7")
PYEOF
python3 /tmp/build_wb2.pycd /vercel/sandbox && python skills/xlsx/scripts/workbook_integrity_scan.py output/Amber_Pines_Valuation.xlsx --json /tmp/integrity.json; echo "EXIT:$?"; cat /tmp/integrity.json | python3 -c "import json,sys; d=json.load(sys.stdin); print(json.dumps([f for f in d.get('findings',[]) if f.get('severity')=='high'], indent=2)[:3000])"cat > /tmp/build_wb_full.py << 'PYEOF'
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
wb = openpyxl.Workbook()
BLUE = Font(color="0000FF")
BLACK = Font(color="000000")
GREEN = Font(color="008000")
BOLD = Font(bold=True)
HEADER_FILL = PatternFill("solid", fgColor="1F4E78")
HEADER_FONT = Font(bold=True, color="FFFFFF")
def style_header(ws, row, col1, col2):
for c in range(col1, col2+1):
cell = ws.cell(row=row, column=c)
cell.fill = HEADER_FILL
cell.font = HEADER_FONT
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
def set_widths(ws, widths):
for i, w in enumerate(widths, start=1):
ws.column_dimensions[get_column_letter(i)].width = w
A = "Assumptions"
# ---------- Sheet 1: Assumptions ----------
ws = wb.active
ws.title = "Assumptions"
ws["A1"] = "Amber Pines at Fosters Ridge - Valuation Assumptions"
ws["A1"].font = Font(bold=True, size=14)
# fixed row map
# 4 Units, 5 Year built, 6 Avg unit sqft
# 9 GPR, 10 Stabilized occupancy, 11 vacancy+collection loss %, 12 other income, 13 opex,
# 14 rent growth, 15 expense growth
# 18 going-in cap, 19 exit cap, 20 discount rate, 21 hold period
rows = {
3: ("Property", None, None, True),
4: ("Units", 124, "user-provided / property data", False),
5: ("Year built", 2018, "property data", False),
6: ("Avg unit size (sqft)", 1571, "property data", False),
7: (None, None, None, False),
8: ("Income Assumptions", None, None, True),
9: ("Gross potential rent (annual, budget)", 3232832, "uploaded budget", False),
10: ("Stabilized occupancy", 0.92, "submarket benchmark (current actual 86.3%)", False),
11: ("Stabilized vacancy + collection loss (combined, % of GPR)", 0.135, "derived: normalized to stabilized occupancy", False),
12: ("Other income (annual)", 432055, "uploaded budget", False),
13: ("Operating expenses (annual, budget)", 1880061, "uploaded budget, incl. $980K RE tax", False),
14: ("Market rent growth (annual)", 0.02, "default; submarket T12 asking rent is -2.7%", False),
15: ("Expense growth (annual)", 0.025, "default", False),
16: (None, None, None, False),
17: ("Valuation Assumptions", None, None, True),
18: ("Going-in cap rate", 0.054, "blend: Houston MF 5.49% / SFR-BFR proxy 5.30%, 2Q26", False),
19: ("Exit / terminal cap rate", 0.058, "default: plus 40bps over going-in", False),
20: ("Discount rate", 0.085, "default", False),
21: ("Hold period (years)", 5, "default", False),
}
for r, (label, val, src, is_header) in rows.items():
if label is None:
continue
ws.cell(row=r, column=1, value=label)
if is_header:
ws.cell(row=r, column=1).font = BOLD
continue
c = ws.cell(row=r, column=2, value=val)
c.font = BLUE
if isinstance(val, float) and val < 1:
c.number_format = "0.0%"
elif isinstance(val, (int, float)):
c.number_format = "#,##0"
if src:
ws.cell(row=r, column=3, value=src).font = Font(italic=True, size=9, color="666666")
set_widths(ws, [46, 16, 58])
for r in range(3, 22):
ws.row_dimensions[r].height = 15
# ---------- Sheet 2: Fundamentals ----------
ws2 = wb.create_sheet("Fundamentals")
ws2["A1"] = "Property Fundamentals"
ws2["A1"].font = Font(bold=True, size=14)
r = 3
ws2.cell(row=r, column=1, value="Units"); ws2.cell(row=r, column=2, value=f"='{A}'!B4").font=GREEN; ws2.cell(row=r,column=2).number_format="#,##0"; r+=1
ws2.cell(row=r, column=1, value="Avg unit size (sqft)"); ws2.cell(row=r, column=2, value=f"='{A}'!B6").font=GREEN; ws2.cell(row=r,column=2).number_format="#,##0"; r+=1
ws2.cell(row=r, column=1, value="Total rentable sqft"); c=ws2.cell(row=r, column=2, value=194804); c.font=BLUE; c.number_format="#,##0"; r+=1
ws2.cell(row=r, column=1, value="Year built"); ws2.cell(row=r, column=2, value=f"='{A}'!B5").font=GREEN; ws2.cell(row=r,column=2).number_format="#,##0"; r+=1
ws2.cell(row=r, column=1, value="Submarket / market"); c=ws2.cell(row=r, column=2, value="The Woodlands, Houston TX MSA"); c.font=BLUE; r+=1
ws2.cell(row=r, column=1, value="Asset class"); c=ws2.cell(row=r, column=2, value="Multifamily, build-for-rent (BFR), single-family style"); c.font=BLUE; r+=1
ws2.cell(row=r, column=1, value="Current physical occupancy (8/26/26)"); c=ws2.cell(row=r, column=2, value=0.863); c.font=BLUE; c.number_format="0.0%"; r+=1
ws2.cell(row=r, column=1, value="Submarket occupancy (latest)"); c=ws2.cell(row=r, column=2, value=0.9266); c.font=BLUE; c.number_format="0.0%"; r+=1
ws2.cell(row=r, column=1, value="Budgeted NOI, FY2026 (property-provided; not a verified T12 actual)"); c=ws2.cell(row=r, column=2, value=1405851); c.font=BLUE; c.number_format="$#,##0"; r+=1
ws2.cell(row=r, column=1, value="Last sale date"); c=ws2.cell(row=r, column=2, value="12/14/2020"); c.font=BLUE; r+=1
ws2.cell(row=r, column=1, value="Last sale price"); c=ws2.cell(row=r, column=2, value="Not disclosed in records"); c.font=BLUE; r+=1
ws2.cell(row=r, column=1, value="Current RE tax assessment, total"); c=ws2.cell(row=r, column=2, value=36353447); c.font=BLUE; c.number_format="$#,##0"; assess_row=r; r+=1
ws2.cell(row=r, column=1, value="Current RE tax assessment, per unit"); ws2.cell(row=r, column=2, value=f"=B{assess_row}/'{A}'!B4").font=BLACK; ws2.cell(row=r,column=2).number_format="$#,##0"; r+=1
ws2.cell(row=r, column=1, value="Annual RE tax embedded in budget OpEx"); c=ws2.cell(row=r, column=2, value=980004); c.font=BLUE; c.number_format="$#,##0"; taxrow=r; r+=1
ws2.cell(row=r, column=1, value="RE tax as % of budgeted EGI"); ws2.cell(row=r, column=2, value=f"=B{taxrow}/3285912").font=BLACK; ws2.cell(row=r,column=2).number_format="0.0%"; r+=1
set_widths(ws2, [55, 24])
# ---------- Sheet 3: Sale Comps ----------
ws3 = wb.create_sheet("Sale Comps")
ws3["A1"] = "Sale Comparables - Houston MSA / Montgomery County"
ws3["A1"].font = Font(bold=True, size=14)
headers = ["Property", "Distance (mi)", "Sale Date", "Units", "Year Built", "Sale Price", "$/Unit", "Notes"]
for i, h in enumerate(headers, start=1):
ws3.cell(row=3, column=i, value=h)
style_header(ws3, 3, 1, len(headers))
comp_data = [
("Encore at Westfork", 6.1, "6/22/2026", 222, "2017", 29925000, "Most recent transaction"),
("Wildwood Forest", 9.6, "1/29/2025", 252, "1997/renov 2018", 43167810, ""),
("Berkshire Jones Forest", 1.7, "3/31/2020", 416, "2015", 56968800, "Same submarket; stale, over 6 yrs"),
("Enclave at Woodland Lakes", 1.8, "11/19/2021", 340, "2017", 53870320, "Same submarket; stale"),
("Woodridge at Grand Central Park", 3.6, "8/30/2022", 288, "2021", 52238410, "Newest vintage comp; stale"),
("The Mill Apartments", 9.5, "8/18/2021", 201, "2020", 31250000, "Stale"),
]
r = 4
first_data_row = r
for row in comp_data:
ws3.cell(row=r, column=1, value=row[0])
ws3.cell(row=r, column=2, value=row[1]).number_format="0.0"
ws3.cell(row=r, column=3, value=row[2])
ws3.cell(row=r, column=4, value=row[3]).number_format="#,##0"
ws3.cell(row=r, column=5, value=row[4])
ws3.cell(row=r, column=6, value=row[5]).number_format="$#,##0"
ppu_cell = ws3.cell(row=r, column=7, value=f"=F{r}/D{r}")
ppu_cell.number_format = "$#,##0"
ws3.cell(row=r, column=8, value=row[6])
r += 1
last_data_row = r-1
avg_row = r+1
ws3.cell(row=avg_row, column=1, value="Average, all comps").font = BOLD
ws3.cell(row=avg_row, column=7, value=f"=AVERAGE(G{first_data_row}:G{last_data_row})").number_format = "$#,##0"
ws3.cell(row=avg_row, column=7).font = BOLD
ws3.cell(row=avg_row+1, column=1, value="Subject is BFR product with no direct product-type sale comp; premium applied in Reconciliation").font = Font(italic=True)
set_widths(ws3, [32,13,12,9,16,14,12,34])
# ---------- Sheet 4: Rental Comps ----------
ws4 = wb.create_sheet("Rental Comps")
ws4["A1"] = "Rental Comparables - Montgomery County"
ws4["A1"].font = Font(bold=True, size=14)
headers2 = ["Property", "Distance (mi)", "Style", "Year Built", "Units", "Asking Rent", "In-Place Rent", "In-Place $/SF", "Occupancy"]
for i, h in enumerate(headers2, start=1):
ws4.cell(row=3, column=i, value=h)
style_header(ws4, 3, 1, len(headers2))
rent_data = [
("Sanctuary at Jacobs Reserve", 1.5, "Low-rise", 2017, 384, 1443.92, 1437.43, 1.53, 0.951),
("Cottage Living Woodlands", 1.6, "Build-for-rent", 2022, 314, 1495.52, 1745.57, 1.94, 0.943),
("Berkshire Jones Forest", 1.7, "Low-rise", 2015, 416, 1328.91, 1275.94, 1.54, 0.909),
("Woodland Landings", 2.2, "Garden", 2013, 360, 1313.00, 1381.38, 1.30, 0.911),
("Woodridge at Grand Central Park", 3.6, "Low-rise", 2021, 288, 1579.02, 1487.34, 1.58, 0.903),
("Oasis at Magnolia", 3.8, "Build-for-rent", 2025, 158, 2241.56, 2037.16, 1.87, 0.563),
("The Dominion", 3.8, "Townhouse", 2003, 238, 1603.00, 1563.73, 1.26, 0.954),
("Centro Modern Homes", 5.6, "Build-for-rent", 2022, 84, 2040.79, 1821.67, 1.05, 0.929),
]
r = 4
first_r4 = r
for row in rent_data:
ws4.cell(row=r, column=1, value=row[0])
ws4.cell(row=r, column=2, value=row[1]).number_format="0.0"
ws4.cell(row=r, column=3, value=row[2])
ws4.cell(row=r, column=4, value=row[3]).number_format="0"
ws4.cell(row=r, column=5, value=row[4]).number_format="#,##0"
ws4.cell(row=r, column=6, value=row[5]).number_format="$#,##0"
ws4.cell(row=r, column=7, value=row[6]).number_format="$#,##0"
ws4.cell(row=r, column=8, value=row[7]).number_format="$0.00"
ws4.cell(row=r, column=9, value=row[8]).number_format="0.0%"
r += 1
last_r4 = r-1
avg_row2 = r + 1
ws4.cell(row=avg_row2, column=1, value="Comp-set average").font = BOLD
ws4.cell(row=avg_row2, column=7, value=f"=AVERAGE(G{first_r4}:G{last_r4})").number_format="$#,##0"
ws4.cell(row=avg_row2, column=7).font = BOLD
ws4.cell(row=avg_row2+1, column=1, value="Subject, in-place actual")
c=ws4.cell(row=avg_row2+1, column=7, value=2013.79); c.font = BLUE; c.number_format="$#,##0"
c=ws4.cell(row=avg_row2+1, column=8, value=1.32); c.font = BLUE; c.number_format="$0.00"
c=ws4.cell(row=avg_row2+1, column=9, value=0.863); c.font = BLUE; c.number_format="0.0%"
set_widths(ws4, [32,13,16,11,9,13,13,12,11])
# ---------- Sheet 5: Income Approach ----------
ws5 = wb.create_sheet("Income Approach")
ws5["A1"] = "Income Approach - Direct Capitalization"
ws5["A1"].font = Font(bold=True, size=14)
ws5.cell(row=3, column=1, value="Line"); ws5.cell(row=3, column=2, value="Stabilized (Yr 1)")
style_header(ws5, 3, 1, 2)
r = 4
ws5.cell(row=r, column=1, value="Gross potential rent")
gpr_row = r
ws5.cell(row=r, column=2, value=f"='{A}'!B9*(1+'{A}'!B14)").font=GREEN
ws5.cell(row=r, column=2).number_format = "$#,##0"
r += 1
ws5.cell(row=r, column=1, value="Vacancy + collection loss")
vac_row = r
ws5.cell(row=r, column=2, value=f"=-B{gpr_row}*'{A}'!B11").font=GREEN
ws5.cell(row=r, column=2).number_format = "$#,##0"
r += 1
ws5.cell(row=r, column=1, value="Other income")
oi_row = r
ws5.cell(row=r, column=2, value=f"='{A}'!B12*(1+'{A}'!B14)").font=GREEN
ws5.cell(row=r, column=2).number_format = "$#,##0"
r += 1
ws5.cell(row=r, column=1, value="Effective gross income").font=BOLD
egi_row = r
ws5.cell(row=r, column=2, value=f"=B{gpr_row}+B{vac_row}+B{oi_row}").font=Font(bold=True)
ws5.cell(row=r, column=2).number_format = "$#,##0"
r += 1
ws5.cell(row=r, column=1, value="Operating expenses")
opex_row = r
ws5.cell(row=r, column=2, value=f"='{A}'!B13*(1+'{A}'!B15)").font=GREEN
ws5.cell(row=r, column=2).number_format = "$#,##0"
r += 1
ws5.cell(row=r, column=1, value="Net operating income").font=BOLD
noi_row = r
ws5.cell(row=r, column=2, value=f"=B{egi_row}-B{opex_row}").font=Font(bold=True)
ws5.cell(row=r, column=2).number_format = "$#,##0"
r += 1
ws5.cell(row=r, column=1, value="Going-in cap rate")
cap_row = r
ws5.cell(row=r, column=2, value=f"='{A}'!B18").font=GREEN
ws5.cell(row=r, column=2).number_format = "0.00%"
r += 1
ws5.cell(row=r, column=1, value="Direct cap value").font=BOLD
val_row = r
ws5.cell(row=r, column=2, value=f"=B{noi_row}/B{cap_row}").font=Font(bold=True, color="008000")
ws5.cell(row=r, column=2).number_format = "$#,##0"
r += 1
ws5.cell(row=r, column=1, value="Value per unit")
vpu_row = r
ws5.cell(row=r, column=2, value=f"=B{val_row}/'{A}'!B4").font=GREEN
ws5.cell(row=r, column=2).number_format = "$#,##0"
r += 1
ws5.cell(row=r, column=1, value="Value per sqft")
ws5.cell(row=r, column=2, value=f"=B{val_row}/('{A}'!B4*'{A}'!B6)").font=GREEN
ws5.cell(row=r, column=2).number_format = "$#,##0.00"
set_widths(ws5, [30, 20])
# stash row refs for later sheets
row_refs = dict(gpr=gpr_row, vac=vac_row, oi=oi_row, egi=egi_row, opex=opex_row, noi=noi_row, cap=cap_row, val=val_row, vpu=vpu_row)
# ---------- Sheet 6: Sensitivity ----------
ws6 = wb.create_sheet("Sensitivity")
ws6["A1"] = "Sensitivity - Going-In Cap Rate vs. Stabilized NOI"
ws6["A1"].font = Font(bold=True, size=14)
ws6["A3"] = "Value ($), rows = cap rate, columns = NOI sensitivity"
ws6["A3"].font = BOLD
# put NOI multiplier assumptions as labeled inputs
ws6["F5"] = "NOI sensitivity inputs"
ws6["F5"].font = BOLD
ws6["F6"] = "Downside"; ws6["G6"] = -0.05
ws6["F7"] = "Base"; ws6["G7"] = 0.0
ws6["F8"] = "Upside"; ws6["G8"] = 0.05
for rr in (6,7,8):
ws6.cell(row=rr, column=7).font = BLUE
ws6.cell(row=rr, column=7).number_format = "0%"
ws6["F10"] = "Cap rate step (bps)"
ws6["F10"].font = BOLD
ws6["G10"] = 0.0025
ws6["G10"].font = BLUE
ws6["G10"].number_format = "0.00%"
ws6["B5"] = "NOI Downside"
ws6["C5"] = "NOI Base"
ws6["D5"] = "NOI Upside"
for c in ["B5","C5","D5"]:
ws6[c].font = BOLD
ws6[c].alignment = Alignment(horizontal="center")
cap_base_ref = f"'Income Approach'!B{row_refs['cap']}"
noi_base_ref = f"'Income Approach'!B{row_refs['noi']}"
steps = [-2, -1, 0, 1, 2]
for i, step in enumerate(steps):
row = 6+i
ws6.cell(row=row, column=1, value=f"={cap_base_ref}+({step}*$G$10)")
ws6.cell(row=row, column=1).number_format = "0.00%"
ws6.cell(row=row, column=1).font = BLACK
for j, col_letter in enumerate(["G6","G7","G8"]):
col = 2+j
formula = f"=({noi_base_ref}*(1+${col_letter[0]}${col_letter[1:]}))/$A{row}"
cell = ws6.cell(row=row, column=col, value=formula)
cell.number_format = "$#,##0"
cell.font = BLACK
set_widths(ws6, [14,16,16,16,4,20,10])
# ---------- Sheet 7: Reconciliation ----------
ws7 = wb.create_sheet("Reconciliation")
ws7["A1"] = "Methodology Reconciliation"
ws7["A1"].font = Font(bold=True, size=14)
headers3 = ["Methodology", "Value", "Dollars per Unit", "Implied Cap Rate", "Role"]
for i, h in enumerate(headers3, start=1):
ws7.cell(row=3, column=i, value=h)
style_header(ws7, 3, 1, len(headers3))
# Sales comp: BFR premium input as labeled cell
ws7["G3"] = "BFR premium $/unit input"
ws7["G3"].font = BOLD
ws7["H3"] = 173000
ws7["H3"].font = BLUE
ws7["H3"].number_format = "$#,##0"
ws7.cell(row=4, column=1, value="Income approach: direct cap on stabilized NOI")
ws7.cell(row=4, column=2, value=f"='Income Approach'!B{row_refs['val']}").font=GREEN
ws7.cell(row=4, column=2).number_format="$#,##0"
ws7.cell(row=4, column=3, value=f"='Income Approach'!B{row_refs['vpu']}").font=GREEN
ws7.cell(row=4, column=3).number_format="$#,##0"
ws7.cell(row=4, column=4, value=f"='Income Approach'!B{row_refs['cap']}").font=GREEN
ws7.cell(row=4, column=4).number_format="0.00%"
ws7.cell(row=4, column=5, value="Primary")
ws7.cell(row=5, column=1, value="Sales comparison: adjusted dollars per unit, BFR premium to garden comps")
ws7.cell(row=5, column=2, value=f"=H3*'{A}'!B4").font=GREEN
ws7.cell(row=5, column=2).number_format="$#,##0"
ws7.cell(row=5, column=3, value="=H3").font=GREEN
ws7.cell(row=5, column=3).number_format="$#,##0"
ws7.cell(row=5, column=4, value=f"='Income Approach'!B{row_refs['noi']}/B5").font=GREEN
ws7.cell(row=5, column=4).number_format="0.00%"
ws7.cell(row=5, column=5, value="Supporting, thin and stale comp set")
ws7.cell(row=6, column=1, value="Replacement cost")
ws7.cell(row=6, column=2, value="n/a")
ws7.cell(row=6, column=3, value="n/a")
ws7.cell(row=6, column=4, value="n/a")
ws7.cell(row=6, column=5, value="Considered, skipped: 2018-built asset trading below build cost is expected, not a basis flag")
set_widths(ws7, [48,16,16,16,50,4,22,12])
wb.save("output/Amber_Pines_Valuation.xlsx")
print("saved full workbook", row_refs)
PYEOF
python3 /tmp/build_wb_full.pycd /vercel/sandbox && python skills/xlsx/scripts/workbook_integrity_scan.py output/Amber_Pines_Valuation.xlsx --json /tmp/integrity2.json; echo "EXIT:$?"; python3 -c "
import json
d=json.load(open('/tmp/integrity2.json'))
print('high:',d['high_severity_count'],'advisory:',d['advisory_count'])
for f in d['findings']:
if f['severity']=='high':
print(f['sheet'], f['cell'], f['category'], f.get('reason'))
"cat > /tmp/fix_wb.py << 'PYEOF'
import openpyxl
from openpyxl.styles import Font
wb = openpyxl.load_workbook("output/Amber_Pines_Valuation.xlsx")
BLUE = Font(color="0000FF")
GREEN = Font(color="008000")
BLACK = Font(color="000000")
A = "Assumptions"
wsA = wb[A]
# add two new input rows for total rentable sqft and RE tax at bottom
wsA["A23"] = "Property Detail Inputs"
wsA["A23"].font = Font(bold=True)
wsA["A24"] = "Total rentable sqft"
c = wsA["B24"]; c.value = 194804; c.font = BLUE; c.number_format = "#,##0"
wsA["C24"] = "property data"
wsA["A25"] = "RE tax embedded in budget OpEx (annual)"
c = wsA["B25"]; c.value = 980004; c.font = BLUE; c.number_format = "$#,##0"
wsA["C25"] = "uploaded budget"
wsA["A26"] = "Budgeted EGI (FY2026)"
c = wsA["B26"]; c.value = 3285912; c.font = BLUE; c.number_format = "$#,##0"
wsA["C26"] = "uploaded budget"
ws2 = wb["Fundamentals"]
ws2["B5"] = f"='{A}'!B24"
ws2["B5"].font = GREEN
ws2["B5"].number_format = "#,##0"
ws2["B16"] = f"='{A}'!B25"
ws2["B16"].font = GREEN
ws2["B16"].number_format = "$#,##0"
ws2["B17"] = f"=B16/'{A}'!B26"
ws2["B17"].font = BLACK
ws2["B17"].number_format = "0.0%"
# fix reconciliation C4 to compute inline from value / units, not linking to a differently-labeled cell
ws7 = wb["Reconciliation"]
ws7["C4"] = f"=B4/'{A}'!B4"
ws7["C4"].font = BLACK
ws7["C4"].number_format = "$#,##0"
# widen narrow columns
wsA.column_dimensions["A"].width = 60
ws2.column_dimensions["A"].width = 60
ws7.column_dimensions["A"].width = 52
# unify font sizes to 10 for assumption source notes (avoid font_consistency advisory)
for row in wsA.iter_rows():
for cell in row:
if cell.font and cell.font.italic:
cell.font = Font(italic=True, size=10, color="666666")
# lock anchors: Assumptions!B4 references in Fundamentals col B, Income Approach!B9(noi) refs, Sensitivity refs
ws2_ = wb["Fundamentals"]
for r in range(3, 20):
cell = ws2_.cell(row=r, column=2)
if isinstance(cell.value, str) and cell.value.startswith("=") and f"{A}'!B4" in cell.value:
cell.value = cell.value.replace(f"{A}'!B4", f"{A}'!B$4")
ws5 = wb["Income Approach"]
for r in range(4, 13):
cell = ws5.cell(row=r, column=2)
if isinstance(cell.value, str) and cell.value.startswith("=") and "B9" in cell.value and "Income Approach" not in cell.value:
pass
# noi_row = 9 in Income Approach; lock references to it
for r in range(4, 13):
cell = ws5.cell(row=r, column=2)
if isinstance(cell.value, str) and cell.value.startswith("="):
cell.value = cell.value.replace("B9", "B$9") if "B9" in cell.value and r != 9 else cell.value
ws6 = wb["Sensitivity"]
for r in range(6, 11):
cell = ws6.cell(row=r, column=1)
if isinstance(cell.value, str):
cell.value = cell.value.replace("B10", "B$10")
wb.save("output/Amber_Pines_Valuation.xlsx")
print("fixed")
PYEOF
python3 /tmp/fix_wb.pycd /vercel/sandbox && python skills/xlsx/scripts/workbook_integrity_scan.py output/Amber_Pines_Valuation.xlsx --json /tmp/integrity3.json; echo "EXIT:$?"; python3 -c "
import json
d=json.load(open('/tmp/integrity3.json'))
print('high:',d['high_severity_count'],'advisory:',d['advisory_count'])
for f in d['findings']:
if f['severity']=='high':
print(f['sheet'], f['cell'], f['category'], f.get('reason'))
"
python skills/xlsx/scripts/recalc.py output/Amber_Pines_Valuation.xlsxGate passes with zero errors. Now let me read back the recalculated headline figures for citations.
cd /vercel/sandbox && python3 -c "
import openpyxl
wb = openpyxl.load_workbook('output/Amber_Pines_Valuation.xlsx', data_only=True)
ia = wb['Income Approach']
for r in range(4,13):
print(r, ia.cell(row=r,column=1).value, ia.cell(row=r,column=2).value)
print('---sensitivity---')
sens = wb['Sensitivity']
for r in range(6,11):
print([sens.cell(row=r,column=c).value for c in range(1,5)])
print('---reconciliation---')
rec = wb['Reconciliation']
for r in range(4,7):
print([rec.cell(row=r,column=c).value for c in range(1,6)])
"
Acquisition-defensible value of $21.5M – $25.3M (central $23.4M, ~$189K/unit) at confidence Medium — anchored on direct cap of stabilized NOI, pulled down by a comp set that runs 15-18% below it and by a live occupancy shock the budget hasn't priced in yet.
The read is straightforward but the number is soft: physical occupancy is 86.3% today against 92.7% for The Woodlands submarket and 97.6% just three months ago — a real air-pocket, not seasonality, confirmed independently by the rent roll and the platform's weekly rent index. Eighty-five percent of occupied leases expire in the next 12 months, so the property's near-term trajectory hinges on renewal execution, not just re-leasing the 16 vacant units.
What would tighten this: a signed month-over-month rent roll for September/October showing whether the 16 vacant units are re-leasing at the $2,049 average market rent the rent roll implies, or whether concessions are creeping in — that single data point resolves more of the range than any comp work could.
Direct cap on a stabilized-basis NOI is primary — this is an income-producing, near-stabilized asset, not a ground-up or heavy renovation story, so a rent-comp-validated cap rate does the work. Sales comparison is demoted to supporting because the Houston MSA has no transacted BFR product and most comps are stale; running it co-primary would overweight thin data. Replacement cost is skipped as a formal methodology — the 2018-built asset should trade below new-build cost, which isn't a red flag worth a build-up. Acquisition posture leans conservative: the central estimate sits below the pure income-approach number to reflect the comp pull and the occupancy risk, not at the income approach's high end.
| Metric | Value |
|---|---|
| Units | 124 |
| Avg Unit Size | 1,571 sqft |
| Total Rentable Sqft | 194,804 |
| Year Built | 2018 |
| Submarket | The Woodlands, Houston TX MSA |
| Asset Class | Multifamily, build-for-rent (single-family style, 3-4BR only) |
| Current Physical Occupancy | 86.3% |
| Submarket Occupancy | 92.7% |
| FY2026 Budgeted NOI | $1.41M1 |
| RE Tax Assessment | $293K/unit |
| Current Annual Tax | $980K2 |
| Last Sale Date | 12/14/2020 |
| Last Sale Price | Not disclosed |
| Implied Cap Rate at Last Sale | Not calculable3 |
This is a 2018-vintage build-for-rent community of large, single-family-style units (avg 1,571 sqft, exclusively 3-4BR) — a product type still thin on transaction data in Houston. In-place rents ($2,014 avg) already sit above the rent roll's own "market rent" column by about 3%, so the leasing weakness isn't a pricing problem; it's a fill problem tied to lease expirations concentrated in the next 12 months.
| Property | Distance | Sale Date | Units | Year Built | Sale Price | $/Unit |
|---|---|---|---|---|---|---|
| Encore at Westfork | 6.1 mi | 6/22/2026 | 222 | 2017 | $29,925,000 | $134,797 |
| Wildwood Forest | 9.6 mi | 1/29/2025 | 252 | 1997 (renov. 2018) | $43,167,810 | $171,301 |
| Berkshire Jones Forest | 1.7 mi | 3/31/20201 | 416 | 2015 | $56,968,800 | $136,944 |
| Enclave at Woodland Lakes | 1.8 mi | 11/19/20211 | 340 | 2017 | $53,870,320 | $158,442 |
| Woodridge at Grand Central Park | 3.6 mi | 8/30/20221 | 288 | 2021 | $52,238,410 | $181,383 |
| The Mill Apartments | 9.5 mi | 8/18/20211 | 201 | 2020 | $31,250,000 | $155,473 |
Six comps span $135K-$181K/unit, averaging roughly $156K — but none are true build-for-rent product, and four of six are 3.5+ years stale. Subject would price above this range given its larger, newer, single-family-style units; the Reconciliation sheet applies a $173,000/unit premium to reflect that, still landing well under the income approach.
| Metric | Stabilized (Yr 1) |
|---|---|
| Operating Statement | |
| Gross potential rent | $3,297,489 |
| Vacancy and collection loss | ($445,161) |
| Other income | $440,696 |
| Effective gross income | $3,293,024 |
| Operating expenses | $1,927,063 |
| Net operating income | $1,365,961 |
| Valuation | |
| Going-in cap rate | 5.40% |
| Direct cap value | $25,295,579 |
| Value per unit | $203,997 |
The stabilized basis is a 92% occupancy assumption (submarket benchmark) applied against the property's own uploaded 2026 budget, not the budget's own occupancy assumption — the budget implicitly assumes closer to 94%, which the current 86.3% actual makes optimistic. The 5.40% cap rate blends Houston's institutional MF rate (5.49%) with the SFR/BFR proxy (5.30%), since no BFR-specific cap rate exists in the market. At $203,997/unit, this reads well above every sale comp — the widest single driver of the range.
The adjusted $173,000/unit applied to subject (a premium over the $156K comp average, reflecting newer vintage and larger, single-family-style units) produces $21.45M — about 15% below the income approach. That gap is the honest output of thin, stale comp data pricing a still-uncommon product type, not evidence the income approach is wrong; if anything, a confirmed BFR transaction in this submarket would likely narrow the gap upward, not downward.
| Method | Value | $/Unit | Cap Rate | Role |
|---|---|---|---|---|
| Income Approach (Direct Cap on Stabilized NOI) | $25,295,579 | $203,997 | 5.40% | Primary |
| Sales Comparison (BFR-Adjusted) | $21,452,000 | $173,000 | 6.37% | Supporting1 |
| Replacement Cost | n/a | n/a | n/a | Considered — Skipped2 |
This is a read-only snapshot of a RealAI analysis.