{"id":100,"date":"2025-04-29T14:23:17","date_gmt":"2025-04-29T21:23:17","guid":{"rendered":"http:\/\/www.ashwang.net\/?p=100"},"modified":"2025-04-29T14:24:23","modified_gmt":"2025-04-29T21:24:23","slug":"rolling-auto-rebalanced-mve-portfolio","status":"publish","type":"post","link":"http:\/\/www.ashwang.net\/index.php\/2025\/04\/29\/rolling-auto-rebalanced-mve-portfolio\/","title":{"rendered":"Rolling Auto-rebalanced MVE portfolio"},"content":{"rendered":"\n<p><strong>Strategy description:<\/strong><br>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<br>alpha and beta), full-period backtesting against the S&amp;P 500, and robustness testing using random sampling.<\/p>\n\n\n\n<p><strong>Introduction<\/strong><br>In today&#8217;s dynamic markets, investors demand a portfolio strategy that adapts<br>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.<\/p>\n\n\n\n<p><strong>Step 1: Data Preparation<\/strong><br>We collect 5 years of monthly returns from a group of different assets, including:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>17 selected high-momentum stocks,<\/li>\n\n\n\n<li>Gold,<\/li>\n\n\n\n<li>Bitcoin,<\/li>\n\n\n\n<li>A bond ETF.<br>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&#8217;s data, we calculate the momentum (past 12-month return) for each stock<br>and select the stocks with the highest momentum to construct our portfolio.<br><strong>The additional assets are included for the following reasons:<\/strong><\/li>\n\n\n\n<li>Gold is added because it typically moves inversely with the stock market, providing a hedge during market downturns.<\/li>\n\n\n\n<li>Bitcoin is included due to its high return potential, offering upside compensation during weak market periods.<\/li>\n\n\n\n<li>Bond ETF is used to represent a risk-free component, essential for constructing an MVE portfolio with a full risk-return spectrum.<br><strong>Step 2: Rolling Estimation and MVE Optimization<\/strong><br>Using the monthly returns of the 20 selected assets (17 stocks + Gold + Bitcoin + Bond ETF),we:<\/li>\n\n\n\n<li>Calculate the monthly returns and covariance matrices.<\/li>\n\n\n\n<li>Multiply the expected returns and the inverse covariance matrices to solve for the Mean<br>Variance Efficient (MVE) portfolio weights.<\/li>\n\n\n\n<li>The optimization finds the set of asset weights that maximize expected return for a given<br>level of portfolio risk, with allowing short selling.<\/li>\n\n\n\n<li>Rebalance the portfolio rolling by each month.<br><strong>Step 3: OLS Regression Analysis for Portfolio Evaluation<\/strong><br>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:<\/li>\n\n\n\n<li>Alpha: Measures the portfolio&#8217;s abnormal return that is not explained by market movements.<\/li>\n\n\n\n<li>Beta (Market Exposure): Measures the portfolio&#8217;s sensitivity to overall market fluctuations.<br><strong>Step 4: Cumulative return V.S. SPY 500<\/strong><br>To evaluate the strategy\u2019s overall effectiveness, we backtest the portfolio performance over the full 5-year period.<\/li>\n\n\n\n<li>We compare the cumulative returns of the Rolling Auto Rebalanced MVE Portfolio against the S&amp;P 500 index.<\/li>\n\n\n\n<li>The comparison allows us to see if the strategy delivers higher returns or better risk adjusted returns than simply investing in the broad market.<br><strong>Step 5: Robust Test<\/strong><br>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.<\/li>\n\n\n\n<li>We repeat the sampling process multiple times to ensure consistency.<\/li>\n\n\n\n<li>We plot the distribution of sampled portfolio returns to visualize the overall return pattern and volatility.<br><strong>Conclusion<\/strong><br>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&amp;P 500, and robustness testing through random sampling, and building a portfolio of high-momentum stocks,<br>gold, bitcoin, and bonds, it gives a simple but strong solution to achieve stable and efficient returns.<\/li>\n<\/ul>\n\n\n\n<h4 class=\"wp-block-heading\">Code Part<\/h4>\n\n\n\n<p><strong>Get Data<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import yfinance as yf\nimport pandas as pd\n\n# Define the tickers\ntickers = &#91;\n    \"META\", \"NVDA\", \"AVGO\", \"LLY\", \"JPM\", \"NFLX\", \"GE\", \"MRK\", \"ORCL\",\n    \"WMT\", \"NOW\", \"TXN\", \"CRM\", \"CAT\", \"GOOGL\", \"PLTR\", \"HAS\",\n    \"GLD\",  # SPDR Gold Shares ETF\n    \"BTC-USD\",  # Bitcoin\n    \"SHY\"  # iShares 1-3 Year Treasury Bond ETF as a proxy for risk-free rate\n]\n\n# Download 5 years of monthly historical adjusted close prices\ndata = yf.download(tickers, start=\"2020-04-01\", end=\"2025-04-01\", interval=\"1mo\")&#91;\"Close\"]\n\n# Drop rows with missing values (optional, to ensure full data coverage)\ndata.dropna(inplace=True)\n\n# Save to CSV\ncsv_path = \"portfolio_5y_monthly_prices.csv\"\ndata.to_csv(csv_path)\n\ndata.head()<\/code><\/pre>\n\n\n\n<p><strong>Construct Rolling Monthly MVE Portfolio<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import numpy as np\nimport statsmodels.api as sm\n\n# Load price data\ndata = pd.read_csv(\"portfolio_5y_monthly_prices.csv\", index_col=0, parse_dates=True)\nmonthly_returns = data.pct_change().dropna()\n\n# Rolling MVE Portfolio (Auto-balanced monthly)\nmomentum_portfolio_returns = pd.Series(index=monthly_returns.index)\n\nfor i in range(12, len(monthly_returns)):\n    current_date = monthly_returns.index&#91;i]\n    rolling_returns = monthly_returns.iloc&#91;i-12:i]  # 12-month lookback\n\n    mu = rolling_returns.mean()\n    cov = rolling_returns.cov()\n    inv_cov = np.linalg.inv(cov)\n    ones = np.ones(len(mu))\n    weights_mve = inv_cov @ mu\n    weights_mve \/= ones @ weights_mve\n\n    current_month_returns = monthly_returns.iloc&#91;i]\n    momentum_portfolio_returns&#91;current_date] = (current_month_returns * weights_mve).sum()\n\nmomentum_portfolio_returns.dropna(inplace=True)\n\n# Download SPY market return\nspy = yf.download(\"SPY\",\n                  start=momentum_portfolio_returns.index&#91;0].strftime('%Y-%m-%d'),\n                  end=momentum_portfolio_returns.index&#91;-1].strftime('%Y-%m-%d'),\n                  interval=\"1mo\")&#91;\"Close\"]\nspy = spy.pct_change().dropna()\nspy = spy&#91;spy.index.isin(momentum_portfolio_returns.index)]\nmomentum_portfolio_returns = momentum_portfolio_returns&#91;spy.index]\n\n# Recompute MVE weights for second-to-last month\nlookback_returns = monthly_returns.loc&#91;monthly_returns.index&#91;-14:-2]]\nmu = lookback_returns.mean()\ncov = lookback_returns.cov()\ninv_cov = np.linalg.inv(cov)\nones = np.ones(len(mu))\nfinal_weights = inv_cov @ mu\nfinal_weights \/= ones @ final_weights\n\n# Portfolio using static weights from second-to-last month \nfinal_portfolio_returns = (monthly_returns @ final_weights).dropna()\nspy_final = spy&#91;spy.index.isin(final_portfolio_returns.index)]\nfinal_portfolio_returns = final_portfolio_returns&#91;spy_final.index]\n\nX = sm.add_constant(spy_final)\nmodel = sm.OLS(final_portfolio_returns, X).fit()\nprint(model.summary())\nprint(f'Last Five Months Portfolio Return:\\n{final_portfolio_returns.tail(5)}')\nprint(f'Portfolio Volatility: {final_portfolio_returns.std():.4f}')\nsharpe_ratio_annualized = (final_portfolio_returns.mean() * 12) \/ (final_portfolio_returns.std() * np.sqrt(12))\nprint(f'Annualized Sharpe Ratio: {sharpe_ratio_annualized:.4f}')<\/code><\/pre>\n\n\n\n<p><strong>Cumulative Return V.S. SPY<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import matplotlib.pyplot as plt\n\n\n# Ensure proper alignment and remove missing values\ncommon_index = momentum_portfolio_returns.index.intersection(spy.index)\naligned_portfolio = momentum_portfolio_returns.loc&#91;common_index]\naligned_spy = spy.loc&#91;common_index]\n\n# Compute cumulative returns\ncumulative_portfolio = (1 + aligned_portfolio).cumprod()\ncumulative_spy = (1 + aligned_spy).cumprod()\n\n# Plot\nplt.figure(figsize=(12, 6))\nplt.plot(cumulative_portfolio.index, cumulative_portfolio, label='Rolling Balanced Portfolio')\nplt.plot(cumulative_spy.index, cumulative_spy, label='SPY (Market)', linestyle='--', color='orange')\nplt.title('Cumulative Return: Monthly Rebalanced Momentum Portfolio vs Market')\nplt.xlabel('Date')\nplt.ylabel('Cumulative Return')\nplt.legend()\nplt.grid(True)\nplt.tight_layout()\nplt.show()\n\n<\/code><\/pre>\n\n\n\n<p><strong>Robust Test<\/strong><br><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>np.random.seed(42)  # Reproducibility\noverall_final_returns = &#91;]  # Store final_returns from each of the 100 repetitions\nnum_repetitions = 100  # Repeat the 100-trial simulation 100 times\nnum_trials = 100\nwindow_size = 30  # months\n\nfor repetition in range(num_repetitions):\n    final_returns = &#91;]  # Reset final_returns for each repetition\n    \n    for trial in range(num_trials):\n        start_idx = np.random.randint(12, len(monthly_returns) - window_size)\n        end_idx = start_idx + window_size\n\n        trial_returns = pd.Series(index=monthly_returns.index&#91;start_idx:end_idx])\n\n        for i in range(start_idx + 12, end_idx):\n            current_date = monthly_returns.index&#91;i]\n            lookback_date = monthly_returns.index&#91;i - 12]\n            past_momentum = (data.loc&#91;current_date] \/ data.loc&#91;lookback_date]) - 1\n            top_17 = past_momentum.nlargest(17).index\n            weights = pd.Series(1\/17, index=top_17)\n            current_month_returns = monthly_returns.iloc&#91;i]\n            trial_returns&#91;current_date] = (current_month_returns&#91;top_17] * weights).sum()\n\n        # Final cumulative return for the window\n        cumulative_return = (1 + trial_returns.dropna()).prod() - 1\n        final_returns.append(cumulative_return)\n    \n    overall_final_returns.extend(final_returns)  # Add all 100 trial results into overall\n\n# Plot histogram of 100 x 100 = 10,000 \nplt.figure(figsize=(10, 6))\nplt.hist(overall_final_returns, bins=30, edgecolor='black', alpha=0.75)\nplt.title(\"Histogram of Final Cumulative Returns\\n(100x100 Random 30-Month Auto Balanced Portfolios)\")\nplt.xlabel(\"Cumulative Return\")\nplt.ylabel(\"Frequency\")\nplt.grid(True)\nplt.tight_layout()\nplt.show()\n\n<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>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 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-100","post","type-post","status-publish","format-standard","hentry","category-tutorial"],"_links":{"self":[{"href":"http:\/\/www.ashwang.net\/index.php\/wp-json\/wp\/v2\/posts\/100","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/www.ashwang.net\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/www.ashwang.net\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/www.ashwang.net\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/www.ashwang.net\/index.php\/wp-json\/wp\/v2\/comments?post=100"}],"version-history":[{"count":2,"href":"http:\/\/www.ashwang.net\/index.php\/wp-json\/wp\/v2\/posts\/100\/revisions"}],"predecessor-version":[{"id":102,"href":"http:\/\/www.ashwang.net\/index.php\/wp-json\/wp\/v2\/posts\/100\/revisions\/102"}],"wp:attachment":[{"href":"http:\/\/www.ashwang.net\/index.php\/wp-json\/wp\/v2\/media?parent=100"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/www.ashwang.net\/index.php\/wp-json\/wp\/v2\/categories?post=100"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/www.ashwang.net\/index.php\/wp-json\/wp\/v2\/tags?post=100"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}