amibroker

HomeKnowledge Base

Broad market timing in system formulas

Some trading systems may benefit from attempt to time the broad market. A market-wide valuation, such as moving average, sentiment or some other mechanism may be used to tell if we should be in the market or not.

Flexibility of AFL language allows to create rules or indicators, which are based on more than just one symbol. This enables us to introduce additional filters based on wide-market index performance.

For the purpose of reading quotes of another symbol one can use Foreign or SetForeign functions.

The following formula shows how to generate entry signals in individual stocks when S&P500 index is above its 200-period moving average and exit signals when S&P500 is equal or below 200-period average (^GSPC is a ticker for Yahoo Finance data for S&P500)

//
// read S&P 500 values from ^GSPC ticker
//
sp500 Foreign"^GSPC""C" );
//
// market-wide filter should be in "state" form
// (so it is True all the time when market is up)
//
marketup sp500 MAsp500200 );
marketdown NOT marketup;
//
// sample trading rules (MACD crossovers)
//
BuySignal CrossMACD(), Signal() );
SellSignal CrossSignal(), MACD() );
//
// combine per-symbo signals with broad-market timing
//
Buy BuySignal AND marketup// enter trade only when buy signal AND market is in up trend
Sell SellSignal OR marketdown// exit position if sell signal OR market turns dow

A more complex broad-market timing that requires not only closing price of market index can be implemented using SetForeign function. SetForeign replaces all OHLCV data series with that of the “other” security and allows to calculate all kind of indicators that would normally use current security. Broad market timing does not need to be just “all-in” or “all-out” switch. For example one can switch the trading method depending on whenever broad market is trending or sideways.

//
// Switch to S&P symbol to calculate broad-market timing
//
SetForeign"^GSPC" );
//
// now we can calculate any indicator based on SP500
//
MarketIsTrending ADX40 ) > 20// ADX (40 days) from SP500
//
// now go back to original data (current symbol)
//
RestorePriceArrays();
//
// you can have different rules that are switched
// depending on what broad market is doing
//
TrendingBuy CrossCMAC30 ) );
TrendingSell CrossMAC30 ), );
//
SidewaysBuy CrossMACD(), Signal() );
SidewaysSell CrossSignal(), MACD() );
//
// switch methods using broad-market timing
//
Buy IIfMarketIsTrendingTrendingBuySidewaysBuy );
Sell IIfMarketIsTrendingTrendingSellSidewaysSell )

In this simple example we assume that market timing signals change very infrequently so they change much less often than Trending/Sideways Buy/Sell signals are generated. If that is not the case the switching logic would need to be more complex to decide what to do when we are in the “trending” trade and market switches to sideways mode. In such situation, the code above uses SidewaysSell signal to sell the position, which may or may not be what you are after.

Another example is changing position sizing depending on broad market conditions. We can choose to be fully invested when broead market is up and only 30% invested in down market.

//
// Switch to S&P symbol to calculate broad-market timing
//
SetForeign"^GSPC" );
//
// now we can calculate any indicator based on
// SP500
//
MarketIsUp MAC200 ); // here C represents closing price of SP500
//
// now go back to origiginal data (current symbol)
//
RestorePriceArrays();
//
// normal rules (in this example they do not chang)
//
Buy CrossMACD(), Signal() );
Sell CrossSignal(), MACD() );
//
// no more than 10 positions open at a time
//
SetOption("MaxOpenPositions"10 );
//
// change position sizing depending on broad market conditions
// in this example we will allocate:
// 10% per position
// if broad market is up (so we can be allocated upto 100% of funds)
// 3% per position
// if broad market is down (so we can be allocated upto 30% of funds)
//
SetPositionSizeIIfMarketIsUp10), spsPercentOfEquity )

Too small / unreadable Profit Table in the backtest report

Some of users may observe that their Profit table is too small, so the numbers get truncated or the text is too small.

First let me tell you that profit table in the backtest report is not really a table, but a bitmap image with fixed dimensions. Profit table, like any other user-definable report chart, is created by running an AFL formula present in the “Report charts” subfolder. The chart is rendered into bitmap image that gets later embedded in the backtest report.

The size of backtest report images depends on Analysis window settings. In order to increase the size of generated images, it is necessary to go to Analysis -> Settings, Report tab and increase the picture dimensions:

reportchartdim

Once you change it, newly generated reports will use enlarged image dimensions. Adjusted settings will affect new backtests only, but not the old reports that have already been generated.

Tip: You can create your own report charts by placing your own AFL formula in the “Report charts” subfolder.

Why do backtest results change?

From time to time we receive questions about why you can get different results back-testing the same code.

There are many reasons for differences in backtest results:

  1. Different data

    For example if past history is updated/changed due to splits for example or backfill or external data update.

  2. Different settings / parameters

    May happen if your formula uses Param() functions that output values that may be changed from the Parameter window.

    Note that Parameters in Analysis window use shared ChartID=0, which means that if two formulas use same parameter names they would share parameter values as well.

  3. Different formula

    Sometimes even slight change to the formula causes big change in the results, for example if your formula uses #include and included code has changed

  4. The formula that self-references its previously generated results.

    Such code produces some data that is later used to produce next run output (for example your code produces composites that are later used – if those composites change – the input data change so the results change, or if your formula uses previous backtest equity or if your formula uses STATIC variables without deleting/clearing them at the beginning)

  5. The formula uses random number generator

    This occurs when your formula directly or indirectly calls any function that produces random numbers such as Random or mtRandom

  6. The formula reads external files/data that might have changed

    This occurs when your formula directly or indirectly calls any functions that read external files or data that have changed between backtests. It can be as trivial as using current time in your formula.

  7. The formula sets global (per-backtest) settings to non-constant value – for example sets global option differently for each symbol.

    This one is subtle and can be easily overlooked by non-experienced users. One has to understand that global (per-backtest) settings are applied to entire portfolio. If one changes those settings on per-symbol basis, then “last write” counts, and it means that result of your backtest will depend on which execution thread ended last. In Windows OS, it is generally unpredicatable which thread ends first because of so many factors that affect execution time. So your backtest may use different settings in different runs as a result of this. Bottom line: never change global settings in your formula to non-constant value.

Using Zig-Zag in trading systems

Zig-zag indicator, as well as other functions using it (Peak/Trough, PeakBars, Troughbars), inherently look into the future. As such they should not be used in trading system formulas without taking precautions. The only way to fix the ‘problem’ is to delay the signal as long as it takes for zig/zag to stabilise last ‘leg’. The delay is variable and depends how much time it takes for defined percentage change to occur in the price series since last peak/trough.

Ready-to-use solution is presented in the Traders’ Tips section of the AmiBroker members area:
http://www.amibroker.com/members/traders/11-2003.html

(NOTE: access to members’ area is limited to licensed users only, if you forgot your password use reminder at http://www.amibroker.com/login.html)

Points-only backtest

Some users coming from Metastock ask for “points-only” test.

One needs to know that AmiBroker features way more sophisticated futures mode than MS ever had: http://www.amibroker.com/guide/h_futbacktest.html
It provides full support for futures trading, handling margin deposit, point value, etc.

“Points-only” test is kind of the simplest possible case, but if you want to do just that, it can be implemented using these two lines:

SetOption("FuturesMode"True );
SetPostionSize1spsShares ); // trade just 1 contrac

That is all what you need to add to your formula to get point-only test.

Historical portfolio backtest metrics

Recently on the AmiBroker mailing list some users expressed wish to have access to some of portfolio backtest metrics available in “historical” form (i.e. as date series, as opposed to scalars), so they can be plotted as an indicator.

Implementing such functionality is actually easy with existing tools and does not require any OLE scripts. Everything you need is small custom-backtester procedure that just reads built-in stats every bar and puts them into composite ticker.
In the accompanying indicator code all you need to do is simply use Foreign() function to access the historical metrics data generated during backtest.

The code below shows the BACKTEST formula with custom backtester part:

// Replace lines below with YOUR TRADING SYSTEM
EnableRotationalTrading();
PositionScore 1/RSI(14);
PositionSize = -25;
SetOption("WorstRankHeld");
SetOption("MaxOpenPositions"); 

////////////////////////////////////////
// BELOW IS ACTUAL CUSTOM BACKTESTER PART
// that can read any built-in metric (in this example UlcerIndex)
// and store it into composite ticker for further
// retrieval as data series

SetOption("UseCustomBacktestProc"True ); 

if( 
Status("action") == actionPortfolio )
{
  
bo GetBacktesterObject();

  
bo.PreProcess(); // Initialize backtester

  // initialize with null
  // you can have as many historical metrics as you want
  // (just duplicate line below for many metrics you want)
  
MyHistStat1 Null;
  
MyHistStat2 Null// add your own 

  
for(bar=0bar BarCountbar++)
  {
   
bo.ProcessTradeSignalsbar );
  
   
// recalculate built-in stats on EACH BAR
   
stats bo.GetPerformanceStats); 
 
   
// the line below reads the metric and stores it as array element
   // you can add many lines for each metric of your choice
   
MyHistStat1bar ] = stats.GetValue("UlcerIndex"); // get ulcer index value calculated this bar
   
MyHistStat2bar ] = stats.GetValue("WinnersPercent"); // add your own

  
}

  
bo.PostProcess(); // Finalize backtester

  // now STORE the historical data series representing the metric of your choice
  // duplicate the line below for as many metrics as you want
  
AddToCompositeMyHistStat1"~~~UI_HISTORICAL""X"atcFlagEnableInPortfolio atcFlagDefaults );

  
// you can add your own as shown below
  
AddToCompositeMyHistStat2"~~~WP_HISTORICAL""X"atcFlagEnableInPortfolio atcFlagDefaults ); 

In the code above, for illustration purposes, we are exporting UlcerIndex and Winners Percent metrics as data series. They are stored in composite tickers for easy retrieval from indicator level.
You can easily extend code to include ANY number of metrics you want.

Now in order to Plot metrics as indicators, use this simple formula:

PlotForeign("~~~UI_HISTORICAL""UlcerIndex Historical"colorRedstyleLine );
PlotForeign("~~~WP_HISTORICAL""Winners Percent"colorBluestyleLine styleOwnScale )

As you can see with one Foreign function call you can read the historical value of any metric generated by the backtester.

NOTE: when running backtest please setup a filter in AA that EXCLUDES composites (group 253) from backtest set.

Getting started with automatic Walk-Forward optimization

Recently released AmiBroker 5.05 BETA features the automatic Walk-Forward Optimization mode.

The automatic Walk forward optimization is a system design and validation technique in which you optimize the parameter values on a past segment of market data (“in-sample”), then test the system forward in time on data following the optimization segment (“out-of-sample”). You evaluate the system based on how well it performs on the test data (“out-of-sample”), not the data it was optimized on.

To use Walk-Forward optimization please follow these steps:

  1. Goto Tools->Automatic Analysis
  2. Click Settings button, then switch to “Walk-Forward tab”
  3. Here you can see Walk forward settings for In-sample optimization, out-of-sample backtest
    “Start” and “End” dates mark initial period begin / end
    This period will be moved forward by “Step” until the “End” reaches the “Last” date.
    The “Start” date can move forward by “step” too, or can be anchored (constant) if “Anchored” check is on.
    If you mark “Use today” then “Last” date entered will be ignored and TODAY (current date) will be used instead

    By default an “EASY MODE” is selected which simplifies the process of setting up WF parameters.
    It assumes that:
    a) Out-of-sample segment immediatelly follows in-sample segment
    b) the length of out-of-sample segment equals to the walk-forward step

    Based on these two assumptions the “EASY” mode takes in-sample END date and sets
    out-of-sample START date to the following day. Then adds in-sample STEP and this becomes out-of-sample END date.
    In-sample and Out-of-sample step values are set to the same values.

    The “EASY” mode guarantees correctness of WF procedure settings.

    In the “ADVANCED” mode, the user has complete control over all values, to the extent that
    they may not constitute valid WF procedure.
    The interface allows to selectivelly disable in-sample and out-of-sample phases using checkboxes at top
    (for special things like runnign sequential backtests without optimization).

    All settings are immediatelly reflected in the PREVIEW list that shows all generated IS/OOS segments and their dates.

    The “Optimization target” field defines the optimization raport COLUMN NAME that
    will be used for sorting results and finding the BEST one. Any built-in column can be used
    (as appears in the optimization output), or you can use any custom metric that you define
    in custom backtester. The default is CAR/MDD, you can however select any other built-in metric from the combo.
    You can also TYPE-IN any custom metric that you have added via custom backtester interface.

  4. Once you defined Walk-Forward settings, please go to Automatic Analysis and
  5. press the dropdown ARROW on the Optimize button and select “Walk Forward Optimization”

This will run sequence of optimizaitons and backtest and the results will be displayed in the “Walk Forward” document that is open in the main application frame.
When optimization is running you can click “MINIMIZE” button on the Progress dialog to minimize it – this allows to see the Walk Forward output during the optimization steps.

IN-SAMPLE and OUT-OF-SAMPLE combined equity

Combined in-sample and out-sample equities are available by
~~~ISEQUITY and ~~~OSEQUITY composite tickers (consecutive periods of IS and OOS are concatenated and scaled to
maintain continuity of equity line – this approach assumes that you generally speaking are compounding profits)
To display IS and OOS equity you may use for example this:

PlotForeign("~~~ISEQUITY","In-Sample Equity"colorRedstyleLine);
PlotForeign("~~~OSEQUITY","Out-Of-Sample Equity"colorGreenstyleLine);
Title "{{NAME}} - {{INTERVAL}} {{DATE}} {{VALUES}}"

Low-level gfx example: Yearly/monthly profit chart

The code below is an little bit more complex example of Low Level Graphics functions (see http://www.amibroker.com/guide/a_lowlevelgfx.html)

It allows to display three kinds of charts:

  1. yearly/monthly profit table
  2. yearly profit bar chart
  3. average monthly profit bar chart

The type of chart is switchable from Parameters dialog.

It should be applied to ~~~EQUITY – portfolio equity symbol (so it only produces output if you run backtest before using it).


SetBarsRequired(1000000,1000000);
eq Foreign("~~~EQUITY""C" );

yr Year();
mo Month();

YearChange yr != Refyr, -);
MonChange mo != Refmo, -);

FirstYr 0;
LastYr 0;

startbar 0;

////////////////////////////
// SKIP non-trading bars
////////////////////////////
for( 0BarCounti++ )
{
  if( 
eq] )
  {
    
startbar i;
    break;
  } 
}

////////////////////////////
// collect yearly / monthly changes in equity
// into dynamic variables
////////////////////////////

LastYrValue eqstartbar  ];
LastMoValue eqstartbar  ];

MaxYrProfit MinYrProfit 0;
MaxMoProfit MinMoProfit 0;

for( 
startbar 1BarCounti++ )
{
  if( 
YearChange] || == BarCount )
  {
    
Chg 100 * ( -eq] / LastYrValue );
    
VarSet("ChgYear"yr], Chg );

    
MaxYrProfit MaxMaxYrProfitChg );
    
MinYrProfit MinMinYrProfitChg );

    if( 
FirstYr == FirstYr yr];
    
LastYr yr];

    
LastYrValue eq];
  }

  if( 
MonChange ] || == BarCount )
  {
    
mon mo];

    
Chg 100 * ( -eq] / LastMoValue );

    
VarSet("ChgMon" yr] + "-" monChg );
    
VarSet("SumChgMon"monChg NzVarGet("SumChgMon"mon ) ) );
    
VarSet("SumMon" monNzVarGet("SumMon"mon ) ) );
 
    
MaxMoProfit MaxMaxMoProfitChg );
    
MinMoProfit MinMinMoProfitChg );

    
LastMoValue eq];
  }
}

/////////////////////////////////////////////////
// Drawing code & helper functions
////////////////////////////////////////////////

GfxSetOverlayMode);

CellHeight = (Status("pxheight")-1)/(LastYr FirstYr ); 
CellWidth = (Status("pxwidth")-1)/14
GfxSelectFont"Tahoma"8.5 ); 

GfxSetBkMode);

function 
PrintInCellstringrowCol 
{
 
Color =  ColorRGBIIfrow == || col == || col == 13220255 ), 255IIfrow 2255220 ) );
 
GfxSelectSolidBrushColor   );
 
GfxRectangleCol CellWidth
                    
row CellHeight, (Col ) * CellWidth 1
                    (
row ) * CellHeight  1); 
 
GfxDrawTextstringCol CellWidth 1
                    
row CellHeight 1
                    (
Col ) * CellWidth, (row ) * CellHeight32+); 



YOffset 25;
XOffset 15;

function 
DrawBartextbarnumbarsyMinyMaxy )
{
 
BarWidth = (Status("pxwidth") - XOffset )/( numbars ); 
 
BarHeight Status("pxheight") - YOffset;
 
relpos = ( Miny ) / (Maxy Miny );

 
xp XOffset + ( bar 0.5 ) * BarWidth;
 
yp YOffset BarHeight * ( relpos );
 
xe XOffset + ( bar ) * BarWidth;
 
ye YOffset BarHeight * ( - ( -miny )/( maxy miny ) );
  
 if( 
)
 {
 
GfxGradientRectxpyp,
                  
xe ye,
                  
ColorHSB70255 relpos255 ), ColorHSB7020255 ) );
 }
 else
 {
 
GfxGradientRectxpye,
                  
xe yp,
                  
ColorHSB020255 ), ColorHSB0255 * ( relpos ), 255 ) );
 }
 
GfxTextOuttextxpye );
 
GfxTextOutStrFormat("%.2f"), xpyp );
}    

function 
DrawLevelsMinyMaxy )
{
  
range Maxy Miny;

  
grid 100;
  if( 
range 10 grid 1;
  else 
  if( 
range 20 grid 2;
  else 
  if( 
range 50 grid 5;
  else 
  if( 
range 100 grid 10;
  else 
  if( 
range 200 grid 20;
  else 
  if( 
range 500 grid 50;

  
_TRACE("grid = "+grid +" range "+range );
  
  
width Status("pxwidth") - XOffset;
  
height Status("pxheight") - YOffset;

  
GfxSelectPencolorBlack1);
  for( 
grid ceilMiny grid ); <= grid floorMaxy grid ); += grid )
  {
    
yp =  YOffset Height * ( -  ( Miny ) / (Maxy Miny ) );

    
GfxMoveToXOffsetyp );
    
GfxLineToXOffset width yp );
    
GfxTextOut""yXOffset widthyp );
  }

  
GfxSelectPencolorBlack1);
  
GfxMoveToXOffsetYOffset );
  
GfxLineToXOffset widthYOffset );
  
GfxLineToXOffset widthYOffset Height );
  
GfxLineToXOffset YOffset Height );
  
GfxLineToXOffset YOffset );
}

MonthNames "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec";

function 
DisplayProfitTable( )
{
 
Header "Year,"+MonthNames+",Yr Profit%";
 for( 
Col 0; (Colname StrExtractHeaderCol ) ) != ""Col++ )
 {
  
PrintInCellColName0Col );
 }

 
Row 1;
 for( 
FirstYr<= LastYry++ )
 {
  
PrintInCellStrFormat("%g"), Row); 
  
PrintInCellStrFormat("%.1f%%"VarGet("ChgYear" ) ), Row13 ); 
  for( 
1<= 12m++ )
  { 
   
Chg VarGet("ChgMon" "-" m);
   if( 
Chg 
     
PrintInCellStrFormat("%.1f%%"Chg ), Row);
   else
     
PrintInCell"N/A"Row);
  }
  
Row++;
 } 

 
PrintInCell("Mon. Avg"Row);
 for( 
1<= 12m++ )
 { 
   
PrintInCellStrFormat("%.1f%%",  NzVarGet("SumChgMon" m)/VarGet("SumMon" ) ) ), Row);
 }

}

function 
DisplayYearlyProfits()
{
 
Bar 0;
 for( 
FirstYr<= LastYry++ )
 {
   
Chg VarGet("ChgYear" );
   
DrawBar""+yBar++, ( LastYr FirstYr ), ChgMinYrProfitMaxYrProfit );
 }
 
GfxTextOut("Yearly % Profit chart"1010 );

 
DrawLevelsMinYrProfitMaxYrProfit ); 
}

function 
DisplayMonthlyProfits()
{
 
Bar 0;
 
 
MinAvgProf MaxAvgProf 0;
 for( 
1<= 12y++ )
 {
   
Chg VarGet("SumChgMon" ) / VarGet("SumMon" );
   
MinAvgProf MinMinAvgProfChg );
   
MaxAvgProf MaxMaxAvgProfChg );
 }

 for( 
1<= 12y++ )
 {
   
Chg VarGet("SumChgMon" ) / VarGet("SumMon" );
   
DrawBarStrExtract(MonthNamesy-), Bar++, 13ChgMinAvgProf MaxAvgProf );
 }
 
GfxTextOut("Avg. Monthly % Profit chart"1010 );

 
DrawLevelsMinAvgProf MaxAvgProf ); 
}

///////////////////////////
// This function checks if currently selected symbol
// is portfolio equity
//////////////////////////
function CheckSymbol()
{
 if( 
Name() != "~~~EQUITY" )
 {
  
GfxSelectFont"Tahoma"20 ); 
  
GfxSetBkMode);
  
GfxTextOut("For accurate results switch to ~~~EQUITY symbol"1010 );
 }
}

////////////////////////////
// Main program - chart type switch
////////////////////////////
type ParamList("Chart Type""Profit Table|Yearly Profits|Avg. Monthly Profits");

switch( 
type )
{
 case 
"Profit Table"
         
DisplayProfitTable();  
         break;
 case 
"Yearly Profits"
         
DisplayYearlyProfits();
         break;
 case 
"Avg. Monthly Profits"
         
DisplayMonthlyProfits();
         break;
}

CheckSymbol()

Figure 1. Profit chart in table mode

Profit chart example 2

Figure 2. Profit chart in yearly mode

Profit chart example 3

Figure 3. Profit chart in monthly mode

Profit chart example 4

How to close open positions at the end of the day (for daytraders)

If we are backtesting the intraday strategy and we do not want to keep open positions overnight – then it’s necessary to force closing all the open positions before the session end. In order to code it in AFL – the easiest is to use TimeNum() function for that purpose. (more…)

How to set individual trading rules for symbols in the same backtest

The following code shows how to use separate trading rules for several symbols included in the same backtest. (more…)

« Previous PageNext Page »