Amibroker Custom Backtester: Step by Step Tutorial

Amibroker is one of the most versatile tools for Trading system development and testing. It has a very robust backtest and optimization engine out of the box. Additionally, it also provide custom backtester interface using which you can play around the default backtest rules and metrics. It allows customizing the operation of the backtester’s second phase which processes the trading signals. In this post, we’ll try to explore Amibroker custom backtester features and examples. You should be ready to write your own custom backtest AFL after reading this post.

Amibroker Custom Backtester model

Amibroker uses object oriented model for custom backtesting. Amibroker provides a single Backtester object to perform backtests. To use the Backtester object, you first have to get a copy of it and assign that to your own variable:
bo = GetBacktesterObject();
The variable “bo” is your own variable, and you can call it whatever you like within the naming rules of AFL.

The interface also exposes the signal object, stats object and trade object, but only object directly accessible from AFL is Backtester object, all other objects are accessible by calling Backtester object methods as shown in the picture below.

Amibroker Custom Backtester Model

Image Source: Amibroker Official Documentation

Each of these objects have multiple methods which can be accesses from within the AFL code. The detailed documentation of these methods can be found here.

The Amibroker custom backtester interface provides three levels of user customization, simply called high-level, mid-level, and low-level. The high-level approach requires the least programming knowledge, and the low-level approach the most.

  • high-level approach (the easiest)
    – using Backtest() method and it runs default backtest procedure (as in old versions) – allows simple implementation of custom metrics
  • mid-level approach
    – using PreProcess()/ProcessTradeSignal()/PostProcess() methods – allows to modify signals, query open positions (good for advanced position sizing)
  • low-level approach (the most complex)
    – using PreProcess()/EnterTrade()/ExitTrade()/ScaleTrade()/UpdateStats()/HandleStops()/PostProcess() methods – provides full control over entire backtest process for hard-code programmers only

Using the Amibroker custom backtester interface

To use your own custom backtest procedure, you first need to tell Amibroker that you will be doing so. There are a few ways of doing this:

  • By setting a path to the file holding the procedure in the Automatic Analysis Settings Portfolio page. This procedure will then be used with all backtests, if the “Enable custom backtest procedure” checkbox is checked.auto-analysis
  • By specifying these same two settings in your AFL code using the functions SetOption(“UseCustomBacktestProc”, True) and SetCustomBacktestProc(“<path to procedure AFL file>”). Note that path separators inside strings need to use two backslashes, for example “c:\\AmiBroker\\Formulas\\Custom\\Backtests\\MyProc.afl”.
  • By putting the procedure in the same file as the other AFL code and using the statement SetCustomBacktestProc(“”). This tells AmiBroker that there is a custom backtest procedure but there’s no path for it, because it’s in the current file. This option will be used in the examples going forward in this post.

The next thing that’s required in all backtest procedures is to ensure the procedure only runs during the second phase of the backtest. That’s achieved with the following
conditional statement:

if (Status(“action”) == actionPortfolio)
{
. . . .
}

And finally, before anything else can be done, a copy of the Backtester object is needed:

bo = GetBacktesterObject();

So all custom backtest procedures, where they’re in the same file as the other AFL code, will have a template like this:

SetCustomBacktestProc(“”);
if (Status(“action”) == actionPortfolio)
{
bo = GetBacktesterObject();
// Rest of procedure goes here
}

Amibroker Custom Backtester Examples

Example 1: Profit/Loss percentage for individual symbols in portfolio backtest

This AFL will calculate Profit/Loss % for each individual scrip in Portfolio Backtesting. This can help to determine on which securities the trading system works well, and on which securities it doesn’t.

function ProcessTrade( trade )
{
  global tradedSymbols;
  symbol = trade.Symbol;
  //
  if( ! StrFind( tradedSymbols, "," + symbol + "," ) )
  {
    tradedSymbols += symbol + ",";
  }
  //
  // HINT: you may replace it with GetPercentProfit if you wish
  profit = trade.GetPercentProfit (); 
  //
  if( trade.IsLong() )
  {
      varname = "long_" + symbol;
      VarSet( varname, Nz( VarGet( varname ) ) + profit );
  }
  else
  {
      varname = "short_" + symbol;
      VarSet( varname, Nz( VarGet( varname ) ) + profit );
  }
} 
//  
SetCustomBacktestProc( "" );
//
/* Now custom-backtest procedure follows */
//
if ( Status( "action" ) == actionPortfolio )
{
    bo = GetBacktesterObject();
    //
    bo.Backtest(); // run default backtest procedure
    //
    tradedSymbols = ",";
    //
    //iterate through closed trades
    for ( trade = bo.GetFirstTrade( ); trade; trade = bo.GetNextTrade( ) )
    {
        ProcessTrade( trade );
    }
    //
    //iterate through open positions
    for ( trade = bo.GetFirstOpenPos( ); trade; trade = bo.GetNextOpenPos( ) )
    {
        ProcessTrade( trade );
    }
    //
    //iterate through the list of traded symbols and generate custom metrics
    for ( i = 1; ( sym = StrExtract( tradedSymbols, i ) ) != ""; i++ )
    {
        longprofit = VarGet( "long_" + sym );
        shortprofit = VarGet( "short_" + sym );
        allprofit = Nz( longprofit ) + Nz( shortprofit );
        // metric uses 2 decimal points and
        // 3 (calculate sum) as a "combine method" for walk forward out-of-sample
        bo.AddCustomMetric( "Profit for " + sym, allprofit, longprofit, shortprofit, 2, 3 );
    }
}
//
SetOption( "MaxOpenPositions", 10 );
//
Buy = Cross( MACD(), Signal() );
Sell = Cross( Signal(), MACD() );
Short = Sell;
Cover = Buy;
SetPositionSize( 10, spsPercentOfEquity ) ; 
SetOption("InitialEquity",1000000); 

multiple-symbols-backtest

Example 2: Relative Average Profit/Loss for each trade

This AFL will add a metric in the trade log which will specify for each winning trade how far above or below the average winning profit it was as a percentage, and similarly for each losing trade, how far above or below the average loss it was as a percentage. For this we need the “WinnersAvgProfit” and “LosersAvgLoss” values from the Stats object, and the profit from the Trade objects for each closed trade (for this example we’ll ignore open positions). Relative loss percentages are displayed as negative numbers.

SetCustomBacktestProc( "" );

if( Status( "action" ) == actionPortfolio )
{
    bo = GetBacktesterObject(); // Get backtester object
    bo.Backtest( True ); // Run backtests with no trade listing
    stat = bo.GetPerformanceStats( 0 ); // Get Stats object for all trades
    winAvgProfit = stat.GetValue( "WinnersAvgProfit" );
    loseAvgLoss = stat.GetValue( "LosersAvgLoss" );

    for( trade = bo.GetFirstTrade(); trade; trade = bo.GetNextTrade() )
    {
        // Loop through all closed trades
        prof = trade.GetProfit(); // Get trade profit in dollars
        relProf = 0; // This will be profit/avgProfit as %

        if( prof > 0 ) // If a winner (profit > 0)
            relProf = prof / winAvgProfit * 100; // Profit relative to average
        else // Else if a loser (profit <= 0)
            relProf = -prof / loseAvgLoss * 100; // Loss relative to average

        trade.AddCustomMetric( "Rel Avg Profit%", relProf ); // Add metric
    } // End of for loop over all trades

    bo.ListTrades(); // Generate list of trades
}

SetOption( "MaxOpenPositions", 10 );
//
Buy = Cross( MACD(), Signal() );
Sell = Cross( Signal(), MACD() );
Short = Sell;
Cover = Buy;
SetPositionSize( 10, spsPercentOfEquity ) ;
SetOption( "InitialEquity", 1000000 );

relative-avg-profit

Leave a Reply

Your email address will not be published. Required fields are marked *