Page 2 of 2

Re: 🛢️ High-Volume Oil (WTI) Scalping Strategy – 18 Years of Price Action Distilled

Posted: Mon Aug 31, 2026 9:31 pm
by PTScalper
To implement a daily maximum drawdown limit, the EA needs to capture a "snapshot" of your account balance at the very first tick of a new trading day. It will then continuously compare your live floating equity against that starting balance.

If your equity drops below the allowed threshold, a lockout flag is triggered, preventing any new trades until the broker server rolls over to the next day.

Here is how to integrate this safety net into your MQL5 script.

1. Add the Drawdown Inputs and Global Variables
Place these new variables at the top of your script, right below your existing risk management inputs. We need global variables to store the state of the current day so the EA doesn't lose track of the limit between ticks.

Code: Select all

//--- Daily Drawdown Limits
input double MaxDailyDrawdown = 5.0; // Max daily drawdown % (0 to disable)

//--- Global variables for Drawdown Tracker
double dayStartBalance = 0.0;
int    currentDay = -1;
bool   dailyLimitHit = false;
Add the Drawdown Check Function
Add this custom function at the bottom of your script. This function detects when a new day begins, resets the tracking variables, and calculates the live drawdown percentage.

Code: Select all

//+------------------------------------------------------------------+
//| Custom Function: Check Daily Drawdown Limit                      |
//+------------------------------------------------------------------+
bool IsDailyDrawdownHit()
  {
   // If set to 0, the filter is disabled
   if(MaxDailyDrawdown <= 0) return false; 

   MqlDateTime timeStruct;
   TimeCurrent(timeStruct);

   // 1. Detect a new server day
   if(timeStruct.day_of_year != currentDay)
     {
      currentDay = timeStruct.day_of_year;
      // Snapshot the balance at the start of the day
      dayStartBalance = AccountInfoDouble(ACCOUNT_BALANCE); 
      dailyLimitHit = false; // Reset the lockout flag
      Print("New day started. Starting balance snapshot: ", dayStartBalance);
     }

   // 2. If the limit was already hit today, stay locked
   if(dailyLimitHit) return true;

   // 3. Calculate current drawdown against the starting balance
   double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
   
   if(currentEquity < dayStartBalance)
     {
      double drawdownAmount = dayStartBalance - currentEquity;
      double currentDrawdownPercent = (drawdownAmount / dayStartBalance) * 100.0;
      
      if(currentDrawdownPercent >= MaxDailyDrawdown)
        {
         Print("WARNING: Daily Drawdown Limit (", MaxDailyDrawdown, "%) Hit! Trading locked for today.");
         dailyLimitHit = true; 
         return true;
        }
     }

   return false;
  }

Re: 🛢️ High-Volume Oil (WTI) Scalping Strategy – 18 Years of Price Action Distilled

Posted: Mon Aug 31, 2026 9:32 pm
by PTScalper
Update OnTick() to Enforce the Lockout

Inside your OnTick() function, you must place the drawdown check after ManageTrailingStop(). You still want the EA to trail stops and lock in profits on existing trades, even if it is banned from opening new ones.

Code: Select all

void OnTick()
  {
   // 1. Manage open positions (Trailing Stop must always run)
   ManageTrailingStop();
   
   // 2. Check Daily Drawdown Limit
   // If hit, exit OnTick here so no new trades evaluate
   if(IsDailyDrawdownHit()) return; 

   // 3. Check time filter
   MqlDateTime timeStruct;
   TimeCurrent(timeStruct);
   bool isTradingTime = (timeStruct.hour >= TradeStartHour && timeStruct.hour < TradeEndHour);
   if(!isTradingTime) return;

   // 4. Prevent multiple positions (Count only this EA's positions)
   if(PositionsTotalThisEA() > 0) return;

   // ... rest of your indicator and entry logic continues here ...

Re: 🛢️ High-Volume Oil (WTI) Scalping Strategy – 18 Years of Price Action Distilled

Posted: Mon Aug 31, 2026 9:33 pm
by PTScalper
To instantly liquidate open trades when your drawdown threshold is breached, we need to introduce a dedicated function that sweeps through the terminal, identifies the positions owned by this specific EA, and fires market-close orders.

In MT5, when you close a position, the total index size of the active positions array shrinks. To prevent the loop from skipping trades due to shifting indexes, the liquidation loop must iterate backwards.

Here is how to update your script.

Add the Liquidation Function
Place this new function anywhere outside of OnTick(). It utilizes the existing CTrade object to rapidly close the positions matching your symbol and Magic Number.

Code: Select all

//+------------------------------------------------------------------+
//| Custom Function: Force Close All EA Positions                    |
//+------------------------------------------------------------------+
void CloseAllPositions()
  {
   // Iterate backwards to avoid index shifting as positions are removed
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);
      
      // Ensure we only close trades opened by this specific EA on this chart
      if(PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber)
        {
         // Send the market close request
         if(trade.PositionClose(ticket))
           {
            Print("Emergency Close: Position ticket ", ticket, " successfully liquidated.");
           }
         else
           {
            Print("Error closing ticket ", ticket, ". Code: ", GetLastError());
           }
        }
     }
  }
Update the Drawdown Function
Now, inject CloseAllPositions() directly into your existing IsDailyDrawdownHit() function. This guarantees the liquidation fires the exact millisecond the equity drops below your limit, before locking the EA out for the rest of the day.

Code: Select all

//+------------------------------------------------------------------+
//| Custom Function: Check Daily Drawdown Limit                      |
//+------------------------------------------------------------------+
bool IsDailyDrawdownHit()
  {
   if(MaxDailyDrawdown <= 0) return false; 

   MqlDateTime timeStruct;
   TimeCurrent(timeStruct);

   // 1. Detect a new server day
   if(timeStruct.day_of_year != currentDay)
     {
      currentDay = timeStruct.day_of_year;
      dayStartBalance = AccountInfoDouble(ACCOUNT_BALANCE); 
      dailyLimitHit = false; 
      Print("New day started. Starting balance snapshot: ", dayStartBalance);
     }

   // 2. If the limit was already hit today, stay locked
   if(dailyLimitHit) return true;

   // 3. Calculate current drawdown against the starting balance
   double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
   
   if(currentEquity < dayStartBalance)
     {
      double drawdownAmount = dayStartBalance - currentEquity;
      double currentDrawdownPercent = (drawdownAmount / dayStartBalance) * 100.0;
      
      // 4. Trigger Liquidation Protocol
      if(currentDrawdownPercent >= MaxDailyDrawdown)
        {
         Print("CRITICAL: Daily Drawdown Limit (", MaxDailyDrawdown, "%) breached at ", currentDrawdownPercent, "%!");
         
         CloseAllPositions(); // Fire market close orders immediately
         dailyLimitHit = true; // Lock out further trading
         
         return true;
        }
     }

   return false;
  }

Re: 🛢️ High-Volume Oil (WTI) Scalping Strategy – 18 Years of Price Action Distilled

Posted: Mon Aug 31, 2026 9:34 pm
by PTScalper
To add a lightweight, non-intrusive dashboard directly to your MT5 chart, you can use built-in MQL5 graphical objects (OBJ_LABEL).

Labels are anchored to the screen coordinates rather than the chart price, meaning they stay perfectly in place even when you scroll or zoom. Because rendering graphics can sometimes consume CPU, we want to keep the code efficient so it doesn't slow down your scalping execution.

Here is how to build and integrate the dashboard.

Add the Dashboard Functions
Place these three new functions at the bottom of your script. They handle creating the text objects, updating the live data, and cleaning up the chart if you remove the EA.

Code: Select all

//+------------------------------------------------------------------+
//| Dashboard: Create Text Label Helper                              |
//+------------------------------------------------------------------+
void CreateLabel(string name, string text, int x, int y, color clr, int fontSize = 10)
  {
   ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
   ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetString(0, name, OBJPROP_FONT, "Arial");
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize);
   ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
  }

//+------------------------------------------------------------------+
//| Dashboard: Initialize the UI                                     |
//+------------------------------------------------------------------+
void InitDashboard()
  {
   CreateLabel("Dash_Title", "forex-scalping.com Scalper", 20, 20, clrGold, 12);
   CreateLabel("Dash_Equity", "Equity: --", 20, 40, clrWhite);
   CreateLabel("Dash_Spread", "Spread: --", 20, 55, clrWhite);
   CreateLabel("Dash_DD", "Daily DD: --", 20, 70, clrLime);
  }

//+------------------------------------------------------------------+
//| Dashboard: Update Live Data                                      |
//+------------------------------------------------------------------+
void UpdateDashboard()
  {
   // Get Equity & Spread
   double equity = AccountInfoDouble(ACCOUNT_EQUITY);
   long spread = SymbolInfoInteger(_Symbol, SYMBOL_SPREAD);
   
   // Calculate Current Drawdown
   double ddPercent = 0.0;
   if(dayStartBalance > 0 && equity < dayStartBalance)
     {
      ddPercent = ((dayStartBalance - equity) / dayStartBalance) * 100.0;
     }

   // Dynamic color coding for Drawdown (Turns red if near the limit)
   color ddColor = clrLime;
   if(ddPercent > (MaxDailyDrawdown * 0.75)) ddColor = clrOrange;
   if(ddPercent >= MaxDailyDrawdown) ddColor = clrRed;

   // Update Text Strings
   ObjectSetString(0, "Dash_Equity", OBJPROP_TEXT, StringFormat("Equity: %.2f", equity));
   ObjectSetString(0, "Dash_Spread", OBJPROP_TEXT, StringFormat("Spread: %d points", spread));
   ObjectSetString(0, "Dash_DD", OBJPROP_TEXT, StringFormat("Daily DD: %.2f%%", ddPercent));
   
   ObjectSetInteger(0, "Dash_DD", OBJPROP_COLOR, ddColor);
   
   // Force the chart to redraw and apply the changes immediately
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Dashboard: Cleanup on Exit                                       |
//+------------------------------------------------------------------+
void CleanupDashboard()
  {
   ObjectDelete(0, "Dash_Title");
   ObjectDelete(0, "Dash_Equity");
   ObjectDelete(0, "Dash_Spread");
   ObjectDelete(0, "Dash_DD");
   ChartRedraw();
  }

Re: 🛢️ High-Volume Oil (WTI) Scalping Strategy – 18 Years of Price Action Distilled

Posted: Mon Aug 31, 2026 9:34 pm
by PTScalper
Connect the Functions to the EA Core
Now, you simply need to call these functions inside the three main event handlers of your EA (OnInit, OnDeinit, and OnTick).

Update the top section of your EA to look like this:

Code: Select all

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   trade.SetExpertMagicNumber(MagicNumber);

   // Initialize indicator handles
   handleFastEMA = iMA(_Symbol, PERIOD_CURRENT, FastEMA, 0, MODE_EMA, PRICE_CLOSE);
   handleSlowEMA = iMA(_Symbol, PERIOD_CURRENT, SlowEMA, 0, MODE_EMA, PRICE_CLOSE);
   handleRSI = iRSI(_Symbol, PERIOD_CURRENT, RSIPeriod, PRICE_CLOSE);

   if(handleFastEMA == INVALID_HANDLE || handleSlowEMA == INVALID_HANDLE || handleRSI == INVALID_HANDLE)
     {
      Print("Error: Failed to initialize indicators.");
      return(INIT_FAILED);
     }
     
   // --- ADD THIS LINE ---
   InitDashboard(); 
     
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   IndicatorRelease(handleFastEMA);
   IndicatorRelease(handleSlowEMA);
   IndicatorRelease(handleRSI);
   
   // --- ADD THIS LINE ---
   CleanupDashboard(); 
  }

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   // --- ADD THIS LINE ---
   UpdateDashboard(); 

   // 1. Manage open positions (Trailing Stop)
   ManageTrailingStop();
   
   // 2. Check Daily Drawdown Limit
   if(IsDailyDrawdownHit()) return; 

   // ... rest of your OnTick logic continues as normal ...
How the Visuals Work
Location: The dashboard will appear cleanly in the top-left corner of the chart, offset by 20 pixels so it doesn't overlap with the MT5 one-click trading panel.

Dynamic Colors: The Daily DD text stays green by default. If your drawdown hits 75% of your maximum allowed limit, it turns orange as a warning. If you breach the limit and trigger the liquidation protocol, it turns red.

Ghost Objects: CleanupDashboard() ensures that if you detach the EA from the chart, it wipes the text away so your charting workspace remains perfectly clean.

Re: 🛢️ High-Volume Oil (WTI) Scalping Strategy – 18 Years of Price Action Distilled

Posted: Mon Aug 31, 2026 9:35 pm
by PTScalper
Moving from the MetaQuotes ecosystem to cTrader (cAlgo) is a significant architectural shift. Because you are using C#, you get access to a much more modern, event-driven API.

In cTrader, you don't need to manually calculate point conversions or manage arrays for indicators. The cAlgo API natively handles pip values, position management, and chart graphics with built-in objects.

Here is the complete, compiled C# cBot for forex-scalping.com, integrating the EMA/RSI logic, step trailing stop, spread filter, daily drawdown limit, and the on-chart dashboard.

Code: Select all

using System;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Indicators;
using cAlgo.API.Internals;

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class OilScalperBot : Robot
    {
        // --- Risk Management Inputs
        [Parameter("Risk Per Trade (%)", Group = "Risk Management", DefaultValue = 1.0, MinValue = 0.1)]
        public double RiskPercent { get; set; }

        [Parameter("Stop Loss (Pips)", Group = "Risk Management", DefaultValue = 20.0)]
        public double StopLossPips { get; set; }

        [Parameter("Take Profit (Pips)", Group = "Risk Management", DefaultValue = 40.0)]
        public double TakeProfitPips { get; set; }

        // --- Indicator Settings
        [Parameter("Fast EMA", Group = "Indicators", DefaultValue = 20)]
        public int FastEmaPeriod { get; set; }

        [Parameter("Slow EMA", Group = "Indicators", DefaultValue = 50)]
        public int SlowEmaPeriod { get; set; }

        [Parameter("RSI Period", Group = "Indicators", DefaultValue = 14)]
        public int RsiPeriod { get; set; }

        // --- Session Time Filter
        [Parameter("Trade Start Hour", Group = "Session Filter", DefaultValue = 14)]
        public int TradeStartHour { get; set; }

        [Parameter("Trade End Hour", Group = "Session Filter", DefaultValue = 17)]
        public int TradeEndHour { get; set; }

        // --- Trailing Stop Settings
        [Parameter("Trailing Stop (Pips)", Group = "Trailing Stop", DefaultValue = 15.0)]
        public double TrailingStopPips { get; set; }

        [Parameter("Trailing Step (Pips)", Group = "Trailing Stop", DefaultValue = 5.0)]
        public double TrailingStepPips { get; set; }

        // --- Filters & Safety
        [Parameter("Max Spread (Pips)", Group = "Filters", DefaultValue = 4.0)]
        public double MaxSpreadPips { get; set; }

        [Parameter("Max Daily Drawdown (%)", Group = "Filters", DefaultValue = 5.0)]
        public double MaxDailyDrawdown { get; set; }

        // --- Globals
        private ExponentialMovingAverage _fastEma;
        private ExponentialMovingAverage _slowEma;
        private RelativeStrengthIndex _rsi;
        
        private const string BotLabel = "OilScalp_ForexScalping";
        private double _dayStartBalance;
        private int _currentDay = -1;
        private bool _dailyLimitHit = false;

        protected override void OnStart()
        {
            // Initialize indicators
            _fastEma = Indicators.ExponentialMovingAverage(Bars.ClosePrices, FastEmaPeriod);
            _slowEma = Indicators.ExponentialMovingAverage(Bars.ClosePrices, SlowEmaPeriod);
            _rsi = Indicators.RelativeStrengthIndex(Bars.ClosePrices, RsiPeriod);

            // Set initial day state
            _currentDay = Server.Time.DayOfYear;
            _dayStartBalance = Account.Balance;
        }

        protected override void OnTick()
        {
            UpdateDashboard();

            ManageTrailingStop();

            if (IsDailyDrawdownHit())
                return;

            // Check Session Filter
            int currentHour = Server.Time.Hour;
            bool isTradingTime = (currentHour >= TradeStartHour && currentHour < TradeEndHour);
            if (!isTradingTime)
                return;

            // Prevent multiple positions
            var activePositions = Positions.FindAll(BotLabel, SymbolName);
            if (activePositions.Length > 0)
                return;

            // Check Spread Filter
            double currentSpreadPips = Symbol.Spread / Symbol.PipSize;
            if (currentSpreadPips > MaxSpreadPips)
                return;

            // Retrieve Indicator Values (Last(0) is current forming candle, Last(1) is last closed)
            double fastEma0 = _fastEma.Result.Last(0);
            double fastEma1 = _fastEma.Result.Last(1);
            
            double slowEma0 = _slowEma.Result.Last(0);
            double slowEma1 = _slowEma.Result.Last(1);
            
            double currentRsi = _rsi.Result.Last(0);

            // Buy Condition
            if (fastEma1 <= slowEma1 && fastEma0 > slowEma0 && currentRsi > 50)
            {
                double volume = CalculateVolume();
                if (volume > 0)
                    ExecuteMarketOrder(TradeType.Buy, SymbolName, volume, BotLabel, StopLossPips, TakeProfitPips);
            }

            // Sell Condition
            if (fastEma1 >= slowEma1 && fastEma0 < slowEma0 && currentRsi < 50)
            {
                double volume = CalculateVolume();
                if (volume > 0)
                    ExecuteMarketOrder(TradeType.Sell, SymbolName, volume, BotLabel, StopLossPips, TakeProfitPips);
            }
        }

        private double CalculateVolume()
        {
            double riskAmount = Account.Equity * (RiskPercent / 100.0);
            // cTrader natively calculates the exact monetary value of 1 pip for the requested symbol
            double lossPerUnit = StopLossPips * Symbol.PipValue; 

            if (lossPerUnit <= 0) 
                return 0;

            double rawVolume = riskAmount / lossPerUnit;
            // Normalize to broker's allowed volume steps
            return Symbol.NormalizeVolumeInUnits(rawVolume, RoundingMode.Down);
        }

        private void ManageTrailingStop()
        {
            var botPositions = Positions.FindAll(BotLabel, SymbolName);

            foreach (var position in botPositions)
            {
                if (position.TradeType == TradeType.Buy)
                {
                    double distanceInPips = (Symbol.Bid - position.EntryPrice) / Symbol.PipSize;
                    if (distanceInPips > TrailingStopPips)
                    {
                        double newStopLossPrice = Symbol.Bid - (TrailingStopPips * Symbol.PipSize);
                        
                        // Check if we need to apply the Step
                        if (!position.StopLoss.HasValue || position.StopLoss.Value < Symbol.Bid - ((TrailingStopPips + TrailingStepPips) * Symbol.PipSize))
                        {
                            ModifyPosition(position, newStopLossPrice, position.TakeProfit);
                        }
                    }
                }
                else if (position.TradeType == TradeType.Sell)
                {
                    double distanceInPips = (position.EntryPrice - Symbol.Ask) / Symbol.PipSize;
                    if (distanceInPips > TrailingStopPips)
                    {
                        double newStopLossPrice = Symbol.Ask + (TrailingStopPips * Symbol.PipSize);
                        
                        if (!position.StopLoss.HasValue || position.StopLoss.Value > Symbol.Ask + ((TrailingStopPips + TrailingStepPips) * Symbol.PipSize))
                        {
                            ModifyPosition(position, newStopLossPrice, position.TakeProfit);
                        }
                    }
                }
            }
        }

        private bool IsDailyDrawdownHit()
        {
            if (MaxDailyDrawdown <= 0) return false;

            // Detect new day
            if (Server.Time.DayOfYear != _currentDay)
            {
                _currentDay = Server.Time.DayOfYear;
                _dayStartBalance = Account.Balance;
                _dailyLimitHit = false;
                Print("New day started. Starting balance snapshot: {0}", _dayStartBalance);
            }

            if (_dailyLimitHit) return true;

            if (Account.Equity < _dayStartBalance)
            {
                double currentDdPercent = ((_dayStartBalance - Account.Equity) / _dayStartBalance) * 100.0;
                
                if (currentDdPercent >= MaxDailyDrawdown)
                {
                    Print("CRITICAL: Daily Drawdown Limit ({0}%) breached at {1}%!", MaxDailyDrawdown, Math.Round(currentDdPercent, 2));
                    
                    CloseAllPositions();
                    _dailyLimitHit = true;
                    return true;
                }
            }

            return false;
        }

        private void CloseAllPositions()
        {
            var botPositions = Positions.FindAll(BotLabel, SymbolName);
            foreach (var position in botPositions)
            {
                ClosePosition(position);
            }
        }

        private void UpdateDashboard()
        {
            double currentDdPercent = 0.0;
            if (_dayStartBalance > 0 && Account.Equity < _dayStartBalance)
            {
                currentDdPercent = ((_dayStartBalance - Account.Equity) / _dayStartBalance) * 100.0;
            }

            Color ddColor = Color.Lime;
            if (currentDdPercent > (MaxDailyDrawdown * 0.75)) ddColor = Color.Orange;
            if (currentDdPercent >= MaxDailyDrawdown) ddColor = Color.Red;

            string dashboardText = $"forex-scalping.com Scalper\n" +
                                   $"Equity: {Math.Round(Account.Equity, 2)}\n" +
                                   $"Spread: {Math.Round(Symbol.Spread / Symbol.PipSize, 1)} pips\n";

            Chart.DrawStaticText("Dash_Main", dashboardText, VerticalAlignment.Top, HorizontalAlignment.Left, Color.White);
            Chart.DrawStaticText("Dash_DD", $"\n\n\nDaily DD: {Math.Round(currentDdPercent, 2)}%", VerticalAlignment.Top, HorizontalAlignment.Left, ddColor);
        }
    }
}
Key Architectural Shifts for cTrader:
Pips vs. Points: Unlike MQL4/5 which forces you to deal with raw points (_Point), cTrader uses Symbol.PipSize and Symbol.PipValue natively. The script's inputs are now defined strictly in Pips for cleaner C# mathematics.

Symbol.NormalizeVolumeInUnits(): cTrader handles the lot step sizing naturally. Dividing the risk amount by Symbol.PipValue gives us the raw units, and the normalization method ensures it complies with the broker's specific increments without manual MathRound logic.

LINQ and Position Filtering: MQL requires backward for loops to safely manage arrays when orders close. In C#, Positions.FindAll(BotLabel, SymbolName) queries the API for an immutable array of positions owned specifically by this cBot, making iteration totally clean and thread-safe.

Native Chart Graphics: cTrader renders UI text natively using Chart.DrawStaticText(). You don't need to create, manage, and delete custom MT5 Object Labels. The API manages the text overlay directly on the UI thread without dragging down execution speeds.

Re: 🛢️ High-Volume Oil (WTI) Scalping Strategy – 18 Years of Price Action Distilled

Posted: Mon Aug 31, 2026 9:35 pm
by PTScalper
Moving this logic to TradingView requires a mindset shift. While MQL and C# process every real-time tick in an infinite loop, Pine Script evaluates historically on candle closes (OHLC data).

TradingView's strategy engine handles a lot of the heavy lifting for us. We don't need to write custom trailing stop loops or risk management math from scratch—Pine Script's strategy.exit and built-in equity variables manage this natively.

Here is the complete forex-scalping.com strategy translated into Pine Script v5.

Code: Select all

//@version=5
strategy("WTI Scalper - forex-scalping.com", overlay=true, calc_on_every_tick=true, initial_capital=10000, default_qty_type=strategy.cash)

// =========================================================================
// INPUTS
// =========================================================================
grp_risk = "Risk Management"
risk_pct = input.float(1.0, title="Risk Per Trade (%)", group=grp_risk, step=0.1)
sl_ticks = input.int(200, title="Stop Loss (Ticks)", group=grp_risk)
tp_ticks = input.int(400, title="Take Profit (Ticks)", group=grp_risk)

grp_ind  = "Indicators"
fast_len = input.int(20, title="Fast EMA", group=grp_ind)
slow_len = input.int(50, title="Slow EMA", group=grp_ind)
rsi_len  = input.int(14, title="RSI Period", group=grp_ind)

grp_sess = "Session & Filters"
sess_str = input.session("1400-1700", title="Trading Session", group=grp_sess)
max_dd   = input.float(5.0, title="Max Daily Drawdown (%)", group=grp_sess, step=0.5)

grp_trail = "Trailing Stop"
trail_pts = input.int(150, title="Trailing Activation (Ticks)", group=grp_trail)
trail_stp = input.int(50, title="Trailing Step (Ticks)", group=grp_trail)

// =========================================================================
// INDICATORS & CONDITIONS
// =========================================================================
fast_ema = ta.ema(close, fast_len)
slow_ema = ta.ema(close, slow_len)
rsi      = ta.rsi(close, rsi_len)

// Time Filter
in_session = not na(time(timeframe.period, sess_str))

// =========================================================================
// DAILY DRAWDOWN TRACKER
// =========================================================================
var float day_start_equity = na
var bool  is_locked_today  = false

// Reset at the start of a new daily session
if ta.change(time("D"))
    day_start_equity := strategy.equity
    is_locked_today  := false

// Calculate live drawdown
current_dd = 0.0
if not na(day_start_equity)
    current_dd := ((day_start_equity - strategy.equity) / day_start_equity) * 100
    
    // Trigger liquidation and lockout
    if current_dd >= max_dd and not is_locked_today
        strategy.close_all(comment="Max DD Hit")
        is_locked_today := true

// =========================================================================
// POSITION SIZING
// =========================================================================
// Calculate how many contracts to buy based on risk % and tick value
risk_amount = strategy.equity * (risk_pct / 100)
tick_value = syminfo.mintick * syminfo.pointvalue
loss_per_contract = sl_ticks * tick_value
qty = loss_per_contract > 0 ? (risk_amount / loss_per_contract) : 0

// =========================================================================
// EXECUTION LOGIC
// =========================================================================
// Entry Conditions
buy_cond  = ta.crossover(fast_ema, slow_ema) and rsi > 50 and in_session and not is_locked_today
sell_cond = ta.crossunder(fast_ema, slow_ema) and rsi < 50 and in_session and not is_locked_today

if buy_cond and strategy.position_size == 0
    strategy.entry("Long", strategy.long, qty=qty)
    strategy.exit("Exit Long", "Long", loss=sl_ticks, profit=tp_ticks, trail_points=trail_pts, trail_offset=trail_stp)

if sell_cond and strategy.position_size == 0
    strategy.entry("Short", strategy.short, qty=qty)
    strategy.exit("Exit Short", "Short", loss=sl_ticks, profit=tp_ticks, trail_points=trail_pts, trail_offset=trail_stp)

// =========================================================================
// ON-CHART DASHBOARD
// =========================================================================
var table dash = table.new(position.top_left, 2, 4, bgcolor=color.new(color.black, 70), border_color=color.gray, border_width=1)

if barstate.islast
    // Color logic for DD
    color dd_color = color.lime
    if current_dd > (max_dd * 0.75)
        dd_color := color.orange
    if current_dd >= max_dd
        dd_color := color.red

    // Headers
    table.cell(dash, 0, 0, "forex-scalping.com", text_color=color.yellow, text_halign=text.align_left)
    table.cell(dash, 1, 0, "Scalper", text_color=color.yellow, text_halign=text.align_right)
    
    // Data
    table.cell(dash, 0, 1, "Live Equity:", text_color=color.white, text_halign=text.align_left)
    table.cell(dash, 1, 1, str.tostring(strategy.equity, "#.##"), text_color=color.white, text_halign=text.align_right)
    
    table.cell(dash, 0, 2, "Daily DD:", text_color=color.white, text_halign=text.align_left)
    table.cell(dash, 1, 2, str.tostring(current_dd, "#.##") + "%", text_color=dd_color, text_halign=text.align_right)
    
    table.cell(dash, 0, 3, "Status:", text_color=color.white, text_halign=text.align_left)
    table.cell(dash, 1, 3, is_locked_today ? "LOCKED" : "ACTIVE", text_color=is_locked_today ? color.red : color.lime, text_halign=text.align_right)
Key Architectural Shifts in Pine Script
Native Trailing Stop Math: Notice how short the strategy.exit() line is. Pine Script natively handles the step-based trailing stop by utilizing trail_points (activation distance) and trail_offset (the step size).

calc_on_every_tick: Because this is a scalping strategy with tight trailing stops, calc_on_every_tick=true is enabled in the header. If you deploy this live, the strategy engine will evaluate the trailing stop intra-bar, exactly like MT5 or cTrader.

The Missing Spread Filter: You'll notice the maximum spread filter is gone. TradingView does not store historical spread data (syminfo.spread does not exist historically). While we could track the real-time spread using close - ask, it would ruin historical backtesting results by generating errors. For TradingView, it's safer to rely on your broker execution settings or use webhooks to route signals to a local MT5/cTrader terminal where the spread filter handles execution.

Re: 🛢️ High-Volume Oil (WTI) Scalping Strategy – 18 Years of Price Action Distilled

Posted: Mon Aug 31, 2026 9:36 pm
by PTScalper
To route these signals, TradingView will act as the analytical "brain," while your local MT4, MT5, or cTrader terminals act purely as execution engines.

The most robust way to do this in Pine Script v5 is to construct a JSON string dynamically and pass it to the alert_message parameter inside your strategy.entry() and strategy.exit() commands. Because your lot size (qty) is calculated dynamically, we will use string concatenation to inject the exact volume into the JSON payload.

The Pine Script Update
Replace the Execution Logic and the Daily Drawdown Tracker sections in your script with this updated code:

Code: Select all

// =========================================================================
// JSON WEBHOOK PAYLOADS
// =========================================================================
// We construct the JSON dynamically so it injects the exact calculated lot size.
// Update the "symbol" and "magic" to match your local terminal's requirements.

string secret_key = "FX_SCALP_888" // Hardcode a security key to authenticate your incoming webhooks
string sym        = "USOIL"

// Format the dynamic volume to 2 decimal places to prevent broker API rejections
string vol_str = str.tostring(qty, "#.##")

string buy_json  = '{"key": "' + secret_key + '", "action": "buy", "symbol": "' + sym + '", "volume": ' + vol_str + '}'
string sell_json = '{"key": "' + secret_key + '", "action": "sell", "symbol": "' + sym + '", "volume": ' + vol_str + '}'
string exit_json = '{"key": "' + secret_key + '", "action": "close_all", "symbol": "' + sym + '"}'

// =========================================================================
// DAILY DRAWDOWN TRACKER (Updated with Webhook)
// =========================================================================
var float day_start_equity = na
var bool  is_locked_today  = false

if ta.change(time("D"))
    day_start_equity := strategy.equity
    is_locked_today  := false

current_dd = 0.0
if not na(day_start_equity)
    current_dd := ((day_start_equity - strategy.equity) / day_start_equity) * 100
    
    if current_dd >= max_dd and not is_locked_today
        // Send an explicit alert for the emergency liquidation
        alert(exit_json, alert.freq_once_per_bar_close)
        strategy.close_all(comment="Max DD Hit")
        is_locked_today := true

// =========================================================================
// EXECUTION LOGIC (Updated with Webhook)
// =========================================================================
buy_cond  = ta.crossover(fast_ema, slow_ema) and rsi > 50 and in_session and not is_locked_today
sell_cond = ta.crossunder(fast_ema, slow_ema) and rsi < 50 and in_session and not is_locked_today

if buy_cond and strategy.position_size == 0
    strategy.entry("Long", strategy.long, qty=qty, alert_message=buy_json)
    strategy.exit("Exit Long", "Long", loss=sl_ticks, profit=tp_ticks, trail_points=trail_pts, trail_offset=trail_stp, alert_message=exit_json)

if sell_cond and strategy.position_size == 0
    strategy.entry("Short", strategy.short, qty=qty, alert_message=sell_json)
    strategy.exit("Exit Short", "Short", loss=sl_ticks, profit=tp_ticks, trail_points=trail_pts, trail_offset=trail_stp, alert_message=exit_json)