Skip to content
All library documents

Monthly S&P 500 Trend Filter Using a 10-Month Moving Average

Article Strategy library · Author: Farrell Aultman (pinkfish)

Summary

This strategy uses the S&P 500’s month-end closing price relative to its 10-month simple moving average to determine whether to hold a long position. It buys when the month-end close is above the average and sells when it is below. The moving average is calculated from monthly closing prices, then carried across daily records so the signal can be evaluated at each month’s end.

The supplied implementation also closes any remaining position at the end of the selected data period and records daily balances and trade statistics. It describes the entry and exit rules but provides no performance results or comparison with other approaches. The method is a simple trend-following filter; it does not specify short positions, position sizing, or a separate protective stop. Its behavior depends on the instrument, data period, and execution assumptions, which are not evaluated in the document.

Key ideas

  • The strategy buys when the S&P 500 closes above its 10-month moving average at month-end.
  • It sells when the month-end close falls below that average.
  • Monthly averages are mapped onto daily data so signals can be checked at month-end.
  • The document gives implementation logic but no evidence of historical performance.

Tags

Full text
# monthly-sma


# monthly-sma









The Monthly SMA.

Entry and Exit Points
Entry (Buy Signal): Buy when the S&P 500 closes above the 10-month moving
average at the end of the month.

Exit (Sell Signal): Sell when the S&P 500 closes below the 10-month moving
average at the end of the month.

## Source (MIT)

```python
"""
The Monthly SMA.

Entry and Exit Points
Entry (Buy Signal): Buy when the S&P 500 closes above the 10-month moving
average at the end of the month.

Exit (Sell Signal): Sell when the S&P 500 closes below the 10-month moving
average at the end of the month.
"""

import pinkfish as pf


default_options = {
    'use_adj' : False,
    'use_cache' : True,
    'monthly_sma' : 10
}

class Strategy:

    def __init__(self, symbol, capital, start, end, options=default_options):

        self.symbol = symbol
        self.capital = capital
        self.start = start
        self.end = end
        self.options = options.copy()

        self.ts = None
        self.rlog = None
        self.tlog = None
        self.dbal = None
        self.stats = None

    def _algo(self):

        pf.TradeLog.cash = self.capital

        for i, row in enumerate(self.ts.itertuples()):

            date = row.Index.to_pydatetime()
            close = row.close; 
            end_flag = pf.is_last_row(self.ts, i)

            # Buy
            if self.tlog.shares == 0:
                if row.last_dotm and row.close > row.monthly_sma:
                    self.tlog.buy(date, close)
            # Sell
            else:
                if (row.last_dotm and close < row.monthly_sma) or end_flag:
                    self.tlog.sell(date, close)
            

            # Record daily balance.
            self.dbal.append(date, close)

    def run(self):

        # Fetch and select timeseries
        self.ts = pf.fetch_timeseries(self.symbol, use_cache=self.options['use_cache'])
        self.ts = pf.select_tradeperiod(self.ts, self.start, self.end,
                                        use_adj=self.options['use_adj'])

        # Step 1: Resample to get the last closing price of each month
        df_monthly = self.ts["close"].resample('ME').last()  # 'M' ensures robustness

        # Step 2: Compute the 10-month moving average
        df_monthly_sma = df_monthly.rolling(window=self.options['monthly_sma'],
                                            min_periods=self.options['monthly_sma']).mean()

        # Step 3: Reindex the monthly SMA to the daily index and forward-fill
        self.ts['monthly_sma'] = df_monthly_sma.reindex(self.ts.index, method='ffill')
        
        # Add calendar columns.
        self.ts = pf.calendar(self.ts, columns=['last_dotm'])
        
        # Finalize timeseries
        self.ts, self.start = pf.finalize_timeseries(self.ts, self.start,
                                dropna=True, drop_columns=['open', 'high', 'low'])

        self.tlog = pf.TradeLog(self.symbol)
        self.dbal = pf.DailyBal()

        self._algo()
        self._get_logs()
        self._get_stats()

    def _get_logs(self):
        self.rlog = self.tlog.get_log_raw()
        self.tlog = self.tlog.get_log()
        self.dbal = self.dbal.get_log(self.tlog)

    def _get_stats(self):
        self.stats = pf.stats(self.ts, self.tlog, self.dbal, self.capital)

```

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.