 test(unit): add MQL5 unit-test EAs for the now-decoupled arithmetic modules
CFirstPassageLadder (RungFor/StoreBar/FirstTouch/OutcomeR/WinShare),
CTripleBarrier+CLabelOverlap (ApplyMinStopWidening/ComputeLevels/SnapToLadder,
the label-overlap effective-sample-size correction), CMetaFamilies (the
classic-pattern taxonomy + table-naming rule), SGeometryScan::Reset() (guards
against 7452bd1's partial-reset shape recurring) and System/BinomialStats.mqh
(every deploy-gate/edge-floor formula this codebase shares). Each is a small
.mq5 Expert Advisor under Tests\ printing PASS/FAIL per assertion via
Print(), sharing Tests\TestHarness.mqh. All 5 self-compile-verified 0
errors/0 warnings. FirstPassageLadder.mqh/TripleBarrier.mqh expect
BARRIER_LADDER_COUNT/BARRIER_LADDER/BARRIER_HORIZON_LADDER_COUNT predefined
by their includer (normally ExpertSignalAIBase.mqh); the test EAs define
copies matching production values rather than including the whole AIBase
chain. SGeometryScan is reproduced verbatim from ExpertSignalAIBase.mqh for
the same reason, flagged in-file as needing to stay byte-identical.
Also adds Tests\convert_sample_data.py, which runs research/sqxbars.py's
decoder against a COPY of SP500_the5ers_H1.dat (never the SQX install
itself) so the operator has real sample data to point a manual tester run
at. Decodes structurally (52,542 H1 bars, monotonic, 0 high<low violations)
without calibrating a price scale - sqxbars.load()/sqx.calibrate_decimals()
both require a validated reference series to do that safely, which this
self-contained script does not have. Tests\sample_data\ (the raw copy +
decoded .npz) is gitignored, same policy as the existing Market Data/ rule.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 04:28:49 -04:00 | | | //+------------------------------------------------------------------+
|
| | | //| 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");
|
| | | }
|
| | |
|
2026-08-25 23:37:22 -04:00 | | | //+------------------------------------------------------------------+
|
| | | //| 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");
|
| | | }
|
| | |
|
 test(unit): add MQL5 unit-test EAs for the now-decoupled arithmetic modules
CFirstPassageLadder (RungFor/StoreBar/FirstTouch/OutcomeR/WinShare),
CTripleBarrier+CLabelOverlap (ApplyMinStopWidening/ComputeLevels/SnapToLadder,
the label-overlap effective-sample-size correction), CMetaFamilies (the
classic-pattern taxonomy + table-naming rule), SGeometryScan::Reset() (guards
against 7452bd1's partial-reset shape recurring) and System/BinomialStats.mqh
(every deploy-gate/edge-floor formula this codebase shares). Each is a small
.mq5 Expert Advisor under Tests\ printing PASS/FAIL per assertion via
Print(), sharing Tests\TestHarness.mqh. All 5 self-compile-verified 0
errors/0 warnings. FirstPassageLadder.mqh/TripleBarrier.mqh expect
BARRIER_LADDER_COUNT/BARRIER_LADDER/BARRIER_HORIZON_LADDER_COUNT predefined
by their includer (normally ExpertSignalAIBase.mqh); the test EAs define
copies matching production values rather than including the whole AIBase
chain. SGeometryScan is reproduced verbatim from ExpertSignalAIBase.mqh for
the same reason, flagged in-file as needing to stay byte-identical.
Also adds Tests\convert_sample_data.py, which runs research/sqxbars.py's
decoder against a COPY of SP500_the5ers_H1.dat (never the SQX install
itself) so the operator has real sample data to point a manual tester run
at. Decodes structurally (52,542 H1 bars, monotonic, 0 high<low violations)
without calibrating a price scale - sqxbars.load()/sqx.calibrate_decimals()
both require a validated reference series to do that safely, which this
self-contained script does not have. Tests\sample_data\ (the raw copy +
decoded .npz) is gitignored, same policy as the existing Market Data/ rule.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 04:28:49 -04:00 | | | //+------------------------------------------------------------------+
|
| | | int OnInit(void)
|
| | | {
|
| | | TestBinomialVarAndSE();
|
| | | TestBinomialCallsForEdge();
|
| | | TestShrunkRatePct();
|
| | | TestNormalUpperTailQ();
|
| | | TestSidakFamilyP();
|
| | | TestPermutationPValue();
|
2026-08-25 23:37:22 -04:00 | | | TestBinomialUpperTailP();
|
| | | TestExactEdgeFloorPct();
|
 test(unit): add MQL5 unit-test EAs for the now-decoupled arithmetic modules
CFirstPassageLadder (RungFor/StoreBar/FirstTouch/OutcomeR/WinShare),
CTripleBarrier+CLabelOverlap (ApplyMinStopWidening/ComputeLevels/SnapToLadder,
the label-overlap effective-sample-size correction), CMetaFamilies (the
classic-pattern taxonomy + table-naming rule), SGeometryScan::Reset() (guards
against 7452bd1's partial-reset shape recurring) and System/BinomialStats.mqh
(every deploy-gate/edge-floor formula this codebase shares). Each is a small
.mq5 Expert Advisor under Tests\ printing PASS/FAIL per assertion via
Print(), sharing Tests\TestHarness.mqh. All 5 self-compile-verified 0
errors/0 warnings. FirstPassageLadder.mqh/TripleBarrier.mqh expect
BARRIER_LADDER_COUNT/BARRIER_LADDER/BARRIER_HORIZON_LADDER_COUNT predefined
by their includer (normally ExpertSignalAIBase.mqh); the test EAs define
copies matching production values rather than including the whole AIBase
chain. SGeometryScan is reproduced verbatim from ExpertSignalAIBase.mqh for
the same reason, flagged in-file as needing to stay byte-identical.
Also adds Tests\convert_sample_data.py, which runs research/sqxbars.py's
decoder against a COPY of SP500_the5ers_H1.dat (never the SQX install
itself) so the operator has real sample data to point a manual tester run
at. Decodes structurally (52,542 H1 bars, monotonic, 0 high<low violations)
without calibrating a price scale - sqxbars.load()/sqx.calibrate_decimals()
both require a validated reference series to do that safely, which this
self-contained script does not have. Tests\sample_data\ (the raw copy +
decoded .npz) is gitignored, same policy as the existing Market Data/ rule.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 04:28:49 -04:00 | | | TSummary("BinomialStats");
|
| | | return(INIT_SUCCEEDED);
|
| | | }
|
| | | void OnTick(void) { }
|
| | | //+------------------------------------------------------------------+
|