Skip to content
All library documents

Monthly Stock Momentum Rotation with Equal-Weighted Top Performers

Code Lumibot strategies

Summary

This strategy ranks a fixed universe of ten large U.S. stocks by price momentum over roughly one trading year. On the first trading day of each month, it selects the three strongest performers, sells positions outside that group, and adjusts each selected holding toward an equal share of portfolio value. The example uses daily iterations and historical price data from Yahoo Finance, with a backtest period from 2020 through 2023 and SPY as the benchmark.

The code illustrates a simple relative-momentum rotation rule, including ranking, monthly rebalancing, and whole-share position sizing. The document provides an implementation example but no reported returns, comparison, or analysis of risk. Its fixed universe and historical data source limit what can be inferred; it also does not describe transaction costs, slippage, survivorship bias, or safeguards for missing price data. Backtest results would need careful evaluation before the rule could inform live allocation.

Key ideas

  • The strategy ranks a fixed stock universe by returns over a 252-day lookback.
  • It holds the three highest-ranked stocks and rebalances monthly.
  • Selected holdings are adjusted toward equal portfolio weights using whole shares.
  • The sample backtest uses historical Yahoo Finance data and SPY as its benchmark, but reports no performance results.
  • The example does not account explicitly for trading costs, slippage, or survivorship bias.

Tags

Full text
# stock_momentum.py


```py
"""
Stock Momentum Rotation

Asset class: Stocks
Data source: Yahoo Finance (free)
Description: Ranks a universe of stocks by 12-month momentum.
Holds the top N performers, rebalances monthly.
"""

from datetime import datetime
from lumibot.strategies import Strategy
from lumibot.backtesting import YahooDataBacktesting


class MomentumRotation(Strategy):
    parameters = {
        "universe": ["AAPL", "MSFT", "GOOGL", "AMZN", "META", "NVDA", "TSLA", "JPM", "V", "UNH"],
        "top_n": 3,
        "lookback_days": 252,
    }

    def initialize(self):
        self.sleeptime = "1D"
        self.vars.last_rebalance = None

    def on_trading_iteration(self):
        current_date = self.get_datetime().date()

        # Rebalance monthly (first trading day of each month)
        if self.vars.last_rebalance and current_date.month == self.vars.last_rebalance.month:
            return

        universe = self.parameters["universe"]
        lookback = self.parameters["lookback_days"]
        top_n = self.parameters["top_n"]

        # Calculate momentum for each stock
        momentum_scores = {}
        for symbol in universe:
            bars = self.get_historical_prices(symbol, lookback + 5)
            if bars is not None and len(bars.df) >= lookback:
                df = bars.df
                momentum_scores[symbol] = df["close"].iloc[-1] / df["close"].iloc[0] - 1

        if not momentum_scores:
            return

        # Pick top N by momentum
        ranked = sorted(momentum_scores.items(), key=lambda x: x[1], reverse=True)
        winners = [symbol for symbol, score in ranked[:top_n]]

        self.log_message(f"Top {top_n} by momentum: {winners}")

        # Sell anything not in winners
        for position in self.get_positions():
            if position.symbol not in winners:
                self.sell_all(position.symbol)

        # Equal-weight the winners
        target_value = self.portfolio_value / top_n
        for symbol in winners:
            price = self.get_last_price(symbol)
            if price and price > 0:
                target_qty = int(target_value // price)
                current_position = self.get_position(symbol)
                current_qty = current_position.quantity if current_position else 0

                diff = target_qty - current_qty
                if diff > 0:
                    order = self.create_order(symbol, diff, "buy")
                    self.submit_order(order)
                elif diff < 0:
                    order = self.create_order(symbol, abs(diff), "sell")
                    self.submit_order(order)

        self.vars.last_rebalance = current_date


if __name__ == "__main__":
    MomentumRotation.backtest(
        YahooDataBacktesting,
        datetime(2020, 1, 1),
        datetime(2024, 1, 1),
        benchmark_asset="SPY",
    )

```

Shown in full with attribution under the source's licence. Licence: MIT

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.