Page 2 of 3

Re: An Event Calendar Workflow That Takes Ten Minutes a Week

Posted: Tue Oct 06, 2026 9:14 pm
by PTScalper
Pine Script v5: Event Risk & 1-Hour Reaction Tracker

Code: Select all

//@version=5
indicator("Event Risk & 1H Reaction Tracker [Workflow]", overlay=true, max_labels_count=50, max_boxes_count=50)

// =========================================================================
// INPUTS: Weekend Event Configuration
// =========================================================================
grp1 = "Event 1 Setup"
ev1_on   = input.bool(true, "Enable Event 1", group=grp1)
ev1_name = input.string("US CPI", "Event Name", group=grp1)
ev1_time = input.time(timestamp("2026-10-08 08:30"), "Event Time", group=grp1)

grp2 = "Event 2 Setup"
ev2_on   = input.bool(true, "Enable Event 2", group=grp2)
ev2_name = input.string("FOMC Rate Decision", "Event Name", group=grp2)
ev2_time = input.time(timestamp("2026-10-08 14:00"), "Event Time", group=grp2)

grp3 = "Event 3 Setup"
ev3_on   = input.bool(true, "Enable Event 3", group=grp3)
ev3_name = input.string("ECB Press Conf", "Event Name", group=grp3)
ev3_time = input.time(timestamp("2026-10-09 08:30"), "Event Time", group=grp3)

grp4 = "Event 4 Setup"
ev4_on   = input.bool(false, "Enable Event 4", group=grp4)
ev4_name = input.string("US Non-Farm Payrolls", "Event Name", group=grp4)
ev4_time = input.time(timestamp("2026-10-09 10:00"), "Event Time", group=grp4)

grpPolicy = "Policy & Display Options"
minsBefore   = input.int(30, "No-Trade Window Before (Mins)", group=grpPolicy)
minsAfter    = input.int(30, "No-Trade Window After (Mins)", group=grpPolicy)
alertMins    = input.int(10, "Early Warning Alert (Mins)", group=grpPolicy)
reactionMins = input.int(60, "Post-Event Reaction Window (Mins)", minval=15, group=grpPolicy)
showTable    = input.bool(true, "Show Weekly Summary Table", group=grpPolicy)

// =========================================================================
// HELPER CALCULATIONS
// =========================================================================
// Converts price difference to pips for Forex, or raw points for indices/commodities
toPips(float priceDelta) =>
    pipUnit = syminfo.type == "forex" ? syminfo.mintick * 10 : syminfo.mintick
    priceDelta / pipUnit

unitLabel = syminfo.type == "forex" ? " pips" : " pts"

// Check if current bar overlaps with the No-Trade Policy Window
inNoTradeZone(int evTime, bool active) =>
    if not active
        false
    else
        startT = evTime - (minsBefore * 60000)
        endT   = evTime + (minsAfter * 60000)
        time <= endT and time_close >= startT

// Check if 10-minute warning alert should fire
isAlertCandidate(int evTime, bool active) =>
    if not active
        false
    else
        alertT = evTime - (alertMins * 60000)
        time <= alertT and time_close > alertT

// =========================================================================
// DATA STRUCTURE FOR EVENT TRACKING
// =========================================================================
type EventTracker
    string name
    int    startTime
    int    endTime
    bool   enabled
    bool   started
    bool   completed
    float  basePrice
    float  maxHigh
    float  minLow
    float  endPrice

// Initialize event trackers once
var EventTracker[] events = array.new<EventTracker>()

if barstate.isfirst
    if ev1_on
        array.push(events, EventTracker.new(ev1_name, ev1_time, ev1_time + (reactionMins * 60000), ev1_on, false, false, na, na, na, na))
    if ev2_on
        array.push(events, EventTracker.new(ev2_name, ev2_time, ev2_time + (reactionMins * 60000), ev2_on, false, false, na, na, na, na))
    if ev3_on
        array.push(events, EventTracker.new(ev3_name, ev3_time, ev3_time + (reactionMins * 60000), ev3_on, false, false, na, na, na, na))
    if ev4_on
        array.push(events, EventTracker.new(ev4_name, ev4_time, ev4_time + (reactionMins * 60000), ev4_on, false, false, na, na, na, na))

// Table initialization
var table summaryTable = table.new(position.top_right, 4, 6, bgcolor=color.new(#1e222d, 10), border_width=1, border_color=color.gray)

if barstate.isfirst and showTable
    table.cell(summaryTable, 0, 0, "Event", bgcolor=color.new(color.blue, 40), text_color=color.white, text_size=size.small)
    table.cell(summaryTable, 1, 0, "1H Net Move", bgcolor=color.new(color.blue, 40), text_color=color.white, text_size=size.small)
    table.cell(summaryTable, 2, 0, "1H Range (Vol)", bgcolor=color.new(color.blue, 40), text_color=color.white, text_size=size.small)
    table.cell(summaryTable, 3, 0, "Direction", bgcolor=color.new(color.blue, 40), text_color=color.white, text_size=size.small)

// =========================================================================
// MAIN EXECUTION LOOP (BAR-BY-BAR)
// =========================================================================
bool anyNoTradeZone = false
bool anyAlertTrigger = false

if array.size(events) > 0
    for i = 0 to array.size(events) - 1
        EventTracker item = array.get(events, i)
        
        // 1. Evaluate risk window & alerts
        if inNoTradeZone(item.startTime, item.enabled)
            anyNoTradeZone := true
        if isAlertCandidate(item.startTime, item.enabled)
            anyAlertTrigger := true

        // 2. Track 1-Hour Post-Event Reaction
        if item.enabled and not item.completed
            // Detect event start
            if time_close >= item.startTime and not item.started
                item.started   := true
                item.basePrice := open
                item.maxHigh   := high
                item.minLow    := low
            
            // During the 1-hour window
            if item.started
                item.maxHigh := math.max(nz(item.maxHigh, high), high)
                item.minLow  := math.min(nz(item.minLow, low), low)

                // Detect completion of the 1-hour window
                if time_close >= item.endTime or (time <= item.endTime and time_close > item.endTime)
                    item.completed := true
                    item.endPrice  := close

                    // Metric Calculations
                    netDelta   = item.endPrice - item.basePrice
                    netPips    = toPips(netDelta)
                    rangePips  = toPips(item.maxHigh - item.minLow)
                    pctDelta   = (netDelta / item.basePrice) * 100
                    isBullish  = netDelta >= 0

                    dirColor   = isBullish ? color.green : color.red
                    dirSymbol  = isBullish ? "▲ BULLISH" : "▼ BEARISH"

                    // Draw 1-Hour Reaction Highlight Box
                    box.new(left=item.startTime, top=item.maxHigh, right=item.endTime, bottom=item.minLow, 
                            xloc=xloc.bar_time, border_color=dirColor, border_width=1, 
                            bgcolor=color.new(dirColor, 90))

                    // Draw Detailed Reaction Label on Chart
                    labelText = item.name + " (1-Hour Reaction)\n" +
                                "Direction: " + dirSymbol + "\n" +
                                "Net: " + (isBullish ? "+" : "") + str.tostring(netPips, "#.#") + unitLabel + " (" + str.tostring(pctDelta, "#.##") + "%)\n" +
                                "Total Range: " + str.tostring(rangePips, "#.#") + unitLabel
                    
                    label.new(x=time, y=item.endPrice, text=labelText, xloc=xloc.bar_time,
                              color=color.new(#1e222d, 10), textcolor=color.white,
                              style=isBullish ? label.style_label_down : label.style_label_up,
                              size=size.normal)

                    // Update HUD Table Row
                    if showTable
                        row = i + 1
                        table.cell(summaryTable, 0, row, item.name, text_color=color.white, text_size=size.small)
                        table.cell(summaryTable, 1, row, (isBullish ? "+" : "") + str.tostring(netPips, "#.#") + unitLabel, text_color=dirColor, text_size=size.small)
                        table.cell(summaryTable, 2, row, str.tostring(rangePips, "#.#") + unitLabel, text_color=color.yellow, text_size=size.small)
                        table.cell(summaryTable, 3, row, isBullish ? "Bullish" : "Bearish", text_color=dirColor, text_size=size.small)

// =========================================================================
// VISUALS & ALERTS
// =========================================================================
// Shade the background during your defined No-Trade policy window
bgcolor(anyNoTradeZone ? color.new(color.red, 88) : na, title="No-Trade Risk Zone")

// Audio/Push Notification
if anyAlertTrigger
    alert("⚠️️ EVENT WARNING: Tier-1 event in " + str.tostring(alertMins) + " minutes. Follow your policy!", alert.freq_once_per_bar)

Re: An Event Calendar Workflow That Takes Ten Minutes a Week

Posted: Tue Oct 06, 2026 9:15 pm
by PTScalper
What Has Changed and How It Works

The 1-Hour Reaction Engine:

The script automatically locks the price at the minute the event goes live (basePrice), monitors the maximum swing high and lowest low across the next 60 minutes, and captures the exact close at the end of the hour.

Automatic Pip/Point Normalization:

It checks whether you are looking at a Forex pair (where 1 pip = 10 points) or an index/commodity like US500, NAS100, or XAU/USD, displaying the movements in standard pips or points accordingly.

The On-Chart Reaction Box:

Paints a shaded boundary covering the high and low of the 60-minute window, colored green or red depending on whether the post-event resolution was bullish or bearish.

The HUD Summary Table:

Permanently pins the results of the week's events in the upper-right corner of your chart. When completing your weekend review, you can reference the table directly instead of manually measuring bars with the price tool.

Re: An Event Calendar Workflow That Takes Ten Minutes a Week

Posted: Tue Oct 06, 2026 9:17 pm
by PTScalper
Moving this workflow from TradingView to MetaTrader requires a slight shift in how we handle the visual data. Pine Script evaluates bar-by-bar natively, but MQL4 and MQL5 are strictly object-oriented. Instead of painting a background color on candles, we tell MetaTrader to draw OBJ_RECTANGLE objects directly onto the chart using specific timestamps.

CRITICAL RULE FOR METATRADER: TradingView automatically converts event times to your local PC timezone. MetaTrader does not. When you do your weekend prep, you MUST enter the event times in your broker's Server Time (the time displayed in your Market Watch window), otherwise your boxes and alerts will trigger hours early or late.

Re: An Event Calendar Workflow That Takes Ten Minutes a Week

Posted: Tue Oct 06, 2026 9:17 pm
by PTScalper
Here are the indicator files for both platforms.

1. The MT4 Version (.mq4)

Code: Select all

//+------------------------------------------------------------------+
//|                                             EventRiskManager.mq4 |
//|                                      Event Risk & Reaction Tracker|
//+------------------------------------------------------------------+
#property copyright "MacroTrader Workflow"
#property strict
#property indicator_chart_window

input string   grp1="--- Event 1 ---";
input bool     Ev1_On = true;
input string   Ev1_Name = "US CPI";
input datetime Ev1_Time = D'2026.10.08 14:30'; // BROKER SERVER TIME

input string   grp2="--- Event 2 ---";
input bool     Ev2_On = true;
input string   Ev2_Name = "FOMC Rate";
input datetime Ev2_Time = D'2026.10.08 20:00'; // BROKER SERVER TIME

input string   grpPolicy="--- Policy & Display ---";
input int      MinsBefore = 30;
input int      MinsAfter = 30;
input int      AlertMins = 10;
input int      ReactionMins = 60;

bool alert1_fired=false, alert2_fired=false;

int OnInit() { return(INIT_SUCCEEDED); }
void OnDeinit(const int reason) { ObjectsDeleteAll(0, "ERM_"); }

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[])
{
   ProcessEvent(1, Ev1_On, Ev1_Name, Ev1_Time, alert1_fired);
   ProcessEvent(2, Ev2_On, Ev2_Name, Ev2_Time, alert2_fired);
   return(rates_total);
}

void ProcessEvent(int id, bool enable, string name, datetime evTime, bool &alertFired)
{
   if(!enable) return;
   
   string prefix = "ERM_" + IntegerToString(id);
   datetime now = TimeCurrent();
   
   // 1. Early Warning Alert
   datetime alertTime = evTime - (AlertMins * 60);
   if(now >= alertTime && now < evTime && !alertFired)
   {
      Alert("10-MIN WARNING: ", name, " approaching. Flatten per policy!");
      alertFired = true;
   }
   
   // 2. Risk Box (Red Background)
   string riskBox = prefix + "_Risk";
   if(ObjectFind(riskBox) < 0)
   {
      ObjectCreate(0, riskBox, OBJ_RECTANGLE, 0, evTime - (MinsBefore*60), 99999, evTime + (MinsAfter*60), 0.00001);
      ObjectSetInteger(0, riskBox, OBJPROP_COLOR, clrDarkRed);
      ObjectSetInteger(0, riskBox, OBJPROP_BACK, true);
   }
   
   // 3. Post-Event 1-Hour Reaction Box
   datetime reactionEnd = evTime + (ReactionMins * 60);
   if(now >= reactionEnd)
   {
      string rxnBox = prefix + "_Rxn";
      if(ObjectFind(rxnBox) < 0) // Only draw it once it finishes
      {
         int startShift = iBarShift(Symbol(), Period(), evTime);
         int endShift = iBarShift(Symbol(), Period(), reactionEnd);
         
         if(startShift >= 0 && endShift >= 0)
         {
            int range = startShift - endShift + 1;
            int hIndex = iHighest(Symbol(), Period(), MODE_HIGH, range, endShift);
            int lIndex = iLowest(Symbol(), Period(), MODE_LOW, range, endShift);
            
            double rHigh = High[hIndex];
            double rLow = Low[lIndex];
            double pOpen = Open[startShift];
            double pClose = Close[endShift];
            bool isBullish = (pClose >= pOpen);
            
            // Draw Reaction Box Outline
            ObjectCreate(0, rxnBox, OBJ_RECTANGLE, 0, evTime, rHigh, reactionEnd, rLow);
            ObjectSetInteger(0, rxnBox, OBJPROP_COLOR, isBullish ? clrGreen : clrRed);
            ObjectSetInteger(0, rxnBox, OBJPROP_WIDTH, 2);
            ObjectSetInteger(0, rxnBox, OBJPROP_BACK, false);
            
            // Text Label at the close
            string txtName = prefix + "_Txt";
            ObjectCreate(0, txtName, OBJ_TEXT, 0, reactionEnd, pClose);
            ObjectSetString(0, txtName, OBJPROP_TEXT, " " + name + (isBullish ? " (BULL)" : " (BEAR)"));
            ObjectSetInteger(0, txtName, OBJPROP_COLOR, isBullish ? clrLime : clrRed);
         }
      }
   }
}

Re: An Event Calendar Workflow That Takes Ten Minutes a Week

Posted: Tue Oct 06, 2026 9:17 pm
by PTScalper
2. The MT5 Version (.mq5)

MT5 handles arrays differently. Instead of iHighest/iLowest, we use MQL5's CopyHigh and CopyLow to pinpoint the exact 60-minute range without relying on chart shifts.

Code: Select all

//+------------------------------------------------------------------+
//|                                             EventRiskManager.mq5 |
//|                                      Event Risk & Reaction Tracker|
//+------------------------------------------------------------------+
#property copyright "MacroTrader Workflow"
#property indicator_chart_window
#property indicator_plots 0

input string   grp1="--- Event 1 ---";
input bool     Ev1_On = true;
input string   Ev1_Name = "US CPI";
input datetime Ev1_Time = D'2026.10.08 14:30'; // BROKER SERVER TIME

input string   grp2="--- Event 2 ---";
input bool     Ev2_On = true;
input string   Ev2_Name = "FOMC Rate";
input datetime Ev2_Time = D'2026.10.08 20:00'; // BROKER SERVER TIME

input string   grpPolicy="--- Policy & Display ---";
input int      MinsBefore = 30;
input int      MinsAfter = 30;
input int      AlertMins = 10;
input int      ReactionMins = 60;

bool alert1_fired=false, alert2_fired=false;

int OnInit() { return(INIT_SUCCEEDED); }
void OnDeinit(const int reason) { ObjectsDeleteAll(0, 0, "ERM_"); }

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[])
{
   ProcessEvent(1, Ev1_On, Ev1_Name, Ev1_Time, alert1_fired);
   ProcessEvent(2, Ev2_On, Ev2_Name, Ev2_Time, alert2_fired);
   return(rates_total);
}

void ProcessEvent(int id, bool enable, string name, datetime evTime, bool &alertFired)
{
   if(!enable) return;
   
   string prefix = "ERM_" + IntegerToString(id);
   datetime now = TimeCurrent();
   
   // 1. Early Warning Alert
   datetime alertTime = evTime - (AlertMins * 60);
   if(now >= alertTime && now < evTime && !alertFired)
   {
      Alert("10-MIN WARNING: ", name, " approaching. Flatten per policy!");
      alertFired = true;
   }
   
   // 2. Risk Box (Red Background)
   string riskBox = prefix + "_Risk";
   if(ObjectFind(0, riskBox) < 0)
   {
      ObjectCreate(0, riskBox, OBJ_RECTANGLE, 0, evTime - (MinsBefore*60), 99999, evTime + (MinsAfter*60), 0.00001);
      ObjectSetInteger(0, riskBox, OBJPROP_COLOR, clrDarkRed);
      ObjectSetInteger(0, riskBox, OBJPROP_BACK, true);
   }
   
   // 3. Post-Event 1-Hour Reaction Box
   datetime reactionEnd = evTime + (ReactionMins * 60);
   if(now >= reactionEnd)
   {
      string rxnBox = prefix + "_Rxn";
      if(ObjectFind(0, rxnBox) < 0) // Only draw it once it finishes
      {
         double hArr[], lArr[], cArr[], oArr[];
         
         // Fetch the exact price arrays for the 1-hour window
         if(CopyHigh(Symbol(), Period(), evTime, reactionEnd, hArr) > 0 &&
            CopyLow(Symbol(), Period(), evTime, reactionEnd, lArr) > 0 &&
            CopyClose(Symbol(), Period(), evTime, reactionEnd, cArr) > 0 &&
            CopyOpen(Symbol(), Period(), evTime, reactionEnd, oArr) > 0)
         {
            double rHigh = hArr[ArrayMaximum(hArr)];
            double rLow  = lArr[ArrayMinimum(lArr)];
            double pOpen = oArr[0];  // open of the start candle
            double pClose = cArr[ArraySize(cArr)-1]; // close of the end candle
            bool isBullish = (pClose >= pOpen);
            
            // Draw Reaction Box Outline
            ObjectCreate(0, rxnBox, OBJ_RECTANGLE, 0, evTime, rHigh, reactionEnd, rLow);
            ObjectSetInteger(0, rxnBox, OBJPROP_COLOR, isBullish ? clrGreen : clrRed);
            ObjectSetInteger(0, rxnBox, OBJPROP_WIDTH, 2);
            ObjectSetInteger(0, rxnBox, OBJPROP_BACK, false);
            
            // Text Label at the close
            string txtName = prefix + "_Txt";
            ObjectCreate(0, txtName, OBJ_TEXT, 0, reactionEnd, pClose);
            ObjectSetString(0, txtName, OBJPROP_TEXT, " " + name + (isBullish ? " (BULL)" : " (BEAR)"));
            ObjectSetInteger(0, txtName, OBJPROP_COLOR, isBullish ? clrLime : clrRed);
         }
      }
   }
}

Re: An Event Calendar Workflow That Takes Ten Minutes a Week

Posted: Tue Oct 06, 2026 9:18 pm
by PTScalper
How to Install:

1.) Open MT4/MT5 and hit F4 to open the MetaEditor.

2.) In the Navigator, right-click Indicators -> New File -> Custom Indicator.

3.) Give it a name, click finish, delete the auto-generated code, and paste the code block above.

4.) Hit Compile (F7) and attach it to your chart!

If you'd like to get a better visual sense of how ObjectCreate positions shape elements on your charts, check out this MQL4 simple rectangle tutorial. It covers the foundational logic used to enclose specific time periods with custom rectangles.

Re: An Event Calendar Workflow That Takes Ten Minutes a Week

Posted: Tue Oct 06, 2026 9:20 pm
by PTScalper
For those executing on cTrader, moving this workflow over requires C# and the cAlgo.API. Because cTrader handles visual chart objects and historical rendering much more cleanly than MQL, we can build a very elegant tool that tracks both the No-Trade Zone dynamically and draws the 1-Hour Reaction box once the window closes.

Re: An Event Calendar Workflow That Takes Ten Minutes a Week

Posted: Tue Oct 06, 2026 9:20 pm
by PTScalper
Since cTrader allows us to pass date-time inputs smoothly as strings, you can type your weekend prep times straight into the indicator settings without messing with complex UI menus.

CRITICAL RULE FOR CTRADER: The script looks at Server.Time. When you punch in your times on the weekend, make sure they align with your broker's server time (usually UTC+2 or UTC+3, visible at the bottom right of the cTrader terminal).

Re: An Event Calendar Workflow That Takes Ten Minutes a Week

Posted: Tue Oct 06, 2026 9:21 pm
by PTScalper
The C# cTrader Automate Script

1.) Open cTrader and head to the Automate tab on the left menu.

2.) Under "Indicators", click New.

3.) Name it EventRiskManager, delete the default boilerplate code, and paste the code below.

4.) Click Build (the hammer icon).

Re: An Event Calendar Workflow That Takes Ten Minutes a Week

Posted: Tue Oct 06, 2026 9:21 pm
by PTScalper
Ctrader version 1.0

Code: Select all

//+------------------------------------------------------------------+
//|                                     EventRiskManager.cs          |
//|                                     Event Risk & Reaction Tracker|
//+------------------------------------------------------------------+
using System;
using cAlgo.API;

namespace cAlgo
{
    [Indicator(IsOverlay = true, TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class EventRiskManager : Indicator
    {
        // --- EVENT 1 ---
        [Parameter("Enable Event 1", DefaultValue = true, Group = "Event 1")]
        public bool Ev1On { get; set; }

        [Parameter("Name", DefaultValue = "US CPI", Group = "Event 1")]
        public string Ev1Name { get; set; }

        [Parameter("Time (Server Time: yyyy-MM-dd HH:mm)", DefaultValue = "2026-10-08 14:30", Group = "Event 1")]
        public string Ev1TimeStr { get; set; }

        // --- EVENT 2 ---
        [Parameter("Enable Event 2", DefaultValue = true, Group = "Event 2")]
        public bool Ev2On { get; set; }

        [Parameter("Name", DefaultValue = "FOMC Rate Decision", Group = "Event 2")]
        public string Ev2Name { get; set; }

        [Parameter("Time (Server Time: yyyy-MM-dd HH:mm)", DefaultValue = "2026-10-08 20:00", Group = "Event 2")]
        public string Ev2TimeStr { get; set; }

        // --- POLICY SETTINGS ---
        [Parameter("No-Trade Mins Before", DefaultValue = 30, Group = "Policy")]
        public int MinsBefore { get; set; }

        [Parameter("No-Trade Mins After", DefaultValue = 30, Group = "Policy")]
        public int MinsAfter { get; set; }

        [Parameter("Alert Mins Before", DefaultValue = 10, Group = "Policy")]
        public int AlertMins { get; set; }

        [Parameter("Reaction Window Mins", DefaultValue = 60, Group = "Policy")]
        public int ReactionMins { get; set; }


        private DateTime _ev1Time, _ev2Time;
        private bool _alert1Fired, _alert2Fired;
        private bool _rxn1Drawn, _rxn2Drawn;

        protected override void Initialize()
        {
            // Safely parse the weekend string inputs into DateTimes
            if (Ev1On && !DateTime.TryParse(Ev1TimeStr, out _ev1Time))
                Print("Format error for Event 1 Time. Please use yyyy-MM-dd HH:mm");
            
            if (Ev2On && !DateTime.TryParse(Ev2TimeStr, out _ev2Time))
                Print("Format error for Event 2 Time. Please use yyyy-MM-dd HH:mm");
        }

        public override void Calculate(int index)
        {
            if (Ev1On && _ev1Time != default)
                ProcessEventOnBar(index, 1, Ev1Name, _ev1Time, ref _alert1Fired, ref _rxn1Drawn);

            if (Ev2On && _ev2Time != default)
                ProcessEventOnBar(index, 2, Ev2Name, _ev2Time, ref _alert2Fired, ref _rxn2Drawn);
        }

        private void ProcessEventOnBar(int index, int id, string name, DateTime evTime, ref bool alertFired, ref bool rxnDrawn)
        {
            DateTime currentBarTime = Bars.OpenTimes[index];
            DateTime riskStart = evTime.AddMinutes(-MinsBefore);
            DateTime riskEnd = evTime.AddMinutes(MinsAfter);
            DateTime alertTime = evTime.AddMinutes(-AlertMins);
            DateTime rxnEnd = evTime.AddMinutes(ReactionMins);

            string prefix = $"ERM_{id}";

            // 1. RISK BOX LOGIC: Dark Red No-Trade Background
            // We draw/update it dynamically in real-time, or pop it in historically
            if (currentBarTime >= riskStart && currentBarTime <= riskEnd)
            {
                DrawOrUpdateRiskBox(prefix + "_Risk", riskStart, riskEnd, index);
            }
            else if (currentBarTime > riskEnd && Chart.FindObject(prefix + "_Risk") == null)
            {
                DrawOrUpdateRiskBox(prefix + "_Risk", riskStart, riskEnd, index);
            }

            // 2. 1-HOUR REACTION BOX LOGIC
            // Only fires once the window completes, tracking exactly where price swept
            if (currentBarTime >= rxnEnd && !rxnDrawn)
            {
                int endIndex = index;
                int startIndex = index;

                // Walk backward in index to find the bar where the event started
                while (startIndex > 0 && Bars.OpenTimes[startIndex] > evTime)
                    startIndex--;

                double high = double.MinValue;
                double low = double.MaxValue;

                for (int i = startIndex; i <= endIndex; i++)
                {
                    if (Bars.HighPrices[i] > high) high = Bars.HighPrices[i];
                    if (Bars.LowPrices[i] < low) low = Bars.LowPrices[i];
                }

                double openPrice = Bars.OpenPrices[startIndex];
                double closePrice = Bars.ClosePrices[endIndex];
                bool isBullish = closePrice >= openPrice;
                
                Color rxnColor = isBullish ? Color.LimeGreen : Color.Red;

                var rxnBox = Chart.DrawRectangle(prefix + "_Rxn", Bars.OpenTimes[startIndex], high, Bars.OpenTimes[endIndex], low, rxnColor);
                rxnBox.IsFilled = false;
                rxnBox.Thickness = 2;
                rxnBox.IsInteractive = false;

                var txt = Chart.DrawText(prefix + "_Txt", $" {name} ({(isBullish ? "BULL" : "BEAR")})", Bars.OpenTimes[endIndex], closePrice, rxnColor);
                txt.VerticalAlignment = VerticalAlignment.Center;
                
                rxnDrawn = true;
            }

            // 3. EARLY WARNING ALERTS (Real-Time Only)
            if (IsLastBar && !alertFired)
            {
                if (Server.Time >= alertTime && Server.Time < evTime)
                {
                    Notifications.PlaySound(SoundType.Ring);
                    Print($"⚠️ 10-MIN WARNING: {name} is approaching. Flatten positions per policy!");
                    alertFired = true;
                }
            }
        }

        // Helper to keep the Red Risk Box dynamically sized to local highs/lows so it doesn't zoom out the chart infinitely
        private void DrawOrUpdateRiskBox(string objectName, DateTime startTime, DateTime endTime, int currentIndex)
        {
            int startIdx = currentIndex;
            while (startIdx > 0 && Bars.OpenTimes[startIdx] > startTime) 
                startIdx--;

            int endIdx = currentIndex;
            while (endIdx > 0 && Bars.OpenTimes[endIdx] > endTime) 
                endIdx--;

            if (endIdx > currentIndex) endIdx = currentIndex; 

            double maxH = double.MinValue;
            double minL = double.MaxValue;

            for (int i = startIdx; i <= endIdx; i++)
            {
                if (Bars.HighPrices[i] > maxH) maxH = Bars.HighPrices[i];
                if (Bars.LowPrices[i] < minL) minL = Bars.LowPrices[i];
            }

            // Pad the box vertically by 50% of the range or a minimum 20 pips so the background looks continuous
            double padding = Math.Max((maxH - minL) * 0.5, Symbol.PipSize * 20); 

            var box = Chart.FindObject(objectName) as ChartRectangle;
            if (box == null)
            {
                box = Chart.DrawRectangle(objectName, startTime, maxH + padding, endTime, minL - padding, Color.FromArgb(40, Color.Red));
                box.IsFilled = true;
                box.IsInteractive = false;
            }
            else
            {
                box.Y1 = maxH + padding;
                box.Y2 = minL - padding;
            }
        }
    }
}