Equal Volume & Range Bars ports the MetaTrader 4 script equalvolumebars.mq4 to StockSharp. The original script generated off-line charts whose candles closed after either a fixed number of ticks or after the price had traversed a configurable point range. The strategy reproduces the same candle-building logic inside the StockSharp environment: it listens to live ticks, optionally preloads historical M1 candles, and emits detailed log entries whenever a synthetic bar is completed.
Candle construction logic
Dual operating modes – EqualVolumeBars closes the bar once the accumulated tick volume exceeds the configured threshold, while RangeBars requires the candle's high-low range (measured in security price steps) to exceed the same numeric threshold.
Tick-driven updates – every trade update refreshes the current candle high, low, close, and tick volume. When the threshold would be exceeded, the strategy finalizes the previous candle with the existing statistics and immediately starts a fresh bar with the current tick as its first entry.
Minute history seeding (optional) – when FromMinuteHistory is enabled, the strategy replays finished M1 candles as a sequence of synthetic ticks (open → intermediate extremes → close). This approximates the offline chart's initialization step without requiring external CSV tick files.
Monotonic timestamps – the builder enforces strictly increasing timestamps so that log consumers or downstream modules can load the data without encountering duplicate time keys.
Parameters
Work Mode – selects between EqualVolumeBars and RangeBars candle construction.
Ticks In Bar – number of ticks per candle (equal volume mode) or point range measured in price steps (range mode).
Use Minute History – enables the synthetic replay of finished M1 candles before live ticks arrive.
Minute Candle Type – candle subscription used for the historical seeding step (defaults to one-minute time frame).
Additional notes
The strategy infers the point size from Security.PriceStep (falling back to Security.MinPriceStep or 0.0001 when no metadata is available) to mirror the _Point constant used by MetaTrader.
Instead of writing .hst files and refreshing a chart window, the C# port logs every finished candle with full OHLCV data, making it easy to feed another component or to compare results with the MT4 offline chart builder.
No orders are ever submitted; the class focuses exclusively on data transformation just like the original script.
Only the C# version is provided. A Python version and folder are intentionally omitted per the conversion requirements.
using System;
using System.Collections.Generic;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Equal Volume Range Bars strategy - ATR-based volatility breakout.
/// Buys when close breaks above EMA + ATR band.
/// Sells when close breaks below EMA - ATR band.
/// </summary>
public class EqualVolumeRangeBarsStrategy : Strategy
{
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<DataType> _candleType;
public int EmaPeriod { get => _emaPeriod.Value; set => _emaPeriod.Value = value; }
public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
public decimal AtrMultiplier { get => _atrMultiplier.Value; set => _atrMultiplier.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public EqualVolumeRangeBarsStrategy()
{
_emaPeriod = Param(nameof(EmaPeriod), 20)
.SetDisplay("EMA Period", "EMA lookback", "Indicators");
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetDisplay("ATR Period", "ATR lookback", "Indicators");
_atrMultiplier = Param(nameof(AtrMultiplier), 2m)
.SetDisplay("ATR Multiplier", "ATR band multiplier", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candle timeframe", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];
protected override void OnReseted() { base.OnReseted(); }
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var ema = new ExponentialMovingAverage { Length = EmaPeriod };
var atr = new AverageTrueRange { Length = AtrPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ema, atr, ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal ema, decimal atr)
{
if (candle.State != CandleStates.Finished)
return;
var close = candle.ClosePrice;
var upper = ema + atr * AtrMultiplier;
var lower = ema - atr * AtrMultiplier;
if (close > upper && Position <= 0)
{
if (Position < 0)
BuyMarket();
BuyMarket();
}
else if (close < lower && Position >= 0)
{
if (Position > 0)
SellMarket();
SellMarket();
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class equal_volume_range_bars_strategy(Strategy):
def __init__(self):
super(equal_volume_range_bars_strategy, self).__init__()
self._ema_period = self.Param("EmaPeriod", 20).SetDisplay("EMA Period", "EMA lookback", "Indicators")
self._atr_period = self.Param("AtrPeriod", 14).SetDisplay("ATR Period", "ATR lookback", "Indicators")
self._atr_multiplier = self.Param("AtrMultiplier", 2.0).SetDisplay("ATR Multiplier", "ATR band multiplier", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))).SetDisplay("Candle Type", "Candle timeframe", "General")
@property
def ema_period(self): return self._ema_period.Value
@property
def atr_period(self): return self._atr_period.Value
@property
def atr_multiplier(self): return self._atr_multiplier.Value
@property
def candle_type(self): return self._candle_type.Value
def OnReseted(self): super(equal_volume_range_bars_strategy, self).OnReseted()
def OnStarted2(self, time):
super(equal_volume_range_bars_strategy, self).OnStarted2(time)
ema = ExponentialMovingAverage(); ema.Length = self.ema_period
atr = AverageTrueRange(); atr.Length = self.atr_period
sub = self.SubscribeCandles(self.candle_type)
sub.Bind(ema, atr, self.process_candle).Start()
def process_candle(self, candle, ema, atr):
if candle.State != CandleStates.Finished: return
close = float(candle.ClosePrice); e = float(ema); a = float(atr)
upper = e + a * self.atr_multiplier; lower = e - a * self.atr_multiplier
if close > upper and self.Position <= 0:
if self.Position < 0: self.BuyMarket()
self.BuyMarket()
elif close < lower and self.Position >= 0:
if self.Position > 0: self.SellMarket()
self.SellMarket()
def CreateClone(self): return equal_volume_range_bars_strategy()