In partnership with

What 100K+ Engineers Read to Stay Ahead

Your GitHub stars won't save you if you're behind on tech trends.

That's why over 100K engineers read The Code to spot what's coming next.

  • Get curated tech news, tools, and insights twice a week

  • Learn about emerging trends you can leverage at work in just 10 mins

  • Become the engineer who always knows what's next

🚀 Your Investing Journey Just Got Better: Premium Subscriptions Are Here! 🚀

It’s been 4 months since we launched our premium subscription plans at GuruFinance Insights, and the results have been phenomenal! Now, we’re making it even better for you to take your investing game to the next level. Whether you’re just starting out or you’re a seasoned trader, our updated plans are designed to give you the tools, insights, and support you need to succeed.

Here’s what you’ll get as a premium member:

  • Exclusive Trading Strategies: Unlock proven methods to maximize your returns.

  • In-Depth Research Analysis: Stay ahead with insights from the latest market trends.

  • Ad-Free Experience: Focus on what matters most—your investments.

  • Monthly AMA Sessions: Get your questions answered by top industry experts.

  • Coding Tutorials: Learn how to automate your trading strategies like a pro.

  • Masterclasses & One-on-One Consultations: Elevate your skills with personalized guidance.

Our three tailored plans—Starter Investor, Pro Trader, and Elite Investor—are designed to fit your unique needs and goals. Whether you’re looking for foundational tools or advanced strategies, we’ve got you covered.

Don’t wait any longer to transform your investment strategy. The last 4 months have shown just how powerful these tools can be—now it’s your turn to experience the difference.

Buying strength beats guessing direction. The best part is that you do not need a complex model to follow market strength.

One thing that has been clear in the last decade is that Assets that outperform tend to keep outperforming. At least in the short term.

Here, we present a strategy that applies that principle through weekly asset rotations.

It ranks a defined set of equities by their trailing 3-month returns and reallocates into the top performers each Friday.

Each week, the model evaluates recent performance, selects the top n assets, and holds them until the next rebalance.

If no asset shows positive momentum, it defaults to capital preservation by reducing exposure.

Kickstart your holiday campaigns

CTV should be central to any growth marketer’s Q4 strategy. And with Roku Ads Manager, launching high-performing holiday campaigns is simple and effective.

With our intuitive interface, you can set up A/B tests to dial in the most effective messages and offers, then drive direct on-screen purchases via the remote with shoppable Action Ads that integrate with your Shopify store for a seamless checkout experience.

Don’t wait to get started. Streaming on Roku picks up sharply in early October. By launching your campaign now, you can capture early shopping demand and be top of mind as the seasonal spirit kicks in.

Get a $500 ad credit when you spend your first $500 today with code: ROKUADS500. Terms apply.

The complete Python notebook for the analysis is provided below.

1. Asset Rotation Strategy Overview

This strategy is built on a simple belief: assets with strong recent performance are more likely to continue outperforming.

To capture that persistence, we use a ranking system based on trailing momentum.

Specifically, it ranks assets using trailing 3-month returns, selects the top performers, and rotates into them systematically every Friday.

The goal is to allocate capital to what’s already working, not to predict future winners.

1.1 Selection Universe

The strategy operates on a fixed universe of assets.

In our example, it uses six large-cap U.S. equities:

["AAPL", "MSFT", "AMZN", "GOOGL", "TSLA", "NVDA"]

This can be replaced with any basket, e.g. sector ETFs, country indices, crypto, etc. It all depends on your use case.

2.2 Momentum Signal

Each asset is scored based on its return over a configurable trailing window, defined in days.

This window is controlled by the MOM_DAYS parameter:

MOM_DAYS = 63  # lookback period (e.g. 3 months)

The momentum score is computed as:

Pt is the price at time t.

200+ AI Side Hustles to Start Right Now

From prompt engineering to AI apps, there are countless ways to profit from AI now. Our guide reveals 200+ actionable AI business models, from no-code solutions to advanced applications. Learn how people are earning $500-$10,000 monthly with tools that didn't exist last year. Sign up for The Hustle to get the guide and daily insights.

2.3 Weekly Rebalancing

Every Friday, the strategy performs the following:

  1. Ranks all assets by their trailing momentum

  2. Selects the top N assets based on positive momentum

  3. Allocates equal weight to the selected names

  4. Rebalances once per week

If fewer than N assets meet the criteria, the model adjusts exposure accordingly.

This avoids forced allocation into weak assets and keeps the portfolio responsive to trend strength.

The number of assets held (TOP_N) is adjustable.

2.4 Return Basis and Execution

Returns are computed on an open-to-open basis to simulate execution at Monday’s open following a Friday signal.

This avoids lookahead bias and reflects how systematic strategies would operate in production.

The model includes optional transaction cost modeling using a fixed basis point adjustment on each rebalance. In this implementation:

COST_BPS = 5  # one-way cost

We benchmark the strategy against:

  • Equal-Weight: All assets held with equal weight, rebalanced daily.

  • Buy-and-Hold: Individual equity curves for each stock in the universe.

The complete strategy workflow is summarized below:

Figure 1. Step-by-step decision flow of the weekly momentum rotation strategy.

2. Implementation In Python

The implementation is structured to match the logic described above.

2.1 Parameters and Tickers

We define the universe of assets, lookback window for momentum, portfolio size, and configuration for rebalancing logic.

# 1. Params
TICKERS       = ["AAPL","MSFT","AMZN","GOOGL","TSLA","NVDA"]
START_DATE    = "2020-01-01"
END_DATE      = "2025-07-01"
MOM_DAYS      = 63      # ~3-month momentum
TOP_N         = 2       # number of winners to select
USD_SECONDARY = True    # allow USD as second slot when there is no 2nd winner
COST_BPS      = 5       # one-way cost

2.2 Data Retrieval

We pull open and close prices from Yahoo Finance. The data is forward-filled and aligned across tickers.

# 2. Download OHLC
data = {}
for t in TICKERS:
    df = yf.download(t,
                     start=START_DATE,
                     end=END_DATE,
                     interval="1d",
                     auto_adjust=True,
                     progress=False)
    if isinstance(df.columns, pd.MultiIndex):
        df.columns = df.columns.get_level_values(0)
    data[t] = df.dropna()

opens  = pd.concat([data[t]["Open"]  for t in TICKERS], axis=1)
closes = pd.concat([data[t]["Close"] for t in TICKERS], axis=1)
opens.columns  = TICKERS
closes.columns = TICKERS

2.3 Weekly Signal Construction

Momentum is defined as the trailing 3-month return. Rebalancing happens only on Fridays.

# 3. Weekly momentum signal
mom       = closes.pct_change(MOM_DAYS)
is_friday = closes.index.weekday == 4
signal_dt = closes.index[is_friday]

basket = pd.Series(index=closes.index, dtype=object)

for dt in signal_dt:
    top    = mom.loc[dt].nlargest(TOP_N)
    winners = top[top > 0].index.tolist()
    
    if len(winners) == 0:
        # no positive momentum → all USD
        basket.loc[dt] = ["USD"]
    elif len(winners) < TOP_N:
        if USD_SECONDARY:
            # fill missing slots with USD
            fill = ["USD"] * (TOP_N - len(winners))
            basket.loc[dt] = winners + fill
        else:
            # only real winners, no USD fill
            basket.loc[dt] = winners
    else:
        basket.loc[dt] = winners

# seed first day
if not isinstance(basket.iloc[0], list):
    basket.iloc[0] = ["USD"]
basket = basket.ffill()

2.4 Holdings Construction and Returns

We allocate capital equally across the selected tickers at each rebalance. If fewer than the target number qualify, the remaining weight goes to cash.

Returns are computed open-to-open, i.e. buying at today’s open and selling at tomorrow’s open.

This avoids lookahead bias and matches how real rebalancing would occur.

# 4. Compute open-to-open returns
open_ret = opens.pct_change().fillna(0)
open_ret["USD"] = 0.0

# 5. Build weights
slots      = opens.columns.tolist() + ["USD"]
weights    = pd.DataFrame(0.0, index=opens.index, columns=slots)
for dt, assets in basket.items():
    w = 1.0 / len(assets)
    for a in assets:
        weights.at[dt, a] = w
weights = weights.ffill()

# 6. Strategy return
port_ret = (weights.shift() * open_ret).sum(axis=1)

# 7. Apply transaction cost
rebal = (weights != weights.shift()).any(axis=1)
port_ret.loc[rebal] -= COST_BPS / 10000

2.5 Equity Curve Comparison

We compare the rotation strategy against equal-weight and individual buy-and-hold baselines.

# 8. Benchmarks
eq_ret = open_ret[TICKERS].mean(axis=1)
bh_ret = open_ret[TICKERS]

# 9. Equity curves
rot_eq = (1 + port_ret).cumprod()
eq_eq  = (1 + eq_ret).cumprod()
bh_eq  = (1 + bh_ret).cumprod()

# 10. Plot equity comparison
plt.style.use("dark_background")
fig, ax = plt.subplots(figsize=(13,7))

# BH faint, default colors
for t in TICKERS:
    bh_eq[t].plot(ax=ax, alpha=0.25, lw=1, label=f"BH {t}")

# Equal weight dashed
eq_eq.plot(ax=ax, color="lightgrey", lw=2, ls="--", label="Equal Weight")

# Strategy neon line
ax.plot(rot_eq.index, rot_eq, color="#39FF14", lw=2, label="Strategy")

ax.set_title("Weekly Momentum Rotation vs BH & Equal Weight")
ax.set_xlabel("Date")
ax.set_ylabel("Equity (Growth of $1)")
ax.legend(loc="upper left", ncol=2)
plt.tight_layout()
plt.show()

Figure 2. Equity curve comparison between the momentum rotation strategy, equal-weight portfolio, and individual buy-and-hold tickers. The strategy dynamically reallocates into top performers and shows consistent compounding over time. Top Chart shows performance by selecting the top 2 assets whereas the bottom one shows the performance of only selecting one asset each week.

2.6 Holding Timeline

Next, we also visualize portfolio holdings over time to show hich assets were selected each week.

# 11. Holding timeline
fig, ax = plt.subplots(figsize=(13,2))
idx_map = {t:i for i,t in enumerate(TICKERS+['USD'])}

if TOP_N == 1:
    hold = basket.map(lambda x: x[0])
    hold_idx = hold.map(idx_map)
    ax.step(basket.index, hold_idx, where="post", linewidth=2)
else:
    for pos in range(TOP_N):
        series = basket.map(lambda x: x[pos] if pos < len(x) else 'USD')
        series_idx = series.map(idx_map)
        ax.step(basket.index, series_idx, where="post", linewidth=2)

ax.set_yticks(list(idx_map.values()))
ax.set_yticklabels(list(idx_map.keys()))
ax.set_xlabel("Date")
ax.set_title("Holding Timeline")
ax.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.show()

Figure 3. Holdings Timeline (one asset at the time).

2.7 Performance Metrics

Finally, we summarize the strategy performance with standard metrics.

# 12. Performance summary
TRD    = 252
yrs    = len(port_ret)/TRD
cagr   = rot_eq.iloc[-1]**(1/yrs) - 1
vol    = port_ret.std()*np.sqrt(TRD)
sharpe = port_ret.mean()/port_ret.std()*np.sqrt(TRD)
maxdd  = (rot_eq/rot_eq.cummax()-1).min()

print(f"CAGR   {cagr:.2%}")
print(f"Vol    {vol:.2%}")
print(f"Sharpe {sharpe:.2f}")
print(f"MaxDD  {maxdd:.2%}")
CAGR   87.49%
Vol    52.21%
Sharpe 1.46
MaxDD  -60.16%

3. Limitations

  • Fixed Lookback Window: Using a static 63-day momentum window may not adapt well to different market regimes. In trending periods it works well, but in sideways or volatile environments, it can produce false signals.

  • No Volatility Awareness: The strategy treats all assets equally regardless of volatility. A high-beta stock receives the same weight as a stable one, which may increase portfolio risk unintentionally.

  • Simple Ranking Logic: Asset selection is based solely on past returns. It ignores other dimensions of momentum like volume, volatility, or recent acceleration.

  • Weekly Frequency Assumption: Weekly rebalancing is practical and reduces churn, but might miss sharp reversals or underreact during fast-moving markets.

  • Cash Handling Is Crude: The fallback to USD when not enough assets qualify is simple, but not optimal. It doesn’t consider yield, inflation, or market signals beyond momentum.

4. Extensions

  • Volatility-Adjusted Weights: Instead of equal weight, allocate based on inverse volatility. This can smooth the return profile and reduce drawdowns.

  • Dynamic Lookback Period: Adapt the momentum window based on realized volatility or regime classification (e.g. trend vs chop).

  • Regime Filters: Incorporate a macro or trend filter such as requiring SPY to be above its 200-day moving average to reduce exposure during broad selloffs.

  • Multi-Factor Ranking: Combine momentum with other factors like low volatility or quality to refine selection. Even basic composite scores can improve risk-adjusted returns.

Final Thoughts

Momentum doesn’t need to be complicated to be effective.

This rotation strategy shows how a simple rule like ranking assets by recent returns, rebalance weekly can deliver strong, but adaptive performance.

It avoids forecasting, doesn’t rely on market timing, and operates with transparency.

The model isn’t perfect. It has no volatility control, no macro awareness, and no prediction engine.

But that’s also its strength. Fewer assumptions mean fewer ways to overfit.

The full end-to-end workflow is available as a Google Colab notebook is paywalled only for paid subs below 👇

logo

Subscribe to our premium content to get the full code snippet.

Become a paying subscriber to get access to this post and other subscriber-only content.

Upgrade

Keep Reading

No posts found