Buy at low - a future leak?

The code:

Buy3 = L < O*0.975;
Buy = Buy1 AND Buy2 AND Buy3;
BuyPrice = O*0.975;

The results below suggest a future leak, but how? If a trader enters such a buy order basing on the Open price at the begining of the day, it will only get filled if the price reaches that low, wouldn't it? Doesn't the Buy3 condition simulate exactly that?

The results do not account for slippage, ofc, but thats a different matter.

Thanks in advance.

Yes, using High and Low prices without one bar trading delay is a future leak.

No it is not different matter. Typically such systems trade very often (your system generated 50K trades and earn only minimum (your system 0.63%), once you account for slippage AND commission, system starts losing money. Always setup commissions so high that they account for slippage and brokerage commission.

Sorry, @Tomasz , I do not understand about the future leak here.

Why isn't my code an equivalent to the situation where the open price is, say, $10.00 at 9:30:00 and the user logs in to the Trader Workstation at 9:30:01 and enters a LMT Buy order with the limit set at 9.75?

I meant to say that slippage is a different matter from a possible future leak. I am completely aware that the result might be very much different in the reality because of the slippage. Are you suggesting that there is no future leak and the results are unrealistic due to the slippage?

If you want to code a strategy that enters on a limit order and which can actually be traded live (i.e. does not assume unlimited capital or margin), you will most likely need to utilize a CBT.

@John_Dolittle Given the size of your trading universe, your position sizing, and the amount of capita//margin available, could the hypothetical trader place orders for every symbol in the universe at 9:30:01? Because that's what you're simulating.

1 Like

Future leak here exists but it is subtle
Your code assumes that you are able place those limit orders on ALL symbols, which in practice is not realistic due to limited funds. With limited funds Your code behaves as it knew the low and placed the orders only on those symbols that you know will hit the limit. This is future leak.

General rule is don't use current bar High or Low without delay

i think it also depends also on how buy1 and buy2 are calculated. If buy1 and buy2 uses only the open price of current bar, you are fine. but if they are calculated using close or high of the current bar, then it introduces future leak.

@mradtke No, I am not simulating buying all stocks on all possible occasions. Buy1 and Buy2 impose additional conditions. I ran the backtest over SP500 stocks' daily prices from 01.10.2007 to 20.12.2022. The amount of trades seems large but it actually is not, considering the time frame. The position size is 5% of the equity.

@Tomasz Thanks for taking time to answer. You certainly have a point. I believe that I do not have enough signals to reach the limits of concurrent orders imposed by the IB, but I will have to check this. Will post again after that.

@AmiUser2030 Yes, I am certain that Buy1 and Buy2 use only Ref(x, -1) for all prices and indicators.

I really appreciate all the answers so far. I am quite confused about the results.

Your position sizing dictates that you cannot have more than 20 open positions. Consider a situation where you already have 12 open trades and you're not using leverage. Most brokers will not allow you to place more than 8 additional limit orders, i.e. they want to guarantee that even if all limit orders are filled, you will not exceed your available cash.

Even if IB will let you place more than 8 orders, your backtest will have an issue. That's because if you have 15 setups today (i.e. Buy1 and Buy2 are true, meaning there are 15 orders that you would be sending to your broker) and 10 of those 15 breach the limit price, then the backtest will select the 8 orders that have the highest absolute position score. In live trading, you would have taken the first 8 orders that breached the limit price. Depending on what you're using for a position score (which is another place that future leaks can creep in), this may have a significant impact on your results.

@mradtke I am not using position scoring, but you are right, of course. If there are, say, 30 signals responding to Buy1 and Buy2 and 25 signals to Buy1 and Buy2 and Buy3, then the signals to enter will be selected from those 25.

@mradtke , @Tomasz

I put the starting amount to 100 000 and the position size to 0.1 percent of the equity. The inclusion of the Buy3 condition still significantly improves the results although the overwhelming part of the portfolio is cash at any bar, there could be 1000 simultaneous open positions and the effect described above should therefore have no influence.

Without Buy3:

With Buy3:

OK, so you've proven to yourself that the limit order is actually adding value. That's good!

You said that your first set of results was "Without Buy3". I assume that you changed your BuyPrice to be the Open price for that test. If you left this assignment:

BuyPrice = O*0.975;

Then on days that the limit price was not breached, AmiBroker's default behavior is to force the entry price to be the Low, which is obviously not what you want.

If the entry price is valid, then I would suggest looking at your Buy1 and Buy2 logic carefully. A Risk Adjusted Return of 272% definitely feels like it's wandering into the land of TGTBT: Too Good To Be True.

Also, as @Tomasz noted, your transaction costs seem quite low: less than $0.10 per round trip.

1 Like

Yes, I left that statement in once by accident and the results went instantly stellar. I am certain that it is removed from the formula "without Buy3".

I have no idea how this 272% is calculated. When I put the Position Size back to 5% and the starting amount to 10 000, then the results of "without Buy3" are good but not stellar:

As for the transaction costs, the IB charge max 0.005 USD per share: https://www.interactivebrokers.ie/en/pricing/commissions-home.php . I am unsure about the slippage, however. For a LMT order, the result of the slippage could only be that less shares are bought than requested, isn't that so? This will heavily depend on volume and is difficult to simulate besides having the entry bar volume limit (currently 2%).

It seems to be that this method is plausible to simulate a LMT order below the Open price if all buy signals are processed to full extent.

RAR is simply CAR divided by Exposure.

You mentioned that you're "not using" Position Scoring, but if you don't assign the PositionScore variable then AmiBroker will order your entry signals for you. Usually it's alphabetically by ticker symbol, but I believe there are circumstances when it uses a different ordering. In any event, you should specify the order yourself so that on days when there are more entry signals than available slots you get the trades you want.

It seems to be that this method is plausible to simulate a LMT order below the Open price if all buy signals are processed to full extent.

Yes, with that caveat it's valid. I would still be very cautious. I would also double-check your "not stellar" 44% returns trading S&P 500 stocks. I assume you are using Norgate Data or another source that allows you to identify historical index constituents?

if you don't assign the PositionScore variable then AmiBroker will order your entry signals for you. Usually it's alphabetically by ticker symbol, but I believe there are circumstances when it uses a different ordering.

Many thanks, I did not know that.

I assume you are using Norgate Data or another source that allows you to identify historical index constituents?

I am using the AlphaVantage price & volume data and this list: GitHub - fja05680/sp500: Current and Historical Lists of S&P 500 components since 1996 .

About the "non-stellar" suspiciously good results, the drawdown for this particular setup is -32.16%. However, I got it to around 13% with an advanced version that uses the performance of the 1st version as input and the VIX readings as a market filter:

I have not tried the advanced version with the LMT order below the Open price yet.

I will write a Custom Backtester simulating the LMT below Open next, compare the results and post it here.

@mradtke For some reason, the code below gets only 767 trades. At the present time of the night, I am quite unable to figure out, why. Do you see anything obvious?

SetOption("UseCustomBacktestProc", True);   
  
if (Status("action") == actionPortfolio) { 
 
	bo = GetBacktesterObject(); 
	bo.PreProcess();		 
	 
	for (bar=0; bar < BarCount; bar++) {			 
		 
		if (bar > 0) {					 		
			for (sig=bo.GetFirstSignal(bar); sig; sig=bo.GetNextSignal(bar)) { 
				if (sig.IsEntry()) {	
					sig.RoundLotSize = 1;			 				
					_Open = StaticVarGet(sig.Symbol + "_O" );
					_Low = StaticVarGet(sig.Symbol + "_L" );
					if (_Low[bar] < _Open[bar]*0.975) {
						sig.Price = _Open[bar]*0.975;						
					} else {
						sig.PosSize = 0;
					} 
				} 
			}			
		} 
			 
		bo.ProcessTradeSignals(bar);			 
	}	 
	 	
	bo.PostProcess();	 	 
		 
}
 
_vix = Ref(Foreign("~~~VIX_History", "~~~VIX_History"), -1); 
_av_profit = Ref(Foreign("~~~Av_profits_LONG","X"), -1);  
 
if (StrMid(Name(), 0, 3) == "~~~") { 
	Buy = 0; 
	Sell = 0; 
} else { 
  
	SetBacktestMode(backtestRegular);			 
	StaticVarSet(Name() + "_O", O);
	StaticVarSet(Name() + "_L", L);
	 
	_lastclose = Ref(C, -1);
	etc., etc, etc.

Nothing jumps out at me. Add some _TRACE statements and also compare the trade lists for the CBT and non-CBT versions of your code.

This is a WRONG way to cancel a signal: sig.PosSize = 0;

This works:

SetOption("UseCustomBacktestProc", True);   
  
if (Status("action") == actionPortfolio) { 
 
	bo = GetBacktesterObject(); 
	bo.PreProcess();		 
	 
	for (bar=0; bar < BarCount; bar++) {			 
		 
		if (bar > 0) {					 		
			for (sig=bo.GetFirstSignal(bar); sig; sig=bo.GetNextSignal(bar)) { 
				if (sig.IsEntry()) {	
					sig.RoundLotSize = 1;			 				
					_Open = StaticVarGet(sig.Symbol + "_O");					
					_Low = StaticVarGet(sig.Symbol + "_L");					
					if (_Low[bar] < _Open[bar]*0.975) {
						_TRACE("YES " + sig.Symbol + "_O " + NumToStr(_Open[bar]) + " " + NumToStr(_Low[bar]) + " " + NumToStr(bar));
						sig.Price = _Open[bar]*0.975;						
					} else {
						_TRACE("NO " + sig.Symbol + "_O " + NumToStr(_Open[bar]) + " " + NumToStr(_Low[bar]) + " " + NumToStr(bar));
						sig.Price = -1;
					} 
				} 
			}			
		} 
			 
		bo.ProcessTradeSignals(bar);			 
	}	 
	 	
	bo.PostProcess();	 	 
		 
}

The results are not so high as with the Buy3 condition in the first post but the win rate is still quite remarkable:

Does anybody see anything wrong with that CBT?

If not, then I will check the Buy1 and Buy2 once more.

this issue looks like a problem I worked on around 2010 after Herman posted code. His code is here:

gapdown

Because you use EOD data you do not know which symbols will hit your entry first. So you get some sort of average picking of stocks. Or if you do not use PositionScore your picking will be alphabetical. While in reality often the worst stocks hit your entry first.

Or at least when I tested this with intraday data I got far worse results.

here is some code similar to hermans code;

//https://www.amibroker.org/userkb/2011/09/01/a-long-only-eod-gap-trading-idea/

Short = Cover = 0;
Qty = 10;
SetTradeDelays( 0, 0, 0, 0 );
SetOption( "AllowSameBarExit", True );
SetOption( "maxopenpositions", Qty );
SetOption("CommissionMode",3);
SetOption("CommissionAmount",0.005);

BuyLevel = Ref( L, -1 ) - Ref( L, -1 ) / 100 * 1.5;
Buy = L < BuyLevel;
BuyPrice = Min( O, BuyLevel );
SellPrice = O;
Sell = Ref( Buy, -1 );
Buy = ExRem( Buy, Sell );
Sell = ExRem( Sell, Buy );

//PositionScore = mtRandom();
PositionSize = -100 / Qty;
SetChartOptions( 0, chartShowDates );
Plot( Buylevel, "", colorBrightGreen, styleDashed );
Plot( C, "", colorWhite, 64 );
PlotShapes( IIf( Buy, shapeUpArrow, shapeNone ), colorGreen, 0, L, -15 );
PlotShapes( IIf( Buy, shapeHollowUpArrow, shapeNone ), colorWhite, 0, L, -15 );
PlotShapes( IIf( Buy, shapeSmallCircle, shapeNone ), colorAqua, 0, BuyPrice, 0 );
PlotShapes( IIf( Sell, shapeDownArrow, shapeNone ), colorRed, 0, H, -15 );
PlotShapes( IIf( Sell, shapeHollowDownArrow, shapeNone ), colorWhite, 0, H, -15 );
PlotShapes( IIf( Sell, shapeSmallCircle, shapeNone ), colorOrange, 0, SellPrice, 0 );

just tested on Nasdaq 100, last 10 years. These are some good results :smiley: Unfortunately this is because the selection of available signals is not what you will get in reality.

1 Like