@@ -119,6 +119,7 @@ def __init__(
119119 self .full_frontier = full_frontier
120120 self ._ef_points = pd .DataFrame (dtype = float )
121121 self ._mdp_points = pd .DataFrame (dtype = float )
122+ self ._ror_cache : Dict [tuple , pd .Series ] = {} # Cache for portfolio return time series by weights
122123
123124 def __repr__ (self ):
124125 dic = {
@@ -213,6 +214,34 @@ def n_points(self, n_points: int):
213214 def _clear_cache (self ):
214215 self ._ef_points = pd .DataFrame (dtype = float ) # renew EF points DataFrame
215216 self ._mdp_points = pd .DataFrame (dtype = float ) # renew MDP points DataFrame
217+ self ._ror_cache = {} # clear portfolio return time series cache
218+
219+ def _get_portfolio_ror_ts (self , weights : np .ndarray ) -> pd .Series :
220+ """
221+ Get portfolio return time series with caching based on weights.
222+
223+ This method caches the results of portfolio return calculations to avoid
224+ redundant computations during optimization when the same weights are evaluated
225+ multiple times (e.g., in objective function and constraints).
226+
227+ Parameters
228+ ----------
229+ weights : np.ndarray
230+ Portfolio weights for each asset.
231+
232+ Returns
233+ -------
234+ pd.Series
235+ Monthly rate of return time series for the rebalanced portfolio.
236+ """
237+ # Round weights to avoid floating point precision issues in cache keys
238+ weights_key = tuple (np .round (weights , decimals = 10 ))
239+
240+ if weights_key not in self ._ror_cache :
241+ rebalance = Rebalance (period = self .rebalancing_strategy .period )
242+ self ._ror_cache [weights_key ] = rebalance .return_ror_ts_ef (weights , self .assets_ror )
243+
244+ return self ._ror_cache [weights_key ]
216245
217246 @property
218247 def rebalancing_strategy (self ) -> Rebalance :
@@ -341,10 +370,6 @@ def get_most_diversified_portfolio(
341370 n = self .assets_ror .shape [1 ]
342371 init_guess = np .repeat (1 / n , n )
343372
344- args = dict (
345- period = self .rebalancing_strategy .period ,
346- )
347-
348373 def objective_function (w ):
349374 # Diversification Ratio
350375 assets_risk = ror .std ()
@@ -353,7 +378,7 @@ def objective_function(w):
353378 weights = np .asarray (w )
354379 assets_sigma_weighted_sum = weights .T @ assets_annualized_risk
355380
356- portfolio_ror = Rebalance ( ** args ). return_ror_ts_ef ( w , ror )
381+ portfolio_ror = self . _get_portfolio_ror_ts ( w )
357382 portfolio_mean_return_monthly = portfolio_ror .mean ()
358383 portfolio_risk_monthly = portfolio_ror .std ()
359384
@@ -428,17 +453,12 @@ def get_tangency_portfolio(self, rf_return: float = 0, rate_of_return: str = "ca
428453 >>> ef.get_tangency_portfolio(rate_of_return="mean_return", rf_return=0.03)
429454 {'Weights': array([2.95364739e-01, 1.08420217e-17, 7.04635261e-01]), 'Rate_of_return': 0.10654206521088283, 'Risk': 0.048279725208422115}
430455 """
431- ror = self .assets_ror
432456 n = self .assets_ror .shape [1 ]
433457 init_guess = np .repeat (1 / n , n )
434458
435- args = dict (
436- period = self .rebalancing_strategy .period ,
437- )
438-
439459 def of_arithmetic_mean (w ):
440460 # Sharpe ratio with arithmetic mean
441- portfolio_ror = Rebalance ( ** args ). return_ror_ts_ef ( w , ror )
461+ portfolio_ror = self . _get_portfolio_ror_ts ( w )
442462 mean_return_monthly = portfolio_ror .mean ()
443463 risk_monthly = portfolio_ror .std ()
444464 of_arithmetic_mean .rate_of_return = helpers .Float .annualize_return (mean_return_monthly )
@@ -447,7 +467,7 @@ def of_arithmetic_mean(w):
447467
448468 def of_geometric_mean (w ):
449469 # Sharpe ratio with CAGR
450- portfolio_ror = Rebalance ( ** args ). return_ror_ts_ef ( w , ror )
470+ portfolio_ror = self . _get_portfolio_ror_ts ( w )
451471 mean_return_monthly = portfolio_ror .mean ()
452472 of_geometric_mean .rate_of_return = helpers .Frame .get_cagr (portfolio_ror )
453473 # Risk
@@ -501,16 +521,12 @@ def gmv_monthly_weights(self) -> np.ndarray:
501521 >>> frontier.gmv_monthly_weights
502522 array([0.0578446, 0.9421554])
503523 """
504- ror = self .assets_ror
505- args = dict (
506- period = self .rebalancing_strategy .period ,
507- )
508524 n = self .assets_ror .shape [1 ]
509525 init_guess = np .repeat (1 / n , n )
510526
511527 # Set the objective function
512528 def objective_function (w ):
513- risk = okama . common . helpers . rebalancing . Rebalance ( ** args ). return_ror_ts_ef ( w , ror ).std ()
529+ risk = self . _get_portfolio_ror_ts ( w ).std ()
514530 return risk
515531
516532 # construct the constraints
@@ -546,16 +562,12 @@ def gmv_annual_weights(self) -> np.ndarray:
546562 >>> frontier.gmv_monthly_weights
547563 array([0.05373824, 0.94626176])
548564 """
549- ror = self .assets_ror
550- args = dict (
551- period = self .rebalancing_strategy .period ,
552- )
553565 n = self .assets_ror .shape [1 ]
554566 init_guess = np .repeat (1 / n , n )
555567
556568 # Set the objective function
557569 def objective_function (w ):
558- ts = Rebalance ( ** args ). return_ror_ts_ef ( w , ror )
570+ ts = self . _get_portfolio_ror_ts ( w )
559571 mean_return = ts .mean ()
560572 risk = ts .std ()
561573 return helpers .Float .annualize_risk (risk = risk , mean_return = mean_return )
@@ -578,10 +590,7 @@ def _get_gmv_monthly(self) -> Tuple[float, float]:
578590
579591 Global Minimum Volatility portfolio is a portfolio with the lowest risk of all possible.
580592 """
581- args = dict (
582- period = self .rebalancing_strategy .period ,
583- )
584- ts = Rebalance (** args ).return_ror_ts_ef (self .gmv_monthly_weights , self .assets_ror )
593+ ts = self ._get_portfolio_ror_ts (self .gmv_monthly_weights )
585594 return ts .std (), ts .mean ()
586595
587596 @property
@@ -606,10 +615,7 @@ def gmv_annual_values(self) -> Tuple[float, float]:
606615 >>> frontier.gmv_annual_values
607616 (0.03695845106087943, 0.04418318557516887)
608617 """
609- args = dict (
610- period = self .rebalancing_strategy .period ,
611- )
612- returns = Rebalance (** args ).return_ror_ts_ef (self .gmv_annual_weights , self .assets_ror )
618+ returns = self ._get_portfolio_ror_ts (self .gmv_annual_weights )
613619 return (
614620 helpers .Float .annualize_risk (returns .std (), returns .mean ()),
615621 (returns + 1.0 ).prod () ** (settings ._MONTHS_PER_YEAR / returns .shape [0 ]) - 1.0 ,
@@ -639,17 +645,13 @@ def global_max_return_portfolio(self) -> dict:
639645 >>> frontier.global_max_return_portfolio
640646 {'Weights': array([1., 0.]), 'CAGR': 0.10797159166196812, 'Risk': 0.1583011735798155, 'Risk_monthly': 0.0410282468594492}
641647 """
642- ror = self .assets_ror
643- args = dict (
644- period = self .rebalancing_strategy .period ,
645- )
646648 n = self .assets_ror .shape [1 ] # Number of assets
647649 init_guess = np .repeat (1 / n , n )
648650
649651 # Set the objective function
650652 def objective_function (w ):
651653 # Accumulated return for rebalanced portfolio time series
652- objective_function .returns = Rebalance ( ** args ). return_ror_ts_ef ( w , ror )
654+ objective_function .returns = self . _get_portfolio_ror_ts ( w )
653655 accumulated_return = (objective_function .returns + 1.0 ).prod () - 1.0
654656 return - accumulated_return
655657
@@ -678,11 +680,8 @@ def objective_function(w):
678680 }
679681 return point
680682
681- def _get_cagr (self , weights ):
682- args = dict (
683- period = self .rebalancing_strategy .period ,
684- )
685- ts = Rebalance (** args ).return_ror_ts_ef (weights , self .assets_ror )
683+ def _get_cagr (self , weights : np .ndarray ) -> float :
684+ ts = self ._get_portfolio_ror_ts (weights )
686685 acc_return = (ts + 1.0 ).prod () - 1.0
687686 return (1.0 + acc_return ) ** (settings ._MONTHS_PER_YEAR / ts .shape [0 ]) - 1.0
688687
@@ -730,17 +729,13 @@ def minimize_risk(self, target_value: float) -> Dict[str, float]:
730729
731730 max_ratio_data = self ._max_ratio_asset_right_to_max_cagr
732731
733- args = dict (
734- period = self .rebalancing_strategy .period ,
735- )
736-
737732 if max_ratio_data is not None :
738733 init_guess = np .repeat (0 , n ) # clear weights
739734 init_guess [self ._min_ratio_asset ["list_position" ]] = 1.0
740735
741736 def objective_function (w ):
742737 # annual risk
743- ts = Rebalance ( ** args ). return_ror_ts_ef ( w , self . assets_ror )
738+ ts = self . _get_portfolio_ror_ts ( w )
744739 risk_monthly = ts .std ()
745740 objective_function .mean_return = ts .mean ()
746741 return helpers .Float .annualize_risk (risk_monthly , objective_function .mean_return )
@@ -798,13 +793,9 @@ def _maximize_risk(self, target_return: float) -> Dict[str, float]:
798793 """
799794 n = self .assets_ror .shape [1 ] # number of assets
800795
801- args = dict (
802- period = self .rebalancing_strategy .period ,
803- )
804-
805796 def objective_function (w ):
806797 # annual risk
807- ts = Rebalance ( ** args ). return_ror_ts_ef ( w , self . assets_ror )
798+ ts = self . _get_portfolio_ror_ts ( w )
808799 risk_monthly = ts .std ()
809800 objective_function .mean_return = ts .mean ()
810801 result = - helpers .Float .annualize_risk (risk_monthly , objective_function .mean_return )
@@ -1266,26 +1257,19 @@ def get_monte_carlo(self, n: int = 100) -> pd.DataFrame:
12661257 >>> ax.legend()
12671258 >>> plt.show()
12681259 """
1269- weights_df = helpers .Float .get_random_weights (n , self .assets_ror .shape [1 ], self .bounds )
1270-
1271- args = dict (
1272- period = self .rebalancing_strategy .period ,
1273- )
1274- # Portfolio risk and cagr for each set of weights
1275- portfolios_ror = weights_df .aggregate (
1276- Rebalance (** args ).return_ror_ts_ef ,
1277- ror = self .assets_ror ,
1278- )
1260+ weights_series = helpers .Float .get_random_weights (n , self .assets_ror .shape [1 ], self .bounds )
1261+ # Portfolio risk and cagr for each set of weights using cache
12791262 rows_list = [] # Collect all rows to create DataFrame once at the end
1280- for _ , data in portfolios_ror .iterrows ():
1281- risk_monthly = data .std ()
1282- mean_return = data .mean ()
1263+ for weights in weights_series :
1264+ # Use cached portfolio return time series calculation
1265+ portfolio_ror = self ._get_portfolio_ror_ts (weights )
1266+ risk_monthly = portfolio_ror .std ()
1267+ mean_return = portfolio_ror .mean ()
12831268 risk = helpers .Float .annualize_risk (risk_monthly , mean_return )
1284- cagr = helpers .Frame .get_cagr (data )
1269+ cagr = helpers .Frame .get_cagr (portfolio_ror )
12851270 row = {"Risk" : risk , "CAGR" : cagr }
12861271 rows_list .append (row )
1287- random_portfolios = pd .DataFrame .from_records (rows_list )
1288- return random_portfolios
1272+ return pd .DataFrame .from_records (rows_list )
12891273
12901274 def plot_pair_ef (self , tickers = "tickers" , figsize : Optional [tuple ] = None ) -> Axes :
12911275 """
0 commit comments