//+------------------------------------------------------------------+
//| FXP_PinBarReversal_EA.mq5
//| FXPIPS ONLINE - https://fxpips.online/mt5-expert-advisors/pin-bar-reversal-ea/
//|
//| MIT License. Copyright (c) 2026 FXPIPS ONLINE.
//| Permission is granted, free of charge, to use, copy, modify and
//| distribute this software, provided this notice is kept. The software
//| is provided "as is", without warranty of any kind.
//|
//| Risk disclaimer: educational tool, not financial advice. Trading
//| forex and CFDs on margin carries a high risk of loss. Test on a demo
//| account and in the Strategy Tester before any live use.
//+------------------------------------------------------------------+
#property copyright "FXPIPS ONLINE"
#property link      "https://fxpips.online/mt5-expert-advisors/pin-bar-reversal-ea/"
#property version   "1.00"
#property description "Trades pin bars that print at a swing high or low.\nStop beyond the wick, reward:risk target."

#include <Trade/Trade.mqh>

#define EA_TAG "FXP Pin Bar"

input group "Strategy"
input int    InpSwingLookback      = 20;    // Pin bar must make the lowest low / highest high of N bars
input double InpMinWickRatio       = 0.60;  // Rejection wick as a share of the bar range
input double InpMaxBodyRatio       = 0.33;  // Maximum body as a share of the bar range
input double InpMinRangeAtr        = 0.8;   // Minimum pin-bar range (x ATR)
input int    InpAtrPeriod          = 14;    // ATR period
input double InpSlBufferAtr        = 0.1;   // Stop buffer beyond the wick (x ATR)
input double InpMaxStopAtr         = 3.0;   // Skip if the stop is wider than this (x ATR)
input double InpRewardRisk         = 2.0;   // Take-profit as a multiple of the stop (0 = none)
input group "Risk"
input double InpRiskPercent        = 1.0;   // Risk per trade (% of balance)
input double InpFixedLots          = 0.0;   // Fixed lots (0 = use risk %)
input group "Filters"
input int    InpMaxSpreadPoints    = 30;    // Max spread in points (0 = off)
input int    InpStartHour          = 7;     // Trading start hour (server time)
input int    InpEndHour            = 20;    // Trading end hour (server time, exclusive)
input group "Trade management"
input bool   InpUseBreakEven       = true;  // Move stop to break-even
input double InpBreakEvenAtr       = 1.0;   // Break-even trigger (profit in x ATR)
input int    InpBreakEvenLockPoints = 20;   // Points locked beyond entry at break-even
input bool   InpUseTrailing        = false; // Use ATR trailing stop
input double InpTrailStartAtr      = 1.5;   // Trailing starts after profit of x ATR
input double InpTrailAtr           = 1.5;   // Trailing distance (x ATR)
input group "General"
input long   InpMagic              = 26110410; // Magic number
input int    InpSlippagePoints     = 10;    // Max slippage (points)

//--- server-time trading window; start = end means 24 hours
bool InTradingHours()
{
   if(InpStartHour == InpEndHour)
      return true;
   MqlDateTime dt;
   TimeToStruct(TimeCurrent(), dt);
   if(InpStartHour < InpEndHour)
      return (dt.hour >= InpStartHour && dt.hour < InpEndHour);
   return (dt.hour >= InpStartHour || dt.hour < InpEndHour);
}

bool HoursInputsOk()
{
   if(InpStartHour < 0 || InpStartHour > 23 || InpEndHour < 0 || InpEndHour > 23)
   {
      Print("Init failed: trading hours must be between 0 and 23.");
      return false;
   }
   return true;
}

//==================================================================
// Shared helpers: risk sizing, filters, order handling, management
//==================================================================
CTrade   g_trade;
int      g_atrHandle   = INVALID_HANDLE;
datetime g_lastBarTime = 0;

//--- set up CTrade and the ATR handle used for stops and management
bool InitCommon(const int atrPeriod)
{
   g_trade.SetExpertMagicNumber((ulong)InpMagic);
   g_trade.SetDeviationInPoints((ulong)MathMax(0, InpSlippagePoints));
   g_trade.SetTypeFillingBySymbol(_Symbol);
   g_trade.SetMarginMode();
   g_trade.LogLevel(LOG_LEVEL_ERRORS);
   if(InpRiskPercent <= 0.0 && InpFixedLots <= 0.0)
   {
      Print("Init failed: set InpRiskPercent above 0 or InpFixedLots above 0.");
      return false;
   }
   if(InpFixedLots <= 0.0 && InpRiskPercent > 5.0)
      PrintFormat("Warning: %.2f%% risk per trade is very aggressive.", InpRiskPercent);
   if(atrPeriod < 1)
   {
      Print("Init failed: ATR period must be at least 1.");
      return false;
   }
   g_atrHandle = iATR(_Symbol, _Period, atrPeriod);
   if(g_atrHandle == INVALID_HANDLE)
   {
      PrintFormat("Init failed: cannot create ATR(%d), error %d.", atrPeriod, GetLastError());
      return false;
   }
   return true;
}

void DeinitCommon()
{
   if(g_atrHandle != INVALID_HANDLE)
      IndicatorRelease(g_atrHandle);
   g_atrHandle = INVALID_HANDLE;
}

//--- true once per new bar of the chart timeframe
bool IsNewBar()
{
   datetime t = iTime(_Symbol, _Period, 0);
   if(t == 0 || t == g_lastBarTime)
      return false;
   g_lastBarTime = t;
   return true;
}

//--- read one value of an indicator buffer
bool GetBuf(const int handle, const int buffer, const int shift, double &value)
{
   double tmp[];
   if(CopyBuffer(handle, buffer, shift, 1, tmp) != 1)
      return false;
   value = tmp[0];
   return (value != EMPTY_VALUE);
}

bool GetAtr(const int shift, double &atr)
{
   return (GetBuf(g_atrHandle, 0, shift, atr) && atr > 0.0);
}

double NormPrice(const double price)
{
   double tick = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
   if(tick <= 0.0)
      return NormalizeDouble(price, _Digits);
   return NormalizeDouble(MathRound(price / tick) * tick, _Digits);
}

string Px(const double price)
{
   return DoubleToString(price, _Digits);
}

//--- minimum distance between price and SL/TP/pending entry (stops and freeze levels)
double MinStopDistance()
{
   long stops  = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   long freeze = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL);
   long lvl    = (stops > freeze) ? stops : freeze;
   return (double)(lvl + 1) * _Point;
}

bool IsMine(const string symbol, const long magic)
{
   return (symbol == _Symbol && magic == InpMagic);
}

int CountPositions()
{
   int n = 0;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      if(PositionGetTicket(i) == 0)
         continue;
      if(IsMine(PositionGetString(POSITION_SYMBOL), PositionGetInteger(POSITION_MAGIC)))
         n++;
   }
   return n;
}

bool IsPendingType(const long type)
{
   return (type == ORDER_TYPE_BUY_STOP || type == ORDER_TYPE_SELL_STOP ||
           type == ORDER_TYPE_BUY_LIMIT || type == ORDER_TYPE_SELL_LIMIT ||
           type == ORDER_TYPE_BUY_STOP_LIMIT || type == ORDER_TYPE_SELL_STOP_LIMIT);
}

int CountPendings()
{
   int n = 0;
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      if(OrderGetTicket(i) == 0)
         continue;
      if(IsMine(OrderGetString(ORDER_SYMBOL), OrderGetInteger(ORDER_MAGIC)) &&
         IsPendingType(OrderGetInteger(ORDER_TYPE)))
         n++;
   }
   return n;
}

//--- positions plus pending orders of this EA on this symbol
int CountExposure()
{
   return CountPositions() + CountPendings();
}

void DeletePendings(const string reason)
{
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      ulong ticket = OrderGetTicket(i);
      if(ticket == 0)
         continue;
      if(!IsMine(OrderGetString(ORDER_SYMBOL), OrderGetInteger(ORDER_MAGIC)) ||
         !IsPendingType(OrderGetInteger(ORDER_TYPE)))
         continue;
      if(g_trade.OrderDelete(ticket) && g_trade.ResultRetcode() == TRADE_RETCODE_DONE)
         PrintFormat("Deleted pending order #%I64u: %s.", ticket, reason);
      else
         PrintFormat("Could not delete pending order #%I64u (%s): retcode %u %s.", ticket, reason,
                     g_trade.ResultRetcode(), g_trade.ResultRetcodeDescription());
   }
}

//--- delete pending orders older than maxAgeSeconds (expiry handled by the EA itself)
void DeleteExpiredPendings(const int maxAgeSeconds)
{
   if(maxAgeSeconds <= 0)
      return;
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      ulong ticket = OrderGetTicket(i);
      if(ticket == 0)
         continue;
      if(!IsMine(OrderGetString(ORDER_SYMBOL), OrderGetInteger(ORDER_MAGIC)) ||
         !IsPendingType(OrderGetInteger(ORDER_TYPE)))
         continue;
      datetime setup = (datetime)OrderGetInteger(ORDER_TIME_SETUP);
      if(TimeCurrent() - setup < maxAgeSeconds)
         continue;
      if(g_trade.OrderDelete(ticket) && g_trade.ResultRetcode() == TRADE_RETCODE_DONE)
         PrintFormat("Deleted pending order #%I64u: expired.", ticket);
      else
         PrintFormat("Could not delete expired order #%I64u: retcode %u %s.", ticket,
                     g_trade.ResultRetcode(), g_trade.ResultRetcodeDescription());
   }
}

//--- close this EA's positions: dir 1 = longs, -1 = shorts, 0 = all
void ClosePositions(const int dir, const string reason)
{
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0)
         continue;
      if(!IsMine(PositionGetString(POSITION_SYMBOL), PositionGetInteger(POSITION_MAGIC)))
         continue;
      long type = PositionGetInteger(POSITION_TYPE);
      if(dir == 1 && type != POSITION_TYPE_BUY)
         continue;
      if(dir == -1 && type != POSITION_TYPE_SELL)
         continue;
      if(g_trade.PositionClose(ticket) && g_trade.ResultRetcode() == TRADE_RETCODE_DONE)
         PrintFormat("Closed position #%I64u: %s.", ticket, reason);
      else
         PrintFormat("Could not close position #%I64u (%s): retcode %u %s.", ticket, reason,
                     g_trade.ResultRetcode(), g_trade.ResultRetcodeDescription());
   }
}

//--- bars elapsed since the oldest open position of this EA (-1 when flat)
int BarsSinceEntry()
{
   datetime oldest = 0;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      if(PositionGetTicket(i) == 0)
         continue;
      if(!IsMine(PositionGetString(POSITION_SYMBOL), PositionGetInteger(POSITION_MAGIC)))
         continue;
      datetime t = (datetime)PositionGetInteger(POSITION_TIME);
      if(oldest == 0 || t < oldest)
         oldest = t;
   }
   if(oldest == 0)
      return -1;
   return iBarShift(_Symbol, _Period, oldest, false);
}

//--- one trade at a time: once a position exists, cancel remaining pending orders.
//--- If two positions ever exist (both pending orders filled in a gap), close the newer one.
void EnforceSinglePosition()
{
   int pos = CountPositions();
   if(pos == 0)
      return;
   if(CountPendings() > 0)
      DeletePendings("a position is open, cancelling the remaining order");
   if(pos < 2)
      return;
   ulong newest = 0;
   long  newestTime = 0;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0)
         continue;
      if(!IsMine(PositionGetString(POSITION_SYMBOL), PositionGetInteger(POSITION_MAGIC)))
         continue;
      long t = PositionGetInteger(POSITION_TIME_MSC);
      if(t >= newestTime)
      {
         newestTime = t;
         newest = ticket;
      }
   }
   if(newest > 0)
   {
      if(g_trade.PositionClose(newest) && g_trade.ResultRetcode() == TRADE_RETCODE_DONE)
         PrintFormat("Closed position #%I64u: only one position per symbol is allowed.", newest);
      else
         PrintFormat("Could not close extra position #%I64u: retcode %u %s.", newest,
                     g_trade.ResultRetcode(), g_trade.ResultRetcodeDescription());
   }
}

void OnTradeTransaction(const MqlTradeTransaction &trans, const MqlTradeRequest &request,
                        const MqlTradeResult &result)
{
   if(trans.type == TRADE_TRANSACTION_DEAL_ADD && trans.symbol == _Symbol)
      EnforceSinglePosition();
}

bool SpreadOk(const string label)
{
   if(InpMaxSpreadPoints <= 0)
      return true;
   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   int spread = (int)MathRound((ask - bid) / _Point);
   if(spread > InpMaxSpreadPoints)
   {
      PrintFormat("%s skipped: spread %d points is above the maximum of %d.", label, spread, InpMaxSpreadPoints);
      return false;
   }
   return true;
}

bool TradingAllowed(const string label)
{
   if(!MQLInfoInteger(MQL_TRADE_ALLOWED) || !TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
   {
      PrintFormat("%s skipped: algo trading is disabled in the terminal or EA settings.", label);
      return false;
   }
   long mode = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_MODE);
   if(mode != SYMBOL_TRADE_MODE_FULL)
   {
      PrintFormat("%s skipped: the symbol does not allow opening trades in both directions now.", label);
      return false;
   }
   return true;
}

//--- checks shared by every entry: exposure, permissions, hours, spread
bool PreTradeChecks(const string label)
{
   if(CountExposure() > 0)
   {
      PrintFormat("%s skipped: a position or pending order of this EA is already open.", label);
      return false;
   }
   if(!TradingAllowed(label))
      return false;
   if(!InTradingHours())
   {
      PrintFormat("%s skipped: outside the trading hours.", label);
      return false;
   }
   return SpreadOk(label);
}

//--- lot size from stop distance (price units) and the risk percentage
double CalcLots(const double stopDistance)
{
   double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   double step   = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
   if(step <= 0.0)
      step = minLot;
   if(minLot <= 0.0 || step <= 0.0)
   {
      Print("Trade skipped: the symbol reports no valid volume limits.");
      return 0.0;
   }
   double lots;
   if(InpFixedLots > 0.0)
      lots = InpFixedLots;
   else
   {
      double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
      double tickSize  = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
      if(tickValue <= 0.0 || tickSize <= 0.0 || stopDistance <= 0.0)
      {
         Print("Trade skipped: invalid tick value, tick size or stop distance.");
         return 0.0;
      }
      double riskMoney  = AccountInfoDouble(ACCOUNT_BALANCE) * InpRiskPercent / 100.0;
      double lossPerLot = stopDistance / tickSize * tickValue;
      lots = riskMoney / lossPerLot;
      if(lots < minLot)
      {
         if(minLot * lossPerLot > 2.0 * riskMoney)
         {
            PrintFormat("Trade skipped: the minimum lot %.2f would risk %.2f %s, more than twice the risk budget of %.2f.",
                        minLot, minLot * lossPerLot, AccountInfoString(ACCOUNT_CURRENCY), riskMoney);
            return 0.0;
         }
         PrintFormat("Note: risk budget allows %.4f lots; using the minimum lot %.2f.", lots, minLot);
         lots = minLot;
      }
   }
   lots = MathFloor(lots / step + 1e-9) * step;
   if(lots < minLot)
      lots = minLot;
   if(lots > maxLot)
      lots = maxLot;
   int volDigits = (int)MathMax(0.0, MathCeil(-MathLog10(step) - 1e-9));
   return NormalizeDouble(lots, volDigits);
}

bool MarginOk(const ENUM_ORDER_TYPE type, const double lots, const double price)
{
   double margin = 0.0;
   if(!OrderCalcMargin(type, _Symbol, lots, price, margin))
      return true;
   if(margin > AccountInfoDouble(ACCOUNT_MARGIN_FREE))
   {
      PrintFormat("Trade skipped: %.2f lots need %.2f margin, free margin is %.2f.",
                  lots, margin, AccountInfoDouble(ACCOUNT_MARGIN_FREE));
      return false;
   }
   return true;
}

bool ResultOk()
{
   uint rc = g_trade.ResultRetcode();
   return (rc == TRADE_RETCODE_DONE || rc == TRADE_RETCODE_DONE_PARTIAL || rc == TRADE_RETCODE_PLACED);
}

//--- market order; a stop-loss is mandatory, tp = 0 means no take-profit
bool OpenMarket(const bool isBuy, double sl, double tp, const string tag)
{
   MqlTick tick;
   if(!SymbolInfoTick(_Symbol, tick))
   {
      Print("Trade skipped: no current tick.");
      return false;
   }
   double price   = isBuy ? tick.ask : tick.bid;
   double minDist = MinStopDistance();
   if(sl <= 0.0 || (isBuy && sl >= price) || (!isBuy && sl <= price))
   {
      PrintFormat("Trade skipped: stop-loss %s is not on the correct side of price %s.", Px(sl), Px(price));
      return false;
   }
   if(tp > 0.0 && ((isBuy && tp <= price) || (!isBuy && tp >= price)))
   {
      PrintFormat("Trade skipped: take-profit %s is already behind price %s.", Px(tp), Px(price));
      return false;
   }
   if(MathAbs(price - sl) < minDist)
      sl = isBuy ? price - minDist : price + minDist;
   if(tp > 0.0 && MathAbs(tp - price) < minDist)
      tp = isBuy ? price + minDist : price - minDist;
   sl = NormPrice(sl);
   if(tp > 0.0)
      tp = NormPrice(tp);
   double lots = CalcLots(MathAbs(price - sl));
   if(lots <= 0.0)
      return false;
   if(!MarginOk(isBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL, lots, price))
      return false;
   bool sent = isBuy ? g_trade.Buy(lots, _Symbol, price, sl, tp, tag)
                     : g_trade.Sell(lots, _Symbol, price, sl, tp, tag);
   if(!sent || !ResultOk())
   {
      PrintFormat("%s order failed: retcode %u %s.", isBuy ? "Buy" : "Sell",
                  g_trade.ResultRetcode(), g_trade.ResultRetcodeDescription());
      return false;
   }
   PrintFormat("Opened %s %.2f lots near %s, SL %s, TP %s.", isBuy ? "buy" : "sell", lots, Px(price), Px(sl),
               tp > 0.0 ? Px(tp) : "none");
   return true;
}

//--- buy stop / sell stop with a mandatory stop-loss (expiry is managed by the EA)
bool PlaceStopOrder(const bool isBuy, double entry, double sl, double tp, const string tag)
{
   MqlTick tick;
   if(!SymbolInfoTick(_Symbol, tick))
   {
      Print("Pending order skipped: no current tick.");
      return false;
   }
   string label = isBuy ? "Buy stop" : "Sell stop";
   double minDist = MinStopDistance();
   entry = NormPrice(entry);
   if(isBuy && entry - tick.ask < minDist)
   {
      PrintFormat("%s skipped: entry %s is not far enough above the ask %s.", label, Px(entry), Px(tick.ask));
      return false;
   }
   if(!isBuy && tick.bid - entry < minDist)
   {
      PrintFormat("%s skipped: entry %s is not far enough below the bid %s.", label, Px(entry), Px(tick.bid));
      return false;
   }
   if(sl <= 0.0 || (isBuy && sl >= entry) || (!isBuy && sl <= entry))
   {
      PrintFormat("%s skipped: stop-loss %s is on the wrong side of entry %s.", label, Px(sl), Px(entry));
      return false;
   }
   if(MathAbs(entry - sl) < minDist)
      sl = isBuy ? entry - minDist : entry + minDist;
   if(tp > 0.0 && MathAbs(tp - entry) < minDist)
      tp = isBuy ? entry + minDist : entry - minDist;
   sl = NormPrice(sl);
   if(tp > 0.0)
      tp = NormPrice(tp);
   double lots = CalcLots(MathAbs(entry - sl));
   if(lots <= 0.0)
      return false;
   if(!MarginOk(isBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL, lots, entry))
      return false;
   long expModes = SymbolInfoInteger(_Symbol, SYMBOL_EXPIRATION_MODE);
   ENUM_ORDER_TYPE_TIME tt = ((expModes & SYMBOL_EXPIRATION_GTC) != 0) ? ORDER_TIME_GTC : ORDER_TIME_DAY;
   bool sent = isBuy ? g_trade.BuyStop(lots, entry, _Symbol, sl, tp, tt, 0, tag)
                     : g_trade.SellStop(lots, entry, _Symbol, sl, tp, tt, 0, tag);
   if(!sent || !ResultOk())
   {
      PrintFormat("%s failed: retcode %u %s.", label, g_trade.ResultRetcode(), g_trade.ResultRetcodeDescription());
      return false;
   }
   PrintFormat("Placed %s %.2f lots at %s, SL %s, TP %s.", label, lots, Px(entry), Px(sl),
               tp > 0.0 ? Px(tp) : "none");
   return true;
}

//--- break-even and ATR trailing stop, run on every tick
void ManagePositions()
{
   if(!InpUseBreakEven && !InpUseTrailing)
      return;
   if(CountPositions() == 0)
      return;
   double atr;
   if(!GetAtr(1, atr))
      return;
   MqlTick tick;
   if(!SymbolInfoTick(_Symbol, tick))
      return;
   double minDist = MinStopDistance();
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0)
         continue;
      if(!IsMine(PositionGetString(POSITION_SYMBOL), PositionGetInteger(POSITION_MAGIC)))
         continue;
      long   type  = PositionGetInteger(POSITION_TYPE);
      double open  = PositionGetDouble(POSITION_PRICE_OPEN);
      double sl    = PositionGetDouble(POSITION_SL);
      double tp    = PositionGetDouble(POSITION_TP);
      double newSl = sl;
      if(type == POSITION_TYPE_BUY)
      {
         double profit = tick.bid - open;
         if(InpUseBreakEven && profit >= InpBreakEvenAtr * atr)
         {
            double be = open + InpBreakEvenLockPoints * _Point;
            if(newSl == 0.0 || be > newSl)
               newSl = be;
         }
         if(InpUseTrailing && profit >= InpTrailStartAtr * atr)
         {
            double tr = tick.bid - InpTrailAtr * atr;
            if(newSl == 0.0 || tr > newSl)
               newSl = tr;
         }
         newSl = NormPrice(newSl);
         if(newSl <= sl + _Point * 0.5 || tick.bid - newSl < minDist)
            continue;
      }
      else
      {
         double profit = open - tick.ask;
         if(InpUseBreakEven && profit >= InpBreakEvenAtr * atr)
         {
            double be = open - InpBreakEvenLockPoints * _Point;
            if(newSl == 0.0 || be < newSl)
               newSl = be;
         }
         if(InpUseTrailing && profit >= InpTrailStartAtr * atr)
         {
            double tr = tick.ask + InpTrailAtr * atr;
            if(newSl == 0.0 || tr < newSl)
               newSl = tr;
         }
         newSl = NormPrice(newSl);
         if((sl > 0.0 && newSl >= sl - _Point * 0.5) || newSl - tick.ask < minDist)
            continue;
      }
      if(!g_trade.PositionModify(ticket, newSl, tp) || g_trade.ResultRetcode() != TRADE_RETCODE_DONE)
         PrintFormat("Stop update failed for #%I64u: retcode %u %s.", ticket,
                     g_trade.ResultRetcode(), g_trade.ResultRetcodeDescription());
   }
}


int OnInit()
{
   if(!HoursInputsOk())
      return INIT_PARAMETERS_INCORRECT;
   if(InpSwingLookback < 3 || InpMinWickRatio <= 0.0 || InpMinWickRatio >= 1.0 || InpMaxBodyRatio <= 0.0 ||
      InpMaxBodyRatio >= 1.0 || InpMinRangeAtr < 0.0 || InpSlBufferAtr < 0.0 || InpMaxStopAtr <= 0.0 || InpRewardRisk < 0.0)
   {
      Print("Init failed: check the pin-bar shape, swing and stop settings.");
      return INIT_PARAMETERS_INCORRECT;
   }
   if(!InitCommon(InpAtrPeriod))
      return INIT_FAILED;
   return INIT_SUCCEEDED;
}

void OnDeinit(const int reason)
{
   DeinitCommon();
}

void OnTick()
{
   EnforceSinglePosition();
   ManagePositions();
   if(!IsNewBar())
      return;

   double atr;
   if(!GetAtr(1, atr))
   {
      Print("Waiting for indicator data.");
      return;
   }
   if(Bars(_Symbol, _Period) < InpSwingLookback + 2)
      return;
   double o = iOpen(_Symbol, _Period, 1), h = iHigh(_Symbol, _Period, 1);
   double l = iLow(_Symbol, _Period, 1),  c = iClose(_Symbol, _Period, 1);
   double range = h - l;
   if(range <= 0.0)
      return;
   double body      = MathAbs(c - o);
   double lowerWick = MathMin(o, c) - l;
   double upperWick = h - MathMax(o, c);
   bool   shapeOk   = (body <= InpMaxBodyRatio * range);

   int signal = 0;
   if(shapeOk && lowerWick >= InpMinWickRatio * range &&
      iLowest(_Symbol, _Period, MODE_LOW, InpSwingLookback, 1) == 1)
      signal = 1;    // bullish pin at a swing low
   else if(shapeOk && upperWick >= InpMinWickRatio * range &&
           iHighest(_Symbol, _Period, MODE_HIGH, InpSwingLookback, 1) == 1)
      signal = -1;   // bearish pin at a swing high
   if(signal == 0)
      return;
   string label = (signal > 0) ? "Bullish pin bar" : "Bearish pin bar";

   if(range < InpMinRangeAtr * atr)
   {
      PrintFormat("%s skipped: range %.1f points is below %.2f x ATR.", label, range / _Point, InpMinRangeAtr);
      return;
   }
   if(!PreTradeChecks(label))
      return;

   bool   isBuy = (signal > 0);
   double price = isBuy ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double sl    = isBuy ? l - InpSlBufferAtr * atr : h + InpSlBufferAtr * atr;
   double stop  = MathAbs(price - sl);
   if(stop > InpMaxStopAtr * atr)
   {
      PrintFormat("%s skipped: stop of %.1f points is wider than %.1f x ATR.", label, stop / _Point, InpMaxStopAtr);
      return;
   }
   double tp = 0.0;
   if(InpRewardRisk > 0.0)
      tp = isBuy ? price + InpRewardRisk * stop : price - InpRewardRisk * stop;
   OpenMarket(isBuy, sl, tp, EA_TAG);
}
