fix(ai): realistic pre-market rent recommendation — annual units + anchor on current price
- getMarketRent now returns CHF/m²/YEAR (stored medians are monthly → ×12); fixes annual-vs-monthly mismatch that also affected MarketPricePanel and NegotiationInsightsPanel - recommendPreMarketRent: anchor on the unit's current rent and adjust within a bounded ±15% by market momentum (vacancy=supply, demand strength, days-on-market, trend) instead of jumping to the city-wide (prime-skewed) median — no more "double the price" suggestions; median only used as a headroom check - tests updated to annual market values Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -364,38 +364,41 @@ export const MockAIService: IAIService = {
|
||||
return { data, provenance: mockProvenance() }
|
||||
}
|
||||
|
||||
// Angebot/Nachfrage-Anpassung auf die regionale Vergleichsmiete
|
||||
let adj = 0
|
||||
if (intel.vacancyRatePct < 2.5) adj += 0.06
|
||||
// Empfehlung am HEUTIGEN Preis des Objekts verankert (realistisch) und nur begrenzt
|
||||
// nach Marktmomentum (Angebot/Nachfrage/Trend) angepasst — keine Sprünge auf den
|
||||
// stadtweiten Median (segment-grob). Vergleichsmiete dient nur als Spielraum-Check.
|
||||
let adj = intel.rentTrend12m / 100 // Trend vorwärts (Pre-Market liegt in der Zukunft)
|
||||
if (intel.vacancyRatePct < 2.5) adj += 0.04
|
||||
else if (intel.vacancyRatePct < 4) adj += 0.02
|
||||
else if (intel.vacancyRatePct > 5.5) adj -= 0.06
|
||||
else if (intel.vacancyRatePct > 4.5) adj -= 0.03
|
||||
adj += { VERY_HIGH: 0.06, HIGH: 0.03, MEDIUM: 0, LOW: -0.05 }[intel.demandStrength]
|
||||
if (intel.avgDaysOnMarket < 35) adj += 0.02
|
||||
else if (intel.avgDaysOnMarket > 75) adj -= 0.03
|
||||
const trendFwd = intel.rentTrend12m / 100 // Pre-Market liegt in der Zukunft → Trend vorwärts
|
||||
else if (intel.vacancyRatePct > 5.5) adj -= 0.05
|
||||
else if (intel.vacancyRatePct > 4.5) adj -= 0.02
|
||||
adj += { VERY_HIGH: 0.05, HIGH: 0.025, MEDIUM: 0, LOW: -0.04 }[intel.demandStrength]
|
||||
if (intel.avgDaysOnMarket < 35) adj += 0.015
|
||||
else if (intel.avgDaysOnMarket > 75) adj -= 0.025
|
||||
// Spielraum-Check: liegt der heutige Preis bereits über dem regionalen Marktband → kaum Luft nach oben
|
||||
if (comp != null && current >= comp) adj = Math.min(adj, 0.02)
|
||||
adj = Math.max(-0.10, Math.min(0.15, adj)) // realistischer Rahmen: −10 % … +15 %
|
||||
|
||||
const recommended = Math.round(comp * (1 + adj + trendFwd))
|
||||
const rangeMin = Math.round(recommended * 0.93)
|
||||
const rangeMax = Math.round(recommended * 1.07)
|
||||
const deltaVsCurrentPct = Math.round(((recommended - current) / current) * 100)
|
||||
const recommended = Math.round(current * (1 + adj))
|
||||
const rangeMin = Math.round(recommended * 0.95)
|
||||
const rangeMax = Math.round(recommended * 1.05)
|
||||
const deltaVsCurrentPct = Math.round(adj * 100)
|
||||
const verdict: PreMarketRentRecommendation['verdict'] =
|
||||
deltaVsCurrentPct >= 6 ? 'UNDERPRICED' : deltaVsCurrentPct <= -6 ? 'AMBITIOUS' : 'FAIR'
|
||||
deltaVsCurrentPct >= 5 ? 'UNDERPRICED' : deltaVsCurrentPct <= -4 ? 'AMBITIOUS' : 'FAIR'
|
||||
|
||||
const supplyLabel = intel.vacancyRatePct < 3 ? 'sehr knappes Angebot' : intel.vacancyRatePct > 5 ? 'entspanntes Angebot' : 'ausgeglichenes Angebot'
|
||||
const demandLabel = { VERY_HIGH: 'sehr hohe Nachfrage', HIGH: 'hohe Nachfrage', MEDIUM: 'mittlere Nachfrage', LOW: 'schwache Nachfrage' }[intel.demandStrength]
|
||||
const drivers = [
|
||||
`Vergleichsmiete Region: CHF ${comp}/m²`,
|
||||
`Leerstand ${intel.vacancyRatePct}% (${supplyLabel})`,
|
||||
demandLabel,
|
||||
`Miettrend ${intel.rentTrend12m >= 0 ? '+' : ''}${intel.rentTrend12m}% (12 M)`,
|
||||
`Ø Vermietungsdauer ${intel.avgDaysOnMarket} Tage`,
|
||||
]
|
||||
const verdictText =
|
||||
verdict === 'UNDERPRICED' ? `Ihr heutiger Preis (CHF ${current}/m²) liegt ${Math.abs(deltaVsCurrentPct)}% unter der Empfehlung — klarer Spielraum nach oben.`
|
||||
: verdict === 'AMBITIOUS' ? `Ihr heutiger Preis liegt ${Math.abs(deltaVsCurrentPct)}% über der Markteinschätzung — ambitioniert.`
|
||||
: 'Ihr heutiger Preis ist marktgerecht.'
|
||||
const rationale = `Auf Basis vergleichbarer ${assetLabel} in ${input.city} (Median CHF ${comp}/m²), ${supplyLabel} und ${demandLabel}. Empfehlung für Pre-Market: CHF ${recommended}/m² (CHF ${rangeMin}–${rangeMax}). ${verdictText}`
|
||||
verdict === 'UNDERPRICED' ? `Marktumfeld lässt Spielraum nach oben (+${deltaVsCurrentPct}% ggü. heute).`
|
||||
: verdict === 'AMBITIOUS' ? `Marktumfeld eher schwächer (${deltaVsCurrentPct}% ggü. heute) — vorsichtig ansetzen.`
|
||||
: 'Heutiger Preis ist marktgerecht.'
|
||||
const rationale = `${assetLabel} in ${input.city}: ${supplyLabel}, ${demandLabel}, Miettrend ${intel.rentTrend12m >= 0 ? '+' : ''}${intel.rentTrend12m}%. Empfehlung für Pre-Market: CHF ${recommended}/m² (CHF ${rangeMin}–${rangeMax}) — verankert am heutigen Preis CHF ${current}/m². ${verdictText}`
|
||||
const confidence: PreMarketRentRecommendation['confidence'] =
|
||||
intel.demandStrength === 'LOW' || intel.avgDaysOnMarket > 75 ? 'MEDIUM' : 'HIGH'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user