For the Los Angeles, Miami, Portland, Atlanta, Dallas–Fort Worth, San Antonio, Phoenix and Austin multifamily markets, show 12-month in-place and asking rent change, new-lease tradeout, occupancy and its 12-month change, median days on market, and operating expenses as a share of EGI. Rank them from strongest to weakest for a lender underwriting a stabilized acquisition loan today, and tell me which markets are where recapitalization demand will be highest. Include a few data visualizations to illustrate your conclusions.
mkdir -p mkt && cd mkt && cat > build_records.py <<'EOF'
import json
snap = { # from query toolu_bdrk_01Ug7QEc82Yzj5X4nn8nwfpa
"Atlanta":dict(id="12060",ask_t12=-0.0166,ip_t12=-0.0050,trade=0.0019,occ=0.935,occ_12=0.954,occ_chg=-0.0189,dom=68,opex=0.5028,ins=0.0419,rti=0.2992,uc=13609,deliv=7104,absn=6230,vac_cw=0.112,cap=0.0533,ask=1790.62,ip=1665.25,permits_t12=12042,permits_t13=11551,retention=0.663),
"Austin":dict(id="12420",ask_t12=-0.0309,ip_t12=-0.0237,trade=-0.0351,occ=0.9258,occ_12=0.9425,occ_chg=-0.0167,dom=72,opex=0.5489,ins=0.0387,rti=0.2147,uc=15174,deliv=5208,absn=5369,vac_cw=0.127,cap=0.0490,ask=1547.27,ip=1501.88,permits_t12=8433,permits_t13=15559,retention=0.648),
"Dallas-Fort Worth":dict(id="19100",ask_t12=-0.0107,ip_t12=-0.0151,trade=-0.0120,occ=0.9269,occ_12=0.9529,occ_chg=-0.026,dom=74,opex=0.5419,ins=0.046,rti=0.2586,uc=30321,deliv=15375,absn=10146,vac_cw=0.119,cap=0.0519,ask=1619.26,ip=1553.15,permits_t12=23605,permits_t13=25675,retention=0.655),
"Los Angeles":dict(id="31080",ask_t12=0.0126,ip_t12=0.0058,trade=0.0166,occ=0.9609,occ_12=0.9718,occ_chg=-0.011,dom=56,opex=0.4309,ins=0.0384,rti=0.4193,uc=19309,deliv=6509,absn=2910,vac_cw=0.058,cap=0.0519,ask=3087.05,ip=2803.94,permits_t12=21985,permits_t13=14684,retention=0.713),
"Miami":dict(id="33100",ask_t12=-0.0051,ip_t12=0.0075,trade=0.0074,occ=0.9635,occ_12=0.9802,occ_chg=-0.0166,dom=63,opex=0.4437,ins=0.0657,rti=0.4729,uc=15067,deliv=3806,absn=1901,vac_cw=0.087,cap=0.0505,ask=2976.17,ip=2682.74,permits_t12=13151,permits_t13=13627,retention=0.670),
"Phoenix":dict(id="38060",ask_t12=-0.0186,ip_t12=-0.0206,trade=-0.0301,occ=0.9326,occ_12=0.9512,occ_chg=-0.0186,dom=67,opex=0.4006,ins=0.0256,rti=0.2667,uc=16457,deliv=8454,absn=6032,vac_cw=0.116,cap=0.0525,ask=1675.94,ip=1597.19,permits_t12=12626,permits_t13=12753,retention=0.629),
"Portland":dict(id="38900",ask_t12=-0.0208,ip_t12=-0.0132,trade=0.0031,occ=0.952,occ_12=0.9627,occ_chg=-0.0107,dom=54,opex=0.4708,ins=0.0285,rti=0.2917,uc=2466,deliv=1170,absn=1150,vac_cw=0.074,cap=None,ask=1805.68,ip=1729.38,permits_t12=3382,permits_t13=3083,retention=0.704),
"San Antonio":dict(id="41700",ask_t12=-0.0362,ip_t12=-0.0185,trade=-0.0377,occ=0.92,occ_12=0.9497,occ_chg=-0.0297,dom=79,opex=0.5827,ins=0.0512,rti=0.2719,uc=3677,deliv=3092,absn=1764,vac_cw=0.157,cap=0.0573,ask=1300.76,ip=1297.49,permits_t12=1471,permits_t13=3751,retention=0.666),
}
# tracked units + in-place Jul-2024 & Aug-2026 from mf_rent_ts toolu_bdrk_011pNQ9hEwRGvMohGGPQguLu
ts = {"Atlanta":(495715,1654.99,1666.14,1834.20,1779.78),"Austin":(355034,1574.35,1505.42,1723.24,1551.36),
"Dallas-Fort Worth":(835724,1541.62,1552.40,1686.38,1617.78),"Los Angeles":(615349,2671.65,2803.23,3186.45,3059.88),
"Miami":(567656,2464.25,2692.70,3024.42,2984.20),"Phoenix":(419412,1637.43,1596.76,1781.73,1677.01),
"Portland":(184014,1756.04,1728.44,1929.60,1809.89),"San Antonio":(211398,1279.89,1298.59,1454.34,1300.89)}
# cap rates 1Q22 (peak pricing) from caprate_ts toolu_bdrk_013caZkfeqH3TDgKWvq4GsQg
cap22 = {"Atlanta":0.0383,"Austin":0.0358,"Dallas-Fort Worth":0.0395,"Los Angeles":0.0387,"Miami":0.0389,"Phoenix":0.0367,"San Antonio":0.0437,"Portland":None}
recs=[]
for k,v in snap.items():
u,ip24,ip26,ask24,ask26 = ts[k]
r=dict(entity=k,**v)
r["tracked_units"]=u
r["ip_chg_since_jul24"]=ip26/ip24-1
r["ask_chg_since_jul24"]=ask26/ask24-1
r["uc_pct_tracked"]=v["uc"]/u
r["spread"]=v["ask"]/v["ip"]-1
r["cap_1q22"]=cap22[k]
if v["cap"] and cap22[k]:
r["cap_expansion_bps"]=(v["cap"]-cap22[k])*1e4
r["value_hit_cap_only"]=cap22[k]/v["cap"]-1
else:
r["cap_expansion_bps"]=None; r["value_hit_cap_only"]=None
recs.append(r)
json.dump(recs,open("records.json","w"),indent=1)
recap=[r for r in recs if r["cap"] is not None]
json.dump(recap,open("records_recap.json","w"),indent=1)
for r in recs: print(r["entity"], {k:(round(r[k],4) if isinstance(r[k],float) else r[k]) for k in ["ip_chg_since_jul24","ask_chg_since_jul24","uc_pct_tracked","spread","cap_expansion_bps","value_hit_cap_only"]})
EOF
python build_records.py
S=../skills/multi-entity-analysis/scripts/cross_check.py
python $S --check coverage records.json | head -c 1500; echo
echo LENDER; python $S --check composite --components ip_t12,ask_t12,trade,occ,occ_chg,dom,opex --weights 0.142857,0.142857,0.142857,0.142857,0.142857,-0.142857,-0.142857 records.json
echo RECAP; python $S --check composite --components value_hit_cap_only,ip_chg_since_jul24,occ_chg,trade,uc_pct_tracked --weights -0.2,-0.2,-0.2,-0.2,0.2 records_recap.jsoncd mkt && S=../skills/multi-entity-analysis/scripts/cross_check.py
python $S --check composite --components value_hit_cap_only,ip_chg_since_jul24,occ_chg,trade,uc_pct_tracked --weights=-0.2,-0.2,-0.2,-0.2,0.2 records_recap.json | python -c "import json,sys;d=json.load(sys.stdin);[print(r['entity'],r['score'],r['contributions']) for r in d['ranked']]"
for f in "ask_t12 max Los Angeles" "trade min San Antonio" "occ min San Antonio" "occ max Miami" "dom min Portland" "dom max San Antonio" "opex min Phoenix" "opex max San Antonio" "occ_chg min San Antonio" "value_hit_cap_only min Phoenix" "ip_chg_since_jul24 max Miami" "ip_chg_since_jul24 min Austin" "uc_pct_tracked max Austin" "ins max Miami"; do set -- $f; fld=$1; dir=$2; shift 2; python $S --check extremum --field $fld --direction $dir --claim "$*" records.json | python -c "import json,sys;d=json.load(sys.stdin);print('$fld','$dir',d.get('claim_holds'),d.get('holders',d.get('value')))"; done
python $S --check predicate --field ip_t12 --op gt --value 0 --claim "Los Angeles" records.json | python -c "import json,sys;d=json.load(sys.stdin);print('ip>0',d.get('qualifiers'))"
python $S --check predicate --field ask_t12 --op gt --value 0 --claim "Los Angeles" records.json | python -c "import json,sys;d=json.load(sys.stdin);print('ask>0',d.get('qualifiers'))"
python $S --check predicate --field trade --op lt --value 0 --claim "x" records.json | python -c "import json,sys;d=json.load(sys.stdin);print('trade<0',d.get('qualifiers'))"
python $S --check predicate --field occ_chg --op gt --value 0 --claim "x" records.json | python -c "import json,sys;d=json.load(sys.stdin);print('occchg>0',d.get('qualifiers'))"cd mkt && S=../skills/multi-entity-analysis/scripts/cross_check.py python $S --check predicate --field ip_t12 --op gt --value 0 --claim "Los Angeles" records.json | head -c 600; echo python $S --check predicate --field ask_t12 --op gt --value 0 --claim "Los Angeles" records.json | head -c 600; echo python $S --check predicate --field occ_chg --op ge --value 0 --claim "Los Angeles" records.json | head -c 400
cd mkt && cat > final_table.py <<'EOF'
import json
recs={r["entity"]:r for r in json.load(open("records.json"))}
lender={"Los Angeles":65.0,"Miami":59.8,"Portland":56.2,"Atlanta":50.5,"Phoenix":48.3,"Dallas-Fort Worth":43.9,"Austin":41.2,"San Antonio":35.1}
recap={"Phoenix":62.0,"Austin":60.6,"Dallas-Fort Worth":52.8,"San Antonio":50.8,"Atlanta":49.3,"Los Angeles":38.9,"Miami":35.5,"Portland":None}
def tier(s): return "Strong" if s>=55 else ("Watch" if s>=45 else "Weak")
print("rank|market|lender_score|tier|inplace_t12%|asking_t12%|tradeout%|occ%|occ_chg_pp|DOM|opex%EGI|UC%tracked|recap_score|cap_1Q22|cap_2Q26|capexp_bps|value_hit_cap_only%|ip_chg_since_Jul24%|tracked_units")
for i,(m,s) in enumerate(sorted(lender.items(),key=lambda x:-x[1]),1):
r=recs[m]
f=lambda x,n=1: "n/a" if x is None else f"{x*100:.{n}f}"
print(f"{i}|{m}|{s}|{tier(s)}|{f(r['ip_t12'])}|{f(r['ask_t12'])}|{f(r['trade'])}|{f(r['occ'])}|{f(r['occ_chg'])}|{r['dom']}|{f(r['opex'])}|{f(r['uc_pct_tracked'])}|{recap[m]}|{f(r['cap_1q22'],2)}|{f(r['cap'],2)}|{'n/a' if r['cap_expansion_bps'] is None else round(r['cap_expansion_bps'])}|{f(r['value_hit_cap_only'])}|{f(r['ip_chg_since_jul24'])}|{r['tracked_units']}")
EOF
python final_table.py; echo '[{"entity":"x","label":"Strong"}]' >/dev/null
python - <<'EOF'
import json
L={"Los Angeles":65.0,"Miami":59.8,"Portland":56.2,"Atlanta":50.5,"Phoenix":48.3,"Dallas-Fort Worth":43.9,"Austin":41.2,"San Antonio":35.1}
t=lambda s:"Strong" if s>=55 else ("Watch" if s>=45 else "Weak")
json.dump([{"entity":k,"label":t(v)} for k,v in L.items()],open("mkt/labels.json","w"))
EOF
python ../skills/multi-entity-analysis/scripts/cross_check.py --check buckets --field label --expected "Strong,Watch,Weak" labels.jsonLA and Miami are the safest collateral for a stabilized acquisition loan today, and they're also where the least recapitalization money will be needed. The demand for recap capital sits in Phoenix and Austin, and in Dallas–Fort Worth by sheer size.
All eight markets lost occupancy over the past 12 months. Los Angeles is the only one where asking rents rose , and only LA and Miami grew in-place rents . So this ranking sorts markets by how much weakness each one has, not by which ones are growing.
| Rank | Market | Lender Score | Tier | In-Place Rent 12-Mo Chg % | Asking Rent 12-Mo Chg % | New-Lease Tradeout % | Occupancy % | Occupancy 12-Mo Chg (pp) | Median Days on Market | OpEx % of EGI | Recap-Demand Score |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | Los Angeles | 65.01 | Strong1 | +0.6%2 | +1.3%2 | +1.7% | 96.1% | -1.1 pp2 | 56 | 43.1% | 38.9 |
| 2 | Miami | 59.8 | Strong | +0.8% | -0.5% | +0.7% | 96.4% | -1.7 pp | 63 | 44.4% | 35.5 |
| 3 | Portland | 56.2 | Strong | -1.3% | -2.1% | +0.3% | 95.2% | -1.1 pp | 54 | 47.1% | N/A3 |
| 4 | Atlanta | 50.5 | Watch | -0.5% | -1.7% | +0.2% | 93.5% | -1.9 pp | 68 | 50.3% | 49.3 |
| 5 | Phoenix | 48.3 | Watch | -2.1% | -1.9% | -3.0% | 93.3% | -1.9 pp | 67 | 40.1% | 62.0 |
| 6 | Dallas-Fort Worth | 43.9 | Weak | -1.5% | -1.1% | -1.2% | 92.7% | -2.6 pp | 74 | 54.2% | 52.8 |
| 7 | Austin | 41.2 | Weak | -2.4% | -3.1% | -3.5% | 92.6% | -1.7 pp | 72 | 54.9% | 60.6 |
| 8 | San Antonio | 35.1 | Weak | -1.8% | -3.6% | -3.8% | 92.0% | -3.0 pp | 79 | 58.3% | 50.8 |
How to read the ranking
Where recap demand will be highest
A recap is needed when a loan comes due and the property is worth less than the debt, so the owner has to bring in new equity. I sized that gap from three things: how far cap rates have risen since peak pricing, how rents have moved, and how much new supply is still coming.
Refinancing is also getting more expensive. One industry source reports the Fed raised its target rate to 3.75–4.00% this month, with the 10-year Treasury near 4.94% . If that holds, the equity gap gets wider in every market.
What I'd act on: lend in LA and Miami at in-place rents and hold back on Miami insurance. Pursue recap and rescue-capital deals in Phoenix and Austin, and in DFW for volume. What would change this: if tradeouts in Austin or Phoenix turn positive for a sustained stretch, those two markets move from recap targets to recovery lending, faster than the ranking implies.
How this was scored
This is a read-only snapshot of a RealAI analysis.