@@ -109,6 +109,7 @@ def period(self, value: str):
109109 self ._period = value
110110 self ._validate_condition ()
111111 self ._pandas_frequency = settings .frequency_mapping [self .period ]
112+ self ._pandas_frequency_grouper = settings .grouper_frequency_mapping [self .period ]
112113
113114 def wealth_ts (
114115 self , target_weights : list , ror : pd .DataFrame , calculate_assets_wealth_indexes : bool = False
@@ -353,17 +354,28 @@ def wealth_ts_ef(self, weights: list, ror: pd.DataFrame) -> pd.Series:
353354 assets_wealth_indexes = inv_period_spread * (1 + ror ).cumprod ()
354355 wealth_index = assets_wealth_indexes .sum (axis = 1 )
355356 else :
356- # Collect chunks in a list for single concatenation
357- wealth_chunks = []
358- for x in ror .resample (rule = self ._pandas_frequency , convention = "start" ):
359- df = x [1 ] # select ror part of the grouped data
360- inv_period_spread = weights_array * initial_inv # rebalancing
361- assets_wealth_indexes = inv_period_spread * (1 + df ).cumprod ()
362- wealth_index_local = assets_wealth_indexes .sum (axis = 1 )
363- wealth_chunks .append (wealth_index_local )
364- initial_inv = wealth_index_local .iloc [- 1 ]
365- # Single concatenation outside the loop
366- wealth_index = pd .concat (wealth_chunks , sort = False )
357+ # Fully vectorized approach without loops
358+ period_grouper = pd .Grouper (freq = self ._pandas_frequency_grouper , convention = "start" )
359+ period_ids = ror .groupby (period_grouper ).ngroup ()
360+
361+ growth_factors = 1 + ror
362+ cumulative_growth_within_period = growth_factors .groupby (period_ids ).cumprod ()
363+
364+ wealth_per_asset_normalized = weights_array * cumulative_growth_within_period
365+ portfolio_wealth_normalized = wealth_per_asset_normalized .sum (axis = 1 )
366+
367+ # Calculate ending wealth for each period (growth factor for that period)
368+ period_ends = portfolio_wealth_normalized .groupby (period_ids ).last ()
369+
370+ # Calculate cumulative multiplier for the START of each period
371+ period_multipliers = period_ends .shift (1 ).fillna (1.0 ).cumprod ()
372+
373+ # Map multipliers back to the original time series
374+ aligned_multipliers = period_ids .map (period_multipliers )
375+
376+ # Calculate final wealth index
377+ wealth_index = initial_inv * aligned_multipliers * portfolio_wealth_normalized
378+
367379 return wealth_index
368380
369381 def return_ror_ts_ef (self , weights : Union [list , np .ndarray ], ror : pd .DataFrame ) -> pd .Series :
0 commit comments