//+------------------------------------------------------------------+
//| FXP_AsianRangeBreakout_EA.mq5
//| FXPIPS ONLINE - https://fxpips.online/mt5-expert-advisors/asian-range-breakout-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/asian-range-breakout-ea/"
#property version   "1.00"
#property description "Brackets the Asian session range with stop orders for London.\nStop at the opposite side, unfilled order cancelled."

#include <Trade/Trade.mqh>

#define EA_TAG "FXP Asian Breakout"

input group "Session (hours in GMT)"
input int    InpServerGmtOffset    = 2;     // Broker server time minus GMT (hours)
input int    InpAsiaStartHourGMT   = 0;     // Asian range start (GMT hour)
input int    InpAsiaEndHourGMT     = 7;     // Asian range end = London entry window start (GMT hour)
input int    InpEntryEndHourGMT    = 11;    // Entry window end; unfilled orders deleted (GMT hour)
input bool   InpCloseAtHour        = true;  // Close an open trade at the close hour
input int    InpCloseHourGMT       = 20;    // Close hour (GMT)
input group "Strategy"
input int    InpMinRangePoints     = 100;   // Minimum Asian range (points)
input int    InpMaxRangePoints     = 700;   // Maximum Asian range (points)
input int    InpMinRangeBars       = 60;    // Minimum M1 bars inside the range window
input int    InpEntryBufferPoints  = 20;    // Entry and stop buffer beyond the range (points)
input double InpRewardRisk         = 1.0;   // Take-profit as a multiple of the stop (0 = none)
input int    InpAtrPeriod          = 14;    // ATR period (break-even and trailing)
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 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              = 26110412; // Magic number
input int    InpSlippagePoints     = 10;    // Max slippage (points)

long g_setupDay = -1;

//--- session helpers: session inputs are in GMT, InpServerGmtOffset converts to server time
long GmtSeconds(const datetime serverTime)
{
   return (long)serverTime - (long)InpServerGmtOffset * 3600;
}

long GmtDayId(const datetime serverTime)
{
   return GmtSeconds(serverTime) / 86400;
}

int GmtMinuteOfDay(const datetime serverTime)
{
   return (int)((GmtSeconds(serverTime) % 86400) / 60);
}

//--- server time of a GMT minute-of-day on the GMT day that contains serverTime
datetime ServerTimeAt(const datetime serverTime, const int gmtMinute)
{
   long dayStart = GmtDayId(serverTime) * 86400;
   return (datetime)(dayStart + (long)gmtMinute * 60 + (long)InpServerGmtOffset * 3600);
}

//--- true when minute m lies in [fromMin, toMin), windows may wrap past midnight
bool MinuteInWindow(const int m, const int fromMin, const int toMin)
{
   if(fromMin == toMin)
      return false;
   if(fromMin < toMin)
      return (m >= fromMin && m < toMin);
   return (m >= fromMin || m < toMin);
}

//--- high/low of M1 bars in [fromTime, toTime) server time
bool RangeHighLow(const datetime fromTime, const datetime toTime, double &hi, double &lo, int &bars)
{
   hi = 0.0;
   lo = 0.0;
   bars = 0;
   if(toTime <= fromTime)
      return false;
   MqlRates r[];
   int n = CopyRates(_Symbol, PERIOD_M1, fromTime, toTime - 1, r);
   if(n <= 0)
      return false;
   for(int i = 0; i < n; i++)
   {
      if(r[i].time < fromTime || r[i].time >= toTime)
         continue;
      if(bars == 0 || r[i].high > hi)
         hi = r[i].high;
      if(bars == 0 || r[i].low < lo)
         lo = r[i].low;
      bars++;
   }
   return (bars > 0);
}

bool ValidHourMinute(const int h, const int m)
{
   return (h >= 0 && h <= 23 && m >= 0 && m <= 59);
}

//--- true if this EA opened a trade since the given server time (survives restarts)
bool TradedSince(const datetime from)
{
   if(!HistorySelect(from, TimeCurrent() + 60))
      return false;
   for(int i = HistoryDealsTotal() - 1; i >= 0; i--)
   {
      ulong deal = HistoryDealGetTicket(i);
      if(deal == 0)
         continue;
      if(HistoryDealGetString(deal, DEAL_SYMBOL) == _Symbol && HistoryDealGetInteger(deal, DEAL_MAGIC) == InpMagic &&
         HistoryDealGetInteger(deal, DEAL_ENTRY) == DEAL_ENTRY_IN)
         return true;
   }
   return false;
}

//--- true if an open position of this EA was opened on an earlier GMT day
bool OpenedBeforeToday()
{
   long today = GmtDayId(TimeCurrent());
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      if(PositionGetTicket(i) == 0)
         continue;
      if(PositionGetString(POSITION_SYMBOL) != _Symbol || PositionGetInteger(POSITION_MAGIC) != InpMagic)
         continue;
      if(GmtDayId((datetime)PositionGetInteger(POSITION_TIME)) < today)
         return true;
   }
   return false;
}

//--- the London entry window: from the Asian range end to the entry end hour (GMT)
bool InTradingHours()
{
   return MinuteInWindow(GmtMinuteOfDay(TimeCurrent()), InpAsiaEndHourGMT * 60, InpEntryEndHourGMT * 60);
}

//==================================================================
// 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(InpServerGmtOffset < -12 || InpServerGmtOffset > 14 ||
      !ValidHourMinute(InpAsiaStartHourGMT, 0) || !ValidHourMinute(InpAsiaEndHourGMT, 0) ||
      !ValidHourMinute(InpEntryEndHourGMT, 0) || !ValidHourMinute(InpCloseHourGMT, 0) ||
      InpAsiaStartHourGMT == InpAsiaEndHourGMT || InpEntryEndHourGMT <= InpAsiaEndHourGMT)
   {
      Print("Init failed: check the session hours (entry end must be later than the Asian end on the same GMT day).");
      return INIT_PARAMETERS_INCORRECT;
   }
   if(InpCloseAtHour && (InpCloseHourGMT < InpEntryEndHourGMT))
   {
      Print("Init failed: the close hour must not be earlier than the entry window end.");
      return INIT_PARAMETERS_INCORRECT;
   }
   if(InpMinRangePoints < 0 || InpMaxRangePoints <= InpMinRangePoints || InpEntryBufferPoints < 0 || InpRewardRisk < 0.0)
   {
      Print("Init failed: check the range and buffer settings.");
      return INIT_PARAMETERS_INCORRECT;
   }
   if(!InitCommon(InpAtrPeriod))
      return INIT_FAILED;
   return INIT_SUCCEEDED;
}

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

void OnTick()
{
   EnforceSinglePosition();
   ManagePositions();

   int m = GmtMinuteOfDay(TimeCurrent());
   if(CountPendings() > 0 && CountPositions() == 0 && !InTradingHours())
      DeletePendings("London entry window closed");
   if(InpCloseAtHour && CountPositions() > 0 && (m >= InpCloseHourGMT * 60 || OpenedBeforeToday()))
      ClosePositions(0, "session close hour reached");

   if(!IsNewBar())
      return;

   datetime now = TimeCurrent();
   long day = GmtDayId(now);
   if(!InTradingHours() || day == g_setupDay)
      return;
   g_setupDay = day;   // one setup per day, even when it is skipped
   string label = "Asian range setup";

   datetime asiaEnd   = ServerTimeAt(now, InpAsiaEndHourGMT * 60);
   datetime asiaStart = ServerTimeAt(now, InpAsiaStartHourGMT * 60);
   if(InpAsiaStartHourGMT > InpAsiaEndHourGMT)
      asiaStart -= 86400;   // range starts on the previous GMT day

   double hi, lo;
   int bars;
   if(!RangeHighLow(asiaStart, asiaEnd, hi, lo, bars) || bars < InpMinRangeBars)
   {
      PrintFormat("%s skipped: only %d M1 bars in the Asian window.", label, bars);
      return;
   }
   double rangePts = (hi - lo) / _Point;
   if(rangePts < InpMinRangePoints || rangePts > InpMaxRangePoints)
   {
      PrintFormat("%s skipped: range of %.0f points is outside %d-%d.", label, rangePts, InpMinRangePoints, InpMaxRangePoints);
      return;
   }
   if(TradedSince(asiaEnd))
   {
      PrintFormat("%s skipped: a trade was already taken today.", label);
      return;
   }
   if(!PreTradeChecks(label))
      return;

   double buf       = InpEntryBufferPoints * _Point;
   double buyEntry  = hi + buf, sellEntry = lo - buf;
   double stop      = buyEntry - sellEntry;   // stop sits at the opposite side of the range
   double buyTp     = (InpRewardRisk > 0.0) ? buyEntry + InpRewardRisk * stop : 0.0;
   double sellTp    = (InpRewardRisk > 0.0) ? sellEntry - InpRewardRisk * stop : 0.0;
   PrintFormat("%s: high %s, low %s (%.0f points).", label, Px(hi), Px(lo), rangePts);
   PlaceStopOrder(true, buyEntry, sellEntry, buyTp, EA_TAG);
   PlaceStopOrder(false, sellEntry, buyEntry, sellTp, EA_TAG);
}
