Trailing Stop Question

I have this trailing stop code I've used that when spot checking seems to work as expected. However, today in live trading I noticed my actual sell point was higher than the backtest. The backtest sold at the initial stop instead of having that trailing stop level rise up to the low of the previous day as expected.

Ticker: GFS
Entry Date: 12/9/22
Exit Date: 12/16/22
Initial Stop Level: $60.43
Trailing Stop was at $61.80 on 12/15/22
Gap over my stop, so expected to sell the open: $61.32
Instead it sold lower at my initial stop: $60.43

I'm not clear why the trailing stop level of 12/15/22 was not used in this instance.

GFS issue

/*
SIMPLE DC CHECK IF IN POSITION
This strategy looks for pullbacks in uptrends (i.e. Flags) anticipating that they will break bullish and continue the trend.
Simplified and added explanation to the code to post to Amibroker Forum for assistance in adding a Filter so that stocks
that already have a buy signal won't show up.
*/

#include_once "Formulas\Norgate Data\Norgate Data Functions.afl"

// ==========================================================================================
// === TRADING SYSTEM PARAMETERS ===
// ==========================================================================================
SetOption("InitialEquity", 100000);
SetTradeDelays( 0, 0, 0, 0 ); // Trades intra-day with stop orders that are placed the night before
SetOption("AllowPositionShrinking", True);
RoundLotSize = 1;  // Disallow fractional share purchasing
SetOption("MaxOpenPositions", 12); // Maximum number of open positions: 30
SetOption("AccountMargin", 50); // 100 = Cash account, 75 = Using half available margin, 50 = 2X margin


// ==========================================================================================
// === RISK MANAGEMENT & POSITION SIZING ===
// ==========================================================================================
// === INITIAL (MAX) STOP LOSS ====	
SetOption("ActivateStopsImmediately", False); // Stop behavior defined by trailing stop below - IS THIS THE CORRECT SETTING?
stopLoss = 4;
ApplyStop(stopTypeLoss, stopModePercent, stopLoss, ExitAtStop = 1, True);  // Allows for intraday stops even with TradeDelays

// === POSITION SIZING ===
PositionRisk = 1.0; // Percentage of account at risk if wrong
PosSize = 100 * PositionRisk / stopLoss; // Position size as a percent of equity for a given stop level
SetPositionSize(PosSize, spsPercentOfEquity);


// ==========================================================================================
// === PLOT CHART & INDICATORS ===
// ==========================================================================================
// Chart
SetChartOptions(0,chartShowArrows|chartShowDates);
_N(Title = StrFormat("{{NAME}} - " + FullName() + " | {{INTERVAL}} {{DATE}}: Open %g, Hi %g, Lo %g, Close %g (%.1f%%) {{VALUES}}", O, H, L, C, SelectedValue( ROC( C, 1 ) ) ));
Plot( C, "Close", ParamColor("Color", colorDefault ), styleNoTitle | ParamStyle("Style") | GetPriceStyle() ); 

// Donchain Channels
pdsU=4;  // High channel lookback
pdsL=5;  // Low channel lookback

DonchianUpper =HHV(H,pdsU);
DonchianLower = LLV(L,pdsL);

Plot(DonchianUpper,"DU",/*color*/ 2,styleDashed);
Plot(DonchianLower,"DL",/*color*/ 2, styleDashed);

// Moving Averages (use Nz to avoid lack of history screening strong stocks out)
sma50 = Nz(ma(Close, 50));
sma150 = Nz(ma(Close, 150));

Plot(sma50,"50MA",/*color*/ 3, styleNoTitle);
Plot(sma150,"150MA",colorWhite, styleNoTitle);


// ==========================================================================================
// === FLAG DETECTION === https://forum.amibroker.com/t/high-tight-flag-power-play-detection/21895
// ==========================================================================================
flagLookback = 13; // Number of bars to look back for top of flag
PoleHigh = HHV(H, flagLookback);  // Highest high within the lookback period
PHBar 	 = HHVBars(H, flagLookback);  // How many bars back the pivot high bar occurred
	
FlagLow = LLV(L, PHBar);  // Flag Low is the lowest price since PoleHigh
FLBar = LLVBars(L, PHBar);  // How many bars back is the low point of the flag

flagPoleLookback = 21;  // Look ~20 days prior to the pole for shorter term flags
PoleLow = Ref(LLV(L, flagPoleLookback), -PHBar-1);  
PLBar = Ref(LLVBars(L, flagPoleLookback), -PHBar-1);

// Flag characteristics
PoleLength = Nz((PoleHigh - PoleLow)/PoleLow);  // Percentage move that forms the flag pole
FlagDepth = Nz((PoleHigh - FlagLow)/PoleHigh);  // Percentage retracement off the highs
FlagRatio = Nz(FlagDepth/PoleLength);  // Flag-to-Pole ratio

// Flag Requirements
Flag = 
  PoleLength > 0.10 AND  // Must have had at least a 10% rise
  FlagDepth < 0.25 AND // Flag must not have come down more than 25% off the flag pole top
  FlagRatio < 0.5 AND  // Flag cannot have retraced more than 50% of the initial move
  PHBar > 1;  // Flag must be more than two candles (will trigger at third)

PlotShapes(Flag*shapeCircle, colorBlue, 0, Low);// Plot dots indicating consolidation meets requirements


// ==========================================================================================
// === FILTERING AND SCREENING CRITERIA ===
// ==========================================================================================
// Set other non-chart visible variables
adr = MA((High - Low) / abs(Low) * 100, 21);
vol = MA(Volume, 20); 
inTrade = 0;

tradeFilter = 
  DonchianUpper > sma50 AND  // when the buy is triggered stock is above the 50 SMA
  Close > sma150 AND  // Only look at stocks in Stage 2 uptrends as defined by the 30 week/150 day MA (Stain Weinstein)
  Flag == True AND  // The screen the night before should show the stock being in a proper consolidation
  adr >= 4.0 AND  // Look for stocks with higher average daily ranges
  vol >= 300000 AND  // Filter out illiquid stocks
  Close >= 1.5;  // Filter out penny stocks
  
  
// Filter Ribbon - add for better visibility when the strategy thinks a security is viable/not viable
Color = IIf(tradeFilter,0,1);
Plot ( 1, "", Color, styleArea |styleNoLabel | styleOwnScale , 0, 100);
  
// ==========================================================================================
// === BUY & SELL LOGIC ===
// ==========================================================================================
// === NORGATE VARIABLES TO AVOID DESLISTED STOCKS ===
dt = DateTime();
bi = BarIndex();
delistedSecurity = Nz(DateTimeDiff(dt, GetFnData("DelistingDate")) >= 0);
barsBeforeDelisting = LastValue(ValueWhen(dt == GetFnData("DelistingDate"), bi, 1)) -bi;
OnSecondLastBarOfDelistedSecurity = barsBeforeDelisting == 1;
OnLastTwoBarsOfDelistedSecurity = barsBeforeDelisting == 2 OR barsBeforeDelisting == 1 OR delistedSecurity;

// === BUY AND SELL LOGIC
Buy = (High > Ref(DonchianUpper, -1)) AND Ref(tradeFilter, -1)
  AND NOT OnLastTwoBarsOfDelistedSecurity;
  //AND (NorgateIndexConstituentTimeSeries("$SPX") OR NorgateIndexConstituentTimeSeries("$NDX"));  // Unhide to for historical testing


BuyPrice=Max(Ref(DonchianUpper, -1), Open);

// == TRAILING STOP ===
// Initialize variables
Sell=0;
trailArray = Null;
trailStop = 0;
inTrade = 0;

//Trailing stop logic
for( i = 0; i < BarCount; i++ )
{  
  if( trailstop == 0 AND Buy[ i ] ) 
  { 
	trailstop = BuyPrice[i] * (1- stopLoss[i]/100);  // Use stoploss level
	inTrade = 1;
  }
  else Buy[i] = 0; // remove excess buy signals
  if( trailstop > 0 AND Low[ i ] < trailstop AND (NOT Buy[i] OR (Buy[i] AND Close[i] < Open[i])))  // adding Buy check ensures you don't stop out on the breakout candle when the stop is within it unless it's a bearish candle
   {   
	 Sell[ i ] = 1; 
	 SellPrice[ i ] = Min( Open[ i ], trailstop );
	 trailstop = 0;
	 inTrade = 0;
   }
   
  if( trailstop > 0 )
   {
	  trailStop = Max(DonchianLower[i], trailStop); 
	  trailARRAY[ i ] = trailstop;
   }
}


Plot(trailArray, "Trailing Stop Level", colorRed, styleThick);

// Overwrite sells to ensure not holding deslisted securities
Sell = Sell OR OnSecondLastBarOfDelistedSecurity;

// Plot arrows indicating trades
PlotShapes(IIf(Buy, shapeUpTriangle, shapeNone),colorLime, 0, L, Offset=-50);
PlotShapes(IIf(Sell, shapeDownTriangle, shapeNone),colorRed, 0, H, Offset=-50);


// ==========================================================================================
// === POSITION SCORE FOR RANKING STOCKS ===
// ==========================================================================================
PositionScore = Ref(vol, -1); // sort by most liquid stocks


// ==========================================================================================
// === EXPLORATION / SCANNING COLUMNS ===
// ==========================================================================================

Filter = tradeFilter AND NOT inTrade; // Filter out stocks that don't have current buy signals

AddTextColumn(FullName(), "Name");
AddTextColumn( SectorID(1), "Sector");
AddTextColumn(IndustryID(1), "Industry Name");
AddColumn(adr, "ADR", 1.1);
AddColumn(vol, "Avg. Volume", 1.0);
AddColumn(inTrade, "In Trade?", 1.0);

// Add other things to title bar I want to see
Title += StrFormat(" | ADR: %.1f", ADR);  // Add ADR to the title of charts

Your fixed stop is being communicated to AmiBroker as an actual stop, because you are using the ApplyStop function. However, your trailing stop is being communicated to AmiBroker as an exit signal because you are setting Sell and SellPrice when your trailing stop is hit. My guess is that AmiBroker is always processing stops before exits, so the fixed stop gets executed before the Sell signal (your trailing stop) is processed.

You can control the order in which stops are processed using SetStopPrecedence(). However, that function only deals with the various stop types, and not regular exits controlled by the Sell and Cover variables. As far as I know, you would have to write a CBT if you wanted more control over the order in which stops and exits are processed.

1 Like

There is a setting "ApplyStopsImmediately" that controls when stops are executed with regards to regular signals. It is all explained as always IN THE MANUAL. Read "Scenarios" in the ApplyStop docs:
https://www.amibroker.com/guide/afl/applystop.html

So, no, you don't need CBT.

2 Likes

Good to know, thanks. I always thought that referred to entries and stops... it didnt occur to me that it also applies to exits.

Thank you @Tomasz. It looks like I had this line that should not have been there at all if I was controlling my stops via the trailing stop For Loop.

ApplyStop(stopTypeLoss, stopModePercent, stopLoss, ExitAtStop = 1, True); 

When I comment this line out, the stops move up as I would expect.

I appreciate all the explanation you have in the manual about the different functions (it is very well documented), but sometimes it's not clear where to look. I was assuming this was an issue with my For Loop, but it looks like it was an improper setting all along. This forum is a great place for pointing that out. Thanks!

1 Like

I also had this line in the code which it seems like I should also remove if I'm not using the built-in stops.

SetOption("ActivateStopsImmediately", False);

Commenting this line out also seems to not have any effect after commenting out the first ApplyStop line.

As written in the manual, the setting in question controls when ApplyStop()s are checked and executed in relationship to regular exits. If you don't use ApplyStop()s in your formula then the setting will have no effect.

Anything that is assigned to Sell or Cover variable is a regular exit, not ApplyStop-exit.

1 Like

Yes, that was what I gathered about

SetOption("ActivateStopsImmediately", False);

from reading the manual. Commented it out to confirm.

The takeaway for me is this: since my Sell code includes the stop level, using built-in stops with ApplyStop is not needed and is causing an issue by taking priority when both the Sell signal and Stop signal are given in the same bar.

Removing it entirely allows the Sell definition to define both initial stops and trailing stops.

This topic was automatically closed 100 days after the last reply. New replies are no longer allowed.