845 lines
28 KiB
MQL5
845 lines
28 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| 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);
|
|
}
|
|
//+------------------------------------------------------------------+
|