Scraper resume: - /admin/scrape/queue autoclean уменьшен с 60min started_at до 10min COALESCE(heartbeat_at, started_at). Не режет валидные long sweeps. - Phase A пишет heartbeat после каждого objStatus fetch (3min Phase A больше не рискует быть отмеченной как zombie при новом 10min threshold). Recommend (Уровень 1 калькулятор): - POST /api/v1/analytics/recommend/mix с velocity baseline (sale_graph), price elasticity (regr_slope/r2 на sale_graph, fallback -1.5), inverse mode (target_months → required price_factor), liquidity score 24mo, headline. - /analytics/recommend: RecommendVelocityPanel (price slider 0.85..1.15 + 4 KPI Чек/Срок/Темп/Ликвидность + методология эластичности), RecommendLiquidityChart (cumulative 0..36 mo с пунктиром на 24). - BucketsTable: +колонки Темп и Срок, цены/выручка масштабируются по priceFactor live. - Все слайдеры и target_months считаются клиентски — никаких round-trip. Fix: SQLAlchemy не парсит :cls::text — заменено на CAST(:cls AS TEXT).
55 lines
1.8 KiB
Python
55 lines
1.8 KiB
Python
"""IO contracts for the rule-based квартирография recommender.
|
|
|
|
POST /api/v1/analytics/recommend/mix
|
|
"""
|
|
|
|
from typing import Any, Literal
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
ClassLiteral = Literal["Comfort", "Comfort+", "Business", "Elite"]
|
|
|
|
|
|
class RecommendMixInput(BaseModel):
|
|
district_name: str = Field(..., min_length=2, max_length=80)
|
|
area_total_m2: float | None = Field(default=None, ge=100, le=500_000)
|
|
target_class: ClassLiteral | None = None
|
|
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)
|
|
target_months: int | None = Field(default=None, ge=3, le=120)
|
|
|
|
|
|
class RecommendBucket(BaseModel):
|
|
bucket: str
|
|
share_pct: float
|
|
deal_count: int
|
|
area_avg_m2: float
|
|
area_median_m2: float
|
|
price_median_per_m2: float
|
|
price_p25_per_m2: float
|
|
price_p75_per_m2: float
|
|
units_planned: int | None = None
|
|
revenue_planned_rub: float | None = None
|
|
# Velocity baseline (units/month for THIS project allocated to this bucket
|
|
# at price_factor=1.0). Frontend scales by price_factor^elasticity for live
|
|
# what-if recompute.
|
|
velocity_per_month: float | None = None
|
|
months_to_sellout: float | None = None
|
|
|
|
|
|
class RecommendComparable(BaseModel):
|
|
obj_id: int
|
|
comm_name: str | None = None
|
|
dev_name: str | None = None
|
|
obj_class: str | None = None
|
|
flat_count: int | None = None
|
|
sold_perc: float | None = None
|
|
|
|
|
|
class RecommendMixOutput(BaseModel):
|
|
scope: dict[str, Any]
|
|
buckets: list[RecommendBucket]
|
|
summary: dict[str, Any]
|
|
comparables: list[RecommendComparable]
|