@@ -42,6 +42,7 @@ def __init__(
4242 self .discount_rate = discount_rate
4343 self ._wealth_index_fv = pd .DataFrame (dtype = float )
4444 self ._monte_carlo_wealth_fv = pd .DataFrame (dtype = float )
45+ self ._monte_carlo_cash_flow_fv = pd .DataFrame (dtype = float )
4546 self ._cash_flow_fv = pd .Series (dtype = float , name = "cash_flow_fv" )
4647 self .mc = mc .MonteCarlo (self )
4748 self ._cashflow_parameters : Optional [cf .CashFlow ] = None
@@ -122,7 +123,7 @@ def set_mc_parameters(self, distribution: str, period: int, number: int):
122123 >>> # Assign the strategy to Portfolio
123124 >>> pf.dcf.cashflow_parameters = ind
124125 >>> # Plot wealth index with cash flow
125- >>> pf.dcf.wealth_index_fv .plot()
126+ >>> pf.dcf.wealth_index(discounting="fv", include_negative_values=False) .plot()
126127 >>> plt.show()
127128 """
128129 self .mc .distribution = distribution
@@ -156,7 +157,7 @@ def wealth_index(self, discounting: Literal["fv", "pv"], include_negative_values
156157 >>> ind.amount = -0.5 * 12 # initial withdrawals amount
157158 >>> ind.indexation = "inflation" # the indexation is equal to inflation
158159 >>> pf.dcf.cashflow_parameters = ind # assign the strategy to Portfolio
159- >>> pf.dcf.wealth_index_fv .plot()
160+ >>> pf.dcf.wealth_index(discounting="fv", include_negative_values=False) .plot()
160161 >>> plt.show()
161162 """
162163 if self .cashflow_parameters is None :
@@ -178,9 +179,9 @@ def wealth_index(self, discounting: Literal["fv", "pv"], include_negative_values
178179 wealth_index_fv [self .parent .name ] = wealth_index_fv_s .fillna (0 )
179180 else :
180181 wealth_index_fv = self ._wealth_index_fv .copy ()
181- if discounting == "fv" :
182+ if discounting . lower () == "fv" :
182183 return wealth_index_fv
183- elif discounting == "pv" :
184+ elif discounting . lower () == "pv" :
184185 n_rows = wealth_index_fv .shape [0 ]
185186 discount_factors = (1.0 + self .discount_rate / settings ._MONTHS_PER_YEAR ) ** np .arange (n_rows )
186187 wealth_index_pv = wealth_index_fv .div (discount_factors , axis = 0 )
@@ -216,72 +217,35 @@ def cash_flow_ts(self, discounting: Literal["fv", "pv"], remove_if_wealth_index_
216217 >>> ind.amount = -0.5 * 12 # initial withdrawals amount
217218 >>> ind.indexation = "inflation" # the indexation is equal to inflation
218219 >>> pf.dcf.cashflow_parameters = ind # assign the strategy to Portfolio
219- >>> pf.dcf.wealth_index_fv .plot()
220+ >>> pf.dcf.wealth_index(discounting="fv", include_negative_values=False) .plot()
220221 >>> plt.show()
221222 """
222223 if self .cashflow_parameters is None :
223224 raise AttributeError ("'cashflow_parameters' is not defined." )
224225 if self ._cash_flow_fv .empty :
225226 df = self .parent .ror
226- df = dcf_calculations .get_cash_flow_fv (
227+ self . _cash_flow_fv = dcf_calculations .get_cash_flow_fv (
227228 ror = df ,
228229 portfolio_symbol = self .parent .symbol ,
229230 cashflow_parameters = self .cashflow_parameters ,
230231 task = "backtest" ,
231232 )
232- self ._cash_flow_fv = df
233233 if remove_if_wealth_index_negative :
234234 cash_flow_fv = self ._cash_flow_fv .copy ()
235235 wealth_index = self .wealth_index (discounting = "fv" , include_negative_values = False )
236236 condition = wealth_index [self .parent .name ] == 0
237237 cash_flow_fv [condition ] = 0
238238 else :
239239 cash_flow_fv = self ._cash_flow_fv .copy ()
240- if discounting == "fv" :
240+ if discounting . lower () == "fv" :
241241 return cash_flow_fv
242- elif discounting == "pv" :
242+ elif discounting . lower () == "pv" :
243243 n_rows = cash_flow_fv .shape [0 ]
244244 discount_factors = (1.0 + self .discount_rate / settings ._MONTHS_PER_YEAR ) ** np .arange (n_rows )
245- cash_flow_fv = cash_flow_fv .div (discount_factors , axis = 0 )
246- return cash_flow_fv
247-
248- @property
249- def cash_flow_ts_pv (self ) -> pd .Series :
250- """
251- Wealth index Future Values (FV) time series for the portfolio with cash flow (contributions and
252- withdrawals).
253-
254- Wealth index (Cumulative Wealth Index) is a time series that presents the value of portfolio over
255- historical time period considering cash flows.
256-
257- Accumulated inflation time series is added if `inflation=True` in the Portfolio.
258-
259- If there is no cash flow, Wealth index is obtained from the accumulated return multiplicated
260- by the initial investments. That is: initial_amount_pv * (Acc_Return + 1)
261-
262- Returns
263- -------
264- Time series of wealth index values for portfolio and accumulated inflation.
265-
266- Examples
267- --------
268- >>> import matplotlib.pyplot as plt
269- >>> pf = ok.Portfolio(['VOO.US', 'GLD.US'], weights=[0.8, 0.2])
270- >>> ind = ok.IndexationStrategy(pf) # Set Cash Flow Strategy parameters
271- >>> ind.initial_investment = 100 # initial investments value
272- >>> ind.frequency = "year" # withdrawals frequency
273- >>> ind.amount = -0.5 * 12 # initial withdrawals amount
274- >>> ind.indexation = "inflation" # the indexation is equal to inflation
275- >>> pf.dcf.cashflow_parameters = ind # assign the strategy to Portfolio
276- >>> pf.dcf.wealth_index_fv.plot()
277- >>> plt.show()
278- """
279- cf_fv = self .cash_flow_ts_fv .copy ()
280- # Vectorized discounting
281- n_rows = cf_fv .shape [0 ]
282- discount_factors = (1.0 + self .discount_rate / settings ._MONTHS_PER_YEAR ) ** np .arange (n_rows )
283- cf_pv = cf_fv .div (discount_factors , axis = 0 )
284- return cf_pv
245+ cash_flow_pv = cash_flow_fv .div (discount_factors , axis = 0 )
246+ return cash_flow_pv
247+ else :
248+ raise ValueError ("'discounting' must be either 'fv' or 'pv'" )
285249
286250 @property
287251 def wealth_index_fv_with_assets (self ) -> pd .DataFrame :
@@ -406,7 +370,7 @@ def survival_date_hist(self, threshold: float = 0) -> pd.Timestamp:
406370 >>> pf.dcf.survival_date_hist(threshold=0)
407371 Timestamp('2015-01-31 00:00:00')
408372 """
409- ws = self .wealth_index_fv .loc [:, self .parent .symbol ]
373+ ws = self .wealth_index ( discounting = "fv" , include_negative_values = False ) .loc [:, self .parent .symbol ]
410374 # TODO: change threshold to nominal value (idea)
411375 return helpers .Frame .get_survival_date (ws , self .discount_rate , threshold )
412376
@@ -473,8 +437,11 @@ def initial_investment_fv(self) -> Optional[float]:
473437 return None
474438
475439
476- @property
477- def monte_carlo_wealth_fv (self ) -> pd .DataFrame :
440+ def monte_carlo_wealth (
441+ self ,
442+ discounting : Literal ["fv" , "pv" ],
443+ include_negative_values : bool = True
444+ ) -> pd .DataFrame :
478445 """
479446 Portfolio not discounted random wealth indexes with cash flows (withdrawals/contributions) by Monte Carlo simulation.
480447
@@ -511,7 +478,7 @@ def monte_carlo_wealth_fv(self) -> pd.DataFrame:
511478 return_ts = self .parent .monte_carlo_returns_ts (
512479 distr = self .mc .distribution , years = self .mc .period , n = self .mc .number
513480 )
514- df = return_ts .apply (
481+ self . _monte_carlo_wealth_fv = return_ts .apply (
515482 dcf_calculations .get_wealth_indexes_fv_with_cashflow ,
516483 axis = 0 ,
517484 args = (
@@ -521,49 +488,90 @@ def monte_carlo_wealth_fv(self) -> pd.DataFrame:
521488 "monte_carlo" , # calculate wealth index for Monte Carlo
522489 ),
523490 )
524- df = df .apply (dcf_calculations .remove_negative_values , axis = 0 )
525- all_cells_are_nan = df .isna ().all (axis = 1 )
526- self ._monte_carlo_wealth_fv = df [~ all_cells_are_nan ]
527- return self ._monte_carlo_wealth_fv
491+ if not include_negative_values :
492+ wealth_index_fv = self ._monte_carlo_wealth_fv .copy ()
493+ wealth_index_fv = wealth_index_fv .apply (dcf_calculations .remove_negative_values , axis = 0 )
494+ # all_cells_are_nan = wealth_index_fv.isna().all(axis=1)
495+ # monte_carlo_wealth_fv = wealth_index_fv[~all_cells_are_nan]
496+ monte_carlo_wealth_fv = wealth_index_fv .fillna (0 )
497+ else :
498+ monte_carlo_wealth_fv = self ._monte_carlo_wealth_fv .copy ()
499+ if discounting .lower () == "fv" :
500+ return monte_carlo_wealth_fv
501+ elif discounting .lower () == "pv" :
502+ n_rows = monte_carlo_wealth_fv .shape [0 ]
503+ discount_factors = (1.0 + self .discount_rate / settings ._MONTHS_PER_YEAR ) ** np .arange (n_rows )
504+ monte_carlo_wealth_pv = monte_carlo_wealth_fv .div (discount_factors , axis = 0 )
505+ return monte_carlo_wealth_pv
506+ else :
507+ raise ValueError ("'discounting' must be either 'fv' or 'pv'" )
528508
529- @property
530- def monte_carlo_wealth_pv (self ) -> pd .DataFrame :
509+
510+ def monte_carlo_cash_flow (
511+ self ,
512+ discounting : Literal ["fv" , "pv" ],
513+ remove_if_wealth_index_negative : bool = True
514+ ) -> pd .DataFrame :
531515 """
532- Portfolio discounted random wealth indexes with cash flows (withdrawals/contributions) by Monte Carlo simulation.
516+ Portfolio not discounted random wealth indexes with cash flows (withdrawals/contributions) by Monte Carlo simulation.
533517
534- Random Monte Carlo simulation monthly time series are discounted using `discount_rate` parameter .
518+ Monte Carlo simulation generates n random monthly time series (not discounted) .
535519 Each wealth index is calculated with rate of return time series of a given distribution.
536520
537- `discount_rate` parameter can be set in Portfolio.dcf.discount_rate .
538-
539- Monte Carlo parameters are defined by Portfolio.dcf.set_mc_parameters() method .
521+ First date of forecasted returns is portfolio last_date .
522+ First value for the forecasted wealth indexes is the last historical portfolio index value. It is useful
523+ for a chart with historical wealth index and forecasted values .
540524
541525 Returns
542526 -------
543527 DataFrame
544- Table with random discounted wealth indexes monthly time series.
528+ Table with n random wealth indexes monthly time series.
545529
546530 Examples
547531 --------
548532 >>> import matplotlib.pyplot as plt
549533 >>> pf = ok.Portfolio(['SPY.US', 'AGG.US', 'GLD.US'], weights=[.60, .35, .05], rebalancing_strategy='month')
550- >>> pc = ok.PercentageStrategy(pf) # Define withdrawals strategy with fixed percentage
551- >>> pc.frequency = "year" # set withdrawals frequency
552- >>> pc.percentage = -0.08 # investor would take 8% every year
553- >>> pf.dcf.cashflow_parameters = pc # Assign the strategy to Portfolio
554- >>> pf.dcf.discount_rate = 0.05 # set dicount rate value to 5%
555534 >>> pf.dcf.set_mc_parameters(distribution="t", period=10, number=100) # Set Monte Carlo parameters
556- >>> df = pf.dcf.monte_carlo_wealth_pv # calculate discounted random wealth indexes
557- >>> df.plot() # create a chart
558- >>> plt.legend("") # no legend is required
535+ >>> # set cash flow parameters
536+ >>> ind = ok.IndexationStrategy(pf) # create cash flow strategy linked to the portfolio
537+ >>> ind.initial_investment = 10_000 # add initial investment to cash flow strategy
538+ >>> ind.amount = -500 # set withdrawal size
539+ >>> ind.frequency = "year" # set withdrawal frequency
540+ >>> pf.dcf.cashflow_parameters = ind # assign cash flow strategy to portfolio
541+ >>> pf.dcf.monte_carlo_wealth_fv.plot()
542+ >>> plt.legend("") # don't show legend for each line
559543 >>> plt.show()
560544 """
561- wealth_df = self .monte_carlo_wealth_fv .copy ()
562- # Vectorized discounting
563- n_rows = wealth_df .shape [0 ]
564- discount_factors = (1.0 + self .discount_rate / settings ._MONTHS_PER_YEAR ) ** np .arange (n_rows )
565- wealth_df_pv = wealth_df .div (discount_factors , axis = 0 )
566- return wealth_df_pv
545+ if self .cashflow_parameters is None :
546+ raise AttributeError ("'cashflow_parameters' is not defined." )
547+ if self ._monte_carlo_cash_flow_fv .empty :
548+ return_ts = self .parent .monte_carlo_returns_ts (
549+ distr = self .mc .distribution , years = self .mc .period , n = self .mc .number
550+ )
551+ self ._monte_carlo_cash_flow_fv = return_ts .apply (
552+ dcf_calculations .get_cash_flow_fv ,
553+ axis = 0 ,
554+ args = (
555+ self .parent .symbol , # portfolio_symbol
556+ self .cashflow_parameters ,
557+ "monte_carlo" , # task
558+ ),
559+ )
560+ if remove_if_wealth_index_negative :
561+ mc_cash_flow_fv = self ._monte_carlo_cash_flow_fv .copy ()
562+ mc_wealth_index = self .monte_carlo_wealth (discounting = "fv" , include_negative_values = False )
563+ condition = mc_wealth_index == 0
564+ mc_cash_flow_fv [condition ] = 0
565+ else :
566+ mc_cash_flow_fv = self ._monte_carlo_cash_flow_fv .copy ()
567+ if discounting .lower () == "fv" :
568+ return mc_cash_flow_fv
569+ elif discounting .lower () == "pv" :
570+ n_rows = mc_cash_flow_fv .shape [0 ]
571+ discount_factors = (1.0 + self .discount_rate / settings ._MONTHS_PER_YEAR ) ** np .arange (n_rows )
572+ return mc_cash_flow_fv .div (discount_factors , axis = 0 )
573+ else :
574+ raise ValueError ("'discounting' must be either 'fv' or 'pv'" )
567575
568576 def plot_forecast_monte_carlo (
569577 self ,
@@ -612,7 +620,7 @@ def plot_forecast_monte_carlo(
612620 if self .cashflow_parameters is None :
613621 raise AttributeError ("'cashflow_parameters' is not defined." )
614622 backup_obj = self .cashflow_parameters
615- s1 = self .wealth_index_fv [self .parent .symbol ]
623+ s1 = self .wealth_index ( discounting = "fv" , include_negative_values = False ) [self .parent .symbol ]
616624 s1 .plot (legend = None , figsize = figsize )
617625 last_backtest_value = s1 .iloc [- 1 ]
618626 if last_backtest_value > 0 :
0 commit comments