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