Ver no GitHub

Volume Trader Strategy

Overview

  • 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

  1. Subscribes to candles defined by CandleType (default: 1-hour time frame) and reads their tick volume (TotalVolume).
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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;
	}
}