Code: Select all
//+------------------------------------------------------------------+
//| QLM7_Institutional_Scalper.mq5 |
//| Quantum-Liquidity Matrix (QLM-7) Pro ECN Scalping Engine |
//| 5-Layer Quantitative, Order-Flow & House-Money EA + Live HUD |
//+------------------------------------------------------------------+
#property copyright "QLM-7 Quantitative Architecture"
#property version "7.10"
#property strict
#include <Trade\Trade.mqh>
#include <Trade\PositionInfo.mqh>
#include <Trade\SymbolInfo.mqh>
#include <Trade\AccountInfo.mqh>
//+------------------------------------------------------------------+
//| Enumerations & Input Parameters |
//+------------------------------------------------------------------+
enum ENUM_REGIME_STATE
{
REGIME_RANDOM_WALK = 0, // Neutral / Veto Zone (0.45 <= H <= 0.55)
REGIME_MEAN_REV = 1, // Anti-Persistent Fade Mode (H < 0.45)
REGIME_MOMENTUM = 2 // Persistent Trend Mode (H > 0.55)
};
input group "=== [1] General & ECN Execution Settings ==="
input ulong InpMagicNumber = 7007007; // EA Magic Number
input double InpCommissionPerLotRT = 7.0; // ECN Round-Turn Commission ($ per 1.0 Lot)
input double InpMaxCVRRatio = 0.18; // Max Cost-to-Volatility Ratio (Spread+Comm / M1 ATR)
input uint InpMaxSlippagePoints = 4; // Max Permitted Slippage (Points)
input int InpLondonStartHourUTC = 7; // Session Start Hour (UTC)
input int InpNYCloseHourUTC = 17; // Session End Hour (UTC)
input group "=== [2] Layer 1: Regime & VWAP Parameters (M15) ==="
input int InpHurstPeriod = 100; // Rolling M15 Hurst Lookback Bars
input double InpHurstMeanRevMax = 0.45; // Hurst Upper Bound for Mean-Reversion
input double InpHurstMomentumMin = 0.55; // Hurst Lower Bound for Momentum
input double InpVWAPBandSigma = 2.0; // Session VWAP Band Multiplier (Sigma)
input group "=== [3] Layer 2: Cross-Asset Triangular Basis ==="
input bool InpUseTriangularFilter = true; // Enable Synthetic Cross-Pair Basis Filter
input string InpCrossPairA = "EURGBP"; // Synthetic Leg A (Include broker suffix if any)
input string InpCrossPairB = "GBPUSD"; // Synthetic Leg B (Include broker suffix if any)
input double InpMinBasisZScore = 1.50; // Required Synthetic Dislocation Z-Score
input group "=== [4] Layer 3 & 4: Microstructure & Order Flow ==="
input int InpSwingLookbackM5 = 12; // M5 Liquidity Swing Lookback (Bars)
input double InpMinSweepPips = 1.5; // Min Stop-Sweep Penetration (Standard Pips)
input double InpMaxSweepPips = 6.0; // Max Stop-Sweep Penetration (Standard Pips)
input double InpDisplacementATRMult = 1.35; // M1 Rejection Displacement Body / ATR
input int InpTickDeltaLookback = 400; // Tick Count for CVD Absorption Analysis
input double InpMinBookImbalance = 0.35; // Required L2 DOM / Synthetic Imbalance Ratio
input group "=== [5] Layer 5: House-Money Risk & 3-Tranche Exits ==="
input double InpBaseRiskPercent = 0.25; // Base Risk Per Scalp (% of Core Equity)
input double InpProfitLockRatio = 0.50; // Fraction of Net Win Locked (0.50 = 50% Lock / 50% Scale)
input double InpBufferScalingFactor = 0.75; // Gamma: Portion of Active Buffer Deployed on Next Trade
input double InpMaxRiskCapPercent = 1.50; // Hard Cap on Scaled Risk (% of Equity)
input double InpDailyMaxDrawdownPct = 1.25; // Hard Daily Stop Loss (% of Starting Day Equity)
input int InpCooldownMinutes = 45; // Cooldown Duration After 3 Consecutive Losses (Min)
input int InpVelocityTimeStopSec = 180; // Velocity Time-Stop (Seconds to reach +0.5R)
input double InpTranche1RR = 1.20; // Tranche 1 Target (R-Multiple, closes 50%)
input double InpTranche2RR = 2.20; // Tranche 2 Target (R-Multiple, closes 30%)
input double InpKalmanProcessNoise = 0.0005; // Kalman Filter Process Variance (Q) for 20% Runner
input double InpKalmanMeasureNoise = 0.0050; // Kalman Filter Measurement Variance (R)
input group "=== [6] Telemetry HUD Visual Settings ==="
input bool InpShowHUD = true; // Enable On-Chart Telemetry HUD Panel
input int InpHUD_X = 16; // HUD X Offset (Pixels from Top-Left)
input int InpHUD_Y = 28; // HUD Y Offset (Pixels from Top-Left)
input color InpHUD_BgColor = C'16,20,28'; // HUD Background Panel Color
input color InpHUD_BorderColor = C'55,70,95'; // HUD Border Color
//+------------------------------------------------------------------+
//| Internal Position State Tracking Structure |
//+------------------------------------------------------------------+
struct PositionState
{
ulong ticket;
double initialVolume;
double initialRiskPrice;
double entryPrice;
bool tranche1Closed;
bool tranche2Closed;
double kalmanEstimate;
double kalmanErrorCov;
};
//+------------------------------------------------------------------+
//| Global Variables & Objects |
//+------------------------------------------------------------------+
CTrade m_trade;
CPositionInfo m_position;
CSymbolInfo m_symbol;
CAccountInfo m_account;
int g_atrHandleM1 = INVALID_HANDLE;
bool g_domSubscribed = false;
bool g_crossPairsValid = false;
// House-Money & Session State
int g_currentDayOfYear = -1;
double g_dayStartEquity = 0.0;
double g_lockedProfitFloor = 0.0;
double g_activeScalingBuffer = 0.0;
int g_consecutiveLosses = 0;
datetime g_cooldownUntil = 0;
// Triangular Basis Circular Buffer
#define BASIS_BUFFER_SIZE 60
double g_basisBuffer[BASIS_BUFFER_SIZE];
int g_basisIndex = 0;
int g_basisCount = 0;
// Cached Regime & Live Telemetry Metrics
datetime g_lastM15BarTime = 0;
datetime g_lastM1BarTime = 0;
double g_currentHurst = 0.50;
ENUM_REGIME_STATE g_currentRegime = REGIME_RANDOM_WALK;
double g_sessionVWAP = 0.0;
double g_sessionVAH = 0.0;
double g_sessionVAL = 0.0;
double g_liveBasisZScore = 0.0;
double g_liveBIR = 0.0;
double g_liveCVDMomentum = 0.0;
double g_liveCVR = 0.0;
string g_lastSweepStatus = "SCANNING M5 POOLS";
uint g_lastHUDUpdateTick = 0;
#define HUD_PREFIX "QLM7_HUD_"
#define HUD_ROWS 13
// Active Position Tracker
PositionState g_posStates[];
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
if(!m_symbol.Name(_Symbol))
return(INIT_FAILED);
m_symbol.RefreshRates();
m_trade.SetExpertMagicNumber(InpMagicNumber);
m_trade.SetDeviationInPoints(InpMaxSlippagePoints);
m_trade.SetTypeFillingBySymbol(_Symbol);
m_trade.SetAsyncMode(false);
g_atrHandleM1 = iATR(_Symbol, PERIOD_M1, 14);
if(g_atrHandleM1 == INVALID_HANDLE)
{
Print("CRITICAL: Failed to create M1 ATR(14) handle.");
return(INIT_FAILED);
}
if(MarketBookAdd(_Symbol))
g_domSubscribed = true;
else
Print("INFO: L2 DOM unavailable for ", _Symbol, ". Using Synthetic Tick-Velocity Imbalance fallback.");
if(InpUseTriangularFilter)
{
if(SymbolSelect(InpCrossPairA, true) && SymbolSelect(InpCrossPairB, true))
g_crossPairsValid = true;
else
Print("INFO: Cross pairs (", InpCrossPairA, "/", InpCrossPairB, ") unavailable. Using internal velocity Z-Score.");
}
ArrayInitialize(g_basisBuffer, 0.0);
ResetDailyHouseMoneyState();
// Compute initial Layer 1 metrics immediately on startup
g_currentHurst = CalculateHurstExponent(PERIOD_M15, InpHurstPeriod);
UpdateSessionVWAPAndBands();
if(g_currentHurst < InpHurstMeanRevMax)
g_currentRegime = REGIME_MEAN_REV;
else if(g_currentHurst > InpHurstMomentumMin)
g_currentRegime = REGIME_MOMENTUM;
else
g_currentRegime = REGIME_RANDOM_WALK;
if(InpShowHUD)
{
CreateHUDPanel();
UpdateHUDPanel();
}
Print("QLM-7 v7.10 Institutional Scalper + Telemetry HUD initialized on ", _Symbol);
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
if(g_atrHandleM1 != INVALID_HANDLE)
IndicatorRelease(g_atrHandleM1);
if(g_domSubscribed)
MarketBookRelease(_Symbol);
ObjectsDeleteAll(0, HUD_PREFIX);
ChartRedraw(0);
}
//+------------------------------------------------------------------+
//| Create On-Chart Telemetry HUD Objects |
//+------------------------------------------------------------------+
void CreateHUDPanel()
{
ObjectsDeleteAll(0, HUD_PREFIX);
string bgName = HUD_PREFIX + "BG";
ObjectCreate(0, bgName, OBJ_RECTANGLE_LABEL, 0, 0, 0);
ObjectSetInteger(0, bgName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, bgName, OBJPROP_XDISTANCE, InpHUD_X);
ObjectSetInteger(0, bgName, OBJPROP_YDISTANCE, InpHUD_Y);
ObjectSetInteger(0, bgName, OBJPROP_XSIZE, 365);
ObjectSetInteger(0, bgName, OBJPROP_YSIZE, 286);
ObjectSetInteger(0, bgName, OBJPROP_BGCOLOR, InpHUD_BgColor);
ObjectSetInteger(0, bgName, OBJPROP_BORDER_TYPE, BORDER_FLAT);
ObjectSetInteger(0, bgName, OBJPROP_COLOR, InpHUD_BorderColor);
ObjectSetInteger(0, bgName, OBJPROP_BACK, false);
ObjectSetInteger(0, bgName, OBJPROP_SELECTABLE, false);
for(int i = 0; i < HUD_ROWS; i++)
{
string lblName = HUD_PREFIX + "ROW_" + IntegerToString(i);
ObjectCreate(0, lblName, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, lblName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, lblName, OBJPROP_XDISTANCE, InpHUD_X + 12);
ObjectSetInteger(0, lblName, OBJPROP_YDISTANCE, InpHUD_Y + 10 + (i * 20));
ObjectSetString(0, lblName, OBJPROP_FONT, "Consolas");
ObjectSetInteger(0, lblName, OBJPROP_FONTSIZE, (i == 0) ? 10 : 9);
ObjectSetInteger(0, lblName, OBJPROP_COLOR, clrSilver);
ObjectSetInteger(0, lblName, OBJPROP_SELECTABLE, false);
}
}
//+------------------------------------------------------------------+
//| Helper: Update Specific HUD Row Text and Color |
//+------------------------------------------------------------------+
void SetHUDRow(int row, string text, color clr)
{
string lblName = HUD_PREFIX + "ROW_" + IntegerToString(row);
ObjectSetString(0, lblName, OBJPROP_TEXT, text);
ObjectSetInteger(0, lblName, OBJPROP_COLOR, clr);
}
//+------------------------------------------------------------------+
//| Refresh On-Chart Telemetry HUD Values |
//+------------------------------------------------------------------+
void UpdateHUDPanel()
{
if(!InpShowHUD)
return;
// Avoid unnecessary GUI overhead during non-visual Strategy Tester optimizations
if(MQLInfoInteger(MQL_TESTER) && !MQLInfoInteger(MQL_VISUAL_MODE))
return;
// Throttle redraws to ~4 FPS (every 250ms)
uint nowTick = GetTickCount();
if(nowTick - g_lastHUDUpdateTick < 250)
return;
g_lastHUDUpdateTick = nowTick;
MqlDateTime dt;
TimeGMT(dt);
bool sessionOpen = (dt.hour >= InpLondonStartHourUTC && dt.hour < InpNYCloseHourUTC);
// Row 0: Header
SetHUDRow(0, "=== QLM-7 QUANT TELEMETRY [" + _Symbol + "] ===", clrAqua);
// Row 1: Session & System Status
double ddPct = (g_dayStartEquity > 0.0) ? ((g_dayStartEquity - m_account.Equity()) / g_dayStartEquity) * 100.0 : 0.0;
string sysStatus = "ARMED (ACTIVE)";
color sysColor = clrLime;
if(ddPct >= InpDailyMaxDrawdownPct)
{
sysStatus = "HALTED (DAILY MAX DD)";
sysColor = clrTomato;
}
else if(TimeCurrent() < g_cooldownUntil)
{
sysStatus = "COOLDOWN (" + IntegerToString((int)(g_cooldownUntil - TimeCurrent()) / 60) + "m LEFT)";
sysColor = clrTomato;
}
else if(!sessionOpen)
{
sysStatus = "STANDBY (OFF-SESSION)";
sysColor = clrGold;
}
SetHUDRow(1, StringFormat("SYS STATUS : %-24s", sysStatus), sysColor);
// Row 2: Layer 1 Hurst & Regime
string regStr = "RANDOM WALK [VETO]";
color regClr = clrTomato;
if(g_currentRegime == REGIME_MEAN_REV)
{
regStr = "MEAN-REVERSION [FADE]";
regClr = clrDeepSkyBlue;
}
else if(g_currentRegime == REGIME_MOMENTUM)
{
regStr = "MOMENTUM [TREND]";
regClr = clrLime;
}
SetHUDRow(2, StringFormat("L1 HURST : H=%.3f | %s", g_currentHurst, regStr), regClr);
// Row 3: Layer 1 Session VWAP & Bands
int d = m_symbol.Digits();
SetHUDRow(3, StringFormat("L1 VWAP : %s [%s .. %s]",
DoubleToString(g_sessionVWAP, d),
DoubleToString(g_sessionVAL, d),
DoubleToString(g_sessionVAH, d)), clrSilver);
// Row 4: Layer 2 Triangular Basis Z-Score
bool basisArmed = (MathAbs(g_liveBasisZScore) >= InpMinBasisZScore);
string basisMode = g_crossPairsValid ? "TRI-ARB" : "VEL-Z";
SetHUDRow(4, StringFormat("L2 BASIS Z : %+.2f sigma (%s | Req %.2f)",
g_liveBasisZScore, basisMode, InpMinBasisZScore),
basisArmed ? clrLime : clrSilver);
// Row 5: Layer 3 Liquidity Sweep & FVG Status
SetHUDRow(5, StringFormat("L3 STRUCT : %s", g_lastSweepStatus),
(StringFind(g_lastSweepStatus, "SWEEP") >= 0) ? clrLime : clrSilver);
// Row 6: Layer 4 L2 Book Imbalance (BIR) & Tick CVD
double reqBIR = (g_currentRegime == REGIME_MOMENTUM) ? MathMax(InpMinBookImbalance, 0.45) : InpMinBookImbalance;
bool birArmed = (MathAbs(g_liveBIR) >= reqBIR);
string domMode = g_domSubscribed ? "L2-DOM" : "SYNTH";
SetHUDRow(6, StringFormat("L4 OFLOW : BIR=%+.2f (%s) | CVD=%+.0f",
g_liveBIR, domMode, g_liveCVDMomentum),
birArmed ? clrLime : clrSilver);
// Row 7: Layer 5 Cost-to-Volatility Gate (CVR)
bool cvrPass = (g_liveCVR > 0.0 && g_liveCVR <= InpMaxCVRRatio);
SetHUDRow(7, StringFormat("L5 COST CVR: %.3f / %.3f (%s)",
g_liveCVR, InpMaxCVRRatio, cvrPass ? "PASS" : "VETO - HIGH SPREAD"),
cvrPass ? clrLime : clrTomato);
// Row 8: Divider
SetHUDRow(8, "-------------------------------------------", C'55,70,95');
// Row 9: House-Money Floor & Active Scaling Buffer
SetHUDRow(9, StringFormat("HM BUFFER : Lock:+$%.2f | Scale:$%.2f",
g_lockedProfitFloor, g_activeScalingBuffer),
(g_activeScalingBuffer > 0.0) ? clrLime : clrSilver);
// Row 10: Effective Next-Trade Risk Calculation
double coreEq = m_account.Equity();
double baseRiskUSD = coreEq * (InpBaseRiskPercent / 100.0);
double nextRiskUSD = MathMin(coreEq * (InpMaxRiskCapPercent / 100.0),
baseRiskUSD + InpBufferScalingFactor * g_activeScalingBuffer);
double nextRiskPct = (coreEq > 0.0) ? (nextRiskUSD / coreEq) * 100.0 : InpBaseRiskPercent;
SetHUDRow(10, StringFormat("NEXT RISK : $%.2f (%.2f%% Eq) | LossStreak:%d",
nextRiskUSD, nextRiskPct, g_consecutiveLosses),
(nextRiskPct > InpBaseRiskPercent) ? clrAqua : clrSilver);
// Row 11 & 12: Active Position & Kalman Filter Telemetry
string posLine1 = "OPEN TRADE : NONE (AWAITING A+ CONFLUENCE)";
string posLine2 = "KALMAN TRL : STANDBY";
color posClr = clrGray;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(m_position.SelectByIndex(i) && m_position.Symbol() == _Symbol && m_position.Magic() == InpMagicNumber)
{
ulong ticket = m_position.Ticket();
long pType = m_position.PositionType();
double openPrc = m_position.PriceOpen();
double currPrc = (pType == POSITION_TYPE_BUY) ? m_symbol.Bid() : m_symbol.Ask();
int elapsed = (int)(TimeCurrent() - m_position.Time());
double rr = 0.0;
string tState = "T0";
double kEst = openPrc;
for(int k = 0; k < ArraySize(g_posStates); k++)
{
if(g_posStates[k].ticket == ticket && g_posStates[k].initialRiskPrice > 0.0)
{
double pDist = (pType == POSITION_TYPE_BUY) ? (currPrc - openPrc) : (openPrc - currPrc);
rr = pDist / g_posStates[k].initialRiskPrice;
if(g_posStates[k].tranche2Closed) tState = "T2(RUNNER)";
else if(g_posStates[k].tranche1Closed) tState = "T1(BE+50%)";
kEst = g_posStates[k].kalmanEstimate;
break;
}
}
posLine1 = StringFormat("OPEN TRADE : %s %.2fL | %+.2fR | %ds [%s]",
(pType == POSITION_TYPE_BUY ? "BUY" : "SELL"),
m_position.Volume(), rr, elapsed, tState);
posLine2 = StringFormat("KALMAN EST : %s | PnL: %+.2f USD",
DoubleToString(kEst, d), m_position.Profit());
posClr = (rr >= 0.0) ? clrLime : clrOrange;
break;
}
}
SetHUDRow(11, posLine1, posClr);
SetHUDRow(12, posLine2, posClr);
ChartRedraw(0);
}
//+------------------------------------------------------------------+
//| Reset Daily House-Money State on UTC Day Rollover |
//+------------------------------------------------------------------+
void ResetDailyHouseMoneyState()
{
MqlDateTime dt;
TimeGMT(dt);
g_currentDayOfYear = dt.day_of_year;
g_dayStartEquity = m_account.Equity();
g_lockedProfitFloor = 0.0;
g_activeScalingBuffer = 0.0;
g_consecutiveLosses = 0;
g_cooldownUntil = 0;
}
//+------------------------------------------------------------------+
//| Helper: Standard Pip Size Calculation (FX vs Metals) |
//+------------------------------------------------------------------+
double GetPipSize()
{
int digits = m_symbol.Digits();
if(digits == 5 || digits == 3 || digits == 2)
return(m_symbol.Point() * 10.0);
return(m_symbol.Point());
}
//+------------------------------------------------------------------+
//| Layer 1A: Rolling Rescaled Range (R/S) Hurst Exponent on M15 |
//+------------------------------------------------------------------+
double CalculateHurstExponent(ENUM_TIMEFRAMES tf, int period)
{
double close[];
ArraySetAsSeries(close, true);
if(CopyClose(_Symbol, tf, 0, period + 1, close) < period + 1)
return(0.50);
double logReturns[];
ArrayResize(logReturns, period);
double mean = 0.0;
for(int i = 0; i < period; i++)
{
if(close[i + 1] <= 0.0)
return(0.50);
logReturns[i] = MathLog(close[i] / close[i + 1]);
mean += logReturns[i];
}
mean /= (double)period;
double cumDev = 0.0;
double maxDev = -DBL_MAX;
double minDev = DBL_MAX;
double sumSq = 0.0;
for(int i = 0; i < period; i++)
{
double dev = logReturns[i] - mean;
cumDev += dev;
if(cumDev > maxDev) maxDev = cumDev;
if(cumDev < minDev) minDev = cumDev;
sumSq += dev * dev;
}
double range = maxDev - minDev;
double stdev = MathSqrt(sumSq / (double)period);
if(stdev <= 1e-12 || range <= 1e-12)
return(0.50);
double rs = range / stdev;
double hurst = MathLog(rs) / MathLog((double)period);
return(MathMax(0.05, MathMin(0.95, hurst)));
}
//+------------------------------------------------------------------+
//| Layer 1B: Intraday Session VWAP & +/- Sigma Bands |
//+------------------------------------------------------------------+
void UpdateSessionVWAPAndBands()
{
datetime dayStart = iTime(_Symbol, PERIOD_D1, 0);
MqlRates rates[];
ArraySetAsSeries(rates, true);
int copied = CopyRates(_Symbol, PERIOD_M5, dayStart, TimeCurrent(), rates);
if(copied <= 2)
return;
double sumPV = 0.0;
double sumV = 0.0;
for(int i = 0; i < copied; i++)
{
double typical = (rates[i].high + rates[i].low + rates[i].close) / 3.0;
double vol = (double)MathMax((long)1, rates[i].tick_volume);
sumPV += typical * vol;
sumV += vol;
}
if(sumV <= 0.0)
return;
g_sessionVWAP = sumPV / sumV;
double sumVar = 0.0;
for(int i = 0; i < copied; i++)
{
double typical = (rates[i].high + rates[i].low + rates[i].close) / 3.0;
double vol = (double)MathMax((long)1, rates[i].tick_volume);
double diff = typical - g_sessionVWAP;
sumVar += vol * diff * diff;
}
double sigma = MathSqrt(sumVar / sumV);
g_sessionVAH = g_sessionVWAP + InpVWAPBandSigma * sigma;
g_sessionVAL = g_sessionVWAP - InpVWAPBandSigma * sigma;
}
//+------------------------------------------------------------------+
//| Layer 2: Update & Compute Triangular Synthetic Basis Z-Score |
//+------------------------------------------------------------------+
double GetTriangularBasisZScore()
{
m_symbol.RefreshRates();
double spotMid = (m_symbol.Bid() + m_symbol.Ask()) * 0.5;
if(spotMid <= 0.0)
return(0.0);
double basisVal = 0.0;
if(InpUseTriangularFilter && g_crossPairsValid)
{
double bidA = SymbolInfoDouble(InpCrossPairA, SYMBOL_BID);
double askA = SymbolInfoDouble(InpCrossPairA, SYMBOL_ASK);
double bidB = SymbolInfoDouble(InpCrossPairB, SYMBOL_BID);
double askB = SymbolInfoDouble(InpCrossPairB, SYMBOL_ASK);
double midA = (bidA + askA) * 0.5;
double midB = (bidB + askB) * 0.5;
if(midA > 0.0 && midB > 0.0)
basisVal = MathLog(spotMid) - MathLog(midA * midB);
}
else
{
MqlTick ticks[];
if(CopyTicks(_Symbol, ticks, COPY_TICKS_INFO, 0, 20) == 20)
{
double sumMid = 0.0;
for(int i = 0; i < 20; i++)
sumMid += (ticks[i].bid + ticks[i].ask) * 0.5;
double avgMid = sumMid / 20.0;
if(avgMid > 0.0)
basisVal = MathLog(spotMid) - MathLog(avgMid);
}
}
g_basisBuffer[g_basisIndex] = basisVal;
g_basisIndex = (g_basisIndex + 1) % BASIS_BUFFER_SIZE;
if(g_basisCount < BASIS_BUFFER_SIZE)
g_basisCount++;
if(g_basisCount < 20)
return(0.0);
double sum = 0.0;
for(int i = 0; i < g_basisCount; i++)
sum += g_basisBuffer[i];
double mean = sum / (double)g_basisCount;
double sqSum = 0.0;
for(int i = 0; i < g_basisCount; i++)
{
double d = g_basisBuffer[i] - mean;
sqSum += d * d;
}
double stdev = MathSqrt(sqSum / (double)g_basisCount);
if(stdev <= 1e-12)
return(0.0);
g_liveBasisZScore = (basisVal - mean) / stdev;
return(g_liveBasisZScore);
}
//+------------------------------------------------------------------+
//| Layer 3A: Detect Unmitigated M15 Fair Value Gap (FVG) Proximity |
//+------------------------------------------------------------------+
bool CheckM15FVGConfluence(int direction)
{
MqlRates m15[];
ArraySetAsSeries(m15, true);
if(CopyRates(_Symbol, PERIOD_M15, 0, 12, m15) < 12)
return(false);
double currentPrice = (direction > 0) ? m_symbol.Bid() : m_symbol.Ask();
double tolerance = GetPipSize() * 3.0;
for(int i = 1; i <= 8; i++)
{
if(direction > 0 && m15[i].low > m15[i + 2].high)
{
double fvgTop = m15[i].low + tolerance;
double fvgBottom = m15[i + 2].high - tolerance;
if(currentPrice <= fvgTop && currentPrice >= fvgBottom)
return(true);
}
if(direction < 0 && m15[i].high < m15[i + 2].low)
{
double fvgTop = m15[i + 2].low + tolerance;
double fvgBottom = m15[i].high - tolerance;
if(currentPrice <= fvgTop && currentPrice >= fvgBottom)
return(true);
}
}
return(false);
}
//+------------------------------------------------------------------+
//| Layer 3B: Detect M5 Liquidity Stop-Sweep + M1 Displacement |
//+------------------------------------------------------------------+
bool DetectLiquiditySweepAndDisplacement(int &outSignalDir, double &outSweepExtreme)
{
outSignalDir = 0;
outSweepExtreme = 0.0;
MqlRates m5[], m1[];
ArraySetAsSeries(m5, true);
ArraySetAsSeries(m1, true);
if(CopyRates(_Symbol, PERIOD_M5, 1, InpSwingLookbackM5 + 2, m5) < InpSwingLookbackM5 + 2)
return(false);
if(CopyRates(_Symbol, PERIOD_M1, 1, 4, m1) < 4)
return(false);
double atrBuf[];
ArraySetAsSeries(atrBuf, true);
if(CopyBuffer(g_atrHandleM1, 0, 1, 1, atrBuf) < 1 || atrBuf[0] <= 0.0)
return(false);
double m1ATR = atrBuf[0];
double swingHigh = -DBL_MAX;
double swingLow = DBL_MAX;
for(int i = 1; i <= InpSwingLookbackM5; i++)
{
if(m5[i].high > swingHigh) swingHigh = m5[i].high;
if(m5[i].low < swingLow) swingLow = m5[i].low;
}
double pipSize = GetPipSize();
double minSweepDist = InpMinSweepPips * pipSize;
double maxSweepDist = InpMaxSweepPips * pipSize;
double recentLowest = MathMin(m1[0].low, m1[1].low);
double recentHighest = MathMax(m1[0].high, m1[1].high);
double lastBody = MathAbs(m1[0].close - m1[0].open);
double bullSweepDepth = swingLow - recentLowest;
if(bullSweepDepth >= minSweepDist && bullSweepDepth <= maxSweepDist)
{
if(m1[0].close > swingLow && m1[0].close > m1[0].open && lastBody >= InpDisplacementATRMult * m1ATR)
{
outSignalDir = 1;
outSweepExtreme = recentLowest;
g_lastSweepStatus = StringFormat("BULL SWEEP (%.1f pips)", bullSweepDepth / pipSize);
return(true);
}
}
double bearSweepDepth = recentHighest - swingHigh;
if(bearSweepDepth >= minSweepDist && bearSweepDepth <= maxSweepDist)
{
if(m1[0].close < swingHigh && m1[0].close < m1[0].open && lastBody >= InpDisplacementATRMult * m1ATR)
{
outSignalDir = -1;
outSweepExtreme = recentHighest;
g_lastSweepStatus = StringFormat("BEAR SWEEP (%.1f pips)", bearSweepDepth / pipSize);
return(true);
}
}
g_lastSweepStatus = "SCANNING M5 POOLS";
return(false);
}
//+------------------------------------------------------------------+
//| Refresh Live Order-Flow Telemetry (CVD & L2 Book Imbalance) |
//+------------------------------------------------------------------+
void RefreshOrderFlowTelemetry()
{
MqlTick ticks[];
int copied = CopyTicks(_Symbol, ticks, COPY_TICKS_ALL, 0, InpTickDeltaLookback);
if(copied < 100)
return;
int half = copied / 2;
double cvdFirstHalf = 0.0;
double cvdSecondHalf = 0.0;
double synthBuyVol = 0.0;
double synthSellVol = 0.0;
for(int i = 1; i < copied; i++)
{
double vol = (ticks[i].volume > 0) ? (double)ticks[i].volume : 1.0;
double signedVol = 0.0;
bool isBuy = ((ticks[i].flags & TICK_FLAG_BUY) != 0);
bool isSell = ((ticks[i].flags & TICK_FLAG_SELL) != 0);
if(isBuy && !isSell)
signedVol = vol;
else if(isSell && !isBuy)
signedVol = -vol;
else
{
double midCurr = (ticks[i].bid + ticks[i].ask) * 0.5;
double midPrev = (ticks[i - 1].bid + ticks[i - 1].ask) * 0.5;
if(midCurr > midPrev) signedVol = vol;
else if(midCurr < midPrev) signedVol = -vol;
}
if(i < half) cvdFirstHalf += signedVol;
else cvdSecondHalf += signedVol;
if(i >= copied - 50)
{
if(signedVol > 0) synthBuyVol += vol;
if(signedVol < 0) synthSellVol += vol;
}
}
g_liveCVDMomentum = cvdSecondHalf - cvdFirstHalf;
MqlBookInfo book[];
if(g_domSubscribed && MarketBookGet(_Symbol, book) && ArraySize(book) >= 2)
{
double weightedBid = 0.0;
double weightedAsk = 0.0;
int bidRank = 0;
int askRank = 0;
int totalEntries = ArraySize(book);
for(int i = 0; i < totalEntries; i++)
{
if(book[i].type == BOOK_TYPE_BUY || book[i].type == BOOK_TYPE_BUY_MARKET)
{
if(bidRank < 5)
{
double w = MathExp(-0.35 * bidRank);
weightedBid += w * (double)book[i].volume_real;
bidRank++;
}
}
else if(book[i].type == BOOK_TYPE_SELL || book[i].type == BOOK_TYPE_SELL_MARKET)
{
if(askRank < 5)
{
double w = MathExp(-0.35 * askRank);
weightedAsk += w * (double)book[i].volume_real;
askRank++;
}
}
}
if(weightedBid + weightedAsk > 0.0)
g_liveBIR = (weightedBid - weightedAsk) / (weightedBid + weightedAsk);
}
else
{
if(synthBuyVol + synthSellVol > 0.0)
g_liveBIR = (synthBuyVol - synthSellVol) / (synthBuyVol + synthSellVol);
}
}
//+------------------------------------------------------------------+
//| Layer 4: Verify Order-Flow Trigger Using Cached Telemetry |
//+------------------------------------------------------------------+
bool VerifyOrderFlowTrigger(int direction)
{
if(direction > 0 && g_liveCVDMomentum <= 0.0)
return(false);
if(direction < 0 && g_liveCVDMomentum >= 0.0)
return(false);
double requiredBIR = (g_currentRegime == REGIME_MOMENTUM) ? MathMax(InpMinBookImbalance, 0.45) : InpMinBookImbalance;
if(direction > 0 && g_liveBIR < requiredBIR)
return(false);
if(direction < 0 && g_liveBIR > -requiredBIR)
return(false);
return(true);
}
//+------------------------------------------------------------------+
//| Layer 5A: Pre-Trade Cost-to-Volatility Ratio (CVR) Gate |
//+------------------------------------------------------------------+
bool CheckPreTradeCostGate()
{
m_symbol.RefreshRates();
double spreadPrice = m_symbol.Ask() - m_symbol.Bid();
double atrBuf[];
ArraySetAsSeries(atrBuf, true);
if(CopyBuffer(g_atrHandleM1, 0, 0, 1, atrBuf) < 1 || atrBuf[0] <= 0.0)
return(false);
double m1ATR = atrBuf[0];
double tickValue = m_symbol.TickValue();
double tickSize = m_symbol.TickSize();
double commPrice = 0.0;
if(tickValue > 0.0 && tickSize > 0.0)
commPrice = (InpCommissionPerLotRT / tickValue) * tickSize;
g_liveCVR = (spreadPrice + commPrice) / m1ATR;
return(g_liveCVR <= InpMaxCVRRatio);
}
//+------------------------------------------------------------------+
//| Layer 5B: House-Money Exponential Position Sizer |
//+------------------------------------------------------------------+
double CalculateHouseMoneyLots(double stopLossDistancePrice)
{
if(stopLossDistancePrice <= 0.0)
return(0.0);
double coreEquity = m_account.Equity();
double baseRiskUSD = coreEquity * (InpBaseRiskPercent / 100.0);
double scaledRiskUSD = baseRiskUSD + (InpBufferScalingFactor * g_activeScalingBuffer);
double maxCapUSD = coreEquity * (InpMaxRiskCapPercent / 100.0);
scaledRiskUSD = MathMin(scaledRiskUSD, maxCapUSD);
double tickValue = m_symbol.TickValue();
double tickSize = m_symbol.TickSize();
if(tickValue <= 0.0 || tickSize <= 0.0)
return(0.0);
double lossPerLot = (stopLossDistancePrice / tickSize) * tickValue + InpCommissionPerLotRT;
if(lossPerLot <= 0.0)
return(0.0);
double rawLots = scaledRiskUSD / lossPerLot;
double step = m_symbol.LotsStep();
double minLot = m_symbol.LotsMin();
double maxLot = m_symbol.LotsMax();
double normLots = MathFloor(rawLots / step) * step;
normLots = MathMax(minLot, MathMin(maxLot, normLots));
return(NormalizeDouble(normLots, 2));
}
//+------------------------------------------------------------------+
//| Register Newly Opened Position in State Tracker |
//+------------------------------------------------------------------+
void RegisterPositionState(ulong ticket, double volume, double entry, double sl)
{
int n = ArraySize(g_posStates);
ArrayResize(g_posStates, n + 1);
g_posStates[n].ticket = ticket;
g_posStates[n].initialVolume = volume;
g_posStates[n].entryPrice = entry;
g_posStates[n].initialRiskPrice = MathAbs(entry - sl);
g_posStates[n].tranche1Closed = false;
g_posStates[n].tranche2Closed = false;
g_posStates[n].kalmanEstimate = entry;
g_posStates[n].kalmanErrorCov = 1.0;
}
//+------------------------------------------------------------------+
//| Manage Open Positions: 180s Time-Stop, 3-Tranche & Kalman Trail |
//+------------------------------------------------------------------+
void ManageOpenPositions()
{
m_symbol.RefreshRates();
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(!m_position.SelectByIndex(i))
continue;
if(m_position.Symbol() != _Symbol || m_position.Magic() != InpMagicNumber)
continue;
ulong ticket = m_position.Ticket();
long posType = m_position.PositionType();
double openPrc = m_position.PriceOpen();
double currSL = m_position.StopLoss();
double currTP = m_position.TakeProfit();
double currVol = m_position.Volume();
double currPrc = (posType == POSITION_TYPE_BUY) ? m_symbol.Bid() : m_symbol.Ask();
int elapsed = (int)(TimeCurrent() - m_position.Time());
int idx = -1;
for(int k = 0; k < ArraySize(g_posStates); k++)
{
if(g_posStates[k].ticket == ticket)
{
idx = k;
break;
}
}
if(idx < 0)
{
double fallbackRisk = (currSL > 0.0) ? MathAbs(openPrc - currSL) : GetPipSize() * 4.0;
RegisterPositionState(ticket, currVol, openPrc, openPrc - fallbackRisk);
idx = ArraySize(g_posStates) - 1;
}
double riskDist = g_posStates[idx].initialRiskPrice;
if(riskDist <= 0.0)
continue;
double profitDist = (posType == POSITION_TYPE_BUY) ? (currPrc - openPrc) : (openPrc - currPrc);
double currentRR = profitDist / riskDist;
// 1. 180-Second Velocity Time-Stop
if(!g_posStates[idx].tranche1Closed && elapsed >= InpVelocityTimeStopSec && currentRR < 0.50)
{
Print("QLM-7 Velocity Time-Stop triggered on ticket #", ticket, " at R=", DoubleToString(currentRR, 2));
m_trade.PositionClose(ticket);
continue;
}
double lotStep = m_symbol.LotsStep();
double minLot = m_symbol.LotsMin();
// 2. Tranche 1: Close 50% at +1.2R & Move SL to Hard Breakeven + 0.2 Pips
if(!g_posStates[idx].tranche1Closed && currentRR >= InpTranche1RR)
{
double closeVol = MathFloor((g_posStates[idx].initialVolume * 0.50) / lotStep) * lotStep;
if(closeVol >= minLot && (currVol - closeVol) >= minLot)
m_trade.PositionClosePartial(ticket, NormalizeDouble(closeVol, 2));
double beBuffer = GetPipSize() * 0.20;
double newSL = (posType == POSITION_TYPE_BUY) ? (openPrc + beBuffer) : (openPrc - beBuffer);
newSL = m_symbol.NormalizePrice(newSL);
if((posType == POSITION_TYPE_BUY && newSL > currSL) || (posType == POSITION_TYPE_SELL && (currSL == 0.0 || newSL < currSL)))
m_trade.PositionModify(ticket, newSL, currTP);
g_posStates[idx].tranche1Closed = true;
continue;
}
// 3. Tranche 2: Close 30% at Session VWAP or +2.2R
bool vwapReached = (g_sessionVWAP > 0.0) &&
((posType == POSITION_TYPE_BUY && currPrc >= g_sessionVWAP && openPrc < g_sessionVWAP) ||
(posType == POSITION_TYPE_SELL && currPrc <= g_sessionVWAP && openPrc > g_sessionVWAP));
if(g_posStates[idx].tranche1Closed && !g_posStates[idx].tranche2Closed && (currentRR >= InpTranche2RR || vwapReached))
{
double closeVol = MathFloor((g_posStates[idx].initialVolume * 0.30) / lotStep) * lotStep;
if(closeVol >= minLot && (currVol - closeVol) >= minLot)
m_trade.PositionClosePartial(ticket, NormalizeDouble(closeVol, 2));
g_posStates[idx].tranche2Closed = true;
continue;
}
// 4. Tranche 3: 1D Kalman Filter Trail on Final 20% Runner
if(g_posStates[idx].tranche1Closed)
{
double predEst = g_posStates[idx].kalmanEstimate;
double predCov = g_posStates[idx].kalmanErrorCov + InpKalmanProcessNoise;
double kGain = predCov / (predCov + InpKalmanMeasureNoise);
g_posStates[idx].kalmanEstimate = predEst + kGain * (currPrc - predEst);
g_posStates[idx].kalmanErrorCov = (1.0 - kGain) * predCov;
double trailBuffer = GetPipSize() * 1.0;
if(posType == POSITION_TYPE_BUY)
{
double kalmanSL = m_symbol.NormalizePrice(g_posStates[idx].kalmanEstimate - trailBuffer);
if(kalmanSL > currSL && kalmanSL < currPrc)
m_trade.PositionModify(ticket, kalmanSL, currTP);
}
else if(posType == POSITION_TYPE_SELL)
{
double kalmanSL = m_symbol.NormalizePrice(g_posStates[idx].kalmanEstimate + trailBuffer);
if((currSL == 0.0 || kalmanSL < currSL) && kalmanSL > currPrc)
m_trade.PositionModify(ticket, kalmanSL, currTP);
}
}
}
}
//+------------------------------------------------------------------+
//| Expert Tick Function |
//+------------------------------------------------------------------+
void OnTick()
{
MqlDateTime dt;
TimeGMT(dt);
if(dt.day_of_year != g_currentDayOfYear)
ResetDailyHouseMoneyState();
// Continuously refresh live telemetry for HUD and gates
ManageOpenPositions();
double basisZScore = GetTriangularBasisZScore();
RefreshOrderFlowTelemetry();
bool costGateOK = CheckPreTradeCostGate();
// Update Layer 1 on new M15 candle
datetime m15Time = iTime(_Symbol, PERIOD_M15, 0);
if(m15Time != g_lastM15BarTime)
{
g_lastM15BarTime = m15Time;
g_currentHurst = CalculateHurstExponent(PERIOD_M15, InpHurstPeriod);
UpdateSessionVWAPAndBands();
if(g_currentHurst < InpHurstMeanRevMax)
g_currentRegime = REGIME_MEAN_REV;
else if(g_currentHurst > InpHurstMomentumMin)
g_currentRegime = REGIME_MOMENTUM;
else
g_currentRegime = REGIME_RANDOM_WALK;
}
// Render On-Chart Telemetry HUD
UpdateHUDPanel();
// Enforce Daily Drawdown, Cooldown, Session & Regime Vetoes
double currentDrawdownPct = ((g_dayStartEquity - m_account.Equity()) / g_dayStartEquity) * 100.0;
if(currentDrawdownPct >= InpDailyMaxDrawdownPct || TimeCurrent() < g_cooldownUntil)
return;
if(dt.hour < InpLondonStartHourUTC || dt.hour >= InpNYCloseHourUTC)
return;
if(g_currentRegime == REGIME_RANDOM_WALK)
return;
// Only allow one active QLM-7 position per symbol
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(m_position.SelectByIndex(i) && m_position.Symbol() == _Symbol && m_position.Magic() == InpMagicNumber)
return;
}
// Evaluate Layer 3 Liquidity Sweep on M1 Bar
datetime m1Time = iTime(_Symbol, PERIOD_M1, 0);
if(m1Time == g_lastM1BarTime)
return;
int signalDir = 0;
double sweepExtreme = 0.0;
if(!DetectLiquiditySweepAndDisplacement(signalDir, sweepExtreme))
return;
m_symbol.RefreshRates();
double midPrice = (m_symbol.Bid() + m_symbol.Ask()) * 0.5;
if(g_currentRegime == REGIME_MEAN_REV)
{
if(signalDir > 0 && g_sessionVAL > 0.0 && midPrice > g_sessionVWAP)
return;
if(signalDir < 0 && g_sessionVAH > 0.0 && midPrice < g_sessionVWAP)
return;
}
else if(g_currentRegime == REGIME_MOMENTUM)
{
if(signalDir > 0 && midPrice < g_sessionVWAP)
return;
if(signalDir < 0 && midPrice > g_sessionVWAP)
return;
if(!CheckM15FVGConfluence(signalDir))
return;
}
if(InpUseTriangularFilter)
{
if(signalDir > 0 && basisZScore > -InpMinBasisZScore)
return;
if(signalDir < 0 && basisZScore < InpMinBasisZScore)
return;
}
if(!VerifyOrderFlowTrigger(signalDir) || !costGateOK)
return;
double atrBuf[];
ArraySetAsSeries(atrBuf, true);
if(CopyBuffer(g_atrHandleM1, 0, 0, 1, atrBuf) < 1)
return;
double m1ATR = atrBuf[0];
double entryPrice = (signalDir > 0) ? m_symbol.Ask() : m_symbol.Bid();
double slPrice = (signalDir > 0) ? (sweepExtreme - 0.35 * m1ATR) : (sweepExtreme + 0.35 * m1ATR);
slPrice = m_symbol.NormalizePrice(slPrice);
double stopDist = MathAbs(entryPrice - slPrice);
double minStops = m_symbol.StopsLevel() * m_symbol.Point();
if(stopDist <= minStops)
return;
double lots = CalculateHouseMoneyLots(stopDist);
if(lots < m_symbol.LotsMin())
return;
double tpPrice = (signalDir > 0) ? (entryPrice + 4.5 * stopDist) : (entryPrice - 4.5 * stopDist);
tpPrice = m_symbol.NormalizePrice(tpPrice);
bool sent = false;
if(signalDir > 0)
sent = m_trade.Buy(lots, _Symbol, entryPrice, slPrice, tpPrice, "QLM7_Long_Regime" + IntegerToString(g_currentRegime));
else
sent = m_trade.Sell(lots, _Symbol, entryPrice, slPrice, tpPrice, "QLM7_Short_Regime" + IntegerToString(g_currentRegime));
if(sent && m_trade.ResultRetcode() == TRADE_RETCODE_DONE)
{
g_lastM1BarTime = m1Time;
RegisterPositionState(m_trade.ResultOrder(), lots, entryPrice, slPrice);
UpdateHUDPanel();
}
}
//+------------------------------------------------------------------+
//| TradeTransaction: Asymmetric 50/50 House-Money Profit Accounting |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction &trans,
const MqlTradeRequest &request,
const MqlTradeResult &result)
{
if(trans.type != TRADE_TRANSACTION_DEAL_ADD)
return;
if(!HistoryDealSelect(trans.deal))
return;
long dealMagic = HistoryDealGetInteger(trans.deal, DEAL_MAGIC);
string dealSym = HistoryDealGetString(trans.deal, DEAL_SYMBOL);
long dealEntry = HistoryDealGetInteger(trans.deal, DEAL_ENTRY);
if(dealMagic != (long)InpMagicNumber || dealSym != _Symbol)
return;
if(dealEntry != DEAL_ENTRY_OUT && dealEntry != DEAL_ENTRY_INOUT)
return;
double netPnL = HistoryDealGetDouble(trans.deal, DEAL_PROFIT)
+ HistoryDealGetDouble(trans.deal, DEAL_COMMISSION)
+ HistoryDealGetDouble(trans.deal, DEAL_SWAP);
if(netPnL > 0.0)
{
g_consecutiveLosses = 0;
double lockedPart = netPnL * InpProfitLockRatio;
double scaledPart = netPnL * (1.0 - InpProfitLockRatio);
g_lockedProfitFloor += lockedPart;
g_activeScalingBuffer += scaledPart;
}
else if(netPnL < 0.0)
{
g_consecutiveLosses++;
g_activeScalingBuffer = MathMax(0.0, g_activeScalingBuffer + netPnL);
if(g_consecutiveLosses >= 3)
g_cooldownUntil = TimeCurrent() + InpCooldownMinutes * 60;
}
UpdateHUDPanel();
}
//+------------------------------------------------------------------+