GTAA Timing Model - "Rotational Strategy"

[Cross post from old Yahoo forum]
Some have asked for an example of the code for a rotational strategy. Attached and below is a version of the timing model from Faber's famous Quantitative Approach to Tactical Asset Allocation (see: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=962461). Faber refers the model as “Global Tactical Asset Allocation” or “GTAA”.

"GTAA consists of five global asset classes: US stocks, foreign stocks, bonds, real estate and commodities. The returns for a buy and hold allocation are referenced as “Buy & Hold” or “B&H” and are equally weighted across the five asset classes. The timing model also uses equal weightings and treats each asset class independently – it is either long the asset class or in cash with its 20% allocation of the funds." (p. 29)

Note for setting up the model: the code refers to 2 watchlists (see Parameter window). Put the out-of-market asset (i.e. SHY) in the "bonds list" and the global assets (i.e. SPY, EFA, IEF, GSG, IYR) in the "stocks list". All used assets need to be accounted for in the "filter list" too.

By adding a relative strength metric along with a ranking routine, the GTAA timing framework is extended with the "Aggressive" approach as described on page 50 of the QA-paper. Assets are ranked on the average of 1, 3, 6, and 12-month total returns (momentum). Assets are included in the portfolio provided they are above their long-term moving average, otherwise that portion of the portfolio is moved to an out-of-market fund ("cash"). Deploying the same momentum metric for selection of the best out-of-market fund adds the prospect for harvesting crisis alpha to the GTAA framework while preserving downside protection.

For the attached example the dual universe has the following population:

  • a broad diversified universe for "stocks" (risky assets): SPY, IWM, VGK, EWJ, EEM, GLD, GSG, IYR, HYG, LQD
  • a treasury universe for selecting the alternating out-of-market asset: IEF, SHY

With monthly rebalancing capital is allocated to i.e. the best 5 (adjustable) out of 10 "risky" assets based on the 1/3/6/12m momentum metric. Each selected fund gets 20% of capital unless the asset fails to pass the 10-month SMA timing filter in which case that portion goes to the selected out-of-market asset: IEF or SHY.

BTW: for concentrated portfolios the level of crash protection is limited, see: http://seekingalpha.com/article/3972798-global-lookout-quantify-mitigate-risk-crash-protection.

The GTAA code is paste below and attached. Feedback welcome.

Enjoy,
JW

GTAA_Dual_Ranking_v1.afl (7.7 KB)

/* GTAA - Global Tactical Asset Allocation with Out-Of-Market universe
**
** 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: March 5, 2017
**
** NB! Designed for monthly data 
*/

// --- begin of code ---

// --- inputs --- 
wln_bonds  = Param( "WatchListNumber for Bonds:" , 30, 0, 150, 1 );
wln_stocks = Param( "WatchListNumber for Stocks:", 31, 0, 150, 1 );
lookback   = Param( "MA lookback (m):", 10, 1, 24, 1 );

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

// --- detect stocks universe ---
stockslist      = GetCategorySymbols( categoryWatchlist, wln_stocks );
NumberOfStocks  = StrCount( stockslist, "," ) + 1;
NoS             = NumberOfStocks;

// --- detect bonds universe ---
bondslist       = GetCategorySymbols( categoryWatchlist, wln_bonds );
NumberOfBonds   = StrCount( bondslist, "," ) + 1;
NoB             = NumberOfBonds;

// --- top selection ---
PqS        = Param( "Number of Stock Positions:", 3, 1, NoS, 1 );
PqB        = Param( "Number of Bond Positions:" , 1, 1, NoB, 1 );

// --- inputs for return mix ---
w1m        = ParamToggle( "Include  1 month return?", "No|Yes", 1 );
w3m        = ParamToggle( "Include  3 month return?", "No|Yes", 1 );
w6m        = ParamToggle( "Include  6 month return?", "No|Yes", 1 );
w12m       = ParamToggle( "Include 12 month return?", "No|Yes", 1 );
   
// --- backtester settings ---
SetOption( "CommissionAmount", 0.00 );
SetOption( "InitialEquity", 100000 );
SetTradeDelays( 0, 0, 0, 0 ); 
SetOption( "MaxOpenLong", NoS );
SetOption( "MaxOpenPositions", NoS );
SetOption( "AllowPositionShrinking", True );
SetOption( "AllowSameBarExit", True ); 
SetOption( "ReverseSignalForcesExit", False ); 
SetOption( "HoldMinBars", 1 );
SetBacktestMode( backtestRegular );
SetOption("ExtraColumnsLocation", 1 ); 
RoundLotSize = 0.0001;

// --- init values ---
cashCount = cashScore = 0;

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


	// --- stocks universe --- //

    for ( i = 0; ( symbol = StrExtract( stockslist, i ) )  != "";  i++ )
    {
        SetForeign( symbol );
        
			// --- load quotes ---
			Data     = Close;

			// ---  calculate SMA and trend filter ---
			SMA       = Sum( Data, lookback ) / lookback;
			SMAfilter = Data > SMA;
									
			// --- 1m, 3m, 6m and 12m and avg total return ---			
			stocksRet1m  = Nz( -1 + Data / Ref( Data, -1  ) );
			stocksRet3m  = Nz( -1 + Data / Ref( Data, -3  ) );
			stocksRet6m  = Nz( -1 + Data / Ref( Data, -6  ) );
			stocksRet12m = Nz( -1 + Data / Ref( Data, -12 ) );
			avgStocksRet = ( w1m * stocksRet1m + w3m * stocksRet3m + w6m * stocksRet6m + w12m * stocksRet12m ) / ( w1m + w3m + w6m + w12m );
						
		RestorePriceArrays();
		
		// --- store values ---
        StaticVarSet( "SMAfilter"    + symbol, SMAfilter    );
        StaticVarSet( "avgStocksRet" + symbol, avgStocksRet );
    }
    
    // --- generate ranks for stocks ---
    StaticVarGenerateRanks( "Rank_", "avgStocksRet", 0, 1224 );

    for ( i = 0; ( symbol = StrExtract( stockslist, i ) )  != "";  i++ )
    {
    	// --- retrieve stored values ---
    	Rank_avgStocksRet = StaticVarGet( "Rank_avgStocksRet" + symbol );
    	SMAfilter         = StaticVarGet( "SMAfilter"         + symbol ); 
    	
    	// --- count number of top ranked symbols below SMA filter ---
    	cashCount         = IIf( Rank_avgStocksRet > PqS, cashCount, IIf( SMAfilter, cashCount, cashCount + 1 ) );    	
        
        // --- allocate capital to top ranked symbols above SMA filter ---
		PosScore          = IIf( Rank_avgStocksRet <= PqS AND SMAfilter, Nz( 100 / PqS ), 0 );
		
		// --- store values ---		
		StaticVarSet( "PosScore" + symbol, PosScore );
	}
       
    // --- calculate allocation for out-of-market "cash" fund ---
    PosCash = cashCount * 100 / PqS; 
    
    // --- store value ---
    StaticVarSet( "PosCash"  , PosCash   );
    
            
	// --- bonds universe --- //

    for ( i = 0; ( symbol = StrExtract( bondslist, i ) )  != "";  i++ )
    {
        SetForeign ( symbol );
        
			// --- load quotes ---
			Data = Close;
			
			// --- 1m, 3m, 6m and 12m and avg total return ---			
			bondsRet1m  = Nz( -1 + Data / Ref( Data, -1  ) );
			bondsRet3m  = Nz( -1 + Data / Ref( Data, -3  ) );
			bondsRet6m  = Nz( -1 + Data / Ref( Data, -6  ) );
			bondsRet12m = Nz( -1 + Data / Ref( Data, -12 ) );
			avgBondsRet = ( w1m * bondsRet1m + w3m * bondsRet3m + w6m * bondsRet6m + w12m * bondsRet12m ) / ( w1m + w3m + w6m + w12m );
			
        RestorePriceArrays();
        
        // --- store values ---
        StaticVarSet( "avgBondsRet" + symbol, avgBondsRet );
    }

    // --- generate ranks for bonds ---    
    StaticVarGenerateRanks( "Rank_", "avgBondsRet", 0, 1224 );
    
    for ( i = 0; ( symbol = StrExtract( bondslist, i ) )  != "";  i++ )
    {		
		// --- retrieve values ---
		Rank_avgBondsRet = StaticVarGet( "Rank_avgBondsRet" + symbol );
		PosCash          = StaticVarGet( "PosCash"                   );
		
		// --- check if bond symbol is part of stockslist too ---
		_PosScore    = StaticVarGet( "PosScore" + symbol );
		PosScore     = IIf( Rank_avgBondsRet <= PqB, Nz( PosCash / PqB ), 0 );
		PosScore     = IIf( !IsNull( _PosScore ), PosScore + _PosScore, PosScore );
		
		// --- store values ---
		StaticVarSet( "PosScore" + symbol, PosScore );	
    }
}

// --- retrieve values ---
symbol    = Name();

if( StrFind( stockslist, Name() ) )
{
	avgReturn = StaticVarGet( "avgStocksRet"      + symbol ) * 100;
	PosScore  = StaticVarGet( "PosScore"          + symbol );
	Rank      = StaticVarGet( "Rank_avgStocksRet" + symbol );
}

if( StrFind( bondslist, Name() ) )
{
	avgReturn = StaticVarGet( "avgBondsRet"       + symbol ) * 100;
	PosScore  = StaticVarGet( "PosScore"          + symbol );
	Rank      = StaticVarGet( "Rank_avgBondsRet"  + symbol ) + 100;
}

// --- set position sizes ---
PositionSize = -PosScore; // "-" sign for spsPercentOfEquity

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

// --- exit at the end of the month for rebalancing ---
ApplyStop( stopTypeNBar, stopModeBars, numbars = 1, exitatstop = 0 );

// --- 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, -5, 4 );

	// --- columns for exploration ---
	ColorRet = IIf( avgReturn > 0, colorBrightGreen, colorRed );
	
	if( StrFind( stockslist, Name() ) ) PosColor = IIf( PosScore > 0, colorGold  , colorWhite );
	if( StrFind( bondslist , Name() ) ) PosColor = IIf( PosScore > 0, colorYellow, colorWhite );

	AddColumn( avgReturn, "Performance (%)", 3.3, 1, ColorRet );
	AddColumn( Rank     , "Rank"           , 1.0              );
	AddColumn( PosScore , "PosScore (%)"   , 3.3, 1, PosColor );
}


// --- end of code ---
22 Likes

@TrendXplorer, excellent idea to repost here the most recent code! Thanks.

And, as I wrote in this off topic post, the original Meb Faber's essay that described this strategy is the top-downloaded paper of all time on SSRN, so this rotation strategy seems to have some merit!

It is very nice, thanks to your code, to have the opportunity to test it in AmiBroker on all the different markets/instruments available to us.

1 Like

Hi , i tryed this code in Amibroker v 6.10.0. and i get this error: Variable 'posscore' used without having been initialized. Could it be due my AB version is outdated (newer AFL version / functions are included in this AFL ) or there something wrong in code?

1 Like

No, it is just the way AmiBroker is telling you... posscore variable is unknown to code trying to call it outside of if statements posscore is part of.

if( StrFind( stockslist, Name() ) )
{
	avgReturn = StaticVarGet( "avgStocksRet"      + symbol ) * 100;
	PosScore  = StaticVarGet( "PosScore"          + symbol );
	Rank      = StaticVarGet( "Rank_avgStocksRet" + symbol );
}

if( StrFind( bondslist, Name() ) )
{
	avgReturn = StaticVarGet( "avgBondsRet"       + symbol ) * 100;
	PosScore  = StaticVarGet( "PosScore"          + symbol );
	Rank      = StaticVarGet( "Rank_avgBondsRet"  + symbol ) + 100;
}

If those if statements do not execute any true case releasing that variable as being visible to code further below then posscore variable will remain hidden to that code trying to call posscore outside of those if statement further below. So you will get error of variable not being intialized.. Pretty logical consequence, isn't it. If something is not there then it is unknown. So it got nothing to do with any Amibroker version but with the fact that code creator is not a programmer in majority of cases.

Solution is to simply follow AmiBroker message and initializising posscore before if statement it is part of. So add

posscore = 0;

before. Same applies for other invisible ones.

As aside general rule is following... look for own errors in your or others' codes before calling for AmiBroker being at fault first. AmiBroker has been made by a professional but not by wannabe. In 99.99% of cases it is wrong/flawed user codes.

4 Likes

Thank You so much. Its my fault i overlooked my watchlist setup. It's logical...iam sorry

Thank you for posting the GTAA AFL code. Is there an easy way to code a specific % allocation to each asset as in the original paper?

GTAA%20Individual%20Asset%20Allocation

Thanks

One way would be "hard coding" the various percentages, like:

if ( symbol == "XXX" ) PosSize = IIf( Rank_avgStocksRet <= PqS AND SMAfilter, %%, 0 );

The code would check all 13 asset classes and assign the appropriate position sizes.

if ( symbol == "EFA" ) PosSize = IIf( Rank_avgStocksRet <= PqS AND SMAfilter, 10, 0 );
if ( symbol == "EEM" ) PosSize = IIf( Rank_avgStocksRet <= PqS AND SMAfilter, 10, 0 );
if ( symbol == "IEF" ) PosSize = IIf( Rank_avgStocksRet <= PqS AND SMAfilter,  5, 0 );
if ( symbol == "TLT" ) PosSize = IIf( Rank_avgStocksRet <= PqS AND SMAfilter,  5, 0 );
if ( symbol == "LQD" ) PosSize = IIf( Rank_avgStocksRet <= PqS AND SMAfilter,  5, 0 );
if ( symbol == "DBC" ) PosSize = IIf( Rank_avgStocksRet <= PqS AND SMAfilter, 10, 0 );
if ( symbol == "GLD" ) PosSize = IIf( Rank_avgStocksRet <= PqS AND SMAfilter, 10, 0 );
if ( symbol == "IYR" ) PosSize = IIf( Rank_avgStocksRet <= PqS AND SMAfilter, 20, 0 );
etc.

For properly calculating the allocation to the "out-of-market" fund, use total summing:

SumPosSize = SumPosSize + PosSize;

and don't forget to initialize before the looping starts:

// --- init values ---
cashCount = cashScore = PosSize = SumPosSize = 0;

Finally, outside of the allocation loop:

/ --- calculate allocation for out-of-market "cash" fund ---
PosCash = 100 - SumPosSize;

Full code with fixed position sizes:

/* 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 ---
23 Likes

@TrendXplorer Thank you very much, this is exactly what I was looking for.

@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.