GitHub で見る

Volume Trader V2 Strategy

Overview

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

  1. Handle only finished candles to guarantee bar-by-bar parity with the EA.
  2. Cache Volume[1] and Volume[2] equivalents in _previousVolume and _twoBarsAgoVolume before any signal evaluation.
  3. 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.
  4. 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.
  5. If the desired direction differs from the current position, close the opposite position first (BuyMarket(-Position) or SellMarket(Position)).
  6. Enter the new position using the configured TradeVolume only when the strategy is flat or positioned in the opposite direction.
  7. 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();
		}
	}
}