catch22/Include/Catch22/Catch22Features.mqh

331 lines
12 KiB
MQL5
Raw Permalink Normal View History

2026-07-12 10:15:50 +00:00
//+------------------------------------------------------------------+
//| Catch22Features.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 <Catch22\Catch22.mqh>
//+------------------------------------------------------------------+
//| Shared feature builder for the ablation study. Both the Lab |
//| script (offline training) and the EA (live inference) build |
//| their feature vectors HERE so the columns are guaranteed |
//| identical — the single most important correctness constraint of |
//| a train-offline / trade-live pipeline. |
//| |
//| Three "arms" select which block of columns is produced: |
//| ARM_CLASSIC : classic indicators only (baseline control) |
//| ARM_CATCH22 : the 22 catch22 features |
//| ARM_COMBINED : classic + catch22 |
//+------------------------------------------------------------------+
enum ENUM_C22_ARM
{
ARM_CLASSIC=0, // classic indicators only
ARM_CATCH22, // catch22 only
ARM_COMBINED // both
};
//--- number of classic indicator features produced by this module
#define C22_NCLASSIC 8
//+------------------------------------------------------------------+
//| CCatch22FeatureBuilder |
//| Owns the indicator handles and a CCatch22 engine. Call Init() |
//| once, then Build() per bar index to fill a feature vector for |
//| the chosen arm. Column count is Dim(arm). |
//+------------------------------------------------------------------+
class CCatch22FeatureBuilder
{
private:
string m_symbol;
ENUM_TIMEFRAMES m_tf;
int m_window; // rolling window for catch22 (bars of returns)
ENUM_C22_ARM m_arm;
//--- classic indicator handles
int m_hRSI;
int m_hATR;
int m_hMAfast;
int m_hMAslow;
int m_hStd;
CCatch22 m_c22;
//--- fill the classic block for the window ENDING at bar 'shift'
bool BuildClassic(int shift,double &out[]);
//--- fill the catch22 block from returns of the window ending at 'shift'
bool BuildCatch22(int shift,double &out[]);
public:
CCatch22FeatureBuilder(void);
~CCatch22FeatureBuilder(void);
bool Init(const string symbol,ENUM_TIMEFRAMES tf,
int window,ENUM_C22_ARM arm);
void Deinit(void);
//--- number of feature columns for the current arm
int Dim(void) const;
static int DimOf(ENUM_C22_ARM arm);
//--- build the full feature vector for the bar at 'shift'. Returns
//--- false if the required history is not yet available.
bool Build(int shift,double &out[]);
//--- column name for diagnostics / CSV headers / importance labels
string ColumnName(int col) const;
};
//+------------------------------------------------------------------+
//| Construct with empty handles. |
//+------------------------------------------------------------------+
CCatch22FeatureBuilder::CCatch22FeatureBuilder(void)
{
m_hRSI=INVALID_HANDLE;
m_hATR=INVALID_HANDLE;
m_hMAfast=INVALID_HANDLE;
m_hMAslow=INVALID_HANDLE;
m_hStd=INVALID_HANDLE;
m_window=128;
m_arm=ARM_COMBINED;
}
//+------------------------------------------------------------------+
//| Release handles on destruction. |
//+------------------------------------------------------------------+
CCatch22FeatureBuilder::~CCatch22FeatureBuilder(void)
{
Deinit();
}
//+------------------------------------------------------------------+
//| Number of feature columns for a given arm. |
//+------------------------------------------------------------------+
static int CCatch22FeatureBuilder::DimOf(ENUM_C22_ARM arm)
{
switch(arm)
{
case ARM_CLASSIC:
return C22_NCLASSIC;
case ARM_CATCH22:
return CATCH22_N;
case ARM_COMBINED:
return C22_NCLASSIC+CATCH22_N;
}
return 0;
}
//+------------------------------------------------------------------+
//| Dimension for the configured arm. |
//+------------------------------------------------------------------+
int CCatch22FeatureBuilder::Dim(void) const
{
return DimOf(m_arm);
}
//+------------------------------------------------------------------+
//| Create indicator handles and store configuration. |
//+------------------------------------------------------------------+
bool CCatch22FeatureBuilder::Init(const string symbol,ENUM_TIMEFRAMES tf,
int window,ENUM_C22_ARM arm)
{
m_symbol=symbol;
m_tf=tf;
m_window=window;
m_arm=arm;
//--- classic indicators are only needed for the classic/combined arms
if(arm==ARM_CLASSIC || arm==ARM_COMBINED)
{
m_hRSI =iRSI(symbol,tf,14,PRICE_CLOSE);
m_hATR =iATR(symbol,tf,14);
m_hMAfast=iMA(symbol,tf,20,0,MODE_EMA,PRICE_CLOSE);
m_hMAslow=iMA(symbol,tf,50,0,MODE_EMA,PRICE_CLOSE);
m_hStd =iStdDev(symbol,tf,20,0,MODE_SMA,PRICE_CLOSE);
if(m_hRSI==INVALID_HANDLE || m_hATR==INVALID_HANDLE ||
m_hMAfast==INVALID_HANDLE || m_hMAslow==INVALID_HANDLE ||
m_hStd==INVALID_HANDLE)
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Release indicator handles. |
//+------------------------------------------------------------------+
void CCatch22FeatureBuilder::Deinit(void)
{
if(m_hRSI!=INVALID_HANDLE)
{
IndicatorRelease(m_hRSI);
m_hRSI=INVALID_HANDLE;
}
if(m_hATR!=INVALID_HANDLE)
{
IndicatorRelease(m_hATR);
m_hATR=INVALID_HANDLE;
}
if(m_hMAfast!=INVALID_HANDLE)
{
IndicatorRelease(m_hMAfast);
m_hMAfast=INVALID_HANDLE;
}
if(m_hMAslow!=INVALID_HANDLE)
{
IndicatorRelease(m_hMAslow);
m_hMAslow=INVALID_HANDLE;
}
if(m_hStd!=INVALID_HANDLE)
{
IndicatorRelease(m_hStd);
m_hStd=INVALID_HANDLE;
}
}
//+------------------------------------------------------------------+
//| Classic indicator block, read at bar 'shift' (already-closed |
//| bar). Eight interpretable features a discretionary trader would |
//| have on the chart: RSI, ATR (normalized), MA spread & slopes, |
//| return, rolling volatility, and price-vs-MA stretch. |
//+------------------------------------------------------------------+
bool CCatch22FeatureBuilder::BuildClassic(int shift,double &out[])
{
//--- dynamic, series-ordered buffers so index 0 == the bar at 'shift'
//--- (newest of the copied range) and index 1 == the bar before it.
double rsi[],atr[],maf[],mas[],std[],close[];
ArraySetAsSeries(rsi,true);
ArraySetAsSeries(atr,true);
ArraySetAsSeries(maf,true);
ArraySetAsSeries(mas,true);
ArraySetAsSeries(std,true);
ArraySetAsSeries(close,true);
if(CopyBuffer(m_hRSI,0,shift,1,rsi)<1)
return false;
if(CopyBuffer(m_hATR,0,shift,1,atr)<1)
return false;
if(CopyBuffer(m_hMAfast,0,shift,2,maf)<2)
return false;
if(CopyBuffer(m_hMAslow,0,shift,2,mas)<2)
return false;
if(CopyBuffer(m_hStd,0,shift,1,std)<1)
return false;
if(CopyClose(m_symbol,m_tf,shift,3,close)<3)
return false;
double price=close[0];
double atrn =(price>0.0)?atr[0]/price:0.0; // ATR as fraction of price
double ret =(close[1]>0.0)?MathLog(close[0]/close[1]):0.0;
double maspread=(price>0.0)?(maf[0]-mas[0])/price:0.0;
double mafslope=(price>0.0)?(maf[0]-maf[1])/price:0.0;
double masslope=(price>0.0)?(mas[0]-mas[1])/price:0.0;
double stretch =(atr[0]>0.0)?(price-maf[0])/atr[0]:0.0; // price-vs-MA in ATRs
double volrel =(price>0.0)?std[0]/price:0.0;
ArrayResize(out,C22_NCLASSIC);
out[0]=rsi[0]/100.0; // scale RSI to [0,1]
out[1]=atrn;
out[2]=ret;
out[3]=maspread;
out[4]=mafslope;
out[5]=masslope;
out[6]=stretch;
out[7]=volrel;
return true;
}
//+------------------------------------------------------------------+
//| catch22 block: build a window of log returns ending at bar |
//| 'shift' and run the CCatch22 engine over it. |
//+------------------------------------------------------------------+
bool CCatch22FeatureBuilder::BuildCatch22(int shift,double &out[])
{
int need=m_window+1;
double close[];
if(CopyClose(m_symbol,m_tf,shift,need,close)<need)
return false;
ArraySetAsSeries(close,false); // oldest -> newest
double ret[];
ArrayResize(ret,m_window);
for(int i=0;i<m_window;i++)
{
if(close[i]<=0.0 || close[i+1]<=0.0)
{
ret[i]=0.0;
continue;
}
ret[i]=MathLog(close[i+1]/close[i]);
}
double feats[];
if(!m_c22.Compute(ret,m_window,feats))
{
//--- degenerate window: emit zeros so the row is still usable
ArrayResize(out,CATCH22_N);
ArrayInitialize(out,0.0);
return true;
}
ArrayResize(out,CATCH22_N);
for(int i=0;i<CATCH22_N;i++)
out[i]=feats[i];
return true;
}
//+------------------------------------------------------------------+
//| Build the full feature vector for the configured arm. |
//+------------------------------------------------------------------+
bool CCatch22FeatureBuilder::Build(int shift,double &out[])
{
double cls[],c22[];
if(m_arm==ARM_CLASSIC)
{
if(!BuildClassic(shift,out))
return false;
return true;
}
if(m_arm==ARM_CATCH22)
{
if(!BuildCatch22(shift,out))
return false;
return true;
}
//--- combined: classic block first, then catch22
if(!BuildClassic(shift,cls))
return false;
if(!BuildCatch22(shift,c22))
return false;
ArrayResize(out,C22_NCLASSIC+CATCH22_N);
for(int i=0;i<C22_NCLASSIC;i++)
out[i]=cls[i];
for(int i=0;i<CATCH22_N;i++)
out[C22_NCLASSIC+i]=c22[i];
return true;
}
//+------------------------------------------------------------------+
//| Human-readable column name for a feature index in the current |
//| arm (used for CSV headers and feature-importance reports). |
//+------------------------------------------------------------------+
string CCatch22FeatureBuilder::ColumnName(int col) const
{
string classic[C22_NCLASSIC]=
{
"RSI14","ATR_norm","logret","MA_spread","MAfast_slope",
"MAslow_slope","price_stretch_ATR","vol_rel"
};
if(m_arm==ARM_CLASSIC)
return (col>=0 && col<C22_NCLASSIC)?classic[col]:"?";
if(m_arm==ARM_CATCH22)
return CCatch22::Name(col);
//--- combined
if(col<C22_NCLASSIC)
return classic[col];
return CCatch22::Name(col-C22_NCLASSIC);
}
//+------------------------------------------------------------------+