amibroker

HomeKnowledge Base

Long-only rotational back-test

Rotational trading is a kind of backtest where you trade by switching positions between various symbols based on their relative score instead of traditional buy/sell/short/cover signals.

Since there are no signals used, only PositionScore assigned to given symbol matters.

It is worth noting that in case of rotational test, the Positions field in General tab of the Analysis settings is ignored. It is only used for regular backtests that use actual buy/sell/short/cover signals.

In the rotational mode the trades are driven by values of PositionScore variable alone.

In particular:

  • higher positive score means better candidate for entering long trade
  • lower negative score means better candidate for entering short trade

As you can see the SIGN of PositionScore variable decides whenever it is long or short.

Therefore – if we want to test long-only system in rotational backtesting mode, then we should use only positive values in PositionScore variable. For example – if we are trading a system, which uses 252-bar rate of change for scoring purposes:

SetBacktestModebacktestRotational );
SetOption("MaxOpenPositions",5);
SetOption("WorstRankHeld",5);
SetPositionSize20spsPercentOfEquity );
PositionScore ROCClose252 );

Then, to trade only long positions, we should change PositionScore defintion for example to:

PositionScore 1000 ROCClose252 ); // make sure it is positive by adding big constant

This way our scores will remain positive and that will effectively disable short trades.

More information about the rotational mode of the backtester can be found in the manual: http://www.amibroker.com/guide/afl/enablerotationaltrading.html

Separate ranks for categories that can be used in backtesting

When we want to develop a trading system, which enters only N top-scored symbols from each of the sectors, industries or other sub-groups of symbols ranked separately, we should build appropriate ranks for each of such categories. This can be done with ranking functionalities provided by StaticVarGenerateRanks function.

The formula presented below iterates though the list of symbols included in the test, then calculates the scores used for ranking and writes them into static variables. The static variables names are based on category number (sectors in this example) and that allows to create separate ranks for each sector.

// watchlist should contain all symbols included in the test
wlnum GetOption"FilterIncludeWatchlist" );
List = 
CategoryGetSymbolscategoryWatchlistwlnum ) ;

if( 
Status"stocknum" ) == )
{
    
// cleanup variables created in previous runs (if any)
    
StaticVarRemove"rank*" );
    
StaticVarRemove"values*" );
    
categoryList ",";

    for( 
0; ( Symbol StrExtract( List, ) )  != "";  n++ )
    {
        
SetForeignsymbol );

        
// use sectors for ranking
        
category sectorID();

        
// add sector to the list
        
if( ! StrFindcategoryList"," category "," ) ) categoryList += NumToStrcategory1) + ",";

        
// write our ranking criteria to a variable
        // in this example we will use 10-bar rate-of-change
        
values RocClose10 );
        
        
RestorePriceArrays();

        
// write ranked values to a static variable
        
StaticVarSet"values" category "_" symbolvalues );

    }

    
// generate separate ranks for each category from the list
    
for( 1; ( category StrExtractcategoryList) ) != ""i++ )
    {
        
StaticVarGenerateRanks"rank""values" category "_"01224 );
    }
}

category sectorID();
symbol Name();
Month();

values StaticVarGet"values" category "_" symbol );
rank StaticVarGet"rank" "values" category "_" symbol );

// exploration code for verification
AddColumnvalues"values" );
AddColumnrank"rank" );
AddTextColumnSectorID), "Sector" );
AddColumnSectorID(), "Sector No");
Filter rank <= 2;

if( 
Status"Action" ) == actionExplore SetSortColumns25);

// sample backtesting rules
SetBacktestModebacktestRotational );
score IIfrank <= 2values);
// switch symbols at the beginning of the month only
PositionScore IIf!= Refm, -), scorescoreNoRotate );
SetPositionSize1spsPercentOfEquity );

Our test should be applied to a watchlist, which contains all symbols we want to include in our ranking code:

Watch list selection

Running the exploration will show two top-ranked symbols for each of the sectors:

Ranking

We can also change Filter variable definition to

Filter 1;

and show all ranked symbols instead.

Such ranking information can be used in backtest and sample rules included at the end of the code use rank information to allow only two top-scored symbols to be traded.

How to run certain piece of code only once

There are situations where we may need to run certain code components just once, e.g. to initialize some static variables before auto-trading execution or perform some tasks (such as ranking) at the very beginning of backtest or exploration. The following techniques may be useful in such cases:

When we want to execute certain part of code just once after starting AmiBroker, we may use a flag written to a static variable that would indicate if our initialization has been triggered or not.

if( NzStaticVarGet("InitializationDone") ) == )
{
   
StaticVarSet("InitializationDone"1);
   
// code for first execution
}

If we want to run certain part of code at the beginning of the test run in Analysis window, we can use:

if ( Status("stocknum") == )
{
   
// our code here
}

When Status(“stocknum”) is detected in the code, then execution is performed in a single thread for the very first symbol. Only after processing of this first symbol has finished the other threads will start.

A practical example showing use of this feature is presented in the following tutorial:

http://www.amibroker.com/guide/h_ranking.html

Symbol selection when PositionScore is not defined

AmiBroker’s portfolio backtester allows to define stock ranking and selection criteria by means of PositionScore variable. This is explained in details in the following tutorial chapter:

http://www.amibroker.com/guide/h_portfolio.html

If PositionScore is not defined or it has the same value for two or more symbols, then AmiBroker will use the following rules:

  1. transaction with greater PositionSize is preferred – the comparison method depends on the position sizing approach used in our code:
    • If we use SetPositionSize( dollarvalue, spsValue) – then $ value is compared.
    • If we use SetPositionSize( shares, spsShares) – then number of shares is used for comparison.
    • If we use SetPositionSize( perc, spsPercentOfEquity) – then % equity matters.
  2. alphabetical order
  3. long trades rather than short trades, if both occur at the same time for the same symbol.

How to handle delisted symbols in rotational test

This Knowledge Base article: http://www.amibroker.com/kb/2014/09/26/closing-trades-in-delisted-symbols/ explains how to close trades in delisted symbols in regular backtest (to avoid holding delisted stocks in the trade list and have our max symbol limit impacted by those positions).

In rotational test however we cannot use Sell variable, because trades are driven by symbols’ ranking by PositionScore values. Therefore we would need to assign zero to PositionScore variable for the exit bars respectively – this will force exiting any positions held in given stock.

EnableRotationalTrading();

bi BarIndex();
lastbi LastValuebi ) - Status("BuyDelay"); 
exitLastBar bi == lastbi;

score /*our regular positionScore*/;
PositionScore IIfexitLastBar 0score );

Note that we are adjusting the last bar index in case trade delays are set in the settings.

As in the regular test, we can also use DelistingDate information if we have it imported into Symbol ->Information window.

EnableRotationalTrading();
exitLastBar datetime() >= GetFnData("DelistingDate");

score /*our regular positionScore*/;
PositionScore IIfexitLastBar 0score );

Using Exclude statement to skip unwanted optimization steps

Sometimes when we optimize our system, we may want to use only a subset of all parameter permutations for our analysis and ignore the others that do not meet our requirements.

For example – if we test a simple trend-following strategy, where we enter long position when short MA crosses above long MA using code such as:

shortPeriods Optimize("Short MA"1011001);
longPeriods Optimize("long MA"5011001);

Buy CrossMACloseshortPeriods), MACloselongPeriods) );
Sell CrossMACloselongPeriods), MACloseshortPeriods) );

Then, shortPeriods parameter value should remain smaller than longPeriods, otherwise the trading rules would work against the main principle of the tested strategy.

There is an easy way to ignore the unwanted sets of parameters by using Exclude statement in our code. If the variable is true – the backtester will not calculate any statistics for that particular run:

shortPeriods Optimize("Short MA"1011001);
longPeriods Optimize("long MA"5011001);

Buy CrossMACloseshortPeriods), MACloselongPeriods) );
Sell CrossMACloselongPeriods), MACloseshortPeriods) );

Exclude shortPeriods >= longPeriods;

The information from Info tab of Analysis window shows the difference between first execution (all 10000 backtest runs) and second one using Exclude statement. Note reduced number of steps and reduced optimization time.

Exclude results

Using optimum parameter values in backtesting

After Optimization process has found optimum values for parameters of our trading system, typically we want to use optimum values in subsequent backtesting or explorations. In order to achieve that, we need to manually update default_val (second) argument of Optimize function with the values obtained from the optimization report.

The arguments of Optimize function are shown below (note second parameter marked in dark red color – this is the default value parameter we will be changing after optimization run):

some_var = Optimize( "description", default_val, min_val , max_val, step );

Let us consider the following example formula used for optimization process:

SetOption("ExtraColumnsLocation"1);
periods Optimize"Periods"2550); // note that default value is 2
Level Optimize"Level"22150); // note that default value is 2

Buy CrossCCIperiods ), -Level );
Sell CrossLevelCCIperiods ) );

If we perform Optimization process and check the results (for this example we use Net Profit as the optimization target), we can see that the best results use Periods = 6 and Level = 126.

Optimization result

Now in order to run backtest and obtain exactly the same results as in the respective line of the above Optimization results, we need to enter the values into default argument, so the modified code will look like this:

SetOption("ExtraColumnsLocation"1);
periods Optimize"Periods"6550); // we changed default value to 6
Level Optimize"Level"1262150); // we changed default value to 126

Buy CrossCCIperiods ), -Level );
Sell CrossLevelCCIperiods ) );

Now we can use the code with modes other than Optimization and the formula will use optimized values we retrieved from the results.

How to copy backtest trade list to a spreadsheet

There are several ways to transfer the backtest results to a spreadsheet.

  1. Immediately after the test we can just click on the results list with right mouse button and choose Copy from the menu. It is also possible to click on the results and use Ctrl+C key shortcut.

    Copy Trade List

    The operation will copy the entire list, so there is no need to select all rows manually.

  2. After the test, we can also use File->Export option from the main program menu to export the results list to a CSV or HTML file, which could be opened from Excel later on.

    Export Trade List

  3. Backtest results are also accessible through the Report Explorer:

    Backtest Report Explorer

    In order to open detailed report for the particular test it is enough to double-click on the selected line. Then, after we navigate to Trade List page, to copy the results, the best option to use is Edit->Copy Table

    Copy Table

    Unlike the regular Copy option, Copy Table transforms HTML tables into CSV format and copies it into clipboard so tables can be pasted easily to Excel. Also it divides Entry/Exit columns into separate Entry/exit date/price columns.