//+------------------------------------------------------------------+
//|                                                  TradeGate.mqh   |
//|                              Copyright 2026, Jorge Eliecer       |
//|  Single entry point for market orders and SL/TP changes.         |
//|  Distances arrive in price units from the strategy; the gate     |
//|  widens them to the broker floor, sizes the volume from the      |
//|  money at risk, checks margin and limits, and only then sends.   |
//+------------------------------------------------------------------+
#ifndef PORTABLE_TRADE_TRADE_GATE_MQH
#define PORTABLE_TRADE_TRADE_GATE_MQH

#include "SymbolProfile.mqh"

//--- reasons a request did not reach the server (or was refused by it)
enum ENUM_GATE_REASON
  {
   GATE_NO_TICK=0,        // no valid quote
   GATE_TRADE_DISABLED,   // terminal, program or symbol trade mode forbids it
   GATE_MARKET_CLOSED,    // outside the trade sessions of the symbol
   GATE_RISK_TOO_HIGH,    // minimum lot would risk more than allowed
   GATE_NO_MARGIN,        // not enough free margin even for the minimum lot
   GATE_VOLUME_LIMIT,     // SYMBOL_VOLUME_LIMIT reached
   GATE_STOPS_LEVEL,      // new level inside the stops level
   GATE_FREEZE,           // current level inside the freeze level
   GATE_NO_CHANGE,        // modification without real changes
   GATE_CHECK_FAILED,     // OrderCheck or a calculation failed
   GATE_SEND_FAILED       // the server rejected the request
  };
#define GATE_REASONS_TOTAL 11

//+------------------------------------------------------------------+
//| Pre-trade gate                                                   |
//+------------------------------------------------------------------+
class CTradeGate
  {
private:
   CSymbolProfile    m_sym;
   ulong             m_magic;
   ulong             m_deviation;
   double            m_risk_pct;
   double            m_max_risk_pct;
   double            m_spread_mult;
   double            m_buffer;
   double            m_margin_use;
   ENUM_ORDER_TYPE_FILLING m_filling;
   int               m_sent;
   int               m_done;
   int               m_modified;
   int               m_counts[GATE_REASONS_TOTAL];
   datetime          m_last_log[GATE_REASONS_TOTAL];

   void              Reject(const ENUM_GATE_REASON reason,const string details);
   bool              EnvironmentAllows(const ENUM_ORDER_TYPE type) const;
   ENUM_ORDER_TYPE_FILLING DetectFilling(void) const;
   static string     ReasonKey(const ENUM_GATE_REASON reason);

public:
                     CTradeGate(void);
   bool              Init(const string symbol,const ulong magic,const ulong deviation,
                          const double risk_pct,const double max_risk_pct,
                          const double spread_mult,const double buffer);
   bool              Open(const ENUM_ORDER_TYPE type,double sl_dist,double tp_dist,
                          const string comment);
   bool              Modify(const ulong ticket,double sl,double tp);
   double            MinStop(void) const { return(m_sym.MinStopDistance(m_spread_mult,m_buffer)); }
   int               Done(void) const    { return(m_done); }
   int               Rejected(const ENUM_GATE_REASON reason) const { return(m_counts[reason]); }
   void              PrintSummary(const string extra) const;
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CTradeGate::CTradeGate(void) : m_magic(0),
                               m_deviation(10),
                               m_risk_pct(1.0),
                               m_max_risk_pct(3.0),
                               m_spread_mult(2.0),
                               m_buffer(0.1),
                               m_margin_use(0.9),
                               m_filling(ORDER_FILLING_FOK),
                               m_sent(0),
                               m_done(0),
                               m_modified(0)
  {
   ArrayInitialize(m_counts,0);
   ArrayInitialize(m_last_log,0);
  }
//+------------------------------------------------------------------+
//| Initialization                                                   |
//+------------------------------------------------------------------+
bool CTradeGate::Init(const string symbol,const ulong magic,const ulong deviation,
                      const double risk_pct,const double max_risk_pct,
                      const double spread_mult,const double buffer)
  {
   if(!m_sym.Init(symbol))
      return(false);
   m_magic       =magic;
   m_deviation   =deviation;
   m_risk_pct    =risk_pct;
   m_max_risk_pct=max_risk_pct;
   m_spread_mult =spread_mult;
   m_buffer      =buffer;
   m_filling     =DetectFilling();
   return(true);
  }
//+------------------------------------------------------------------+
//| Opens a market position.                                         |
//| sl_dist and tp_dist are price distances (tp_dist=0 means no TP). |
//+------------------------------------------------------------------+
bool CTradeGate::Open(const ENUM_ORDER_TYPE type,double sl_dist,double tp_dist,const string comment)
  {
   if(type!=ORDER_TYPE_BUY && type!=ORDER_TYPE_SELL)
      return(false);
   if(!EnvironmentAllows(type))
     {
      Reject(GATE_TRADE_DISABLED,"trading is not allowed in this direction");
      return(false);
     }
   if(!m_sym.SessionOpen(60))
     {
      Reject(GATE_MARKET_CLOSED,"outside the trade session");
      return(false);
     }
   MqlTick tick;
   if(!SymbolInfoTick(m_sym.Name(),tick) || tick.bid<=0.0 || tick.ask<=0.0)
     {
      Reject(GATE_NO_TICK,"no valid quote");
      return(false);
     }
   const bool is_buy=(type==ORDER_TYPE_BUY);
   const double entry=is_buy ? tick.ask : tick.bid;
//--- the server measures stops from the price that would close the position
   const double ref  =is_buy ? tick.bid : tick.ask;
//--- 1. distances: the broker floor wins, the strategy distance is only widened
   const double floor_dist=m_sym.MinStopDistance(m_spread_mult,m_buffer);
   if(sl_dist<floor_dist)
      sl_dist=floor_dist;
   if(tp_dist>0.0 && tp_dist<floor_dist)
      tp_dist=floor_dist;
   double sl=is_buy ? m_sym.RoundAway(ref-sl_dist,false) : m_sym.RoundAway(ref+sl_dist,true);
   double tp=0.0;
   if(tp_dist>0.0)
      tp=is_buy ? m_sym.RoundAway(ref+tp_dist,true) : m_sym.RoundAway(ref-tp_dist,false);
//--- 2. volume from money at risk, computed after the stop is final
   double loss_per_lot=0.0;
   if(!m_sym.LossPerLot(type,entry,sl,loss_per_lot))
     {
      Reject(GATE_CHECK_FAILED,"cannot evaluate the loss per lot");
      return(false);
     }
   const double equity=AccountInfoDouble(ACCOUNT_EQUITY);
   if(equity<=0.0)
     {
      Reject(GATE_NO_MARGIN,"equity is not positive");
      return(false);
     }
   double lots=m_sym.FloorVolume(equity*m_risk_pct/100.0/loss_per_lot);
   if(lots<m_sym.VolumeMin())
     {
      //--- the minimum lot is accepted only while its risk stays under the hard cap
      double min_risk_pct=100.0*loss_per_lot*m_sym.VolumeMin()/equity;
      if(min_risk_pct>m_max_risk_pct)
        {
         Reject(GATE_RISK_TOO_HIGH,StringFormat("minimum lot %.2f would risk %.2f%% (cap %.2f%%)",
                                                m_sym.VolumeMin(),min_risk_pct,m_max_risk_pct));
         return(false);
        }
      lots=m_sym.VolumeMin();
     }
//--- 3. margin: shrink the volume, never the stop
   double margin=0.0;
   if(!OrderCalcMargin(type,m_sym.Name(),lots,entry,margin))
     {
      Reject(GATE_CHECK_FAILED,StringFormat("OrderCalcMargin failed, error %d",GetLastError()));
      return(false);
     }
   const double free_margin=AccountInfoDouble(ACCOUNT_MARGIN_FREE);
   if(margin>free_margin*m_margin_use)
     {
      if(margin>0.0)
         lots=m_sym.FloorVolume(lots*free_margin*m_margin_use/margin);
      if(lots<m_sym.VolumeMin())
        {
         Reject(GATE_NO_MARGIN,StringFormat("free margin %.2f is not enough for %.2f lots",
                                            free_margin,m_sym.VolumeMin()));
         return(false);
        }
     }
//--- 4. aggregate volume limit of the symbol
   const double room=m_sym.VolumeLimitRoom(type);
   if(lots>room)
     {
      lots=m_sym.FloorVolume(room);
      if(lots<m_sym.VolumeMin())
        {
         Reject(GATE_VOLUME_LIMIT,"SYMBOL_VOLUME_LIMIT reached");
         return(false);
        }
     }
//--- 5. let the terminal validate the request before it reaches the server
   MqlTradeRequest     request;
   MqlTradeCheckResult check;
   MqlTradeResult      result;
   ZeroMemory(request);
   ZeroMemory(check);
   ZeroMemory(result);
   request.action      =TRADE_ACTION_DEAL;
   request.symbol      =m_sym.Name();
   request.volume      =lots;
   request.type        =type;
   request.price       =entry;
   request.sl          =sl;
   request.tp          =tp;
   request.deviation   =m_deviation;
   request.magic       =m_magic;
   request.type_filling=m_filling;
   request.comment     =comment;
   if(!OrderCheck(request,check))
     {
      Reject(GATE_CHECK_FAILED,StringFormat("OrderCheck retcode %u (%s)",check.retcode,check.comment));
      return(false);
     }
   m_sent++;
   if(!OrderSend(request,result) ||
      (result.retcode!=TRADE_RETCODE_DONE && result.retcode!=TRADE_RETCODE_PLACED))
     {
      Reject(GATE_SEND_FAILED,StringFormat("OrderSend retcode %u (%s)",result.retcode,result.comment));
      return(false);
     }
   m_done++;
   return(true);
  }
//+------------------------------------------------------------------+
//| Changes SL/TP of an open position (0 removes the level)          |
//+------------------------------------------------------------------+
bool CTradeGate::Modify(const ulong ticket,double sl,double tp)
  {
   if(!PositionSelectByTicket(ticket))
      return(false);
   if(PositionGetString(POSITION_SYMBOL)!=m_sym.Name())
      return(false);
   const bool   is_buy=(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY);
   const double cur_sl=PositionGetDouble(POSITION_SL);
   const double cur_tp=PositionGetDouble(POSITION_TP);
   sl=(sl>0.0) ? m_sym.RoundPrice(sl) : 0.0;
   tp=(tp>0.0) ? m_sym.RoundPrice(tp) : 0.0;
//--- 1. nothing to do: avoids TRADE_RETCODE_NO_CHANGES
   const double half_tick=m_sym.TickSize()*0.5;
   if(MathAbs(sl-cur_sl)<half_tick && MathAbs(tp-cur_tp)<half_tick)
     {
      Reject(GATE_NO_CHANGE,StringFormat("position %I64u already has these levels",ticket));
      return(false);
     }
   if(!m_sym.SessionOpen(60))
     {
      Reject(GATE_MARKET_CLOSED,"outside the trade session");
      return(false);
     }
   MqlTick tick;
   if(!SymbolInfoTick(m_sym.Name(),tick) || tick.bid<=0.0 || tick.ask<=0.0)
     {
      Reject(GATE_NO_TICK,"no valid quote");
      return(false);
     }
   const double ref=is_buy ? tick.bid : tick.ask;
//--- 2. current levels inside the freeze zone lock the whole position
   const double freeze=m_sym.FreezeDistance();
   if(freeze>0.0)
     {
      if((cur_sl>0.0 && MathAbs(ref-cur_sl)<=freeze) || (cur_tp>0.0 && MathAbs(cur_tp-ref)<=freeze))
        {
         Reject(GATE_FREEZE,StringFormat("position %I64u is inside the freeze level",ticket));
         return(false);
        }
     }
//--- 3. new levels must respect the stops level on the correct side
   const double floor_dist=m_sym.MinStopDistance(m_spread_mult,m_buffer);
   bool sl_ok=(sl==0.0) || (is_buy ? (ref-sl>=floor_dist) : (sl-ref>=floor_dist));
   bool tp_ok=(tp==0.0) || (is_buy ? (tp-ref>=floor_dist) : (ref-tp>=floor_dist));
   if(!sl_ok || !tp_ok)
     {
      const int digits=(int)SymbolInfoInteger(m_sym.Name(),SYMBOL_DIGITS);
      Reject(GATE_STOPS_LEVEL,StringFormat("new levels too close to price (sl=%s tp=%s min=%s)",
                                           DoubleToString(sl,digits),DoubleToString(tp,digits),
                                           DoubleToString(floor_dist,digits)));
      return(false);
     }
//--- 4. check and send
   MqlTradeRequest     request;
   MqlTradeCheckResult check;
   MqlTradeResult      result;
   ZeroMemory(request);
   ZeroMemory(check);
   ZeroMemory(result);
   request.action  =TRADE_ACTION_SLTP;
   request.position=ticket;
   request.symbol  =m_sym.Name();
   request.sl      =sl;
   request.tp      =tp;
   request.magic   =m_magic;
   if(!OrderCheck(request,check))
     {
      Reject(GATE_CHECK_FAILED,StringFormat("OrderCheck retcode %u (%s)",check.retcode,check.comment));
      return(false);
     }
   if(!OrderSend(request,result) || result.retcode!=TRADE_RETCODE_DONE)
     {
      Reject(GATE_SEND_FAILED,StringFormat("modify retcode %u (%s)",result.retcode,result.comment));
      return(false);
     }
   m_modified++;
   return(true);
  }
//+------------------------------------------------------------------+
//| Counts every rejection; prints at most once a minute per reason  |
//| so a repeating condition does not flood the journal.             |
//+------------------------------------------------------------------+
void CTradeGate::Reject(const ENUM_GATE_REASON reason,const string details)
  {
   m_counts[reason]++;
   datetime now=TimeCurrent();
   if(now-m_last_log[reason]<60)
      return;
   m_last_log[reason]=now;
   PrintFormat("TradeGate %s %s: %s (count %d)",m_sym.Name(),ReasonKey(reason),details,m_counts[reason]);
  }
//+------------------------------------------------------------------+
//| Terminal, program and symbol permissions                         |
//+------------------------------------------------------------------+
bool CTradeGate::EnvironmentAllows(const ENUM_ORDER_TYPE type) const
  {
//--- the tester ignores the AutoTrading button, a live chart does not
   if(!MQLInfoInteger(MQL_TESTER))
     {
      if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) || !MQLInfoInteger(MQL_TRADE_ALLOWED))
         return(false);
     }
   return(m_sym.DirectionAllowed(type));
  }
//+------------------------------------------------------------------+
//| Filling policy supported by the symbol                           |
//+------------------------------------------------------------------+
ENUM_ORDER_TYPE_FILLING CTradeGate::DetectFilling(void) const
  {
   long modes=SymbolInfoInteger(m_sym.Name(),SYMBOL_FILLING_MODE);
   if((modes&SYMBOL_FILLING_FOK)==SYMBOL_FILLING_FOK)
      return(ORDER_FILLING_FOK);
   if((modes&SYMBOL_FILLING_IOC)==SYMBOL_FILLING_IOC)
      return(ORDER_FILLING_IOC);
   return(ORDER_FILLING_RETURN);
  }
//+------------------------------------------------------------------+
//| Short key used in the journal and in the summary line            |
//+------------------------------------------------------------------+
string CTradeGate::ReasonKey(const ENUM_GATE_REASON reason)
  {
   switch(reason)
     {
      case GATE_NO_TICK:        return("no_tick");
      case GATE_TRADE_DISABLED: return("disabled");
      case GATE_MARKET_CLOSED:  return("closed");
      case GATE_RISK_TOO_HIGH:  return("risk");
      case GATE_NO_MARGIN:      return("margin");
      case GATE_VOLUME_LIMIT:   return("vol_limit");
      case GATE_STOPS_LEVEL:    return("stops");
      case GATE_FREEZE:         return("freeze");
      case GATE_NO_CHANGE:      return("no_change");
      case GATE_CHECK_FAILED:   return("check");
      case GATE_SEND_FAILED:    return("send");
     }
   return("unknown");
  }
//+------------------------------------------------------------------+
//| One machine-readable line for validation_lab.py                  |
//+------------------------------------------------------------------+
void CTradeGate::PrintSummary(const string extra) const
  {
   string line=StringFormat("PT_SUMMARY symbol=%s %s sent=%d done=%d modified=%d",
                            m_sym.Name(),extra,m_sent,m_done,m_modified);
   for(int i=0; i<GATE_REASONS_TOTAL; i++)
      line+=StringFormat(" %s=%d",ReasonKey((ENUM_GATE_REASON)i),m_counts[i]);
   Print(line);
  }

#endif // PORTABLE_TRADE_TRADE_GATE_MQH
//+------------------------------------------------------------------+
