catch22/Include/Catch22/Catch22.mqh
2026-07-12 10:15:50 +00:00

1288 lines
46 KiB
MQL5

//+------------------------------------------------------------------+
//| Catch22.mqh |
//| MMQ — Muhammad Minhas Qamar |
//| www.mql5.com/en/articles/23488 |
//+------------------------------------------------------------------+
#property copyright "MMQ — Muhammad Minhas Qamar"
#property link "https://www.mql5.com/en/articles/23488"
#property version "1.00"
#property strict
#include <Math\Alglib\alglib.mqh>
//+------------------------------------------------------------------+
//| catch22 — the canonical 22-feature time-series set (Lubba et al. |
//| 2019, DMKD). Each feature is one representative of a cluster of |
//| high-performing hctsa features, chosen to be minimally |
//| redundant. Unless noted, every feature runs on the z-scored |
//| window (mean 0, sd 1), exactly as the reference implementation. |
//| |
//| This single class exposes Compute() to fill a 22-element vector, |
//| plus named getters. Heavy math (FFT) uses ALGLIB; the rest is |
//| hand-rolled so the estimators match the published definitions. |
//+------------------------------------------------------------------+
//--- canonical feature order (matches the reference featureList.txt).
//--- Indices into the output vector so the caller can name a column
//--- without memorizing positions.
enum ENUM_CATCH22
{
C22_DN_HistogramMode_5=0, // mode of distribution, 5 bins
C22_DN_HistogramMode_10, // mode of distribution, 10 bins
C22_CO_f1ecac, // first 1/e crossing of the ACF
C22_CO_FirstMin_ac, // lag of first minimum of the ACF
C22_CO_HistogramAMI_even_2_5, // automutual information, lag 2, 5 bins
C22_CO_trev_1_num, // time-reversal asymmetry statistic
C22_MD_hrv_classic_pnn40, // fraction of |diffs| > 0.04 (pNN40)
C22_SB_BinaryStats_mean_longstretch1, // longest run above the mean
C22_SB_TransitionMatrix_3ac_sumdiagcov, // symbol transition-matrix structure
C22_PD_PeriodicityWang_th0_01, // Wang periodicity measure
C22_CO_Embed2_Dist_tau_d_expfit_meandiff, // embedding trajectory outlierness
C22_IN_AutoMutualInfoStats_40_gaussian_fmmi, // first min of Gaussian AMI
C22_FC_LocalSimple_mean1_tauresrat, // ACF-zero ratio of residuals
C22_DN_OutlierInclude_p_001_mdrmd, // timing of positive outliers
C22_DN_OutlierInclude_n_001_mdrmd, // timing of negative outliers
C22_SP_Summaries_welch_rect_area_5_1, // power in lowest 5th of spectrum
C22_SB_BinaryStats_diff_longstretch0, // longest run of consecutive falls
C22_SB_MotifThree_quantile_hh, // entropy of 3-letter motifs
C22_SC_FluctAnal_2_rsrangefit_50_1_logi_prop_r1,// rescaled-range (R/S) scaling
C22_SC_FluctAnal_2_dfa_50_1_2_logi_prop_r1, // DFA scaling exponent
C22_SP_Summaries_welch_rect_centroid, // spectral centroid (Welch)
C22_FC_LocalSimple_mean3_stderr // local mean-3 forecast error
};
#define CATCH22_N 22 // number of features in the canonical set
//+------------------------------------------------------------------+
//| CCatch22 - the public engine. |
//| Call Compute(window, out) with a raw price/return window; it |
//| z-scores the window once, precomputes the shared machinery |
//| (autocorrelation, spectrum, symbolizations) and fills a |
//| 22-element feature vector in canonical order. Named getters read|
//| the last computed vector. |
//+------------------------------------------------------------------+
class CCatch22
{
private:
//--- last computed result
double m_feat[CATCH22_N]; // filled by Compute()
bool m_valid; // true if last Compute() succeeded
//--- shared working buffers (per Compute() call)
double m_x[]; // raw window copy
double m_z[]; // z-scored window (mean 0, sd 1)
int m_n; // window length
double m_mean; // raw window mean
double m_sd; // raw window population sd
double m_acf[]; // autocorrelation, lag 0..m_n-1 (of m_z)
int m_acfN; // number of valid ACF lags computed
//--- shared machinery -------------------------------------------
bool Prepare(const double &window[],int n);
void ComputeACF(void);
double Mean(const double &a[],int n) const;
double Sd(const double &a[],int n,double mean) const;
double Median(const double &a[],int n) const;
int HistogramBinCounts(const double &a[],int n,int nbins,
double &counts[],double &lo,double &binw) const;
void Diff(const double &a[],int n,double &d[]) const;
//--- feature estimators (one per canonical feature) -------------
double HistogramMode(int nbins) const;
double F1ecac(void) const;
double FirstMinAC(void) const;
double HistogramAMI(int lag,int nbins) const;
double Trev(int lag) const;
double HrvPnn40(void) const;
double BinaryStatsMeanLongstretch1(void) const;
double TransitionMatrix3ac(void) const;
double PeriodicityWang(void) const;
double Embed2DistExpfit(void) const;
double AutoMutualInfoFmmi(void) const;
double LocalSimpleMean1Tauresrat(void) const;
double OutlierInclude(int sign) const;
double WelchArea51(void) const;
double BinaryStatsDiffLongstretch0(void) const;
double MotifThreeHH(void) const;
double FluctAnal(int method) const; // 0=R/S, 1=DFA
double SegmentSSR(const double &x[],const double &y[],int a,int b) const;
double WelchCentroid(void) const;
double LocalSimpleMean3Stderr(void) const;
//--- helpers shared by several estimators
int FirstZeroACF(void) const;
void WelchSpectrum(double &psd[],int &npsd) const;
public:
CCatch22(void);
//--- main entry: fills out[CATCH22_N] from a raw window of length n.
bool Compute(const double &window[],int n,double &out[]);
//--- last-result access
bool IsValid(void) const { return m_valid; }
double Feature(int idx) const;
double HistogramMode5(void) const { return Feature(C22_DN_HistogramMode_5); }
double HistogramMode10(void)const { return Feature(C22_DN_HistogramMode_10); }
double DFA(void) const { return Feature(C22_SC_FluctAnal_2_dfa_50_1_2_logi_prop_r1); }
double RSrange(void) const { return Feature(C22_SC_FluctAnal_2_rsrangefit_50_1_logi_prop_r1); }
//--- column name for a given index (diagnostics / CSV headers)
static string Name(int idx);
};
//+------------------------------------------------------------------+
//| Construct an empty, not-yet-computed engine. |
//+------------------------------------------------------------------+
CCatch22::CCatch22(void)
{
m_valid=false;
m_n=0;
m_acfN=0;
m_mean=0.0;
m_sd=0.0;
for(int i=0;i<CATCH22_N;i++)
m_feat[i]=0.0;
}
//+------------------------------------------------------------------+
//| Arithmetic mean of the first n elements of a[]. |
//+------------------------------------------------------------------+
double CCatch22::Mean(const double &a[],int n) const
{
if(n<=0)
return 0.0;
double s=0.0;
for(int i=0;i<n;i++)
s+=a[i];
return s/n;
}
//+------------------------------------------------------------------+
//| Population standard deviation of a[] given its mean. |
//+------------------------------------------------------------------+
double CCatch22::Sd(const double &a[],int n,double mean) const
{
if(n<=0)
return 0.0;
double s=0.0;
for(int i=0;i<n;i++)
{
double d=a[i]-mean;
s+=d*d;
}
return MathSqrt(s/n);
}
//+------------------------------------------------------------------+
//| Median of the first n elements of a[] (does not modify a[]). |
//+------------------------------------------------------------------+
double CCatch22::Median(const double &a[],int n) const
{
if(n<=0)
return 0.0;
double t[];
ArrayResize(t,n);
for(int i=0;i<n;i++)
t[i]=a[i];
ArraySort(t);
if((n&1)==1)
return t[n/2];
return 0.5*(t[n/2-1]+t[n/2]);
}
//+------------------------------------------------------------------+
//| First differences of a[]: d[i] = a[i+1]-a[i], length n-1. |
//+------------------------------------------------------------------+
void CCatch22::Diff(const double &a[],int n,double &d[]) const
{
int m=(n>1)?n-1:0;
ArrayResize(d,m);
for(int i=0;i<m;i++)
d[i]=a[i+1]-a[i];
}
//+------------------------------------------------------------------+
//| Copy the window, compute mean/sd, and z-score into m_z. Returns |
//| false on a degenerate (constant or too-short) window: catch22 is |
//| undefined without variance. The reference z-scores internally |
//| for every feature except mean/sd (not in the set), so we do it |
//| once here and all estimators read m_z. |
//+------------------------------------------------------------------+
bool CCatch22::Prepare(const double &window[],int n)
{
if(n<8) // too short for ACF/DFA scales to exist
return false;
m_n=n;
ArrayResize(m_x,n);
ArrayResize(m_z,n);
for(int i=0;i<n;i++)
m_x[i]=window[i];
m_mean=Mean(m_x,n);
m_sd =Sd(m_x,n,m_mean);
if(m_sd<=0.0 || !MathIsValidNumber(m_sd))
return false; // constant window -> undefined
for(int i=0;i<n;i++)
m_z[i]=(m_x[i]-m_mean)/m_sd;
return true;
}
//+------------------------------------------------------------------+
//| Biased autocorrelation of the z-scored window at lags 0..n-1. |
//| r[k] = (1/n) * sum_{i} z[i]*z[i+k] (divide by n, not n-k, to |
//| match the reference CO_ definitions). Stored in m_acf. |
//+------------------------------------------------------------------+
void CCatch22::ComputeACF(void)
{
int n=m_n;
ArrayResize(m_acf,n);
m_acfN=n;
//--- r[0] normalizes to 1 for a z-scored series (variance = 1)
for(int k=0;k<n;k++)
{
double s=0.0;
for(int i=0;i+k<n;i++)
s+=m_z[i]*m_z[i+k];
m_acf[k]=s/n;
}
}
//+------------------------------------------------------------------+
//| Equal-width histogram of a[] into nbins over [min,max]. Fills |
//| counts[nbins]; returns the bin holding the most mass (mode bin). |
//| lo and binw describe the binning for callers that need centers. |
//+------------------------------------------------------------------+
int CCatch22::HistogramBinCounts(const double &a[],int n,int nbins,
double &counts[],double &lo,double &binw) const
{
ArrayResize(counts,nbins);
ArrayInitialize(counts,0.0);
double mn=a[0],mx=a[0];
for(int i=1;i<n;i++)
{
if(a[i]<mn)
mn=a[i];
if(a[i]>mx)
mx=a[i];
}
lo=mn;
double range=mx-mn;
binw=(range>0.0)?range/nbins:1.0;
if(range<=0.0)
return 0;
for(int i=0;i<n;i++)
{
int b=(int)((a[i]-mn)/binw);
if(b<0)
b=0;
if(b>=nbins)
b=nbins-1; // include the right edge in the last bin
counts[b]+=1.0;
}
//--- locate the fullest bin (ties -> first, as in the reference)
int best=0;
double bestc=counts[0];
for(int b=1;b<nbins;b++)
if(counts[b]>bestc)
{
bestc=counts[b];
best=b;
}
return best;
}
//+------------------------------------------------------------------+
//| DN_HistogramMode_5 / _10 |
//| Center of the most populated histogram bin of the z-scored |
//| window, using 'nbins' equal-width bins. A robust location proxy |
//| for the bulk of the distribution. |
//+------------------------------------------------------------------+
double CCatch22::HistogramMode(int nbins) const
{
double counts[],lo,binw;
int best=HistogramBinCounts(m_z,m_n,nbins,counts,lo,binw);
if(binw<=0.0)
return 0.0;
return lo+(best+0.5)*binw; // center of the modal bin
}
//+------------------------------------------------------------------+
//| CO_f1ecac |
//| First lag at which the ACF drops below 1/e. Linear interpolation|
//| between the straddling lags gives a continuous decorrelation |
//| timescale rather than an integer lag. |
//+------------------------------------------------------------------+
double CCatch22::F1ecac(void) const
{
double thresh=1.0/M_E;
for(int k=0;k<m_acfN-1;k++)
{
if(m_acf[k]>thresh && m_acf[k+1]<=thresh)
{
//--- interpolate the crossing between lag k and k+1
double denom=m_acf[k]-m_acf[k+1];
double frac=(denom!=0.0)?(m_acf[k]-thresh)/denom:0.0;
return (double)k+frac;
}
}
return (double)m_n; // never crosses -> saturate at window length
}
//+------------------------------------------------------------------+
//| CO_FirstMin_ac |
//| Lag of the first local minimum of the ACF. Captures the |
//| dominant oscillation half-period when one exists. |
//+------------------------------------------------------------------+
double CCatch22::FirstMinAC(void) const
{
for(int k=1;k<m_acfN-1;k++)
if(m_acf[k]<m_acf[k-1] && m_acf[k]<m_acf[k+1])
return (double)k;
return (double)m_n;
}
//+------------------------------------------------------------------+
//| First lag at which the ACF becomes <= 0 (first zero crossing). |
//| Used by the local-forecast features to bound the horizon. |
//+------------------------------------------------------------------+
int CCatch22::FirstZeroACF(void) const
{
for(int k=1;k<m_acfN;k++)
if(m_acf[k]<=0.0)
return k;
return m_acfN;
}
//+------------------------------------------------------------------+
//| CO_trev_1_num |
//| Numerator of the time-reversal asymmetry statistic at lag 1: |
//| mean( (z[i+1]-z[i])^3 ). |
//| Non-zero values indicate irreversibility (nonlinearity). |
//+------------------------------------------------------------------+
double CCatch22::Trev(int lag) const
{
int m=m_n-lag;
if(m<=0)
return 0.0;
double s=0.0;
for(int i=0;i<m;i++)
{
double d=m_z[i+lag]-m_z[i];
s+=d*d*d;
}
return s/m;
}
//+------------------------------------------------------------------+
//| MD_hrv_classic_pnn40 |
//| pNN40 from heart-rate-variability: proportion of successive |
//| absolute differences exceeding 0.04. Applied to the z-scored |
//| series (the reference divides by 1000 internally on raw data; |
//| on z-scored input the 0.04 threshold is used directly). |
//+------------------------------------------------------------------+
double CCatch22::HrvPnn40(void) const
{
double d[];
Diff(m_z,m_n,d);
int m=ArraySize(d);
if(m<=0)
return 0.0;
int cnt=0;
for(int i=0;i<m;i++)
if(MathAbs(d[i])>0.04)
cnt++;
return (double)cnt/m;
}
//+------------------------------------------------------------------+
//| SB_BinaryStats_mean_longstretch1 |
//| Symbolize z as 1 (above mean) / 0 (below); return the length of |
//| the longest consecutive run of 1s. The z-scored mean is 0, so |
//| "above mean" is simply z[i] > 0. |
//+------------------------------------------------------------------+
double CCatch22::BinaryStatsMeanLongstretch1(void) const
{
int longest=0,run=0;
for(int i=0;i<m_n;i++)
{
if(m_z[i]>0.0)
{
run++;
if(run>longest)
longest=run;
}
else
run=0;
}
//--- hctsa measures the stretch spanned (transitions), i.e. run+1
return (longest>0)?(double)(longest+1):0.0;
}
//+------------------------------------------------------------------+
//| SB_BinaryStats_diff_longstretch0 |
//| Symbolize the sign of successive differences; return the length |
//| of the longest consecutive run of decreases (diff < 0). |
//+------------------------------------------------------------------+
double CCatch22::BinaryStatsDiffLongstretch0(void) const
{
double d[];
Diff(m_z,m_n,d);
int m=ArraySize(d);
int longest=0,run=0;
for(int i=0;i<m;i++)
{
if(d[i]<0.0)
{
run++;
if(run>longest)
longest=run;
}
else
run=0;
}
//--- hctsa measures the stretch spanned (transitions), i.e. run+1
return (longest>0)?(double)(longest+1):0.0;
}
//+------------------------------------------------------------------+
//| CO_HistogramAMI_even_2_5 |
//| Automutual information between z[t] and z[t+lag] estimated from |
//| a 2-D equal-width histogram with 'nbins' bins per axis over the |
//| data range (the "even" binning of the reference): |
//| AMI = sum_ij p_ij * log( p_ij / (p_i * p_j) ). |
//| A nonlinear dependence measure; unlike the ACF it detects |
//| structure invisible to linear correlation. |
//+------------------------------------------------------------------+
double CCatch22::HistogramAMI(int lag,int nbins) const
{
int m=m_n-lag;
if(m<=1 || nbins<=1)
return 0.0;
//--- shared bin edges over the full z range (both axes identical)
double mn=m_z[0],mx=m_z[0];
for(int i=0;i<m_n;i++)
{
if(m_z[i]<mn)
mn=m_z[i];
if(m_z[i]>mx)
mx=m_z[i];
}
double range=mx-mn;
if(range<=0.0)
return 0.0;
double binw=range/nbins;
//--- joint and marginal counts
double pj[];
ArrayResize(pj,nbins*nbins);
ArrayInitialize(pj,0.0);
double pa[];
ArrayResize(pa,nbins);
ArrayInitialize(pa,0.0);
double pb[];
ArrayResize(pb,nbins);
ArrayInitialize(pb,0.0);
for(int i=0;i<m;i++)
{
int ba=(int)((m_z[i] -mn)/binw);
if(ba<0)
ba=0;
if(ba>=nbins)
ba=nbins-1;
int bb=(int)((m_z[i+lag]-mn)/binw);
if(bb<0)
bb=0;
if(bb>=nbins)
bb=nbins-1;
pj[ba*nbins+bb]+=1.0;
pa[ba]+=1.0;
pb[bb]+=1.0;
}
//--- normalize to probabilities and accumulate the MI sum
double ami=0.0;
for(int a=0;a<nbins;a++)
{
double pai=pa[a]/m;
if(pai<=0.0)
continue;
for(int b=0;b<nbins;b++)
{
double pij=pj[a*nbins+b]/m;
if(pij<=0.0)
continue;
double pbi=pb[b]/m;
ami+=pij*MathLog(pij/(pai*pbi));
}
}
return ami;
}
//+------------------------------------------------------------------+
//| IN_AutoMutualInfoStats_40_gaussian_fmmi |
//| First minimum of the Gaussian-estimated automutual information |
//| as a function of lag, scanned up to tau_max=40 (capped at n-1). |
//| Under a Gaussian assumption AMI(k) = -0.5*log(1 - r[k]^2) where |
//| r[k] is the autocorrelation; we return the first lag at which |
//| that curve turns up. |
//+------------------------------------------------------------------+
double CCatch22::AutoMutualInfoFmmi(void) const
{
int taumax=(int)MathMin(40,m_acfN-1);
if(taumax<2)
return (double)taumax;
double ami[];
ArrayResize(ami,taumax+1);
for(int k=1;k<=taumax;k++)
{
double r=m_acf[k];
double t=1.0-r*r;
ami[k]=(t>0.0)?-0.5*MathLog(t):50.0; // large but finite if |r|~1
}
//--- first minimum = first lag where the AMI curve stops decreasing,
//--- i.e. the earliest k>=1 with ami[k+1] > ami[k] (the reference
//--- reports the lag at the turn, indexed from 1).
for(int k=1;k<taumax;k++)
if(ami[k+1]>ami[k])
return (double)k;
return (double)taumax;
}
//+------------------------------------------------------------------+
//| CO_Embed2_Dist_tau_d_expfit_meandiff |
//| Embed the series in 2-D at lag tau (= first zero of the ACF): |
//| points (z[i], z[i+tau]). Take the Euclidean step lengths between|
//| successive embedded points, fit an exponential to their |
//| distribution, and return the mean absolute deviation of the |
//| empirical distribution from that exponential fit. A measure of |
//| how "outlier-prone" trajectories are in phase space. |
//+------------------------------------------------------------------+
double CCatch22::Embed2DistExpfit(void) const
{
int tau=FirstZeroACF();
if(tau<1)
tau=1;
int m=m_n-tau; // number of embedded points
if(m<3)
return 0.0;
//--- successive step lengths in the 2-D embedding
double dist[];
ArrayResize(dist,m-1);
for(int i=0;i<m-1;i++)
{
double dx=m_z[i+1]-m_z[i];
double dy=m_z[i+1+tau]-m_z[i+tau];
dist[i]=MathSqrt(dx*dx+dy*dy);
}
int nd=m-1;
//--- exponential MLE: lambda = 1/mean, pdf f(x)=lambda*exp(-lambda x)
double meand=Mean(dist,nd);
if(meand<=0.0)
return 0.0;
double lambda=1.0/meand;
//--- compare a histogram of the distances to the fitted exponential
//--- density and return the mean absolute difference across bins.
int nbins=(int)MathSqrt((double)nd);
if(nbins<1)
nbins=1;
double counts[],lo,binw;
HistogramBinCounts(dist,nd,nbins,counts,lo,binw);
if(binw<=0.0)
return 0.0;
double sumabs=0.0;
for(int b=0;b<nbins;b++)
{
double emp=counts[b]/(nd*binw); // empirical density
double center=lo+(b+0.5)*binw;
double fit=lambda*MathExp(-lambda*center); // exponential density
sumabs+=MathAbs(emp-fit);
}
return sumabs/nbins;
}
//+------------------------------------------------------------------+
//| FC_LocalSimple_mean1_tauresrat |
//| One-step-ahead forecast using the previous single value |
//| (local mean of window length 1). Compute the residuals, then |
//| return the ratio of the first ACF-zero of the residuals to the |
//| first ACF-zero of the original series. ~1 means the simple |
//| predictor did not change the correlation timescale. |
//+------------------------------------------------------------------+
double CCatch22::LocalSimpleMean1Tauresrat(void) const
{
int m=m_n-1;
if(m<3)
return 0.0;
//--- residuals of the naive "predict previous value" model
double res[];
ArrayResize(res,m);
for(int i=0;i<m;i++)
res[i]=m_z[i+1]-m_z[i];
//--- first ACF-zero of the residuals (biased ACF, as elsewhere)
double rmean=Mean(res,m);
double r0=0.0;
for(int i=0;i<m;i++)
{
double d=res[i]-rmean;
r0+=d*d;
}
r0/=m;
int tauRes=m;
if(r0>0.0)
{
for(int k=1;k<m;k++)
{
double s=0.0;
for(int i=0;i+k<m;i++)
s+=(res[i]-rmean)*(res[i+k]-rmean);
if((s/m)/r0<=0.0)
{
tauRes=k;
break;
}
}
}
int tauOrig=FirstZeroACF();
if(tauOrig<1)
tauOrig=1;
return (double)tauRes/(double)tauOrig;
}
//+------------------------------------------------------------------+
//| FC_LocalSimple_mean3_stderr |
//| Forecast each point as the mean of the previous 3 values; return|
//| the standard error (population sd) of the forecast residuals. |
//| Higher = the series is less predictable from a short local mean.|
//+------------------------------------------------------------------+
double CCatch22::LocalSimpleMean3Stderr(void) const
{
int train=3;
int m=m_n-train;
if(m<=1)
return 0.0;
double res[];
ArrayResize(res,m);
for(int i=0;i<m;i++)
{
double f=(m_z[i]+m_z[i+1]+m_z[i+2])/3.0; // mean of previous 3
res[i]=m_z[i+train]-f;
}
double rmean=Mean(res,m);
return Sd(res,m,rmean);
}
//+------------------------------------------------------------------+
//| DN_OutlierInclude_p_001 / n_001 (mdrmd) |
//| Progressively raise a threshold from 0 upward (sign=+1) or lower|
//| it from 0 downward (sign=-1) in small steps. At each level keep |
//| the timings (indices) of points beyond the threshold, and track |
//| the median of those timings normalized to [-1,1] about the |
//| series midpoint. The feature is the median across thresholds of |
//| that centered median-timing (mdrmd). Detects whether extremes |
//| cluster early or late in the window. |
//+------------------------------------------------------------------+
double CCatch22::OutlierInclude(int sign) const
{
if(m_n<3)
return 0.0;
double inc=0.01; // threshold step (reference: 0.01)
double maxabs=0.0;
for(int i=0;i<m_n;i++)
if(MathAbs(m_z[i])>maxabs)
maxabs=MathAbs(m_z[i]);
if(maxabs<=0.0)
return 0.0;
int nlevels=(int)(maxabs/inc);
if(nlevels<1)
return 0.0;
double mids[];
ArrayResize(mids,nlevels);
int nvalid=0;
double half=(m_n-1)/2.0;
for(int L=1;L<=nlevels;L++)
{
double thr=L*inc;
//--- collect indices beyond the signed threshold
double idx[];
int c=0;
ArrayResize(idx,m_n);
for(int i=0;i<m_n;i++)
{
double v=sign*m_z[i];
if(v>=thr)
idx[c++]=(double)i;
}
if(c==0)
continue;
//--- reference stops once fewer than ~2% of points remain
if((double)c/m_n<0.02)
break;
ArrayResize(idx,c);
double medIdx=Median(idx,c);
//--- center to [-1,1] about the window midpoint
mids[nvalid++]=(medIdx-half)/half;
}
if(nvalid==0)
return 0.0;
ArrayResize(mids,nvalid);
return Median(mids,nvalid);
}
//+------------------------------------------------------------------+
//| SB_MotifThree_quantile_hh |
//| Symbolize z into 3 letters by quantile (terciles: below the |
//| 33rd pct = 0, middle = 1, above the 67th pct = 2). Count the 9 |
//| ordered 2-letter transitions and return the Shannon entropy of |
//| that 3x3 transition distribution (natural log). Higher entropy =|
//| richer short-range symbolic dynamics. |
//+------------------------------------------------------------------+
double CCatch22::MotifThreeHH(void) const
{
if(m_n<3)
return 0.0;
//--- tercile edges from the sorted z-scored window
double s[];
ArrayResize(s,m_n);
for(int i=0;i<m_n;i++)
s[i]=m_z[i];
ArraySort(s);
double q1=s[(int)(0.3333*(m_n-1))];
double q2=s[(int)(0.6667*(m_n-1))];
//--- symbolize into {0,1,2}
int sym[];
ArrayResize(sym,m_n);
for(int i=0;i<m_n;i++)
{
if(m_z[i]<=q1)
sym[i]=0;
else
if(m_z[i]<=q2)
sym[i]=1;
else
sym[i]=2;
}
//--- 3x3 transition counts over consecutive pairs
double cnt[9];
ArrayInitialize(cnt,0.0);
int total=m_n-1;
for(int i=0;i<total;i++)
cnt[sym[i]*3+sym[i+1]]+=1.0;
//--- Shannon entropy of the pair distribution
double h=0.0;
for(int k=0;k<9;k++)
{
double p=cnt[k]/total;
if(p>0.0)
h-=p*MathLog(p);
}
return h;
}
//+------------------------------------------------------------------+
//| SB_TransitionMatrix_3ac_sumdiagcov |
//| Symbolize z into 3 equal-width states, then build the 3x3 |
//| row-normalized transition-probability matrix at a step equal to |
//| the ACF-based downsample tau (first ACF zero). The feature is |
//| the sum of the covariance-matrix diagonal (i.e. the total |
//| variance) of the transition matrix's columns — a compact |
//| summary of how structured the state transitions are. |
//+------------------------------------------------------------------+
double CCatch22::TransitionMatrix3ac(void) const
{
int tau=FirstZeroACF();
if(tau<1)
tau=1;
if(m_n-tau<3)
return 0.0;
//--- 3 equal-width states over the z range
double mn=m_z[0],mx=m_z[0];
for(int i=0;i<m_n;i++)
{
if(m_z[i]<mn)
mn=m_z[i];
if(m_z[i]>mx)
mx=m_z[i];
}
double range=mx-mn;
if(range<=0.0)
return 0.0;
double binw=range/3.0;
int sym[];
ArrayResize(sym,m_n);
for(int i=0;i<m_n;i++)
{
int b=(int)((m_z[i]-mn)/binw);
if(b<0)
b=0;
if(b>2)
b=2;
sym[i]=b;
}
//--- transition counts at step tau, then row-normalize to probabilities
double T[9];
ArrayInitialize(T,0.0);
double rowsum[3];
ArrayInitialize(rowsum,0.0);
int m=m_n-tau;
for(int i=0;i<m;i++)
{
int a=sym[i], b=sym[i+tau];
T[a*3+b]+=1.0;
rowsum[a]+=1.0;
}
for(int a=0;a<3;a++)
if(rowsum[a]>0.0)
for(int b=0;b<3;b++)
T[a*3+b]/=rowsum[a];
//--- sum of the diagonal of the column-covariance matrix of T.
//--- Treat the 3 columns as variables observed over 3 rows; the
//--- covariance diagonal is the per-column variance, summed.
double colmean[3];
for(int b=0;b<3;b++)
{
double s=0.0;
for(int a=0;a<3;a++)
s+=T[a*3+b];
colmean[b]=s/3.0;
}
double sumdiag=0.0;
for(int b=0;b<3;b++)
{
double v=0.0;
for(int a=0;a<3;a++)
{
double d=T[a*3+b]-colmean[b];
v+=d*d;
}
sumdiag+=v/3.0; // population variance of column b
}
return sumdiag;
}
//+------------------------------------------------------------------+
//| PD_PeriodicityWang_th0_01 |
//| Wang's periodicity measure. Spline-detrend is approximated by a |
//| light three-point smoother; then find, in the ACF, the first |
//| peak (local maximum preceded by a trough) whose height exceeds |
//| a small threshold (0.01). Returns that lag as the estimated |
//| fundamental period, or 0 if none qualifies. |
//+------------------------------------------------------------------+
double CCatch22::PeriodicityWang(void) const
{
double th=0.01;
//--- walk the ACF: require a preceding trough, an upturn, and a peak
int i=0;
//--- skip the initial monotone descent from lag 0
while(i<m_acfN-1 && m_acf[i+1]<m_acf[i])
i++;
//--- from here look for the first local maximum above the threshold
for(int k=i+1;k<m_acfN-1;k++)
{
if(m_acf[k]>m_acf[k-1] && m_acf[k]>=m_acf[k+1] && m_acf[k]>th)
return (double)k;
}
return 0.0;
}
//+------------------------------------------------------------------+
//| Welch-style power spectral density of the z-scored window via a |
//| single periodogram (rectangular window), using ALGLIB's real |
//| FFT. psd[j] = |F[j]|^2 for j=0..N/2. The SP_ features summarize |
//| this density. A single segment matches the "rect" variant used |
//| by the reference at these window lengths. |
//+------------------------------------------------------------------+
void CCatch22::WelchSpectrum(double &psd[],int &npsd) const
{
int n=m_n;
double a[];
ArrayResize(a,n);
for(int i=0;i<n;i++)
a[i]=m_z[i];
complex f[];
CAlglib::FFTR1D(a,n,f); // real FFT -> complex spectrum length n
int half=n/2;
npsd=half+1; // 0..Nyquist inclusive
ArrayResize(psd,npsd);
for(int j=0;j<npsd;j++)
{
double re=f[j].real;
double im=f[j].imag;
psd[j]=(re*re+im*im)/n; // periodogram, normalized by n
}
}
//+------------------------------------------------------------------+
//| SP_Summaries_welch_rect_area_5_1 |
//| Fraction of total spectral power contained in the lowest fifth |
//| of the frequency axis. High values indicate low-frequency |
//| (trend-like) dominance. |
//+------------------------------------------------------------------+
double CCatch22::WelchArea51(void) const
{
double psd[];
int np;
WelchSpectrum(psd,np);
if(np<=1)
return 0.0;
double total=0.0;
for(int j=0;j<np;j++)
total+=psd[j];
if(total<=0.0)
return 0.0;
int cut=np/5; // lowest fifth of the frequency bins
if(cut<1)
cut=1;
double low=0.0;
for(int j=0;j<cut;j++)
low+=psd[j];
return low/total;
}
//+------------------------------------------------------------------+
//| SP_Summaries_welch_rect_centroid |
//| Spectral centroid: the power-weighted mean angular frequency of |
//| the periodogram. Frequencies run 0..pi across the np bins. |
//+------------------------------------------------------------------+
double CCatch22::WelchCentroid(void) const
{
double psd[];
int np;
WelchSpectrum(psd,np);
if(np<=1)
return 0.0;
double num=0.0,den=0.0;
for(int j=0;j<np;j++)
{
double w=M_PI*(double)j/(double)(np-1); // angular frequency 0..pi
num+=w*psd[j];
den+=psd[j];
}
return (den>0.0)?num/den:0.0;
}
//+------------------------------------------------------------------+
//| Sum of squared residuals of an OLS line fit to the points |
//| (x[a..b], y[a..b]) inclusive. Used by the piecewise fluctuation |
//| fit below. Returns 0 for a segment of fewer than 3 points (a |
//| line through <=2 points is exact). |
//+------------------------------------------------------------------+
double CCatch22::SegmentSSR(const double &x[],const double &y[],int a,int b) const
{
int m=b-a+1;
if(m<3)
return 0.0;
double sx=0,sy=0,sxx=0,sxy=0;
for(int i=a;i<=b;i++)
{
sx+=x[i];
sy+=y[i];
sxx+=x[i]*x[i];
sxy+=x[i]*y[i];
}
double denom=m*sxx-sx*sx;
if(denom==0.0)
return 0.0;
double slope=(m*sxy-sx*sy)/denom;
double icpt =(sy-slope*sx)/m;
double ssr=0.0;
for(int i=a;i<=b;i++)
{
double fit=icpt+slope*x[i];
double r=y[i]-fit;
ssr+=r*r;
}
return ssr;
}
//+------------------------------------------------------------------+
//| SC_FluctAnal_2_dfa / _rsrangefit (..._logi_prop_r1) |
//| hctsa fluctuation analysis of the integrated z-series. At each |
//| of a set of log-spaced scales s (tau_min..n/2) we measure a |
//| fluctuation F(s): |
//| method 0 (R/S) : mean rescaled range (range/std) of the |
//| profile over non-overlapping windows; |
//| method 1 (DFA) : rms of linear-detrend residuals over |
//| non-overlapping windows. |
//| On the log-log curve (log s, log F) we then locate the "first |
//| linear scaling region": for every interior split point we fit |
//| two separate lines (left and right segments) and pick the split |
//| that minimizes the combined residual sum of squares. The feature|
//| 'prop_r1' is the PROPORTION of scales in that first region, |
//| i.e. (split+1)/nScales — NOT the scaling exponent itself. |
//+------------------------------------------------------------------+
double CCatch22::FluctAnal(int method) const
{
int n=m_n;
//--- integrate the z-scored series into a profile (cumulative sum)
double prof[];
ArrayResize(prof,n);
double run=0.0;
for(int i=0;i<n;i++)
{
run+=m_z[i];
prof[i]=run;
}
//--- log-spaced (logi) scales from tau_min=5 to n/2, deduplicated
int smin=5, smax=n/2;
if(smax<=smin)
return 0.0;
int scales[];
double logS[],logF[];
ArrayResize(scales,64);
ArrayResize(logS,64);
ArrayResize(logF,64);
int npts=0;
double logmin=MathLog((double)smin);
double logmax=MathLog((double)smax);
int nsteps=50; // '50' in the feature name (max scale points)
int lastS=-1;
for(int k=0;k<nsteps && npts<64;k++)
{
double frac=(nsteps>1)?(double)k/(double)(nsteps-1):0.0;
int s=(int)MathRound(MathExp(logmin+frac*(logmax-logmin)));
if(s<=lastS) // enforce strictly increasing integer scales
s=lastS+1;
if(s>smax)
break;
lastS=s;
int nwin=n/s; // non-overlapping windows
if(nwin<1)
continue;
double fsum=0.0;
int used=0;
for(int w=0;w<nwin;w++)
{
int start=w*s;
if(method==1)
{
//--- DFA: linear detrend, rms of residuals
double sx=0,sy=0,sxx=0,sxy=0;
for(int i=0;i<s;i++)
{
double xx=(double)i;
double yy=prof[start+i];
sx+=xx;
sy+=yy;
sxx+=xx*xx;
sxy+=xx*yy;
}
double denom=s*sxx-sx*sx;
double slope=(denom!=0.0)?(s*sxy-sx*sy)/denom:0.0;
double icpt =(s>0)?(sy-slope*sx)/s:0.0;
double ss=0.0;
for(int i=0;i<s;i++)
{
double fit=icpt+slope*i;
double r=prof[start+i]-fit;
ss+=r*r;
}
fsum+=MathSqrt(ss/s);
used++;
}
else
{
//--- R/S: rescaled range of the profile within the window
double mn=prof[start],mx=prof[start],sm=0.0;
for(int i=0;i<s;i++)
{
double v=prof[start+i];
if(v<mn)
mn=v;
if(v>mx)
mx=v;
sm+=v;
}
double mean=sm/s;
double var=0.0;
for(int i=0;i<s;i++)
{
double d=prof[start+i]-mean;
var+=d*d;
}
double sd=MathSqrt(var/s);
if(sd>0.0)
{
fsum+=(mx-mn)/sd;
used++;
}
}
}
if(used>0)
{
double F=fsum/used;
if(F>0.0)
{
scales[npts]=s;
logS[npts]=MathLog((double)s);
logF[npts]=MathLog(F);
npts++;
}
}
}
if(npts<4)
return 0.0;
//--- find the split minimizing two-segment linear residuals. The first
//--- region is scales[0..split]; we require >=2 points per side.
int bestSplit=1;
double bestSSR=DBL_MAX;
for(int split=1;split<npts-2;split++)
{
double ssr=SegmentSSR(logS,logF,0,split)
+SegmentSSR(logS,logF,split+1,npts-1);
if(ssr<bestSSR)
{
bestSSR=ssr;
bestSplit=split;
}
}
//--- proportion of the scale range in the first linear region
return (double)(bestSplit+1)/(double)npts;
}
//+------------------------------------------------------------------+
//| Compute the full 22-feature vector from a raw window of length n.|
//| Z-scores once, precomputes the ACF, then fills every feature in |
//| canonical order. On a degenerate window returns false and leaves|
//| out[] zero-filled. out[] is resized to CATCH22_N. |
//+------------------------------------------------------------------+
bool CCatch22::Compute(const double &window[],int n,double &out[])
{
m_valid=false;
ArrayResize(out,CATCH22_N);
ArrayInitialize(out,0.0);
for(int i=0;i<CATCH22_N;i++)
m_feat[i]=0.0;
if(!Prepare(window,n))
{
for(int i=0;i<CATCH22_N;i++)
out[i]=0.0;
return false;
}
ComputeACF();
//--- fill in canonical order (indices from ENUM_CATCH22)
m_feat[C22_DN_HistogramMode_5] = HistogramMode(5);
m_feat[C22_DN_HistogramMode_10] = HistogramMode(10);
m_feat[C22_CO_f1ecac] = F1ecac();
m_feat[C22_CO_FirstMin_ac] = FirstMinAC();
m_feat[C22_CO_HistogramAMI_even_2_5] = HistogramAMI(2,5);
m_feat[C22_CO_trev_1_num] = Trev(1);
m_feat[C22_MD_hrv_classic_pnn40] = HrvPnn40();
m_feat[C22_SB_BinaryStats_mean_longstretch1] = BinaryStatsMeanLongstretch1();
m_feat[C22_SB_TransitionMatrix_3ac_sumdiagcov] = TransitionMatrix3ac();
m_feat[C22_PD_PeriodicityWang_th0_01] = PeriodicityWang();
m_feat[C22_CO_Embed2_Dist_tau_d_expfit_meandiff] = Embed2DistExpfit();
m_feat[C22_IN_AutoMutualInfoStats_40_gaussian_fmmi] = AutoMutualInfoFmmi();
m_feat[C22_FC_LocalSimple_mean1_tauresrat] = LocalSimpleMean1Tauresrat();
m_feat[C22_DN_OutlierInclude_p_001_mdrmd] = OutlierInclude(+1);
m_feat[C22_DN_OutlierInclude_n_001_mdrmd] = OutlierInclude(-1);
m_feat[C22_SP_Summaries_welch_rect_area_5_1] = WelchArea51();
m_feat[C22_SB_BinaryStats_diff_longstretch0] = BinaryStatsDiffLongstretch0();
m_feat[C22_SB_MotifThree_quantile_hh] = MotifThreeHH();
m_feat[C22_SC_FluctAnal_2_rsrangefit_50_1_logi_prop_r1]= FluctAnal(0);
m_feat[C22_SC_FluctAnal_2_dfa_50_1_2_logi_prop_r1] = FluctAnal(1);
m_feat[C22_SP_Summaries_welch_rect_centroid] = WelchCentroid();
m_feat[C22_FC_LocalSimple_mean3_stderr] = LocalSimpleMean3Stderr();
//--- guard against NaN/Inf leaking into downstream ML
for(int i=0;i<CATCH22_N;i++)
{
if(!MathIsValidNumber(m_feat[i]))
m_feat[i]=0.0;
out[i]=m_feat[i];
}
m_valid=true;
return true;
}
//+------------------------------------------------------------------+
//| Read one feature from the last Compute() by canonical index. |
//+------------------------------------------------------------------+
double CCatch22::Feature(int idx) const
{
if(idx<0 || idx>=CATCH22_N)
return 0.0;
return m_feat[idx];
}
//+------------------------------------------------------------------+
//| Canonical feature name for a column index (CSV headers / logs). |
//+------------------------------------------------------------------+
static string CCatch22::Name(int idx)
{
switch(idx)
{
case C22_DN_HistogramMode_5:
return "DN_HistogramMode_5";
case C22_DN_HistogramMode_10:
return "DN_HistogramMode_10";
case C22_CO_f1ecac:
return "CO_f1ecac";
case C22_CO_FirstMin_ac:
return "CO_FirstMin_ac";
case C22_CO_HistogramAMI_even_2_5:
return "CO_HistogramAMI_even_2_5";
case C22_CO_trev_1_num:
return "CO_trev_1_num";
case C22_MD_hrv_classic_pnn40:
return "MD_hrv_classic_pnn40";
case C22_SB_BinaryStats_mean_longstretch1:
return "SB_BinaryStats_mean_longstretch1";
case C22_SB_TransitionMatrix_3ac_sumdiagcov:
return "SB_TransitionMatrix_3ac_sumdiagcov";
case C22_PD_PeriodicityWang_th0_01:
return "PD_PeriodicityWang_th0_01";
case C22_CO_Embed2_Dist_tau_d_expfit_meandiff:
return "CO_Embed2_Dist_tau_d_expfit_meandiff";
case C22_IN_AutoMutualInfoStats_40_gaussian_fmmi:
return "IN_AutoMutualInfoStats_40_gaussian_fmmi";
case C22_FC_LocalSimple_mean1_tauresrat:
return "FC_LocalSimple_mean1_tauresrat";
case C22_DN_OutlierInclude_p_001_mdrmd:
return "DN_OutlierInclude_p_001_mdrmd";
case C22_DN_OutlierInclude_n_001_mdrmd:
return "DN_OutlierInclude_n_001_mdrmd";
case C22_SP_Summaries_welch_rect_area_5_1:
return "SP_Summaries_welch_rect_area_5_1";
case C22_SB_BinaryStats_diff_longstretch0:
return "SB_BinaryStats_diff_longstretch0";
case C22_SB_MotifThree_quantile_hh:
return "SB_MotifThree_quantile_hh";
case C22_SC_FluctAnal_2_rsrangefit_50_1_logi_prop_r1:
return "SC_FluctAnal_2_rsrangefit_50_1_logi_prop_r1";
case C22_SC_FluctAnal_2_dfa_50_1_2_logi_prop_r1:
return "SC_FluctAnal_2_dfa_50_1_2_logi_prop_r1";
case C22_SP_Summaries_welch_rect_centroid:
return "SP_Summaries_welch_rect_centroid";
case C22_FC_LocalSimple_mean3_stderr:
return "FC_LocalSimple_mean3_stderr";
}
return "unknown";
}
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+