Portfolio Allocation Examples
This document demonstrates various portfolio optimization methods available in the pypulate
library using the Allocation
class.
Overview
The Allocation
class provides several portfolio optimization methods:
- Mean-Variance Optimization
- Minimum Variance Portfolio
- Maximum Sharpe Ratio Portfolio
- Risk Parity Portfolio
- Maximum Diversification Portfolio
- Equal Weight Portfolio
- Market Cap Weight Portfolio
- Kelly Criterion Portfolio
- Black-Litterman Portfolio
- Hierarchical Risk Parity Portfolio
Basic Usage
First, let's import the necessary components:
Basic Portfolio Optimization
Let's start with a simple example using 3 assets:
# Sample returns data (3 assets, 252 days of daily returns)
returns = Parray([
# Asset 1 (e.g., AAPL)
[0.02, -0.01, 0.015, 0.03, -0.005, 0.01, 0.02, -0.015, 0.025, 0.01] +
[0.015, -0.02, 0.01, 0.02, -0.01, 0.015, 0.025, -0.01, 0.02, 0.015] * 24 +
[0.015, -0.02, 0.01, 0.02, -0.01, 0.015, 0.025, -0.01, 0.02, 0.015],
# Asset 2 (e.g., MSFT)
[0.015, 0.02, -0.01, 0.025, 0.01, -0.015, 0.02, 0.01, -0.02, 0.015] +
[0.02, -0.015, 0.015, 0.025, -0.01, 0.02, 0.015, -0.015, 0.02, 0.01] * 24 +
[0.02, -0.015, 0.015, 0.025, -0.01, 0.02, 0.015, -0.015, 0.02, 0.01],
# Asset 3 (e.g., GOOGL)
[0.025, -0.02, 0.02, 0.015, -0.015, 0.02, 0.025, -0.02, 0.03, 0.02] +
[0.025, -0.02, 0.02, 0.015, -0.015, 0.02, 0.025, -0.02, 0.03, 0.02] * 24 +
[0.025, -0.02, 0.02, 0.015, -0.015, 0.02, 0.025, -0.02, 0.03, 0.02]
]).T
# Initialize the Allocation class
allocation = Allocation()
# Set risk-free rate (e.g., current 10-year Treasury yield)
risk_free_rate = 0.04 # 4% annual rate
# Perform Mean-Variance Optimization
weights, ret, risk = allocation.mean_variance(
returns=returns,
target_return=None, # Maximize Sharpe ratio
risk_free_rate=risk_free_rate
)
print(f"Optimal Portfolio Weights: {weights}")
print(f"Expected Return: {ret:.4f}")
print(f"Portfolio Risk: {risk:.4f}")
Risk Parity Portfolio
Risk Parity aims to equalize the risk contribution of each asset:
# Calculate Risk Parity weights
weights, ret, risk = allocation.risk_parity(returns=returns)
print(f"Risk Parity Weights: {weights}")
print(f"Expected Return: {ret:.4f}")
print(f"Portfolio Risk: {risk:.4f}")
Kelly Criterion with Conservative Sizing
The Kelly Criterion can be aggressive, so we often use a fraction of the optimal weights:
# Calculate Kelly Criterion weights
weights, ret, risk = allocation.kelly_criterion(
returns=returns,
risk_free_rate=risk_free_rate
)
# Use half-Kelly for more conservative position sizing
half_kelly_weights = weights * 0.5
print(f"Full Kelly Weights: {weights}")
print(f"Half-Kelly Weights: {half_kelly_weights}")
Black-Litterman Portfolio with Views
Black-Litterman allows incorporating market views into the optimization:
# Market capitalizations
market_caps = Parray([2.5e12, 2.8e12, 1.8e12]) # in USD
# Define views (e.g., AAPL to outperform by 2%, GOOGL to underperform by 1%)
views = {0: 0.02, 2: -0.01} # Asset indices and expected excess returns
view_confidences = {0: 0.8, 2: 0.7} # Confidence in views (0-1)
# Calculate Black-Litterman weights
weights, ret, risk = allocation.black_litterman(
returns=returns,
market_caps=market_caps,
views=views,
view_confidences=view_confidences,
tau=0.05, # Uncertainty in the prior distribution
risk_free_rate=risk_free_rate
)
print(f"Black-Litterman Weights: {weights}")
Hierarchical Risk Parity
HRP uses hierarchical clustering to build a more robust portfolio:
# Calculate HRP weights
weights, ret, risk = allocation.hierarchical_risk_parity(
returns=returns,
linkage_method='ward', # Using Ward linkage for clustering
distance_metric='correlation' # Using correlation-based distance
)
print(f"HRP Weights: {weights}")
Comparing Different Methods
Here's how to compare the performance of different optimization methods:
# Define methods to compare
methods = [
("Mean-Variance", allocation.mean_variance(returns, risk_free_rate=risk_free_rate)),
("Minimum Variance", allocation.minimum_variance(returns)),
("Maximum Sharpe", allocation.maximum_sharpe(returns, risk_free_rate=risk_free_rate)),
("Risk Parity", allocation.risk_parity(returns)),
("Kelly Criterion", allocation.kelly_criterion(returns, risk_free_rate=risk_free_rate))
]
# Compare results
print("\nMethod Comparison Summary:")
print("-" * 50)
print(f"{'Method':<25} {'Return':>10} {'Risk':>10} {'Sharpe':>10}")
print("-" * 50)
for method_name, (weights, ret, risk) in methods:
sharpe = (ret - risk_free_rate) / risk
print(f"{method_name:<25} {ret*100:>9.2f}% {risk*100:>9.2f}% {sharpe:>9.2f}")
Best Practices
1. Data Preparation
- 1.1. Data Quality: Use clean, adjusted price data
- 1.2. Missing Values: Handle missing values appropriately
- 1.3. Transaction Costs: Consider transaction costs and liquidity
2. Risk Management
- 2.1. Position Sizing: Consider using half-Kelly or quarter-Kelly for more conservative position sizing
- 2.2. Constraints: Implement position limits and constraints
- 2.3. Monitoring: Monitor portfolio turnover and rebalancing needs
3. Method Selection
- 3.1. Mean-Variance: Good for traditional portfolio optimization
- 3.2. Risk Parity: Better for risk management
- 3.3. Kelly Criterion: Best for long-term growth
- 3.4. Black-Litterman: Ideal when you have strong market views
- 3.5. HRP: More robust to estimation errors
4. Portfolio Maintenance
- 4.1. Rebalancing Thresholds: Set appropriate rebalancing thresholds
- 4.2. Cost Management: Consider transaction costs when rebalancing
- 4.3. Tracking Error: Monitor tracking error against benchmarks
Common Pitfalls
1. Estimation Issues
- 1.1. Overfitting: Use sufficient historical data
- 1.2. Sample Bias: Consider using rolling windows
- 1.3. Validation: Implement out-of-sample testing
2. Statistical Challenges
- 2.1. Estimation Error: Use robust estimation methods
- 2.2. Shrinkage: Consider shrinkage estimators
- 2.3. Regularization: Implement proper regularization
3. Implementation Realities
- 3.1. Bid-Ask Spreads: Account for bid-ask spreads
- 3.2. Market Impact: Consider market impact of trades
- 3.3. Turnover Constraints: Implement turnover constraints