//+------------------------------------------------------------------+
//|                                                    SwordEA.mq5    |
//|                                                                  |
//|  SWORD (Doubling Mode) — Momentum Breakout EA                    |
//|                                                                  |
//|  ACCOUNT TYPE:                                                   |
//|    Personal cash account — PRIMARY use case                      |
//|    FTMO funded account   — OPTIONAL (set InpFTMOMode = true)    |
//|    FTMO challenge        — NOT RECOMMENDED (too aggressive)      |
//|                                                                  |
//|  STRATEGY:                                                       |
//|    Donchian Channel breakout — price breaks above the highest    |
//|    high (or below the lowest low) of the last N bars.            |
//|    ATR Expansion Filter — only enters when volatility is         |
//|    actually expanding (real move, not noise).                    |
//|    Optional Pyramid — adds a second position once the first      |
//|    trade is in profit by 1x ATR. Max 2 positions total.         |
//|                                                                  |
//|  BEST PAIRS: GBPJPY, USDJPY, XAUUSD (trending, high momentum)  |
//|  TIMEFRAME:  M30 (default). M5 for more signals, more noise.   |
//+------------------------------------------------------------------+
#property copyright "Educational template - Sword Edition"
#property version   "1.00"
#include <Trade/Trade.mqh>

//==================================================================
//  INPUTS
//==================================================================
input group "=== FTMO Safety (enable for funded accounts) ==="
input bool   InpFTMOMode           = false;       // Enable FTMO rule tracking
input double InpInitialBalance     = 100000.0;    // Challenge starting balance (FTMO mode only)
input double InpDailyLossLimitPct  = 4.5;         // Daily loss limit % (FTMO=5%, buffer at 4.5)
input double InpTotalLossFloorPct  = 9.0;         // Total loss floor % (FTMO=10%, buffer at 9.0)
input double InpProfitTargetPct    = 9.5;         // Stop new entries at this profit % (Phase1=10%)

input group "=== Strategy ==="
input ENUM_TIMEFRAMES InpTimeframe = PERIOD_M30;  // Timeframe (attach chart to match)
input int    InpBreakoutPeriod     = 20;          // Bars to look back for high/low breakout
input int    InpATRPeriod          = 14;          // ATR period
input int    InpATRMAPeriod        = 8;           // ATR smoothing (expansion filter)

input group "=== Risk ==="
input double InpRiskPercent        = 2.0;         // Risk per trade % (cash). Use 1.0 for FTMO.
input double InpATR_SL_Mult        = 1.5;         // Stop loss = ATR x this
input double InpATR_TP_Mult        = 3.0;         // Take profit = ATR x this
input double InpATR_Trail_Mult     = 1.5;         // Trailing stop = ATR x this
input bool   InpUsePyramid         = true;        // Add 2nd position when 1st is 1x ATR in profit
input bool   InpUseTrailing        = true;        // Use ATR trailing stop

input group "=== Misc ==="
input long   InpMagic              = 30260601;    // Magic number
input int    InpMaxSlippage        = 30;

//==================================================================
//  GLOBALS
//==================================================================
CTrade   trade;
int      hATR = INVALID_HANDLE;

datetime g_lastBarTime    = 0;
datetime g_currentDay     = 0;
double   g_dayStartBalance= 0.0;
bool     g_tradingDisabled= false;
string   g_disableReason  = "";
double   g_dailyLossPct   = 0.0;
double   g_totalLossPct   = 0.0;
double   g_profitPct      = 0.0;
bool     g_profitTargetHit= false;
datetime g_lastTickLocal  = 0;

//==================================================================
//  INIT / DEINIT
//==================================================================
int OnInit()
{
   trade.SetExpertMagicNumber(InpMagic);
   trade.SetDeviationInPoints(InpMaxSlippage);
   trade.SetTypeFillingBySymbol(_Symbol);

   hATR = iATR(_Symbol, InpTimeframe, InpATRPeriod);
   if(hATR == INVALID_HANDLE)
   { Print("ERROR: ATR handle failed."); return(INIT_FAILED); }

   g_currentDay       = StartOfDay(TimeCurrent());
   g_dayStartBalance  = AccountInfoDouble(ACCOUNT_BALANCE);
   g_lastTickLocal    = TimeLocal();

   Print("SwordEA started. FTMO Mode: ", InpFTMOMode ? "ON" : "OFF");
   EventSetTimer(1);
   UpdateDashboard();
   return(INIT_SUCCEEDED);
}

void OnDeinit(const int reason)
{
   EventKillTimer();
   if(hATR != INVALID_HANDLE) IndicatorRelease(hATR);
   Comment("");
}

//==================================================================
//  MAIN TICK
//==================================================================
void OnTick()
{
   g_lastTickLocal = TimeLocal();

   // Day rollover
   datetime today = StartOfDay(TimeCurrent());
   if(today != g_currentDay)
   {
      g_currentDay      = today;
      g_dayStartBalance = AccountInfoDouble(ACCOUNT_BALANCE);
      if(g_disableReason == "DAILY_LOSS")
      { g_tradingDisabled = false; g_disableReason = ""; }
   }

   // FTMO metrics
   if(InpFTMOMode)
   {
      double equity  = AccountInfoDouble(ACCOUNT_EQUITY);
      g_dailyLossPct = (g_dayStartBalance > 0.0)
                       ? (g_dayStartBalance - equity) / g_dayStartBalance * 100.0 : 0.0;
      g_totalLossPct = (InpInitialBalance > 0.0)
                       ? (InpInitialBalance - equity) / InpInitialBalance * 100.0  : 0.0;
      g_profitPct    = (InpInitialBalance > 0.0)
                       ? (equity - InpInitialBalance) / InpInitialBalance * 100.0  : 0.0;

      if(!g_tradingDisabled)
      {
         if(g_dailyLossPct >= InpDailyLossLimitPct)
         { CloseAllPositions(); g_tradingDisabled=true; g_disableReason="DAILY_LOSS";
           Print("Daily loss limit hit: ", DoubleToString(g_dailyLossPct,2), "%"); }
         else if(g_totalLossPct >= InpTotalLossFloorPct)
         { CloseAllPositions(); g_tradingDisabled=true; g_disableReason="MAX_LOSS";
           Print("Total loss floor hit: ", DoubleToString(g_totalLossPct,2), "%"); }
         else if(g_profitPct >= InpProfitTargetPct && !g_profitTargetHit)
         { g_profitTargetHit=true;
           Print("Profit target reached: ", DoubleToString(g_profitPct,2), "%"); }
      }
   }

   if(InpUseTrailing) ManageTrailing();
   UpdateDashboard();
   if(g_tradingDisabled || g_profitTargetHit) return;

   // New bar only
   datetime barTime = iTime(_Symbol, InpTimeframe, 0);
   if(barTime == g_lastBarTime) return;
   g_lastBarTime = barTime;

   CheckPyramid();
   CheckForEntry();
}

void OnTimer() { UpdateDashboard(); }

//==================================================================
//  BREAKOUT ENTRY
//==================================================================
void CheckForEntry()
{
   if(CountPositions() >= 1) return;  // already in a trade

   double atr[];
   ArraySetAsSeries(atr, true);
   if(CopyBuffer(hATR, 0, 0, InpATRMAPeriod+2, atr) < InpATRMAPeriod+2) return;

   double atrNow = atr[1];
   // ATR expansion: current ATR must be above its own recent average
   double atrSum = 0.0;
   for(int i = 1; i <= InpATRMAPeriod; i++) atrSum += atr[i];
   double atrAvg = atrSum / InpATRMAPeriod;
   if(atrNow <= atrAvg) return;  // no real momentum — skip

   // Donchian high/low of the last InpBreakoutPeriod CLOSED bars
   int   hiBar = iHighest(_Symbol, InpTimeframe, MODE_HIGH, InpBreakoutPeriod, 1);
   int   loBar = iLowest (_Symbol, InpTimeframe, MODE_LOW,  InpBreakoutPeriod, 1);
   double highestHigh = iHigh(_Symbol, InpTimeframe, hiBar);
   double lowestLow   = iLow (_Symbol, InpTimeframe, loBar);

   double closeNow = iClose(_Symbol, InpTimeframe, 1);

   bool buyBreak  = (closeNow > highestHigh);
   bool sellBreak = (closeNow < lowestLow);

   if(buyBreak)  OpenTrade(ORDER_TYPE_BUY,  atrNow);
   else if(sellBreak) OpenTrade(ORDER_TYPE_SELL, atrNow);
}

//==================================================================
//  PYRAMID — add 2nd position once 1st is 1x ATR in profit
//==================================================================
void CheckPyramid()
{
   if(!InpUsePyramid)     return;
   if(CountPositions() != 1) return;  // only add to exactly 1 open position

   double atr[];
   ArraySetAsSeries(atr, true);
   if(CopyBuffer(hATR, 0, 0, 2, atr) < 2) return;
   double atrNow = atr[1];

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

      long   pType  = PositionGetInteger(POSITION_TYPE);
      double openP  = PositionGetDouble(POSITION_PRICE_OPEN);
      double bid    = SymbolInfoDouble(_Symbol, SYMBOL_BID);
      double ask    = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      double profit = (pType == POSITION_TYPE_BUY) ? bid - openP : openP - ask;

      if(profit >= atrNow)  // in profit by 1x ATR — add the pyramid
         OpenTrade((ENUM_ORDER_TYPE)pType, atrNow);
   }
}

void OpenTrade(ENUM_ORDER_TYPE type, double atrVal)
{
   double ask    = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double bid    = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double slDist = atrVal * InpATR_SL_Mult;
   double tpDist = atrVal * InpATR_TP_Mult;
   double entry, sl, tp;

   if(type == ORDER_TYPE_BUY)
   { entry=ask; sl=entry-slDist; tp=entry+tpDist; }
   else
   { entry=bid; sl=entry+slDist; tp=entry-tpDist; }

   double lots = CalcLotSize(slDist);
   if(lots <= 0.0) return;

   bool ok = (type == ORDER_TYPE_BUY)
             ? trade.Buy(lots, _Symbol, 0.0, NormalizeDouble(sl,_Digits), NormalizeDouble(tp,_Digits), "SwordEA")
             : trade.Sell(lots, _Symbol, 0.0, NormalizeDouble(sl,_Digits), NormalizeDouble(tp,_Digits), "SwordEA");

   if(!ok)
      Print("Order failed: ", trade.ResultRetcode(), " - ", trade.ResultRetcodeDescription());
}

double CalcLotSize(double slDistPrice)
{
   double balance   = AccountInfoDouble(ACCOUNT_BALANCE);
   double riskMoney = balance * InpRiskPercent / 100.0;
   double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
   double tickSize  = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
   if(tickSize<=0.0 || tickValue<=0.0) return(0.0);
   double lossPerLot = (slDistPrice / tickSize) * tickValue;
   if(lossPerLot<=0.0) return(0.0);
   double lots    = riskMoney / lossPerLot;
   double minLot  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double maxLot  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
   if(lotStep>0.0) lots = MathFloor(lots/lotStep)*lotStep;
   if(lots<minLot){ Print("Lot below minimum. Skipping."); return(0.0); }
   if(lots>maxLot) lots=maxLot;
   return(NormalizeDouble(lots,2));
}

void ManageTrailing()
{
   double atr[]; ArraySetAsSeries(atr,true);
   if(CopyBuffer(hATR,0,0,2,atr)<2) return;
   double trailDist = atr[1] * InpATR_Trail_Mult;

   for(int i=PositionsTotal()-1; i>=0; i--)
   {
      ulong ticket=PositionGetTicket(i);
      if(ticket==0) continue;
      if(PositionGetInteger(POSITION_MAGIC)!=InpMagic) continue;
      if(PositionGetString(POSITION_SYMBOL)!=_Symbol)  continue;
      long   pType=PositionGetInteger(POSITION_TYPE);
      double openP=PositionGetDouble(POSITION_PRICE_OPEN);
      double curSL=PositionGetDouble(POSITION_SL);
      double curTP=PositionGetDouble(POSITION_TP);
      double bid=SymbolInfoDouble(_Symbol,SYMBOL_BID);
      double ask=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
      if(pType==POSITION_TYPE_BUY)
      { double newSL=NormalizeDouble(bid-trailDist,_Digits);
        if(newSL>curSL && newSL>openP) trade.PositionModify(ticket,newSL,curTP); }
      else
      { double newSL=NormalizeDouble(ask+trailDist,_Digits);
        if((curSL==0.0||newSL<curSL) && newSL<openP) trade.PositionModify(ticket,newSL,curTP); }
   }
}

int CountPositions()
{
   int count=0;
   for(int i=PositionsTotal()-1; i>=0; i--)
   {
      ulong t=PositionGetTicket(i);
      if(t==0) continue;
      if(PositionGetInteger(POSITION_MAGIC)==InpMagic &&
         PositionGetString(POSITION_SYMBOL)==_Symbol) count++;
   }
   return(count);
}

void CloseAllPositions()
{
   for(int i=PositionsTotal()-1; i>=0; i--)
   {
      ulong t=PositionGetTicket(i);
      if(t==0) continue;
      if(PositionGetInteger(POSITION_MAGIC)==InpMagic &&
         PositionGetString(POSITION_SYMBOL)==_Symbol)
         trade.PositionClose(t);
   }
}

void UpdateDashboard()
{
   double balance=AccountInfoDouble(ACCOUNT_BALANCE);
   double equity =AccountInfoDouble(ACCOUNT_EQUITY);
   int    secs   =(int)(TimeLocal()-g_lastTickLocal);
   string status = g_tradingDisabled ? (g_disableReason=="DAILY_LOSS"
                   ? "!!! DAILY LOSS LIMIT !!!" : "!!! MAX LOSS HIT !!!")
                   : g_profitTargetHit ? "*** PROFIT TARGET — NO NEW ENTRIES ***" : "ACTIVE";
   string ftmoLine = InpFTMOMode
      ? "  Daily Loss:   "+DoubleToString(MathMax(g_dailyLossPct,0.0),2)+
        "% / "+DoubleToString(InpDailyLossLimitPct,1)+"%  |  "+
        "Total Loss: "+DoubleToString(MathMax(g_totalLossPct,0.0),2)+
        "% / "+DoubleToString(InpTotalLossFloorPct,1)+"%\n"+
        "  Profit:       "+DoubleToString(g_profitPct,2)+
        "% / "+DoubleToString(InpProfitTargetPct,1)+"%\n"
      : "  FTMO Mode:    OFF\n";

   Comment(
      "============= SwordEA (Momentum Breakout) =============\n"+
      "Symbol: "+_Symbol+"   |   TF: "+EnumToString(InpTimeframe)+
      "   <-- attach chart to THIS\n"+
      "Status: "+status+"\n"+
      "-------------------------------------------------------\n"+
      "  Balance:  $"+DoubleToString(balance,2)+
      "   Equity: $"+DoubleToString(equity,2)+"\n"+
      "  Positions open: "+IntegerToString(CountPositions())+
      " / 2 max (pyramid)\n"+
      "  Risk/trade: "+DoubleToString(InpRiskPercent,1)+
      "%   Pyramid: "+(InpUsePyramid?"ON":"OFF")+"\n"+
      "-------------------------------------------------------\n"+
      ftmoLine+
      "  Heartbeat: "+IntegerToString(secs)+"s since last tick\n"+
      "=======================================================");
}

datetime StartOfDay(datetime t)
{ MqlDateTime s; TimeToStruct(t,s); s.hour=0;s.min=0;s.sec=0; return(StructToTime(s)); }
//+------------------------------------------------------------------+
