//+------------------------------------------------------------------+
//|                                               PreflightRules.mqh |
//|                                                       Ali Rajput |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Ali Rajput"
#property link      "https://www.mql5.com"
//+------------------------------------------------------------------+
//| Includes                                                         |
//+------------------------------------------------------------------+
#include "PreflightTypes.mqh"
//+------------------------------------------------------------------+
//| CPreflightRules: pure checks that read only the SSymbolSpec      |
//| passed in, so a test script can feed them any broker settings    |
//+------------------------------------------------------------------+
class CPreflightRules
  {
public:
   static double     RoundToTick(const SSymbolSpec &s,const double price,const int direction);
   static int        Points(const SSymbolSpec &s,const double upper,const double lower);
   static bool       SameLevel(const SSymbolSpec &s,const double a,const double b);
   static bool       OnTickGrid(const SSymbolSpec &s,const double price);
   static bool       SessionCovers(const uint seconds,const uint start,const uint end,
                                   const bool from_yesterday);
   static ENUM_PREFLIGHT_CODE CheckVolume(const SSymbolSpec &s,double &volume,const bool repair,
                                          bool &changed,string &detail);
   static ENUM_PREFLIGHT_CODE CheckStops(const SSymbolSpec &s,const ENUM_ORDER_TYPE type,
                                         const double open_price,double &sl,double &tp,
                                         const bool repair_sl,const bool repair_tp,
                                         bool &changed,string &detail);
   static ENUM_PREFLIGHT_CODE CheckPendingPrice(const SSymbolSpec &s,const ENUM_ORDER_TYPE type,
                                                const double price,string &detail);
   static bool       IsFrozen(const SSymbolSpec &s,const bool is_buy,const double sl,const double tp);

private:
   static int        VolumeDigits(const double step);
   static ENUM_PREFLIGHT_CODE CheckLevel(const SSymbolSpec &s,const string name,const bool below,
                                         const double ref,const int need,double &level,
                                         const bool repair,bool &changed,string &detail);
  };
//+------------------------------------------------------------------+
//| RoundToTick: direction -1 rounds down, +1 up, 0 to the nearest   |
//+------------------------------------------------------------------+
double CPreflightRules::RoundToTick(const SSymbolSpec &s,const double price,const int direction)
  {
   double tick=(s.tick_size>0.0) ? s.tick_size : s.point;
   double n=price/tick;
//--- the small epsilon keeps 1.10000/0.00001 from landing on 109999.99999
   if(direction<0)
      n=MathFloor(n+1e-7);
   else
      if(direction>0)
         n=MathCeil(n-1e-7);
      else
         n=MathRound(n);
   return(NormalizeDouble(n*tick,s.digits));
  }
//+------------------------------------------------------------------+
//| Points: distance upper - lower in whole points                   |
//+------------------------------------------------------------------+
int CPreflightRules::Points(const SSymbolSpec &s,const double upper,const double lower)
  {
   return((int)MathRound((upper-lower)/s.point));
  }
//+------------------------------------------------------------------+
//| SameLevel: true when two prices differ by less than half a point |
//+------------------------------------------------------------------+
bool CPreflightRules::SameLevel(const SSymbolSpec &s,const double a,const double b)
  {
   return(MathAbs(a-b)<s.point*0.5);
  }
//+------------------------------------------------------------------+
//| OnTickGrid: true when a price is a whole number of tick sizes    |
//+------------------------------------------------------------------+
bool CPreflightRules::OnTickGrid(const SSymbolSpec &s,const double price)
  {
   double tick=(s.tick_size>0.0) ? s.tick_size : s.point;
   double n=price/tick;
   return(MathAbs(n-MathRound(n))<1e-6);
  }
//+------------------------------------------------------------------+
//| SessionCovers: is a time of day (seconds) inside one session.    |
//| from_yesterday checks only the part of yesterday's session that  |
//| runs past midnight                                               |
//+------------------------------------------------------------------+
bool CPreflightRules::SessionCovers(const uint seconds,const uint start,const uint end,
                                    const bool from_yesterday)
  {
   const uint day=86400;
   if(from_yesterday)
     {
      //--- written with an end beyond 24:00, or with the end before the start
      if(end>day)
         return(seconds<end-day);
      if(start>end)
         return(seconds<end);
      return(false);
     }
   if(end>day)
      return(seconds>=start);
//--- across midnight: the evening part, and the morning part if the day lists it
   if(start>end)
      return(seconds>=start || seconds<end);
   return(seconds>=start && seconds<end);
  }
//+------------------------------------------------------------------+
//| VolumeDigits: decimal places of the volume step                  |
//+------------------------------------------------------------------+
int CPreflightRules::VolumeDigits(const double step)
  {
   int digits=0;
   double scaled=step;
   while(digits<8 && MathAbs(scaled-MathRound(scaled))>1e-7)
     {
      scaled*=10.0;
      digits++;
     }
   return(digits);
  }
//+------------------------------------------------------------------+
//| CheckVolume: min, max and step. Repair only ever lowers volume   |
//+------------------------------------------------------------------+
ENUM_PREFLIGHT_CODE CPreflightRules::CheckVolume(const SSymbolSpec &s,double &volume,const bool repair,
                                                 bool &changed,string &detail)
  {
   if(s.vol_step<=0.0 || s.vol_min<=0.0)
     {
      detail="symbol has no volume step or minimum";
      return(PF_BAD_REQUEST);
     }
   double v=volume;
//--- above the maximum: clamp down when allowed
   if(v>s.vol_max)
     {
      if(!repair)
        {
         detail=StringFormat("volume %.2f is above SYMBOL_VOLUME_MAX %.2f",volume,s.vol_max);
         return(PF_VOLUME_INVALID);
        }
      v=s.vol_max;
     }
//--- round down to the step, never up
   double steps=MathFloor(v/s.vol_step+1e-7);
   double on_step=NormalizeDouble(steps*s.vol_step,VolumeDigits(s.vol_step));
   if(MathAbs(on_step-v)>1e-8)
     {
      if(!repair)
        {
         detail=StringFormat("volume %.3f is not a multiple of SYMBOL_VOLUME_STEP %.3f",volume,s.vol_step);
         return(PF_VOLUME_INVALID);
        }
      v=on_step;
     }
//--- below the minimum: raising it would raise the risk, so refuse
   if(v<s.vol_min-1e-8)
     {
      detail=StringFormat("volume %.3f is below SYMBOL_VOLUME_MIN %.2f",v,s.vol_min);
      return(PF_VOLUME_INVALID);
     }
   if(MathAbs(v-volume)>1e-8)
     {
      detail=StringFormat("volume %.3f changed to %.2f",volume,v);
      changed=true;
      volume=v;
     }
   return(PF_OK);
  }
//+------------------------------------------------------------------+
//| CheckLevel: one SL or TP against a reference price               |
//+------------------------------------------------------------------+
ENUM_PREFLIGHT_CODE CPreflightRules::CheckLevel(const SSymbolSpec &s,const string name,const bool below,
                                                const double ref,const int need,double &level,
                                                const bool repair,bool &changed,string &detail)
  {
//--- 0 means the level is not set, there is nothing to check
   if(level==0.0)
      return(PF_OK);
//--- distance in points, positive when the level sits on the correct side
   int dist=below ? Points(s,ref,level) : Points(s,level,ref);
   if(dist<0)
     {
      detail=StringFormat("%s %s is on the wrong side of %s",name,
                          DoubleToString(level,s.digits),DoubleToString(ref,s.digits));
      return(PF_STOPS_SIDE);
     }
//--- the server takes only prices on the tick grid, however far they are
   if(!OnTickGrid(s,level))
     {
      if(!repair)
        {
         detail=StringFormat("%s %s is not a multiple of the tick size %s",name,
                             DoubleToString(level,s.digits),DoubleToString(s.tick_size,s.digits));
         return(PF_PRICE_OFF_TICK);
        }
      //--- round away from the reference price, so the distance can only grow
      level=RoundToTick(s,level,below ? -1 : 1);
      changed=true;
      if(detail!="")
         detail+=", ";
      detail+=StringFormat("%s rounded to the tick grid at %s",name,DoubleToString(level,s.digits));
      dist=below ? Points(s,ref,level) : Points(s,level,ref);
     }
   if(dist>need)
      return(PF_OK);
//--- too close: move it away only when the caller allowed it
   if(!repair)
     {
      detail=StringFormat("%s %s is %d points from %s, needs more than %d",name,
                          DoubleToString(level,s.digits),dist,DoubleToString(ref,s.digits),need);
      return(PF_STOPS_TOO_CLOSE);
     }
   double moved=below ? ref-(need+1)*s.point : ref+(need+1)*s.point;
   level=RoundToTick(s,moved,below ? -1 : 1);
   changed=true;
   if(detail!="")
      detail+=", ";
   detail+=StringFormat("%s moved to %s",name,DoubleToString(level,s.digits));
   return(PF_OK);
  }
//+------------------------------------------------------------------+
//| CheckStops: SL and TP of a market or pending order               |
//+------------------------------------------------------------------+
ENUM_PREFLIGHT_CODE CPreflightRules::CheckStops(const SSymbolSpec &s,const ENUM_ORDER_TYPE type,
                                                const double open_price,double &sl,double &tp,
                                                const bool repair_sl,const bool repair_tp,
                                                bool &changed,string &detail)
  {
   bool is_market=(type==ORDER_TYPE_BUY || type==ORDER_TYPE_SELL);
   bool is_pending=(type==ORDER_TYPE_BUY_LIMIT || type==ORDER_TYPE_BUY_STOP ||
                    type==ORDER_TYPE_SELL_LIMIT || type==ORDER_TYPE_SELL_STOP);
   if(!is_market && !is_pending)
     {
      detail="order type "+EnumToString(type)+" is not supported";
      return(PF_BAD_REQUEST);
     }
   bool is_buy=(type==ORDER_TYPE_BUY || type==ORDER_TYPE_BUY_LIMIT || type==ORDER_TYPE_BUY_STOP);
//--- a market position closes at Bid (buy) or Ask (sell), a pending order at its own price
   double ref=is_market ? (is_buy ? s.bid : s.ask) : open_price;
//--- the buffer covers the market moving before the server checks the request.
//--- a pending order's SL and TP are measured from its own fixed price, so no buffer
   int need=s.stops_level+(is_market ? s.delay_points : 0);
   ENUM_PREFLIGHT_CODE code=CheckLevel(s,"SL",is_buy,ref,need,sl,repair_sl,changed,detail);
   if(code!=PF_OK)
      return(code);
   return(CheckLevel(s,"TP",!is_buy,ref,need,tp,repair_tp,changed,detail));
  }
//+------------------------------------------------------------------+
//| CheckPendingPrice: open price of a pending order vs the market   |
//+------------------------------------------------------------------+
ENUM_PREFLIGHT_CODE CPreflightRules::CheckPendingPrice(const SSymbolSpec &s,const ENUM_ORDER_TYPE type,
                                                       const double price,string &detail)
  {
   int dist=0;
   switch(type)
     {
      case ORDER_TYPE_BUY_LIMIT:  dist=Points(s,s.ask,price); break;
      case ORDER_TYPE_BUY_STOP:   dist=Points(s,price,s.ask); break;
      case ORDER_TYPE_SELL_LIMIT: dist=Points(s,price,s.bid); break;
      case ORDER_TYPE_SELL_STOP:  dist=Points(s,s.bid,price); break;
      default:
         detail="order type "+EnumToString(type)+" is not a pending order";
         return(PF_BAD_REQUEST);
     }
   if(!OnTickGrid(s,price))
     {
      detail=StringFormat("%s price %s is not a multiple of the tick size %s",EnumToString(type),
                          DoubleToString(price,s.digits),DoubleToString(s.tick_size,s.digits));
      return(PF_PRICE_OFF_TICK);
     }
//--- the delay buffer applies here too: the market can move toward the price
   int need=s.stops_level+s.delay_points;
   if(dist<0)
     {
      detail=StringFormat("%s price %s is on the wrong side of the market",
                          EnumToString(type),DoubleToString(price,s.digits));
      return(PF_STOPS_SIDE);
     }
   if(dist<=need)
     {
      detail=StringFormat("%s price %s is %d points from the market, needs more than %d",
                          EnumToString(type),DoubleToString(price,s.digits),dist,need);
      return(PF_STOPS_TOO_CLOSE);
     }
   return(PF_OK);
  }
//+------------------------------------------------------------------+
//| IsFrozen: true when an existing SL or TP is inside freeze level  |
//+------------------------------------------------------------------+
bool CPreflightRules::IsFrozen(const SSymbolSpec &s,const bool is_buy,const double sl,const double tp)
  {
   if(s.freeze_level<=0)
      return(false);
   double ref=is_buy ? s.bid : s.ask;
//--- TP above the price for a buy, below it for a sell
   if(tp>0.0)
     {
      int dist=is_buy ? Points(s,tp,ref) : Points(s,ref,tp);
      if(dist<=s.freeze_level)
         return(true);
     }
//--- SL below the price for a buy, above it for a sell
   if(sl>0.0)
     {
      int dist=is_buy ? Points(s,ref,sl) : Points(s,sl,ref);
      if(dist<=s.freeze_level)
         return(true);
     }
   return(false);
  }
//+------------------------------------------------------------------+
