Low Level Custom Backtester - backtestRegularRaw2Multi

Hi,
I would like to run multiple buy and multiple sell rules on 1 symbol. Multiple open positions per symbol allowed.

  • Every buy 1 gets a corresponding sell 1 which results in trade 1.
  • Every buy 2 gets a corresponding sell 2 which results in trade 2.
  • ... and so on.
    The trades can overlap:

Example:

Is there a way to match Entry Signal1 to Exit Signal 1, Entry Signal 2 to Exit Signal 2, etc., when running custom backtest.
My database consists out of daily futures, max 100 symbols, mostly 50 symbols, so I think it won't be a memory problem.
I read about the ScaleIn option, but that way the information "which rule created what trade" gets lost and I would like to create individual reports for each rule, or combinations thereof, or all rules together.

I need the Equity array to do some position sizing based on current equity, so I can't really run the separate backtests conveniently.

The last thing I can think of is to make duplicates of every symbol (I.e. 4 rules, 4 duplicate symbols) and run a normal backtest which brakes up each rule to the corresponding symbol list, seems rather unconventional to me.

Here is the code that doesn't work, it runs, but doesn't achieve my goal - I cannot match entry signal to corresponding exit signal.

SetOption("UseCustomBacktestProc",1);
SetBacktestMode(backtestRegularRaw2Multi);
SetPositionSize(1,spsShares);

buy1 = BarIndex() == 100;
StaticVarSet(Name()+"buy1",IIf(buy1,1,0));//

buy2 = BarIndex() == 100;
StaticVarSet(Name()+"buy2",IIf(buy2,1,0));//

buy3 = BarIndex() == 100;
StaticVarSet(Name()+"buy3",IIf(buy3,1,0));//

sell1 = BarIndex() == 106;
StaticVarSet(Name()+"sell1",IIf(sell1,1,0));//

sell2 = BarIndex() == 105;
StaticVarSet(Name()+"sell2",IIf(sell2,1,0));//

sell3 = BarIndex() == 108;
StaticVarSet(Name()+"sell3",IIf(sell3,1,0));//


PlotShapes(shapedigit1*sell1,colorred,0,L-3*ATR(10));
PlotShapes(shapedigit2*sell2,colorred,0,L-4*ATR(10));
PlotShapes(shapeDigit3*sell3,colorred,0,L-5*ATR(10));


PlotShapes(shapedigit1*buy1,colorGreen,0,H+3*ATR(10));
PlotShapes(shapedigit2*buy2,colorGreen,0,H+4*ATR(10));
PlotShapes(shapeDigit3*buy3,colorGreen,0,H+5*ATR(10));

Buy=buy1 OR buy2 OR buy3;
Sell=sell1 OR sell2 or sell3;

SetCustomBacktestProc("");
if (Status("action") == actionPortfolio)
{
	_TRACE("!CLEAR!");
    bo = GetBacktesterObject();    //  Get backtester object
    bo.PreProcess();    //  Do pre-processing
    for (i = 0; i < BarCount; i++)    //  Loop through all bars
    {
        

        for (sig = bo.GetFirstSignal(i); sig; sig = bo.GetNextSignal(i))
        {    //  Loop through all signals at this bar

			buy1 = staticvarget(sig.symbol + "buy1");//
			buy2 = staticvarget(sig.symbol + "buy2");//
			buy3 = staticvarget(sig.symbol + "buy3");//				
			sell1= staticvarget(sig.symbol + "sell1");//
			sell2= staticvarget(sig.symbol + "sell2");//
			sell3= staticvarget(sig.symbol + "sell3");//

			
			if( sig.IsExit() && sell1[i] )// 
			{
				bo.ExitTrade(i,sig.Symbol,sig.Price);
				b1=bo.GetSignalQty(i,2);
				
				_TRACE("Sig.Price1 =" + sig.Price);
				_TRACE("EXIT-bar1 = " + i);
				_TRACE("EXIT-sell1 = " + sell1[i]);
				_TRACE("EXIT-sigQTYsell1 = " +b1);
				_TRACE("EXIT-isExit1 = " + sig.IsExit());
				_TRACE("               ");
				
			}
			
			if( sig.IsExit() && sell2[i]  )// 
			{
				
				bo.ExitTrade(i,sig.Symbol,sig.Price);
				b2=bo.GetSignalQty(i,2);
				
				_TRACE("Sig.Price2 =" + sig.Price);
				_TRACE("EXIT-bar2 = " + i);
				_TRACE("EXIT-sell2 = " + sell2[i]);
				_TRACE("EXIT-sigQTYsell2 = " +b2);
				_TRACE("EXIT-isExit2 = " + sig.IsExit());
				_TRACE("               ");
				
			}
			if( sig.IsExit() && sell3[i]  )// 
			{
				bo.ExitTrade(i,sig.Symbol,sig.Price);
				b3=bo.GetSignalQty(i,2);
				
				_TRACE("Sig.Price3 =" + sig.Price);
				_TRACE("EXIT-bar3 = " + i);
				_TRACE("EXIT-sell3 = " + sell3[i]);
				_TRACE("EXIT-sigQTYsell3 = " +b3);
				_TRACE("EXIT-isExit3 = " + sig.IsExit());
				_TRACE("               ");
				
			}
						
			if (buy1[i] == 1 && sig.IsEntry() && sig.IsLong())// 
			{	

				a1=bo.GetSignalQty(i,1);
				sig.PosSize = -2002;
				
				bo.EnterTrade(i, sig.Symbol, True, sig.Price, sig.PosSize);
				
				_TRACE("ENTRY-bar1 = " + i);
				_TRACE("ENTRY-buy1 = " +buy1[i]);
				_TRACE("ENTRY-sigQTY1 = " +a1);
				_TRACE("ENTRY-possize1 = " + sig.PosSize);
				_TRACE("ENTRY-isExit1 = " + sig.IsExit());
				_TRACE("ENTRY-isEntry1 = " + sig.IsEntry());
				_TRACE("ENTRY-isLong1 = " + sig.IsLong());
				//_TRACE("roundlotSize1= "+ sig.RoundLotSize);
				//_TRACE("PointValue1= "+ sig.PointValue);		
				_TRACE("                   ");				
					
			}
				
			if (buy2[i] && sig.IsEntry() && sig.IsLong())
			{
				sig.PosSize = -2004;
				
				bo.EnterTrade(i, sig.Symbol, True, sig.Price, sig.PosSize);
					
				a2=bo.GetSignalQty(i,1);
				
				_TRACE("ENTRY-bar2 = " + i);
				_TRACE("ENTRY-buy2 = " +buy2[i]);
				_TRACE("ENTRY-sigQTY2 = " +a2[i]);
				_TRACE("ENTRY-possize2 = " + sig.PosSize);
				_TRACE("ENTRY-isExit2 = " + sig.IsExit());
				_TRACE("ENTRY-isEntry2 = " + sig.IsEntry());
				_TRACE("ENTRY-isLong2 = " + sig.IsLong());
				_TRACE("                   ");		
			}
			if (buy3[i] && sig.IsEntry() && sig.IsLong())
			{
				sig.PosSize = -2016;
				
				bo.EnterTrade(i, sig.Symbol, True, sig.Price, sig.PosSize);
			
				a3=bo.GetSignalQty(i,1);
				
				_TRACE("ENTRY-bar3 = " + i);
				_TRACE("ENTRY-buy3 = " +buy3[i]);
				_TRACE("ENTRY-sigQTY3 = " +a3[i]);
				_TRACE("ENTRY-possize3 = " + sig.PosSize);
				_TRACE("ENTRY-isExit3 = " + sig.IsExit());
				_TRACE("ENTRY-isEntry3 = " + sig.IsEntry());
				_TRACE("ENTRY-isLong3 = " + sig.IsLong());
				_TRACE("                   ");		
			}
			

			
        } 
           //  End of for loop over signals at this bar
        //bo.HandleStops(i);    //  Handle programmed stops at this bar
        bo.UpdateStats(i, 1);    //  Update MAE/MFE stats for bar
        bo.UpdateStats(i, 2);    //  Update stats at bar's end
    }    //  End of for loop over bars
    bo.PostProcess();    //  Do post-processing
}

The entry works (I think):

but there are several exit signals which I don't know where they come from:

Exit 2 (red tick is ok)
Exit 1 (yellow, creates 2 exit signals, I don't know why - any ideas?)
Exit 3 (green, creates 2 exit signals, - no idea why)

After some more playing around and visualizing several variables in CBT (sig.Price, sig.barnumber, etc. , see "_Trace" in code, here are some things I figured out.

  1. The previous' post statement: "the first exit seems to work" is incorrect. It is AB's out-of-the-box behavior, but this does not achieve my matching entry1 to exit1 matching goal. See correction here:

  1. Now I tried this - (Entry on the same bar)
buy1 = BarIndex() == 100;
StaticVarSet(Name()+"buy1",IIf(buy1,1,0));//

buy2 = BarIndex() == 100;
StaticVarSet(Name()+"buy2",IIf(buy2,1,0));//

buy3 = BarIndex() == 100;
StaticVarSet(Name()+"buy3",IIf(buy3,1,0));//

I would have assumed that on bar=100, I would get 3 entry signals. Instead I see only 1:

Any thoughts why I don't see 3 entry signals?

  1. When exiting on the following bars: (i.e. all exits on a separate bar)
sell1 = BarIndex() == 106;
StaticVarSet(Name()+"sell1",IIf(sell1,1,0));//

sell2 = BarIndex() == 105;
StaticVarSet(Name()+"sell2",IIf(sell2,1,0));//

sell3 = BarIndex() == 108;
StaticVarSet(Name()+"sell3",IIf(sell3,1,0));//

I get one exit on signal2 = correct ==> one signal
but signal1 and signal3 create 2 exit signals, even though there is only 1 signal
see here:

If I run exploration, I get all signals correct:

  1. After playing, I found out that AB always exits the first opened position, regardless if the exit signal is Exit-sig1, Exit-sig2, Exit-sig3. My goal is still to match sig1 to exit1, sig2 to exit2, etc.. All in order to run several rules/"trading systems" out of 1 portfolio without using scale, or without creating ticker duplicates. At the moment I'm not sure if this is possible.

If someone has played with this before, or you have an idea/suggestion on how to achieve:
sig1 to exit1, sig2 to exit2, etc - in SetBacktestMode(backtestRegularRaw2Multi);, please help me out!

Thank you.

PS I have some more questions, but I wanna see if somebody can help first, otherwise it gets confusing. If my question is not clear, please feel free to ask. Thanks again.

Nobody can help without having code that can be actually RUN (minimum viable runnable code). Please follow this advice: How to ask a good question

1 Like

@Tomasz, I never worked in the "multi" backtesting modes.
Looking at the documentation:

AFL Function Reference - SETBACKTESTMODE

  • backtestRegularRawMulti - signal-based backtest, redundant (raw) entry signals are NOT removed, MULTIPLE positions per symbol will be open if BUY/SHORT signal is true for more than one bar and there are free funds, Sell/Cover exit all open positions on given symbol, Scale-In/Out work on all open positions of given symbol at once.
  • backtestRegularRaw2 - for custom backtester users only, the same as backtestRegularRaw, but redundant exit signals are also kept. AVOID this mode - it requires lots of memory and slows everything down.
  • backtestRegularRaw2Multi - for custom backtester users only, same as backtestRegularRawMulti, but redundant exit signals are also kept. AVOID this mode - it requires lots of memory and slows everything down.

AFAIU, it says that is not possible to exit the multiple positions for the same symbols independently (cit. exit all open positions on given symbol).
Moreover it is not clear what happens when "scaling": if i have 3 open positions for the same symbol (each position with 50 shares) and I process a single scaleOut of 10 shares in the CBT the result is that all the 3 positions will now have 40 shares each? Please, clarify. Thanks

P.S. How to trace the value of a trade.handle?
Using the %g in a _TRACEF() call I get an error:
6-th argument of the _TRACEF() call has incorrect type. The function expected a NUMBER here, but found a HANDLE.

1 Like

So, the minimum, running code is actually in post#1. I will re-post it below.
After you asked about the code, I wanted to send over also the other settings and noticed (after 4+ days of trial and error - shame on me :slight_smile: ), that I forgot to uncheck "Allow same bar exit / entry signal". Now the EXIT signal quantity works flawless (so far). Please find the updated request for help below.

All relevant settings:




I am still pondering about this:

  1. I have 3 entries at bar number 100. But I only see signalQTY =1 for buy1, buy2, buy3. I expected to see a signalQTY of 3 at bar 100. Any thoughts on this - for me to better understand the CBT behavior?
    _TRACE output:

  2. Main issue: matching BUY1 to SELL1 and BUY2 to SELL2, etc.
    I have 3 trades open on bar 100. When the first Exit signal is triggered on bar 105, corresponding to SELL2, it exits the first opened position. That seems to be AB's standard behavior: First opened position gets closed first, second opened position second, and so on.
    Now, I would like to match my BUY1 signal to SELL1 signal. I'm missing something here, a way to link the entries and exits, I don't know a way how to match those signals. You can see from the _TRACE output for the exits:

The way it is:
SELL2 comes first: Exit Position1
SELL1 comes second: Exit Position 2
SELL3 comes third: Exit Position 3

What I would like to achieve:
SELL2 comes first: Exit ---->Position2
SELL1 comes second: Exit ---->Position 1
SELL3 comes third: Exit Position 3

here is the working code again:

SetOption("UseCustomBacktestProc",1);
SetBacktestMode(backtestRegularRaw2Multi);
SetPositionSize(1,spsShares);

buy1 = BarIndex() == 100;
StaticVarSet(Name()+"buy1",IIf(buy1,1,0));//

buy2 = BarIndex() == 100;
StaticVarSet(Name()+"buy2",IIf(buy2,1,0));//

buy3 = BarIndex() == 100;
StaticVarSet(Name()+"buy3",IIf(buy3,1,0));//

sell1 = BarIndex() == 106;
StaticVarSet(Name()+"sell1",IIf(sell1,1,0));//

sell2 = BarIndex() == 105;
StaticVarSet(Name()+"sell2",IIf(sell2,1,0));//

sell3 = BarIndex() == 108;
StaticVarSet(Name()+"sell3",IIf(sell3,1,0));//


PlotShapes(shapedigit1*sell1,colorred,0,L-3*ATR(10));
PlotShapes(shapedigit2*sell2,colorred,0,L-4*ATR(10));
PlotShapes(shapeDigit3*sell3,colorred,0,L-5*ATR(10));


PlotShapes(shapedigit1*buy1,colorGreen,0,H+3*ATR(10));
PlotShapes(shapedigit2*buy2,colorGreen,0,H+4*ATR(10));
PlotShapes(shapeDigit3*buy3,colorGreen,0,H+5*ATR(10));

Buy=buy1 OR buy2 OR buy3;
Sell=sell1 OR sell2 or sell3;

SetCustomBacktestProc("");
if (Status("action") == actionPortfolio)
{
	_TRACE("!CLEAR!");
    bo = GetBacktesterObject();    //  Get backtester object
    bo.PreProcess();    //  Do pre-processing
    for (i = 0; i < BarCount; i++)    //  Loop through all bars
    {
        

        for (sig = bo.GetFirstSignal(i); sig; sig = bo.GetNextSignal(i))
        {    //  Loop through all signals at this bar

			buy1 = staticvarget(sig.symbol + "buy1");//
			buy2 = staticvarget(sig.symbol + "buy2");//
			buy3 = staticvarget(sig.symbol + "buy3");//				
			sell1= staticvarget(sig.symbol + "sell1");//
			sell2= staticvarget(sig.symbol + "sell2");//
			sell3= staticvarget(sig.symbol + "sell3");//

			
			if( sig.IsExit() && sell1[i] )// 
			{
				bo.ExitTrade(i,sig.Symbol,sig.Price);
				b1=bo.GetSignalQty(i,2);
				
				_TRACE("Sig.Price1 =" + sig.Price);
				_TRACE("EXIT-bar1 = " + i);
				_TRACE("EXIT-sell1 = " + sell1[i]);
				_TRACE("EXIT-sigQTYsell1 = " +b1);
				_TRACE("EXIT-isExit1 = " + sig.IsExit());
				_TRACE("               ");
				
			}
			
			if( sig.IsExit() && sell2[i]  )// 
			{
				
				bo.ExitTrade(i,sig.Symbol,sig.Price);
				b2=bo.GetSignalQty(i,2);
				
				_TRACE("Sig.Price2 =" + sig.Price);
				_TRACE("EXIT-bar2 = " + i);
				_TRACE("EXIT-sell2 = " + sell2[i]);
				_TRACE("EXIT-sigQTYsell2 = " +b2);
				_TRACE("EXIT-isExit2 = " + sig.IsExit());
				_TRACE("               ");
				
			}
			if( sig.IsExit() && sell3[i]  )// 
			{
				bo.ExitTrade(i,sig.Symbol,sig.Price);
				b3=bo.GetSignalQty(i,2);
				
				_TRACE("Sig.Price3 =" + sig.Price);
				_TRACE("EXIT-bar3 = " + i);
				_TRACE("EXIT-sell3 = " + sell3[i]);
				_TRACE("EXIT-sigQTYsell3 = " +b3);
				_TRACE("EXIT-isExit3 = " + sig.IsExit());
				_TRACE("               ");
				
			}
						
			if (buy1[i] && sig.IsEntry() && sig.IsLong())// 
			{	

				a1=bo.GetSignalQty(i,1);
				sig.PosSize = -2002;
				
				bo.EnterTrade(i, sig.Symbol, True, sig.Price, sig.PosSize);
				
				_TRACE("ENTRY-bar1 = " + i);
				_TRACE("ENTRY-buy1 = " +buy1[i]);
				_TRACE("ENTRY-sigQTY1 = " +a1);
				_TRACE("ENTRY-possize1 = " + sig.PosSize);
				_TRACE("ENTRY-isExit1 = " + sig.IsExit());
				_TRACE("ENTRY-isEntry1 = " + sig.IsEntry());
				_TRACE("ENTRY-isLong1 = " + sig.IsLong());
				//_TRACE("roundlotSize1= "+ sig.RoundLotSize);
				//_TRACE("PointValue1= "+ sig.PointValue);		
				_TRACE("                   ");				
					
			}
				
			if (buy2[i] && sig.IsEntry() && sig.IsLong())
			{
				sig.PosSize = -2004;
				
				bo.EnterTrade(i, sig.Symbol, True, sig.Price, sig.PosSize);
					
				a2=bo.GetSignalQty(i,1);
				
				_TRACE("ENTRY-bar2 = " + i);
				_TRACE("ENTRY-buy2 = " +buy2[i]);
				_TRACE("ENTRY-sigQTY2 = " +a2[i]);
				_TRACE("ENTRY-possize2 = " + sig.PosSize);
				_TRACE("ENTRY-isExit2 = " + sig.IsExit());
				_TRACE("ENTRY-isEntry2 = " + sig.IsEntry());
				_TRACE("ENTRY-isLong2 = " + sig.IsLong());
				_TRACE("                   ");		
			}
			if (buy3[i] && sig.IsEntry() && sig.IsLong())
			{
				sig.PosSize = -2016;
				
				bo.EnterTrade(i, sig.Symbol, True, sig.Price, sig.PosSize);
			
				a3=bo.GetSignalQty(i,1);
				
				_TRACE("ENTRY-bar3 = " + i);
				_TRACE("ENTRY-buy3 = " +buy3[i]);
				_TRACE("ENTRY-sigQTY3 = " +a3[i]);
				_TRACE("ENTRY-possize3 = " + sig.PosSize);
				_TRACE("ENTRY-isExit3 = " + sig.IsExit());
				_TRACE("ENTRY-isEntry3 = " + sig.IsEntry());
				_TRACE("ENTRY-isLong3 = " + sig.IsLong());
				_TRACE("                   ");		
			}
			

			
        } 
           //  End of for loop over signals at this bar
        //bo.HandleStops(i);    //  Handle programmed stops at this bar
        bo.UpdateStats(i, 1);    //  Update MAE/MFE stats for bar
        bo.UpdateStats(i, 2);    //  Update stats at bar's end
    }    //  End of for loop over bars
    bo.PostProcess();    //  Do post-processing
}

Thank you very much for your time and support.

Hi Beppe,

Blockquote
AFAIU, it says that is not possible to exit the multiple positions for the same symbols independently (cit. exit all open positions on given symbol).

that seems to be the case. the behavior I get: 1st position gets closed on 1st exit, 2nd position on 2nd, etc., regardless of order. From my perspective, matching Entries and Exits in

SetBacktestMode(backtestRegularRaw2Multi);

would help a lot when backtesting multiple systems simultaneously.

Thanks for your thoughts.

If you want to close a specific trade, you can use the Handle value, which is one of the properties of a Trade object.

Alternatively, instead of working so hard to support multiple systems within a single backtest, you can write backtests for each one and combine the equity curves after the fact to get a good approximation of the combined performance.

1 Like

Thanks Matt,
Did you ever play around with trade.Handle, do you maybe have a code snippet / pseudo code?

I iterated over the open positions, trying to get a hold of what trade.handle does, but no avail. I didn't find any examples either.

			for ( trade = bo.GetFirstOpenPos( ); trade; trade = bo.GetNextOpenPos( ) )
			{
				tradeHandle=trade.Handle();
				rate = trade.EntryFxRate();
				EntryDate = trade.EntryDateTime();
				EntryPrice = trade.EntryPrice();
				_TRACE("OPENPOS OPENPOS OPENPOS");
				_TRACE("Handle " + tradeHandle);
				_TRACE("EntryRate " + rate);
				_TRACE("Entrydate " + Entrydate);
				_TRACE("EntryPrice " + EntryPrice);
				_TRACE("                   ");	
					
			}

The idea of trading multiple systems in one account has one (maybe more) advantages: the position size / qty can be managed with regards to what the other systems are doing, i.e. you use current equity to size trades.
If you have different, separate backtests, yes, you could convert the cash curves into cumulative %-return-curves and add them together, but the systems don't know about each other's equity still.
So having, for example, 3 systems, you allocate 33.3% to each system and you constantly (ie. monthly, special threshold, etc.) rebalance the allocation, that would also work when compounding profits.
That is quite useful when trading portfolios. And don't underestimate trading multiple entry/exit rules, it adds diversification.
So the 33.3% are equal allocation, hedge funds go to the extend and optimize those allocation on a rolling basis for the optimal outcome (that's beyond me, though). You can look at some of the work of Rob Carver, he has shared a lot about his trading style that he used when managing a multi-billion hedge fund at MAN (in the UK). It's really good stuff there.
Rob's blog
Rob's books

Thank you.

I agree that there are advantages to combining strategies directly, but it's not particularly easy to do that in AmiBroker. That's why I built a tool to combine equity curves, optimize the allocation of each strategy, rebalance on a periodic basis, etc. It's a reasonable (if imperfect) solution for many people.

Regarding Trade Handles, my experience has been that they are a bit hard to work with, because ideally I would store the handle in a static variable so that I could easily retrieve it later when I found a corresponding exit signal. However, last time I tried that, I found that StaticVarSet() would not accept a handle because of its underlying type (double vs. float). As a workaround, I have sometimes stored a "trade type" value in the PositionScore field of the Trade object, i.e. by specifying that value when calling bo.EnterTrade(). Then when I find an exit signal, I iterate through all the open trades to find one with the same ticker symbol and "trade type" as the exit signal. If found, then I can close it with the handle that's also stored in the trade object. That whole approach is a little clumsy and slow, but it works. Perhaps @Tomasz has a more elegant way of achieving the same goal.

3 Likes

Generally the idea behind LOW level backtester, is that you are free to use anything and you are NOT really limited to "modes" of backtester at all, as you essentially call EnterTrade/ExitTrade/ScaleTrade yourself and you don't need signals at all.

Hi,
Would you be so kind and show us 1 or 2 examples on how to use the trade.handle property?
Is there a way to store the trade.handle in a static variable, Matt was having problems?
How can we use _TRACE to to check for the handle?

Thank you.

You don't display handle using _TRACE because there is nothing human readable in handle. Handle is an address in memory (RAM) and nothing more. No, handle can't be stored in static variable because it is temporary address in RAM, and it is not something that you can use later or in other formula/thread. The example is given in "Release Notes" document:

CHANGES FOR VERSION 4.76.0
CBT: Trade object has now "Handle" property that can be passed to ExitTrade and ScaleTrade methods instead of symbol to allow control over exiting/scaling multiple positions of the same symbol

// This is sample formula that allows
// to open multiple, separate positions on the same symbol
// without averaging effect (i.e. each position on the same
// symbol is completely independent).
//
// Sample code is provided for trading one symbol
// Enter symbol you want to trade below
Symbol = "MSFT";

Buy = Sell = Short = Cover = 0; // real rules are defined inside custom backtest proc

SetCustomBacktestProc( "" ); // enable custom backtest

if( Status( "action" ) == actionPortfolio )
{
    // actual backtest routine
    // (low-level)

    bo = GetBacktesterObject();

    SetForeign( Symbol );
    // make sure to calculate actual buy and buyprice arrays for symbol we need to backtest
    EntrySignal = 1; // For testing purposes just enter new position every bar
    EntryPrice = Open;
    RestorePriceArrays();

    // actual backtest loop
    bo.PreProcess();

    for( i = 1; i < BarCount; i++ )
    {
        // first update backtest stats and handle stops
        bo.UpdateStats( i, 0 );
        bo.HandleStops( i );

        bo.RawTextOutput( "Bar " + i );

        if( EntrySignal [ i - 1 ] ) // if buy signal in previous bar
        {
            bo.EnterTrade( i, Symbol, True, EntryPrice[ i ], 500 /* $5000 into one trade */ );
        }


        for( OpenPos = bo.GetFirstOpenPos(); OpenPos; OpenPos = bo.GetNextOpenPos() )
        {
            // exit positions if their age is > 5 bars and they are profitable
            if( OpenPos.BarsInTrade > 5 AND OpenPos.GetProfit() > 0 )
            {
                // HERE IS A NEW PART !!!
                // WE ARE PASSING HANDLE instead of ticker symbol
                // THIS ENSURES PROPER OPERATION even if we have multiple positions of the same
                // stock
                bo.ExitTrade( i, OpenPos.Handle, OpenPos.GetPrice( i, "O" ) );
            }
        }

        bo.RawTextOutput( "Number of open positions: " + bo.GetOpenPosQty() );


        bo.UpdateStats( i, 2 );
    }

    bo.PostProcess();
}
1 Like

Hi again,
Thank you very much posting the example. I'm still having problems with the trade.handle.

So the example ( CHANGES FOR VERSION 4.76.0 - example) does enter one new trade at every NEW bar (not the same bar), then after 5 days plus positive profit, it exits. The trades are essentially in order all the time. I'm not sure if I see the point of trade.handle here.

So I changed it to the following:

  1. Enter 3 trades on the first day
  2. Have 3 different exits at different bars (bar = 5, 6, 7)
  3. I thought every time I use "ExitTrade" it generates ONE exit signal only. But that's not the case. Once the ExitTrade is called, all positions are closed.

if( OpenPos.BarsInTrade > 5 AND OpenPos.GetProfit() > 0 ) is true first, followed by
if( OpenPos.BarsInTrade > 6 AND OpenPos.GetProfit() > 0 )
and then
if( OpenPos.BarsInTrade > 7 AND OpenPos.GetProfit() > 0 )

Result: all trades exit at the same time. I'm missing something here again to uniquely identify my trades again: Buy1 corresponds to Sell1, Buy2 to Sell2, etc.

Code used:

Symbol = "A50";

Buy = Sell = Short = Cover = 0; // real rules are defined inside custom backtest proc

SetCustomBacktestProc( "" ); // enable custom backtest

if( Status( "action" ) == actionPortfolio )
{
    // actual backtest routine
    // (low-level)

    bo = GetBacktesterObject();

    SetForeign( Symbol );
    // make sure to calculate actual buy and buyprice arrays for symbol we need to backtest
    Buy1 = BarIndex() == 100;// OR BarIndex()==101 OR BarIndex()==102 ; // For testing purposes just enter new position every bar
    Buy2 = BarIndex() == 100;
    Buy3 = BarIndex() == 100;
    BuyPrice = Open;
    RestorePriceArrays();

    // actual backtest loop
    bo.PreProcess();

    for( i = 1; i < BarCount; i++ )
    {
        // first update backtest stats and handle stops
        bo.UpdateStats( i, 0 );
        bo.HandleStops( i );

        //bo.RawTextOutput( "Bar " + i );

        if( Buy1[ i - 1 ] ) // if buy signal in previous bar
        {
            bo.EnterTrade( i, Symbol, True, BuyPrice[ i ], 50000 /* $5000 into one trade */ );
        }
        if( Buy2[ i - 1 ] ) // if buy signal in previous bar
        {
            bo.EnterTrade( i, Symbol, True, BuyPrice[ i ], 50000 /* $5000 into one trade */ );
        }        
        if( Buy3[ i - 1 ] ) // if buy signal in previous bar
        {
            bo.EnterTrade( i, Symbol, True, BuyPrice[ i ], 50000 /* $5000 into one trade */ );
        }

        for( OpenPos = bo.GetFirstOpenPos(); OpenPos; OpenPos = bo.GetNextOpenPos() )
        {
            // exit positions if their age is > 5 bars and they are profitable
            if( OpenPos.BarsInTrade > 5 AND OpenPos.GetProfit() > 0 )
            {
                // HERE IS A NEW PART !!!
                // WE ARE PASSING HANDLE instead of ticker symbol
                // THIS ENSURES PROPER OPERATION even if we have multiple positions of the same
                // stock
                bo.ExitTrade( i, OpenPos.Handle, OpenPos.GetPrice( i, "O" ) );
            }
            if( OpenPos.BarsInTrade > 6 AND OpenPos.GetProfit() > 0 )
            {
                // HERE IS A NEW PART !!!
                // WE ARE PASSING HANDLE instead of ticker symbol
                // THIS ENSURES PROPER OPERATION even if we have multiple positions of the same
                // stock
                bo.ExitTrade( i, OpenPos.Handle, OpenPos.GetPrice( i, "O" ) );
            }
            if( OpenPos.BarsInTrade > 7 AND OpenPos.GetProfit() > 0 )
            {
                // HERE IS A NEW PART !!!
                // WE ARE PASSING HANDLE instead of ticker symbol
                // THIS ENSURES PROPER OPERATION even if we have multiple positions of the same
                // stock
                bo.ExitTrade( i, OpenPos.Handle, OpenPos.GetPrice( i, "O" ) );
            }
            
        }

        //bo.RawTextOutput( "Number of open positions: " + bo.GetOpenPosQty() );


        bo.UpdateStats( i, 2 );
    }

    bo.PostProcess();
}

So then I modified my code from post #3 to correspond to the example template (CHANGES FOR VERSION 4.76.0)

  • 3 entries at the same bar
  • Now we have 3 Exits (sell1, sell2, sell3), that don't go "True" at the same bar. Still, once the first ExitTrade is called (corresponding to sell2 in this case), all positions are closed at the same time. Again I'm missing the connection how to identify opened positions. Can someone help?

here is the updated code with the openpos.Handle:


Symbol = "A50";

Buy = Sell = Short = Cover = 0; // real rules are defined inside custom backtest proc --> ok

SetCustomBacktestProc( "" ); // enable custom backtest --> ok
SetBacktestMode(backtestRegularRaw2Multi); // not sure if it's needed
if( Status( "action" ) == actionPortfolio )
{
    // actual backtest routine
    // (low-level)
	_TRACE("!CLEAR!");
    bo = GetBacktesterObject();

    SetForeign( Symbol );
    // make sure to calculate actual buy and buyprice arrays for symbol we need to backtest
    buy1 = BarIndex() == 100;
    buy2 = BarIndex() == 100;
	buy3 = BarIndex() == 100;
	    
    sell1 = BarIndex() == 106;
	sell2 = BarIndex() == 105;
	sell3 = BarIndex() == 108;
	
    Buy = 0;//buy1 OR buy2 or buy3; // Different entries now
    BuyPrice = C;	// I trade on Close
    Sell= 0;//sell1 OR sell2 OR sell3; //different Exits now
    SellPrice = C; // I trade on Close
        
    RestorePriceArrays();

    // actual backtest loop
    bo.PreProcess();

    for( i = 0; i < BarCount; i++ )
    {
        // first update backtest stats and handle stops
        bo.UpdateStats( i, 0 );
        bo.HandleStops( i );

        //bo.RawTextOutput( "Bar " +i );
		
        if( Buy1[ i ]) // if buy1 signal 
        {
			a1=bo.GetSignalQty(i,0);
            bo.EnterTrade( i, symbol, True, BuyPrice[ i ], -2002 );
            	_TRACE("ENTRY-bar1 = " + i);
				_TRACE("ENTRY-buy1 = " +buy1[i]);
				_TRACE("ENTRY-sigQTY1 = " +a1);
				//_TRACE("ENTRY-possize1 = " + sig.PosSize); // no more signal used, so comment out
				//_TRACE("ENTRY-isExit1 = " + sig.IsExit());
				//_TRACE("ENTRY-isEntry1 = " + sig.IsEntry());
				//_TRACE("ENTRY-isLong1 = " + sig.IsLong());
				//_TRACE("roundlotSize1= "+ sig.RoundLotSize);
				//_TRACE("PointValue1= "+ sig.PointValue);		
				_TRACE("                   ");	
        }
        
        if( Buy2[ i ] ) // if buy2 
        {
            bo.EnterTrade( i, Symbol, True, BuyPrice[ i ], -2004 );
                _TRACE("ENTRY-bar2 = " + i);
				_TRACE("ENTRY-buy2 = " +buy2[i]);
				_TRACE("ENTRY-sigQTY2 = " +a1);
				//_TRACE("ENTRY-possize2 = " + sig.PosSize); // no more signal used, so comment out
				//_TRACE("ENTRY-isExit2 = " + sig.IsExit());
				//_TRACE("ENTRY-isEntry2 = " + sig.IsEntry());
				//_TRACE("ENTRY-isLong2 = " + sig.IsLong());
				//_TRACE("roundlotSize1= "+ sig.RoundLotSize);
				//_TRACE("PointValue1= "+ sig.PointValue);		
				_TRACE("                   ");	
        }
        if( Buy3[ i ] ) // if buy3 signal
        {
            bo.EnterTrade( i, Symbol, True, BuyPrice[ i ], -2006 );
            a3=bo.GetSignalQty(i,0);
				
			_TRACE("ENTRY-bar3 = " + i);
			_TRACE("ENTRY-buy3 = " +buy3[i]);
			_TRACE("ENTRY-sigQTY2 = " +a3[i]);
			//_TRACE("ENTRY-possize3 = " + sig.PosSize); // no more signal used, so comment out
			//_TRACE("ENTRY-isExit3 = " + sig.IsExit());
			//_TRACE("ENTRY-isEntry3 = " + sig.IsEntry());
			//_TRACE("ENTRY-isLong3 = " + sig.IsLong());
			_TRACE("                   ");		
        }	
		
        for( OpenPos = bo.GetFirstOpenPos(); OpenPos; OpenPos = bo.GetNextOpenPos() )
        {
            
           if( Sell1[i] ) //// sell1 = BarIndex() == 106;
            {
                // HERE IS A NEW PART !!!
                // WE ARE PASSING HANDLE instead of ticker symbol
                // THIS ENSURES PROPER OPERATION even if we have multiple positions of the same
                // stock
                b1=bo.GetSignalQty(i,0);
                bo.ExitTrade( i, OpenPos.Handle, OpenPos.GetPrice( i, "C" ) );
				//_TRACE("Sig.Price1 =" + sig.Price); // no more signal used, so comment out
				_TRACE("EXIT-bar1 = " + i);
				_TRACE("EXIT-sell1 = " + sell1[i]);
				_TRACE("EXIT-sigQTYsell1 = " +b1);
				//_TRACE("EXIT-isExit1 = " + sig.IsExit());
				_TRACE("               ");
								
             
                
                
            }
            
            if( Sell2[i] ) // sell2 = BarIndex() == 105; <-- this one is supposed to Exit position2 only (corresponding to buy2 ==True) only, but it exits all 3 open positions instead.
            {
                b2=bo.GetSignalQty(i,0);
                bo.ExitTrade( i, OpenPos.Handle, OpenPos.GetPrice( i, "C" ) );               
				//_TRACE("Sig.Price2 =" + sig.Price); // no more signal used, so comment out
				_TRACE("EXIT-bar2 = " + i);
				_TRACE("EXIT-sell2 = " + sell2[i]);
				_TRACE("EXIT-sigQTYsell2 = " +b2);
				//_TRACE("EXIT-isExit2 = " + sig.IsExit());
				_TRACE("               ");				
				        
			}
			
            if( Sell3[i] ) // sell3 = BarIndex() == 108;
            
            {
                b3=bo.GetSignalQty(i,0);
                bo.ExitTrade( i, OpenPos.Handle, OpenPos.GetPrice( i, "C" ) );            			
								
				//_TRACE("Sig.Price3 =" + sig.Price); // no more signal used, so comment out
				_TRACE("EXIT-bar3 = " + i);
				_TRACE("EXIT-sell3 = " + sell3[i]);
				_TRACE("EXIT-sigQTYsell3 = " +b3);
				//_TRACE("EXIT-isExit3 = " + sig.IsExit()); // no more signal used, so comment out
				_TRACE("               ");
				
			}
			
        }

        //bo.RawTextOutput( "Number of open positions: " + bo.GetOpenPosQty() );


        bo.UpdateStats( i, 2 );
    }

    bo.PostProcess();
}




/* Comment in, move formula to chart to visualize the entries and exits as "shapedigit", staticvars not needed for exploration, but left inside from previous code (ignore)

buy1 = BarIndex() == 100;
StaticVarSet(Name()+"buy1",IIf(buy1,1,0));//

buy2 = BarIndex() == 100;
StaticVarSet(Name()+"buy2",IIf(buy2,1,0));//

buy3 = BarIndex() == 100;
StaticVarSet(Name()+"buy3",IIf(buy3,1,0));//

sell1 = BarIndex() == 106;
StaticVarSet(Name()+"sell1",IIf(sell1,1,0));//

sell2 = BarIndex() == 105;
StaticVarSet(Name()+"sell2",IIf(sell2,1,0));//

sell3 = BarIndex() == 108;
StaticVarSet(Name()+"sell3",IIf(sell3,1,0));//

printf("Barindex = "+BarIndex()+"\n");
PlotShapes(shapedigit1*sell1,colorred,0,L-3*ATR(10));
PlotShapes(shapedigit2*sell2,colorred,0,L-4*ATR(10));
PlotShapes(shapeDigit3*sell3,colorred,0,L-5*ATR(10));


PlotShapes(shapedigit1*buy1,colorGreen,0,H+3*ATR(10));
PlotShapes(shapedigit2*buy2,colorGreen,0,H+4*ATR(10));
PlotShapes(shapeDigit3*buy3,colorGreen,0,H+5*ATR(10));

Filter=1;
//buy1 = staticvarget(sig.symbol + "buy1");
AddColumn(StaticVarGet(Name()+"buy1"),"buy1",1);
AddColumn(StaticVarGet(Name()+"buy2"),"buy2",1);
AddColumn(StaticVarGet(Name()+"buy3"),"buy3",1);
AddColumn(StaticVarGet(Name()+"sell1"),"sell1",1);
AddColumn(StaticVarGet(Name()+"sell2"),"sell2",1);
AddColumn(StaticVarGet(Name()+"sell3"),"sell3",1);
*/ 

Thank you very much.

Sorry, just to add on:
I understand from mradtke the usage of assigning+storing a number in "postionscore" to make the openpos unique, but is there a way to avoid this?

The code is all wrong.

  1. It doesn't make sense to use SetForeign() inside custom backtester to get "barindex" because it doesn't change anything. Please read documentation on SetForeign and Foreign what these functions do
  2. It doesn't make sense to assign Open as BuyPrice in CBT. BuyPrice is variable reserved for FIRST phase of backtest. Use different variable name to avoid confusion. UPDATE: Example code was using that but it was poor choice and example has been changed to use EntryPrice
  3. Positions are closed because YOU ARE CLOSING THEM. Your code on Sell1 closes ALL THREE POSITIONS because it calls ExitTrade THREE TIMES for each open position. You would have to BREAK OUT OF THE LOOP (stop processing further open positions) so you just exit ONE open position not all open positions

As per my interpretation:

Point 2:

It doesn't make sense to assign Open as BuyPrice in CBT. BuyPrice is variable reserved for FIRST phase of backtest. Use different variable name to avoid confusion

==> Correct me if I'm wrong, but your example code does exactly that. However, that's besides my point.

// This is sample formula that allows
// to open multiple, separate positions on the same symbol
// without averaging effect (i.e. each position on the same
// symbol is completely independent).
//
// Sample code is provided for trading one symbol
// Enter symbol you want to trade below
Symbol = "MSFT";

Buy = Sell = Short = Cover = 0; // real rules are defined inside custom backtest proc

SetCustomBacktestProc( "" ); // enable custom backtest

if( Status( "action" ) == actionPortfolio )
{
    // actual backtest routine
    // (low-level)

    bo = GetBacktesterObject();

    SetForeign( Symbol );
    // make sure to calculate actual buy and buyprice arrays for symbol we need to backtest
    Buy = 1; // For testing purposes just enter new position every bar
    BuyPrice = Open;
    RestorePriceArrays();

    // actual backtest loop
    bo.PreProcess();

    for( i = 1; i < BarCount; i++ )
    {
        // first update backtest stats and handle stops
        bo.UpdateStats( i, 0 );
        bo.HandleStops( i );

        bo.RawTextOutput( "Bar " + i );

        if( Buy[ i - 1 ] ) // if buy signal in previous bar
        {
            bo.EnterTrade( i, Symbol, True, BuyPrice[ i ], 500 /* $5000 into one trade */ ); //
################################
################################
No BuyPrice here - right!??
        }


        for( OpenPos = bo.GetFirstOpenPos(); OpenPos; OpenPos = bo.GetNextOpenPos() )
        {
            // exit positions if their age is > 5 bars and they are profitable
            if( OpenPos.BarsInTrade > 5 AND OpenPos.GetProfit() > 0 )
            {
                // HERE IS A NEW PART !!!
                // WE ARE PASSING HANDLE instead of ticker symbol
                // THIS ENSURES PROPER OPERATION even if we have multiple positions of the same
                // stock
                bo.ExitTrade( i, OpenPos.Handle, OpenPos.GetPrice( i, "O" ) );
            }
        }

        bo.RawTextOutput( "Number of open positions: " + bo.GetOpenPosQty() );


        bo.UpdateStats( i, 2 );
    }

    bo.PostProcess();
}

Point 3:

Positions are closed because YOU ARE CLOSING THEM. Your code on Sell1 closes ALL THREE POSITIONS because it calls ExitTrade THREE TIMES for each open position. You would have to BREAK OUT OF THE LOOP (stop processing further open positions) so you just exit ONE open position not all open positions

That is absolutely correct, sell2 is triggered 3 times on bar 105:


The whole thread here essentially boils down to your point 3:

How can I break the loop?

If sell2 = True, I want it to correspond to position opened by Buy2 and Buy2 to only. In other words, I (maybe?!) need another identifier for sell2, i.e. another "True" condition, so the sell2 condition triggers only the position opened for buy2 and disregards the other 2 exits. I am not able to find a solution to this because I'm not able to identify my first entered position in an AB way, store it, and thus separate them from the others. Maybe I'm missing something, I really don't know.

Do you have any thoughts on that?

Thank you.

Oh well, that is true, but I am not god and I am not error-free. I should have used different variable name. It is harmless to assign BuyPrice there as the code works fine, regardless from the fact that variable name choice was poor. It is misleading to use BuyPrice name here as it is special purpose variable. I changed variable names in the example to EntryPrice and EntrySignal as they are generic names not reserved/special purpose variables.

You break from the loop using..... the break statement:

http://www.amibroker.com/guide/keyword/break.html

If sell2 = True, I want it to correspond to position opened by Buy2 and Buy2 to only.

If Sell2[ i ] is true, your current code is performing ENTIRE LOOP - for as many open positions as you have, and it essentially calling ExitTrade many times - as many times as there are open positions, because you are LOOPING and check the same Sell2[i] condition over and over for as long as there are open positions.

There are TWO possibilities to handle that: either BREAK out of the loop (using break statement) as I wrote you. Or set Sell2[ i ] = False ( set it to FALSE after you used the value to exit position, in order to PREVENT your code from doing exits for all remaining open positions).

FINAL NOTE: Low-level backtester is NOT meant to be used by non-professionals as it REQUIRES solid programming knowledge. There are certain parts of AmiBroker that are not for everyone. If you attempt to use functionality that is complex without having expertise in given area chances are that your code would produce incorrect results.

I know the break statement and many other things.
What I'm looking for is an idea how to connect Buy1 to Sell1, etc.

I can exit the loop with break, that is correct, but if you look at the code carefully, sell signal 2 (=Sell2) exits the buy1 openpos if I throw a break statement in there.

I am still not able to get a unique identifier for my entries and exits. If I just throw a break statement in there, Sell2 exits Buy1, it's supposed to Exit Sell2 only.

So what I'm asking:
What is the way to create a "unique identity" for an entry in low level BT(bo.EnterTrade) to carry forward to bo.ExitTrade.

So in PseudoCode:

if (buy1[i])
 {
      uniqueTradeIdentifier1 = 1
      bo.EnterTrade( i, symbol, True, C [ i ], -2002 );
 }

....and so on

if (sell1[i] && uniqueTradeIdentifier1=True)
  {
      bo.ExitTrade( i, OpenPos.Handle, OpenPos.GetPrice( i, "C" ) );
  }

alternatively:

if (buy1[i])
 {
      
      bo.EnterTrade( i, symbol, True, C [ i ], -2002 , IDENTIFIER=1);
 }

....and so on

if (sell1[i] )
  {
      bo.ExitTrade( i, OpenPos.Handle, OpenPos.GetPrice( i, "C" ) , IDENTIFIER=1);
  }

As per manual:

long EnterTrade ( long Bar, string Symbol, bool bLong, float Price, float PosSize, [optional] variant PosScore, [optional] variant RoundLotSize, [optional] variant MarginDeposit, [optional] variant TickSize, [optional] variant PointValue )

For the backtester object EnterTrade, there is nothing that I can use to clearly identify what caused the trade to open, which I can then refer to in the trade object. Is there a workaround?
I am not able to phrase the question any clearer.
If it's still not clear, please ask.

I can make it work, if I abuse positionScore, like so:

long EnterTrade ( long Bar, string Symbol, bool bLong, float Price, float PosSize, [optional] variant :point_right: :point_right:PosScore =1,2,3 depending on buy1,buy2,bu3 :point_left: :point_left: :point_left:, etc, [optional] variant RoundLotSize, [optional] variant MarginDeposit, [optional] variant TickSize, [optional] variant PointValue )

... but then I lose my postionScores initial intention = to rank trades.

I am still hoping that someone can help with this.

Thank you.

1 Like

Yes, a trade handle is something that you can use. After you call EnterTrade, the trade is added to the OpenPositions list (at the end). You can get last item from OpenPositionList and that would be the trade you have just opened.

As documentation says Porfolio Backtester Interface Reference

Trade object

  • double Handle
    internal handle value that allows to uniquely identify and manage (for example exit or scale in/out) multiple trades open on the same symbol at the same time. It can be passed to ExitTrade / ScaleTrade instead of the symbol.

You can store this object in a variable (say my_trade). Then you can use my_trade.Handle in conjunction with bo.ExitTrade to exit that trade (as sample code has shown).

It is entirely up to you to store the pair of "what signal caused entry" and "trade object" information in your local variables.

Having said that, as far as your earlier codes are considered, you don't even need that, because to exit those three trades using different exit signals, I wrote you already:

Or set Sell2[ i ] = False ( set it to FALSE after you used the value to exit position, in order to PREVENT your code from doing exits for all remaining open positions).

          if( Sell1[i] ) //// sell1 = BarIndex() == 106;
            {
                bo.ExitTrade( i, OpenPos.Handle, OpenPos.GetPrice( i, "C" ) );
                Sell1[ i ] = False; // THIS IS IMPORTANT ! (don't act again on same sell)
           }
         if( Sell2[i] ) //// sell2 = BarIndex() == 106;
            {
                bo.ExitTrade( i, OpenPos.Handle, OpenPos.GetPrice( i, "C" ) );
                Sell2[ i ] = False; // THIS IS IMPORTANT ! (don't act again on same sell)
           }

As for "abusing" trade object to store your own things, you could use TickSize for example or any other field that you don't currently use for other purposes. Trade object has plenty of fields: Porfolio Backtester Interface Reference

3 Likes

From an efficiency standpoint, it would be nice if bo.EnterTrade() returned the trade object or another unique value which could be associated with my own trade identifier via a static variable. That would save the time spent iterating through the open position list every time a trade is opened.

Even better would be the ability to store my own "User Trade ID" string in the Trade object, preferably by specifying that string as a new parameter to the bo.EnterTrade() call. Then a new function similar to bo.FindOpenPos() -- for example, bo.FindOpenPosByID() -- could be provided to return the trade object with a matching ID. Perhaps this could be added to the v7.0 wish list?

4 Likes