Most investors chase sectors after they've already moved. A sector rotation model flips that: it downloads price data for 11 sector ETFs, calculates which ones are outperforming the market across multiple time periods, and ranks them before the crowd notices. You can build one in Python using yfinance and pandas in under 100 lines.
Key Takeaways
- Download adjusted close prices for sector ETFs using yfinance — no paid data feed required
- Calculate relative strength by comparing each sector's returns against a benchmark over 1-month, 3-month, and 6-month periods
- Generate a composite momentum score that updates automatically when you rerun the script with fresh data
Before You Start
This guide assumes you know what sector rotation means: shifting portfolio weight toward sectors showing relative strength and away from lagging ones. You should be comfortable running Python scripts locally or in Jupyter notebooks. Familiarity with pandas DataFrames and basic financial metrics like percentage returns will help. You don't need paid market data — yfinance pulls free historical prices from Yahoo Finance. That 15-minute delay won't matter if you're rebalancing monthly, not day-trading.
What You Need
- Python 3.8 or later
- The yfinance library:
pip install yfinance - The pandas library:
pip install pandas - Internet connection for price data
- Text editor or Jupyter notebook
Step 1: Install Required Libraries and Set Up Your Script
Open terminal and install both libraries: pip install yfinance pandas. Create a new file called sector_rotation.py or open a Jupyter notebook. Import at the top:
import yfinance as yf
import pandas as pd
This gives you the tools to fetch data and manipulate it in tabular form. Nothing exotic.
Step 2: Define the Sector ETFs and Benchmark
Create a dictionary mapping sector names to SPDR Sector ETF tickers. The 11 SPDR funds cover the entire S&P 500: XLC (Communication Services), XLY (Consumer Discretionary), XLP (Consumer Staples), XLE (Energy), XLF (Financials), XLV (Health Care), XLI (Industrials), XLB (Materials), XLRE (Real Estate), XLK (Technology), XLU (Utilities). Add a benchmark ticker — SPY for the S&P 500. This dictionary drives your data download loop and makes results readable.
Step 3: Download Historical Price Data with yfinance
Use yf.download() to pull adjusted close prices for all tickers at once. Specify tickers as a list, set a start date — one year ago gives sufficient lookback — and set end to today:
prices = yf.download(ticker_list, start='2024-01-15')['Adj Close']
The function returns a pandas DataFrame with columns for each ticker. You've just fetched all sector and benchmark prices in one call. What matters: the 'Adj Close' column, which accounts for splits and dividends. Unadjusted prices will wreck your momentum calculations.
Step 4: Calculate Percentage Returns Over Multiple Periods
Relative strength models compare performance across multiple time horizons to avoid overfitting to a single trend. Calculate 1-month, 3-month, and 6-month returns using pandas' pct_change() with a periods parameter:
returns_1m = prices.pct_change(periods=21) # 21 trading days ≈ 1 month
returns_3m = prices.pct_change(periods=63) # 63 trading days ≈ 3 months
returns_6m = prices.pct_change(periods=126) # 126 trading days ≈ 6 months
These percentage changes form the raw material for relative strength scoring. The interesting part comes next.
Step 5: Compute Relative Strength vs. Benchmark
Subtract the benchmark's return from each sector's return for each period. If $SPY rose 5% over three months and $XLK rose 8%, the relative strength is +3%. Use pandas broadcasting:
rel_strength_3m = returns_3m.sub(returns_3m['SPY'], axis=0)
Repeat for all three periods. This isolates sector-specific momentum and removes broad market drift — critical for rotation decisions. A sector that gained 12% sounds impressive until you learn the market gained 15%. Relative strength strips out that illusion.
Step 6: Create a Composite Momentum Score
Combine the relative strength scores from all three periods into a single ranking metric. Equal weighting is the neutral starting point:
composite = (rel_strength_1m + rel_strength_3m + rel_strength_6m) / 3
Alternatively, weight recent periods more heavily if you prefer responsiveness over stability. The composite score gives each sector a single number representing its average outperformance or underperformance relative to the benchmark. One number. Clean signal.
Step 7: Rank Sectors and Generate Output
Take the most recent row from your composite DataFrame — the latest trading day — and sort sectors by composite scores descending:
ranked = composite.iloc[-1].sort_values(ascending=False)
print(ranked)
The top-ranked sectors are showing the strongest relative momentum. The bottom-ranked are lagging. You now have a sector rotation signal that updates automatically when you rerun the script with fresh data.
Common Problems
Missing data for recent dates: Yahoo Finance delays ETF data by 15 minutes during market hours. If your script runs intraday and returns NaN values, wait until after market close or use dropna() to remove incomplete rows before calculating returns.
Index alignment errors: If you see a KeyError when subtracting benchmark returns, verify the benchmark ticker exists in your prices DataFrame. Check column names with prices.columns and confirm 'SPY' appears.
Lookback period too short: Calculating 6-month returns on only 3 months of data produces mostly NaN values. Ensure your start date provides at least 6 months + 1 week of history. If you get a DataFrame full of NaN, extend the start date backward.
Best Practices
- Run the script after market close to avoid incomplete trading-day data
- Store historical rankings in a CSV each time you run the script — this creates a time series of rotation signals you can backtest
- Consider adding a volatility filter: exclude sectors with unusually high standard deviation of returns to avoid chasing unstable moves
- Use
yf.download()withauto_adjust=Trueto simplify data cleaning — it automatically applies dividend and split adjustments - Normalize composite scores with z-scores if you plan to combine this model with other quantitative signals — raw percentage differences vary widely in magnitude
When Not to Use This
This approach works for medium-term tactical allocation — rebalancing monthly or quarterly. It is not designed for day trading. Daily sector ETF price moves are heavily influenced by intraday noise rather than sustained trends. Avoid using this model during extreme market dislocations when correlations across all sectors approach 1.0 and relative strength signals break down. If you need real-time rotation signals, yfinance's free data has a 15-minute delay during market hours — consider a paid data provider instead. Finally, this model ignores valuation entirely. A sector can rank first on momentum while being fundamentally overextended.
FAQ
How often should I recalculate the sector rotation model?
For individual investors, recalculating weekly or monthly is sufficient. Institutional strategies may run daily, but higher frequency does not necessarily improve performance — sector trends persist over weeks to months, not hours. Match your recalculation frequency to your rebalancing schedule.
Can I use different sector ETFs instead of SPDR funds?
Yes. Replace the tickers in your dictionary with Vanguard or iShares sector ETFs. Just ensure all ETFs in your list have the same inception date and sufficient trading history, or you will introduce survivorship bias into your rankings.
What composite weighting scheme works best?
Equal weighting across 1-month, 3-month, and 6-month returns is a neutral starting point. Some practitioners weight shorter periods more heavily to increase responsiveness. Test both approaches against historical data for your specific rebalancing frequency before committing.
How do I automate this script to run on a schedule?
On Linux or macOS, use cron to schedule your Python script. On Windows, use Task Scheduler. Point the scheduler at your script path and set it to run after market close. Add logic to append results to a CSV file so you build a historical record of rankings over time. That historical record is where the real edge starts to appear.