//+------------------------------------------------------------------+ //| 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) { } //+------------------------------------------------------------------+