Make custom metric in backtest display results up to 4 decimal places

I managed to create a custom metric in backtest results. Here is the code which displays the volume gain as custom metric.

SetCustomBacktestProc( "" );

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

    // iterate through closed trades
    for ( trade = bo.GetFirstTrade( ); trade; trade = bo.GetNextTrade( ) )
    {
        // read Vol values and display as custom metric
        symbolVol = StaticVarGet( trade.Symbol + "VolG" );
        trade.AddCustomMetric( "VolumeGain", Lookup( symbolVol, trade.EntryDateTime ) );
    }

    // iterate through open positions
    for ( trade = bo.GetFirstOpenPos( ); trade; trade = bo.GetNextOpenPos( ) )
    {
        // read Vol values and display as custom metric
        symbolVol = StaticVarGet( trade.Symbol + "VolG" );
        trade.AddCustomMetric( "VolumeGain", Lookup( symbolVol, trade.EntryDateTime ) );
    }

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

// assign indicator values to ticker-specific variables
//GetVolGain() is function to calculate the desired volume information
StaticVarSet( Name() + "VolG", GetVolGain(NUM_BARS_MID_TERM) );

The custom metric results are displayed up to only 2 decimal places by default. However, I want the results to display up to 4 decimal places. How can this be done?

Thank you.

See manual:
https://www.amibroker.com/guide/a_custombacktest.html

bool AddCustomMetric ( string Title , variant Value , [optional] variant LongOnlyValue , [optional] variant ShortOnlyValue , [optional] variant DecPlaces = 2, [optional] variant CombineMethod = 2 )

// for backtest metric
bo.AddCustomMetric( "Backtest Metric", value, longonlyval, shortonlyval, 4 );

// for trade metric
trade.AddCustomMetric( "Trade Metric", value, 4 );

1 Like

I added the extra parameters to AddCustomMetric.

VolGain = StaticVarGet( trade.Symbol + "VolGain" );
trade.AddCustomMetric( "VolGain", Lookup( VolGain, trade.EntryDateTime ), Null, Null, 4);

I got the following error when I run the backtest;

Error 19.
COM method/function `AddCustomMetric` call failed.

COM error: Incorrect number of parameters

@thankyou18,

Please take a look at the manual (provided link in @Tomasz's post).


There is AddCustomMetric method for backtester object (having six function arguments)

8

So

bo = GetBacktesterObject(); // Retrieve the interface to portfolio backtester
bo.Backtest( 1 ); 

//...

bo.AddCustomMetric( Title, Value, LongOnlyValue, ShortOnlyValue, DecPlaces, Combine);

//.... 

And there is AddCustomMetric method for trade object (having three function arguments)

7

So that's the one you need for trade list loop.

trade.AddCustomMetric( "VolumeGain", Lookup( symbolVol, trade.EntryDateTime ), DecPlaces = 4 );
2 Likes

Thank you @fxshrat and @Tomasz

I did read the docs after Tomasz pointed out the function but jumped to the wrong conclusion on seeing the first AddCustomMetric() which had 6 arguments. I thought the error message was wrong. Well, at least this mistake will serve as a reminder to future users to take note. Will be more careful next time.

Thank you for taking the time to answer.

Hi.
Is it possible to define different decimals based on tickers?

DecPlaces = IIf( StrFind( Name(), "JPY" ), 3, 5)

This always gives 5 decimals. even for JPY pairs.


-> trade object

/// INCOMPLETE SNIPPET!!

    // iterate through closed trades
    for ( trade = bo.GetFirstTrade( ); trade; trade = bo.GetNextTrade( ) )
    {
        DecPlaces = IIf(StrFind(trade.Symbol, "JPY" ), 3, 5);
        trade.AddCustomMetric( "...", ...., DecPlaces );
    }

/// INCOMPLETE SNIPPET!!


For AddCustomMetric methof of backtester object you would have to do it differently by iterating traded symbol list.

1 Like

I found the below code online in the Guide and have added it my code. I will add the "Expectancy" calc to the Summary Report but I would like it to display 4 or 6 decimals. I added DecPlaces=4 in the snippet and when run expectancy number is still produced to two decimals and the number "4" is to the right, under the Long Trades column.

I must be missing something obvious.

Thanks,

Mike


SetCustomBacktestProc("");

/* Now custom-backtest procedure follows */

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

    bo.Backtest(); // run default backtest procedure

   SumProfitPer100Inv = 0;
   NumTrades = 0;

   // iterate through closed trades first
   for( trade = bo.GetFirstTrade(); trade; trade = bo.GetNextTrade() )
   {
      // here we sum up profit per $100 invested
       SumProfitPer100Inv = SumProfitPer100Inv + trade.GetPercentProfit();
       NumTrades++;
   }

   // iterate through eventually still open positions
   for( trade = bo.GetFirstOpenPos(); trade; trade = bo.GetNextOpenPos() )
   {
       SumProfitPer100Inv = SumProfitPer100Inv + trade.GetPercentProfit();
       NumTrades++;
   }

   expectancy2 = SumProfitPer100Inv / NumTrades;

    bo.AddCustomMetric( "Expectancy (per $100 inv.)", expectancy2 , DecPlaces=4);

}

You need to look at the documentation for the backtester object's version of AddCustomMetric, not the trade object's version of the AddCustomMetric function. See below.

bool AddCustomMetric( string Title*, variant* Value*, [optional] variant* LongOnlyValue*, [optional] variant* ShortOnlyValue , [optional] variant DecPlaces = 2, [optional] variant CombineMethod = 2 )

This method adds custom metric to the backtest report, backtest "summary" and optimization result list. Title is a name of the metric to be displayed in the report, Value is the value of the metric, optional arguments LongOnlyValue, ShortOnlyValue allow to provide values for additional long/short-only columns in the backtest report. DecPlaces argument controls how many decimal places should be used to display the value. The last CombineMethod argument defines how custom metrics are combined for walk-forward out-of-sample summary report

Wiithout adding "DecPlaces=4", the code produces the correct result on bottom of the bactest report. However, if I add the optional field, "DecPlaces=4", it still displays the calc to 2 decimal places and displays a second number, 4.0. I do not understand your comment to look at the documentation.

Matt..I appreciate your help. I have reviewed the doc you posted but I am using it to display the 4 decimals in the Backtest Report, not a trade report, and it still only displays two decimals. What has me confused is that the Exepctancy is being calculated and listed on the Backtest report but only to 2 decimals. It seems like it would be a simple to add the add optional field DecPlaces=4 but I cannot get it.

Thanks,

Mike

Please re-read following two posts of this very same thread carefully and slowly (you have all the time in the world so don't rush).


They already explain everything you need to know to correct your single line of code.
Hint: your line uses backtester object but not trade object.

bo.AddCustomMetric( "Expectancy (per $100 inv.)", expectancy2 , Null, Null, DecPlaces=4);

Matt & FXSHRAT,

Thank you both for your help & patience! I was reading posts but taking Optional as Optional to include. My mistake.

Thank you,

Mike

Optional is optional, but you can NOT SKIP arguments if LATER along the line is the argument that you want to include. How on earth program would guess which argument you wanted to pass if you skipped randomly some of them in the middle. The order must be kept. Absolute position too. You can skip only AT THE END, not in the middle.

I've noticed in the past that some people seem to assume that they can use named arguments similar to the way it's done in Visual Basic and some other languages: https://docs.microsoft.com/en-us/dotnet/visual-basic/programming-guide/language-features/procedures/passing-arguments-by-position-and-by-name. Obviously this mechanism is not supported by AFL, but not everyone realizes that.

"Assume" . Assumptions are the root of all evil. Why would anyone assume that is beyond my imagination. If AFL is syntactically similar to any language it would be C/C++ or JavaScript. Neither of which has "named arguments".

AFL is NOT similar to Visual Basic in anything. There is absolutely NOTHING in AFL that would suggest similarity to MS Basic.

And no, don't ask for "named arguments". They are evil and ugly.

By the way, people writing code like this:

// WRONG - assignment inside is useless, does nothing except of using extra CPU cycles
bo.AddCustomMetric( "Expectancy (per $100 inv.)", expectancy2 , Null, Null, DecPlaces=4);

are making MISTAKE. There should BE NO ASSIGNMENT in function call arguments.

Should be this:

// CORRECT - DO NOT place useless assignments inside function calls 
bo.AddCustomMetric( "Expectancy (per $100 inv.)", expectancy2 , Null, Null, 4); 

People don't understand that the names in function PROTOTYPES are for DOCUMENTATION purposes only - so you know immediately from function signature that function accepts given input like that MyFunction( text ) accepts text argument.

But this DOES NOT mean that you should type text variable like crazy when you call the function. You should just pass actual VALUE, not the name of argument and for heavens' sake don't make assignments:

MyFunction("actual stringto be passed"); // Correct.  Just the value, nothing else

Do NOT do that:

// WRONG - assignment inside function call is USELESS does not DO anything purposeful and is just nonsense
MyFunction( text= "actual stringto be passed"); // DO NOT do that 
5 Likes

In the online Guide, there are three methods to calculate Expectancy. I have edited the first example with your help and it now produces the stat to 4 decimals. Thank you!
I just tried the second example, posted below, and it runs, flashes "Error 15 Endless loop detected" , I acknowledge the error and the script finishes, producing the correct 4 decimal results.

The error is generated by the first "for" loop.

The base code is the same in Example 1 and Example 2 so I am not sure why it would throw the error.

Thanks and I apologize for the coding mistakes. I carry over some coding form R, as Matt commented, and it creates some problems with AFL.
Thanks for all your help, support, and a great product!

Mike

SetCustomBacktestProc("");

/* Now custom-backtest procedure follows */

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

    bo.Backtest(); // run default backtest procedure

   SumProfitPer100Inv = 0;
   NumTrades = 0;

   // iterate through closed trades first
   for( trade = bo.GetFirstTrade(); trade; trade = bo.GetNextTrade() )
   {
      // here we sum up profit per $100 invested
       SumProfitPer100Inv = SumProfitPer100Inv + trade.GetPercentProfit();
       NumTrades++;
   }

   // iterate through eventually still open positions
   for( trade = bo.GetFirstOpenPos(); trade; trade = bo.GetNextOpenPos() )
   {
       SumProfitPer100Inv = SumProfitPer100Inv + trade.GetPercentProfit();
       NumTrades++;
   }

   expectancy2 = SumProfitPer100Inv / NumTrades;

    bo.AddCustomMetric( "Expectancy (per $100 inv.)", expectancy2, Null, Null, 4 );

}


@Tomasz,

You may disagree but I use a lot of named parameters in Amibroker afl code. They work as long as I use them in the right order in Amibroker. While they may be useless to the machine, they are useful to the human reader. Named parameters are more descriptive. Maybe there are other programmers who agree with me. I also use named parameters a lot in python.

I hope you have no intention to take away this Amibroker "feature". I like it, even if it is an accidental "feature" which you dislike.

Use comments instead.

bo.AddCustomMetric( "Expectancy (per $100 inv.)", expectancy2 , Null, Null, /*DecPlaces*/ 4 );
1 Like

To further clarify: these ARE NOT named arguments but are assignments.
Please, reread carefully the last answer by @Tomasz.

This thing too confused me at first, as there is a lot of old code examples on the internet that use them without the proper purpose (as you wrote, they seem to be used primarily for self-documentation)
I have seen them employed in a "useful" way only in sporadic cases when these assigned variables are reused later in the code.

2 Likes