returns: 3 models — Shiller bootstrap (default), manual %, Wealthfolio history
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Adds a "Returns model" picker on /what-if that switches how the
simulator's `paths` (n_paths × n_years × 3) is built:
1. shiller (default) — current behaviour, block-bootstrap of the
Shiller 1871+ historical series (or its synthetic-calibrated
fallback when the CSV isn't mounted).
2. manual — every year of every path = the user's "real return %"
input. Deterministic, no fan, useful for sanity checks. New
helper `constant_real_return_paths` constructs the (n_paths,
n_years, 3) tensor with stock=bond=real, cpi=0 so the simulator's
`(1+nominal)/(1+cpi)-1` short-circuits to exactly the input.
3. wealthfolio — pulls daily_account_valuation from the wealthfolio_sync
PG mirror, sums total_value + net_contribution across accounts per
day (FX-adjusted), strips contribution deltas to isolate market
return, compounds daily returns into per-calendar-year samples,
block-bootstraps with block_size=1 (only ~6 distinct samples
available, no serial-correlation signal to preserve). Glide path
is a no-op in this mode — the user's actual blended portfolio is
treated as a single asset.
API: SimulateRequest gains `returns_mode` ("shiller"|"manual"|
"wealthfolio") + `manual_real_return_pct`. simulate.py's `_build_paths`
dispatches; wealthfolio mode opens a transient session against the
mirror DB.
UI: new Field on the form (next to Strategy / Glide path) with a
contextual hint that explains each option's tradeoff. The "About the
model" panel at the bottom now has a "Returns model" section
mirroring the same content. The Manual % input only shows when
returns_mode='manual'.
10 new tests on the Wealthfolio helper (contribution-stripping,
multi-account aggregation, FX, partial-year drop, TOTAL filter,
empty-input, plus 3 deterministic-paths tests). 198 backend tests +
7 frontend tests. mypy strict + ruff + tsc strict all pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
f2c36bc4a3
commit
00ec874889
6 changed files with 515 additions and 11 deletions
|
|
@ -226,6 +226,18 @@ class SimulateRequest(BaseModel):
|
|||
n_paths: int = Field(ge=100, le=50_000, default=5_000)
|
||||
seed: int = 42
|
||||
life_events: list[LifeEventInput] = Field(default_factory=list)
|
||||
# Returns model — controls how `paths` (n_paths × n_years × 3) is built:
|
||||
# "shiller" — block-bootstrap of Shiller 1871+ historical returns
|
||||
# (or the synthetic Shiller-calibrated stream when the
|
||||
# CSV isn't mounted). The default; broadest regime
|
||||
# coverage including 1929/1973/2000/2008.
|
||||
# "manual" — every year of every path = `manual_real_return_pct`.
|
||||
# Deterministic, no fan, useful for sanity checks.
|
||||
# "wealthfolio" — block-bootstrap of the user's actual blended real
|
||||
# returns derived from wealthfolio_sync. Reflects the
|
||||
# recent regime only (~6 years). Glide path is moot.
|
||||
returns_mode: str = Field(default="shiller", pattern="^(shiller|manual|wealthfolio)$")
|
||||
manual_real_return_pct: Decimal | None = None
|
||||
|
||||
|
||||
class SimulateResult(BaseModel):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue