Equal Weight Portfolio
Implement a simple equal-weight portfolio as a surprisingly powerful and robust allocation benchmark, rigorously comparing its out-of-sample risk-adjusted performance against far more complex optimization-based methodologies across multiple market regimes and time periods.
Equal Weight Portfolio — Portfolio Construction
Category: Portfolio | Subcategory: Construction
What This Notebook Does
Equal weight (1/N) is the simplest possible portfolio: allocate the same dollar amount to every asset. Despite its simplicity, research by DeMiguel et al. (2009) showed that 1/N often outperforms sophisticated optimisation methods out-of-sample because it avoids estimation error.
Key properties:
- No estimation required — no expected returns, no covariance matrix needed
- Maximum diversification by count (but not by risk)
- Rebalancing drag — frequent rebalancing back to 1/N can hurt in trending markets
This notebook:
- Fetches price data (Yahoo Finance or synthetic) for a crypto portfolio
- Constructs the equal-weight portfolio and tracks its equity curve
- Analyses drift — how weights deviate from 1/N over time without rebalancing
- Compares monthly rebalanced vs buy-and-hold equal-weight
- Computes standard performance metrics: Sharpe, Sortino, max drawdown
- Visualises weight drift, equity curves, and rolling Sharpe
- Exports results
!pip install numpy pandas matplotlib seaborn scipy yfinance --quietimport numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline
plt.rcParams['figure.figsize'] = (14, 5)
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.right'] = False
print('Imports ready.')Imports ready.
Section 1 — Configuration
Set USE_LIVE_DATA = True to pull real OHLC data from Yahoo Finance (free, no API key). The tickers use Yahoo's -USD suffix for crypto. REBAL_FREQ controls how often the portfolio is reset to equal weights — 'ME' means month-end.
# ── Data Source Toggle ─────────────────────────────
USE_LIVE_DATA = False # Set True to fetch from Yahoo Finance (no API key needed)
TICKERS = ['BTC-USD','ETH-USD','SOL-USD','BNB-USD','AVAX-USD','MATIC-USD']
START_DATE = '2022-01-01'
END_DATE = '2024-12-31'
# ──────────────────────────────────────────────────
ASSETS = ['BTC','ETH','SOL','BNB','AVAX','MATIC']
REBAL_FREQ = 'ME' # month-end rebalancing
RF_ANNUAL = 0.04
print('Config ready.')Config ready.
Section 2 — Data Acquisition
When USE_LIVE_DATA = True, yfinance.download() fetches adjusted closing prices directly from Yahoo Finance — no API key required. The synthetic path simulates correlated crypto returns using a Cholesky decomposition of a realistic covariance matrix, ensuring the data has properties (fat tails, positive correlation) similar to real crypto assets.
if USE_LIVE_DATA:
import yfinance as yf
prices = yf.download(TICKERS, start=START_DATE, end=END_DATE)['Close']
prices.columns = ASSETS
prices.dropna(inplace=True)
print(f'Live data loaded: {len(prices)} trading days')
else:
rng = np.random.default_rng(42)
n = 730
vols = np.array([0.65, 0.75, 1.20, 0.70, 1.10, 1.40]) / np.sqrt(252)
corr = np.array([
[1.00, 0.85, 0.70, 0.75, 0.65, 0.60],
[0.85, 1.00, 0.75, 0.70, 0.68, 0.62],
[0.70, 0.75, 1.00, 0.65, 0.72, 0.75],
[0.75, 0.70, 0.65, 1.00, 0.60, 0.55],
[0.65, 0.68, 0.72, 0.60, 1.00, 0.78],
[0.60, 0.62, 0.75, 0.55, 0.78, 1.00]])
cov = np.outer(vols, vols) * corr
L = np.linalg.cholesky(cov)
mu = np.array([0.50, 0.45, 0.80, 0.35, 0.70, 0.90]) / 252
rets_arr = rng.standard_normal((n, len(ASSETS))) @ L.T + mu
idx = pd.date_range('2022-01-01', periods=n, freq='B')
returns = pd.DataFrame(rets_arr, columns=ASSETS, index=idx)
prices = (1 + returns).cumprod() * 100
print(f'Synthetic data: {len(prices)} days')
returns = prices.pct_change().dropna()
print(f'Return matrix shape: {returns.shape}')Synthetic data: 730 days Return matrix shape: (729, 6)
Section 3 — Equal-Weight Portfolio Construction
The daily equal-weight portfolio return is simply the average of all asset returns: r_p = mean(r_i). For the rebalanced version, we reset weights to 1/N on every month-end date and let them drift in between. This captures the realistic cost and benefit of rebalancing — selling winners, buying losers.
n_assets = len(ASSETS)
w_ew = np.ones(n_assets) / n_assets
# Buy-and-hold equal weight (weights drift freely)
port_bah = returns.mean(axis=1)
# Monthly rebalanced equal weight
rebal_dates = returns.resample(REBAL_FREQ).last().index
daily_rets = []
current_w = w_ew.copy()
for date, row in returns.iterrows():
if date in rebal_dates:
current_w = w_ew.copy()
daily_rets.append(float(current_w @ row.values))
# Update weights for drift
current_w = current_w * (1 + row.values)
if current_w.sum() > 0:
current_w /= current_w.sum()
port_rebal = pd.Series(daily_rets, index=returns.index)
equity_bah = (1 + port_bah).cumprod()
equity_rebal = (1 + port_rebal).cumprod()
print(f'Buy-and-hold total return: {(equity_bah.iloc[-1]-1):.1%}')
print(f'Monthly rebalanced return: {(equity_rebal.iloc[-1]-1):.1%}')Buy-and-hold total return: -68.4% Monthly rebalanced return: -64.9%
Section 4 — Performance Metrics
We compute the standard set of risk-adjusted performance metrics. The Sharpe ratio uses an annualisation factor of √252 (trading days). Sortino only penalises downside deviation — days where returns fall below zero — which is more relevant for assets like crypto where large positive skewness can inflate the Sharpe.
def performance_metrics(rets: pd.Series, rf: float = RF_ANNUAL / 252) -> dict:
"""
Compute annualised performance metrics for a daily return series.
Parameters
----------
rets : pd.Series Daily return series.
rf : float Daily risk-free rate.
Returns
-------
dict Annual return, volatility, Sharpe, Sortino, max drawdown.
"""
ann_ret = rets.mean() * 252
ann_vol = rets.std() * np.sqrt(252)
sharpe = (rets.mean() - rf) / (rets.std() + 1e-9) * np.sqrt(252)
down = rets[rets < 0].std() + 1e-9
sortino = (rets.mean() - rf) / down * np.sqrt(252)
eq = (1 + rets).cumprod()
drawdown = ((eq - eq.cummax()) / eq.cummax()).min()
return {'Ann Return': f'{ann_ret:.1%}', 'Ann Vol': f'{ann_vol:.1%}',
'Sharpe': f'{sharpe:.2f}', 'Sortino': f'{sortino:.2f}',
'Max DD': f'{drawdown:.1%}'}
for name, rets in [('Buy-and-Hold EW', port_bah), ('Monthly Rebalanced EW', port_rebal)]:
m = performance_metrics(rets)
print(f'{name}: {m}')Buy-and-Hold EW: {'Ann Return': '-6.2%', 'Ann Vol': '81.8%', 'Sharpe': '-0.12', 'Sortino': '-0.20', 'Max DD': '-93.3%'}
Monthly Rebalanced EW: {'Ann Return': '-3.0%', 'Ann Vol': '81.3%', 'Sharpe': '-0.09', 'Sortino': '-0.14', 'Max DD': '-92.9%'}
Section 5 — Weight Drift Analysis
Without rebalancing, high-return assets grow to dominate the portfolio. This plot shows how far each asset's weight drifts from the initial 1/N target over the full holding period. A large drift means the portfolio has become concentrated in a few winners — a hidden risk that rebalancing prevents.
cumulative = (1 + returns).cumprod()
drifted_w = cumulative.div(cumulative.sum(axis=1), axis=0)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
fig.suptitle('Equal Weight Portfolio Analysis', fontsize=13, fontweight='bold')
ax1 = axes[0]
for asset in ASSETS:
ax1.plot(drifted_w.index, drifted_w[asset], lw=1.2, label=asset)
ax1.axhline(1/n_assets, color='black', ls='--', lw=1, label=f'Target 1/N = {1/n_assets:.2f}')
ax1.set_ylabel('Portfolio Weight')
ax1.legend(fontsize=7, ncol=2)
ax1.set_title('Weight Drift Without Rebalancing')
ax2 = axes[1]
ax2.plot(equity_bah.index, equity_bah, label='Buy-and-Hold EW', color='#9e9e9e', lw=1.5)
ax2.plot(equity_rebal.index, equity_rebal, label='Monthly Rebalanced EW', color='#1976d2', lw=1.5)
ax2.set_ylabel('Growth of $1')
ax2.legend(fontsize=8)
ax2.set_title('Rebalanced vs Buy-and-Hold Equity Curve')
plt.tight_layout(); plt.show()Section 6 — Export
Save the daily portfolio returns for both strategies and the weight drift history to CSV for downstream use in backtesting or reporting notebooks.
out = pd.DataFrame({'bah_return': port_bah, 'rebal_return': port_rebal})
out.to_csv('equal_weight_portfolio.csv')
drifted_w.to_csv('equal_weight_drift.csv')
print('Saved: equal_weight_portfolio.csv, equal_weight_drift.csv')Saved: equal_weight_portfolio.csv, equal_weight_drift.csv
Conclusion
This notebook demonstrates the construction and analysis of an equal-weight portfolio strategy, comparing a buy-and-hold approach with a monthly rebalanced version. We've seen how weights can drift significantly without rebalancing, and how rebalancing can impact overall portfolio performance and risk metrics. The analysis provides insights into the trade-offs between simplicity, diversification, and rebalancing frequency in portfolio management.