Article-24649-GrangerMTF-Ca.../GrangerMTF.mq5

845 lines
28 KiB
MQL5

2026-09-22 21:31:29 +02:00
//+------------------------------------------------------------------+
//| GrangerMTF.mq5 |
//| Copyright 2025, MetaQuotes Ltd. |
//| https://www.mql5.com/en/users/johnhlomohang/ |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link "https://www.mql5.com/en/users/johnhlomohang/"
#property version "1.00"
#include <Trade\Trade.mqh>
#include <Math\Stat\F.mqh>
#define GC_SCALE 10000.0 // returns in basis points (must match SCALE in the notebook)
#define GC_PREFIX "GCM_"
enum ENUM_STATUS_SOURCE
{
STATUS_OFFLINE = 0, // Offline (Jupyter table only)
STATUS_LIVE = 1, // Live (rolling test in MQL5 only)
STATUS_BOTH = 2 // Both must agree
};
//--- inputs
input group "=== Causality Source ==="
input string InpCausalityFile = "granger_mtf_causality.csv"; // Calibration file from Jupyter
input bool InpUseCommonFolder = true; // Read from Common\Files
input ENUM_STATUS_SOURCE InpStatusSource = STATUS_BOTH; // How VALID is decided
input int InpRetestBars = 12; // Re-run live F-test every N lag-TF bars
input string InpFallbackPairs = "M30>M15,H1>M15,M15>M5,H1>M5"; // Pairs if file is missing
input int InpFallbackP = 1; // Fallback lead lags
input int InpFallbackQ = 2; // Fallback own lags
input int InpFallbackWindow = 1500; // Fallback window (bars)
input double InpFallbackAlpha = 0.05; // Fallback alpha
input group "=== Signal Generator ==="
input ENUM_TIMEFRAMES InpSignalTF = PERIOD_M5; // Signal evaluation timeframe
input bool InpUseCausalityFilter = true; // Only VALID pairs may vote
input double InpMinEdgeSigma = 0.05; // Min |forecast| as a fraction of return std
input int InpMinVotes = 1; // Min agreeing votes (with zero opposition)
input bool InpShowSignals = true; // Display signals (panel + chart arrows)
input bool InpAlertOnSignal = false; // Pop-up alert on a new signal
input group "=== Execution ==="
input bool InpExecuteTrades = true; // Trade the internal signals
input double InpLots = 0.01; // Lot size
input int InpATRPeriod = 14; // ATR period (signal TF)
input double InpSL_ATR = 1.5; // Stop loss (x ATR)
input double InpTP_ATR = 2.0; // Take profit (x ATR)
input bool InpCloseOnOpposite = true; // Close on an opposite signal
input ulong InpMagic = 110011; // Magic number
input group "=== Panel ==="
input bool InpShowPanel = true; // Show the causality panel
input int InpPanelX = 15; // Panel X offset
input int InpPanelY = 25; // Panel Y offset
input int InpFontSize = 9; // Panel font size
//--- one lead -> lag relationship
struct SCausalPair
{
ENUM_TIMEFRAMES lead;
ENUM_TIMEFRAMES lag;
string leadName;
string lagName;
int p; // lags of the lead TF (completed lead bars)
int q; // own lags of the lag TF
int window; // rolling window (lag-TF bars)
double alpha;
//--- offline (Jupyter) results
bool hasOffline;
bool offlineValid;
double offlineF;
double offlineP;
double stability;
//--- live results
bool liveReady;
double liveF;
double liveP;
double beta[]; // unrestricted model: [const, y lags, x lags]
double sigmaY;
datetime lastLagBar;
int barsSinceTest;
//--- signal state
double pred; // forecast of the current lag-TF bar return (bps)
int vote; // +1 / -1 / 0
bool valid;
};
//--- globals
SCausalPair g_pairs[];
int g_pairCount = 0;
bool g_fileLoaded = false;
datetime g_lastSigBar = 0;
int g_lastSignal = 0;
int g_bulls = 0;
int g_bears = 0;
int g_atrHandle = INVALID_HANDLE;
CTrade g_trade;
//+------------------------------------------------------------------+
//| Timeframe helpers |
//+------------------------------------------------------------------+
ENUM_TIMEFRAMES TfFromString(const string text)
{
string s = text;
StringTrimLeft(s);
StringTrimRight(s);
StringToUpper(s);
if(s == "M1")
return PERIOD_M1;
if(s == "M5")
return PERIOD_M5;
if(s == "M15")
return PERIOD_M15;
if(s == "M30")
return PERIOD_M30;
if(s == "H1")
return PERIOD_H1;
if(s == "H4")
return PERIOD_H4;
if(s == "D1")
return PERIOD_D1;
return PERIOD_CURRENT;
}
string TfToString(const ENUM_TIMEFRAMES tf)
{
string s = EnumToString(tf);
StringReplace(s, "PERIOD_", "");
return s;
}
//+------------------------------------------------------------------+
//| Register one pair |
//+------------------------------------------------------------------+
bool AddPair(const ENUM_TIMEFRAMES lead, const ENUM_TIMEFRAMES lag,
const int p, const int q, const int window, const double alpha)
{
if(lead == PERIOD_CURRENT || lag == PERIOD_CURRENT)
return false;
if(PeriodSeconds(lead) <= PeriodSeconds(lag))
{
PrintFormat("Pair %s>%s rejected: lead must be the higher timeframe",
TfToString(lead), TfToString(lag));
return false;
}
if(p < 1 || q < 1 || window < 5 * (1 + p + q))
{
PrintFormat("Pair %s>%s rejected: invalid lags/window", TfToString(lead), TfToString(lag));
return false;
}
int i = g_pairCount;
ArrayResize(g_pairs, i + 1);
g_pairs[i].lead = lead;
g_pairs[i].lag = lag;
g_pairs[i].leadName = TfToString(lead);
g_pairs[i].lagName = TfToString(lag);
g_pairs[i].p = p;
g_pairs[i].q = q;
g_pairs[i].window = window;
g_pairs[i].alpha = alpha;
g_pairs[i].hasOffline = false;
g_pairs[i].offlineValid = false;
g_pairs[i].offlineF = 0.0;
g_pairs[i].offlineP = 1.0;
g_pairs[i].stability = 0.0;
g_pairs[i].liveReady = false;
g_pairs[i].liveF = 0.0;
g_pairs[i].liveP = 1.0;
g_pairs[i].sigmaY = 0.0;
g_pairs[i].lastLagBar = 0;
g_pairs[i].barsSinceTest = 0;
g_pairs[i].pred = 0.0;
g_pairs[i].vote = 0;
g_pairs[i].valid = false;
ArrayResize(g_pairs[i].beta, 1 + q + p);
ArrayInitialize(g_pairs[i].beta, 0.0);
g_pairCount++;
return true;
}
//+------------------------------------------------------------------+
//| Load the Jupyter calibration table |
//+------------------------------------------------------------------+
bool LoadCausalityFile()
{
int flags = FILE_READ | FILE_TXT | FILE_ANSI | FILE_SHARE_READ;
if(InpUseCommonFolder)
flags |= FILE_COMMON;
int h = FileOpen(InpCausalityFile, flags);
if(h == INVALID_HANDLE)
{
PrintFormat("Causality file '%s' not found (error %d). Using fallback pairs.",
InpCausalityFile, GetLastError());
return false;
}
int line = 0;
while(!FileIsEnding(h))
{
string row = FileReadString(h);
line++;
if(line == 1)
continue; // header
StringTrimLeft(row);
StringTrimRight(row);
if(StringLen(row) == 0)
continue;
string f[];
if(StringSplit(row, ',', f) < 13)
continue;
ENUM_TIMEFRAMES lead = TfFromString(f[0]);
ENUM_TIMEFRAMES lag = TfFromString(f[1]);
if(!AddPair(lead, lag, (int)StringToInteger(f[2]), (int)StringToInteger(f[3]),
(int)StringToInteger(f[4]), StringToDouble(f[5])))
continue;
int i = g_pairCount - 1;
string status = f[12];
StringTrimLeft(status);
StringTrimRight(status);
g_pairs[i].hasOffline = true;
g_pairs[i].offlineF = StringToDouble(f[6]);
g_pairs[i].offlineP = StringToDouble(f[8]);
g_pairs[i].stability = StringToDouble(f[9]);
g_pairs[i].offlineValid = (status == "VALID");
}
FileClose(h);
return (g_pairCount > 0);
}
//+------------------------------------------------------------------+
//| Fallback pairs from inputs, "H1>M15,M30>M15" |
//+------------------------------------------------------------------+
void LoadFallbackPairs()
{
string items[];
int n = StringSplit(InpFallbackPairs, ',', items);
for(int i = 0; i < n; i++)
{
string tf[];
if(StringSplit(items[i], '>', tf) != 2)
continue;
AddPair(TfFromString(tf[0]), TfFromString(tf[1]),
InpFallbackP, InpFallbackQ, InpFallbackWindow, InpFallbackAlpha);
}
}
//+------------------------------------------------------------------+
//| Log return of bar i (series order) in basis points |
//+------------------------------------------------------------------+
double LogRet(const MqlRates &r[], const int i)
{
if(r[i].close <= 0.0 || r[i + 1].close <= 0.0)
return 0.0;
return GC_SCALE * MathLog(r[i].close / r[i + 1].close);
}
//+------------------------------------------------------------------+
//| Gaussian elimination with partial pivoting |
//+------------------------------------------------------------------+
bool SolveLinear(const double &A[], const double &b[], const int k, double &x[])
{
double M[], v[];
ArrayCopy(M, A);
ArrayCopy(v, b);
for(int c = 0; c < k; c++)
{
int piv = c;
double best = MathAbs(M[c * k + c]);
for(int r = c + 1; r < k; r++)
{
double a = MathAbs(M[r * k + c]);
if(a > best)
{
best = a;
piv = r;
}
}
if(best < 1e-12)
return false;
if(piv != c)
{
for(int j = 0; j < k; j++)
{
double t = M[c * k + j];
M[c * k + j] = M[piv * k + j];
M[piv * k + j] = t;
}
double tb = v[c];
v[c] = v[piv];
v[piv] = tb;
}
for(int r = c + 1; r < k; r++)
{
double factor = M[r * k + c] / M[c * k + c];
if(factor == 0.0)
continue;
for(int j = c; j < k; j++)
M[r * k + j] -= factor * M[c * k + j];
v[r] -= factor * v[c];
}
}
ArrayResize(x, k);
for(int r = k - 1; r >= 0; r--)
{
double s = v[r];
for(int j = r + 1; j < k; j++)
s -= M[r * k + j] * x[j];
x[r] = s / M[r * k + r];
}
return true;
}
//+------------------------------------------------------------------+
//| OLS on the first kUse columns |
//+------------------------------------------------------------------+
bool FitOLS(const double &X[], const double &Y[], const int n, const int kTot,
const int kUse, double &beta[], double &rss)
{
double XtX[], Xty[];
ArrayResize(XtX, kUse * kUse);
ArrayResize(Xty, kUse);
ArrayInitialize(XtX, 0.0);
ArrayInitialize(Xty, 0.0);
for(int i = 0; i < n; i++)
{
int o = i * kTot;
for(int a = 0; a < kUse; a++)
{
double xa = X[o + a];
Xty[a] += xa * Y[i];
for(int c = a; c < kUse; c++)
XtX[a * kUse + c] += xa * X[o + c];
}
}
for(int a = 0; a < kUse; a++)
{
for(int c = 0; c < a; c++)
XtX[a * kUse + c] = XtX[c * kUse + a];
XtX[a * kUse + a] += 1e-9; // tiny ridge for numerical safety
}
if(!SolveLinear(XtX, Xty, kUse, beta))
return false;
rss = 0.0;
for(int i = 0; i < n; i++)
{
int o = i * kTot;
double fit = 0.0;
for(int a = 0; a < kUse; a++)
fit += beta[a] * X[o + a];
double e = Y[i] - fit;
rss += e * e;
}
return true;
}
//+------------------------------------------------------------------+
//| Live Granger F-test |
//+------------------------------------------------------------------+
bool RunGranger(SCausalPair &cp)
{
const int p = cp.p;
const int q = cp.q;
const int W = cp.window;
const int k = 1 + q + p;
int needLow = W + q + 3;
MqlRates lo[];
ArraySetAsSeries(lo, true);
if(CopyRates(_Symbol, cp.lag, 0, needLow, lo) < needLow)
return false;
int ratio = MathMax(1, PeriodSeconds(cp.lead) / PeriodSeconds(cp.lag));
int needHigh = needLow / ratio + p + 20;
MqlRates hi[];
ArraySetAsSeries(hi, true);
int gotHigh = CopyRates(_Symbol, cp.lead, 0, needHigh, hi);
if(gotHigh < p + 3)
return false;
int perH = PeriodSeconds(cp.lead);
double X[], Y[];
ArrayResize(X, W * k);
ArrayResize(Y, W);
int n = 0, h = 0;
double sumY = 0.0, sumY2 = 0.0;
for(int s = 1; s <= W; s++)
{
if(s + q + 1 >= needLow)
break;
datetime cutoff = lo[s].time - perH; // lead bar must have closed by lo[s].time
while(h < gotHigh && hi[h].time > cutoff)
h++;
if(h + p >= gotHigh)
break;
int o = n * k;
X[o] = 1.0;
for(int j = 1; j <= q; j++)
X[o + j] = LogRet(lo, s + j);
for(int m = 1; m <= p; m++)
X[o + q + m] = LogRet(hi, h + m - 1);
Y[n] = LogRet(lo, s);
sumY += Y[n];
sumY2 += Y[n] * Y[n];
n++;
}
if(n < 5 * k)
return false;
double betaU[], betaR[];
double rssU = 0.0, rssR = 0.0;
if(!FitOLS(X, Y, n, k, k, betaU, rssU))
return false;
if(!FitOLS(X, Y, n, k, 1 + q, betaR, rssR))
return false;
int df2 = n - k;
double fStat = (rssU > 0.0) ? ((rssR - rssU) / p) / (rssU / df2) : 0.0;
if(fStat < 0.0)
fStat = 0.0;
int err = 0;
double cdf = MathCumulativeDistributionF(fStat, (double)p, (double)df2, true, false, err);
double pv = (err == 0 && MathIsValidNumber(cdf)) ? 1.0 - cdf : 1.0;
cp.liveF = fStat;
cp.liveP = MathMax(pv, 0.0);
ArrayCopy(cp.beta, betaU);
double mean = sumY / n;
cp.sigmaY = MathSqrt(MathMax(sumY2 / n - mean * mean, 0.0));
cp.liveReady = true;
return true;
}
//+------------------------------------------------------------------+
//| Forecast the current lag-TF bar from closed bars only |
//+------------------------------------------------------------------+
bool ComputePrediction(SCausalPair &cp)
{
cp.pred = 0.0;
cp.vote = 0;
if(!cp.liveReady)
return false;
const int p = cp.p;
const int q = cp.q;
MqlRates lo[];
ArraySetAsSeries(lo, true);
if(CopyRates(_Symbol, cp.lag, 0, q + 2, lo) < q + 2)
return false;
//--- most recent lead bar that closed at or before the current lag bar opened
datetime cutoff = lo[0].time - PeriodSeconds(cp.lead);
int h0 = iBarShift(_Symbol, cp.lead, cutoff, false);
if(h0 < 0)
return false;
while(iTime(_Symbol, cp.lead, h0) > cutoff)
h0++;
while(h0 > 0 && iTime(_Symbol, cp.lead, h0 - 1) <= cutoff)
h0--;
MqlRates hi[];
ArraySetAsSeries(hi, true);
if(CopyRates(_Symbol, cp.lead, h0, p + 1, hi) < p + 1)
return false;
double f = cp.beta[0];
for(int j = 1; j <= q; j++)
f += cp.beta[j] * LogRet(lo, j);
for(int m = 1; m <= p; m++)
f += cp.beta[q + m] * LogRet(hi, m - 1);
cp.pred = f;
double thr = InpMinEdgeSigma * cp.sigmaY;
if(f > thr)
cp.vote = 1;
else
if(f < -thr)
cp.vote = -1;
return true;
}
//+------------------------------------------------------------------+
//| VALID / INVALID decision |
//+------------------------------------------------------------------+
void UpdateValidity(SCausalPair &cp)
{
bool live = cp.liveReady && cp.liveP < cp.alpha;
bool offline = cp.hasOffline && cp.offlineValid;
switch(InpStatusSource)
{
case STATUS_OFFLINE:
cp.valid = cp.hasOffline ? offline : live;
break;
case STATUS_LIVE:
cp.valid = live;
break;
default:
cp.valid = cp.hasOffline ? (offline && live) : live;
break;
}
}
//+------------------------------------------------------------------+
//| Combine the pair votes into one BUY / SELL / NONE signal |
//+------------------------------------------------------------------+
int AggregateSignal()
{
g_bulls = 0;
g_bears = 0;
for(int i = 0; i < g_pairCount; i++)
{
if(!g_pairs[i].liveReady)
continue;
if(InpUseCausalityFilter && !g_pairs[i].valid)
continue;
if(g_pairs[i].vote > 0)
g_bulls++;
if(g_pairs[i].vote < 0)
g_bears++;
}
if(g_bulls >= InpMinVotes && g_bears == 0)
return 1;
if(g_bears >= InpMinVotes && g_bulls == 0)
return -1;
return 0;
}
//+------------------------------------------------------------------+
//| Publish filter state for other EAs (terminal global variables) |
//+------------------------------------------------------------------+
void ExportGlobals(const int signal)
{
for(int i = 0; i < g_pairCount; i++)
GlobalVariableSet("GC_" + _Symbol + "_" + g_pairs[i].leadName + "_" + g_pairs[i].lagName,
g_pairs[i].valid ? 1.0 : 0.0);
GlobalVariableSet("GC_" + _Symbol + "_SIGNAL", (double)signal);
}
//+------------------------------------------------------------------+
//| Trading |
//+------------------------------------------------------------------+
double NormalizeLots(const double lots)
{
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
double stepLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
double l = (stepLot > 0.0) ? MathFloor(lots / stepLot) * stepLot : lots;
return MathMax(minLot, MathMin(maxLot, l));
}
bool GetOurPosition(ulong &ticket, long &type)
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong t = PositionGetTicket(i);
if(t == 0 || !PositionSelectByTicket(t))
continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol)
continue;
if((ulong)PositionGetInteger(POSITION_MAGIC) != InpMagic)
continue;
ticket = t;
type = PositionGetInteger(POSITION_TYPE);
return true;
}
return false;
}
//+------------------------------------------------------------------+
//| Manage Trades |
//+------------------------------------------------------------------+
void ManageTrades(const int signal, const bool isNewSignal)
{
ulong ticket = 0;
long type = -1;
bool hasPos = GetOurPosition(ticket, type);
//--- close on an opposite signal
if(hasPos && InpCloseOnOpposite && signal != 0)
{
bool opposite = (type == POSITION_TYPE_BUY && signal < 0) ||
(type == POSITION_TYPE_SELL && signal > 0);
if(opposite && g_trade.PositionClose(ticket))
hasPos = false;
}
if(hasPos || signal == 0 || !isNewSignal)
return;
double atr[];
if(CopyBuffer(g_atrHandle, 0, 1, 1, atr) < 1 || atr[0] <= 0.0)
return;
int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
double minDist = (double)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * point;
double slDist = MathMax(InpSL_ATR * atr[0], minDist + point);
double tpDist = MathMax(InpTP_ATR * atr[0], minDist + point);
double lots = NormalizeLots(InpLots);
if(signal > 0)
{
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
g_trade.Buy(lots, _Symbol, ask, NormalizeDouble(ask - slDist, digits),
NormalizeDouble(ask + tpDist, digits), "GC BUY");
}
else
{
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
g_trade.Sell(lots, _Symbol, bid, NormalizeDouble(bid + slDist, digits),
NormalizeDouble(bid - tpDist, digits), "GC SELL");
}
}
//+------------------------------------------------------------------+
//| Chart arrows |
//+------------------------------------------------------------------+
void DrawSignalArrow(const int signal, const datetime t)
{
string name = GC_PREFIX + "SIG_" + IntegerToString((long)t);
double price = iClose(_Symbol, InpSignalTF, 1);
ENUM_OBJECT type = (signal > 0) ? OBJ_ARROW_BUY : OBJ_ARROW_SELL;
if(ObjectFind(0, name) >= 0)
ObjectDelete(0, name);
if(ObjectCreate(0, name, type, 0, t, price))
{
ObjectSetInteger(0, name, OBJPROP_COLOR, signal > 0 ? clrDodgerBlue : clrOrangeRed);
ObjectSetInteger(0, name, OBJPROP_WIDTH, 2);
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
}
}
//+------------------------------------------------------------------+
//| Panel |
//+------------------------------------------------------------------+
int PanelRows()
{
return 1 + g_pairCount + 2 + (InpShowSignals ? 1 : 0);
}
//+------------------------------------------------------------------+
//| Set Lable |
//+------------------------------------------------------------------+
void SetLabel(const int row, const string text, const color clr)
{
string name = GC_PREFIX + "L" + IntegerToString(row);
if(ObjectFind(0, name) < 0)
{
ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, name, OBJPROP_XDISTANCE, InpPanelX + 10);
ObjectSetInteger(0, name, OBJPROP_YDISTANCE, InpPanelY + 8 + row * (InpFontSize * 2 + 2));
ObjectSetString(0, name, OBJPROP_FONT, "Consolas");
ObjectSetInteger(0, name, OBJPROP_FONTSIZE, InpFontSize);
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
}
ObjectSetString(0, name, OBJPROP_TEXT, text);
ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
}
//+------------------------------------------------------------------+
//| Create Panel |
//+------------------------------------------------------------------+
void CreatePanel()
{
if(!InpShowPanel)
return;
string bg = GC_PREFIX + "BG";
if(ObjectFind(0, bg) < 0)
ObjectCreate(0, bg, OBJ_RECTANGLE_LABEL, 0, 0, 0);
ObjectSetInteger(0, bg, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, bg, OBJPROP_XDISTANCE, InpPanelX);
ObjectSetInteger(0, bg, OBJPROP_YDISTANCE, InpPanelY);
ObjectSetInteger(0, bg, OBJPROP_XSIZE, InpFontSize * 48);
ObjectSetInteger(0, bg, OBJPROP_YSIZE, PanelRows() * (InpFontSize * 2 + 2) + 16);
ObjectSetInteger(0, bg, OBJPROP_BGCOLOR, C'18,22,30');
ObjectSetInteger(0, bg, OBJPROP_BORDER_TYPE, BORDER_FLAT);
ObjectSetInteger(0, bg, OBJPROP_COLOR, clrDimGray);
ObjectSetInteger(0, bg, OBJPROP_SELECTABLE, false);
}
//+------------------------------------------------------------------+
//| Update panel |
//+------------------------------------------------------------------+
void UpdatePanel(const int signal)
{
if(!InpShowPanel)
return;
int row = 0;
SetLabel(row++, "MULTI-TIMEFRAME CAUSALITY", clrWhite);
for(int i = 0; i < g_pairCount; i++)
{
string status;
color clr;
if(!g_pairs[i].liveReady && InpStatusSource != STATUS_OFFLINE)
{
status = "WARMUP";
clr = clrGray;
}
else
{
status = g_pairs[i].valid ? "VALID" : "INVALID";
clr = g_pairs[i].valid ? clrLimeGreen : clrTomato;
}
double pShow = g_pairs[i].liveReady ? g_pairs[i].liveP : g_pairs[i].offlineP;
double fShow = g_pairs[i].liveReady ? g_pairs[i].liveF : g_pairs[i].offlineF;
string text = StringFormat("%-3s → %-3s %-7s p=%.3f F=%6.2f",
g_pairs[i].leadName, g_pairs[i].lagName, status, pShow, fShow);
if(InpShowSignals && g_pairs[i].liveReady)
text += StringFormat(" %+.2fbp", g_pairs[i].pred);
SetLabel(row++, text, clr);
}
string src = (InpStatusSource == STATUS_OFFLINE) ? "OFFLINE" :
(InpStatusSource == STATUS_LIVE) ? "LIVE" : "BOTH";
SetLabel(row++, StringFormat("Source: %s | File: %s", src, g_fileLoaded ? "loaded" : "fallback"),
clrSilver);
SetLabel(row++, StringFormat("Filter: %s | Retest: %d bars", InpUseCausalityFilter ? "ON" : "OFF",
InpRetestBars), clrSilver);
if(InpShowSignals)
{
string sigText = (signal > 0) ? "BUY" : (signal < 0) ? "SELL" : "NONE";
color sigClr = (signal > 0) ? clrDodgerBlue : (signal < 0) ? clrOrangeRed : clrGray;
SetLabel(row++, StringFormat("SIGNAL: %s (bull %d / bear %d)", sigText, g_bulls, g_bears), sigClr);
}
ChartRedraw(0);
}
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
g_pairCount = 0;
ArrayResize(g_pairs, 0);
g_fileLoaded = LoadCausalityFile();
if(!g_fileLoaded)
LoadFallbackPairs();
if(g_pairCount == 0)
{
Print("No valid causality pairs configured.");
return INIT_PARAMETERS_INCORRECT;
}
g_atrHandle = iATR(_Symbol, InpSignalTF, InpATRPeriod);
if(g_atrHandle == INVALID_HANDLE)
{
Print("Failed to create ATR handle.");
return INIT_FAILED;
}
g_trade.SetExpertMagicNumber(InpMagic);
g_trade.SetTypeFillingBySymbol(_Symbol);
g_trade.SetDeviationInPoints(20);
for(int i = 0; i < g_pairCount; i++)
PrintFormat("Pair %s -> %s | p=%d q=%d window=%d alpha=%.3f | offline: %s",
g_pairs[i].leadName, g_pairs[i].lagName, g_pairs[i].p, g_pairs[i].q,
g_pairs[i].window, g_pairs[i].alpha,
g_pairs[i].hasOffline ? (g_pairs[i].offlineValid ? "VALID" : "INVALID") : "n/a");
CreatePanel();
UpdatePanel(0);
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
if(g_atrHandle != INVALID_HANDLE)
IndicatorRelease(g_atrHandle);
ObjectsDeleteAll(0, GC_PREFIX);
ChartRedraw(0);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
datetime t = iTime(_Symbol, InpSignalTF, 0);
if(t == 0 || t == g_lastSigBar)
return;
g_lastSigBar = t;
for(int i = 0; i < g_pairCount; i++)
{
datetime lb = iTime(_Symbol, g_pairs[i].lag, 0);
if(lb != g_pairs[i].lastLagBar)
{
g_pairs[i].lastLagBar = lb;
g_pairs[i].barsSinceTest++;
}
if(!g_pairs[i].liveReady || g_pairs[i].barsSinceTest >= InpRetestBars)
{
if(RunGranger(g_pairs[i]))
g_pairs[i].barsSinceTest = 0;
}
ComputePrediction(g_pairs[i]);
UpdateValidity(g_pairs[i]);
}
int signal = AggregateSignal();
bool isNewSignal = (signal != 0 && signal != g_lastSignal);
ExportGlobals(signal);
if(isNewSignal && InpShowSignals)
{
DrawSignalArrow(signal, t);
if(InpAlertOnSignal)
Alert(StringFormat("%s Granger MTF signal: %s", _Symbol, signal > 0 ? "BUY" : "SELL"));
}
if(InpExecuteTrades)
ManageTrades(signal, isNewSignal);
g_lastSignal = signal;
UpdatePanel(signal);
}
//+------------------------------------------------------------------+