Live Trading·Trading Infrastructure·Intermediate

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.

asset-allocationlive-tradingtrading-strategies

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:

ConceptDescriptionKey Metrics
Capital AllocationThe process of distributing investment capital among different assets or strategies to achieve objectives.Portfolio Return, Risk, Sharpe Ratio
Investment StrategiesDefined approaches for making investment decisions (e.g., value, growth, momentum).Alpha, Beta, Volatility
Risk ManagementIdentifying, assessing, and mitigating investment risks.Value at Risk (VaR), Conditional VaR
Portfolio OptimizationSelecting the best portfolio based on risk-return trade-offs.Efficient Frontier
Mean-Variance OptimizationA classical approach to constructing optimal portfolios by balancing expected return and variance.Expected Return, Standard Deviation
Risk ParityAn allocation approach where each component (asset/strategy) contributes equally to the total portfolio risk.Risk Contribution
Simulated ReturnsGenerating 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 yfinance
Requirement 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 minimize

Core 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 to logging.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 state

Function 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 match num_assets.
  • stds (np.ndarray, optional): Array of daily standard deviations for each asset. If None, defaults to small positive values. The length must match num_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 state

Function 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 metrics

Function 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_returns

Function 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 contain asset_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 the portfolio_returns (as a pd.Series) and portfolio_value (as a pd.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 state

Function 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, containing num_assets.

Returns:

  • dict: The updated state dictionary with the current_weights attribute 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 state

Function 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 contain asset_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, including expected_annual_returns, annual_stds, and annual_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 state

Function 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 contain expected_annual_returns and annual_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 if max_sharpe is True).
  • 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 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 state

Function 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 to func.
  • kwargs (dict, optional): Keyword 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.

Returns:

  • Any: The result of the func if successful.

Raises:

  • Exception: If the function fails after max_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 above

Function 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_contributions

Function 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 contain annual_cov_matrix.

Returns:

  • dict: The updated state dictionary with current_weights set 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 state

Function 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 of asset_returns from real stock data, and updated asset_names and num_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 state

Function 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 contain portfolio_value and portfolio_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 contain asset_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 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.
[ ]
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 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.
[ ]
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 contain asset_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:

  1. Generate simulated asset returns for a set of hypothetical strategies.
  2. Apply different allocation strategies: Equal Weight, Mean-Variance Optimization (Max Sharpe), and Risk Parity.
  3. Simulate portfolio performance for each strategy.
  4. Calculate and display performance metrics for comparison.
  5. Visualize portfolio performance (equity curves, daily returns).
  6. Visualize asset characteristics (returns distribution, correlation).
  7. Plot the Efficient Frontier to illustrate MVO.
  8. 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.
cell output
cell output
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.
cell output
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.
cell output
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')
cell output
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.
cell output
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.
cell output
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')
cell output
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.
cell output
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')
cell output
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:

ConsiderationBest Practice
Data QualityEnsure clean, reliable, and timely data feeds for asset prices, returns, and other relevant factors.
Transaction CostsFactor in brokerage fees, bid-ask spreads, and market impact, as they can significantly erode returns.
Rebalancing FrequencyDetermine optimal rebalancing periods (e.g., daily, weekly, monthly) considering costs and strategy drift.
Liquidity ConstraintsAccount for asset liquidity, especially for large allocations, to avoid market disruption and unfavorable prices.
Regulatory ComplianceAdhere to all relevant financial regulations and reporting requirements.
Risk LimitsDefine and enforce strict risk limits (e.g., maximum drawdown, VaR) to protect capital.
Stress TestingSimulate portfolio performance under various extreme market conditions to assess robustness.
Model RobustnessRegularly re-evaluate and recalibrate allocation models to ensure they remain effective and adapt to changing markets.
Execution SlippagePlan for potential differences between expected and actual execution prices, especially in volatile markets.
Operational MonitoringImplement robust monitoring systems for trades, portfolio performance, and system health.
Error Handling/RetriesUse 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 logging for tracking execution flow, debugging, and warning about potential issues.
  • Visualization: Plotting tools like matplotlib and seaborn are 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 cvxpy and scipy.optimize provide 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.