What are the biggest risks facing multifamily investors in Charlotte over a five-year hold? Help me understand what could go wrong and how likely each scenario is. Show me the trends I need to be the most concerned with.
cat /vercel/sandbox/skills/forecasting/references/request.md | head -200
mkdir -p /vercel/sandbox/fc && cat > /vercel/sandbox/fc/build_rent.py << 'EOF'
import json
# Monthly mf_rent_ts data for Charlotte, NC (market id 16740), asking_rent_latest_avg
# transcribed from query_data tool result (mf_rent_ts monthly, 2024-07 through 2026-08)
raw = [
("2024-07-01",1753.969848),
("2024-08-01",1753.171875),
("2024-09-01",1754.279629),
("2024-10-01",1740.692272),
("2024-11-01",1769.705247),
("2024-12-01",1775.797513),
("2025-01-01",1788.354141),
("2025-02-01",1811.304485),
("2025-03-01",1806.699566),
("2025-04-01",1821.247286),
("2025-05-01",1797.914021),
("2025-06-01",1795.295299),
("2025-07-01",1785.882117),
("2025-08-01",1775.170864),
("2025-09-01",1774.527958),
("2025-10-01",1768.814775),
("2025-11-01",1767.561487),
("2025-12-01",1773.278777),
("2026-01-01",1790.853088),
("2026-02-01",1788.405708),
("2026-03-01",1775.853021),
("2026-04-01",1782.821672),
("2026-05-01",1782.040846),
("2026-06-01",1774.490162),
("2026-07-01",1768.612978),
("2026-08-01",1758.452463),
]
history = [{"period": p, "value": v, "source": "datamart"} for p, v in raw]
history_check = {"count": len(raw), "sum": sum(v for _, v in raw), "first_value": raw[0][1], "last_value": raw[-1][1]}
payload = {
"metric": {"name": "mf_rent_ts.asking_rent_latest_avg — Charlotte, NC MSA", "units": "$", "family": "rent_or_occupancy"},
"subject": {"entity_type": "market", "entity_id": "16740", "label": "Charlotte, NC"},
"horizon": {"years": 5, "intervals": "annual"},
"as_of": "2026-09-11",
"caller_disclosures": {
"peer_omitted_reason": "not gathered",
"sibling_series_note": "In-place rent (sibling) run as separate call; t12 asking -0.23% vs in-place +0.03% — under 100bps divergence, both run individually rather than combined.",
"sibling_divergence_pct": 0.26,
"lookback_note": "Full available monthly history from RealAI Rent Index (25 months, 2024-07 to 2026-08); prior months had zero sample size and were dropped.",
"signals_omitted": [{"signal": "migration.inbound_income/outbound_income", "reason": "sourced separately, see context_signals"}]
},
"history": history,
"history_check": history_check,
"context_signals": {
"supply_pipeline": {
"existing_stock": 220961,
"under_construction_t12": 16508,
"permitted_units_t13_t24": 6610
},
"migration": {
"inbound_income": 124599,
"outbound_income": 125286
},
"employment": {
"job_growth_1_year_pct": 0.0399
},
"structural_ceiling": {
"ratio_current": 0.2934,
"ratio_ceiling": 0.35
}
},
"scenarios": ["base","upside","downside"]
}
with open("/vercel/sandbox/fc/rent_payload.json","w") as f:
json.dump(payload, f, indent=2)
print("done", len(raw))
EOF
python3 /vercel/sandbox/fc/build_rent.pycat > /vercel/sandbox/fc/build_others.py << 'EOF'
import json
inplace_raw = [
("2024-07-01",1577.495748),
("2024-08-01",1584.859514),
("2024-09-01",1591.670307),
("2024-10-01",1591.086693),
("2024-11-01",1592.503817),
("2024-12-01",1591.676417),
("2025-01-01",1593.273694),
("2025-02-01",1597.277551),
("2025-03-01",1601.306034),
("2025-04-01",1606.092707),
("2025-05-01",1610.102446),
("2025-06-01",1615.758083),
("2025-07-01",1617.333695),
("2025-08-01",1623.704515),
("2025-09-01",1623.962995),
("2025-10-01",1624.488346),
("2025-11-01",1624.281588),
("2025-12-01",1622.310479),
("2026-01-01",1622.504010),
("2026-02-01",1623.212987),
("2026-03-01",1626.251516),
("2026-04-01",1627.629909),
("2026-05-01",1628.801013),
("2026-06-01",1630.565078),
("2026-07-01",1629.342185),
("2026-08-01",1625.495258),
]
occ_raw = [
("2024-07-01",0.9752988083870004),
("2024-08-01",0.9716465801657306),
("2024-09-01",0.9684650232393952),
("2024-10-01",0.9668719819334634),
("2024-11-01",0.9661523979344772),
("2024-12-01",0.9662881685003236),
("2025-01-01",0.9621064350722526),
("2025-02-01",0.9594724860948313),
("2025-03-01",0.9588886726616914),
("2025-04-01",0.9636225397242048),
("2025-05-01",0.9579790098705201),
("2025-06-01",0.959047071655179),
("2025-07-01",0.956548893243604),
("2025-08-01",0.9559243486407103),
("2025-09-01",0.9523626341300049),
("2025-10-01",0.9475020478727015),
("2025-11-01",0.9474839451305886),
("2025-12-01",0.9448726245808083),
("2026-01-01",0.9432026466208969),
("2026-02-01",0.9424106516534592),
("2026-03-01",0.9461081367300112),
("2026-04-01",0.9460764569313137),
("2026-05-01",0.9370160345038264),
("2026-06-01",0.9360294350586755),
("2026-07-01",0.933223510031182),
("2026-08-01",0.9252220980172972),
]
def mk(raw, name, units, extra_ctx=None):
history = [{"period": p, "value": v, "source": "datamart"} for p, v in raw]
hc = {"count": len(raw), "sum": sum(v for _, v in raw), "first_value": raw[0][1], "last_value": raw[-1][1]}
payload = {
"metric": {"name": name, "units": units, "family": "rent_or_occupancy"},
"subject": {"entity_type": "market", "entity_id": "16740", "label": "Charlotte, NC"},
"horizon": {"years": 5, "intervals": "annual"},
"as_of": "2026-09-11",
"caller_disclosures": {
"peer_omitted_reason": "not gathered",
"lookback_note": "Full available monthly history from RealAI Rent Index (26 months, 2024-07 to 2026-08).",
},
"history": history,
"history_check": hc,
"context_signals": {
"supply_pipeline": {
"existing_stock": 220961,
"under_construction_t12": 16508,
"permitted_units_t13_t24": 6610
},
"migration": {
"inbound_income": 124599,
"outbound_income": 125286
},
"employment": {
"job_growth_1_year_pct": 0.0399
}
},
"scenarios": ["base","upside","downside"]
}
return payload
with open("/vercel/sandbox/fc/inplace_payload.json","w") as f:
json.dump(mk(inplace_raw, "mf_rent_ts.in_place_rent_latest_avg — Charlotte, NC MSA", "$"), f, indent=2)
with open("/vercel/sandbox/fc/occ_payload.json","w") as f:
json.dump(mk(occ_raw, "mf_rent_ts.occupancy_latest — Charlotte, NC MSA", "%"), f, indent=2)
# Cap rate directional payload - full history
cap_raw = [
("2011-07-01",0.0663),("2011-10-01",0.0662),
("2012-01-01",0.0639),("2012-04-01",0.0627),("2012-07-01",0.0635),("2012-10-01",0.0634),
("2013-01-01",0.0628),("2013-04-01",0.0616),("2013-07-01",0.0629),("2013-10-01",0.0642),
("2014-01-01",0.0642),("2014-04-01",0.0642),("2014-07-01",0.0607),("2014-10-01",0.0583),
("2015-01-01",0.0573),("2015-04-01",0.0560),("2015-07-01",0.0550),("2015-10-01",0.0534),
("2016-01-01",0.0534),("2016-04-01",0.0531),("2016-07-01",0.0531),("2016-10-01",0.0531),
("2017-01-01",0.0531),("2017-04-01",0.0530),("2017-07-01",0.0530),("2017-10-01",0.0529),
("2018-01-01",0.0527),("2018-04-01",0.0528),("2018-07-01",0.0518),("2018-10-01",0.0517),
("2019-01-01",0.0505),("2019-04-01",0.0505),("2019-07-01",0.0479),("2019-10-01",0.0479),
("2020-01-01",0.0467),("2020-04-01",0.0493),("2020-07-01",0.0451),("2020-10-01",0.0440),
("2021-01-01",0.0440),("2021-04-01",0.0398),("2021-07-01",0.0382),("2021-10-01",0.0369),
("2022-01-01",0.0385),("2022-04-01",0.0416),("2022-07-01",0.0425),("2022-10-01",0.0501),
("2023-01-01",0.0517),("2023-04-01",0.0517),("2023-07-01",0.0579),("2023-10-01",0.0553),
("2024-01-01",0.0554),("2024-04-01",0.0528),("2024-07-01",0.0513),("2024-10-01",0.0513),
("2025-01-01",0.0513),("2025-04-01",0.0513),("2025-07-01",0.0513),("2025-10-01",0.0513),
("2026-01-01",0.0514),("2026-04-01",0.0514),
]
history = [{"period": p, "value": v, "source": "datamart"} for p, v in cap_raw]
hc = {"count": len(cap_raw), "sum": sum(v for _, v in cap_raw), "first_value": cap_raw[0][1], "last_value": cap_raw[-1][1]}
cap_payload = {
"metric": {"name": "caprate_ts.multifamily — Charlotte, NC MSA", "units": "%", "family": "capital_markets"},
"subject": {"entity_type": "market", "entity_id": "16740", "label": "Charlotte, NC"},
"horizon": {"years": 5, "intervals": "annual"},
"as_of": "2026-09-11",
"caller_disclosures": {
"rate_instrument": "fed_funds",
"lookback_note": "Full available quarterly history from GreenStreet Market Cap Rates, 3Q11 through 2Q26 (60 quarters)."
},
"history": history,
"history_check": hc,
"context_signals": {
"rate_environment": {
"current_rate": 4.33,
"projected_rate": 3.4,
"projected_as_of": "2027",
"direction": "easing"
}
}
}
with open("/vercel/sandbox/fc/cap_payload.json","w") as f:
json.dump(cap_payload, f, indent=2)
print("built")
EOF
python3 /vercel/sandbox/fc/build_others.py "context_signals": {
"rate_environment": {
"current_rate": 4.33,
"projected_rate": 3.4,
"projected_as_of": "2027",
"direction": "easing"
}
}
} "context_signals": {
"rate_environment": {
"current_rate": 3.63,
"projected_rate": 3.6,
"projected_as_of": "2027",
"direction": "easing"
}
}
}cd /vercel/sandbox && python3 fc/build_others.py && python3 skills/forecasting/scripts/forecast.py --version
cd /vercel/sandbox && for f in rent_payload inplace_payload occ_payload cap_payload; do echo "=== $f ===" python3 skills/forecasting/scripts/forecast.py --file fc/$f.json 2>&1 | python3 -m json.tool | head -80 echo done
cd /vercel/sandbox && for f in rent_payload inplace_payload occ_payload; do
echo "=== $f ==="
python3 skills/forecasting/scripts/forecast.py --file fc/$f.json 2>&1 | python3 -c "
import json,sys
d=json.load(sys.stdin)
print('confidence:', d.get('confidence'))
print('flags:', d.get('data_quality_flags'))
print('downside:', d.get('downside_case'))
print('methodology:', json.dumps(d.get('methodology'), indent=2))
"
echo
doneCharlotte multifamily's biggest five-year risk isn't a single shock — it's that three separate stress signals (supply, rent momentum, and renter credit) are flashing at the same time this year, while migration and cap rates carry the slower-moving risk.
Risk register — five-year hold
| Risk | Probability over the hold | When it bites | Current signal |
|---|---|---|---|
| Supply overhang extends lease-up drag | High — already underway | Year 1-2 | 16,508 units under construction (7.5% of 220,961-unit stock); YTD deliveries of 9,050 units outpacing net absorption of 5,036; occupancy down 412bps in 12 months to 91.2%; vacancy at 12.3% |
| Flat-to-negative rent growth persists | Medium-High | Year 1-2, easing after | Asking rent down 0.2%; new-lease tradeouts averaging -1.6% over the trailing year; permits decelerating slightly (T12 6,375 vs. T13-24 6,610)1 |
| Renter credit deterioration hits collections/bad debt | Medium, building | Ongoing, compounds late in hold if unaddressed | FICO down 0.9pts in 12 months to 691; total past-due balances up 67.6% YoY; mortgage delinquency rate up 109bps to 5.33%2 |
| Cap rate re-expansion compresses exit value | Medium | Exit-dependent, Year 5 | Cap rate at 5.14%, up ~130bps from the 2021 trough of 3.69%3 |
| Migrant cohort quality softens the rent growth ceiling | Low-Medium, slow-moving | Cumulative over the hold | Net migration positive but only average (67th percentile nationally); outbound migrants show higher net worth (tier 6.1 vs. 4.3 inbound) and investable resources (tier 9.1 vs. 7.9 inbound) |
1. Supply is the dominant near-term risk, and it's already showing up in the numbers. Occupancy has fallen from 95.3% to 91.2% over the past 12 months , a fast, real slide — not noise. With 16,508 units under construction against a 220,961-unit base , and deliveries running well ahead of absorption (9,050 delivered YTD vs. 5,036 net absorbed) , Charlotte is mid-digestion of its own construction boom. The good news: permits are no longer accelerating — trailing-12-month permits (6,375) are running slightly below the prior 12 months (6,610) — so new starts have already begun to moderate, which should let the pipeline drain by roughly year 2-3 of a five-year hold rather than persist throughout it.
2. Rent growth is flat now, and the forecast confidence on the recovery is only medium. New-lease tradeouts have averaged -1.6% over the trailing year, meaning tenants are re-leasing units for less than the outgoing tenant paid — a leading indicator, and it's been negative for most of the last 12 months . Running Charlotte's 26-month asking-rent history through the forecast engine, the base case shows growth reaccelerating from roughly flat in year 1 to 2.4% by year 5 (asking rent from $1,758 to $1,874) , with in-place rent compounding from $1,625 to $1,800 (2.6% terminal growth) — but the engine flagged medium confidence on both, because the only history available sits entirely inside a single post-2020 regime and can't be leveled against a full cycle . The downside case is materially worse: asking rent drifting down to $1,638 by year 5 (-7% cumulative) if the supply-driven softening doesn't resolve on schedule . The occupancy forecast itself came back low confidence with a "cyclical drawdown suspected" flag — the engine held occupancy flat near 91% after year 1 rather than assuming a rebound, explicitly warning not to treat that as a multi-year assumption without a defended recovery view . Given the permit deceleration noted above, a recovery by year 2-3 is the reasonable read, but it is analyst judgment layered on the flag, not the engine's own claim.
3. Renter credit stress is the risk least visible in headline affordability. Rent-to-income sits at a comfortable 29.3%, below the national average (31st percentile) — on that metric alone, Charlotte renters have room. But underneath it, credit quality is eroding fast: total past-due balances are up 67.6% year-over-year, mortgage delinquency is up 109bps to 5.33%, and the average FICO score has slipped for three straight quarters to 691 . That combination — comfortable current affordability, deteriorating trajectory — is exactly the setup that turns into higher bad debt and slower collections partway through a hold, well before it shows up in the rent-to-income ratio.
4. Cap rates carry the exit-timing risk, and the read genuinely depends on which window you trust. At 5.14%, Charlotte multifamily cap rates sit in the middle of their full 15-year range (49th percentile) but in the "rich" two-thirds of the trailing 10-year range (69th percentile) — the market has re-priced up ~130bps from the 2021 trough of 3.69% but hasn't round-tripped to pre-2020 norms (mid-5% to low-6%) . The Fed funds rate sits at 3.63% today with projections easing only modestly to ~3.6% by 2027 — a stable-to-slightly-easing rate backdrop, which argues against further material cap rate expansion but doesn't promise compression either. A year-5 exit into materially higher cap rates than today is a real but not high-probability risk given where rates are headed.
5. Migration is a slow burn, not a near-term threat. Net migration is positive but unremarkable (67th percentile nationally) , and the cohort quality has a soft edge: households leaving Charlotte carry meaningfully more wealth (net worth tier 6.1) and investable resources (tier 9.1) than those arriving (tiers 4.3 and 7.9) . This won't move rents in year one, but a persistent gap like this over a five-year hold is exactly what caps how much rent growth a market can sustainably support — it's the reason the base-case forecast above tops out at modest, not exceptional, terminal growth.
Act on this now: underwrite year 1-2 rent growth at the flat-to-slightly-negative end of the range shown here, not the year-5 terminal rate, and stress-test debt coverage against the downside occupancy path (high-80s) rather than assuming a quick snap-back. What would flip this posture: a clear turn in net absorption relative to deliveries over the next two quarters — that single data point tells you whether the supply digestion is resolving on the 2-3 year schedule the permit deceleration implies, or running longer.
This is a read-only snapshot of a RealAI analysis.