3.2 Simple Moving Averages Strategies

From Forbes How a Top Trader Uses Moving Average Crossovers

But I’m telling you, and don’t tell anyone this one, Kate: A 15/30 crossover is all you need, or a 10/20 depending on how quick you want to get out. Keep in as long as the ten is above the 20. Most people don’t know anything about that, and the sad part is nobody’s going to tell them.

Isn’t that awful? I had a girl one time call me, and she asked me if I traded Japanese stocks or if I had a model for the Japanese market? I said I do not. Then I sat with her and I set up this program, and I think I used a 15/30, we’ll say. And I said, “Now give me a symbol.”

She gives me a symbol, Japanese symbol, I didn’t even know if it would come up. We put it in and it popped up, and her first words were, “Oh my God.”

I said, “What?” She said, “I sold that too soon.”

I said, “How do you know that?” She said, “Well, if you look at the chart, you can see that the 15-day is above the 30-day, and it was above when I sold it. I should have waited for it to turn down. And now it’s turned down and the stock is dropping.”

Interesting theory. Let’s test it.

Import Packages

This package import includes functions developed in section 2.

In [1]:
import numpy as np
import pandas as pd
import datetime as dt
import seaborn as sns
import math
import helper_functions as hf
%matplotlib inline
from scipy import stats
import cPickle as pickle

shortUniverse = True
loadResults = False

Load Pickle Data

First, we’ll load the pickle we developed in the previous section. The new prices dataframe is given a new name to reflect edits made during this notebook.

In [2]:
if shortUniverse:
    with open('intermediaries/prices.p', 'rb') as handle:
        prices = pickle.load(handle)
else:
    with open('intermediaries/prices-full.p', 'rb') as handle:
        prices = pickle.load(handle)

Simple Moving Average Crossovers

15-day vs 30-day

The following function adds a 15-30 day MA crossover column and tests the results. A bull (bear) signal occurs when the shorter-run MA opens below (above) the longer-run MA and closes above (below) it.

This can only occur when the recent data is higher than the longer-run data. If this theory works, we should continue to see higher returns over the next few days.

In [3]:
def add_ma_15_30_cross(df):
    df = hf.add_MA(df, lag=15)
    df = hf.add_MA(df, lag=30)
    
    df['ma15_ma30_cross'] = 'neut'
    df.loc[((df['ma_15_lag_1'] < df['ma_30_lag_1']) & (df['ma_15'] > df['ma_30'])),'ma15_ma30_cross'] = 'bull'
    df.loc[((df['ma_15_lag_1'] > df['ma_30_lag_1']) & (df['ma_15'] < df['ma_30'])),'ma15_ma30_cross'] = 'bear'
    df.drop(['ma_15', 'ma_15_lag_1', 'ma_30', 'ma_30_lag_1'], axis=1, inplace=True)
    return df

results_ma_15_30_cross = hf.strat_results(add_ma_15_30_cross(prices),
                                          strat='ma15_ma30_cross',
                                          lag=5)
results_ma_15_30_cross
Out[3]:
bear bull
count mean std min 50% max testStat p-value count mean std min 50% max testStat p-value
signal_age
0 488 -0.000214 0.009984 -0.040889 -0.000711 0.031200 -0.000968 0.999228 410 0.000446 0.010940 -0.035103 7.848041e-09 0.083806 0.002015 0.998393
1 11 0.003618 0.017531 -0.033144 0.007458 0.035605 0.062216 0.951617 14 -0.002481 0.007733 -0.015114 -2.633339e-03 0.014715 -0.085737 0.932982
2 11 0.001289 0.010051 -0.013813 -0.001468 0.018892 0.038675 0.969911 13 0.000661 0.007653 -0.011685 -1.205725e-04 0.015234 0.023966 0.981273
3 11 0.001092 0.010310 -0.006823 -0.002157 0.030039 0.031929 0.975157 13 -0.000665 0.012880 -0.023553 -1.219944e-03 0.027221 -0.014328 0.988804
4 11 0.000138 0.008537 -0.009555 -0.002802 0.016179 0.004876 0.996206 13 0.003985 0.008193 -0.009849 3.635094e-03 0.021775 0.134904 0.894924
5 11 -0.001498 0.010329 -0.025365 0.001415 0.012467 -0.043738 0.965974 13 -0.000177 0.009154 -0.018831 -1.209856e-04 0.014500 -0.005348 0.995821

10-day vs 20-day

The following code recreates this analysis on the 10 and 20 day moving average level.

In [4]:
def add_ma_10_20_cross(df):
    df = hf.add_MA(df, lag=10)
    df = hf.add_MA(df, lag=20)
    
    df['ma10_ma20_cross'] = 'neut'
    df.loc[((df['ma_10_lag_1'] < df['ma_20_lag_1']) & (df['ma_10'] > df['ma_20'])),'ma10_ma20_cross'] = 'bull'
    df.loc[((df['ma_10_lag_1'] > df['ma_20_lag_1']) & (df['ma_10'] < df['ma_20'])),'ma10_ma20_cross'] = 'bear'
    df.drop(['ma_10', 'ma_10_lag_1', 'ma_20', 'ma_20_lag_1'], axis=1, inplace=True)
    return df

results_ma_10_20_cross = hf.strat_results(add_ma_10_20_cross(prices),
                                          strat='ma15_ma30_cross',
                                          lag=5)
results_ma_10_20_cross
Out[4]:
bear bull
count mean std min 50% max testStat p-value count mean std min 50% max testStat p-value
signal_age
0 488 -0.000214 0.009984 -0.040889 -0.000711 0.031200 -0.000968 0.999228 410 0.000446 0.010940 -0.035103 7.848041e-09 0.083806 0.002015 0.998393
1 11 0.003618 0.017531 -0.033144 0.007458 0.035605 0.062216 0.951617 14 -0.002481 0.007733 -0.015114 -2.633339e-03 0.014715 -0.085737 0.932982
2 11 0.001289 0.010051 -0.013813 -0.001468 0.018892 0.038675 0.969911 13 0.000661 0.007653 -0.011685 -1.205725e-04 0.015234 0.023966 0.981273
3 11 0.001092 0.010310 -0.006823 -0.002157 0.030039 0.031929 0.975157 13 -0.000665 0.012880 -0.023553 -1.219944e-03 0.027221 -0.014328 0.988804
4 11 0.000138 0.008537 -0.009555 -0.002802 0.016179 0.004876 0.996206 13 0.003985 0.008193 -0.009849 3.635094e-03 0.021775 0.134904 0.894924
5 11 -0.001498 0.010329 -0.025365 0.001415 0.012467 -0.043738 0.965974 13 -0.000177 0.009154 -0.018831 -1.209856e-04 0.014500 -0.005348 0.995821

50-day vs 200-day

Long term crossovers, particularly of the 50- and 200-day moving averages, are commonly called “golden crosses” and “death crosses,” and they are frequently discussed by the financial media. They are tested below.

In [5]:
def add_ma_50_200_cross(df):
    df = hf.add_MA(df, lag=50)
    df = hf.add_MA(df, lag=200)
    
    df['ma50_ma200_cross'] = 'neut'
    df.loc[((df['ma_50_lag_1'] < df['ma_200_lag_1']) & (df['ma_50'] > df['ma_200'])),'ma50_ma200_cross'] = 'bull'
    df.loc[((df['ma_50_lag_1'] > df['ma_200_lag_1']) & (df['ma_50'] < df['ma_200'])),'ma50_ma200_cross'] = 'bear'
    df.drop(['ma_50', 'ma_50_lag_1', 'ma_200', 'ma_200_lag_1'], axis=1, inplace=True)
    return df

results_ma_50_200_cross = hf.strat_results(add_ma_50_200_cross(prices),
                                          strat='ma15_ma30_cross',
                                          lag=5)
results_ma_50_200_cross
Out[5]:
bear bull
count mean std min 50% max testStat p-value count mean std min 50% max testStat p-value
signal_age
0 488 -0.000214 0.009984 -0.040889 -0.000711 0.031200 -0.000968 0.999228 410 0.000446 0.010940 -0.035103 7.848041e-09 0.083806 0.002015 0.998393
1 11 0.003618 0.017531 -0.033144 0.007458 0.035605 0.062216 0.951617 14 -0.002481 0.007733 -0.015114 -2.633339e-03 0.014715 -0.085737 0.932982
2 11 0.001289 0.010051 -0.013813 -0.001468 0.018892 0.038675 0.969911 13 0.000661 0.007653 -0.011685 -1.205725e-04 0.015234 0.023966 0.981273
3 11 0.001092 0.010310 -0.006823 -0.002157 0.030039 0.031929 0.975157 13 -0.000665 0.012880 -0.023553 -1.219944e-03 0.027221 -0.014328 0.988804
4 11 0.000138 0.008537 -0.009555 -0.002802 0.016179 0.004876 0.996206 13 0.003985 0.008193 -0.009849 3.635094e-03 0.021775 0.134904 0.894924
5 11 -0.001498 0.010329 -0.025365 0.001415 0.012467 -0.043738 0.965974 13 -0.000177 0.009154 -0.018831 -1.209856e-04 0.014500 -0.005348 0.995821

Simple Moving Average Bounces – Support Resistance

We will also test the theory that a price hitting testing a moving average line will “bounce” off the line.

A bull (bear) signal occurs if the stock opens above (below) today’s moving average, has a low (high) below (above) today’s moving average, yet finishes above (below) the moving average.

10-day MA bounce

In [6]:
def add_ma_bounce_10(df):
    df = hf.add_MA(df, lag=10)
    
    df['ma_bounce_10'] = 'neut'
    df.loc[(df['ma_10'] < df['open']) & (df['ma_10'] > df['low']) & (df['ma_10'] < df['close']),'ma_bounce_10'] = 'bull'
    df.loc[(df['ma_10'] > df['open']) & (df['ma_10'] < df['high']) & (df['ma_10'] > df['close']),'ma_bounce_10'] = 'bear'
    df.drop(['ma_10', 'ma_10_lag_1'], axis=1, inplace=True)
    return df

results_ma_bounce_10 = hf.strat_results(add_ma_bounce_10(prices),
                                          strat='ma_bounce_10',
                                          lag=5)
results_ma_bounce_10
Out[6]:
bear bull
count mean std min 50% max testStat p-value count mean std min 50% max testStat p-value
signal_age
0 653 -0.000171 0.010014 -0.040889 -0.000608 0.035605 -0.000667 0.999468 605 0.000344 0.010437 -0.040889 -0.000242 0.083806 0.001338 0.998933
1 12 0.000580 0.011772 -0.021191 0.001131 0.025958 0.014230 0.988901 26 0.000859 0.009580 -0.015870 -0.000290 0.026385 0.017586 0.986109
2 9 0.004848 0.009585 -0.011940 0.007454 0.017120 0.168597 0.870298 17 0.000759 0.010100 -0.027862 -0.000249 0.015156 0.018218 0.985690
3 8 0.002836 0.013522 -0.024547 0.006459 0.018232 0.074154 0.942962 12 -0.002341 0.007051 -0.012521 -0.002399 0.011261 -0.095823 0.925385
4 6 -0.007661 0.016176 -0.029610 -0.006652 0.015837 -0.193340 0.854302 10 -0.004621 0.009197 -0.021522 -0.003686 0.008654 -0.158894 0.877261
5 6 0.005755 0.009323 -0.001933 0.003411 0.023739 0.252023 0.811055 8 0.001620 0.006961 -0.012579 0.004189 0.009125 0.082273 0.936732

30-Day MA Bounce

In [7]:
def add_ma_bounce_30(df):
    df = hf.add_MA(df, lag=30)
    
    df['ma_bounce_30'] = 'neut'
    df.loc[(df['ma_30'] < df['open']) & (df['ma_30'] > df['low']) & (df['ma_30'] < df['close']),'ma_bounce_30'] = 'bull'
    df.loc[(df['ma_30'] > df['open']) & (df['ma_30'] < df['high']) & (df['ma_30'] > df['close']),'ma_bounce_30'] = 'bear'
    df.drop(['ma_30', 'ma_30_lag_1'], axis=1, inplace=True)
    return df

results_ma_bounce_30 = hf.strat_results(add_ma_bounce_30(prices),
                                          strat='ma_bounce_30',
                                          lag=5)
results_ma_bounce_30
Out[7]:
bear bull
count mean std min 50% max testStat p-value count mean std min 50% max testStat p-value
signal_age
0 671 -0.000085 0.010200 -0.040889 -0.000476 0.035605 -0.000324 0.999742 607 0.000254 0.010322 -0.040889 -0.000476 0.083806 0.000997 0.999205
1 10 0.000657 0.010538 -0.021522 0.001427 0.015156 0.019720 0.984697 15 -0.004206 0.008575 -0.021191 -0.004736 0.007452 -0.126643 0.901024
2 8 0.004576 0.002532 0.001059 0.004447 0.007995 0.639116 0.543087 11 0.003646 0.010997 -0.005614 -0.000479 0.026662 0.099951 0.922358
3 5 0.003998 0.013958 -0.012185 0.000514 0.025958 0.128092 0.904258 7 -0.005205 0.009610 -0.020525 -0.003327 0.011261 -0.204706 0.844569
4 5 -0.004169 0.006593 -0.011940 -0.006568 0.004453 -0.282841 0.791332 7 0.001999 0.006667 -0.006410 0.001930 0.014376 0.113314 0.913478
5 4 -0.004799 0.017857 -0.024547 -0.002987 0.011326 -0.134368 0.901620 6 -0.002872 0.011753 -0.015028 -0.007548 0.017744 -0.099754 0.924416

200-Day MA Bounce

In [8]:
def add_ma_bounce_200(df):
    df = hf.add_MA(df, lag=200)
    
    df['ma_bounce_200'] = 'neut'
    df.loc[(df['ma_200'] < df['open']) & (df['ma_200'] > df['low']) & (df['ma_200'] < df['close']),'ma_bounce_200'] = 'bull'
    df.loc[(df['ma_200'] > df['open']) & (df['ma_200'] < df['high']) & (df['ma_200'] > df['close']),'ma_bounce_200'] = 'bear'
    df.drop(['ma_200', 'ma_200_lag_1'], axis=1, inplace=True)
    return df

results_ma_bounce_200 = hf.strat_results(add_ma_bounce_200(prices),
                                          strat='ma_bounce_200',
                                          lag=5)
results_ma_bounce_200
Out[8]:
bear bull
count mean std min 50% max testStat p-value count mean std min 50% max testStat p-value
signal_age
0 749 0.000146 0.010583 -0.040889 -0.000343 0.083806 0.000504 0.999598 749 0.000146 0.010583 -0.040889 -0.000343 0.083806 0.000504 0.999598

Potential Extensions

This space is reserved.