# Backtest Equity Trading with SMA Strategy

In the below analysis, we'll use a simple moving average strategy and ultimately determine whether it outperforms a [buy and hold](https://en.wikipedia.org/wiki/Buy_and_hold) strategy investing over the course of 20 years. We'll use the The *200 day simple moving average* of the the S\&P 500 using the [`SPY` ETF](https://en.wikipedia.org/wiki/SPDR_S%26P_500_Trust_ETF) and hold if the month-end price is over the *200 day simple moving average (SMA)* thereshold. Conversely, we'll sell if the `SPY` price was under the *200 day SMA* on the last day of the month. The end of month signal will avoid all the intra-month false signals when price chops under and over the threshold. This is not the pinnacle of trend trading strategies, however it can provide a fundamental & solid approach to trading equities.&#x20;

A backtest is a historical simulation of how a strategy would have performed should it have been run over a past period of time.To test our above hypothesis, we'll backtest this SMA Strategy on the `SPY` over the past 20 years. \
As indicated above, since we're testing it on the S\&P500, we'll use the *SPDR S\&P 500 Trust ETF* which is represented by the ticker `SPY`. We'll use the `yfinance` module to get some simple data and feed it to our startegy to get going.

```python
import yfinance
data = yfinance.download("SPY", start="2000-01-01", end="2021-06-16")
data.to_csv("spy.csv")
```

![spy.csv ](https://2282290372-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MJcwT61yCiz5H4ZddkF%2Fuploads%2FyyVvgJJ1XB9X4duuJhdy%2FScreen%20Shot%202021-11-13%20at%207.36.09%20PM.png?alt=media\&token=2070844f-17d7-47a8-8d49-a87b6d17f3d7)

### Buy-Hold backtesting

```python
#backtest-buyhold
from pyalgotrade import strategy
from pyalgotrade.barfeed import yahoofeed

class BuyAndHoldStrategy(strategy.BacktestingStrategy):

    def __init__(self, feed, instrument):
        super(BuyAndHoldStrategy, self).__init__(feed)
        self.instrument = instrument
        self.setUseAdjustedValues(True)
        self.position = None #<-- Trading positions

    def onEnterOk(self, position):
        self.info(f"{position.getEntryOrder().getExecutionInfo()}")
    
    def onBars(self, bars):
        bar = bars[self.instrument]

        if self.position is None:
            close = bar.getAdjClose() #--> Adjusts for dividends & splits
            broker = self.getBroker()
            cash = broker.getCash()
            quantity = cash / close

            self.position = self.enterLong(self.instrument, quantity)

feed = yahoofeed.Feed()
feed.addBarsFromCSV("spy", "spy.csv")

strategy = BuyAndHoldStrategy(feed, "spy")
cash = strategy.getBroker().getCash()
print(f"Initial Cash: ${cash}")
strategy.run()

strategy.run()
portfolio_value = strategy.getBroker().getEquity() + strategy.getBroker().getCash()
print(f"Ending Portfolio Value: ${portfolio_value}")

change_percentage = ((portfolio_value - cash)/cash)*100
print(f"% change: {round(change_percentage,2)}%")
```

![](https://2282290372-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MJcwT61yCiz5H4ZddkF%2Fuploads%2FWD4dOtuVLf0Geo88fg9s%2FScreen%20Shot%202021-11-13%20at%208.26.58%20PM.png?alt=media\&token=2cfcb75c-d942-4c67-983c-e4230c850623)

We initally had a default value of $1million of Cash. We can observe that the amount of cash we initially dispensed was $986,838 . (1028 \* 95.92...) thereby leaving us some remaining cash. In the aftermath of the Buy-hold strategy, our portfolio value is $2,916,764 representing a total increase of 191.68% which is commendable.&#x20;

### Moving-Average Backtesting&#x20;

We'll now implement the 200 Day SMA average in order to contrast the results with the above and determine which of these method can generate a greater alpha.&#x20;

```python
import pandas
import pandas_market_calendars as market_calendar

from pyalgotrade import strategy
from pyalgotrade.barfeed import yahoofeed
from pyalgotrade.technical import ma
from pyalgotrade import plotter
from pyalgotrade.stratanalyzer import returns, drawdown, trades
```

Note that one of the rules that we have to take into account is the **end of the month**  rule. In the above Buy-Hold strategy, we were looping bar by bar and continually making a decision at each bar iteration. Using this approach, we only want to make a decision if and only it is the last day of the month. Determining the *last trading day of the month* can actually be a non-trivial as it may potential fall January 31st, February 28th, February 29th on a leap year, market holidays etc. We'll use the `import pandas_market_calendars` coupled with the below 5 lines of code to take care of these specificities.

```python
# get last days of month
nyse = market_calendar.get_calendar('NYSE')
df = nyse.schedule(start_date='2000-01-01', end_date='2021-12-31')
df = df.groupby(df.index.strftime('%Y-%m')).tail(1)
df['date'] = pandas.to_datetime(df['market_open']).dt.date
last_days_of_month = [date.isoformat() for date in df['date'].tolist()]
```

```python
class MovingAverageStrategy(strategy.BacktestingStrategy):
    def __init__(self, feed, instrument):
        super(MovingAverageStrategy, self).__init__(feed)
        self.instrument = instrument
        self.position = None
        self.ma = ma.SMA(feed[instrument].getAdjCloseDataSeries(), 200)
        self.setUseAdjustedValues(True)

    def onEnterOk(self, position):
        execInfo = position.getEntryOrder().getExecutionInfo()
        self.info(f"===== BUY at {execInfo.getPrice()} {execInfo.getQuantity()} =====")

    def onExitOk(self, position):
        execInfo = position.getExitOrder().getExecutionInfo()
        self.info(f"===== SELL at {execInfo.getPrice()} =====")
        self.position = None
```

In the below, note how we're only using **98%** of our cash. We're only using **98%** to ensure that we have sufficent cash to fill our order. Additionally, in a scenario that we would want to keep hold of half of our "cash", we would pass in `0.50`.&#x20;

```python
def onBars(self, bars):
        if self.ma[-1] is None:
            return

        bar = bars[self.instrument]
        close = bar.getAdjClose()
        date = bar.getDateTime().date().isoformat()

        if date in last_days_of_month:
            if self.position is None:
                broker = self.getBroker()
                cash = broker.getCash() * .98
                
                if date in last_days_of_month and close > self.ma[-1]:
                    quantity = cash / close
                    self.info(f"buying at {close}, which is above {self.ma[-1]}")
                    self.position = self.enterLong(self.instrument, quantity)
            
            elif close < self.ma[-1] and self.position is not None:
                self.info(f"selling at {close}, which is below {self.ma[-1]}")
                self.position.exitMarket()
                self.position = None


# # Load the bar feed from the CSV file
feed = yahoofeed.Feed()
feed.addBarsFromCSV("spy", "spy.csv")

strategy = MovingAverageStrategy(feed, "spy")

returnsAnalyzer = returns.Returns()
tradesAnalyzer = trades.Trades()
drawDownAnalyzer = drawdown.DrawDown()

strategy.attachAnalyzer(returnsAnalyzer)
strategy.attachAnalyzer(drawDownAnalyzer)
strategy.attachAnalyzer(tradesAnalyzer)

plt = plotter.StrategyPlotter(strategy) 
plt.getInstrumentSubplot("spy").addDataSeries("200 day", strategy.ma)

strategy.run()

plt.plot()

print("Final portfolio value: $%.2f" % strategy.getResult())
print("Cumulative returns: %.2f %%" % (returnsAnalyzer.getCumulativeReturns()[-1] * 100))
print("Max. drawdown: %.2f %%" % (drawDownAnalyzer.getMaxDrawDown() * 100))
print("Longest drawdown duration: %s" % (drawDownAnalyzer.getLongestDrawDownDuration()))

print("")
print("Total trades: %d" % (tradesAnalyzer.getCount()))
if tradesAnalyzer.getCount() > 0:
    profits = tradesAnalyzer.getAll()
    print("Avg. profit: $%2.f" % (profits.mean()))
    print("Profits std. dev.: $%2.f" % (profits.std()))
    print("Max. profit: $%2.f" % (profits.max()))
    print("Min. profit: $%2.f" % (profits.min()))
    returns = tradesAnalyzer.getAllReturns()
    print("Avg. return: %2.f %%" % (returns.mean() * 100))
    print("Returns std. dev.: %2.f %%" % (returns.std() * 100))
    print("Max. return: %2.f %%" % (returns.max() * 100))
    print("Min. return: %2.f %%" % (returns.min() * 100))

print("")
print("Profitable trades: %d" % (tradesAnalyzer.getProfitableCount()))
if tradesAnalyzer.getProfitableCount() > 0:
    profits = tradesAnalyzer.getProfits()
    print("Avg. profit: $%2.f" % (profits.mean()))
    print("Profits std. dev.: $%2.f" % (profits.std()))
    print("Max. profit: $%2.f" % (profits.max()))
    print("Min. profit: $%2.f" % (profits.min()))
    returns = tradesAnalyzer.getPositiveReturns()
    print("Avg. return: %2.f %%" % (returns.mean() * 100))
    print("Returns std. dev.: %2.f %%" % (returns.std() * 100))
    print("Max. return: %2.f %%" % (returns.max() * 100))
    print("Min. return: %2.f %%" % (returns.min() * 100))

print("")
print("Unprofitable trades: %d" % (tradesAnalyzer.getUnprofitableCount()))
if tradesAnalyzer.getUnprofitableCount() > 0:
    losses = tradesAnalyzer.getLosses()
    print("Avg. loss: $%2.f" % (losses.mean()))
    print("Losses std. dev.: $%2.f" % (losses.std()))
    print("Max. loss: $%2.f" % (losses.min()))
    print("Min. loss: $%2.f" % (losses.max()))
    returns = tradesAnalyzer.getNegativeReturns()
    print("Avg. return: %2.f %%" % (returns.mean() * 100))
    print("Returns std. dev.: %2.f %%" % (returns.std() * 100))
    print("Max. return: %2.f %%" % (returns.max() * 100))
    print("Min. return: %2.f %%" % (returns.min() * 100))
```

![output 1.1](https://2282290372-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MJcwT61yCiz5H4ZddkF%2Fuploads%2FyIkv1R5mdZDNwhZUWQko%2FScreen%20Shot%202021-11-13%20at%2010.28.28%20PM.png?alt=media\&token=60f25892-f37f-4acb-9b0b-f8df91affdb4)

![output 1.2](https://2282290372-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MJcwT61yCiz5H4ZddkF%2Fuploads%2Ft9Vm1WoCOy6GdDK8Geoi%2FImage%2011-13-21%20at%2010.34%20PM.jpg?alt=media\&token=27cd7020-7455-42c0-bb9c-90ba3faf4eb6)

![output 1.3](https://2282290372-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MJcwT61yCiz5H4ZddkF%2Fuploads%2F6kmPeazOI0RfamPc0NoN%2FScreen%20Shot%202021-11-13%20at%2010.48.21%20PM.png?alt=media\&token=143b912d-b351-4392-b9b1-2e4932c47366)

**We can see that by employing this strategy, our final portfolio value equates $6.5mil with an overall increase of 554.94% from our initial cash value thereby surpassing the 191.68% performance of the `S&P500` <-> Buy\&Hold Strategy over the last 20years**. From *2003 to 2007*, we were able to capture a huge upswing in the markets`(output 1.1)` and adjacently we were also able to avoid some of the major drawdowns in the market. As illustrated above `(output 1.3)`, our maximum drawdown was `22.44%` sparing us the `50%` drawdown of the [bear market of 2007-2009](https://en.wikipedia.org/wiki/United_States_bear_market_of_2007%E2%80%932009) since we had sold our position on 12-31-2007. Similarly, with the coronavirus pandemic which sent equity markets reeling as the `S&P500` plummeted `51%`, our trading algorithm was able to evade the eye of this crisis as we exited on *02-28-2020.*&#x20;

Overall, quite interesting to observe how splendid our simple startegy performed *(No machine learning needed)*. It'd be very interesting to backtest some learning algorithms and see how they fare against this simple yet solid strategy.

{% embed url="<https://github.com/SebasIvan26/SMA-Backtesting>" %}

References: <https://www.newtraderu.com/2019/10/05/moving-average-trading-strategy-that-crushes-buy-and-hold/>
