SFA/Indicators/SFA/SFAAnalog.mq5
2026-08-11 12:37:59 +00:00

543 lines
23 KiB
MQL5

//+------------------------------------------------------------------+
//| 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 <SFA\SFAAnalogs.mqh>
//+------------------------------------------------------------------+
//| 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(got<InpWinLen+InpHorizon+InpATRPeriod+5)
return 0;
ArraySetAsSeries(close,false);
double atrBuf[];
if(CopyBuffer(g_hATR,0,1,got,atrBuf)<got)
return 0;
ArraySetAsSeries(atrBuf,false);
ArrayResize(atr,got);
for(int i=0;i<got;i++)
atr[i]=atrBuf[i];
return got;
}
//+------------------------------------------------------------------+
//| Create-or-update a trend segment object between two chart points.|
//+------------------------------------------------------------------+
void SetTrend(string name,datetime t1,double p1,datetime t2,double p2,
color clr,int width,int style)
{
if(ObjectFind(0,name)<0)
ObjectCreate(0,name,OBJ_TREND,0,t1,p1,t2,p2);
ObjectSetInteger(0,name,OBJPROP_TIME,0,t1);
ObjectSetDouble(0,name,OBJPROP_PRICE,0,p1);
ObjectSetInteger(0,name,OBJPROP_TIME,1,t2);
ObjectSetDouble(0,name,OBJPROP_PRICE,1,p2);
ObjectSetInteger(0,name,OBJPROP_COLOR,clr);
ObjectSetInteger(0,name,OBJPROP_WIDTH,width);
ObjectSetInteger(0,name,OBJPROP_STYLE,style);
ObjectSetInteger(0,name,OBJPROP_RAY_RIGHT,false);
ObjectSetInteger(0,name,OBJPROP_BACK,false);
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
}
//+------------------------------------------------------------------+
//| Width of a string in pixels, in the font it will actually be |
//| drawn with. Sizing the panel from measured text — rather than |
//| from a character count — is what keeps it from clipping when the |
//| word length, the symbol name or the user's DPI changes. |
//+------------------------------------------------------------------+
int TextW(string s,string font,int fs)
{
TextSetFont(font,-fs*10); // negative size = points*10, dpi-aware
uint w=0,h=0;
TextGetSize(s,w,h);
return (int)w;
}
//+------------------------------------------------------------------+
//| Place a filled rectangle in panel-relative pixels. |
//| (px,py) is the offset from the panel's visual top-left whichever|
//| corner it is pinned to; the mirroring for right and lower |
//| corners happens here and nowhere else. |
//| |
//| 'edge' draws the one-pixel outline. Passing the fill colour |
//| makes the shape borderless, which is what every element except |
//| the outer card wants. |
//+------------------------------------------------------------------+
void PanelRect(string tag,int px,int py,int w,int h,color bg,color edge)
{
string name=g_pfx+tag;
if(ObjectFind(0,name)<0)
ObjectCreate(0,name,OBJ_RECTANGLE_LABEL,0,0,0);
int x=g_pLeft ?(g_px+px):(g_px+g_pw-px-w);
int y=g_pUpper?(g_py+py):(g_py+g_ph-py-h);
ObjectSetInteger(0,name,OBJPROP_CORNER,InpCorner);
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
ObjectSetInteger(0,name,OBJPROP_XSIZE,w);
ObjectSetInteger(0,name,OBJPROP_YSIZE,h);
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,bg);
ObjectSetInteger(0,name,OBJPROP_BORDER_TYPE,BORDER_FLAT);
ObjectSetInteger(0,name,OBJPROP_COLOR,edge);
ObjectSetInteger(0,name,OBJPROP_BACK,false);
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
}
//+------------------------------------------------------------------+
//| Place a text run in panel-relative pixels. |
//| |
//| py is the vertical CENTRE of the text, not its top, and |
//| right=true anchors the string by its right edge. Between them |
//| the two rails do all the alignment work: values stay flush |
//| without padding, and a string sits truly centred in its block |
//| whatever the font size or the reader's DPI turns out to be. |
//| Offsets tuned by eye against one font drift on both counts. |
//+------------------------------------------------------------------+
void PanelText(string tag,int px,int py,string text,color clr,
string font,int fs,bool right)
{
string name=g_pfx+tag;
if(ObjectFind(0,name)<0)
ObjectCreate(0,name,OBJ_LABEL,0,0,0);
int x=g_pLeft ?(g_px+px):(g_px+g_pw-px);
int y=g_pUpper?(g_py+py):(g_py+g_ph-py);
ENUM_ANCHOR_POINT a=right?ANCHOR_RIGHT:ANCHOR_LEFT;
ObjectSetInteger(0,name,OBJPROP_CORNER,InpCorner);
ObjectSetInteger(0,name,OBJPROP_ANCHOR,a);
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
ObjectSetInteger(0,name,OBJPROP_COLOR,clr);
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,fs);
ObjectSetString(0,name,OBJPROP_FONT,font);
ObjectSetString(0,name,OBJPROP_TEXT,text);
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
}
//+------------------------------------------------------------------+
//| Erase the fan-cone objects (they are rebuilt each new bar). |
//+------------------------------------------------------------------+
void ClearCone()
{
for(int k=0;k<InpHorizon+2;k++)
{
ObjectDelete(0,g_pfx+"med_"+(string)k);
ObjectDelete(0,g_pfx+"p25_"+(string)k);
ObjectDelete(0,g_pfx+"p75_"+(string)k);
}
}
//+------------------------------------------------------------------+
//| Draw the fan cone. |
//| The per-step distribution is in ATR units; multiply by the |
//| CURRENT ATR to turn it into a price offset from the last close, |
//| so the cone sits on the real price axis. Future bar times don't |
//| exist yet, so their timestamps are extrapolated from the last |
//| bar time + k * PeriodSeconds(). |
//+------------------------------------------------------------------+
void DrawCone(datetime lastTime,double lastClose,double curATR,color clr)
{
int ps=PeriodSeconds(_Period);
double medPrev=lastClose,p25Prev=lastClose,p75Prev=lastClose;
datetime tPrev=lastTime;
for(int k=1;k<=InpHorizon;k++)
{
SSfaStepStat st;
if(!g_finder.StepStats(k,st))
break;
datetime tk=lastTime+(datetime)(k*ps);
double med=lastClose+st.median_atr*curATR;
double p25=lastClose+st.p25_atr *curATR;
double p75=lastClose+st.p75_atr *curATR;
SetTrend(g_pfx+"med_"+(string)k,tPrev,medPrev,tk,med,clr,2,STYLE_SOLID);
SetTrend(g_pfx+"p75_"+(string)k,tPrev,p75Prev,tk,p75,InpBandColor,1,STYLE_DOT);
SetTrend(g_pfx+"p25_"+(string)k,tPrev,p25Prev,tk,p25,InpBandColor,1,STYLE_DOT);
tPrev=tk;
medPrev=med;
p25Prev=p25;
p75Prev=p75;
}
}
//+------------------------------------------------------------------+
//| Draw the verdict panel. |
//| |
//| The layout is a single column with two alignment rails: dim |
//| labels flush left, bright values flush right. Because the |
//| values are anchored by their right edge rather than padded into |
//| place, the columns stay true for any content without needing a |
//| monospace font — only the symbolic word is monospaced, because |
//| there it is the letters themselves that carry meaning. |
//| |
//| Weight follows importance. The median forward move gets its own |
//| oversized row in the accent colour, since it is the one number |
//| a reader wants at a glance; the up-rate gets a bar, because a |
//| proportion is read faster as a length than as digits; and the |
//| diagnostics that matter only when something looks wrong sit |
//| below a divider in the smallest type. |
//| |
//| Everything is sized from measured text and laid out against a |
//| running cursor, so changing a metric or a string reflows the |
//| panel instead of overlapping it. |
//+------------------------------------------------------------------+
void DrawPanel(SSfaForwardStats &s,string word,string verdict,color clr)
{
const int fLab=8,fVal=9,fHdr=9,fHero=15,fChip=9;
//--- palette. The card must sit clearly ON the chart rather than in
//--- it, so the fill is pushed away from typical chart backgrounds
//--- and carried by a visible edge.
color bgCol =(InpTheme==THEME_LIGHT)?C'255,255,255':C'26,28,36';
color edgCol=(InpTheme==THEME_LIGHT)?C'208,213,222':C'56,61,76';
color labCol=(InpTheme==THEME_LIGHT)?C'122,130,146':C'130,138,157';
color valCol=(InpTheme==THEME_LIGHT)?C'22,26,35' :C'238,240,246';
color divCol=(InpTheme==THEME_LIGHT)?C'230,233,240':C'48,53,66';
color trkCol=(InpTheme==THEME_LIGHT)?C'232,235,242':C'54,59,73';
//--- prune share on this bar: what the lower bound actually saved
int scanned=g_finder.Scanned();
double pruneRate=(scanned>0)
?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);
}
//+------------------------------------------------------------------+