PositionScore / Ranking for trades taken “next day at limit”

The issue is that you are generating Buy signals by peeking into the future, i.e. only setting Buy = True when the Low is less than the Limit price. In essence you are saying "On Monday night I will only place limit orders for stocks that are going to decline in price on Tuesday".

With your current code, 17-July-18 you created 4 Buy signals for symbols that had a rank less than 10, and yes, AmiBroker correctly entered only 2 trades before it hit your max of 10 open positions. But how did those 4 Buy signals rank among the 10? They could have been 7,8,9, and 10, which means you would have needed to place orders for at least 8 symbols on the night of 16-July. And since you didn't know that 4 stocks would meet the limit, you really would have had to place orders for all 10, because what if only ONE stock hit the limit, and it was ranked 10th?

As I said previously, if you're willing to use margin to place trades, then this may all be fine. But if not, then you're cheating by looking into the future.

1 Like

Aaargh. Yes. I see what you are saying, and that is what this whole exercise was trying to avoid. Thank you for hammering that in @mradtke , as I was not getting it at first! I believe this could be fixed if I could change the buy statement to something like:

Buy = Buy AND L < BuyLimitPrice AND rank <= (10 - currentOpenPositions);

Though Current Open Positions is listed in the backtest detail, I do not understand quite how to grab that number.

This post from @Tomasz appears to address it using CBT, as you suggest needs to be done.


But I am afraid I am not instituting properly. Below is my current attempt.

Buy = Sell = Short = Cover = 0;

// we run the code on WatchList 0
List = CategoryGetSymbols( categoryWatchlist, 15 );
SetOption("MaxOpenPositions", 10);
SetOption("AllowSameBarExit", True ); 

///////////////////////////////////////@Tomasz code inserted here to try and find current open positions
SetCustomBacktestProc( "" );

if ( Status( "action" ) == actionPortfolio )
    bo = GetBacktesterObject();    //  Get backtester object
    bo.PreProcess();    //  Do pre-processing (always required)

    for ( i = 0; i < BarCount; i++ )  //  Loop through all bars
        bo.ProcessTradeSignals( i );  //  Process trades at bar (always required)

        for ( openpos = bo.GetFirstOpenPos(); openpos; openpos = bo.GetNextOpenPos() )
            // openpos variable now holds Trade object - you can query it

    }    //  End of for loop over bars

    bo.PostProcess();    //  Do post-processing (always required)
///////////////////////////////////////end of current open positions code

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

     for ( n = 0; ( Symbol = StrExtract( List, n ) )  != "";  n++    )
         SetForeign ( symbol );
        isAboveR = C > MA(C, 200);  
		isLowR = C == LLV(C, 5);
         // value used for scoring
         values = IIf(Ref(isAboveR,-1) AND Ref(isLowR,-1), Ref(100-ROC(Close,5),-1), 0);  ///////slight change to @portfoliobuilder's code to include all required conditions and lok at day before
         StaticVarSet (  "values"  +  symbol, values );
         _TRACE( symbol );

     StaticVarGenerateRanks( "rank", "values", 0, 1224 );

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

PositionScore = values;

isAbove = C > MA(C, 200);  
isLow = C == LLV(C, 5);

Buysignal = isAbove AND isLow ;

// buy on the next bar
Buy = Ref( BuySignal, -1);
BuyLimitPrice = Ref( Close, -1);

// now we check if limit was hit for the symbols ranked 
Buy = Buy AND L < BuyLimitPrice AND rank <= (10 - openpos); //my attempt at pulling openpos from the CBT code near the top that looks for the open positions, but this line will not verify,  States that openpos was not initialized
BuyPrice = Min( Open, BuyLimitPrice );

////////////////////Sell next day's open
SellSignal = C == HHV(C, 5);
Sell = Ref(SellSignal, -1) OR exitLastBar; //Sell when security closes at a 5-day high
SellPrice = Open;
////////////////////End Sell next day's open

Sell = C == HHV(C, 5); //Sell when security closes at a 5-day high

//////////////////// removing buys you don't want
intrade = False;
for( i = 0; i < BarCount; i++ )
	if( NOT intrade )
		if( Buy[ i ] ) 
			intrade = True;
			// same bar sell
			if( Sell[ i ] ) intrade = False;
		if( Sell[ i ] ) 
			intrade = False;
			Buy[ i ] = False; // remove buy if exited this bar
//////////////////// end removing buys you don't want

If there is a simple way to throw a Current Open Position number in there that someone knows, then I would appreciate a suggestion. Otherwise, I will look to dig into CBT a bit more next week.


1 Like

@QEdges you have a good start there. For completeness, I would use all three of these lines when you're implementing a CBT:

SetOption("usecustombacktestproc", True);
SetBacktestMode(<your desired mode here>);

Also add some _TRACE() statements to verify that you're actually executing your CBT. Over time, you will find that TRACE() is your best friend for verifying the operation of your AFL.

The next thing you need to do is add some logic before you call bo.ProcessTradeSignals. Specifically, you need to see how many open positions you have, which tells you how many orders you can place on the current bar. That, in turn, tells you how many entry signals to keep so that you can disable all the remaining ones. One way to do that is to simply set the position size to 0 for any signals that you want to ignore.

1 Like

@QEdges the CBT is really best left to advanced AmiBroker users.

With that caveat out of the way I can take a stab as a non-advanced user with some ideas for you to investigate. In your CBT you loop through the open positions but did not "do anything" in that loop. I think one method of obtaining the number of current open positions may be like this,

// Count number of open positions
OpenPos = 0;

for( trade = bo.getFirstOpenPos(); trade; trade = bo.getNextOpenPos() )

How to use that information is I believe dependent on the rest of your code. Setting a StaticVariable is one possible way that I can think of to access this info later,

StaticVarSet(myStrategy + "OpenPos", OpenPos);

I think another method to access the number of positions is via the backtester object and the property of GetOpenPosQty

In the CBT something like this may work, as you loop bar-by-bar,

	PosQty = 0;

	for (i = 0; i < BarCount; i++) // Loop through all bars
		PosQty = bo.GetOpenPosQty();
// and go on with your code from here

but please take all of the above with a large grain of salt as I am just in the process of learning how to use the CBT and far from an expert.

Good luck!

1 Like

Hi all

Just saw this thread and has helped me confirm my own back test using limit orders is a bit like having a crystal ball! did anyone ever manage to get a CBT working for this?

As is usually the case with AmiBroker, there are multiple ways to solve this problem. I prefer to do it by sending all Setups to the CBT as if they were entries, which allows me to count how many "orders" I'm placing. What have you tried, and what is not working for you?

Looking at the code again at the bottom of the web page, "Handling limit orders in the backtester", the Buy is looking at yesterday's BuySignal but is the rank being calculated using the close of the same day of the buy at a limit on the open? Should a line be added such as

rank= Ref( rank, -1);

or is it OK the way it is?
Web page I'm referring to:


@Marcel: I believe that both rank (used in the Buy assignment) and values (used for PositionScore) should be referencing the previous bar's data, i.e. they should be determined at the same time as BuySignal. However, you probably should get confirmation from @Tomasz since it's his code. In the meantime, you could verify the values with an Exploration.


I ran a backtest and exploration on the code for limit orders with multiple positions. I ran the code against the default watchlist number 0 which for Norgate, corresponds to "Dow Jones Industrial Average Current & Past" This is a copy of the exploration for the most recent trade that shows up on February 14 2019:

I can see in the exploration that it is looking at the rank calculated for Feb 14 which is 3. The previous day's rank is 34 so it couldn't be looking at that to qualify the trade. The Feb 14 rank is calculated from the RSI which looks at the same day close value. As far as I understand, this would be a future leak. If anyone else is interested to look, I've copied the code below which is identical to the one found on this page, except that I've added an exploration on the end:

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

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

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

        // value used for scoring
        values = 100 - RSI();
        StaticVarSet( "values"  +  symbol, values );
        _TRACE( symbol );

    StaticVarGenerateRanks( "rank", "values", 0, 1224 );

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

PositionScore = values;

BuySignal = Cross( Close, MA( Close, 100 ) );

// buy on the next bar
Buy = Ref( BuySignal, -1 );
BuyLimitPrice = Ref( Close, -1 ) * 0.999;

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

// sample exit rules - 5 - bar stop
Sell = 0;
ApplyStop( stopTypeNBar, stopModeBars, 5, 1 );

Filter = 1;
AddColumn( Ref( Close, -1 ) * 0.999, "EntryLimit", 1.2 );
AddColumn( Open, "Open", 1.2 );
AddColumn( Low, "Low", 1.2 );
AddColumn( Close, "Close", 1.2 );
AddColumn( RSI(), "RSI", 1.2 );
AddColumn( values, "values", 1.2 );
AddColumn( rank, "rank", 1.0 );
AddColumn( ref( RSI(), -1 ), "YdayRSI", 1.2 );
AddColumn( Ref( values, -1 ), "Ydayvalues", 1.2 );
AddColumn( Ref( rank, -1 ), "Ydayrank", 1.0 );
AddColumn( Ref( BuySignal, -1 ), "YdayBuySignal", 1.0 );
AddColumn( IIF( Buy, 'B', ' ' ), "Buy", formatChar, colorDefault, IIf( Buy, colorLime, colorDefault ) );
AddColumn( IIF( Sell, 'S', ' ' ), "Sell", formatChar, colorDefault, IIf( Sell, colorRed, colorDefault ) );
1 Like

I think that the story does not finish here @mradtke
I use your example:

  • You want to allow a max of 10 open positions, and you are not placing trades using margin
  • At Monday's close, you have 8 open positions
  • Monday night you do your ranking, and there are fives stocks that meet the Setup criteria, and therefore receive ranks 1-5

It's right, you can use margin and place all 5 limit orders on the market. However In this case you incur in the risk of buy all 5 stocks and in this case the strategy became a totally different strategy respect of what Amibroker is backtesting.
Alternately you can place the orders in real time when the stock price fell below the limit, but as a i said the story does not finish here.
In fact , in our example, imagine that monday night we find 3 stocks that meet the setup criteria: stock1 (rank1) stock2 (rank2) stock3 (rank3). Remeber that we already have 8 open positions.
You decide to follow live the market before place your orders.
At 10am none of the 3 stocks met their price limit.
At 11am stock3 fell below the price limit. What you have to do in this case? You decide to buy stock3.
At 12pm also stock2 fell below the price limit and you are forced to buy this stocks because it can go higher not giving you a second chance.
Now all your 10 slots are filled.
Everything is going to be ok but at 15pm also stock1 fell below its price limit.
You don't buy stock1 because all the 10 slots are filled.
The problem is that amibroker , at the end of that day, will buy stock1 and stock2 (leaving unfilled stock3) because they have a better rank.

All that said, is correct to make the following statement? :
The code's structure of @QEdges (the last one) is completly impossibile to execute in reality.

In other word there is no way to be allined with what Amibroker do.

It follows that Amibroker (in this case) is testing something that is not realistic because there is no way to be deterministic in the live execution.

We should rethink the code structure so that Amibroker can do a realistic backtest.


It's easy to do a realistic backtest with the CBT. If you only have two open slots on Tuesday, then you only consider the first two Setups, i.e. the Buy logic without a limit price check. The Setups may or may not breach the limit price and result in an entry.

@mradtke would you be so kind to give me an example to study? In the contest of our example how would you do a perfectly realistic backtest?
I would appreciate a lot.

I would be interested to know how to deal with the issue @Heisenberg postulated.

You said that we can deal with it using CBT.
Here is what I think:

  1. For EOD strategies like this which rely on limit orders, I should use 1min data or even better tick by tick data so the backtester simulates real world equity requirements and order sequence correctly.

  2. Using a CBT, I can send a Buy order without the limit price check but since there is no way to know which of the stock breached the limit price first, I cannot rely on it to decide which stock to take position in.

  3. In absence of 1min/tick data, I don't use percentage based position sizing currently. I start with a huge initial equity and use static absolute value based positions so that I have a better idea of strategy usability. But this method has its disadvantages like for one, I cannot see the compounded equity and have to rely on Win-rates to evaluate strategy.

As I see it, only 1st point is the most reliable way to simulate such conditions.
Your suggestions are greatly appreciated.

Your backtest should reflect the process you intend to use for live trading. In the example that I described previously, the assumption is that if you only have capacity to take two trades on Tuesday, then you will place AT MOST two limit orders before the market opens, no matter how many Setups you have on Monday night. The two orders that you place will correspond to the two entry signals with the highest position scores.

The sequence in which orders are filled is only relevant if you plan to place MORE THAN two orders before Tuesday's open, or if you intend to monitor the market in real time and place your orders as the limit price is approached or breached.

1 Like

Suppose I have a strategy which calculates RSI(2) values after the close and then buys the next day on Open - x% limit. Only a maximum of 5 positions can be taken any day. And all positions are closed EOD close.
I calculate the RSI(2) values on Monday night and found 7 stocks which have a value lesser than the cutoff value of 10. These stocks are then shortlisted and ranked 1-7 accordingly.
I then send it to CBT which takes the top 5 ranks(stocks 1-5) and takes position in it.

In real world, suppose stock 7 breached the limit price earliest at 10AM followed by stock 6 at 10:15AM. On a real trading day, my positions are in the following stocks: stock-7, stock-6, stock-1, stock-2, stock-3. Stock 4 & 5 also breach the limit price but since I am already maxed out, I have to pass on them.

However in the case of CBT , it would have taken positions in stock1-5. This is a kind of lookahead bias which only takes position in the best ranked stocks. I don't see a way where we can simulate real world trading scenario UNLESS and UNTIL we have data of lower granularlity(1min/tick-by-tick).
In that case, we can take signals on EOD data but run the strategy on lower timeframe data to simulate real exchange.

As I said, your AFL needs to reflect your trading rules. If you only placed orders for stocks 1-5, then you would not have an issue running a backtest with EOD data. If you are going to place orders for stocks 1-7 as described in your most recent post, then you are correct that you will need intraday data to determine the sequence in which your orders actually get filled.

1 Like

On the Amibroker Knowledge Base the bottom code on this page:
It deals with limit orders for multiple positions. In this example, all of the stocks in the watchlist are ranked using the RSI and only the top 3 can be candidates for a limit order. Then the possible setups are calculated by a cross of the close over the 100 bar moving average and if none of those setups are ranked in the top 3, there won't be any limit orders. Shouldn't it be that you find all the setups first, and then only rank the setups for possible limit orders instead of ranking all the stocks? Would a proper CBT for limit orders would only rank the setups for limit orders and not rank all the stocks?

Sorry if I ask a lot of questions about limit orders with multiple positions, it can be somewhat complicated. However I believe that limit orders can give you an advantage for a lot of trading systems. I think it's important for us to understand how they work with Amibroker and that it warrants discussion.

One challenge with using limit entries is that you need to consider how many orders you can place in live trading. Let's assume you have a 10k account with 5 equally sized positions of 2k each, and no margin available. If you have four open positions coming into today, then in live trading you would only be able to place one order before running out of buying power.

In AmiBroker terms, the number of "orders" you place corresponds to how far down the ranked list of Setups you go. In the example above, you should only consider the Setup with the highest position score. If you have 2 open positions today (and therefore 3 slots available), then you should only consider the Setups with the 3 highest position scores. It should be noted that when testing a portfolio, there is no way to know how many slots are available in the Phase 1 code. This can only be known in Phase 2 (CBT).

The example that you referenced is not attempting to solve the challenge described above. Instead, it is simply reducing the number of potential setups to three, and as such there is nothing wrong with the code. You could proceed as you described by only ranking the valid setups instead of ranking all stocks, but that just changes the logic of the strategy slightly without solving the "number of orders" problem.


this post has helped me a lot. I started learning custom backtest to solve this problem and had some success. just thought i would post this here for sharing as a starting point. this replaces the ranking code in the original post. its way faster than staticvargenerateranks. it ranks, checks limit order fill, and only looks at the first (max position - # of open position) rankings.

SetOption("UseCustomBacktestProc", True );

if( Status("action")== actionPortfolio ) 
    // retrieve the interface to portfolio backtester 
     bo = GetBacktesterObject(); 
     bo.PreProcess();    //  Do pre-processing (always required)

	for (i = 0; i < BarCount; i++) // Loop through all bars
		PosQty = bo.GetOpenPosQty();
		_TRACE("Day ------ " + i);
		_TRACE("Open Position: " + PosQty);
		for(sig=bo.Getfirstsignal(i) ; sig ; sig=bo.GetNextSignal(i))
			LowArray=Foreign(sig.Symbol,"Low"); //grab the low to check limit order fill
				if(count>9-PosQty or LowArray[i]>sig.price) 
		bo.ProcessTradeSignals( i );

    bo.PostProcess();    //  Do post-processing (always required)

Also make sure to turn off priceboundchecking so the buyprice will actually be sent to CBT un-altered for checking.