//+------------------------------------------------------------------+ //| SFA.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 //--- alphabet limits. Words are meant to be short and readable, so //--- the alphabet is kept small: 2..10 letters, the same practical //--- range used across the symbolic-representation literature. #define SFA_MIN_ALPHABET 2 #define SFA_MAX_ALPHABET 10 //--- word-length (number of retained coefficients) limits. Each //--- coefficient contributes one letter, and the DFT of a length-w //--- window can supply at most floor(w/2) non-DC coefficients. #define SFA_MIN_WORDLEN 1 #define SFA_MAX_WORDLEN 16 //--- a window whose standard deviation falls below this is treated //--- as flat: z-normalization would divide by ~0, so no meaningful //--- shape exists and the window is rejected. #define SFA_FLAT_EPS 1.0e-12 //--- status of a single encode attempt. enum ENUM_SFA_STATUS { SFA_OK, // word produced and usable SFA_NOT_FITTED, // MCB breakpoints not learned yet SFA_BAD_PARAMS, // word length / alphabet out of range, or len too small SFA_FLAT_WINDOW // stdev ~ 0: window is flat, no meaningful shape }; //--- shared z-normalization used by both CMCB and CSFA (defined //--- below at file scope). Declared here so CMCB::Fit can call it. bool ZNorm(const double &src[],int len,double &dst[]); //+------------------------------------------------------------------+ //| CMCB - Multiple Coefficient Binning. | //| | //| This is the one piece that separates SFA from the older SAX. | //| SAX assumes every value is drawn from a standard normal and | //| cuts the alphabet at fixed Gaussian quantiles. SFA makes no | //| such assumption: it LEARNS the cut points from data, and it | //| learns a SEPARATE set of cuts for each Fourier coefficient, | //| because a low-frequency coefficient and a high-frequency one | //| have very different spreads. | //| | //| Fit() takes a pile of training windows, transforms each to its | //| first 'l' coefficients (l = word length x nothing extra; each | //| coefficient is one real and one imaginary value, so there are | //| 2l real series to bin). For every one of those 2l positions it | //| collects the value across all windows, sorts them, and places | //| alpha-1 breakpoints at equal-count (equi-depth) quantiles so | //| that each of the 'alpha' letters is used about equally often. | //| | //| The result is a breakpoint table m_break[pos][b], pos in | //| 0..2l-1, b in 0..alpha-2. Symbolizing a value is then a simple | //| lookup: count how many breakpoints it exceeds. | //+------------------------------------------------------------------+ class CMCB { private: int m_dims; // number of real series binned = 2 * word length int m_alphabet; // letters per position bool m_fitted; // Fit() succeeded //--- breakpoints: m_dims rows, (alphabet-1) columns, ascending. //--- flattened row-major: m_break[pos*(alphabet-1) + b]. double m_break[]; double Quantile(double &sorted[],int n,double q) const; public: CMCB(void); //--- learn breakpoints from 'count' training windows. 'windows' //--- is a flat array of count*win_len doubles (window 0 first, //--- oldest sample first within each window). 'word_len' is the //--- number of non-DC coefficients kept; 'alphabet' the letters //--- per position. Windows are z-normalized here before the DFT, //--- exactly as they will be at encode time. Returns false on bad //--- parameters or if no usable (non-flat) window is supplied. bool Fit(const double &windows[],int count,int win_len, int word_len,int alphabet); bool IsFitted(void) const { return m_fitted; } int Dimensions(void) const { return m_dims; } int Alphabet(void) const { return m_alphabet; } //--- symbol code 0..alphabet-1 for 'value' at coefficient slot //--- 'pos' (0..m_dims-1): the count of learned breakpoints it //--- exceeds. Returns 0 if not fitted or pos out of range. int SymbolOf(double value,int pos) const; //--- read a single learned breakpoint (pos,b) for inspection or //--- teaching. Returns 0.0 if out of range. double Breakpoint(int pos,int b) const; }; //+------------------------------------------------------------------+ //| Construct an unfitted binner. | //+------------------------------------------------------------------+ CMCB::CMCB(void) { m_dims=0; m_alphabet=0; m_fitted=false; } //+------------------------------------------------------------------+ //| Linear-interpolated quantile of an ascending sorted array. | //+------------------------------------------------------------------+ double CMCB::Quantile(double &sorted[],int n,double q) const { if(n<=0) return 0.0; if(n==1) return sorted[0]; double pos=q*(n-1); int lo=(int)MathFloor(pos); int hi=lo+1; if(hi>=n) return sorted[n-1]; double frac=pos-lo; return sorted[lo]+frac*(sorted[hi]-sorted[lo]); } //+------------------------------------------------------------------+ //| Learn equi-depth breakpoints for every coefficient slot. | //| | //| For each of the 2*word_len real/imag slots we gather that | //| slot's value from every non-flat training window, sort the | //| collection, and cut it into 'alphabet' equal-count bins. The | //| b-th breakpoint sits at quantile (b+1)/alphabet, so bin 0 holds | //| the lowest values, bin alphabet-1 the highest, each with about | //| the same number of training points. | //+------------------------------------------------------------------+ bool CMCB::Fit(const double &windows[],int count,int win_len, int word_len,int alphabet) { m_fitted=false; if(count<=0 || win_len<2) return false; if(word_lenSFA_MAX_WORDLEN) return false; if(alphabetSFA_MAX_ALPHABET) return false; if(word_len>CDFT::MaxCoefficients(win_len)) return false; m_dims=word_len*DFT_REALS_PER_COEFF; m_alphabet=alphabet; //--- transform every training window and stack the coefficient //--- slots column by column. 'col' holds slot 'pos' across windows. double coeffs[]; // reused per window: 2*word_len reals double norm[]; // reused per window: z-normalized samples double raw[]; // reused per window: raw samples ArrayResize(raw,win_len); double collected[]; // all values of the current slot, all windows ArrayResize(m_break,m_dims*(alphabet-1)); int usable=0; // count of non-flat windows actually gathered //--- first pass builds a compact per-slot matrix. To avoid a second //--- transform pass we transform once and store all slots, then bin //--- slot by slot. double all[]; // usable*m_dims coefficients, row = window ArrayResize(all,count*m_dims); for(int wi=0;wi=m_dims) return 0; int nb=m_alphabet-1; int code=0; for(int b=0;b=m_break[pos*nb+b]) code++; else break; // breakpoints ascend: stop early } return code; } //+------------------------------------------------------------------+ //| Read one learned breakpoint, for inspection/teaching. | //+------------------------------------------------------------------+ double CMCB::Breakpoint(int pos,int b) const { if(!m_fitted || pos<0 || pos>=m_dims || b<0 || b>=m_alphabet-1) return 0.0; return m_break[pos*(m_alphabet-1)+b]; } //+------------------------------------------------------------------+ //| Free helper: z-normalize src[0..len-1] into dst[]. | //| Returns false on a flat window (stdev below SFA_FLAT_EPS), | //| leaving dst[] resized but unfilled. Defined at file scope so | //| both CMCB and CSFA share one z-norm and cannot drift apart. | //+------------------------------------------------------------------+ bool ZNorm(const double &src[],int len,double &dst[]) { if(len<=0) return false; double mean=0.0; for(int i=0;i compare shape, not price level | //| 2. real DFT, keep the first 'word_len' non-DC coefficients | //| -> low-pass filter that drops noise | //| 3. bin each coefficient's real and imaginary part via the | //| fitted MCB table -> one letter per part | //| giving a word of 2*word_len letters. Steps 1-2 live in CDFT and | //| ZNorm; CSFA owns the MCB table and the letter-per-slot mapping. | //| | //| Configure() sets the shape (word length, alphabet) and Fit() | //| learns the breakpoints from training windows. After that the | //| same instance encodes any number of windows cheaply. | //+------------------------------------------------------------------+ class CSFA { private: int m_word_len; // non-DC coefficients kept int m_alphabet; // letters per slot int m_win_len; // window length locked at Fit time bool m_ready; // Configure() succeeded CMCB m_mcb; // learned breakpoints public: CSFA(void); //--- set word length and alphabet. Returns false if out of range. //--- Must be called before Fit(). bool Configure(int word_len,int alphabet); //--- learn MCB breakpoints from 'count' training windows, each of //--- 'win_len' samples, laid out flat (window 0 first, oldest //--- sample first). Returns false if not configured, parameters //--- clash with win_len, or no non-flat window exists. bool Fit(const double &windows[],int count,int win_len); bool IsReady(void) const { return m_ready && m_mcb.IsFitted(); } int WordLength(void) const { return m_word_len; } int Alphabet(void) const { return m_alphabet; } int WindowLength(void)const { return m_win_len; } //--- number of letters in an emitted word (= 2 * word length). int SymbolCount(void)const { return m_word_len*DFT_REALS_PER_COEFF; } //--- encode one raw window (win_len samples, oldest first) into //--- integer letters word[0..2*word_len-1]. Returns SFA_OK, or a //--- status describing why no word was produced. ENUM_SFA_STATUS Encode(const double &raw[],int len,int &word[]) const; //--- render a letter word as a lowercase string ("cbadba") for //--- logging and on-chart display. string WordToString(const int &word[]) const; //--- expose the binner for breakpoint inspection / teaching. const CMCB *Binner(void) const { return GetPointer(m_mcb); } }; //+------------------------------------------------------------------+ //| Construct an unconfigured transform. | //+------------------------------------------------------------------+ CSFA::CSFA(void) { m_word_len=0; m_alphabet=0; m_win_len=0; m_ready=false; } //+------------------------------------------------------------------+ //| Configure word length and alphabet. | //+------------------------------------------------------------------+ bool CSFA::Configure(int word_len,int alphabet) { m_ready=false; if(word_lenSFA_MAX_WORDLEN) return false; if(alphabetSFA_MAX_ALPHABET) return false; m_word_len=word_len; m_alphabet=alphabet; m_ready=true; return true; } //+------------------------------------------------------------------+ //| Learn breakpoints, delegating to the MCB binner. | //+------------------------------------------------------------------+ bool CSFA::Fit(const double &windows[],int count,int win_len) { if(!m_ready) return false; if(win_len<2 || m_word_len>CDFT::MaxCoefficients(win_len)) return false; if(!m_mcb.Fit(windows,count,win_len,m_word_len,m_alphabet)) return false; m_win_len=win_len; return true; } //+------------------------------------------------------------------+ //| Encode one raw window into a word of integer letters. | //+------------------------------------------------------------------+ ENUM_SFA_STATUS CSFA::Encode(const double &raw[],int len,int &word[]) const { if(!m_ready) return SFA_BAD_PARAMS; if(!m_mcb.IsFitted()) return SFA_NOT_FITTED; if(len<2 || m_word_len>CDFT::MaxCoefficients(len)) return SFA_BAD_PARAMS; double norm[]; if(!ZNorm(raw,len,norm)) return SFA_FLAT_WINDOW; double coeffs[]; if(!CDFT::LowCoefficients(norm,len,m_word_len,coeffs)) return SFA_BAD_PARAMS; int nsym=SymbolCount(); ArrayResize(word,nsym); for(int p=0;p26, not reachable here) would wrap;| //| the alphabet cap of 10 keeps every letter in a..j. | //+------------------------------------------------------------------+ string CSFA::WordToString(const int &word[]) const { string s=""; int n=ArraySize(word); for(int i=0;i