@@ -1612,7 +1612,7 @@ def monte_carlo_wealth(self, distr: str = "norm", years: int = 1, n: int = 100)
16121612 Examples
16131613 --------
16141614 >>> pf = ok.Portfolio(['SPY.US', 'AGG.US', 'GLD.US'], weights=[.60, .35, .05], rebalancing_period='month')
1615- >>> pf.monte_carlo_wealth (distr='lognorm', years=5, n=1000)
1615+ >>> pf.monte_carlo_wealth_fv (distr='lognorm', years=5, n=1000)
16161616 0 1 ... 998 999
16171617 2021-07 3895.377293 3895.377293 ... 3895.377293 3895.377293
16181618 2021-08 3869.854680 4004.814981 ... 3874.455244 3935.913516
@@ -2966,21 +2966,23 @@ def monte_carlo_survival_period(self, threshold: float = 0) -> pd.Series:
29662966
29672967 def find_the_largest_withdrawals_size (
29682968 self ,
2969- withdrawal_steps : int ,
2970- confidence_level : float ,
29712969 goal : str ,
2970+ target_survival_period : int = 25 ,
2971+ percentile : float = 0.20 ,
29722972 threshold : float = 0 ,
2973- target_survival_period : int = 25
2974- ) -> float :
2973+ tolerance_rel : float = 0.01 ,
2974+ iter_max : int = 100
2975+ ) -> Tuple [float , float ]:
29752976 """
29762977 Find the largest withdrawals size for Monte Carlo simulation according to Cashflow Strategy.
29772978
29782979 It's possible to find the largest withdrawl with 2 kind of goals:
29792980
2980- — 'maintain_balance ' to keep the purchasing power of the invesments after inflation
2981+ — 'maintain_balance_pv ' to keep the purchasing power of the invesments after inflation
29812982 for the whole period defined in Monte Carlo parameteres.
29822983 — 'survival_period' to keep positive balance for a period defined by 'target_survival_period'.
2983- The method works with IndexationStrategy and PercentageStrategy only.
2984+ The method works with IndexationStrategy
2985+ and PercentageStrategy only.
29842986
29852987 The withdrawal size defined in cash flow strategy must be negative.
29862988
@@ -2991,18 +2993,15 @@ def find_the_largest_withdrawals_size(
29912993
29922994 Parameters
29932995 ----------
2994- withdrawal_steps : int
2995- The number of intermediate steps during the iteration of values fom maximum
2996- to minimum of the withdrawal size.
2997- The withdrawal size varies from 100% of the initial investment to zero.
2998-
2999- confidence_level : float
2996+ percentile : float
30002997 Confidence level must be form 0 to 1.
30012998 Confidence level defines the percentile of Monte Carlo time series. 0.01 or 0.05 are the examples of "bad"
30022999 scenarios. 0.50 is mediane (50% percentile). 0.95 or 0.99 are optimiststic scenarios.
30033000
3004- goal : {'maintain_balance', 'survival_period'}
3005- 'maintain_balance' - the goal is to keep the purchasing power of the invesments after inflation
3001+ goal : {'maintain_balance_fv', 'maintain_balance_pv', 'survival_period'}
3002+ 'maintain_balance_fv' - the goal is to maintain the balance not lower than the nominal amount of the initial investment after inflation
3003+ for the whole period defined in Monte Carlo parameteres.
3004+ 'maintain_balance_pv' - the goal is to keep the purchasing power of the invesments after inflation
30063005 for the whole period defined in Monte Carlo parameteres.
30073006 'survival_period' - the goal is to keep positive balance
30083007 for a period defined by 'target_survival_period'.
@@ -3014,6 +3013,10 @@ def find_the_largest_withdrawals_size(
30143013 target_survival_period: int, default 25
30153014 The smallest acceptable survival period. It wokrs with the 'survival_period' goal only.
30163015
3016+ iter_max : integer, default 100
3017+
3018+ tolerance_rel : float, default 0.10
3019+
30173020 Examples
30183021 --------
30193022 >>> pf = ok.Portfolio(
@@ -3037,50 +3040,89 @@ def find_the_largest_withdrawals_size(
30373040 ...)
30383041 >>> pf.dcf.find_the_largest_withdrawals_size(
30393042 ... withdrawal_steps=30,
3040- ... confidence_level =0.50,
3043+ ... percentile =0.50,
30413044 ... goal="survival_period",
30423045 ... threshold=0.05,
30433046 ... target_survival_period=25
30443047 ...)
30453048 np.float64(-0.10344827586206895)
30463049 """
30473050 # TODO: introduce withdrawals_range (500, 0)
3048- max_withdrawal = 0
30493051 if target_survival_period > self .mc .period :
30503052 raise ValueError (f"target_survival_period must be less or equal than Monte Carlo simulation period ({ self .mc .period } )." )
3051- if confidence_level > 1 or confidence_level < 0 :
3052- raise ValueError ("confidence level must be between 0 and 1 " )
3053+ if percentile > 100 or percentile < 0 :
3054+ raise ValueError ("percentile must be between 0 and 100 " )
30533055 if threshold > 1 or threshold < 0 :
30543056 raise ValueError ("threshold must be between 0 and 1" )
3057+ backup_obj = self .cashflow_parameters
3058+ start_investment = self .cashflow_parameters .initial_investment
3059+ expected_min_withdrawal = 0
30553060 if self .cashflow_parameters .NAME == "fixed_amount" :
3056- size_range = np .linspace (- self .cashflow_parameters .initial_investment , 0 , withdrawal_steps )
3061+
3062+ if goal == "maintain_balance_pv" :
3063+ self .cashflow_parameters .amount = - 0.03 * start_investment / self .cashflow_parameters .periods_per_year
3064+ expected_max_withdrawal = - 0.10 * start_investment / self .cashflow_parameters .periods_per_year
3065+ elif goal == "maintain_balance_fv" :
3066+ self .cashflow_parameters .amount = - 0.04 * start_investment / self .cashflow_parameters .periods_per_year
3067+ expected_max_withdrawal = - 0.15 * start_investment / self .cashflow_parameters .periods_per_year
3068+ elif goal == "survival_period" :
3069+ self .cashflow_parameters .amount = - 0.10 * start_investment / self .cashflow_parameters .periods_per_year
3070+ expected_max_withdrawal = - 0.20 * start_investment / self .cashflow_parameters .periods_per_year
30573071 elif self .cashflow_parameters .NAME == "fixed_percentage" :
3058- size_range = np .linspace (- 1 , 0 , withdrawal_steps )
3059- else :
3060- raise TypeError ("find_the_largest_withdrawals_size works with 'fixed_amount' or 'fixed_percentag' only." )
3061- backup_obj = self .cashflow_parameters
3062- for size in size_range :
3063- if self .cashflow_parameters .NAME == "fixed_amount" :
3064- # TODO: amount should depend on "frequency" (devide by 12 for month)
3065- self .cashflow_parameters .amount = size
3066- elif self .cashflow_parameters .NAME == "fixed_percentage" :
3067- self .cashflow_parameters .percentage = size
3068-
3069- sp_at_quantile = self .monte_carlo_survival_period (threshold = threshold ).quantile (confidence_level )
3070- if goal == "maintain_balance" :
3071- wealth_at_quantile = self .monte_carlo_wealth .iloc [- 1 , :].quantile (confidence_level )
3072- condition = (sp_at_quantile == self .mc .period ) and (wealth_at_quantile >= self .initial_investment_fv )
3073- if condition :
3074- max_withdrawal = size
3075- break
3072+ # TODO: amount should depend on "frequency" (devide by 12 for month)
3073+ self .cashflow_parameters .percentage = - 0.03 * start_investment / self .cashflow_parameters .periods_per_year
3074+ # NEW Code ()
3075+ iter = 0
3076+ solutions = pd .DataFrame (columns = ["withdrawal" , "error_rel" , "error_rel_change" ])
3077+ while True :
3078+ wealth_at_quantile = self .monte_carlo_wealth_pv .iloc [- 1 , :].quantile (percentile / 100 )
3079+ sp_at_quantile = self .monte_carlo_survival_period (threshold = 0 ).quantile (percentile / 100 )
3080+
3081+ if goal in ["maintain_balance_fv" , "maintain_balance_pv" ]:
3082+ condition = (wealth_at_quantile >= start_investment ) and (sp_at_quantile == self .mc .period )
3083+ print (wealth_at_quantile , self .cashflow_parameters .amount )
3084+ error_rel = abs (wealth_at_quantile - start_investment ) / start_investment
30763085 elif goal == "survival_period" :
30773086 condition = sp_at_quantile >= target_survival_period
3078- if condition :
3079- max_withdrawal = size
3087+ print (f'{ sp_at_quantile = :.2f} , { self .cashflow_parameters .amount = :.2f} ' )
3088+ error_rel = abs (sp_at_quantile - target_survival_period ) / target_survival_period
3089+
3090+ solutions .at [iter , "withdrawal" ] = self .cashflow_parameters .amount
3091+ solutions .at [iter , "error_rel" ] = error_rel
3092+ gradient = solutions .at [iter , "error_rel" ] - solutions .at [iter - 1 , "error_rel" ] if iter != 0 else 0
3093+ solutions .at [iter , "error_rel_change" ] = gradient
3094+
3095+ print (f'{ error_rel = :.3f} , { gradient = :.3f} ' )
3096+
3097+ if condition :
3098+ if error_rel < tolerance_rel :
3099+ max_withdrawal = self .cashflow_parameters .amount
3100+ print (f'solution found: { max_withdrawal } after { iter } steps.' )
3101+ break
3102+ expected_min_withdrawal = self .cashflow_parameters .amount
3103+ delta = abs (expected_max_withdrawal - self .cashflow_parameters .amount ) # ind.amount must be negative
3104+ self .cashflow_parameters .amount -= delta / 2
3105+ print ("increasing withdrawal" )
3106+ else :
3107+ if error_rel < tolerance_rel :
3108+ max_withdrawal = self .cashflow_parameters .amount
3109+ print (f'solution found: { max_withdrawal } after { iter } steps.' )
30803110 break
3111+ expected_max_withdrawal = self .cashflow_parameters .amount
3112+ delta = abs (self .cashflow_parameters .amount - expected_min_withdrawal )
3113+ self .cashflow_parameters .amount += delta / 2
3114+ print ("decreasing withdrawal" )
3115+ iter += 1
3116+ if iter > iter_max :
3117+ condition = solutions ["error_rel" ].idxmin ()
3118+ best_result = solutions .loc [condition ]["withdrawal" ]
3119+ max_withdrawal = solutions .loc [condition ]["error_rel" ]
3120+ print (
3121+ f"'Didn't found solution after { iter } steps. The closest withdrawal was { best_result } or { self .cashflow_parameters .amount / start_investment * 12 } with an error: { max_withdrawal } " )
3122+ break
30813123 self .cashflow_parameters = backup_obj
30823124 self .cashflow_parameters ._clear_cf_cache ()
3083- return max_withdrawal
3125+ return max_withdrawal , error_rel
30843126
30853127
30863128class MonteCarlo :
@@ -3236,7 +3278,14 @@ def frequency(self, frequency):
32363278 raise ValueError (f"frequency must be in { settings .frequency_mapping .keys ()} " )
32373279
32383280 @property
3239- def initial_investment (self ):
3281+ def periods_per_year (self ) -> int :
3282+ """
3283+ Show the number of periods per year. Period is defined by the frequency.
3284+ """
3285+ return settings .frequency_periods_per_year [self .frequency ]
3286+
3287+ @property
3288+ def initial_investment (self ) -> float :
32403289 """
32413290 Portfolio initial investment FV size (at last_date).
32423291
0 commit comments