Ver en GitHub

Distribuidores comerciales v7.51 RIVOT (C#)

Resumen

Dealers Trade v7.51 es una estrategia de cuadrícula estilo martingala que se entregó originalmente como el MetaTrader 4 asesor experto Dealers_Trade_v_7.51_RIVOT.mq4. El puerto mantiene la idea original de operar lejos de un sesgo direccional basado en pivotes, escalando hacia el lado dominante cada vez que el precio retrocede una distancia de pip configurable. La implementación StockSharp utiliza ayudas estratégicas de alto nivel para suscribirse a velas, calcular las zonas de pivote y gestionar el tamaño, el riesgo y las salidas de las posiciones.

Lógica de trading

  1. Marco pivote

    • La estrategia construye dos precios de referencia para cada vela terminada:
      • Pivote clásico (P) = (previous high + previous low + previous close + current open) / 4.
      • Pivote flotante (FLP) = (current high + current low + current close) / 3.
    • Una brecha en pips entre P y FLP debe ser mayor o igual a GapThreshold para permitir el comercio de la barra actual.
  2. Sesgo direccional

    • Cuando el cierre de la vela está por encima de ambos pivotes y el filtro de brecha se satisface, el sesgo cambia a largo.
    • Cuando el cierre de la vela está por debajo de ambos pivotes con la brecha confirmada, el sesgo cambia a corto.
    • El sesgo permanece vigente hasta que la serie de posiciones está completamente cerrada o aparece la condición opuesta después de que finaliza la serie.
  3. Entradas de escala

    • Sólo puede haber una serie de operaciones activas a la vez.
    • La primera entrada sigue inmediatamente el sesgo.
    • Las entradas adicionales se abren solo cuando el precio retrocede contra el sesgo activo en al menos PipDistance pips desde el llenado más reciente, emulando el promedio de martingala original.
    • Cada nuevo pedido multiplica el tamaño anterior por VolumeMultiplier pero nunca excede MaxVolume.
    • El número de entradas apiladas está limitado por MaxTrades.
  4. Controles de riesgo

    • Un stop-loss estricto a StopLoss pips desde la entrada promedio ponderada por volumen cierra toda la serie.
    • Una toma de ganancias fija en TakeProfit pips bloquea las ganancias una vez que el precio vuelve a ser favorable.
    • Cuando está habilitado, el trailing-stop bloquea dinámicamente las ganancias acercándose al precio cada vez que se mueve más de TrailingStop pips más allá de la entrada promedio.
  5. Restablecer condiciones

    • Cualquier salida completa (stop-loss, take-profit, trailing-stop o aplanamiento manual de posición) restablece los contadores martingala y elimina el sesgo direccional.

Parámetros

Parámetro Predeterminado Descripción
Volume 1 Tamaño de pedido base para la primera entrada.
MaxTrades 5 Número máximo de entradas promediadas por serie.
PipDistance 4 Movimiento adverso mínimo (en pips) requerido antes de agregar una nueva posición.
TakeProfit 15 Distancia desde la entrada promedio ponderada por volumen para cerrar toda la red con ganancias.
StopLoss 90 Distancia desde la entrada media que desencadena una salida protectora.
TrailingStop 15 La compensación de trailing-stop se aplica una vez que el precio se mueve a favor; establezca en cero para desactivar el seguimiento.
VolumeMultiplier 1.5 Factor utilizado para aumentar el tamaño del pedido para cada entrada posterior.
MaxVolume 5 Límite para el volumen de un solo pedido después de aplicar el multiplicador.
GapThreshold 7 Espacio mínimo (en pips) entre los pivotes clásico y flotante requerido para activar el sesgo.
CandleType Velas con marco de tiempo de 15 minutos Tipo de vela utilizada para cálculos y toma de decisiones.

Todos los parámetros se configuran a través de StrategyParam<T> para que puedan optimizarse dentro de StockSharp Designer o Strategy Runner.

Notas de uso

  • La estrategia se basa únicamente en datos de velas; no se requiere ningún flujo directo de oferta/demanda a nivel de tick. Asegúrese de que su proveedor de datos pueda entregar el CandleType seleccionado.
  • Debido a que StockSharp agrega posiciones de forma predeterminada, la implementación mantiene un promedio interno ponderado por volumen para emular el libro de cuadrícula MT4. Si se producen llenados parciales, la contabilidad de posiciones incorporada mantiene los valores consistentes.
  • La representación del gráfico agrega dos líneas horizontales (Pivot y FloatingPivot) al área del gráfico cuando está disponible.
  • No existe comercio inverso automático; el sistema espera a que termine la serie en curso antes de aceptar un cambio de sesgo.

Diferencias con la versión MQL

  • El guión original dibujó múltiples etiquetas y comentarios en el gráfico MT4. El puerto mantiene solo la lógica comercial funcional y reemplaza las imágenes con StockSharp líneas del gráfico.
  • Las funciones de protección de cuentas basadas en el total de órdenes abiertas, el filtrado manual de números mágicos y las tablas de valores de pips de símbolos específicos no son obligatorias en StockSharp y se omitieron.
  • El cierre de la orden a precios de tick exactos (Ask == tp) en el código MetaTrader se aproxima con comparaciones de precios en los cierres de velas.
  • La gestión comercial se implementa con órdenes de mercado (BuyMarket/SellMarket) en lugar de bucles de tickets MT4. Las paradas y salidas dinámicas ocurren en las actualizaciones de velas.

Mejores prácticas

  • Pruebe siempre la estrategia en operaciones en papel o simulaciones históricas con modelos realistas de diferenciales/comisiones antes de ponerlas en funcionamiento.
  • Considere reducir VolumeMultiplier o MaxTrades en instrumentos altamente volátiles para controlar la reducción.
  • Para productos intradiarios, ajuste CandleType para que coincida con la granularidad de datos de la configuración original (el valor predeterminado es 15 minutos, pero EA se usó con frecuencia en M15 y H1).

Archivos

  • CS/DealersTradeV751RivotStrategy.cs – Implementación principal de C#.
  • README_zh.md – Documentación china.
  • README_ru.md – Documentación rusa.
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;
	}
}