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:
Build a
PortfolioAllocatorAllocation families
Efficient frontier and risk metrics
Backtest and stress tests
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']
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.