Docs
Open Studio

US examples

S&P 500 regime filter

On this page4

A market regime filter acts as a master switch: it exits all positions when the broad market turns bearish, to avoid sustained losses in systemic downturns. Here the switch is market breadth, the count of stocks in bullish versus bearish moving-average alignment. It suits anyone who wants a stock-picking strategy to step aside in bear markets.

Strategy logic

  1. Market breadth: across all stocks in the market, count those in bullish alignment (short MA ≥ medium MA ≥ long MA) and those in bearish alignment (short MA < medium MA < long MA). The defaults are 5-day (short), 10-day (medium) and 30-day (long) moving averages.
  2. Market switch: when more stocks are in bullish alignment than bearish, the market is judged to be in an uptrend and positions are allowed. Otherwise, all positions are exited.
  3. Stock selection: the closing price must be above its level both 20 and 60 trading days ago, a sign of short-to-medium-term upward momentum.
  4. A stock is held only when both its own momentum condition and the market switch are positive.

Code

strategy.py
from finlab import data
from finlab.backtest import sim
from finlab.markets.us import USMarket

data.set_market('us')

close = data.get('price:adj_close')
close = close[close.index.dayofweek < 5]  # remove weekends

# Market breadth: bullish vs. bearish alignment count
def ls_order_position(short=5, mid=10, long=30):
    short_ma = close.average(short)
    mid_ma = close.average(mid)
    long_ma = close.average(long)

    bullish = (short_ma >= mid_ma) & (mid_ma >= long_ma)
    bullish_count = bullish.sum(axis=1)

    bearish = (short_ma < mid_ma) & (mid_ma < long_ma)
    bearish_count = bearish.sum(axis=1)

    entry = bullish_count > bearish_count
    cond = ~close.isna()
    position = cond & entry
    return position

# Individual stock momentum condition
buy = (close > close.shift(20)) & (close > close.shift(60))

# Apply market breadth filter
position = buy & ls_order_position()

report = sim(position, market=USMarket(), fee_ratio=0.001, tax_ratio=0)
report.display()

ls_order_position is reusable: combine it with any stock selection strategy as a top-level regime overlay.

How to adjust it

  • short=5, mid=10, long=30: the moving-average periods of the breadth indicator. 5/10/30 catches short-term trend shifts while filtering out daily noise; change them to tune how sensitive the switch is.
  • bullish_count > bearish_count: the market counts as bullish when more stocks are in bullish alignment than bearish. Breadth reflects the health of the whole market more accurately than an index moving average: a market where a few large caps rise while most stocks fall correctly registers as weak.
  • close > close.shift(20) and close > close.shift(60): the stock must be above its price 20 and 60 trading days ago, confirming both short-term and medium-term momentum.
  • No resample: the portfolio rebalances whenever the position changes, so it reacts quickly to changes in the regime indicator. Add resample='W' if you prefer weekly adjustments with less trading.
↑↓ Move Enter Open Esc Close