//+------------------------------------------------------------------+
//| FXP_KamaTrend_EA.mq5
//| KAMA Trend Expert Advisor for MetaTrader 5
//| FXPIPS ONLINE - https://fxpips.online/mt5-expert-advisors/kama-trend-ea/
//+------------------------------------------------------------------+
#property copyright "FXPIPS ONLINE"
#property link      "https://fxpips.online/mt5-expert-advisors/kama-trend-ea/"
#property version   "1.00"
#property description "Trades closes across Kaufman's Adaptive Moving Average when the"
#property description "efficiency ratio shows a directional market. ATR stop, R target, risk-% sizing."
/*
   MIT License

   Copyright (c) 2026 FXPIPS ONLINE

   Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
   associated documentation files (the "Software"), to deal in the Software without restriction,
   including without limitation the rights to use, copy, modify, merge, publish, distribute,
   sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
   furnished to do so, subject to the following conditions:

   The above copyright notice and this permission notice shall be included in all copies or
   substantial portions of the Software.

   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
   NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
   DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
   OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

   RISK DISCLAIMER
   Educational tool - not financial advice. Trading forex and CFDs on margin involves substantial
   risk of loss and is not suitable for every investor. Past or simulated performance does not
   guarantee future results. Test on a demo account and in the Strategy Tester before any live use.

   Design rules: every order carries a stop-loss; one position per symbol and magic number;
   no martingale, grid, averaging down, hedging or lot multipliers after losses.
*/
#include <Trade/Trade.mqh>

//--- Risk and money management
input group "Risk and money management"
input ulong  InpMagic               = 2611007;   // Magic number
input double InpRiskPercent         = 1.0;       // Risk per trade (% of balance)
input double InpFixedLots           = 0.0;       // Fixed lots (0 = use risk %)

//--- Trade filters
input group "Trade filters"
input int    InpMaxSpreadPoints     = 20;        // Maximum spread (points, 0 = off)
input int    InpStartHour           = 7;         // Trading start hour (server time, 0-23)
input int    InpEndHour             = 20;        // Trading end hour (server time, exclusive; same as start = 24h)
input int    InpSlippagePoints      = 10;        // Maximum slippage (points)

//--- Trade management
input group "Trade management"
input bool   InpUseBreakEven        = true ;      // Move stop to break-even
input double InpBreakEvenAtR        = 1.0;       // Break-even trigger (profit in R multiples)
input int    InpBreakEvenLockPoints = 20;        // Points locked beyond the entry at break-even
input bool   InpUseTrailing         = false;     // Use trailing stop (ATR distance)
input double InpTrailAtrMult        = 2.0;       // Trailing distance (x ATR)
input int    InpTrailStepPoints     = 10;        // Minimum stop improvement per modification (points)

//--- Volatility
input group "Volatility (ATR)"
input int    InpAtrPeriod           = 14;        // ATR period (stops and trailing)

//--- Strategy
input group "Strategy: KAMA"
input int    InpKamaPeriod          = 10;        // KAMA efficiency period
input int    InpKamaFast            = 2;         // KAMA fast EMA period
input int    InpKamaSlow            = 30;        // KAMA slow EMA period
input int    InpErPeriod            = 10;        // Efficiency ratio period (filter)
input double InpMinEfficiency       = 0.30;      // Minimum efficiency ratio (0-1)
input double InpStopAtrMult         = 1.5;       // Stop-loss distance (x ATR)
input double InpTakeProfitR         = 2.0;       // Take-profit (R multiple, 0 = none)
input bool   InpExitOnOppositeCross = true;      // Close when price closes back across KAMA

//--- Globals
CTrade   g_trade;
int      g_atrHandle   = INVALID_HANDLE;
datetime g_lastBarTime = 0;
bool     g_exitLong    = false;
bool     g_exitShort   = false;
string   g_exitReason  = "";
string   g_eaName      = "FXP KAMA Trend";

//+------------------------------------------------------------------+
//| Small helpers                                                    |
//+------------------------------------------------------------------+
string DirText(const int dir)
  {
   return (dir > 0) ? "Buy" : "Sell";
  }

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

void ReleaseHandle(int &handle)
  {
   if(handle != INVALID_HANDLE)
      IndicatorRelease(handle);
   handle = INVALID_HANDLE;
  }

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

bool LoadSeries(const int handle, const int buffer, const int count, double &values[])
  {
   if(handle == INVALID_HANDLE)
      return false;
   ArraySetAsSeries(values, true);
   return (CopyBuffer(handle, buffer, 0, count, values) == count);
  }

bool LoadRates(const int count, MqlRates &rates[])
  {
   ArraySetAsSeries(rates, true);
   return (CopyRates(_Symbol, _Period, 0, count, rates) == count);
  }

double AtrValue(const int shift)
  {
   double values[];
   if(g_atrHandle == INVALID_HANDLE)
      return 0.0;
   if(CopyBuffer(g_atrHandle, 0, shift, 1, values) != 1)
      return 0.0;
   return values[0];
  }

bool IsNewBar()
  {
   datetime barTime = iTime(_Symbol, _Period, 0);
   if(barTime == 0 || barTime == g_lastBarTime)
      return false;
   g_lastBarTime = barTime;
   return true;
  }

//--- selects our position (symbol + magic) when it returns true
bool SelectOurPosition()
  {
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0)
         continue;
      if(PositionGetString(POSITION_SYMBOL) != _Symbol)
         continue;
      if((ulong)PositionGetInteger(POSITION_MAGIC) != InpMagic)
         continue;
      return true;
     }
   return false;
  }

bool IsTradingHour()
  {
   if(InpStartHour == InpEndHour)
      return true;
   MqlDateTime now;
   TimeToStruct(TimeCurrent(), now);
   if(InpStartHour < InpEndHour)
      return (now.hour >= InpStartHour && now.hour < InpEndHour);
   return (now.hour >= InpStartHour || now.hour < InpEndHour);
  }

int CurrentSpreadPoints()
  {
   MqlTick tick;
   if(!SymbolInfoTick(_Symbol, tick))
      return INT_MAX;
   return (int)MathRound((tick.ask - tick.bid) / _Point);
  }

double MinStopDistance()
  {
   double stopsLevel  = (double)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   double freezeLevel = (double)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL);
   return MathMax(stopsLevel, freezeLevel) * _Point;
  }

//+------------------------------------------------------------------+
//| Position size from stop distance                                 |
//+------------------------------------------------------------------+
double CalculateLots(const int dir, const double entry, const double sl)
  {
   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: symbol volume settings are not available.");
      return 0.0;
     }

   double lots = 0.0;
   if(InpFixedLots > 0.0)
      lots = InpFixedLots;
   else
     {
      double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
      double tickSize  = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
      double distance  = MathAbs(entry - sl);
      if(tickValue <= 0.0 || tickSize <= 0.0 || distance <= 0.0)
        {
         Print("Trade skipped: tick value, tick size or stop distance is invalid.");
         return 0.0;
        }
      double riskMoney  = AccountInfoDouble(ACCOUNT_BALANCE) * InpRiskPercent / 100.0;
      double lossPerLot = distance / tickSize * tickValue;
      lots = riskMoney / lossPerLot;
      if(lots < minLot)
        {
         double minLotRisk = minLot * lossPerLot;
         if(minLotRisk > 2.0 * riskMoney)
           {
            PrintFormat("Trade skipped: the minimum lot (%.2f) would risk %.2f %s, more than twice the %.2f risk budget.",
                        minLot, minLotRisk, AccountInfoString(ACCOUNT_CURRENCY), riskMoney);
            return 0.0;
           }
         PrintFormat("Note: risk budget allows %.3f lots; using the broker minimum of %.2f lots.", lots, minLot);
         lots = minLot;
        }
     }

   lots = MathFloor(lots / step + 1e-9) * step;
   if(lots < minLot)
      lots = minLot;
   if(lots > maxLot)
      lots = maxLot;
   int volumeDigits = (int)MathMax(0.0, MathCeil(-MathLog10(step) - 1e-9));
   lots = NormalizeDouble(lots, volumeDigits);

   double margin = 0.0;
   ENUM_ORDER_TYPE orderType = (dir > 0) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
   if(OrderCalcMargin(orderType, _Symbol, lots, entry, margin) &&
      margin > AccountInfoDouble(ACCOUNT_MARGIN_FREE))
     {
      PrintFormat("Trade skipped: %.2f lots need %.2f margin but only %.2f is free.",
                  lots, margin, AccountInfoDouble(ACCOUNT_MARGIN_FREE));
      return 0.0;
     }
   return lots;
  }

//+------------------------------------------------------------------+
//| Stop / target builders used by the strategy section              |
//+------------------------------------------------------------------+
bool BuildStopsFromAtr(const int dir, const double entry, const double atrMult, const double rewardR,
                       double &sl, double &tp)
  {
   double atr = AtrValue(1);
   if(atr <= 0.0)
     {
      PrintFormat("%s signal skipped: ATR value is not available yet.", DirText(dir));
      return false;
     }
   double distance = atr * atrMult;
   sl = entry - dir * distance;
   tp = (rewardR > 0.0) ? entry + dir * distance * rewardR : 0.0;
   return true;
  }

bool BuildStopsFromLevel(const int dir, const double entry, const double level, const double rewardR,
                         const double maxStopAtr, double &sl, double &tp)
  {
   double distance = (dir > 0) ? entry - level : level - entry;
   if(level <= 0.0 || distance <= 0.0)
     {
      PrintFormat("%s signal skipped: stop level %s is on the wrong side of the entry price %s.",
                  DirText(dir), PriceText(level), PriceText(entry));
      return false;
     }
   if(maxStopAtr > 0.0)
     {
      double atr = AtrValue(1);
      if(atr > 0.0 && distance > atr * maxStopAtr)
        {
         PrintFormat("%s signal skipped: stop distance %s is wider than %.1f x ATR (%s).",
                     DirText(dir), PriceText(distance), maxStopAtr, PriceText(atr * maxStopAtr));
         return false;
        }
     }
   sl = level;
   tp = (rewardR > 0.0) ? entry + dir * distance * rewardR : 0.0;
   return true;
  }

bool LoadCloses(const int count, double &closes[])
  {
   ArraySetAsSeries(closes, true);
   return (CopyClose(_Symbol, _Period, 0, count, closes) == count);
  }

//+------------------------------------------------------------------+
//| STRATEGY SECTION                                                 |
//+------------------------------------------------------------------+
int g_kamaHandle = INVALID_HANDLE;

bool InitStrategy()
  {
   if(InpErPeriod < 2)
     {
      Print("The efficiency ratio period must be at least 2.");
      return false;
     }
   g_kamaHandle = iAMA(_Symbol, _Period, InpKamaPeriod, InpKamaFast, InpKamaSlow, 0, PRICE_CLOSE);
   if(g_kamaHandle == INVALID_HANDLE)
     {
      Print("Could not create the adaptive moving average.");
      return false;
     }
   return true;
  }

void DeinitStrategy()
  {
   ReleaseHandle(g_kamaHandle);
  }

//--- Kaufman efficiency ratio: net change / sum of absolute bar-to-bar changes
double EfficiencyRatio(const double &closes[], const int shift)
  {
   double change = MathAbs(closes[shift] - closes[shift + InpErPeriod]);
   double noise  = 0.0;
   for(int k = shift; k < shift + InpErPeriod; k++)
      noise += MathAbs(closes[k] - closes[k + 1]);
   return (noise > 0.0) ? change / noise : 0.0;
  }

bool ReadSignal(int &signal)
  {
   double kama[], closes[];
   int count = InpErPeriod + 3;
   if(!LoadSeries(g_kamaHandle, 0, 3, kama) || !LoadCloses(count, closes))
      return false;

   signal = 0;
   bool crossUp   = (closes[1] > kama[1] && closes[2] <= kama[2]);
   bool crossDown = (closes[1] < kama[1] && closes[2] >= kama[2]);

   if(InpExitOnOppositeCross)
     {
      if(crossDown) { g_exitLong  = true; g_exitReason = "close below KAMA"; }
      if(crossUp)   { g_exitShort = true; g_exitReason = "close above KAMA"; }
     }
   if(!crossUp && !crossDown)
      return true;

   double er = EfficiencyRatio(closes, 1);
   if(er < InpMinEfficiency)
     {
      PrintFormat("%s KAMA cross ignored: efficiency ratio %.2f is below %.2f.",
                  crossUp ? "Bullish" : "Bearish", er, InpMinEfficiency);
      return true;
     }
   signal = crossUp ? 1 : -1;
   return true;
  }

bool GetStops(const int dir, const double entry, double &sl, double &tp)
  {
   return BuildStopsFromAtr(dir, entry, InpStopAtrMult, InpTakeProfitR, sl, tp);
  }

//--- ATR trailing stop: candidate stop level for the open position
bool GetTrailLevel(const bool isLong, const double price, double &level)
  {
   double atr = AtrValue(1);
   if(atr <= 0.0)
      return false;
   level = isLong ? price - atr * InpTrailAtrMult : price + atr * InpTrailAtrMult;
   return true;
  }

//+------------------------------------------------------------------+
//| Trading                                                          |
//+------------------------------------------------------------------+
void TryOpen(const int dir)
  {
   if(!IsTradingHour())
     {
      PrintFormat("%s signal skipped: outside trading hours (%02d:00-%02d:00 server time).",
                  DirText(dir), InpStartHour, InpEndHour);
      return;
     }
   int spread = CurrentSpreadPoints();
   if(InpMaxSpreadPoints > 0 && spread > InpMaxSpreadPoints)
     {
      PrintFormat("%s signal skipped: spread %d points is above the %d-point limit.",
                  DirText(dir), spread, InpMaxSpreadPoints);
      return;
     }
   if(SymbolInfoInteger(_Symbol, SYMBOL_TRADE_MODE) == SYMBOL_TRADE_MODE_DISABLED)
     {
      PrintFormat("%s signal skipped: trading is disabled for %s.", DirText(dir), _Symbol);
      return;
     }
   MqlTick tick;
   if(!SymbolInfoTick(_Symbol, tick))
     {
      PrintFormat("%s signal skipped: no current price for %s.", DirText(dir), _Symbol);
      return;
     }

   double entry = (dir > 0) ? tick.ask : tick.bid;
   double sl = 0.0, tp = 0.0;
   if(!GetStops(dir, entry, sl, tp))
      return;
   sl = NormalizePrice(sl);
   tp = (tp > 0.0) ? NormalizePrice(tp) : 0.0;

   double minDist = MinStopDistance();
   if(dir > 0)
     {
      if(sl <= 0.0 || sl >= tick.bid - minDist)
        {
         PrintFormat("Buy signal skipped: stop-loss %s is too close to the price (Bid %s, broker minimum %d points).",
                     PriceText(sl), PriceText(tick.bid), (int)MathRound(minDist / _Point));
         return;
        }
      if(tp > 0.0 && tp <= tick.bid + minDist)
        {
         PrintFormat("Buy signal skipped: take-profit %s is too close to the price.", PriceText(tp));
         return;
        }
     }
   else
     {
      if(sl <= tick.ask + minDist)
        {
         PrintFormat("Sell signal skipped: stop-loss %s is too close to the price (Ask %s, broker minimum %d points).",
                     PriceText(sl), PriceText(tick.ask), (int)MathRound(minDist / _Point));
         return;
        }
      if(tp > 0.0 && tp >= tick.ask - minDist)
        {
         PrintFormat("Sell signal skipped: take-profit %s is too close to the price.", PriceText(tp));
         return;
        }
     }

   double lots = CalculateLots(dir, entry, sl);
   if(lots <= 0.0)
      return;

   bool sent = (dir > 0) ? g_trade.Buy(lots, _Symbol, 0.0, sl, tp, g_eaName)
                         : g_trade.Sell(lots, _Symbol, 0.0, sl, tp, g_eaName);
   uint retcode = g_trade.ResultRetcode();
   if(!sent || (retcode != TRADE_RETCODE_DONE && retcode != TRADE_RETCODE_DONE_PARTIAL &&
                retcode != TRADE_RETCODE_PLACED))
     {
      PrintFormat("%s order failed: retcode %u (%s).", DirText(dir), retcode, g_trade.ResultRetcodeDescription());
      return;
     }
   PrintFormat("%s %.2f lots opened at %s, stop-loss %s, take-profit %s.", DirText(dir), lots,
               PriceText(g_trade.ResultPrice()), PriceText(sl), (tp > 0.0) ? PriceText(tp) : "none");
  }

void CloseOurPosition(const string reason)
  {
   if(!SelectOurPosition())
      return;
   ulong ticket = (ulong)PositionGetInteger(POSITION_TICKET);
   if(!g_trade.PositionClose(ticket, InpSlippagePoints))
     {
      PrintFormat("Close failed for #%I64u: retcode %u (%s).", ticket, g_trade.ResultRetcode(),
                  g_trade.ResultRetcodeDescription());
      return;
     }
   PrintFormat("Position #%I64u closed: %s.", ticket, reason);
  }

//+------------------------------------------------------------------+
//| Break-even and trailing stop (runs on every tick)                |
//+------------------------------------------------------------------+
void ManageOpenPosition()
  {
   if(!InpUseBreakEven && !InpUseTrailing)
      return;
   if(!SelectOurPosition())
      return;

   ulong  ticket = (ulong)PositionGetInteger(POSITION_TICKET);
   bool   isLong = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY);
   double open   = PositionGetDouble(POSITION_PRICE_OPEN);
   double sl     = PositionGetDouble(POSITION_SL);
   double tp     = PositionGetDouble(POSITION_TP);
   MqlTick tick;
   if(!SymbolInfoTick(_Symbol, tick))
      return;
   double price   = isLong ? tick.bid : tick.ask;
   double minDist = MinStopDistance();
   double step    = MathMax(1, InpTrailStepPoints) * _Point;
   double newSl   = sl;

   if(InpUseBreakEven && sl > 0.0)
     {
      if(isLong && sl < open && price - open >= (open - sl) * InpBreakEvenAtR)
         newSl = MathMax(newSl, open + InpBreakEvenLockPoints * _Point);
      if(!isLong && sl > open && open - price >= (sl - open) * InpBreakEvenAtR)
         newSl = MathMin(newSl, open - InpBreakEvenLockPoints * _Point);
     }

   if(InpUseTrailing)
     {
      double level = 0.0;
      if(GetTrailLevel(isLong, price, level) && level > 0.0)
        {
         if(isLong)
            newSl = (newSl <= 0.0) ? level : MathMax(newSl, level);
         else
            newSl = (newSl <= 0.0) ? level : MathMin(newSl, level);
        }
     }

   if(newSl <= 0.0)
      return;
   if(isLong)
     {
      double maxAllowed = price - minDist - _Point;
      if(newSl > maxAllowed)
         newSl = maxAllowed;
      newSl = NormalizePrice(newSl);
      if(sl > 0.0 && newSl < sl + step)
         return;
     }
   else
     {
      double minAllowed = price + minDist + _Point;
      if(newSl < minAllowed)
         newSl = minAllowed;
      newSl = NormalizePrice(newSl);
      if(sl > 0.0 && newSl > sl - step)
         return;
     }

   if(!g_trade.PositionModify(ticket, newSl, tp))
      PrintFormat("Stop update failed for #%I64u: retcode %u (%s).", ticket, g_trade.ResultRetcode(),
                  g_trade.ResultRetcodeDescription());
  }

//+------------------------------------------------------------------+
//| Expert events                                                    |
//+------------------------------------------------------------------+
int OnInit()
  {
   if(InpRiskPercent <= 0.0 && InpFixedLots <= 0.0)
     {
      Print("Set InpRiskPercent or InpFixedLots above zero.");
      return INIT_PARAMETERS_INCORRECT;
     }
   if(InpStartHour < 0 || InpStartHour > 23 || InpEndHour < 0 || InpEndHour > 23)
     {
      Print("Trading hours must be between 0 and 23.");
      return INIT_PARAMETERS_INCORRECT;
     }
   if(InpAtrPeriod < 1)
     {
      Print("ATR period must be at least 1.");
      return INIT_PARAMETERS_INCORRECT;
     }

   g_trade.SetExpertMagicNumber(InpMagic);
   g_trade.SetDeviationInPoints(InpSlippagePoints);
   g_trade.SetTypeFillingBySymbol(_Symbol);

   g_atrHandle = iATR(_Symbol, _Period, InpAtrPeriod);
   if(g_atrHandle == INVALID_HANDLE)
     {
      Print("Could not create the ATR indicator.");
      return INIT_FAILED;
     }
   if(!InitStrategy())
      return INIT_FAILED;
   g_lastBarTime = 0;
   return INIT_SUCCEEDED;
  }

void OnDeinit(const int reason)
  {
   DeinitStrategy();
   ReleaseHandle(g_atrHandle);
  }

void OnTick()
  {
   ManageOpenPosition();

   if(!IsNewBar())
      return;

   g_exitLong   = false;
   g_exitShort  = false;
   g_exitReason = "";
   int signal = 0;
   if(!ReadSignal(signal))
     {
      g_lastBarTime = 0;   // indicator data not ready: try again on the next tick
      return;
     }

   bool hasPosition = SelectOurPosition();
   if(hasPosition)
     {
      bool isLong = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY);
      if((isLong && g_exitLong) || (!isLong && g_exitShort))
        {
         CloseOurPosition(g_exitReason);
         hasPosition = SelectOurPosition();
        }
     }

   if(signal == 0)
      return;
   if(hasPosition)
     {
      PrintFormat("%s signal skipped: a position with magic %I64u is already open on %s.",
                  DirText(signal), InpMagic, _Symbol);
      return;
     }
   TryOpen(signal);
  }
//+------------------------------------------------------------------+
