//+------------------------------------------------------------------+ //| BOSSvsDTWBenchmark.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 "Head-to-head on real bars: dictionary-based BOSS versus elastic DTW, both as 1-nearest-neighbour classifiers of the SAME regime labels on the SAME data." #property version "1.00" #property strict #property script_show_inputs #include #include #include input string Symbol_ = "BTCUSD"; // symbol to pull (falls back to chart symbol) input ENUM_TIMEFRAMES TF = PERIOD_H1; // timeframe input int Bars_ = 50000; // history bars to load input int Segment = 120; // bars per classified segment input int Window = 24; // SFA sliding-window length (inside a segment) input int WordLen = 2; // SFA coefficients kept (robust low-pass point) input int Alphabet = 4; // SFA letters per coefficient slot input double NoiseSigma = 0.4; // stdev of injected noise (in window-stdev units) //--- labeler thresholds (tune these to balance the regime classes): input double TrendACF = 0.05; // lag-1 autocorrelation above this -> TREND input double VolHighMult= 1.3; // volatility above mult*reference -> VOLATILE //--- forward declarations (functions call each other out of order). void RunBenchmark(const CBossClassifier &boss, CBossEnsemble &ens,bool ensOK, const double &segs[],const int &labels[], int offset,int ntest,int seg, const double &protos[],const int &plab[],int nproto, double noise); double Recall(int hit,int tot); //+------------------------------------------------------------------+ //| Load 'count' closes for the chosen symbol/timeframe into 'out', | //| oldest first. Returns the number actually loaded. | //+------------------------------------------------------------------+ int LoadCloses(double &out[]) { string sym=Symbol_; if(sym=="" || !SymbolSelect(sym,true)) sym=_Symbol; double c[]; int got=CopyClose(sym,TF,1,Bars_,c); if(got<=0) { PrintFormat("CopyClose failed for %s: %d",sym,GetLastError()); return 0; } ArraySetAsSeries(c,false); // oldest first ArrayResize(out,got); for(int i=0;i0)?volsum/nseg:0.0); int kept=0, cnt[4]= {0,0,0,0}; for(int s=0;s trend=%d range=%d volatile=%d", kept,Segment,cnt[REGIME_TREND],cnt[REGIME_RANGE],cnt[REGIME_VOLATILE]); if(kept<8) { Print("too few labelled segments; load more bars or shrink Segment"); return; } //--- STEP 2: split into train (first half) and test (second half). int split=kept/2; int ntrain=split, ntest=kept-split; //--- prototype segments for DTW (the train half), flat + labels. double protos[]; int plab[]; ArrayResize(protos,ntrain*Segment); ArrayResize(plab,ntrain); for(int i=0;i %s",ens.MemberCount(),kept); } else Print("ensemble training failed"); //--- STEP 5: benchmark on the test half, clean then noisy. Print("=== clean queries ==="); RunBenchmark(boss,ens,ensOK,segs,labels,split,ntest,Segment,protos,plab,ntrain,0.0); PrintFormat("=== noisy queries (sigma=%.2f) ===",NoiseSigma); RunBenchmark(boss,ens,ensOK,segs,labels,split,ntest,Segment,protos,plab,ntrain,NoiseSigma); Print("=== benchmark complete ==="); } //+------------------------------------------------------------------+ //| Run BOSS and DTW over the test segments and print accuracy, | //| wall-clock, a majority-class baseline and per-class recall. | //| 'offset' is where the test half begins inside segs[]. 'noise'>0 | //| injects noise into each query segment before classifying. | //+------------------------------------------------------------------+ void RunBenchmark(const CBossClassifier &boss, CBossEnsemble &ens,bool ensOK, const double &segs[],const int &labels[], int offset,int ntest,int seg, const double &protos[],const int &plab[],int nproto, double noise) { int bossCorrect=0, ensCorrect=0, dtwCorrect=0; ulong bossUs=0, ensUs=0, dtwUs=0; double q[]; int perTot[4]= {0,0,0,0}; int bossHit[4]= {0,0,0,0}; int ensHit[4]= {0,0,0,0}; int dtwHit[4]= {0,0,0,0}; for(int t=0;t0.0) AddNoise(q,seg,noise); int truth=labels[idx]; string truthName=CRegimeLabeler::RegimeName((ENUM_REGIME)truth); perTot[truth]++; //--- single BOSS: 1-NN over word-frequency bags ulong t0=GetMicrosecondCount(); double bd; string blab=boss.Classify(q,seg,bd); bossUs+=GetMicrosecondCount()-t0; if(blab==truthName) { bossCorrect++; bossHit[truth]++; } //--- BOSS ensemble: majority vote over kept window sizes if(ensOK) { ulong te=GetMicrosecondCount(); string elab=ens.Classify(q,seg); ensUs+=GetMicrosecondCount()-te; if(elab==truthName) { ensCorrect++; ensHit[truth]++; } } //--- DTW 1-NN over the same prototype segments ulong t1=GetMicrosecondCount(); int dlab=DTWClassify(q,seg,protos,plab,nproto); dtwUs+=GetMicrosecondCount()-t1; if(dlab==truth) { dtwCorrect++; dtwHit[truth]++; } } //--- majority-class baseline: always guess the most common test label int major=0; for(int c=1;c<3;c++) if(perTot[c]>perTot[major]) major=c; double baseline=(ntest>0)?100.0*perTot[major]/ntest:0.0; //--- balanced (macro) accuracy: mean of per-class recalls double bMacro=0.0, eMacro=0.0, dMacro=0.0; int seen=0; for(int c=0;c<3;c++) if(perTot[c]>0) { bMacro+=100.0*bossHit[c]/perTot[c]; eMacro+=100.0*ensHit[c]/perTot[c]; dMacro+=100.0*dtwHit[c]/perTot[c]; seen++; } if(seen>0) { bMacro/=seen; eMacro/=seen; dMacro/=seen; } PrintFormat(" baseline (always '%s'): %.1f%%", CRegimeLabeler::RegimeName((ENUM_REGIME)major),baseline); PrintFormat(" single BOSS : %d/%d = %.1f%% macro=%.1f%% (%.1f us/q)", bossCorrect,ntest,100.0*bossCorrect/ntest,bMacro,(double)bossUs/ntest); if(ensOK) PrintFormat(" BOSS ENSEM : %d/%d = %.1f%% macro=%.1f%% (%.1f us/q)", ensCorrect,ntest,100.0*ensCorrect/ntest,eMacro,(double)ensUs/ntest); PrintFormat(" DTW : %d/%d = %.1f%% macro=%.1f%% (%.1f us/q)", dtwCorrect,ntest,100.0*dtwCorrect/ntest,dMacro,(double)dtwUs/ntest); PrintFormat(" recall BOSS[tr=%.0f rg=%.0f vo=%.0f] ENS[tr=%.0f rg=%.0f vo=%.0f] DTW[tr=%.0f rg=%.0f vo=%.0f]", Recall(bossHit[0],perTot[0]),Recall(bossHit[1],perTot[1]),Recall(bossHit[2],perTot[2]), Recall(ensHit[0],perTot[0]), Recall(ensHit[1],perTot[1]), Recall(ensHit[2],perTot[2]), Recall(dtwHit[0],perTot[0]), Recall(dtwHit[1],perTot[1]), Recall(dtwHit[2],perTot[2])); } //+------------------------------------------------------------------+ //| Per-class recall as a percentage, 0 if the class is absent. | //+------------------------------------------------------------------+ double Recall(int hit,int tot) { return (tot>0)?100.0*hit/tot:0.0; } //+------------------------------------------------------------------+