I am a development analyst for Dapper Construction in Charlotte NC, here are our projects we have done before. Map them out in the market, and show me 3-5 as-is tear-down listings that might pencil for our next project.
https://www.dapperdevelopmentre.com/available-properties
"""Builds Dapper Construction teardown screening model.
Sheets:
Assumptions - labeled input cells (blue) for cost, carry, profit, fees
Comps - new-construction resale comps (raw, hardcoded) w/ formula $/SF + medians
Candidates - 5 for-sale teardown listings; residual land value + margin at ask (all formulas)
Sensitivity - margin-at-ask grid: hard cost/SF x exit $/SF, for the lead candidate
Projects - Dapper's 16 delivered/listed projects, street-level geocode
"""
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
BLUE = Font(name="Arial", size=10, color="0000FF")
BLACK = Font(name="Arial", size=10, color="000000")
GREEN = Font(name="Arial", size=10, color="008000")
BOLD = Font(name="Arial", size=10, bold=True)
TITLE = Font(name="Arial", size=12, bold=True)
HDR_FILL = PatternFill("solid", fgColor="D9E1F2")
SUB_FILL = PatternFill("solid", fgColor="F2F2F2")
TOPB = Border(top=Side(style="thin"))
CTR = Alignment(horizontal="center", vertical="center", wrap_text=True)
LEFT = Alignment(horizontal="left", vertical="center")
USD = '$#,##0;($#,##0);"-"'
USD2 = '$#,##0.00;($#,##0.00);"-"'
PCT = '0.0%;(0.0%);"-"'
NUM = '#,##0;(#,##0);"-"'
AC = '0.00'
wb = openpyxl.Workbook()
# ---------------------------------------------------------------- Assumptions
a = wb.active
a.title = "Assumptions"
a["A1"] = "Dapper Construction - Teardown Screening Model"
a["A1"].font = TITLE
a["A2"] = "Residual land value framework. Blue cells are inputs - override with Dapper's own cost book."
a["A2"].font = Font(name="Arial", size=9, italic=True)
rows = [
("Construction cost", None, None, None),
("Hard cost per heated SF", 300, USD, "Charlotte high-end custom range $300-$450/SF; spec-builder basis at low end"),
("Soft cost (% of hard cost)", 0.09, PCT, "Architecture, engineering, permits, survey, utilities, insurance"),
("Demolition + site work", 65000, USD, "Teardown, haul-off, grading, tree protection, erosion control"),
("Contingency (% of hard cost)", 0.05, PCT, "Analyst judgment - override with Dapper's historical change-order rate"),
("Sale + closing costs (% of GDV)", 0.055, PCT, "Listing commission, buyer co-op, closing, staging"),
("Carry", None, None, None),
("Construction loan rate", 0.0775, PCT, "Analyst judgment - replace with current bank quote"),
("Project duration (months)", 16, NUM, "Entitlement + demo + build + sale"),
("Average outstanding balance (% of total cost)", 0.6, PCT, "Analyst judgment - straight-line draw proxy"),
("Return requirement", None, None, None),
("Target developer profit (% of GDV)", 0.18, PCT, "Analyst judgment - Dapper's hurdle; drives residual land value"),
]
r = 4
anchor = {}
for label, val, fmt, note in rows:
if val is None:
a.cell(r, 1, label).font = BOLD
a.cell(r, 1).fill = SUB_FILL
a.cell(r, 2).fill = SUB_FILL
a.cell(r, 3).fill = SUB_FILL
else:
a.cell(r, 1, label).font = BLACK
c = a.cell(r, 2, val)
c.font = BLUE
c.number_format = fmt
a.cell(r, 3, note).font = Font(name="Arial", size=9, italic=True, color="595959")
anchor[label] = f"$B${r}"
a.cell(r, 1).alignment = LEFT
r += 1
a.column_dimensions["A"].width = 42
a.column_dimensions["B"].width = 14
a.column_dimensions["C"].width = 78
HARD = f"Assumptions!{anchor['Hard cost per heated SF']}"
SOFT = f"Assumptions!{anchor['Soft cost (% of hard cost)']}"
DEMO = f"Assumptions!{anchor['Demolition + site work']}"
CONT = f"Assumptions!{anchor['Contingency (% of hard cost)']}"
SALEC = f"Assumptions!{anchor['Sale + closing costs (% of GDV)']}"
RATE = f"Assumptions!{anchor['Construction loan rate']}"
MONTHS = f"Assumptions!{anchor['Project duration (months)']}"
OUTST = f"Assumptions!{anchor['Average outstanding balance (% of total cost)']}"
PROFIT = f"Assumptions!{anchor['Target developer profit (% of GDV)']}"
# ---------------------------------------------------------------------- Comps
c = wb.create_sheet("Comps")
c["A1"] = "New-construction resale comps by micro-market"
c["A1"].font = TITLE
c["A2"] = "Closed sales of 2022-2026 vintage new builds. Source: Zillow sold listings, pulled 2026-09-22."
c["A2"].font = Font(name="Arial", size=9, italic=True)
comp_hdr = ["Address", "ZIP", "Micro-market", "Sold price", "Heated SF", "$ / SF", "Lot (ac)", "Year built"]
COMPS = [
# Ashbrook / Sedgefield (28209 east of Park Rd)
("1109 Wimbledon Rd", "28209", "Ashbrook / Sedgefield", 2570000, 5032, 0.31, 2025),
("1316 Holmes Dr", "28209", "Ashbrook / Sedgefield", 2520000, 5150, 0.31, 2026),
("517 Heather Ln", "28209", "Ashbrook / Sedgefield", 2540000, 4863, 0.28, 2026),
("725 Hillside Ave", "28209", "Ashbrook / Sedgefield", 1550000, 3259, 0.19, 2025),
("4339 Castlewood Rd", "28209", "Ashbrook / Sedgefield", 1335000, 3043, 0.20, 2025),
("624 Heather Ln", "28209", "Ashbrook / Sedgefield", 1900000, 3881, 0.26, 2025),
# Collingwood / Selwyn Park (28209-28210 west of Park Rd)
("710 Lochridge Rd", "28209", "Collingwood / Selwyn Park", 1622250, 3475, 0.20, 2026),
("4900 Gilmore Dr", "28209", "Collingwood / Selwyn Park", 1400000, 3601, 0.30, 2026),
("4733 Gilmore Dr", "28209", "Collingwood / Selwyn Park", 1395000, 3419, 0.17, 2026),
("4724 Gilmore Dr", "28209", "Collingwood / Selwyn Park", 1375000, 3204, 0.27, 2025),
("4658 Murrayhill Rd", "28209", "Collingwood / Selwyn Park", 1349000, 3311, 0.21, 2025),
("5139 Murrayhill Rd", "28210", "Collingwood / Selwyn Park", 1400000, 3274, 0.19, 2025),
("5012 Baylor Dr", "28210", "Collingwood / Selwyn Park", 1495000, 3880, 0.35, 2026),
("4311 Collingwood Dr", "28209", "Collingwood / Selwyn Park", 1800000, 3753, 0.20, 2025),
# Cotswold / Providence Park (28211)
("3434 Fielding Ave", "28211", "Cotswold / Providence Park", 2627685, 4600, 0.41, 2026),
("409 Bertonley Ave", "28211", "Cotswold / Providence Park", 2595000, 4540, 0.24, 2023),
("713 Bertonley Ave", "28211", "Cotswold / Providence Park", 2164455, 4175, 0.14, 2025),
("4811 Gaynor Rd", "28211", "Cotswold / Providence Park", 2765000, 5445, 0.41, 2026),
("444 Hunter Ln", "28211", "Cotswold / Providence Park", 2450000, 4798, 0.50, 2026),
("4409 Castleton Rd", "28211", "Cotswold / Providence Park", 1615000, 3553, 0.24, 2026),
("4341 Castleton Rd", "28211", "Cotswold / Providence Park", 1361750, 3081, 0.24, 2026),
("3822 Topsfield Rd", "28211", "Cotswold / Providence Park", 1730000, 4347, 0.29, 2025),
]
for j, h in enumerate(comp_hdr, start=1):
cc = c.cell(4, j, h)
cc.font = BOLD
cc.fill = HDR_FILL
cc.alignment = CTR
r = 5
for addr, zc, mkt, price, sf, lot, yb in COMPS:
c.cell(r, 1, addr).font = BLACK
c.cell(r, 2, zc).font = BLACK
c.cell(r, 3, mkt).font = BLACK
c.cell(r, 4, price).font = BLACK
c.cell(r, 4).number_format = USD
c.cell(r, 5, sf).font = BLACK
c.cell(r, 5).number_format = NUM
c.cell(r, 6, f"=D{r}/E{r}").font = BLACK
c.cell(r, 6).number_format = USD
c.cell(r, 7, lot).font = BLACK
c.cell(r, 7).number_format = AC
c.cell(r, 8, str(yb)).font = BLACK
c.cell(r, 8).alignment = Alignment(horizontal="center")
r += 1
last_comp = r - 1
# medians block
c.cell(r + 1, 1, "Micro-market medians").font = BOLD
c.cell(r + 1, 1).fill = SUB_FILL
for j in range(2, 7):
c.cell(r + 1, j).fill = SUB_FILL
med_hdr = ["Micro-market", "Comps", "Median $ / SF", "Median heated SF", "Median sold price"]
for j, h in enumerate(med_hdr, start=1):
cc = c.cell(r + 2, j, h)
cc.font = BOLD
cc.fill = HDR_FILL
cc.alignment = CTR
mkts = ["Ashbrook / Sedgefield", "Collingwood / Selwyn Park", "Cotswold / Providence Park"]
med_row = {}
rr = r + 3
for m in mkts:
c.cell(rr, 1, m).font = BLACK
c.cell(rr, 2, f'=COUNTIF($C$5:$C${last_comp},$A{rr})').font = BLACK
c.cell(rr, 2).number_format = NUM
c.cell(rr, 3, f'=MEDIAN(IF($C$5:$C${last_comp}=$A{rr},$F$5:$F${last_comp}))').font = BLACK
c.cell(rr, 3).number_format = USD
c.cell(rr, 4, f'=MEDIAN(IF($C$5:$C${last_comp}=$A{rr},$E$5:$E${last_comp}))').font = BLACK
c.cell(rr, 4).number_format = NUM
c.cell(rr, 5, f'=MEDIAN(IF($C$5:$C${last_comp}=$A{rr},$D$5:$D${last_comp}))').font = BLACK
c.cell(rr, 5).number_format = USD
med_row[m] = rr
rr += 1
for w, col in zip([26, 8, 27, 14, 12, 11, 10, 11], "ABCDEFGH"):
c.column_dimensions[col].width = w
c.freeze_panes = "A5"
# ----------------------------------------------------------------- Candidates
d = wb.create_sheet("Candidates")
d["A1"] = "As-is teardown candidates - residual land value screen"
d["A1"].font = TITLE
d["A2"] = "Active for-sale listings, Charlotte. Asking price, lot and vintage are listing facts; everything below Gross development value is calculated."
d["A2"].font = Font(name="Arial", size=9, italic=True)
CANDS = [
# addr, zip, micro-market, ask, as-is sf, lot ac, yr, DOM, target new SF
("710 Manhasset Rd", "28209", "Collingwood / Selwyn Park", 475000, 1087, 0.19, 1954, 13, 3600),
("4824 Gilmore Dr", "28209", "Collingwood / Selwyn Park", 599000, 1637, 0.32, 1954, 28, 3700),
("4318 Ruskin Dr", "28209", "Ashbrook / Sedgefield", 650000, 1603, 0.30, 1958, 69, 4400),
("5017 Randolph Rd", "28211", "Cotswold / Providence Park", 950000, 1600, 0.61, 1954, 46, 4800),
("4124 Randolph Rd", "28211", "Cotswold / Providence Park", 1095000, 1694, 0.45, 1968, 91, 4800),
]
labels = [
("Address", "t"), ("ZIP", "t"), ("Micro-market", "t"),
("Asking price", USD), ("As-is heated SF", NUM), ("Lot size (acres)", AC),
("Year built", "t"), ("Days on market", NUM),
("Target new-build heated SF", NUM),
("Exit $ / SF (micro-market median)", USD),
("Gross development value (GDV)", USD),
("Hard cost", USD),
("Contingency", USD),
("Soft cost", USD),
("Demolition + site work", USD),
("Sale + closing costs", USD),
("Carry / construction interest", USD),
("Total cost excl. land", USD),
("Target developer profit", USD),
("Residual land value (supportable)", USD),
("Spread vs. asking price", USD),
("Profit at asking price", USD),
("Profit margin at asking price", PCT),
("Profit per new-build SF", USD),
("Land as % of GDV at ask", PCT),
]
for i, (lab, _) in enumerate(labels, start=4):
cc = d.cell(i, 1, lab)
cc.font = BOLD if i in (4, 14, 21, 23, 24, 27) else BLACK
cc.alignment = LEFT
d.cell(4, 1).fill = HDR_FILL
for k, (addr, zc, mkt, ask, sf, lot, yb, dom, tsf) in enumerate(CANDS):
col = 2 + k
L = get_column_letter(col)
d.cell(4, col, addr).font = BOLD
d.cell(4, col).fill = HDR_FILL
d.cell(4, col).alignment = CTR
d.cell(5, col, zc).font = BLACK
d.cell(6, col, mkt).font = BLACK
d.cell(6, col).alignment = Alignment(wrap_text=True, horizontal="center")
d.cell(7, col, ask).font = BLUE
d.cell(8, col, sf).font = BLUE
d.cell(9, col, lot).font = BLUE
d.cell(10, col, str(yb)).font = BLUE
d.cell(10, col).alignment = Alignment(horizontal="center")
d.cell(11, col, dom).font = BLUE
d.cell(12, col, tsf).font = BLUE
mr = med_row[mkt]
d.cell(13, col, f"=Comps!$C${mr}").font = GREEN
d.cell(14, col, f"={L}12*{L}13").font = BLACK
d.cell(15, col, f"={L}12*{HARD}").font = BLACK
d.cell(16, col, f"={L}15*{CONT}").font = BLACK
d.cell(17, col, f"={L}15*{SOFT}").font = BLACK
d.cell(18, col, f"={DEMO}").font = BLACK
d.cell(19, col, f"={L}14*{SALEC}").font = BLACK
d.cell(20, col, f"=({L}7+{L}15+{L}16+{L}17+{L}18)*{OUTST}*{RATE}*{MONTHS}/12").font = BLACK
d.cell(21, col, f"=SUM({L}15:{L}20)").font = BLACK
d.cell(22, col, f"={L}14*{PROFIT}").font = BLACK
d.cell(23, col, f"={L}14-{L}21-{L}22").font = BLACK
d.cell(24, col, f"={L}23-{L}7").font = BLACK
d.cell(25, col, f"={L}14-{L}21-{L}7").font = BLACK
d.cell(26, col, f"={L}25/{L}14").font = BLACK
d.cell(27, col, f"={L}25/{L}12").font = BLACK
d.cell(28, col, f"={L}7/{L}14").font = BLACK
for rr2, (lab, fmt) in enumerate(labels, start=4):
if fmt not in ("t",):
d.cell(rr2, col).number_format = fmt
for rr2 in (7, 14, 21, 23, 24, 25):
d.cell(rr2, col).border = TOPB
d.cell(rr2, col).font = Font(name="Arial", size=10, bold=True,
color="0000FF" if rr2 == 7 else "000000")
d.cell(26, col).font = BOLD
d.cell(24, col).font = BOLD
d.column_dimensions[L].width = 17
d.column_dimensions["A"].width = 38
for rr2 in (7, 14, 21, 23, 24, 25, 26):
d.cell(rr2, 1).border = TOPB
d.cell(rr2, 1).font = BOLD
d.freeze_panes = "B5"
# ---------------------------------------------------------------- Sensitivity
s = wb.create_sheet("Sensitivity")
s["A1"] = "Profit margin at asking price - lead candidate"
s["A1"].font = TITLE
s["A3"] = "Candidate"
s["A3"].font = BOLD
s["B3"] = "=Candidates!B4"
s["B3"].font = GREEN
s["A4"] = "Asking price"
s["A4"].font = BOLD
s["B4"] = "=Candidates!B7"
s["B4"].font = GREEN
s["B4"].number_format = USD
s["A5"] = "Target new-build heated SF"
s["A5"].font = BOLD
s["B5"] = "=Candidates!B12"
s["B5"].font = GREEN
s["B5"].number_format = NUM
s["A7"] = "Rows: hard cost per heated SF. Columns: exit $ per heated SF. Interior: profit margin on GDV after all costs and the asking price as land basis."
s["A7"].font = Font(name="Arial", size=9, italic=True)
hard_axis = [260, 280, 300, 320, 340, 360]
exit_axis = [400, 440, 470, 510, 550]
s.cell(9, 1, "Hard cost $/SF").font = BOLD
s.cell(9, 1).fill = HDR_FILL
s.cell(9, 1).alignment = CTR
for j, e in enumerate(exit_axis, start=2):
cc = s.cell(9, j, e)
cc.font = BLUE
cc.number_format = USD
cc.fill = HDR_FILL
cc.alignment = CTR
for i, h in enumerate(hard_axis, start=10):
cc = s.cell(i, 1, h)
cc.font = BLUE
cc.number_format = USD
cc.fill = HDR_FILL
for j in range(2, 2 + len(exit_axis)):
JL = get_column_letter(j)
gdv = f"$B$5*{JL}$9"
hard = f"$B$5*$A{i}"
cost = (f"({hard})+({hard})*{CONT}+({hard})*{SOFT}+{DEMO}+({gdv})*{SALEC}"
f"+($B$4+({hard})+({hard})*{CONT}+({hard})*{SOFT}+{DEMO})*{OUTST}*{RATE}*{MONTHS}/12")
f = s.cell(i, j, f"=(({gdv})-({cost})-$B$4)/({gdv})")
f.font = BLACK
f.number_format = PCT
s.column_dimensions["A"].width = 18
for j in range(2, 2 + len(exit_axis)):
s.column_dimensions[get_column_letter(j)].width = 13
# ------------------------------------------------------------------- Projects
p = wb.create_sheet("Projects")
p["A1"] = "Dapper Development - delivered and listed projects"
p["A1"].font = TITLE
p["A2"] = "Source: dapperdevelopmentre.com. Coordinates are street-level centroids from the RealAI property table; exact parcels are not published."
p["A2"].font = Font(name="Arial", size=9, italic=True)
phdr = ["Project", "Street", "Neighborhood", "ZIP", "Heated SF", "Beds", "Baths", "Status", "Latitude", "Longitude"]
PROJ = [
("Wimbledon", "Wimbledon Rd", "Ashbrook", "28209", 5135, 5, 5.5, "Available", 35.1779, -80.8550),
("Drexel", "Drexel Pl", "Ashbrook", "28209", 4300, 4, 4.5, "Available", 35.1736, -80.8539),
("Rockford", "Rockford Ct", "Collingwood", "28209", 3827, 4, 4.5, "Available", 35.1761, -80.8689),
("Manhasset", "Manhasset Rd", "Collingwood", "28209", 3572, 4, 4.5, "Available", 35.1780, -80.8688),
("Northgate", "Northgate Ave", "Selwyn Park", "28209", 3000, 4, 4.0, "Under contract", 35.1779, -80.8721),
("Annlin II", "Annlin Ave", "Colonial Village", "28209", 4124, 5, 5.0, "Available", 35.1849, -80.8661),
("Bertonley", "Bertonley Ave", "Cotswold", "28211", 5684, 5, 6.0, "Available", 35.1847, -80.7959),
("Addison", "Addison Dr", "Cotswold", "28211", 5000, 5, 5.0, "Available", 35.1712, -80.7856),
("Shasta", "Shasta Ln", "Cotswold / Sherwood Forest", "28211", 7500, 7, 8.0, "Under contract", 35.1640, -80.7885),
("Fielding Ave", "Fielding Ave", "Cotswold / Providence Park", "28211", 4600, 4, 4.5, "Sold", 35.1654, -80.8012),
("Cavendish", "Cavendish Ct", "Providence Park", "28211", 4444, 5, 4.5, "Available", 35.1625, -80.8032),
("Chatham", "Chatham Ave", "Plaza Midwood", "28205", 3853, 5, 4.5, "Available", 35.2245, -80.8012),
("Mantle", "Mantle Ct", "Eastway / Plaza Midwood", "28205", 4180, 5, 5.0, "Under contract", 35.1970, -80.7825),
("Guildbrook", "Guildbrook Rd", "Old Farm", "28226", 4095, 5, 4.5, "Available", 35.1396, -80.7894),
("Cornish Pl", "Cornish Pl", "Quail Hollow area", "28210", 4300, 5, 4.5, "Under contract", 35.1464, -80.8473),
("Prett Ct", "Prett Ct", "Lansdowne", "28270", 5300, 5, 4.5, "Under contract", 35.1439, -80.7810),
]
for j, h in enumerate(phdr, start=1):
cc = p.cell(4, j, h)
cc.font = BOLD
cc.fill = HDR_FILL
cc.alignment = CTR
r = 5
for row in PROJ:
for j, v in enumerate(row, start=1):
cc = p.cell(r, j, str(v) if j == 4 else v)
cc.font = BLACK
p.cell(r, 5).number_format = NUM
p.cell(r, 7).number_format = "0.0"
p.cell(r, 9).number_format = "0.0000"
p.cell(r, 10).number_format = "0.0000"
r += 1
lastp = r - 1
p.cell(r + 1, 1, "Portfolio summary").font = BOLD
p.cell(r + 1, 1).fill = SUB_FILL
p.cell(r + 2, 1, "Projects").font = BLACK
p.cell(r + 2, 2, f"=COUNTA($A$5:$A${lastp})").font = BLACK
p.cell(r + 2, 2).number_format = NUM
p.cell(r + 3, 1, "Median heated SF").font = BLACK
p.cell(r + 3, 2, f"=MEDIAN($E$5:$E${lastp})").font = BLACK
p.cell(r + 3, 2).number_format = NUM
p.cell(r + 4, 1, "Average heated SF").font = BLACK
p.cell(r + 4, 2, f"=AVERAGE($E$5:$E${lastp})").font = BLACK
p.cell(r + 4, 2).number_format = NUM
p.cell(r + 5, 1, "Median bedrooms").font = BLACK
p.cell(r + 5, 2, f"=MEDIAN($F$5:$F${lastp})").font = BLACK
p.cell(r + 5, 2).number_format = NUM
p.cell(r + 6, 1, "ZIP codes represented").font = BLACK
p.cell(r + 6, 2, f"=SUMPRODUCT(($D$5:$D${lastp}<>\"\")/COUNTIF($D$5:$D${lastp},$D$5:$D${lastp}&\"\"))").font = BLACK
p.cell(r + 6, 2).number_format = NUM
for w, col in zip([16, 17, 30, 8, 11, 8, 8, 16, 11, 12], "ABCDEFGHIJ"):
p.column_dimensions[col].width = w
p.freeze_panes = "A5"
wb.save("Dapper_Teardown_Screen.xlsx")
print("saved")
cd /vercel/sandbox && pip -q install openpyxl 2>/dev/null; python build_dapper_model.py
cd /vercel/sandbox && python - <<'EOF'
import re
s=open('build_dapper_model.py').read()
old = """mkts = ["Ashbrook / Sedgefield", "Collingwood / Selwyn Park", "Cotswold / Providence Park"]
med_row = {}
rr = r + 3
for m in mkts:
c.cell(rr, 1, m).font = BLACK
c.cell(rr, 2, f'=COUNTIF($C$5:$C${last_comp},$A{rr})').font = BLACK
c.cell(rr, 2).number_format = NUM
c.cell(rr, 3, f'=MEDIAN(IF($C$5:$C${last_comp}=$A{rr},$F$5:$F${last_comp}))').font = BLACK
c.cell(rr, 3).number_format = USD
c.cell(rr, 4, f'=MEDIAN(IF($C$5:$C${last_comp}=$A{rr},$E$5:$E${last_comp}))').font = BLACK
c.cell(rr, 4).number_format = NUM
c.cell(rr, 5, f'=MEDIAN(IF($C$5:$C${last_comp}=$A{rr},$D$5:$D${last_comp}))').font = BLACK
c.cell(rr, 5).number_format = USD
med_row[m] = rr
rr += 1"""
new = """# comps are written grouped by micro-market, so each median reads a contiguous block
blocks = {}
for i, (addr, zc, mkt, price, sf, lot, yb) in enumerate(COMPS):
row_i = 5 + i
lo, hi = blocks.get(mkt, (row_i, row_i))
blocks[mkt] = (min(lo, row_i), max(hi, row_i))
mkts = ["Ashbrook / Sedgefield", "Collingwood / Selwyn Park", "Cotswold / Providence Park"]
med_row = {}
rr = r + 3
for m in mkts:
lo, hi = blocks[m]
c.cell(rr, 1, m).font = BLACK
c.cell(rr, 2, f'=COUNTA($A${lo}:$A${hi})').font = BLACK
c.cell(rr, 2).number_format = NUM
c.cell(rr, 3, f'=MEDIAN($F${lo}:$F${hi})').font = BLACK
c.cell(rr, 3).number_format = USD
c.cell(rr, 4, f'=MEDIAN($E${lo}:$E${hi})').font = BLACK
c.cell(rr, 4).number_format = NUM
c.cell(rr, 5, f'=MEDIAN($D${lo}:$D${hi})').font = BLACK
c.cell(rr, 5).number_format = USD
med_row[m] = rr
rr += 1"""
assert old in s
s=s.replace(old,new)
open('build_dapper_model.py','w').write(s)
EOF
python build_dapper_model.py && python skills/xlsx/scripts/workbook_integrity_scan.py Dapper_Teardown_Screen.xlsx --json integrity.json; echo "EXIT=$?"; python -c "
import json;d=json.load(open('integrity.json'))
for f in d.get('findings',[]):
print(f.get('severity'),f.get('check'),f.get('sheet'),f.get('cells') or f.get('cell'),str(f.get('detail') or f.get('message'))[:150])
"cd /vercel/sandbox && python - <<'PY'
s=open('build_dapper_model.py').read()
# --- transpose medians block: metrics as rows, micro-markets as columns
old = s[s.index("# comps are written grouped"):s.index('for w, col in zip([26, 8, 27, 14, 12, 11, 10, 11]')]
new = '''# comps are written grouped by micro-market, so each median reads a contiguous block
blocks = {}
for i, (addr, zc, mkt, price, sf, lot, yb) in enumerate(COMPS):
row_i = 5 + i
lo, hi = blocks.get(mkt, (row_i, row_i))
blocks[mkt] = (min(lo, row_i), max(hi, row_i))
mkts = ["Ashbrook / Sedgefield", "Collingwood / Selwyn Park", "Cotswold / Providence Park"]
base = r + 2
c.cell(base, 1, "Micro-market").font = BOLD
c.cell(base, 1).fill = HDR_FILL
c.cell(base, 1).alignment = CTR
mkt_col = {}
for k, m in enumerate(mkts):
cc = c.cell(base, 2 + k, m)
cc.font = BOLD
cc.fill = HDR_FILL
cc.alignment = CTR
mkt_col[m] = 2 + k
metric_rows = [
("Comps in set", "=COUNTA($A${lo}:$A${hi})", NUM),
("Median exit $ / SF", "=MEDIAN($F${lo}:$F${hi})", USD),
("Median heated SF", "=MEDIAN($E${lo}:$E${hi})", NUM),
("Median sold price", "=MEDIAN($D${lo}:$D${hi})", USD),
]
med_row = {}
for i2, (lab, tmpl, fmt) in enumerate(metric_rows):
rr = base + 1 + i2
c.cell(rr, 1, lab).font = BOLD if lab == "Median exit $ / SF" else BLACK
c.cell(rr, 1).alignment = LEFT
for m in mkts:
lo, hi = blocks[m]
cc = c.cell(rr, mkt_col[m], tmpl.format(lo=lo, hi=hi))
cc.font = BLACK
cc.number_format = fmt
if lab == "Median exit $ / SF":
med_row[lab] = rr
EXIT_ROW = med_row["Median exit $ / SF"]
'''
s = s.replace(old, new)
# candidates: reference the transposed median grid
s = s.replace(
' mr = med_row[mkt]\n d.cell(13, col, f"=Comps!$C${mr}").font = GREEN',
' ml = get_column_letter(mkt_col[mkt])\n d.cell(13, col, f"=Comps!${ml}${EXIT_ROW}").font = GREEN')
# lock anchors inside candidate column formulas
s = s.replace('f"={L}12*{L}13"', 'f"={L}$12*{L}$13"')
s = s.replace('f"={L}12*{HARD}"', 'f"={L}$12*{HARD}"')
s = s.replace('f"={L}15*{CONT}"', 'f"={L}$15*{CONT}"')
s = s.replace('f"={L}15*{SOFT}"', 'f"={L}$15*{SOFT}"')
s = s.replace('f"={L}14*{SALEC}"', 'f"={L}$14*{SALEC}"')
s = s.replace('f"=({L}7+{L}15+{L}16+{L}17+{L}18)*{OUTST}*{RATE}*{MONTHS}/12"',
'f"=({L}$7+{L}$15+{L}$16+{L}$17+{L}$18)*{OUTST}*{RATE}*{MONTHS}/12"')
s = s.replace('f"={L}14*{PROFIT}"', 'f"={L}$14*{PROFIT}"')
s = s.replace('f"={L}14-{L}21-{L}22"', 'f"={L}$14-{L}$21-{L}$22"')
s = s.replace('f"={L}23-{L}7"', 'f"={L}$23-{L}$7"')
s = s.replace('f"={L}14-{L}21-{L}7"', 'f"={L}$14-{L}$21-{L}$7"')
s = s.replace('f"={L}25/{L}14"', 'f"={L}$25/{L}$14"')
s = s.replace('f"={L}25/{L}12"', 'f"={L}$25/{L}$12"')
s = s.replace('f"={L}7/{L}14"', 'f"={L}$7/{L}$14"')
# sensitivity: lock row-axis anchor, fix label, widen col A, right-align headers
s = s.replace('s["A3"] = "Candidate"', 's["A3"] = "Address"')
s = s.replace('$A{i}', '$A${i}')
s = s.replace('s.column_dimensions["A"].width = 18', 's.column_dimensions["A"].width = 30')
s = s.replace(""" cc.fill = HDR_FILL
cc.alignment = CTR
for i, h in enumerate(hard_axis, start=10):""",
""" cc.fill = HDR_FILL
cc.alignment = Alignment(horizontal="right", vertical="center")
for i, h in enumerate(hard_axis, start=10):""")
# one body font size everywhere
s = s.replace('Font(name="Arial", size=9, italic=True)', 'Font(name="Arial", size=10, italic=True)')
s = s.replace('Font(name="Arial", size=9, italic=True, color="595959")',
'Font(name="Arial", size=10, italic=True, color="595959")')
# projects: widen col A, consistent freeze
s = s.replace('for w, col in zip([16, 17, 30, 8, 11, 8, 8, 16, 11, 12], "ABCDEFGHIJ")',
'for w, col in zip([24, 17, 30, 8, 11, 8, 8, 16, 11, 12], "ABCDEFGHIJ")')
s = s.replace('d.freeze_panes = "B5"', 'd.freeze_panes = "A5"')
open('build_dapper_model.py','w').write(s)
PY
python build_dapper_model.py && python skills/xlsx/scripts/workbook_integrity_scan.py Dapper_Teardown_Screen.xlsx --json integrity.json >/dev/null; echo "EXIT=$?"; python -c "
import json;d=json.load(open('integrity.json'))
print('high',d['high_severity_count'],'adv',d['advisory_count'])
for f in d.get('findings',[]):
if f['severity']=='high': print(f)
"cd /vercel/sandbox && python skills/xlsx/scripts/recalc.py Dapper_Teardown_Screen.xlsx 2>&1 | tail -20
cd /vercel/sandbox && python - <<'PY'
import openpyxl
wb=openpyxl.load_workbook('Dapper_Teardown_Screen.xlsx',data_only=True)
c=wb['Comps']
print("MEDIANS")
for r in range(29,35):
print(r,[c.cell(r,j).value for j in range(1,5)])
d=wb['Candidates']
labels=[d.cell(r,1).value for r in range(4,29)]
for r in range(4,29):
print(r, d.cell(r,1).value, [d.cell(r,j).value for j in range(2,7)])
s=wb['Sensitivity']
print("SENS")
for r in range(9,16):
print([s.cell(r,j).value for j in range(1,7)])
p=wb['Projects']
for r in range(22,28): print([p.cell(r,j).value for j in range(1,3)])
PYcd /vercel/sandbox && python - <<'PY'
s=open('build_dapper_model.py').read()
# --- assumptions: realistic spec hard cost + product premium input
s = s.replace(
' ("Hard cost per heated SF", 300, USD, "Charlotte high-end custom range $300-$450/SF; spec-builder basis at low end"),',
' ("Hard cost per heated SF", 225, USD, "Analyst judgment - spec-built luxury basis backed into from closed comps. REPLACE with Dapper cost book; this is the single most sensitive input"),')
s = s.replace(
' ("Return requirement", None, None, None),',
' ("Exit pricing", None, None, None),\n'
' ("Dapper premium to micro-market median $ / SF", 0.08, PCT, "Fielding Ave closed at $571/SF vs a $509/SF Cotswold median - Dapper prices at the top of its comp set"),\n'
' ("Return requirement", None, None, None),')
s = s.replace("PROFIT = f\"Assumptions!{anchor['Target developer profit (% of GDV)']}\"",
"PROFIT = f\"Assumptions!{anchor['Target developer profit (% of GDV)']}\"\n"
"PREM = f\"Assumptions!{anchor['Dapper premium to micro-market median $ / SF']}\"")
# --- four micro-markets: split Collingwood proper from Selwyn Park / Murrayhill
s = s.replace(''' # Collingwood / Selwyn Park (28209-28210 west of Park Rd)
("710 Lochridge Rd", "28209", "Collingwood / Selwyn Park", 1622250, 3475, 0.20, 2026),
("4900 Gilmore Dr", "28209", "Collingwood / Selwyn Park", 1400000, 3601, 0.30, 2026),
("4733 Gilmore Dr", "28209", "Collingwood / Selwyn Park", 1395000, 3419, 0.17, 2026),
("4724 Gilmore Dr", "28209", "Collingwood / Selwyn Park", 1375000, 3204, 0.27, 2025),
("4658 Murrayhill Rd", "28209", "Collingwood / Selwyn Park", 1349000, 3311, 0.21, 2025),
("5139 Murrayhill Rd", "28210", "Collingwood / Selwyn Park", 1400000, 3274, 0.19, 2025),
("5012 Baylor Dr", "28210", "Collingwood / Selwyn Park", 1495000, 3880, 0.35, 2026),
("4311 Collingwood Dr", "28209", "Collingwood / Selwyn Park", 1800000, 3753, 0.20, 2025),''',
''' # Collingwood (28209, Lochridge / Manhasset / Collingwood Dr)
("710 Lochridge Rd", "28209", "Collingwood", 1622250, 3475, 0.20, 2026),
("4311 Collingwood Dr", "28209", "Collingwood", 1800000, 3753, 0.20, 2025),
("1105 Urban Pl", "28209", "Collingwood", 1590000, 3691, 0.20, 2022),
# Selwyn Park / Murrayhill (28209-28210 west of Park Rd)
("4900 Gilmore Dr", "28209", "Selwyn Park / Murrayhill", 1400000, 3601, 0.30, 2026),
("4733 Gilmore Dr", "28209", "Selwyn Park / Murrayhill", 1395000, 3419, 0.17, 2026),
("4724 Gilmore Dr", "28209", "Selwyn Park / Murrayhill", 1375000, 3204, 0.27, 2025),
("4658 Murrayhill Rd", "28209", "Selwyn Park / Murrayhill", 1349000, 3311, 0.21, 2025),
("5139 Murrayhill Rd", "28210", "Selwyn Park / Murrayhill", 1400000, 3274, 0.19, 2025),
("5012 Baylor Dr", "28210", "Selwyn Park / Murrayhill", 1495000, 3880, 0.35, 2026),''')
s = s.replace('mkts = ["Ashbrook / Sedgefield", "Collingwood / Selwyn Park", "Cotswold / Providence Park"]',
'mkts = ["Ashbrook / Sedgefield", "Collingwood", "Selwyn Park / Murrayhill", "Cotswold / Providence Park"]')
# --- candidates: remap markets, size to lot, add max-bid rows
s = s.replace(''' ("710 Manhasset Rd", "28209", "Collingwood / Selwyn Park", 475000, 1087, 0.19, 1954, 13, 3600),
("4824 Gilmore Dr", "28209", "Collingwood / Selwyn Park", 599000, 1637, 0.32, 1954, 28, 3700),
("4318 Ruskin Dr", "28209", "Ashbrook / Sedgefield", 650000, 1603, 0.30, 1958, 69, 4400),
("5017 Randolph Rd", "28211", "Cotswold / Providence Park", 950000, 1600, 0.61, 1954, 46, 4800),
("4124 Randolph Rd", "28211", "Cotswold / Providence Park", 1095000, 1694, 0.45, 1968, 91, 4800),''',
''' ("710 Manhasset Rd", "28209", "Collingwood", 475000, 1087, 0.19, 1954, 13, 3600),
("4318 Ruskin Dr", "28209", "Ashbrook / Sedgefield", 650000, 1603, 0.30, 1958, 69, 5000),
("5017 Randolph Rd", "28211", "Cotswold / Providence Park", 950000, 1600, 0.61, 1954, 46, 5400),
("4824 Gilmore Dr", "28209", "Selwyn Park / Murrayhill", 599000, 1637, 0.32, 1954, 28, 3900),
("4124 Randolph Rd", "28211", "Cotswold / Providence Park", 1095000, 1694, 0.45, 1968, 91, 5000),''')
s = s.replace(' ("Exit $ / SF (micro-market median)", USD),',
' ("Micro-market median $ / SF", USD),\n ("Exit $ / SF (median x Dapper premium)", USD),')
s = s.replace(' ("Land as % of GDV at ask", PCT),',
' ("Land as % of GDV at ask", PCT),\n ("Required discount from ask to hit target", PCT),')
# shift the candidate body rows down one (new row 14 = exit after premium)
old_body = s[s.index(' ml = get_column_letter(mkt_col[mkt])'):s.index(' for rr2, (lab, fmt) in enumerate(labels, start=4):')]
new_body = ''' ml = get_column_letter(mkt_col[mkt])
d.cell(13, col, f"=Comps!${ml}${EXIT_ROW}").font = GREEN
d.cell(14, col, f"={L}$13*(1+{PREM})").font = BLACK
d.cell(15, col, f"={L}$12*{L}$14").font = BLACK
d.cell(16, col, f"={L}$12*{HARD}").font = BLACK
d.cell(17, col, f"={L}$16*{CONT}").font = BLACK
d.cell(18, col, f"={L}$16*{SOFT}").font = BLACK
d.cell(19, col, f"={DEMO}").font = BLACK
d.cell(20, col, f"={L}$15*{SALEC}").font = BLACK
d.cell(21, col, f"=({L}$7+{L}$16+{L}$17+{L}$18+{L}$19)*{OUTST}*{RATE}*{MONTHS}/12").font = BLACK
d.cell(22, col, f"=SUM({L}$16:{L}$21)").font = BLACK
d.cell(23, col, f"={L}$15*{PROFIT}").font = BLACK
d.cell(24, col, f"={L}$15-{L}$22-{L}$23").font = BLACK
d.cell(25, col, f"={L}$24-{L}$7").font = BLACK
d.cell(26, col, f"={L}$15-{L}$22-{L}$7").font = BLACK
d.cell(27, col, f"={L}$26/{L}$15").font = BLACK
d.cell(28, col, f"={L}$26/{L}$12").font = BLACK
d.cell(29, col, f"={L}$7/{L}$15").font = BLACK
d.cell(30, col, f"=IF({L}$24>={L}$7,0,({L}$24-{L}$7)/{L}$7)").font = BLACK
'''
s = s.replace(old_body, new_body)
s = s.replace('for i, (lab, _) in enumerate(labels, start=4):\n cc = d.cell(i, 1, lab)\n cc.font = BOLD if i in (4, 14, 21, 23, 24, 27) else BLACK',
'for i, (lab, _) in enumerate(labels, start=4):\n cc = d.cell(i, 1, lab)\n cc.font = BOLD if i in (4, 15, 22, 24, 25, 27, 30) else BLACK')
s = s.replace(' for rr2 in (7, 14, 21, 23, 24, 25):', ' for rr2 in (7, 15, 22, 24, 25, 26):')
s = s.replace(' d.cell(26, col).font = BOLD\n d.cell(24, col).font = BOLD',
' d.cell(27, col).font = BOLD\n d.cell(25, col).font = BOLD\n d.cell(30, col).font = BOLD')
s = s.replace('for rr2 in (7, 14, 21, 23, 24, 25, 26):', 'for rr2 in (7, 15, 22, 24, 25, 26, 27):')
s = s.replace('d.cell(rr2, col).number_format = fmt', 'd.cell(rr2, col).number_format = fmt')
# --- sensitivity keyed to new rows + axes
s = s.replace('s["B5"] = "=Candidates!B12"', 's["B5"] = "=Candidates!B12"')
s = s.replace('hard_axis = [260, 280, 300, 320, 340, 360]', 'hard_axis = [185, 205, 225, 245, 265, 285]')
s = s.replace('exit_axis = [400, 440, 470, 510, 550]', 'exit_axis = [440, 480, 510, 550, 590]')
open('build_dapper_model.py','w').write(s)
PY
python build_dapper_model.py && python skills/xlsx/scripts/workbook_integrity_scan.py Dapper_Teardown_Screen.xlsx --json integrity.json >/dev/null; echo "EXIT=$?"
python -c "
import json;d=json.load(open('integrity.json'));print('high',d['high_severity_count'],'adv',d['advisory_count'])
for f in d['findings']: print(f['severity'],f['category'],f.get('sheet'),f.get('cell'),f.get('reason','')[:120])
"cd /vercel/sandbox && python skills/xlsx/scripts/recalc.py Dapper_Teardown_Screen.xlsx && python - <<'PY'
import openpyxl
wb=openpyxl.load_workbook('Dapper_Teardown_Screen.xlsx',data_only=True)
c=wb['Comps']
for r in range(28,36): print(r,[c.cell(r,j).value for j in range(1,6)])
d=wb['Candidates']
for r in range(4,31): print(r, d.cell(r,1).value, [round(v,4) if isinstance(v,float) else v for v in [d.cell(r,j).value for j in range(2,7)]])
s=wb['Sensitivity']
for r in range(9,16): print([s.cell(r,j).value if not isinstance(s.cell(r,j).value,float) else round(s.cell(r,j).value,3) for j in range(1,7)])
PYcd /vercel/sandbox && python - <<'PY'
s=open('build_dapper_model.py').read()
s=s.replace(''' ("710 Manhasset Rd", "28209", "Collingwood", 475000, 1087, 0.19, 1954, 13, 3600),
("4318 Ruskin Dr", "28209", "Ashbrook / Sedgefield", 650000, 1603, 0.30, 1958, 69, 5000),''',
''' ("4318 Ruskin Dr", "28209", "Ashbrook / Sedgefield", 650000, 1603, 0.30, 1958, 69, 5000),
("710 Manhasset Rd", "28209", "Collingwood", 475000, 1087, 0.19, 1954, 13, 3600),''')
open('build_dapper_model.py','w').write(s)
PY
python build_dapper_model.py && python skills/xlsx/scripts/workbook_integrity_scan.py Dapper_Teardown_Screen.xlsx >/dev/null 2>&1; echo scan=$?; python skills/xlsx/scripts/recalc.py Dapper_Teardown_Screen.xlsx && python - <<'PY'
import openpyxl
wb=openpyxl.load_workbook('Dapper_Teardown_Screen.xlsx',data_only=True)
d=wb['Candidates']
for r in [4,7,12,14,15,22,24,25,26,27,30]:
print(r, d.cell(r,1).value, [round(v,4) if isinstance(v,float) else v for v in [d.cell(r,j).value for j in range(2,7)]])
s=wb['Sensitivity']
print([s.cell(3,2).value, s.cell(4,2).value, s.cell(5,2).value])
for r in range(9,16): print([s.cell(r,j).value if not isinstance(s.cell(r,j).value,float) else round(s.cell(r,j).value,3) for j in range(1,7)])
PYOne of five listings pencils at your ask — 4318 Ruskin Dr — and it only clears 14.2% against an 18% hurdle, so the market is telling you land is bid ahead of exit pricing right now.
Your 16 projects cluster in two tight pockets and one straggler set. Ten of the sixteen sit inside a 2.5-mile band from Ashbrook/Collingwood (28209) across to Cotswold/Providence Park (28211) — Wimbledon, Drexel, Rockford, Manhasset, Northgate, Annlin, Bertonley, Addison, Shasta, Fielding, Cavendish. Plaza Midwood (Chatham, Mantle), Old Farm, Quail Hollow and Lansdowne are one-offs. Median delivered size is 4,300 heated SF at 5 beds. Site plots are street-centroid, since exact parcels aren't published on your site.
The screen values each lot as a residual: what the finished house sells for in its own micro-market, less vertical cost, carry, sale costs and your profit. The exit side is anchored on 23 closed new-construction resales in those four pockets — and one of them is yours: 3434 Fielding Ave closed at $2,627,685, $571/SF against a $509/SF Cotswold median. That premium is why the model marks exits 8% above median rather than at it.
| Property | Micro-market | Ask | Lot (ac) | Target new SF | Exit $/SF | GDV | Residual land value | Spread vs ask | Margin at ask | Discount needed |
|---|---|---|---|---|---|---|---|---|---|---|
| 4318 Ruskin Dr | Ashbrook / Sedgefield | $650,000 | 0.30 | 5,000 | $5291 | $2,642,989 | $550,54223 | ($99,458) | 14.2%3 | 15%2 |
| 710 Manhasset Rd | Collingwood | $475,000 | 0.19 | 3,600 | $5041 | $1,815,053 | $309,38423 | ($165,616) | 8.9%3 | 35%2 |
| 5017 Randolph Rd | Cotswold / Providence Park | $950,000 | 0.61 | 5,400 | $5501 | $2,969,756 | $672,95723 | ($277,043) | 8.7%3 | 29%2 |
| 4824 Gilmore Dr | Selwyn Park / Murrayhill | $599,000 | 0.32 | 3,900 | $4401 | $1,717,325 | $145,21423 | ($453,786) | (8.4%)3 | 76%2 |
| 4124 Randolph Rd | Cotswold / Providence Park | $1,095,000 | 0.45 | 5,000 | $5501 | $2,749,774 | $604,64223 | ($490,358) | 0.2%3 | 45%2 |
Ruskin is the only one worth an offer. 0.30 acres on a quiet Ashbrook street a block from your Wimbledon and Drexel sites, and the three closest closed comps — 1109 Wimbledon at $2.57M/5,032 SF, 1316 Holmes at $2.52M/5,150 SF, 517 Heather at $2.54M/4,863 SF — all sit on 0.28–0.31 acre lots. You have built this exact box on this exact lot size. At $650K it throws $376K of profit on a $2.64M GDV; at $550K it hits your hurdle. It has been listed 69 days with a $25K cut already taken, which is the profile of a seller who will take $560–575K.
710 Manhasset is the cheap-basis trap. It's literally on the street where you built Manhasset, and $475K for a 1,087 SF 1954 ranch looks like a steal at 26% of GDV. But the 0.19-acre lot caps you near 3,600 SF, and Collingwood's exit is a $467/SF median off a thin three-comp set — the strongest data point, 710 Lochridge, closed at $467/SF. Only 13 days on market, so there's no negotiating room yet. Watch it; don't chase it.
Both Randolph Road lots fail on the same thing. 5017 Randolph is the best dirt in the set — 0.61 acres, the largest lot here — but Randolph is a four-lane arterial, and an arterial address is precisely what strips the Cotswold premium the $550/SF exit assumes. Price it as a Cotswold interior street and you need $673K, not $950K. 4124 Randolph is worse: 91 days, highest basis, 40% land-to-GDV, and essentially zero profit at ask.
Gilmore is the clearest no. Six closed new builds on Gilmore/Murrayhill/Baylor cluster at $385–429/SF with an absolute ceiling near $1.5M. A $599K teardown basis is 35% of a $1.72M GDV in a pocket where the exit is $90/SF below Collingwood one street over. The lot supports $145K.
Where this breaks: hard cost. At $225/SF Ruskin clears 14.2%; at $205/SF it clears roughly 16% and at $265/SF it drops to about 2%. The sensitivity grid in the workbook flexes hard cost against exit price, and $245/SF paired with a $480/SF exit is the corner where Ruskin goes to 1%. Drop your actual per-SF into the Assumptions tab and the whole screen re-ranks.
Practical read: bid Ruskin at $565K with a 30-day feasibility, keep Manhasset on a watch list for a 60-day price cut, and pass on the rest. Worth noting there are only five listings in the whole footprint that even fit a teardown profile at under $1.1M right now — inventory, not underwriting, is your binding constraint this quarter. I can widen the search into Plaza Midwood and Old Farm, or re-run this against off-market absentee-owner parcels with low improvement-to-land ratios, if you want a deeper pipeline.
This is a read-only snapshot of a RealAI analysis.