//+------------------------------------------------------------------+ //| CTrailingATR.mqh | //| AnimateDread | //| https://tawarriors.com| //+------------------------------------------------------------------+ #property copyright "AnimateDread" #include #include "..\System\TradeChecks.mqh" // wizard description start //+----------------------------------------------------------------------+ //| Description of the class | //| Title=Trailing Stop based on ATR Indicator | //| Type=Trailing | //| Name=ATR | //| Class=CTrailingATR | //| Page= | //| Parameter=Multiplier,double,2, ATR Multiplier | //| Parameter=Periods,int,14, ATR Periods | //| Parameter=Shift,int,0, ATR Shift | //+----------------------------------------------------------------------+ // wizard description end //+------------------------------------------------------------------+ //| Class CTrailingATR. | //| Purpose: Class of trailing stops based on ATR * Multiplier. | //| Derives from class CExpertTrailing. | //+------------------------------------------------------------------+ //--- HOW FAR PAST ENTRY THE BREAKEVEN STOP SITS, in ATR. Covers the spread paid on the way out //--- AND the commission, which the quote never shows - so a "breakeven" exit is a small win rather //--- than a small loss. Small on purpose: raising it stops being cost cover and starts being a //--- profit target, which is what the take-profit is for. #define WARRIOR_BE_OFFSET_ATR 0.10 class CTrailingATR : public CExpertTrailing { protected: CiATR m_ATR; // ATR indicator //--- input parameters double m_multiplier; // Configurable multiple for ATR int m_periods; // Configurable periods for ATR int m_shift; // Configurable shift for ATR //--- Print-once-until-resolved: a dead/cold ATR made CheckTrailingStop() return false with //--- nothing logged, which reads identically to "the stop simply hasn't moved yet" - trailing //--- silently stops working. Reset the moment a good read comes back so a LATER outage logs again. bool m_atrDeadWarned; //--- BREAKEVEN STATE, LATCHED PER TICKET. //--- //--- R must be the ORIGINAL risk, and this is the one place that is easy to get catastrophically //--- wrong: measuring it from the CURRENT stop means that the moment the stop moves to entry, //--- risk reads as zero and every subsequent "profit in R" is infinite. This repo has already //--- shipped that bug once - breakeven destroyed the trail because the two shared a denominator //--- that one of them was moving. So entry and the opening stop are captured when the ticket //--- first appears and never recomputed for the life of that position. ulong m_beTicket; // position the latch belongs to; 0 = nothing latched double m_beEntry; double m_beRisk; // |entry - original SL|, in price. Always > 0 when latched. bool m_beMoved; // the stop has already been taken to breakeven double m_beTrigger; // move at this many R; <= 0 disables the whole feature public: CTrailingATR(void); ~CTrailingATR(void); //--- methods of initialization of protected data void Multiplier(double multiplier) { m_multiplier = multiplier; } void Periods(int periods) { m_periods = periods; } void Shift(int shift) { m_shift = shift; } //--- Move the stop to entry once the trade is this many R in front. 0 = off. void BreakevenAt(double r) { m_beTrigger = r; } virtual bool InitIndicators(CIndicators* indicators); virtual bool ValidationSettings(); virtual bool CheckTrailingStopLong(CPositionInfo* position, double& sl, double& tp); virtual bool CheckTrailingStopShort(CPositionInfo* position, double& sl, double& tp); protected: bool AdjustStopLoss(double& sl, double currentPrice, double atrValue, bool isLong); bool CheckTrailingStop(CPositionInfo* position, double& sl, double& tp, bool isLong); //--- Returns true and sets `sl` when the stop should move to entry on this call. bool CheckBreakeven(CPositionInfo* position, double& sl, bool isLong); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ void CTrailingATR::CTrailingATR(void) : m_beTicket(0), m_beEntry(0.0), m_beRisk(0.0), m_beMoved(false), m_beTrigger(0.0), m_multiplier(2), m_periods(14), m_shift(0), m_atrDeadWarned(false) { } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ void CTrailingATR::~CTrailingATR(void) { } //+------------------------------------------------------------------+ //| Validation settings protected data. | //+------------------------------------------------------------------+ bool CTrailingATR::ValidationSettings() { if(!CExpertTrailing::ValidationSettings()) return (false); // Check multiplier. ZERO IS LEGAL and means "no ATR trail" - this object also carries the // breakeven stop, which is an independent feature and the only one the operator may want. // Rejecting 0 here would make breakeven-only configurations fail to initialise, which reads // as a broken EA rather than as a refused setting. if(m_multiplier < 0.0 || m_multiplier > 100) { printf(__FUNCTION__ + ": multiplier must be >= 0 (0 = no trail) and lesser than 100"); return (false); } if(m_multiplier == 0.0 && m_beTrigger <= 0.0) { printf(__FUNCTION__ + ": neither the ATR trail nor breakeven is enabled - this object would" " do nothing; install CExpertTrailing instead"); return (false); } // Check ATR Periods if(m_periods <= 0 || m_periods > 200) { printf(__FUNCTION__ + ": ATR Periods must be greater than 0 and lesser than 200"); return (false); } // Check ATR shift if(m_shift < 0 || m_shift > 200) { printf(__FUNCTION__ + ": ATR shift must be 0-200"); return (false); } //--- ok return (true); } //+------------------------------------------------------------------+ //| Checking for input parameters and setting protected data. | //+------------------------------------------------------------------+ bool CTrailingATR::InitIndicators(CIndicators* indicators) { if(indicators == NULL) return (false); // Add ATR indicator to the collection if(!indicators.Add(GetPointer(m_ATR))) { printf(__FUNCTION__ + ": error adding object"); return (false); } // Initialize ATR indicator if(!m_ATR.Create(m_symbol.Name(), m_period, m_periods)) { return (false); } //--- ok return (true); } //+------------------------------------------------------------------+ //| Common logic for adjusting SL considering freeze level | //+------------------------------------------------------------------+ bool CTrailingATR::AdjustStopLoss(double& sl, double currentPrice, double atrValue, bool isLong) { // Brokers often set SYMBOL_TRADE_STOPS_LEVEL independently of (and sometimes larger than) // SYMBOL_TRADE_FREEZE_LEVEL - validating only against the freeze level let a computed new_sl pass // here yet still be inside the broker's minimum-stop-distance zone, so the eventual // PositionModify() outside this function would get rejected with nothing logged here to explain it. // TCMinStopDistance() is the shared max(stops, freeze) rule from System\TradeChecks.mqh (article // 2555 #6/#7); it also floors the stops level at the current spread, which brokers that publish a // 0 SYMBOL_TRADE_STOPS_LEVEL and enforce a floating spread-derived limit instead require. double minDistance = TCMinStopDistance(m_symbol.Name()); int digits = m_symbol.Digits(); // Get the number of digits after the decimal for the instrument // Calculate new SL based on position type (Long or Short) double new_sl = isLong ? NormalizeDouble(currentPrice - atrValue * m_multiplier, digits) : NormalizeDouble(currentPrice + atrValue * m_multiplier, digits); // Calculate the level beyond which SL cannot be set due to freeze/stops level double level = isLong ? currentPrice - minDistance : currentPrice + minDistance; // Check if new SL is in the correct direction and respects the freeze level bool isSlValid = isLong ? (new_sl > sl && new_sl < level) : (new_sl < sl && new_sl > level); if(isSlValid) { sl = new_sl; return true; } return false; } //+------------------------------------------------------------------+ //| Checking trailing stop and/or profit for long position. | //+------------------------------------------------------------------+ bool CTrailingATR::CheckTrailingStopLong(CPositionInfo* position, double& sl, double& tp) { return CheckTrailingStop(position, sl, tp, true); } //+------------------------------------------------------------------+ //| Checking trailing stop and/or profit for short position. | //+------------------------------------------------------------------+ bool CTrailingATR::CheckTrailingStopShort(CPositionInfo* position, double& sl, double& tp) { return CheckTrailingStop(position, sl, tp, false); } //+------------------------------------------------------------------+ //| Shared body of both directions. A long trails off the Bid, a | //| short off the Ask; everything else is identical. | //+------------------------------------------------------------------+ bool CTrailingATR::CheckTrailingStop(CPositionInfo* position, double& sl, double& tp, bool isLong) { sl = EMPTY_VALUE; tp = EMPTY_VALUE; if(position == NULL) return false; //--- BREAKEVEN FIRST, and independently of the ATR trail. //--- //--- Measured on 5,491 traded-side firings (AUDUSD D1 2015-2026): the median LOSER reaches //--- +0.50R before it is stopped, and half of all losers touch it. Moving the stop to entry at //--- +0.5R turns those into scratches - counterfactual mean R -0.132 -> +0.216. //--- //--- ⚠ THIS IS NOT A TRAIL, AND THE DIFFERENCE IS THE WHOLE POINT. Every ATR-trail setting //--- tested made things WORSE (+150.67 -> +90.56 at 1 ATR, and monotonically worse as the trail //--- widened) because a trail keeps moving and exits the winner on its first pullback - it caps //--- the upside to pay for the downside. Breakeven moves ONCE, to entry, and then stands still: //--- the loser becomes a scratch and the winner still runs to its target untouched. double be_sl = 0.0; if(CheckBreakeven(position, be_sl, isLong)) { sl = be_sl; tp = EMPTY_VALUE; return true; } //--- ATR TRAIL OFF. A zero multiplier is not "trail tightly", it is a stop AT THE MARKET - the //--- next tick closes the position. This path exists because breakeven and the trail are //--- separate features on one object: the operator may want breakeven with no trail at all. if(m_multiplier <= 0.0) return false; double new_sl = position.StopLoss(); double price = isLong ? m_symbol.Bid() : m_symbol.Ask(); double atr = m_ATR.Main(m_shift); //--- A cold/dead ATR reads EMPTY_VALUE == DBL_MAX, a real MathIsValidNumber()-passing number, not //--- a recognisable failure - AdjustStopLoss would compute against it and its own validity tests //--- would fail, returning false with NOTHING logged. That reads identically to "the stop simply //--- has not moved yet", so trailing can silently stop working for as long as the handle stays bad. if(atr == EMPTY_VALUE || atr <= 0.0 || !MathIsValidNumber(atr)) { if(!m_atrDeadWarned) { m_atrDeadWarned = true; PrintFormat("%s: ATR trailing STALLED for %s - m_ATR.Main(%d)=%.10g (needs a valid > 0" " value). Stops will not trail until this indicator recovers.", __FUNCTION__, (position != NULL ? position.Symbol() : "?"), m_shift, atr); } return false; } m_atrDeadWarned = false; //--- AdjustStopLoss honours the freeze level; only publish sl when it actually moved if(!AdjustStopLoss(new_sl, price, atr, isLong)) return false; sl = new_sl; return true; } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| MOVE THE STOP TO ENTRY once the trade is m_beTrigger R in front. | //| | //| Latches entry and the ORIGINAL stop the first time it sees a | //| ticket, and re-latches when the ticket changes - so R is fixed | //| for the life of the position and cannot be corrupted by its own | //| output. See the member declarations for why that matters. | //| | //| Moves once. After m_beMoved the ATR trail (if enabled) takes over | //| from a stop that is already at entry, so the two compose instead | //| of fighting: breakeven protects, the trail then follows. | //+------------------------------------------------------------------+ bool CTrailingATR::CheckBreakeven(CPositionInfo* position, double& sl, bool isLong) { if(m_beTrigger <= 0.0 || position == NULL || m_symbol == NULL) return false; const ulong ticket = position.Ticket(); if(ticket != m_beTicket) { //--- A NEW POSITION. Capture entry and the opening stop now, while the stop is still the one //--- the entry was sized against. m_beTicket = ticket; m_beEntry = position.PriceOpen(); m_beMoved = false; const double opening = position.StopLoss(); m_beRisk = (opening > 0.0) ? MathAbs(m_beEntry - opening) : 0.0; } //--- No original stop means no R, and a breakeven rule without R is a guess. Do nothing. if(m_beMoved || m_beRisk <= 0.0) return false; const double price = isLong ? m_symbol.Bid() : m_symbol.Ask(); const double profit = isLong ? (price - m_beEntry) : (m_beEntry - price); if(profit < m_beTrigger * m_beRisk) return false; //--- ENTRY PLUS THE COST OF LEAVING, PLUS A LITTLE. //--- //--- A stop exactly at PriceOpen() is NOT breakeven: the exit crosses the spread, so a long //--- stopped "at entry" still loses it - and the spread is only half the bill. Commission is //--- charged per side and never appears in the quote at all; on EURUSD it was measured as 73% //--- of total cost, so a spread-only offset leaves the larger half uncovered and the "scratch" //--- is still a loss. //--- //--- The offset is a FRACTION OF ATR rather than a fixed pip count, because commission has to be //--- covered on gold and on EURUSD with one number, and only a volatility-relative one travels. //--- It is deliberately small: this buys a tiny win, it is not a profit target. const double spread = m_symbol.Ask() - m_symbol.Bid(); const double atrNow = m_ATR.Main(m_shift); const double pad = (atrNow > 0.0 && atrNow != EMPTY_VALUE && MathIsValidNumber(atrNow)) ? WARRIOR_BE_OFFSET_ATR * atrNow : 0.0; // a dead ATR degrades to spread-only, never to junk double target = isLong ? (m_beEntry + spread + pad) : (m_beEntry - spread - pad); target = NormalizeDouble(target, m_symbol.Digits()); //--- Never move a stop BACKWARDS. If the ATR trail has already carried it past entry, breakeven //--- would be a retreat - and a stop that loosens is a risk increase nobody asked for. const double current = position.StopLoss(); if(current > 0.0 && (isLong ? (target <= current) : (target >= current))) { m_beMoved = true; // already better than breakeven; consider it done return false; } //--- The broker's minimum distance and freeze band still apply to a modification. string why = ""; if(!TCFreezeOkForPosition(position.Symbol(), (isLong ? POSITION_TYPE_BUY : POSITION_TYPE_SELL), target, 0.0, why)) return false; sl = target; m_beMoved = true; return true; }