Открыть на GitHub

Dealers Trade v7.51 RIVOT (C#)

Краткое описание

Dealers Trade v7.51 — это советник MetaTrader 4 (Dealers_Trade_v_7.51_RIVOT.mq4), основанный на сетке с элементами мартингейла. Портированная версия в StockSharp повторяет исходную концепцию: направление задаётся классическим и плавающим пивотом, а каждый неблагоприятный откат на заданное число пунктов приводит к добавлению позиции с увеличенным объёмом. Риск контролируется стоп-лоссом, тейк-профитом и опциональным трейлинг-стопом.

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

  1. Пивоты

    • Для каждой завершённой свечи рассчитываются две опорные цены:
      • Классический пивот P = (High_{prev} + Low_{prev} + Close_{prev} + Open_{curr}) / 4;
      • Плавающий пивот FLP = (High_{curr} + Low_{curr} + Close_{curr}) / 3.
    • Торговля разрешается только при разнице между пивотами не меньше GapThreshold пунктов.
  2. Определение направления

    • Если закрытие свечи выше обоих пивотов, активируется лонговый сценарий;
    • Если закрытие ниже обоих пивотов — шортовый;
    • Направление сохраняется, пока текущая серия сделок не будет полностью закрыта.
  3. Добавление позиций

    • Одновременно ведётся только одна серия усреднений;
    • Первая сделка открывается сразу после подтверждения направления;
    • Следующие сделки добавляются, когда цена двигается против позиции минимум на PipDistance пунктов от последнего входа;
    • Объём каждой новой сделки умножается на VolumeMultiplier, но не превышает MaxVolume;
    • Общее количество сделок в серии ограничено MaxTrades.
  4. Управление рисками

    • Стоп-лосс закрывает всю серию при движении против позиции на StopLoss пунктов от средневзвешенной цены входа;
    • Тейк-профит забирает прибыль при движении в нужную сторону на TakeProfit пунктов;
    • При включённом трейлинг-стопе фиксируется лучшая цена и подтягивается защитный уровень на расстоянии TrailingStop пунктов.
  5. Сброс состояния

    • Любое полное закрытие позиции (стоп, тейк, трейлинг или ручное) очищает счётчики и снимает направление.

Параметры

Параметр Значение по умолчанию Назначение
Volume 1 Базовый объём первой сделки.
MaxTrades 5 Максимум сделок в одной серии.
PipDistance 4 Минимальный откат против позиции для следующего входа.
TakeProfit 15 Тейк-профит от средневзвешенной цены входа.
StopLoss 90 Стоп-лосс от средневзвешенной цены входа.
TrailingStop 15 Шаг трейлинг-стопа; 0 — отключено.
VolumeMultiplier 1.5 Множитель объёма при усреднении.
MaxVolume 5 Верхний предел объёма отдельной сделки.
GapThreshold 7 Минимальная разница между пивотами для активации торговли.
CandleType Свечи M15 Тип свечей, используемых для расчётов.

Все параметры описаны через StrategyParam<T>, поэтому легко оптимизируются в Designer и других модулях StockSharp.

Практические рекомендации

  • Для работы стратегии достаточно свечных данных, поток заявок или тиков не требуется, но важно, чтобы провайдер поставлял выбранный таймфрейм.
  • StockSharp использует агрегированную позицию, поэтому код хранит внутреннюю средневзвешенную цену, чтобы повторить поведение MT4 с несколькими ордерами.
  • При наличии графика стратегия рисует две линии (Pivot и FloatingPivot) для визуального контроля уровней.
  • Новое направление оценивается только после завершения текущей серии сделок.

Отличия от версии MT4

  • В MT4 советник выводил текст и объекты на графике; в StockSharp оставлена только функциональная часть, визуализация заменена линиями.
  • Логика «защиты счёта» (учёт магических номеров, вручную заданных стоимостей пункта и т. п.) опущена как нерелевантная для StockSharp.
  • Проверка условий Ask == tp заменена на сравнение с ценой свечи; управление позициями происходит рыночными ордерами при обработке свечей.

Файлы

  • CS/DealersTradeV751RivotStrategy.cs — C#-реализация стратегии.
  • README.md — подробное описание на английском языке.
  • README_zh.md — документация на китайском языке.
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>
/// Dealers Trade v7.51 strategy ported from MetaTrader 4 implementation.
/// Builds directional bias from classic pivot and floating pivot levels
/// and scales into the bias when price retraces by a fixed pip distance.
/// Applies martingale-style position sizing with configurable stop-loss,
/// take-profit, and trailing-stop management.
/// </summary>
public class DealersTradeV751RivotStrategy : Strategy
{
	private readonly StrategyParam<int> _maxTrades;
	private readonly StrategyParam<decimal> _pipDistance;
	private readonly StrategyParam<decimal> _takeProfit;
	private readonly StrategyParam<decimal> _stopLoss;
	private readonly StrategyParam<decimal> _trailingStop;
	private readonly StrategyParam<decimal> _volumeMultiplier;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<decimal> _gapThreshold;
	private readonly StrategyParam<DataType> _candleType;

	private ICandleMessage _previousCandle;
	private decimal _pivotLevel;
	private decimal _floatingPivot;
	private decimal _gapInPips;
	private decimal _lastEntryPrice;
	private decimal _averageEntryPrice;
	private decimal? _trailingStopLevel;
	private int _direction; // -1 short, 0 neutral, 1 long
	private int _entriesInSeries;

	/// <summary>
	/// Maximum number of entries allowed in one scaling series.
	/// </summary>
	public int MaxTrades
	{
		get => _maxTrades.Value;
		set => _maxTrades.Value = value;
	}

	/// <summary>
	/// Distance in pips between martingale entries.
	/// </summary>
	public decimal PipDistance
	{
		get => _pipDistance.Value;
		set => _pipDistance.Value = value;
	}

	/// <summary>
	/// Take-profit distance in pips.
	/// </summary>
	public decimal TakeProfit
	{
		get => _takeProfit.Value;
		set => _takeProfit.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in pips.
	/// </summary>
	public decimal StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}

	/// <summary>
	/// Trailing-stop distance in pips.
	/// </summary>
	public decimal TrailingStop
	{
		get => _trailingStop.Value;
		set => _trailingStop.Value = value;
	}

	/// <summary>
	/// Multiplier applied to volume for each additional entry.
	/// </summary>
	public decimal VolumeMultiplier
	{
		get => _volumeMultiplier.Value;
		set => _volumeMultiplier.Value = value;
	}

	/// <summary>
	/// Maximum allowed volume for a single entry.
	/// </summary>
	public decimal MaxVolume
	{
		get => _maxVolume.Value;
		set => _maxVolume.Value = value;
	}

	/// <summary>
	/// Minimum pivot gap in pips required to activate the bias.
	/// </summary>
	public decimal GapThreshold
	{
		get => _gapThreshold.Value;
		set => _gapThreshold.Value = value;
	}

	/// <summary>
	/// Type of candles used for pivot calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public DealersTradeV751RivotStrategy()
	{
		_maxTrades = Param(nameof(MaxTrades), 2)
		.SetGreaterThanZero()
		.SetDisplay("Max Trades", "Maximum number of martingale entries", "Position Sizing")
		
		.SetOptimize(1, 10, 1);

		_pipDistance = Param(nameof(PipDistance), 10m)
		.SetGreaterThanZero()
		.SetDisplay("Pip Distance", "Distance between averaged entries in pips", "Position Sizing")
		
		.SetOptimize(2m, 15m, 1m);

		_takeProfit = Param(nameof(TakeProfit), 15m)
		.SetGreaterThanZero()
		.SetDisplay("Take Profit", "Take-profit distance in pips", "Risk Management")
		
		.SetOptimize(5m, 50m, 5m);

		_stopLoss = Param(nameof(StopLoss), 90m)
		.SetGreaterThanZero()
		.SetDisplay("Stop Loss", "Stop-loss distance in pips", "Risk Management")
		
		.SetOptimize(30m, 200m, 10m);

		_trailingStop = Param(nameof(TrailingStop), 15m)
		.SetGreaterThanZero()
		.SetDisplay("Trailing Stop", "Trailing-stop distance in pips", "Risk Management")
		
		.SetOptimize(5m, 40m, 5m);

		_volumeMultiplier = Param(nameof(VolumeMultiplier), 1.5m)
		.SetGreaterThanZero()
		.SetDisplay("Volume Multiplier", "Multiplier applied after each new entry", "Position Sizing")
		
		.SetOptimize(1.1m, 3m, 0.1m);

		_maxVolume = Param(nameof(MaxVolume), 5m)
		.SetGreaterThanZero()
		.SetDisplay("Max Volume", "Upper limit for single-entry volume", "Position Sizing");

		_gapThreshold = Param(nameof(GapThreshold), 15m)
		.SetGreaterThanZero()
		.SetDisplay("Gap Threshold", "Minimal pivot gap required to enable trading", "Signal")
		
		.SetOptimize(3m, 15m, 1m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Type of candles used for pivot calculations", "Signal");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType)];

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		ResetSeries();
		_previousCandle = null;
		_pivotLevel = 0m;
		_floatingPivot = 0m;
		_gapInPips = 0m;
	}

	/// <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);
		}
	}

	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);

		if (Position == 0m)
		{
			// Reset martingale state once the position is closed externally.
			ResetSeries();
		}
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
		return;

		if (_previousCandle == null)
		{
			_previousCandle = candle;
			return;
		}

		UpdatePivots(candle);

		if (Position == 0m && _entriesInSeries > 0)
		{
			// Force reset when no exposure remains but scaling data still exists.
			ResetSeries();
		}

		if (_entriesInSeries > 0)
		{
			ManageRisk(candle.ClosePrice);
		}

		if (_entriesInSeries >= MaxTrades)
		{
			_previousCandle = candle;
			return;
		}

		if (_direction == 0)
		{
			EvaluateDirection(candle);
		}

		TryEnter(candle);

		_previousCandle = candle;
	}

	private void UpdatePivots(ICandleMessage candle)
	{
		var step = GetPriceStep();
		_pivotLevel = (_previousCandle!.HighPrice + _previousCandle.LowPrice + _previousCandle.ClosePrice + candle.OpenPrice) / 4m;
		_floatingPivot = (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m;
		_gapInPips = step == 0m ? 0m : Math.Abs(_pivotLevel - _floatingPivot) / step;
	}

	private void EvaluateDirection(ICandleMessage candle)
	{
		var price = candle.ClosePrice;

		if (price > _pivotLevel && price > _floatingPivot && _gapInPips >= GapThreshold)
		{
			_direction = 1;
			LogInfo($"Bias switched to long. Pivot={_pivotLevel:F5}, Floating={_floatingPivot:F5}, Gap={_gapInPips:F2} pips.");
		}
		else if (price < _pivotLevel && price < _floatingPivot && _gapInPips >= GapThreshold)
		{
			_direction = -1;
			LogInfo($"Bias switched to short. Pivot={_pivotLevel:F5}, Floating={_floatingPivot:F5}, Gap={_gapInPips:F2} pips.");
		}
	}

	private void TryEnter(ICandleMessage candle)
	{
		if (_direction == 0)
		return;

		var price = candle.ClosePrice;
		var step = GetPriceStep();
		var distance = PipDistance * step;

		if (_direction > 0)
		{
			if (_entriesInSeries == 0 || (_lastEntryPrice - price) >= distance)
			{
				EnterLong(price);
			}
		}
		else
		{
			if (_entriesInSeries == 0 || (price - _lastEntryPrice) >= distance)
			{
				EnterShort(price);
			}
		}
	}

	private void EnterLong(decimal price)
	{
		var volume = CalculateNextVolume();
		_lastEntryPrice = price;
		_averageEntryPrice = UpdateAveragePrice(price, volume, true);
		_entriesInSeries++;
		LogInfo($"Opening long entry #{_entriesInSeries} at {price:F5} with volume {volume}.");
		BuyMarket(volume);
	}

	private void EnterShort(decimal price)
	{
		var volume = CalculateNextVolume();
		_lastEntryPrice = price;
		_averageEntryPrice = UpdateAveragePrice(price, volume, false);
		_entriesInSeries++;
		LogInfo($"Opening short entry #{_entriesInSeries} at {price:F5} with volume {volume}.");
		SellMarket(volume);
	}

	private decimal CalculateNextVolume()
	{
		var volume = Volume;

		for (var i = 0; i < _entriesInSeries; i++)
		{
			volume *= VolumeMultiplier;
			if (volume >= MaxVolume)
			{
				volume = MaxVolume;
				break;
			}
		}

		var volumeStep = Security?.VolumeStep ?? 0.01m;
		if (volumeStep > 0m)
		{
			volume = Math.Ceiling(volume / volumeStep) * volumeStep;
		}

		return volume;
	}

	private decimal UpdateAveragePrice(decimal price, decimal volume, bool isLong)
	{
		var existingVolume = Math.Abs(Position);
		var side = isLong ? 1m : -1m;

		if (existingVolume <= 0m)
		{
			return price;
		}

		var totalVolume = existingVolume + volume;
		var weightedAverage = ((_averageEntryPrice * existingVolume * side) + (price * volume)) / totalVolume;
		return Math.Abs(weightedAverage);
	}

	private void ManageRisk(decimal price)
	{
		if (_entriesInSeries == 0)
		{
			_trailingStopLevel = null;
			return;
		}

		var step = GetPriceStep();
		var stopDistance = StopLoss * step;
		var takeDistance = TakeProfit * step;
		var trailingDistance = TrailingStop * step;

		if (_direction > 0)
		{
			var lossLevel = _averageEntryPrice - stopDistance;
			var profitLevel = _averageEntryPrice + takeDistance;

			if (price <= lossLevel)
			{
				LogInfo($"Long stop-loss triggered at {price:F5}. Average entry {_averageEntryPrice:F5}.");
				SellMarket(Math.Abs(Position));
				ResetSeries();
				return;
			}

			if (price >= profitLevel)
			{
				LogInfo($"Long take-profit triggered at {price:F5}. Average entry {_averageEntryPrice:F5}.");
				SellMarket(Math.Abs(Position));
				ResetSeries();
				return;
			}

			if (TrailingStop > 0m)
			{
				var candidate = price - trailingDistance;
				if (_trailingStopLevel == null || candidate > _trailingStopLevel)
				{
					_trailingStopLevel = candidate;
				}

				if (_trailingStopLevel != null && price <= _trailingStopLevel)
				{
					LogInfo($"Long trailing stop activated at {price:F5}.");
					SellMarket(Math.Abs(Position));
					ResetSeries();
				}
			}
		}
		else if (_direction < 0)
		{
			var lossLevel = _averageEntryPrice + stopDistance;
			var profitLevel = _averageEntryPrice - takeDistance;

			if (price >= lossLevel)
			{
				LogInfo($"Short stop-loss triggered at {price:F5}. Average entry {_averageEntryPrice:F5}.");
				BuyMarket(Math.Abs(Position));
				ResetSeries();
				return;
			}

			if (price <= profitLevel)
			{
				LogInfo($"Short take-profit triggered at {price:F5}. Average entry {_averageEntryPrice:F5}.");
				BuyMarket(Math.Abs(Position));
				ResetSeries();
				return;
			}

			if (TrailingStop > 0m)
			{
				var candidate = price + trailingDistance;
				if (_trailingStopLevel == null || candidate < _trailingStopLevel)
				{
					_trailingStopLevel = candidate;
				}

				if (_trailingStopLevel != null && price >= _trailingStopLevel)
				{
					LogInfo($"Short trailing stop activated at {price:F5}.");
					BuyMarket(Math.Abs(Position));
					ResetSeries();
				}
			}
		}
	}

	private decimal GetPriceStep()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step == 0m)
		{
			// Fallback to four decimal places when instrument metadata is unknown.
			step = 0.0001m;
		}
		return step;
	}

	private void ResetSeries()
	{
		_direction = 0;
		_entriesInSeries = 0;
		_lastEntryPrice = 0m;
		_averageEntryPrice = 0m;
		_trailingStopLevel = null;
	}
}