AB Portfolio Rotation CBT Level 1

With the help of another very knowledgeable local AB user, we are creating a level 1 CBT for portfolio rotation. We used the level 2 example at http://www.amibroker.com/kb/2006/03/06/re-balancing-open-positions/ and converted it to level 1 CBT as in code below.

Without the CBT, the code works exactly as expected. But with the CBT active, we get strange results with shorts appearing. We both have extensively reviewed AB CBT documentation, KBs, web, etc. and performed numerous web searches. This is a very simple momentum portfolio rotation strategy (Max ROC symbols). We think we are overlooking something simple, but what? After hours of looking?

Guidance to a solution will be greatly appreciated.

Thank you.

SetPositionSize( 5 , spsPercentOfEquity); // 5% positionsize
PositionScore = 1000 + ROC( C, 20 ); // long only
SetOption("WorstRankHeld", 40 );
SetOption("MaxOpenPositions", 20 ); 
SetOption("UseCustomBacktestProc", True ); 
if( Status("action") == actionPortfolio )
  bo = GetBacktesterObject();
  bo.PreProcess(); // Initialize backtester
  for (i = 0; i < BarCount; i++)	//  Loop through all bars
        for (sig = bo.GetFirstSignal( i ); sig; sig = bo.GetNextSignal( i ) )
        {	//  Loop through all signals at this bar
           if( sig.IsExit() )
                { bo.ExitTrade( i, sig.Symbol, sig.Price ); }
        for (sig = bo.GetFirstSignal( i ); sig; sig = bo.GetNextSignal( i ) )
        {	//  Loop through all signals at this bar
           if( sig.IsEntry() ) 
                {bo.EnterTrade( i, sig.Symbol, sig.IsLong() , sig.Price, sig.PosSize ); }
        bo.HandleStops( i );	//  Handle programmed stops at this bar
        bo.UpdateStats( i, 1 );	//  Update MAE/MFE stats for bar
        bo.UpdateStats( i, 2 );	//  Update stats at bar's end
    }	//  End of for loop over bars
    bo.PostProcess();	//  Do post-processing

I don't believe I've ever seen an example that uses EnableRotationalTrading() (which is obsolete, by the way) with a low-level CBT (bo.EnterTrade, bo.ExitTrade, etc.). Is there a reason that you couldn't use a mid-level CBT (bo.ProcessTradeSignals)?

If you really need COMPLETE control over your rotational system, then I would not use EnableRotationalTrading() or SetBacktestMode( backtestRotational ) mode at all. Instead use Buy and Sell arrays as usual and force the "rotational" aspect from within the CBT.

1 Like

@JGunn I don’t know if I understand your nomenclature – what is a level 1 CBT ? The Custom Backtest Interface refers to high-level, mid-level and low-level approaches. Looking at your code I assume you mean a low-level CBT.

If your intent is to code a rotational strategy using your own low-level CBT instead of AmiBroker’s built in rotational capabilities, then I suggest you carefully plot all the steps necessary in recreating a rotational strategy. For example on a bar-by-bar basis you will need to identify your “rotation bar”. And on each rotation bar you should evaluate and execute exits, re-balance remaining open positions (if you choose to rebalance), and then execute entries.

Depending upon the logic of your strategy the “Sell” logic may require two different steps. For example selling due to regular Sell rules and selling due to the rotational logic of a security no longer ranking above the equivalent of “WorstRankHeld” from the built in rotational capabilities.

The 2006 example from the Official Knowledge base that you refer to can serve as a guide to the re-balance portion (a one-step rebalance code), but you could also consider breaking your code into separate “Scale Up” and “Scale Down” steps.

IMHO building such a CBT is an excellent way to learn about writing a CBT and though it is advanced I would suggest everyone tackle that at some point.

Good luck!

PS I agree with @mradtke that EnableRotationalTrading(); is old/obsolete and its replacement SetBacktestMode( backtestRotational ); is also not necessary if you are building a strategy via a low-level CBT.

Depending on how you write your "rotational" strategy CBT code it may also be worth investigating the difference between SetBacktestMode( backtestRegular ); and SetBacktestMode(backtestRegularRaw);

Thank you Mradtke and Portfoliobuilder for your suggestions.

I should use the term low level (not level 1). I tried SetBacktestMode( backtestRotational ); and got the same incorrect results. I have successfully run this strategy using no CBT, high level CBT, and mid-level CVT. Only the low level CVT is not working as expected. And think perhaps (?) that the bo.UpdateStats( i, 1 ); instruction is needed multiple times in low level CVT; the other time includes in the sell if statement to update equity and cash parameters prior to buys. But so far it has not worked. Perhaps we don't understand how?

This example is a very simple, and CBT is not required. Developed as per the IMHO comment about excellent way to learn CBT (with a simple verified example).

I will take a look at using SetBacktestMode( backtestRegular ); and SetBacktestMode(backtestRegularRaw);` instead of SetBacktestMode( backtestRotational ); to do the rotation.

Again many Thanks!!

@JGunn, Don't let you confuse you.

There is zero (documented) difference between EnableRotationalTrading() and SetBacktestMode( backtestRotational ). Both apply the same thing. The difference is only that rotational mode has been incorporated into SetBacktestMode also.

The first one is still fully functional as ever before.
It is so "obsolete" that even the developer himself is still using it in new code examples in this forum.


Thanks for suggestions. I Understand this example simple doesn't require CBT, but planned future strategies do.

I ran this example using mid-level CBT (using bo.ProcessTradeSignals(bar); on every bar). I added a signal object loop that did nothing but use _Trace to output signal Object methods (isEntry, IsExit, etc.) and properties. I was surprised to find that the output did not match documentation, which appears to to only address standard mode CBT (not portfolio rotation). Perhaps, the low-level CBT portfolio rotation is not supported or documented? Can anyone confirm or refute my conclusion?

Thjank you.

First and foremost thing to understand that in Rotational mode there are NO entry / exit signals (Buy/Sell). It is very clearly stated in documentation, in BOLD font, from http://www.amibroker.com/guide/afl/enablerotationaltrading.html

Regular buy/sell/short/cover signals are not used at all.

Which means if you use IsEntry() or IsExit() you will get False, because these are NOT entry or exit signals.

Rotational mode works completely differently. Again, it is described in http://www.amibroker.com/guide/afl/enablerotationaltrading.html

When in rotational mode, in custom backtest, ALL "signals" that you get are not entry/exit signals, but just TOP SCORING symbols. They are sorted according to PositionScore. Rotational mode works so it only holds top ranked symbols. If symbol drops off top-N it is exited. If new one appears in top N then it is entered.

The code for custom backtester in rotational mode therefore would NOT use IsExit/IsEntry at all.


I had read AB documentation several time (and with another local AB user) from http://www.amibroker.com/guide/afl/enablerotationaltrading.html and the bolded statement:

Regular buy/sell/short/cover signals are not used at all.

I don't think most would interpret ".... not used at all" to mean IsEntry() and/or IsExit are not used in CBT as AB could be coded to do this. And this particular document has no CBT discussions at all. And AB CBT documentation provides no guidance on this at all (to me and at least 3 other AB users).

With that being said, prior to your response, I did use the _Trace() statement to view the signal parameters and did see the top ranked symbols by date sorted from highest to lower as you state. There is a statement in AB CBT documentation stating that only 2 x "number of open symbols" are passed (saved?) in signals. Does this apply to rotation? This might cause rotation problems with larger "hold until" numbers?

Thank you.

In rotational mode as written here http://www.amibroker.com/guide/afl/enablerotationaltrading.html the number of symbols in the top ranked list is equal to "WorstRankHeld" parameter.


Using Rotational Backtest Mode, you get a sorted list of signals of length

MAX ( WorstRankHeld, MaxOpenPositions ) 

I'm shorting here the lowest ranked from top 10 ranked symbols using midlevel CBT , manipulating sig.Price, and sig.PosScore


Thank you for your support. I now have portfolio rotation CBT working.

However, the Report List detailed log result of my CBT is not the same as no-CBT detailed log result. Can AFL code be added to achieve the same results?

Also, i am not sure I understand all times when

  1. bo.HandleStops( bar );
  2. bo.UpdateStats( bar, 1 );
  3. bo.UpdateStats( bar, 2 );
    must be run. I have these as the final code in my bar loop. But I have separate loops for sell and buy. Can you provide guidance on if they must be also added after first sell loop in addition to after last buy loop.

Thank you.

Thanks for posting your earlier version; as I am working on something similar, could you please post your corrected code. Much appreciated.

Corrected code is below. Code runs as expected. But Results List Detailed Log is not correct. Still searching for solution to this.

Hope this helps.

/*	CBT_Test.AFL
	Simple Momentum Portfolio Rotation Strategy
	Tested with SP500
openpos = 5;
SetBacktestMode( backtestRotational );
SetPositionSize( 100/openpos , spsPercentOfEquity); // % positionsize x 100
dt = DateTime();
PositionScore = 1000 + ROC( C, 20 ); // long only
SetOption("MaxOpenPositions", openpos ); 
SetOption("WorstRankHeld", 3*openpos );

if( Status("action") == actionPortfolio )
	bo = GetBacktesterObject();
	bo.PreProcess(); // Initialize backtester
	maxOpenPoss = GetOption("MaxOpenPositions");
	worstRankHeld = GetOption("WorstRankHeld");
	for (bar = 0; bar < BarCount; bar++)	//  Loop through all bars
    {	// Sells 
		for ( opPos = bo.GetFirstOpenPos(); opPos; opPos = bo.GetNextOpenPos() )
		{	thisOpenPos = bo.FindSignal(bar, opPos.Symbol, 0);
			if( IsEmpty( bo.FindSignal(bar, opPos.Symbol, 0) ) )  // Sell if not in Signals, below WRH 
			{ 	bo.ExitTrade(bar, opPos.Symbol, opPos.GetPrice(bar,"C"), 1); }
        bo.UpdateStats(bar, 1); // Not sure if this is required?
        // buys
		for ( sig = bo.getFirstSignal(bar); sig ; sig = bo.GetNextSignal(bar) )
		{	if ( bo.GetOpenPosQty() < maxOpenPoss )
			{	inOpen = bo.FindOpenPos(sig.Symbol); 
				if (IsNull(inOpen) ) bo.EnterTrade(bar, sig.Symbol, true, sig.Price, sig.PosSize);
        bo.HandleStops( bar );	//  Handle programmed stops at this bar
        bo.UpdateStats( bar, 1 );	//  Update MAE/MFE stats for bar
        bo.UpdateStats( bar, 2 );	//  Update stats at bar's end
    }	//  End of for loop over bars
    bo.PostProcess();	//  Do post-processing

1 Like

Awesome thank you ...

1 Like

The Detailed Log is always correct, i.e. represent accurately what you are doing in the code.

But the fact that your code produces different output than "built-in" just means that your CBT is using different logic than built-in.

Internal code works differently depending on settings (like stop precedence, etc) and handles many extra features (round lot sizing, FX currency conversion) and many fine details (like for example scoreNoRotate, scoreExitAll, and many others). Your code realizes just one simplified scenario.

Exact inner workings of AmiBroker are our intellectual property rights and trade secret as such are private. Way too many companies try to copy us.

1 Like

My CBT code output data appears the same as the built-in when viewed with _Trace() statements. And the Detailed Log Data is the same also, except only the trades are included. The dates, scores, and Open positions are missing (not incorrect or different) in my CBT Detailed Log.

Surely providing information on how to pass this data in my CBT to the Detailed Log would not compromise AB's Intellectual Property Rights?? And would not seem to require disclosing all of more complex build-in code. Other AB users have have commented on this issue. And surely appreciate a solution.

My use of AB is only for more productive and efficient management of my personal investments. I have no broader commercial interests.

Thank you.

First you wrote "Code runs as expected.". Now you write that it doesn't.

Anyway, as I understand you want to copy everything what built-in code does.

The solution is simply to use:

bo.ProcessTradeSignals( bar );  // this single line does whatever is needed

Entire code is here: http://www.amibroker.com/kb/2006/03/06/re-balancing-open-positions/ and it shows that you can STILL add customizations (scaling / entry / exit / whatever ).

The other example is here: http://www.amibroker.com/kb/2014/10/23/how-to-exclude-top-ranked-symbols-in-rotational-backtest/ and it shows that you can use the line above even in cases when you want to exclude some symbols. The thing is that you can do whatever you wish while having all details implemented in this single line.

PS. This is public forum and yes, companies copying ideas from us read that forum as well and we don't give away our IP.


@JGunn if you want to supplement the data in the Detailed Log report, you can use bo.RawTextOutput().

Thank you. I had not noticed bo.RawTextOutput(). The data in _Trace() in spot checks appears okay. Had hoped to use Detailed Log for more complete validation of my code and results.

bo.RawTextOutput() might work, I don't find much documentation on it. I guess trial and error will provide answers, but it you know more details they will be appreciated.

I actually use _TRACE for all of my validation, so can't really help you out on bo.RawTextOutput(). Looks straightforward though.

1 Like