Открыть на GitHub

Стратегия Volume Trader V2

Обзор

Volume Trader V2 — прямой порт советника MetaTrader Volume_trader_v2_www_forex-instruments_info.mq4. Оригинальная версия отслеживает, как меняется суммарный объём последних свечей, и на основе этого потока удерживает позицию в одну сторону. При переносе сохранены ключевые особенности: работа только с одной позицией, фильтр по времени и обработка сигнала один раз на каждую завершённую свечу.

Стратегия подписывается на настраиваемый поток свечей и кеширует объёмы двух последних завершённых баров. Когда формируется новая свеча, объёмы предыдущих двух баров (в терминах MetaTrader — Volume[1] и Volume[2]) сравниваются и определяется актуальное направление торговли:

  • Volume[1] < Volume[2] — формируется покупка.
  • Volume[1] > Volume[2] — формируется продажа.
  • Равные объёмы или отключённые торговые часы приводят к закрытию позиции.

Перед отправкой нового приказа стратегия закрывает противоположную позицию, чтобы поведение в StockSharp полностью совпадало с жизненным циклом ордеров в MetaTrader.

Параметры

Имя Значение по умолчанию Описание
CandleType Таймфрейм 5 минут Тип данных, запрашиваемый через SubscribeCandles. Настройте его под период исходного графика в MetaTrader.
StartHour 8 Начальный час (включительно), когда разрешено торговать. Вне этого окна сигналы игнорируются, позиция закрывается.
EndHour 20 Последний час (включительно), когда разрешено торговать. Если свеча начинается позже, стратегия остаётся вне рынка.
TradeVolume 0.1 Лотность, унаследованная от советника. Значение также присваивается Strategy.Volume, чтобы вспомогательные методы использовали тот же объём.

Все параметры реализованы через StrategyParam<T>, поэтому их можно оптимизировать и отображать в пользовательском интерфейсе.

Логика торговли

  1. Обрабатываются только завершённые свечи — это гарантирует посвечный паритет с исходным советником.
  2. Перед расчётом сигнала в переменные _previousVolume и _twoBarsAgoVolume сохраняются значения Volume[1] и Volume[2].
  3. Проверяется, что время открытия свечи попадает в диапазон StartHourEndHour включительно. Если условие не выполнено, позиция закрывается и новые заявки не создаются.
  4. Определяется целевое направление:
    • Лонг, если объём последней свечи меньше объёма предыдущей.
    • Шорт, если объём последней свечи больше объёма предыдущей.
    • Нейтрально во всех остальных случаях.
  5. Если целевое направление отличается от текущей позиции, сначала закрывается противоположная позиция (BuyMarket(-Position) или SellMarket(Position)).
  6. Новая заявка с объёмом TradeVolume отправляется только тогда, когда стратегия находится вне рынка или сразу после закрытия обратной позиции.
  7. После обработки обновляются кешированные объёмы, чтобы в следующем цикле снова сравнивались две последние завершённые свечи.

Такой порядок действий исключает появление заявок на незавершённых свечах и обеспечивает ту же частоту принятия решений, что и MetaTrader-версия с переменной LastBarChecked.

Дополнительные замечания

  • В OnStarted вызывается StartProtection(), чтобы использовать встроенный механизм защиты позиции.
  • Свойство Comment выводит те же диагностические сообщения, что и советник ("Up trend", "Down trend", "No trend...", "Trading paused"), что упрощает мониторинг.
  • Стратегия не создаёт дополнительных коллекций и полностью опирается на высокоуровневое API подписки на свечи, что соответствует требованиям проекта.
  • Для воспроизведения результатов оригинала настройте тип свечей, инструмент и объём в соответствии с параметрами, использовавшимися в MetaTrader.
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();
		}
	}
}