GTAA Timing Model - "Rotational Strategy"

@TrendXplorer, this has been one of the most helpful posts I've seen on the board. Thank you for your gentle explanations and the hard work you did on the example code. For us newbies it is most appreciated.

Thank you JW for sharing! I'm very appreciative of your contributions here and on your website.
Kindest Regards,
Tony

thanks sir ji,

very nice coding and explain it.

I'd like to try this out but I get an error on line 76 when RestorePriceArrays() is called: Error 47. Exception occurred during AFL formula execution at address: 0047A28E, code: C000005

See screenshot below.

I've searched and can't find anything about this error. Can anybody help me out?

amibroker_error

The full code that I'm using is identical to what @TrendXplorer posted:

/* GTAA - Global Tactical Asset Allocation 
**
** Setup for 13 asset classes with fixed position sizes and SHY asl out-of-market fund
**
** from: 
** A Quantitative Approach to Tactical Asset Allocation
** by Mebane T. Faber 2007, 2013
**
** source: http://papers.ssrn.com/sol3/papers.cfm?abstract_id=962461
**
** AmiBroker implementation by TrendXplorer
** www.trendxplorer.info
** trendxplorer@gmail.com
**
** latest version: April 2, 2018
**
** NB! Designed for monthly data 
**
*/

// --- begin of code ---

// --- inputs --- 
frequency    = ParamList( "Rebalance Frequency:", "Monthly|Bi-Monthly|Quarterly|Annually", 0 );
lookback     = Param( "MA lookback (m):", 10, 1, 24, 1 );

// --- detect tradelist ---
wlnumber     = GetOption( "FilterIncludeWatchlist" );
watchlist    = GetCategorySymbols( categoryWatchlist, wlnumber );

// --- backtester settings ---
SetBacktestMode( backtestRegular );
SetOption( "CommissionAmount", 0.00 );
SetOption( "InitialEquity", 100000 );
SetTradeDelays( 0, 0, 0, 0 ); 
SetOption( "MaxOpenLong", 13 );
SetOption( "MaxOpenPositions", 13 );
SetOption( "AllowPositionShrinking", True );
SetOption( "AllowSameBarExit", True ); 
SetOption( "ReverseSignalForcesExit", False ); 
SetOption( "HoldMinBars", 1 );
SetOption("ExtraColumnsLocation", 11 ); 
RoundLotSize = 1;

// --- detect period ends ---
MonthEnd    = Month() != Ref( Month(), 1 );
BiMonthEnd  = Month()%2  == 0 AND MonthEnd;
QuarterEnd  = Month()%3  == 0 AND MonthEnd;
YearEnd     = Month()%12 == 0 AND MonthEnd;

// --- init rebalancing frequency ---
if ( frequency == "Monthly"    ) Rebalance = MonthEnd;
if ( frequency == "Bi-Monthly" ) Rebalance = BiMonthEnd;
if ( frequency == "Quarterly"  ) Rebalance = QuarterEnd;
if ( frequency == "Annually"   ) Rebalance = YearEnd;

Rebalance = Rebalance OR Status( "LastBarInTest" );

// --- init values ---
PosCash = PosSize = SumPosSize = 0;

// --- asset selection and capital allocation routine ---
// based on https://groups.yahoo.com/neo/groups/amibroker/conversations/topics/178791
if ( Status( "stocknum" ) == 0  )
{
    StaticVarRemove( "Pos*"  );

    for ( i = 0; ( symbol = StrExtract( watchlist, i ) )  != "";  i++ )
    {
        SetForeign( symbol );
        
			// ---  calculate SMA and trend filter ---
			SMA       = Sum( Close, lookback ) / lookback;
			SMAfilter = Close > SMA;
		
		RestorePriceArrays(); 
		        
        // --- allocate capital to top ranked symbols above SMA filter ---		
		if ( symbol == "IVE"  ) PosSize = IIf( SMAfilter,  5, 0 );
		if ( symbol == "IVV"  ) PosSize = IIf( SMAfilter,  5, 0 );
		if ( symbol == "IJS"  ) PosSize = IIf( SMAfilter,  5, 0 );
		if ( symbol == "IJR"  ) PosSize = IIf( SMAfilter,  5, 0 );
		if ( symbol == "EFA"  ) PosSize = IIf( SMAfilter, 10, 0 );
		if ( symbol == "EEM"  ) PosSize = IIf( SMAfilter, 10, 0 );
		if ( symbol == "IEF"  ) PosSize = IIf( SMAfilter,  5, 0 );
		if ( symbol == "IGOV" ) PosSize = IIf( SMAfilter,  5, 0 );
		if ( symbol == "TLT"  ) PosSize = IIf( SMAfilter,  5, 0 );
		if ( symbol == "LQD"  ) PosSize = IIf( SMAfilter,  5, 0 );
		if ( symbol == "DBC"  ) PosSize = IIf( SMAfilter, 10, 0 );
		if ( symbol == "GLD"  ) PosSize = IIf( SMAfilter, 10, 0 );
		if ( symbol == "IYR"  ) PosSize = IIf( SMAfilter, 20, 0 );
		if ( symbol == "SHY"  ) PosSize = 0;
		
		SumPosSize = SumPosSize + PosSize;
		
		// --- store values ---
		StaticVarSet( "PosSize" + symbol, PosSize );
	}
       
    // --- calculate allocation for out-of-market "cash" fund ---
    PosCash = 100 - SumPosSize; 
    
    for ( i = 0; ( symbol = StrExtract( watchlist, i ) )  != "";  i++ )
    {
        if ( symbol == "SHY" ) StaticVarSet( "PosSize" + symbol, PosCash );
    }
}

// --- retrieve values ---
symbol    = Name();
PosSize   = StaticVarGet( "PosSize" + symbol );

// --- set position sizes ---
SetPositionSize( PosSize, spsPercentOfEquity );

// --- re-balance at the end/close of every month ---
Buy          = Rebalance AND PosSize > 0;
Sell         = Rebalance;
Short        = Cover = 0;
BuyPrice     = Close;
SellPrice    = Close;

// --- exploration filter ---
ExploreFilter = ParamToggle( "ExploreFilter", "LastBarInTest|All", 1 );
if ( ExploreFilter )
	Filter = 1;
else
	Filter = Status( "LastBarInTest" );

// --- sort for exploration only (not on backtest) ---
if ( Status( "actionex" ) == actionExplore ) 
{
	SetSortColumns( 2, -3 );

	// --- set position size coloring ---
	PosColor = IIf( PosSize > 0, IIf( symbol == "SHY", colorYellow, colorGold ), colorWhite );
	
	AddColumn( PosSize, "PosSize (%)", 3.3, 1, PosColor );
}

// --- initiate custom-backtest procedure ---
SetCustomBacktestProc( "" );
//
// --- set custom name for portfolio equity ---
_PortfolioName = ParamStr( "~~~PortfolioName", "~~~GTAA" );
//
if ( Status( "action" ) == actionPortfolio )
{
    bo = GetBacktesterObject();
    //
    bo.Backtest( 1 ); 
    //
    eq = bo.EquityArray;
    //
    // --- iterate through closed trades collect EquityAtExit data ---
    for ( trade = bo.GetFirstTrade(); trade; trade = bo.GetNextTrade() )
    {
        EquityAtExit = Lookup( eq, trade.ExitDateTime );
        trade.AddCustomMetric( "Equity at exit", EquityAtExit );
    }
    bo.ListTrades();
    //
    // --- save equity data to composite symbol ---
    AddToComposite( bo.EquityArray, 
                 _PortfolioName, "X", 
                 atcFlagDeleteValues | atcFlagEnableInPortfolio );
    // 
}

// --- end of code ---

I got a few hits via google.
These might be worth having a look through and trying, particularly the bit about not having any invalid character in a symbol.
https://www.amibroker.com/guide/x_bugrecovery.html

@porcupine I might not be able to tell you what is the exact reason of this Error in your case, becauses Error 47 occurs when formula causes unhandled system exception (and there are many possible reasons for that - also those specific to your system) but I would like to show that in general getting more information about Errors in AB is simple. For example:

  1. The simplest and quickest option. Just double click the line with the Error in the Formula Edititor. If you do so, In case of Error 47 you will get this description:

Error%2047%20Description

  1. Search AB User's guide (offline or online) or simply in AmiBroker click in the main menu Help --> Search and type Error 47

Error%2047%20Description%202

  1. In AB User's guide, you can find the list of almost all Errors with short descriptions (or longer descriptions if you click them):

AFL%20Errors

  1. Read Tomasz advice regarding using Log window and Google:

https://www.amibroker.com/guide/w_log.html

  1. Read this:
1 Like

@ porcupine

really Searching forum is first thing to do before posting. The thread exists and solution exists too

This is the only proper solution.

3 Likes

I'll post the resolution to this issue here just in case anybody else comes across it in the future. The info on Error 47 in AB Help is relatively vague but indicates that there could be an issue related to insufficient in-memory caching. There are settings for this in Tools > Preferences > Data. I found some helpful information on the Norgate Premium Data related to this: https://www.premiumdata.net/support/amibroker.php

I tried adjusting these values (as Norgate recommends) and it didn't fix the issue. By using the debugging tool and watching the output window in the code editor, I noticed that the script was trying to process a large number of symbols. This was due to wlnumber = GetOption( "FilterIncludeWatchlist" ); My guess (though unconfirmed) is that AB uses the watchlist for the last active Analysis window, which in my case was for a different strategy, which runs on the Russell 3000 (a lot of symbols). Thus, no matter what I set the caching preferences to, it couldn't handle it.

The fix was simply to change the line above to reference a hard-coded watchlist number, e.g. wlnumber = 621; that only has a few symbols in it.

Hope this helps someone in the future. Note that Googling "Error 47" doesn't provide any good answers (except for the link to Norgate), neither does searching this forum, or any of the help text. I found one reference to Error 47 in the old Yahoo Group but it wasn't related to my issue.

As I wrote above: a simple fix is just installing version 6.20 (or higher).

Below is my simplified version of this strategy. It's meant to model the "GTAA Aggressive" version discussed in the article here.

The results that they report are much better than what I got. One major difference is that they use total return data, which includes ordinary dividends. I'm currently using Norgate Premium data, which I believe does NOT include dividends and is thus not "total return data."

Backtesting this strategy from 9/1/2000 to present (9/28/2018) yields a 6.2% CAR and -12.7% Max DD. Not terrible, but not that great either.

For me, the takeaway is that a large portion of the 17.8% CAR (average) that they reported likely comes from dividends and not the strategy itself. This result somewhat makes sense as they are in the market for a large percentage of the time. This is assuming that I modeled my strategy correctly according to their paper; this assumption may be incorrect.

// Run on watchlist consisting of: IVE, IVV, IJS, IJR, EFA, EEM, IEF, IGOV, TLT, LQD, DBC, GLD, IYR, SHY
// Run on monthly data 

// Parameters
numPositions = 6; 
numExtras = 0; 
capital = 100000;

// Small Calcs required for backtest settings
worstHeld = numPositions + numExtras;

// Backtest Settings
SetBacktestMode( backtestRotational );
SetOption("InitialEquity", capital); 
SetOption("MaxOpenPositions", numPositions);
SetOption("WorstRankHeld", worstHeld);
BuyPrice = Open; 
SellPrice = Open;
SetTradeDelays(1, 1, 1, 1); 

// Fixed fraction position sizing  
posSizePct = 100/numPositions;
SetPositionSize( posSizePct, spsPercentOfEquity );

momo_1 = ROC(Close, 1); 
momo_3 = ROC(Close, 3); 
momo_6 = ROC(Close, 6); 
momo_12 = ROC(Close, 12); 
momo_avg = (momo_1 + momo_3 + momo_6 + momo_12)/4; 

lb_sma = 15; //Optimize("lb_sma", 10, 1, 20, 1); 
bull_permit = Close > MA(Close, lb_sma); 

compScore = IIf(bull_permit, Max(momo_avg, 0), 0);
PositionScore = compScore; 


I think that it is impossible to enumerate all possible reasons for Error 47 in the documentation. For example who could expect that accidentally you were using a watchlist containing 3000 symbols instead of just a few. In this context I think, that the information about insufficient resources to process correctly all data was still quite accurate...


BTW. Can somebody please explain to me what is the meaning of this line (from the above code):

if( Status("stocknum") == -1 // minus one

I have always been using Status("stocknum") == 0 (zero or above - only positive numbers).

Thank you in advance.

Warning: Formulas should NOT use

if( Status("stocknum") == -1 ) // ALL WRONG !!!! This must **NEVER** appear!!!

The minus one stocknum ("impossible value") was introduced to prevent accidental (second) execution of code that was intended to run ONCE (at the beginning of analysis), not to allow them.
Using it the way it is used above is completely INCORRECT.

I removed obviously incorrect code from the above so people do NOT copy these incorrect ways.

The only proper way to use Status("action") in the code that performs ranking is:

if ( Status( "stocknum" ) == 0  ) // THE ONLY CORRECT WAY to run code on first symbol only
{
  // StaticVarGenerateRanks code here
  // or any other code that is intended to be run on first symbol
}

Zero, not minus one, is the only correct value here.

BTW: If you want to run code in custom backtest context you should be using

if( Status("action") == actionPortfolio )
{

}

but it is another story

4 Likes

Dear TrendXplorer, thanks very much for posting. how do we back-test this with what conditions please? thank you

See the setup instructions sent in private.

Thanks. Appreciate it.

I also tried your simpler version with one single watchlist. The issue of your formula is that your ranking based on Max(momo_avg,0) switch you to cash when the best asset of your universe has a negative momentum and your strategy is not fully invested. This might explain the large perfromance difference. I would be interested in how you solve this. Thanks !

Hi, first of all thanks for the effort and providing the AFL code, this is exactly what I was looking for.

When I test the code with a filter (watchlist) I get no result, when I test without a filter then amibroker crashes, where could my error be?

Thanks in advance!

Just a heads-up for people who blindly copy that formula. The formula is written with ASSUMPTIONS that if not met, cause this formula to throw errors.

  1. It assumes that you are running it in Analysis window (it should NOT make such assumption)
  2. It assumes that you have Filter set to some watch list (again, it should run WITHOUT such assumption)
  3. It assumes that given watch list is NOT empty (again it should NOT assume that).

If any of the three assumptions are NOT valid it will fail with errors (because it does not handle situation when these conditions are not meet) because it fails to define all variables (namely PosSize and PosScore) UNCONDITIONALLY.

At minimum the code MUST DEFINE ALL VARIABLES UNCONDITIONALLY

Of course Tomasz is right regarding the assumptions, hence the reference to i.e. the watch- and filterlists in the description, but how to account for them in the code? Any guidance would be appreciated.

This topic was automatically closed 100 days after the last reply. New replies are no longer allowed.