col: simulator auto-adjusts spending to local prices via Numbeo+Expatistan
The Monte Carlo used to compare jurisdictions at a flat London-equivalent spend, which silently overstated the cost-of-living for any move to a cheaper region. Now every cross-jurisdiction simulation auto-scales spending_gbp by the real Numbeo/Expatistan ratio between the user's baseline city and the target city. Architecture: - fire_planner/col/baseline.py — 22 cities with headline Numbeo data (source URLs + snapshot dates embedded) — fallback when scraper fails - col/numbeo.py + col/expatistan.py — httpx async scrapers, regex-parsed, polite 1.1s rate-limit, EUR/USD anchored - col/cache.py — PG-backed cache (col_snapshot table, 1-year TTL) - col/service.py — sync compute_col_ratio() for the simulator; async lookup_city_cached() with source reconciliation for the refresh CronJob - alembic 0005 — col_snapshot table, UNIQUE(city_slug, source_name) Simulator wiring: - SimulateRequest gains col_auto_adjust=True (default), col_baseline_city, col_target_city. Defaults pick the jurisdiction's representative city. - _resolve_col_adjustment scales spending_gbp before path-building. - SimulateResult surfaces col_multiplier_applied + col_adjusted_spending_gbp. CLIs: - python -m fire_planner col-seed — loads BASELINES into col_snapshot (post-migration seed step) - python -m fire_planner col-refresh-stale --within-days 7 — used by the weekly fire-planner-col-refresh CronJob 268 tests pass. Mypy strict + ruff clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
70101c836c
commit
e72fd22a17
14 changed files with 1641 additions and 6 deletions
|
|
@ -504,6 +504,22 @@ class SimulateRequest(BaseModel):
|
|||
annual_real_adjust_pct: Decimal = Decimal("0")
|
||||
guardrail_threshold_pct: Decimal | None = None
|
||||
guardrail_cut_pct: Decimal = Decimal("0.10")
|
||||
# Cost-of-living auto-adjust: when `col_auto_adjust=True`, the
|
||||
# simulator looks up COL ratio (target/baseline) from `fire_planner.col`
|
||||
# and scales `spending_gbp` BEFORE running paths. Defaults to True so
|
||||
# cross-jurisdiction comparisons are honest by default — earlier
|
||||
# comparisons used hand-wave 0.5x/0.75x multipliers, which were
|
||||
# consistently optimistic vs. actual Numbeo data (Bulgaria is 0.41x,
|
||||
# not 0.50x; Cyprus 0.67x, not 0.75x).
|
||||
#
|
||||
# `col_target_city` defaults to the jurisdiction's representative
|
||||
# city (uk→london, cyprus→limassol, etc.). Set explicitly to anchor
|
||||
# on a different city (e.g. `cyprus`+`paphos` if Limassol is too
|
||||
# expensive a proxy). For `jurisdiction='nomad'` there is no
|
||||
# representative city and auto-adjust is skipped silently.
|
||||
col_auto_adjust: bool = True
|
||||
col_baseline_city: str = "london"
|
||||
col_target_city: str | None = None
|
||||
|
||||
|
||||
class SimulateResult(BaseModel):
|
||||
|
|
@ -516,6 +532,13 @@ class SimulateResult(BaseModel):
|
|||
elapsed_seconds: Decimal
|
||||
yearly: list[ProjectionPoint]
|
||||
goals_probability: list[GoalProbability] = Field(default_factory=list)
|
||||
# When `col_auto_adjust=True`, surface the applied multiplier + the
|
||||
# COL-adjusted spending so the user can see what was used. Null when
|
||||
# auto-adjust was off, jurisdiction had no representative city
|
||||
# (nomad), or baseline==target (London-to-London).
|
||||
col_multiplier_applied: Decimal | None = None
|
||||
col_adjusted_spending_gbp: Decimal | None = None
|
||||
col_target_city: str | None = None
|
||||
|
||||
|
||||
class CompareRequest(BaseModel):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue