2026-09-13 14:32:40 -04:00 | | | //+------------------------------------------------------------------+
|
| | | //| CTrailingATR.mqh |
|
| | | //| AnimateDread |
|
| | | //| https://tawarriors.com|
|
| | | //+------------------------------------------------------------------+
|
| | | #property copyright "AnimateDread"
|
| | | #include <Expert\ExpertTrailing.mqh>
|
| | | #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;
|
| | | }
|