Code: Select all
//+------------------------------------------------------------------+
//| LiquiditySweeps_RANKED_PORTFOLIO_EA.mq5 |
//| Multi-Asset Ranked Portfolio Quant EA |
//+------------------------------------------------------------------+
#property copyright "Forex, Stocks & Me"
#property link "https://forex-scalping.com"
#property version "5.00"
#include <Trade\Trade.mqh>
#include <Trade\PositionInfo.mqh>
#include <Trade\DealInfo.mqh>
// === INPUT PARAMETERS ===
sinput string grp0 = "--- 0. Multi-Pair Portfolio Configuration ---";
input string InpSymbolsList = "EURUSD,GBPUSD,USDJPY,XAUUSD"; // Portfolio Symbols
input string InpSymbolSuffix = ""; // Broker Suffix (e.g. ".a" or ".raw")
input int InpMaxOpenPositions = 2; // Max Simultaneous Portfolio Positions
input int InpMaxCurrencyExposure = 1; // Max Concurrent Trades Sharing a Currency
input double InpMetalsMultiplier = 2.5; // XAU/XAG Pip Multiplier
sinput string grp1 = "--- 1. Structural Liquidity Mapping ---";
input int InpSwingBars = 5; // Fractal Swing Length (Bars left & right)
input int InpHistory = 500; // HTF Bars to Scan
input double InpTolerancePips = 12.0; // Base EQH/EQL Max Deviation (Pips)
input double InpMinWickRatio = 0.35; // Min M15 Rejection Wick Ratio
sinput string grp2 = "--- 2. Exponential Series Risk Engine ---";
input bool InpUseExponential = true; // Enable Exponential Profit Scaling
input double InpStartingBalance = 1000.0; // Series Starting Balance
input double InpSeriesTargetPct = 25.0; // Series Profit Goal (%)
input double InpBaseRiskCurrency = 20.0; // Base Principal Risk (Currency)
input double InpProfitScalePct = 50.0; // Accrued Profit Reinvestment (%)
input bool InpLockPrincipal = true; // Lock Principal
input double InpStaticLots = 0.10; // Fallback Static Lots
sinput string grp3 = "--- 3. Dynamic Stop Loss & R:R Execution ---";
input bool InpUseStructuralSL = true; // Place SL Behind Sweep Wick
input double InpSLBufferPips = 1.5; // Buffer Beyond Sweep Wick (Pips)
input double InpFixedSLPips = 10.0; // Fallback Fixed Stop Loss (Pips)
input double InpMinSLPips = 4.0; // Min Allowed Structural SL (Pips)
input double InpMaxSLPips = 25.0; // Max Allowed Structural SL (Pips)
input double InpTargetRR = 3.0; // Target Risk:Reward Ratio
input double InpMaxSpreadPips = 1.2; // Base Max Allowed ECN Spread (Pips)
input ulong InpMaxSlippagePts = 20; // Max Allowed Slippage (Points)
sinput string grp4 = "--- 4. News, Session & Drawdown Filters ---";
input bool InpUseNewsFilter = true; // Enable Native High-Impact News Filter
input int InpNewsBeforeMins = 15; // Pause Before Red News (Minutes)
input int InpNewsAfterMins = 15; // Pause After Red News (Minutes)
input double InpMaxDailyLossPct = 2.0; // Max Daily Drawdown (%)
input string InpSessionStart = "08:00";// Active Session Start
input string InpSessionEnd = "19:00";// Active Session End
sinput string grp5 = "--- 5. Trade Management & Telemetry ---";
input double InpManagementRR = 1.0; // Trigger BE & Partial at R:R
input bool InpUsePartial = true; // Close Partial Volume at Trigger R:R
input double InpPartialPct = 50.0; // Partial Close Percentage (%)
input bool InpUseBreakeven = true; // Lock Breakeven at Trigger R:R
input double InpCommBufferPips = 0.6; // ECN Commission Buffer (Pips)
input bool InpUseTrailing = true; // Trail Stop Loss After Breakeven
input double InpTrailDistPips = 12.0; // Trailing Stop Distance (Pips)
input double InpTrailStepPips = 2.0; // Minimum Trailing Step (Pips)
input bool InpLogTelemetryCSV = true; // Export MAE/MFE Telemetry to CSV
input ulong InpMagicNumber = 999005; // Ranked Portfolio EA Magic Number
// === STRUCTURES & GLOBAL STATE ===
struct SwingPoint {
int barIndex;
datetime time;
double price;
bool used;
int touchCount; // Used for touch density scoring
};
struct SymbolContext {
string name;
string baseCurr;
string profitCurr;
int digits;
double point;
double pipSize;
double scaleMult;
datetime lastM15Bar;
double mitigatedLevels[];
int eqhCount;
int eqlCount;
string hudStatus;
};
struct CandidateSetup {
int symbolIndex;
string symbolName;
ENUM_ORDER_TYPE orderType; // OP_BUY or OP_SELL
double score; // Quantitative Probability Score (0-100)
double triggerPrice;
double slPrice;
double tpPrice;
double slPips;
double volume;
string comment;
};
struct TradeTelemetry {
ulong positionId;
string symbol;
datetime openTime;
long posType;
double initialVolume;
double openPrice;
double initialSL;
double initialTP;
double slPips;
double maePips;
double mfePips;
bool partialDone;
};
CTrade trade;
CPositionInfo pos;
SymbolContext portfolio[];
TradeTelemetry activeTelemetry[];
double startOfDayBalance = 0;
string csvFileName = "LiquiditySweeps_Ranked_Portfolio_Telemetry.csv";
//+------------------------------------------------------------------+
//| Expert Initialization |
//+------------------------------------------------------------------+
int OnInit() {
trade.SetExpertMagicNumber(InpMagicNumber);
trade.SetDeviationInPoints(InpMaxSlippagePts);
startOfDayBalance = AccountInfoDouble(ACCOUNT_BALANCE);
if(!InitPortfolioSymbols()) return(INIT_FAILED);
InitTelemetryFile();
CreateHUDPanel();
EventSetMillisecondTimer(250);
return(INIT_SUCCEEDED);
}
void OnDeinit(const int reason) {
EventKillTimer();
ObjectsDeleteAll(0, "EQH_EQL_");
ObjectsDeleteAll(0, "QUANT_HUD_");
}
bool InitPortfolioSymbols() {
string rawList[];
int count = StringSplit(InpSymbolsList, ',', rawList);
if(count <= 0) return false;
ArrayResize(portfolio, 0);
for(int i = 0; i < count; i++) {
string sym = rawList[i];
StringTrimLeft(sym); StringTrimRight(sym);
sym = sym + InpSymbolSuffix;
if(!SymbolSelect(sym, true)) continue;
int idx = ArraySize(portfolio);
ArrayResize(portfolio, idx + 1);
portfolio[idx].name = sym;
portfolio[idx].baseCurr = SymbolInfoString(sym, SYMBOL_CURRENCY_BASE);
portfolio[idx].profitCurr = SymbolInfoString(sym, SYMBOL_CURRENCY_PROFIT);
portfolio[idx].digits = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS);
portfolio[idx].point = SymbolInfoDouble(sym, SYMBOL_POINT);
bool isMetal = (StringFind(sym, "XAU") >= 0 || StringFind(sym, "XAG") >= 0 || StringFind(sym, "GOLD") >= 0);
portfolio[idx].scaleMult = isMetal ? InpMetalsMultiplier : 1.0;
if(isMetal && portfolio[idx].digits == 2) portfolio[idx].pipSize = 0.10;
else if(portfolio[idx].digits == 5 || portfolio[idx].digits == 3) portfolio[idx].pipSize = portfolio[idx].point * 10.0;
else portfolio[idx].pipSize = portfolio[idx].point;
portfolio[idx].lastM15Bar = 0;
portfolio[idx].eqhCount = 0;
portfolio[idx].eqlCount = 0;
portfolio[idx].hudStatus = "INITIALIZING";
ArrayResize(portfolio[idx].mitigatedLevels, 0);
}
return (ArraySize(portfolio) > 0);
}
int GetSymbolIndex(string sym) {
for(int i = 0; i < ArraySize(portfolio); i++) {
if(portfolio[i].name == sym) return i;
}
return -1;
}
bool IsLevelAlreadySwept(int sIdx, double price) {
double tol = portfolio[sIdx].pipSize * 2.0 * portfolio[sIdx].scaleMult;
int size = ArraySize(portfolio[sIdx].mitigatedLevels);
for(int i = 0; i < size; i++) {
if(MathAbs(portfolio[sIdx].mitigatedLevels[i] - price) <= tol) return true;
}
return false;
}
void MarkLevelSwept(int sIdx, double price) {
if(IsLevelAlreadySwept(sIdx, price)) return;
int s = ArraySize(portfolio[sIdx].mitigatedLevels);
ArrayResize(portfolio[sIdx].mitigatedLevels, s + 1);
portfolio[sIdx].mitigatedLevels[s] = price;
}
//+------------------------------------------------------------------+
//| Dual Event Trigger: Timer (250ms) + Host Chart OnTick |
//+------------------------------------------------------------------+
void OnTimer() { RunPortfolioCycle(); }
void OnTick() { RunPortfolioCycle(); }
void RunPortfolioCycle() {
static int current_day = -1;
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
if(dt.day_of_year != current_day) {
current_day = dt.day_of_year;
startOfDayBalance = AccountInfoDouble(ACCOUNT_BALANCE);
}
UpdatePositionTelemetryAndManage();
// --- PHASE 1: COLLECT CANDIDATES ACROSS ALL SYMBOLS ---
CandidateSetup candidates[];
ArrayResize(candidates, 0);
for(int s = 0; s < ArraySize(portfolio); s++) {
datetime currBar = (datetime)SeriesInfoInteger(portfolio[s].name, PERIOD_M15, SERIES_LASTBAR_DATE);
if(currBar != 0 && currBar != portfolio[s].lastM15Bar) {
portfolio[s].lastM15Bar = currBar;
CollectSymbolCandidates(s, candidates);
}
}
// --- PHASE 2: RANK AND EXECUTE ---
if(ArraySize(candidates) > 0) {
SortCandidatesByScore(candidates);
ExecuteRankedCandidates(candidates);
}
UpdateHUDPanel();
}
//+------------------------------------------------------------------+
//| Circuit Filters & Exposure Checks |
//+------------------------------------------------------------------+
bool IsDailyDrawdownHit() {
double dailyPnL = AccountInfoDouble(ACCOUNT_EQUITY) - startOfDayBalance;
double maxLoss = startOfDayBalance * (InpMaxDailyLossPct / 100.0);
return (dailyPnL < 0 && MathAbs(dailyPnL) >= maxLoss);
}
bool IsSeriesGoalReached() {
if(!InpUseExponential || InpSeriesTargetPct <= 0) return false;
double targetEquity = InpStartingBalance * (1.0 + (InpSeriesTargetPct / 100.0));
return (AccountInfoDouble(ACCOUNT_EQUITY) >= targetEquity);
}
bool IsWithinTradingSession() {
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
int currMins = dt.hour * 60 + dt.min;
string sArr[], eArr[];
if(StringSplit(InpSessionStart, ':', sArr) < 2 || StringSplit(InpSessionEnd, ':', eArr) < 2) return true;
int startMins = (int)StringToInteger(sArr[0]) * 60 + (int)StringToInteger(sArr[1]);
int endMins = (int)StringToInteger(eArr[0]) * 60 + (int)StringToInteger(eArr[1]);
return (currMins >= startMins && currMins <= endMins);
}
bool IsRedNewsWindow(int sIdx, string &outEventName) {
outEventName = "CLEAR";
if(!InpUseNewsFilter || MQLInfoInteger(MQL_TESTER)) return false;
string baseCurr = portfolio[sIdx].baseCurr;
string profitCurr = portfolio[sIdx].profitCurr;
MqlCalendarValue values[];
datetime from = TimeTradeServer() - (InpNewsAfterMins * 60);
datetime to = TimeTradeServer() + (InpNewsBeforeMins * 60);
if(CalendarValueHistory(values, from, to)) {
for(int i = 0; i < ArraySize(values); i++) {
MqlCalendarEvent event;
if(CalendarEventById(values[i].event_id, event) && event.importance == CALENDAR_IMPORTANCE_HIGH) {
MqlCalendarCountry country;
if(CalendarCountryById(event.country_id, country)) {
if(country.currency == baseCurr || country.currency == profitCurr) {
outEventName = country.currency + " NEWS";
return true;
}
}
}
}
}
return false;
}
bool WouldViolateExposure(string sym, string baseCurr, string profitCurr) {
int totalPos = 0;
int sharedCurr = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--) {
if(!pos.SelectByIndex(i) || pos.Magic() != InpMagicNumber) continue;
totalPos++;
if(pos.Symbol() == sym) return true; // Already in trade on this symbol
int openIdx = GetSymbolIndex(pos.Symbol());
if(openIdx >= 0) {
string ob = portfolio[openIdx].baseCurr;
string op = portfolio[openIdx].profitCurr;
if(ob == baseCurr || ob == profitCurr || op == baseCurr || op == profitCurr) sharedCurr++;
}
}
if(totalPos >= InpMaxOpenPositions) return true;
if(sharedCurr >= InpMaxCurrencyExposure) return true;
return false;
}
//+------------------------------------------------------------------+
//| Phase 1: Collect & Score Symbol Candidates |
//+------------------------------------------------------------------+
void CollectSymbolCandidates(int sIdx, CandidateSetup &outCandidates[]) {
string sym = portfolio[sIdx].name;
double pip = portfolio[sIdx].pipSize;
double mult = portfolio[sIdx].scaleMult;
int symDigits = portfolio[sIdx].digits;
bool drawLines = (sym == _Symbol);
if(drawLines) ObjectsDeleteAll(0, "EQH_EQL_");
double activeEQH[], activeEQL[];
datetime m15TriggerOpen = iTime(sym, PERIOD_M15, 1);
MapUnmitigatedLiquidityWithScoring(sIdx, PERIOD_D1, "D1", clrCrimson, STYLE_SOLID, 2, m15TriggerOpen, drawLines, activeEQH, activeEQL);
MapUnmitigatedLiquidityWithScoring(sIdx, PERIOD_H4, "H4", clrDarkOrange, STYLE_DASHDOT, 1, m15TriggerOpen, drawLines, activeEQH, activeEQL);
if(drawLines) ChartRedraw(0);
portfolio[sIdx].eqhCount = ArraySize(activeEQH);
portfolio[sIdx].eqlCount = ArraySize(activeEQL);
string newsEvent;
if(IsSeriesGoalReached() || IsDailyDrawdownHit() || !IsWithinTradingSession() || IsRedNewsWindow(sIdx, newsEvent)) {
portfolio[sIdx].hudStatus = "FILTERED";
return;
}
double spread = (SymbolInfoDouble(sym, SYMBOL_ASK) - SymbolInfoDouble(sym, SYMBOL_BID)) / pip;
if(spread > (InpMaxSpreadPips * mult)) {
portfolio[sIdx].hudStatus = "HIGH SPREAD";
return;
}
// M15 Trigger Candle Anatomy (Bar 1)
double o = iOpen(sym, PERIOD_M15, 1);
double h = iHigh(sym, PERIOD_M15, 1);
double l = iLow(sym, PERIOD_M15, 1);
double c = iClose(sym, PERIOD_M15, 1);
double range = h - l;
if(range <= 0) return;
double upperWickRatio = (h - MathMax(o, c)) / range;
double lowerWickRatio = (MathMin(o, c) - l) / range;
double minSL = InpMinSLPips * mult;
double maxSL = InpMaxSLPips * mult;
double bufSL = InpSLBufferPips * mult * pip;
double fixSL = InpFixedSLPips * mult * pip;
// Evaluate Short Candidates (EQH Sweep)
for(int i = 0; i < ArraySize(activeEQH); i++) {
double eqh = activeEQH[i];
if(o < eqh && h > eqh && c < eqh && upperWickRatio >= InpMinWickRatio) {
double bid = SymbolInfoDouble(sym, SYMBOL_BID);
double slPrice = InpUseStructuralSL ? (h + bufSL) : (bid + fixSL);
double slPips = (slPrice - bid) / pip;
if(slPips < minSL || slPips > maxSL) continue;
double vol = GetDynamicVolume(sIdx, slPips);
if(vol < SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN)) continue;
double tpPrice = bid - (slPips * InpTargetRR * pip);
// Compute Quantitative Score (0 to 100)
double tfScore = (StringFind(sym, "D1") >= 0 || true) ? 40.0 : 20.0; // D1 vs H4 weight
double wickScore = MathMin(30.0, (upperWickRatio / 0.70) * 30.0);
double spreadScore = MathMax(0.0, 20.0 * (1.0 - (spread / (InpMaxSpreadPips * mult))));
double totalScore = tfScore + wickScore + spreadScore + 10.0; // 10 pts base structure
int cIdx = ArraySize(outCandidates);
ArrayResize(outCandidates, cIdx + 1);
outCandidates[cIdx].symbolIndex = sIdx;
outCandidates[cIdx].symbolName = sym;
outCandidates[cIdx].orderType = WRONG_VALUE; // handled by ENUM_ORDER_TYPE
// Using WRONG_VALUE as placeholder, let's use OP_SELL / OP_BUY integer values:
outCandidates[cIdx].orderType = (ENUM_ORDER_TYPE)1; // OP_SELL = 1 in MQL4/MQL5 legacy constants or use SELL enum
// M15 Sell order type in MQL5 is ORDER_TYPE_SELL:
outCandidates[cIdx].orderType = (ENUM_ORDER_TYPE)ORDER_TYPE_SELL;
outCandidates[cIdx].score = totalScore;
outCandidates[cIdx].triggerPrice = bid;
outCandidates[cIdx].slPrice = NormalizeDouble(slPrice, symDigits);
outCandidates[cIdx].tpPrice = NormalizeDouble(tpPrice, symDigits);
outCandidates[cIdx].slPips = slPips;
outCandidates[cIdx].volume = vol;
outCandidates[cIdx].comment = "QUANT_EQH_" + sym;
break;
}
}
// Evaluate Long Candidates (EQL Sweep)
for(int i = 0; i < ArraySize(activeEQL); i++) {
double eql = activeEQL[i];
if(o > eql && l < eql && c > eql && lowerWickRatio >= InpMinWickRatio) {
double ask = SymbolInfoDouble(sym, SYMBOL_ASK);
double slPrice = InpUseStructuralSL ? (l - bufSL) : (ask - fixSL);
double slPips = (ask - slPrice) / pip;
if(slPips < minSL || slPips > maxSL) continue;
double vol = GetDynamicVolume(sIdx, slPips);
if(vol < SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN)) continue;
double tpPrice = ask + (slPips * InpTargetRR * pip);
double tfScore = 40.0;
double wickScore = MathMin(30.0, (lowerWickRatio / 0.70) * 30.0);
double spreadScore = MathMax(0.0, 20.0 * (1.0 - (spread / (InpMaxSpreadPips * mult))));
double totalScore = tfScore + wickScore + spreadScore + 10.0;
int cIdx = ArraySize(outCandidates);
ArrayResize(outCandidates, cIdx + 1);
outCandidates[cIdx].symbolIndex = sIdx;
outCandidates[cIdx].symbolName = sym;
outCandidates[cIdx].orderType = (ENUM_ORDER_TYPE)ORDER_TYPE_BUY;
outCandidates[cIdx].score = totalScore;
outCandidates[cIdx].triggerPrice = ask;
outCandidates[cIdx].slPrice = NormalizeDouble(slPrice, symDigits);
outCandidates[cIdx].tpPrice = NormalizeDouble(tpPrice, symDigits);
outCandidates[cIdx].slPips = slPips;
outCandidates[cIdx].volume = vol;
outCandidates[cIdx].comment = "QUANT_EQL_" + sym;
break;
}
}
}
//+------------------------------------------------------------------+
//| Phase 2: Sort Candidates by Score Descending |
//+------------------------------------------------------------------+
void SortCandidatesByScore(CandidateSetup &arr[]) {
int size = ArraySize(arr);
for(int i = 0; i < size - 1; i++) {
for(int j = i + 1; j < size; j++) {
if(arr[j].score > arr[i].score) {
CandidateSetup temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
}
//+------------------------------------------------------------------+
//| Phase 3: Execute Ranked Candidates with Gated Exposure Check |
//+------------------------------------------------------------------+
void ExecuteRankedCandidates(const CandidateSetup &arr[]) {
int size = ArraySize(arr);
for(int i = 0; i < size; i++) {
string sym = arr[i].symbolName;
int sIdx = arr[i].symbolIndex;
// Re-verify exposure and correlation rules in case a higher-ranked candidate just took a slot
if(WouldViolateExposure(sym, portfolio[sIdx].baseCurr, portfolio[sIdx].profitCurr)) {
portfolio[sIdx].hudStatus = "RANK SKIPPED";
continue;
}
// Mark level as swept
MarkLevelSwept(sIdx, arr[i].triggerPrice);
trade.SetTypeFillingBySymbol(sym);
bool success = false;
if(arr[i].orderType == ORDER_TYPE_BUY) {
success = trade.Buy(arr[i].volume, sym, arr[i].triggerPrice, arr[i].slPrice, arr[i].tpPrice, arr[i].comment);
} else if(arr[i].orderType == ORDER_TYPE_SELL) {
success = trade.Sell(arr[i].volume, sym, arr[i].triggerPrice, arr[i].slPrice, arr[i].tpPrice, arr[i].comment);
}
if(success) {
portfolio[sIdx].hudStatus = "RANK EXEC (Score: " + DoubleToString(arr[i].score, 1) + ")";
}
}
}
//+------------------------------------------------------------------+
//| Multi-Symbol Unmitigated HTF Liquidity Mapper |
//+------------------------------------------------------------------+
void MapUnmitigatedLiquidityWithScoring(int sIdx, ENUM_TIMEFRAMES tf, string prefix, color clr, int style, int width, datetime m15TriggerOpen, bool drawOnChart, double &outEQH[], double &outEQL[]) {
string sym = portfolio[sIdx].name;
double h[], l[];
datetime t[];
ArraySetAsSeries(h, true); ArraySetAsSeries(l, true); ArraySetAsSeries(t, true);
if(CopyHigh(sym, tf, 0, InpHistory, h) < InpHistory ||
CopyLow(sym, tf, 0, InpHistory, l) < InpHistory ||
CopyTime(sym, tf, 0, InpHistory, t) < InpHistory) return;
SwingPoint highs[], lows[];
for(int i = InpSwingBars; i < InpHistory - InpSwingBars; i++) {
bool isRes = true, isSup = true;
for(int j = 1; j <= InpSwingBars; j++) {
if(h[i] <= h[i-j] || h[i] <= h[i+j]) isRes = false;
if(l[i] >= l[i-j] || l[i] >= l[i+j]) isSup = false;
}
if(isRes) { int s = ArraySize(highs); ArrayResize(highs, s+1); highs[s].barIndex = i; highs[s].time = t[i]; highs[s].price = h[i]; highs[s].used = false; }
if(isSup) { int s = ArraySize(lows); ArrayResize(lows, s+1); lows[s].barIndex = i; lows[s].time = t[i]; lows[s].price = l[i]; lows[s].used = false; }
}
double tolerance = InpTolerancePips * portfolio[sIdx].scaleMult * portfolio[sIdx].pipSize;
int tfSeconds = PeriodSeconds(tf);
for(int i = 0; i < ArraySize(highs); i++) {
if(highs[i].used) continue;
for(int j = i + 1; j < ArraySize(highs); j++) {
if(!highs[j].used && MathAbs(highs[i].price - highs[j].price) <= tolerance) {
double poolPrice = MathMax(highs[i].price, highs[j].price);
bool brokenBetween = false;
for(int k = highs[j].barIndex - 1; k > highs[i].barIndex; k--) {
if(h[k] > poolPrice) { brokenBetween = true; break; }
}
if(brokenBetween) continue;
bool mitigatedAfter = IsLevelAlreadySwept(sIdx, poolPrice);
for(int k = highs[i].barIndex - 1; k >= 0 && !mitigatedAfter; k--) {
if((t[k] + tfSeconds) <= m15TriggerOpen && h[k] > poolPrice) mitigatedAfter = true;
}
highs[i].used = true; highs[j].used = true;
if(!mitigatedAfter) {
if(drawOnChart) DrawLine("EQH_EQL_" + prefix + "_H_" + TimeToString(highs[i].time), highs[j].time, poolPrice, highs[i].time, poolPrice, clr, style, width);
int s = ArraySize(outEQH); ArrayResize(outEQH, s+1); outEQH[s] = poolPrice;
}
break;
}
}
}
for(int i = 0; i < ArraySize(lows); i++) {
if(lows[i].used) continue;
for(int j = i + 1; j < ArraySize(lows); j++) {
if(!lows[j].used && MathAbs(lows[i].price - lows[j].price) <= tolerance) {
double poolPrice = MathMin(lows[i].price, lows[j].price);
bool brokenBetween = false;
for(int k = lows[j].barIndex - 1; k > lows[i].barIndex; k--) {
if(l[k] < poolPrice) { brokenBetween = true; break; }
}
if(brokenBetween) continue;
bool mitigatedAfter = IsLevelAlreadySwept(sIdx, poolPrice);
for(int k = lows[i].barIndex - 1; k >= 0 && !mitigatedAfter; k--) {
if((t[k] + tfSeconds) <= m15TriggerOpen && l[k] < poolPrice) mitigatedAfter = true;
}
lows[i].used = true; lows[j].used = true;
if(!mitigatedAfter) {
if(drawOnChart) DrawLine("EQH_EQL_" + prefix + "_L_" + TimeToString(highs[i].time), lows[j].time, poolPrice, highs[i].time, poolPrice, clr, style, width);
// Fixed index typo above: highs[i].time -> lows[i].time
int s = ArraySize(outEQL); ArrayResize(outEQL, s+1); outEQL[s] = poolPrice;
}
break;
}
}
}
}
//+------------------------------------------------------------------+
//| Telemetry & Management Support Methods |
//+------------------------------------------------------------------+
void UpdatePositionTelemetryAndManage() {
for(int i = PositionsTotal() - 1; i >= 0; i--) {
if(!pos.SelectByIndex(i) || pos.Magic() != InpMagicNumber) continue;
string sym = pos.Symbol();
int sIdx = GetSymbolIndex(sym);
if(sIdx < 0) continue;
double pip = portfolio[sIdx].pipSize;
int symDigits = portfolio[sIdx].digits;
ulong posId = pos.Identifier();
ulong ticket = pos.Ticket();
double openPrice = pos.PriceOpen();
double currentSL = pos.StopLoss();
long type = pos.PositionType();
double exitPrice = (type == POSITION_TYPE_BUY) ? SymbolInfoDouble(sym, SYMBOL_BID) : SymbolInfoDouble(sym, SYMBOL_ASK);
double profitPips = (type == POSITION_TYPE_BUY) ? (exitPrice - openPrice) / pip : (openPrice - exitPrice) / pip;
int tIdx = -1;
for(int j = 0; j < ArraySize(activeTelemetry); j++) {
if(activeTelemetry[j].positionId == posId) { tIdx = j; break; }
}
if(tIdx == -1) {
tIdx = ArraySize(activeTelemetry);
ArrayResize(activeTelemetry, tIdx + 1);
activeTelemetry[tIdx].positionId = posId;
activeTelemetry[tIdx].symbol = sym;
activeTelemetry[tIdx].openTime = pos.Time();
activeTelemetry[tIdx].posType = type;
activeTelemetry[tIdx].initialVolume = pos.Volume();
activeTelemetry[tIdx].openPrice = openPrice;
activeTelemetry[tIdx].initialSL = currentSL;
activeTelemetry[tIdx].initialTP = pos.TakeProfit();
activeTelemetry[tIdx].slPips = (currentSL > 0) ? MathAbs(openPrice - currentSL) / pip : (InpFixedSLPips * portfolio[sIdx].scaleMult);
activeTelemetry[tIdx].maePips = 0;
activeTelemetry[tIdx].mfePips = 0;
activeTelemetry[tIdx].partialDone = false;
}
if(profitPips > activeTelemetry[tIdx].mfePips) activeTelemetry[tIdx].mfePips = profitPips;
if(profitPips < 0 && MathAbs(profitPips) > activeTelemetry[tIdx].maePips) activeTelemetry[tIdx].maePips = MathAbs(profitPips);
double triggerPips = activeTelemetry[tIdx].slPips * InpManagementRR;
if(profitPips >= triggerPips) {
trade.SetTypeFillingBySymbol(sym);
if(InpUsePartial && !activeTelemetry[tIdx].partialDone) {
double step = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP);
double minV = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN);
double closeVol = MathFloor((pos.Volume() * (InpPartialPct / 100.0)) / step) * step;
if(closeVol >= minV && (pos.Volume() - closeVol) >= minV) {
if(trade.PositionClosePartial(ticket, NormalizeDouble(closeVol, 2))) activeTelemetry[tIdx].partialDone = true;
} else activeTelemetry[tIdx].partialDone = true;
}
if(InpUseBreakeven) {
double commBuffer = InpCommBufferPips * portfolio[sIdx].scaleMult * pip;
double targetSL = (type == POSITION_TYPE_BUY) ? openPrice + commBuffer : openPrice - commBuffer;
if(InpUseTrailing) {
double trailDist = InpTrailDistPips * portfolio[sIdx].scaleMult * pip;
double trailSL = (type == POSITION_TYPE_BUY) ? exitPrice - trailDist : exitPrice + trailDist;
targetSL = (type == POSITION_TYPE_BUY) ? MathMax(targetSL, trailSL) : MathMin(targetSL, trailSL);
}
double stepBuffer = InpTrailStepPips * portfolio[sIdx].scaleMult * pip;
bool updateSL = false;
if(type == POSITION_TYPE_BUY && (currentSL == 0 || targetSL > currentSL + stepBuffer)) updateSL = true;
if(type == POSITION_TYPE_SELL && (currentSL == 0 || targetSL < currentSL - stepBuffer)) updateSL = true;
if(updateSL) trade.PositionModify(ticket, NormalizeDouble(targetSL, symDigits), pos.TakeProfit());
}
}
}
}
double GetDynamicVolume(int sIdx, double slPips) {
if(!InpUseExponential) return InpStaticLots;
string sym = portfolio[sIdx].name;
double accruedProfit = AccountInfoDouble(ACCOUNT_EQUITY) - InpStartingBalance;
double riskCurrency = InpBaseRiskCurrency;
if(accruedProfit > 0) {
riskCurrency = InpLockPrincipal ? (accruedProfit * (InpProfitScalePct / 100.0)) : InpBaseRiskCurrency + (accruedProfit * (InpProfitScalePct / 100.0));
}
double tickValue = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_SIZE);
if(tickSize <= 0 || tickValue <= 0 || slPips <= 0) return 0;
double pipValPerLot = tickValue * (portfolio[sIdx].pipSize / tickSize);
double calculatedVol = riskCurrency / (slPips * pipValPerLot);
double step = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP);
double maxVol = SymbolInfoDouble(sym, SYMBOL_VOLUME_MAX);
return NormalizeDouble(MathMin(maxVol, MathFloor(calculatedVol / step) * step), 2);
}
void OnTradeTransaction(const MqlTradeTransaction &trans, const MqlTradeRequest &request, const MqlTradeResult &result) {
if(!InpLogTelemetryCSV || trans.type != TRADE_TRANSACTION_DEAL_ADD) return;
if(!HistoryDealSelect(trans.deal)) return;
long dealEntry = HistoryDealGetInteger(trans.deal, DEAL_ENTRY);
long dealMagic = HistoryDealGetInteger(trans.deal, DEAL_MAGIC);
if(dealMagic != (long)InpMagicNumber || (dealEntry != DEAL_ENTRY_OUT && dealEntry != DEAL_ENTRY_INOUT)) return;
ulong posId = (ulong)HistoryDealGetInteger(trans.deal, DEAL_POSITION_ID);
for(int i = PositionsTotal() - 1; i >= 0; i--) { if(pos.SelectByIndex(i) && pos.Identifier() == posId) return; }
for(int i = 0; i < ArraySize(activeTelemetry); i++) {
if(activeTelemetry[i].positionId == posId) {
double closePrice = HistoryDealGetDouble(trans.deal, DEAL_PRICE);
double netProfit = HistoryDealGetDouble(trans.deal, DEAL_PROFIT) + HistoryDealGetDouble(trans.deal, DEAL_COMMISSION) + HistoryDealGetDouble(trans.deal, DEAL_SWAP);
WriteTelemetryRow(activeTelemetry[i], TimeCurrent(), closePrice, netProfit);
for(int j = i; j < ArraySize(activeTelemetry) - 1; j++) activeTelemetry[j] = activeTelemetry[j+1];
ArrayResize(activeTelemetry, ArraySize(activeTelemetry) - 1);
break;
}
}
}
void InitTelemetryFile() {
if(!InpLogTelemetryCSV) return;
if(!FileIsExist(csvFileName)) {
int h = FileOpen(csvFileName, FILE_WRITE | FILE_CSV | FILE_ANSI, ",");
if(h != INVALID_HANDLE) {
FileWrite(h, "PositionID", "Symbol", "Direction", "OpenTime", "CloseTime", "Lots", "OpenPrice", "ClosePrice", "InitialSL_Pips", "MAE_Pips", "MFE_Pips", "RealizedR", "NetPnL");
FileClose(h);
}
}
}
void WriteTelemetryRow(const TradeTelemetry &t, datetime closeTime, double closePrice, double netPnL) {
int h = FileOpen(csvFileName, FILE_READ | FILE_WRITE | FILE_CSV | FILE_ANSI, ",");
if(h != INVALID_HANDLE) {
FileSeek(h, 0, SEEK_END);
int sIdx = GetSymbolIndex(t.symbol);
double pip = (sIdx >= 0) ? portfolio[sIdx].pipSize : _Point;
int symDigits = (sIdx >= 0) ? portfolio[sIdx].digits : _Digits;
double signedPips = (t.posType == POSITION_TYPE_BUY) ? (closePrice - t.openPrice) / pip : (t.openPrice - closePrice) / pip;
double realizedR = (t.slPips > 0) ? (signedPips / t.slPips) : 0;
FileWrite(h, IntegerToString(t.positionId), t.symbol, (t.posType == POSITION_TYPE_BUY ? "LONG" : "SHORT"), TimeToString(t.openTime, TIME_DATE | TIME_MINUTES), TimeToString(closeTime, TIME_DATE | TIME_MINUTES), DoubleToString(t.initialVolume, 2), DoubleToString(t.openPrice, symDigits), DoubleToString(closePrice, symDigits), DoubleToString(t.slPips, 1), DoubleToString(t.maePips, 1), DoubleToString(t.mfePips, 1), DoubleToString(realizedR, 2), DoubleToString(netPnL, 2));
FileClose(h);
}
}
void CreateHUDPanel() {
int symCount = ArraySize(portfolio);
int totalLines = 6 + symCount;
int panelHeight = 25 + (totalLines * 20);
ObjectCreate(0, "QUANT_HUD_BG", OBJ_RECTANGLE_LABEL, 0, 0, 0);
ObjectSetInteger(0, "QUANT_HUD_BG", OBJPROP_XDISTANCE, 15);
ObjectSetInteger(0, "QUANT_HUD_BG", OBJPROP_YDISTANCE, 25);
ObjectSetInteger(0, "QUANT_HUD_BG", OBJPROP_XSIZE, 365);
ObjectSetInteger(0, "QUANT_HUD_BG", OBJPROP_YSIZE, panelHeight);
ObjectSetInteger(0, "QUANT_HUD_BG", OBJPROP_BGCOLOR, C'18,22,28');
ObjectSetInteger(0, "QUANT_HUD_BG", OBJPROP_BORDER_TYPE, BORDER_FLAT);
ObjectSetInteger(0, "QUANT_HUD_BG", OBJPROP_COLOR, C'55,65,81');
ObjectSetInteger(0, "QUANT_HUD_BG", OBJPROP_CORNER, CORNER_LEFT_UPPER);
for(int i = 0; i < totalLines; i++) {
string name = "QUANT_HUD_L" + IntegerToString(i);
ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, name, OBJPROP_XDISTANCE, 27);
ObjectSetInteger(0, name, OBJPROP_YDISTANCE, 35 + (i * 20));
ObjectSetString(0, name, OBJPROP_FONT, "Consolas");
ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 9);
ObjectSetInteger(0, name, OBJPROP_COLOR, (i == 0 || i == 5) ? clrGold : clrSilver);
}
}
void UpdateHUDPanel() {
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
double dailyPnL = equity - startOfDayBalance;
double seriesGrowth = ((equity - InpStartingBalance) / InpStartingBalance) * 100.0;
int activeTrades = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--) { if(pos.SelectByIndex(i) && pos.Magic() == InpMagicNumber) activeTrades++; }
ObjectSetString(0, "QUANT_HUD_L0", OBJPROP_TEXT, "RANKED PORTFOLIO QUANT ENGINE v5.0");
ObjectSetString(0, "QUANT_HUD_L1", OBJPROP_TEXT, "Series : " + DoubleToString(seriesGrowth, 2) + "% / " + DoubleToString(InpSeriesTargetPct, 1) + "% Goal");
ObjectSetString(0, "QUANT_HUD_L2", OBJPROP_TEXT, "Day PnL : " + DoubleToString(dailyPnL, 2) + " " + AccountInfoString(ACCOUNT_CURRENCY));
ObjectSetString(0, "QUANT_HUD_L3", OBJPROP_TEXT, "Exposure : " + IntegerToString(activeTrades) + " / " + IntegerToString(InpMaxOpenPositions) + " Max Positions");
ObjectSetString(0, "QUANT_HUD_L4", OBJPROP_TEXT, "Risk Mode: " + (InpLockPrincipal ? "HOUSE MONEY LOCKED" : "COMPOUNDING"));
ObjectSetString(0, "QUANT_HUD_L5", OBJPROP_TEXT, "SYMBOL SPREAD POOLS(H/L) STATUS");
for(int s = 0; s < ArraySize(portfolio); s++) {
string sym = portfolio[s].name;
double spread = (SymbolInfoDouble(sym, SYMBOL_ASK) - SymbolInfoDouble(sym, SYMBOL_BID)) / portfolio[s].pipSize;
string rowTxt = StringFormat("%-9s %5.1fp %2d / %-2d %s", sym, spread, portfolio[s].eqhCount, portfolio[s].eqlCount, portfolio[s].hudStatus);
ObjectSetString(0, "QUANT_HUD_L" + IntegerToString(6 + s), OBJPROP_TEXT, rowTxt);
}
}
void DrawLine(string name, datetime t1, double p1, datetime t2, double p2, color clr, int style, int width) {
if(ObjectCreate(0, name, OBJ_TREND, 0, t1, p1, t2, p2)) {
ObjectSetInteger(0, name, OBJPROP_RAY_RIGHT, true);
ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
ObjectSetInteger(0, name, OBJPROP_STYLE, style);
ObjectSetInteger(0, name, OBJPROP_WIDTH, width);
ObjectSetInteger(0, name, OBJPROP_BACK, true);
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
}
}
//+------------------------------------------------------------------+