//+------------------------------------------------------------------+ //| BOSS.mqh | //| 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 version "1.00" #property strict #include //--- a series shorter than this many samples cannot host even one //--- window plus a little context; treated as too short to classify. #define BOSS_MIN_SERIES 4 //+------------------------------------------------------------------+ //| CBossHistogram - a sparse word-count histogram. | //| | //| A BOSS representation of a series is just a bag of words: how | //| many times each SFA word appears as the sliding window moves | //| along the series. Most possible words never occur, so the bag | //| is stored sparsely as parallel arrays of word strings and their | //| counts rather than a dense vector over the whole vocabulary. | //| | //| Word strings double as human-readable labels, which is what | //| lets us later print the vocabulary of a class - something the | //| elastic-distance classifiers cannot offer. | //+------------------------------------------------------------------+ class CBossHistogram { private: string m_word[]; // distinct words seen int m_count[]; // occurrences of each word int m_size; // number of distinct words int IndexOf(const string w) const; public: CBossHistogram(void) { m_size=0; } void Clear(void); //--- add one occurrence of word 'w' (new or existing). void Add(const string w); int Size(void) const { return m_size; } string WordAt(int i) const { return (i>=0 && i=0 && i=0) { m_count[idx]++; return; } ArrayResize(m_word,m_size+1); ArrayResize(m_count,m_size+1); m_word[m_size]=w; m_count[m_size]=1; m_size++; } //+------------------------------------------------------------------+ //| Count of a specific word (0 if absent). | //+------------------------------------------------------------------+ int CBossHistogram::CountOf(const string w) const { int idx=IndexOf(w); return (idx>=0)?m_count[idx]:0; } //+------------------------------------------------------------------+ //| Deep-copy this histogram into 'dst'. | //+------------------------------------------------------------------+ void CBossHistogram::CopyTo(CBossHistogram &dst) const { dst.Clear(); for(int i=0;i 0 of (q[w]-s[w])^2 | //| i.e. an ordinary squared-Euclidean distance, but summed ONLY | //| over words that actually occur in the query. Words that appear | //| only in the sample are ignored. The intuition: a match is | //| judged by whether the sample reproduces the patterns the query | //| contains, not penalised for extra patterns the query never had. | //| This asymmetry is a core reason BOSS beats plain Euclidean on | //| noisy series, so it is implemented exactly, not "symmetrised". | //+------------------------------------------------------------------+ class CBossClassifier { private: CBossTransform m_transform; //--- training set: parallel arrays of histogram and its label. CBossHistogram m_train[]; string m_label[]; int m_ntrain; public: CBossClassifier(void) { m_ntrain=0; } //--- configure/fit the SFA+bag pipeline from training windows, //--- exactly as CBossTransform::Build. Call once before Add(). bool Build(int word_len,int alphabet, const double &windows[],int count,int win_len); bool IsReady(void) const { return m_transform.IsReady(); } int TrainCount(void)const { return m_ntrain; } //--- add one labelled training series. It is transformed to a bag //--- immediately and stored. Returns false if not ready / empty. bool AddExample(const double &series[],int len,const string label); //--- non-symmetric BOSS distance from query bag q to sample bag s. static double BossDistance(const CBossHistogram &q,const CBossHistogram &s); //--- classify a query series: label of the nearest training bag. //--- 'best_dist' receives that distance. Returns "" if unable. string Classify(const double &series[],int len,double &best_dist) const; //--- expose the transform for vocabulary inspection / teaching. const CBossTransform *Transform(void) const { return GetPointer(m_transform); } //--- read back the i-th stored training pair. string LabelAt(int i) const { return (i>=0 && i class name string //--- map an integer label to its class name (empty if out of range). string ClassName(int code) const { return (code>=0 && code=0 && i=0 && im_seglen) return false; //--- MCB pool: all sliding windows across all training segments. int perSeg=m_seglen-win_len+1; int poolN=m_nseg*perSeg; if(poolN<=0) return false; double pool[]; ArrayResize(pool,poolN*win_len); int pk=0; for(int s=0;swin_len/2) // need >= word_len non-DC coeffs continue; if(!FitMember(win_len,cand[ncand])) continue; double acc=cand[ncand].LooAccuracy(); cwin[ncand]=win_len; cacc[ncand]=acc; if(acc>best) best=acc; ncand++; } if(ncand<=0) return false; //--- keep members within 92% of the best LOO accuracy. double cut=best*BOSS_ENSEMBLE_KEEP; ArrayResize(m_member,ncand); ArrayResize(m_win,ncand); ArrayResize(m_acc,ncand); for(int i=0;i=cut) { m_win[m_count]=cwin[i]; m_acc[m_count]=cacc[i]; //--- rebuild the kept member into the persistent slot. FitMember(cwin[i],m_member[m_count]); m_count++; } return (m_count>0); } //+------------------------------------------------------------------+ //| Classify a query segment by majority vote of the kept members. | //| Ties are broken by the earliest-listed label reaching the top | //| count, which is deterministic given the member order. | //+------------------------------------------------------------------+ string CBossEnsemble::Classify(const double &series[],int len) const { if(m_count<=0) return ""; //--- tally votes as (label,count) pairs. string vlab[]; int vcnt[]; int nv=0; ArrayResize(vlab,m_count); ArrayResize(vcnt,m_count); for(int i=0;ibestc) { bestc=vcnt[v]; best=vlab[v]; } return best; } //+------------------------------------------------------------------+