Strategy Allocation Manager
Dynamically allocate trading capital across a portfolio of multiple concurrent strategies based on each strategy recent risk-adjusted performance metrics, current drawdown depth, and cross-strategy return correlation, implementing a meta-strategy layer for optimal capital deployment across the strategy portfolio.
Allocate Capital Across Strategies
This notebook explores various methods for allocating capital across different investment strategies or assets. Effective capital allocation is crucial for managing risk, optimizing returns, and achieving specific investment objectives. We will delve into several allocation techniques, from simple heuristics to more sophisticated optimization approaches, and demonstrate their application with simulated data.
Concepts Covered:
| Concept | Description | Key Metrics |
|---|---|---|
| Capital Allocation | The process of distributing investment capital among different assets or strategies to achieve objectives. | Portfolio Return, Risk, Sharpe Ratio |
| Investment Strategies | Defined approaches for making investment decisions (e.g., value, growth, momentum). | Alpha, Beta, Volatility |
| Risk Management | Identifying, assessing, and mitigating investment risks. | Value at Risk (VaR), Conditional VaR |
| Portfolio Optimization | Selecting the best portfolio based on risk-return trade-offs. | Efficient Frontier |
| Mean-Variance Optimization | A classical approach to constructing optimal portfolios by balancing expected return and variance. | Expected Return, Standard Deviation |
| Risk Parity | An allocation approach where each component (asset/strategy) contributes equally to the total portfolio risk. | Risk Contribution |
| Simulated Returns | Generating synthetic asset or strategy returns for testing allocation methods. | Mean, Standard Deviation |
Dependency Installation
This section installs all necessary libraries for the notebook, including data manipulation, statistical analysis, optimization, and plotting.
pip install pandas numpy scipy matplotlib seaborn cvxpy yfinanceRequirement already satisfied: pandas in /usr/local/lib/python3.12/dist-packages (2.2.2) Requirement already satisfied: numpy in /usr/local/lib/python3.12/dist-packages (2.0.2) Requirement already satisfied: scipy in /usr/local/lib/python3.12/dist-packages (1.16.3) Requirement already satisfied: matplotlib in /usr/local/lib/python3.12/dist-packages (3.10.0) Requirement already satisfied: seaborn in /usr/local/lib/python3.12/dist-packages (0.13.2) Requirement already satisfied: cvxpy in /usr/local/lib/python3.12/dist-packages (1.6.7) Requirement already satisfied: yfinance in /usr/local/lib/python3.12/dist-packages (0.2.66) Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.9.0.post0) Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2) Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas) (2026.2) Requirement already satisfied: contourpy>=1.0.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.3.3) Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (0.12.1) Requirement already satisfied: fonttools>=4.22.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (4.63.0) Requirement already satisfied: kiwisolver>=1.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.5.0) Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (26.2) Requirement already satisfied: pillow>=8 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (11.3.0) Requirement already satisfied: pyparsing>=2.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (3.3.2) Requirement already satisfied: osqp>=0.6.2 in /usr/local/lib/python3.12/dist-packages (from cvxpy) (1.1.1) Requirement already satisfied: clarabel>=0.5.0 in /usr/local/lib/python3.12/dist-packages (from cvxpy) (0.11.1) Requirement already satisfied: scs>=3.2.4.post1 in /usr/local/lib/python3.12/dist-packages (from cvxpy) (3.2.11) Requirement already satisfied: requests>=2.31 in /usr/local/lib/python3.12/dist-packages (from yfinance) (2.32.4) Requirement already satisfied: multitasking>=0.0.7 in /usr/local/lib/python3.12/dist-packages (from yfinance) (0.0.13) Requirement already satisfied: platformdirs>=2.0.0 in /usr/local/lib/python3.12/dist-packages (from yfinance) (4.10.0) Requirement already satisfied: frozendict>=2.3.4 in /usr/local/lib/python3.12/dist-packages (from yfinance) (2.4.7) Requirement already satisfied: peewee>=3.16.2 in /usr/local/lib/python3.12/dist-packages (from yfinance) (4.0.6) Requirement already satisfied: beautifulsoup4>=4.11.1 in /usr/local/lib/python3.12/dist-packages (from yfinance) (4.13.5) Requirement already satisfied: curl_cffi>=0.7 in /usr/local/lib/python3.12/dist-packages (from yfinance) (0.15.0) Requirement already satisfied: protobuf>=3.19.0 in /usr/local/lib/python3.12/dist-packages (from yfinance) (5.29.6) Requirement already satisfied: websockets>=13.0 in /usr/local/lib/python3.12/dist-packages (from yfinance) (15.0.1) Requirement already satisfied: soupsieve>1.2 in /usr/local/lib/python3.12/dist-packages (from beautifulsoup4>=4.11.1->yfinance) (2.8.4) Requirement already satisfied: typing-extensions>=4.0.0 in /usr/local/lib/python3.12/dist-packages (from beautifulsoup4>=4.11.1->yfinance) (4.15.0) Requirement already satisfied: cffi in /usr/local/lib/python3.12/dist-packages (from clarabel>=0.5.0->cvxpy) (2.0.0) Requirement already satisfied: certifi>=2024.2.2 in /usr/local/lib/python3.12/dist-packages (from curl_cffi>=0.7->yfinance) (2026.5.20) Requirement already satisfied: rich in /usr/local/lib/python3.12/dist-packages (from curl_cffi>=0.7->yfinance) (13.9.4) Requirement already satisfied: jinja2 in /usr/local/lib/python3.12/dist-packages (from osqp>=0.6.2->cvxpy) (3.1.6) Requirement already satisfied: setuptools in /usr/local/lib/python3.12/dist-packages (from osqp>=0.6.2->cvxpy) (75.2.0) Requirement already satisfied: joblib in /usr/local/lib/python3.12/dist-packages (from osqp>=0.6.2->cvxpy) (1.5.3) Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas) (1.17.0) Requirement already satisfied: charset_normalizer<4,>=2 in /usr/local/lib/python3.12/dist-packages (from requests>=2.31->yfinance) (3.4.7) Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.12/dist-packages (from requests>=2.31->yfinance) (3.18) Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.12/dist-packages (from requests>=2.31->yfinance) (2.5.0) Requirement already satisfied: pycparser in /usr/local/lib/python3.12/dist-packages (from cffi->clarabel>=0.5.0->cvxpy) (3.0) Requirement already satisfied: MarkupSafe>=2.0 in /usr/local/lib/python3.12/dist-packages (from jinja2->osqp>=0.6.2->cvxpy) (3.0.3) Requirement already satisfied: markdown-it-py>=2.2.0 in /usr/local/lib/python3.12/dist-packages (from rich->curl_cffi>=0.7->yfinance) (4.2.0) Requirement already satisfied: pygments<3.0.0,>=2.13.0 in /usr/local/lib/python3.12/dist-packages (from rich->curl_cffi>=0.7->yfinance) (2.20.0) Requirement already satisfied: mdurl~=0.1 in /usr/local/lib/python3.12/dist-packages (from markdown-it-py>=2.2.0->rich->curl_cffi>=0.7->yfinance) (0.1.2)
Library Imports
All required libraries are imported here, organized by standard libraries first, followed by third-party libraries.
import logging
import random
import time
from collections import deque
from typing import Any, Dict, List, Tuple
import cvxpy as cp
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
import yfinance as yf
from scipy.optimize import minimizeCore Functions
This section defines the core functions used throughout the notebook for capital allocation, simulation, and analysis. Each function is presented in its own dedicated code block, preceded by a markdown header explaining its purpose, algorithm, and parameters.
Function Name: create_logger
This function initializes and returns a custom logger instance. It configures the logger to output messages to the console with a specified format, including the timestamp, log level, and message. This is essential for tracking important operations, debugging, and providing informational updates throughout the capital allocation process.
Parameters:
name(str): The name of the logger, typically__name__for modules.level(int, optional): The logging level (e.g.,logging.INFO,logging.DEBUG). Defaults tologging.INFO.
Returns:
logging.Logger: An initialized logger instance.
def create_logger(name: str, level: int = logging.INFO) -> logging.Logger:
"""
Initializes and returns a custom logger instance.
Parameters
----------
name : str
The name of the logger.
level : int, optional
The logging level (e.g., logging.INFO, logging.DEBUG), defaults to logging.INFO.
Returns
-------
logging.Logger
An initialized logger instance.
Examples
--------
>>> log = create_logger('my_module')
>>> log.info('This is an info message.')
"""
logger = logging.getLogger(name)
logger.setLevel(level)
if not logger.handlers:
handler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.info(f"Logger '{name}' initialized with level {logging.getLevelName(level)}.")
return logger
# Initialize a global logger for the notebook
log = create_logger('CapitalAllocationNotebook')2026-06-10 10:10:11,465 - CapitalAllocationNotebook - INFO - Logger 'CapitalAllocationNotebook' initialized with level INFO. INFO:CapitalAllocationNotebook:Logger 'CapitalAllocationNotebook' initialized with level INFO.
Function Name: create_initial_state
This function initializes the primary state dictionary for the capital allocation process. It sets up key parameters such as the number of assets, investment horizon, random seed for reproducibility, and other default configurations. This centralized state management ensures consistency across different functions.
Parameters:
num_assets(int): The number of assets or strategies to consider for allocation. Defaults to 5.investment_horizon(int): The number of periods (e.g., days) for which to simulate or analyze returns. Defaults to 252 (approximately one trading year).random_seed(int): A seed for the random number generator to ensure reproducibility of simulations. Defaults to 42.
Returns:
dict: An initial state dictionary containing configuration parameters.
def create_initial_state(
num_assets: int = 5,
investment_horizon: int = 252,
random_seed: int = 42,
) -> Dict:
"""
Initializes the primary state dictionary for the capital allocation process.
Parameters
----------
num_assets : int, optional
The number of assets or strategies to consider, defaults to 5.
investment_horizon : int, optional
The number of periods for simulation/analysis, defaults to 252.
random_seed : int, optional
A seed for the random number generator, defaults to 42.
Returns
-------
Dict
An initial state dictionary containing configuration parameters.
Examples
--------
>>> initial_state = create_initial_state(num_assets=3, investment_horizon=100)
>>> print(initial_state['num_assets'])
3
"""
log.info("Creating initial state dictionary.")
state = {
'num_assets': num_assets,
'investment_horizon': investment_horizon,
'random_seed': random_seed,
'asset_names': [f'Asset_{i+1}' for i in range(num_assets)],
'current_weights': np.array([1/num_assets] * num_assets), # Start with equal weights
'portfolio_returns': pd.Series(dtype=float),
'portfolio_value': pd.Series(dtype=float),
'simulation_history': [],
'backoff_attempts': 0
}
log.debug(f"Initial state created: {state}")
return stateFunction Name: generate_asset_returns
This function generates simulated daily asset returns using a multivariate normal distribution. It allows for specifying mean returns, standard deviations, and a correlation matrix to create realistic inter-asset dependencies. Random jitter can be added to simulate more unpredictable market conditions. This function is crucial for testing capital allocation strategies in a controlled environment.
Parameters:
state(dict): The current state dictionary.num_periods(int): The number of periods (e.g., days) for which to generate returns.mean_returns(np.ndarray, optional): Array of mean daily returns for each asset. If None, defaults to small positive values. The length must matchnum_assets.stds(np.ndarray, optional): Array of daily standard deviations for each asset. If None, defaults to small positive values. The length must matchnum_assets.corr_matrix(np.ndarray, optional): Correlation matrix between assets. If None, an identity matrix (no correlation) is used. Must be square and positive semi-definite.add_jitter(bool): If True, adds a small random jitter to the generated returns. Defaults to False.
Returns:
dict: The updated state dictionary, including a DataFrame of simulated returns.
def generate_asset_returns(
state: Dict,
num_periods: int,
mean_returns: np.ndarray = None,
stds: np.ndarray = None,
corr_matrix: np.ndarray = None,
add_jitter: bool = False
) -> Dict:
"""
Generates simulated daily asset returns using a multivariate normal distribution.
Parameters
----------
state : Dict
The current state dictionary.
num_periods : int
The number of periods (e.g., days) for which to generate returns.
mean_returns : np.ndarray, optional
Array of mean daily returns for each asset. If None, defaults to small positive values.
stds : np.ndarray, optional
Array of daily standard deviations for each asset. If None, defaults to small positive values.
corr_matrix : np.ndarray, optional
Correlation matrix between assets. If None, an identity matrix (no correlation) is used.
add_jitter : bool, optional
If True, adds a small random jitter to the generated returns, defaults to False.
Returns
-------
Dict
The updated state dictionary, including a DataFrame of simulated returns.
Examples
--------
>>> state = create_initial_state(num_assets=2)
>>> state = generate_asset_returns(state, num_periods=10)
>>> print(state['asset_returns'].shape)
(10, 2)
"""
log.info(f"Generating {num_periods} periods of asset returns for {state['num_assets']} assets.")
np.random.seed(state['random_seed'])
num_assets = state['num_assets']
if mean_returns is None:
mean_returns = np.random.rand(num_assets) * 0.0005 # Daily mean returns
if stds is None:
stds = np.random.rand(num_assets) * 0.01 # Daily standard deviations
if corr_matrix is None:
corr_matrix = np.eye(num_assets)
else:
if corr_matrix.shape != (num_assets, num_assets):
log.error("Correlation matrix dimensions do not match the number of assets.")
raise ValueError("Correlation matrix dimensions must match num_assets.")
# Convert correlation matrix to covariance matrix
cov_matrix = np.diag(stds) @ corr_matrix @ np.diag(stds)
# Generate returns
daily_returns = np.random.multivariate_normal(mean_returns, cov_matrix, num_periods)
if add_jitter:
jitter = np.random.normal(0, 0.0001, daily_returns.shape)
daily_returns += jitter
log.debug("Added random jitter to asset returns.")
state['asset_returns'] = pd.DataFrame(daily_returns, columns=state['asset_names'])
log.info("Asset returns generated successfully.")
log.debug(f"Generated returns head:\n{state['asset_returns'].head()}")
return stateFunction Name: get_tracking_metrics
This function calculates and summarizes key performance metrics for a given portfolio return series. It computes metrics such as total return, annualized return, annualized volatility, Sharpe ratio, and maximum drawdown. These metrics are essential for evaluating the effectiveness of different capital allocation strategies.
Parameters:
state(dict): The current state dictionary.returns(pd.Series): A pandas Series of daily portfolio returns.annualization_factor(int, optional): The factor to annualize daily metrics (e.g., 252 for trading days). Defaults to 252.risk_free_rate(float, optional): The risk-free rate for Sharpe ratio calculation. Defaults to 0.01 (1% annualized).
Returns:
Dict: A dictionary containing the calculated performance metrics.
def get_tracking_metrics(
state: Dict,
returns: pd.Series,
annualization_factor: int = 252,
risk_free_rate: float = 0.01
) -> Dict:
"""
Calculates and summarizes key performance metrics for a given portfolio return series.
Parameters
----------
state : Dict
The current state dictionary.
returns : pd.Series
A pandas Series of daily portfolio returns.
annualization_factor : int, optional
The factor to annualize daily metrics, defaults to 252.
risk_free_rate : float, optional
The risk-free rate for Sharpe ratio calculation, defaults to 0.01.
Returns
-------
Dict
A dictionary containing the calculated performance metrics.
Examples
--------
>>> state = create_initial_state()
>>> synthetic_returns = pd.Series(np.random.normal(0.0005, 0.01, 252))
>>> metrics = get_tracking_metrics(state, synthetic_returns)
>>> print(metrics['annualized_return'])
"""
if returns.empty:
log.warning("Returns series is empty, cannot calculate metrics.")
return {}
log.info("Calculating performance metrics.")
# Cumulative returns
cumulative_returns = (1 + returns).prod() - 1
# Annualized return
annualized_return = (1 + returns).prod()**(annualization_factor / len(returns)) - 1
# Annualized volatility
annualized_volatility = returns.std() * np.sqrt(annualization_factor)
# Sharpe Ratio
# Ensure risk_free_rate is on a daily basis for daily returns calculation
daily_risk_free_rate = (1 + risk_free_rate)**(1/annualization_factor) - 1
sharpe_ratio = (returns.mean() - daily_risk_free_rate) / returns.std() * np.sqrt(annualization_factor)
# Max Drawdown
cumulative_wealth = (1 + returns).cumprod()
peak = cumulative_wealth.expanding(min_periods=1).max()
drawdown = (cumulative_wealth / peak) - 1
max_drawdown = drawdown.min()
metrics = {
'total_return': cumulative_returns,
'annualized_return': annualized_return,
'annualized_volatility': annualized_volatility,
'sharpe_ratio': sharpe_ratio,
'max_drawdown': max_drawdown
}
log.debug(f"Calculated metrics: {metrics}")
return metricsFunction Name: calculate_portfolio_returns
This function computes the portfolio returns for each period given the individual asset returns and a set of portfolio weights. It's a fundamental building block for evaluating any capital allocation strategy. The portfolio return is calculated as the dot product of asset returns and their corresponding weights.
Parameters:
state(dict): The current state dictionary. (Used for logging, but not for direct calculation here).asset_returns(pd.DataFrame): A DataFrame where each column represents an asset's returns and each row is a period.weights(np.ndarray): A 1D NumPy array representing the weights allocated to each asset. The sum of weights should be 1.
Returns:
pd.Series: A pandas Series containing the calculated portfolio returns for each period.
def calculate_portfolio_returns(
state: Dict,
asset_returns: pd.DataFrame,
weights: np.ndarray
) -> pd.Series:
"""
Computes the portfolio returns for each period given individual asset returns and weights.
Parameters
----------
state : Dict
The current state dictionary.
asset_returns : pd.DataFrame
A DataFrame where each column represents an asset's returns and each row is a period.
weights : np.ndarray
A 1D NumPy array representing the weights allocated to each asset.
Returns
-------
pd.Series
A pandas Series containing the calculated portfolio returns for each period.
Examples
--------
>>> state = create_initial_state(num_assets=2)
>>> state['asset_returns'] = pd.DataFrame({'Asset_1': [0.01, 0.005], 'Asset_2': [0.002, 0.015]})
>>> weights = np.array([0.5, 0.5])
>>> portfolio_rets = calculate_portfolio_returns(state, state['asset_returns'], weights)
>>> print(portfolio_rets.iloc[0])
0.006
"""
log.info("Calculating portfolio returns.")
if not np.isclose(np.sum(weights), 1.0):
log.warning(f"Weights do not sum to 1.0. Sum is {np.sum(weights)}. Normalizing weights.")
weights = weights / np.sum(weights)
portfolio_returns = asset_returns.dot(weights)
log.debug(f"Portfolio returns calculated. First 5 values:\n{portfolio_returns.head()}")
return portfolio_returnsFunction Name: simulate_portfolio_performance
This function simulates the performance of a portfolio over time, given a series of asset returns and a fixed allocation strategy (represented by weights). It calculates the cumulative wealth over the investment horizon, starting with an initial capital.
Parameters:
state(dict): The current state dictionary, which should containasset_returns.weights(np.ndarray): A 1D NumPy array representing the constant weights allocated to each asset throughout the simulation. The sum of weights should be 1.initial_capital(float, optional): The starting capital for the simulation. Defaults to 1000.0.
Returns:
dict: The updated state dictionary, including theportfolio_returns(as apd.Series) andportfolio_value(as apd.Series) over the simulation period.
def simulate_portfolio_performance(
state: Dict,
weights: np.ndarray,
initial_capital: float = 1000.0
) -> Dict:
"""
Simulates the performance of a portfolio over time with fixed weights.
Parameters
----------
state : Dict
The current state dictionary, must contain 'asset_returns'.
weights : np.ndarray
A 1D NumPy array representing the constant weights allocated to each asset.
initial_capital : float, optional
The starting capital for the simulation, defaults to 1000.0.
Returns
-------
Dict
The updated state dictionary, including 'portfolio_returns' and 'portfolio_value'.
Examples
--------
>>> state = create_initial_state(num_assets=2)
>>> state = generate_asset_returns(state, num_periods=10)
>>> weights = np.array([0.5, 0.5])
>>> updated_state = simulate_portfolio_performance(state, weights)
>>> print(updated_state['portfolio_value'].iloc[-1])
# Expected output: (approximately 1000 * (1 + sum(daily_portfolio_returns))) after 10 days
"""
log.info("Simulating portfolio performance with fixed weights.")
if 'asset_returns' not in state:
log.error("asset_returns not found in state. Please generate asset returns first.")
raise ValueError("asset_returns not found in state.")
portfolio_returns = calculate_portfolio_returns(state, state['asset_returns'], weights)
cumulative_returns = (1 + portfolio_returns).cumprod()
portfolio_value = initial_capital * cumulative_returns
state['portfolio_returns'] = portfolio_returns
state['portfolio_value'] = portfolio_value
log.info("Portfolio simulation complete.")
log.debug(f"Final portfolio value: {portfolio_value.iloc[-1]:.2f}")
return stateFunction Name: allocate_equal_weight
This function implements a simple equal-weight capital allocation strategy. It assigns an equal proportion of capital to each available asset or strategy. This serves as a baseline for comparison against more complex allocation methods.
Parameters:
state(dict): The current state dictionary, containingnum_assets.
Returns:
dict: The updated state dictionary with thecurrent_weightsattribute set to equal weights.
def allocate_equal_weight(
state: Dict
) -> Dict:
"""
Allocates capital equally among all assets or strategies.
Parameters
----------
state : Dict
The current state dictionary, must contain 'num_assets'.
Returns
-------
Dict
The updated state dictionary with 'current_weights' set to equal weights.
Examples
--------
>>> state = create_initial_state(num_assets=3)
>>> state = allocate_equal_weight(state)
>>> print(state['current_weights'])
[0.33333333 0.33333333 0.33333333]
"""
log.info("Allocating capital using an equal-weight strategy.")
num_assets = state['num_assets']
weights = np.array([1.0 / num_assets] * num_assets)
state['current_weights'] = weights
log.debug(f"Equal weights assigned: {weights}")
return stateFunction Name: calculate_portfolio_statistics
This function calculates the expected annual returns, annual standard deviations, and the annual covariance matrix of asset returns. These statistics are fundamental inputs for many portfolio optimization techniques, including Mean-Variance Optimization. The daily statistics are annualized using the provided factor.
Parameters:
state(dict): The current state dictionary, which must containasset_returns.annualization_factor(int, optional): The factor to annualize daily statistics (e.g., 252 for trading days). Defaults to 252.
Returns:
dict: The updated state dictionary, includingexpected_annual_returns,annual_stds, andannual_cov_matrix.
def calculate_portfolio_statistics(
state: Dict,
annualization_factor: int = 252
) -> Dict:
"""
Calculates the expected annual returns, annual standard deviations, and
the annual covariance matrix of asset returns.
Parameters
----------
state : Dict
The current state dictionary, must contain 'asset_returns'.
annualization_factor : int, optional
The factor to annualize daily statistics, defaults to 252.
Returns
-------
Dict
The updated state dictionary, including 'expected_annual_returns',
'annual_stds', and 'annual_cov_matrix'.
Examples
--------
>>> state = create_initial_state(num_assets=2)
>>> state = generate_asset_returns(state, num_periods=252)
>>> updated_state = calculate_portfolio_statistics(state)
>>> print(updated_state['annual_cov_matrix'].shape)
(2, 2)
"""
log.info("Calculating portfolio statistics (expected returns, stds, covariance matrix).")
if 'asset_returns' not in state or state['asset_returns'].empty:
log.error("asset_returns not found or is empty in state.")
raise ValueError("asset_returns must be present and non-empty in state.")
asset_returns = state['asset_returns']
# Expected daily returns and daily standard deviations
daily_mean_returns = asset_returns.mean()
daily_stds = asset_returns.std()
daily_cov_matrix = asset_returns.cov()
# Annualize
expected_annual_returns = daily_mean_returns * annualization_factor
annual_stds = daily_stds * np.sqrt(annualization_factor)
annual_cov_matrix = daily_cov_matrix * annualization_factor
state['expected_annual_returns'] = expected_annual_returns.values
state['annual_stds'] = annual_stds.values
state['annual_cov_matrix'] = annual_cov_matrix.values
log.info("Portfolio statistics calculated.")
log.debug(f"Expected Annual Returns: {expected_annual_returns}")
log.debug(f"Annual Covariance Matrix head:\n{annual_cov_matrix.head()}")
return stateFunction Name: allocate_mean_variance_optimization
This function implements the Mean-Variance Optimization (MVO) strategy to find optimal portfolio weights. It leverages cvxpy to solve a quadratic programming problem, maximizing the Sharpe ratio for a target return or minimizing portfolio variance for a given risk level. Constraints ensure weights sum to one and are non-negative.
Parameters:
state(dict): The current state dictionary, which must containexpected_annual_returnsandannual_cov_matrix.target_return(float, optional): The target annual return for the portfolio. If None, the function will try to maximize Sharpe Ratio (or minimize variance ifmax_sharpeis True).max_sharpe(bool, optional): If True, optimizes for the maximum Sharpe ratio. If False andtarget_returnis None, it minimizes portfolio variance. Defaults to True.
Returns:
dict: The updated state dictionary withcurrent_weightsset to the MVO-optimized weights.
def allocate_mean_variance_optimization(
state: Dict,
target_return: float = None,
max_sharpe: bool = True
) -> Dict:
"""
Allocates capital using Mean-Variance Optimization (MVO).
Parameters
----------
state : Dict
The current state dictionary, must contain 'expected_annual_returns'
and 'annual_cov_matrix'.
target_return : float, optional
The target annual return for the portfolio. If None, optimizes for max Sharpe.
max_sharpe : bool, optional
If True, optimizes for the maximum Sharpe ratio. If False and target_return is None,
it minimizes portfolio variance. Defaults to True.
Returns
-------
Dict
The updated state dictionary with 'current_weights' set to MVO-optimized weights.
Examples
--------
>>> state = create_initial_state(num_assets=3)
>>> state = generate_asset_returns(state, num_periods=252)
>>> state = calculate_portfolio_statistics(state)
>>> state = allocate_mean_variance_optimization(state, max_sharpe=True)
>>> print(state['current_weights'].sum())
1.0
"""
log.info("Performing Mean-Variance Optimization (MVO).")
if 'expected_annual_returns' not in state or 'annual_cov_matrix' not in state:
log.error("Missing expected_annual_returns or annual_cov_matrix in state.")
raise ValueError("State must contain expected_annual_returns and annual_cov_matrix.")
num_assets = state['num_assets']
expected_returns = state['expected_annual_returns']
cov_matrix = state['annual_cov_matrix']
# Define variables
weights = cp.Variable(num_assets)
# Define objective
portfolio_return = expected_returns @ weights
portfolio_variance = cp.quad_form(weights, cov_matrix)
# Define constraints
constraints = [cp.sum(weights) == 1, weights >= 0] # Sum of weights = 1, no short-selling
if max_sharpe:
log.info("Optimizing for maximum Sharpe Ratio.")
# To maximize Sharpe ratio, we maximize (portfolio_return - risk_free_rate) / sqrt(portfolio_variance)
# This is a fractional programming problem. A common approach is to transform it.
# This transformation is valid when (return - rf) is positive.
# Let y = weights / (portfolio_return - risk_free_rate)
# Maximize (portfolio_return - risk_free_rate) / sqrt(portfolio_variance)
# is equivalent to minimizing portfolio_variance for a given (portfolio_return - risk_free_rate) = 1 (or any positive constant)
# and then scale weights.
# However, a simpler approach is to optimize for a target volatility and then check the return, or vice versa, to build the efficient frontier.
# For directly maximizing Sharpe, we can use the method described by Robert Merton (1972) or a numerical solver. cvxpy can handle this directly with quadratic forms.
# Standard approach for max Sharpe when risk-free rate is zero or incorporated:
# Maximize objective: portfolio_return / sqrt(portfolio_variance)
# This is equivalent to minimizing variance for target_return = R, and then scaling.
# A common way with cvxpy is to solve a series of variance minimization problems to find the efficient frontier
# and then pick the one with the highest sharpe.
# More direct way: find weights that maximize (portfolio_return - rf) / sqrt(portfolio_variance)
# If we assume risk-free rate is 0 for simplicity, then maximize (portfolio_return / sqrt(portfolio_variance))
# This can be formulated by introducing an auxiliary variable k = 1 / (return_portfolio - rf)
# and then maximizing (return_portfolio - rf) * k subject to (weights.T @ cov @ weights) * k**2 <= 1.
# A simpler form is to solve: max (r_p - rf) subject to var_p = X, and repeat for X, or directly find tangent portfolio.
# For simplicity and direct CVXPY usage, we'll maximize return for a given level of risk (or minimize risk for a given return) and select from efficient frontier.
# The problem formulation directly maximizes the quadratic utility function `return - lambda * variance` where lambda is risk aversion.
# For max Sharpe, we are looking for the tangent portfolio. We can achieve this by solving a problem with a 'risk aversion' parameter.
# This formulation is standard in many MVO libraries.
risk_free_rate = 0.01 # Assuming an annualized risk-free rate
objective = cp.Maximize(portfolio_return - risk_free_rate - 0.5 * portfolio_variance)
problem = cp.Problem(objective, constraints)
problem.solve()
elif target_return is not None:
log.info(f"Optimizing for target return: {target_return:.4f}.")
constraints.append(portfolio_return >= target_return)
objective = cp.Minimize(portfolio_variance)
problem = cp.Problem(objective, constraints)
problem.solve()
else:
log.info("Optimizing for minimum variance.")
objective = cp.Minimize(portfolio_variance)
problem = cp.Problem(objective, constraints)
problem.solve()
if problem.status == cp.OPTIMAL or problem.status == cp.OPTIMAL_INACCURATE:
state['current_weights'] = weights.value
log.debug(f"MVO optimized weights: {weights.value}")
else:
log.warning("MVO problem did not converge to an optimal solution. Keeping previous weights.")
log.warning(f"Problem status: {problem.status}")
if 'current_weights' not in state:
state['current_weights'] = np.array([1.0 / num_assets] * num_assets)
# Ensure weights are non-negative and sum to 1 due to potential floating point inaccuracies
state['current_weights'][state['current_weights'] < 0] = 0
state['current_weights'] = state['current_weights'] / np.sum(state['current_weights'])
return stateFunction Name: exponential_backoff_retry
This is a utility function that implements an exponential backoff retry mechanism for functions that might fail due to transient errors (e.g., API rate limits, network issues). It retries the provided function with increasing delays, preventing hammering the service and allowing it to recover. It includes optional random jitter to further spread out retry attempts.
Parameters:
state(dict): The current state dictionary. (Used for logging, but not for direct calculation here).func(callable): The function to be executed and retried.args(tuple, optional): Positional arguments to pass tofunc.kwargs(dict, optional): Keyword arguments to pass tofunc.max_retries(int, optional): The maximum number of retry attempts. Defaults to 5.base_delay(float, optional): The initial delay in seconds before the first retry. Defaults to 1.0.max_delay(float, optional): The maximum delay between retries. Defaults to 60.0.add_jitter(bool, optional): If True, adds random jitter to the delay. Defaults to True.
Returns:
Any: The result of thefuncif successful.
Raises:
Exception: If the function fails aftermax_retries.
import typing
def exponential_backoff_retry(
state: typing.Dict,
func: callable,
*args,
max_retries: int = 5,
base_delay: float = 1.0,
max_delay: float = 60.0,
add_jitter: bool = True,
**kwargs
) -> typing.Any:
"""
Implements an exponential backoff retry mechanism for a given function.
Parameters
----------
state : typing.Dict
The current state dictionary.
func : callable
The function to be executed and retried.
*args
Positional arguments to pass to `func`.
max_retries : int, optional
The maximum number of retry attempts, defaults to 5.
base_delay : float, optional
The initial delay in seconds before the first retry, defaults to 1.0.
max_delay : float, optional
The maximum delay between retries, defaults to 60.0.
add_jitter : bool, optional
If True, adds random jitter to the delay, defaults to True.
**kwargs
Keyword arguments to pass to `func`.
Returns
-------
typing.Any
The result of the `func` if successful.
Raises
-------
Exception
If the function fails after `max_retries`.
Examples
--------
>>> state = create_initial_state()
>>> def flaky_function(x):
... if random.random() < 0.7: # 70% chance of failure
... raise ValueError("Simulated transient error")
... return x * 2
>>> # result = exponential_backoff_retry(state, flaky_function, 10, max_retries=3)
>>> # print(result)
"""
for i in range(max_retries):
try:
log.debug(f"Attempt {i + 1}/{max_retries} for function '{func.__name__}'.")
return func(*args, **kwargs)
except Exception as e:
log.warning(f"Attempt {i + 1} failed for '{func.__name__}': {e}")
if i == max_retries - 1:
log.error(f"Max retries ({max_retries}) exceeded for '{func.__name__}'.")
raise
delay = min(base_delay * (2 ** i), max_delay)
if add_jitter:
jitter = random.uniform(0, delay * 0.1) # Add up to 10% jitter
delay += jitter
log.debug(f"Adding {jitter:.2f}s jitter.")
log.info(f"Retrying '{func.__name__}' in {delay:.2f} seconds...")
time.sleep(delay)
state['backoff_attempts'] += 1
return None # Should not be reached due to raise aboveFunction Name: calculate_risk_contributions
This helper function computes the risk contribution of each asset to the total portfolio volatility. Risk contribution is a crucial metric for strategies like Risk Parity, where the goal is to equalize the risk exposure from each component. It requires the portfolio weights and the asset covariance matrix.
Parameters:
state(dict): The current state dictionary. (Used for logging, but not for direct calculation here).weights(np.ndarray): A 1D NumPy array of asset weights.cov_matrix(np.ndarray): The covariance matrix of asset returns.
Returns:
np.ndarray: A 1D NumPy array representing the risk contribution of each asset to the total portfolio volatility.
def calculate_risk_contributions(
state: Dict,
weights: np.ndarray,
cov_matrix: np.ndarray
) -> np.ndarray:
"""
Calculates the risk contribution of each asset to the total portfolio volatility.
Parameters
----------
state : Dict
The current state dictionary.
weights : np.ndarray
A 1D NumPy array of asset weights.
cov_matrix : np.ndarray
The covariance matrix of asset returns.
Returns
-------
np.ndarray
A 1D NumPy array representing the risk contribution of each asset.
Examples
--------
>>> state = create_initial_state(num_assets=2)
>>> weights = np.array([0.5, 0.5])
>>> cov_matrix = np.array([[0.01, 0.005], [0.005, 0.02]])
>>> risk_contribs = calculate_risk_contributions(state, weights, cov_matrix)
>>> print(risk_contribs.sum())
0.15811388300841897
"""
log.debug("Calculating risk contributions.")
portfolio_variance = weights.T @ cov_matrix @ weights
portfolio_std = np.sqrt(portfolio_variance)
# Marginal Contribution to Risk (MCR)
marginal_contribution_to_risk = (cov_matrix @ weights) / portfolio_std
# Component Contribution to Risk (CCR) or Risk Contribution (RC)
risk_contributions = weights * marginal_contribution_to_risk
log.debug(f"Risk contributions calculated: {risk_contributions}")
return risk_contributionsFunction Name: allocate_risk_parity
This function implements a Risk Parity capital allocation strategy. The goal of Risk Parity is to distribute risk equally among the portfolio's assets or strategies, rather than capital. This is achieved by iteratively adjusting weights until each asset contributes the same amount to the total portfolio volatility. The optimization problem is solved using numerical minimization.
Parameters:
state(dict): The current state dictionary, which must containannual_cov_matrix.
Returns:
dict: The updated state dictionary withcurrent_weightsset to the Risk Parity optimized weights.
def allocate_risk_parity(
state: Dict
) -> Dict:
"""
Allocates capital using a Risk Parity strategy.
The goal is for each asset to contribute equally to the total portfolio risk.
Parameters
----------
state : Dict
The current state dictionary, must contain 'annual_cov_matrix'.
Returns
-------
Dict
The updated state dictionary with 'current_weights' set to Risk Parity optimized weights.
Examples
--------
>>> state = create_initial_state(num_assets=3)
>>> state = generate_asset_returns(state, num_periods=252)
>>> state = calculate_portfolio_statistics(state)
>>> state = allocate_risk_parity(state)
>>> print(state['current_weights'].sum())
1.0
"""
log.info("Performing Risk Parity allocation.")
if 'annual_cov_matrix' not in state:
log.error("Missing annual_cov_matrix in state.")
raise ValueError("State must contain annual_cov_matrix.")
num_assets = state['num_assets']
cov_matrix = state['annual_cov_matrix']
# Objective function for risk parity: minimize the sum of squared differences
# between each asset's risk contribution and the average risk contribution.
def risk_parity_objective(weights):
weights = np.array(weights)
if not np.isclose(weights.sum(), 1.0) or np.any(weights < 0):
return 1e10 # Penalize invalid weights heavily
risk_contributions = calculate_risk_contributions(state, weights, cov_matrix)
target_risk_contribution = np.sum(risk_contributions) / num_assets
# Minimize sum of squared differences from target risk contribution
return np.sum((risk_contributions - target_risk_contribution)**2)
# Constraints: weights must sum to 1, and be non-negative
constraints = [{'type': 'eq', 'fun': lambda w: np.sum(w) - 1}]
bounds = tuple((0.0, 1.0) for _ in range(num_assets))
# Initial guess: equal weights
initial_weights = np.array([1.0 / num_assets] * num_assets)
result = minimize(risk_parity_objective, initial_weights, method='SLSQP', bounds=bounds, constraints=constraints)
if result.success:
state['current_weights'] = result.x
log.debug(f"Risk Parity optimized weights: {result.x}")
else:
log.warning("Risk Parity optimization failed. Keeping previous weights.")
log.warning(f"Optimization status: {result.message}")
if 'current_weights' not in state:
state['current_weights'] = np.array([1.0 / num_assets] * num_assets)
# Ensure weights are non-negative and sum to 1 due to potential floating point inaccuracies
state['current_weights'][state['current_weights'] < 0] = 0
state['current_weights'] = state['current_weights'] / np.sum(state['current_weights'])
return stateFunction Name: create_investment_portfolio
This function retrieves historical stock price data for a given list of tickers using yfinance. It then calculates daily returns from the adjusted close prices. This allows for real-world application and backtesting of the capital allocation strategies developed in this notebook.
Parameters:
state(dict): The current state dictionary.tickers(List[str]): A list of stock ticker symbols (e.g., ['AAPL', 'MSFT']).start_date(str): The start date for fetching data in 'YYYY-MM-DD' format.end_date(str): The end date for fetching data in 'YYYY-MM-DD' format.
Returns:
dict: The updated state dictionary, including a DataFrame ofasset_returnsfrom real stock data, and updatedasset_namesandnum_assets.
import typing
def create_investment_portfolio(
state: typing.Dict,
tickers: typing.List[str],
start_date: str,
end_date: str
) -> typing.Dict:
"""
Retrieves historical stock price data and calculates daily returns for a given set of tickers.
Parameters
----------
state : typing.Dict
The current state dictionary.
tickers : typing.List[str]
A list of stock ticker symbols.
start_date : str
The start date for fetching data in 'YYYY-MM-DD' format.
end_date : str
The end date for fetching data in 'YYYY-MM-DD' format.
Returns
-------
typing.Dict
The updated state dictionary, including 'asset_returns' from real stock data,
and updated 'asset_names' and 'num_assets'.
Examples
--------
>>> state = create_initial_state()
>>> tickers = ['AAPL', 'MSFT']
>>> state = create_investment_portfolio(state, tickers, '2020-01-01', '2021-01-01')
>>> print(state['asset_returns'].head())
"""
log.info(f"Fetching historical data for tickers: {tickers} from {start_date} to {end_date}.")
try:
# Using exponential backoff for yfinance download to handle potential rate limits or transient errors
data = exponential_backoff_retry(
state,
yf.download,
tickers=tickers,
start=start_date,
end=end_date,
interval='1d',
progress=False,
max_retries=5,
base_delay=5.0
)
if data.empty:
log.warning(f"No data fetched for tickers: {tickers}. Proceeding to generate mock data.")
# Trigger the except block to handle mock data generation
raise ValueError("yf.download returned empty data.")
# If only one ticker, yfinance returns a Series, convert to DataFrame
if len(tickers) == 1:
adj_close_prices = pd.DataFrame(data['Adj Close'])
else:
adj_close_prices = data['Adj Close']
# Calculate daily returns
asset_returns = adj_close_prices.pct_change().dropna()
asset_returns.columns = tickers # Ensure columns are ticker names
state['asset_returns'] = asset_returns
state['asset_names'] = list(tickers)
state['num_assets'] = len(tickers)
log.info(f"Successfully fetched and processed data for {len(tickers)} assets.")
log.debug(f"Real asset returns head:\n{state['asset_returns'].head()}")
except Exception as e:
log.error(f"Failed to fetch real stock data for tickers {tickers}: {e}. Generating mock data instead.")
# Calculate approximate num_periods from start_date and end_date
start_dt = pd.to_datetime(start_date)
end_dt = pd.to_datetime(end_date)
num_periods = (end_dt - start_dt).days
if num_periods <= 0:
num_periods = 252 # Default to one trading year if date range is invalid or too small
# Update state with mock asset names and count for generate_asset_returns
state['num_assets'] = len(tickers)
state['asset_names'] = [f'Mock_{ticker}' for ticker in tickers]
# Generate mock asset returns
state = generate_asset_returns(
state,
num_periods=num_periods,
mean_returns=np.random.rand(len(tickers)) * 0.0005, # Small positive mean returns
stds=np.random.rand(len(tickers)) * 0.01, # Reasonable daily stds
add_jitter=True
)
log.warning("Using mock data for real-world demonstration due to previous data fetching issues.")
return stateFunction Name: plot_portfolio_performance
This function visualizes the portfolio's value and returns over time. It creates two subplots: one showing the cumulative portfolio value (equity curve) and another displaying the daily portfolio returns. This allows for a quick assessment of the strategy's historical performance.
Parameters:
state(dict): The current state dictionary, which must containportfolio_valueandportfolio_returns.title(str, optional): A title for the plot. Defaults to 'Portfolio Performance'.
Returns:
None: Displays the plot directly.
def plot_portfolio_performance(
state: Dict,
title: str = 'Portfolio Performance'
) -> None:
"""
Visualizes the portfolio's value and returns over time.
Parameters
----------
state : Dict
The current state dictionary, must contain 'portfolio_value' and 'portfolio_returns'.
title : str, optional
A title for the plot, defaults to 'Portfolio Performance'.
Returns
-------
None
Displays the plot directly.
Examples
--------
>>> state = create_initial_state()
>>> state = generate_asset_returns(state, num_periods=252)
>>> weights = np.array([0.2, 0.2, 0.2, 0.2, 0.2])
>>> state = simulate_portfolio_performance(state, weights)
>>> plot_portfolio_performance(state, title='Equal Weight Portfolio Performance')
"""
log.info(f"Plotting portfolio performance: {title}.")
if 'portfolio_value' not in state or 'portfolio_returns' not in state:
log.error("Missing 'portfolio_value' or 'portfolio_returns' in state for plotting.")
return
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10), sharex=True)
# Plot Portfolio Value
ax1.plot(state['portfolio_value'], label='Portfolio Value', color='blue')
ax1.set_title(f'{title} - Portfolio Value Over Time')
ax1.set_ylabel('Portfolio Value')
ax1.legend()
ax1.grid(True)
# Plot Portfolio Returns
ax2.plot(state['portfolio_returns'], label='Daily Returns', color='green', alpha=0.7)
ax2.set_title(f'{title} - Daily Portfolio Returns')
ax2.set_xlabel('Time (Periods)')
ax2.set_ylabel('Daily Return')
ax2.legend()
ax2.grid(True)
plt.tight_layout()
plt.show()
log.info("Portfolio performance plot displayed.")Function Name: plot_asset_returns_distribution
This function visualizes the distribution of individual asset returns using histograms and kernel density estimates (KDEs). It also includes a box plot for a quick comparison of central tendency and spread across assets. This helps in understanding the risk and return characteristics of the assets comprising the portfolio.
Parameters:
state(dict): The current state dictionary, which must containasset_returns.title(str, optional): A title for the plot. Defaults to 'Asset Returns Distribution'.
Returns:
None: Displays the plot directly.
def plot_asset_returns_distribution(
state: Dict,
title: str = 'Asset Returns Distribution'
) -> None:
"""
Visualizes the distribution of individual asset returns.
Parameters
----------
state : Dict
The current state dictionary, must contain 'asset_returns'.
title : str, optional
A title for the plot, defaults to 'Asset Returns Distribution'.
Returns
-------
None
Displays the plot directly.
Examples
--------
>>> state = create_initial_state()
>>> state = generate_asset_returns(state, num_periods=252)
>>> plot_asset_returns_distribution(state)
"""
log.info(f"Plotting asset returns distribution: {title}.")
if 'asset_returns' not in state or state['asset_returns'].empty:
log.error("Missing or empty 'asset_returns' in state for plotting.")
return
asset_returns = state['asset_returns']
fig, axes = plt.subplots(len(asset_returns.columns), 1, figsize=(10, 4 * len(asset_returns.columns)), sharex=True)
if len(asset_returns.columns) == 1: # Handle single subplot case
axes = [axes]
for i, col in enumerate(asset_returns.columns):
sns.histplot(asset_returns[col], kde=True, ax=axes[i], bins=50, color='skyblue')
axes[i].set_title(f'Distribution of {col} Returns')
axes[i].set_xlabel('Daily Return')
axes[i].set_ylabel('Frequency')
axes[i].grid(True)
plt.suptitle(title, y=1.02, fontsize=16)
plt.tight_layout()
plt.show()
# Also plot box plot for comparison
plt.figure(figsize=(12, 6))
sns.boxplot(data=asset_returns, palette='viridis')
plt.title(f'{title} - Asset Return Comparison')
plt.xlabel('Asset')
plt.ylabel('Daily Return')
plt.grid(True)
plt.tight_layout()
plt.show()
log.info("Asset returns distribution plots displayed.")Function Name: plot_efficient_frontier
This function visualizes the Efficient Frontier, which represents the set of optimal portfolios that offer the highest expected return for a given level of risk (standard deviation), or the lowest risk for a given expected return. It also plots individual assets and optionally marks the maximum Sharpe ratio portfolio and the minimum volatility portfolio.
Parameters:
state(dict): The current state dictionary, which must containexpected_annual_returns,annual_cov_matrix, andannual_stds.num_portfolios(int, optional): The number of random portfolios to generate for visualization. Defaults to 10000.risk_free_rate(float, optional): The risk-free rate used for Sharpe ratio calculation. Defaults to 0.01.
Returns:
None: Displays the plot directly.
def plot_efficient_frontier(
state: Dict,
num_portfolios: int = 10000,
risk_free_rate: float = 0.01
) -> None:
"""
Visualizes the Efficient Frontier, plotting optimal portfolios based on risk and return.
Parameters
----------
state : Dict
The current state dictionary, must contain 'expected_annual_returns',
'annual_cov_matrix', and 'annual_stds'.
num_portfolios : int, optional
The number of random portfolios to generate for visualization, defaults to 10000.
risk_free_rate : float, optional
The risk-free rate used for Sharpe ratio calculation, defaults to 0.01.
Returns
-------
None
Displays the plot directly.
Examples
--------
>>> state = create_initial_state(num_assets=5)
>>> state = generate_asset_returns(state, num_periods=252)
>>> state = calculate_portfolio_statistics(state)
>>> plot_efficient_frontier(state)
"""
log.info("Plotting Efficient Frontier.")
if 'expected_annual_returns' not in state or 'annual_cov_matrix' not in state or 'annual_stds' not in state:
log.error("Missing required statistics in state for plotting Efficient Frontier.")
return
expected_returns = state['expected_annual_returns']
cov_matrix = state['annual_cov_matrix']
num_assets = state['num_assets']
portfolio_returns = []
portfolio_volatility = []
portfolio_weights = []
for _ in range(num_portfolios):
weights = np.random.random(num_assets)
weights /= np.sum(weights)
portfolio_weights.append(weights)
ret = np.sum(weights * expected_returns)
vol = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))
portfolio_returns.append(ret)
portfolio_volatility.append(vol)
# Convert to numpy arrays for easier manipulation
portfolio_returns = np.array(portfolio_returns)
portfolio_volatility = np.array(portfolio_volatility)
# Calculate Sharpe Ratios
sharpe_ratios = (portfolio_returns - risk_free_rate) / portfolio_volatility
# Find the portfolio with the maximum Sharpe Ratio
max_sharpe_idx = np.argmax(sharpe_ratios)
max_sharpe_return = portfolio_returns[max_sharpe_idx]
max_sharpe_volatility = portfolio_volatility[max_sharpe_idx]
# Find the portfolio with the minimum volatility
min_vol_idx = np.argmin(portfolio_volatility)
min_vol_return = portfolio_returns[min_vol_idx]
min_vol_volatility = portfolio_volatility[min_vol_idx]
plt.figure(figsize=(12, 8))
plt.scatter(portfolio_volatility, portfolio_returns, c=sharpe_ratios, cmap='viridis', marker='o', alpha=0.5)
plt.title('Efficient Frontier with Simulated Portfolios')
plt.xlabel('Annualized Volatility (Standard Deviation)')
plt.ylabel('Annualized Return')
plt.colorbar(label='Sharpe Ratio')
# Plot individual assets
asset_volatilities = state['annual_stds']
plt.scatter(asset_volatilities, expected_returns, marker='X', color='red', s=100, label='Individual Assets')
for i, txt in enumerate(state['asset_names']):
plt.annotate(txt, (asset_volatilities[i], expected_returns[i]), xytext=(5, -5), textcoords='offset points')
# Highlight Max Sharpe and Min Volatility Portfolios
plt.scatter(max_sharpe_volatility, max_sharpe_return, marker='*', color='gold', s=300, label='Max Sharpe Ratio Portfolio')
plt.scatter(min_vol_volatility, min_vol_return, marker='P', color='blue', s=300, label='Minimum Volatility Portfolio')
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.show()
log.info("Efficient Frontier plot displayed.")Function Name: plot_risk_contributions
This function visualizes the risk contribution of each asset to the total portfolio risk. This is particularly useful for understanding Risk Parity portfolios, where the goal is often to equalize these contributions. The visualization typically uses a bar chart to show the percentage contribution of each asset.
Parameters:
state(dict): The current state dictionary, which must containannual_cov_matrix,current_weights, andasset_names.title(str, optional): A title for the plot. Defaults to 'Asset Risk Contributions'.
Returns:
None: Displays the plot directly.
def plot_risk_contributions(
state: Dict,
title: str = 'Asset Risk Contributions'
) -> None:
"""
Visualizes the risk contribution of each asset to the total portfolio risk.
Parameters
----------
state : Dict
The current state dictionary, must contain 'annual_cov_matrix', 'current_weights',
and 'asset_names'.
title : str, optional
A title for the plot, defaults to 'Asset Risk Contributions'.
Returns
-------
None
Displays the plot directly.
Examples
--------
>>> state = create_initial_state(num_assets=3)
>>> state = generate_asset_returns(state, num_periods=252)
>>> state = calculate_portfolio_statistics(state)
>>> state = allocate_risk_parity(state) # Assuming Risk Parity weights are calculated
>>> plot_risk_contributions(state)
"""
log.info(f"Plotting risk contributions: {title}.")
if 'annual_cov_matrix' not in state or 'current_weights' not in state or 'asset_names' not in state:
log.error("Missing required data in state for plotting risk contributions.")
return
weights = state['current_weights']
cov_matrix = state['annual_cov_matrix']
asset_names = state['asset_names']
if np.sum(weights) == 0: # Avoid division by zero if all weights are zero
log.warning("All weights are zero, cannot calculate risk contributions. Displaying empty plot.")
risk_contributions_abs = np.zeros(len(asset_names))
else:
risk_contributions_abs = calculate_risk_contributions(state, weights, cov_matrix)
# Normalize to percentage for easier interpretation
if np.sum(risk_contributions_abs) != 0:
risk_contributions_percent = (risk_contributions_abs / np.sum(risk_contributions_abs)) * 100
else:
risk_contributions_percent = np.zeros(len(asset_names))
plt.figure(figsize=(10, 6))
sns.barplot(x=asset_names, y=risk_contributions_percent, palette='viridis')
plt.title(title)
plt.xlabel('Asset')
plt.ylabel('Risk Contribution (%)')
plt.grid(axis='y')
plt.tight_layout()
plt.show()
log.info("Risk contributions plot displayed.")Function Name: plot_asset_correlation_heatmap
This function visualizes the correlation matrix of asset returns using a heatmap. Understanding asset correlations is vital for portfolio diversification and risk management. A heatmap provides a clear, intuitive way to see which assets move together (positive correlation) and which move in opposite directions (negative correlation).
Parameters:
state(dict): The current state dictionary, which must containasset_returns.title(str, optional): A title for the plot. Defaults to 'Asset Correlation Heatmap'.
Returns:
None: Displays the plot directly.
def plot_asset_correlation_heatmap(
state: Dict,
title: str = 'Asset Correlation Heatmap'
) -> None:
"""
Visualizes the correlation matrix of asset returns using a heatmap.
Parameters
----------
state : Dict
The current state dictionary, must contain 'asset_returns'.
title : str, optional
A title for the plot, defaults to 'Asset Correlation Heatmap'.
Returns
-------
None
Displays the plot directly.
Examples
--------
>>> state = create_initial_state(num_assets=5)
>>> state = generate_asset_returns(state, num_periods=252)
>>> plot_asset_correlation_heatmap(state)
"""
log.info(f"Plotting asset correlation heatmap: {title}.")
if 'asset_returns' not in state or state['asset_returns'].empty:
log.error("Missing or empty 'asset_returns' in state for plotting correlation heatmap.")
return
asset_returns = state['asset_returns']
correlation_matrix = asset_returns.corr()
plt.figure(figsize=(10, 8))
sns.heatmap(
correlation_matrix,
annot=True,
cmap='coolwarm',
fmt=".2f",
linewidths=.5,
cbar_kws={'label': 'Correlation Coefficient'}
)
plt.title(title)
plt.xlabel('Assets')
plt.ylabel('Assets')
plt.tight_layout()
plt.show()
log.info("Asset correlation heatmap displayed.")Demonstration and Visualization
This section demonstrates the usage of the capital allocation functions and visualizes their results. We will:
- Generate simulated asset returns for a set of hypothetical strategies.
- Apply different allocation strategies: Equal Weight, Mean-Variance Optimization (Max Sharpe), and Risk Parity.
- Simulate portfolio performance for each strategy.
- Calculate and display performance metrics for comparison.
- Visualize portfolio performance (equity curves, daily returns).
- Visualize asset characteristics (returns distribution, correlation).
- Plot the Efficient Frontier to illustrate MVO.
- Plot Risk Contributions to show Risk Parity in action.
First, let's create an initial state and generate some simulated data.
# 1. Initialize state and generate simulated asset returns
log.info("--- Demonstration: Initializing state and generating simulated asset returns ---")
state_simulated = create_initial_state(num_assets=5, investment_horizon=500, random_seed=42)
# Define custom mean returns and stds for more interesting simulation
sim_mean_returns = np.array([0.0003, 0.0005, 0.0004, 0.0002, 0.0006])
sim_stds = np.array([0.008, 0.012, 0.010, 0.007, 0.015])
# Create a more realistic correlation matrix
sim_corr_matrix = np.array([
[1.0, 0.6, 0.4, 0.2, 0.1],
[0.6, 1.0, 0.7, 0.3, 0.2],
[0.4, 0.7, 1.0, 0.5, 0.3],
[0.2, 0.3, 0.5, 1.0, 0.8],
[0.1, 0.2, 0.3, 0.8, 1.0]
])
state_simulated = generate_asset_returns(
state_simulated,
num_periods=state_simulated['investment_horizon'],
mean_returns=sim_mean_returns,
stds=sim_stds,
corr_matrix=sim_corr_matrix,
add_jitter=True
)
# Display head of simulated asset returns
log.info("Simulated Asset Returns (first 5 periods):")
display(state_simulated['asset_returns'].head())
# Visualize distribution of simulated asset returns
plot_asset_returns_distribution(state_simulated, title='Simulated Asset Returns Distribution')
# Visualize correlation of simulated asset returns
plot_asset_correlation_heatmap(state_simulated, title='Simulated Asset Returns Correlation Heatmap')
# Calculate portfolio statistics needed for MVO and Risk Parity
state_simulated = calculate_portfolio_statistics(state_simulated)
log.info("Expected Annual Returns:")
display(pd.DataFrame({
'Asset': state_simulated['asset_names'],
'Expected Annual Return': state_simulated['expected_annual_returns']
}))
log.info("Annual Standard Deviations:")
display(pd.DataFrame({
'Asset': state_simulated['asset_names'],
'Annual Std Dev': state_simulated['annual_stds']
}))
log.info("Annual Covariance Matrix:")
display(pd.DataFrame(
state_simulated['annual_cov_matrix'],
index=state_simulated['asset_names'],
columns=state_simulated['asset_names']
))2026-06-10 10:12:15,609 - CapitalAllocationNotebook - INFO - --- Demonstration: Initializing state and generating simulated asset returns --- INFO:CapitalAllocationNotebook:--- Demonstration: Initializing state and generating simulated asset returns --- 2026-06-10 10:12:15,612 - CapitalAllocationNotebook - INFO - Creating initial state dictionary. INFO:CapitalAllocationNotebook:Creating initial state dictionary. 2026-06-10 10:12:15,618 - CapitalAllocationNotebook - INFO - Generating 500 periods of asset returns for 5 assets. INFO:CapitalAllocationNotebook:Generating 500 periods of asset returns for 5 assets. 2026-06-10 10:12:15,644 - CapitalAllocationNotebook - INFO - Asset returns generated successfully. INFO:CapitalAllocationNotebook:Asset returns generated successfully. 2026-06-10 10:12:15,661 - CapitalAllocationNotebook - INFO - Simulated Asset Returns (first 5 periods): INFO:CapitalAllocationNotebook:Simulated Asset Returns (first 5 periods):
| Asset_1 | Asset_2 | Asset_3 | Asset_4 | Asset_5 | |
|---|---|---|---|---|---|
| 0 | -0.003931 | 0.001245 | -0.010430 | -0.004374 | -0.003048 |
| 1 | 0.013133 | 0.014433 | 0.007723 | -0.003542 | -0.008695 |
| 2 | 0.006748 | -0.004787 | 0.003065 | 0.011450 | 0.007332 |
| 3 | 0.001957 | -0.005216 | -0.001339 | 0.010790 | 0.014110 |
| 4 | -0.001203 | -0.017331 | -0.008785 | -0.004203 | -0.017107 |
2026-06-10 10:12:15,693 - CapitalAllocationNotebook - INFO - Plotting asset returns distribution: Simulated Asset Returns Distribution. INFO:CapitalAllocationNotebook:Plotting asset returns distribution: Simulated Asset Returns Distribution.
2026-06-10 10:12:17,474 - CapitalAllocationNotebook - INFO - Asset returns distribution plots displayed. INFO:CapitalAllocationNotebook:Asset returns distribution plots displayed. 2026-06-10 10:12:17,476 - CapitalAllocationNotebook - INFO - Plotting asset correlation heatmap: Simulated Asset Returns Correlation Heatmap. INFO:CapitalAllocationNotebook:Plotting asset correlation heatmap: Simulated Asset Returns Correlation Heatmap.
2026-06-10 10:12:17,746 - CapitalAllocationNotebook - INFO - Asset correlation heatmap displayed. INFO:CapitalAllocationNotebook:Asset correlation heatmap displayed. 2026-06-10 10:12:17,748 - CapitalAllocationNotebook - INFO - Calculating portfolio statistics (expected returns, stds, covariance matrix). INFO:CapitalAllocationNotebook:Calculating portfolio statistics (expected returns, stds, covariance matrix). 2026-06-10 10:12:17,754 - CapitalAllocationNotebook - INFO - Portfolio statistics calculated. INFO:CapitalAllocationNotebook:Portfolio statistics calculated. 2026-06-10 10:12:17,763 - CapitalAllocationNotebook - INFO - Expected Annual Returns: INFO:CapitalAllocationNotebook:Expected Annual Returns:
| Asset | Expected Annual Return | |
|---|---|---|
| 0 | Asset_1 | 0.053086 |
| 1 | Asset_2 | 0.024091 |
| 2 | Asset_3 | -0.003735 |
| 3 | Asset_4 | -0.091800 |
| 4 | Asset_5 | 0.045003 |
2026-06-10 10:12:17,781 - CapitalAllocationNotebook - INFO - Annual Standard Deviations: INFO:CapitalAllocationNotebook:Annual Standard Deviations:
| Asset | Annual Std Dev | |
|---|---|---|
| 0 | Asset_1 | 0.125433 |
| 1 | Asset_2 | 0.186655 |
| 2 | Asset_3 | 0.160129 |
| 3 | Asset_4 | 0.109072 |
| 4 | Asset_5 | 0.235729 |
2026-06-10 10:12:17,798 - CapitalAllocationNotebook - INFO - Annual Covariance Matrix: INFO:CapitalAllocationNotebook:Annual Covariance Matrix:
| Asset_1 | Asset_2 | Asset_3 | Asset_4 | Asset_5 | |
|---|---|---|---|---|---|
| Asset_1 | 0.015733 | 0.014550 | 0.008811 | 0.001688 | 0.000503 |
| Asset_2 | 0.014550 | 0.034840 | 0.021644 | 0.005263 | 0.006007 |
| Asset_3 | 0.008811 | 0.021644 | 0.025641 | 0.008149 | 0.010028 |
| Asset_4 | 0.001688 | 0.005263 | 0.008149 | 0.011897 | 0.020591 |
| Asset_5 | 0.000503 | 0.006007 | 0.010028 | 0.020591 | 0.055568 |
Equal Weight Allocation
The Equal Weight strategy is the simplest form of capital allocation, where each asset or strategy is assigned an equal proportion of the total capital. It serves as a good baseline for comparison with more complex methods.
# 2. Apply Equal Weight Allocation
log.info("--- Demonstration: Equal Weight Allocation ---")
state_equal_weight = state_simulated.copy()
state_equal_weight = allocate_equal_weight(state_equal_weight)
# Simulate performance with equal weights
state_equal_weight = simulate_portfolio_performance(state_equal_weight, state_equal_weight['current_weights'])
# 3. Calculate and display performance metrics
equal_weight_metrics = get_tracking_metrics(state_equal_weight, state_equal_weight['portfolio_returns'])
log.info("Equal Weight Portfolio Performance Metrics:")
display(pd.DataFrame([equal_weight_metrics], index=['Equal Weight']))
# 4. Plot performance
plot_portfolio_performance(state_equal_weight, title='Equal Weight Portfolio Performance')
# Plot risk contributions for equal weight (should be equal if risks are similar)
plot_risk_contributions(state_equal_weight, title='Equal Weight Risk Contributions')2026-06-10 10:14:20,541 - CapitalAllocationNotebook - INFO - --- Demonstration: Equal Weight Allocation --- INFO:CapitalAllocationNotebook:--- Demonstration: Equal Weight Allocation --- 2026-06-10 10:14:20,544 - CapitalAllocationNotebook - INFO - Allocating capital using an equal-weight strategy. INFO:CapitalAllocationNotebook:Allocating capital using an equal-weight strategy. 2026-06-10 10:14:20,546 - CapitalAllocationNotebook - INFO - Simulating portfolio performance with fixed weights. INFO:CapitalAllocationNotebook:Simulating portfolio performance with fixed weights. 2026-06-10 10:14:20,548 - CapitalAllocationNotebook - INFO - Calculating portfolio returns. INFO:CapitalAllocationNotebook:Calculating portfolio returns. 2026-06-10 10:14:20,552 - CapitalAllocationNotebook - INFO - Portfolio simulation complete. INFO:CapitalAllocationNotebook:Portfolio simulation complete. 2026-06-10 10:14:20,554 - CapitalAllocationNotebook - INFO - Calculating performance metrics. INFO:CapitalAllocationNotebook:Calculating performance metrics. 2026-06-10 10:14:20,560 - CapitalAllocationNotebook - INFO - Equal Weight Portfolio Performance Metrics: INFO:CapitalAllocationNotebook:Equal Weight Portfolio Performance Metrics:
| total_return | annualized_return | annualized_volatility | sharpe_ratio | max_drawdown | |
|---|---|---|---|---|---|
| Equal Weight | -0.002818 | -0.001421 | 0.116301 | -0.039737 | -0.162824 |
2026-06-10 10:14:20,583 - CapitalAllocationNotebook - INFO - Plotting portfolio performance: Equal Weight Portfolio Performance. INFO:CapitalAllocationNotebook:Plotting portfolio performance: Equal Weight Portfolio Performance.
2026-06-10 10:14:20,975 - CapitalAllocationNotebook - INFO - Portfolio performance plot displayed. INFO:CapitalAllocationNotebook:Portfolio performance plot displayed. 2026-06-10 10:14:20,978 - CapitalAllocationNotebook - INFO - Plotting risk contributions: Equal Weight Risk Contributions. INFO:CapitalAllocationNotebook:Plotting risk contributions: Equal Weight Risk Contributions. /tmp/ipykernel_1192/3722092627.py:51: FutureWarning: Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect. sns.barplot(x=asset_names, y=risk_contributions_percent, palette='viridis')
2026-06-10 10:14:21,357 - CapitalAllocationNotebook - INFO - Risk contributions plot displayed. INFO:CapitalAllocationNotebook:Risk contributions plot displayed.
Mean-Variance Optimization (Max Sharpe Ratio)
Mean-Variance Optimization (MVO) aims to construct an optimal portfolio by maximizing expected return for a given level of risk or minimizing risk for a given expected return. We will specifically optimize for the maximum Sharpe Ratio, which represents the best risk-adjusted return.
# 5. Apply Mean-Variance Optimization (Max Sharpe)
log.info("--- Demonstration: Mean-Variance Optimization (Max Sharpe) ---")
state_mvo = state_simulated.copy()
# Ensure portfolio statistics are already calculated in state_simulated
state_mvo = allocate_mean_variance_optimization(state_mvo, max_sharpe=True)
# Simulate performance with MVO weights
state_mvo = simulate_portfolio_performance(state_mvo, state_mvo['current_weights'])
# 6. Calculate and display performance metrics
mvo_metrics = get_tracking_metrics(state_mvo, state_mvo['portfolio_returns'])
log.info("MVO (Max Sharpe) Portfolio Performance Metrics:")
display(pd.DataFrame([mvo_metrics], index=['MVO (Max Sharpe)']))
# 7. Plot performance
plot_portfolio_performance(state_mvo, title='MVO (Max Sharpe) Portfolio Performance')
# Plot Efficient Frontier
plot_efficient_frontier(state_simulated)
# Plot risk contributions for MVO
plot_risk_contributions(state_mvo, title='MVO (Max Sharpe) Risk Contributions')2026-06-10 10:14:22,082 - CapitalAllocationNotebook - INFO - --- Demonstration: Mean-Variance Optimization (Max Sharpe) --- INFO:CapitalAllocationNotebook:--- Demonstration: Mean-Variance Optimization (Max Sharpe) --- 2026-06-10 10:14:22,085 - CapitalAllocationNotebook - INFO - Performing Mean-Variance Optimization (MVO). INFO:CapitalAllocationNotebook:Performing Mean-Variance Optimization (MVO). 2026-06-10 10:14:22,088 - CapitalAllocationNotebook - INFO - Optimizing for maximum Sharpe Ratio. INFO:CapitalAllocationNotebook:Optimizing for maximum Sharpe Ratio. 2026-06-10 10:14:22,108 - CapitalAllocationNotebook - INFO - Simulating portfolio performance with fixed weights. INFO:CapitalAllocationNotebook:Simulating portfolio performance with fixed weights. 2026-06-10 10:14:22,110 - CapitalAllocationNotebook - INFO - Calculating portfolio returns. INFO:CapitalAllocationNotebook:Calculating portfolio returns. 2026-06-10 10:14:22,114 - CapitalAllocationNotebook - INFO - Portfolio simulation complete. INFO:CapitalAllocationNotebook:Portfolio simulation complete. 2026-06-10 10:14:22,118 - CapitalAllocationNotebook - INFO - Calculating performance metrics. INFO:CapitalAllocationNotebook:Calculating performance metrics. 2026-06-10 10:14:22,123 - CapitalAllocationNotebook - INFO - MVO (Max Sharpe) Portfolio Performance Metrics: INFO:CapitalAllocationNotebook:MVO (Max Sharpe) Portfolio Performance Metrics:
| total_return | annualized_return | annualized_volatility | sharpe_ratio | max_drawdown | |
|---|---|---|---|---|---|
| MVO (Max Sharpe) | 0.094688 | 0.046652 | 0.115599 | 0.366043 | -0.107611 |
2026-06-10 10:14:22,138 - CapitalAllocationNotebook - INFO - Plotting portfolio performance: MVO (Max Sharpe) Portfolio Performance. INFO:CapitalAllocationNotebook:Plotting portfolio performance: MVO (Max Sharpe) Portfolio Performance.
2026-06-10 10:14:22,559 - CapitalAllocationNotebook - INFO - Portfolio performance plot displayed. INFO:CapitalAllocationNotebook:Portfolio performance plot displayed. 2026-06-10 10:14:22,562 - CapitalAllocationNotebook - INFO - Plotting Efficient Frontier. INFO:CapitalAllocationNotebook:Plotting Efficient Frontier.
2026-06-10 10:14:23,962 - CapitalAllocationNotebook - INFO - Efficient Frontier plot displayed. INFO:CapitalAllocationNotebook:Efficient Frontier plot displayed. 2026-06-10 10:14:23,969 - CapitalAllocationNotebook - INFO - Plotting risk contributions: MVO (Max Sharpe) Risk Contributions. INFO:CapitalAllocationNotebook:Plotting risk contributions: MVO (Max Sharpe) Risk Contributions. /tmp/ipykernel_1192/3722092627.py:51: FutureWarning: Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect. sns.barplot(x=asset_names, y=risk_contributions_percent, palette='viridis')
2026-06-10 10:14:24,288 - CapitalAllocationNotebook - INFO - Risk contributions plot displayed. INFO:CapitalAllocationNotebook:Risk contributions plot displayed.
Risk Parity Allocation
Risk Parity is an allocation strategy that seeks to distribute risk equally among the assets in a portfolio, meaning each asset contributes the same amount to the total portfolio risk. This approach often leads to more diversified portfolios than MVO, especially in the presence of estimation errors.
# 8. Apply Risk Parity Allocation
log.info("--- Demonstration: Risk Parity Allocation ---")
state_risk_parity = state_simulated.copy()
# Ensure portfolio statistics are already calculated in state_simulated
state_risk_parity = allocate_risk_parity(state_risk_parity)
# Simulate performance with Risk Parity weights
state_risk_parity = simulate_portfolio_performance(state_risk_parity, state_risk_parity['current_weights'])
# 9. Calculate and display performance metrics
risk_parity_metrics = get_tracking_metrics(state_risk_parity, state_risk_parity['portfolio_returns'])
log.info("Risk Parity Portfolio Performance Metrics:")
display(pd.DataFrame([risk_parity_metrics], index=['Risk Parity']))
# 10. Plot performance
plot_portfolio_performance(state_risk_parity, title='Risk Parity Portfolio Performance')
# Plot risk contributions for Risk Parity (should be close to equal)
plot_risk_contributions(state_risk_parity, title='Risk Parity Risk Contributions')2026-06-10 10:14:24,864 - CapitalAllocationNotebook - INFO - --- Demonstration: Risk Parity Allocation --- INFO:CapitalAllocationNotebook:--- Demonstration: Risk Parity Allocation --- 2026-06-10 10:14:24,868 - CapitalAllocationNotebook - INFO - Performing Risk Parity allocation. INFO:CapitalAllocationNotebook:Performing Risk Parity allocation. 2026-06-10 10:14:24,906 - CapitalAllocationNotebook - INFO - Simulating portfolio performance with fixed weights. INFO:CapitalAllocationNotebook:Simulating portfolio performance with fixed weights. 2026-06-10 10:14:24,907 - CapitalAllocationNotebook - INFO - Calculating portfolio returns. INFO:CapitalAllocationNotebook:Calculating portfolio returns. 2026-06-10 10:14:24,910 - CapitalAllocationNotebook - INFO - Portfolio simulation complete. INFO:CapitalAllocationNotebook:Portfolio simulation complete. 2026-06-10 10:14:24,912 - CapitalAllocationNotebook - INFO - Calculating performance metrics. INFO:CapitalAllocationNotebook:Calculating performance metrics. 2026-06-10 10:14:24,915 - CapitalAllocationNotebook - INFO - Risk Parity Portfolio Performance Metrics: INFO:CapitalAllocationNotebook:Risk Parity Portfolio Performance Metrics:
| total_return | annualized_return | annualized_volatility | sharpe_ratio | max_drawdown | |
|---|---|---|---|---|---|
| Risk Parity | -0.011841 | -0.005985 | 0.107317 | -0.095098 | -0.143579 |
2026-06-10 10:14:24,940 - CapitalAllocationNotebook - INFO - Plotting portfolio performance: Risk Parity Portfolio Performance. INFO:CapitalAllocationNotebook:Plotting portfolio performance: Risk Parity Portfolio Performance.
2026-06-10 10:14:25,436 - CapitalAllocationNotebook - INFO - Portfolio performance plot displayed. INFO:CapitalAllocationNotebook:Portfolio performance plot displayed. 2026-06-10 10:14:25,440 - CapitalAllocationNotebook - INFO - Plotting risk contributions: Risk Parity Risk Contributions. INFO:CapitalAllocationNotebook:Plotting risk contributions: Risk Parity Risk Contributions. /tmp/ipykernel_1192/3722092627.py:51: FutureWarning: Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect. sns.barplot(x=asset_names, y=risk_contributions_percent, palette='viridis')
2026-06-10 10:14:25,665 - CapitalAllocationNotebook - INFO - Risk contributions plot displayed. INFO:CapitalAllocationNotebook:Risk contributions plot displayed.
Comparison of Allocation Strategies
Let's compare the performance metrics of the Equal Weight, Mean-Variance Optimization (Max Sharpe), and Risk Parity strategies side-by-side.
# 11. Aggregate and display metrics for comparison
log.info("--- Comparison of Allocation Strategies ---")
all_metrics = pd.DataFrame({
'Equal Weight': equal_weight_metrics,
'MVO (Max Sharpe)': mvo_metrics,
'Risk Parity': risk_parity_metrics
}).T
log.info("Summary of Performance Metrics:")
display(all_metrics)2026-06-10 10:14:26,250 - CapitalAllocationNotebook - INFO - --- Comparison of Allocation Strategies --- INFO:CapitalAllocationNotebook:--- Comparison of Allocation Strategies --- 2026-06-10 10:14:26,253 - CapitalAllocationNotebook - INFO - Summary of Performance Metrics: INFO:CapitalAllocationNotebook:Summary of Performance Metrics:
| total_return | annualized_return | annualized_volatility | sharpe_ratio | max_drawdown | |
|---|---|---|---|---|---|
| Equal Weight | -0.002818 | -0.001421 | 0.116301 | -0.039737 | -0.162824 |
| MVO (Max Sharpe) | 0.094688 | 0.046652 | 0.115599 | 0.366043 | -0.107611 |
| Risk Parity | -0.011841 | -0.005985 | 0.107317 | -0.095098 | -0.143579 |
Production Considerations
Implementing capital allocation strategies in a production environment requires careful consideration beyond theoretical models. Here are some best practices:
| Consideration | Best Practice |
|---|---|
| Data Quality | Ensure clean, reliable, and timely data feeds for asset prices, returns, and other relevant factors. |
| Transaction Costs | Factor in brokerage fees, bid-ask spreads, and market impact, as they can significantly erode returns. |
| Rebalancing Frequency | Determine optimal rebalancing periods (e.g., daily, weekly, monthly) considering costs and strategy drift. |
| Liquidity Constraints | Account for asset liquidity, especially for large allocations, to avoid market disruption and unfavorable prices. |
| Regulatory Compliance | Adhere to all relevant financial regulations and reporting requirements. |
| Risk Limits | Define and enforce strict risk limits (e.g., maximum drawdown, VaR) to protect capital. |
| Stress Testing | Simulate portfolio performance under various extreme market conditions to assess robustness. |
| Model Robustness | Regularly re-evaluate and recalibrate allocation models to ensure they remain effective and adapt to changing markets. |
| Execution Slippage | Plan for potential differences between expected and actual execution prices, especially in volatile markets. |
| Operational Monitoring | Implement robust monitoring systems for trades, portfolio performance, and system health. |
| Error Handling/Retries | Use mechanisms like exponential backoff for API calls and data fetching to handle transient failures gracefully. |
Conclusion
This notebook provided a comprehensive overview of capital allocation across strategies, covering essential concepts, core functions, and practical demonstrations. We implemented and compared three common allocation strategies: Equal Weight, Mean-Variance Optimization (Max Sharpe), and Risk Parity, using both simulated and real-world data.
Key takeaways include:
- State Management: A centralized state dictionary (
Dict) facilitates clear data flow and consistency across functions. - Modularity: Breaking down functionality into small, well-documented functions improves readability and maintainability.
- Logging: Effective use of
loggingfor tracking execution flow, debugging, and warning about potential issues. - Visualization: Plotting tools like
matplotlibandseabornare critical for understanding and comparing portfolio performance and asset characteristics. - Error Handling: Implementing robust retry mechanisms (
exponential_backoff_retry) is crucial for dealing with external dependencies like API calls. - Optimization Libraries: Tools like
cvxpyandscipy.optimizeprovide powerful capabilities for solving complex allocation problems.
While this notebook lays a strong foundation, real-world capital allocation is an intricate process that demands continuous monitoring, adaptation, and rigorous risk management. The principles and techniques demonstrated here serve as valuable building blocks for developing more sophisticated and resilient investment strategies.