Skip to content

Commit 233bab0

Browse files
committed
add handling of one asset universe for risk-budgeting
1 parent 647aacc commit 233bab0

2 files changed

Lines changed: 15 additions & 5 deletions

File tree

optimalportfolios/examples/solve_risk_budgets_balanced_portfolio.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,14 +130,12 @@ def run_local_test(local_test: LocalTests):
130130
if local_test == LocalTests.SOLVE_FOR_RISK_BUDGETS:
131131
risk_budgets = solve_for_risk_budgets_from_given_weights(prices=prices,
132132
given_weights=given_static_weights,
133-
time_period=time_period,
134133
covar_dict=covar_dict)
135134
print(risk_budgets)
136135

137136
elif local_test == LocalTests.ILLUSTRATE_WEIGHTS:
138137
risk_budgets = solve_for_risk_budgets_from_given_weights(prices=prices,
139138
given_weights=given_static_weights,
140-
time_period=time_period,
141139
covar_dict=covar_dict)
142140
risk_budgets_weights = rolling_risk_budgeting(prices=prices,
143141
covar_dict=covar_dict,

optimalportfolios/optimization/general/risk_budgeting.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ def rolling_risk_budgeting(prices: pd.DataFrame,
7474
Returns:
7575
DataFrame of portfolio weights.
7676
"""
77+
# Single-asset universe: trivial 100% allocation at every rebalancing date.
78+
if len(risk_budget) == 1:
79+
asset = risk_budget.index[0]
80+
weights = pd.DataFrame(1.0,
81+
index=pd.DatetimeIndex(list(covar_dict.keys())),
82+
columns=[asset])
83+
return weights.reindex(columns=prices.columns.to_list()).fillna(0.0)
84+
7785
if rebalancing_indicators is not None:
7886
rebalancing_dates = list(covar_dict.keys())
7987
rebalancing_indicators = rebalancing_indicators.reindex(index=rebalancing_dates).fillna(0.0)
@@ -313,7 +321,6 @@ def risk_budget_objective(x, pars) -> float:
313321

314322
def solve_for_risk_budgets_from_given_weights(prices: pd.DataFrame,
315323
given_weights: pd.Series,
316-
time_period: qis.TimePeriod,
317324
covar_dict: Dict[pd.Timestamp, pd.DataFrame],
318325
min_risk_budget: float = 1e-4,
319326
max_risk_budget: float = 0.99
@@ -324,14 +331,19 @@ def solve_for_risk_budgets_from_given_weights(prices: pd.DataFrame,
324331
Args:
325332
prices: Asset price panel.
326333
given_weights: Target portfolio weights to reproduce.
327-
time_period: Period for rolling backtest within the objective function.
328334
covar_dict: Pre-computed covariance matrices.
329335
min_risk_budget: Lower bound on each non-zero risk budget.
330336
max_risk_budget: Upper bound on each risk budget.
331337
332338
Returns:
333339
Optimal risk budgets as pd.Series. Budgets sum to 1.
334340
"""
341+
# Single-asset universe: the only budget consistent with sum=1 is 1.0
342+
# on the lone asset. Skip the solver — it would be infeasible under the
343+
# max_risk_budget=0.99 cap anyway.
344+
if prices.shape[1] == 1:
345+
return pd.Series(1.0, index=prices.columns)
346+
335347
given_weights_np = given_weights.to_numpy()
336348

337349
def objective_function(risk_budgets: np.ndarray) -> float:
@@ -371,4 +383,4 @@ def objective_function(risk_budgets: np.ndarray) -> float:
371383
warnings.warn(f"solve_for_risk_budgets_from_given_weights: solver failed, using zero budgets")
372384
risk_budgets = np.zeros_like(x0)
373385
risk_budgets = pd.Series(risk_budgets, index=prices.columns)
374-
return risk_budgets
386+
return risk_budgets

0 commit comments

Comments
 (0)