Volatility Contraction Pattern (VCP) Detection

Hello friends,

I think I nailed some code that can help find VCPs in their final contraction. If you are familiar with Mark Minervini's VCP, you'll know it's now a popular way of detecting low risk entry points if the stock is already in a Stage 2 Uptrend, which I already discussed here.

I also showed a way to use studies to measure VCPs here.

However now I came up with something a little more automated. It's not perfect, but can raise efficiency in your daily workflow and allow you to narrow down on stocks that are ready for a buy-stop order.

It has tunable parameters so set them to what you want.

What is this code looking for?

  • It should be using a watchlist (via the filter in the Analysis window, or code it in) that meets the trend template from Mark Minervini (uptrending growth stocks)

  • The pivot should be within a constructive base (usually not deeper than 30-40%), or at least the pivot should not be below that level)

  • Volume is decreasing, which I use the slope of a linear regression of the 50 day average volume to determine

  • The pivot is of good quality, where

  1. The highest and lowest points of the last n days is less than 10% or less
  2. The highest point should be at the beginning of the pivot (start or continuance of contraction)
  3. The lowest point can be anywhere in the pivot area
  4. Volume is below average during the pivot period

So it looks for major consolidations (bases) and then checks the quality of the final pivot.

Within context, here is a generic VCP:

Here is a figure of what the final contraction should look like

What this code doesn't do is judge the quality of the initial contractions, but that is not as important as the stock cleaning up for the final contraction.

You will get some false positives, but that's just the nature of the game of filtering.

Here's the code:

// VCP Search

// Goal is to easily find possible VCP pivots that are tight enough to be attractive to buy

// Must be used with an Amibroker analysis that selects only a universe of stocks that meet the trend template and
// other minimum requirements for an uptrending growth stock (per Minervini parameters)

// Michael Mustillo 2020/05/04

// Tunables ////////////////////////////////////////////////
Timeframe  		= 252;  // one year of trading. Looking at growth stock a base is usually starts at a new ATH
// the above concept also tends to elminate stocks with too much overhead supply

VolTf		 	= 50;  // 50 days for volume averaging (similar to most IBD-inspired charts)

// Most good bases only retrace up to 30%. Even if the base is deeper, it's probably not a good 
// idea to buy cheats pivots (3C patterns) that are really deep into the base
BaseLowerLimit 	= 0.6;

// Pivot length. How long does the last contraction need to be to qualify?
// Three days is probably the absolute minimum if combined with a dry-up in supply
PivotLength 	= 5;

// How wide can the pivot be to be a pivot? 6% seems to be a sweet spot for a high quality final contraction
// 10% is the limit
PVLimit 		= 0.10;

Ticker = Name();

// Filter parameters ///////////////////////////////////////
// Stock should not be making a new high, but be close to it
// initial correction in the base must be less than 30%, but could be more in a volatile or bear market

// Price within base 
// find the highest price over the timeframe
HighPrice = HHV(C, Timeframe);

// Make sure the current price is till within the base and not too much below
NearHigh  = C < HighPrice AND C > BaseLowerLimit * HighPrice;

// Average volume is decreasing 
// volume must be contracting and/or below average, preferably both to ensure supply is indeed drying up
// Use the typical 50-day average for volume
Vma = MA(V, VolTf);

// A negative slope of the linear regression of the average volume should indicate supply trends
VolSlope = LinRegSlope(Vma, VolTf);

// Volume is indeed decreasing if slope is negative
VolDecreasing = VolSlope < 0;

// Pivot Quality 
// The last few days must be tight, < 10%, preferably less than 6% if possible to get a tight entry and good 
// stop-loss
// The high of the pivot should be in the first day of the pivot formation to really be a contraction
// The low can occur anywhere in the pivot
PivotHighPrice  = HHV(H, PivotLength);
PivotLowPrice	= LLV(L, PivotLength);
PivotWidth 		= (PivotHighPrice - PivotLowPrice)/C;
PivotStartHP	= Ref(H, -PivotLength +1);
IsPivot 		= PivotWidth < PVLimit AND PivotHighPrice == PivotStartHP;

// Now make sure the volume is really drying up in the pivot area
// Set initial value to true
VolDryUp = True;

// loop throught PivotLength days and ensure volume is below average
for (i = 0; i < PivotLength; i++)
	VolDryup = VolDryup AND Ref(V, -i) < Ref(Vma, -i);

// Filter results ///////////////////////////////////////////

Filter = NearHigh AND VolDecreasing AND IsPivot AND VolDryUp;


Here's some example detections from the April 2020 rally:


Not all pivots work, of course:

I literally just wrote this, so if you see any issues or improvements, please feel free to share!

Happy trading,


PS: To use this effectively, I would run this screen daily, verify the results are real pivots and set buy stops at the highest point.

I recommend a limit on the buiy stop as well as these regions of contraction in price volatility and volume lead to very fast jumps in price. I got burned on bad fills exposing me to risk. Your stop should never be more than 10% from the buy point and the stop should be set to the bottom of the pivot or the low before that if you have headroom. I would aim more for 5-7% initial stop losses for a higher probability trade.



Looks like you have out done yourself yet again @rocketPower !


I am curious what you mean by " you may get some false positives". I have been working on something here recently that might help depending on what you mean. Can you provide a couple of specific examples? Do you mean it identified some patterns that weren't actually VCP or that they were patterns that broke down in some way?


What I mean is the filter will catch things sometimes that I wouldn't consider a final contraction but still meets the rules. That's why I state to verify the results.

Note this isn't a bad thing, I would rather have a looser filter with some false positives than have it too tight and it misses good setups (false negatives).

Hope this helps!

Take care

1 Like

I'm still a learner but i think you can eliminate this loop by Summing over the last PivotLength.periods.

like this

VolDryup = Sum( Ref(V, -i) < Ref(Vma, -i), PivotLength) == PivotLength;

VolDryup, all the V bars in the period should be less than the running V average.


missed to set -i as PivotLength


I can be forgiven for rookie mistakes.

I think you meant each V bar less than the Moving average V for the previous n bars.
Then u dont need the Ref() either.

VolDryup = Sum( V < Vma, PivotLength) == PivotLength;

Yeah you're right it was lazy coding! If I'm not worried about computational power I will do what comes to my head first. :slight_smile:.

For the sake of the art, I modified to match your optimization :relaxed:

However I got slightly different results with your code (including your correction in your followup), so I looked into this more and made the following adjustments:

// loop through PivotLength days and ensure volume is below average
//for (i = 0; i < PivotLength; i++)
	//VolDryup = VolDryup AND Ref(V, -i) < Ref(Vma, -i);
VolDryup = Sum( V < Vma, PivotLength) == PivotLength;

Now the results match with the loop!

Thanks so much!


One last thing, I would set PivotLength =3. This way you can catch some shorter pivots. 5 is way too ideal and you'll miss a lot.



This is very helpful. Thank you!


Thank you for this - it's really neat.

I have worked a while ago on some kind of VCP recognition exploration and can't find what I did anymore but I still remember the logic I used back then and tried to recreate it and compare it to your approach using two recent VCPs that I have traded: VAPO and TMF.

Note: I have cancelled out the volume condition in your code for now to compare apples to apples.

I have pasted the code below (I know it looks horrible) its the equivalent of napkin notes and there is tons of room for improvement.

Test1 Test2








Checkmark=InterContract<1 AND IntraContract<1 AND PeriodLong>50 AND C>mid AND (final_depth/initial_depth < 0.3 );


Awesome, I'll take a look when I have some free time! I already noticed some possible improvements in mine after a few days.


My line of thinking was always towards a kind of Bollinger band squeeze. So I searched in the memberszone library for: bbands, congestion, contraction and squeeze. I found three interesting indicators (for the code refer to the link):

  1. Congestions Detection (the yellow rectangle areas in the chart):
  2. TTM Squeeze (2nd indicator on the bottom of the chart):
  3. Bandwidth Rank or BBand Squeeze (3rd indicator on the bottom of the chart):

I also added the code of shavedlemon in the VAPO chart for comparison, the green arrows.

I especially like those yellow boxes as they do come the closest to an extreme contraction.



Very nice! I'll take a look. I'm in the middle of a move so might take a while. :slight_smile:

Thanks for sharing the code and ideas behind it. I've been trying to model the "T" finding method this whole afternoon, but realized there are so many different variations, if I just go for the textbook ones I'll miss lots of good candidates. This thread gave me a new point of view - the volume.

Kudos to rocketPower, I'm a beginner at amibroker and I've been learning by reading your code (comments ftw :D).


1 Like