feat(tradein): ppm²-tier segmentation of asking→sold ratio (#928, flag OFF) #934

Merged
bot-reviewer merged 2 commits from feat/tradein-ppm2-tier-ratio into main 2026-05-31 21:22:40 +00:00

2 commits

Author SHA1 Message Date
4b0fd10cb0 fix(tradein): #928 cache-key collision, migration-098 band guard, tier-signal note
M1 (bug): flag-OFF path now caches under (bucket, '_legacy') instead of
(bucket, None), preventing stale old-table ratio being served when
tier_aware_ratio_enabled is flipped ON without process restart.

M2 (guard): add band-drift comment to migration 098 seed + regression test
asserting _PPM2_MIN==30000 and asking_ratio_ppm2_max default==1_200_000.

M3 (doc): append NOTE(#928) to estimator call-site comment explaining that
tier placement uses the pre-anchor radius median, not the same-building Tukey
anchor; flags the revisit needed before enabling the flag in production.
2026-06-01 00:13:16 +03:00
46db7ebcba feat(tradein): ppm2-tier segmentation of asking->sold ratio pool, flag-gated (#928)
Add ppm2-tier (low/mid/high) asking->sold ratio lookup behind
tier_aware_ratio_enabled feature flag (default OFF). Flag-OFF path
remains byte-identical to pre-#928 behavior.

Migration 098 (additive, idempotent):
- asking_to_sold_tier_bounds: 33.33/66.67 pct ppm2 bounds per rooms_bucket
- asking_to_sold_ratios_tiered: per-tier ratio + 'all' aggregates + global -1
  Parallel table to avoid fetchone() regression on asking_to_sold_ratios.

Task (asking_to_sold_ratio.py):
- Extends recompute_asking_to_sold_ratios with tiered DELETE + re-derive
  in same transaction (atomic with old table refresh).
- Two new counters: tiered_rows_written, tier_bounds_written.

Config (config.py):
- tier_aware_ratio_enabled: bool = False  (feature flag, ENV: TIER_AWARE_RATIO_ENABLED)
- tier_ratio_shrink_k: int = 150  (empirical-Bayes shrink K, ENV: TIER_RATIO_SHRINK_K)

Estimator (estimator.py):
- _get_asking_sold_ratio gains anchor_ppm2 param (default None, backward-compat).
- Cache key changed from int to tuple[int, str|None] (bucket, tier).
- Flag-ON path: bounds lookup -> tier assign -> shrunk ratio w/ Bayes shrink.
- Fallback chain: tier -> 'all' -> global_all -> (None, None).

Tests:
- tests/services/test_asking_sold_tier_ratio.py: 8 new unit tests.
- tests/test_asking_to_sold_ratio.py: updated FakeDB (8 execute calls) + counter assertions.

Refs #928
2026-05-31 23:58:46 +03:00