Volume Trader V2 is a direct conversion of the MetaTrader expert advisor Volume_trader_v2_www_forex-instruments_info.mq4. The original system observes how the total volume of the latest candles evolves and uses this short-term flow to decide whether a simple long or short exposure should be active. The StockSharp port keeps the one-position-at-a-time behaviour, the time-of-day filter and the requirement to act only once per completed candle.
The strategy subscribes to a configurable candle series and caches the volume of the last two finished candles. When a new bar closes, the volumes from the previous two bars (MetaTrader's Volume[1] and Volume[2]) are compared and an updated trade direction is produced:
Volume[1] < Volume[2] generates a long bias.
Volume[1] > Volume[2] generates a short bias.
Equal volumes or disabled trading hours remove any open exposure.
Before sending a new order the current position is flattened if it points in the opposite direction so that the StockSharp implementation matches the MetaTrader order lifecycle.
Parameters
Name
Default
Description
CandleType
5-minute time frame
Data type requested from SubscribeCandles. Set it to match the chart period used in MetaTrader.
StartHour
8
First trading hour (inclusive). Signals outside the window are ignored and any position is closed.
EndHour
20
Last trading hour (inclusive). When the current candle starts after this hour the strategy stays flat.
TradeVolume
0.1
Lot size replicated from the EA. The value is also assigned to Strategy.Volume so helper methods use the same amount.
All parameters are regular StrategyParam<T> instances so they can be optimised or exposed through the UI.
Trading Logic
Handle only finished candles to guarantee bar-by-bar parity with the EA.
Cache Volume[1] and Volume[2] equivalents in _previousVolume and _twoBarsAgoVolume before any signal evaluation.
Validate that the candle start time falls between StartHour and EndHour (inclusive). Outside this range any active position is closed and no new orders are created.
Compute the desired direction:
Long when the most recent volume is lower than the previous bar.
Short when the most recent volume is higher than the previous bar.
Neutral otherwise.
If the desired direction differs from the current position, close the opposite position first (BuyMarket(-Position) or SellMarket(Position)).
Enter the new position using the configured TradeVolume only when the strategy is flat or positioned in the opposite direction.
Update the cached volumes so the next cycle still compares the last two completed candles.
This flow guarantees that no orders are placed while a candle is still building and that the StockSharp strategy reacts exactly once per bar, just like the MetaTrader implementation that relied on LastBarChecked.
Additional Notes
StartProtection() is called in OnStarted to reuse the framework protection helper that keeps track of the current position.
The Comment property mirrors the EA diagnostic messages ("Up trend", "Down trend", "No trend..." or "Trading paused") to simplify monitoring.
The strategy does not maintain extra collections, and it leverages the high-level candle subscription API in line with the project guidelines.
Set the candle type, security and volume to match the instrument and timeframe originally used in MetaTrader for comparable results.
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Volume Trader V2 strategy converted from the MetaTrader expert Volume_trader_v2_www_forex-instruments_info.mq4.
/// Follows the original logic by comparing the volume of the last two finished candles and trading only during configured hours.
/// </summary>
public class VolumeTraderV2Strategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<int> _endHour;
private readonly StrategyParam<decimal> _tradeVolume;
private decimal? _previousVolume;
private decimal? _twoBarsAgoVolume;
/// <summary>
/// Initializes a new instance of the <see cref="VolumeTraderV2Strategy"/> class.
/// </summary>
public VolumeTraderV2Strategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromDays(1).TimeFrame())
.SetDisplay("Candle Type", "Time frame used to request candles", "Data");
_startHour = Param(nameof(StartHour), 0)
.SetDisplay("Start Hour", "First hour (inclusive) when trading is allowed", "Trading")
.SetRange(0, 23);
_endHour = Param(nameof(EndHour), 23)
.SetDisplay("End Hour", "Last hour (inclusive) when trading is allowed", "Trading")
.SetRange(0, 23);
_tradeVolume = Param(nameof(TradeVolume), 0.1m)
.SetDisplay("Trade Volume", "Order volume replicated from the original EA", "Trading")
.SetGreaterThanZero();
Volume = TradeVolume;
}
/// <summary>
/// Candle type used for the strategy calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// First trading hour (inclusive).
/// </summary>
public int StartHour
{
get => _startHour.Value;
set => _startHour.Value = value;
}
/// <summary>
/// Last trading hour (inclusive).
/// </summary>
public int EndHour
{
get => _endHour.Value;
set => _endHour.Value = value;
}
/// <summary>
/// Default order volume for market operations.
/// </summary>
public decimal TradeVolume
{
get => _tradeVolume.Value;
set
{
_tradeVolume.Value = value;
Volume = value;
}
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
// Drop cached volume values so the warm-up sequence matches the EA behavior after a reset.
_previousVolume = null;
_twoBarsAgoVolume = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Subscribe to candles and process them with the same granularity as the original indicator buffers.
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
StartProtection(null, null);
}
private void ProcessCandle(ICandleMessage candle)
{
// Only act on finished candles to replicate the bar-by-bar logic.
if (candle.State != CandleStates.Finished)
return;
var currentVolume = candle.TotalVolume;
// Collect the first two candles before generating signals.
if (_previousVolume is null)
{
_previousVolume = currentVolume;
return;
}
if (_twoBarsAgoVolume is null)
{
_twoBarsAgoVolume = _previousVolume;
_previousVolume = currentVolume;
return;
}
var volume1 = _previousVolume.Value;
var volume2 = _twoBarsAgoVolume.Value;
var hour = candle.OpenTime.Hour;
var hourValid = hour >= StartHour && hour <= EndHour;
var shouldGoLong = hourValid && volume1 < volume2;
var shouldGoShort = hourValid && volume1 > volume2;
var comment = !hourValid
? "Trading paused"
: shouldGoLong
? "Up trend"
: shouldGoShort
? "Down trend"
: "No trend...";
if (!shouldGoLong && !shouldGoShort)
{
// Exit the market when no direction is active (equal volume or outside trading hours).
ClosePosition();
}
else if (shouldGoLong)
{
// Flatten any short position before opening a new long trade.
if (Position < 0)
BuyMarket();
if (Position <= 0)
BuyMarket();
}
else if (shouldGoShort)
{
// Flatten any long position before opening a new short trade.
if (Position > 0)
SellMarket();
if (Position >= 0)
SellMarket();
}
// Shift the cached volumes to emulate Volume[1] and Volume[2] from MetaTrader.
_twoBarsAgoVolume = _previousVolume;
_previousVolume = currentVolume;
}
private void ClosePosition()
{
// Mirror the EA behavior by leaving the market whenever the signal is neutral.
if (Position > 0)
{
SellMarket();
}
else if (Position < 0)
{
BuyMarket();
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class volume_trader_v2_strategy(Strategy):
"""Volume Trader V2: compares volume of the last two finished candles and trades
only during configured hours. Goes long when previous volume < two-bars-ago volume,
short when previous volume > two-bars-ago volume."""
def __init__(self):
super(volume_trader_v2_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromDays(1))) \
.SetDisplay("Candle Type", "Time frame used to request candles", "Data")
self._start_hour = self.Param("StartHour", 0) \
.SetDisplay("Start Hour", "First hour (inclusive) when trading is allowed", "Trading")
self._end_hour = self.Param("EndHour", 23) \
.SetDisplay("End Hour", "Last hour (inclusive) when trading is allowed", "Trading")
self._trade_volume = self.Param("TradeVolume", 0.1) \
.SetGreaterThanZero() \
.SetDisplay("Trade Volume", "Order volume replicated from the original EA", "Trading")
self._previous_volume = None
self._two_bars_ago_volume = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def StartHour(self):
return self._start_hour.Value
@property
def EndHour(self):
return self._end_hour.Value
@property
def TradeVolume(self):
return self._trade_volume.Value
def OnReseted(self):
super(volume_trader_v2_strategy, self).OnReseted()
self._previous_volume = None
self._two_bars_ago_volume = None
def OnStarted2(self, time):
super(volume_trader_v2_strategy, self).OnStarted2(time)
self.Volume = float(self.TradeVolume)
self.StartProtection(None, None)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
current_volume = candle.TotalVolume
if self._previous_volume is None:
self._previous_volume = current_volume
return
if self._two_bars_ago_volume is None:
self._two_bars_ago_volume = self._previous_volume
self._previous_volume = current_volume
return
volume1 = self._previous_volume
volume2 = self._two_bars_ago_volume
hour = candle.OpenTime.Hour
hour_valid = hour >= self.StartHour and hour <= self.EndHour
should_go_long = hour_valid and volume1 < volume2
should_go_short = hour_valid and volume1 > volume2
if not should_go_long and not should_go_short:
# Exit position when no direction is active
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
elif should_go_long:
if self.Position < 0:
self.BuyMarket()
if self.Position <= 0:
self.BuyMarket()
elif should_go_short:
if self.Position > 0:
self.SellMarket()
if self.Position >= 0:
self.SellMarket()
self._two_bars_ago_volume = self._previous_volume
self._previous_volume = current_volume
def CreateClone(self):
return volume_trader_v2_strategy()