//+------------------------------------------------------------------+
//|                                                 DabsPro_3.1.mq5   |
//|        1.37 Fibonacci extension + hammer pin-bar long-reversal     |
//|        strategy with full money management.                       |
//|                                                                   |
//|  Detects a hammer pin bar (small body, long lower wick) whose      |
//|  wick tags the 1.37 extension of the most recent down swing leg,   |
//|  and trades the LONG reversal with risk-based sizing, broker-aware  |
//|  ATR stops and an R-multiple target.                              |
//|                                                                   |
//|  HONEST STATUS — this is a CANDIDATE thin-session reversion idea,  |
//|  NOT an established edge. Read before trusting any number:         |
//|   * The headline "Sharpe ~5 / 70% WR" came from grid-searching     |
//|     600 configs and keeping the best. After correcting for the     |
//|     trial count (deflated Sharpe, Bailey/Lopez de Prado) the       |
//|     selected Sharpe is ~0. The selection IS the result.            |
//|   * The only true out-of-sample is 43 trades. 70% on n=43 has a    |
//|     95% CI of roughly [56%, 84%]; at 1:1 payoff (breakeven 50%,    |
//|     ~55-56% after realistic off-hours spread) you cannot reject    |
//|     "no edge".                                                     |
//|   * These are off-hours stock CFDs. A Strategy Tester run with     |
//|     modeled/fixed spread under-charges the real pre/post-market    |
//|     spread and takes fills live execution would skip. Re-run with  |
//|     real bid/ask (or a 3-10x off-hours spread penalty).            |
//|                                                                   |
//|  v3.1 FIXES (this file) addressing a code review:                  |
//|   * Session windows are now defined in explicit US Eastern time    |
//|     (InpSessionsInET) instead of raw broker-server hours, which    |
//|     previously mislabeled the "golden hours". Server->ET offset is  |
//|     auto-measured or set via InpBrokerUTCOffset, and printed at     |
//|     init for verification.                                         |
//|   * Break-even / trailing now default OFF and warn if they collide  |
//|     with the take-profit (BreakEvenR/TrailStartR >= RR). At 1:1     |
//|     the old defaults booked sub-1R scratches as "wins", inflating   |
//|     the win-rate; a clean SL/TP bracket logs a TRUE +1R win.        |
//|   * PassMinLegFilter now measures the ACTUAL swing leg             |
//|     (pivot_high - pivot_low), not the fib-to-wick distance the      |
//|     tag check already pins to ~0 (the old filter was inert).        |
//|                                                                   |
//|  Money management (sound, verified against MQL5 best practice):    |
//|   * Risk-% sizing from tick value/size, clamped to volume step.    |
//|   * OrderCalcMargin pre-trade check; SYMBOL_TRADE_STOPS_LEVEL /     |
//|     freeze-level aware SL/TP; per-ticket initial-risk cache so      |
//|     BE/trailing use the ORIGINAL R.                                |
//|                                                                   |
//|  Config: 1.5 ATR stop, 1.0R target (1:1, breakeven 50% gross).     |
//|  Educational only. Validate with real-cost fills before any        |
//|  live capital.                                                     |
//+------------------------------------------------------------------+
#property copyright "DabsPro"
#property version   "3.10"
#property strict
#property description "1.37-fib / hammer pin-bar long-reversal EA v3.1. M15. Candidate edge (not established); review bugs fixed, ET sessions, honest accounting."

#include <Trade/Trade.mqh>
#include <Trade/SymbolInfo.mqh>

//==================== INPUTS ========================================
input group "=== Strategy ==="
input ENUM_TIMEFRAMES InpTF            = PERIOD_CURRENT; // Signal timeframe (CURRENT = chart). M15 has the strongest generalized OOS edge.
input double  InpFib                   = 1.37;        // Fibonacci extension level
input int     InpPivotK                = 3;           // Swing pivot half-window (bars)
input int     InpLookback              = 200;         // Bars scanned for swings
input double  InpBodyMaxFrac           = 0.35;        // Max body / range  (loose — captures more edge)
input double  InpDomWickFrac           = 0.50;        // Min dominant wick / range
input double  InpOppWickFrac           = 0.25;        // Max opposite wick / range
input double  InpTagTolPct             = 0.15;        // Wick-tag tolerance (% of price)

enum DirMode { BOTH=0, BULL_ONLY=1, BEAR_ONLY=2 };
input DirMode InpDirMode               = BULL_ONLY;  // LONG ONLY — shorts had 33% WR, destroyed edge

input group "=== Multi-timeframe trend filter (OFF — counter-trend strategy) ==="
input bool    InpUseTrendFilter        = false;       // Gate entries by a higher-timeframe trend (OFF — kills edge)
input ENUM_TIMEFRAMES InpTrendTF       = PERIOD_H1;   // Higher timeframe for the trend
input int     InpTrendEMA              = 50;          // EMA period on the higher timeframe
input bool    InpTrendStrict           = true;        // true: price & EMA-slope must agree; false: slope only

input group "=== Session timezone (define golden hours in ET, not broker time) ==="
input bool    InpSessionsInET          = true;        // Interpret the session hours below as US Eastern (recommended)
input int     InpBrokerUTCOffset       = 99;          // Broker server time = UTC + this (e.g. 2 or 3). 99 = auto-measure

input group "=== Edge filters (session windows in ET when InpSessionsInET) ==="
input bool    InpUseSession            = true;        // Only trade golden hours (highest WR periods)
input int     InpSessionStartHour      = 5;           // Window 1 start hour (ET) — pre-market 05:00
input int     InpSessionEndHour        = 6;           // Window 1 end hour (ET, exclusive)
input bool    InpUseSession2           = true;        // Second session window (power hour + after-hours)
input int     InpSession2StartHour     = 14;          // Window 2 start (ET) — power hour + AH
input int     InpSession2EndHour       = 19;          // Window 2 end (ET, exclusive)
input bool    InpUseRelVolume          = false;       // Require elevated volume (off — vol filter hurts in study)
input double  InpMinRelVolume          = 1.5;         // Min signal vol / median(20)
input bool    InpUseRSI                = true;        // Require counter-trend RSI context
input int     InpRSIPeriod             = 14;          // RSI period
input double  InpRSIMin                = 0.0;         // RSI lower bound (0 = auto: hammer<50, star>50)
input double  InpRSIMax                = 100.0;       // RSI upper bound (100 = auto)

input group "=== Enhanced edge filters (new in v0.2) ==="
input bool    InpUseGapFilter          = true;        // Reject signals where price gapped into the bar
input double  InpMaxGapATR             = 1.0;         // Max gap (|close-prev_close|/ATR) — relaxed from 0.3 (too restrictive in gen test)
input bool    InpUseOvershootFilter    = true;        // Reject signals where wick blew past fib level too far
input double  InpMaxOvershootATR       = 0.5;         // Max overshoot past 1.37 level in ATR
input bool    InpUseMinLegATR          = true;        // Require minimum swing leg size
input double  InpMinLegATR             = 1.5;         // Min swing leg in ATR units

input group "=== Sector exclusion (v0.3 — financial & consumer have negative EV) ==="
input bool    InpExcludeSectors        = true;        // Skip symbols in excluded sectors
input string  InpExcludedSectors       = "financial,consumer"; // Comma-separated sector names to exclude

input group "=== Money management ==="
input bool    InpUseRiskPercent        = true;        // Size by risk % (else fixed lots)
input double  InpRiskPercent           = 0.5;         // Risk per trade (% of equity)
input double  InpFixedLots             = 0.10;        // Fixed lot size (if risk % off)
input int     InpATRPeriod             = 14;          // ATR period
input double  InpATRmultSL             = 1.5;         // Stop = wick extreme +/- this*ATR (1.5 ATR — wider stop = higher WR)
input double  InpRR                    = 1.0;         // Take-profit = RR * stop distance (1.0R — quick target, 65% WR)
input double  InpMaxRiskLots           = 5.0;         // Hard cap on lots
input bool    InpSkipIfBelowMinLot     = false;       // Skip trade if risk-lot < broker min (else use min)
input double  InpMaxSpreadPoints       = 50;          // Skip if spread above this (points)
input double  InpMarginBufferPct       = 20;          // Require this % free-margin headroom

input group "=== Trade management ==="
input int     InpMaxPositions          = 1;           // Max concurrent positions (this symbol+magic)
// NOTE: with RR=1.0, enabling BE/trailing at >=1.0R collides with the take-profit
//       and books sub-1R scratches as "wins" (inflated WR). Keep OFF for a clean
//       1:1 bracket, or set RR > BreakEvenR/TrailStartR. OnInit warns on collision.
input bool    InpUseBreakEven          = false;       // Move SL to break-even (OFF: avoids TP collision)
input double  InpBreakEvenR            = 0.5;         // ...after price travels this many R (must be < RR)
input double  InpBreakEvenLockPts      = 5;           // Points locked beyond entry at BE
input bool    InpUseTrailing           = false;       // ATR trailing stop (OFF: avoids TP collision)
input double  InpTrailATRmult          = 1.5;         // Trailing distance in ATR
input double  InpTrailStartR           = 0.5;         // Only start trailing after this many R (must be < RR)
input bool    InpCloseOppositeSignal   = false;       // Close on opposite signal

input group "=== Misc ==="
input long    InpMagic                 = 137037;      // Magic number
input int     InpSlippage              = 20;          // Deviation (points)
input string  InpComment               = "DabsPro3.1";// Order comment

//==================== GLOBALS =======================================
CTrade            trade;
CSymbolInfo       sym;
ENUM_TIMEFRAMES   gTF;                     // resolved signal timeframe
int           hATR   = INVALID_HANDLE;
int           hRSI   = INVALID_HANDLE;
int           hTrend = INVALID_HANDLE;     // HTF EMA handle
datetime      lastBarTime = 0;

// initial-risk cache (so BE / trailing use the ORIGINAL R, not the moved SL)
ulong         gTicket[];
double        gInitRisk[];

//+------------------------------------------------------------------+
int OnInit()
  {
   if(!sym.Name(_Symbol))
     {
      Print("DabsPro: cannot select symbol ", _Symbol);
      return(INIT_FAILED);
     }
   trade.SetExpertMagicNumber(InpMagic);
   trade.SetDeviationInPoints(InpSlippage);
   trade.SetTypeFillingBySymbol(_Symbol);
   trade.SetMarginMode();
   trade.LogLevel(LOG_LEVEL_ERRORS);

   gTF = (InpTF == PERIOD_CURRENT) ? (ENUM_TIMEFRAMES)_Period : InpTF;

   hATR = iATR(_Symbol, gTF, InpATRPeriod);
   hRSI = iRSI(_Symbol, gTF, InpRSIPeriod, PRICE_CLOSE);
   if(hATR == INVALID_HANDLE || hRSI == INVALID_HANDLE)
     {
      Print("DabsPro: failed to create indicator handles");
      return(INIT_FAILED);
     }
   if(InpUseTrendFilter)
     {
      hTrend = iMA(_Symbol, InpTrendTF, InpTrendEMA, 0, MODE_EMA, PRICE_CLOSE);
      if(hTrend == INVALID_HANDLE)
        { Print("DabsPro: failed to create HTF trend handle"); return(INIT_FAILED); }
     }
   ArrayResize(gTicket, 0);
   ArrayResize(gInitRisk, 0);

   // --- break-even / trailing vs take-profit collision guard ---
   if(InpUseBreakEven && InpBreakEvenR >= InpRR)
      Print("DabsPro WARNING: BreakEvenR (", InpBreakEvenR, ") >= RR (", InpRR,
            "). Break-even collides with the take-profit; 'wins' will include "
            "sub-1R scratches and the win-rate will be inflated. Set RR > BreakEvenR "
            "or disable break-even.");
   if(InpUseTrailing && InpTrailStartR >= InpRR)
      Print("DabsPro WARNING: TrailStartR (", InpTrailStartR, ") >= RR (", InpRR,
            "). Trailing starts at/after the take-profit; same WR-inflation risk.");

   // --- timezone verification (the old code mislabeled broker hours as ET) ---
   PrintFormat("DabsPro 3.1 on %s %s | risk %.2f%% RR %.1f | broker UTC offset %+dh, "
               "sessions in %s, current bar ET hour = %d",
               _Symbol, EnumToString(gTF), InpRiskPercent, InpRR,
               BrokerUTCOffsetHours(), InpSessionsInET ? "ET" : "broker-time",
               BarHourET(TimeTradeServer()));
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if(hATR   != INVALID_HANDLE) IndicatorRelease(hATR);
   if(hRSI   != INVALID_HANDLE) IndicatorRelease(hRSI);
   if(hTrend != INVALID_HANDLE) IndicatorRelease(hTrend);
  }

//+------------------------------------------------------------------+
void OnTick()
  {
   sym.RefreshRates();
   ManageOpenPositions();          // manage every tick

   // act once per new closed bar
   datetime t[2];
   if(CopyTime(_Symbol, gTF, 0, 2, t) < 2) return;
   if(t[0] == lastBarTime) return;
   lastBarTime = t[0];

   CheckForSignal();
  }

//+------------------------------------------------------------------+
//| Detect pin bar at the last CLOSED bar (shift 1) and trade it     |
//+------------------------------------------------------------------+
void CheckForSignal()
  {
   if(PositionsForThisEA() >= InpMaxPositions) return;

   int need = InpLookback + InpPivotK + 5;
   MqlRates r[];
   ArraySetAsSeries(r, true);
   if(CopyRates(_Symbol, gTF, 0, need, r) < need) return;

   int sig = 1;                    // last closed bar
   double o = r[sig].open, h = r[sig].high, l = r[sig].low, c = r[sig].close;
   double rng = h - l;
   if(rng <= 0) return;
   double body  = MathAbs(c - o);
   double upper = h - MathMax(o, c);
   double lower = MathMin(o, c) - l;
   double dom   = MathMax(upper, lower);
   double opp   = MathMin(upper, lower);

   bool isPin = (body <= InpBodyMaxFrac * rng) &&
                (dom  >= InpDomWickFrac * rng) &&
                (opp  <= InpOppWickFrac * rng);
   if(!isPin) return;

   bool bull = (lower >= upper);   // hammer
   bool bear = (upper >  lower);   // shooting star
   if(InpDirMode == BULL_ONLY && !bull) return;
   if(InpDirMode == BEAR_ONLY && !bear) return;

   double upLevel, dnLevel, upLegSize, dnLegSize; bool haveUp, haveDn;
   GetFibLevels(r, sig, haveUp, upLevel, upLegSize, haveDn, dnLevel, dnLegSize);

   double tol     = InpTagTolPct / 100.0 * c;
   double bodyTop = MathMax(o, c), bodyBot = MathMin(o, c);

   bool buySignal = false, sellSignal = false;

   if(bear && haveUp)                 // upside 1.37 rejected by shooting star -> SELL
      if(bodyTop - tol <= upLevel && upLevel <= h + tol) sellSignal = true;
   if(!sellSignal && bull && haveDn)  // downside 1.37 rejected by hammer -> BUY
      if(l - tol <= dnLevel && dnLevel <= bodyBot + tol) buySignal = true;

   if(!buySignal && !sellSignal) return;

   double atr = GetATR();
   if(atr <= 0) return;

   // ---- confirmation filters ----
   if(!PassSessionFilter(r[sig].time))        return;
   if(!PassVolumeFilter(r, sig))              return;
   if(!PassRSIFilter(bull))                   return;
   if(!PassTrendFilter(buySignal))            return;
   if(!PassGapFilter(r, sig, atr))             return;
   if(!PassOvershootFilter(r, sig, buySignal, upLevel, dnLevel, atr)) return;
   if(!PassMinLegFilter(buySignal, upLegSize, dnLegSize))      return;
   if(SpreadPoints() > InpMaxSpreadPoints)    return;

   if(InpCloseOppositeSignal)
      CloseOpposite(buySignal ? POSITION_TYPE_BUY : POSITION_TYPE_SELL);
   if(PositionsForThisEA() >= InpMaxPositions) return;

   if(buySignal)  OpenTrade(true,  l, atr);
   else           OpenTrade(false, h, atr);
  }

//+------------------------------------------------------------------+
//| Build & send an order with full risk control                     |
//+------------------------------------------------------------------+
void OpenTrade(bool isBuy, double wickExtreme, double atr)
  {
   sym.RefreshRates();
   double entry = isBuy ? sym.Ask() : sym.Bid();
   double rawSL = isBuy ? MathMin(wickExtreme, entry) - InpATRmultSL * atr
                        : MathMax(wickExtreme, entry) + InpATRmultSL * atr;

   double sl = BrokerizeStop(entry, rawSL, isBuy);     // honour stops level
   double slDist = MathAbs(entry - sl);
   if(slDist <= 0) return;

   double tp = isBuy ? entry + InpRR * slDist : entry - InpRR * slDist;
   tp = BrokerizeTP(entry, tp, isBuy);

   double lots = CalcLots(slDist);
   if(lots <= 0) { Print("DabsPro: lot size resolves to 0 -> skip"); return; }

   ENUM_ORDER_TYPE ot = isBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
   if(!HasEnoughMargin(ot, lots, entry))
     {
      PrintFormat("DabsPro: insufficient free-margin headroom for %.2f lots -> skip", lots);
      return;
     }

   entry = NormalizeDouble(entry, _Digits);
   sl    = NormalizeDouble(sl, _Digits);
   tp    = NormalizeDouble(tp, _Digits);

   bool ok = isBuy ? trade.Buy(lots, _Symbol, entry, sl, tp, InpComment)
                   : trade.Sell(lots, _Symbol, entry, sl, tp, InpComment);
   if(!ok || (trade.ResultRetcode() != TRADE_RETCODE_DONE &&
              trade.ResultRetcode() != TRADE_RETCODE_PLACED))
      PrintFormat("DabsPro: order failed retcode=%d (%s)",
                  trade.ResultRetcode(), trade.ResultRetcodeDescription());
   else
      PrintFormat("DabsPro: %s %.2f lots @ %.5f  SL %.5f  TP %.5f  (risk %.1f pts)",
                  isBuy?"BUY":"SELL", lots, entry, sl, tp, slDist/_Point);
  }

//+------------------------------------------------------------------+
//| 1.37 extension of the most recent up-leg and down-leg            |
//+------------------------------------------------------------------+
void GetFibLevels(const MqlRates &r[], int sig,
                  bool &haveUp, double &upLevel, double &upLegSize,
                  bool &haveDn, double &dnLevel, double &dnLegSize)
  {
   haveUp = haveDn = false;
   upLevel = dnLevel = upLegSize = dnLegSize = 0;
   int K = InpPivotK;

   int    pivType[];  double pivPrice[];  int n = 0;
   ArrayResize(pivType, InpLookback); ArrayResize(pivPrice, InpLookback);

   // pivots, scanned newest -> oldest (shift grows with age)
   for(int i = sig + K; i <= InpLookback && i + K < ArraySize(r); i++)
     {
      bool isHigh = true, isLow = true;
      for(int j = 1; j <= K; j++)
        {
         if(r[i].high < r[i-j].high || r[i].high < r[i+j].high) isHigh = false;
         if(r[i].low  > r[i-j].low  || r[i].low  > r[i+j].low ) isLow  = false;
        }
      if(isHigh)      { pivType[n]=+1; pivPrice[n]=r[i].high; n++; }
      else if(isLow)  { pivType[n]=-1; pivPrice[n]=r[i].low;  n++; }
     }
   for(int a = 0; a < n - 1; a++)
     {
      int newer = a, older = a + 1;          // newer has smaller shift
      if(!haveUp && pivType[older]==-1 && pivType[newer]==+1)
        {                                    // up leg: low(older) -> high(newer)
         upLegSize = pivPrice[newer] - pivPrice[older];
         upLevel   = pivPrice[older] + InpFib * upLegSize;
         haveUp = true;
        }
      if(!haveDn && pivType[older]==+1 && pivType[newer]==-1)
        {                                    // down leg: high(older) -> low(newer)
         dnLegSize = pivPrice[older] - pivPrice[newer];
         dnLevel   = pivPrice[older] - InpFib * dnLegSize;
         haveDn = true;
        }
      if(haveUp && haveDn) break;
     }
  }

//+------------------------------------------------------------------+
//| Filters                                                          |
//+------------------------------------------------------------------+
// --- timezone helpers: convert broker-server bar time to a US-Eastern hour ---
int BrokerUTCOffsetHours()
  {
   if(InpBrokerUTCOffset != 99) return InpBrokerUTCOffset;
   long diff = (long)TimeTradeServer() - (long)TimeGMT();   // server minus GMT
   return (int)MathRound((double)diff / 3600.0);
  }

datetime NthSundayUTC(int year, int month, int n, int hourUTC)
  {
   MqlDateTime t; t.year=year; t.mon=month; t.day=1; t.hour=hourUTC; t.min=0; t.sec=0;
   datetime d = StructToTime(t);
   MqlDateTime w; TimeToStruct(d, w);                       // day_of_week: 0=Sunday
   int firstSun = (w.day_of_week == 0) ? 1 : (8 - w.day_of_week);
   t.day = firstSun + (n - 1) * 7;
   return StructToTime(t);
  }

bool IsUSEasternDST(datetime utc)
  {
   MqlDateTime t; TimeToStruct(utc, t);
   datetime dstStart = NthSundayUTC(t.year, 3, 2, 7);       // 2nd Sun Mar, 07:00 UTC
   datetime dstEnd   = NthSundayUTC(t.year, 11, 1, 6);      // 1st Sun Nov, 06:00 UTC
   return (utc >= dstStart && utc < dstEnd);
  }

int BarHourET(datetime serverTime)
  {
   datetime utc = (datetime)((long)serverTime - (long)BrokerUTCOffsetHours() * 3600);
   int etOff = IsUSEasternDST(utc) ? -4 : -5;               // EDT vs EST
   datetime et = (datetime)((long)utc + (long)etOff * 3600);
   MqlDateTime t; TimeToStruct(et, t);
   return t.hour;
  }

bool PassSessionFilter(datetime barTime)
  {
   if(!InpUseSession) return true;
   int hr;
   if(InpSessionsInET)
      hr = BarHourET(barTime);
   else
     { MqlDateTime dt; TimeToStruct(barTime, dt); hr = dt.hour; }

   if(hr >= InpSessionStartHour && hr < InpSessionEndHour) return true;
   if(InpUseSession2)
      return (hr >= InpSession2StartHour && hr < InpSession2EndHour);
   return false;
  }

bool PassVolumeFilter(const MqlRates &r[], int sig)
  {
   if(!InpUseRelVolume) return true;
   int m = 20;
   if(sig + 1 + m >= ArraySize(r)) return true;
   double vols[]; ArrayResize(vols, m);
   for(int i = 0; i < m; i++) vols[i] = (double)r[sig + 1 + i].tick_volume;
   ArraySort(vols);
   double med = (vols[m/2 - 1] + vols[m/2]) / 2.0;
   if(med <= 0) return true;
   return ((double)r[sig].tick_volume / med >= InpMinRelVolume);
  }

bool PassRSIFilter(bool bull)
  {
   if(!InpUseRSI) return true;
   double v[1];
   if(CopyBuffer(hRSI, 0, 1, 1, v) < 1) return false;
   // Auto counter-trend mode: hammer requires RSI < 50, shooting star requires RSI > 50
   if(InpRSIMin <= 0.0 && InpRSIMax >= 100.0)
     {
      if(bull)  return (v[0] < 50.0);   // hammer in oversold context
      else     return (v[0] > 50.0);   // shooting star in overbought context
     }
   return (v[0] >= InpRSIMin && v[0] <= InpRSIMax);
  }

//+------------------------------------------------------------------+
//| Enhanced edge filters (v0.2)                                     |
//+------------------------------------------------------------------+
bool PassGapFilter(const MqlRates &r[], int sig, double atr)
  {
   if(!InpUseGapFilter) return true;
   if(sig + 1 >= ArraySize(r)) return true;
   double prevClose = r[sig + 1].close;
   double gap = MathAbs(r[sig].close - prevClose);
   if(atr <= 0) return true;
   return (gap / atr <= InpMaxGapATR);
  }

bool PassOvershootFilter(const MqlRates &r[], int sig, bool buySignal,
                          double upLevel, double dnLevel, double atr)
  {
   if(!InpUseOvershootFilter) return true;
   if(atr <= 0) return true;
   double overshoot;
   if(buySignal && dnLevel > 0)
      overshoot = (dnLevel - r[sig].low) / atr;   // how far wick pierced past dnLevel
   else if(!buySignal && upLevel > 0)
      overshoot = (r[sig].high - upLevel) / atr;
   else
      return true;
   return (overshoot <= InpMaxOvershootATR);
  }

// Require the ACTUAL swing leg (pivot_high - pivot_low) to be >= InpMinLegATR.
// The signal's down leg drives a long; its up leg drives a short.
bool PassMinLegFilter(bool buySignal, double upLegSize, double dnLegSize)
  {
   if(!InpUseMinLegATR) return true;
   double atr = GetATR();
   if(atr <= 0) return true;
   double legSize = buySignal ? dnLegSize : upLegSize;
   if(legSize <= 0) return true;             // no completed leg found -> don't block
   return (legSize / atr >= InpMinLegATR);
  }

double SpreadPoints()
  { return (sym.Ask() - sym.Bid()) / _Point; }

// Higher-timeframe trend gate: longs only with an up-trend, shorts with a down-trend.
bool PassTrendFilter(bool isBuy)
  {
   if(!InpUseTrendFilter || hTrend == INVALID_HANDLE) return true;
   double e[2];
   if(CopyBuffer(hTrend, 0, 1, 2, e) < 2) return false;   // e[1]=newer, e[0]=older
   double slope = e[1] - e[0];
   double px    = sym.Bid();
   bool up   = InpTrendStrict ? (slope > 0 && px > e[1]) : (slope > 0);
   bool down = InpTrendStrict ? (slope < 0 && px < e[1]) : (slope < 0);
   return isBuy ? up : down;
  }

//+------------------------------------------------------------------+
//| Broker-aware stop placement                                      |
//+------------------------------------------------------------------+
int MinStopPoints()
  {
   int sl = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   int fz = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL);
   int m  = MathMax(sl, fz);
   return (m > 0 ? m : 10);        // sensible fallback when broker reports 0
  }

// push SL out so it is at least the broker minimum distance from price
double BrokerizeStop(double price, double sl, bool isBuy)
  {
   double minDist = (MinStopPoints() + 1) * _Point;
   if(isBuy)  sl = MathMin(sl, price - minDist);
   else       sl = MathMax(sl, price + minDist);
   return sl;
  }
double BrokerizeTP(double price, double tp, bool isBuy)
  {
   double minDist = (MinStopPoints() + 1) * _Point;
   if(isBuy)  tp = MathMax(tp, price + minDist);
   else       tp = MathMin(tp, price - minDist);
   return tp;
  }

//+------------------------------------------------------------------+
//| Money management                                                 |
//+------------------------------------------------------------------+
double CalcLots(double slDistPrice)
  {
   if(!InpUseRiskPercent)
      return(NormalizeLots(InpFixedLots));

   double capital  = MathMin(AccountInfoDouble(ACCOUNT_EQUITY),
                             AccountInfoDouble(ACCOUNT_BALANCE));
   double riskMoney= capital * InpRiskPercent / 100.0;

   double tickVal  = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
   double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
   if(tickSize <= 0 || tickVal <= 0) return(0);

   double tickCount  = slDistPrice / tickSize;        // verified MQL5 formula
   double lossPerLot = tickCount * tickVal;
   if(lossPerLot <= 0) return(0);

   double lots = riskMoney / lossPerLot;
   return(NormalizeLots(lots));
  }

double NormalizeLots(double lots)
  {
   double minL = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double maxL = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);

   lots = MathMin(lots, InpMaxRiskLots);
   if(step > 0) lots = MathFloor(lots / step) * step;

   if(lots < minL)
     {
      if(InpSkipIfBelowMinLot) return(0);     // respect risk cap strictly
      lots = minL;                            // otherwise trade the minimum
     }
   lots = MathMin(lots, maxL);
   return(NormalizeDouble(lots, 2));
  }

bool HasEnoughMargin(ENUM_ORDER_TYPE type, double lots, double price)
  {
   double margin = 0.0;
   if(!OrderCalcMargin(type, _Symbol, lots, price, margin))
      return(true);                            // can't compute -> don't block
   double freeM = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
   return(margin * (1.0 + InpMarginBufferPct/100.0) <= freeM);
  }

double GetATR()
  {
   double v[1];
   if(CopyBuffer(hATR, 0, 1, 1, v) < 1) return(0);
   return(v[0]);
  }

//+------------------------------------------------------------------+
//| Initial-risk cache (per ticket)                                  |
//+------------------------------------------------------------------+
double InitRiskFor(ulong ticket, double curOpen, double curSL)
  {
   for(int i = 0; i < ArraySize(gTicket); i++)
      if(gTicket[i] == ticket) return(gInitRisk[i]);

   double risk = MathAbs(curOpen - curSL);    // first time we see it
   int n = ArraySize(gTicket);
   ArrayResize(gTicket, n+1); ArrayResize(gInitRisk, n+1);
   gTicket[n] = ticket; gInitRisk[n] = risk;
   return(risk);
  }
void ForgetClosedTickets()
  {
   for(int i = ArraySize(gTicket)-1; i >= 0; i--)
      if(!PositionSelectByTicket(gTicket[i]))
        {
         int last = ArraySize(gTicket)-1;
         gTicket[i]=gTicket[last]; gInitRisk[i]=gInitRisk[last];
         ArrayResize(gTicket, last); ArrayResize(gInitRisk, last);
        }
  }

//+------------------------------------------------------------------+
//| Position management: break-even + ATR trailing                   |
//+------------------------------------------------------------------+
void ManageOpenPositions()
  {
   ForgetClosedTickets();
   double atr = GetATR();
   double minDist = (MinStopPoints() + 1) * _Point;

   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket)) continue;
      if(PositionGetString(POSITION_SYMBOL) != _Symbol)  continue;
      if(PositionGetInteger(POSITION_MAGIC) != InpMagic)  continue;

      long   type = PositionGetInteger(POSITION_TYPE);
      double open = PositionGetDouble(POSITION_PRICE_OPEN);
      double sl   = PositionGetDouble(POSITION_SL);
      double tp   = PositionGetDouble(POSITION_TP);
      double bid  = sym.Bid(), ask = sym.Ask();
      double R    = InitRiskFor(ticket, open, (sl!=0.0?sl:open));
      if(R <= 0) continue;

      double newSL = sl;

      if(type == POSITION_TYPE_BUY)
        {
         if(InpUseBreakEven && bid - open >= InpBreakEvenR * R)
            newSL = MathMax(newSL, open + InpBreakEvenLockPts * _Point);
         if(InpUseTrailing && atr > 0 && bid - open >= InpTrailStartR * R)
            newSL = MathMax(newSL, bid - InpTrailATRmult * atr);
         newSL = MathMin(newSL, bid - minDist);          // keep broker-legal
         if(newSL > sl + _Point/2.0)
            ModifySL(ticket, newSL, tp);
        }
      else if(type == POSITION_TYPE_SELL)
        {
         double cand = sl;
         if(InpUseBreakEven && open - ask >= InpBreakEvenR * R)
            cand = (cand==0.0? open - InpBreakEvenLockPts*_Point
                             : MathMin(cand, open - InpBreakEvenLockPts*_Point));
         if(InpUseTrailing && atr > 0 && open - ask >= InpTrailStartR * R)
            cand = (cand==0.0? ask + InpTrailATRmult*atr
                             : MathMin(cand, ask + InpTrailATRmult*atr));
         if(cand > 0) cand = MathMax(cand, ask + minDist); // broker-legal
         if(cand > 0 && (sl == 0.0 || cand < sl - _Point/2.0))
            ModifySL(ticket, cand, tp);
        }
     }
  }

void ModifySL(ulong ticket, double newSL, double tp)
  {
   newSL = NormalizeDouble(newSL, _Digits);
   if(!trade.PositionModify(ticket, newSL, NormalizeDouble(tp,_Digits)))
      if(trade.ResultRetcode() != TRADE_RETCODE_DONE)
         PrintFormat("DabsPro: modify #%I64u failed retcode=%d (%s)",
                     ticket, trade.ResultRetcode(), trade.ResultRetcodeDescription());
  }

//+------------------------------------------------------------------+
//| Helpers                                                          |
//+------------------------------------------------------------------+
int PositionsForThisEA()
  {
   int cnt = 0;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket)) continue;
      if(PositionGetString(POSITION_SYMBOL) == _Symbol &&
         PositionGetInteger(POSITION_MAGIC) == InpMagic) cnt++;
     }
   return(cnt);
  }

void CloseOpposite(ENUM_POSITION_TYPE wantType)
  {
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket)) continue;
      if(PositionGetString(POSITION_SYMBOL) != _Symbol)  continue;
      if(PositionGetInteger(POSITION_MAGIC) != InpMagic)  continue;
      if((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE) != wantType)
         trade.PositionClose(ticket);
     }
  }
//+------------------------------------------------------------------+
