Portfolio Optimization and Quantitative Strategic Asset Allocation in Python
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Advanced constraint modeling including network constraints, clustering constraints, risk budgeting, factor risk constraints, and sophisticated portfolio construction techniques for institutional-level portfolio optimization.
Functions for defining constraints on individual assets and asset groups.
def assets_constraints(assets, min_val=None, max_val=None):
"""
Create asset-level constraints matrix.
Parameters:
- assets (list): List of asset names
- min_val (dict or float): Minimum weight constraints per asset
- max_val (dict or float): Maximum weight constraints per asset
Returns:
tuple: (A_matrix, B_vector) for inequality constraints Aw <= b
"""
def assets_views(P, Q):
"""
Create asset views matrix for Black-Litterman optimization.
Parameters:
- P (DataFrame): Picking matrix defining which assets each view affects
- Q (DataFrame): Views vector with expected returns for each view
Returns:
tuple: (P_matrix, Q_vector) formatted for optimization
"""
def assets_clusters(returns, codependence='pearson', linkage='ward', k=None, max_k=10):
"""
Create asset clustering constraints for hierarchical allocation.
Parameters:
- returns (DataFrame): Asset returns data
- codependence (str): Distance measure for clustering
- linkage (str): Linkage method for hierarchical clustering
- k (int): Number of clusters (optional, will optimize if None)
- max_k (int): Maximum number of clusters to consider
Returns:
DataFrame: Cluster assignment matrix
"""Constraints based on risk factor models and factor exposures.
def factors_constraints(B, factors, min_val=None, max_val=None):
"""
Create factor exposure constraints.
Parameters:
- B (DataFrame): Factor loadings matrix (assets x factors)
- factors (list): List of factor names to constrain
- min_val (dict or float): Minimum factor exposure limits
- max_val (dict or float): Maximum factor exposure limits
Returns:
tuple: (A_matrix, B_vector) for factor constraints
"""
def factors_views(B, P, Q):
"""
Create factor views for Black-Litterman factor model.
Parameters:
- B (DataFrame): Factor loadings matrix
- P (DataFrame): Factor picking matrix
- Q (DataFrame): Factor views vector
Returns:
tuple: (P_factor, Q_factor) for factor-based views
"""Advanced risk budgeting and risk contribution constraints.
def risk_constraint(w, cov, rm='MV', rf=0, alpha=0.05):
"""
Calculate risk constraint for portfolio optimization.
Parameters:
- w (DataFrame): Portfolio weights
- cov (DataFrame): Covariance matrix
- rm (str): Risk measure code
- rf (float): Risk-free rate
- alpha (float): Significance level for risk measures
Returns:
float: Risk constraint value
"""
def hrp_constraints(returns, codependence='pearson', covariance='hist',
linkage='single', max_k=10, leaf_order=True):
"""
Generate Hierarchical Risk Parity constraints.
Parameters:
- returns (DataFrame): Asset returns
- codependence (str): Codependence measure
- covariance (str): Covariance estimation method
- linkage (str): Linkage method for clustering
- max_k (int): Maximum number of clusters
- leaf_order (bool): Optimize dendrogram leaf order
Returns:
dict: HRP constraint specifications
"""Constraints based on asset correlation networks and graph theory.
def connection_matrix(returns, network_method='MST', codependence='pearson'):
"""
Generate network connection matrix based on asset correlations.
Parameters:
- returns (DataFrame): Asset returns data
- network_method (str): Network construction method ('MST', 'PMFG', 'TN', 'DBHT')
- codependence (str): Measure of dependence between assets
Returns:
DataFrame: Binary connection matrix (1 = connected, 0 = not connected)
"""
def centrality_vector(adjacency_matrix, centrality='degree'):
"""
Calculate centrality measures for network nodes.
Parameters:
- adjacency_matrix (DataFrame): Network adjacency matrix
- centrality (str): Centrality measure ('degree', 'betweenness', 'closeness', 'eigenvector')
Returns:
DataFrame: Centrality scores for each asset
"""
def clusters_matrix(returns, codependence='pearson', linkage='ward', k=None, max_k=10):
"""
Generate cluster assignment matrix for portfolio constraints.
Parameters:
- returns (DataFrame): Asset returns
- codependence (str): Codependence measure
- linkage (str): Hierarchical linkage method
- k (int): Number of clusters (will optimize if None)
- max_k (int): Maximum clusters to consider
Returns:
DataFrame: Binary cluster assignment matrix
"""
def average_centrality(adjacency_matrix, centrality='degree'):
"""
Calculate average network centrality.
Parameters:
- adjacency_matrix (DataFrame): Network adjacency matrix
- centrality (str): Centrality measure type
Returns:
float: Average centrality value
"""
def connected_assets(adjacency_matrix, asset):
"""
Find assets directly connected to a given asset in the network.
Parameters:
- adjacency_matrix (DataFrame): Network adjacency matrix
- asset (str): Asset name to find connections for
Returns:
list: List of directly connected asset names
"""
def related_assets(adjacency_matrix, asset, max_distance=2):
"""
Find assets related to a given asset within specified network distance.
Parameters:
- adjacency_matrix (DataFrame): Network adjacency matrix
- asset (str): Asset name to find relations for
- max_distance (int): Maximum network distance to consider
Returns:
dict: Dictionary mapping distance to list of assets at that distance
"""Portfolio constraints for institutional and complex optimization scenarios.
# Network SDP (Semi-Definite Programming) Constraints
def network_sdp_constraint(returns, network_method='MST', alpha=0.05):
"""
Generate network-based SDP constraints for portfolio optimization.
Parameters:
- returns (DataFrame): Asset returns
- network_method (str): Network construction method
- alpha (float): Network density parameter
Returns:
dict: SDP constraint specifications
"""
# Cluster SDP Constraints
def cluster_sdp_constraint(returns, k=5, linkage='ward', alpha=0.05):
"""
Generate cluster-based SDP constraints.
Parameters:
- returns (DataFrame): Asset returns
- k (int): Number of clusters
- linkage (str): Clustering linkage method
- alpha (float): Constraint strength parameter
Returns:
dict: Cluster SDP constraint specifications
"""
# Integer Programming Constraints
def network_ip_constraint(adjacency_matrix, max_assets=20):
"""
Generate network-based integer programming constraints.
Parameters:
- adjacency_matrix (DataFrame): Network adjacency matrix
- max_assets (int): Maximum number of assets to select
Returns:
dict: Integer programming constraint specifications
"""
def cluster_ip_constraint(cluster_matrix, max_per_cluster=5):
"""
Generate cluster-based integer programming constraints.
Parameters:
- cluster_matrix (DataFrame): Cluster assignment matrix
- max_per_cluster (int): Maximum assets per cluster
Returns:
dict: Cluster IP constraint specifications
"""import riskfolio as rp
import pandas as pd
# Load returns
returns = pd.read_csv('returns.csv', index_col=0, parse_dates=True)
assets = returns.columns.tolist()
# Create basic asset constraints
# Minimum 1%, maximum 10% per asset
A, b = rp.assets_constraints(
assets=assets,
min_val=0.01, # 1% minimum
max_val=0.10 # 10% maximum
)
# Create portfolio with constraints
port = rp.Portfolio(returns=returns)
port.assets_stats()
# Set constraints
port.ainequality = A
port.binequality = b
# Optimize with constraints
w = port.optimization(model='Classic', rm='MV', obj='Sharpe', rf=0.02)
print("Constrained Portfolio Weights:")
print(w.sort_values('weights', ascending=False).head(10))import riskfolio as rp
import pandas as pd
import numpy as np
# Load returns and sector mapping
returns = pd.read_csv('returns.csv', index_col=0, parse_dates=True)
sectors = pd.read_csv('sectors.csv', index_col=0) # Asset -> Sector mapping
# Create sector constraints (max 30% per sector)
sector_names = sectors['Sector'].unique()
A_sector = []
b_sector = []
for sector in sector_names:
sector_assets = sectors[sectors['Sector'] == sector].index
# Create constraint row: sum of weights in sector <= 0.30
constraint_row = pd.Series(0.0, index=returns.columns)
constraint_row[sector_assets] = 1.0
A_sector.append(constraint_row)
b_sector.append(0.30)
A_sector = pd.DataFrame(A_sector)
b_sector = pd.Series(b_sector)
# Apply to portfolio
port = rp.Portfolio(returns=returns)
port.assets_stats()
port.ainequality = A_sector
port.binequality = b_sector
w_sector = port.optimization(model='Classic', rm='MV', obj='Sharpe', rf=0.02)
# Analyze sector allocation
sector_allocation = w_sector.groupby(sectors['Sector']).sum()
print("Sector Allocation:")
print(sector_allocation.sort_values('weights', ascending=False))import riskfolio as rp
import pandas as pd
# Load returns and factors
returns = pd.read_csv('returns.csv', index_col=0, parse_dates=True)
factors = pd.read_csv('factors.csv', index_col=0, parse_dates=True)
# Estimate factor loadings
B = rp.loadings_matrix(X=returns, Y=factors, method='stepwise')
# Create factor constraints (e.g., market beta between 0.8 and 1.2)
factor_constraints = {
'Market': {'min': 0.8, 'max': 1.2},
'Value': {'min': -0.5, 'max': 0.5},
'Size': {'min': -0.3, 'max': 0.3}
}
A_factors = []
b_factors = []
for factor, bounds in factor_constraints.items():
if factor in B.columns:
# Beta <= max constraint
A_factors.append(B[factor])
b_factors.append(bounds['max'])
# Beta >= min constraint (converted to -Beta <= -min)
A_factors.append(-B[factor])
b_factors.append(-bounds['min'])
A_factors = pd.DataFrame(A_factors).T
b_factors = pd.Series(b_factors)
# Apply factor constraints
port = rp.Portfolio(returns=returns, factors=factors)
port.factors_stats()
port.B = B
port.afrcinequality = A_factors
port.bfrcinequality = b_factors
w_factor = port.optimization(model='FM', rm='MV', obj='Sharpe', rf=0.02)
# Check factor exposures
factor_exposures = (w_factor.T @ B).T
print("Factor Exposures:")
print(factor_exposures)import riskfolio as rp
import pandas as pd
# Load returns
returns = pd.read_csv('returns.csv', index_col=0, parse_dates=True)
# Generate network connection matrix
adjacency = rp.connection_matrix(
returns=returns,
network_method='MST',
codependence='pearson'
)
# Calculate centrality measures
centrality = rp.centrality_vector(
adjacency_matrix=adjacency,
centrality='degree'
)
# Create centrality-based constraints (limit high centrality assets)
# Assets with high centrality (>75th percentile) limited to 5% each
high_centrality_threshold = centrality['centrality'].quantile(0.75)
high_centrality_assets = centrality[centrality['centrality'] > high_centrality_threshold].index
A_centrality = []
b_centrality = []
for asset in high_centrality_assets:
constraint_row = pd.Series(0.0, index=returns.columns)
constraint_row[asset] = 1.0
A_centrality.append(constraint_row)
b_centrality.append(0.05) # 5% limit
if A_centrality: # Only if there are high centrality assets
A_centrality = pd.DataFrame(A_centrality)
b_centrality = pd.Series(b_centrality)
# Apply network constraints
port = rp.Portfolio(returns=returns)
port.assets_stats()
port.acentrality = A_centrality
port.bcentrality = b_centrality
w_network = port.optimization(model='Classic', rm='MV', obj='Sharpe', rf=0.02)
print("Network-Constrained Portfolio:")
print(w_network.sort_values('weights', ascending=False).head(10))
# Find connected assets for top holdings
top_asset = w_network.sort_values('weights', ascending=False).index[0]
connected = rp.connected_assets(adjacency, top_asset)
print(f"\nAssets connected to {top_asset}: {connected}")import riskfolio as rp
import pandas as pd
import numpy as np
# Load returns
returns = pd.read_csv('returns.csv', index_col=0, parse_dates=True)
# Create risk budget vector (equal risk contribution desired)
n_assets = len(returns.columns)
risk_budget = pd.DataFrame(1/n_assets, index=returns.columns, columns=['budget'])
# Create portfolio for risk parity
port = rp.Portfolio(returns=returns)
port.assets_stats()
# Risk parity optimization
w_rp = port.rp_optimization(
model='Classic',
rm='MV',
b=risk_budget,
rf=0.02
)
# Calculate actual risk contributions
risk_contrib = rp.Risk_Contribution(w_rp, port.cov, rm='MV')
print("Risk Parity Portfolio:")
print("Weights vs Risk Contributions:")
comparison = pd.DataFrame({
'Weights': w_rp['weights'],
'Risk_Contrib': risk_contrib['Risk'],
'Target_Budget': risk_budget['budget']
})
print(comparison.head(10))
# Custom risk budgets (e.g., sector-based)
# Suppose we want 40% risk from tech, 30% from finance, 30% from others
sectors = pd.read_csv('sectors.csv', index_col=0)
custom_budget = pd.Series(0.0, index=returns.columns)
tech_assets = sectors[sectors['Sector'] == 'Technology'].index
finance_assets = sectors[sectors['Sector'] == 'Finance'].index
other_assets = sectors[~sectors['Sector'].isin(['Technology', 'Finance'])].index
custom_budget[tech_assets] = 0.40 / len(tech_assets)
custom_budget[finance_assets] = 0.30 / len(finance_assets)
custom_budget[other_assets] = 0.30 / len(other_assets)
custom_budget_df = pd.DataFrame(custom_budget, columns=['budget'])
# Optimize with custom risk budget
w_custom_rp = port.rp_optimization(
model='Classic',
rm='CVaR',
b=custom_budget_df,
rf=0.02
)
print("\nCustom Risk Budget Results:")
print(w_custom_rp.head(10))Install with Tessl CLI
npx tessl i tessl/pypi-riskfolio-lib