# 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 ](/files/kkRWmIRJT0AiHXchUsmG)

### 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)}%")
```

![](/files/A2CC5Y2mXH3ZS4i4tRyY)

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](/files/uUoWABK0DlaVdQk1ha1o)

![output 1.2](/files/EdaYMTBOIAPjsRW4jLAH)

![output 1.3](/files/F6CuKXeWG2ctYWy4gV2T)

**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–2009) 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/>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://kseb0.gitbook.io/whiteboard/projects/backtest-equity-trading-with-sma-strategy.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
