Advertisement
Share, develop, and backtest custom MQL4/MQL5 Expert Advisors, Python data-scraping scripts, trading bots, and automated market alert systems.
FTtrader
Posts: 908 Joined: Mon Aug 03, 2026 2:43 pm
Post
by FTtrader » Fri Sep 25, 2026 8:33 am
Code: Select all
//+------------------------------------------------------------------+
//| Session_VWAP_SD_Bands.mq5 |
//| Pavel Tuček |
//+------------------------------------------------------------------+
#property copyright "Pavel Tuček"
#property link ""
#property version "1.00"
#property indicator_chart_window
#property indicator_buffers 7
#property indicator_plots 7
//--- plot properties
#property indicator_label1 "VWAP"
#property indicator_type1 DRAW_LINE
#property indicator_color1 clrYellow
#property indicator_style1 STYLE_SOLID
#property indicator_width1 2
#property indicator_label2 "Upper 2.0 SD"
#property indicator_type2 DRAW_LINE
#property indicator_color2 clrDodgerBlue
#property indicator_style2 STYLE_DASH
#property indicator_label3 "Lower 2.0 SD"
#property indicator_type3 DRAW_LINE
#property indicator_color3 clrDodgerBlue
#property indicator_style3 STYLE_DASH
#property indicator_label4 "Upper 2.5 SD"
#property indicator_type4 DRAW_LINE
#property indicator_color4 clrOrange
#property indicator_style4 STYLE_SOLID
#property indicator_label5 "Lower 2.5 SD"
#property indicator_type5 DRAW_LINE
#property indicator_color5 clrOrange
#property indicator_style5 STYLE_SOLID
#property indicator_label6 "Upper 3.0 SD"
#property indicator_type6 DRAW_LINE
#property indicator_color6 clrRed
#property indicator_style6 STYLE_SOLID
#property indicator_width6 2
#property indicator_label7 "Lower 3.0 SD"
#property indicator_type7 DRAW_LINE
#property indicator_color7 clrRed
#property indicator_style7 STYLE_SOLID
#property indicator_width7 2
//--- indicator buffers
double VWAPBuffer[];
double Upper20Buffer[];
double Lower20Buffer[];
double Upper25Buffer[];
double Lower25Buffer[];
double Upper30Buffer[];
double Lower30Buffer[];
//--- global variables for Welford's algorithm
double g_sum_vol = 0;
double g_vwap = 0;
double g_S = 0;
int g_last_day = -1;
//+------------------------------------------------------------------+
//| Custom indicator initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
SetIndexBuffer(0, VWAPBuffer, INDICATOR_DATA);
SetIndexBuffer(1, Upper20Buffer, INDICATOR_DATA);
SetIndexBuffer(2, Lower20Buffer, INDICATOR_DATA);
SetIndexBuffer(3, Upper25Buffer, INDICATOR_DATA);
SetIndexBuffer(4, Lower25Buffer, INDICATOR_DATA);
SetIndexBuffer(5, Upper30Buffer, INDICATOR_DATA);
SetIndexBuffer(6, Lower30Buffer, INDICATOR_DATA);
IndicatorSetString(INDICATOR_SHORTNAME, "Session VWAP + SD Bands");
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| 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 < 1) return(0);
int start = prev_calculated - 1;
if(start < 0)
{
start = 0;
g_sum_vol = 0;
g_vwap = 0;
g_S = 0;
g_last_day = -1;
}
for(int i = start; i < rates_total; i++)
{
datetime current_time = time[i];
MqlDateTime dt;
TimeToStruct(current_time, dt);
int current_day = dt.day_of_year;
double typ_price = (high[i] + low[i] + close[i]) / 3.0;
double vol = (double)tick_volume[i];
// Reset at the start of a new session (midnight server time)
if(current_day != g_last_day)
{
g_sum_vol = vol;
g_vwap = typ_price;
g_S = 0;
g_last_day = current_day;
}
else
{
g_sum_vol += vol;
if(g_sum_vol > 0)
{
double old_vwap = g_vwap;
g_vwap = old_vwap + (vol / g_sum_vol) * (typ_price - old_vwap);
g_S = g_S + vol * (typ_price - old_vwap) * (typ_price - g_vwap);
}
}
double variance = (g_sum_vol > 0) ? (g_S / g_sum_vol) : 0;
double sd = MathSqrt(variance);
// Populate buffers
VWAPBuffer[i] = g_vwap;
Upper20Buffer[i] = g_vwap + (2.0 * sd);
Lower20Buffer[i] = g_vwap - (2.0 * sd);
Upper25Buffer[i] = g_vwap + (2.5 * sd);
Lower25Buffer[i] = g_vwap - (2.5 * sd);
Upper30Buffer[i] = g_vwap + (3.0 * sd);
Lower30Buffer[i] = g_vwap - (3.0 * sd);
}
return(rates_total);
}
//+------------------------------------------------------------------+
Recommended broker for automated trading & scalping
FTtrader
Posts: 908 Joined: Mon Aug 03, 2026 2:43 pm
Post
by FTtrader » Fri Sep 25, 2026 8:34 am
Implementation Notes
Server Time vs. Local Time: This calculates the session from your broker's midnight (server time). If your broker operates on a standard New York close schedule (EET/EST structure), this aligns perfectly out of the box.
Tick Volume Integration: It uses tick_volume[] naturally. Because it resets strictly at the day flip, it prevents historical tick data from skewing the active session valuation.
Array Formatting: The prev_calculated logic handles tick-by-tick updates efficiently. During active market hours, the for loop only executes on the current index i, updating the running Welford totals in milliseconds.
FTtrader
Posts: 908 Joined: Mon Aug 03, 2026 2:43 pm
Post
by FTtrader » Fri Sep 25, 2026 8:35 am
Translating this to cTrader requires a fundamental architectural shift. Because cTrader’s Calculate(int index) method executes on every single tick for the active bar, using global variables for Welford’s running totals (like we did in MQL5) will double-count the current bar's volume thousands of times.
To make the calculation idempotent, this C# implementation stores the cumulative running totals inside IndicatorDataSeries. This ensures that tick-by-tick updates recalculate safely from the previous bar's finalized state without corrupting the math.
Code: Select all
using System;
using cAlgo.API;
using cAlgo.API.Internals;
namespace cAlgo
{
[Indicator(IsOverlay = true, TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
public class SessionVWAP_SDBands : Indicator
{
[Output("VWAP", LineColor = "Yellow", Thickness = 2)]
public IndicatorDataSeries Vwap { get; set; }
[Output("Upper 2.0 SD", LineColor = "DodgerBlue", LineStyle = LineStyle.Lines)]
public IndicatorDataSeries Upper20 { get; set; }
[Output("Lower 2.0 SD", LineColor = "DodgerBlue", LineStyle = LineStyle.Lines)]
public IndicatorDataSeries Lower20 { get; set; }
[Output("Upper 2.5 SD", LineColor = "Orange", Thickness = 1)]
public IndicatorDataSeries Upper25 { get; set; }
[Output("Lower 2.5 SD", LineColor = "Orange", Thickness = 1)]
public IndicatorDataSeries Lower25 { get; set; }
[Output("Upper 3.0 SD", LineColor = "Red", Thickness = 2)]
public IndicatorDataSeries Upper30 { get; set; }
[Output("Lower 3.0 SD", LineColor = "Red", Thickness = 2)]
public IndicatorDataSeries Lower30 { get; set; }
// DataSeries used to hold Welford's running totals safely across tick updates
private IndicatorDataSeries _sumVol;
private IndicatorDataSeries _S;
protected override void Initialize()
{
_sumVol = CreateDataSeries();
_S = CreateDataSeries();
}
public override void Calculate(int index)
{
double typPrice = (Bars.HighPrices[index] + Bars.LowPrices[index] + Bars.ClosePrices[index]) / 3.0;
double vol = Bars.TickVolumes[index];
// Reset condition based on calendar day rollover
bool isNewDay = index == 0 || Bars.OpenTimes[index].Date != Bars.OpenTimes[index - 1].Date;
if (isNewDay)
{
_sumVol[index] = vol;
Vwap[index] = typPrice;
_S[index] = 0;
}
else
{
// Pull the finalized values from the previous bar
double prevSumVol = _sumVol[index - 1];
double prevVwap = Vwap[index - 1];
double prevS = _S[index - 1];
_sumVol[index] = prevSumVol + vol;
if (_sumVol[index] > 0)
{
Vwap[index] = prevVwap + (vol / _sumVol[index]) * (typPrice - prevVwap);
_S[index] = prevS + vol * (typPrice - prevVwap) * (typPrice - Vwap[index]);
}
else
{
Vwap[index] = prevVwap;
_S[index] = prevS;
}
}
double variance = _sumVol[index] > 0 ? _S[index] / _sumVol[index] : 0;
double sd = Math.Sqrt(variance);
Upper20[index] = Vwap[index] + (2.0 * sd);
Lower20[index] = Vwap[index] - (2.0 * sd);
Upper25[index] = Vwap[index] + (2.5 * sd);
Lower25[index] = Vwap[index] - (2.5 * sd);
Upper30[index] = Vwap[index] + (3.0 * sd);
Lower30[index] = Vwap[index] - (3.0 * sd);
}
}
}
FTtrader
Posts: 908 Joined: Mon Aug 03, 2026 2:43 pm
Post
by FTtrader » Fri Sep 25, 2026 8:35 am
cAlgo Implementation Notes
Idempotent Ticks: Because _sumVol[index] and _S[index] fetch their baseline from index - 1, the current bar can update 10,000 times during an active 1-minute candle without breaking the cumulative session variance.
Time Zones: The indicator is decorated with TimeZone = TimeZones.UTC. This ensures the .Date rollover behaves consistently across different brokers. If you prefer the bands to reset exactly at the 5:00 PM EST New York close rather than midnight UTC, you can switch the timezone property to TimeZones.EasternStandardTime.
FTtrader
Posts: 908 Joined: Mon Aug 03, 2026 2:43 pm
Post
by FTtrader » Fri Sep 25, 2026 8:36 am
To automate this in cTrader, you build a cBot that hooks into the custom SessionVWAP_SDBands indicator we just wrote.
Because you want to trade a structural rejection rather than a blind touch, the logic must execute inside the OnBar method. This guarantees the 1-minute or 5-minute candle has officially closed, confirming the liquidity sweep (the wick) before committing capital.
Here is the C# implementation for the automated fade.
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 VWAPMeanReversionBot : Robot
{
[Parameter("Trade Volume (Lots)", DefaultValue = 0.1, MinValue = 0.01)]
public double TradeVolume { get; set; }
[Parameter("Stop Loss Buffer (Pips)", DefaultValue = 1.0, MinValue = 0.0)]
public double SlBufferPips { get; set; }
// Reference to the custom indicator
private SessionVWAP_SDBands _vwapIndicator;
private string _label = "VWAP_Fade_Bot";
protected override void Initialize()
{
// Load the custom indicator we built previously
// Note: You must add the indicator as a reference in the cBot manager
_vwapIndicator = Indicators.GetIndicator<SessionVWAP_SDBands>();
}
protected override void OnBar()
{
// Prevent pyramiding; only take one setup at a time
if (Positions.Count(p => p.Label == _label && p.SymbolName == SymbolName) > 0)
return;
// Analyze the previously closed candle (index 1)
int index = Bars.Count - 2;
double high = Bars.HighPrices[index];
double low = Bars.LowPrices[index];
double close = Bars.ClosePrices[index];
double open = Bars.OpenPrices[index];
double upper25 = _vwapIndicator.Upper25[index];
double lower25 = _vwapIndicator.Lower25[index];
double vwap = _vwapIndicator.Vwap[index];
// ---------------------------------------------------------
// SHORT SETUP (Fade the Upper Band)
// Condition 1: Wick sweeps above the 2.5 SD band
// Condition 2: Candle closes bearish (Close < Open)
// ---------------------------------------------------------
if (high >= upper25 && close < open && close < upper25)
{
// Dynamic SL: Sweep High + Buffer
double stopLossPrice = high + (SlBufferPips * Symbol.PipSize);
double slDistancePips = (stopLossPrice - close) / Symbol.PipSize;
// Dynamic TP: Return to VWAP
double tpDistancePips = (close - vwap) / Symbol.PipSize;
// Only take the trade if the risk-to-reward makes sense (e.g., TP > SL)
if (tpDistancePips > slDistancePips)
{
ExecuteMarketOrder(TradeType.Sell, SymbolName, Symbol.QuantityToVolumeInUnits(TradeVolume), _label, slDistancePips, tpDistancePips);
}
}
// ---------------------------------------------------------
// LONG SETUP (Fade the Lower Band)
// Condition 1: Wick sweeps below the 2.5 SD band
// Condition 2: Candle closes bullish (Close > Open)
// ---------------------------------------------------------
if (low <= lower25 && close > open && close > lower25)
{
// Dynamic SL: Sweep Low - Buffer
double stopLossPrice = low - (SlBufferPips * Symbol.PipSize);
double slDistancePips = (close - stopLossPrice) / Symbol.PipSize;
// Dynamic TP: Return to VWAP
double tpDistancePips = (vwap - close) / Symbol.PipSize;
if (tpDistancePips > slDistancePips)
{
ExecuteMarketOrder(TradeType.Buy, SymbolName, Symbol.QuantityToVolumeInUnits(TradeVolume), _label, slDistancePips, tpDistancePips);
}
}
}
}
}
FTtrader
Posts: 908 Joined: Mon Aug 03, 2026 2:43 pm
Post
by FTtrader » Fri Sep 25, 2026 8:36 am
Architectural Details for cTrader
Indicator Referencing: Because SessionVWAP_SDBands is a custom indicator, the cBot cannot natively see it. In cTrader Automate, you must right-click your cBot, select Manage References, navigate to the Indicators tab, and check the box next to your VWAP indicator before this will compile.
The "Index 1" Shift: The OnBar event fires the millisecond a new candle opens. Therefore, the setup is evaluated on Bars.Count - 2 (the candle that just closed). If you evaluate Bars.Count - 1, you are reading the open price of the brand-new, empty candle.
Dynamic Targeting: The ExecuteMarketOrder method in cAlgo requires Stop Loss and Take Profit to be passed as pip distances, not absolute price levels. The code dynamically calculates the pip distance from the entry (the close of the rejection candle) to the sweep wick (SL) and the VWAP line (TP).
Built-in R:R Filter: The logic includes a hard filter if (tpDistancePips > slDistancePips). If the 2.5 SD band is sitting too close to the VWAP, or if the rejection wick is unusually massive, the risk-to-reward ratio inverts. The bot will automatically pass on the trade rather than risking 15 pips to make 4.