//+------------------------------------------------------------------+
//| FXP_KeltnerBreakout_EA.mq5
//| FXPIPS ONLINE - https://fxpips.online/mt5-expert-advisors/keltner-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/keltner-breakout-ea/"
#property version   "1.00"
#property description "Trades closes outside the Keltner Channel confirmed by momentum.\nATR stop-loss, reward:risk target, optional midline exit."

#include <Trade/Trade.mqh>

#define EA_TAG "FXP Keltner Breakout"

input group "Strategy"
input int    InpKcEmaPeriod        = 20;    // Keltner midline EMA period
input int    InpKcAtrPeriod        = 10;    // Keltner ATR period
input double InpKcMultiplier       = 2.0;   // Keltner width (x ATR)
input int    InpMomentumPeriod     = 10;    // Momentum period
input double InpMomentumMin        = 100.1; // Buy needs momentum above this, sell below 200 minus this
input bool   InpRequireBodyDir     = true;  // Breakout candle must close in the trade direction
input int    InpAtrPeriod          = 14;    // ATR period for the stop
input double InpSlAtrMult          = 1.5;   // Stop-loss distance (x ATR)
input double InpRewardRisk         = 2.0;   // Take-profit as a multiple of the stop (0 = none)
input bool   InpExitAtMidline      = true;  // Close when a bar closes back across the midline
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              = 26110404; // Magic number
input int    InpSlippagePoints     = 10;    // Max slippage (points)

int g_emaHandle   = INVALID_HANDLE;
int g_kcAtrHandle = INVALID_HANDLE;
int g_momHandle   = INVALID_HANDLE;

//--- 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(InpKcEmaPeriod < 2 || InpKcAtrPeriod < 1 || InpKcMultiplier <= 0.0 || InpMomentumPeriod < 1 ||
      InpMomentumMin < 100.0 || InpSlAtrMult <= 0.0 || InpRewardRisk < 0.0)
   {
      Print("Init failed: check the Keltner, momentum and stop settings.");
      return INIT_PARAMETERS_INCORRECT;
   }
   if(!InitCommon(InpAtrPeriod))
      return INIT_FAILED;
   g_emaHandle   = iMA(_Symbol, _Period, InpKcEmaPeriod, 0, MODE_EMA, PRICE_CLOSE);
   g_kcAtrHandle = iATR(_Symbol, _Period, InpKcAtrPeriod);
   g_momHandle   = iMomentum(_Symbol, _Period, InpMomentumPeriod, PRICE_CLOSE);
   if(g_emaHandle == INVALID_HANDLE || g_kcAtrHandle == INVALID_HANDLE || g_momHandle == INVALID_HANDLE)
   {
      PrintFormat("Init failed: cannot create indicator handles, error %d.", GetLastError());
      return INIT_FAILED;
   }
   return INIT_SUCCEEDED;
}

void OnDeinit(const int reason)
{
   if(g_emaHandle != INVALID_HANDLE)   IndicatorRelease(g_emaHandle);
   if(g_kcAtrHandle != INVALID_HANDLE) IndicatorRelease(g_kcAtrHandle);
   if(g_momHandle != INVALID_HANDLE)   IndicatorRelease(g_momHandle);
   DeinitCommon();
}

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

   double ema1, ema2, kat1, kat2, mom1, atr;
   if(!GetBuf(g_emaHandle, 0, 1, ema1) || !GetBuf(g_emaHandle, 0, 2, ema2) ||
      !GetBuf(g_kcAtrHandle, 0, 1, kat1) || !GetBuf(g_kcAtrHandle, 0, 2, kat2) ||
      !GetBuf(g_momHandle, 0, 1, mom1) || !GetAtr(1, atr))
   {
      Print("Waiting for indicator data.");
      return;
   }
   double up1 = ema1 + InpKcMultiplier * kat1, lo1 = ema1 - InpKcMultiplier * kat1;
   double up2 = ema2 + InpKcMultiplier * kat2, lo2 = ema2 - InpKcMultiplier * kat2;
   double c1 = iClose(_Symbol, _Period, 1), c2 = iClose(_Symbol, _Period, 2), o1 = iOpen(_Symbol, _Period, 1);

   if(InpExitAtMidline && CountPositions() > 0)
   {
      if(c1 < ema1)
         ClosePositions(1, "close back below the Keltner midline");
      if(c1 > ema1)
         ClosePositions(-1, "close back above the Keltner midline");
   }

   int signal = 0;
   if(c1 > up1 && c2 <= up2)
      signal = 1;
   else if(c1 < lo1 && c2 >= lo2)
      signal = -1;
   if(signal == 0)
      return;
   string label = (signal > 0) ? "Buy breakout" : "Sell breakout";

   if(signal > 0 && mom1 <= InpMomentumMin)
   {
      PrintFormat("%s skipped: momentum %.2f is not above %.2f.", label, mom1, InpMomentumMin);
      return;
   }
   if(signal < 0 && mom1 >= 200.0 - InpMomentumMin)
   {
      PrintFormat("%s skipped: momentum %.2f is not below %.2f.", label, mom1, 200.0 - InpMomentumMin);
      return;
   }
   if(InpRequireBodyDir && ((signal > 0 && c1 <= o1) || (signal < 0 && c1 >= o1)))
   {
      PrintFormat("%s skipped: the breakout candle closed against the trade direction.", label);
      return;
   }
   if(!PreTradeChecks(label))
      return;

   bool   isBuy = (signal > 0);
   double price = isBuy ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double stop  = InpSlAtrMult * atr;
   double sl    = isBuy ? price - stop : price + stop;
   double tp    = 0.0;
   if(InpRewardRisk > 0.0)
      tp = isBuy ? price + InpRewardRisk * stop : price - InpRewardRisk * stop;
   OpenMarket(isBuy, sl, tp, EA_TAG);
}
