Warrior_EA/System/Random.mqh

76 lines
3.1 KiB
MQL5
Raw Permalink Normal View History

feat(rng): ALGLIB's L'Ecuyer generator replaces MathRand, and a seed collision goes with it MQL5's MathRand() is the 15-bit MSVC LCG - 32768 distinct values and the lattice structure that shape of generator has. Two places here actually lean on randomness and both were hurt by it: WEIGHT INIT. Six He/LeCun-uniform sites drew ((MathRand()+1)/32768.0 - 0.5) * 2 * scale, so a first dense layer of ~250k weights had only 32768 possible values and thousands of connections started byte-identical. Breaking that symmetry is the whole job of random init. SHUFFLING. ShuffleRandomIndex() already had to splice TWO MathRand() draws to reach 30 bits, and its own comment documented the residual modulo bias it still carried. HQRndUniformI() is rejection-sampled and exactly uniform, so the splice and the bias note both go. CHighQualityRand is L'Ecuyer's combined multiplicative congruential generator - two differenced streams, 31-bit output, period ~2.3e18 - and it ships with the terminal. AND A BUG THE MIGRATION EXPOSED. The three MathSrand(GetTickCount()) calls sit immediately before "build a fresh topology", once per model. GetTickCount() steps in ~15.6 ms on Windows and an ensemble builds every member inside one OnInit, so members could be handed the SAME seed and draw the SAME weights wherever their shapes coincide - and members that start identical are not an ensemble. WarriorRandSeed() takes a salt (the model id) plus a never-reset call counter, so a collision is impossible rather than merely unlikely, while the tick keeps the run itself genuinely unrepeatable the way those call sites asked for. Seeds are masked positive rather than trusted: HQRndSeed computes s % (M-1) + 1 and MQL5's % keeps the sign, so a negative seed leaves the generator in a state its own assertions reject. GetTickCount() is a uint and goes negative as an int after ~24 days of uptime - a fault that would surface as "training is broken" on a long-running terminal and nowhere else. The indicator tuner's 52 draws move across too: its random search is where sample quality earns its keep. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:29:12 -04:00
//+------------------------------------------------------------------+
//| Random.mqh |
//| AnimateDread |
//| MQL5's MathRand() is the 15-bit MSVC LCG: 32768 distinct values, |
//| period 2^31, and the lattice structure every LCG of that shape |
//| has. That is tolerable for a jittered sleep and wrong for the |
//| two places this project actually leans on randomness: |
feat(rng): ALGLIB's L'Ecuyer generator replaces MathRand, and a seed collision goes with it MQL5's MathRand() is the 15-bit MSVC LCG - 32768 distinct values and the lattice structure that shape of generator has. Two places here actually lean on randomness and both were hurt by it: WEIGHT INIT. Six He/LeCun-uniform sites drew ((MathRand()+1)/32768.0 - 0.5) * 2 * scale, so a first dense layer of ~250k weights had only 32768 possible values and thousands of connections started byte-identical. Breaking that symmetry is the whole job of random init. SHUFFLING. ShuffleRandomIndex() already had to splice TWO MathRand() draws to reach 30 bits, and its own comment documented the residual modulo bias it still carried. HQRndUniformI() is rejection-sampled and exactly uniform, so the splice and the bias note both go. CHighQualityRand is L'Ecuyer's combined multiplicative congruential generator - two differenced streams, 31-bit output, period ~2.3e18 - and it ships with the terminal. AND A BUG THE MIGRATION EXPOSED. The three MathSrand(GetTickCount()) calls sit immediately before "build a fresh topology", once per model. GetTickCount() steps in ~15.6 ms on Windows and an ensemble builds every member inside one OnInit, so members could be handed the SAME seed and draw the SAME weights wherever their shapes coincide - and members that start identical are not an ensemble. WarriorRandSeed() takes a salt (the model id) plus a never-reset call counter, so a collision is impossible rather than merely unlikely, while the tick keeps the run itself genuinely unrepeatable the way those call sites asked for. Seeds are masked positive rather than trusted: HQRndSeed computes s % (M-1) + 1 and MQL5's % keeps the sign, so a negative seed leaves the generator in a state its own assertions reject. GetTickCount() is a uint and goes negative as an int after ~24 days of uptime - a fault that would surface as "training is broken" on a long-running terminal and nowhere else. The indicator tuner's 52 draws move across too: its random search is where sample quality earns its keep. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:29:12 -04:00
//+------------------------------------------------------------------+
#ifndef WARRIOR_SYSTEM_RANDOM_MQH
#define WARRIOR_SYSTEM_RANDOM_MQH
#include <Math\Alglib\ap.mqh>
CHighQualityRandState g_warriorRandState;
//--- Distinguishes two seedings that share a salt AND a millisecond. Never reset.
int g_warriorRandSeedCount = 0;
bool g_warriorRandReady = false;
//+------------------------------------------------------------------+
//| Seed the shared stream. `salt` should identify the caller - the |
//| model id is what every current call site passes. |
feat(rng): ALGLIB's L'Ecuyer generator replaces MathRand, and a seed collision goes with it MQL5's MathRand() is the 15-bit MSVC LCG - 32768 distinct values and the lattice structure that shape of generator has. Two places here actually lean on randomness and both were hurt by it: WEIGHT INIT. Six He/LeCun-uniform sites drew ((MathRand()+1)/32768.0 - 0.5) * 2 * scale, so a first dense layer of ~250k weights had only 32768 possible values and thousands of connections started byte-identical. Breaking that symmetry is the whole job of random init. SHUFFLING. ShuffleRandomIndex() already had to splice TWO MathRand() draws to reach 30 bits, and its own comment documented the residual modulo bias it still carried. HQRndUniformI() is rejection-sampled and exactly uniform, so the splice and the bias note both go. CHighQualityRand is L'Ecuyer's combined multiplicative congruential generator - two differenced streams, 31-bit output, period ~2.3e18 - and it ships with the terminal. AND A BUG THE MIGRATION EXPOSED. The three MathSrand(GetTickCount()) calls sit immediately before "build a fresh topology", once per model. GetTickCount() steps in ~15.6 ms on Windows and an ensemble builds every member inside one OnInit, so members could be handed the SAME seed and draw the SAME weights wherever their shapes coincide - and members that start identical are not an ensemble. WarriorRandSeed() takes a salt (the model id) plus a never-reset call counter, so a collision is impossible rather than merely unlikely, while the tick keeps the run itself genuinely unrepeatable the way those call sites asked for. Seeds are masked positive rather than trusted: HQRndSeed computes s % (M-1) + 1 and MQL5's % keeps the sign, so a negative seed leaves the generator in a state its own assertions reject. GetTickCount() is a uint and goes negative as an int after ~24 days of uptime - a fault that would surface as "training is broken" on a long-running terminal and nowhere else. The indicator tuner's 52 draws move across too: its random search is where sample quality earns its keep. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:29:12 -04:00
//+------------------------------------------------------------------+
void WarriorRandSeed(const string salt)
{
g_warriorRandSeedCount++;
uint h1 = 2166136261;
int len = StringLen(salt);
for(int i = 0; i < len; i++)
{
h1 ^= (uint)StringGetCharacter(salt, i);
h1 *= 16777619;
}
uint tick = GetTickCount();
uint h2 = h1 ^ (tick * 2654435761) ^ ((uint)g_warriorRandSeedCount * 40503);
h1 ^= tick + (uint)g_warriorRandSeedCount;
int s1 = (int)(h1 & 0x7FFFFFFF);
int s2 = (int)(h2 & 0x7FFFFFFF);
CHighQualityRand::HQRndSeed(MathMax(s1, 1), MathMax(s2, 1), g_warriorRandState);
g_warriorRandReady = true;
}
//+------------------------------------------------------------------+
//| Lazily seed, so a draw can never read an uninitialised state. |
//| CHighQualityRand asserts on that rather than returning garbage, |
//| which would be a hard stop mid-training. |
//+------------------------------------------------------------------+
void WarriorRandEnsureSeeded(void)
{
if(!g_warriorRandReady)
WarriorRandSeed("warrior");
}
//--- Uniform on [0,1]. Both endpoints are attainable - callers that cannot take 0 must say so.
double WarriorRandUniform(void)
{
WarriorRandEnsureSeeded();
return CHighQualityRand::HQRndUniformR(g_warriorRandState);
}
//--- Uniform on [-1,1] - the shape every weight-init site wants before scaling by its fan-in bound.
double WarriorRandSymmetric(void)
{
return (WarriorRandUniform() - 0.5) * 2.0;
}
//--- Uniform integer on [0,n). Exactly uniform, not modulo-folded.
int WarriorRandInt(const int n)
{
if(n <= 1)
return 0;
WarriorRandEnsureSeeded();
return CHighQualityRand::HQRndUniformI(g_warriorRandState, n);
}
//--- Standard normal. Unused by the uniform init sites, but this is the draw a Gaussian He/Xavier
//--- init needs, and hand-rolling Box-Muller beside a library that already has it would be silly.
double WarriorRandNormal(void)
{
WarriorRandEnsureSeeded();
return CHighQualityRand::HQRndNormal(g_warriorRandState);
}
#endif