Momentum Trading Strategy

 



Momentum Trading Strategy  

In this post I will show you how you can develop a simple momentum trading strategy and evaluate its potential use and statistics. This could be the good foundation for more advanced momentum trading strategies. The general premise this trading signal is that outperforming stocks keep to outperform in some time in a particular market and vice versa for underperformers.

In this momentum trading strategy, we going to buy outperformers and short underperformers.

So, let’s start.

First, we need to import our packages.

1.  import yfinance as yf  

2.  import pandas as pd  

3.  import numpy as np  

For building our trading strategy we need to find our top outperformers and underperformers among a group of stocks or index.

I use SP500 socks for this example.

We need to pull all SP500 members historical data. You can find the list of SP500 stocks from the below address and save it as .csv format.

SP500 list from https://datahub.io/core/s-and-p-500-companies#data 

 

Now it’s time to download all SP500 stocks prices.

First, we need to get SP list from our Sp500.csv file and prepare it for the stocks download function.

1.  SP=pd.read_csv('YOUR WORKING DIRECTORY~ /SP500.csv')  

# Now lets see how the SP500.csv looks like

 


We need to change the SP500 list to a format that can be used by our function. We just need the Symbol column for this example.

2.  SP500=list(SP.Symbol)  

3.  watchlist='^GSPC'  

4.  for i in SP500:  

5.          watchlist= watchlist + " "+i  

 

After preparing the SP list we use it to download the daily prices.

1.  df = yf.download(watchlist,period='5y')  

2.  df=df.iloc[1:,:]  

Now let’s look at our data.

It has 1261 rows and 3036 columns.



For each ticker we have 6 columns what we need is just the close price. Next step is to extract the close prices.

Let’s first create a Data Frame for the SP500 index close price itself and then add the stocks prices to that Data Frame.

1.  close=pd.DataFrame(pd.DataFrame(df[('Adj Close''^GSPC')])['Adj Close',])  

2.  for symbol in SP500:    

3.      close = close.join(pd.DataFrame(df[('Adj Close', symbol)])['Adj Close',],how="left")

4.  apple_ticker = 'AAPL'

5.  close[apple_ticker].plot()

Now it is time to see how Apple prices look like.

As you see we have daily price data for this project we want to use either weekly or monthly data so, we need to resample the prices into specific frequency. Here we resample close prices in monthly frequency which means we need to extract the close price of last day of each month.

1.  def resample_prices(close_prices, freq='M'):  

2.      """ 

3.      Resample close prices for each ticker at specified frequency. 

4.         

5.      Returns 

6.          Resampled prices for each ticker and date 

7.      """        

8.    return close_prices.resample(freq).last() 

9.  Print(resample_prices(close))  


For the next step we need to compute log returns of stocks. We do it by defining the compute_log_returns function below. In this function we need to subtract logarithm of each price with the logarithm of the previous price.

1.  def compute_log_returns(prices):  

2.      """ 

3.      Compute log returns for each ticker. 

4.       

5.      Parameters 

6.      ---------- 

7.      prices : DataFrame 

8.          Prices for each ticker and date 

9.       

10.     Returns 

11.     ------- 

12.     log_returns : DataFrame 

13.         Log returns for each ticker and date 

14.     """        

15.     return  np.log(prices) - np.log(prices.shift(1)) 

16. log_returns = compute_log_returns(close)  

 


Like what we have done in previous function for the stock prices we define a function to shift our log returns either forward or backward.

1.  def shift_returns(returns, shift_n):  

2.      """ 

3.      Generate shifted returns 

4.       

5.      Parameters 

6.      ---------- 

7.      returns : DataFrame 

8.          Returns for each ticker and date 

9.      shift_n : int 

10.         Number of periods to move, can be positive or negative 

11.      

12.     Returns 

13.     ------- 

14.     shifted_returns : DataFrame 

15.         Shifted returns for each ticker and date 

16.     """  

17.       

18.     return returns.shift(shift_n)  

 Now it’s time to use our functions and assign the results to variables.

1.  monthly_close = resample_prices(close)  

2.  monthly_close_returns = compute_log_returns(monthly_close)  

3.  prev_returns = shift_returns(monthly_close_returns, 1)  

4.  lookahead_returns = shift_returns(monthly_close_returns, -1)  


We are reaching the interesting parts, generating trading signal. For our momentum trading strategy, we need to find top performer stocks by ranking all previous returns and pick top (bottom) stocks for long (short) positions.

So for each date we iterate through rows and find top returns and assign 1 to n and 0 to the rest.

1.  def get_top_n(prev_returns, top_n):  

2.      """ 

3.      Select the top performing stocks 

4.       

5.      Parameters 

6.      ---------- 

7.      prev_returns : DataFrame 

8.          Previous shifted returns for each ticker and date 

9.      top_n : int 

10.         The number of top performing stocks to get 

11.      

12.     Returns 

13.     ------- 

14.     top_stocks : DataFrame 

15.         Top stocks for each ticker and date marked with a 1 

16.     """  

17.       

18.     # Create independent copy and assign 0 to it  

19.     prev_returns_signal=prev_returns.copy()  

20.       

21.     for col in prev_returns_signal.columns:  

22.         prev_returns_signal[col].values[:] = 0  

23.           

24.    #itterate through the rows and get top n stock for each month + assign 1 to top n   

25.     for dates in list(prev_returns.index):      

26.        

27.         my_topn = list(next(prev_returns.loc[dates:,].iterrows())[1].nlargest(top_n).index)  

28.           

29.         prev_returns_signal.loc[dates,my_topn] = 1   

30.           

31.     return prev_returns_signal.astype('int64')  

 

 Now it’s the time to find our top 10 longs and top 10 shorts stocks for each month.

The important point here is we can find top 10 outperform by using the same function and just multiply -1 to the returns Data Frame.

1.  top_bottom_n = 10  

2.  df_long = get_top_n(prev_returns, top_bottom_n)  

3.  df_short = get_top_n(-1*prev_returns, top_bottom_n)  

In df_long Data Frame all top 10 stocks (outperformers) for each month are market with 1 and the rest are market with 0.

In df_short Data Frame all top 10 stocks (underperformers) for each month are market with 1 and the rest are market with 0.

Here are the top long and top short stocks based our momentum trading strategy.

It's now time to check if your trading signal has the potential to become profitable!

We'll start by computing the net returns this portfolio would return. For simplicity, we'll assume every stock gets an equal dollar amount of investment (i.e. equally weighted)

The portfolio consists of long and short positions. In order to compute the returns correctly we need to consider the correct structure of the short positions in the portfolio.

Portfolio = long positions + (-short positions)

In the following portfolio_returns function the portfolio Data Frame consist of 1 for long and -1 for short positions and 0 for the rest stocks.

At the end we need to take the average return of stocks (10 + 10 = 20 stocks in this example)

1.  def portfolio_returns(df_long, df_short, lookahead_returns, n_stocks):  

2.      """ 

3.      Compute expected returns for the portfolio, assuming equal investment in each long/short stock. 

4.       

5.      Parameters 

6.      ---------- 

7.      df_long : DataFrame 

8.          Top stocks for each ticker and date marked with a 1 

9.      df_short : DataFrame 

10.         Bottom stocks for each ticker and date marked with a 1 

11.     lookahead_returns : DataFrame 

12.         Lookahead returns for each ticker and date 

13.     n_stocks: int 

14.         The number of stocks chosen for each month 

15.      

16.     Returns 

17.     ------- 

18.     portfolio_returns : DataFrame 

19.         Expected portfolio returns for each ticker and date 

20.     """  

21.     portfo = df_long + -df_short  

22.      

23.     return (lookahead_returns.mul(portfo))/n_stocks  

 The below statistics help us to understand the portfolio expected returns better.

1.  expected_portfolio_returns = portfolio_returns(df_long, df_short, lookahead_returns, 2*top_bottom_n)  

2.  expected_portfolio_returns.T.sum().plot()  

3.  expected_portfolio_returns.T.sum().sum()  

4.    

5.    

6.  expected_portfolio_returns_by_date = expected_portfolio_returns.T.sum().dropna()  

7.  portfolio_ret_mean = expected_portfolio_returns_by_date.mean()  

8.  portfolio_ret_ste = expected_portfolio_returns_by_date.sem()  

9.  portfolio_ret_annual_rate = (np.exp(portfolio_ret_mean * 12) - 1) * 100  

10.   

11. print(""" 

12. Mean:                       {:.6f} 

13. Standard Error:             {:.6f} 

14. Annualized Rate of Return:  {:.2f}% """.format(portfolio_ret_mean, portfolio_ret_ste, portfolio_ret_annual_rate))

 Now let’s look at the statistics.


T-Test

Our null hypothesis (𝐻0) is that the actual mean return from the signal is zero. We'll perform a one-sample, one-sided t-test on the observed mean return, to see if we can reject 𝐻0.

We'll need to first compute the t-statistic, and then find its corresponding p-value. The p-value will indicate the probability of observing a t-statistic equally or more extreme than the one we observed if the null hypothesis were true. A small p-value means that the chance of observing the t-statistic we observed under the null hypothesis is small, and thus casts doubt on the null hypothesis. It's good practice to set a desired level of significance or alpha (𝛼before computing the p-value, and then reject the null hypothesis if 𝑝<𝛼.

For this project, we'll use 𝛼=0.05α=0.05, since it's a common value to use.

15. from scipy import stats  

16.   

17. def analyze_alpha(expected_portfolio_returns_by_date):  

18.     """ 

19.     Perform a t-test with the null hypothesis being that the expected mean return is zero. 

20.      

21.     Parameters 

22.     ---------- 

23.     expected_portfolio_returns_by_date : Pandas Series 

24.         Expected portfolio returns for each date 

25.      

26.     Returns 

27.     ------- 

28.     t_value 

29.         T-statistic from t-test 

30.     p_value 

31.         Corresponding p-value 

32.     """  

33.     test = stats.ttest_1samp(expected_portfolio_returns_by_date,0)  

34.     return (test.statistic,test.pvalue/2)  

 

 

As you can see the P-Value is not small enough and we can’t reject the null hypothesis. The null hypothesis assumes that the data distribution is normal. If the p-value is greater than the chosen p-value i.e. 0.05 here, we'll assume that it's normal. Otherwise we assume that it's not normal



Comments

  1. Really helpful down to the ground, happy to read such a useful post. I got a lot of information through it and I will surely keep it in my mind. Keep sharing. Learn about forex trading and Instant funding prop firm.

    ReplyDelete

Post a Comment

Popular posts from this blog

Application of GARCH models in R – Part II ( APARCH)

How to scrap Zacks Rank signal in Python

How to check if a distribution is normal?