#property strict
#property version   "1.00"
#property description "Tester-only EMA sample with per-pass exit-reason CSV export."

#include <Trade/Trade.mqh>

input int    InpFastPeriod = 8;       // Fast EMA period
input int    InpSlowPeriod = 24;      // Slow EMA period
input int    InpHoldBars   = 12;      // Bars to hold each sample trade
input double InpLots       = 0.10;    // Sample trade volume
input ulong  InpMagic      = 870022;  // Tester position identifier
input int    InpStopPoints = 80;      // Stop Loss distance in points
input int    InpTakePoints = 120;     // Take Profit distance in points
sinput string InpCsvFile   = "87_Optimization_Exit_Reasons.csv";

#define FRAME_NAME "MQL87_EXIT_REASONS_V1"
#define FRAME_ID   8701
#define SCHEMA_VERSION 2
#define FRAME_END_OF_DATA 4000

enum ENUM_PAYLOAD_FIELD
  {
   PAYLOAD_SCHEMA = 0,
   PAYLOAD_FAST,
   PAYLOAD_SLOW,
   PAYLOAD_TRADES,
   PAYLOAD_WINS,
   PAYLOAD_LOSSES,
   PAYLOAD_BREAKEVEN,
   PAYLOAD_WIN_RATE,
   PAYLOAD_NET_PROFIT,
   PAYLOAD_PROFIT_FACTOR,
   PAYLOAD_EQUITY_DD_PERCENT,
   PAYLOAD_VALID,
   PAYLOAD_EXIT_DEALS,
   PAYLOAD_SL,
   PAYLOAD_TP,
   PAYLOAD_EXPERT,
   PAYLOAD_OTHER,
   PAYLOAD_FIELD_COUNT
  };

CTrade   g_trade;
int      g_fast_handle = INVALID_HANDLE;
int      g_slow_handle = INVALID_HANDLE;
datetime g_last_bar = 0;
int      g_held_bars = 0;
double   g_trade_lots = 0.0;

int g_csv_handle = INVALID_HANDLE;
int g_rows_written = 0;
int g_ignored_frames = 0;
int g_frame_errors = 0;
int g_mapping_errors = 0;
long g_stream_exits=0;
long g_stream_sl=0;
long g_stream_tp=0;
long g_stream_expert=0;
long g_stream_other=0;
long g_stream_lookup_errors=0;

bool IsSafeCsvName(const string filename)
  {
   const int length = StringLen(filename);
   if(length < 5 || length > 80)
      return false;
   if(StringFind(filename,"..") >= 0 || StringFind(filename,"/") >= 0 ||
      StringFind(filename,"\\") >= 0 || StringFind(filename,":") >= 0)
      return false;
   return StringSubstr(filename,length-4) == ".csv";
  }

double NormalizeVolume(const double requested)
  {
   const double minimum = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
   const double maximum = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX);
   const double step = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP);
   if(requested <= 0.0 || minimum <= 0.0 || maximum < minimum || step <= 0.0)
      return 0.0;
   const double bounded = MathMax(minimum,MathMin(maximum,requested));
   const double normalized = MathFloor(bounded/step+1.0e-8)*step;
   return NormalizeDouble(MathMax(minimum,MathMin(maximum,normalized)),8);
  }

bool TradeResultAccepted()
  {
   const uint retcode = g_trade.ResultRetcode();
   return retcode == TRADE_RETCODE_DONE || retcode == TRADE_RETCODE_DONE_PARTIAL ||
          retcode == TRADE_RETCODE_PLACED;
  }

bool OwnPosition(ulong &ticket)
  {
   if(!PositionSelect(_Symbol))
      return false;
   if((ulong)PositionGetInteger(POSITION_MAGIC) != InpMagic)
     {
      PrintFormat("MQL87 foreign position detected on %s; no action taken",_Symbol);
      return false;
     }
   ticket = (ulong)PositionGetInteger(POSITION_TICKET);
   return ticket > 0;
  }

bool OpenSamplePosition(const bool buy)
  {
   MqlTick tick={};
   if(!SymbolInfoTick(_Symbol,tick) || tick.bid<=0.0 || tick.ask<tick.bid)
      return false;
   const double price=buy ? tick.ask : tick.bid;
   const double sl=NormalizeDouble(price+(buy ? -1.0 : 1.0)*InpStopPoints*_Point,_Digits);
   const double tp=NormalizeDouble(price+(buy ? 1.0 : -1.0)*InpTakePoints*_Point,_Digits);
   ResetLastError();
   const bool requested = buy
                          ? g_trade.Buy(g_trade_lots,_Symbol,0.0,sl,tp,"MQL87 tester sample")
                          : g_trade.Sell(g_trade_lots,_Symbol,0.0,sl,tp,"MQL87 tester sample");
   if(!requested || !TradeResultAccepted())
     {
      PrintFormat("MQL87 open failed direction=%s retcode=%u error=%d",
                  buy ? "BUY" : "SELL",g_trade.ResultRetcode(),GetLastError());
      return false;
     }
   g_held_bars = 0;
   return true;
  }

bool CloseSamplePosition(const ulong ticket)
  {
   ResetLastError();
   if(!g_trade.PositionClose(ticket) || !TradeResultAccepted())
     {
      PrintFormat("MQL87 close failed retcode=%u error=%d",
                  g_trade.ResultRetcode(),GetLastError());
      return false;
     }
   g_held_bars = 0;
   return true;
  }

bool ReadIntegerInput(const string &parameters[],const uint count,
                      const string name,long &value)
  {
   const string prefix = name+"=";
   for(uint i=0; i<count && i<(uint)ArraySize(parameters); ++i)
     {
      if(StringFind(parameters[i],prefix) != 0)
         continue;
      value = StringToInteger(StringSubstr(parameters[i],StringLen(prefix)));
      return true;
     }
   return false;
  }

bool InputsMatch(const ulong pass,const int fast_period,const int slow_period,
                 uint &input_count)
  {
   string parameters[];
   input_count = 0;
   ResetLastError();
   if(!FrameInputs(pass,parameters,input_count))
     {
      PrintFormat("MQL87 FrameInputs failed pass=%I64u error=%d",pass,GetLastError());
      return false;
     }
   long frame_fast = 0;
   long frame_slow = 0;
   return ReadIntegerInput(parameters,input_count,"InpFastPeriod",frame_fast) &&
          ReadIntegerInput(parameters,input_count,"InpSlowPeriod",frame_slow) &&
          frame_fast == fast_period && frame_slow == slow_period;
  }

string IntegerField(const double value)
  {
   return StringFormat("%I64d",(long)value);
  }

bool WriteFrameRow(const ulong pass,const double frame_value,const double &payload[])
  {
   const int fast_period = (int)payload[PAYLOAD_FAST];
   const int slow_period = (int)payload[PAYLOAD_SLOW];
   uint input_count = 0;
   const bool inputs_verified = InputsMatch(pass,fast_period,slow_period,input_count);
   if(!inputs_verified)
      ++g_mapping_errors;

   ResetLastError();
   const uint written = FileWrite(g_csv_handle,
                                  StringFormat("%I64u",pass),
                                  IntegerToString(fast_period),
                                  IntegerToString(slow_period),
                                  IntegerField(payload[PAYLOAD_TRADES]),
                                  IntegerField(payload[PAYLOAD_WINS]),
                                  IntegerField(payload[PAYLOAD_LOSSES]),
                                  IntegerField(payload[PAYLOAD_BREAKEVEN]),
                                  DoubleToString(payload[PAYLOAD_WIN_RATE],6),
                                  DoubleToString(payload[PAYLOAD_NET_PROFIT],2),
                                  DoubleToString(payload[PAYLOAD_PROFIT_FACTOR],6),
                                  DoubleToString(payload[PAYLOAD_EQUITY_DD_PERCENT],6),
                                  DoubleToString(frame_value,6),
                                  IntegerField(payload[PAYLOAD_VALID]),
                                  inputs_verified ? "1" : "0",
                                  IntegerToString((int)input_count),
                                  IntegerField(payload[PAYLOAD_EXIT_DEALS]),
                                  IntegerField(payload[PAYLOAD_SL]),
                                  IntegerField(payload[PAYLOAD_TP]),
                                  IntegerField(payload[PAYLOAD_EXPERT]),
                                  IntegerField(payload[PAYLOAD_OTHER]));
   if(written == 0)
     {
      ++g_frame_errors;
      PrintFormat("MQL87 CSV row write failed pass=%I64u error=%d",pass,GetLastError());
      return false;
     }
   FileFlush(g_csv_handle);
   ++g_rows_written;
   PrintFormat("MQL87 pass=%I64u fast=%d slow=%d exits=%I64d sl=%I64d tp=%I64d expert=%I64d other=%I64d inputs_verified=%s",
               pass,fast_period,slow_period,(long)payload[PAYLOAD_EXIT_DEALS],
               (long)payload[PAYLOAD_SL],(long)payload[PAYLOAD_TP],
               (long)payload[PAYLOAD_EXPERT],(long)payload[PAYLOAD_OTHER],
               inputs_verified ? "true" : "false");
   return true;
  }

void DrainFrames()
  {
   if(g_csv_handle == INVALID_HANDLE)
      return;
   while(true)
     {
      ulong pass = 0;
      string name = "";
      long id = 0;
      double frame_value = 0.0;
      double payload[];
      ResetLastError();
      if(!FrameNext(pass,name,id,frame_value,payload))
        {
         const int error = GetLastError();
         if(error != 0 && error != FRAME_END_OF_DATA)
           {
            ++g_frame_errors;
            PrintFormat("MQL87 FrameNext failed error=%d",error);
           }
         break;
        }
      if(name != FRAME_NAME || id != FRAME_ID)
        {
         ++g_ignored_frames;
         continue;
        }
      if(ArraySize(payload) != PAYLOAD_FIELD_COUNT ||
         (int)payload[PAYLOAD_SCHEMA] != SCHEMA_VERSION)
        {
         ++g_frame_errors;
         PrintFormat("MQL87 invalid payload pass=%I64u fields=%d schema=%d",
                     pass,ArraySize(payload),
                     ArraySize(payload)>0 ? (int)payload[PAYLOAD_SCHEMA] : -1);
         continue;
        }
      WriteFrameRow(pass,frame_value,payload);
     }
  }

int OnInit()
  {
   if(!MQLInfoInteger(MQL_TESTER))
     {
      Print("MQL87 is a Strategy Tester sample and will not run on a live chart.");
      return INIT_FAILED;
     }
   if(InpFastPeriod < 2 || InpSlowPeriod <= InpFastPeriod || InpHoldBars < 1 ||
      InpStopPoints < 10 || InpTakePoints < 10)
      return INIT_PARAMETERS_INCORRECT;
   g_trade_lots = NormalizeVolume(InpLots);
   if(g_trade_lots <= 0.0)
      return INIT_PARAMETERS_INCORRECT;

   g_fast_handle = iMA(_Symbol,_Period,InpFastPeriod,0,MODE_EMA,PRICE_CLOSE);
   g_slow_handle = iMA(_Symbol,_Period,InpSlowPeriod,0,MODE_EMA,PRICE_CLOSE);
   if(g_fast_handle == INVALID_HANDLE || g_slow_handle == INVALID_HANDLE)
     {
      PrintFormat("MQL87 iMA creation failed error=%d",GetLastError());
      return INIT_FAILED;
     }
   g_trade.SetExpertMagicNumber(InpMagic);
   g_trade.SetMarginMode();
   g_trade.SetDeviationInPoints(20);
   g_trade.SetAsyncMode(false);
   if(!g_trade.SetTypeFillingBySymbol(_Symbol))
     {
      PrintFormat("MQL87 filling mode setup failed error=%d",GetLastError());
      return INIT_FAILED;
     }
   return INIT_SUCCEEDED;
  }

void OnDeinit(const int reason)
  {
   if(g_fast_handle != INVALID_HANDLE)
      IndicatorRelease(g_fast_handle);
   if(g_slow_handle != INVALID_HANDLE)
      IndicatorRelease(g_slow_handle);
  }

void OnTick()
  {
   if(!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_FRAME_MODE))
      return;
   const datetime current_bar = iTime(_Symbol,_Period,0);
   if(current_bar == 0 || current_bar == g_last_bar)
      return;
   g_last_bar = current_bar;

   ulong position_ticket = 0;
   if(PositionSelect(_Symbol))
     {
      if(!OwnPosition(position_ticket))
         return;
      ++g_held_bars;
      if(g_held_bars >= InpHoldBars)
         CloseSamplePosition(position_ticket);
      return;
     }

   if(BarsCalculated(g_fast_handle) < InpSlowPeriod+2 ||
      BarsCalculated(g_slow_handle) < InpSlowPeriod+2)
      return;
   double fast[1];
   double slow[1];
   if(CopyBuffer(g_fast_handle,0,1,1,fast) != 1 ||
      CopyBuffer(g_slow_handle,0,1,1,slow) != 1)
      return;
   OpenSamplePosition(fast[0] >= slow[0]);
  }

void OnTradeTransaction(const MqlTradeTransaction &trans,
                        const MqlTradeRequest &request,
                        const MqlTradeResult &result)
  {
   if(trans.type!=TRADE_TRANSACTION_DEAL_ADD || trans.deal==0) return;
   if(!HistoryDealSelect(trans.deal))
     {
      g_stream_lookup_errors++;
      return;
     }
   if(HistoryDealGetString(trans.deal,DEAL_SYMBOL)!=_Symbol ||
      (ulong)HistoryDealGetInteger(trans.deal,DEAL_MAGIC)!=InpMagic) return;
   const ENUM_DEAL_TYPE type=(ENUM_DEAL_TYPE)HistoryDealGetInteger(trans.deal,DEAL_TYPE);
   const ENUM_DEAL_ENTRY entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(trans.deal,DEAL_ENTRY);
   if((type!=DEAL_TYPE_BUY && type!=DEAL_TYPE_SELL) ||
      (entry!=DEAL_ENTRY_OUT && entry!=DEAL_ENTRY_OUT_BY && entry!=DEAL_ENTRY_INOUT)) return;
   const ENUM_DEAL_REASON reason=(ENUM_DEAL_REASON)HistoryDealGetInteger(trans.deal,DEAL_REASON);
   g_stream_exits++;
   if(reason==DEAL_REASON_SL) g_stream_sl++;
   else if(reason==DEAL_REASON_TP) g_stream_tp++;
   else if(reason==DEAL_REASON_EXPERT) g_stream_expert++;
   else g_stream_other++;
  }

bool CountExitReasons(long &exit_deals,long &sl_count,long &tp_count,
                      long &expert_count,long &other_count)
  {
   exit_deals=0;
   sl_count=0;
   tp_count=0;
   expert_count=0;
   other_count=0;
   if(!HistorySelect(0,TimeCurrent()+86400))
     {
      PrintFormat("MQL87 HistorySelect failed error=%d",GetLastError());
      return false;
     }
   const int total=HistoryDealsTotal();
   for(int i=0;i<total;i++)
     {
      const ulong ticket=HistoryDealGetTicket(i);
      if(ticket==0 || HistoryDealGetString(ticket,DEAL_SYMBOL)!=_Symbol ||
         (ulong)HistoryDealGetInteger(ticket,DEAL_MAGIC)!=InpMagic)
         continue;
      const ENUM_DEAL_TYPE type=(ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket,DEAL_TYPE);
      if(type!=DEAL_TYPE_BUY && type!=DEAL_TYPE_SELL) continue;
      const ENUM_DEAL_ENTRY entry=(ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket,DEAL_ENTRY);
      if(entry!=DEAL_ENTRY_OUT && entry!=DEAL_ENTRY_OUT_BY && entry!=DEAL_ENTRY_INOUT)
         continue;
      const ENUM_DEAL_REASON reason=(ENUM_DEAL_REASON)HistoryDealGetInteger(ticket,DEAL_REASON);
      exit_deals++;
      if(reason==DEAL_REASON_SL) sl_count++;
      else if(reason==DEAL_REASON_TP) tp_count++;
      else if(reason==DEAL_REASON_EXPERT) expert_count++;
      else other_count++;
     }
   return exit_deals==sl_count+tp_count+expert_count+other_count;
  }

double OnTester()
  {
   const long trades = (long)TesterStatistics(STAT_TRADES);
   const long wins = (long)TesterStatistics(STAT_PROFIT_TRADES);
   const long losses = (long)TesterStatistics(STAT_LOSS_TRADES);
   const long breakeven = trades-wins-losses;
   const bool valid = trades > 0 && wins >= 0 && losses >= 0 && breakeven >= 0;
   const double win_rate = valid ? 100.0*(double)wins/(double)trades : -1.0;
   long exit_deals=0,sl_count=0,tp_count=0,expert_count=0,other_count=0;
   const bool history_valid=CountExitReasons(exit_deals,sl_count,tp_count,
                                              expert_count,other_count);
   const bool stream_valid=g_stream_lookup_errors==0 &&
      exit_deals==g_stream_exits && sl_count==g_stream_sl &&
      tp_count==g_stream_tp && expert_count==g_stream_expert &&
      other_count==g_stream_other;
   if(!stream_valid)
      PrintFormat("MQL87 stream mismatch history=%I64d/%I64d/%I64d/%I64d/%I64d stream=%I64d/%I64d/%I64d/%I64d/%I64d lookup_errors=%I64d",
                  exit_deals,sl_count,tp_count,expert_count,other_count,
                  g_stream_exits,g_stream_sl,g_stream_tp,g_stream_expert,g_stream_other,
                  g_stream_lookup_errors);

   double payload[PAYLOAD_FIELD_COUNT];
   ArrayInitialize(payload,0.0);
   payload[PAYLOAD_SCHEMA] = SCHEMA_VERSION;
   payload[PAYLOAD_FAST] = InpFastPeriod;
   payload[PAYLOAD_SLOW] = InpSlowPeriod;
   payload[PAYLOAD_TRADES] = (double)trades;
   payload[PAYLOAD_WINS] = (double)wins;
   payload[PAYLOAD_LOSSES] = (double)losses;
   payload[PAYLOAD_BREAKEVEN] = (double)breakeven;
   payload[PAYLOAD_WIN_RATE] = win_rate;
   payload[PAYLOAD_NET_PROFIT] = TesterStatistics(STAT_PROFIT);
   payload[PAYLOAD_PROFIT_FACTOR] = TesterStatistics(STAT_PROFIT_FACTOR);
   payload[PAYLOAD_EQUITY_DD_PERCENT] = TesterStatistics(STAT_EQUITY_DDREL_PERCENT);
   payload[PAYLOAD_VALID] = valid && history_valid && stream_valid && exit_deals>0 ? 1.0 : 0.0;
   payload[PAYLOAD_EXIT_DEALS] = (double)exit_deals;
   payload[PAYLOAD_SL] = (double)sl_count;
   payload[PAYLOAD_TP] = (double)tp_count;
   payload[PAYLOAD_EXPERT] = (double)expert_count;
   payload[PAYLOAD_OTHER] = (double)other_count;

   if(MQLInfoInteger(MQL_OPTIMIZATION))
     {
      ResetLastError();
      if(!FrameAdd(FRAME_NAME,FRAME_ID,win_rate,payload))
         PrintFormat("MQL87 FrameAdd failed error=%d",GetLastError());
     }
   return win_rate;
  }

int OnTesterInit()
  {
   if(!IsSafeCsvName(InpCsvFile))
     {
      PrintFormat("MQL87 unsafe CSV filename: %s",InpCsvFile);
      return INIT_PARAMETERS_INCORRECT;
     }
   ResetLastError();
   g_csv_handle = FileOpen(InpCsvFile,FILE_WRITE|FILE_CSV|FILE_ANSI,',');
   if(g_csv_handle == INVALID_HANDLE)
     {
      PrintFormat("MQL87 CSV open failed file=%s error=%d",InpCsvFile,GetLastError());
      return INIT_FAILED;
     }
   ResetLastError();
   const uint written = FileWrite(g_csv_handle,
                                  "pass_id","fast_period","slow_period","total_trades",
                                  "winning_trades","losing_trades","breakeven_trades",
                                  "win_rate_percent","net_profit","profit_factor",
                                  "equity_drawdown_percent","frame_value","valid",
                                  "inputs_verified","input_count","exit_deals",
                                  "sl_deals","tp_deals","expert_deals","other_deals");
   if(written == 0)
     {
      PrintFormat("MQL87 CSV header write failed error=%d",GetLastError());
      FileClose(g_csv_handle);
      g_csv_handle = INVALID_HANDLE;
      return INIT_FAILED;
     }
   FileFlush(g_csv_handle);
   PrintFormat("MQL87 export started file=%s frame_mode=%s",
               InpCsvFile,
               MQLInfoInteger(MQL_FRAME_MODE) ? "true" : "false");
   return INIT_SUCCEEDED;
  }

void OnTesterPass()
  {
   DrainFrames();
  }

void OnTesterDeinit()
  {
   DrainFrames();
   if(g_csv_handle != INVALID_HANDLE)
     {
      FileFlush(g_csv_handle);
      FileClose(g_csv_handle);
      g_csv_handle = INVALID_HANDLE;
     }
   const bool passed = g_rows_written > 0 && g_frame_errors == 0 && g_mapping_errors == 0;
   PrintFormat("MQL87_EXPORT rows=%d ignored=%d frame_errors=%d mapping_errors=%d status=%s",
               g_rows_written,g_ignored_frames,g_frame_errors,g_mapping_errors,
               passed ? "PASS" : "FAIL");
  }
