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) |
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') |
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 |
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
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