Rebalancing¶
The rebalancing module implements calendar-based, threshold-based, and hybrid rebalancing strategies for portfolio management. It determines when to trade (the rebalancing signal) and provides utility functions for computing drift, turnover, and transaction costs.
Overview¶
After optimization produces target weights, the rebalancing module answers the question: "Should we actually trade to reach these weights?" Trading too frequently incurs unnecessary transaction costs, while trading too infrequently allows the portfolio to drift far from optimal allocations. The three strategies offer different trade-offs:
- Calendar — rebalance at fixed intervals regardless of drift
- Threshold — rebalance only when drift exceeds a limit
- Hybrid — check drift only at calendar review dates (best of both worlds)
Calendar Rebalancing¶
Triggers portfolio reconstruction at fixed time intervals regardless of how much the portfolio has drifted.
from optimizer.rebalancing import CalendarRebalancingConfig, RebalancingFrequency
config = CalendarRebalancingConfig(
frequency=RebalancingFrequency.QUARTERLY,
)
| Frequency | Trading Days | Approximate Period |
|---|---|---|
MONTHLY |
21 | 1 month |
QUARTERLY |
63 | 3 months |
SEMIANNUAL |
126 | 6 months |
ANNUAL |
252 | 1 year |
Presets¶
CalendarRebalancingConfig.for_monthly() # 21 trading days
CalendarRebalancingConfig.for_quarterly() # 63 trading days
CalendarRebalancingConfig.for_semiannual() # 126 trading days
CalendarRebalancingConfig.for_annual() # 252 trading days
Threshold Rebalancing¶
Rebalances only when portfolio drift exceeds specified limits. This avoids unnecessary turnover during stable periods while catching significant deviations.
from optimizer.rebalancing import ThresholdRebalancingConfig, ThresholdType
config = ThresholdRebalancingConfig(
threshold_type=ThresholdType.ABSOLUTE,
threshold=0.05, # 5 percentage points
)
| Field | Type | Default | Description |
|---|---|---|---|
threshold_type |
ThresholdType |
ABSOLUTE |
Drift measurement method |
threshold |
float |
0.05 | Drift limit triggering rebalance |
Absolute vs Relative Thresholds¶
-
Absolute (
ThresholdType.ABSOLUTE): Triggers when any asset's weight deviates by more thanthresholdpercentage points from its target. E.g.,threshold=0.05means a 25% target weight triggers rebalancing when it drifts below 20% or above 30%. -
Relative (
ThresholdType.RELATIVE): Triggers when any asset's weight deviates by more thanthresholdfraction of its target. E.g.,threshold=0.25means a 20% target triggers at 15% or 25% (25% of 20% = 5pp).
Presets¶
ThresholdRebalancingConfig.for_absolute(threshold=0.05) # 5pp absolute
ThresholdRebalancingConfig.for_relative(threshold=0.25) # 25% relative
Hybrid Rebalancing¶
Combines calendar and threshold strategies: the portfolio is reviewed at regular calendar intervals, but trades are executed only when drift exceeds the threshold at the review date. Between review dates, should_rebalance_hybrid always returns False regardless of drift.
This is the recommended strategy for most institutional portfolios — it reduces monitoring overhead (only check at review dates) while avoiding unnecessary trades (only trade when drift is significant).
from optimizer.rebalancing import HybridRebalancingConfig
config = HybridRebalancingConfig(
calendar=CalendarRebalancingConfig.for_monthly(),
threshold=ThresholdRebalancingConfig.for_absolute(threshold=0.05),
)
| Field | Type | Default | Description |
|---|---|---|---|
calendar |
CalendarRebalancingConfig |
Quarterly | Review schedule |
threshold |
ThresholdRebalancingConfig |
5pp absolute | Drift threshold at review |
Presets¶
HybridRebalancingConfig.for_monthly_with_5pct_threshold()
# Monthly reviews, rebalance only if 5pp drift
HybridRebalancingConfig.for_quarterly_with_10pct_threshold()
# Quarterly reviews, rebalance only if 10pp drift
Decision Functions¶
should_rebalance¶
Checks whether the portfolio should be rebalanced based on threshold drift:
from optimizer.rebalancing import should_rebalance, ThresholdRebalancingConfig
import numpy as np
previous = np.array([0.25, 0.25, 0.25, 0.25])
current = np.array([0.30, 0.20, 0.28, 0.22])
config = ThresholdRebalancingConfig(threshold=0.05)
needs_rebalance = should_rebalance(previous, current, config=config)
print(needs_rebalance) # True — 5pp drift in first asset
should_rebalance_hybrid¶
Checks both the calendar gate and the threshold:
from optimizer.rebalancing import should_rebalance_hybrid, HybridRebalancingConfig
import pandas as pd
config = HybridRebalancingConfig.for_monthly_with_5pct_threshold()
needs_rebalance = should_rebalance_hybrid(
previous, current, config,
current_date=pd.Timestamp("2024-03-15"),
last_review_date=pd.Timestamp("2024-02-15"),
)
Utility Functions¶
compute_drifted_weights¶
Compute what weights would be after one period of returns (without rebalancing):
from optimizer.rebalancing import compute_drifted_weights
import numpy as np
weights = np.array([0.50, 0.30, 0.20])
returns = np.array([0.02, -0.01, 0.03])
drifted = compute_drifted_weights(weights, returns)
print(drifted) # Weights after market movements, normalized to sum to 1
compute_turnover¶
One-way turnover between two weight vectors:
from optimizer.rebalancing import compute_turnover
import numpy as np
old_weights = np.array([0.25, 0.25, 0.25, 0.25])
new_weights = np.array([0.30, 0.20, 0.30, 0.20])
turnover = compute_turnover(old_weights, new_weights)
print(f"Turnover: {turnover:.2%}") # 10%
compute_rebalancing_cost¶
Transaction cost estimation based on turnover:
from optimizer.rebalancing import compute_rebalancing_cost
import numpy as np
cost = compute_rebalancing_cost(
old_weights=np.array([0.25, 0.25, 0.25, 0.25]),
new_weights=np.array([0.30, 0.20, 0.30, 0.20]),
cost_bps=10.0, # 10 basis points per unit of turnover
)
print(f"Cost: {cost:.4%}")
Code Examples¶
Rebalancing in the pipeline¶
from optimizer.optimization import MeanRiskConfig, build_mean_risk
from optimizer.pipeline import run_full_pipeline
from optimizer.rebalancing import ThresholdRebalancingConfig
import numpy as np
optimizer = build_mean_risk(MeanRiskConfig.for_max_sharpe())
result = run_full_pipeline(
prices=prices,
optimizer=optimizer,
previous_weights=np.array([0.25, 0.25, 0.25, 0.25]),
rebalancing_config=ThresholdRebalancingConfig(threshold=0.05),
)
if result.rebalance_needed:
print(f"Rebalance! Turnover: {result.turnover:.2%}")
print(f"New weights: {result.weights}")
else:
print("No rebalance needed — drift within threshold")
Hybrid rebalancing in the pipeline¶
from optimizer.rebalancing import HybridRebalancingConfig
import pandas as pd
result = run_full_pipeline(
prices=prices,
optimizer=optimizer,
previous_weights=current_portfolio_weights,
rebalancing_config=HybridRebalancingConfig.for_monthly_with_5pct_threshold(),
current_date=pd.Timestamp("2024-06-28"),
last_review_date=pd.Timestamp("2024-05-31"),
)
Gotchas and Tips¶
Hybrid always returns False between reviews
should_rebalance_hybrid returns False between calendar review dates regardless of drift. This is by design — it prevents over-trading. If you need continuous monitoring, use ThresholdRebalancingConfig alone.
previous_weights alignment
When passed to run_full_pipeline(), previous weights are automatically aligned to the post-pre-selection universe and re-normalized. Assets dropped by pre-selection have their weights set to zero.
Calendar frequency constants
The TRADING_DAYS dictionary maps each RebalancingFrequency to its trading-day count: {MONTHLY: 21, QUARTERLY: 63, SEMIANNUAL: 126, ANNUAL: 252}.
Cost estimation
compute_rebalancing_cost uses a simple proportional model: cost = turnover * cost_bps / 10000. For more realistic costs, consider bid-ask spreads, market impact, and commission schedules.
Quick Reference¶
| Task | Code |
|---|---|
| Monthly calendar | CalendarRebalancingConfig.for_monthly() |
| 5pp absolute threshold | ThresholdRebalancingConfig.for_absolute(0.05) |
| 25% relative threshold | ThresholdRebalancingConfig.for_relative(0.25) |
| Monthly + 5pp hybrid | HybridRebalancingConfig.for_monthly_with_5pct_threshold() |
| Check rebalance | should_rebalance(prev, new, config=cfg) |
| Compute turnover | compute_turnover(old, new) |
| Estimate costs | compute_rebalancing_cost(old, new, cost_bps=10) |
| Drifted weights | compute_drifted_weights(weights, returns) |