diff --git a/backend/app/schemas/recommend.py b/backend/app/schemas/recommend.py
index 0139e0e1..81236116 100644
--- a/backend/app/schemas/recommend.py
+++ b/backend/app/schemas/recommend.py
@@ -17,7 +17,9 @@ class RecommendMixInput(BaseModel):
months_window: int = Field(default=12, ge=3, le=36)
# Velocity / pricing scenario knobs (live-tuned client-side; backend just
# ships base coefficients so frontend can recompute without round-trips).
- price_factor: float = Field(default=1.0, ge=0.5, le=2.0)
+ # 0.01..3.0 = -99%..+200% к рынку. min=0.01 (а не 0) чтобы избежать
+ # деления на 0 / pf^elasticity = ∞ при «бесплатной» цене.
+ price_factor: float = Field(default=1.0, ge=0.01, le=3.0)
target_months: int | None = Field(default=None, ge=3, le=120)
diff --git a/frontend/src/app/analytics/recommend/page.tsx b/frontend/src/app/analytics/recommend/page.tsx
index 7286a298..065a68bf 100644
--- a/frontend/src/app/analytics/recommend/page.tsx
+++ b/frontend/src/app/analytics/recommend/page.tsx
@@ -68,20 +68,9 @@ export default function RecommendPage() {
});
}, [data, effectiveShares, input.area_total_m2]);
- const totalRevenue = derivedRows.reduce(
- (a, r) => a + (r.effective_revenue_rub ?? 0),
- 0,
- );
- const weightedAvgPrice = useMemo(() => {
- let num = 0;
- let den = 0;
- for (const r of derivedRows) {
- const w = r.effective_share_pct * r.area_avg_m2;
- num += w * r.price_median_per_m2;
- den += w;
- }
- return den > 0 ? num / den : null;
- }, [derivedRows]);
+ // totalRevenue и weightedAvgPrice сейчас уже отображаются внутри
+ // RecommendVelocityPanel и headline summary — здесь больше не считаем,
+ // чтобы не дублировать вычисления и держать одну точку правды для KPI.
const errMsg = mutation.error
? mutation.error instanceof Error
@@ -190,6 +179,21 @@ export default function RecommendPage() {
/>
+ {/* Mix sliders — сразу под KPI/слайдером цены, чтобы при движении
+ было видно как меняются метрики и графики live. */}
+
+ b.bucket)}
+ shares={effectiveShares}
+ recommended={recommendedShares}
+ onChange={setOverrideShares}
+ onReset={() => setOverrideShares(null)}
+ />
+
+
{/* Liquidity chart — кумулятивная кривая продаж */}
{hasAllocation ? (
) : null}
+ {/* District-specific KPIs — что отличается между районами:
+ competitors, sale_graph покрытие, district/class факторы.
+ Раньше тут были city-wide дубли (Сделок 3815 / Weighted /
+ Выручка) — все одинаковые независимо от района. */}
-
-
-
+
+
+
b.bucket)}
@@ -263,19 +286,6 @@ export default function RecommendPage() {
) : null}
-
- b.bucket)}
- shares={effectiveShares}
- recommended={recommendedShares}
- onChange={setOverrideShares}
- onReset={() => setOverrideShares(null)}
- />
-
-
) : null}
- {/* Price factor slider */}
+ {/* Price factor slider — 0.01..3.0 (от -99% до +200%).
+ min=0.01 (а не 0) чтобы избежать pf^elasticity = ∞ при свободной
+ цене. */}
Цена ± к рынку
onPriceFactorChange(Number(e.target.value))}
@@ -224,7 +226,7 @@ export function RecommendVelocityPanel({
{requiredPct}% к рынку).{" "}