Portfolio Optimization

This notebook covers abaquant.portfolio: allocation families (mean-variance, risk-based, downside-risk), the Markowitz efficient frontier, Monte Carlo portfolio clouds, a compact backtest, and historical stress testing.

Sections:

  1. Build a PortfolioAllocator

  2. Allocation families

  3. Efficient frontier and risk metrics

  4. Backtest and stress tests

  5. Visualizations

Setup

import abaquant
print(f"AbaQuant version: {abaquant.__version__}")
AbaQuant version: 1.0.0rc1
import numpy as np
import pandas as pd

from abaquant.portfolio.backtesting import run_backtest
from abaquant.portfolio.efficient_frontier import markowitz_frontier, monte_carlo_portfolios
from abaquant.portfolio.optimization import PortfolioAllocator
from abaquant.portfolio.risk_metrics import compute_all_metrics, portfolio_returns
from abaquant.portfolio.stress_testing import run_all_scenarios
from abaquant.visualization import VisualizationError

A small deterministic price/return panel

def sample_prices() -> pd.DataFrame:
    dates = pd.date_range("2025-01-02", periods=36, freq="B")
    trend = np.linspace(0.0, 1.0, len(dates))
    seasonal = np.sin(np.linspace(0.0, 4.0 * np.pi, len(dates)))
    return pd.DataFrame(
        {
            "ALPHA": 100.0 + 9.0 * trend + 1.8 * seasonal,
            "BETA": 82.0 + 6.0 * trend - 1.2 * seasonal,
            "GAMMA": 54.0 + 3.0 * trend + 0.7 * np.cos(np.linspace(0.0, 3.0 * np.pi, len(dates))),
        },
        index=dates,
    )

def sample_returns() -> pd.DataFrame:
    return sample_prices().pct_change().dropna()

prices = sample_prices()
returns = sample_returns()
returns.head()
ALPHA BETA GAMMA
2025-01-03 0.008896 -0.003051 0.001106
2025-01-06 0.008018 -0.002403 0.000216
2025-01-07 0.006469 -0.001174 -0.000574
2025-01-08 0.004453 0.000479 -0.001212
2025-01-09 0.002221 0.002339 -0.001650

1. Build a PortfolioAllocator

The allocator groups three families of allocation strategies: mean_variance, risk_based, and downside_risk.

allocator = PortfolioAllocator(returns, annual_risk_free_rate=0.02)

2. Allocation families

Compare equal weight, minimum variance, maximum Sharpe, risk parity, inverse volatility, and minimum-CVaR allocations on the same universe.

allocations = {
    "equal_weight": allocator.mean_variance.equal_weight(),
    "minimum_variance": allocator.mean_variance.minimum_variance(),
    "maximum_sharpe": allocator.mean_variance.maximum_sharpe(),
    "risk_parity": allocator.risk_based.risk_parity(),
    "inverse_volatility": allocator.risk_based.inverse_volatility(),
    "minimum_cvar": allocator.downside_risk.minimum_cvar(alpha=0.05),
}

summary = {}
for name, result in allocations.items():
    weights = getattr(result, "weights", result)
    first_weight = float(weights.iloc[0]) if hasattr(weights, "iloc") else float(np.asarray(weights)[0])
    summary[f"{name}_alpha_weight"] = first_weight

for key, value in summary.items():
    print(f"{key:32s}: {value:.4f}")
equal_weight_alpha_weight       : 0.3333
minimum_variance_alpha_weight   : 0.4496
maximum_sharpe_alpha_weight     : 0.4500
risk_parity_alpha_weight        : 0.3438
inverse_volatility_alpha_weight : 0.2422
minimum_cvar_alpha_weight       : 0.4501

3. Efficient frontier and risk metrics

Trace the constrained Markowitz efficient frontier, sample a random portfolio cloud, and compute a full performance/risk summary for one fixed-weight portfolio.

mean_returns = returns.mean() * 252
covariance = returns.cov() * 252
frontier = markowitz_frontier(mean_returns, covariance, n_points=8)
cloud = monte_carlo_portfolios(mean_returns, covariance, n_portfolios=250, rf=0.02, seed=42)

weights = np.array([0.4, 0.35, 0.25])
portfolio_series = portfolio_returns(returns, weights)
metrics = compute_all_metrics(portfolio_series, rf=0.02)

print(f"Frontier points:   {len(frontier)}")
print(f"Cloud portfolios:  {len(cloud)}")
print(f"Sharpe ratio:      {metrics['Sharpe Ratio']:.4f}")
print(f"Max drawdown:      {metrics['Max Drawdown']:.4f}")
Frontier points:   8
Cloud portfolios:  250
Sharpe ratio:      39.0699
Max drawdown:      0.0000
frontier.head()
Return Volatility ALPHA BETA GAMMA Sharpe
0 0.564308 0.003313 0.475283 0.524717 0.000000 170.338660
1 0.504977 0.004638 0.354761 0.494026 0.151213 108.883058
2 0.445647 0.009414 0.254455 0.435556 0.309989 47.337831
3 0.386317 0.014219 0.154148 0.377087 0.468764 27.169874
4 0.326986 0.019030 0.053842 0.318618 0.627540 17.182896

4. Backtest and stress tests

Run a simple rebalanced backtest on prices, then apply predefined historical stress scenarios to a fixed-weight portfolio. For the richer, object-oriented backtesting API (rebalance schedules, transaction costs, benchmarks, rolling metrics), see notebook 19 — Portfolio Backtesting.

backtest = run_backtest(
    prices, strategy_name="equal_weight", rebalance_freq="monthly", lookback_days=8
)
stress_weights = pd.Series([0.4, 0.35, 0.25], index=prices.columns)
stress = run_all_scenarios(prices, stress_weights)

print(f"Backtest object created: {backtest is not None}")
print(f"Stress scenarios evaluated: {len(stress)}")
Backtest object created: True
Stress scenarios evaluated: 4

5. Visualizations

Allocation weights, cumulative-return path, and the asset-correlation heatmap.

try:
    max_sharpe_weights = allocations["maximum_sharpe"]
    figures = {
        "weights": allocator.visualize(weights=max_sharpe_weights, chart="weights"),
        "cumulative_returns": allocator.visualize(
            weights=max_sharpe_weights, chart="cumulative_returns"
        ),
        "correlation": allocator.visualize(chart="correlation"),
    }
    print(f"Created {len(figures)} figures: {list(figures)}")
except VisualizationError as exc:
    print(f"Visualization skipped (optional dependency missing): {exc}")
Created 3 figures: ['weights', 'cumulative_returns', 'correlation']
../../_images/58dbcdc8dacc5c9ca6f438c3cdd35971e26e211cc0a8f5bbab67527c7790bacc.png ../../_images/abeb944c20f8e5e5daccd5299afc1436b834ad79b57309474edaae255de3c27e.png ../../_images/43760528783a7ef4de9c3e5b23f94fac6f35271ca67292963132a5e698337a10.png

Takeaway

PortfolioAllocator composes three allocation philosophies behind one facade. Combine it with .backtest(), .scenario_analysis(), and .report() for a full research-to-report workflow — see notebooks 19 — Portfolio Backtesting, 14 — Scenario Analysis, and 21 — Exportable Reports.