Port of the MetaTrader 5 expert advisor "Volume trader" (ID 21050) by Vladimir Karputov.
Recreated on top of the StockSharp high level strategy API.
Trades in the direction of the latest tick volume change while a custom trading session filter is active.
Trading logic
Subscribes to candles defined by CandleType (default: 1-hour time frame) and reads their tick volume (TotalVolume).
On every finished candle the strategy compares the volumes of the two previous closed candles, mimicking the MQL5 script that runs at the birth of a new bar.
If the more recent volume is higher than the one before it and there is no long position, the strategy buys Volume contracts and additionally covers an existing short position.
If the more recent volume is lower than the one before it and there is no short position, the strategy sells Volume contracts and additionally closes an existing long position.
Trading signals are ignored when the opening time of the next bar falls outside the [StartHour, EndHour] window. The default range 09:00–18:00 replicates the original inputs.
No stop loss or take profit is defined by default; the strategy simply reverses on the opposite signal.
Order management
Entry orders are sent via BuyMarket or SellMarket to flip the position immediately at the start of a new candle.
When a reversal signal appears, the strategy automatically trades the absolute position size plus the configured Volume, ensuring the previous position is closed before a new one opens.
There is no built-in position sizing logic besides the fixed Volume parameter.
Parameters
Parameter
Default
Description
CandleType
1-hour time frame
Candle series used to calculate tick volume. Adjust to match the timeframe used in the original expert.
StartHour
9
Inclusive hour (0–23) that marks the beginning of the trading session. Signals before this hour are ignored.
EndHour
18
Inclusive hour (0–23) that marks the end of the trading session. Signals after this hour are ignored.
Volume
0.1
Order volume for new entries. Also used when flipping an existing position.
Usage notes
Ensure that the data source provides tick volume in the candle messages. When only real traded volume is available, the behaviour will follow that data instead.
Align the CandleType parameter with the chart timeframe you intend to reproduce from MetaTrader.
Consider wrapping the strategy with external risk management (stop loss, take profit, daily loss limits) if required by your trading rules.
The strategy calls LogInfo when a position is opened, making it easier to audit signal decisions in the log.
Differences vs. original MQL implementation
Uses StockSharp's candle subscription pipeline instead of manually calling CopyTickVolume.
Session filtering relies on the CloseTime of the finished candle (the start time of the next bar) to stay aligned with the MQL logic that executes at bar opening.
Order execution is handled through high level API helpers (BuyMarket, SellMarket) rather than direct CTrade calls.
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 based reversal strategy that reacts to increasing or decreasing tick volume.
/// </summary>
public class VolumeTraderStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<int> _endHour;
private decimal? _previousVolume;
private decimal? _previousPreviousVolume;
/// <summary>
/// Candle type used to calculate the signals.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Inclusive start hour of the trading session.
/// </summary>
public int StartHour
{
get => _startHour.Value;
set => _startHour.Value = value;
}
/// <summary>
/// Inclusive end hour of the trading session.
/// </summary>
public int EndHour
{
get => _endHour.Value;
set => _endHour.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="VolumeTraderStrategy"/>.
/// </summary>
public VolumeTraderStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for signal calculation", "General");
_startHour = Param(nameof(StartHour), 9)
.SetDisplay("Start Hour", "Inclusive start hour for trading", "Session")
.SetRange(0, 23);
_endHour = Param(nameof(EndHour), 18)
.SetDisplay("End Hour", "Inclusive end hour for trading", "Session")
.SetRange(0, 23);
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousVolume = null;
_previousPreviousVolume = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
// Wait until the candle is finished to avoid partial data.
if (candle.State != CandleStates.Finished)
return;
var currentVolume = candle.TotalVolume;
if (_previousVolume.HasValue && _previousPreviousVolume.HasValue)
{
// MQL version trades at the open of the next bar, so use the next bar time for the filter.
var nextBarTime = candle.CloseTime;
var hour = nextBarTime.Hour;
var inSession = hour >= StartHour && hour <= EndHour;
if (inSession && IsFormedAndOnlineAndAllowTrading())
{
var prevVolume = _previousVolume.Value;
var prevPrevVolume = _previousPreviousVolume.Value;
// Rising volume suggests upward pressure -> go long.
if (prevVolume > prevPrevVolume * 1.1m && Position <= 0)
{
var volumeToTrade = Volume + (Position < 0 ? Math.Abs(Position) : 0m);
if (volumeToTrade > 0)
{
BuyMarket(volumeToTrade);
LogInfo($"Volume increased from {prevPrevVolume} to {prevVolume}. Opening long position.");
}
}
// Falling volume suggests weakening demand -> go short.
else if (prevVolume < prevPrevVolume * 0.9m && Position >= 0)
{
var volumeToTrade = Volume + (Position > 0 ? Math.Abs(Position) : 0m);
if (volumeToTrade > 0)
{
SellMarket(volumeToTrade);
LogInfo($"Volume decreased from {prevPrevVolume} to {prevVolume}. Opening short position.");
}
}
}
}
// Shift stored volumes so the latest closed candle becomes the previous reference.
_previousPreviousVolume = _previousVolume;
_previousVolume = currentVolume;
}
}
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.Strategies import Strategy
from datatype_extensions import *
from indicator_extensions import *
class volume_trader_strategy(Strategy):
"""Volume-based reversal: rising volume suggests long, falling volume suggests short."""
def __init__(self):
super(volume_trader_strategy, self).__init__()
self._start_hour = self.Param("StartHour", 9).SetDisplay("Start Hour", "Trading session start", "Session")
self._end_hour = self.Param("EndHour", 18).SetDisplay("End Hour", "Trading session end", "Session")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))).SetDisplay("Candle Type", "Timeframe", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(volume_trader_strategy, self).OnReseted()
self._prev_vol = None
self._prev_prev_vol = None
def OnStarted2(self, time):
super(volume_trader_strategy, self).OnStarted2(time)
self._prev_vol = None
self._prev_prev_vol = None
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
vol = float(candle.TotalVolume)
if self._prev_vol is not None and self._prev_prev_vol is not None:
hour = candle.CloseTime.Hour
start_h = self._start_hour.Value
end_h = self._end_hour.Value
in_session = hour >= start_h and hour <= end_h
if in_session:
if self._prev_vol > self._prev_prev_vol * 1.1 and self.Position <= 0:
vol_to_trade = self.Volume + (abs(self.Position) if self.Position < 0 else 0)
if vol_to_trade > 0:
self.BuyMarket(vol_to_trade)
elif self._prev_vol < self._prev_prev_vol * 0.9 and self.Position >= 0:
vol_to_trade = self.Volume + (abs(self.Position) if self.Position > 0 else 0)
if vol_to_trade > 0:
self.SellMarket(vol_to_trade)
self._prev_prev_vol = self._prev_vol
self._prev_vol = vol
def CreateClone(self):
return volume_trader_strategy()