FinPlan
- class FinPlan(stages, initial_investment=1000.0, discount_rate=None, mc_number=100, seed=None, name='plan')
Bases:
objectA financial plan: an ordered sequence of portfolio stages.
Each stage holds its own portfolio under its own cash flow strategy for a fixed number of years. The terminal balance of one stage opens the next, per Monte Carlo scenario, which is what makes the plan more than a list of independent forecasts: a retirement is funded by whatever the accumulation produced, scenario by scenario.
- Parameters:
- stagessequence of FinPlanStage
Stages in chronological order. At least one is required and all stage portfolios must share a currency.
- initial_investmentfloat, default 1000.0
Balance at the start of the plan. This is the only source of the opening balance: the initial_investment of the stages’ cash flow strategies is ignored, because CashFlow always assigns a default and “unset” cannot be told apart from “deliberately set”.
- discount_ratefloat or None, default None
Annual effective discount rate for the whole horizon. If None and the first stage’s portfolio has inflation data, its inflation CAGR is used; otherwise settings.DEFAULT_DISCOUNT_RATE.
- mc_numberint, default 100
Number of Monte Carlo scenarios, shared by every stage.
- seedint or None, default None
Plan-level seed. Each stage draws from its own stream spawned from it, so the whole plan is reproducible. Note that a one-stage plan is therefore not bit-identical with Portfolio.dcf.monte_carlo_wealth() at the same seed, only statistically equivalent.
- namestr, default ‘plan’
Label for the wealth column, __repr__ and plots.
Notes
indexation=”inflation” on a stage’s cash flow strategy resolves to the inflation CAGR of that stage’s own portfolio history, not of the plan. Stages built from assets with different history windows therefore index at different rates. To index the whole plan at one rate, pass an explicit float to every stage strategy.
Examples
>>> acc = ok.Portfolio(["SPY.US", "AGG.US"], weights=[0.8, 0.2], ccy="USD") >>> ret = ok.Portfolio(["AGG.US", "SPY.US"], weights=[0.7, 0.3], ccy="USD") >>> pension = ok.IndexationStrategy(ret) >>> pension.frequency = "month" >>> pension.amount = -3_000 >>> pension.indexation = "inflation" >>> plan = ok.FinPlan( ... stages=[ ... ok.FinPlanStage(acc, period=20, name="accumulation"), ... ok.FinPlanStage(ret, period=30, cashflow_parameters=pension, name="retirement"), ... ], ... initial_investment=100_000, ... mc_number=1_000, ... seed=0, ... ) >>> plan.probability_of_success()
Methods & Attributes
balance_percentiles([percentiles, discounting])Distribution of the balance at every stage boundary and at the end.
Portfolio of the first stage; it anchors the plan clock and the discount rate.
cash_flow_ts([discounting, ...])Cash flow of the plan over actual history.
Discard cached Monte Carlo results.
Annual effective discount rate used for the whole horizon.
Earliest and latest dates covered by every stage portfolio at once.
Balance at the start of the plan.
Number of Monte Carlo scenarios.
monte_carlo_cash_flow([discounting, ...])Cash flow of the whole plan for every Monte Carlo scenario.
Money-weighted IRR of the whole plan for every scenario.
monte_carlo_survival_period([threshold])How long each scenario keeps a positive balance, in years from t0.
monte_carlo_wealth([discounting, ...])Wealth index of the whole plan for every Monte Carlo scenario.
Plan horizon in years.
Plan horizon in months.
plot_forecast_monte_carlo([figsize])Plot the plan's scenario cloud with the stage boundaries marked.
probability_of_success([threshold])Share of scenarios that reach the end of the plan above threshold.
Plan-level seed; per-stage streams are spawned from it.
Stages of the plan in chronological order.
Start of the forecast: the last date of the first stage's portfolio.
wealth_index([discounting, ...])Backtest the plan over actual history.
- property stages
Stages of the plan in chronological order.
- property base_portfolio
Portfolio of the first stage; it anchors the plan clock and the discount rate.
- property t0
Start of the forecast: the last date of the first stage’s portfolio.
- property period
Plan horizon in years.
- property period_months
Plan horizon in months.
- property initial_investment
Balance at the start of the plan.
- property discount_rate
Annual effective discount rate used for the whole horizon.
- property mc_number
Number of Monte Carlo scenarios.
- property seed
Plan-level seed; per-stage streams are spawned from it.
- clear_cache()
Discard cached Monte Carlo results.
Plan-level setters call this on their own. Call it by hand after editing a stage’s cash flow strategy in place (pension.amount = -4000), which the plan cannot intercept.
- monte_carlo_wealth(discounting='fv', include_negative_values=True)
Wealth index of the whole plan for every Monte Carlo scenario.
Stages are simulated in order and concatenated, so the balance a scenario reaches at the end of one stage is the balance it starts the next one with (floored at zero).
- Parameters:
- discounting{‘fv’, ‘pv’}, default ‘fv’
‘fv’ returns nominal values; ‘pv’ discounts them to the plan start with discount_rate. The discounting is applied once over the whole horizon, so present values of different stages are comparable.
- include_negative_valuesbool, default True
If False, the first non-positive value of a scenario and everything after it become 0.
- Returns:
- DataFrame
(period_months + 1, mc_number). The first row is the plan’s opening balance, dated one month before t0.
- monte_carlo_cash_flow(discounting='fv', remove_if_wealth_index_negative=True)
Cash flow of the whole plan for every Monte Carlo scenario.
- Parameters:
- discounting{‘fv’, ‘pv’}, default ‘fv’
As in monte_carlo_wealth.
- remove_if_wealth_index_negativebool, default True
If True, cash flow is zeroed for months in which the (floored) wealth index is zero.
- Returns:
- DataFrame
(period_months, mc_number), starting at t0.
- monte_carlo_survival_period(threshold=0)
How long each scenario keeps a positive balance, in years from t0.
- Parameters:
- thresholdfloat, default 0
Share of the opening balance below which the plan counts as voided. Useful with PercentageStrategy, whose balance approaches zero asymptotically.
- Returns:
- Series
One survival period per Monte Carlo scenario.
- monte_carlo_irr()
Money-weighted IRR of the whole plan for every scenario.
The investor’s cash flow runs from t0 (minus the plan’s initial investment) to the end of the horizon, where the terminal balance is added back.
- Returns:
- Series
One annualized effective IRR per scenario; NaN where the cash flow has no sign change.
- probability_of_success(threshold=0)
Share of scenarios that reach the end of the plan above threshold.
For a retirement plan this is the headline number: the chance the money outlives the horizon.
- Returns:
- float
A value between 0 and 1.
- balance_percentiles(percentiles=(10, 50, 90), discounting='fv')
Distribution of the balance at every stage boundary and at the end.
The row for the accumulation stage answers the question a plan is built around: how much is there on the day the next stage has to live off it.
- Parameters:
- percentilestuple of int, default (10, 50, 90)
Percentiles to report.
- discounting{‘fv’, ‘pv’}, default ‘fv’
As in monte_carlo_wealth.
- Returns:
- DataFrame
One row per stage, indexed by stage name; a date column followed by one column per percentile.
- property history_window
Earliest and latest dates covered by every stage portfolio at once.
- wealth_index(discounting='fv', include_negative_values=False, first_date=None)
Backtest the plan over actual history.
The stages divide the common history window sequentially: stage one runs on its portfolio’s real returns for its own length, stage two continues from the balance stage one reached, and so on. This is a glide-path backtest, not a forecast — for the forecast use monte_carlo_wealth.
- Parameters:
- discounting{‘fv’, ‘pv’}, default ‘fv’
‘fv’ returns nominal values; ‘pv’ discounts to the window start.
- include_negative_valuesbool, default False
If False, the balance is zeroed from the first non-positive value on.
- first_datestr or Timestamp or None, default None
Start of the window. By default the plan starts at the earliest date covered by every stage portfolio, which uses the longest available run.
- Returns:
- DataFrame
One column named after the plan, plus accumulated inflation when the first stage’s portfolio carries inflation data.
- Raises:
- ValueError
If the common history is shorter than the plan’s horizon.
- cash_flow_ts(discounting='fv', remove_if_wealth_index_negative=True, first_date=None)
Cash flow of the plan over actual history.
- Parameters:
- discounting{‘fv’, ‘pv’}, default ‘fv’
As in wealth_index.
- remove_if_wealth_index_negativebool, default True
If True, cash flow is zeroed for months in which the (floored) wealth index is zero. This matches monte_carlo_cash_flow behavior.
- first_datestr or Timestamp or None, default None
As in wealth_index.
- Returns:
- Series
Monthly cash flow over the plan’s historical window.
- plot_forecast_monte_carlo(figsize=None)
Plot the plan’s scenario cloud with the stage boundaries marked.
Without the boundary markers a multi-stage chart gives no hint of where the portfolio and the cash flow regime change, which is the whole point of a plan.
- Parameters:
- figsizetuple of (float, float) or None, default None
Figure size in inches; matplotlib defaults are used when None.
- Returns:
- Axes
Matplotlib axes object.
Notes
Unlike PortfolioDCF.plot_forecast_monte_carlo this method has no backtest flag. A plan’s history is a chained backtest of its own, with its own window; plot it with plan.wealth_index().plot().
Examples
>>> import matplotlib.pyplot as plt >>> plan.plot_forecast_monte_carlo() >>> plt.show()