Portfolio Optimization
This example demonstrates various portfolio optimization techniques using openseries, including both theoretical approaches and real-world applications with actual fund data.
Basic Portfolio Optimization Setup
import yfinance as yf
from openseries import OpenTimeSeries, OpenFrame
from openseries import efficient_frontier, simulate_portfolios
# Define investment universe
universe = {
"VTI": "Total Stock Market",
"VEA": "Developed Markets",
"VWO": "Emerging Markets",
"BND": "Total Bond Market",
"VNQ": "Real Estate",
"VDE": "Energy",
"VGT": "Technology",
"VHT": "Healthcare"
}
# Load data
assets = []
for ticker, name in universe.items():
# This may fail if the ticker is invalid or data unavailable
data = yf.Ticker(ticker).history(period="5y")
series = OpenTimeSeries.from_df(dframe=data['Close'])
series.set_new_label(lvl_zero=name)
assets.append(series)
print(f"Loaded {name}")
# Create investment universe frame
investment_universe = OpenFrame(constituents=assets)
print(f"\nInvestment universe: {investment_universe.item_count} assets")
print(f"Period: {investment_universe.first_idx} to {investment_universe.last_idx}")
Mean-Variance Optimization
# Calculate efficient frontier
# This may fail with various exceptions
frontier_df, simulated_df, optimal_portfolio = efficient_frontier(
eframe=investment_universe,
num_ports=100,
seed=42
)
print("=== EFFICIENT FRONTIER RESULTS ===")
print(f"Generated {len(frontier_df)} efficient portfolios")
print(f"Simulated {len(simulated_df)} random portfolios")
# Find key portfolios
returns = frontier_df['ret']
volatilities = frontier_df['stdev']
sharpe_ratios = returns / volatilities
# Maximum Sharpe ratio portfolio
max_sharpe_idx = sharpe_ratios.idxmax()
max_sharpe_weights = optimal_portfolio[-len(investment_universe.constituents):]
print(f"\n=== MAXIMUM SHARPE RATIO PORTFOLIO ===")
print(f"Expected Return: {frontier_df.iloc[max_sharpe_idx]['ret']:.2%}")
print(f"Volatility: {frontier_df.iloc[max_sharpe_idx]['stdev']:.2%}")
print(f"Sharpe Ratio: {sharpe_ratios.iloc[max_sharpe_idx]:.2f}")
print("\nOptimal Weights:")
for i, weight in enumerate(max_sharpe_weights):
asset_name = investment_universe.constituents[i].label
if weight > 0.01: # Only show weights > 1%
print(f" {asset_name}: {weight:.1%}")
# Minimum volatility portfolio
min_vol_idx = volatilities.idxmin()
min_vol_weights = frontier_df.iloc[min_vol_idx][investment_universe.columns_lvl_zero].values
print(f"\n=== MINIMUM VOLATILITY PORTFOLIO ===")
print(f"Expected Return: {min_vol_row['ret']:.2%}")
print(f"Volatility: {min_vol_row['stdev']:.2%}")
print(f"Sharpe Ratio: {sharpe_ratios.iloc[min_vol_idx]:.2f}")
print("\nMinimum Volatility Weights:")
for col in investment_universe.columns_lvl_zero:
weight = min_vol_row[col]
if weight > 0.01:
print(f" {col}: {weight:.1%}")
Monte Carlo Portfolio Simulation
# Generate random portfolios
# This may fail with various exceptions
simulation_results = simulate_portfolios(
simframe=investment_universe,
num_ports=50000,
seed=42
)
print(f"\n=== MONTE CARLO SIMULATION ===")
print(f"Simulated {len(simulation_results)} random portfolios")
sim_returns = simulation_results['ret'].values
sim_volatilities = simulation_results['stdev'].values
sim_sharpe_ratios = sim_returns / sim_volatilities
# Statistics of simulated portfolios
print(f"\nSimulation Statistics:")
print(f"Return range: {sim_returns.min():.2%} to {sim_returns.max():.2%}")
print(f"Volatility range: {sim_volatilities.min():.2%} to {sim_volatilities.max():.2%}")
print(f"Sharpe range: {sim_sharpe_ratios.min():.2f} to {sim_sharpe_ratios.max():.2f}")
# Best portfolios from simulation
sorted_indices = sorted(range(len(sim_sharpe_ratios)), key=lambda i: sim_sharpe_ratios.iloc[i], reverse=True)
top_sharpe_indices = sorted_indices[:5]
print(f"\n=== TOP 5 SIMULATED PORTFOLIOS ===")
for i, idx in enumerate(reversed(top_sharpe_indices)):
print(f"\nRank {i+1}:")
print(f" Return: {sim_returns[idx]:.2%}")
print(f" Volatility: {sim_volatilities[idx]:.2%}")
print(f" Sharpe: {sim_sharpe_ratios[idx]:.2f}")
weights = simulation_results.iloc[idx][investment_universe.columns_lvl_zero].values
print(" Weights:")
for j, weight in enumerate(weights):
if weight > 0.05: # Only show weights > 5%
asset_name = investment_universe.constituents[j].label
print(f" {asset_name}: {weight:.1%}")
Risk-Based Portfolio Strategies
Equal Weight Portfolio
# Equal weight portfolio using native weight_strat
equal_weight_portfolio_df = investment_universe.make_portfolio(
name="Equal Weight",
weight_strat="eq_weights"
)
equal_weight_portfolio = OpenTimeSeries.from_df(dframe=equal_weight_portfolio_df)
print(f"\n=== EQUAL WEIGHT PORTFOLIO ===")
print(f"Return: {equal_weight_portfolio.geo_ret:.2%}")
print(f"Volatility: {equal_weight_portfolio.vol:.2%}")
print(f"Sharpe: {equal_weight_portfolio.ret_vol_ratio:.2f}")
Inverse Volatility Portfolio
# Inverse volatility weighting using native weight_strat
inv_vol_portfolio_df = investment_universe.make_portfolio(
name="Inverse Volatility",
weight_strat="inv_vol"
)
inv_vol_portfolio = OpenTimeSeries.from_df(dframe=inv_vol_portfolio_df)
print(f"\n=== INVERSE VOLATILITY PORTFOLIO ===")
print(f"Return: {inv_vol_portfolio.geo_ret:.2%}")
print(f"Volatility: {inv_vol_portfolio.vol:.2%}")
print(f"Sharpe: {inv_vol_portfolio.ret_vol_ratio:.2f}")
Maximum Diversification Portfolio
The maximum diversification strategy aims to maximize portfolio diversification by optimizing the correlation structure. This strategy can encounter numerical issues in certain scenarios:
# Maximum diversification portfolio using native weight_strat
# This may fail with MaxDiversificationNaNError or MaxDiversificationNegativeWeightsError
max_div_portfolio_df = investment_universe.make_portfolio(
name="Maximum Diversification",
weight_strat="max_div"
)
max_div_portfolio = OpenTimeSeries.from_df(dframe=max_div_portfolio_df)
print(f"\n=== MAXIMUM DIVERSIFICATION PORTFOLIO ===")
print(f"Return: {max_div_portfolio.geo_ret:.2%}")
print(f"Volatility: {max_div_portfolio.vol:.2%}")
print(f"Sharpe: {max_div_portfolio.ret_vol_ratio:.2f}")
Minimum Volatility Overweight Portfolio
# Minimum volatility overweight portfolio using native weight_strat
min_vol_portfolio_df = investment_universe.make_portfolio(
name="Min Vol Overweight",
weight_strat="min_vol_overweight"
)
min_vol_portfolio = OpenTimeSeries.from_df(dframe=min_vol_portfolio_df)
print(f"\n=== MINIMUM VOLATILITY OVERWEIGHT PORTFOLIO ===")
print(f"Return: {min_vol_portfolio.geo_ret:.2%}")
print(f"Volatility: {min_vol_portfolio.vol:.2%}")
print(f"Sharpe: {min_vol_portfolio.ret_vol_ratio:.2f}")
Portfolio Comparison
# Compare all portfolio strategies
portfolios = [
equal_weight_portfolio,
inv_vol_portfolio,
max_div_portfolio,
min_vol_portfolio
]
# Add optimized portfolios if available
if 'max_sharpe_weights' in locals():
investment_universe.weights = max_sharpe_weights.tolist()
max_sharpe_portfolio_df = investment_universe.make_portfolio(
name="Max Sharpe (Optimized)"
)
max_sharpe_portfolio = OpenTimeSeries.from_df(dframe=max_sharpe_portfolio_df)
portfolios.append(max_sharpe_portfolio)
if 'min_vol_weights' in locals():
investment_universe.weights = min_vol_weights.tolist()
min_vol_portfolio_df = investment_universe.make_portfolio(
name="Min Vol (Optimized)"
)
min_vol_portfolio = OpenTimeSeries.from_df(dframe=min_vol_portfolio_df)
portfolios.append(min_vol_portfolio)
# Create comparison frame
comparison_frame = OpenFrame(constituents=portfolios)
comparison_metrics = comparison_frame.all_properties()
# Display key metrics
key_metrics = comparison_metrics.loc[['geo_ret', 'vol', 'ret_vol_ratio', 'max_drawdown']]
key_metrics.index = ['Annual Return', 'Volatility', 'Sharpe Ratio', 'Max Drawdown']
print(f"\n=== PORTFOLIO STRATEGY COMPARISON ===")
print((key_metrics * 100).round(2)) # Convert to percentages
Weight Strategy Details
The openseries library provides several built-in weight strategies for portfolio construction:
- Equal Weights (``eq_weights``)
Assigns equal weight to all assets
Most robust strategy, always works
Good baseline for comparison
- Inverse Volatility (``inv_vol``)
Weights assets inversely to their volatility
Lower volatility assets get higher weights
Generally stable and reliable
- Maximum Diversification (``max_div``)
Optimizes correlation structure for maximum diversification
Can encounter numerical issues with certain data patterns
May produce negative weights in some scenarios
Raises
MaxDiversificationNaNErrorfor numerical issuesRaises
MaxDiversificationNegativeWeightsErrorfor negative weights
- Minimum Volatility Overweight (``min_vol_overweight``)
Overweights the least volatile asset (60% weight)
Distributes remaining 40% equally among other assets
Based on the low volatility anomaly
- Exception Handling
When using the maximum diversification strategy, it’s recommended to handle potential exceptions:
from openseries.owntypes import ( MaxDiversificationNaNError, MaxDiversificationNegativeWeightsError ) # This may fail with MaxDiversificationNaNError or MaxDiversificationNegativeWeightsError portfolio_df = frame.make_portfolio(name="Max Div", weight_strat="max_div")
Backtesting Framework
# Define strategies to backtest using native weight_strat
strategies = {
'Equal Weight': 'eq_weights',
'Inverse Volatility': 'inv_vol',
'Max Diversification': 'max_div',
'Min Vol Overweight': 'min_vol_overweight'
}
# Run backtest using native strategies
backtest_results = {}
for strategy_name, weight_strat in strategies.items():
# This may fail with MaxDiversificationNaNError, MaxDiversificationNegativeWeightsError, or other exceptions
portfolio_df = investment_universe.make_portfolio(
name=strategy_name,
weight_strat=weight_strat
)
portfolio = OpenTimeSeries.from_df(dframe=portfolio_df)
backtest_results[strategy_name] = {
'return': portfolio.geo_ret,
'volatility': portfolio.vol,
'sharpe': portfolio.ret_vol_ratio,
'max_drawdown': portfolio.max_drawdown,
'calmar': portfolio.geo_ret / abs(portfolio.max_drawdown) if portfolio.max_drawdown != 0 else float('nan')
}
print(f"\n=== BACKTEST RESULTS ===")
for strategy_name, metrics in backtest_results.items():
print(f"\n{strategy_name}:")
print(f" Return: {metrics['return']:.4f}")
print(f" Volatility: {metrics['volatility']:.4f}")
print(f" Sharpe: {metrics['sharpe']:.4f}")
print(f" Max Drawdown: {metrics['max_drawdown']:.4f}")
print(f" Calmar: {metrics['calmar']:.4f}")
# Rank strategies
sorted_strategies = sorted(backtest_results.items(), key=lambda x: x[1]['sharpe'], reverse=True)
best_strategy = sorted_strategies[0][0]
print(f"\nBest performing strategy: {best_strategy}")
print(f"Sharpe ratio: {sorted_strategies[0][1]['sharpe']:.3f}")
Export Optimization Results
# Export using openseries native methods
# Export frame data
investment_universe.to_xlsx('portfolio_optimization_results.xlsx')
# Note: For comprehensive Excel export with multiple sheets,
# the DataFrames returned by all_properties() and correl_matrix
# are pandas DataFrames and support to_excel() method
print("\nOptimization results exported to 'portfolio_optimization_results.xlsx'")
Real-World Fund Portfolio Optimization
This section demonstrates portfolio optimization using actual fund data from professional fund managers, showing how optimization techniques apply in practice.
Using Real Fund Data for Optimization
Here’s how to work with real fund data using openseries methods directly:
from requests import get as requests_get
from openseries import (
OpenTimeSeries, OpenFrame, ValueType,
efficient_frontier, prepare_plot_data, sharpeplot,
load_plotly_dict, get_previous_business_day_before_today
)
# Define fund universe for optimization
fund_universe_isins = [
"SE0015243886", # Global High Yield
"SE0011337195", # Global Equity
"SE0011670843", # Global Bond
"SE0017832280", # Alternative Strategy
"SE0017832330", # Multi-Asset Strategy
]
# Load fund data using openseries methods
response = requests_get(url="https://api.captor.se/public/api/nav", timeout=10)
response.raise_for_status()
series_list = []
result = response.json()
for data in result:
if data["isin"] in fund_universe_isins:
series = OpenTimeSeries.from_arrays(
name=data["longName"],
isin=data["isin"],
baseccy=data["currency"],
dates=data["dates"],
values=data["navPerUnit"],
valuetype=ValueType.PRICE,
)
series_list.append(series)
# Create fund universe using openseries OpenFrame
fund_universe = OpenFrame(constituents=series_list)
# Process data using openseries methods
fund_universe = fund_universe.value_nan_handle().trunc_frame().to_cumret()
print(f"Fund universe created with {fund_universe.item_count} funds")
print(f"Analysis period: {fund_universe.first_idx} to {fund_universe.last_idx}")
Advanced Optimization with Real Data
# Set optimization parameters
simulations = 10000
frontier_points = 50
seed = 55
# Create current portfolio (equal weights)
current_portfolio_df = fund_universe.make_portfolio(
name="Current Portfolio",
weight_strat="eq_weights",
)
current_portfolio = OpenTimeSeries.from_df(dframe=current_portfolio_df)
# Calculate efficient frontier
frontier, simulated_portfolios, optimal_portfolio = efficient_frontier(
eframe=fund_universe,
num_ports=simulations,
seed=seed,
frontier_points=frontier_points,
)
# Prepare visualization data
plot_data = prepare_plot_data(
assets=fund_universe,
current=current_portfolio,
optimized=optimal_portfolio,
)
# Load plotly configuration
figdict, _ = load_plotly_dict()
# Create efficient frontier plot
optimization_plot, _ = sharpeplot(
sim_frame=simulated_portfolios,
line_frame=frontier,
point_frame=plot_data,
point_frame_mode="markers+text",
title="Real Fund Portfolio Optimization",
add_logo=False,
auto_open=False,
output_type="div",
)
optimization_plot = optimization_plot.update_layout(width=1200, height=700)
# Display the optimization results
optimization_plot.show(config=figdict["config"])
Performance Comparison Analysis
# Compare different portfolio strategies
strategies = {}
# Equal weight portfolio
equal_weight_portfolio_df = fund_universe.make_portfolio(
name="Equal Weight", weight_strat="eq_weights"
)
equal_weight_portfolio = OpenTimeSeries.from_df(dframe=equal_weight_portfolio_df)
strategies['Equal Weight'] = equal_weight_portfolio
# Optimal portfolio from efficient frontier
fund_universe.weights = optimal_portfolio[-fund_universe.item_count:].tolist()
optimal_portfolio_df = fund_universe.make_portfolio(name="Optimal Portfolio")
optimal_portfolio_series = OpenTimeSeries.from_df(dframe=optimal_portfolio_df)
strategies['Optimal Portfolio'] = optimal_portfolio_series
# Create comparison frame
comparison_frame = OpenFrame(constituents=list(strategies.values()))
comparison_metrics = comparison_frame.all_properties()
# Display key metrics
key_metrics = comparison_metrics.loc[['geo_ret', 'vol', 'ret_vol_ratio', 'max_drawdown']]
key_metrics.index = ['Annual Return', 'Volatility', 'Sharpe Ratio', 'Max Drawdown']
print("=== PORTFOLIO STRATEGY COMPARISON ===")
print((key_metrics * 100).round(2))
# Calculate improvement metrics
improvement = {
'Return Improvement': (optimal_portfolio_series.geo_ret - equal_weight_portfolio.geo_ret) * 100,
'Volatility Change': (optimal_portfolio_series.vol - equal_weight_portfolio.vol) * 100,
'Sharpe Improvement': optimal_portfolio_series.ret_vol_ratio - equal_weight_portfolio.ret_vol_ratio,
}
print("\n=== OPTIMIZATION IMPROVEMENTS ===")
for metric, value in improvement.items():
print(f"{metric}: {value:+.2f}")
Complete Optimization Workflow
Here’s how to perform portfolio optimization using openseries methods directly:
# Example: Optimize ETF portfolio using openseries methods
etf_tickers = ["VTI", "VEA", "VWO", "BND", "VNQ"]
# Load data using openseries methods
assets = []
for ticker in etf_tickers:
# This may fail if the ticker is invalid or data unavailable
data = yf.Ticker(ticker).history(period="5y")
series = OpenTimeSeries.from_df(dframe=data['Close'])
series.set_new_label(lvl_zero=ticker)
assets.append(series)
if len(assets) < 2:
print("Need at least 2 assets for optimization")
else:
frame = OpenFrame(constituents=assets)
# Use openseries native weight strategies
strategies = {
'Equal Weight': 'eq_weights',
'Inverse Volatility': 'inv_vol',
'Max Diversification': 'max_div',
'Min Vol Overweight': 'min_vol_overweight'
}
# Create portfolios using openseries make_portfolio method
results = {}
for name, weight_strat in strategies.items():
# This may fail with MaxDiversificationNaNError, MaxDiversificationNegativeWeightsError, or other exceptions
portfolio_df = frame.make_portfolio(name=name, weight_strat=weight_strat)
portfolio = OpenTimeSeries.from_df(dframe=portfolio_df)
results[name] = {
'Return': portfolio.geo_ret,
'Volatility': portfolio.vol,
'Sharpe': portfolio.ret_vol_ratio,
'Max Drawdown': portfolio.max_drawdown
}
print("=== PORTFOLIO OPTIMIZATION RESULTS ===")
for name, metrics in results.items():
print(f"\n{name}:")
print(f" Return: {metrics['Return']*100:.2f}%")
print(f" Volatility: {metrics['Volatility']*100:.2f}%")
print(f" Sharpe: {metrics['Sharpe']:.2f}")
print(f" Max Drawdown: {metrics['Max Drawdown']*100:.2f}%")