Pull recent sales comps from Mecklenburg County records and other available sources to validate a $2.3MM to $2.5MM after-repair value (ARV) exit price for a new luxury single-family home at 611 Reynolds Drive. Then back into the maximum lot or property purchase price that delivers a 15% to 20% gross return. The site is a teardown with an existing home, and I am planning a ~4,500 SF home with combined hard and soft costs of $225/sf, a 6 to 9 month timeline, and no construction financing. Deliver an Excel model and a one-page memo showing the comps that support the ARV, the maximum entry price at 15%, 17.5%, and 20% gross return, and sensitivities to exit price, cost overruns, and timeline.
mkdir -p extracts scripts; cat > extracts/zillow_comps_raw.csv << 'EOF'
address,zpid,lat,lon,beds,full_ba,half_ba,sqft,lot_ac,year_built,price,sold_ms,dom,status,src
215 Hartford Ave,6240371,35.193695,-80.87031,4,4,1,3532,0.24,2020,2100000,1790568000000,3,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
3537 Trent St,459963566,35.189266,-80.86807,5,4,1,3772,0.186,2026,1608300,1790308800000,6,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
1329 Bevis Dr,6243183,35.17598,-80.85368,4,4,2,4954,0.308,2026,3175000,1790222400000,7,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
517 Heather Ln,6243237,35.18336,-80.8603,6,5,0,4863,0.28,2026,2540000,1787889600000,34,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
914 Edinburgh Ln,6240775,35.19302,-80.85505,5,5,1,4573,0.32,2026,3050000,1787198400000,42,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
1436 Townes Rd,6244582,35.184532,-80.84685,6,5,1,5613,0.596,2023,4250000,1787025600000,44,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
439 Melbourne Ct,6240313,35.19588,-80.86552,4,3,2,3759,0.22,2021,1910000,1786420800000,51,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
1241 Heather Ln,6243153,35.174965,-80.85475,4,3,1,3485,0.39,2021,1975000,1785124800000,66,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
3333 Anson St,463061376,35.192352,-80.868324,4,3,1,3522,0.198,2025,2175000,1784692800000,71,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
710 Lochridge Rd,6241855,35.179386,-80.86753,4,5,0,3475,0.203,2026,1622250,1783396800000,86,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
4900 Gilmore Dr,6257217,35.17221,-80.87313,5,4,1,3601,0.3,2026,1400000,1782878400000,92,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
408 Melbourne Ct,6240347,35.19612,-80.86675,4,3,1,3174,0.22,2019,1695000,1782187200000,100,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
1316 Holmes Dr,6243192,35.17641,-80.85331,5,6,1,5150,0.31,2026,2520000,1782187200000,100,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
1437 Montford Dr,6257098,35.17044,-80.851906,4,3,1,4013,0.385,2025,2464450,1780977600000,114,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
725 Hillside Ave,454830381,35.179794,-80.85275,4,3,0,3259,0.19,2025,1550000,1780977600000,114,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
3300 Auburn Ave,6240522,35.191353,-80.86457,5,5,1,4670,0.26,2026,2525000,1780891200000,115,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
621 Poindexter Dr,6240743,35.197132,-80.85903,6,5,1,5570,0.43,2025,1732328,1779854400000,127,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
2739 Idlewood Cres,6243804,35.191895,-80.84891,5,5,2,6103,0.35,2026,4200000,1777608000000,153,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
1020 Habersham Dr,6241117,35.1907,-80.85566,5,5,1,5032,0.27,2025,3010000,1776830400000,162,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
3212 Willow Oak Rd,6244393,35.185413,-80.848816,6,6,1,5433,0.39,2025,4100000,1776052800000,171,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
301 Dover Ave,6240429,35.19441,-80.868546,4,4,1,4159,0.19,2022,1850000,1775534400000,177,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
1708 Paddock Cir,448506632,35.182228,-80.85899,5,4,2,4715,0.322,2026,2695000,1775448000000,178,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
1519 Lynway Dr,6243702,35.198288,-80.84436,6,6,1,6859,0.33,2025,3794276,1775188800000,181,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
5012 Baylor Dr,6257196,35.170586,-80.8737,4,3,1,3880,0.35,2026,1495000,1774929600000,184,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
1400 Heather Ln,6243133,35.17415,-80.85304,5,5,0,4418,0.31,2023,2550000,1774584000000,188,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
1233 Reece Rd,339864351,35.176125,-80.84865,6,4,1,5200,0.17,2024,2112500,1774497600000,189,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
2921 Windsor Ave,6240794,35.191902,-80.85399,5,4,2,4583,0.24,2025,2545000,1773979200000,195,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
1420 Bevis Dr,6243167,35.17527,-80.85237,4,5,0,5073,0.48,2025,3093763,1766552400000,281,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
624 Heather Ln,6243222,35.18246,-80.861176,5,5,1,3881,0.26,2025,1900000,1766120400000,286,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
408 Marsh Rd,6240320,35.196556,-80.86549,6,3,1,3239,0.35,2019,1550000,1765256400000,296,sold,toolu_bdrk_017Foo88ZF1ZzYfU4fpJp524
1109 Wimbledon Rd,6242936,35.17776,-80.85433,6,5,1,5032,0.31,2025,2570000,1764651600000,303,sold,toolu_bdrk_01Y7Rw7M3CrqBCJzmWKQpsg4
1728 Jameston Dr,6244528,35.18334,-80.84477,5,5,2,5632,0.7,2021,3825000,1762491600000,328,sold,toolu_bdrk_01Y7Rw7M3CrqBCJzmWKQpsg4
615 Melbourne Ct,6240571,35.19387,-80.863075,5,4,1,4345,0.25,2023,2272500,1761105600000,344,sold,toolu_bdrk_01Y7Rw7M3CrqBCJzmWKQpsg4
1000 Sewickley Dr,6242752,35.17919,-80.86044,5,4,1,3378,0.33,2021,1585000,1760673600000,349,sold,toolu_bdrk_01Y7Rw7M3CrqBCJzmWKQpsg4
427 Greystone Rd,6240467,35.194664,-80.867165,5,4,1,4275,0.193,2025,1997744,1757044800000,391,sold,toolu_bdrk_01Y7Rw7M3CrqBCJzmWKQpsg4
1401 Heather Ln,6243160,35.174816,-80.853,5,5,2,4474,0.321,2021,2987500,1756958400000,392,sold,toolu_bdrk_01Y7Rw7M3CrqBCJzmWKQpsg4
552 Marsh Rd,6240588,35.1945,-80.86295,5,4,1,4358,0.269,2024,3000000,1756440000000,398,sold,toolu_bdrk_01Y7Rw7M3CrqBCJzmWKQpsg4
722 Hillside Ave,6242424,35.180492,-80.85249,5,5,1,5340,0.44,2025,2964732,1753070400000,437,sold,toolu_bdrk_01Y7Rw7M3CrqBCJzmWKQpsg4
3209 Mayfield Ave #11,450117148,35.19235,-80.86436,5,4,1,4311,0.27,2022,2050000,1750737600000,464,sold,toolu_bdrk_01Y7Rw7M3CrqBCJzmWKQpsg4
680 Ideal Way,6240711,35.199593,-80.857605,4,4,0,3582,0.21,2022,2000000,1749441600000,479,sold,toolu_bdrk_01Y7Rw7M3CrqBCJzmWKQpsg4
1446 Townes Rd,299900862,35.184486,-80.84638,6,6,1,5693,0.37,2021,3675000,1748404800000,491,sold,toolu_bdrk_01Y7Rw7M3CrqBCJzmWKQpsg4
4311 Collingwood Dr,6241508,35.182705,-80.86908,4,4,1,3753,0.2,2025,1800000,1747713600000,499,sold,toolu_bdrk_01Y7Rw7M3CrqBCJzmWKQpsg4
1220 Yale Pl,6244374,35.187267,-80.850365,4,4,0,4101,0.25,2025,2350000,1747713600000,499,sold,toolu_bdrk_01Y7Rw7M3CrqBCJzmWKQpsg4
510 Poindexter Dr,6240268,35.19825,-80.860374,5,4,1,4303,0.22,2021,1937500,1746590400000,512,sold,toolu_bdrk_01Y7Rw7M3CrqBCJzmWKQpsg4
5139 Murrayhill Rd,6256736,35.17046,-80.86825,4,4,1,3274,0.19,2025,1400000,1746158400000,517,sold,toolu_bdrk_01Y7Rw7M3CrqBCJzmWKQpsg4
932 Heather Ln,6242835,35.178658,-80.85783,6,6,1,6753,0.31,2026,4000000,,8,active,toolu_bdrk_01QezSV62gm6dKcWGeWYrgHJ
4647 General Pershing Dr,6242547,35.17576,-80.87188,4,3,1,3452,0.306,2026,1650000,,21,active,toolu_bdrk_01QezSV62gm6dKcWGeWYrgHJ
4214 Waterbury Dr,6243093,35.175262,-80.85772,5,4,0,4214,0.316,2026,2195000,,21,active,toolu_bdrk_01QezSV62gm6dKcWGeWYrgHJ
355 Tranquil Ave,465152015,35.177296,-80.8451,5,5,0,4619,0.15,2026,2750000,,21,active,toolu_bdrk_01QezSV62gm6dKcWGeWYrgHJ
815 Manhasset Rd,6241849,35.178635,-80.86715,5,4,0,4254,0.21,2026,1650000,,48,active,toolu_bdrk_01QezSV62gm6dKcWGeWYrgHJ
3240 Sunset Dr,6241033,35.188896,-80.85786,6,5,1,5241,0.33,2026,2950000,,90,active,toolu_bdrk_01QezSV62gm6dKcWGeWYrgHJ
4448 Halstead Dr,6257074,35.17043,-80.85537,5,4,1,4389,0.393,2026,2475000,,265,active,toolu_bdrk_01QezSV62gm6dKcWGeWYrgHJ
4616 General Pershing Dr,6242522,35.177048,-80.87163,4,4,1,3773,0.2,2027,1695000,,309,active,toolu_bdrk_01QezSV62gm6dKcWGeWYrgHJ
EOF
cat > extracts/zillow_teardowns_raw.csv << 'EOF'
address,zpid,lat,lon,sqft,lot_ac,year_built,price,sold_ms
4200 Murrayhill Rd,6241918,35.18091,-80.86699,1304,0.33,1959,630000,1790654400000
1115 Clawson Ct,6242289,35.185066,-80.85525,1300,0.21,1971,510000,1790222400000
535 Tyson St,6241439,35.188656,-80.86745,1619,0.491,1955,685000,1789444800000
409 Scaleybark Rd,6241631,35.186363,-80.8704,2019,0.27,1956,820000,1788148800000
3633 Conway Ave,6241660,35.1887,-80.8712,1659,0.27,1954,810000,1788148800000
3836 Annlin Ave,6241742,35.183456,-80.86657,1367,0.25,1959,640000,1788148800000
3834 Moultrie St,6241747,35.184284,-80.86723,2360,0.26,1959,875000,1786680000000
1122 Zion Ct,6242305,35.183243,-80.8558,1300,0.227,1971,615000,1786334400000
523 Kenlough Dr,6241978,35.1804,-80.8717,1104,0.244,1960,520000,1785816000000
1137 Sewickley Dr,6242806,35.17779,-80.85811,2411,0.4,1954,805000,1785816000000
3222 Auburn Ave,6240525,35.191757,-80.86407,1133,0.306,1950,825000,1784606400000
4439 Applegate Rd,6242009,35.179546,-80.86968,1219,0.37,1955,607000,1783915200000
609 Kenlough Dr,6241985,35.179398,-80.87079,1287,0.21,1957,530000,1783310400000
1623 Paddock Cir,2074715298,35.182316,-80.85776,1742,0.26,1966,775000,1782964800000
1217 Belrose Ln,6242822,35.176258,-80.85787,1439,0.39,1955,760725,1782100800000
4601 General Pershing Dr,6242538,35.177277,-80.870895,1362,0.32,1955,715000,1781064000000
401 Hollis Rd,6241470,35.190884,-80.86862,1161,0.45,1956,620000,1780977600000
4418 Murrayhill Rd,6241993,35.179085,-80.86956,2018,0.34,1955,745000,1779422400000
620 Shawnee Dr,6241833,35.187237,-80.86578,1544,0.42,1957,760000,1779076800000
820 Manhasset Rd,6242592,35.178467,-80.86641,1493,0.24,1953,632000,1778644800000
912 Sewickley Dr,6242736,35.179295,-80.86188,1617,0.26,1954,787000,1778212800000
4101 Castlewood Rd,6242815,35.17695,-80.859215,1600,0.34,1954,759818,1778212800000
4417 Collingwood Dr,6241518,35.18157,-80.87063,1189,0.224,1956,515000,1778126400000
4210 Murrayhill Rd,6241917,35.180702,-80.867195,1378,0.38,1959,840000,1777953600000
3817 Conway Ave,6241611,35.186966,-80.8731,1855,0.234,1956,805000,1777521600000
4119 Wall St,6241866,35.180164,-80.86508,1922,0.39,1966,800000,1776916800000
1512 Hartford Ave,6241597,35.183167,-80.871124,1228,0.207,1956,539000,1776657600000
1229 Ashcraft Ln,6242987,35.178993,-80.85601,1522,0.26,1959,910000,1776052800000
EOF
cat > scripts/comp_screen.py << 'EOF'
import pandas as pd, numpy as np, datetime as dt
S=(35.184157,-80.864458)
def mi(lat,lon):
R=3958.8; p1,p2=np.radians(S[0]),np.radians(lat); dl=np.radians(lon-S[1]); dp=p2-p1
a=np.sin(dp/2)**2+np.cos(p1)*np.cos(p2)*np.sin(dl/2)**2; return 2*R*np.arcsin(np.sqrt(a))
c=pd.read_csv('extracts/zillow_comps_raw.csv'); c['dist_mi']=mi(c.lat,c.lon).round(2)
c['sold_date']=pd.to_datetime(c.sold_ms,unit='ms').dt.date; c['ppsf']=(c.price/c.sqft).round(0)
c.to_csv('extracts/comps_all.csv',index=False)
sel=c[(c.status=='sold')&(c.year_built>=2023)&(c.sqft.between(3800,5400))&(pd.to_datetime(c.sold_date)>='2025-04-01')&(c.dist_mi<=1.0)&(c.lon<=-80.850)]
print(sel[['address','dist_mi','sqft','lot_ac','year_built','price','ppsf','sold_date','dom']].sort_values('dist_mi').to_string())
print('n',len(sel),'median price',sel.price.median(),'median ppsf',sel.ppsf.median(),'mean ppsf',sel.ppsf.mean().round(0))
print('ppsf p25/p75',sel.ppsf.quantile([.25,.75]).values, 'implied @4500',(sel.ppsf.median()*4500))
sel.to_csv('extracts/arv_comps_selected.csv',index=False)
t=pd.read_csv('extracts/zillow_teardowns_raw.csv'); t['dist_mi']=mi(t.lat,t.lon).round(2); t['sold_date']=pd.to_datetime(t.sold_ms,unit='ms').dt.date
t['per_ac']=(t.price/t.lot_ac).round(0)
tt=t[t.dist_mi<=0.75]
print(tt[['address','dist_mi','sqft','lot_ac','price','per_ac','sold_date']].sort_values('dist_mi').to_string())
print('teardown n',len(tt),'median',tt.price.median(),'lot>=0.33 median',tt[tt.lot_ac>=0.33].price.median())
tt.to_csv('extracts/teardown_comps.csv',index=False)
a=c[c.status=='active']; print(a[['address','dist_mi','sqft','price','ppsf','dom']].to_string())
EOF
python3 scripts/comp_screen.pycat > scripts/build_model.py << 'EOF'
import pandas as pd
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
import datetime as dt
comps = pd.read_csv('extracts/arv_comps_selected.csv').sort_values('dist_mi')
allc = pd.read_csv('extracts/comps_all.csv')
active = allc[allc.status=='active'].sort_values('dist_mi')
land = pd.read_csv('extracts/teardown_comps.csv').sort_values('dist_mi')
verified = {'517 Heather Ln','624 Heather Ln','1400 Heather Ln'}
F='Arial'
BLUE=Font(name=F,size=10,color='0000FF'); BLK=Font(name=F,size=10); BOLD=Font(name=F,size=10,bold=True)
GRN=Font(name=F,size=10,color='008000'); TITLE=Font(name=F,size=13,bold=True)
HDR=Font(name=F,size=10,bold=True,color='FFFFFF'); HFILL=PatternFill('solid',fgColor='1F3864')
YEL=PatternFill('solid',fgColor='FFFF00'); TOP=Border(top=Side(style='thin'))
USD='$#,##0;($#,##0);-'; PCT='0.0%;(0.0%);-'; NUM='#,##0;(#,##0);-'; DATE='yyyy-mm-dd'
wb=Workbook()
def hdr(ws,row,vals,col=1):
for i,v in enumerate(vals):
c=ws.cell(row=row,column=col+i,value=v); c.font=HDR; c.fill=HFILL
c.alignment=Alignment(horizontal='center',vertical='center',wrap_text=True)
def put(ws,ref,val,font=BLK,fmt=None,bold=False):
c=ws[ref]; c.value=val; c.font=Font(name=F,size=10,bold=bold,color=font.color) if bold else font
if fmt: c.number_format=fmt
return c
# ---------------- Inputs
ws=wb.active; ws.title='Inputs'
put(ws,'A1','611 Reynolds Dr, Charlotte NC 28209 - Teardown / Spec Build Model: Inputs',TITLE)
put(ws,'A2','Blue = editable input. Yellow = assumption not provided by user; confirm before relying on it.')
hdr(ws,3,['Input','Value','Basis / source'])
rows=[
('Planned heated SF',4500,NUM,'User',False),
('Hard + soft cost ($/SF, heated)',225,USD,'User. Published Charlotte custom ranges start at $250/SF (see memo).',False),
('Demolition, abatement & site prep ($)',25000,USD,'Assumption; set to 0 if included in $/SF budget',True),
('Cost overrun / contingency (% of hard + soft)',0.0,PCT,'Base = user budget with no contingency; see Sensitivity',False),
('Hold period, acquisition to sale closing (months)',9,NUM,'User range 6-9 months; base uses top of range',False),
('Property tax (annual $)',4749,USD,'Mecklenburg County 2025 bill on existing parcel',False),
("Insurance - builder's risk + liability (annual $)",9000,USD,'Assumption',True),
('Utilities & site upkeep ($/month)',400,USD,'Assumption',True),
('Acquisition closing costs (% of purchase price)',0.0075,PCT,'Assumption (attorney, title, recording)',True),
('Sales commission (% of ARV)',0.05,PCT,'Assumption; set to 0 for a pure gross (pre-sale-cost) return',True),
('Seller closing costs incl. NC excise tax (% of ARV)',0.005,PCT,'Assumption (excise $2 per $1,000 + attorney)',True),
('Marketing & staging ($)',10000,USD,'Assumption',True),
('ARV - Low ($)',2300000,USD,'User range low end',False),
('ARV - Base ($)',2400000,USD,'Midpoint of user range; see Comps for support',False),
('ARV - High ($)',2500000,USD,'User range high end',False),
('Target gross return - 1',0.15,PCT,'User',False),
('Target gross return - 2',0.175,PCT,'User',False),
('Target gross return - 3',0.20,PCT,'User',False),
('Test purchase price ($)',800000,USD,'Illustrative ask; replace with actual offer price',True),
('Lot size (SF)',19076,NUM,'Mecklenburg County assessor (0.44 ac)',False),
('Max building coverage (N1-A, lots 10,000 SF+)',0.40,PCT,'Charlotte UDO Article 4; confirm parcel zoning',False),
('Number of stories (planned)',2,NUM,'Assumption',True),
]
for i,(lab,val,fmt,basis,flag) in enumerate(rows):
r=4+i; put(ws,f'A{r}',lab); c=put(ws,f'B{r}',val,BLUE,fmt); put(ws,f'C{r}',basis)
if flag: c.fill=YEL
ws.column_dimensions['A'].width=50; ws.column_dimensions['B'].width=14; ws.column_dimensions['C'].width=70
# address map
I={k:f'Inputs!$B${4+i}' for i,k in enumerate(['sf','cost','demo','ovr','months','tax','ins','util','acq','comm','sclose','mkt','arvL','arvB','arvH','r1','r2','r3','test','lot','cov','stories'])}
# ---------------- Comps
wc=wb.create_sheet('Comps')
put(wc,'A1','ARV Support - New-Construction Sales within 1.0 mi (built 2023+, 3,800-5,400 SF, sold since Apr-2025)',TITLE)
put(wc,'A2','Source: Zillow recently-sold records (MLS-fed); sale prices for rows marked Yes confirmed against Mecklenburg County deed records (RealAI Datamart).')
cols=['Address','Distance (mi)','Sale date','Sale price ($)','Heated SF','Price per SF ($)','Lot (ac)','Year built','Implied value at subject SF ($)','County deed verified']
hdr(wc,4,cols)
r0=5
for i,(_,x) in enumerate(comps.iterrows()):
r=r0+i
put(wc,f'A{r}',x.address); put(wc,f'B{r}',float(x.dist_mi),BLUE,'0.00')
put(wc,f'C{r}',dt.datetime.strptime(str(x.sold_date),'%Y-%m-%d'),BLUE,DATE)
put(wc,f'D{r}',int(x.price),BLUE,USD); put(wc,f'E{r}',int(x.sqft),BLUE,NUM)
put(wc,f'F{r}',f'=D{r}/E{r}',BLK,USD); put(wc,f'G{r}',float(x.lot_ac),BLUE,'0.00')
put(wc,f'H{r}',str(int(x.year_built)),BLUE); put(wc,f'I{r}',f"=F{r}*{I['sf']}",BLK,USD)
put(wc,f'J{r}','Yes' if x.address in verified else 'No',BLUE)
rN=r0+len(comps)-1
s=rN+2
stats=[('Comp count',f'=COUNT(D{r0}:D{rN})',NUM),
('Median price per SF',f'=MEDIAN(F{r0}:F{rN})',USD),
('Average price per SF',f'=AVERAGE(F{r0}:F{rN})',USD),
('25th percentile price per SF',f'=PERCENTILE(F{r0}:F{rN},0.25)',USD),
('75th percentile price per SF',f'=PERCENTILE(F{r0}:F{rN},0.75)',USD),
('Median $/SF - 5 closest comps (rows sorted by distance)',f'=MEDIAN(F{r0}:F{r0+4})',USD),
('Implied ARV at median $/SF',f"=F{s+1}*{I['sf']}",USD),
('Implied ARV at 25th percentile $/SF',f"=F{s+3}*{I['sf']}",USD),
('Implied ARV at 75th percentile $/SF',f"=F{s+4}*{I['sf']}",USD),
('Implied ARV at 5-closest median $/SF',f"=F{s+5}*{I['sf']}",USD),
('Base ARV ($/SF)',f"={I['arvB']}/{I['sf']}",USD),
('Base ARV percentile rank within comp $/SF',f'=PERCENTRANK(F{r0}:F{rN},F{s+10})',PCT),
('Median sale price (all comps)',f'=MEDIAN(D{r0}:D{rN})',USD),
('Median heated SF (all comps)',f'=MEDIAN(E{r0}:E{rN})',NUM)]
for j,(lab,f,fmt) in enumerate(stats):
put(wc,f'E{s+j}',lab,BOLD); put(wc,f'F{s+j}',f,BLK,fmt,bold=True)
STAT={k:f'Comps!$F${s+j}' for j,k in enumerate(['n','med','avg','p25','p75','med5','arv_med','arv_p25','arv_p75','arv_med5','arvB_psf','rank','medprice','medsf'])}
a0=s+len(stats)+2
put(wc,f'A{a0}','Active new-construction listings within ~1.2 mi (competing supply; asking prices, not closed)',BOLD)
hdr(wc,a0+1,['Address','Distance (mi)','Days listed','Asking price ($)','Heated SF','Asking per SF ($)','Lot (ac)','Year built'])
for i,(_,x) in enumerate(active.iterrows()):
r=a0+2+i
put(wc,f'A{r}',x.address); put(wc,f'B{r}',float(x.dist_mi),BLUE,'0.00'); put(wc,f'C{r}',int(x.dom),BLUE,NUM)
put(wc,f'D{r}',int(x.price),BLUE,USD); put(wc,f'E{r}',int(x.sqft),BLUE,NUM); put(wc,f'F{r}',f'=D{r}/E{r}',BLK,USD)
put(wc,f'G{r}',float(x.lot_ac),BLUE,'0.00'); put(wc,f'H{r}',str(int(x.year_built)),BLUE)
for col,w in zip('ABCDEFGHIJ',[26,12,12,15,52,15,10,10,18,14]): wc.column_dimensions[col].width=w
wc.freeze_panes='A5'
# ---------------- Land comps
wl=wb.create_sheet('Land Comps')
put(wl,'A1','Entry-Price Reality Check - Older-Home (Teardown-Candidate) Sales within 0.75 mi, Apr-Sep 2026',TITLE)
put(wl,'A2','Source: Zillow recently-sold records. Pre-1975 homes on 0.2-0.5 ac lots; land is the value driver.')
hdr(wl,4,['Address','Distance (mi)','Sale date','Sale price ($)','Lot (ac)','Existing home SF','Price per lot acre ($)','Year built'])
l0=5
for i,(_,x) in enumerate(land.iterrows()):
r=l0+i
put(wl,f'A{r}',x.address); put(wl,f'B{r}',float(x.dist_mi),BLUE,'0.00')
put(wl,f'C{r}',dt.datetime.strptime(str(x.sold_date),'%Y-%m-%d'),BLUE,DATE)
put(wl,f'D{r}',int(x.price),BLUE,USD); put(wl,f'E{r}',float(x.lot_ac),BLUE,'0.00'); put(wl,f'F{r}',int(x.sqft),BLUE,NUM)
put(wl,f'G{r}',f'=D{r}/E{r}',BLK,USD); put(wl,f'H{r}',str(int(x.year_built)),BLUE)
lN=l0+len(land)-1; t=lN+2
lst=[('Sale count',f'=COUNT(D{l0}:D{lN})',NUM),('Median sale price',f'=MEDIAN(D{l0}:D{lN})',USD),
('25th percentile sale price',f'=PERCENTILE(D{l0}:D{lN},0.25)',USD),('75th percentile sale price',f'=PERCENTILE(D{l0}:D{lN},0.75)',USD),
('Maximum sale price',f'=MAX(D{l0}:D{lN})',USD),
('Median sale price - lots 0.33 ac and larger',f'=MEDIAN(IF(E{l0}:E{lN}>=0.33,D{l0}:D{lN}))',USD),
('Subject lot (ac)',f"={I['lot']}/43560",'0.00')]
for j,(lab,f,fmt) in enumerate(lst):
put(wl,f'F{t+j}',lab,BOLD); put(wl,f'G{t+j}',f,BLK,fmt,bold=True)
LST={k:f"'Land Comps'!$G${t+j}" for j,k in enumerate(['n','med','p25','p75','max','med33','lotac'])}
for col,w in zip('ABCDEFGH',[26,12,12,15,10,40,18,10]): wl.column_dimensions[col].width=w
wl.freeze_panes='A5'
# ---------------- Model
wm=wb.create_sheet('Model')
put(wm,'A1','Residual Land Value - Maximum Entry Price at Target Gross Return (all-cash, no construction loan)',TITLE)
put(wm,'A2','Gross return = (ARV - selling costs - total project cost) / total project cost. Total project cost includes land, closing, build, demo, carry, marketing.')
hdr(wm,3,['Project cost (excluding land)','Amount ($)'])
lines=[('Hard + soft cost',f"={I['sf']}*{I['cost']}"),('Cost overrun / contingency',f"=B4*{I['ovr']}"),
('Demolition, abatement & site prep',f"={I['demo']}"),('Monthly carry (tax, insurance, utilities)',f"=({I['tax']}+{I['ins']})/12+{I['util']}"),
('Total carry over hold period',f"=B7*{I['months']}"),('Marketing & staging',f"={I['mkt']}"),
('Total project cost excluding land',"=B4+B5+B6+B8+B9"),('Total project cost excluding land, per heated SF',f"=B10/{I['sf']}")]
for i,(lab,f) in enumerate(lines):
r=4+i; put(wm,f'A{r}',lab); c=put(wm,f'B{r}',f,BLK,USD)
for ref in ('A10','B10'): wm[ref].font=BOLD; wm[ref].border=TOP
put(wm,'A11','Total project cost excluding land, per heated SF')
put(wm,'B11',"=B10/"+I['sf'],BLK,USD)
# max price table
put(wm,'A13','Maximum entry price (lot + existing home) by target return and ARV',BOLD)
hdr(wm,14,['Target gross return','ARV - Low','ARV - Base','ARV - High'])
put(wm,'A15','ARV ($)');
for col,k in zip('BCD',['arvL','arvB','arvH']): put(wm,f'{col}15',f"={I[k]}",GRN,USD)
put(wm,'A16','Net sale proceeds after selling costs ($)')
for col in 'BCD': put(wm,f'{col}16',f"={col}15*(1-{I['comm']}-{I['sclose']})",BLK,USD)
for i,k in enumerate(['r1','r2','r3']):
r=17+i; put(wm,f'A{r}',f"={I[k]}",GRN,PCT)
for col in 'BCD': put(wm,f'{col}{r}',f"=({col}$16/(1+$A{r})-$B$10)/(1+{I['acq']})",BLK,USD)
put(wm,'A21','Maximum entry price as % of ARV',BOLD)
for i in range(3):
r=22+i; put(wm,f'A{r}',f'=A{17+i}',BLK,PCT)
for col in 'BCD': put(wm,f'{col}{r}',f'={col}{17+i}/{col}$15',BLK,PCT)
put(wm,'A26','Alternative definition - return before selling costs (commission and seller closing excluded from the return)',BOLD)
hdr(wm,27,['Target gross return','ARV - Low','ARV - Base','ARV - High'])
for i in range(3):
r=28+i; put(wm,f'A{r}',f'=A{17+i}',BLK,PCT)
for col in 'BCD': put(wm,f'{col}{r}',f"=(({col}$15-{I['mkt']})/(1+$A{r})-($B$10-{I['mkt']}))/(1+{I['acq']})",BLK,USD)
put(wm,'A31','Note: this variant also excludes marketing & staging; it shows how much the return definition moves the answer.')
# proof block
put(wm,'A33','Proof - Base ARV at target return 2 (should return exactly the target)',BOLD)
pf=[('Purchase price','=C18'),('Acquisition closing costs',f"=B34*{I['acq']}"),('Project cost excluding land','=B10'),
('Total project cost','=B34+B35+B36'),('Net sale proceeds','=C16'),('Gross profit','=B38-B37'),('Gross return on cost','=B39/B37'),
('Simple annualized return',f"=B40*12/{I['months']}"),('Check: return equals target (1 = yes)','=IF(ABS(B40-A18)<0.00001,1,0)')]
for i,(lab,f) in enumerate(pf):
r=34+i; put(wm,f'A{r}',lab); put(wm,f'B{r}',f,BLK,PCT if r in (40,41) else (NUM if r==42 else USD))
# test price block
put(wm,'A44','Return at test purchase price',BOLD)
hdr(wm,45,['Metric','ARV - Low','ARV - Base','ARV - High'])
tl=[('Test purchase price',lambda c:f"={I['test']}",USD),('Total project cost',lambda c:f"={c}46*(1+{I['acq']})+$B$10",USD),
('Net sale proceeds',lambda c:f'={c}16',USD),('Gross profit',lambda c:f'={c}48-{c}47',USD),
('Gross return on cost',lambda c:f'={c}49/{c}47',PCT),('Simple annualized return',lambda c:f"={c}50*12/{I['months']}",PCT),
('Return before selling costs',lambda c:f'=({c}15-{c}47)/{c}47',PCT)]
for i,(lab,fn,fmt) in enumerate(tl):
r=46+i; put(wm,f'A{r}',lab)
for col in 'BCD': put(wm,f'{col}{r}',fn(col),BLK,fmt)
# breakevens
put(wm,'A54','Break-evens at test purchase price and Base ARV',BOLD)
put(wm,'A55','ARV needed for target return 2 ($)'); put(wm,'B55',f"=(B46*(1+{I['acq']})+B10)*(1+A18)/(1-{I['comm']}-{I['sclose']})",BLK,USD)
put(wm,'A56','ARV needed for target return 2, per heated SF ($)'); put(wm,'B56',f"=B55/{I['sf']}",BLK,USD)
put(wm,'A57','Cost overrun that drops return to target 1 (% of hard + soft)'); put(wm,'B57',f"=((C16/(1+A17)-C46*(1+{I['acq']})-B6-B8-B9)/({I['sf']}*{I['cost']}))-1",BLK,PCT)
put(wm,'A58','Implied all-in hard + soft $/SF at that overrun ($)'); put(wm,'B58',f"={I['cost']}*(1+B57)",BLK,USD)
put(wm,'A59','Cost overrun that drops profit to zero (% of hard + soft)'); put(wm,'B59',f"=((C16-C46*(1+{I['acq']})-B6-B8-B9)/({I['sf']}*{I['cost']}))-1",BLK,PCT)
# market cross-check
put(wm,'A61','Cross-checks',BOLD)
xc=[('Comp-implied ARV at median $/SF',f"={STAT['arv_med']}",USD),('Comp-implied ARV at 25th percentile $/SF',f"={STAT['arv_p25']}",USD),
('Comp-implied ARV at 75th percentile $/SF',f"={STAT['arv_p75']}",USD),
('Median teardown sale within 0.75 mi',f"={LST['med']}",USD),('Median teardown sale, lots 0.33 ac+',f"={LST['med33']}",USD),
('Max entry (Base ARV, target 2) less median teardown sale','=C18-B65',USD),
('Planned first-floor footprint (SF)',f"={I['sf']}/{I['stories']}",NUM),('Max footprint allowed by coverage (SF)',f"={I['lot']}*{I['cov']}",NUM),
('Coverage check (1 = fits)','=IF(B67<=B68,1,0)',NUM)]
for i,(lab,f,fmt) in enumerate(xc):
r=62+i; put(wm,f'A{r}',lab); put(wm,f'B{r}',f,GRN if '!' in f and r<67 else BLK,fmt)
wm.column_dimensions['A'].width=62
for col in 'BCD': wm.column_dimensions[col].width=16
# ---------------- Sensitivity
wsn=wb.create_sheet('Sensitivity')
put(wsn,'A1','Sensitivities - all cells are live formulas driven by the blue axis values and Inputs',TITLE)
put(wsn,'A3','Target return used in tables 1-2'); put(wsn,'B3',f"={I['r2']}",GRN,PCT)
ARVS=[2200000,2300000,2400000,2500000,2600000]; OVR=[0,0.05,0.10,0.15,0.20,0.30]; MON=[6,9,12,15,18]
def proj_ex_land(ovr_ref,mon_ref):
return f"({I['sf']}*{I['cost']}*(1+{ovr_ref})+{I['demo']}+Model!$B$7*{mon_ref}+{I['mkt']})"
NSP=lambda a:f"{a}*(1-{I['comm']}-{I['sclose']})"
# Table 1: max entry ARV x overrun
put(wsn,'A5','1. Maximum entry price ($): ARV (rows) by cost overrun (columns); hold period per Inputs',BOLD)
hdr(wsn,6,['ARV ($)']+['']*len(OVR))
for j,o in enumerate(OVR): put(wsn,f'{get_column_letter(2+j)}6',o,BLUE,'0%'); wsn[f'{get_column_letter(2+j)}6'].fill=PatternFill(None)
put(wsn,'A7','Equivalent hard + soft $/SF')
for j in range(len(OVR)):
cl=get_column_letter(2+j); put(wsn,f'{cl}7',f"={I['cost']}*(1+{cl}$6)",BLK,USD)
for i,a in enumerate(ARVS):
r=8+i; put(wsn,f'A{r}',a,BLUE,USD)
for j in range(len(OVR)):
cl=get_column_letter(2+j)
put(wsn,f'{cl}{r}',f"=({NSP('$A'+str(r))}/(1+$B$3)-{proj_ex_land(cl+'$6',I['months'])})/(1+{I['acq']})",BLK,USD)
# Table 2: max entry ARV x months
put(wsn,'A14','2. Maximum entry price ($): ARV (rows) by hold period in months (columns); overrun per Inputs',BOLD)
hdr(wsn,15,['ARV ($)']+['']*len(MON))
for j,m in enumerate(MON): put(wsn,f'{get_column_letter(2+j)}15',m,BLUE,NUM)
for i,a in enumerate(ARVS):
r=16+i; put(wsn,f'A{r}',a,BLUE,USD)
for j in range(len(MON)):
cl=get_column_letter(2+j)
put(wsn,f'{cl}{r}',f"=({NSP('$A'+str(r))}/(1+$B$3)-{proj_ex_land(I['ovr'],cl+'$15')})/(1+{I['acq']})",BLK,USD)
# Table 3: return at test price ARV x overrun
put(wsn,'A22','3. Gross return at test purchase price: ARV (rows) by cost overrun (columns); hold period per Inputs',BOLD)
put(wsn,'A23','Test purchase price'); put(wsn,'B23',f"={I['test']}",GRN,USD)
hdr(wsn,24,['ARV ($)']+['']*len(OVR))
for j,o in enumerate(OVR): put(wsn,f'{get_column_letter(2+j)}24',o,BLUE,'0%')
for i,a in enumerate(ARVS):
r=25+i; put(wsn,f'A{r}',a,BLUE,USD)
for j in range(len(OVR)):
cl=get_column_letter(2+j); tc=f"($B$23*(1+{I['acq']})+{proj_ex_land(cl+'$24',I['months'])})"
put(wsn,f'{cl}{r}',f"=({NSP('$A'+str(r))}-{tc})/{tc}",BLK,PCT)
# Table 4: annualized return at test price, ARV x months
put(wsn,'A31','4. Simple annualized return at test purchase price: ARV (rows) by hold period in months (columns)',BOLD)
hdr(wsn,32,['ARV ($)']+['']*len(MON))
for j,m in enumerate(MON): put(wsn,f'{get_column_letter(2+j)}32',m,BLUE,NUM)
for i,a in enumerate(ARVS):
r=33+i; put(wsn,f'A{r}',a,BLUE,USD)
for j in range(len(MON)):
cl=get_column_letter(2+j); tc=f"($B$23*(1+{I['acq']})+{proj_ex_land(I['ovr'],cl+'$32')})"
put(wsn,f'{cl}{r}',f"=(({NSP('$A'+str(r))}-{tc})/{tc})*12/{cl}$32",BLK,PCT)
# Table 5: total gross return at test price, ARV x months
put(wsn,'A39','5. Gross return (not annualized) at test purchase price: ARV (rows) by hold period in months (columns)',BOLD)
hdr(wsn,40,['ARV ($)']+['']*len(MON))
for j,m in enumerate(MON): put(wsn,f'{get_column_letter(2+j)}40',m,BLUE,NUM)
for i,a in enumerate(ARVS):
r=41+i; put(wsn,f'A{r}',a,BLUE,USD)
for j in range(len(MON)):
cl=get_column_letter(2+j); tc=f"($B$23*(1+{I['acq']})+{proj_ex_land(I['ovr'],cl+'$40')})"
put(wsn,f'{cl}{r}',f"=({NSP('$A'+str(r))}-{tc})/{tc}",BLK,PCT)
wsn.column_dimensions['A'].width=30
for j in range(2,9): wsn.column_dimensions[get_column_letter(j)].width=14
# header cells with blue axis: restyle headers text color white on fill but values blue? keep fill, white bold is fine for axis -> use blue on light fill
LIGHT=PatternFill('solid',fgColor='D9E1F2')
for row in (6,15,24,32,40):
for j in range(2,2+ (len(OVR) if row in (6,24) else len(MON))):
c=wsn.cell(row=row,column=j); c.font=Font(name=F,size=10,bold=True,color='0000FF'); c.fill=LIGHT
for ws_ in wb.worksheets:
ws_.sheet_view.showGridLines=False
wb.save('outputs/611_Reynolds_Teardown_Model.xlsx')
print('saved')
EOF
mkdir -p outputs && python3 scripts/build_model.py && python skills/xlsx/scripts/workbook_integrity_scan.py outputs/611_Reynolds_Teardown_Model.xlsx --json scratch/integrity.json; echo exit $?; python skills/xlsx/scripts/recalc.py outputs/611_Reynolds_Teardown_Model.xlsxcd /vercel/sandbox && python3 - << 'EOF'
p='scripts/build_model.py'; s=open(p).read()
s=s.replace(""" put(wl,f'G{r}',f'=D{r}/E{r}',BLK,USD); put(wl,f'H{r}',str(int(x.year_built)),BLUE)
lN""",""" put(wl,f'G{r}',f'=D{r}/E{r}',BLK,USD); put(wl,f'H{r}',str(int(x.year_built)),BLUE)
put(wl,f'I{r}',f'=IF(E{r}>=$I$2,D{r},"")',BLK,USD)
lN""")
s=s.replace("hdr(wl,4,['Address','Distance (mi)','Sale date','Sale price ($)','Lot (ac)','Existing home SF','Price per lot acre ($)','Year built'])",
"hdr(wl,4,['Address','Distance (mi)','Sale date','Sale price ($)','Lot (ac)','Existing home SF','Price per lot acre ($)','Year built','Sale price if lot at or above threshold ($)'])\nput(wl,'H2','Large-lot threshold (ac)',BOLD); put(wl,'I2',0.33,BLUE,'0.00'); put(wl,'J2',43560,BLUE,NUM); put(wl,'K2','SF per acre')")
s=s.replace("('Median sale price - lots 0.33 ac and larger',f'=MEDIAN(IF(E{l0}:E{lN}>=0.33,D{l0}:D{lN}))',USD)","('Median sale price - lots at or above threshold (I2)',f'=MEDIAN(I{l0}:I{lN})',USD)")
s=s.replace("""f"={I['lot']}/43560",'0.00')""","""f"={I['lot']}/$J$2",'0.00')""")
s=s.replace("for col,w in zip('ABCDEFGH',[26,12,12,15,10,40,18,10])","for col,w in zip('ABCDEFGHIJK',[26,12,12,15,10,40,18,24,20,10,12])")
s=s.replace("('Check: return equals target (1 = yes)','=IF(ABS(B40-A18)<0.00001,1,0)')","('Check: return equals target (1 = yes)','=IF(ROUND(B40-A18,6)=0,1,0)')")
s=s.replace("""('Gross profit',lambda c:f'={c}48-{c}47',USD),
('Gross return on cost',lambda c:f'={c}49/{c}47',PCT)""","""('Gross profit',lambda c:f'={c}48-{c}$47',USD),
('Gross return on cost',lambda c:f'={c}49/{c}$47',PCT)""")
s=s.replace("('Return before selling costs',lambda c:f'=({c}15-{c}47)/{c}47',PCT)","('Return before selling costs',lambda c:f'=({c}15-{c}$47)/{c}$47',PCT)")
s=s.replace(""" c=wsn.cell(row=row,column=j); c.font=Font(name=F,size=10,bold=True,color='0000FF'); c.fill=LIGHT""",""" c=wsn.cell(row=row,column=j); c.font=Font(name=F,size=10,bold=True,color='0000FF'); c.fill=LIGHT; c.alignment=Alignment(horizontal='right')""")
s=s.replace("wm.column_dimensions['A'].width=62","""for rr in (37,47):
for col in 'ABCD':
wm[f'{col}{rr}'].font=Font(name=F,size=10,bold=True); wm[f'{col}{rr}'].border=TOP
wm.column_dimensions['A'].width=62""")
open(p,'w').write(s)
EOF
python3 scripts/build_model.py && python skills/xlsx/scripts/workbook_integrity_scan.py outputs/611_Reynolds_Teardown_Model.xlsx --json scratch/integrity.json >/dev/null; echo exit $?; python skills/xlsx/scripts/recalc.py outputs/611_Reynolds_Teardown_Model.xlsx; python3 -c "
import json;d=json.load(open('scratch/integrity.json'));print(d['counts_by_category'])"cd /vercel/sandbox && cat > scripts/readback.py << 'EOF'
from openpyxl import load_workbook
wb=load_workbook('outputs/611_Reynolds_Teardown_Model.xlsx',data_only=True)
m=wb['Model']; c=wb['Comps']; l=wb['Land Comps']; s=wb['Sensitivity']
for r in range(4,12): print('Model',r,m[f'A{r}'].value,m[f'B{r}'].value)
for r in range(15,25): print('Model',r,[m[f'{x}{r}'].value for x in 'ABCD'])
for r in range(28,31): print('Model',r,[m[f'{x}{r}'].value for x in 'ABCD'])
for r in range(34,43): print('Model',r,m[f'A{r}'].value,m[f'B{r}'].value)
for r in range(46,53): print('Model',r,m[f'A{r}'].value,[m[f'{x}{r}'].value for x in 'BCD'])
for r in range(55,60): print('Model',r,m[f'A{r}'].value,m[f'B{r}'].value)
for r in range(62,71): print('Model',r,m[f'A{r}'].value,m[f'B{r}'].value)
for r in range(23,37): print('Comps',r,c[f'E{r}'].value,c[f'F{r}'].value)
for r in range(34,41): print('Land',r,l[f'F{r}'].value,l[f'G{r}'].value)
for blk in [(6,13),(15,21),(24,30),(32,38),(40,46)]:
for r in range(*blk): print('Sens',r,[s.cell(row=r,column=j).value for j in range(1,8)])
# independent recompute: base ARV 17.5%
sf,cost,demo=4500,225,25000; carry=(4749+9000)/12+400; months=9; mkt=10000; acq=.0075; sell=.055
fixed=sf*cost+demo+carry*months+mkt
for arv in (2.3e6,2.4e6,2.5e6):
print(arv,[round(((arv*(1-sell))/(1+r)-fixed)/(1+acq)) for r in (.15,.175,.2)])
EOF
python3 scripts/readback.pycd /vercel/sandbox && sed -i "s/('Coverage check (1 = fits)','=IF(B67<=B68,1,0)',NUM)/('Coverage check (1 = fits)','=IF(B68<=B69,1,0)',NUM)/" scripts/build_model.py && python3 scripts/build_model.py && python skills/xlsx/scripts/workbook_integrity_scan.py outputs/611_Reynolds_Teardown_Model.xlsx >/dev/null && python skills/xlsx/scripts/recalc.py outputs/611_Reynolds_Teardown_Model.xlsx && python3 -c "
from openpyxl import load_workbook
m=load_workbook('outputs/611_Reynolds_Teardown_Model.xlsx',data_only=True)['Model'];print(m['B70'].value, m['C18'].value)"Workbook is clean. Now the one-page memo.
cd /vercel/sandbox && cat > scripts/build_memo.py << 'EOF'
from docx import Document
from docx.shared import Inches
from docx.enum.style import WD_STYLE_TYPE
from openpyxl import load_workbook
wb=load_workbook('outputs/611_Reynolds_Teardown_Model.xlsx',data_only=True)
m=wb['Model']; c=wb['Comps']; s=wb['Sensitivity']; l=wb['Land Comps']
k=lambda v: f"${v/1000:,.0f}k"
M=lambda v: f"${v/1e6:,.2f}M"
p=lambda v: f"{v*100:.1f}%"
doc=Document()
for sec in doc.sections:
sec.page_width,sec.page_height=Inches(8.5),Inches(11)
sec.top_margin=sec.bottom_margin=Inches(0.6); sec.left_margin=sec.right_margin=Inches(0.75)
for n in ("Eyebrow","Body Small","Disclaimer"): doc.styles.add_style(n,WD_STYLE_TYPE.PARAGRAPH)
doc.add_paragraph("Teardown spec build screen",style="Eyebrow")
doc.add_paragraph("611 Reynolds Dr, Charlotte NC 28209",style="Title")
doc.add_paragraph(
f"Bottom line: the comps support a {M(2.3e6)}–{M(2.4e6)} exit with confidence; {M(2.5e6)} takes top-of-market finish. "
f"Pay no more than {k(m['C18'].value)} at a {M(2.4e6)} ARV for a 17.5% gross return. That is about {k(m['C18'].value-l['G35'].value)} above "
f"the {k(l['G35'].value)} median teardown sale nearby, so the deal works at market land prices. "
f"The bigger risk is the build budget. Your $225/SF is below every published Charlotte custom-build range, and each 10% overrun "
f"cuts about $100k off the price you can pay.")
doc.add_paragraph("Comps support the ARV",style="Heading 2")
doc.add_paragraph(
f"{c['F23'].value} new builds (2023+, 3,800–5,400 SF) closed within 1.0 mi since Apr-2025. Median {k(c['F24'].value)[:-1]}/SF "
f"→ {M(c['F29'].value)} at 4,500 SF. The 25th percentile gives {M(c['F30'].value)}, and the 5 closest give {M(c['F32'].value)}. "
f"Base ARV of {M(2.4e6)} is {k(c['F33'].value)[:-1]}/SF, the {c['F34'].value*100:.0f}th percentile of the set.",style="Body Small")
t=doc.add_table(rows=1,cols=6); t.style="Table Grid"
for i,h in enumerate(["Comp","Dist (mi)","Closed","Price","SF","$/SF"]): t.rows[0].cells[i].text=h
for r in range(5,11):
row=t.add_row().cells
row[0].text=c[f'A{r}'].value+(" *" if c[f'J{r}'].value=='Yes' else "")
row[1].text=f"{c[f'B{r}'].value:.2f}"; row[2].text=c[f'C{r}'].value.strftime('%b-%y')
row[3].text=M(c[f'D{r}'].value); row[4].text=f"{c[f'E{r}'].value:,}"; row[5].text=f"${c[f'F{r}'].value:,.0f}"
doc.add_paragraph("* Sale price confirmed in Mecklenburg County deed records. The nearest new build, 624 Heather, sat about 9 months and took a $100k cut. "
"Smaller new builds on the streets west of the site (Lochridge, Collingwood) closed at $467–$480/SF, and 8 new-construction listings within 1.2 mi are competing supply.",style="Caption")
doc.add_paragraph("Maximum entry price (lot + existing house)",style="Heading 2")
t2=doc.add_table(rows=1,cols=4); t2.style="Table Grid"
for i,h in enumerate(["Gross return","ARV $2.3M","ARV $2.4M","ARV $2.5M"]): t2.rows[0].cells[i].text=h
for r in (17,18,19):
row=t2.add_row().cells; row[0].text=p(m[f'A{r}'].value)
for j,col in enumerate('BCD'): row[j+1].text=k(m[f'{col}{r}'].value)
doc.add_paragraph(f"All cash. Return = (ARV − 5.5% selling costs − all-in cost) ÷ all-in cost. All-in cost covers land, 0.75% closing, the $1.01M build, "
f"$25k demo, 9 months of carry and $10k marketing. That is {k(m['B10'].value)} before land. If you measure return before selling costs, the 17.5% cap rises to {k(m['C29'].value)}.",style="Caption")
doc.add_paragraph("Sensitivities (max entry price at 17.5%)",style="Heading 2")
t3=doc.add_table(rows=1,cols=6); t3.style="Table Grid"
hd=["ARV"]+[f"{int(s.cell(row=6,column=j).value*100)}% overrun (${s.cell(row=7,column=j).value:,.0f}/SF)" for j in range(2,7)]
for i,h in enumerate(hd): t3.rows[0].cells[i].text=h
for r in (9,10,11):
row=t3.add_row().cells; row[0].text=M(s[f'A{r}'].value)
for j in range(2,7): row[j-1].text=k(s.cell(row=r,column=j).value)
doc.add_paragraph(
f"Timeline: with no construction loan, a slip barely moves the price you can pay. Going from 6 to 18 months costs only "
f"{k(s['B18'].value-s['F18'].value)} at a $2.4M ARV, because carry runs about {k(m['B7'].value*1000/1000)[:-1]}/month. The slip shows up in annualized return instead. "
f"Buying at $800k with a $2.4M exit, the annualized return falls from {p(s['B35'].value)} at 6 months to {p(s['C35'].value)} at 9, {p(s['D35'].value)} at 12 and {p(s['F35'].value)} at 18. "
f"At $800k, you need at least {M(m['B55'].value)} (${m['B56'].value:,.0f}/SF) to clear 17.5%, and a {p(m['B57'].value)} overrun (${m['B58'].value:,.0f}/SF) cuts the return to 15%.",style="Body Small")
doc.add_paragraph("Risks to underwrite",style="Heading 2")
for b in [
"Cost: $225/SF is below published Charlotte custom ranges, which start around $250/SF and run $400+ near Myers Park. Get a fixed-price bid or a GMP before you go hard on the contract.",
"Schedule: 6–9 months is aggressive for 4,500 SF; local builders quote 12–18 months. Plan on 2–3 months of marketing after you finish.",
"Site: the 0.44 ac N1 lot fits the footprint with room under the 40% coverage cap. Confirm zoning, tree-save rules and any utility or grade issues before closing."]:
doc.add_paragraph(b,style="List Bullet")
doc.add_paragraph("Sources: Zillow recently-sold and active listings; Mecklenburg County assessor and deed records (RealAI Datamart); builder cost guides. All figures are from the attached model. This is a screen, not an appraisal.",style="Disclaimer")
doc.save('scratch/memo_plain.docx'); print('ok')
EOF
rm -f scratch/memo-formatted*.docx scratch/memo-final*.docx; python3 scripts/build_memo.py && python skills/docx/scripts/apply_realai_format.py scratch/memo_plain.docx --output scratch/memo-formatted.docx --small-table 0 --small-table 1 --small-table 2 | tail -5 && mkdir -p scratch/qa && rm -f scratch/qa/* && libreoffice --headless --convert-to pdf scratch/memo-formatted.docx --outdir scratch/qa >/dev/null 2>&1; pdfinfo scratch/qa/memo-formatted.pdf | grep Pages; pdftoppm -png -r 110 scratch/qa/memo-formatted.pdf scratch/qa/pagecd /vercel/sandbox && pdftotext -f 2 -l 2 scratch/qa/memo-formatted.pdf - | head -30; python3 -c "
from docx import Document; d=Document('scratch/memo-formatted.docx'); s=d.sections[0]; print(s.top_margin.inches,s.left_margin.inches)"cd /vercel/sandbox && python3 - << 'EOF'
p='scripts/build_memo.py'; s=open(p).read()
s=s.replace("for r in range(5,11):","for r in range(5,10):")
s=s.replace('hd=["ARV"]+[f"{int(s.cell(row=6,column=j).value*100)}% overrun (${s.cell(row=7,column=j).value:,.0f}/SF)" for j in range(2,7)]',
'hd=["ARV / overrun"]+[f"{int(s.cell(row=6,column=j).value*100)}% (${s.cell(row=7,column=j).value:,.0f})" for j in range(2,7)]')
open(p,'w').write(s)
EOF
cat > scripts/memo_margins.py << 'EOF'
import sys
from docx import Document
from docx.shared import Inches
d=Document(sys.argv[1])
for sec in d.sections:
sec.top_margin=sec.bottom_margin=Inches(0.6); sec.left_margin=sec.right_margin=Inches(0.8)
d.save(sys.argv[2])
EOF
rm -f scratch/memo-formatted*.docx scratch/memo-final*.docx scratch/memo-m*.docx scratch/qa/*; python3 scripts/build_memo.py && python skills/docx/scripts/apply_realai_format.py scratch/memo_plain.docx --output scratch/memo-formatted.docx --small-table 0 --small-table 1 --small-table 2 >/dev/null && python3 scripts/memo_margins.py scratch/memo-formatted.docx scratch/memo-m.docx && python skills/docx/scripts/verify_text_unchanged.py scratch/memo_plain.docx scratch/memo-m.docx | tail -2 && libreoffice --headless --convert-to pdf scratch/memo-m.docx --outdir scratch/qa >/dev/null 2>&1; pdfinfo scratch/qa/memo-m.pdf | grep Pages; pdftoppm -png -r 200 scratch/qa/memo-m.pdf scratch/qa/page; ls scratch/qacd /vercel/sandbox && python3 - << 'EOF'
p='scripts/build_memo.py'; s=open(p).read()
s=s.replace("Median {k(c['F24'].value)[:-1]}/SF","Median ${c['F24'].value:,.0f}/SF")
s=s.replace("is {k(c['F33'].value)[:-1]}/SF","is ${c['F33'].value:,.0f}/SF")
s=s.replace("carry runs about {k(m['B7'].value*1000/1000)[:-1]}/month","carry runs only about ${m['B7'].value:,.0f}/month")
s=s.replace("That is {k(m['B10'].value)} before land.","That is {M(m['B10'].value)} before land.")
s=s.replace("""doc.add_paragraph("Risks to underwrite",style="Heading 2")
for b in [
"Cost: $225/SF is below published Charlotte custom ranges, which start around $250/SF and run $400+ near Myers Park. Get a fixed-price bid or a GMP before you go hard on the contract.",
"Schedule: 6–9 months is aggressive for 4,500 SF; local builders quote 12–18 months. Plan on 2–3 months of marketing after you finish.",
"Site: the 0.44 ac N1 lot fits the footprint with room under the 40% coverage cap. Confirm zoning, tree-save rules and any utility or grade issues before closing."]:
doc.add_paragraph(b,style="List Bullet")""","""rp=doc.add_paragraph(style="Body Small"); rr=rp.add_run("Risks to underwrite. "); rr.bold=True
rp.add_run("Cost: $225/SF is below published Charlotte custom ranges, which start around $250/SF and run $400+ near Myers Park; get a fixed-price bid or GMP before going hard. "
"Schedule: builders quote 12–18 months for custom work, so 6–9 months is aggressive; add 2–3 months to sell. "
"Site: the 0.44 ac lot fits a two-story footprint well under the 40% N1-A coverage cap; confirm zoning, tree-save and grading before closing.")""")
open(p,'w').write(s)
p2='scripts/memo_margins.py'; t=open(p2).read().replace("Inches(0.6)","Inches(0.5)").replace("Inches(0.8)","Inches(0.7)"); open(p2,'w').write(t)
EOF
rm -f scratch/memo-formatted*.docx scratch/memo-m*.docx scratch/qa/*; python3 scripts/build_memo.py && python skills/docx/scripts/apply_realai_format.py scratch/memo_plain.docx --output scratch/memo-formatted.docx --small-table 0 --small-table 1 --small-table 2 >/dev/null && python3 scripts/memo_margins.py scratch/memo-formatted.docx scratch/memo-m.docx && python skills/docx/scripts/verify_text_unchanged.py scratch/memo_plain.docx scratch/memo-m.docx | tail -2 && libreoffice --headless --convert-to pdf scratch/memo-m.docx --outdir scratch/qa >/dev/null 2>&1; pdfinfo scratch/qa/memo-m.pdf | grep Pages; pdftoppm -png -r 200 scratch/qa/memo-m.pdf scratch/qa/page; ls scratch/qacd /vercel/sandbox && pdftotext -f 2 -l 2 scratch/qa/memo-m.pdf -
cd /vercel/sandbox && python3 - << 'EOF'
p='scripts/memo_margins.py'; t=open(p).read()
t=t.replace("d.save(sys.argv[2])","""from docx.shared import Pt
d.styles['Title'].font.size=Pt(22)
for n in ('Heading 2',):
st=d.styles[n]; st.font.size=Pt(13); st.paragraph_format.space_before=Pt(8); st.paragraph_format.space_after=Pt(3)
d.save(sys.argv[2])""")
open(p,'w').write(t)
p='scripts/build_memo.py'; s=open(p).read()
s=s.replace("Sources: Zillow recently-sold and active listings; Mecklenburg County assessor and deed records (RealAI Datamart); builder cost guides. All figures are from the attached model. This is a screen, not an appraisal.",
"Sources: Zillow sold/active listings; Mecklenburg County assessor and deed records; builder cost guides. Figures from the attached model. A screen, not an appraisal.")
open(p,'w').write(s)
EOF
rm -f scratch/memo-formatted*.docx scratch/memo-m*.docx scratch/qa/*; python3 scripts/build_memo.py && python skills/docx/scripts/apply_realai_format.py scratch/memo_plain.docx --output scratch/memo-formatted.docx --small-table 0 --small-table 1 --small-table 2 >/dev/null && python3 scripts/memo_margins.py scratch/memo-formatted.docx scratch/memo-m.docx && python skills/docx/scripts/verify_text_unchanged.py scratch/memo_plain.docx scratch/memo-m.docx | tail -2 && libreoffice --headless --convert-to pdf scratch/memo-m.docx --outdir scratch/qa >/dev/null 2>&1; pdfinfo scratch/qa/memo-m.pdf | grep Pages; pdftoppm -png -r 200 scratch/qa/memo-m.pdf scratch/qa/page; ls scratch/qacd /vercel/sandbox && pdftotext -f 2 -l 2 scratch/qa/memo-m.pdf - ; python3 -c "
from docx import Document; d=Document('scratch/memo-m.docx'); print(d.styles['Title'].font.size, [ (p.style.name, p.runs[0].font.size if p.runs else None) for p in d.paragraphs[:3]])"cd /vercel/sandbox && sed -i 's/^doc.add_paragraph("Teardown spec build screen",style="Eyebrow")$//' scripts/build_memo.py && sed -i 's/doc.add_paragraph("611 Reynolds Dr, Charlotte NC 28209",style="Title")/doc.add_paragraph("611 Reynolds Dr teardown: ARV and max entry price",style="Title")/' scripts/build_memo.py && rm -f scratch/memo-formatted*.docx scratch/memo-m*.docx scratch/qa/*; python3 scripts/build_memo.py && python skills/docx/scripts/apply_realai_format.py scratch/memo_plain.docx --output scratch/memo-formatted.docx --small-table 0 --small-table 1 --small-table 2 >/dev/null && python3 scripts/memo_margins.py scratch/memo-formatted.docx scratch/memo-m.docx && python skills/docx/scripts/verify_text_unchanged.py scratch/memo_plain.docx scratch/memo-m.docx | tail -1 && libreoffice --headless --convert-to pdf scratch/memo-m.docx --outdir scratch/qa >/dev/null 2>&1; pdfinfo scratch/qa/memo-m.pdf | grep Pages; pdftoppm -png -r 200 scratch/qa/memo-m.pdf scratch/qa/pagecd /vercel/sandbox && pdftotext -f 2 -l 2 scratch/qa/memo-m.pdf - | head;
cd /vercel/sandbox && sed -i 's/611 Reynolds Dr teardown: ARV and max entry price/611 Reynolds Dr teardown screen/' scripts/build_memo.py && python3 - << 'EOF'
p='scripts/memo_margins.py'; t=open(p).read()
t=t.replace("d.save(sys.argv[2])","""for n in ('Caption','Body Small','Normal'):
try: d.styles[n].paragraph_format.space_after=Pt(3)
except KeyError: pass
d.save(sys.argv[2])""",1)
open(p,'w').write(t)
EOF
rm -f scratch/memo-formatted*.docx scratch/memo-m*.docx scratch/qa/*; python3 scripts/build_memo.py && python skills/docx/scripts/apply_realai_format.py scratch/memo_plain.docx --output scratch/memo-formatted.docx --small-table 0 --small-table 1 --small-table 2 >/dev/null && python3 scripts/memo_margins.py scratch/memo-formatted.docx scratch/memo-m.docx && python skills/docx/scripts/verify_text_unchanged.py scratch/memo_plain.docx scratch/memo-m.docx | tail -1 && libreoffice --headless --convert-to pdf scratch/memo-m.docx --outdir scratch/qa >/dev/null 2>&1; pdfinfo scratch/qa/memo-m.pdf | grep Pages; pdftoppm -png -r 200 scratch/qa/memo-m.pdf scratch/qa/pagecd /vercel/sandbox && python skills/docx/scripts/finalize_metadata.py scratch/memo-m.docx --title "611 Reynolds Dr teardown screen: ARV support and maximum entry price" --subject "Sales comps validating a \$2.3M-\$2.5M ARV and residual land value at 15-20% gross return for a 4,500 SF spec home" --keywords "611 Reynolds Dr; Charlotte; teardown; ARV comps; residual land value; spec build" --output outputs/611_Reynolds_Teardown_Memo.docx
I built the Excel model and a one-page memo. The comps support a $2.3M–$2.4M ARV with confidence. $2.5M is possible but needs top-of-market finish. At a $2.4M exit, the most you can pay for the property and still make 17.5% is about $862k . That is roughly $110k above the median recent sale of older teardown-candidate homes nearby, so the deal works at today's land prices. The bigger threat is your $225/SF budget, not the exit price.
The comps. Seventeen new homes (built 2023 or later, 3,800–5,400 SF) sold within 1 mile since April 2025:
I matched three of these sales against Mecklenburg County deed records:
Your site is a 0.44-acre lot with a 1957 ranch house , and the county values the land alone at $400k .
Maximum entry price (lot plus existing house), all cash
| Gross return | ARV $2.3M | ARV $2.4M | ARV $2.5M |
|---|---|---|---|
| 15% | $822k | $904k | $986k |
| 17.5% | $783k | $862k | $942k |
| 20% | $744k | $822k | $901k |
Return here means profit after about 5.5% selling costs, divided by total cost. Total cost before land is $1.06M : the $1.01M build plus $25k demolition, 9 months of holding costs and marketing. Older homes nearby sold for a median of $752k , with the middle half between $619k and $805k .
What moves the answer
Assumptions to check. These are mine, not yours, and are highlighted yellow on the Inputs sheet:
If you measure return before selling costs, the 17.5% maximum at $2.4M rises to $975k . Zoning, tree-save rules and grading still need to be confirmed with the city.
Files are in your library:
can we add more conditional formatting to the model?
I also want the hard costs and soft costs budget to have another tab that has more line by line item details. You can use placeholders (like you would see in a homebuilder budget) that would add up to the total per sf costs assumed.
Also make the base case Hard + Soft costs $250/sf and add sensitivity grid up showing: Hard Costs per SF ($200/sf to $350/sf in 25/sf increments X axis) Exit Price $1.7mm to $2.5mm ($100k increments) Y axis Interior of grid would be total Profit (assuming you pay $650k base price for lot.
cd /vercel/sandbox && ls outputs extracts scripts && sed -n 1,200p scripts/build_memo.py | grep -n "225\|800k\|1.01M\|above\|100k"
Now the model edits: budget tab, $250 base, new profit grid, and conditional formatting.
cd /vercel/sandbox && python3 - << 'EOF'
p='scripts/build_model.py'; s=open(p).read()
# 1) Inputs: cost becomes link to Budget; test price 650k; add cost floor input
s=s.replace(" ('Hard + soft cost ($/SF, heated)',225,USD,'User. Published Charlotte custom ranges start at $250/SF (see memo).',False),",
" ('Hard + soft cost ($/SF, heated)','=Budget!$C$47',USD,'Linked to Budget tab total (edit line items there). Base case $250/SF per user.',False),")
s=s.replace(" ('Test purchase price ($)',800000,USD,'Illustrative ask; replace with actual offer price',True),",
" ('Lot / property purchase price - base case ($)',650000,USD,'User base case; drives test-price returns and the profit grid',False),")
s=s.replace(" ('Number of stories (planned)',2,NUM,'Assumption',True),\n]",
" ('Number of stories (planned)',2,NUM,'Assumption',True),\n ('Published Charlotte custom-build cost floor ($/SF)',250,USD,'Builder cost guides (2026); cost cell turns orange if below',False),\n]")
s=s.replace("'test','lot','cov','stories'])}","'test','lot','cov','stories','floor'])}")
s=s.replace(""" r=4+i; put(ws,f'A{r}',lab); c=put(ws,f'B{r}',val,BLUE,fmt); put(ws,f'C{r}',basis)""",
""" r=4+i; put(ws,f'A{r}',lab); c=put(ws,f'B{r}',val,GRN if isinstance(val,str) else BLUE,fmt); put(ws,f'C{r}',basis)""")
s=s.replace("ws.column_dimensions['C'].width=70","ws.column_dimensions['C'].width=74")
# 2) Budget sheet inserted after Inputs
budget = '''
# ---------------- Budget
wbud=wb.create_sheet('Budget')
put(wbud,'A1','Construction Budget - Hard and Soft Cost Line Items (placeholders; replace with contractor bids)',TITLE)
put(wbud,'A2','Blue $/SF cells are placeholder allowances typical of a Charlotte infill spec-home budget. Total feeds Inputs!B5. Demo, carry, insurance and taxes are budgeted separately on Inputs.')
hdr(wbud,3,['Line item','Category','Allowance ($/SF heated)','Budget ($)','% of total','Notes'])
HARD=[('Site work, clearing, grading & erosion control',6,'Tree protection, silt fence, rough grade'),
('Footings, foundation & waterproofing',14,'Crawlspace or slab; excludes rock/over-dig'),
('Framing lumber, sheathing & trusses',22,''),('Framing labor',10,''),
('Windows & exterior doors',12,'Clad-wood package'),('Roofing & flashing',6,'Architectural shingle with metal accents'),
('Exterior cladding & trim (brick, stone, siding)',16,''),('Plumbing - rough & finish labor',11,''),
('HVAC (zoned systems)',9,''),('Electrical & low voltage',10,'Incl. pre-wire'),('Insulation (spray foam / batt)',3.5,''),
('Drywall',7,''),('Interior doors, trim & millwork',10,''),('Cabinetry & vanities',11,''),('Countertops',5,'Quartz / quartzite'),
('Tile & stone',7,''),('Hardwood & other flooring',8,''),('Painting - interior & exterior',6,''),
('Appliance package',4,''),('Lighting & plumbing fixtures',5,''),('Shower glass, mirrors, hardware & closets',3,''),
('Fireplace & specialties',1.5,''),('Driveway, flatwork & hardscape',5,''),('Landscaping & irrigation',4,''),
('Garage doors, gutters & misc. exterior',2,''),('Temp utilities, dumpsters, toilets & final clean',3,''),
('Site supervision / general conditions',8,'Superintendent time over the build')]
SOFT=[('Architecture & structural engineering',6,'Plans, structural, energy calcs'),
('Survey, geotech & tree survey',1,''),('Permits, impact & water/sewer tap fees',4,'Charlotte Water taps are a large share'),
('Builder fee / overhead',18,'Set to 0 if you self-perform as GC'),('Interior design & selections',2,''),
('Legal & accounting',1,''),('Warranty reserve',2,'1-year builder warranty'),
('Budget contingency (inside the $/SF)',7,'Separate from the Inputs overrun scenario')]
r=4
put(wbud,f'A{r}','Hard costs',BOLD); r+=1; h0=r
for lab,v,n in HARD:
put(wbud,f'A{r}',lab); put(wbud,f'B{r}','Hard'); put(wbud,f'C{r}',v,BLUE,'$#,##0.00')
put(wbud,f'D{r}',f"=C{r}*{I['sf']}",BLK,USD); put(wbud,f'E{r}',f'=D{r}/$D$47',BLK,PCT); put(wbud,f'F{r}',n); r+=1
h1=r-1; put(wbud,f'A{r}','Subtotal - hard costs',BOLD); put(wbud,f'C{r}',f'=SUM(C{h0}:C{h1})',BLK,'$#,##0.00',bold=True)
put(wbud,f'D{r}',f'=SUM(D{h0}:D{h1})',BLK,USD,bold=True); put(wbud,f'E{r}',f'=D{r}/$D$47',BLK,PCT,bold=True); hs=r
for col in 'ABCDEF': wbud[f'{col}{r}'].border=TOP
r+=2; put(wbud,f'A{r}','Soft costs',BOLD); r+=1; s0=r
for lab,v,n in SOFT:
put(wbud,f'A{r}',lab); put(wbud,f'B{r}','Soft'); put(wbud,f'C{r}',v,BLUE,'$#,##0.00')
put(wbud,f'D{r}',f"=C{r}*{I['sf']}",BLK,USD); put(wbud,f'E{r}',f'=D{r}/$D$47',BLK,PCT); put(wbud,f'F{r}',n); r+=1
s1=r-1; put(wbud,f'A{r}','Subtotal - soft costs',BOLD); put(wbud,f'C{r}',f'=SUM(C{s0}:C{s1})',BLK,'$#,##0.00',bold=True)
put(wbud,f'D{r}',f'=SUM(D{s0}:D{s1})',BLK,USD,bold=True); put(wbud,f'E{r}',f'=D{r}/$D$47',BLK,PCT,bold=True); ss=r
for col in 'ABCDEF': wbud[f'{col}{r}'].border=TOP
assert r==45, r
r=47
put(wbud,f'A{r}','Total hard + soft cost',BOLD); put(wbud,f'C{r}',f'=C{hs}+C{ss}',BLK,'$#,##0.00',bold=True)
put(wbud,f'D{r}',f'=D{hs}+D{ss}',BLK,USD,bold=True); put(wbud,f'E{r}',f'=D{r}/$D$47',BLK,PCT,bold=True)
for col in 'ABCDEF': wbud[f'{col}{r}'].border=Border(top=Side(style='thin'),bottom=Side(style='double'))
put(wbud,'A48','Target hard + soft cost ($/SF, base case)'); put(wbud,'C48',250,BLUE,'$#,##0.00')
put(wbud,'A49','Variance to target ($/SF; 0 = line items tie to target)'); put(wbud,'C49','=C47-C48',BLK,'$#,##0.00;($#,##0.00);-')
put(wbud,'A50','Soft costs as % of hard costs'); put(wbud,'C50',f'=D{ss}/D{hs}',BLK,PCT)
for col,w in zip('ABCDEF',[48,10,16,16,11,44]): wbud.column_dimensions[col].width=w
wbud.freeze_panes='A4'
BUD=(h0,h1,s0,s1)
'''
s=s.replace("# ---------------- Comps", budget+"\n# ---------------- Comps")
# Model label for test price
s=s.replace("put(wm,'A44','Return at test purchase price',BOLD)","put(wm,'A44','Return at base-case purchase price (Inputs)',BOLD)")
s=s.replace("('Test purchase price',lambda c:f\"={I['test']}\",USD)","('Base-case purchase price',lambda c:f\"={I['test']}\",USD)")
s=s.replace("put(wm,'A54','Break-evens at test purchase price and Base ARV',BOLD)","put(wm,'A54','Break-evens at base-case purchase price and Base ARV',BOLD)")
s=s.replace("put(wsn,'A23','Test purchase price')","put(wsn,'A23','Base-case purchase price')")
s=s.replace("'3. Gross return at test purchase price:","'3. Gross return at base-case purchase price:")
s=s.replace("'4. Simple annualized return at test purchase price:","'4. Simple annualized return at base-case purchase price:")
s=s.replace("'5. Gross return (not annualized) at test purchase price:","'5. Gross return (not annualized) at base-case purchase price:")
# 3) Profit grid + return grid + conditional formatting, before gridlines loop
grid = '''
# Table 6: profit grid hard+soft $/SF (cols) x exit price (rows)
PSF=[200,225,250,275,300,325,350]; EXIT=[1700000+100000*i for i in range(9)]
put(wsn,'A48','6. Total gross profit ($): exit price (rows) by hard + soft cost $/SF (columns), at the base-case lot price',BOLD)
put(wsn,'A49','Lot purchase price'); put(wsn,'B49',f"={I['test']}",GRN,USD)
hdr(wsn,50,['Exit price ($)']+['']*len(PSF))
for j,v in enumerate(PSF): put(wsn,f'{get_column_letter(2+j)}50',v,BLUE,USD)
def tc_psf(cl,lotref='$B$49'):
return f"({lotref}*(1+{I['acq']})+{I['sf']}*{cl}$50*(1+{I['ovr']})+{I['demo']}+Model!$B$7*{I['months']}+{I['mkt']})"
for i,a in enumerate(EXIT):
r=51+i; put(wsn,f'A{r}',a,BLUE,USD)
for j in range(len(PSF)):
cl=get_column_letter(2+j); put(wsn,f'{cl}{r}',f"={NSP('$A'+str(r))}-{tc_psf(cl)}",BLK,USD)
put(wsn,'A61','7. Gross return on cost: same axes as table 6 (exit price rows by hard + soft $/SF columns)',BOLD)
hdr(wsn,62,['Exit price ($)']+['']*len(PSF))
for j in range(len(PSF)):
cl=get_column_letter(2+j); put(wsn,f'{cl}62',f'={cl}50',BLUE,USD)
for i in range(len(EXIT)):
r=63+i; put(wsn,f'A{r}',f'=A{51+i}',BLK,USD)
for j in range(len(PSF)):
cl=get_column_letter(2+j)
put(wsn,f'{cl}{r}',f"={cl}{51+i}/({tc_psf(cl).replace(cl+'$50',cl+'$62')})",BLK,PCT)
'''
s=s.replace("wsn.column_dimensions['A'].width=30", grid+"\nwsn.column_dimensions['A'].width=30")
s=s.replace("for row in (6,15,24,32,40):\n for j in range(2,2+ (len(OVR) if row in (6,24) else len(MON))):",
"for row in (6,15,24,32,40,50,62):\n for j in range(2,2+ (len(OVR) if row in (6,24) else (7 if row in (50,62) else len(MON)))):")
cf = '''
# ---------------- Conditional formatting
from openpyxl.formatting.rule import CellIsRule, ColorScaleRule, FormulaRule, DataBarRule
G=PatternFill('solid',fgColor='C6EFCE'); R=PatternFill('solid',fgColor='FFC7CE'); Y=PatternFill('solid',fgColor='FFEB9C'); O=PatternFill('solid',fgColor='F8CBAD')
GF=Font(name=F,size=10,color='006100'); RF=Font(name=F,size=10,color='9C0006'); YF=Font(name=F,size=10,color='9C5700')
TD="'Land Comps'!$G$35"; T1=I['r1']; T2=I['r2']
def vs_land(ws_,rng):
ws_.conditional_formatting.add(rng,CellIsRule(operator='greaterThanOrEqual',formula=[TD],fill=G,font=GF))
ws_.conditional_formatting.add(rng,CellIsRule(operator='lessThan',formula=[TD],fill=R,font=RF))
def vs_target(ws_,rng):
ws_.conditional_formatting.add(rng,CellIsRule(operator='greaterThanOrEqual',formula=[T2],fill=G,font=GF))
ws_.conditional_formatting.add(rng,CellIsRule(operator='between',formula=[T1,T2],fill=Y,font=YF))
ws_.conditional_formatting.add(rng,CellIsRule(operator='lessThan',formula=[T1],fill=R,font=RF))
def check(ws_,rng):
ws_.conditional_formatting.add(rng,CellIsRule(operator='equal',formula=['1'],fill=G,font=GF))
ws_.conditional_formatting.add(rng,CellIsRule(operator='equal',formula=['0'],fill=R,font=RF))
# Inputs: flag cost below published floor
ws.conditional_formatting.add('B5',FormulaRule(formula=[f"$B$5<{I['floor']}"],fill=O))
# Budget: data bars on % of total, check variance
wbud.conditional_formatting.add(f'E{BUD[0]}:E{BUD[1]}',DataBarRule(start_type='num',start_value=0,end_type='max',color='5B9BD5'))
wbud.conditional_formatting.add(f'E{BUD[2]}:E{BUD[3]}',DataBarRule(start_type='num',start_value=0,end_type='max',color='ED7D31'))
wbud.conditional_formatting.add('C49',FormulaRule(formula=['ROUND($C$49,2)=0'],fill=G,font=GF))
wbud.conditional_formatting.add('C49',FormulaRule(formula=['ROUND($C$49,2)<>0'],fill=R,font=RF))
# Comps: $/SF colour scale, flag comps at/above base ARV $/SF
wc.conditional_formatting.add(f'F{r0}:F{rN}',ColorScaleRule(start_type='min',start_color='F8696B',mid_type='percentile',mid_value=50,mid_color='FFEB84',end_type='max',end_color='63BE7B'))
wc.conditional_formatting.add(f'I{r0}:I{rN}',CellIsRule(operator='greaterThanOrEqual',formula=[I['arvB']],fill=G,font=GF))
wc.conditional_formatting.add(f'I{r0}:I{rN}',CellIsRule(operator='lessThan',formula=[I['arvB']],fill=R,font=RF))
wc.conditional_formatting.add(f'J{r0}:J{rN}',CellIsRule(operator='equal',formula=['"Yes"'],fill=G,font=GF))
# Land comps: price colour scale (cheap = green for a buyer)
wl.conditional_formatting.add(f'D{l0}:D{lN}',ColorScaleRule(start_type='min',start_color='63BE7B',mid_type='percentile',mid_value=50,mid_color='FFEB84',end_type='max',end_color='F8696B'))
# Model
vs_land(wm,'B17:D19'); vs_target(wm,'B50:D50'); vs_target(wm,'B52:D52'); check(wm,'B42'); check(wm,'B70')
wm.conditional_formatting.add('B49:D49',CellIsRule(operator='lessThan',formula=['0'],fill=R,font=RF))
wm.conditional_formatting.add('B67',CellIsRule(operator='lessThan',formula=['0'],fill=R,font=RF))
wm.conditional_formatting.add('B67',CellIsRule(operator='greaterThanOrEqual',formula=['0'],fill=G,font=GF))
# Sensitivity
vs_land(wsn,'B8:G12'); vs_land(wsn,'B16:F20'); vs_target(wsn,'B25:G29'); vs_target(wsn,'B41:F45'); vs_target(wsn,'B63:H71')
wsn.conditional_formatting.add('B33:F37',ColorScaleRule(start_type='min',start_color='F8696B',mid_type='percentile',mid_value=50,mid_color='FFEB84',end_type='max',end_color='63BE7B'))
wsn.conditional_formatting.add('B51:H59',ColorScaleRule(start_type='num',start_value=0,start_color='F8696B',mid_type='percentile',mid_value=50,mid_color='FFEB84',end_type='max',end_color='63BE7B'))
wsn.conditional_formatting.add('B51:H59',CellIsRule(operator='lessThan',formula=['0'],font=Font(name=F,size=10,bold=True,color='9C0006')))
put(wsn,'A73','Colour key: tables 1-2 green = max price at or above the median nearby teardown sale (buyable at market), red = below. Tables 3, 5, 7: green = at/above target return 2, yellow = between targets 1 and 2, red = below target 1. Table 6: red-to-green scale on profit, bold red = loss.')
'''
s=s.replace("for ws_ in wb.worksheets:\n ws_.sheet_view.showGridLines=False", cf+"\nfor ws_ in wb.worksheets:\n ws_.sheet_view.showGridLines=False")
open(p,'w').write(s)
EOF
grep -n "r0=5\|rN=" scripts/build_model.py | head; python3 scripts/build_model.py && python skills/xlsx/scripts/workbook_integrity_scan.py outputs/611_Reynolds_Teardown_Model.xlsx --json scratch/integrity.json >/dev/null; echo exit $?; python skills/xlsx/scripts/recalc.py outputs/611_Reynolds_Teardown_Model.xlsx; python3 -c "
import json;d=json.load(open('scratch/integrity.json'));print(d['counts_by_category']);[print(f) for f in d['findings'] if f['category'] not in ('formula_literal',)]"cd /vercel/sandbox && python3 - << 'EOF'
p='scripts/build_model.py'; s=open(p).read()
a=s.index('# ---------------- Budget'); b=s.index('# ---------------- Comps')
blk=s[a:b]
blk=blk.replace('$D$47','$D$45').replace("assert r==45, r\nr=47","assert r==43, r\nr=45")
blk=blk.replace("'A48'","'A46'").replace("'C48'","'C46'").replace("'A49'","'A47'").replace("'C49','=C47-C48'","'C47','=C45-C46'").replace("'A50'","'A48'").replace("'C50'","'C48'")
s=s[:a]+blk+s[b:]
s=s.replace("'=Budget!$C$47'","'=Budget!$C$45'")
s=s.replace("wbud.conditional_formatting.add('C49',FormulaRule(formula=['ROUND($C$49,2)=0'],fill=G,font=GF))\nwbud.conditional_formatting.add('C49',FormulaRule(formula=['ROUND($C$49,2)<>0'],fill=R,font=RF))",
"wbud.conditional_formatting.add('C47',FormulaRule(formula=['ROUND($C$47,2)=0'],fill=G,font=GF))\nwbud.conditional_formatting.add('C47',FormulaRule(formula=['ROUND($C$47,2)<>0'],fill=R,font=RF))")
open(p,'w').write(s)
EOF
python3 scripts/build_model.py && python skills/xlsx/scripts/workbook_integrity_scan.py outputs/611_Reynolds_Teardown_Model.xlsx --json scratch/integrity.json >/dev/null; echo exit $?; python skills/xlsx/scripts/recalc.py outputs/611_Reynolds_Teardown_Model.xlsx; python3 -c "
import json;d=json.load(open('scratch/integrity.json'));print(d['counts_by_category']);[print(f) for f in d['findings'] if f['category'] not in ('formula_literal',)]"cd /vercel/sandbox && python3 - << 'EOF'
p='scripts/build_model.py'; s=open(p).read()
s=s.replace(" ('Published Charlotte custom-build cost floor ($/SF)',250,USD,'Builder cost guides (2026); cost cell turns orange if below',False),\n]",
" ('Published Charlotte custom-build cost floor ($/SF)',250,USD,'Builder cost guides (2026); cost cell turns orange if below',False),\n ('Target hard + soft budget ($/SF, base case)',250,USD,'User base case; Budget tab line items should tie to this',False),\n]")
s=s.replace("'stories','floor'])}","'stories','floor','tgt'])}")
s=s.replace("put(wbud,'C46',250,BLUE,'$#,##0.00')","put(wbud,'C46',f\"={I['tgt']}\",GRN,'$#,##0.00')")
s=s.replace(""" c=wsn.cell(row=row,column=j); c.font=Font(name=F,size=10,bold=True,color='0000FF'); c.fill=LIGHT""",
""" c=wsn.cell(row=row,column=j); c.font=Font(name=F,size=10,bold=True,color='000000' if row==62 else '0000FF'); c.fill=LIGHT""")
s=s.replace("for col,w in zip('ABCDEFGHIJK',[26,12,12,15,10,40,18","for col,w in zip('ABCDEFGHIJK',[26,12,12,15,10,52,18")
s=s.replace("wbud.freeze_panes='A4'","wbud.freeze_panes='A5'")
open(p,'w').write(s)
EOF
python3 scripts/build_model.py && python skills/xlsx/scripts/workbook_integrity_scan.py outputs/611_Reynolds_Teardown_Model.xlsx >/dev/null && python skills/xlsx/scripts/recalc.py outputs/611_Reynolds_Teardown_Model.xlsx 2>/dev/null; unzip -p outputs/611_Reynolds_Teardown_Model.xlsx xl/worksheets/sheet2.xml | grep -o "dataBar" | head -2; unzip -p outputs/611_Reynolds_Teardown_Model.xlsx xl/worksheets/sheet6.xml | grep -c "cfRule"cd /vercel/sandbox && python3 - << 'EOF'
from openpyxl import load_workbook
wb=load_workbook('outputs/611_Reynolds_Teardown_Model.xlsx',data_only=True)
b=wb['Budget'];m=wb['Model'];s=wb['Sensitivity'];i=wb['Inputs']
print('inputs B5',i['B5'].value,'B22',i['B22'].value)
print('budget', b['C31'].value,b['D31'].value,b['C43'].value,b['D43'].value,b['C45'].value,b['D45'].value,b['C47'].value,b['C48'].value)
for r in (10,17,18,19): print(r,[m[f'{c}{r}'].value for c in 'ABCD'])
for r in (49,50,51): print(r,[m[f'{c}{r}'].value for c in 'BCD'])
for r in (55,56,57,58,67): print(r,m[f'A{r}'].value,m[f'B{r}'].value)
for r in range(50,60): print(r,[round(x) if isinstance(x,float) else x for x in [s.cell(row=r,column=j).value for j in range(1,9)]])
for r in range(62,72): print(r,[round(x,3) if isinstance(x,float) else x for x in [s.cell(row=r,column=j).value for j in range(1,9)]])
# independent check: profit at 2.4M, 250, 650k
sf=4500; carry=(4749+9000)/12+400
print(2.4e6*.945-(650000*1.0075+sf*250+25000+carry*9+10000))
EOFcd /vercel/sandbox && sed -n 14,60p scripts/build_memo.py
cd /vercel/sandbox && python3 - << 'EOF'
p='scripts/build_memo.py'; s=open(p).read()
s=s.replace('''f"Pay no more than {k(m['C18'].value)} at a {M(2.4e6)} ARV for a 17.5% gross return. That is about {k(m['C18'].value-l['G35'].value)} above "
f"the {k(l['G35'].value)} median teardown sale nearby, so the deal works at market land prices. "
f"The bigger risk is the build budget. Your $225/SF is below every published Charlotte custom-build range, and each 10% overrun "
f"cuts about $100k off the price you can pay.")''','''f"At a $250/SF build budget, pay no more than {k(m['C18'].value)} at a {M(2.4e6)} ARV for a 17.5% gross return. That matches "
f"the {k(l['G35'].value)} median teardown sale nearby, so there is no cushion at market land prices. "
f"At your $650k base lot price, profit is {k(m['C49'].value)} ({p(m['C50'].value)} on cost) at a $2.4M exit. "
f"Build cost is the main risk: every $25/SF adds about $113k to cost.")''')
s=s.replace("the $1.01M build, ","the $1.13M build at $250/SF, ")
s=s.replace('''doc.add_paragraph("Sensitivities (max entry price at 17.5%)",style="Heading 2")
t3=doc.add_table(rows=1,cols=6); t3.style="Table Grid"
hd=["ARV / overrun"]+[f"{int(s.cell(row=6,column=j).value*100)}% (${s.cell(row=7,column=j).value:,.0f})" for j in range(2,7)]
for i,h in enumerate(hd): t3.rows[0].cells[i].text=h
for r in (9,10,11):
row=t3.add_row().cells; row[0].text=M(s[f'A{r}'].value)
for j in range(2,7): row[j-1].text=k(s.cell(row=r,column=j).value)''','''doc.add_paragraph("Total profit at a $650k lot: exit price vs. build $/SF",style="Heading 2")
t3=doc.add_table(rows=1,cols=8); t3.style="Table Grid"
hd=["Exit"]+[f"${s.cell(row=50,column=j).value:,.0f}/SF" for j in range(2,9)]
for i,h in enumerate(hd): t3.rows[0].cells[i].text=h
for r in (54,55,56,57,58,59):
row=t3.add_row().cells; row[0].text=M(s[f'A{r}'].value)
for j in range(2,9):
v=s.cell(row=r,column=j).value; row[j-1].text=(f"({k(-v)})" if v<0 else k(v))''')
s=s.replace('''f"Buying at $800k with a $2.4M exit''','''f"Buying at $650k with a $2.4M exit''')
s=s.replace('''f"At $800k, you need''','''f"At $650k, you need''')
s=s.replace("Cost: $225/SF is below published Charlotte custom ranges, which start around $250/SF and run $400+ near Myers Park; get a fixed-price bid or GMP before going hard. ",
"Cost: $250/SF sits at the bottom of published Charlotte custom ranges ($250–$500/SF); the Budget tab line items are placeholders until you have a fixed-price bid or GMP. ")
open(p,'w').write(s)
EOF
rm -f scratch/memo*.docx scratch/qa/*; mkdir -p scratch/qa; python3 scripts/build_memo.py 2>/dev/null && python skills/docx/scripts/apply_realai_format.py scratch/memo_plain.docx --output scratch/memo-formatted.docx --small-table 0 --small-table 1 --small-table 2 >/dev/null && python3 scripts/memo_margins.py scratch/memo-formatted.docx scratch/memo-m.docx && python skills/docx/scripts/verify_text_unchanged.py scratch/memo_plain.docx scratch/memo-m.docx | tail -1 && libreoffice --headless --convert-to pdf scratch/memo-m.docx --outdir scratch/qa >/dev/null 2>&1; pdfinfo scratch/qa/memo-m.pdf | grep Pages; pdftoppm -png -r 200 scratch/qa/memo-m.pdf scratch/qa/pagecd /vercel/sandbox && pdftotext -f 2 -l 2 scratch/qa/memo-m.pdf -
Trimming two grid rows to keep it to one page.
cd /vercel/sandbox && sed -i 's/for r in (54,55,56,57,58,59):/for r in (55,56,57,58,59):/' scripts/build_memo.py && sed -i 's/Smaller new builds on the streets west of the site (Lochridge, Collingwood) closed at \$467–\$480\/SF, and 8 new-construction listings within 1.2 mi are competing supply./West-side new builds closed at $467–$480\/SF; 8 new-construction listings within 1.2 mi compete./' scripts/build_memo.py && rm -f scratch/memo*.docx scratch/qa/*; python3 scripts/build_memo.py 2>/dev/null && python skills/docx/scripts/apply_realai_format.py scratch/memo_plain.docx --output scratch/memo-formatted.docx --small-table 0 --small-table 1 --small-table 2 >/dev/null && python3 scripts/memo_margins.py scratch/memo-formatted.docx scratch/memo-m.docx && python skills/docx/scripts/verify_text_unchanged.py scratch/memo_plain.docx scratch/memo-m.docx | tail -1 && libreoffice --headless --convert-to pdf scratch/memo-m.docx --outdir scratch/qa >/dev/null 2>&1; pdfinfo scratch/qa/memo-m.pdf | grep Pages; pdftoppm -png -r 200 scratch/qa/memo-m.pdf scratch/qa/page
cd /vercel/sandbox && pdftotext -f 2 -l 2 scratch/qa/memo-m.pdf -; sed -i 's/Inches(0.5); sec.left_margin/Inches(0.45); sec.left_margin/; s/Inches(0.7)/Inches(0.6)/' scripts/memo_margins.py; grep Inches scripts/memo_margins.py
cd /vercel/sandbox && rm -f scratch/memo*.docx scratch/qa/*; python3 scripts/build_memo.py 2>/dev/null && python skills/docx/scripts/apply_realai_format.py scratch/memo_plain.docx --output scratch/memo-formatted.docx --small-table 0 --small-table 1 --small-table 2 >/dev/null && python3 scripts/memo_margins.py scratch/memo-formatted.docx scratch/memo-m.docx && python skills/docx/scripts/verify_text_unchanged.py scratch/memo_plain.docx scratch/memo-m.docx | tail -1 && libreoffice --headless --convert-to pdf scratch/memo-m.docx --outdir scratch/qa >/dev/null 2>&1; pdfinfo scratch/qa/memo-m.pdf | grep Pages; pdftoppm -png -r 200 scratch/qa/memo-m.pdf scratch/qa/page
cd /vercel/sandbox && sed -i 's/ If you measure return before selling costs, the 17.5% cap rises to {k(m\[.C29.\].value)}.//' scripts/build_memo.py && sed -i 's/for r in range(5,10):/for r in range(5,9):/' scripts/build_memo.py && rm -f scratch/memo*.docx scratch/qa/*; python3 scripts/build_memo.py 2>/dev/null && python skills/docx/scripts/apply_realai_format.py scratch/memo_plain.docx --output scratch/memo-formatted.docx --small-table 0 --small-table 1 --small-table 2 >/dev/null && python3 scripts/memo_margins.py scratch/memo-formatted.docx scratch/memo-m.docx && python skills/docx/scripts/verify_text_unchanged.py scratch/memo_plain.docx scratch/memo-m.docx | tail -1 && libreoffice --headless --convert-to pdf scratch/memo-m.docx --outdir scratch/qa >/dev/null 2>&1; pdfinfo scratch/qa/memo-m.pdf | grep Pages; pdftoppm -png -r 200 scratch/qa/memo-m.pdf scratch/qa/pagecd /vercel/sandbox && rm -f outputs/611_Reynolds_Teardown_Memo.docx && python skills/docx/scripts/finalize_metadata.py scratch/memo-m.docx --title "611 Reynolds Dr teardown screen: ARV support and maximum entry price" --subject "Sales comps validating a \$2.3M-\$2.5M ARV, residual land value and profit sensitivity for a 4,500 SF spec home at \$250/SF" --keywords "611 Reynolds Dr; Charlotte; teardown; ARV comps; residual land value; spec build" --output outputs/611_Reynolds_Teardown_Memo.docx | head -3
I've updated both files and saved them as new versions of the earlier ones. The bigger budget changes the answer. At $250/SF, the most you can pay at a $2.4M exit for a 17.5% return falls to $751k , from $862k at $225/SF. That's about the median recent teardown sale nearby, so there's no cushion if you pay market for the lot. At your $650k lot price, profit is $439k , a 24.0% return on cost .
New Budget tab. It has 27 hard-cost lines and 8 soft-cost lines, each entered as $/SF and multiplied by 4,500 SF:
All line items are placeholder allowances. If you act as your own general contractor, set the builder fee to zero.
New profit grid (Sensitivity sheet, table 6). Exit price runs $1.7M–$2.5M down the side and build cost runs $200–$350/SF across the top, all at the $650k lot. A matching return-on-cost grid sits below it.
Conditional formatting added:
The memo now uses the $250/SF and $650k base case and shows part of the new profit grid in place of the cost-overrun table.
Files are in your library:
This is a read-only snapshot of a RealAI analysis.