Back to Community
Support/Resistance Algo for Penny Stocks

I've been trying all the technical indicators and supposed "edges" I can find information about. So far I've been deeply unimpressed -- some work some of the time with some stocks, but none really reliably enough to beat market consistently. I saw a couple old posts on the forum asking about how to identify "support" and "resistance" levels (established and retested lows and highs), but haven't seen any Q algos that imploy this common technical indicator -- perhaps because it's a bit nuanced and there's a bit of art to interpreting it.

With penny stocks though, they often display very rigid conformity to this pattern, and so it's not too difficult to code for. So I decided to code up an algo to see if there was anything behind this concept. My algo uses a short window (90 minutes). It checks if a support level has been retested twice and a resistance level retested at least once in order to determine if the stock is oscillating within a predictable range. If so it buys it at the low end of the range and sells it at the high. Ideally it'll do this multiple times a day in order to extract significant alpha from a stock that doesn't have much or any upwards movement to it.

I've tested the script with a number of different penny stocks, and at least with the ones I threw in there it was very, very consistent. Different thresholds, window lengths, levels, support/resistance requirements, and the like have some effects on the results but in general the concept is proving all-around solid for these certain kinds of penny stocks.

Since the algo only goes long for short amounts of time, a lot of the time it's just sitting on cash, so I played around with adding more penny stocks to the loop (add them to the context.penny_stocks array and reduce the overall weight if needed to keep leverage in check). With 20 in there the returns even out quite a bit while averaging around 0.5 leverage -- 0.46 alpha, 3.03sharpe, 5.28 sortino, but a bit of drawdawn during Jan '16 ...that could be improved.

Caveats/Potential Shortfalls:
- Due to low liquidity, this algo is likely only profitable via Robinhood (no commission fee) and low investment amounts. Multiple people live-trading the algo would be competing over the same limited gains. (That's why I didn't post it with my full array of penny stocks.)
- It might be that the penny stocks I chose for my tests (from my Robinhood watch list) introduce look-ahead bias.
- The algo uses a fairly dumb method of determining support and resistance. It might not be so accurate at all.
- The law of diminishing returns -- due to the aforementioned liquidity issue, as the algo racks up gains and reinvests them those trades put increasing liquidity pressure on the stock.

Some possible improvements:
- Weighting the order amounts appropriately by liquidity capacity of stock could improve returns+stats significantly.
- Supposedly it's a fairly reliable bullish indicator when a stock breaks through a strongly established resistance level. Since the algo is already tracking how often resistance has been retested, it wouldn't take much to make it go long in these cases.
- Employ pipeline to vet more suitable stocks. Due to the low liquidity of typical of penny stocks there's only a tiny bit of alpha to be extracted per stock. So the only way to get more profits out of this is to trade more tickers. I still haven't learned pipeline properly. I'm not sure what the screen criteria might be or how to program it. You don't want something that's making big moves up or down, but rather something that oscillates a couple % as often as possible. Perhaps stocktwits sentiment data could be used to find stocks generating a lot of retail interest and filter out duds that technically otherwise look nice. Who wants to take a stab at this?
- It might be possible to adapt the algo to larger window lengths for the slower moving Q500US universe of stocks, making it possible to exploit for bigger profits, and thus Q Open. Though, typically those types of stocks don't have as solidly defined support and resistance levels -- more of an art to interpret them. Not sure how to do that in Python.

My Conclusion:
Support/resistance is the most reliable technical indicator I've found, though there is definitely room for improvement.

Throw your comments/criticisms this way! Please feel free to adapt it, improve it, contribute to it, and post your work here.

Thanks!

PS -- If you live trade this algo, I'd love to know, and send me an update on how it works out for you! Cheers!

Clone Algorithm
88
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
# Backtest ID: 596d194850e1774e42409828
There was a runtime error.
17 responses

I added a pipeline and a few filters. It was running really slowly so I only backtested it for a month. There were lots of unfilled orders but it seemed to be generating a profit. Looking back at the results, it seems like the pipeline was mostly returning small caps and even a few mid caps instead of just penny stocks. This wouldn't work very well with commissions.

Clone Algorithm
13
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
# Backtest ID: 596de14e8bb376520f030931
There was a runtime error.

@Eric, thanks! I'm currently trying to run it over a longer time period to see how it performs. It was really slow to begin with... looks like pipeline made it exponentially slower! (Well, it is now looping through 200+ stocks every minute.) I made some really obvious optimizations to the code to try to speed things up, but it doesn't seem to have helped none -- been waiting FOREVER for this backtest to complete.

I think to further optimize it I'm going to use some context variables and a deque to keep track of the support and resistance test stats instead of having to loop through 90 minutes of prices every minute (since 89 of those minutes should be redundant). Should run faster then.

Well that took all day to finish the backtest, literally... Here's the version with my minor speed optimizations. I don't think it changed anything.

That graph looks good. In retrospect you could easily pump up the leverage quite a bit (2x or 3x more) and still be under 1.0 pretty much the whole time. Still I was expecting higher alpha (based on my other tests) even at these miniscule leverage levels -- at least 0.4. Probably the pipeline screen needs to filter better for the right kind of stocks. The rest of the stats look great though. Pretty good for a long-only, Robinhood-compatible algo. Market neutral. I ran it (the non-pipeline version) over the 2008 crash as well -- soars over it like nothing happened. If anything, the extra volatility helps.

Clone Algorithm
37
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
# Backtest ID: 596e48e1587c194dfa035ab9
There was a runtime error.

I worry on backtests like these that the penny stocks could be so thinly traded the backtest won't match actual results

Think the original algorithm was using penny stocks and the pipeline version is mostly returning small caps. The pipeline version used Q1500 as one of the filters and when I looked at the stocks it was trading, many of them were over $1B market cap and were worth $10-$50 per share. Alpha might be higher with actual penny stocks, but capacity could be a lot higher with small caps. Looks like multiple people are interested in trading this so I want to see if capacity can be increased further.

I am not sure it's faster, but it is nicer:

price_hist_shifted = hist.shift(1)  
support_retests = sum( (hist <= low_threshold) & (price_hist_shifted > low_threshold) )  
resistance_retests = sum( (hist >= high_threshold) & (price_hist_shifted < high_threshold) )  

Also, pay attention to this zipline "behaviour": https://www.quantopian.com/posts/simulation-of-non-marketable-limit-orders
When you use limit orders you get better execution price on zipline than reality, especially on low volume securities.

I think instead of pulling in small and mid cap stocks, simply having a huge universe of micro caps would make it more profitable. My theory here is that it is precisely the low liquidity nature of the stocks that makes them conform to these predictable patterns, so it just needs to trade more of them to compensate for that.

Otherwise, one idea would be to dynamically vary the lookback period length by market cap... so a slower-moving, larger market cap stock would have a longer look-back window and a longer sample interval.

This is sooo slow it's excruciating to test different hypotheses. I don't understand why it's so slow. I guess python is a slow language, and these cloud servers don't allocate a lot of CPU?

@Tyler, is the concern that the default Q slippage model isn't properly accounting for slippage? Or that it's not scalable beyond a few grand?

@Luca, thanks for the elegant code. I'm still wrapping my head around Python. Thanks also for that link. Is there any evidence that this is indeed a problem? Has anybody compared the performance of a live-trading algo vs a paper traded algo to show significant discrepancies?

I encountered that problem with two algorithms that I traded via IB. Fantastic results on backtests, horrible in live trading. Am I sure the bad performance was due to that bug mentioned above? No, I cannot be sure 100% but I spent quite some time trying to figure out the problem and that's the most plausible explanation.

I think it might be a combination of Q Slippage and real world trading.

Say for example your thinly traded penny stock is bought at .50 and hits a high of day of .75, which just happens to be where you were looking to sell. The issue is that in Q backtesting it just sees a price of .75, but there might have only been 1-2 shares traded at this level when it hit .75 and not the 1,000s of shares you might have in your backtest.

I believe Charles Witt had a "bottom fishing" algo where this type of scenario played out and the backtest vs live trading was drastically different.

I think setting up the filters with get fundamentals would be faster than using the pipeline, although one of the filters compares the current market cap to the market cap a month ago and I don't think that's possible with get fundamentals. Comparing the current stock price to the stock price a month ago should work, though. I haven't tried live algorithm trading yet so don't know how closely it would match the backtest results.

Is there a typo on the last line? The original value was 1.04, wondering if it's supposed to be 1.004.

I'd always thought zipline took volume into account when simulating order fills (which is why you often get partial fills in the backtest). Nonetheless, wouldn't Eric's modifications that use pipeline to pull stocks only from the Q1500US make it more resistant to that problem? It's not as profitable as the penny stock version, but it still appears to still churn out alpha with low beta and low drawdowns.

@Eric, it's not a typo. Keep in mind that penny stocks easily fluctuate up and down 4% within a day. That's why there's so much potential in trading them. But for sure 4% is a bit extreme a range for within 90 minutes, so it might be more effective set the profit take threshold a little bit lower, but not 0.4% -- I think that'd be too low. The idea there is that if the price recently dropped really hard and establishes a new support level it's not likely to go all the way back up again, so there should be some level where we should just take profit instead of expecting it to hit a previous resistance level. I didn't really do any testing with threshold levels and such... I just popped some numbers in there that seemed reasonable. So those could all be optimized for sure. Again, this might be an instance where it would pay off to make the thresholds adaptive -- such as using a standard deviation or machine learning.

Also currently this algo only trades during mid-day and hopefully manages to close all its positions before close. That seemed to make sense since penny stocks are so risky, but if we're adapting the algo for small and mid caps, maybe that isn't so important any more and better to just let it run all day and hold positions over night?

I looked at the chart for PLCE, Children's Place, since it was one of the stocks returned by the Q1500 screen. After considering the chart, it looks like small caps and mid caps can rise 4% sometimes, but it usually happens the next day. So I think holding the position overnight might work better for the bigger stocks. Seems like this was happening anyway since many of the orders entered late in the day were not going through. If I'm looking at the algorithm correctly it's risking $1 to make $20 so it doesn't have to be correct very often. Tests with values other than 4% didn't work as well, although it's hard to test a lot of different values because the algorithm is slow.

I think this strategy could definitely work for certain large caps. Look at GE's chart from the past week, for example. It hit a support level, retested it three times, and then shot back up, and crashed down and did it again. The stock didn't make any gains, but an algorithm of this sort could have made two easy winning trades (if it were adjusted for a longer lookback window and smaller threshold requirements to account for the slower momentum effects of the larger market cap). I think the trick is in identifying the kinds of stocks this would work for.

I did another test with a higher starting balance. Seems like it still works pretty well, although alpha did go down a bit.

Clone Algorithm
18
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
# Backtest ID: 59719c66fbe2235241d3fe8d
There was a runtime error.

Think it might run faster outside of the cloud platform, the code and inputs would need significant modifications and market cap might have to be replaced with share price depending on the data that's available.

Interesting. I was going to backtest it myself, but I was too impatient. I should probably let it run overnight. I tried something similar a week or two ago, but used the highs and lows of a moving average to determine support and resistance. I'll have to track down the code (I'm not good at labeling them or remembering which backtest is which) and post it. For trading penny stocks, I'd probably set high relative volume as a condition for entering or exiting a position to avoid liquidity issues.