//+------------------------------------------------------------------+
//|                                     TestEntryTimingAnalytics.mq5 |
//+------------------------------------------------------------------+
#property description "Verifies MAE/MFE computation, entry efficiency, and the "
#property description "efficiency statistics using synthetic ticks and synthetic "
#property description "trade records, independent of any live account or tick history."
#property script_show_inputs

#include <EntryTimingAnalyzer/TradeExcursionTypes.mqh>
#include <EntryTimingAnalyzer/ExcursionCalculator.mqh>
#include <EntryTimingAnalyzer/EfficiencyStatistics.mqh>

//--- assertion counters, updated by the ASSERT macro used throughout this script
int g_assertion_passes   = 0;
int g_assertion_failures = 0;

//+----------------------------------------------------------------------+
//| ASSERT                                                               |
//| Records a pass or a failure for one checked condition and prints a   |
//| diagnostic message, including the source line number, whenever the   |
//| condition is false.                                                  |
//+----------------------------------------------------------------------+
#define ASSERT(condition, message)                                          \
   if(!(condition))                                                         \
     {                                                                      \
      ::PrintFormat("ASSERTION FAILED: %s (line %d)", (message), __LINE__); \
      g_assertion_failures++;                                               \
     }                                                                      \
   else                                                                     \
     {                                                                      \
      g_assertion_passes++;                                                 \
     }

//+--------------------------------------------------------------------+
//| MakeTick                                                           |
//| Builds one synthetic MqlTick with only the fields this project's   |
//| computations actually read: bid and ask. Tests use this helper so  |
//| every synthetic tick array is easy to construct and easy to verify |
//| by inspection.                                                     |
//+--------------------------------------------------------------------+
MqlTick MakeTick(double bid,double ask)
  {
   MqlTick tick;
   tick.time        = 0;
   tick.bid         = bid;
   tick.ask         = ask;
   tick.last        = 0.0;
   tick.volume      = 0;
   tick.time_msc    = 0;
   tick.flags       = 0;
   tick.volume_real = 0.0;
   return(tick);
  }

//+-----------------------------------------------------------------------+
//| TestComputeExcursionFromTicksLong                                     |
//| Verifies MAE and MFE for a long position: the bid rising above entry  |
//| price is favorable, the bid falling below entry price is adverse, and |
//| both figures are reported as non-negative distances regardless of the |
//| order the ticks arrive in.                                            |
//+-----------------------------------------------------------------------+
void TestComputeExcursionFromTicksLong(void)
  {
   MqlTick ticks[];
   ArrayResize(ticks, 5);
   double entry_price = 1.1000;
   ticks[0] = MakeTick(1.1000, 1.1002);
   ticks[1] = MakeTick(1.1030, 1.1032); // +30 pips favorable so far
   ticks[2] = MakeTick(1.0985, 1.0987); // -15 pips adverse so far
   ticks[3] = MakeTick(1.1050, 1.1052); // +50 pips favorable, new best
   ticks[4] = MakeTick(1.1010, 1.1012); // pulls back, but not past prior extremes

   CExcursionCalculator calculator;
   double mae = 0.0, mfe = 0.0;
   bool ok = calculator.ComputeExcursionFromTicks(ticks, ArraySize(ticks), entry_price, true, mae, mfe);

   ASSERT(ok, "ComputeExcursionFromTicks must succeed for a non-empty tick array.");
   ASSERT(MathAbs(mfe - 0.0050) < 0.00001, "Long MFE must equal the largest bid rise above entry price.");
   ASSERT(MathAbs(mae - 0.0015) < 0.00001, "Long MAE must equal the largest bid drop below entry price.");
  }

//+----------------------------------------------------------------------+
//| TestComputeExcursionFromTicksShort                                   |
//| Verifies the mirrored logic for a short position: the ask falling    |
//| below entry price is favorable, and the ask rising above entry price |
//| is adverse.                                                          |
//+----------------------------------------------------------------------+
void TestComputeExcursionFromTicksShort(void)
  {
   MqlTick ticks[];
   ArrayResize(ticks, 4);
   double entry_price = 1.2000;
   ticks[0] = MakeTick(1.1998, 1.2000);
   ticks[1] = MakeTick(1.1960, 1.1962); // ask down 38 pips, favorable for a short
   ticks[2] = MakeTick(1.2025, 1.2027); // ask up 27 pips, adverse for a short
   ticks[3] = MakeTick(1.1980, 1.1982); // partial retrace, not a new extreme

   CExcursionCalculator calculator;
   double mae = 0.0, mfe = 0.0;
   bool ok = calculator.ComputeExcursionFromTicks(ticks, ArraySize(ticks), entry_price, false, mae, mfe);

   ASSERT(ok, "ComputeExcursionFromTicks must succeed for a non-empty tick array.");
   ASSERT(MathAbs(mfe - 0.0038) < 0.00001, "Short MFE must equal the largest ask drop below entry price.");
   ASSERT(MathAbs(mae - 0.0027) < 0.00001, "Short MAE must equal the largest ask rise above entry price.");
  }

//+----------------------------------------------------------------------+
//| TestComputeExcursionFromTicksEmpty                                   |
//| Verifies that an empty tick array is reported as a failure rather    |
//| than as a computed zero MAE and MFE, so a caller can distinguish "no |
//| ticks available" from "ticks available but no movement occurred."    |
//+----------------------------------------------------------------------+
void TestComputeExcursionFromTicksEmpty(void)
  {
   MqlTick ticks[];
   ArrayResize(ticks, 0);
   CExcursionCalculator calculator;
   double mae = 0.0, mfe = 0.0;
   bool ok = calculator.ComputeExcursionFromTicks(ticks, 0, 1.1000, true, mae, mfe);
   ASSERT(!ok, "ComputeExcursionFromTicks must return false for an empty tick array.");
  }

//+---------------------------------------------------------------------+
//| TestComputeEfficiencyNormal                                         |
//| Verifies the standard efficiency formula for a typical, nonzero MAE |
//| and MFE pair.                                                       |
//+---------------------------------------------------------------------+
void TestComputeEfficiencyNormal(void)
  {
   CExcursionCalculator calculator;
   double efficiency = 0.0;
   bool ok = calculator.ComputeEfficiency(20.0, 80.0, efficiency);
   ASSERT(ok, "ComputeEfficiency must succeed when MAE and MFE are not both zero.");
   ASSERT(MathAbs(efficiency - 0.8) < 0.0001, "Efficiency must equal MFE / (MFE + MAE).");
  }

//+---------------------------------------------------------------------+
//| TestComputeEfficiencyAllZero                                        |
//| Verifies that a trade with zero MAE and zero MFE, meaning no tick-  |
//| level price movement was observed at all, is reported as undefined  |
//| rather than as a computed 0.0 efficiency.                           |
//+---------------------------------------------------------------------+
void TestComputeEfficiencyAllZero(void)
  {
   CExcursionCalculator calculator;
   double efficiency = 0.0;
   bool ok = calculator.ComputeEfficiency(0.0, 0.0, efficiency);
   ASSERT(!ok, "ComputeEfficiency must report undefined when MAE and MFE are both exactly zero.");
  }

//+------------------------------------------------------------------------+
//| TestComputeEfficiencyOneSided                                          |
//| Verifies the two boundary cases: an MAE of exactly zero must yield an  |
//| efficiency of exactly 1.0, and an MFE of exactly zero must yield an    |
//| efficiency of exactly 0.0.                                             |
//+------------------------------------------------------------------------+
void TestComputeEfficiencyOneSided(void)
  {
   CExcursionCalculator calculator;
   double efficiency_all_favorable = 0.0;
   bool ok1 = calculator.ComputeEfficiency(0.0, 50.0, efficiency_all_favorable);
   ASSERT(ok1, "ComputeEfficiency must succeed when MFE alone is nonzero.");
   ASSERT(efficiency_all_favorable == 1.0, "Zero MAE with nonzero MFE must give an efficiency of exactly 1.0.");

   double efficiency_all_adverse = 0.0;
   bool ok2 = calculator.ComputeEfficiency(50.0, 0.0, efficiency_all_adverse);
   ASSERT(ok2, "ComputeEfficiency must succeed when MAE alone is nonzero.");
   ASSERT(efficiency_all_adverse == 0.0, "Zero MFE with nonzero MAE must give an efficiency of exactly 0.0.");
  }

//+----------------------------------------------------------------------+
//| TestComputeMeanEfficiency                                            |
//| Verifies that the mean is computed only over trades with a defined   |
//| efficiency, and that an all-undefined array is reported as a failure |
//| rather than a mean of zero.                                          |
//+----------------------------------------------------------------------+
void TestComputeMeanEfficiency(void)
  {
   CTradeExcursion trades[];
   ArrayResize(trades, 3);
   trades[0].m_efficiency_defined = true;
   trades[0].m_efficiency = 0.80;
   trades[1].m_efficiency_defined = false;
   trades[1].m_efficiency = 0.0;
   trades[2].m_efficiency_defined = true;
   trades[2].m_efficiency = 0.40;

   CEfficiencyStatistics statistics;
   double mean_efficiency = 0.0;
   bool ok = statistics.ComputeMeanEfficiency(trades, ArraySize(trades), mean_efficiency);

   ASSERT(ok, "ComputeMeanEfficiency must succeed when at least one trade has a defined efficiency.");
   ASSERT(MathAbs(mean_efficiency - 0.60) < 0.0001, "Mean efficiency must average only the defined entries.");

   CTradeExcursion undefined_trades[];
   ArrayResize(undefined_trades, 2);
   undefined_trades[0].m_efficiency_defined = false;
   undefined_trades[1].m_efficiency_defined = false;
   double undefined_mean = 0.0;
   bool undefined_ok = statistics.ComputeMeanEfficiency(undefined_trades, ArraySize(undefined_trades), undefined_mean);
   ASSERT(!undefined_ok, "ComputeMeanEfficiency must fail when no trade has a defined efficiency.");
  }

//+------------------------------------------------------------------------+
//| TestComputeHistogramBinsBoundary                                       |
//| Verifies the histogram boundary cases most likely to expose an         |
//| off-by-one error: an efficiency of exactly 0.0 must land in the first  |
//| bin, and an efficiency of exactly 1.0 must land in the last bin rather |
//| than one position past it.                                             |
//+------------------------------------------------------------------------+
void TestComputeHistogramBinsBoundary(void)
  {
   CTradeExcursion trades[];
   ArrayResize(trades, 3);
   trades[0].m_efficiency_defined = true;
   trades[0].m_efficiency = 0.0;  // must land in bin 0
   trades[1].m_efficiency_defined = true;
   trades[1].m_efficiency = 1.0;  // must land in the last bin
   trades[2].m_efficiency_defined = true;
   trades[2].m_efficiency = 0.55; // must land in the middle

   CEfficiencyStatistics statistics;
   int bins[];
   int bin_count = 10;
   statistics.ComputeHistogramBins(trades, ArraySize(trades), bin_count, bins);

   ASSERT(ArraySize(bins) == bin_count, "ComputeHistogramBins must produce exactly bin_count bins.");
   ASSERT(bins[0] == 1, "An efficiency of exactly 0.0 must land in bin 0.");
   ASSERT(bins[bin_count - 1] == 1, "An efficiency of exactly 1.0 must land in the last bin, not one past it.");
   ASSERT(bins[5] == 1, "An efficiency of 0.55 must land in bin 5 of 10.");
  }

//+-----------------------------------------------------------------------+
//| TestComputeWinLossCounts                                              |
//| Verifies the win/loss/breakeven classification, including a trade     |
//| with a net profit of exactly zero, which must count as breakeven and  |
//| never as a win or a loss.                                             |
//+-----------------------------------------------------------------------+
void TestComputeWinLossCounts(void)
  {
   CTradeExcursion trades[];
   ArrayResize(trades, 4);
   trades[0].m_net_profit = 120.50;
   trades[1].m_net_profit = -45.25;
   trades[2].m_net_profit = 0.0;
   trades[3].m_net_profit = -10.00;

   CEfficiencyStatistics statistics;
   int win_count = 0, loss_count = 0, breakeven_count = 0;
   statistics.ComputeWinLossCounts(trades, ArraySize(trades), win_count, loss_count, breakeven_count);

   ASSERT(win_count == 1, "Exactly one trade has a positive net profit.");
   ASSERT(loss_count == 2, "Exactly two trades have a negative net profit.");
   ASSERT(breakeven_count == 1, "A net profit of exactly zero must count as breakeven, not a win or a loss.");
  }

//+---------------------------------------------------------------------+
//| Script program start function                                       |
//| Runs every test function in sequence and prints a final pass/fail   |
//| summary. This script performs no chart rendering and requires no    |
//| live account history or tick history: every input is synthetic.     |
//+---------------------------------------------------------------------+
void OnStart(void)
  {
   g_assertion_passes   = 0;
   g_assertion_failures = 0;

   ::Print("Running TestEntryTimingAnalytics...");

   TestComputeExcursionFromTicksLong();
   TestComputeExcursionFromTicksShort();
   TestComputeExcursionFromTicksEmpty();
   TestComputeEfficiencyNormal();
   TestComputeEfficiencyAllZero();
   TestComputeEfficiencyOneSided();
   TestComputeMeanEfficiency();
   TestComputeHistogramBinsBoundary();
   TestComputeWinLossCounts();

   ::PrintFormat("TestEntryTimingAnalytics complete: %d passed, %d failed.", g_assertion_passes, g_assertion_failures);
   if(g_assertion_failures == 0)
      ::Print("TEST SUITE PASSED");
   else
      ::Print("TEST SUITE FAILED");
  }
//+------------------------------------------------------------------+