Is there a monthly basis to the S&P markets? Can we locate a monthly short-term swing pattern? Let's find out.
Lately, I've seen some videos about short-term patterns. For example, buy near the close of the month and hold until the next month. Or, buy the first trading day of the month and close it three days later. This got me thinking about creating a simple market study to test short-term trading trends based on each month.
So, I created a simple EasyLanguage strategy that would buy the N day of the month and sell X days later. What does that mean?
The Monthly Bias Strategy Concept
Well, N will be the number of days in the month. One will be the first day of the month. Two will be the second day of the month, and so on. For example, if N is two, we're going to place an order to buy at the open of the next bar. If that next bar is not a trading day, TradeStation will delay the order until the next trading day.
This code is not perfect, but it will give us a rough idea if our concept is viable. Ideally I would like to have a function that will give me just the trading days so when I say N=5 it will be entering on the 5th trading day. I don't have a function to do that. If you know of an EasyLanguage function, please let me know in the comments below.
Once a trade is opened, we'll hold it for X days. After that, we'll close it at the open of the next bar.
These two values will be inputs so I can use TradeStation's optimization feature to test a range of values quickly. The inputs will look like this.
- OpenDay(10),
- HoldDays(3)
Given the above example, this would indicate a new trade is signaled on the 10th day of the month and should be closed 3 bars later.
I also added a few more enhancements to the code.
Regime Filter
I also added a regime filter as defined by a simple moving average. I included as inputs the ability to enable/disable the regime filter, invert the regime filter and modify the lookback period. Here are the inputs.
- EnableRegime(1), // 1 enable 0 disable
- RegimeInvert(0), // 1 invert 0 normal
- RegimeLookback(200), // number of bars in calculation
What does inverting the regime filter mean? Well, this is a long-only study. Thus, a normal regime filter will only allow trades to happen when the market's current price is above the regime filter. In this case, the regime filter is a 200-day simple moving average. Inverting the regime filter will allow trades only to happen when the price is below the 200-day simple moving average.
Limit Trades Once Per Month
Initially, my strategy code was making several trades per month, and I added a bit of code to limit it to once per month. This is controlled by the okToTrade variable.
Testing Environment
Unless otherwise stated, all the tests in this post will have the following assumptions.
- Testing dates: January 1, 2006, to November 2022.
- There will be no slippage or commissions deducted.
- Only one contract will be traded per signal.
Testing The Monthly Bias on E-mini S&P
The first test will be on the E-mini S&P futures (@ES) without the regime filter. I will optimize the "OpenDay" input with values 1-31 and the "HoldDays" input with values 0-4. We start at zero for "HoldDays," meaning we hold for 1 bar. So, we're holding for 1-5 bars. This gives us 155 unique combinations to test.
118 of the 155 (76%) combinations make money. That's a good sign. I picked the optimal value based on perfect profit correlation, and that test was...
- OpenDay = 23
- HoldDays = 4
Next, let's test the behavior above and below the regime filter. I performed an optimization for each of the situations utilizing the same range of values as above. Why split the market by a regime filter? My reasoning is market behavior changes between these two regime states. I picked the best value for each optimization based on perfect profit correlation. Here are the results
Bull Market
Profitable Tests: 114/155 (74%)
OpenDay = 3
HoldDays = 3
Bear market
Profitable Tests: 101/155 (65%)
OpenDay = 22
HoldDays = 3
Below is a table with all three results.
No Regime | Bull | Bear | |
---|---|---|---|
%Profitable | 76% | 74% | 65% |
OpenDay | 23 | 3 | 22 |
HoldDays | 4 | 3 | 3 |
When price is trading above the regime filter (Bull), we can see the first few days of the month hold the best promise. In this case, we're entering on the 3rd day and holding for three days.
However, when price is below the regime filter (Bear), more toward the end of the month is where we see our bias—in this case, entering on the 22nd day and holding for three days.
Below is the performance of each of these three different trading systems with their optimized values based upon perfect profit correlation.
No Regime | Bull Only | Bear Only | |
---|---|---|---|
Net Profit | $61,163 | $57,425 | $69,550 |
Profit Factor | 1.80 | 1.81 | 3.01 |
Total Trades | 194 | 159 | 54 |
%Winners | 64% | 66% | 69% |
Avg.Trade Net Profit | $315 | $361 | $1,288 |
Max Drawdown | $16,738 | $13,100 | $14,275 |
NPDD | 3.7 | 4.4 | 4.9 |
These numbers look good. Looks like we have a promising trading system that is simple to trade. Anyone can do it!
You can imagine combing the Bull Only and Bear Only into a single killer strategy. I think we can build a good trading system from this study.
Is This Valid?
But before we do that, I would like to perform another test. My fear with our current analysis is all our data is in-sample, and this approach is not statistically valid.
Did you catch that? If you did CONGRATULATIONS!
Our results look great. It's tempting to add as stop and start trading. But that would be a mistake. Our results are probably overly optimistic, and at worst, they might be an illusion. Our results might be fooling us.
I'm skeptical of using backtested, in-sample results, used to discover monthly patterns, and expect good real-world results.
One way around this is to leave a segment of historical data as out-of-sample. For example, leave the last four years out of your in-sample backtest. Then locate your favored parameters and move the strategy to the three years of unseen data. The strategy results on the out-of-sample are what you would use to judge the quality of your strategy.
But I'm going to try something else.
I want to validate our results using a rolling window over the historical data. Thus, I will use TradeStation's Walk Forward Optimizer to test our idea further.
Testing on Walk Forward Optimizer
I first tested only taking trades in a bull market. Once again, I optimize the "OpenDay" input with values 1-31 and the "HoldDays" input with values 0-4. Then used the optimizer to perform a cluster analysis.
The results are not optimistic. We can see many of the tests failed.
I then performed the same test during a bear market and again, not looking so good.
I then decided to remove the regime filter and test once more. This looked better.
This had several more passing tests, but it was still far from ideal.
This is not to say we can't make a trading system from this concept. Some people may be trading something like this. Maybe that person is you. However, this makes me pause that such a simple system would work on the live market.
How would I proceed?
When I started working on this study, the results looked promising. However, the rolling window for optimizing the two input values threw a cold water on that hot idea.
This article is an excellent example of how we can fool ourselves into thinking we found an edge and this could lead us to trading live and losing money. Of course, maybe we do have a valid edge! But, when we tried a different method of analyzing the input parameters, the results don't look good. Thus, caution is warranted.
So, for moving forward, I would put this concept into incubation. Take the results of the Walk Forward Optimizer without the regime filter, which is the last WFO test we did. Select the optimal result, the five runs with 20% OOS. Here are the optimal parameters for the test:
OpenDay = 14
HoldDays = 0
These settings are the latest optimization that are valid through September 4, 2024. I would put this into incubation and see how it performs in 2023. That would be a good starting point.
Excellent article and example. Thank you!
You’re welcome. Glad you liked it.
Jeff… with trying to find the trading day of the month could you do something like this…. If Month(Date) Month(Date[1]) then DayCounter=0. If Dayofweek saturday or sunday then DayCounter=DayCounter+1. This would not get holidays, but I think would count the number of days since the first of the month. Is that what you are looking for?