Rotation of 5 Momentum stocks (like Clenow)

Thank you BlackCat and Dio (and all the others) for sharing here your valuable experience!

I tested on the S&P500 as well. What was striking for me are bigger drawdowns in the phase 2000,2001 of 40%. I wonder if this is because I did not include the ATR weighting of positions in the code which Clenow does (by the way: I cannot post the code here because it was a cooperation with colleagues, so please do not ask).
So my question is here to the pros: How big is the influence on this ATR weighting? I mean the fact that you would buy smaller portions of more volatile stocks while you buy bigger portions of less volatile.

Risk parity won't do much for reducing drawdowns as momentum has large drawdowns by definition.

Some of the code previously posted appears to be wrong.

The natural/decimal logarithm of the C should be used in the RSquared, not C or BarIndex().

The decimal logarithm can drop the refundant -1 +1 and arrive at the same values as the natural.

Hello friends,

Try to get not enter in falling market so i add "ScoreNoRotate" in my system,
but "^index" below ma period then also find rank and add new position,
i am not understand where my code is wrong.
if some one help me out of this.
Thanks in Advance.
code is below.

if( Status( "stocknum" ) == 0 )
  FilterIndex = Foreign("^INDEX", "Close", 1);
  FilterIndexMA = MA( FilterIndex, 200);
  inBearMkt = FilterIndex < FilterIndexMA;
  StaticVarSet("inBearMkt", inBearMkt);

stockDisqualified = C < EMA(Close, 200) OR  RankMom >22; 

//SetBacktestMode( backtestRotational );
PositionScore = 1000-RankMom;
PositionScore = IIf(stockDisqualified, 0, PositionScore);  
PositionScore = IIf(inBearMkt,PositionScore,scoreNoRotate);

Hello VIPUL,

Are you missing a 'not' in your last line of code?

PositionScore = IIf(! inBearMkt,PositionScore,scoreNoRotate);

Also,scoreNoRotate may not do what you want it to do. See here. If you set it for one stock, it will stop trading all stocks, and none of them will be sold.

Thanks @BlackCat SIr, for your reply.
I look for it and back with positive review.
Thanks once again.

Hello @BlackCat Sir,
You are right , is not work as i thought.
You know what i have to add in my afl.
can you solve my problem?

Only users with "Verified Badge" are allowed to post on this forum.

My interpretation of Clenow's codes for his Weekly Momentum Rotation Strategy (Stocks on the Move by Andreas Clenow, 2015). This code generated results for 1999.01.01 - 2014.12.31 on S&P500:

  • Recreated Results: CAR 12.06%, MDD -24.41% , Win% 51.94%, Expectancy 3.38%
  • Book Results: CAR 12.30%, MDD -24.00%

Updated results for 2001.01.01 - 2020.12.31 on S&P500 is:

  • Updated Results: CAR 9.41%, MDD -31.76% , Win% 52.42%, Expectancy 2.44%

Notable features in the codes are:

  1. Weekly momentum score ranking using StaticVarGenerateRanks
  2. Stop buying (sell only) when market regime filter is active (when SPX is <200d MA) (CBT)
  3. Bi-Weekly position rebalancing using ATR Risk Parity (CBT)

Grateful for peer review. Many thanks

//Weekly Rotational in S&P500 Momentum Stocks 
//Stocks on the Move - Andreas Clenow
//Code by Wealthero

//Dummy = Optimize("Dummy", 1, 1, 1, 1);
	//Dummy code for WalkForward
//Part0--Trading Setups 
  SetOption("accountmargin", 100);
	//Portfolio may have up to 30-40 stocks
	//If set to 10-20 stocks and use ATR based Risk Parity Position Sizing 
	//Return may be low as a portion of Portfolio may be in Cash and under utilized
  SetOption("allowsamebarexit", False);
  BuyPrice = O;
  SellPrice = O;
  #include_once "Formulas\Norgate Data\Norgate Data"

//Part1--Date Setups  

 //Portfolio rebalancing every Wed, omitted holiday treatment
  Weekly = DayOfWeek()==3; //Trade on Wed

 //Position rebalancing every 2nd Wed (or 2 weeks)
  CountWed = Cum(Weekly);  //Count Wed
  ReBal = Weekly AND (CountWed % 2 == 0); //Rebalance every 2nd Wed 
	//Credit: Tomasz, Ref:

//Part2--Algo Setups
	//Ch7: Ranking Stocks -- Adj Regression Slope Scoring 
	 ExpRegperiod = 90;
	 AnnSlope = ((exp(LinRegSlope(ln(C),ExpRegperiod))^250) - 1)*100;
	 R2 = (Correlation(BarIndex(),ln(C), ExpRegperiod))^2;
	 Momentum = R2 * AnnSlope;

	//Ch7: Add'l Filters  

	 //Trading above MA100d
	 IsUp = IIf( C > MA(C,100),1,0);      

	 //No move >15% in past 90days
	 GapMax = GapUp() AND ROC(C,1) > 15;   
	 GapMin = GapDown() AND ROC(C,1) < -15;   
	 IsNoGap = IIf(HHV(GapMax,90) OR HHV(GapMin,90) == 0,1,0); 
	  //Book implied GapUp +15%, but adding GapDn -15% yield better results

	 //Stock is member of S&P500 at time of trade
	 IsSPX = IIf(NorgateIndexConstituentTimeSeries("$SPX") == 1,1,0);

	 //No longer in top 20% (or Top 100 Rank if S&P500)  
	 WorstRank = 100;  

	//Ch8: Position Size -- Risk Parity Weighting Method 
	 RiskATR = ATR(20);
	 PositionRisk = (0.001 * 100);
	 PctSize = PositionRisk * BuyPrice / RiskATR;
	 SetPositionSize( PctSize, spsPercentOfEquity );

	//Others: Delisted Symbols
	//Credit: Tomasz, Ref:
	 bi = BarIndex();
	 lastbi = LastValue( bi ) - Status("BuyDelay");
	 exitLastBar = bi == lastbi;

	 Vix = Foreign("$VIX","C",True);
	 IsDanger = IIf(Vix > 30,1,0);
//Part3--Rotational Scoring, putting it all together
 IsKeep = IsNoGap * IsUp * IsSPX; 
	//If anyone equals 0 (or false) then triggers Sell in PositionScore  
 PositionScore = IIf(exitLastBar,0,Momentum); 
 //PositionScore = IIf(Rank>=100,0,PositionScore); 
   //Top100 check is done by WorstRank=100 statement in Part2
   //If you want to use Rank here, remove comment out WorstRank
 PositionScore = IIf(IsKeep==0,0,PositionScore);
 PositionScore = IIf(Weekly,PositionScore,scoreNoRotate);

//Part4--Record metrics in StaticVar
	//Credit: Tomasz, Ref:

// Watchlist should contain all symbols included in the test
wlnum = GetOption( "FilterIncludeWatchlist" );
List = CategoryGetSymbols( categoryWatchlist, wlnum ) ;

if( Status( "stocknum" ) == 0 )
    // cleanup variables created in previous runs (if any)
	StaticVarRemove( "Value*" );
    StaticVarRemove( "IsBearMkt*" );
    StaticVarRemove( "RiskATR*" );
    categoryList = ",";

    for( n = 0; ( Symbol = StrExtract( List, n ) )  != "";  n++ )
        SetForeign( Symbol );

		//***Duplicate code to generate Ranking in StaticVar
		//***Better UX in exploration auditing, not necessary for Backtest
		//***Somehow PositonScore cannot carry into the Loop, thus replicate here
			ExpRegperiod = 90;
			AnnSlope = ((exp(LinRegSlope(ln(C),ExpRegperiod))^250) - 1)*100;
			R2 = (Correlation(BarIndex(),ln(C), ExpRegperiod))^2;
			Momentum = R2 * AnnSlope;

			IsUp = IIf( C > MA(C,100),1,0);      
			GapMax = GapUp() AND ROC(C,1) > 15;   
			GapMin = GapDown() AND ROC(C,1) < -15;   
			IsNoGap = IIf(HHV(GapMax,90) OR HHV(GapMin,90) == 0,1,0);  
			IsSPX = IIf(NorgateIndexConstituentTimeSeries("$SPX") == 1,1,0);

			bi = BarIndex();
			lastbi = LastValue( bi ) - Status("BuyDelay");
			exitLastBar = bi == lastbi;

			IsKeep = IsNoGap * IsUp * IsSPX;  
			PositionScore = IIf(exitLastBar,0,Momentum); 
			PositionScore = IIf(IsKeep==0,0,PositionScore);
			PositionScore = IIf(Weekly,PositionScore,scoreNoRotate);
			Value = PositionScore;

		//***Necessary Code for Part5--CBT below
		//Ch6: Market Regime Filter -- Add new position only if Market is uptrend  
		 Index = Foreign("$SPX","C",True);
		 IndexMA = MA(Index, 200);
		 IsBearMkt = Index < IndexMA;  //For use in CBT ignore signal if BearMkt 
		//Ch8: Position Size -- Risk Parity Weighting Method 
		 //For use in CBT Bi-Weekly Position Rebalance
		 SVarATR = ATR(20);  

        //Write Ranked values to a static variable
        StaticVarSet( "Value" + "_" + Symbol, Value);        
        StaticVarSet( "IsBearMkt", IsBearMkt);
        StaticVarSet( "SVarATR" + "_" + Symbol, SVarATR);        
	StaticVarGenerateRanks( "Rank", "Value" + "_", 0, 1234 );

//Part5--Custom Backtest Object to: 

// 1) Ignore new buys when market regime filter is false (IsBearMkt)
   //Credit: Tomasz, Ref:
   //Credit: BlackCat (Post Dec 2018), Ref 	

// 2) Rebalance positions every 2nd Wed 
   //Credit: Tomasz, Ref:

if ( Status( "action" ) == actionPortfolio )
	bo = GetBacktesterObject();

	for ( bar = 0; bar < BarCount; bar++ )
		for ( sig = bo.GetFirstSignal( bar ); sig; sig = bo.GetNextSignal( bar ) )
			IsBearMkt = StaticVarGet( "IsBearMkt");
			//Market Regime Filter SPX MA200d
			if (IsBearMkt[bar])
				sig.Price = -1; // exclude signal
	bo.ProcessTradeSignals( bar );
	CurEquity = bo.Equity;
		for( pos = bo.GetFirstOpenPos(); pos; pos = bo.GetNextOpenPos() )
			PosVal = pos.GetPositionValue();
			price = pos.GetPrice( bar, "C" );
			xBuyPrice = pos.GetPrice( bar, "O" );
			xRiskATR = StaticVarGet( "SVarATR" + "_" + pos.Symbol);
			xPositionRisk = (0.001 * 100);
			xPctSize = xPositionRisk * xBuyPrice / xRiskATR;
			diff = PosVal - xPctSize/100 * CurEquity;  
			//Rebalance only if difference between desired and
			//current position value is greater than 0.5% of equity
			//and greater than price of single share
			if( ReBal[bar] AND diff[bar] != 0 AND
				abs( diff[bar] ) > 0.005 * CurEquity AND
				abs( diff[bar] ) > price )
				bo.ScaleTrade( bar, pos.Symbol, diff[bar] < 0, price, abs( diff[bar] ));

//Part6--Columns in Exploration for checking

if( Status( "Action" ) == actionExplore ) SetSortColumns(2,-4); 

IsBearMkt = StaticVarGet("IsBearMkt");
Symbol = Name(); 
Value = StaticVarGet( "Value" + "_" + Symbol);
Rank = StaticVarGet( "Rank" + "Value" + "_" + Symbol);

Filter = Weekly AND Rank <=50;
//Filter = Weekly; //See all stocks and Ranking
AddColumn(SectorID(), "SectorID", 1.0);
AddTextColumn( SectorID( 1 ), "Sector Name" );

//Lastly set report to Detailed Log to audit for 
// 1) Weekly Portfolio Rebalance -- Entry/Exit
// 2) Bi-Weekly Position Rebalance -- Scale In/Out
// 3) Market Regime Filter -- If SPX is downtrend, then Exit only without adding new positions

Hi @Wealthero,
I have virtually simultaneously started a new topic with the idea of developing Andreas' ideas from ground up.
Being an Amibroker newbie, I didn't want to spam this broadly watched thread with stuff that might be boring to all those guys who are doing this for years.

I appreciate your approach of examining GapMax and GapMin, because I've read confusing posts, trying to capture real gaps, while my understanding of the original idea is: we want to filter out "big moves", which do not necessarily have to be gaps.
Nevertheless I ask myself - and you as well as all others - whether a TrueRange would be the best value to limit here (divided by Close of Course).
Overall that should not cause much of a difference, but just for the purpose of accuracy I can't stop wondering about TR vs ROC.


On Page 85 of Clenow's book (Stocks on the Move):

" Second, gaps make me nervous. If there has been any move larger than 15% in the past 90 days, the stock is also disqualified. If you include these situations, there's a risk that you end up getting stocks that are not really momentum situations. Short term shocks may have caused the stock to move significantly, at times even enough to push the volatility adjusted momentum ranking very high. We want long term developments, not sudden gaps."

In Clenow's words, referring to the last sentence, Gaps may be large enough to skew the Momentum Scoring (i.e. Slope x R2). Thus, inferring form Clenow, using True Range instead of Gap (as a filter) may be subjected to the situations Clenow is trying to avoid?

Ultimately, back testing will give the best indication whether True Range or Gaps is more useful. Try substituting with True Range in the code, and remember to share your results with us if you have the answer.

1 Like

Sure I will share,
although as of now I use very different data (TaiPan EOD, german provider), but at least I could compare both ideas' consequences using that.
Just a final word on the logic behind the idea: From my point a major news (e.g. takeover offering) can cause either a huge move (open to close) or a real gap, just depending on whether it was reported during market open hours or not. And I totally agree with your results that down gaps are somewhat more important than up, but then I remember (just as an example) major german companies' (at least three were noted in DAX-30, when it happened) HQs getting visited by prosecutors and police during market open hours, probably not causing any gaps, but quick intraday downturns > 15%, effectively breaking any existing trends in a single session.
I don't want to seem stubborn, but I think catching such events is also covered by the words "any move larger...".


1 Like

A year later - worth noting I am impressed by the quality of your afl work; thanks for posting!

Flattered by your compliments. I just copied and pasted from a lot of intelligent people who contributed to this community =)

@Wealthero thanks for sharing. I haven't had a full look at your code but one small point to show you

IsSPX = IIf( NorgateIndexConstituentTimeSeries( "$SPX" ) == 1, 1, 0 );

The Immediate If function is unnecessary here as this line delivers the same output (i.e. a True or False, a 1 or 0).

IsSPX= NorgateIndexConstituentTimeSeries( "$SPX" );

Could you kindly help how do we modify this to work on $COMP in norgate data? I am running it but not getting nay trades. What am I missing? How do we select watchlist number?
Grateful in advance.

$COMP has about 3700 stocks, which maybe too broad for the momentum strategy. Besides, Norgate has the full NASDAQ listing, but not the Past & Current member list for $COMP.

If you want to trade NASDAQ only stocks, I suggest you to use $NDX (Nasdaq 100) instead. Try this:

  1. Use find and replace to change "$SPX" to "$NDX" in the codes.
  2. In the Analysis filter, select the Watchlist for Nasdaq 100 Past & Current.

If you really want to use the $COMP then (i) either use the full Nasdaq universe as proxy or (ii) import the historical index member list into AmiBroker (a bit more complicated).

I have seen a strategy by Llewelyn James on Nasdaq 100, with performance better than Clenow's S&P500, provided James's version has a larger drawdown (such can be mitigated with mixing bonds to lower volatility).

Nasdaq stocks tend to have longer bull trends than S&P500, thus James changed the rebalancing period from bi-weekly to monthly. Also, James' formula to measure momentum is different from Clenow.

1 Like

Thank you this is very helpful. I will read his book and stay in touch!

Is there a way I can message you privately? Alternatively you can please email me at