Skip to content

Commit 5ff2c34

Browse files
committed
feat: EfficientFrontier uses fast vectorized methods of Rebalance (without abs and rel bands)
1 parent 5c7248f commit 5ff2c34

File tree

2 files changed

+24
-11
lines changed

2 files changed

+24
-11
lines changed

okama/common/helpers/rebalancing.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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:

okama/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@
2525

2626
# From Pandas resamples alias: https://pandas.pydata.org/docs/user_guide/timeseries.html#timeseries-offset-aliases
2727
frequency_mapping = {"none": "none", "year": "Y", "half-year": "2Q", "quarter": "Q", "month": "M"}
28+
grouper_frequency_mapping = {"none": "none", "year": "YE", "half-year": "2QE", "quarter": "QE", "month": "ME"}
2829
frequency_periods_per_year = {"none": 0, "year": 1, "half-year": 2, "quarter": 4, "month": 12}

0 commit comments

Comments
 (0)