//+------------------------------------------------------------------+ //| 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 #include #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); } //+------------------------------------------------------------------+