Открыть на GitHub

Стратегия Volume Trader

Общее описание

  • Порт эксперта MetaTrader 5 «Volume trader» (ID 21050) автора Владимир Карпутов.
  • Переписана на высокоуровневом API StockSharp без изменения базовой логики.
  • Ориентирована на работу по направлению изменения тикового объёма внутри заданного торгового окна.

Логика работы

  1. Подписывается на свечи типа CandleType (по умолчанию таймфрейм 1 час) и считывает их тиковый объём TotalVolume.
  2. После закрытия каждой свечи сравниваются объёмы двух предыдущих закрытых баров, что повторяет логику MQL5, запускаемую в момент рождения новой свечи.
  3. Если последний объём выше, чем объём бара до него, и нет длинной позиции, стратегия покупает Volume контрактов и одновременно закрывает существующий шорт.
  4. Если последний объём ниже, чем объём бара до него, и нет короткой позиции, стратегия продаёт Volume контрактов и одновременно закрывает существующий лонг.
  5. Сигналы игнорируются, если время открытия следующей свечи выходит за пределы интервала [StartHour, EndHour]. Значения 09:00–18:00 повторяют исходные настройки.
  6. Стоп-лосс и тейк-профит по умолчанию отсутствуют — позиции разворачиваются при появлении противоположного сигнала.

Управление заявками

  • Вход выполняется рыночными приказами через BuyMarket или SellMarket, что позволяет немедленно развернуть позицию на открытии новой свечи.
  • При смене направления стратегия автоматически торгует объём, равный модулю текущей позиции плюс Volume, полностью закрывая старую позицию перед открытием новой.
  • Дополнительных правил мани-менеджмента, помимо фиксированного параметра Volume, не предусмотрено.

Параметры

Параметр Значение по умолчанию Описание
CandleType Таймфрейм 1 час Свечная серия для расчёта тикового объёма. Настройте под используемый таймфрейм в MetaTrader.
StartHour 9 Начало торговой сессии (включительно), часы 0–23. Сигналы раньше этого времени игнорируются.
EndHour 18 Завершение торговой сессии (включительно), часы 0–23. Сигналы позже этого времени игнорируются.
Volume 0.1 Объём заявки для новых входов и переворота позиции.

Рекомендации по использованию

  • Убедитесь, что источник данных передаёт тиковый объём в сообщениях свечей. При отсутствии тиковых данных стратегия будет опираться на фактический торговый объём.
  • Приведите параметр CandleType к таймфрейму, на котором тестировалась оригинальная MQL-версия.
  • При необходимости добавьте внешние ограничения по рискам (стоп-лосс, тейк-профит, дневной лимит потерь).
  • В журнал (LogInfo) выводятся сообщения при открытии позиций, что упрощает анализ сигналов.

Отличия от оригинала на MQL5

  • Используется механизм подписки на свечи StockSharp вместо явного вызова CopyTickVolume.
  • Фильтр по времени рассчитывается через CloseTime завершённой свечи, что соответствует проверке времени открытия следующего бара в исходном советнике.
  • Заявки отправляются через высокоуровневые методы BuyMarket/SellMarket вместо прямого обращения к CTrade.
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;
	}
}