Page 2 of 3

Re: Scalping US100 and US500

Posted: Wed Oct 28, 2026 5:48 pm
by PTScalper
3. MetaTrader 5 (MQL5) Indicator (with Mobile Push Notifications)

In MetaEditor 5, create a new Custom Indicator named NYOpenSweepDetector.mq5 and paste the code below. It includes native SendNotification() integration so any M15 sweep immediately pushes the Entry, SL, and TP coordinates to your MT5 mobile app.

Code: Select all

//+------------------------------------------------------------------+
//|                                          NYOpenSweepDetector.mq5 |
//|                     M15 Pre-Market Sweep & Close Inside Detector |
//+------------------------------------------------------------------+
#property copyright "Algorithmic Index Scalping"
#property version   "1.00"
#property indicator_chart_window
#property indicator_buffers 2
#property indicator_plots   2

// Plot 1: Bearish Sweep Arrow
#property indicator_label1  "Bearish Sweep"
#property indicator_type1   DRAW_ARROW
#property indicator_color1  clrCrimson
#property indicator_width1  2

// Plot 2: Bullish Sweep Arrow
#property indicator_label2  "Bullish Sweep"
#property indicator_type2   DRAW_ARROW
#property indicator_color2  clrLimeGreen
#property indicator_width2  2

//--- Input Parameters (Defaulted to IC Markets Server Time: GMT+2 / GMT+3 DST)
input group "1. Session Window (Broker Server Time)"
input int      InpPmStartHour    = 9;      // Pre-Market/London Start Hour (Server)
input int      InpPmStartMin     = 0;      // Pre-Market/London Start Min
input int      InpNyOpenHour     = 16;     // NY Cash Open Hour (Server: 16:30)
input int      InpNyOpenMin      = 30;     // NY Cash Open Min
input int      InpNyEndHour      = 19;     // NY Killzone End Hour (Server: 19:00)
input int      InpNyEndMin       = 0;      // NY Killzone End Min

input group "2. M15 Sweep & Rejection Filters"
input double   InpMinSweepPts    = 1.5;    // Min Sweep Beyond Level (Index Points)
input double   InpMinWickPct     = 30.0;   // Min Wick Rejection (% of M15 Range)
input double   InpSlBufferPts    = 2.5;    // Stop-Loss Buffer (Index Points)

input group "3. Telemetry & Mobile Alerts"
input bool     InpPopupAlert     = true;   // Desktop Popup & Sound Alert
input bool     InpMobilePush     = true;   // Send MT5 Mobile Push Notification

//--- Indicator Buffers
double BearishSweepBuffer[];
double BullishSweepBuffer[];

//--- State Tracking
datetime g_lastBarTime = 0;
datetime g_lastBearAlertDay = 0;
datetime g_lastBullAlertDay = 0;

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
   SetIndexBuffer(0, BearishSweepBuffer, INDICATOR_DATA);
   SetIndexBuffer(1, BullishSweepBuffer, INDICATOR_DATA);
   
   PlotIndexSetInteger(0, PLOT_ARROW, 234); // Down Arrow
   PlotIndexSetInteger(1, PLOT_ARROW, 233); // Up Arrow
   
   ArraySetAsSeries(BearishSweepBuffer, true);
   ArraySetAsSeries(BullishSweepBuffer, true);
   
   PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, 0.0);
   PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, 0.0);
   
   if(_Period != PERIOD_M15)
      Print("WARNING: Indicator is specifically calibrated for the M15 timeframe.");
      
   return(INIT_SUCCEEDED);
}

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

//+------------------------------------------------------------------+
//| Helper: Convert HH:MM on a given bar's date into datetime        |
//+------------------------------------------------------------------+
datetime GetSessionTime(datetime barTime, int hour, int minute)
{
   MqlDateTime dt;
   TimeToStruct(barTime, dt);
   dt.hour = hour;
   dt.min  = minute;
   dt.sec  = 0;
   return StructToTime(dt);
}

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
{
   if(rates_total < 100) return(0);
   
   ArraySetAsSeries(time, true);
   ArraySetAsSeries(open, true);
   ArraySetAsSeries(high, true);
   ArraySetAsSeries(low, true);
   ArraySetAsSeries(close, true);
   
   int limit = rates_total - prev_calculated;
   if(prev_calculated == 0)
   {
      ArrayInitialize(BearishSweepBuffer, 0.0);
      ArrayInitialize(BullishSweepBuffer, 0.0);
      limit = MathMin(rates_total - 2, 1500); // Scan last ~15 days on init
   }
   
   // Only run full evaluation when a new bar opens (evaluating closed bar index 1)
   bool isNewBar = (time[0] != g_lastBarTime);
   if(!isNewBar && prev_calculated > 0) return(rates_total);
   g_lastBarTime = time[0];
   
   int startIdx = (prev_calculated == 0) ? limit : 1;
   
   for(int i = startIdx; i >= 1; i--)
   {
      BearishSweepBuffer[i] = 0.0;
      BullishSweepBuffer[i] = 0.0;
      
      datetime barTime   = time[i];
      datetime dayAnchor = GetSessionTime(barTime, 0, 0);
      datetime pmStart   = GetSessionTime(barTime, InpPmStartHour, InpPmStartMin);
      datetime nyOpen    = GetSessionTime(barTime, InpNyOpenHour, InpNyOpenMin);
      datetime nyEnd     = GetSessionTime(barTime, InpNyEndHour, InpNyEndMin);
      
      // Only evaluate bars falling inside the NY Open Killzone
      if(barTime < nyOpen || barTime >= nyEnd) continue;
      
      // Scan backwards to find Pre-Market High & Low for this day
      double pmHigh = -DBL_MAX;
      double pmLow  =  DBL_MAX;
      bool   foundPm = false;
      
      for(int j = i + 1; j < rates_total; j++)
      {
         if(time[j] < pmStart) break;
         if(time[j] >= pmStart && time[j] < nyOpen)
         {
            if(high[j] > pmHigh) pmHigh = high[j];
            if(low[j]  < pmLow)  pmLow  = low[j];
            foundPm = true;
         }
      }
      
      if(!foundPm) continue;
      
      // Draw Pre-Market High & Low projection lines for the day
      string highObj = "NYSwp_High_" + TimeToString(dayAnchor, TIME_DATE);
      string lowObj  = "NYSwp_Low_"  + TimeToString(dayAnchor, TIME_DATE);
      
      if(ObjectFind(0, highObj) < 0)
      {
         ObjectCreate(0, highObj, OBJ_TREND, 0, pmStart, pmHigh, nyEnd, pmHigh);
         ObjectSetInteger(0, highObj, OBJ_COLOR, clrDodgerBlue);
         ObjectSetInteger(0, highObj, OBJ_STYLE, STYLE_DOT);
         ObjectSetInteger(0, highObj, OBJ_WIDTH, 2);
         ObjectSetInteger(0, highObj, OBJ_RAY_RIGHT, false);
      }
      if(ObjectFind(0, lowObj) < 0)
      {
         ObjectCreate(0, lowObj, OBJ_TREND, 0, pmStart, pmLow, nyEnd, pmLow);
         ObjectSetInteger(0, lowObj, OBJ_COLOR, clrDodgerBlue);
         ObjectSetInteger(0, lowObj, OBJ_STYLE, STYLE_DOT);
         ObjectSetInteger(0, lowObj, OBJ_WIDTH, 2);
         ObjectSetInteger(0, lowObj, OBJ_RAY_RIGHT, false);
      }
      
      // Evaluate M15 Candle Geometry
      double rng = high[i] - low[i];
      if(rng <= 0.0) continue;
      
      // 1. Check Bearish Liquidity Sweep (PM High Swept & Closed Inside)
      double bearSweepDist = high[i] - pmHigh;
      double upperWick     = high[i] - MathMax(open[i], close[i]);
      double upperWickPct  = (upperWick / rng) * 100.0;
      
      if(bearSweepDist >= InpMinSweepPts && close[i] < pmHigh && upperWickPct >= InpMinWickPct)
      {
         BearishSweepBuffer[i] = high[i] + (rng * 0.25);
         
         if(i == 1 && g_lastBearAlertDay != dayAnchor)
         {
            g_lastBearAlertDay = dayAnchor;
            double sl   = high[i] + InpSlBufferPts;
            double risk = sl - close[i];
            double tp1  = close[i] - (risk * 1.5);
            double tp2  = close[i] - (risk * 3.0);
            
            FireAlerts("SHORT (PM High Sweep)", close[i], sl, tp1, tp2);
         }
      }
      
      // 2. Check Bullish Liquidity Sweep (PM Low Swept & Closed Inside)
      double bullSweepDist = pmLow - low[i];
      double lowerWick     = MathMin(open[i], close[i]) - low[i];
      double lowerWickPct  = (lowerWick / rng) * 100.0;
      
      if(bullSweepDist >= InpMinSweepPts && close[i] > pmLow && lowerWickPct >= InpMinWickPct)
      {
         BullishSweepBuffer[i] = low[i] - (rng * 0.25);
         
         if(i == 1 && g_lastBullAlertDay != dayAnchor)
         {
            g_lastBullAlertDay = dayAnchor;
            double sl   = low[i] - InpSlBufferPts;
            double risk = close[i] - sl;
            double tp1  = close[i] + (risk * 1.5);
            double tp2  = close[i] + (risk * 3.0);
            
            FireAlerts("LONG (PM Low Sweep)", close[i], sl, tp1, tp2);
         }
      }
   }
   
   return(rates_total);
}

//+------------------------------------------------------------------+
//| Dispatch Desktop & Mobile Push Notifications                     |
//+------------------------------------------------------------------+
void FireAlerts(string signalType, double entry, double sl, double tp1, double tp2)
{
   string msg = StringFormat("%s M15 %s | Entry: %.2f | SL: %.2f | TP1(1.5R): %.2f | TP2(3R): %.2f",
                             _Symbol, signalType, entry, sl, tp1, tp2);
                             
   if(InpPopupAlert) Alert(msg);
   if(InpMobilePush) SendNotification(msg);
}
//+------------------------------------------------------------------+

Re: Scalping US100 and US500

Posted: Wed Oct 28, 2026 5:49 pm
by PTScalper
4. Calibration Matrix: US500 vs. US100 (and DST Note)

Because US500 and US100 differ significantly in nominal price and average M15 bar range, adjust the three point-based inputs accordingly.

Important Note on Late-October / Early-November DST Transition

Standard Alignment (Most of the Year): The NY Cash Open (09:30 EST/EDT) occurs at 13:30 UTC during US Daylight Saving Time and 14:30 UTC during US Standard Time (15:30 CET/CEST in Central Europe).

Current Transition Week (Oct 25 – Nov 1): Because Europe shifted back to CET (UTC+1) on Sunday, October 25 while the US remains on EDT (UTC-4) until Sunday, November 1, the NY Cash Open temporarily takes place at 14:30 CET (13:30 UTC) this week before returning to 15:30 CET (14:30 UTC) next Monday. Both indicators above allow you to toggle the exact open hour in seconds.

Re: Scalping US100 and US500

Posted: Wed Oct 28, 2026 5:49 pm
by PTScalper
Upgrading this setup from a basic retail script to a production-grade quantitative tool requires removing hardcoded point values, automating timezone/DST math, and layering institutional order-flow filters alongside dynamic risk sizing.

Re: Scalping US100 and US500

Posted: Wed Oct 28, 2026 5:49 pm
by PTScalper
Key Architectural Upgrades in the Pro Version

Zero-Config NY Timezone & DST Engine (America/New_York): Automatically computes US Daylight Saving Time transitions (2nd Sunday in March to 1st Sunday in November) relative to UTC/Broker Server Time. You never have to manually shift session hours during the October/March EU–US DST mismatch.

ATR-Normalized Volatility Scaling: Replaces static point inputs with M15 ATR(14) multipliers for minimum sweep penetration, maximum candle displacement (filtering out abnormal CPI/FOMC spike bars), and stop-loss buffers. The exact same configuration adapts seamlessly between US500, US100, DE40, and XAU/USD.

Multi-Pool Liquidity Engine (PDH/PDL + PMH/PML): Tracks both Previous Day High/Low and London/Pre-Market High/Low, flagging high-conviction Dual-Pool Sweeps when an M15 bar raids both levels simultaneously.

Institutional Order-Flow Filters (Session VWAP + Tick Volume + H4 Trend): Grades every signal (A+ Graded vs. Standard) by checking whether the M15 sweep candle exhibits institutional volume absorption (Tick Volume greater than 1.25x its 20-period average), closes back across the Session VWAP, and aligns with the H4 EMA(50) structural trend.

Hybrid Linear/Exponential Position Sizing & Mobile Execution Payload: Calculates the exact Lot/Contract size dynamically—using base risk on principal and scaling strictly with accrued series profits—and embeds the calculated lot size, spread check, entry, SL, and R:R targets directly into the MT5/cTrader mobile push notification.

Re: Scalping US100 and US500

Posted: Wed Oct 28, 2026 5:50 pm
by PTScalper
1. C# cTrader Automate (cBot Hybrid Signal & Execution Engine)

In cTrader Automate, building this as a cBot rather than a passive indicator gives you full chart rendering and mobile alerts in AlertOnly_Mobile mode, plus the option to switch on AutoExecute_WithBreakEven for automated order entry, spread-guard protection, and automatic 1R break-even stop management.

Create a new cBot named NYOpenQuantSweepPro and paste the following code:

Code: Select all

using System;
using System.Collections.Generic;
using cAlgo.API;
using cAlgo.API.Indicators;
using cAlgo.API.Internals;

namespace cAlgo.Robots
{
    public enumBotMode
    {
        AlertOnly_Mobile,
        AutoExecute_WithBreakEven
    }

    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class NYOpenQuantSweepPro : Robot
    {
        #region 1. Execution & Risk Management Parameters

        [Parameter("Operating Mode", Group = "1. Execution & Money Management", DefaultValue = enumBotMode.AlertOnly_Mobile)]
        public enumBotMode OperatingMode { get; set; }

        [Parameter("Series Starting Capital ($)", Group = "1. Execution & Money Management", DefaultValue = 5000.0, MinValue = 100.0)]
        public double SeriesBaseCapital { get; set; }

        [Parameter("Base Trade Risk (%)", Group = "1. Execution & Money Management", DefaultValue = 1.0, MinValue = 0.1, MaxValue = 5.0)]
        public double BaseRiskPercent { get; set; }

        [Parameter("Accrued Profit Scaling Factor", Group = "1. Execution & Money Management", DefaultValue = 1.5, MinValue = 1.0, MaxValue = 3.0)]
        public double ExponentialProfitMultiplier { get; set; }

        [Parameter("Max Allowed Spread (Index Pts)", Group = "1. Execution & Money Management", DefaultValue = 1.8, MinValue = 0.1)]
        public double MaxSpreadPoints { get; set; }

        [Parameter("Auto Move SL to BE at R-Multiple", Group = "1. Execution & Money Management", DefaultValue = 1.2, MinValue = 0.5)]
        public double BreakEvenTriggerR { get; set; }

        #endregion

        #region 2. Adaptive ATR & Order-Flow Filters

        [Parameter("ATR Period (M15)", Group = "2. Quant & Microstructure Filters", DefaultValue = 14, MinValue = 5)]
        public int AtrPeriod { get; set; }

        [Parameter("Min Sweep Depth (x M15 ATR)", Group = "2. Quant & Microstructure Filters", DefaultValue = 0.10, MinValue = 0.01, MaxValue = 1.0)]
        public double MinSweepAtrMult { get; set; }

        [Parameter("Max Candle Size (x M15 ATR)", Group = "2. Quant & Microstructure Filters", DefaultValue = 2.8, MinValue = 1.0, MaxValue = 6.0)]
        public double MaxCandleAtrMult { get; set; }

        [Parameter("SL Buffer Beyond Wick (x M15 ATR)", Group = "2. Quant & Microstructure Filters", DefaultValue = 0.20, MinValue = 0.05, MaxValue = 1.0)]
        public double SlBufferAtrMult { get; set; }

        [Parameter("Min Rejection Wick (% of Bar)", Group = "2. Quant & Microstructure Filters", DefaultValue = 32.0, MinValue = 10.0, MaxValue = 85.0)]
        public double MinWickPercent { get; set; }

        [Parameter("Require Volume Spike (> 1.2x SMA20)", Group = "2. Quant & Microstructure Filters", DefaultValue = true)]
        public bool RequireVolumeDisplacement { get; set; }

        [Parameter("Require Session VWAP Alignment", Group = "2. Quant & Microstructure Filters", DefaultValue = true)]
        public bool RequireVwapFilter { get; set; }

        #endregion

        #region 3. Telemetry & Alerts

        [Parameter("Enable Sound & Popup Alert", Group = "3. Telemetry & Notifications", DefaultValue = true)]
        public bool EnableSoundPopup { get; set; }

        [Parameter("Send Email / Push Bridge", Group = "3. Telemetry & Notifications", DefaultValue = false)]
        public bool EnableEmail { get; set; }

        [Parameter("Destination Email", Group = "3. Telemetry & Notifications", DefaultValue = "")]
        public string EmailAddress { get; set; }

        #endregion

        private const string BotLabel = "NYQuantSweep_M15";
        private AverageTrueRange _m15Atr;
        private SimpleMovingAverage _volSma;
        private Bars _h4Bars;
        private Bars _d1Bars;
        private ExponentialMovingAverage _h4Ema50;

        // Intraday Session State (Anchored to New York Date)
        private DateTime _currentNyDate = DateTime.MinValue;
        private double _pmHigh = double.MinValue;
        private double _pmLow = double.MaxValue;
        private double _pdHigh = double.MinValue;
        private double _pdLow = double.MaxValue;
        private DateTime _pmStartUtc;
        private DateTime _nyOpenUtc;
        private DateTime _nyCloseWatchUtc;
        private bool _shortTriggeredToday;
        private bool _longTriggeredToday;

        // Cumulative Session VWAP State
        private double _cumTypVol;
        private double _cumVol;
        private double _currentVwap;

        protected override void OnStart()
        {
            if (TimeFrame != TimeFrame.Minute15)
            {
                Print("CRITICAL: Attach NYOpenQuantSweepPro to an M15 chart for accurate execution.");
            }

            _m15Atr = Indicators.AverageTrueRange(AtrPeriod, MovingAverageType.Exponential);
            _volSma = Indicators.SimpleMovingAverage(Bars.TickVolumes, 20);
            _h4Bars = MarketData.GetBars(TimeFrame.Hour4);
            _d1Bars = MarketData.GetBars(TimeFrame.Daily);
            _h4Ema50 = Indicators.ExponentialMovingAverage(_h4Bars.ClosePrices, 50);

            InitializeOrRollSession(Server.TimeInUtc);
            UpdateHud("STANDBY - MONITORING LIQUIDITY POOLS");
        }

        protected override void OnTick()
        {
            if (OperatingMode == enumBotMode.AutoExecute_WithBreakEven)
            {
                ManageOpenPositionsBreakEven();
            }
        }

        protected override void OnBar()
        {
            // Index 1 is the just-closed M15 bar
            int closedIdx = Bars.Count - 2;
            if (closedIdx < 30) return;

            DateTime closedBarUtc = Bars.OpenTimes[closedIdx];
            InitializeOrRollSession(closedBarUtc);

            // Update Session VWAP on every closed M15 bar
            double typicalPrice = (Bars.HighPrices[closedIdx] + Bars.LowPrices[closedIdx] + Bars.ClosePrices[closedIdx]) / 3.0;
            double tickVol = Bars.TickVolumes[closedIdx];
            _cumTypVol += typicalPrice * tickVol;
            _cumVol += tickVol;
            if (_cumVol > 0) _currentVwap = _cumTypVol / _cumVol;

            // Phase 1: Accumulate London / Pre-Market High & Low (03:00 NY to 09:30 NY)
            if (closedBarUtc >= _pmStartUtc && closedBarUtc < _nyOpenUtc)
            {
                if (Bars.HighPrices[closedIdx] > _pmHigh) _pmHigh = Bars.HighPrices[closedIdx];
                if (Bars.LowPrices[closedIdx] < _pmLow) _pmLow = Bars.LowPrices[closedIdx];
                DrawInstitutionalLevels();
                UpdateHud("ACCUMULATING PRE-MARKET RANGE");
                return;
            }

            // Phase 2: NY Open Killzone Evaluation (09:30 NY to 12:00 NY)
            if (closedBarUtc >= _nyOpenUtc && closedBarUtc < _nyCloseWatchUtc && _pmHigh > _pmLow)
            {
                DrawInstitutionalLevels();
                EvaluateQuantSweep(closedIdx);
            }
            else if (closedBarUtc >= _nyCloseWatchUtc)
            {
                UpdateHud("NY KILLZONE COMPLETE - LOCKED");
            }
        }

        private void EvaluateQuantSweep(int idx)
        {
            double open = Bars.OpenPrices[idx];
            double high = Bars.HighPrices[idx];
            double low = Bars.LowPrices[idx];
            double close = Bars.ClosePrices[idx];
            double range = high - low;
            double atr = _m15Atr.Result[idx];

            if (range <= double.Epsilon || atr <= double.Epsilon) return;

            // 1. ATR Volatility Guard: Reject freak macro whipsaw candles (> MaxCandleAtrMult * ATR)
            if (range > atr * MaxCandleAtrMult)
            {
                Print($"[Filter] Bar rejected: Range ({range:F2}) exceeds Max ATR threshold ({atr * MaxCandleAtrMult:F2}).");
                return;
            }

            // 2. Tick Volume Displacement Check
            bool hasVolumeSpike = !RequireVolumeDisplacement || (Bars.TickVolumes[idx] >= _volSma.Result[idx] * 1.20);
            if (!hasVolumeSpike) return;

            // 3. Higher-Timeframe (H4) Structural Trend Check
            int h4Idx = _h4Bars.OpenTimes.GetIndexByTime(Bars.OpenTimes[idx]);
            bool h4Bullish = h4Idx >= 0 && _h4Bars.ClosePrices[h4Idx] > _h4Ema50.Result[h4Idx];
            bool h4Bearish = h4Idx >= 0 && _h4Bars.ClosePrices[h4Idx] < _h4Ema50.Result[h4Idx];

            double minSweepDist = atr * MinSweepAtrMult;
            double slBuffer = atr * SlBufferAtrMult;

            // --- BEARISH SWEEP EVALUATION (PMH or PDH Raid + Close Inside) ---
            if (!_shortTriggeredToday)
            {
                bool sweptPmHigh = (high >= _pmHigh + minSweepDist) && (close < _pmHigh);
                bool sweptPdHigh = (_pdHigh > 0) && (high >= _pdHigh + minSweepDist) && (close < _pdHigh);

                if (sweptPmHigh || sweptPdHigh)
                {
                    double upperWick = high - Math.Max(open, close);
                    double wickPct = (upperWick / range) * 100.0;
                    bool vwapOk = !RequireVwapFilter || (close <= _currentVwap || open > _currentVwap);

                    if (wickPct >= MinWickPercent && vwapOk)
                    {
                        _shortTriggeredToday = true;
                        string poolTag = (sweptPmHigh && sweptPdHigh) ? "DUAL SWEEP (PMH+PDH)" : (sweptPdHigh ? "PDH SWEEP" : "PMH SWEEP");
                        string grade = h4Bearish ? "A+ (With H4 Trend)" : "B (Counter-H4 Scalp)";

                        double sl = high + slBuffer;
                        double stopPts = sl - close;
                        double tp1 = close - (stopPts * 1.5);
                        double tp2 = Math.Max(_pmLow, close - (stopPts * 3.0));

                        Chart.DrawIcon($"Short_{idx}", ChartIconType.DownArrow, Bars.OpenTimes[idx], high + (atr * 0.15), Color.Crimson);
                        DispatchSignalAndExecute(TradeType.Sell, poolTag, grade, close, sl, tp1, tp2, stopPts);
                    }
                }
            }

            // --- BULLISH SWEEP EVALUATION (PML or PDL Raid + Close Inside) ---
            if (!_longTriggeredToday)
            {
                bool sweptPmLow = (low <= _pmLow - minSweepDist) && (close > _pmLow);
                bool sweptPdLow = (_pdLow > 0 && _pdLow < double.MaxValue) && (low <= _pdLow - minSweepDist) && (close > _pdLow);

                if (sweptPmLow || sweptPdLow)
                {
                    double lowerWick = Math.Min(open, close) - low;
                    double wickPct = (lowerWick / range) * 100.0;
                    bool vwapOk = !RequireVwapFilter || (close >= _currentVwap || open < _currentVwap);

                    if (wickPct >= MinWickPercent && vwapOk)
                    {
                        _longTriggeredToday = true;
                        string poolTag = (sweptPmLow && sweptPdLow) ? "DUAL SWEEP (PML+PDL)" : (sweptPdLow ? "PDL SWEEP" : "PML SWEEP");
                        string grade = h4Bullish ? "A+ (With H4 Trend)" : "B (Counter-H4 Scalp)";

                        double sl = low - slBuffer;
                        double stopPts = close - sl;
                        double tp1 = close + (stopPts * 1.5);
                        double tp2 = Math.Min(_pmHigh, close + (stopPts * 3.0));

                        Chart.DrawIcon($"Long_{idx}", ChartIconType.UpArrow, Bars.OpenTimes[idx], low - (atr * 0.15), Color.LimeGreen);
                        DispatchSignalAndExecute(TradeType.Buy, poolTag, grade, close, sl, tp1, tp2, stopPts);
                    }
                }
            }
        }

        private void DispatchSignalAndExecute(TradeType dir, string poolTag, string grade, double entry, double sl, double tp1, double tp2, double stopPts)
        {
            double currentSpreadPts = Symbol.Spread;
            double volumeUnits = CalculateHybridVolumeUnits(stopPts, out double riskUsd, out double lotsDisplay);

            string payload = $"[{Symbol.Name} M15 {dir.ToString().ToUpper()}] {poolTag} | Grade: {grade}\n" +
                             $"Entry: {entry:F2} | SL: {sl:F2} ({stopPts:F1} pts) | TP1(1.5R): {tp1:F2} | TP2: {tp2:F2}\n" +
                             $"Size: {lotsDisplay:F2} Lots (Risk: ${riskUsd:F0}) | VWAP: {_currentVwap:F2} | Spread: {currentSpreadPts:F2}";

            Print(payload);
            UpdateHud($"SIGNAL FIRED: {dir} ({poolTag})");

            if (EnableSoundPopup)
            {
                Notifications.PlaySound(SoundType.Announcement);
                MessageBox.Show(payload, $"Institutional Sweep Alert: {Symbol.Name}", MessageBoxButton.OK, MessageBoxImage.Information);
            }

            if (EnableEmail && !string.IsNullOrWhiteSpace(EmailAddress))
            {
                Notifications.SendEmail(EmailAddress, EmailAddress, $"[{Symbol.Name} {grade}] {dir} {poolTag}", payload);
            }

            if (OperatingMode == enumBotMode.AutoExecute_WithBreakEven)
            {
                if (currentSpreadPts > MaxSpreadPoints)
                {
                    Print($"[Execution Aborted] Current spread ({currentSpreadPts:F2}) > Max Allowed ({MaxSpreadPoints:F2}).");
                    return;
                }

                double stopPips = stopPts / Symbol.PipSize;
                double tpPips = Math.Abs(tp2 - entry) / Symbol.PipSize;
                ExecuteMarketOrder(dir, SymbolName, volumeUnits, BotLabel, stopPips, tpPips, $"{poolTag}_{grade}");
            }
        }

        /// <summary>
        /// Hybrid Linear + Exponential Position Sizing:
        /// Protects principal on drawdown and compounds aggressively strictly on accrued floating/banked series gains.
        /// </summary>
        private double CalculateHybridVolumeUnits(double stopDistancePrice, out double riskUsd, out double standardLots)
        {
            double equity = Account.Equity;
            double accruedProfit = Math.Max(0.0, equity - SeriesBaseCapital);

            double baseRiskUsd = SeriesBaseCapital * (BaseRiskPercent / 100.0);
            double bonusExponentialRiskUsd = accruedProfit * ((BaseRiskPercent * ExponentialProfitMultiplier) / 100.0);

            // If below starting series capital, scale linearly on current equity; if in profit, add exponential profit tranche
            riskUsd = (equity < SeriesBaseCapital)
                ? equity * (BaseRiskPercent / 100.0)
                : baseRiskUsd + bonusExponentialRiskUsd;

            double ticksInStop = stopDistancePrice / Symbol.TickSize;
            double riskPerUnit = ticksInStop * Symbol.TickValue;

            double rawUnits = (riskPerUnit > 0) ? (riskUsd / riskPerUnit) : Symbol.VolumeInUnitsMin;
            double normalizedUnits = Symbol.NormalizeVolumeInUnits(rawUnits, RoundingMode.Down);
            normalizedUnits = Math.Max(Symbol.VolumeInUnitsMin, Math.Min(Symbol.VolumeInUnitsMax, normalizedUnits));

            standardLots = Symbol.VolumeInUnitsToQuantity(normalizedUnits);
            return normalizedUnits;
        }

        private void ManageOpenPositionsBreakEven()
        {
            var positions = Positions.FindAll(BotLabel, SymbolName);
            foreach (var pos in positions)
            {
                if (!pos.StopLoss.HasValue) continue;

                if (pos.TradeType == TradeType.Buy && pos.StopLoss.Value < pos.EntryPrice)
                {
                    double initialRisk = pos.EntryPrice - pos.StopLoss.Value;
                    if (Symbol.Bid >= pos.EntryPrice + (initialRisk * BreakEvenTriggerR))
                    {
                        double newSl = pos.EntryPrice + (Symbol.Spread * 1.5);
                        ModifyPosition(pos, newSl, pos.TakeProfit);
                        Print($"[Auto-BE] Moved LONG #{pos.Id} Stop-Loss to Break-Even + Buffer ({newSl:F2}).");
                    }
                }
                else if (pos.TradeType == TradeType.Sell && pos.StopLoss.Value > pos.EntryPrice)
                {
                    double initialRisk = pos.StopLoss.Value - pos.EntryPrice;
                    if (Symbol.Ask <= pos.EntryPrice - (initialRisk * BreakEvenTriggerR))
                    {
                        double newSl = pos.EntryPrice - (Symbol.Spread * 1.5);
                        ModifyPosition(pos, newSl, pos.TakeProfit);
                        Print($"[Auto-BE] Moved SHORT #{pos.Id} Stop-Loss to Break-Even + Buffer ({newSl:F2}).");
                    }
                }
            }
        }

        /// <summary>
        /// Automatically computes New York (EST/EDT) session boundaries in UTC for any date,
        /// handling US DST rules (2nd Sunday in March to 1st Sunday in November) without OS dependencies.
        /// </summary>
        private void InitializeOrRollSession(DateTime currentUtc)
        {
            int nyOffsetHours = GetNewYorkUtcOffsetHours(currentUtc);
            DateTime nyNow = currentUtc.AddHours(nyOffsetHours);

            if (nyNow.Date == _currentNyDate) return;

            _currentNyDate = nyNow.Date;
            _pmHigh = double.MinValue;
            _pmLow = double.MaxValue;
            _shortTriggeredToday = false;
            _longTriggeredToday = false;
            _cumTypVol = 0;
            _cumVol = 0;

            // 03:00 NY (London Open) -> 09:30 NY (NY Cash Open) -> 12:00 NY (Killzone End)
            _pmStartUtc = _currentNyDate.AddHours(3).AddHours(-nyOffsetHours);
            _nyOpenUtc = _currentNyDate.AddHours(9).AddMinutes(30).AddHours(-nyOffsetHours);
            _nyCloseWatchUtc = _currentNyDate.AddHours(12).AddHours(-nyOffsetHours);

            // Fetch Previous Day High/Low (PDH/PDL)
            if (_d1Bars.Count >= 2)
            {
                _pdHigh = _d1Bars.HighPrices[_d1Bars.Count - 2];
                _pdLow = _d1Bars.LowPrices[_d1Bars.Count - 2];
            }
        }

        private int GetNewYorkUtcOffsetHours(DateTime utcTime)
        {
            int year = utcTime.Year;
            // 2nd Sunday in March at 07:00 UTC (02:00 EST)
            DateTime marchFirst = new DateTime(year, 3, 1, 7, 0, 0, DateTimeKind.Utc);
            int daysToSecondSun = ((int)DayOfWeek.Sunday - (int)marchFirst.DayOfWeek + 7) % 7 + 7;
            DateTime dstStartUtc = marchFirst.AddDays(daysToSecondSun);

            // 1st Sunday in November at 06:00 UTC (02:00 EDT)
            DateTime novFirst = new DateTime(year, 11, 1, 6, 0, 0, DateTimeKind.Utc);
            int daysToFirstSun = ((int)DayOfWeek.Sunday - (int)novFirst.DayOfWeek + 7) % 7;
            DateTime dstEndUtc = novFirst.AddDays(daysToFirstSun);

            return (utcTime >= dstStartUtc && utcTime < dstEndUtc) ? -4 : -5;
        }

        private void DrawInstitutionalLevels()
        {
            string tag = _currentNyDate.ToString("yyyyMMdd");
            if (_pmHigh > _pmLow)
            {
                var box = Chart.DrawRectangle($"PMBox_{tag}", _pmStartUtc, _pmHigh, _nyOpenUtc, _pmLow, Color.FromArgb(28, 65, 105, 225));
                box.IsFilled = true;

                Chart.DrawTrendLine($"PMH_{tag}", _nyOpenUtc, _pmHigh, _nyCloseWatchUtc, _pmHigh, Color.DodgerBlue, 2, LineStyle.DotsRare);
                Chart.DrawTrendLine($"PML_{tag}", _nyOpenUtc, _pmLow, _nyCloseWatchUtc, _pmLow, Color.DodgerBlue, 2, LineStyle.DotsRare);
            }

            if (_pdHigh > 0 && _pdLow < double.MaxValue)
            {
                Chart.DrawTrendLine($"PDH_{tag}", _pmStartUtc, _pdHigh, _nyCloseWatchUtc, _pdHigh, Color.Goldenrod, 1, LineStyle.Solid);
                Chart.DrawTrendLine($"PDL_{tag}", _pmStartUtc, _pdLow, _nyCloseWatchUtc, _pdLow, Color.Goldenrod, 1, LineStyle.Solid);
            }
        }

        private void UpdateHud(string stateText)
        {
            double atr = (_m15Atr != null && _m15Atr.Result.Count > 0) ? _m15Atr.Result.LastValue : 0.0;
            string hud = $"[NY OPEN QUANT ENGINE | {Symbol.Name} M15]\n" +
                         $"State: {stateText} | Mode: {OperatingMode}\n" +
                         $"NY Open (UTC): {_nyOpenUtc:HH:mm} | M15 ATR(14): {atr:F2} pts | Spread: {Symbol.Spread:F2}\n" +
                         $"PMH: {(_pmHigh > 0 ? _pmHigh.ToString("F2") : "--")} | PML: {(_pmLow < double.MaxValue ? _pmLow.ToString("F2") : "--")}\n" +
                         $"PDH: {(_pdHigh > 0 ? _pdHigh.ToString("F2") : "--")} | PDL: {(_pdLow < double.MaxValue ? _pdLow.ToString("F2") : "--")} | VWAP: {_currentVwap:F2}";

            Chart.DrawStaticText("QuantHUD", hud, VerticalAlignment.Top, HorizontalAlignment.Left, Color.LightSteelBlue);
        }
    }
}

Re: Scalping US100 and US500

Posted: Wed Oct 28, 2026 5:50 pm
by PTScalper
2. MetaTrader 5 (MQL5) Pro Indicator with Mobile Execution Telemetry

This MQL5 version upgrades the standard indicator with automatic US EDT/EST timezone synchronization (using TimeTradeServer() and TimeGMT()), native iATR(M15) and iMA(H4) handles, PDH/PDL + PMH/PML confluence detection, Session VWAP, and an embedded Hybrid Lot-Size Calculator so your MT5 mobile push notification gives you the exact lot size to punch in from your phone.

Create NYOpenQuantSweepPro.mq5 in MetaEditor 5:

Code: Select all

//+------------------------------------------------------------------+
//|                                         NYOpenQuantSweepPro.mq5  |
//|     Institutional M15 NY Open Liquidity Sweep & Telemetry Engine |
//+------------------------------------------------------------------+
#property copyright "Quant Index Scalping Architecture"
#property version   "2.00"
#property indicator_chart_window
#property indicator_buffers 3
#property indicator_plots   3

// Plot 1: Bearish Sweep Signal
#property indicator_label1  "Bearish Liquidity Sweep"
#property indicator_type1   DRAW_ARROW
#property indicator_color1  clrCrimson
#property indicator_width1  2

// Plot 2: Bullish Liquidity Sweep
#property indicator_label2  "Bullish Liquidity Sweep"
#property indicator_type2   DRAW_ARROW
#property indicator_color2  clrLimeGreen
#property indicator_width2  2

// Plot 3: Intraday Session VWAP
#property indicator_label3  "Session VWAP"
#property indicator_type3   DRAW_LINE
#property indicator_color3  clrGold
#property indicator_style3  STYLE_SOLID
#property indicator_width3  1

//--- Input Parameters
input group "1. Hybrid Money Management (For Mobile Alert Lot Calc)"
input double   InpSeriesCapital     = 5000.0;  // Series Base Risk Capital ($)
input double   InpBaseRiskPct       = 1.0;     // Base Risk Per Setup (%)
input double   InpExpProfitMult     = 1.5;     // Accrued Profit Scaling Multiplier

input group "2. Adaptive ATR(14) & Order-Flow Filters"
input int      InpAtrPeriod         = 14;      // M15 ATR Period
input double   InpMinSweepAtr       = 0.10;    // Min Sweep Penetration (x M15 ATR)
input double   InpMaxBarAtr         = 2.80;    // Max Candle Range Filter (x M15 ATR)
input double   InpSlBufferAtr       = 0.20;    // Stop-Loss Buffer (x M15 ATR)
input double   InpMinWickPct        = 32.0;    // Min Wick Rejection (% of Candle)
input bool     InpRequireVolSpike   = true;    // Require Tick Volume > 1.2x SMA(20)
input bool     InpRequireVwap       = true;    // Require Close Inside Session VWAP

input group "3. Notifications & Visuals"
input bool     InpShowHud           = true;    // Render On-Chart Telemetry HUD
input bool     InpDesktopAlert      = true;    // Desktop Alert Popup
input bool     InpMobilePush        = true;    // Send MT5 Mobile Push (With Lot Size)

//--- Indicator Buffers
double BearSweepBuffer[];
double BullSweepBuffer[];
double VwapBuffer[];

//--- Indicator Handles & Runtime State
int      g_atrHandle    = INVALID_HANDLE;
int      g_h4EmaHandle  = INVALID_HANDLE;
datetime g_lastBarTime  = 0;
datetime g_lastBearDay  = 0;
datetime g_lastBullDay  = 0;

//+------------------------------------------------------------------+
//| Initialization                                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   SetIndexBuffer(0, BearSweepBuffer, INDICATOR_DATA);
   SetIndexBuffer(1, BullSweepBuffer, INDICATOR_DATA);
   SetIndexBuffer(2, VwapBuffer,      INDICATOR_DATA);
   
   PlotIndexSetInteger(0, PLOT_ARROW, 234);
   PlotIndexSetInteger(1, PLOT_ARROW, 233);
   
   ArraySetAsSeries(BearSweepBuffer, true);
   ArraySetAsSeries(BullSweepBuffer, true);
   ArraySetAsSeries(VwapBuffer,      true);
   
   PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, 0.0);
   PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, 0.0);
   PlotIndexSetDouble(2, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   
   g_atrHandle   = iATR(_Symbol, PERIOD_M15, InpAtrPeriod);
   g_h4EmaHandle = iMA(_Symbol, PERIOD_H4, 50, 0, MODE_EMA, PRICE_CLOSE);
   
   if(g_atrHandle == INVALID_HANDLE || g_h4EmaHandle == INVALID_HANDLE)
   {
      Print("CRITICAL: Failed to initialize M15 ATR or H4 EMA handles.");
      return(INIT_FAILED);
   }
   
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Deinitialization                                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   if(g_atrHandle   != INVALID_HANDLE) IndicatorRelease(g_atrHandle);
   if(g_h4EmaHandle != INVALID_HANDLE) IndicatorRelease(g_h4EmaHandle);
   ObjectsDeleteAll(0, "NYPro_");
   Comment("");
}

//+------------------------------------------------------------------+
//| Compute US New York UTC Offset (-4 EDT / -5 EST) automatically   |
//+------------------------------------------------------------------+
int GetNyUtcOffsetSeconds(datetime utcTime)
{
   MqlDateTime dt;
   TimeToStruct(utcTime, dt);
   
   // 2nd Sunday in March at 07:00 UTC
   MqlDateTime mar1 = {dt.year, 3, 1, 7, 0, 0, 0, 0};
   datetime tMar1 = StructToTime(mar1);
   TimeToStruct(tMar1, mar1);
   int daysTo2ndSun = (7 - mar1.day_of_week) % 7 + 7;
   datetime dstStart = tMar1 + (daysTo2ndSun * 86400);
   
   // 1st Sunday in November at 06:00 UTC
   MqlDateTime nov1 = {dt.year, 11, 1, 6, 0, 0, 0, 0};
   datetime tNov1 = StructToTime(nov1);
   TimeToStruct(tNov1, nov1);
   int daysTo1stSun = (7 - nov1.day_of_week) % 7;
   datetime dstEnd = tNov1 + (daysTo1stSun * 86400);
   
   return (utcTime >= dstStart && utcTime < dstEnd) ? (-4 * 3600) : (-5 * 3600);
}

//+------------------------------------------------------------------+
//| Calculate Broker Server Offset from UTC in Seconds               |
//+------------------------------------------------------------------+
int GetBrokerServerUtcOffsetSeconds()
{
   datetime srv = TimeTradeServer();
   datetime gmt = TimeGMT();
   int diff = (int)MathRound((double)(srv - gmt) / 1800.0) * 1800;
   return diff;
}

//+------------------------------------------------------------------+
//| Hybrid Linear + Exponential Lot Size Calculator for Mobile Alert |
//+------------------------------------------------------------------+
double CalculateHybridLots(double stopPointsPrice, double &outRiskUsd)
{
   double equity = AccountInfoDouble(ACCOUNT_EQUITY);
   if(equity <= 0) equity = InpSeriesCapital;
   
   double accruedProfit = MathMax(0.0, equity - InpSeriesCapital);
   double baseRisk      = InpSeriesCapital * (InpBaseRiskPct / 100.0);
   double expBonusRisk  = accruedProfit * ((InpBaseRiskPct * InpExpProfitMult) / 100.0);
   
   outRiskUsd = (equity < InpSeriesCapital) ? (equity * (InpBaseRiskPct / 100.0)) : (baseRisk + expBonusRisk);
   
   double tickSize  = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
   double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
   double lotStep   = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
   double minLot    = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double maxLot    = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   
   if(tickSize <= 0 || tickValue <= 0 || stopPointsPrice <= 0) return minLot;
   
   double lossPerLot = (stopPointsPrice / tickSize) * tickValue;
   double rawLots    = (lossPerLot > 0) ? (outRiskUsd / lossPerLot) : minLot;
   double steppedLot = MathFloor(rawLots / lotStep) * lotStep;
   
   return MathMax(minLot, MathMin(maxLot, steppedLot));
}

//+------------------------------------------------------------------+
//| Main Calculation Loop                                            |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
{
   if(rates_total < 150) return(0);
   
   ArraySetAsSeries(time,        true);
   ArraySetAsSeries(open,        true);
   ArraySetAsSeries(high,        true);
   ArraySetAsSeries(low,         true);
   ArraySetAsSeries(close,       true);
   ArraySetAsSeries(tick_volume, true);
   
   int serverToUtcSec = GetBrokerServerUtcOffsetSeconds();
   
   // Pre-calculate Session VWAP across visible bars
   int vwapLimit = (prev_calculated == 0) ? MathMin(rates_total - 2, 1200) : 2;
   for(int v = vwapLimit; v >= 0; v--)
   {
      MqlDateTime dtCur, dtPrev;
      TimeToStruct(time[v], dtCur);
      TimeToStruct(time[v + 1], dtPrev);
      
      double typPrice = (high[v] + low[v] + close[v]) / 3.0;
      double tv       = (double)tick_volume[v];
      
      // Accumulate from midnight broker server time
      double sumPV = 0.0, sumV = 0.0;
      for(int k = v; k < rates_total; k++)
      {
         MqlDateTime dtk;
         TimeToStruct(time[k], dtk);
         if(dtk.day != dtCur.day || dtk.mon != dtCur.mon) break;
         double tp = (high[k] + low[k] + close[k]) / 3.0;
         sumPV += tp * (double)tick_volume[k];
         sumV  += (double)tick_volume[k];
      }
      VwapBuffer[v] = (sumV > 0) ? (sumPV / sumV) : close[v];
   }
   
   // Only evaluate closed M15 signals when a new bar opens
   bool isNewBar = (time[0] != g_lastBarTime);
   if(!isNewBar && prev_calculated > 0) return(rates_total);
   g_lastBarTime = time[0];
   
   int scanStart = (prev_calculated == 0) ? MathMin(rates_total - 30, 1000) : 1;
   
   for(int i = scanStart; i >= 1; i--)
   {
      BearSweepBuffer[i] = 0.0;
      BullSweepBuffer[i] = 0.0;
      
      datetime barUtc = time[i] - serverToUtcSec;
      int nyOffsetSec = GetNyUtcOffsetSeconds(barUtc);
      
      // Anchor midnight in New York time, then convert NY 03:00, 09:30, and 12:00 back to Broker Server Time
      datetime barNy  = barUtc + nyOffsetSec;
      MqlDateTime dtNy;
      TimeToStruct(barNy, dtNy);
      dtNy.hour = 0; dtNy.min = 0; dtNy.sec = 0;
      datetime nyMidnightInNy = StructToTime(dtNy);
      
      datetime pmStartSrv = (nyMidnightInNy + 3 * 3600) - nyOffsetSec + serverToUtcSec;
      datetime nyOpenSrv  = (nyMidnightInNy + 9 * 3600 + 1800) - nyOffsetSec + serverToUtcSec;
      datetime nyEndSrv   = (nyMidnightInNy + 12 * 3600) - nyOffsetSec + serverToUtcSec;
      
      if(time[i] < nyOpenSrv || time[i] >= nyEndSrv) continue;
      
      // 1. Gather Pre-Market High & Low (03:00 NY - 09:30 NY)
      double pmHigh = -DBL_MAX, pmLow = DBL_MAX;
      bool hasPm = false;
      for(int j = i + 1; j < rates_total; j++)
      {
         if(time[j] < pmStartSrv) break;
         if(time[j] >= pmStartSrv && time[j] < nyOpenSrv)
         {
            if(high[j] > pmHigh) pmHigh = high[j];
            if(low[j]  < pmLow)  pmLow  = low[j];
            hasPm = true;
         }
      }
      if(!hasPm) continue;
      
      // 2. Fetch Previous Day High & Low (PDH / PDL)
      double pdHighArr[1], pdLowArr[1];
      double pdHigh = (CopyHigh(_Symbol, PERIOD_D1, time[i] - 86400, 1, pdHighArr) > 0) ? pdHighArr[0] : -DBL_MAX;
      double pdLow  = (CopyLow(_Symbol,  PERIOD_D1, time[i] - 86400, 1, pdLowArr)  > 0) ? pdLowArr[0]  :  DBL_MAX;
      
      // Render Session Levels on Chart
      string dayKey = TimeToString(nyMidnightInNy, TIME_DATE);
      DrawLevelLine("NYPro_PMH_" + dayKey, pmStartSrv, pmHigh, nyEndSrv, clrDodgerBlue, STYLE_DOT, 2);
      DrawLevelLine("NYPro_PML_" + dayKey, pmStartSrv, pmLow,  nyEndSrv, clrDodgerBlue, STYLE_DOT, 2);
      if(pdHigh > 0)        DrawLevelLine("NYPro_PDH_" + dayKey, pmStartSrv, pdHigh, nyEndSrv, clrGoldenrod, STYLE_SOLID, 1);
      if(pdLow < DBL_MAX)   DrawLevelLine("NYPro_PDL_" + dayKey, pmStartSrv, pdLow,  nyEndSrv, clrGoldenrod, STYLE_SOLID, 1);
      
      // 3. Copy M15 ATR(14) and H4 EMA(50)
      double atrBuf[1], h4EmaBuf[1], h4CloseBuf[1];
      if(CopyBuffer(g_atrHandle, 0, time[i], 1, atrBuf) <= 0) continue;
      double atr = atrBuf[0];
      
      bool h4Bull = false, h4Bear = false;
      if(CopyBuffer(g_h4EmaHandle, 0, time[i], 1, h4EmaBuf) > 0 &&
         CopyClose(_Symbol, PERIOD_H4, time[i], 1, h4CloseBuf) > 0)
      {
         h4Bull = (h4CloseBuf[0] > h4EmaBuf[0]);
         h4Bear = (h4CloseBuf[0] < h4EmaBuf[0]);
      }
      
      // 4. Candle Range & Volume Displacement Checks
      double rng = high[i] - low[i];
      if(rng <= 0.0 || rng > atr * InpMaxBarAtr) continue;
      
      double volSum = 0.0;
      for(int v = 1; v <= 20; v++) volSum += (double)tick_volume[i + v];
      double volAvg = volSum / 20.0;
      if(InpRequireVolSpike && (double)tick_volume[i] < volAvg * 1.20) continue;
      
      double minSweep = atr * InpMinSweepAtr;
      double slBuf    = atr * InpSlBufferAtr;
      
      // --- BEARISH SWEEP CHECK ---
      bool sweptPmH = (high[i] >= pmHigh + minSweep && close[i] < pmHigh);
      bool sweptPdH = (pdHigh > 0 && high[i] >= pdHigh + minSweep && close[i] < pdHigh);
      
      if(sweptPmH || sweptPdH)
      {
         double uWick    = high[i] - MathMax(open[i], close[i]);
         double uWickPct = (uWick / rng) * 100.0;
         bool   vwapOk   = !InpRequireVwap || (close[i] <= VwapBuffer[i] || open[i] > VwapBuffer[i]);
         
         if(uWickPct >= InpMinWickPct && vwapOk)
         {
            BearSweepBuffer[i] = high[i] + (atr * 0.20);
            if(i == 1 && g_lastBearDay != nyMidnightInNy)
            {
               g_lastBearDay = nyMidnightInNy;
               string tag    = (sweptPmH && sweptPdH) ? "DUAL(PMH+PDH)" : (sweptPdH ? "PDH-SWEEP" : "PMH-SWEEP");
               string grade  = h4Bear ? "A+ [H4 Aligned]" : "B [Counter-H4]";
               double sl     = high[i] + slBuf;
               double riskPt = sl - close[i];
               double tp1    = close[i] - (riskPt * 1.5);
               double tp2    = MathMax(pmLow, close[i] - (riskPt * 3.0));
               
               DispatchMobileTelemetry("SHORT", tag, grade, close[i], sl, tp1, tp2, riskPt);
            }
         }
      }
      
      // --- BULLISH SWEEP CHECK ---
      bool sweptPmL = (low[i] <= pmLow - minSweep && close[i] > pmLow);
      bool sweptPdL = (pdLow < DBL_MAX && low[i] <= pdLow - minSweep && close[i] > pdLow);
      
      if(sweptPmL || sweptPdL)
      {
         double lWick    = MathMin(open[i], close[i]) - low[i];
         double lWickPct = (lWick / rng) * 100.0;
         bool   vwapOk   = !InpRequireVwap || (close[i] >= VwapBuffer[i] || open[i] < VwapBuffer[i]);
         
         if(lWickPct >= InpMinWickPct && vwapOk)
         {
            BullSweepBuffer[i] = low[i] - (atr * 0.20);
            if(i == 1 && g_lastBullDay != nyMidnightInNy)
            {
               g_lastBullDay = nyMidnightInNy;
               string tag    = (sweptPmL && sweptPdL) ? "DUAL(PML+PDL)" : (sweptPdL ? "PDL-SWEEP" : "PML-SWEEP");
               string grade  = h4Bull ? "A+ [H4 Aligned]" : "B [Counter-H4]";
               double sl     = low[i] - slBuf;
               double riskPt = close[i] - sl;
               double tp1    = close[i] + (riskPt * 1.5);
               double tp2    = MathMin(pmHigh, close[i] + (riskPt * 3.0));
               
               DispatchMobileTelemetry("LONG", tag, grade, close[i], sl, tp1, tp2, riskPt);
            }
         }
      }
      
      if(i == 1 && InpShowHud)
      {
         Comment(StringFormat("[NY QUANT SWEEP PRO | %s M15]\nNY Open Server Time: %s | M15 ATR(14): %.2f\nPMH: %.2f | PML: %.2f | PDH: %.2f | PDL: %.2f\nSession VWAP: %.2f | H4 Bias: %s",
                 _Symbol, TimeToString(nyOpenSrv, TIME_MINUTES), atr, pmHigh, pmLow, pdHigh, pdLow, VwapBuffer[1],
                 h4Bull ? "BULLISH (>EMA50)" : "BEARISH (<EMA50)"));
      }
   }
   
   return(rates_total);
}

//+------------------------------------------------------------------+
//| Helper: Draw Non-Repainting Session Liquidity Lines              |
//+------------------------------------------------------------------+
void DrawLevelLine(string name, datetime t1, double price, datetime t2, color clr, ENUM_LINE_STYLE style, int width)
{
   if(ObjectFind(0, name) < 0)
   {
      ObjectCreate(0, name, OBJ_TREND, 0, t1, price, t2, price);
      ObjectSetInteger(0, name, OBJ_COLOR, clr);
      ObjectSetInteger(0, name, OBJ_STYLE, style);
      ObjectSetInteger(0, name, OBJ_WIDTH, width);
      ObjectSetInteger(0, name, OBJ_RAY_RIGHT, false);
   }
}

//+------------------------------------------------------------------+
//| Format & Send Actionable Mobile Push Notification                |
//+------------------------------------------------------------------+
void DispatchMobileTelemetry(string dir, string poolTag, string grade, double entry, double sl, double tp1, double tp2, double stopPts)
{
   double riskUsd = 0.0;
   double lots    = CalculateHybridLots(stopPts, riskUsd);
   
   string msg = StringFormat("%s M15 %s [%s | %s] Lot: %.2f ($%.0f) | Ent: %.2f | SL: %.2f | TP1: %.2f | TP2: %.2f",
                             _Symbol, dir, poolTag, grade, lots, riskUsd, entry, sl, tp1, tp2);
                             
   if(InpDesktopAlert) Alert(msg);
   if(InpMobilePush)   SendNotification(msg);
}
//+------------------------------------------------------------------+

Re: Scalping US100 and US500

Posted: Wed Oct 28, 2026 5:53 pm
by PTScalper
Running four separate chart instances eats unnecessary workspace and forces you to manage duplicate parameter sets. By shifting to an Event-Driven Multi-Symbol Scanner Architecture, you can attach a single instance to any chart and monitor US500, US100, DE40, and XAUUSD simultaneously in the background.

Re: Scalping US100 and US500

Posted: Wed Oct 28, 2026 5:55 pm
by PTScalper
Key Architectural Highlights of the Multi-Symbol Engine

Instrument-Specific Session Profiles:

Unlike US indices, DE40 (DAX) experiences its primary institutional liquidity raids during both the Frankfurt/Xetra Cash Open (08:00 – 10:30 CET) and the New York Cash Open. Meanwhile, XAUUSD (Gold) sweeps Asian highs/lows at the London Open and sweeps London extremes during the NY/COMEX Open (08:20 – 11:30 NY). The scanner assigns custom session rules per asset automatically.

US500 & US100: 03:00 – 09:30 NY (London / Pre-Market) + 09:30 – 12:00 NY (NY Cash Open) + PMH / PML + PDH / PDL

DE40 :
1) 01:00 – 08:00 CET (Asian Range)
2) 08:00 – 15:30 CET (EU Morning)
3) 08:00 – 10:30 CET (Xetra Open)
4) 15:30 – 17:30 CET (US Overlap)
AND Asian H/L, EU H/L + PDH / PDL

Re: Scalping US100 and US500

Posted: Wed Oct 28, 2026 5:55 pm
by PTScalper
Broker Symbol Auto-Resolver: Automatically detects whether your broker uses raw tickers (US500, DE40) or suffixed tickers (US500.cash, #US100, GER40).

Cross-Asset Correlation Warning: Because US500 and US100 share a greater than 0.90 intraday correlation, if both trigger a sweep on the same M15 candle, the scanner flags a [CORRELATED CLUSTER: SPLIT RISK 50/50] warning directly in your mobile notification so you never accidentally double your effective leverage.

Live Multi-Asset Telemetry Matrix: Renders a single unified dashboard on your chart showing live price, spread, M15 ATR, H4 trend bias, active session state, and liquidity levels across all 4 symbols.

Re: Scalping US100 and US500

Posted: Wed Oct 28, 2026 5:56 pm
by PTScalper
1. C# cTrader Automate (cBot Multi-Symbol Scanner & Dashboard)

Create a new cBot in cTrader Automate named MultiSymbolQuantScannerPro and paste the code below. Attach it to any single chart (e.g., US500 M15) and it will pull M15, H4, and D1 bars for all four instruments concurrently using a 1-second timer loop.

Code: Select all

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using cAlgo.API;
using cAlgo.API.Indicators;
using cAlgo.API.Internals;

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class MultiSymbolQuantScannerPro : Robot
    {
        #region Parameters

        [Parameter("Symbols (Comma Separated)", Group = "1. Watchlist & Broker Mapping", DefaultValue = "US500,US100,DE40,XAUUSD")]
        public string WatchlistCsv { get; set; }

        [Parameter("Series Base Risk Capital ($)", Group = "2. Hybrid Money Management", DefaultValue = 5000.0, MinValue = 100.0)]
        public double SeriesBaseCapital { get; set; }

        [Parameter("Base Risk Per Setup (%)", Group = "2. Hybrid Money Management", DefaultValue = 1.0, MinValue = 0.1, MaxValue = 5.0)]
        public double BaseRiskPercent { get; set; }

        [Parameter("Accrued Profit Multiplier", Group = "2. Hybrid Money Management", DefaultValue = 1.5, MinValue = 1.0, MaxValue = 3.0)]
        public double ExpProfitMultiplier { get; set; }

        [Parameter("Min Sweep Depth (x M15 ATR)", Group = "3. Quant Microstructure Filters", DefaultValue = 0.10, MinValue = 0.02, MaxValue = 1.0)]
        public double MinSweepAtrMult { get; set; }

        [Parameter("Max Candle Size (x M15 ATR)", Group = "3. Quant Microstructure Filters", DefaultValue = 2.80, MinValue = 1.0, MaxValue = 6.0)]
        public double MaxCandleAtrMult { get; set; }

        [Parameter("SL Buffer Beyond Wick (x M15 ATR)", Group = "3. Quant Microstructure Filters", DefaultValue = 0.20, MinValue = 0.05, MaxValue = 1.0)]
        public double SlBufferAtrMult { get; set; }

        [Parameter("Min Rejection Wick (% of Range)", Group = "3. Quant Microstructure Filters", DefaultValue = 32.0, MinValue = 10.0, MaxValue = 85.0)]
        public double MinWickPercent { get; set; }

        [Parameter("Require Volume Spike (> 1.15x SMA20)", Group = "3. Quant Microstructure Filters", DefaultValue = true)]
        public bool RequireVolumeSpike { get; set; }

        [Parameter("Desktop Sound & Popup", Group = "4. Mobile & Desktop Telemetry", DefaultValue = true)]
        public bool EnableSoundPopup { get; set; }

        [Parameter("Send Email / Push Notification", Group = "4. Mobile & Desktop Telemetry", DefaultValue = false)]
        public bool EnableEmail { get; set; }

        [Parameter("Alert Email Address", Group = "4. Mobile & Desktop Telemetry", DefaultValue = "")]
        public string EmailAddress { get; set; }

        #endregion

        private enum AssetClassProfile
        {
            US_Index,
            EU_Index_DAX,
            Gold_Comex
        }

        private class SymbolMonitorContext
        {
            public string RawName;
            public SymbolInstrument;
            public AssetClassProfile Profile;
            public Bars M15Bars;
            public Bars H4Bars;
            public Bars D1Bars;
            public AverageTrueRange M15Atr;
            public ExponentialMovingAverage H4Ema50;
            public SimpleMovingAverage VolSma20;

            public DateTime LastProcessedBarTime = DateTime.MinValue;
            public DateTime CurrentSessionDay = DateTime.MinValue;

            public double RangeHigh = double.MinValue;
            public double RangeLow = double.MaxValue;
            public double PdHigh = double.MinValue;
            public double PdLow = double.MaxValue;

            public bool ShortFiredThisSession = false;
            public bool LongFiredThisSession = false;
            public string ActiveStatus = "INIT";
            public string LastSignalSummary = "None";
        }

        private readonly List<SymbolMonitorContext> _monitors = new List<SymbolMonitorContext>();
        private readonly Dictionary<DateTime, List<string>> _barClusterTracker = new Dictionary<DateTime, List<string>>();

        protected override void OnStart()
        {
            string[] requestedNames = WatchlistCsv.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries);

            foreach (string raw in requestedNames)
            {
                string cleanName = raw.Trim();
                Symbol resolvedSymbol = ResolveBrokerSymbol(cleanName);

                if (resolvedSymbol == null)
                {
                    Print($"[Scanner Warning] Could not resolve symbol '{cleanName}' on this broker feed. Check ticker spelling.");
                    continue;
                }

                var ctx = new SymbolMonitorContext
                {
                    RawName = resolvedSymbol.Name,
                    SymbolInstrument = resolvedSymbol,
                    Profile = ClassifyAsset(resolvedSymbol.Name),
                    M15Bars = MarketData.GetBars(TimeFrame.Minute15, resolvedSymbol.Name),
                    H4Bars = MarketData.GetBars(TimeFrame.Hour4, resolvedSymbol.Name),
                    D1Bars = MarketData.GetBars(TimeFrame.Daily, resolvedSymbol.Name)
                };

                ctx.M15Atr = Indicators.AverageTrueRange(ctx.M15Bars, 14, MovingAverageType.Exponential);
                ctx.H4Ema50 = Indicators.ExponentialMovingAverage(ctx.H4Bars.ClosePrices, 50);
                ctx.VolSma20 = Indicators.SimpleMovingAverage(ctx.M15Bars.TickVolumes, 20);

                _monitors.Add(ctx);
                Print($"[Scanner Initialized] Monitoring {ctx.RawName} | Profile: {ctx.Profile}");
            }

            // Poll every 1 second to catch M15 bar closes across all 4 symbols deterministically
            Timer.Start(1);
            RenderDashboard();
        }

        protected override void OnTimer()
        {
            foreach (var ctx in _monitors)
            {
                EvaluateSymbolContext(ctx);
            }
            RenderDashboard();
        }

        private void EvaluateSymbolContext(SymbolMonitorContext ctx)
        {
            if (ctx.M15Bars.Count < 35) return;

            int closedIdx = ctx.M15Bars.Count - 2;
            DateTime closedBarUtc = ctx.M15Bars.OpenTimes[closedIdx];

            // Compute Session Windows for this asset on the closed bar's date
            GetSessionWindowsUtc(ctx.Profile, closedBarUtc, out DateTime rangeStartUtc, out DateTime killzoneOpenUtc, out DateTime killzoneEndUtc, out DateTime sessionAnchorDate);

            // Reset daily/session state if entering a new session anchor
            if (sessionAnchorDate != ctx.CurrentSessionDay)
            {
                ctx.CurrentSessionDay = sessionAnchorDate;
                ctx.RangeHigh = double.MinValue;
                ctx.RangeLow = double.MaxValue;
                ctx.ShortFiredThisSession = false;
                ctx.LongFiredThisSession = false;

                if (ctx.D1Bars.Count >= 2)
                {
                    ctx.PdHigh = ctx.D1Bars.HighPrices[ctx.D1Bars.Count - 2];
                    ctx.PdLow = ctx.D1Bars.LowPrices[ctx.D1Bars.Count - 2];
                }
            }

            // Rebuild Pre-Killzone Range High/Low from M15 history for the active window
            double rHigh = double.MinValue;
            double rLow = double.MaxValue;
            for (int i = closedIdx; i >= 0; i--)
            {
                DateTime t = ctx.M15Bars.OpenTimes[i];
                if (t < rangeStartUtc) break;
                if (t >= rangeStartUtc && t < killzoneOpenUtc)
                {
                    if (ctx.M15Bars.HighPrices[i] > rHigh) rHigh = ctx.M15Bars.HighPrices[i];
                    if (ctx.M15Bars.LowPrices[i] < rLow) rLow = ctx.M15Bars.LowPrices[i];
                }
            }
            ctx.RangeHigh = rHigh;
            ctx.RangeLow = rLow;

            // Update Live Status String
            DateTime nowUtc = Server.TimeInUtc;
            if (nowUtc >= rangeStartUtc && nowUtc < killzoneOpenUtc)
                ctx.ActiveStatus = "BUILDING RANGE";
            else if (nowUtc >= killzoneOpenUtc && nowUtc < killzoneEndUtc)
                ctx.ActiveStatus = "KILLZONE ACTIVE";
            else
                ctx.ActiveStatus = "STANDBY";

            // Only evaluate trigger once per newly closed M15 bar
            if (closedBarUtc <= ctx.LastProcessedBarTime) return;
            ctx.LastProcessedBarTime = closedBarUtc;

            if (closedBarUtc < killzoneOpenUtc || closedBarUtc >= killzoneEndUtc) return;
            if (ctx.RangeHigh <= ctx.RangeLow) return;

            CheckForM15Sweep(ctx, closedIdx, closedBarUtc);
        }

        private void CheckForM15Sweep(SymbolMonitorContext ctx, int idx, DateTime barUtc)
        {
            double open = ctx.M15Bars.OpenPrices[idx];
            double high = ctx.M15Bars.HighPrices[idx];
            double low = ctx.M15Bars.LowPrices[idx];
            double close = ctx.M15Bars.ClosePrices[idx];
            double range = high - low;
            double atr = ctx.M15Atr.Result[idx];

            if (range <= double.Epsilon || atr <= double.Epsilon) return;
            if (range > atr * MaxCandleAtrMult) return; // Reject freak news spikes

            if (RequireVolumeSpike && ctx.M15Bars.TickVolumes[idx] < ctx.VolSma20.Result[idx] * 1.15)
                return;

            int h4Idx = ctx.H4Bars.OpenTimes.GetIndexByTime(barUtc);
            bool h4Bull = h4Idx >= 0 && ctx.H4Bars.ClosePrices[h4Idx] > ctx.H4Ema50.Result[h4Idx];
            bool h4Bear = h4Idx >= 0 && ctx.H4Bars.ClosePrices[h4Idx] < ctx.H4Ema50.Result[h4Idx];

            double minSweep = atr * MinSweepAtrMult;
            double slBuffer = atr * SlBufferAtrMult;

            // 1. Bearish Sweep Evaluation
            if (!ctx.ShortFiredThisSession)
            {
                bool sweptRangeH = (high >= ctx.RangeHigh + minSweep) && (close < ctx.RangeHigh);
                bool sweptPdH = (ctx.PdHigh > 0) && (high >= ctx.PdHigh + minSweep) && (close < ctx.PdHigh);

                if (sweptRangeH || sweptPdH)
                {
                    double uWickPct = ((high - Math.Max(open, close)) / range) * 100.0;
                    if (uWickPct >= MinWickPercent)
                    {
                        ctx.ShortFiredThisSession = true;
                        string pool = (sweptRangeH && sweptPdH) ? "DUAL(PMH+PDH)" : (sweptPdH ? "PDH-SWEEP" : "PMH-SWEEP");
                        string grade = h4Bear ? "A+ [H4 Trend]" : "B [Counter-H4]";
                        double sl = high + slBuffer;
                        double stopPts = sl - close;
                        double tp1 = close - (stopPts * 1.5);
                        double tp2 = Math.Max(ctx.RangeLow, close - (stopPts * 3.0));

                        FireMultiSymbolAlert(ctx, "SHORT", pool, grade, close, sl, tp1, tp2, stopPts, barUtc);
                    }
                }
            }

            // 2. Bullish Sweep Evaluation
            if (!ctx.LongFiredThisSession)
            {
                bool sweptRangeL = (low <= ctx.RangeLow - minSweep) && (close > ctx.RangeLow);
                bool sweptPdL = (ctx.PdLow > 0 && ctx.PdLow < double.MaxValue) && (low <= ctx.PdLow - minSweep) && (close > ctx.PdLow);

                if (sweptRangeL || sweptPdL)
                {
                    double lWickPct = ((Math.Min(open, close) - low) / range) * 100.0;
                    if (lWickPct >= MinWickPercent)
                    {
                        ctx.LongFiredThisSession = true;
                        string pool = (sweptRangeL && sweptPdL) ? "DUAL(PML+PDL)" : (sweptPdL ? "PDL-SWEEP" : "PML-SWEEP");
                        string grade = h4Bull ? "A+ [H4 Trend]" : "B [Counter-H4]";
                        double sl = low - slBuffer;
                        double stopPts = close - sl;
                        double tp1 = close + (stopPts * 1.5);
                        double tp2 = Math.Min(ctx.RangeHigh, close + (stopPts * 3.0));

                        FireMultiSymbolAlert(ctx, "LONG", pool, grade, close, sl, tp1, tp2, stopPts, barUtc);
                    }
                }
            }
        }

        private void FireMultiSymbolAlert(SymbolMonitorContext ctx, string dir, string pool, string grade, double entry, double sl, double tp1, double tp2, double stopPts, DateTime barUtc)
        {
            if (!_barClusterTracker.ContainsKey(barUtc))
                _barClusterTracker[barUtc] = new List<string>();
            _barClusterTracker[barUtc].Add(ctx.RawName);

            bool isCorrelatedCluster = _barClusterTracker[barUtc].Count > 1;
            string clusterNote = isCorrelatedCluster ? " [CORRELATED CLUSTER - SPLIT RISK 50/50]" : "";

            double lots = CalculateHybridLots(ctx.SymbolInstrument, stopPts, out double riskUsd);
            int decimals = ctx.SymbolInstrument.Digits;

            string msg = $"[{ctx.RawName} M15 {dir}] {pool} | {grade}{clusterNote}\n" +
                         $"Lot: {lots:F2} (${riskUsd:F0}) | Ent: {entry.ToString($"F{decimals}")} | SL: {sl.ToString($"F{decimals}")} | TP1: {tp1.ToString($"F{decimals}")} | TP2: {tp2.ToString($"F{decimals}")}";

            ctx.LastSignalSummary = $"{dir} @ {entry.ToString($"F{decimals}")} ({pool})";
            Print(msg);

            if (EnableSoundPopup)
            {
                Notifications.PlaySound(SoundType.Announcement);
                MessageBox.Show(msg, $"Multi-Asset Sweep Alert: {ctx.RawName}", MessageBoxButton.OK, MessageBoxImage.Information);
            }

            if (EnableEmail && !string.IsNullOrWhiteSpace(EmailAddress))
            {
                Notifications.SendEmail(EmailAddress, EmailAddress, $"[SWEEP] {ctx.RawName} M15 {dir} ({grade})", msg);
            }
        }

        private double CalculateHybridLots(Symbol sym, double stopDistancePrice, out double riskUsd)
        {
            double equity = Account.Equity;
            double accruedProfit = Math.Max(0.0, equity - SeriesBaseCapital);
            double baseRisk = SeriesBaseCapital * (BaseRiskPercent / 100.0);
            double bonusRisk = accruedProfit * ((BaseRiskPercent * ExpProfitMultiplier) / 100.0);

            riskUsd = (equity < SeriesBaseCapital) ? equity * (BaseRiskPercent / 100.0) : baseRisk + bonusRisk;

            double ticks = stopDistancePrice / sym.TickSize;
            double lossPerUnit = ticks * sym.TickValue;
            double rawUnits = (lossPerUnit > 0) ? (riskUsd / lossPerUnit) : sym.VolumeInUnitsMin;
            double normUnits = sym.NormalizeVolumeInUnits(rawUnits, RoundingMode.Down);
            normUnits = Math.Max(sym.VolumeInUnitsMin, Math.Min(sym.VolumeInUnitsMax, normUnits));

            return sym.VolumeInUnitsToQuantity(normUnits);
        }

        /// <summary>
        /// Computes Asset-Specific Killzones (US Cash Open for US500/US100, Frankfurt + NY for DE40, COMEX + NY for Gold)
        /// </summary>
        private void GetSessionWindowsUtc(AssetClassProfile profile, DateTime barUtc, out DateTime rangeStartUtc, out DateTime killzoneOpenUtc, out DateTime killzoneEndUtc, out DateTime anchorDate)
        {
            int nyOffset = GetUsDstOffsetHours(barUtc);
            int euOffset = GetEuDstOffsetHours(barUtc);

            if (profile == AssetClassProfile.EU_Index_DAX)
            {
                DateTime cetTime = barUtc.AddHours(euOffset);
                anchorDate = cetTime.Date;

                // DAX Dual Killzone: Morning Frankfurt/Xetra Open (08:00-10:30 CET) OR Afternoon US Overlap (15:30-17:30 CET)
                if (cetTime.Hour < 13)
                {
                    rangeStartUtc = anchorDate.AddHours(1).AddHours(-euOffset);      // 01:00 - 08:00 CET Asian Range
                    killzoneOpenUtc = anchorDate.AddHours(8).AddHours(-euOffset);    // 08:00 CET Xetra Open
                    killzoneEndUtc = anchorDate.AddHours(10).AddMinutes(30).AddHours(-euOffset);
                }
                else
                {
                    anchorDate = cetTime.Date.AddHours(12); // Separate anchor for afternoon session
                    rangeStartUtc = cetTime.Date.AddHours(8).AddHours(-euOffset);    // 08:00 - 14:30/15:30 CET EU Range
                    DateTime nyDay = barUtc.AddHours(nyOffset).Date;
                    killzoneOpenUtc = nyDay.AddHours(9).AddMinutes(30).AddHours(-nyOffset); // 09:30 NY Open
                    killzoneEndUtc = nyDay.AddHours(11).AddMinutes(30).AddHours(-nyOffset);
                }
                return;
            }

            DateTime nyNow = barUtc.AddHours(nyOffset);
            anchorDate = nyNow.Date;

            if (profile == AssetClassProfile.Gold_Comex)
            {
                // Gold: Accumulate London morning (03:00 - 08:15 NY), watch COMEX + NY Equity Open (08:15 - 11:30 NY)
                rangeStartUtc = anchorDate.AddHours(3).AddHours(-nyOffset);
                killzoneOpenUtc = anchorDate.AddHours(8).AddMinutes(15).AddHours(-nyOffset);
                killzoneEndUtc = anchorDate.AddHours(11).AddMinutes(30).AddHours(-nyOffset);
            }
            else
            {
                // US500 / US100: Accumulate Pre-Market (03:00 - 09:30 NY), watch NY Open (09:30 - 12:00 NY)
                rangeStartUtc = anchorDate.AddHours(3).AddHours(-nyOffset);
                killzoneOpenUtc = anchorDate.AddHours(9).AddMinutes(30).AddHours(-nyOffset);
                killzoneEndUtc = anchorDate.AddHours(12).AddHours(-nyOffset);
            }
        }

        private int GetUsDstOffsetHours(DateTime utc)
        {
            int y = utc.Year;
            DateTime mar1 = new DateTime(y, 3, 1, 7, 0, 0);
            DateTime dstStart = mar1.AddDays(((int)DayOfWeek.Sunday - (int)mar1.DayOfWeek + 7) % 7 + 7);
            DateTime nov1 = new DateTime(y, 11, 1, 6, 0, 0);
            DateTime dstEnd = nov1.AddDays(((int)DayOfWeek.Sunday - (int)nov1.DayOfWeek + 7) % 7);
            return (utc >= dstStart && utc < dstEnd) ? -4 : -5;
        }

        private int GetEuDstOffsetHours(DateTime utc)
        {
            int y = utc.Year;
            DateTime mar31 = new DateTime(y, 3, 31, 1, 0, 0);
            DateTime dstStart = mar31.AddDays(-(int)mar31.DayOfWeek);
            DateTime oct31 = new DateTime(y, 10, 31, 1, 0, 0);
            DateTime dstEnd = oct31.AddDays(-(int)oct31.DayOfWeek);
            return (utc >= dstStart && utc < dstEnd) ? 2 : 1;
        }

        private AssetClassProfile ClassifyAsset(string symbolName)
        {
            string u = symbolName.ToUpperInvariant();
            if (u.Contains("DE40") || u.Contains("GER40") || u.Contains("DAX")) return AssetClassProfile.EU_Index_DAX;
            if (u.Contains("XAU") || u.Contains("GOLD")) return AssetClassProfile.Gold_Comex;
            return AssetClassProfile.US_Index;
        }

        private Symbol ResolveBrokerSymbol(string target)
        {
            string[] candidates = { target, target + ".cash", "#" + target, target + "Roll" };
            foreach (string c in candidates)
            {
                try
                {
                    var s = Symbols.GetSymbol(c);
                    if (s != null) return s;
                }
                catch { }
            }
            return null;
        }

        private void RenderDashboard()
        {
            var sb = new StringBuilder();
            sb.AppendLine("==========================================================================================");
            sb.AppendLine($" MULTI-SYMBOL M15 QUANT SWEEP SCANNER | UTC: {Server.TimeInUtc:HH:mm:ss} | Equity: ${Account.Equity:F0}");
            sb.AppendLine("==========================================================================================");
            sb.AppendLine(string.Format("{0,-10} | {1,-16} | {2,-10} | {3,-9} | {4,-18} | {5}",
                "SYMBOL", "SESSION STATE", "M15 ATR", "H4 BIAS", "PMH / PML", "LAST SIGNAL"));
            sb.AppendLine("------------------------------------------------------------------------------------------");

            foreach (var ctx in _monitors)
            {
                double atr = (ctx.M15Atr.Result.Count > 0) ? ctx.M15Atr.Result.LastValue : 0;
                bool h4Bull = (ctx.H4Bars.Count > 0 && ctx.H4Ema50.Result.Count > 0) &&
                              ctx.H4Bars.ClosePrices.LastValue > ctx.H4Ema50.Result.LastValue;
                string bias = h4Bull ? "BULLISH" : "BEARISH";
                int d = ctx.SymbolInstrument.Digits;
                string levels = (ctx.RangeHigh > ctx.RangeLow)
                    ? $"{ctx.RangeHigh.ToString($"F{d}")} / {ctx.RangeLow.ToString($"F{d}")}"
                    : "Wait Range";

                sb.AppendLine(string.Format("{0,-10} | {1,-16} | {2,-10:F2} | {3,-9} | {4,-18} | {5}",
                    ctx.RawName, ctx.ActiveStatus, atr, bias, levels, ctx.LastSignalSummary));
            }
            sb.AppendLine("==========================================================================================");

            Chart.DrawStaticText("MultiScannerHUD", sb.ToString(), VerticalAlignment.Top, HorizontalAlignment.Left, Color.LightSteelBlue);
        }
    }
}