How to imitate IBD Relative Strength Percentile Ranking of Stocks

Hi all,

I thought I'd share some of my code for anyone that wants to use it. I used to be a big IBD fan, but have tried to automate most of my approach over time using MATLAB and then later Amibroker.

One ranking I really find useful is IBD's Relative Strength. It ranks all stocks against each other using a score that is based on their short, medium and long term returns. After some forum research on the net, it seems like the score formula that best imitates this the sum of returns over 1qtr, 2 qtr, 3qtr and a full trading year. The 1qtr return has double the weight of the others. Then a percentile ranking using static vars is created from this giving the IBD-style 0-100 score you see in their stock checkups and charts.

I then store the percentile rankings for each stock in their own static variable for easy retrieval when running my screens.

This runs every night as it can take some time depending how far back you want to calculate the history for each stock.

Comments, criticism and improvements are more than welcome!

Code is below.

// Relative Strength ranking of stocks

// Choose markets to include in the universe (I only use NYSE Market and delisted databases for backtesting hence why they are commented out)
listNYSE = CategoryGetSymbols(categoryMarket, 1);
listNSDQ = CategoryGetSymbols(categoryMarket, 2);
listARCA = CategoryGetSymbols(categoryMarket, 3);
//listNMKT = CategoryGetSymbols(categoryMarket, 4);
//listDLST = CategoryGetSymbols(categoryMarket, 14);

// Create the full list of stocks to be ranked
List    = listNYSE + "," + listNSDQ + "," + listARCA;// "," + listNMKT + "," + listDLST;
ListQty = StrCount(List, ",") + 1; 
// Determine the quantity of stocks to rank
StaticVarSet("UniListTotal", ListQty, True);

// Clear the static vars from last run
StaticVarRemove( "RS_*" ); 

// Generate the raw RS score for every stock and store in a static var
for (n = 0; (Symbol = StrExtract(List, n)) != "";  n++) 
{ 
    SetForeign (Symbol); 
    // relative strength IBD style
	ThreeMthRS  = 0.4*(C/Ref(C,-63));
	SixMthRS    = 0.2*(C/Ref(C,-63*2));
	NineMthRS   = 0.2*(C/Ref(C,-63*3));
	TwelveMthRS = 0.2*(C/Ref(C,-63*4));
	RSraw = ThreeMthRS + SixMthRS + NineMthRS + TwelveMthRS; 
    RestorePriceArrays(); 
    StaticVarSet("RSraw_"  +  Symbol, RSraw); 
} 

// rank the stocks using this extremely useful function!
StaticVarGenerateRanks("Rank", "RSraw_", 0, 1224); 

// Convert the static var ranks generated into a percentile score.
for (n = 0; (Symbol = StrExtract(List, n))  != "";  n++) 
{
	Rank  = StaticVarGet ("RankRSraw_" +  Symbol); 
	RSpctile = 100 - 100*Rank/ListQty;
	StaticVarSet("RS_" + Symbol, RSpctile, True);
	
	// use this opportunity to store the highest ranking stocks in a watchlist.
	if (LastValue(RSpctile) >= 95)
		CategoryAddSymbol(   Symbol, categoryWatchlist, 1132);
	else
		CategoryRemoveSymbol(Symbol, categoryWatchlist, 1132);
}

// remove unnecessary static data
// free up old stuff
StaticVarRemove("RSraw_*");
StaticVarRemove("RankRSraw*_");  
10 Likes

@rocketPower, to run the ranking code ONCE, this kind of logic, when used in explorations, in general, is included in a conditional block:

if( Status( "stocknum" ) == 0 )
{
    // Ranking code - Static vars....

}

See for instance the @Tomasz answer in this other thread (and search the forum for further code snippets):

or for a generic example, take a look at this Kb article:
Separate ranks for categories that can be used in backtesting

I leave suggestions related to the logic used to evaluate the Relative Strength Percentile to other users with more experience than me. For sure it would be nice to hear about different methods.
Thanks for sharing your.

5 Likes

Hi @beppe,

Yes that is a good tactic to make sure it has run before every screen I run that uses RS.

However since I have many and I know I run a daily batch script to keep the RS up to date, I didn't feel it was necessary.

It would be cool to figure out a clean way (with integrity check) to update only missing values for each stock's RS array kept as a static variable, rather than recalculating the entire array each time. Since it doesn't take too long on a 2nd gen i7 I haven't bothered, but it would be nice.

Thanks for your input!

Take care,
Mike

I ran your code on Hong Kong and China market, it ran for few hours but finish nicely. But when I ran against the whole US market, it ended with "out of memory" error. I feel the footprint for this ranking algorithm is too huge to run against 8000+ stocks. So I made some modification to use monthly timeframe instead in the hope to reduce memory footprint the the amount of calculation. This, I hope, will speed things up.

Also your code of calculating RS may have problem. See the modification I made below. Here, I also modify the code to take input from watchlist instead of market. This way the code can be used to run against anything such as all ETFs, etc.

// Relative Strength ranking of stocks
TimeFrameSet( inMonthly );
// watchlist should contain all symbols included in the test
wlnum = GetOption( "FilterIncludeWatchlist" );
List = CategoryGetSymbols( categoryWatchlist, wlnum ) ;
ListQty = StrCount(List, ",") + 1; 
// Determine the quantity of stocks to rank
StaticVarSet("UniListTotal", ListQty, True);

// Clear the static vars from last run
StaticVarRemove( "RS_*" ); 

// Generate the raw RS score for every stock and store in a static var
for (n = 0; (Symbol = StrExtract(List, n)) != "";  n++) 
{ 
    SetForeign (Symbol); 
    RSraw = 0;
    if (BarCount > 800)	//ensure to have two years of data before participating in ranking. all new shares will be ignored. 
    {
		// relative strength IBD style
		ThreeMthRS  = 0.4*((C-Ref(C,-3))/Ref(C,-3));
		SixMthRS    = 0.2*((C-Ref(C,-6))/Ref(C,-6));
		NineMthRS   = 0.2*((C-Ref(C,-9))/Ref(C,-9));
		TwelveMthRS = 0.2*((C-Ref(C,-12))/Ref(C,-12));
		RSraw = ThreeMthRS + SixMthRS + NineMthRS + TwelveMthRS; 
	};
		
    RestorePriceArrays(); 
    StaticVarSet("RSraw_"  +  Symbol, RSraw); 
} 

// rank the stocks using this extremely useful function!
StaticVarGenerateRanks("Rank", "RSraw_", 0, 1224); 


//Create result watchlist based on market of first symbol on list. Default is US market
firstSym=StrExtract(List, 0);
marketSym=StrExtract(firstSym, 1, '.');
resultwl = -1;

if ((marketSym == "SZ") OR (marketSym == "SS"))
	resultwl = CategoryCreate( "Top Rank China Stocks", categoryWatchlist ); 
else if (marketSym == "HK")
	resultwl = CategoryCreate( "Top Rank HK Stocks", categoryWatchlist );
else
	resultwl = CategoryCreate( "Top Rank US Stocks", categoryWatchlist ); 

// Convert the static var ranks generated into a percentile score.
for (n = 0; (Symbol = StrExtract(List, n))  != "";  n++) 
{
	Rank  = StaticVarGet ("RankRSraw_" +  Symbol); 
	RSpctile = 100 - 100*Rank/ListQty;
	StaticVarSet("RS_" + Symbol, RSpctile, True);
	
	// use this opportunity to store the highest ranking stocks in a watchlist.
	if (LastValue(RSpctile) >= 95)
		CategoryAddSymbol(Symbol, categoryWatchlist, resultwl);
	else 
		CategoryRemoveSymbol(Symbol, categoryWatchlist, resultwl);
};
TimeFrameRestore();
// remove unnecessary static data
// free up old stuff
StaticVarRemove("RSraw_*");
StaticVarRemove("RankRSraw*_");  
3 Likes

BTW, may be the original RS calculation is ok since the missing part of the calculation can be simplified and reduced into a constant which will not affect ranking.

@kzliao The most important question is: do you run this code in indicator/chart window or in AA (automatic analysis)? If it is AA window, as @beppe wrote, your and @rocketPower code would be extremely inefficient, because the whole ranking process should be performed only once and enclosed with:

if( Status( "stocknum" ) == 0 )
{
    // Ranking code - Static vars....
}

If you run the code (as it is) as Exploration against 8000 tickers, the whole ranking process is repeated 8000 times ----> 7999 times more than necessary! Besides in that case you shouldn't use TimeFrameSet(inMonthly) in AA code if it is not absolutely necessary. Monthly periodicyty should be choosen instead in the Backtester settings.

Besides (I haven't checked your code in the AFL Editor), but I've noticed, that you use semicolon " ; " after curly braces " } ". You shouldn't do that:

E1

E2

Regards

5 Likes

@Milosz Yep, you are absolutely correct. I ran it in AA window and my code is extreamly inefficient. I will make the modification as you suggested and give it a test run again.

Thanks much!

@Milosz Amazing, I just modified the code as suggested, it ran amazingly fast now :slightly_smiling_face:

1 Like

I made modification of the code. Now it ran for only 20+ seconds on the universe of 8000+ stocks. I also direct output to the Analysis Engine window instead of saving them in watchlist because that can easily be done manually within Analysis window. Here is the modified code I have:

// Relative Strength ranking of stocks
// This code must be run in Analysis Engine with Monthly timeframe. 

// watchlist should contain all symbols included in the test
wlnum = GetOption( "FilterIncludeWatchlist" );
List = CategoryGetSymbols( categoryWatchlist, wlnum ) ;
ListQty = StrCount(List, ",") + 1; 
// Determine the quantity of stocks to rank
StaticVarSet("UniListTotal", ListQty, True);
 
if( Status( "stocknum" ) == 0 )
{
	// Clear the static vars from last run
	StaticVarRemove( "RS_*" );
	StaticVarRemove("RSraw_*");
	StaticVarRemove("RankRSraw*_"); 

	// Generate the raw RS score for every stock and store in a static var
	for (n = 0; (Symbol = StrExtract(List, n)) != "";  n++) 
	{ 
		SetForeign (Symbol,0); 
		RSraw = 0;
		
		// relative strength IBD style
		ThreeMthRS  = 0.4*(C/Ref(C,-3));
		SixMthRS    = 0.2*(C/Ref(C,-6));
		NineMthRS   = 0.2*(C/Ref(C,-9));
		TwelveMthRS = 0.2*(C/Ref(C,-12));
		RSraw = ThreeMthRS + SixMthRS + NineMthRS + TwelveMthRS; 
		
		RestorePriceArrays(); 
		StaticVarSet("RSraw_"  +  Symbol, RSraw); 
	} 

	// rank the stocks using this extremely useful function!
	StaticVarGenerateRanks("Rank", "RSraw_", 0, 1224); 

	/**** If you want to automaticlly stores the result in watch list, uncomment the following lines. ***/
	//Create result watchlist based on market of first symbol on list. Default is US market
	//firstSym=StrExtract(List, 0);
	//marketSym=StrExtract(firstSym, 1, '.');
	//resultwl = -1;

	//if ((marketSym == "SZ") OR (marketSym == "SS"))
	//	resultwl = CategoryCreate( "Top Rank China Stocks", categoryWatchlist ); 
	//else if (marketSym == "HK")
	//	resultwl = CategoryCreate( "Top Rank HK Stocks", categoryWatchlist );
	//else
	//	resultwl = CategoryCreate( "Top Rank US Stocks", categoryWatchlist ); 

	// Convert the static var ranks generated into a percentile score.
	for (n = 0; (Symbol = StrExtract(List, n))  != "";  n++) 
	{
		Rank  = StaticVarGet ("RankRSraw_" +  Symbol); 
		RSpctile = 100 - 100*Rank/ListQty;
		StaticVarSet("RS_" + Symbol, RSpctile, True);
	
		/**** If you want to automaticlly stores the result in watch list, uncomment the following lines. ***/
		// use this opportunity to store the highest ranking stocks in a watchlist.
		//if (LastValue(RSpctile) >= 95)
		//	CategoryAddSymbol(Symbol, categoryWatchlist, resultwl);
		//else 
		//	CategoryRemoveSymbol(Symbol, categoryWatchlist, resultwl);
	}

}

Rank  = StaticVarGet ("RS_" +  Name()); 
		
// exploration code for verification
NumColumns = 2;

Column0 = FullName();     
Column0Name = "Ticker name";

Column1 = Rank;
Column1Name = "Rank";

//Avoid penny stock, and has at least two years worth of data and monthly traded vol at least 1M
Filter = (Rank >= 95) AND (BarCount >= 50) AND (C > 1.0) AND (V > 1000000);

if( Status( "Action" ) == actionExplore ) SetSortColumns(-4,1);
 
16 Likes

A few hours or "out of memory" error vs. 20 seconds - that is something! But frankly - as I wrote, huge differences were to be expected! That is a great example showing how efficiently and inefficiently written AFL code works ...

Thanks for posting the code - it should be useful for other users :+1:

4 Likes

@Milosz My sincere thanks to your great suggestion!

... I have only reiterated (and pointed some additional issues) to what @beppe has already written In his post above :wink:

1 Like

Yep, yep! Wonderful community of sharing :slightly_smiling_face:

@kzliao, well done.

A small note: I see that in your code you still use the "old" way to add exploration columns like:

NumColumns = 2;
Column0 = FullName();     
Column0Name = "Ticker name";
Column1 = Rank;
Column1Name = "Rank";

(For recent AmiBroker users that never saw this kind of code, it was, for example, documented in this 2001 AmiBroker Tips Issue).

While your code still works correctly (in a similar way to the old graphX....) I suggest you adopt the more flexible functions (that have additional parameters and easier to rearrange) that were officially introduced in AmiBroker 4.8 like AddColumn(), AddTextColumn(), that allows also to be consistent with any more recent additions like AddMultiTextColumn().

By the way, an infrequently seen way to visually show ranking in explorations (without previous sorting) is done using code like this:

Filter=1;
AddColumn( Close, "Close" );
rank = PercentRank( Close, 100 );
Color = ColorHSB( rank * 64/100, 255, 255 );
AddColumn( rank, "100-day percent rank", 1.2, colorDefault, Color, -1, rank );

(This code sample is taken from the "example" section of the AddColumn() function)

4 Likes

I see. I was copying from old sample codes from different places :slightly_smiling_face:
I will make the modification as you suggested. :+1: :ok_hand:

Yep, you only need to run it once a day and store as a persistent static var. Then you can just refer to the RS of any stock. I made a simple function to make it easy so I don't have to bother with static calls:

function GetRS(Symbol)
{
	// Retrieve relative strength static var
	return StaticVarGet ( "RS_" +  Symbol ); 
}
1 Like

Hello,
I am not a programmer and I am always amazed how you guys do that kind of stuff...

I trade in my country's stocks only, and I tried to run the code in the explorer, but there is nothing came up. Is it because there is a specific line that restrict only for US market code? how to make it run to all of the stocks listed in my AB?

Since I am so not good at this can you tell me the step by step using this code? is it right it should

  1. send to analysis window
  2. set the date
  3. on setting chose monthly
    4, click explore button

but the result is always no result. I am using amibroker 6.2
please kindly help

thanks

Hello,
how can we put this code in our chart, lets say in the title, automatically show us the RS rank?

thanks

@sikatgigi, did you set the "Apply To" box? You probably want it set to All Symbols.

ApplyTo

Assuming you have generated the ranking, and have @rocketPower's function in your code.. it should be as simple as...

Title = Name() + " " + GetRS(name());