Build an excel underwriting model for The Prato at Midtown in Atlanta
Now pulling subject property attributes/financials and market cap rate context.
cd /vercel/sandbox && cat > caprate_payload.json << 'EOF'
{
"metric": {"name": "caprate_ts.multifamily", "units": "%", "family": "capital_markets"},
"subject": {"entity_type": "market", "entity_id": "12060", "label": "Atlanta MSA"},
"horizon": {"years": 10, "intervals": "annual"},
"as_of": "2026-09-11",
"history": [
{"period":"2011-07-01","value":6.33,"source":"datamart"},
{"period":"2011-10-01","value":6.33,"source":"datamart"},
{"period":"2012-01-01","value":6.43,"source":"datamart"},
{"period":"2012-04-01","value":6.43,"source":"datamart"},
{"period":"2012-07-01","value":6.43,"source":"datamart"},
{"period":"2012-10-01","value":6.32,"source":"datamart"},
{"period":"2013-01-01","value":6.31,"source":"datamart"},
{"period":"2013-04-01","value":6.20,"source":"datamart"},
{"period":"2013-07-01","value":6.38,"source":"datamart"},
{"period":"2013-10-01","value":6.56,"source":"datamart"},
{"period":"2014-01-01","value":6.55,"source":"datamart"},
{"period":"2014-04-01","value":6.55,"source":"datamart"},
{"period":"2014-07-01","value":6.23,"source":"datamart"},
{"period":"2014-10-01","value":6.01,"source":"datamart"},
{"period":"2015-01-01","value":5.91,"source":"datamart"},
{"period":"2015-04-01","value":5.79,"source":"datamart"},
{"period":"2015-07-01","value":5.69,"source":"datamart"},
{"period":"2015-10-01","value":5.63,"source":"datamart"},
{"period":"2016-01-01","value":5.64,"source":"datamart"},
{"period":"2016-04-01","value":5.61,"source":"datamart"},
{"period":"2016-07-01","value":5.61,"source":"datamart"},
{"period":"2016-10-01","value":5.61,"source":"datamart"},
{"period":"2017-01-01","value":5.77,"source":"datamart"},
{"period":"2017-04-01","value":5.75,"source":"datamart"},
{"period":"2017-07-01","value":5.75,"source":"datamart"},
{"period":"2017-10-01","value":5.58,"source":"datamart"},
{"period":"2018-01-01","value":5.56,"source":"datamart"},
{"period":"2018-04-01","value":5.52,"source":"datamart"},
{"period":"2018-07-01","value":5.52,"source":"datamart"},
{"period":"2018-10-01","value":5.52,"source":"datamart"},
{"period":"2019-01-01","value":5.44,"source":"datamart"},
{"period":"2019-04-01","value":5.44,"source":"datamart"},
{"period":"2019-07-01","value":4.97,"source":"datamart"},
{"period":"2019-10-01","value":4.97,"source":"datamart"},
{"period":"2020-01-01","value":4.81,"source":"datamart"},
{"period":"2020-04-01","value":5.07,"source":"datamart"},
{"period":"2020-07-01","value":4.66,"source":"datamart"},
{"period":"2020-10-01","value":4.49,"source":"datamart"},
{"period":"2021-01-01","value":4.59,"source":"datamart"},
{"period":"2021-04-01","value":3.90,"source":"datamart"},
{"period":"2021-07-01","value":3.80,"source":"datamart"},
{"period":"2021-10-01","value":3.67,"source":"datamart"},
{"period":"2022-01-01","value":3.83,"source":"datamart"},
{"period":"2022-04-01","value":4.14,"source":"datamart"},
{"period":"2022-07-01","value":4.23,"source":"datamart"},
{"period":"2022-10-01","value":4.99,"source":"datamart"},
{"period":"2023-01-01","value":5.15,"source":"datamart"},
{"period":"2023-04-01","value":5.15,"source":"datamart"},
{"period":"2023-07-01","value":5.79,"source":"datamart"},
{"period":"2023-10-01","value":5.53,"source":"datamart"},
{"period":"2024-01-01","value":5.54,"source":"datamart"},
{"period":"2024-04-01","value":5.28,"source":"datamart"},
{"period":"2024-07-01","value":5.13,"source":"datamart"},
{"period":"2024-10-01","value":5.33,"source":"datamart"},
{"period":"2025-01-01","value":5.34,"source":"datamart"},
{"period":"2025-04-01","value":5.33,"source":"datamart"},
{"period":"2025-07-01","value":5.34,"source":"datamart"},
{"period":"2025-10-01","value":5.33,"source":"datamart"},
{"period":"2026-01-01","value":5.34,"source":"datamart"},
{"period":"2026-04-01","value":5.33,"source":"datamart"}
],
"history_check": {"count": 60, "last_value": 5.33},
"context_signals": {
"rate_environment": {"current_rate": 3.66, "projected_rate": 3.4, "projected_as_of": "2028", "note": "SOFR vs FOMC median fed funds projection"}
},
"caller_disclosures": {"rate_instrument": "SOFR (current) vs FOMC median fed funds projection (2028, furthest available)"},
"scenarios": ["base","upside","downside"]
}
EOF
python skills/forecasting/scripts/forecast.py --file caprate_payload.json > caprate_result.json
cat caprate_result.jsoncd /vercel/sandbox && cat > expense_payload.json << 'EOF'
{
"metric": {"name": "mf_property_financials.mf_pnl_total_operating_expenses", "units": "$", "family": "operating"},
"subject": {"entity_type": "property", "entity_id": "b04253aa9247850584aef754fcdb72c8", "label": "The Prato at Midtown"},
"horizon": {"years": 10, "intervals": "annual"},
"as_of": "2026-09-11",
"history": [
{"period": "2026-06-30", "value": 3777781.09, "source": "datamart"}
],
"history_check": {"count": 1, "last_value": 3777781.09},
"caller_disclosures": {"lookback_note": "single T12 available at property grain; datamart carries no multi-year expense time series (per forecasting skill sourcing rules)"},
"scenarios": ["base","upside","downside"]
}
EOF
python skills/forecasting/scripts/forecast.py --file expense_payload.json > expense_result.json
cat expense_result.jsonAll confirmed as pre-filled. Proceeding to build the model — pulling rent/sale comps and populating the workbook.
Find 6-8 multifamily rental comps for "The Prato at Midtown" at 400 Central Park Pl NE, Atlanta, GA 30308 (property_mfr id b04253aa9247850584aef754fcdb72c8, lat 33.76626223325738, lon -84.37878191471101, 342 units, built 1992, renovated 2011, garden-style, in Midtown Atlanta submarket 7f7153adbd20534a0d342fdd55596f7e, market Atlanta GA 12060). Use the rental-comps skill methodology. For each comp return: name, address, city/state/zip, distance in miles from subject, unit_count, rentable_sf (if available), year_built, year_renovated, overall in-place rent per unit (monthly $), unit counts by bedroom (studio/1br/2br/3br/4br) if available, in-place rent by bedroom type if available, occupancy rate (decimal). Return as a structured JSON list, one object per comp, with null for any unavailable field. Do not fabricate any value.
Identified 8 multifamily rental comps for The Prato at Midtown (342 units, GARDEN-style, built 1992/renovated 2011, 400 Central Park Pl NE, Atlanta GA 30308, Midtown Atlanta submarket).
Methodology: Queried all MARKET-rate property_mfr records in Atlanta census place (1304000) with non-null in-place rent and unit count (323 eligible candidates after retrieval), computed haversine distance from subject coordinates, and filtered to the low-density building-style bucket matching the subject (GARDEN/LOW_RISE/TOWNHOUSE per the rental-comps skill's density grouping), excluding tiny properties (<75 units). All 8 selected comps are unrestricted market-rate properties within 1.3 miles of the subject, with rent data covering studio/1BR/2BR (some also 3BR) unit types, matching the subject's overlap needs. Bedroom-level unit counts are not exposed in the datamart's mf_property_attributes topic (only aggregate unit_count, not a mix breakdown), so those fields are null for all comps — not fabricated.
Selected comps (closest-first):
Subject context for comparison: in-place rent avg $1,372.51/mo (studio $1,115.68, 1BR $1,298.81, 2BR $1,564.36, 3BR $2,342.50), occupancy 95.32%, retention 80.7%, 342 units, 294,804 SF, GARDEN-style, built 1992/renovated 2011. The Prato's in-place rents run notably below all 8 comps (roughly 20-45% below comp average) despite comparable or higher occupancy — consistent with its older vintage/lighter renovation relative to several nearby comps (Highland View, Virginia Highland renovated 2009-2015; AMLI Parkside, Pencil Factory Flats built 2009 as ground-up product without a renovation cycle).
All comp fields returned as-is from the RealAI Rent Index / property_mfr snapshot; bedroom-level unit counts unavailable in the datamart (null, not fabricated).
Find 4-6 multifamily sale comps for "The Prato at Midtown" at 400 Central Park Pl NE, Atlanta, GA 30308 (property_mfr id b04253aa9247850584aef754fcdb72c8, lat 33.76626223325738, lon -84.37878191471101, 342 units, built 1992, renovated 2011, garden-style, Midtown Atlanta submarket, Atlanta GA market id 12060). Use the sales-comps skill methodology. For each comp return: name, address, city/state/zip, distance in miles from subject, unit_count, year_built, year_renovated, sale_date, sale_price, in-place rent per unit at time of sale (monthly $, if available), buyer, seller. Return as a structured JSON list, one object per comp, with null for unavailable fields. Do not fabricate any value.
Sales comps for The Prato at Midtown (400 Central Park Pl NE, Atlanta, GA 30308) were sourced from the RealAI datamart (property_mfr entity: mf_sales_history, mf_property_attributes, mf_rent_and_occupancy_snapshot, mf_ownership_and_management) following the sales-comps skill methodology (zip → census place progressive widening; distance, size, style, vintage, recency ranking). Pool started at zipcode 30308 (25 hits) then widened to Atlanta census place (unit_count ≥100 filter) to reach ≥10 qualifying market-rate multifamily sales. Affordable/senior/student/income-restricted and mixed market+affordable properties were excluded per hard exclusion rule. 6 comps selected, all MARKET rent_type, RENTAL property_type.
Important data caveats (do not treat as fabricated/estimated — these are direct field limitations in the datamart):
Subject reference: 342 units, built 1992, renovated 2011, garden-style, Midtown Atlanta submarket, lat/lon (33.76626, -84.37878).
Comps (JSON): [ { "name": "Axis Midtown", "address": "811 Juniper St NE", "city": "Atlanta", "state": "GA", "zip": "30308", "distance_miles": 0.75, "unit_count": 218, "year_built": 2017, "year_renovated": null, "sale_date": "2023-12-01", "sale_price": 61200000, "sale_price_per_unit": 280733.94, "in_place_rent_per_unit_monthly": 1973.88, "in_place_rent_as_of": "2026-09-05 (current snapshot, not at-sale-date)", "buyer": "BES AXIS MIDTOWN FUND XIV LLC ET AL", "seller": null, "building_style": "MID_RISE" }, { "name": "Trace", "address": "782 Peachtree St NE", "city": "Atlanta", "state": "GA", "zip": "30308", "distance_miles": 0.76, "unit_count": 290, "year_built": 2016, "year_renovated": null, "sale_date": "2025-09-22", "sale_price": 94300000, "sale_price_per_unit": 325172.41, "in_place_rent_per_unit_monthly": 2384.08, "in_place_rent_as_of": "2026-09-05 (current snapshot)", "buyer": "CH REALTY X PENLER MF ATLANTA TRACE LL", "seller": null, "building_style": "HIGH_RISE" }, { "name": "Cortland at Armour Yards", "address": "415 Armour Dr NE", "city": "Atlanta", "state": "GA", "zip": "30324", "distance_miles": 3.15, "unit_count": 372, "year_built": 2008, "year_renovated": 2025, "sale_date": "2025-04-11", "sale_price": 71250000, "sale_price_per_unit": 191532.26, "in_place_rent_per_unit_monthly": 1751.37, "in_place_rent_as_of": "2026-09-05 (current snapshot)", "buyer": "ARMOUR DRIVE GA PARTNERS LLC", "seller": null, "building_style": "MID_RISE" }, { "name": "Collier Trailside", "address": "1391 Collier Rd NW", "city": "Atlanta", "state": "GA", "zip": "30318", "distance_miles": 4.46, "unit_count": 184, "year_built": 2014, "year_renovated": null, "sale_date": "2026-04-29", "sale_price": 30079000, "sale_price_per_unit": 163472.83, "in_place_rent_per_unit_monthly": 1594.39, "in_place_rent_as_of": "2026-09-05 (current snapshot, ~4 months post-sale)", "buyer": "ELYSIAN AT COLLIER LOFTS LP & ELYSIAN AT COLLIER LOFTS TIC LLC", "seller": null, "building_style": "MID_RISE" }, { "name": "Aspire Lenox Park", "address": "1050 Lenox Park Blvd NE", "city": "Atlanta", "state": "GA", "zip": "30319", "distance_miles": 6.16, "unit_count": 407, "year_built": 2000, "year_renovated": 2018, "sale_date": "2024-03-05", "sale_price": 78655768, "sale_price_per_unit": 193257.42, "in_place_rent_per_unit_monthly": 1773.72, "in_place_rent_as_of": "2026-09-05 (current snapshot, not at-sale-date)", "buyer": "1050 LENOX PARK BLVD OWNER LLC", "seller": null, "building_style": "LOW_RISE" }, { "name": "Cielo at West Midtown", "address": "2265 Perry Blvd NW", "city": "Atlanta", "state": "GA", "zip": "30318", "distance_miles": 5.65, "unit_count": 112, "year_built": 1961, "year_renovated": 2016, "sale_date": "2023-07-27", "sale_price": 11400000, "sale_price_per_unit": 101785.71, "in_place_rent_per_unit_monthly": 1306.78, "in_place_rent_as_of": "2026-09-05 (current snapshot, not at-sale-date; property shows only 64% occupancy currently — possible value-add/lease-up context)", "buyer": "TW CIELO OWNER LLC", "seller": null, "building_style": "GARDEN" } ]
Summary read: The two closest comps (Axis Midtown and Trace, both <1 mile, same 30308 zip as subject) traded at $280,733/unit and $325,172/unit respectively — both newer (2016-2017 vintage) mid/high-rise product commanding premium pricing well above what a 1992-vintage garden-style asset like the subject would likely support. Farther comps with older/renovated vintage closer to the subject's profile (Cortland at Armour Yards, $191,532/unit; Aspire Lenox Park, $193,257/unit; Collier Trailside, $163,472/unit) offer a more size/vintage-comparable pricing band of roughly $160K-$195K/unit. Cielo at West Midtown, while style-matched (GARDEN), is a much smaller, much older (1961), currently under-leased asset and should be weighted lightly.
Now populating the workbook per the manifest.
cd /vercel/sandbox && cp "agents/019eb33d-d26e-739e-91bf-0c2b1fe4a447/RealAI_Pro_Forma_Template_v4.xlsx" ./Prato_underwriting.xlsx && python3 -c "
import openpyxl
wb = openpyxl.load_workbook('Prato_underwriting.xlsx')
print(wb.sheetnames)
"
import openpyxl
from datetime import date
wb = openpyxl.load_workbook('Prato_underwriting.xlsx')
A = wb['Assumptions']
RC = wb['Rent Comps']
SC = wb['Sales Comps']
# --- Property Overview ---
A['C5'] = 'The Prato at Midtown'
A['C6'] = '400 Central Park Pl NE'
A['C7'] = 'Atlanta, GA 30308'
A['C8'] = 'Multifamily'
A['C9'] = 342
A['C10'] = 294804
A['C11'] = 1992
A['C12'] = 2011
# --- Acquisition ---
A['C15'] = 52500000
A['C18'] = 0.025 # closing costs, silent default
# --- Disposition ---
A['C22'] = 0.0575 # exit cap rate, forecast-engine synthesized
A['C23'] = 0.02 # disposition costs, area benchmark default
# --- CapEx: blank (stabilized acquisition, no renovation) ---
# --- Reserves ---
A['C32'] = 300 # $/unit/yr, approved AI estimate (industry-standard stabilized reserve)
# --- Growth & Hold Period ---
A['H15'] = 10
A['H16'] = 0.02 # fallback single rate (staged grid governs via H22=Yes)
A['H17'] = 0.035 # expense growth - forecast engine structural terminal rate (operating family, T12 fallback)
A['H18'] = 0.02 # other income growth - approved AI estimate, in line with blended rent growth
A['H19'] = 0.9532 # stabilized occupancy - datamart property level (occupancy_latest)
# --- Year-by-year staged rent growth override (user-specified schedule) ---
A['H22'] = 'Yes'
staged = [0.015, 0.015, 0.02, 0.02, 0.02, 0.025, 0.025, 0.025, 0.025, 0.025]
cols = ['H','I','J','K','L','M','N','O','P','Q']
for c, v in zip(cols, staged):
A[f'{c}26'] = v
# --- T12 Operating Statement (Year 0) ---
A['H33'] = 5679064.09 # GPR
A['H34'] = -289055.91 # vacancy & credit loss (negative convention)
A['H37'] = 1018997.36 # other income
A['H41'] = 1222313.28 # real estate taxes
A['H42'] = 157672.49 # insurance
A['H43'] = 742420.04 # utilities
A['H44'] = 433487.71 # repairs & maintenance
A['H45'] = 160224.78 # management fees
A['H46'] = 817306.73 # payroll & benefits
A['H47'] = 149754.49 # G&A
A['H48'] = 94601.59 # advertising & marketing
# H49 other expenses: not reported - leave blank
# --- Senior Debt Financing ---
A['C36'] = 'Hybrid IO/Amort' # override protected default - user-confirmed structure
A['C38'] = 0.65 # LTV
A['C40'] = 0.0593 # interest rate - Freddie Mac CME 10-yr fixed avg
A['C41'] = 2 # IO period years
A['C42'] = 30 # amortization years
A['C43'] = 10 # loan term years
wb.save('Prato_underwriting.xlsx')
print("Assumptions written")
cd /vercel/sandbox && python3 populate.py
Now the Rent Comps and Sales Comps tabs.
import openpyxl
from datetime import date
wb = openpyxl.load_workbook('Prato_underwriting.xlsx')
RC = wb['Rent Comps']
SC = wb['Sales Comps']
cols = ['D','E','F','G','H','I','J','K']
rent_comps = [
dict(name="AMLI Parkside", address="660 Ralph McGill Blvd NE", csz="Atlanta, GA 30312",
dist=0.74, units=301, sf=269696, yb=2009, yr=None, rent=2334.46,
r0=1741.22, r1=2063.46, r2=2818.33, r3=None, occ=0.9269),
dict(name="Piedmont Pad Apartments", address="171 Auburn Ave NE", csz="Atlanta, GA 30303",
dist=0.78, units=161, sf=160517, yb=2007, yr=None, rent=1657.63,
r0=None, r1=1515.19, r2=1959.60, r3=None, occ=0.8882),
dict(name="Ford Factory Lofts", address="699 Ponce De Leon Ave NE", csz="Atlanta, GA 30308",
dist=0.96, units=123, sf=91512, yb=1914, yr=1985, rent=1766.32,
r0=None, r1=1667.46, r2=2091.14, r3=None, occ=0.9268),
dict(name="Highland View Apartments", address="784 Ponce De Leon Pl NE", csz="Atlanta, GA 30306",
dist=1.05, units=109, sf=90034, yb=1997, yr=2015, rent=1817.00,
r0=1457.07, r1=1751.25, r2=2255.25, r3=None, occ=0.9174),
dict(name="Mariposa Lofts", address="100 Montag Cir", csz="Atlanta, GA 30307",
dist=1.06, units=253, sf=246422, yb=2004, yr=None, rent=2118.47,
r0=1722.03, r1=2058.00, r2=2556.30, r3=None, occ=0.9644),
dict(name="Pencil Factory Flats", address="349 Decatur St SE", csz="Atlanta, GA 30312",
dist=1.16, units=188, sf=205296, yb=2009, yr=None, rent=1778.33,
r0=1390.65, r1=1543.57, r2=2005.33, r3=None, occ=0.8830),
dict(name="Virginia Highland Apartment Homes", address="609 Virginia Ave NE", csz="Atlanta, GA 30306",
dist=1.23, units=270, sf=200880, yb=1988, yr=2009, rent=2011.14,
r0=None, r1=1844.31, r2=2591.85, r3=None, occ=0.8963),
dict(name="Broadstone 2Thirty", address="230 Martin Luther King Jr Dr SE", csz="Atlanta, GA 30312",
dist=1.26, units=278, sf=221566, yb=2022, yr=None, rent=1856.27,
r0=1394.10, r1=1743.13, r2=2289.12, r3=3169.00, occ=0.9317),
]
for col, c in zip(cols, rent_comps):
RC[f'{col}6'] = c['name']
RC[f'{col}7'] = c['address']
RC[f'{col}8'] = c['csz']
RC[f'{col}9'] = c['dist']
RC[f'{col}10'] = c['units']
RC[f'{col}11'] = c['sf']
RC[f'{col}12'] = c['yb']
RC[f'{col}13'] = c['yr']
RC[f'{col}14'] = c['rent']
if c['r0'] is not None:
RC[f'{col}25'] = c['r0']
if c['r1'] is not None:
RC[f'{col}26'] = c['r1']
if c['r2'] is not None:
RC[f'{col}27'] = c['r2']
if c['r3'] is not None:
RC[f'{col}28'] = c['r3']
RC[f'{col}34'] = c['occ']
sale_comps = [
dict(name="Axis Midtown", address="811 Juniper St NE", csz="Atlanta, GA 30308",
dist=0.75, units=218, yb=2017, yr=None, sd=date(2023,12,1), sp=61200000,
rent=1973.88, buyer="BES AXIS MIDTOWN FUND XIV LLC ET AL",
note="RealAI datamart mf_sales_history; in-place rent is current snapshot (2026-09-05), not at-sale"),
dict(name="Trace", address="782 Peachtree St NE", csz="Atlanta, GA 30308",
dist=0.76, units=290, yb=2016, yr=None, sd=date(2025,9,22), sp=94300000,
rent=2384.08, buyer="CH REALTY X PENLER MF ATLANTA TRACE LL",
note="RealAI datamart mf_sales_history; in-place rent is current snapshot"),
dict(name="Cortland at Armour Yards", address="415 Armour Dr NE", csz="Atlanta, GA 30324",
dist=3.15, units=372, yb=2008, yr=2025, sd=date(2025,4,11), sp=71250000,
rent=1751.37, buyer="ARMOUR DRIVE GA PARTNERS LLC",
note="RealAI datamart mf_sales_history; in-place rent is current snapshot"),
dict(name="Collier Trailside", address="1391 Collier Rd NW", csz="Atlanta, GA 30318",
dist=4.46, units=184, yb=2014, yr=None, sd=date(2026,4,29), sp=30079000,
rent=1594.39, buyer="ELYSIAN AT COLLIER LOFTS LP & ELYSIAN AT COLLIER LOFTS TIC LLC",
note="RealAI datamart mf_sales_history; in-place rent ~4mo post-sale snapshot"),
dict(name="Aspire Lenox Park", address="1050 Lenox Park Blvd NE", csz="Atlanta, GA 30319",
dist=6.16, units=407, yb=2000, yr=2018, sd=date(2024,3,5), sp=78655768,
rent=1773.72, buyer="1050 LENOX PARK BLVD OWNER LLC",
note="RealAI datamart mf_sales_history; in-place rent is current snapshot"),
dict(name="Cielo at West Midtown", address="2265 Perry Blvd NW", csz="Atlanta, GA 30318",
dist=5.65, units=112, yb=1961, yr=2016, sd=date(2023,7,27), sp=11400000,
rent=1306.78, buyer="TW CIELO OWNER LLC",
note="RealAI datamart mf_sales_history; only 64% occupancy currently - value-add/lease-up, weight lightly"),
]
n = len(sale_comps)
weight = round(1.0 / n, 4)
for col, c in zip(cols, sale_comps):
SC[f'{col}6'] = c['name']
SC[f'{col}7'] = c['address']
SC[f'{col}8'] = c['csz']
SC[f'{col}9'] = c['dist']
SC[f'{col}10'] = c['units']
SC[f'{col}11'] = c['yb']
SC[f'{col}12'] = c['yr']
SC[f'{col}15'] = c['sd']
SC[f'{col}16'] = c['sp']
SC[f'{col}18'] = c['rent']
SC[f'{col}19'] = c['buyer']
# row 20 (Seller) left blank - not available in datamart
SC[f'{col}21'] = c['note']
# adjustments - no formal appraisal-style adjustment; write explicit 0 to avoid unguarded blank
for r in [24,25,26,27,28]:
SC[f'{col}{r}'] = 0
SC[f'{col}30'] = weight
wb.save('Prato_underwriting.xlsx')
print("Comps written", n, "sale comps, weight", weight)
cd /vercel/sandbox && python3 populate_comps.py
Now recalculate with LibreOffice and validate outputs.
cd /vercel/sandbox && soffice --headless --convert-to xlsx --outdir recalced Prato_underwriting.xlsx 2>&1 | tail -5 && ls -la recalced/
which libreoffice; libreoffice --headless --convert-to xlsx --outdir /vercel/sandbox/recalced /vercel/sandbox/Prato_underwriting.xlsx 2>&1 | tail -20
cd /vercel/sandbox && python3 -c "
import openpyxl
wb = openpyxl.load_workbook('recalced/Prato_underwriting.xlsx', data_only=True)
A = wb['Assumptions']
cells = ['H5','H6','H7','H8','H9','H10','H11','C50','C51','C52','H52','M52']
labels = ['T12 Cap Rate','Yr1 Cap Rate','Yield on Cost','Unlevered IRR','Levered IRR','Avg CoC','Equity Multiple','Going-In DSCR','Debt Yield','Breakeven Occ','T12 NOI','Yr1 NOI']
for l,c in zip(labels,cells):
print(l, c, A[c].value)
print()
SU = wb['Sources & Uses']
for c in ['C9','G9','C11','G11']:
print('S&U', c, SU[c].value)
print()
RC = wb['Rent Comps']
for c in ['C38','C39','C40','C41','C42','C47','C48','C49']:
print('RentComps', c, RC[c].value)
print()
SC = wb['Sales Comps']
for c in ['C34','C35','C36','C37']:
print('SalesComps', c, SC[c].value)
"
cd /vercel/sandbox && python3 -c "
import openpyxl
wb = openpyxl.load_workbook('recalced/Prato_underwriting.xlsx', data_only=True)
PF = wb['Pro Forma']
for row in range(1,60):
vals = [PF.cell(row=row, column=c).value for c in range(1,5)]
if any(v is not None for v in vals):
print(row, vals)
"
cd /vercel/sandbox && python3 -c "
import openpyxl
wb = openpyxl.load_workbook('recalced/Prato_underwriting.xlsx', data_only=True)
A = wb['Assumptions']
for c in ['H33','H34','H35','H36','H37']:
print(c, A[c].value)
"
cd /vercel/sandbox && python3 -c "
import openpyxl
wb = openpyxl.load_workbook('Prato_underwriting.xlsx', data_only=False)
PF = wb['Pro Forma']
for row in [6,7,8,9,10,11]:
for col in ['C','D','E']:
v = PF[f'{col}{row}'].value
if v is not None:
print(col,row,v)
"
cd /vercel/sandbox && python3 -c "
import openpyxl
wb = openpyxl.load_workbook('Prato_underwriting.xlsx', data_only=False)
PF = wb['Pro Forma']
for row in [14,23,24]:
for col in ['D']:
print(col,row,PF[f'{col}{row}'].value)
"
Confirmed. Let me fill the remaining staged grids and re-recalc.
cd /vercel/sandbox && python3 -c "
import openpyxl
wb = openpyxl.load_workbook('Prato_underwriting.xlsx')
A = wb['Assumptions']
cols = ['H','I','J','K','L','M','N','O','P','Q']
for c in cols:
A[f'{c}27'] = 0.9532 # stabilized occupancy held flat across hold
A[f'{c}28'] = 0.02 # other income growth
A[f'{c}29'] = 0.035 # expense growth (forecast engine base case)
wb.save('Prato_underwriting.xlsx')
print('done')
"
rm -rf recalced && libreoffice --headless --convert-to xlsx --outdir /vercel/sandbox/recalced /vercel/sandbox/Prato_underwriting.xlsx 2>&1 | tail -5cd /vercel/sandbox && python3 -c "
import openpyxl
wb = openpyxl.load_workbook('recalced/Prato_underwriting.xlsx', data_only=True)
A = wb['Assumptions']
cells = ['H5','H6','H7','H8','H9','H10','H11','C50','C51','C52','H52','M52']
labels = ['T12 Cap Rate','Yr1 Cap Rate','Yield on Cost','Unlevered IRR','Levered IRR','Avg CoC','Equity Multiple','Going-In DSCR','Debt Yield','Breakeven Occ','T12 NOI','Yr1 NOI']
for l,c in zip(labels,cells):
print(l, c, A[c].value)
print()
SU = wb['Sources & Uses']
for c in ['C9','G9','C11','G11']:
print('S&U', c, SU[c].value)
print()
PF = wb['Pro Forma']
for row in range(1,45):
vals = [PF.cell(row=row, column=c).value for c in range(2,13)]
if any(v is not None for v in vals):
print(row, vals)
"
cd /vercel/sandbox && python3 -c "
import openpyxl
wb = openpyxl.load_workbook('recalced/Prato_underwriting.xlsx', data_only=True)
RS = wb['Returns Summary']
for row in range(1,40):
vals = [RS.cell(row=row, column=c).value for c in range(2,14)]
if any(v is not None for v in vals):
print(row, vals)
"
cd /vercel/sandbox && python3 -c "
import openpyxl
wb = openpyxl.load_workbook('recalced/Prato_underwriting.xlsx', data_only=True)
errs = []
for name in wb.sheetnames:
ws = wb[name]
for row in ws.iter_rows():
for cell in row:
if isinstance(cell.value, str) and cell.value.startswith('#'):
errs.append((name, cell.coordinate, cell.value))
print(len(errs))
for e in errs[:40]:
print(e)
A = wb['Assumptions']
print('H50', A['H50'].value if 'H50' in A else 'n/a')
print('H38', A['H38'].value if 'H38' in A else 'n/a')
for r in range(30,62):
v=A.cell(row=r,column=8).value
lab=A.cell(row=r,column=2).value
if v is not None or lab is not None:
print(r, lab, v)
"
cd /vercel/sandbox && for price in 46000000 42000000 38000000; do
python3 -c "
import openpyxl
wb = openpyxl.load_workbook('Prato_underwriting.xlsx')
wb['Assumptions']['C15'] = $price
wb.save('test_price.xlsx')
"
rm -rf test_recalc
libreoffice --headless --convert-to xlsx --outdir test_recalc test_price.xlsx > /dev/null 2>&1
python3 -c "
import openpyxl
wb = openpyxl.load_workbook('test_recalc/test_price.xlsx', data_only=True)
A = wb['Assumptions']
RS = wb['Returns Summary']
print('Price $price EntryCap', A['H5'].value, ' LevIRR', A['H9'].value, ' EqMult', A['H11'].value, ' AvgDSCR', RS['C26'].value, ' BreakevenOcc', A['C52'].value)
"
donecd /vercel/sandbox && rm -f test_price.xlsx && rm -rf test_recalc && python3 -c "
import openpyxl
wb = openpyxl.load_workbook('recalced/Prato_underwriting.xlsx', data_only=True)
W = wb['Waterfall']
print('Waterfall tie-out C71:', W['C71'].value)
Sum = wb['Summary']
for row in range(1,20):
vals=[Sum.cell(row=row,column=c).value for c in range(2,7)]
if any(v is not None for v in vals):
print(row, vals)
"
All checks pass — 0 error cells, Sources=Uses balanced, Waterfall N/A (promote off, tie-out PASS anyway). Workbook is validated. Saving and presenting.
cd /vercel/sandbox && cp recalced/Prato_underwriting.xlsx ThePratoAtMidtown_underwriting_2026-09-11.xlsx && ls -la ThePratoAtMidtown_underwriting_2026-09-11.xlsx
CONDITIONAL — The Prato at Midtown at $52.5M ($153,509/unit) prices to a 5.01% entry cap on $2.63M T12 NOI vs. a 5.33% Atlanta MSA multifamily cap rate , and the model doesn't clear a lender's DSCR floor at 65% leverage.
At the stated price, terms, and your rent-growth schedule, levered IRR comes back -2.3% with a 0.80x equity multiple over the 10-year hold — expense growth (3.5%/yr, forecast-engine floor since only one T12 is on file) outpaces your rent growth (1.5–2.5%/yr) enough that NOI barely holds flat while debt service steps up when the 2-year IO period rolls off . Average DSCR lands at 1.11x, below the 1.20–1.25x floor most agency lenders require, and breakeven occupancy hits 102% in Year 1 — meaning even full occupancy wouldn't cover debt service and opex as modeled .
Closing note: this verdict turns on two model choices you control — the leverage/price stack and whether to model the rent gap as capturable. Neither is a market-data question; both are worth stress-testing before deciding go/no-go.
| Property | Distance | Units | Year Built | In-Place Rent/Unit | Occupancy |
|---|---|---|---|---|---|
| 0.74 mi | 301 | 2009 | $2,3341 | 92.7%1 | |
| 0.78 mi | 161 | 2007 | $1,658 | 88.8% | |
| 0.96 mi | 123 | 1914 / 1985 renov | $1,766 | 92.7% | |
| 1.05 mi | 109 | 1997 / 2015 renov | $1,817 | 91.7% | |
| 1.06 mi | 253 | 2004 | $2,118 | 96.4% | |
| 1.16 mi | 188 | 2009 | $1,778 | 88.3% | |
| 1.23 mi | 270 | 1988 / 2009 renov | $2,011 | 89.6% | |
| 1.26 mi | 278 | 2022 | $1,856 | 93.2% |
| Property | Sale Date | Distance | Units | Year Built | Sale Price | $/Unit |
|---|---|---|---|---|---|---|
| 2023-12-01 | 0.75 mi | 218 | 2017 | $61.2M | $280,7341 | |
| 2025-09-22 | 0.76 mi | 290 | 2016 | $94.3M | $325,172 | |
| 2025-04-11 | 3.15 mi | 372 | 2008 / 2025 Renov | $71.25M | $191,532 | |
| 2026-04-29 | 4.46 mi | 184 | 2014 | $30.08M | $163,473 | |
| 2024-03-05 | 6.16 mi | 407 | 2000 / 2018 Renov | $78.66M | $193,257 | |
| 2023-07-27 | 5.65 mi | 112 | 1961 / 2016 Renov | $11.4M | $101,7862 |
How it underwrites (read from the model, $52.5M / 65% LTV / 5.93% Freddie Mac CME 10-yr, 2-yr IO / 30-yr amort ):
| Metric | Stabilized |
|---|---|
| Effective Gross Income (T12) | $6,409,006 |
| Total Operating Expenses (T12, 58.9% of EGI) | $3,777,781 |
| Net Operating Income (NOI) | $2,631,224 |
| Going-In Cap Rate | 5.01% |
| Exit Cap Rate | 5.75%1 |
| Year-1 Cash-on-Cash | 2.5% |
| Avg Cash-on-Cash (Hold Period) | 0.7% |
| Average DSCR | 1.11x |
| Debt Yield | 7.7% |
| Breakeven Occupancy (Year 1) | 102% |
| Unlevered IRR | 3.1% |
| Levered IRR | -2.3% |
| Equity Multiple | 0.80x |
Sensitivity — leverage/price is the lever. At the $52.5M ask, average DSCR (1.11x) sits well under the 1.25x floor most agencies want. Dropping to ~$46M (entry cap ~5.72%) clears 1.26x DSCR and flips levered IRR positive; $42M gets to 1.38x DSCR and a 6.2% levered IRR . The rent-gap capture (comp evidence points to $1,700–$1,900/unit achievable, vs. $1,373 in-place) is the other lever and isn't modeled here since you specified stabilized, not value-add.
Bottom line: Pass at $52.5M as structured — the DSCR floor breach isn't a rounding error, it's -2.3% levered IRR and a 100%+ breakeven occupancy. Two paths to Go: reprice toward $46M (or de-lever to ~55-58% LTV) to clear the debt covenant, or come back and re-underwrite this as a value-add play against the $1,700-1,900/unit comp-supported rent range — that's a materially different (and likely more attractive) deal than the one modeled here. Happy to build the value-add version, or run this workbook with a lower price/leverage scenario if you want to see exactly where it turns Go.
This is a read-only snapshot of a RealAI analysis.