//+------------------------------------------------------------------+ //| SAXTransform.mqh | //| MMQ — Muhammad Minhas Qamar | //| www.mql5.com/en/articles/23484 | //+------------------------------------------------------------------+ #property copyright "MMQ — Muhammad Minhas Qamar" #property link "https://www.mql5.com/en/articles/23484" #property version "1.00" #property strict //+------------------------------------------------------------------+ //| Symbolic Aggregate approXimation (SAX) — core transform. | //| | //| A raw window of prices is turned into a short string of letters | //| in three steps: | //| 1. z-normalize the window (shape, not price level) | //| 2. PAA: average it into 'w' cells (dimensionality reduction) | //| 3. map each cell to a letter using equal-probability | //| breakpoints of the standard normal (discretization) | //| | //| Letters are stored as int codes 0..a-1 (0 = lowest band). The | //| MINDIST routine returns a distance between two words that | //| provably lower-bounds the true Euclidean distance of the | //| z-normalized series — this is what makes SAX search sound. | //+------------------------------------------------------------------+ //--- status of a single encode attempt enum ENUM_SAX_STATUS { SAX_OK, // word produced and usable SAX_BAD_PARAMS, // w/a out of range, or len < w SAX_FLAT_WINDOW // stdev ~ 0: window is flat, no meaningful shape }; //--- limits kept deliberately small; SAX words are meant to be short #define SAX_MIN_ALPHABET 2 #define SAX_MAX_ALPHABET 10 #define SAX_FLAT_EPS 1.0e-12 // stdev below this -> treat window as flat //+------------------------------------------------------------------+ //| CSAXTransform - encodes windows and compares words. | //| | //| One instance is configured once (word length w, alphabet size | //| a) and then reused to encode many windows. The Gaussian | //| breakpoints for the chosen alphabet are built once at Configure | //| time and cached. | //+------------------------------------------------------------------+ class CSAXTransform { private: //--- configuration int m_w; // word length (number of PAA cells / letters) int m_a; // alphabet size (number of symbols) bool m_ready; // Configure() succeeded //--- cached breakpoints: a-1 cut points that split N(0,1) into //--- 'a' equal-probability bands. m_beta[i] is the i-th cut. double m_beta[]; //--- helpers void BuildBreakpoints(void); double NormInv(double p) const; public: CSAXTransform(void); //--- set word length and alphabet; rebuilds breakpoints. Returns //--- false if parameters are out of the supported range. bool Configure(int word_len,int alphabet); bool IsReady(void) const { return m_ready; } int WordLength(void)const { return m_w; } int Alphabet(void) const { return m_a; } //--- z-normalize src[0..len-1] into dst[]. Returns false on a flat //--- window (stdev ~ 0), leaving dst untouched. bool ZNormalize(const double &src[],int len,double &dst[]) const; //--- PAA: average 'src' (length len) into 'w' cells -> paa[0..w-1]. //--- Handles len not divisible by w via fractional cell boundaries. bool PAA(const double &src[],int len,double &paa[]) const; //--- full encode: raw window -> integer symbol codes word[0..w-1]. //--- 'raw' holds len prices in chronological order (oldest first). ENUM_SAX_STATUS Encode(const double &raw[],int len,int &word[]) const; //--- map a single z-normalized value to its symbol code 0..a-1. int SymbolOf(double zval) const; //--- MINDIST between two words of equal length. Lower-bounds the //--- Euclidean distance of the underlying z-normalized series when //--- scaled by sqrt(n/w) (see Lin & Keogh, 2003). 'orig_len' is n, //--- the length of the original window before PAA. double MinDist(const int &wordA[],const int &wordB[],int orig_len) const; //--- distance between two symbol codes under the current alphabet: //--- 0 for adjacent/equal bands, otherwise the gap between the //--- outer breakpoints they straddle. This is the cell table used //--- by MINDIST; exposed for inspection/teaching. double CellDist(int c1,int c2) const; //--- render a word as a lowercase letter string ("cbaabdcc") for //--- logging and on-chart display. string WordToString(const int &word[]) const; }; //+------------------------------------------------------------------+ //| Construct an unconfigured transform. | //+------------------------------------------------------------------+ CSAXTransform::CSAXTransform(void) { m_w=0; m_a=0; m_ready=false; } //+------------------------------------------------------------------+ //| Configure word length and alphabet and cache the breakpoints. | //+------------------------------------------------------------------+ bool CSAXTransform::Configure(int word_len,int alphabet) { m_ready=false; if(word_len<1) return false; if(alphabetSAX_MAX_ALPHABET) return false; m_w=word_len; m_a=alphabet; BuildBreakpoints(); m_ready=true; return true; } //+------------------------------------------------------------------+ //| Inverse standard-normal CDF (probit) via Acklam's rational | //| approximation. Accurate to ~1e-9 over p in (0,1), which is far | //| more than the breakpoints need. Returns the z with P(Z=1.0) return DBL_MAX; double q,r; if(pphigh) { //--- upper tail q=MathSqrt(-2.0*MathLog(1.0-p)); return -(((((c1*q+c2)*q+c3)*q+c4)*q+c5)*q+c6) / ((((d1*q+d2)*q+d3)*q+d4)*q+1.0); } //--- central region q=p-0.5; r=q*q; return (((((a1*r+a2)*r+a3)*r+a4)*r+a5)*r+a6)*q / (((((b1*r+b2)*r+b3)*r+b4)*r+b5)*r+1.0); } //+------------------------------------------------------------------+ //| Build the a-1 breakpoints that split N(0,1) into 'a' bands of | //| equal probability 1/a. m_beta[i] = probit((i+1)/a). | //+------------------------------------------------------------------+ void CSAXTransform::BuildBreakpoints(void) { ArrayResize(m_beta,m_a-1); for(int i=0;ifalse. | //+------------------------------------------------------------------+ bool CSAXTransform::ZNormalize(const double &src[],int len,double &dst[]) const { if(len<1) return false; double mean=0.0; for(int i=0;i0.0) s+=src[i]*wgt; } paa[j]=s/cell; } return true; } //+------------------------------------------------------------------+ //| Map a z-normalized value to its symbol code in 0..a-1. | //| Band 0 is (-inf, beta0), band a-1 is [beta_{a-2}, +inf). | //+------------------------------------------------------------------+ int CSAXTransform::SymbolOf(double zval) const { int c=0; while(c=m_beta[c]) c++; return c; } //+------------------------------------------------------------------+ //| Full encode: raw window -> integer symbol codes. | //| raw[0..len-1] is oldest-first. The window is z-normalized, PAA | //| reduced, then each cell mapped to a symbol. | //+------------------------------------------------------------------+ ENUM_SAX_STATUS CSAXTransform::Encode(const double &raw[],int len,int &word[]) const { if(!m_ready || lenc2)?c1:c2; int lo=(c1 'a', 1 -> 'b', ... | //+------------------------------------------------------------------+ string CSAXTransform::WordToString(const int &word[]) const { int n=ArraySize(word); string s=""; for(int j=0;j