Rotation of 5 Momentum stocks (like Clenow)

@Sunshine :slight_smile: it's good if it works for you. Just saying that the American stocks looked totally shredded, missing splits, missing adjustments, it was completely un-usable. The German stocks were fine, albeit not survivorship bias free. Anything European worked fine, SMI, GBL, GBM, GBS, ESTX50, FDAX, etc.. Things that came across the pond were terrible. This was about 8 years ago.

It's also great that you basically have (had?) not streaming limits, if you wanted to stream 300 symbols, it was possible.
CSI Unfair advatage has great data, a huge beautiful EOD future database, you can adjust your future the way you want. I just wish I had a broker that would allow me to trade all the Indian and Chinese markets.

Anyways, I would suggest you stay away from running this strat on the DAX stocks, there are too little stocks in the index.

@Dionysos

First about CSI Unfair Advantage. Yes, I know that this is one of the best futures data suppliers. But here in this thread we talk about stocks and Clenow. So, back to TAI-PAN.
We must seperate here into two different product categories, TAI-PAN End of Day and TAI-PAN Realtime.

First about TAI-PAN End of Day. About US-Data you speak about the quality 6-8 years ago?! I remember this but this is fortunately over. And yes, I agree with you that of course just to sort and trade the DAX universe is for beginners. In my opinion Clenow lives and dies with the universe you sort! (Even his 500 stocks from the S&P500 are a lower limit. DAX is a joke- I know a guy trading HDAX, but the commodity stocks are completely missing. The last 5 years it was good but how about the next years?)
You can use the pre-built filters to sort in a first step 5 different universes (according to a book of Mebane Faber these 5 are: US stocks, stocks rest of the world, real estate, commodities and bonds) . The US stock universe consists of 3500 US-stocks according to the Vanguard US stock market ETF, the stocks rest world contains 2500 stocks from all over the world. (Of course this is not survivorship bias free -no dead stocks or split stocks included etc.- but easy to use with just some clicks, everything is pre-built and included free). So in the second step you can sort these 3500 or 2500 stocks (or any other stocks universe you like) according to the rules of Andreas Clenow (also programmed and free). Of course no backtest but for this the “normal” investor can trust the two books from Faber and Clenow. (Or use Amibroker which can easily be connected via a plugin).

(Concerning TAI-PAN Realtime. You talk about this? Yes, it is right that most suppliers are limited to 500 stocks. But TAI-PAN Realtime is unlimited! This is very rare and one of their biggest advantages over other suppliers…).

The biggest disadvantage is that TAI-PAN is only in german, hope that they translate their software also into English one day….

HELLO SIR,
CAN YOU MAKE THIS AFL FOR DAILY USE?
INSTED OF TRADIG DAYS (ONE DAY OF WEEK).
IF POSSIBLE PLS HELP ME FOR SAME AND SEND MODIFY CODE.
THANKS .

Blockquote

you totally NEED TO DO YOUR OWN HOME WORK.

5 Likes

HELLO SIR,

VERY GOOD MORNING,

CAN WE WRITE LIKE THIS.

//Trade once a week only TradingDay = DayOfWeek()==1 OR 3 OR 5;

IS THAT CORRECT METHOD ?

Here is my code for Andreas Clenow's "Stocks on the Move". Using EOD Norgate data.

To recap: its a long-only momentum rotational system, using exponential regression (exponential line-of-best-fit) to rank stocks. With a market filter (no new positions taken when SPX below 200 MA).

This is my first coded strategy. Comments or suggestions for improvement are appreciated.

I leave out a few things that Clenow had. No risk parity sizing, just 25 equal weight positions. I trade every day. And I don't exclude gaps.

Some points for discussion below. Most of the talk in this forum is abt code. I don't find any place for discussing strategies or trading - am I in the right place?

#include_once "Formulas\Norgate Data\Norgate Data Functions.afl"

// ***********************************************************************************
// Rorational ranking system. Using Clenow's ranking.
// ***********************************************************************************

// ***********************************************************************************
// 1) Check if stock in index.
// ***********************************************************************************
stockInIdnex = NorgateIndexConstituentTimeSeries("$SPX");

// ***********************************************************************************
// 2) Market Filter Condition
// ***********************************************************************************
FilterIndex = Foreign("$SPX", "Close", 1);
FilterIndexMA = MA( FilterIndex, 200);
inBearMkt = FilterIndex < FilterIndexMA;

// ***********************************************************************************
// 3) Custom Backtest Object to ignore new buys when market filter is false.
//    Based on http://www.amibroker.com/kb/2014/10/23/how-to-exclude-top-ranked-symbols-in-rotational-backtest/
// ***********************************************************************************
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 ) )
        {
            if (inBearMkt[bar])
              sig.Price = -1; // exclude
        }
        bo.ProcessTradeSignals( bar );
    }

    bo.PostProcess();
}

// ***********************************************************************************
// 6) Clenow score.  Find the exponential line-of-best-fit.  
//    Multiply the expected annual increase by R-squared.
// ***********************************************************************************
colD = log10(Close );           // the log of the close
colE = LinRegSlope( colD, 90 ); // this gives the slope of the regression line
                                // and we've manipulated it from dollars/day to average percentage move/day
ModelledDailyPercentIncrease = ( ( 10 ^ colE ) ) -1; 
ModelledAnnualPercentIncrease = (( (ModelledDailyPercentIncrease +1 ) ^ 250 ));    //   eg: 12 means 12%
// we took the log so now reverse that.
colG = ( Correlation( Cum( 1 ), colD, 90 ) ^ 2 ); // 90 day R-Squared value
ClenowValue = ModelledAnnualPercentIncrease * colG;




// ***********************************************************************************
// 7) Sell when stock is delisted (https://norgatedata.com/amibroker-faq.php#exitpriortodelisting).
//    I had to modify the code to use DateTimeAdd().
// ***********************************************************************************
OnLastTwoBarsOfDelistedSecurity = !IsNull(GetFnData("DelistingDate")) AND (BarIndex() >= (LastValue(BarIndex())) OR DateTimeAdd( DateTime(), 2, inDaily) >= GetFnData("DelistingDate") );
 


// ***********************************************************************************
// 8) Stock Filter Condition
// ***********************************************************************************
stockDisqualified = Close < MA(Close, 100);



SetBacktestMode( backtestRotational );


// ***********************************************************************************
// 9) Position sizing and Trade Entry.  25 positions, 4% each
// ***********************************************************************************
SetOption("MaxOpenPositions",25);
SetOption("WorstRankHeld",800);
SetOption("InitialEquity",100000);
SetPositionSize( 4, spsPercentOfEquity );
SetOption("AllowPositionShrinking", True);
// Configure Trade Entry.
// Can't do it here.
// The rotational trading mode uses "buy price" and "buy delay" from the Settings | Trade page as trade price and delay for both entries and exits (long and short)
/*
BuyPrice = Open;
SellPrice = Open;
SetTradeDelays(1,1,1,1);  // Buy/Sell on the next day.
*/

// For exploration
Filter =  stockInIdnex;
AddColumn( Close, "Close" );
AddColumn( ClenowValue, "ClenowValue" );
AddColumn( BuyPrice, "BuyPrice" );

// ***********************************************************************************
// Assign final score
// ***********************************************************************************
PositionScore = ClenowValue;
// If stock is disqualified from trading (eg: delisted, not in index, below MA), 
// then force its Positionscore to be zero.
PositionScore = IIf(stockDisqualified, 0, PositionScore);  
PositionScore = IIf(NOT stockInIdnex, 0, PositionScore);  
PositionScore = IIf(OnLastTwoBarsOfDelistedSecurity, 0, PositionScore);

Discussion.

  1. Results
    Good returns but large drawdowns. From 1999 to Aug 2018:

    • For S&P 500: annual return of 14%, maxDD of 32.5%
    • For Russel 3000, annual return of 17.7%, maxDD 32.5%

    The drawdowns come after the peaks, where it typically gives back 30-40% of the total portfolio value.
    Not an easy system to follow.

    I don't have 1987 data, but expect it would perform terribly.

  2. Tweaks and Improvements

    a) I think it selects better stocks - ie: nicer looking charts - if we put more emphasis on R-squared. eg: by multiplying by this value twice.
    In other words: select more for stocks going up smoothly, rather than just going up.
    This increases annual returns into the 20s, and drawdowns into the 40s.

    b) Let both winners and losers run further by changing the stock filter condition from MA 100 to Bollinger Bands 100 (1 std dev). This further increases returns and drawdowns.

  3. Clenow's ranking looks at different factors derived from stock prices:

    • Momentum (Rate of change, or RoC)
    • Acceleration (RoC of RoC)
    • Volatility (picks lower volatility by using R-squared)

    It does not look at:

    • price/vol action
    • support & resistance

    Would be interesting to see if these other factors can separately (....and independently) predict momentum.

  4. Would I trade this now (Oct 2018) ?
    Maybe, with a small amount. The current bull market may have a few years left.
    But after 9 years, we are closer to the end than the beginning.
    After the bull market ends and its safer to wade back in, I can always add more money.:grinning:

Is there anyone else out there that trades trend following or momentum systems?

13 Likes

@BlackCat I haven't had a close look at your code or tested it, but one suggestion is to use your market regime filter in one simple line to block new trades.

Different ways of accomplishing this but one example,

// market regime filter
PositionScore = IIf(inBearMkt , 0, ClenowValue);

Some codes use a similar idea to adjust PositionSize (ie make that zero if the market filter isn't passed).

A couple of other suggestions to test and then compare your results.

Try trading less frequently than every day as that usually just increases your turnover and associated costs without improving your returns.

Test other indexes.

Try taking fewer positions.

All of those look worth testing. Easily said, some are not so easily quantified.

Otherwise it looks like you are off to a good start and welcome to the forum!

4 Likes

Thanks for your reply, @portfoliobuilder. :slightly_smiling_face:
Actually the reason for putting the market filter in the custom backtest object was to preserve any existing positions. In a bear market I only want to stop new positions, and let existing positions slowly be closed as they fail other criteria. I couldn't find a simple way to do this in afl.

Agree with you abt daily trading. I'd prefer to run the system over the weekend and trade Mondays.

Thanks again.

Another possibility,

From the User Guide " the score equal to scoreNoRotate constant means that already open trades should be kept and no new trades entered"

PositionScore = IIf(! inBearMkt , ClenowValue, scoreNoRotate );
1 Like

Thanks @portfoliobuilder , I tried earlier, but scoreNoRotate stopped any open trades being closed (if they failed other criteria). It wasn't obvious to me in the documentation, but Thomasz mentions it here. I'm really looking for a scoreCloseOpenPositions value.

2 Likes

For your 4)

Would I trade this now (Oct 2018) ?
Maybe, with a small amount. The current bull market may have a few years left.
But after 9 years, we are closer to the end than the beginning.
After the bull market ends and its safer to wade back in, I can always add more money.:grinning:

I consciously made the decision to not trade it. For exactly the same reasons as you outline.
My concern was more the 1987 crash that it really didn't handle well and needed 6+years to recover (don't quote me, it was very long). Otherwise you can, of course, fiddle around but I really think if one tries to get the params "most stable", i.e. in a region where changing them doesn't affect performance much, that'd be it. I really try to throw out as many rules as possible. What I could get out (2000-now) was 13% CAR for the $RUI+delisted, incl. rebalancing monthly + comms. + slippage. I'm happy with that. MDD was something like 25%.

Yep, I got exactly the same return for the Russel 3000 for that time, trading weekly (The previous 20% mentioned was a testing error). Got better results from the S&P 500, but I think thats due to the last 10 years passive investing craze, probably not sustainable.

Only additional change I've made is to slowly 'ease in' to the market when the index goes above its MA - don't go all in and buy 100% just cause the index pops above the MA for a day.

I've allocated some money to this for Russel 3000. Momentum strategies do well in the last manic phase of the bull market, and we might be there soon. Right now the index is below the MA, so doing nothing.

1 Like

There's a bug in the above code, jist in case anyone tries to trade on it.

'inBearMkt' needs to be a static variable. Set it:

if( Status( "stocknum" ) == 0 )
{
  StaticVarRemove("inBearMkt");
  FilterIndex = Foreign("$RUA", "Close", 1);
  FilterIndexMA = MA( FilterIndex, 200);
  inBearMkt = FilterIndex < FilterIndexMA;
  StaticVarSet("inBearMkt", inBearMkt);
}

And retrieve it in the custom back tester:

SetCustomBacktestProc("");
if ( Status( "action" ) == actionPortfolio )
{
    inBearMkt = StaticVarGet("inBearMkt");
}

I found that, when not a static variable, the value was not carried over into the Custom Backtester. Whent he start date was in a bear market, the value was wrong.

2 Likes

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 )
{
  StaticVarRemove("inBearMkt");
  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.