Skip to main content

Amibroker stands out as a versatile tool for developing and testing trading systems. It boasts a robust out-of-the-box backtest and optimization engine. Moreover, it offers a custom backtesting interface that allows you to tweak default backtest rules and metrics. With this post, we aim to explore Amibroker’s custom backtester features and provide examples. By the end, you’ll be ready to craft your own custom backtest AFL (AmiBroker Formula Language).

Amibroker’s Custom Backtesting Model

Amibroker adopts an object-oriented model for custom backtesting. It provides a single Backtester object to conduct backtests. To use this object, you must obtain a copy of it and assign it to your variable of choice, like so:
bo = GetBacktesterObject();
The variable “bo” is your own, and you can name it as you see fit, following AFL naming conventions.

The interface also exposes the signal object, stats object, and trade object. However, the only object directly accessible from AFL is the Backtester object. You can access all other objects by calling Backtester object methods, as illustrated in the image below.

Custom-Backtest-Model

Each of these objects offers multiple methods accessible from within AFL code. For in-depth documentation on these methods, you can refer to this link.

The Amibroker custom backtester interface provides three levels of user customization: high-level, mid-level, and low-level. The high-level approach requires minimal programming knowledge, while the low-level approach is the most complex.

  • High-level approach (the easiest): Utilizes the Backtest() method, running the default backtest procedure (as in old versions), allowing straightforward implementation of custom metrics.
  • Mid-level approach: Utilizes PreProcess()/ProcessTradeSignal()/PostProcess() methods, enabling signal modification and open position querying (ideal for advanced position sizing).
  • Low-level approach (the most complex): Leverages PreProcess()/EnterTrade()/ExitTrade()/ScaleTrade()/UpdateStats()/HandleStops()/PostProcess() methods, granting full control over the entire backtest process for proficient programmers.

Read Also: Mastering Amibroker: Streamlining Automated Backtesting

Utilizing the Amibroker Custom Backtesting Interface

To implement your custom backtest procedure, you must inform Amibroker of your intentions. You can achieve this in several ways:

  • Set a path to the file holding the procedure in the Automatic Analysis Settings Portfolio page. This procedure will then be used in all backtests if the “Enable custom backtest procedure” checkbox is selected.
  • Specify these settings in your AFL code using the functions SetOption(“UseCustomBacktestProc”, True) and SetCustomBacktestProc(“”). Remember that path separators inside strings should use double backslashes, e.g., “c:\\AmiBroker\\Formulas\\Custom\\Backtests\\MyProc.afl”.
  • If your procedure resides in the same file as other AFL code, use the statement SetCustomBacktestProc(“”). This indicates that there is a custom backtest procedure in the current file without specifying a path. This option will be used in the examples below.

Before anything else, in all backtest procedures, ensure that the procedure runs only during the second phase of the backtest with this conditional statement:

if (Status(“action”) == actionPortfolio)
{
// Rest of the procedure goes here
}

Lastly, a copy of the Backtester object is required before proceeding:

bo = GetBacktesterObject();

Therefore, all custom backtest procedures in the same file as other AFL code will follow a template like this:

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

Amibroker Custom Backtester Examples

Example 1: Profit/Loss Percentage for Individual Symbols in Portfolio Backtest

This AFL calculates Profit/Loss percentages for each individual security in Portfolio Backtesting. This analysis helps identify which securities the trading system performs well on and which ones 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 adds a metric to the trade log that indicates, for each winning trade, how far above or below the average winning profit it was as a percentage. It does the same for losing trades, showing how far above or below the average loss it was as a percentage. To achieve this, we need “WinnersAvgProfit” and “LosersAvgLoss” values from the Stats object, as well as the profit from the Trade objects for each closed trade (ignoring open positions in this example). 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