amibroker

HomeKnowledge Base

How to restrict trading to certain hours of the day

In order to include time-based conditions in the back-testing code – we can use TimeNum() function to check the time-stamp of given bar and use it as input for any time-based conditions.
http://www.amibroker.com/f?timenum

In order to code a strategy that triggers trades only in certain hours of the day, 9:30-11:00 in this example, we can use the following approach (code uses simple MACD crossovers to generate signals):

tn TimeNum();
startTime 93000// start in HHMMSS format
endTime 110000;  // end in HHMMSS format
timeOK tn >= startTime AND tn <= endTime;

Buy CrossMACD(), Signal() ) AND timeOK;
Sell CrossSignal(), MACD() ) AND timeOK

It is also possible to force an exit signal after 11:00 to avoid overnight positions:

tn TimeNum();
startTime 93000// start in HHMMSS format
endTime 110000;  // end in HHMMSS format
timeOK tn >= startTime AND tn <= endTime;

Buy CrossMACD(), Signal() ) AND timeOK;
Sell = (CrossSignal(), MACD() ) AND timeOK) OR CrosstnendTime ); 

Handling limit orders in the backtester

In order to simulate limit orders in backtesting it is necessary to check in the code if Low price of the entry bar is below the limit price we want to use. The following example shows an entry signal based on Close price crossing over 100-period simple moving average. The position is opened on the next bar if price drops 1% below the Close of signal bar.

BuySignal CrossCloseMA(Close100 ) );

// buy on the next bar
Buy RefBuySignal, -1);
BuyLimitPrice RefClose, -1) * 0.99;

// now we check if limit was hit
Buy Buy AND BuyLimitPrice;

// if Open price is below the limit, then we use Open for entry
BuyPrice MinOpenBuyLimitPrice )

If we want the order to be valid for more than one bar, then we can use Hold function for this purpose:

BuySignal CrossCloseMA(Close100 ) );

// buy on the next bar
Buy RefBuySignal, -1);
BuyLimitPrice ValueWhen(BuySignalClose) * 0.99;

// now we check if limit was hit
Buy HoldBuy) AND BuyLimitPrice;

// if Open price is below the limit, then we use Open for entry
BuyPrice MinOpenBuyLimitPrice )

In a portfolio-level backtest we usually advocate against using limit orders. Why? Simply because we may not have enough cash in your account to place limit orders for all possible entry candidates. If your trading system generates 100 possible entries, you would need to place 100 limit orders only to find out that eventually only few of them fired. With limited buying power, we may need to place limit orders only for the top N-scored tickers that have generated BuySignal and skip the others. To simulate the situation when we only place small set of limit orders for top ranked stocks we can use new ranking functionalities introduced in AmiBroker 5.70. Knowing the rank at this stage is required if we only want to allow orders for top-scored tickers. Let us say that we prefer symbols with smallest RSI values.

The code would look the following way: Formula first generates a ranking for all tickers included in the test (below example uses Watchlist 0), then when testing individual symbols – checks the pre-calculated rank and generates Buy signal based on that reading.

// we run the code on WatchList 0
List = CategoryGetSymbolscategoryWatchlist);
SetOption("MaxOpenPositions"3);

if ( 
Status("stocknum") == // Generate ranking when we are on the very first symbol
{
     
StaticVarRemove"values*" );

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

         
// value used for scoring
         
values 100 RSI();
         
RestorePriceArrays();
         
StaticVarSet (  "values"  +  symbolvalues );
         
_TRACEsymbol );
     }

     
StaticVarGenerateRanks"rank""values"01224 );
}

symbol Name();
values StaticVarGet "values" +  symbol );
rank StaticVarGet "rankvalues" +  symbol );

PositionScore values;

BuySignal CrossCloseMA(Close100 ) );

// buy on the next bar
Buy RefBuySignal, -1);
BuyLimitPrice RefClose, -1) * 0.999;

// now we check if limit was hit for the symbols ranked as top 3
Buy Buy AND BuyLimitPrice AND rank <= 3;
BuyPrice MinOpenBuyLimitPrice );

// sample exit rules - 5 - bar stop
Sell 0;
ApplyStopstopTypeNBarstopModeBars51)

Detailed description of the ranking functionality used above is available in the manual at: http://www.amibroker.com/guide/h_ranking.html

Using loops with TimeFrame functions

AmiBroker features a powerful set of TimeFrame functions that allow combining different time intervals in single system formula. There is one aspect of TimeFrame functions that is important to understand to properly use them. When we switch to higher interval using TimeFrameSet function – the BarCount does not really change – TimeFrameSet just squeezes the arrays so we have first N-bars filled with Null values (undefined) and then – last part of the array contains the actual time-compressed values. This is explained in details here: http://www.amibroker.com/guide/h_timeframe.html

Normally it does not present any problem as long as we use array functions, because array functions check for Nulls occuring at the beginning of the data series and skip them appropriately. The story is different when we try to use loops.

If we want to use looping code in higher time-frame, we can not really start our calculations from the bar 0, because it would contain Null instead of real data. That is why we would first need to detect were the actual compressed data begins and start calculations on that particular bar instead.

Here is a sample formula showing how to compute AMA function in a loop, based on weekly data (the code should be applied in Daily interval). Code will identify the first non-Null bar and initialize the first AMA value with Close of that bar, then it will continue calculations

PlotClose"Close"colorBlack );

// switch to higher timeframe
TimeFrameSetinWeekly );

smooth 0.2;
myAMA Close;

// search for start (non-null) bar
for( start 0start BarCountstart++ )
{
   if( 
NOT IsNullClosestart ] ) ) break;
}

// looping code
for ( start 1BarCounti++ )
{
    
// this part will execute only after the first non-null bar has been identified
    
myAMA] = Close] * smooth myAMA] * ( smooth );
}

// regular AMA function for comparison
weeklyAMA AMAClose0.2 );

//restore original time-frame
TimeFrameRestore();

// plot expanded values retrieved from Weekly frame
PlotTimeFrameExpandmyAMAinWeekly ), "weekly AMA loop"colorRed );
PlotTimeFrameExpandweeklyAMAinWeekly ), "weekly AMA"colorBluestyleDots )

The code above is good for pre-5.90 versions. In version 5.90 we have a new function that counts Nulls for us making the code shorter and clearer, as shown below:

Version5.90 );

PlotClose"Close"colorBlack );

// switch to higher timeframe
TimeFrameSetinWeekly );

smooth 0.2;
myAMA Close;

// new 5.90 function that counts leading Nulls
start NullCountClose );

// looping code
for ( start 1BarCounti++ )
{
    
// this part will execute only after the first non-null bar has been identified
    
myAMA] = Close] * smooth myAMA] * ( smooth );
}

// regular AMA function for comparison
weeklyAMA AMAClose0.2 );

//restore original time-frame
TimeFrameRestore();

// plot expanded values retrieved from Weekly frame
PlotTimeFrameExpandmyAMAinWeekly ), "weekly AMA loop"colorRed );
PlotTimeFrameExpandweeklyAMAinWeekly ), "weekly AMA"colorBluestyleDots )

How to display indicator values in the backtest trade list

Backtesting engine in AmiBroker allows to add custom metrics to the report, both in the summary report and in the trade list. This is possible with Custom Backtester Interface, which allows to modify the execution of portfolio-level phase of the test and (among many other features) adjust report generation.

Due to the fact that the report generation occurs in 2nd phase of the test, when the backtester works on ~~~EQUITY ticker, we can not refer directly to given indicators. For example, to display ATR values – calling ATR() function directly is not enough, because we want to see ATR values of the traded symbol, while in portfolio-phase of the test we are no longer working on that symbol’s quotes.

So, we need to:

  1. store the values of indicators in static variables in the 1st phase of the test (when individual symbols are processed). This can be done with static variables, creating separate static variable for each symbol
  2. read stored values once the backtester reaches the portfolio phase of the test.

The following formula shows how this can be coded. The formula below displays the value of ATR indicator for the entry bar of given trade:

SetCustomBacktestProc"" );

if ( 
Status"action" ) == actionPortfolio )
{
    
bo GetBacktesterObject();
    
// run default backtest procedure without generating the trade list
    
bo.BacktestTrue );

    
// iterate through closed trades
    
for ( trade bo.GetFirstTrade( ); tradetrade bo.GetNextTrade( ) )
    {
        
// read ATR values and display as custom metric
        
symbolATR StaticVarGettrade.Symbol "ATR" );
        
trade.AddCustomMetric"Entry ATR"LookupsymbolATRtrade.EntryDateTime ) );
    }

    
// iterate through open positions
    
for ( trade bo.GetFirstOpenPos( ); tradetrade bo.GetNextOpenPos( ) )
    {
        
// read ATR values and display as custom metric
        
symbolATR StaticVarGettrade.Symbol "ATR" );
        
trade.AddCustomMetric"Entry ATR"LookupsymbolATRtrade.EntryDateTime ) );
    }

    
// generate trade list
    
bo.ListTrades( );
}

// your trading system here
Buy CrossMACD(), Signal() );
Sell CrossSignal(), MACD() );

// assign indicator values to ticker-specific variables
StaticVarSetName() + "ATR"ATR15 ) )

How to export quotes to separate text files per symbol

The following KB article: http://www.amibroker.com/kb/2006/03/04/how-to-export-quotations-from-amibroker-to-csv-file/ already explained how to use exploration to export quotes into a single text / CSV file.

If, for some reason, we need individual files for each symbol, AmiBroker offers another way of writing data to text files. This can be achieved by using fputs function that would write directly to external files. Using fputs allows us also to fully control formatting of the output data and file naming can be dynamically set based on Name() function output.

To perform the export procedure, we need to run a Scan over the list of symbols we want to export data for.

In the Analysis->Formula Editor please enter the following code:

// create folder for exporting purposes
fmkdir"C:\\DataExport\\" );

// open file for writing
// file name depends on currently processed ticker
fh fopen"c:\\DataExport\\" Name() + ".txt""w" );

// proceed if file handle is correct
if ( fh )
{
    
dt DateTime();

    
// write header line
    
fputs"Ticker,Date/Time,Open,High,Low,Close,Volume\n"fh );

    
// iterate through all the bars

    
for ( 0BarCounti++ )
    {
        
// write ticker name
        
fputsName() + "," fh );

        
// write date/time information
        
fputsDateTimeToStrdt] ) + ","fh );

        
//write quotations and go to the next line
        
qs StrFormat"%g,%g,%g,%g,%g\n"O], H], L], C], V] );
        
fputsqsfh );

    }
    
// close file handle
    
fclosefh );
}
 
// line required by SCAN option
Buy 0

Now please select Tools->Send to Analysis, select the list of symbols (e.g. Apply To: Filter, pick the watchlist in the Filter dialog), set Range to All Quotations, and press Scan

How to add exploration results to a watchlist

In order to add analysis results to a selected watchlist manually, we can use context menu from the results list:

Add results to watch list

There is, however, a way to automate this process and add the symbols to a watchlist directly from the code. To do so, we need to:
– check if our Filter variable was true at least once in the tested Analysis range
– based on the above condition, use CategoryAddSymbol() function to add tickers to a watchlist.

Additionally, we can erase the watchlist at the beginning of the test if we want to store just the new results.

The code below shows how to implement this procedure in AFL.

listnum 10// we use watchlist 10 for storing results

// erase the watchlist when we process very first symbol
if ( Status"stocknum" ) == )
{
    
// retrieve watchlist members
    
oldlist CategoryGetSymbolscategoryWatchlistlistnum );

    
// iterate through the list and remove tickers
    
for ( 0; ( sym StrExtractoldlist) ) != ""i++ )
    {
        
CategoryRemoveSymbolsymcategoryWatchlistlistnum );
    }
}

// sample exploration code
Filter ROCClose) > AND Volume 1000000;
AddColumnClose"Close" );
AddColumnROCClose), "ROC" );
AddColumnVolume"Volume" );

// check how many times Filter variable was true in the tested range
// if non-zero value detected, add current symbol to a watchlist
if ( LastValueCumFilter AND Status"barinrange" ) ) )  )
    
CategoryAddSymbol""categoryWatchlistlistnum )

Troubleshooting procedure when backtest shows no trades

When we run backtest and get no results at all – there may be several reasons of such behaviour. The main potential causes are the following:

  1. our system does not generate any entry signals within the tested range
  2. our settings do not allow the backtester to take any trades

To verify if we are getting any signals – the first thing to do is to run a Scan. This allows us to check if we are getting any Buy or Short signals at all. If there are none, then we need to check the formula and make sure that data interval we are working on are correct (in Periodicity in Analysis->Settings->General).

If Scan works fine and returns trading signals, but backtester still does not produce any output, it usually means that the settings are wrong, i.e. the constraints set in the settings prevent trades from being opened mainly because requested position size is too big or too small.

To check what is going on, it is best to switch Report mode to Detailed log and re-run backtest.

Report - Detailed log

Once you run backtest in Detailed Log mode you will be able to find out exact reasons why trades can not be opened for each and every bar:

Detailed log output

Using the following settings may be helpful to minimize chances of not entering trades because of various constraints:

In Analysis->Settings, General tab:

  1. check if Initial Equity is high enough
  2. set Periodicity to the appropriate interval
  3. Allow position size shrinking – turn it On
  4. Round Lot Size – set it to 0
  5. in Min. Shares box enter 0.01
  6. in Min. pos. value enter 0
  7. Account Margin – set it to 100

Settings - General

in Portfolio tab, enter 0 in Limit trade size as % of entry bar volume box.

Settings - Portfolio

How to sync a chart with the Analysis window

When we want to sync a chart with the selected symbol in the Analysis results list, it is enough just to double-click on the particular line in the list and AmiBroker will automatically switch the selected symbol and interval to match the Analysis window.

Sync by double click

Additionally, when we browse through Scan or Backtest results, double-clicking would be an equivalent of Show arrows for all raw signals option from the context menu and would display trading arrows in the chart to match the signals generated by the formula.

If we find that double-clicking is too much work, it is possible to mark Sync chart on select option in Analysis window settings menu:

Sync chart on select

and then single click to select a chart is enough to sync the symbol in the chart. This also allows to use keyboard (up/down cursor keys) to change the selection and sync automatically.

When we have more than one chart window displayed, then Analysis window will always sync the last opened chart window.

If we want to sync multiple chart windows we can use Symbol Link feature. Once multiple windows have the same “Symbol Link” color selected, browsing through the results list in Analysis automatically will automatically sync all linked chart windows (e.g. for the purpose of showing different intervals in each of the charts).

Linking

More information about chart link functionality is available in tutorials at:
http://www.amibroker.com/guide/h_sheets.html. See also video tutorial showing how to use symbol linking: http://www.amibroker.com/video/FloatAndLink.html

How to exclude top ranked symbol(s) in rotational backtest

Rotational trading is based on scoring and ranking of multiple symbols based on user-defined criteria. For each symbol a user-definable “score” is assigned on bar by bar basis. Then, each bar, symbols are sorted according to that score and N top ranked symbols are bought, while existing positions that don’t appear in top N rank are closed.

Sometimes however, we may want to exclude the highest ranking symbol (or a couple of them) from trading. The code below shows how to do that using custom backtester.

ExcludeTopN 1// how many top positions to exclude
SetCustomBacktestProc("");

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

    for ( 
bar 0bar BarCountbar++ )
    {
        
Cnt 0;
        for ( 
sig bo.GetFirstSignalbar ); sigsig bo.GetNextSignalbar ) )
        {
            if ( 
Cnt ExcludeTopN )
                
sig.Price = -1// exclude

            
Cnt++;
        }

        
bo.ProcessTradeSignalsbar );
    }

    
bo.PostProcess();
}

EnableRotationalTradingTrue );

SetOption"MaxOpenPositions");
SetOption"WorstRankHeld"10 );
SetPositionSize20spsPercentOfEquity );
PositionScore RSI14 )

The code is pretty straightforward mid-level custom backtest loop but it uses one trick – setting signal price to -1 tells AmiBroker to exclude given signal from further processing. Note also that signals retrieved by GetFirstSignal / GetNextSignal are already sorted, so the highest ranked signal appears first in the list.

How to customize list-view columns

A list-view is a view that displays a list of scrollable items in a table-like format. List-views are used in Real-Time quote window, Analysis window, Symbol list, etc. The columns in any list-view in AmiBroker can be customized in various ways to better match our needs and display the required statistics and readings the way we find it most useful. For the sake of example let us consider Analysis window result list.

Many of the customization actions can be performed directly on the column headers. It is possible to re-order the columns by dragging them with mouse cursor:

Column drag

and their width can be re-sized by dragging the divider lines between columns (double-clicking on that area will auto-resize the columns to match their contents).

Column resize

Hint: You can auto-resize all columns to their content at once by holding down Ctrl key and pressing + (plus sign) key on the numeric keypad.

For more operations it is possible to use Setup Columns… menu available from the context menu, which displays after right-clicking on the headers.

Column resize

Setup Columns dialog allows to re-order, hide/show selected columns.

Column setup

To hide a column uncheck the box, to show it back again, check the box. To re-arrange columns, select a column and click Move Up/Move Down buttons.

It is important to remember that the set of columns will depend on the last run mode, so it will be different for Scan, for the Backtest Trade List, for Summary type of report or Optimization.

Column setup 2

Further customization options are available programmatically. Custom Backtest interface allows to add your own metrics to the backtest report (more info: http://www.amibroker.com/guide/a_custommetrics.html)

We can also define in our code where those additional columns are positioned in the report (this includes both custom metrics added to the report or optimized parameter values in the Optimization). By default they would be listed at the very end, but SetOption() function allows to set different position, for example:

SetOption("ExtraColumnsLocation")
« Previous PageNext Page »