Rolling Auto-rebalanced MVE portfolio

Strategy description:
We construct a dynamic Mean-Variance Efficient (MVE) portfolio using 5 years of monthly returns from 20 assets, including 17 high-momentum stocks, gold, bitcoin, and a bond ETF. We use a rolling 60-month window to estimate returns and covariance matrices, solving for optimal portfolio weights monthly without allowing short selling. The portfolio is continuously rebalanced to stay on the efficient frontier. Evaluation includes OLS regression (to estimate
alpha and beta), full-period backtesting against the S&P 500, and robustness testing using random sampling.

Introduction
In today’s dynamic markets, investors demand a portfolio strategy that adapts
systematically to evolving conditions while maintaining optimal risk-adjusted returns. The Rolling Auto Rebalanced MVE Portfolio meets this demand. It automates the process of constructing a Mean-Variance Efficient (MVE) portfolio on a rolling basis, ensuring that allocations remain efficient over time without manual intervention. This strategy is designed for investors seeking consistent portfolio growth with minimized drift.

Step 1: Data Preparation
We collect 5 years of monthly returns from a group of different assets, including:

  • 17 selected high-momentum stocks,
  • Gold,
  • Bitcoin,
  • A bond ETF.
    First, we clean the data, fix missing values, make sure the time frequency is correct, and adjust for dividends and stock splits. From the final year’s data, we calculate the momentum (past 12-month return) for each stock
    and select the stocks with the highest momentum to construct our portfolio.
    The additional assets are included for the following reasons:
  • Gold is added because it typically moves inversely with the stock market, providing a hedge during market downturns.
  • Bitcoin is included due to its high return potential, offering upside compensation during weak market periods.
  • Bond ETF is used to represent a risk-free component, essential for constructing an MVE portfolio with a full risk-return spectrum.
    Step 2: Rolling Estimation and MVE Optimization
    Using the monthly returns of the 20 selected assets (17 stocks + Gold + Bitcoin + Bond ETF),we:
  • Calculate the monthly returns and covariance matrices.
  • Multiply the expected returns and the inverse covariance matrices to solve for the Mean
    Variance Efficient (MVE) portfolio weights.
  • The optimization finds the set of asset weights that maximize expected return for a given
    level of portfolio risk, with allowing short selling.
  • Rebalance the portfolio rolling by each month.
    Step 3: OLS Regression Analysis for Portfolio Evaluation
    After building the portfolio, we collect the most recent one-month return data for the portfolio. We then perform an Ordinary Least Squares (OLS) regression, using the portfolio returns as the dependent variable and the market returns as the independent variable. Through this regression, we obtain two important evaluation metrics:
  • Alpha: Measures the portfolio’s abnormal return that is not explained by market movements.
  • Beta (Market Exposure): Measures the portfolio’s sensitivity to overall market fluctuations.
    Step 4: Cumulative return V.S. SPY 500
    To evaluate the strategy’s overall effectiveness, we backtest the portfolio performance over the full 5-year period.
  • We compare the cumulative returns of the Rolling Auto Rebalanced MVE Portfolio against the S&P 500 index.
  • The comparison allows us to see if the strategy delivers higher returns or better risk adjusted returns than simply investing in the broad market.
    Step 5: Robust Test
    To further test the robustness of the strategy, we randomly sample 30 months of data from the full return series and calculate the portfolio returns based on these randomly selected months.
  • We repeat the sampling process multiple times to ensure consistency.
  • We plot the distribution of sampled portfolio returns to visualize the overall return pattern and volatility.
    Conclusion
    The Rolling Auto Rebalanced MVE Portfolio keeps mean-variance efficiency by adjusting itself to the market automatically. By combining rolling estimations, MVE optimization, OLS-based portfolio evaluation, backtesting comparison with the S&P 500, and robustness testing through random sampling, and building a portfolio of high-momentum stocks,
    gold, bitcoin, and bonds, it gives a simple but strong solution to achieve stable and efficient returns.

Code Part

Get Data

import yfinance as yf
import pandas as pd

# Define the tickers
tickers = [
    "META", "NVDA", "AVGO", "LLY", "JPM", "NFLX", "GE", "MRK", "ORCL",
    "WMT", "NOW", "TXN", "CRM", "CAT", "GOOGL", "PLTR", "HAS",
    "GLD",  # SPDR Gold Shares ETF
    "BTC-USD",  # Bitcoin
    "SHY"  # iShares 1-3 Year Treasury Bond ETF as a proxy for risk-free rate
]

# Download 5 years of monthly historical adjusted close prices
data = yf.download(tickers, start="2020-04-01", end="2025-04-01", interval="1mo")["Close"]

# Drop rows with missing values (optional, to ensure full data coverage)
data.dropna(inplace=True)

# Save to CSV
csv_path = "portfolio_5y_monthly_prices.csv"
data.to_csv(csv_path)

data.head()

Construct Rolling Monthly MVE Portfolio

import numpy as np
import statsmodels.api as sm

# Load price data
data = pd.read_csv("portfolio_5y_monthly_prices.csv", index_col=0, parse_dates=True)
monthly_returns = data.pct_change().dropna()

# Rolling MVE Portfolio (Auto-balanced monthly)
momentum_portfolio_returns = pd.Series(index=monthly_returns.index)

for i in range(12, len(monthly_returns)):
    current_date = monthly_returns.index[i]
    rolling_returns = monthly_returns.iloc[i-12:i]  # 12-month lookback

    mu = rolling_returns.mean()
    cov = rolling_returns.cov()
    inv_cov = np.linalg.inv(cov)
    ones = np.ones(len(mu))
    weights_mve = inv_cov @ mu
    weights_mve /= ones @ weights_mve

    current_month_returns = monthly_returns.iloc[i]
    momentum_portfolio_returns[current_date] = (current_month_returns * weights_mve).sum()

momentum_portfolio_returns.dropna(inplace=True)

# Download SPY market return
spy = yf.download("SPY",
                  start=momentum_portfolio_returns.index[0].strftime('%Y-%m-%d'),
                  end=momentum_portfolio_returns.index[-1].strftime('%Y-%m-%d'),
                  interval="1mo")["Close"]
spy = spy.pct_change().dropna()
spy = spy[spy.index.isin(momentum_portfolio_returns.index)]
momentum_portfolio_returns = momentum_portfolio_returns[spy.index]

# Recompute MVE weights for second-to-last month
lookback_returns = monthly_returns.loc[monthly_returns.index[-14:-2]]
mu = lookback_returns.mean()
cov = lookback_returns.cov()
inv_cov = np.linalg.inv(cov)
ones = np.ones(len(mu))
final_weights = inv_cov @ mu
final_weights /= ones @ final_weights

# Portfolio using static weights from second-to-last month 
final_portfolio_returns = (monthly_returns @ final_weights).dropna()
spy_final = spy[spy.index.isin(final_portfolio_returns.index)]
final_portfolio_returns = final_portfolio_returns[spy_final.index]

X = sm.add_constant(spy_final)
model = sm.OLS(final_portfolio_returns, X).fit()
print(model.summary())
print(f'Last Five Months Portfolio Return:\n{final_portfolio_returns.tail(5)}')
print(f'Portfolio Volatility: {final_portfolio_returns.std():.4f}')
sharpe_ratio_annualized = (final_portfolio_returns.mean() * 12) / (final_portfolio_returns.std() * np.sqrt(12))
print(f'Annualized Sharpe Ratio: {sharpe_ratio_annualized:.4f}')

Cumulative Return V.S. SPY

import matplotlib.pyplot as plt


# Ensure proper alignment and remove missing values
common_index = momentum_portfolio_returns.index.intersection(spy.index)
aligned_portfolio = momentum_portfolio_returns.loc[common_index]
aligned_spy = spy.loc[common_index]

# Compute cumulative returns
cumulative_portfolio = (1 + aligned_portfolio).cumprod()
cumulative_spy = (1 + aligned_spy).cumprod()

# Plot
plt.figure(figsize=(12, 6))
plt.plot(cumulative_portfolio.index, cumulative_portfolio, label='Rolling Balanced Portfolio')
plt.plot(cumulative_spy.index, cumulative_spy, label='SPY (Market)', linestyle='--', color='orange')
plt.title('Cumulative Return: Monthly Rebalanced Momentum Portfolio vs Market')
plt.xlabel('Date')
plt.ylabel('Cumulative Return')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()

Robust Test

np.random.seed(42)  # Reproducibility
overall_final_returns = []  # Store final_returns from each of the 100 repetitions
num_repetitions = 100  # Repeat the 100-trial simulation 100 times
num_trials = 100
window_size = 30  # months

for repetition in range(num_repetitions):
    final_returns = []  # Reset final_returns for each repetition
    
    for trial in range(num_trials):
        start_idx = np.random.randint(12, len(monthly_returns) - window_size)
        end_idx = start_idx + window_size

        trial_returns = pd.Series(index=monthly_returns.index[start_idx:end_idx])

        for i in range(start_idx + 12, end_idx):
            current_date = monthly_returns.index[i]
            lookback_date = monthly_returns.index[i - 12]
            past_momentum = (data.loc[current_date] / data.loc[lookback_date]) - 1
            top_17 = past_momentum.nlargest(17).index
            weights = pd.Series(1/17, index=top_17)
            current_month_returns = monthly_returns.iloc[i]
            trial_returns[current_date] = (current_month_returns[top_17] * weights).sum()

        # Final cumulative return for the window
        cumulative_return = (1 + trial_returns.dropna()).prod() - 1
        final_returns.append(cumulative_return)
    
    overall_final_returns.extend(final_returns)  # Add all 100 trial results into overall

# Plot histogram of 100 x 100 = 10,000 
plt.figure(figsize=(10, 6))
plt.hist(overall_final_returns, bins=30, edgecolor='black', alpha=0.75)
plt.title("Histogram of Final Cumulative Returns\n(100x100 Random 30-Month Auto Balanced Portfolios)")
plt.xlabel("Cumulative Return")
plt.ylabel("Frequency")
plt.grid(True)
plt.tight_layout()
plt.show()


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *