//+------------------------------------------------------------------+ //| SFAAnalog.mq5 | //| MMQ — Muhammad Minhas Qamar | //| www.mql5.com/en/articles/24006 | //+------------------------------------------------------------------+ #property copyright "MMQ — Muhammad Minhas Qamar" #property link "https://www.mql5.com/en/articles/24006" #property version "1.00" #property strict #property indicator_chart_window #property indicator_plots 0 #include //+------------------------------------------------------------------+ //| Turns the SFA analog search into something you can read on a | //| chart: a fan cone over the next H bars and a corner panel. | //| | //| The cone is NOT a forecast line. Each step plots the median and | //| the p25/p75 band of what actually happened after the closest | //| historical matches, scaled by the current ATR. A wide band | //| means the precedents disagreed, and that is the useful signal — | //| it is drawn only when the sample clears both conviction gates, | //| so an empty chart is a verdict, not a failure. | //| | //| The panel carries two rows the SAX version cannot: the size of | //| the MCB fitting sample, and the share of candidates the lower | //| bound rejected on this bar. The first tells you whether the bin | //| edges rest on enough history to be meaningful; the second tells | //| you what the symbolic layer is actually buying you. | //+------------------------------------------------------------------+ //--- shape / search input int InpWinLen = 48; // window length (bars) input int InpHorizon = 12; // forecast horizon (bars) input int InpWordLen = 8; // SFA word length (even) input int InpAlphabet = 8; // alphabet size input ENUM_SFA_BINNING InpBinning = SFA_BINS_EQUI_DEPTH; // bin edge rule input int InpMaxAnalogs= 50; // max analogs kept input int InpMinAnalogs= 10; // min analogs for a verdict input double InpEdgeATR = 0.25; // |median| ATR for an edge input ENUM_SFA_METRIC InpMetric = SFA_METRIC_MINDIST; // search metric input int InpATRPeriod = 14; // ATR period (scale) input int InpMaxHistory= 6000; // bars of history to scan //--- panel theme (background + body-text colors) enum ENUM_PANEL_THEME { THEME_DARK, // dark box, light text (for dark charts) THEME_LIGHT // light box, dark text (for light charts) }; input color InpUpColor = clrDodgerBlue; // bullish cone / panel input color InpDownColor = clrTomato; // bearish cone / panel input color InpFlatColor = clrGray; // no-edge / too-few input color InpBandColor = clrSlateGray; // p25..p75 band lines input ENUM_BASE_CORNER InpCorner = CORNER_LEFT_UPPER; // panel corner input ENUM_PANEL_THEME InpTheme = THEME_LIGHT; // panel theme input int InpPanelX = 14; // panel offset X (px) input int InpPanelY = 18; // panel offset Y (px) //--- panel metrics in pixels. Everything the panel draws is placed from //--- these, so changing the pitch or padding reflows the whole layout. #define P_PAD 14 // inner margin #define P_HDR 28 // header strip height #define P_ROW 20 // body row pitch #define P_HERO 34 // hero (median move) block height #define P_BAR 6 // up-rate bar thickness #define P_CHIP 28 // verdict chip height #define P_GAP 28 // minimum gutter between a label and its value #define P_SEC 16 // breathing room around a section divider #define P_MINW 212 // panel never narrower than this //--- fonts: proportional for prose, monospace for the symbolic word #define F_UI "Segoe UI" #define F_MONO "Consolas" //--- globals int g_hATR=INVALID_HANDLE; string g_pfx="SFAAN_"; // object-name prefix (all ours, easy cleanup) datetime g_lastBar=0; CSFAAnalogFinder g_finder; //--- geometry of the panel being drawn, shared with the two placement //--- helpers so callers can work in panel-relative pixels and stay //--- unaware of which chart corner the panel is pinned to int g_px=0,g_py=0,g_pw=0,g_ph=0; bool g_pLeft=true,g_pUpper=true; //+------------------------------------------------------------------+ //| Indicator init: configure the finder and create the ATR handle. | //+------------------------------------------------------------------+ int OnInit() { //--- clear anything a previous load left behind, so a parameter change //--- that reflows the panel cannot strand objects from the old layout ObjectsDeleteAll(0,g_pfx); if(!g_finder.Configure(InpWinLen,InpHorizon,InpWordLen,InpAlphabet, InpBinning,InpMetric)) { Print("SFAAnalog: bad configuration. Word length must be even and " "below the window length; alphabet must be 2..10."); return(INIT_PARAMETERS_INCORRECT); } g_finder.SetMaxAnalogs(InpMaxAnalogs); g_finder.SetMinAnalogs(InpMinAnalogs); g_finder.SetEdgeThreshold(InpEdgeATR); g_hATR=iATR(_Symbol,_Period,InpATRPeriod); if(g_hATR==INVALID_HANDLE) { Print("SFAAnalog: failed to create ATR handle"); return(INIT_FAILED); } IndicatorSetString(INDICATOR_SHORTNAME,"SFA Analog Forecaster"); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Indicator shutdown: drop our objects and release the ATR handle. | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { ObjectsDeleteAll(0,g_pfx); if(g_hATR!=INVALID_HANDLE) IndicatorRelease(g_hATR); ChartRedraw(); } //+------------------------------------------------------------------+ //| Recompute only when a new bar has formed. | //+------------------------------------------------------------------+ bool NewBar() { datetime t=iTime(_Symbol,_Period,0); if(t==g_lastBar) return false; g_lastBar=t; return true; } //+------------------------------------------------------------------+ //| Load closes + aligned ATR into chronological buffers (oldest at | //| index 0). 'shift 1' so only CLOSED bars are used. Returns the | //| number of usable bars, or 0 on failure. | //+------------------------------------------------------------------+ int LoadHistory(double &close[],double &atr[]) { int got=CopyClose(_Symbol,_Period,1,InpMaxHistory,close); if(got0) ?100.0*(1.0-(double)g_finder.TrueCalcs()/(double)scanned) :0.0; //--- header, hero and chip strings string hTitle="SFA ANALOG"; string hSym =_Symbol+" · "+StringSubstr(EnumToString((ENUM_TIMEFRAMES)_Period),7); string heroL =StringFormat("MEDIAN %d BARS",InpHorizon); string heroV =(s.count>0)?StringFormat("%+.2f ATR",s.median_atr):"—"; //--- verdict chip. The arrow repeats the direction in a second channel //--- so the panel still reads correctly on a monochrome screenshot. string mark="•"; if(s.verdict==SFA_ANALOG_BULLISH) mark="▲"; if(s.verdict==SFA_ANALOG_BEARISH) mark="▼"; string chip=verdict; StringToUpper(chip); //--- body rows, as label/value pairs string lab[6],val[6]; string vfont[6]; lab[0]="BAND"; val[0]=(s.count>0)?StringFormat("%+.2f … %+.2f",s.p25_atr,s.p75_atr):"—"; lab[1]="UP RATE"; val[1]=(s.count>0)?StringFormat("%.0f%%",100.0*s.up_rate):"—"; lab[2]="WORD"; val[2]=word; lab[3]="ANALOGS"; val[3]=StringFormat("%d · avg %.2f",s.count,s.avg_distance); lab[4]="MCB BINS"; val[4]=StringFormat("%d win · l=%d a=%d",g_finder.FitWindows(), InpWordLen,InpAlphabet); lab[5]="PRUNED"; val[5]=StringFormat("%.0f%%",pruneRate); for(int i=0;i<6;i++) vfont[i]=(i==2)?F_MONO:F_UI; //--- width: the widest label+gutter+value pair, header and hero included int need=P_MINW; for(int i=0;i<6;i++) { int w=TextW(lab[i],F_UI,fLab)+P_GAP+TextW(val[i],vfont[i],fVal); if(w>need) need=w; } int wh=TextW(hTitle,F_UI,fHdr)+P_GAP+TextW(hSym,F_UI,fLab); if(wh>need) need=wh; int wv=TextW(heroL,F_UI,fLab)+P_GAP+TextW(heroV,F_UI,fHero); if(wv>need) need=wv; int wc=30+TextW(chip,F_UI,fChip); if(wc>need) need=wc; //--- publish the geometry the placement helpers work against g_px=InpPanelX; g_py=InpPanelY; g_pw=need+2*P_PAD; g_pLeft =(InpCorner==CORNER_LEFT_UPPER || InpCorner==CORNER_LEFT_LOWER); g_pUpper=(InpCorner==CORNER_LEFT_UPPER || InpCorner==CORNER_RIGHT_UPPER); //--- walk the layout with a running cursor so every gap is stated once int rail=g_pw-P_PAD; // x of the right-hand value rail int y=P_HDR+14; // cursor, starts under the header strip int yHero=y; y+=P_HERO+P_SEC; int yDiv1=y; y+=1+P_SEC; int yRow0=y; y+=P_ROW; // BAND int yRow1=y; y+=P_ROW+10; // UP RATE, then clear air for its bar int yBar=y; y+=P_BAR+P_SEC; int yDiv2=y; y+=1+P_SEC; int yRow2=y; y+=P_ROW*4+P_SEC; // WORD / ANALOGS / MCB BINS / PRUNED int yChip=y; g_ph=yChip+P_CHIP+P_PAD; //--- shell and header strip PanelRect("bg",0,0,g_pw,g_ph,bgCol,edgCol); PanelRect("hdr",0,0,g_pw,P_HDR,clr,clr); PanelText("hT",P_PAD,P_HDR/2,hTitle,clrWhite,F_UI,fHdr,false); PanelText("hS",rail,P_HDR/2,hSym,clrWhite,F_UI,fLab,true); //--- hero row: the one number worth reading from across the room. //--- Label and value share one centre line, so the small caps sit //--- optically level with the middle of the large figure. PanelText("heroL",P_PAD,yHero+P_HERO/2,heroL,labCol,F_UI,fLab,false); PanelText("heroV",rail,yHero+P_HERO/2,heroV, (s.count>0)?clr:labCol,F_UI,fHero,true); PanelRect("d1",P_PAD,yDiv1,g_pw-2*P_PAD,1,divCol,divCol); //--- forward-distribution rows PanelText("l0",P_PAD,yRow0+P_ROW/2,lab[0],labCol,F_UI,fLab,false); PanelText("v0",rail,yRow0+P_ROW/2,val[0],valCol,F_UI,fVal,true); PanelText("l1",P_PAD,yRow1+P_ROW/2,lab[1],labCol,F_UI,fLab,false); PanelText("v1",rail,yRow1+P_ROW/2,val[1],valCol,F_UI,fVal,true); //--- up-rate bar: a proportion reads faster as a length than as digits int barW=g_pw-2*P_PAD; int fill=(s.count>0)?(int)MathRound(barW*MathMax(0.0,MathMin(1.0,s.up_rate))) :0; PanelRect("barBg",P_PAD,yBar,barW,P_BAR,trkCol,trkCol); if(fill>0) PanelRect("barFg",P_PAD,yBar,fill,P_BAR,clr,clr); else ObjectDelete(0,g_pfx+"barFg"); PanelRect("d2",P_PAD,yDiv2,g_pw-2*P_PAD,1,divCol,divCol); //--- diagnostics: only interesting when something looks wrong for(int i=2;i<6;i++) { int ry=yRow2+P_ROW*(i-2)+P_ROW/2; PanelText("l"+(string)i,P_PAD,ry,lab[i],labCol,F_UI,fLab,false); PanelText("v"+(string)i,rail,ry,val[i],valCol,vfont[i],fVal,true); } PanelRect("chip",P_PAD,yChip,g_pw-2*P_PAD,P_CHIP,clr,clr); PanelText("chipM",P_PAD+12,yChip+P_CHIP/2,mark,clrWhite,F_UI,fChip,false); PanelText("chipT",P_PAD+31,yChip+P_CHIP/2,chip,clrWhite,F_UI,fChip,false); } //+------------------------------------------------------------------+ //| Map a verdict to its display color. | //+------------------------------------------------------------------+ color VerdictColor(ENUM_SFA_VERDICT v) { switch(v) { case SFA_ANALOG_BULLISH: return InpUpColor; case SFA_ANALOG_BEARISH: return InpDownColor; default: return InpFlatColor; // no-edge / too-few / no-data } } //+------------------------------------------------------------------+ //| Main: run the search on each new bar and redraw cone + panel. | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { if(!NewBar() && prev_calculated>0) return(rates_total); double c[],atr[]; int n=LoadHistory(c,atr); if(n<=0) return(rates_total); //--- query ends at the newest loaded (closed) bar bool ok=g_finder.Find(c,atr,n,n-1); SSfaForwardStats s=g_finder.Stats(); color clr=VerdictColor(s.verdict); //--- last CLOSED bar anchors the cone (index 1 on the live chart) datetime lastTime =iTime(_Symbol,_Period,1); double lastClose=iClose(_Symbol,_Period,1); double curATR =(atr[n-1]>0.0)?atr[n-1]:0.0; ClearCone(); //--- draw the cone only when there is a high-conviction edge if(ok && curATR>0.0 && (s.verdict==SFA_ANALOG_BULLISH || s.verdict==SFA_ANALOG_BEARISH)) DrawCone(lastTime,lastClose,curATR,clr); DrawPanel(s,ok?g_finder.QueryWord():"-",g_finder.VerdictText(),clr); ChartRedraw(); return(rates_total); } //+------------------------------------------------------------------+