Advertisement IC Markets

Checklist fatigue: how I kept my pre-trade list to five lines

Master exponential money management, position sizing calculators, strict daily stop-loss limits, and overcoming FOMO on micro-timeframes.
FTtrader
Posts: 1800
Joined: Mon Aug 03, 2026 2:43 pm

Re: Checklist fatigue: how I kept my pre-trade list to five lines

Post by FTtrader »

Dynamic Structural Wick Stop Loss & Rejection Anatomy: Replaces arbitrary fixed-pip stops with structural placement. The EA verifies that the sweeping M15 candle leaves a genuine rejection wick (default minimum 35% of total candle range), places the Stop Loss directly behind the sweeping wick plus a pip buffer, filters out anomalous micro or macro candles, and scales the Take Profit to an exact $R:R$ multiple while dynamically recalculating lot size so currency risk remains strictly constant.

Series Goal Lockout & Daily Circuit Breaker: Automatically halts the EA when your 2-to-6 week series profit target is reached, locking in series gains until you manually reset it, alongside the daily drawdown circuit breaker.

Tick-Level MAE / MFE CSV Telemetry Logger: Tracks Maximum Adverse Excursion (worst drawdown during the trade) and Maximum Favorable Excursion (peak floating profit) in pips on every tick. When a position closes, OnTradeTransaction appends a complete quantitative record to LiquiditySweeps_Telemetry.csv in your MQL5\Files directory.

Dark-Theme Chart UI Panel & Broker Filling Auto-Detection: Automatically binds to IC Markets' required execution filling mode (FOK / IOC) via SetTypeFillingBySymbol and renders a clean graphical telemetry panel on the chart.
Recommended broker for automated trading & scalping IC Markets
FTtrader
Posts: 1800
Joined: Mon Aug 03, 2026 2:43 pm

Re: Checklist fatigue: how I kept my pre-trade list to five lines

Post by FTtrader »

Save this in MQL5\Experts as LiquiditySweeps_QUANT_EA.mq5 and compile:

Code: Select all

//+------------------------------------------------------------------+
//|                                   LiquiditySweeps_QUANT_EA.mq5   |
//|                                   Quantitative Auto-Execution EA |
//+------------------------------------------------------------------+
#property copyright "Forex, Stocks & Me"
#property link      "https://forex-scalping.com"
#property version   "3.00"

#include <Trade\Trade.mqh>
#include <Trade\PositionInfo.mqh>
#include <Trade\DealInfo.mqh>

// === INPUT PARAMETERS ===
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;    // EQH/EQL Max Deviation (Pips)
input double InpMinWickRatio     = 0.35;    // Min M15 Rejection Wick Ratio (0.35 = 35%)

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 (%) - Halts EA when hit
input double InpBaseRiskCurrency = 20.0;    // Base Principal Risk (Currency)
input double InpProfitScalePct   = 50.0;    // Accrued Profit Reinvestment (%)
input bool   InpLockPrincipal    = true;    // Lock Principal (Risk ONLY Accrued Profit)
input double InpStaticLots       = 0.10;    // Fallback Static Lots (if Exponential OFF)

sinput string grp3 = "--- 3. Dynamic Stop Loss & R:R Execution ---";
input bool   InpUseStructuralSL  = true;    // Place SL Behind Sweep Wick (False = Fixed)
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 (3.0 = 1:3)
input double InpMaxSpreadPips    = 1.2;     // 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 (%) - Circuit Breaker
input string InpSessionStart     = "08:00"; // Active Session Start (Broker Time)
input string InpSessionEnd       = "19:00"; // Active Session End (Broker Time)

sinput string grp5 = "--- 5. Trade Management & Telemetry ---";
input double InpManagementRR     = 1.0;     // Trigger BE & Partial at R:R (1.0 = 1R)
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      = 999003;  // Quantitative EA Magic Number

// === STRUCTURES & GLOBAL STATE ===
struct SwingPoint {
    int      barIndex;
    datetime time;
    double   price;
    bool     used;
};

struct TradeTelemetry {
    ulong    positionId;
    datetime openTime;
    long     posType;
    double   initialVolume;
    double   openPrice;
    double   initialSL;
    double   initialTP;
    double   slPips;
    double   maePips;
    double   mfePips;
    bool     partialDone;
};

CTrade         trade;
CPositionInfo  pos;
TradeTelemetry activeTelemetry[];
double         mitigatedLevels[];
double         startOfDayBalance = 0;
string         csvFileName       = "LiquiditySweeps_Telemetry.csv";

//+------------------------------------------------------------------+
//| Expert Initialization                                            |
//+------------------------------------------------------------------+
int OnInit() {
    trade.SetExpertMagicNumber(InpMagicNumber);
    trade.SetDeviationInPoints(InpMaxSlippagePts);
    trade.SetTypeFillingBySymbol(_Symbol); // Auto-binds FOK/IOC for IC Markets ECN
    
    startOfDayBalance = AccountInfoDouble(ACCOUNT_BALANCE);
    InitTelemetryFile();
    CreateHUDPanel();
    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert Deinitialization                                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
    ObjectsDeleteAll(0, "EQH_EQL_");
    ObjectsDeleteAll(0, "QUANT_HUD_");
}

//+------------------------------------------------------------------+
//| Helper: Dynamic Pip Size for 5-Digit / 3-Digit / Metals          |
//+------------------------------------------------------------------+
double GetPipSize() {
    int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
    if(digits == 5 || digits == 3) return _Point * 10.0;
    if(digits == 2 && StringFind(_Symbol, "XAU") >= 0) return 0.10; // Gold standard pip
    return _Point;
}

//+------------------------------------------------------------------+
//| Helper: Check & Record Mitigated Liquidity Levels                |
//+------------------------------------------------------------------+
bool IsLevelAlreadySwept(double price) {
    double tol = GetPipSize() * 2.0;
    for(int i = 0; i < ArraySize(mitigatedLevels); i++) {
        if(MathAbs(mitigatedLevels[i] - price) <= tol) return true;
    }
    return false;
}

void MarkLevelSwept(double price) {
    if(IsLevelAlreadySwept(price)) return;
    int s = ArraySize(mitigatedLevels);
    ArrayResize(mitigatedLevels, s + 1);
    mitigatedLevels[s] = price;
}

//+------------------------------------------------------------------+
//| Core Tick Loop                                                   |
//+------------------------------------------------------------------+
void OnTick() {
    // 1. Daily Rollover Reset
    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);
    }

    // 2. Tick-Level MAE/MFE Tracking & Trade Management
    UpdatePositionTelemetryAndManage();

    // 3. M15 Bar Close Structural Evaluation
    static datetime last_bar = 0;
    datetime curr_bar = (datetime)SeriesInfoInteger(_Symbol, PERIOD_M15, SERIES_LASTBAR_DATE);
    if(curr_bar != last_bar) {
        last_bar = curr_bar;
        ProcessM15BarClose();
    }

    // 4. Refresh UI Telemetry Panel
    UpdateHUDPanel();
}

//+------------------------------------------------------------------+
//| Circuit Breakers: Drawdown, Series Goal, Session & Red News      |
//+------------------------------------------------------------------+
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(string &outEventName) {
    outEventName = "CLEAR";
    if(!InpUseNewsFilter || MQLInfoInteger(MQL_TESTER)) return false;

    string baseCurr   = SymbolInfoString(_Symbol, SYMBOL_CURRENCY_BASE);
    string profitCurr = SymbolInfoString(_Symbol, SYMBOL_CURRENCY_PROFIT);

    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 + ": " + event.name;
                        return true;
                    }
                }
            }
        }
    }
    return false;
}

//+------------------------------------------------------------------+
//| Tick-Level Telemetry & Trade Management (BE, Partial, Trail)     |
//+------------------------------------------------------------------+
void UpdatePositionTelemetryAndManage() {
    double pip = GetPipSize();

    for(int i = PositionsTotal() - 1; i >= 0; i--) {
        if(!pos.SelectByIndex(i) || pos.Symbol() != _Symbol || pos.Magic() != InpMagicNumber) continue;

        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(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK);
        double profitPips = (type == POSITION_TYPE_BUY) ? (exitPrice - openPrice) / pip : (openPrice - exitPrice) / pip;

        // Locate or register telemetry state
        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].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;
            activeTelemetry[tIdx].maePips       = 0;
            activeTelemetry[tIdx].mfePips       = 0;
            activeTelemetry[tIdx].partialDone   = false;
        }

        // Update MAE & MFE
        if(profitPips > activeTelemetry[tIdx].mfePips) activeTelemetry[tIdx].mfePips = profitPips;
        if(profitPips < 0 && MathAbs(profitPips) > activeTelemetry[tIdx].maePips) activeTelemetry[tIdx].maePips = MathAbs(profitPips);

        // Evaluate R:R Trigger based on actual initial risk distance of this specific trade
        double triggerPips = activeTelemetry[tIdx].slPips * InpManagementRR;
        if(profitPips >= triggerPips) {
            // 1. One-Time Partial Close
            if(InpUsePartial && !activeTelemetry[tIdx].partialDone) {
                double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
                double minV = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
                double closeVol = MathFloor((pos.Volume() * (InpPartialPct / 100.0)) / step) * step;

                if(closeVol >= minV && (pos.Volume() - closeVol) >= minV) {
                    if(trade.PositionClosePartial(ticket, closeVol)) {
                        activeTelemetry[tIdx].partialDone = true;
                    }
                } else {
                    activeTelemetry[tIdx].partialDone = true; // Cannot split micro-lot further
                }
            }

            // 2. Breakeven & Trailing Stop
            if(InpUseBreakeven) {
                double commBuffer = InpCommBufferPips * pip;
                double targetSL   = (type == POSITION_TYPE_BUY) ? openPrice + commBuffer : openPrice - commBuffer;

                if(InpUseTrailing) {
                    double trailSL = (type == POSITION_TYPE_BUY)
                                     ? exitPrice - (InpTrailDistPips * pip)
                                     : exitPrice + (InpTrailDistPips * pip);
                    targetSL = (type == POSITION_TYPE_BUY) ? MathMax(targetSL, trailSL) : MathMin(targetSL, trailSL);
                }

                double stepBuffer = InpTrailStepPips * 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, _Digits), pos.TakeProfit());
                }
            }
        }
    }
}

//+------------------------------------------------------------------+
//| M15 Structural Sweep & Execution Engine                          |
//+------------------------------------------------------------------+
void ProcessM15BarClose() {
    ObjectsDeleteAll(0, "EQH_EQL_");

    double activeEQH[], activeEQL[];
    datetime m15TriggerOpen = iTime(_Symbol, PERIOD_M15, 1);

    MapUnmitigatedLiquidity(PERIOD_D1, "D1", clrCrimson, STYLE_SOLID, 2, m15TriggerOpen, activeEQH, activeEQL);
    MapUnmitigatedLiquidity(PERIOD_H4, "H4", clrDarkOrange, STYLE_DASHDOT, 1, m15TriggerOpen, activeEQH, activeEQL);
    ChartRedraw(0);

    // Enforce Institutional Circuit Breakers
    string newsStatus;
    if(IsDailyDrawdownHit() || IsSeriesGoalReached() || !IsWithinTradingSession() || IsRedNewsWindow(newsStatus)) return;

    double pip    = GetPipSize();
    double spread = (SymbolInfoDouble(_Symbol, SYMBOL_ASK) - SymbolInfoDouble(_Symbol, SYMBOL_BID)) / pip;
    if(spread > InpMaxSpreadPips) return;

    // Only allow one active setup on this symbol
    for(int i = PositionsTotal() - 1; i >= 0; i--) {
        if(pos.SelectByIndex(i) && pos.Symbol() == _Symbol && pos.Magic() == InpMagicNumber) return;
    }

    // M15 Trigger Candle Anatomy (Bar 1)
    double o = iOpen(_Symbol, PERIOD_M15, 1);
    double h = iHigh(_Symbol, PERIOD_M15, 1);
    double l = iLow(_Symbol, PERIOD_M15, 1);
    double c = iClose(_Symbol, 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;

    // 1. Evaluate Bearish Sweeps of Virgin EQH
    for(int i = 0; i < ArraySize(activeEQH); i++) {
        double eqh = activeEQH[i];
        if(o < eqh && h > eqh && c < eqh && upperWickRatio >= InpMinWickRatio) {
            MarkLevelSwept(eqh); // Lock pool as mitigated
            double bid    = SymbolInfoDouble(_Symbol, SYMBOL_BID);
            double slPrice = InpUseStructuralSL ? (h + InpSLBufferPips * pip) : (bid + InpFixedSLPips * pip);
            double slPips  = (slPrice - bid) / pip;

            if(slPips < InpMinSLPips || slPips > InpMaxSLPips) return;

            double vol = GetDynamicVolume(slPips);
            if(vol < SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN)) return;

            double tpPrice = bid - (slPips * InpTargetRR * pip);
            trade.Sell(vol, _Symbol, bid, NormalizeDouble(slPrice, _Digits), NormalizeDouble(tpPrice, _Digits), "QUANT_EQH_Sweep");
            return;
        }
    }

    // 2. Evaluate Bullish Sweeps of Virgin EQL
    for(int i = 0; i < ArraySize(activeEQL); i++) {
        double eql = activeEQL[i];
        if(o > eql && l < eql && c > eql && lowerWickRatio >= InpMinWickRatio) {
            MarkLevelSwept(eql); // Lock pool as mitigated
            double ask    = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
            double slPrice = InpUseStructuralSL ? (l - InpSLBufferPips * pip) : (ask - InpFixedSLPips * pip);
            double slPips  = (ask - slPrice) / pip;

            if(slPips < InpMinSLPips || slPips > InpMaxSLPips) return;

            double vol = GetDynamicVolume(slPips);
            if(vol < SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN)) return;

            double tpPrice = ask + (slPips * InpTargetRR * pip);
            trade.Buy(vol, _Symbol, ask, NormalizeDouble(slPrice, _Digits), NormalizeDouble(tpPrice, _Digits), "QUANT_EQL_Sweep");
            return;
        }
    }
}

//+------------------------------------------------------------------+
//| Unmitigated HTF Liquidity Mapper                                 |
//+------------------------------------------------------------------+
void MapUnmitigatedLiquidity(ENUM_TIMEFRAMES tf, string prefix, color clr, int style, int width, datetime m15TriggerOpen, double &outEQH[], double &outEQL[]) {
    double h[], l[];
    datetime t[];
    ArraySetAsSeries(h, true); ArraySetAsSeries(l, true); ArraySetAsSeries(t, true);

    if(CopyHigh(_Symbol, tf, 0, InpHistory, h) < InpHistory ||
       CopyLow(_Symbol, tf, 0, InpHistory, l)  < InpHistory ||
       CopyTime(_Symbol, 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 * GetPipSize();
    int tfSeconds    = PeriodSeconds(tf);

    // Pair & Verify Unmitigated EQH
    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(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) {
                    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;
            }
        }
    }

    // Pair & Verify Unmitigated EQL
    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(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) {
                    DrawLine("EQH_EQL_" + prefix + "_L_" + TimeToString(lows[i].time), lows[j].time, poolPrice, lows[i].time, poolPrice, clr, style, width);
                    int s = ArraySize(outEQL); ArrayResize(outEQL, s+1); outEQL[s] = poolPrice;
                }
                break;
            }
        }
    }
}

//+------------------------------------------------------------------+
//| Exponential Risk & Margin-Safe Volume Calculator                 |
//+------------------------------------------------------------------+
double GetDynamicVolume(double slPips) {
    if(!InpUseExponential) return InpStaticLots;

    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(_Symbol, SYMBOL_TRADE_TICK_VALUE);
    double tickSize  = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
    if(tickSize <= 0 || tickValue <= 0 || slPips <= 0) return 0;

    double pipValPerLot  = tickValue * (GetPipSize() / tickSize);
    double calculatedVol = riskCurrency / (slPips * pipValPerLot);

    double step   = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
    double maxVol = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
    double finalVol = MathMin(maxVol, MathFloor(calculatedVol / step) * step);
    return NormalizeDouble(finalVol, 2);
}

//+------------------------------------------------------------------+
//| Trade Transaction Listener: Logs MAE/MFE to CSV on Position Exit |
//+------------------------------------------------------------------+
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);

    // Check if position is completely closed
    for(int i = PositionsTotal() - 1; i >= 0; i--) {
        if(pos.SelectByIndex(i) && pos.Identifier() == posId) return; // Still partially open
    }

    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);

            // Remove from active array
            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);
        double pip = GetPipSize();
        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),
                  _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, _Digits),
                  DoubleToString(closePrice, _Digits),
                  DoubleToString(t.slPips, 1),
                  DoubleToString(t.maePips, 1),
                  DoubleToString(t.mfePips, 1),
                  DoubleToString(realizedR, 2),
                  DoubleToString(netPnL, 2));
        FileClose(h);
    }
}

//+------------------------------------------------------------------+
//| Chart Graphical Telemetry Panel & Drawing Helpers                |
//+------------------------------------------------------------------+
void CreateHUDPanel() {
    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, 295);
    ObjectSetInteger(0, "QUANT_HUD_BG", OBJPROP_YSIZE, 155);
    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 < 6; 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 * 22));
        ObjectSetString(0, name, OBJPROP_FONT, "Consolas");
        ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 9);
        ObjectSetInteger(0, name, OBJPROP_COLOR, (i == 0) ? clrGold : clrSilver);
    }
}

void UpdateHUDPanel() {
    double equity        = AccountInfoDouble(ACCOUNT_EQUITY);
    double dailyPnL      = equity - startOfDayBalance;
    double seriesGrowth  = ((equity - InpStartingBalance) / InpStartingBalance) * 100.0;
    double spread        = (SymbolInfoDouble(_Symbol, SYMBOL_ASK) - SymbolInfoDouble(_Symbol, SYMBOL_BID)) / GetPipSize();
    string newsEvent;
    bool   newsBlocked   = IsRedNewsWindow(newsEvent);

    string status = "ACTIVE (SCANNING M15)";
    if(IsSeriesGoalReached())          status = "LOCKED: SERIES GOAL HIT";
    else if(IsDailyDrawdownHit())      status = "HALTED: MAX DAILY LOSS";
    else if(newsBlocked)               status = "PAUSED: " + newsEvent;
    else if(!IsWithinTradingSession()) status = "STANDBY: OUTSIDE SESSION";

    ObjectSetString(0, "QUANT_HUD_L0", OBJPROP_TEXT, "LIQUIDITY SWEEPS QUANT v3.0");
    ObjectSetString(0, "QUANT_HUD_L1", OBJPROP_TEXT, "State   : " + status);
    ObjectSetString(0, "QUANT_HUD_L2", OBJPROP_TEXT, "Series  : " + DoubleToString(seriesGrowth, 2) + "% / " + DoubleToString(InpSeriesTargetPct, 1) + "% Goal");
    ObjectSetString(0, "QUANT_HUD_L3", OBJPROP_TEXT, "Day PnL : " + DoubleToString(dailyPnL, 2) + " " + AccountInfoString(ACCOUNT_CURRENCY));
    ObjectSetString(0, "QUANT_HUD_L4", OBJPROP_TEXT, "Spread  : " + DoubleToString(spread, 1) + " / " + DoubleToString(InpMaxSpreadPips, 1) + " pips");
    ObjectSetString(0, "QUANT_HUD_L5", OBJPROP_TEXT, "Risk    : " + (InpLockPrincipal ? "HOUSE MONEY LOCKED" : "COMPOUNDING"));
}

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);
    }
}
//+------------------------------------------------------------------+
FTtrader
Posts: 1800
Joined: Mon Aug 03, 2026 2:43 pm

Re: Checklist fatigue: how I kept my pre-trade list to five lines

Post by FTtrader »

Key Quantitative Upgrades in v3.00

Unmitigated Pool Filtering (MapUnmitigatedLiquidity): The loop checks all bars between the older swing (highs[j]) and newer swing (highs) to verify a clean valley, and then scans forward from the second touch up to the start of the M15 trigger candle. If any HTF candle pierced the level prior to the current setup, the pool is discarded.

Dynamic Risk Invariance: Because GetDynamicVolume(double slPips) calculates lot size dynamically from the actual distance between entry and the sweeping candle's wick (slPips), a 6-pip stop loss and an 18-pip stop loss risk the exact same dollar amount (InpBaseRiskCurrency or your scaled floating profit percentage).

Post-Trade MAE/MFE Analytics: Every closed trade writes a row to MQL5\Files\LiquiditySweeps_Telemetry.csv recording InitialSL_Pips, MAE_Pips, MFE_Pips, and RealizedR. After a 2-to-6 week series, you can inspect the CSV to see if winners routinely reach higher R:R multiples or if InpSLBufferPips can be tightened.
FTtrader
Posts: 1800
Joined: Mon Aug 03, 2026 2:43 pm

Re: Checklist fatigue: how I kept my pre-trade list to five lines

Post by FTtrader »

Running a multi-asset portfolio from a single MT5 chart requires stripping out every single-chart dependency (_Symbol, _Digits, _Point, and OnTick reliance) and replacing them with an event-driven, multi-symbol state machine.
FTtrader
Posts: 1800
Joined: Mon Aug 03, 2026 2:43 pm

Re: Checklist fatigue: how I kept my pre-trade list to five lines

Post by FTtrader »

Architectural Upgrades in v4.00 Portfolio Quant

250ms Asynchronous Timer Engine (OnTimer): OnTick() only fires when a quote arrives on the host chart. If EURUSD goes quiet for 4 seconds while XAUUSD spikes through your 1R target, a tick-based EA misses the execution window. This version runs a 250-millisecond high-frequency timer (EventSetMillisecondTimer(250)) that polls all symbols in MarketWatch independently for both tick-level trade management and M15 bar closes.

Per-Symbol State Isolation (SymbolContext): Each instrument (EURUSD, GBPUSD, USDJPY, XAUUSD) is initialized into its own isolated memory structure storing its specific digits, point, pipSize, lastM15Bar timestamp, and its own isolated mitigatedLevels[] array.

Metals Volatility Multiplier (InpMetalsMultiplier): Gold (XAUUSD) exhibits wider structural swings and raw spreads than major FX pairs. The engine automatically detects precious metals (XAU / XAG) and scales the EQH/EQL tolerance, Stop Loss bounds, and maximum spread allowance by InpMetalsMultiplier (default 2.5x).

Portfolio Correlation & Exposure Guard: Prevents the EA from unintentionally stacking 4 identical USD bets on the exact same M15 candle close. You can cap both total simultaneous portfolio positions (InpMaxOpenPositions) and maximum concurrent exposure to any single currency (InpMaxCurrencyExposure).

Host-Chart Visual Isolation & Multi-Pair Matrix HUD: Trendlines (EQH_EQL_) are only rendered for the symbol matching the currently open chart so XAUUSD levels at 2750.00 do not crush the vertical scale of a EURUSD chart. The graphical HUD dynamically expands to display a live telemetry row for every pair in your portfolio.
FTtrader
Posts: 1800
Joined: Mon Aug 03, 2026 2:43 pm

Re: Checklist fatigue: how I kept my pre-trade list to five lines

Post by FTtrader »

Save this in MQL5\Experts as LiquiditySweeps_PORTFOLIO_QUANT_EA.mq5, compile, and attach it to any single M15 chart:

Code: Select all

//+------------------------------------------------------------------+
//|                          LiquiditySweeps_PORTFOLIO_QUANT_EA.mq5  |
//|                          Multi-Asset Quantitative Portfolio EA   |
//+------------------------------------------------------------------+
#property copyright "Forex, Stocks & Me"
#property link      "https://forex-scalping.com"
#property version   "4.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 (Comma-Separated)
input string InpSymbolSuffix         = "";     // Broker Suffix (e.g. ".a" or ".raw", leave blank if none)
input int    InpMaxOpenPositions     = 2;      // Max Simultaneous Portfolio Positions
input int    InpMaxCurrencyExposure  = 1;      // Max Concurrent Trades Sharing a Currency (Correlation Guard)
input double InpMetalsMultiplier     = 2.5;    // XAU/XAG Pip Multiplier (Scales Spread, Tol & SL Bounds)

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 (0.35 = 35%)

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 (%) - Halts EA when hit
input double InpBaseRiskCurrency     = 20.0;   // Base Principal Risk (Currency)
input double InpProfitScalePct       = 50.0;   // Accrued Profit Reinvestment (%)
input bool   InpLockPrincipal        = true;   // Lock Principal (Risk ONLY Accrued Profit)
input double InpStaticLots           = 0.10;   // Fallback Static Lots (if Exponential OFF)

sinput string grp3 = "--- 3. Dynamic Stop Loss & R:R Execution ---";
input bool   InpUseStructuralSL      = true;   // Place SL Behind Sweep Wick (False = Fixed)
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 (3.0 = 1:3)
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 (%) - Circuit Breaker
input string InpSessionStart         = "08:00";// Active Session Start (Broker Time)
input string InpSessionEnd           = "19:00";// Active Session End (Broker Time)

sinput string grp5 = "--- 5. Trade Management & Telemetry ---";
input double InpManagementRR         = 1.0;    // Trigger BE & Partial at R:R (1.0 = 1R)
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          = 999004; // Portfolio Quant EA Magic Number

// === STRUCTURES & GLOBAL STATE ===
struct SwingPoint {
    int      barIndex;
    datetime time;
    double   price;
    bool     used;
};

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 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_Portfolio_Telemetry.csv";

//+------------------------------------------------------------------+
//| Expert Initialization                                            |
//+------------------------------------------------------------------+
int OnInit() {
    trade.SetExpertMagicNumber(InpMagicNumber);
    trade.SetDeviationInPoints(InpMaxSlippagePts);

    startOfDayBalance = AccountInfoDouble(ACCOUNT_BALANCE);
    
    if(!InitPortfolioSymbols()) {
        Print("CRITICAL: No valid symbols initialized. Check InpSymbolsList and InpSymbolSuffix.");
        return(INIT_FAILED);
    }

    InitTelemetryFile();
    CreateHUDPanel();
    
    // 250ms asynchronous timer ensures no missed ticks or bar closes across any pair
    EventSetMillisecondTimer(250);
    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert Deinitialization                                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
    EventKillTimer();
    ObjectsDeleteAll(0, "EQH_EQL_");
    ObjectsDeleteAll(0, "QUANT_HUD_");
}

//+------------------------------------------------------------------+
//| Parse & Validate Portfolio Symbols in MarketWatch                |
//+------------------------------------------------------------------+
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)) {
            Print("WARNING: Symbol ", sym, " not found in MarketWatch. Skipping.");
            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);
}

//+------------------------------------------------------------------+
//| Helper: Find Symbol Index in Portfolio                           |
//+------------------------------------------------------------------+
int GetSymbolIndex(string sym) {
    for(int i = 0; i < ArraySize(portfolio); i++) {
        if(portfolio[i].name == sym) return i;
    }
    return -1;
}

//+------------------------------------------------------------------+
//| Helper: Per-Symbol Mitigated Level Tracking                      |
//+------------------------------------------------------------------+
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() {
    // 1. Daily Rollover Reset
    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);
    }

    // 2. Tick-Level MAE/MFE Tracking & Multi-Pair Trade Management
    UpdatePositionTelemetryAndManage();

    // 3. Scan Every Symbol in Portfolio for M15 Bar Close
    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;
            ProcessSymbolM15BarClose(s);
        }
    }

    // 4. Refresh UI Telemetry Panel
    UpdateHUDPanel();
}

//+------------------------------------------------------------------+
//| Circuit Breakers: Drawdown, Series Goal, Session, News & Corr    |
//+------------------------------------------------------------------+
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;
}

//+------------------------------------------------------------------+
//| Portfolio Exposure & Currency Correlation Guard                  |
//+------------------------------------------------------------------+
bool IsExposureAllowed(int sIdx, int &outTotalPos, bool &outHasSymbolPos) {
    outTotalPos     = 0;
    outHasSymbolPos = false;
    int sharedCurrencyCount = 0;

    string targetBase   = portfolio[sIdx].baseCurr;
    string targetProfit = portfolio[sIdx].profitCurr;

    for(int i = PositionsTotal() - 1; i >= 0; i--) {
        if(!pos.SelectByIndex(i) || pos.Magic() != InpMagicNumber) continue;
        
        outTotalPos++;
        string openSym = pos.Symbol();
        if(openSym == portfolio[sIdx].name) outHasSymbolPos = true;

        int openIdx = GetSymbolIndex(openSym);
        if(openIdx >= 0) {
            string ob = portfolio[openIdx].baseCurr;
            string op = portfolio[openIdx].profitCurr;
            if(ob == targetBase || ob == targetProfit || op == targetBase || op == targetProfit) {
                sharedCurrencyCount++;
            }
        }
    }

    if(outHasSymbolPos) return false;
    if(outTotalPos >= InpMaxOpenPositions) return false;
    if(sharedCurrencyCount >= InpMaxCurrencyExposure) return false;
    return true;
}

//+------------------------------------------------------------------+
//| Multi-Symbol Telemetry & Trade Management                        |
//+------------------------------------------------------------------+
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);

            // 1. One-Time Partial Close
            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;
                }
            }

            // 2. Breakeven & Trailing Stop
            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());
                }
            }
        }
    }
}

//+------------------------------------------------------------------+
//| Per-Symbol M15 Structural Sweep & Execution Engine               |
//+------------------------------------------------------------------+
void ProcessSymbolM15BarClose(int sIdx) {
    string sym       = portfolio[sIdx].name;
    double pip       = portfolio[sIdx].pipSize;
    double mult      = portfolio[sIdx].scaleMult;
    int    symDigits = portfolio[sIdx].digits;
    bool   drawLines = (sym == _Symbol); // Only draw trendlines if symbol matches host chart

    if(drawLines) ObjectsDeleteAll(0, "EQH_EQL_");

    double activeEQH[], activeEQL[];
    datetime m15TriggerOpen = iTime(sym, PERIOD_M15, 1);

    MapUnmitigatedLiquidity(sIdx, PERIOD_D1, "D1", clrCrimson, STYLE_SOLID, 2, m15TriggerOpen, drawLines, activeEQH, activeEQL);
    MapUnmitigatedLiquidity(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);

    // Check Global & Symbol Circuit Breakers
    string newsEvent;
    if(IsSeriesGoalReached())          { portfolio[sIdx].hudStatus = "GOAL LOCKED"; return; }
    if(IsDailyDrawdownHit())           { portfolio[sIdx].hudStatus = "DD HALTED";   return; }
    if(!IsWithinTradingSession())      { portfolio[sIdx].hudStatus = "OFF SESSION"; return; }
    if(IsRedNewsWindow(sIdx, newsEvent)){ portfolio[sIdx].hudStatus = newsEvent;     return; }

    int  totalPos = 0;
    bool hasSymPos = false;
    if(!IsExposureAllowed(sIdx, totalPos, hasSymPos)) {
        portfolio[sIdx].hudStatus = hasSymPos ? "IN TRADE" : "CORR BLOCKED";
        return;
    }

    double spread    = (SymbolInfoDouble(sym, SYMBOL_ASK) - SymbolInfoDouble(sym, SYMBOL_BID)) / pip;
    double maxSpread = InpMaxSpreadPips * mult;
    if(spread > maxSpread) {
        portfolio[sIdx].hudStatus = "HIGH SPREAD";
        return;
    }

    portfolio[sIdx].hudStatus = "SCANNING";

    // M15 Trigger Candle Anatomy (Bar 1 of the target symbol)
    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;

    trade.SetTypeFillingBySymbol(sym);

    // 1. Evaluate Bearish Sweeps of Virgin EQH
    for(int i = 0; i < ArraySize(activeEQH); i++) {
        double eqh = activeEQH[i];
        if(o < eqh && h > eqh && c < eqh && upperWickRatio >= InpMinWickRatio) {
            MarkLevelSwept(sIdx, eqh);
            double bid     = SymbolInfoDouble(sym, SYMBOL_BID);
            double slPrice = InpUseStructuralSL ? (h + bufSL) : (bid + fixSL);
            double slPips  = (slPrice - bid) / pip;

            if(slPips < minSL || slPips > maxSL) return;

            double vol = GetDynamicVolume(sIdx, slPips);
            if(vol < SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN)) return;

            double tpPrice = bid - (slPips * InpTargetRR * pip);
            if(trade.Sell(vol, sym, bid, NormalizeDouble(slPrice, symDigits), NormalizeDouble(tpPrice, symDigits), "PORT_EQH_" + sym)) {
                portfolio[sIdx].hudStatus = "SHORT EXEC";
            }
            return;
        }
    }

    // 2. Evaluate Bullish Sweeps of Virgin EQL
    for(int i = 0; i < ArraySize(activeEQL); i++) {
        double eql = activeEQL[i];
        if(o > eql && l < eql && c > eql && lowerWickRatio >= InpMinWickRatio) {
            MarkLevelSwept(sIdx, eql);
            double ask     = SymbolInfoDouble(sym, SYMBOL_ASK);
            double slPrice = InpUseStructuralSL ? (l - bufSL) : (ask - fixSL);
            double slPips  = (ask - slPrice) / pip;

            if(slPips < minSL || slPips > maxSL) return;

            double vol = GetDynamicVolume(sIdx, slPips);
            if(vol < SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN)) return;

            double tpPrice = ask + (slPips * InpTargetRR * pip);
            if(trade.Buy(vol, sym, ask, NormalizeDouble(slPrice, symDigits), NormalizeDouble(tpPrice, symDigits), "PORT_EQL_" + sym)) {
                portfolio[sIdx].hudStatus = "LONG EXEC";
            }
            return;
        }
    }
}

//+------------------------------------------------------------------+
//| Multi-Symbol Unmitigated HTF Liquidity Mapper                    |
//+------------------------------------------------------------------+
void MapUnmitigatedLiquidity(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);

    // Pair & Verify Unmitigated EQH
    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;
            }
        }
    }

    // Pair & Verify Unmitigated EQL
    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(lows[i].time), lows[j].time, poolPrice, lows[i].time, poolPrice, clr, style, width);
                    int s = ArraySize(outEQL); ArrayResize(outEQL, s+1); outEQL[s] = poolPrice;
                }
                break;
            }
        }
    }
}

//+------------------------------------------------------------------+
//| Per-Symbol Exponential Volume Calculator                         |
//+------------------------------------------------------------------+
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);
    double finalVol = MathMin(maxVol, MathFloor(calculatedVol / step) * step);
    return NormalizeDouble(finalVol, 2);
}

//+------------------------------------------------------------------+
//| Trade Transaction Listener: Multi-Pair MAE/MFE CSV Logger        |
//+------------------------------------------------------------------+
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 : GetPipSizeFallback();
        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);
    }
}

double GetPipSizeFallback() {
    return (_Digits == 5 || _Digits == 3) ? _Point * 10.0 : _Point;
}

//+------------------------------------------------------------------+
//| Multi-Asset Graphical Telemetry Matrix HUD                       |
//+------------------------------------------------------------------+
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, "PORTFOLIO QUANT ENGINE v4.0 (MULTI-ASSET)");
    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[sIdxPad(s)];
        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);
    }
}

int sIdxPad(int idx) { return idx; }

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);
    }
}
//+------------------------------------------------------------------+
FTtrader
Posts: 1800
Joined: Mon Aug 03, 2026 2:43 pm

Re: Checklist fatigue: how I kept my pre-trade list to five lines

Post by FTtrader »

Multi-Symbol Operational Notes

Strategy Tester Multi-Currency Backtesting: Because MetaTrader 5's Strategy Tester natively downloads tick history for every symbol requested via SymbolSelect(sym, true), you can run a single backtest on EURUSD M15 in Every Tick based on real ticks mode, and MT5 will simultaneously simulate and execute trades across EURUSD, GBPUSD, USDJPY, and XAUUSD inside a unified equity curve.

Correlation Protection (InpMaxCurrencyExposure = 1): Because all four pairs in your list carry USD as either the base or quote currency, setting InpMaxCurrencyExposure to 1 ensures that if a sudden USD news spike sweeps levels across three pairs simultaneously, the EA only takes the highest-priority setup first and blocks the remaining correlated USD exposure until the active trade closes. If you want to allow two concurrent USD trades, raise InpMaxCurrencyExposure to 2.
FTtrader
Posts: 1800
Joined: Mon Aug 03, 2026 2:43 pm

Re: Checklist fatigue: how I kept my pre-trade list to five lines

Post by FTtrader »

To rank simultaneous triggers across your portfolio, we must transition from an immediate-execution model to a two-phase evaluation pipeline:

Phase 1 (Collection & Scoring): When multiple symbols close an M15 candle with a valid sweep, the EA pauses execution, evaluates every candidate, and computes a quantitative Probability Score (0 to 100) for each setup based on institutional confluence factors.

Phase 2 (Sorting & Gated Execution): The EA sorts all valid candidates descending by score, then iterates through the ranked list—checking exposure, correlation guards, and max open position limits—and executes the highest-scoring setups first.
FTtrader
Posts: 1800
Joined: Mon Aug 03, 2026 2:43 pm

Re: Checklist fatigue: how I kept my pre-trade list to five lines

Post by FTtrader »

The Quantitative Scoring Matrix

Every candidate setup is evaluated across four core structural pillars:

Timeframe Weight (+40 pts): A D1 liquidity pool carries heavier institutional weight than an H4 pool.

Rejection Wick Anatomy (+30 pts): Scaled dynamically by the size of the M15 rejection wick (e.g., a 60% wick scores higher than a 35% wick).

Spread Efficiency (+20 pts): Rewards tighter ECN spreads relative to the pair's volatility allowance.

Structural Touch Density (+10 pts): Awards bonus weight if the liquidity pool was formed by 3 or more touches (triple top/bottom) rather than a simple double top.
FTtrader
Posts: 1800
Joined: Mon Aug 03, 2026 2:43 pm

Re: Checklist fatigue: how I kept my pre-trade list to five lines

Post by FTtrader »

Save this upgraded version in MQL5\Experts as LiquiditySweeps_RANKED_PORTFOLIO_EA.mq5 and attach it to any M15 chart:

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