470 lines
12 KiB
MQL5
470 lines
12 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| RollingMinMax.Script.Test.mq5 |
|
|
//+------------------------------------------------------------------+
|
|
#property script_show_inputs
|
|
|
|
#include "..\\Algorithms\\RollingMin.mqh"
|
|
#include "..\\Algorithms\\RollingMax.mqh"
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Compare |
|
|
//+------------------------------------------------------------------+
|
|
bool Equal(const double left,
|
|
const double right)
|
|
{
|
|
return(MathAbs(left - right) < 1e-10);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Brute Force Minimum |
|
|
//+------------------------------------------------------------------+
|
|
double BruteMin(const double &values[],
|
|
const int count)
|
|
{
|
|
double result = values[0];
|
|
|
|
for(int i = 1; i < count; i++)
|
|
result = MathMin(result, values[i]);
|
|
|
|
return(result);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Brute Force Maximum |
|
|
//+------------------------------------------------------------------+
|
|
double BruteMax(const double &values[],
|
|
const int count)
|
|
{
|
|
double result = values[0];
|
|
|
|
for(int i = 1; i < count; i++)
|
|
result = MathMax(result, values[i]);
|
|
|
|
return(result);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Script program start function |
|
|
//+------------------------------------------------------------------+
|
|
void OnStart()
|
|
{
|
|
Print("--- Starting RollingMinMax Isolated Test ---");
|
|
|
|
// 1. Test Initialization
|
|
CRollingMin rollingMin;
|
|
CRollingMax rollingMax;
|
|
|
|
if(rollingMin.Init(0) || rollingMax.Init(0))
|
|
{
|
|
Print("FAIL: Initialization succeeded with zero capacity.");
|
|
return;
|
|
}
|
|
|
|
if(!rollingMin.Init(3) || !rollingMax.Init(3))
|
|
{
|
|
Print("FAIL: Initialization failed.");
|
|
return;
|
|
}
|
|
|
|
if(rollingMin.Value() != 0.0 || rollingMax.Value() != 0.0)
|
|
{
|
|
Print("FAIL: Invalid initial state.");
|
|
return;
|
|
}
|
|
|
|
// 2. Test Warmup
|
|
rollingMin.Append(30.0);
|
|
rollingMax.Append(30.0);
|
|
|
|
if(rollingMin.Value() != 30.0 || rollingMax.Value() != 30.0)
|
|
{
|
|
Print("FAIL: First warmup value incorrect.");
|
|
return;
|
|
}
|
|
|
|
rollingMin.Append(10.0);
|
|
rollingMax.Append(10.0);
|
|
|
|
if(rollingMin.Value() != 10.0 || rollingMax.Value() != 30.0)
|
|
{
|
|
Print("FAIL: Second warmup value incorrect.");
|
|
return;
|
|
}
|
|
|
|
rollingMin.Append(20.0);
|
|
rollingMax.Append(20.0);
|
|
|
|
if(rollingMin.Value() != 10.0 || rollingMax.Value() != 30.0)
|
|
{
|
|
Print("FAIL: Full warmup window incorrect.");
|
|
return;
|
|
}
|
|
|
|
// 3. Test Domination
|
|
rollingMin.Reset();
|
|
rollingMax.Reset();
|
|
|
|
rollingMin.Append(30.0);
|
|
rollingMax.Append(10.0);
|
|
|
|
rollingMin.Append(20.0);
|
|
rollingMax.Append(20.0);
|
|
|
|
rollingMin.Append(10.0);
|
|
rollingMax.Append(30.0);
|
|
|
|
rollingMin.Append(25.0);
|
|
rollingMax.Append(15.0);
|
|
|
|
if(rollingMin.Value() != 10.0 || rollingMax.Value() != 30.0)
|
|
{
|
|
Print("FAIL: Monotonic domination handling incorrect.");
|
|
return;
|
|
}
|
|
|
|
// 4. Test Expiry Boundaries
|
|
rollingMin.Reset();
|
|
rollingMax.Reset();
|
|
|
|
rollingMin.Append(10.0);
|
|
rollingMax.Append(30.0);
|
|
|
|
rollingMin.Append(20.0);
|
|
rollingMax.Append(20.0);
|
|
|
|
rollingMin.Append(30.0);
|
|
rollingMax.Append(10.0);
|
|
|
|
if(rollingMin.Value() != 10.0 || rollingMax.Value() != 30.0)
|
|
{
|
|
Print("FAIL: Pre-expiry boundary incorrect.");
|
|
return;
|
|
}
|
|
|
|
rollingMin.Append(40.0);
|
|
rollingMax.Append(5.0);
|
|
|
|
if(rollingMin.Value() != 20.0 || rollingMax.Value() != 20.0)
|
|
{
|
|
Print("FAIL: First expiry boundary incorrect.");
|
|
return;
|
|
}
|
|
|
|
rollingMin.Append(50.0);
|
|
rollingMax.Append(4.0);
|
|
|
|
if(rollingMin.Value() != 30.0 || rollingMax.Value() != 10.0)
|
|
{
|
|
Print("FAIL: Second expiry boundary incorrect.");
|
|
return;
|
|
}
|
|
|
|
// 5. Test Repeated Arbitrary ReplaceLast
|
|
rollingMin.Reset();
|
|
rollingMax.Reset();
|
|
|
|
rollingMin.Append(10.0);
|
|
rollingMax.Append(10.0);
|
|
|
|
rollingMin.Append(20.0);
|
|
rollingMax.Append(20.0);
|
|
|
|
rollingMin.Append(30.0);
|
|
rollingMax.Append(30.0);
|
|
|
|
if(!rollingMin.ReplaceLast(5.0) ||
|
|
!rollingMax.ReplaceLast(5.0))
|
|
{
|
|
Print("FAIL: First ReplaceLast execution failed.");
|
|
return;
|
|
}
|
|
|
|
if(rollingMin.Value() != 5.0 || rollingMax.Value() != 20.0)
|
|
{
|
|
Print("FAIL: First ReplaceLast result incorrect.");
|
|
return;
|
|
}
|
|
|
|
rollingMin.ReplaceLast(40.0);
|
|
rollingMax.ReplaceLast(40.0);
|
|
|
|
if(rollingMin.Value() != 10.0 || rollingMax.Value() != 40.0)
|
|
{
|
|
Print("FAIL: Second ReplaceLast result incorrect.");
|
|
return;
|
|
}
|
|
|
|
rollingMin.ReplaceLast(15.0);
|
|
rollingMax.ReplaceLast(15.0);
|
|
|
|
if(rollingMin.Value() != 10.0 || rollingMax.Value() != 20.0)
|
|
{
|
|
Print("FAIL: Third ReplaceLast result incorrect.");
|
|
return;
|
|
}
|
|
|
|
rollingMin.ReplaceLast(-5.0);
|
|
rollingMax.ReplaceLast(50.0);
|
|
|
|
if(rollingMin.Value() != -5.0 || rollingMax.Value() != 50.0)
|
|
{
|
|
Print("FAIL: Fourth ReplaceLast result incorrect.");
|
|
return;
|
|
}
|
|
|
|
// 6. Test Next Append Commit After Replacement
|
|
rollingMin.Reset();
|
|
rollingMax.Reset();
|
|
|
|
rollingMin.Append(10.0);
|
|
rollingMax.Append(10.0);
|
|
|
|
rollingMin.Append(20.0);
|
|
rollingMax.Append(20.0);
|
|
|
|
rollingMin.Append(30.0);
|
|
rollingMax.Append(30.0);
|
|
|
|
rollingMin.ReplaceLast(5.0);
|
|
rollingMax.ReplaceLast(50.0);
|
|
|
|
rollingMin.Append(40.0);
|
|
rollingMax.Append(40.0);
|
|
|
|
if(rollingMin.Value() != 5.0 || rollingMax.Value() != 50.0)
|
|
{
|
|
Print("FAIL: Replaced live value was not committed on next Append.");
|
|
return;
|
|
}
|
|
|
|
rollingMin.Append(60.0);
|
|
rollingMax.Append(5.0);
|
|
|
|
if(rollingMin.Value() != 5.0 || rollingMax.Value() != 50.0)
|
|
{
|
|
Print("FAIL: Committed replacement state expired too early.");
|
|
return;
|
|
}
|
|
|
|
rollingMin.Append(70.0);
|
|
rollingMax.Append(4.0);
|
|
|
|
if(rollingMin.Value() != 40.0 || rollingMax.Value() != 40.0)
|
|
{
|
|
Print("FAIL: Committed replacement expiry incorrect.");
|
|
return;
|
|
}
|
|
|
|
// 7. Test Reset
|
|
rollingMin.Reset();
|
|
rollingMax.Reset();
|
|
|
|
if(rollingMin.Value() != 0.0 || rollingMax.Value() != 0.0)
|
|
{
|
|
Print("FAIL: Reset did not restore empty state.");
|
|
return;
|
|
}
|
|
|
|
if(rollingMin.ReplaceLast(10.0) ||
|
|
rollingMax.ReplaceLast(10.0))
|
|
{
|
|
Print("FAIL: ReplaceLast succeeded after Reset.");
|
|
return;
|
|
}
|
|
|
|
rollingMin.Append(7.0);
|
|
rollingMax.Append(7.0);
|
|
|
|
if(rollingMin.Value() != 7.0 || rollingMax.Value() != 7.0)
|
|
{
|
|
Print("FAIL: Reuse after Reset incorrect.");
|
|
return;
|
|
}
|
|
|
|
// 8. Test Period 1
|
|
CRollingMin periodOneMin;
|
|
CRollingMax periodOneMax;
|
|
|
|
if(!periodOneMin.Init(1) || !periodOneMax.Init(1))
|
|
{
|
|
Print("FAIL: Period 1 initialization failed.");
|
|
return;
|
|
}
|
|
|
|
periodOneMin.Append(10.0);
|
|
periodOneMax.Append(10.0);
|
|
|
|
if(periodOneMin.Value() != 10.0 || periodOneMax.Value() != 10.0)
|
|
{
|
|
Print("FAIL: Period 1 first value incorrect.");
|
|
return;
|
|
}
|
|
|
|
periodOneMin.ReplaceLast(20.0);
|
|
periodOneMax.ReplaceLast(20.0);
|
|
|
|
if(periodOneMin.Value() != 20.0 || periodOneMax.Value() != 20.0)
|
|
{
|
|
Print("FAIL: Period 1 ReplaceLast incorrect.");
|
|
return;
|
|
}
|
|
|
|
periodOneMin.Append(30.0);
|
|
periodOneMax.Append(30.0);
|
|
|
|
if(periodOneMin.Value() != 30.0 || periodOneMax.Value() != 30.0)
|
|
{
|
|
Print("FAIL: Period 1 Append incorrect.");
|
|
return;
|
|
}
|
|
|
|
// 9. Compare Against Brute-Force Window Min/Max
|
|
const int period = 5;
|
|
|
|
CRollingMin testMin;
|
|
CRollingMax testMax;
|
|
|
|
if(!testMin.Init(period) || !testMax.Init(period))
|
|
{
|
|
Print("FAIL: Brute-force comparison initialization failed.");
|
|
return;
|
|
}
|
|
|
|
double source[] =
|
|
{
|
|
12.0, -5.0, 8.0, 8.0, 30.0,
|
|
4.0, -10.0, 25.0, 7.0, 40.0,
|
|
3.0, 3.0, -20.0, 50.0, 1.0
|
|
};
|
|
|
|
double window[];
|
|
ArrayResize(window, period);
|
|
|
|
int windowCount = 0;
|
|
|
|
for(int i = 0; i < ArraySize(source); i++)
|
|
{
|
|
testMin.Append(source[i]);
|
|
testMax.Append(source[i]);
|
|
|
|
if(windowCount < period)
|
|
{
|
|
window[windowCount] = source[i];
|
|
++windowCount;
|
|
}
|
|
else
|
|
{
|
|
for(int j = 1; j < period; j++)
|
|
window[j - 1] = window[j];
|
|
|
|
window[period - 1] = source[i];
|
|
}
|
|
|
|
double expectedMin = BruteMin(window, windowCount);
|
|
double expectedMax = BruteMax(window, windowCount);
|
|
|
|
double actualMin = testMin.Value();
|
|
double actualMax = testMax.Value();
|
|
|
|
if(!Equal(actualMin, expectedMin) ||
|
|
!Equal(actualMax, expectedMax))
|
|
{
|
|
PrintFormat("FAIL: Brute-force mismatch at index %d. Expected Min=%.1f, Actual Min=%.1f, Expected Max=%.1f, Actual Max=%.1f",
|
|
i,
|
|
expectedMin,
|
|
actualMin,
|
|
expectedMax,
|
|
actualMax);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 10. Brute-Force Stress Test with Interleaved Append and ReplaceLast
|
|
int periods[] = {1, 2, 3, 5, 20};
|
|
|
|
MathSrand(123456);
|
|
|
|
for(int p = 0; p < ArraySize(periods); p++)
|
|
{
|
|
int period = periods[p];
|
|
|
|
CRollingMin stressMin;
|
|
CRollingMax stressMax;
|
|
|
|
if(!stressMin.Init(period) || !stressMax.Init(period))
|
|
{
|
|
PrintFormat("FAIL: Stress test initialization failed for period %d.", period);
|
|
return;
|
|
}
|
|
|
|
double window[];
|
|
ArrayResize(window, period);
|
|
|
|
int windowCount = 0;
|
|
|
|
for(int operation = 0; operation < 10000; operation++)
|
|
{
|
|
bool append = (windowCount == 0 || MathRand() % 3 == 0);
|
|
|
|
double value = (double)(MathRand() % 20001 - 10000) / 100.0;
|
|
|
|
if(append)
|
|
{
|
|
stressMin.Append(value);
|
|
stressMax.Append(value);
|
|
|
|
if(windowCount < period)
|
|
{
|
|
window[windowCount] = value;
|
|
++windowCount;
|
|
}
|
|
else
|
|
{
|
|
for(int i = 1; i < period; i++)
|
|
window[i - 1] = window[i];
|
|
|
|
window[period - 1] = value;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(!stressMin.ReplaceLast(value) ||
|
|
!stressMax.ReplaceLast(value))
|
|
{
|
|
PrintFormat("FAIL: Stress ReplaceLast failed. Period=%d, Operation=%d",
|
|
period, operation);
|
|
return;
|
|
}
|
|
|
|
window[windowCount - 1] = value;
|
|
}
|
|
|
|
double expectedMin = BruteMin(window, windowCount);
|
|
double expectedMax = BruteMax(window, windowCount);
|
|
|
|
double actualMin = stressMin.Value();
|
|
double actualMax = stressMax.Value();
|
|
|
|
if(!Equal(actualMin, expectedMin) ||
|
|
!Equal(actualMax, expectedMax))
|
|
{
|
|
PrintFormat("FAIL: Stress mismatch. Period=%d, Operation=%d, Type=%s, Value=%.2f, Expected Min=%.2f, Actual Min=%.2f, Expected Max=%.2f, Actual Max=%.2f",
|
|
period,
|
|
operation,
|
|
append ? "Append" : "ReplaceLast",
|
|
value,
|
|
expectedMin,
|
|
actualMin,
|
|
expectedMax,
|
|
actualMax);
|
|
return;
|
|
}
|
|
}
|
|
|
|
PrintFormat("PASS: Stress test completed for period %d.", period);
|
|
}
|
|
|
|
Print("SUCCESS: All RollingMinMax isolated tests passed!");
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//+------------------------------------------------------------------+
|