mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
The financial-uplift model per ADR-0018. `estimate_valuation_uplift( current_band, target_band, current_value=None, total_cost=None)` returns a `ValuationUplift`: band-transition uplift compounded from four broker tables (MoneySupermarket / Lloyds per-step, Knight Frank / Rightmove whole-jump), taking min/max/mean across the covering sources. Always a percentage; absolute £ forms (increase at each bound + post-retrofit value) only when a current market value is supplied; the 2x ROI cap rescales the percentages and can only bite once a value is known. A non-improving jump is a clean 0% no-op. Pure function, no external dependency. Persisting it (where the value lands) and sourcing the current market value stay deferred (ADR-0018). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
84 lines
3.5 KiB
Python
84 lines
3.5 KiB
Python
"""Valuation Uplift — the percentage-primary financial-uplift model (ADR-0018).
|
|
|
|
Band-transition uplift compounded from four broker tables (MoneySupermarket,
|
|
Lloyds, Knight Frank, Rightmove); always a percentage, an absolute £ only when a
|
|
Property Valuation is supplied, capped so the £ uplift never exceeds 2x cost.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from domain.modelling.valuation import ValuationUplift, estimate_valuation_uplift
|
|
|
|
|
|
def test_band_jump_yields_percentage_uplift_without_a_current_value() -> None:
|
|
# Arrange / Act — D -> C, no current market value supplied.
|
|
uplift: ValuationUplift = estimate_valuation_uplift("D", "C")
|
|
|
|
# Assert — D->C sources: MSM 2%, Lloyds 2%, Knight Frank 3%, Rightmove 3%
|
|
# → min 2%, max 3%, mean 2.5%. The £ forms stay None with no Property Valuation.
|
|
assert abs(uplift.lower_pct - 0.02) <= 1e-9
|
|
assert abs(uplift.upper_pct - 0.03) <= 1e-9
|
|
assert abs(uplift.average_pct - 0.025) <= 1e-9
|
|
assert uplift.lower_value is None
|
|
assert uplift.upper_value is None
|
|
assert uplift.average_value is None
|
|
assert uplift.post_retrofit_value is None
|
|
|
|
|
|
def test_a_current_value_yields_absolute_pound_uplift() -> None:
|
|
# Arrange / Act — D -> C with a £200k market value, no cost cap.
|
|
uplift: ValuationUplift = estimate_valuation_uplift(
|
|
"D", "C", current_value=200_000.0
|
|
)
|
|
|
|
# Assert — £ increase at each source % plus the resulting post-retrofit value.
|
|
assert uplift.lower_value is not None and abs(uplift.lower_value - 4_000.0) <= 1e-6
|
|
assert uplift.upper_value is not None and abs(uplift.upper_value - 6_000.0) <= 1e-6
|
|
assert (
|
|
uplift.average_value is not None
|
|
and abs(uplift.average_value - 5_000.0) <= 1e-6
|
|
)
|
|
assert (
|
|
uplift.post_retrofit_value is not None
|
|
and abs(uplift.post_retrofit_value - 205_000.0) <= 1e-6
|
|
)
|
|
|
|
|
|
def test_roi_cap_rescales_the_uplift_to_twice_the_cost() -> None:
|
|
# Arrange / Act — a £1m property whose raw 2.5% average (£25k) exceeds twice
|
|
# a £10k retrofit, so the uplift is capped at 2x cost.
|
|
uplift: ValuationUplift = estimate_valuation_uplift(
|
|
"D", "C", current_value=1_000_000.0, total_cost=10_000.0
|
|
)
|
|
|
|
# Assert — average £ uplift binds to 2x cost, and the bounds rescale by the
|
|
# same 0.8 scalar (0.025 -> 0.02).
|
|
assert (
|
|
uplift.average_value is not None
|
|
and abs(uplift.average_value - 20_000.0) <= 1e-6
|
|
)
|
|
assert abs(uplift.average_pct - 0.02) <= 1e-9
|
|
assert abs(uplift.lower_pct - 0.016) <= 1e-9
|
|
assert abs(uplift.upper_pct - 0.024) <= 1e-9
|
|
|
|
|
|
def test_multi_band_jump_compounds_steps_and_takes_a_spot_source() -> None:
|
|
# Arrange / Act — F -> C: MSM/Lloyds compound three steps; Rightmove gives a
|
|
# 15% whole-jump spot; Knight Frank has no F->C entry.
|
|
uplift: ValuationUplift = estimate_valuation_uplift("F", "C")
|
|
|
|
# Assert — min = compounded MSM (~4.05%), max = Rightmove 15%, mean of the
|
|
# three covering sources.
|
|
assert abs(uplift.lower_pct - 0.040502) <= 1e-6
|
|
assert abs(uplift.upper_pct - 0.15) <= 1e-9
|
|
assert abs(uplift.average_pct - (0.040502 + 0.07476992 + 0.15) / 3) <= 1e-6
|
|
|
|
|
|
def test_no_improvement_yields_zero_uplift() -> None:
|
|
# Arrange / Act — same band in and out.
|
|
uplift: ValuationUplift = estimate_valuation_uplift("C", "C")
|
|
|
|
# Assert — every source compounds over no steps / matches no spot.
|
|
assert abs(uplift.lower_pct) <= 1e-12
|
|
assert abs(uplift.upper_pct) <= 1e-12
|
|
assert abs(uplift.average_pct) <= 1e-12
|