//+------------------------------------------------------------------+
//|                                            TestRiskAnalytics.mq5 |
//+------------------------------------------------------------------+

#include <RiskAnalyzer/RiskTypes.mqh>
#include <RiskAnalyzer/TradeStatsExtractor.mqh>
#include <RiskAnalyzer/StreakProbabilityCalculator.mqh>
#include <RiskAnalyzer/RiskOfRuinCalculator.mqh>

#define ASSERT(condition, message) TestAssert((condition), (message))

int g_pass_count = 0;
int g_fail_count = 0;

//+------------------------------------------------------------------+
//| TestAssert                                                       |
//| Prints a pass or fail message for a single test condition and    |
//| tracks the running pass and fail counts.                         |
//+------------------------------------------------------------------+
void TestAssert(bool condition, string message)
  {
   if(condition)
     {
      g_pass_count++;
      ::Print("PASS: ", message);
     }
   else
     {
      g_fail_count++;
      ::Print("FAIL: ", message);
     }
  }

//+------------------------------------------------------------------+
//| OnStart                                                          |
//| Checks the max-loss-streak walk, the win rate and average win    |
//| and loss size, the geometric streak probability formula, and the |
//| risk-of-ruin formula including its negative-edge clamp, all      |
//| against hand-worked values.                                      |
//+------------------------------------------------------------------+
void OnStart(void)
  {
//--- a 13-trade sequence: two win-break bookends, a breakeven that
//--- resets a 3-loss run, and a 4-loss run that becomes the max streak
   double profits[13] = {50, -20, -30, -10, 0, -15, -25, 40, -5, -5, -5, -5, 100};
   CTradeStatsExtractor extractor;
//--- test 1: the max loss streak should be 4, not 7 (the breakeven must break the chain)
   int max_streak = extractor.ComputeMaxLossStreak(profits, 13);
   ASSERT(max_streak == 4, "max loss streak computes to 4, correctly broken by the breakeven trade");
//--- tests 2-5: win rate, average win, average loss, and the streak figure via ComputeStats
   CTradeStats stats;
   extractor.ComputeStats(profits, 13, stats);
   ASSERT(stats.trade_count == 13 && stats.win_count == 3,
          "13 trades produce 3 wins");
   ASSERT(::MathAbs(stats.win_rate - (3.0 / 12.0)) < 0.0001,
          "win rate computes to 3/12, computed over decided trades only");
   ASSERT(::MathAbs(stats.avg_win - (190.0 / 3.0)) < 0.001,
          "average win computes to 190/3");
   ASSERT(::MathAbs(stats.avg_loss - (120.0 / 9.0)) < 0.001,
          "average loss computes to 120/9");
//--- tests 6-7: the geometric streak probability formula at clean values
   CStreakProbabilityCalculator streak_calc;
   double p5  = streak_calc.ComputeStreakProbability(0.5, 5);
   double p10 = streak_calc.ComputeStreakProbability(0.5, 10);
   ASSERT(::MathAbs(p5  - 0.03125) < 0.000001,     "P(5 losses) at 50% win rate computes to 0.03125");
   ASSERT(::MathAbs(p10 - 0.0009765625) < 0.0000001, "P(10 losses) at 50% win rate computes to 0.0009765625");
//--- test 8 and 9: an out-of-range win rate is clamped rather than
//--- producing a meaningless probability outside 0 and 1
   double p_over  = streak_calc.ComputeStreakProbability(1.5, 5);
   double p_under = streak_calc.ComputeStreakProbability(-0.5, 5);
   ASSERT(::MathAbs(p_over - 0.0) < 0.0000001,
          "a win rate above 1.0 clamps to 1.0, giving a 0.0 streak probability");
   ASSERT(::MathAbs(p_under - 1.0) < 0.0000001,
          "a win rate below 0.0 clamps to 0.0, giving a 1.0 streak probability");
//--- tests 10-11: risk of ruin rises as risk-per-trade increases, at a known positive edge
   CRiskOfRuinCalculator ror_calc;
   CRiskOfRuinResult ror_low, ror_high;
   ror_calc.Compute(0.45, 150.0, 100.0, 2.0, ror_low);
   ror_calc.Compute(0.45, 150.0, 100.0, 10.0, ror_high);
   ASSERT(::MathAbs(ror_low.edge - 0.102041) < 0.0001,
          "edge at 45% win rate, 150/100 payoff computes to about 0.1020");
   ASSERT(ror_high.ror > ror_low.ror,
          "risk of ruin at 10% risk-per-trade is higher than at 2%");
//--- test 12: a zero or negative edge clamps risk of ruin to exactly 1.0
   CRiskOfRuinResult ror_negative;
   ror_calc.Compute(0.40, 100.0, 100.0, 2.0, ror_negative);
   ASSERT(::MathAbs(ror_negative.ror - 1.0) < 0.0000001,
          "risk of ruin clamps to 1.0 for a negative-edge system");
//--- print the final summary of pass and fail counts
   ::Print("TestRiskAnalytics: ", g_pass_count, " passed, ", g_fail_count, " failed");
  }
//+------------------------------------------------------------------+