230 lines
8.4 KiB
MQL5
230 lines
8.4 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| BOSSSelfTest.mq5|
|
|
//| MMQ — Muhammad Minhas Qamar |
|
|
//| www.mql5.com/en/articles/23491 |
|
|
//+------------------------------------------------------------------+
|
|
#property copyright "MMQ — Muhammad Minhas Qamar"
|
|
#property link "https://www.mql5.com/en/articles/23491"
|
|
#property description "Behavioral Self-Test"
|
|
#property version "1.00"
|
|
#property strict
|
|
#property script_show_inputs
|
|
|
|
#include <BOSS\BOSS.mqh>
|
|
|
|
//--- shape parameters shared by every synthetic series in the test.
|
|
input int WordLen = 2; // SFA coefficients kept (word = 2*WordLen letters).
|
|
input int Alphabet = 4; // letters per coefficient slot
|
|
input int Window = 24; // sliding window length
|
|
input int SeriesLen = 240; // length of each labelled/query series
|
|
input double NoiseAmp = 0.35; // noise amplitude for the robustness test
|
|
|
|
//--- three canonical shapes the classifier must tell apart. Each is
|
|
//--- built on a common grid so only the shape differs, not the scale.
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Fill 'out' with a trending series: a steady drift plus a small |
|
|
//| oscillation and optional noise. |
|
|
//+------------------------------------------------------------------+
|
|
void MakeTrend(double &out[],int len,double noise)
|
|
{
|
|
ArrayResize(out,len);
|
|
for(int i=0;i<len;i++)
|
|
out[i]=0.02*i
|
|
+0.5*MathSin(2.0*M_PI*i/Window)
|
|
+noise*(MathRand()/32767.0-0.5);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Fill 'out' with a ranging series: a clean oscillation about a |
|
|
//| flat mean, no drift. |
|
|
//+------------------------------------------------------------------+
|
|
void MakeRange(double &out[],int len,double noise)
|
|
{
|
|
ArrayResize(out,len);
|
|
for(int i=0;i<len;i++)
|
|
out[i]=MathSin(2.0*M_PI*i/Window)
|
|
+noise*(MathRand()/32767.0-0.5);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Fill 'out' with a volatile series: large erratic jumps with no |
|
|
//| coherent period. |
|
|
//+------------------------------------------------------------------+
|
|
void MakeVolatile(double &out[],int len,double noise)
|
|
{
|
|
ArrayResize(out,len);
|
|
double v=0.0;
|
|
for(int i=0;i<len;i++)
|
|
{
|
|
v+=1.5*(MathRand()/32767.0-0.5); // random walk
|
|
out[i]=v+noise*(MathRand()/32767.0-0.5);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Build the MCB training set: many windows drawn from all three |
|
|
//| shapes so the learned breakpoints see the full spread. |
|
|
//+------------------------------------------------------------------+
|
|
void BuildTrainingWindows(double &windows[],int &count,int win)
|
|
{
|
|
int per=120; // windows per shape
|
|
count=per*3;
|
|
ArrayResize(windows,count*win);
|
|
|
|
double s[];
|
|
int idx=0;
|
|
for(int rep=0;rep<per;rep++)
|
|
{
|
|
MakeTrend(s,win,NoiseAmp);
|
|
for(int i=0;i<win;i++)
|
|
windows[idx*win+i]=s[i];
|
|
idx++;
|
|
MakeRange(s,win,NoiseAmp);
|
|
for(int i=0;i<win;i++)
|
|
windows[idx*win+i]=s[i];
|
|
idx++;
|
|
MakeVolatile(s,win,NoiseAmp);
|
|
for(int i=0;i<win;i++)
|
|
windows[idx*win+i]=s[i];
|
|
idx++;
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Test 1: does the classifier separate the three shapes? |
|
|
//| Train one clean example per class, then classify many fresh, |
|
|
//| independently generated series and report the accuracy. |
|
|
//+------------------------------------------------------------------+
|
|
void TestDiscrimination(CBossClassifier &clf)
|
|
{
|
|
Print("=== Test 1: discrimination (clean queries) ===");
|
|
int trials=30, correct=0;
|
|
double bd;
|
|
double s[];
|
|
string truth[3]= {"trend","range","volatile"};
|
|
|
|
for(int t=0;t<trials;t++)
|
|
for(int c=0;c<3;c++)
|
|
{
|
|
if(c==0)
|
|
MakeTrend(s,SeriesLen,NoiseAmp*0.3);
|
|
if(c==1)
|
|
MakeRange(s,SeriesLen,NoiseAmp*0.3);
|
|
if(c==2)
|
|
MakeVolatile(s,SeriesLen,NoiseAmp*0.3);
|
|
string got=clf.Classify(s,SeriesLen,bd);
|
|
if(got==truth[c])
|
|
correct++;
|
|
}
|
|
int total=trials*3;
|
|
PrintFormat(" accuracy: %d/%d = %.1f%%",correct,total,100.0*correct/total);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Test 2: noise robustness - the headline claim. |
|
|
//| Classify range-shaped queries at rising noise levels and show |
|
|
//| the accuracy holds where a raw point-wise matcher would fail. |
|
|
//+------------------------------------------------------------------+
|
|
void TestNoiseRobustness(CBossClassifier &clf)
|
|
{
|
|
Print("=== Test 2: noise robustness (range queries) ===");
|
|
double levels[4]= {0.1,0.5,1.0,1.5};
|
|
double s[],bd;
|
|
for(int L=0;L<4;L++)
|
|
{
|
|
int correct=0, trials=40;
|
|
for(int t=0;t<trials;t++)
|
|
{
|
|
MakeRange(s,SeriesLen,levels[L]);
|
|
if(clf.Classify(s,SeriesLen,bd)=="range")
|
|
correct++;
|
|
}
|
|
PrintFormat(" noise=%.1f -> %d/%d correct (%.0f%%)",
|
|
levels[L],correct,trials,100.0*correct/trials);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Test 3: vocabulary readout - the interpretability money-shot. |
|
|
//| Print the most frequent words each clean shape produces, so the |
|
|
//| reader can literally see the "dialect" of each regime. |
|
|
//+------------------------------------------------------------------+
|
|
void TestVocabulary(const CBossClassifier &clf)
|
|
{
|
|
Print("=== Test 3: vocabulary of each shape ===");
|
|
const CBossTransform *tr=clf.Transform();
|
|
if(tr==NULL)
|
|
{
|
|
Print(" no transform");
|
|
return;
|
|
}
|
|
|
|
double s[];
|
|
string names[3]= {"trend","range","volatile"};
|
|
for(int c=0;c<3;c++)
|
|
{
|
|
if(c==0)
|
|
MakeTrend(s,SeriesLen,NoiseAmp*0.3);
|
|
if(c==1)
|
|
MakeRange(s,SeriesLen,NoiseAmp*0.3);
|
|
if(c==2)
|
|
MakeVolatile(s,SeriesLen,NoiseAmp*0.3);
|
|
|
|
CBossHistogram h;
|
|
tr.Transform(s,SeriesLen,h);
|
|
|
|
//--- find the top-3 words by count (small vocab: linear scans)
|
|
string top="";
|
|
for(int pick=0;pick<3 && pick<h.Size();pick++)
|
|
{
|
|
int bestC=-1, bestI=-1;
|
|
for(int i=0;i<h.Size();i++)
|
|
{
|
|
bool taken=(StringFind(top,"["+h.WordAt(i)+"]")>=0);
|
|
if(!taken && h.CountAt(i)>bestC)
|
|
{ bestC=h.CountAt(i); bestI=i; }
|
|
}
|
|
if(bestI<0)
|
|
break;
|
|
top+=StringFormat("[%s]x%d ",h.WordAt(bestI),h.CountAt(bestI));
|
|
}
|
|
PrintFormat(" %-9s (%d distinct words): %s",names[c],h.Size(),top);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Script entry: build the classifier, train one clean example per |
|
|
//| class, then run the three behavioural tests. |
|
|
//+------------------------------------------------------------------+
|
|
void OnStart()
|
|
{
|
|
MathSrand(20260712);
|
|
|
|
//--- learn MCB breakpoints from a broad window pool
|
|
double windows[];
|
|
int nwin;
|
|
BuildTrainingWindows(windows,nwin,Window);
|
|
|
|
CBossClassifier clf;
|
|
if(!clf.Build(WordLen,Alphabet,windows,nwin,Window))
|
|
{ Print("Build() failed - check WordLen/Alphabet/Window vs limits"); return; }
|
|
PrintFormat("classifier built: word=%d letters, alphabet=%d, window=%d",
|
|
WordLen*2,Alphabet,Window);
|
|
|
|
//--- one clean labelled prototype per class
|
|
double s[];
|
|
MakeTrend(s,SeriesLen,NoiseAmp*0.2);
|
|
clf.AddExample(s,SeriesLen,"trend");
|
|
MakeRange(s,SeriesLen,NoiseAmp*0.2);
|
|
clf.AddExample(s,SeriesLen,"range");
|
|
MakeVolatile(s,SeriesLen,NoiseAmp*0.2);
|
|
clf.AddExample(s,SeriesLen,"volatile");
|
|
PrintFormat("trained on %d prototypes",clf.TrainCount());
|
|
|
|
TestDiscrimination(clf);
|
|
TestNoiseRobustness(clf);
|
|
TestVocabulary(clf);
|
|
Print("=== self-test complete ===");
|
|
}
|
|
//+------------------------------------------------------------------+
|