Skip to content
All library documents

Monthly SPY Momentum with Periodic Long Entries and Exits

Article Strategy library · Author: Farrell Aultman (pinkfish)

Summary

This is a basic long only price momentum strategy for SPY. It compares the current price with its value a chosen number of months earlier: positive momentum triggers a purchase, while negative momentum prompts an exit. The code calculates monthly momentum measures for lookbacks from three to eighteen months. When no lookback is supplied, it randomly selects a value from six through twelve months while flat, then checks signals on the first trading day of the week. Any open position is closed at the end of the data period.

The document supplies implementation details and default options, including unadjusted prices, caching, and a margin setting, but gives no backtest results or evidence that the strategy is profitable. Randomizing the lookback can make runs differ, complicating comparisons unless the chosen value is controlled or recorded. The simple rule also omits details such as a benchmark comparison, transaction cost assumptions, and risk controls, limiting conclusions about its performance in live trading.

Key ideas

  • The strategy buys SPY when its price is above the selected lookback value and sells when it falls below it.
  • Momentum is measured over monthly lookbacks, with several candidate periods calculated by the implementation.
  • When the lookback is unspecified, a period from six to twelve months is randomly selected while flat.
  • Signals are checked on the first trading day of the week, and open positions are closed at the data end.
  • No performance results or detailed risk controls are provided.

Tags

Full text
# momentum


# momentum









A basic price based momentum strategy.

1. The SPY is higher than X months ago, buy
2. If the SPY is lower than X months ago, sell your long position.

A lookback of None means a random lookback = {6-12} months.

## Source (MIT)

```python
"""
A basic price based momentum strategy.

1. The SPY is higher than X months ago, buy
2. If the SPY is lower than X months ago, sell your long position.

A lookback of None means a random lookback = {6-12} months.
"""

import random

import pinkfish as pf


default_options = {
    'use_adj' : False,
    'use_cache' : True,
    'lookback': None,
    'margin': 1
}


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
        pf.TradeLog.margin = self.options['margin']
        lookback = None

        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)

            # If lookback is None, then select a random lookback of
            # 6,7,8,...,or 12 months, if not in a position.
            if self.tlog.shares == 0:
                if self.options['lookback'] is None:
                    lookback = random.choice(range(6, 12+1))
                else:
                    lookback = self.options['lookback']

            #mom = getattr(row, 'mom'+str(lookback))
            mom = self.tlog.get_price(row, 'mom'+str(lookback))

            # Sell Logic
            # First we check if an existing position in symbol should be sold
            #  - if first_dotw
            #            sell if mom < 0
            #            sell if end_flag
  
            if self.tlog.shares > 0:
                if ((row.first_dotw and mom < 0) or end_flag):
#                 if ((row.last_dotm and mom < 0) or end_flag):
                    self.tlog.sell(date, close)

            # Buy Logic
            #  - if first_dotw
            #            buy if mom > 0

            else:
                if (row.first_dotw and mom > 0):
#                 if (row.last_dotm and mom > 0):
                    self.tlog.buy(date, close)

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

    def run(self):
        self.ts = pf.fetch_timeseries(self.symbol, use_cache=self.options['use_cache'])
        self.ts = pf.select_tradeperiod(self.ts, self.start,
                                        self.end, self.options['use_adj'])

        # Add calendar columns
        self.ts = pf.calendar(self.ts)

        # Add momentum indicator for 3...18 months
        lookbacks = range(3, 18+1)
        for lookback in lookbacks:
            self.ts['mom'+str(lookback)] = pf.MOMENTUM(self.ts,
                lookback=lookback, time_frame='monthly',
                price='close', prevday=False)

        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.