Start now →

I Built a Regime-Based Portfolio Strategy Using Python — And Put Sector Rotation to the Test

By Ayushman Pranav · Published March 10, 2026 · 10 min read · Source: DataDrivenInvestor
EthereumRegulation
I Built a Regime-Based Portfolio Strategy Using Python — And Put Sector Rotation to the Test

I Built a Regime-Based Portfolio Strategy Using Python — And Put Sector Rotation to the Test

I tested whether sector rotation strategies actually work using a Hidden Markov Model and 10 years of Indian stock market data.
The results were surprising.

Finance loves clean stories.

The kind that fit neatly into textbooks:

This narrative is the backbone of what investors call sector rotation.

Can AI Detect Market Regimes? Testing Sector Rotation with Python
Can AI Detect Market Regimes?
Testing Sector Rotation with Python

It sounds elegant. Almost obvious.

But markets have a habit of humiliating obvious ideas.

So I asked a more interesting question:

Does sector rotation actually improve portfolio performance when tested against real data?

Instead of debating theory, I decided to run an experiment.

I built a regime-based portfolio strategy in Python, applied it to Indian equities, and let the data decide.

The idea was straightforward:

Detect market regimes → Allocate capital dynamically across sectors.

If sector rotation really works, the model should outperform a simple diversified portfolio.

Why Regime Detection Might Make Sector Rotation Work

Markets rarely move in straight lines. They shift between different states.

Think of them as hidden regimes:

Each environment changes the rules of the game.

Certain sectors should theoretically dominate specific regimes.

If this pattern holds, a regime-aware portfolio should have a clear edge over static allocation.

That’s the hypothesis.

Now it needed a proper test.

Step 1 — Constructing Sector Proxies

Instead of relying on sector indices, I built simple sector baskets using representative companies.

Auto sector

auto = ["MARUTI.NS","M&M.NS","EICHERMOT.NS"]

IT sector

it = ["TCS.NS","INFY.NS","HCLTECH.NS"]

Pharma sector

pharma = ["SUNPHARMA.NS","DRREDDY.NS","CIPLA.NS"]

Next, I pulled historical prices using yfinance.

import yfinance as yf

# Using 10 years of historical data to capture multiple market cycles
auto_data = yf.download(auto, start="2015-01-01", auto_adjust=True)['Close']
it_data = yf.download(it, start="2015-01-01", auto_adjust=True)['Close']
pharma_data = yf.download(pharma, start="2015-01-01", auto_adjust=True)['Close']

Then I converted prices into sector-level returns.

auto_ret = auto_data.pct_change().mean(axis=1)
it_ret = it_data.pct_change().mean(axis=1)
pharma_ret = pharma_data.pct_change().mean(axis=1)

Each sector now behaves like its own synthetic index.

Clean. Simple. Comparable.

Chart 1 — Do Sectors Actually Rotate?

Before building any strategy, we need to answer a basic question:

Do these sectors actually take turns leading the market?

import matplotlib.pyplot as plt
auto_cum = (1 + auto_ret).cumprod()
it_cum = (1 + it_ret).cumprod()
pharma_cum = (1 + pharma_ret).cumprod()
plt.figure(figsize=(10,5))
plt.plot(auto_cum, label="Auto Sector")
plt.plot(it_cum, label="IT Sector")
plt.plot(pharma_cum, label="Pharma Sector")
plt.title("Sector Performance Comparison")
plt.xlabel("Date")
plt.ylabel("Cumulative Returns")
plt.legend()
plt.show()
Over the 10-year period, sector leadership shifts clearly across time. Auto dominates early years, IT leads the post-COVID technology rally, and Auto regains strength in later years while Pharma behaves more defensively.

If sector rotation exists, we should see leadership shifting over time.

If not, the entire strategy collapses before it even begins.

We can observe clear leadership shifts across different periods:

Step 2 — Designing the Regime-Based Allocation Model

Once a regime is detected, the portfolio adjusts its exposure.

The allocation rule is aggressive by design:

Example allocation logic:

weights = {
0: {"IT":0.7,"Auto":0.15,"Pharma":0.15},
1: {"Pharma":0.7,"IT":0.15,"Auto":0.15},
2: {"Auto":0.7,"IT":0.15,"Pharma":0.15}
}

Instead of assigning sectors manually, compute which sector has the highest average return in each regime.

Step 1 — Combine sector returns

sector_returns = pd.DataFrame({
"Auto": auto_ret,
"IT": it_ret,
"Pharma": pharma_ret
})

Step 2 — Create regime labels using HMM

from hmmlearn.hmm import GaussianHMM
model = GaussianHMM(n_components=3, covariance_type="full", n_iter=1000)
model.fit(sector_returns.dropna())
regime = model.predict(sector_returns.dropna())
sector_returns = sector_returns.dropna()
sector_returns["Regime"] = regime

Step 3 — Find best sector per regime

regime_performance = sector_returns.groupby("Regime")[["Auto","IT","Pharma"]].mean()
print(regime_performance)
best_sector = regime_performance.idxmax(axis=1)
best_sector

Step 4 — Generate weights dynamically

weights = {}

for r, sector in best_sector.items():
weights[r] = {"Auto":0.15,"IT":0.15,"Pharma":0.15}
weights[r][sector] = 0.7

weights

Now the mapping is data-driven, not assumed.

Portfolio returns follow directly from these weights.

portfolio = []

for date, row in sector_returns.iterrows():

r = int(row["Regime"])

w = weights[r]

p_ret = (
w["Auto"] * row["Auto"] +
w["IT"] * row["IT"] +
w["Pharma"] * row["Pharma"]
)

portfolio.append(p_ret)

portfolio = pd.Series(portfolio, index=sector_returns.index)
portfolio

These weights represent one portfolio strategy, not three separate portfolios.

What you created is:

A single dynamic portfolio that changes its sector allocation depending on the detected regime.

How to interpret your result

The model identified three regimes. Two regimes favoured Pharma, while one regime strongly favoured Auto.

So the model learned:

That produced these weights

{
0: {'Auto':0.15,'IT':0.15,'Pharma':0.7},
1: {'Auto':0.15,'IT':0.15,'Pharma':0.7},
2: {'Auto':0.7,'IT':0.15,'Pharma':0.15}
}

Meaning:

What happens in practice

Each day:

  1. The HMM detects the current regime
  2. The portfolio switches to the corresponding allocation

Example:

So it is one portfolio that changes composition over time.

Important insight from your results

Your model discovered only two effective sector regimes:

IT never became the top sector in any regime.

This is actually an interesting research result.

Interestingly, the model did not assign any regime where IT dominated. Two regimes favoured Pharma, while one favoured Auto, suggesting that defensive and cyclical dynamics were more prominent in the data than technology-driven leadership.

This becomes the engine of the regime-based portfolio strategy.

The system detects the environment.
The portfolio adapts.

In theory, this should capture sector leadership as it emerges.

Step 3 — Setting Up a Fair Benchmark

Every strategy needs a baseline.

Otherwise, you’re just admiring your own model.

The benchmark here is intentionally simple:

An equal-weight portfolio across the three sectors.

static_portfolio = (auto_ret + it_ret + pharma_ret)/3

No regime detection.
No dynamic allocation.

Just diversification.

If sector rotation truly works, the dynamic strategy should outperform this benchmark.

Chart 2 — Strategy vs Benchmark

Now the real test begins.

strategy_cum = (1 + portfolio).cumprod()
benchmark_cum = (1 + static_portfolio).cumprod()
plt.figure(figsize=(10,5))
plt.plot(strategy_cum, label="Regime-Based Portfolio")
plt.plot(benchmark_cum, label="Equal Weight Portfolio")
plt.title("Strategy Performance Comparison")
plt.xlabel("Date")
plt.ylabel("Cumulative Returns")
plt.legend()
plt.show()

This chart answers the most important question in investing:

Did the strategy actually beat a simple portfolio?

Chart 3 — Drawdowns (Where Strategies Truly Break)

Returns are only half the story.

Risk matters just as much.

Professional investors obsess over drawdowns — how deep a portfolio falls during market stress.

strategy_drawdown = strategy_cum / strategy_cum.cummax() - 1
benchmark_drawdown = benchmark_cum / benchmark_cum.cummax() - 1
plt.figure(figsize=(10,5))
plt.plot(strategy_drawdown, label="Regime Strategy Drawdown")
plt.plot(benchmark_drawdown, label="Equal Weight Drawdown")
plt.title("Drawdown Comparison")
plt.xlabel("Date")
plt.ylabel("Drawdown")
plt.legend()
plt.show()

If regime strategies work, they should limit downside during turbulent markets.

That’s where dynamic allocation should shine.

Interpreting the Strategy Results

To evaluate whether the regime-based approach actually adds value, both portfolios were compared using three core performance metrics: CAGR, Sharpe ratio, and maximum drawdown.

import numpy as np
import pandas as pd

# Align benchmark with strategy dates
benchmark = static_portfolio.loc[portfolio.index]

# CAGR
def CAGR(returns):
cumulative = (1 + returns).cumprod()
years = len(returns) / 252
return cumulative.iloc[-1] ** (1 / years) - 1

# Sharpe Ratio (assuming risk-free rate ≈ 0)
def sharpe_ratio(returns):
return (returns.mean() / returns.std()) * np.sqrt(252)

# Max Drawdown
def max_drawdown(returns):
cumulative = (1 + returns).cumprod()
drawdown = cumulative / cumulative.cummax() - 1
return drawdown.min()

metrics = pd.DataFrame({
"Strategy": ["Regime Portfolio", "Equal Weight"],
"CAGR": [
CAGR(portfolio),
CAGR(benchmark)
],
"Sharpe": [
sharpe_ratio(portfolio),
sharpe_ratio(benchmark)
],
"Max Drawdown": [
max_drawdown(portfolio),
max_drawdown(benchmark)
]
})

metrics

The regime-based portfolio outperformed the equal-weight benchmark across all major metrics. It achieved a CAGR of nearly 26% compared to about 15%, while also producing a higher Sharpe ratio, indicating stronger risk-adjusted returns.

The strategy also improved downside protection. Maximum drawdown declined from -36% to -25%, suggesting that dynamic sector allocation helped cushion the portfolio during periods of market stress.

Taken together, these results suggest that regime-aware allocation can improve both return and risk characteristics compared to static diversification.

import matplotlib.pyplot as plt

plt.figure(figsize=(12,3))

plt.scatter(
sector_returns.index,
sector_returns["Regime"],
c=sector_returns["Regime"],
cmap="Set1", # strong contrasting colours
s=6
)

plt.title("Market Regime Timeline", fontsize=12, weight="bold")
plt.xlabel("Date")
plt.ylabel("Regime")

plt.show()

Understanding the Regime Timeline

The regime timeline reveals an interesting pattern in how markets evolve over time.

Most of the sample is dominated by Regime 1, while Regime 0 appears intermittently and Regime 2 occurs only occasionally. This behavior is actually typical in financial markets, where a dominant market environment persists for long periods, punctuated by shorter transitions during shocks or structural changes.

These regime shifts likely correspond to periods of heightened volatility, economic stress, or shifts in sector leadership.

import matplotlib.pyplot as plt

plt.figure(figsize=(12,3))

plt.scatter(
sector_returns.index,
sector_returns["Regime"],
c=sector_returns["Regime"],
cmap="Set1", # strong contrasting colours
s=6
)

plt.title("Market Regime Timeline", fontsize=12, weight="bold")
plt.xlabel("Date")
plt.ylabel("Regime")

plt.show()

What the Model Discovered About Market Structure

Looking deeper into the regime characteristics, the model effectively identified three distinct market environments.

interestingly, this structure closely resembles the classic three-state market framework often discussed in quantitative finance:

In this case, Pharma emerges as the defensive leader, while Auto benefits most from cyclical expansions. The model’s ability to recover these patterns directly from historical data provides further evidence that sector behavior is closely linked to underlying market regimes.

How the Strategies Were Evaluated

To compare performance objectively, I measured four core metrics:

CAGR
Long-term growth rate.

Volatility
Return variability.

Sharpe Ratio
Risk-adjusted performance.

Maximum Drawdown
Worst peak-to-trough decline.

Together, these metrics reveal whether a strategy is truly superior — or just complicated.

The Result

The outcome was surprising.

The regime-based strategy outperformed the equal-weight portfolio by a significant margin over the 10-year period.

However, the improvement did not come from perfect regime prediction. Instead, the model concentrated exposure in sectors that dominated specific market phases.

The strategy also achieved slightly lower drawdowns, suggesting dynamic allocation helped reduce downside risk during turbulent periods.

But an important question remains:

Is this outperformance robust, or simply the result of a few strong sector cycles?

That’s where regime models become more complicated than they first appear.

Final Thought

Regime detection is intellectually appealing.

It reveals patterns hidden beneath noisy market data.

But here’s the hard truth:

Identifying regimes doesn’t automatically produce better portfolios.

In this experiment, the regime-based strategy did outperform a simple diversified portfolio, but the deeper question is whether that outperformance is robust or simply the result of favourable sector cycles.

Regime detection reveals structure in markets that traditional models often ignore.

But turning that structure into consistent investment alpha is far more difficult.
The real question is not whether regimes exist.
The real question is whether we can detect them early enough to profit from them.

Which raises a deeper question worth exploring:

Are regime models better at explaining markets than beating them?

That’s the question I decided to investigate next.


I Built a Regime-Based Portfolio Strategy Using Python — And Put Sector Rotation to the Test was originally published in DataDrivenInvestor on Medium, where people are continuing the conversation by highlighting and responding to this story.

This article was originally published on DataDrivenInvestor and is republished here under RSS syndication for informational purposes. All rights and intellectual property remain with the original author. If you are the author and wish to have this article removed, please contact us at [email protected].

NexaPay — Accept Card Payments, Receive Crypto

No KYC · Instant Settlement · Visa, Mastercard, Apple Pay, Google Pay

Get Started →