Warrior_EA/Tests/Test_BinomialStats.mq5

189 lines
11 KiB
MQL5

//+------------------------------------------------------------------+
//| Test_BinomialStats.mq5 |
//| Warrior_EA |
//| AnimateDread |
//| |
//| Unit test EA for System\BinomialStats.mqh - the free-function |
//| statistics every deploy gate, edge floor and baseline report in |
//| this codebase shares. Closed-form checks against hand-computed |
//| values; NormalUpperTailQ/SidakFamilyP are checked at points with |
//| known or easily-bounded answers rather than re-deriving the |
//| normal CDF here. |
//+------------------------------------------------------------------+
#property copyright "AnimateDread"
#property strict
#include "TestHarness.mqh"
#include "..\System\BinomialStats.mqh"
//+------------------------------------------------------------------+
void TestBinomialVarAndSE(void)
{
//--- p=0.5, n=100 -> var = 0.5*0.5/100 = 0.0025; SE% = 100*sqrt(0.0025) = 5.0
TAssertNear(BinomialVar(0.5, 100.0), 0.0025, "BinomialVar: p=0.5,n=100 -> 0.0025");
TAssertNear(BinomialSEPct(0.5, 100.0), 5.0, "BinomialSEPct: p=0.5,n=100 -> 5.0pp");
TAssertNear(BinomialVar(0.0, 100.0), 0.0, "BinomialVar: p=0 is degenerate -> 0");
TAssertNear(BinomialVar(1.0, 100.0), 0.0, "BinomialVar: p=1 is degenerate -> 0");
TAssertNear(BinomialVar(0.5, 0.0), 0.0, "BinomialVar: n<=0 -> 0");
TAssertNear(BinomialVar(0.5, -10.0), 0.0, "BinomialVar: negative n -> 0");
}
//+------------------------------------------------------------------+
void TestBinomialCallsForEdge(void)
{
//--- n = sigmas^2 * p(1-p) / edge^2 = 4 * 0.25 / 0.0025 = 400
TAssertNear(BinomialCallsForEdge(0.5, 0.05, 2.0), 400.0, "BinomialCallsForEdge: closed-form n");
TAssertNear(BinomialCallsForEdge(0.5, 0.0, 2.0), 0.0, "BinomialCallsForEdge: edge<=0 -> 0");
TAssertNear(BinomialCallsForEdge(0.0, 0.05, 2.0), 0.0, "BinomialCallsForEdge: p<=0 -> 0");
TAssertNear(BinomialCallsForEdge(1.0, 0.05, 2.0), 0.0, "BinomialCallsForEdge: p>=1 -> 0");
}
//+------------------------------------------------------------------+
void TestShrunkRatePct(void)
{
//--- No prior: the raw rate.
TAssertNear(ShrunkRatePct(30.0, 100.0, 0.0, 0.0), 30.0, "ShrunkRatePct: no prior -> raw rate");
//--- No evidence, with a prior: the prior is the estimate.
TAssertNear(ShrunkRatePct(0.0, 0.0, 62.0, 30.0), 62.0, "ShrunkRatePct: n<=0 with a prior -> the prior");
//--- No evidence, no prior: 0.
TAssertNear(ShrunkRatePct(0.0, 0.0, 0.0, 0.0), 0.0, "ShrunkRatePct: no evidence, no prior -> 0");
//--- hits=10,n=20 (50%) shrunk toward a 50% prior with priorN=30 stays 50% (both agree).
TAssertNear(ShrunkRatePct(10.0, 20.0, 50.0, 30.0), 50.0, "ShrunkRatePct: evidence == prior -> unchanged");
//--- hits=5,n=20 (25%) shrunk toward a 50% prior, priorN=30:
//--- (5 + 30*0.5) * 100 / (20+30) = (5+15)*100/50 = 40.0
TAssertNear(ShrunkRatePct(5.0, 20.0, 50.0, 30.0), 40.0, "ShrunkRatePct: pulled toward the prior");
}
//+------------------------------------------------------------------+
void TestNormalUpperTailQ(void)
{
TAssertNear(NormalUpperTailQ(0.0), 0.5, "NormalUpperTailQ: Q(0) == 0.5", 1e-4);
//--- Monotonically decreasing in z.
TAssert(NormalUpperTailQ(1.0) < NormalUpperTailQ(0.0), "NormalUpperTailQ: Q(1) < Q(0)");
TAssert(NormalUpperTailQ(3.0) < NormalUpperTailQ(1.0), "NormalUpperTailQ: Q(3) < Q(1)");
//--- z=1.959964 is the familiar two-sided-5% cutoff -> Q ~= 0.025.
TAssertNear(NormalUpperTailQ(1.959964), 0.025, "NormalUpperTailQ: Q(1.96) ~= 0.025", 2e-4);
//--- The invalid-z guard (MathIsValidNumber(z) false -> returns 1.0, "not significant") is not
//--- exercised here - producing a real NaN/inf at runtime without relying on undefined-behaviour
//--- floating-point division is not worth the risk of aborting the rest of this test's assertions;
//--- the guard is a two-line early return, read and confirmed present at
//--- System\BinomialStats.mqh:21-22 rather than executed.
}
//+------------------------------------------------------------------+
void TestSidakFamilyP(void)
{
//--- N=1: family-wise p equals the single-draw p exactly, 1-(1-Q)^1 == Q.
TAssertNear(SidakFamilyP(0.0, 1), 0.5, "SidakFamilyP: N=1,z=0 -> Q(0) == 0.5", 1e-4);
//--- More candidates tried -> a larger (more permissive-looking, i.e. worse) family-wise p for the
//--- same observed z - the whole point of the best-of-N correction.
double pFew = SidakFamilyP(2.0, 1);
double pMany = SidakFamilyP(2.0, 100);
TAssert(pMany > pFew, "SidakFamilyP: more candidates tried inflates the family-wise p for the same z");
//--- nTried is floored at 1 via MathMax inside the function - a caller passing 0 or negative must
//--- not get a p smaller than the single-draw case.
TAssertNear(SidakFamilyP(2.0, 0), SidakFamilyP(2.0, 1), "SidakFamilyP: nTried<=0 floors to 1");
//--- p is always in [0,1].
double p = SidakFamilyP(5.0, 1000);
TAssert(p >= 0.0 && p <= 1.0, "SidakFamilyP: result stays in [0,1] even for a large family");
}
//+------------------------------------------------------------------+
void TestPermutationPValue(void)
{
TAssertNear(PermutationPValue(0, 0), 1.0, "PermutationPValue: draws<=0 -> 1.0 (abandoned null)");
TAssertNear(PermutationPValue(5, -3), 1.0, "PermutationPValue: negative draws -> 1.0");
//--- (1+5)/(10+1) = 6/11
TAssertNear(PermutationPValue(5, 10), 6.0 / 11.0, "PermutationPValue: add-one-smoothed formula");
//--- (1+0)/(10+1) = 1/11 - the smallest attainable p at 10 draws (add-one smoothing floors it above 0).
TAssertNear(PermutationPValue(0, 10), 1.0 / 11.0, "PermutationPValue: zero-hit floor is 1/(draws+1)");
//--- Every null draw at least as extreme: (1+10)/(10+1) == 1.0.
TAssertNear(PermutationPValue(10, 10), 1.0, "PermutationPValue: all draws extreme -> p == 1.0");
}
//+------------------------------------------------------------------+
//| THE EXACT TAIL. Anchored against P(X>=k) summed directly from the |
//| binomial PMF - an INDEPENDENT computation, not a restatement of |
//| the incomplete-beta identity the implementation uses. That |
//| independence is the point: the shipped version of this function |
//| passed tail=false (upper tail) where the identity needs the LOWER |
//| tail, returning 1-I_p0 for every input. It compiled clean, read |
//| plausibly, and made every deploy gate in the project report a |
//| 100.0% floor - i.e. "no win rate can ever clear this" - on every |
//| model. Only a value check against an outside source catches that. |
//+------------------------------------------------------------------+
void TestBinomialUpperTailP(void)
{
//--- Reference values: sum_{i=k..n} C(n,i) p^i (1-p)^(n-i).
TAssertNear(BinomialUpperTailP(6.0, 10.0, 0.5), 0.376953125,
"BinomialUpperTailP: P(X>=6 | n=10,p=0.5)", 1e-9);
TAssertNear(BinomialUpperTailP(8.0, 10.0, 0.5), 0.0546875,
"BinomialUpperTailP: P(X>=8 | n=10,p=0.5)", 1e-9);
TAssertNear(BinomialUpperTailP(15.0, 20.0, 0.5), 0.020694732666,
"BinomialUpperTailP: P(X>=15 | n=20,p=0.5)", 1e-9);
TAssertNear(BinomialUpperTailP(30.0, 50.0, 0.6), 0.561034932040,
"BinomialUpperTailP: P(X>=30 | n=50,p=0.6)", 1e-9);
//--- DIRECTION SANITY, the assertion that would have failed loudest on the inverted flag: an
//--- observation far above the null is SIGNIFICANT (small p), one at the null is not.
TAssert(BinomialUpperTailP(90.0, 100.0, 0.5) < 0.001,
"BinomialUpperTailP: 90/100 against a fair coin is highly significant");
TAssert(BinomialUpperTailP(50.0, 100.0, 0.5) > 0.4,
"BinomialUpperTailP: 50/100 against a fair coin is not significant");
//--- Monotone DECREASING in k at fixed n,p - the property the bisection in ExactEdgeFloorPct relies on.
TAssert(BinomialUpperTailP(60.0, 100.0, 0.5) < BinomialUpperTailP(55.0, 100.0, 0.5),
"BinomialUpperTailP: decreasing in k (the bisection's monotonicity precondition)");
//--- Guards.
TAssertNear(BinomialUpperTailP(5.0, 0.0, 0.5), 1.0, "BinomialUpperTailP: n<=0 -> 1.0 (no evidence)");
TAssertNear(BinomialUpperTailP(0.0, 10.0, 0.5), 1.0, "BinomialUpperTailP: k<=0 -> 1.0 (no evidence)");
TAssertNear(BinomialUpperTailP(10.0, 10.0, 0.5), 0.0, "BinomialUpperTailP: k>=n -> 0.0 (every trial won)");
TAssertNear(BinomialUpperTailP(5.0, 10.0, 1.0), 1.0, "BinomialUpperTailP: p0>=1 -> nothing beats a certain null");
}
//+------------------------------------------------------------------+
//| THE FLOOR the deploy gate actually compares precision against. |
//+------------------------------------------------------------------+
void TestExactEdgeFloorPct(void)
{
//--- THE REGRESSION TEST. At the effN this gate really sees (hundreds of independent calls) the
//--- exact floor must sit a few points above chance - NOT at 100.0, and not at chance itself.
double floor440 = ExactEdgeFloorPct(56.0, 440.4, 2.0);
TAssert(floor440 > 56.0 && floor440 < 70.0,
StringFormat("ExactEdgeFloorPct: chance=56%%, effN=440 -> a REACHABLE floor (got %.2f%%)", floor440));
//--- It must track the normal approximation it replaces closely at this n - that approximation is
//--- only mildly anticonservative here, so a large divergence means the exact test is wrong, not strict.
double approx440 = 56.0 + 2.0 * BinomialSEPct(0.56, 440.4);
TAssertNear(floor440, approx440, "ExactEdgeFloorPct: tracks chance+2SE within 1pp at effN=440", 1.0);
//--- MORE INDEPENDENT OBSERVATIONS -> A LOWER BAR. The floor falls toward chance as effN grows.
TAssert(ExactEdgeFloorPct(50.0, 2000.0, 2.0) < ExactEdgeFloorPct(50.0, 200.0, 2.0),
"ExactEdgeFloorPct: the floor falls as effN rises");
//--- MORE SIGMAS -> A HIGHER BAR.
TAssert(ExactEdgeFloorPct(50.0, 400.0, 3.0) > ExactEdgeFloorPct(50.0, 400.0, 2.0),
"ExactEdgeFloorPct: the floor rises with the required sigmas");
//--- The floor is always ABOVE the chance rate it is built from, and inside [0,100].
TAssert(floor440 >= 56.0 && floor440 <= 100.0, "ExactEdgeFloorPct: floor stays in [chance,100]");
//--- GENUINELY UNREACHABLE is still reachable as an ANSWER: at a handful of observations no win
//--- rate clears 2 sigma, and the bisection must converge to 100.0 on its own rather than by guard.
TAssertNear(ExactEdgeFloorPct(50.0, 3.0, 2.0), 100.0,
"ExactEdgeFloorPct: too few observations -> genuinely unreachable (100%)", 1e-6);
//--- ZERO EVIDENCE IS NOT AN IMPOSSIBLE BAR. effN<=0 must degrade to chancePct, matching the
//--- normal approximation (SE=0), so BarUnreachable() does not read "nothing scored yet" as
//--- "this geometry can never work".
TAssertNear(ExactEdgeFloorPct(56.0, 0.0, 2.0), 56.0, "ExactEdgeFloorPct: effN<=0 -> chancePct, not 100");
TAssertNear(ExactEdgeFloorPct(56.0, -5.0, 2.0), 56.0, "ExactEdgeFloorPct: negative effN -> chancePct");
}
//+------------------------------------------------------------------+
int OnInit(void)
{
TestBinomialVarAndSE();
TestBinomialCallsForEdge();
TestShrunkRatePct();
TestNormalUpperTailQ();
TestSidakFamilyP();
TestPermutationPValue();
TestBinomialUpperTailP();
TestExactEdgeFloorPct();
TSummary("BinomialStats");
return(INIT_SUCCEEDED);
}
void OnTick(void) { }
//+------------------------------------------------------------------+