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:

- Weekly momentum score ranking using StaticVarGenerateRanks
- Stop buying (sell only) when market regime filter is active (when SPX is <200d MA) (CBT)
- 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
SetTradeDelays(1,1,1,1);
SetOption("initialequity",100000);
SetOption("accountmargin", 100);
SetOption("MaxOpenPositions",50);
//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);
SetOption("allowpositionshrinking",False);
EnableRotationalTrading();
BuyPrice = O;
SellPrice = O;
#include_once "Formulas\Norgate Data\Norgate Data Functions.afl"
//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: https://www.amibroker.com/kb/2015/01/27/detecting-n-th-occurrence-of-a-condition-using-modulus-operator/
//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;
SetOption("WorstRankHeld",WorstRank);
//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: https://www.amibroker.com/kb/2014/09/26/closing-trades-in-delisted-symbols/
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: https://www.amibroker.com/kb/2016/01/30/separate-ranks-for-categories-that-can-be-used-in-backtesting/
// 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);
RestorePriceArrays();
//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: https://www.amibroker.com/kb/2014/10/23/how-to-exclude-top-ranked-symbols-in-rotational-backtest/
//Credit: BlackCat (Post Dec 2018), Ref https://forum.amibroker.com/t/rotation-of-5-momentum-stocks-like-clenow/2736/65
// 2) Rebalance positions every 2nd Wed
//Credit: Tomasz, Ref: https://www.amibroker.com/kb/2006/03/06/re-balancing-open-positions/
SetCustomBacktestProc("");
if ( Status( "action" ) == actionPortfolio )
{
bo = GetBacktesterObject();
bo.PreProcess();
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] ));
}
RestorePriceArrays();
}
}
bo.PostProcess();
}
//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
AddTextColumn(FullName(),"Name",1.2);
AddColumn(Momentum,"Score",1.2);
AddColumn(PositionScore,"PosScore",1.2);
AddColumn(Value,"Value",1.2);
AddColumn(Rank,"Rank",1.0);
AddColumn(PctSize,"PctSize",1.2);
AddColumn(IsNoGap,"IsNoGap",1.0);
AddColumn(IsUp,"IsUp",1.0);
AddColumn(IsSPX,"IsSPX",1.0);
AddColumn(IsKeep,"IsKeep",1.0);
AddColumn(IsBearMkt,"IsBearMkt",1.0);
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
```