Strategy 6 — A lean mean-reverting machine.
I tweak a mean reversion strategy and test it against index constituents (current and delisted) from 2021 and got a ROR of 115%, MaxDD -20%, 59% win rate, PF 1.73, a Sharpe of 2.36 and 1,314 trades.
Disclaimer: the following post is an organized representation of my research and project notes. It doesn’t represent any type of advice, financial or otherwise. Its purpose is to be informative and educational. Backtest results are based on historical data, not real-time data. There is no guarantee that these hypothetical results will continue in the future. Day trading is extremely risky, and I do not suggest running any of these strategies live.
Welcome back, fellow traders, hunters, developers, and nerds!
If you can't tell, I've postponed doing any programming-oriented posts for a while. I had a couple of strategies stuck in my brain that I wanted to get out to y'all, and this mean-reversion strategy is the next one in line. Full disclosure, I pulled the concept for this strategy from the RealTest for Dummies guide a RealTest user put together for the community. Of course, I took the parts from it that worked and added some concepts I've learned recently to improve the strategy a little. However, you can easily read through that guide and get a pretty solid understanding of how this strategy is going to work without having read through this post that may or may not contain a story about the author being caught naked during a time he would have liked to have had some clothing on… and maybe a weapon.
I've been following a fellow trader/quant researcher, Quantitativo, recently, as they have also been researching some mean reversion trading strategies. I am very hesitant to compare myself to this researcher, as it is clear that we are of different pedigree. I'm just a knuckle dragger who learned how to sign with humans. Larry, good gorilla.
Anyway, it just happened to coincide with my reading of the RealTest for Dummies guide, which uses a type of mean reversion strategy for teaching how to use RealTest. As I was going through this guide, I also worked on the other strategies I've been posting using TradingView or RealTest. The strategy in the guide has decent results, but it was also written several years ago, and it is being used as a tutorial for those trying to figure out how to work RealTest. I saw this as an opportunity to learn and improve, and I wanted to see if I could tweak it a little and get better results. We're on this journey together, and I'm excited to share my progress with you.
Much like Quantitativo, I doubt that mean-reversion strategies are dead. Time keeps moving, events happen, and markets change. This is a feature, not a bug. The goal of a trader or researcher is to change with them. There is nothing worse than getting caught unaware or with your pants down.
There the segue is. I wasn't sure I could pull this one off, but it looks like I still have a bit of creativity left in me.
Larry, creative gorilla.
Lesson from the Past
We are still farther back in time. I’m still a young private in the Army, and we haven’t moved into GarryOwen yet, and I’m living with the Iraqi Army (IA). These days were some of the best I had during deployment. We ran missions during the day (unless it was Friday) and kicked it with the IA guys when we weren’t. Some of us, the ones who liked learning about different cultures, used to play soccer (read, get our asses kicked) with some of them, and in the evenings, we would sit down with a few of them and an interpreter in the laundry room and eat local food and try and learn some Arabic. The food was pretty damn good, but it was very different, and you needed to keep a bottle of Pepto on standby, just in case.
My squad was originally living in a tent on base, but after we had a few late-night problems with some IA guys masturbating outside our tent (not a joke), we moved into an empty concrete room that was just down the way from the showers and toilets (the Iraqi ones). Since you can’t see through concrete, the issue with weirdos touching themselves dissipated, or at least we didn’t know it was happening. Whatever the case, it was a nice change.
During a typical day, after a mission, I would shower in the IA facilities. Some soldiers abstained from doing this. The water only had one temperature (chilly), and sometimes it smelled worse than we did. Those soldiers decided that wet wipes were good enough (it wasn’t) until we got our own shower facilities. Not me. The water wasn’t potable, but taking a fast shower was fine. If it smelled bad (like sulfur), you just waited a few minutes, and it would go away. The trickiest part about showering here was leaving all your belongings in the room with the rest of the squad. It wasn’t uncommon for some or all of your shit to disappear if you left it in the shower room while taking a shower, and no one wants to chase a thief down soaking wet and naked. That meant I walked to the showers with my toiletry bag, wearing only a towel and my busted flip-flops. Hot.
Well, one day, like any other day, I went and took a shower. The shower itself was uneventful. After I had toweled off and wrapped my towel back around my waist to trek back to the room, I decided I was going to take a quick piss before leaving. I crossed the room to the toilets and opened the first stall. As I opened it and looked down at the hole in the ground, I saw a giant camel spider (larger than my hand; most weren’t this big). He saw me, raised his front legs (which I believe are antennae), and charged.
These fuckers move fast. I screamed (I’m sure) and jumped straight out of my towel. Not surprising, really. It was just wrapped around me and tucked back into itself. It’s not a garment designed for battle, and the battle was underway whether or not I wanted it to be. Except, now all I was wearing were some thin-ass busted flip-flops. I deduced that my shower shoes would not be good against this enemy. My only option was to retreat quickly. Well, that’s not exactly something you can do well in flip-flops, so they came off too. Now I’m running out of the shower room at full speed, butt-naked, and probably still screaming.
As I exited, I ran right into the biggest fucking Iraqi I had ever seen up until that point. He saw that I was in distress and tried asking me what happened. I spoke zero Arabic, so the only way I knew how to communicate to him that there was a spider-shaped demon in the latrine was by looping my thumbs together and wiggling all my fingers, hoping he would understand that I meant, “There is a big god damn spider in the shower and it tried to kill me.”
His eyes got wide, and he seemed to get excited. He left quickly, returned with what looked like a miniature cricket bat, and then ran into battle without hesitation. I could only see through the door back into the latrine, and what I saw looked slightly like an old cartoon. The spider would run across the door opening (presumably away from his new assailant), followed quickly by the soldier wielding a stick; this happened a few times before I finally heard a thump and a gleeful yip from the IA soldier.
Now, I’ve just been standing there butt-naked for the last minute or two, not realizing that several IA soldiers had stopped at the other end of the walkway. They were all staring at me, confused and possibly entertained. I waved because what the fuck else was I going to do? The guy eventually came out of the shower room holding the dead spider, and everyone at the other end seemed to understand exactly what happened. The Iraqi man was also nice enough to bring my towel and shower shoes, too. He did not, however, bring my dignity back with him. With a solemn shake of his head, I understood that it hadn’t survived the encounter with the spider.
Back to the Present
No one ever thinks about bad shit happening to them in the bathroom. Even the movies don't seem to show this often. One time, when I was living in Stone Mountain, there was this Mexican restaurant at Redan and South Hairston (if you know, you know) I used to go to. On Friday and Saturday nights, it got pretty crowded. One night, a guy sitting at the bar was talking to a woman next to him. Her husband was somewhere there, but the guy didn't know that. He got up to go take a piss and never came back. The jealous husband followed him into the restaurant and waited until he was starting to piss before shooting him. Poor fellow never saw it coming, and the shooter just walked out the back of the restaurant like it never happened.
When markets change, and suddenly, strategies that worked before no longer work, it is like having to fight or flee in a bathroom. We don't think about it happening, yet it's the perfect time to catch someone/something unaware. I don't know if I scared that camel spider more than he scared me, but I know I had to deal with the situation naked. Remember our ABCs (always be cool)? Fighting or running away naked is not cool. There is an argument that winning the fight, despite being naked, could be cool, but that isn't what we are talking about, is it?
Now that I think about it, what are we talking about, Larry?
Backtest Results, other Larry. We’re talking about backtest results.
This is the third most exciting part of the post. Obviously, my stories from my past are first, said Larry’s ego. The second is the strategy criteria and code. Of course, that part is only available to paid subscribers. If you aren’t a paid subscriber, it’s still ok because two out of three ain’t bad.
I also ensured there was no survivorship bias in this one. I'm learning my from my mistakes. Larry, smart gorilla.
The test was run against the S&P 500, Russel 2000, and Nasdaq 100 constituents (past and current). It was only tested back to 2021. This was because 2020 broke a lot of strategies, and I would instead turn off my strategies during wild times like that than try and tune them so that they are robust enough to handle black swan events. When I learn how to identify these account-destroying events via quantified methods, I will start adding them to the strategies. It could still lead to overfitting, given that we only ever have past event information to go on.
When examining the results, it's clear that the long version of the strategy slowed down in 2022 and didn't continue performing as well as the short variation. This aspect of the strategy could certainly benefit from further refinement to enhance its robustness. However, this also presents an opportunity for improvement and future success.
Strategy Stats
Combined Monthly Percent Gains
Equity
Drawdown
Daily Returns
Monthly Returns
Individual Gains
Monte Carlo — Equity
Monte Carlo — Drawdown
Distribution of Gains
Distribution of Excursions
The rest of this post is for paid subscribers. Paid subscribers will have access to the private GitHub where all the Hunt gather Trade code and strategies live. It is continuously being worked and will expand as we explore the world of automated trade systems.
Strategy Logic
The parts of this strategy that differ from the one in the guide are the price range, the liquidity check (I use turnover instead of an average volume), the lack of a market condition indicator (market breadth indicator, et cetera), and the use of the VIX for determining position size. However, I would like to experiment with volatility-driven dynamic sizing in the future. The guide also used Yahoo data and the constituents from the Russel 1000, whereas I used Norgate Data, 3 different index constituent lists with delisted and current equities.
The strategy is not intended to hold any position for very long. It will exit the trade if it doesn’t hit the target price in 2-3 days. It is meant to find short-term (almost scalping) movements back to the mean. There are probably a million different ways to improve this strategy, and I will keep working on it.
Entry Logic:
Price Range: Close price between $5 and $20 (C >= 5 and C <= 20)
Volatility Condition: ATR above 5% of Close price (atr5 > 0.03 * Close)
Liquidity: Weekly turnover greater than $1,000,000 and turnover greater than $500,000(MA(turnover, 5) > 1e6 and turnover > 5e5)
Overbought Condition: A Stock is considered overvalued if its current closing price is higher than the maximum of the opening price, the previous day's closing price, or the 5-day exponential moving average (EMA). It must also exceed this maximum by at least one ATR(5), suggesting the price has risen significantly and may be due for a pullback. (C > Max(Open, Close[1], EMA(C, 5)) + (atr5))
Oversold Condition: A stock is considered undervalued if its current closing price is lower than the minimum of the opening price, the previous day's closing price, or the 5-day EMA. It must exceed this minimum by at least one ATR(5) multiplied by an oversold multiplier. (C < Min(Open, Close[1], EMA(C, 5)) - (atr5 * oversold_multiplier))
Limit Order Entry Logic:
Long Entry Limit: Close price minus ATR(5) times multiplier (C - (atr5 * long_entry_atr)
Short Entry Limit: Close price plus ATR(5) times multiplier (C + (atr5 * short_entry_atr)
Exit Logic:
Long Exit Limit: Fill price plus ATR multiplied by the limit exit multiplier (long_exit_limit = C + (atr5 * limit_exit_multiplier))
Short Exit Limit: Fill price minus ATR multiplied by the limit exit multiplier (long_exit_limit = C + (atr5 * limit_exit_multiplier))
Long Exit Rule: Exit trade when either the turnover is less than 1e8 or when position has been held for X days. (Select(BarsHeld == ShortFixedBarExit, "Time stop", turnover < 1e8, "Turnover Exit")Exit trade when either the turnover is less than 1e7 or when the position has been held for X days. (Select(BarsHeld == LongFixedBarExit, "Time stop", turnover < 1e7, "Turnover Exit"))
Short Exit Rule: Exit trade when either the turnover is less than 1e8 or when position has been held for X days. (Select(BarsHeld == ShortFixedBarExit, "Time stop", turnover < 1e8, "Turnover Exit")
Code
The following code is in RealTest Script. If you have any questions about converting this into a different platform, feel free to message me or e-mail me.
Notes: This is a mean reversions strategy that uses price movement
in comparison to the ATR to determine entry.
Import: // requires Norgate Platinum subscription
DataSource: Norgate
IncludeList: .S&P 500 Current & Past
IncludeList: .Russell 2000 Current & Past
IncludeList: .Nasdaq 100 Current & Past
IncludeList: SPY
StartDate: 1/1/21
EndDate: Latest
SaveAs: norgate_current_and_delisted_index_universe.rtd
Settings:
DataFile: norgate_current_and_delisted_index_universe.rtd
StartDate: 01/01/21
EndDate: Latest
BarSize: Daily
UseAvailableBars: True
AccountSize: 100000
Parameters:
short_entry_atr: from 0.1 to 3 step 0.1 def 2
limit_exit_multiplier: from 0.5 to 3 step 0.1 def 3
ShortFixedBarExit: from 1 to 5 step 1 def 2
LongFixedBarExit: from 1 to 5 step 1 def 2
long_exit_multiplier: from 0.5 to 3 step 0.1 def 1.1
long_entry_atr: from 0.1 to 3 step 0.1 def 1.1
oversold_multiplier: from 0.1 to 3 step 0.1 def 0.75
short_positions: 2
short_allocation: 40
long_positions: 4
long_allocation: 60
Benchmark: Benchmark
// Buy and hold SPY as a benchmark.
// Exit and immediately re-enter on each ex-dividend day to re-invest the dividend.
Side: Long
EntrySetup: Symbol=$SPY
ExitRule: Dividend > 0
Data:
atr5: ATR(5)
turnover: V*C
price_range: C >= 5 and C <= 20
liquid: MA(turnover, 5) > 1e6 and turnover > 5e5
volatile: atr5 > 0.05 * Close
overbought: C > Max(Open, Close[1], EMA(C, 5)) + (atr5)
short_entry_limit: C + (atr5 * short_entry_atr)
short_setup: price_range and volatile and liquid and overbought
oversold: C < Min(Open, Close[1], EMA(C, 5)) - (atr5 * oversold_multiplier)
long_entry_limit: C - (atr5 * long_entry_atr)
long_setup: price_range and liquid and volatile and oversold
Charts:
ema9: EMA(C, 5)
Template: common
Allocation: S.Equity
QtyType: Percent
Commission: Max(1, 0.005 * Shares)
LimitExtra: 0.001 * C
Strategy: strategy_6_short
Using: common
Side: Short
Quantity: short_allocation/short_positions
EntrySetup: short_setup
EntryLimit: short_entry_limit
ExitLimit: FillPrice - (limit_exit_multiplier * atr5)
ExitRule: Select(BarsHeld == ShortFixedBarExit, "Time stop", turnover < 1e8, "Turnover Exit")
Strategy: strategy_6_long
Using: common
Side: Long
Quantity: long_allocation/long_positions
EntrySetup: long_setup
EntryLimit: long_entry_limit
ExitLimit: FillPrice + (long_exit_multiplier * atr5)
ExitRule: Select(BarsHeld == LongFixedBarExit, "Time stop", turnover < 1e7, "Turnover Exit")
Summary
That’s a wrap. We have a simple ATR-driven mean-reversion strategy with a long and short side. There is plenty of room for improvement, and I will improve it as I continue to learn. There are many different methods we can use to create mean-reversion strategies, and the next strategy post (a test on futures contracts this time) uses a different method than we have here.
I finally reached the end of my trial period with RealTest and decided to purchase it. For now, until I can create a Python-based alternative, I will continue using this program for backtesting. It does one job, and it does that job very well. I would love to see it get support for intra-day time frames, and it appears that is in the works, but who knows how long that will take. For now, I am happy with what I’ve got.
Happy hunting!
The code for strategies and the custom functions/framework I use for strategy development in Python or NinjaScript can be found at the Hunt Gather Trade GitHub. This code repository will house all code related to articles and strategy development. If there are any paid subscribers without access, please contact me via e-mail. I do my best to invite members quickly after they subscribe. That being said, please try and make sure the e-mail you use with Substack is the e-mail associated with GitHub. It is difficult to track members otherwise.
Hunt-Gather-Trade/real-test-scripts
Feel free to comment below or e-mail me if you need help with anything, wish to criticize, or have thoughts on improvements. Paid subscribers can access this code and more at the private HGT repo. As always, this newsletter represents refined versions of my research notes. That means these notes are plastic. There could be mistakes or better ways to accomplish what I am trying to do. Nothing is perfect, and I always look for ways to improve my techniques.