CBT, bo.ScaleOut antecipates one day

As you can see below, the detail log shows a Scale-Out at 19/11/2024
and in the log windows it says that the bar where it did the bo.scaleout was 20/11/2024

I can not find what is making my bo.scaleout not being executed in at 20/11/2024.

Anyone ?

Using AB 64 7.00, with EOD database. In Analysis/Backtest I used current having SPY set and last 300 recent bars , but the problem arises with any symbol


SetBacktestMode( backtestRegularRaw );
SetTradeDelays(0, 0, 0, 0);
SetOption("FuturesMode",false);
SetOption("AllowPositionShrinking",false);//
SetOption("InitialEquity",1000000);
SetOption("CommissionMode",1); // 1- Percent of trade
Comission = 0.037;
SetOption("CommissionAmount",0.037); //0.37%
SetOption("maxopenpositions",1);//
SetOption("MinShares",1);//

PercentInitial = 4;

bir = Status("barinrange");
Buy = Cum(bir) == 1;
Buy = Ref(Buy, -1);
BuyPrice = Close;

StaticVarSet("xPrice" + Name(), Close);

Sell = Short = Cover = 0;
PositionSize = SetPositionSize(100, spsPercentOfEquity); // will be adjusted in CBT

//CBT code begins here

SetCustomBacktestProc( "" );

debugPrefix = "#AB: ";
dbMaxLines = 10; // debug log lines

if( Status( "action" ) == actionPortfolio )
{
	DateNumberArray = DateTimeConvert(2,DateNum());
	Withdraw = Day() == 20;
	TotalWithdraw = 0;
	Lines = 0;
	
    bo = GetBacktesterObject();
    bo.PreProcess();
    
	yearChange = Year() != Ref(Year(), -1);
	MonthWithdrawAmmount = bo.InitialEquity * (PercentInitial / 100) / 12;

	xSymbol = "";
   
	for( i = 0; i < BarCount; i++ )
	{
		if( xSymbol == "" )
			for( openpos = bo.GetFirstOpenPos(); openpos; openpos = bo.GetNextOpenPos() )
			{
				xSymbol = openpos.Symbol;
				if( dbMaxLines > 0 )
				_TRACE(debugPrefix + DateTimeToStr( DateNumberArray [0] ) + ": openpos.Symbol : " + xSymbol);
				xPrice_a = StaticVarGet("xPrice" + xSymbol);
				break;
			}
		if( xSymbol == "" )
			for( trade = bo.GetFirstTrade(); trade; trade = bo.GetNextTrade() )
			{
				xSymbol = trade.Symbol;
				if( dbMaxLines > 0 )
				_TRACE(debugPrefix + DateTimeToStr( DateNumberArray [0] ) + ": trade.Symbol : " + xSymbol);
				xPrice_a = StaticVarGet("xPrice" + xSymbol);
				break;
			}
			
		if( yearChange[i] )
		{
			CurEquity = bo.Equity;
			MonthWithdrawAmmount =  CurEquity * (PercentInitial / 100) / 12;
		}
		
		if( Withdraw[i] AND xSymbol != "")
		{
			CurEquity = bo.Equity;
			xPrice = xPrice_a[i];				
			
			PosSize = MonthWithdrawAmmount + (MonthWithdrawAmmount * (Comission + 0.003));
			
			qty = ceil(SafeDivide(PosSize, xPrice));
			PosSize = qty * xPrice;
			
			if( CurEquity > 0 AND PosSize < CurEquity )
			{

				//long ScaleTrade(long Bar, string Symbol, bool bIncrease, float Price, float PosSize, [optional] variant Deposit)
				sc_res = bo.ScaleTrade(i, xSymbol, false, xPrice, PosSize);
				if( sc_res != 1 )
					_TRACE(debugPrefix + DateTimeToStr( DateNumberArray [i] ) + ": ##### sc_res : " + sc_res);

				Lines++;
				if( Lines < dbMaxLines )
				_TRACE(debugPrefix + DateTimeToStr( DateNumberArray [i] ) 
					+ ": MonthWithdrawAmmount : " + MonthWithdrawAmmount
					+ ": bo.Cash : " + bo.Cash
					+ ": CurEquity : " + CurEquity
					+ ": xSymbol : " + xSymbol
					+ ": xPrice : " + xPrice
					+ ": qty : " + qty
					+ ": PosSize : " + PosSize);
					
				bo.UpdateStats(i, 1); // update regular stats
			}
		
		} //if( Withdraw[i] )

		//bo.HandleStops( i );
		bo.ProcessTradeSignals( i ); 
		// Update equity after withdrawal
		bo.UpdateStats(i, 2); // update regular stats
		
	} //for

	bo.PostProcess();

} //if( xSymbol != "" )

It looks like your CBT logic will only print those _TRACE lines and scale out when Withdraw[i] is true, which only happens on the 20th of the month. The other scale-out that occurs as part of the bo.ProcessTradeSignals() call does not have any associated output, so you wouldn’t see it in your log. Or am I missing something?

1 Like

Well, as you can see the only way the backtest can know about a scale-out is from CBT
nowhere eslse there is scale-out command
And in CBT the only place there is a scale-out command has also a _TRACE

In detail log the scale out is done day 19

Oh, I see now that there are no actual scaleout signals generated in Phase 1. The detailed log can get a bit screwy with a CBT… are you sure that the date is being printed before the output, and not after it? In other words, maybe (probably?) your scale-out trace output really belongs on the 20th.

1 Like

all information that appears in detail log is correct,
the symbol is correct, the price used to scaleout belongs to day 20, the only thing wrong is that in detail log it happens day 19

Note that ProcessTradeSignals calls UpdateStats internally. Since you are calling it yourself with timeinbar=2 that should occur only once, it may lead to unpredictable behaviour.

1 Like

Thank you for your attention and pointing that out, unfortunatelly after commenting out line with UpdateSTats (2) the result is stil the same


SetBacktestMode( backtestRegularRaw );
SetTradeDelays(0, 0, 0, 0);
SetOption("FuturesMode",false);
SetOption("AllowPositionShrinking",false);//
SetOption("InitialEquity",1000000);
SetOption("CommissionMode",1); // 1- Percent of trade
Comission = 0.037;
SetOption("CommissionAmount",0.037); //0.37%
SetOption("maxopenpositions",1);//
SetOption("MinShares",1);//

PercentInitial = 4;

bir = Status("barinrange");
Buy = Cum(bir) == 1;
Buy = Ref(Buy, -1);
BuyPrice = Close;

StaticVarSet("xPrice" + Name(), Close);

Sell = Short = Cover = 0;
PositionSize = SetPositionSize(100, spsPercentOfEquity); // will be adjusted in CBT

//CBT code begins here

SetCustomBacktestProc( "" );

debugPrefix = "#AB: ";
dbMaxLines = 10; // debug log lines

if( Status( "action" ) == actionPortfolio )
{
	DateNumberArray = DateTimeConvert(2,DateNum());
	Withdraw = Day() == 20;
	TotalWithdraw = 0;
	Lines = 0;
	
    bo = GetBacktesterObject();
    bo.PreProcess();
    
	yearChange = Year() != Ref(Year(), -1);
	MonthWithdrawAmmount = bo.InitialEquity * (PercentInitial / 100) / 12;

	xSymbol = "";
   
	for( i = 0; i < BarCount; i++ )
	{
		if( xSymbol == "" )
			for( openpos = bo.GetFirstOpenPos(); openpos; openpos = bo.GetNextOpenPos() )
			{
				xSymbol = openpos.Symbol;
				if( dbMaxLines > 0 )
				_TRACE(debugPrefix + DateTimeToStr( DateNumberArray [0] ) + ": openpos.Symbol : " + xSymbol);
				xPrice_a = StaticVarGet("xPrice" + xSymbol);
				break;
			}
		if( xSymbol == "" )
			for( trade = bo.GetFirstTrade(); trade; trade = bo.GetNextTrade() )
			{
				xSymbol = trade.Symbol;
				if( dbMaxLines > 0 )
				_TRACE(debugPrefix + DateTimeToStr( DateNumberArray [0] ) + ": trade.Symbol : " + xSymbol);
				xPrice_a = StaticVarGet("xPrice" + xSymbol);
				break;
			}
			
		if( yearChange[i] )
		{
			CurEquity = bo.Equity;
			MonthWithdrawAmmount =  CurEquity * (PercentInitial / 100) / 12;
		}
		
		if( Withdraw[i] AND xSymbol != "")
		{
			CurEquity = bo.Equity;
			xPrice = xPrice_a[i];				
			
			PosSize = MonthWithdrawAmmount + (MonthWithdrawAmmount * (Comission + 0.003));
			
			qty = ceil(SafeDivide(PosSize, xPrice));
			PosSize = qty * xPrice;
			
			if( CurEquity > 0 AND PosSize < CurEquity )
			{

				//long ScaleTrade(long Bar, string Symbol, bool bIncrease, float Price, float PosSize, [optional] variant Deposit)
				sc_res = bo.ScaleTrade(i, xSymbol, false, xPrice, PosSize);
				if( sc_res != 1 )
					_TRACE(debugPrefix + DateTimeToStr( DateNumberArray [i] ) + ": ##### sc_res : " + sc_res);

				Lines++;
				if( Lines < dbMaxLines )
				_TRACE(debugPrefix + DateTimeToStr( DateNumberArray [i] ) 
					+ ": MonthWithdrawAmmount : " + MonthWithdrawAmmount
					+ ": bo.Cash : " + bo.Cash
					+ ": CurEquity : " + CurEquity
					+ ": xSymbol : " + xSymbol
					+ ": xPrice : " + xPrice
					+ ": qty : " + qty
					+ ": PosSize : " + PosSize);
					
				bo.UpdateStats(i, 1); // update regular stats
			}
		
		} //if( Withdraw[i] )

		//bo.HandleStops( i );
		bo.ProcessTradeSignals( i ); 
		// Update equity after withdrawal
		//bo.UpdateStats(i, 2); // update regular stats
		
	} //for

	bo.PostProcess();

} //if( xSymbol != "" )

You should keep bo.ProcessTradeSignals(i) at the Top of the Loop.

1 Like

Thank you. In fact moving bo.ProcessTradeSignals(i) to the begining of the loop did the trick.

I thought it had to be executed as the last command of each bar

I thought it had to be executed as the last command of each bar

Exactly. This is how it is typically used in the documentation examples and even in the predefined "snippets".

I would really appreciate it if @Tomasz could clarify how this different behavior arises in your specific case.

Proper sample codes are included in the Knowledge Base, the one with scaling is here:

2 Likes

Again thank you very much for your time and patience.

Now scale out is on day 20,
but the first bo.scale is not respecting PosSize parameter.
It scaleout less money then I expected.

I pass 3490.13 and got 2941.64 although SPY have a lot more

The second scaleout seams ok and the others too.

Any idea ?

// Parameters - adjust these

PercentInitial = 4;

SetBacktestMode( backtestRegularRaw );
SetTradeDelays(0, 0, 0, 0);
SetOption("FuturesMode",false);
SetOption("AllowPositionShrinking",false);//
SetOption("InitialEquity",1000000);
SetOption("CommissionMode",1); // 1- Percent of trade
Comission = 0.037;
SetOption("CommissionAmount",0.037); //0.37%
SetOption("maxopenpositions",1);//
SetOption("MinShares",1);//

bir = Status("barinrange");
Buy = Cum(bir) == 1;
Buy = Ref(Buy, -1); // buy 100% one time only
BuyPrice = Close;

Sell = Short = Cover = 0;
PositionSize = SetPositionSize(100, spsPercentOfEquity); // will be adjusted in CBT

SetCustomBacktestProc( "" );

debugPrefix = "#AB: ";
dbMaxLines = 100;

if( Status( "action" ) == actionPortfolio )
{
	DateNumberArray = DateTimeConvert(2,DateNum());
	
	Withdraw = Day() == 20; 
	TotalWithdraw = 0;
	Lines = 0;
	
    bo = GetBacktesterObject();
    bo.PreProcess();
    
	yearChange = Year() != Ref(Year(), -1);
	MonthWithDraw = bo.InitialEquity * (PercentInitial / 100) / 12;

	for( i = 0; i < BarCount; i++ )
	{
		bo.ProcessTradeSignals( i ); 

		CurEquity = bo.Equity;
		
		if( yearChange[i] )
		{
			MonthWithDraw =  CurEquity * (PercentInitial / 100) / 12;

			if( dbMaxLines > 0 )
				_TRACE(debugPrefix + DateTimeToStr( DateNumberArray [i] ) + ": YEAR : "
					+ ": TotalWithdraw : " + TotalWithdraw
					+ ": CurEquity : " + CurEquity
					+ ": MonthWithDraw : " + MonthWithDraw);
		}
		
		if( Withdraw[i] )
		{
			for( pos = bo.GetFirstOpenPos(); pos; pos = bo.GetNextOpenPos() )
			{
				posval = pos.GetPositionValue();
				xPrice = pos.GetPrice( i, "C" );
    			xSymbol = pos.Symbol;
				
				PosSize = MonthWithDraw + (MonthWithDraw * (Comission + 0.001));
				
				qty = ceil(SafeDivide(PosSize, xPrice));
				PosSize = qty * xPrice;
				
				if( PosSize >= posval )
				{
					//Lines++;
					_TRACE(debugPrefix + DateTimeToStr( DateNumberArray [i] ) + ": PosSize >= posval : "
						+ ": qty : " + qty
						+ ": PosSize : " + PosSize
						+ ": bo.Cash : " + bo.Cash
						+ ": CurEquity : " + CurEquity);
				}
				else
				{
					//long ScaleTrade(long Bar, string Symbol, bool bIncrease, float Price, float PosSize, [optional] variant Deposit)

					sc_res = bo.ScaleTrade(i, xSymbol, false, xPrice, PosSize);

					if( sc_res != 1 ) _TRACE(debugPrefix + DateTimeToStr( DateNumberArray [i] ) + ": ScaleTrade : " + ": sc_res : " + sc_res);

					if( Lines < dbMaxLines )
					{
						Lines++;
						_TRACE( debugPrefix + DateTimeToStr( DateNumberArray [i] )  + ": ScaleOut : "
							+ ": MonthWithDraw : " + MonthWithDraw
							+ ": bo.Cash : " + bo.Cash
							+ ": CurEquity : " + CurEquity
							+ ": xSymbol : " + xSymbol
							+ ": xPrice : " + xPrice
							+ ": qty : " + qty
							+ ": PosSize : " + PosSize);
					}
					
					barsSinceWD = 0;
					
					str = debugPrefix + DateTimeToStr( DateNumberArray [i] )  + "\t: ScaleOut : "
							+ ": MonthWithDraw : " + MonthWithDraw
							+ ": bo.Cash : " + bo.Cash
							+ ": CurEquity : " + CurEquity
							+ ": xSymbol : " + xSymbol
							+ ": xPrice : " + xPrice
							+ ": qty : " + qty
							+ ": PosSize : " + PosSize;
					bo.RawTextOutput( str );

				}
				break;

			} // for( pos = bo.GetFirstOpenPos()
		} //if( Withdraw[i] )
	} //for Barcount

	bo.PostProcess();

	_TRACE(debugPrefix + ": TotalWithdraw : " + TotalWithdraw);

} //if( Status( "action" ) == actionPortfolio )

If your Round Lot Size is set to 1, then AmiBroker will scale out as many whole shares as possible without exceeding your scaleout value. One more share at $589 would exceed your requested value of $3490.

1 Like

Perfect you nail it.
Thank you everybody

Your code is also working correctly.
The issue is only a misunderstanding caused by the order in which logs are printed.

The scale-out shown in your detailed log does happen on 20-11-2024, exactly as expected.

What happens in your code is this:

  • The position is scaled out first.
  • Then bo.ProcessTradeSignals(i) is called
  • Because of this order, the detailed log prints messages out of chronological sequence.

This creates the impression that the scale-out happened on a different date, even though it did not.

You can clearly verify this by printing the datetime directly in the detailed log before scale-out :

bo.RawTextOutput( "SCALE-OUT DateTime: " + DateTimeToStr( DateNumberArray [i] ) );
sc_res = bo.ScaleTrade(i, xSymbol, false, xPrice, PosSize);

When you do this, you will observe the following:

  • Logs initially show 19-11-2024
  • Then the position is scaled out on 20-11-2024
  • After that, bo.ProcessTradeSignals(i) runs and prints 20-11-2024

So the confusion comes from when the log lines are printed, not from when the trade action actually occurs.

3 Likes

Exactly. As I wrote low-level is tricky because you can place your code anywhere and do ANYTHING and be surprised.

2 Likes

Apreciatte the attention, one more question about bo.ScaleTrade

Base currency of my database is USD
and I want to scaleout a Symbol that uses another currency, let say EUR.

long ScaleTrade(long Bar, string Symbol, bool bIncrease, float Price, float PosSize, [optional] variant Deposit)

parameters price and possize, should be in USD or an EURO ?

Results are not what I expected , so to find my error I would like to clarify the point.

Thnaks again